Compare commits

...
108 Commits
Author SHA1 Message Date
openspec-release-bot[bot]andgithub-actions[bot] 4e16790d90 Version Packages (#1380)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-07-29 01:25:23 +00:00
Clay GoodandClaude Opus 5 87312900f5 fix(telemetry): send the usage event directly instead of via posthog-node (#1476)
* fix(telemetry): send the usage event directly instead of via posthog-node

Installing OpenSpec shipped posthog-node's transitive tree
(@posthog/core, @posthog/types) to every consumer. Those packages
release several times a day, so any freshly resolved install tripped
supply-chain age policies — pnpm's minimumReleaseAge failed with
ERR_PNPM_MINIMUM_RELEASE_AGE_VIOLATION on entries younger than the
policy window (#1390). No pinning fixes this: exact-pinning posthog-node
leaves its own ranges floating, npm overrides only apply at a consumer's
root, pnpm ignores a dependency's npm-shrinkwrap, and bundledDependencies
under a pnpm-managed node_modules packs the virtual-store layout and
breaks module resolution (verified: the bundled CLI crashes on import).

The SDK's only remaining job here was the wire format: the client was
already configured to send one event immediately, time-bounded, with no
retries, through an injected fetch that never throws. Post the same
capture payload to the same /batch/ endpoint with that fetch directly.
Same event name, properties, distinct id, and opt-out guards; shutdown
still flushes in-flight events, each bounded by the request timeout.

Verified end to end: the packed tarball contains zero posthog files, a
pnpm consumer with minimumReleaseAge: 1440 installs cleanly with zero
posthog lockfile entries, and the live endpoint answers 200 OK to the
new payload. Regression tests pin the manifest and src free of posthog.

Fixes #1390

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

* build(nix): update the pnpm deps hash for the posthog-node removal

Value taken from the CI mismatch report.

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

* fix(telemetry): dispose the response body so no socket outlives shutdown

undici keeps the connection occupied until the response body is consumed
or canceled, and telemetry never reads it — on both the success and
non-2xx paths the socket could linger after shutdown() returned. Cancel
the body before the tracked promise resolves, with coverage for both
paths (bodyUsed asserted after shutdown), and the live endpoint
re-verified with disposal in place.

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

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-29 01:06:32 +00:00
Clay GoodandClaude Opus 5 17af60c66e fix(archive): make the scenario-drift check fence-aware, plus release-audit follow-ups (#1475)
* fix(archive): make the scenario-drift check fence-aware

parseScenarioBlocks matched #### Scenario: headers on raw lines while the
validator's countScenarios masks fenced code blocks (#1151). The drift
check (#1391) inherited the raw scan, so a fenced scenario example in the
current spec aborted an archive that validate had passed, and a fenced
name in the MODIFIED block counted as keeping a scenario the block had
actually dropped. Build the shared code-fence mask and skip masked lines
in both the header scan and the block-end scan.

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

* fix(update): tear down the redirected request when the budget expires

The overall request budget was armed inside the first send() and its
callback closed over that hop's request. After a redirect the timer
destroyed the already-dead first request, so a redirect target that
trickled bytes kept resetting its idle timeout and held the socket open
until the body-size cap. Track the in-flight request and have the budget
timer destroy whichever one is open.

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

* chore(release): add changesets for user-facing changes missing from the 1.7.0 notes

18 feat/fix commits merged since v1.6.0 without a changeset, so the
pending Version Packages PR would have released them silently: five tool
integrations (ZCode, Hermes, CodeArts, Kimi Code rename, Codex
skills-only), skills.sh distribution, symlinked schema dirs, nested spec
discovery, drift multiplicity, checkbox markers, Windows welcome input,
npx avoidance, doctor store drift, local dates, missing-core-workflows
warning, store-aware main specs, open-questions guidance, and spec
content guidance. Plus changesets for this branch's two fixes.

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

* fix(adapters): escape TOML-active characters in Gemini command files

The gemini adapter interpolated the description into a TOML basic string
and the body into a multiline basic string with no escaping. Every
current template value happens to be safe; the first description with a
double quote or backslash would silently produce invalid TOML for all
Gemini command files. Escape both contexts (#1447 fixed the same class
for the YAML adapters but scoped itself to YAML).

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

* fix(update): harden install detection and redirect handling

Three follow-ups from the release audit:
- A path segment literally named volta (a user or project directory)
  classified the install as volta-managed and swallowed the upgrade
  offer. The undotted spelling now requires volta's own tools/image
  layout, matching how pnpm and yarn already demand corroboration.
- The Windows npm-ownership fallback checked that the npm prefix exists,
  which is true of any X\node_modules\pkg tree, hand-copied ones
  included. Corroborate with the openspec.cmd shim npm actually writes.
- A https registry redirecting to plain http was followed; a MITM on
  that reply controls the newer-version answer. Refuse the downgrade.

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

* chore(cli): export zcodeAdapter from the barrel and sync a completion description

zcode was registered but missing from the adapters barrel (its test
imported the module directly), and the completion registry still carried
the pre-#1062 description for the instructions command.

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

* fix(parser): strip a UTF-8 BOM before parsing specs and deltas

A BOM-prefixed delta spec (Windows editors, PowerShell Out-File) failed
validate and archive with 'No delta sections found' because the first
line never matched '## ADDED Requirements'. Strip the BOM in both
normalizers, the same way tool detection already does.

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

* fix(cli): reject over-long change names with a validation message

A 300-character change name surfaced two raw ENAMETOOLONG errno dumps
from stat and mkdir. Bound the name at 200 characters in
validateChangeName so the failure is a normal validation error.

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

* fix(archive): finish the early-sync no-op rules for MODIFIED and RENAMED

Two asymmetries left over from the #1376/#1386/#1437 no-op work:

- MODIFIED counted every delta as applied even when the block was
  byte-equal to the main spec, so a fully early-synced change rewrote
  the file (normalization churn), printed '~ N modified', and reported
  specsUpdated: true where its ADDED/REMOVED/RENAMED twins print 'Specs
  already in sync; no files changed.' Count only real replacements.
- RENAMED's already-synced skip (source gone, target present) had no
  near-miss guard: a case/whitespace variant of the source still in the
  spec means a typo'd header, and REMOVED already hard-aborts on that
  signal. Apply the same guard, excluding the target itself so a
  case-only rename still no-ops.

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

* fix(validate): stop reporting an unreadable specs dir as 'no deltas'

The delta-validation loop swallowed every error as 'if no specs dir,
treat as no deltas', so an EACCES capability folder produced the
misleading 'Change must have at least one delta' while archive let the
same error propagate. Tolerate only ENOENT and ENOTDIR (a stray specs
file); anything else stays loud, matching discoverSpecFiles' documented
fail-loud contract.

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

* fix(update): say when commands-only delivery leaves a tool with nothing

Under delivery: commands, update removed the skills of adapterless
skills-only tools (Hermes, Kimi Code, Vibe, CodeArts, ForgeCode) without
a word — leaving zero OpenSpec artifacts while the tool's detection dir
kept re-suggesting an init that would also generate nothing. Print the
same per-tool configuration correction init already prints, pointing at
'openspec config set delivery both'.

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

* fix(completion): honor $ZSH and $ZSH_CUSTOM for Oh My Zsh installs

The installer used a set $ZSH only as an is-installed signal and then
wrote to ~/.oh-my-zsh regardless, so a custom OMZ location got a
freshly created ~/.oh-my-zsh tree that no shell ever loads — and
isInstalled/uninstall looked in the same wrong place. Route every path
through the $ZSH/$ZSH_CUSTOM-aware helpers.

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

* fix(init): make the static welcome screen wait for the Enter it asks for

The static branch printed 'Press Enter to select tools...' and returned
immediately, so the Enter landed in the tool picker and submitted the
pre-selected set sight-unseen. #1462 routed reduced-motion,
OPENSPEC_NO_ANIMATION, --no-animation, NO_COLOR, and narrow-terminal
users onto this path. Wait in a TTY; drop the prompt line when there is
no TTY to wait on.

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

* fix(feedback): keep the manual fallback on every gh failure

Only missing-gh and unauthenticated flows showed the formatted feedback
and pre-filled submission URL; issues-disabled, network, or rate-limit
failures printed gh's stderr and discarded the path to submit what the
user had already typed. Route those through the same manual fallback,
preserving gh's exit code.

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

* test(update): model the npm shim in the Windows prefix fixture

The ownership corroboration now checks for the openspec.cmd shim npm
writes beside node_modules; the Homebrew-prefix fixture built the layout
without it, so the test failed on windows-pwsh. Write the shim in the
fixture and pin the inverse: the same shape with nothing npm wrote (a
hand-copied portable tree) is not an npm install.

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

* fix(update): require volta's full tools/image layout for the undotted spelling

The corroboration used has('tools', 'image'), which is some() — volta
AND (tools OR image) — so /srv/volta/tools/apps/... still classified as
a Volta install and swallowed the upgrade offer. Require both segments,
matching the real %LOCALAPPDATA%\Volta\tools\image layout.

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

* fix(adapters): escape control characters in Gemini multiline prompts

escapeTomlMultilineBasicString handled backslashes and quote-triples but
not the C0 controls that are as invalid in a multiline basic string as
in a single-line one. Reuse TOML_CONTROL_CHARS, applied last so the
escapes it introduces are not re-doubled.

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

* fix(completion): finish the $ZSH_CUSTOM support and isolate it in tests

The fpath verification advice still grepped the literal
custom/completions, which a relocated $ZSH_CUSTOM need never contain —
grep the actual directory instead. The installer tests cleared only
$ZSH, so on a machine exporting $ZSH_CUSTOM they would have written
into (and deleted from) the developer's real OMZ custom dir — the same
leakage class #1400 fixed for $ZSH. Clear/restore both, and pin the
custom-location paths with two new tests.

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

* chore(release): correct the hermes and zcode changeset wording

Hermes is skills-only (no command adapter), and zcode's namespaced
commands register /opsx:<id>, not /opsx-* — the release notes must not
reintroduce the invocation-spelling confusion #1471 removed.

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

* test(feedback): pin the manual fallback on a non-label gh failure

The new reportGhFailure output (formatted feedback + pre-filled URL) had
no coverage; the network-failure test now asserts it.

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

* fix(completion): match fpath entries as literal strings in the OMZ guidance

The verification advice interpolated the completions dir into
grep "<dir>" where regex metacharacters make the check unreliable and
quotes could break the displayed command. Print one fpath entry per
line and match with grep -F on a shell-quoted literal.

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

* fix(adapters): never emit a bare carriage return in Gemini TOML prompts

A lone CR is illegal in a multiline basic string — Python 3.13 tomllib
rejects the file — and the control-char pass deliberately skipped it on
the assumption it only appears as CRLF. Normalize CRLF to LF and escape
any remaining CR as \r. The escaping guarantee is now parser-backed:
smol-toml (new devDependency) round-trips every hostile body in the
regression matrix (lone CR, CRLF, CR before a quote run, NUL/VT/FF,
trailing backslash, four- and five-quote runs), and the same outputs
were verified against Python tomllib.

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

* build(nix): update the pnpm deps hash for the smol-toml devDependency

The lockfile changed, so the fixed-output derivation hash moved; value
taken from the CI mismatch report.

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

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 23:57:41 +00:00
1637856c42 feat(adapters): follow the Windsurf rename to Devin Desktop (#1167)
* proposal: add devin desktop support

* feat(adapters): add devin desktop command adapter

- Create new Devin Desktop adapter for .devin/workflows/opsx-<id>.md
- Register adapter in CommandAdapterRegistry
- Export adapter from adapters index
- Update docs/supported-tools.md with Devin Desktop entry
- Add 'devin' to available tool IDs list

Devin Desktop uses the same Cascade workflow system as Windsurf,
making it a natural migration path for existing users.

* fix(config): add devin desktop to AI_TOOLS

Add Devin Desktop entry to AI_TOOLS configuration so that:
- getToolsWithSkillsDir() includes 'devin' as a valid tool ID
- getWorkspaceSkillToolIds() returns 'devin' in the list
- parseWorkspaceSkillToolsValue() accepts 'devin' as valid input
- openspec init --tools devin works correctly

This fixes validation failures where 'devin' was documented in
docs/supported-tools.md but not recognized by validation functions
that derive valid IDs from AI_TOOLS.

* fix(devin-adapter): escape implicit YAML scalars in frontmatter

Update escapeYamlValue to detect and quote implicit YAML scalars that
would be coerced by parsers:
- Booleans: true, false, yes, no, on, off
- Null variants: null, ~
- Numbers: integers, floats, exponentials, hex (0x), octal (0o)
- Edge cases: standalone dash (-) and dot (.)

This ensures values like 'true', '123', 'null' remain strings in YAML
frontmatter instead of being interpreted as booleans, numbers, or nulls.

Preserves existing escaping logic for special characters and newlines.

* test(devin-adapter): add comprehensive tests for Devin Desktop adapter

Add test coverage for the Devin Desktop adapter including:
- Command reference transformation from colon to hyphen syntax
- YAML frontmatter escaping for special characters and implicit scalars
- File path generation for workflows
- Integration with available tools detection
- Init and update command workflows

* Add cross-platform testcase.

* fix(devin): refresh deltas against canonical specs and point skills at skills

Addresses the two release blockers on this PR.

Archive: the change's MODIFIED blocks were written against an older
canonical `cli-init`, so `openspec archive add-devin-desktop-support`
aborted rather than merging. The deltas are regenerated from the current
canonical specs (cli-init `Skill Generation` + `Slash Command
Generation`, cli-update `Slash Command Updates`, and a new
`ai-tool-paths` delta for the `.devin` skillsDir), each restating every
existing scenario so archive is purely additive.

Invocation syntax: only Devin Desktop reads `.devin/workflows/`, so a
`/opsx-*` workflow reference is dead text on Devin Local, which supports
skills only. Devin now takes the skill-reference transformer, so skill
bodies and the getting-started hint say `/openspec-*`. Workflow bodies
keep hyphen references, applied by devinAdapter itself.

The adapter also drops its private copy of escapeYamlValue /
formatTagsArray in favor of the shared helpers main centralized in
#1447, which quote unconditionally and escape control characters.

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

* fix(devin): correct commands-only hint, fill doc gaps, cover both surfaces

Follow-up from adversarial review of the previous commit.

The devin special case in getTransformerForTool was unconditional, so
under commands-only delivery — where `.devin/skills/` is deleted — the
getting-started hint named `/openspec-propose`, a skill that is not on
disk. Devin now takes the skill transformer only when skills are
generated, and the hyphen form otherwise. The cli-init delta records the
fallback, and a unit test pins all three delivery modes.

Docs: `devin` was missing from the `--tools` list in docs/cli.md (which
mirrors the list supported-tools.md already had) and from the
command-syntax tables in docs/commands.md and docs/how-commands-work.md.
The supported-tools row gains a footnote citing Cognition's docs for the
`.windsurf/` -> `.devin/` move and the Devin Local workflow gap.

Tests: init and update now assert both surfaces — workflows carry
`/opsx-*`, skills carry `/openspec-*`, neither carries `/opsx:` — and
update checks the seeded skill was actually refreshed. Adds the negative
detection case. Drops three devin-only YAML assertions that duplicated,
less rigorously, the registry-derived escaping matrix that now enrolls
devin automatically.

Also reverts an unrelated zcode export and lingma reorder that a merge
resolution had pulled into adapters/index.ts. zcodeAdapter is registered
but missing from that barrel on main; that is a pre-existing gap and
belongs in its own change.

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

* fix(devin): name the right command in the profile migration notice

The profile-migration notice printed by both `init` and `update` hardcoded
`/opsx:propose` for every adapter-backed tool. Devin registers no such
command on any surface — its workflows answer to `/opsx-propose` and its
skills to `/openspec-propose` — so an upgrading Devin user was told to run
something that does not exist:

  Migrated: custom profile with 6 workflows
  New in this version: /opsx:propose.

The reference now goes through getTransformerForTool, the same call
init.ts already makes for the getting-started hint. Devin prints
`/openspec-propose`; opencode and the other filename-invoked tools are
corrected to `/opsx-propose` as a side effect; claude is unchanged.

Also corrects two inherited false claims in the cli-update delta — Devin
workflows carry no OpenSpec markers, and update writes every profile
workflow rather than only refreshing files that already exist, which the
PR's own test demonstrates. Qualifies the supported-tools footnote for
commands-only delivery, and strips trailing whitespace.

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

* fix(devin): keep the cli-update delta in step with the canonical spec

The delta restates the whole 'Slash Command Updates' requirement, and its
copy of the OpenCode scenario predated #1471 — archiving it would have
quietly reverted the spec to calling the hyphen rewrite an OpenCode special
case, the hand-maintained framing #1471 removed. Archive on a scratch copy
is now purely additive.

Also point tasks.md at the generator rather than the deleted
transformToHyphenCommands, and enroll devin in the pure-formatter tripwire —
it is the one adapter whose private body transform was just removed, so it
is the likeliest to have it re-added.

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

* feat(adapters): follow the Windsurf rename to Devin Desktop, with migration

Windsurf was rebranded to Devin Desktop on 2026-06-02 and its config
directory moved: `.devin/` is the preferred read+write location, `.windsurf/`
a legacy read-only fallback. Devin Local does not read `.windsurf/` at all,
so an existing Windsurf user's OpenSpec files are invisible to it.

Carrying `devin` as a second tool id alongside `windsurf` would list one
product twice and leave upgraders with two parallel installs — `openspec
update` even told them to create the second one ("Detected new tool: Devin
Desktop"). This follows the rename instead, as the repo already did for
Kimi CLI -> Kimi Code:

- `windsurf` is retired as a tool id; `devin` takes its place, with
  `detectionPaths: ['.devin', '.windsurf']` so pre-rebrand projects are
  still recognized. The Windsurf adapter is replaced, not duplicated.
- `TOOL_ID_ALIASES` keeps `--tools windsurf` resolving, so existing setup
  scripts and CI keep working; they now configure `.devin/`.
- OpenSpec-managed skills (`openspec-*`) and command files (`opsx-*`) under
  `.windsurf/` move to `.devin/`. The kimi migration handled skills only;
  command files now move too, deriving the legacy path from the adapter's
  own getFilePath rather than hard-coding a layout.
- The move is offered, not taken: nothing on disk distinguishes a user who
  took the rebrand from one still on a pre-rebrand Windsurf build that reads
  only `.windsurf/`. `openspec update` explains the rename and asks; --force
  and non-interactive runs migrate; declining leaves every file untouched and
  says what that costs. Files the user wrote are never moved.

Also gives Devin its own row in the authoritative invocation table — the
catch-all row claimed `/opsx-<id>` for both agents, which is wrong for Devin
Local.

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

* fix(devin): stop the migration from deleting anything it does not own

An adversarial pass found two ways the move destroyed files.

Symlinked roots wiped the install. `ln -s .devin .windsurf` is a realistic
way to straddle the rebrand, and it makes source and destination the same
file — so the "destination exists, drop the legacy copy" branch deleted the
only copy. Twelve generated files, gone, and not regenerated: the wipe
happens before tool detection, so update then reported no configured tools.
Both roots are now realpath'd and a self-move is skipped.

User content inside an OpenSpec-managed path was deleted. The same branch
rm -rf'd the whole legacy skill directory, taking a hand-written
reference.md beside SKILL.md with it, and deleted a legacy command file even
when the user had edited it. Now only SKILL.md is removed from a skill
directory, and a command file is removed only when byte-identical to the
one that survives — an edit is left where it is.

Also: declining the move stranded the user. `update` then printed "No
configured tools found. Run openspec init", which is wrong — the project is
configured, just in the directory OpenSpec no longer writes. It now says so
and how to resume. A closed stdin during the prompt aborted the whole
update; it is treated as a decline.

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

* docs(devin): add a changeset for the Windsurf rename and migration

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

* fix(devin): move only SKILL.md, never the skill directory around it

alfred caught a data-loss path the earlier fix missed. When the destination
did not yet exist, migration renamed the whole legacy skill directory into
`.devin/` — carrying any file the user kept beside `SKILL.md` with it. That
destination is a directory OpenSpec owns and removes on its own: under
commands-only delivery, or for a workflow outside the active profile. So the
move handed the user's file to a later rm and it vanished.

Reproduced on `d94af8b`: with `delivery: commands`, a `reference.md` beside a
legacy `SKILL.md` was gone after `openspec update`.

Only `SKILL.md` crosses now, in both branches; anything else stays under the
legacy root, and the legacy directory is still removed when the move leaves
it empty. Regression tests cover the commands-only and deselected-workflow
cases and both fail against the previous code.

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

* fix(devin): treat an edited skill the way an edited command is already treated

A final adversarial pass found the two paths disagreeing. When both roots
held the same file with different content, the command path compared bytes
and kept the user's version; the skill path deleted it with no comparison —
so one `openspec update` destroyed an edited SKILL.md while preserving an
edited opsx-*.md in the same project.

Both now share one `classifyManagedFile` rule: move when the destination is
empty, drop the legacy copy only when byte-identical, otherwise leave it.
Anything left behind is reported, so a user who customized a file knows two
copies exist rather than discovering it later.

Note on the other finding from that pass: OpenSpec regenerating or pruning
the files it owns is long-standing behavior, not something this PR
introduces. Verified against main — an edited SKILL.md under a deselected
workflow, and an edited selected skill and command, are all destroyed by
`openspec update` on 9a937cb too. No regression, so left alone here.

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

* fix(devin): report divergent legacy files even when nothing is movable

collectLegacyToolMigrations only returned a result when something moved, so
a project where EVERY legacy file differs from its counterpart produced no
output at all — two divergent copies and not a word about them. That is the
one case where the report matters most, since it is entirely made of files
the migration deliberately refused to touch.

Kept-only results are retained now. Callers gate on hasMovableContent(), so
a kept-only result reports what was left without offering to move nothing
and without claiming a migration that did not happen.

Also reworded the notice. A legacy file can differ because the user edited
it or simply because an older OpenSpec generated it, so it no longer asserts
an edit — it states that nothing was overwritten and leaves the user to
compare the two copies.

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

* test(devin): stop matching the unrelated profile-migration line

The kept-only regression asserted no line matched /Migrated\s*:/, which also
matches OpenSpec's profile migration message, "Migrated: custom profile with
N workflows". That line only prints when the global config has no profile
yet — true on a fresh CI runner, false on a developer machine that has run
OpenSpec before — so the test passed locally and failed on all three CI
platforms.

Now matched on the directory arrow, ".windsurf → .devin", which is specific
to a migration report and unaffected by config state.

Reproduced both ways with an empty XDG_CONFIG_HOME: the old assertion fails
there, the new one passes, and the full suite is green under CI's
XDG_CONFIG_HOME + VITEST_MAX_WORKERS=4.

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

---------

Co-authored-by: Clay Good <hi@claygood.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 21:02:23 +00:00
Clay GoodandClaude Opus 5 9a937cb9b3 fix(adapters): reference slash commands by the names each tool registers (#1471)
* fix(adapters): reference slash commands by the names each tool registers

Generated command bodies, skills and the post-setup hints all advertised
/opsx:<id>, but only 7 of 28 adapter-backed tools register that name. The
other 21 write .../opsx-<id>.md, where the filename is the command, so
their users were told to type a command their palette never had. Codex,
which registers no slash commands at all, was told to type them too.

The invocation style is now derived from the command file each adapter
writes rather than a hand-maintained tool list, so every tool-specific
surface - command bodies, SKILL.md cross-references, and the init,
update and migration hints - names the command that tool answers to.

Closes #1307
Closes #727
Closes #1379
Closes #1110
Refs #1129

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

* docs: name the per-tool invocation exceptions in the table itself

Review follow-up: the "every other adapter-backed tool" row swept Amazon Q,
Cline and Kilo Code into the plain /opsx-<id> form. Each is now its own row
with the wrapper it actually uses, and the command-references tests pass the
now-required invocation style explicitly.

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

* docs: make every invocation reference match what OpenSpec generates

Review follow-up across the docs, the living specs and two hardcoded
strings:

- supported-tools: the How To Invoke section no longer splits the "How It
  Works" profile paragraph from its heading, keys rows on the file shape
  rather than a `.md` extension the Gemini/Continue/Copilot/Kiro adapters
  do not use, and drops the Cline/Kilo Code/Amazon Q rows. Kilo Code's
  docs say the current format drops the `.md` suffix, and the Cline and
  Amazon Q forms could not be confirmed - a wrong exception row is worse
  than none, so the caveat now describes the shape without asserting a
  spelling OpenSpec does not generate.
- commands, how-commands-work: the two partial nine-row tables that drifted
  into #727/#1307 now key on the same file shape and defer to the
  authoritative table; both note that skill rows carry skill names, which
  are not command ids.
- faq, troubleshooting, installation, README: stop telling skills-only
  users they have no slash command, stop offering "/opsx autocompletes" as
  a health check on tools where it never will, and name Hermes with the
  other adapterless tools.
- specs: cli-init no longer claims every tool gets `commands/opsx/`,
  cli-update no longer frames the hyphen rewrite as OpenCode-specific, and
  command-generation describes the classifier the code implements.
- the legacy-cleanup summary and the pre-selection welcome banner no longer
  print `/opsx:*` at users whose tool never registers it.
- adds the missing changeset; it supersedes the Codex sentence in the
  pending adapterless-skill-references note.

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

* test: cover the update and migration paths a mutation run found unguarded

Mutation testing showed five ways to delete parts of this change without
failing a single test. All five now fail:

- `openspec update` had no flat-tool coverage at all, so the headline
  upgrade path - an existing Cursor project still carrying `/opsx:`
  references - was asserted nowhere. Two tests now cover it: one heals a
  project seeded with stale references, one runs claude+qwen together and
  pins each to its own form.
- the legacy-upgrade getting-started menu is covered for a newly
  configured Cursor project, so passing the wrong invocation style there
  is caught.
- migration.ts had no flat-tool case: reverting it to a hard-coded
  `/opsx:propose` passed the whole suite. A qwen-only migration and a
  claude+qwen disagreement now pin the message.
- the unknown-command-id guard in `transformToHyphenCommands` was new
  behaviour with no test; removing it was invisible.

Also tightened assertions the same run showed were weak: the
`resolveCommandInvocationStyle` loop compared the implementation against
itself, the per-id consistency check asserted only that a style was
uniform rather than which one, and the init test's `/opsx-` assertion was
satisfied by frontmatter rather than a body reference. The adapter tests
that moved to `generateCommand` are renamed after their real subject, and
a new case pins the contract those five adapters now rely on: they stay
pure formatters and do not rewrite the body themselves.

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

* test: assert the rewritten form, not just the absence of the old one

Review follow-up: the refreshed-skill checks were negative-only, so a
regression that dropped every command reference rather than rewriting it
would have passed. Each now pins the invocation its tool registers, the
stale fixture asserts it really seeded a colon reference into the skill,
and the claude+qwen case pins Claude's namespaced skill alongside Qwen's.

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

* fix(adapters): spell Amazon Q's prompts with @, not as slash commands

The invocation model derived the whole command name from the file an
adapter writes, which covers `/opsx:<id>` versus `/opsx-<id>` but not the
wrapper around it. Amazon Q loads `.amazonq/prompts/opsx-<id>.md` into its
prompt library, invoked as `@opsx-propose`; it registers no slash command,
so its command bodies, skills, and the "Getting started" hint all named
something the tool never answers to.

The name still comes from the file path. The prefix is now adapter
metadata (`invocationPrefix`, defaulting to `/`), so it cannot be guessed
wrong and a new adapter has to declare it deliberately — invocation.test.ts
fails if one appears undeclared.

Also fixes three copy issues:
- The FAQ told users to run `openspec update` when command files are
  missing; update only refreshes files for already-configured tools, so a
  tool that was never initialized needs `openspec init`.
- The installation prompt omitted Kimi Code's `/skill:openspec-propose`.
- The welcome screen promised "opsx slash commands" before tool selection,
  which is wrong for skills-only tools that correctly get no command files.

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

* fix(init): stop naming slash commands where none are registered

Two spots still promised a slash command to users who get none:

- The welcome screen's quick start shows canonical names (/opsx:propose),
  but renders one prompt before tools are picked — an Amazon Q user types
  @opsx-propose and a Codex user $openspec-propose. It now says the
  spelling varies by tool, so the canonical form stops reading as the
  literal thing to type. "Getting started" still prints the real form.
- The post-setup restart line said "slash commands to take effect"
  whenever commands were generated. Amazon Q's generated files are prompt
  library entries, not slash commands, so it now says "the new commands".

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

* docs: name Amazon Q's @ form where the other exceptions are listed

The README's one-line exception list and the troubleshooting checklist
both enumerated the per-tool spellings and skipped Amazon Q. The
troubleshooting entry was actively misleading: it explains that /opsx
never autocompletes for tools without command files, and Amazon Q is not
one of those — it has command files, they just land in the prompt library.

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

* test(migration): cover the legacy-upgrade hint for amazon-q

The migration hint resolves its propose reference through the same
transformer as init and update, but no case exercised a non-slash prefix
there. The second test is the one that matters: @opsx-propose and
/opsx-propose are both "flat", so a style-only model would treat Amazon Q
and Qwen as agreeing and advertise one form to both. Reverting the prefix
to a constant '/' fails 5 tests, so neither assertion is a tautology.

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

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 17:09:36 +00:00
10fa39b1c3 fix(update): refresh command files for tools configured without skills (#1442)
* fix(update): mark command-configured tools as needing update when skill version is missing

* fix(update): compare command content fingerprint for commands-only tools when skill version is missing

* test(update): isolate config homes in regressions

* fix(update): keep skill drift detectable behind the command fingerprint

Review follow-ups on the commands-only update fix:

- Only fall back to the command-content fingerprint when a tool has no skill
  files at all. Gating on `generatedByVersion === null` also swallowed the case
  where a SKILL.md exists but its version is unreadable, so a truncated or
  hand-edited skill file could never be repaired by `openspec update` again.
- Drop the command `generatedBy` scan: command adapters emit no version stamp,
  so the loop was unreachable and made the fingerprint fallback read as a
  secondary path rather than the only one.
- Compute version status with the same workflow set the generation loop writes
  (`legacyWorkflowOverrides[toolId] ?? desiredWorkflows`), so a legacy-upgraded
  tool is not fingerprinted against commands it was never given.
- Remove the unread `delivery` option from the three tool-detection signatures,
  the leftover `getCommandConfiguredTools` / `COMMAND_IDS` imports, and the
  unused `toolHasAnyConfiguredCommand` re-export.
- runCLI: never let temp-dir cleanup replace the CLI result or a real failure,
  and treat an explicitly-empty XDG_CONFIG_HOME as an override.

Adds regressions for the unreadable-skill case and for a deselected workflow
leaving a command file behind, and documents how "up to date" is decided.

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

* fix(update): ignore CRLF and BOM when fingerprinting command files

Round-two review follow-ups:

- Command files are committed project files. A Windows clone with
  `core.autocrlf` re-materializes them with CRLF endings, which the byte-exact
  comparison read as drift: every fresh checkout spent one `openspec update`
  rewriting identical content and announcing a bogus "unknown → <version>".
  Normalize CRLF and a leading BOM on both sides before comparing.
- Collapse `getCommandConfiguredTools`, which the widened `getConfiguredTools`
  made a strict subset of itself, into the single remaining caller.
- Correct the new `openspec update` doc paragraph: content drift is only
  detected for commands-only installs, so it must not promise that hand edits
  are always overwritten.
- Add the changeset this repo requires per fix.

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

* chore(update): drop dead exports and correct stale status doc comments

`ToolVersionStatus.configured` and `.generatedByVersion` are now fed by command
files too, so their comments no longer say "skills". Removes the barrel exports
and the `options` parameter this change added but nothing consumes, and the
import left dangling by the previous commit.

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

* test(update): make the CRLF fixture idempotent on a CRLF checkout

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

* test(update): cover non-claude adapters and a custom profile

The fingerprint regressions all ran against claude and the core profile, so two
things were covered by reasoning rather than by an executed test:

- Command paths differ in shape per adapter. Added a parametrized case over
  gemini (nested dir, TOML), cursor (flat opsx-* file), and cline, whose
  commands live in .clinerules/workflows — not in its skillsDir (.cline) at
  all, so a commands-only install leaves that directory absent. Each asserts
  detection, a clean fingerprint, and drift. Reverting the getConfiguredTools
  widening fails all three.
- A custom profile must be fingerprinted against its own workflow subset.
  The new case inits with ['explore', 'apply'] and asserts the same tree reads
  as drifted when compared against the wider core set. Making the fingerprint
  ignore the caller's workflows and fall back to global config fails it.

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

---------

Co-authored-by: Clay Good <hi@claygood.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 17:09:32 +00:00
Clay GoodandClaude Opus 5 6295515d4d feat(update): offer to upgrade a stale CLI during openspec update (#1470)
* fix(update): flag a stale global CLI during openspec update

Instruction files are generated by the installed CLI, so running
`openspec update` against an outdated global install printed
"All 1 tool(s) up to date (v1.6.0)" while the workflows newer releases
ship were never written. Users read that as success and reported the
missing workflows as bugs.

`openspec update` now checks the npm registry alongside the update and,
when the installed CLI is behind, prints the upgrade command instead of
leaving the up-to-date line to speak for itself.

The check never gets in the way: it runs concurrently with the update,
times out after 1.5s, caches the answer for 24h, returns null on any
failure, and is skipped in CI, under tests, and whenever
OPENSPEC_NO_UPDATE_CHECK is set.

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

* fix(update): name the right install in the stale-CLI hint

The hint assumed a global install. A project-local dependency is now
pointed at that dependency instead of `npm install -g`, and every hint
prints the directory the running CLI was loaded from, so anyone who
upgraded but still runs an old pnpm/volta/npx shim can see which copy
answered.

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

* fix(update): drop the temp-file cache and fix prerelease ordering

CodeQL flagged the version-check cache twice: a predictable path in the
shared OS temp dir (js/insecure-temporary-file, high) and registry data
written to that file (js/http-to-file-access, medium). `openspec update`
is a rare, human-run command, so the cache bought little — removing it
resolves both alerts outright and deletes the code that needed them.

Also from review: CI=1 now opts out alongside CI=true, and prerelease
tags compare per SemVer (dot-separated identifiers, numeric compared
numerically) so 1.7.0-beta.10 outranks 1.7.0-beta.2. Build metadata is
ignored per spec.

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

* fix(update): make the version check actually reach the registry

Adversarial review found the check could never fire: the request sent
`accept: application/vnd.npm.install-v1+json`, which npm serves only on
the full packument — on `/<pkg>/latest` it answers 406, so every real run
returned null. Every test mocked fetch, so nothing caught it. The header
is gone, and a new suite exercises the real fetch path against a local
HTTP server, including an assertion that we never send that Accept type.

Also from review:

- Validate the published version against a strict SemVer pattern before
  printing it. It lands in the terminal beside an install command, so an
  unvalidated string could smuggle ANSI cursor controls and repaint the
  surrounding lines.
- Honor DO_NOT_TRACK=1 and OPENSPEC_TELEMETRY=0, the opt-outs telemetry
  already respects, and update SECURITY.md, which promised telemetry was
  the only network egress.
- Anchor project-local detection on the path being updated and its
  ancestors instead of process.cwd(), so `openspec update <path>` and
  workspace sub-packages with a hoisted root node_modules are no longer
  told to install globally. It can no longer throw when the working
  directory has been deleted.
- Send npx/dlx users `npx @fission-ai/openspec@latest update` rather than
  advice that would create the global install they avoided.
- Query npm_config_registry when set, so private mirrors get an answer
  their own install command can deliver.

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

* test(update): make version-check fixtures portable on Windows

The new fixtures mixed unresolved POSIX literals with path.join output.
On Windows path.resolve adds a drive letter and path.join does not, so
the prefix match could never succeed and two assertions failed there.
Fixtures now derive from resolved roots.

Real installs were unaffected — both sides come from resolved absolute
paths — but case and drive-letter casing can still differ between
require.resolve and path.resolve on Windows, so the comparison is now
case-insensitive on win32.

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

* fix(update): stop a blackholed registry from holding the CLI open

Verification found the 1.5s timeout did not bound the command. Aborting
a fetch still completing its TCP handshake — a firewall dropping packets,
a captive portal — leaves the connect handle ref'd, so `openspec update`
sat for ~10s after printing everything. Measured against an unroutable
address: resolved at 1523ms, process exited at 10558ms.

The request now uses node:http(s), whose socket the timeout can actually
destroy: same probe resolves at 1547ms and exits at 1550ms.

Because the client is no longer fetch, the mocked tests would have gone
inert and silently reached the real registry. The whole suite now drives
the real code path against a local server, which is also the only way to
prove an opt-out sent nothing. Added a child-process guard for the
teardown itself (no in-process assertion can see it), a case for a
non-JSON body — the captive-portal login page — and order-independence
fixes: the mock leak between describes made the 406 regression guard the
first casualty under --sequence.shuffle.

Also: bound the version pattern and the response body so neither can be
absurdly long, and narrow the ephemeral-runner match so a user directory
named "dlx" is no longer mistaken for a pnpm cache.

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

* feat(update): offer to run the upgrade instead of only printing it

Being told to run a command, then run the update again, is two steps the
CLI can take for you. `openspec update` now asks:

  A newer OpenSpec CLI is available (v1.6.0 -> v1.7.0).
    Running from: /usr/local/lib/node_modules/@fission-ai/openspec
  ? Upgrade to v1.7.0 now? (Y/n)

Yes runs `npm install -g` with stdio inherited — so any auth or sudo
prompt reaches the user directly — then re-runs the update with the new
CLI, because this process still holds the old templates and cannot write
the new workflows itself. No prints the command and updates with the CLI
you have.

It asks rather than acting: a CLI that mutates a global install without
consent is the wrong default. Guards:

- Interactive terminals only, via the repo's isInteractive() (no TTY, or
  CI set, means the note prints exactly as before).
- Global npm installs only. A project dependency belongs to that
  project's package manager, and an npx/dlx cache has nothing to
  upgrade; both get the command instead.
- The re-run carries OPENSPEC_NO_UPDATE_CHECK=1, so a PATH that still
  resolves to the old binary cannot loop.
- A failed upgrade, a missing openspec on PATH, and Ctrl-C at the prompt
  each fall back to the printed command rather than an error.

The check now runs before the update rather than alongside it, so an
accepted upgrade regenerates files with the new templates in one pass.

Verified end to end against a stubbed npm and openspec on PATH, both
answers, plus the unchanged non-interactive path.

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

* fix(update): offer the upgrade only where npm install -g would help

Review found `canSelfUpgrade` treated "not a project dependency and not an
npx cache" as proof of a global npm install. It is not: a pnpm, bun, yarn
or volta global, and a plain git clone, all qualified. Reproduced by
running the CLI from this repo — it offered to npm install -g over the
checkout, which would have shadowed it with a second copy.

The offer now requires npm to own the install, derived from the running
node's global root (and APPDATA/npm_config_prefix) rather than by
shelling out to `npm prefix -g`. Everything else gets the command that
matches how it was installed — `pnpm add -g`, `bun add -g`, `yarn global
add`, `volta install` — a project dependency is pointed at its own
package manager with no npm command at all, and a source checkout gets
no note, since its version is whatever the branch says.

Docs corrected where they had drifted from the code:

- The check runs before the update, not alongside it; it can delay the
  update by up to 1.5s. docs/cli.md and the changeset said otherwise.
- npm_config_registry is only honored when npm exports it; an .npmrc
  setting alone is invisible to us. Docs and JSDoc claimed more.
- SECURITY.md gains an "Installing software" row: running a package
  manager on the user's behalf is the most security-relevant behavior
  here and the table did not mention it. The "Running other programs"
  row now covers the re-run's path argument and cross-spawn's Windows
  shim escaping, and the network row lists every opt-out precisely.
- troubleshooting.md's "Commands don't show up" — the exact symptom this
  PR exists to fix — now explains that instruction files come from the
  installed CLI, and installation.md's Updating section links onward.
- The env-var table notes the CI and NODE_ENV skips, and that
  npm_config_registry must be an http(s) URL.
- "the new workflows land in the same command" no longer overpromises:
  when the upgraded openspec is not on PATH, the CLI now says the files
  were not regenerated instead of printing a dim aside.

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

* fix(update): make the upgrade offer tell the truth about what happened

Adversarial review found the flow could claim success it had not earned,
and could strand a non-interactive caller. All verified by running the
CLI, all fixed:

- `npm install -g` exits 0 even when it installs nothing, so "✓ Upgraded
  to vX" was an assertion, not a fact. The version is now read back from
  the installed binary; when another install earlier on PATH still
  answers with the old one — the exact silent staleness this feature
  exists to fix — it says so instead of claiming the upgrade landed.
- The prompt hung forever under `openspec update > log.txt`: the
  question went to the file while the user watched a blank terminal.
  The offer now requires stdout to be a terminal too.
- Ctrl-C at the prompt read as "no thanks" and carried on into the next
  prompt. It now stops the command with 130.
- `--force` never reached the re-run, so `openspec update --force` could
  regenerate nothing and exit 0. Flags are forwarded, with `--` before
  the path so a flag-shaped path stays a path.
- A signal-killed re-run, and a re-run with no CLI to hand off to, both
  reported 0. Both now report failure.
- `process.exit()` skipped commander's postAction hook, killing the
  telemetry flush mid-request. The action sets process.exitCode and
  returns instead.
- The check read only npm_config_registry, which npm exports only under
  `npm run` — so an enterprise user with a mirror in .npmrc got an
  unannounced call to public npm. It now reads .npmrc too.
- Two different CI predicates: `CI=yes` suppressed the prompt but not
  the request. One predicate now, and it treats any value except an
  explicit off-value as CI.
- A project-local install was offered a global one when updating a
  different directory; both anchors are checked now.

Tests: the re-run had no coverage at all and now has four cases.
Mutation testing over nine mutations (406 header, DO_NOT_TRACK, version
validation, prerelease ordering, canSelfUpgrade, the anti-loop env
guard, the cwd-vs-target anchor, the timeout) — one survived, the
anti-loop guard, so it has a test now and the mutation dies.

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

* test(update): compare re-run arguments as tokens, not as a raw line

cmd.exe echoes `%*` with every argument quoted, so the Windows job saw
`"update" "--force" "--" "--weird-path"` and the substring assertion for
`-- --weird-path` failed. The forwarding itself was correct on both
platforms; the assertion now splits and unquotes before checking that
the separator immediately precedes the path.

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

* fix(update): read only the user's .npmrc for the registry

CodeQL flagged file data reaching an outbound request, and it has a
point: the project `.npmrc` travels with the repository, so honoring it
let a cloned repo choose where the version check sends its request.

Only `~/.npmrc` is read now — which is where a mirror is configured
anyway, since `npm config set registry` writes there — and a test pins
that a project `.npmrc` cannot redirect the request. Docs and changeset
say so explicitly.

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

* fix(update): detect the install from its own layout, not from node's path

Adversarial review found the offer never appeared on Homebrew — a
mainstream macOS install — and I reproduced it on this machine:

  npm root -g:    /opt/homebrew/lib/node_modules
  derived roots:  /opt/homebrew/Cellar/node/25.8.1_1/lib/node_modules

process.execPath is realpath'd through Homebrew's symlink into the
Cellar, so a root derived from the node binary never matches the prefix
npm installs into. The same mismatch hits Debian-style layouts.

The install's own shape is now the primary signal:
<prefix>/lib/node_modules/<pkg> (POSIX) or <prefix>/node_modules/<pkg>
(Windows), confirmed by the bin directory npm would have written the
shim into. The node-derived roots stay as a fast path.

Also from the same review, each reproduced first:

- volta nests a whole node install, so its packages sit in exactly npm's
  layout: we called it npm-owned, ran `npm install -g`, and on failure
  told the user to run volta. Ownership is now decided before location.
- upgradedBinPath returned the first prefix that merely had an openspec
  in it, preferring a stale one over the prefix npm just wrote to. It
  now derives from the running install first.
- readCliVersion took the first version-shaped token anywhere in stdout,
  so a wrapper banner ("Node.js v25.8.1 | OpenSpec") was read as the
  answer — turning a real upgrade into a false "still reports vX", or
  worse, claiming success for a version nobody installed. It now takes
  the line that is only a version.
- The probe child could outlive its 5s timeout indefinitely: SIGTERM
  with no escalation and no unref, so a signal-trapping wrapper held the
  CLI open for as long as it ran.
- "Another install earlier on your PATH is answering first" was a
  misdiagnosis whenever we had asked a known binary directly.
- A `registry=${VAR}` or `@scope:registry=` line in .npmrc — both npm's
  documented syntax, the latter being how a scoped package is normally
  routed to a mirror — silently fell back to the public registry.
- A 3xx from the registry disabled the check permanently and silently.
  Redirects are followed, bounded, under one timeout budget.
- An incidental directory named "pnpm" or "yarn" was read as a global
  install of one, printing the wrong upgrade command.

Plus the earlier docs-audit round: the npx branch no longer tells users
to run an update they were just handed, the check no longer fires for a
source checkout whose answer is discarded, the offer gate moved into a
tested pure function, and the declined command now prints below the
update output instead of scrolling away above it.

Docs: install-flavor table, CI off-values, empty-value opt-out, the
"no cache" fact in SECURITY.md, and a changeset trimmed to a summary
that points at the CLI reference. The changeset is now `minor` — this
adds a prompt, an env var, an outbound request, and the ability to
install software; that is not a patch.

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

* fix(update): stop reading .npmrc for the registry

CodeQL flagged file data reaching an outbound request (js/file-access-to-http),
and it is right that a file choosing where a request goes is a flow worth
avoiding. The convenience did not earn it: reading ~/.npmrc needed three
follow-up fixes in one review round (project-vs-user precedence, ${VAR}
expansion, scoped registry keys), and none of it is necessary — anyone on a
private mirror can export npm_config_registry, which is still honored, or
turn the check off.

Removes the .npmrc read and its two helpers; a test pins that a
registry= line in a .npmrc cannot steer the request.

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

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 15:39:54 +00:00
ec6cbb4b0b docs: add anvil to Community Schemas table (#1469)
* docs: add anvil to Community Schemas table

Adds a row to the Community Schemas catalog in docs/customization.md for
the anvil schema (jikkujoyce/openspec-schemas), a spec-driven workflow
with TDD discipline and an adversarial review gate.

Documentation only; the schema itself lives in its own repository.

Generated with Cursor using Claude Opus 5.

* docs(customization): describe anvil's review verdict as advisory

The row said the VERDICT: line "gates test-plan, tasks, and apply",
which reads as enforcement. OpenSpec's artifact graph only checks that
artifact files exist, and the anvil bundle ships no CI or hook — its own
schema.yaml and README say the gate is honored by the agent, not
mechanically enforced. Reword to match, and backtick artifact names
consistently across the cell.

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

* docs(customization): trim the anvil row to sibling length

The cell ran nearly twice as long as any other row in the table. Drop
the verdict-staleness rule and the 1:1 mapping detail — both are README
material — and keep the flow, the adversarial review gate, its advisory
caveat, and the test-plan ledger.

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

---------

Co-authored-by: Clay Good <hi@claygood.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 15:39:47 +00:00
Clay GoodandClaude Fable 5 fc886af7f9 fix(templates): auto-select the only active change instead of always prompting (#1468)
* fix(templates): auto-select the only active change instead of always prompting

The continue, update, verify, sync, and archive workflows told agents
'Do NOT guess or auto-select a change. Always let the user choose', which
contradicted their own Input line ('check if it can be inferred from
conversation context') and stalled every invocation on a question with a
single possible answer when only one change was active. Align them with
the selection pattern /opsx:apply has used since #513: use the provided
name, infer from context, auto-select a sole active change, prompt only
when ambiguous, and always announce the selection with how to override.
Bulk archive keeps its always-prompt behavior.

Closes #679

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

* docs(openspec): add the announce clause to the update workflow's selection contract

CodeRabbit noted the add-update-workflow delta spec and design sketch
adopted auto-selection without the 'Using change: <name>' announcement
the other selection contracts require. Add the same announce-and-override
clause so the update skill's contract matches the template it describes.

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-28 03:04:17 +00:00
fb196995da fix(adapters): escape YAML frontmatter values consistently across all command adapters (#1447)
* fix(adapters): escape YAML frontmatter values consistently across all command adapters

* fix(yaml): safely double-quote all frontmatter string values and expand table-driven adapter coverage

* fix(archive): make command and bulk archive paths root-aware, synchronous, and verified

* fix(archive): honor bulk sync inclusion decisions

* fix(adapters): honor caller delta subsets, escape control characters, close test gaps

Adversarial review of the merged branch turned up four gaps:

- Bulk archive tells the sync workflow to ignore `excludedDeltas`, but
  main's sync-specs calls `existingOutputPaths` the "complete list" of
  delta specs. An agent following both would sync the delta the caller
  withheld, step 8b would not catch it (it verifies only included
  deltas), and the run would still report `sync skipped`. Sync now
  honors a caller-supplied subset, mirroring the inline rule-snapshot
  handoff main already added.
- escapeYamlValue left C0/DEL/C1 control characters raw. The repo's own
  parser accepts them, so tests passed while stricter parsers used by
  other tools reject the document. Emit them as \xHH.
- The adapter matrix was a hand-maintained list driving only
  `description`, so raw interpolation in lingma's name/category/tags —
  and any newly registered adapter — passed green. It now derives from
  the registry and drives every string field.
- Four bulk-archive template lines were guarded only by golden hashes,
  which this repo regenerates as routine.

Also corrects the escapeYamlValue docstring, which still described the
pre-PR conditional-quoting behavior, and adds the missing changeset.

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

* fix(templates): iterate the selected delta subset, not the full CLI list

A second adversarial pass found the previous fix incomplete. Narrowing
step 3 ("Find delta specs") left step 4 — the loop that actually applies
the changes — still reading "for each capability delta spec path
returned by the CLI". An agent treating step 3 as descriptive and step 4
as operative re-widens to the full list and syncs the delta bulk archive
withheld: the original defect, one step further down the template.

Step 4 now iterates the step-3 selection, and the parity test pins both
the new wording and the absence of the old.

Also from that pass:
- Generalize the carve-out beyond archive. It was conditioned on
  "archive invoked this workflow inline", so a user asking /opsx:sync to
  sync one delta read as an instruction to ignore them.
- Define the two undefined edges: a named path outside
  existingOutputPaths, and an empty named list. Both stop and report
  rather than proceeding on a guess.
- Drive control characters through the adapter matrix. It drove none, so
  the escaping this suite exists to prove had no adapter-level coverage
  and the raw-CR assertion could never fail. Verified live by mutation.
- Give contentDerivedFields two markers that differ in length and shape.
  Same-shaped markers render identically for a length- or slice-derived
  field, which would drop it from every assertion silently.

Drops the `not.toContain('complete list of delta spec files')`
assertion: it banned one exact synonym while any reword of the same
conflicting instruction passed, so it read as coverage without being it.

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

---------

Co-authored-by: Clay Good <hi@claygood.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 02:39:38 +00:00
Alfred d32d49f066 chore(openspec): archive schema init force validation change (#1467) 2026-07-28 00:51:33 +00:00
Clay GoodandClaude Opus 5 9a61f3f30d docs(installation): add an AI-assistant setup prompt (#1466)
* docs(installation): add an AI-assistant setup prompt

Adds a provider-neutral "Install with your AI assistant" section to
docs/installation.md with one copyable prompt that detects the runtime and
package manager, installs the CLI, runs `openspec init --tools <id>`, and
verifies the result. Surfaced from the README Quick Start and the docs map.
The manual package-manager instructions stay the source of truth.

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

* docs(installation): harden the AI-assistant prompt and link it from the install paths

Adversarial review found the first draft's verify step false-failing on healthy
installs and its guardrails unenforceable. The prompt now reports what init
actually printed instead of asserting config.yaml and command files (config.yml
is equally valid; six tools and delivery=skills correctly generate zero
commands), warns that --tools auto-cleans legacy files including opsx-*.md
prompts under $HOME, picks the package manager by what's on PATH rather than by
lockfile, scopes yarn to 1.x, and stops cleanly on EACCES, a missing pnpm global
bin dir, or a version-manager shim.

Also links the flow from getting-started, the docs map, troubleshooting, and the
website CTA; notes Berry dropped `yarn global`; replaces `npm bin -g` (removed in
npm 9) with `npm prefix -g`.

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

* docs(installation): close the gaps two trial runs found in the setup prompt

Two assistants (different models) ran the prompt end to end in sandboxes, one
on Cursor and one on Codex with deliberately messy legacy files. Both finished
with a working, verified setup. Their findings:

- Cursor's commands are `/opsx-propose`, not `/opsx:propose`. The prompt named
  the colon form and init's summary agrees with it, so the assistant would have
  handed back a command the tool doesn't match. It now takes the spelling from
  the files init created.
- "List whatever you find and wait for my go-ahead" was undefined when the list
  is empty, i.e. on every fresh project. It now says to carry on.
- `openspec --version` succeeding doesn't prove it's the copy just installed;
  an older one earlier on PATH shadows it. Step 3 now compares the two.
- The request asked for confirmation before privileged/global changes; the
  prompt only stopped reactively on failure. It now shows the global install
  command and waits.

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

* docs: correct the core profile to six workflows and the tool count to 30+

Two long-standing inaccuracies, found while verifying the install docs.

`CORE_WORKFLOWS` (src/core/profiles.ts:14) is six — propose, explore, apply,
update, sync, archive — and a real `openspec init` generates six skills and six
commands. Eleven pages listed five, omitting `update`; migration-guide listed
four and filed `sync` under the expanded set. supported-tools also dropped
`update` from the full workflow-ID list. docs/commands.md was already right and
is untouched, as are flow diagrams that show a typical path rather than a
profile roster.

The tool count was written as both "25+" and "30+" against 34 supported tools.
Now consistently "30+".

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

* docs: address CodeRabbit review on the AI-assisted install flow

- Windows puts global npm binaries directly in the prefix directory, not in a
  `bin/` subdirectory; the troubleshooting fix I added said otherwise.
- Tell the assistant to stop rather than improvise when none of npm/pnpm/yarn/bun
  is available, and point Nix users at the Nix section.
- Drop the blockquote on the getting-started pointer so it isn't a second `>`
  block adjacent to the explore callout (markdownlint MD028).

Two other comments were already fixed in 1bf0706 (stop on a PATH problem; map
the user's answer to an exact tool id).

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

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 00:39:47 +00:00
Clay GoodandClaude Opus 5 f917b8be5e fix(status): order artifacts by the schema, not the alphabet (#1465)
* fix(status): order artifacts by the schema, not the alphabet

Artifacts that become ready at the same time were sorted alphabetically,
so spec-driven's `specs` and `design` - both requiring only `proposal` -
came back as design first. `openspec status` listed design above specs
and `nextSteps` pointed at design, sending agents to write design.md
before any spec existed. That contradicts the schema's own description
(proposal -> specs -> design -> tasks), the design instruction ("reference
the specs for requirements"), the workflow docs, and the schema `openspec
schema init` scaffolds (where design requires specs).

Break ties by the order the schema declares its artifacts instead. The
dependency edges are untouched, so nothing newly blocks and no artifact
becomes mandatory - only the order of equally-ready artifacts changes, and
it now follows the sequence the schema author wrote, for custom schemas
too.

Closes #692
Closes #695

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

* fix(status): re-sort the whole ready queue, not just new arrivals

CodeRabbit caught it: sorting only the newly ready artifacts left an
already-queued artifact ahead of one declared earlier. For [root, child,
laterRoot] where child requires root, the build order came out root ->
laterRoot -> child even though child is declared first and both are ready
after root. Sort the full queue after each push.

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

* fix(instructions): order unlocks like status, and document the guarantee

Adversarial review found `unlocks` was left alphabetical while build order,
ready lists and blocked lists moved to declaration order, so `openspec
instructions proposal` said "enables: design, specs" while `openspec status`
listed specs first - the one field whose job is naming what comes next
disagreed with everything else. getAllArtifacts() already yields declaration
order, so the stray sort is simply dropped.

Also make compareByDeclarationOrder a method rather than an arrow-valued
field: the field added an own enumerable function property that made
ArtifactGraph fail structuredClone.

Docs and specs updated for the new guarantee:
- openspec/specs/{artifact-graph,cli-artifact-workflow,instruction-loader}
- docs/agent-contract.md: status --json and instructions --json ordering
- docs/opsx.md: the status sample's missingDeps was missing design
- changeset

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

* docs(commands): correct the continue transcript's blocked and unlocked lines

The sample said tasks was blocked by specs alone and that creating specs
made tasks available; tasks needs design too. Same class of inaccuracy as
the status samples this branch already corrected.

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

* docs: state the ordering guarantee as dependency-order-then-declaration

CodeRabbit was right that "artifacts appear in the order the schema
declares them" over-claims: dependency order still wins, and declaration
order only breaks ties. Proved with a schema that declares tasks, specs,
proposal - status renders proposal, specs, tasks, not the declared order.
Corrected in the cli-artifact-workflow spec, agent-contract.md, cli.md and
the changeset.

Also restores "status": "blocked" in the opsx.md status sample (split across
two lines so the ASCII box still aligns) and uses "recommends writing next"
in the artifact-graph spec.

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

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 00:39:44 +00:00
6b3623a39e fix(cli): resolve store pointer for view command (#1455)
* fix(cli): resolve store pointer for view command

* fix(skill): add view command to list of commands which can take a store

* chore(changeset): note view store-pointer resolution

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

* fix(cli): keep view's cwd fallback and cover store resolution

Dropping the implicit-root fallback made view reject a pre-config.yaml
openspec/ directory that list and status still accept, so projects
initialized before config.yaml existed lost the dashboard entirely.
view now resolves the root the same way its siblings do.

Adds the store-pointer, --store, and fallback regression coverage the
review asked for.

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

---------

Co-authored-by: Clay Good <hi@claygood.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-27 23:43:21 +00:00
Clay GoodandClaude Fable 5 5bcf05766a fix(templates): replace Claude-only AskUserQuestion instruction with neutral ask-the-user guidance (#1464)
The workflow skill/command templates told agents to use the
AskUserQuestion tool, which only exists in Claude Code. The same
templates generate skills and commands for every supported tool, so
OpenCode (whose tool is named question), Factory Droid (whose native
AskUser parser errors on the instruction), Codex, and the rest were
instructed to use a tool they don't have. The guidance is now
runtime-neutral, matching the TodoWrite fix in #1403.

Fixes #920
Fixes #717

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-27 23:02:02 +00:00
Clay GoodandClaude Fable 5 caed05e8b8 fix(cli): render multi-select prompts with checkbox markers (#1463)
* fix(cli): render multi-select prompts with checkbox markers

The init/update tool picker and the schema init artifact picker are
multi-selects but rendered radio-button symbols, so users read them as
single-choice. Use the [x]/[ ] markers the config profile picker
already uses.

Closes #647

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

* test(prompts): assert [ ] returns after deselection

CodeRabbit nit: the deselect test passed even if the marker reverted
to a radio symbol instead of [ ].

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-27 22:41:12 +00:00
Clay GoodandClaude Fable 5 ebf66c7ee1 fix(init): skip the welcome animation for reduced-motion users (#1462)
* fix(init): skip the welcome animation for reduced-motion users

The openspec init welcome animation had no off switch: it repainted eight
frames on a 120ms loop with ANSI cursor-clearing, which is a seizure and
nausea trigger for motion-sensitive users (#722).

canAnimate() now also yields the existing static welcome screen when:
- the OS reduced-motion preference is on (macOS Reduce Motion, GNOME
  animations disabled), detected best-effort with a 500ms timeout and
  animation kept on any lookup failure
- OPENSPEC_NO_ANIMATION is set
- the new init --no-animation flag is passed

Closes #722

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

* fix(init): honor an empty OPENSPEC_NO_ANIMATION value

Presence is what counts, like NO_COLOR: OPENSPEC_NO_ANIMATION= (set but
empty) now also disables the welcome animation, matching the documented
'when set' behavior. CodeRabbit review follow-up.

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

* docs(init): state animation-skip env semantics precisely

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-27 22:14:05 +00:00
Clay GoodandClaude Fable 5 05c701970a chore(security): override brace-expansion to fix the failing audit (#1461)
* chore(security): override brace-expansion to fix the failing audit

A new advisory (GHSA-mh99-v99m-4gvg, high) flags brace-expansion <= 5.0.7
with the only patched release being 5.0.8. The scheduled Security workflow
has failed on every run since 2026-07-27.

pnpm audit --fix adds a scoped override in both the root and website
packages; the lockfile diffs touch only brace-expansion and its subtree.

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

* chore(nix): blank pnpmDeps hash to surface the new lockfile hash

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

* chore(nix): pin pnpmDeps hash for the updated lockfile

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-27 21:15:19 +00:00
Clay GoodandClaude Opus 5 abb422a04b chore(deps): consolidate dependabot bumps with flake hash update (#1457)
* chore(deps): consolidate dependabot bumps (typescript 6, @types/node 26, ora 9, commander, posthog-node)

Replaces #1448, #1451, #1452 and #1453 with a single lockfile resolution.
Each of those PRs changed pnpm-lock.yaml, so merging them serially would
invalidate the flake.nix pnpmDeps hash four times over.

- typescript 5.9.3 -> 6.0.3 (#1452)
- @types/node 24.2.0 -> 26.x (#1451)
- ora 8.2.0 -> 9.4.1 (#1453)
- commander 14.0.0 -> 14.0.3, posthog-node 5.46.0 -> 5.46.1 (#1448, lockfile only)

#1450 (@inquirer/prompts 8) is deliberately excluded: it needs a code
migration, not a version bump.

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

* chore(nix): update pnpmDeps hash for bumped lockfile

Hash taken from this PR's first Nix Flake Validation run. Note it differs
from the hash any individual dependabot PR would have produced -- the
combined lockfile resolves to its own content hash.

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

* chore(deps): align @types/node with the Node 20.19 runtime floor

Addresses review feedback: compiling against Node 26 declarations lets the
type checker admit APIs that are unavailable on the runtimes OpenSpec
actually supports (engines: node >=20.19.0).

Pins @types/node to ^20.19.43, the latest release in the line matching the
declared floor. This also corrects a pre-existing drift -- main was on
@types/node 24 against the same 20.19 floor, so the types were already
ahead of the supported runtime before this PR.

Verified: build clean, tsc --noEmit clean, eslint clean, 112 files /
2253 tests passing, dist/ emit byte-identical to origin/main, and no peer
dependency warnings.

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

* chore(nix): update pnpmDeps hash for the realigned lockfile

The @types/node downgrade to the 20.19 line changed the dependency set
again (it pulls undici-types 6.21.0), so the previous hash no longer
matches. Value taken from a forced-mismatch Nix run on this branch.

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

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-27 20:37:39 +00:00
eac2973819 feat(instructions): add runtime context and operation guidance (#1062)
* docs(openspec): define runtime guidance for apply and archive

- add typed apply and archive operation guidance
- extend runtime instruction inputs for apply and archive
- preserve existing archive execution and spec sync behavior

* docs(openspec): refine apply and archive guidance design

- carry artifact rules into archive-driven spec sync
- reuse one config snapshot per instruction command
- clarify that operation guidance is advisory
- classify bulk archive skill as a new capability

* docs(openspec): clarify artifact rule handling for archive and sync
- define owning artifact resolution for mixed schemas
- apply artifact rules in archive and standalone sync flows
- align archive and bulk guidance conflict semantics
- clarify existing apply pause-on-blocker behavior

* docs(openspec): tighten archive and spec sync contracts

- scope delta discovery and artifact rules to the specs artifact
- fail closed on invalid archive and specs instruction responses
- clarify no-write and no-move behavior for single and bulk archive

* feat(workflow): extend config injection to apply and archive

- expose project context and operation guidance in apply/archive instructions
- apply context and guidance across apply, archive, bulk archive, and spec sync
- preserve workflow state, artifact-rule boundaries, and fail-closed behavior
- update generated skills, documentation, tests, and parity hashes

* fix(skills): make the archive-inputs lookup fail open

`openspec instructions archive` is introduced by this PR, so no released
CLI has it. The archive and bulk-archive skills required a zero exit
status from that lookup and told the agent to stop when it failed.

`skills/` is installed standalone via `npx skills add Fission-AI/OpenSpec`
and drives whatever CLI the user already has, so between merging this and
publishing the next release every skills.sh consumer would have had
archiving blocked outright — verified against @fission-ai/openspec@1.6.0,
which exits 1 on that command.

The lookup only supplies optional prompt inputs, so it now degrades: on a
non-zero exit or invalid JSON the workflow continues with no context and
no operation guidance. The `openspec instructions specs` lookup is an
existing command and stays fail-closed, since a missing rule set there
would silently change what gets written to main specs.

Parity assertions updated to encode fail-open for archive inputs and
fail-closed for specs rules.

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

---------

Co-authored-by: showms <showms@users.noreply.github.com>
Co-authored-by: Clay Good <hi@claygood.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-07-27 20:37:34 +00:00
dependabot[bot]andClay Good 3e3cbd3f3e ci: bump actions/checkout in the github-actions group (#1449)
Bumps the github-actions group with 1 update: [actions/checkout](https://github.com/actions/checkout).


Updates `actions/checkout` from 7.0.0 to 7.0.1
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0...3d3c42e5aac5ba805825da76410c181273ba90b1)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: 7.0.1
  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>
Co-authored-by: Clay Good <hi@claygood.com>
2026-07-27 20:00:08 +00:00
5348da930c fix(schema): validate artifacts before forced init (#1446)
* fix(schema): validate artifacts before forced init

* test(schema): assert successful forced init exit status

---------

Co-authored-by: showms <showms@users.noreply.github.com>
Co-authored-by: Clay Good <hi@claygood.com>
2026-07-27 19:59:51 +00:00
Tabish BidiwaleandAlfred c33fcb3fdb chore: route reviews to maintainer team (#1441)
* chore: route reviews to maintainer team

* docs: add Alfred as automation maintainer

---------

Co-authored-by: Alfred <alfred@fissionai.io>
2026-07-27 11:49:25 +00:00
Clay GoodandClaude Fable 5 19d41714c8 fix(archive): treat early-synced REMOVED deltas as no-ops, plus audit follow-ups (#1437)
* fix(archive): treat early-synced REMOVED deltas as no-ops, plus audit follow-ups

Follow-ups from the post-v1.6.0 full-branch audit:

- archive: a REMOVED delta whose requirement is already gone from the main
  spec (early-sync pattern) now warns and continues instead of aborting,
  matching the ADDED (#1376) and RENAMED (#1386) escapes; spec-update totals
  now count applied removals only
- archive: the has-delta-specs gate matches section headers
  case-insensitively like the parser, so lowercase headers get the same
  delta validation errors validate reports
- discovery: a symlinked specs/<cap>/spec.md is resolved instead of being
  invisible (hasAnyFileUnder and the artifact graph already counted it);
  dangling links are skipped
- show: a plain `openspec show <change>` no longer warns about the
  never-passed `scenarios` flag (commander defaults --no-scenarios to true)
- parsers: buildCodeFenceMask now has a single implementation in
  code-fence.ts; requirement-text.ts re-exports it
- templates: apply/update/onboard no longer dead-end core-profile users on
  /opsx:continue and /opsx:new - they name the CLI fallback (openspec
  status/instructions) for profiles that do not install those workflows
- qwen/bob: command bodies and skills reference commands by the hyphen
  names their files actually answer to (/opsx-<id>), matching
  opencode/pi/oh-my-pi
- specs-apply: remove the dead applySpecs export (no callers, bypassed
  store-aware roots)

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

* fix(archive): reject RENAMED+REMOVED conflicts, surface JSON warnings, skip no-op writes

Adversarial-review round for #1437:

- a delta that both RENAMEs and REMOVEs the same requirement is rejected
  explicitly by both validate and archive - the warn-and-continue REMOVED
  path would otherwise have masked the contradiction that previously
  failed incidentally at apply time
- buildUpdatedSpec collects its warnings and archive --json carries them
  in a new optional `warnings` array, so agent flows see the same
  skipped-REMOVED signal humans get on stdout
- archive skips rewriting a spec whose operations were all already
  synced, instead of churning normalization differences into the file
  (and no longer materializes an empty skeleton for a REMOVED-only new
  spec)
- init's getting-started hint uses each tool's real invocation form
  (/opsx-propose for qwen/bob/opencode/pi/oh-my-pi)
- onboard's pause guidance names the CLI fallback when /opsx:continue is
  not installed (CodeRabbit)
- openspec-conventions spec updated to state the idempotent archive
  semantics; changeset added

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

* fix(archive): abort on near-miss REMOVED typos, honest specsUpdated for no-op archives

Round-2 adversarial review for #1437:

- a REMOVED header that differs only in case or interior whitespace from
  an existing requirement is a typo, not an early sync - it stays a hard
  abort naming the near-miss, instead of degrading to warn-and-continue
- specsUpdated is true only when a spec file was actually written; a
  fully-already-synced change prints "Specs already in sync; no files
  changed." and reports specsUpdated: false in JSON (CodeRabbit)
- agent-contract documents the archive warnings field and specsUpdated
  semantics; changeset wording fixed (CodeRabbit)

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

* fix(archive): compare the RENAMED+REMOVED conflict case- and whitespace-insensitively

Addresses alfred's review on #1437: `RENAMED FROM: Old Name` plus
`REMOVED: old name` slipped past the exact-match cross-section guard,
so validate passed, archive renamed the requirement, reported the
removal as already synced, and archived the change.

Both the validator and the apply-side guard now compare the two
spellings with the shared foldRequirementName (lowercase, collapsed
whitespace), and the error names the variant spelling when it differs.
Focused regressions cover both paths; requirement matching everywhere
else stays case-sensitive.

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-23 22:53:31 +00:00
Clay GoodandClaude Opus 4.8 6a5171e186 fix(validate): allow numeric-prefixed change names (#1435)
* fix(validate): allow numeric-prefixed change names

`validateChangeName` required a leading letter, so `openspec new change
100-add-feature` or `00001-add-auth` failed with "Change name must start
with a letter". This contradicted the rest of OpenSpec: the shared kebab-id
grammar in src/core/id.ts (store ids, workset names, change metadata ids)
already allows a leading digit, and archive explicitly supports
`YYYY-MM-DD-` prefixed change names as a convention (#1309).

Reuse the canonical `isKebabId` grammar for change names so numeric prefixes
work, keeping the tailored error messages for the other failure cases. Fully
backward-compatible: every previously valid name still validates
(`[a-z]` ⊂ `[a-z0-9]`), and consecutive/leading/trailing hyphens, uppercase,
spaces, underscores and other characters are still rejected.

Closes #850
Closes #1169

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

* docs, changeset, tests for numeric-prefixed change names

Address review of the numeric-prefix change:
- add the required changeset (patch)
- fix docs/cli.md which still said names "cannot start with a number"
  and advised prefixing ticket IDs with a word (website copy regenerates
  from this file at build time)
- pin the all-numeric case (`100`) so accepting it is a conscious decision

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

* test: name the tiered-prefix test for what it covers

CodeRabbit noted the 101-01-fix-auth fixture contains letters, so the old
title 'all digits and hyphens' was inaccurate. Rename it to describe the
tiered numeric-prefix case (#850); the dedicated all-numeric case is the
separate '100' test.

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 20:04:36 +00:00
Clay GoodandClaude Opus 4.8 b976fc0666 fix(website): show openspec init in the homepage getting-started box (#1434)
The final "Ship your first change in five minutes" call-to-action showed
only `npm install -g @fission-ai/openspec@latest`, which installs the CLI
but does nothing on its own. New users who copy that one line get no
scaffolding and nothing to run. Add the `cd your-project && openspec init`
step so the box matches the canonical README flow.

Closes #1282

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 19:22:06 +00:00
Clay GoodandClaude Opus 4.8 26f009d940 fix(change): resolve changes by directory instead of requiring proposal.md (#1433)
* fix(show): resolve changes by directory instead of requiring proposal.md

`openspec show <change>` and shell completion resolved a change only when
`openspec/changes/<name>/proposal.md` existed. Every sibling command --
`list`, `status`, `instructions`, `validate` -- resolves a change by its
directory (`getAvailableChanges`). The two rules disagree the moment a
change is created: `openspec new change <name>` scaffolds only
`.openspec.yaml`, so `list` showed the change while `show` reported
`Unknown item`. A custom schema that defines no proposal artifact was
never resolvable at all (#1161).

Resolve by directory in `getActiveChangeIds`/`getArchivedChangeIds`, and
report a change that exists without a proposal accurately -- pointing at
`openspec status --change <name>` -- rather than as missing.

The deprecated `openspec change list` keeps its own proposal-backed scan;
its JSON output parses proposal.md per change.

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

* fix(show): offer proposal-less changes in the no-name selector too

`ChangeCommand.show` resolved a named proposal-less change but still built
its no-name selector (and the non-interactive "Available IDs" hint) from
the proposal-gated scan, so a scaffolded change could not be picked.

Use directory-based discovery there as well. `list` keeps the local
proposal-backed scan: its --json output parses proposal.md per change.

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

* fix(change): unify list/show discovery and report a missing proposal honestly

Adversarial review of the first two commits surfaced four defects.

`change list` still used a private proposal-gated scan, so the deprecated
alias reported a different set than `openspec list` -- the command its own
deprecation warning tells you to use -- while the `show` selector beside it
offered the wider set. Move it to `getActiveChangeIds` and drop the now
unused helper and its ARCHIVE_DIR constant.

Widening that list exposed three follow-on bugs, all fixed here:

- Task counts were computed inside the proposal try block, so a change with
  tasks but no proposal.md reported 0/0. Task progress is independent of the
  proposal; resolve it first.
- `--long` printed "(unable to read)" and `--json` "Unknown" for a change
  that is simply not written yet. Distinguish a missing proposal from an
  unreadable one by testing existence, not by sniffing error codes.
- `show` reported a stray file under changes/, or a traversing name such as
  `../..`, as a change awaiting its proposal, pointing the user at a
  `status --change` call that cannot work. Require a directory that is a
  direct child of changes/.

Also drops a duplicated proposal.md read in the --long path.

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

* fix(change): contain change lookup and stop guessing at unreadable proposals

Second review round.

`isDefinitelyMissing` replaces the plain existence check: fs.access can fail
with EACCES or an I/O error, and treating that as "no proposal.md yet" hid a
real read failure behind an ordinary-looking state. Only ENOENT counts as
absent; anything else falls through to the existing unreadable handling.

`show` now rejects a name that is not a direct child of changes/ before
touching the filesystem. This closes a pre-existing traversal on main:
`openspec change show ../..` resolved openspec/changes/../../proposal.md and
printed a file from outside the changes directory. Both new tests fail
without the guard.

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 17:32:10 +00:00
Clay GoodandClaude Opus 4.8 a874d1d671 chore(security): create test temp dirs with mkdtemp and override two pinned CVEs (#1432)
* test: create temp dirs with fs.mkdtemp

Every one of these suites built its temp directory by hand:

    testDir = path.join(os.tmpdir(), `openspec-test-${randomUUID()}`);
    await fs.mkdir(testDir, { recursive: true });

That check-then-create is what CodeQL's js/insecure-temporary-file flags,
and it accounted for 452 of the repo's 467 dismissed code-scanning alerts.
Dismissing them one by one is a treadmill: every new suite that copies the
idiom mints fresh alerts, and a real finding is easy to lose in that volume.

fs.mkdtemp creates the directory atomically at mode 0700 with a random
suffix, so there is no window to pre-empt and no name to guess. The suites
that already used it are the evidence this silences the rule: 33 of the 34
files calling mkdtemp carry zero alerts. The lone exception, archive.test.ts,
was scanned one commit before its own mkdtemp fix landed and is left to that
change rather than conflicting with it.

The dirs named on Date.now() alone were genuinely predictable; the randomUUID
ones were not, but they trained the same copy-paste. Both are gone now.

Behavior is unchanged: mkdtemp creates the root the old mkdir created, and
no assertion depended on the root being absent. Verified with the full suite
(2196 tests, 111 files).

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

* chore(deps): override postcss and sharp in the website

Two advisories on the docs site have no Dependabot PR and never will:
Next pins postcss at 8.4.31 as a direct dependency and sharp at ^0.34.5 as
an optional one, so Dependabot cannot raise either without a Next release
that does it first. 16.2.11 does not — it still pins both. Left alone these
sit open indefinitely.

  postcss  8.4.31 -> 8.5.22   GHSA-qx2v-qp2m-jg93  (XSS via unescaped </style>)
  sharp    0.34.5 -> 0.35.3   GHSA-f88m-g3jw-g9cj  (4 libvips CVEs)

A version-ranged selector (`postcss@<8.5.10`) was the first instinct, since it
lapses on its own once Next moves past it. It is the wrong tool: it pins the
override to one advisory's floor, and silently stops applying when the next
advisory raises that floor. GHSA-6g55-p6wh-862q landed while this branch was
open and moved postcss's patched floor to 8.5.12 — under the ranged selector a
dependency pinning 8.5.11 would have resolved to 8.5.11 and stayed vulnerable.
A plain floor cannot under-match, so that is what this uses.

The floor also covers the new advisory: 8.5.22 is past 8.5.12. postcss dedupes
to the single copy the site already had for Tailwind.

These are the last two open Dependabot alerts on the repo. `pnpm audit` on
website/ goes from 2 advisories to "No known vulnerabilities found". The site
builds clean, OG image generation included, and the package set grows by
exactly two platform-gated wasm32 binaries that never install on CI.

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

* ci(security): audit the docs site too

Both `pnpm audit` steps run at the repo root. The docs site keeps its own
lockfile and is not a workspace member, so neither step could see it — the
root audit passed all the way through while postcss and sharp sat open in
website/. That blind spot is why they needed a manual override to find.

Blocking rule copied from the published-dependency audit: advisory on pull
requests, blocking on the weekly schedule and on pushes to main. An
always-advisory step would only relocate the blind spot — the sweep would stay
green with a live advisory and someone would have to read the log of a passing
run to notice.

`!cancelled()` because the two audits above can fail hard. Without it a root
advisory would skip this step entirely, in exactly the situation where the
site's own state matters most.

Verified against the pre-fix lockfile — the step reports the two advisories it
would have caught:

  2 vulnerabilities found
  Severity: 1 moderate | 1 high

and reports "No known vulnerabilities found" against the fixed one. Confirmed
it reads website/pnpm-lock.yaml and not the root: with a vulnerable website
lockfile and a clean root, it exits 1; a missing website lockfile is a hard
ERR_PNPM_AUDIT_NO_LOCKFILE rather than a false clean.

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 16:17:26 +00:00
Clay GoodandClaude Opus 4.8 6a4f0d7f33 fix(archive): keep the delta spec's Purpose in a new main spec (#1431)
* fix(archive): keep the delta spec's Purpose in a new main spec

Archiving a change that creates a brand-new capability always overwrote
the delta's authored `## Purpose` with the TBD placeholder, so the
Purpose had to be re-typed by hand after every archive.

buildSpecSkeleton now takes the delta's Purpose when there is one. The
placeholder still appears when the delta has no Purpose or an empty one,
and an existing main spec's Purpose is never touched.

The spec-driven schema now tells agents to open a new capability's delta
with a `## Purpose` (and not to add one to a delta for an existing
capability), so the default workflow stops producing placeholders.

Closes #1413
Closes #369

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

* test(archive): create the temp dir with fs.mkdtemp

Matches the mkdtemp pattern the rest of the suite already uses and
clears the CodeQL insecure-temp-file alerts on this file.

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

* test(archive): pin fenced-Purpose behavior and align the spec wording

Review flagged that the spec scenario read as "only non-fenced content
counts", which the code does not do. Masking fenced lines out of the
Purpose body would truncate a legitimate Purpose that includes an
example block, so the code is right and the wording was wrong.

- Reword the cli-archive scenarios: the fence check is on the `## Purpose`
  header, and the section body is copied verbatim.
- Add regressions: fenced code inside a real Purpose survives, a Purpose
  header that only appears inside a fence falls back to TBD, and an empty
  Purpose section falls back to TBD.

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

* fix(archive): never let a carried Purpose abort the archive

Self-review found a regression introduced by the carry-over: a delta
whose `## Purpose` body contains a `### Requirement:` header put that
header outside `## Requirements` in the new main spec, so the structure
guard rejected it and archive exited 1. The same delta archived fine
before this branch.

Fall back to the placeholder and warn when the carried Purpose would
make the new spec structurally invalid, so archive completes as it did
before.

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

* fix(archive): make the Purpose carry-over safe and consistent

Three adversarial reviews of the carry-over found the guard added in
651b42c was too narrow and the guidance half-landed. Addressed:

Engine
- Replace the two-rule structural guard with a readability check against
  the parser validate/list/archive actually use. A Purpose body holding a
  heading or an unterminated code fence used to abort the archive, or
  write a spec with a duplicated `## Requirements` that its own validator
  rejects. Both now fall back to the placeholder and warn.
- Ignore markdown inside HTML comments when locating the Purpose, so a
  commented-out draft cannot beat the real section and an unfilled
  template placeholder counts as empty.
- Warn when a carried Purpose is under the strict-mode minimum: the old
  placeholder always cleared it, so this was the first way archive could
  leave a spec that `validate --strict` fails.
- Warn instead of silently dropping a delta Purpose when the main spec
  already exists.

Guidance, which disagreed with itself and with the agent path
- openspec-sync-specs told agents to write TBD, so `/openspec-archive`
  undid what the CLI now does. It carries the delta Purpose too.
- The specs artifact template and the instruction's own example had no
  `## Purpose` while the prose asked for one.
- Document the section in concepts, writing-specs, their website copies,
  openspec-conventions and specs-sync-skill; state the 50-character
  threshold and how to change an existing spec's Purpose.

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

* fix(archive): stop HTML comments in a carried Purpose from corrupting the spec

Round-two adversarial review found the comment masking added in fff5fb2 was a
one-sided defense: it hid markdown from the section scan but handed the raw
text to the file, where the spec parsers and markdown renderers have no comment
awareness at all. Three ways that broke:

- `## Requirements` inside a comment in the Purpose body: the merged requirement
  landed under the commented-out header, the real section was left empty, and
  `validate --strict` still passed.
- `### Requirement:` inside a comment: archive exited 1 where main exited 0 -
  the same regression class 651b42c was supposed to have closed.
- An unterminated comment: carried verbatim, blanking the whole spec in any
  markdown renderer while validation stayed green.

A carried Purpose containing comment markers is now refused outright, so the
spec never reads differently to different readers. This also subsumes the
"comment truncates the parsed Purpose" case, where the too-brief warning
measured the raw slice and stayed silent while validate failed - the warning now
measures the parsed overview, the same string the validator reads.

Also from review:
- Emptiness now ignores fenced blocks as well as comments, so a Purpose that is
  only a code sample falls back to the placeholder. This is what CodeRabbit and
  alfred originally asked for; the earlier reply refuted their mechanism, which
  truncates a mixed Purpose, but the requirement itself was satisfiable and the
  shipped spec already claimed it.
- The "already has one" warning was false when the target had no Purpose, and
  noise when the two bodies matched. It now fires only when the spec has a
  different Purpose of its own, and names the resolved path so it is correct
  under --store.
- sync-specs was silent on the existing-spec case and on `## Purpose` in its
  delta format reference, and never surfaced a TBD placeholder it wrote.
- openspec-conventions said SHALL NOT for a rule nothing enforces and this
  repo's own deltas break; softened to SHOULD NOT.

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

* fix(archive): treat --!> as a comment terminator

CodeQL's "Bad HTML filtering regexp" rule: HTML closes a comment on `--!>` as
well as `-->`. The guard already refused anything with a `<!--` in it, so the
outcome was safe either way, but the mask now recognizes both spellings and a
test pins the case.

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

* fix(archive): only an HTML comment opener disqualifies a carried Purpose

The guard rejected any `-->` as well, which threw away a legitimate Purpose
over prose like "ingest --> transform --> sink". A bare terminator hides
nothing and renders as text; only a `<!--` can conceal markdown.

Safety is unchanged: a comment that opens before the section header masks the
header itself, so there is no body to carry, and a body can therefore only hide
content behind a `<!--` of its own. All three comment hazards still fall back
and warn, now pinned alongside an arrow-notation regression.

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

* fix(archive): mask an unterminated HTML comment through end of file

alfred caught that maskHtmlComments only matched closed comments, so an
unterminated `<!--` above a `## Purpose` left the commented-out header looking
real: archive completed on the normal validated path and wrote the abandoned
draft as the new capability's Purpose.

An unclosed comment runs to EOF, so everything after it is commented out.
Masking it that way restores the invariant readableOverview relies on - a
comment opening above the header always masks the header, so a carried body can
only hide content behind a `<!--` of its own - and that dependency is now named
in the comment rather than left implicit.

Regression covers both the closed and unterminated spellings; reverting the EOF
masking fails the unterminated one and nothing else.

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 16:17:23 +00:00
dependabot[bot] 2b503389f5 chore(deps): bump next from 16.2.10 to 16.2.11 in /website (#1429)
Bumps [next](https://github.com/vercel/next.js) from 16.2.10 to 16.2.11.
- [Release notes](https://github.com/vercel/next.js/releases)
- [Commits](https://github.com/vercel/next.js/compare/v16.2.10...v16.2.11)

---
updated-dependencies:
- dependency-name: next
  dependency-version: 16.2.11
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-23 14:39:38 +00:00
TandClay Good 81d5109b86 docs: switch Roo Code references to Zoo Code (#1428)
* docs: switch Roo Code references to Zoo Code

* no-mistakes(review): Remove unrelated AGENTS.md changes

* no-mistakes(document): Update Roo Code references to Zoo Code; fix unused eslint directive

---------

Co-authored-by: Clay Good <hi@claygood.com>
2026-07-22 22:58:35 +00:00
Clay GoodandClaude Fable 5 5406c8b3ff chore(deps): consolidate dependabot bumps with flake hash update (#1427)
* chore(deps): consolidate dependabot bumps (chalk, posthog-node, zod, changesets, typescript-eslint, eslint 10)

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

* chore(nix): update pnpmDeps hash for bumped lockfile

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-22 21:54:37 +00:00
dependabot[bot] 11a301d5fb chore(deps): bump next in /website in the website-dependencies group (#1420)
Bumps the website-dependencies group in /website with 1 update: [next](https://github.com/vercel/next.js).


Updates `next` from 16.2.9 to 16.2.10
- [Release notes](https://github.com/vercel/next.js/releases)
- [Commits](https://github.com/vercel/next.js/compare/v16.2.9...v16.2.10)

---
updated-dependencies:
- dependency-name: next
  dependency-version: 16.2.10
  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-07-22 21:17:20 +00:00
6832cc4a17 ci: bump the github-actions group with 6 updates (#1419)
* ci: bump the github-actions group with 6 updates

Bumps the github-actions group with 6 updates:

| Package | From | To |
| --- | --- | --- |
| [actions/checkout](https://github.com/actions/checkout) | `5.1.0` | `7.0.0` |
| [actions/setup-node](https://github.com/actions/setup-node) | `6.5.0` | `7.0.0` |
| [actions/upload-artifact](https://github.com/actions/upload-artifact) | `6.0.0` | `7.0.1` |
| [DeterminateSystems/nix-installer-action](https://github.com/determinatesystems/nix-installer-action) | `21` | `22` |
| [DeterminateSystems/magic-nix-cache-action](https://github.com/determinatesystems/magic-nix-cache-action) | `13` | `14` |
| [actions/dependency-review-action](https://github.com/actions/dependency-review-action) | `4.9.0` | `5.0.0` |


Updates `actions/checkout` from 5.1.0 to 7.0.0
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09...9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0)

Updates `actions/setup-node` from 6.5.0 to 7.0.0
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](https://github.com/actions/setup-node/compare/249970729cb0ef3589644e2896645e5dc5ba9c38...820762786026740c76f36085b0efc47a31fe5020)

Updates `actions/upload-artifact` from 6.0.0 to 7.0.1
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/b7c566a772e6b6bfb58ed0dc250532a479d7789f...043fb46d1a93c77aae656e7c1c64a875d1fc6a0a)

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

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

Updates `actions/dependency-review-action` from 4.9.0 to 5.0.0
- [Release notes](https://github.com/actions/dependency-review-action/releases)
- [Commits](https://github.com/actions/dependency-review-action/compare/2031cfc080254a8a887f58cffee85186f0e49e48...a1d282b36b6f3519aa1f3fc636f609c47dddb294)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: 7.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions
- dependency-name: actions/setup-node
  dependency-version: 7.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions
- dependency-name: actions/upload-artifact
  dependency-version: 7.0.1
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions
- dependency-name: DeterminateSystems/nix-installer-action
  dependency-version: '22'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions
- dependency-name: DeterminateSystems/magic-nix-cache-action
  dependency-version: '14'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions
- dependency-name: actions/dependency-review-action
  dependency-version: 5.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions
...

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

* ci: correct dependency-review-action pin comment to v5.0.0

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

---------

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>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-22 21:17:16 +00:00
Clay GoodandClaude Opus 4.8 cac44ecfcf test(cli): invoke the CLI without a shell (#1426)
Twenty-five test call sites built a command string and handed it to
execSync, which runs it through a shell. The interpolated value is a
constant path in every case, so nothing was exploitable, but it is the
pattern CodeQL reports as shell-command-injection and it accounts for
every remaining alert on the repository.

Each call now passes an argument array to execFileSync, which never
involves a shell. Error handling is unaffected: both APIs reject with the
same spawnSync error carrying status and stderr, which these tests assert
on. A path containing spaces would now be passed as one argument rather
than word-split, which is the more correct behavior.

Test-only. Nothing in test/ ships in the npm package.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 20:43:51 +00:00
Clay GoodandClaude Opus 4.8 040a86931f fix(config): compare prototype key guards literally so analysis can see them (#1425)
* fix(config): guard prototype keys at the write, not behind a helper

The guards added in #1415 are effective — Object.prototype is never
touched — but they sit at the top of the function and delegate to a Set
lookup, which CodeQL cannot follow. Three prototype-pollution alerts stayed
open on src/core/config-schema.ts after that PR merged, on exactly the code
it hardened.

Each key segment is now compared literally in the loop that performs the
write. Same behavior for every input, including the empty path and nested
creation; the check is simply local to the danger and visible to a reader
and to the analyzer.

Also tightens a feedback test that matched the issue URL with a substring,
which CodeQL rated high. It now parses the URL and compares origin and
pathname, so a lookalike host cannot satisfy the assertion.

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

* fix(config): validate the whole key path before writing

The first attempt at making the guard analyzer-visible moved the check
into the write loop, which changed behavior: a path whose unsafe segment
came after a safe one created the intermediate objects for the safe prefix
before bailing out. `setNestedValue({a:'x'}, 'b.constructor.c', v)` left
behind `b: {}` where the previous implementation wrote nothing.

A differential run against the implementation on main caught it — 47,782
mismatches in 400,000 cases. The literal comparisons stay, but they now
scan the whole path before any mutation, so a rejected key leaves the
object untouched. Re-run of the same comparison: 0 mismatches.

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

* test(config): pin the partial-write behavior of a rejected key path

The fix landed without a test that would fail if the guard moved back into
the write loop, so the regression could return unnoticed. The existing
prototype-pollution tests only assert Object.prototype, and every path they
use starts with an unsafe segment on an empty object, so no intermediate
object is created before the guard trips.

Adds cases that put the unsafe segment after a safe one and assert the
whole target, plus one for a trailing unsafe segment, where the debris is a
re-parented prototype rather than an extra key and a structural comparison
alone would miss it.

Verified by reintroducing the regression: 5 of the new assertions fail
against the buggy build and pass against the fix.

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 20:01:54 +00:00
Clay GoodandClaude Opus 4.8 e2f748c64f chore(security): add security policy, dependabot config, and config key guards (#1415)
* chore(security): add security policy, dependabot config, and config key guards

Adds a SECURITY.md with a private disclosure path and an explicit threat
model, a Dependabot configuration covering the CLI package, the docs site,
and CI actions, and closes a prototype-pollution path in `config set`.

`--allow-unknown` was meant to relax the known-key check but skipped every
key check, so `openspec config set --allow-unknown __proto__.polluted x`
reported success and assigned onto Object.prototype for the process
lifetime. Unsafe segments are now rejected at the command layer regardless
of `--allow-unknown`, and setNestedValue/deleteNestedValue refuse them for
any caller.

Also bumps the bundled yaml dependency from 2.8.2 to 2.9.0, the only
advisory in this repo that affects code shipped in the npm package.

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

* chore(nix): update pnpmDeps hash for the yaml bump

fetchPnpmDeps pins a fixed-output hash over the whole dependency set, so
changing pnpm-lock.yaml invalidates it. Recovered the new value from a
hash-mismatch build.

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

* chore(security): clear dependency advisories and automate future checks

Refreshes both lockfiles so every open advisory in the CLI package is
resolved, replaces a quadratically-backtracking heading parser, and adds
the automation to catch the next one.

Dependency refresh (in-range, lockfile only): brace-expansion, flatted,
js-yaml, minimatch, postcss, rollup, and vite all move to patched versions
in the root lockfile; fast-uri and brace-expansion move in the website
lockfile. Only @changesets/cli needed a declared floor bump, to reach a
patched js-yaml. Production dependencies now report zero advisories.

extractFirstPurposeLine parsed ATX headings with /\s+#+\s*$/, which
backtracks quadratically on a whitespace-padded title. Replaced with a
linear hand-rolled scan, verified identical to the old implementation
across 303,000 generated inputs.

Automation: a Security workflow runs dependency review on pull requests,
blocks on advisories in published dependencies, and re-audits weekly; every
GitHub Action is pinned to a commit SHA so a moved tag cannot change what
CI executes.

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

* fix(ci): drop the pnpm cache from the audit job

Nothing is installed there, so setup-node's cache-save post step failed on
the missing store path even though both audit steps passed.

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

* chore(nix): repin pnpmDeps hash after the dependency refresh

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

* chore(security): apply opengrep findings and fix a dependency-review permission gap

Ran opengrep against the repository to check the claims in #1414. Of 208
findings, 201 are one path-traversal rule firing on joins built from module
constants, argv, or readdir entry names; 1 non-literal-regexp is fed only by
LEGACY_SLASH_COMMAND_PATHS. Neither is reachable from untrusted input. The
actionable results are applied here.

- dependabot: add a cooldown so a freshly published version is not adopted
  immediately. Security updates ignore the cooldown, so this delays only
  routine bumps, long enough for a compromised release to be yanked.
- getNestedValue now refuses prototype-reaching segments, matching the
  guards already on setNestedValue and deleteNestedValue.
- dependency-review no longer asks to comment on the pull request. That
  needs `pull-requests: write`, which the workflow does not grant and a
  fork's token never gets, so a real finding would have failed on the
  comment instead of reporting the vulnerable dependency.
- SECURITY.md: show the command that proves build tooling is absent from an
  installed copy, rather than asking readers to take it on trust.

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

* fix(dependabot): drop semver cooldown keys unsupported by github-actions

Dependabot rejected the whole config file, which would have silently
disabled every version update.

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

* chore(security): make the audit advisory and document runtime behavior

The published-dependency audit no longer fails the job. A newly published
advisory should not block an unrelated pull request, and the step depends on
registry availability; Dependabot alerts and dependency review remain the
gates. SECURITY.md is corrected to match — it claimed the audit was blocking.

Also documents what the CLI does on your machine, all verified rather than
asserted: the install script prints one line and makes no network request or
file write; every shell-invoking call uses a fixed literal while anything
carrying user input uses an argument array with shell:false; telemetry sends
a command name, a version, and a local random UUID, with IP capture disabled.

Secret scanning is now listed, confirmed enabled by a repository admin.

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

* fix(ci): keep the production audit blocking off pull requests

Making the step advisory on every event meant a newly published
high-severity advisory in a shipped dependency could not fail any run
unless a dependency changed. It stays advisory on pull requests, so an
unrelated change is never blocked by an advisory published that morning,
and blocks on the weekly schedule and on pushes to main, where a failure
is the signal rather than a tax on someone else's work.

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 18:55:51 +00:00
Clay GoodandClaude Opus 4.8 ffe27de18d chore(scripts): add a parity-hash regeneration helper (#1416)
skill-templates-parity.test.ts pins a SHA-256 per workflow template so an
unintended template edit fails loudly. The cost lands on every intended
edit: the pinned hashes go stale, and because all 37 live in two maps in
one file, two branches editing different templates collide there on rebase.
Resolving that means hand-editing 64-character hashes, which is where
transcription mistakes come from - and the test proves a hash matches its
source, never that the source is right, so a bad value regenerated over a
bad merge passes CI in silence.

Recompute every pinned hash from the built dist/ and rewrite the map in
place, reporting which entries moved. The skill-directory mapping comes
from getSkillTemplates(), the same helper the skills.sh generator uses, so
adding a workflow needs no second list here; function labels resolve
dynamically against the module exports, so there is no hard-coded list at
all.

"Nothing to update" has to mean it, so four things abort the run without
writing:
  - dist/ missing or older than src/, which would pin hashes from a stale
    build that the parity test - which reads src/ - then rejects
  - a pinned label with no matching export, from a renamed or deleted
    template
  - a pinned hash whose line the patterns do not recognise, counted by
    comparing 64-hex literals found against literals rewritten; the count
    uses a deliberately broader pattern so it is a real cross-check rather
    than a restatement of the same patterns
  - a skill the registry deploys that nothing pins, compared in the other
    direction: pins-to-registry only sees pins that already exist

That last direction closes a hole that predates this script. A workflow
added to getSkillTemplates() but never pinned was invisible to the parity
test too, which compares only the entries it already lists - so it shipped
with no golden hash while everything reported success. skill-templates-
parity.test.ts now pins the registry itself, so CI catches it whether or
not anyone runs this script.

The rewriting lives in parity-hash-shared.mjs, following the split between
generate-skillssh.mjs and skillssh-shared.mjs, so those guards can be
exercised against fabricated input. Running the script for real from a test
would rewrite the repository's own parity test file mid-suite. Each case in
parity-hash-shared.test.ts was mutation-checked: removing the guard it
covers makes it fail.

The script cannot silently emit a wrong hash: the parity test recomputes
the same values independently and compares, so a drift between the two
copies of stableStringify fails the test. The test stays the authority.

Dev tooling only. scripts/ is not published (package.json files ships just
scripts/postinstall.js), no src/ is touched, and no runtime behaviour
changes - hence no changeset.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 18:54:13 +00:00
Clay Good 27b22ab4cb feat(validate): accept zero-delta changes that declare skip_specs (#1399)
Squashed for rebase; see PR #1399 for the full commit history.
2026-07-22 15:55:59 +00:00
Clay Good 1dc670deea fix(templates): stop propose from skipping the specs artifact (#1412)
Squashed for rebase; see PR #1412 for the full commit history.
2026-07-22 15:38:13 +00:00
Clay GoodandClaude Fable 5 5dfef4b00c fix(templates): make the schema instruction field authoritative for artifact creation (#1405)
* fix(templates): make the schema instruction field authoritative for artifact creation

The continue-change skill and command embedded hard-coded spec-driven
artifact patterns that agents followed instead of the schema's
instruction field whenever a custom schema reused familiar artifact
names, so schemas could not delegate artifact creation to their own
skills. Drop the hard-coded patterns, state that the instruction field
is authoritative, and tell both continue and ff workflows to invoke a
skill when the instruction delegates to one.

Fixes #777

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

* fix(templates): apply instruction-field delegation at the creation step and in propose

Adversarial review findings: the propose workflow shared the same
creation loop and pre-fix wording as ff, and the numbered creation
steps still commanded a direct write before the agent ever reached the
delegation guideline. Add the delegation conditional at the point of
creation in propose, continue, and ff (skill and command variants),
add the authoritative-instruction bullets to propose, and verify the
artifact exists after a delegated skill runs.

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

* fix(templates): read dependencies before delegating, pin #777 behavior in tests

Address alfred's review: the hash baselines alone accepted any regenerated
prompt, so add a focused parity assertion covering all six variants
(propose/continue/ff x skill/command) that the instruction field is the
authoritative guidance, delegated creation is invoked and verified at the
creation step and restated in the guidelines, and the old "Common artifact
patterns" shortcut stays gone. The test fails against the pre-fix templates.

Also fix an ordering contradiction the adversarial review surfaced: the
continue-change delegation bullet preceded the dependency-read bullet and
said "instead of following the bullets below", telling agents to skip
dependency reads that the guardrails require. It now mirrors propose/ff:
read dependencies first, then delegate "instead of writing the file
yourself" - making the sentence identical across all six variants.

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-22 15:23:07 +00:00
Clay GoodandClaude Fable 5 2d6c447100 fix(templates): replace Claude-only TodoWrite instruction with a generic todo list (#1403)
The propose and ff-change skill/command templates told agents to use the
TodoWrite tool, which only exists in Claude Code. The same templates
generate commands for every supported tool, so Codex, Cursor, Gemini,
and the rest were instructed to use a tool they don't have. The
instruction is now runtime-neutral.

Fixes #643

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-22 15:07:30 +00:00
Clay GoodandClaude Opus 4.8 378d468ad3 fix(templates): give explore the project's context and rules (#1408)
* fix(templates): give explore the project's context and rules

Explore was the only workflow that never loaded openspec/config.yaml.
Every artifact-creating workflow receives the project's `context` and
`rules` through `openspec instructions --json`, but explore has no
artifact or change name, so it never travels that path — it started a
session knowing only what `openspec list --json` returns.

The result was a thinking partner blind to the project's own tech stack,
conventions, and constraints.

Both the skill and command surfaces now read the config through the
`root.path` reported by `openspec list --json`, so stores and workspace
planning homes resolve correctly instead of assuming a repo-local path.
Guidance-only: no CLI behavior, schema, or architecture changes.

Fixes #696

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

* fix(templates): cover config.yml and scope rules to their artifact

Review follow-ups on the explore context guidance:

- `config.yml` is a first-class alternative to `config.yaml`
  (`resolveConfigFilePath` probes both, and `init` leaves a `.yml`
  project on `.yml` permanently). Naming only `.yaml` meant those
  projects hit the skip-if-missing branch and silently lost their
  context - the exact failure this change set out to fix.

- `rules` is keyed by artifact id, and explore holds no artifact at
  startup. The guidance now says the entries apply when writing that
  artifact, so rules for one artifact are not applied to another.

- Match house style on leakage: every sibling template and the
  instructions renderer forbid copying context/rules into the artifact,
  not just into the conversation. The wording now covers both.

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 14:55:12 +00:00
Clay GoodandClaude Fable 5 0da5f98e14 fix(templates): show the main spec format in the sync-specs skill (#1402)
The sync-specs skill's only markdown example was the delta format, so
agents (Junie in #1120) copied delta files into openspec/specs/ as-is,
leaving ## MODIFIED Requirements headers that the spec parser rejects —
openspec view reported 0 requirements. Add a Main Spec Format Reference,
point step 4d at it, and add a guardrail against wholesale delta copies.

Fixes #1120

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-22 14:49:14 +00:00
Clay Good b3b05e1abe fix(init): only advertise slash commands the profile installs (#1410)
Fixes #1409. The `openspec init` welcome screen and the `openspec update`
legacy-upgrade menu hardcoded /opsx:new and /opsx:continue. The default core
profile is propose/explore/apply/update/sync/archive, so it never generates
them and users were told to run commands that did not exist.

getOnboardingCommands() holds the hints in lifecycle order and returns only
those whose workflow is installed; both surfaces print its result. In
`update` the set is what the newly configured tools actually received, since
a legacy upgrade installs an inferred subset for Codex.

Stacked on #1404, which decides how each hint is spelled per tool. This
commit decides which hints appear; #1404's referenceFor/printStartHints
decide the reference form, so Kimi still gets /skill:openspec-*.

The welcome screen's quick-start block is width-constrained: it renders
beside a 24-column art column and only animates at MIN_WIDTH (60) or wider,
and the animation moves the cursor up a fixed count of logical lines. A
wrapped line desyncs it, so descriptions are capped at DESCRIPTION_BUDGET
and a test asserts no rendered line exceeds 59.

Also validates --profile before the welcome screen rather than casting it,
so an invalid value fails before the user presses Enter.
2026-07-22 14:32:11 +00:00
Clay GoodandClaude Opus 4.8 97d441a8ee fix(templates): stop the bulk archive when the user picks Cancel (#1398)
The bulk archive confirmation offered a "Cancel" option but never told
the agent what to do with it. Step 8 then archived every selected
change, so an agent following the skill literally moved the changes
even after the user cancelled.

Route each answer by intent rather than by literal label: the option
labels are written by the agent and carry an `N` placeholder, so
matching them verbatim would send every legitimate answer down the
"ask again" path. Cancel now stops without archiving and skips the
remaining steps, the ready-only option is bound to the status table
that decides what "ready" means (re-deriving conflict resolutions when
a Ready* partner is skipped), and a guardrail repeats that a cancelled
batch archives nothing.

Regression tests cover both archive paths, so the single-change
routing can no longer be silently reverted either.

Instruction text only — no CLI behavior changes.

Closes #1381

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 14:26:22 +00:00
Jun 9b5d2cdd0c fix(templates): stop instructing a second date prefix on dated archive names (#1388)
* fix(templates): stop instructing a second date prefix on dated archive names

The archive-change and bulk-archive-change workflow templates told agents
to unconditionally build the archive target as YYYY-MM-DD-<name>, so a
change already named with the common YYYY-MM-DD- convention came out
double-dated — the template-side twin of the CLI bug fixed in #1316,
which a CLI fix cannot reach because the behavior is baked into
instruction text.

The generate-target-name step and the bulk guardrail now mirror the CLI
rule: use the change name as-is when it already starts with a
YYYY-MM-DD- prefix, otherwise prepend the current date. The literal mv
commands move to <target-name> so an agent copying them verbatim cannot
stack dates, and the onboarding walkthrough's archived-path example
carries the same caveat. Regenerated skills/ and updated the pinned
parity hashes; a new parity test guards the caveat and rejects the raw
stacked mv target.

* fix(templates): report the derived archive name in success summaries

The success and failure summaries still printed archive/YYYY-MM-DD-<name>,
so an agent copying them would report a stacked date for a change whose name
already carries a YYYY-MM-DD- prefix. Point those examples at <target-name>
instead, and widen the regression guard from the mv target to any date used
as a path segment, which leaves the rule statements that must keep explaining
the derivation untouched.

The opsx-archive-skill spec still specified the unconditional current-date
rule the previous commit removed from the template, so bring it in line with
the wording cli-archive already carries.

* fix(specs): name the derived target in the archive scenario

The successful-archive scenario still spelled the destination as
archive/YYYY-MM-DD-<name>/, the same literal form this PR removed from the
templates, so it contradicted the keep-as-is rule the behavior requirements
now carry.
2026-07-22 14:03:21 +00:00
Clay GoodandClaude Fable 5 a84ae70e8c fix(init): use skill references for tools without a command adapter (#1404)
* fix(init): use skill references for tools without a command adapter

Adapterless tools (kimi, vibe, hermes, forgecode, codeartsagent, agents)
skip command generation even under the default 'both' delivery, but their
generated SKILL.md files still told agents to run /opsx:* commands that
were never created, and the init summary suggested /opsx:propose. Route
the existing skill-reference transform by command-surface capability so
these tools get /openspec-* references, and point the getting-started
hint at the skill when no selected tool got commands.

Fixes #1155

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

* fix(init): address adversarial review findings for adapterless skill references

- transform the committed skills.sh distribution too: pass
  transformToSkillReferences in generate-skillssh.mjs and the parity
  test, regenerate skills/ (that channel installs SKILL.md files only,
  so /opsx:* commands never exist there)
- key the getting-started hint purely on whether any selected tool got
  commands, so the delivery=commands + adapterless corner can no longer
  print /opsx:propose
- make the one-time profile-migration message capability-aware for
  projects whose detected tools have no command adapter
- import CommandSurfaceCapability type-only instead of duplicating the
  union inline (a value import would close a module cycle)
- cover the update path: the kimi migration test now asserts refreshed
  skills contain no /opsx references

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

* fix(init): honor Kimi Code's documented /skill: invocation syntax

Per review: the blanket /openspec-* rewrite contradicted Kimi's
documented invocation contract (/skill:openspec-*, see
docs/supported-tools.md). Skill-reference transforms are now selected
per tool via getSkillReferenceTransformer, with Kimi mapped to
/skill:<name> and every other tool keeping the documented /<name>
form; the getting-started hint and migration message use the same
per-tool syntax. End-to-end Kimi assertions cover generated skill
content, the refreshed update path, and the hint.

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

* fix(init): gate the getting-started hint on a generated surface

Per review: with delivery=commands and only adapterless tools selected,
init generated neither skills nor commands yet still advertised an
invocation. Print a configuration correction instead, with the exact
'openspec config set delivery both' remedy, covered by an end-to-end
commands-only adapterless test. Also from the adversarial review round:
mixed selections that disagree on invocation syntax (kimi + vibe) now
fall back to the default /openspec-* form in the shared hint and
migration message instead of picking the first tool's syntax; add the
missing changeset; correct the codex doc comment.

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

* fix(init): suppress the restart hint when no surface was generated

From the third adversarial review round: the 'Restart your IDE for
slash commands' line printed directly after the message saying nothing
was generated. Gate it on an actually generated surface and pin that in
the commands-only adapterless test. Also: use randomUUID() for init
test temp dirs (matches update.test.ts, removes a theoretical Date.now
collision), and clarify the changeset wording about the skills.sh
channel's default reference form.

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

* fix(init): print one usable getting-started hint per invocation syntax

Per review: the mixed-syntax fallback advertised /openspec-propose,
which Mistral Vibe accepts but Kimi Code does not. Group successful
tools by their transformed reference and print one labeled hint line
per distinct form, so every advertised instruction is usable by the
tool it names; the mixed-tool test asserts exactly that. The migration
message compares transformed outputs instead of function identities
(also per review) and stays syntax-neutral ('the openspec-propose
skill') when detected tools disagree.

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

* fix(init): keep codex hints syntax-neutral (skills-invocable, no slash surface)

Codex has no slash-command surface: docs direct users to
.codex/skills/openspec-*. The getting-started hint and the one-time
migration message now name the skill ('the openspec-propose skill')
instead of advertising a /openspec-* form Codex does not accept, and the
restart line only claims slash commands when commands were generated.

Hint lines are also limited to tools that actually got skills: under
delivery=commands, codex+kimi previously advertised /skill:openspec-propose
for Kimi while .kimi-code was never created.

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

* fix(init): advertise a usable instruction for every configured tool

Adversarial-review round fixes:

- Mixed adapter-backed + skill-only selections (claude+kimi, claude+codex)
  printed a single unlabeled /opsx:propose hint that the skill-only tool
  cannot use; hints are now derived per tool from its generated surface
  and labeled when the selection disagrees.
- The delivery=commands configuration correction keyed on the global
  aggregate, so a tool that got zero artifacts lost its correction as
  soon as any other tool generated something; it is now per-tool.
- The migration message advertised /opsx:propose under an explicit
  'delivery: skills' config where commands will never exist; the command
  form is now gated on the effective delivery.
- Migration-message coverage extended (kimi, codex+kimi, delivery=skills,
  commands-installed); profile-describe init tests use randomUUID temp
  dirs like the first describe block.

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

* fix(update): derive migration and legacy-upgrade references per tool surface

The one-time migration message collapsed mixed command + skill-only
selections to /opsx:propose (Claude commands + a Kimi skill told the
Kimi user to run a command it cannot invoke); the reference is now
computed per detected tool and falls back to the syntax-neutral form on
disagreement. The legacy-upgrade getting-started menu had the same
capability blindness with hard-coded /opsx:new/continue/apply — a legacy
Codex upgrade advertised commands Codex lost in #1283; menu lines are
now derived the same way (byte-identical for command-tool upgrades).

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-22 13:53:49 +00:00
Clay GoodandClaude Opus 4.8 c439a4ee48 fix(parser): stop delta section dividers from becoming phantom requirements (#1411)
* fix(archive): stop reporting phantom proposal warnings from delta specs

`openspec validate --strict` reported a change as valid while `openspec
archive` printed "Proposal warnings in proposal.md" for the same change,
blaming requirements that do not exist.

Archive validates the proposal with `validateChange`, which parses the
change together with its delta specs. Requirement-level issues from those
deltas were printed in the proposal block even though they are not
proposal issues. Two problems followed:

- The change parser records every requirement under both `requirement`
  and `requirements`, so each defect was printed twice, then a third time
  by the delta report.
- A heading inside a delta section that is not a `### Requirement:`
  heading was parsed as a requirement, producing a scenario warning
  against a requirement that does not exist. The delta reader already
  handles this correctly and reports it as an informational note.

Proposal warnings now report proposal-level issues only. Delta spec
issues keep being reported once, by the delta report, with the capability
file path and requirement name. Exit codes are unchanged: this block was
already non-blocking, and blocking delta validation is untouched.

Refs #498

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

* fix(archive): correct proposal-warning claims and pin bracket-path rules

Review follow-ups, no behavior change:

- The delta report prints only the issue message, never `issue.path`, so it
  does not name the capability file. Drop that claim from the spec scenario
  and the code comment; two capabilities with the same defect print two
  identical lines.
- Only the missing-scenario class was reported three times. Say that
  precisely instead of generalizing to every delta error.
- Widen the spec scenario: the filter applies to every archive, not only to
  changes carrying a stray heading.
- Add a test pinning that applyChangeRules bracket paths
  (`deltas[<n>].description`) survive the dot-anchored filter, so a future
  path normalization cannot silently widen it.

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

* fix(parser): ignore delta headers that are not "### Requirement:"

Fixes the cause of #498 rather than one of its symptoms.

A header inside a delta section that is not a `### Requirement:` header —
a divider such as `### Documentation Requirements` — was read as a
requirement with no scenario. That invented a delta that does not exist:
`openspec archive` warned about a missing scenario, and `openspec show
<change> --json` and `openspec change list` counted it.

ChangeParser now filters those headers before reading requirements,
matching REQUIREMENT_HEADER_REGEX, which the delta reader already uses.
The override lives in ChangeParser, so main spec parsing — view, list,
spec --json, spec validation — is untouched.

The archive filter stays: it covers the half the parser cannot. The
change parser records every requirement under both `requirement` and
`requirements`, so each delta defect was printed twice, and REMOVED
requirements are names-only by design yet were reported as missing a
scenario on every correct removal.

Also from review:
- Soften the spec scenario; delta spec validation does not always run
  (the hasDeltaSpecs gate is case-sensitive), so it cannot be promised
  as the reporter.
- Assert VALIDATION_MESSAGES constants instead of message literals.
- Add parser-level and REMOVED-only regression tests.

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 13:47:51 +00:00
Clay GoodandClaude Fable 5 d3a9982d32 ci: clear Node 20 deprecation warnings by bumping action runtimes (#1407)
* ci: clear Node 20 deprecation warnings by bumping action runtimes

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

* ci: stop persisting checkout credentials in ci.yml jobs

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-22 13:37:30 +00:00
Clay GoodandClaude Fable 5 9d40ae98f0 fix(nix): build with Node.js 22 now that nixpkgs marks Node 20 insecure (#1406)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-22 13:32:00 +00:00
Clay GoodandClaude Opus 4.8 b33b15d98a fix(schemas): stop design.md from restating the proposal (#1401)
* fix(schemas): keep design.md from restating the proposal

The spec-driven design instruction asked for background, current state,
and goals without saying the motivation and scope already live in
proposal.md, so generated designs often duplicated the proposal instead
of adding technical decisions. Scope the Context and Goals guidance to
what the approach needs, and state the boundary explicitly: the proposal
covers why and what, design covers how - reference, don't restate.

Closes #1382

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

* fix(schemas): qualify the specs reference for design's parallel ordering

design.requires is [proposal] only, so a design can be drafted before
the specs exist. Say "once written" instead of implying the specs are
always there to reference.

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

* chore(changeset): add the patch changeset for the design/proposal boundary

schemas/ ships in the npm package files list, so this guidance change
reaches users on upgrade and needs a changelog entry. The Validate
Release Tracking check only validates changesets when present, so its
absence was not caught.

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-22 13:25:53 +00:00
34d2d67d1c test(completion): isolate ZshInstaller tests from a real Oh My Zsh install (#1400)
* test(completion): isolate ZshInstaller tests from a real Oh My Zsh install

process.env.ZSH (exported by Oh My Zsh) short-circuits
isOhMyZshInstalled() before the fallback check against the injected
test home directory, so 17 of the 50 tests failed on any machine with
Oh My Zsh installed. Clear $ZSH in beforeEach and restore it in
afterEach, matching the save/restore idiom already used for
OPENSPEC_NO_AUTO_CONFIG in this file and for SHELL/COMSPEC in
shell-detection.test.ts.

Fixes #1321

Co-Authored-By: Stanley Kao <stanleykao72@gmail.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test(completion): cover the $ZSH env-var detection branch explicitly

Clearing $ZSH in setup left isOhMyZshInstalled()'s env-var branch with
no coverage anywhere (before, it was only exercised accidentally on
machines with Oh My Zsh). Assert detection succeeds from $ZSH alone,
with no .oh-my-zsh directory present.

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

---------

Co-authored-by: Stanley Kao <stanleykao72@gmail.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-22 13:20:35 +00:00
Clay GoodandClaude Opus 4.8 60f720c43a fix(feedback): submit feedback when the repo has no feedback label (#1396)
`openspec feedback` passed `--label feedback` unconditionally, but the
repository does not define that label. gh resolves label names before
creating the issue, so it failed with "could not add label: labels not
found: feedback" on every invocation and the command exited non-zero,
discarding the feedback the user had just composed.

Retry once without the label when — and only when — gh's stderr reports
that it could not add the label, and tell the user the label was not
applied. Every other failure keeps its existing behavior: print gh's error
and exit with gh's exit code, with no retry. Only stderr is matched,
because the error message also embeds the command line, which carries the
user's own feedback text.

The cli-feedback spec gains a scenario for the unlabeled path, and its
gh-failure scenario is narrowed to exclude it. The fallback scenarios are
unchanged.

Refs #1091

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-20 19:42:49 +00:00
Łukasz SarnaandClay Good fdf3d1282d docs: add e2e-runbooks to Community Schemas table (#1255)
Co-authored-by: Clay Good <hi@claygood.com>
2026-07-20 19:42:46 +00:00
Nicolas MartinandClay Good a824aae9da docs: add nanopm to community schemas catalog (#1109)
* docs: add nanopm community schema to catalog

* docs: add design artifact mapping to nanopm description

---------

Co-authored-by: Clay Good <hi@claygood.com>
2026-07-20 19:21:50 +00:00
Clay GoodandClaude Opus 4.8 b474f81cb4 fix(templates): don't archive a change before its spec sync finishes (#1394)
* fix(templates): wait for the spec sync before archiving a change

The generated openspec-archive-change skill dispatched the spec sync to a
subagent via the Task tool and then moved changeRoot in the very next step,
with nothing requiring it to wait. Where subagents run asynchronously, the
archive relocates the delta specs out from under the running sync, so the
change is archived while openspec/specs/ is never updated — and the success
summary still reports "Specs: ✓ Synced".

Step 4 now requires waiting for the dispatched sync to return, verifying the
synced requirements are present in the main spec, and stopping without
archiving if either check fails. Adds a matching guardrail bullet and a
parity assertion so the gate cannot silently disappear again.

Fixes #1393

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

* fix(templates): run the spec sync inline and verify it before archiving

Addresses review on #1394. The first pass asked the agent to "wait" for a
dispatched subagent, but subagents run in the background by default and the
wait is not reliably expressible in prose — the race survived. It also gated
the archive on the synced requirements being *present*, which a correct
REMOVED-only or RENAMED-only sync does not satisfy, turning a successful sync
into a hard block.

The sync now runs inline via the Skill tool, with a synchronous-subagent
fallback for harnesses that need one. Verification follows delta semantics:
ADDED/MODIFIED present, REMOVED gone, RENAMED under the new name, checked
across every capability the sync touched.

Also resolves the opsx command variant's contradiction with its own guardrail,
stops the summary reporting a checkmark that step 4 never verified, and updates
openspec/specs/opsx-archive-skill/spec.md, which still said the skill proceeds
with the archive regardless of the sync choice.

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

* docs(specs): encode delta verification semantics in the archive skill spec

The scenario said the agent verifies each capability "matches its delta",
which is ambiguous about what a match means — and a REMOVED-only sync
correctly leaves requirements absent. Spell out the predicate the template
implements, and separate an explicit "Archive without syncing" choice from a
requested sync that failed or could not be verified: only the former may skip
verification, the latter must stop.

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

* fix(templates): close verification holes and drop Claude-only tool names

Second review pass on #1394.

The gate was weaker than it looked. "MODIFIED requirements present" is
vacuous — a MODIFIED requirement exists in the main spec before the sync runs,
so a no-op sync passed the check for the most common delta shape, which is the
exact symptom #1393 reports. "RENAMED under their new name" passed a sync that
copied rather than renamed, leaving both names behind. And scoping the re-check
to "every capability it touched" derived the verification set from the artifact
being verified, so a silently skipped capability escaped it.

Verification is now bound to the delta specs in artifactPaths.specs, covers the
changes each MODIFIED delta names, and requires RENAMED requirements to be gone
from the old name.

Separately, the previous pass named the Claude Code "Skill tool" and
run_in_background in a template that is also the slash-command source for ~28
other tools, where skills are removed entirely for commands-only delivery. Both
variants now use the runtime-neutral phrasing bulk-archive-change already uses.

Also: route the prompt options explicitly instead of defaulting unknown answers
to archive, tell the user a stopped archive is recoverable, and mark the summary
line as a conditional rather than literal text to copy.

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

* fix(templates): verify the sync by re-running step 4's own comparison

The verification predicate restated delta semantics in its own words, which
could drift from what openspec-sync-specs actually does. Anchor it instead to
the comparison step 4 already performs before prompting: a successful sync
leaves nothing to apply, so every capability must read as already synced. The
explicit ADDED/MODIFIED/REMOVED/RENAMED bullets stay as the definition of what
"nothing left to apply" means.

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-20 18:59:51 +00:00
HowardandClay Good d2082d1f91 docs: align cli-update OpenCode spec with commands/ and opsx-* paths (#1170)
Update OpenCode cli-update spec to .opencode/commands/opsx-*.md and document legacy path cleanup via init.

Co-authored-by: Clay Good <hi@claygood.com>
2026-07-20 18:59:17 +00:00
Clay GoodandClaude Opus 4.8 a13abeac47 fix(validate): reject a delta spec at the change's specs/ root (#1392)
* fix(validate): reject a delta spec at the change's specs/ root (#1385)

A `spec.md` written directly under a change's `specs/` directory was
accepted by `validate` — including `--strict` — but skipped by the
apply/archive merge, which only reads capability folders. The change
validated clean, archived successfully, and its requirements never
reached `openspec/specs/`.

Point the validator at the shared `discoverSpecFiles` helper so it applies
exactly the merge path's rules, and report a root-level `specs/spec.md` as
an error naming the capability-folder convention. Archive's delta-detection
gate now also sees that file, so validation runs and blocks the archive
instead of completing with the delta dropped.

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

* fix(archive): trip the delta gate on any root-level spec.md

Follow-up to the same divergence class as the parent commit, found while
re-reviewing it.

Archive's gate only ran validation when a candidate file carried delta
headers, so a root-level `specs/spec.md` written in main-spec shape
(`## Requirements`) still archived with exit 0 while `validate` reported an
error — the two commands disagreed again. The file is never merged whatever
its shape, so existence alone now trips the gate.

Also stop reporting a *directory* named `specs/spec.md` as misplaced: that
is an ordinary capability folder the merge path reads normally, and
`fileExists` matched it. Both sites now require a regular file.

Finally, suppress the generic "No deltas found" error when the root-level
error already fired: it contradicted the precise message by claiming there
were no deltas in the very file just named.

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-20 17:26:14 +00:00
Ben MosesandClay Good 5e365b962f feat(schema): resolve symlinked schema directories (#1299)
* feat(schema): resolve symlinked schema directories

Schema discovery filtered directory entries with `Dirent.isDirectory()`,
which reports the raw entry type and returns false for symlinks — even
those pointing at a real directory. As a result, symlinked schema dirs in
the user/project/package schema locations were silently skipped.

Add a shared `isSchemaDir()` helper that accepts real directories and
dereferences symlinks (via statSync) to admit symlinked directories while
still rejecting symlinks-to-files and broken links. Use it at all six
discovery sites in resolver.ts and in `schema validate` in schema.ts.

* test(schema): cover symlinked schema directory resolution

Add unit tests for isSchemaDir (real dir, symlink-to-dir, symlink-to-file,
broken symlink, regular file) plus integration tests confirming listSchemas
and listSchemasWithInfo pick up a symlinked user schema dir while ignoring
symlinks whose target is a file.

---------

Co-authored-by: Clay Good <hi@claygood.com>
2026-07-20 17:26:05 +00:00
Vishnu J 470f5727ad fix(archive): make scenario-drift check multiplicity-aware (#1246) (#1391)
findMissingCurrentScenarios stored incoming scenario names in a Set, so
when the current requirement had N scenarios sharing a name and a
MODIFIED block kept fewer, membership still looked covered and archive
silently dropped the extras. Count occurrences per name instead and
report each excess instance as missing, keeping the existing error shape.

Refs #1246 (residual after #1252). Analysis credit: @HerbertGao.
2026-07-20 16:44:30 +00:00
showmsandshowms 596d6ba7f4 fix(ui): preserve Windows input after welcome screen (#1175)
Co-authored-by: showms <showms@users.noreply.github.com>
2026-07-18 20:59:33 +00:00
a0eb70ef07 fix: avoid npx when applying profile changes (#1351)
* fix: avoid npx when applying profile changes

* fix(config): apply profile updates in process

* fix(config): address profile apply review feedback

---------

Co-authored-by: showms <showms@users.noreply.github.com>
Co-authored-by: Tabish Bidiwale <30385142+TabishB@users.noreply.github.com>
Co-authored-by: TabishB <tabishbidiwale@gmail.com>
2026-07-18 20:59:26 +00:00
Jun 520aa8c470 fix(doctor): note when a store checkout is behind its upstream ref (#1287)
* fix(doctor): note when a store checkout is behind its upstream ref

Stores have no commit pin, so teammates on different commits of the same
store silently resolve different specs. Add a read-only info diagnostic
(store_checkout_drift) reporting ahead/behind counts against the local
upstream ref, and surface them in doctor --json. Fires only when behind
(ahead-only is normal — OpenSpec never pushes stores) and never fetches,
so it flags already-known drift, not a live cross-machine check.

Refs #1273

* refactor(doctor): parallelize git probes and share drift-test setup

Run the independent gitOriginUrl / gitTrackingDrift probes concurrently,
and extract the repeated git-bootstrap in the drift tests into a shared
initGitStore() helper. No behavior change.

* test(doctor): pass the isolated git env when capturing the branch name
2026-07-18 20:55:11 +00:00
Jun 9b70481df7 fix(archive): keep an existing date prefix instead of stacking a new one (#1316)
Archiving unconditionally prepended today's date to the change name, so a
change already named with the common YYYY-MM-DD- convention came out
double-dated (2026-07-07-2026-07-04-voice-copilot-v1) — and when archived
on a later day, the folder sorted under a day on which the change did not
happen.

Detect a full YYYY-MM-DD- prefix and archive the change under its own name.
Names without one (including partial dates like 2026-07-feature) keep the
current behavior. This also makes the naming idempotent.

Nothing in src/ parses the date back out of archive folder names — the
prefix only drives human chronological sorting — so keeping the original
date is the minimal, non-breaking choice. The cli-archive spec wording is
updated to match.

Fixes #1309
2026-07-18 20:55:05 +00:00
Jun b419e965bb fix(archive): treat already-synced RENAMED deltas as no-ops (#1386)
The early-sync idempotency fix (#1376) covered only ADDED requirements.
A change whose RENAMED deltas were already applied to the baseline by
the sync workflow still aborted archive with 'RENAMED failed - source
not found'.

RENAMED now skips when the source header is gone but the target header
exists in the spec — the target's presence is positive evidence the
rename was already applied. A rename whose source and target are both
missing still aborts, as does every other genuine conflict. Reported
counts now reflect only renames actually applied.

REMOVED is intentionally left strict: validation does not compare
REMOVED names against the baseline, so treating a missing requirement
as a no-op would let a typo'd or stale name archive silently.
2026-07-18 20:54:48 +00:00
Jun b7c85c741c fix: use skill references in SKILL.md for skills-only delivery (#1194)
* fix(skills): use skill references in skills-only delivery mode

When delivery is configured as 'skills', generated SKILL.md files
contained hardcoded /opsx:* command references pointing to commands
that were never generated, breaking cross-skill workflows.

Add transformToSkillReferences() with the explicit command-to-skill
mapping (kept in sync with WORKFLOW_TO_SKILL_DIR) and wire it in
init and update wherever skill content is generated, following the
approach outlined in #881.

Closes #881
Closes #879

Generated with Claude (Cowork) using claude-fable-5; verified with the
full vitest suite (1683 tests passing).

* fix(skills): wire skill references in workspace skill generation

Review follow-up for the skills-only delivery fix: workspace skill
setup (src/core/workspace/skills.ts) generates SKILL.md via the same
generateSkillContent path but was not wired with
transformToSkillReferences, leaving dangling /opsx:* references when
delivery is 'skills'. Wire both call sites, add a regression test
(verified to fail against the unwired code), strengthen the update
skills-only test with content assertions, and correct the
COMMAND_TO_SKILL_REFERENCE comment (WORKFLOW_TO_SKILL_DIR exists in
both profile-sync-drift.ts and init.ts).

Generated with Claude (Cowork) using claude-fable-5; verified with
eslint and targeted vitest suites (144 tests passing).

* refactor(skills): extract transformer selection into getTransformerForTool

Address CodeRabbit review: the tool/delivery transformer selection was
duplicated at five call sites across init.ts, update.ts, and
workspace/skills.ts. Extract it into a documented helper in
command-references.ts with unit tests locking the selection matrix
(opencode/pi precedence, skills-only delivery, default).

Generated with Claude (Cowork) using claude-fable-5; verified with
eslint, tsc, and targeted vitest suites (147 tests passing).

* fix(skills): prioritize skill references over hyphen commands in skills-only delivery

Address review: getTransformerForTool returned transformToHyphenCommands
for opencode/pi before checking delivery, so skills-only delivery still
emitted /opsx-* references to commands that were never generated. Check
delivery === 'skills' first so skill references win for every tool, and
keep the hyphen transform for opencode/pi only when commands are
generated. Add unit coverage for the opencode/pi selection matrix and an
opencode skills-only init integration test asserting no /opsx: or /opsx-
references remain.

Generated with Claude (Cowork) using claude-fable-5; verified with
eslint, tsc, and targeted vitest suites (148 tests passing).

* docs(skills): add docstrings to init/update generation entry points

* test(skills): reject stale hyphenated references in skills-only assertions

* fix(skills): add missing update entry to command-to-skill reference map

COMMAND_TO_SKILL_REFERENCE was missing the update workflow, so a
/opsx:update reference in any skill template would survive skills-only
transformation untouched. Align the map with the canonical 12-entry
WORKFLOW_TO_SKILL_DIR and assert the generated openspec-update-change
skill carries no raw command references.
2026-07-18 20:54:44 +00:00
9acddcda07 fix: use local dates for CLI date-only values (#1361)
Co-authored-by: showms <showms@users.noreply.github.com>
Co-authored-by: Tabish Bidiwale <30385142+TabishB@users.noreply.github.com>
2026-07-18 13:29:29 +00:00
showmsandshowms 79f1dac668 feat(codex): make Codex skills-only and retire managed custom prompts (#1283)
* feat: make Codex skills-only

* test: cover codex legacy prompt paths

* fix: address review feedback for Codex skills-only migration

* fix(codex): revalidate managed global prompt paths before cleanup

* resolve conflicts with upstream/main

* fix(codex): refine legacy prompt migration

---------

Co-authored-by: showms <showms@users.noreply.github.com>
2026-07-18 12:53:27 +00:00
Clay GoodandClaude Fable 5 46a4d78222 feat(skills): publish workflow skills to skills.sh (#1357)
* feat(skills): publish workflow skills to skills.sh

Commit the 12 OpenSpec workflow skills as static skills/<name>/SKILL.md so
`npx skills add Fission-AI/OpenSpec` can install them (skills.sh reads static
files from the repo; OpenSpec otherwise only generates skills at init time).

Files are generated from the existing templates via `pnpm generate:skills`,
not hand-copied, and skillssh-parity.test.ts fails CI if a template changes
without regenerating. The volatile generatedBy frontmatter line is stripped so
the committed copies stay byte-stable across releases.

Closes #1258

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

* fix(skills): force LF on committed skills/ so Windows CI parity holds

The skills.sh distribution files are generated LF-only and compared
byte-for-byte by skillssh-parity.test.ts. Windows autocrlf checked them
out as CRLF, failing the parity assertion. A scoped .gitattributes pins
them to LF on checkout.

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

* fix(skills): reject symlinks and assert the exact committed skill set

Review feedback (alfred): the parity test only visited expected templates,
so an extra or renamed skills/ directory shipped with green CI, and the
generator would write through a pre-existing symlinked skill directory to
anywhere on disk.

- generator: refuse to run if skills/ contains any symlink (checked before
  any deletion, so a bad tree is left intact), validate dirNames against a
  path-segment allowlist, and lstat the target before writing.
- parity test: assert skills/ holds exactly README.md plus one real
  directory per template, each containing a single real SKILL.md.
- focused tests cover symlink refusal (no partial deletion), traversal
  names, and stale-directory cleanup.

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

* fix(templates): abort archive on Cancel, honest summary, fence languages

CodeRabbit review on #1357, fixed at the template source and regenerated:

- archive-change: choosing "Cancel" at the sync prompt now stops the flow
  instead of archiving anyway (skill + command templates).
- archive-change skill: the success output no longer hardcodes "All
  artifacts complete. All tasks complete." when archiving incomplete work.
- archive/bulk-archive/sync-specs/verify-change: language identifiers on
  previously plain code fences (MD040), skill and command twins alike.

Golden hashes in skill-templates-parity.test.ts recomputed from dist/;
skills/ regenerated via pnpm generate:skills.

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-17 21:41:11 +00:00
ac656c983f feat: add CodeArts Agent skills support (#1266)
* feat/add codeartsagent to tool list

* feat/add codeartsagent to tool list

* test: clarify CodeArts init log assertions

* docs(cli): union hermes and zcode into the supported tool-ID list

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

---------

Co-authored-by: Clay Good <hi@claygood.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 21:20:40 +00:00
Jun 7704702d61 fix(qwen): generate Markdown commands instead of deprecated TOML format (#1191)
* fix(qwen): generate Markdown commands instead of deprecated TOML format

Qwen Code deprecated TOML custom commands in favor of Markdown files
with YAML frontmatter. Update the Qwen adapter to emit
.qwen/commands/opsx-<id>.md and register the old opsx-*.toml files as
legacy artifacts so they are cleaned up on update.

Closes #838

Generated with Claude (Cowork) using claude-fable-5; tested with the
full vitest suite (1663 tests passing).

* test(qwen): assert YAML frontmatter for qwen in registry adapter test
2026-07-17 20:56:49 +00:00
57a88a3d12 feat(zcode): add ZCode as supported tool (#1209)
* feat(zcode): add ZCode as supported tool

Register ZCode in the AI tools registry and provide a command adapter
so `openspec init --tools zcode` generates per-project artifacts under
a single .zcode/ root (no split across .agents + .zcode):

- Skills: .zcode/skills/openspec-*/SKILL.md (ZCode-native discovery path,
  highest priority among project-level skill roots)
- Commands: .zcode/commands/opsx/<id>.md (Claude-compatible frontmatter)

Both .zcode/skills and .agents/skills are valid ZCode discovery roots
(verified from ZCode source: skillRootsForBase registers them in pairs);
we use .zcode to keep all artifacts under one directory.

ZCode auto-detection triggers on .zcode or .agents at the project root.

Verification:
- pnpm build passes (TypeScript compiles clean)
- pnpm lint passes (no new warnings)
- pnpm test: 1661 tests pass (no regressions)
- E2E: `openspec init --tools zcode --profile core` produces
  5 skills + 5 commands, all under .zcode/ (no .agents created)

* fix(zcode): scope auto-detection to .zcode only

ZCode's detectionPaths included '.agents', a generic directory used by
many agent frameworks. A bare '.agents' at the project root caused
false-positive ZCode detection (mirroring the Copilot bare-.github
problem the codebase already guards against).

Drop the detectionPaths override so ZCode is detected solely via its
strongly-identifying skillsDir '.zcode'. Add tests locking the new
contract: a bare '.agents' must not trigger detection, and '.agents'
co-located with '.zcode' must not suppress real detection.

* test(zcode): lock adapter path and frontmatter escaping contract

Add focused coverage for the ZCode command adapter that the existing
broad tests did not protect:

- getFilePath lands under .zcode/commands/opsx/<id>.md and never
  references .agents
- formatFile emits name/description/category/tags frontmatter
- YAML escaping across all branches: colons/quotes/newlines (quoted
  values), special chars in name/category, per-tag quoting, plus the
  previously uncovered backslash-doubling and leading/trailing
  whitespace branches

* test(zcode): lock command adapter registry presence

Verify the ZCode adapter is registered in CommandAdapterRegistry so
openspec init/update can resolve it via get/getAll/has. The existing
registry tests only sampled a few tools, so a future refactor that
drops the zcode registration would have passed silently.

* test(zcode): lock init/update generation stays under .zcode

End-to-end coverage that init and update generate ZCode skills and
commands under .zcode/ and never create a .agents directory. The
adapter path/detection unit tests alone cannot catch a generation-time
regression that writes outside .zcode, so this asserts the contract on
disk for both entry points.

---------

Co-authored-by: young <young@example.com>
Co-authored-by: Clay Good <hi@claygood.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 20:50:55 +00:00
4a0f15d3b2 feat: add Hermes Agent support (#1292)
* feat: add Hermes Agent support

* feat(init): surface Hermes external_dirs setup note during init and update

Hermes only loads skills from ~/.hermes/skills unless the project
.hermes/skills directory is added to skills.external_dirs in
~/.hermes/config.yaml, so init could report success for skills Hermes
ignores. Add a setupNote field to AIToolOption, print it after init and
update (including the up-to-date path), and cover the adapterless init
path, the adapter registry, and both update paths with tests.

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

---------

Co-authored-by: Clay Good <hi@claygood.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 20:37:22 +00:00
e60ff53644 update Kimi CLI to Kimi Code (#1208)
* update Kimi CLI to Kimi Code

* feat(migration): migrate OpenSpec skills from legacy .kimi to .kimi-code

Renaming the Kimi skillsDir stranded OpenSpec-managed skills under
.kimi/skills: update and cleanup only inspect current AI_TOOLS paths, so
old installs would never be detected or refreshed again. Add a legacy
skillsDir migration (run by init and update before tool detection) that
moves openspec-* skill directories to .kimi-code/skills, preserves user
files, and removes the legacy directories only when empty. Keep .kimi as
a detection path and cover the migration with focused init and update
tests.

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

* docs(specs): update cli-init Kimi scenario to .kimi-code

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

---------

Co-authored-by: Clay Good <hi@claygood.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 20:23:39 +00:00
Clay GoodandClaude Fable 5 d423a594f9 fix(update): warn when a custom profile is missing core workflows (#1354)
* fix(update): warn when a custom profile is missing core workflows

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

* fix(update): use singular pronoun when one core workflow is missing

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 20:17:34 +00:00
Clay GoodandClaude Fable 5 5199f41a5d feat(stores): set one default store for every repo on your machine (#1363)
* feat(stores): add global defaultStore fallback for root resolution

Adds a machine-level `defaultStore` to the global config. When no --store
flag, local planning root, or project-level `store:` pointer resolves, root
resolution now consults `defaultStore` before erroring — so users who plan
many code repos into one store can set it once instead of editing every
repo's openspec/config.yaml.

Purely additive: existing precedence (--store > local root > project pointer)
is unchanged; the fallback only replaces the failure path. A stale or
unregistered defaultStore degrades to the existing error, reshaped to point
at clearing the global default.

Closes #1359

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

* feat(stores): report distinct global_default root provenance

The machine-level defaultStore fallback resolved with source
'declared', so status, context, and doctor JSON could not tell a
global default from a repo's store: pointer (review feedback).
Add 'global_default' to OpenSpecRootSource, resolve the fallback
with it, and cover the status, context, and doctor JSON surfaces
plus the agent contract and store docs. Add the missing changeset.

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-17 20:11:33 +00:00
Clay GoodandClaude Fable 5 7958924e95 fix(archive): stop failing on specs that were already synced before archiving (#1376)
* fix(archive): treat already-synced ADDED requirements as a no-op

Archiving a change whose specs were synced to the baseline first (the
early-sync pattern from the sync workflow) failed with 'ADDED failed -
already exists'. An ADDED requirement that already exists in the target
spec with identical content is now skipped; differing content still
aborts as a genuine conflict. Fixes #1332.

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

* Restore pnpm-lock.yaml from main (accidental v6 rewrite during merge)

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 20:04:19 +00:00
Clay GoodandClaude Opus 4.8 3fdd2f2f7b fix(specs): discover nested spec paths recursively across parse, apply, and archive (#1355)
* fix(specs): discover nested spec paths recursively across parse, apply, and archive

Delta discovery previously read only specs/<name>/spec.md one directory
level below a change's specs root, so nested layouts like
specs/<area>/<capability>/spec.md were silently skipped: show reported
deltaCount 0, and archive/apply completed without merging the delta into
the main specs directory. Main-spec discovery had the same one-level
assumption, so nested capabilities were also invisible to list, show,
view, and validate.

Introduce a shared recursive discoverSpecFiles() helper and use it in the
change parser, findSpecUpdates (apply/sync/archive), archive's delta
detection, and main-spec discovery (item-discovery, list, spec list,
view). Capability ids are the directory path relative to the specs root,
forward-slash separated on every platform, and apply/archive preserve the
relative path when writing the target spec. Symlinks are not followed and
dot-directories are skipped.

Fixes #1353

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

* fix(specs): surface non-ENOENT errors in spec discovery

discoverSpecFiles swallowed every readdir error, so an unreadable
(EACCES/EIO) capability directory silently vanished from validate/show/
archive/apply — recreating the data-loss class #1353 is closing, now on
the merge path. Suppress only the expected missing-root ENOENT and rethrow
everything else. Adds regression tests for non-ENOENT (ENOTDIR + guarded
EACCES) and the documented symlink-not-followed behavior.

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

* fix(specs): sort discovered specs by code point, not locale

localeCompare follows the process's ICU locale, so ordering could vary by
OS/CI. Code-point comparison guarantees the deterministic output the
docstring promises.

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 19:41:39 +00:00
Clay GoodandClaude Fable 5 de78c31ffd fix(templates): re-read dependency artifacts from disk before creating the next one (#1368)
The continue/propose/ff workflow instructions said to "read dependency
files for context," but an agent that already saw those files earlier in
the session treats them as read and regenerates downstream artifacts
from its stale in-context copy. Editing spec.md and deleting
design.md/tasks.md to regenerate them silently produced artifacts based
on the pre-edit spec.

The step guidance, the guardrails, and the `openspec instructions`
dependency block now say explicitly: re-read dependency files from disk
even if seen earlier in the conversation, because the user may have
edited them.

Discussion: https://github.com/Fission-AI/OpenSpec/discussions/909

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 19:34:55 +00:00
Clay GoodandClaude Opus 4.8 15ef3bcf31 fix(templates): use store-aware root for main specs in sync/archive (#1360)
The sync-specs and archive-change workflow instructions hardcoded
`openspec/specs/<capability>/spec.md` for main specs, assuming they always
live in the current repository. With `--store <id>` the change and its main
specs belong to the selected store, so sync could write the repo's specs
instead of the store's, and archive could report specs as already synced
based on the wrong location.

Derive the main-spec path from the store-aware `planningHome.root` the CLI
already returns, and stop labeling CLI-returned delta paths "repo-local"
since they may belong to a store. Repo-local behavior is unchanged.

Fixes #1358

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-17 18:12:24 +00:00
Clay GoodandClaude Fable 5 a313bf1bfe fix(schemas): resolve blocking open questions instead of deferring them to design.md (#1366)
The design instruction told agents to end design.md with an Open Questions
section but never said what to do with those questions, so blocking decisions
flowed silently into tasks and implementation. Now the design instruction
scopes the section to safely deferrable unknowns and tells the agent to ask
the user about anything that would change the specs, approach, or tasks; the
tasks instruction adds the matching check before writing the task list.

Discussion: https://github.com/Fission-AI/OpenSpec/discussions/1296

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 18:06:22 +00:00
Clay GoodandClaude Fable 5 4fdb2a5f08 fix(schemas): include spec content guidance from concepts docs in specs instructions (#1326)
Closes #1289

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 17:58:41 +00:00
Javier GomezandCursor 18cbf5d32f fix(parser): ignore fenced code blocks when parsing delta specs (#1151)
* fix(parser): ignore fenced code blocks when parsing delta specs

Requirement headers, delta section headers, scenarios and REMOVED/RENAMED
entries written inside fenced code blocks were parsed as real content by
the delta-spec parser. A fenced `### Requirement:` example became a phantom
requirement, producing spurious `validate` errors and risking incorrect
`archive` output.

Fence detection was duplicated across MarkdownParser and spec-structure but
missing entirely from requirement-blocks (which powers both validate and
archive). Extract a single shared `buildCodeFenceMask` helper and make the
delta-spec parser and validator block helpers honor it, so all parsers
treat fenced code consistently.

Co-authored-by: Cursor <cursoragent@cursor.com>

* test(validator): cover fenced-only scenario headers in delta specs

Add a regression test asserting that a `#### Scenario:` appearing only
inside a fenced code block does not count toward the required scenario
count, so the validator still reports the missing-scenario error.

This guards the fence awareness of `countScenarios()`: the existing
fenced-example test always includes a real (unfenced) scenario, so it
would not catch a regression that began counting fenced scenario
headers. Addresses the review suggestion on PR #1151.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-17 17:46:17 +00:00
Clay GoodandClaude Opus 4.8 285dfd7d76 fix(config): stop warning about rules keys that belong to another schema (#1377)
The global rules: map is validated against only the current change's schema,
so a key valid for a different schema prints a spurious "Unknown artifact ID"
warning on every command in multi-schema projects. Validate against the union
of artifact IDs across all available schemas; warn only when a key matches no
schema.

Fixes #1322.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-17 17:40:23 +00:00
Clay GoodandClaude Fable 5 52a8bce1fd fix(cli): let --change find change names that exist on disk (#1375)
* fix(cli): let --change find change names that exist on disk

The --change flag on status and instructions validated names with the
creation-time kebab-case rule, so digit-leading names (e.g. the
date-prefixed convention 2026-07-04-voice-copilot-v1) were rejected at
parse time even though list, validate, and archive all handle them.

Lookup now only guards against unsafe directory names (path separators,
relative segments, null bytes, hidden entries) and otherwise accepts
whatever getAvailableChanges could return. Creating a change still
enforces kebab-case via validateChangeName.

Fixes #1308

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

* fix(cli): reject the reserved 'archive' name in --change lookup

Matches the getAvailableChanges filter so --change archive can't address
the archive directory as if it were a change (CodeRabbit review).

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 17:27:23 +00:00
Clay GoodandClaude Fable 5 f58b445692 fix(completion): install the right completions for fish users (#1364)
* fix(completion): detect the interactive shell from the parent process

`openspec completion install` read only $SHELL, the login shell, so users
whose interactive shell differs (e.g. fish users on distros where the login
shell is bash) got bash completions installed by default (#1197).

Detection now consults the parent process via `ps` before falling back to
$SHELL. It only trusts a parent that maps to a supported shell, so npx/npm
and other non-shell parents still fall back cleanly; Windows is unaffected.

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

* fix(completion): match shell basename exactly and pin platform in parent-process tests

Two fixes from CI and review on #1364:

- matchSupportedShell now matches the executable basename exactly
  (stripping a login-shell leading dash) instead of substring matching,
  so parents like fish-lsp or bash-language-server no longer get
  mistaken for the shell (CodeRabbit review).
- The parent-process tests pin process.platform to linux so they
  exercise the ps path on Windows CI, where detection otherwise
  short-circuits and the tests failed.

Adds regression tests for -zsh login shells and fish-lsp fallback.

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-17 17:20:35 +00:00
Clay GoodandClaude Fable 5 da3907b8a9 fix(completion): stop emitting empty switch blocks that break the PowerShell script (#1374)
An empty switch body is a parse error in PowerShell, and the generator
emitted one for every command whose positionals are all path-typed
(18 in the current registry). PowerShell parses the entire file before
execution, so the whole completion script failed to load. Skip the
positional-index block when no positional produces completions.

Fixes #1293

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 16:58:56 +00:00
Clay GoodandClaude Fable 5 924354b726 docs(readme): show what a spec actually looks like in "See it in action" (#1365)
Answers discussion #1024: the README walkthrough showed the workflow
creating specs/ but never the content of a spec. Add a collapsible
example spec delta right after the transcript, plus links to this
repo's own live openspec/specs and openspec/changes as real examples.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 16:52:20 +00:00
Tabish Bidiwale 0a99f41045 Deploy docs through Cloudflare Pages (#1342)
* Deploy docs with Cloudflare Pages

* Harden docs routing worker
2026-07-10 15:46:44 +00:00
Tabish Bidiwale 3f02c686c5 chore: add OpenSpec release skill (#1341)
* chore: add OpenSpec release skill

* fix: address release skill review
2026-07-10 14:56:31 +00:00
openspec-release-bot[bot]andgithub-actions[bot] e1b51d111a Version Packages (#1295)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-07-10 13:05:54 +00:00
Tabish Bidiwale 15527310f9 chore: add missing v1.6.0 changeset (#1340) 2026-07-10 12:55:40 +00:00
Tabish Bidiwale 93e27a755c fix empty store registration (#1328) 2026-07-08 14:52:31 +00:00
8e9e457c05 ci(release): add beta prerelease workflow (#1327)
* ci(release): add beta prerelease workflow

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

* fix: harden beta release workflow

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: TabishB <tabishbidiwale@gmail.com>
2026-07-08 14:34:30 +00:00
Tabish Bidiwale 296ecbc20a Fix Windows CI flake hardening (#1325)
* fix windows ci test flake hardening

* restore required test check status
2026-07-08 11:37:27 +00:00
Tabish Bidiwale 871dece1be chore: remove scheduled docs workflow (#1324) 2026-07-08 10:15:18 +00:00
8886e3ae22 feat: add Oh My Pi (OMP) tool support (#1276)
* feat: add Oh My Pi (OMP) tool support

Add ToolCommandAdapter for Oh My Pi terminal AI coding agent.

- New adapter: src/core/command-generation/adapters/oh-my-pi.ts
  - Commands: .omp/commands/opsx-<id>.md with description frontmatter
  - Hyphen transform: /opsx: -> /opsx- (filename = command name)
  - Argument injection: **Provided arguments**: $@ after **Input**: heading
  - escapeYamlValue applied to description field
- Register in CommandAdapterRegistry and adapters/index.ts
- Add oh-my-pi to AI_TOOLS with skillsDir: '.omp'
- Add to hyphen command transformer whitelist in init.ts and update.ts
- Full test coverage (10 cases) in adapters.test.ts
- Update docs/supported-tools.md with directory reference and tool ID

Closes #713

* fix: address CodeRabbit nitpicks

- Move ohMyPiAdapter import before opencodeAdapter (alphabetical order)
- Break long SHALL sentence and remove redundant 'follows after' in spec

* docs: polish Oh My Pi support

* docs: address Oh My Pi review nits

---------

Co-authored-by: TabishB <tabishbidiwale@gmail.com>
Co-authored-by: Tabish Bidiwale <30385142+TabishB@users.noreply.github.com>
2026-07-07 18:03:24 +00:00
3f0ca3f6ce feat: add Trae command adapter (#1090)
* feat(tools): add Trae command adapter

- Added Trae command adapter for generating `.trae/commands/opsx-<id>.md` files
- Complete unit tests (9 test cases) and integration tests
- Updated documentation and .gitignore
- Fixed YAML escaping for carriage returns (\r)

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

* fix: handle empty string in YAML escaping

- Add explicit check for empty string in escapeYamlValue
- Return quoted empty string '""' instead of unquoted empty scalar
- Update test to verify empty string is properly quoted

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

* fix: address PR review feedback for Trae adapter

- Update docs/commands.md Trae entry to reflect generated opsx-* commands
- Export traeAdapter from adapters/index.ts

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

* docs: align Trae command adapter docs

---------

Co-authored-by: jjxyxsjr <jjxyxsjr@users.noreply.github.com>
Co-authored-by: Claude Code <noreply@anthropic.com>
Co-authored-by: Tabish Bidiwale <30385142+TabishB@users.noreply.github.com>
Co-authored-by: TabishB <tabishbidiwale@gmail.com>
2026-07-07 17:51:42 +00:00
Tabish Bidiwale 8ac624b279 chore: remove stale npm lockfile (#1319)
* chore: remove stale npm lockfile

* ci: use package manager metadata for pnpm setup

* chore: scope npm lockfile ignore to root
2026-07-07 17:08:25 +00:00
Ercan Erdoğan 4ef0761080 docs: clarify change name format (#1261) 2026-07-07 16:46:56 +00:00
zhangsan582 7e21cc59ef fix archive scenario drift for #1246 (#1252)
* fix archive scenario drift for #1246

* fix archive scenario drift for #1246

* remove local openspec change docs
2026-07-07 16:46:47 +00:00
Clay GoodandClaude Opus 4.8 a5bfedafc8 feat(skills): auto-approve the openspec CLI in generated skills and commands (#1300)
* feat(skills): auto-approve the openspec CLI in generated skills

Emit `allowed-tools: Bash(openspec:*)` in every generated SKILL.md so
agents that honor the Agent Skills standard run `openspec` commands
without prompting on each call. Scope is limited to the CLI; per the
standard `allowed-tools` pre-approves rather than restricts, so every
other tool a skill uses stays available under the user's normal
permission settings.

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

* feat(commands): auto-approve the openspec CLI in Claude slash commands

Extend the allowed-tools pre-approval to the second surface: Claude Code
/opsx:* slash commands share the skill frontmatter contract, so the
Claude command adapter now emits `allowed-tools: Bash(openspec:*)` too.
The value is single-sourced in `src/core/shared/allowed-tools.ts` (a
leaf module both surfaces import). Other command adapters are unchanged
— no other tool's slash-command format defines a per-command
pre-approval field; on the skills side every tool already gets the
standard field via generateSkillContent and non-implementing tools
ignore the unknown key.

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-07 16:31:00 +00:00
9a0dfb5cd1 refactor: unify requirement reader and surface #498 (#1281)
* docs(openspec): propose spec parser reading fidelity (fixes #361, #498, #312)

The requirement-parsing layer silently misreads valid Markdown:

- #361: requirement-body extraction returns only the first non-blank line,
  so a SHALL/MUST that wraps onto line 2 fails `validate --strict`.
- #498: `validate` (delta-block parser) and `archive` (full-spec parser)
  recognize requirements by different rules, so a stray `###` header passes
  validate but becomes a phantom requirement that blocks archive.
- #312 (residual): the requirement-body loop breaks on any `#` line without
  consulting the code-fence mask, truncating bodies that contain fenced
  code with `#` comments.

Proposal: one shared, multi-line, fence-aware requirement-body extractor used
by both the validator and the markdown parser; recognize only
`### Requirement:`-prefixed level-3 headers; guarantee validate/archive parity.
Adds regression + parity tests. #559 investigated and deferred (ambiguous root
cause — see design.md).

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

* docs(openspec): bulletproof parser-fidelity proposal with empirical evidence

Hardened the proposal after reproducing every claim against main with the
bundled CLI and correcting two inaccuracies:

- #498 reframed: archive does NOT hard-fail. validate passes; archive emits
  NON-BLOCKING phantom "Proposal warnings in proposal.md" because
  validateChange/parseRequirements counts every level-3 header as a
  requirement, while the delta-block parser (validate) and specs-apply
  (rebuild) only recognize canonical `### Requirement:`. It is a consistency
  bug, not data loss. Verified the rebuilt spec is clean.
- #312 reframed: the original repro is already fixed by codeFenceLineMask
  (requirement count verified correct). The residual is a regression hazard:
  the body loop is fence-unaware, harmless only while first-line-only, so the
  multi-line fix must be fence-aware from the start.

Also: unify recognition on the canonical REQUIREMENT_HEADER_REGEX
(/^###\s*Requirement:\s*(.+)$/i, case-insensitive); surfaced a third latent
inconsistency (Zod substring includes('SHALL') vs delta word-boundary
\b(SHALL|MUST)\b) and added a single-predicate requirement; verified zero
non-Requirement level-3 headers in repo specs (CI-safe); added edge-case
scenarios (multi-line spec+delta paths, fenced scenario-looking lines,
REMOVED/RENAMED unaffected, display vs detection); replaced broken relative
links with plain paths. Proposal passes `openspec validate --strict`.

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

* docs(openspec): deepen parser-fidelity proposal — add #418, upgrade #312, tier the risk

Second adversarial bulletproofing pass (reproduced everything against main):

- Add #418 (metadata-before-description): live on the spec path
  (req.text = "**ID**: ...") but ALREADY fixed on the delta path. The
  asymmetry is direct evidence for unifying the two extractors.
- Upgrade #312 from "regression hazard" to LIVE bug: a fenced code block
  before the prose line makes req.text = "```bash" on both paths today
  (distinct from the already-fixed section-count manifestation).
- Tier the fixes by risk after auditing the existing test contract
  (markdown-parser.test.ts, 15 tests green on main):
    Tier 1 (false-negative fixes #361/#418/#312): only widens what is read;
      updates one test (:331, which asserts the first-line bug). Fence tests
      (:106/:139) preserved because skip-and-join keeps SHALL-first bodies.
    Tier 2 (recognition tightening #498): canonical ### Requirement: only;
      a deliberate behavior change that updates bare-header tests (:258/:310)
      and needs a migration note. Flagged for maintainer decision, with a
      conservative opt-in-lint alternative documented.
- Surface the four-column extractor divergence table (capture / metadata /
  recognition / predicate) and an explicit "Behavior changes and test impact"
  section with exact test line refs.

Proposal passes `openspec validate --strict`. Does not claim #1156 (PR #1280).

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

* docs(openspec): third pass — reject recognition tightening, add fenced-scenario bug, #498→safe INFO

Third deep pass found the prior Tier 2 (recognition tightening to
`### Requirement:`) was the WRONG fix and over-scoped:

- Bare `### <statement>` headers are a SUPPORTED, tested requirement format:
  test/core/validation.test.ts asserts a bare-header spec is valid, and bare
  headers appear across json-converter/archive/spec tests and tmp-init
  fixtures. Tightening would break a large test surface and silently drop
  requirements from real specs. REJECTED, with evidence documented.
- Replace the #498 fix with a SAFE INFO note in validate <change> that surfaces
  non-`### Requirement:` headers in delta sections. INFO never fails validation
  (strict: valid = no errors && no warnings), so nothing newly fails.
- New bug found and folded in: countScenarios is fence-unaware, so a `####
  Scenario:` inside a fenced block is counted as real — a malformed delta passes
  validate <change> while validate <spec> correctly fails. Same fence family.
- Proved the archive WRITE path is independent of the reader: specs-apply
  rebuilds from raw `### Requirement:` blocks (extractRequirementsSection +
  RequirementBlock.raw), never parseSpec/req.text → Part A cannot change
  archived content.

Net effect: recognition is unchanged, so the proposal now updates exactly ONE
existing test (:331, the first-line assertion) instead of breaking bare-header
tests. Consolidated to a single cli-validate delta (dropped cli-archive and
openspec-conventions deltas). Dropped the no-space-header hypothesis (no
divergence). Passes `openspec validate --strict`.

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

* fix(parser): unify the requirement reader, fence/metadata/multi-line aware (#361, #418, #312); surface #498

The requirement reader was implemented twice — MarkdownParser.parseRequirements
(validate <spec>/archive) and Validator.extractRequirementText/countScenarios
(validate <change>) — and the two had drifted. Both now delegate to one shared,
fence-/metadata-/multi-line-aware extraction in parsers/requirement-text.ts so
they cannot diverge again.

Part A — unify the reader:
- Capture the full requirement body up to the first non-fenced `#### Scenario:`,
  skipping blank, `**metadata**:`, and fenced-code lines; run SHALL/MUST
  detection over the whole body. Fixes a wrapped keyword being dropped (#361),
  metadata before the description failing validate <spec> (#418), and a fenced
  block before the prose line becoming the requirement text (#312).
- Count only non-fenced `#### ` headers, so a `#### Scenario:` inside a fenced
  example no longer counts as a real scenario in validate <change> (parity with
  validate <spec>).
- One whole-word `\b(SHALL|MUST)\b` predicate (containsShallOrMust) shared by the
  validator and base.schema, replacing the substring/word-boundary split.
- Extract buildCodeFenceMask into the shared module; MarkdownParser and
  ChangeParser import it (single fence implementation).

Part B — surface #498 safely:
- validate <change> emits an INFO note when an ADDED/MODIFIED Requirements
  section contains a non-`### Requirement:` level-3 header (one the delta reader
  silently skips). INFO never changes the valid result, including under --strict,
  so nothing newly fails. Recognition is unchanged: bare `### <statement>`
  headers remain a supported requirement format.

Write path is unaffected: specs-apply rebuilds from raw `### Requirement:`
blocks, never req.text, so archived content cannot change. Displayed text in JSON
output and delta descriptions now reflects the full body.

Tests: markdown-parser.test.ts:331 updated to expect the full body; regression
tests added for #361/#418/#312, the fenced scenario, the #498 INFO note, a
single-line guard, and CRLF. Changeset added (patch). tasks.md completed.

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

* test(parser): add cross-reader predicate + metadata-only guards (design edge cases)

Exhaustive verification of the unified reader surfaced two design "edge cases
for tests" not yet covered by committed unit tests:

- Cross-reader predicate agreement: a SHALL substring inside a word ("MARSHALL")
  is rejected identically by validate <change> and validate <spec> — proving the
  one shared whole-word predicate, and guarding against a regression to the old
  substring check.
- Metadata-only body still fails validation (no requirement text) on the delta
  path.

Behavior unchanged; tests only. Full end-to-end parity across all four spec
requirements confirmed against the real Validator; no spurious INFO note fires on
any existing repo change.

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

* fix(parser): address review — metadata-only bodies, header-bounded extraction, reader-derived INFO

- Skip **metadata**: lines only when other body text remains; a body written
  entirely as metadata (e.g. `**Constraint**: The system MUST ...`) is kept as
  the requirement text instead of being emptied (was a regression vs main).
- Move the empty-body rule into the shared reader: both paths fall back to the
  header title, so the same block cannot pass one path and fail the other.
- End body extraction at any non-fenced markdown header, restoring old-reader
  parity: a stray `### Background` divider's notes no longer satisfy the
  SHALL/MUST check.
- Replace the standalone fence-aware INFO scanner with skipped-header
  collection inside parseDeltaSpec, so the note reflects exactly what the
  reader skipped (same section boundaries, no whole-file fence mask).
- Special-case the nameless `### Requirement:` INFO message; document that the
  any-#### scenario match is deliberate spec-path parity; un-export
  REQUIREMENT_HEADER_REGEX; move the import up top.
- Soften the changeset claim and list the known remaining divergences in
  design.md.

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

* docs(openspec): record the no-space ###Requirement: divergence as a known leftover

Jun's edge (reproduced): the delta/write reader's REQUIREMENT_HEADER_REGEX
accepts `###Requirement:` with no space, but MarkdownParser.parseSections
requires whitespace (per GFM) — so a no-space requirement validates as a
change with zero INFO, syncs as-is, then fails validate <spec>. Pre-existing
on main and out of scope here (tightening the shared regex would change
write-path recognition); documented under known remaining divergences with
the follow-up options, folded together with the bullet from the merge
resolution. Corrects c63913b's 'no divergence' note.

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: TabishB <tabishbidiwale@gmail.com>
2026-07-07 16:13:47 +00:00
Clay GoodandClaude Fable 5 a70daccf0e feat(skills): propose /opsx:update planning-artifact update skill (#1278)
* docs(openspec): propose add-update-workflow — graph-driven /opsx:update + cohesive audit

Dogfooded OpenSpec proposal for the missing first-class "update" action:
a /opsx:update workflow that propagates an edit to one artifact across its
downstream dependents (targeted mode) or audits a whole change for stale/
incoherent artifacts (audit mode) — driven by the schema's artifact graph,
never hardcoded filenames, editing planning artifacts only (never code).

- artifact-graph: expose reverse-dependency queries (getDependents/getDownstream)
  + a requires-edge mtime staleness signal (the engine already builds the
  dependents map at graph.ts:98 and discards it).
- cli-artifact-workflow: surface requires/dependents/stale on `openspec status
  --json` and add a `--impact <artifact>` downstream-revisit-order selector.
- opsx-update-skill: the user-facing /opsx:update command (targeted + audit).

Supersedes the proposal-only stub add-artifact-regeneration-support. Addresses
the cluster #1188/#705/#673/#247 (closes), #694/#684/#618 (answers), and is
graph-driven to avoid the #777/#666 hardcoded-artifact-pattern bug class.
Validates clean under `openspec validate --strict`.

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

* docs(openspec): make add-update-workflow deterministic & grounded (Tabish review)

Reframe per the steer "more deterministic and grounded in reality":

- Deterministic spine: the CLI computes the impact set (which downstream
  artifacts to revisit, in build order, with paths) as a pure function of
  schema edges + filesystem. The agent only rewrites prose. Grounded in real
  APIs already present: getUnlockedArtifacts (direct dependents), getBuildOrder
  (order), resolveArtifactOutputs (paths); reverse map built at graph.ts:82-87.
- Replace fragile mtime staleness with a newline-normalized SHA-256 content
  digest (reproducible cross-platform). Drift = upstream digest vs recorded
  baseline; no baseline => "unknown", never a false positive. mtime and pure-git
  rejected with rationale; digest ledger is a separable, optional layer.
- Explicit determinism boundary decision (CLI decides files/order/drift; agent
  rewrites). Skill MUST source the file list/order from `openspec status
  --impact`, never compute it.
- Corrected all code citations to verified lines (graph.ts:82-87,
  instruction-loader.ts:366/429, status.ts); noted #1277's coverage helpers are
  not in this branch's base (coordinate, don't reuse).
- Specs updated: artifact-graph Content Digest requirement; cli status digest +
  deterministic impact ordering; skill determinism + baseline-aware audit.
  tasks add digest/determinism/cross-platform tests + optional ledger section.

Still validates clean under `openspec validate add-update-workflow --strict`.

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

* docs(openspec): harden add-update-workflow determinism; drop direct name refs

- Digest ledger tracks DIRECT upstream digests; document that transitive drift
  emerges hop-by-hop as downstream is reconciled (no transitive bookkeeping).
- Ground audit's no-baseline structural facts on signals available in this
  branch (missing/empty output, blocked/incomplete); capability-coverage is an
  add-on only when #1277's validateChangeCapabilityCoverage is present.
- Add the "update revises only existing downstream; defer not-yet-created ones
  to /opsx:continue" rule across proposal/design/specs/tasks; impact entries now
  carry existence/status.
- Note artifact-level (not file-level) granularity and that getDownstream
  terminates by the schema's acyclic guarantee.
- Remove direct personal references from the docs.

Validates clean under `openspec validate add-update-workflow --strict`; 10 deltas.

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

* docs(openspec): full issue/PR/discussion coverage + command-family design

After a comprehensive sweep of open issues, PRs, and discussions, grounded the
proposal in the complete adjacent landscape and answered the open design
questions the cluster raises:

- #783 (Cross-artifact quality review before apply) is now a primary Closes:
  it IS audit mode. Answer its open "new skill vs. extend validate" question via
  the determinism split — deterministic checks (drift/completeness/coverage) are
  CLI/validate-shaped; the semantic cross-artifact review is the skill. Added a
  skill spec scenario for the #783 patterns (scope contradiction, spec gap,
  duplication).
- Discussion #1206 ("refine proposal now?") + prior-art PR #372: official answer
  is /opsx:update.
- New design Decision 8 (command family): delineate /opsx:update from
  /opsx:clarify (#702, within-artifact), /opsx:review (#1251, plan-vs-code), and
  verify; /opsx:update consolidates update+regen+refine into one action,
  addressing skill-sprawl (#1263, #783).
- Reuse, don't reinvent: audit's empty/incomplete check reuses #1098's
  artifactOutputComplete (same outputs.ts the digest helper lives in); capability
  coverage reuses #1277's validateChangeCapabilityCoverage.
- New open questions: surface deterministic coherence in `validate` for a CI gate
  (#783-B, #829); naming reconciliation with #783's /opsx:refine.
- Confirmed add-update-command* branches are the `openspec update` tool-file
  refresh (not artifact update) — no collision.

Validates clean under --strict; 10 deltas; all relative links resolve.

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

* docs(openspec): resolve open questions to committed decisions; drift in scope

Per review steer, every open question is now a committed happy-path decision so
build-out has no dangling forks, and the deterministic drift baseline is pulled
into scope (it is what makes audit-mode drift deterministic vs. agent-guessed):

- Digest ledger IN SCOPE (design Decision 3): per-artifact DIRECT upstream
  digests in ChangeMetadataSchema, written by a deterministic `openspec status
  --record`; pre-existing changes (no baseline) degrade to drift `unknown` +
  structural checks. Generating-flow auto-recording stays optional (graceful).
- cli-artifact-workflow spec: folded drift into the digest requirement (record
  baseline / drift vs baseline / unknown-without-baseline) — stays at 10 deltas.
- opsx-update-skill spec: skill records baseline via `--record` after each
  confirmed edit, so audits clear once reconciled.
- Replaced "## Open Questions" with "## Decisions resolved": ledger in scope;
  targeted entry baseline-aware; apply stays standalone (points to update on
  drift); cross-change (#247), continue/ff de-hardcoding (#777), and validate
  CI-gate (#783-B/#829) are named follow-ups, not deferrals of the core feature;
  /opsx:update kept as the umbrella name.
- Migration Plan + Capabilities + Impact + tasks updated; status JSON gains
  `drift`, CLI gains `--record`. Re-synced with upstream main (0 behind).

Validates clean under --strict; 10 deltas; all links resolve.

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

* docs(openspec): harden add-update-workflow — close cross-OS, read-only, edge gaps

Stress-tested every claim against live source and fixed the soft spots:

- Cross-OS digest determinism (real bug): resolveArtifactOutputs (outputs.ts:34)
  sorts ABSOLUTE paths via .sort(), which differs by OS — so a multi-file glob
  artifact (specs/**/*.md) would hash differently on Windows vs POSIX. Digest now
  specified to order files by change-relative forward-slash path and hash
  relpath+content. Added spec scenarios (cross-platform glob stability; rename
  changes digest) and a cross-OS test task.
- Read-only status invariant: moved baseline recording OFF `openspec status`
  (a read command silently mutating the drift reference is a footgun) to a
  dedicated `openspec reconcile` write verb. Updated spec, skill, design, impact,
  capabilities, tasks; reconciled the "no new verb" claims.
- Edge case: missing upstream at record time is stored as an explicit `absent`
  marker so later creating it registers as drift (spec scenario added).
- Edge case: coherent change yields no edits (clean-path scenario).
- Grounding fixes: continue-change hardcoded block is duplicated (skill 103-112 +
  command 225-234) — both must be fixed in the #777 follow-up; verified no
  content-hash util exists.
- Fixed two stale claims the layered edits left: the Impact digest bullet
  (concatenation→relative-path) and the naming-boundary line.

Validates clean under --strict; 10 deltas (4+3+3), 44 scenarios; all links
resolve; re-synced with upstream main (0 behind); issue/PR/discussion sweep
re-run, no new items.

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

* docs(openspec): pin data contracts + digest forward-compat; delineate #880

Grounded the surface so an implementer builds it without guessing, and added
proportionate forward-compatibility:

- New design "Data contracts" section with exact shapes: extended ArtifactStatus
  (requires/dependents/digest/drift/driftFrom — additive to the real interface at
  instruction-loader.ts:120), the --impact response, and the `.openspec.yaml`
  baselines ledger. All additive; nothing existing changes type.
- Digest scheme tag (`sha256-relpath-v1:`) + forward-compat: drift compares only
  same-scheme digests; an unrecognized/older scheme reports `unknown` rather than
  silently mis-comparing — re-reconcile restores it. Added a cli spec scenario
  and tasks for it.
- Grounded the ledger write: there is no central change-metadata writer today
  (change-metadata/index.ts only re-exports schema), so reconcile does a safe
  read-modify-write of .openspec.yaml mirroring the store's
  parse/serialize/writeStoreMetadataState pattern (foundation.ts).
- Coverage: re-swept; folded #880 (/opsx:validate code-vs-living-specs) into the
  plan-vs-code delineation alongside #1251/#1073. Main unchanged (546224e); all
  citations still valid.

Validates clean under --strict; 10 deltas; links resolve.

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

* docs(openspec): simplify add-update-workflow to a thin /opsx:update skill

Rework per @TabishB review (PR #1278): the proposal over-built. Drop the
deterministic-spine machinery and lean on the existing status command.

- Cut the reverse-dependency graph API (getDependents/getDownstream),
  SHA-256 content digests, the .openspec.yaml baseline ledger, the
  `openspec reconcile` write op, the drift report, and `status --impact`.
  Removes the artifact-graph and cli-artifact-workflow spec deltas.
- Reframe propagation as bidirectional coherence (editing design can
  require revising proposal), not downstream-only.
- Center the feature on one thin skill over the existing
  `openspec status` / `openspec list`; design now sketches the actual
  minimal skill instruction body ("written by hand").
- v1 adds no new CLI/graph/schema code: just update-change.ts + wiring.

Validates clean: `openspec validate add-update-workflow --strict`.

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

* docs(update-workflow): pin the status path contract to existingOutputPaths

Address @alfred-openspec's review: the skill's write target was described
loosely as "resolved paths." Make it precise across proposal/design/spec/tasks:

- `openspec status --json` already returns everything the skill needs, in the
  top-level `artifactPaths` map — `resolvedOutputPath` and `existingOutputPaths`
  per artifact. No new CLI field is required.
- The skill edits `existingOutputPaths` (the concrete, glob-expanded files) and
  never writes to `resolvedOutputPath`, which for a glob artifact like
  `specs/**/*.md` remains the glob pattern rather than a real file.
- Add spec scenarios for editing a glob artifact's concrete files and for
  deferring a brand-new file under a glob artifact to `/opsx:continue`.
- Tighten the cross-platform scenario and add a template test (3.4) asserting
  the write target is `existingOutputPaths`, not a glob `resolvedOutputPath`.

Validates clean under `openspec validate add-update-workflow --strict`.

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

* docs(update-workflow): address review — default profile, next-step guidance, change-scoped naming

- Register /opsx:update in the default core profile, not expanded-only
  (maintainer call on the PR)
- Add next-step guidance: after updating, recommend /opsx:continue,
  /opsx:apply (esp. when the change was already implemented), or
  /opsx:archive — guidance only, never acted on
- Pin naming scope: skill openspec-update-change, change proposals only;
  generalizing update to other graph types is an explicit non-goal

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

* feat(skills): implement the /opsx:update skill (openspec-update-change)

Implements the approved add-update-workflow change: one thin skill over
the existing status/list commands, in the default core profile.

- new update-change.ts template (skill + command), registered across
  init, profiles, skill-generation, tool-detection, profile-sync-drift
- update joins CORE_WORKFLOWS and ALL_WORKFLOWS
- docs: opsx.md command row + usage note, commands.md reference section,
  supported-tools.md skill list
- retire the superseded add-artifact-regeneration-support stub
- template tests pin the guardrails (schema-driven ids, planning-only,
  existingOutputPaths write contract, next-step guidance); parity hashes
  regenerated; profile/init/update/config tests cover the new core set
- tasks.md checked off; validate --strict passes

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-07 16:13:36 +00:00
Danilo 5956a8e872 Fix archive exit code on validation failure (#1311)
* Fix archive exit code on validation failure

In human (non-JSON) mode, openspec archive returned exit code 0 when
validation failed and nothing was archived. The three blocking paths
in ArchiveCommand.run() printed an error message but returned null
silently, leaving process.exitCode at 0. Scripts and CI could not
distinguish a blocked archive from a successful one.

The --json path was already correct (it throws ArchiveBlockedError,
caught by printJsonFailure which sets exitCode = 1). This was an
asymmetry between the two modes for the same failure.

Set process.exitCode = 1 at the three human-mode abort points before
returning null:
  - delta-spec validation failure
  - spec rebuild failure
  - rebuilt-spec validation failure

Legitimate user cancellations (selecting no change, declining a
confirmation prompt) remain exit 0 by design.

Aligns archive with the same exit-code guarantee already approved for
apply instructions in #1250. References #498.

* Add regression test for rebuilt-spec validation exit code

Cover the third archive blocking path (spot 3): buildUpdatedSpec
succeeds but Validator.validateSpecContent rejects the rebuilt
content. Spy on validateSpecContent (same pattern as the existing
--no-validate test) to force the rebuilt spec invalid while the rest
of the flow runs for real, since this branch is otherwise defensive
and nearly unreachable — spot 1 already enforces the same
SHALL/MUST/scenario rules on the delta.

Asserts process.exitCode === 1, the failure is logged, the main spec
is left unchanged, and no archive is created.
2026-07-06 13:27:46 +00:00
65a7233f36 docs: add cloudflare documentation deployment website (#1285)
* docs(website): add Fumadocs documentation site for Cloudflare Pages

Add a self-contained marketing + documentation site under website/, built
with Fumadocs (Next.js) and configured as a static export so it deploys
directly to Cloudflare Pages with no server runtime.

What's included:
- A marketing landing page (hero, the two-folder model, the four core
  ideas, the explore→propose→apply→archive loop, and the "why").
- 13 documentation pages rewritten for clarity and delight: introduction,
  installation, getting started, how commands work, core concepts, the
  workflow, explore first, existing projects, editing a change,
  customization, FAQ, and a reference section (slash commands, CLI,
  supported tools).
- Static client-side search (Orama), per-page Open Graph images, and
  llms.txt / llms-full.txt routes — fitting for an AI-native tool.
- website/README.md with one-table Cloudflare Pages deploy settings
  (root: website, build: npm run build, output: out).

Content is faithful to the docs/ overhaul from #1237, restated in a
simpler, friendlier voice. Verified with a clean `next build` (48 static
pages, no warnings).

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

* docs(website): sharpen the sell, add a Stores guide

Completes the documentation work begun in #1237 by tightening the
Fumadocs site toward the quality bar of the stores user-guide:

- Intro now opens problem-first ("the requirements lived only in chat"),
  adds an honest "How it compares" table (Spec Kit / Kiro / nothing), and
  frames the tradeoff in a "When the ceremony isn't worth it" callout.
- New Stores guide (beta) distilled from docs/stores-beta/user-guide.md:
  the problem, the annotated shape, a five-minute walkthrough with real
  command output, a role-based story, the root-resolution order, and an
  honest-limitations section. Linked from Existing Projects.

Verified with a clean `next build` (51 static pages, no warnings).

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

* docs(website): make the value tangible — landing sections + Examples page

Continue the #1237 docs completion with a stronger product story:

- Landing page now reads like a real product site:
  - "Works with the tools you already use" strip (15 named assistants + more)
  - "What a change actually looks like" — three real artifacts
    (proposal.md, a spec delta, tasks.md) so the workflow is concrete
  - "The honest middle" comparison block (Spec Kit / Kiro / no specs)
  - Robust hero gradient via color-mix instead of v3 theme() syntax
- New Examples & Recipes page: seven copy-pasteable, narrated walkthroughs
  (small feature, bug fix, explore-first, parallel changes, no-behavior
  refactor with --skip-specs, step-by-step, onboard). Linked from the intro
  and getting-started.

Verified: clean `next build` (54 static pages, no warnings); Tailwind
opacity/color-mix utilities confirmed in the generated CSS; all internal
links resolve.

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

* docs(website): add favicon, sitemap, and robots for a complete public site

- Branded SVG favicon (app/icon.svg) in the OpenSpec indigo.
- Static sitemap.xml covering the home page and every doc, built from the
  content source and NEXT_PUBLIC_SITE_URL.
- robots.txt allowing all and pointing at the sitemap.

All three are emitted by the static export. Clean `next build`, 57 pages.

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

* docs: lead with stores as "why teams adopt OpenSpec"; complete docs coverage

Final pass completing the #1237 documentation work.

Reposition stores (beta) as the team adoption story, consistently:
- README.md gains a prominent "Why teams adopt OpenSpec" section right
  after the demo (cross-repo features, shared requirements, plan before
  code), leading with stores.
- Landing page gains a matching "Why teams adopt OpenSpec" section.
- Docs intro gains a teams card + callout pointing at stores.
- Stores page expanded with full References and Worksets technical
  examples (the cross-team requirements story, workset create/open).

Incorporate the remaining source-doc knowledge so the site is complete:
- New pages: Glossary, Troubleshooting, Multi-Language, and an
  Agents & Automation reference (the machine-readable --json surfaces and
  workflow primitives that make OpenSpec AI-native).
- The Workflow page now covers ff-vs-continue, a three-dimension verify
  example, and the update-vs-start-fresh decision guide.
- Nav restructured with a Help section; reference section gains Agents.

Build hardening: `build` now runs `fumadocs-mdx && next build` so the
content source is always regenerated. Clean build: 69 static pages, no
warnings; all internal links verified.

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

* docs(website): fix docs GitHub source links + address review nits

- page.tsx: prefix ViewOptionsPopover githubUrl with website/ so the
  "view/edit source" links resolve to website/content/docs/... instead
  of 404-ing on every deployed docs page (Alfred blocker).
- installation.mdx: note that `yarn global add` is Classic Yarn only and
  point Yarn Berry users at `yarn dlx` / npm / pnpm.
- index.mdx: label the comparison table's first column ("Option").
- (home)/page.tsx: use the shared docsRoute constant for all /docs links
  instead of hardcoded paths.

Verified with `npm run build` in website/ — 69 static pages, and the
built getting-started page links to blob/main/website/content/docs/...

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

* docs(website): mirror docs/*.md into the site + auto-deploy on a cadence

Make the repository's docs/*.md the single source of truth for the docs
site instead of maintaining a parallel set of hand-written MDX pages that
silently drift.

- scripts/sync-docs.mjs mirrors ../docs into content/docs/ on every build:
  derives title/description, injects Fumadocs frontmatter (+ githubSource),
  rewrites internal *.md links to /docs routes, and emits meta.json. Pages
  are written as .md so <placeholders>/{braces} in the docs stay literal
  and never break the MDX build.
- docs.sync.config.mjs is the one manifest deciding which docs publish and
  their slug/section/icon. content/docs/ is now generated + git-ignored;
  the curated .mdx pages are removed. The marketing landing page stays
  hand-authored.
- build/dev/types:check run sync:docs first, so the site is always current.
- .github/workflows/deploy-docs.yml rebuilds and deploys to Cloudflare
  Pages via Wrangler on push to docs/**|website/**, daily on a schedule,
  on demand, and as a build-only check on PRs. Needs CLOUDFLARE_API_TOKEN
  + CLOUDFLARE_ACCOUNT_ID secrets and the DOCS_SITE_URL variable.
- source.config.ts carries githubSource so "edit this page" opens the real
  docs/*.md; website/README.md documents the pipeline.

Verified: clean build, 23 pages generated, 78 static pages, no warnings;
all internal doc links resolve; MDX-hazard docs (cli, customization) build.

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

* fix(website): fall back to default site URL when NEXT_PUBLIC_SITE_URL is empty

The deploy workflow passes NEXT_PUBLIC_SITE_URL from the DOCS_SITE_URL repo
variable, which resolves to an empty string when unset. `?? fallback` does
not catch '' (only null/undefined), so `metadataBase: new URL('')` crashed
`next build` with ERR_INVALID_URL while collecting page data. Use `||` so an
empty value also falls back. Verified: `NEXT_PUBLIC_SITE_URL='' npm run build`
now generates all 78 static pages.

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

* docs: add reviewing, writing-specs, and team-workflow guides

Fill the biggest gaps a new user hits, in the plain-language voice of the
stores user guide:

- reviewing-changes.md: the two-minute human review of an AI-drafted plan
  before /opsx:apply — what to open, in what order, and the red flags per
  artifact — plus the /opsx:verify pass after code.
- writing-specs.md: what a strong requirement and scenario are made of,
  choosing ADDED/MODIFIED/REMOVED, and right-sizing a change.
- team-workflow.md: how a change maps onto a branch and a pull request,
  reviewing spec deltas in a PR, when to archive, and parallel changes —
  framed as convention, since OpenSpec never touches git.

Wire them into the docs map (README), the site nav (docs.sync.config.mjs),
and light "next steps" cross-links from getting-started, editing-changes,
and workflows. Verified: site builds clean, 26 pages, all internal links
resolve.

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

* docs(website): add one-time deploy setup checklist + landing-page note

Spell out the three maintainer steps that activate auto-deploy (create the
openspec-docs Pages project, add CLOUDFLARE_API_TOKEN/ACCOUNT_ID secrets,
merge to main), and note that the pipeline mirrors docs on build regardless.
Also flag that openspec.dev is a separate Astro landing page and whether to
keep/port this Fumadocs landing page is a maintainer decision.

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

* fix(website): address review feedback on docs-site PR

Maintainer review (TabishB) + Alfred blocker:

- deploy-docs.yml: guard the Cloudflare deploy on `github.ref ==
  refs/heads/main`. A `workflow_dispatch` on a feature branch previously
  passed the guard and, since wrangler hardcodes `--branch=main` (a
  production deploy), would overwrite the live docs site. Non-main
  dispatches are now build-only. Also resolves Alfred's deploy-path blocker.
- package.json: drop the direct `cnfast` dependency and delete the dead
  `lib/cn.ts` (nothing imports it; a class-merge helper isn't used).
- package.json: declare `zod` (^4.4.3) — it was a phantom dep only
  resolving via fumadocs-mdx's hoisted copy. Refresh the lockfile.
- docs page: omit the on-page <DocsDescription>. The frontmatter
  description is derived from the first body paragraph, so it rendered
  the intro twice on every page. Kept in generateMetadata for SEO/OG.
- team-workflow.md: `openspec store create` does an initial commit, so
  scope "never commits" to the user's project and reframe the store
  clause as "never clones or syncs on its own."
- README.md: bump stale "20+ AI assistants" to "30+" to match the site.

Verified: npm run types:check + npm run build pass, 26 docs synced,
intro paragraph now renders once per page.

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

* chore(website): use pnpm to match the rest of the repo

Per maintainer review (TabishB): the root repo is pnpm (ci.yml runs
`pnpm install --frozen-lockfile` against a v9 `pnpm-lock.yaml`), but
`website/` had introduced npm + a `package-lock.json`. Standardize on
one package manager:

- Replace website/package-lock.json with website/pnpm-lock.yaml
  (lockfileVersion 9.0, generated with pnpm v9 to match root).
- deploy-docs.yml: add pnpm/action-setup@v4 (version 9, before
  setup-node, as in ci.yml), switch setup-node to `cache: pnpm` /
  `cache-dependency-path: website/pnpm-lock.yaml`, and
  `npm ci` → `pnpm install --frozen-lockfile`, `npm run build` →
  `pnpm run build`.
- package.json scripts + README: `npm run ...` → `pnpm run ...`.

website/ stays a standalone package (no pnpm-workspace.yaml), as before.

Verified: `pnpm install --frozen-lockfile`, `pnpm run build`, and
`pnpm run types:check` all pass — 26 docs synced, 87/87 static pages.

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

* chore: temporarily disable docs deploy

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Tabish Bidiwale <30385142+TabishB@users.noreply.github.com>
Co-authored-by: TabishB <tabishbidiwale@gmail.com>
2026-07-03 14:21:42 +00:00
Clay GoodandClaude Opus 4.8 a3253051ea fix(resolution): converge validate, view, and archive onto canonical resolution (#1182, #1202, #1156) (#1280)
* docs(openspec): propose resolution/validation parity bug bundle (#1182, #1202, #1156)

Planning artifacts only (proposal/design/spec deltas/tasks) for a focused
bug-fix bundle. Three read/validate paths silently diverge from the canonical
logic a sibling command already gets right:

- #1182 validate ignores workspace planning homes that status/instructions resolve
- #1202 view counts only changes/<name>/tasks.md, ignoring the schema tasks glob
- #1156 the SHALL/MUST body-keyword hint fires for deltas but not main specs

Fix converges each divergent path onto the canonical one; parity is asserted by
test. No new surface, no behavior change to the already-correct paths. Validates
--strict.

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

* docs(openspec): bulletproof the parity bundle after adversarial source review

Hardened all three bugs after tracing each path to source with parallel
verification agents. Material corrections:

- #1182: reframed from "workspace planning home resolution" (planning homes are
  repo-only; the feature is now 'stores', and validate already accepts --store)
  to the real, reproducible-at-HEAD mechanism: validate's proposal.md membership
  gate (getActiveChangeIds) vs status/instructions' directory-existence rule
  (validateChangeExists). Pulled nested specs/<area>/<cap> delta discovery and
  bulk --all into scope; noted show.ts sibling.
- #1202: widened from view-only to the shared helper's real blast radius — also
  the archive incomplete-task gate (silently archives unfinished glob-tasks
  changes: data safety) and a 2nd hardcoded copy in change.ts. Pinned apply.tracks
  as the source, change-dir scope containment, and the no-schema fallback. Added
  cli-archive delta for the gate.
- #1156: the main-spec parser discards the requirement header before Zod runs, so
  the hint can't be "lifted" — fix needs header recovery (reuse requirement-blocks)
  + Zod de-dup, and the main-spec message can't be byte-identical to the delta's
  (no ADDED prefix). Pinned the actionable sentence + single-emission + regression
  scenarios across all main-spec surfaces.

4 deltas (cli-validate x2, cli-view, cli-archive). Validates --strict.

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

* docs(openspec): deep-harden the parity bundle with empirical reproduction

Round 2 of bulletproofing: 3 parallel agents reproduced every bug against the
built (pre-fix) CLI and traced fix sites. This pass corrected two substantive
errors in my own prior spec and closed several gaps.

#1202 (two corrections to the prior draft):
- apply.tracks is a FILENAME that selects the tracked artifact, NOT a glob; the
  glob is that artifact's `generates`. status resolves via
  resolveArtifactOutputs(changeDir, artifact.generates). Fixed all wording.
- "view/archive counts equal status" is FALSE: status checks file EXISTENCE, not
  checkboxes (proven: status calls a 3/5 change isComplete:true). Deleted the two
  count-parity scenarios; reframed as resolution-mechanism parity (same files).
- Added schema-resolution-failure fallback (resolveSchema throws; helper must
  catch or view/list/archive crash). Added projectRoot param + 6-site wiring.
- Empirically PROVEN data-safety bug: archive moved a 3/5 unfinished change into
  changes/archive/.

#1182:
- Found a THIRD getActiveChangeIds site (interactive selector, validate.ts:97).
- Proven: --all with a lone proposal-less change exits 0 silently. Added
  exit-code scenarios. Trimmed over-scope: getSpecIds spec-side is NOT a bug;
  no store-specific scenario needed; noun-form scoped out.

#1156:
- Refine-relaxation regression resolved: deltas don't use the Zod refine
  (validate imperatively), so REMOVE it (not relax) once applySpecRules owns both
  header-only and no-keyword cases. Added RENAMED (out-of-scope), lowercase, and
  the new no-body-line-valid-today scenarios; pinned exact message + prefix.

Still 4 deltas; validates --strict; empirical evidence section added to design.

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

* fix: converge validate/view/archive onto canonical resolution (#1182, #1202, #1156)

Implements the resolution/validation parity bug bundle planned in
openspec/changes/fix-validate-view-resolution-parity. Each fix points a
divergent read/validate path at the canonical implementation a sibling
command already gets right, with parity tests guarding against re-forking.

#1182 — validate resolves changes like status. validate now resolves a
change by directory existence (shared getAvailableChanges) instead of
requiring proposal.md, at all three sites (targeted, bulk, interactive
selector). A scaffolded/still-authoring change is validated rather than
reported Unknown item; a resolved-but-invalid change exits non-zero.
show.ts and the deprecated noun-form change validate are scoped out.

#1182b — validateChangeDeltaSpecs recurses the nested multi-area layout
(specs/<area>/<capability>/spec.md) via a new findDeltaSpecFiles walker,
so a resolved multi-area change validates its deltas instead of reporting
"No delta sections found".

#1202 — getTaskProgressForChange resolves task progress through the
tracked-tasks artifact's generates glob (the same resolveArtifactOutputs
status uses), aggregating checkboxes across every matched tasks.md scoped
to the change dir, with a never-throw fallback to a single top-level
tasks.md. Updates all four callers (view/list/archive x2) for the new
projectRoot arg and folds the second copy in change.ts onto the helper.
Fixes view's Draft misclassification and the archive incomplete-task
gate that let an unfinished glob-tasks change archive (data safety).

#1156 — the SHALL/MUST body-keyword hint applies to main specs.
applySpecRules recovers the requirement header via extractRequirementsSection
and emits the targeted hint (header-only) or generic message (no keyword),
exactly once; the Zod refine is removed (deltas never used it). The
actionable sentence is byte-identical to the change-delta path.

Adds parity/regression tests (Decision 7): validate<->status resolution
incl. exit code, view/archive resolve the same files as status, and the
main-spec<->delta actionable-sentence parity. Full suite green (1791
passed; only the pre-existing, environment-specific zsh-installer
failures remain). Change validates --strict; all 36 repo specs pass
--specs --strict with no new false positives.

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 08:00:44 +00:00
394 changed files with 35710 additions and 8309 deletions
+180
View File
@@ -0,0 +1,180 @@
---
name: release-openspec
description: >-
Use this skill when releasing OpenSpec: audit merged work and changeset
coverage, decide whether a catch-up changeset PR is needed, prepare or resume
the Changesets Version Packages PR, cut a beta or stable release, verify
publishing, and polish GitHub release notes. Also use when asked whether an
open release PR is complete, what the next release step is, or to continue a
release paused for human approval.
---
# Release OpenSpec
Run the OpenSpec release workflow as a resumable state machine. Inspect live GitHub state on every invocation and take only the next safe action. Do not assume an earlier invocation completed.
## Principles
- Treat `Fission-AI/OpenSpec` and `origin/main` as the release source of truth.
- Default to a read-only audit when the user asks for status, readiness, or advice.
- Treat a request to release, prepare a release, continue, or resume as authorization to perform the applicable release actions.
- Preserve the user's checkout. Never discard unrelated changes or switch their current branch just to prepare a changeset.
- Use a temporary worktree from current `origin/main` for release-authored commits when the checkout is dirty or not on `main`.
- Never approve your own PR. Human review is a deliberate gate.
- Treat merge-queue entry as an intermediate state, not a merge. Advance only after GitHub reports `mergedAt` and the commit is present on `main`.
- Never create the automated Version Packages PR manually. The Changesets action owns it.
- Never push an empty commit merely to retrigger CI. Diagnose the failed or missing run first.
- Report URLs, the state reached, and the exact human action needed whenever pausing.
## Know the two PR types
Keep these distinct in output and decisions:
- **Changeset PR**: A normal human-authored PR that adds one or more `.changeset/*.md` files. Prefer adding a changeset to the feature/fix PR; create a catch-up changeset PR only for already-merged work that should be included.
- **Version Packages PR**: The automated `changeset-release/main` PR titled `chore(release): version packages`. Merging or adding changesets to `main` updates this same PR. Merging it publishes the stable release.
An open Version Packages PR does not prohibit a catch-up changeset PR. It means a catch-up PR is useful only when the audit finds missing release-worthy work. Once that PR merges, wait for the existing Version Packages PR to update.
## Start with a release audit
1. Verify the repository and tools:
- Resolve the GitHub repository with `gh repo view --json nameWithOwner,url`.
- Require authenticated `gh`, `git`, and `pnpm` before write actions.
- Stop before release mutations if the canonical repository is not `Fission-AI/OpenSpec`.
2. Refresh without modifying the worktree:
```bash
git fetch origin main
```
Do not fetch every tag indiscriminately. This repository may contain a conflicting historical local tag, which can make `git fetch --tags` fail even though `origin/main` fetched successfully.
3. Find the latest stable GitHub release. Exclude drafts and prereleases; do not use `git describe`, because a beta tag may be newer than the stable baseline.
```bash
gh release list --repo Fission-AI/OpenSpec \
--exclude-drafts --exclude-pre-releases --limit 100 \
--json tagName,publishedAt \
--jq 'max_by(.publishedAt) | {tagName, publishedAt}'
```
Ensure that exact stable tag resolves locally before using it as a `git log` boundary. Fetch only that tag if it is missing. If a same-named local tag disagrees with the canonical remote, report the mismatch and use a separately resolved canonical commit; never force-rewrite the user's tag as part of an audit.
4. Find open release-related PRs:
```bash
gh pr list --repo Fission-AI/OpenSpec --state open \
--head changeset-release/main \
--json number,title,headRefName,baseRefName,url,reviewDecision,statusCheckRollup
```
Identify the Version Packages PR by `headRefName == "changeset-release/main"`, not title alone. Separately list likely changeset PRs and inspect their files; require positive additions to `.changeset/*.md`. Do not mistake the Version Packages PR's changeset deletions for authored changesets, and do not rely on titles because a feature/fix PR may add release tracking.
5. Read the live release policy in `.changeset/README.md`, pending `.changeset/*.md` files on `origin/main`, and the Version Packages PR body/files when it exists.
6. List first-parent commits since the latest stable tag:
```bash
git log --first-parent --date=short \
--pretty=format:'%h%x09%ad%x09%s' <stable-tag>..origin/main
```
7. Map release-worthy merged PRs to existing changesets. Use PR files and changeset history; do not infer coverage from similar wording alone.
8. Classify the audit as:
- `missing-tracking`: user-facing work intended for this release lacks a changeset;
- `awaiting-changeset-review`: a suitable changeset PR already exists;
- `awaiting-merge-queue`: an approved changeset or Version Packages PR is queued but has not landed on `main`;
- `awaiting-version-update`: required changesets are on `main`, but the Version Packages PR has not incorporated them;
- `awaiting-version-review`: the Version Packages PR is current but lacks approval;
- `ready-to-publish`: the Version Packages PR is current, approved, and green;
- `publishing`: the Version Packages PR merged but artifacts are incomplete;
- `needs-finalization`: npm, tag, and GitHub Release exist but notes are still raw;
- `complete`: package, tag, GitHub Release, and polished notes agree.
Present a compact audit with the stable baseline, proposed version, covered changes, possible omissions, intentionally skipped internal/docs work, open PRs, and next action.
## Decide changeset coverage
Follow `.changeset/README.md` rather than assuming every merged PR needs a changeset.
Include work selected for release tracking, especially:
- new user-facing features or commands;
- notable fixes or hotfixes;
- breaking changes or deprecations;
- user-visible performance improvements.
Normally skip documentation-only work, tests, CI/tooling, and internal refactors. Flag ambiguous user-visible changes instead of silently excluding them. Ask the user only when the ambiguity materially changes release scope or the semantic version; otherwise use best judgment and let PR review be the approval gate.
## Create or continue a changeset PR
Do this only for `missing-tracking`.
1. If an open changeset PR already covers the missing work, reuse it. Inspect its `headRefName`, head repository, and `maintainerCanModify`; fetch that exact head branch from its owning repository into a temporary worktree, make the update there, and push back to the same PR head. Stop if the branch is not writable. Do not create a duplicate PR or replacement branch.
2. Read `.changeset/README.md` immediately before authoring.
3. Only when no suitable PR exists, create a short `changeset-<scope>` branch from current `origin/main`. Use a temporary worktree so the operator's checkout remains untouched.
4. Prefer one changeset per coherent release unit. A single catch-up changeset may summarize several small items selected for the same release.
5. Use the exact package name `"@fission-ai/openspec"`, the highest required semantic bump, only relevant headings, and user-focused descriptions.
6. Validate before pushing:
```bash
pnpm exec changeset status
```
7. Commit, push, and open a PR whose body lists the covered merged PRs and explains why the catch-up is needed.
8. Stop after returning the PR URL and request human approval. Do not approve it yourself.
On a later invocation, if the PR is approved and checks are green, merge or enqueue it only when the user asked to continue or complete the release. If GitHub uses a merge queue, inspect `mergeQueueEntry`, queue checks, and `mergedAt`; remain in `awaiting-merge-queue` until the PR actually lands on `main`. Then wait for the Changesets action on `main` to update the existing Version Packages PR. Poll with concise progress updates; do not push an empty commit or another branch update, because that can dismiss approval and restart the queue.
## Validate the Version Packages PR
Before calling it ready:
1. Confirm it targets `main` from `changeset-release/main` and is generated by the expected automation.
2. Enumerate every pending `.changeset/*.md` file on current `main`, excluding `.changeset/README.md`. Verify the PR consumes every one and contains the corresponding changelog content. If any pending changeset should be deferred, stop: remove or revise it through a separately reviewed change and wait for automation to regenerate the Version Packages PR before continuing.
3. Fetch `baseRefOid` and `headRefOid` with `gh pr view`, require `baseRefOid` to equal current `origin/main`, and create clean detached temporary worktrees for both revisions. If the head object is missing locally, fetch the immutable `pull/<number>/head` ref first. Never validate from the operator's current worktree.
4. In the base worktree, run `pnpm exec changeset status --output changeset-status.json` and read the expected package/version from that file. Install locked dependencies in the temporary worktree first if the Changesets CLI is unavailable.
5. Compare the base status and complete pending-changeset set against the head worktree: `package.json`, `CHANGELOG.md`, removed changeset files, PR body, and proposed version must all agree. This is a base-to-head comparison because the head has already consumed the changesets and cannot calculate the pending release itself.
6. Remove the temporary worktrees after validation, then inspect all required checks and review state with `gh pr view` / `gh pr checks`.
If current but unapproved, return the URL and pause for human approval. If approved and green, merge or enqueue only when the user asked to release or continue. With merge queue enabled, do not treat approval, auto-merge enablement, or queue entry as the stable publish trigger; wait for `mergedAt` and confirmation that the merge reached `main`.
## Verify stable publishing
After the Version Packages PR merges:
1. Find the release workflow run for the merge commit and wait for completion.
2. Verify all three artifacts independently:
- `npm view @fission-ai/openspec@<version> version`
- remote tag `v<version>` points at the expected commit;
- `gh release view v<version>` exists and is not a prerelease.
3. If only some artifacts exist, report partial state and resume verification before retrying any publish action. Never republish a version already on npm.
4. Once all artifacts exist, read [references/release-notes.md](references/release-notes.md), polish the GitHub Release, and verify the saved title/body.
## Cut a beta
Only enter this path when the user explicitly asks for a beta or prerelease.
1. Run the same audit and confirm pending changesets produce a next stable version.
2. Explain that beta publishing does not consume changesets or replace the stable Version Packages PR.
3. Trigger the existing `release-prepare.yml` workflow on `main`; do not calculate or set the beta version locally.
4. Verify the workflow-selected version, npm `beta` dist-tag, remote tag, and prerelease GitHub Release.
5. Do not merge the stable Version Packages PR as part of a beta request.
## Handle failures
- For failed CI, inspect the failing check and logs before proposing a rerun or code change.
- For a stale Version Packages PR, first confirm a successful `push` run of `release-prepare.yml` occurred after the latest changeset reached `main`.
- For branch divergence, let the Changesets action update its branch. Do not force-push `changeset-release/main`.
- For a queued PR, inspect merge-group checks and queue state. Do not re-enqueue, update the branch, or rerun unrelated checks while it is progressing normally.
- For a version that already exists on npm, stop and reconcile the tag/GitHub Release rather than incrementing or republishing implicitly.
- For missing GitHub permissions or required review, report the exact gate and URL; preserve the detected state so the next invocation can resume by inspection.
## Completion report
Report:
- released version and stable/beta channel;
- changeset PR and Version Packages PR URLs, when applicable;
- release workflow result;
- npm package, tag, and GitHub Release verification;
- release-notes finalization status;
- any intentionally deferred changes.
@@ -0,0 +1,4 @@
interface:
display_name: "Release OpenSpec"
short_description: "Audit, prepare, publish, and finalize releases"
default_prompt: "Use $release-openspec to audit the current release state and take the next safe release step."
@@ -0,0 +1,89 @@
# GitHub release notes
Read this file only after the npm package, tag, and GitHub Release exist, or when the user explicitly asks to preview or polish release notes.
## Gather source material
1. Bind the release values once and fetch the current release. Replace the example values, but keep every expansion quoted:
```bash
tag="vX.Y.Z"
previous_tag="vA.B.C"
gh release view "$tag" --repo Fission-AI/OpenSpec \
--json body,name,isPrerelease,url
```
2. For a stable release, find the preceding stable release by excluding drafts and prereleases. For a beta, compare against the preceding tag in the same beta series when one exists; otherwise compare against the latest stable release.
3. Fetch GitHub-generated notes to recover first-time contributor attribution and the full changelog link:
```bash
gh api repos/Fission-AI/OpenSpec/releases/generate-notes \
-f "tag_name=$tag" -f "previous_tag_name=$previous_tag" -q '.body'
```
4. Cross-check the final content against the released `CHANGELOG.md` section and the merged Version Packages PR. Never invent an item from commit titles alone.
## Title
Use:
```text
<tag> - <one-to-four-word theme>
```
Lead with the most notable user-facing addition. For two similarly important additions, comma-separate them. For a fix-only release, name the primary fixed area.
## Body
Use only the sections that contain content:
```markdown
## What's New in <tag>
<One direct sentence describing the release theme.>
### New
- **Feature** - What users can now do and when it helps.
### Improved
- **Area** - What became easier, safer, faster, or more consistent.
### Fixed
- **Area** - What now behaves correctly.
## New Contributors
* @username made their first contribution in #PR
**Full Changelog**: <compare-link>
```
## Voice and cleanup
- Write for developers using OpenSpec with AI coding assistants.
- Be direct and practical; avoid marketing language.
- Lead with user capability or impact, not implementation.
- Keep each item to one or two sentences.
- Remove commit hashes, changeset wrappers, raw semantic-bump headings, and inline `Thanks @user` boilerplate.
- Omit internal CI, test, and refactor details unless users experience the result.
- Keep contribution credit in `New Contributors`, not inside feature bullets.
- Preserve GitHub's first-contribution wording and PR link.
- Exclude core maintainer `@TabishB` from `New Contributors`. If no external first-time contributors remain, omit that section.
- Always retain the full changelog compare link.
## Apply and verify
Create a temporary file, write the body to it with the available file-editing tool, bind the final title, then update:
```bash
notes_file="$(mktemp)"
title="$tag - Release Theme"
# Write the polished Markdown body to "$notes_file" before continuing.
gh release edit "$tag" --repo Fission-AI/OpenSpec \
--title "$title" --notes-file "$notes_file"
```
When the user asked only for a preview or audit, show the proposed title/body without editing. When the user asked to run, continue, or complete the release, apply the polished notes without an extra confirmation pause, then fetch the release again and verify the saved title/body.
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "OpenSpec Development",
"image": "mcr.microsoft.com/devcontainers/typescript-node:1-20-bookworm",
"image": "mcr.microsoft.com/devcontainers/typescript-node:1-22-bookworm",
// Additional tools and features
"features": {
+4
View File
@@ -0,0 +1,4 @@
# The skills.sh distribution files are generated LF-only and compared
# byte-for-byte by test/core/templates/skillssh-parity.test.ts. Force LF on
# checkout so Windows autocrlf doesn't turn them into CRLF and fail parity.
skills/** text eol=lf
+1 -1
View File
@@ -1,2 +1,2 @@
# Default code ownership
* @TabishB
* @Fission-AI/openspec-maintainers
+75
View File
@@ -0,0 +1,75 @@
version: 2
updates:
# Published CLI package
- package-ecosystem: npm
directory: /
schedule:
interval: weekly
day: monday
# Let a freshly published version sit before adopting it. Security updates
# ignore the cooldown, so this only delays routine bumps — long enough for a
# compromised release to be yanked before it reaches this repo.
cooldown:
default-days: 7
semver-major-days: 30
semver-minor-days: 7
semver-patch-days: 3
open-pull-requests-limit: 5
commit-message:
prefix: chore
include: scope
groups:
production-dependencies:
dependency-type: production
update-types:
- minor
- patch
development-dependencies:
dependency-type: development
update-types:
- minor
- patch
# Documentation site (not published to npm)
- package-ecosystem: npm
directory: /website
schedule:
interval: weekly
day: monday
# Let a freshly published version sit before adopting it. Security updates
# ignore the cooldown, so this only delays routine bumps — long enough for a
# compromised release to be yanked before it reaches this repo.
cooldown:
default-days: 7
semver-major-days: 30
semver-minor-days: 7
semver-patch-days: 3
open-pull-requests-limit: 3
commit-message:
prefix: chore
include: scope
groups:
website-dependencies:
patterns:
- "*"
update-types:
- minor
- patch
# CI workflow actions
- package-ecosystem: github-actions
directory: /
schedule:
interval: weekly
day: monday
# Actions are not semver-versioned the way packages are, so this ecosystem
# accepts default-days only.
cooldown:
default-days: 7
commit-message:
prefix: ci
groups:
github-actions:
patterns:
- "*"
+47 -65
View File
@@ -25,10 +25,12 @@ jobs:
nix: ${{ steps.filter.outputs.nix }}
steps:
- name: Checkout code
uses: actions/checkout@v4
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Check for Nix-related changes
uses: dorny/paths-filter@v3
uses: dorny/paths-filter@7b450fff21473bca461d4b92ce414b9d0420d706 # v4
id: filter
with:
filters: |
@@ -40,50 +42,11 @@ jobs:
- 'scripts/update-flake.sh'
- '.github/workflows/ci.yml'
test_pr:
name: Test
runs-on: ubuntu-latest
timeout-minutes: 10
if: github.event_name == 'pull_request' || github.event_name == 'merge_group'
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: 9
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20.19.0'
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Build project
run: pnpm run build
- name: Run tests
run: pnpm test
- name: Upload test coverage
uses: actions/upload-artifact@v4
with:
name: coverage-report-pr
path: coverage/
retention-days: 7
test_matrix:
name: Test (${{ matrix.label }})
runs-on: ${{ matrix.os }}
timeout-minutes: 15
if: github.event_name == 'push' || github.event_name == 'workflow_dispatch'
if: github.event_name == 'pull_request' || github.event_name == 'merge_group' || github.event_name == 'push' || github.event_name == 'workflow_dispatch'
strategy:
fail-fast: false
matrix:
@@ -91,12 +54,15 @@ jobs:
- os: ubuntu-latest
shell: bash
label: linux-bash
vitest_workers: 4
- os: macos-latest
shell: bash
label: macos-bash
vitest_workers: 4
- os: windows-latest
shell: pwsh
label: windows-pwsh
vitest_workers: 2
defaults:
run:
@@ -104,17 +70,16 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v4
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
persist-credentials: false
- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: 9
uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6
- name: Setup Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: '20.19.0'
cache: 'pnpm'
@@ -130,30 +95,46 @@ jobs:
run: pnpm run build
- name: Run tests
env:
VITEST_MAX_WORKERS: ${{ matrix.vitest_workers }}
run: pnpm test
- name: Upload test coverage
if: matrix.os == 'ubuntu-latest'
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: coverage-report-main
name: coverage-report-${{ github.event_name }}
path: coverage/
retention-days: 7
test_pr_required:
name: Test
runs-on: ubuntu-latest
needs: [test_matrix]
if: always() && (github.event_name == 'pull_request' || github.event_name == 'merge_group')
steps:
- name: Verify matrix tests passed
run: |
if [[ "${{ needs.test_matrix.result }}" != "success" ]]; then
echo "Matrix test job failed"
exit 1
fi
echo "All matrix tests passed!"
lint:
name: Lint & Type Check
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: 9
uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6
- name: Setup Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: '20.19.0'
cache: 'pnpm'
@@ -189,13 +170,15 @@ jobs:
if: needs.changes.outputs.nix == 'true'
steps:
- name: Checkout code
uses: actions/checkout@v4
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Install Nix
uses: DeterminateSystems/nix-installer-action@v21
uses: DeterminateSystems/nix-installer-action@ef8a148080ab6020fd15196c2084a2eea5ff2d25 # v22
- name: Setup Nix cache
uses: DeterminateSystems/magic-nix-cache-action@v13
uses: DeterminateSystems/magic-nix-cache-action@908b263ff629f4cc17666315b7fd3ec127c6244d # v14
- name: Build with Nix
run: nix build
@@ -247,9 +230,10 @@ jobs:
if: github.event_name == 'pull_request' || github.event_name == 'merge_group'
steps:
- name: Checkout code
uses: actions/checkout@v4
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
persist-credentials: false
- name: Determine release tracking
id: changed-changesets
@@ -269,13 +253,11 @@ jobs:
- name: Setup pnpm
if: steps.changed-changesets.outputs.has_changesets == 'true'
uses: pnpm/action-setup@v4
with:
version: 9
uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6
- name: Setup Node.js
if: steps.changed-changesets.outputs.has_changesets == 'true'
uses: actions/setup-node@v4
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: '20.19.0'
cache: 'pnpm'
@@ -296,13 +278,13 @@ jobs:
required-checks-pr:
name: All checks passed
runs-on: ubuntu-latest
needs: [test_pr, lint, nix-flake-validate]
needs: [test_matrix, lint, nix-flake-validate]
if: always() && (github.event_name == 'pull_request' || github.event_name == 'merge_group')
steps:
- name: Verify all checks passed
run: |
if [[ "${{ needs.test_pr.result }}" != "success" ]]; then
echo "Test job failed"
if [[ "${{ needs.test_matrix.result }}" != "success" ]]; then
echo "Matrix test job failed"
exit 1
fi
if [[ "${{ needs.lint.result }}" != "success" ]]; then
+131 -9
View File
@@ -1,8 +1,9 @@
name: Release (prepare)
name: Release
on:
push:
branches: [main]
workflow_dispatch: # manually cut a beta prerelease from main
permissions:
contents: write
@@ -15,7 +16,7 @@ concurrency:
jobs:
prepare:
if: github.repository == 'Fission-AI/OpenSpec'
if: github.repository == 'Fission-AI/OpenSpec' && github.event_name == 'push'
runs-on: ubuntu-latest
steps:
# Generate GitHub App token first - used for checkout and changesets
@@ -23,21 +24,19 @@ jobs:
# (GITHUB_TOKEN cannot trigger workflows by design)
- name: Generate GitHub App Token
id: app-token
uses: actions/create-github-app-token@v2
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3
with:
app-id: ${{ vars.APP_ID }}
private-key: ${{ secrets.APP_PRIVATE_KEY }}
- uses: actions/checkout@v4
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
token: ${{ steps.app-token.outputs.token }}
- uses: pnpm/action-setup@v4
with:
version: 9
- uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6
- uses: actions/setup-node@v4
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: '24' # Node 24 includes npm 11.5.1+ required for OIDC
cache: 'pnpm'
@@ -48,7 +47,7 @@ jobs:
# Opens/updates the Version Packages PR; publishes when the Version PR merges
- name: Create/Update Version PR
id: changesets
uses: changesets/action@v1
uses: changesets/action@a45c4d594aa4e2c509dc14a9f2b3b67ba3780d0d # v1
with:
title: 'chore(release): version packages'
createGithubReleases: true
@@ -58,3 +57,126 @@ jobs:
env:
GITHUB_TOKEN: ${{ steps.app-token.outputs.token }}
# npm authentication handled via OIDC trusted publishing (no token needed)
# Manually-dispatched beta prerelease from main: version is the next stable
# release per pending changesets with a -beta.N suffix (e.g. v1.6.0-beta.1),
# published to npm under the `beta` dist-tag and posted as a prerelease-flagged
# GitHub Release. Changesets are left unconsumed, so the stable flow above is
# unaffected. This job lives in this file because npm trusted publishing
# authorizes a single workflow file per package.
#
# Users opt in with: npm install -g @fission-ai/openspec@beta
beta:
if: github.repository == 'Fission-AI/OpenSpec' && github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
- uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: '24' # Node 24 includes npm 11.5.1+ required for OIDC
cache: 'pnpm'
registry-url: 'https://registry.npmjs.org'
- run: pnpm install --frozen-lockfile
# Beta version = next stable version per pending changesets, plus a
# -beta.N suffix that increments over existing beta tags for that version.
- name: Compute beta version
id: version
env:
GH_TOKEN: ${{ github.token }}
run: |
git fetch --tags --force origin
pnpm exec changeset status --output=changeset-status.json
NEXT=$(node -p "JSON.parse(require('fs').readFileSync('changeset-status.json','utf8')).releases[0]?.newVersion ?? ''")
rm changeset-status.json
if [ -z "$NEXT" ]; then
echo "No pending changesets on main - nothing to cut a beta from."
exit 1
fi
N=1
while true; do
VERSION="${NEXT}-beta.${N}"
TAG="v${VERSION}"
TAG_EXISTS=false
NPM_EXISTS=false
RELEASE_EXISTS=false
if git rev-parse -q --verify "refs/tags/${TAG}" >/dev/null; then
TAG_EXISTS=true
fi
if npm view "@fission-ai/openspec@${VERSION}" version >/dev/null 2>&1; then
NPM_EXISTS=true
fi
if gh release view "${TAG}" >/dev/null 2>&1; then
RELEASE_EXISTS=true
fi
if [ "$TAG_EXISTS" = false ] && [ "$NPM_EXISTS" = false ] && [ "$RELEASE_EXISTS" = false ]; then
break
fi
if [ "$RELEASE_EXISTS" = false ]; then
echo "Resuming incomplete beta ${TAG}"
break
fi
N=$((N + 1))
done
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
echo "Cutting ${TAG}"
- name: Set package version
env:
VERSION: ${{ steps.version.outputs.version }}
run: npm version "$VERSION" --no-git-tag-version
# prepublishOnly runs the build. npm authentication handled via OIDC
# trusted publishing (no token needed).
- name: Publish to npm under the beta dist-tag
env:
VERSION: ${{ steps.version.outputs.version }}
run: |
if npm view "@fission-ai/openspec@${VERSION}" version >/dev/null 2>&1; then
echo "@fission-ai/openspec@${VERSION} is already on npm; skipping publish."
exit 0
fi
npm publish --tag beta
- name: Tag and create GitHub prerelease
env:
GH_TOKEN: ${{ github.token }}
VERSION: ${{ steps.version.outputs.version }}
run: |
TAG="v${VERSION}"
HEAD_SHA=$(git rev-parse HEAD)
if git rev-parse -q --verify "refs/tags/${TAG}" >/dev/null; then
TAG_SHA=$(git rev-list -n 1 "${TAG}")
if [ "$TAG_SHA" != "$HEAD_SHA" ]; then
echo "${TAG} already exists at ${TAG_SHA}, not current HEAD ${HEAD_SHA}."
exit 1
fi
else
git tag "${TAG}"
fi
if git ls-remote --exit-code --tags origin "refs/tags/${TAG}" >/dev/null 2>&1; then
echo "${TAG} already exists on origin; skipping tag push."
else
git push origin "${TAG}"
fi
if gh release view "${TAG}" >/dev/null 2>&1; then
echo "GitHub Release ${TAG} already exists; skipping release creation."
else
gh release create "${TAG}" \
--prerelease \
--generate-notes \
--title "${TAG}" \
--notes "Beta prerelease. Install with \`npm install -g @fission-ai/openspec@beta\`."
fi
+90
View File
@@ -0,0 +1,90 @@
name: Security
on:
push:
branches: [main]
paths:
- '**/package.json'
- '**/pnpm-lock.yaml'
- '.github/workflows/security.yml'
pull_request:
branches: [main]
schedule:
# Weekly, so a newly published advisory surfaces even with no commits.
- cron: '17 6 * * 1'
workflow_dispatch:
permissions:
contents: read
concurrency:
group: security-${{ github.ref }}
cancel-in-progress: true
jobs:
# Blocks a pull request that introduces a vulnerable or badly licensed dependency.
dependency-review:
name: Dependency Review
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
# No PR comment: that needs `pull-requests: write`, which a fork's token
# never gets. The failed check plus its log is the signal.
- name: Review dependency changes
uses: actions/dependency-review-action@a1d282b36b6f3519aa1f3fc636f609c47dddb294 # v5.0.0
with:
fail-on-severity: high
audit:
name: Audit
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Setup pnpm
uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6
# No dependency cache: `pnpm audit` reads the lockfile, nothing is installed,
# so a cache-save step would fail on the missing store path.
- name: Setup Node.js
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: '20.19.0'
# Advisory on pull requests: a newly published advisory should not stop an
# unrelated change, and the step depends on registry availability.
# Blocking everywhere else — on the weekly schedule and on pushes to main
# — so a high-severity advisory in a shipped dependency still fails a run
# even when no dependency changed.
- name: Audit published dependencies
continue-on-error: ${{ github.event_name == 'pull_request' }}
run: pnpm audit --prod --audit-level high
# Build and test tooling never reaches an installed copy of OpenSpec, so an
# advisory here is a scheduled-update item.
- name: Audit build and test tooling
continue-on-error: true
run: pnpm audit --audit-level high
# The docs site keeps its own lockfile and is not a workspace member, so
# neither audit above can see it. Without this step a website advisory is
# invisible — which is how two of them sat open long enough to need a
# manual override.
#
# Same blocking rule as the published-dependency audit: advisory on pull
# requests, blocking on the weekly schedule and on pushes to main. Green
# here has to mean the site is clean, or the step just relocates the blind
# spot into a passing log. `!cancelled()` because the two audits above can
# fail hard, and a root advisory must not silently skip this one.
- name: Audit documentation site
if: ${{ !cancelled() }}
continue-on-error: ${{ github.event_name == 'pull_request' }}
run: pnpm audit --audit-level high --dir website
+7
View File
@@ -148,6 +148,7 @@ CLAUDE.md
# Pnpm
.pnpm-store/
/package-lock.json
result
# OpenCode
@@ -159,3 +160,9 @@ opencode.json
# Bob
.bob/
# Trae
.trae/
# Cursor
.cursor/
+297
View File
@@ -1,5 +1,302 @@
# @fission-ai/openspec
## 1.7.0
### Minor Changes
- [#1475](https://github.com/Fission-AI/OpenSpec/pull/1475) [`17af60c`](https://github.com/Fission-AI/OpenSpec/commit/17af60c66e4c049e3986fdbafcdc16b202cda59f) Thanks [@clay-good](https://github.com/clay-good)! - Add CodeArts Agent skills support: `openspec init --tools codeartsagent` installs the workflow skills.
- [#1475](https://github.com/Fission-AI/OpenSpec/pull/1475) [`17af60c`](https://github.com/Fission-AI/OpenSpec/commit/17af60c66e4c049e3986fdbafcdc16b202cda59f) Thanks [@clay-good](https://github.com/clay-good)! - Add Hermes Agent as a supported AI tool: `openspec init --tools hermes` installs the workflow skills (Hermes is skills-only and invokes them directly).
- [#1475](https://github.com/Fission-AI/OpenSpec/pull/1475) [`17af60c`](https://github.com/Fission-AI/OpenSpec/commit/17af60c66e4c049e3986fdbafcdc16b202cda59f) Thanks [@clay-good](https://github.com/clay-good)! - Add ZCode as a supported AI tool: `openspec init --tools zcode` generates its skills and `/opsx:*` commands.
- [#1475](https://github.com/Fission-AI/OpenSpec/pull/1475) [`17af60c`](https://github.com/Fission-AI/OpenSpec/commit/17af60c66e4c049e3986fdbafcdc16b202cda59f) Thanks [@clay-good](https://github.com/clay-good)! - Codex is now skills-only: workflows install as `$openspec-*` skills and previously managed custom prompts are retired (existing ones are cleaned up on update).
- [#1062](https://github.com/Fission-AI/OpenSpec/pull/1062) [`eac2973`](https://github.com/Fission-AI/OpenSpec/commit/eac2973819037727b10214f70db2f54d82f2d891) Thanks [@showms](https://github.com/showms)! - Add current project context and per-operation guidance to apply and archive workflows. Projects can configure `operations.apply.guidance` and `operations.archive.guidance`; `openspec instructions apply` returns apply inputs, and the new read-only `openspec instructions archive` surface returns archive inputs for the selected root.
Archive, bulk archive, and sync skills now load current archive inputs and `specs` artifact rules at execution time, fail before writes or moves when required instruction lookups fail, and reuse specs-rule snapshots during inline sync.
- [#1475](https://github.com/Fission-AI/OpenSpec/pull/1475) [`17af60c`](https://github.com/Fission-AI/OpenSpec/commit/17af60c66e4c049e3986fdbafcdc16b202cda59f) Thanks [@clay-good](https://github.com/clay-good)! - Publish the workflow skills as static `skills/<name>/SKILL.md` files so `npx skills add Fission-AI/OpenSpec` works.
- [#1399](https://github.com/Fission-AI/OpenSpec/pull/1399) [`27b22ab`](https://github.com/Fission-AI/OpenSpec/commit/27b22ab4cbf530fa00e17f0f6b75a44d56777542) Thanks [@clay-good](https://github.com/clay-good)! - Add `skip_specs: true` change metadata for work with no spec-level behavior change (pure refactors, tooling, docs). `openspec validate` accepts a zero-delta change that declares the marker (honored only when the metadata parses under the shared change-metadata schema and names a schema that loads) and errors when the marker and delta specs are both present, the artifact graph no longer blocks `tasks` on spec files for such changes, `openspec status` renders the specs stage as explicitly skipped, and the propose/specs guidance points to the marker instead of contradicting the validator.
- [#1475](https://github.com/Fission-AI/OpenSpec/pull/1475) [`17af60c`](https://github.com/Fission-AI/OpenSpec/commit/17af60c66e4c049e3986fdbafcdc16b202cda59f) Thanks [@clay-good](https://github.com/clay-good)! - Resolve symlinked schema directories so schemas shared via symlink (e.g. from a dotfiles repo) are discovered.
- [#1470](https://github.com/Fission-AI/OpenSpec/pull/1470) [`6295515`](https://github.com/Fission-AI/OpenSpec/commit/6295515d4da4f7c76eaed00b7f1926771eae92de) Thanks [@clay-good](https://github.com/clay-good)! - `openspec update` now offers to upgrade the CLI when yours is behind the published one. Instruction files are generated by the installed CLI, so a stale install reported `✓ All 1 tool(s) up to date (v1.6.0)` while the workflows added in newer releases were never written:
```text
A newer OpenSpec CLI is available (v1.6.0 → v1.7.0).
Running from: /usr/local/lib/node_modules/@fission-ai/openspec
? Upgrade to v1.7.0 now? (Y/n)
```
Say yes and it upgrades, confirms the new version is the one that answers, then re-runs the update so the new workflows arrive in the same command. Say no and it prints the command matching how you installed OpenSpec, and updates with what you have. Nothing happens to your machine that you did not agree to: the offer appears only in an interactive terminal and only where `npm install -g` would help, and the check is skipped in CI or when `OPENSPEC_NO_UPDATE_CHECK`, `DO_NOT_TRACK=1`, or `OPENSPEC_TELEMETRY=0` is set.
See [CLI reference → `openspec update`](https://github.com/Fission-AI/OpenSpec/blob/main/docs/cli.md#openspec-update) for the per-install-method behavior and every opt-out.
### Patch Changes
- [#1404](https://github.com/Fission-AI/OpenSpec/pull/1404) [`a84ae70`](https://github.com/Fission-AI/OpenSpec/commit/a84ae70e8c6ef6ffaab56599d6f91fa39873e63d) Thanks [@clay-good](https://github.com/clay-good)! - Generated skills for tools without a command adapter (Kimi Code, Mistral Vibe, Hermes, ForgeCode, CodeArts) no longer reference `/opsx:*` commands that were never generated: skill cross-references, the init getting-started hint, and the profile-migration message now use each tool's documented skill invocation (Kimi Code: `/skill:openspec-*`; others: `/openspec-*`), and Codex — skills-invocable with no slash surface — gets a syntax-neutral hint that names the skill. Selections that mix invocation syntaxes print one labeled hint per distinct form, so every advertised instruction is usable by the tool it names. When `delivery: commands` would generate nothing for a selected tool, init prints a configuration correction naming that tool, even when other tools did get commands or skills. The committed skills.sh distribution is regenerated with skill references (default `/openspec-*` form, as that channel installs skills only).
- [#1363](https://github.com/Fission-AI/OpenSpec/pull/1363) [`5199f41`](https://github.com/Fission-AI/OpenSpec/commit/5199f41a5d523b9212dd2854ec5e505d2f80e2e7) Thanks [@clay-good](https://github.com/clay-good)! - ### Features
- **One default store for every repo on your machine** — `openspec config set defaultStore <id>` sets a machine-level fallback root: any command run outside a planning root, with no `--store` flag and no project `store:` pointer, resolves to that store. It sits at the bottom of the precedence list, so `--store`, a local root, and a project pointer all still win. The root banner and JSON `root` block report the distinct provenance `source: "global_default"`, so users and tooling can tell a machine-wide default from a repo's own pointer. A stale id degrades to the underlying store error with a fix that names `openspec config unset defaultStore`.
- [#1435](https://github.com/Fission-AI/OpenSpec/pull/1435) [`6a5171e`](https://github.com/Fission-AI/OpenSpec/commit/6a5171e18630db4ed8e78c9edfaae4be532e2af6) Thanks [@clay-good](https://github.com/clay-good)! - `openspec new change` now accepts numeric-prefixed names like `100-add-feature` or `00001-add-auth`, useful for ordering or tiering changes. Change names now use the same kebab-case grammar as store ids and change metadata (a leading digit is allowed); `archive` already treated date-prefixed names as a supported convention. Uppercase, spaces, underscores, and leading/trailing or consecutive hyphens are still rejected, and every previously valid name stays valid.
- [#1425](https://github.com/Fission-AI/OpenSpec/pull/1425) [`040a869`](https://github.com/Fission-AI/OpenSpec/commit/040a86931f5398167137a483b2e8081aec13016e) Thanks [@clay-good](https://github.com/clay-good)! - Compare config key guards literally instead of through a helper.
`setNestedValue` and `deleteNestedValue` rejected prototype-reaching key segments through a helper that did a `Set` lookup. That is correct, but static analysis could not follow it, so CodeQL kept reporting prototype-pollution on the very assignments the guard protects. The segments are now compared literally in the same function, still checked across the whole path before anything is written. Behavior is unchanged for every input, verified against the previous implementation across 400,000 generated cases.
- [#1431](https://github.com/Fission-AI/OpenSpec/pull/1431) [`6a4f0d7`](https://github.com/Fission-AI/OpenSpec/commit/6a4f0d7f3384486132cb9c516b635c23cadc1fa2) Thanks [@clay-good](https://github.com/clay-good)! - A delta spec that introduces a brand-new capability can now open with a `## Purpose`, and `openspec archive` uses it as the Purpose of the main spec it creates instead of writing the `TBD - created by archiving change <name>. Update Purpose after archive.` placeholder over it. The `specs` artifact instruction, its example, the delta template and the `openspec-sync-specs` skill all tell authors and agents to write one, so the CLI and agent-driven sync paths produce the same main spec.
Archive keeps the placeholder when the delta has no usable `## Purpose`:
- no `## Purpose` header outside a code fence or HTML comment, or a body that is only a code fence or only a comment
- a body that would leave a spec its own parser cannot read — a heading or requirement header that truncates a section, an unterminated fence, or any HTML comment
- in the second case archive also says why, and still completes rather than aborting
A carried Purpose under 50 characters is kept but warned about, since `openspec validate --strict` reports it as too brief. The Purpose of an existing main spec is never touched; archive warns when it ignores a delta's Purpose there.
- [#1437](https://github.com/Fission-AI/OpenSpec/pull/1437) [`19d4171`](https://github.com/Fission-AI/OpenSpec/commit/19d41714c8b790488732687443713e406ef5aeef) Thanks [@clay-good](https://github.com/clay-good)! - `openspec archive` no longer aborts when a REMOVED delta's requirement is already gone from the main spec (the early-sync pattern the sync skill teaches): it warns, treats the removal as already applied, and reports applied-only totals. In `--json` mode those warnings are carried in a new optional `warnings` array on the archive result. When every operation for a spec was already synced, archive skips rewriting that file instead of churning normalization differences into it. A delta that both RENAMEs and REMOVEs the same requirement is now rejected explicitly, by both `validate` and `archive` — the two spellings are compared case- and whitespace-insensitively — and a REMOVED header that differs only in case or whitespace from an existing requirement still aborts (that is a typo, not an early sync). Also fixed: the archive delta gate matches section headers case-insensitively like the parser; symlinked `specs/<capability>/spec.md` files are discovered instead of silently dropped; `openspec show <change>` no longer prints a spurious "scenarios" flag warning; files generated for qwen and bob reference commands by their real hyphenated names (`/opsx-<id>`), and init's getting-started hint follows suit; apply/update/onboard guidance names the CLI fallback for profiles that don't install `/opsx:continue` or `/opsx:new`.
- [#1411](https://github.com/Fission-AI/OpenSpec/pull/1411) [`c439a4e`](https://github.com/Fission-AI/OpenSpec/commit/c439a4ee48ef02dcdae6ac8101b7d12924695e7e) Thanks [@clay-good](https://github.com/clay-good)! - Fix phantom requirements parsed from delta specs, which made `openspec archive` warn about problems `openspec validate` never reported.
A header inside a delta section that is not a `### Requirement:` header — a divider such as `### Documentation Requirements` — was read as a requirement with no scenario. `openspec archive` warned that it was missing a scenario, and `openspec show <change> --json` and `openspec change list` counted it as an extra delta. The change parser now ignores those headers, matching the delta reader, so the phantom is gone from the warnings and from the JSON. Main spec parsing is unchanged.
`openspec archive` also no longer repeats requirement-level issues from the delta specs in its non-blocking "Proposal warnings in proposal.md" block. Each defect was printed twice there, and a `## REMOVED Requirements` entry — names-only by design — was reported as missing a scenario on every correct removal. Delta spec validation still reports and blocks on genuine defects, and proposal-level warnings are unchanged.
- [#1394](https://github.com/Fission-AI/OpenSpec/pull/1394) [`b474f81`](https://github.com/Fission-AI/OpenSpec/commit/b474f81cb4bebbeff0e447fd78c34a613ebd02fa) Thanks [@clay-good](https://github.com/clay-good)! - ### Bug Fixes
- **Archive no longer races the spec sync, or reports a sync that never landed** — the generated `openspec-archive-change` skill (and the matching `opsx:archive` command) handed the spec sync to a background task and then moved the change folder immediately. The archive could move the delta specs out from under the running sync: the change ended up archived, `openspec/specs/` was never updated, and the summary still reported `Specs: ✓ Synced`. The sync now runs inline, and the archive only proceeds once every capability with a delta spec has been checked against it — ADDED present, MODIFIED changes applied, REMOVED gone, RENAMED under the new name and not the old. If the sync fails or a capability doesn't match, the archive stops and reports what differs instead of claiming success; nothing has moved, so you can fix it and retry.
- [#1475](https://github.com/Fission-AI/OpenSpec/pull/1475) [`17af60c`](https://github.com/Fission-AI/OpenSpec/commit/17af60c66e4c049e3986fdbafcdc16b202cda59f) Thanks [@clay-good](https://github.com/clay-good)! - Apply profile changes with the installed CLI instead of shelling out to `npx`, which could run a different version.
- [#1475](https://github.com/Fission-AI/OpenSpec/pull/1475) [`17af60c`](https://github.com/Fission-AI/OpenSpec/commit/17af60c66e4c049e3986fdbafcdc16b202cda59f) Thanks [@clay-good](https://github.com/clay-good)! - Delta and main-spec parsers strip a UTF-8 BOM, so files saved by Windows editors or PowerShell redirects no longer fail with "No delta sections found".
- [#1398](https://github.com/Fission-AI/OpenSpec/pull/1398) [`97d441a`](https://github.com/Fission-AI/OpenSpec/commit/97d441a8ee2738d3008709e61acfc91925c7ae3a) Thanks [@clay-good](https://github.com/clay-good)! - ### Bug Fixes
- **Bulk archive now stops when you pick "Cancel"** — the generated `openspec-bulk-archive-change` skill (and the matching `opsx:bulk-archive` command) offered a "Cancel" option at the confirmation prompt but never told the agent what to do with it, so the next step archived every selected change anyway. The prompt now routes each answer by intent: "Cancel" stops without archiving anything, the archive options proceed (the ready-only option archives just the changes the status table marks `Ready` or `Ready*`), and any other answer re-asks instead of archiving. The single-change archive skill already routes Cancel this way; this brings the bulk variant in line.
- [#1375](https://github.com/Fission-AI/OpenSpec/pull/1375) [`52a8bce`](https://github.com/Fission-AI/OpenSpec/commit/52a8bce1fd2bc98c51fa35cf0cfa05e799eb4404) Thanks [@clay-good](https://github.com/clay-good)! - `--change` now accepts any change name that exists on disk (e.g. date-prefixed names like `2026-07-04-voice-copilot-v1`), matching what `list`, `validate`, and `archive` already resolve. Lookup still rejects unsafe names (path separators, `..`, hidden entries); the kebab-case naming rule still applies when creating a change.
- [#1475](https://github.com/Fission-AI/OpenSpec/pull/1475) [`17af60c`](https://github.com/Fission-AI/OpenSpec/commit/17af60c66e4c049e3986fdbafcdc16b202cda59f) Thanks [@clay-good](https://github.com/clay-good)! - `openspec new change` rejects names over 200 characters with a validation message instead of surfacing a raw ENAMETOOLONG filesystem error.
- [#1447](https://github.com/Fission-AI/OpenSpec/pull/1447) [`fb19699`](https://github.com/Fission-AI/OpenSpec/commit/fb196995dad017074415a638824eb546f3321cbc) Thanks [@hsusul](https://github.com/hsusul)! - Generated tool command files now carry valid YAML frontmatter for every supported tool. Command names ship as `OPSX: Explore`, and the unquoted `name: OPSX: Explore` that adapters emitted is not parseable YAML — strict parsers rejected the whole file, so the command failed to load. Several adapters also re-implemented their own escaping, and a few interpolated descriptions in raw.
Escaping now lives in one place (`escapeYamlValue` / `formatTagsArray`) and every adapter uses it. String frontmatter values are always double-quoted, which also keeps values like `true`, `null` and `123` from round-tripping as booleans, nulls and numbers. Non-string fields such as `allowed-tools` and `invokable` are unchanged. Expect the first `openspec update` after upgrading to rewrite the frontmatter lines of your generated command files.
Archive workflow guidance also gets two corrections: bulk archive now carries its per-delta include/exclude decisions into execution, so a delta whose implementation was not found is reported as `sync skipped` instead of being synced anyway, and both archive workflows verify the main specs before moving the change directory.
- [#1471](https://github.com/Fission-AI/OpenSpec/pull/1471) [`9a937cb`](https://github.com/Fission-AI/OpenSpec/commit/9a937cb9b36fb1040bdbde3bab3fa3903944ef10) Thanks [@clay-good](https://github.com/clay-good)! - Reference slash commands by the name each tool actually registers. Command bodies, generated `SKILL.md` cross-references, and the `init`/`update`/migration hints all advertised `/opsx:<id>`, but only 7 of the 28 tools with a command adapter register that name — the ones whose files sit in an `opsx/` directory. The other 21 write `.../opsx-<id>.md`, where the filename is the command, so tools such as Cursor, GitHub Copilot, Windsurf and Kilo Code were told to type a command their palette never had; a single generated Cursor file named itself `/opsx-apply` in frontmatter and then told the reader to run `/opsx:apply`. The command _name_ is now derived from the command file each adapter writes rather than a hand-maintained tool list, so a newly added adapter cannot drift, and the _wrapper_ around it is adapter metadata: Amazon Q loads its files into a prompt library invoked with `@`, so it now gets `@opsx-<id>` in command bodies, skills, and the onboarding hint instead of a slash command it never registers. Codex, which generates no command files at all, now gets `$openspec-<skill>` — the syntax its CLI actually accepts — everywhere it previously advertised `/opsx:*`, superseding the syntax-neutral hint described in the pending `adapterless-skill-references` note. Command filenames and paths are unchanged, and Claude Code output is byte-identical.
- [#1364](https://github.com/Fission-AI/OpenSpec/pull/1364) [`f58b445`](https://github.com/Fission-AI/OpenSpec/commit/f58b4456925b6331f3e5902a1c57905afe7edbf5) Thanks [@clay-good](https://github.com/clay-good)! - Fix `openspec completion install` detecting the wrong shell for fish (and other)
users whose interactive shell differs from their login shell. Detection now
consults the parent process before falling back to `$SHELL`, so running the
command from fish installs fish completions instead of defaulting to bash.
- [#1377](https://github.com/Fission-AI/OpenSpec/pull/1377) [`285dfd7`](https://github.com/Fission-AI/OpenSpec/commit/285dfd7d764752b2a1e7e8cc843d613421e62652) Thanks [@clay-good](https://github.com/clay-good)! - ### Bug Fixes
- Config `rules:` keys are no longer reported as `Unknown artifact ID` when they belong to a different schema. The global rules map is now validated against the union of artifact IDs across every available schema, so multi-schema projects stop seeing spurious warnings on every command ([#1322](https://github.com/Fission-AI/OpenSpec/issues/1322)).
- [#1401](https://github.com/Fission-AI/OpenSpec/pull/1401) [`b33b15d`](https://github.com/Fission-AI/OpenSpec/commit/b33b15d98ae929624c991632c7382ebc234d4ca7) Thanks [@clay-good](https://github.com/clay-good)! - Stop `design.md` from restating the proposal. In the default `spec-driven` schema, the design instruction asked for "Background, current state, constraints, stakeholders" and "What this design achieves and excludes" without saying that motivation and scope already live in `proposal.md`, so agents restated the proposal's Why and What Changes instead of adding the design's own value - approach, alternatives, and trade-offs. The instruction and the design template now state the boundary explicitly (the proposal covers why and what, design covers how) and tell the agent to reference those documents rather than repeat them ([#1382](https://github.com/Fission-AI/OpenSpec/issues/1382)).
- [#1167](https://github.com/Fission-AI/OpenSpec/pull/1167) [`1637856`](https://github.com/Fission-AI/OpenSpec/commit/1637856c423f2e84457652d1ab58885fe9744fb2) Thanks [@mehdishahdoost](https://github.com/mehdishahdoost)! - **Windsurf is now Devin Desktop.** Windsurf was rebranded on June 2, 2026 and its config directory moved: `.devin/` is the preferred read + write location, `.windsurf/` a legacy read-only fallback that the Devin Local agent does not read at all. OpenSpec follows the rename rather than carrying two ids for one product — the tool id is `devin`, writing `.devin/workflows/opsx-<id>.md` and `.devin/skills/openspec-*/SKILL.md`, and it is detected from either directory.
- `--tools windsurf` still resolves, so existing setup scripts keep working; it now configures `.devin/`.
- If your OpenSpec files are still in `.windsurf/`, `openspec update` explains the rebrand and offers to move them. `--force` and non-interactive runs take the move; declining leaves every file exactly where it is. Only the files OpenSpec generates move — each skill's `SKILL.md` and commands named `opsx-*`. A hand-written Cascade workflow, a reference file you keep beside a `SKILL.md`, a command file you edited, and `.devin/rules/` all stay exactly where they are.
- Devin skills and the getting-started hint reference `/openspec-*` skills rather than `/opsx-*` workflows, because only Devin Desktop reads workflows; the `/openspec-*` form works on both agents. Workflow bodies still use `/opsx-<id>`, the name Devin registers for a workflow file.
- [#1475](https://github.com/Fission-AI/OpenSpec/pull/1475) [`17af60c`](https://github.com/Fission-AI/OpenSpec/commit/17af60c66e4c049e3986fdbafcdc16b202cda59f) Thanks [@clay-good](https://github.com/clay-good)! - `openspec doctor` now notes when a store checkout is behind its upstream ref.
- [#1475](https://github.com/Fission-AI/OpenSpec/pull/1475) [`17af60c`](https://github.com/Fission-AI/OpenSpec/commit/17af60c66e4c049e3986fdbafcdc16b202cda59f) Thanks [@clay-good](https://github.com/clay-good)! - Make the archive scenario-drift check multiplicity-aware: a MODIFIED block that keeps only one of two same-named scenarios no longer silently drops the other.
- [#1408](https://github.com/Fission-AI/OpenSpec/pull/1408) [`378d468`](https://github.com/Fission-AI/OpenSpec/commit/378d468ad348dc1e973ed30c5cfa458fb77c9de3) Thanks [@clay-good](https://github.com/clay-good)! - Explore now reads the project's context and rules from `openspec/config.yaml` (or `config.yml`) at the start of a session, so it reasons with the same tech stack and conventions the artifact-creating workflows already receive.
- [#1475](https://github.com/Fission-AI/OpenSpec/pull/1475) [`17af60c`](https://github.com/Fission-AI/OpenSpec/commit/17af60c66e4c049e3986fdbafcdc16b202cda59f) Thanks [@clay-good](https://github.com/clay-good)! - `openspec feedback` shows the formatted text and a pre-filled submission URL on any gh failure (issues disabled, network, rate limit), not only when gh is missing or unauthenticated.
- [#1396](https://github.com/Fission-AI/OpenSpec/pull/1396) [`60f720c`](https://github.com/Fission-AI/OpenSpec/commit/60f720c43acd94de7645ac8629c614ede4682b6a) Thanks [@clay-good](https://github.com/clay-good)! - Fix `openspec feedback` failing when the repository does not define the `feedback` label. The command now retries without the label and notes that it was not applied, instead of exiting with an error and discarding the feedback.
- [#1151](https://github.com/Fission-AI/OpenSpec/pull/1151) [`18cbf5d`](https://github.com/Fission-AI/OpenSpec/commit/18cbf5d32ffe1bff4fff692e24568c605cf1e0fa) Thanks [@javigomez](https://github.com/javigomez)! - ### Fixed
- Ignore Markdown structure (requirement headers, delta sections, scenarios, REMOVED/RENAMED entries) that appears inside fenced code blocks when parsing delta specs. Previously a fenced `### Requirement:` example was parsed as a real (phantom) requirement, producing spurious `validate` errors and risking incorrect `archive` output. Fenced-code detection is now shared across the Markdown parsers so `validate` and `archive` behave consistently.
- [#1475](https://github.com/Fission-AI/OpenSpec/pull/1475) [`17af60c`](https://github.com/Fission-AI/OpenSpec/commit/17af60c66e4c049e3986fdbafcdc16b202cda59f) Thanks [@clay-good](https://github.com/clay-good)! - The archive scenario-drift check now ignores `#### Scenario:` lines inside fenced code blocks, matching validate: a fenced example no longer false-aborts an archive, and a fenced name no longer masks a genuinely dropped scenario.
- [#1316](https://github.com/Fission-AI/OpenSpec/pull/1316) [`9b70481`](https://github.com/Fission-AI/OpenSpec/commit/9b70481df727ab9f7a00dd0118e4e09373a36fb9) Thanks [@mc856](https://github.com/mc856)! - ### Bug Fixes
- **`archive` no longer stacks a second date prefix** — archiving a change whose name already starts with a `YYYY-MM-DD-` prefix (a common authoring convention) keeps the name as-is instead of prepending today's date. Previously `openspec archive 2026-07-04-voice-copilot-v1 --yes` produced `2026-07-06-2026-07-04-voice-copilot-v1`, and when run on a later day the folder sorted under a day on which the change did not happen. Names without a full date prefix (including partial dates like `2026-07-feature`) are dated as before, and the naming is now idempotent.
- [#1374](https://github.com/Fission-AI/OpenSpec/pull/1374) [`da3907b`](https://github.com/Fission-AI/OpenSpec/commit/da3907b8a9170711c8b7f63e18352e8577cf7df5) Thanks [@clay-good](https://github.com/clay-good)! - fix(completion): make the PowerShell completion script parse and load again
The generated `OpenSpecCompletion.ps1` contained 18 empty `switch ($positionalIndex) { }` blocks — emitted for commands whose positionals are all `path`-typed (PowerShell completes paths natively, so those cases produce no clauses). A switch with no clauses is a PowerShell parse error ("Missing condition in switch statement clause"), and PowerShell parses the whole file before running it, so the script never loaded and completions never registered. The generator now skips the positional-index block entirely when no positional produces completions, so the script parses clean (18 → 0 errors) and tab completion works.
- [#1388](https://github.com/Fission-AI/OpenSpec/pull/1388) [`9b5d2cd`](https://github.com/Fission-AI/OpenSpec/commit/9b5d2cdd0c1aa4b1b49da4f95c6cec8d7d38b155) Thanks [@mc856](https://github.com/mc856)! - ### Bug Fixes
- **Archive workflow templates no longer teach agents to stack a second date prefix** — the `openspec-archive-change` and `openspec-bulk-archive-change` skill/command templates (and the onboarding walkthrough's archived-path example) now mirror the `openspec archive` rule: a change whose name already starts with a `YYYY-MM-DD-` prefix is archived under its own name, while other names get the current date prepended as before. Previously an agent following the workflow instructions on a change named `2026-07-04-voice-copilot-v1` produced `archive/2026-07-07-2026-07-04-voice-copilot-v1`, whatever the CLI did.
- [#1475](https://github.com/Fission-AI/OpenSpec/pull/1475) [`17af60c`](https://github.com/Fission-AI/OpenSpec/commit/17af60c66e4c049e3986fdbafcdc16b202cda59f) Thanks [@clay-good](https://github.com/clay-good)! - Gemini command files escape TOML-active characters (quotes, backslashes, control characters) in the description and prompt, so a template value containing them can no longer produce an invalid `.toml` file.
- [#1464](https://github.com/Fission-AI/OpenSpec/pull/1464) [`5bcf057`](https://github.com/Fission-AI/OpenSpec/commit/5bcf05766a70ec0163c3e700a3029b1c1da895d8) Thanks [@clay-good](https://github.com/clay-good)! - Workflow skills and commands no longer tell agents to use the Claude Code-only AskUserQuestion tool. The same templates are generated for every supported tool, and agents without that tool (OpenCode, Factory Droid, Codex, and others) errored or stalled on the instruction. The guidance is now runtime-neutral: agents are simply told to ask the user.
- [#1403](https://github.com/Fission-AI/OpenSpec/pull/1403) [`2d6c447`](https://github.com/Fission-AI/OpenSpec/commit/2d6c447100c51fb1e5f65c6f6a35ce02a3196a10) Thanks [@clay-good](https://github.com/clay-good)! - ### Bug Fixes
- **Propose and fast-forward skills no longer name the Claude-only TodoWrite tool** — the generated `openspec-propose` and `openspec-ff-change` skills (and their `/opsx:propose` / `/opsx:ff` commands) told every agent to "Use the **TodoWrite tool**", which only exists in Claude Code. Codex, Cursor, Gemini, Copilot, and the other supported tools have no such tool, so agents either errored or stalled looking for it. The instruction is now runtime-neutral ("Use a todo list to track progress"), which works everywhere — including Claude Code.
- [#1415](https://github.com/Fission-AI/OpenSpec/pull/1415) [`e2f748c`](https://github.com/Fission-AI/OpenSpec/commit/e2f748c64f05efaeac720f83c71fb6f1b6f6e18d) Thanks [@clay-good](https://github.com/clay-good)! - Reject config key paths that reach the prototype chain, and update the bundled `yaml` dependency.
`openspec config set --allow-unknown __proto__.polluted <value>` reported success and assigned onto `Object.prototype` for the rest of the process. `--allow-unknown` was meant to relax the known-key check only, but it skipped every key check, so `__proto__`, `constructor`, and `prototype` segments reached the nested-write helper. Those segments are now rejected in `config set` whether or not `--allow-unknown` is passed, and `setNestedValue` / `deleteNestedValue` refuse them regardless of caller. Ordinary keys such as `featureFlags.myFlag` behave exactly as before.
The `yaml` runtime dependency moves from 2.8.2 to 2.9.0, picking up the fix for a stack overflow on deeply nested input (GHSA / advisory patched in 2.8.3).
- [#1376](https://github.com/Fission-AI/OpenSpec/pull/1376) [`7958924`](https://github.com/Fission-AI/OpenSpec/commit/7958924e95654af981437951e967983385da8001) Thanks [@clay-good](https://github.com/clay-good)! - ### Bug Fixes
- **Archive after early sync** — `openspec archive` no longer fails with `ADDED failed … already exists` when a change's specs were already synced to the main specs before archiving (the early-sync pattern from the `sync` workflow). If an ADDED requirement already exists in the target spec with identical content, applying it is treated as a no-op; a same-named requirement with different content still aborts the archive as a genuine conflict ([#1332](https://github.com/Fission-AI/OpenSpec/issues/1332)).
- [#1386](https://github.com/Fission-AI/OpenSpec/pull/1386) [`b419e96`](https://github.com/Fission-AI/OpenSpec/commit/b419e965bbf413cc658bbac37325ebc147b1c869) Thanks [@mc856](https://github.com/mc856)! - ### Bug Fixes
- **Archive after early sync (RENAMED)** — `openspec archive` no longer fails with `RENAMED failed … source not found` when a change's renames were already synced to the main specs before archiving (the early-sync pattern from the `sync` workflow). If a RENAMED requirement's source header is gone but the target header exists in the spec, applying the rename is treated as a no-op; a rename whose source and target are both missing still aborts the archive as a genuine error, and reported counts reflect only renames actually applied.
- [#1462](https://github.com/Fission-AI/OpenSpec/pull/1462) [`ebf66c7`](https://github.com/Fission-AI/OpenSpec/commit/ebf66c7ee1df3f7465d7f480753f952483133a73) Thanks [@clay-good](https://github.com/clay-good)! - Respect reduced-motion preferences in `openspec init`: the welcome animation is skipped when the OS reduced-motion setting is on (macOS Reduce Motion, GNOME animations disabled), when `OPENSPEC_NO_ANIMATION` is set, or when the new `--no-animation` flag is passed. The static welcome screen is shown instead.
- [#1405](https://github.com/Fission-AI/OpenSpec/pull/1405) [`5dfef4b`](https://github.com/Fission-AI/OpenSpec/commit/5dfef4b00c233fbe78f40488bd4ff98f4204684c) Thanks [@clay-good](https://github.com/clay-good)! - ### Bug Fixes
- **Custom schema instructions are no longer overridden by hard-coded spec-driven patterns** — the `openspec-continue-change` skill/command embedded one-line "common artifact patterns" for proposal.md, specs, design.md, and tasks.md, so agents followed those shortcuts instead of the schema's `instruction` field whenever a custom schema reused familiar artifact names. The templates now state that the `instruction` field is the authoritative guidance, and the `propose`, `continue`, and `ff` workflows direct the agent — both in the artifact-creation step and in the guidelines — to invoke a skill when the instruction delegates artifact creation to one, verifying the artifact exists afterward (fixes [#777](https://github.com/Fission-AI/OpenSpec/issues/777)).
- [#1475](https://github.com/Fission-AI/OpenSpec/pull/1475) [`17af60c`](https://github.com/Fission-AI/OpenSpec/commit/17af60c66e4c049e3986fdbafcdc16b202cda59f) Thanks [@clay-good](https://github.com/clay-good)! - Follow the Kimi CLI rename to Kimi Code: new install paths with automatic migration of existing `.kimi` setups.
- [#1415](https://github.com/Fission-AI/OpenSpec/pull/1415) [`e2f748c`](https://github.com/Fission-AI/OpenSpec/commit/e2f748c64f05efaeac720f83c71fb6f1b6f6e18d) Thanks [@clay-good](https://github.com/clay-good)! - Parse spec headings in linear time when the title is padded with whitespace.
Building the reference index read the first Purpose line with a regex that backtracked quadratically on a heading full of spaces: 10,000 characters of padding took 60ms, and 100,000 would have taken roughly six seconds. The heading scan is now hand-rolled and linear. Behavior is unchanged — the replacement was checked against the old implementation across 303,000 generated inputs, including CommonMark closing sequences (`## Purpose ##`), seven-hash lines, and headings with no space after the hashes.
- [#1475](https://github.com/Fission-AI/OpenSpec/pull/1475) [`17af60c`](https://github.com/Fission-AI/OpenSpec/commit/17af60c66e4c049e3986fdbafcdc16b202cda59f) Thanks [@clay-good](https://github.com/clay-good)! - Use local dates for CLI date-only values (archive names, timestamps) instead of UTC, so late-evening archives no longer get tomorrow's date.
- [#1475](https://github.com/Fission-AI/OpenSpec/pull/1475) [`17af60c`](https://github.com/Fission-AI/OpenSpec/commit/17af60c66e4c049e3986fdbafcdc16b202cda59f) Thanks [@clay-good](https://github.com/clay-good)! - `openspec update` warns when a custom profile is missing core workflows instead of silently generating a partial install.
- [#1428](https://github.com/Fission-AI/OpenSpec/pull/1428) [`81d5109`](https://github.com/Fission-AI/OpenSpec/commit/81d5109b86f16537deb99f84a772a83235dc9e09) Thanks [@taltas](https://github.com/taltas)! - Update current Roo Code product references to its community successor, Zoo Code.
- [#1475](https://github.com/Fission-AI/OpenSpec/pull/1475) [`17af60c`](https://github.com/Fission-AI/OpenSpec/commit/17af60c66e4c049e3986fdbafcdc16b202cda59f) Thanks [@clay-good](https://github.com/clay-good)! - Archive treats a MODIFIED delta whose content already matches the main spec as a no-op: a fully early-synced change now reports "Specs already in sync" instead of rewriting the file and claiming modifications.
- [#1475](https://github.com/Fission-AI/OpenSpec/pull/1475) [`17af60c`](https://github.com/Fission-AI/OpenSpec/commit/17af60c66e4c049e3986fdbafcdc16b202cda59f) Thanks [@clay-good](https://github.com/clay-good)! - Render multi-select prompts with `[x]`/`[ ]` checkbox markers instead of radio-button icons.
- [#1475](https://github.com/Fission-AI/OpenSpec/pull/1475) [`17af60c`](https://github.com/Fission-AI/OpenSpec/commit/17af60c66e4c049e3986fdbafcdc16b202cda59f) Thanks [@clay-good](https://github.com/clay-good)! - Discover nested spec paths like `specs/<area>/<capability>/spec.md` recursively and consistently across parse, apply, and archive.
- [#1410](https://github.com/Fission-AI/OpenSpec/pull/1410) [`b3b05e1`](https://github.com/Fission-AI/OpenSpec/commit/b3b05e1abeb312caefd57e60be799aeb466c1d0e) Thanks [@clay-good](https://github.com/clay-good)! - Only advertise onboarding commands that will actually exist. The `openspec init` welcome screen and the `openspec update` "Getting started" summary listed `/opsx:new` and `/opsx:continue`, which the default `core` profile never generates, so users were told to run commands that did not exist. Both surfaces now list the commands for the installed workflows. The `init` and `update` completion hints also name the skill (`/openspec-propose`) instead of a command for tools that receive no command files — Codex, and any tool under skills-only delivery.
- [#1412](https://github.com/Fission-AI/OpenSpec/pull/1412) [`1dc670d`](https://github.com/Fission-AI/OpenSpec/commit/1dc670deea741b8313b8a22fb975741f84677b3f) Thanks [@clay-good](https://github.com/clay-good)! - ### Fixed
- **`/opsx:propose` and `/opsx:ff` no longer finish a change with no spec written.** The workflows listed only `proposal`/`design`/`tasks` and treated the apply phase's `tasks` artifact as the stop condition — but `status` marks an artifact `done` as soon as a matching file exists, so writing `tasks.md` early satisfied the loop while `specs/<capability>/spec.md` was never created (a spec-less change in a spec-driven tool). The loop now derives the full required set — every apply dependency plus everything it transitively `requires` — from a single `status` call, creates each missing artifact, and only skips one when its own `instruction` field marks it conditional. ([#1260](https://github.com/Fission-AI/OpenSpec/issues/1260), [#788](https://github.com/Fission-AI/OpenSpec/issues/788))
### Changed
- **`openspec status --json` now reports each artifact's `requires` edges.** Every entry in the `artifacts` array carries a `requires` array of the ids it directly depends on, present for every status (including `done`) so agents can compute the transitive required set from `status` alone. Additive and backward-compatible — existing fields are unchanged.
- [#1191](https://github.com/Fission-AI/OpenSpec/pull/1191) [`7704702`](https://github.com/Fission-AI/OpenSpec/commit/7704702d61fa71e4f553c21a06bdf8e4ee803b4a) Thanks [@mc856](https://github.com/mc856)! - Generate Markdown commands for Qwen Code instead of deprecated TOML format. Qwen Code now recommends Markdown custom commands with YAML frontmatter; the old `.qwen/commands/opsx-*.toml` files are cleaned up as legacy artifacts on update.
- [#1475](https://github.com/Fission-AI/OpenSpec/pull/1475) [`17af60c`](https://github.com/Fission-AI/OpenSpec/commit/17af60c66e4c049e3986fdbafcdc16b202cda59f) Thanks [@clay-good](https://github.com/clay-good)! - An already-synced RENAMED delta aborts when a case/whitespace variant of the source requirement still exists — the same typo guard REMOVED deltas have.
- [#1368](https://github.com/Fission-AI/OpenSpec/pull/1368) [`de78c31`](https://github.com/Fission-AI/OpenSpec/commit/de78c31ffd885a0558ae55d332f74d5485dc01c0) Thanks [@clay-good](https://github.com/clay-good)! - ### Fixes
- **Regenerated artifacts now pick up your manual edits** — the continue, propose, and fast-forward workflows (and the `openspec instructions` dependency block) now tell the agent to re-read dependency artifacts from disk before creating the next one, instead of trusting whatever version it saw earlier in the conversation. Previously, editing `spec.md` and deleting `design.md`/`tasks.md` to regenerate them could silently produce artifacts based on the stale, pre-edit content.
- [#1475](https://github.com/Fission-AI/OpenSpec/pull/1475) [`17af60c`](https://github.com/Fission-AI/OpenSpec/commit/17af60c66e4c049e3986fdbafcdc16b202cda59f) Thanks [@clay-good](https://github.com/clay-good)! - Proposal guidance now resolves blocking open questions with the user instead of deferring them to design.md.
- [#1392](https://github.com/Fission-AI/OpenSpec/pull/1392) [`a13abea`](https://github.com/Fission-AI/OpenSpec/commit/a13abeac47d419462b0193dbf9423dd466ffe6c7) Thanks [@clay-good](https://github.com/clay-good)! - ### Fixed
- Stop a delta spec written directly at a change's `specs/` root from being silently dropped. `validate` accepted `specs/spec.md` and counted its deltas, but the apply/archive merge only reads capability folders (`specs/<capability>/spec.md`), so the change could pass validation and be archived while its requirements never reached `openspec/specs/`. `validate` now uses the same discovery rules as the merge path and reports the misplaced file with a fix hint, and `archive` blocks instead of completing.
- [#1465](https://github.com/Fission-AI/OpenSpec/pull/1465) [`f917b8b`](https://github.com/Fission-AI/OpenSpec/commit/f917b8be5e1100189ef62320ba9322763053640e) Thanks [@clay-good](https://github.com/clay-good)! - Order artifacts by the schema's declaration order instead of alphabetically.
`specs` and `design` both require only `proposal`, so both become ready at once - and the tie used to be broken alphabetically, which put `design` first. `openspec status` listed design above specs and `nextSteps` recommended writing `design.md` before any spec existed, contradicting the spec-driven schema's own documented `proposal → specs → design → tasks` sequence.
Ties now follow the order the schema declares its artifacts, so `openspec status`, `status --json`, `nextSteps`, `blocked by:` lists, and an artifact's `unlocks` all agree. No dependency edges changed, so nothing newly blocks and `design.md` stays optional - only the order of equally-ready artifacts moved. Custom schemas get the same guarantee: dependency order still comes first, but wherever your schema leaves two artifacts equally ready, the order of its `artifacts:` list now decides which one the CLI recommends - so reorder that list if it was never deliberate.
- [#1446](https://github.com/Fission-AI/OpenSpec/pull/1446) [`5348da9`](https://github.com/Fission-AI/OpenSpec/commit/5348da930c4038ffd5b5a521702b71315dcd0019) Thanks [@showms](https://github.com/showms)! - ### Bug Fixes
- Preserve an existing project-local schema when `openspec schema init --force` rejects an unknown artifact ID. Forced replacement now begins only after artifact validation succeeds.
- [#1433](https://github.com/Fission-AI/OpenSpec/pull/1433) [`26f009d`](https://github.com/Fission-AI/OpenSpec/commit/26f009d940f311b99db7f310816bb166a99fb3ef) Thanks [@clay-good](https://github.com/clay-good)! - Change lookup no longer requires `proposal.md`. `openspec show`, `openspec change list/show/validate`, and shell completion now resolve a change by its directory, matching `openspec list`, `status`, `instructions`, and `validate`.
Previously a change created by `openspec new change` — which scaffolds only `.openspec.yaml` — was reported as `Unknown item` by `openspec show` and was missing from completions and `openspec change list` until a proposal was written, and a change from a schema with no proposal artifact was never resolvable. `openspec change list` now reports the same set as `openspec list`, keeps task counts for a change that has no proposal yet, and labels it `(no proposal.md yet)` rather than `(unable to read)`. Showing such a change explains that the proposal is not written yet and points at `openspec status --change <name>`.
- [#1468](https://github.com/Fission-AI/OpenSpec/pull/1468) [`fc886af`](https://github.com/Fission-AI/OpenSpec/commit/fc886af7f93068482bbf2c66fd1eb76b40c6a22f) Thanks [@clay-good](https://github.com/clay-good)! - The continue, update, verify, sync, and archive workflow skills now select a change the same way apply does: use the provided name, infer it from conversation context, auto-select when exactly one active change exists, and only prompt when the choice is genuinely ambiguous. Previously these workflows were told to always prompt ("Do NOT guess or auto-select"), so invoking them with a single active change stalled on a question with only one possible answer. The selection is always announced ("Using change: <name>") with how to override, and bulk archive still always prompts.
- [#1194](https://github.com/Fission-AI/OpenSpec/pull/1194) [`b7c85c7`](https://github.com/Fission-AI/OpenSpec/commit/b7c85c741ca56748a4ae095b573fe4550c5c977f) Thanks [@mc856](https://github.com/mc856)! - Fix skills-only delivery emitting `/opsx:*` command references. SKILL.md files generated by init, update, and workspace skill setup now reference the corresponding skills (e.g. `/openspec-apply-change`) when `delivery: 'skills'` is configured, instead of commands that were never generated.
- [#1475](https://github.com/Fission-AI/OpenSpec/pull/1475) [`17af60c`](https://github.com/Fission-AI/OpenSpec/commit/17af60c66e4c049e3986fdbafcdc16b202cda59f) Thanks [@clay-good](https://github.com/clay-good)! - Specs instructions include the spec content guidance from the concepts docs, so generated specs follow the requirement/scenario format.
- [#1475](https://github.com/Fission-AI/OpenSpec/pull/1475) [`17af60c`](https://github.com/Fission-AI/OpenSpec/commit/17af60c66e4c049e3986fdbafcdc16b202cda59f) Thanks [@clay-good](https://github.com/clay-good)! - The static welcome screen (reduced motion, `--no-animation`, narrow terminals) now waits for the Enter it asks for instead of letting the keystroke submit the tool picker unseen.
- [#1475](https://github.com/Fission-AI/OpenSpec/pull/1475) [`17af60c`](https://github.com/Fission-AI/OpenSpec/commit/17af60c66e4c049e3986fdbafcdc16b202cda59f) Thanks [@clay-good](https://github.com/clay-good)! - Sync and archive workflows resolve main specs through the store-aware root instead of assuming `openspec/specs` in the repo.
- [#1402](https://github.com/Fission-AI/OpenSpec/pull/1402) [`0da5f98`](https://github.com/Fission-AI/OpenSpec/commit/0da5f98e147543a44379e32295e2e9798d775d83) Thanks [@clay-good](https://github.com/clay-good)! - Show the main spec format in the sync-specs skill so agents stop leaving delta operation headers (`## ADDED/MODIFIED Requirements`) in `openspec/specs/` — merged main specs with those headers parse as 0 requirements in `openspec view` ([#1120](https://github.com/Fission-AI/OpenSpec/issues/1120)).
- [#1476](https://github.com/Fission-AI/OpenSpec/pull/1476) [`8731290`](https://github.com/Fission-AI/OpenSpec/commit/87312900f532c6c13ea556d4badaff2efdfa9602) Thanks [@clay-good](https://github.com/clay-good)! - Telemetry no longer depends on `posthog-node`: the single usage event is sent with a plain fetch to the same endpoint. Installing OpenSpec no longer pulls the fast-publishing `posthog-node`/`@posthog/core`/`@posthog/types` tree, which broke downstream installs under supply-chain age policies like pnpm's `minimumReleaseAge` ([#1390](https://github.com/Fission-AI/OpenSpec/issues/1390)).
- [#1475](https://github.com/Fission-AI/OpenSpec/pull/1475) [`17af60c`](https://github.com/Fission-AI/OpenSpec/commit/17af60c66e4c049e3986fdbafcdc16b202cda59f) Thanks [@clay-good](https://github.com/clay-good)! - The stale-CLI check hardens its install detection: a directory merely named `volta` no longer changes the upgrade hint, the Windows npm-ownership check corroborates against the `openspec.cmd` shim npm actually writes, and a registry redirect from https to plain http is no longer followed.
- [#1475](https://github.com/Fission-AI/OpenSpec/pull/1475) [`17af60c`](https://github.com/Fission-AI/OpenSpec/commit/17af60c66e4c049e3986fdbafcdc16b202cda59f) Thanks [@clay-good](https://github.com/clay-good)! - The stale-CLI check tears down a redirected registry connection when its time budget expires instead of leaving the socket open.
- [#1442](https://github.com/Fission-AI/OpenSpec/pull/1442) [`10fa39b`](https://github.com/Fission-AI/OpenSpec/commit/10fa39b1c3a3e88c02ae7d3053864c03a793ff47) Thanks [@hsusul](https://github.com/hsusul)! - `openspec update` now refreshes tools that are configured with command files but no skills (delivery `commands`). Previously it read the generating version only from skill files, so such a tool was reported as "up to date" forever and its command files were never regenerated after a CLI upgrade. Command files carry no version stamp, so OpenSpec compares their contents against what it would generate now — including removing a command file left behind by a workflow you have since deselected. CRLF line endings and a UTF-8 BOM are treated as checkout artifacts rather than drift, so a Windows clone does not report a spurious update.
- [#1475](https://github.com/Fission-AI/OpenSpec/pull/1475) [`17af60c`](https://github.com/Fission-AI/OpenSpec/commit/17af60c66e4c049e3986fdbafcdc16b202cda59f) Thanks [@clay-good](https://github.com/clay-good)! - `openspec update` with `delivery: commands` prints the same configuration correction as init when it removes the skills of a tool that supports only skills, instead of deleting them silently.
- [#1475](https://github.com/Fission-AI/OpenSpec/pull/1475) [`17af60c`](https://github.com/Fission-AI/OpenSpec/commit/17af60c66e4c049e3986fdbafcdc16b202cda59f) Thanks [@clay-good](https://github.com/clay-good)! - `openspec validate` reports an unreadable specs/ directory as the error it is instead of misdiagnosing it as "no deltas found".
- [#1455](https://github.com/Fission-AI/OpenSpec/pull/1455) [`6b3623a`](https://github.com/Fission-AI/OpenSpec/commit/6b3623a39e96f49995d38d642738b31f68e92039) Thanks [@c4patino](https://github.com/c4patino)! - `openspec view` now resolves the configured OpenSpec root instead of always reading the current directory, and accepts `--store <id>` like its sibling commands. Projects whose `openspec/config.yaml` points at an external store saw an empty dashboard — 0 specs, 0 requirements — while `openspec list` read the same store correctly.
- [#1475](https://github.com/Fission-AI/OpenSpec/pull/1475) [`17af60c`](https://github.com/Fission-AI/OpenSpec/commit/17af60c66e4c049e3986fdbafcdc16b202cda59f) Thanks [@clay-good](https://github.com/clay-good)! - Preserve keyboard input on Windows after the welcome screen instead of dropping the first keystrokes.
- [#1475](https://github.com/Fission-AI/OpenSpec/pull/1475) [`17af60c`](https://github.com/Fission-AI/OpenSpec/commit/17af60c66e4c049e3986fdbafcdc16b202cda59f) Thanks [@clay-good](https://github.com/clay-good)! - zsh completion install honors `$ZSH` and `$ZSH_CUSTOM`, so Oh My Zsh setups at custom locations get the completion where their shell actually loads it.
## 1.6.0
### Minor Changes
- [#1090](https://github.com/Fission-AI/OpenSpec/pull/1090) [`3f0ca3f`](https://github.com/Fission-AI/OpenSpec/commit/3f0ca3f6ce6f2ec41260c5cbe7954b7e46adcf43) Thanks [@jjxyxsjr](https://github.com/jjxyxsjr)! - ### New Features
- **TRAE command adapter** — Added command adapter for Trae IDE, enabling generation of `.trae/commands/opsx-<id>.md` files for custom slash commands
- [#1340](https://github.com/Fission-AI/OpenSpec/pull/1340) [`1552731`](https://github.com/Fission-AI/OpenSpec/commit/15527310f9be13cc9a4035ea01b93ba85873d956) Thanks [@TabishB](https://github.com/TabishB)! - ### New Features
- **Oh My Pi support** — Generate native OPSX commands and skills for Oh My Pi projects, including tool detection and the expected `.omp` directory layout.
- **Update planning artifacts in place** — Use `/opsx:update` to revise an existing change's planning artifacts, reconcile related artifacts, and keep implementation work delegated to `/opsx:apply`.
### Bug Fixes
- **Fresh store registration** — Register and use newly created stores before their empty changes, specs, or archive directories have been committed.
- **Safer requirement archiving** — Stop stale `MODIFIED` requirements from silently deleting scenarios that were added by an earlier archive.
### Patch Changes
- [#1300](https://github.com/Fission-AI/OpenSpec/pull/1300) [`a5bfeda`](https://github.com/Fission-AI/OpenSpec/commit/a5bfedafc8b3d914fe01d05eb36ad9ad3fbe35a2) Thanks [@clay-good](https://github.com/clay-good)! - ### Features
- **Auto-approve the OpenSpec CLI in generated skills and commands** — every generated `SKILL.md` (all tools) and every Claude Code `/opsx:*` slash command now carries `allowed-tools: Bash(openspec:*)` in its frontmatter, so agents that honor the Agent Skills standard run `openspec` commands without prompting for approval on each call; tools that don't recognize the field ignore it. Scope is limited to the `openspec` CLI; because `allowed-tools` pre-approves rather than restricts, every other tool a skill or command uses stays available under your normal permission settings.
- [#1311](https://github.com/Fission-AI/OpenSpec/pull/1311) [`5956a8e`](https://github.com/Fission-AI/OpenSpec/commit/5956a8e872f41a8f690922b5c9b6927970252b2a) Thanks [@danilopopeye](https://github.com/danilopopeye)! - ### Bug Fixes
- **`archive` exits non-zero when blocked in human mode** — `openspec archive <change> -y` (and any non-`--json` invocation) no longer returns exit code 0 when validation fails and nothing is archived. The three blocking paths in human mode — delta-spec validation failure, spec rebuild failure, and rebuilt-spec validation failure — now set `process.exitCode = 1`, matching the existing `--json` behavior. Previously the command printed "Validation failed" (or "Aborted. No files were changed.") and exited 0, letting scripts and CI believe the archive succeeded. Aligns `archive` with the same exit-code guarantee already approved for `apply` instructions (#1250).
- [#1280](https://github.com/Fission-AI/OpenSpec/pull/1280) [`a325305`](https://github.com/Fission-AI/OpenSpec/commit/a3253051ea1934fd0d76620addb855dfce801742) Thanks [@clay-good](https://github.com/clay-good)! - ### Bug Fixes
- **`validate` resolves changes like `status`** — `openspec validate <change>` (and `--all`/`--changes` and the interactive selector) now resolves a change by directory existence, matching `status`/`instructions`, instead of requiring `proposal.md`. A scaffolded or still-authoring change is validated rather than reported as `Unknown item`, and a resolved-but-invalid change now exits non-zero. Delta discovery also recurses the nested `specs/<area>/<capability>/spec.md` layout. (#1182)
- **Task progress reads nested/glob `tasks.md`** — `openspec view`, `list`, and the `archive` incomplete-task gate now resolve task progress through the tracked-tasks artifact's `generates` glob (the same file-resolution `status` uses), so a change whose tasks live in nested `tasks.md` files is classified correctly and can no longer archive while unfinished. (#1202)
- **SHALL/MUST body-keyword hint applies to main specs** — A main-spec requirement whose normative keyword sits only in the `### Requirement:` header now receives the same targeted "move it to the body line" remediation as a change delta, emitted exactly once. (#1156)
- [#1281](https://github.com/Fission-AI/OpenSpec/pull/1281) [`9a0dfb5`](https://github.com/Fission-AI/OpenSpec/commit/9a0dfb5cd136b423c9f13c0b29ec3ea69761b4e6) Thanks [@clay-good](https://github.com/clay-good)! - ### Bug Fixes
- **Requirement reading fidelity** — The requirement reader used by `validate <change>`, `validate <spec>`, and `archive` is now unified into one fence-, metadata-, and multi-line-aware extraction, closing the known divergences between the change-delta path and the main-spec path (the remaining ones are documented in the change's design doc):
- A `SHALL`/`MUST` keyword that wraps onto a later body line is detected instead of dropped (#361).
- Metadata lines (`**ID**:`, `**Priority**:`) before the description are skipped on the spec path, matching the change path (#418). A requirement written entirely as metadata (e.g. `**Constraint**: The system MUST ...`) keeps that line as its text instead of being emptied.
- A fenced code block before the prose line no longer becomes the requirement text (#312).
- A `#### Scenario:` inside a fenced example no longer counts as a real scenario in `validate <change>`, matching `validate <spec>`.
- `SHALL`/`MUST` detection uses one whole-word predicate across all readers, and a requirement with no body text falls back to its header title on both paths.
Displayed requirement text (e.g. in JSON output and delta descriptions) now reflects the full requirement body rather than only its first line. Archived spec content is unchanged — the archive rebuild reads raw `### Requirement:` blocks, not the parsed text.
- **Surface non-canonical delta headers** — `validate <change>` now emits an INFO note when an `## ADDED`/`## MODIFIED Requirements` section contains a level-3 header that is not a canonical `### Requirement:` header (one the delta reader silently skips, such as a stray `### Documentation Requirements` divider). The note never changes the `valid` result, including under `--strict` (#498).
## 1.5.0
### Minor Changes
+7
View File
@@ -7,6 +7,13 @@ People who maintain and guide OpenSpec.
| Name | GitHub | Role |
|------|--------|------|
| Tabish Bidiwale | [@TabishB](https://github.com/TabishB) | Lead maintainer |
| Clay Good | [@clay-good](https://github.com/clay-good) | Maintainer |
## Automation Maintainers
| Name | GitHub | Role |
|------|--------|------|
| Alfred | [@alfred-openspec](https://github.com/alfred-openspec) | Automation maintainer |
## Advisors
+41 -2
View File
@@ -76,6 +76,29 @@ AI: Archived to openspec/changes/archive/2025-01-23-add-dark-mode/
Specs updated. Ready for the next feature.
```
<details>
<summary><strong>What do the specs actually look like?</strong></summary>
Plain Markdown — requirements with concrete scenarios, no special syntax to learn. Here's what goes in the `specs/` folder created above:
```markdown
## ADDED Requirements
### Requirement: Theme selection
The app SHALL let users switch between light and dark themes,
defaulting to the system preference.
#### Scenario: User toggles dark mode
- **WHEN** the user clicks the theme toggle
- **THEN** the app switches to dark mode and persists the choice
```
Your AI writes these; you review the plan before any code is written.
OpenSpec is built with OpenSpec — browse this repo's live [specs](openspec/specs) and in-flight [changes](openspec/changes) for real examples at scale.
</details>
<details>
<summary><strong>OpenSpec Dashboard</strong></summary>
@@ -85,6 +108,18 @@ AI: Archived to openspec/changes/archive/2025-01-23-add-dark-mode/
</details>
## Why teams adopt OpenSpec
Solo, OpenSpec keeps you and your AI honest on a single repo. On a team, the hard part moves: a feature spans the API server, the web app, and a shared library; requirements are owned by one team and consumed by others; planning starts before any code exists.
**[Stores](docs/stores-beta/user-guide.md)** are the answer — planning in a repo of its own. The same `openspec/` shape you already know (specs and changes), shared by `git push` like anything else. One source of truth your whole team and every coding agent can read, across every repo.
- **Cross-repo features** — one change, one plan, even when the code lands in three repos.
- **Shared requirements** — a platform team owns the specs; product teams reference them read-only, right where their coding agent can read them. No drifting wiki.
- **Plan before code** — capture the plan in the store now; the code repos catch up later.
> Stores are in **beta**. Start with the [Stores User Guide](docs/stores-beta/user-guide.md).
## Quick Start
**Requires Node.js 20.19.0 or higher.**
@@ -102,6 +137,8 @@ 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.
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))
@@ -109,8 +146,10 @@ Now talk to your AI:
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`.
`/opsx:propose` is the canonical name; your tool may spell it `/opsx-propose` (Cursor, GitHub Copilot), `@opsx-propose` (Amazon Q) or `$openspec-propose` (Codex). `openspec init` prints the right form for the tools you picked — see [How To Invoke](docs/supported-tools.md#how-to-invoke).
> [!NOTE]
> Not sure if your tool is supported? [View the full list](docs/supported-tools.md) – we support 25+ tools and growing.
> Not sure if your tool is supported? [View the full list](docs/supported-tools.md) – we support 30+ tools and growing.
>
> Also works with pnpm, yarn, bun, and nix. [See installation options](docs/installation.md).
@@ -150,7 +189,7 @@ AI coding assistants are powerful but unpredictable when requirements live only
- **Agree before you build** — human and AI align on specs before code gets written
- **Stay organized** — each change gets its own folder with proposal, specs, design, and tasks
- **Work fluidly** — update any artifact anytime, no rigid phase gates
- **Use your tools** — works with 20+ AI assistants via slash commands
- **Use your tools** — works with 30+ AI assistants via slash commands
### How we compare
+62
View File
@@ -0,0 +1,62 @@
# Security Policy
## Reporting a vulnerability
Report privately through [GitHub Security Advisories](https://github.com/Fission-AI/OpenSpec/security/advisories/new). Please don't open a public issue for a suspected vulnerability.
Include what you can: affected version, reproduction steps, and the impact you believe it has. We aim to acknowledge within 3 business days and to ship a fix or a decision within 30 days. Valid reports are credited in the advisory unless you'd rather stay anonymous.
## Supported versions
Fixes ship in the latest published version on npm. Older versions are not patched — upgrade to pick up a fix.
## Threat model
OpenSpec is a local command-line tool. It has no server, no network listener, and no privileged daemon. It reads and writes markdown under the directory you run it in, using paths you supply, with your own user permissions. It can offer to upgrade itself during `openspec update`, and only with your say-so. It sends anonymous usage telemetry, which you can disable with `OPENSPEC_TELEMETRY=0`.
That shapes what is and isn't a vulnerability here:
| In scope | Out of scope |
| --- | --- |
| Code execution triggered by parsing a spec, config, or template file | Reading or writing a file path you passed to the CLI yourself |
| Escaping the directory OpenSpec was pointed at, via untrusted input | Static-analysis findings on file-path joins with no untrusted input |
| Leaking credentials or file contents through telemetry or logs | Vulnerabilities in devDependencies that don't ship in the published package |
| Prototype pollution or injection reachable from a config or spec file | Denial of service against your own machine using your own input |
If you think something sits on the boundary, report it and we'll work it out together.
## Published package contents
The `openspec` npm package publishes `dist/`, `bin/`, `schemas/`, and `scripts/postinstall.js`. Build and test tooling (vite, rollup, vitest, eslint, and their transitive dependencies) is not published. Scanners that read `pnpm-lock.yaml` without separating dependency scope will report advisories for packages that never reach an installed copy of OpenSpec.
You do not have to take that on trust — install the package and look:
```sh
npm install @fission-ai/openspec
ls node_modules | grep -E '^(vite|rollup|vitest|eslint|js-yaml|minimatch)$' # no matches
```
`pnpm audit --prod` in this repository reports the same scope, and CI runs it on every pull request.
## What the CLI does on your machine
| Surface | Behavior |
| --- | --- |
| Install script | `scripts/postinstall.js` prints one line suggesting shell completions. It makes no network request, writes no files, and runs no shell. Completions are opt-in via `openspec completion install`. |
| Running other programs | Every call that goes through a shell uses a fixed literal (`which gh`, `gh auth status`). Anything carrying your input — issue text, editor paths, workset commands, the path passed to `openspec update` — uses an argument array, never string interpolation into a shell. On Windows, `.cmd` shims are launched through `cross-spawn`, which escapes arguments rather than concatenating them. |
| Installing software | `openspec update` can run `npm install -g @fission-ai/openspec@latest` and then re-run `openspec update` with the upgraded CLI. It does this only after you answer yes to a prompt, only for the OpenSpec package itself, only when npm owns the install, and never in CI or a non-interactive shell. A global install lives outside your project, so it runs with your permissions there and executes whatever lifecycle scripts the published package ships. It then reads the installed binary's version back rather than assuming the upgrade took. Decline and it prints the command for you to run yourself. |
| Telemetry | Command name, OpenSpec version, and a locally generated random UUID. No file paths, no file contents, no environment, no hostname, and IP capture is explicitly disabled. Opt out with `OPENSPEC_TELEMETRY=0` or `DO_NOT_TRACK=1`; it is off in CI automatically. |
| Network | Telemetry when enabled, and one npm registry request during `openspec update` to check whether a newer CLI has been published. That request sends no data about you beyond what any HTTP request reveals, runs once per `openspec update` with nothing cached, and is skipped when `CI` is set to anything but an explicit off-value, under `NODE_ENV=test`, or when `OPENSPEC_NO_UPDATE_CHECK`, `DO_NOT_TRACK=1`, or `OPENSPEC_TELEMETRY=0` is set. Reading, writing, and validating specs is entirely local. |
## Automated checks
| Tool | Covers |
| --- | --- |
| [CodeQL](https://github.com/Fission-AI/OpenSpec/security/code-scanning) | Static analysis on every push and pull request to `main` |
| [Dependabot](https://github.com/Fission-AI/OpenSpec/security/dependabot) | Dependency advisories plus weekly update pull requests for the CLI, the docs site, and CI actions |
| Dependency review | Blocks a pull request that introduces a high-severity dependency |
| Secret scanning | Enabled on the repository, including push protection |
| `pnpm audit` | Published dependencies are audited on every pull request, on pushes to `main`, and weekly. Advisory on pull requests so an unrelated change is not blocked; failing elsewhere, so a new advisory surfaces even when no dependency changed. Build tooling is always advisory. |
| Pinned actions | Every GitHub Action runs from a commit SHA, so a moved tag cannot change what CI executes |
Alerts are triaged against the threat model above, so a finding in build-only tooling is fixed on the normal update cadence rather than treated as an incident.
+10 -3
View File
@@ -21,10 +21,14 @@ That second one matters more than it looks. OpenSpec has two halves: a command l
**I have a big existing codebase.** You don't document all of it. [Using OpenSpec in an Existing Project](existing-projects.md) shows how to start on real, brownfield code without boiling the ocean.
**I just want to get it working.** [Install](installation.md), run `openspec init`, then read [How Commands Work](how-commands-work.md) so your first slash command lands in the right place.
**I just want to get it working.** [Install](installation.md), run `openspec init`, then read [How Commands Work](how-commands-work.md) so your first slash command lands in the right place. Or hand the setup to your assistant with the [AI-assisted install prompt](installation.md#install-with-your-ai-assistant).
**I learn by example.** The [Examples & Recipes](examples.md) page walks through real changes start to finish: a small feature, a bug fix, a refactor, an exploration.
**The AI just drafted a plan — now what?** Read it. [Reviewing a Change](reviewing-changes.md) shows the two-minute pass that catches a wrong turn while it's still cheap, and [Writing Good Specs](writing-specs.md) covers what a plan worth approving is made of.
**I work on a team.** [OpenSpec on a Team](team-workflow.md) shows how a change maps onto a branch and a pull request, and how teammates review a plan before the code.
**I'm coming from the old workflow.** The [Migration Guide](migration-guide.md) explains what changed and why, and promises your existing work is safe.
**I want to bend it to my team's process.** [Customization](customization.md) covers project config, custom schemas, and shared context.
@@ -41,7 +45,7 @@ That second one matters more than it looks. OpenSpec has two halves: a command l
| [Explore First](explore.md) | Use `/opsx:explore` to think through an idea before you commit |
| [How Commands Work](how-commands-work.md) | Where slash commands run, what "interactive mode" means, terminal vs chat |
| [Core Concepts at a Glance](overview.md) | The whole mental model on one page: specs, changes, deltas, archive |
| [Installation](installation.md) | npm, pnpm, yarn, bun, Nix, and how to verify it worked |
| [Installation](installation.md) | npm, pnpm, yarn, bun, Nix, a prompt that hands setup to your AI assistant, and how to verify it worked |
### Use it day to day
@@ -49,6 +53,9 @@ That second one matters more than it looks. OpenSpec has two halves: a command l
|-----|-------------------|
| [Workflows](workflows.md) | Common patterns and when to reach for each command |
| [Examples & Recipes](examples.md) | Full walkthroughs of real changes, copy-pasteable |
| [Writing Good Specs](writing-specs.md) | What a strong requirement and scenario look like, and how to right-size a change |
| [Reviewing a Change](reviewing-changes.md) | The two-minute pass on a drafted plan before any code is written |
| [OpenSpec on a Team](team-workflow.md) | How changes fit branches, pull requests, and review |
| [Using OpenSpec in an Existing Project](existing-projects.md) | Adopting OpenSpec on a large brownfield codebase |
| [Editing & Iterating on a Change](editing-changes.md) | Update artifacts, go back, reconcile manual edits |
| [Commands](commands.md) | Reference for every `/opsx:*` slash command |
@@ -68,7 +75,7 @@ That second one matters more than it looks. OpenSpec has two halves: a command l
|-----|-------------------|
| [Customization](customization.md) | Project config, custom schemas, shared context |
| [Multi-Language](multi-language.md) | Generate artifacts in languages other than English |
| [Supported Tools](supported-tools.md) | The 25+ AI tools OpenSpec integrates with, and where files land |
| [Supported Tools](supported-tools.md) | The 30+ AI tools OpenSpec integrates with, and where files land |
### When you need help
+22 -18
View File
@@ -27,17 +27,18 @@ Diagnostics appear in two positions: **status arrays** (`status: StoreDiagnostic
## 3. Root selection and `RootOutput`
All root-resolving commands (`list`, `show`, `validate`, `status`, `instructions`, `instructions apply`, `new change`, `archive`, `doctor`, `context`) resolve one OpenSpec root with one precedence:
All root-resolving commands (`list`, `show`, `validate`, `status`, `instructions`, `instructions apply`, `instructions archive`, `new change`, `archive`, `doctor`, `context`) resolve one OpenSpec root with one precedence:
1. `--store <id>` → the registered store's root (`source: "store"`).
2. Otherwise, nearest ancestor with `openspec/`: planning shape → `source: "nearest"` (a `store:` pointer is ignored with a stderr warning); config-only dir with a valid `store:` pointer → that store, `source: "declared"`.
3. No nearest root + registered stores exist → error `no_root_with_registered_stores`.
4. No root, no stores: scaffolding commands treat the cwd as `source: "implicit"`; diagnostic commands (`doctor`, `context`) fail with `no_openspec_root` instead — they inspect, never scaffold.
3. No nearest root + global `defaultStore` set (`openspec config set defaultStore <id>`) → that store, `source: "global_default"`; a stale id fails with the underlying store error and a `fix` naming `openspec config unset defaultStore`.
4. No nearest root, no default + registered stores exist → error `no_root_with_registered_stores`.
5. No root, no default, no stores: scaffolding commands treat the cwd as `source: "implicit"`; diagnostic commands (`doctor`, `context`) fail with `no_openspec_root` instead — they inspect, never scaffold.
Successful JSON payloads embed the root:
```json
"root": { "path": "/abs/path", "source": "store" | "declared" | "nearest" | "implicit", "store_id": "id (only when store-selected)" }
"root": { "path": "/abs/path", "source": "store" | "declared" | "global_default" | "nearest" | "implicit", "store_id": "id (only when store-selected)" }
```
**Root-failure contract**: in JSON mode a resolution failure prints `{ ...commandNullShape, "status": [diagnostic] }` on stdout and exits 1.
@@ -54,32 +55,35 @@ Change: `{ "id", "title", "deltaCount", "deltas": [...], "root" }`. Spec: `{ "id
`{ "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" }, "isComplete", "applyRequires", "artifacts": [ {id, outputPath, status: "done"|"ready"|"blocked", missingDeps?} ], "root" }`. 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" }, "isComplete", "applyRequires", "artifacts": [ {id, outputPath, status: "done"|"skipped"|"ready"|"blocked", requires, missingDeps?} ], "root" }`. 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.
### 4.5 `instructions <artifact> --json`
`{ "changeName", "artifactId", "schemaName", "changeDir", "planningHome"?, "outputPath", "resolvedOutputPath", "existingOutputPaths", "description", "instruction"?, "context"?, "rules"?, "references"?: ReferenceIndexEntry[], "template", "dependencies": [{id,done,path,description}], "unlocks", "root" }`.
`{ "changeName", "artifactId", "schemaName", "changeDir", "planningHome"?, "outputPath", "resolvedOutputPath", "existingOutputPaths", "description", "instruction"?, "context"?, "rules"?, "references"?: ReferenceIndexEntry[], "skipped"?, "warning"?, "template", "dependencies": [{id,done,path,description,skipped?}], "unlocks", "root" }`. `unlocks` lists the artifacts this one makes ready, in the schema's declaration order (the same order `status` recommends them). `"skipped": true` (with `"warning"`) appears when the change declares `skip_specs: true` and this artifact is skipped — do not create its files. A dependency entry with `skipped: true` is satisfied without files — do not try to read its paths.
`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"?, "root" }`.
`{ "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.
### 4.7 `new change <name> --json`
### 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.
### 4.8 `new change <name> --json`
Success: `{ "change": { "id", "path", "metadataPath", "schema" }, "root" }`. Failure: `{ "change": null, "status": [d] }`, exit 1.
### 4.8 `archive <name> --json`
Success: `{ "archive": { "change", "archivedAs": "YYYY-MM-DD-name", "path", "specsUpdated", "totals"? }, "root" }`. Failure: `{ "archive": null, "root"?, "status": [d] }`, exit 1. JSON mode is strictly non-interactive: every prompt point becomes an `archive_*` code.
### 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; 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.
### 4.9 `doctor --json`
`{ "root": { "path", "source", "store_id"?, "healthy", "status": [] }, "store": { "id", "metadata": {present,valid,remote?}, "origin_url"?, "status": [] } | null, "references": [...], "status": [] }`. Health findings of any severity exit 0. Failure payload: `{ "root": null, "store": null, "references": [], "status": [d] }`, exit 1.
### 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.
### 4.10 `context --json`
### 4.11 `context --json`
`{ "root": { "path", "source", "store_id"?, "role": "openspec_root" }, "members": [ { "role": "referenced_store", "id", "path"?, "remote"?, "fetch"?, "status": [] } ], "status": [] }`. AVAILABLE = path present AND status empty. `--code-workspace <path>` writes `{folders:[{name,path}]}` (available referenced stores only, `ref:` prefixes); in JSON mode the write runs before printing so stdout holds exactly one document even on write failure. Failure: `{ "root": null, "members": [], "status": [d] }`, exit 1.
### 4.11 `store ... --json`
### 4.12 `store ... --json`
setup/register: `{ "store": {id, root, metadata_path?}, "registry": {path, registered, already_registered}, "git": {is_repository, initialized, committed}, "created_files": [], "status": [] }`. unregister/remove: `{ "store", "registry": {path, removed}, "files": {deleted, deleted_path, left_on_disk}, "status": [] }`. list: `{ "stores": [{id, root}], "status": [] }`. doctor: `{ "stores": [ { id, root, metadata_path?, openspec_root: {...healthy, status}, metadata: {present, valid, id?, remote}, git: {is_repository, has_commits, has_uncommitted_changes, has_remote, origin_url}, status } ], "status": [] }` (`null` = unknown/not probed). Health findings exit 0; failures exit 1 with the matching null-shape. Prompt cancellation exits 130.
### 4.12 `schemas --json` / `templates --json`
### 4.13 `schemas --json` / `templates --json`
`schemas`: bare array `[ {name, description, artifacts, source} ]`. `templates`: keyed object `{ "<artifactId>": {path, source} }`. Both cwd-based, no root/status keys.
## 5. Exit-code contract
@@ -97,16 +101,16 @@ setup/register: `{ "store": {id, root, metadata_path?}, "registry": {path, regis
`no_openspec_root`, `no_root_with_registered_stores`, `no_registered_stores`, `unknown_store`, `store_identity_mismatch`, `unhealthy_store_root`, `store_path_not_supported`, `invalid_store_pointer`, `initiative_option_removed`, `areas_option_removed`; pass-through: `invalid_store_id`, `invalid_store_registry`, `invalid_store_metadata`.
### OpenSpec-root health (error, no fix)
`openspec_store_root_missing`, `openspec_root_missing`, `openspec_config_missing`, `openspec_specs_missing`, `openspec_changes_missing`, `openspec_archive_missing`, plus `_not_directory` variants of each.
`openspec_store_root_missing`, `openspec_store_root_not_directory`, `openspec_root_missing`, `openspec_root_not_directory`, `openspec_config_missing`, `openspec_config_not_file`, `openspec_specs_not_directory`, `openspec_changes_not_directory`, `openspec_archive_not_directory`. During the stores beta, `openspec/specs/`, `openspec/changes/`, and `openspec/changes/archive/` may be absent in a healthy root; they are only health errors when present but not directories.
### Store registry/identity/state
`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_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_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_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).
### References (warning)
`reference_invalid_id`, `reference_registry_unreadable`, `reference_unresolved`, `reference_root_unhealthy`, `reference_index_truncated`.
+104 -15
View File
@@ -82,7 +82,7 @@ These options work with all commands:
Initialize OpenSpec in your project. Creates the folder structure and configures AI tool integrations.
Default behavior uses global config defaults: profile `core`, delivery `both`, workflows `propose, explore, apply, sync, archive`.
Default behavior uses global config defaults: profile `core`, delivery `both`, workflows `propose, explore, apply, update, sync, archive`.
```
openspec init [path] [options]
@@ -101,10 +101,13 @@ openspec init [path] [options]
| `--tools <list>` | Configure AI tools non-interactively. Use `all`, `none`, or comma-separated list |
| `--force` | Auto-cleanup legacy files without prompting |
| `--profile <profile>` | Override global profile for this init run (`core` or `custom`) |
| `--no-animation` | Show a static welcome screen instead of the animated one |
`--profile custom` uses whatever workflows are currently selected in global config (`openspec config profile`).
**Supported tool IDs (`--tools`):** `amazon-q`, `antigravity`, `auggie`, `bob`, `claude`, `cline`, `codex`, `forgecode`, `codebuddy`, `continue`, `costrict`, `crush`, `cursor`, `factory`, `gemini`, `github-copilot`, `iflow`, `junie`, `kilocode`, `kimi`, `kiro`, `lingma`, `vibe`, `opencode`, `pi`, `qoder`, `qwen`, `roocode`, `trae`, `windsurf`
The welcome animation is also skipped when the `OPENSPEC_NO_ANIMATION` environment variable is set (any value, including empty), when `NO_COLOR` is set to a non-empty value, or when the OS reduced-motion preference is enabled (macOS Reduce Motion, GNOME animations disabled).
**Supported tool IDs (`--tools`)** — `windsurf` is also accepted, as an alias for `devin`: `amazon-q`, `antigravity`, `auggie`, `bob`, `claude`, `cline`, `codeartsagent`, `codex`, `devin`, `forgecode`, `codebuddy`, `continue`, `costrict`, `crush`, `cursor`, `factory`, `gemini`, `github-copilot`, `hermes`, `iflow`, `junie`, `kilocode`, `kimi`, `kiro`, `lingma`, `vibe`, `oh-my-pi`, `opencode`, `pi`, `qoder`, `qwen`, `roocode`, `trae`, `zcode`
> This list mirrors `AI_TOOLS` in `src/core/config.ts`. See [Supported Tools](supported-tools.md) for each tool's skill and command paths.
@@ -170,10 +173,46 @@ openspec update [path] [options]
```bash
# Update instruction files after npm upgrade
npm update @fission-ai/openspec
npm install -g @fission-ai/openspec@latest
openspec update
```
Upgrade the package first. Instruction files are generated by the installed CLI, so running `openspec update` against a stale install reports everything up to date without adding the workflows newer releases ship.
To make that visible, `openspec update` asks the npm registry whether a newer CLI has been published. When yours is behind, it offers to upgrade:
```text
A newer OpenSpec CLI is available (v1.6.0 → v1.7.0).
Running from: /usr/local/lib/node_modules/@fission-ai/openspec
? Upgrade to v1.7.0 now? (Y/n)
```
Answer yes and it runs `npm install -g @fission-ai/openspec@latest`, then re-runs the update with the new CLI so the new workflows land in the same command. It confirms the upgrade by asking the installed binary its version rather than trusting npm's exit code, so if another install earlier on your `PATH` is still answering, it tells you instead of claiming success. Answer no and it prints the command and updates with the CLI you have. Ctrl-C stops the command.
The offer appears only in an interactive terminal, and only when npm owns the install — the one case `npm install -g` actually fixes. Everything else gets the command that matches how it was installed instead:
| How OpenSpec is installed | What you get |
|---------------------------|--------------|
| Global npm install | The prompt, and the upgrade run for you — in an interactive terminal; piped output gets the printed command instead |
| Global pnpm, bun, yarn, or volta install | That manager's own command: `pnpm add -g …@latest`, `bun add -g …@latest`, `yarn global add …@latest`, or `volta install …@latest` |
| A dependency of the project | A note to update the dependency, since its package manager owns the lockfile |
| An `npx` / `dlx` cache | `npx @fission-ai/openspec@latest update` — that command is the update, so there is no second step |
| A git clone | Nothing — your version is whatever the branch says |
Whenever anything is printed, it names the directory the running CLI was loaded from — the thing to check when you did upgrade but a stale shim still owns your `PATH`.
It asks the registry in `npm_config_registry` when npm exports it, and `https://registry.npmjs.org` otherwise. No `.npmrc` is read: letting file contents choose where an outbound request goes is a flow worth avoiding, and a project's `.npmrc` travels with the repository. On a private mirror, export `npm_config_registry` — or set `OPENSPEC_NO_UPDATE_CHECK` to skip the check entirely. The check is skipped when `CI` is set to anything but an explicit off-value (`false`, `0`, `no`, `off`, or empty), under `NODE_ENV=test`, and whenever `OPENSPEC_NO_UPDATE_CHECK` (any value), `DO_NOT_TRACK=1`, or `OPENSPEC_TELEMETRY=0` is set. It runs before the update and can delay it by at most 1.5 seconds — it gives up after that even when the network drops packets silently, and stays quiet when the registry is unreachable.
**How "up to date" is decided:** skill files record the version that generated
them, so OpenSpec compares that against the installed CLI. Command files carry no
version stamp, so for a tool that has commands but no skills (delivery
`commands`), OpenSpec compares the file contents against what it would generate
now — edits to those files count as drift and are overwritten. With delivery
`skills` or `both`, only the recorded version is checked, so a hand-edited file
whose version still matches is left alone; use `--force` to rewrite it. Either
way, generated files are OpenSpec's to own — keep your own instructions
elsewhere.
---
## Stores (standalone OpenSpec repos)
@@ -215,7 +254,12 @@ openspec store setup team-context --path ~/openspec/team-context --no-init-git -
### `openspec store register`
Register an existing local store folder.
Register an existing local store folder. During the stores beta, a root may be
registered before any changes exist, specs have been applied, or changes have
been archived; in that case `openspec/changes/`, `openspec/specs/`, and
`openspec/changes/archive/` may be absent until normal commands create them.
A config-only repo that declares `store: <id>` remains a pointer to another
store and is not registered as a store root unless that pointer is removed.
```bash
openspec store register [path] [options]
@@ -317,6 +361,8 @@ store: team-context
Normal commands then resolve to the declared store automatically; the root banner and JSON `root` block report `source: "declared"` with the store id, and printed hints still carry `--store <id>`. The declaration is a fallback, never an override: explicit `--store` always wins, and a directory with real planning folders ignores the pointer (with a warning). To convert a pointer repo into a local OpenSpec root, remove the `store:` line and run `openspec init` — init refuses to scaffold while the declaration is present.
A machine-level variant covers every repo at once: `openspec config set defaultStore <id>` (see Configuration). It is consulted only after `--store`, a local root, and a project pointer have all failed to resolve; the root banner and JSON `root` block then report `source: "global_default"`.
## Doctor (relationship health)
One read-only question, one place: is the OpenSpec root healthy, and are the stores it references available on this machine?
@@ -325,7 +371,7 @@ One read-only question, one place: is the OpenSpec root healthy, and are the sto
openspec doctor [--store <id>] [--json]
```
The report separates root health, store metadata health (including a note when the recorded remote and the checkout's origin diverge), and reference health (the same diagnostics instructions show, with clone fixes for unresolved references). Health findings of any severity exit 0 — agents read the `status` arrays; only command failures (no root, unknown store) exit 1. Doctor never clones, syncs, or repairs. To get the assembled set itself rather than its health, use `openspec context`.
The report separates root health, store metadata health (including a note when the recorded remote and the checkout's origin diverge, and a note when the store checkout has drifted behind its last-fetched upstream tracking ref), and reference health (the same diagnostics instructions show, with clone fixes for unresolved references). Health findings of any severity exit 0 — agents read the `status` arrays; only command failures (no root, unknown store) exit 1. Doctor never clones, syncs, or repairs. To get the assembled set itself rather than its health, use `openspec context`.
## Working context (the assembled set)
@@ -486,6 +532,8 @@ Validate changes and specs for structural issues.
openspec validate [item-name] [options]
```
A change with zero spec deltas fails validation unless its `.openspec.yaml` declares `skip_specs: true` (for pure refactors, tooling, or docs work — see [Recipe 5](examples.md#recipe-5-a-refactor-with-no-behavior-change)).
**Arguments:**
| Argument | Required | Description |
@@ -580,7 +628,7 @@ openspec archive [change-name] [options]
| Option | Description |
|--------|-------------|
| `-y, --yes` | Skip confirmation prompts |
| `--skip-specs` | Skip spec updates (for infrastructure/tooling/doc-only changes) |
| `--skip-specs` | Skip spec updates for one archive run. A change that permanently has no spec deltas should declare `skip_specs: true` in its `.openspec.yaml` instead — it archives with no flag |
| `--no-validate` | Skip validation (requires confirmation) |
**Examples:**
@@ -620,6 +668,12 @@ Create a change directory and optional checked-in metadata in the resolved OpenS
openspec new change <name> [options]
```
Change names must use lowercase kebab-case: lowercase letters, numbers, and
single hyphens. They cannot contain spaces, underscores, uppercase letters,
consecutive hyphens, or leading/trailing hyphens. A leading number is allowed,
so you can prefix names to order or tier changes, for example `100-add-feature`
or `00001-add-auth`.
**Options:**
| Option | Description |
@@ -674,11 +728,13 @@ Schema: spec-driven
Progress: 2/4 artifacts complete
[x] proposal
[ ] design
[x] specs
[ ] design
[-] tasks (blocked by: design)
```
A change that declares `skip_specs: true` shows its specs stage as `[~] specs (skipped: change declares skip_specs)` and excludes it from the progress count.
**Output (JSON):**
```json
@@ -688,14 +744,20 @@ Progress: 2/4 artifacts complete
"isComplete": false,
"applyRequires": ["tasks"],
"artifacts": [
{"id": "proposal", "outputPath": "proposal.md", "status": "done"},
{"id": "design", "outputPath": "design.md", "status": "ready"},
{"id": "specs", "outputPath": "specs/**/*.md", "status": "done"},
{"id": "tasks", "outputPath": "tasks.md", "status": "blocked", "missingDeps": ["design"]}
{"id": "proposal", "outputPath": "proposal.md", "status": "done", "requires": []},
{"id": "specs", "outputPath": "specs/**/*.md", "status": "done", "requires": ["proposal"]},
{"id": "design", "outputPath": "design.md", "status": "ready", "requires": ["proposal"]},
{"id": "tasks", "outputPath": "tasks.md", "status": "blocked", "requires": ["specs", "design"], "missingDeps": ["design"]}
]
}
```
Artifacts are listed in dependency order - a dependency never appears after
something that requires it - and artifacts that become ready at the same time
(spec-driven's `specs` and `design` both need only `proposal`) keep the order the
schema declares them rather than an alphabetical one. So the first `ready` entry
is the artifact to write next.
---
### `openspec instructions`
@@ -710,7 +772,7 @@ openspec instructions [artifact] [options]
| Argument | Required | Description |
|----------|----------|-------------|
| `artifact` | No | Artifact ID: `proposal`, `specs`, `design`, `tasks`, or `apply` |
| `artifact` | No | Artifact ID, or workflow input surface: `apply` or `archive` |
**Options:**
@@ -720,7 +782,9 @@ openspec instructions [artifact] [options]
| `--schema <name>` | Schema override |
| `--json` | Output as JSON |
**Special case:** Use `apply` as the artifact to get task implementation instructions.
**Special cases:** Use `apply` to get task implementation instructions. Use
`archive` to fetch current, read-only archive inputs (`context` and
`operationGuidance`) for a valid change; it does not archive or mutate anything.
**Examples:**
@@ -734,6 +798,9 @@ openspec instructions design --change add-dark-mode
# Get apply/implementation instructions
openspec instructions apply --change add-dark-mode
# Get current archive operation inputs without archiving
openspec instructions archive --change add-dark-mode --json
# JSON for agent consumption
openspec instructions design --change add-dark-mode --json
```
@@ -744,6 +811,21 @@ openspec instructions design --change add-dark-mode --json
- Project context from config
- Content from dependency artifacts
- Per-artifact rules from config
- Current project context and matching operation guidance for `apply`/`archive`
Operation inputs are read from the resolved repo or selected store on every
invocation. Project context is a required prompt-level input: agents read it and
apply relevant project facts, conventions, and constraints. Operation guidance is
optional additive advice: agents consider every entry and follow only entries that
are applicable and compatible with the built-in workflow. Both fields remain
separate from explicit user choices, CLI-controlled state, built-in instructions,
and artifact rules. Conflicting context is reported; conflicting or inapplicable
guidance is not followed and the reason is explained. These are behavioral
contracts for generated agents, not enforceable CLI checks. `instructions archive`
returns only the selected change, optional inputs, and root metadata; it does not
include the static archive workflow.
For an artifact skipped via `skip_specs: true`, the output is a warning only (JSON adds `skipped`/`warning` fields) — the artifact must not be created.
---
@@ -1032,6 +1114,10 @@ openspec config set user.name "My Name" --string
# Remove a custom setting
openspec config unset user.name
# Set a machine-level default store (fallback root when no --store,
# local root, or project store: pointer resolves)
openspec config set defaultStore team-plans
# Reset all configuration
openspec config reset --all --yes
@@ -1154,11 +1240,14 @@ openspec completion uninstall
| Variable | Description |
|----------|-------------|
| `OPENSPEC_TELEMETRY` | Set to `0` to disable telemetry |
| `DO_NOT_TRACK` | Set to `1` to disable telemetry (standard DNT signal) |
| `OPENSPEC_TELEMETRY` | Set to `0` to disable telemetry and the `openspec update` version check |
| `DO_NOT_TRACK` | Set to `1` to disable telemetry and the `openspec update` version check (standard DNT signal) |
| `OPENSPEC_CONCURRENCY` | Default concurrency for bulk validation (default: 6) |
| `EDITOR` or `VISUAL` | Editor for `openspec config edit` |
| `NO_COLOR` | Disable color output when set |
| `OPENSPEC_NO_ANIMATION` | Disable the `openspec init` welcome animation when set |
| `OPENSPEC_NO_UPDATE_CHECK` | Disable the `openspec update` check for a newer published CLI when set (any value, including empty). Also skipped when `CI` is set (unless `false`/`0`/`no`/`off`) or `NODE_ENV=test` |
| `npm_config_registry` | Registry the `openspec update` version check asks. Must be an `http(s)` URL or it falls back to `https://registry.npmjs.org`. No `.npmrc` file is read |
---
+71 -12
View File
@@ -1,9 +1,14 @@
# Commands
This is the reference for OpenSpec's slash commands. These commands are invoked in your AI coding assistant's chat interface (e.g., Claude Code, Cursor, Windsurf).
This is the reference for OpenSpec's slash commands. These commands are invoked in your AI coding assistant's chat interface (e.g., Claude Code, Cursor, Devin Desktop).
For workflow patterns and when to use each command, see [Workflows](workflows.md). For CLI commands, see [CLI](cli.md).
These pages use `/opsx:<command>` as the canonical name. Some tools spell it
differently — Cursor and GitHub Copilot register `/opsx-propose`, Codex uses
`$openspec-propose` — so check [How To Invoke](supported-tools.md#how-to-invoke)
for your tool. The files OpenSpec generates already use the right form.
## Quick Reference
### Default Quick Path (`core` profile)
@@ -13,6 +18,7 @@ For workflow patterns and when to use each command, see [Workflows](workflows.md
| `/opsx:propose` | Create a change and generate planning artifacts in one step |
| `/opsx:explore` | Think through ideas before committing to a change |
| `/opsx:apply` | Implement tasks from the change |
| `/opsx:update` | Revise a change's planning artifacts and keep them coherent |
| `/opsx:sync` | Merge delta specs into main specs |
| `/opsx:archive` | Archive a completed change |
@@ -204,7 +210,7 @@ AI: Change: add-dark-mode
✓ proposal (done)
◆ specs (ready)
◆ design (ready)
○ tasks (blocked - needs: specs)
○ tasks (blocked - needs: specs, design)
Creating specs...
@@ -212,7 +218,7 @@ AI: Change: add-dark-mode
✓ Created openspec/changes/add-dark-mode/specs/ui/spec.md
Now available: tasks
Now available: design
Run /opsx:continue to create the next artifact.
```
@@ -317,6 +323,55 @@ AI: Implementing add-dark-mode...
---
### `/opsx:update`
Revise a change's existing planning artifacts and keep them coherent with one another. Planning artifacts only - it never edits code.
**Syntax:**
```text
/opsx:update [change-name]
```
**Arguments:**
| Argument | Required | Description |
|----------|----------|-------------|
| `change-name` | No | Which change to update (inferred from context if not provided) |
**What it does:**
- Reads the change's artifacts via `openspec status --change <name> --json`
- 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)
**Example:**
```text
You: /opsx:update add-dark-mode - we're storing the theme in a cookie now, not localStorage
AI: Reading add-dark-mode artifacts...
The design references localStorage in two places; tasks 1.3 covers
localStorage persistence; the proposal doesn't mention storage.
Proposed revisions:
1. design.md - swap localStorage decision for cookie storage
2. tasks.md - reword task 1.3 to cookie persistence
Apply revision 1? (design.md)
```
**Tips:**
- It won't create missing artifacts - that's `/opsx:continue`
- 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))
---
### `/opsx:verify`
Validate that implementation matches your change artifacts. Checks completeness, correctness, and coherence.
@@ -614,16 +669,20 @@ AI: Welcome to OpenSpec!
Different AI tools use slightly different command syntax. Use the format that matches your tool:
| Tool | Syntax Example |
|------|----------------|
| Claude Code | `/opsx:propose`, `/opsx:apply` |
| Cursor | `/opsx-propose`, `/opsx-apply` |
| Windsurf | `/opsx-propose`, `/opsx-apply` |
| Copilot (IDE) | `/opsx-propose`, `/opsx-apply` |
| Kimi CLI | Skill-based invocations such as `/skill:openspec-propose`, `/skill:openspec-apply-change` (no generated `opsx-*` command files) |
| Trae | Skill-based invocations such as `/openspec-propose`, `/openspec-apply-change` (no generated `opsx-*` command files) |
| Your tool's command file | Syntax example | Example tools |
|--------------------------|----------------|---------------|
| `.../commands/opsx/<id>.*` | `/opsx:propose`, `/opsx:apply` | Claude Code, Gemini CLI, Crush |
| `.../opsx-<id>.*` | `/opsx-propose`, `/opsx-apply` | Cursor, Devin Desktop, Copilot (IDE), Trae, Oh My Pi |
| none — skills only | `/openspec-propose`, `/openspec-apply-change` | CodeArts, ForgeCode, Hermes, Mistral Vibe |
| none — Kimi Code | `/skill:openspec-propose` | Kimi Code |
| none — Codex CLI | `$openspec-propose` | Codex |
The intent is the same across tools, but how commands are surfaced can differ by integration.
> **Devin Desktop vs Devin Local:** the `.devin/workflows/opsx-*.md` files give
> Devin Desktop `/opsx-propose`. Devin Local has no workflows — use the skills
> OpenSpec writes to `.devin/skills/`, e.g. `/openspec-propose`, which work on
> both agents.
The intent is the same across tools, but how commands are surfaced can differ by integration. [How To Invoke](supported-tools.md#how-to-invoke) lists every supported tool; this table shows only examples of each shape.
> **Note:** GitHub Copilot commands (`.github/prompts/*.prompt.md`) are only available in IDE extensions (VS Code, JetBrains, Visual Studio). GitHub Copilot CLI does not currently support custom prompt files — see [Supported Tools](supported-tools.md) for details and workarounds.
+2 -1
View File
@@ -190,7 +190,7 @@ openspec/changes/add-dark-mode/
├── proposal.md # Why and what
├── design.md # How (technical approach)
├── tasks.md # Implementation checklist
├── .openspec.yaml # Change metadata (optional)
├── .openspec.yaml # Change metadata (optional): schema, created, skip_specs
└── specs/ # Delta specs
└── ui/
└── spec.md # What's changing in ui/spec.md
@@ -393,6 +393,7 @@ The system MUST expire sessions after 15 minutes of inactivity.
| `## ADDED Requirements` | New behavior | Appended to main spec |
| `## MODIFIED Requirements` | Changed behavior | Replaces existing requirement |
| `## REMOVED Requirements` | Deprecated behavior | Deleted from main spec |
| `## Purpose` | What a brand-new capability is for | Seeds the Purpose of the main spec being created; ignored when the spec already exists |
### Why Deltas Instead of Full Specs
+70
View File
@@ -17,6 +17,7 @@ The `openspec/config.yaml` file is the easiest way to customize OpenSpec for you
- **Set a default schema** - Skip `--schema` on every command
- **Inject project context** - AI sees your tech stack, conventions, etc.
- **Add per-artifact rules** - Custom rules for specific artifacts
- **Add per-operation guidance** - Advisory preferences for apply and archive work
### Quick Setup
@@ -43,6 +44,14 @@ rules:
specs:
- Use Given/When/Then format
- Reference existing patterns before inventing new ones
operations:
apply:
guidance:
- Run focused tests before the full suite
archive:
guidance:
- Keep the completion summary concise
```
### How It Works
@@ -80,6 +89,60 @@ Tech stack: TypeScript, React, Node.js, PostgreSQL
- **Context** appears in ALL artifacts
- **Rules** ONLY appear for the matching artifact
**Operation guidance:**
`operations.apply.guidance` and `operations.archive.guidance` are optional arrays
of advisory instructions for how an agent should conduct those operations. They
are separate from `rules`: operation guidance does not constrain artifact content,
and artifact rules are never relabeled as operation guidance.
Apply and archive fetch these inputs at execution time:
```bash
openspec instructions apply --change my-feature --json
openspec instructions archive --change my-feature --json
```
Both surfaces return current project `context` and matching
`operationGuidance` as separate optional fields. Each invocation reads a fresh
snapshot from the resolved root. When `--store <id>` is selected, the change,
context, and guidance all come from that store rather than the current repository.
The archive instruction command is read-only: it does not inspect or merge delta
specs, write main specs, move the change, or run the static archive workflow.
Project context is a required prompt-level input. Generated workflows read it and
apply relevant project facts, conventions, and constraints. Operation guidance is
optional additive advice: workflows consider every entry and follow entries that
are applicable and compatible with the built-in workflow.
Both fields remain separate from CLI-controlled state, resolved paths, built-in
steps, explicit user choices, and artifact rules. A workflow reports context
conflicts while preserving the controlling value. It does not follow inapplicable
or conflicting guidance and explains why. Neither field is an enforceable check,
and workflows do not copy their text into implementation files, specs, change
artifacts, or summaries unless the user separately requests that content.
**Archive and spec-sync input safety:**
Archive, bulk archive, and standalone sync use
`artifactPaths.specs.existingOutputPaths` from `openspec status --json` as the
only delta-spec source. A schema without a `specs` artifact, or a change whose
concrete output list is empty, has nothing to sync; other artifacts are not used
to infer delta specs.
Before a semantic merge writes a main spec, the workflow consumes current
`openspec instructions specs --change <name> --json` output. The returned
`specs` rules constrain only the main specs produced by that merge. Single archive
passes that snapshot into inline sync, standalone sync fetches it directly, and
bulk archive obtains every required snapshot before its first spec write. A
non-zero or invalid JSON archive/specs instruction response is a lookup failure,
not an empty input: the workflow stops before the affected spec write or change
move (for bulk archive, before any batch write or move).
This configuration does not change archive execution phases, user prompts,
filesystem operations, semantic merge ownership, the direct `openspec archive`
command, or the structure and output of artifact `rules`.
### Schema Resolution Order
When OpenSpec needs a schema, it checks in this order:
@@ -197,6 +260,10 @@ apply:
| `instruction` | AI instructions for creating this artifact |
| `requires` | Dependencies - which artifacts must exist first |
List artifacts in the order you want them written. `requires` decides what is
possible; the order of the `artifacts:` list decides what comes first when
several artifacts are ready at once.
### Templates
Templates are markdown files that guide the AI. They're injected into the prompt when creating that artifact.
@@ -346,6 +413,9 @@ Community schemas are not vendored into OpenSpec core — they live in their own
| Schema | Maintainer | Repository | Description |
|--------|-----------|-----------|-------------|
| `superpowers-bridge` | @JiangWay | [JiangWay/openspec-schemas](https://github.com/JiangWay/openspec-schemas/tree/main/superpowers-bridge) | Integrates OpenSpec's artifact governance with [obra/superpowers](https://github.com/obra/superpowers) execution skills (brainstorming, writing-plans, TDD via subagents, code review, finishing). Adds an evidence-first `retrospective` artifact filling a gap Superpowers does not natively cover. |
| `nanopm` | @nmrtn | [nmrtn/nanopm](https://github.com/nmrtn/nanopm/tree/main/openspec-schema) | PM-first workflow. Runs [nanopm](https://github.com/nmrtn/nanopm)'s planning pipeline (audit → strategy → roadmap → PRD) upstream of implementation. Bridges product planning to OpenSpec's spec-driven engineering workflow. Artifacts read from `.nanopm/` if present — proposal sources the audit, design sources the strategy, and tasks source the PRD breakdown. |
| `e2e-runbooks` | @Lukk17 | [Lukk17/openspec-schemas](https://github.com/Lukk17/openspec-schemas/tree/master/openspec/schemas/e2e-runbooks) | Capability-level end-to-end test runbooks. Each capability gets an immutable spec, an immutable tasks-template, and one timestamped run record per execution. Assertions are observable behaviour only (HTTP status, response body, persisted state — never log substrings); each run records start/end UTC, duration, and best-estimate LLM token consumption. |
| `anvil` | @jikkujoyce | [jikkujoyce/openspec-schemas](https://github.com/jikkujoyce/openspec-schemas/tree/main/schemas/anvil) | Spec-driven workflow with TDD discipline and an adversarial review step. Flow: `proposal` → `specs` → `design` → `review` → `test-plan` → `tasks` → `apply` → `verify`. `review` is written by a fresh-context, read-only reviewer (a second model when one is available) and emits a `VERDICT:` line telling the agent to gate `test-plan`, `tasks`, and `apply`; OpenSpec only checks that artifacts exist, so enforce the gate with your own CI or hook. `test-plan` maps every spec scenario to a named test and doubles as a red/green ledger that `verify` audits. |
> Want to contribute a community schema? Open an issue with a link to your repository, or submit a PR adding a row to this table.
+1
View File
@@ -85,6 +85,7 @@ There's a full flowchart and worked examples in [Workflows: When to Update vs St
## Where to go next
- [Workflows](workflows.md) - patterns, plus the update-vs-new decision guide
- [Reviewing a Change](reviewing-changes.md) - the two-minute pass on a plan before you build it
- [Explore First](explore.md) - the place to step back to when an idea needs rethinking
- [Commands](commands.md) - `/opsx:continue`, `/opsx:apply`, and `/opsx:verify` in detail
- [Concepts: Artifacts](concepts.md#artifacts) - what each artifact is for
+11 -2
View File
@@ -1,6 +1,6 @@
# Examples & Recipes
Real changes, start to finish. Each recipe shows the commands you'd type and what you'd see back, so you can match your situation to a pattern and copy it. These use the default **core** commands (`propose`, `explore`, `apply`, `sync`, `archive`); where the expanded set helps, it's noted.
Real changes, start to finish. Each recipe shows the commands you'd type and what you'd see back, so you can match your situation to a pattern and copy it. These use the default **core** commands (`propose`, `explore`, `apply`, `update`, `sync`, `archive`); where the expanded set helps, it's noted.
A reminder before you start: slash commands like `/opsx:propose` go in your **AI assistant's chat**, and `openspec` commands go in your **terminal**. If that's new, read [How Commands Work](how-commands-work.md) first. In the transcripts below, `You:` and `AI:` are the chat, and lines starting with `$` are the terminal.
@@ -140,7 +140,16 @@ AI: Created the change. The proposal states the goal (split the
Ready for implementation.
```
When you archive a change that doesn't touch specs, you can tell the terminal command to skip the spec step:
Declare the empty delta explicitly by setting `skip_specs: true` in the change's `.openspec.yaml`:
```yaml
schema: spec-driven
skip_specs: true
```
Without the marker, `openspec validate` rejects a change with zero deltas (so a forgotten specs phase still gets caught); with it, validation passes and `openspec status` shows the specs stage as explicitly skipped rather than pending. If the refactor turns out to change behavior after all, remove `skip_specs` from `.openspec.yaml` and write the delta specs — validate treats the marker plus spec files as a conflict, so the stale marker can't linger silently.
Archiving a marked change needs no extra flags (there are no deltas to merge). Independently, the `--skip-specs` flag tells the terminal command to skip the spec step explicitly:
```bash
$ openspec archive refactor-payment-module --skip-specs
+1 -1
View File
@@ -38,7 +38,7 @@ That's the point. Exploring costs you nothing and commits you to nothing. You ca
## It's already installed
Good news: `/opsx:explore` ships in the default **core** profile, right alongside `propose`, `apply`, `sync`, and `archive`. You don't need to enable anything. If OpenSpec is set up in your project, explore is ready in your AI chat. (As with all `/opsx:*` commands, you type it in your assistant's chat, not the terminal. See [How Commands Work](how-commands-work.md).)
Good news: `/opsx:explore` ships in the default **core** profile, right alongside `propose`, `apply`, `update`, `sync`, and `archive`. You don't need to enable anything. If OpenSpec is set up in your project, explore is ready in your AI chat. (As with all `/opsx:*` commands, you type it in your assistant's chat, not the terminal. See [How Commands Work](how-commands-work.md).)
## A full example
+4 -4
View File
@@ -22,7 +22,7 @@ Existing codebases are the main event. OpenSpec is brownfield-first: you do not
### Is it tied to one AI tool?
No. OpenSpec works with 25+ assistants, including Claude Code, Cursor, Windsurf, GitHub Copilot, Gemini CLI, Codex, and more. The full list and per-tool details are in [Supported Tools](supported-tools.md).
No. OpenSpec works with 30+ assistants, including Claude Code, Cursor, Devin Desktop, GitHub Copilot, Gemini CLI, Codex, and more. The full list and per-tool details are in [Supported Tools](supported-tools.md).
## Running commands
@@ -36,11 +36,11 @@ There isn't a separate mode to start. You open your AI assistant like normal and
### I typed a slash command and nothing happened. Why?
Most likely you typed it in the terminal instead of your AI chat, or the commands aren't installed yet. Run `openspec update` in your project, restart your assistant, then try typing `/opsx` in chat and watch for autocomplete. [Troubleshooting](troubleshooting.md#commands-dont-show-up) has the full checklist.
Most likely you typed it in the terminal instead of your AI chat, you used a spelling your tool doesn't register, or the commands aren't installed yet. If the files are missing — or you never set the tool up — run `openspec init`; `openspec update` only refreshes files that already exist. Then restart your assistant and use the form printed under "Getting started" — see [How To Invoke](supported-tools.md#how-to-invoke). [Troubleshooting](troubleshooting.md#commands-dont-show-up) has the full checklist.
### Why is the syntax `/opsx:propose` in one tool and `/opsx-propose` in another?
Each AI tool surfaces custom commands a little differently. The intent is identical; only the punctuation changes. Type a slash in your chat and the autocomplete shows you the form your tool expects. The per-tool table is in [How Commands Work](how-commands-work.md#slash-command-syntax-by-tool).
Each AI tool surfaces custom commands a little differently, and OpenSpec spells them the way your tool loads the file it wrote. A command file named `opsx-propose.md` is typed `/opsx-propose`; one filed under `commands/opsx/` is typed `/opsx:propose`. Tools that take skills instead of commands use the skill name — Codex needs `$openspec-propose`, Kimi Code `/skill:openspec-propose`. The `openspec init` "Getting started" line already prints the right form for the tools you picked; the full table is in [How To Invoke](supported-tools.md#how-to-invoke).
### What's the difference between a skill and a command?
@@ -66,7 +66,7 @@ Explore to think it through, propose to draft the plan, apply to build it, archi
### What are `core` and expanded profiles?
A profile decides which slash commands get installed. **Core** (the default) gives you `propose`, `explore`, `apply`, `sync`, `archive`. The **expanded** set adds `new`, `continue`, `ff`, `verify`, `bulk-archive`, and `onboard` for finer control. Switch with `openspec config profile`, then apply with `openspec update`.
A profile decides which slash commands get installed. **Core** (the default) gives you `propose`, `explore`, `apply`, `update`, `sync`, `archive`. The **expanded** set adds `new`, `continue`, `ff`, `verify`, `bulk-archive`, and `onboard` for finer control. Switch with `openspec config profile`, then apply with `openspec update`.
### Do I need to run `/opsx:sync`?
+5 -1
View File
@@ -24,6 +24,8 @@ AI CHAT /opsx:archive (specs updated, change filed away)
Two terminal steps to set up, then you live in chat. The rest of this guide unpacks what each step does and what you'll see.
**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).
## How It Works
@@ -45,7 +47,7 @@ Start with `/opsx:explore` when you're figuring out what to do, or jump straight
/opsx:new ──► /opsx:ff or /opsx:continue ──► /opsx:apply ──► /opsx:verify ──► /opsx:archive
```
The default global profile is `core`, which includes `propose`, `explore`, `apply`, `sync`, and `archive`. You can enable the expanded workflow commands with `openspec config profile` and then `openspec update`.
The default global profile is `core`, which includes `propose`, `explore`, `apply`, `update`, `sync`, and `archive`. You can enable the expanded workflow commands with `openspec config profile` and then `openspec update`.
## What OpenSpec Creates
@@ -275,6 +277,8 @@ openspec view
## Next Steps
- [Explore First](explore.md) - Use `/opsx:explore` to think through an idea before you commit
- [Reviewing a Change](reviewing-changes.md) - What to check in the plan the AI drafts, before any code
- [Writing Good Specs](writing-specs.md) - What a strong requirement and scenario look like
- [Using OpenSpec in an Existing Project](existing-projects.md) - Start on a large brownfield codebase
- [Editing & Iterating on a Change](editing-changes.md) - Update artifacts, go back, reconcile manual edits
- [Core Concepts at a Glance](overview.md) - The whole mental model on one page
+1 -1
View File
@@ -54,7 +54,7 @@ Terms are grouped by topic, then alphabetized within each group.
**Command file.** A per-tool slash command file (`.../commands/opsx-*`). The older delivery mechanism, still supported alongside skills. You rarely touch these directly.
**Profile.** The set of slash commands installed in your project. **Core** (the default) is `propose`, `explore`, `apply`, `sync`, `archive`. The **expanded** set adds `new`, `continue`, `ff`, `verify`, `bulk-archive`, `onboard`. Change it with `openspec config profile`.
**Profile.** The set of slash commands installed in your project. **Core** (the default) is `propose`, `explore`, `apply`, `update`, `sync`, `archive`. The **expanded** set adds `new`, `continue`, `ff`, `verify`, `bulk-archive`, `onboard`. Change it with `openspec config profile`.
**Delivery.** Whether OpenSpec installs skills, command files, or both for your tools. Configured globally and applied with `openspec update`.
+31 -17
View File
@@ -21,7 +21,7 @@ openspec list # see active changes
openspec view # open the interactive dashboard
```
**The slash commands (chat half).** Short commands like `/opsx:propose` and `/opsx:apply` that you type into your AI assistant. These tell the AI to follow the OpenSpec workflow: draft a proposal, write specs, build from the task list, archive when done. You type these into Claude Code, Cursor, Windsurf, Copilot, or whichever assistant you use.
**The slash commands (chat half).** Short commands like `/opsx:propose` and `/opsx:apply` that you type into your AI assistant. These tell the AI to follow the OpenSpec workflow: draft a proposal, write specs, build from the task list, archive when done. You type these into Claude Code, Cursor, Devin Desktop, Copilot, or whichever assistant you use.
```text
/opsx:propose add-dark-mode (typed in your AI chat)
@@ -51,7 +51,7 @@ You don't enter a special OpenSpec mode. You just open your AI coding assistant
So the real instructions are:
1. Open your AI coding assistant (Claude Code, Cursor, Windsurf, and so on) in your project.
1. Open your AI coding assistant (Claude Code, Cursor, Devin Desktop, and so on) in your project.
2. Type `/opsx:propose` in its chat, the same place you type any other request.
3. Watch the autocomplete: if OpenSpec is installed, you'll see `/opsx:propose`, `/opsx:apply`, and friends appear as you type the slash.
@@ -61,37 +61,50 @@ One thing that *is* genuinely interactive lives in the terminal: `openspec view`
## Why this split exists
It's worth understanding, because it explains why OpenSpec works with 25+ different AI tools.
It's worth understanding, because it explains why OpenSpec works with 30+ different AI tools.
The CLI is the **engine**. It knows the rules: what a change folder looks like, which artifacts depend on which, how to merge a delta spec into your source of truth. It's the same everywhere.
The slash commands are the **steering wheel**, and every AI tool has a slightly different one. Claude Code calls them commands. Cursor and Windsurf have their own formats. Some tools call them skills. When you run `openspec init`, OpenSpec generates the right kind of file for each tool you selected, so the same `/opsx:propose` intent works no matter which assistant you prefer.
The slash commands are the **steering wheel**, and every AI tool has a slightly different one. Claude Code calls them commands. Cursor and Devin Desktop have their own formats. Some tools call them skills. When you run `openspec init`, OpenSpec generates the right kind of file for each tool you selected, so the same `/opsx:propose` intent works no matter which assistant you prefer.
The strength of this design: you learn the workflow once and carry it across tools. The tradeoff: the exact syntax of a command can differ slightly between tools, which is the next section.
## Slash command syntax by tool
The intent is identical everywhere. The punctuation differs. Use the form that matches your assistant.
The intent is identical everywhere. The spelling follows the file your tool loads.
| Tool | How you type it |
|------|-----------------|
| Claude Code | `/opsx:propose`, `/opsx:apply` |
| Cursor | `/opsx-propose`, `/opsx-apply` |
| Windsurf | `/opsx-propose`, `/opsx-apply` |
| GitHub Copilot (IDE) | `/opsx-propose`, `/opsx-apply` |
| Kimi CLI | skill-style, e.g. `/skill:openspec-propose` |
| Trae | skill-style, e.g. `/openspec-propose` |
| Your tool's command file | How you type it | Example tools |
|--------------------------|-----------------|---------------|
| `.../commands/opsx/<id>.*` | `/opsx:propose` | Claude Code, Gemini CLI, Crush |
| `.../opsx-<id>.*` | `/opsx-propose` | Cursor, GitHub Copilot (IDE), Devin Desktop, Trae, Oh My Pi |
| `.amazonq/prompts/opsx-<id>.md` | `@opsx-propose` | Amazon Q Developer |
| none — skills only | `/openspec-propose` | CodeArts, ForgeCode, Hermes, Mistral Vibe |
| none — Kimi Code | `/skill:openspec-propose` | Kimi Code |
| none — Codex CLI | `$openspec-propose` | Codex |
Most tools use either the colon form (`/opsx:propose`) or the dash form (`/opsx-propose`). A few tools surface OpenSpec as named skills instead of slash commands; for those you invoke the skill by name. The full per-tool list, including exactly which files get written where, lives in [Supported Tools](supported-tools.md).
Devin is the one tool that spans two rows. Devin Desktop reads
`.devin/workflows/`, so `/opsx-propose` works there; [Devin Local does
not](https://docs.devin.ai/desktop/devin-local), so on that agent use the
`/openspec-propose` skill instead. The skills OpenSpec writes to
`.devin/skills/` work on both, which is why they reference each other by skill
name.
When in doubt, type a slash in your AI chat and look at the autocomplete. Your tool will show you the form it expects.
Every tool is listed in [How To Invoke](supported-tools.md#how-to-invoke) — that
table is the authoritative one. Two rows are not slash commands at all: Amazon Q
loads its files into a prompt library invoked with `@`, and the last three rows
use the *skill* name, which is not the command id (`/opsx:apply` is the
`openspec-apply-change` skill).
When in doubt, read the "Getting started" line `openspec init` printed: it already
uses the form your tools registered. Typing a slash and watching the autocomplete
works too, for the tools that surface slash commands at all.
## How the commands got there: skills and commands
When you run `openspec init` (or `openspec update`), OpenSpec writes small files into your project so your AI tool can find the workflow. Depending on your tool and settings, these are **skills**, **commands**, or both.
- **Skills** live in places like `.claude/skills/openspec-*/SKILL.md`. They're the emerging cross-tool standard: a folder of instructions your assistant auto-detects.
- **Commands** live in places like `.claude/commands/opsx/<id>.md`. They're the older per-tool slash command files.
- **Commands** live in places like `.cursor/commands/opsx-<id>.md` or `.claude/commands/opsx/<id>.md` — the layout is the tool's, and it decides how you type the command. They're the older per-tool slash command files. Codex does not get generated command files; use `.codex/skills/openspec-*`.
You don't have to care which one your tool uses. You just type the slash command and it works. But knowing these files exist helps when something goes wrong: if your commands vanish, it usually means these files are missing or stale, and `openspec update` regenerates them.
@@ -101,7 +114,7 @@ See [Supported Tools](supported-tools.md) for the exact paths per tool, and [Mig
Quick checks, fastest first:
1. **Type a slash in your AI chat.** Start typing `/opsx` and watch for autocomplete suggestions. If they appear, you're set.
1. **Type a slash in your AI chat.** Start typing `/opsx` and watch for autocomplete suggestions. If they appear, you're set. On a skills-only tool (Codex, Kimi Code, CodeArts, ForgeCode, Hermes, Mistral Vibe) `/opsx` never completes even on a healthy install — try the skill name from the table above instead.
2. **Look for the files.** For Claude Code, check that `.claude/skills/` contains `openspec-*` folders. Other tools use their own directories ([Supported Tools](supported-tools.md) lists them).
3. **Re-run setup.** From your project root, run `openspec update`. This regenerates the skill and command files for whatever tools you configured.
4. **Restart your assistant.** Many tools scan for skills and commands at startup, so a fresh window can be the missing step.
@@ -113,6 +126,7 @@ By default, OpenSpec installs the **core** set of slash commands:
- `/opsx:explore`: think through an idea with the AI before committing to a change (great first step when you're unsure)
- `/opsx:propose`: create a change and draft all its planning artifacts in one step
- `/opsx:apply`: build the change by working through its task list
- `/opsx:update`: revise a change's planning artifacts and keep them coherent
- `/opsx:sync`: merge a change's spec updates into your main specs (usually automatic)
- `/opsx:archive`: finish a change and file it away
+75 -1
View File
@@ -4,6 +4,78 @@
- **Node.js 20.19.0 or higher** — Check your version: `node --version`
## Install with your AI assistant
Rather not do this by hand? Paste the prompt below into any coding assistant that can run shell commands — Claude Code, Codex, Cursor, Gemini CLI, Copilot, and the rest of the [supported tools](supported-tools.md). It installs the CLI, initializes this project, and reports back what actually happened.
The manual steps below are the source of truth — the prompt just runs them for you. If your assistant stops and hands something back, that's by design: it asks before anything privileged and never edits your shell startup files. Finish those bits yourself with [Package Managers](#package-managers) and [Troubleshooting](troubleshooting.md).
```text
Install OpenSpec in this project and set it up for me. Follow these steps in
order, and stop where a step tells you to stop.
1. RUNTIME. Run `node --version`. OpenSpec needs Node.js 20.19.0 or higher. If
Node is missing or older, say so and stop — don't install Node, switch
versions, or reconfigure my version manager for me.
2. INSTALL. Use whichever package manager is already on my PATH, preferring npm:
npm install -g @fission-ai/openspec@latest
pnpm add -g @fission-ai/openspec@latest
bun add -g @fission-ai/openspec@latest
yarn global add @fission-ai/openspec@latest (Yarn 1.x only)
Don't pick based on this project's lockfile — a global install has nothing to
do with how this repo's own dependencies are installed. If none of those four
is available, stop and tell me — don't improvise an install. (If I'm on Nix,
point me at the Nix section of the OpenSpec installation docs instead.)
Show me the exact command and let me confirm before you run it; this installs
software outside the project, and I may want a different package manager to
own it.
Stop and ask me again if the install needs sudo or admin rights, fails with a
permissions error, or reports that its global bin directory is missing or
unconfigured. Never edit my shell startup files (.bashrc, .zshrc, .profile,
fish, PowerShell profile), and never run a setup command that edits them for
me — show me the change and let me make it.
3. PATH. Run `openspec --version`. If the command isn't found, it may just be
missing from this shell: tell me where the package manager installed it and
how to add that directory to PATH for my shell and OS, then stop until I
confirm. If it prints an older version than the one the install just
reported, an earlier copy is shadowing it on PATH — tell me both versions
instead of continuing. If I use a version manager, say so rather than editing
PATH around it: with nvm or fnm the CLI is tied to the Node version that was
active when you installed it, and with asdf or volta a shim may need
regenerating.
4. INITIALIZE. Ask me which AI coding tool or tools I use and map each to an id
from `openspec init --help` (Copilot is `github-copilot`, Zoo Code is
`roocode`). `--tools` takes a comma-separated list, so name all of them.
`openspec init --tools <ids>` deletes leftovers from older OpenSpec versions
automatically, without asking — including `opsx-*.md` prompt files in my home
directory (Codex keeps them in ~/.codex/prompts). Before you run it, look for
those: `.../commands/openspec/` folders, OpenSpec marker blocks in files like
CLAUDE.md or AGENTS.md, and home-directory `opsx-*.md` prompts. List whatever
you find and wait for my go-ahead; if you find nothing, say so and carry on
without asking. An existing `openspec/` folder is not a problem — init
refreshes it and leaves my specs and changes alone.
Confirm I'm in the right folder too: init creates `openspec/` wherever it
runs, including inside a monorepo package.
Then run: openspec init --tools <ids>
5. REPORT. Don't assume what should exist — tell me what init actually printed:
how many skills and/or commands it created and where, the config file line,
any "Setup required" note, and what to restart or reload. Some tools are
skills-only and correctly create zero command files, so missing commands is
not a failure on its own. If init said nothing was generated, relay the fix
it suggested instead of retrying. Finish by telling me how to invoke OpenSpec
in my tool, and take the exact spelling from the files init created rather
than from its summary line: the punctuation differs per tool (/opsx:propose
in some, /opsx-propose in others, @opsx-propose in Amazon Q), and tools that
get skills instead of commands are invoked by skill name (/openspec-propose,
or $openspec-propose in Codex, or /skill:openspec-propose in Kimi Code).
```
Nothing in the prompt is vendor-specific: it's plain instructions plus the same commands documented on this page. It works on macOS, Linux, and Windows, and it deliberately stops rather than improvising when a step needs your permission. Your assistant does need to be able to run shell commands — a few IDE integrations can't.
## Package Managers
### npm
@@ -24,6 +96,8 @@ pnpm add -g @fission-ai/openspec@latest
yarn global add @fission-ai/openspec@latest
```
Yarn 2 and later (Berry) removed the `global` command. On those versions, install OpenSpec with npm, pnpm, or bun instead — a global CLI doesn't need to share your project's package manager.
### bun
Bun can install OpenSpec globally, but OpenSpec currently runs on Node.js.
@@ -79,7 +153,7 @@ npm install -g @fission-ai/openspec@latest # or pnpm/yarn/bun equivalent
openspec update # run inside each project
```
`openspec update` regenerates the skill and command files for the tools you've configured, so your slash commands stay current with the installed version.
`openspec update` regenerates the skill and command files for the tools you've configured, so your slash commands stay current with the installed version. It also checks whether a newer CLI has been published and offers to upgrade, since upgrading is what makes new workflows available in the first place — see [CLI Reference](cli.md#openspec-update).
## Uninstalling
+12 -4
View File
@@ -8,7 +8,7 @@ OPSX replaces the old phase-locked workflow with a fluid, action-based approach.
| Aspect | Legacy | OPSX |
|--------|--------|------|
| **Commands** | `/openspec:proposal`, `/openspec:apply`, `/openspec:archive` | Default: `/opsx:propose`, `/opsx:apply`, `/opsx:sync`, `/opsx:archive` (expanded workflow commands optional) |
| **Commands** | `/openspec:proposal`, `/openspec:apply`, `/openspec:archive` | Default: `/opsx:propose`, `/opsx:explore`, `/opsx:apply`, `/opsx:update`, `/opsx:sync`, `/opsx:archive` (expanded workflow commands optional) |
| **Workflow** | Create all artifacts at once | Create incrementally or all at once—your choice |
| **Going back** | Awkward phase gates | Natural—update any artifact anytime |
| **Customization** | Fixed structure | Schema-driven, fully hackable |
@@ -43,10 +43,11 @@ Only OpenSpec-managed files that are being replaced:
- Claude Code: `.claude/commands/openspec/`
- Cursor: `.cursor/commands/openspec-*.md`
- Windsurf: `.windsurf/workflows/openspec-*.md`
- Devin Desktop, formerly Windsurf: `.windsurf/workflows/openspec-*.md`
- Cline: `.clinerules/workflows/openspec-*.md`
- Roo: `.roo/commands/openspec-*.md`
- GitHub Copilot: `.github/prompts/openspec-*.prompt.md` (IDE extensions only; not supported in Copilot CLI)
- Codex: OpenSpec now uses `.codex/skills/openspec-*`; legacy cleanup only targets OpenSpec's allowlisted prompt filenames in `$CODEX_HOME/prompts` or `~/.codex/prompts`, and only removes them after replacement skills exist.
- And others (Augment, Continue, Amazon Q, etc.)
The migration detects whichever tools you have configured and cleans up their legacy files.
@@ -84,7 +85,7 @@ Don't worry about getting it perfect. We're still learning what works best here,
Both `openspec init` and `openspec update` detect legacy files and guide you through the same cleanup process. Use whichever fits your situation:
- New installs default to profile `core` (`propose`, `explore`, `apply`, `sync`, `archive`).
- New installs default to profile `core` (`propose`, `explore`, `apply`, `update`, `sync`, `archive`).
- Migrated installs preserve your previously installed workflows by writing a `custom` profile when needed.
### Using `openspec init`
@@ -156,6 +157,8 @@ openspec init --force --tools claude
The `--force` flag skips prompts and auto-accepts cleanup.
This includes cleanup of OpenSpec-managed Codex prompt files in the global Codex prompt directory. Cleanup only targets OpenSpec's allowlisted legacy Codex prompt filenames, removes them only after replacement `.codex/skills/openspec-*` skills exist, and preserves all other files.
---
## Migrating project.md to config.yaml
@@ -287,6 +290,8 @@ Command availability is profile-dependent:
| `/opsx:propose` | Create a change and generate planning artifacts in one step |
| `/opsx:explore` | Think through ideas with no structure |
| `/opsx:apply` | Implement tasks from tasks.md |
| `/opsx:update` | Revise a change's planning artifacts and keep them coherent |
| `/opsx:sync` | Merge delta specs into main specs |
| `/opsx:archive` | Finalize and archive the change |
**Expanded workflow (custom selection):**
@@ -297,7 +302,6 @@ Command availability is profile-dependent:
| `/opsx:continue` | Create the next artifact (one at a time) |
| `/opsx:ff` | Fast-forward—create planning artifacts at once |
| `/opsx:verify` | Validate implementation matches specs |
| `/opsx:sync` | Merge delta specs into main specs |
| `/opsx:bulk-archive` | Archive multiple changes at once |
| `/opsx:onboard` | Guided end-to-end onboarding workflow |
@@ -407,6 +411,8 @@ OPSX uses the emerging **skills** standard:
Skills are recognized across multiple AI coding tools and provide richer metadata.
Codex is skills-only in OPSX. OpenSpec no longer generates Codex custom prompt files; use the generated `.codex/skills/openspec-*` directories instead.
---
## Continuing Existing Changes
@@ -561,7 +567,9 @@ project/
│ ├── openspec-propose/ # default core profile
│ ├── openspec-explore/
│ ├── openspec-apply-change/
│ ├── openspec-update-change/
│ ├── openspec-sync-specs/
│ ├── openspec-archive-change/
│ └── ... # expanded profile adds new/continue/ff/etc.
├── CLAUDE.md # OpenSpec markers removed, your content preserved
└── AGENTS.md # OpenSpec markers removed, your content preserved
+11 -3
View File
@@ -65,7 +65,7 @@ openspec init
This creates skills in `.claude/skills/` (or equivalent) that AI coding assistants auto-detect.
By default, OpenSpec uses the `core` workflow profile (`propose`, `explore`, `apply`, `sync`, `archive`). If you want the expanded workflow commands (`new`, `continue`, `ff`, `verify`, `bulk-archive`, `onboard`), configure them with `openspec config profile` and apply with `openspec update`.
By default, OpenSpec uses the `core` workflow profile (`propose`, `explore`, `apply`, `update`, `sync`, `archive`). If you want the expanded workflow commands (`new`, `continue`, `ff`, `verify`, `bulk-archive`, `onboard`), configure them with `openspec config profile` and apply with `openspec update`.
During setup, you'll be prompted to create a **project config** (`openspec/config.yaml`). This is optional but recommended.
@@ -163,6 +163,7 @@ rules:
| `/opsx:continue` | Create the next artifact (expanded workflow) |
| `/opsx:ff` | Fast-forward planning artifacts (expanded workflow) |
| `/opsx:apply` | Implement tasks, updating artifacts as needed |
| `/opsx:update` | Revise a change's planning artifacts and keep them coherent |
| `/opsx:verify` | Validate implementation against artifacts (expanded workflow) |
| `/opsx:sync` | Sync delta specs to main (default workflow, optional) |
| `/opsx:archive` | Archive when done |
@@ -208,6 +209,12 @@ Creates all planning artifacts at once. Use when you have a clear picture of wha
```
Works through tasks, checking them off as you go. If you're juggling multiple changes, you can run `/opsx:apply <name>`; otherwise it should infer from the conversation and prompt you to choose if it can't tell.
### Updating a change
```
/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).
### Finish up
```
/opsx:archive # Move to archive when done (prompts to sync specs if needed)
@@ -412,7 +419,7 @@ Examples in this section use the expanded command set (`new`, `continue`, etc.);
│ ▼ │
│ Skill Files (.claude/skills/openspec-*/SKILL.md) │
│ │
│ • Cross-editor compatible (Claude Code, Cursor, Windsurf) │
│ • Cross-editor compatible (Claude Code, Cursor, Devin) │
│ • Skills query CLI for structured data │
│ • Fully customizable via schema files │
│ │
@@ -497,7 +504,8 @@ Artifacts form a directed acyclic graph (DAG). Dependencies are **enablers**, no
│ │ {"id": "proposal", "status": "done"}, │ │
│ │ {"id": "specs", "status": "ready"}, ◄── First ready │ │
│ │ {"id": "design", "status": "ready"}, │ │
│ │ {"id": "tasks", "status": "blocked", "missingDeps": ["specs"]}│ │
│ │ {"id": "tasks", "status": "blocked", │ │
│ │ "missingDeps": ["specs", "design"]} │ │
│ │ ] │ │
│ │ } │ │
│ └────────────────────────────────────────────────────────────────────┘ │
+143
View File
@@ -0,0 +1,143 @@
# Reviewing a Change
OpenSpec's whole promise is that you and your AI **agree on what to build before any code is written.** That agreement only means something if you actually read what the AI drafted. This page is about the two minutes where you do that — what to open, in what order, and what to look for.
The bet is simple: catching a wrong turn in a one-paragraph plan is nearly free. Catching the same wrong turn in 300 lines of code is not. Review is where you collect on that bet.
## The two moments you review
There are exactly two:
```
/opsx:propose ──► REVIEW THE PLAN ──► /opsx:apply ──► REVIEW THE CODE ──► /opsx:archive
(before any code) (/opsx:verify)
```
1. **After `/opsx:propose`** (or `/opsx:ff`), before `/opsx:apply` — read the plan while it's still just words.
2. **After building**, with `/opsx:verify` — check that the code actually did what the plan said.
The first review is the one that saves you the most, and the one people skip. This page spends most of its time there.
## Read it in this order
A change is a folder of plain Markdown in `openspec/changes/<name>/`. Read the files in the order that lets you quit earliest if something's wrong:
```
openspec/changes/add-dark-mode/
├── proposal.md 1. the intent and scope ← if this is wrong, stop here
├── specs/…/spec.md 2. the requirements ← the heart of the review
├── design.md (only for bigger changes) — the technical approach
└── tasks.md 3. the plan of work
```
You don't need to read every line. You need to answer three questions, one per file.
## The proposal: is this the right problem?
Open `proposal.md` first. It captures the "why" and "what" — the intent, the scope, the approach in a paragraph or two.
**What good looks like:** one clear intent, a scope you recognize, and a reason this is worth doing now.
**Red flags:**
- It solves a slightly *different* problem than the one you asked for.
- The scope has grown — you asked for a theme toggle and the proposal also touches auth "while we're in there."
- It's vague. "Improve the settings page" is not a scope; "add a dark-mode toggle that respects the OS preference" is.
**The question to answer:** *Does this match what I actually asked for, and is anything sneaking in?* If the answer is no, stop — don't read further, fix the proposal (see [Pushing back](#pushing-back-is-cheap)).
## The spec deltas: is "done" defined correctly?
This is the heart of the review. The delta specs under `specs/` say what will be *true* when the change ships — as requirements and the scenarios that prove them:
```markdown
## ADDED Requirements
### Requirement: Dark Mode Toggle
The system SHALL let a user switch between light and dark themes.
#### Scenario: Respects the OS preference on first load
- GIVEN a user who has never set a theme
- WHEN they open the app on a device set to dark mode
- THEN the app renders in dark mode
```
**What a good requirement looks like:** one clear `SHALL`/`MUST` statement you could hand to a tester, and at least one scenario whose GIVEN/WHEN/THEN actually exercises that statement.
**Red flags:**
- **A vague requirement.** "The system SHALL be fast" can't be built or tested. What's fast?
- **A requirement with no scenario**, or a scenario that doesn't test the requirement it sits under.
- **The most valuable catch of all: what's missing.** The AI faithfully writes down what you *said*. Your job is to notice what you *forgot* to say. If you cared most about the OS-preference case and no scenario mentions it, that's the review paying for itself.
Read the deltas asking *would I be happy if the system did exactly — and only — this?* Nothing here is about code yet, so it stays cheap to change.
## The tasks: is the plan of work sane?
Open `tasks.md` last. It's the implementation checklist the AI will work through.
**What good looks like:** ordered steps, each traceable to a requirement, nothing mysterious.
**Red flags:**
- A task with no matching requirement (where did that come from?).
- One giant "implement the feature" task that hides all the real decisions.
- A task that touches something outside the scope you just approved.
You're not estimating or micromanaging here — you're checking that the plan matches the requirements you already accepted.
## Pushing back is cheap
If any of the three questions came back wrong, say so. There are no phases and nothing is locked — you fix it and move on. Two ways, exactly as in [Editing a change](editing-changes.md):
- **Edit the file yourself.** It's plain Markdown; change the scope line, tighten a requirement, delete a task.
- **Tell the AI what's wrong** and let it revise: *"drop the auth changes — out of scope,"* *"add a scenario for when the user has already picked a theme,"* *"split task 3 into schema and UI."*
Then re-read the part you changed. Re-draft until it's a plan you'd sign your name to. That back-and-forth *is* the product working.
## After the code: verify
Once the work is built, `/opsx:verify` is your second review. It re-reads the artifacts and the code and reports mismatches across three dimensions:
| Dimension | What it checks |
|-----------|----------------|
| **Completeness** | Every task done, every requirement implemented, scenarios covered |
| **Correctness** | The implementation matches the spec's intent, edge cases handled |
| **Coherence** | Design decisions actually show up in the code |
```
You: /opsx:verify
AI: Verifying add-dark-mode...
COMPLETENESS
✓ All 8 tasks in tasks.md are checked
✓ All requirements in specs have corresponding code
⚠ Scenario "Respects the OS preference on first load" has no test coverage
```
It flags issues as CRITICAL, WARNING, or SUGGESTION, and it does **not** block archiving — it surfaces the gaps and leaves the call to you. This is the difference between "did the AI write code" and "did it build what we agreed."
`/opsx:verify` is in the expanded profile. If you don't have it, turn it on with `openspec config profile` (then `openspec update`), or just re-read the change and the diff yourself.
## Right-size the review
Not every change earns the full pass. A one-file typo fix deserves a twenty-second skim. A change that touches auth, payments, or data you can't recover deserves every question above. The point was never ceremony — it's spending your attention where a mistake would be expensive, and skimming where it wouldn't.
## The two-minute checklist
- [ ] The proposal's intent matches what I asked for.
- [ ] Nothing extra has crept into the scope.
- [ ] Every requirement is specific enough to test.
- [ ] Every requirement has a scenario that actually exercises it.
- [ ] The case I care about most is covered.
- [ ] Tasks map to requirements; nothing is mysterious or out of scope.
- [ ] I'd be comfortable if the AI built exactly this and nothing more.
If all seven pass, run `/opsx:apply` with confidence. If any fail, that's not a setback — it's the two minutes doing its job.
## Where to go next
- [Writing Good Specs](writing-specs.md) — the flip side: how to draft requirements and scenarios worth approving.
- [Editing & Iterating on a Change](editing-changes.md) — the mechanics of changing a plan after you've started.
- [Workflows](workflows.md) — where review fits in the larger loop.
+28 -1
View File
@@ -148,6 +148,23 @@ The pointer is a fallback, never an override: an explicit `--store` always
wins, and if the repo grows real planning folders of its own, those win
(with a warning to remove the stale pointer).
**One default for every repo on your machine.** If you work across many
code repos that all plan into the same store, set it once, globally,
instead of adding the `store:` line to each repo:
```bash
openspec config set defaultStore team-plans
```
Now any command run outside a planning root — and with no `--store` and no
project pointer — resolves to `team-plans`. It sits at the bottom of the
precedence list, so `--store`, a local root, and a project `store:` pointer
all still win. The root banner and JSON `root` block report
`source: "global_default"` with the store id, so you can always tell a
machine-wide default from a repo's own pointer. Clear it with
`openspec config unset defaultStore`. If the id is not registered, commands
error and tell you to register it or clear the stale default.
## Story: requirements that cross team lines
A platform team owns the requirements. Product teams build against them,
@@ -289,7 +306,9 @@ Every normal command resolves its root the same way, in this order:
2. nearest openspec/ a real planning root here → this repo
(walking up from cwd)
3. store: pointer config.yaml declares a store → that store
4. none of the above stores registered on this → error with a
4. defaultStore global config sets a machine → that store
default
5. none of the above stores registered on this → error with a
machine? selection hint
no stores registered? → the current
directory
@@ -308,6 +327,14 @@ tells you which case you're in.
- **No sync, ever — by design.** OpenSpec never clones, pulls, or pushes.
A stale checkout shows stale specs until *you* pull; references are
indexed live from whatever is on disk.
- **Empty planning folders can be absent.** A new store may not have
`openspec/changes/`, `openspec/specs/`, or `openspec/changes/archive/` in Git
yet. That is accepted during the beta; those folders appear once normal
commands create files for them.
- **Pointer repos stay pointers.** A config-only repo whose
`openspec/config.yaml` declares `store: <id>` is treated as externalized
planning, not as a store checkout to register. Remove the `store:` line first
if you intentionally want to convert that repo into a local store root.
- **Some commands stay where they are.** `view`, `templates`, `schemas`,
and the deprecated noun forms (`openspec change show`, ...) act on the
current directory only — no `--store`.
+60 -11
View File
@@ -9,15 +9,57 @@ For each selected tool, OpenSpec can install:
1. **Skills** (if delivery includes skills): `.../skills/openspec-*/SKILL.md`
2. **Commands** (if delivery includes commands): tool-specific `opsx-*` command files
Codex is skills-only: OpenSpec installs `.codex/skills/openspec-*/SKILL.md` for Codex even when delivery is set to `commands`, and it does not generate Codex custom prompt files.
By default, OpenSpec uses the `core` profile, which includes:
- `propose`
- `explore`
- `apply`
- `update`
- `sync`
- `archive`
You can enable expanded workflows (`new`, `continue`, `ff`, `verify`, `bulk-archive`, `onboard`) via `openspec config profile`, then run `openspec update`.
## How To Invoke
These docs use `/opsx:propose` as the canonical name, but each tool spells it the
way it loads the file OpenSpec wrote. Find your tool's command path in the
[Tool Directory Reference](#tool-directory-reference) below, then match its shape here.
| Command file OpenSpec writes | You type | Tools |
|------------------------------|----------|-------|
| `.../commands/opsx/<id>.*` — an `opsx/` folder namespaces it | `/opsx:<id>` | Claude Code, CodeBuddy, Crush, Gemini CLI, Lingma, Qoder, ZCode |
| `.../opsx-<id>.*` — the filename is the command | `/opsx-<id>` | Every other tool with generated command files, except Amazon Q and Devin |
| `.devin/workflows/opsx-<id>.md` — read by only one of Devin's two agents | `/opsx-<id>` on Devin Desktop, `/openspec-<skill>` on Devin Local | Devin Desktop\*\*\*\* |
| `.amazonq/prompts/opsx-<id>.md` — a prompt, not a command | `@opsx-<id>` | Amazon Q Developer |
| none — skills only | `/openspec-<skill>` | CodeArts, ForgeCode, Hermes, Mistral Vibe |
| none — Kimi Code | `/skill:openspec-<skill>` | Kimi Code |
| none — Codex CLI | `$openspec-<skill>` | Codex ([`/openspec-<skill>` is not recognized](https://github.com/openai/codex/issues/11817)) |
So `/opsx:propose` is `/opsx-propose` in Cursor, `@opsx-propose` in Amazon Q, and
`$openspec-propose` in Codex.
Two things vary independently, which is why the rows do not collapse:
- **The name.** Rows 1–2 differ only in how the file names the command, and the
`opsx-<id>` / `opsx:<id>` stem is the same for every tool with generated
command files.
- **The wrapper.** Amazon Q loads its files into a prompt library invoked with
`@`. Skills-only tools generate no command files at all, so their last three
rows use *skill* names — listed under
[Generated Skill Names](#generated-skill-names) — which do not map one-to-one
onto command ids (`/opsx:apply` is the `openspec-apply-change` skill).
The command path patterns above are extension-neutral (`.*`) on purpose: the
extension is the tool's (`.toml` for Gemini CLI, `.prompt` for Continue,
`.prompt.md` for Kiro and GitHub Copilot), and a few tools show the name with
its extension in the picker. Match the directory shape, not the extension.
The files OpenSpec generates, and the "Getting started" hint printed after setup,
already use the right form for the tools you selected — so the fastest answer is
to read the hint.
## Tool Directory Reference
| Tool (ID) | Skills path pattern | Command path pattern |
@@ -28,8 +70,10 @@ You can enable expanded workflows (`new`, `continue`, `ff`, `verify`, `bulk-arch
| IBM Bob Shell (`bob`) | `.bob/skills/openspec-*/SKILL.md` | `.bob/commands/opsx-<id>.md` |
| Claude Code (`claude`) | `.claude/skills/openspec-*/SKILL.md` | `.claude/commands/opsx/<id>.md` |
| Cline (`cline`) | `.cline/skills/openspec-*/SKILL.md` | `.clinerules/workflows/opsx-<id>.md` |
| CodeArts (`codeartsagent`) | `.codeartsdoer/skills/openspec-*/SKILL.md` | Not generated (no command adapter; use skill-based `/openspec-*` invocations) |
| CodeBuddy (`codebuddy`) | `.codebuddy/skills/openspec-*/SKILL.md` | `.codebuddy/commands/opsx/<id>.md` |
| Codex (`codex`) | `.codex/skills/openspec-*/SKILL.md` | `$CODEX_HOME/prompts/opsx-<id>.md`\* |
| Codex (`codex`) | `.codex/skills/openspec-*/SKILL.md` | Not generated (skills-only; use `.codex/skills/openspec-*`) |
| Devin Desktop, formerly Windsurf (`devin`) | `.devin/skills/openspec-*/SKILL.md` | `.devin/workflows/opsx-<id>.md`\*\*\*\* |
| ForgeCode (`forgecode`) | `.forge/skills/openspec-*/SKILL.md` | Not generated (no command adapter; use skill-based `/openspec-*` invocations) |
| Continue (`continue`) | `.continue/skills/openspec-*/SKILL.md` | `.continue/prompts/opsx-<id>.prompt` |
| CoStrict (`costrict`) | `.cospec/skills/openspec-*/SKILL.md` | `.cospec/openspec/commands/opsx-<id>.md` |
@@ -38,25 +82,29 @@ You can enable expanded workflows (`new`, `continue`, `ff`, `verify`, `bulk-arch
| Factory Droid (`factory`) | `.factory/skills/openspec-*/SKILL.md` | `.factory/commands/opsx-<id>.md` |
| Gemini CLI (`gemini`) | `.gemini/skills/openspec-*/SKILL.md` | `.gemini/commands/opsx/<id>.toml` |
| GitHub Copilot (`github-copilot`) | `.github/skills/openspec-*/SKILL.md` | `.github/prompts/opsx-<id>.prompt.md`\*\* |
| 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` |
| Kimi CLI (`kimi`) | `.kimi/skills/openspec-*/SKILL.md` | Not generated (no command adapter; use skill-based `/skill:openspec-*` invocations) |
| 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` |
| Mistral Vibe (`vibe`) | `.vibe/skills/openspec-*/SKILL.md` | Not generated (no command adapter; use skill-based `/openspec-*` invocations) |
| Oh My Pi (`oh-my-pi`) | `.omp/skills/openspec-*/SKILL.md` | `.omp/commands/opsx-<id>.md` |
| OpenCode (`opencode`) | `.opencode/skills/openspec-*/SKILL.md` | `.opencode/commands/opsx-<id>.md` |
| Pi (`pi`) | `.pi/skills/openspec-*/SKILL.md` | `.pi/prompts/opsx-<id>.md` |
| Qoder (`qoder`) | `.qoder/skills/openspec-*/SKILL.md` | `.qoder/commands/opsx/<id>.md` |
| Qwen Code (`qwen`) | `.qwen/skills/openspec-*/SKILL.md` | `.qwen/commands/opsx-<id>.toml` |
| RooCode (`roocode`) | `.roo/skills/openspec-*/SKILL.md` | `.roo/commands/opsx-<id>.md` |
| Trae (`trae`) | `.trae/skills/openspec-*/SKILL.md` | Not generated (no command adapter; use skill-based `/openspec-*` invocations) |
| Windsurf (`windsurf`) | `.windsurf/skills/openspec-*/SKILL.md` | `.windsurf/workflows/opsx-<id>.md` |
\* Codex commands are installed in the global Codex home (`$CODEX_HOME/prompts/` if set, otherwise `~/.codex/prompts/`), not your project directory.
| Qwen Code (`qwen`) | `.qwen/skills/openspec-*/SKILL.md` | `.qwen/commands/opsx-<id>.md` |
| [Zoo Code](https://github.com/Zoo-Code-Org/Zoo-Code) (`roocode`) | `.roo/skills/openspec-*/SKILL.md` | `.roo/commands/opsx-<id>.md` |
| Trae (`trae`) | `.trae/skills/openspec-*/SKILL.md` | `.trae/commands/opsx-<id>.md` |
| ZCode (`zcode`) | `.zcode/skills/openspec-*/SKILL.md` | `.zcode/commands/opsx/<id>.md` |
\*\* GitHub Copilot prompt files are recognized as custom slash commands in IDE extensions (VS Code, JetBrains, Visual Studio). Copilot CLI does not currently consume `.github/prompts/*.prompt.md` directly.
\*\*\* Hermes loads skills from `~/.hermes/skills/` by default. To use project-local OpenSpec skills, add the project `.hermes/skills/` directory to `skills.external_dirs` in `~/.hermes/config.yaml`; Hermes then exposes skills with user-facing slash invocations such as `/openspec-propose`.
\*\*\*\* Windsurf was [rebranded to Devin Desktop](https://docs.devin.ai/desktop/devin-desktop-faq) on June 2, 2026, and its config directory moved: `.devin/` is the preferred read + write location, `.windsurf/` a legacy read-only fallback. OpenSpec follows the rename — the tool id is `devin`, and `--tools windsurf` still resolves to it so existing setup scripts keep working. A project still holding OpenSpec files in `.windsurf/` is offered the move on the next `openspec update`; declining leaves them in place, and files you wrote yourself are never touched. Workflows are invoked by filename, so `.devin/workflows/opsx-apply.md` is `/opsx-apply`. The [Devin Local agent does not support workflows](https://docs.devin.ai/desktop/devin-local) — only skills, and it does not read `.windsurf/` at all — so whenever OpenSpec writes Devin skills it keeps their bodies, and the getting-started hint, on `/openspec-*` skill invocations, which work on both agents. Under commands-only delivery no skills are written and both fall back to `/opsx-*`.
## Non-Interactive Setup
For CI/CD or scripted setup, use `--tools` (and optionally `--profile`):
@@ -75,15 +123,15 @@ openspec init --tools none
openspec init --profile core
```
**Available tool IDs (`--tools`):** `amazon-q`, `antigravity`, `auggie`, `bob`, `claude`, `cline`, `codex`, `forgecode`, `codebuddy`, `continue`, `costrict`, `crush`, `cursor`, `factory`, `gemini`, `github-copilot`, `iflow`, `junie`, `kilocode`, `kimi`, `kiro`, `lingma`, `opencode`, `pi`, `qoder`, `qwen`, `roocode`, `trae`, `vibe`, `windsurf`
**Available tool IDs (`--tools`)** — `windsurf` is also accepted, as an alias for `devin`: `amazon-q`, `antigravity`, `auggie`, `bob`, `claude`, `cline`, `codeartsagent`, `codex`, `devin`, `forgecode`, `codebuddy`, `continue`, `costrict`, `crush`, `cursor`, `factory`, `gemini`, `github-copilot`, `hermes`, `iflow`, `junie`, `kilocode`, `kimi`, `kiro`, `lingma`, `vibe`, `oh-my-pi`, `opencode`, `pi`, `qoder`, `qwen`, `roocode`, `trae`, `zcode`
## Workflow-Dependent Installation
OpenSpec installs workflow artifacts based on selected workflows:
- **Core profile (default):** `propose`, `explore`, `apply`, `sync`, `archive`
- **Core profile (default):** `propose`, `explore`, `apply`, `update`, `sync`, `archive`
- **Custom selection:** any subset of all workflow IDs:
`propose`, `explore`, `new`, `continue`, `apply`, `ff`, `sync`, `archive`, `bulk-archive`, `verify`, `onboard`
`propose`, `explore`, `new`, `continue`, `apply`, `update`, `ff`, `sync`, `archive`, `bulk-archive`, `verify`, `onboard`
In other words, skill/command counts are profile-dependent and delivery-dependent, not fixed.
@@ -96,6 +144,7 @@ When selected by profile/workflow config, OpenSpec generates these skills:
- `openspec-new-change`
- `openspec-continue-change`
- `openspec-apply-change`
- `openspec-update-change`
- `openspec-ff-change`
- `openspec-sync-specs`
- `openspec-archive-change`
+74
View File
@@ -0,0 +1,74 @@
# OpenSpec on a Team
Everything in the other guides works the same whether you're solo or on a team of twenty. What changes on a team is the questions around the edges: where do the specs live, how do teammates review a plan, and how does any of this fit the pull-request flow we already have?
The short answer: a change is just files, and OpenSpec never touches git. So it fits your existing workflow instead of replacing it. This page spells out the conventions that work well.
## One rule: OpenSpec doesn't touch git
OpenSpec reads and writes plain Markdown under `openspec/`. It never commits, branches, pushes, or pulls in your project — and it never clones or syncs a [store](stores-beta/user-guide.md) on its own. That means:
- **You commit `openspec/` like any source.** Specs, active changes, and the archive are part of your project's history. (Yes, commit the whole folder — see the [FAQ](faq.md#should-i-commit-the-openspec-folder-to-git).)
- **A change is a folder you version like code.** `openspec/changes/add-dark-mode/` is just files on a branch.
- **Everything below is convention, not enforcement.** OpenSpec won't make you do it this way; it just fits cleanly.
## The everyday loop
The workflow that works well maps a change onto a branch and a pull request:
```
git switch -c add-dark-mode start a branch, as usual
│
/opsx:propose add-dark-mode draft the plan (proposal + specs + tasks)
│
REVIEW THE PLAN you read it before any code — see Reviewing a Change
│
/opsx:apply build it; artifacts + code change together
│
git commit && open a PR the PR contains the spec delta AND the code
│
teammate reviews, merges
│
/opsx:archive fold the delta into specs/, move the change to archive/
```
The plan and the code live side by side in the same branch, so your teammates review both together, and six months later the archived spec still explains why the code looks the way it does.
## Reviewing specs in a pull request
This is where a team feels the payoff. When a PR includes the change's delta spec, the reviewer gets something a raw diff never gives them: **a plain-language statement of what this change is supposed to do**, before they read a single line of code.
A good review order for the reviewer:
1. **Read `proposal.md`** — is this the right problem and scope?
2. **Read the delta under `specs/`** — is "done" defined correctly? (This is the [Reviewing a Change](reviewing-changes.md) two-minute pass, now happening in the PR.)
3. **Then read the code diff** — does it deliver exactly those requirements?
A reviewer who disagrees with the *approach* can say so against the proposal, cheaply, instead of relitigating it across 300 lines of code. Put the delta spec near the top of the PR description, or point reviewers at the change folder, so they start there.
## When to archive
Archiving folds a change's deltas into your main `openspec/specs/` and moves the change folder to `openspec/changes/archive/YYYY-MM-DD-<name>/`. Because `specs/` is the **shared source of truth**, the timing matters on a team. Two workable conventions:
- **Archive after the PR merges (recommended).** The branch carries the active change; once it's merged to your main branch, archive there (often a tiny follow-up commit or a scheduled cleanup). This keeps the shared `specs/` moving forward only with work that actually shipped.
- **Archive inside the PR.** Simpler for small teams: the same PR that adds the code also syncs and archives. The tradeoff is that your `specs/` diff and your code diff land together, which can make the PR noisier.
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.
## Two people, parallel changes
Because changes are separate folders, they don't collide:
- **Different changes, different people — no problem.** `add-dark-mode` and `rate-limit-login` are different folders on different branches; they never touch each other until they both archive.
- **One change, one owner.** Two people editing the same change folder conflict exactly like two people editing the same file. Keep a change to a single author, or split it into two changes (another reason to [right-size](writing-specs.md#right-size-the-change)).
- **The one place conflicts show up is `specs/`.** If two changes both modify the *same* requirement, archiving the second one will conflict in `openspec/specs/…/spec.md` — resolve it like any merge conflict, keeping the requirement that reflects reality. This is rare, and it's a feature: it's git telling you two changes disagreed about how the system should behave.
## When planning outgrows one repo
Everything above assumes the plan lives in the code repo's own `openspec/` folder, which is the right default. When your planning genuinely spans several repos or teams — one feature touching three services, or requirements one team owns and others consume — that's what the beta **stores** feature is for: planning gets its own repo that any code repo can point at. Start with the [Stores User Guide](stores-beta/user-guide.md).
## Where to go next
- [Reviewing a Change](reviewing-changes.md) — the review pass, now inside your PR.
- [Writing Good Specs](writing-specs.md) — including how to right-size a change so it fits one branch.
- [Stores User Guide](stores-beta/user-guide.md) — planning that spans repos and teams.
+8 -2
View File
@@ -13,7 +13,9 @@ npm install -g @fission-ai/openspec@latest
openspec --version
```
If it installed but still isn't found, your global npm bin directory probably isn't on your `PATH`. Run `npm bin -g` to see where global binaries live, and make sure that path is in your shell profile.
If it installed but still isn't found, your global npm bin directory probably isn't on your `PATH`. Run `npm prefix -g` to see where global packages live: on macOS and Linux the binaries are in that directory's `bin/`, and on Windows they sit directly in it. Make sure that path is on your `PATH`. (`npm bin -g` was removed in npm 9.)
If you used the [AI-assisted install](installation.md#install-with-your-ai-assistant), this is the expected hand-off point: that prompt tells your assistant to show you the `PATH` change rather than edit your shell startup files itself.
### "Requires Node.js 20.19.0 or higher"
@@ -49,13 +51,15 @@ If `/opsx:propose` (or your tool's equivalent) doesn't appear or doesn't do anyt
This rewrites the skill and command files for every tool you've configured.
Instruction files come from the *installed* CLI, so an outdated CLI reports everything up to date without ever writing the newer workflows. `openspec update` now checks for that and offers to upgrade — take the offer if you see it.
3. **Restart your assistant.** Most tools scan for skills and commands at startup. A fresh window often does it.
4. **Confirm the files exist.** For Claude Code, check that `.claude/skills/` contains `openspec-*` folders. Other tools use their own directories, all listed in [Supported Tools](supported-tools.md).
5. **Check you initialized this project.** Skills are written per project. If you cloned a repo or switched folders, run `openspec init` (or `openspec update`) there.
6. **Confirm your tool supports command files.** A few tools (Kimi CLI, Trae, ForgeCode, Mistral Vibe) don't get generated `opsx-*` command files; they use skill-based invocations instead. The forms differ per tool: see [Supported Tools](supported-tools.md) and [How Commands Work](how-commands-work.md#slash-command-syntax-by-tool).
6. **Confirm your tool supports command files.** Codex, CodeArts, ForgeCode, Hermes, Kimi Code and Mistral Vibe don't get generated `opsx-*` command files; they use skill-based invocations instead, so `/opsx` will never autocomplete for them. Type `$openspec-propose` in Codex, `/skill:openspec-propose` in Kimi Code, and `/openspec-propose` in the rest. Amazon Q does get command files, but loads them into its prompt library rather than its slash menu — type `@opsx-propose` there, not `/opsx`. Every tool's form is listed in [How To Invoke](supported-tools.md#how-to-invoke).
## Working with changes
@@ -149,6 +153,8 @@ You're in CI or a non-interactive shell, and OpenSpec found old files to clean u
openspec init --force
```
For Codex, OpenSpec may detect old managed prompt files in `$CODEX_HOME/prompts` or `~/.codex/prompts`. That cleanup is limited to OpenSpec's allowlisted legacy Codex prompt filenames, and non-interactive `openspec init` removes only the files whose replacement `.codex/skills/openspec-*` skills exist. Non-interactive `openspec update` leaves all legacy cleanup untouched unless you pass `--force`.
### Commands didn't appear after migrating
Restart your IDE. Skills are detected at startup. If they still don't appear, run `openspec update` and check the file locations in [Supported Tools](supported-tools.md).
+4
View File
@@ -36,6 +36,7 @@ New installs default to `core`, which provides:
- `/opsx:explore`
- `/opsx:propose`
- `/opsx:apply`
- `/opsx:update`
- `/opsx:sync`
- `/opsx:archive`
@@ -474,6 +475,9 @@ For full command details and options, see [Commands](commands.md).
## Next Steps
- [Writing Good Specs](writing-specs.md) - What a strong requirement and scenario look like, and how to right-size a change
- [Reviewing a Change](reviewing-changes.md) - The two-minute pass on a drafted plan before any code
- [OpenSpec on a Team](team-workflow.md) - How changes fit branches and pull requests
- [Commands](commands.md) - Full command reference with options
- [Concepts](concepts.md) - Deep dive into specs, artifacts, and schemas
- [Customization](customization.md) - Create custom workflows
+103
View File
@@ -0,0 +1,103 @@
# Writing Good Specs
You rarely write a spec from a blank page. You describe a change in plain language, `/opsx:propose` drafts the requirements and scenarios, and then you make them good. This page is about that last part — what "good" looks like, and how to steer the AI toward it.
It's the companion to [Reviewing a Change](reviewing-changes.md): reviewing is catching the weak spots in a draft, writing is knowing what a strong one is made of.
## A spec is behavior, not code
A spec says what your system *does*, in terms anyone could check — not how it's built. It's made of **requirements** (statements of behavior) and **scenarios** (concrete examples that prove them).
```markdown
### Requirement: Session Timeout
The system SHALL expire a session after 30 minutes of inactivity.
#### Scenario: Idle timeout
- GIVEN an authenticated session
- WHEN 30 minutes pass with no activity
- THEN the session is invalidated and the user must re-authenticate
```
Keep the *how* — the queue, the library, the table schema — in `design.md` or the code. When behavior and implementation get mixed into one requirement, the requirement stops being testable and starts going stale the moment the code changes.
## What makes a good requirement
A good requirement is one behavior, stated so plainly you could hand it to someone else to test.
- **One statement, one `SHALL`/`MUST`.** If a requirement has three "and also" clauses, it's really three requirements. Split them.
- **Observable.** Someone outside the code should be able to tell whether it holds. "The system SHALL show an error banner when the upload exceeds 10 MB" is observable. "The system SHALL handle large uploads gracefully" is not.
- **The right strength.** OpenSpec uses the RFC 2119 keywords, and they mean different things:
| Keyword | Meaning |
|---------|---------|
| `MUST` / `SHALL` | A hard requirement. Non-negotiable. |
| `SHOULD` | A strong recommendation, with room for a justified exception. |
| `MAY` | Genuinely optional. |
Reach for `MUST`/`SHALL` by default. Use `SHOULD` only when you truly mean "unless there's a good reason not to."
The test for a requirement: *could a tester who's never seen the code tell whether it passed?* If not, it needs sharpening.
## What makes a good scenario
Scenarios are where a requirement earns its keep. Each one is a concrete GIVEN / WHEN / THEN that could become an automated test.
- **It exercises its requirement.** A scenario that just restates the requirement in other words tests nothing. Make it a specific situation with a specific outcome.
- **Cover the cases that matter, not just the happy path.** The valid login is easy. The empty input, the expired token, the second click, the thing that goes wrong — those are where bugs live, and where a scenario is worth the most.
- **Name the case in the title.** "Scenario: Rejects an expired token" tells a reviewer what's covered at a glance; "Scenario: Test 2" doesn't.
A useful habit: before approving, ask *what's the one case I'd be upset to see broken?* — and make sure a scenario names it.
## Pick the right kind of delta
A change describes its edits to the specs with three section types. Using the right one keeps your archived specs honest:
- **`## ADDED Requirements`** — brand-new behavior that didn't exist before.
- **`## MODIFIED Requirements`** — behavior that already existed and is changing. Include the full new version; a short note on what changed helps a reviewer.
- **`## REMOVED Requirements`** — behavior going away, with a line on why.
On archive, ADDED gets appended to the main spec, MODIFIED replaces the old version, and REMOVED is deleted. If you mark a real change as ADDED, you end up with two competing requirements; if you describe new behavior as MODIFIED, there's nothing to replace. When in doubt, open the current spec and see whether the requirement is already there.
One more section is worth knowing about. When your delta creates a capability that doesn't exist yet, open it with `## Purpose` — a sentence or two on what the capability is for. Archive uses it as the Purpose of the main spec it creates; skip it and you get a `TBD` placeholder to fill in by hand. An existing spec already has a Purpose, so a delta's is ignored there — edit `openspec/specs/<capability>/spec.md` directly to change one.
## Right-size the change
The single most common authoring mistake isn't a badly worded requirement — it's a change that's trying to be three changes.
**A good change has one intent you can say in a sentence.** "Add a dark-mode toggle." "Rate-limit the login endpoint." "Migrate sessions off cookies." If describing the change needs a lot of "and also," that's the signal to split it.
Signs a change is too big:
- The proposal's scope reads like a list of unrelated features.
- Reviewing it would take an afternoon, so nobody will.
- Two people couldn't work on it without colliding.
- Half the tasks could ship on their own.
Smaller changes are easier to review, easier to build in one focused session, and easier to reason about six months later when the archive is all that's left. You can always run several changes in parallel — see [Editing & iterating](editing-changes.md) and [Workflows](workflows.md).
The opposite also happens: a one-line typo fix doesn't need three requirements and a design doc. Match the ceremony to the stakes.
## How to steer the AI toward a good draft
Because `/opsx:propose` does the first draft, the quality of what you get back tracks the quality of what you give it. You don't have to write requirements by hand — you have to aim the AI well:
- **State the intent and the boundary.** *"Add a dark-mode toggle that follows the OS setting on first load — don't touch the existing theme API."* The out-of-scope half matters as much as the in-scope half.
- **Name the cases you care about.** *"Make sure there's a scenario for a user who already picked a theme manually."* The AI covers what you point at.
- **Then edit.** It's plain Markdown. Tighten a vague `SHALL`, delete a scenario that tests nothing, add the case it missed — or ask the AI to: *"the timeout requirement is vague, pin it to 30 minutes."*
Draft, sharpen, repeat. A few rounds of that produces a spec you'd trust, which is the whole point.
## A quick checklist
- [ ] Each requirement is one observable behavior with a `SHALL`/`MUST`.
- [ ] No implementation details are baked into the requirements.
- [ ] Every requirement has at least one scenario that actually exercises it.
- [ ] The important edge and error cases have scenarios, not just the happy path.
- [ ] Deltas use ADDED / MODIFIED / REMOVED correctly against the current spec.
- [ ] The whole change has one intent you can state in a sentence.
## Where to go next
- [Reviewing a Change](reviewing-changes.md) — the two-minute pass that catches what slipped through.
- [Concepts](concepts.md) — the deeper model behind specs, changes, and deltas.
- [Examples & Recipes](examples.md) — real changes from start to finish.
+3 -3
View File
@@ -51,11 +51,11 @@
inherit (finalAttrs) pname version src;
pnpm = pkgs.pnpm_9;
fetcherVersion = 3;
hash = "sha256-cFY6phUPK4IOthG/aOtMenyQlLYCCilcOIG+G+v/q04=";
hash = "sha256-AHPKWjhrk4aTJvp9uqTJk15vASEZyRUoSw0W9oV2650=";
};
nativeBuildInputs = with pkgs; [
nodejs_20
nodejs_22
npmHooks.npmInstallHook
pnpmConfigHook
pnpm_9
@@ -97,7 +97,7 @@
{
default = pkgs.mkShell {
buildInputs = with pkgs; [
nodejs_20
nodejs_22
pnpm_9
];
@@ -1,136 +0,0 @@
# Add Artifact Regeneration Support
## Problem
Currently, there is **no way to regenerate artifacts** in the OPSX workflow:
- `/opsx:apply` just reads whatever's on disk
- `/opsx:continue` only creates the NEXT artifact - won't touch existing ones
If you edit `design.md` after `tasks.md` exists, your only options are:
1. Delete tasks.md manually, then run `/opsx:continue`
2. Edit tasks.md manually
The documentation claims you can "update artifacts mid-flight and continue" but there's no mechanism that actually supports this.
## Proposed Solution
Two parts:
### Part 1: Staleness Detection
Add artifact staleness detection to `/opsx:apply`:
1. **Track modification times**: When generating an artifact, record the mtime of its dependencies
2. **Detect staleness**: When `/opsx:apply` runs, check if upstream artifacts (design.md, specs) have been modified since tasks.md was generated
3. **Prompt user**: If stale, ask: "Design was modified after tasks were generated. Would you like to regenerate tasks with `/opsx:continue`?"
## User Experience
### Vision: Seamless Mid-Flight Correction
This is the workflow we want to enable (currently documented but not supported):
```
You: /opsx:apply
AI: Working through tasks...
✓ Task 1.1: Created caching layer
✓ Task 1.2: Added cache invalidation
Working on 1.3: Implement TTL...
I noticed the design assumes Redis, but your project uses
in-memory caching. Should I update the design?
You: Yes, update it to use the existing cache module.
AI: Updated design.md to use CacheManager from src/cache/
Updated tasks.md with revised implementation steps
Continuing implementation...
✓ Task 1.3: Implemented TTL using CacheManager
...
```
**No restart needed.** Just update the artifact and continue.
### Staleness Warning UX
When user manually edits an upstream artifact:
```
$ /opsx:apply
⚠️ Detected changes to upstream artifacts:
- design.md modified 5 minutes ago (after tasks.md was generated)
Options:
1. Regenerate tasks (recommended)
2. Continue anyway with current tasks
3. Cancel
>
```
### Part 2: Regeneration Capability
Add a way to regenerate specific artifacts:
```bash
# Option A: Flag on continue
/opsx:continue --regenerate tasks
# Option B: Separate command
/opsx:regenerate tasks
# Option C: Interactive prompt when staleness detected
/opsx:apply
# "Design changed. Regenerate tasks? [y/N]"
```
## Technical Approach
### Option A: Metadata File
Store `.openspec-meta.json` in change directory:
```json
{
"tasks.md": {
"generated_at": "2025-01-24T10:00:00Z",
"dependencies": {
"design.md": "2025-01-24T09:55:00Z",
"specs/feature/spec.md": "2025-01-24T09:50:00Z"
}
}
}
```
### Option B: Frontmatter
Add YAML frontmatter to generated artifacts:
```markdown
---
generated_at: 2025-01-24T10:00:00Z
depends_on:
- design.md@2025-01-24T09:55:00Z
---
# Tasks
...
```
### Option C: Git-based
Use git to detect if upstream files changed since downstream was last modified. No extra metadata needed but requires git.
## Non-Goals
- Automatic regeneration (user should always choose)
- Blocking apply entirely (just warn)
- Tracking code file changes (only artifact dependencies)
## Dependencies
- Should be implemented after `fix-midflight-update-docs` so docs are accurate first
- Could be combined with that change if desired
## Success Criteria
- User is warned when applying with stale artifacts
- Clear path to regenerate if needed
- No false positives (only warn when genuinely stale)
- Documentation claims become actually true
@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-06-04
@@ -0,0 +1,32 @@
## Why
- Windsurf has been [rebranded to **Devin Desktop**](https://docs.devin.ai/desktop/devin-desktop-faq) as of June 2, 2026. Same IDE, same editor, new brand.
- The rebrand moved the config directory: `.devin/` is now the preferred read + write location and `.windsurf/` the legacy read-only fallback, for `rules/`, `workflows/`, `skills/`, and `plans/`. OpenSpec writes only `.windsurf/`, so every Devin install lands in the deprecated path.
- Devin ships two agents. Devin Desktop (Cascade) reads workflows; the [Devin Local agent does not](https://docs.devin.ai/desktop/devin-local) — its docs say to migrate workflows to skills, and it does not read `.windsurf/` at all. An existing Windsurf user's OpenSpec files are therefore invisible to Devin Local entirely.
- Adding `devin` as a *second* tool id alongside `windsurf` would list one product twice in the picker and leave existing users with two parallel installs. This follows the rename instead, matching what OpenSpec already did for Kimi CLI → Kimi Code.
## What Changes
- **Rename the tool, don't duplicate it.** `windsurf` is retired as a tool id; `devin` (Devin Desktop) takes its place with `skillsDir: '.devin'` and `detectionPaths: ['.devin', '.windsurf']`. The Windsurf adapter is replaced by a Devin adapter writing `.devin/workflows/opsx-<id>.md`.
- **Keep `--tools windsurf` working.** A `TOOL_ID_ALIASES` map resolves retired ids, so existing setup scripts and CI keep running; they now configure `.devin/`.
- **Migrate existing installs, with consent.** OpenSpec-managed skills (`openspec-*`) and command files (`opsx-*`) under `.windsurf/` move to `.devin/`. `openspec update` explains the rebrand and asks first; `--force` and non-interactive runs take the move. Selecting the tool during `openspec init` is itself consent. Files the user wrote are never touched.
- Route Devin's **skill** bodies and the getting-started hint through the skill-reference transformer so they say `/openspec-*`, the one invocation both Devin agents accept.
- Update the tool reference, invocation, and command-syntax tables in `docs/`, plus the website tool list.
## Impact
- **Specs:** `ai-tool-paths`, `cli-init`, `cli-update`, `command-generation`
- **Code:**
- `src/core/command-generation/adapters/devin.ts` (new; `windsurf.ts` deleted)
- `src/core/command-generation/registry.ts`, `adapters/index.ts`, `index.ts`
- `src/core/config.ts` (`AI_TOOLS` row, `TOOL_ID_ALIASES`, `resolveToolIdAlias`)
- `src/core/migration.ts` (`LEGACY_TOOL_ROOTS`, consent-aware migration of skills *and* command files)
- `src/core/init.ts`, `src/core/update.ts` (alias resolution, migration prompt)
- `src/core/legacy-cleanup.ts` (pre-opsx `.windsurf/` files now key to `devin`)
- `src/utils/command-references.ts` (Devin's skill-reference transformer)
- **Docs:** `supported-tools.md`, `cli.md`, `commands.md`, `how-commands-work.md`, `faq.md`, `migration-guide.md`, `opsx.md`, website home page
## Notes
- **Who could be affected:** a user still on a pre-rebrand Windsurf build reads only `.windsurf/`. That is why the move is offered rather than taken — declining leaves every file where it is. Declining does mean `.windsurf/` stops being refreshed, which the prompt says plainly.
- The `.devin/` directory also covers `rules/` and `plans/`. OpenSpec writes neither, so they are out of scope and untouched.
@@ -0,0 +1,137 @@
# ai-tool-paths Delta Specification
## ADDED Requirements
### Requirement: Migrating OpenSpec content out of a renamed tool's former directory
When a tool's directory is renamed, OpenSpec-managed content left in the former
location SHALL be moved to the current one. Content the user wrote SHALL never
be moved or deleted.
Some renames are safe to apply silently and some are not, so each former root
declares whether leaving it needs the user's consent. Kimi CLI is gone, so
`.kimi` can be vacated without asking. Windsurf's `.windsurf` cannot: a
pre-rebrand Windsurf build reads only that directory, and nothing on disk
distinguishes that user from one who took the rebrand.
#### Scenario: Moving a former directory that needs no consent
- **WHEN** `openspec init` or `openspec update` runs and OpenSpec-managed content is found under a former root marked as needing no consent, such as `.kimi`
- **THEN** move it to the tool's current directory without prompting
- **AND** report what moved
#### Scenario: Offering a move that needs consent
- **GIVEN** OpenSpec skills or command files under `.windsurf/`
- **WHEN** `openspec update` runs interactively without `--force`
- **THEN** explain that Windsurf is now Devin Desktop, that `.devin/` is the current directory, and that Devin Local does not read `.windsurf/` at all
- **AND** ask before moving anything
- **AND** on decline, leave every file untouched and state that `.windsurf/` will no longer be refreshed until it is moved
#### Scenario: Unattended runs take the move
- **WHEN** `openspec update` runs with `--force`, or non-interactively
- **THEN** perform the move without prompting, reporting what moved
#### Scenario: Selecting a renamed tool is consent
- **WHEN** `openspec init` configures a tool that has OpenSpec content under a former root
- **THEN** move that content as part of setup, rather than leaving the user with two installs of one tool
#### Scenario: Both directories already hold OpenSpec content
- **GIVEN** the same OpenSpec-managed skill or command exists under both the former and the current root
- **WHEN** the move runs
- **THEN** the copy under the current root SHALL win, rather than being merged or overwritten
- **AND** only the file OpenSpec generated SHALL be removed from the former root — for a skill directory that is `SKILL.md` alone, never the directory and whatever else it holds
- **AND** one rule SHALL govern skills and command files alike: the former copy SHALL be removed only when it is byte-identical to the surviving one
- **AND** a former copy that differs SHALL be left where it is, since the difference may be a customization
- **AND** files left behind for that reason SHALL be reported, so the user knows two copies now exist
#### Scenario: Every former file differs, so nothing is movable
- **GIVEN** every OpenSpec-managed file under the former root differs from its counterpart under the current one
- **WHEN** the move runs
- **THEN** report the files left in place, rather than staying silent because nothing moved
- **AND** NOT offer to move anything, since there is nothing movable to consent to
- **AND** NOT report a migration that did not happen
#### Scenario: One root is a symbolic link to the other
- **GIVEN** the former and current roots resolve to the same directory, as when a user symlinks one at the other to straddle the rename
- **WHEN** the move runs
- **THEN** recognize that source and destination are the same file and change nothing, rather than deleting the only copy
#### Scenario: User files survive the move
- **GIVEN** a former root also holds files the user wrote, such as a hand-written workflow beside the generated ones
- **WHEN** the move runs
- **THEN** move only the files OpenSpec generates — each skill's `SKILL.md` and command files named `opsx-*`
- **AND** delete the former directory only when the move leaves it empty
#### Scenario: A user file beside a generated skill is not carried into a directory OpenSpec prunes
- **GIVEN** a former skill directory holds `SKILL.md` alongside a file the user wrote
- **AND** OpenSpec removes whole skill directories it owns, as under commands-only delivery or for a workflow outside the active profile
- **WHEN** the move runs
- **THEN** move `SKILL.md` alone and leave the user's file under the former root
- **AND** never move the enclosing directory, which would hand that file to a later removal
#### Scenario: The move is idempotent
- **WHEN** `openspec update` runs again after a completed move
- **THEN** find nothing to migrate and report nothing
## 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
- **GIVEN** RETIRED — Windsurf was rebranded to Devin Desktop and `windsurf` is no longer a tool id
- **WHEN** looking up the `windsurf` tool
- **THEN** no `AI_TOOLS` entry SHALL exist for it
- **AND** the id SHALL resolve to `devin`, whose `skillsDir` is `.devin` and whose `detectionPaths` still include the legacy `.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: Devin Desktop paths defined
- **WHEN** looking up the `devin` tool
- **THEN** `skillsDir` SHALL be `.devin`
- **AND** workflow files SHALL be written to `.devin/workflows/opsx-<id>.md`
- **AND** `detectionPaths` SHALL include both `.devin` and the legacy `.windsurf`, so a project set up before the rebrand is still recognized
#### Scenario: Retired tool ids resolve on the command line
- **WHEN** a retired brand is named on the command line, such as `--tools windsurf`
- **THEN** it SHALL resolve to the current tool id `devin` rather than erroring as unknown
- **AND** generation SHALL write the current directory `.devin/`, not the retired one
#### 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,72 @@
# cli-init Delta Specification
## MODIFIED Requirements
### Requirement: Skill Generation
The command SHALL generate Agent Skills for selected AI tools.
#### Scenario: Generating skills for a tool
- **WHEN** a tool is selected during initialization
- **THEN** create 9 skill directories under `.<tool>/skills/`:
- `openspec-explore/SKILL.md`
- `openspec-new-change/SKILL.md`
- `openspec-continue-change/SKILL.md`
- `openspec-apply-change/SKILL.md`
- `openspec-ff-change/SKILL.md`
- `openspec-verify-change/SKILL.md`
- `openspec-sync-specs/SKILL.md`
- `openspec-archive-change/SKILL.md`
- `openspec-bulk-archive-change/SKILL.md`
- **AND** each SKILL.md SHALL contain YAML frontmatter with name and description
- **AND** each SKILL.md SHALL contain the skill instructions
#### Scenario: Devin skills reference skills rather than workflows
- **GIVEN** the Devin Local agent does not support workflows and its documentation directs users to skills instead
- **WHEN** generating skills for the `devin` tool
- **THEN** rewrite `/opsx:<id>` references in the skill body to the matching `/openspec-<skill>` invocation, which both Devin agents accept
- **AND** the getting-started hint SHALL name `/openspec-propose` rather than a workflow
- **AND** under commands-only delivery, where no Devin skills are written, both the workflow bodies and the hint SHALL fall back to `/opsx-<id>`
### 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 Code skips command-file generation
- **WHEN** the user selects Kimi Code during initialization
- **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: Generating workflows for Devin Desktop
- **WHEN** the user selects Devin Desktop during initialization
- **THEN** create one workflow file per profile workflow at `.devin/workflows/opsx-<id>.md`
- **AND** include frontmatter with `name`, `description`, `category`, and `tags`
- **AND** rewrite `/opsx:<id>` references in the body to `/opsx-<id>`, the name Devin registers for a workflow file
@@ -0,0 +1,113 @@
# cli-update Delta Specification
## MODIFIED Requirements
### Requirement: Slash Command Updates
The update command SHALL refresh existing slash command files for configured tools without creating new ones, and ensure the OpenCode archive command accepts change ID arguments.
#### Scenario: Updating slash commands for Antigravity
- **WHEN** `.agent/workflows/` contains `openspec-proposal.md`, `openspec-apply.md`, and `openspec-archive.md`
- **THEN** refresh the OpenSpec-managed portion of each file so the workflow copy matches other tools while preserving the existing single-field `description` frontmatter
- **AND** skip creating any missing workflow files during update, mirroring the behavior for Devin Desktop and other IDEs
#### Scenario: Updating slash commands for Claude Code
- **WHEN** `.claude/commands/openspec/` contains `proposal.md`, `apply.md`, and `archive.md`
- **THEN** refresh each file using shared templates
- **AND** ensure templates include instructions for the relevant workflow stage
#### Scenario: Updating slash commands for CodeBuddy Code
- **WHEN** `.codebuddy/commands/openspec/` contains `proposal.md`, `apply.md`, and `archive.md`
- **THEN** refresh each file using the shared CodeBuddy templates that include YAML frontmatter for the `description` and `argument-hint` fields
- **AND** use square bracket format for `argument-hint` parameters (e.g., `[change-id]`)
- **AND** preserve any user customizations outside the OpenSpec managed markers
#### Scenario: Updating slash commands for Cline
- **WHEN** `.clinerules/workflows/` contains `openspec-proposal.md`, `openspec-apply.md`, and `openspec-archive.md`
- **THEN** refresh each file using shared templates
- **AND** include Cline-specific Markdown heading frontmatter
- **AND** ensure templates include instructions for the relevant workflow stage
#### Scenario: Updating slash commands for Continue
- **WHEN** `.continue/prompts/` contains `openspec-proposal.prompt`, `openspec-apply.prompt`, and `openspec-archive.prompt`
- **THEN** refresh each file using shared templates
- **AND** ensure templates include instructions for the relevant workflow stage
#### Scenario: Updating slash commands for Crush
- **WHEN** `.crush/commands/` contains `openspec/proposal.md`, `openspec/apply.md`, and `openspec/archive.md`
- **THEN** refresh each file using shared templates
- **AND** include Crush-specific frontmatter with OpenSpec category and tags
- **AND** ensure templates include instructions for the relevant workflow stage
#### Scenario: Updating slash commands for Cursor
- **WHEN** `.cursor/commands/` contains `openspec-proposal.md`, `openspec-apply.md`, and `openspec-archive.md`
- **THEN** refresh each file using shared templates
- **AND** ensure templates include instructions for the relevant workflow stage
#### Scenario: Updating slash commands for Factory Droid
- **WHEN** `.factory/commands/` contains `openspec-proposal.md`, `openspec-apply.md`, and `openspec-archive.md`
- **THEN** refresh each file using the shared Factory templates that include YAML frontmatter for the `description` and `argument-hint` fields
- **AND** ensure the template body retains the `$ARGUMENTS` placeholder so user input keeps flowing into droid
- **AND** update only the content inside the OpenSpec managed markers, leaving any unmanaged notes untouched
- **AND** skip creating missing files during update
#### Scenario: Updating slash commands for OpenCode
- **WHEN** `.opencode/commands/` contains OpenSpec-managed `opsx-*.md` command files for the configured profile (for example `opsx-propose.md`, `opsx-apply.md`, and `opsx-archive.md`)
- **THEN** refresh each file using shared templates
- **AND** transform command references to hyphen form (for example `/opsx-propose`), as for every tool whose command files are named `opsx-<id>`
- **AND** ensure templates include instructions for the relevant workflow stage
- **AND** ensure the archive command includes `$ARGUMENTS` placeholder in frontmatter for accepting change ID arguments
#### Scenario: Legacy OpenCode command path cleanup
- **WHEN** a project still has command files under the legacy singular path `.opencode/command/` (for example `opsx-*.md` or `openspec-*.md`)
- **THEN** `openspec init` or legacy cleanup SHALL remove those files and generate replacements under `.opencode/commands/`
- **AND** `openspec update` SHALL NOT refresh files that remain only under `.opencode/command/`
#### Scenario: Updating slash commands for Windsurf
- **WHEN** the legacy Windsurf location `.windsurf/workflows/`, now Devin's, contains `openspec-proposal.md`, `openspec-apply.md`, and `openspec-archive.md`
- **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)
#### Scenario: Updating workflows for Devin Desktop
- **WHEN** Devin Desktop is a configured tool (its `.devin/` directory exists)
- **THEN** write `.devin/workflows/opsx-<id>.md` for each workflow in the active profile, from shared templates
- **AND** emit frontmatter with `name`, `description`, `category`, and `tags`
- **AND** transform command references to hyphen form (for example `/opsx-propose`), the name Devin registers for a workflow file
- **AND** refresh `.devin/skills/openspec-*/SKILL.md` with `/openspec-*` skill references, the one invocation both Devin agents accept
#### Scenario: Updating slash commands for Kilo Code
- **WHEN** `.kilocode/workflows/` contains `openspec-proposal.md`, `openspec-apply.md`, and `openspec-archive.md`
- **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)
#### Scenario: Updating slash commands for Codex
- **GIVEN** the global Codex prompt directory contains `openspec-proposal.md`, `openspec-apply.md`, and `openspec-archive.md`
- **WHEN** a user runs `openspec update`
- **THEN** refresh each file using the shared slash-command templates (including placeholder guidance)
- **AND** preserve any unmanaged content outside the OpenSpec marker block
- **AND** skip creation when a Codex prompt file is missing
#### Scenario: Updating slash commands for GitHub Copilot
- **WHEN** `.github/prompts/` contains `openspec-proposal.prompt.md`, `openspec-apply.prompt.md`, and `openspec-archive.prompt.md`
- **THEN** refresh each file using shared templates while preserving the YAML frontmatter
- **AND** update only the OpenSpec-managed block between markers
- **AND** ensure templates include instructions for the relevant workflow stage
#### Scenario: Updating slash commands for Gemini CLI
- **WHEN** `.gemini/commands/openspec/` contains `proposal.toml`, `apply.toml`, and `archive.toml`
- **THEN** refresh the body of each file using the shared proposal/apply/archive templates
- **AND** replace only the content between `<!-- OPENSPEC:START -->` and `<!-- OPENSPEC:END -->` markers inside the `prompt = """` block so the TOML framing (`description`, `prompt`) stays intact
- **AND** skip creating any missing `.toml` files during update; only pre-existing Gemini commands are refreshed
#### Scenario: Updating slash commands for iFlow CLI
- **WHEN** `.iflow/commands/` contains `openspec-proposal.md`, `openspec-apply.md`, and `openspec-archive.md`
- **THEN** refresh each file using shared templates
- **AND** preserve the YAML frontmatter with `name`, `id`, `category`, and `description` fields
- **AND** update only the OpenSpec-managed block between markers
- **AND** ensure templates include instructions for the relevant workflow stage
#### Scenario: Missing slash command file
- **WHEN** a tool lacks a slash command file
- **THEN** do not create a new file during update
@@ -0,0 +1,45 @@
# command-generation Delta Specification
## MODIFIED Requirements
### Requirement: ToolCommandAdapter interface
The system SHALL define a `ToolCommandAdapter` interface for per-tool formatting.
#### Scenario: Adapter interface structure
- **WHEN** implementing a tool adapter
- **THEN** `ToolCommandAdapter` SHALL require:
- `toolId`: string identifier matching `AIToolOption.value`
- `getFilePath(commandId: string)`: returns file path for command (relative from project root, or absolute for global-scoped tools like Codex)
- `formatFile(content: CommandContent)`: returns complete file content with frontmatter
#### Scenario: Claude adapter formatting
- **WHEN** formatting a command for Claude Code
- **THEN** the adapter SHALL output YAML frontmatter with `name`, `description`, `category`, `tags` fields
- **AND** file path SHALL follow pattern `.claude/commands/opsx/<id>.md`
#### Scenario: Cursor adapter formatting
- **WHEN** formatting a command for Cursor
- **THEN** the adapter SHALL output YAML frontmatter with `name` as `/opsx-<id>`, `id`, `category`, `description` fields
- **AND** file path SHALL follow pattern `.cursor/commands/opsx-<id>.md`
#### Scenario: Windsurf adapter formatting
- **GIVEN** RETIRED — Windsurf was rebranded to Devin Desktop and its config directory moved
- **WHEN** looking for a Windsurf adapter
- **THEN** none SHALL be registered — it is replaced by the Devin adapter below, not kept alongside a second adapter for the same product
#### Scenario: Devin Desktop adapter formatting
- **WHEN** formatting a command for Devin Desktop
- **THEN** the adapter SHALL output YAML frontmatter with `name`, `description`, `category`, `tags` fields
- **AND** file path SHALL follow pattern `.devin/workflows/opsx-<id>.md`
#### Scenario: Trae adapter formatting
- **WHEN** formatting a command for Trae
- **THEN** the adapter SHALL output YAML frontmatter with `name` and `description` fields
- **AND** file path SHALL follow pattern `.trae/commands/opsx-<id>.md`
@@ -0,0 +1,44 @@
# Implementation Tasks
## 1. Adapter
- [x] 1.1 Add `src/core/command-generation/adapters/devin.ts`: `.devin/workflows/opsx-<id>.md`, frontmatter `name`/`description`/`category`/`tags` via the shared helpers in `command-generation/yaml.ts`.
- [x] 1.2 Keep the adapter a pure formatter: the `opsx-` filename prefix makes Devin a flat invocation, so the generator rewrites `/opsx:<id>` body references to `/opsx-<id>` — the name Devin registers for a workflow file.
- [x] 1.3 Delete `adapters/windsurf.ts` and its registry/barrel entries; register `devinAdapter` in their place.
## 2. Tool wiring
- [x] 2.1 Replace the `windsurf` row in `AI_TOOLS` with `devin` (`skillsDir: '.devin'`, `detectionPaths: ['.devin', '.windsurf']`). Detection, the init picker, `--tools` validation, update, and profile sync all derive from this row.
- [x] 2.2 Add `TOOL_ID_ALIASES` / `resolveToolIdAlias` in `src/core/config.ts` and apply it when parsing `--tools`, so `--tools windsurf` still resolves.
- [x] 2.3 Re-key the pre-opsx `.windsurf/workflows/openspec-*.md` entry in `LEGACY_SLASH_COMMAND_PATHS` to `devin` — that map's keys are tool ids.
- [x] 2.4 In `getTransformerForTool`, give `devin` the skill-reference transformer whenever skills are generated, so skill bodies and the getting-started hint say `/openspec-*` — the Devin Local agent has no workflows. Under commands-only delivery, fall through to the invocation rewrite.
## 3. Migration
- [x] 3.1 Replace `LEGACY_SKILLS_DIRS` with `LEGACY_TOOL_ROOTS`, each root carrying whether leaving it needs consent (`.kimi` no, `.windsurf` yes).
- [x] 3.2 Extend the move to command files, deriving the legacy path from the adapter's own `getFilePath` so no layout is hard-coded. Skip absolute paths.
- [x] 3.3 Split find from apply (`findLegacyToolMigrations` / `migrateLegacyToolDirs`) so a consent-gated move can be described before it happens.
- [x] 3.4 `openspec update`: explain the rebrand, prompt interactively, migrate under `--force` or non-interactively, and say plainly what declining costs.
- [x] 3.5 `openspec init`: treat selecting the tool as consent and migrate for the selected tools only.
## 4. Documentation
- [x] 4.1 `docs/supported-tools.md`: give Devin its own row in the authoritative "How To Invoke" table — the catch-all row would otherwise claim `/opsx-<id>` for both agents. Replace the Windsurf directory row and rewrite the footnote to cover the rename, the alias, and the migration.
- [x] 4.2 Drop `windsurf` from the `--tools` ID lists in `docs/cli.md` and `docs/supported-tools.md`, noting it is still accepted as an alias.
- [x] 4.3 Update the command-syntax tables in `docs/commands.md` and `docs/how-commands-work.md`, plus prose mentions in `faq.md`, `migration-guide.md`, `opsx.md`, and the website tool list.
## 5. Tests
- [x] 5.1 Adapter: tool id, `getFilePath`, and frontmatter. Hyphen rewriting is asserted end to end in the `generateCommand` flat-tool loop, and YAML escaping by the registry-derived parity matrix — both enroll Devin automatically.
- [x] 5.2 Detection: `.devin` and legacy `.windsurf` both resolve to `devin`; neither present means not detected.
- [x] 5.3 Alias: `--tools windsurf` writes `.devin/` and leaves no `.windsurf/`.
- [x] 5.4 Migration: skills and workflows move, user-authored files in `.windsurf/` survive, and a second run migrates nothing.
- [x] 5.5 `init`/`update`: both surfaces — `.devin/workflows/opsx-*.md` carry `/opsx-*`, `.devin/skills/openspec-*/SKILL.md` carry `/openspec-*`, and neither carries `/opsx:`.
- [x] 5.6 `getTransformerForTool` returns the skill transformer for Devin under `both`/`skills` delivery and the hyphen form under `commands`.
## 6. Verification
- [x] 6.1 `openspec validate add-devin-desktop-support --strict`.
- [x] 6.2 `openspec archive add-devin-desktop-support --yes` merges cleanly and additively (run on a scratch copy, then reverted).
- [x] 6.3 Full suite green.
- [x] 6.4 Manual journeys in scratch repos: legacy `.windsurf` install upgraded; both directories populated; IDE-written `.devin/rules/` preserved; `--tools windsurf` alias.
@@ -0,0 +1,27 @@
## Why
Every generated OpenSpec skill drives the `openspec` CLI (`openspec list`, `status`, `instructions`, …). Today the skill frontmatter never pre-approves those calls, so agents that gate Bash on permission prompt the user on every single `openspec` invocation. The workflow stalls on approvals for a first-party, read-mostly CLI the user already opted into by installing OpenSpec.
The Agent Skills standard already solves this: an `allowed-tools` frontmatter field pre-approves listed tools while a skill is active. We just aren't emitting it.
## What Changes
- Every generated `SKILL.md` gains `allowed-tools: Bash(openspec:*)` in its YAML frontmatter, so agents run `openspec` commands from the skill without prompting. Emitted centrally in `generateSkillContent`, so `init`, `update`, every tool's skills directory, and every current and future skill get it uniformly.
- Claude Code slash commands (`.claude/commands/opsx/*.md`) gain the same field — commands share the skill frontmatter contract, so the same pre-approval applies when a user runs `/opsx:*`.
- Scope is deliberately narrow: only the `openspec` CLI is pre-approved. Per the standard, `allowed-tools` pre-approves rather than restricts — so any other tool a skill or command uses (Read, Write, or arbitrary Bash for builds/tests in `apply`/`onboard`) stays available under the user's normal permission settings, still prompting as before.
- Cross-tool: skills go to every supported tool's skills directory, and `allowed-tools` is an Agent Skills standard field — tools that implement the standard honor it; tools that don't ignore the unknown key. Only the Claude command adapter changes, because no other tool's slash-command format defines a per-command pre-approval field.
## Capabilities
### Modified Capabilities
- `cli-init`: the Skill Generation requirement now specifies the `allowed-tools` pre-approval in generated skill frontmatter.
- `command-generation`: the Claude adapter frontmatter now includes the `allowed-tools` field.
## Impact
- `src/core/shared/allowed-tools.ts` — the shared `OPENSPEC_CLI_ALLOWED_TOOLS` constant (single source for both surfaces).
- `src/core/shared/skill-generation.ts` — emit `allowed-tools` in the SKILL.md frontmatter.
- `src/core/command-generation/adapters/claude.ts` — emit `allowed-tools` in the slash-command frontmatter.
- Tests: regenerated golden skill-content hashes; new assertions that every deployed skill and the Claude command format pre-approve the CLI.
- No behavior change for agents that ignore `allowed-tools`; pure upside for agents that honor it.
@@ -0,0 +1,28 @@
## MODIFIED Requirements
### Requirement: Skill Generation
The command SHALL generate Agent Skills for selected AI tools.
#### Scenario: Generating skills for a tool
- **WHEN** a tool is selected during initialization
- **THEN** create 9 skill directories under `.<tool>/skills/`:
- `openspec-explore/SKILL.md`
- `openspec-new-change/SKILL.md`
- `openspec-continue-change/SKILL.md`
- `openspec-apply-change/SKILL.md`
- `openspec-ff-change/SKILL.md`
- `openspec-verify-change/SKILL.md`
- `openspec-sync-specs/SKILL.md`
- `openspec-archive-change/SKILL.md`
- `openspec-bulk-archive-change/SKILL.md`
- **AND** each SKILL.md SHALL contain YAML frontmatter with name and description
- **AND** each SKILL.md SHALL contain the skill instructions
#### Scenario: Pre-approving the OpenSpec CLI in skill frontmatter
- **WHEN** generating a skill's YAML frontmatter
- **THEN** the frontmatter SHALL include an `allowed-tools` field with the value `Bash(openspec:*)`
- **AND** an agent that honors `allowed-tools` SHALL run `openspec` commands from the skill without prompting for approval
- **AND** because `allowed-tools` pre-approves rather than restricts, any other tool the skill uses SHALL remain available under the user's existing permission settings
@@ -0,0 +1,32 @@
## MODIFIED Requirements
### Requirement: ToolCommandAdapter interface
The system SHALL define a `ToolCommandAdapter` interface for per-tool formatting.
#### Scenario: Adapter interface structure
- **WHEN** implementing a tool adapter
- **THEN** `ToolCommandAdapter` SHALL require:
- `toolId`: string identifier matching `AIToolOption.value`
- `getFilePath(commandId: string)`: returns file path for command (relative from project root, or absolute for global-scoped tools like Codex)
- `formatFile(content: CommandContent)`: returns complete file content with frontmatter
#### Scenario: Claude adapter formatting
- **WHEN** formatting a command for Claude Code
- **THEN** the adapter SHALL output YAML frontmatter with `name`, `description`, `allowed-tools`, `category`, `tags` fields
- **AND** the `allowed-tools` field SHALL have the value `Bash(openspec:*)` so Claude Code runs `openspec` commands from the slash command without prompting for approval
- **AND** file path SHALL follow pattern `.claude/commands/opsx/<id>.md`
#### Scenario: Cursor adapter formatting
- **WHEN** formatting a command for Cursor
- **THEN** the adapter SHALL output YAML frontmatter with `name` as `/opsx-<id>`, `id`, `category`, `description` fields
- **AND** file path SHALL follow pattern `.cursor/commands/opsx-<id>.md`
#### Scenario: Windsurf adapter formatting
- **WHEN** formatting a command for Windsurf
- **THEN** the adapter SHALL output YAML frontmatter with `name`, `description`, `category`, `tags` fields
- **AND** file path SHALL follow pattern `.windsurf/workflows/opsx-<id>.md`
@@ -0,0 +1,15 @@
## 1. Implementation
- [x] 1.1 Add the shared `OPENSPEC_CLI_ALLOWED_TOOLS = 'Bash(openspec:*)'` constant (`src/core/shared/allowed-tools.ts`) and emit `allowed-tools` in the frontmatter built by `generateSkillContent`
- [x] 1.2 Emit the same `allowed-tools` field in the Claude command adapter's frontmatter (`src/core/command-generation/adapters/claude.ts`); other adapters unchanged — no other tool defines a per-command pre-approval field
## 2. Tests
- [x] 2.1 Regenerate the golden generated-content hashes in `skill-templates-parity.test.ts`
- [x] 2.2 Add a test asserting every deployed skill's generated content contains `allowed-tools: Bash(openspec:*)` (iterates the registry so new skills are covered)
- [x] 2.3 Assert the Claude adapter output contains the field (`adapters.test.ts`)
- [x] 2.4 Verify end-to-end: `openspec init --tools claude` emits the field in both SKILL.md and `.claude/commands/opsx/*.md`, and it parses as the YAML string `Bash(openspec:*)`
## 3. Release
- [x] 3.1 Add a changeset describing the auto-approval
@@ -2,13 +2,13 @@
OpenSpec currently assumes command delivery maps directly to command adapters. That assumption does not hold for all tools.
Trae is a concrete example: it invokes OpenSpec workflows via skill entries (for example `/openspec-new-change`) rather than adapter-generated command files. In this model, skills are the command surface.
Some tools expose OpenSpec workflows via skill entries rather than adapter-generated command files. Kimi CLI is a concrete example: it invokes skills with forms such as `/skill:openspec-new-change`. In this model, skills are the command surface.
Today, this creates a behavior gap:
- `delivery=commands` can remove skills
- tools without adapters skip command generation
- result: selected tools like Trae can end up with no invocable workflow artifacts
- result: selected tools like Kimi CLI, ForgeCode, or Mistral Vibe can end up with no invocable workflow artifacts
This is more than a prompt UX issue because non-interactive and CI flows bypass interactive guidance. We need a capability-aware model in core generation logic.
@@ -25,9 +25,13 @@ Add an optional field in tool metadata to describe how a tool exposes commands:
Field should be optional. Default behavior is inferred from adapter registry presence: tools with a registered adapter resolve to `adapter`; tools with no adapter registration and no explicit annotation resolve to `none`.
Capability values use kebab-case string tokens for consistency with serialized metadata conventions.
Initial explicit override:
Initial explicit overrides:
- Trae -> `skills-invocable`
- ForgeCode -> `skills-invocable`
- Kimi CLI -> `skills-invocable`
- Mistral Vibe -> `skills-invocable`
Trae no longer belongs in this override set once its `.trae/commands/opsx-<id>.md` adapter is available; it should resolve to `adapter` like other file-backed command integrations.
### 2. Make delivery behavior capability-aware
@@ -62,12 +66,12 @@ Update summaries to show effective delivery outcomes per tool (for example, when
### 4. Update docs and tests
- document capability model and Trae behavior under delivery modes
- document capability model and skills-invocable behavior under delivery modes
- ensure CLI docs and supported-tools docs reflect effective behavior
- add test coverage for:
- `init --tools trae` with `delivery=commands`
- `update` with Trae configured under `delivery=commands`
- mixed selections (`claude + trae`) across all delivery modes
- `init --tools kimi` with `delivery=commands`
- `update` with Kimi CLI configured under `delivery=commands`
- mixed selections (`claude + kimi`) across all delivery modes
- explicit error path for tools with no command surface under `delivery=commands`
### 5. Coordinate with install-scope behavior
@@ -94,7 +98,7 @@ Implementation tests should cover mixed-tool matrices to ensure deterministic be
## Impact
- `src/core/config.ts` - add optional command-surface metadata and Trae override
- `src/core/config.ts` - add optional command-surface metadata and skills-invocable tool overrides
- `src/core/command-generation/registry.ts` (or shared helper) - capability inference from adapter presence
- `src/core/init.ts` - capability-aware generation/removal planning + compatibility validation + summary messaging
- `src/core/update.ts` - capability-aware sync/removal planning + compatibility validation + summary messaging
@@ -9,7 +9,7 @@
- [ ] 1.1 Extend tool metadata in `src/core/config.ts` with an optional command-surface capability field
- [ ] 1.2 Define supported capability values: `adapter`, `skills-invocable`, `none`
- [ ] 1.3 Mark Trae as `skills-invocable`
- [ ] 1.3 Mark known skills-invocable tools such as ForgeCode, Kimi CLI, and Mistral Vibe as `skills-invocable`
- [ ] 1.4 Add a shared capability resolver (explicit metadata override first, inferred fallback from adapter presence second)
- [ ] 1.5 Add focused unit tests for capability resolution (explicit override, inferred adapter, inferred none)
@@ -20,7 +20,7 @@
- [ ] 2.3 In `delivery=commands`, fail fast before writes when any selected tool resolves to `none`
- [ ] 2.4 Update init output to clearly report effective behavior for `skills-invocable` tools (skills used as command surface)
- [ ] 2.5 Ensure init no longer reports "no adapter" for tools intentionally using `skills-invocable`
- [ ] 2.6 Add/adjust init tests for `delivery=commands` + `trae` (skills retained/generated, no adapter error), mixed tools (`claude,trae`) with per-tool expected outputs, and deterministic failure path for unsupported command surface (`none`)
- [ ] 2.6 Add/adjust init tests for `delivery=commands` + `kimi` (skills retained/generated, no adapter error), mixed tools (`claude,kimi`) with per-tool expected outputs, and deterministic failure path for unsupported command surface (`none`)
## 3. Update: Capability-Aware Sync and Drift Detection
@@ -30,7 +30,7 @@
- [ ] 3.4 Update profile/delivery drift detection to avoid perpetual drift for `skills-invocable` tools under commands delivery
- [ ] 3.5 Ensure configured-tool detection still includes `skills-invocable` tools under commands delivery when managed skills exist
- [ ] 3.6 Update summary output so skills-invocable behavior is reported as expected behavior (not implicit skip/error)
- [ ] 3.7 Add/adjust update tests for `delivery=commands` + configured Trae (skills retained/generated), idempotent second update (no false drift loop), mixed configured tools (`claude` + `trae`), and deterministic preflight failure for unsupported command surface (`none`)
- [ ] 3.7 Add/adjust update tests for `delivery=commands` + configured Kimi CLI (skills retained/generated), idempotent second update (no false drift loop), mixed configured tools (`claude` + `kimi`), and deterministic preflight failure for unsupported command surface (`none`)
## 4. UX and Error Messaging
@@ -40,7 +40,7 @@
## 5. Documentation Updates
- [ ] 5.1 Update `docs/supported-tools.md` to document command-surface semantics for Trae and clarify delivery interactions
- [ ] 5.1 Update `docs/supported-tools.md` to document command-surface semantics for skills-invocable tools and clarify delivery interactions
- [ ] 5.2 Update `docs/cli.md` delivery guidance to explain capability-aware behavior for `delivery=commands`
- [ ] 5.3 Add a short troubleshooting note for "commands-only + unsupported tool" failures
@@ -49,5 +49,5 @@
- [ ] 6.1 Run targeted tests: `test/core/init.test.ts` and `test/core/update.test.ts`
- [ ] 6.2 Run any new capability/unit test files added in this change
- [ ] 6.3 Run full test suite (`pnpm test`) and resolve regressions
- [ ] 6.4 Manual smoke check: `openspec init --tools trae` with `delivery=commands`
- [ ] 6.5 Manual smoke check: mixed tools (`claude,trae`) with `delivery=commands`
- [ ] 6.4 Manual smoke check: `openspec init --tools kimi` with `delivery=commands`
- [ ] 6.5 Manual smoke check: mixed tools (`claude,kimi`) with `delivery=commands`
@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-06-29
@@ -0,0 +1,116 @@
# Design: `/opsx:update` — a thin update skill
## Context
OPSX models a change as a small DAG of planning artifacts. Each schema declares artifacts with `requires` edges ([schemas/spec-driven/schema.yaml](../../../schemas/spec-driven/schema.yaml)); `ArtifactGraph` ([src/core/artifact-graph/graph.ts](../../../src/core/artifact-graph/graph.ts)) topologically sorts them, and `openspec status --change <id> --json` already reports, per artifact: its `status` (`done`/`ready`/`blocked`), its `outputPath`, and — via the top-level `artifactPaths` map — its `resolvedOutputPath` and `existingOutputPaths`, plus the change's `schemaName` and `isComplete`. The two path fields differ in a way that matters for a write operation: `existingOutputPaths` is the concrete files that exist on disk (for a glob artifact such as `specs/**/*.md`, the glob already expanded to real files); `resolvedOutputPath` is the change-dir-joined declared path, which for a glob artifact is still the glob (`.../specs/**/*.md`) and is therefore **not** a write target. `/opsx:update` edits the files in `existingOutputPaths`. `openspec list --json` lists changes by recency.
That is everything an update skill needs. The artifacts are a handful of markdown files on disk; the agent can read them. So `/opsx:update` is built as a thin skill over the **existing** CLI, in the same shape as `continue-change.ts` (select change → `openspec status --json` → act).
This proposal began larger — a reverse-dependency graph API, content digests, a baseline ledger, a `reconcile` write op, a `status --impact` selector. Review feedback ([PR #1278](https://github.com/Fission-AI/OpenSpec/pull/1278)) was that this over-builds: coding agents tend to over-complicate skills, and the feature should work off the existing `status` command with as little new code as possible. This design follows that steer.
## Goals / Non-Goals
**Goals**
- A `/opsx:update` action that revises a change's existing planning artifacts and keeps them coherent with one another.
- Drive it from the artifact set and paths the CLI already reports — zero hardcoded artifact names — so custom schemas work.
- Edit planning artifacts only; never touch code. Confirm every edit with the user.
- Add as little code as possible: one skill template, no changes to the graph engine, the `status` command, or the metadata schema.
**Non-Goals**
- A new top-level `openspec update*` CLI verb (name is taken; see Naming).
- Automatic, unattended regeneration (the user always confirms).
- Content digests, a drift/staleness signal, a baseline ledger, a `reconcile` op, or a `status --impact` selector (see "Why not the heavier machinery").
- Regenerating *code* from updated artifacts — that is `/opsx:apply`'s job; `/opsx:update` stops at the plan and hands off.
- Cross-change audit ([#247](https://github.com/Fission-AI/OpenSpec/issues/247) in full) — a later proposal; this change is intra-change.
- Updating anything other than a change's planning artifacts. v1 is specific to change proposals; generalizing "update" to other graph types is deferred until such a graph exists (see Naming).
## The skill, written by hand
Working backwards from "what is the minimal instruction set," here is the skill body in sketch form. It is short on purpose — few tokens, few commands:
```
Revise a change's planning artifacts and keep them coherent. Never edit code.
1. Resolve the change.
- If named, use it. Else infer from context, or auto-select the only active change;
if still unclear, run `openspec list --json` and ask the user to choose
(most-recently-modified first). Announce the selection and how to override.
2. Get the artifacts.
- Run `openspec status --change "<id>" --json`.
- Read `artifacts[]` (ids + status) and the `artifactPaths` map. These come from the
active schema — do not assume the artifact ids or paths.
- The files to edit are `artifactPaths.<id>.existingOutputPaths` (already glob-expanded
for artifacts like `specs/**/*.md`). Do not write to `resolvedOutputPath`: for a glob
artifact it is still the glob pattern, not a real file.
3. Understand the request.
- If the user named a change ("the design now uses X"), that is the starting edit.
- If they only said "update" / "make this coherent," treat it as a coherence review.
4. Read and reconcile.
- Read the artifact(s) the request touches and the other existing artifacts in the change.
- Apply the requested edit. Then check every other existing artifact against it — in any
direction (an edit to design may require revising the proposal, not only the tasks) —
and note what is now inconsistent, missing, or contradictory.
- Do not invent artifacts that don't exist yet; point the user to `/opsx:continue` to create them.
5. Confirm and apply, one artifact at a time.
- Show each proposed revision and why. Write only after the user confirms.
- When a substantial rewrite is needed, `openspec instructions <artifact> --change "<id>" --json`
gives that artifact's rules/template to follow.
6. Point to the next step (guidance only — never act on it).
- Artifacts still missing → suggest `/opsx:continue`. Change already implemented (tasks
checked off / applied) → the code may no longer match the revised plan; suggest
`/opsx:apply` to carry the delta. Fully done and implemented → suggest `/opsx:archive`.
Guardrails:
- Planning artifacts only. If the plan now implies code changes, stop and point to `/opsx:apply`.
- Use artifact ids/paths from `openspec status`; never branch on literal proposal/specs/design/tasks names.
- If the request changes the change's *intent* rather than refining it, recommend `/opsx:new`
(the "Update vs. Start Fresh" heuristic, docs/opsx.md).
```
The `spec-driven` artifact names may appear once, as a worked *example* of how to apply step 4, exactly as `continue-change.ts` does today — but the control flow reads ids from the CLI, so the skill never branches on those names. A template test asserts there is no name-based branching (the anti-[#777](https://github.com/Fission-AI/OpenSpec/issues/777) guard).
## Decisions
### 1. Bidirectional coherence, not downstream propagation
The artifact graph has a build *order*, but "what needs updating after an edit" is not strictly downstream. If `design` changes, the `proposal` it elaborates may need to change too; if `tasks` reveal a missing capability, the `specs` may need a new requirement. The skill therefore reads the change's artifacts and reconciles them in whatever direction the edit demands. Build order is still useful as a default *reading* order and for presenting fixes, but it is not a constraint on which artifacts may be revised. This is why the design does not add a one-directional `getDownstream` / `--impact` primitive: it would encode the wrong model.
### 2. Lean on the existing `status` command
`openspec status --change <id> --json` already returns the artifact set, per-artifact status, and, in the `artifactPaths` map, the on-disk paths. The skill writes to `artifactPaths.<id>.existingOutputPaths` — the concrete files, glob-expanded — and deliberately not to `resolvedOutputPath`, which for a glob artifact is the pattern itself and not a file. That is everything the skill needs to know what exists and where it lives; no new CLI field is required. Picking the change reuses `openspec list --json`, exactly like `/opsx:continue`. No new CLI surface is introduced.
### 3. Why not the heavier machinery (digests, ledger, reconcile, impact)
The first draft proposed SHA-256 content digests, a per-change baseline ledger in `.openspec.yaml`, an `openspec reconcile` write op, a derived drift signal, and a `status --impact` selector — so the CLI could tell the agent *which* artifacts are stale without the agent reading them.
Rejected for v1, because the cost outweighs the need:
- The artifacts are a few markdown files. An agent that is going to *rewrite* them must read them anyway, so computing staleness for it saves little and adds a stateful subsystem (a ledger that `status` must not mutate, a separate write verb, scheme-versioning for forward-compat, cross-platform digest canonicalization, and the round-trip tests for all of it).
- A digest/ledger only earns its keep when something must judge staleness *without* reading content — e.g. unattended drift detection across many changes ([#247](https://github.com/Fission-AI/OpenSpec/issues/247) cross-change, [#846](https://github.com/Fission-AI/OpenSpec/issues/846) tracking files). Those are out of scope here. When one of them becomes concrete, this machinery can be designed against that real need.
So `/opsx:update` v1 has the agent read the change's artifacts and judge coherence directly. If, after using it, a deterministic signal proves necessary, the smallest first step is to expose the schema's `requires` edges on `status --json` (a single additive field, no new command) — and only then consider digests.
### 4. Naming: `/opsx:update` skill, not `openspec update` CLI
`openspec update [path]` already regenerates AI tool/skill files ([src/cli/index.ts](../../../src/cli/index.ts)). Overloading it would give one verb two unrelated meanings. The artifact-update action is therefore the **skill** `/opsx:update`, with no new `openspec` verb at all. Considered and rejected: `openspec regen --from <artifact>` ([#705](https://github.com/Fission-AI/OpenSpec/issues/705)) — a mutating CLI verb that rewrites artifacts duplicates the skill's job and bypasses user confirmation; the value is in the agent's semantic revision, not a CLI rewrite.
Review feedback flagged that "update" alone is generic — could it apply to any graph? The resolution: the skill is scoped to **change proposals only**, and the specific name carries that scope. The skill is `openspec-update-change`, following the `openspec-<verb>-change` naming of its siblings (`openspec-continue-change`, `openspec-new-change`, …). The command is `/opsx:update` because every verb in the `/opsx:` family operates on a change (`continue`, `apply`, `archive` — none says `-change`); a change-scoped meaning is what the namespace already promises. If a future graph type needs its own update action, it gets its own specific skill name then — nothing here blocks or breaks that.
### 5. Guardrails (the part that makes it the requested command)
- **Planning artifacts only.** The skill's write targets are the artifact paths from `status`; if a revision implies code changes it stops and points to `/opsx:apply`. This directly answers [#1188](https://github.com/Fission-AI/OpenSpec/issues/1188)'s complaint that the manual workaround edits code.
- **Schema-driven.** Ids and paths come from `status`; no branching on literal `proposal`/`specs`/`design`/`tasks`. Works for custom schemas ([#777](https://github.com/Fission-AI/OpenSpec/issues/777), [#666](https://github.com/Fission-AI/OpenSpec/issues/666)).
- **Confirm each edit.** One artifact at a time, shown before writing.
- **Intent guard.** A revision that changes intent rather than refining it is redirected to `/opsx:new` (the "Update vs. Start Fresh" heuristic, [docs/opsx.md](../../../docs/opsx.md)).
### 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.
## 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).
- **Coherence quality depends on the agent.** Mitigated by confirming every edit and by keeping scope to one change's artifacts (a small, readable set).
- **Skill drifts back to hardcoding artifact names.** Mitigated by a template test asserting the control flow reads ids from `status` JSON and contains no name-based branching.
## Migration Plan
Additive and backward-compatible. One new skill template, installed with the default `core` profile (maintainer call on the PR: update is part of the default happy path, not expanded-only); one docs row. No existing command changes behavior; no schema or graph changes. The superseded stub (`add-artifact-regeneration-support`) is removed or folded in the same PR to avoid two competing proposals in the tree.
@@ -0,0 +1,66 @@
## Why
OPSX names **four** first-class actions — "create, implement, **update**, archive — do any of them anytime" ([docs/opsx.md:52](../../../docs/opsx.md)). Three ship as commands. **`update` does not exist.** The only mechanism offered is *"edit the files manually"* — and when you edit one artifact, nothing helps you keep the rest of the change coherent. Worse, the manual workaround lets the agent edit **code** when the user only wanted to revise the **plan** ([#1188](https://github.com/Fission-AI/OpenSpec/issues/1188)).
This is the most-requested missing capability in the tracker. It is one gap with several faces, and the fix is small: a thin `/opsx:update` skill that revises a change's planning artifacts and keeps them coherent with each other, built on the **existing** `openspec status` / `openspec list` commands. No new graph engine, no digests, no ledger — just an agent that reads the change's artifacts and updates what needs updating, with the user's confirmation.
## What Changes
The whole feature is a single new workflow skill, `/opsx:update`. The skill is deliberately change-scoped — `openspec-update-change`, following the `openspec-<verb>-change` naming of its siblings — and applies to change proposals only, not arbitrary artifact graphs (see design, Naming). Written by hand, its instruction set is short:
1. **Understand the request** — what the user wants to revise (or, with no specific ask, "review this change for coherence").
2. **Get the artifacts** — run `openspec status --change <id> --json`. Its `artifactPaths` map reports, per artifact, which files exist and where: `existingOutputPaths` is the concrete file list to edit — already expanded for glob artifacts like `specs/**/*.md`. (`openspec list --json` to pick the change when it isn't given.)
3. **Read and revise** — read the relevant artifacts, make the requested edit, then check the change's **other** artifacts against it and propose any follow-on edits needed to keep the plan coherent.
4. **Confirm and apply** — show each proposed revision, write only after the user confirms.
5. **Point to the next step** — report where the change now stands and recommend what comes next: artifacts still missing → `/opsx:continue`; plan revised after the change was already implemented → `/opsx:apply` to carry the delta into code; everything done and implemented → `/opsx:archive`. Guidance only — the skill never acts on it.
Two guardrails make it the command the cluster asked for:
- **Planning artifacts only, never code.** If a revised plan implies code changes, it hands off to `/opsx:apply` ([#1188](https://github.com/Fission-AI/OpenSpec/issues/1188)).
- **Schema-driven, not name-driven.** Artifact ids and paths come from `openspec status`, so the skill works for custom schemas, not just the default `proposal → specs → design → tasks` ([#777](https://github.com/Fission-AI/OpenSpec/issues/777), [#666](https://github.com/Fission-AI/OpenSpec/issues/666)).
**Coherence is bidirectional.** Earlier framing treated update as strictly "downstream" propagation. That is wrong: in `proposal → specs → design → tasks`, editing `design` can require revising `proposal` too. The skill reads the change's artifacts and reconciles them in whatever direction the edit demands, rather than assuming a fixed flow.
### Deliberately not built (yet)
Per the steer to introduce as little code as possible, and only when there is a defined need, this change does **not** add: a reverse-dependency graph API, content digests / staleness signals, a `.openspec.yaml` baseline ledger, an `openspec reconcile` write op, a drift report, or a `status --impact` selector. The agent reads the change's artifacts directly — a handful of markdown files — which is enough to judge coherence. If a future, concrete need emerges (e.g. unattended drift detection across many changes), exposing the schema's `requires` edges on `openspec status --json` is a one-field additive follow-up. It is out of scope here.
## Capabilities
### New Capabilities
- `opsx-update-skill`: A new `/opsx:update` workflow skill that revises a change's existing planning artifacts and keeps them coherent with one another. It reads the artifact set and paths from `openspec status`, reviews related artifacts in any direction (not only downstream), edits planning artifacts only and never code, and confirms each edit with the user. It ends with next-step guidance — recommending `/opsx:continue`, `/opsx:apply`, or `/opsx:archive` based on the change's state — without acting on it.
## Impact
- `src/core/templates/workflows/update-change.ts` (**new**) — the `openspec-update-change` skill template and the `/opsx:update` command template, mirroring the structure of `continue-change.ts`. Reads artifact ids and paths from `openspec status --json`; embeds no artifact-name patterns.
- Skill/command registration + [src/core/profiles.ts](../../../src/core/profiles.ts) — add `update` to `ALL_WORKFLOWS` **and to the default `core` profile** (`propose`, `explore`, `apply`, `sync`, `archive`), so `/opsx:update` is part of the default install rather than expanded-only (maintainer call on the PR).
- `docs/opsx.md` — add a `/opsx:update` row to the command table and a short "Updating a change" usage note.
- `openspec/changes/add-artifact-regeneration-support/` — the in-repo proposal-only stub for this gap is superseded; retire it or fold its notes into design.
- No changes to `src/core/artifact-graph/*`, `src/commands/workflow/status.ts`, or `ChangeMetadataSchema`. The skill uses `openspec status` / `openspec list` as they exist today.
## Issues addressed
Verified against `Fission-AI/OpenSpec` on 2026-06-30.
Closes (the missing-update-action family):
- [#1188](https://github.com/Fission-AI/OpenSpec/issues/1188) — "Add a command to update proposal, design and task" (and stop it editing code). Delivered as `/opsx:update`, planning-artifacts-only.
- [#705](https://github.com/Fission-AI/OpenSpec/issues/705) — "Rebuild downstream artifacts from a modified upstream." Delivered as the skill's read-and-reconcile pass over the change's artifacts.
- [#673](https://github.com/Fission-AI/OpenSpec/issues/673) — "clarify": update existing artifacts without auto-advancing the build frontier. `/opsx:update` revises in place and never creates the next artifact.
- [#247](https://github.com/Fission-AI/OpenSpec/issues/247) — "review and update all change proposals." Delivered as the within-a-change coherence review; cross-change audit is a separate, later proposal.
Answers (questions whose honest answer today is "no command exists"):
- [#694](https://github.com/Fission-AI/OpenSpec/issues/694), [#684](https://github.com/Fission-AI/OpenSpec/issues/684), [#618](https://github.com/Fission-AI/OpenSpec/issues/618) — "which command regenerates a document after the flow progressed / after apply?" → `/opsx:update`.
- Discussion [#1206](https://github.com/Fission-AI/OpenSpec/discussions/1206) — the official answer becomes `/opsx:update`.
Supersedes:
- `openspec/changes/add-artifact-regeneration-support` (in-repo, proposal-only stub) — same problem, replaced by this skill. Its hardcoded-filename dependency tracking and metadata-file staleness mechanism are dropped in favor of letting the agent read the artifacts.
Delineated from adjacent commands (distinct surfaces — coordinate, don't collide):
- [#702](https://github.com/Fission-AI/OpenSpec/pull/702) `/opsx:clarify` — resolves ambiguity *within one artifact* via Q&A; a complementary upstream step. `/opsx:update` then reconciles the change's artifacts with each other.
- [#1251](https://github.com/Fission-AI/OpenSpec/pull/1251) `/opsx:review`, [#880](https://github.com/Fission-AI/OpenSpec/issues/880) — review the *implementation (code)* against the plan. `/opsx:update` is the mirror image: it keeps the *plan* coherent and never touches code.
- [#783](https://github.com/Fission-AI/OpenSpec/issues/783) — cross-artifact quality review. The skill's coherence pass is the lightweight form of this; a deterministic `validate`-side check is a separate proposal.
@@ -0,0 +1,139 @@
## ADDED Requirements
### Requirement: Update Workflow Command
The system SHALL provide a `/opsx:update` workflow skill that revises a change's existing planning artifacts in place. It SHALL NOT advance the build frontier (it does not create a not-yet-started artifact) and SHALL edit planning artifacts only, never implementation code.
#### Scenario: Select the change to update
- **WHEN** the user invokes `/opsx:update` without a change name
- **THEN** the skill infers the change from conversation context if possible, or auto-selects the change when only one active change exists
- **AND** if it is still ambiguous, it lists available changes (most-recently-modified first) via `openspec list --json` and asks the user to choose
- **AND** it announces which change was selected and how to override
#### Scenario: Revise without advancing the frontier
- **WHEN** the user asks `/opsx:update` to revise an existing artifact
- **THEN** the skill updates that artifact and reconciles the change's other existing artifacts with it
- **AND** it does NOT create any artifact that does not yet exist (that remains the job of `/opsx:continue`/`/opsx:propose`)
#### Scenario: Missing artifacts are deferred to continue
- **WHEN** keeping the change coherent would require an artifact that has not been created yet
- **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
#### Scenario: Update stays within the plan
- **WHEN** revising artifacts would imply changes to implementation code
- **THEN** the skill updates the planning artifacts only
- **AND** it directs the user to `/opsx:apply` to carry the revised plan into code, rather than editing code itself
### Requirement: Schema-Driven Artifact Resolution
The `/opsx:update` skill SHALL learn which artifacts exist and where they live by reading the change's status from the CLI, and SHALL NOT rely on hardcoded artifact names or assumed path separators. This makes the skill correct for custom schemas and on every platform, not only the default `spec-driven` schema.
#### Scenario: Reads the artifact set from status
- **WHEN** the skill needs to know which artifacts a change has and where they are
- **THEN** it runs `openspec status --change <id> --json` and uses the reported artifact ids, statuses, and the `artifactPaths` map (`existingOutputPaths` for the files to edit)
- **AND** it does not assume the artifact ids or output paths
#### Scenario: Does not branch on hardcoded artifact names
- **WHEN** the skill decides which artifacts to read and revise
- **THEN** its control flow uses the ids reported by the CLI
- **AND** it does not branch on literal `proposal`/`specs`/`design`/`tasks` names
#### Scenario: Works for a custom schema
- **WHEN** the active change uses a custom schema whose artifact ids are not `proposal`/`specs`/`design`/`tasks`
- **THEN** the skill uses the artifact ids and paths reported by the CLI
- **AND** it works without any change to the skill
#### Scenario: Resolve artifact paths cross-platform
- **WHEN** the skill reads or writes an artifact on macOS, Linux, or Windows
- **THEN** it uses the `existingOutputPaths` provided by the CLI status output
- **AND** it does not assume forward-slash separators
#### Scenario: Edit the concrete files of a glob artifact
- **WHEN** an artifact's declared output path is a glob (for example `specs/**/*.md`)
- **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
- **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
### Requirement: Bidirectional Coherence Review
The `/opsx:update` skill SHALL keep a change's existing planning artifacts coherent with one another after a revision, reviewing affected artifacts in any direction rather than assuming a fixed downstream flow.
#### Scenario: Reconcile related artifacts after an edit
- **WHEN** the user revises one artifact
- **THEN** the skill reviews the change's other existing artifacts against the revision
- **AND** it proposes follow-on edits to any artifact that is now inconsistent, whether that artifact is upstream or downstream of the edited one
#### Scenario: Upstream artifact may be revised
- **WHEN** an edit to a later artifact (for example design) contradicts an earlier one (for example the proposal)
- **THEN** the skill may propose revising the earlier artifact to restore coherence
- **AND** it does not treat propagation as downstream-only
#### Scenario: Coherence review with no specific edit
- **WHEN** the user invokes `/opsx:update` without a specific revision in mind ("make this change coherent")
- **THEN** the skill reads the change's existing artifacts and reviews them against each other for contradictions, gaps, and duplication
- **AND** it presents any findings for the user to confirm before editing
#### Scenario: Coherent change yields no changes
- **WHEN** the skill finds the change's artifacts already coherent
- **THEN** it reports the change as coherent and makes no edits
### Requirement: Next-Step Guidance
After applying confirmed revisions (or finding none needed), the `/opsx:update` skill SHALL report where the change stands and recommend the next command, without acting on the recommendation itself.
#### Scenario: Updating an already-implemented change
- **WHEN** the user updates a change whose implementation already happened (for example tasks are checked off or `/opsx:apply` was already run)
- **THEN** the skill still revises planning artifacts only
- **AND** it notes that the implementation may no longer match the revised plan and recommends `/opsx:apply` to carry the delta into code
- **AND** it does not implement anything itself
#### Scenario: Next step when artifacts are incomplete
- **WHEN** the update finishes and the change still has not-yet-created artifacts
- **THEN** the skill recommends `/opsx:continue` to create them
#### Scenario: Next step when the change is fully done
- **WHEN** the update finishes and the change's artifacts are complete and already implemented
- **THEN** the skill recommends `/opsx:archive`
### Requirement: User-Confirmed Incremental Application
The `/opsx:update` skill SHALL propose each artifact revision and apply it only after user confirmation.
#### Scenario: Confirm before writing
- **WHEN** the skill has a proposed revision for an artifact
- **THEN** it shows the user what it intends to change and why before writing
- **AND** it writes only after the user confirms
#### Scenario: Rejected revision is not written
- **WHEN** the user rejects a proposed revision for an artifact
- **THEN** the skill does not write that revision
- **AND** the artifact is left unchanged
#### Scenario: Intent change is redirected to a new change
- **WHEN** the requested revision changes the intent of the change rather than refining it (per the "Update vs. Start Fresh" heuristic)
- **THEN** the skill recommends starting a new change (`/opsx:new`) instead of mutating the existing proposal into different work
@@ -0,0 +1,30 @@
# Tasks: `/opsx:update` — a thin update skill
> The whole feature is one new skill template over the existing `openspec status` / `openspec list` commands. No changes to the graph engine, the `status` command, or the metadata schema.
## 1. The `/opsx:update` skill
- [x] 1.1 Create `src/core/templates/workflows/update-change.ts` with `getUpdateChangeSkillTemplate()` (skill) and `getOpsxUpdateCommandTemplate()` (command), mirroring `continue-change.ts`. The skill name is `openspec-update-change` — change-scoped, per the `openspec-<verb>-change` convention (see design, Naming).
- [x] 1.2 Instruction body (see design "The skill, written by hand"): resolve the change (infer / `openspec list --json` / ask) → `openspec status --change <id> --json` → read the relevant artifacts → apply the requested edit → reconcile the change's other existing artifacts in any direction → confirm and apply one artifact at a time → end with next-step guidance (`/opsx:continue` / `/opsx:apply` / `/opsx:archive` based on the change's state; see design Decision 6), never acting on it. Read artifact ids from the status JSON only, and write to `artifactPaths.<id>.existingOutputPaths` (never to a glob `resolvedOutputPath`).
- [x] 1.3 Encode the guardrails: (a) planning artifacts only — never edit code, hand off to `/opsx:apply`; (b) schema-driven — no branching on literal `proposal`/`specs`/`design`/`tasks`; ids/paths come from `openspec status`; (c) revise only existing files (`existingOutputPaths`) — defer not-yet-created artifacts, and new files under a glob artifact, to `/opsx:continue`; (d) intent change → recommend `/opsx:new` (the "Update vs. Start Fresh" heuristic in `docs/opsx.md`).
- [x] 1.4 Register the skill/command and add `update` to `ALL_WORKFLOWS` **and the default `core` profile** in `src/core/profiles.ts` (maintainer call: default install, not expanded-only).
## 2. Docs & supersede the stub
- [x] 2.1 Add a `/opsx:update` row to the command table in `docs/opsx.md`, plus a short "Updating a change" usage note.
- [x] 2.2 Remove (or fold) `openspec/changes/add-artifact-regeneration-support/` so the tree has a single update proposal.
- [x] 2.3 Update any generated-skill manifests/fixtures that enumerate workflow skills so `openspec-update-change` is included.
## 3. Tests
- [x] 3.1 Template generation snapshot for the skill and command templates.
- [x] 3.2 Assert the template's control flow contains NO hardcoded artifact-name branching (the anti-#777 guard): artifact ids must be read from `openspec status` JSON.
- [x] 3.3 Assert the template instructs planning-artifacts-only with a hand-off to `/opsx:apply` for code, and never advances the build frontier.
- [x] 3.4 Assert the template instructs writing to `existingOutputPaths` (the glob-expanded concrete files) and not to a glob `resolvedOutputPath`.
- [x] 3.5 Assert the template ends with next-step guidance (`/opsx:continue`/`/opsx:apply`/`/opsx:archive`) and instructs the agent never to act on it.
- [x] 3.6 Assert `update` is included in the `core` profile's workflows (profiles test).
## 4. End-to-end verification
- [x] 4.1 `openspec validate add-update-workflow --strict` passes; `openspec status --change add-update-workflow` shows all artifacts complete.
- [x] 4.2 Manual walk-through: on a `spec-driven` change, edit `design`, run `/opsx:update`, confirm it proposes coherence edits to other existing artifacts (including upstream where warranted) and never touches code.
@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-07-26
@@ -0,0 +1,53 @@
## Context
The `schema init` action currently checks whether the destination exists and, when `--force` is present, immediately removes that directory. Only afterward does it collect the remaining inputs and validate `--artifacts`. An unknown artifact therefore produces the expected error only after the existing schema has already been deleted.
The command is implemented as one Commander action in `src/commands/schema.ts`. Its current tests largely exercise supporting schema functions or manually create expected files instead of invoking the registered command, so they do not observe mutation ordering.
## Goals / Non-Goals
**Goals:**
- Finish collecting and validating schema-init inputs before any forced replacement mutates the destination.
- Preserve the complete existing schema when an artifact ID is invalid.
- Keep error output, exit status, and successful `--force` replacement behavior compatible.
- Cover the behavior through the real registered `schema init` command on cross-platform temporary paths.
**Non-Goals:**
- Change the set of artifact IDs accepted by `schema init` or how the comma-separated list is parsed.
- Make replacement transactional for filesystem failures that occur after validation succeeds.
- Change overwrite behavior in other schema subcommands.
## Decisions
### Separate preparation from destination mutation
The action will retain the early destination-exists check so an invocation without `--force` still fails without prompting or doing extra work. When overwrite is allowed, it will defer `fs.rmSync()` until after the command has:
1. Determined interactive or non-interactive mode.
2. Collected the description and artifact selection.
3. Rejected an empty selection or unknown artifact ID.
4. Constructed the artifact definitions and in-memory schema object.
Only then will the command remove the existing directory and write the replacement.
This directly fixes the deterministic validation failure without introducing temporary-directory swaps or rollback machinery. Staging and atomically swapping the entire schema was considered, but it would broaden this targeted fix to cover unrelated filesystem failures and platform-specific rename behavior.
### Preserve the existing failure contract
Invalid artifacts will continue to produce the same text or JSON error, set a non-zero exit code, and report the valid artifact IDs. The only observable difference is that an existing destination remains unchanged.
Keeping the output contract stable limits the change for scripts and agents that already consume the JSON response.
### Add command-level regression tests
Tests will register `schema` on a fresh Commander program and call `parseAsync()` with real command arguments inside a temporary project directory. The primary regression test will place a sentinel file in an existing schema, invoke `schema init --force` with an unknown artifact, and verify that both the directory and sentinel content survive.
A successful overwrite test will use valid artifact IDs and verify that the old sentinel is removed while the expected generated files exist. Paths will be constructed with Node.js `path` helpers so the same tests run on Windows, macOS, and Linux.
## Risks / Trade-offs
- **Risk: Moving mutation later could accidentally weaken successful overwrite behavior.** Mitigation: Keep a positive command-level test that proves a valid forced initialization still replaces the destination.
- **Risk: Commander tests can leak `process.exitCode` or the working directory into neighboring tests.** Mitigation: Save and restore process state in test setup and teardown.
- **Trade-off: A write failure after validation can still leave a partial replacement.** Mitigation: Treat full transactional replacement as a separate hardening effort; this change guarantees safety for input and selection failures only.
@@ -0,0 +1,28 @@
## Why
`openspec schema init --force` removes an existing project-local schema before validating the requested artifact list. A command that ultimately fails for an unknown artifact can therefore destroy the schema it was supposed to replace, turning a recoverable input error into data loss.
## What Changes
- Complete schema-init input collection and artifact validation before replacing an existing schema.
- Preserve the existing schema and its contents when validation fails, including when `--force` is present.
- Keep successful `--force` replacement behavior unchanged once all inputs are valid.
- Add command-level regression coverage for both failed preservation and successful replacement.
- Keep the change narrowly scoped to `schema init` artifact validation and forced replacement; no other CLI behavior changes.
## Capabilities
### New Capabilities
- None.
### Modified Capabilities
- `schema-init-command`: Require failed schema-init validation to leave an existing schema unchanged before any forced replacement begins.
## Impact
- **CLI behavior**: Failed `schema init --force` validation no longer deletes an existing project-local schema.
- **Code**: The `schema init` action in `src/commands/schema.ts` will separate non-destructive preparation from the destructive replacement step.
- **Tests**: `test/commands/schema.test.ts` will exercise the registered command instead of simulating schema creation for the affected cases.
- **Dependencies and APIs**: No new dependencies or public API changes.
@@ -0,0 +1,21 @@
## ADDED Requirements
### Requirement: Schema init validates artifacts before forced replacement
The CLI SHALL validate all requested artifact IDs before replacing an existing project-local schema. If artifact validation fails, the CLI SHALL leave the existing schema directory and all of its contents unchanged on every supported platform.
#### Scenario: Unknown artifact preserves existing schema
- **GIVEN** `openspec/schemas/tdd-driven/` already exists with user-authored files
- **WHEN** the user runs `schema init tdd-driven` with `--force` and an artifact list containing the unknown ID `task`
- **THEN** the command exits with a non-zero status and reports the unknown artifact
- **AND** the existing `tdd-driven` schema directory and its contents remain unchanged
#### Scenario: Unknown artifact preserves a schema at a Windows project path
- **GIVEN** an existing project-local schema is resolved from a Windows filesystem path
- **WHEN** forced schema initialization fails artifact validation
- **THEN** the resolved schema directory and its contents remain unchanged
#### Scenario: Valid artifacts allow forced replacement
- **GIVEN** a project-local schema already exists
- **WHEN** the user runs `schema init` with `--force` and only valid artifact IDs
- **THEN** the command replaces the existing schema with the newly generated schema
- **AND** reports successful creation
@@ -0,0 +1,17 @@
## 1. Command-Level Regression Coverage
- [x] 1.1 Add a test helper that registers the schema command on a fresh Commander program and restores `cwd`, `process.exitCode`, environment variables, and console spies after each test.
- [x] 1.2 Add a regression test that creates an existing schema with a sentinel file, runs forced initialization with an unknown artifact ID, and verifies the non-zero JSON error plus byte-for-byte preservation of the existing schema.
- [x] 1.3 Add a positive regression test that runs forced initialization with valid artifact IDs and verifies the old sentinel is removed and the expected schema and templates are generated.
## 2. Validation-First Forced Replacement
- [x] 2.1 Reorganize the `schema init` action so it collects inputs, validates artifact IDs, and constructs the in-memory schema before deleting an existing destination.
- [x] 2.2 Keep the existing unknown-artifact output and exit status unchanged while ensuring every pre-mutation return path leaves the destination untouched.
- [x] 2.3 Confirm a valid `--force` invocation still replaces the existing schema and reports the same successful result.
## 3. Cross-Platform Verification and Release Metadata
- [x] 3.1 Use Node.js path helpers and temporary directories in the regression tests, and confirm the affected test runs in the existing Windows CI environment.
- [x] 3.2 Run `pnpm exec vitest run test/commands/schema.test.ts`, `pnpm run lint`, and `pnpm run build`.
- [x] 3.3 Add a patch changeset describing that failed forced schema initialization now preserves the existing schema.
@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-07-22
@@ -0,0 +1,183 @@
## Context
OpenSpec project config currently provides a top-level `context` value and an artifact-keyed `rules` map. Artifact instruction generation reads both values at runtime, but the apply and archive workflow surfaces do not expose equivalent current inputs.
Apply already has a dynamic instruction command: `openspec instructions apply --change <name>`. Archive skills are generated from static templates and currently have no dedicated runtime-input command. Adding operation-specific advice directly to generated templates would make it stale whenever project config changes.
This change adds a small runtime contract for apply and archive without changing archive execution ownership. The existing single-change archive skill, bulk archive skill, spec sync behavior, and direct `openspec archive` command keep their current flows.
## Goals / Non-Goals
**Goals:**
- Model optional apply and archive working advice as `operations.<operation>.guidance`.
- Fetch current project context and matching operation guidance whenever apply or archive instructions are requested.
- Return context and operation guidance as separate structured fields.
- Make the single-change and bulk archive skills consume current inputs at execution time.
- Carry current `specs` artifact rules into archive-driven and standalone spec sync whenever concrete delta specs are merged into main specs.
- Preserve existing artifact rules, skill steps, user prompts, and CLI behavior.
- Keep config parsing resilient so malformed operation config does not invalidate unrelated fields.
**Non-Goals:**
- Change archive execution ownership, phases, safety guarantees, or filesystem behavior.
- Change `openspec archive`, its flags, filesystem behavior, or compatibility contract.
- Change semantic spec sync ownership, merge phases, or main-spec format.
- Add new enforceable archive checks or configurable operation checks.
- Make any natural-language instruction input a security or validation boundary.
- Change the structure or meaning of artifact `rules`.
- Generalize semantic spec sync to arbitrary artifact IDs or infer delta specs from non-`specs` artifacts.
## Decisions
### D1: Give operation guidance its own typed namespace
Project config gains this optional shape:
```yaml
context: |
TypeScript project using pnpm.
rules:
specs:
- Preserve requirement IDs when meaning is unchanged.
operations:
apply:
guidance:
- Keep test summaries concise.
archive:
guidance:
- Summarize the archive outcome before finishing.
```
The in-memory model uses explicit operation IDs:
```ts
const OPERATION_IDS = ['apply', 'archive'] as const;
type OperationId = (typeof OPERATION_IDS)[number];
interface OperationConfig {
guidance?: string[];
}
```
Parsing remains resilient and field-by-field. An invalid operation entry is omitted with a warning without discarding valid context, rules, references, store settings, or other operation entries. Unknown operation IDs and unknown fields receive actionable warnings. Empty guidance strings are removed while non-empty strings retain their original order, line breaks, and Markdown.
Artifact `rules` remain unchanged and are not read as operation guidance.
### D2: Load operation inputs through one shared helper
Apply and archive instruction generation use a shared helper conceptually shaped as:
```ts
loadOperationInputs(projectConfig, operationId): {
context?: string;
operationGuidance?: string[];
}
```
The existing root-config loader calls `readProjectConfig()` once for each instruction command and passes that parsed `ProjectConfig` to the helper. The same config snapshot supplies references, context, and operation guidance, so malformed-field warnings are not duplicated and one command cannot mix values from two reads. There is no generated-skill or module-state cache, so the next command observes later config changes.
Absent context and empty guidance are omitted rather than returned as empty values.
### D3: Extend apply output without changing apply state behavior
`generateApplyInstructions()` adds the shared operation inputs to its existing result:
```ts
{
context?: string;
operationGuidance?: string[];
}
```
The existing apply state, task progress, missing-artifact checks, context files, references, and schema instruction remain unchanged. JSON serialization includes the new fields automatically. Text output renders project context as a required instruction-input section and operation guidance as a distinct advisory section after the built-in apply instruction content.
The apply skill template keeps both fields structurally separate from CLI-returned state, progress, tasks, missing artifacts, context files, and built-in instruction. When context is present, the agent must read it and apply relevant project facts, conventions, and constraints as a required prompt-level input. When operation guidance is present, the agent must read and consider it as optional additive advice and follow entries that are applicable and compatible with the built-in workflow.
This change does not modify CLI-controlled fields or their state transitions. The template tells the agent not to treat context or guidance as task completion, a replacement for the state-driven workflow, or permission to bypass a blocked state. It must report context conflicts with the built-in instruction, explicit user choices, or CLI-controlled values. If guidance is inapplicable or conflicts with those controlling inputs, the agent preserves the built-in flow and explains why the advice was not followed. It must not copy either field's contents into implementation files or planning artifacts.
### D4: Add a dedicated archive runtime-input branch
`openspec instructions archive --change <name> --json` is handled as a workflow instruction branch alongside apply. It:
- resolves the selected repo or store using the existing instruction-command options;
- requires and validates the change name so the invocation stays scoped to the intended planning root;
- reads the current config through the shared operation-input helper;
- returns `changeName`, optional `context`, optional `operationGuidance`, and the normal resolved-root envelope;
- does not return a static archive workflow template;
- does not inspect delta specs, update specs, move the change, or invoke `openspec archive`.
Human-readable output shows project context as a required instruction-input section and operation guidance as a separate advisory section. If neither value is configured, the command still succeeds with the change and root metadata so skill behavior is uniform.
Keeping this as an instruction surface makes the runtime contract available immediately while leaving archive execution redesign independent.
### D5: Archive and sync skills consume inputs without changing their flow
After resolving the target change and selected root, the single-change archive skill calls:
```bash
openspec instructions archive --change "<name>" --json
```
It must read returned context and apply relevant project facts, conventions, and constraints as a required prompt-level input. It reads and considers returned archive guidance as optional additive advice and follows applicable entries that are compatible with the built-in archive workflow. Explicit user choices, target paths, CLI checks, and command flags are not replaced or inferred from either field. Context conflicts are reported; conflicting or inapplicable guidance is not followed and the reason is explained.
A successful response may omit both optional fields, which means no archive operation inputs are configured. If the command exits non-zero or does not return valid archive-instruction JSON, the single-change skill reports the error and stops before inspecting or writing specs or moving the change. A failed lookup is never treated as an empty successful response.
The bulk archive skill makes the same call once for the selected root, using one selected change to establish context, and applies the returned inputs across that batch. If this lookup exits non-zero or returns invalid archive-instruction JSON, the skill reports the error and stops the batch before inspecting or writing specs or moving any change. It does not change the existing bulk conflict analysis or archive orchestration.
Semantic spec sync keeps its existing artifact contract. The concrete delta spec paths are exactly `artifactPaths.specs.existingOutputPaths` from the selected change's status output. If `artifactPaths.specs` is absent or its concrete output list is empty, that change has no delta specs for this workflow: archive continues without a spec-sync prompt, standalone sync reports that there is nothing to sync, and neither workflow infers delta specs from other artifacts.
When concrete `specs` outputs exist and a write-producing sync will run:
1. Use the same selected change and planning root that supplied the status result.
2. Call `openspec instructions specs --change "<name>" --json` once immediately before the semantic merge.
3. Apply only its returned artifact rules to the main specs produced by that merge.
4. Keep those rules separate from archive operation guidance and unrelated workflow steps.
A valid artifact-instruction response that omits `rules` means that no `specs` rules are configured and the existing semantic merge continues. A non-zero exit or a response that is not valid artifact-instruction JSON is a lookup failure, not an empty rule set. Single-change archive and standalone sync report that error and stop before modifying any main spec; archive also stops before moving the change.
The single-change archive skill fetches this specs-instruction snapshot after sync has been selected and immediately before invoking inline semantic sync. The bulk archive skill resolves every required specs-instruction snapshot after its sync decisions but before the first main-spec write; if any lookup fails, it reports the affected change and stops the whole batch before writing any main spec or moving any change. Archive passes each successful specs-rule snapshot into the inline sync workflow, which reuses it without fetching the same instructions again. When the sync skill is invoked directly, with no archive-supplied snapshot, it fetches current `specs` instructions itself.
For a mixed-schema batch, this decision is made independently for each change. A change whose resolved schema exposes concrete `artifactPaths.specs.existingOutputPaths` participates in spec sync and receives that change's current `specs` rules. A change whose schema has no `specs` artifact, such as a research/design/plan workflow, has no spec sync and continues through the existing archive path.
Artifact rules are not returned from the archive operation-input surface, relabeled as archive guidance, or applied to unrelated archive steps.
The archive, bulk archive, and sync templates retain the existing rule that runtime context, operation guidance, and rule text must not be copied verbatim into specs, change artifacts, summaries, or other files unless the user separately asks for that content. Artifact rules constrain the produced artifact without becoming artifact content.
### D6: Require context consumption while keeping guidance advisory
Current context is a required prompt-level input, not optional-to-ignore metadata. When present, the generated skill must tell the agent to read it and apply relevant project facts, conventions, and constraints.
Operation guidance is optional additive advice. When present, the generated skill must tell the agent to read and consider it and to follow entries that are applicable and compatible with the built-in workflow. If guidance is inapplicable or conflicts with an explicit user choice, resolved path, CLI-controlled state, or command contract, the skill preserves the controlling value and explains why the advice was not followed.
Both semantics remain behavioral contracts for the agent, not enforcement mechanisms. OpenSpec guarantees that it validates the config shape, keeps fields separate from CLI-controlled values, delivers current inputs through the documented instruction surfaces, and leaves existing CLI checks unchanged. Existing checks continue to run wherever the current CLI already owns them. Any invariant that must be non-bypassable belongs in a real CLI check and remains outside this change; stronger archive guarantees require a separate archive execution design.
## Risks / Trade-offs
- **Context conflicts with the built-in workflow** -> Require the skill to report the conflict, preserve explicit user choices and CLI-controlled state, validation, paths, and command contracts, and do not claim prompt-level enforcement.
- **Guidance is inapplicable or conflicts with the built-in workflow** -> Keep it advisory and separate, preserve controlling workflow inputs, and explain why the advice was not followed.
- **Generated skills become stale** -> Skills fetch current inputs on every invocation instead of embedding config content.
- **Repo/store roots diverge** -> Instruction commands reuse existing root selection and read one config snapshot from the resolved root.
- **Archive runtime input is mistaken for archive execution** -> Command naming, JSON fields, docs, and tests state that the instruction surface is read-only and performs no archive mutation.
- **Bulk archive spans an unexpected root** -> The skill resolves the batch root first and fetches inputs once for that root; cross-root batching remains outside the current behavior.
- **Artifact rules are mistaken for archive guidance** -> Fetch them only when writing their artifact, keep them out of `operationGuidance`, and test that they do not affect unrelated archive steps.
- **A custom schema has no `specs` artifact** -> Treat it as having no semantic spec-sync input; do not infer delta specs from unrelated artifacts.
- **Archive and inline sync fetch different rule snapshots** -> Archive fetches once and inline sync reuses the supplied specs-rule snapshot; only standalone sync performs its own lookup.
- **A failed instruction lookup is mistaken for absent optional input** -> Require a successful, valid JSON response before continuing; archive-input failures stop before spec inspection or change moves, and specs-instruction failures stop before main-spec writes or change moves.
## Implementation Plan
1. Add typed operation config parsing and tests.
2. Add the shared runtime-input loader using the root command's single parsed config snapshot.
3. Extend apply instruction JSON and text output.
4. Add archive instruction JSON and text output without changing archive execution.
5. Update single-change archive, bulk archive, and standalone sync templates to fetch current `specs` rules when concrete delta specs exist and reuse the same snapshot during inline sync.
6. Update generated config help, documentation, template parity fixtures, and end-to-end coverage.
Rollback is a code revert. The config field is additive, and no archive filesystem format or durable project state changes in this change.
## Open Questions
None.
@@ -0,0 +1,56 @@
## Why
Project configuration reaches agents while they create OpenSpec artifacts, but apply and archive workflows cannot fetch the same current project context or operation-specific working preferences when they run. Generated skills therefore lack a stable runtime input contract and can become disconnected from later configuration changes.
OpenSpec needs a clear separation between project context, artifact requirements, and operation advice. Project `context` supplies facts, conventions, and constraints the agent must apply when relevant. Artifact `rules` continue to describe the artifacts an agent produces, while optional operation guidance provides additive advice about how an agent should conduct apply or archive work. Both apply and archive should fetch their current inputs from OpenSpec at execution time.
## What Changes
- Add optional `operations.apply.guidance` and `operations.archive.guidance` configuration for additive operation advice. A skill considers returned guidance and follows it when applicable and compatible with the built-in workflow.
- Keep `rules` artifact-specific and preserve all existing artifact-instruction behavior.
- Extend apply instruction output with separate optional fields for current project context and apply operation guidance.
- Update the apply skill template to consume those current runtime inputs while preserving its existing state-driven workflow.
- Add an archive runtime-input surface through `openspec instructions archive --change <name>` so archive skills can fetch current project context and archive operation guidance when they run.
- Treat a non-zero or invalid archive-input response as blocking: report the error and stop before inspecting or writing specs or moving the change. A successful response with omitted optional fields remains the valid no-input case.
- Update the single-change and bulk archive skill templates to consume current archive inputs without embedding configuration snapshots in generated skill text.
- Keep the existing spec-sync contract: delta specs come from `artifactPaths.specs.existingOutputPaths`; schemas without that artifact do not participate in spec sync.
- When archive-driven or standalone spec sync updates main specs, fetch current `specs` artifact instructions and apply their rules to the semantic merge. Archive passes its fetched specs-rule snapshot into the inline sync workflow; standalone sync fetches the same input itself.
- Treat a non-zero or invalid `specs` instruction response as blocking before any main-spec write or archive move. A successful response that omits `rules` continues with the existing semantic merge.
- Treat current context as a required prompt-level input: the agent must read it and apply relevant project facts, conventions, and constraints.
- Treat operation guidance as optional additive advice: the agent considers it and follows applicable entries, but guidance does not define or replace the built-in workflow.
- Keep current context and operation guidance structurally separate from explicit user choices and CLI-controlled behavior. Context conflicts must be reported; guidance that is inapplicable or conflicts with controlling workflow input is not followed and the reason is explained. Neither field is presented as an enforceable security or validation boundary.
- Validate the `operations` config field independently so one malformed operation entry does not discard otherwise valid project configuration.
This change does not redesign archive execution or the semantic spec-merge algorithm. The existing archive skill orchestration and `openspec archive` command remain intact.
## Capabilities
### New Capabilities
- `operation-guidance`: define the `operations.<operation>.guidance` config model, resilient validation, advisory semantics, and runtime delivery for apply and archive
- `cli-archive-instructions`: provide current archive operation inputs in structured JSON and readable text form
- `opsx-apply-skill`: consume current apply context and guidance without changing the built-in apply workflow
- `opsx-bulk-archive-skill`: fetch current archive inputs for a selected batch and apply relevant artifact rules during each spec sync
### Modified Capabilities
- `config-loading`: parse operation guidance independently from existing project-config fields
- `context-injection`: expose the latest project context to apply and archive runtime surfaces in addition to artifact instructions
- `cli-artifact-workflow`: include current context and apply operation guidance in schema-aware apply instruction output
- `opsx-archive-skill`: fetch and apply current archive context and guidance, and carry artifact rules into archive-driven spec sync, while preserving the existing archive flow
- `specs-sync-skill`: apply current `specs` artifact rules during standalone sync, while reusing an archive-supplied specs-rule snapshot when invoked inline
## Impact
- Project config types, parsing, generated help text, and documentation gain an optional `operations` section.
- Apply JSON and text instruction output gain separate optional `context` and `operationGuidance` fields.
- The apply skill must consume current context as a required prompt-level input and consider current operation guidance as optional additive advice, while CLI-returned state, tasks, progress, and instructions remain structurally unchanged.
- `openspec instructions archive --change <name>` becomes a reserved workflow instruction surface and returns current archive inputs without performing archive work.
- Archive skill templates call the runtime surface at execution time, must apply relevant returned context, consider and follow applicable operation guidance, and do not copy their text into output files.
- Archive and bulk archive stop before spec inspection, spec writes, or change moves when the required archive-input lookup fails or returns invalid JSON.
- Archive-driven and standalone spec sync continue to use `artifactPaths.specs.existingOutputPaths`, fetch current `specs` instructions when delta specs exist, and follow those rules without exposing them as operation guidance.
- Archive, bulk archive, and standalone sync stop before writing main specs when a required `specs` instruction lookup fails or returns invalid JSON; only a valid response with no `rules` means that no artifact rules are configured.
- Schemas without a `specs` artifact, or changes with no concrete `specs` outputs, continue without spec sync and do not infer delta specs from other artifacts.
- Inline sync reuses the specs-rule snapshot supplied by archive, avoiding a second fetch with potentially different config or duplicate warnings.
- Existing artifact-rule configuration and instruction output, archive filesystem behavior, direct archive CLI options, semantic merge ownership, and bulk archive orchestration remain unchanged.
- Tests cover resilient config parsing, runtime freshness, single-read config handling, field separation, required context consumption, advisory operation guidance, conflict reporting, selected-root behavior, output rendering, archive and standalone-sync `specs` rule consumption, failed and invalid instruction responses, no-write/no-move failure behavior, schemas with and without `specs`, mixed-schema batches, and generated-template parity.
@@ -0,0 +1,60 @@
## ADDED Requirements
### Requirement: Provide current archive operation inputs
The CLI SHALL provide `openspec instructions archive --change <name>` as a read-only workflow instruction surface for current archive operation inputs.
#### Scenario: Archive JSON contains context and guidance
- **WHEN** a user runs `openspec instructions archive --change <name> --json`
- **AND** config contains project context and `operations.archive.guidance`
- **THEN** the JSON contains `changeName`, `context`, and `operationGuidance` as separate fields
- **AND** includes the normal resolved-root envelope
#### Scenario: Archive text contains context and guidance
- **WHEN** a user runs `openspec instructions archive --change <name>` with configured inputs
- **THEN** text output labels project context as a required instruction input
- **AND** labels operation guidance as separate advisory input
#### Scenario: Archive inputs are absent
- **WHEN** config has no non-empty context or archive guidance
- **THEN** the command succeeds with change and root metadata
- **AND** omits both optional fields
#### Scenario: Archive reads current config
- **WHEN** config changes between two archive instruction calls
- **THEN** the second output reflects the current context and archive guidance
### Requirement: Scope archive inputs to a valid selected root
The archive instruction surface SHALL require a valid change and use existing repo/store root selection before reading config.
#### Scenario: Change is missing
- **WHEN** the archive instruction command is called without `--change`
- **THEN** it returns the existing actionable missing-change error
#### Scenario: Change does not exist in the selected root
- **WHEN** the supplied change is absent from the resolved repo or store
- **THEN** the command fails before returning operation inputs
#### Scenario: Store is selected
- **WHEN** the command is run with a selected store
- **THEN** change validation and config loading both use that store's planning root
### Requirement: Keep archive instructions read-only
The archive instruction surface SHALL return runtime instruction inputs without performing archive execution work.
#### Scenario: Archive instructions are requested
- **WHEN** the command succeeds
- **THEN** it does not inspect or rewrite delta specs
- **AND** does not update main specs
- **AND** does not move or otherwise modify the change
- **AND** does not include the static archive workflow template in JSON output
@@ -0,0 +1,35 @@
## ADDED Requirements
### Requirement: Apply instructions include current operation inputs
The system SHALL include current project context and apply operation guidance as separate optional fields in schema-aware apply instruction output without changing existing apply state behavior.
#### Scenario: Apply JSON contains context and guidance
- **WHEN** a user runs `openspec instructions apply --change <id> --json`
- **AND** config contains project context and `operations.apply.guidance`
- **THEN** the JSON contains separate `context` and `operationGuidance` fields
- **AND** preserves existing apply state, task, progress, context-file, reference, and root fields
#### Scenario: Apply text contains context and guidance
- **WHEN** a user runs `openspec instructions apply --change <id>` with configured context and apply guidance
- **THEN** text output labels project context as a required instruction input
- **AND** labels operation guidance as separate advisory input
- **AND** preserves the built-in apply instruction content
#### Scenario: Apply has artifact rules only
- **WHEN** config contains artifact rules but no apply operation guidance
- **THEN** apply instruction output does not expose artifact rules as operation guidance
#### Scenario: Apply reads current config
- **WHEN** config changes between two apply instruction calls
- **THEN** the second output reflects the current context and apply guidance
#### Scenario: Apply operation inputs are absent
- **WHEN** config has no non-empty context or apply guidance
- **THEN** apply output omits both optional fields
- **AND** otherwise matches existing apply behavior
@@ -0,0 +1,40 @@
## ADDED Requirements
### Requirement: Load operation guidance independently
The system SHALL parse the optional `operations` project-config field independently from `schema`, `context`, `rules`, `references`, and `store` so an invalid operation entry does not discard other valid configuration.
#### Scenario: Valid operation guidance
- **WHEN** config contains `operations.apply.guidance` and `operations.archive.guidance` as arrays of strings
- **THEN** the returned project config includes both operation entries
#### Scenario: One operation is malformed
- **WHEN** apply guidance is a valid string array and archive guidance is malformed
- **THEN** the returned project config includes apply guidance
- **AND** omits archive guidance with an actionable warning
#### Scenario: Operations field is not an object
- **WHEN** config contains a non-object `operations` value
- **THEN** the system warns about the invalid field
- **AND** continues with all independently valid config fields
#### Scenario: Unknown operation ID
- **WHEN** config contains an unsupported operation ID
- **THEN** the system warns with the supported operation IDs
- **AND** ignores only the unsupported operation entry
#### Scenario: Unknown fields in an operation
- **WHEN** a supported operation contains fields other than `guidance`
- **THEN** the system warns about those fields
- **AND** preserves valid guidance for that operation
#### Scenario: Empty and formatted guidance
- **WHEN** a guidance array contains empty strings and non-empty strings with line breaks or Markdown
- **THEN** the system removes the empty entries
- **AND** preserves the non-empty entries in their original order and form
@@ -0,0 +1,48 @@
## ADDED Requirements
### Requirement: Expose current context to operation instruction surfaces
The system SHALL expose project context to apply and archive instruction output by reading the current config from the selected planning root at execution time.
#### Scenario: Apply requests current context
- **WHEN** a user requests apply instructions and config contains project context
- **THEN** apply output includes that context as a structured optional field
#### Scenario: Archive requests current context
- **WHEN** a user requests archive instructions and config contains project context
- **THEN** archive output includes that context as a structured optional field
#### Scenario: Selected store supplies context
- **WHEN** apply or archive instructions target a selected store
- **THEN** context is read from that store's resolved config rather than the current repository config
#### Scenario: Context changes between operations
- **WHEN** project context changes after one instruction call
- **THEN** the next apply or archive instruction call receives the updated context
#### Scenario: Context is absent
- **WHEN** project config has no non-empty context
- **THEN** apply and archive structured outputs omit the context field
### Requirement: Consume operation context as required agent instruction
The system SHALL identify returned operation context as a required agent instruction input with the same prompt-level consumption expectation as the built-in instruction. Context supplies applicable project facts, conventions, and constraints without becoming output content or replacing CLI-controlled workflow state.
#### Scenario: Skill applies project context
- **WHEN** an apply or archive skill receives project context
- **THEN** the skill tells the agent to read and consider the context
- **AND** apply its relevant project facts, conventions, and constraints while performing the operation
- **AND** the workflow does not automatically insert the context into an output file
#### Scenario: Context conflicts with controlling workflow input
- **WHEN** project context conflicts with a built-in workflow step, explicit user choice, resolved path, CLI-controlled state, or command contract
- **THEN** the skill reports the conflict
- **AND** does not use context to replace or bypass the controlling workflow input
- **AND** does not claim that prompt text can enforce agent compliance
@@ -0,0 +1,58 @@
## ADDED Requirements
### Requirement: Configure operation guidance
The system SHALL allow projects to configure additive advice for supported operations under `operations.<operation>.guidance` without treating that guidance as an artifact rule, the built-in workflow, or an enforceable check.
#### Scenario: Configure apply and archive guidance
- **WHEN** config contains guidance arrays under `operations.apply.guidance` and `operations.archive.guidance`
- **THEN** both operation configurations are available to their matching operation
- **AND** artifact rules remain unchanged
#### Scenario: Operation has no guidance
- **WHEN** a supported operation has no configured guidance or only empty guidance entries
- **THEN** the operation output omits `operationGuidance`
### Requirement: Consume operation guidance as optional additive advice
The system SHALL present returned operation guidance as optional additive advice rather than as the operation's built-in flow or an enforceable check. A skill that receives guidance SHALL tell the agent to read and consider every entry, follow entries that are applicable and compatible with the built-in workflow, and keep the field separate from built-in instructions, CLI-controlled state, and explicit user choices.
#### Scenario: Guidance complements built-in flow
- **WHEN** archive guidance asks for a concise completion summary
- **THEN** the archive skill tells the agent to follow that applicable guidance
- **AND** preserves its built-in steps and prompts
#### Scenario: Guidance conflicts with built-in behavior
- **WHEN** operation guidance conflicts with a built-in workflow step, explicit user choice, resolved path, or command contract
- **THEN** instruction output keeps the conflicting text in `operationGuidance` rather than merging it into built-in instruction, state, path, or command fields
- **AND** the generated skill tells the agent to explain why the advice was not followed
- **AND** does not use the conflicting entry to replace or bypass the controlling workflow input
- **AND** existing CLI validation, state calculation, resolved paths, and command contracts remain unchanged
- **AND** the system does not claim that prompt text can enforce agent compliance
### Requirement: Load operation guidance at execution time
The system SHALL read operation guidance from the current selected-root config whenever an apply or archive instruction surface is invoked.
#### Scenario: Guidance changes after skill generation
- **WHEN** a generated skill already exists and project operation guidance is later changed
- **THEN** the next matching operation receives the updated guidance without regenerating the skill
#### Scenario: Selected store supplies guidance
- **WHEN** operation instructions target a selected store
- **THEN** guidance is read from that store's config
### Requirement: Preserve guidance content
The system SHALL preserve non-empty guidance strings, including line breaks and Markdown, when returning them to an operation.
#### Scenario: Multi-line Markdown guidance
- **WHEN** configured operation guidance contains multiple lines and Markdown
- **THEN** structured operation output returns the text without rewriting its content
@@ -0,0 +1,42 @@
## ADDED Requirements
### Requirement: Consume current apply operation inputs
The `/opsx:apply` skill SHALL consume current project context and apply operation guidance returned by `openspec instructions apply --change "<name>" --json` while preserving its existing state-driven workflow.
#### Scenario: Apply context and guidance are configured
- **WHEN** apply instruction output contains `context` and `operationGuidance`
- **THEN** the skill treats context as a required prompt-level instruction input
- **AND** tells the agent to read it and apply relevant project facts, conventions, and constraints
- **AND** treats operation guidance as optional additive advice
- **AND** tells the agent to read and consider it and follow entries that are applicable and compatible with the built-in workflow
#### Scenario: Apply operation inputs are absent
- **WHEN** apply instruction output omits context and operation guidance
- **THEN** the skill continues with its existing apply workflow
#### Scenario: Runtime instructions conflict with apply state
- **WHEN** context or operation guidance conflicts with CLI-returned state, missing artifacts, tasks, progress, context files, or built-in instruction
- **THEN** the generated skill keeps required project context and advisory operation guidance separate from the CLI-returned apply fields
- **AND** tells the agent to report context conflicts
- **AND** tells the agent to explain why conflicting or inapplicable operation guidance was not followed
- **AND** this change does not modify the CLI-returned state, missing artifacts, tasks, progress, context files, or built-in instruction
- **AND** the template tells the agent that neither field is evidence of task completion or permission to bypass a blocked state
- **AND** the system does not represent that prompt-level precedence as an enforceable check
#### Scenario: Apply consumes runtime instructions without copying them
- **WHEN** the skill receives context or operation guidance
- **THEN** it does not copy those fields verbatim into implementation files or planning artifacts unless separately requested by the user
### Requirement: Preserve apply workflow behavior
The `/opsx:apply` skill template and CLI contract SHALL keep their existing change selection, context loading, task progression, pause-on-blocker behavior, and completion reporting structure in this change.
#### Scenario: Runtime inputs are consumed
- **WHEN** apply instructions return configured operation inputs
- **THEN** no CLI-controlled apply state transition, required implementation task, or completion criterion is added, removed, or replaced solely by this change
@@ -0,0 +1,106 @@
## ADDED Requirements
### Requirement: Load current archive operation inputs
The `/opsx:archive` skill SHALL request current archive operation inputs after resolving the target change and selected planning root, while preserving its existing archive workflow.
#### Scenario: Archive context and guidance are configured
- **WHEN** the skill has selected a change
- **AND** current config contains project context and `operations.archive.guidance`
- **THEN** the skill calls `openspec instructions archive --change "<name>" --json` with the selected-root context
- **AND** treats context as a required prompt-level instruction input
- **AND** tells the agent to read it and apply relevant project facts, conventions, and constraints
- **AND** treats operation guidance as optional additive advice
- **AND** tells the agent to read and consider it and follow entries that are applicable and compatible with the built-in archive workflow
#### Scenario: Archive operation inputs are absent
- **WHEN** archive instruction output omits context and operation guidance
- **THEN** the skill continues with its existing archive workflow
#### Scenario: Archive instruction lookup fails
- **WHEN** `openspec instructions archive --change "<name>" --json` exits non-zero or does not return valid archive-instruction JSON
- **THEN** the skill reports the instruction lookup error
- **AND** stops before inspecting or writing specs or moving the change
- **AND** does not treat the failed lookup as absent context or operation guidance
#### Scenario: Archive context or guidance conflicts with the workflow
- **WHEN** returned context or operation guidance conflicts with a built-in archive step, explicit user choice, resolved path, or command contract
- **THEN** the generated skill keeps required project context and advisory operation guidance separate from built-in steps and CLI-derived values
- **AND** tells the agent to report context conflicts
- **AND** tells the agent to explain why conflicting or inapplicable operation guidance was not followed
- **AND** this change leaves existing CLI checks, resolved paths, and command contracts unchanged
- **AND** the template tells the agent not to infer replacement paths, skipped prompts, or command flags from either field
- **AND** the system does not represent that prompt-level precedence as an enforceable check
#### Scenario: Archive consumes runtime instructions without copying them
- **WHEN** the skill receives context or operation guidance
- **THEN** it does not copy those fields verbatim into specs, change artifacts, or archive summaries unless separately requested by the user
### Requirement: Preserve archive execution behavior
The `/opsx:archive` skill SHALL keep its existing completion checks, task checks, spec-sync decision, confirmation behavior, archive move, and completion summary in this change.
#### Scenario: Runtime inputs are loaded
- **WHEN** archive instructions return configured inputs
- **THEN** no archive execution phase, filesystem operation, or user decision is added, removed, or reordered solely by this change
### Requirement: Carry artifact rules into archive-driven spec sync
The `/opsx:archive` skill SHALL fetch current `specs` artifact instructions before archive-driven spec sync writes main specs and SHALL use the returned artifact rules only to constrain those specs.
#### Scenario: Archive discovers delta specs from the specs artifact
- **WHEN** archive assesses delta specs for a selected change
- **THEN** it uses `artifactPaths.specs.existingOutputPaths` from that change's status output as the complete delta-spec input
- **AND** does not infer delta specs from other artifacts
#### Scenario: Schema or change has no specs outputs
- **WHEN** `artifactPaths.specs` is absent or its `existingOutputPaths` list is empty
- **THEN** archive continues without a spec-sync prompt
- **AND** does not request `specs` artifact instructions
#### Scenario: Archive sync writes main specs
- **WHEN** `artifactPaths.specs.existingOutputPaths` contains delta specs
- **AND** the user chooses to sync them during archive
- **THEN** the skill requests `openspec instructions specs --change "<name>" --json` once using the selected change and planning root
- **AND** applies the returned artifact rules while semantically merging the delta into the main spec
- **AND** keeps artifact rules separate from archive `operationGuidance`
- **AND** passes the specs-rule snapshot to the inline sync workflow so that workflow does not fetch the same instructions again
#### Scenario: Specs instruction lookup fails
- **WHEN** delta specs exist and the user chooses to sync them during archive
- **AND** `openspec instructions specs --change "<name>" --json` exits non-zero or does not return valid artifact-instruction JSON
- **THEN** the skill reports the instruction lookup error
- **AND** stops before modifying any main spec or moving the change
- **AND** does not treat the failed lookup as an absent artifact rule set
#### Scenario: User archives without syncing
- **WHEN** delta specs exist and the user explicitly chooses archive without syncing
- **THEN** the skill does not request `specs` artifact instructions for a merge
- **AND** the existing archive-without-sync path continues
#### Scenario: Artifact rules are absent
- **WHEN** archive-driven spec sync receives no rules from `specs` artifact instructions
- **THEN** the existing semantic merge behavior continues unchanged
#### Scenario: Artifact rules contain operation-like advice
- **WHEN** an artifact rule describes archive paths, prompts, command flags, or unrelated workflow steps
- **THEN** the generated skill limits that rule to the content and form of the artifact being written
- **AND** existing archive paths, prompts, CLI checks, and command contracts remain unchanged
#### Scenario: Artifact rule text is consumed
- **WHEN** archive-driven spec sync applies artifact rules
- **THEN** the rules guide the resulting artifact without being copied verbatim into that artifact or the archive summary
@@ -0,0 +1,78 @@
## ADDED Requirements
### Requirement: Load current archive inputs for a batch
The `/opsx:bulk-archive` skill SHALL request current archive operation inputs once for the selected planning root without changing its existing batch orchestration.
#### Scenario: Batch context and guidance are configured
- **WHEN** the skill has selected one or more changes from one planning root
- **THEN** it calls `openspec instructions archive --change "<selected-change>" --json` once for that root
- **AND** treats context as a required prompt-level instruction input and applies relevant project facts, conventions, and constraints across the batch
- **AND** treats operation guidance as optional additive advice, considers every entry, and follows entries that are applicable and compatible with the built-in batch workflow
#### Scenario: Batch operation inputs are absent
- **WHEN** archive instruction output omits context and operation guidance
- **THEN** the skill continues with its existing bulk archive behavior
#### Scenario: Batch archive instruction lookup fails
- **WHEN** `openspec instructions archive --change "<selected-change>" --json` exits non-zero or does not return valid archive-instruction JSON
- **THEN** the skill reports the instruction lookup error
- **AND** stops the batch before inspecting or writing specs or moving any change
- **AND** does not treat the failed lookup as absent context or operation guidance
#### Scenario: Context or guidance conflicts with batch behavior
- **WHEN** context or operation guidance conflicts with built-in conflict analysis, explicit user choices, resolved paths, or command contracts
- **THEN** the generated skill keeps required project context and advisory operation guidance separate from conflict analysis and CLI-derived values
- **AND** tells the agent to report context conflicts
- **AND** tells the agent to explain why conflicting or inapplicable operation guidance was not followed
- **AND** this change leaves existing CLI checks, resolved paths, and command contracts unchanged
- **AND** the template tells the agent not to infer skipped prompts, replacement paths, or command flags from either field
- **AND** the system does not represent that prompt-level precedence as an enforceable check
### Requirement: Carry artifact rules into each batch spec sync
The `/opsx:bulk-archive` skill SHALL fetch current `specs` artifact instructions for each selected change with concrete delta specs and SHALL use the returned artifact rules only for main specs written by that change's merge.
#### Scenario: Discover specs inputs per change
- **WHEN** bulk archive assesses delta specs for a selected change
- **THEN** it uses that change's `artifactPaths.specs.existingOutputPaths` as the complete delta-spec input
- **AND** does not infer delta specs from other artifacts
#### Scenario: Selected changes use different schemas
- **WHEN** a batch contains changes using different schemas
- **THEN** the skill evaluates `artifactPaths.specs.existingOutputPaths` separately for each change
- **AND** requests `specs` artifact instructions once for each change whose list contains delta specs, using that change and selected root
- **AND** obtains every required specs-instruction snapshot before the first main-spec write
- **AND** applies each returned rule set only to main specs produced from that change
- **AND** passes each change's specs-rule snapshot to its inline sync workflow without a duplicate instruction fetch
#### Scenario: A batch specs instruction lookup fails
- **WHEN** a required `openspec instructions specs --change "<name>" --json` lookup exits non-zero or does not return valid artifact-instruction JSON
- **THEN** the skill reports the affected change and instruction lookup error
- **AND** stops the whole batch before writing any main spec or moving any change
- **AND** does not treat the failed lookup as an absent artifact rule set
#### Scenario: A batch change has no specs outputs
- **WHEN** a selected change has no `artifactPaths.specs` entry or its `existingOutputPaths` list is empty
- **THEN** no spec sync or `specs` instruction lookup is performed for that change
- **AND** the change continues through the existing batch archive flow
#### Scenario: Batch artifact rules remain separate from archive guidance
- **WHEN** artifact instructions contain rules and archive instructions contain `operationGuidance`
- **THEN** artifact rules constrain spec content and form
- **AND** configured archive guidance remains optional additive advice for choices within the archive operation
- **AND** neither field is relabeled or merged into the other
#### Scenario: Batch has no artifact rules
- **WHEN** `specs` artifact instructions return no rules for a selected change
- **THEN** the existing batch conflict resolution and semantic merge behavior continue unchanged
@@ -0,0 +1,42 @@
## ADDED Requirements
### Requirement: Carry artifact rules into standalone spec sync
The `/opsx:sync` skill SHALL use the selected change's concrete `specs` artifact outputs as its delta-spec input and SHALL apply current `specs` artifact rules before writing a main spec.
#### Scenario: Discover delta specs from status
- **WHEN** standalone sync assesses a selected change
- **THEN** it uses `artifactPaths.specs.existingOutputPaths` from that change's status output as the complete delta-spec input
- **AND** does not infer delta specs from other artifacts
#### Scenario: Standalone sync fetches current artifact rules
- **WHEN** `artifactPaths.specs.existingOutputPaths` contains one or more delta specs
- **THEN** standalone sync requests `openspec instructions specs --change "<name>" --json` once using the selected change and planning root
- **AND** applies only the returned artifact rules to main specs produced from those delta paths
- **AND** keeps artifact rules separate from operation guidance and unrelated workflow steps
#### Scenario: Specs instruction lookup fails
- **WHEN** `openspec instructions specs --change "<name>" --json` exits non-zero or does not return valid artifact-instruction JSON
- **THEN** standalone sync reports the instruction lookup error
- **AND** stops before writing any main spec
- **AND** does not treat the failed lookup as an absent artifact rule set
#### Scenario: Schema or change has no specs outputs
- **WHEN** `artifactPaths.specs` is absent or its `existingOutputPaths` list is empty
- **THEN** standalone sync reports that there are no delta specs to sync
- **AND** does not request artifact instructions or write a main spec
#### Scenario: Archive supplies an artifact-rule snapshot
- **WHEN** the sync workflow is invoked inline by archive with a specs-rule snapshot from current artifact instructions
- **THEN** it reuses that supplied snapshot
- **AND** does not fetch `specs` artifact instructions again
#### Scenario: Artifact rules are absent
- **WHEN** current `specs` instructions contain no rules
- **THEN** the existing semantic merge behavior continues unchanged
@@ -0,0 +1,51 @@
## 1. Project Config Model
- [x] 1.1 Add explicit `apply` and `archive` operation IDs plus typed `operations.<operation>.guidance` config structures without changing artifact `rules`
- [x] 1.2 Extend resilient config parsing to preserve valid operations, omit malformed entries independently, filter empty guidance, and warn for unknown operations or fields
- [x] 1.3 Preserve non-empty multi-line and Markdown guidance without rewriting its content
- [x] 1.4 Update config generation and help text with separate artifact-rule and advisory operation-guidance examples
- [x] 1.5 Add project-config tests for valid, absent, malformed, mixed-validity, empty, unknown, multi-line, and Markdown operation guidance
## 2. Shared Runtime Inputs
- [x] 2.1 Extend the existing root-config loading path to read project config once per instruction command, then pass that parsed snapshot to a shared operation-input helper returning separate optional `context` and `operationGuidance` fields
- [x] 2.2 Ensure each new command invocation reads a fresh config snapshot, omits empty values, avoids duplicate malformed-field warnings, and never exposes artifact rules as operation guidance
- [x] 2.3 Add unit tests for operation matching, runtime freshness across commands, one-read/one-warning behavior within a command, absent fields, field separation, and selected-store roots
## 3. Apply Instructions
- [x] 3.1 Extend apply instruction types and generation with current `context` and apply `operationGuidance` while preserving existing state, progress, tasks, context files, references, and root output
- [x] 3.2 Render project context as a required prompt-level input section and operation guidance as a separate advisory section in apply text output
- [x] 3.3 Update the apply skill and generated templates to require relevant context consumption, consider every guidance entry, and follow guidance only when applicable and compatible with the built-in workflow
- [x] 3.4 Keep both fields separate from CLI-returned state, tasks, progress, context files, and built-in instructions; report context conflicts, explain rejected guidance, prevent input copying, and preserve blocked/ready/all-done behavior
- [x] 3.5 Add unit, CLI integration, and template-parity tests for required context labeling and consumption, advisory guidance handling, conflict reporting, absent inputs, runtime freshness, and unchanged apply state behavior
## 4. Archive Runtime Inputs
- [x] 4.1 Route `openspec instructions archive --change <name>` to a dedicated read-only archive instruction handler using existing repo/store root resolution and change validation
- [x] 4.2 Return `changeName`, optional current `context`, optional archive `operationGuidance`, and the normal root envelope in JSON without returning the static archive workflow template
- [x] 4.3 Render project context as a required prompt-level input section and operation guidance as a separate advisory section in human-readable archive output, with a valid empty-input result
- [x] 4.4 Add tests for required and invalid changes, selected stores, runtime freshness, absent inputs, JSON output, final text labels, and absence of archive filesystem mutations
## 5. Archive and Sync Skill Consumption
- [x] 5.1 Fetch current archive inputs in the single-change archive workflow after resolving the selected change and root, and stop before spec inspection, writes, or moves on a non-zero or invalid JSON response
- [x] 5.2 Fetch archive inputs once per selected root in bulk archive and stop the whole batch before spec inspection, writes, or moves on lookup failure
- [x] 5.3 Require single and bulk archive skills to apply relevant context, treat operation guidance as advisory, report context conflicts, and explain guidance that is inapplicable or conflicts with controlling workflow input
- [x] 5.4 Keep `artifactPaths.specs.existingOutputPaths` as the only delta-spec source in archive, bulk archive, and standalone sync; treat a missing `specs` entry or empty output list as no spec sync and do not infer deltas from other artifacts
- [x] 5.5 Before archive-driven spec sync writes a main spec, fetch `openspec instructions specs` once for the selected change/root, apply its rules to the semantic merge, and pass the specs-rule snapshot into inline sync; stop before any main-spec write or change move on lookup failure
- [x] 5.6 Fetch current `specs` instructions during standalone sync, reuse an archive-supplied specs-rule snapshot without re-fetching, and stop before writing a main spec on direct lookup failure
- [x] 5.7 Resolve every required specs-instruction snapshot in bulk archive before the first main-spec write; report the affected change and stop the whole batch before writes or moves if any lookup fails
- [x] 5.8 Keep context, advisory operation guidance, artifact rules, conflict analysis, and CLI-derived values structurally separate; constrain rules to written artifacts, preserve existing checks and contracts, and prevent instruction text from being copied into output files
- [x] 5.9 Preserve existing single-change and bulk archive orchestration, prompts, semantic merge ownership, filesystem operations, and summaries
- [x] 5.10 Add tests for required context and advisory guidance semantics, conflict reporting, present/missing/empty `artifactPaths.specs`, artifact rules, selected roots, direct and inline sync, snapshot reuse, invalid responses, no-write/no-move behavior, mixed-schema batches, field separation, unchanged CLI checks, and non-copying
- [x] 5.11 Regenerate checked-in apply, archive, bulk archive, and sync skills and update affected template/golden hashes
## 6. Documentation and Verification
- [x] 6.1 Document required context consumption, advisory `operations.apply.guidance` and `operations.archive.guidance`, runtime freshness, selected-root behavior, field separation, fail-closed archive/specs instruction consumption, `artifactPaths.specs` as the spec-sync contract, `specs` rules travelling with produced main specs, and the read-only archive instruction command
- [x] 6.2 Document that archive execution phases, semantic merge ownership, direct archive CLI behavior, and artifact-rule configuration/output remain unchanged by this change
- [x] 6.3 Add a minor changeset covering runtime apply/archive inputs and archive-driven spec-rule consumption
- [x] 6.4 Run formatting, type checking, build, targeted config/apply/archive/template tests, and the full test suite
- [x] 6.5 Verify repo/store root selection and path handling on Windows CI and the existing supported platforms
- [x] 6.6 Run `openspec validate extend-config-injection-to-apply-archive --strict` and reconcile every task with the final implementation diff
@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-06-29
@@ -0,0 +1,59 @@
## Context
OpenSpec supports AI coding assistants by generating two artifact types per tool: skill files (for agent instruction loading) and command files (for slash-command invocation). Each tool has a `ToolCommandAdapter` that controls the output path and file format.
Oh My Pi (OMP) is a terminal AI coding agent that uses a `.omp/` project directory. Its command system uses the filename stem as the slash command name (e.g., `opsx-propose.md` → `/opsx-propose`), which requires command body references to be in hyphenated form (`/opsx-propose` rather than `/opsx:propose`). This is the same pattern already used by Pi and OpenCode.
## Goals / Non-Goals
**Goals:**
- Add a `ToolCommandAdapter` for Oh My Pi producing `.omp/commands/opsx-<id>.md` with `description` frontmatter.
- Inject `**Provided arguments**: $@` after the `**Input**:` heading in command bodies so user-supplied arguments are visible to the agent when a command is invoked with arguments.
- Register the adapter so `init` and `update` can generate command files and skill files for OMP.
- Apply `transformToHyphenCommands` to OMP skill bodies so `/opsx:` references become `/opsx-` for consistency with the command naming convention.
- Add OMP to `AI_TOOLS` so it appears in tool selection and auto-detection.
**Non-Goals:**
- Changing the file format used by Pi or OpenCode.
- Adding OMP-specific frontmatter fields beyond `description`.
- Auto-detecting OMP presence (the `.omp/` directory is sufficient as `skillsDir`).
## Decisions
### Reuse the existing `transformToHyphenCommands` transformer for skill files
**Decision**: Add `'oh-my-pi'` to the `tool.value` conditional in `init.ts` and `update.ts` that selects the hyphen transformer.
**Rationale**: Pi and OpenCode follow the same filename-as-command-name convention and are already handled by this branch. OMP has an identical convention. Extending the same conditional is minimal-diff and keeps the pattern consistent.
**Alternative considered**: Storing the transformer flag on the `AIToolOption` object (e.g., `useHyphenCommands: true`). This is cleaner long-term but is a larger refactor than this change warrants. It can be done separately if more tools adopt this convention.
### Use `description`-only frontmatter in command files
**Decision**: The `formatFile` method outputs only a `description` YAML field in frontmatter.
**Rationale**: OMP's command format uses filename for the slash command name and `description` for display. No additional frontmatter fields (name, category, tags) are needed, matching the minimalist approach used by Pi.
### Inject `$@` into command bodies (matching Pi)
**Decision**: Apply the same `injectArgs` logic as Pi's adapter — append `**Provided arguments**: $@` on the line after the `**Input**:` heading, skipping injection if `$@` or `$ARGUMENTS` is already present.
**Rationale**: OpenSpec command templates contain an `**Input**:` heading that describes what arguments the command accepts (e.g., `**Input**: The argument after /opsx-propose is the change name…`). Without injecting `$@`, a user running `/opsx-propose my-feature` passes `my-feature` as `$@` but the agent never sees it — the argument is silently discarded. OMP's prompt template spec explicitly supports `$@` and positional forms. Pi faces the same problem and already solves it with identical injection logic.
**Alternative considered**: Leaving injection out and relying on users to add `$@` manually to the template. Rejected: this would silently break argument passing for all OMP commands and diverge from Pi's established behavior.
### Tool ID is `'oh-my-pi'`, skills directory is `'.omp'`
**Decision**: `value: 'oh-my-pi'` in `AI_TOOLS`; `skillsDir: '.omp'`.
**Rationale**: The tool ID uses the full kebab-case name for human clarity. The `.omp/` directory is the short canonical path users will see on disk. The two are independent and follow the precedent set by `kilocode` (ID) → `.kilocode` (dir).
## Risks / Trade-offs
- **`.omp/` directory collision**: If a project uses `.omp/` for another purpose, OMP detection will yield a false positive. → Mitigation: This is consistent with how every other tool is detected; no special handling is warranted.
- **Conditional growth in init.ts / update.ts**: Adding a third value to the `tool.value === 'opencode' || tool.value === 'pi'` checks makes the long-term refactor to a per-tool flag more urgent. → Mitigation: Document in tasks; the refactor is low-risk and can follow separately.
- **Adapter missing `escapeYamlValue`**: If a command description contains special YAML characters, the description frontmatter could be malformed. → Mitigation: `escapeYamlValue` is applied in this implementation (task 1.2), consistent with Pi adapter.
## Open Questions
None — implementation is well-defined by the existing Pi/OpenCode/OMP pattern.
@@ -0,0 +1,34 @@
## Why
Oh My Pi (OMP) is a terminal AI coding agent whose users expect OpenSpec workflows to be available as slash commands. Without an adapter, users who have OMP configured in their project cannot generate OMP-native command files or get the correct skill transformations from `openspec init` or `openspec update`.
## What Changes
- Add a `ToolCommandAdapter` for Oh My Pi that generates command files at `.omp/commands/opsx-<id>.md` with YAML `description` frontmatter, hyphen-based command references, and `$@` argument injection after the `**Input**:` heading (matching Pi's convention so user-supplied arguments are visible to the agent).
- Register `oh-my-pi` in `AI_TOOLS` with `skillsDir: '.omp'` so detection and skill generation work.
- Register the new adapter in `CommandAdapterRegistry` and `adapters/index.ts`.
- Add Oh My Pi to the `transformToHyphenCommands` whitelist in `init.ts` and `update.ts` so skill files use the correct `/opsx-*` invocation form that matches OMP's filename-based command naming.
- Add test coverage for the new adapter.
- Update `docs/supported-tools.md` with the new tool's directory reference.
## Capabilities
### New Capabilities
- `oh-my-pi-tool`: Command and skill generation support for the Oh My Pi (OMP) AI coding agent, following its `.omp/commands/opsx-<id>.md` format with `description` frontmatter, hyphen-based command references, and `$@` argument injection.
### Modified Capabilities
- `cli-init`: Oh My Pi is added to the supported tool list and the hyphen-command transformer whitelist.
- `cli-update`: Oh My Pi is added to the hyphen-command transformer whitelist for skill regeneration.
## Impact
- `src/core/command-generation/adapters/oh-my-pi.ts` — new adapter
- `src/core/command-generation/adapters/index.ts` — export new adapter
- `src/core/command-generation/registry.ts` — register adapter
- `src/core/config.ts` — add `oh-my-pi` entry to `AI_TOOLS`
- `src/core/init.ts` — extend hyphen-command transformer conditional
- `src/core/update.ts` — extend hyphen-command transformer conditional (two call sites)
- `test/core/command-generation/adapters.test.ts` — adapter unit tests
- `docs/supported-tools.md` — add Oh My Pi row to directory reference table
@@ -0,0 +1,15 @@
## ADDED Requirements
### Requirement: Oh My Pi tool supported in init
The `openspec init` command SHALL support Oh My Pi as a configurable tool, generating both skill files and command files using Oh My Pi's conventions when selected.
#### Scenario: Selecting Oh My Pi during init
- **WHEN** a user selects Oh My Pi during `openspec init`
- **THEN** skill files are written to `.omp/skills/openspec-<id>/SKILL.md` for each active command
- **AND** command files are written to `.omp/commands/opsx-<id>.md` for each active command
- **AND** skill file bodies use hyphen-based `/opsx-<id>` command references
- **AND** command file bodies have `**Provided arguments**: $@` injected after any `**Input**:` heading
#### Scenario: Oh My Pi listed when .omp directory is detected
- **WHEN** the project root contains a `.omp/` directory
- **THEN** Oh My Pi is pre-checked in the tool selection during `openspec init`
@@ -0,0 +1,13 @@
## ADDED Requirements
### Requirement: Oh My Pi tool supported in update
The `openspec update` command SHALL refresh Oh My Pi skill files and command files when Oh My Pi is configured, using Oh My Pi's hyphen-based command reference convention.
#### Scenario: Updating Oh My Pi skill files
- **WHEN** `openspec update` runs and Oh My Pi is a configured tool
- **THEN** skill files in `.omp/skills/openspec-<id>/SKILL.md` are refreshed with the latest templates
- **AND** skill file bodies use hyphen-based `/opsx-<id>` command references
#### Scenario: Updating Oh My Pi command files
- **WHEN** `openspec update` runs and Oh My Pi is a configured tool
- **THEN** command files are written to `.omp/commands/opsx-<id>.md` for each workflow in the active profile, creating them if they do not yet exist and overwriting them if they do
@@ -0,0 +1,48 @@
## ADDED Requirements
### Requirement: Oh My Pi command file generation
OpenSpec SHALL generate command files for Oh My Pi in `.omp/commands/opsx-<id>.md`, one per active workflow command.
Each file SHALL include a YAML frontmatter block with a `description` field. The command body SHALL transform `/opsx:` references to `/opsx-` to match Oh My Pi's filename-based slash command naming (e.g., `opsx-propose.md` → `/opsx-propose`). It SHALL inject `**Provided arguments**: $@` on the line immediately following any `**Input**:` heading, unless `$@` or `$ARGUMENTS` is already present in the body.
#### Scenario: Command file path follows OMP convention
- **WHEN** OpenSpec generates a command file for Oh My Pi for workflow command `propose`
- **THEN** the file is written to `.omp/commands/opsx-propose.md`
#### Scenario: Command file format includes description frontmatter
- **WHEN** OpenSpec writes a command file for Oh My Pi
- **THEN** the file begins with a YAML frontmatter block containing only a `description` field
- **AND** the body follows the closing `---`
#### Scenario: Command body uses hyphen-based references
- **WHEN** OpenSpec writes a command file for Oh My Pi whose body contains `/opsx:apply` or similar colon-style references
- **THEN** those references are transformed to `/opsx-apply` in the output file
#### Scenario: Command body exposes user arguments via $@
- **WHEN** OpenSpec writes a command file for Oh My Pi whose body contains a `**Input**:` heading and no existing `$@` or `$ARGUMENTS` reference
- **THEN** `**Provided arguments**: $@` is injected on the line immediately after the `**Input**:` heading
- **AND** when the user invokes `/opsx-propose my-feature`, the agent receives `my-feature` as the value of `$@`
### Requirement: Oh My Pi skill file generation
OpenSpec SHALL generate skill files for Oh My Pi in `.omp/skills/openspec-<id>/SKILL.md`, one per active workflow command.
Skill file bodies SHALL have `/opsx:` references transformed to `/opsx-` so that skill invocations refer to the correct hyphen-based slash command names.
#### Scenario: Skill file path follows OMP convention
- **WHEN** OpenSpec generates a skill file for Oh My Pi for workflow command `explore`
- **THEN** the file is written to `.omp/skills/openspec-explore/SKILL.md`
#### Scenario: Skill body uses hyphen-based references
- **WHEN** OpenSpec writes a skill file for Oh My Pi whose body contains `/opsx:explore`
- **THEN** the reference is transformed to `/opsx-explore` in the output file
### Requirement: Oh My Pi tool detection
OpenSpec SHALL detect an Oh My Pi installation when the `.omp/` directory exists at the project root, and SHALL present Oh My Pi as a selectable tool in `openspec init` and `openspec update`.
#### Scenario: Auto-detection when .omp directory exists
- **WHEN** the project root contains a `.omp/` directory
- **THEN** Oh My Pi is listed as a detected tool during `openspec init` and `openspec update`
#### Scenario: Oh My Pi appears in the tool selection list
- **WHEN** a user runs `openspec init` interactively
- **THEN** Oh My Pi appears as a selectable option in the tool list
@@ -0,0 +1,30 @@
## 1. Adapter
- [x] 1.1 Create `src/core/command-generation/adapters/oh-my-pi.ts` with `ohMyPiAdapter` (toolId `'oh-my-pi'`, path `.omp/commands/opsx-<id>.md`, description-only frontmatter, `transformToHyphenCommands` on body)
- [x] 1.2 Use `escapeYamlValue` for the `description` frontmatter field (consistent with Pi adapter)
- [x] 1.3 Export `ohMyPiAdapter` from `src/core/command-generation/adapters/index.ts`
- [x] 1.4 Import and register `ohMyPiAdapter` in `src/core/command-generation/registry.ts`
- [x] 1.5 In `formatFile`, inject `**Provided arguments**: $@` on the line after the `**Input**:` heading (skip if `$@` or `$ARGUMENTS` already present) — matching Pi adapter's `injectPiArgs` logic
## 2. Tool Registration
- [x] 2.1 Add `{ name: 'Oh My Pi', value: 'oh-my-pi', available: true, successLabel: 'Oh My Pi', skillsDir: '.omp' }` to `AI_TOOLS` in `src/core/config.ts` (alphabetical by name, between Mistral Vibe and OpenCode)
## 3. Skill Transformer Wiring
- [x] 3.1 In `src/core/init.ts`, extend the skill transformer conditional to include `tool.value === 'oh-my-pi'` alongside `'opencode'` and `'pi'` (one occurrence, in `generateSkillsAndCommands`)
- [x] 3.2 In `src/core/update.ts`, extend the skill transformer conditional to include `tool.value === 'oh-my-pi'` alongside `'opencode'` and `'pi'` (two occurrences: primary update loop and `upgradeLegacyTools`)
## 4. Tests
- [x] 4.1 In `test/core/command-generation/adapters.test.ts`, add unit tests for `ohMyPiAdapter`: verify `toolId`, `getFilePath` output uses `path.join('.omp', 'commands', 'opsx-<id>.md')`, and `formatFile` produces correct description frontmatter and transformed body
- [x] 4.2 Verify all path assertions in the new tests use `path.join()` (not hardcoded slashes) for cross-platform correctness
## 5. Documentation
- [x] 5.1 Add Oh My Pi row to the tool directory reference table in `docs/supported-tools.md`: `| Oh My Pi (\`oh-my-pi\`) | \`.omp/skills/openspec-*/SKILL.md\` | \`.omp/commands/opsx-<id>.md\` |`
## 6. Verification
- [x] 6.1 Run `pnpm test` and confirm all tests pass, including the new adapter tests
- [x] 6.2 Run `pnpm build` to confirm TypeScript compilation succeeds with the new adapter
@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-07-15
@@ -0,0 +1,52 @@
## Context
The CLI currently creates two user-visible date-only values by truncating `Date#toISOString()`: archive directory prefixes and the `created` field in newly scaffolded `.openspec.yaml` files. ISO serialization is UTC, so either value can disagree with the calendar date in the effective local time zone of the Node.js process running the CLI.
The repository supports Node.js 20.19+ on Windows, macOS, and Linux. The selected contract is the calendar date in the executing Node.js process's effective local time zone, rather than a project-wide or UTC time zone. "Effective local time zone" means the time zone used by Node.js local `Date` accessors, normally derived from the host environment and any runtime-supported process time-zone configuration.
## Goals / Non-Goals
**Goals:**
- Produce date-only archive prefixes and new-change metadata from the executing CLI process's effective local calendar date.
- Keep the date representation stable as zero-padded `YYYY-MM-DD` on every supported platform.
- Cover a UTC/local-calendar boundary with deterministic tests.
**Non-Goals:**
- Rename or migrate existing archive directories or existing change metadata.
- Add a project time-zone setting, CLI flag, or user-selectable time zone.
- Change full UTC timestamps used for logs, JSON timestamps, feedback metadata, or backup identifiers.
- Alter agent-generated date prefixes in OPSX archive workflows, which do not derive their dates through `Date#toISOString()`.
## Decisions
### Use a shared local calendar-date formatter
Introduce one small shared formatter for date-only values. It will derive year, month, and day with local `Date` accessors and zero-pad the numeric parts into `YYYY-MM-DD`. It will accept a `Date` value (defaulting to the current time) so callers share the same behavior and tests can provide a fixed instant.
Both archive naming and change creation will call this formatter. This prevents the two date-only concepts from diverging again while keeping the existing archive and metadata APIs unchanged.
`toISOString().split('T')[0]` is not suitable because it deliberately selects the UTC calendar date. Locale-formatted strings are also unsuitable as a storage and path contract because their separators and ordering are locale-dependent.
### Bind the rule to the executing CLI process's effective local time zone
The formatter will use the local time zone effective for the Node.js process. This matches the user-visible meaning of "today" for an interactive CLI session and gives scripts deterministic behavior when the process time zone is configured. Processes in different time zones may produce different dates for the same instant near a boundary; that is intentional under the selected contract.
### Test the boundary through the process time zone
Tests will temporarily set the Node process time zone to `Asia/Shanghai` and use a fixed instant such as `2026-07-14T16:30:00.000Z`. At that instant the local date is `2026-07-15` while the UTC date is `2026-07-14`, so the test fails if UTC truncation returns. The test setup will restore time and environment state after each case.
## Risks / Trade-offs
- [Different processes can choose different dates at the same instant] → This is the explicit effective-local-time-zone contract and is covered by the affected behavior.
- [Date formatting is accidentally made locale-sensitive] → Use numeric local `Date` parts rather than locale display formatting.
- [Existing historical names retain UTC-derived dates] → Apply the new rule prospectively and leave existing directories and metadata untouched.
## Migration Plan
No data migration is required. New archives and newly created changes use the local-date rule after release; existing archives and metadata remain valid as-is.
## Open Questions
None.
@@ -0,0 +1,27 @@
## Why
Two CLI code paths currently derive date-only values by truncating a UTC ISO timestamp: archive directory prefixes and the `created` field in newly scaffolded change metadata. Near a local midnight boundary, these values can resolve to the previous or next calendar date instead of the date in the CLI process's effective local time zone.
## What Changes
- Define CLI-generated date-only values as the calendar date in the effective local time zone of the Node.js process executing the CLI, formatted as `YYYY-MM-DD`.
- Generate CLI archive directory names from that local date.
- Record the same local date in the `created` field of newly created change metadata.
- Add regression coverage for a non-UTC local-date boundary.
## Capabilities
### New Capabilities
None.
### Modified Capabilities
- `cli-archive`: archive target names use the CLI process's effective local calendar date.
- `change-creation`: newly created change metadata records the CLI process's effective local calendar date.
## Impact
- Affected code: archive naming, change-creation metadata, and a shared date-only formatter.
- Affected tests: archive and change-creation coverage.
- Existing archive directories remain unchanged; the rule applies to newly generated names and metadata only.
@@ -0,0 +1,12 @@
## ADDED Requirements
### Requirement: Local Creation Date Metadata
The system SHALL record the `created` value in metadata for a newly created change as the `YYYY-MM-DD` calendar date in the effective local time zone of the Node.js process executing the CLI.
#### Scenario: Create change across a UTC date boundary
- **GIVEN** the CLI process's effective local time zone is `Asia/Shanghai`
- **AND** the current instant is `2026-07-14T16:30:00.000Z`
- **WHEN** the user creates a change
- **THEN** the new change's `.openspec.yaml` contains `created: 2026-07-15`
@@ -0,0 +1,17 @@
## ADDED Requirements
### Requirement: Local Archive Date
The archive command SHALL derive the `YYYY-MM-DD` prefix of a new archive target from the calendar date in the effective local time zone of the Node.js process executing the CLI.
#### Scenario: Archive crosses a UTC date boundary
- **GIVEN** the CLI process's effective local time zone is `Asia/Shanghai`
- **AND** the current instant is `2026-07-14T16:30:00.000Z`
- **WHEN** the user archives a change named `add-auth`
- **THEN** the target archive name begins with `2026-07-15-add-auth`
#### Scenario: Non-interactive archive uses the local date
- **WHEN** an automation invokes `openspec archive <change-name> --yes`
- **THEN** the target archive name uses the CLI process's effective local calendar date
@@ -0,0 +1,13 @@
## 1. Local date behavior
- [x] 1.1 Add a shared formatter that returns the calendar date in the executing Node.js process's effective local time zone as `YYYY-MM-DD`.
- [x] 1.2 Use the shared formatter for native archive target names.
- [x] 1.3 Use the shared formatter when writing `created` metadata for a new change.
## 2. Regression coverage and validation
- [x] 2.1 Add archive and change-creation tests for a fixed `Asia/Shanghai` UTC-boundary instant, restoring clock and environment state afterward.
- [x] 2.2 Update affected archive test expectations to use the effective-local-date contract.
- [x] 2.3 Add archive and change-creation tests for a non-boundary instant where UTC and local calendar dates match.
- [x] 2.4 Run focused archive and change-creation tests on the supported cross-platform test suite.
- [x] 2.5 Run the full build and OpenSpec validation for the change.
@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-06-29
@@ -0,0 +1,77 @@
# Design: Spec parser reading fidelity
## The requirement reader is implemented twice
| | spec reader: `MarkdownParser.parseRequirements` → `req.text` | delta reader: `Validator.extractRequirementText` / `countScenarios` |
|---|---|---|
| Recognition | every level-3 child of the section | canonical `REQUIREMENT_HEADER_REGEX` `/^###\s*Requirement:\s*(.+)$/i` |
| Body capture | first non-empty line | first substantial line |
| Skip `**metadata**:` | no | yes |
| Fenced code in body | not skipped | not skipped |
| Fenced `#### Scenario:` | not counted (parseSections fence-masks it) | **counted** (`/^####\s+/gm` is fence-unaware) |
| `SHALL`/`MUST` | `text.includes('SHALL')` (substring) | `/\b(SHALL\|MUST)\b/` (word boundary) |
| Reached by | `validate <spec>`, `archive` | `validate <change>` |
`ChangeParser extends MarkdownParser` and reuses `parseRequirements`, so there is no third reader. Every row where the two columns differ is a reproduced defect.
## Reproductions (against `main`)
- **#361** — `### Requirement: …` with `SHALL` on body line 2 → `validate <change>` `✗ must contain SHALL or MUST`; `validate <spec>` `✗ requirements.0.text: …`.
- **#418** — metadata lines before a `MUST` description → `validate <change>` **valid**; `validate <spec>` `✗`, `req.text` = `**ID**: REQ-FILE-001`.
- **#312** — fenced block (with `#` comments) before the prose line → both paths `✗`; `req.text` = `` ```bash ``. (Distinct from the already-fixed section-count manifestation.)
- **Fenced scenario** — requirement whose only `#### Scenario:` is inside a ` ```markdown ` block → `validate <change>` **valid** (counts the fenced scenario); `validate <spec>` `✗ requirements.0.scenarios: must have at least one scenario`. The delta reader passes a malformed requirement.
- **#498** — stray `### Documentation Requirements` divider → `validate <change>` **valid**; `archive` prints non-blocking phantom `Proposal warnings in proposal.md`; `validate <spec>` blocking `✗`. (Also: `show`/`view` count the divider as a requirement — `count=2` with `text='Documentation Notes'`.)
## Approach
### Part A — one shared, fence-aware extraction
A single helper takes the requirement block's lines plus the fence mask and returns the full body: lines from after the header to the first markdown header found on a **non-fence-masked** line (usually `#### Scenario:`, but also a stray `###` divider the delta reader absorbed into the block — its notes must not feed the keyword check), skipping fence-masked lines and blank lines. `**metadata**:` lines are skipped only when other body text remains; a requirement written entirely as `**Constraint**: The system MUST ...` keeps that line as its body. When the body comes back empty, `MarkdownParser` still falls back to the header title for display and bare-header compatibility; validator body-keyword checks for canonical `### Requirement:` blocks use the body-only extraction so #1280's "keyword only in header" hint remains intact on both validation paths. A companion fence-aware scenario counter counts only non-fence-masked `####` headers (deliberately *any* `####`, since the spec path treats every level-4 child as a scenario). Both readers delegate to these. `SHALL`/`MUST` detection uses one predicate.
Why the existing fence tests still pass: in `markdown-parser.test.ts:106`/`:139` the `SHALL` line is first and the fenced block follows, so skipping fenced lines leaves `text` exactly equal to the `SHALL` line — the asserted value. The breaking case (#312) is the inverse — fence *before* prose — which no test covers.
### Part B — surface the #498 divergence (INFO, no recognition change)
`parseDeltaSpec` records the non-canonical level-3 headers it skips *while parsing* the `## ADDED`/`## MODIFIED Requirements` sections, and `validateChangeDeltaSpecs` emits each as an INFO issue. Collecting during the parse (rather than with a separate scanner) guarantees the note describes the reader's real boundaries — a header the reader never saw (e.g. after a fenced `##` line ended the section early) gets no note, and a fenced `###` example line, which the body reader treats as content, is not reported. Under `--strict`, `valid = errors === 0 && warnings === 0` — **INFO is excluded**, so this never changes pass/fail; it only informs. This is the minimal change that makes `validate <change>` stop *silently* passing the #498 input.
## Why recognition tightening is rejected
The obvious #498 fix is to make `parseRequirements` recognize only `### Requirement:` headers. It is rejected because **bare `### <statement>` headers are a supported, tested requirement format**, not a convention violation:
- `test/core/validation.test.ts` builds a spec whose requirements are `### The system SHALL provide secure user authentication` (no `Requirement:` prefix) and asserts `report.valid === true`.
- Bare headers also appear as valid requirements in `test/core/converters/json-converter.test.ts`, `test/core/archive.test.ts`, `test/commands/spec.test.ts`, and `test/core/parsers/markdown-parser.test.ts` (`:258`, `:310`, and the fixtures at `:14`/`:22`/`:55`/`:85`).
Tightening would reclassify all of these as non-requirements, breaking those tests and silently dropping requirements from any real spec that uses the bare style. The cost is not justified by #498, whose harm is a *confusing signal*, not data loss (the archive rebuild already filters to `### Requirement:` blocks, so rebuilt specs are correct regardless). Part B fixes the signal safely. If maintainers later decide to make `### Requirement:` mandatory, that belongs in its own change with a deprecation cycle and fixture migration.
## Safety: write path is independent of the reader
`src/core/specs-apply.ts` rebuilds specs during archive from `extractRequirementsSection` + `RequirementBlock.raw` (raw text split on the canonical header). It does not import or call `parseSpec`/`parseRequirements` and never reads `req.text`. Consequently Part A changes only what is *read/validated/displayed*; archived spec bytes are unchanged. (Note: this means `specs-apply` already uses the canonical `### Requirement:` rule — another reason recognition divergence is a reader-only concern.)
## Read-only blast radius (no write path)
Consumers of `parseSpec`/`req.text`: `view.ts`/`list.ts` (requirement **counts** — unchanged, since recognition is unchanged), `json-converter.ts` (JSON `text` — now the full body), `spec.ts` (display), `change-parser.ts:96` (delta descriptions `Add requirement: ${req.text}` — may span lines), and the `MAX_REQUIREMENT_TEXT_LENGTH` INFO (non-blocking). None affect archived content or pass/fail of valid specs.
## Edge cases for tests
- Single-line requirement unchanged (text and count byte-for-byte).
- Metadata-only body still flags missing `SHALL`/`MUST`.
- Fenced `#### Scenario:` / `#`-comment lines do not corrupt text or inflate scenario count.
- LF/CRLF/CR via `normalizeContent`; `~~~`/length-≥3/leading-whitespace fences via existing `buildCodeFenceMask`.
- INFO note appears for a stray delta header but does not change `valid` (including `--strict`).
## Known remaining divergences
Unification closes the reproduced defects; these divergences remain and are accepted:
- **Empty scenarios** — a `#### Scenario:` header with no body counts on the delta path (`countScenarios` counts headers) but not on the spec path (`parseScenarios` keeps only scenarios with content), so `validate <change>` passes what `validate <spec>`/`archive` rejects.
- **Recognition** — bare `### <statement>` headers are requirements on the spec path but skipped on the delta path. Deliberate (see "Why recognition tightening is rejected"); the Part B INFO note surfaces it instead of unifying it.
- **No-space `###Requirement:` headers** — `REQUIREMENT_HEADER_REGEX` (`\s*` after `###`) accepts them on the delta and write paths, but `MarkdownParser.parseSections` requires whitespace (matching GFM, which does not treat `###Requirement:` as a heading). So a no-space requirement validates as a change with zero INFO (the reader accepts it, so the skip note never fires), syncs into the main spec as-is, and the synced spec then fails `validate <spec>` — the same shape as #498. Pre-existing (both regexes unchanged from `main`) and accepted here: the no-space form is a tested normalization case (`requirement-blocks.test.ts`), and tightening the shared regex would change write-path recognition. Closing it should be a separate compatibility change — deprecate no-space headers with an INFO/WARN first, or broaden the skipped-header collection to any `^###` line before tightening recognition.
- **Delta section/block splitting is not fence-aware** — `splitTopLevelSections` and `parseRequirementBlocksFromSection` treat a fenced `## ...` line as a section boundary and a fenced `### Requirement:` line as a new block, while the spec path fence-masks its sectioning. The skipped-header INFO is collected during the actual parse precisely so it reflects these boundaries instead of describing different ones.
## Prior art
`findMainSpecStructureIssues` (`spec-structure.ts`) already flags a `### Requirement:` header *outside* the `## Requirements` section and delta headers inside a main spec. The Part B INFO note is complementary: it flags non-`Requirement:` headers *inside* a delta Requirements section, which that function does not cover.
## Out of scope: #559
Deferred — transcript shows an unqualified `changes/<id>/...` path (missing `openspec/` prefix), not a demonstrated folder-vs-title mismatch.
@@ -0,0 +1,71 @@
## Why
OpenSpec's promise is that the spec is the source of truth, and `validate`/`archive` are the gate that protects it. That gate is undermined by a fragmented requirement-parsing layer: the requirement **reader** is implemented twice — `MarkdownParser.parseRequirements` (used by `validate <spec>` and `archive`) and `Validator.extractRequirementText` + `countScenarios` (used by `validate <change>`) — and the two have drifted apart. Every defect below was reproduced against `main` with the bundled CLI; outputs are quoted in `design.md`.
The two readers differ in ways that are each a reproduced bug:
| | spec reader (`parseRequirements`) | delta reader (`extractRequirementText`/`countScenarios`) |
|---|---|---|
| Body capture | first line only | first line only |
| Skips `**metadata**:` lines | **no** | yes |
| Ignores fenced code in body | **no** | **no** |
| Counts fenced `#### Scenario:` | no (fence-masked) | **yes** |
| `SHALL`/`MUST` predicate | substring `includes('SHALL')` | word-boundary `\b(SHALL\|MUST)\b` |
### Reproduced bugs
- **#361 — wrapped keyword invisible.** Both readers capture only the first body line, so a `SHALL`/`MUST` on line 2 fails both `validate <change>` and `validate <spec>`.
- **#418 — metadata before description, spec path only.** A requirement that opens with `**ID**:`/`**Priority**:` lines passes `validate <change>` (delta reader skips metadata) but fails `validate <spec>` (`req.text` = `**ID**: REQ-FILE-001`).
- **#312 — fenced block before prose corrupts text.** The original count-corruption is already fixed by `codeFenceLineMask`, but the body loop is still fence-unaware: a fenced code block before the `SHALL` line makes `req.text` = `` ```bash `` on both paths today.
- **Fenced scenario counted as real (discovered during hardening, no open issue).** `countScenarios` matches `^####` with a fence-unaware regex, so a requirement whose only `#### Scenario:` lives inside a fenced example passes `validate <change>` — while the same content correctly fails `validate <spec>`. A malformed delta slips through the gate.
- **#498 — validate and archive disagree.** `validate <change>` recognizes requirements only by the canonical `### Requirement:` header; `parseRequirements` treats every level-3 header as a requirement. A stray divider like `### Documentation Requirements` is silently ignored by `validate <change>` but flagged by `archive` (non-blocking phantom warning) and `validate <spec>` (blocking error). The author gets no signal at validate time.
## What Changes
### Part A — unify the reader (fixes #361, #418, #312, fenced-scenario counting)
One shared, fence-/metadata-/multi-line-aware extraction used by **both** readers, so they cannot drift again:
- Requirement-body capture spans every line from after the `### Requirement:` header to the first `#### Scenario:` header found on a **non-fenced** line, skipping fence-masked lines and `**metadata**:` lines; `SHALL`/`MUST` detection runs over the full body.
- Scenario counting ignores fence-masked `####` lines, so fenced examples never count as real scenarios.
- One normative-keyword predicate (`\b(SHALL|MUST)\b`) replaces the substring/word-boundary split.
Part A only corrects what is *detected*. It fixes false negatives (#361/#418/#312) and one false positive (fenced scenario), and does **not** change which headers count as requirements.
### Part B — make the #498 divergence visible (safe, no recognition change)
`validate <change>` emits an **INFO**-level note when an `## ADDED`/`## MODIFIED Requirements` section contains a level-3 header that is not a canonical `### Requirement:` header — i.e. one the delta reader will silently skip. This surfaces the stray-header problem at validate time instead of letting it appear only at archive, **without** changing recognition. INFO never fails validation (not even `--strict`), so no currently-passing change newly fails.
### Rejected: tightening recognition to `### Requirement:` only
The tempting #498 fix — make `parseRequirements` recognize only `### Requirement:` headers — is **rejected**. Bare `### <statement>` headers (e.g. `### The system SHALL …`) are a **supported, widely-tested requirement format**: `test/core/validation.test.ts` asserts a bare-header spec is `valid`, and bare headers appear across `json-converter`, `archive`, and `spec` tests plus the `tmp-init` fixtures. Tightening would reclassify those as non-requirements and break a large swath of the suite (and likely real user specs). Surfacing the divergence (Part B) achieves consistency of *signal* without a breaking change to recognition. See `design.md` for the full analysis.
Out of scope (investigated, deferred): #559 — its transcript shows an unqualified `changes/...` path, not a proven folder-vs-title mismatch.
## Safety: the archive write path is unaffected
`specs-apply` (the archive rebuild) reconstructs specs from raw `### Requirement:` blocks via `extractRequirementsSection` + `RequirementBlock.raw` — it never calls `parseSpec`/`parseRequirements` and never reads `req.text`. Therefore changing the reader (Part A) **cannot alter archived spec content**; it only changes what `validate`/`view`/`show` report. Verified by inspection of `src/core/specs-apply.ts`.
## Existing-test impact
All 15 tests in `test/core/parsers/markdown-parser.test.ts` pass on `main`. Because recognition is unchanged, this proposal updates **one** test: `should extract requirement text from first non-empty content line` (`:331`), which asserts `req.text` is only the first body line — the #361 bug itself; it is updated to expect the full body. The fence tests (`:106`, `:139`) are preserved (skip-and-join keeps `SHALL`-first bodies intact). Bare-header tests (`:258`, `:310`) and `validation.test.ts`/`json-converter.test.ts` are **not** affected, because recognition does not change.
## Capabilities
### New Capabilities
_None._
### Modified Capabilities
- `cli-validate`: requirement-text extraction becomes multi-line, fence-aware, and metadata-aware; scenario counting becomes fence-aware; one normative-keyword predicate; an INFO note surfaces non-`Requirement:` headers in delta sections.
## Impact
- `src/core/parsers/markdown-parser.ts` — shared multi-line/fence/metadata-aware body extraction.
- `src/core/validation/validator.ts` — `extractRequirementText` and `countScenarios` delegate to the shared, fence-aware helpers; INFO note for stray delta headers.
- `src/core/parsers/requirement-blocks.ts` — export the canonical `REQUIREMENT_HEADER_REGEX` for the INFO check.
- `src/core/schemas/base.schema.ts` — schema-level `SHALL`/`MUST` enforcement stays removed after #1280; the imperative validator uses the shared predicate.
- `test/core/parsers/markdown-parser.test.ts:331` updated; regression tests added.
- Read-only blast radius (display only, no write path): `view`/`list` requirement counts and `json-converter`/`spec` JSON `text` reflect the fuller body; `change-parser` delta descriptions built from `req.text` may span multiple lines; the `MAX_REQUIREMENT_TEXT_LENGTH` check is INFO (non-blocking). Requirement **counts** are unchanged (recognition unchanged).
- Fixes #361, #418, #312; surfaces #498. Related: #559 (deferred). Does not claim #1156 (PR #1280). Hardens the reader that #1112/#1246/#1277 rely on.
@@ -0,0 +1,69 @@
## ADDED Requirements
### Requirement: Requirement bodies SHALL be parsed in full for normative keywords
The validator SHALL detect `SHALL`/`MUST` across the entire requirement body, not only the first body line. Requirement-text extraction SHALL capture every body line from after the `### Requirement:` header up to the first Markdown header on a non-fenced line (a `#### Scenario:` header, or a stray `###` divider absorbed into a delta block), skipping blank lines and lines inside fenced code blocks. `**metadata**:` lines SHALL be skipped only when other body text remains; a body consisting solely of metadata lines SHALL be kept as the requirement text. Detection SHALL run over the full captured body. Canonical `### Requirement:` blocks with no body text SHALL NOT satisfy body-keyword validation from the header title alone; they SHALL receive the existing body-keyword hint when the keyword appears only in the header. The Markdown parser MAY still use the header title as display text for supported bare-header specs. The change-delta reader and the main-spec validator SHALL share this body extraction so they cannot diverge.
#### Scenario: Normative keyword on the second wrapped line (change and spec)
- **GIVEN** a requirement whose text wraps across two lines with `SHALL` on the second line
- **WHEN** running `openspec validate <id> --strict` for both a change delta and a main spec
- **THEN** both SHALL detect the keyword and SHALL NOT report a missing-`SHALL`/`MUST` error
#### Scenario: Metadata fields precede the description
- **GIVEN** a requirement whose body begins with `**ID**:`/`**Priority**:` lines before a `MUST` description
- **WHEN** running `openspec validate <spec-id> --strict`
- **THEN** validation SHALL skip the metadata lines, detect `MUST`, and pass — matching `openspec validate <change-id>`
#### Scenario: Requirement written entirely as a metadata line
- **GIVEN** a requirement whose whole body is `**Constraint**: The system MUST ...`
- **WHEN** running `openspec validate <id> --strict` for both a change delta and a main spec
- **THEN** both SHALL keep that line as the requirement text and detect the `MUST`
#### Scenario: Stray divider bounds the requirement body
- **GIVEN** a delta requirement followed by a stray `### Background` divider whose notes contain `MUST`
- **WHEN** running `openspec validate <change-id> --strict`
- **THEN** the requirement body SHALL end at the divider and the `MUST` in the notes SHALL NOT satisfy the keyword check
#### Scenario: Single-line requirement is unaffected
- **GIVEN** a requirement whose `SHALL` statement is on a single body line
- **WHEN** running `openspec validate <id> --strict`
- **THEN** validation behavior, messages, and displayed text SHALL be unchanged from before this change
### Requirement: Fenced code blocks SHALL NOT corrupt extraction or scenario counting
The validator and Markdown parser SHALL ignore lines inside fenced code blocks (` ``` ` or `~~~`) when extracting requirement body text, when locating the body-ending header boundary, and when counting scenarios. A fenced block before the prose line SHALL NOT make the fence marker the requirement text, and a `#### Scenario:` inside a fenced block SHALL NOT count as a real scenario.
#### Scenario: Fenced block before the prose line
- **GIVEN** a requirement whose body opens with a fenced code block containing `#`-comment lines, followed by the `SHALL` prose line
- **WHEN** the spec or change is validated
- **THEN** the captured requirement text SHALL be the prose line (not the fence marker) and validation SHALL pass
#### Scenario: Fenced scenario is not a real scenario
- **GIVEN** a requirement whose only `#### Scenario:` appears inside a fenced code example, with no real scenario
- **WHEN** running `openspec validate <change-id> --strict`
- **THEN** validation SHALL report the requirement as missing a scenario — the same result as `openspec validate <spec-id>`
### Requirement: A single normative-keyword predicate SHALL be used across readers
All `SHALL`/`MUST` detection SHALL use one predicate that matches `SHALL` or `MUST` as whole words (delimited by word boundaries, so a substring inside a longer word such as `MARSHALL` does not match), so the change-delta reader and the schema-based reader accept and reject identical text.
#### Scenario: Keyword detection agrees across readers
- **GIVEN** identical requirement body text validated once as a change delta and once as a main spec
- **WHEN** running `openspec validate` on each
- **THEN** both SHALL reach the same conclusion about whether the body contains a normative keyword
### Requirement: Non-canonical headers in delta sections SHALL be surfaced without changing recognition
When an `## ADDED`/`## MODIFIED Requirements` section in a change delta contains a level-3 header that is not a canonical `### Requirement:` header, `openspec validate <change>` SHALL emit an INFO-level note identifying it, because the delta reader will otherwise skip it silently. The note SHALL be derived from the headers the delta reader actually skips while parsing, so it describes the reader's real section and fence boundaries. This note SHALL NOT change which headers are recognized as requirements, and SHALL NOT change the `valid` result — including under `--strict`. This behavior applies only to change deltas: bare `### <statement>` headers in main specs are recognized requirements (see the scenario below) and SHALL NOT trigger such notes.
#### Scenario: Stray divider header is reported, not silently skipped
- **GIVEN** a delta whose `## ADDED Requirements` section contains `### Documentation Requirements` followed by a valid `### Requirement: …` block
- **WHEN** running `openspec validate <change-id> --strict`
- **THEN** validation SHALL emit an INFO note naming the stray `### Documentation Requirements` header
- **AND** the `valid` result SHALL be unchanged from current behavior (the INFO does not cause failure)
#### Scenario: Nameless requirement header gets a dedicated hint
- **GIVEN** a delta whose `## ADDED Requirements` section contains a bare `### Requirement:` header with no name
- **WHEN** running `openspec validate <change-id>`
- **THEN** the INFO note SHALL say the header is missing a requirement name (not suggest `### Requirement: Requirement:`)
#### Scenario: Bare requirement headers in main specs remain supported
- **GIVEN** a main spec whose requirements use bare `### <statement>` headers without the `Requirement:` prefix
- **WHEN** running `openspec validate <spec-id> --strict`
- **THEN** those headers SHALL continue to be recognized as requirements exactly as before this change
@@ -0,0 +1,43 @@
## 1. Part A — shared, fence-aware extraction (#361, #418, #312, fenced-scenario)
- [x] 1.1 Add a shared `extractRequirementBody(lines, fenceMask, startIndex)` helper in `src/core/parsers/` returning the full body: lines after the header up to the first `#### Scenario:` on a non-fence-masked line, skipping fence-masked and `**metadata**:` lines.
- [x] 1.2 Add a fence-aware scenario counter (count only non-fence-masked `####` headers).
- [x] 1.3 Rewrite `MarkdownParser.parseRequirements` to use the body helper (replacing first-line logic) and consult `codeFenceLineMask`.
- [x] 1.4 Rewrite `Validator.extractRequirementText` to delegate to the body helper, and `countScenarios` to the fence-aware counter.
- [x] 1.5 Run `SHALL`/`MUST` detection over the full body in both paths.
## 2. Part A — single normative-keyword predicate
- [x] 2.1 Use the shared `containsShallOrMust` (`/\b(SHALL|MUST)\b/`) for validator keyword checks; after the #1280 merge, schema-level keyword enforcement remains removed and owned by the imperative validator.
## 3. Part B — surface the #498 divergence (INFO, no recognition change)
- [x] 3.1 Record the non-canonical level-3 headers `parseDeltaSpec` skips while parsing ADDED/MODIFIED sections (`DeltaPlan.skippedHeaders`), so the note reflects the reader's real boundaries.
- [x] 3.2 In `validateChangeDeltaSpecs`, emit an INFO issue for each skipped header. Do **not** change recognition. Special-case a nameless `### Requirement:` header.
- [x] 3.3 Confirm INFO does not affect `valid` under `--strict` (`valid = errors === 0 && warnings === 0`).
## 4. Update the one affected existing test
- [x] 4.1 `markdown-parser.test.ts:331` (*first non-empty content line*) → assert `req.text` is the full joined body. Confirm `:106`/`:139` (fence) and `:258`/`:310` (bare-header) tests still pass unchanged.
## 5. Regression tests
- [x] 5.1 (#361) `SHALL` wrapped onto body line 2 passes `validate <change>` and `validate <spec>`.
- [x] 5.2 (#418) metadata lines before the prose pass `validate <spec>`; delta path stays green.
- [x] 5.3 (#312) fenced block before the prose line captures the real body and passes.
- [x] 5.4 (fenced scenario) a requirement whose only `#### Scenario:` is inside a fence FAILS `validate <change>` (parity with `validate <spec>`).
- [x] 5.5 (#498) a stray `### Documentation Requirements` divider in a delta yields an INFO note from `validate <change>` and does not change `valid` (including `--strict`).
- [x] 5.6 Guard: single-line requirements unchanged; bare-header specs still valid; LF/CRLF covered.
## 6. Release
- [x] 6.1 Add a changeset: Fixes #361, #418, #312; surfaces #498. Note the read-only display changes (fuller `req.text` in JSON/descriptions); no archived-content change.
## 7. Review fixes (PR #1281)
- [x] 7.1 Skip `**metadata**:` lines only when other body text remains; a metadata-only body (e.g. `**Constraint**: The system MUST ...`) is kept as the requirement text.
- [x] 7.2 Keep header-title fallback in the Markdown parser for display/bare-header compatibility, while validator checks use body-only extraction so canonical header-only requirements still receive the #1280 body-keyword hint.
- [x] 7.3 End the body at any non-fenced Markdown header, so a stray `###` divider's notes cannot satisfy the keyword check (old-reader parity).
- [x] 7.4 Replace the standalone INFO scanner with skipped-header collection inside `parseDeltaSpec` (notes match the reader's real boundaries).
- [x] 7.5 Special-case the nameless `### Requirement:` INFO message; document that the any-`####` scenario match is deliberate; un-export `REQUIREMENT_HEADER_REGEX`.
- [x] 7.6 Soften the changeset wording and document the known remaining divergences in `design.md`.
@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-06-29
@@ -0,0 +1,93 @@
# Design
## Context
This is a bug-fix bundle, not a feature. The three issues are grouped because they share one structural defect: a read/validate command forks its own resolution or validation logic instead of reusing the canonical implementation a sibling command already gets right. Fixing them together lets the implementation converge the divergent paths onto shared helpers in one pass, and lets one set of *parity tests* guard all three against future drift.
The unifying invariant this change establishes:
> A command that reports on or validates a change MUST resolve files the same way `openspec status` does, and MUST produce the same requirement-quality messages the change-delta validator does. Divergence is a bug, and parity is asserted by test.
Every claim below was verified against source at the base commit `546224e` **and reproduced empirically against the built (pre-fix) CLI**. The reproductions are summarized under "Empirical evidence." The framings that changed under this review are flagged inline; two of them (the #1202 `apply.tracks` mechanism and the "agrees with status" count claim) were corrections to an earlier draft of this very proposal.
## Root causes (verified)
| # | Symptom | Canonical path (correct) | Divergent path (bug) | Anchor |
|---|---------|--------------------------|----------------------|--------|
| #1182 | `validate <change>` → `Unknown item`; `--all` → "No items found" | `status`/`instructions` resolve by **directory existence** (`validateChangeExists`) | `validate` resolves via `getActiveChangeIds`, which **requires `proposal.md`** | `src/commands/validate.ts:97,120,238`; `src/utils/item-discovery.ts:11-16`; `src/commands/workflow/shared.ts:168-170`; scaffolder omits proposal.md `src/utils/change-utils.ts:121-210` |
| #1182b | resolved nested-layout change → "No delta sections found" | spec-driven specs glob is `specs/**/*.md` | `validateChangeDeltaSpecs` discovers deltas one level deep only | `src/core/validation/validator.ts:115-138,265` |
| #1202 | `view` shows a tasked change as `Draft`; `archive` archives an unfinished change | `status` tests the tasks **artifact's `generates` glob** via `resolveArtifactOutputs` | `getTaskProgressForChange` hardcodes `changes/<name>/tasks.md` | `src/utils/task-progress.ts:28`; callers `src/core/view.ts:100`, `src/core/list.ts:112`, `src/core/archive.ts:342,540`; 2nd copy `src/commands/change.ts:111,164`; helper `src/core/artifact-graph/outputs.ts:17`; tracks type `src/core/artifact-graph/types.ts:18` |
| #1156 | Main-spec SHALL-in-header-only → generic error; delta → targeted hint | Delta validator runs `containsShallOrMust` + `buildMissingShallOrMustMessage` | Main-spec requirement validation falls through to generic `REQUIREMENT_NO_SHALL`; header is discarded before validation | `src/core/validation/validator.ts:167-189,443-463`; `src/core/schemas/base.schema.ts:11-14`; `src/core/parsers/markdown-parser.ts:220-226` |
## Empirical evidence (built pre-fix CLI, fresh `init`'d projects)
- **#1182:** `new change foo` writes `changes/foo/.openspec.yaml` only. `status --change foo` resolves (exit 0); `validate foo` → `Unknown item 'foo'` (exit 1); `validate --all` with foo as the sole change → `No items found to validate` (**exit 0**, a silent CI failure). Writing `proposal.md` flips `validate` to resolve — confirming the exact lever. A valid two-level `specs/<area>/<cap>/spec.md` change → `No deltas found` (#1182b); one-level control validates clean.
- **#1202:** project-local schema with tasks `generates: "**/tasks.md"`; change `foo` = `backend/tasks.md` (2/2) + `frontend/tasks.md` (1/3) = 3/5, no top-level file. `status` → `4/4 artifacts complete, isComplete:true` (file existence, not checkboxes); `view` → **Draft**; `list` → `No tasks`; `list --json` → `totalTasks:0`. `archive foo --skip-specs --no-validate --yes` **moved the unfinished change into `changes/archive/`** — the incomplete-task gate was wholly bypassed. Baselines (default schema top-level `tasks.md`; bare project) classify correctly and are preserved by the fix.
- **#1156:** main spec, SHALL in header only → generic `Requirement must contain SHALL or MUST keyword`. The same mistake as an ADDED/MODIFIED delta → the targeted hint. RENAMED delta → no error (no body). No-keyword-anywhere main spec → generic error. Lowercase `shall` → error on both paths. **Header-only with no body line at all → reported VALID today** (parser keeps `text` = header, which contains SHALL).
## Decisions
### Decision 1 — Converge, don't re-implement
Each fix points the divergent path at the *existing* canonical implementation rather than writing a second copy. A second copy is what created every one of these bugs.
### Decision 2 — #1182: the lever is the membership gate, not "workspace homes"
The original framing (validate doesn't understand workspace planning homes) is wrong at HEAD: planning homes are repo-only (`PlanningHomeKind = 'repo'`), the workspace feature is now **stores**, and `validate` already accepts `--store` and resolves the store root through the same `resolveRootForCommand` as `status`. The actual, reproducible divergence is that `validate` gates change membership on `proposal.md` (`getActiveChangeIds`) at **three** sites — targeted (validate.ts:120), bulk (validate.ts:238), and the interactive "pick one" selector (validate.ts:97) — while `status`/`instructions` gate on directory existence (`validateChangeExists`). Since `createChange` writes `.openspec.yaml` but not `proposal.md`, any scaffolded or still-authoring change resolves everywhere except `validate`.
The fix: `validate` resolves a change by directory existence within the already-resolved root, at all three sites. This is store-correct for free (the store root is resolved identically by all three commands), so the reported store/workspace symptom is covered transitively, and no store-specific scenario is needed. `getChangeDir`/`resolveCurrentPlanningHomeSync` are **not** the lever (the former is a pure path join with no membership decision).
Two boundaries confirmed empirically and held out of scope: (a) the **spec** side is correct — `getSpecIds` requires `spec.md`, and `spec show` agrees, so a spec dir without `spec.md` is correctly "not found"; no spec-side scenario is added. (b) The deprecated noun-form `openspec change validate <name>` already resolves a passed name by directory existence (change.ts:215) but is cwd-based (cannot reach a `--store` root) and its JSON mode does not set a non-zero exit on invalid — pre-existing noun-form defects, explicitly not addressed here.
### Decision 3 — #1182: nested delta discovery is in scope
Resolution success is not validation success. `validateChangeDeltaSpecs` discovers deltas exactly one directory deep (`changeDir/specs/<dir>/spec.md`), but the multi-area layout that motivates stores/workspaces is `changeDir/specs/<area>/<capability>/spec.md`. Without recursing, a resolved multi-area change reports "No delta sections found" (reproduced). So delta discovery is extended to the nested layout in this change; otherwise the #1182 fix does not actually let the reported change validate.
### Decision 4 — #1202: resolve via the tracked artifact's `generates` glob, and parity is resolution-only
The fix lands in the shared helper `getTaskProgressForChange`, correcting all four call sites at once; the spec pins two consumers explicitly (`cli-view` — the filed Draft symptom; `cli-archive` — the incomplete-task gate, a data-safety regression that lets an unfinished change archive). `openspec list` is corrected by the same helper; the independent second copy in `openspec change list` (`change.ts:111,164`, its own `countTasks`) is folded onto the shared helper by a task — not left as an orphan.
Two corrections to an earlier draft, both load-bearing:
- **`apply.tracks` is a filename that *selects* the artifact; it is not the glob.** `apply.tracks` is typed `string | null` and is consumed elsewhere as a literal path (`path.join(changeDir, tracks)` + `existsSync`), so `apply.tracks: "**/tasks.md"` cannot match nested files. The glob `status` actually uses is the tracked artifact's **`generates`**, resolved by `resolveArtifactOutputs(changeDir, artifact.generates)`. So the fix identifies the tracked-tasks artifact (the artifact whose `generates` equals `apply.tracks`, falling back to artifact id `tasks` when no `apply` block is present), then counts checkboxes across `resolveArtifactOutputs(changeDir, thatArtifact.generates)`. `resolveArtifactOutputs` roots `fast-glob` at the change directory (so a sibling `changes/archive/` or another change's `tasks.md` cannot match) and de-dups via a `Set` (so no double counting).
- **`status` checks file *existence*, not checkbox completion.** Empirically `status` calls a 3/5 change `4/4 complete, isComplete:true`. So the parity established here is **resolution-mechanism parity** (`view`/`archive` resolve the same set of files `status` resolves), not count parity — `view`/`archive` additionally count checkboxes. Any "view/archive task counts equal status" claim is false and is removed from the spec.
The signature gains `projectRoot` (needed to resolve project-local schemas via `resolveSchema`); all four call sites plus the two `change.ts` sites can derive it. `resolveSchema` **throws** on an unresolvable/misnamed schema, whereas the current helper never throws — so the helper MUST catch and fall back to single-file `tasks.md`, or `view`/`list`/`archive` would crash on a project whose config names a deleted schema. This fallback is specified and tested.
### Decision 5 — #1156: recover the header, remove the refine, pin an exact (not byte-identical) message
The targeted delta hint works because the delta parser keeps the requirement header (`RequirementBlock.name`) separate from the body. The **main-spec parser overwrites the header with the first body line** (`markdown-parser.ts:220-226`) before validation, so the Zod refine that emits `REQUIREMENT_NO_SHALL` never sees the header and cannot detect "keyword in header only."
The fix:
1. **Recover the header.** Reuse the header-preserving parser `src/core/parsers/requirement-blocks.ts` (`extractRequirementsSection`, which yields header+body pairs and is the same source the delta path trusts) and run the existing `containsShallOrMust` + `buildMissingShallOrMustMessage` detection in the imperative main-spec rules (`applySpecRules`, validator.ts:290-329), which already loops requirements and has the raw content.
2. **Remove the Zod refine, don't merely relax it.** Change deltas do **not** use the refine — they validate imperatively in `validateChangeDeltaSpecs` (proven: a no-keyword delta emits the imperative `must contain SHALL or MUST` base string, not the Zod `REQUIREMENT_NO_SHALL` string). So the refine is exercised only on the main-spec path. Once the imperative rule in `applySpecRules` owns **both** sub-cases — keyword-in-header-only → targeted hint, and keyword-nowhere → generic message — the `.refine` on `RequirementSchema` (base.schema.ts:11-14) is **removed entirely**. Keeping a conditional refine "for the no-keyword case only" risks double-emission on the header-only case, which the "exactly one issue" scenario forbids.
3. **Message.** The actionable sentence is byte-identical to the delta path; the prefix differs (main specs have no `ADDED`/`MODIFIED`). The main-spec message is: `Requirement "<name>" must contain SHALL or MUST in the requirement body, not only in the header. Move the SHALL/MUST statement to the line immediately after the "### Requirement: ..." header.` Generalize `buildMissingShallOrMustMessage` to accept the prefix so the actionable sentence lives in one place and cannot drift between paths. Lowercase is rejected via the shared `\b(SHALL|MUST)\b` regex (converging the main-spec path off the case-sensitive Zod `.includes`).
RENAMED requirements carry no body and are not subject to the hint (the `'ADDED' | 'MODIFIED'` action set is correct); a scenario pins this so it is not mistaken for a gap.
### Decision 6 — Additive coverage, with one intended behavior change called out
The fixes are additive coverage: changes that already have `proposal.md`, single-file `tasks.md` projects, projects with no resolvable schema, and delta-spec validation produce byte-identical output before and after. One main-spec case is an **intended** behavior change, not an unchanged case: a requirement with the keyword in the header and **no body line at all** is reported valid today and becomes a body-keyword hint under header recovery (the delta path already errors on this case). This is called out explicitly so it is not discovered as an accidental regression; every other previously-passing case is unchanged.
### Decision 7 — Parity is the test strategy
Tests assert *agreement*, not just fixed outputs in isolation:
- a change that `status --change <name>` resolves (including a proposal-less and a store change) is also resolved by `validate <name>`, included by `validate --all`, and listed by the interactive selector; a resolved-but-invalid change exits non-zero;
- for a schema whose tracked-tasks `generates` is `**/tasks.md`, `view`, `list`, and the `archive` gate resolve the **same set of files** `status` resolves (and additionally count checkboxes consistently with each other);
- a requirement with SHALL/MUST in the header only yields the same actionable sentence whether it appears in `openspec/specs/**` or a change delta, emitted exactly once.
Parity assertions fail loudly if a future refactor re-forks any path.
### Decision 8 — Scope boundary against sibling proposals
- #1112 (delta header absent from base passing `validate`, aborting at `archive`) is an *authoring* false-positive resolved by the deterministic `sync --check` gate in the sync/unarchive proposal — out of scope here.
- Artifact *completeness* gaps (a half-written or skipped artifact reported as done, #1084/#1260) belong to the artifact-graph/update-workflow proposal — out of scope here. #1202 is narrower: *where* task counts are read from, not whether the tasks are complete.
## Risks and mitigations
- **Risk:** relaxing the change membership gate changes ambiguity behavior when a name exists as both a change directory and a spec. **Mitigation:** preserve the existing ambiguity/`--type` semantics; only swap the change-membership predicate (proposal.md → directory existence) at all three sites, keeping `getSpecIds` as the spec predicate. Covered by an ambiguity scenario.
- **Risk:** the task-progress signature change breaks the other call sites, or crashes on an unresolvable schema. **Mitigation:** update all six sites (four helper callers + two `change.ts` copies) in the same change; catch `resolveSchema` failure and fall back to single-file `tasks.md`; assert `view`/`archive` resolve the same files as `status`.
- **Risk:** the glob over-matches or the archive gate regresses. **Mitigation:** reuse `resolveArtifactOutputs` (rooted at the change dir, de-duped); add scope-containment and archive-gate scenarios.
- **Risk:** removing the Zod refine drops the no-keyword error on the main-spec path. **Mitigation:** the imperative `applySpecRules` rule must own the no-keyword case before the refine is removed; assert the no-keyword regression and single emission. Delta validation is untouched (it never used the refine).
@@ -0,0 +1,56 @@
## Why
Three commands silently give a wrong or incomplete answer about the spec source of truth, because sibling read/validate paths reimplement narrower logic than the canonical path each should share.
- `openspec validate <change>` rejects a change as `Unknown item` whenever `proposal.md` is absent (a scaffolded or still-authoring change, in a repo or a store), though `status`/`instructions` resolve it by directory existence — so spec checks are skipped for the changes most likely to be malformed (#1182).
- `openspec view` labels a fully-tasked change `Draft` when its tasks live in nested/glob `tasks.md` files, contradicting `status`; the same blind spot lets `archive` silently archive an unfinished change (#1202).
- `openspec validate` gives the targeted "move SHALL/MUST onto the body line" hint for deltas, but only the generic message for the same mistake in a main spec (#1156).
Each is deterministic and fixed by converging a divergent path onto the canonical one.
## Background: one root cause, three commands
OpenSpec sells one promise — the specs are the source of truth and the CLI tells you the truth about them. These three bugs break that promise the same way: a command that *reads* or *validates* state quietly forks its own resolution logic instead of reusing the canonical implementation a sibling command already gets right. The fork is invisible until the two paths disagree, and then the tool reports a confident falsehood (`Unknown item`, `Draft`, a clean archive of an unfinished change, a worse error message) with no signal that anything diverged.
This proposal was hardened by tracing each path to source (anchors in `design.md`). Two framings changed during that review and are called out so reviewers can check them:
- **#1182 is a membership-gate bug, not a "home" bug.** Planning homes are repo-only today (`PlanningHomeKind = 'repo'`); the "managed workspace planning home" from the 1.4.1 issue is the feature since renamed **stores**, and `validate` already accepts `--store`. The real divergence is narrower and reproducible at HEAD: `status`/`instructions` resolve a change by **directory existence** (`validateChangeExists`), while `validate` resolves it through `getActiveChangeIds`, which **requires `proposal.md`**. `createChange` does not write `proposal.md`, so a scaffolded change — including a store change still being authored — resolves everywhere except `validate`. Sharing the canonical resolution covers the reported store/workspace symptom transitively, because the store root is already resolved identically by all three commands.
- **#1202 is wider than `view`.** The buggy helper `getTaskProgressForChange` is consumed by `view`, `list`, and the `archive` incomplete-task gate. The `archive` case is a correctness/data-safety risk, not a cosmetic mislabel: under a glob-tasks schema it reads zero tasks, finds nothing incomplete, and archives a change whose work is not done. A second, independent hardcoded copy lives in `openspec change list`.
## What Changes
- **`validate` shares the canonical change-resolution rule (#1182).** `openspec validate <change>` resolves a change by directory existence — the same rule `status`/`instructions` use — instead of requiring `proposal.md`. This applies to targeted `validate <name>`, bulk `validate --all`/`--changes`, **and** the interactive "pick one" selector, within both the repo root and a `--store`-selected root. Spec/change ambiguity handling and `--type` overrides are preserved. Delta discovery is extended to the nested `specs/<area>/<capability>/spec.md` layout so a resolved multi-area change actually validates its deltas instead of reporting "no deltas found."
- **`view`/`archive`/`list` resolve tasks through the tracked-tasks artifact glob (#1202).** Task progress for a change is resolved through the tracked-tasks artifact's `generates` glob — the same file-resolution `status` uses — counting every matching `tasks.md` scoped to the change directory, with the single-file `tasks.md` and no-resolvable-schema cases preserved as today. (The tracked artifact is selected via `apply.tracks`, which is a filename, not a glob; the glob is that artifact's `generates`.) As a result `view`'s Draft/Active/Completed classification stops being blind to nested files, and `archive`'s incomplete-task gate no longer passes an unfinished glob-tasks change. The second hardcoded copy in `openspec change list` is folded onto the same shared resolution. Because `status` checks task-file *existence* (not checkboxes), the guarantee is that these commands resolve the *same files* `status` resolves — not that they reproduce a count `status` does not compute.
- **The SHALL/MUST body-keyword hint applies to main specs (#1156).** A main-spec requirement whose normative keyword sits only in the `### Requirement:` header receives the same targeted "move it to the body line" remediation as a change delta, instead of the generic message — emitted exactly once (no duplicate generic error), across every main-spec surface (`validate <spec>`, `--all`, JSON, `spec validate`, and rebuilt-spec validation).
### What this deliberately does *not* change
- The canonical paths (`status`, `instructions`, the delta-spec validator) are not changed in behavior — the divergent paths are moved onto them.
- No new command, flag, schema field, or output format. Existing JSON shapes are preserved; only the values they carry become correct.
- Resolution for changes that already have `proposal.md`, single-file `tasks.md` projects, projects with no resolvable schema, and delta-spec validation are byte-for-byte unchanged — these fixes only add coverage where a path was previously blind.
- It does not address the #1112 authoring false-positive (a delta MODIFIED/REMOVED header absent from the base spec passing `validate`, aborting at `archive`); that is handled by the deterministic `sync --check` gate in the separate sync/unarchive proposal. The overlap is intentionally avoided.
- It does not change artifact *completeness* semantics (whether a half-written artifact counts as done, #1084/#1260); #1202 here is strictly about *where* task counts are read from, not whether the tasks are complete.
## Capabilities
### Modified Capabilities
- `cli-validate`: resolves a change by directory existence (matching `status`/`instructions`) for targeted, bulk, and interactive-selector validation in repo and store roots; discovers deltas under nested `specs/**` layouts; and emits the targeted SHALL/MUST body-keyword hint for main specs, once, across all surfaces.
- `cli-view`: resolves task progress through the tracked-tasks artifact's `generates` glob (the same file-resolution `status` uses), so Draft/Active/Completed classification stops being blind to nested `tasks.md` files.
- `cli-archive`: the incomplete-task gate reads task progress through the same tracked-tasks resolution, so a glob-tasks change with unfinished work cannot pass the gate.
## Impact
- **Affected specs:** `cli-validate` (2 added requirements), `cli-view` (1 added requirement), `cli-archive` (1 added requirement).
- **Affected code (implementation follow-up, not in this planning PR):**
- `src/commands/validate.ts` — replace the `getActiveChangeIds` membership gate with directory-existence resolution mirroring `validateChangeExists` (`src/commands/workflow/shared.ts:168-170`) at all three sites: targeted (line 120), bulk (line 238), interactive selector (line 97). Reconcile with `getSpecIds` for the change/spec ambiguity path (leave `getSpecIds` unchanged — it is correct). Sibling `src/commands/show.ts:81,115,121` shares the gate and should be folded in or explicitly scoped out; the deprecated noun-form `change validate` is out of scope.
- `src/core/validation/validator.ts` — extend delta discovery (`validateChangeDeltaSpecs`, lines 115-138) to recurse the nested `specs/<area>/<capability>/spec.md` layout.
- `src/utils/task-progress.ts` — `getTaskProgressForChange` gains a `projectRoot` param, identifies the tracked-tasks artifact (artifact whose `generates` equals the schema `apply.tracks`, fallback id `tasks`), counts checkboxes across `resolveArtifactOutputs(changeDir, artifact.generates)` (`src/core/artifact-graph/outputs.ts:17`, de-duped, change-rooted). `apply.tracks` selects the artifact; the glob is its `generates`. Catch `resolveSchema` failure → fall back to single-file `tasks.md` (never throw). Update all four call sites (`src/core/view.ts:100`, `src/core/list.ts:112`, `src/core/archive.ts:342`, `:540`) for the new arg; fold the second copy in `src/commands/change.ts:111,164` onto the helper.
- `src/core/validation/validator.ts` + `src/core/parsers/requirement-blocks.ts` — recover the requirement header (lost at `markdown-parser.ts:220-226`) via `extractRequirementsSection` so the main-spec rule in `applySpecRules` can detect "keyword in header only" and emit the targeted hint via a prefix-generalized `buildMissingShallOrMustMessage` (lines 443-463); **remove** the Zod refine (`src/core/schemas/base.schema.ts:11-14`) once the imperative rule owns both the header-only and no-keyword cases (deltas validate imperatively and never used the refine, so removal cannot regress them).
- **Risk:** low-to-moderate. Each fix points a command at logic that already exists for the canonical path; the larger surface is the task-progress signature change (six sites incl. schema-failure fallback) and the validator header recovery. Regression risk is bounded by parity tests asserting `validate`/`view`/`archive`/the main-spec validator agree with their canonical counterparts, plus explicit no-regression scenarios for the unchanged cases.
## Issues addressed
- [#1182](https://github.com/Fission-AI/OpenSpec/issues/1182) — `openspec validate` cannot resolve a change that `status`/`instructions` resolve (reported for a managed workspace/store home; root cause is the `proposal.md` membership gate).
- [#1202](https://github.com/Fission-AI/OpenSpec/issues/1202) — `openspec view` does not detect nested/glob `tasks.md`, classifying complete changes as `Draft` (and the same helper silently weakens the `archive` incomplete-task gate).
- [#1156](https://github.com/Fission-AI/OpenSpec/issues/1156) — the 1.4.0 SHALL/MUST body-keyword hint applies to change deltas but not main specs.
@@ -0,0 +1,32 @@
## ADDED Requirements
### Requirement: Archive incomplete-task gate SHALL use the tracked-tasks artifact glob
`openspec archive`'s incomplete-task gate — the check that prevents archiving a change whose tasks are not all complete — SHALL read task progress through the change's tracked-tasks artifact glob, the same file-resolution `openspec status` and `openspec view` use, rather than a fixed `changes/<name>/tasks.md` path. The tracked-tasks artifact SHALL be identified as the artifact whose `generates` equals the schema's `apply.tracks` value, falling back to the artifact with id `tasks` when no `apply` block is present; checkbox counts SHALL be aggregated across every file matched by that artifact's `generates` glob, scoped to the change directory. When the schema cannot be resolved or no tracked-tasks artifact is found, the gate SHALL fall back to a single top-level `tasks.md` exactly as today and SHALL NOT crash. This closes the data-safety gap where a change whose tasks live in nested/glob `tasks.md` files is read as having zero tasks, no incomplete work, and is allowed to archive while unfinished.
#### Scenario: Glob-tasks change with unfinished work cannot archive
- **GIVEN** a schema whose tasks artifact `generates` is `**/tasks.md`
- **AND** a change with `backend/tasks.md` containing unchecked tasks and no top-level `tasks.md`
- **WHEN** running `openspec archive` on that change
- **THEN** the incomplete-task gate SHALL detect the unfinished tasks and block (or require explicit override of) the archive
- **AND** SHALL NOT treat the change as having zero tasks
#### Scenario: Archive gate resolves the same tracked files as view
- **GIVEN** any change with a tracked-tasks glob
- **WHEN** the `archive` incomplete-task gate and `openspec view` each compute task progress for that change
- **THEN** they SHALL resolve the same set of `tasks.md` files and count the same checkboxes
#### Scenario: Unresolvable schema falls back without error
- **GIVEN** a change whose configured schema cannot be resolved
- **WHEN** running `openspec archive` on that change
- **THEN** the incomplete-task gate SHALL fall back to a single top-level `tasks.md`
- **AND** SHALL NOT crash
#### Scenario: Single top-level tasks file archiving is unchanged
- **GIVEN** a change with a single top-level `changes/<name>/tasks.md`, or a project with no resolvable schema
- **WHEN** running `openspec archive`
- **THEN** the incomplete-task gate SHALL behave exactly as today
@@ -0,0 +1,113 @@
## ADDED Requirements
### Requirement: Validate SHALL resolve changes by directory existence, matching status
`openspec validate` SHALL resolve whether a named item is a change using the same rule `openspec status` and `openspec instructions` use — directory existence within the resolved root — rather than requiring a `proposal.md` to be present. This SHALL apply to targeted validation (`openspec validate <name>`), bulk validation (`openspec validate --all` / `--changes`), and the interactive "pick one" selector shown when no item is given in a TTY — within both the repository root and a `--store`-selected root. A resolved change with a nested multi-area spec layout SHALL have its deltas discovered and validated. Spec/change ambiguity handling and `--type` overrides SHALL remain unchanged. The spec-resolution side (a spec is resolved by the presence of its `spec.md`) is correct today and SHALL be left unchanged.
#### Scenario: Scaffolded change without proposal.md
- **GIVEN** a change directory created by `openspec new change <name>` that has not yet had `proposal.md` written
- **WHEN** executing `openspec validate <name>`
- **THEN** validate resolves the change and validates it
- **AND** it SHALL NOT print `Unknown item '<name>'`
#### Scenario: Targeted-resolution parity with status
- **GIVEN** any change that `openspec status --change <name>` resolves, including a change in a `--store`-selected root
- **WHEN** executing `openspec validate <name>` (passing the same `--store` when applicable)
- **THEN** validate SHALL resolve the same change that status resolved, and SHALL NOT report it as unknown
#### Scenario: Bulk validation includes a sole proposal-less change
- **GIVEN** a repository whose only active change lacks `proposal.md` and is listed by `openspec status`
- **WHEN** executing `openspec validate --all` (or `--changes`)
- **THEN** validate SHALL validate that change, and SHALL NOT print "No items found to validate"
- **AND** the exit status SHALL reflect the change's validity
#### Scenario: Interactive selector lists proposal-less changes
- **GIVEN** a TTY and a change directory without `proposal.md` that `openspec status` lists
- **WHEN** executing `openspec validate` with no item name
- **THEN** the interactive "pick one" selector SHALL include that change
#### Scenario: Resolved-but-invalid change exits non-zero
- **GIVEN** a change that resolves by directory existence but fails validation
- **WHEN** executing `openspec validate <name>` or `openspec validate --all`
- **THEN** validate SHALL exit with a non-zero status
- **AND** SHALL NOT exit 0 while reporting the change as having issues
#### Scenario: Nested multi-area delta discovery
- **GIVEN** a resolved change whose deltas live at `specs/<area>/<capability>/spec.md` (nested deeper than one directory)
- **WHEN** validating that change
- **THEN** validate SHALL discover and validate those delta specs
- **AND** SHALL NOT report "No delta sections found" for a change that does contain deltas
#### Scenario: Change/spec ambiguity is preserved
- **GIVEN** a name that exists both as a change directory and as a spec
- **WHEN** executing `openspec validate <name>`
- **THEN** validate SHALL print the ambiguity error and respect `--type change` / `--type spec`, exactly as before
#### Scenario: Changes with proposal.md are unaffected
- **GIVEN** a change that already contains `proposal.md`
- **WHEN** validating it targeted or in bulk
- **THEN** resolution and validation behavior SHALL be byte-for-byte unchanged from today
### Requirement: SHALL/MUST body-keyword hint SHALL apply to main specs
When a requirement places the normative keyword (SHALL or MUST) only in its `### Requirement:` header and omits it from the requirement body line, `openspec validate` SHALL emit the same targeted remediation guidance for main specs under `openspec/specs/**` as it already does for change delta specs, instead of the generic "must contain SHALL or MUST" message. The targeted message SHALL be emitted exactly once for such a requirement, the generic `REQUIREMENT_NO_SHALL` message SHALL no longer be emitted on the main-spec path, and the behavior SHALL be uniform across every main-spec validation surface (`openspec validate <spec>`, `--all`, JSON output, `openspec spec validate`, and rebuilt-spec validation via `validateSpecContent`). The main-spec message's actionable sentence SHALL be byte-identical to the change-delta message; only the leading prefix differs (main specs have no `ADDED`/`MODIFIED` action).
#### Scenario: Main spec with the keyword in the header only
- **GIVEN** a main spec requirement whose header contains SHALL or MUST but whose body line omits it
- **WHEN** running `openspec validate` over that spec
- **THEN** the error message SHALL contain the actionable sentence: "must contain SHALL or MUST in the requirement body, not only in the header. Move the SHALL/MUST statement to the line immediately after the \"### Requirement: ...\" header."
- **AND** SHALL NOT be the generic "Requirement must contain SHALL or MUST keyword" message
#### Scenario: Actionable-sentence parity with change deltas
- **GIVEN** the identical header-only-keyword mistake authored once in a main spec and once in a change delta
- **WHEN** validating each
- **THEN** the actionable remediation sentence SHALL be byte-identical between the two (the change-delta `ADDED`/`MODIFIED` prefix is not required for the main-spec message)
#### Scenario: Exactly one issue is emitted
- **GIVEN** a main spec requirement with the keyword in the header only
- **WHEN** validating it
- **THEN** validate SHALL emit exactly one issue for the missing body keyword
- **AND** SHALL NOT emit both the generic message and the targeted message for the same requirement
#### Scenario: Requirement missing the keyword entirely still errors
- **GIVEN** a main spec requirement that contains no SHALL or MUST in either the header or the body
- **WHEN** running `openspec validate` over that spec
- **THEN** validate SHALL report that the requirement must contain SHALL or MUST, as it does today
#### Scenario: Keyword present in the body is not flagged
- **GIVEN** a main spec requirement whose body line contains SHALL or MUST (whether or not the header also does)
- **WHEN** running `openspec validate` over that spec
- **THEN** validate SHALL NOT raise a missing-keyword error for that requirement
#### Scenario: Lowercase keyword does not satisfy the body requirement
- **GIVEN** a main spec requirement whose only "shall"/"must" is lowercase
- **WHEN** running `openspec validate` over that spec
- **THEN** validate SHALL report a missing-keyword error, matching the change-delta behavior for the same lowercase mistake
#### Scenario: Header keyword with no body line emits the hint
- **GIVEN** a main spec requirement whose header contains SHALL or MUST and that has no body line before its first scenario
- **WHEN** running `openspec validate` over that spec
- **THEN** validate SHALL emit the body-keyword hint (the keyword is only in the header)
- **AND** this case, which is reported valid today, becomes a deliberate, additive validation improvement
#### Scenario: Renamed requirements are not subject to the body-keyword hint
- **GIVEN** a change delta `## RENAMED Requirements` whose TO header contains SHALL or MUST
- **WHEN** validating that change
- **THEN** validate SHALL NOT emit the body-keyword hint for the renamed pair
- **AND** RENAMED validation behavior SHALL be byte-for-byte unchanged
@@ -0,0 +1,58 @@
## ADDED Requirements
### Requirement: Task progress SHALL be resolved through the tracked-tasks artifact glob
`openspec view` SHALL determine a change's task progress by resolving its tracked-tasks artifact and counting checkboxes across that artifact's output glob (`generates`) — the same file-resolution `openspec status` uses to detect the tasks artifact — rather than assuming a fixed `changes/<name>/tasks.md` path. The tracked-tasks artifact SHALL be identified as the artifact whose `generates` equals the schema's `apply.tracks` value, falling back to the artifact with id `tasks` when no `apply` block is present. (`apply.tracks` is a filename that selects the artifact; the glob is that artifact's `generates`.) Resolution SHALL be scoped to the change directory, SHALL aggregate completed and total checkbox counts across every matching file, and SHALL NOT double-count. When the schema cannot be resolved, no tracked-tasks artifact is found, or the glob matches no file, `view` SHALL fall back to counting a single top-level `tasks.md` exactly as today, and SHALL NOT raise an error.
Note on scope: `openspec status` detects whether the tasks artifact *file exists*; it does not count checkboxes (a change whose nested `tasks.md` files exist is reported by `status` as having the tasks artifact complete even when boxes are unchecked). The parity established here is therefore **resolution-mechanism parity** — `view` resolves the same set of `tasks.md` files `status` resolves — and `view` additionally counts checkboxes within them. The fix removes `view`'s blindness to nested files; it does not make `view` agree with a task count `status` does not produce.
#### Scenario: Nested tasks files under a glob schema
- **GIVEN** a schema whose tasks artifact `generates` is `**/tasks.md`
- **AND** a change with `backend/tasks.md` and `frontend/tasks.md` and no top-level `tasks.md`
- **WHEN** running `openspec view`
- **THEN** the change SHALL show aggregated task progress summed across both files
- **AND** SHALL NOT be classified as a Draft change solely because no top-level `tasks.md` exists
#### Scenario: Tracked-tasks files resolve the same as status
- **GIVEN** a schema whose tasks artifact `generates` is `**/tasks.md`
- **WHEN** running `openspec view` and `openspec status --change <name>`
- **THEN** both SHALL resolve the same set of `tasks.md` files for the change — `status` to detect the tasks artifact, `view` to count checkboxes within them
#### Scenario: Files exist but tasks unchecked are not Completed
- **GIVEN** a glob-tasks change whose matched `tasks.md` files contain unchecked boxes
- **WHEN** running `openspec view`
- **THEN** the change SHALL be classified Active (not Completed), even though `status` reports the tasks artifact as present
#### Scenario: Tracked-tasks artifact identified by apply.tracks, not a fixed id
- **GIVEN** a custom schema whose tracked-tasks artifact is not named `tasks` but is selected by `apply.tracks`
- **WHEN** running `openspec view`
- **THEN** task progress SHALL be resolved from that artifact's `generates` glob
#### Scenario: Resolution stays scoped to the change directory
- **WHEN** resolving a change's `tasks.md` files
- **THEN** matching SHALL be rooted at `changes/<name>/` only
- **AND** SHALL NOT count `tasks.md` files belonging to another change or under `changes/archive/`
#### Scenario: Unresolvable schema falls back without error
- **GIVEN** a change whose configured schema cannot be resolved (for example, the config names a missing schema)
- **WHEN** running `openspec view`
- **THEN** task progress SHALL fall back to counting a single top-level `tasks.md`
- **AND** `view` SHALL NOT crash
#### Scenario: Single top-level tasks file is unchanged
- **GIVEN** a change with exactly one top-level `changes/<name>/tasks.md`, or a project with no resolvable schema
- **WHEN** running `openspec view`
- **THEN** task progress SHALL be counted from that single file exactly as before
#### Scenario: A change with no tasks anywhere stays Draft
- **GIVEN** a change with no `tasks.md` matching the tracked-tasks glob
- **WHEN** running `openspec view`
- **THEN** the change SHALL report zero tasks and be classified as Draft, as today

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