Compare commits

..
Author SHA1 Message Date
Tabish Bidiwale 2500d6da97 fix(view): keep archived changes off the dashboard (#2031)
* fix(view): keep archived changes off the dashboard

openspec view is a one-screen dashboard for a person reading a terminal.
#399 added every archived change to it, so projects with hundreds of
archived changes pushed active work off the screen (#2030). The dashboard
shows current work again; `openspec list --archived` still shows history.

To catch this class of mistake earlier, the cli-view spec now states who
the command serves and that it shows current work only, view.ts says the
same where the code lives, and CONTRIBUTING asks how a human view grows
as a project ages before anything is added to it.

* docs(view): describe archive exclusion without promising a screen height

* docs(view): keep internal rationale out of the user reference

The CLI reference describes what view prints, so it goes back to its
pre-#399 text. The why lives in the cli-view spec Purpose, the code
comment points there, and the CONTRIBUTING rule no longer names a PR.

* revert: drop bug-specific guardrails

The CONTRIBUTING section, the cli-view spec requirement, and the view.ts
comment each restated this one bug instead of guarding the general
mistake. The regression test stays as the guardrail.
2026-10-02 17:34:46 +00:00
Tabish Bidiwale bfa670eda9 perf(cli): load each command's implementation only when it runs (#2025)
* perf(cli): load each command's implementation only when it runs

src/cli/index.ts statically imported every command module, so every
invocation, even `openspec --version`, loaded 485 modules (zod, yaml,
fast-glob, ora, diff and every command) before commander ran. Callers
that run the CLI many times, such as editors and agents, paid for that on
each call, most on Windows where Node loads modules slowly.

Command definitions (names, options, help) stay eager; implementations
move behind `await import()` in their actions, the pattern `init` already
used. The `register*Command` modules that mixed both are split: the
definitions live in src/cli/commands/, and each action body moves
unchanged into an exported function in src/commands/. Telemetry, the
completion tip, ora in failWithError, and the config profile's drift
check and update load on demand too.

`--version` and `--help` now load 24 modules (commander and the
definitions); median wall time on macOS drops from ~150 ms to ~33 ms
(`node -e 0` is 18 ms). Help for every command, completion scripts,
exit codes, `--json` output, error messages and telemetry events are
byte-identical before and after.

test/cli-e2e/startup-modules.test.ts runs the built CLI with a
module-recording hook and asserts that `--version` and `--help` load no
package but commander and no command implementation, and that a command
loads only its own implementation. It fails on main.

* docs(contributing): keep the CLI's startup fast

* test(cli): compare loaded modules against the real dist/ path

Node reports loaded modules by their real paths, so a checkout reached
through a symlink or a Windows short name made every module look foreign
and the absence checks pass without checking anything. Resolve dist/ with
realpathSync.native, fail when no CLI module was recorded, and require
--version and --help to exit 0.
2026-10-02 04:38:30 +00:00
Clay GoodandClaude Opus 5.5 760584ba9a fix(validate): fail --strict on requirements over the length limit (#2020)
* fix(validate): fail --strict on requirements over the length limit

A requirement description over 500 characters was an INFO finding, so
`openspec validate --all --strict` still exited 0 and CI could not hold
the limit. It is now a WARNING: normal validation and archive still pass,
while strict mode fails, the same split the SHALL/MUST keyword warning
already uses. The specs instruction and its docs page say so.

Closes #1976

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(validate): check ADDED requirement length and document splitting

With the length finding now failing --strict, a change could still add an
overlong requirement and pass `validate <change> --strict`; CI only went red
after archive merged it into the main spec. ADDED requirements now get the
same warning, using the shared body reader so the limit matches the main
spec exactly. MODIFIED is left alone, since its text is the existing
requirement the instruction says to keep whole.

The specs instruction (and its docs-lab page) now says how to split an
existing long requirement in a dedicated change: keep the MODIFIED header and
every scenario, cut the description to one behavior, and add each removed
behavior as its own ADDED requirement. Verified end to end: the split change
validates strict, archives, and the main spec then passes --strict.

Closes the rest of #1976.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 20:42:19 +00:00
ba0f508763 fix(archive): report retirement cleanup failures accurately (#1792)
* fix(archive): report retirement cleanup failures accurately

* test(archive): canonicalize recovery fixture paths

* test(archive): deny staged-root removal after main's verified-tree cleanup

Main now removes a fallback-copy staged source entry by entry and deletes
the staged root last, so the recovery-path test no longer reached its
fs.rm injection. Deny the final rmdir of the staged root instead, which
still leaves a staged source behind for the diagnostic to report.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Clay Good <hi@claygood.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 20:42:16 +00:00
7056a58056 fix(tasks): separate completion from archive readiness (#1791)
* fix(tasks): separate completion from archive readiness

* fix(tasks): clarify optional workflow follow-up

* fix(tasks): keep profile-aware archive handoff and sync generated surfaces

Rebased onto main: the apply template now reports tracked completion
through the existing optional-workflow ARCHIVE_HANDOFF (#1775), so
profiles without the archive workflow still get the CLI fallback.
Regenerates the skills/ mirror and parity hashes, and syncs the
docs-lab spec-driven reference with the updated tasks instruction (#1952).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Clay Good <hi@claygood.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 20:42:13 +00:00
Clay GoodandClaude Opus 5.5 3a34ea309d fix(website): pin brace-expansion and fast-uri past new advisories (#2019)
Security's docs-site audit went red on main after four advisories landed
in the site's dev-only serve dependency chain. Raise the site's existing
overrides so they resolve patched versions.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 23:24:14 +00:00
openspec-release-bot[bot]andgithub-actions[bot] 94ca9c1eb1 Version Packages (#2005)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-09-30 22:39:18 +00:00
Clay GoodandClaude Opus 5.5 81c2f9fce3 chore(changeset): track apply, archive and view fixes for 1.14.0 (#2018)
#1994, #1759 and #1987 merged without changesets, so the 1.14.0 release
notes would omit them.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 22:11:42 +00:00
JinandClaude Opus 5.5 cd4f9e4a5f fix(completion): restore .zshrc byte for byte on zsh uninstall (#2016)
removeZshrcConfig stripped every blank line at the top of .zshrc after
removing the OpenSpec block, so a file that started with blank lines
lost them in an install/uninstall round trip, even when the user had
moved the block further down. Drop only the separator line install
added, and only when the block sits at the top, as #1872 did for bash.

Closes #2015

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 21:22:12 +00:00
Clay GoodandClaude Opus 5.5 cf2859a520 fix(status): include the declaring repo in store-backed edit roots (#2014)
* fix(status): include the declaring repo in store-backed edit roots

For a store-selected root, actionContext.allowedEditRoots listed only the
store and claimed implementation edits were scoped to it, so the apply
skill reported a conflict and refused to implement tasks. The project whose
store: pointer names the store is now listed first; with no declaring
project, the constraint asks the agent to confirm the target repo instead.
Repo-local output is byte-identical.

Closes #2013

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(status): keep actionContext key order and scope the store wording

Adversarial review of the #2013 fix found:

- The editScope spread moved `constraints` ahead of
  `requiresAffectedAreaSelection`, changing repo-local JSON key order.
  toEqual could not see it; the byte-stable test now pins the serialized
  form, and the keys are written out in contract order.
- "Implementation edits belong to <repo>" claimed task routing OpenSpec
  does not do (a store change can span repos). It now names the current
  declaring project and asks before editing any other repository.
- The no-declaring-project text was false when a pointer exists but is
  ignored, and did not say the user's answer is where edits go.

New tests cover a subdirectory cwd, an ignored pointer on a real planning
root, and status --all.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(status): pin the declaring repo's canonical path through an alias

test/AGENTS.md asks for an alias-path regression when touching path
identity logic (raised by CodeRabbit). The new case reaches the declaring
repo through a symlink (a junction on Windows) and asserts the canonical
path, both through the CLI and through a direct findDeclaringProjectRoot
call whose start path keeps the alias spelling on every platform.

The helper's own canonicalizeExistingPath call was redundant: the root
walk already returns canonical paths, and removing it changes no output.
The alias test fails only when that walk stops resolving aliases.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 21:22:09 +00:00
c879d13d5f feat: add support for code studio AI-powered coding agent (#883)
* feat: add Code Studio tool integration

* feat: add Code Studio tool integration

* feat: add Code Studio tool integration

* feat: add Code Studio tool integration

---------

Co-authored-by: AshokkumarKaruppasamy <ashokkumar.karuppasamy@syncfusion.com>
Co-authored-by: Clay Good <hi@claygood.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 21:42:57 +00:00
e232080d09 feat(grok): add skills-only support for grok build (#1349)
* feat: add Grok Build skills-only support

* test(grok): cover skills-only installation and update lifecycle

---------

Co-authored-by: Clay Good <hi@claygood.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 21:42:53 +00:00
Clay GoodandClaude Opus 5 781c7f9447 feat(warp): add project skills support (#1738)
* feat(warp): add project skills support

* docs(warp): align integration contract and references

* docs(warp): remove changes to frozen legacy docs

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-29 20:24:15 +00:00
d1642cb58c feat(easycode): add project skills and commands (#1352)
* feat: add Easy Code as a supported AI tool

- Register 'easycode' in AI_TOOLS (config.ts) with skillsDir '.easycode'
- Add EasycodeAdapter (easycode.ts): generates TOML commands at
  .easycode/commands/opsx/<id>.toml, matching Easy Code's native format
- Export easycodeAdapter from adapters/index.ts
- Register easycodeAdapter in CommandAdapterRegistry

Easy Code (https://easycode.ai) is a terminal-based AI coding assistant.
Its commands use TOML with a description field and a prompt multiline
literal string, distinct from the Markdown/YAML frontmatter format used
by most other tools.

Tested locally: `openspec init --tools easycode` generates 5 SKILL.md
files and 5 .toml command files in the expected directory structure.

* fix: robust TOML serialization for Easy Code adapter

Per code review: the original formatFile had unsafe manual escaping
that would corrupt output for descriptions containing backslashes or
control characters, and prompt bodies containing triple-single-quotes.

Changes:
- Add src/core/command-generation/toml.ts with two helpers:
    escapeTOMLBasicString  — escapes \, ", \n, \r, \t for TOML
                              basic strings (double-quoted)
    escapeTOMLMultilineString — escapes \ and \r, and breaks any
                              run of 3+ consecutive " (lookahead match)
                              for TOML basic multiline strings
- Switch prompt block from triple-single-quote literal string (''')
  to triple-double-quote basic multiline string ("""), which allows
  full escape sequence support and handles arbitrary body content
- Update easycode.ts to use both helpers

* fix: escape disallowed control characters in TOML helpers

Per code review: TOML basic strings forbid U+0000-U+0008, U+000B-U+000C,
U+000E-U+001F, and U+007F. Add escapeControlChars() helper that replaces
these with \uXXXX sequences, and apply it in both escapeTOMLBasicString
and escapeTOMLMultilineString after their named-escape passes.

* feat(easycode): harden project skills and command integration

---------

Co-authored-by: Trae <konghaifeng@cmcm.com>
Co-authored-by: Clay Good <hi@claygood.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 20:24:09 +00:00
a7f08b8a46 feat(tools): add GSD skills support (#1082)
Co-authored-by: Clay Good <hi@claygood.com>
Co-authored-by: Storm-Chaser <Storm-Chaser@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 20:24:04 +00:00
f197804a38 feat(nix): expose openspec as a consumable overlay (#1439)
* feat(nix): expose openspec as a consumable overlay

Refactor the flake so the package derivation is defined once, in
`overlays.default`, and every other output consumes it. Previously the
derivation lived inline in `packages.default`, so anyone who wanted
`openspec` in their own package set had to copy the derivation rather than
import it.

What changed:
- Add `overlays.default`, a standard `final: _prev:` overlay that exposes
  `pkgs.openspec`. The derivation is written against `final`, so downstream
  overlay composition and `overrideAttrs` behave as expected.
- Route `packages.{default,openspec}` through the overlay via a `pkgsFor`
  helper (`import nixpkgs { overlays = [ self.overlays.default ]; }`), so the
  flake's own package resolves exactly as a consumer's would. No more
  duplicated build definition.
- Refresh the nixpkgs pin in `flake.lock`.

`apps` and `devShells` are unchanged in behaviour.

Usage — a downstream flake:

    nixpkgs.overlays = [ openspec.overlays.default ];
    # -> pkgs.openspec

or devenv, via `devenv.yaml`:

    inputs:
      openspec:
        url: github:Fission-AI/OpenSpec
        overlays:
          - default

Assisted-by: Claude Opus 4.8 <noreply@anthropic.com>

* test(nix): cover downstream overlay composition and updater warnings

* fix(ci): compare Nix dependencies across multiple outputs

* chore: add Nix overlay changeset

---------

Co-authored-by: John Muchovej <jmuchovej@users.noreply.github.com>
Co-authored-by: Clay Good <hi@claygood.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 20:23:57 +00:00
ded99e27de feat(cli): show archived changes in list and view (#399)
* feat: show archived changes in list and view commands

Add --archived and --all flags to the list command to display archived changes.
The view dashboard now includes an "Archived Changes" section with a count in
the summary.

- list --archived: shows only archived changes
- list --all: shows both active and archived changes
- view: displays archived changes section in dashboard

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(list): reject malformed archive parents on Windows

* fix(list): preserve browsing of archived symbolic links

* fix(list,view): keep archived entries off nested findings and render past a file archive

An archived change that shares a name with an active namespace folder was
reported as nested. view now treats an archive path that is a file as no
archived changes, as it did before, instead of failing mid-dashboard.
The archive symlink test now uses a portable link and checks the link itself.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Co-authored-by: Clay Good <hi@claygood.com>
2026-09-29 20:23:52 +00:00
Clay GoodandClaude Opus 5.5 772819417a fix(archive): stop sending the archive skill to an uninstalled sync skill (#1977)
* fix(archive): stop sending the archive skill to an uninstalled sync skill

The archive skill always told the agent to run the `openspec-sync-specs`
skill, even when that skill was not installed, so an agent following it
stalled at the sync step. The archive command already chose between the
sync workflow and an inline merge based on what was installed; the skill
now does the same.

Closes #1975

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(skills): document archive sync fallback

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 20:23:49 +00:00
Alfred 405d8b51ed chore: route docs-lab reviews to owner (#1754) 2026-09-29 19:30:37 +00:00
Clay GoodandClaude Opus 5 e923d05c8f feat(github): add issue forms and a PR template (#1847)
* feat(github): add issue forms and a PR template

CONTRIBUTING asks every change to start with an issue or a discussion, but
the repository had no `.github/ISSUE_TEMPLATE/` and no PR template, so a
contributor who clones the repo and opens a PR meets a blank box and, later,
a review comment asking them to go file the issue they did not know they
needed.

- `ISSUE_TEMPLATE/bug_report.yml` asks for expected, actual, a minimal repro,
  `openspec --version`, and the agent and model, and points users who already
  have OpenSpec installed at `openspec feedback`.
- `ISSUE_TEMPLATE/feature_request.yml` asks for the problem, who it affects,
  and what was tried, and routes core-design topics to Discussions.
- `ISSUE_TEMPLATE/config.yml` keeps blank issues enabled, so the pre-filled
  URL that `openspec feedback` prints when `gh` is unavailable still works,
  and links Discussions and Discord.
- `PULL_REQUEST_TEMPLATE.md` puts `Closes #` on the first line with the
  "no issue yet?" path directly under it, catching the PR-first contributor
  at the moment they need it — no bot and no CI gate.

Labels referenced by the forms (`bug`, `enhancement`, `needs-triage`) all
exist in this repository.

Part of #1834

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(github): clarify issue submission and verification guidance

* fix(github): point agent disclosure to notes section

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-29 18:26:09 +00:00
Clay Good de4141f0fd docs(schemas): document Superpowers community bridge (#2002) 2026-09-29 18:26:07 +00:00
MS8ATandClay Good ee3ca3821e docs(review): map requirements to verifiable evidence (#1937)
* docs(review): map requirements to verifiable evidence

Add an optional documentation recipe with fictional examples. Explain input and environment binding, evidence provenance, missing results, and reuse. No runtime changes. Related to #1652.

* docs(review): clarify evidence acceptance and reuse

* docs(review): simplify evidence guidance in docs-lab

* docs(review): make evidence checks easier to skim

---------

Co-authored-by: Clay Good <hi@claygood.com>
2026-09-29 18:26:05 +00:00
Clay Good 56528ea454 feat(cli): report version and update metadata (#2001)
* docs(openspec): propose version reporting command

* docs(openspec): clarify update guidance availability

* feat(cli): report version and update metadata

* fix(cli): harden version install detection
2026-09-29 18:26:04 +00:00
3de7c72c26 feat(atomcode): add project skills and commands (#1211)
* feat: add AtomCode command adapter support

Add AtomCode as a supported tool with its own command adapter.
AtomCode is an open-source terminal AI coding assistant that uses
the same Agent Skills spec as Claude Code.

Changes:
- New adapter: src/core/command-generation/adapters/atomcode.ts
  - File path: .atomcode/commands/opsx-<id>.md
  - Frontmatter: description only
  - Command references transformed from colon to hyphen format
- Register adapter in index.ts, registry.ts, and config.ts
- Update docs (supported-tools.md, cli.md) with AtomCode entry
- Add 6 test cases for atomcodeAdapter
- Add changeset for version tracking

Closes #1210

Co-Authored-By: hu-qi, AtomCode (GLM-5.1) <huqi1024@gmail.com>

* docs(atomcode): correct parser compatibility comments

* test(atomcode): cover detection and registry registration

The adapter shipped without the two per-tool checks its siblings carry:
a detection test asserting `.atomcode` surfaces the tool from
getAvailableTools, and registry assertions that the adapter is reachable
by id. Also adds the missing AtomCode row to the docs-lab support matrix.

Verified non-vacuous: removing the registry registration and the
AI_TOOLS entry fails all four new assertions.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* test(atomcode): lock the literal name/args invariant

AtomCode's custom-command loader scans for `key:` and takes the rest of
the line verbatim, with no YAML unquoting, then matches `args` against
the exact strings "required"/"optional". A quoted `args: "optional"`
falls through to ArgsRequirement::None and silently drops every
argument, so this adapter cannot use the shared escapeYamlValue helper
the way its siblings do.

That made the deviation look like an oversight inviting a "consistency"
refactor. Document why it exists and add a case proving a description
that needs quoting does not drag name/args into quotes.

Also drops the literal `description:` assertion. It passed only because
no current description needs quoting; the first one containing ": "
would have failed it, looking like an adapter bug rather than a
description edit. The parsed-frontmatter toEqual already covers it.

Verified non-vacuous: routing all three fields through a quoting helper
fails 13 tests, including the new one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(atomcode): declare args: none for workflows that take no input

Every generated command shipped `args: optional`, including `onboard`,
which reads no invocation input. AtomCode treats the two differently at
the slash menu: `none` executes the command immediately, while
`optional` completes to `/name ` and waits for a second Enter. Onboard
therefore cost every AtomCode user an extra keystroke to start, and its
body carried a `**Provided arguments**:` line that was always empty.

Gate both on the `**Input**:` contract the workflow bodies already
declare, matching how the Command Code adapter decides the same thing.

Output for the other 11 workflows is byte-identical; only `onboard`
changes. Adds the same tripwire assertion Command Code carries, so a
future workflow cannot silently lose its arguments.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs(atomcode): describe the args contract in the changeset

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs(atomcode): keep updates in docs-lab

---------

Co-authored-by: Clay Good <hi@claygood.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-29 18:26:02 +00:00
297092cb25 feat(tools): add DeepSeek Harness support (#1672)
* feat(tools): add DeepSeek Harness support

* docs(cli): clarify dsh tool id shorthand

* docs(changeset): clarify dsh skill invocations vs command files

* fix(tools): harden deepseek harness integration

---------

Co-authored-by: Clay Good <hi@claygood.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-29 18:26:00 +00:00
c21d897261 feat(view): display workflow status in active changes (#807)
* feat(view): display workflow status in active changes

Active changes now show workflow artifact status below each entry,
indicating schema name and completion state of each artifact (done✓,
ready→, blocked). Powered by loadChangeContext and formatChangeStatus.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>

* chore(release): track dashboard workflow status

* fix(view): neutralize controls in workflow output

* docs(view): move workflow status docs to docs-lab

* docs(view): preserve store in status guidance

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Clay Good <hi@claygood.com>
2026-09-29 18:25:57 +00:00
070de01dfa feat(init): add Amp skills support (#420)
* proposal: add Amp skill support for OpenSpec workflows

Amp-Thread-ID: https://ampcode.com/threads/T-019b6a90-6107-755b-8087-942d9b5460ac

* feat(init): add Amp skills support for OpenSpec workflows

Add AmpSlashCommandConfigurator that generates Amp-native skill files
at .agents/skills/openspec-{proposal,apply,archive}/SKILL.md with YAML
frontmatter containing name and description fields.

- Register Amp in the native tool picker for init and update commands
- Include comprehensive test coverage for init and update scenarios
- Mark all add-amp-support tasks as complete

Amp-Thread-ID: https://ampcode.com/threads/T-019b6a90-6107-755b-8087-942d9b5460ac

* proposal: rename SlashCommandConfigurator to WorkflowConfigurator

Fix semantic mismatch with diverse tool terminology (skills, prompts,
commands). Old names kept as deprecated aliases for compatibility.

Amp-Thread-ID: https://ampcode.com/threads/T-019b6a90-6107-755b-8087-942d9b5460ac

* feat(init): add Amp skills support

---------

Co-authored-by: Jean du Plessis <jean@upbound.io>
Co-authored-by: Clay Good <hi@claygood.com>
2026-09-29 18:25:55 +00:00
5a360c2088 feat(veai): add skills-only support (#848)
* feat: Add support for Veai coding agent

* chore(changeset): track Veai support

* docs(changeset): clarify Veai user impact

---------

Co-authored-by: TabishB <tabishbidiwale@gmail.com>
Co-authored-by: Clay Good <hi@claygood.com>
2026-09-29 18:25:53 +00:00
3c3e6e3d42 feat: add GigaCode as a supported --tools target (#1961)
* feat: add GigaCode as a supported --tools target

GigaCode is Sber's CLI coding agent, a fork of Qwen Code that reuses
its config directory shape (.gigacode/ vs .qwen/) and file formats.
Register a gigacodeAdapter mirroring the qwen adapter (Markdown
commands with a description frontmatter field at
.gigacode/commands/opsx-<id>.md), wire it into the config tool list
and command-adapter registry, and document it in
docs/supported-tools.md, docs/cli.md and docs-lab/reference/supported-tools.md.

Verified: GigaCode's fork relationship to Qwen Code and its .gigacode
config directory are confirmed by the task's own primary-source brief;
I could not independently find a GigaCode-authored doc page that
enumerates its custom-command file format (Markdown vs TOML), so this
mirrors Qwen Code's current (post-deprecation) Markdown spec on the
stated fork/format-compatibility basis. Flagging this assumption for
review.

Generated with Claude Code (claude-sonnet-5); tested with the full
vitest suite (5879 tests passing across 199 files), pnpm build,
tsc --noEmit, and eslint, all green.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(gigacode): use current documentation sources

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-authored-by: Clay Good <hi@claygood.com>
2026-09-29 18:25:51 +00:00
dependabot[bot] 486cfeb8ca chore(deps): bump the website-dependencies group (#1991)
Bumps the website-dependencies group in /website with 3 updates: [fumadocs-core](https://github.com/fuma-nama/fumadocs), [fumadocs-ui](https://github.com/fuma-nama/fumadocs) and [next](https://github.com/vercel/next.js).


Updates `fumadocs-core` from 16.15.11 to 16.15.14
- [Release notes](https://github.com/fuma-nama/fumadocs/releases)
- [Commits](https://github.com/fuma-nama/fumadocs/compare/fumadocs@16.15.11...fumadocs@16.15.14)

Updates `fumadocs-ui` from 16.15.11 to 16.15.14
- [Release notes](https://github.com/fuma-nama/fumadocs/releases)
- [Commits](https://github.com/fuma-nama/fumadocs/compare/fumadocs@16.15.11...fumadocs@16.15.14)

Updates `next` from 16.3.5 to 16.3.6
- [Release notes](https://github.com/vercel/next.js/releases)
- [Commits](https://github.com/vercel/next.js/compare/v16.3.5...v16.3.6)

---
updated-dependencies:
- dependency-name: fumadocs-core
  dependency-version: 16.15.14
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: website-dependencies
- dependency-name: fumadocs-ui
  dependency-version: 16.15.14
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: website-dependencies
- dependency-name: next
  dependency-version: 16.3.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: website-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-09-29 18:25:49 +00:00
dependabot[bot]andClay Good f7d426ab9d chore(deps-dev): bump the development-dependencies group with 2 updates (#1990)
* chore(deps-dev): bump the development-dependencies group with 2 updates

Bumps the development-dependencies group with 2 updates: [eslint](https://github.com/eslint/eslint) and [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint).


Updates `eslint` from 10.10.0 to 10.11.0
- [Release notes](https://github.com/eslint/eslint/releases)
- [Commits](https://github.com/eslint/eslint/compare/v10.10.0...v10.11.0)

Updates `typescript-eslint` from 8.70.0 to 8.70.1
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/typescript-eslint/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.70.1/packages/typescript-eslint)

---
updated-dependencies:
- dependency-name: eslint
  dependency-version: 10.11.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: development-dependencies
- dependency-name: typescript-eslint
  dependency-version: 8.70.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>

* ci(nix): keep dependabot on github cache

* chore(nix): update dependency hash

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Clay Good <hi@claygood.com>
2026-09-29 18:25:46 +00:00
Casey GollanandClay Good 817cdb64be fix(tools): identify Bob by product name (#1722)
* chore(openspec): add change for IBM Bob rename and skills-only

* feat: rename Bob Shell to IBM Bob and make skills-only

- Rename AI_TOOLS entry from 'Bob Shell' to 'IBM Bob' (name + successLabel)
- Remove bobAdapter from CommandAdapterRegistry and adapters/index.ts
- Add 'bob' to skills-invocable capability path in command-surface.ts
- Add cleanupLegacyBobCommandFiles() to migration.ts for .bob/commands/ cleanup
- Call cleanup from init.ts and update.ts after skills are generated
- Update docs/supported-tools.md: IBM Bob, mark commands as not generated
- Remove bobAdapter tests; update flat-invocation and all-adapters lists

* chore: delete dead bob command adapter file

* fix: address CodeRabbit review comments on Bob command cleanup

- Guard bobCommandsDir and each commandFile against symlink escape
  using areProjectArtifacts() before deletion
- Only remove files containing 'argument-hint:' frontmatter (OpenSpec
  marker), leaving user-authored files with matching names intact
- Run cleanupLegacyBobCommandFiles on the up-to-date early-return
  path in update.ts so stale .bob/commands/ files are cleaned even
  when no tools need a version update

* fix: scope argument-hint check to YAML frontmatter block only

* fix(tools): identify Bob by product name

* chore(changeset): track IBM Bob naming fix

* docs(changeset): clarify IBM Bob user impact

---------

Co-authored-by: Clay Good <hi@claygood.com>
2026-09-29 18:25:45 +00:00
88692b3bb4 fix(change-metadata): warn on unrecognized .openspec.yaml keys (#1925)
* fix(change-metadata): warn on unrecognized .openspec.yaml keys

Unknown keys such as skip_design were stripped with no signal, so status
still demanded design and validate --strict exited 0. Warn on the shared
status/validate/archive read path without rejecting the file.

Closes #1920

AI-assisted (Grok)

* fix(change-metadata): sanitize unknown keys before they are printed

A quoted YAML key can carry a terminal control sequence, and the warning
printed it as it was written. The listed keys now go through
sanitizeInline, which also flattens C1 controls from now on.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* test(change-metadata): cover sanitized archive warnings

* fix(change-metadata): keep warnings safe and structured

* fix(change-metadata): label key names as untrusted

* test(change-metadata): keep known keys in step with the schema

CHANGE_METADATA_KNOWN_KEYS is a hand-kept copy of ChangeMetadataSchema's
keys. A key added to the schema but not the list would warn on, and fail
validate --strict for, every change that uses it. Pin the two together.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Co-authored-by: Clay Good <hi@claygood.com>
2026-09-29 18:25:42 +00:00
openspec-cloud[bot]andClay Good 5fe58590ba docs(openspec): align requirements with current behavior (#1919)
* docs(openspec): correct 4 requirements that the code has outgrown

- cli-artifact-workflow/experimental-isolation#8: Updated the file path in the Single file implementation scenario to match the actual command location (src/commands/workflow/status.ts).
- cli-init/exit-codes#7: Updated the exit code for user-cancelled operations from 3 to 130 to match implementation.
- openspec-conventions/header-based-requirement-identification#6: Updated the normalization rule to remove a trailing run of '#' characters and surrounding spaces before trimming.
- cli-config/profile-configuration-flow#9: Changed the first action option label from 'Change delivery + workflows' to 'Delivery and workflows' to match the code.

None of these reduce what a requirement demands.

Scanned at bae58cf614 by openai/gpt-5-mini-2025-08-07.

* docs(openspec): harden drift corrections

* docs(openspec): clarify header normalization

---------

Co-authored-by: openspec-cloud[bot] <311461291+openspec-cloud[bot]@users.noreply.github.com>
Co-authored-by: Clay Good <hi@claygood.com>
2026-09-29 18:25:40 +00:00
47thandClay Good 9557b43aaf fix(init): install integrations with external stores (#1969)
* fix(init): install integrations with external stores

* Canonicalize pointer root and skip config writes in integrations-only mode

* test(init): cover external-store integration choices

* docs(init): document store-only integration setup

* fix(init): preserve pointer-repo planning files

---------

Co-authored-by: Clay Good <hi@claygood.com>
2026-09-29 18:25:36 +00:00
Ryan de Melo d28fb49c1c fix(show): include requirement and scenario names in JSON (#1972)
show --json described each requirement as its SHALL sentence and each
scenario as its bullets. The parser read both headers and dropped them,
so a JSON reader could not name a requirement the way archive matches it.

Requirements and scenarios now carry a name, normalized by the same
helpers archive and the MODIFIED scenario loss check use. The field is
additive and optional in the schema.

Closes #1971
2026-09-29 18:25:33 +00:00
Clay Good bda85565ef fix(init): guide project.md migration (#1999)
* feat(init): offer project.md migration

* fix(config): preserve context newline state

* test(init): prove migration preserves files

* docs(setup): document project.md migration

* fix(init): guide project.md migration

* fix(init): cover migration destinations
2026-09-29 18:25:31 +00:00
nhconganhmediaandClay Good 28f864351b docs(community): add MySpec to showcase (#1974)
* docs: add MySpec to complementary SDD tools

* docs(community): move MySpec to showcase

* docs(community): note MySpec data handling

---------

Co-authored-by: Clay Good <hi@claygood.com>
2026-09-29 18:25:29 +00:00
Clay Good 9a40b58928 docs(archive): document retention options (#1998) 2026-09-29 18:25:27 +00:00
Clay Good baad4494b4 fix(config): clarify project context guidance (#1995)
* fix(config): clarify project context guidance

Generated config comments and canonical docs now steer agents toward constraints that shape OpenSpec artifacts and away from discoverable codebase facts.\n\nPart of #1966

* test(config): cover generated context examples

* docs(config): harden context examples

* docs(config): correct context scope
2026-09-29 18:25:26 +00:00
Clay Good e70dcc7c82 fix(propose): guide capability naming (#1997)
* fix(propose): guide capability naming

* test(propose): cover published naming guidance
2026-09-29 18:25:24 +00:00
Clay GoodandClaude Opus 5.5 187298289d fix(schemas): tell agents the requirement length limit (#1978)
* fix(schemas): tell agents the requirement length limit

The validator flags requirement text over 500 characters, but the specs
instruction never mentioned the limit, so agents kept writing requirements
that tripped it. State the limit and how to split, and make the validator
message say how to fix it.

Part of #1976

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(schemas): match requirement length boundary

* test(validation): lock requirement length boundary

* fix(schemas): keep MODIFIED requirements whole under the length hint

The 500-character check is an INFO hint, not an error. Splitting or trimming
an existing requirement under MODIFIED drops scenarios the main spec still
has, which validate and archive reject. Say so, and scope the splitting
advice to new requirements.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 18:25:22 +00:00
42671df890 fix(config): name the offending rules item when a list is malformed (#1984)
* fix(config): name the offending rules item when a list is malformed

A rule item containing an unquoted ": " is valid-looking YAML but parses as
a mapping, so the artifact's whole rule set is dropped. The warning named only
the artifact, so the bad item had to be found by bisecting the list by hand.

Name every offending index with the shape YAML produced there, and point at the
quoting fix when a mapping is the cause. Parsing behavior is unchanged.

* test(config): cover remaining malformed rule shapes

---------

Co-authored-by: Yi-111-a <41823681+Yi-111-a@users.noreply.github.com>
Co-authored-by: Clay Good <hi@claygood.com>
2026-09-29 18:25:21 +00:00
Clay Good e7a9512d7c fix(apply): include task source locations (#1994)
* fix(apply): include task source locations

* fix(apply): verify task locations before updates

* docs(apply): document task source locations
2026-09-29 18:25:18 +00:00
fffe3d3850 fix(view): align Active Changes progress bars for long names (#1987)
* fix(view): align Active Changes progress bars for long names

* fix(view): cap the aligned name column at 48 characters

Pad names to the longest one only up to 48 characters, so rows with a
20-block bar and percentage still fit in 80 columns (#1986). Longer
names stay whole and start their own bar later.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(view): keep progress alignment scoped to changes

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Clay Good <hi@claygood.com>
2026-09-29 18:25:16 +00:00
0ff63dbae6 fix(archive): require successful spec sync before archiving (#1759)
* Updated Archive Skill

* Updated Testcase

* Updated Skill and Updated Unit Test

* Updated Archive and Bulk Archive Skill

* Updated Bulk Archive Skill

* fix(archive): keep existing main spec titles during sync verification

The post-sync structure check required every main spec to start with a
'# <capability> Specification' title. Neither sync-specs nor the validator
enforces that, and valid specs (including this repo's opsx-archive-skill)
use other titles, so an agent could stop the archive or rewrite a
user's title. Scope the title and heading rules to what the sync writes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(specs): match opsx-archive-skill spec to the stricter sync check

The archive skill now treats a sync that reports a blocked retirement as a
failed sync, so the capability spec no longer calls that kept spec verified.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Clay Good <hi@claygood.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 18:25:14 +00:00
Clay Goodand胥寅 d4e1c77eba fix(cleanup): clarify legacy file deletion warning (#2004)
Adapt #1820 to current cleanup behavior and docs-lab; cover directory, file, and marker-only summaries.

Co-authored-by: 胥寅 <xuuyin@dingtalk.com>
2026-09-28 21:46:37 +00:00
Clay GoodandClaude Opus 5.5 79b6aa9c98 test: stop two Windows subprocess tests timing out at 10s (#1981)
* test(flake): give the bash-spawning scope test a 60s timeout

The Windows runner took 13.1s to spawn bash three times on the Version
Packages push to main, tripping the 10s default. The same test ran in
0.3s and 4.2s on the two previous main runs; nothing in the code changed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(e2e): give the git-clone init test a 60s timeout

Timed out at the 10s default on windows-pwsh three times (#1953 merge
queue, two changeset-release runs); it normally takes ~2.6s there.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 17:12:24 +00:00
openspec-release-bot[bot]andgithub-actions[bot] db23097835 Version Packages (#1953)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-09-23 21:33:22 +00:00
Clay GoodandClaude Opus 5.5 7ac58dc790 chore(changeset): track Kilo Code and Continue fixes for 1.13.2 (#1964)
* chore(changeset): track Kilo Code and Continue fixes for 1.13.2

#1938 and #1944 merged without changesets, so their user-visible
fixes would be missing from the 1.13.2 release notes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* chore(changeset): describe Kilo cleanup by file name

Cleanup deletes the known legacy file names without checking content, so
an edited copy is removed too; drop the claim that user files are kept.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-23 20:56:54 +00:00
336414665f fix(verify): stop reporting removed requirements as missing (#1962)
* fix(verify): stop reporting removed requirements as missing

Verify walked every "### Requirement:" in the delta specs and looked for
an implementation of each one, whatever section it sat under. A REMOVED
requirement that the change had removed correctly came back as CRITICAL
"Requirement not found", with a recommendation to implement it. An agent
that follows the report puts back the behavior the change just deleted.

Verify now notes the delta section of each requirement first. ADDED and
MODIFIED keep the existing checks. REMOVED is checked the other way
round: finding nothing is the expected result, and it is only critical
while the behavior is still in the code. RENAMED only changes a name, so
the old name is not reported as missing. Scenario coverage skips removed
requirements, since there is nothing left to cover.

Archive and sync already handle each section on its own terms; verify
was the one step in the loop that did not.

Closes #1959

* docs(specs): scope the general verify scenarios to ADDED and MODIFIED

The Spec coverage, Requirement implementation mapping and Scenario
coverage scenarios still told the verifier to check every requirement
in the delta specs, which contradicts the Removed requirement scenario
added in the previous commit. A verifier following them would repeat
the #1959 failure.

* fix(verify): report a removal-only change as ready when nothing remains

With #1732 merged, a change whose delta specs only remove or rename
requirements left Requirement Implementation Mapping and Scenario
Coverage with nothing to check. The "no usable requirements" rule then
marked them not verified, so verify never reported the change ready,
the exact case #1959 describes. Those two checks are now not applicable
when the readable delta specs hold REMOVED or RENAMED requirements and
no ADDED or MODIFIED ones. An empty or unparseable delta still marks
them not verified.

Keyword matches in openspec/ artifacts, docs, or code that serves only
the Migration note or an ADDED requirement are no longer evidence by
themselves that a removed requirement is still implemented; a code path
that still delivers the removed behavior is reported even when shared.
The summary counts removals separately from covered requirements.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(verify): check a renamed requirement's behavior against its baseline

A RENAMED entry only told verify not to report the FROM name as
missing, and a rename-only change marked the correctness checks not
applicable. Nothing checked that the renamed requirement's behavior was
still implemented, so verify could report readiness unchecked.

Spec Coverage now reads the baseline requirement from the main spec
(under the FROM name, or the TO name once synced) and checks that its
behavior is still implemented, without requiring code symbols to be
renamed. A missing behavior is CRITICAL "Renamed requirement not
found"; an unreadable baseline marks the entry not verified. A TO name
that also appears under MODIFIED is still checked there.

Regression tests cover the skill template, the command template, and
the committed skills/ mirror.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Clay Good <hi@claygood.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-23 19:28:55 +00:00
072de6bc39 fix(verify): do not report unverified dimensions as passing (#1732)
* fix(verify): do not report unverified dimensions as passing

Step 5 gates task and spec coverage on `contextFiles.tasks` and
`contextFiles.specs`. `contextFiles` is an artifact-id map and artifact
ids come from the active schema, so on a schema that defines neither, both
branches are no-ops: nothing is checked, no issues are raised, and step 8
concludes "All checks passed. Ready for archive."

The Graceful Degradation guardrail already asks the agent to note skipped
checks, but nothing stopped the all-clear verdict. Mark an unchecked
dimension `Not verified` in the scorecard and require the final assessment
to name it.

* fix(verify): map skipped checks to report outcomes

* fix(verify): retain no-task and skipped-check context

* fix(verify): harden evidence gaps and final assessments

* fix(verify): preserve optional workflows and task artifact fallback

* fix(apply): resolve tracked task globs by schema path

* fix(verify): preserve unavailable task evidence

* fix(verify): distinguish untracked tasks from missing evidence

* docs(apply): document tracked globs and JSON evidence

* test(parity): regenerate hashes after merging #1940

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* test(parity): restore the #1837 regression tests dropped in the merge

The earlier conflict resolution took our whole side of the parity file,
which discarded the two threshold tests main gained in #1940. Take main's
file verbatim and regenerate the hashes instead.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* test(parity): regenerate hashes after merging #1955

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* test(parity): regenerate hashes after merging #1795 and #1926

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* test(parity): regenerate hashes after merging #1731

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Clay Good <hi@claygood.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-23 17:38:11 +00:00
d6bdef6577 fix(workflows): stop reading schema from list output (#1731)
* fix(workflows): resolve the change picker's schema label from status

The update and continue templates tell the agent to read a `schema` field
from `openspec list --json` and fall back to "spec-driven" when it is
absent. `list --json` returns only `name`, `completedTasks`,
`totalTasks`, `lastModified`, and `status` (docs/agent-contract.md 4.1),
so the field is never present and the fallback fires every time: a change on
a custom schema is shown to the user as `spec-driven`.

Make the schema line optional and, when shown, resolve it from
`openspec status --change "<name>" --json` (`schemaName`).

* fix(workflows): align list prompts with JSON fields

* test(workflows): verify list and status schema contracts

* fix(workflows): keep bulk archive sync available in custom profiles

* test(parity): regenerate hashes after merging #1940

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* test(parity): restore the #1837 regression tests dropped in the merge

The earlier conflict resolution took our whole side of the parity file,
which discarded the two threshold tests main gained in #1940. Take main's
file verbatim and regenerate the hashes instead.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* test(parity): regenerate hashes after merging #1955

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* test(parity): regenerate hashes after merging #1733

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* test(archive-progress): resolve optional-workflow blocks before generating

This change makes the bulk-archive surfaces conditional on the sync
workflow being installed. #1795's task-progress test built them from the
raw templates, which leaves the [[opsx:if-workflow ...]] markers in the
text and makes skill generation throw.

Build both surfaces through getSkillTemplates/getCommandTemplates, which
resolve the blocks against an installed set, and name sync in that set so
the assertions keep testing the wording they were written for.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Clay Good <hi@claygood.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-23 16:59:45 +00:00
fb1b87613b fix(archive): use schema-aware task progress in workflows (#1795)
* fix(archive): use schema-aware task progress in workflows

* test(archive): verify task lookup follows the selected store

* fix(archive): reject invalid task progress in workflows

* test(parity): regenerate hashes after merging #1940

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* test(parity): restore the #1837 regression tests dropped in the merge

The earlier conflict resolution took our whole side of the parity file,
which discarded the two threshold tests main gained in #1940. Take main's
file verbatim and regenerate the hashes instead.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* test(parity): regenerate hashes after merging #1955

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Clay Good <hi@claygood.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-23 16:25:32 +00:00
f2812f6d18 fix(archive): copy without staging when Windows EPERM blocks rename (#1926)
* fix(archive): copy without staging when Windows EPERM blocks rename

fs.rename of a non-leaf change directory fails with EPERM on Windows
when a watcher holds a handle. The fallback required a staging rename
of the same directory, so it never ran: specs were rolled back after
printing success, and a newly created capability was left as an empty
folder git cannot see. Copy from the original source when staging also
fails with EPERM/EXDEV, and prune empty capability dirs on rollback.

Closes #1895

AI-assisted (Grok)

* fix(archive): bound the unstaged cleanup to what it verified

Addresses both review findings on the copy fallback.

The staging rename was what claimed the source before it was deleted.
Falling back without it means copy-then-remove now runs against the live
change directory, which the archive claim does not cover, and a recursive
remove deletes whatever is there at that moment - including a file
written after the final fingerprint, which never reached the destination.

Cleanup now removes a named set: the entries listed after the last
verification, deepest first. A later arrival is not in that set, so it is
never deleted, and the rmdir of its parent fails with ENOTEMPTY, which
the caller already reports as a retained destination. The move fails
loudly rather than completing with data missing.

Rollback of a created spec pruned the capability directory unconditionally,
which also removed one the user already had, along with its mode and ACLs.
The snapshot now records whether that parent existed, and only a directory
this write created is pruned.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(archive): close the listing window and the nested prune boundary

Both follow-ups from CodeRabbit's second pass, and both are right.

The removal listing was taken after the final fingerprint, which left the
window it was meant to close: a file arriving between the fingerprint and
the listing landed in the set and was deleted, having never reached the
destination. Listing before the verification makes the two orderings
exhaustive - an arrival either changes the fingerprint and aborts the
move, or is absent from the set and survives. The one case this cannot
cover, an edit to an already-listed file, is now stated in the comment.

`parentExisted` only described the target's direct parent, so a nested
capability id whose intermediate directory already existed still lost it:
the prune walked to the specs root. The snapshot now records the deepest
pre-existing ancestor and passes it as the prune boundary, which
pruneEmptyDirs never removes. That one mechanism covers the flat case too.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(archive): claim each entry before removing it in the unstaged fallback

An editor could rewrite an already-verified file between the final
fingerprint and its removal. The destination held the older bytes, the
newer ones were deleted with the source, and archive reported success.

Cleanup now renames each entry to a private claim name before reading it.
rename is atomic, so a rewrite that lands after the claim creates a new
file at the original path, which is not in the verified set, is never
deleted, and makes the parent rmdir fail. A rewrite that lands first is
caught by comparing the claimed entry against the copy, which puts the
file back and abandons the move with both trees intact.

Symlinks are compared by their target rather than by reading them, since
a link to a directory is not a directory entry and reading one is EISDIR.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(archive): draw the cleanup claim suffix per move

A fixed '.openspec-claim' suffix collided with a source file that
legitimately ends in it: claiming 'collision' renamed it over a real
'collision.openspec-claim', and that file's own turn then failed with
ENOENT after part of the live source had already been removed. A valid
tree could not archive, and its source was damaged for nothing.

The suffix is now drawn per move and checked against the entries being
removed, so no claim of one entry can land on another.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Clay Good <hi@claygood.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-23 16:11:13 +00:00
72fbe4c904 fix(update): close the dead end for partially populated glob artifacts (#1733)
* fix(update): close the dead end for partially populated glob artifacts

`artifactOutputExists` returns true as soon as a single file matches a glob
`generates`, so a `specs/**/*.md` artifact is `done` after the first
delta spec. `/opsx:continue` selects only `ready` artifacts and never
revisits it.

The update templates forbid creating new files under a glob artifact and
point the user to `/opsx:continue` instead, which cannot act on it. A
capability spec the coherence review finds missing therefore has no
supported way to be created.

Allow update to write that file: concrete path only, rules fetched from
`openspec instructions`, and the same confirm-before-write rule as every
other revision. Creating an artifact that has no files at all stays out of
scope - that one is genuinely `/opsx:continue`'s job.

* fix(update): tighten glob gap write guardrails

* fix(update): narrow continue handoff to empty artifacts

* fix(update): harden glob gap creation guidance

* fix(update): preserve creation safeguards for glob companions

* fix(update): integrate glob guidance with current workflows

* docs(skills): note update's glob companion-file exception

docs-lab/reference/skills.md said update creates nothing new, which this
change makes false for glob artifacts.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* test(parity): restore the #1837 regression tests dropped in the merge

The earlier conflict resolution took our whole side of the parity file,
which discarded the two threshold tests main gained in #1940. Take main's
file verbatim and regenerate the hashes instead.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* test(explore-docs): allow the update guidance line that says "no files yet"

main's #1833 guard flags any docs line containing "no files". The new
/opsx:update guidance describes which artifacts update leaves to
continue, not what explore writes, so allow that exact line.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Clay Good <hi@claygood.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-23 15:55:52 +00:00
Clay GoodandClaude Opus 5 ed5d386a55 fix(tasks): keep tests and docs inside each task group (#1955)
* fix(tasks): keep tests and docs inside each task group

Closes #1952

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(tasks): carry the per-group rule to onboarding and the docs

Teach the same rule where a user first meets task groups, and stop the
published schema reference from quoting instruction text that drifted two
revisions behind schema.yaml.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(tasks): scope the per-group rule to the work each group does

The MUST read as an absolute per-group requirement while the worked
example's Setup group carries neither tests nor docs. Scope the rule to
what a group's work calls for and name the scaffolding case explicitly,
so the rule and its example agree.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-23 15:04:37 +00:00
1d35e90880 fix(windows): preserve a file's existing line endings on rewrite (#1958)
* fix(windows): preserve a file's existing line endings on rewrite

The parsers normalize CRLF to LF on read, but nothing restored it on
write. On a Windows checkout (core.autocrlf=true) that turned every
rewrite into a whole-file change: applying a delta that added one
requirement produced a diff of 21 insertions and 14 deletions, burying
the real change. Archiving the same spec now writes 7 insertions and 0
deletions.

- specs-apply: write an updated spec back with the convention the file
  already used; a spec that does not exist yet stays LF.
- file-system: same fix for updateFileWithMarkers, so installing shell
  completions into a CRLF .bashrc/.zshrc no longer leaves mixed endings,
  which bash reports as "$'\r': command not found".
- pack-version-check: spawn npm through cross-spawn, since execFile
  cannot resolve npm.cmd on Windows.

Adds src/utils/line-endings.ts for the detect/restore pair, plus tests
pinning the CRLF round trip through the real write paths. Also adds
regression tests for path containment under Windows case variance:
path.win32.relative already folds case, and those tests pin both halves
of the contract so a future "case-insensitive" change cannot quietly
loosen the traversal guard.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(windows): keep removeMarkerBlock on the file's own newline

Addresses the two review points and one more instance of the same bug.

`removeMarkerBlock` collapses a run of blank lines, and rebuilt the
separator as a bare '\n' regardless of the file it came from. Removing a
managed block from a CRLF CLAUDE.md or rc file therefore left a lone LF
behind - the mixed ending this PR exists to prevent. It now uses the
newline it already detects for the trailing ending.

Test fixes:

- `marker-updates.test.ts`: close `describe('line endings')` so
  `removeMarkerBlock` is no longer nested inside `updateFileWithMarkers`.
- `path-containment.test.ts`: exercise `FileSystemUtils.assertPathWithin`
  and `resolveProjectArtifactPath` instead of a private copy of the
  containment logic, which passed whatever the production guard did. The
  guard had no coverage at all; a prefix-comparison regression now fails
  the sibling case.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(windows): read the file's convention consistently, and only ENOENT as absent

Three follow-ups from CodeRabbit's pass on the superseding PR.

`writeUpdatedSpec` turned every read error into "no previous file", so an
existing but unreadable spec was treated as absent and rewritten as LF.
Only ENOENT means absent now; everything else propagates.

`removeMarkerBlock` chose CRLF whenever the content held one anywhere, so
a single stray CRLF in an otherwise-LF file pulled the whole rewrite to
CRLF. It now uses detectLineEnding, the same dominant-ending reading
matchLineEnding uses, so both write paths agree.

Added the alias-path case the containment suite was missing: a directory
link inside the root that resolves outside it. That exercises the
canonicalization half of the guard, which a lexical check cannot do - the
link's own path looks contained. Skipped where creating a directory link
needs a privilege the runner lacks.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Travis James <travis@tribehealthsolutions.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-23 14:30:24 +00:00
8826c0c4a1 fix(validate): require marker punctuation after a leading TBD/TODO (#1912)
* fix(validate): require marker punctuation after a leading TBD/TODO

Closes #1897

* fix(validate): keep a shouted TODO a placeholder marker

Requiring marker punctuation after a leading TBD/TODO fixed the Spanish
and Portuguese false positive, but it also stopped reporting the plainest
unwritten Purpose there is: `TODO write this once the capability settles
down.`

Case is what actually separates the marker from the word. In capitals it
is the marker whatever follows it. In any other case it is a marker only
when punctuation or the end of the line says so, which is how the
lowercase forms an agent leaves behind are written (`todo - `, `tbd.`)
and is not how a Spanish sentence opens.

Covers `todo el ...` in lowercase too, which the capitals-only reading of
the original fix would have reported.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs(changeset): drop the trailing space inside a code span

markdownlint MD038. Changeset text only; no behaviour change.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Clay Good <hi@claygood.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-23 14:30:07 +00:00
Clay GoodandClaude Opus 5 f179ed4e40 chore(deps): bump @inquirer/core to 12.0.0 and @changesets/cli to 3.0.3 (#1954)
* chore(deps): bump @inquirer/core to 12.0.0 and @changesets/cli to 3.0.3

Consolidates Dependabot #1930 and #1931 into one PR so the flake.nix
pnpmDeps hash is computed once against the final lockfile.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* chore(nix): refresh pnpmDeps hash for the new lockfile

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 23:35:21 +00:00
1515edbbbd docs(community): add openspec-guard (#1845)
* docs(community): add openspec-guard

A CLI and GitHub Action that reports which OpenSpec scenarios are covered by a
Vitest or Jest test, without running the tests.

Closes #1844.

* docs(community): update spec-guard link

---------

Co-authored-by: Clay Good <hi@claygood.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 23:35:18 +00:00
Clay Good 02d8c243c4 docs(troubleshooting): explain missing workflow commands (#1941)
* docs(troubleshooting): explain missing workflow commands

* test(docs): guard command troubleshooting guidance

* docs(setup): move workflow recovery to docs lab

* test(docs): scope core workflow assertions
2026-09-22 23:35:16 +00:00
fd56e12c9e fix(artifact-graph): support brace expansion and extglob output patterns (#1885)
* fix(artifact-graph): support brace expansion and extglob output patterns (#1854)

* fix(artifact-graph): preserve literal output filenames

* fix(artifact-graph): confine expanded brace patterns

* test(artifact-graph): cover later brace ranges and specify glob contract

* test(artifact-graph): resolve brace globs with Windows separators

---------

Co-authored-by: 胥寅 <xuuyin@dingtalk.com>
Co-authored-by: Clay Good <hi@claygood.com>
2026-09-22 18:09:53 +00:00
Clay Good 0b5ce44b55 fix(templates): align approval thresholds (#1940)
* fix(templates): align approval thresholds

* fix(onboard): preserve implementation choice

* test(onboard): reject premature implementation prompt
2026-09-22 17:30:19 +00:00
dependabot[bot]andClay Good fe81461d51 chore(deps): bump the production-dependencies group with 2 updates (#1929)
* chore(deps): bump the production-dependencies group with 2 updates

Bumps the production-dependencies group with 2 updates: [yaml](https://github.com/eemeli/yaml) and [zod](https://github.com/colinhacks/zod).


Updates `yaml` from 2.9.0 to 2.9.1
- [Release notes](https://github.com/eemeli/yaml/releases)
- [Commits](https://github.com/eemeli/yaml/compare/v2.9.0...v2.9.1)

Updates `zod` from 4.5.4 to 4.6.5
- [Release notes](https://github.com/colinhacks/zod/releases)
- [Commits](https://github.com/colinhacks/zod/compare/v4.5.4...v4.6.5)

---
updated-dependencies:
- dependency-name: yaml
  dependency-version: 2.9.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: production-dependencies
- dependency-name: zod
  dependency-version: 4.6.5
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: production-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>

* chore(nix): update pnpm dependency hash

Matches the production dependency lockfile update and fixes Nix Flake Validation.

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Clay Good <hi@claygood.com>
2026-09-22 17:30:17 +00:00
dependabot[bot]andClay Good 2a8500a849 chore(deps): bump the website-dependencies group across 1 directory with 3 updates (#1932)
* chore(deps): bump the website-dependencies group across 1 directory with 3 updates

Bumps the website-dependencies group with 3 updates in the /website directory: [fumadocs-core](https://github.com/fuma-nama/fumadocs), [fumadocs-ui](https://github.com/fuma-nama/fumadocs) and [next](https://github.com/vercel/next.js).


Updates `fumadocs-core` from 16.15.5 to 16.15.11
- [Release notes](https://github.com/fuma-nama/fumadocs/releases)
- [Commits](https://github.com/fuma-nama/fumadocs/compare/fumadocs@16.15.5...fumadocs@16.15.11)

Updates `fumadocs-ui` from 16.15.5 to 16.15.11
- [Release notes](https://github.com/fuma-nama/fumadocs/releases)
- [Commits](https://github.com/fuma-nama/fumadocs/compare/fumadocs@16.15.5...fumadocs@16.15.11)

Updates `next` from 16.3.4 to 16.3.5
- [Release notes](https://github.com/vercel/next.js/releases)
- [Commits](https://github.com/vercel/next.js/compare/v16.3.4...v16.3.5)

---
updated-dependencies:
- dependency-name: fumadocs-core
  dependency-version: 16.15.11
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: website-dependencies
- dependency-name: fumadocs-ui
  dependency-version: 16.15.11
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: website-dependencies
- dependency-name: next
  dependency-version: 16.3.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: website-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>

* fix(website): await llms index generation

Adapts the llms.txt route to the asynchronous Fumadocs 16.15.11 API and restores the production build.

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Clay Good <hi@claygood.com>
2026-09-22 17:30:15 +00:00
VeryComplexAndLongName 1d2f8f2b75 OpenSpec-UI -> OpenSpec Workbench (#1934) 2026-09-22 17:29:11 +00:00
Clay Good fe429a13dc fix(kilocode): generate commands in canonical directory (#1938)
* fix(kilocode): generate commands in canonical directory

* fix(kilocode): preserve unrelated legacy workflows
2026-09-22 17:29:09 +00:00
Javier GomezandClay Good 5b55263775 fix(init): clarify Codex desktop skill usage (#1744)
* fix(init): clarify Codex desktop skill usage

* fix(init): clarify Codex desktop skill usage

* test(init): cover Codex hints across delivery modes

---------

Co-authored-by: Clay Good <hi@claygood.com>
2026-09-22 17:29:07 +00:00
Ryan de MeloandClay Good a5ceea32cf fix(validate): report what a MODIFIED block adds, not only what it drops (#1809)
* fix(validate): report what a MODIFIED block adds, not only what it drops

When the scenario-loss guard fires it names the scenarios the block omits,
which is the whole message. A reader's next question is what the block put
there instead, and answering it separates the two cases the guard cannot:
a block that omits two names and introduces two is shaped like a rename,
one that omits two and introduces none is shaped like a truncation. Today
that costs opening both files.

Add the counts and the introduced names to the message. This decides
nothing. Intent is not recoverable from structure, the guard fires exactly
as before, and the exit code is unchanged.

The comparison already walked one direction, so the other is the same pass
run the other way. Both directions now come from diffScenarioNames, and
both commands print one shared sentence, so archive and validate cannot
drift on what they report any more than they can on what they catch.
findMissingCurrentScenarios stays as its missing half.

Also names the antecedent in validate's fix instruction, which became
ambiguous once a second list of scenarios appeared before it.

* chore(changeset): track the scenario balance in the loss guard message

* fix(validate): clarify scenario balance diagnostic

* test(validate): cover one-to-two scenario replacement

---------

Co-authored-by: Clay Good <hi@claygood.com>
2026-09-22 17:29:05 +00:00
d3d770736f fix: release archive lock on Windows (#1769)
* fix: release archive lock on Windows

* test(archive): make the Windows claim-release regression actually fail

The new test compared the lstat target against the temp-dir claim path
verbatim. The command stats the resolved real path, so on macOS
(/var -> /private/var) the comparison never matched, `dev: 0n` was never
injected, and the test only asserted that an ordinary archive releases its
claim — which already passed before the fix. Verified: it passed with the
source change reverted.

Match the claim by file name instead, and count the interceptions so the
test fails loudly if the mock ever goes inert again rather than silently
passing. With the source change reverted the test now fails as intended.

Also add the missing changeset for the user-visible fix.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* test(archive): keep replaced claims with absent device ids

* test(archive): retain claim when zero-device path identity changes

---------

Co-authored-by: Clay Good <hi@claygood.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 17:29:03 +00:00
summerandClay Good e09916e232 docs(start): explain the first change in a new project (#1146)
* docs: add beginner workflow guide

* docs(start): guide the first change in a new project

* docs(start): make greenfield guidance tool-neutral

* docs(start): use portable first-change prompts

---------

Co-authored-by: Clay Good <hi@claygood.com>
2026-09-22 17:29:01 +00:00
Clay Good c681df7058 docs(install): document Homebrew (#1946)
* docs(install): document Homebrew

* docs(install): clarify the Node prerequisite
2026-09-22 17:28:58 +00:00
Clay Good 91f2925c63 docs(config): document opener settings (#1945)
* docs(config): document opener settings

* test(config): harden opener argument contract
2026-09-22 17:28:56 +00:00
Clay Good 416599a85e docs(copilot): document workflow rediscovery (#1943)
* docs(copilot): document workflow rediscovery

* test(copilot): pair discovery claims

* docs(copilot): harden workflow recovery guidance
2026-09-22 17:28:55 +00:00
Clay Good 0dde57b401 fix(continue): keep active prompts from becoming tool calls (#1944) 2026-09-22 17:28:53 +00:00
Clay Good a64303fe1e fix(update): report legacy Codex bootstrap failures (#1939)
* fix(update): report legacy Codex bootstrap failures

* fix(update): preserve failures after declined migrations

* test(update): cover every bootstrap failure exit
2026-09-22 17:28:51 +00:00
dependabot[bot] 02fade2077 ci: bump the github-actions group across 1 directory with 4 updates (#1928)
Bumps the github-actions group with 4 updates in the / directory: [pnpm/action-setup](https://github.com/pnpm/action-setup), [DeterminateSystems/nix-installer-action](https://github.com/determinatesystems/nix-installer-action), [DeterminateSystems/magic-nix-cache-action](https://github.com/determinatesystems/magic-nix-cache-action) and [changesets/action](https://github.com/changesets/action).


Updates `pnpm/action-setup` from 6.0.10 to 6.1.0
- [Release notes](https://github.com/pnpm/action-setup/releases)
- [Commits](https://github.com/pnpm/action-setup/compare/0977fd99725f1db4007ccb2928dbb4e90d06cc86...ea17c68df8912ef543352723c149a84f56e3d413)

Updates `DeterminateSystems/nix-installer-action` from 22 to 23
- [Release notes](https://github.com/determinatesystems/nix-installer-action/releases)
- [Commits](https://github.com/determinatesystems/nix-installer-action/compare/ef8a148080ab6020fd15196c2084a2eea5ff2d25...3138316df39ed29be04236d7ffc686fa525866aa)

Updates `DeterminateSystems/magic-nix-cache-action` from 14 to 15
- [Release notes](https://github.com/determinatesystems/magic-nix-cache-action/releases)
- [Commits](https://github.com/determinatesystems/magic-nix-cache-action/compare/908b263ff629f4cc17666315b7fd3ec127c6244d...84c0677f58dcedf3b91f8223ce36a9ea5b3c84b7)

Updates `changesets/action` from 2.1.1 to 2.1.2
- [Release notes](https://github.com/changesets/action/releases)
- [Changelog](https://github.com/changesets/action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/changesets/action/compare/8488615a623b1b9c987934bb89eae8af6a946ac1...ae32849d5ba541f9ae29e40e22a623bc13562f51)

---
updated-dependencies:
- dependency-name: pnpm/action-setup
  dependency-version: 6.1.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: github-actions
- dependency-name: DeterminateSystems/nix-installer-action
  dependency-version: '23'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions
- dependency-name: DeterminateSystems/magic-nix-cache-action
  dependency-version: '15'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions
- dependency-name: changesets/action
  dependency-version: 2.1.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: github-actions
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-09-22 17:28:49 +00:00
Clay GoodandClaude Opus 5 518e1a0124 fix(legacy-cleanup): read a legacy command through the handle it checks (#1905)
isGeneratedLegacyCommand lstat'ed a path and then re-read it by path, so the
file judged "generated" could differ from the file read (CodeQL
js/file-system-race, alert #524, added by #1874). Open once with
O_NOFOLLOW|O_NONBLOCK, fstat that handle, and read from it. Windows lacks
O_NOFOLLOW, so links are still refused there via lstat.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 17:26:31 +00:00
Tabish Bidiwale bae58cf614 docs: fix Docslab links to unfinished pages (#1903) 2026-09-17 06:31:19 +00:00
openspec-release-bot[bot]andgithub-actions[bot] 634c557bd0 Version Packages (#1896)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-09-17 01:01:33 +00:00
Clay GoodandClaude Opus 5 eb03b9e933 chore(changeset): track #1835 security hardening and #1785 nix completions (#1902)
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-17 00:39:28 +00:00
Clay GoodandClaude Opus 5 3312af4799 fix(templates): open generated artifacts with a top-level heading (#1777)
* fix(templates): open generated artifacts with a top-level heading

Generated proposal.md, design.md, spec.md and tasks.md started on a
section header, so every OpenSpec artifact tripped markdownlint MD041
("first line in a file should be a top-level heading") in editors that
run it. The files were also, literally, documents without a title.

Each packaged template now opens with `# Proposal`, `# Design`,
`# Spec Delta` or `# Tasks` followed by a blank line, and
`openspec schema init` scaffolds custom templates the same way. The
schema's own examples and the customization docs match.

Titles are inert to every reader downstream: the parsers anchor on `##`
and `###`, and archive builds a new main spec from the delta's sections,
so the main spec keeps its own generated `# <capability> Specification`
and only that one.

Closes #1138

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* test(templates): compare template headings on normalized line endings

The Windows runner checks out CRLF, so splitting the template on "\n"
left the blank second line as "\r" and the new guards failed there while
passing everywhere else. Normalize before splitting; verified against a
CRLF copy of the templates locally.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* test(templates): pin each template to its own title

Review feedback: the guards accepted any top-level heading, so a wrong
title would have passed, and the archive check counted `# ` lines only,
so a demoted `## Spec Delta` would have slipped through. Assert the exact
heading per artifact, the blank line under it in both guards, and that no
heading of any level named "Spec Delta" survives archive.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(workflows): teach the artifact titles everywhere the shape is shown

The templates were only half the story. The onboarding walkthrough drafts
each artifact in the conversation and then saves what it drafted, so its
previews would have written untitled proposal.md, spec.md, design.md and
tasks.md whatever the template said. The sync workflow's delta format
reference had the same gap, sitting directly beneath a main-spec
reference that does carry a title. Both now show the template's title,
and `docs/opsx.md` no longer documents a `template` value the CLI never
returned.

Guards added:

- The template guard now enumerates every artifact of every packaged
  schema from schema.yaml rather than a hardcoded list of four, and the
  exact-title table must name every artifact the schema declares.
- A drift guard reads the titles out of the packaged templates and
  requires the onboard and sync surfaces to show those same titles, so
  guidance and template cannot part ways again.
- Parser tests pin the claim the fix rests on: a title is inert, and a
  spec or proposal parses identically with and without one.

Regenerated the skill mirrors and parity hashes for the two workflows
touched; no other hash moved.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs: move the artifact-title update to the canonical docs-lab tree

The live site builds from docs-lab/, so the exact-format page there is the
one readers see. It still showed all four templates opening on a section
header and described the delta as starting with `## Purpose`.

- reference/schemas/spec-driven/index.md: each template block now matches
  the shipped template byte for byte, and the quoted spec/tasks
  instructions match schema.yaml again.
- customize/schemas.md: one line telling fork authors to keep the `#`
  title on the first line.

Reverts the edits to docs/customization.md and docs/opsx.md: that tree is
no longer published and docs-lab/README.md keeps it as source material
only, so editing it would leave two versions of the same fact.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(show): keep the change id as title for a bare `# Proposal` heading

The packaged proposal template now opens with `# Proposal`, which
`extractTitle` read as the change's title, so `show --json` and
`change list --json/--long` titled every templated change "Proposal"
instead of its id. Treat that bare heading as untitled.

Also re-quote the proposal and specs instructions in the canonical
docs-lab schema page after #1700 changed schema.yaml.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-17 00:07:26 +00:00
Clay GoodandClaude Opus 5 5f5914e7f7 fix(skills): match natural "openspec <verb>" phrasing to its workflow (#1852)
* feat(skills): match natural "openspec <verb>" phrasing to its workflow

Users and agents say "openspec propose" / "openspec apply", but no workflow
skill description contained that phrasing, so an agent hearing it had nothing
to match and routinely hand-built the artifacts with the CLI instead of
running the workflow.

Each workflow skill's description now names the phrasings that should route
to it. `openspec update` is deliberately left unclaimed: it is a real CLI
command that refreshes generated files, unrelated to the update-change
workflow, so that skill claims "openspec update change" instead.

Descriptions are emitted as unquoted YAML plain scalars, so the new tests also
pin that the generated frontmatter still parses and the description round-trips.

Closes #1221

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(skills): derive the CLI-collision guard instead of hardcoding it

Review found the guard codified the one exception rather than the rule, so it
could never catch the next collision. It now reads every command name the CLI
registers and fails on any claimed phrase that shadows one, unless the phrase
is listed in DELIBERATE_CLI_PHRASE_CLAIMS with a reason.

Two routing fixes fall out of stating the rule:

- bulk-archive also claims "openspec archive all", so an exact-phrase match on
  "openspec archive" no longer pulls a multi-change request to the
  single-change skill.
- update-change now disclaims the openspec update CLI command in prose, not
  only by avoiding the string.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(skills): drop the trailing clause and close the plural-archive hole

Review found the "- follow this skill rather than doing the work by hand"
trailer was decoration that contradicted two of the skills it was appended to:
sync-specs opens "This is an agent-driven operation - you will read delta specs
and directly edit main specs", and explore says "This is a stance, not a
workflow. There are no fixed steps." A description is read at selection time,
so the clause could not reach the hand-building it targeted anyway; the bodies
already carry that guidance. Removing it from all 12 also drops ~800 chars of
identical boilerplate that made update-change's CLI redirect read as filler.

Routing fixes:

- bulk-archive claims the plural phrasings that do not contain "all", so
  "openspec archive these three changes" no longer loses to the single-change
  skill on the bare literal.
- update-change redirects to the CLI command positively instead of negating
  ("run that command instead"), which routers honor far better than "not for".
- apply also claims "openspec implement", the natural English verb for it,
  which shadows no CLI command.

Corrects the recorded reason for claiming "openspec archive": the CLI command
does merge delta specs (docs/cli.md:631, src/core/archive.ts:1402). The real
reason is that the workflow confirms and verifies the merge before anything
moves, where the bare command does it in one shot.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs(quickstart): name the verb phrasing that now routes to a workflow

Also rewrites the changeset to house style: links the issue, names the
commands-only scope limit, and tells a reader they need `openspec update`
to pick it up.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(skills): walk the real command tree instead of scanning the entrypoint

Mutation testing found the collision guard was a strict subset of reality,
not the superset its comment claimed. It scanned src/cli/index.ts for
`.command('…')`, but seven groups — spec, config, schema, store, doctor,
context, workset — are registered from their own modules, so 23 real command
names were invisible. A description claiming "openspec doctor" or
"openspec spec" passed 18/18 green.

It now walks the commander tree from the exported `program` (importing it does
not parse argv; runCli does that), and a sanity test pins the seven delegated
groups so the blind spot cannot come back.

Three more holes the same pass found, all confirmed by re-running the
mutations that previously slipped through:

- phrase extraction was case-sensitive and double-quote-only, so
  "Openspec update" and `openspec update` in backticks both evaded every
  guard. Matching is now case-insensitive and accepts either delimiter.
  Unquoted prose stays excluded on purpose: the update-change redirect names
  the CLI command in prose, and prose is not a routing trigger.
- prefix shadowing was unguarded, which is the exact shape of the
  archive/bulk-archive tension. A shorter phrase contained in another skill's
  longer phrase must now be declared in DELIBERATE_PHRASE_SHADOWING.
- both allowlists accepted an empty reason and never flagged stale entries.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* test(skills): let the CLI-collision guard ignore hidden workflow-verb hints

PR #1776 registers the workflow verbs (explore, propose, apply, ...) as
hidden CLI commands that only point the user at the workflow. Walking the
commander tree then saw "openspec explore" as a real command and failed
the collision guard for every skill trigger.

Skip a subcommand only when it is hidden AND named after a workflow.
Visible commands and hidden non-workflow commands are still guarded,
pinned by a synthetic commander tree.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs(changeset): drop em dashes from the release note

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(skills): drop the generic by-hand clause from the explore description

The other eleven descriptions dropped it; explore is a stance, not a
workflow, so telling the agent to follow it instead of doing the work
contradicts it. Adds a regression over every workflow description.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 23:34:57 +00:00
Clay GoodandClaude Opus 5 9827762d2d fix(skills): stop workflows from adopting a project that never ran init (#1787)
* fix(skills): stop workflows from adopting a project that never ran init

Generated skills and commands are installed once per machine and offered in
every repository the agent opens, including ones with no OpenSpec at all.
Nothing stopped the workflow there: root resolution falls back to an implicit
root at the current directory, so `openspec new change` quietly creates
`openspec/` in whatever repo the agent happened to be standing in (#1645).

Two changes, both in the generated instructions:

- Every workflow now carries a shared project check. Before the first step
  that writes, the agent reads `root.source` from `openspec status --json`;
  `implicit` (or a `No OpenSpec root found` error) means the project is not
  set up, and the agent stops and asks the user whether to run `openspec
  init`, target a store, or drop OpenSpec for that request. It may not
  initialize the project on its own or let a command create the root as a
  side effect.
- Every deployed skill description now names OpenSpec. Hosts pick skills by
  description, and "Enter explore mode - a thinking partner..." reads as a
  generic offer in a repository that has never heard of OpenSpec.

Closes #1645

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* chore(changeset): note the uninitialized-project guard

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(skills): let onboarding run init once the user asks for it

The guard read as an absolute ban on `openspec init`, which contradicts the
option it offers one sentence earlier and the onboard workflow's job.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs(troubleshooting): explain an OpenSpec workflow starting in an unset-up project

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(skills): check the root with a command that never fabricates one

`openspec status --json` demands --change once a project has changes, so the
guard's own check could fail in exactly the projects it should wave through.
`openspec list --json` answers in one shape everywhere: a root object when the
project is set up, `root: null` both when nothing is set up and when only
stores are registered.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* test(skills): pin the guard against every write, not the first fence

CodeRabbit's point: checking only the first ```bash fence would miss a write
outside a fence. Assert instead that nothing preceding the guard runs a
command or writes, and that the guard sits directly under the store-selection
guidance - both fail when the guard is moved down a workflow.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* feat(new-change): say when the command had to create the root itself

The generated workflows now check for a root before writing, but the guard is
instructions - an agent that ignores it, or a human running the CLI directly,
still turned an unset-up directory into an OpenSpec project without a word.
Creating the root stays zero-config; it is no longer silent.

Human output only: --json is unchanged, and `root.source` already carried the
same fact for programmatic callers.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(skills): tell agents the root check's non-zero exit is the answer

`openspec list --json` exits 1 when there is no root. An agent that reads that
as a broken CLI is one step from hand-creating `openspec/` instead, which is
the failure the guard exists to prevent.

Also drops a vacuous assertion: the notice test now checks that the note names
the directory it created and that the change really landed there.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* style(skills): read the guard back and untangle its two 'in that case' clauses

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(skills): carry the store flag into the root check; pin the notice path exactly

CodeRabbit, both valid:
- With a store selected the store IS the root, so the check has to run as
  `openspec list --json --store <id>`. The store-selection paragraph above
  already says to append the flag to every command it lists, but leaving it
  implicit here invited a check against the wrong directory.
- The notice assertion matched any `openspec/` suffix. It now pins the exact
  rendered path, and a new case runs the command from a subdirectory to show
  the note names the directory actually adopted (and that the repo above it
  is left alone).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs: move the no-root contract to the canonical docs-lab pages

alfred-openspec on #1787: docs-lab/README.md makes docs-lab/ canonical and the
old docs/ tree legacy, and the canonical pages were stale in the two places the
review named.

- docs-lab/reference/cli.md, 'openspec new': documents the implicit-root notice
  after the 'Next:' line, with the exact output the CLI prints, that it goes to
  stdout and never appears with --json, and that JSON carries the same fact as
  root.source: implicit. Verified against a real run in an empty directory with
  an isolated HOME.
- docs-lab/reference/skills.md: states the shared response and stop behavior
  once, above the index table, since it now holds for every skill: confirm the
  resolved root before the first write, stop when there is none, offer init, a
  store, or dropping OpenSpec, wait for the answer, never create openspec/ on
  its own.

Drops the legacy docs/troubleshooting.md addition.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(skills): make the no-root answer depend on how the workflow was reached

alfred-openspec's product call on #1787. One answer could not serve both
arrivals: #1645 asks the workflow to get out of the way ('it can go through the
normal general propose not the openspec'), while a user who typed the skill's
name is owed an answer about OpenSpec.

The guard now branches after the same `openspec list --json` check:

- Auto-selected: the model picked this workflow without the user naming
  OpenSpec, naming the skill, or running its command. Drop OpenSpec and answer
  the request normally, with no setup question and no mention of OpenSpec.
- Explicit OpenSpec request: stop before writing and ask whether to run
  `openspec init`, target a store, or continue without OpenSpec, then wait.

Neither branch may create the root as a side effect, stated once for both.

One text serves both surfaces rather than a command-only variant, because
apply-change and onboard render a single body into the skill and the command
alike; a command-only constant would mean threading a surface flag through
bodies that deliberately have none (#1515). The bullets scope themselves
instead, and a slash command is an explicit invocation, so only the ask branch
can apply there. A test pins that branch reaching every generated opsx command.

Four regressions: the auto-selected branch (asserting it does not mention
`openspec init` or `--store`), the explicit branch, the explicit branch's
presence in every command file, and the shared no-side-effect rule.

docs-lab/reference/skills.md said every no-root invocation asks. It now carries
the same two branches as scan anchors.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(skills): keep the root guard off store-only projects and fix its docs

A store-only project whose `store:` line names a store this machine has
not registered reports `"root": null` from `openspec list --json`, so the
guard read a real OpenSpec project as uninitialized. The guard now checks
for the `Declared in` status message first and shows the store error
instead. A stale global defaultStore reports the same codes in unrelated
repositories, which is why the message prefix, not the code, decides.

Propose's context step from #1657 offered `openspec init` on
`no_openspec_root` regardless of how the workflow was reached. It now
defers to the project check, so an auto-selected propose skill stays
silent.

The changeset names both no-root branches, and the `new change --json`
docs separate the initialized example from the verified `implicit` one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(skills): keep the root guard off projects with a malformed store line

A config-only project whose `store:` line is malformed reports
`"root": null` with an `Invalid store declaration in` message, not
`Declared in`, so the project check read it as never initialized and
would drop OpenSpec or offer `openspec init` there. The guard now names
both prefixes, and a root-selection test pins that every declaration
failure starts with one of them while a stale global defaultStore
starts with neither.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* test(skills): check guard ordering in the skill body, not its frontmatter

A skill's YAML frontmatter is metadata a host reads to choose the skill,
not instructions the agent runs, so a description that quotes a command
name must not trip the ordering check. Scope the scan to the text after
the closing frontmatter delimiter; a command injected into the body ahead
of the guard still fails.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 23:05:28 +00:00
Clay GoodandClaude Opus 5 11a9691524 fix(tasks): count unrecognised checkbox markers as not done (#1773)
* fix(tasks): count unrecognised checkbox markers as not done

A checkbox marker the task parser did not recognise was dropped from
progress entirely: it counted toward neither the numerator nor the
denominator. A tasks.md whose remaining work was written `- [~] ...`
therefore reported `✓ Complete` in `openspec list`/`status`, and
`openspec archive` raised no incomplete-task warning for it. Marking
open items with such a marker *shrank* the denominator instead of
leaving them counted as not-done.

Widen the marker to any single non-`]` character and keep `x`/`X` as
the only done state, so an unrecognised marker reads as not-done - the
fail-safe direction the pattern's own docblock argues for. OpenSpec
adopts no new marker semantics; it just stops losing the line.

Closes #1761

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(tasks): close the remaining silent-loss holes in checkbox parsing

Hardening pass on the #1761 fix.

Parser: an empty `[]` and a padded `[ x]` were dropped exactly the way
`[~]` was - checkbox-like lines counting toward neither the numerator
nor the denominator, so archive stopped warning about them. The marker
is now "at most one non-whitespace token", which covers all three.
Multi-character brackets stay unmatched on purpose: widening to
`[^\]]*` would match `- [Some doc](./doc.md)`, whose `]` is followed by
`(` rather than a space, and turn every Markdown link list into phantom
unfinished work. The docblock states that boundary and a test pins it.

Guidance: archive, bulk-archive and verify told agents to count
`- [ ]` vs `- [x]` by hand, which reproduced the same bug one layer up -
an agent following it saw no incomplete tasks in a `[~]` file even with
the CLI fixed. All six bodies (skill + command per workflow) now state
the rule the parser implements; skills/ mirror and parity hashes
regenerated.

Coverage now spans every consumer of the shared counter: `openspec
list`, archive's gate, the apply task list, validate's task-numbering
check and the parser itself, including the reported 42-done/17-deferred
ratio. Reverting only src/utils/task-progress.ts fails 7 of them.

Verified empirically: the widened pattern newly matches 0 lines across
all 1090 .md/.ts files in the repo, templates and docs included.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(tasks): state the padded-tick rule in workflow guidance too

CodeRabbit review, verified and valid: the new guidance named only
exact `[x]`/`[X]` as complete, while the parser also counts `[ x]`,
`[x ]` and `[ x ]`. An agent hand-counting by that wording would have
reported a padded tick as unfinished work and disagreed with `openspec
list` - the same guidance/CLI split this PR set out to close.

All six bodies now phrase it as the parser implements it: complete
means the box holds only `x`/`X`, spacing inside the brackets ignored;
every other marker, including an empty box, is incomplete. skills/
mirror and parity hashes regenerated.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(tasks): keep one-character link bullets out of the task count

alfred-openspec on #1773: the widened marker class let a one-character
Markdown link label parse as a checkbox, so `- [A](https://example.com)` and
`- [1](./one)` read as unfinished tasks and made progress and archive report
phantom work. The multi-character guard did not cover them: a one-character
label is one token.

The closing bracket may now not be followed by `(` or `[`, the only two
characters that continue Markdown link syntax. Nothing that used to count is
lost: a checkbox is followed by its description or by end of line, and
`- [x]done` still parses. Regressions cover both link forms, the reference
form, and a link inside a real task description.

Also updates the canonical contract, which said tasks not written `- [ ]` are
not tracked while this change deliberately tracks `[~]`, `[-]`, `[]` and a
padded `x`. Fixed in schemas/spec-driven/schema.yaml and in the docs-lab page
that quotes it, so the quote stays faithful.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs: say that a done checkbox is case-insensitive

CodeRabbit on #1773: the parser lowercases the marker before comparing it with
`x`, so `- [X]` is done, but the contract I added said only `- [x]` counts.
Corrected in both copies, which are the same text.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(tasks): keep empty boxes followed by link syntax in the task count

The one-character link guard also dropped `- [ ](...)` and `- [ ][...]`,
which the strict pre-#1761 pattern counted as unfinished tasks. A line the
parser drops is one archive stops warning about, so a whitespace-only box
now bypasses the guard. The canonical tasks instruction also names `[-]`
and the padded `x` rule, in schema.yaml and docs-lab in lockstep.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs(changeset): drop em dashes and state the padded-x done rule

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 22:37:25 +00:00
Clay GoodandClaude Opus 5 62106f40e3 fix(explore): name the propose workflow at every handoff (#1788)
* fix(explore): name the propose workflow at every handoff

Explore mode refuses to implement, but nowhere named the workflow that
turns the discussion into a change. The refusal, the "flow into a
proposal" ending, the closing summary, and the do-not-implement
guardrail all described the next step as prose. With no named exit,
agents answered the discovery questions and then started writing code
(#869).

All four handoff points now point at `/opsx:propose`, written in the
canonical `/opsx:<id>` form so each tool renders the invocation it
actually registers. Skill and command bodies are patched together, the
skills.sh mirror is regenerated, and parity hashes are refreshed.

Closes #869

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(explore): name the continuation after a seamless capture

The capture path let explore scaffold a change and write artifacts, then
said nothing about what came next. An agent holding a fresh proposal
inside explore mode has an obvious wrong next move, and it is the one
#869 reported. The capture now ends by naming `/opsx:propose` for the
remaining planning artifacts and `/opsx:apply` for implementation, and
says plainly that capturing artifacts is not permission to implement
them.

Widen the rendering guard to walk the real registries instead of a
hand-picked few: every registered command adapter and every entry in
AI_TOOLS must rewrite every canonical reference in both bodies, with no
`/opsx:` form surviving on any skills surface. A new adapter or a
changed invocation shape now fails here rather than shipping a command
nobody answers to.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs(changeset): cover the capture-path handoff

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(explore): resolve handoffs against the installed workflow set

A custom profile can install explore without propose or apply, and the
explore skill and command still named both. Handoffs are now authored with
optionalWorkflow() and resolved in getSkillTemplates()/getCommandTemplates()
against the workflow filter every init/update path already passes. Missing
workflows fall back to explore's own capture path and the openspec
instructions apply CLI. Output with every workflow installed is unchanged.

Uses the same API and marker syntax as #1775 so the two compose.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs(changeset): drop em dashes from the release note

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 22:12:45 +00:00
e67ac47f3a fix(bulk-archive): check the archive target before moving changeRoot (#1829)
* fix(bulk-archive): check the archive target before moving changeRoot

Step 8c ran `mv` with no existence check. POSIX `mv` moves changeRoot
inside an existing target directory and exits 0, so a same-day name
collision produced archive/<target>/<target>/ and was recorded as a
successful archive.

The workflow already promised the opposite in three places: the guardrail
"If archive target exists, fail that change but continue with others" and
both failure output templates listing "Archive directory already exists".
The single-change archive workflow implements the check; the bulk path did
not.

Closes #1827

* fix(bulk-archive): check archive targets before the first spec write

The existence check at the move ran after step 8a had already synced the
change's delta specs, so a collision still left main specs rewritten for a
change that stayed active. `openspec archive` settles the destination before
touching any spec; the batch now does the same in step 3 (including two
selected changes that resolve to the same target), keeps blocked changes out
of sync, conflict resolution, and the archive-everything option, and
re-checks just before the move.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs(changeset): describe the pre-sync archive target check

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(bulk-archive): undo a nested move and keep blocked changes failed

The last target check and `mv` are separate steps, so a target created in
between still nested the change with exit 0. Step 8c now confirms the move
did not land inside the target and moves the change back, recording
`Archive directory already exists`, instead of reporting success.

The ready-only option recorded every non-Ready change as Skipped, which
misreported archive collisions; Blocked changes now stay Failed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* test(bulk-archive): require the move before asserting the nest check follows it

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(bulk-archive): say the guardrail's dated target is the one step 3d recorded

The guardrail still read 'uses current date', which invites recomputing the
name at the move. It now points to the step 3d value, matching step 8c.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: choi138 <dev@silviahealth.com>
Co-authored-by: Clay Good <hi@claygood.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 22:12:42 +00:00
605d9e7a2b fix(config): leave an unparseable global config untouched (#1876)
* fix(config): leave an unparseable global config untouched

A typo in config.json made getGlobalConfig() fall back to defaults, which telemetry read as consent: any command, even a read-only list, minted a new anonymous id and wrote it over the whole file, dropping a telemetry.enabled false opt-out and every other setting. config set, unset and profile likewise saved the defaults over it.

saveGlobalConfig() and telemetry's writeConfig() now refuse to overwrite a file they cannot parse, telemetry and the update check treat such a file as opted out, and config set, unset and profile exit with an error pointing to config edit. config reset --all can still replace the file, and the existing warning is unchanged.

* fix(config): treat a non-object global config as unreadable

Valid JSON that is not an object (null, an array, a string) also makes
getGlobalConfig() fall back to defaults, silently, so `config set` still
saved those defaults over the user's file. isGlobalConfigUnreadable() now
reports such a file as unreadable, which keeps telemetry off and routes
every save through the same refusal as a parse failure. This matches how
completion-tip already treats a non-object config.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs(config): document the refusal to rewrite an unparseable config

docs-lab/reference/cli.md said `config unset` always exits 0. With an
unparseable global config, `config set`, `config unset` and
`config profile` now exit 1 and leave the file unchanged; say so, show
the message and the two fixes, and note telemetry and the update check
stay off until it is fixed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(config): warn about an unparseable global config once per command

Telemetry, the update check and the command each read the global config,
and now that none of them rewrites the broken file, the "Invalid JSON"
warning printed two or three times per command. Warn once per path.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(migration): skip profile migration for a config that is not a JSON object

A global config holding [] reached saveGlobalConfig, which now refuses
it, so init and update failed. null already crashed on a property read.
migrateIfNeeded now skips such a file, as it does for a parse failure.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(telemetry): refuse to write over a non-object global config

The telemetry writer had its own notion of an unreadable config: only a
JSON parse failure counted. Valid JSON that is not an object slipped
through, so updateTelemetryConfig() merged into it and replaced the file
-- an array, a number or a boolean became a bare telemetry object, a
string spread into numeric character keys, and null threw a TypeError
instead of the actionable refusal every other writer reports.

Funnel both notions through one predicate: isConfigRootObject() in
core/global-config.ts now backs isGlobalConfigUnreadable() and the
telemetry reader, so every shape the global guard rejects is classified
invalid on read and refused on write. Both writers report the same
one-line message via unreadableGlobalConfigMessage().

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(config): read a non-object global config as plain defaults

getGlobalConfig() spread the parsed root into its result before the
unreadable predicate was consulted, so the shape of the root leaked to
every caller: a config of "abc" returned defaults plus the numeric
character keys 0, 1 and 2. Check isConfigRootObject() right after
parsing and answer with plain defaults, as for a file that did not parse
at all.

Reported by CodeRabbit as an outside-the-diff finding.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(config): stop `config list` crashing on a null config root

`config list` re-reads the raw file to mark each value explicit or
default, and assigned JSON.parse() straight to rawConfig. A root of
`null` then crashed the command with a TypeError stack trace, the one
failure mode this PR is meant to remove, and it did so on a read-only
command. Normalize a non-object root to {} through the shared
isConfigRootObject() predicate so the listing shows plain defaults.

Reported by CodeRabbit as an outside-the-diff finding.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* test(config): show the telemetry notice before the first-run write check

Since #1835, nothing is tracked until the notice has been shown, so the
first-run test must show it before tracking the command.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Clay Good <hi@claygood.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 19:24:40 +00:00
Clay GoodandClaude Opus 5 626269ed73 fix(templates): stop generated skills naming workflows the profile omits (#1775)
* fix(templates): stop generated skills naming workflows the profile omits

The `core` profile installs six of the twelve workflows, but the update
and apply templates named `/opsx:continue` (6 times) and `/opsx:new`
(twice) regardless. On a default install those became
`/openspec-continue-change` and `/openspec-new-change` — skills that were
never written — so `update-change` refused to create a missing artifact
and handed off to a dead end. The only guard was a sentence asking the
model to check availability at runtime, 70 lines above the two places it
hits the wall.

`command-references.ts` decides how a reference is spelled; nothing
decided whether it should be emitted at all. Add that: templates author
both wordings with `optionalWorkflow()`, and `getSkillTemplates()` /
`getCommandTemplates()` — the one place every generation path already
funnels the resolved workflow set through — pick a branch before the
reference transformers run. A profile that omits a workflow now gets a
concrete `openspec status` / `openspec instructions` fallback instead of
a reference to a skill that does not exist.

Closes #1734

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* chore(changeset): describe profile-aware workflow references

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* test(init): assert the core profile names no uninstalled workflow

The end-to-end init test pinned the runtime availability hedging that
#1734 is about, and asserted `/opsx:continue` appears in the default
profile's generated update workflow — the bug itself. Assert the fixed
behavior instead: neither `/opsx:continue` nor `/opsx:new` appears, and
the CLI fallback is stated outright, for both the update and apply
surfaces.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(templates): resolve every cross-workflow reference, not just core's

The first commit fixed the two templates the default profile broke on.
Every other cross-workflow handoff had the same shape, and arbitrary
subsets are reachable: a `custom` profile is whatever the user picked,
and `openspec update` re-derives a workflow set from what it finds on
disk (legacy tool overrides, inferred Codex workflows) without passing
it through getProfileWorkflows.

So resolve all of them:

- `apply` -> archive; `continue` -> apply, archive; `ff` -> apply;
  `new` -> continue; `propose` -> apply; `update` -> apply, archive;
  `archive` and `bulk-archive` -> sync.
- `onboard`'s two command-reference tables are built from the installed
  set rather than printed in full with an "only if installed" caveat, and
  its explore, resume and next-step prompts are resolved the same way.

Two supporting changes:

- `onlyWithWorkflow()` plus a whole-line rule in the resolver: a
  conditional that owns its line takes the line with it when it resolves
  to empty, so a dropped table row cannot leave a blank line that ends
  the table in markdown.
- `generateSkillContent()` and `generateCommand()` now throw on an
  unresolved marker. A generation path that skips the choke point fails
  loudly instead of writing `[[opsx:...]]` into a user's SKILL.md.

The propose and ff surfaces keep their deliberate wording difference
(#258): the command surface never invites "ask me to implement", so its
missing-`apply` fallback names the CLI rather than a conversation.

The guard test now runs the property over every subset that could expose
a reference — each workflow alone, everything but one, the empty set, and
the two shipped profiles — for skills and commands, in both spellings.
Twenty-plus of those cases fail against the previous commit.

Only `openspec-onboard` changes in the skills/ mirror: with every
workflow installed, all other templates render byte for byte as before.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* chore(changeset): cover the full cross-workflow reference fix

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(templates): validate conditionals before choosing a branch

Resolution discarded the unselected branch and only then checked for
residual markers, so a truncated block inside the *missing* branch was
accepted for a profile that installs the workflow and rejected for one
that does not. Profile-dependent authoring errors are exactly what this
module exists to remove.

Validate the authored text up front instead: every marker must be one of
the three recognized forms, and they must appear as a flat sequence of
if / else / end. A malformed block now throws identically for every
profile. The post-resolution check stays as a backstop.

Caught by CodeRabbit on #1775.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs: state the profile-aware handoff rule in the skills reference

alfred-openspec on #1775: docs-lab/reference/skills.md described several
handoffs as unconditional while this change deliberately emits a CLI or
conversational fallback when the profile omits the target.

Stated once, above the entries, rather than as a caveat on each of the eleven
affected Response and Creates rows: the page's recipe is one fact per row, and
repeating the same conditional eleven times would bury the contracts it exists
to state. The rows keep naming the skill that owns the next step, which is the
fact a reader looks up; the rule above them says what happens when that skill
is not installed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(workflows): fold #1735 into the profile-aware references

#1735 fixes the same issue (#1734) by removing the optional handoffs outright.
This PR resolves them at generation time instead, which is strictly better for
the template layer: an install that has `continue` still gets told about it.
So the mechanism here wins and #1735's content is folded in, rather than the
two competing for the same lines.

What #1735 had that this did not:

- src/commands/workflow/instructions.ts. The CLI's own runtime strings named
  the openspec-continue-change skill. Those are chosen at run time, so
  optionalWorkflow() cannot reach them; taken from #1735 verbatim.
- The blocked-state fallback. It was a one-line pointer; it now carries #1735's
  full CLI recovery (select the next `ready` artifact, not `skipped` or
  `blocked`, read its rules with `openspec instructions`, keep the selected
  `--store` on both commands) plus the tracking-file repair path and the
  `missingArtifacts` field it branches on. The installed branch still names
  `/opsx:continue`, so neither audience loses.

#1735's update-change.ts rewrite is not carried over: this PR already covers
all six of those sites conditionally, which is the better answer.

Both of #1735's test suites come across, and they are worth more here than
there. test/core/templates/profile-handoffs.test.ts asserts that no generated
file names an uninstalled workflow across every tool and all three delivery
modes, which is the property this PR's mechanism exists to provide, and it
passes against it. test/commands/profile-handoffs.test.ts covers the runtime
CLI strings. The two guards are complementary: that one is broad on tools and
deliveries, this PR's own profile-workflow-references.test.ts is broad on
workflow subsets.

#1735's command-references.test.ts assertions could not be carried as written,
since they assume the reference is gone unconditionally. Replaced with a case
that resolves the template against a set without `continue` and asserts the
fallback carries the whole recovery. Verified it fails when the fallback is
shortened.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs: name the core-profile fallback on the two rows that hit it

The rule above the entries covers every profile, but apply-change and
update-change are Core skills whose rows name openspec-continue-change, which
the core profile never installs. On the default install those rows now say what
the generated skill points to instead: openspec status and openspec instructions.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs(changeset): drop em dashes from the release note

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 19:17:38 +00:00
Clay GoodandClaude Opus 5 086c93b40b chore(deps-dev): bump changesets, eslint and typescript-eslint (#1901)
Applies dependabot #1898 (lockfile-only: @changesets/changelog-github 1.0.1,
@changesets/cli 3.0.2, eslint 10.10.0, typescript-eslint 8.70.0) plus the two
follow-ups its CI needed: the transitive esbuild 0.28.1 -> 0.28.2 bump in
allowBuilds, and the pnpmDeps hash in flake.nix computed by the Nix job.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 19:11:26 +00:00
7090e16d74 fix(schema): validate the apply block against declared artifacts (#1868)
* fix(schema): validate the apply block against declared artifacts

parseSchema checked each artifact's requires but never the apply block, so schema validate passed a schema whose apply.requires named an artifact that does not exist or whose apply.tracks named a file no artifact generates. At run time apply skipped the unknown id, turning the apply gate off, or blocked forever on a tracked file nothing produces.

Reject both in parseSchema, naming the bad value and what the schema declares. tracks is compared to generates by exact string, the same comparison the tracked-tasks lookups use, so any schema that parses is one they can resolve.

* fix(schema): warn instead of failing the load on an unmatched apply.tracks

apply.tracks is a path that apply reads as written, not an artifact id.
A schema that tracks a hand-written TODO.md, or one file under a glob
generates such as tasks/main.md, loads and applies correctly on main.
Rejecting it in parseSchema made every command on that schema fail.

Keep the unknown apply.requires id as a load error, the same as an
unknown artifact requires. Report an apply.tracks path that matches no
artifact's generates as a warning from `openspec schema validate`,
which still exits 0.

Move the docs note from legacy docs/customization.md into docs-lab
(schema-yaml.md validation section, cli.md schema validate). The
schema-yaml.md table had said unknown apply.requires IDs go unreported.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(schema): describe apply.tracks as a generates mismatch, not as ungenerated

The apply.tracks check compares `tracks` to each artifact's `generates` with
exact string equality, but its warning said the tracked file "is not generated
by any artifact". That is false for the case this PR deliberately supports:
`tracks: tasks/main.md` under `generates: tasks/*.md`, where the glob really
does generate the file and only the strings differ.

The diagnostic now names the real condition (the `tracks` value does not
exactly match any `generates` value, so OpenSpec cannot tell which artifact's
progress it tracks) and keeps both remedies. Both docs-lab pages, the changeset
and the JSDoc that repeated the claim are corrected the same way, and a new
CLI test pins that the glob case is described as a mismatch and never as
ungenerated.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* test(schema): pin apply.tracks matching across Windows path separators

The tracked-tasks lookup compares apply.tracks and generates as plain
strings, so a backslash on one side and a forward slash on the other must
warn, and the same backslash spelling on both sides must not.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Clay Good <hi@claygood.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 19:11:23 +00:00
a5bf5c6844 fix(validate): report requirements outside delta sections (#1804)
* fix(validate): report requirements outside delta sections

* docs(test): document orphaned-requirement test helpers

* test(parser): pin orphan reporting to the reader's section rule

Cover a header the reader does not match exactly (`## ADDED  Requirements`),
which must be reported, and a repeated `## ADDED Requirements` header with a
non-delta section between the copies, which must not. Add a patch changeset
matching the other parser fixes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Clay Good <hi@claygood.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 19:11:19 +00:00
Clay GoodandClaude Opus 5 6a87a514ec test(store): give the git probe cleanup hook the setup's timeout (#1900)
Deleting the 12000 fixture files on the Windows runner outlasts vitest's
default 10s hook timeout, so the suite failed after every test passed and
knocked PRs out of the merge queue.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 19:11:17 +00:00
Clay GoodandClaude Opus 5 fede536c27 fix(update-change): draft the requested edit in step 4, write only in step 5 (#1840)
* fix(update-change): draft the requested edit in step 4, write only in step 5

Step 4 told the agent to "Apply the requested edit" while step 5 and the
guardrails told it to write only after the user confirms each revision.
"Apply" is a write verb in this very document - step 5 is titled "Confirm
and apply" - so the same request either wrote immediately or stopped and
showed the revision first, depending on which passage the agent weighed.
Step 5 is the workflow's only write path, so its confirmation guarantee
was unenforceable whenever step 4 governed.

Step 4 now drafts and says explicitly that it writes nothing; step 5
claims every write and shows the drafted edit for confirmation. Both
delivery surfaces and the committed skill carry the same wording, and a
regression test slices step 4 out of each body so no write verb can
reappear there.

Closes #1836

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(update-change): keep every edit verb out of the write-free step

Adversarial review of the first commit found three gaps.

Step 4 still opened a bullet with "Revise only files that already
exist" - the same shape as the bug, an imperative edit verb inside the
step that now declares it writes nothing. It reads as a scoping rule,
but "revise" is the write verb everywhere else in this body ("proposed
revision", "Which artifacts were revised"). It now says "Propose
revisions only to files that already exist".

The guard's `not.toMatch(/\bWrite\b/)` was inert and inverted: it was
case-sensitive, so it never matched the wording it was meant to pin,
and it could not be made case-insensitive because the fix's own text
says "do not write anything yet". It now strips that one sanctioned
sentence and rejects any remaining form of write or apply,
case-insensitively - so lowercase "write the drafted edit now", the
dangerous case, is caught. A second assertion rejects any step 4 bullet
opening with Revise/Edit/Update/Rewrite.

The changeset claimed no write verb could reappear in step 4, which was
not what the old guard did. It now states what the guard checks.

Also passes the surface label into section() so a marker drift names the
surface that broke.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(update-change): let no passage outside step 5 authorize a write

Mutation-testing the first guard found it defeatable: 20 of 25 mutations
broke the #1836 contract and still passed. The two worst were structural,
not lexical - the guard only sliced steps 4 and 5, so an authorization
placed in the intro, in step 3, in the Guardrails or in the Output
section governed the agent while no assertion ever saw it; and step 5
carried only positive assertions, so its gate could be kept and then
exempted in the next sentence, or the whole-body confirmation guardrail
deleted outright, with nothing failing.

The guard now pins step 4's draft rule and the whole of step 5 verbatim,
requires the whole-body guardrail to survive, and scans every other
passage for write verbs and their synonyms (commit/save/persist/
overwrite/reapply/flush/emit), verb-free equivalents (perform, carry
out, in place, to disk), consent-bypass phrasing, and imperative edit
bullets including ones led by an adverb. All 22 mutations are now caught.

Two wording corrections came out of the same review. "nothing earlier
writes to disk" was false - every openspec invocation persists a
telemetry id via the root preAction hook - so step 5 now claims every
artifact write instead. Step 4 says "in the conversation, not in files",
borrowing explore.ts's phrasing, so "draft" cannot be read as writing a
draft file.

docs/commands.md carried the same apply-vs-confirm collision two lines
above the confirmation bullet, contradicting the worked example directly
below it; it now says "Drafts your requested revision".

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* test(update-change): catch more imperative edit verbs outside step 5

CodeRabbit noted `- Modify the artifact now` slipped past the
imperative-bullet guard. Added Modify, Amend, Patch and Replace, each
verified to trip the guard. `Change` is deliberately excluded: step 6
already opens a bullet with "Change already implemented ...".

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* test(update-change): prove the write-gate scan trips in every section

The outside-step-5 scan was only ever checked against the real body, so
nothing showed it could fail. Injecting a verb-free authorization into the
intro, Input, steps 1-2, Output or Guardrails passed on both surfaces (7 of
10 sections). The bare "already applied" allowlist entry also erased
"treat the requested edit as already applied" before any check ran.

- Move the scan into a function and add a mutation table: one injected
  authorization per section, asserted on skill and command bodies.
- Spell every sanctioned mention in full context.
- Flag any mention of the requested edit outside the pinned draft rule.
- Widen the consent-bypass filter (needs no / exempt from / skip confirm).

Also revert the legacy docs/commands.md edit: docs-lab reference/skills.md
is the published page and already states the confirm-then-write contract.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs(changeset): drop em dashes from the release note

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 16:35:49 +00:00
8fc65b7f70 fix(tasks): count checkboxes under every list marker (#1862)
* fix(tasks): count checkboxes under every list marker

The task counter behind list, status, view, instructions apply,
validate --archived and archive's incomplete-task check read only `-`
and `*` bullets. A GFM task is a list item, and CommonMark also allows
`+` and ordered `1.` / `1)` markers, so unchecked ordered or plus tasks
were dropped from every count: the change read as complete and archive
skipped its incomplete-task warning.

Accept every CommonMark list marker in TASK_LINE_PATTERN, keeping its
existing tolerances (indentation, CRLF, a missing space after the
marker). Every consumer goes through parseTaskLines, so this one change
fixes them all, and task-numbering validation now sees these tasks too.

* docs(tasks): list every counted checkbox marker in schema.yaml reference

The apply.tracks section listed only - and * checkbox forms. Task lines under + and ordered markers now count, so show them and name the marker rule. Also use American spelling in the changeset.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Clay Good <hi@claygood.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 16:21:23 +00:00
Clay GoodandClaude Opus 5 92fb72d1dc fix(workflows): create the main spec when a capability is new (#1701)
* fix(workflows): create the main spec when a capability is new

The agent-driven archive workflow told agents to "compare each delta spec
with its corresponding main spec" and said nothing about the case where
that main spec does not exist yet. Comparing against nothing reads as
"already synced", so the agent took the archive branch and the new
capability's main spec was never written — the change landed in
changes/archive/ with openspec/specs/ still empty.

`openspec archive` already handles this: buildUpdatedSpec creates the spec
from the delta's ADDED requirements, rejects MODIFIED/RENAMED with "only
ADDED requirements are allowed for new specs", and warns past REMOVED. The
guidance now says the same thing, so the agent path and the CLI path agree:

- archive-change: a missing main spec counts as changes needed and is named
  in the summary as a spec the sync will create — never as already synced.
- sync-specs: MODIFIED and RENAMED have no requirement to act on when the
  main spec is absent, so the sync stops and reports rather than inventing
  one; REMOVED is skipped with a warning.

Guidance text only — no CLI, parser, or archive behavior changes.

Closes #1222
Closes #1264

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(sync): never create a main spec with nothing to put in it

CodeRabbit caught a gap in the previous commit: step 4b now tells the agent
a REMOVED-only delta has nothing to remove, but step 4d still read as
"create the main spec if the capability doesn't exist yet" unconditionally.
Following both would write a spec whose `## Requirements` section is empty.

Verified against the CLI on a REMOVED-only delta targeting a capability with
no main spec:

    Specs to update:
      parking: create
    ⚠️  Warning: parking - 1 REMOVED requirement(s) ignored for new spec.
    Validation errors in rebuilt spec for parking (will not write changes):
      ✗ Spec must have at least one requirement
    Aborted. No files were changed.

So step 4d is now gated on the delta having ADDED requirements to seed the
spec with, and says what the CLI reports when it does not.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs: define "main spec" and close the no-ADDED archive loop

Hardening pass over the two fixes in this branch.

Guidance: the archive step's verification pass re-runs the same comparison
the fix touched, so a delta that can create nothing — no ADDED requirements,
no main spec to merge into — would have been reported as "still needs sync"
after a sync that correctly created nothing, and an agent could loop on it.
That case now short-circuits with the reason, matching `openspec archive`,
which refuses it with "Spec must have at least one requirement".

Docs: the glossary defined "delta spec" but never "main spec", which is
half of #1647's terminology complaint. It now defines the term and says
that for a new capability the main spec is created by the archive rather
than written up front; concepts.md says the same in the delta-section table
and the archive process. The docs site generates from docs/ at build time,
so no website files change.

Changeset rewritten in the house style (prose, no commit header; the
changelog-github action supplies attribution) and renamed descriptively.

All three CLI branches this guidance describes were verified end to end:
ADDED against a greenfield repo creates the spec and carries its Purpose;
MODIFIED reports "target spec does not exist; only ADDED requirements are
allowed for new specs"; REMOVED-only aborts with "Spec must have at least
one requirement" and writes nothing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs(glossary): a standalone sync creates the main spec too

The "Main spec" entry said the spec is created "by the archive", but the
"Sync" entry two sections down says /opsx:sync creates it as well, without
archiving — and specs-sync-skill's "New capability spec" scenario is the
sync's own behavior. Names both paths so the two entries agree.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(workflows): clarify removed deltas for missing specs

* fix(workflows): preserve explicitly retired missing specs

* fix(archive): preserve explicit skip-sync choice for blocked deltas

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 15:47:51 +00:00
Clay GoodandClaude Opus 5 4c369e022b fix(explore): make the capture request the write confirmation (#1832)
* fix(explore): make the capture request the write confirmation

Explore's write-confirmation rule named `openspec new change` as an action
requiring a separate yes/no, while the capture branch told the agent to
transition "seamlessly" into running it. Both readings were defensible, so
the same request either wrote files immediately or stopped and asked.

State the resolution in all three places: an explicit capture request is
the confirmation, for the change and artifacts that request names. The
guardrail keeps its teeth where #1715 reported the problem — an
agent-proposed capture, or work beyond the requested scope, still asks.

Closes #1828

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* test(explore): guard the capture branch against a re-added confirmation gate

Also disambiguate the scope fence in the IMPORTANT block: "the artifacts
that request names" parses as a relative clause, and it is the sentence an
agent weighs first. Match the article used by both restatements.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(explore): put the capture discriminators where the decision is made

Four hardening findings from review of the first pass:

- A yes to an offer the agent made looks identical to a user-initiated
  capture request at the point the branch decides. The discriminator sat
  190 lines away in Guardrails. Move it into the branch, and require the
  offer to name what it would create.
- "Do not ask for a second confirmation" contradicted step 2 nine lines
  below it, which requires asking before expanding the capture. Narrow it
  to re-asking for what was already asked for.
- Scope the carve-out to change artifacts, so it cannot be read to reach
  the workflow configuration #1715 reported an agent editing.
- The Guardrails bullet restated the whole contract a third time, in a
  quick-reference list whose next-longest entry is 43 words. Replace with
  a pointer to the branch that owns it.

Tests: the three not.toContain guards could not see a gate phrased in
words they did not anticipate. Replace with a structural check that
collects every consent-bearing sentence and requires each to be sanctioned
— inside the capture branch with no topic filter, since a gate written
there is about the capture whether or not it says so. Mutation testing:
kills 6 of 8 contradiction mutations that survived before, and all three
sites stay independently pinned. The two survivors reverse the resolution
without any consent word and are noted as review-only.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(explore): keep the contact clause in the scope fence

The previous commit reintroduced "the change artifacts that request
names", the garden-path parse that 55eeac6 removed: read as a relative
clause it says the artifacts name a request. Restore "the request names",
matching both restatements. Caught by CodeRabbit.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs(changeset): drop em dashes from the release note

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 15:47:47 +00:00
8146be5546 fix(list): skip unresolvable entries when dating changes (#1866)
* fix(list): skip unresolvable entries when dating changes

To sort changes by recency, list stats every file inside each change,
and any entry it could not stat failed the whole command. A dangling
symlink, such as the .#file lock Emacs keeps beside every file with
unsaved edits, or a symlink loop made list exit 1 and list --json report
no changes at all.

Skip an entry that no longer resolves (ENOENT or ELOOP) when computing a
change's last-modified time. Valid symlinks are dated as before, and any
other error still fails the listing.

* test(list): pin that a permission error still fails the listing

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Clay Good <hi@claygood.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 15:47:46 +00:00
72bf7600a5 fix(completion): restore .bashrc byte for byte on bash uninstall (#1872)
* fix(completion): restore .bashrc byte for byte on bash uninstall

Uninstall removed the OpenSpec block but kept the blank separator line
install adds after it at the top of .bashrc, then popped every trailing
blank line and wrote the file back without its final newline. The next
tool to append with >> (nvm, conda, rustup) merged its first line into
the user's last line.

Drop only the separator line install added when the block sits at the
top, and leave the rest of the file, including its final newline,
trailing blank lines and CRLF line endings, as it was.

* test(completion): cover content right after the block and CRLF without final newline

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Clay Good <hi@claygood.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 15:47:43 +00:00
388d34473a fix(init): keep user files in legacy command folders (#1874)
* fix(init): keep user files in legacy command folders

Legacy cleanup removed each pre-skills tool's <tool>/commands/openspec/ folder recursively whenever it existed, deleting any command the user kept there along with OpenSpec's three files. init runs that cleanup unprompted when there is no TTY, so agents and CI lost those files without --force.

Directory entries now name the files OpenSpec wrote there. Cleanup deletes only those, removes the folder only once nothing else is left in it, and reports each entry it kept. A folder holding none of OpenSpec's files is no longer treated as legacy, and a folder holding only them is removed exactly as before.

* docs: drop legacy migration-guide edit from legacy cleanup fix

docs/ is legacy; the canonical docs-lab page (help/legacy/migration.md) is
still a skeleton, so there is nothing to update there yet.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(init): recognize legacy command files by their OpenSpec markers

Legacy cleanup treated any regular file named proposal/apply/archive in a
<tool>/commands/openspec/ folder as OpenSpec's, so a user-authored file
with one of those names, including one swapped in while the upgrade prompt
waited, was still deleted.

Every legacy slash command was generated with the OpenSpec markers, and
OpenSpec refused to update one without them. A file now counts as
OpenSpec's only when its content still carries them, and cleanup checks
that again immediately before each unlink. A symlinked command folder is
never followed. Test fixtures now use marker-wrapped content like the real
generated files.

Closes #1873

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(init): recheck each legacy command file before the directory cleanup deletes it

The directory cleanup loop classified a folder's managed files once and then
unlinked every one of them. A file the user swapped in after that scan was
deleted and reported as deleted. Each file is now checked for the OpenSpec
markers immediately before its unlink; a file that fails the check is kept
and reported as kept.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Clay Good <hi@claygood.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 15:47:41 +00:00
2ef6fbde3d fix(config): run an EDITOR that carries arguments (#1878)
* fix(config): run an EDITOR that carries arguments

config edit passed the whole EDITOR/VISUAL value to spawn as the program
name with shell: false, so common settings such as `code --wait` failed
with ENOENT, and the uncaught rejection printed a raw Node stack trace.

Run the value the way git does: through sh -c '<editor> "$@"' with the
config path as a positional argument, and through cmd.exe on Windows so
.cmd shims resolve. A value that is itself the absolute path of an
existing file still runs directly, so unquoted paths with spaces keep
working. A failed start, a non-zero exit or a signal is now reported as
a one-line error and the command exits 1.

* fix(config): split EDITOR into argv instead of running a shell

Run the editor without a shell. The EDITOR or VISUAL value is split into a
program and arguments (double quotes everywhere; single quotes and backslash
escapes on POSIX; literal backslashes on Windows) and the config path is
appended as its own argument, spawned via cross-spawn with shell: false so
Windows .cmd shims such as code.cmd still resolve. Shell metacharacters in
the value are now inert.

Show the install hint only when the program is missing (ENOENT), not for
EACCES or EPERM. Move the EDITOR documentation from legacy docs/cli.md to
docs-lab/reference/cli.md.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Clay Good <hi@claygood.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 15:47:38 +00:00
9f8dec5dd9 fix(store): refuse to remove a store that contains another store (#1880)
* fix(store): refuse to remove a store that contains another store

store remove deleted the target folder recursively after checking only
the target's own metadata. Another registered store living inside that
folder, such as a shared store vendored as a git submodule (a layout
store register accepts), was deleted with it, uncommitted work
included, and its registry entry was left pointing at a missing path.

Refuse with store_remove_contains_registered_store when another
registration's canonical root is inside the folder. The check runs in
the beforeCommit hook, under the registry lock that commits the
removal, which now receives the registrations that will remain.

* docs(store): move nested-store remove refusal to docs-lab

Document store_remove_contains_registered_store on the canonical docs-lab CLI reference instead of legacy docs/cli.md. docs/agent-contract.md keeps the error-code entry.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Clay Good <hi@claygood.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 15:47:36 +00:00
208b5b5510 fix(store): stop a store named specs or changes becoming the root (#1882)
* fix(store): stop a store named specs or changes becoming the root

Stores live at ~/openspec/<id>, so a store with the id specs or changes
is itself ~/openspec/specs or ~/openspec/changes. classifyOpenSpecDir
counted that folder as planning shape, which made $HOME the nearest
root for every command under the home tree: defaultStore was never
consulted and new changes were written outside the store.

A specs/ or changes/ directory that carries store metadata is a store
root, not planning content of the directory above it.

* test(store): canonicalize phantom-root identities and cover an alias path

Compare resolved root paths with fs.realpathSync.native on both sides, and add a symlinked-alias case that must resolve the same canonical store root, per review.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Clay Good <hi@claygood.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 15:47:33 +00:00
5d221456e5 fix(store): let setup --no-init-git run inside a git repository (#1884)
* fix(store): let setup --no-init-git run inside a git repository

The nested-repo guard refused a setup path inside another Git
repository even with --no-init-git, which creates no repository, so a
store at ~/openspec/<id> was impossible for anyone whose home directory
is a dotfiles repo. The CLI never passed the existing bypass either:
prepareSetupInput ignored its options.

Skip the guard when initGit is false, and forward --init-git and
--no-init-git into the prepare step. The default setup and an explicit
--init-git still refuse. Two store.test.ts cases passed --no-init-git
while asserting the refusal; they now run the default setup the guard
exists for.

* docs(store): move setup nested-repo note to docs-lab

The setup --no-init-git explanation belongs on the canonical docs-lab CLI reference, not legacy docs/cli.md.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* test(store): compare canonical paths in setup --no-init-git test

Canonicalize payload.store.root and the registry local_path with fs.realpathSync.native before comparing, per review.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Clay Good <hi@claygood.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 15:47:31 +00:00
e01ed070f1 fix(archive): refuse delta files the merge path never reads (#1870)
* fix(archive): refuse delta files the merge path never reads

validate and archive read a change's deltas only from specs/<capability-path>/spec.md, but the spec-driven artifact graph counts any markdown file under specs/ as written. A delta at specs/user-auth.md was reported done by status and ready by apply, rejected by validate only as having no deltas, and then archived with exit 0 and nothing merged.

Name every markdown file under specs/ that carries delta sections but is not a capability's spec.md. validate reports it as an error with the spec.md its requirements belong in, archive runs that validation and refuses the change, and apply lists it in its warnings. --no-validate and changes with no spec files behave as before.

* docs(schemas): move delta file placement note to docs-lab

The legacy docs/ tree is frozen; the canonical page for the spec-driven
delta layout is docs-lab/reference/schemas/spec-driven/index.md.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Clay Good <hi@claygood.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 15:47:26 +00:00
Dwin Gharibi db560ae33f fix(validate): reject a scenario header with no body (#1858)
A requirement whose only scenario is a header with nothing under it passed validate and was then refused by archive. The delta scenario counter counted every #### header, while the spec path that archive uses to validate the rebuilt spec keeps a scenario only when its body has content, so the two verdicts disagreed and the archive error did not name the requirement.

Share one rule, hasScenarioBody, between both paths and read a scenario body up to the same boundary the spec path uses, so validate rejects exactly what archive rejects. The error names the requirement and explains that a header with no body does not count.
2026-09-16 15:47:24 +00:00
46ff91f2d6 fix(parser): read change deltas with the archive reader (#1856)
* fix(parser): read change deltas with the archive reader

`show --json --deltas-only`, the command OpenSpec's error text recommends for inspecting parsed deltas, read deltas through ChangeParser's own section lookup instead of parseDeltaSpec, the reader archive applies. A bullet-form REMOVED was invisible to it, so it fell back to the proposal's What Changes prose and reported an invented MODIFIED while archive deleted the requirement. A repeated section header was read once, and a RENAMED line written with * or + was dropped.

Derive every operation from parseDeltaSpec, keeping the existing section parser for requirement text and scenarios, and describe a change that has delta spec files by those files alone. A change with no delta spec files still falls back to the What Changes bullets.

* fix(parser): keep the prose fallback for legacy spec files with no delta section

A change whose specs/ held full future-state specs (no ADDED/MODIFIED/REMOVED/RENAMED section) lost its What Changes deltas: show --json reported none, change list counted zero, and archive printed an extra No deltas found warning. The prose fallback now applies whenever no spec file carries a delta section, which is exactly main's behavior for such changes, while a bullet-form REMOVED (which has a REMOVED section) is still read from the delta file.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Clay Good <hi@claygood.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 15:47:22 +00:00
Dwin Gharibi 4b5c07a0c2 fix(parser): drop closing hashes from requirement names (#1860)
CommonMark lets an ATX heading end in a closing run of `#`s, so
`### Requirement: Late Fees ###` renders as `Late Fees`. Requirement
names kept the run, so a REMOVED written that way missed the requirement
and archive exited 0 with a false "treating it as already removed"
warning, while a closed MODIFIED or RENAMED heading failed as not found.

normalizeRequirementName now strips the closing run with the rule scenario
names already use: only a run preceded by a space or tab counts, so `C#`
keeps its `#`. Every reader goes through it, and the main-spec duplicate
check now does too, so a closed and an open heading of one requirement are
reported as duplicates.
2026-09-16 15:47:19 +00:00
Dwin Gharibi 767d63c926 fix(archive): refuse requirement names differing only in case (#1864)
ADDED and the RENAMED target compared requirement names exactly, while
REMOVED and the RENAMED source already treated a name that differs only
in case or interior whitespace as a mistyped header. An ADDED `late fees`
beside an existing `Late Fees`, or a rename to `LATE FEES`, therefore
archived cleanly and left two contradicting copies of one requirement in
the main spec, which validate then accepted.

Both now refuse with an error naming the existing requirement. The source
of a rename is exempt from the target check, so a case-only rename of a
requirement to its own name still applies, and ADDED is still checked
against the spec as it stands after the earlier operations, so a variant
of a requirement the same delta removes or renames away is allowed.
2026-09-16 15:47:17 +00:00
6e62b1d522 fix(parser): refuse malformed RENAMED pairs (#1806)
* fix(parser): refuse malformed RENAMED pairs

* docs(test): document RENAMED pairing test helpers

* test(parser): pin RENAMED pairing across header copies and bullet markers

Cover the two shapes main gained after this branch was cut: a FROM in one
`## RENAMED Requirements` copy and a TO in another are both reported as
unpaired, and unpaired lines written with `*` or `+` are reported like
`-` ones. Add a patch changeset matching the other parser fixes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Clay Good <hi@claygood.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 15:47:15 +00:00
Clay GoodandClaude Opus 5 e571b5b9ae fix(security): harden CLI against hostile-repository inputs and clear dependency advisories (#1835)
* fix(deps): upgrade vitest to 4.1.11 and override fflate

Clears GHSA-82fw-gwwq-j7x9 (path traversal / arbitrary file read via
@vitest/mocker redirect mock), the subject of all three open Dependabot
alerts. No patched 3.x exists — 4.1.11 is the first fixed release — so the
major bump is unavoidable.

Two things the plain Dependabot bump (#1823) got wrong, which is why its
tests failed on every platform:

- It left @vitest/ui on 3.x, which dragged vite/esbuild to 0.28.2 and broke
  the allowBuilds pin assertion in pnpm-workspace-config.test.ts. Upgrading
  @vitest/ui in lockstep keeps esbuild on 0.28.1.
- Vitest 4 no longer lets an arrow function stand in as a constructor
  implementation, so the ZshInstaller module mock threw "is not a
  constructor" across 8 completion tests. Converted the three mock factories
  to function expressions.

Also adds a pnpm override for fflate (GHSA-px8p-9vwx-vf98, infinite loop on
malformed ZIP64), which @vitest/ui 4.1.11 still pulls at 0.8.2.

`pnpm audit` is now clean: 0 vulnerabilities across all severities.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(core): bound schema size and stop prototype keys shadowing worksets

Two low-severity robustness defects found during the security review.

Workset lookups tested membership with `state.worksets[name] !== undefined`
on a plain-prototype object. `constructor`, `toString`, `valueof` and
friends are all valid kebab ids, so `openspec workset add constructor`
reported "already exists" against empty state, `getWorkset` returned a
function off the prototype, and `withoutWorkset` took the found branch for a
workset that was never there. All three sites now use `hasOwnProperty`.
This was never prototype *pollution* — nothing is written through these
keys and Zod's `z.record` drops `__proto__` — only a correctness defect.

`SchemaYamlSchema.artifacts` was unbounded while `validateNoCycles` walks it
with a recursive DFS, so a project-local schema declaring a long `requires`
chain crashed the CLI with an uncaught `RangeError: Maximum call stack size
exceeded` instead of a validation error. Capped at 1000 artifacts, which
also bounds the reference-resolution and graph work.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(security): stop repo-supplied text forging agent instructions

OpenSpec prints a pseudo-XML envelope that an AI coding agent consumes as
instructions, and interpolated repo content went in raw. The tags carry
authority - <project_context> means "background only", <task> means "do
this" - so a value that closes its own block is promoted from data to
directive.

Confirmed against a fresh build: a config.yaml `context:` value containing
`</project_context><system_override priority="critical">` landed a top-level
override block outside every "do NOT treat as instructions" guard. The same
breakout worked from `rules`, `description`, a dependency description, and a
schema `instruction`. A change directory name containing a quote forged
attributes on the <artifact> tag. In markdown output, a `context` line
starting with `##` forged a peer of the printer's own headings.

src/core/references.ts already had sanitizeInline written for exactly this
threat, documented as such, and simply was not applied here - it also only
flattened newlines, which one line of markup is enough to defeat. Extended it
and added three siblings beside it: escapeEnvelopeText, escapeEnvelopeAttribute,
and escapeEnvelopeCloseTags for content that must stay verbatim.

Template bodies deliberately get only their closing tags neutralized: the
shipped templates are full of `<!-- ... -->` comments and <placeholder>
markers that are copied into the generated artifact, so blanket escaping
would write &lt;!-- into every file. A block can only end at a closing tag,
so that is the load-bearing control.

Rules and operation guidance are flattened but explicitly not truncated -
they are instructions an agent must follow in full.

Separately, `openspec update` decided skill freshness from the generatedBy:
line alone and never compared bodies, so appending a step to a generated
SKILL.md still printed "All 1 tool(s) up to date". Skills now get the same
byte-comparison command files already had, and the plan names the reason.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(security): remove super-linear scans and tighten write containment

Three regexes ran over whole repository files with a `\s` class that crosses
newlines under the `m` flag, so `^\s*` re-scanned from every line start. The
blowup is in the *failing* scan - a file with no `generatedBy:` line at all -
which is also the realistic attack file. Measured on this machine:

  extractGeneratedByVersion, 63 KB whitespace SKILL.md   3,103 ms -> <5 ms
  legacy-skill compare, 63 KB whitespace frontmatter     8,131 ms -> <5 ms
  buildUpdatedSpec, 195 KB of `<!--` openers             6,817 ms -> ~90 ms

The first two are reached by `openspec update`, the first command run after
cloning. The third is reached from extractPurposeSection during `openspec
archive`, on the write path.

The scans now use `[ \t]` and walk lines, and maskHtmlComments is an indexOf
scan that visits each character once instead of a lazy regex that re-scans to
EOF from every `<!--`. Both rewrites were fuzzed against the originals -
200,000 random inputs each, 0 mismatches - so the `--!>` terminator and the
"unterminated comment runs to EOF" rule from #1413 are preserved exactly.

resolveTrustedSpecPath treated a failed containment check as permission to
re-root trust on the symlink's own target, on the theory that monorepo
symlinks may be intentional. A repo shipping openspec/specs/<cap> as a link
out of the tree therefore got `openspec archive` to write attacker-controlled
markdown to <external>/spec.md while printing the in-project path. The
fallback root must now still be inside the project, matching retireSpec,
which already refused to delete an external target.

Also: `validate <id> --type spec|change` short-circuited the name guard that
`show` applies, so a traversing id reached a bare path.join; and markTipSeen
wrote the global config through a predictable <config>.<pid>.tmp at default
0644 instead of the repo's existing writeFileAtomically (random name, 0600).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(security): harden subprocess and shell-config write paths

Defense in depth. The audit confirmed there is no shell injection anywhere in
src/ - no `shell: true`, no user value concatenated into a command line - so
none of these are live exploits; they are the sharp edges next to that line.

Completion install wrote the completions directory into .bashrc/.zshrc inside
double quotes, so a `$(...)` or backtick in HOME/XDG_DATA_HOME became command
execution on every future shell start. Both installers now single-quote the
path through a shared helper.

`feedback` shelled out for two probes (`which gh`, `gh auth status`) directly
alongside free-form user title and body text - the most plausible site for a
future injection regression. Both are execFileSync now, behavior unchanged.

Git subprocesses inherited the default 1 MB maxBuffer with no timeout, so a
large dirty tree made `git status --porcelain` throw ENOBUFS, which gitProbe's
bare catch turned into "no git facts" - `openspec doctor` then silently stopped
reporting uncommitted changes. They now share GIT_EXEC_OPTIONS (15s timeout,
16 MB buffer) the way readCliVersion already did, and the catch distinguishes
a resource failure from "git absent" so the degraded path is no longer silent.

The GITHUB_OUTPUT heredoc in validate-changesets used a fixed EOF delimiter
over a list of PR-authored paths; it is now run-unique.

Finally, both package.json files still carried a `pnpm` block. pnpm 10 uses
that block *instead of* pnpm-workspace.yaml rather than merging with it, which
is exactly the override-displacement trap dependabot.yml documents as #1812 -
and it is where Dependabot writes when it bumps an overridden package. The
block only duplicated `allowBuilds`, so removing it leaves both lockfiles
byte-identical with every advisory override intact, and denies Dependabot the
block to write into. The workspace test now asserts `pnpm` is absent entirely.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(security): pin the update check to TLS and honor telemetry opt-out

The update check asked whatever `npm_config_registry` named, over any
protocol. A code comment asserted that file contents deliberately cannot
choose the destination; that was not true. npm exports every config source it
reads, including a `registry=` line in a repository-local .npmrc that travels
with a clone - reproduced: `registry=http://169.254.169.254/` came straight
through `npm run`.

That is a cleartext GET at an address of the repository's choosing, and it
escalates. The attacker's reply says `{"version":"99.0.0"}`, which triggers
the upgrade prompt whose default is Yes; accepting runs `npm install -g`,
which npm resolves against that same attacker registry. Cloning a repository
and answering one prompt installs an attacker-chosen global package.

Both halves are now closed. The registry override is honored only over https,
falling back to the public registry otherwise, and canSelfUpgrade() refuses
when the resolved registry is not the public origin - a private mirror can
still inform the check but can never drive an install prompt. Redirects must
stay https and on the origin resolved up front, not merely the previous hop,
so no single reply can steer the request elsewhere. The comment now describes
what the code actually guarantees.

Separately, the opt-out env vars were exact-string matches, so DO_NOT_TRACK=true
and OPENSPEC_TELEMETRY=false both silently left telemetry ON - the spellings a
user is most likely to reach for, and inconsistent with the tolerant
isCiEnvironment() helper beside them. Parsing is now tolerant, shared between
both call sites, and fails safe: an unparseable value suppresses the request.

The first --json run also sent an event before the disclosure was ever shown -
the notice is correctly deferred so it cannot corrupt machine-readable output,
but trackCommand fired regardless, and agent-driven --json may be a user's only
mode. No event is sent and no anonymous id is created until the notice has
actually been printed.

No existing guard was weakened: the 256 KB body cap, 3-redirect cap, single
budget timer, strict version regex, argv-based spawn, CI/test guards and the
four-field payload are untouched.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(security): make the close-tag escape linear

CodeQL js/polynomial-redos on the escape added in a563b0591 - and it is the
same defect class this PR set out to remove, in the fix for it.

`/<\/[A-Za-z][^>]*>/g` scans for a closing `>` from every `</`, so a template
of `</A` repeated is quadratic. Measured before: 20 KB 274 ms, 40 KB 1,223 ms,
80 KB 3,917 ms. Reachable, because escapeEnvelopeCloseTags is applied to
`template`, which is repo-controlled.

Rewritten to rewrite the `</` opener alone. The escape only ever swapped the
`<`, so for a well-formed tag the output is byte-identical - verified across
200,000 fuzzed inputs, with zero cases where the new form escapes fewer
closers than the old. It needs no scan at all (2 MB in 35 ms) and additionally
catches a closer whose `>` never arrives.

The other regexes added by this PR were re-checked the same way and are all
linear.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* chore(nix): refresh pnpmDeps hash for the upgraded lockfile

The flake pins a fixed-output hash over pnpm-lock.yaml, so it goes stale on
any lockfile change - here the vitest 4.1.11 upgrade and the fflate override.
Hash taken from the Nix Flake Validation job, which builds specifically to
report the correct one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(completions): quote the completions dir in printed instructions too

Two review findings.

CodeRabbit caught that only the auto-configured rc block was quoted. When
auto-configuration is off or fails, the installer prints the same lines for
the user to paste into their own rc file - and those were still interpolated
raw, so an expansion in HOME/XDG_DATA_HOME runs on every future shell start
exactly as it would have from the written block. The zsh fpath line was worse
than the bash one: not even double-quoted, so an ordinary space broke it.
Both now go through the same shellSingleQuote helper, with coverage that
exercises the auto-config-disabled path.

Windows CI also failed on a test of this PR's own: it created a change
directory literally named `x"  IGNORE-PREVIOUS  y="`, and Windows forbids `"`
in a filename. The end-to-end vector therefore does not exist on Windows, so
that case is skipped there and the escape itself is now unit-tested on every
platform.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(security): narrow envelope escaping to the tags that carry authority

The first pass escaped every `&`, `<` and `>` in repo-supplied text. A
regression review found that mangles ordinary content for every user - and
worse, OpenSpec's own shipped spec-driven schema, which writes
`### Requirement: <name>`, `specs/<capability-path>/spec.md` and
`openspec show "<spec-id>"` on eleven lines. Agents were reading OpenSpec's
own format guidance as `### Requirement: &lt;name&gt;`. Ordinary `context:`
values suffered the same way: `R&D`, `pnpm build && pnpm test`,
`Result<T, E>`, `2> api.log`.

Only a fixed vocabulary is neutralized now - the tags the printer actually
uses to frame its blocks - in both their opening and closing forms, with
attributes. That is the entire breakout surface: a block ends at its own
closing tag, and a forged opener only carries authority if it names one of
these. Everything else reaches the agent exactly as written. Verified by
rendering the real spec-driven instructions: no entity encoding anywhere.

Escaping both forms is also stronger than the first pass in one respect - it
neutralizes a forged `<task priority="highest">` opener, which the earlier
close-tag-only rule for templates let through.

Markdown heading escaping is dropped entirely. It fired inside fenced code
blocks, so a `# install deps` in a project's context became `\# install deps`
for everyone, and it defended a markdown surface with no envelope to break out
of. Guidance entries are still flattened, so the one-line forgery is still
blocked; a multi-line `context:` can add a heading inside its own labelled
block, which is an accepted limit now recorded in the test.

sanitizeInline goes back to flattening only, so JSON output stops
entity-encoding spec Purpose lines.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(security): repair four regressions the hardening introduced

A regression review of this PR found four ways the fixes broke legitimate
behavior. All are confirmed and reproduced.

**validate rejected every nested spec id.** The new name guard sits in
validateByType, which is the funnel for three entry paths, not just --type.
Nested capabilities (specs/<area>/<capability>/spec.md, #1353) have ids
containing `/`, so `openspec validate platform/session-layout` started
failing - including the exact command `validate --specs` prints as its own
hint. The guard now runs per path segment, so `..` and backslashes are still
refused while nested ids pass.

**git writes could wedge a user's repository.** GIT_EXEC_OPTIONS was applied
to `init`, `add`, `commit` and the rollback `rm --cached`, with
killSignal SIGKILL. git traps SIGTERM to remove .git/index.lock on its way
out; a signal it cannot catch leaves the lock behind, so every later git
command in the store fails with "Another git process seems to be running" -
including the best-effort unstage, which runs in exactly that case. 15s was
also too short for a signed commit waiting on pinentry. Writes now have their
own bounds: no hard kill, 120s.

**A private registry silently became the public one.** Rejecting a non-https
registry fell back to registry.npmjs.org, which sends the request an internal
mirror deliberately avoided and reports a version resolved against a registry
the eventual `npm install -g` does not use. A rejected registry now disables
the check instead, and isDefaultRegistry reads the raw env var so it still
disqualifies a self-upgrade.

**Redirects were pinned to one origin**, which killed the mirror and corporate
front-end case the redirect support exists for. Cross-host is allowed again;
leaving TLS is not.

Also: an external capability symlink is no longer refused. Two places in this
codebase document such links as intentional monorepo layout, so refusing them
broke a supported setup. The real defect was silence - the CLI reported the
in-project path while writing elsewhere - so the write proceeds and names its
actual destination.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* test: make the new security tests portable and discriminating

A test-quality audit of this PR's own tests.

The ReDoS bounds passed on reverted code far too easily - discrimination was
only 1.9x, 3.0x and 2.6x, so a full revert could slip through on a fast
machine. These scans are quadratic, so the hostile inputs are now large enough
to separate the two decisively: 32x, 32x and 10.6x, with the fixed code still
running in milliseconds against a 500-800ms bound. Comments that cited
invented pre-fix timings are replaced with measured figures or a plain
statement of the complexity.

git-probe-limits wrote 6000 files with 245-character basenames, putting the
absolute path past MAX_PATH on a windows-latest runner - and it sits in
beforeAll, so the whole file would have died there. 120-character names x
12000 files keeps porcelain output over the 1 MB threshold at ~190-character
paths. This is the same class of defect as the Windows failure already fixed
in this PR.

validate.name-guard built its fixture at process.cwd(), which is not
gitignored; a security test should not leave files in the working tree.

The worksets test looped over three names but only `constructor` is actually
on Object.prototype and a legal id, so two thirds of it passed unchanged on
main. `__proto__` is not reachable - isKebabId rejects underscores - and both
facts are now stated rather than papered over.

Adds the missing coverage for the git timeout half of the exec hardening,
against synthetic error shapes rather than a 15-second sleep, including the
negative cases that keep the classifier honest.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* test: write the git probe fixture without exhausting file descriptors

Sizing the fixture up to 12000 files to keep porcelain output over 1 MB made
the writes EMFILE on the macOS and Windows runners - 12000 concurrent
fs.writeFile handles is well past their descriptor limit, and the failure took
the whole beforeAll with it. Written one at a time instead; the hook still
finishes in about a second.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs(instructions): correct a comment that claimed a dropped heading guard

The operation-inputs printer never escaped leading '#'; that escaping was
removed because it fired inside fenced code. The comment still described it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(archive): name the capability in the external-link warning

The warning printed 'spec.md' for every external capability, so the
link could not be identified. Report the capability directory, and assert
the full platform-specific completion lines in the bash and zsh fallback
instruction tests.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(security): escape envelope tags split by line-break whitespace

ENVELOPE_TAG only accepted a space or tab between the tag name and `>`,
so a multiline repo value such as `</project_context\n>` closed the
context block and could forge a top-level `<task>`. Use `\s`; the
`[^<>]` tail keeps the match linear. Adds regressions for split closing
and opening tags and a timing guard on multiline openers.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 15:47:12 +00:00
Clay GoodandClaude Opus 5 3b8e5b6616 fix(nix): install shell completions with the flake package (#1785)
* fix(nix): install shell completions with the flake package

The flake exposed `openspec completion generate SHELL` but installed no
completion scripts, so a Nix install had no completions at the standard
locations. Generate the Bash, Fish, and Zsh scripts during postInstall and
place them with installShellCompletion.

Closes #1740

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs: note that cross-compiled Nix builds omit completions

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* ci: run the Nix job when the completion generator changes

The Nix build now runs `openspec completion generate`, so a change under
src/core/completions can break packaging without touching flake.nix.

Also drops the cross-compilation caveat from the Nix install docs: every
package this flake exposes is native (`legacyPackages.<system>` has
buildPlatform == hostPlatform), so `canExecute` is always true and the
completions are never omitted.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs: move the Nix completions note to the canonical pages

docs-lab/README.md makes docs-lab/ canonical and the old docs/ tree legacy, so
the fact is recorded in the two docs-lab pages that own it and docs/cli.md and
docs/installation.md are back to their state on main.

- docs-lab/start/installation.md, Nix: the package ships the scripts at the
  standard locations, so completion install is not needed.
- docs-lab/reference/cli.md, openspec completion: the same exception, stated
  where a reader looking up the command will hit it, linking to the Nix
  section rather than repeating the paths.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 15:47:11 +00:00
Clay GoodandClaude Opus 5 7de24044ef fix(init): make the universal tool target findable in the picker (#1778)
* fix(init): make the universal tool target findable in the picker

Closes #653

`openspec init`'s tool picker is a searchable list of product names. The
vendor-neutral target every unlisted assistant is meant to use was named
"Shared .agents skills" — after the directory it writes, which is not a
word anyone in that position searches for. Typing "universal", "other" or
"generic" returned "No matches", so the escape hatch was unreachable and
the reporter had to open an issue to find it.

Rename the entry to "Other / Universal (shared .agents skills)" and give
choices optional `searchAliases` the filter also matches. The picker also
dropped every non-alphanumeric keystroke: readline reports punctuation
only in `key.sequence`, leaving `key.name` undefined, so ".agents" and
"amazon-q" could not be typed at all.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(init): point at the universal target when a tool search matches nothing

"No matches" is where someone whose assistant is not on the list gives
up — the picker knows the answer and does not say it. Add an optional
`emptyHint` to the searchable multi-select, and have init name the
vendor-neutral entry there.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(init): close every dead end that hides the universal tool target

Hardening pass over the same defect. Reviewing the first fix turned up
four more places the answer was withheld:

- `openspec update`'s tool picker builds its own choices and never
  passed searchAliases through, so the same search failed there.
- `--tools <unknown>` printed a bare list of ids. It now names the
  fallback, the scripted counterpart of the picker's empty hint. The
  hint I first put on validateTools sat on an unreachable branch; the
  path users actually hit is the "Invalid tool(s)" parse error, and a
  test now pins it.
- The search box dropped pasted text as well as punctuation. Any
  sequence whose characters are all printable is now accepted, which
  also lets a space reach the box so "claude code" filters. Escape
  sequences carry control characters and are still rejected, and the
  `name` fallback stays single-character so readline names like 'tab'
  are never typed.
- docs-lab still taught the old label in two places.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs: keep the FAQ a router and finish the alias list

- help/faq.md is a one-liner surface (README's "FAQ is one-liners" rule),
  so the answer points at the support matrix's Other / Universal section
  instead of restating the picker's search terms. Drops the em dash that
  writing.md forbids.
- reference/supported-tools.md keeps the search terms, now all nine the
  picker actually matches: `vendor-neutral` and `agents.md` were added to
  searchAliases after the first draft of this page. Same correction in the
  legacy docs/supported-tools.md paragraph.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* chore(init): keep the tool-not-listed hint ASCII

The non-interactive fallback hint is new terminal output and carried an em
dash, an ambiguous-width glyph in the class #983 covered. A colon reads the
same and cannot misalign a terminal.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs: drop the legacy docs/ tool-matrix edit

docs-lab/reference/supported-tools.md already carries the label and search terms.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* test(init): prove picker punctuation through real readline key events

The prompt tests fed a multi-character sequence that readline never emits:
it splits a paste into one key per character, so a pasted space still
toggles. Drive the handler with real emitKeypressEvents output (fails on
main with 'amazonq'), pin the space limit, and stop claiming multi-word
paste in the changeset and code comment.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 15:47:10 +00:00
Clay GoodandClaude Opus 5 8b99c07bd0 fix(status): name the command that resumes a change (#1786)
* fix(status): name the command that resumes a change

`openspec status` printed the artifact checklist and stopped. The command
that moves the change forward was computed already and shipped in the JSON
`nextSteps` sentence, but the text surface never rendered it, so anyone
resuming a change had to know the next command by heart.

Extract `resolveNextStep` so the command and the published sentence come
from one place, and print it as a `Next:` line — matching the idiom
`openspec new change` already uses to hand off to `openspec status`.

The completion case matters most: "All planning artifacts complete!" reads
as "you are done" even while implementation tasks remain, and it is now
followed by the `openspec instructions apply` command that resumes the work.

Closes #906

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* style(status): keep the loadStatus comment on loadStatus

The store-flag note landed between the "single definition" comment and the
declaration it describes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* test(status): cover the next-step line, and record it in the spec

The behavior change shipped without the OpenSpec change this repo requires
of user-facing work, and without coverage for the paths a reviewer would
reasonably ask about.

Adds `add-status-next-step`, whose delta modifies `Next Artifact Discovery`
in `cli-artifact-workflow` - the requirement that already says status is how
you learn what comes next. It now also says status names the command.

Coverage added:
- Unit tests for `resolveNextStep`, pinning the published `nextSteps`
  sentences verbatim. Confirmed byte-identical to main's output, so the
  split into command + sentence provably did not reword the contract.
- Custom-schema case: the line is built from the resolved artifact id, so a
  project with neither proposal/specs/design/tasks still gets a usable
  command.
- Skipped artifacts are never named - they satisfy dependents but must not
  be created.
- `--json` stays parseable and carries no `Next:` line.
- `--all` gives every change its own line, and a failed entry none.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* test(status): assert the next-step line closes the output

The spec delta says the text output ends with the `Next:` line, but the
assertions used toContain, which a later line would still satisfy. Compare
the last non-empty line instead, across all four ready/complete cases and
the skipped one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* test(status): read the closing line CRLF-safely

Split on /\r?\n/ so a CRLF stream cannot leave a carriage return attached
to the line under comparison, and route the parity check through the same
helper instead of its own scan.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs: move the status Next: line to the canonical CLI page

docs-lab/README.md makes docs-lab/ canonical and the old docs/ tree legacy, so
the entry now lives under 'openspec status' in docs-lab/reference/cli.md and
docs/cli.md is back to its state on main.

Both documented outputs were captured from real runs rather than written by
hand: the blocked case in a change with proposal and specs, and the complete
case with all four artifacts.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs(status): drop em dashes from the changeset and proposal

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 15:47:08 +00:00
Clay GoodandClaude Opus 5 09a999bbb2 fix(changes): report a change nested in a namespace folder (#1849)
* fix(changes): report a change nested in a namespace folder

Specs can be nested by domain (`specs/mobile/tutorial-videos/spec.md`), so
laying changes out the same way looks reasonable - but a change is only ever a
directory directly under `changes/`. `changes/mobile/refresh-token/` left the
real change invisible to every command while `mobile`, the folder around it,
was reported as an ordinary task-less change. Nothing said so, and
`openspec archive mobile` moved the unfinished change into the archive under
the namespace's name with none of its deltas applied.

A directory under `changes/` is now recognised as a namespace folder when it
carries no change-root artifact of its own and wraps a directory that does.
The probe is deliberately conservative: a miss behaves exactly as before, and
an ordinary change - including a scaffolded one with no artifacts yet, and one
whose only content is a root delta spec - is never reported as a folder.

- `openspec list` marks it `not a change` and explains the flat-only layout
  instead of printing `No tasks`; `--json` gains an additive `warnings` entry.
- `openspec show` and `openspec status --change` say the same rather than
  reporting a change that has no proposal yet.
- `openspec validate` reports the nesting instead of "must have at least one
  delta", with next steps that name the fix rather than delta authoring.
- `openspec archive` refuses it outright - burying an active change is data
  loss, not something to warn about.

Closes #1846.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs(agent-contract): document the list --json nesting warning

Agents read 4.1 as the shape contract, so the additive `warnings` array and
per-entry `nested` field have to appear there, along with the instruction not
to treat a flagged entry as a change.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(changes): harden the nested-change probe and cover status --all

Adversarial review of the first commit found three holes.

A custom schema may generate every root artifact into a subdirectory
(`generates: rfc/proposal.md`). Created by hand, such a change carries no
`.openspec.yaml`, so the marker probe read it as a namespace folder wrapping a
change called `rfc` - a legitimate change that validated clean before, now
refused by validate, status, instructions and archive, with no flag to get
past it.

The same probe missed the case it was written for whenever the nested change
started from its delta specs rather than a proposal: `mobile/refresh-token/`
holding only `specs/auth/spec.md` still archived as `mobile`, deltas dropped.
A nested change is always hand-made - `openspec new change` rejects a name with
a separator - so "mkdir the tree, write the deltas first" is a common way to
arrive here.

Both come from asking one question. A directory is now recognised as a change
by a root artifact OR a populated `specs/`, and a directory holding any file of
its own is never a namespace folder. The `specs/**/*.md` shape is fixed by the
delta format rather than by the schema, which is what makes it safe to lean on.

`change show archive` also reached the probe - it has no reserved-name guard -
and offered to rename every dated archive entry into an active change. The
reserved name is rejected in the probe itself, so no caller can repeat it.

`status --all` read each directory straight through `loadStatus`, bypassing the
lookup guard, and printed a whole artifact plan for work that is not there. It
now carries the same per-change diagnostic a malformed change does, and the
sweep continues past it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* chore(changeset): state the nesting-detection bound

CodeRabbit asked for the three-level limit in the release note. Stated as a
closing sentence rather than a qualifier on the headline, so the note still
leads with what changed for users.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(changes): trust the resolved schema before calling a folder a namespace

A hand-made change under a custom schema that generates into a subdirectory
(rfc/proposal.md), with no .openspec.yaml and no delta specs yet, was read as a
namespace folder and refused by status, show, validate, instructions and
archive. The probe now also counts a file at any path the resolved schema
generates, the same check status uses to mark an artifact done.

Move the flat-change-folder docs from legacy docs/ to docs-lab reference/cli.md.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 15:47:06 +00:00
Clay GoodandClaude Opus 5 09984b8242 fix(validate): warn when tracked tasks have no checkboxes (#1774)
* fix(validate): warn when tracked tasks have no checkboxes

Progress counts checkboxes and nothing else, so a tasks.md written as
plain bullets or a numbered list is worse than an empty one: `openspec
list` and `openspec status` report "No tasks", and `openspec archive`
has no incomplete task to warn about. The file reads as finished to the
tool and unfinished to a human.

`openspec validate` now warns when every task file the change's schema
tracks contains list items but not one checkbox, pointing at the first
offending line. Reported per change, not per file, so a checklist
alongside a prose file stays silent, and only files an artifact actually
declares are linted - a bare tasks.md no schema tracks is left alone.

Closes #354

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(validate): honor fence delimiter length and width

CommonMark closes a fence only on the same character, a run at least as
long as the opener's, and no info string. Comparing the first character
alone let an inner ``` end an outer ```` block, exposing the bullets of
a nested code sample as a task list. The delimiter pattern also loses
its end anchor: `.` does not match `\r`, so an anchored info-string
group matched nothing in a CRLF file and blinded the scan to fences.

Adds the nested-fence, annotated-closer, tilde/backtick, longer-closer
and CRLF cases, plus an e2e change whose nested task files are all
bullets, asserting both reported paths stay POSIX-separated.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(validate): scan rendered content and complete evidence only

Hardening pass over the checkbox warning.

The scan for the offending line now skips YAML front matter and HTML
comment blocks alongside fenced code. A list under `tags:` is metadata
about the file rather than the work it tracks, and a commented-out list
is not work either; each exclusion can only silence a warning, never
drop a real task, which is the opposite trade from the task parser. An
unterminated `---` opener rewinds to the top, because that is a thematic
break and everything below it is still content. Only a comment opening
its own line hides that line, so the template's `## 1. <!-- Task Group
Name -->` heading cannot swallow the checklist beneath it.

A tracked file that exists but cannot be read now withdraws the warning
entirely: "no file here holds a checkbox" is a claim about the whole
tracked set, and the checkboxes may be in exactly the file that would
not open. `validate --archived` stays the surface that reports an
unreadable task file loudly (#205).

The message leads with the consequence rather than an accusation, since
a file may legitimately carry a bulleted note and no tasks yet.

New coverage: every packaged tasks template is asserted checkbox-shaped
(the guard fails if a template loses its boxes), a schema tracking tasks
by artifact id with no `apply` block, the deprecated `change validate`
text output, and an unreadable tracked file.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(validate): match CommonMark on fence indent and front matter

Two block-scanning defects, both of which hid list items.

A fence indented four spaces is an indented code block, not an opener.
Accepting it left the scan inside a block that never began, so every
list below it went unseen. Fence recognition now stops at three spaces.

`----` is a thematic break, not a YAML front-matter delimiter. Matching
three-or-more dashes let one open a block that swallowed the list under
it until the next `---`. Front matter is now exactly three dashes.

Two test defects alongside them. The deprecated-command test claimed to
assert the reported line, but the text renderer prints no line for any
issue; it now asserts the level and path prefix that surface actually
emits, with the line left to the JSON assertion that already covers it.
The unreadable-file fixture would have passed for the wrong reason had
the mode not taken, since the checkbox it hides would have silenced the
warning by itself; the read failure is now asserted first.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(validate): name task files relative to a canonical change dir

Windows CI caught the report naming a task file
`../../../../../../../../runneradmin/AppData/.../tasks.md` instead of
`tasks.md`. `resolveArtifactOutputs` hands back real paths while
`changeDir` carries whatever spelling the caller resolved, and a short
8.3 alias against its expanded form is a difference in spelling, not in
location, so the relative path escaped the change. A symlinked project
directory reproduces it off Windows.

Canonicalizing both sides recovers the relationship. A path that still
escapes falls back to the file name, so no report can leak an absolute
filesystem path. Numbering issues are named through the same helper and
gain the same fix.

The deprecated-command test now asserts the `[WARNING] tasks.md:` prefix
that exposed this, and the Windows job is its regression guard: the
mismatch cannot be staged on POSIX, where the spawned CLI's cwd is
already physical.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(validate): keep indented code out of the uncheckboxed-task scan

alfred-openspec on #1774: the scan said it reads rendered content, but
LIST_ITEM began with \s* and so reported top-level four-space-indented code
such as '    - example output' as an uncheckboxed task list. Under --strict
that false positive failed validation on a correct file.

A list-shaped line is now taken only below four visual columns of indent, the
same cut the fence logic already applies, with tabs counting as four. Genuine
nested lists are untouched: this scan reports the first list item it finds and
a nested item always sits under a shallower parent, so the parent is reported
exactly as before. A list-shaped line four columns deep with nothing shallower
above it is not nested under anything, which is what makes it code.

Regressions cover space-indented, tab-indented and numbered code samples, and
pin both the nested-list case (parent still reported) and three-space indent
(not code). Verified the guard bites: removing the column test fails them.

Also moves the documentation to its canonical home. docs-lab/README.md says the
old docs/ tree is legacy and must stay untouched, so the docs/concepts.md line
is dropped and the warning is documented under 'openspec validate' in
docs-lab/reference/cli.md, beside the archive merge findings.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(validate): skip BOM-prefixed front matter and cap ordered markers at nine digits

A prose-only task file that opened with a UTF-8 BOM before its front
matter was warned about, because the opener never matched and the
tags list was scanned. A number longer than nine digits followed by a
period also matched as a list item, which CommonMark does not allow.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs(changeset): drop the em dash

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* test(tasks): use a multi-character marker for the unrecognised-checkbox case

A single-character marker such as `[~]` becomes a task once #1773 lands,
which would flip this expectation. `[ab]` is not a task under either
parser, so the test keeps asserting that checkbox-looking list items that
count as no task still warn, whichever PR merges first.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 15:47:04 +00:00
Clay GoodandClaude Opus 5 b928165276 docs(explore): stop claiming explore never writes files (#1838)
* docs(explore): stop claiming explore never writes files

Sixteen lines across both documentation trees told users that
`/opsx:explore` creates no artifacts and writes no files, full stop.

That has been false since explore shipped (#467): its capture branch
writes the planning artifacts the user asked for, and can edit an
existing change's artifacts. #1503 later made it scaffold with
`openspec new change` first, closing #668 and #720.

The claim appeared in two shapes. Six lines denied the capability
outright ("Explore creates no artifacts and writes no code"). Ten more
said the same thing as a timing claim ("before any artifact exists"),
which reads as ordinary pitch copy and is what escaped the first pass.

Every site now carries one guarantee, worded the same way: explore
never writes code, and writes nothing else unless you ask, or say yes
when it offers. Four sites described only the user-initiated trigger,
which left the offer path - the one a reader actually hits - looking
like it did not exist.

docs/explore.md and docs/commands.md also gain a positive description
of capture where the denial used to sit, including what scaffolding
creates beyond the artifacts you named, and how capture differs from
handing off to propose (propose writes the set your schema requires;
capture writes only what you named).

Closes #1833

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* test(docs): keep the retired explore wording retired

A flat list of the phrasings that actually carried the claim, swept
over the eleven pages that pitch explore. Fails on main with all
sixteen offenders; clean on this branch.

Modeled on test/vocabulary-sweep.test.ts, and deliberately a list
rather than a grammar. An earlier draft built the grammar - section
splitting, code-fence tracking, a conditional-marker exemption so
"creates no artifacts unless you ask" would pass - and measured
against realistic prose it was imprecise in both directions while
returning the same verdict on the real input. The list has no
exemption logic to get wrong, and any maintainer can extend it.

Phrasings that are only wrong in the absolute ("writes nothing",
"creates nothing") are left to review, since the conditional form of
each is the wording the failure message recommends.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* test(docs): pin the explore capture contract, not the word

The guide check matched any "capture", so "explore automatically captures
every artifact" passed. Both explore.md and commands.md now must name the
user trigger, `openspec new change`, and the named-artifacts scope, with no
capture line claiming it happens unprompted, and keep "never writes code".

Also scope the explore.md guarantee to the setup files a new change needs,
and make the commands.md offer name the change and its scope, which the
template asks for on main and after #1832.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* test(docs): accept negated unprompted-capture wording, catch non-capture verbs

The unprompted check matched "automatically" on any capture line, so the
correct "Explore does not automatically capture artifacts" failed, while
"Explore automatically writes planning artifacts" was never scanned because
it lacks the word capture. Check each clause of lines naming explore or
capture for an unprompted write verb with no preceding negation.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 15:45:53 +00:00
openspec-release-bot[bot]andgithub-actions[bot] 9d4e5974e5 Version Packages (#1822)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-09-09 20:59:22 +00:00
Clay GoodandClaude Opus 5 e4e112d94f chore(deps): declare pnpm overrides only in pnpm-workspace.yaml (#1816)
The security overrides were declared twice: in pnpm-workspace.yaml, with
the advisory comments explaining each pin, and again under
package.json's pnpm.overrides. The copies are not additive — pnpm 10
uses package.json's block instead of the workspace list when both are
present — and Dependabot rewrites plain-name entries in package.json
whenever it bumps the same package. So a routine bump silently
displaces the pins that patch advisories, and fails the equality test
that guards them (#1812).

Keeps one declaration, in the file that carries the reasoning, and
asserts the mirror stays gone.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-09 17:47:22 +00:00
aedf4d0c64 fix(archive): preserve blank lines inside code fences (#1798)
* fix(archive): preserve blank lines inside code fences

* docs(test): document fence-preservation test helpers

* chore(changeset): track the fenced blank-line fix

The fix changes archive output for any spec documenting a fenced sample with
consecutive blank lines, so it belongs in the changelog. Release tracking only
validates changesets that exist; it never requires one, which is why CI stayed
green without it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Clay Good <hi@claygood.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-09 17:03:39 +00:00
d9e1a28c38 fix(update): refresh generated files that drifted (#1808)
* fix(update): refresh generated files that drifted

* test(update): isolate command drift from missing files and host config

* chore(changeset): track the command-drift fix

`openspec update` now reports and repairs tools it previously called up to
date, so users will see a behavior change. That belongs in the changelog.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Clay Good <hi@claygood.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-09 16:32:45 +00:00
8251763ecd fix(parser): apply every delta section header (#1802)
* fix(parser): apply every delta section header

* test(parser): assert section presence for a header after another section

* chore(changeset): track the repeated-section fix

Deltas that previously applied only part of what was authored now apply all of
it, which changes archive output for affected changes. That belongs in the
changelog.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Clay Good <hi@claygood.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-09 16:32:39 +00:00
fadac3e1c9 fix(parser): accept all CommonMark list markers in deltas (#1800)
* fix(parser): accept all CommonMark list markers in deltas

* docs(parser): document the REMOVED and RENAMED readers

* chore(changeset): track the list-marker fix

A removal or rename written with `*` or `+` now takes effect where it
previously did nothing, so existing specs can change on the next archive. That
is a user-visible behavior change and belongs in the changelog.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Clay Good <hi@claygood.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-09 16:32:35 +00:00
3915db763a fix(guidance): teach the spec-inventory verb to generated guidance (#1700)
* fix(guidance): teach the spec-inventory verb to generated guidance

`openspec list --specs` appeared in no generated skill, command, or
artifact instruction, while `openspec list --json` — the in-flight
CHANGE list — appeared throughout. An agent asked to read the existing
specs first reached for the one enumeration verb it had been taught,
got the change list, found it plausible, and reported the step complete
against the wrong object.

Explore now lists the spec inventory alongside the change list and says
which is which. The spec-driven `proposal` and `specs` instructions name
the command at the two points that need it: researching existing
capabilities before filling in the Capabilities section, and confirming
a delta's path matches an existing capability.

Guidance text only — no CLI, parser, or archive behavior changes.

Closes #1689

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(guidance): carry the store qualifier wherever the command is named

A bare `openspec list --specs` reads the local inventory, so under a
selected store it confirms a capability path against the wrong root. The
proposal instruction carried the qualifier; the modified-capability
instruction did not. All four sites now use the same wording, and the
guard is scoped to the passage that names the command — every explore
body already carries the qualifier in its unrelated capture steps, so a
whole-body assertion would pass with it dropped here.

Addresses CodeRabbit review on #1700.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(guidance): read a listed capability with the store-aware command

The read step I added defeated the fix under a store. It told the agent to
list the inventory with `--store "<id>"`, then read the result back from
`openspec/specs/<capability-path>/spec.md` — a local path. Verified against
a registered store: `list --specs --store mystore` returns
`store-only-capability`, and the corresponding local read fails outright
(or, when a local capability happens to share the name, silently returns a
different one). That is the same wrong-object failure #1689 is about,
reintroduced one line later.

Capabilities are now read with
`openspec show "<spec-id>" --type spec --json --no-scenarios`, which
resolves against the same root the listing came from and returns purpose
plus requirement texts without pulling whole spec files into context.
`--type spec` is load-bearing: a change and a spec sharing a name is an
ambiguous_item error, and change names routinely mirror capability names.

Also documents `--store` on `list` and `show` in docs/cli.md. Both already
accepted the flag — the prose at line 228 says so — but neither options
table listed it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix: Use ASCII arrows instead of unicode

This fixes the issue of ambiguous unicode character width when visualizing on terminals

* fix: Update remaining docs within explore to use ASCII

* fix(explore): finish the ASCII conversion and guard it

Rebase onto main and close the gaps in the original fix:

- Regenerate skills/openspec-explore/SKILL.md. The static skills/ mirror
  landed after this branch was cut, so the parity test would have failed
  with the template and the mirror out of sync.
- Regenerate the three parity hashes through scripts/regen-parity-hashes.mjs.
- Convert the ambiguous-width glyphs the first pass missed: the bullets in
  the CLI-storage example, and the check/cross marks in its comparison
  table, which sat in the column-aligned block the bug is about.
- Tighten the ASCII guidance to two lines. It ships into every user
  project on both delivery surfaces, so the paragraph was pure overhead.
- Add regression tests (#983): every fenced example in both the skill and
  the command body must be free of box-drawing, arrow, bullet, and
  check/cross glyphs, and the guidance must state the rule and the reason.
- Add a patch changeset.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* test(explore): cover every check/cross dingbat in the ASCII guard

The matcher listed U+2713 and U+2717 only, so a fenced example could use
✕ (U+2715) or ✘ (U+2718) — same ambiguous width, same misalignment — and
still pass. Widen to the U+2713-U+2718 run.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(explore): require explicit confirmation before writing files

* test(explore): harden write confirmation guardrail

* fix(explore): scope write confirmation precisely

* test(guidance): pin store-aware spec reads

* test(templates): regenerate explore parity hashes

The explore template now carries three independent guidance edits: the
spec-inventory verb, the ASCII diagram conversion, and the write
confirmation contract. Each pinned its own hash constants, so the pinned
values no longer describe the combined template.

Regenerate them from the merged source with `regen:parity-hashes` rather
than hand-editing, and confirm the committed skills mirror still matches
byte-for-byte.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(guidance): read complete specs before coverage decisions

* docs: drop the redundant legacy docs/cli.md edit

docs-lab/README.md makes docs-lab/ canonical and the old docs/ tree legacy.
docs-lab/reference/cli.md already documents `--store <id>` for both
`openspec list` and `openspec show`, so this branch's docs/cli.md rows added
a third copy in the stale tree and nothing else.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* chore(changeset): drop the docs claim this PR no longer makes

alfred-openspec on #1700: the release note still said docs/cli.md now documents
--store on list and show, but that legacy-tree edit was removed from this head
and the diff does not touch docs/cli.md. The canonical docs-lab/reference/cli.md
already documented the flag on both commands, which is why the edit went.

Removing the sentence rather than repointing it at docs-lab: nothing in
docs-lab changed either, so there is no documentation change to announce.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Shooks <justanormalme@gmail.com>
Co-authored-by: Ayman D. <ayman.bacc@gmail.com>
2026-09-09 16:25:02 +00:00
Clay GoodandClaude Opus 5 c170dc77ad fix(archive): read a wrapped scenario bullet as one bullet (#1782)
* fix(archive): read a wrapped scenario bullet as one bullet

A repository that wraps its prose at a column limit writes most scenario
bullets over two lines. The retirement guard read the continuation line
as content the merge could not account for, so `retire_capabilities`
refused every such spec - and because the hint that names the marker is
gated on that same count, an unmarked author got the bare "must have at
least one requirement" abort and never learned the retirement path
exists.

A line indented to the content column of the item above it, with no
blank line between, is part of that item. It is accounted for when the
item was and already reported when it was not, so nothing is deleted
unmentioned either way. A blank line still ends the item, so a note
written below the scenarios is still the author's own however it is
indented.

Closes #1780

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(archive): keep an indented heading out of a bullet's continuation

Continuation is for wrapped prose. A raw HTML heading indented under a
scenario bullet was absorbed by it, so indenting a section one level
would have smuggled it past the audit and deleted it with the file. ATX
headings were already excluded; HTML ones now are too, matching how the
pass above the requirements section reads them.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(archive): flag a setext heading indented under a bullet

A setext underline turns the line above it into a heading, so indenting
the pair one level under a scenario bullet let a whole section be
absorbed as continuation and deleted with the file. Checked ahead of the
continuation branch now, the same way the ATX and raw HTML forms already
are.

Found by CodeRabbit on this PR.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(archive): read an unindented wrapped bullet as one bullet too

Not every wrap indents its continuation, and the indent-only rule left
the reported bug fixed for one spelling and live for the other: a
hand-wrapped scenario bullet still refused the retirement.

Inside a scenario's unbroken bullet run a lazy continuation is now read
as part of the bullet above it. This widens nothing - a sibling bullet
written in that same position is already read as the scenario's own, and
a lazy line is part of the bullet where a sibling is merely next to it.
Past the blank line that ends the run the indent is still required, so a
note bulleted below the scenarios and the line that wraps it stay the
author's.

Also covers CRLF specs, and asserts the refusal report names only the
real leftover in a wrapped multi-requirement spec rather than burying it
under continuations.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(archive): stop a lazy continuation at anything that opens a block

CommonMark lets a blockquote, thematic break, table, list item or raw
HTML interrupt a paragraph, so one written flush against a scenario
bullet starts something new rather than continuing it. The lazy
allowance absorbed all of them, which would have deleted an author's
note with the file and named nothing.

The bullet's paragraph is now tracked as its own state: opened by a
bullet, closed by a blank line, a fence, a heading, or a line that opens
a block - including one indented inside the item, whose own paragraph
ends the bullet's. Lazy continuation applies only while it is open.
Indented continuation is unaffected: a nested list or quote sitting
inside the item is still the item's own content.

Each of the six holes is pinned by a test proven to fail with the
narrower rule removed.

Found by CodeRabbit on this PR.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(archive): classify a line as the list item sees it

A marker as wide as `100. ` puts the item's content past the three
columns a Markdown construct is allowed at the file's left margin, so
`## Retention` written inside such an item read as five spaces of
nothing and was absorbed as continuation - a regression against the
behavior before continuation existed, which named it.

Every syntax test in the audit now reads the line with the item's
indent removed, so a heading, a setext underline or a block start is
recognized wherever the item sits.

Found by CodeRabbit on this PR.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* chore: drop test scratch directory committed by mistake

`test-spec-command-tmp/` is a fixture a test run leaves behind, swept up
by `git add -A` in the previous commit. It is not part of the change.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(archive): share one list-marker definition with the paragraph rule

Folds in the marker coverage from the duplicate PR #1789, which fixes the same
issue (#1780) with a shallower model.

The audit named `-`, `*` and ordered items as list markers, while
INTERRUPTS_PARAGRAPH, added in this same PR, already named `+` and capped an
ordered marker at CommonMark's nine digits. The two disagreed, so a line one
called a bullet and the other did not was read as both at once.

Both now use one LIST_ITEM constant:

- `+` is the behavior fix. A spec bulleted with `+` validates like any other,
  and every one of its scenario bullets was reported as unaccounted content, so
  that capability could not be retired at all. Regression added, verified to
  fail against the old marker set.
- The nine-digit cap changes no verdict in this design, since a line the
  pattern rejects is weighed by the same rules either way. It is here for the
  consistency, and the comment says so rather than claiming a fix. The case is
  pinned so a later change cannot start deleting such a note.

LIST_ITEM also no longer requires content after the marker, so an empty `- `
reads as the bullet it is instead of falling through to the leftovers, which is
what the surrounding indent tracking already assumed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-09 16:24:59 +00:00
Clay GoodandClaude Opus 5 8ba4ac1b16 fix(apply): warn when a change is ready to implement with no specs (#1783)
* fix(apply): warn when a change is ready to implement with no specs

Apply gates on the schema's `apply.requires` (tasks) alone, so a change
whose tasks file was written ahead of its specs read as ready even though
it had no delta specs at all — the state `openspec validate` rejects.
Apply was the one surface that green-lit a change every other surface
flags, which is how agents end up implementing before the specs exist.

Report it as a warning, in the text output and in `--json`, naming both
ways out: write the specs, or declare `skip_specs: true`. Blocking would
be a policy change; naming the gap is not. Changes that have specs,
declare `skip_specs`, or are still blocked on their own required
artifacts are unaffected.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* refactor(apply): name the metadata file from its shared constant

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* test(apply): cover custom schemas in the no-specs warning

A schema with no spec-producing artifact must stay quiet, and one whose
spec artifact is not called `specs` must still warn - the rule keys off
the output path, not the artifact id.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* test(apply): stop asserting an absolute temp path on Windows

os.tmpdir() hands back the short form (C:\Users\RUNNER~1) while the CLI
resolves the long one, so the assertion pinned a path that never matched
on windows-pwsh. Assert the change-relative tail instead.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(apply): name the whole chain a blocked change still needs

Apply blocks on the schema's `apply.requires` alone, so its message
stopped at the first hop: a change holding only a proposal was told
"Missing artifacts: tasks" while the specs `tasks` depends on were
missing too. Taken literally that is an instruction to write the
tracking file straight from the proposal and skip everything between —
the failure reported in #834 and #869.

Walk `requires` and report the whole set, in build order, as
`missingPrerequisites` (text and `--json`). What apply blocks on is
unchanged, and the wording leaves conditional artifacts to the schema
rather than demanding them.

The remedies these messages give are now CLI commands rather than the
`openspec-continue-change` skill: `continue` is not in CORE_WORKFLOWS,
so on the default profile the old advice named a skill that is never
installed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(apply): name the schema's own spec artifact in the warning

alfred-openspec on #1783: collectApplyWarnings() discovers spec-producing
artifacts by output path, so it correctly fires for a schema whose artifact id
is `contracts`, but the remediation text then hardcoded
`openspec instructions specs`. That names an artifact such a schema does not
declare, so the advertised custom-schema support dead-ended at the exact step
meant to resolve the warning.

The command now derives its target from specArtifacts: the artifact's own id
when the schema declares one spec-producing artifact, and `<artifact-id>` as a
placeholder when it declares several, since there is no single right answer
there and a guess would read as an instruction.

The renamed-artifact test now asserts the command names `contracts` and
rejects the hardcoded `specs` spelling, and a new test pins the two-artifact
placeholder. Verified both fail against the hardcoded string.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* chore(changeset): bump apply warnings to minor

This adds `missingPrerequisites` and `warnings` to the documented
`instructions apply --json` contract in docs/agent-contract.md. New fields are
backward compatible, but they are new capability an agent can consume, which is
a minor under semver rather than a patch.

Taking the conservative direction deliberately: shipping new API surface as a
patch is the violation, since a consumer pinned to a patch range would receive
it without opting in. A minor costs nothing if the fields turn out to be
uninteresting.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-09 16:24:55 +00:00
Clay GoodandClaude Opus 5 3c6d318b83 fix(init): name the workflows the profile left out (#1779)
* fix(init): name the workflows the profile left out

Setup output listed the workflows it installed but never mentioned the
ones it did not, so a user on the default core profile who typed
/opsx:ff saw nothing and read it as a broken install. The docs explain
profiles; nobody reads them before typing a command that should be
there.

init now closes with the missing workflows by name and the two commands
that add them. The note is skipped when nothing was generated at all,
where the existing delivery correction is the whole story, and when the
profile already installs everything.

Also adds a troubleshooting entry for the "only some /opsx: commands
show up" symptom, which the existing list did not cover.

Relates to #1076 (the optional-workflow discoverability half; the Windows command-discovery repro in that thread is not addressed here)

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* chore: drop a test scratch directory committed by mistake

test-show-command-tmp/ is created by a test run and does not exist on
main; it was picked up by a `git add -A`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(init): keep the workflow note off runs that generate nothing

With no tools selected (or only tools that could not receive a surface),
`openspec config profile` followed by `openspec update` writes nothing,
so naming the missing workflows pointed at the wrong problem.

Reported by CodeRabbit on this PR.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* refactor(init): drop the redundant update step from the workflow note

`openspec config profile` offers to apply to the current project before
it exits, and prints the `openspec update` guidance itself when the user
declines, so naming a second command was one step too many.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs(troubleshooting): match the profile steps to what the CLI does

`openspec config profile` applies to the current project itself, so
listing `openspec update` as a second required step was wrong; it is the
fallback for declining the prompt or for other projects.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(update): name the workflows the profile left out

`openspec update` is what the troubleshooting checklist tells a user to
run when a command they read about never appeared, and it is what people
run after upgrading the CLI. Neither of its existing profile notes fires
on the default `core` profile, so that user reached "All tools up to
date" and still learned nothing about the six workflows they don't have.

The note is the fallback pointer: silent when the extra-workflow or
missing-core note already named `openspec config profile`, and when no
configured tool can receive a workflow surface under the active delivery.

Reading the two existing notes as one short-circuited `||` would have
swallowed whichever ran second; they are evaluated separately.

Relates to #1076 (the optional-workflow discoverability half; the Windows command-discovery repro in that thread is not addressed here)

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* refactor(update): gather the profile notes behind one call

The two call sites had grown identical six-line blocks. One
displayProfileNotes() keeps the ordering and the single-pointer rule in
one place, where the "evaluate every note, never chain them with ||"
constraint can be stated once.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs: drop the legacy troubleshooting entry

alfred-openspec on #1779: docs-lab/README.md says the old docs/ tree is legacy,
is no longer used by the site, and must stay untouched. The canonical
docs-lab/customize/profiles.md already lists the six optional workflows and the
'openspec config profile' command that adds them, and the root README already
calls out the expanded set, so this entry was a third copy in a stale tree.

The docs-lab troubleshooting page is a heading-only skeleton held back from the
site, so there is nothing to move it to; this PR is now source and tests only.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-09 16:24:52 +00:00
Clay GoodandClaude Opus 5 6d2dbe62d3 fix(propose): load project context before planning (#1657)
* fix(propose): load project context before planning

* test(propose): assert project context is applied

* fix(propose): honor project context limits

* fix(propose): fail closed on unsafe context

* fix(propose): skip config without a root

* chore(parity): regenerate hashes after merging main

* fix(propose): harden early context loading guidance

* fix(propose): require initialization before planning in bare repos

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-09 16:24:48 +00:00
Clay GoodandClaude Opus 5 1c0ee701e5 docs: add CONTRIBUTING.md (#1781)
* docs: add CONTRIBUTING.md

Require a discussion (core design changes) or an issue before a PR is
opened, and require every PR to link its issue.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs: add setup and PR steps to CONTRIBUTING.md

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs: make CONTRIBUTING.md the single source for the process

The README's Contributing section said small fixes could go straight to
a PR, which contradicts the new discussion/issue requirement. Point it at
CONTRIBUTING.md and carry over the conventional-commit and AI-disclosure
policies so nothing is lost.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs: close the three process gaps in CONTRIBUTING.md

alfred-openspec on #1781:

1. The OpenSpec-proposal rule was dropped from the README with nothing
   replacing it, recreating the gap in #1727. New step 2 carries the threshold
   over verbatim from the README (new features, significant refactors,
   architectural changes) plus the philosophy paragraph, says to open the
   proposal as its own PR and wait for approval, and tells anyone unsure to ask
   in the issue from step 1.

2. The discussion path contradicted itself: step 1 accepted a prior discussion
   while step 3 required 'Closes #123'. The PR step now says to link what you
   opened in step 1, 'Closes #123' for an issue or a link to the discussion
   when there is no issue. CodeRabbit's thread on README.md:227 is the same
   defect, so the README sentence says 'the issue or discussion' too.

3. The local setup was missing 'pnpm exec tsc --noEmit', which CI runs, and the
   README called the guide a development setup after 'pnpm run dev' and
   'dev:cli' were removed. The command is added, the guide states that those
   four commands are exactly what CI runs, and the README pointer now describes
   the guide as the full process rather than a setup.

Verified each documented command against this checkout: build, tsc --noEmit and
lint all pass as written.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-09 16:24:45 +00:00
Clay GoodandClaude Opus 5 0b60a0ac1f chore(deps): bump the website-dependencies group in /website with 5 updates (#1815)
Applies dependabot's website bumps (#1812) and syncs the postcss
override in website/pnpm-workspace.yaml, which dependabot does not know
about, keeping the three override declarations in agreement.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-09 16:24:41 +00:00
Clay GoodandClaude Opus 5 6981c84df0 chore(deps): bump zod to 4.5.4 and eslint to 10.9.1 (#1814)
Consolidates the two open root-lockfile dependabot bumps (#1810, #1811)
into one PR so the pinned flake.nix pnpmDeps hash only has to be
regenerated once.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-09 16:24:37 +00:00
Clay GoodandClaude Opus 5 63666c8bb2 ci: report the correct pnpmDeps hash when flake.nix is stale (#1817)
* ci: report the correct pnpmDeps hash when flake.nix is stale

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* ci: scope the reported hash to the pnpmDeps block

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* ci(flake): scope every hash rewrite to the pnpmDeps block

alfred-openspec on #1817: the workflow read is scoped now, but the script it
runs is not. update-flake.sh read CURRENT_HASH from the first hash assignment
anywhere in flake.nix, and all three in-place rewrites matched every hash
assignment. flake.nix holds one fixed-output derivation today, so that lands on
the right line by luck; add a second and the script stamps the placeholder over
both, reads back whichever mismatch Nix reported first, and writes pnpmDeps'
hash into the other derivation. Scoping only the workflow left that path
fragile, as the review says.

The address range is declared once as PNPM_DEPS_BLOCK and used by the read and
all three rewrites, so the scoping cannot drift between call sites.

Also guards the read: an unmatched block previously left CURRENT_HASH empty,
and the failure path would then restore hash = "". It now exits before
touching the file.

Verified against a three-derivation fixture with pnpmDeps in the middle, which
catches both shapes of the bug: the scoped read returns the pnpmDeps hash while
an unscoped read returns the first derivation's, the placeholder is written
once rather than three times, and the neighbouring hashes survive the restore.
That fixture is the new test, alongside a static check that no hash read or
rewrite in the script is missing the range. Verified the static check fails
when any one call site is unscoped.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* test(flake): run the scoping fixture on its own volume

The new test failed on windows-pwsh with 'sed: cannot rename ./sedKaAflu:
Invalid cross-device link'. sed -i writes its temp file in the working
directory and renames it over the target; on a GitHub Windows runner the repo
is on D: and os.tmpdir() is on C:, so that rename crosses volumes.

bash now runs with cwd set to the fixture directory and addresses the file by
name, which keeps the temp file and its rename on one volume. The assertions
are unchanged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-09 16:24:16 +00:00
373 changed files with 32465 additions and 7835 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"@fission-ai/openspec": patch
---
The CLI starts faster: each command now loads its implementation only when it runs. `openspec --version` and `--help` load 24 modules instead of 485, and commands such as `config list`, `store list` and `doctor` load only what they use, which matters most where Node loads modules slowly, such as Windows. Output, help text, shell completions, exit codes and telemetry are unchanged.
+5
View File
@@ -0,0 +1,5 @@
---
"@fission-ai/openspec": patch
---
A requirement description over 500 characters is now a warning instead of an informational hint, so `openspec validate --strict` fails on it and CI can enforce the limit. The check also covers ADDED requirements in a change, so `openspec validate <change> --strict` catches a new overlong requirement before archive. Normal validation and archive are unchanged: they still pass when this is the only finding. The specs instruction now explains how to split an existing long requirement without losing its scenarios.
-13
View File
@@ -1,13 +0,0 @@
---
"@fission-ai/openspec": minor
---
Add `openspec sync`, which folds a change's delta specs into the main specs without archiving it, and an optional `status: proposed | shipped` field in a change's `.openspec.yaml`.
`openspec sync --check` gates on one property: a change that claims to be shipped has its deltas in `specs/`. A proposed change passes for free, so the check is green as its resting state and red only on a real mistake — unlike a check for "is everything archived?", which is red for the whole life of every open pull request. It reads only files on disk, so a pre-commit hook, a pre-push hook and CI run the same command and agree.
`openspec list --status <state>` filters changes by that field.
Everything here is opt-in and inert by default. The `status` field is absent unless a project writes it, nothing generates it, and `archive` is unchanged.
Designed by [@ixxie](https://github.com/ixxie) in [#1683](https://github.com/Fission-AI/OpenSpec/issues/1683) — the diagnosis that `archive` welds a state transition to a text merge, `shipped ⇒ folded` as a predicate over the working tree, and the standalone `sync` that makes it checkable. This ships a smaller, additive subset of that proposal.
+5
View File
@@ -0,0 +1,5 @@
---
"@fission-ai/openspec": patch
---
`openspec view` no longer lists or counts archived changes. In projects with many archived changes, the list pushed active work off the screen. The dashboard shows current work again, and `openspec list --archived` still shows archived changes on request.
+3
View File
@@ -1,2 +1,5 @@
# Default code ownership
* @Fission-AI/openspec-maintainers
# Route docs-lab changes to the docs owner for review
/docs-lab/ @TabishB
+68
View File
@@ -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
+12
View File
@@ -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
+30
View File
@@ -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
+8 -4
View File
@@ -1,10 +1,14 @@
version: 2
# Dependabot does not manage two dependency surfaces in this repo:
# 1. pnpm `overrides` (pnpm-workspace.yaml + package.json, root and website) —
# transitive version pins that remediate advisories Dependabot can't otherwise
# reach. It never bumps or removes these; each carries an inline advisory
# comment noting the removal condition (see pnpm-workspace.yaml).
# 1. pnpm `overrides` (pnpm-workspace.yaml, root and website) — transitive
# version pins that remediate advisories Dependabot can't otherwise reach.
# It never bumps or removes these; each carries an inline advisory comment
# noting the removal condition (see pnpm-workspace.yaml). They live in
# pnpm-workspace.yaml only, because Dependabot *does* rewrite a plain-name
# override (`postcss: ^8.5.26`) mirrored under package.json's `pnpm.overrides`
# — and that block replaces the workspace list rather than merging with it,
# so the mirror displaces the real pins. See #1812.
# 2. The Nix flake (flake.nix / flake.lock). Update nixpkgs manually with
# `nix flake update`; there is no Dependabot ecosystem for Nix. Note that the
# pnpmDeps FOD hash in flake.nix must be regenerated on any root lockfile change.
+94 -26
View File
@@ -42,6 +42,10 @@ jobs:
- 'pnpm-workspace.yaml'
- 'scripts/update-flake.sh'
- '.github/workflows/ci.yml'
# The Nix build runs `openspec completion generate`, so a change to
# the generator can break packaging without touching flake.nix.
- 'src/commands/completion.ts'
- 'src/core/completions/**'
test_matrix:
name: Test (${{ matrix.label }})
@@ -77,7 +81,7 @@ jobs:
persist-credentials: false
- name: Setup pnpm
uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6
uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0
- name: Setup Node.js
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
@@ -132,7 +136,7 @@ jobs:
persist-credentials: false
- name: Setup pnpm
uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6
uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0
- name: Setup Node.js
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
@@ -176,10 +180,76 @@ jobs:
persist-credentials: false
- name: Install Nix
uses: DeterminateSystems/nix-installer-action@ef8a148080ab6020fd15196c2084a2eea5ff2d25 # v22
uses: DeterminateSystems/nix-installer-action@3138316df39ed29be04236d7ffc686fa525866aa # v23
- name: Setup Nix cache
uses: DeterminateSystems/magic-nix-cache-action@908b263ff629f4cc17666315b7fd3ec127c6244d # v14
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
# stale hash is reported here as the exact value to paste. Built first, the
# same staleness surfaces as pnpm's ERR_PNPM_NO_OFFLINE_TARBALL — which names
# a missing tarball, not the hash — and the script never runs to say otherwise.
# Every root lockfile change needs this value, and Dependabot cannot produce it.
- name: Verify pnpmDeps hash matches the lockfile
run: |
bash scripts/update-flake.sh
if git diff --quiet flake.nix; then
echo "✅ flake.nix pnpmDeps hash is up to date"
exit 0
fi
# Scoped to the pnpmDeps block: a bare first-match would report some other
# FOD's hash if one is ever added above it.
HASH=$(sed -n '/pnpmDeps = /,/};/p' flake.nix \
| sed -nE 's/.*hash = "(sha256-[^"]+)".*/\1/p' | head -1)
git diff flake.nix
echo "::error file=flake.nix::Stale pnpmDeps hash. Set pnpmDeps.hash to $HASH and push."
exit 1
- name: Restore flake.nix
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
@@ -194,6 +264,19 @@ jobs:
echo "Error: openspec binary not found in build output"
exit 1
fi
for completion in \
"share/bash-completion/completions/openspec.bash" \
"share/fish/vendor_completions.d/openspec.fish" \
"share/zsh/site-functions/_openspec"; do
if [ ! -s "result/$completion" ]; then
echo "Error: completion script missing or empty: $completion"
exit 1
fi
done
if [ "$(head -1 result/share/zsh/site-functions/_openspec)" != "#compdef openspec" ]; then
echo "Error: zsh completion is not autoloadable (missing #compdef header)"
exit 1
fi
echo "✅ Build output verified"
- name: Test binary execution
@@ -206,25 +289,6 @@ jobs:
fi
echo "✅ Binary execution successful"
- name: Validate update script
run: |
echo "Testing update-flake.sh script..."
bash scripts/update-flake.sh
echo "✅ Update script executed successfully"
- name: Check flake.nix modifications
run: |
if git diff --quiet flake.nix; then
echo "ℹ️ flake.nix unchanged (hash already up-to-date)"
else
echo "✅ flake.nix was updated by script"
git diff flake.nix
fi
- name: Restore flake.nix
if: always()
run: git checkout -- flake.nix || true
validate-changesets:
name: Validate Release Tracking
runs-on: ubuntu-latest
@@ -242,10 +306,14 @@ jobs:
changed_changesets="$(git diff --name-only --diff-filter=ACMRT origin/main...HEAD -- '.changeset/*.md' ':!.changeset/README.md')"
if [[ -n "$changed_changesets" ]]; then
echo "has_changesets=true" >> "$GITHUB_OUTPUT"
# Run-unique delimiter: the value is a list of PR-authored paths, so a
# fixed "EOF" would let a crafted path close the block early and append
# its own key=value outputs.
delim="EOF_$(openssl rand -hex 16)"
{
echo "files<<EOF"
echo "files<<$delim"
echo "$changed_changesets"
echo "EOF"
echo "$delim"
} >> "$GITHUB_OUTPUT"
else
echo "has_changesets=false" >> "$GITHUB_OUTPUT"
@@ -254,7 +322,7 @@ jobs:
- name: Setup pnpm
if: steps.changed-changesets.outputs.has_changesets == 'true'
uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6
uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0
- name: Setup Node.js
if: steps.changed-changesets.outputs.has_changesets == 'true'
+3 -3
View File
@@ -40,7 +40,7 @@ jobs:
fetch-depth: 0
token: ${{ steps.app-token.outputs.token }}
- uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6
- uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
@@ -53,7 +53,7 @@ jobs:
# Opens/updates the Version Packages PR; publishes when the Version PR merges
- name: Create/Update Version PR
id: changesets
uses: changesets/action@8488615a623b1b9c987934bb89eae8af6a946ac1 # v2.1.1
uses: changesets/action@ae32849d5ba541f9ae29e40e22a623bc13562f51 # v2.1.2
with:
github-token: ${{ steps.app-token.outputs.token }}
pr-title: 'chore(release): version packages'
@@ -84,7 +84,7 @@ jobs:
with:
fetch-depth: 0
- uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6
- uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
+2 -2
View File
@@ -51,7 +51,7 @@ jobs:
persist-credentials: false
- name: Setup pnpm
uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6
uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0
# No dependency cache: `pnpm audit` reads the lockfile, nothing is installed,
# so a cache-save step would fail on the missing store path.
@@ -109,7 +109,7 @@ jobs:
persist-credentials: false
- name: Setup pnpm
uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6
uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0
- name: Setup Node.js
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
+262
View File
@@ -1,5 +1,267 @@
# @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
- [#1940](https://github.com/Fission-AI/OpenSpec/pull/1940) [`0b5ce44`](https://github.com/Fission-AI/OpenSpec/commit/0b5ce44b55e0d793a312290ba5a41170a78e47c6) Thanks [@clay-good](https://github.com/clay-good)! - Keep fast-forward clarification guidance and onboarding task approval consistent across generated skills and commands. Fast-forward now asks only when context is critically unclear, while onboarding asks users to approve the task breakdown before saving it and separately asks whether to begin implementation.
- [#1926](https://github.com/Fission-AI/OpenSpec/pull/1926) [`f2812f6`](https://github.com/Fission-AI/OpenSpec/commit/f2812f6d185f47cb577055f2fe243f12000d6cd2) Thanks [@kevin9327](https://github.com/kevin9327)! - ### Bug Fixes
- **Archive** — When Windows `EPERM` blocks renaming a change directory that still has children, copy from the original source instead of requiring a staging rename that fails the same way. That lets archive finish instead of rolling back the spec write and leaving an empty capability directory git cannot see. A staging failure that is not `EPERM`/`EXDEV` still leaves the source untouched.
The source of that unstaged copy is still the live change directory, which the archive claim does not cover, so cleanup removes only the entries it copied and verified rather than whatever is present when it runs. A file written in that window is left alone and the complete destination is retained for recovery, instead of being deleted without ever reaching the archive.
An edit to a file that was already verified is covered too. Cleanup claims each entry with an atomic rename before reading it, then compares what it claimed against the copy. A rewrite that lands first is caught by that comparison and the file is put back; one that lands after creates a new file at the original path, which is never deleted. Either way the newer bytes stay on disk and archive reports the move as incomplete rather than succeeding with the older copy.
Rollback of a newly created spec now also prunes the capability directory it created — and only that one. An empty capability directory that was already there is left in place with its own permissions.
- [#1795](https://github.com/Fission-AI/OpenSpec/pull/1795) [`fb1b876`](https://github.com/Fission-AI/OpenSpec/commit/fb1b87613b7cdbe8d74e8147833904f46f0468c6) Thanks [@runsonmypc](https://github.com/runsonmypc)! - Archive workflows now use schema-aware task progress from `openspec list --json`, so custom task files and globs still trigger incomplete-task warnings.
- [#1885](https://github.com/Fission-AI/OpenSpec/pull/1885) [`fd56e12`](https://github.com/Fission-AI/OpenSpec/commit/fd56e12c9e7fdbbfdc2dcd0a5ef3fab04840909d) Thanks [@philo-x](https://github.com/philo-x)! - Fix artifact output resolution to recognize brace expansion and extglob patterns while preserving literal output filenames and confining brace-expanded paths to the change directory.
- [#1964](https://github.com/Fission-AI/OpenSpec/pull/1964) [`7ac58dc`](https://github.com/Fission-AI/OpenSpec/commit/7ac58dc7905a4eeaaad7eff2b2b64cf971fd6ec7) Thanks [@clay-good](https://github.com/clay-good)! - Continue commands now open with an instruction to follow the active OpenSpec workflow directly, so local models no longer try to call a tool named after it ([#1944](https://github.com/Fission-AI/OpenSpec/issues/1944)).
- [#1964](https://github.com/Fission-AI/OpenSpec/pull/1964) [`7ac58dc`](https://github.com/Fission-AI/OpenSpec/commit/7ac58dc7905a4eeaaad7eff2b2b64cf971fd6ec7) Thanks [@clay-good](https://github.com/clay-good)! - Generate Kilo Code commands in `.kilo/command/`, the directory Kilo Code reads, instead of `.kilocode/workflows/` ([#1938](https://github.com/Fission-AI/OpenSpec/issues/1938)). `openspec init` and legacy cleanup remove the workflow files OpenSpec generated there, matched by their known file names (including copies you edited), and leave files with other names in place.
- [#1958](https://github.com/Fission-AI/OpenSpec/pull/1958) [`1d35e90`](https://github.com/Fission-AI/OpenSpec/commit/1d35e908804dbb3c4a1851516759c5de190aa4d5) Thanks [@clay-good](https://github.com/clay-good)! - Preserve a file's existing line endings when rewriting it, so Windows users no longer get whole-file diffs. Applying a delta to a CRLF spec (the default on a Windows checkout with `core.autocrlf=true`) rewrote the file to LF, turning a one-requirement change into a diff that touched every line. `openspec archive` now writes the spec back with the convention it already used; a spec that does not exist yet is still written with LF.
The same fix covers marker-managed files: installing or updating shell completions in a CRLF `.bashrc` or `.zshrc` no longer leaves the file with mixed endings, which `bash` reports as `$'\r': command not found`.
Removing a managed block is fixed the same way: the blank-line collapse in `removeMarkerBlock` rebuilt its separator as a bare LF, so cleaning up legacy artifacts left a lone LF inside an otherwise-CRLF `CLAUDE.md` or rc file. Both write paths now read the file the same way, by dominant ending, so one stray CRLF in an otherwise-LF file no longer pulls the whole rewrite to CRLF.
`scripts/pack-version-check.mjs` now spawns `npm` through `cross-spawn`, so the release guard can run on Windows, where `npm` is `npm.cmd` and cannot be resolved by `execFile`.
- [#1912](https://github.com/Fission-AI/OpenSpec/pull/1912) [`8826c0c`](https://github.com/Fission-AI/OpenSpec/commit/8826c0c4a17d3511947b7c5e0934257f153f0ed2) Thanks [@Tyagiquamar](https://github.com/Tyagiquamar)! - Fix `validate --strict` reporting `PURPOSE_IS_PLACEHOLDER` for a Purpose that opens with the ordinary word "Todo" followed by prose, as in Spanish ("Todo el…") and Portuguese ("Todo o…") specs ([#1897](https://github.com/Fission-AI/OpenSpec/issues/1897)).
- Case now separates the marker from the word. `TBD`/`TODO` in capitals is still a placeholder marker whatever follows it, so `TODO write this later` is still reported.
- In any other case it counts as a marker only when followed by the end of the Purpose, a line break, or marker punctuation (`todo -`, `tbd.`), so an authored Spanish or Portuguese sentence is not reported.
- [#1744](https://github.com/Fission-AI/OpenSpec/pull/1744) [`5b55263`](https://github.com/Fission-AI/OpenSpec/commit/5b5526377506c2f0179674a869c1ac64ca9ab72d) Thanks [@javigomez](https://github.com/javigomez)! - Clarify the Codex setup hint for CLI, IDE, and desktop app users.
- [#1809](https://github.com/Fission-AI/OpenSpec/pull/1809) [`a5ceea3`](https://github.com/Fission-AI/OpenSpec/commit/a5ceea32cf110b6d8bbfea0bf1c65fe55abb133b) Thanks [@ryandemelo](https://github.com/ryandemelo)! - Say what a `MODIFIED` block adds when the scenario-loss guard fires ([#1809](https://github.com/Fission-AI/OpenSpec/pull/1809)). `openspec validate` and `openspec archive` already named the scenarios a block omits. They now also print how many scenarios each side has and which ones the block introduces, capped at three names, so a rename and a truncation read differently without opening either file. The guard catches exactly what it did before, and no exit code changes.
- [#1731](https://github.com/Fission-AI/OpenSpec/pull/1731) [`d6bdef6`](https://github.com/Fission-AI/OpenSpec/commit/d6bdef6577a077614382ef47b64100852182d6a6) Thanks [@runsonmypc](https://github.com/runsonmypc)! - Stop workflows from displaying schema names that `openspec list --json` does not return. Update and continue no longer fabricate a `spec-driven` picker label, while bulk archive and explore describe only the change fields the list command actually provides.
- [#1955](https://github.com/Fission-AI/OpenSpec/pull/1955) [`ed5d386`](https://github.com/Fission-AI/OpenSpec/commit/ed5d386a559c0215af1182d479d7f99b309fdcd2) Thanks [@clay-good](https://github.com/clay-good)! - Task guidance now requires each task group to land its own tests and documentation updates instead of deferring them to a trailing group. The onboarding walkthrough teaches the same rule, and the published schema reference no longer quotes stale instruction text.
- [#1939](https://github.com/Fission-AI/OpenSpec/pull/1939) [`a64303f`](https://github.com/Fission-AI/OpenSpec/commit/a64303fe1e24f08dbf44f78032fadbeac3a6f7fa) Thanks [@clay-good](https://github.com/clay-good)! - Return a nonzero exit status when `openspec update --force` cannot replace a legacy-only Codex installation.
- [#1733](https://github.com/Fission-AI/OpenSpec/pull/1733) [`72fbe4c`](https://github.com/Fission-AI/OpenSpec/commit/72fbe4c904707396151921a20b506d081a9dc024) Thanks [@runsonmypc](https://github.com/runsonmypc)! - Let `/opsx:update` fill a missing file under an already-satisfied glob artifact. A glob artifact is complete once one file matches it, and `/opsx:continue` only picks up `ready` artifacts, so the previous "point the user to `/opsx:continue`" handoff was unreachable and the missing file could never be created through the documented flow.
- [#1962](https://github.com/Fission-AI/OpenSpec/pull/1962) [`3364146`](https://github.com/Fission-AI/OpenSpec/commit/336414665f3f987ae424177ab1b6891a4304baeb) Thanks [@ryandemelo](https://github.com/ryandemelo)! - Stop `/opsx:verify` from reporting a correctly removed requirement as missing. Verify now reads which delta section each requirement sits under: ADDED and MODIFIED requirements are checked for an implementation as before, a REMOVED requirement passes once its behavior is gone and is flagged only while it is still present, and the old name of a RENAMED requirement is no longer reported as missing.
- [#1732](https://github.com/Fission-AI/OpenSpec/pull/1732) [`072de6b`](https://github.com/Fission-AI/OpenSpec/commit/072de6bc39b1c47b9aacf4d484be16345ca4f38e) Thanks [@runsonmypc](https://github.com/runsonmypc)! - Stop `/opsx:verify` from reporting skipped checks as passing. Task completion now uses the schema-aware `tasks` and `progress` fields returned by apply instructions, while absent spec or design inputs are mapped to every check they prevent. Apply instructions aggregate every file matched by the configured task path or glob, regardless of the tracked artifact ID. Verification stays advisory and does not require optional or intentionally omitted artifacts. The scorecard identifies each skipped check, and the final assessment does not claim archive readiness when any check did not run.
- [#1769](https://github.com/Fission-AI/OpenSpec/pull/1769) [`d3d7707`](https://github.com/Fission-AI/OpenSpec/commit/d3d770736fc01bb246b4f12a7cef7e3572ec1fb6) Thanks [@kikeprzn](https://github.com/kikeprzn)! - Fix `openspec archive` leaving `.openspec-archive.lock` behind on Windows. Node can report `dev: 0n` from a path stat while the open file handle reports the real volume id, so the claim-ownership check never matched and the stale lock blocked every later archive. The check now treats an absent device id as unavailable while still requiring the inode and the claim's contents to match before unlinking.
## 1.13.1
### Patch Changes
- [#1864](https://github.com/Fission-AI/OpenSpec/pull/1864) [`767d63c`](https://github.com/Fission-AI/OpenSpec/commit/767d63c926ab1996170f2d101acac0bac6da0287) Thanks [@dwin-gharibi](https://github.com/dwin-gharibi)! - Stop archive adding a second copy of an existing requirement under a name that differs only in case or spacing. ADDED and the RENAMED target compared requirement names exactly, while REMOVED and the RENAMED source already treated a case or whitespace variant as a mistyped header, so an ADDED `late fees` beside an existing `Late Fees`, or a rename to `LATE FEES`, archived cleanly and left two contradicting requirements in the main spec, which `validate` then accepted. Both now refuse with an error naming the existing requirement, in the same form REMOVED already used. The exact-duplicate error is unchanged, a case-only rename of a requirement to its own name still works, and a variant of a requirement the same delta removes or renames away is still allowed, because ADDED is checked against the spec as it stands after the earlier operations, as the exact check already was.
- [#1872](https://github.com/Fission-AI/OpenSpec/pull/1872) [`72bf760`](https://github.com/Fission-AI/OpenSpec/commit/72bf7600a5f7bdf74d6387e163577086fb4c68e0) Thanks [@dwin-gharibi](https://github.com/dwin-gharibi)! - Make `openspec completion uninstall bash` hand `.bashrc` back exactly as `completion install bash` found it. Install adds the OpenSpec block at the top of the file followed by a blank separator line; uninstall removed the block but kept that blank line at the top, then stripped every trailing blank line and wrote the file back without its final newline. The byte count happened to come out unchanged, but the next tool to append to `.bashrc` with `>>` (the nvm, conda and rustup installers all do) merged its first line into the user's last line and broke both. Uninstall now also drops the separator line install added when the block sits at the top of the file, and leaves the rest untouched: the final newline, trailing blank lines and CRLF line endings all survive the round trip. A block the user moved elsewhere in the file is still removed, and the zsh, fish and PowerShell installers are unchanged.
- [#1829](https://github.com/Fission-AI/OpenSpec/pull/1829) [`e67ac47`](https://github.com/Fission-AI/OpenSpec/commit/e67ac47f3a164cf6d87ddcd9f50272b88f39ee0c) Thanks [@choi138](https://github.com/choi138)! - Fix bulk archive nesting a change inside an existing archive target. The workflow now checks every archive target before it writes any main spec, the same order `openspec archive` uses. A change whose target already exists, or that shares a target with another selected change, is reported as failed and is never synced or moved, while the rest of the batch continues. The check runs again just before each move.
- [#1878](https://github.com/Fission-AI/OpenSpec/pull/1878) [`2ef6fbd`](https://github.com/Fission-AI/OpenSpec/commit/2ef6fbde3da95f6e471bcb504d13711308091be0) Thanks [@dwin-gharibi](https://github.com/dwin-gharibi)! - Let `openspec config edit` run an `EDITOR` or `VISUAL` that carries arguments. The whole value was passed to `spawn` as the program name, so common settings such as `code --wait`, `subl -w` or `emacsclient -t` failed with `spawn code --wait ENOENT`, and because that error was never caught the command died with a raw Node stack trace. The value is now split into a program and its arguments, honoring quoted paths with spaces, and the config path is appended as its own argument. No shell is involved, so shell metacharacters in the value are passed through literally. On Windows, `.cmd` shims such as `code.cmd` are found. A value that is itself the absolute path of an existing file is still run as-is, so an unquoted editor path containing spaces keeps working. An editor that cannot be started, exits non-zero or is killed is now reported as a one-line error naming the editor, with an install hint when the program was not found, and the command exits 1 instead of throwing. `EDITOR` still takes precedence over `VISUAL`, and the file is still validated after the editor closes.
- [#1773](https://github.com/Fission-AI/OpenSpec/pull/1773) [`11a9691`](https://github.com/Fission-AI/OpenSpec/commit/11a9691524bad84a575854bf6dc5124f630479ba) Thanks [@clay-good](https://github.com/clay-good)! - Stop dropping checkbox lines whose marker the task parser does not recognise. A `tasks.md` whose remaining work used a marker other than `[ ]`/`[x]`/`[X]`, for example `- [~] 1.2 Deferred`, reported `✓ Complete` in `openspec list`/`status` and archived with no incomplete-task warning, because unmatched lines counted toward neither the numerator nor the denominator. An empty `[]` and a padded `[ x]` were lost the same way. Only a box holding `x` or `X` means done (spacing inside the brackets is ignored, so `[ x]` is done), and every other marker now reads as unfinished, across progress, the apply task list, archive's gate and validate's task-numbering check. The archive, bulk-archive and verify workflows now tell agents the same rule, so a hand-counted tally cannot disagree with the CLI, and the `tasks` instruction in the `spec-driven` schema states it where agents author the file. Markdown link bullets stay out of the count: `- [Some doc](./doc.md)` and the one-character `- [A](https://example.com)` are not tasks.
- [#1701](https://github.com/Fission-AI/OpenSpec/pull/1701) [`92fb72d`](https://github.com/Fission-AI/OpenSpec/commit/92fb72d1dcd5fa6e43802c5b2f74b5e78416e545) Thanks [@clay-good](https://github.com/clay-good)! - Agent-driven archive and sync workflows now create a missing main spec from `ADDED` requirements instead of treating it as already synced. They block sync rather than inventing `MODIFIED` or `RENAMED` requirements or writing an empty spec for a `REMOVED`-only delta, while preserving the user's explicit choice to archive without syncing. A REMOVED-only delta with `retire_capabilities: true` remains already synced when its main spec is gone. Fixes [#1222](https://github.com/Fission-AI/OpenSpec/issues/1222) and [#1264](https://github.com/Fission-AI/OpenSpec/issues/1264).
- [#1804](https://github.com/Fission-AI/OpenSpec/pull/1804) [`a5bf5c6`](https://github.com/Fission-AI/OpenSpec/commit/a5bf5c68447f03e4f99206e49e00b5d9111301a4) Thanks [@dwin-gharibi](https://github.com/dwin-gharibi)! - Say so when a requirement in a delta sits outside every delta section. A well-formed `### Requirement:` block written under `## Notes`, under a misspelled header such as `## Add Requirements`, or above the first `## ` header was dropped with no diagnostic: `openspec validate` reported the change valid and `openspec archive` exited 0 without applying it. `openspec validate` now reports each one as a WARNING naming the section and line, and archive prints the same warning. Nothing else changes: the block is still not applied, the verdict stays valid outside `--strict`, and requirements shown inside a code fence are not reported. Fixes [#1803](https://github.com/Fission-AI/OpenSpec/issues/1803).
- [#1832](https://github.com/Fission-AI/OpenSpec/pull/1832) [`4c369e0`](https://github.com/Fission-AI/OpenSpec/commit/4c369e022b1d397842d2b85675e34da6287f5801) Thanks [@clay-good](https://github.com/clay-good)! - Resolve the contradiction that left explore mode's capture branch without a governing rule. Explore states twice that the agent must ask a direct yes/no question and wait for confirmation in a separate user message before its first write-capable action, naming `openspec new change` as an example, while the capture branch tells the agent to transition "seamlessly" into running `openspec new change` and creating artifacts with no confirmation step. Both readings were defensible from the text, so the same "capture this as a change" request either wrote `.openspec.yaml` plus several artifacts immediately or stopped and asked, depending on which passage the agent weighed, which made the [#1715](https://github.com/Fission-AI/OpenSpec/issues/1715) guarantee unenforceable in the one explore path that writes files. An explicit capture request is now stated to be that confirmation, covering the change and the artifacts the request names and nothing else. The guardrail keeps its teeth for the case [#1715](https://github.com/Fission-AI/OpenSpec/issues/1715) actually reported: when the agent is the one proposing the capture, or when the work would go beyond the requested scope, it still asks first, and answers to design or clarifying questions are still never consent to write. Both explore delivery surfaces and the committed skill carry the same wording. Fixes [#1828](https://github.com/Fission-AI/OpenSpec/issues/1828).
- [#1788](https://github.com/Fission-AI/OpenSpec/pull/1788) [`62106f4`](https://github.com/Fission-AI/OpenSpec/commit/62106f40e3b7b7364529a2f928717e23e37282eb) Thanks [@clay-good](https://github.com/clay-good)! - Name the workflow where explore hands off. Explore mode refuses to implement, but every place it said what to do instead described the next step as prose ("create a change proposal") without naming the workflow that does it: the refusal itself, the "flow into a proposal" ending, the closing summary, and the do-not-implement guardrail. Its seamless capture path was worse: it scaffolded a change, wrote artifacts, and then said nothing at all about what came next. With no named exit, agents finished the discovery questions and started writing code, which is the failure reported through GitHub Copilot in [#869](https://github.com/Fission-AI/OpenSpec/issues/869), and which the docs already promised would not happen ("when the picture is clear, it hands off to `/opsx:propose`").
The explore skill and command now name `/opsx:propose` at all four prose handoffs, and the capture path ends by naming `/opsx:propose` for the remaining planning artifacts and `/opsx:apply` for implementation, with an explicit note that capturing artifacts is not permission to implement them. The references are written in the canonical `/opsx:<id>` form so each tool renders the invocation it actually registers (`/openspec-propose` for skills-only delivery, `/opsx-propose`, `/opsx:propose`, or `@opsx-propose` for command surfaces). The handoffs follow the installed workflow set: a custom profile without `propose` or `apply` gets explore's own capture path and the `openspec instructions apply` CLI instead of a command it never installed. Fixes [#869](https://github.com/Fission-AI/OpenSpec/issues/869).
- [#1787](https://github.com/Fission-AI/OpenSpec/pull/1787) [`9827762`](https://github.com/Fission-AI/OpenSpec/commit/9827762d2d18d8076acf90be79d64894255099ea) Thanks [@clay-good](https://github.com/clay-good)! - ### Bug Fixes
- Generated skills and commands no longer adopt a project that never ran `openspec init`. Every workflow now checks `root` from `openspec list --json` before its first write, and `"root": null` means the project is not set up. What happens next depends on how the workflow was reached. A skill the agent picked on its own drops OpenSpec and answers the request normally, without asking about setup. A workflow the user asked for by name, or ran as a slash command, stops and asks whether to initialize the project, target a store, or handle the request without OpenSpec. A project whose `openspec/config.yaml` names a store this machine cannot resolve (not registered, or a malformed `store:` line) is not mistaken for an uninitialized one: the workflow stops and shows the store error. Neither path lets `openspec new change` create `openspec/` in the current directory as a side effect. Skill descriptions now name OpenSpec so hosts stop offering these workflows in unrelated repositories. `openspec new change` also says when it had to create the root itself, so a directory that was never set up no longer picks up an `openspec/` directory in silence (human output only; `--json` is unchanged).
- [#1902](https://github.com/Fission-AI/OpenSpec/pull/1902) [`eb03b9e`](https://github.com/Fission-AI/OpenSpec/commit/eb03b9e93320cf74a0df9043577d8023116cb1aa) Thanks [@clay-good](https://github.com/clay-good)! - Harden the CLI against repositories you have cloned but not yet read ([#1835](https://github.com/Fission-AI/OpenSpec/pull/1835)).
- A `config.yaml` value can no longer close the project context block and inject its own directives into the instructions an agent receives.
- A crafted delta or skill file no longer stalls `openspec update` or `openspec archive` with catastrophic regex backtracking.
- A repository's `.npmrc` can no longer point the update check at a cleartext or attacker-controlled registry; a rejected registry now disables the check instead of falling back.
- `openspec update` now notices a generated `SKILL.md` that was edited by hand and restores it, instead of reporting every tool as up to date.
- `DO_NOT_TRACK=true` and other common spellings of an opt-out now turn telemetry off, and nothing is sent until the first-run notice has been shown.
- Shell-completion installs quote directory paths safely, git probes run with bounded time and output, and dependencies are cleared of known advisories.
- [#1874](https://github.com/Fission-AI/OpenSpec/pull/1874) [`388d344`](https://github.com/Fission-AI/OpenSpec/commit/388d34473a40529320b2b7b9c5bb6723d18322b0) Thanks [@dwin-gharibi](https://github.com/dwin-gharibi)! - Stop legacy cleanup deleting the user's own files. The six pre-skills tools that kept their commands in a `<tool>/commands/openspec/` folder (Claude Code, CodeBuddy, Qoder, Lingma, Crush and Gemini CLI) had that whole folder removed recursively whenever it existed, so a command the user kept there, such as a team review checklist, was deleted along with OpenSpec's files, and the summary named only the folder. Because `openspec init` cleans up automatically when there is no TTY, an agent or CI running plain `openspec init` did this without `--force` and without a prompt, and `openspec update --force` did the same. Cleanup now deletes only the files OpenSpec wrote there: `proposal`, `apply` and `archive` files that still carry the OpenSpec markers every legacy command was generated with, so a same-named file the user wrote is kept. It never follows a symlinked command folder, removes the folder only once nothing else is left in it, and lists each thing it kept. A folder holding nothing OpenSpec wrote is no longer reported as legacy at all. A folder holding only OpenSpec's files, or nothing, is still removed exactly as before, with the same summary line.
- [#1866](https://github.com/Fission-AI/OpenSpec/pull/1866) [`8146be5`](https://github.com/Fission-AI/OpenSpec/commit/8146be5546918cdffce860f1e327d929c5a49bd3) Thanks [@dwin-gharibi](https://github.com/dwin-gharibi)! - Stop one unresolvable file from breaking `openspec list`. To sort changes by recency, `list` stats every file inside each change, and any entry it could not stat failed the whole command: a dangling symlink, such as the `.#tasks.md` lock Emacs keeps beside every file with unsaved edits, or a symlink loop made `list` exit 1 and `list --json` report `"changes": []`, so agents discovering work through it saw no changes at all. An entry that no longer resolves (removed mid-walk, a dangling symlink, or a loop) is now skipped when computing a change's last-modified time. Valid symlinks are dated as before, and any other error, such as a permission failure, still fails the listing.
- [#1849](https://github.com/Fission-AI/OpenSpec/pull/1849) [`09a999b`](https://github.com/Fission-AI/OpenSpec/commit/09a999bbb258c2ad6d7cdc33436c698c15d4eebe) Thanks [@clay-good](https://github.com/clay-good)! - Report a change directory nested in a namespace folder instead of silently listing the folder around it as a change. Specs can be nested by domain (`specs/mobile/tutorial-videos/spec.md`), so it looks reasonable to lay changes out the same way, but a change is only ever a directory directly under `changes/`: `changes/mobile/refresh-token/` left the real change invisible while `mobile` was reported as a task-less change everywhere. `openspec archive mobile` then moved the unfinished change into the archive under the namespace's name and applied none of its deltas. `openspec list` now marks the folder `not a change` and names the nested directories and a flat alternative, `openspec show`, `openspec status --change` and `openspec status --all` say the same instead of reporting a missing proposal or a full artifact plan, `openspec validate` reports it instead of "must have at least one delta", `openspec list --json` carries a `warnings` entry, and `openspec archive` refuses the folder outright. Detection looks up to three directory levels below `changes/`, which covers every namespace layout seen in practice; a change buried deeper than that behaves as it did before. Fixes [#1846](https://github.com/Fission-AI/OpenSpec/issues/1846).
- [#1902](https://github.com/Fission-AI/OpenSpec/pull/1902) [`eb03b9e`](https://github.com/Fission-AI/OpenSpec/commit/eb03b9e93320cf74a0df9043577d8023116cb1aa) Thanks [@clay-good](https://github.com/clay-good)! - Install shell completions with the Nix flake package ([#1785](https://github.com/Fission-AI/OpenSpec/pull/1785)). The package now ships bash, zsh and fish completions in their standard `share/` locations, so Nix users get tab completion without running `openspec completion install` against their home directory.
- [#1775](https://github.com/Fission-AI/OpenSpec/pull/1775) [`626269e`](https://github.com/Fission-AI/OpenSpec/commit/626269ed732250492d8bd220a83df23dd756ee5d) Thanks [@clay-good](https://github.com/clay-good)! - Generated skills and commands no longer point at workflows the active profile does not install. On the default `core` profile, the update workflow told agents to hand off to `/opsx:continue` for missing artifacts and to `/opsx:new` for a change of intent, neither of which `core` generates. Every cross-workflow handoff is now decided at generation time against the installed workflow set, and renders a concrete CLI fallback (`openspec status`, `openspec instructions`, `openspec archive`) when the workflow it would name is absent, rather than relying on a runtime availability check the agent had to perform. The onboarding tutorial's command tables are likewise built from the workflows you actually have.
Also folds in [#1735](https://github.com/Fission-AI/OpenSpec/issues/1735), which fixed the same issue ([#1734](https://github.com/Fission-AI/OpenSpec/issues/1734)) by removing the optional handoffs outright. The CLI's own runtime instructions no longer name the `openspec-continue-change` skill either, since those strings are chosen at run time and cannot be resolved against a profile; and the blocked-state fallback now carries the full CLI recovery (select the next `ready` artifact from `openspec status`, read its rules with `openspec instructions`, keep the selected `--store`) rather than a one-line pointer.
- [#1870](https://github.com/Fission-AI/OpenSpec/pull/1870) [`e01ed07`](https://github.com/Fission-AI/OpenSpec/commit/e01ed070f18e15529f82563d4c5af35d8124bad3) Thanks [@dwin-gharibi](https://github.com/dwin-gharibi)! - Stop archiving a change whose delta was written somewhere `archive` never reads. `validate` and `archive` read a change's deltas only from `specs/<capability-path>/spec.md`, but the spec-driven artifact graph counts any markdown file under `specs/` as the specs being written, so a delta at `specs/user-auth.md`, or in a second file beside a capability's `spec.md`, was reported done by `status` and ready by `instructions apply` with no warning, rejected by `validate` only as "no deltas found", and then archived with exit 0 and nothing merged into `openspec/specs/`. A markdown file that carries delta sections but is not a capability's `spec.md` is now a validation error naming the file and the `spec.md` its requirements belong in; `archive` runs that validation and refuses the change instead of archiving it unmerged, and `instructions apply` lists each such file in its `warnings`. `--no-validate` still archives as before, a change with no spec files still archives, and notes without delta sections under `specs/` are not affected.
- [#1806](https://github.com/Fission-AI/OpenSpec/pull/1806) [`6e62b1d`](https://github.com/Fission-AI/OpenSpec/commit/6e62b1d522cfadb4b9836b63d5afa127bc950743) Thanks [@dwin-gharibi](https://github.com/dwin-gharibi)! - Refuse a `## RENAMED Requirements` section whose `FROM:` and `TO:` lines do not pair up, instead of guessing. The reader kept one pending pair and dropped whatever did not fit: a `TO:` before its `FROM:`, a `FROM:` displaced by a second `FROM:`, or a trailing `FROM:` vanished with no diagnostic. Listing the old names and then the new ones paired the second `FROM:` with the first `TO:`, so `openspec archive` renamed a requirement the delta never named, under a name written for a different one, and exited 0. `openspec validate` now reports each unpaired line as an ERROR with its line number, and archive refuses the change until the pairing is fixed. Well-formed renames, including several consecutive pairs, are unchanged. A change that used to archive with a malformed RENAMED section is now rejected. Fixes [#1805](https://github.com/Fission-AI/OpenSpec/issues/1805).
- [#1860](https://github.com/Fission-AI/OpenSpec/pull/1860) [`4b5c07a`](https://github.com/Fission-AI/OpenSpec/commit/4b5c07a0c2e5a4a1dcb3ed9f3a040f826eb7d457) Thanks [@dwin-gharibi](https://github.com/dwin-gharibi)! - Read a requirement heading written with a CommonMark closing sequence, such as `### Requirement: Late Fees ###`, as the requirement it renders as. The trailing `#` run stayed in the name, so a REMOVED written that way looked for "Late Fees ###", missed the requirement, and archive exited 0 with a false "treating it as already removed" warning while the requirement stayed in the spec; a closed MODIFIED or RENAMED heading failed as "not found", and a closed and an open heading of one requirement were not reported as duplicates. Requirement names now drop the closing run wherever they are read, exactly as scenario names already did: only a run preceded by a space or tab counts, so a name such as `C#` keeps its `#`. Headings without a closing run are unaffected.
- [#1868](https://github.com/Fission-AI/OpenSpec/pull/1868) [`7090e16`](https://github.com/Fission-AI/OpenSpec/commit/7090e16d74dfe588dad72bc4fda9bf124e71b0af) Thanks [@dwin-gharibi](https://github.com/dwin-gharibi)! - Reject a schema whose `apply.requires` names an artifact that does not exist. `parseSchema` checked every artifact's `requires` but never `apply.requires`, so `openspec schema validate` passed a one-character typo there, and apply then skipped the unknown id: `apply.requires: [desgin]` turned the apply gate off and told the agent "Proceed with implementation" with only a proposal written. That is now a schema error, raised wherever the schema is loaded, exactly like an unknown artifact `requires`, and it names the bad id and the artifacts the schema declares. `openspec schema validate` also warns, without failing, when `apply.tracks` isn't exactly equal to some artifact's `generates` value, because OpenSpec finds the tracked artifact by comparing those two strings and can otherwise not tell which artifact's progress the file belongs to. That covers a typo such as `task.md` and also `tracks: tasks/main.md` against `generates: tasks/*.md`, where the glob does produce the file but the strings still differ. Apply reads that path as written either way, so schemas that track a hand-written file keep loading and working. Every built-in schema parses as before.
- [#1856](https://github.com/Fission-AI/OpenSpec/pull/1856) [`46ff91f`](https://github.com/Fission-AI/OpenSpec/commit/46ff91f2d626ef2c3f9f55ff345aa23cd44a6e95) Thanks [@dwin-gharibi](https://github.com/dwin-gharibi)! - Make `openspec show --json --deltas-only` report the deltas archive applies. `ChangeParser`, which backs `show --json`, the `change list` delta counts and archive's proposal warnings, read delta specs with its own section lookup instead of `parseDeltaSpec`, the reader archive uses, and the two disagreed. A REMOVED written in the bullet form (`` - `### Requirement: X` ``) was invisible to it, so it fell back to the proposal's "What Changes" prose and reported an invented MODIFIED while archive deleted the requirement; a repeated section header was read only once; and a RENAMED line written with `*` or `+` was dropped. The inspection command OpenSpec's own error text recommends therefore misreported a deletion. `ChangeParser` now derives every operation from `parseDeltaSpec`, and a change whose delta spec files carry a delta section is described by them alone, so proposal prose is never reported in place of what archive applies. Requirement text and scenarios are read exactly as before, header-form deltas produce the same output, and a change with no delta spec files, or a legacy change whose spec files carry no delta section, still falls back to the "What Changes" bullets.
- [#1786](https://github.com/Fission-AI/OpenSpec/pull/1786) [`8b99c07`](https://github.com/Fission-AI/OpenSpec/commit/8b99c07bd0d455f72e746d3950f03e52a025d655) Thanks [@clay-good](https://github.com/clay-good)! - `openspec status` now names the command that moves the change forward.
The text output reported state and stopped there, so picking a change back up (after a lost session, or on a change you did not start) meant already knowing which command came next. The JSON surface had carried that command all along in `nextSteps`; the text surface never printed it.
Status now ends with a `Next:` line: the next ready artifact's `openspec instructions` command while planning is unfinished, and `openspec instructions apply` once every planning artifact exists. It carries `--store <id>` when the resolved root is a store, and it is built from the same source as the JSON `nextSteps` sentence, so the two surfaces cannot name different commands.
- [#1882](https://github.com/Fission-AI/OpenSpec/pull/1882) [`208b5b5`](https://github.com/Fission-AI/OpenSpec/commit/208b5b55106fbeda2ed9f099671b8ce85a01cbaa) Thanks [@dwin-gharibi](https://github.com/dwin-gharibi)! - Stop a store named `specs` or `changes` from taking over root selection. Stores are placed at `~/openspec/<id>`, so a store with one of those ids is itself `~/openspec/specs` or `~/openspec/changes`, and that made `$HOME` look like a planning root. Every command run anywhere under the home directory then resolved `$HOME` as the nearest root: the global `defaultStore` was never consulted, and `new change` wrote into `~/openspec/changes`, outside any store. A `specs/` or `changes/` directory that carries store metadata no longer counts as planning content of the directory above it, so these stores resolve like any other. A real project's `openspec/specs/` and `openspec/changes/` are unaffected.
- [#1880](https://github.com/Fission-AI/OpenSpec/pull/1880) [`9f8dec5`](https://github.com/Fission-AI/OpenSpec/commit/9f8dec5dd937da78bbdaeff5e5dfd041bb43cf5c) Thanks [@dwin-gharibi](https://github.com/dwin-gharibi)! - Stop `openspec store remove` deleting a store the user did not name. Remove deletes the target's folder recursively, but it checked only the target's own metadata, so any other registered store living inside that folder was deleted with it, uncommitted planning work included, while its registry entry was left pointing at a path that no longer existed. The natural way to get there is a shared store vendored into another as a git submodule, a layout `store register` accepts. Remove now refuses when another registration points inside the folder, checked under the same registry lock that commits the removal, and the error names each nested store with the `openspec store unregister` command to run first. Removing a store whose other registrations are siblings is unchanged, and `store register` still accepts nested checkouts.
- [#1884](https://github.com/Fission-AI/OpenSpec/pull/1884) [`5d22145`](https://github.com/Fission-AI/OpenSpec/commit/5d221456e57feb9277de40482fade427201b9bdb) Thanks [@dwin-gharibi](https://github.com/dwin-gharibi)! - Let `openspec store setup --no-init-git` create a store inside an existing Git repository. Setup refuses a path inside another repository because initializing the store there would nest one repository in another, but it ran that check even with `--no-init-git`, which creates no repository at all. Users who keep their home directory as a dotfiles repository therefore could not set up a store at the recommended `~/openspec/<id>` path with any flag. With `--no-init-git` the check is now skipped, and the store never records the enclosing repository's remote. The default setup and an explicit `--init-git` still refuse a path inside another repository.
- [#1862](https://github.com/Fission-AI/OpenSpec/pull/1862) [`8fc65b7`](https://github.com/Fission-AI/OpenSpec/commit/8fc65b7f70c4bd730a1dbe500cbe165d156f3c58) Thanks [@dwin-gharibi](https://github.com/dwin-gharibi)! - Count task checkboxes under every CommonMark list marker. The task counter shared by `list`, `status`, `view`, `instructions apply`, `validate --archived` and archive's incomplete-task check recognized only `-` and `*` bullets, so a task written as an ordered item (`1. [ ]`, `1) [ ]`) or under a `+` bullet was invisible to all of them: a change with unfinished ordered tasks reported "✓ Complete", and `openspec archive` archived it without its incomplete-task warning. Task lines under `+` and ordered markers (`.` or `)`, up to nine digits, as CommonMark allows) now count exactly like `-` and `*` ones, including nested sub-tasks, CRLF files and the existing tolerance of a missing space after the marker, and task-numbering checks now see them too. Ordered and `+` items without a checkbox are still ignored, and `-` and `*` tasks count as before.
- [#1777](https://github.com/Fission-AI/OpenSpec/pull/1777) [`3312af4`](https://github.com/Fission-AI/OpenSpec/commit/3312af4799eb162d3ddb7804d643ace5282c22cb) Thanks [@clay-good](https://github.com/clay-good)! - Start generated proposal, spec, design, and tasks files with a top-level heading, so artifacts are complete markdown documents instead of files whose first line is a section header. Editors that run markdownlint no longer flag every OpenSpec artifact with MD041. `openspec schema init` scaffolds custom templates the same way.
`openspec show --json` and `openspec change list --json` keep naming a change by its id when its proposal opens with the template's bare `# Proposal` title.
- [#1778](https://github.com/Fission-AI/OpenSpec/pull/1778) [`7de2404`](https://github.com/Fission-AI/OpenSpec/commit/7de24044ef4c635f634b78fa6bc4b5905967bfd8) Thanks [@clay-good](https://github.com/clay-good)! - Make the vendor-neutral tool target findable when your assistant is not on the list. `openspec init` now shows it as "Other / Universal (shared .agents skills)"; the picker's search box matches it on `universal`, `other`, `generic`, `custom`, `proprietary`, `unlisted`, `unsupported`, `vendor-neutral` and `agents.md`; a search that matches nothing points at it instead of ending at "No matches"; and `--tools <unknown>` names it in the error. The search box also accepts punctuation, so `.agents` and `amazon-q` filter instead of silently dropping their `.` and `-`.
- [#1876](https://github.com/Fission-AI/OpenSpec/pull/1876) [`605d9e7`](https://github.com/Fission-AI/OpenSpec/commit/605d9e7a2bb5c1bab90268933f9b84ff1eb8807c) Thanks [@dwin-gharibi](https://github.com/dwin-gharibi)! - Stop OpenSpec rewriting a global config file it cannot parse. After a hand edit left a typo such as a trailing comma in `config.json`, the next command of any kind, including read-only ones like `openspec list`, read the fallback defaults as telemetry consent, minted a new anonymous ID and wrote it back, replacing the whole file: a `telemetry.enabled false` opt-out, the chosen profile and the workflow list were all lost, and usage events were sent. A config file that exists but does not hold a JSON object, whether it failed to parse or its root is something else such as `null`, an array or a string, is now never written implicitly, and telemetry and the update check treat it as opted out. `config set`, `config unset` and `config profile` refuse with an error that names the file and points to `openspec config edit`, and `openspec config reset --all` still replaces it. The existing "Invalid JSON" warning is unchanged, and valid or missing config files behave exactly as before.
- [#1840](https://github.com/Fission-AI/OpenSpec/pull/1840) [`fede536`](https://github.com/Fission-AI/OpenSpec/commit/fede536c27e03c1aaa3c17caffa837f483d9e9b9) Thanks [@clay-good](https://github.com/clay-good)! - Resolve the contradiction that left `/opsx:update`'s only write path without a governing rule. Step 4 told the agent to "Apply the requested edit", while step 5 and the guardrails told it to write only after the user confirms each revision, so the same `/opsx:update "the design now uses X"` either wrote immediately or stopped and showed the proposed revision first, depending on which passage the agent weighed. Step 4 now drafts the edit in the conversation and step 5 owns every artifact write, matching the workflow's own specified behavior: propose each revision and apply it only after user confirmation. Fixes [#1836](https://github.com/Fission-AI/OpenSpec/issues/1836).
- [#1858](https://github.com/Fission-AI/OpenSpec/pull/1858) [`db560ae`](https://github.com/Fission-AI/OpenSpec/commit/db560ae33f565b76ebbc040782ec7007295e8133) Thanks [@dwin-gharibi](https://github.com/dwin-gharibi)! - Stop `validate` accepting a requirement whose only scenario is a bare header. The delta scenario counter counted every `####` header, while the spec path that archive uses to validate the rebuilt spec keeps a scenario only when its body has content, so `validate` called such a change valid and `archive` then refused it with a generic "Requirement must have at least one scenario" that did not name the requirement. Both paths now share one rule, `hasScenarioBody`, and read a scenario's body up to the same boundary, so `validate` rejects exactly what archive rejects, naming the requirement and saying that a header with no body under it does not count. A scenario whose body is only a fenced block or a deeper header still counts, a requirement with one real scenario is still accepted even when another is empty, and main-spec validation is unchanged.
- [#1774](https://github.com/Fission-AI/OpenSpec/pull/1774) [`09984b8`](https://github.com/Fission-AI/OpenSpec/commit/09984b824254f9e35bcdf628fdb052a689a57f37) Thanks [@clay-good](https://github.com/clay-good)! - ### Bug Fixes
- **Task lists without checkboxes are now caught**: a `tasks.md` written as plain bullets or a numbered list counts as zero tasks, so `openspec list` and `openspec status` reported "No tasks" and `openspec archive` had no unfinished work to warn about. `openspec validate` now warns when a change's tracked task files contain list items but no checkbox at all, and points at the first offending line.
- [#1852](https://github.com/Fission-AI/OpenSpec/pull/1852) [`5f5914e`](https://github.com/Fission-AI/OpenSpec/commit/5f5914e7f7a817262c7564ac92694db833564978) Thanks [@clay-good](https://github.com/clay-good)! - Match the natural "openspec <verb>" phrasing to the workflow it names. Users and agents say "openspec propose" or "do an openspec apply", but no workflow skill's description contained that phrasing (and a skill's description is what an agent matches on), so the phrase read as an invitation to hand-build the artifacts with the CLI instead of running the workflow. Every workflow skill's description now names the phrasings a user actually types ("openspec propose", "opsx apply", and so on). Run `openspec update` to pick it up. `openspec update` itself is deliberately left unclaimed: it is a real CLI command that refreshes generated files, unrelated to the update-change workflow, which claims "openspec update change" instead. Commands-only installs write no skills and are unchanged. Fixes [#1221](https://github.com/Fission-AI/OpenSpec/issues/1221).
## 1.13.0
### Minor Changes
- [#1783](https://github.com/Fission-AI/OpenSpec/pull/1783) [`8ba4ac1`](https://github.com/Fission-AI/OpenSpec/commit/8ba4ac1b16a33830a766f0c03d224d516b6a90ce) Thanks [@clay-good](https://github.com/clay-good)! - Apply now says when a change has no delta specs. Apply gates on the schema's `apply.requires` alone, so a change whose `tasks.md` was written ahead of its specs read as ready to implement even though it had no spec deltas at all — the state `openspec validate` rejects. `openspec instructions apply` now reports that gap as a warning (text and `--json`), naming both ways out: write the specs, or declare `skip_specs: true`. Changes that have specs, declare `skip_specs`, or are still blocked on their own required artifacts are unaffected.
A blocked apply also names the whole chain now, not just the first hop: a change holding only a proposal reported `Missing artifacts: tasks` while the specs that `tasks` depends on were missing too, which reads as an instruction to write the tracking file straight from the proposal. The full build order is reported as `missingPrerequisites` in `--json`. The remedies these messages give are CLI commands (`openspec instructions <artifact> --change <name>`) rather than the `openspec-continue-change` skill, which the `core` profile never installs.
### Patch Changes
- [#1798](https://github.com/Fission-AI/OpenSpec/pull/1798) [`aedf4d0`](https://github.com/Fission-AI/OpenSpec/commit/aedf4d0c64c4bdde2e21f199aee93fa0d598c33e) Thanks [@dwin-gharibi](https://github.com/dwin-gharibi)! - Stop archive rewriting the inside of fenced code blocks. The final assembly in `buildUpdatedSpec` collapsed runs of blank lines across the whole rebuilt document to tidy the seams between the slices it rejoins, but the pass was not fence-aware, so a requirement documenting a sample with two or more consecutive blank lines had that sample silently edited on archive, and edited again on every later archive. That matters wherever whitespace carries meaning: YAML block scalars, Python, expected-output fixtures, Markdown inside Markdown. Blank runs are now collapsed only outside fenced blocks, using the same `buildCodeFenceMask` every other structural pass in the module already used. Behavior outside fences is unchanged, including that only a truly empty line counts as blank, so a line of spaces is still never a collapse boundary. Fixes [#1797](https://github.com/Fission-AI/OpenSpec/issues/1797).
- [#1800](https://github.com/Fission-AI/OpenSpec/pull/1800) [`fadac3e`](https://github.com/Fission-AI/OpenSpec/commit/fadac3e1c927bf5180ce82c806435c61db5d5adb) Thanks [@dwin-gharibi](https://github.com/dwin-gharibi)! - Read a removal or rename written with `*` or `+` as the operation it is. CommonMark opens a bullet list with `-`, `*` or `+`, but the bullet form of `## REMOVED Requirements` and the `FROM:`/`TO:` lines of `## RENAMED Requirements` both hardcoded `-`, so either other marker matched nothing at all. The operation then silently never happened: `openspec validate` reported the change valid, `openspec archive` exited 0 with "Specs updated successfully", and the requirement that was supposed to be deleted or renamed stayed exactly as it was. The change archived as complete, leaving the spec quietly disagreeing with the delta that was meant to update it. Both forms now accept `[-*+]`, the `FROM:`/`TO:` bullet stays optional, and the plain `### Requirement:` header form is unchanged. Fixes [#1799](https://github.com/Fission-AI/OpenSpec/issues/1799).
- [#1802](https://github.com/Fission-AI/OpenSpec/pull/1802) [`8251763`](https://github.com/Fission-AI/OpenSpec/commit/8251763ecdc349a0a1484f09da85f7e5c33f4836) Thanks [@dwin-gharibi](https://github.com/dwin-gharibi)! - Apply every delta section, not just one copy of each. A delta file that wrote the same header twice, two `## ADDED Requirements` sections say, silently kept one of them: sections were collected into a record keyed by title, so a repeated title overwrote the earlier body, and the case-insensitive lookup returned only the first entry that folded to the target, so `## ADDED Requirements` beside `## Added Requirements` left the second unread. Every requirement under the discarded copy was gone before validation or the merge could see it, so `openspec validate` reported zero issues and `openspec archive` exited 0 having applied less than the author wrote, then moved the change to the archive with the live spec quietly diverging from what was reviewed. Sections are now kept as a list and every body whose title matches is read, each keeping its own line numbers so diagnostics still point at the right copy. Rename pairs are read per section, so a `FROM:` in one copy of the header can never pair with a `TO:` in another. An author does not have to repeat a header on purpose to hit this: a delta documenting OpenSpec's own syntax inside a fenced example produces the duplicate on its own. Fixes [#1801](https://github.com/Fission-AI/OpenSpec/issues/1801).
- [#1657](https://github.com/Fission-AI/OpenSpec/pull/1657) [`6d2dbe6`](https://github.com/Fission-AI/OpenSpec/commit/6d2dbe62d3386ac0df576136759775678102acf2) Thanks [@clay-good](https://github.com/clay-good)! - Load project context before proposal planning, using the selected project or store root and honoring config precedence and validation limits. When no root exists, stop without writing files and offer initialization instead of creating an implicit root.
- [#1700](https://github.com/Fission-AI/OpenSpec/pull/1700) [`3915db7`](https://github.com/Fission-AI/OpenSpec/commit/3915db763ad5394b29cdd895c87a91c837313ae8) Thanks [@clay-good](https://github.com/clay-good)! - Teach the generated guidance how to find and read a project's specs. `openspec list --specs` appeared in no generated skill, command, or artifact instruction, while `openspec list --json` (the in-flight *change* list) appeared throughout, so an agent asked to read the existing specs first enumerated changes instead and reported the step complete against the wrong object. The explore skill and command now list the spec inventory alongside the change list and say which is which, and the spec-driven `proposal` and `specs` instructions name the command where they ask for existing capabilities to be researched and for a delta's path to match an existing one. Both steps carry `--store "<id>"`, and capabilities are read with `openspec show "<spec-id>" --type spec --json --no-scenarios` so the read resolves against the same root the listing came from. Fixes [#1689](https://github.com/Fission-AI/OpenSpec/issues/1689).
The filtered read is only an overview. Agents read relevant specs in full, including scenarios, before deciding what is already covered or what should change.
- [#1779](https://github.com/Fission-AI/OpenSpec/pull/1779) [`3c6d318`](https://github.com/Fission-AI/OpenSpec/commit/3c6d318b837f443d1aabf969ce368e78ae12e891) Thanks [@clay-good](https://github.com/clay-good)! - `openspec init` and `openspec update` now name the workflows your profile left out and how to add them, so a command that was never installed no longer reads as a broken setup.
- [#1808](https://github.com/Fission-AI/OpenSpec/pull/1808) [`d9e1a28`](https://github.com/Fission-AI/OpenSpec/commit/d9e1a28c38927e6d649781976cd1a753e28973af) Thanks [@dwin-gharibi](https://github.com/dwin-gharibi)! - Stop `openspec update` reporting a tool up to date while a damaged command file sits on disk. The check read the `generatedBy` version marker in a tool's skill files alone, which proves only that the skill files came from this CLI and says nothing about the command files written beside them, so a hand-edited or truncated command file left update printing "All 1 tool(s) up to date" and repairing nothing; the file could be restored only by knowing to pass `--force`. A deleted command file was already detected, so the claim was false only for a damaged one. Update now also compares command-file content, using the comparison that already existed and was simply never consulted once a skill file supplied a version. Scoped to tools configured for both skills and commands, so the commands-only path is unchanged, and skipped when the delivery mode generates no commands for the tool. Fixes [#1807](https://github.com/Fission-AI/OpenSpec/issues/1807).
- [#1782](https://github.com/Fission-AI/OpenSpec/pull/1782) [`c170dc7`](https://github.com/Fission-AI/OpenSpec/commit/c170dc77adbe868ce731e07df2806a7ca8fcb4ec) Thanks [@clay-good](https://github.com/clay-good)! - Fix `retire_capabilities` refusing any spec whose scenario bullets wrap onto a second line. The continuation line was counted as content the merge could not account for, which blocked the retirement and suppressed the hint that names the marker ([#1780](https://github.com/Fission-AI/OpenSpec/issues/1780)). A spec bulleted with `+` is covered too: naming only `-` and `*` as list markers reported every one of its scenario bullets as unaccounted content, so that capability could not be retired at all either.
## 1.12.0
### Minor Changes
+56
View File
@@ -0,0 +1,56 @@
# Contributing
Thanks for helping improve OpenSpec.
## 1. Open a discussion or an issue first
Every change starts here, including small ones.
- [Start a discussion](https://github.com/Fission-AI/OpenSpec/discussions) if it affects OpenSpec's core design.
- [Open an issue](https://github.com/Fission-AI/OpenSpec/issues) for bugs and everything else.
This is so we can agree on the approach before you spend time building. PRs without a linked issue or a prior discussion may be closed.
## 2. Decide whether it needs a change proposal
A bug fix, a typo, or a small improvement goes straight to a PR.
A new feature, a significant refactor, or anything that changes OpenSpec's architecture needs an OpenSpec change proposal first, so we can align on intent and goals before implementation begins. Open it as a PR containing only `openspec/changes/<name>/` and wait for it to be approved before you write the code.
When writing a proposal, keep the OpenSpec philosophy in mind: we serve a wide variety of users across different coding agents, models, and use cases. Changes should work well for everyone.
If you are not sure which side of the line your change falls on, ask in the discussion or issue from step 1.
## 3. Make your change
You need Node 20.19+ and pnpm.
```bash
pnpm install
pnpm build # tests run against the build output
pnpm test
pnpm exec tsc --noEmit
pnpm lint
```
Those four commands are what CI runs, so a green local run means a green CI run.
Run `pnpm changeset` if your change affects users, and commit the file it generates.
### Keep the CLI's startup fast
Editors, agents and OpenSpec Desktop run the CLI many times, and each call pays for every module it loads before the command runs. Before this rule, `openspec --version` loaded 485 modules: about 0.5 s per call on a Windows machine. So a command loads only the command definitions and its own code:
- **Definitions** (name, options, help text) go in `src/cli/index.ts` or `src/cli/commands/<name>.ts`. Import nothing heavy there: no zod, yaml, fast-glob, ora, and no other command's code.
- **The command's code** goes in `src/commands/<name>.ts` or `src/core/`, loaded inside the action with `await import()`.
`test/cli-e2e/startup-modules.test.ts` checks which modules each command loads, and fails if a definition starts pulling in an implementation. When you add a command, add it to that test's list.
## 4. Open the PR
- Branch off `main` in your fork.
- Title it as a conventional commit: `type(scope): subject`, for example `fix(archive): keep authored Purpose`.
- Link what you opened in step 1: `Closes #123` for an issue, or a link to the discussion when there is no issue.
- If a coding agent wrote the code, say which agent and model, and confirm you tested it. AI-generated code is welcome when it has been verified.
Maintainers are listed in [MAINTAINERS.md](MAINTAINERS.md).
+18 -18
View File
@@ -122,7 +122,7 @@ Solo, OpenSpec keeps you and your AI honest on a single repo. On a team, the har
## Quick Start
**Requires Node.js 20.19.0 or higher.**
**Requires Node.js 20.19.0 or higher.** Homebrew installs it as a dependency.
Install OpenSpec globally:
@@ -130,6 +130,12 @@ Install OpenSpec globally:
npm install -g @fission-ai/openspec@latest
```
Or install the official [Homebrew formula](https://formulae.brew.sh/formula/openspec) on macOS or Linux:
```bash
brew install openspec
```
Then navigate to your project directory and initialize:
```bash
@@ -137,11 +143,11 @@ cd your-project
openspec init
```
> **Want your AI to do it?** Paste the [setup prompt](docs/installation.md#install-with-your-ai-assistant) into your coding assistant — it installs the CLI, runs `openspec init`, and verifies the result.
> **Want your AI to do it?** Paste the [setup prompt](docs-lab/start/installation.md#install-with-your-ai-assistant) into your coding assistant — it installs the CLI, runs `openspec init`, and verifies the result.
Now talk to your AI:
- **Not sure what to build yet?** Start with `/opsx:explore`, a no-stakes thinking partner that reads your code, weighs options, and shapes a plan before anything is written. ([Explore guide](docs/explore.md))
- **Not sure what to build yet?** Start with `/opsx:explore`, a no-stakes thinking partner that reads your code, weighs options, and shapes a plan before any code gets written. ([Explore guide](docs/explore.md))
- **Already know what you want?** Go straight to `/opsx:propose <what-you-want-to-build>`.
Both are in the default profile. If you want the expanded workflow (`/opsx:new`, `/opsx:continue`, `/opsx:ff`, `/opsx:verify`, `/opsx:bulk-archive`, `/opsx:onboard`), select it with `openspec config profile` and apply with `openspec update`.
@@ -151,7 +157,7 @@ Both are in the default profile. If you want the expanded workflow (`/opsx:new`,
> [!NOTE]
> Not sure if your tool is supported? [View the full list](docs/supported-tools.md) – we support 30+ tools and growing.
>
> Also works with pnpm, yarn, bun, and nix. [See installation options](docs/installation.md).
> Also works with Homebrew, pnpm, yarn, bun, and Nix. [See installation options](docs-lab/start/installation.md).
## Docs
@@ -208,6 +214,12 @@ AI coding assistants are powerful but unpredictable when requirements live only
npm install -g @fission-ai/openspec@latest
```
If you installed OpenSpec with Homebrew:
```bash
brew upgrade openspec
```
**Refresh agent instructions**
Run this inside each project to regenerate AI guidance and ensure the latest slash commands are active:
@@ -224,21 +236,9 @@ openspec update
## Contributing
**Small fixes** — Bug fixes, typo corrections, and minor improvements can be submitted directly as PRs.
Open a discussion (for core design changes) or an issue before you open a PR, and link the issue or discussion from the PR. New features, significant refactors, and architectural changes need an OpenSpec change proposal first.
**Larger changes** — For new features, significant refactors, or architectural changes, please submit an OpenSpec change proposal first so we can align on intent and goals before implementation begins.
When writing proposals, keep the OpenSpec philosophy in mind: we serve a wide variety of users across different coding agents, models, and use cases. Changes should work well for everyone.
**AI-generated code is welcome** — as long as it's been tested and verified. PRs containing AI-generated code should mention the coding agent and model used (e.g., "Generated with Claude Code using claude-opus-4-5-20251101").
### Development
- Install dependencies: `pnpm install`
- Build: `pnpm run build`
- Test: `pnpm test`
- Develop CLI locally: `pnpm run dev` or `pnpm run dev:cli`
- Conventional commits (one-line): `type(scope): subject`
→ **[CONTRIBUTING.md](CONTRIBUTING.md)**: the full process, from first issue to merged PR
## Other
+1 -1
View File
@@ -96,7 +96,7 @@ the page or rewriting the goal in both places, never letting them drift.
| [Overview](start/overview.md) | _TODO: emptied 2026-08-21 for a from-scratch rewrite and pulled from the site (`/docs` redirects to Installation meanwhile); the old goal line was dropped as too weak a pitch. Brief in Notes.md._ |
| [Installation](start/installation.md) | Install the `openspec` CLI on your machine, update it, and uninstall it. |
| [Set up your project](start/setup.md) | Add OpenSpec to a project: run init, see what it wrote, and adjust it. |
| [Quickstart](start/quickstart.md) | Your first change on your existing repo, from idea to archived. |
| [Quickstart](start/quickstart.md) | Your first change in a new or existing project, from idea to archived. |
### Guides: understand the system, use it well, bring it to your codebase and team
+7 -10
View File
@@ -35,8 +35,8 @@ For example, with a `context` field and the rule from the top of this page, here
<!-- From your config.yaml: context -->
<project_context>
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 -->
@@ -58,8 +58,6 @@ For example, with a `context` field and the rule from the top of this page, here
Your config arrives first, then OpenSpec's built-in instruction and template. Rules add to the built-ins and never replace them. Edits to config.yaml reach the agent on the next run.
[Workflow runs](../reference/architecture/workflow-runs.md) covers the full run, from invocation to written artifacts.
## The fields
Three fields shape what the agent receives. Each field's exact contract (types, limits, validation) is in [Project configuration (config.yaml)](../reference/configuration/config-yaml.md).
@@ -76,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
+7 -1
View File
@@ -125,7 +125,7 @@ The scaffold is bare. Artifacts come from the built-in four ids only, and the ge
A fork has two kinds of files to edit:
- **templates/** change the skeleton of each document. Add a section to the tasks template and every new tasks.md starts with it.
- **templates/** change the skeleton of each document. Add a section to the tasks template and every new tasks.md starts with it. Keep the `#` title on the first line: the artifact inherits it, so every generated file opens as a titled document.
- **schema.yaml** changes the workflow itself: which artifacts exist, what each one requires first, and the instruction the agent gets when creating it.
For example, to drop the design document for a leaner flow:
@@ -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.
+22 -1
View File
@@ -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.
+3 -2
View File
@@ -17,7 +17,8 @@ once the prose lands. -->
If it has a row in the [support matrix](../reference/supported-tools.md), yes.
Pick its id at init. If it isn't listed but reads the shared `.agents/skills/`
folder, pick **Shared `.agents` skills** (`--tools agents`). If neither, request
it in the [OpenSpec repo](https://github.com/Fission-AI/OpenSpec/issues).
folder, pick **Other / Universal** (`--tools agents`), covered by the support
matrix's Other / Universal section. If neither, request it in the
[OpenSpec repo](https://github.com/Fission-AI/OpenSpec/issues).
## Where did the old /openspec:* commands go?
+8
View File
@@ -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
+17
View File
@@ -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:
+232 -108
View File
@@ -22,7 +22,6 @@
| [`openspec show`](#openspec-show) | Print a change or spec, as markdown or JSON. |
| [`openspec view`](#openspec-view) | One-screen dashboard of specs and changes. |
| [`openspec validate`](#openspec-validate) | Check changes and specs for structural issues. |
| [`openspec sync`](#openspec-sync) | Fold a change's delta specs into the main specs, without archiving it. |
| [`openspec archive`](#openspec-archive) | Move a completed change to the archive and update the main specs. |
**Workflows and schemas**
@@ -51,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. |
@@ -78,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 |
@@ -89,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. |
@@ -118,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
@@ -303,6 +319,15 @@ Pass --allow-unknown to bypass this check.
Error: Invalid configuration - delivery: Invalid option: expected one of "both"|"skills"|"commands"
```
If the config file exists but does not hold a JSON object, whether because it is not valid JSON at all or because its root is something else such as `null` or an array, `config set`, `config unset` and `config profile` exit 1 and leave the file unchanged. Fix it with `openspec config edit`, or replace it with `openspec config reset --all`:
```
Error: /home/you/.config/openspec/config.json could not be parsed, so it was left unchanged.
Fix it with "openspec config edit", or reset it with "openspec config reset --all".
```
Until it is fixed, telemetry and the update check stay off.
### openspec config unset
```bash
@@ -315,7 +340,7 @@ Removes the key so the default applies again. Keys with built-in defaults always
Unset delivery (reverted to default)
```
A key with no value at all prints `Key "featureFlags.nothere" was not set`. Both cases exit 0.
A key with no value at all prints `Key "featureFlags.nothere" was not set`. Both cases exit 0. A config file that cannot be parsed exits 1 instead, as for `config set`.
### openspec config reset
@@ -351,7 +376,11 @@ Without `--all` it exits 1 and prints the usage line.
openspec config edit
```
Opens the config file in `$EDITOR` (falling back to `$VISUAL`), creating it with defaults first if missing. When the editor closes, the file is validated. Invalid JSON or an invalid config exits 1. With no editor configured it exits 1:
Opens the config file in `$EDITOR` (falling back to `$VISUAL`), creating it with defaults first if missing. When the editor closes, the file is validated. Invalid JSON or an invalid config exits 1.
The editor value may carry arguments and quoted paths, for example `code --wait` or `"/Applications/Sublime Text.app/Contents/SharedSupport/bin/subl" -w`. It is split into words without a shell, so `$VAR`, `~` and `;` are passed through literally. An editor that cannot start, or exits non-zero, prints a one-line error and exits 1.
With no editor configured it exits 1:
```
Error: No editor configured
@@ -397,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**
@@ -410,8 +441,9 @@ 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. |
| `--status <state>` | Only list changes in this lifecycle state: `proposed` or `shipped`. A change with no `status` in its `.openspec.yaml` counts as `proposed`. |
| `--json` | Print JSON instead of the table. |
| `--store <id>` | Use a registered store as the OpenSpec root instead of the current project. |
@@ -424,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
@@ -449,7 +491,16 @@ 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:
```bash
mv openspec/changes/mobile/refresh-token openspec/changes/mobile-refresh-token
rmdir openspec/changes/mobile
```
**Exit codes**
@@ -523,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"
}
]
@@ -540,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
{
@@ -552,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"
}
]
@@ -621,6 +678,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.
@@ -669,6 +741,18 @@ Bulk runs print one status line per item, followed by any findings, and end with
Totals: 2 passed, 0 failed (2 items)
```
**Task checkbox findings**
Progress counts checkboxes and nothing else, so a task file written as plain bullets reads as zero tasks: `openspec list` and `openspec status` report no work, and `openspec archive` has nothing to flag as incomplete. Validate reports a `WARNING` on each tracked task file that lists work without a checkbox:
```text
⚠ [WARNING] tasks.md: This change counts as 0 tasks: no line in its tracked task files is a checkbox, so "openspec list" and "openspec status" report no work and "openspec archive" has nothing to flag as incomplete. Write each task as "- [ ] 1.1 Description".
```
The warning fires only when the change's whole tracked set holds no checkbox at all. One file of prose beside a real checklist is not reported, and a change mid-authoring keeps its progress the moment a single checkbox exists. `--strict` turns the warning into a failure. The line number is in the `--json` report.
Fenced blocks, HTML comments, YAML front matter and indented code are not scanned, so a pasted terminal sample is never mistaken for a task list.
**Archive merge findings**
For changes, validate runs archive's merge builder against the current main specs without writing files. It reports merge conflicts, such as a missing `MODIFIED` target or a conflicting `ADDED` requirement, as `INFO`:
@@ -869,100 +953,6 @@ exit $validationExit
These custom views keep the full report's keys but omit clean items. They are neither complete full-v1 reports nor the versioned `--report findings` shape.
## openspec sync
Folds a change's delta specs into the main specs, without archiving the change.
```bash
openspec sync add-rate-limit # fold one change now; nothing moves
openspec sync add-rate-limit --ship # mark it shipped and fold it, in one set of changes
openspec sync # fold every change declaring status: shipped
openspec sync --check # exit 1 if a shipped change has unfolded deltas
```
`archive` folds and moves in one step, so the fold can only happen at the moment the
change is finished. `sync` separates them: the specs can be brought up to date while
the change is still open, and CI can check that they are.
**Arguments**
| Argument | What it is |
|---|---|
| `change-name` | The change to sync. Omitted, every change declaring `status: shipped` |
**Options**
| Flag | Effect |
|---|---|
| `--check` | Report shipped changes with unfolded deltas and exit 1. Writes nothing. |
| `--ship` | Fold the named change, then set `status: shipped` on it. If the fold fails, the field is not set. |
| `-y, --yes` | Sync even when the change has incomplete tasks. |
| `--no-validate` | Skip validation. |
| `--json` | Print a structured result instead of text. |
| `--store <id>` | Use a registered store as the OpenSpec root. |
**The lifecycle field**
A change may declare where it sits, in its `.openspec.yaml`:
```yaml
schema: spec-driven
status: shipped
```
Optional and absent by default. No `status` means `proposed`, which is what a change
under `changes/` has always meant. Nothing writes the field on its own.
**The gate**
`openspec sync --check` asserts that a change claiming to be shipped has its deltas in
`specs/`. A proposed change passes for free, so green is the resting state:
```
✓ 1 shipped change(s) are folded into the main specs.
```
and red names both the gap and the fix:
```
Sync check failed:
add-rate-limit
api: +1 not applied
Run openspec sync to fold them, then commit the result.
```
It reads only files on disk — no VCS history, no timing — so a pre-commit hook, a
pre-push hook and CI run the same command and agree.
**Output**
```
Applying changes to openspec/specs/api/spec.md:
+ 1 added
Totals: + 1, ~ 0, - 0, → 0
Specs updated successfully.
```
Running it again reports `Specs already in sync; no files changed.` — and so does
`openspec archive` afterwards, because re-applying a folded delta is a no-op.
**What it will not do**
Sync never deletes a spec. When a change's `REMOVED` entries take a capability's last
requirement, retiring it deletes the file, which stays with `openspec archive` behind
the `retire_capabilities` marker. Sync reports the case and names archive instead.
Sync also never examines archived changes: their deltas are history, superseded by
whatever came after.
**Exit codes**
- `0`: the specs were folded, or `--check` found nothing wrong.
- `1`: `--check` found an unfolded shipped change, validation failed, tasks were
incomplete, or the change was not found.
## openspec archive
Moves a completed change to the archive and updates the main specs.
@@ -1095,7 +1085,20 @@ Schema: spec-driven
Next: openspec status --change add-caching
```
With `--json`:
When no `openspec/` directory was found, `new change` creates one where you are and says so:
```
Created change 'add-caching' at openspec/changes/add-caching/
Schema: spec-driven
Next: openspec status --change add-caching
Note: no OpenSpec root was found here, so one was created at openspec/.
Run `openspec init` to finish setting this project up, or delete that directory if you meant a different project.
```
The notice goes to stdout with the rest of the human output, and never appears with `--json`.
With `--json`, in a project that already has `openspec/`:
```json
{
@@ -1112,6 +1115,15 @@ With `--json`:
}
```
When no `openspec/` directory was found and `new change` created one, the JSON has the same shape. `root.path` is the directory you ran it from, and `root.source` reads `implicit`:
```json
"root": {
"path": "/Users/you/projects/my-app",
"source": "implicit"
}
```
**Exit codes**
- `0`: change created.
@@ -1161,8 +1173,24 @@ Progress: 2/4 artifacts complete
[x] specs
[ ] design
[-] tasks (blocked by: design)
Next: openspec instructions design --change "add-rate-limit" --json
```
The `Next:` line names the one command that moves the change forward, so `openspec status` is enough to pick a change back up in a fresh session. It names the next ready artifact while planning is unfinished, and `openspec instructions apply` once every planning artifact exists:
```
[x] proposal
[x] specs
[x] design
[x] tasks
All planning artifacts complete!
Next: openspec instructions apply --change "add-rate-limit" --json
```
It carries `--store <id>` whenever the resolved root is a store, and names the same command as the JSON `nextSteps` sentence.
`--json` adds per-artifact dependencies, resolved file paths, and a suggested next step. Trimmed:
```json
@@ -1314,7 +1342,11 @@ 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`, `state` (`blocked`, `ready`, `all_done`), and `instruction`.
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**
@@ -1506,7 +1538,7 @@ openspec schema validate spec-driven # one schema, from any source
openspec schema validate # every project-local schema
```
It verifies that `schema.yaml` exists and parses, that the structure matches the schema format, that every artifact's template file exists inside the schema's `templates/` directory, and that the dependency graph has no cycles or unknown references.
It verifies that `schema.yaml` exists and parses, that the structure matches the schema format, that every artifact's template file exists inside the schema's `templates/` directory, and that the dependency graph has no cycles or unknown references, including in `apply.requires`. An `apply.tracks` value that isn't exactly equal to some artifact's `generates` value prints a `warning:` line but does not fail validation, because OpenSpec then can't tell which artifact's progress that file belongs to.
**Options**
@@ -1655,6 +1687,8 @@ openspec store setup team-context --path ~/openspec/team-context
In an interactive terminal, setup prompts for a missing name and location and confirms before creating anything. Outside one, a missing name or `--path` exits 1 with the flag to pass. Rerunning setup for a registered store reports `Registry: already registered`.
Setup exits 1 with `store_setup_inside_git_repo` when `--path` is inside another Git repository, because initializing the store there would nest one repository in another. `--no-init-git` creates no repository, so it skips that check. Use it to keep a store at `~/openspec/<id>` when your home directory is itself a Git repository, such as a dotfiles repo.
**Arguments**
| Argument | What it is |
@@ -1778,6 +1812,8 @@ Error: Pass --yes to delete store files non-interactively.
Fix: openspec store remove design-system --yes
```
Remove exits 1 and deletes nothing when the folder lacks matching store metadata, or when it contains another registered store (for example a store vendored as a Git submodule). In that case the error is `store_remove_contains_registered_store`: run `openspec store unregister <nested-id>` first, or `openspec store unregister <id>` to forget the store without deleting files.
**Options**
| Flag | Effect |
@@ -2157,6 +2193,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.
@@ -2214,6 +2336,8 @@ Supported shells: `zsh`, `bash`, `fish`, `powershell`. Every subcommand takes an
| `install [shell]` | Write the script and configure your shell startup file. |
| `uninstall [shell]` | Remove the script and the config block. |
Installed with Nix, completions are already in place: the flake package ships the Bash, Fish, and Zsh scripts at the standard locations, so `install` is not needed ([Installation](../start/installation.md#nix)).
### openspec completion generate
Prints the script and writes nothing.
@@ -20,7 +20,7 @@ Each change keeps its metadata at `openspec/changes/<change-name>/.openspec.yaml
### schema
The workflow schema this change follows. It is set when the change is created and wins over the project config, so a change keeps its schema even if `openspec/config.yaml` changes afterwards. Valid names are listed in [Schemas](../schemas/index.md).
The workflow schema this change follows. It is set when the change is created and wins over the project config, so a change keeps its schema even if `openspec/config.yaml` changes afterwards. Run [`openspec schemas`](../cli.md#openspec-schemas) to list the available schema names.
### initiative
@@ -36,11 +36,11 @@ Keys other than `store` and `id` are rejected. No command reads the link today.
### skip_specs
Declares the change intentionally makes no spec deltas: a pure refactor, tooling, or docs change. With it set, validation accepts zero deltas, and artifacts that would generate spec files count as complete. Setting it while spec files exist under specs/ is a validation error. Its effect on deltas and archive is on [spec-driven](../schemas/spec-driven/index.md).
Declares the change intentionally makes no spec deltas: a pure refactor, tooling, or docs change. With it set, validation accepts zero deltas, and artifacts that would generate spec files count as complete. Setting it while spec files exist under specs/ is a validation error.
### retire_capabilities
Authorizes archive to retire a capability. When this change's REMOVED deltas take away the last requirement a capability has, archive deletes that capability's main spec instead of stopping. The flag exists because the deletion is only recoverable from git, so it stays the author's call. The archive behavior is on [spec-driven](../schemas/spec-driven/index.md).
Authorizes archive to retire a capability. When this change's REMOVED deltas take away the last requirement a capability has, archive deletes that capability's main spec instead of stopping. The flag exists because the deletion is only recoverable from git, so it stays the author's call.
## Example
@@ -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.
@@ -15,8 +15,8 @@ The CLI keeps its machine-level settings at `~/.config/openspec/config.json` on
| `workflows` | list of strings | No | The workflow list a `custom` profile installs |
| `featureFlags` | map: flag → boolean | No | Boolean feature toggles |
| `defaultStore` | string | No | Machine-level fallback store for root resolution |
| `openers` | list | No | The tools worksets open in, and how each is launched |
| `telemetry` | map | No | State the CLI keeps: anonymous id and notice-seen |
| `openers` | map: tool id → settings | No | The tools worksets open in, and how each is launched |
| `telemetry` | map | No | Telemetry opt-out, anonymous id, and notice-seen state |
### profile
@@ -40,11 +40,45 @@ The machine-level fallback store id for root resolution, consulted only when no
### openers
The tools a workset can open in, and how each is launched. Entries are hand-edited and validated on use. Each may set `style` (`workspace-file` or `attach-dirs`), `label`, `command`, `args`, and `attach_flag`, and is merged over the built-in defaults.
The tools a workset can open in, keyed by tool id. Edit `openers` in the global `config.json` with `openspec config edit` in your terminal.
| Field | Contract |
| --- | --- |
| `style` | `workspace-file` or `attach-dirs`. Required for a new tool; optional for a built-in. |
| `label` | Non-empty string shown in the tool picker. Defaults to the id for a new tool. |
| `command` | Non-empty executable name or path. Defaults to the id for a new tool. Put arguments in `args`, not in this string. |
| `args` | Array of strings passed before the workspace file or attach flags. Defaults to `[]` for a new tool. |
| `attach_flag` | Non-empty string paired with each member path for `attach-dirs`. Defaults to `--add-dir` for a new tool. Ignored for `workspace-file`. |
**Built-in overrides:** `code`, `cursor`, `claude`, and `codex` retain any fields you omit. Setting `args` replaces the entire argument list; `[]` clears it.
**Launch styles:** `workspace-file` passes the generated `.code-workspace` path to the executable. `attach-dirs` passes one flag/path pair per member, including the primary member.
**Availability:** `attach-dirs` openers, including Claude Code and Codex, are disabled by default. You cannot select or save them with `--tool`, and OpenSpec refuses to open a workset that already names one. Configuration overrides do not enable the `attach-dirs` launch style.
**Validation:** unknown fields, invalid types, and a new tool without `style` fail when a workset command reads the opener table.
This example adds VS Code Insiders and passes `--new-window` whenever the built-in VS Code opener launches:
```json
{
"openers": {
"code-insiders": {
"style": "workspace-file",
"label": "VS Code Insiders"
},
"code": {
"args": ["--new-window"]
}
}
}
```
The corresponding `code-insiders` or `code` executable must be installed and available on `PATH`.
### telemetry
State the CLI writes for telemetry: your anonymous id and whether the first-run notice was shown. It is not the opt-out. Disabling telemetry is an environment variable, on [Environment variables](environment-variables.md).
The CLI stores your anonymous id and whether the first-run notice was shown. Set `telemetry.enabled` to `false` to disable telemetry. You can also opt out with `OPENSPEC_TELEMETRY=0` or `DO_NOT_TRACK=1` in your environment.
## Example
@@ -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 |
@@ -23,11 +23,11 @@ What to write in these fields is covered in [Project configuration](../../custom
### schema
The workflow schema every change in this project follows. Valid values are `spec-driven` or a schema name the project defines. The names are listed in [Schemas](../schemas/index.md).
The workflow schema every change in this project follows. Valid values are `spec-driven` or a schema name the project defines. Run [`openspec schemas`](../cli.md#openspec-schemas) to list the available schema names.
### 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
+1 -1
View File
@@ -7,5 +7,5 @@
| [Project configuration (config.yaml)](config-yaml.md) | `openspec/config.yaml` | The schema, context, and rules this project plans with |
| [Change metadata (.openspec.yaml)](change-metadata.md) | `openspec/changes/<name>/.openspec.yaml` | The workflow schema, goal, scope, and spec exceptions for one change |
| [CLI settings (config.json)](config-json.md) | `~/.config/openspec/config.json` (Windows varies) | How the openspec CLI behaves on your machine |
| [Environment variables](environment-variables.md) | Your shell or CI environment | Telemetry opt-out, and where the config and data directories live |
| Environment variables | Your shell or CI environment | Telemetry opt-out, and where the config and data directories live |
| Stores | `~/.local/share/openspec/stores/` (Windows varies) | The registry and metadata behind multi-repo stores |
+11 -11
View File
@@ -2,26 +2,26 @@
> Every OpenSpec term, one line each.
OpenSpec reuses words that mean something else in git, CI, and agent tooling. Each row gives the OpenSpec meaning, and the last column links to the page that teaches the term.
OpenSpec reuses words that mean something else in git, CI, and agent tooling. Each row gives the OpenSpec meaning. The last column links to more detail where available.
| Term | Definition | More |
|---|---|---|
| **Apply** | Implement the tasks in a change proposal. Skill: `openspec-apply-change`. | [Apply a change](../guides/apply.md) |
| **Apply** | Implement the tasks in a change proposal. Skill: `openspec-apply-change`. | [Apply a change](skills.md#openspec-apply-change) |
| **Archive** | Complete a change proposal: merge its deltas into the main specs and move its folder to `openspec/changes/archive/`. | [Quickstart](../start/quickstart.md) |
| **Artifact** | A planning document inside a change proposal: `proposal.md`, delta specs, `design.md`, `tasks.md`. Not a build output. | [Concepts](../guides/concepts.md) |
| **Capability** | One behavior area of your system. Each has one spec at `openspec/specs/<capability>/spec.md`. | [Concepts](../guides/concepts.md) |
| **Change proposal** | One unit of work: a folder under `openspec/changes/<name>/` holding its planning artifacts. Often shortened to "change". Not a git commit. | [Concepts](../guides/concepts.md) |
| **Artifact** | A planning document inside a change proposal: `proposal.md`, delta specs, `design.md`, `tasks.md`. Not a build output. | [Artifacts](schemas/spec-driven/index.md#artifacts) |
| **Capability** | One behavior area of your system. Each has one spec at `openspec/specs/<capability>/spec.md`. | [Capabilities](schemas/spec-driven/index.md#proposalmd) |
| **Change proposal** | One unit of work: a folder under `openspec/changes/<name>/` holding its planning artifacts. Often shortened to "change". Not a git commit. | [Propose](../start/quickstart.md#step-2-propose) |
| **Command** | A typed entry point for a workflow. Spelling varies per tool (`/opsx:propose`, `/opsx-propose`). The docs name workflows by skill instead. | [Supported tools](supported-tools.md) |
| **Continue** | Create the next planning artifact for an existing change proposal. Skill: `openspec-continue-change`. | [Skills](skills.md) |
| **Delivery** | How workflows are installed: as skills, commands, or both. | [Set up your project](../start/setup.md) |
| **Delta spec** | A spec inside a change proposal listing only what changes, under `ADDED`, `MODIFIED`, `REMOVED`, and `RENAMED` headers. | [Delta specs](schemas/spec-driven/index.md#delta-specs-specmd) |
| **Explore** | Think an idea through with the agent before proposing. Writes no code. Skill: `openspec-explore`. | [Explore an idea](../guides/explore.md) |
| **Explore** | Think an idea through with the agent before proposing. Writes no code. Skill: `openspec-explore`. | [Explore an idea](skills.md#openspec-explore) |
| **Fast-forward** | Create a change proposal with every planning artifact in one pass, ready to implement. Skill: `openspec-ff-change`. Not a git fast-forward. | [Skills](skills.md) |
| **Legacy workflow** | The pre-OPSX `/openspec:*` commands. | [Migration](../help/legacy/migration.md) |
| **Legacy workflow** | The pre-OPSX `/openspec:*` commands. | |
| **Loop** | The cycle a change proposal moves through: explore, propose, review, apply, archive. | [Quickstart](../start/quickstart.md) |
| **Main specs** | The `openspec/specs/` tree: the current, agreed behavior of your system. Archiving merges deltas into it. | [Concepts](../guides/concepts.md) |
| **Main specs** | The `openspec/specs/` tree: the current, agreed behavior of your system. Archiving merges deltas into it. A capability with no spec yet gets one from its `ADDED` requirements. | [Archive](../start/quickstart.md#step-5-archive) |
| **OpenSpec root** | The `openspec/` tree a command resolves to and operates on: your repo's, or a store's. | [Stores](../multi-repo/stores.md#where-artifacts-get-created-when-using-stores) |
| **OPSX** | The current OpenSpec workflow system, and the command prefix it installs (`/opsx:`). | [Architecture](architecture/index.md) |
| **OPSX** | The current OpenSpec workflow system, and the command prefix it installs (`/opsx:`). | |
| **Profile** | Which workflows init installs: `core` or `custom`. | [Profiles](../customize/profiles.md) |
| **Propose** | Create a change proposal and generate all its planning artifacts in one step. Skill: `openspec-propose`. | [Quickstart](../start/quickstart.md) |
| **Registry** | The machine-level list of registered stores, in `registry.yaml`. Not a package registry. | [CLI](cli.md#openspec-store) |
@@ -29,12 +29,12 @@ OpenSpec reuses words that mean something else in git, CI, and agent tooling. Ea
| **Scenario** | A testable example under a requirement, in WHEN/THEN form. | [Delta specs](schemas/spec-driven/index.md#delta-specs-specmd) |
| **Schema** | The definition of which artifacts a change proposal produces, and in what order. Not JSON Schema. | [Schemas](schemas/index.md) |
| **Skill** | A workflow's instructions, installed where your AI tool reads them (`.agents/skills/`, ...). | [Skills](skills.md) |
| **Spec** | A file describing how one capability behaves today, at `openspec/specs/<capability>/spec.md`. | [Concepts](../guides/concepts.md) |
| **Spec** | A file describing how one capability behaves today, at `openspec/specs/<capability>/spec.md`. | [Archive](../start/quickstart.md#step-5-archive) |
| **spec-driven** | The default schema: proposal, then delta specs, then design, then tasks. | [spec-driven](schemas/spec-driven/index.md) |
| **Store** | A standalone OpenSpec repo registered on your machine, for planning that spans repositories. Not a data store. | [Stores (beta)](../multi-repo/stores.md) |
| **Sync** | Merge implemented deltas into the main specs without archiving. Skill: `openspec-sync-specs`. | [Skills](skills.md) |
| **Template** | The starting content a schema gives each artifact. | [Schemas](../customize/schemas.md) |
| **Update** | As a skill (`openspec-update-change`): revise a change proposal's planning artifacts. As a CLI command (`openspec update`): refresh OpenSpec's installed files. | [Change course](../guides/change-course.md), [CLI](cli.md) |
| **Update** | As a skill (`openspec-update-change`): revise a change proposal's planning artifacts. As a CLI command (`openspec update`): refresh OpenSpec's installed files. | [Update a change](skills.md#openspec-update-change), [CLI](cli.md) |
| **Verify** | Check the implementation matches a change proposal's artifacts before archiving. Skill: `openspec-verify-change`. | [Skills](skills.md) |
| **Workflow** | A named OpenSpec action (propose, apply, archive, ...), installed into your AI tool as a skill or command. | [Set up your project](../start/setup.md) |
| **Workset** | A personal, local group of folders opened together in one tool. Not a store, and nothing is shared. | [Worksets (beta)](../multi-repo/worksets.md) |
+28 -9
View File
@@ -74,7 +74,15 @@ A glob can match several files:
generates: specs/**/*.md
```
This matches Markdown files below `openspec/changes/add-auth/specs/`. OpenSpec treats a value containing `*`, `?`, or `[` as a glob.
This matches Markdown files below `openspec/changes/add-auth/specs/`.
OpenSpec recognizes these glob forms in `generates`:
- **Wildcards and character classes**: values containing `*`, `?`, or `[`, such as `specs/**/*.md` and `review-[ab].md`.
- **Brace expansions**: alternatives such as `review-{api,ui}.md` and ranges such as `file-{1..3}.md`.
- **Extglobs**: patterns such as `@(proposal|design).md`, `+(proposal|design).md`, and `!(proposal|design).md`.
**Literal filenames**: a leading `!` alone does not make a glob. Use `generates: '!review.md'` to name that file. Plain parentheses such as `(proposal|design).md` and single-element braces such as `review-{api}.md` also remain literal.
OpenSpec rejects absolute paths and paths containing a `..` segment.
@@ -119,7 +127,7 @@ OpenSpec rejects absolute paths and paths containing a `..` segment.
| Field | Contract |
|---|---|
| `requires` | **Required.** A non-empty list of artifacts that must exist before apply instructions become ready. |
| `tracks` | An optional relative path to a Markdown task file in the change folder. Default: `null`. |
| `tracks` | An optional relative path or glob for Markdown task files in the change folder. Default: `null`. |
| `instruction` | Optional guidance sent to the agent when apply is ready. OpenSpec uses built-in guidance by default. |
Artifact `requires` controls planning order. `apply.requires` controls when apply instructions become ready.
@@ -132,21 +140,28 @@ The path starts from the change folder. For a change named `add-auth`, `tracks:
openspec/changes/add-auth/tasks.md
```
Apply stays blocked if that file is missing or contains no checkbox with task text. OpenSpec counts these checkbox forms:
A glob such as `tracks: "**/tasks.md"` reads every matching file, such as `backend/tasks.md` and `frontend/tasks.md`. OpenSpec combines their tasks and progress. Use the same value for an artifact's `generates` field so status and list track the same files.
Apply stays blocked if no file matches or the matched files contain no checkbox with task text. OpenSpec counts these checkbox forms:
```markdown
- [ ] Pending task
- [x] Completed task
* [X] Completed task
+ [ ] Pending task
1. [ ] Pending task
2) [x] Completed task
```
Leading spaces are allowed. The [tasks.md section of the spec-driven page](spec-driven/index.md#tasksmd) defines the stricter format produced by the default schema.
Any Markdown list marker works: `-`, `*`, `+`, or a number of up to nine digits followed by `.` or `)`. Leading spaces are allowed. The [tasks.md section of the spec-driven page](spec-driven/index.md#tasksmd) defines the stricter format produced by the default schema.
The tracked file drives the apply state:
The tracked files drive the apply state:
- **`blocked`**: the file is missing, or no checkbox has task text.
- **`ready`**: at least one tracked task is pending.
- **`all_done`**: every tracked task is checked.
- **`blocked`**: no file matches, or no readable file has a checkbox with task text.
- **`ready`**: at least one task is pending, or a matched file could not be read while another provides tasks.
- **`all_done`**: every tracked task is checked and every matched file was read.
If a matched file cannot be read, apply keeps the tasks and progress from readable files but does not mark the change `all_done`. [Apply JSON output](../cli.md#openspec-instructions) identifies each unavailable file and the reason.
OpenSpec rejects absolute paths and paths containing a `..` segment.
@@ -198,12 +213,16 @@ apply:
- Field types and required fields
- Relative paths
- Artifact IDs, dependencies, and cycles
- `apply.requires` IDs: each must be an artifact in the schema
- Template files
A schema with an unknown `apply.requires` ID doesn't load, so every command that uses it reports the error.
Validation warns, without failing, when `apply.tracks` isn't exactly equal to some artifact's `generates` value. OpenSpec finds the tracked artifact by comparing those two strings, so anything else leaves it unable to tell which artifact's progress the file belongs to. That includes a typo like `task.md`, and also `tracks: tasks/main.md` against `generates: tasks/*.md`, where the glob does produce the file but the strings still differ. Apply keeps reading the file either way, but `openspec list` and `openspec status` count `tasks.md` instead.
Validation doesn't catch these mistakes:
| Mistake | What happens |
|---|---|
| A field is misspelled, such as `instrution` | OpenSpec ignores it. Validation doesn't report the typo. |
| `apply.requires` names an unknown artifact ID | Validation doesn't report the unknown ID. |
| `name` differs from the schema directory | Validation passes. OpenSpec still uses the directory name for lookup. |
+83 -19
View File
@@ -54,6 +54,8 @@ Establishes why the change is needed.
The template the agent receives as the output format ([templates/proposal.md](https://github.com/Fission-AI/OpenSpec/blob/main/schemas/spec-driven/templates/proposal.md)):
```md
# Proposal
## Why
<!-- Explain the motivation for this change. What problem does this solve? Why now? -->
@@ -65,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
@@ -96,12 +101,24 @@ 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.
IMPORTANT: The Capabilities section is critical. It creates the contract between
proposal and specs phases. Research existing specs before filling this in.
proposal and specs phases. Research existing specs before filling this in:
run `openspec list --specs` for the project's capability inventory, then
`openspec show "<spec-id>" --type spec --json --no-scenarios` for any that
look related - that returns a capability's purpose and requirement texts
without pulling whole spec files into context. Append `--store "<id>"` to
both commands only for a registered standalone store, and keep `--type
spec`: a change and a spec sharing a name is otherwise an ambiguous-item
error. `openspec list` without `--specs` lists in-flight changes, not
specs - it never shows what the project already covers. Reuse an existing
capability's exact path instead of introducing a near-duplicate name.
The filtered read is only an overview. Before deciding what is already
covered or what should change, read each relevant spec in full, including
scenarios, with `openspec show "<spec-id>" --type spec` (same `--store` rule).
Each capability listed here will need a corresponding spec file.
Every change must either declare at least one capability (new or
@@ -122,11 +139,15 @@ This is the foundation - specs, design, and tasks all build on this.
Defines what behavior changes, with one delta spec per capability the proposal lists.
Each delta spec is the `spec.md` inside its capability folder. `openspec validate` and `openspec archive` reject delta sections written in any other file under `specs/`, such as `specs/user-auth.md`, because archive never merges them.
### Structure
The template the agent receives as the output format ([templates/spec.md](https://github.com/Fission-AI/OpenSpec/blob/main/schemas/spec-driven/templates/spec.md)):
```md
# Spec Delta
## Purpose
<!-- New capabilities only: one or two sentences (50+ characters) on what this capability is for. Delete this section for an existing capability. -->
@@ -168,7 +189,7 @@ Create one spec file per capability listed in the proposal's Capabilities sectio
`<capability-path>` is the spec directory relative to `specs/` (for example,
`user-auth` or `identity/user-auth`). Preserve the full path:
- New capabilities: use the exact path from the proposal at `specs/<capability-path>/spec.md`. Any path segment newly introduced in the proposal must be kebab-case. Follow the project's existing organization; do not add a new domain level when the project uses a flat layout.
- Modified capabilities: use the exact existing path from `openspec/specs/<capability-path>/` when creating the delta at `specs/<capability-path>/spec.md`. Do not move or rename the capability.
- Modified capabilities: use the exact existing path from `openspec/specs/<capability-path>/` when creating the delta at `specs/<capability-path>/spec.md`. Run `openspec list --specs` to confirm that path before writing the delta, appending `--store "<id>"` only for a registered standalone store - a mistyped or invented path targets a capability that does not exist rather than the one you meant. Do not move or rename the capability.
There must be at least one spec file unless the change's `.openspec.yaml`
sets `skip_specs: true` (no spec-level behavior change) - `openspec validate`
@@ -187,8 +208,9 @@ 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 in ADDED requirements and in the main spec. This is a warning: normal validation still passes, but `openspec validate --strict` fails on it. 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. Split an existing long requirement only when the user asks for it, in a change made for that purpose: under MODIFIED, keep its header and every scenario and cut its description down to one behavior without changing its meaning, then add each behavior you removed as its own ADDED requirement with its own scenarios.
New capabilities only: start the delta spec with a `## Purpose` section -
New capabilities only: the delta spec's first section is `## Purpose` -
one or two sentences (50+ characters, or `openspec validate --strict`
reports it as too brief) describing what the capability is for. Archive
copies it into the main spec it creates; without it the new main spec is
@@ -196,10 +218,16 @@ left with a `TBD ... Update Purpose after archive` placeholder to fill in
by hand. Do NOT add `## Purpose` to a delta for an existing capability -
that spec already has one and the delta's is ignored. To change an
existing capability's Purpose - including a leftover `TBD` placeholder -
edit `openspec/specs/<capability-path>/spec.md` directly.
edit `<planningHome.root>/openspec/specs/<capability-path>/spec.md`
directly. `planningHome.root` comes from the `openspec instructions ...
--json` response. Always use it rather than a repo-relative path: it
resolves to the store whenever the change lives in one - whether that
came from `--store`, a project `store:` pointer, or a global default
store - and to the current repository otherwise. Do not try to work out
which case applies; the field already has.
MODIFIED requirements workflow:
1. Locate the existing requirement in openspec/specs/<capability-path>/spec.md
1. Locate the existing requirement in `<planningHome.root>/openspec/specs/<capability-path>/spec.md` (the same store-aware root as above)
2. Copy the ENTIRE requirement block (from `### Requirement:` through all scenarios)
3. Paste under `## MODIFIED Requirements` and edit to reflect new behavior
4. Ensure header text matches exactly (whitespace-insensitive)
@@ -207,8 +235,10 @@ MODIFIED requirements workflow:
Common pitfall: Using MODIFIED with partial content loses detail at archive time.
If adding new concerns without changing existing behavior, use ADDED instead.
Example (a new capability, so it opens with `## Purpose`):
Example (a new capability, so its first section is `## Purpose`):
```
# Spec Delta
## Purpose
Lets users take their data out of the product in a portable format.
@@ -241,6 +271,8 @@ Explains how to implement the change. Drafted only when the change needs one.
The template the agent receives as the output format ([templates/design.md](https://github.com/Fission-AI/OpenSpec/blob/main/schemas/spec-driven/templates/design.md)):
```md
# Design
## Context
<!-- Current state and constraints that shape the approach. See proposal.md for motivation - don't restate it -->
@@ -305,6 +337,8 @@ Breaks the implementation into checkable tasks. [apply](#apply) tracks progress
The template the agent receives as the output format ([templates/tasks.md](https://github.com/Fission-AI/OpenSpec/blob/main/schemas/spec-driven/templates/tasks.md)):
```md
# Tasks
## 1. <!-- Task Group Name -->
- [ ] 1.1 <!-- Task description -->
@@ -327,30 +361,60 @@ Before writing tasks, check design.md for Open Questions. If any of them
would change what gets built, resolve them with the user first - do not
bake an unstated assumption into the task list.
**IMPORTANT: Follow the template below exactly.** The apply phase parses
checkbox format to track progress. Tasks not using `- [ ]` won't be tracked.
**IMPORTANT: Follow the template below for tracked tasks.** The apply phase parses
checkbox format to track progress. A box holding only `x` counts as done,
upper or lower case and with any spacing, so `- [ x]` is done too. Every
other marker, including `- [~]`, `- [-]` and an empty `- []`, reads as
unfinished. A line with no checkbox is not tracked at all.
Guidelines:
- Group related tasks under ## numbered headings
- Each task MUST be a checkbox: `- [ ] X.Y Task description`
- Group related tracked tasks under ## numbered headings
- Each tracked task MUST be a checkbox: `- [ ] X.Y Task description`
- Tasks should be small enough to complete in one session
- Order tasks by dependency (what must be done first?)
- Track implementation and verification work that can be completed before
archive. If the requested workflow includes archive or work that requires
this change to be archived, preserve those steps as plain bullets in an
optional `## Workflow follow-up` section at the end of tasks.md. These
bullets are reference information outside tracked task progress.
- Each task MUST state how to verify completion (a test, command,
observable behavior, or delivered artifact). Put the verification in
that task's checkbox description. Use a separate verification task only
when it checks broader integration or system behavior that spans
multiple implementation tasks.
- Each task group MUST land the tests and documentation its own work
calls for. Do NOT collect testing or documentation into a final group -
when a late group first exercises work from an early one, the failures
cascade back through every group in between and force rework. A group
whose work calls for neither, such as scaffolding or dependency setup,
carries neither. A final group is for integration checks only, not for
the tests and docs an earlier group owed.
Example:
```
# Tasks
## 1. Setup
- [ ] 1.1 Create new module structure
- [ ] 1.2 Add dependencies to package.json
- [ ] 1.1 Create new module structure and verify expected files are present
- [ ] 1.2 Add dependencies to package.json and verify package installation succeeds
## 2. Core Implementation
- [ ] 2.1 Implement data export function
- [ ] 2.2 Add CSV formatting utilities
- [ ] 2.1 Implement data export function and verify the export test passes
- [ ] 2.2 Add CSV formatting utilities and verify unit tests cover quoting and delimiters
- [ ] 2.3 Document the export API in docs/export.md and verify the documented command runs as written
```
When applicable, append workflow follow-up as plain bullets, for example:
```
## Workflow follow-up
- Archive the change after the project's review requirements are satisfied.
- Verify the archived result.
```
Reference specs for what needs to be built, design for how to build it.
Each task should be verifiable - you know when it's done.
````
## Apply
+13 -4
View File
@@ -39,6 +39,13 @@ The skills come in two sets:
- **Core**: installed by default, the main planning loop.
- **Optional**: installed only when you add them, via [Profiles](../customize/profiles.md).
Every skill expects a project that already uses OpenSpec. Before its first step that writes anything, a skill checks for a resolved root. What happens when there is none depends on how the skill was reached:
- **Auto-selected**: your agent picked the skill on its own, without you naming OpenSpec. It drops OpenSpec and answers your request normally, the way it would with OpenSpec not installed.
- **Explicit OpenSpec request**: you named OpenSpec, named the skill, or ran its command. It stops before writing and asks how to proceed: run `openspec init` here, target a store with `--store <id>`, or continue without OpenSpec. It waits for your answer.
Commands are always the second case. A project whose `openspec/config.yaml` names a store this machine cannot resolve (not registered, or a malformed `store:` line) is not treated as uninitialized: the skill stops and shows the store error with its fix. No skill creates an `openspec/` directory on its own in either case. The entries below describe what each skill does once a root is in place.
| Skill | Job | Type |
|---|---|---|
| [openspec-explore](#openspec-explore) | Think through an idea before it becomes a change proposal | Core |
@@ -54,6 +61,8 @@ The skills come in two sets:
| [openspec-bulk-archive-change](#openspec-bulk-archive-change) | Archive several change proposals at once | Optional |
| [openspec-onboard](#openspec-onboard) | Learn the workflow by doing one real change proposal end to end | Optional |
Each entry below names the skill that owns the next step. When your profile leaves that skill out, the installed files never name it: the handoff becomes the equivalent `openspec` command, or a plain request to you, and a line that exists only to point at a missing skill is not written at all. So the skills you have always hand off to skills you have. Which set you get is [Profiles](../customize/profiles.md).
## openspec-explore
Think through an idea before it becomes a change proposal.
@@ -81,8 +90,8 @@ 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]`). |
| **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`. Unclear tasks or errors: pauses and asks. |
| **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
@@ -92,7 +101,7 @@ other.
| Contract | Description |
|---|---|
| **Arguments** | A change proposal name, optional, plus the revision you want. With no revision stated it runs a coherence review: artifacts checked against each other for contradictions, gaps, and duplication. |
| **Creates** | Nothing new. Edits only artifact files that already exist. Missing artifacts are `openspec-continue-change`'s job. Never code. |
| **Creates** | Edits artifact files that already exist. One exception: for an artifact written as a glob, such as `specs/**/*.md`, that already has at least one file, it can add a missing companion file once you confirm the path. An artifact with no files yet is `openspec-continue-change`'s job. Without that skill (the core profile leaves it out), it points to `openspec status` and `openspec instructions` instead. Never code. |
| **Response** | Shows each proposed revision and writes it only after you confirm, one artifact at a time. Ends with what was revised and the next step; implementation waits for `openspec-apply-change`. |
## openspec-sync-specs
@@ -112,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
+77 -19
View File
@@ -15,27 +15,35 @@ 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` |
| Kilo Code | `kilocode` | `.kilocode/skills/` | `/openspec-apply-change` | `.kilocode/workflows/` | `/opsx-apply` |
| Kilo Code | `kilocode` | `.kilocode/skills/` | `/openspec-apply-change` | `.kilo/command/` | `/opsx-apply` |
| Kimi Code | `kimi` | `.kimi-code/skills/` | `/skill:openspec-apply-change` | none | none |
| Kiro | `kiro` | `.kiro/skills/` | `/openspec-apply-change` | `.kiro/prompts/` | `/opsx-apply` |
| Lingma | `lingma` | `.lingma/skills/` | `/openspec-apply-change` | `.lingma/commands/opsx/` | `/opsx:apply` |
@@ -47,21 +55,30 @@ 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` |
| Shared `.agents` skills | `agents` | `.agents/skills/` | `/openspec-apply-change` | none | none |
| Other / Universal | `agents` | `.agents/skills/` | `/openspec-apply-change` | none | none |
- **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
@@ -80,17 +97,34 @@ Skills stay in `.cline/skills/`.
### Codex
- **Invocation**: type `$openspec-<skill>`. Codex does not recognize the
`/openspec-<skill>` form ([upstream issue](https://github.com/openai/codex/issues/11817)).
- **CLI and IDE extension**: mention `$openspec-propose` with your idea, or run
`/skills` to select the skill. Codex does not recognize `/openspec-propose`
([upstream issue](https://github.com/openai/codex/issues/11817)).
- **Desktop app**: open Skills in the sidebar and select `openspec-propose`.
[OpenAI's skills documentation](https://learn.chatgpt.com/docs/build-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.
@@ -102,8 +136,22 @@ Skills stay in `.cline/skills/`.
### GitHub Copilot
Prompt files register as slash commands in the Copilot IDE extensions (VS Code,
JetBrains, Visual Studio). Copilot CLI does not read `.github/prompts/`.
- **IDE extensions (command delivery)**: VS Code, JetBrains, and Visual Studio load
`.github/prompts/opsx-<id>.prompt.md` as `/opsx-<id>`. If a command disappears
while its file still exists, restart the IDE.
- **Copilot CLI (skill delivery)**: the CLI ignores `.github/prompts/` and loads
`.github/skills/openspec-*/SKILL.md` instead. Invoke a skill as
`/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
@@ -118,11 +166,21 @@ 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.
### Shared `.agents` skills
### 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,
including tools with no row in the matrix.
- **Alongside other targets**: Antigravity, Codex, Zed Agent, and this target share
including tools with no row in the matrix. It is the entry to pick when your
assistant is not listed. The init picker's search box finds it by `universal`,
`other`, `generic`, `custom`, `proprietary`, `unlisted`, `unsupported`,
`vendor-neutral`, or `agents.md`.
- **Alongside other targets**: Amp, Antigravity, Codex, Zed Agent, and this target share
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.
+23 -4
View File
@@ -5,7 +5,9 @@
## Prerequisites
OpenSpec is a Node.js CLI. You need version 20.19.0 or newer.
OpenSpec runs on Node.js 20.19.0 or newer. Homebrew installs Node.js as a
dependency, and the Nix package includes the runtime. Check your installed version
before using another install method.
In your terminal:
@@ -13,7 +15,9 @@ In your terminal:
node --version
```
If that prints `v20.19.0` or higher, you're set. If not, install a newer Node from [nodejs.org](https://nodejs.org) or through your version manager (nvm, fnm, asdf, volta).
If that prints `v20.19.0` or higher, you're set. If not, install a newer Node from
[nodejs.org](https://nodejs.org) or through your version manager (nvm, fnm, asdf,
volta). You can skip this check when you install with Homebrew or Nix.
The workflow itself runs inside an AI coding tool: Claude Code, Cursor, or any other tool on the [supported list](../reference/supported-tools.md).
@@ -53,6 +57,16 @@ In your terminal:
npm install -g @fission-ai/openspec@latest
```
### Homebrew
Homebrew installs OpenSpec and its Node.js dependency on macOS or Linux. In your terminal:
```bash
brew install openspec
```
The formula is published in [homebrew-core](https://formulae.brew.sh/formula/openspec), so you don't need to add a tap.
### Yarn
`yarn global add` is Yarn Classic (1.x) only. Modern Yarn removed global installs, so use npm, pnpm, or bun instead. A global CLI doesn't have to share your project's package manager.
@@ -94,6 +108,11 @@ That leaves nothing on your PATH, so there's no install to check afterward.
To put OpenSpec in a project dev shell instead, add the flake as an input and use its default package; [flake.nix](https://github.com/Fission-AI/OpenSpec/blob/main/flake.nix) lists the outputs.
The Nix package ships the Bash, Fish, and Zsh completion scripts at the standard
locations (`share/bash-completion/completions`, `share/fish/vendor_completions.d`,
`share/zsh/site-functions`), so they load with the package and there is no need to run
`openspec completion install`.
### Check it worked
Whichever method you used, in your terminal:
@@ -118,7 +137,7 @@ When a newer CLI is out, [`openspec update`](../reference/cli.md#openspec-update
> [!WARNING]
> On Deno, re-run the [Deno install](#deno) with `-f`; it won't overwrite the installed command without it. On Nix, use `nix profile upgrade openspec`.
> On Homebrew, run `brew upgrade openspec`. On Deno, re-run the [Deno install](#deno) with `-f`; it won't overwrite the installed command without it. On Nix, use `nix profile upgrade openspec`.
> [!NOTE]
> A global npm install belongs to one Node installation. Switch Node versions with nvm and the `openspec` command doesn't come along, so install it again under the new version.
@@ -139,7 +158,7 @@ openspec completion uninstall
npm uninstall -g @fission-ai/openspec
```
On Deno: `deno uninstall --global openspec`. On Nix: `nix profile remove openspec`. Your shell should no longer find `openspec`.
On Homebrew: `brew uninstall openspec`. On Deno: `deno uninstall --global openspec`. On Nix: `nix profile remove openspec`. Your shell should no longer find `openspec`.
**3. Delete what's left, or keep it.**
+33 -13
View File
@@ -1,9 +1,19 @@
# Quickstart
> Your first change on your existing repo, from idea to archived.
> Your first change, from idea to archived, in a new or existing project.
Before you start, you need the CLI on your machine ([Installation](installation.md)) and OpenSpec initialized in your project ([Set up your project](setup.md)).
## Start from an empty project
You can start without a chosen stack or a complete architecture. Initialize OpenSpec in your project folder, then ask your agent to explore the options with you. In your AI chat:
```text
Help me explore a task tracker from scratch. I have not picked a stack. Compare the options and help me choose the first behavior to build.
```
Decide what the first change needs and leave later architecture choices open. Ask your agent to propose that one change, then follow the steps below. You can revisit the architecture as the project grows.
## The loop at a glance
Every change moves through the same five steps: you think the idea through with your agent, it drafts a plan, you correct the plan before any code exists, the agent builds from it, and archiving updates your specs with what shipped.
@@ -17,22 +27,22 @@ flowchart LR
archive -. "next change" .-> explore
```
Every prompt below goes in your AI chat, the same place you ask for code. Each invokes an OpenSpec skill by name, the same spelling in every tool. A plain ask works too ("propose a change to add rate limiting"). Some tools add shorter command aliases (`/opsx:propose` in Claude Code, [other tools vary](../reference/supported-tools.md)).
Every prompt below goes in your AI chat, the same place you ask for code. The examples use plain language so they work across tools. You can also invoke a skill directly; the syntax varies by tool ([supported tools](../reference/supported-tools.md)).
## Step 1: Explore
Think the idea through with your agent before you ask for a plan. In your AI chat:
```text
/openspec-explore how rate limiting should work in this app
Help me explore how rate limiting should work in this app.
```
Explore is a thinking mode. The agent investigates your codebase, asks the questions that matter, sketches options, and challenges assumptions. It writes no code and no files. The output is a sharper idea.
Explore is a thinking mode. The agent investigates your codebase, asks the questions that matter, sketches options, and challenges assumptions. It never writes code. It writes nothing else unless you ask it to capture what you decided, or say yes when it offers. The output is a sharper idea.
Stay here as long as the problem needs. When the shape feels right, hand it off:
```text
/openspec-propose
Propose the change we just discussed.
```
That line starts propose for you, carrying everything you settled. Skip the first prompt in step 2.
@@ -42,7 +52,7 @@ That line starts propose for you, carrying everything you settled. Skip the firs
Propose turns the idea into a reviewable plan. Coming from explore, it's already running. Starting cold, when the change is clear in your head, ask directly. In your AI chat:
```text
/openspec-propose add rate limiting
Propose a change to add rate limiting.
```
The agent asks what it needs to, then writes a change folder:
@@ -75,7 +85,7 @@ To fix something, either works:
Apply turns the plan into code. Start a fresh chat session, since implementation goes better on a clean context window. In your AI chat:
```text
/openspec-apply-change add-rate-limiting
Apply the add-rate-limiting change.
```
The agent reads the change folder, then works through `tasks.md`, checking off each task as it lands.
@@ -91,7 +101,7 @@ Archiving does two things: it updates your main specs with the change's requirem
When every box in `tasks.md` is checked, in your AI chat:
```text
/openspec-archive-change add-rate-limiting
Archive the add-rate-limiting change.
```
Step through what archiving does:
@@ -146,14 +156,24 @@ Step through what archiving does:
└── 2026-08-08-add-rate-limiting/
```
Git is a separate concern. Commit the change folder with the code, and nothing else about your workflow changes. When to archive relative to a PR is a team convention; the [Teams](../guides/teams.md) guide has the tradeoff.
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
- [Concepts](../guides/concepts.md): what the two artifacts are, and how a delta describes a change.
- [Explore](../guides/explore.md): getting more out of explore mode.
- [Apply](../guides/apply.md): pacing, context windows, resuming long changes.
- [Review the plan](../guides/review-the-plan.md): what to look for in specs before you build.
- [Delta specs](../reference/schemas/spec-driven/index.md#delta-specs-specmd): how to write the behavior changes in a delta spec.
- [Profiles](../customize/profiles.md): optional workflows beyond the core set (verify before archive, incremental planning).
## Advanced guides
+40 -2
View File
@@ -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:
@@ -41,7 +57,7 @@ Running init creates two things in your project:
- An `openspec/` folder at the repo root
- Workflow files (skills and commands) added to your AI tool's folder (`.agents/`, `.claude/`, etc.)
Commit all of it like the rest of your source ([FAQ](../help/faq.md) covers why). Init changes nothing else in your repo (if it finds leftovers from an older OpenSpec version, it asks before cleaning them up).
Commit all of it like the rest of your source. Init changes nothing else in your repo (if it finds leftovers from an older OpenSpec version, it asks before cleaning them up).
### The `openspec/` folder
@@ -55,7 +71,7 @@ openspec/
└── archive/ completed changes move here
```
[Concepts](../guides/concepts.md) explains both artifacts; [Project config](../customize/project-config.md) covers `config.yaml`.
[Project config](../customize/project-config.md) covers `config.yaml`.
### The workflow files (skills and commands)
@@ -112,4 +128,26 @@ Config changes:
Answering yes applies it to the current project on the spot. Other projects pick it up on their next `openspec update`. The setting is global, per machine.
#### Claude Code doesn't show the workflows
Claude Code loads OpenSpec workflows from one or both of these project paths, based on your delivery setting:
- **Skills**: `.claude/skills/openspec-*/SKILL.md`
- **Commands**: `.claude/commands/opsx/<id>.md`
If the files are missing, refresh the project. In your terminal:
```bash
openspec update
```
If the command files exist but `/opsx:` shows no OpenSpec commands, update Claude Code and restart it. If commands still don't load, enable skills too. In your terminal:
```bash
openspec config set delivery both
openspec update
```
Restart Claude Code, then run `/openspec-propose` in its chat. If only some workflows are missing, [change your profile](../customize/profiles.md#expanding-the-set-optional-workflows).
Setup is done. The [Quickstart](quickstart.md) takes your first change from here.
+1 -1
View File
@@ -11,7 +11,7 @@ If you read nothing else, read these two pages:
That second one matters more than it looks. OpenSpec has two halves: a command line tool you run in your terminal, and slash commands you give to your AI assistant. Knowing which is which saves you the most common moment of confusion.
> **The best habit to build first: when you're not sure what to build, start with `/opsx:explore`.** It's a no-stakes thinking partner that reads your code, weighs options, and sharpens a fuzzy idea into a concrete plan before any artifact or code exists. The [Explore First](explore.md) guide makes the case.
> **The best habit to build first: when you're not sure what to build, start with `/opsx:explore`.** It's a no-stakes thinking partner that reads your code, weighs options, and sharpens a fuzzy idea into a concrete plan before any code gets written. The [Explore First](explore.md) guide makes the case.
## Pick your path
+12 -6
View File
@@ -47,16 +47,18 @@ deliberately remains the compatibility bare array documented in §4.13:
## 4. Command JSON shapes
### 4.1 `list --json`
`{ "changes": [ { "name", "completedTasks", "totalTasks", "lastModified", "status": "no-tasks"|"complete"|"in-progress" } ], "root": RootOutput }` — note the per-change `status` is a string enum here. `--specs`: `{ "specs": [ { "id", "requirementCount" } ], "root" }`.
`{ "changes": [ { "name", "completedTasks", "totalTasks", "lastModified", "status": "no-tasks"|"complete"|"in-progress", "nested"?: ["<area>/<name>", ...] } ], "warnings"?: [ { "code", "name", "nested", "message" } ], "root": RootOutput }` — note the per-change `status` is a string enum here. `--specs`: `{ "specs": [ { "id", "requirementCount" } ], "root" }`.
`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.
@@ -66,7 +68,7 @@ Change: `{ "id", "title", "deltaCount", "deltas": [...], "root" }`. Spec: `{ "id
`ReferenceIndexEntry`: `{ "store_id", "root"?, "specs"?: [{id,summary}], "fetch"?, "status": [] }` — resolved entries carry root/specs/fetch; unresolved carry store_id + warning status. Index capped at 50KB (`reference_index_truncated`).
### 4.6 `instructions apply --json`
`{ "changeName", "changeDir", "schemaName", "contextFiles": { "<artifactId>": ["/abs", ...] }, "progress": {total,complete,remaining}, "tasks": [{id,description,done}], "state": "blocked"|"all_done"|"ready", "missingArtifacts"?, "instruction", "references"?, "context"?, "operationGuidance"?, "root" }`. Both optional fields are read from the selected root on every invocation. `context` is a required prompt-level input whose relevant project facts, conventions, and constraints must be applied; `operationGuidance` is advisory input whose entries are followed only when applicable and compatible with the built-in workflow. Both remain separate from state, tasks, progress, context files, and the built-in instruction.
`{ "changeName", "changeDir", "schemaName", "contextFiles": { "<artifactId>": ["/abs", ...] }, "progress": {total,complete,remaining}, "tasks": [{id,description,done}], "state": "blocked"|"all_done"|"ready", "missingArtifacts"?, "missingPrerequisites"?, "warnings"?, "instruction", "references"?, "context"?, "operationGuidance"?, "root" }`. `missingArtifacts` is what apply blocks on (the schema's `apply.requires`); `missingPrerequisites` is everything still to build before apply can run, in build order - the transitive closure of those requires, so it can be the longer list. `warnings` lists non-blocking problems with the change itself - today, a change that is ready to implement with no delta specs and no `skip_specs: true`, the state `openspec validate` rejects. Both optional root fields (`context`, `operationGuidance`) are read from the selected root on every invocation. `context` is a required prompt-level input whose relevant project facts, conventions, and constraints must be applied; `operationGuidance` is advisory input whose entries are followed only when applicable and compatible with the built-in workflow. Both remain separate from state, tasks, progress, context files, and the built-in instruction.
### 4.7 `instructions archive --json`
`{ "changeName", "context"?, "operationGuidance"?, "root" }`. Requires a valid `--change` in the resolved repo/store root and uses the same required-context/advisory-guidance semantics as apply. This is a read-only runtime-input surface: it does not return the static archive workflow, inspect or merge delta specs, write main specs, or move the change.
@@ -77,6 +79,10 @@ Success: `{ "change": { "id", "path", "metadataPath", "schema" }, "root" }`. Fai
### 4.9 `archive <name> --json`
Success: `{ "archive": { "change", "archivedAs": "YYYY-MM-DD-name", "path", "specsUpdated", "totals"?, "warnings"? }, "root" }`. Failure: `{ "archive": null, "root"?, "status": [d] }`, exit 1. `specsUpdated` is true only when at least one spec file was written or retired (a capability whose last requirement the change removed has its spec deleted, which requires `retire_capabilities: true` in the change's `.openspec.yaml`; every retirement is named in `warnings`, with a pasteable Git recovery command only when the spec lived in the caller's checkout); an already-synced change archives with all-zero totals and the skips listed in `warnings`. JSON mode is strictly non-interactive: every prompt point becomes an `archive_*` code.
- **`archive: null`**: the command failed. This is not a guarantee that files are unchanged.
- **`archive_retirement_cleanup_failed`**: the change was archived, but retirement backup verification or cleanup failed. A listed backup path may have changed or disappeared. The message can also report a staged source left by a failed fallback-copy cleanup. Inspect the current archive and all reported recovery paths before cleanup. Preserve any needed content. Retrying archive does not clean up these paths.
- **`archive_error`**: the fallback diagnostic does not identify whether files changed. Inspect the change, archive, and affected specs before retrying.
### 4.10 `doctor --json`
`{ "root": { "path", "source", "store_id"?, "healthy", "status": [] }, "store": { "id", "metadata": {present,valid,remote?}, "origin_url"?, "drift"?: {ahead,behind}, "status": [] } | null, "references": [...], "status": [] }`. `drift` (present only for a git-backed store checkout that has an upstream tracking ref) is ahead/behind counts against the last-fetched upstream, not the live remote. Health findings of any severity exit 0. Failure payload: `{ "root": null, "store": null, "references": [], "status": [d] }`, exit 1.
@@ -110,7 +116,7 @@ setup/register: `{ "store": {id, root, metadata_path?}, "registry": {path, regis
`invalid_store_id`, `invalid_store_registry`, `invalid_store_metadata`, `store_registry_busy`, `store_not_found`, `no_store_registry`, `store_registry_changed`, `store_metadata_missing`, `store_metadata_id_mismatch`, `store_metadata_invalid`, `store_id_conflict`, `store_path_conflict`, `store_already_registered` (info).
### Store setup/register/remove
`store_setup_id_required`, `store_setup_path_required`, `store_setup_path_not_directory`, `store_setup_inside_git_repo`, `store_setup_non_empty_directory`, `store_setup_cancelled`, `store_path_required`, `store_path_missing`, `store_path_not_directory`, `store_root_pointer_declared`, `store_register_root_unhealthy`, `store_register_identity_confirmation_required`, `store_register_cancelled`, `store_remote_empty`, `store_remote_requires_hand_edit`, `store_remove_confirmation_required`, `store_remove_cancelled`, `store_remove_path_not_directory`, `store_remove_metadata_missing`, `store_root_missing` (warning in remove, error in doctor), `store_root_not_directory`.
`store_setup_id_required`, `store_setup_path_required`, `store_setup_path_not_directory`, `store_setup_inside_git_repo`, `store_setup_non_empty_directory`, `store_setup_cancelled`, `store_path_required`, `store_path_missing`, `store_path_not_directory`, `store_root_pointer_declared`, `store_register_root_unhealthy`, `store_register_identity_confirmation_required`, `store_register_cancelled`, `store_remote_empty`, `store_remote_requires_hand_edit`, `store_remove_confirmation_required`, `store_remove_cancelled`, `store_remove_path_not_directory`, `store_remove_metadata_missing`, `store_remove_contains_registered_store`, `store_root_missing` (warning in remove, error in doctor), `store_root_not_directory`.
### Store git
`store_git_init_failed`, `store_git_identity_missing`, `store_git_commit_failed`, `store_git_no_commits` (warning), `store_clone_fragile_directories` (warning), `store_remote_divergence` (info, doctor), `store_checkout_drift` (info, doctor).
@@ -122,7 +128,7 @@ setup/register: `{ "store": {id, root, metadata_path?}, "registry": {path, regis
`relationship_registry_unreadable`, `root_pointer_ignored`, `root_pointer_invalid`, `pointer_declarations_inert`.
### Archive (JSON mode)
`archive_change_name_required`, `archive_change_not_found`, `archive_change_symlink`, `archive_validation_failed`, `archive_confirmation_required`, `archive_tasks_incomplete`, `archive_spec_update_failed`, `archive_spec_validation_failed`, `archive_target_exists`, `archive_error`.
`archive_change_name_required`, `archive_change_not_found`, `archive_change_symlink`, `archive_validation_failed`, `archive_confirmation_required`, `archive_tasks_incomplete`, `archive_spec_update_failed`, `archive_spec_validation_failed`, `archive_target_exists`, `archive_retirement_cleanup_failed`, `archive_error`.
### Context writes
`context_file_exists`, `context_output_dir_missing`.
+1 -103
View File
@@ -13,7 +13,7 @@ The OpenSpec CLI (`openspec`) provides terminal commands for project setup, vali
| **Personal worksets** | `workset create`, `workset list`, `workset open`, `workset remove` | Keep and open personal, local working views in your tool |
| **Browsing** | `list`, `view`, `show` | Explore changes and specs |
| **Validation** | `validate` | Check changes and specs for issues |
| **Lifecycle** | `sync`, `archive` | Fold delta specs into the main specs, and finalize completed changes |
| **Lifecycle** | `archive` | Finalize completed changes |
| **Workflow** | `new change`, `status`, `instructions`, `templates`, `schemas` | Artifact-driven workflow support |
| **Schemas** | `schema init`, `schema fork`, `schema validate`, `schema which` | Create and manage custom workflows |
| **Config** | `config` | View and modify settings |
@@ -443,7 +443,6 @@ openspec list [options]
| `--specs` | List specs instead of changes |
| `--changes` | List changes (default) |
| `--sort <order>` | Sort by `recent` (default) or `name` |
| `--status <state>` | Only list changes in this lifecycle state: `proposed` or `shipped`. A change whose `.openspec.yaml` has no `status` counts as `proposed` |
| `--json` | Output as JSON |
**Examples:**
@@ -627,107 +626,6 @@ Validating add-dark-mode...
## Lifecycle Commands
### `openspec sync`
Fold a change's delta specs into the main specs, without archiving the change.
```
openspec sync [change-name] [options]
```
`archive` does two things at once: it folds a change's deltas into `openspec/specs/`
and it moves the change folder. `sync` does only the first, so the specs can be
brought up to date while the change is still open for review — and so CI can check
that they are.
**Arguments:**
| Argument | Required | Description |
|----------|----------|-------------|
| `change-name` | No | Change to sync. Omitted, `sync` acts on every change that declares `status: shipped` |
**Options:**
| Option | Description |
|--------|-------------|
| `--check` | Report shipped changes whose deltas are not in the main specs and exit 1. Writes nothing |
| `--ship` | Fold the named change, then set `status: shipped` on it — both land in one set of file changes for you to commit. If the fold fails, the field is not set |
| `-y, --yes` | Sync even when the change still has incomplete tasks |
| `--no-validate` | Skip validation (not recommended) |
| `--json` | Structured output for hooks and CI |
| `--store <id>` | Use a registered store as the OpenSpec root |
**The lifecycle field.** A change's `.openspec.yaml` may declare where it sits:
```yaml
schema: spec-driven
status: shipped # or: proposed
```
The field is optional and absent by default. A change with no `status` is
`proposed`, which is what every change under `changes/` has always meant, so a
project that never opts in is unaffected. Nothing writes the field on its own —
not `openspec new change`, not `archive`.
If the fold fails — validation, incomplete tasks, a retirement, a write error —
the field is not set. `--ship` writes `status: shipped` only after the specs are
correct, so a failed run never leaves a change claiming to be shipped with its
deltas absent.
**The CI gate.** `openspec sync --check` asserts one property: *a change that
claims to be shipped has its deltas in `specs/`*. A proposed change passes for
free, so the check is green as its resting state and red only on a real mistake —
unlike "is everything archived?", which is red for the entire life of every open
PR. It reads only files on disk, so a pre-commit hook, a pre-push hook and CI run
the same command and reach the same verdict.
```bash
# CI, pre-commit, pre-push — same command
openspec sync --check
```
**Examples:**
```bash
# Fold one change's deltas now; the change stays where it is
openspec sync add-rate-limit
# Mark it shipped and fold it, so both land in one commit when you make it
openspec sync add-rate-limit --ship
# Fold every change that declares status: shipped
openspec sync
# Gate: exits 1 if any shipped change has unfolded deltas
openspec sync --check
# Which changes have claimed to be shipped but aren't archived yet
openspec list --status shipped
```
**What it does:**
1. Validates the change's delta specs (unless `--no-validate`)
2. Refuses a change with incomplete tasks, unless `--yes` — folding a change
nothing implements yet writes requirements into `specs/` that aren't true
3. Validates every rebuilt spec before writing any of them, so a late failure
leaves the whole tree unchanged
4. Writes the updated main specs. Nothing moves; nothing is deleted
**What it deliberately does not do:**
- **It never deletes a spec.** When a change's `REMOVED` entries take a
capability's last requirement, retiring that capability deletes its `spec.md`.
That stays with `openspec archive`, behind the `retire_capabilities` marker.
`sync` reports the case and points you there.
- **It never checks archived changes.** Archived deltas are history, and later
changes supersede them. `--check` looks only at active changes that declare
`status: shipped` — a set that drains itself as those changes archive.
**Syncing early does not change archiving.** Re-applying a delta that is already
in the main specs is a no-op, so `openspec archive` afterwards reports
`Specs already in sync` and moves the folder exactly as it always did.
### `openspec archive`
Archive a completed change and merge delta specs into main specs.
+19 -8
View File
@@ -78,7 +78,7 @@ AI: Created openspec/changes/add-dark-mode/
### `/opsx:explore`
> **Start here when you're unsure.** Explore is a no-stakes thinking partner: it reads your codebase, compares options, and sharpens a fuzzy idea into a concrete plan before any change exists. It ships in the default profile. For the full case and more examples, see the [Explore First](explore.md) guide.
> **Start here when you're unsure.** Explore is a no-stakes thinking partner: it reads your codebase, compares options, and sharpens a fuzzy idea into a concrete plan before any code gets written. It ships in the default profile. For the full case and more examples, see the [Explore First](explore.md) guide.
Think through ideas, investigate problems, and clarify requirements before committing to a change.
@@ -97,6 +97,7 @@ Think through ideas, investigate problems, and clarify requirements before commi
- Investigates the codebase to answer questions
- Compares options and approaches
- Creates visual diagrams to clarify thinking
- Captures the exploration when you ask, or when you say yes to its offer: scaffolds a change with `openspec new change` and writes the planning artifacts you name, or updates an existing change's artifacts
- Can transition to `/opsx:propose` (default) or `/opsx:new` (expanded workflow) when insights crystallize
**Example:**
@@ -119,14 +120,20 @@ AI: Let me investigate your current auth setup...
Your API already has CORS configured. Which direction interests you?
You: Let's go with JWT. Can we start a change for that?
You: Let's go with JWT.
AI: Ready when you are. Run /opsx:propose add-jwt-auth to begin.
AI: That's a decision worth keeping. Want me to start a change called
add-jwt-auth? Just the change folder, nothing else yet.
You: Yes.
AI: Started openspec/changes/add-jwt-auth/. Say the word and I'll
write the proposal, specs, and tasks from what we just worked out.
```
**Tips:**
- Use when requirements are unclear or you need to investigate
- No artifacts are created during exploration
- It never writes code, and writes nothing else unless you ask, or say yes when it offers
- Good for comparing multiple approaches before deciding
- Can read files and search the codebase
@@ -345,7 +352,13 @@ Revise a change's existing planning artifacts and keep them coherent with one an
- Applies your requested revision, or reviews the artifacts for contradictions if you didn't name one
- Reconciles the other existing artifacts in any direction (a design edit may ripple back to the proposal)
- Confirms every edit with you before writing, one artifact at a time
- Ends by recommending the next step: `/opsx:continue` (artifacts missing), `/opsx:apply` (carry a revised plan into code), or `/opsx:archive` (all done)
- Ends by recommending the next step: `/opsx:continue` (unstarted artifacts), `/opsx:apply` (carry a revised plan into code), or `/opsx:archive` (all done)
**Missing files:**
- For a glob artifact such as `specs/**/*.md` with at least one existing file, update can propose a missing companion file. It uses the schema's instructions and asks you to confirm the concrete path before creating it.
- Artifacts with no files yet remain with `/opsx:continue`. Intentionally skipped artifacts stay untouched.
- New files must stay inside the change directory. If a file appears at the confirmed path before creation, update stops instead of overwriting it.
**Example:**
@@ -366,7 +379,7 @@ AI: Reading add-dark-mode artifacts...
**Tips:**
- It won't create missing artifacts - that's `/opsx:continue`
- It won't start an artifact with no existing files. Enable `/opsx:continue` for that, or use `openspec status` and `openspec instructions` if that optional workflow isn't installed.
- If the change was already implemented, follow up with `/opsx:apply` so the code matches the revised plan
- If your revision changes the *intent* of the change, start fresh with a new change instead (see [When to Update vs. Start Fresh](opsx.md#when-to-update-vs-start-fresh))
@@ -444,8 +457,6 @@ AI: Verifying add-dark-mode...
**Optional command.** Merge delta specs from a change into main specs. Archive will prompt to sync if needed, so you typically don't need to run this manually.
> Not the same as the CLI's `openspec sync`. This one is the agent doing the merge in your session. `openspec sync` is a deterministic terminal command that does the same fold without a model, and carries the `--check` gate for CI — see [CLI](cli.md#openspec-sync).
**Syntax:**
```
/opsx:sync [change-name]
+3 -1
View File
@@ -6,7 +6,9 @@ Listed projects are maintained independently. Inclusion does not imply official
## Projects and resources
- **[OpenSpec UI](https://github.com/VeryComplexAndLongName/OpenSpec-UI)**: A standalone web dashboard and VS Code extension for browsing OpenSpec changes, archives, specs, and tasks.
- **[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
+2
View File
@@ -341,6 +341,8 @@ Tasks are the **implementation checklist** — concrete steps with checkboxes.
- Group related tasks under headings
- Use hierarchical numbering (1.1, 1.2, etc.)
- Keep tasks small enough to complete in one session
- State how each task is verified (a test, command, or observable result)
- Land the tests and documentation each group's work calls for inside that group, not in a final catch-up group
- Check tasks off as you complete them
## Delta Specs
+1 -1
View File
@@ -68,7 +68,7 @@ Tip: for a fix, a good scenario is the regression test in prose. "GIVEN a logged
**When to use it:** you have a problem but not yet a plan. You're not sure what to build, or which approach is right.
Start with `/opsx:explore`. It's a thinking partner with no structure and no artifacts created. It reads your codebase and helps you decide.
Start with `/opsx:explore`. It's a thinking partner with no structure. It never writes code, and writes nothing else unless you ask it to capture what you decided, or say yes when it offers. It reads your codebase and helps you decide.
```text
You: /opsx:explore
+12 -6
View File
@@ -1,6 +1,6 @@
# Explore First
**`/opsx:explore` is your thinking partner. Reach for it whenever you have a problem but not yet a plan.** It investigates your codebase, weighs options with you, and clarifies what you actually want, all before a single artifact or line of code is created. When the picture is clear, it hands off to `/opsx:propose`.
**`/opsx:explore` is your thinking partner. Reach for it whenever you have a problem but not yet a plan.** It investigates your codebase, weighs options with you, and clarifies what you actually want, all before a line of code is written. When the picture is clear, it hands off to `/opsx:propose`.
If you take one habit from these docs, take this one: **when you're not sure, explore before you propose.**
@@ -27,14 +27,16 @@ Explore is a **conversation**, not a generator.
- Compare options and name the tradeoffs of each.
- Draw diagrams to make a design legible.
- Help you narrow a vague idea into a concrete, buildable scope.
- Capture the exploration when you ask, or when you accept its offer: it scaffolds the change with `openspec new change` and writes the planning artifacts you named, or updates an existing change's artifacts.
- Transition to `/opsx:propose` when you're ready.
**It does not:**
- Create a change folder.
- Write any artifacts (no proposal, specs, design, or tasks).
- Write or modify code.
- Write or modify code. Explore never writes code, on any path, capture included.
- Design or edit your schemas or templates. Shaping those is a change, not thinking.
- Start a change or write an artifact on its own. It writes nothing unless you ask, or say yes when it offers, and then only what you agreed to, plus the setup files starting a change needs (see below).
- Push you toward capturing. It offers when the thinking crystallizes; you decide.
That's the point. Exploring costs you nothing and commits you to nothing. You can explore three dead ends, learn something from each, and only then propose the path that survived.
That's the point. Exploring costs you nothing and commits you to nothing until you say so. You can explore three dead ends, learn something from each, and only then propose the path that survived.
## It's already installed
@@ -95,6 +97,10 @@ explore ──► propose ──► apply ──► archive
You can say it in plain language ("let's turn this into a change") or run `/opsx:propose <name>` directly. Either way, the exploration you just did becomes the foundation of the proposal, not throwaway chat.
You can also ask explore to capture the change itself, without leaving the conversation: "start a change for this" scaffolds the folder, and "write the proposal too" writes exactly the artifacts you named. Scaffolding also lays down the change's own metadata, and fills in anything your project is missing at the top level (`openspec/specs/`, `openspec/changes/archive/`, a `config.yaml`).
That's the same destination as handing off, with one difference: propose writes the whole set your schema requires to reach implementation, while capture writes only the artifacts you named.
If you use the expanded command set, explore can hand off to `/opsx:new` instead, for step-by-step artifact creation. See [Workflows](workflows.md).
## Tips for a good exploration
@@ -107,7 +113,7 @@ If you use the expanded command set, explore can hand off to `/opsx:new` instead
## The honest tradeoffs
**What you gain:** explore catches wrong turns at the cheapest possible moment, before any artifact exists. It's especially powerful in unfamiliar code, where the AI's ability to read and summarize the system saves you an afternoon of spelunking.
**What you gain:** explore catches wrong turns at the cheapest possible moment, before you've committed to anything. It's especially powerful in unfamiliar code, where the AI's ability to read and summarize the system saves you an afternoon of spelunking.
**What it costs:** a little patience. Explore is a conversation, so it's slower than firing off `/opsx:propose` and hoping. For work you genuinely understand already, that extra step is pure overhead, and you should skip it.
+1 -1
View File
@@ -50,7 +50,7 @@ Both are files OpenSpec writes so your assistant can run the workflow. Skills (`
### Where should I start if I'm not sure what to build?
With `/opsx:explore`. It's a no-stakes thinking partner that reads your codebase, lays out options, and turns a fuzzy problem into a concrete plan, all before any change or code exists. It's in the default profile, so it's always available. When the plan is clear, it hands off to `/opsx:propose`. This is the single best habit to form, because it stops an eager AI from confidently building the wrong thing. See [Explore First](explore.md).
With `/opsx:explore`. It's a no-stakes thinking partner that reads your codebase, lays out options, and turns a fuzzy problem into a concrete plan, all before any code gets written. It's in the default profile, so it's always available. When the plan is clear, it hands off to `/opsx:propose`. This is the single best habit to form, because it stops an eager AI from confidently building the wrong thing. See [Explore First](explore.md).
### What's the simplest possible flow?
+1 -1
View File
@@ -26,7 +26,7 @@ Two terminal steps to set up, then you live in chat. The rest of this guide unpa
**Don't want to do the terminal part yourself?** Paste the [setup prompt](installation.md#install-with-your-ai-assistant) into your assistant and it handles both lines, then reports what it created.
> **Not sure what to build yet? Start with `/opsx:explore`.** It's a no-stakes thinking partner that reads your codebase, weighs options, and sharpens a fuzzy idea into a concrete plan, all before any artifact or code exists. When the picture is clear, it hands off to `/opsx:propose`. This is the single best habit for working with an AI that will otherwise confidently build the wrong thing. See the [Explore guide](explore.md).
> **Not sure what to build yet? Start with `/opsx:explore`.** It's a no-stakes thinking partner that reads your codebase, weighs options, and sharpens a fuzzy idea into a concrete plan, all before any code gets written. When the picture is clear, it hands off to `/opsx:propose`. This is the single best habit for working with an AI that will otherwise confidently build the wrong thing. See the [Explore guide](explore.md).
## How It Works
+2 -4
View File
@@ -38,9 +38,7 @@ Terms are grouped by topic, then alphabetized within each group.
**Archive.** The act of finishing a change. Its delta specs merge into the main specs, and the change folder moves to `openspec/changes/archive/YYYY-MM-DD-<name>/`. After archiving, your specs describe the new reality. See [Concepts](concepts.md#archive).
**Sync.** Merging a change's delta specs into the main specs *without* archiving the change. Usually automatic (archive offers to do it). Available on its own two ways: `/opsx:sync`, where the agent does the merge ([Commands](commands.md#opsxsync)), and `openspec sync`, the deterministic CLI command ([CLI](cli.md#openspec-sync)).
**Shipped / proposed.** A change may declare its lifecycle state as `status: proposed | shipped` in its `.openspec.yaml`. The field is optional and absent by default; no `status` means `proposed`. `openspec sync --check` gates on it — a change that claims to be shipped must have its deltas in the main specs — which makes the specs enforceable in CI without a check that is red for the whole life of every PR. See [OpenSpec on a Team](team-workflow.md#enforcing-it-in-ci).
**Sync.** Merging a change's delta specs into the main specs *without* archiving the change. Usually automatic (archive offers to do it), but available on its own as `/opsx:sync` for long-running changes. See [Commands](commands.md#opsxsync).
## Workflow and commands
@@ -48,7 +46,7 @@ Terms are grouped by topic, then alphabetized within each group.
**Slash command.** A command you type into your AI assistant's chat, like `/opsx:propose`. Slash commands drive the workflow. They are not terminal commands. See [How Commands Work](how-commands-work.md).
**Explore (`/opsx:explore`).** The thinking-partner command. It reads your codebase, compares options, and clarifies a fuzzy idea into a concrete plan, creating no artifacts and writing no code. The recommended starting point whenever you have a problem but not yet a plan. See [Explore First](explore.md).
**Explore (`/opsx:explore`).** The thinking-partner command. It reads your codebase, compares options, and clarifies a fuzzy idea into a concrete plan. It never writes code, and writes nothing else unless you ask it to capture the exploration as a change, or say yes when it offers. The recommended starting point whenever you have a problem but not yet a plan. See [Explore First](explore.md).
**CLI.** The `openspec` program you run in your terminal. It sets up projects, lists and validates changes, opens the dashboard, and archives. The terminal half of OpenSpec. See [CLI](cli.md).
+3 -1
View File
@@ -213,7 +213,9 @@ Works through tasks, checking them off as you go. If you're juggling multiple ch
```
/opsx:update add-dark-mode - we're storing the theme in a cookie now
```
Revises the change's existing planning artifacts and keeps them coherent - in any direction (a design edit may ripple back to the proposal). Planning artifacts only: it never edits code, and it never creates missing artifacts (that's `/opsx:continue`). Every edit is confirmed with you first. If the change was already implemented, it recommends `/opsx:apply` so the code catches up with the revised plan. If your revision changes the change's *intent*, start fresh instead - see [When to Update vs. Start Fresh](#when-to-update-vs-start-fresh).
Revises the change's existing planning artifacts and keeps them coherent in any direction (a design edit may ripple back to the proposal). It never edits code. Every edit is confirmed with you first. See [the update reference](commands.md#opsxupdate) for how it handles missing files without starting a new artifact.
If the change was already implemented, it recommends `/opsx:apply` so the code catches up with the revised plan. If your revision changes the change's *intent*, start fresh instead. See [When to Update vs. Start Fresh](#when-to-update-vs-start-fresh).
### Sync delta specs
```text
+1 -1
View File
@@ -56,7 +56,7 @@ In the default setup, your day looks like this. Optionally think it through firs
/opsx:archive → specs updated, change archived
```
**When in doubt, start by exploring.** `/opsx:explore` is a no-stakes thinking partner: it reads your code, lays out options, and turns a fuzzy idea into a concrete plan before any artifact exists. It's the best antidote to an AI that will otherwise build *something* from a vague prompt. Already know exactly what you want? Skip straight to `/opsx:propose`. Either way, explore ships in the default profile, so it's always there. See the [Explore guide](explore.md).
**When in doubt, start by exploring.** `/opsx:explore` is a no-stakes thinking partner: it reads your code, lays out options, and turns a fuzzy idea into a concrete plan before any code gets written. It's the best antidote to an AI that will otherwise build *something* from a vague prompt. Already know exactly what you want? Skip straight to `/opsx:propose`. Either way, explore ships in the default profile, so it's always there. See the [Explore guide](explore.md).
Those are slash commands, typed in your AI assistant's chat. Setup (`openspec init`) happens in your terminal. If that split is new to you, read [How Commands Work](how-commands-work.md) first; it's the most common point of confusion.
+1 -1
View File
@@ -86,7 +86,7 @@ to read the hint.
| Hermes Agent (`hermes`) | `.hermes/skills/openspec-*/SKILL.md`\*\*\* | Not generated (no command adapter; use skill-based `/openspec-*` invocations) |
| iFlow (`iflow`) | `.iflow/skills/openspec-*/SKILL.md` | `.iflow/commands/opsx-<id>.md` |
| Junie (`junie`) | `.junie/skills/openspec-*/SKILL.md` | `.junie/commands/opsx-<id>.md` |
| Kilo Code (`kilocode`) | `.kilocode/skills/openspec-*/SKILL.md` | `.kilocode/workflows/opsx-<id>.md` |
| Kilo Code (`kilocode`) | `.kilocode/skills/openspec-*/SKILL.md` | `.kilo/command/opsx-<id>.md` |
| Kimi Code (`kimi`) | `.kimi-code/skills/openspec-*/SKILL.md` | Not generated (no command adapter; use skill-based `/skill:openspec-*` invocations) |
| Kiro (`kiro`) | `.kiro/skills/openspec-*/SKILL.md` | `.kiro/prompts/opsx-<id>.prompt.md` |
| Lingma (`lingma`) | `.lingma/skills/openspec-*/SKILL.md` | `.lingma/commands/opsx/<id>.md` |
-32
View File
@@ -55,38 +55,6 @@ Archiving folds a change's deltas into your main `openspec/specs/` and moves the
Pick one and be consistent. Either way, `/opsx:archive` checks that tasks are complete and offers to sync first, so nothing merges half-finished by accident.
## Enforcing it in CI
The obvious CI check — "nothing is left unarchived" — doesn't work, because it's red for the whole life of every PR. An open change sits in `changes/`, unarchived, precisely because it isn't finished. A gate that is red as its resting state is one everyone learns to ignore.
`openspec sync --check` is the check that works. It asks a different question: **does anything that claims to be shipped still have deltas missing from `specs/`?** A change that hasn't made that claim passes for free, so green is the resting state and red means a real mistake.
```yaml
# .github/workflows/specs.yml
- run: npx openspec sync --check
```
The claim is one line in the change's `.openspec.yaml`:
```yaml
schema: spec-driven
status: shipped
```
The everyday shape of it:
1. Open the PR. The change is `proposed` (the default — nothing to write). The gate is green.
2. When the work is done and reviewed, mark it shipped and fold its deltas in one step:
```bash
openspec sync add-rate-limit --ship
```
That sets `status: shipped` and writes the deltas into `specs/` in one command, so both land in the same set of file changes for you to commit together. OpenSpec never runs git itself — commit the result as usual.
3. Merge. Archive whenever you like afterwards — re-applying a delta that's already folded is a no-op, so `openspec archive` behaves exactly as it always did.
The check is a pure function of the files on disk, so the same command works as a pre-commit hook, a pre-push hook, and the CI gate, and all three agree.
`openspec list --status shipped` shows which changes have made the claim but aren't archived yet.
## Two people, parallel changes
Because changes are separate folders, they don't collide:
+2 -2
View File
@@ -140,7 +140,7 @@ You: Yes.
You: /opsx:propose rebuild-search-index-on-write
```
Explore creates no artifacts and writes no code. It's a free, no-stakes conversation that turns a vague worry into a precise change, so the proposal that follows is sharp. Already know exactly what you want? Skip it and go straight to `/opsx:propose`. Full guide: [Explore First](explore.md).
Explore never writes code, and writes nothing else unless you ask, or say yes when it offers. It's a free, no-stakes conversation that turns a vague worry into a precise change, so the proposal that follows is sharp. Already know exactly what you want? Skip it and go straight to `/opsx:propose`. Full guide: [Explore First](explore.md).
### Expanded/Full Workflow (custom selection)
@@ -493,7 +493,7 @@ AI: Let me investigate your current setup and options...
Your current stack suggests #1 or #2. What's your scale?
```
Exploration clarifies thinking before you create artifacts.
Exploration clarifies thinking before any code gets written.
### Verify Before Archiving
+85 -58
View File
@@ -15,71 +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-SNPeEUa+amkZYRO5tHeUwDBT4betXYPKnfZiEyhN7fE=";
};
nativeBuildInputs = with pkgs; [
nodejs_22
npmHooks.npmInstallHook
pnpmConfigHook
pnpm_10
];
buildPhase = ''
runHook preBuild
pnpm run build
runHook postBuild
'';
dontNpmPrune = true;
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;
}
);
@@ -1,2 +1,2 @@
schema: spec-driven
created: 2026-09-07
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
+20
View File
@@ -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.
@@ -1,74 +0,0 @@
# Let a change's specs be folded before it is archived
## Why
`archive` does two separable jobs in one command. It folds a change's deltas into
`openspec/specs/`, and it declares the change finished by moving its directory.
Welding them means the fold can only happen at the moment the move happens, which
on a team that reviews before merging is after the pull request closes.
So a team that wants CI to assert "the specs describe what shipped" has nothing to
assert during review. The only property expressible today is "nothing is left
unarchived", and that is violated by design for the entire life of every open PR:
the change sits in `changes/`, unarchived, precisely because it is not finished.
A gate that is red as its resting state is one everyone learns to ignore, and it
masks the real failures underneath (#1683).
The fix is to make the check conditional on the change's own claim — not "is
everything archived?" but "does anything claiming to be shipped still have deltas
missing from the specs?" A proposed change passes for free, so green is the
resting state and red means a real mistake.
## What Changes
- **`openspec sync [change]`** folds delta specs into the main specs without
archiving. The merge engine already supports this: re-applying a folded delta
is a no-op it names the "early-sync pattern", so `archive` afterwards behaves
exactly as it always did.
- **`openspec sync --check`** asserts `shipped ⇒ folded` over the working tree and
exits 1 with the offending changes named. A pure function of files on disk, so
a pre-commit hook, a pre-push hook and CI run one command and agree.
- **`status: proposed | shipped`** becomes an optional field in a change's
`.openspec.yaml`. Absent means `proposed`, which is what a change under
`changes/` has always meant. Nothing writes it: not `new change`, not `archive`.
- **`openspec sync <change> --ship`** sets the field and folds in one working-tree
diff, so no intermediate commit claims a change is shipped while the specs say
otherwise.
- **`openspec list --status <state>`** filters by the field, and renders a
lifecycle column only when some change in the root declares one.
Two deliberate limits, both to keep this additive rather than a second lifecycle:
- **Sync never deletes a spec.** Retiring a capability is the one irreversible
operation in the system; it stays with `archive`, behind the
`retire_capabilities` marker and its rollback-safe deletion. Sync reports the
case and names archive.
- **Sync never examines archived changes.** Their deltas are history and later
changes supersede them; re-applying a months-old delta over everything that
came after is a merge conflict, not a drift check. The checked set is the
active changes declaring `shipped`, which drains itself as they archive.
"Folded" is decided by running the merge builder and seeing that it applied zero
operations — the same predicate `archive` uses to decide it has nothing to write.
Not a byte-comparison of the rebuilt output: the rebuild normalizes blank lines,
so a hand-formatted main spec would compare unequal while being perfectly in
sync. Sharing archive's own predicate is also what stops the checker and the doer
from drifting apart (#1112).
## Impact
- Affected specs: `cli-sync` (ADDED), `cli-list` (MODIFIED: filtering)
- Affected code: `src/core/sync.ts` (new), `src/core/list.ts`,
`src/utils/change-metadata.ts`, `src/core/change-metadata/schema.ts`,
`src/cli/index.ts`, `src/core/completions/command-registry.ts`,
`src/core/archive.ts` (two helpers exported, no behavior change)
- Affected docs: `docs/cli.md`, `docs/team-workflow.md`,
`docs-lab/reference/cli.md`
Credit: the design is Matan Bendix Shenhav's, from #1683 and his implementation
#1684. His: the diagnosis, `shipped ⇒ folded` as a tree predicate (V), the
checker-versus-doer argument (IV), the standalone idempotent `sync` (III), status
as data (I and II), and shipping in one working-tree diff (VI). This change takes
a smaller, additive subset — no mode, no layout change, no migration — and
decides folded-ness by archive's zero-operations predicate rather than his
byte-identical regeneration.
@@ -1,29 +0,0 @@
## ADDED Requirements
### Requirement: Lifecycle Status Filtering
The command SHALL be able to filter changes by their declared lifecycle state, and
SHALL surface that state without changing the output of a project that has never
declared one.
#### Scenario: Filtering by state
- **WHEN** `openspec list --status shipped` is executed
- **THEN** only changes declaring `status: shipped` SHALL be listed
- **AND** `--status proposed` SHALL list every change that declares `proposed` or
declares no status at all
#### Scenario: An unknown state is rejected
- **WHEN** `--status` is given a value other than `proposed` or `shipped`
- **THEN** the command SHALL exit 1 naming the accepted values
- **AND** SHALL NOT list every change as though the filter matched nothing
#### Scenario: No lifecycle output without a declaration
- **WHEN** no change in the root declares a `status`
- **THEN** the human listing SHALL render no lifecycle column
- **AND** the JSON output SHALL carry no lifecycle key
#### Scenario: The lifecycle appears once any change declares one
- **WHEN** at least one change declares a `status`
- **THEN** the human listing SHALL render a lifecycle column, showing `proposed`
for changes that declare nothing
- **AND** the JSON output SHALL carry a `lifecycle` key for the declaring changes
only, leaving the existing `status` key meaning task progress
@@ -1,142 +0,0 @@
# Sync Command Specification
## Purpose
The `openspec sync` command SHALL fold a change's delta specs into the main specs
without archiving the change, and SHALL provide a check that a change claiming to
be shipped has its deltas present in the main specs.
## ADDED Requirements
### Requirement: Lifecycle Status Field
A change SHALL be able to declare its lifecycle state as data in its
`.openspec.yaml`, using an optional `status` field whose value is `proposed` or
`shipped`. A change that does not declare one SHALL be treated as `proposed`.
#### Scenario: Undeclared status reads as proposed
- **WHEN** a change's `.openspec.yaml` has no `status` field, or the change has no
metadata file at all
- **THEN** every reader SHALL treat the change as `proposed`
- **AND** no command SHALL write the field on the change's behalf
#### Scenario: A status that cannot be determined is not rounded to proposed
- **WHEN** a change's metadata mentions `status` but cannot be honored, because the
file does not parse, carries an unknown value, or names a schema that does not
resolve
- **THEN** the state SHALL be reported as undetermined with its reason
- **AND** `openspec sync --check` SHALL fail rather than pass the change
#### Scenario: Broken metadata that never mentions status is left alone
- **WHEN** a change's metadata cannot be honored and does not mention `status`
- **THEN** the change SHALL read as `proposed`
- **AND** `openspec sync --check` SHALL NOT report it
### Requirement: Folding Delta Specs
The command SHALL apply a change's delta specs to the main specs, leaving the
change directory where it is.
#### Scenario: Folding a named change
- **WHEN** `openspec sync <change>` is executed
- **THEN** each delta under the change's `specs/` SHALL be applied to its main spec
- **AND** the change directory SHALL NOT be moved
- **AND** the change's declared status SHALL NOT affect whether it is folded
#### Scenario: Folding every shipped change
- **WHEN** `openspec sync` is executed with no change name
- **THEN** every active change declaring `status: shipped` SHALL be folded
- **AND** a change declaring no status SHALL NOT be folded
#### Scenario: Folding is idempotent
- **WHEN** `openspec sync` is run against a change whose deltas are already in the
main specs
- **THEN** no file SHALL be written
- **AND** the command SHALL report that the specs are already in sync
#### Scenario: Archiving after a sync is unaffected
- **WHEN** a change is folded by `openspec sync` and later archived
- **THEN** `openspec archive` SHALL apply zero operations and write no spec file
- **AND** the change SHALL be moved to the archive as it always was
### Requirement: Shipped Changes Are Folded
The command SHALL provide a check that asserts one property over the working tree:
every change claiming to be shipped has its deltas present in the main specs.
#### Scenario: A proposed change passes for free
- **WHEN** `openspec sync --check` is executed and no active change declares
`status: shipped`
- **THEN** the command SHALL exit 0
- **AND** SHALL write no file
#### Scenario: A shipped change with unfolded deltas fails the check
- **WHEN** `openspec sync --check` is executed and an active change declaring
`status: shipped` has a delta that is not in its main spec
- **THEN** the command SHALL exit 1
- **AND** SHALL name the change and each capability whose delta is unapplied
- **AND** SHALL name the command that folds them
- **AND** SHALL write no file
#### Scenario: Folded-ness is decided by the merge builder
- **WHEN** deciding whether a change's deltas are present in the main specs
- **THEN** the decision SHALL be that re-applying the delta produces zero applied
operations, which is the same predicate the archive command uses to decide it
has nothing to write
- **AND** SHALL NOT be a byte comparison against a rebuilt spec
#### Scenario: Archived changes are never examined
- **WHEN** `openspec sync --check` is executed
- **THEN** only active changes SHALL be examined
- **AND** a change that has been archived SHALL NOT be checked
### Requirement: Sync Never Deletes A Spec
The command SHALL NOT delete a main spec under any circumstance. Retiring a
capability remains the archive command's operation.
#### Scenario: A retirement is handed to archive
- **WHEN** a change's REMOVED entries would take a capability's last requirement
- **THEN** `openspec sync` SHALL refuse to fold that change
- **AND** SHALL name `openspec archive` as the command that performs a retirement
- **AND** the main spec file SHALL remain on disk
#### Scenario: The check reports a retirement without offering sync as the fix
- **WHEN** `openspec sync --check` finds a shipped change that would retire a
capability
- **THEN** the command SHALL exit 1 naming the retirement
- **AND** SHALL NOT tell the user to run `openspec sync`
### Requirement: Guards Before Writing
The command SHALL run the same guards the archive command runs before it writes a
main spec.
#### Scenario: Delta specs are validated
- **WHEN** a change's delta specs fail validation and `--no-validate` was not passed
- **THEN** the command SHALL refuse the change and write no file
#### Scenario: Incomplete tasks block the fold
- **WHEN** a change has incomplete tasks and `--yes` was not passed
- **THEN** the command SHALL refuse the change and write no file
- **AND** SHALL name the rerun that proceeds anyway
#### Scenario: Every rebuilt spec is validated before any is written
- **WHEN** any rebuilt spec would fail validation
- **THEN** no spec file SHALL be written at all
#### Scenario: A fold that does not settle is named
- **WHEN** two shipped changes claim the same requirement in ways that cannot both
hold, so re-evaluating after the write still reports unfolded deltas
- **THEN** the command SHALL name the changes involved
- **AND** SHALL NOT retry the fold
### Requirement: Shipping In One Diff
The command SHALL offer to set a change's status and fold it in a single run, so
that no intermediate commit claims a change is shipped while its deltas are absent
from the main specs.
#### Scenario: Marking a change shipped and folding it
- **WHEN** `openspec sync <change> --ship` is executed
- **THEN** the change's `.openspec.yaml` SHALL be set to `status: shipped`
- **AND** its deltas SHALL be folded in the same run
- **AND** the metadata file's comments and key order SHALL be preserved
#### Scenario: Ship is refused where it cannot apply
- **WHEN** `--ship` is passed with `--check`, or with no change name
- **THEN** the command SHALL refuse and say which flag combination is valid
@@ -1,25 +0,0 @@
## 1. Lifecycle field
- [x] 1.1 Add optional `status: proposed | shipped` to `ChangeMetadataSchema`
- [x] 1.2 Add `readChangeStatus`, failing closed on metadata it cannot honor
- [x] 1.3 Add `writeChangeStatus`, preserving comments and key order
## 2. Sync command
- [x] 2.1 Add `src/core/sync.ts` with the fold and the `--check` predicate
- [x] 2.2 Run archive's guards before writing: validation, task completion,
rebuilt-spec validation
- [x] 2.3 Refuse retirements and name `openspec archive` instead
- [x] 2.4 Re-evaluate after writing so a non-convergent pair is named, not looped on
- [x] 2.5 Register the CLI command and its completion entry
## 3. List filter
- [x] 3.1 Add `--status <state>`, counting an undeclared change as `proposed`
- [x] 3.2 Render the lifecycle column and the JSON `lifecycle` key only when declared
## 4. Docs and verification
- [x] 4.1 Document `openspec sync` and `list --status`
- [x] 4.2 Add the CI section to the team workflow guide
- [x] 4.3 Tests covering the gate, the guards, and the archive interaction
@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-09-04
@@ -0,0 +1,44 @@
# Name the command that resumes a change
## Why
`openspec status` reported where a change stood and stopped there. The command
that moves it forward was already computed: `buildNextSteps` derives it and
`--json` publishes it as `nextSteps`. But the text surface never rendered it.
So the surface a person actually reads ended on a checklist. `openspec new
change` hands off with `Next: openspec status --change <name>`, and that next
command then had no verb of its own. Picking a change back up after a lost
session, or opening one somebody else started, meant already knowing which
command came next (#906).
The completion case was the worst of it. Once every planning artifact existed,
status printed a lone green "All planning artifacts complete!", which reads as
*you are done* even while `tasks.md` sits half-checked. That is what #906
reports: every artifact showed `done` rather than `ready`, so the conclusion was
that nothing was left to run.
## What Changes
- `openspec status` ends with a `Next:` line naming one command: the next ready
artifact's `openspec instructions` call while planning is unfinished, and
`openspec instructions apply` once every planning artifact exists.
- The line carries `--store <id>` whenever the resolved root is a store. A
command without the flag would resolve against the pointer repo instead of the
store the status was read from.
- `--all` gives every change in the sweep its own line, and gives none to an
entry that failed to load, since a failed entry has no artifact statuses to reason
about.
- The line is built from the same resolution as the JSON `nextSteps` sentence,
so the two surfaces cannot name different commands. `nextSteps` itself is
unchanged, character for character.
No new command, no new flag, no new JSON field. This renders a value the agent
contract already publishes.
## Impact
- Affected specs: `cli-artifact-workflow` (MODIFIED: Next Artifact Discovery)
- Affected code: `src/commands/workflow/status.ts`,
`src/core/change-status-policy.ts`
- Affected docs: `docs/cli.md` (the status text output example)
@@ -0,0 +1,37 @@
## MODIFIED Requirements
### Requirement: Next Artifact Discovery
The workflow SHALL use `openspec status` output to determine what can be created next, rather than a separate next-command surface.
#### Scenario: Discover next artifacts from status output
- **WHEN** a user needs to know which artifact to create next
- **THEN** `openspec status --change <id>` identifies ready artifacts with `[ ]`
- **AND** the first `[ ]` entry is the schema's recommended next artifact
- **AND** no dedicated "next command" is required to continue the workflow
#### Scenario: Status names the command that moves the change forward
- **WHEN** a user runs `openspec status --change <id>` in text mode and a next step resolves
- **THEN** the output ends with a `Next:` line naming exactly one command to run
- **AND** that command is `openspec instructions <artifact> --change "<id>" --json` for the first ready artifact while any planning artifact is still ready
- **AND** it is `openspec instructions apply --change "<id>" --json` once every planning artifact exists, printed after the completion line rather than in place of it, because that line alone reads as "you are done" while implementation tasks remain
- **AND** the named artifact is never one the change skipped, which satisfies its dependents but must not be created
- **AND** the artifact id comes from the resolved schema, so a project whose schema declares neither of the default artifact names still gets a usable command
#### Scenario: The named command carries the store selection
- **WHEN** the resolved root is a store
- **THEN** the `Next:` command includes `--store <id>`, so it resolves against the same root the status was read from rather than the pointer repo
#### Scenario: Both surfaces name the same command
- **WHEN** a next step resolves
- **THEN** the command printed on the `Next:` line and the command inside the JSON `nextSteps` sentence are derived from one resolution, so the two surfaces cannot name different commands
- **AND** the `Next:` line never appears in `--json` output, which stays parseable
#### Scenario: No next step resolves
- **WHEN** no artifact is ready and planning is not complete, or a change in an `--all` sweep failed to load
- **THEN** no `Next:` line is printed for it, rather than a guessed or shared command
@@ -0,0 +1,17 @@
# Tasks
## 1. Resolve the next step once
- [x] 1.1 Extract `resolveNextStep` returning the command and the sentence, leaving `buildNextSteps` returning exactly that sentence so the JSON contract is unchanged
- [x] 1.2 Pin the published sentences verbatim in a unit test, so splitting command from sentence cannot reword the contract
## 2. Render it on the text surface
- [x] 2.1 Print a `Next:` line from the resolved command, after the completion line rather than in place of it
- [x] 2.2 Thread the store selection into the renderer so the command carries `--store`
- [x] 2.3 Give every change in an `--all` sweep its own line, and a failed entry none
## 3. Cover the behavior
- [x] 3.1 Assert the ready, planning-complete, skipped, and custom-schema cases end to end
- [x] 3.2 Assert the printed command appears verbatim inside the JSON sentence, and that the line never leaks into `--json`
## 4. Record it
- [x] 4.1 Update the `cli-artifact-workflow` spec delta and the `docs/cli.md` status output example
@@ -105,6 +105,12 @@ Review feedback flagged that "update" alone is generic — could it apply to any
### 6. Next-step guidance, especially for already-implemented changes
A change can be revised after it was built — tasks checked off, `/opsx:apply` already run. The update itself behaves identically (planning artifacts only), but stopping silently would strand the user: the code and the revised plan now disagree. So the skill ends by reporting where the change stands (from the status JSON and the tasks checklist) and recommending the next command — `/opsx:continue` if artifacts are missing, `/opsx:apply` to carry a revised plan into code, `/opsx:archive` when everything is done. Guidance only: the skill never implements, mirroring the "All artifacts created! You can now implement this change with `/opsx:apply`" hand-off that `continue-change.ts` already uses.
### 7. Companion-file correction (#1733)
The original glob-file deferral was unreachable: one matching file marks an artifact `done`, while continue selects only `ready` artifacts. Update can therefore propose a missing companion file within an already populated glob. This corrects the unarchived spec's former blanket deferral without changing the graph's completion rule or starting another artifact.
The exception uses existing status and instructions output, requires current dependency context and user confirmation, and preserves the change-only planning scope. Immediately before creation, it rechecks scope and the concrete path and uses an operation that refuses an existing target. Delegated creators must obey the same limits. No new CLI command, metadata, graph state, or automatic artifact writer is introduced.
## Risks / Trade-offs
- **No deterministic staleness signal.** With no digest/ledger, the skill relies on the agent reading the artifacts to spot incoherence. Trade-off accepted: an agent that rewrites prose must read it anyway, and a content-blind signal earns its cost only for use cases this change excludes (Decision 3).
@@ -19,7 +19,7 @@ The system SHALL provide a `/opsx:update` workflow skill that revises a change's
#### Scenario: Missing artifacts are deferred to continue
- **WHEN** keeping the change coherent would require an artifact that has not been created yet
- **WHEN** keeping the change coherent would require an artifact with no existing output files and status `ready` or `blocked`
- **THEN** the skill revises only the artifacts that currently exist
- **AND** it notes the not-yet-created artifacts and points the user to `/opsx:continue` to create them
@@ -53,7 +53,7 @@ The `/opsx:update` skill SHALL learn which artifacts exist and where they live b
#### Scenario: Resolve artifact paths cross-platform
- **WHEN** the skill reads or writes an artifact on macOS, Linux, or Windows
- **WHEN** the skill reads or revises an existing artifact file on macOS, Linux, or Windows
- **THEN** it uses the `existingOutputPaths` provided by the CLI status output
- **AND** it does not assume forward-slash separators
@@ -63,11 +63,30 @@ The `/opsx:update` skill SHALL learn which artifacts exist and where they live b
- **THEN** the skill edits the concrete files reported in that artifact's `existingOutputPaths`
- **AND** it does not write to `resolvedOutputPath`, which for a glob artifact remains the glob pattern rather than a real file
#### Scenario: A new file under a glob artifact is deferred to continue
#### Scenario: A missing companion file under a populated glob artifact
- **WHEN** keeping the change coherent would require a new file under a glob artifact that does not exist yet (for example a spec for a not-yet-captured capability)
- **THEN** the skill revises only the files already present in `existingOutputPaths`
- **AND** it points the user to `/opsx:continue`/`/opsx:propose` to create the new file rather than inventing a path from the glob
- **WHEN** reconciliation identifies a missing companion file for a glob artifact with non-empty `existingOutputPaths`
- **THEN** the skill MAY propose creating that file using the artifact's instructions, template, project context, rules, and current dependency files
- **AND** it selects an unused concrete path matching the artifact's `outputPath` inside `changeRoot`, including after resolving linked parent directories
- **AND** it creates the file only after user confirmation, refreshing status, instructions, and path checks immediately before creation
- **AND** creation SHALL fail rather than overwrite a file that appeared in the meantime
- **AND** it SHALL NOT start another artifact, write main specs, or edit implementation code
#### Scenario: Required inputs are no longer available
- **WHEN** a populated glob artifact remains `done` but a required non-skipped dependency is missing
- **THEN** the skill SHALL stop new companion creation and ask the user to restore the dependency first
#### Scenario: Schema delegates companion creation
- **WHEN** the artifact instruction delegates creation to another skill or command
- **THEN** the skill SHALL invoke it only if it can honor the confirmed concrete path and the update guardrails
- **AND** otherwise it SHALL stop rather than invoke broader generation
#### Scenario: Intentionally skipped artifact
- **WHEN** status or instructions mark an artifact as skipped
- **THEN** the skill SHALL leave it untouched and SHALL NOT treat its empty outputs as missing or send it to continue
### Requirement: Bidirectional Coherence Review
@@ -109,7 +128,7 @@ After applying confirmed revisions (or finding none needed), the `/opsx:update`
#### Scenario: Next step when artifacts are incomplete
- **WHEN** the update finishes and the change still has not-yet-created artifacts
- **WHEN** the update finishes and the change still has artifacts with no outputs and status `ready` or `blocked`
- **THEN** the skill recommends `/opsx:continue` to create them
#### Scenario: Next step when the change is fully done
@@ -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`)
@@ -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
@@ -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-09-02
@@ -0,0 +1,28 @@
# Release archive claims on Windows
## Why
`openspec archive` creates `.openspec-archive.lock` before moving a change into
the archive. On Windows, a successful archive can leave that lock behind because
the cleanup check compares the device id returned by the open file handle with
the one returned by `fs.lstat()`. Node reports a real device id from the handle
and `0n` from the path stat on the affected Windows/NTFS setup, so the ownership
check never passes.
The archive itself succeeds, but the next archive is blocked by the stale claim
and the user has to delete `.openspec-archive.lock` by hand.
## What Changes
- Treat an inode match plus matching claim contents as sufficient when either
side reports `dev: 0n`, while still requiring the two path stats around the
read to match.
- Keep the existing protection against deleting a claim that was replaced by
another process.
- Add a regression test that simulates the Windows path-stat device id behavior.
## Impact
- Affected spec: `cli-archive`
- Affected code: `src/core/archive.ts`
- Affected tests: `test/core/archive.test.ts`
@@ -0,0 +1,36 @@
## MODIFIED Requirements
### Requirement: Archive Process
The archive operation SHALL follow a structured process to safely move changes to the archive.
#### Scenario: Performing archive
- **WHEN** archiving a change
- **THEN** execute these steps:
1. Create archive/ directory if it doesn't exist
2. Generate target name as `YYYY-MM-DD-[change-name]` using current date, keeping the name as-is when it already starts with a `YYYY-MM-DD-` prefix
3. Claim the target and verify that it does not already exist
4. Prepare and validate spec updates from the active change's delta specs
5. Apply the spec updates as a rollback-capable transaction
6. Move the entire change directory to the archive location
7. If a spec mutation or final move fails before a complete archive is secured, restore the spec transaction and leave or return the change at its active path
8. If a verified fallback copy completes but staged-source cleanup fails, retain the complete archive and committed spec state for recovery instead of risking the only complete copy
#### Scenario: Archive already exists
- **WHEN** target archive already exists
- **THEN** fail with error message
- **AND** do not overwrite existing archive
#### Scenario: Successful archive
- **WHEN** move succeeds
- **THEN** display success message with archived name and list of updated specs
#### Scenario: Successful archive releases its own claim
- **WHEN** an archive run successfully moves a change to its archive destination
- **THEN** remove the temporary archive claim it created
- **AND** do so on supported platforms even when a path stat does not report a device id
- **AND** never remove a claim whose path identity or contents changed before cleanup
@@ -0,0 +1,12 @@
# Tasks
## 1. Release owned claims cross-platform
- [x] 1.1 Compare archive claim files by inode and tolerate a missing device id from either stat result
- [x] 1.2 Preserve the content and repeated-path-stat checks before unlinking
## 2. Verify behavior
- [x] 2.1 Add regression coverage for the Windows `dev: 0n` path-stat case
- [x] 2.2 Run the focused archive regression test
## 3. Record behavior
- [x] 3.1 Add a `cli-archive` spec delta for successful claim cleanup
@@ -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.
+25
View File
@@ -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
+46 -1
View File
@@ -53,6 +53,10 @@ The system SHALL compute a valid topological build order for artifacts.
### Requirement: State Detection
The system SHALL detect artifact completion state by scanning the filesystem.
The system SHALL recognize `generates` values containing `*`, `?`, or `[` as glob patterns. It SHALL also support brace alternatives, brace ranges, and the `@()`, `+()`, `!()`, `*()`, and `?()` extglob forms. An artifact with a glob output SHALL be completed when at least one matching file exists.
The system SHALL preserve literal filenames with a bare leading `!`, plain parentheses, or single-element braces when no supported glob syntax is present. Brace expansion SHALL preserve literal brace groups and recognize later and nested expansion groups. Expanded output paths and traversed symbolic links SHALL remain within the change directory.
#### Scenario: Simple file exists
- **WHEN** an artifact generates "proposal.md" and the file exists
- **THEN** the artifact is marked as completed
@@ -73,6 +77,48 @@ The system SHALL detect artifact completion state by scanning the filesystem.
- **WHEN** the change directory does not exist
- **THEN** all artifacts are marked as not completed (empty state)
#### Scenario: Brace alternatives with matching files
- **WHEN** an artifact generates "review-{api,ui}.md" and "review-api.md" exists
- **THEN** the artifact is marked as completed
#### Scenario: Brace range after a literal brace group
- **WHEN** an artifact generates "report-{draft}-{1..3}.md"
- **AND** "report-{draft}-1.md", "report-{draft}-2.md", "report-{draft}-3.md", and "report-{draft}-4.md" exist
- **THEN** its resolved outputs contain exactly the first three files
- **AND** the artifact is marked as completed
#### Scenario: Later and nested brace alternatives
- **WHEN** an artifact generates "report-{draft}-{{api},ui}.md" and "report-{draft}-{api}.md" exists
- **THEN** the artifact is marked as completed
#### Scenario: Extglob alternatives with matching files
- **WHEN** an artifact generates "@(proposal|design).md" or "+(proposal|design).md" and "proposal.md" exists
- **THEN** the artifact is marked as completed
#### Scenario: Negative extglob excludes its alternatives
- **WHEN** an artifact generates "!(proposal|design).md"
- **AND** "proposal.md", "design.md", and "notes.md" exist
- **THEN** its resolved outputs contain only "notes.md"
- **AND** the artifact is marked as completed
#### Scenario: Brace or extglob pattern without matching files
- **WHEN** an artifact generates "review-{api,ui}.md" or "@(proposal|design).md" and no matching files exist
- **THEN** the artifact is not marked as completed
#### Scenario: Literal output names remain literal
- **WHEN** an artifact generates "!review.md", "(proposal|design).md", or "review-{api}.md"
- **THEN** completion depends on the existence of a file with that exact name
#### Scenario: Brace expansion escapes the change directory
- **WHEN** an artifact generates "{safe,../outside}/review.md"
- **THEN** output resolution rejects the expanded path outside the change directory before matching files
- **AND** rejection does not depend on whether the outside file exists
#### Scenario: Expanded directory pattern reaches an outbound symbolic link
- **WHEN** an artifact generates "content/{safe,linked}/review.md" or "content/@(safe|linked)/review.md"
- **AND** "content/linked" is a symbolic link to a directory outside the change directory
- **THEN** output resolution rejects traversal through that link even when no matching files exist
### Requirement: Ready Artifact Query
The system SHALL identify which artifacts are ready to be created based on dependency completion.
@@ -137,4 +183,3 @@ The system SHALL support self-contained schema directories with co-located templ
#### Scenario: List available schemas
- **WHEN** listing schemas
- **THEN** the system returns schema names from both user and package directories
@@ -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.
+3 -3
View File
@@ -182,9 +182,9 @@ The `openspec config profile` command SHALL provide an action-first interactive
- **WHEN** user runs `openspec config profile` interactively
- **THEN** the first prompt SHALL offer:
- `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
+6
View File
@@ -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.
+1 -1
View File
@@ -117,7 +117,7 @@ The update command SHALL refresh existing slash command files for configured too
- **AND** skip creating missing files (the update command only refreshes what already exists)
#### Scenario: Updating slash commands for Kilo Code
- **WHEN** `.kilocode/workflows/` contains `openspec-proposal.md`, `openspec-apply.md`, and `openspec-archive.md`
- **WHEN** `.kilo/command/` contains OpenSpec-managed `opsx-*.md` command files for the configured profile
- **THEN** refresh each file using shared templates wrapped in OpenSpec markers
- **AND** ensure templates include instructions for the relevant workflow stage
- **AND** skip creating missing files (the update command only refreshes what already exists)
+29 -1
View File
@@ -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
+7 -1
View File
@@ -32,6 +32,13 @@ The system SHALL detect legacy OpenSpec artifacts from previous init versions.
- `.windsurf/workflows/openspec-*.md`
- And equivalent directories for all tools in the legacy SlashCommandRegistry
#### Scenario: Detecting legacy Kilo Code workflows
- **WHEN** `.kilocode/workflows/` contains OpenSpec-managed `opsx-*.md` or `openspec-*.md` workflow files
- **THEN** `openspec init` or legacy cleanup SHALL remove those files
- **AND** Kilo Code commands SHALL be generated under `.kilo/command/`
- **AND** `openspec update` SHALL NOT refresh files that remain only under `.kilocode/workflows/`
#### Scenario: Detecting legacy OpenSpec structure files
- **WHEN** running `openspec init` on an existing project
@@ -160,4 +167,3 @@ The system SHALL report what was cleaned up.
- **WHEN** no legacy artifacts are found
- **THEN** the system SHALL NOT display the cleanup section
- **AND** proceed directly with skill setup
+2 -2
View File
@@ -99,8 +99,8 @@ Requirement headers SHALL serve as unique identifiers for programmatic matching
- **WHEN** processing delta changes
- **THEN** use the `### Requirement: [Name]` header as the unique identifier
- **AND** 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

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