Compare commits

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

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

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

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

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

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

---------

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

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

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

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

Closes #1959

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

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

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

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

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

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

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

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

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

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

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

---------

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

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

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

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

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

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

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

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

* fix(verify): preserve unavailable task evidence

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

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

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

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

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

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

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

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

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

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

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

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

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

---------

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

---------

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

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

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

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

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

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

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

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

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

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

---------

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

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

Closes #1895

AI-assisted (Grok)

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

Addresses both review findings on the copy fallback.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

---------

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

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

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

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

* fix(update): tighten glob gap write guardrails

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

* fix(update): harden glob gap creation guidance

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

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

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

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

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

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

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

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

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

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

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

---------

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

Closes #1952

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

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

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

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

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

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

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

---------

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

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

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

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

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

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

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

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

Test fixes:

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

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

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

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

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

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

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

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

---------

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

Closes #1897

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

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

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

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

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

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

markdownlint MD038. Changeset text only; no behaviour change.

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

---------

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

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

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

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

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

---------

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

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

Closes #1844.

* docs(community): update spec-guard link

---------

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

* test(docs): guard command troubleshooting guidance

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

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

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

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

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

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

---------

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

* fix(onboard): preserve implementation choice

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

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


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

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

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

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

* chore(nix): update pnpm dependency hash

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

---------

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

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


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

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

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

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

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

* fix(website): await llms index generation

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

---------

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

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

* fix(init): clarify Codex desktop skill usage

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

---------

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

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

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

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

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

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

* fix(validate): clarify scenario balance diagnostic

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

---------

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

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

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

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

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

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

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

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

---------

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

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

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

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

---------

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

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

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

* test(copilot): pair discovery claims

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

* fix(update): preserve failures after declined migrations

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


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

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

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

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

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

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

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

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

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

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

Closes #1138

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

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

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

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

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

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

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

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

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

Guards added:

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

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

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

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

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

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

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

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

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

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

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

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

---------

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

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

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

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

Closes #1221

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

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

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

Two routing fixes fall out of stating the rule:

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

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

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

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

Routing fixes:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

---------

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

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

Two changes, both in the generated instructions:

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

Closes #1645

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Drops the legacy docs/troubleshooting.md addition.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

---------

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

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

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

Closes #1761

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

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

Hardening pass on the #1761 fix.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

---------

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

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

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

Closes #869

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

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

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

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

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

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

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

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

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

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

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

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

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

---------

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

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

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

Closes #1827

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

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

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

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

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

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

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

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

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

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

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

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

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

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

---------

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

---------

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

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

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

Closes #1734

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

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

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

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

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

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

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

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

So resolve all of them:

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

Two supporting changes:

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

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

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

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

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

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

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

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

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

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

Caught by CodeRabbit on #1775.

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

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

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

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

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

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

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

What #1735 had that this did not:

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

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

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

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

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

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

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

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

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

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

---------

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

---------

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

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

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

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

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

---------

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

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

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

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

Closes #1836

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

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

Adversarial review of the first commit found three gaps.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

---------

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

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

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

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

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

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

---------

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

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

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

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

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

Closes #1222
Closes #1264

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

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

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

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

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

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

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

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

Hardening pass over the two fixes in this branch.

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

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

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

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

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

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

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

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

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

* fix(workflows): preserve explicitly retired missing specs

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

---------

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

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

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

Closes #1828

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

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

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

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

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

Four hardening findings from review of the first pass:

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

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

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

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

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

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

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

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

---------

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

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

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

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

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

---------

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

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

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

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

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

---------

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

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

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

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

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

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

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

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

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

Closes #1873

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

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

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

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

---------

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

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

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

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

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

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

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

---------

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

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

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

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

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

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

---------

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

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

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

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

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

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

---------

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

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

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

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

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

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

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

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

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

---------

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

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

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

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

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

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

---------

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

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

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

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

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

A change whose specs/ held full future-state specs (no ADDED/MODIFIED/REMOVED/RENAMED section) lost its What Changes deltas: show --json reported none, change list counted zero, and archive printed an extra No deltas found warning. The prose fallback now applies whenever no spec file carries a delta section, which is exactly main's behavior for such changes, while a bullet-form REMOVED (which has a REMOVED section) is still read from the delta file.

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

---------

Co-authored-by: Clay Good <hi@claygood.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 15:47:22 +00:00
Dwin Gharibi 4b5c07a0c2 fix(parser): drop closing hashes from requirement names (#1860)
CommonMark lets an ATX heading end in a closing run of `#`s, so
`### Requirement: Late Fees ###` renders as `Late Fees`. Requirement
names kept the run, so a REMOVED written that way missed the requirement
and archive exited 0 with a false "treating it as already removed"
warning, while a closed MODIFIED or RENAMED heading failed as not found.

normalizeRequirementName now strips the closing run with the rule scenario
names already use: only a run preceded by a space or tab counts, so `C#`
keeps its `#`. Every reader goes through it, and the main-spec duplicate
check now does too, so a closed and an open heading of one requirement are
reported as duplicates.
2026-09-16 15:47:19 +00:00
Dwin Gharibi 767d63c926 fix(archive): refuse requirement names differing only in case (#1864)
ADDED and the RENAMED target compared requirement names exactly, while
REMOVED and the RENAMED source already treated a name that differs only
in case or interior whitespace as a mistyped header. An ADDED `late fees`
beside an existing `Late Fees`, or a rename to `LATE FEES`, therefore
archived cleanly and left two contradicting copies of one requirement in
the main spec, which validate then accepted.

Both now refuse with an error naming the existing requirement. The source
of a rename is exempt from the target check, so a case-only rename of a
requirement to its own name still applies, and ADDED is still checked
against the spec as it stands after the earlier operations, so a variant
of a requirement the same delta removes or renames away is allowed.
2026-09-16 15:47:17 +00:00
6e62b1d522 fix(parser): refuse malformed RENAMED pairs (#1806)
* fix(parser): refuse malformed RENAMED pairs

* docs(test): document RENAMED pairing test helpers

* test(parser): pin RENAMED pairing across header copies and bullet markers

Cover the two shapes main gained after this branch was cut: a FROM in one
`## RENAMED Requirements` copy and a TO in another are both reported as
unpaired, and unpaired lines written with `*` or `+` are reported like
`-` ones. Add a patch changeset matching the other parser fixes.

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

---------

Co-authored-by: Clay Good <hi@claygood.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 15:47:15 +00:00
Clay GoodandClaude Opus 5 e571b5b9ae fix(security): harden CLI against hostile-repository inputs and clear dependency advisories (#1835)
* fix(deps): upgrade vitest to 4.1.11 and override fflate

Clears GHSA-82fw-gwwq-j7x9 (path traversal / arbitrary file read via
@vitest/mocker redirect mock), the subject of all three open Dependabot
alerts. No patched 3.x exists — 4.1.11 is the first fixed release — so the
major bump is unavoidable.

Two things the plain Dependabot bump (#1823) got wrong, which is why its
tests failed on every platform:

- It left @vitest/ui on 3.x, which dragged vite/esbuild to 0.28.2 and broke
  the allowBuilds pin assertion in pnpm-workspace-config.test.ts. Upgrading
  @vitest/ui in lockstep keeps esbuild on 0.28.1.
- Vitest 4 no longer lets an arrow function stand in as a constructor
  implementation, so the ZshInstaller module mock threw "is not a
  constructor" across 8 completion tests. Converted the three mock factories
  to function expressions.

Also adds a pnpm override for fflate (GHSA-px8p-9vwx-vf98, infinite loop on
malformed ZIP64), which @vitest/ui 4.1.11 still pulls at 0.8.2.

`pnpm audit` is now clean: 0 vulnerabilities across all severities.

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

* fix(core): bound schema size and stop prototype keys shadowing worksets

Two low-severity robustness defects found during the security review.

Workset lookups tested membership with `state.worksets[name] !== undefined`
on a plain-prototype object. `constructor`, `toString`, `valueof` and
friends are all valid kebab ids, so `openspec workset add constructor`
reported "already exists" against empty state, `getWorkset` returned a
function off the prototype, and `withoutWorkset` took the found branch for a
workset that was never there. All three sites now use `hasOwnProperty`.
This was never prototype *pollution* — nothing is written through these
keys and Zod's `z.record` drops `__proto__` — only a correctness defect.

`SchemaYamlSchema.artifacts` was unbounded while `validateNoCycles` walks it
with a recursive DFS, so a project-local schema declaring a long `requires`
chain crashed the CLI with an uncaught `RangeError: Maximum call stack size
exceeded` instead of a validation error. Capped at 1000 artifacts, which
also bounds the reference-resolution and graph work.

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

* fix(security): stop repo-supplied text forging agent instructions

OpenSpec prints a pseudo-XML envelope that an AI coding agent consumes as
instructions, and interpolated repo content went in raw. The tags carry
authority - <project_context> means "background only", <task> means "do
this" - so a value that closes its own block is promoted from data to
directive.

Confirmed against a fresh build: a config.yaml `context:` value containing
`</project_context><system_override priority="critical">` landed a top-level
override block outside every "do NOT treat as instructions" guard. The same
breakout worked from `rules`, `description`, a dependency description, and a
schema `instruction`. A change directory name containing a quote forged
attributes on the <artifact> tag. In markdown output, a `context` line
starting with `##` forged a peer of the printer's own headings.

src/core/references.ts already had sanitizeInline written for exactly this
threat, documented as such, and simply was not applied here - it also only
flattened newlines, which one line of markup is enough to defeat. Extended it
and added three siblings beside it: escapeEnvelopeText, escapeEnvelopeAttribute,
and escapeEnvelopeCloseTags for content that must stay verbatim.

Template bodies deliberately get only their closing tags neutralized: the
shipped templates are full of `<!-- ... -->` comments and <placeholder>
markers that are copied into the generated artifact, so blanket escaping
would write &lt;!-- into every file. A block can only end at a closing tag,
so that is the load-bearing control.

Rules and operation guidance are flattened but explicitly not truncated -
they are instructions an agent must follow in full.

Separately, `openspec update` decided skill freshness from the generatedBy:
line alone and never compared bodies, so appending a step to a generated
SKILL.md still printed "All 1 tool(s) up to date". Skills now get the same
byte-comparison command files already had, and the plan names the reason.

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

* fix(security): remove super-linear scans and tighten write containment

Three regexes ran over whole repository files with a `\s` class that crosses
newlines under the `m` flag, so `^\s*` re-scanned from every line start. The
blowup is in the *failing* scan - a file with no `generatedBy:` line at all -
which is also the realistic attack file. Measured on this machine:

  extractGeneratedByVersion, 63 KB whitespace SKILL.md   3,103 ms -> <5 ms
  legacy-skill compare, 63 KB whitespace frontmatter     8,131 ms -> <5 ms
  buildUpdatedSpec, 195 KB of `<!--` openers             6,817 ms -> ~90 ms

The first two are reached by `openspec update`, the first command run after
cloning. The third is reached from extractPurposeSection during `openspec
archive`, on the write path.

The scans now use `[ \t]` and walk lines, and maskHtmlComments is an indexOf
scan that visits each character once instead of a lazy regex that re-scans to
EOF from every `<!--`. Both rewrites were fuzzed against the originals -
200,000 random inputs each, 0 mismatches - so the `--!>` terminator and the
"unterminated comment runs to EOF" rule from #1413 are preserved exactly.

resolveTrustedSpecPath treated a failed containment check as permission to
re-root trust on the symlink's own target, on the theory that monorepo
symlinks may be intentional. A repo shipping openspec/specs/<cap> as a link
out of the tree therefore got `openspec archive` to write attacker-controlled
markdown to <external>/spec.md while printing the in-project path. The
fallback root must now still be inside the project, matching retireSpec,
which already refused to delete an external target.

Also: `validate <id> --type spec|change` short-circuited the name guard that
`show` applies, so a traversing id reached a bare path.join; and markTipSeen
wrote the global config through a predictable <config>.<pid>.tmp at default
0644 instead of the repo's existing writeFileAtomically (random name, 0600).

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

* fix(security): harden subprocess and shell-config write paths

Defense in depth. The audit confirmed there is no shell injection anywhere in
src/ - no `shell: true`, no user value concatenated into a command line - so
none of these are live exploits; they are the sharp edges next to that line.

Completion install wrote the completions directory into .bashrc/.zshrc inside
double quotes, so a `$(...)` or backtick in HOME/XDG_DATA_HOME became command
execution on every future shell start. Both installers now single-quote the
path through a shared helper.

`feedback` shelled out for two probes (`which gh`, `gh auth status`) directly
alongside free-form user title and body text - the most plausible site for a
future injection regression. Both are execFileSync now, behavior unchanged.

Git subprocesses inherited the default 1 MB maxBuffer with no timeout, so a
large dirty tree made `git status --porcelain` throw ENOBUFS, which gitProbe's
bare catch turned into "no git facts" - `openspec doctor` then silently stopped
reporting uncommitted changes. They now share GIT_EXEC_OPTIONS (15s timeout,
16 MB buffer) the way readCliVersion already did, and the catch distinguishes
a resource failure from "git absent" so the degraded path is no longer silent.

The GITHUB_OUTPUT heredoc in validate-changesets used a fixed EOF delimiter
over a list of PR-authored paths; it is now run-unique.

Finally, both package.json files still carried a `pnpm` block. pnpm 10 uses
that block *instead of* pnpm-workspace.yaml rather than merging with it, which
is exactly the override-displacement trap dependabot.yml documents as #1812 -
and it is where Dependabot writes when it bumps an overridden package. The
block only duplicated `allowBuilds`, so removing it leaves both lockfiles
byte-identical with every advisory override intact, and denies Dependabot the
block to write into. The workspace test now asserts `pnpm` is absent entirely.

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

* fix(security): pin the update check to TLS and honor telemetry opt-out

The update check asked whatever `npm_config_registry` named, over any
protocol. A code comment asserted that file contents deliberately cannot
choose the destination; that was not true. npm exports every config source it
reads, including a `registry=` line in a repository-local .npmrc that travels
with a clone - reproduced: `registry=http://169.254.169.254/` came straight
through `npm run`.

That is a cleartext GET at an address of the repository's choosing, and it
escalates. The attacker's reply says `{"version":"99.0.0"}`, which triggers
the upgrade prompt whose default is Yes; accepting runs `npm install -g`,
which npm resolves against that same attacker registry. Cloning a repository
and answering one prompt installs an attacker-chosen global package.

Both halves are now closed. The registry override is honored only over https,
falling back to the public registry otherwise, and canSelfUpgrade() refuses
when the resolved registry is not the public origin - a private mirror can
still inform the check but can never drive an install prompt. Redirects must
stay https and on the origin resolved up front, not merely the previous hop,
so no single reply can steer the request elsewhere. The comment now describes
what the code actually guarantees.

Separately, the opt-out env vars were exact-string matches, so DO_NOT_TRACK=true
and OPENSPEC_TELEMETRY=false both silently left telemetry ON - the spellings a
user is most likely to reach for, and inconsistent with the tolerant
isCiEnvironment() helper beside them. Parsing is now tolerant, shared between
both call sites, and fails safe: an unparseable value suppresses the request.

The first --json run also sent an event before the disclosure was ever shown -
the notice is correctly deferred so it cannot corrupt machine-readable output,
but trackCommand fired regardless, and agent-driven --json may be a user's only
mode. No event is sent and no anonymous id is created until the notice has
actually been printed.

No existing guard was weakened: the 256 KB body cap, 3-redirect cap, single
budget timer, strict version regex, argv-based spawn, CI/test guards and the
four-field payload are untouched.

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

* fix(security): make the close-tag escape linear

CodeQL js/polynomial-redos on the escape added in a563b0591 - and it is the
same defect class this PR set out to remove, in the fix for it.

`/<\/[A-Za-z][^>]*>/g` scans for a closing `>` from every `</`, so a template
of `</A` repeated is quadratic. Measured before: 20 KB 274 ms, 40 KB 1,223 ms,
80 KB 3,917 ms. Reachable, because escapeEnvelopeCloseTags is applied to
`template`, which is repo-controlled.

Rewritten to rewrite the `</` opener alone. The escape only ever swapped the
`<`, so for a well-formed tag the output is byte-identical - verified across
200,000 fuzzed inputs, with zero cases where the new form escapes fewer
closers than the old. It needs no scan at all (2 MB in 35 ms) and additionally
catches a closer whose `>` never arrives.

The other regexes added by this PR were re-checked the same way and are all
linear.

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

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

The flake pins a fixed-output hash over pnpm-lock.yaml, so it goes stale on
any lockfile change - here the vitest 4.1.11 upgrade and the fflate override.
Hash taken from the Nix Flake Validation job, which builds specifically to
report the correct one.

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

* fix(completions): quote the completions dir in printed instructions too

Two review findings.

CodeRabbit caught that only the auto-configured rc block was quoted. When
auto-configuration is off or fails, the installer prints the same lines for
the user to paste into their own rc file - and those were still interpolated
raw, so an expansion in HOME/XDG_DATA_HOME runs on every future shell start
exactly as it would have from the written block. The zsh fpath line was worse
than the bash one: not even double-quoted, so an ordinary space broke it.
Both now go through the same shellSingleQuote helper, with coverage that
exercises the auto-config-disabled path.

Windows CI also failed on a test of this PR's own: it created a change
directory literally named `x"  IGNORE-PREVIOUS  y="`, and Windows forbids `"`
in a filename. The end-to-end vector therefore does not exist on Windows, so
that case is skipped there and the escape itself is now unit-tested on every
platform.

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

* fix(security): narrow envelope escaping to the tags that carry authority

The first pass escaped every `&`, `<` and `>` in repo-supplied text. A
regression review found that mangles ordinary content for every user - and
worse, OpenSpec's own shipped spec-driven schema, which writes
`### Requirement: <name>`, `specs/<capability-path>/spec.md` and
`openspec show "<spec-id>"` on eleven lines. Agents were reading OpenSpec's
own format guidance as `### Requirement: &lt;name&gt;`. Ordinary `context:`
values suffered the same way: `R&D`, `pnpm build && pnpm test`,
`Result<T, E>`, `2> api.log`.

Only a fixed vocabulary is neutralized now - the tags the printer actually
uses to frame its blocks - in both their opening and closing forms, with
attributes. That is the entire breakout surface: a block ends at its own
closing tag, and a forged opener only carries authority if it names one of
these. Everything else reaches the agent exactly as written. Verified by
rendering the real spec-driven instructions: no entity encoding anywhere.

Escaping both forms is also stronger than the first pass in one respect - it
neutralizes a forged `<task priority="highest">` opener, which the earlier
close-tag-only rule for templates let through.

Markdown heading escaping is dropped entirely. It fired inside fenced code
blocks, so a `# install deps` in a project's context became `\# install deps`
for everyone, and it defended a markdown surface with no envelope to break out
of. Guidance entries are still flattened, so the one-line forgery is still
blocked; a multi-line `context:` can add a heading inside its own labelled
block, which is an accepted limit now recorded in the test.

sanitizeInline goes back to flattening only, so JSON output stops
entity-encoding spec Purpose lines.

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

* fix(security): repair four regressions the hardening introduced

A regression review of this PR found four ways the fixes broke legitimate
behavior. All are confirmed and reproduced.

**validate rejected every nested spec id.** The new name guard sits in
validateByType, which is the funnel for three entry paths, not just --type.
Nested capabilities (specs/<area>/<capability>/spec.md, #1353) have ids
containing `/`, so `openspec validate platform/session-layout` started
failing - including the exact command `validate --specs` prints as its own
hint. The guard now runs per path segment, so `..` and backslashes are still
refused while nested ids pass.

**git writes could wedge a user's repository.** GIT_EXEC_OPTIONS was applied
to `init`, `add`, `commit` and the rollback `rm --cached`, with
killSignal SIGKILL. git traps SIGTERM to remove .git/index.lock on its way
out; a signal it cannot catch leaves the lock behind, so every later git
command in the store fails with "Another git process seems to be running" -
including the best-effort unstage, which runs in exactly that case. 15s was
also too short for a signed commit waiting on pinentry. Writes now have their
own bounds: no hard kill, 120s.

**A private registry silently became the public one.** Rejecting a non-https
registry fell back to registry.npmjs.org, which sends the request an internal
mirror deliberately avoided and reports a version resolved against a registry
the eventual `npm install -g` does not use. A rejected registry now disables
the check instead, and isDefaultRegistry reads the raw env var so it still
disqualifies a self-upgrade.

**Redirects were pinned to one origin**, which killed the mirror and corporate
front-end case the redirect support exists for. Cross-host is allowed again;
leaving TLS is not.

Also: an external capability symlink is no longer refused. Two places in this
codebase document such links as intentional monorepo layout, so refusing them
broke a supported setup. The real defect was silence - the CLI reported the
in-project path while writing elsewhere - so the write proceeds and names its
actual destination.

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

* test: make the new security tests portable and discriminating

A test-quality audit of this PR's own tests.

The ReDoS bounds passed on reverted code far too easily - discrimination was
only 1.9x, 3.0x and 2.6x, so a full revert could slip through on a fast
machine. These scans are quadratic, so the hostile inputs are now large enough
to separate the two decisively: 32x, 32x and 10.6x, with the fixed code still
running in milliseconds against a 500-800ms bound. Comments that cited
invented pre-fix timings are replaced with measured figures or a plain
statement of the complexity.

git-probe-limits wrote 6000 files with 245-character basenames, putting the
absolute path past MAX_PATH on a windows-latest runner - and it sits in
beforeAll, so the whole file would have died there. 120-character names x
12000 files keeps porcelain output over the 1 MB threshold at ~190-character
paths. This is the same class of defect as the Windows failure already fixed
in this PR.

validate.name-guard built its fixture at process.cwd(), which is not
gitignored; a security test should not leave files in the working tree.

The worksets test looped over three names but only `constructor` is actually
on Object.prototype and a legal id, so two thirds of it passed unchanged on
main. `__proto__` is not reachable - isKebabId rejects underscores - and both
facts are now stated rather than papered over.

Adds the missing coverage for the git timeout half of the exec hardening,
against synthetic error shapes rather than a 15-second sleep, including the
negative cases that keep the classifier honest.

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

* test: write the git probe fixture without exhausting file descriptors

Sizing the fixture up to 12000 files to keep porcelain output over 1 MB made
the writes EMFILE on the macOS and Windows runners - 12000 concurrent
fs.writeFile handles is well past their descriptor limit, and the failure took
the whole beforeAll with it. Written one at a time instead; the hook still
finishes in about a second.

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

* docs(instructions): correct a comment that claimed a dropped heading guard

The operation-inputs printer never escaped leading '#'; that escaping was
removed because it fired inside fenced code. The comment still described it.

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

* fix(archive): name the capability in the external-link warning

The warning printed 'spec.md' for every external capability, so the
link could not be identified. Report the capability directory, and assert
the full platform-specific completion lines in the bash and zsh fallback
instruction tests.

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

* fix(security): escape envelope tags split by line-break whitespace

ENVELOPE_TAG only accepted a space or tab between the tag name and `>`,
so a multiline repo value such as `</project_context\n>` closed the
context block and could forge a top-level `<task>`. Use `\s`; the
`[^<>]` tail keeps the match linear. Adds regressions for split closing
and opening tags and a timing guard on multiline openers.

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

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 15:47:12 +00:00
Clay GoodandClaude Opus 5 3b8e5b6616 fix(nix): install shell completions with the flake package (#1785)
* fix(nix): install shell completions with the flake package

The flake exposed `openspec completion generate SHELL` but installed no
completion scripts, so a Nix install had no completions at the standard
locations. Generate the Bash, Fish, and Zsh scripts during postInstall and
place them with installShellCompletion.

Closes #1740

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

* docs: note that cross-compiled Nix builds omit completions

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

* ci: run the Nix job when the completion generator changes

The Nix build now runs `openspec completion generate`, so a change under
src/core/completions can break packaging without touching flake.nix.

Also drops the cross-compilation caveat from the Nix install docs: every
package this flake exposes is native (`legacyPackages.<system>` has
buildPlatform == hostPlatform), so `canExecute` is always true and the
completions are never omitted.

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

* docs: move the Nix completions note to the canonical pages

docs-lab/README.md makes docs-lab/ canonical and the old docs/ tree legacy, so
the fact is recorded in the two docs-lab pages that own it and docs/cli.md and
docs/installation.md are back to their state on main.

- docs-lab/start/installation.md, Nix: the package ships the scripts at the
  standard locations, so completion install is not needed.
- docs-lab/reference/cli.md, openspec completion: the same exception, stated
  where a reader looking up the command will hit it, linking to the Nix
  section rather than repeating the paths.

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

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 15:47:11 +00:00
Clay GoodandClaude Opus 5 7de24044ef fix(init): make the universal tool target findable in the picker (#1778)
* fix(init): make the universal tool target findable in the picker

Closes #653

`openspec init`'s tool picker is a searchable list of product names. The
vendor-neutral target every unlisted assistant is meant to use was named
"Shared .agents skills" — after the directory it writes, which is not a
word anyone in that position searches for. Typing "universal", "other" or
"generic" returned "No matches", so the escape hatch was unreachable and
the reporter had to open an issue to find it.

Rename the entry to "Other / Universal (shared .agents skills)" and give
choices optional `searchAliases` the filter also matches. The picker also
dropped every non-alphanumeric keystroke: readline reports punctuation
only in `key.sequence`, leaving `key.name` undefined, so ".agents" and
"amazon-q" could not be typed at all.

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

* fix(init): point at the universal target when a tool search matches nothing

"No matches" is where someone whose assistant is not on the list gives
up — the picker knows the answer and does not say it. Add an optional
`emptyHint` to the searchable multi-select, and have init name the
vendor-neutral entry there.

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

* fix(init): close every dead end that hides the universal tool target

Hardening pass over the same defect. Reviewing the first fix turned up
four more places the answer was withheld:

- `openspec update`'s tool picker builds its own choices and never
  passed searchAliases through, so the same search failed there.
- `--tools <unknown>` printed a bare list of ids. It now names the
  fallback, the scripted counterpart of the picker's empty hint. The
  hint I first put on validateTools sat on an unreachable branch; the
  path users actually hit is the "Invalid tool(s)" parse error, and a
  test now pins it.
- The search box dropped pasted text as well as punctuation. Any
  sequence whose characters are all printable is now accepted, which
  also lets a space reach the box so "claude code" filters. Escape
  sequences carry control characters and are still rejected, and the
  `name` fallback stays single-character so readline names like 'tab'
  are never typed.
- docs-lab still taught the old label in two places.

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

* docs: keep the FAQ a router and finish the alias list

- help/faq.md is a one-liner surface (README's "FAQ is one-liners" rule),
  so the answer points at the support matrix's Other / Universal section
  instead of restating the picker's search terms. Drops the em dash that
  writing.md forbids.
- reference/supported-tools.md keeps the search terms, now all nine the
  picker actually matches: `vendor-neutral` and `agents.md` were added to
  searchAliases after the first draft of this page. Same correction in the
  legacy docs/supported-tools.md paragraph.

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

* chore(init): keep the tool-not-listed hint ASCII

The non-interactive fallback hint is new terminal output and carried an em
dash, an ambiguous-width glyph in the class #983 covered. A colon reads the
same and cannot misalign a terminal.

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

* docs: drop the legacy docs/ tool-matrix edit

docs-lab/reference/supported-tools.md already carries the label and search terms.

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

* test(init): prove picker punctuation through real readline key events

The prompt tests fed a multi-character sequence that readline never emits:
it splits a paste into one key per character, so a pasted space still
toggles. Drive the handler with real emitKeypressEvents output (fails on
main with 'amazonq'), pin the space limit, and stop claiming multi-word
paste in the changeset and code comment.

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

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 15:47:10 +00:00
Clay GoodandClaude Opus 5 8b99c07bd0 fix(status): name the command that resumes a change (#1786)
* fix(status): name the command that resumes a change

`openspec status` printed the artifact checklist and stopped. The command
that moves the change forward was computed already and shipped in the JSON
`nextSteps` sentence, but the text surface never rendered it, so anyone
resuming a change had to know the next command by heart.

Extract `resolveNextStep` so the command and the published sentence come
from one place, and print it as a `Next:` line — matching the idiom
`openspec new change` already uses to hand off to `openspec status`.

The completion case matters most: "All planning artifacts complete!" reads
as "you are done" even while implementation tasks remain, and it is now
followed by the `openspec instructions apply` command that resumes the work.

Closes #906

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

* style(status): keep the loadStatus comment on loadStatus

The store-flag note landed between the "single definition" comment and the
declaration it describes.

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

* test(status): cover the next-step line, and record it in the spec

The behavior change shipped without the OpenSpec change this repo requires
of user-facing work, and without coverage for the paths a reviewer would
reasonably ask about.

Adds `add-status-next-step`, whose delta modifies `Next Artifact Discovery`
in `cli-artifact-workflow` - the requirement that already says status is how
you learn what comes next. It now also says status names the command.

Coverage added:
- Unit tests for `resolveNextStep`, pinning the published `nextSteps`
  sentences verbatim. Confirmed byte-identical to main's output, so the
  split into command + sentence provably did not reword the contract.
- Custom-schema case: the line is built from the resolved artifact id, so a
  project with neither proposal/specs/design/tasks still gets a usable
  command.
- Skipped artifacts are never named - they satisfy dependents but must not
  be created.
- `--json` stays parseable and carries no `Next:` line.
- `--all` gives every change its own line, and a failed entry none.

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

* test(status): assert the next-step line closes the output

The spec delta says the text output ends with the `Next:` line, but the
assertions used toContain, which a later line would still satisfy. Compare
the last non-empty line instead, across all four ready/complete cases and
the skipped one.

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

* test(status): read the closing line CRLF-safely

Split on /\r?\n/ so a CRLF stream cannot leave a carriage return attached
to the line under comparison, and route the parity check through the same
helper instead of its own scan.

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

* docs: move the status Next: line to the canonical CLI page

docs-lab/README.md makes docs-lab/ canonical and the old docs/ tree legacy, so
the entry now lives under 'openspec status' in docs-lab/reference/cli.md and
docs/cli.md is back to its state on main.

Both documented outputs were captured from real runs rather than written by
hand: the blocked case in a change with proposal and specs, and the complete
case with all four artifacts.

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

* docs(status): drop em dashes from the changeset and proposal

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

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 15:47:08 +00:00
Clay GoodandClaude Opus 5 09a999bbb2 fix(changes): report a change nested in a namespace folder (#1849)
* fix(changes): report a change nested in a namespace folder

Specs can be nested by domain (`specs/mobile/tutorial-videos/spec.md`), so
laying changes out the same way looks reasonable - but a change is only ever a
directory directly under `changes/`. `changes/mobile/refresh-token/` left the
real change invisible to every command while `mobile`, the folder around it,
was reported as an ordinary task-less change. Nothing said so, and
`openspec archive mobile` moved the unfinished change into the archive under
the namespace's name with none of its deltas applied.

A directory under `changes/` is now recognised as a namespace folder when it
carries no change-root artifact of its own and wraps a directory that does.
The probe is deliberately conservative: a miss behaves exactly as before, and
an ordinary change - including a scaffolded one with no artifacts yet, and one
whose only content is a root delta spec - is never reported as a folder.

- `openspec list` marks it `not a change` and explains the flat-only layout
  instead of printing `No tasks`; `--json` gains an additive `warnings` entry.
- `openspec show` and `openspec status --change` say the same rather than
  reporting a change that has no proposal yet.
- `openspec validate` reports the nesting instead of "must have at least one
  delta", with next steps that name the fix rather than delta authoring.
- `openspec archive` refuses it outright - burying an active change is data
  loss, not something to warn about.

Closes #1846.

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

* docs(agent-contract): document the list --json nesting warning

Agents read 4.1 as the shape contract, so the additive `warnings` array and
per-entry `nested` field have to appear there, along with the instruction not
to treat a flagged entry as a change.

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

* fix(changes): harden the nested-change probe and cover status --all

Adversarial review of the first commit found three holes.

A custom schema may generate every root artifact into a subdirectory
(`generates: rfc/proposal.md`). Created by hand, such a change carries no
`.openspec.yaml`, so the marker probe read it as a namespace folder wrapping a
change called `rfc` - a legitimate change that validated clean before, now
refused by validate, status, instructions and archive, with no flag to get
past it.

The same probe missed the case it was written for whenever the nested change
started from its delta specs rather than a proposal: `mobile/refresh-token/`
holding only `specs/auth/spec.md` still archived as `mobile`, deltas dropped.
A nested change is always hand-made - `openspec new change` rejects a name with
a separator - so "mkdir the tree, write the deltas first" is a common way to
arrive here.

Both come from asking one question. A directory is now recognised as a change
by a root artifact OR a populated `specs/`, and a directory holding any file of
its own is never a namespace folder. The `specs/**/*.md` shape is fixed by the
delta format rather than by the schema, which is what makes it safe to lean on.

`change show archive` also reached the probe - it has no reserved-name guard -
and offered to rename every dated archive entry into an active change. The
reserved name is rejected in the probe itself, so no caller can repeat it.

`status --all` read each directory straight through `loadStatus`, bypassing the
lookup guard, and printed a whole artifact plan for work that is not there. It
now carries the same per-change diagnostic a malformed change does, and the
sweep continues past it.

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

* chore(changeset): state the nesting-detection bound

CodeRabbit asked for the three-level limit in the release note. Stated as a
closing sentence rather than a qualifier on the headline, so the note still
leads with what changed for users.

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

* fix(changes): trust the resolved schema before calling a folder a namespace

A hand-made change under a custom schema that generates into a subdirectory
(rfc/proposal.md), with no .openspec.yaml and no delta specs yet, was read as a
namespace folder and refused by status, show, validate, instructions and
archive. The probe now also counts a file at any path the resolved schema
generates, the same check status uses to mark an artifact done.

Move the flat-change-folder docs from legacy docs/ to docs-lab reference/cli.md.

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

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 15:47:06 +00:00
Clay GoodandClaude Opus 5 09984b8242 fix(validate): warn when tracked tasks have no checkboxes (#1774)
* fix(validate): warn when tracked tasks have no checkboxes

Progress counts checkboxes and nothing else, so a tasks.md written as
plain bullets or a numbered list is worse than an empty one: `openspec
list` and `openspec status` report "No tasks", and `openspec archive`
has no incomplete task to warn about. The file reads as finished to the
tool and unfinished to a human.

`openspec validate` now warns when every task file the change's schema
tracks contains list items but not one checkbox, pointing at the first
offending line. Reported per change, not per file, so a checklist
alongside a prose file stays silent, and only files an artifact actually
declares are linted - a bare tasks.md no schema tracks is left alone.

Closes #354

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

* fix(validate): honor fence delimiter length and width

CommonMark closes a fence only on the same character, a run at least as
long as the opener's, and no info string. Comparing the first character
alone let an inner ``` end an outer ```` block, exposing the bullets of
a nested code sample as a task list. The delimiter pattern also loses
its end anchor: `.` does not match `\r`, so an anchored info-string
group matched nothing in a CRLF file and blinded the scan to fences.

Adds the nested-fence, annotated-closer, tilde/backtick, longer-closer
and CRLF cases, plus an e2e change whose nested task files are all
bullets, asserting both reported paths stay POSIX-separated.

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

* fix(validate): scan rendered content and complete evidence only

Hardening pass over the checkbox warning.

The scan for the offending line now skips YAML front matter and HTML
comment blocks alongside fenced code. A list under `tags:` is metadata
about the file rather than the work it tracks, and a commented-out list
is not work either; each exclusion can only silence a warning, never
drop a real task, which is the opposite trade from the task parser. An
unterminated `---` opener rewinds to the top, because that is a thematic
break and everything below it is still content. Only a comment opening
its own line hides that line, so the template's `## 1. <!-- Task Group
Name -->` heading cannot swallow the checklist beneath it.

A tracked file that exists but cannot be read now withdraws the warning
entirely: "no file here holds a checkbox" is a claim about the whole
tracked set, and the checkboxes may be in exactly the file that would
not open. `validate --archived` stays the surface that reports an
unreadable task file loudly (#205).

The message leads with the consequence rather than an accusation, since
a file may legitimately carry a bulleted note and no tasks yet.

New coverage: every packaged tasks template is asserted checkbox-shaped
(the guard fails if a template loses its boxes), a schema tracking tasks
by artifact id with no `apply` block, the deprecated `change validate`
text output, and an unreadable tracked file.

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

* fix(validate): match CommonMark on fence indent and front matter

Two block-scanning defects, both of which hid list items.

A fence indented four spaces is an indented code block, not an opener.
Accepting it left the scan inside a block that never began, so every
list below it went unseen. Fence recognition now stops at three spaces.

`----` is a thematic break, not a YAML front-matter delimiter. Matching
three-or-more dashes let one open a block that swallowed the list under
it until the next `---`. Front matter is now exactly three dashes.

Two test defects alongside them. The deprecated-command test claimed to
assert the reported line, but the text renderer prints no line for any
issue; it now asserts the level and path prefix that surface actually
emits, with the line left to the JSON assertion that already covers it.
The unreadable-file fixture would have passed for the wrong reason had
the mode not taken, since the checkbox it hides would have silenced the
warning by itself; the read failure is now asserted first.

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

* fix(validate): name task files relative to a canonical change dir

Windows CI caught the report naming a task file
`../../../../../../../../runneradmin/AppData/.../tasks.md` instead of
`tasks.md`. `resolveArtifactOutputs` hands back real paths while
`changeDir` carries whatever spelling the caller resolved, and a short
8.3 alias against its expanded form is a difference in spelling, not in
location, so the relative path escaped the change. A symlinked project
directory reproduces it off Windows.

Canonicalizing both sides recovers the relationship. A path that still
escapes falls back to the file name, so no report can leak an absolute
filesystem path. Numbering issues are named through the same helper and
gain the same fix.

The deprecated-command test now asserts the `[WARNING] tasks.md:` prefix
that exposed this, and the Windows job is its regression guard: the
mismatch cannot be staged on POSIX, where the spawned CLI's cwd is
already physical.

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

* fix(validate): keep indented code out of the uncheckboxed-task scan

alfred-openspec on #1774: the scan said it reads rendered content, but
LIST_ITEM began with \s* and so reported top-level four-space-indented code
such as '    - example output' as an uncheckboxed task list. Under --strict
that false positive failed validation on a correct file.

A list-shaped line is now taken only below four visual columns of indent, the
same cut the fence logic already applies, with tabs counting as four. Genuine
nested lists are untouched: this scan reports the first list item it finds and
a nested item always sits under a shallower parent, so the parent is reported
exactly as before. A list-shaped line four columns deep with nothing shallower
above it is not nested under anything, which is what makes it code.

Regressions cover space-indented, tab-indented and numbered code samples, and
pin both the nested-list case (parent still reported) and three-space indent
(not code). Verified the guard bites: removing the column test fails them.

Also moves the documentation to its canonical home. docs-lab/README.md says the
old docs/ tree is legacy and must stay untouched, so the docs/concepts.md line
is dropped and the warning is documented under 'openspec validate' in
docs-lab/reference/cli.md, beside the archive merge findings.

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

* fix(validate): skip BOM-prefixed front matter and cap ordered markers at nine digits

A prose-only task file that opened with a UTF-8 BOM before its front
matter was warned about, because the opener never matched and the
tags list was scanned. A number longer than nine digits followed by a
period also matched as a list item, which CommonMark does not allow.

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

* docs(changeset): drop the em dash

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

* test(tasks): use a multi-character marker for the unrecognised-checkbox case

A single-character marker such as `[~]` becomes a task once #1773 lands,
which would flip this expectation. `[ab]` is not a task under either
parser, so the test keeps asserting that checkbox-looking list items that
count as no task still warn, whichever PR merges first.

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

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 15:47:04 +00:00
Clay GoodandClaude Opus 5 b928165276 docs(explore): stop claiming explore never writes files (#1838)
* docs(explore): stop claiming explore never writes files

Sixteen lines across both documentation trees told users that
`/opsx:explore` creates no artifacts and writes no files, full stop.

That has been false since explore shipped (#467): its capture branch
writes the planning artifacts the user asked for, and can edit an
existing change's artifacts. #1503 later made it scaffold with
`openspec new change` first, closing #668 and #720.

The claim appeared in two shapes. Six lines denied the capability
outright ("Explore creates no artifacts and writes no code"). Ten more
said the same thing as a timing claim ("before any artifact exists"),
which reads as ordinary pitch copy and is what escaped the first pass.

Every site now carries one guarantee, worded the same way: explore
never writes code, and writes nothing else unless you ask, or say yes
when it offers. Four sites described only the user-initiated trigger,
which left the offer path - the one a reader actually hits - looking
like it did not exist.

docs/explore.md and docs/commands.md also gain a positive description
of capture where the denial used to sit, including what scaffolding
creates beyond the artifacts you named, and how capture differs from
handing off to propose (propose writes the set your schema requires;
capture writes only what you named).

Closes #1833

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

* test(docs): keep the retired explore wording retired

A flat list of the phrasings that actually carried the claim, swept
over the eleven pages that pitch explore. Fails on main with all
sixteen offenders; clean on this branch.

Modeled on test/vocabulary-sweep.test.ts, and deliberately a list
rather than a grammar. An earlier draft built the grammar - section
splitting, code-fence tracking, a conditional-marker exemption so
"creates no artifacts unless you ask" would pass - and measured
against realistic prose it was imprecise in both directions while
returning the same verdict on the real input. The list has no
exemption logic to get wrong, and any maintainer can extend it.

Phrasings that are only wrong in the absolute ("writes nothing",
"creates nothing") are left to review, since the conditional form of
each is the wording the failure message recommends.

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

* test(docs): pin the explore capture contract, not the word

The guide check matched any "capture", so "explore automatically captures
every artifact" passed. Both explore.md and commands.md now must name the
user trigger, `openspec new change`, and the named-artifacts scope, with no
capture line claiming it happens unprompted, and keep "never writes code".

Also scope the explore.md guarantee to the setup files a new change needs,
and make the commands.md offer name the change and its scope, which the
template asks for on main and after #1832.

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

* test(docs): accept negated unprompted-capture wording, catch non-capture verbs

The unprompted check matched "automatically" on any capture line, so the
correct "Explore does not automatically capture artifacts" failed, while
"Explore automatically writes planning artifacts" was never scanned because
it lacks the word capture. Check each clause of lines naming explore or
capture for an unprompted write verb with no preceding negation.

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

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 15:45:53 +00:00
openspec-release-bot[bot]andgithub-actions[bot] 9d4e5974e5 Version Packages (#1822)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-09-09 20:59:22 +00:00
Clay GoodandClaude Opus 5 e4e112d94f chore(deps): declare pnpm overrides only in pnpm-workspace.yaml (#1816)
The security overrides were declared twice: in pnpm-workspace.yaml, with
the advisory comments explaining each pin, and again under
package.json's pnpm.overrides. The copies are not additive — pnpm 10
uses package.json's block instead of the workspace list when both are
present — and Dependabot rewrites plain-name entries in package.json
whenever it bumps the same package. So a routine bump silently
displaces the pins that patch advisories, and fails the equality test
that guards them (#1812).

Keeps one declaration, in the file that carries the reasoning, and
asserts the mirror stays gone.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-09 17:47:22 +00:00
aedf4d0c64 fix(archive): preserve blank lines inside code fences (#1798)
* fix(archive): preserve blank lines inside code fences

* docs(test): document fence-preservation test helpers

* chore(changeset): track the fenced blank-line fix

The fix changes archive output for any spec documenting a fenced sample with
consecutive blank lines, so it belongs in the changelog. Release tracking only
validates changesets that exist; it never requires one, which is why CI stayed
green without it.

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

---------

Co-authored-by: Clay Good <hi@claygood.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-09 17:03:39 +00:00
d9e1a28c38 fix(update): refresh generated files that drifted (#1808)
* fix(update): refresh generated files that drifted

* test(update): isolate command drift from missing files and host config

* chore(changeset): track the command-drift fix

`openspec update` now reports and repairs tools it previously called up to
date, so users will see a behavior change. That belongs in the changelog.

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

---------

Co-authored-by: Clay Good <hi@claygood.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-09 16:32:45 +00:00
8251763ecd fix(parser): apply every delta section header (#1802)
* fix(parser): apply every delta section header

* test(parser): assert section presence for a header after another section

* chore(changeset): track the repeated-section fix

Deltas that previously applied only part of what was authored now apply all of
it, which changes archive output for affected changes. That belongs in the
changelog.

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

---------

Co-authored-by: Clay Good <hi@claygood.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-09 16:32:39 +00:00
fadac3e1c9 fix(parser): accept all CommonMark list markers in deltas (#1800)
* fix(parser): accept all CommonMark list markers in deltas

* docs(parser): document the REMOVED and RENAMED readers

* chore(changeset): track the list-marker fix

A removal or rename written with `*` or `+` now takes effect where it
previously did nothing, so existing specs can change on the next archive. That
is a user-visible behavior change and belongs in the changelog.

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

---------

Co-authored-by: Clay Good <hi@claygood.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-09 16:32:35 +00:00
3915db763a fix(guidance): teach the spec-inventory verb to generated guidance (#1700)
* fix(guidance): teach the spec-inventory verb to generated guidance

`openspec list --specs` appeared in no generated skill, command, or
artifact instruction, while `openspec list --json` — the in-flight
CHANGE list — appeared throughout. An agent asked to read the existing
specs first reached for the one enumeration verb it had been taught,
got the change list, found it plausible, and reported the step complete
against the wrong object.

Explore now lists the spec inventory alongside the change list and says
which is which. The spec-driven `proposal` and `specs` instructions name
the command at the two points that need it: researching existing
capabilities before filling in the Capabilities section, and confirming
a delta's path matches an existing capability.

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

Closes #1689

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

* fix(guidance): carry the store qualifier wherever the command is named

A bare `openspec list --specs` reads the local inventory, so under a
selected store it confirms a capability path against the wrong root. The
proposal instruction carried the qualifier; the modified-capability
instruction did not. All four sites now use the same wording, and the
guard is scoped to the passage that names the command — every explore
body already carries the qualifier in its unrelated capture steps, so a
whole-body assertion would pass with it dropped here.

Addresses CodeRabbit review on #1700.

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

* fix(guidance): read a listed capability with the store-aware command

The read step I added defeated the fix under a store. It told the agent to
list the inventory with `--store "<id>"`, then read the result back from
`openspec/specs/<capability-path>/spec.md` — a local path. Verified against
a registered store: `list --specs --store mystore` returns
`store-only-capability`, and the corresponding local read fails outright
(or, when a local capability happens to share the name, silently returns a
different one). That is the same wrong-object failure #1689 is about,
reintroduced one line later.

Capabilities are now read with
`openspec show "<spec-id>" --type spec --json --no-scenarios`, which
resolves against the same root the listing came from and returns purpose
plus requirement texts without pulling whole spec files into context.
`--type spec` is load-bearing: a change and a spec sharing a name is an
ambiguous_item error, and change names routinely mirror capability names.

Also documents `--store` on `list` and `show` in docs/cli.md. Both already
accepted the flag — the prose at line 228 says so — but neither options
table listed it.

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

* fix: Use ASCII arrows instead of unicode

This fixes the issue of ambiguous unicode character width when visualizing on terminals

* fix: Update remaining docs within explore to use ASCII

* fix(explore): finish the ASCII conversion and guard it

Rebase onto main and close the gaps in the original fix:

- Regenerate skills/openspec-explore/SKILL.md. The static skills/ mirror
  landed after this branch was cut, so the parity test would have failed
  with the template and the mirror out of sync.
- Regenerate the three parity hashes through scripts/regen-parity-hashes.mjs.
- Convert the ambiguous-width glyphs the first pass missed: the bullets in
  the CLI-storage example, and the check/cross marks in its comparison
  table, which sat in the column-aligned block the bug is about.
- Tighten the ASCII guidance to two lines. It ships into every user
  project on both delivery surfaces, so the paragraph was pure overhead.
- Add regression tests (#983): every fenced example in both the skill and
  the command body must be free of box-drawing, arrow, bullet, and
  check/cross glyphs, and the guidance must state the rule and the reason.
- Add a patch changeset.

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

* test(explore): cover every check/cross dingbat in the ASCII guard

The matcher listed U+2713 and U+2717 only, so a fenced example could use
✕ (U+2715) or ✘ (U+2718) — same ambiguous width, same misalignment — and
still pass. Widen to the U+2713-U+2718 run.

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

* fix(explore): require explicit confirmation before writing files

* test(explore): harden write confirmation guardrail

* fix(explore): scope write confirmation precisely

* test(guidance): pin store-aware spec reads

* test(templates): regenerate explore parity hashes

The explore template now carries three independent guidance edits: the
spec-inventory verb, the ASCII diagram conversion, and the write
confirmation contract. Each pinned its own hash constants, so the pinned
values no longer describe the combined template.

Regenerate them from the merged source with `regen:parity-hashes` rather
than hand-editing, and confirm the committed skills mirror still matches
byte-for-byte.

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

* fix(guidance): read complete specs before coverage decisions

* docs: drop the redundant legacy docs/cli.md edit

docs-lab/README.md makes docs-lab/ canonical and the old docs/ tree legacy.
docs-lab/reference/cli.md already documents `--store <id>` for both
`openspec list` and `openspec show`, so this branch's docs/cli.md rows added
a third copy in the stale tree and nothing else.

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

* chore(changeset): drop the docs claim this PR no longer makes

alfred-openspec on #1700: the release note still said docs/cli.md now documents
--store on list and show, but that legacy-tree edit was removed from this head
and the diff does not touch docs/cli.md. The canonical docs-lab/reference/cli.md
already documented the flag on both commands, which is why the edit went.

Removing the sentence rather than repointing it at docs-lab: nothing in
docs-lab changed either, so there is no documentation change to announce.

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

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Shooks <justanormalme@gmail.com>
Co-authored-by: Ayman D. <ayman.bacc@gmail.com>
2026-09-09 16:25:02 +00:00
Clay GoodandClaude Opus 5 c170dc77ad fix(archive): read a wrapped scenario bullet as one bullet (#1782)
* fix(archive): read a wrapped scenario bullet as one bullet

A repository that wraps its prose at a column limit writes most scenario
bullets over two lines. The retirement guard read the continuation line
as content the merge could not account for, so `retire_capabilities`
refused every such spec - and because the hint that names the marker is
gated on that same count, an unmarked author got the bare "must have at
least one requirement" abort and never learned the retirement path
exists.

A line indented to the content column of the item above it, with no
blank line between, is part of that item. It is accounted for when the
item was and already reported when it was not, so nothing is deleted
unmentioned either way. A blank line still ends the item, so a note
written below the scenarios is still the author's own however it is
indented.

Closes #1780

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

* fix(archive): keep an indented heading out of a bullet's continuation

Continuation is for wrapped prose. A raw HTML heading indented under a
scenario bullet was absorbed by it, so indenting a section one level
would have smuggled it past the audit and deleted it with the file. ATX
headings were already excluded; HTML ones now are too, matching how the
pass above the requirements section reads them.

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

* fix(archive): flag a setext heading indented under a bullet

A setext underline turns the line above it into a heading, so indenting
the pair one level under a scenario bullet let a whole section be
absorbed as continuation and deleted with the file. Checked ahead of the
continuation branch now, the same way the ATX and raw HTML forms already
are.

Found by CodeRabbit on this PR.

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

* fix(archive): read an unindented wrapped bullet as one bullet too

Not every wrap indents its continuation, and the indent-only rule left
the reported bug fixed for one spelling and live for the other: a
hand-wrapped scenario bullet still refused the retirement.

Inside a scenario's unbroken bullet run a lazy continuation is now read
as part of the bullet above it. This widens nothing - a sibling bullet
written in that same position is already read as the scenario's own, and
a lazy line is part of the bullet where a sibling is merely next to it.
Past the blank line that ends the run the indent is still required, so a
note bulleted below the scenarios and the line that wraps it stay the
author's.

Also covers CRLF specs, and asserts the refusal report names only the
real leftover in a wrapped multi-requirement spec rather than burying it
under continuations.

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

* fix(archive): stop a lazy continuation at anything that opens a block

CommonMark lets a blockquote, thematic break, table, list item or raw
HTML interrupt a paragraph, so one written flush against a scenario
bullet starts something new rather than continuing it. The lazy
allowance absorbed all of them, which would have deleted an author's
note with the file and named nothing.

The bullet's paragraph is now tracked as its own state: opened by a
bullet, closed by a blank line, a fence, a heading, or a line that opens
a block - including one indented inside the item, whose own paragraph
ends the bullet's. Lazy continuation applies only while it is open.
Indented continuation is unaffected: a nested list or quote sitting
inside the item is still the item's own content.

Each of the six holes is pinned by a test proven to fail with the
narrower rule removed.

Found by CodeRabbit on this PR.

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

* fix(archive): classify a line as the list item sees it

A marker as wide as `100. ` puts the item's content past the three
columns a Markdown construct is allowed at the file's left margin, so
`## Retention` written inside such an item read as five spaces of
nothing and was absorbed as continuation - a regression against the
behavior before continuation existed, which named it.

Every syntax test in the audit now reads the line with the item's
indent removed, so a heading, a setext underline or a block start is
recognized wherever the item sits.

Found by CodeRabbit on this PR.

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

* chore: drop test scratch directory committed by mistake

`test-spec-command-tmp/` is a fixture a test run leaves behind, swept up
by `git add -A` in the previous commit. It is not part of the change.

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

* fix(archive): share one list-marker definition with the paragraph rule

Folds in the marker coverage from the duplicate PR #1789, which fixes the same
issue (#1780) with a shallower model.

The audit named `-`, `*` and ordered items as list markers, while
INTERRUPTS_PARAGRAPH, added in this same PR, already named `+` and capped an
ordered marker at CommonMark's nine digits. The two disagreed, so a line one
called a bullet and the other did not was read as both at once.

Both now use one LIST_ITEM constant:

- `+` is the behavior fix. A spec bulleted with `+` validates like any other,
  and every one of its scenario bullets was reported as unaccounted content, so
  that capability could not be retired at all. Regression added, verified to
  fail against the old marker set.
- The nine-digit cap changes no verdict in this design, since a line the
  pattern rejects is weighed by the same rules either way. It is here for the
  consistency, and the comment says so rather than claiming a fix. The case is
  pinned so a later change cannot start deleting such a note.

LIST_ITEM also no longer requires content after the marker, so an empty `- `
reads as the bullet it is instead of falling through to the leftovers, which is
what the surrounding indent tracking already assumed.

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

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-09 16:24:59 +00:00
Clay GoodandClaude Opus 5 8ba4ac1b16 fix(apply): warn when a change is ready to implement with no specs (#1783)
* fix(apply): warn when a change is ready to implement with no specs

Apply gates on the schema's `apply.requires` (tasks) alone, so a change
whose tasks file was written ahead of its specs read as ready even though
it had no delta specs at all — the state `openspec validate` rejects.
Apply was the one surface that green-lit a change every other surface
flags, which is how agents end up implementing before the specs exist.

Report it as a warning, in the text output and in `--json`, naming both
ways out: write the specs, or declare `skip_specs: true`. Blocking would
be a policy change; naming the gap is not. Changes that have specs,
declare `skip_specs`, or are still blocked on their own required
artifacts are unaffected.

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

* refactor(apply): name the metadata file from its shared constant

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

* test(apply): cover custom schemas in the no-specs warning

A schema with no spec-producing artifact must stay quiet, and one whose
spec artifact is not called `specs` must still warn - the rule keys off
the output path, not the artifact id.

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

* test(apply): stop asserting an absolute temp path on Windows

os.tmpdir() hands back the short form (C:\Users\RUNNER~1) while the CLI
resolves the long one, so the assertion pinned a path that never matched
on windows-pwsh. Assert the change-relative tail instead.

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

* fix(apply): name the whole chain a blocked change still needs

Apply blocks on the schema's `apply.requires` alone, so its message
stopped at the first hop: a change holding only a proposal was told
"Missing artifacts: tasks" while the specs `tasks` depends on were
missing too. Taken literally that is an instruction to write the
tracking file straight from the proposal and skip everything between —
the failure reported in #834 and #869.

Walk `requires` and report the whole set, in build order, as
`missingPrerequisites` (text and `--json`). What apply blocks on is
unchanged, and the wording leaves conditional artifacts to the schema
rather than demanding them.

The remedies these messages give are now CLI commands rather than the
`openspec-continue-change` skill: `continue` is not in CORE_WORKFLOWS,
so on the default profile the old advice named a skill that is never
installed.

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

* fix(apply): name the schema's own spec artifact in the warning

alfred-openspec on #1783: collectApplyWarnings() discovers spec-producing
artifacts by output path, so it correctly fires for a schema whose artifact id
is `contracts`, but the remediation text then hardcoded
`openspec instructions specs`. That names an artifact such a schema does not
declare, so the advertised custom-schema support dead-ended at the exact step
meant to resolve the warning.

The command now derives its target from specArtifacts: the artifact's own id
when the schema declares one spec-producing artifact, and `<artifact-id>` as a
placeholder when it declares several, since there is no single right answer
there and a guess would read as an instruction.

The renamed-artifact test now asserts the command names `contracts` and
rejects the hardcoded `specs` spelling, and a new test pins the two-artifact
placeholder. Verified both fail against the hardcoded string.

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

* chore(changeset): bump apply warnings to minor

This adds `missingPrerequisites` and `warnings` to the documented
`instructions apply --json` contract in docs/agent-contract.md. New fields are
backward compatible, but they are new capability an agent can consume, which is
a minor under semver rather than a patch.

Taking the conservative direction deliberately: shipping new API surface as a
patch is the violation, since a consumer pinned to a patch range would receive
it without opting in. A minor costs nothing if the fields turn out to be
uninteresting.

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

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-09 16:24:55 +00:00
Clay GoodandClaude Opus 5 3c6d318b83 fix(init): name the workflows the profile left out (#1779)
* fix(init): name the workflows the profile left out

Setup output listed the workflows it installed but never mentioned the
ones it did not, so a user on the default core profile who typed
/opsx:ff saw nothing and read it as a broken install. The docs explain
profiles; nobody reads them before typing a command that should be
there.

init now closes with the missing workflows by name and the two commands
that add them. The note is skipped when nothing was generated at all,
where the existing delivery correction is the whole story, and when the
profile already installs everything.

Also adds a troubleshooting entry for the "only some /opsx: commands
show up" symptom, which the existing list did not cover.

Relates to #1076 (the optional-workflow discoverability half; the Windows command-discovery repro in that thread is not addressed here)

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

* chore: drop a test scratch directory committed by mistake

test-show-command-tmp/ is created by a test run and does not exist on
main; it was picked up by a `git add -A`.

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

* fix(init): keep the workflow note off runs that generate nothing

With no tools selected (or only tools that could not receive a surface),
`openspec config profile` followed by `openspec update` writes nothing,
so naming the missing workflows pointed at the wrong problem.

Reported by CodeRabbit on this PR.

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

* refactor(init): drop the redundant update step from the workflow note

`openspec config profile` offers to apply to the current project before
it exits, and prints the `openspec update` guidance itself when the user
declines, so naming a second command was one step too many.

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

* docs(troubleshooting): match the profile steps to what the CLI does

`openspec config profile` applies to the current project itself, so
listing `openspec update` as a second required step was wrong; it is the
fallback for declining the prompt or for other projects.

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

* fix(update): name the workflows the profile left out

`openspec update` is what the troubleshooting checklist tells a user to
run when a command they read about never appeared, and it is what people
run after upgrading the CLI. Neither of its existing profile notes fires
on the default `core` profile, so that user reached "All tools up to
date" and still learned nothing about the six workflows they don't have.

The note is the fallback pointer: silent when the extra-workflow or
missing-core note already named `openspec config profile`, and when no
configured tool can receive a workflow surface under the active delivery.

Reading the two existing notes as one short-circuited `||` would have
swallowed whichever ran second; they are evaluated separately.

Relates to #1076 (the optional-workflow discoverability half; the Windows command-discovery repro in that thread is not addressed here)

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

* refactor(update): gather the profile notes behind one call

The two call sites had grown identical six-line blocks. One
displayProfileNotes() keeps the ordering and the single-pointer rule in
one place, where the "evaluate every note, never chain them with ||"
constraint can be stated once.

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

* docs: drop the legacy troubleshooting entry

alfred-openspec on #1779: docs-lab/README.md says the old docs/ tree is legacy,
is no longer used by the site, and must stay untouched. The canonical
docs-lab/customize/profiles.md already lists the six optional workflows and the
'openspec config profile' command that adds them, and the root README already
calls out the expanded set, so this entry was a third copy in a stale tree.

The docs-lab troubleshooting page is a heading-only skeleton held back from the
site, so there is nothing to move it to; this PR is now source and tests only.

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

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-09 16:24:52 +00:00
Clay GoodandClaude Opus 5 6d2dbe62d3 fix(propose): load project context before planning (#1657)
* fix(propose): load project context before planning

* test(propose): assert project context is applied

* fix(propose): honor project context limits

* fix(propose): fail closed on unsafe context

* fix(propose): skip config without a root

* chore(parity): regenerate hashes after merging main

* fix(propose): harden early context loading guidance

* fix(propose): require initialization before planning in bare repos

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-09 16:24:48 +00:00
Clay GoodandClaude Opus 5 1c0ee701e5 docs: add CONTRIBUTING.md (#1781)
* docs: add CONTRIBUTING.md

Require a discussion (core design changes) or an issue before a PR is
opened, and require every PR to link its issue.

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

* docs: add setup and PR steps to CONTRIBUTING.md

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

* docs: make CONTRIBUTING.md the single source for the process

The README's Contributing section said small fixes could go straight to
a PR, which contradicts the new discussion/issue requirement. Point it at
CONTRIBUTING.md and carry over the conventional-commit and AI-disclosure
policies so nothing is lost.

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

* docs: close the three process gaps in CONTRIBUTING.md

alfred-openspec on #1781:

1. The OpenSpec-proposal rule was dropped from the README with nothing
   replacing it, recreating the gap in #1727. New step 2 carries the threshold
   over verbatim from the README (new features, significant refactors,
   architectural changes) plus the philosophy paragraph, says to open the
   proposal as its own PR and wait for approval, and tells anyone unsure to ask
   in the issue from step 1.

2. The discussion path contradicted itself: step 1 accepted a prior discussion
   while step 3 required 'Closes #123'. The PR step now says to link what you
   opened in step 1, 'Closes #123' for an issue or a link to the discussion
   when there is no issue. CodeRabbit's thread on README.md:227 is the same
   defect, so the README sentence says 'the issue or discussion' too.

3. The local setup was missing 'pnpm exec tsc --noEmit', which CI runs, and the
   README called the guide a development setup after 'pnpm run dev' and
   'dev:cli' were removed. The command is added, the guide states that those
   four commands are exactly what CI runs, and the README pointer now describes
   the guide as the full process rather than a setup.

Verified each documented command against this checkout: build, tsc --noEmit and
lint all pass as written.

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

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-09 16:24:45 +00:00
Clay GoodandClaude Opus 5 0b60a0ac1f chore(deps): bump the website-dependencies group in /website with 5 updates (#1815)
Applies dependabot's website bumps (#1812) and syncs the postcss
override in website/pnpm-workspace.yaml, which dependabot does not know
about, keeping the three override declarations in agreement.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-09 16:24:41 +00:00
Clay GoodandClaude Opus 5 6981c84df0 chore(deps): bump zod to 4.5.4 and eslint to 10.9.1 (#1814)
Consolidates the two open root-lockfile dependabot bumps (#1810, #1811)
into one PR so the pinned flake.nix pnpmDeps hash only has to be
regenerated once.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-09 16:24:37 +00:00
Clay GoodandClaude Opus 5 63666c8bb2 ci: report the correct pnpmDeps hash when flake.nix is stale (#1817)
* ci: report the correct pnpmDeps hash when flake.nix is stale

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

* ci: scope the reported hash to the pnpmDeps block

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

* ci(flake): scope every hash rewrite to the pnpmDeps block

alfred-openspec on #1817: the workflow read is scoped now, but the script it
runs is not. update-flake.sh read CURRENT_HASH from the first hash assignment
anywhere in flake.nix, and all three in-place rewrites matched every hash
assignment. flake.nix holds one fixed-output derivation today, so that lands on
the right line by luck; add a second and the script stamps the placeholder over
both, reads back whichever mismatch Nix reported first, and writes pnpmDeps'
hash into the other derivation. Scoping only the workflow left that path
fragile, as the review says.

The address range is declared once as PNPM_DEPS_BLOCK and used by the read and
all three rewrites, so the scoping cannot drift between call sites.

Also guards the read: an unmatched block previously left CURRENT_HASH empty,
and the failure path would then restore hash = "". It now exits before
touching the file.

Verified against a three-derivation fixture with pnpmDeps in the middle, which
catches both shapes of the bug: the scoped read returns the pnpmDeps hash while
an unscoped read returns the first derivation's, the placeholder is written
once rather than three times, and the neighbouring hashes survive the restore.
That fixture is the new test, alongside a static check that no hash read or
rewrite in the script is missing the range. Verified the static check fails
when any one call site is unscoped.

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

* test(flake): run the scoping fixture on its own volume

The new test failed on windows-pwsh with 'sed: cannot rename ./sedKaAflu:
Invalid cross-device link'. sed -i writes its temp file in the working
directory and renames it over the target; on a GitHub Windows runner the repo
is on D: and os.tmpdir() is on C:, so that rename crosses volumes.

bash now runs with cwd set to the fixture directory and addresses the file by
name, which keeps the temp file and its rename on one volume. The assertions
are unchanged.

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

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-09 16:24:16 +00:00
openspec-release-bot[bot]andgithub-actions[bot] e062b9572b Version Packages (#1766)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-09-03 00:00:56 +00:00
Tabish BidiwaleandClay Good fbd4160b37 docs: reroute unfinished store reference links (#1767)
Co-authored-by: Clay Good <hi@claygood.com>
2026-09-02 23:34:01 +00:00
Clay Good 9ec0a090b8 fix(security): patch fast-uri advisories (#1768) 2026-09-02 22:50:29 +00:00
dependabot[bot]andClay Good 9dfffd87b3 chore(deps): bump the website-dependencies group across 1 directory with 7 updates (#1765)
* chore(deps): bump the website-dependencies group across 1 directory with 7 updates

Bumps the website-dependencies group with 7 updates in the /website directory:

| Package | From | To |
| --- | --- | --- |
| [fumadocs-core](https://github.com/fuma-nama/fumadocs) | `16.14.5` | `16.15.2` |
| [fumadocs-mdx](https://github.com/fuma-nama/fumadocs) | `15.2.3` | `15.3.1` |
| [fumadocs-ui](https://github.com/fuma-nama/fumadocs) | `16.14.5` | `16.15.2` |
| [lucide-react](https://github.com/lucide-icons/lucide/tree/HEAD/packages/lucide-react) | `1.31.0` | `1.34.0` |
| [next](https://github.com/vercel/next.js) | `16.3.1` | `16.3.3` |
| [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) | `26.2.0` | `26.3.0` |
| [@types/react-dom](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react-dom) | `19.2.4` | `19.2.5` |



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

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

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

Updates `lucide-react` from 1.31.0 to 1.34.0
- [Release notes](https://github.com/lucide-icons/lucide/releases)
- [Commits](https://github.com/lucide-icons/lucide/commits/1.34.0/packages/lucide-react)

Updates `next` from 16.3.1 to 16.3.3
- [Release notes](https://github.com/vercel/next.js/releases)
- [Commits](https://github.com/vercel/next.js/compare/v16.3.1...v16.3.3)

Updates `@types/node` from 26.2.0 to 26.3.0
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

Updates `@types/react-dom` from 19.2.4 to 19.2.5
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react-dom)

---
updated-dependencies:
- dependency-name: fumadocs-core
  dependency-version: 16.15.2
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: website-dependencies
- dependency-name: fumadocs-mdx
  dependency-version: 15.3.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: website-dependencies
- dependency-name: fumadocs-ui
  dependency-version: 16.15.2
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: website-dependencies
- dependency-name: lucide-react
  dependency-version: 1.34.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: website-dependencies
- dependency-name: next
  dependency-version: 16.3.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: website-dependencies
- dependency-name: "@types/node"
  dependency-version: 26.3.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: website-dependencies
- dependency-name: "@types/react-dom"
  dependency-version: 19.2.5
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: website-dependencies
...

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

* fix(website): align esbuild build approval

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Clay Good <hi@claygood.com>
2026-09-02 22:22:14 +00:00
dependabot[bot]andClay Good cb5ae2cd16 ci: bump changesets/action from 1.9.0 to 2.1.1 in the github-actions group (#1746)
* ci: bump changesets/action in the github-actions group

Bumps the github-actions group with 1 update: [changesets/action](https://github.com/changesets/action).


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

---
updated-dependencies:
- dependency-name: changesets/action
  dependency-version: 2.1.1
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions
...

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

* fix(ci): complete changesets action v2 migration

* fix(nix): invalidate pnpm dependency hash

* fix(nix): use calculated dependency hash

* fix(nix): refresh pnpm dependency hash

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Clay Good <hi@claygood.com>
2026-09-02 21:55:12 +00:00
Marzx13andClay Good db03c6c4b0 feat(validate): add findings-only bulk reports (#1713)
* feat(validate): propose findings report

* docs(validate): clarify findings report contract

* feat(validate): implement and harden bulk findings reports

* test(validate): canonicalize store paths natively

---------

Co-authored-by: Clay Good <hi@claygood.com>
2026-09-02 21:07:46 +00:00
Ryan de MeloandClay Good a4fcdbece6 feat(validate): report the deltas archive would refuse (#1710)
* feat(validate): report the deltas archive would refuse

validate checked a change's deltas against themselves and, for MODIFIED
blocks, against the main spec's scenarios. It never checked whether the
main spec can supply the target a delta acts on, so a MODIFIED naming a
requirement that is not there, a RENAMED whose source is gone, or an
ADDED whose name already exists all validated clean and failed at
archive instead - typically weeks later, after the implementing PR had
shipped and the authoring session was gone.

Run the merge archive runs and report what it refuses. buildUpdatedSpec
returns the rebuilt content without writing it, so the preflight is the
same function on the same inputs with the result discarded, and cannot
disagree with the code that does the writing. That matters here: several
of those preconditions deliberately read a missing target as
already-synced rather than as a failure, and a second copy of the rules
would be free to drift.

Reported as INFO so no verdict changes in any mode. A MODIFIED whose
target is missing is also what a change modifying a sibling's unarchived
requirement looks like, and validate stays valid for that case today;
telling the two apart needs the opt-in marker #1112 asks for. What is
missing until then is the information, not the verdict.

Refs #1112

* fix(validate): skip preflight for deltas whose errors come after the loop

missingHeaderSpecs and emptySectionSpecs are collected inside the
per-spec loop but only become issues after it, so a suppression set
built from the issues raised so far could not see them. A headerless or
empty-section delta has nothing for the merge to apply, so the preflight
reported that as a blocker of its own, on top of the error that names
the actual mistake.

* fix(validate): harden archive preflight diagnostics

* fix(validate): preserve reports when archive preflight cannot start

---------

Co-authored-by: Clay Good <hi@claygood.com>
2026-09-02 20:59:09 +00:00
Br1anandClay Good 0296401b82 fix(init): add .gitkeep files to empty directories (#786)
* fix(init): add .gitkeep files to empty directories

After running openspec init, the specs/, changes/, and changes/archive/
directories are empty. Since git does not track empty directories, these
folders are lost when the repository is cloned, causing openspec list to
recommend re-initialization.

Added .gitkeep file creation to createDirectoryStructure() for both
normal and extend modes, ensuring empty directories are preserved in
version control.

Fixes #269

* fix(init): preserve directory anchors without overwriting user files

---------

Co-authored-by: Clay Good <hi@claygood.com>
2026-09-02 20:50:59 +00:00
Aron LeeandClay Good cd724449ac refactor(core): share the IDE restart hint between init and update (#1725)
* refactor(core): share the IDE restart hint between init and update

init named the surface it generated ("the new commands" / "the new skills");
update printed a generic "changes" for the same event, decided by the same
rule. Extract that rule and its wording into shared/ide-restart.ts so both
commands say the same thing, and update now names the surface too.

No condition changed: the hint still requires one tool that is both
IDE-resident and actually received a generated surface under the active
delivery, so a CLI tool's commands can never speak for an IDE tool that got
nothing.

Verified by mutation: dropping the IDE-resident filter turns 9 tests red
across the helper, init and update; swapping the commands/skills precedence
turns 5 red.

* test(core): harden shared IDE restart guidance

* fix(core): describe restart guidance for removed workflows

---------

Co-authored-by: Clay Good <hi@claygood.com>
2026-09-02 20:41:20 +00:00
Clay Good 954d4796a4 docs(community): add a community showcase (#1739)
* docs(readme): list the independent openspec ui project

* docs(community): move the showcase out of the readme
2026-09-02 20:32:42 +00:00
Clay Good 98bf53e59e fix(workflows): ground proposals in relevant project code (#1737) 2026-09-02 20:25:06 +00:00
HowardandClay Good 2fd175c8b0 docs(cli): document managed PowerShell completion setup (#1070)
* docs(cli): add Windows PowerShell completion example

The shell completion documentation only showed Unix/bash examples,
making it unusable for Windows users. Added platform-specific examples
for both Unix/macOS (bash) and Windows (PowerShell).

Changes:
- Add Unix/macOS (bash) example with ~/.bash_completion.d path
- Add Windows (PowerShell) example with C:\Users\y00031947\Documents\WindowsPowerShell\Microsoft.PowerShell_profile.ps1 path
- Improve clarity with platform labels

Fixes: Windows users cannot use shell completion manual installation

* fix(cli): use append operator for PowerShell profile to avoid data loss

Critical fix: Using '>' operator would overwrite the user's PowerShell
profile, deleting existing configurations. Changed to '>>' to append
instead of overwrite, preserving user's existing settings.

* docs(cli): harden PowerShell completion setup

---------

Co-authored-by: Clay Good <hi@claygood.com>
2026-09-02 20:14:57 +00:00
Dan (Danilo) Rio (Ribeiro)andClay Good b976106d95 fix(explore): guide planning with focused discovery questions (#1017)
* feat: improve explore discovery questions

* chore: add changeset for explore guidance

---------

Co-authored-by: Clay Good <hi@claygood.com>
2026-09-02 20:07:20 +00:00
openspec-cloud[bot]andClay Good cdd06a0594 docs(specs): align four requirements with current behavior (#1707)
* docs(openspec): correct 4 requirements that the code has outgrown

- ai-tool-paths/path-configuration-for-supported-tools#2: Changed the windsurf scenario's required skillsDir from `.windsurf` to `.devin`.
- cli-update/slash-command-updates#6: Require $ARGUMENTS to be placed in the file body (not frontmatter) for OpenCode archive commands.
- rules-injection/validate-artifact-ids-during-instruction-loading#6: Updated the expected warning text to use double quotes and to state it matches no artifact in any available schema, listing known artifact IDs.
- specs-sync-skill/skill-output#3: Changed the expected no-changes message to 'Specs already in sync; no files changed.' to match the code.

None of these reduce what a requirement demands.

Scanned at 1ebddd17f4 by openai/gpt-5-mini.

* docs(openspec): correct 3 requirements that the code has outgrown

- ai-tool-paths/path-configuration-for-supported-tools#2: Updated the Windsurf scenario to expect skillsDir '.devin' instead of '.windsurf'.
- cli-artifact-workflow/experimental-isolation#8: Updated the file-path in the Single file implementation scenario from src/commands/artifact-workflow.ts to src/commands/workflow to match current code organization.
- command-generation/toolcommandadapter-interface#2: Updated the Windsurf adapter file path pattern to use '.devin/workflows/opsx-<id>.md' to match the implemented adapter.

None of these reduce what a requirement demands.

Scanned at 1ebddd17f4 by openai/gpt-5-mini.

* docs(openspec): correct 4 requirements that the code has outgrown

- ai-tool-paths/path-configuration-for-supported-tools#2: Updated the windsurf scenario to require skillsDir `.devin` instead of `.windsurf` to match current mapping.
- cli-artifact-workflow/experimental-isolation#8: Updated the path in the 'Single file implementation' scenario from src/commands/artifact-workflow.ts to src/commands/workflow/*.
- context-injection/format-context-with-xml-style-tags#2: Updated tag name from <context> to <project_context> in the requirement and scenarios to match implementation.
- specs-sync-skill/skill-output#3: Updated the No changes needed scenario message to match the actual output: changed text to 'Specs already in sync; no files changed.'

None of these reduce what a requirement demands.

Scanned at 1ebddd17f4 by openai/gpt-5-mini.

* docs(openspec): correct 5 requirements that the code has outgrown

- cli-artifact-workflow/experimental-isolation#8: Updated the single-file path from src/commands/artifact-workflow.ts to src/cli/index.ts to match code.
- ai-tool-paths/path-configuration-for-supported-tools#2: Updated the Windsurf scenario to require skillsDir `.devin` instead of `.windsurf` to match the alias to Devin.
- command-generation/toolcommandadapter-interface#2: Updated the Windsurf adapter file path requirement to use the .devin/workflows/opsx-<id>.md path.
- cli-artifact-workflow/schema-apply-block#9: Updated the default instruction text to include the word "required", matching the implemented string.
- opsx-onboard-skill/graceful-exit-handling#8: Updated the continuation command from `/opsx:continue <name>` to `/openspec-continue-change <name>` to match the implemented command.

None of these reduce what a requirement demands.

Scanned at f1b521dffa by openai/gpt-5-mini.

* docs(openspec): correct 5 requirements that the code has outgrown

- ai-tool-paths/path-configuration-for-supported-tools#2: Updated the 'windsurf' scenario to require skillsDir '.devin' to match the current mapping of 'windsurf' to 'devin'.
- cli-init/exit-codes#7: Updated the exit code for user-cancelled operations from 3 to 130.
- context-injection/format-context-with-xml-style-tags#2: Replaced <context> tag name with <project_context> in requirement text and both scenarios to match the implemented tag.
- specs-sync-skill/skill-output#3: Replaced the no-changes message text to match the actual logged message ('Specs already in sync; no files changed.').
- telemetry/first-run-telemetry-notice#5: Updated the quoted one-line notice text to include the additional opt-out instruction 'or openspec config set telemetry.enabled false' to match the implemented message.

None of these reduce what a requirement demands.

Scanned at f1b521dffa by openai/gpt-5-mini.

* docs(openspec): correct 4 requirements that the code has outgrown

- ai-tool-paths/path-configuration-for-supported-tools#2: Updated the Windsurf scenario to require skillsDir '.devin' instead of '.windsurf'.
- cli-init/progress-indicators#1: Replaced the grouped spinner text '⠋ Configuring AI tools...' with the per-tool spinner text 'Setting up <tool.name>...'.
- specs-sync-skill/skill-output#3: Replaced the no-changes message to match the code: "Specs already in sync; no files changed."
- telemetry/first-run-telemetry-notice#5: Updated the quoted first-run notice text to include the alternative opt-out command 'or openspec config set telemetry.enabled false'.

None of these reduce what a requirement demands.

Scanned at f1b521dffa by openai/gpt-5-mini.

* docs(openspec): correct 6 requirements that the code has outgrown

- ai-tool-paths/path-configuration-for-supported-tools#2: Updated the Windsurf scenario to require skillsDir '.devin' to match the code mapping.
- cli-change/legacy-compatibility#2: Changed the deprecated command in both scenarios from 'openspec list' to 'openspec change list' and updated the deprecation notice to point users to 'openspec list'.
- cli-init/exit-codes#7: Updated the exit code for user-cancelled operations from 3 to 130 to match implemented behavior.
- cli-artifact-workflow/schema-apply-block#9: Updated default instruction text to match code: changed "All artifacts complete. Proceed with implementation." to "All required artifacts complete. Proceed with implementation."
- cli-artifact-workflow/output-messaging#12: Updated the expected skipped-commands message to match the actual output format: "Commands skipped for: <tools> (no adapter)".
- specs-sync-skill/skill-output#3: Updated the exact no-changes message to match the code's wording.

None of these reduce what a requirement demands.

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

* docs(specs): verify drift corrections against current behavior

---------

Co-authored-by: openspec-cloud[bot] <311461291+openspec-cloud[bot]@users.noreply.github.com>
Co-authored-by: Clay Good <hi@claygood.com>
2026-09-02 19:59:42 +00:00
1bcdf1b032 fix(build): prepare npm git installs without pnpm (#792)
* fix: handle npm git dep installation for GitHub installs

npm v11's git dep preparation runs `prepare` before node_modules exist
in the temp clone directory, causing TypeScript compilation to fail.

Changes:
- build.js: skip build gracefully when node_modules absent
- package.json: use `node build.js` directly in prepare/prepack for
  npm compatibility (avoids pnpm dependency during git dep install)

Note: postinstall.js already handles all errors internally via
main().catch(() => process.exit(0)), so no `|| true` wrapper needed.

Install from GitHub with:
  npm pack github:user/repo#branch
  npm install -g ./fission-ai-openspec-x.y.z.tgz

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

* fix(build): prepare npm git installs without pnpm

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Clay Good <hi@claygood.com>
2026-09-02 19:49:03 +00:00
Александр МелентьевandClay Good 44a39eb24b feat(core): add codeassistant support (#1171)
* feat(core): add codeassistant support

Signed-off-by: Александр Мелентьев <aleksandr4842@ya.ru>

* chore: change format file and add test

Signed-off-by: Александр Мелентьев <aleksandr4842@ya.ru>

* chore: add tests

Signed-off-by: Александр Мелентьев <aleksandr4842@ya.ru>

* fix: escaped description yaml values

Signed-off-by: Александр Мелентьев <aleksandr4842@ya.ru>

* chore: add test

Signed-off-by: Александр Мелентьев <aleksandr4842@ya.ru>

* chore: change adapter

Signed-off-by: Александр Мелентьев <aleksandr4842@ya.ru>

* chore: handle \r in description

Signed-off-by: Александр Мелентьев <aleksandr4842@ya.ru>

* chore: use common escapeYamlValue helper

Signed-off-by: Александр Мелентьев <aleksandr4842@ya.ru>

* chore(release): track sourcecraft support

---------

Signed-off-by: Александр Мелентьев <aleksandr4842@ya.ru>
Co-authored-by: Clay Good <hi@claygood.com>
2026-09-02 19:40:20 +00:00
Jason HindleyandClay Good 142b8a9203 fix(docs): route OpenSpec pixel through Pages (#1757)
* Route /openspec-pixel.svg to the docs Pages deployment

The docs nav logo is referenced via the root-relative path
/openspec-pixel.svg, which isn't matched by isDocsRoute() and so falls
through to the Astro landing site instead of the docs Pages project
that actually has the asset - a 404. /icon.svg already has this exact
special case; this adds the same for the pixel logo.

Fixes #1756

* fix(docs): route pixel logo through worker

---------

Co-authored-by: Clay Good <hi@claygood.com>
2026-09-02 19:31:59 +00:00
dependabot[bot]andClay Good 6911f55175 fix(deps): preserve Node 20 chalk compatibility (#1747)
* chore(deps): bump chalk from 5.6.2 to 6.0.0

Bumps [chalk](https://github.com/chalk/chalk) from 5.6.2 to 6.0.0.
- [Release notes](https://github.com/chalk/chalk/releases)
- [Commits](https://github.com/chalk/chalk/compare/v5.6.2...v6.0.0)

---
updated-dependencies:
- dependency-name: chalk
  dependency-version: 6.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

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

* chore(nix): refresh dependency hash

* fix(nix): use calculated dependency hash

* fix(deps): preserve Node 20 chalk compatibility

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Clay Good <hi@claygood.com>
2026-09-02 19:24:39 +00:00
dependabot[bot]andClay Good c5c38a7aca chore(deps-dev): bump eslint from 10.8.1 to 10.9.0 in the development-dependencies group (#1745)
* chore(deps-dev): bump eslint in the development-dependencies group

Bumps the development-dependencies group with 1 update: [eslint](https://github.com/eslint/eslint).


Updates `eslint` from 10.8.1 to 10.9.0
- [Release notes](https://github.com/eslint/eslint/releases)
- [Commits](https://github.com/eslint/eslint/compare/v10.8.1...v10.9.0)

---
updated-dependencies:
- dependency-name: eslint
  dependency-version: 10.9.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: development-dependencies
...

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

* chore(nix): refresh dependency hash

* fix(nix): use calculated dependency hash

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Clay Good <hi@claygood.com>
2026-09-02 19:15:51 +00:00
Clay Good d0071d7326 docs(archive): show how to retire capabilities (#1751) 2026-09-01 00:16:02 +00:00
openspec-release-bot[bot]andgithub-actions[bot] a0ddb60d04 Version Packages (#1728)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-08-26 21:19:57 +00:00
2fa679f180 fix(schema): make schema init --default actually set the default (#1709)
* fix(schema): make schema init --default actually set the default

--default wrote defaultSchema to openspec/config.yaml, but the config
loader only reads schema, so new changes kept using spec-driven while
the command reported success. Write the key that is read, and drop the
dead one a previous run may have left behind.

Fixes #1708

* fix(schema): preserve supported configs when setting default

* docs(schema): specify default config file handling

* docs(schema): protect default config migration

* fix(schema): make default initialization atomic

* fix(schema): hide init staging and backup dirs from discovery

`schema init` stages into `.init-staging-<rand>` and moves an existing
schema aside to `<name>.init-backup-<pid>-<ts>`, both inside the schemas
dir. `schema fork` already did this and the resolver filters its temp
names out of discovery; the init names were never added, so `listSchemas`
and `listSchemasWithInfo` surfaced them as real schemas -- in shell
completions, "available schemas" error lists, and change-metadata
validation.

The backup is the durable case: cleanup failure is deliberately tolerated
with a warning, so a blocked cleanup (or a crash mid-transaction) leaves a
permanent phantom schema behind.

Generalize the fork-only filter to cover both commands' staging and backup
names. Real schema names are kebab-case, so excluding these dot-bearing
names can never hide a legitimate schema.

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

---------

Co-authored-by: Clay Good <hi@claygood.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-26 20:42:30 +00:00
e5e350d04b fix(explore): use ASCII in diagram examples (#1010)
* fix: Use ASCII arrows instead of unicode

This fixes the issue of ambiguous unicode character width when visualizing on terminals

* fix: Update remaining docs within explore to use ASCII

* fix(explore): finish the ASCII conversion and guard it

Rebase onto main and close the gaps in the original fix:

- Regenerate skills/openspec-explore/SKILL.md. The static skills/ mirror
  landed after this branch was cut, so the parity test would have failed
  with the template and the mirror out of sync.
- Regenerate the three parity hashes through scripts/regen-parity-hashes.mjs.
- Convert the ambiguous-width glyphs the first pass missed: the bullets in
  the CLI-storage example, and the check/cross marks in its comparison
  table, which sat in the column-aligned block the bug is about.
- Tighten the ASCII guidance to two lines. It ships into every user
  project on both delivery surfaces, so the paragraph was pure overhead.
- Add regression tests (#983): every fenced example in both the skill and
  the command body must be free of box-drawing, arrow, bullet, and
  check/cross glyphs, and the guidance must state the rule and the reason.
- Add a patch changeset.

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

* test(explore): cover every check/cross dingbat in the ASCII guard

The matcher listed U+2713 and U+2717 only, so a fenced example could use
✕ (U+2715) or ✘ (U+2718) — same ambiguous width, same misalignment — and
still pass. Widen to the U+2713-U+2718 run.

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

* fix(explore): require explicit confirmation before writing files

* test(explore): harden write confirmation guardrail

* fix(explore): scope write confirmation precisely

---------

Co-authored-by: Clay Good <hi@claygood.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Ayman D. <ayman.bacc@gmail.com>
2026-08-26 20:18:06 +00:00
dd7cea3ffe feat(show): diff delta requirements against the main specs (#980)
* Proposed feature: openspec show --diff to see changed requirements more clearly

* Implementation of the --diff feature, which led to some spec changes during implementation.

* Update the proposal to be clearer. Thanks coderabbit

* Fix a bug identified by coderabbit with excessive trimming, and add a testcase for it.

* fix(show): harden --diff for review feedback

Keeps `openspec show <change>` without `--diff` a raw proposal
passthrough, reports when a change has no delta specs instead of
returning silently, preserves the Reason/Migration body of a REMOVED
requirement, and resolves main specs through the command's root so
`--store <id>` diffs against that store.

Text mode and JSON mode now render from one shared collection pass, the
CLI tests drive argv arrays from a mkdtemp project instead of
interpolated shell strings, `--diff` is registered for shell
completions, and the stray package-lock.json is gone.

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

* build(deps): declare the diff dependency and refresh the flake hash

Adds the `diff` runtime dependency that requirement-diff.ts imports,
updates pnpm-lock.yaml, and regenerates the flake's pnpmDeps hash so
`nix build` matches the new lockfile.

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

* fix(show): diff nested capabilities too

collectSpecDiffs enumerated only top-level directories under the
change's specs/, so a nested capability (specs/<area>/<id>/spec.md) was
skipped: text mode printed nothing for it and its MODIFIED deltas came
back from --json with no diff. It now uses the same discoverSpecFiles()
helper ChangeParser uses, so the capability ids match the `spec` field
of the JSON deltas.

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

* fix(show): report header mismatches instead of hiding them

Two cases where --diff quietly showed something misleading:

A MODIFIED requirement whose capability has no main spec was rendered as
all-additions, which reads like a new capability. It is an authoring
error archive will reject, so it now prints the raw text with a warning
naming the missing spec.

A header that differs from the main spec only in case or interior
spacing found no match at all under exact lookup, or matched under a
lowercase-only comparison that let a real mismatch through silently.
Lookup is now exact first, then the shared foldRequirementName fallback,
and a folded match prints the diff the author meant alongside a warning
that archive matches names exactly.

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

* refactor(show): name the main spec as such in collectSpecDiffs

Comment and locals still called the main spec the "base" spec, and the
no-main-spec comment described the old all-additions behavior.

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

* chore(deps-dev): bump the development-dependencies group with 2 updates

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


Updates `smol-toml` from 1.7.1 to 1.8.0
- [Release notes](https://github.com/squirrelchat/smol-toml/releases)
- [Commits](https://github.com/squirrelchat/smol-toml/compare/v1.7.1...v1.8.0)

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

---
updated-dependencies:
- dependency-name: smol-toml
  dependency-version: 1.8.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: development-dependencies
- dependency-name: typescript-eslint
  dependency-version: 8.67.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: development-dependencies
...

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

* chore(nix): invalidate pnpm dependency hash

* fix(nix): update pnpm dependency hash

* fix(show): harden requirement diff output

* build(nix): pin combined dependency hash

* test(show): assert proposal precedes diffs

* docs(show): clarify JSON diff diagnostics

* fix(show): retain unified diff hunk headers

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: Clay Good <hi@claygood.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-26 20:18:03 +00:00
ab81a4b43a fix(completions): stop Fish completions falling back to filenames (#1199)
* fix(completions): suppress filesystem fallback in Fish completions

* fix(completions): force files back on for path positionals in Fish

Fish never restores filesystem completion once a matching rule sets
--no-files, so a path positional needs an explicit --force-files rule.
Without it, `openspec store register <TAB>` lost file completion because
the sibling subcommand rules in the same context now carry -f.

Also drop retired "context store" vocabulary from the test fixtures.

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

* fix(completions): preserve workset member paths in fish

* fix(completions): force Fish path fallback

* fix(completions): harden Fish option handling

* fix(completions): scope Fish path fallback

* fix(completions): match Fish command paths exactly

* fix(completions): skip parent options in Fish paths

---------

Co-authored-by: Clay Good <hi@claygood.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-26 19:27:27 +00:00
109f81f17d fix(antigravity): update skill and workflow paths from .agent to .agents (#830)
* fix(antigravity): update skill and workflow paths from .agent to .agents

Antigravity has migrated from .agent (singular) to .agents (plural) for
workspace skills and workflows. The old .agent path still works via
backward compatibility, but the official docs now specify .agents.

Changes:
- config.ts: skillsDir '.agent' -> '.agents'
- antigravity adapter: workflow path '.agent/workflows/' -> '.agents/workflows/'
- legacy-cleanup: add patterns for old .agent/ artifacts cleanup
- docs: update supported-tools.md table
- tests: update expected path assertions

Closes #0 (reported by BugsCreator and Minh Pham in Discord)

* fix(antigravity): migrate an existing .agent install to .agents

Pointing Antigravity at `.agents` leaves every existing `.agent/` install
behind, so this registers the move instead of only changing the target:

- `.agent` becomes Antigravity's legacy skills root and legacy tool root, so
  update relocates managed skills and commands after generating their
  replacement, keeping a file the user customized.
- Detection keys off `.agent` and `.agents/workflows`. The bare `.agents` root
  is shared with Codex, Zed, and the vendor-neutral target, so it cannot stand
  in for "Antigravity is set up here".
- Delivery inference reads a tool's legacy roots for command files too.
  Without it the first update after the move saw skills but no commands,
  wrote `delivery: skills`, and the next update deleted every slash command.
- Legacy slash-command cleanup stays scoped to the pre-opsx `openspec-*` names
  under `.agent`; the migration owns the `opsx-*` files, and a shared root is
  never glob-swept.

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

* docs: record the Antigravity root move

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

* fix(antigravity): require a generated command before moving its legacy copy

migrateCommandFiles relocated a legacy command file whether or not the
current root held a replacement. Codex never reached that path — it has no
command adapter — so Antigravity is the first after-generation move where it
matters: under skills-only delivery, or for a deselected workflow, the move
recreated a command OpenSpec had just decided not to install.

Gate the move on an existing destination, the same way migrateSkillDirs
already does for after-generation timing.

Reported by CodeRabbit on #830.

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

* fix(antigravity): arbitrate shared agent skills

* test(antigravity): cover Windows migration paths

* fix(antigravity): harden shared-root migration

---------

Co-authored-by: Clay Good <hi@claygood.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-26 19:25:46 +00:00
Marzx13andClay Good 04b37ac1d5 fix(archive): preserve requirement order when renaming (#1712)
* fix(archive): preserve requirement order when renaming

* chore(archive): add rename-order changeset

---------

Co-authored-by: Clay Good <hi@claygood.com>
2026-08-26 19:25:43 +00:00
126c5d6c59 fix(validate): report a Purpose left as the archive placeholder (#1671)
* docs(openspec): propose warn-on-purpose-placeholder

When a delta introduces a capability with no usable `## Purpose`, archive
writes `TBD - created by archiving change <name>. Update Purpose after
archive.` into the new main spec. Three places already tell authors to
replace it -- the `specs` instruction ("including a leftover `TBD`
placeholder"), the sync-specs summary step ("so it gets written now rather
than lingering"), and the cli-archive contract -- but nothing reports that
it is still there.

`--strict` cannot reach it. The check meant to catch a Purpose nobody wrote
is a 50-character floor and the placeholder is 91 characters, so the one
rule that exists to catch a thin Purpose is satisfied by the exact text
meaning nobody wrote one: a Purpose reading "Does stuff." fails --strict
today, while one saying nothing at all passes.

Proposes reporting it as a warning against the spec's Purpose -- silent by
default, failing under --strict, so a project already carrying placeholders
keeps validating until it opts into the stricter gate. Detection is narrow:
the generated sentence wherever it appears, and otherwise only a `TBD`
opening the Purpose, so prose raising an open question is left alone.

Planning artifacts only; no source changes.

Refs #369

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

* fix(validate): report a Purpose left as the archive placeholder

When a delta introduces a capability with no usable `## Purpose`, archive
writes a placeholder into the new main spec. Nothing read it afterwards, so
the capability kept a to-do in it while every command reported success.

`--strict` could not reach it. The check that exists to catch a Purpose
nobody wrote is a 50-character floor, and the placeholder clears it: a spec
whose Purpose read "Does stuff." failed --strict, while a spec whose Purpose
said nothing at all passed. #369 reported agents leaving the placeholder
behind and stayed open seven months; every remedy since has been an
instruction, which is the mechanism that report described as unreliable.

validate now reports it as a warning against the Purpose, naming the line
and saying to edit the main spec directly -- a delta's `## Purpose` is read
only when the capability is created, so it cannot replace an existing one.

Warning rather than error, because strict mode already means "warnings
fail": a project carrying placeholders keeps validating by default and only
--strict fails. Archive is untouched -- it validates rebuilt specs without
--strict, so a spec archive writes still passes the validation it would have
passed before, and the text archive writes is byte-identical.

The placeholder is recognised through the same constants the writer composes
it from, so the check cannot drift from the sentence it looks for -- the
failure mode of a second, hand-copied spelling being a check that matches
nothing and looks exactly like a check that found nothing. The one case that
cannot be a lookup is an agent-written placeholder, kept to a `TBD` opening
the Purpose: "the retry budget is TBD pending benchmarks" is authored prose
and is left alone.

Verified: 209 archive tests pass unchanged (the placeholder text is
asserted literally, so the output is provably identical); full suite 138
files / 3993 tests; 36/36 strict spec validations; build, lint and typecheck
clean. Against a project carrying four real placeholders, default mode still
exits 0 and --strict fails exactly those four.

Cross-platform CI is not yet confirmed -- it needs a pushed branch. Line
endings are covered by tests asserting a CRLF spec and an LF spec produce
identical findings, and the module does no path handling.

Refs #369

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

* test(validate): make every placeholder guard load-bearing

A mutation pass over the seven guards -- revert one, see which tests die --
found two that no test held.

The prefix/suffix test did not exercise the guard it named. Its Purpose read
"Explains what happens when archiving change my-change runs twice", which
contains neither half of the generated sentence, so it passed whether or not
the suffix was required. Matching on the prefix alone killed nothing. The
Purpose now embeds the real prefix constant and asserts the suffix is absent,
so the case is the one the name claims; the mutation kills it.

The empty-Purpose early return was genuinely dead. Neither rule matches empty
text, so removing the branch changed no behaviour and failed no test. Rather
than keep a guard nothing can hold, the branch is gone and the comment says
why an empty Purpose still yields null. The tests asserting that behaviour
are unchanged and still pass.

Every guard now dies under mutation:

  whole check removed from applySpecRules ......... 6 tests
  brevity no longer suppressed (else -> if) ....... 1
  word boundary dropped from the TBD marker ....... 1
  generated placeholder matched on prefix alone ... 1
  line-ending normalisation removed ............... 2
  section-boundary guard removed from locator ..... 1

Full suite 138 files / 3993 tests, lint and typecheck clean.

Refs #1670

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

* docs(openspec): record the mutation pass in the task list

The mutation work changed the implementation -- a test rewritten and a dead
branch removed -- but no task covered it, so the plan claimed less work than
was done. Added as group 6, marked complete, with why it was not planned.

5.4 now says what blocks it. It needs a pushed branch for the cross-platform
matrix, and the note records that line endings are covered locally by tests
asserting a CRLF spec and an LF spec produce identical findings, so a reader
can tell the difference between unverified and unverifiable-from-here.

The specs, proposal and design are unchanged and were checked: the delta's
empty-Purpose clause constrains behaviour, not structure, and that behaviour
is the same -- the redundant branch went, the rule did not.

26 of 27 tasks complete; the change still validates --strict.

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

* docs(openspec): close 5.4 on a green cross-platform matrix

CI dispatched on the fork against this branch: lint & typecheck, and the test
suite on linux-bash, macos-bash and windows-pwsh -- all green. The Windows job
installed, built and ran the suite rather than short-circuiting, which is the
part 5.4 existed to check, since the placeholder locator counts lines in files
that may carry either ending.

Recorded as a workflow_dispatch run on the fork, not the upstream pull-request
run, because those are not the same gate and the note should not let a reader
assume otherwise. Nix Flake Validation and Validate Release Tracking skipped:
this branch touches neither the flake nor release tracking.

27 of 27 tasks complete.

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

* fix(validate): name the placeholder's line, not the prose above it

The warning tells you which line to fix, and named the wrong one when the
generated sentence did not open the Purpose:

     3  ## Purpose
     4  Handles widget retries.            <- warning pointed here
     5
     6  TBD - created by archiving ...     <- placeholder is here

The locator asked "what is the first non-blank line after ## Purpose?"
rather than "where is the placeholder?". Those are the same line in five of
the six shapes a placeholder can take -- a leading TBD marker is the first
non-blank line by definition, and archive writes the generated sentence as
the section's only content -- so the two questions only diverge when a human
types prose above a leftover placeholder.

Pointing at that prose is worse than pointing nowhere: the reader sees a
sentence that is plainly fine and concludes the check is broken. design.md
already said a wrong line number is worse than none, and the delta already
required naming the line the placeholder is on, so this is the
implementation meeting a contract that was already written, not a change of
contract.

The locator is now told which rule matched. A leading marker keeps the
first-non-blank behaviour, because that is where it sits; the generated
sentence is located by its own text. When both match the leading marker
wins, being the earlier of the two.

Found by CodeRabbit on #1671. The finding was real despite its own
"Addressed" marker, which only tracked the file changing in a later commit.

Two test gaps let it through. The case that covered this input asserted
only that something was reported, never which line -- so it now asserts the
line, and a table pins every position a placeholder can occupy, each case
first checking that the line it expects really carries the placeholder. The
mutation pass could not have caught it either: mutation proves a test dies
when a guard is broken, and cannot invent an assertion nobody wrote.

Reverting the branch fails exactly the three new expectations. Full suite
4000 tests / 138 files, lint and typecheck clean.

Refs #1670

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

* refactor(validate): set the placeholder line unconditionally

ValidationIssue.line is optional and the project does not enable
exactOptionalPropertyTypes, so a plain assignment typechecks and
JSON output is unchanged - JSON.stringify drops undefined values.
findPurposePlaceholderIssue already returns the key unconditionally,
and the neighbouring push sites assign line plainly, so the
conditional spread was the odd one out.

* fix(validate): widen the placeholder check to TODO and read fences as quoted

#1670 left two questions open. Both are answered here, against how OpenSpec
already reads a spec.

A `TODO` opening the Purpose now reports as the same finding as a `TBD`.
Nothing OpenSpec writes produces one, but the marker an author leaves behind is
whichever word they reached for, and a Purpose reading `TODO: fill this in` is
as unwritten as one reading `TBD`. Only the opening position counts, as before,
so `TODOs are tracked in the linked issue` is still authored prose.

Fenced code inside a Purpose is now read as quoted material rather than as the
Purpose speaking, through the `buildCodeFenceMask` the requirement and structure
parsers already share. Without it a spec documenting the sentence archive writes
is reported as carrying it, which is the check failing the one document that
explains it - and a warning that fires on the docs teaches people to ignore the
warning. Fenced lines are skipped when locating the placeholder too, so a
`## Purpose` or `## Requirements` quoted in a fence can neither be mistaken for
the section header nor end the section early.

The message now names both what archive writes and a marker left in its place,
since one message covers both. Severity is unchanged: still a warning, so a
project carrying placeholders keeps validating and only --strict fails.

Every new guard is mutation-checked: dropping `TODO` kills 3 tests, unmasking
detection kills 2, unmasking the line locator kills 3, unmasking the header
search kills 1.

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

* fix(validate): read the marker boundary in any script, not just ASCII

Review found `\b` reading `TODOé` and `TBD١` as a marker followed by
punctuation, because `\b` only knows ASCII word characters. A Purpose is prose
and prose is not always Latin script, so the rule that a longer word beginning
with those letters is not a marker has to hold in any script.

The lookahead rejects letters, digits, combining marks and `_`, and nothing
else, so `TODO:`, `TBD -` and `TODO(owner):` are still the marker they look
like. Held in both directions: loosening it back to `\b` kills 1 test,
tightening it to reject punctuation kills 4.

Also reworded a task line that opened with `#1670`, which markdownlint reads as
a heading missing its space.

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

* fix(validate): locate the matched purpose placeholder

* docs(validate): remove trailing task whitespace

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Clay Good <hi@claygood.com>
2026-08-26 19:25:40 +00:00
a7353aea9a feat(status): add --all for batch status of every active change (#1301)
* feat(status): add --all for batch status of every active change

`openspec status --all --json` reports every active change in one
process instead of one CLI spawn (~500ms module-load) per change,
mirroring the existing `validate --all`. Emits a single
`{ changes: [ChangeStatus, ...], root }` envelope sorted by change
name; a change that fails to load contributes a per-change error entry
instead of failing the sweep. `--all` and `--change` are mutually
exclusive, honoring the --json null-shape on failure.

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

* fix(status): harden --all per adversarial review findings

- Validate --schema before the no-changes early return so a bogus
  schema fails consistently whether or not any change exists.
- Text mode now exits 1 when any change fails to load (mirrors
  validate --all); JSON mode still exits 0 with per-change diagnostics.
- Add tests for the --all --schema interaction (unknown schema
  null-shape, override propagation, broken-metadata precedence) and
  text-mode failure rendering.
- Changeset heading to "### New Features" per repo convention; add
  status --all to the agent quick-reference table in docs/cli.md.

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

* fix(status): thread batch null-shape through root resolution, align sort with validate

Code-review findings on --all:
- Pass failurePayload: { changes: [] } to resolveRootForCommand so a
  root-selection failure under --all --json still emits the documented
  batch null-shape (siblings like list/doctor/context already do this).
- Sort with localeCompare to match validate --all's ordering for
  mixed-case change names.
- Extract a shared loadStatus helper so the batch and single-change
  payloads cannot drift apart.
- Changeset no longer claims exact validate --all parity (JSON exit
  semantics deliberately differ).

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

* refactor(status): give the --all failure null-shape a single owner

Simplify pass on the --all diff: hoist the { changes: [] } batch
null-shape into an exported BATCH_STATUS_FAILURE_PAYLOAD constant so
the root-resolution and CLI-wrapper failure paths cannot drift, replace
the conditional spread with the plain ternary the sibling call site
already uses, drop a redundant array copy before sort, and narrow the
text-mode failure counter to the boolean it actually is.

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

* fix(status): point the missing-target error at --all, correct the docs

`openspec status` with neither --change nor --all listed the available
changes and named only --change, so the batch path was discoverable
only from --help. The error now offers both.

Also corrects two stale claims in the status section of docs/cli.md
that the new row sits next to: the command never prompts for a change
(it errors), and bare `openspec status` is not an interactive check.

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

* test(status): assert --all carries store context like the single-change path

The batch sweep resolves the root once and threads the store id into
every entry. Nothing pinned that: a regression would have shown up only
as a wrong path inside an agent's JSON. Assert the sweep's envelope root
and per-change payload match `status --change` in a registered store.

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

* fix(status): fail incomplete batch reports

* docs(status): clarify empty and batch output

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Clay Good <hi@claygood.com>
2026-08-26 19:25:39 +00:00
dependabot[bot]andClay Good c0c50f9a4c chore(deps-dev): bump the development-dependencies group with 2 updates (#1718)
* chore(deps-dev): bump the development-dependencies group with 2 updates

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


Updates `smol-toml` from 1.7.1 to 1.8.0
- [Release notes](https://github.com/squirrelchat/smol-toml/releases)
- [Commits](https://github.com/squirrelchat/smol-toml/compare/v1.7.1...v1.8.0)

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

---
updated-dependencies:
- dependency-name: smol-toml
  dependency-version: 1.8.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: development-dependencies
- dependency-name: typescript-eslint
  dependency-version: 8.67.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: development-dependencies
...

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

* chore(nix): invalidate pnpm dependency hash

* fix(nix): update pnpm dependency hash

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Clay Good <hi@claygood.com>
2026-08-26 19:25:07 +00:00
Ayman A.andClay Good 7010e26890 fix(explore): require explicit confirmation before writing files (#1716)
* fix(explore): require explicit confirmation before writing files

* test(explore): harden write confirmation guardrail

* fix(explore): scope write confirmation precisely

---------

Co-authored-by: Clay Good <hi@claygood.com>
2026-08-26 19:24:57 +00:00
dependabot[bot] 6926ccb18a chore(deps): bump the website-dependencies group (#1719)
Bumps the website-dependencies group in /website with 2 updates: [fumadocs-core](https://github.com/fuma-nama/fumadocs) and [fumadocs-ui](https://github.com/fuma-nama/fumadocs).


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

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

---
updated-dependencies:
- dependency-name: fumadocs-core
  dependency-version: 16.14.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: website-dependencies
- dependency-name: fumadocs-ui
  dependency-version: 16.14.5
  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-08-24 14:00:04 +00:00
Tabish Bidiwale f1b521dffa docs: rebuild docs site from docs-lab (#1649)
* docs: rebuild docs site from docs-lab

Replace the docs site's source tree with docs-lab, a page-by-page rebuild
of the OpenSpec docs (40 pages: Start / Guides / Customize / Multi-repo /
Reference / Help).

- Point website/docs.sync.config.mjs at ../docs-lab and restructure the
  sidebar into nested groups; sync script gains nested meta.json emission,
  leading-quote descriptions, idempotent writes, and diagram asset copying
- Remove the marketing landing page; / now redirects to /docs
  (meta-refresh page + Cloudflare _redirects)
- Add remark plugins (faq, file-steps, gfm-alert) and the FileSteps
  component backing the new page formats
- Add install.md at the repo root, curled by docs-lab/start/installation.md
  as an agent-executable install prompt
- Add the docs authoring skills (.agents/skills/{write,draft,verify}-
  openspec-docs); docs-lab/README.md links into write-openspec-docs

The old docs/ tree is now unused by the site and left for a follow-up.

Claude-Session: https://claude.ai/code/session_01BMMLYNJQPKXx1QHpnDn4ho

* docs: hold back unwritten pages, add worksets, drop diagram drafts

- website: comment out Overview, Guides, Architecture, Help, Legacy in
  docs.sync.config.mjs until those pages are written; temporary
  /docs -> /docs/installation redirect (Cloudflare _redirects + static
  export meta-refresh fallback in page.tsx)
- docs-lab: new multi-repo/worksets.md page, published under Multi-repo
- docs-lab: content revisions across start/, customize/, reference/,
  help/, multi-repo/; add review notes (Notes.md)
- remove docs-lab/diagrams option-* drafts and their website copies
- write-openspec-docs skill: add spoken-flow sentence rule

* docs: address review on PR #1649

- sync-docs: read the existing output directly instead of exists-then-read
  (CodeQL TOCTOU alert)
- hold back the headings-only Environment variables and Stores reference
  pages until written; links to them fall back to their GitHub source
- sources.md: cutover keeps docs/ in place and points at public/_redirects
- setup.md: label the workflow tree as the default set plus two optional ones

* docs: two review nits (spoken-flow rule, XDG_DATA_HOME note)
2026-08-21 20:45:19 +00:00
openspec-release-bot[bot]andgithub-actions[bot] 1ebddd17f4 Version Packages (#1705)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-08-19 22:23:30 +00:00
Clay Good 7da3f34fb6 fix(tasks): include verification in generated plans (#1660)
* fix(tasks): include verification in generated plans

* test(tasks): enforce inline verification guidance

* fix(tasks): harden verification guidance

* test(tasks): verify every onboarding checkbox
2026-08-19 21:07:20 +00:00
Clay GoodandClaude Opus 5 7276c6c268 fix(packaging): print the completions tip from the CLI, not a postinstall script (#1704)
* fix(packaging): print the completions tip from the CLI, not a postinstall script

The package's only install script existed to print one line suggesting
`openspec completion install`. Shipping it made every `npm install -g`
emit an npm allow-scripts warning, and `npm approve-scripts` then failed
with ENOMATCH because it looks in the local project, not a global install
— so the warning looked like a packaging fault with no way to clear it.

The tip now prints once on the CLI's first run, recorded via a
`completionTipSeen` flag in the existing global config alongside the
telemetry notice's `noticeSeen`. It writes to stderr so it can never
contaminate piped stdout, and is suppressed under CI,
OPENSPEC_NO_COMPLETIONS=1, `--json` runs, and `openspec completion`
itself. The published package now ships no lifecycle scripts at all.

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

* fix(completions): stop the first-run tip from corrupting global config

Adversarial review of the previous commit found it wrote a defaults-merged
config: `saveGlobalConfig({ ...getGlobalConfig(), completionTipSeen: true })`
stamped `profile: "core"` into every user's config.json on first run.
`migrateIfNeeded` treats a raw `profile` as "already migrated", so the
one-time profile migration would never run again — and `openspec update`
then deleted the user's installed workflow skills. Reproduced: 2 skill
directories removed where main reports "Migrated: custom profile with 8
workflows". The same write also overwrote an unparsable config with
defaults and made `openspec config list` report defaults as explicit.

The tip now reads and writes the raw config file and touches only its own
key, leaving an unreadable config strictly alone.

Other hardening from the same review:

- Suppress the tip for the hidden `__complete` resolver. Generated
  completion scripts call it on every Tab press with stderr discarded, so
  the one-shot tip was consumed where nobody could see it.
- Defer, never consume, when stderr is not a terminal. Agents and pipes
  drive this CLI far more often than humans do and would otherwise spend
  the tip into a log nobody opens.
- Skip the tip when completions are already installed. Previously the CLI
  advertised `completion install` to users who had run it — including on
  the very next command after installing. Adds `isInstalled()` to the
  bash/fish/powershell installers, mirroring the zsh one.
- Use the repo's `isCiEnvironment()` instead of a `CI === 'true'` string
  check, so `CI=yes`/`True`/`on` are as quiet as telemetry is.
- Move the call to `postAction` so the tip trails the command's output
  instead of pushing errors and `init`'s setup summary down the screen.
- Record before printing, so an unwritable config dir means silence rather
  than nagging on every run.

Tests: assert the message literal (mutation testing showed the message text
was the one unguarded behavior), the raw-write shape, corrupt-config
safety, the already-installed path, the defer policy, and an e2e case
pinning the non-TTY contract.

Docs: SECURITY.md no longer claims zero lifecycle scripts — `prepare` is
still declared and runs for git/directory installs; the registry-install
claim is the accurate one. `OPENSPEC_NO_COMPLETIONS` is now documented.

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

* test(completions): make the unwritable-config case portable to Windows

fs.chmodSync(dir, 0o555) does not stop a write on Windows, so this test's
unwritable condition never existed there: markTipSeen succeeded, the tip
printed, and windows-pwsh was the only failing job.

Occupy the config directory's path with a file instead. mkdirSync with
recursive: true tolerates an existing directory but throws on an existing
file on every platform, so the persist fails where a real permission error
would - before anything is printed. Also asserts the path is still a file,
so a partial write through the failure would be caught.

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

* fix(completions): retire the first-run tip instead of advising a dead end

Second adversarial pass over the tip, covering the hardening commit itself.

- An undetected or unsupported shell now retires the tip quietly. It used
  to print, but `openspec completion install` exits 1 for exactly those
  users ("Shell 'tcsh' is not supported yet" / "Could not auto-detect
  shell"), so the one message they would ever get about completions sent
  them to a command that fails.
- `markTipSeen` re-reads the config immediately before writing and swaps
  the file in by rename. Deciding whether to show the tip costs a `ps`
  spawn plus a stat, and a sibling process writing config in that window
  got clobbered — on a first run that is exactly when telemetry mints
  `anonymousId`. Concurrent-process loss drops from 15/40 to ~2/40, and
  what now usually loses is the tip's own flag (it simply shows once
  more) rather than telemetry identity. The residual is the non-atomic
  read-modify-write shape shared with telemetry's own writer.
- `isInstalled()` uses stat().isFile(), so a directory at the install
  path no longer counts as an installed completion script.
- Documented what `isInstalled()` actually promises: the script file, not
  the profile sourcing line that bash and PowerShell also need. Callers
  deciding whether to *advertise* completions want the loose reading — a
  user whose profile config failed has already met the installer.
- Corrected a comment claiming the probe costs "one stat": detectShell()
  forks `ps` to read the parent process on every non-Windows run.

Tests: mutation testing found four surviving mutants — dropping
isCompletionRun from the defer policy, reverting isCiEnvironment to a
CI==='true' string check, failing closed on an undetected shell, and
neutering the non-object config guard (which lets a JSON array config be
rewritten as {"0":...}). All four now fail a test. Adds direct coverage
for the three new isInstalled() implementations, which had none.

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

* fix(validate): stop `change validate` exiting past commander's postAction

`change validate` on a failing change called process.exit(exitCode). That
tears down before commander's postAction hook, which is the same trap the
`update` command documents 165 lines earlier: "exiting here would skip
commander's postAction hook, killing the telemetry flush mid-request".

A change that fails validation is a routine outcome, not an error, so this
silently dropped the telemetry flush and — since the completions tip moved
to postAction — the first-run tip for anyone whose first command was a
failing validate. Verified under a pty: before, the tip never printed and
completionTipSeen was never recorded; after, both happen and the exit code
is still 1 (validate() already sets process.exitCode, which Node honours at
natural exit — top-level `validate --all` has always relied on exactly
that). The existing e2e in validate-scenario-loss.test.ts pins the exit
code.

Also wraps the postAction tip in try/finally so the telemetry flush runs
even if the hint throws: program.parse() is synchronous, so a rejection
there has no catch above it.

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

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 20:19:51 +00:00
Clay GoodandClaude Opus 5 9643888a75 fix(schema): resolve main-spec reads against the store-aware root (#1703)
* fix(schema): resolve main-spec reads against the store-aware root

The spec-driven `specs` instruction named
`openspec/specs/<capability-path>/spec.md` — a cwd-relative path — for the
two operations that touch a capability's main spec: step 1 of the MODIFIED
workflow ("locate the existing requirement") and the edit that fixes a
leftover TBD Purpose.

When the change lives in a registered store, the main spec is under the
store root. Verified against one: `openspec instructions specs --store
mystore --json` returns `planningHome.root` pointing at the store while the
instruction sent the read to the working repo, where the capability does
not exist. Where a local capability happens to share the name it is worse
than a miss — the read succeeds against a different capability and step 2
copies the wrong requirement block into the delta, silently.

Both now use `<planningHome.root>/openspec/specs/...`, the root the same
JSON already returns, matching what sync-specs.ts and archive-change.ts
have said since they were written: use the store-aware root, not a
hardcoded repo path.

Guidance text only — no CLI, parser, or archive behavior changes. The two
remaining `openspec/specs/` mentions describe the shape of a capability
path rather than a file operation, and are left alone.

Closes #1702

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

* fix(schema): make the store-aware root unconditional, and prove it resolves

Two hardening findings.

The wording said the root "points at the store when a store is selected."
Verified across all four root configurations, that undersells it: a project
`store:` pointer (source `declared`) and a global default store (source
`global_default`) both resolve to the store with no `--store` flag passed.
An agent reading the old sentence could conclude the case did not apply to
it and fall back to a repo-relative path. It now says to always use the
field and not to reason about which case applies.

The test only pinned the placeholder text, which would still pass if
`planningHome.root` were renamed or the suffix were wrong. Added a guard
that substitutes the placeholder with a real resolved planning home and
asserts the composed path lands on an actual main spec. Mutation-tested:
inserting a path segment and renaming the field each fail it.

Verified end to end that the composed path exists under all three
store-selecting configurations, and under a plain local repo.

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

* test: compose the main-spec path from segments, not string substitution

The guard substituted `planningHome.root` into a template spelled with
forward slashes. On Windows that yields a mixed-separator path, so the
assertion passed because Node accepts forward slashes there rather than
because the path was built correctly. Windows CI was green either way;
this makes the construction right instead of merely tolerated.

The suffix is now captured on its own and joined to the root with
path.join, so the assertion uses native separators everywhere. All three
mutations (cwd-relative path, extra segment, renamed field) still fail
the guard.

Addresses CodeRabbit review on #1703.

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

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 20:19:46 +00:00
Clay Good 18688c8b27 fix(archive): never dead-end a capability retirement (#1699)
* fix(archive): never dead-end a capability retirement

A change whose delta removes the last requirement a capability has
rebuilds the main spec empty, which can never validate. Archive already
knows retiring is the fix and names the `retire_capabilities: true`
marker that authorises deleting the spec - but only when the marker is
the single thing missing.

If the spec also holds a line the merge cannot account for (a `## Notes`
section, a comment under a requirement - both ordinary), that hint was
suppressed, and the hint that names such lines only spoke to authors who
had already set the marker. Neither fired, so the archive aborted on
"Spec must have at least one requirement" with no guidance at all: the
exact dead end the marker exists to close.

Archive now names the blocking content in that case. It deliberately
does not name the marker there - adding it would not have let this run
through, and the marker is only ever named when it really is the one
thing missing. Once the content is resolved, the rerun names the marker.

Closes #1696

* fix(archive): harden the blocked-retirement abort

Three follow-ups to the same message.

The blocking lines are authored spec content printed verbatim to a
terminal, so they now get the treatment `describeChangeName` already
gives a change directory name: control characters replaced, since a raw
CR could forge a line of its own and an ESC could redraw the screen.
Each line is bounded too - one very long line would push the way out of
the abort off the reader's screen - and the cut counts code points so it
can never leave half a surrogate pair. Both the declared and undeclared
branches share the helper, so the marker-declared abort that shipped
with #1484 is hardened with it.

The wording no longer claims retiring is "the way through". It is not,
in the one case this fires on that has a live requirement hiding in a
second `## Requirements` section: merging the sections fixes that spec
without deleting anything.

`openspec/specs/cli-archive/spec.md` records the behavior change - the
blocking lines are named whether or not the marker was declared, and the
marker is still named only when adding it would let the archive through.

* refactor(archive): drop a helper the revised wording made single-use

The marker sentence is said in one place again, so it goes back inline
rather than through a function that now has one caller. Also corrects
the comment above `emptiedByThisRun`: retiring is not the only fix in
every case it covers, which is exactly why the message stopped saying so.

* docs(openspec): record the change as a delta, not a direct spec edit

Both conventions exist in this repo's history, but the two most recent
behavior fixes (#1609, #1616) carry an `openspec/changes/` delta rather
than editing the main spec in place, which is also the workflow this
project asks of everyone else.

The delta reproduces the whole Capability Retirement requirement, so
archiving it drops no scenario. Verified by archiving into a scratch
copy of `openspec/`: the merged main spec differs from today's by
exactly the three added bullets.

* fix(archive): report an unhonorable marker alongside the blocking content

An author who set `retire_capabilities: yes-please` believes they have
authorised the deletion. Clearing the blocking content first, only to
then learn the marker was never read, is two aborts for one mistake.

The abort still never invites the marker to be added while content
blocks the retirement - it only reports the one already there. The spec
delta records that distinction, which the old bullet ("say nothing about
the marker") did not draw.

* style(archive): use one sentence for an unhonorable marker in both aborts

* fix(metadata): strip control characters from an unhonorable marker reason

Every reason a boolean change-metadata marker gives quotes something the
author wrote - a schema name, a parser message carrying one, a
filesystem error carrying a path - and two commands print it straight to
a terminal. A schema name carrying a raw ESC, with the marker set, put
that ESC on screen through `openspec archive`; `openspec validate`
prints the same reason.

Fixed at the source in `readBooleanMarker` rather than at either call
site, so no consumer has to remember. The reason still quotes the name
recognisably; only control characters are replaced.

Reported by CodeRabbit on #1699. Pre-existing on main, and this PR would
have added a second place it reaches the terminal.

* test(archive): fix a comment left behind by the reworded abort
2026-08-19 20:19:34 +00:00
Clay Good c747ed1f34 feat(init): add language option (#1685)
* feat(init): add language option

* fix(init): harden language configuration

* fix(init): fail when language config cannot be written
2026-08-19 20:19:28 +00:00
Clay Good 15e50d6889 fix(opencode): pass command arguments to workflows (#1664)
* fix(opencode): pass command arguments to workflows

* test(opencode): recognize existing argument placeholders

* test(opencode): harden argument generation

* test(opencode): cover commands-only upgrades

* test(opencode): verify repaired command content
2026-08-19 20:19:23 +00:00
Clay Good cf06d45f91 fix(profiles): include sync with archive workflows (#1663)
* fix(profiles): install sync with archive workflows

* test(profiles): harden archive dependency coverage

* fix(config): preserve custom profile ownership
2026-08-19 20:19:15 +00:00
Clay Good f3aa167d6e feat(tools): add Zed Agent support (#1659)
* feat(tools): add Zed Agent support

* fix(tools): detect Zed projects
2026-08-19 20:19:11 +00:00
Clay Good a72a74de65 fix(update): only suggest IDE restarts when needed (#1656)
* fix(update): only suggest IDE restarts when needed

* test(update): cover restart hint edge cases
2026-08-19 20:19:06 +00:00
Clay Good a2b965aa5e fix(workflow): keep no-spec schema changes valid (#1655)
* fix(workflow): scaffold valid no-spec changes

* fix(workflow): normalize specs artifact paths
2026-08-19 20:19:01 +00:00
Clay Good 98c79324ac docs(workflows): fix sequence diagram rendering (#1654) 2026-08-19 20:18:56 +00:00
Clay Good fc0fec1250 fix(feedback): keep full reports in issue bodies (#1653)
* fix(feedback): keep full reports in issue bodies

* fix(feedback): preserve report formatting
2026-08-19 20:18:51 +00:00
91813641cf chore(deps): migrate to @inquirer/prompts v8 + @inquirer/core v11 (#1667)
* chore(deps): migrate to @inquirer/prompts v8 + @inquirer/core v11

Bumps both packages together. The two Dependabot attempts each moved one
half (#1450 prompts->8, #1422 core->11) and failed: prompts@8 pulls
checkbox@5 -> core@^11, while package.json depends on core@^10 directly
for two custom prompts, so a one-sided bump leaves two copies of
@inquirer/core in the tree — custom prompts on v10 internals alongside
bundled prompts on v11.

Resolves the `instructions` removal in checkbox v5 by dropping the
option: the built-in keys help tip now renders a superset of the hint
that was being passed, so no theme override is needed.

Closes #1458

* fix(nix): regenerate pnpmDeps hash for the inquirer v8 lockfile

The pnpmDeps fixed-output hash is pinned to the contents of pnpm-lock.yaml,
so the inquirer v8/core v11 migration invalidated it and Nix Flake Validation
failed with 'pnpm failed to install dependencies'.

Regenerated against this branch's lockfile and verified: nix build .#default
completes (exit 0) through openspec-1.9.0.drv, not just past the fetch.

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

---------

Co-authored-by: Clay Good <hi@claygood.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 18:39:33 +00:00
Ryan de Melo db981f279d fix(telemetry): print first-run notice to stderr, not stdout (#1666)
console.log put the notice on stdout, so any non-JSON command (e.g.
spec show, change show) had it prepended to raw/passthrough output on
a fresh machine with no prior telemetry config — breaking pipes and
consumers expecting exact file content. --json mode already avoided
this by deferring the notice; stderr fixes it for every mode at the
source instead of special-casing each one.
2026-08-19 18:39:30 +00:00
Ryan de Melo d56f9fc766 test: opt the suite out of telemetry (#1668)
34 test files spawn the real CLI across ~62 call sites. Each spawn runs
the preAction hook exactly like a user invocation, so a local `pnpm test`
persisted an anonymousId into the developer's real global config
(~/.config/openspec/config.json) and POSTed a command_executed event per
spawn to the telemetry endpoint.

CI never saw this because CI=<truthy> already disables telemetry; it only
happens on contributor machines, where it also skews the maintainers'
usage data with test traffic.

Set OPENSPEC_TELEMETRY=0 / DO_NOT_TRACK=1 via vitest's env so workers and
the CLI children they spawn are both covered. Telemetry's own tests
delete these vars before asserting, so they are unaffected.
2026-08-19 18:39:27 +00:00
dependabot[bot] cfc74eeb05 chore(deps): bump the website-dependencies group (#1680)
Bumps the website-dependencies group in /website with 4 updates: [fumadocs-core](https://github.com/fuma-nama/fumadocs), [fumadocs-ui](https://github.com/fuma-nama/fumadocs), [next](https://github.com/vercel/next.js) and [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node).


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

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

Updates `next` from 16.3.0 to 16.3.1
- [Release notes](https://github.com/vercel/next.js/releases)
- [Commits](https://github.com/vercel/next.js/compare/v16.3.0...v16.3.1)

Updates `@types/node` from 26.1.2 to 26.2.0
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

---
updated-dependencies:
- dependency-name: fumadocs-core
  dependency-version: 16.14.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: website-dependencies
- dependency-name: fumadocs-ui
  dependency-version: 16.14.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: website-dependencies
- dependency-name: next
  dependency-version: 16.3.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: website-dependencies
- dependency-name: "@types/node"
  dependency-version: 26.2.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  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-08-19 18:39:20 +00:00
dependabot[bot] d789318445 ci: bump dorny/paths-filter in the github-actions group (#1678)
Bumps the github-actions group with 1 update: [dorny/paths-filter](https://github.com/dorny/paths-filter).


Updates `dorny/paths-filter` from 4.0.2 to 4.0.3
- [Release notes](https://github.com/dorny/paths-filter/releases)
- [Changelog](https://github.com/dorny/paths-filter/blob/master/CHANGELOG.md)
- [Commits](https://github.com/dorny/paths-filter/compare/7b450fff21473bca461d4b92ce414b9d0420d706...ceb8a2b8f2d89434be7ff52d3de7ec3738c5cc9d)

---
updated-dependencies:
- dependency-name: dorny/paths-filter
  dependency-version: 4.0.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: github-actions
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-19 18:39:18 +00:00
openspec-release-bot[bot]andgithub-actions[bot] 2826b8889e Version Packages (#1629)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-08-13 15:22:19 +00:00
Clay GoodandClaude Opus 4.8 610b78f655 chore(changeset): add catch-up changesets for 6 untracked fixes (#1640)
Six user-facing fixes merged after v1.8.0 without a changeset, so they
would ship in v1.9.0 with no changelog entry and their authors uncredited.
All are patch fixes; the release target stays at 1.9.0.

Covers: #1637, #1607, #1632, #1616, #1612, #1523.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-12 16:40:35 +00:00
tech_ren 0221ac3d46 fix(archive): preserve blank lines around ## Requirements when syncing specs (#1637) 2026-08-12 16:05:17 +00:00
6d031f12f7 chore(deps): bump the website-dependencies group in /website with 2 updates (#1636)
* chore(deps): bump the website-dependencies group

Bumps the website-dependencies group in /website with 2 updates: [fumadocs-core](https://github.com/fuma-nama/fumadocs) and [fumadocs-ui](https://github.com/fuma-nama/fumadocs).


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

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

---
updated-dependencies:
- dependency-name: fumadocs-core
  dependency-version: 16.14.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: website-dependencies
- dependency-name: fumadocs-ui
  dependency-version: 16.14.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: website-dependencies
...

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

* fix(website): migrate search to ZBSearch for fumadocs 16.14

fumadocs 16.14.0 replaced the Orama search engine with ZBSearch, which
broke the custom static search dialog's type-check (Orama instance no
longer assignable to the ZBSearch client) and failed the Cloudflare
Pages build.

Switch components/search.tsx to the new `staticClient` API and drop the
now-optional client-side DB init — ZBSearch restores the tokenizer from
the exported static data. Remove the direct `@orama/orama` dependency,
which is no longer imported anywhere.

Co-Authored-By: Claude Opus 4.8 <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 Opus 4.8 <noreply@anthropic.com>
2026-08-12 00:08:05 +00:00
8127c7b7cc fix(schema): preserve YAML formatting when forking a schema (#1607)
* fix(schema): preserve YAML formatting when forking a schema

Rename a forked schema via yaml's Document API (parseDocument + doc.set)
instead of round-tripping through parseSchema/stringifyYaml, so block
scalars, comments, and key order in the source schema.yaml survive the
fork. Keep the structural parseSchema validation before the document
mutation so an invalid source is still rejected (addresses PR #1130
review). Adds fork-level regression coverage for both formatting
preservation and invalid-source rejection.

Co-Authored-By: JinzeLin <linjinze999@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* harden(schema): clean up partial fork when validation fails

If the source schema is structurally invalid, parseSchema throws after
copyDirRecursive has already created the destination directory, leaving
a broken half-schema on disk that made the next fork report "already
exists". Wrap the read/validate/rename in a try/catch that removes the
just-created destination on any failure and rethrows so the original
error still drives the JSON/exit-code reporting. The cleanup can only
ever delete a directory this run created: the no-force existing-dest
path returns before the copy, and the --force path removes the prior
directory first. This also closes a mid-write truncation window for free.

Adds regression coverage: cleanup + retryability on invalid source, the
pre-existing-destination-is-never-touched invariant, and a lock-in that
YAML-ambiguous names (true/false/null/off) round-trip as strings.

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

* harden(schema): validate fork source up front; never mask fork errors

Second hardening pass on the fork command, from an adversarial review of
the previously-added cleanup.

1. Atomicity: validate the source's schema.yaml up front, immediately
   after assertSchemaTreeCanBeCopied and BEFORE the --force removal of an
   existing destination. Previously the source was validated only after
   the copy, so `fork --force <invalid-source> <existing-valid-dest>`
   destroyed the existing destination and then failed, leaving nothing.
   This matches `schema init`, which already validates before it
   overwrites. Behavior is unchanged for valid sources, and the redundant
   post-copy validation is dropped.

2. Never mask the real error: the failure-cleanup rmSync is now wrapped
   in its own try/catch. fs.rmSync's `force` only suppresses ENOENT, not
   EPERM/EBUSY/ENOTEMPTY (e.g. a locked file on Windows or a concurrent
   process), so a failed cleanup could previously replace the real
   "Invalid schema" diagnostic with a confusing filesystem error. The
   original error is now always rethrown.

Adds regression coverage: --force with an invalid source leaves a valid
destination intact; the pre-existing-destination test now uses a valid
source so it exercises the no-force "already exists" guard directly.

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

* harden(schema): reject self-fork and stage fork before replacing destination

Two data-loss defects in `schema fork --force` (per alfred-openspec review):

1. Self-fork: forking a schema onto itself removed the destination (which IS
   the source) before the copy, so the copy then read a directory it had just
   deleted — destroying the only copy. Now rejected up front by comparing the
   real (symlink-resolved) source and destination paths before any removal.

2. Non-atomic replacement: an existing destination was removed before the new
   fork was fully copied and name-updated, so a mid-copy failure left the user
   with nothing. The fork is now staged in a temporary sibling directory and
   only swapped into place once complete; any failure while staging leaves both
   the source and the existing destination untouched.

Adds regressions: self-fork is rejected with the source intact; a forced fork
whose copy fails leaves the existing destination byte-identical with no staging
leftovers.

Co-Authored-By: JinzeLin <linjinze999@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* harden(schema): back up destination before installing fork so a failed final move restores

The stage-then-swap still removed the destination and then renamed staging into
place; if that final rename failed (e.g. a Windows lock) the destination was gone
with no restore. Now, when a destination exists, `fork --force` moves it to a
sibling backup, installs the staged fork, and only then discards the backup. If
the install rename throws, the backup is moved back so the original destination
is never lost. Non-existing destinations keep the simple staging rename.

Adds a regression: forcing the final staging->destination move to fail leaves the
pre-existing destination byte-identical with no staging/backup leftovers.

Co-Authored-By: JinzeLin <linjinze999@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* harden(schema): surface unrecoverable fork restore + hide fork temp dirs from discovery

Two more edge cases from alfred's review:

1. A failed backup->destination restore was silently swallowed, so if the final
   install AND the restore both failed the user lost the destination with no clue
   the backup existed. Now that case throws an error naming the backup directory
   and how to move it back, with the original install error attached as cause.

2. The transient `.fork-staging-*` / `<name>.fork-backup-*` directories live
   inside the schemas dir, so a concurrent scan could surface them as real
   schemas. isSchemaDir (the single discovery chokepoint) now excludes them;
   real schema names are kebab-case (no dots) so this can never hide a schema.

Adds regressions: an unrecoverable restore surfaces the backup path (and the
rescued content is really there); fork temp dirs are excluded from listSchemas
and listSchemasWithInfo.

Co-Authored-By: JinzeLin <linjinze999@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* harden(schema): fingerprint fork destination to abort on concurrent edits

A concurrent process could edit an existing fork destination between the moment
--force authorized the overwrite and the moment the destructive swap ran, and
those edits were silently destroyed (reproduced by alfred: mutate destination
schema.yaml during copy; --force completed and deleted the newer content).

Now, when overwriting an existing destination, the fork:
- fingerprints the authorized destination (SHA-256 over every file's relative
  path and bytes) BEFORE staging;
- re-fingerprints and compares immediately before moving the destination aside;
  on mismatch it ABORTS without touching the destination, preserving the
  concurrent changes and telling the user to re-run;
- re-fingerprints the backup before discarding it on the success path; if it
  changed during the install window it is kept, not deleted, and its location is
  surfaced.

All prior guarantees remain: self-fork rejection, stage-then-swap, backup/restore
on failed install with the backup path surfaced, and the temp-dir discovery
filter.

Adds regressions: a destination edited concurrently during staging aborts the
fork and preserves the edit; a backup modified during the install window is kept
and its location surfaced.

Co-Authored-By: JinzeLin <linjinze999@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(schema): avoid stat-then-read in fork fingerprint (CodeQL js/file-system-race)

fingerprintDir called fs.lstatSync then fs.readFileSync on the same path,
which CodeQL flags as a file-system race (the file may change between the
check and the read). Use the Dirent type already returned by readdirSync
({ withFileTypes: true }) instead of a separate lstat, and read files
directly, deriving the size from the bytes read. Behavior is unchanged
(13/13 fork-fidelity tests, incl. the concurrent-edit race regressions,
still pass); one fewer syscall per entry.

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

* harden(schema): validate the completed staged fork before any destructive step

The up-front parseSchema only checks the SOURCE, but copyDirRecursive reads
source files that can change mid-copy, so the staged result can be invalid even
though the source was valid at the pre-check (reproduced by alfred: mutate source
schema.yaml to invalid inside copyFileSync; --force installed the invalid fork
and deleted the valid destination).

Now, after copying and the Document-API name edit, the fork validates the
COMPLETED staged schema.yaml (the exact bytes about to be installed) with
parseSchema BEFORE any destination displacement. On failure it aborts, cleans up
staging, and rethrows a clear error ("the staged fork of '<source>' is not a
valid schema ...; aborted, '<dest>' was not modified") chaining the parse error.
The up-front source parseSchema stays as a fail-fast; this is the authoritative
gate. Order before the swap: validate staged -> fingerprint-revalidate dest ->
rename dest->backup -> rename staging->dest -> revalidate+rm backup.

Adds a regression: a source that becomes structurally invalid during staging
aborts the fork and leaves the valid destination byte-identical, no leftovers.

Co-Authored-By: JinzeLin <linjinze999@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: JinzeLin <linjinze999@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-11 23:36:45 +00:00
Clay GoodandClaude Opus 4.8 3281f1f068 fix(deps): patch js-yaml and nanoid advisories via pnpm overrides (#1635)
Resolve all three open Dependabot alerts (all high severity):

- GHSA-5p4m-2wfm-xmqj — js-yaml quadratic-CPU !!omap DoS (#96, #97).
  Root tree carried js-yaml 3.15.0 (via read-yaml-file) and 4.3.0 (via
  @changesets/parse). Dev-only; never in the published CLI, which uses
  `yaml`, not `js-yaml`. Pinned to >=3.15.1 / >=4.3.1.
- GHSA-2v37-7h3g-55p8 / CVE-2026-67213 — nanoid size=0 infinite loop (#99).
  Present in both root (dev, via postcss<-vitest) and website (build-time,
  via postcss<-next) trees. Pinned to >=3.3.17 (resolves to 3.3.18).

Overrides added to all four override surfaces (pnpm-workspace.yaml +
package.json, root and website) to keep them in sync, each YAML entry
annotated with its advisory id and removal condition.

flake.nix pnpmDeps FOD hash regenerated for the root lockfile change
(verified via nix build; hash-mismatch-count 0). dependabot.yml gains a
note documenting the two surfaces Dependabot cannot manage (pnpm
overrides + the Nix flake).

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-11 23:12:15 +00:00
Clay GoodandClaude Opus 4.8 b96b3e85cd chore(deps): bump safe website-dependencies subset (#1634)
* chore(deps): bump safe website-dependencies subset (defer fumadocs 16.14 / ZBSearch migration)

Ships the non-breaking bumps from dependabot PR #1631, holding back the
fumadocs 16.14 major that requires a website source migration.

Bumped:
- fumadocs-mdx  ^15.2.1 -> ^15.2.2 (resolves 15.2.3; builds cleanly)
- lucide-react  ^1.27.0 -> ^1.28.0 (resolves 1.31.0)
- next          16.2.12 -> 16.3.0
- postcss       ^8.5.25 -> ^8.5.26 (override ^8.5.22 governs resolution)

Held (defer to a dedicated migration PR):
- fumadocs-core ^16.12.1 (16.14 replaces Orama with ZBSearch)
- fumadocs-ui   ^16.12.1 (pairs with core)

fumadocs-mdx 15.2.3 does NOT pull core 16.14 transitively; type-check
and next build both pass. esbuild stays 0.28.1, so allowBuilds is
unchanged.

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

* chore(deps): apply the postcss bump for real (align override + lock to 8.5.26)

The manifest declared postcss ^8.5.26 but the pnpm override stayed ^8.5.22,
so the importer resolved 8.5.25 and the declared bump had no effect. Raise
the override (website/pnpm-workspace.yaml + website/package.json pnpm.overrides)
to ^8.5.26 and re-lock so postcss resolves 8.5.26 everywhere.

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

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-11 23:12:11 +00:00
Clay GoodandClaude Opus 4.8 207f3cc515 fix(config): label the update workflow in the picker; drop "expanded-profile" wording (#1632)
* fix(config): label the update workflow in the picker and drop "expanded-profile" wording

The config workflow picker builds each row's label from WORKFLOW_PROMPT_META
in src/commands/config.ts. The table had entries for 11 of the 12 workflows
but not `update`, so `openspec config` rendered that row as the raw id
`update` with a `Workflow: update` placeholder description. Since `update` is
one of the six core workflows, every user who opens the picker saw it.

Add the missing `update` entry so the row reads "Update change / Revise the
planning artifacts of an existing change".

Also reword the update-change workflow template, which called `/opsx:continue`
and `/opsx:new` "expanded-profile" workflows. There is no "expanded" profile;
the only profile values the product stores are `core` and `custom`. They are
now described as "optional" workflows. Regenerated the committed skills.sh
mirror and parity hashes accordingly.

Harden with a regression test asserting every ALL_WORKFLOWS id has real picker
metadata (no raw-id name, no "Workflow:" placeholder), so a future workflow
addition can't silently reintroduce the fallback.

Closes #1627

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

* chore: regenerate skills and parity hashes after rebase onto main

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

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-11 22:41:44 +00:00
Clay GoodandClaude Opus 4.8 4b114aade9 chore(deps-dev): bump development-dependencies group + refresh flake hash (#1633)
* chore(deps-dev): bump development-dependencies group + refresh flake pnpmDeps hash

Supersedes #1630. Dependabot's PR bumps two dev dependencies within their
existing package.json semver ranges (eslint 10.8.0 -> 10.8.1, typescript-eslint
8.65.0 -> 8.66.0), touching only pnpm-lock.yaml. That lockfile change
invalidates the flake's fixed-output pnpmDeps.hash, so #1630 fails Nix Flake
Validation ("pnpm failed to install dependencies"). Dependabot cannot update the
Nix FOD hash, so this PR carries the same bump together with the refreshed hash.

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

* fix(nix): set pnpmDeps hash for updated lockfile

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

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-11 22:24:01 +00:00
8364428661 fix(schemas): honor canonical root selection (#1616)
* docs(openspec): propose schemas root selection fix

* fix(schemas): honor canonical root selection

* test(schemas): assert complete JSON schema shape

* docs(stores): drop view from the cwd-only, no --store list

view already accepts --store <id> (registered in src/cli/index.ts), so
listing it among the commands that act on the current directory only was
incorrect. Remove it; templates and the deprecated noun forms remain.

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

* chore: regenerate skills and parity hashes after rebase onto main

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

---------

Co-authored-by: Clay Good <hi@claygood.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-11 22:04:03 +00:00
Clay GoodandClaude Opus 4.8 804427b6ff fix(telemetry): suppress first-run notice in --json mode (#1609)
* fix(telemetry): suppress first-run notice in --json mode

The first-run telemetry disclosure notice was written to stdout from the
global preAction hook. On a user's first-ever command with --json this
polluted stdout and could break JSON parsers. Read the executing command's
--json flag (actionCommand.opts().json) and, when set, skip the notice and
leave noticeSeen unset so the disclosure is deferred to the first later
non-JSON run rather than lost.

Spinner suppression, new-change --json output, and structured JSON errors
already landed on main (#960, #1190); this closes the one remaining stdout
writer in --json mode.

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

* harden: detect --json from argv to cover all invocation forms

The preAction guard read actionCommand.opts().json, which only sees a
declared leaf option. That missed two supported --json forms that emit a
single JSON document to stdout:
  - openspec store --json  (permissive group reads --json from residual args;
    never declares the option, so opts().json is undefined)
  - openspec workset --json <sub>  (--json on the parent group, consumed
    before the leaf; leaf opts().json is undefined)
Both would still print the first-run telemetry notice ahead of their JSON.

Detect --json from process.argv instead: it covers leaf, parent, and
residual-arg forms uniformly. Suppressing is always safe (the disclosure
defers to the next non-JSON run, never lost), so a broad argv check is the
correct, conservative signal.

Also add a direct assertion that noticeSeen stays unset after a silent run,
and note the pre-existing raw-stdout commands (completion generate, config
get/path, __complete) as out of scope.

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

* refactor: derive --json from parsed command state + regression test

Replace the process.argv check with isJsonRun(command), an exported pure
helper that reads Commander's parsed state: optsWithGlobals().json (leaf and
parent-group forms) OR command.args (residual --json on permissive bare
groups like store). This is tied to the actually-parsed command rather than
raw args, and — unlike process.argv — is unit-testable in-process.

Add test/core/cli-is-json-run.test.ts: a synthetic program reproducing all
three registration patterns proves isJsonRun returns true for status --json,
store --json, workset --json list, and workset list --json, and false
otherwise. This locks in the store/workset coverage against future
regressions (an e2e test can't: telemetry is disabled under CI, so the notice
never fires there).

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

* docs(spec): qualify first-command notice scenario as non-JSON

The generic 'First command execution' scenario asserted the notice
displays on every first command, contradicting the JSON scenario that
says it does not. Qualify it as 'without --json' so the required
behavior is unambiguous.

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

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-11 21:52:54 +00:00
Clay GoodandClaude Opus 4.8 17581c11ed fix(init): only show 'Restart your IDE' hint for IDE-embedded tools (#1610)
Reconstructed on current main (the original branch predated the Codex
.agents rename, Command Code, Rovo Dev, Antigravity, Zoo Code, and the
Kimi/Windsurf changes, so a direct rebase conflicted heavily in
config.ts/init.ts/init.test.ts).

Adds requiresIdeRestart to AIToolOption and gates the success-screen
restart hint so it shows only when an IDE-resident tool actually
received a surface. Wording follows that tool's own surface. Closes #1067.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-11 21:52:39 +00:00
1a10dd5820 docs(opsx): clarify /opsx:sync description and add usage section (#1606)
* docs(opsx): fix /opsx:sync description and add detailed documentation

- Changed description from 'Sync delta specs to main' to 'Merge delta specs into main specs'

- Added detailed Usage section for /opsx:sync command

- Now consistent with commands.md and migration-guide.md

- Improves documentation completeness and clarity

* docs(opsx): harden Sync delta specs section for accuracy and house style

Fold the /opsx:sync usage entry into a single prose paragraph to match
the six sibling Usage sections (heading -> fence -> paragraph), and fix
two accuracy issues found against src/core/templates/workflows/sync-specs.ts:

- Drop the invented "changes see each other's specs" and "test
  integration" use cases (no cross-change propagation or test step exists).
- State that sync applies the whole delta -- a REMOVED requirement is
  deleted from the main spec and a RENAMED one retitled -- so the section
  no longer reads as additive-only.
- Use the file's spaced em-dash convention.

Docs-site build verified: sync-docs + fumadocs next build compile and
render /docs/opsx end-to-end.

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

---------

Co-authored-by: Howard <yhwelcome1981@gmail.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-11 21:23:38 +00:00
Clay Good 137404b423 fix(cli): reject missing roots for list and validate (#1612)
* fix(cli): reject missing roots for list and validate

* test(cli): cover legacy list root fallback
2026-08-11 21:23:28 +00:00
Clay Good 144901ca74 chore(dependabot): ignore unsupported major updates (#1623) 2026-08-11 21:23:21 +00:00
dependabot[bot] 89169627e0 ci: bump pnpm/action-setup in the github-actions group (#1618)
Bumps the github-actions group with 1 update: [pnpm/action-setup](https://github.com/pnpm/action-setup).


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

---
updated-dependencies:
- dependency-name: pnpm/action-setup
  dependency-version: 6.0.10
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: github-actions
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-11 21:23:11 +00:00
942589741d fix(core): canonicalize rebuilt spec EOF (#1528)
* fix(core): canonicalize rebuilt spec EOF

* chore(changeset): add patch changeset for spec EOF canonicalization

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

---------

Co-authored-by: Clay Good <hi@claygood.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-11 21:22:59 +00:00
Clay GoodandClaude Opus 4.8 c751b3da52 fix(validate): count every level-4 header as a scenario in the loss guard (#1521)
* fix(validate): count every level-4 header as a scenario in the loss guard

The scenario-loss guard (#1482) recognized only `#### Scenario:` headers, but
the spec path (SCENARIO_HEADER / countScenarios) counts every `#### ` child of
a requirement as a scenario. A MODIFIED block that dropped a differently-labeled
level-4 child (e.g. `#### Edge case`) therefore passed validate and was silently
deleted by archive. Align parseScenarioBlocks with the spec path so both agree.

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

* test(validate): guard scenario-header parity; reuse SCENARIO_HEADER

Harden the scenario-loss parity fix after a multi-agent review:
- Export SCENARIO_HEADER from requirement-text.ts and reuse it in the delta
  path (scenarioHeaderAt/scenarioNameAt) so parity is guaranteed by
  construction, not two matching literals plus a comment.
- Add boundary tests for the widened matcher: a level-5 (#####) header must
  not count, an unlabeled #### inside a fence must not count, an optional
  Scenario: label normalizes (relabel is not a loss), and unlabeled scenarios
  are counted by multiplicity. Plus an integration case: a dropped labeled
  scenario is caught even when an unlabeled sibling is kept (validate/archive
  parity, both directions).

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

* test(validate): harden scenario-name folding + incoming-fence parity

Adversarial review of the scenario-loss guard surfaced one over-strict nit
and one untested symmetry:

- scenarioNameAt now also strips a CommonMark closing `#` run, so `#### Foo`
  and `#### Foo ####` fold to the same scenario name. Without this, relabeling
  a scenario's header on one side (ATX-open vs ATX-closed) read as a dropped
  scenario — a false-abort. Safe direction only: a genuine drop still lowers a
  folded name's count and is caught.
- Add unit tests for the untested incoming-side fence mask (a fenced `####` in
  the MODIFIED block must not satisfy a real scenario), lowercase `scenario:`
  label normalization, and the ATX-closed header fold.

Behavior for conventional `#### Scenario:` headers is unchanged; parser,
validation, and archive suites stay green (269 tests).

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

* fix(validate): match CommonMark whitespace in ATX-close strip; changeset nit

Second adversarial-review round follow-ups:

- scenarioNameAt's ATX-closing-sequence strip now matches only a space/tab
  before the trailing `#` run (`[ \t]` not `\s`), exactly as CommonMark defines
  a closing sequence. A looser `\s` could strip a `#` run after an exotic space
  (e.g. NBSP) that CommonMark keeps rendered, folding two distinct scenario
  names into one and masking a real loss. Correct-direction hardening for a
  data-loss guard; no behavior change for real space/tab-authored headers.
- Changeset: describe the header whitespace outside the code span to satisfy
  markdownlint MD038 (no trailing space inside `#### `). Resolves CodeRabbit.

Parser/validation/archive suites green (243 tests).

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

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-11 21:22:45 +00:00
Clay GoodandClaude Opus 4.8 07dea6ed2f fix(update): don't hijack the agents target on legacy Codex upgrade (#1522)
* fix(update): don't hijack the agents target on legacy Codex upgrade

Codex and the vendor-neutral `agents` target share `.agents/skills`. In
upgradeLegacyTools, a Codex install inferred only from global ~/.codex/prompts
wrote Codex skills into `.agents` and flipped the ownership marker
agents -> codex, silently rewriting an existing agents-owned tree. The main
generation path reconciles shared-target ownership first; this legacy-upgrade
path did not. Add sharedSkillRootOwnedByOther() and skip generation when a
different tool already owns the shared root (marker or existing tree), while
still allowing a genuine first-time Codex upgrade with no `.agents` yet.

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

* test(update): cover the hijack guard end-to-end; name the owner on skip

Harden the agents-target ownership fix after a multi-agent review:
- Add an integration test that runs the real update flow for the bug
  scenario (agents-owned .agents + a legacy global Codex prompt) and asserts
  the marker stays `agents` and skills keep generic `/openspec-` syntax. A
  unit test of the predicate can't catch a future refactor that stops calling
  it; this can.
- Name the owning tool in the skip message ("...managed by another tool
  (Shared .agents skills)") via a new sharedSkillRootOwner() helper that
  sharedSkillRootOwnedByOther now delegates to.
- Add a unit case for the ambiguous-tree branch (existing skills, no marker,
  no inferable syntax) and one asserting sharedSkillRootOwner names agents.
- Document the known, harmless re-offer tradeoff (a skipped tool isn't
  recorded as configured, so a persistent legacy prompt re-offers it).

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

* fix(update): preserve skipped tool's legacy files; add upgrade-path tests

Adversarial review of the shared-root ownership guard surfaced one real
integration defect and the review asks from alfred/CodeRabbit.

Defect: when the guard skips a legacy Codex upgrade because the `.agents`
root is owned by another tool, the caller's immediate legacy cleanup still
deleted Codex's repo-local `.codex/prompts/openspec-*.md`. That violates the
cleanup contract (remove X only because replacement Y was written): no
replacement is written for a skipped tool, so its legacy files must stay.

`upgradeLegacyTools` now reports `skippedSharedSkillTools`, and
`performImmediateLegacyCleanup` exempts those tools' repo-local artifacts via
a new `omitToolLegacyArtifacts` helper. Refactored the per-artifact tool
matching out of `getToolsFromLegacyArtifacts` so both share one matcher.

Tests (addressing the review + the defect):
- update.test.ts: hijack test now asserts Codex is absent from the persisted
  configured-tool set and that the skip names the established owner.
- update.test.ts: inverse no-root case proves a first-time Codex upgrade still
  writes the `codex` marker via the real UpdateCommand path.
- update.test.ts: a skipped tool's repo-local `.codex/prompts` is preserved.
- legacy-cleanup.test.ts: unit coverage for omitToolLegacyArtifacts.
- shared-skill-target.test.ts: assert sharedSkillRootOwner resolves 'agents'.

Docs + changeset updated to describe the preserve-on-skip behavior.

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

* test(update): lock in legacy-prompt preservation on skipped Codex upgrade

Address the outstanding CodeRabbit review notes on #1522. The fix itself
is confirmed correct by three independent adversarial reviews — these are
test-only hardening that locks in the guarantees the fix promises:

- Assert the global ~/.codex/prompts survives (byte-for-byte) in the
  hijack scenario. Previously the test set the prompt up but never
  checked it was preserved; on unfixed code Codex would be generated,
  its 'explore' workflow would read as installed, and the deferred
  global cleanup would delete the prompt — so this assertion fails
  without the fix.
- Assert the repo-local .codex/prompts is preserved by content, not
  mere existence (distinguishes 'left untouched' from 'deleted+rewritten').
- Restore the stdout/stderr spies in a finally so a throw can't swallow
  output for the rest of the suite.
- Cover backslash-delimited (Windows) paths in omitToolLegacyArtifacts.

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

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-11 21:22:32 +00:00
Clay GoodandClaude Opus 4.8 bf5099e39f fix(apply): surface deferred scope instead of silently simplifying tasks (#1530)
* fix(apply): surface deferred scope instead of silently simplifying tasks

The /opsx:apply guidance told agents to keep going through tasks but never
told them what to do when a task turned out harder than the spec assumed.
Agents absorbed the extra scope silently — narrowing, deferring, or
declaring partial work done — and marked the task complete anyway (#1529).

Add a pause trigger and two guardrails to the shared apply instructions
(rendered identically by the skill and command surfaces): surface the added
scope and ask rather than simplify to fit, and mark a task complete only
when it is fully implemented as specified. Regenerate the static skill and
parity-hash pins. Guidance text only — no behavioral code paths change.

Fixes #1529

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

* fix(apply): anchor deferred-scope guidance to spec scope, not effort

Adversarial review flagged that "more complex than the spec assumed" could
be read as "takes more effort than I guessed," which would make an agent
pause on nearly every task. Retie the pause trigger and guardrail to a
change in scope — work beyond what the spec/tasks describe, or dropping /
narrowing / deferring specified behavior — so normal implementation effort
does not trip it. Regenerate the static skill and parity pins; update the
regression test and changeset to match.

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

* fix(apply): name the "accept exceptions" pattern in deferred-scope guidance

Issue #1529's concrete example is an agent that found three exceptions to a
"zero writes on the main thread" task, declared them "accepted," and moved
on. Add "accept exceptions to" to the pause trigger's verb list so the
guidance names that exact failure mode, not just drop/narrow/defer. Behavior
is otherwise unchanged; regenerate the static skill and parity pins.

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

* test(apply): assert the deferred-scope guidance requires pausing

CodeRabbit noted the guardrail test checked that added scope is surfaced but
not that the agent pauses, so it could pass if the workflow reported scope and
kept going. Assert the exact "surface the added scope and pause" phrasing.

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

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-11 20:53:53 +00:00
Clay GoodandClaude Opus 4.8 9ae75c86ef fix(archive): don't write ANSI escape codes to a redirected (non-TTY) stdout (#1603)
* fix(archive): stop non-TTY confirm prompts from writing ANSI escapes to stdout

`openspec archive` asks up to three yes/no questions through @inquirer's
`confirm`, which renders by writing ANSI cursor-movement escape sequences —
and emits them even when stdout is not a TTY. When archive runs with its
output captured to a file or pipe (an agent's background task, CI), those
escapes are noise, and in some non-TTY hosts the render loop never settles
and repeats `ESC[NNG` moves until the disk fills (reporter hit 19.8 GB).

Add `confirmPrompt` in interactive.ts: a real terminal (stdin AND stdout
TTY) still gets @inquirer's rich prompt; every other case reads one plain
line via node:readline with `terminal:false`, emitting no escapes. Parsing
mirrors @inquirer/confirm exactly (prefix match on y/yes and n/no, else the
default), and an unreadable stdin rejects with an ExitPromptError-shaped
error so the existing #1479 "rerun with --yes" guidance is unchanged.
archive's confirmOrBlock now calls confirmPrompt.

Closes #1526

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

* test(interactive): cover Windows CRLF and drained-stdin paths; doc note

Adds two regression tests surfaced by adversarial review of the #1526 fix:
- Windows CRLF piped input (`y\r\n`) parses as a clean yes with no ANSI —
  the reporter's platform, previously untested (all inputs used `\n`).
- A second prompt after stdin was already drained blocks with an
  ExitPromptError instead of hanging, exercising the readableEnded guard.

Also documents in troubleshooting.md that a redirected/agent archive run
that pipes an answer no longer writes terminal escape codes into the capture.

Refs #1526

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

* fix(interactive): align non-interactive classification and handle readline errors

Addresses two review findings on the #1526 confirm-prompt fix:

- confirmPrompt drops to the plain reader whenever either stream is not a
  TTY, but isNonInteractivePromptError only checked stdin. A stdin-TTY /
  stdout-redirected run that hit EOF leaked the raw ExitPromptError instead
  of the #1479 "rerun with --yes" guidance. Classification now also counts a
  redirected stdout, matching how the prompt mode is chosen. (isInteractive,
  used broadly elsewhere, is left untouched.)

- readYesNo never listened for the readline/input 'error' event, so a stdin
  error would hang the promise (and go unhandled). It now settles with the
  underlying fault, guarded so the promise resolves or rejects exactly once.

Tests: TTY-stdin/redirected-stdout EOF is classified non-interactive; an
erroring input stream rejects instead of hanging; the archive usable-terminal
test now models a full terminal (both streams TTY).

Refs #1526

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

* fix(archive): gate the change picker on a TTY and tidy the reader

Follow-ups from a second review round:

- selectChange (the no-argument change picker) called @inquirer's `select`
  unconditionally. `select` writes ANSI escapes to stdout even when redirected
  — the same #1526 mechanism the confirm prompts were fixed for — so
  `openspec archive > log.txt` with no change name still spewed cursor moves
  into the capture before blocking. Refuse before rendering when either stream
  is not a TTY, with the same "pass a change name / --yes" guidance the caught
  ExitPromptError already gives. A new test asserts the picker is never
  reached in a non-terminal run.

- readYesNo now removes its input-stream 'error' listener on every settle path
  (it lives on the long-lived process.stdin) and closes the readline interface
  on error too, so nothing accumulates across archive's sequential prompts.

- troubleshooting.md now notes the picker also stays clean.

Refs #1526

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

* chore(changeset): add patch changeset for the archive non-TTY fix (#1526)

User-facing patch note for the archive ANSI/disk-fill fix. Also drops an
unnecessary optional-chain on the non-nullable readline handle in readYesNo
(the listener is only attached after the interface exists).

Refs #1526
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-11 20:53:41 +00:00
Clay GoodandClaude Opus 4.8 83be9d113e feat(validate): add --archived to lint task completion of archived changes (#1604)
* feat(validate): add --archived to lint task completion of archived changes

`openspec validate --archived` scans every change under changes/archive/
and fails (exit 1) if any has unchecked tasks in tasks.md. This catches
changes archived with unfinished work — which the normal validate flow
never sees, since it only looks at active changes — and is meant for a
pre-commit or CI hook.

It is a standalone, opt-in scope: it returns before any existing bulk
path, so no current `validate` invocation changes behavior, and it does
not re-validate already-applied spec deltas. Reuses getTaskProgressForChange
(the same counter status/list/archive use) so task counting never forks,
and reads root.archiveDir so it is store-aware.

Closes #205

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

* fix(validate): fail loudly on archive read errors and unreadable task files

Address adversarial + CodeRabbit review of `validate --archived`:

- listArchivedChangeIds now returns [] only for ENOENT (missing archive
  dir) and rethrows permission/I/O/ENOTDIR errors, so a real archive-read
  failure exits 1 instead of silently reading as "no archived changes".
- Add getTaskProgressDetailForChange, which reports task files that exist
  but cannot be read; --archived turns those into an ERROR (naming the
  file) rather than silently counting them as zero tasks. The shared
  getTaskProgressForChange now wraps it and drops the detail, so
  status/list/archive totals are byte-identical.
- Start the spinner after listing so a thrown listing error never leaves
  a spinner running.

Adds regression tests (archive path is a file; archived tasks.md is
unreadable) and unit tests for the new detail variant. Docs: align the
--archived table verb and add a troubleshooting one-liner.

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

* test(validate): address CodeRabbit nits on archived-task tests

- Build the unreadable-fixture path from separate path.join components
  instead of a hard-coded Unix-separator string.
- Assert the reported unreadable path (canonicalized with
  realpathSync.native), not just the count, so a wrong path can't pass.

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

* refactor(validate): address round-3 review of --archived

From three fresh adversarial reviews (scale/perf, flag/output-shape,
filesystem/security):

- perf: memoize schema→glob resolution across archived changes via a
  run-scoped SchemaGlobCache, so the same schema.yaml isn't re-parsed
  once per change (the archive is append-only and can hold thousands).
  Threaded as an optional arg; existing callers are unchanged. Loop stays
  sequential by design (per-change work is synchronous) — now documented.
- output shape: issue `path` now follows validate's convention —
  'tasks.md' for incomplete tasks, and the POSIX root-relative file path
  for an unreadable file (one issue per file) instead of the bare 'tasks'.
- plain output: print `change/<id>` (matching the JSON `type` and bulk
  validation) instead of `archived/<id>`.
- docs: correct the "Never throws" docstrings (glob resolution can throw
  on a malformed/unsafe schema; the caller guards it) and note the
  load-bearing projectRoot override for the archive path depth.

Store-mode resolution confirmed correct by review. Tests updated + a memo
regression test added.

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

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-11 20:53:06 +00:00
Clay GoodandClaude Opus 4.8 59c16a4461 feat(tools): add Command Code command adapter for /opsx-* commands (#1622)
* feat(tools): add Command Code command adapter for /opsx-* commands

Command Code documents custom slash commands under
`.commandcode/commands/`, where the command name is the markdown
filename without its `.md` extension (see
https://commandcode.ai/docs/reference/slash-commands). That is the same
flat naming Cursor and OpenCode use, so a standard flat adapter writing
`.commandcode/commands/opsx-<id>.md` registers `/opsx-<id>`.

Registering the adapter flips Command Code from `none` to
`adapter-backed`, so with the default `both` delivery `openspec init`
now generates OpenSpec commands alongside the skills it already installs
under `.commandcode/skills/`.

Builds on #1613, which registered Command Code as a skills-only tool.

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

* fix(tools): preserve Command Code command arguments

* test(command-code): cover commands-only delivery and openspec update

Addresses review: prove the Command Code adapter survives both the
commands-only init path and the update path, not just default delivery.

- init: delivery=commands generates .commandcode/commands/opsx-explore.md
  and installs no skills.
- update: a detected .commandcode install regenerates the flat
  opsx-<id>.md command (plain Markdown, $ARGUMENTS injected).

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

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-11 20:52:25 +00:00
Ángel PeñaandCommandCodeBot 42d7f673bc feat(tools): add Command Code support as a skills-only tool (#1613)
* feat(tools): add Command Code support as a skills-only tool

Register Command Code in AI_TOOLS with skillsDir `.commandcode`, so
`openspec init`/`update` install the OpenSpec skills where Command Code
discovers them (.commandcode/skills/<name>/SKILL.md) and reference them
with the `/openspec-*` invocations its skill surface registers.

No command adapter: Command Code has no slash-command files, so the
skills-only target behaves like other adapterless tools and reports
"Commands skipped for: command-code ^(no adapter^)".

Co-authored-by: CommandCodeBot <noreply@commandcode.ai>

* docs(supported-tools): add Command Code to skills-only invocation table

Keeps the How-To-Invoke table consistent with the Tool Directory
Reference row added for Command Code.

Co-authored-by: CommandCodeBot <noreply@commandcode.ai>

---------

Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
2026-08-11 20:26:16 +00:00
aliouswe e50bd0983d fix(validate): warn on ambiguous task numbering (#1523)
* fix(validate): warn on ambiguous task numbering

* fix(validate): honor task numbering review boundaries
2026-08-07 13:10:54 +00:00
openspec-release-bot[bot]andgithub-actions[bot] d57889664c Version Packages (#1488)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-08-05 20:55:58 +00:00
Clay GoodandClaude Opus 4.8 568e56c672 chore(release): add catch-up changeset for Rovo, Codex dir, status (#1518)
* chore(release): add catch-up changeset for Rovo, Codex dir, status

Cover three user-facing PRs that merged without changesets so they
appear in the v1.8.0 CHANGELOG:

- #1516 Atlassian Rovo Dev CLI (new tool)
- #1511 Codex skills move to shared .agents directory
- #1505 openspec status separates planning from implementation

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

* chore(release): correct isPlanningComplete wording in changeset

Skipped planning artifacts count as satisfied without being written; say
"every non-skipped planning artifact exists" to match the CLI and
agent-contract docs (alfred/CodeRabbit review).

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

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-05 20:38:36 +00:00
Clay GoodandClaude Opus 4.8 73207a6f2c feat(copilot): make cloud coding-agent files opt-in (#1517)
* feat(copilot): make cloud coding-agent files opt-in

Selecting the `github-copilot` tool auto-generated a GitHub Actions
workflow (.github/workflows/copilot-setup-steps.yml) plus an agent file.
Writing into a user's CI on init/update is invasive, benefits only the
narrow set of Copilot *cloud* coding-agent users, and couples us to
GitHub's externally-owned custom-agent format.

Cloud files are now opt-in:
- `openspec init` prompts before generating them (default No) and records
  the choice in openspec/config.yaml (`githubCopilot.cloudAgent`).
- `--copilot-cloud` / `--no-copilot-cloud` decide non-interactively.
- `openspec update` never prompts; it only refreshes files for projects
  that opted in, or that already have generated cloud files (so existing
  setups keep working — the migration path).

The pre-existing content-matching guarantees are unchanged and now proven
by regression tests: a user-customized cloud file is never overwritten or
deleted. Opt-in state is persisted via the YAML document model so the
user's hand-authored config comments and formatting survive untouched.

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

* feat(copilot): polish the cloud opt-in — safety, UX, and docs

Follow-up hardening driven by a five-agent review swarm over the opt-in.

Correctness:
- persistCopilotCloudOptIn no longer throws on a scalar/`null` config file
  (reproduced crash); it starts a fresh map while preserving comment-only
  and empty files.
- Explicit opt-out (`--no-copilot-cloud` / `cloudAgent: false`) now removes
  OpenSpec-managed cloud files on both init and update, instead of orphaning
  them. Customized files are still never touched.
- `--copilot-cloud` / `--no-copilot-cloud` warns when github-copilot isn't
  among the selected tools, instead of silently no-opping.

UX / discoverability:
- init prints whether cloud files were written or, when skipped for want of
  a signal, how to enable them (`--copilot-cloud`).
- When the user opts in but already has their own copilot-setup-steps.yml or
  agent file, init/update say it was left untouched and that the OpenSpec
  install step must be added by hand — the direct answer to "will this affect
  my existing Copilot cloud agent?".
- Clearer interactive prompt (names both files; distinguishes the GitHub-hosted
  cloud agent from Copilot in the editor); a dim, interactive-only, decision-
  gated hint on `openspec update`; tightened flag help text.

Docs (the feature was undocumented): new "GitHub Copilot cloud coding agent"
section in supported-tools.md; init flags in cli.md; the githubCopilot.cloudAgent
key in customization.md.

Tests: interactive prompt (accept/decline), opt-out removal + customized-file
preservation, config.yml variant, scalar-config regression, collision
reporting, flag-ignored warning, re-init honoring persisted opt-in, and the
config parse/warn branches. 2763 tests pass; the only failures are pre-existing
and unrelated (completion mocks, adapters loader, one config-profile PATH case,
one experimental-alias case), verified identical on clean main.

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

* fix(copilot): make init cloud-file output honest; harden config guard

Final hardening pass (adversarial review of the opt-in polish).

- init's success line listed both cloud-file paths from the *decision* to
  write, not from what was written — so it claimed files that a write
  skipped (user already owns them) or that the alternate-agent path removed.
  It now lists only OpenSpec-managed files that actually exist after the
  write (listManagedCloudFiles), keeps the "left untouched" caveat for
  user-owned files, and reports opt-out removals in the normal output block.
- persistCopilotCloudOptIn's non-map guard used isCollection, which is also
  true for sequences, so a YAML list at the config root still made setIn
  throw. Gate on isMap so scalars AND sequences fall back to a fresh
  document; empty/comment-only files still round-trip with comments intact.
- Fixed a misleading catch comment on the opt-out removal path.

Tests: success-line accuracy over a user-owned file, sequence-root config
regression, and listManagedCloudFiles coverage. 318 tests pass across the
touched suites; build + lint clean.

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

* fix(copilot): replace a non-map githubCopilot node before setIn

Addresses alfred review on #1517. The prior guard only fixed a non-map
config *root*; a valid top-level map whose `githubCopilot` value is itself a
scalar/null/sequence (`githubCopilot: false`, `null`, or a list) still made
`setIn(['githubCopilot','cloudAgent'], ...)` throw, which init swallowed —
so the explicit opt-in/out was never saved. Now the intermediate node is
replaced with an empty map before descending, keeping the rest of the config
and its comments intact.

Regression covers all three reproduced cases (false/null/sequence). Full
suite: 2770 pass; only the pre-existing unrelated failures remain.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(copilot): never throw persisting into an unparseable config

Deeper pass on persistCopilotCloudOptIn (the function alfred flagged), driven
by an exhaustive input-shape check. Two malformed inputs still threw at
toString(): a multi-document YAML stream and a tab-indented (syntactically
invalid) file. Such a file can't be edited without corrupting it, so persist
now detects parse errors and leaves it untouched (no throw, no clobber) — it
is already invalid, so readProjectConfig ignores it regardless.

With this the function is throw-free across every shape exercised: empty,
comment-only, scalar/sequence root, a non-map githubCopilot value, anchors,
CRLF, BOM, and the two malformed cases (now skipped byte-identical).

Regression added for the multi-document case. Touched suites: 314 pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-05 18:51:26 +00:00
Clay GoodandClaude Opus 4.8 13e213e00f feat(tools): add Atlassian Rovo Dev CLI as a first-class tool (#1516)
* feat(tools): add Atlassian Rovo Dev CLI as a first-class tool

Rovo Dev CLI loads project Agent Skills from `.rovodev/skills/<name>/SKILL.md`
(Atlassian docs), the same SKILL.md format OpenSpec generates. It was usable
only via the generic "Shared .agents skills" fallback; this makes it a named,
selectable target in `openspec init`.

Rovo has no slash-command surface, so it is registered as an adapterless
skills-only tool (like CodeArts/ForgeCode/Hermes) — no command adapter.

Closes #212

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(tools): reference Rovo skills by natural language, not dead slash commands

Rovo Dev CLI has no slash-command surface — it matches skills
automatically or by prompt, and `/skills` only manages them. The
generated skills and the getting-started hint still advertised
`/openspec-*` slash commands (18 references across the skill bodies plus
the "Start your first change" hint), so every one was a dead command.

Adds a natural-language skill-reference path for no-slash tools:
`/opsx:<id>` now renders as "the openspec-<skill> skill" for rovodev, in
both skill bodies and the init hint. Other tools are unchanged.

- src/utils/command-references.ts: NATURAL_LANGUAGE_SKILL_TOOLS +
  usesNaturalLanguageSkillReferences(); getSkillReferenceTransformer
  returns the prose transformer for rovodev.
- src/core/init.ts: phrase the skills-only hint as an instruction for
  no-slash tools ("ask Rovo Dev CLI to use the openspec-propose skill…").
- docs/supported-tools.md: correct the Rovo row (was "use skill-based
  /openspec-* invocations").
- tests: assert generated Rovo skills contain no /openspec-* or /opsx
  slash tokens, the hint advertises no dead command, and the transformer
  emits prose.

Addresses alfred-openspec review on #1516.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-05 17:11:21 +00:00
Clay GoodandClaude Opus 4.8 96a6548664 refactor(templates): share one apply instruction body across skill and command (#1515)
* refactor(templates): share one apply instruction body across skill and command

The apply skill and command templates each carried a full ~150-line copy of
the same instruction body, differing in exactly one line (the `contextFiles`
note). Two near-identical copies invite silent drift.

Author the body once in `getApplyInstructions(contextFilesNote)` and render it
per surface, passing each surface's own note. The single intentional wording
difference stays explicit as a named constant, and further per-surface
parameters can be added here as the surfaces evolve — the skill and command
remain distinct templates.

Pure refactor: the generated skill and command output is byte-identical to
before (SKILL.md and all parity hashes unchanged). Added a contract test that
fails both if the shared body drifts between surfaces and if the intentional
contextFiles difference is flattened away.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* refactor(templates): unify apply instruction body into one shared core

Builds on the shared-core extraction: the apply skill and command still each
carried a slightly different `contextFiles` note (skill spelled out example
artifact sets, command said only "varies by schema"). That difference was
long-standing accidental drift between the two copies, not an intentional
surface distinction — the surfaces are meant to differ only in how they are
invoked, which the generation transformers already handle downstream by
rewriting `/opsx:<id>` tokens per surface.

Resolve the drift by unifying on the more informative note, so both surfaces
render one shared `getApplyInstructions()` body with no per-surface text.
Skill output is unchanged; the command's contextFiles note gains the example
artifact sets. Updated the contract test to assert both surfaces render the
shared core (no silent template-level drift), and regenerated the command
function hash accordingly.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-05 15:50:11 +00:00
aliousweandClay Good d9bcc18582 docs(stores): add multi-repo implementation flow (#1491)
* docs(stores): add multi-repo implementation flow

* docs(stores): qualify project pointer precedence

---------

Co-authored-by: Clay Good <hi@claygood.com>
2026-08-05 15:25:12 +00:00
FasterPHPandMarcus Don 622c509a13 fix(telemetry): honor telemetry.enabled in global config (#1513)
* fix(telemetry): honor telemetry.enabled in global config

Honor the documented global config opt-out while preserving environment and
CI overrides. Keep runtime-managed telemetry identity fields intact and apply
the same privacy setting to update checks.

AI: agentic

* docs(telemetry): address automated review feedback

Document the full opt-out behavior in the changeset and describe the new
test helper so automated documentation coverage meets the project threshold.

AI: agentic

---------

Co-authored-by: Marcus Don <marcus.don@team.blue>
2026-08-05 15:23:41 +00:00
Clay GoodandClaude Opus 4.8 06b310bf57 fix(templates): restore intentional apply skill/command separation (#1514)
* fix(templates): restore intentional apply skill/command separation

Revert the deduplication from #1153. Skills and commands are different
ways to invoke the apply workflow: commands reference /opsx:*, while
skills reference other skills by name and avoid /opsx: (a skill may be
installed without the commands). Teams choose skills-only, commands-only,
or both through profiles, so generating both is intentional, not drift.

#1153 collapsed getApplyChangeSkillTemplate() and getOpsxApplyCommandTemplate()
into one shared body and added a test asserting they are byte-identical,
erasing four deliberate differences (change-name example, contextFiles
note, blocked-state pointer, and completion hint). This restores the two
separate templates and removes the identical-body assertion.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(templates): keep apply skill invocations transformable per target

Address alfred's review on #1514. A plain revert of #1153 restored the
skill template's bare `openspec-continue-change` prose and dropped the
archive/input invocations. The generator only rewrites canonical
`/opsx:<id>` tokens, so bare prose is dead text for skills-only targets:
skills.sh, Codex, and Kimi lost valid continue/apply/archive invocations.

Keep the skill and command templates split (no shared constant, no
identical-body assertion — the design separation #1153 erased stays
reverted), but author the skill's three invocation references as
transformable `/opsx:*` tokens. The generator now emits the correct
per-target skill invocation: `/openspec-continue-change` (default),
`$openspec-continue-change` (Codex), `/skill:openspec-continue-change`
(Kimi) — i.e. "invoked as skills," spelled for each tool.

Regenerated the static SKILL.md and parity hashes, and added
default/Codex/Kimi generation regressions that pin the apply skill's
per-target invocations so this break can't recur silently.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-05 12:59:33 +00:00
Clay Good 59bfb27a76 fix(codex): install skills in canonical agents directory (#1511)
* fix(codex): install skills in canonical agents directory

* fix(codex): preserve shared agents compatibility

* fix(codex): harden shared skill migration

* fix(codex): preserve customized legacy skills

* fix(codex): reject malformed generated versions
2026-08-05 01:43:49 +00:00
161f9454a3 feat: add MiniMax Code skills support (#1214)
* feat: add MiniMax Code skills support to OpenSpec

* fix: separate init skill and command output summaries

* feat(minimax): add global skills support

---------

Co-authored-by: showms <showms@users.noreply.github.com>
Co-authored-by: Clay Good <hi@claygood.com>
2026-08-05 01:01:36 +00:00
SHASHANK DWIVEDIandClay Good 0b233efb86 fix(templates): deduplicate apply skill and command instructions (#1153)
* fix(templates): deduplicate apply skill and command instructions

Extract shared APPLY_INSTRUCTIONS constant so skill and command
templates reference the same string. Eliminates content drift
reported in #1139.

* fix(templates): update parity hashes after parameterizing apply instructions

* test(templates): add normalized body parity assertion for apply skill vs command

* docs(templates): add JSDoc to getApplyInstructions

* docs(templates): add JSDoc to all functions in apply-change

* fix(templates): parameterize /opsx:apply examples and add regression tests for skill /opsx: references

---------

Co-authored-by: Clay Good <hi@claygood.com>
2026-08-05 00:48:30 +00:00
NicoAvanzDevandClay Good 7a4a745d80 feat: generate Copilot coding agent files on openspec init (github-copilot) (#1274)
* feat: generate copilot cloud agent files when github-copilot tool is selected

When `openspec init` or `openspec update` is run with the github-copilot
tool selected, two additional files are now generated in the user's project:

1. `.github/workflows/copilot-setup-steps.yml` - A GitHub Actions workflow
   that pre-installs the OpenSpec CLI in the Copilot coding agent's
   ephemeral environment (required for the agent to use `openspec` commands).

2. `.github/agents/openspec.agent.md` - A custom agent definition that
   instructs the GitHub Copilot coding agent how to use the OpenSpec CLI,
   including all agent-compatible commands with `--json` output, workflow
   patterns, and best practices.

These files are only written if they don't already exist (to preserve
user customizations). The generation is non-fatal — if it fails, init/update
still completes successfully.

New module: src/core/github-copilot/cloud-agent.ts
Tests: test/core/github-copilot-cloud-agent.test.ts

* fix: wire up removeCopilotCloudFiles in update flow

When github-copilot is not in the configured tools during update,
remove the cloud agent files (copilot-setup-steps.yml and
openspec.agent.md) if they exist.

* fix: refresh Copilot cloud agent restore

* fix: address Copilot cloud review feedback

* fix: recognize legacy Copilot cloud files

* fix: harden Copilot legacy file matching

* fix(copilot): harden cloud agent file management

* fix(copilot): harden cloud agent file handling

---------

Co-authored-by: Clay Good <hi@claygood.com>
2026-08-05 00:41:30 +00:00
Ismet TogayandClay Good 3e50944fb0 fix(build): allow esbuild install scripts (#1196)
* Add pnpm-workspace.yaml to allow esbuild build scripts

pnpm 10+ blocks all dependency build scripts by default unless explicitly
approved via allowBuilds or onlyBuiltDependencies in pnpm-workspace.yaml.

esbuild (transitive dependency of vitest -> vite) has a postinstall script
that downloads a platform-specific native binary. Without this config,
pnpm install exits non-zero with [ERR_PNPM_IGNORED_BUILDS], breaking any
downstream packaging (AUR, Nix, Docker) or local setup using pnpm >=10.

Refs: #1195

* fix(build): declare pnpm workspace root

* fix(build): harden pnpm workspace policies

---------

Co-authored-by: Clay Good <hi@claygood.com>
2026-08-05 00:18:37 +00:00
Clay Good 02b124e6b6 fix(security): patch fast-uri, postcss, and brace-expansion advisories (#1510)
* fix(security): patch fast-uri, postcss, and brace-expansion advisories

Resolve the two open Dependabot alerts plus a third high-severity advisory
the repo's own audit surfaces but Dependabot had not filed, all via
version-ranged pnpm overrides (they lapse once the upstream tree moves past
them):

- fast-uri 3.1.4 -> 3.1.5 (website): GHSA-7p8r-x3mc-p8w7, high. Host
  confusion via backslash authority introducer. Pulled in transitively by
  ajv@8.18.0; bounded to ^3.1.5 so it stays on the 3.x line ajv expects.
- postcss 8.5.22 -> 8.5.25 (root): GHSA-fxqj-rqcc-2cmp, moderate. Arbitrary
  .map file read via attacker-controlled sourceMappingURL. Pulled in by
  vite (dev/test tooling).
- brace-expansion 5.0.8 -> 5.0.9 (website): GHSA-rgw5-rvv9-x895, high. DoS
  via unbounded recursion. The existing override capped at >=5.0.8, and
  5.0.8 is itself vulnerable under this newer advisory; the root already
  resolved to 5.0.9.

Root and website audits are clean at --audit-level high (and any-severity
for the website). Full test suite: 3662 passing.

* harden(security): bound overrides, scope release perms, add website lockfile drift check, document archive TOCTOU intent

Hardening pass over the security fixes, from a parallel review of the
dependency, CI, archive, and adjacent-code surfaces. Each item is low-risk
and verified; resolved dependency versions are unchanged.

- deps: bound the three security overrides to their current major
  (brace-expansion ">=5.0.9 <6", postcss ">=8.5.23 <9"). A bare ">=X" pin
  would take a future major on the next lockfile regen without review; the
  website already models the caret-bounded idiom.
- ci: scope release-prepare.yml permissions per job. The top-level block
  dropped "pull-requests: write"; only the "prepare" job (which opens the
  Version Packages PR) now holds it. The "beta" job only tags/releases and
  publishes via OIDC, so it inherits the narrower default (least privilege).
- ci: add a "Website Lockfile Drift" job to security.yml. The website keeps
  its own lockfile and is never installed in CI, so a website override that
  stops resolving would go unnoticed and `pnpm audit` would scan a stale
  graph. A `pnpm install --frozen-lockfile --ignore-scripts --dir website`
  fails fast on that drift (root drift is already caught in ci.yml).
- archive: add intent comments at the 7 js/file-system-race sites in
  src/core/archive.ts. The stat->read->re-stat pattern is a deliberate
  concurrent-change detector; the comments record why, so no future refactor
  (human or scanner-driven) collapses it to fd I/O and blinds the guard.

Verified: 3662 tests pass, build clean, website build clean, root+website
audits clean at --audit-level high, and the new frozen-lockfile check passes
locally.

* chore(nix): refresh pnpmDeps hash for the lockfile change

The root pnpm-lock.yaml changed (postcss + brace-expansion overrides), which
stales the fixed-output pnpmDeps hash and fails Nix Flake Validation. Repin to
the value CI computed from the new lockfile.
2026-08-04 22:52:40 +00:00
Clay Good 3d0701f871 fix(workflows): preserve nested spec paths (#1508)
* fix(workflows): preserve nested spec paths

* fix(workflows): key conflicts by capability path

* fix(workflows): preserve full paths in examples

* fix(workflows): clarify nested path inputs

* test(workflows): align parity hashes after rebase
2026-08-04 21:44:14 +00:00
Clay Good 8a3850da73 fix(explore): scaffold changes before capturing artifacts (#1503)
* fix(explore): scaffold changes before capturing artifacts

* fix(explore): harden artifact capture guidance

* fix(explore): evaluate conditional prerequisites

* fix(explore): retain store during artifact capture

* fix(explore): propagate store in follow-ups

* test(explore): align parity hashes after rebase
2026-08-04 21:25:54 +00:00
Clay Good afea111cd4 fix(status): clarify planning completion (#1505)
* fix(status): clarify planning completion

* test(status): cover skipped planning artifacts

* fix(workflows): gate archive guidance on implementation

* fix(status): clarify human completion message

* fix(status): make completion guidance stage-neutral

* test(status): align parity hashes after rebase
2026-08-04 21:02:50 +00:00
Clay Good f43fe0e7d5 fix(propose): use the requested workflow schema (#1504)
* fix(propose): honor explicit schema selection

* fix(propose): harden schema selection guidance

* fix(propose): preserve selected store

* fix(propose): respect store flag support

* fix(propose): resolve schema discovery root

* fix(propose): preserve rootless schema discovery

* test(propose): align schema parity after rebase
2026-08-04 20:39:09 +00:00
Clay Good 0b20ae3964 fix(propose): wait for explicit implementation request (#1501)
* fix(propose): stop before implementation

* fix(propose): require explicit implementation request

* fix(propose): hand implementation to apply

* test(propose): align parity hashes after rebase
2026-08-04 20:13:59 +00:00
Clay Good 26bd1d4e5c fix(templates): correct generated workflow guidance (#1500)
* fix(templates): correct generated workflow guidance

* fix(templates): address workflow review feedback

* test(templates): pin store-aware commands

* fix(templates): harden generated workflow guidance

* test(templates): align parity hashes after rebase
2026-08-04 19:47:28 +00:00
Clay Good ece8660d44 fix(validate): allow non-English requirements (#1502)
* fix(validate): allow non-English requirements

* test(validate): cover non-English change deltas

* test(validate): distinguish missing bodies from guidance
2026-08-04 19:20:25 +00:00
Clay GoodandClaude Opus 5 521ee33e6e feat(archive): let a change retire a capability it empties (#1484)
* fix(archive): retire a capability when a change removes its last requirement

A delta whose REMOVED entries cover every requirement rebuilt the main spec
empty, and an empty spec fails validation ("Spec must have at least one
requirement"), so the archive aborted with no way forward. Pre-deleting the
main spec did not help: the delta was then treated as a create and landed on
the same empty spec.

Archive now treats an emptied capability as retired. It deletes the
capability's spec.md and any directory the deletion leaves empty, stopping
short of the specs root, and reports the removals in the totals. Nothing is
deleted unless this run actually removed a requirement, so a re-applied or
already-synced delta still leaves the file alone.

Closes #1302

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(archive): decide retirement from the validator and contain the deletion

Adversarial review found the original rule unsound. It retired whenever no
canonical `### Requirement:` blocks were left, but the validator counts
requirements differently: MarkdownParser accepts any `###` heading under
`## Requirements`, while the delta block parser indexes only canonical headers
and sweeps the rest into the preamble, which survives into the rebuilt spec. A
strict-valid spec could therefore be deleted on an archive that previously
succeeded. Retirement is now decided by putting the rebuilt spec to the
validator and retiring only when its sole error is that it has no requirements,
which makes "this spec could not have been written anyway" true by construction.

Also fixed:

- The directory prune walked string prefixes, but path.resolve does not resolve
  symlinks and readdir/rmdir both follow them, so a symlinked capability
  directory let it delete directories outside the repository. Pruning is now
  bounded by real paths and refuses to descend through a symlink.
- A spec that was already requirement-less and lost nothing this run is no
  longer skipped past validation; it aborts exactly as it did before.
- Deletions are deferred until every spec write has succeeded, so a later
  failure cannot leave a spec already deleted.
- Retirement is recorded in `warnings`, naming any other sections the deleted
  file held, so JSON consumers and humans can both see what went.
- Totals carry every applied operation; a rename applied on the way to the
  removal was being dropped.
- bulk-archive guidance, the sync/archive skill specs, and the docs that
  described archive as never deleting a spec.

Closes #1302

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(archive): close the retirement gaps a second review round found

Five adversarial reviews, mutation testing and CodeRabbit went at the reworked
retirement. The findings, all verified by repro before fixing:

- The archive-name collision check ran AFTER the spec merge, so archiving twice
  in one day deleted the capability's spec and then failed, leaving the change
  unarchived and the file gone. The destination depends only on the change name,
  so it is now settled before any spec is written or deleted - which also closes
  the same, older window for ordinary writes.
- `--no-validate` retired too, but the whole safety argument is the validator's
  verdict, and that path produces none. It now writes the spec exactly as it did
  before this feature existed, leaving no exception to the claim that nothing
  previously working changes.
- The validator can be talked out of seeing a requirement: a stray
  `### Requirements` under Purpose captures its section lookup, so a spec still
  holding a real requirement reported "no requirements" and was deleted. Any
  `###` heading left under `## Requirements` now vetoes retirement outright - a
  reader is not fooled by the stray heading even when the parser is.
- A dangling symlink made `update.exists` false (`fs.access` follows links,
  `unlink` does not), skipping the "removed something this run" guard: a run that
  removed nothing deleted an entry and reported a removal. The no-target case is
  now an explicit branch that never deletes, instead of an ENOENT probe.
- `findOtherSections` reported `## ` headings that were inside HTML comments and
  listed duplicates; it now masks comments like every other structural scan here
  and dedupes. The warning also names the `## Purpose`, which the deletion always
  takes, and the resolved path when a symlink puts the file outside the repo.
- A failed `unlink` surfaced a bare errno; it now says what was being attempted
  and what to do.

Tests grew from 19 to 33, killing every surviving mutant the review found:
deferral proven against a failing write (not just a failing validation), the
warnings payload, the already-gone path's output, multi-level pruning, the
`+ path.sep` boundary, a symlinked specs root, two retirements in one archive,
and `isRetirableSpec` unit-tested directly - including the two-error shape that
proves `every` rather than `some`.

Agent guidance, the three living specs and the docs now state the same
conditions the CLI applies, so a sync agent cannot delete a spec archive keeps.

Closes #1302

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(archive): make the write-failure test platform-neutral and the path note meaningful

Windows CI and CodeRabbit each caught one:

- `chmod 0o555` is not a write barrier on Windows, so the test that proves
  deletions are deferred until every write succeeds never failed a write there:
  the archive completed, the spec was retired, and the assertion blew up. It now
  puts a directory where the second spec's file belongs, which fails the write on
  every platform. Verified it still kills the reordering mutant.
- The "resolved to" note compared a canonicalized path against a merely resolved
  one, so any symlinked ancestor - the platform's own /var -> /private/var is
  enough - decorated an ordinary retirement with a path that says nothing. It now
  fires only when the spec really lived outside the specs tree, which is the fact
  the nominal path hides. Both directions are pinned by tests.

Closes #1302

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(archive): make the residual-heading veto position-independent

A third review round, scoped to the code the earlier rounds never saw.

The veto that is supposed to stop a retirement deleting hand-written content
only worked when that content sat ABOVE the first requirement. `parts.preamble`
is by definition the text before the first `### Requirement:` header; anything
after the last one belongs to that block's raw and is discarded with it, so the
rebuilt-body scan never saw it. Identical content, different position: one
aborted, the other was deleted silently. The veto now reads the original
Requirements section - preamble plus every block - so position does not matter.

Also:

- `realpath` follows a symlinked `spec.md` but `unlink` removes the link, so the
  warning declared it had deleted a file outside the repo that was still there.
  The note is now skipped when the target is itself a symlink.
- `findHeadings` masked HTML comments before code fences, so an unterminated
  `<!--` inside a fenced example blanked the rest of the document and truncated
  the very list of sections the deletion was reporting. Fence first, then
  comments.
- Moving the collision check before the merge widened the window between it and
  the move, where a claimed destination surfaced as a raw ENOTEMPTY and degraded
  to `archive_error`. `moveDirectory` now reports that as `archive_target_exists`,
  the same diagnostic the pre-flight check gives.

And a simplification the review asked for: the overlapping `retirable` /
`deletes` / `retired` booleans are now one `decideSpecOutcome()` returning
'write' | 'delete' | 'skip'. Behavior is identical - same clauses, same order -
but the fourth state that existed only as a comment is now a visible return.
Both guards were kept: the review constructed inputs where each is the sole
thing preventing a data-losing delete.

Two tests the review found wanting are gone or rewritten: one killed no unique
mutant, and one assertion straddled two editable message fragments and could
have gone vacuously true.

Closes #1302

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* test(archive): canonicalize both negative path assertions

CodeRabbit caught that `expect(warnings).not.toContain(shared)` passed
vacuously: on macOS the temp root lives under /var, whose realpath is
/private/var, so the warning would print a form the assertion never compared
against. The sibling assertion on `tempDir` had the same flaw.

Both now canonicalize first, and both were confirmed to fail against a mutant -
dropping the lstat guard, and forcing the resolved-path note on - which neither
did before.

Closes #1302

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(archive): move a retired capability's spec into the archive instead of deleting it

Retiring a capability was the first case where archiving deleted a file
under `openspec/specs/`. Nothing in the repo had ever removed spec content
before, so the blast radius of a wrong verdict was a lost file with only
the reflog to recover it.

The spec now moves instead. It is staged into the change directory, which
the archive step renames onto the archive path moments later, so it comes
to rest at `<archive>/retired-specs/<capability>/spec.md` beside the
proposal and tasks that retired it. `git` records a rename, and bringing a
capability back is a `git mv` from the archive.

Staged into the change rather than written to the archive path after the
move, because the archive path must not exist yet and the ordering is
safer: if a later step fails, the spec sits in a change that is still
active and a rerun carries it through, versus stranding the live specs
tree without a spec it still needs.

A symlinked `spec.md` is copied by content and its link removed, rather
than moved: relocating the link itself would archive a relative path that
no longer resolves from where it landed. A spec already staged by an
earlier aborted run is never overwritten - it is the only copy once the
live one moves.

The retirement verdict, its guards, and the deferral until every write has
succeeded are all unchanged.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(archive): clean up staging directories when a retirement move fails

The staging directories are created before the move, so any failure left an
empty `retired-specs/<capability>/` behind. That folder then rode into the
archive with the change, where it reads as a retirement that never happened -
a spec was supposedly retired here, and there is nothing to show for it.

The failure path now prunes back up to the change directory. Only empty
directories go, so a capability the same run already staged next to the
failing one is untouched, and the guard that refuses to overwrite a staged
spec still stops at a non-empty destination.

Both cases are covered by tests that fail without the prune: a dangling
symlink is the reproducible post-staging failure, since lstat sees a file and
the copy then follows the link and finds nothing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs(archive): say "moved" where the retirement path still said "deleted"

Three leftovers from the deletion version: the `residualRequirementHeadings`
comment, `pruneEmptyDirs`'s `mainSpecsDir` parameter - now a boundary that is
the change directory on the cleanup path, not the specs root - and a sentence
in writing-specs.md that used "deleted" for the requirement and then again for
the file, two lines apart.

No behavior change.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(archive): roll back a staged copy when the live spec cannot be removed

Both non-atomic retirement routes - a symlinked main spec, and the
EXDEV/EPERM rename fallback - copy the spec into staging first and remove
the original second. A copy that landed before an `unlink` that failed left
the spec in TWO places, and the staged one then tripped the "already
staged" guard on every rerun. The error told the caller to rerun the
archive, and the rerun could never work.

Reproduced at the previous head with a symlinked `spec.md` in a read-only
capability directory: `copyFile` succeeded, `unlink` returned EACCES, and
both copies remained.

The failure path now deletes the destination this attempt created, so the
capability is left exactly as the attempt found it and the rerun works. The
rollback is gated on a flag set only after the destination is proven free,
so a spec staged by an EARLIER run is never the thing removed - the
overwrite guard still fires ahead of it and rolls nothing back. A partially
written copy is cleaned by the same call.

The message no longer promises more than it delivers: it reports that the
spec is still in place, or names the leftover copy when the rollback itself
failed.

Regression tests cover both routes and assert the rerun succeeds, not just
that the copy is gone. Both fail without the rollback. The cross-device
route injects EXDEV, which cannot be provoked inside one temp directory.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* test(archive): run the rename-fallback rollback case on Windows too

The two post-copy rollback cases shared one `skipIf(win32)`, inherited from
the symlink case, which needs privileges Windows does not grant by default.
The rename-fallback case uses regular files and spies only, and the sibling
errno it stands in for - EPERM - is the Windows case, so skipping it there
left that route untested on the platform that produces it.

Skipping is now per-case.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(archive): claim the retirement destination atomically

`fs.access` followed by a write is not an ownership claim. Two concurrent
retirements both saw the destination free and both set `destIsOurs`; one
moved the spec into staging, and the other - equally convinced the file was
its own - rolled it back out. The source and the staged copy both ended up
gone. Reproduced at the previous head in 36 of 40 iterations.

The claim and the content now arrive in one syscall: `copyFile` with
`COPYFILE_EXCL` fails with EEXIST rather than overwriting, so exactly one
caller can ever own the path. That is also the check that refuses to
clobber a spec an earlier aborted run staged, now decided atomically rather
than by a separate look beforehand.

The losing caller fails two ways, and both used to destroy the winner's
file. EEXIST is the obvious one. ENOENT is not: `copyFile` opens the source
first, so a loser that arrives after the winner removed the source fails
before creating anything - and treating that as "a partial copy of mine"
unlinked the winner's file. Neither errno now claims ownership. Fixing only
EEXIST left 4 of 40 iterations still losing both copies.

Copying rather than renaming is what makes the claim possible: `rename`
overwrites silently on every platform, so it cannot tell "I created this"
from "I destroyed someone else's". It also crosses filesystems, which
retires the EXDEV/EPERM fallback, and reads a symlink's content rather than
moving the link - so the two routes collapse into one shape.

Regression asserts the invariant over 25 rounds: exactly one caller
retires, the spec survives once and intact, and the source is gone. It
fails against the old access-then-write shape.

Not crash-safe, which is a weaker promise and now documented: a process
killed between the copy and the unlink leaves the spec in both places, and
the next run refuses rather than guessing which to keep.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(archive): take retirement ownership from an exclusive create, not an errno

Claiming the destination with `copyFile(..., COPYFILE_EXCL)` closed the
concurrent race but kept reading ownership out of a failure code, and that
cannot be made correct however the errnos are partitioned. An errno says
what went wrong, not what was created: a source-side EACCES is
indistinguishable from a partial copy of our own, so the cleanup deleted a
recovery copy an earlier run had staged - the last remaining copy of a spec
whose live file could not even be read.

Reproduced at the previous head with an unreadable `spec.md` and a
pre-existing `retired-specs/legacy/spec.md`: the staged file was destroyed.

Ownership now comes from `open(dest, 'wx')`. O_CREAT|O_EXCL returns a
handle exactly when it created the file, so the question is answered by the
syscall instead of inferred afterwards, and every failure path leaves the
flag false. EEXIST remains the refusal that protects an earlier run's copy,
now decided by the same operation. Content is written through the claimed
handle, as bytes, and the handle is closed before any rollback so Windows
can unlink it.

The regression uses real mode bits, skipped on Windows and under root: the
defect was a source-side errno being read as proof about the destination,
and stubbing a JS-level read cannot reproduce it, because the copy it has
to fool never went through one. Verified it fails against the errno-
inference version.

All three findings on this path now hold together: the pre-existing copy
survives, 0 of 120 racing iterations lose a spec, and a post-copy unlink
failure still rolls back and reruns cleanly.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(archive): keep the staged copy when the source is already gone

The rollback exists for a copy that landed while the source survived - the
two-places state that blocks every rerun. It must not fire once the source
is gone: at that point the staged copy holds the only remaining content, and
the end state the retirement was reaching for is already reached.

An external delete landing between the read and the unlink produced exactly
that, and the rollback destroyed the spec outright - `retired: false`, no
live file, no staged copy, content gone.

`unlink` returning ENOENT is now a success rather than a failure to roll
back. Every other errno still throws: the source is still sitting there, and
leaving the staged copy beside it is the state that blocks a rerun.

Found reviewing the finished path rather than reported - the same class as
the three review findings before it, all of them the rollback reaching a
copy it should not have. Regression verified against the unconditional
unlink.

Also corrects a doc line that still credited the copy with claiming the
destination; the claim is the exclusive create.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* refactor(archive): gate retirement on a declared marker, drop retired-specs/

Reworks #1302 to follow the design that already exists instead of adding one.

The move-into-the-archive approach introduced two things OpenSpec did not
have: capability retirement as a lifecycle state, and `retired-specs/` as an
on-disk convention no schema declares - which a future unarchive command
would have to know about. Its whole justification was preserving content that
two existing mechanisms already preserve: the archived change carries the
delta naming every REMOVED requirement with its Reason and Migration, and git
carries the file. The approach even conceded the point by advertising `git mv`
as the recovery path.

The issue itself proposed neither. It asked for a delete, or an explicit
retirement marker. This does both: archive deletes the emptied spec, and only
when the change declares `retire_capabilities: true` in its `.openspec.yaml`.

`skip_specs` is the precedent. The marker reader is the same function,
parameterised by key, so the two can never drift apart on what counts as
honorable metadata - a marker in unparseable YAML, or one whose schema does
not load, is not a marker in either case. An explicit `false` is not an
unhonorable marker, it is simply undeclared.

Without the marker nothing changes: the unwritable spec aborts the archive
exactly as before, except the abort now names the marker as the way out - and
says nothing about it when retiring would not have made the spec writable
anyway, so it never sends an author after the wrong fix. Applying REMOVED
already deletes requirement content from a main spec, so deleting the spec
once nothing is left is that same operation carried to its end.

Every guard survives: the validator's verdict, the residual-heading veto,
something-removed-this-run, and never under --no-validate. What goes is the
exclusive claim, the rollback, the staging directories, and the four
data-loss windows they created across four review rounds. Net 307 lines
smaller than the move.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* chore: regenerate parity hashes over the merged sync-specs template

#1482 and this branch both edit the sync-specs template, so the merged
template needs its own hash - neither side's committed value describes it.

* docs(archive): correct claims the redesign left false, and bump to minor

Review findings, all verified before fixing:

- `pruneEmptyDirs`'s doc claimed "two callers, two boundaries", naming the
  change directory as the second. That was the staging walk from the move
  design; there is one caller. The boundary stays a parameter, and the comment
  now says why.
- Three comments still described the retirement as moving the file somewhere.
  It deletes it.
- The sync skill told agents the retirement condition includes "no other
  `###` headings or prose" and then claimed "openspec archive draws exactly
  these lines". It does not draw the prose line: a main spec with loose prose
  under `## Requirements` retires and is deleted, and the prose is not named
  in the warning, which reports `## ` sections only. Verified against the
  built CLI. The condition now states what the CLI enforces, and the template
  tells the agent to read that prose back to the user, since the CLI cannot
  see it for the agent.
- `docs/concepts.md`'s `.openspec.yaml` field list omitted the new marker -
  the one place a user goes to learn what that file may hold.
- `docs/cli.md`'s `--no-validate` row did not mention that it disables
  retirement, though the row two lines down documents retirement.
- Bumped patch -> minor. `skip_specs`, the marker this one mirrors, shipped as
  a minor change in 1.7.0 (#1399); this adds a metadata field and an archive
  outcome on the same footing.

No behavior change.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(archive): refuse to retire a spec with a second Requirements section

Four review agents ran against this branch. Two data-loss findings, both
reproduced before fixing.

1. A spec with a SECOND `## Requirements` section was deleted even though it
   passed `validate --strict` with zero issues, and the report named only
   `Purpose`.

   `extractRequirementsSection` binds to the FIRST `## Requirements`, so
   everything after it rides through the merge untouched: the residual-heading
   veto never sees it, `findOtherSections` filters it out by title, and the
   validator's own section lookup stops there too - which is why a second
   section holding a `SHALL` with a scenario reads as valid and then died with
   the file. The earlier round made that veto position-independent WITHIN the
   section; this is the same evasion one level up.

   Retirement is now refused outright for such a spec, so the archive aborts as
   it did before #1302. The abort's marker hint takes the same conjunct, so it
   never advises a marker that would not have helped.

2. The recovery line promised `git checkout HEAD -- <path>` unconditionally,
   and the path was wrong twice over. Verified failures: an UNTRACKED spec -
   the ordinary case, since an earlier `openspec archive` creates the main spec
   and nobody has committed it yet - is deleted and the printed command errors,
   so the file is gone for good; under a store-selected root the nominal
   `openspec/specs/...` path does not exist in the caller's repo; and a
   symlinked capability directory puts the file somewhere else entirely.

   The line now names the path the file actually lived at, and is phrased as
   the condition it really is rather than a promise archive cannot keep.

Regressions for both, plus the three fail-closed branches on the deletion
authorisation path that no test observed: a marker in unparseable YAML, and a
failing unlink. Each verified against a mutation - removing the veto, restoring
the unconditional promise, swallowing the unlink error, and honouring a marker
in broken YAML each fail their test.

Also pins the sync skill's retirement guidance by content rather than by golden
hash, since a hash proves only that it matches its source.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs(archive): note that retiring a capability strands an in-flight MODIFIED

A capability's main spec is the base #1482's scenario-loss check compares a
MODIFIED block against. Retire the capability and that check goes silent by
design (a missing main spec is the sister-change-in-flight case), so a change
that modifies the retired capability keeps validating clean and then refuses to
archive with "target spec does not exist". Nothing is lost - there are no
scenarios left to drop - but nothing connects the refusal back to the
retirement either, so the changeset says it up front.

Found by testing this PR against the three that merged into main today.

* fix(archive): veto retirement on any heading past the merged section

A sixth data-loss defect, from a second round of review agents. Reproduced
before fixing: a `validate --strict`-clean spec was deleted with a live SHALL
requirement in it, and the report named only "Purpose".

The cause is a mask disagreement. `extractRequirementsSection` - the function
that decides where the Requirements section ENDS - masks fenced blocks only.
`findHeadings`, which both retirement vetoes were built on, masks HTML comments
as well. So a multi-line comment holding a `## ` line terminates the section for
the merge while being invisible to the scan that had to notice it: everything
below became a tail no guard could see. The round-five guard counted `##
Requirements` headings, which the same trick skins straight past.

The veto is now asked of the tail itself - does anything `###`-shaped sit past
the boundary the merge actually chose - read with the fence-only mask, so it
answers the question whatever produced that boundary. That subsumes the
multiple-Requirements-sections case it replaces and every comment variant.

Also from this round:

- The recovery command is derived from the path that was unlinked, not rebuilt
  from the capability id. On a case-insensitive filesystem the id and the real
  directory differ in case, git is case-sensitive, and the printed command was
  one git rejects.
- An absolute recovery path now says which checkout to run it in - for a
  selected store, the file is not under the directory archive was run from.
- A declared marker refused by the tail veto says why, instead of dropping the
  author who did what the docs asked back into the bare #1302 abort.
- Corrected "draws exactly these four lines" in the sync skill, a claim added
  two commits ago that was false when written: the CLI checks two more.

Both regressions are mutation-verified. Reverting the veto to the narrow
multi-section count fails the comment-boundary test.

One reported finding was NOT actioned, because its premise does not hold: a
residual `###` heading INSIDE the section still counts as a requirement to the
validator, so that spec is valid and simply gets written - there is no silent
dead end there to explain.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs(archive): say the marker needs the schema key beside it

`.openspec.yaml` requires `schema:`, so a file holding only
`retire_capabilities: true` is not honorable metadata and the marker does
nothing. The docs and the abort hint both described adding one line, which sends
anyone creating that file from scratch into a dead end. The message did explain
itself once you were there ("schema: Invalid input: expected string, received
undefined"), but it should not need to.

Pre-existing shared behavior - `skip_specs` has the same requirement - so this
is wording, not a behavior change.

* chore: merge main (#1483) and keep both archive test suites

#1483 landed while this branch was in review. Three conflicts:

- `archive.ts`: one import line, both sides' imports kept.
- `skill-templates-parity.test.ts`: hash constants, resolved by key-union and
  then regenerated from the merged source, which is the only authority once two
  branches have edited the same template.
- `archive.test.ts`: the trap this repo documents. Both branches appended a
  DIFFERENT describe block at the same place - `capability retirement (#1302)`
  here, `non-interactive prompts (#1479)` on main - so taking either side would
  have dropped 16 or 133 tests with a green suite. Both are kept.

The conflict boundary also cut the retirement describe's last two closing
braces, which `tsc --noEmit` accepted and only esbuild caught as "Unexpected
end of file". Restored by brace-balance against both parents.

Verified after: every one of main's 91 archive titles and 19 parity titles is
present, #1483's describe still holds its 16 tests, and its own non-interactive
repro still behaves as it does on main.

* fix(archive): only print a recovery command that would actually run

Both blockers from the last review.

The recovery line offered `git checkout HEAD -- <path>` for every retirement,
including ones where the file never lived under the directory archive was run
from: a selected store, or a symlinked capability directory. Git rejects an
absolute path from a different worktree however it is quoted, and an unquoted
path containing a space splits when pasted - a real store path reproduced both.
Those cases now say where the file was and leave recovery to the reader, rather
than handing them a command that cannot work. The ordinary case still gets the
command, quoted when the path needs it, via the portable quoting #1483 already
established for change names.

And `openspec/specs/specs-sync-skill/spec.md` still authorised deletion from the
four original conditions, with no mention of the tail-heading veto the CLI
gained - so the living spec permitted something the code refuses. It now carries
that condition, and a parity test pins it in the generated guidance so the two
cannot drift apart again.

Both fixes are mutation-verified: restoring the unconditional command fails the
escaped-path regression, and rewording the veto out of the template fails the
guidance test.

* fix(archive): retire only what the merge can account for

Replaces the tail-heading veto with a rule that does not read Markdown at all.

Six review rounds each found a different way to dress content so a heading scan
would miss it: a second `## Requirements` section, a `##` inside an HTML comment
ending the section early, a three-space indent, a setext underline. Every fix
was another regex approximating a parser, and every round found the next skin.

`extractRequirementsSection` has already split the file into the parts this
merge understands. So instead of asking "does anything here look like a
requirement" - a question a regex and a renderer answer differently - the guard
now asks where content ended up: anything non-blank between the `## Requirements`
header and the first requirement, or after the section ends, is content the merge
carried through without understanding, and a retirement that would delete the
file is refused. There is no second opinion to disagree with the first, because
there is no second parse.

The in-block heading guard stays, and its comment now says why: a `###` heading
that is not a requirement header is absorbed into the block above it, so it
never reaches the preamble or the tail. Folding that into the rule above needs a
parser that ends a block at any `###` heading, which belongs in the parser.

This narrows the feature: a spec carrying an authored section beyond Purpose can
no longer be retired automatically. That is deliberate. The abort names the
lines that stood in the way, and deleting a file whose contents this merge
cannot enumerate is exactly the case a person should decide.

Depends on #1490 for indented requirement headers, which are swallowed by the
block parser before any of this runs.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(archive): account for the whole spec, not two slices of it

Defect eight, same class as the seven before it. The guard asked where content
landed, which was the right question, but it only read two of the five slices
`extractRequirementsSection` produces: the preamble and the tail. Content simply
moved somewhere nobody looked.

Reproduced: a hand-written migration runbook and a table written below a
requirement's scenarios live inside that requirement's `raw` - the block runs to
the next header the parser RECOGNISES - so removing the requirement deleted them,
and the report said "Its section(s) went with it: Purpose". Not silence: a false
statement the reader can act on. The same hole covered anything written above
the `## Requirements` section. And because the abort hint is gated on the same
checks, an unmarked run RECOMMENDED adding the marker that destroys it.

The audit now covers the whole file. Expected: the title, the `## Purpose`
section, the `## Requirements` header, and inside each block a requirement's own
parts - its header, its statement, its scenarios' bullets. Every other non-blank
line is reported and refuses the retirement. That folds in the `###`-heading
guard, which was a patch on this same leak using the technique the rewrite was
meant to abandon.

One reported shape is deliberately not a case: prose between `## Purpose` and
`## Requirements` IS the Purpose body, since the section runs to the next `##`,
and the warning already names Purpose as going with the file. The test says so.

Both regressions fail against the two-slice version.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(specs): keep content absorbed into a removed requirement

A requirement block's `raw` runs to the next header the parser RECOGNISES, so a
heading it does not - one indented by the 0-3 spaces CommonMark allows, or a
plain `### Notes` - is absorbed into the requirement above it. Removing that
requirement deleted the absorbed content with it. Silently: nothing counted it,
so nothing warned, and the spec left behind still validated.

Reproducible on main with no marker and no capability retirement involved.

Anything from the first `#`/`##`/`###` heading after a removed block's own
header is now kept in place. `####` is excluded deliberately - a requirement's
`#### Scenario:` headings are its own and go with it.

This replaces an earlier attempt on this branch that widened every heading
pattern in both parsers to accept indentation. That was wrong twice over. It
reclassified content, so a spec that was valid became invalid - commented-out
and indented examples started parsing as real requirements, taking `list` from
1 requirement to 3. And it did not even fix the bug: moving the line out of the
block only meant the reconstruction dropped it at a different step, since
`rebuilt` is assembled from `before + header + kept blocks + after` and anything
skipped is simply gone.

So nothing is reclassified now. An indented heading is still not a requirement,
exactly as before; it just survives its neighbour's removal, which is all this
ever needed to do. The repo's own corpus produces byte-identical `list`,
`validate --specs --strict` and `validate --changes --strict` output.

Four regressions, each mutation-verified: removing the salvage fails the three
absorbed-content cases, and counting `####` as a boundary fails the scenario
case.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(specs): keep notes absorbed into a modified or removed requirement

A slow audit of the previous commit found the fix covered one of three paths.

A requirement block absorbs anything below it that the parser does not read as
a new header - a note indented by the 0-3 spaces CommonMark allows, say - so
that content rides inside the block. The previous commit salvaged it when the
requirement was REMOVED and missed MODIFIED entirely: that path rebuilds the
block from the delta, which never carried the note, so it was dropped exactly as
before. Verified against the real CLI: main loses it on both paths.

RENAMED was the opposite trap. It rewrites the original block's header line in
place, so the note is already there - but it also deletes the original key from
the block map, which made the requirement look REMOVED to the salvage and
produced a duplicate. Tracking which operation applied is therefore not reliable
at this point in the merge, so the salvage now asks the assembled result
instead: re-insert a note only when nothing else in the rebuilt section already
carries it. That is correct for all three paths by construction.

Salvaged content also keeps its position now, next to the requirement it was
written beside, rather than being appended at the end of the section.

Six regressions, three of them mutation-verified against this logic: never
re-inserting fails four, always re-inserting duplicates on rename, and appending
at the end loses the position.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(specs): decide salvage by identity, not by matching text

Another audit pass, another defect in my own fix.

Deciding whether a note survived by searching the rebuilt section for its text
is wrong when two requirements carry the same note: the first copy is found,
and the second is dropped. Reproduced - two removed requirements each followed
by an identical `### Notes`, one note destroyed.

Survival is a question about the block, not about text. An untouched block is
the same object the parser produced and still carries its note; a replaced one
is a different object and does not. The RENAMED path previously blurred that by
copying the whole raw, so it now carries only the requirement's own lines and
the salvage puts the note back like every other path. With every replacement
uniformly lacking the tail, `replacement !== block` decides it exactly, and no
text is compared at all.

Four properties, each mutation-verified: matching text instead of identity
loses the duplicate note, always re-inserting doubles an untouched block's note,
letting RENAMED keep the tail doubles it on rename, and counting `####` as a
boundary severs a requirement from its scenarios.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(specs): warn when a note absorbed into a requirement will be deleted

An adversarial review found the previous approach was worse than the bug.

Salvaging the "foreign tail" out of a requirement block relied on a positional
rule: everything after the first heading-shaped line is not the requirement's.
That is not true. A `# comment` inside a scenario bullet, or a markdown example,
matches the same shape - and on MODIFIED the old text was then spliced back in
after the new, so the spec asserted both. The validator called the result valid,
and re-applying the same delta grew the file every time. Reproduced end to end.

It also turned a working archive into a hard abort: preserving an unindented
`### Notes` made the rebuilt spec fail validation as a scenario-less
requirement, so changes that archived cleanly on main stopped archiving, with an
error that never mentioned the note.

Measured before choosing: 3 of 742 requirement blocks in this repo contain a
heading-shaped line, and the repro shows those are false positives. Trading a
rare silent deletion for silent corruption on the most common operation is a bad
trade.

So the merge is left exactly as it was - byte-identical output, verified against
main - and the loss is reported instead. That fixes the part of the bug that
actually hurt: it was silent. A wrong warning costs a line of output; acting on
a wrong answer rewrites the spec.

Eight tests. Dropping the warning fails three; ignoring the fence mask fails
one - the fence case the previous version left unpinned.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(archive): scope a scenario's bullets, and stop refusing ordinary prose

Defect nine, plus the over-refusal it exposed.

Every bullet counted as a scenario's own, anywhere in the block. So an
operational note bulleted below the last scenario - "IMPORTANT: escrow keys
live in the legacy vault" - was deleted with the file, on a spec that passes
`validate --strict`, and the report named only "Purpose". A scenario's bullets
run unbroken beneath its header; a blank line after them ends the run, and
bullets past that point are the author's own note.

Measuring the guard against this repo's 36 specs then showed the opposite
failure was already there: 7 of them could never be retired, almost entirely
because every fenced line inside a requirement was treated as foreign. A code
example inside a scenario is that requirement's own content - a
`### Requirement:` inside a fence is not a heading to any reader - so fenced
lines are now accounted for, as are numbered lists and a statement that opens
with inline code.

One ambiguity is left deliberately unresolved: a scenario whose bullets are
split by a blank line reads exactly like a note bulleted below it, and no
line-based rule separates them. Those specs are REFUSED, never deleted. The
abort quotes the lines, and the author moves them or removes the file by hand.
Refusing costs a message; the alternative costs the file.

Two regressions: the bulleted note must refuse, and a requirement using a
numbered list, a fenced example and an inline-code statement must still retire.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(archive): a section is not only an ATX heading

Defect nine, from a deep adversarial pass, and it is the same species as the
eight before it: the guard decided what a section IS by one syntax while a
reader recognises three.

Once `## Purpose` was seen, every later line in the pre-requirements slice was
accepted as its body until the next ATX `##`. But a setext underline turns the
line above it into a heading, and raw HTML says so outright - a reader sees a
sibling of `## Purpose`, not more of it. So a whole authored section could sit
between Purpose and Requirements, pass `validate --specs --strict`, and be
deleted with the file while the report said only "Purpose". On main the same
archive aborts and loses nothing.

Reproduced with a `Data Migration Notes` section underlined with dashes: the
capability retired, the notes gone, unnamed. Now refused, with the lines quoted.

Two path defects from the same review, one fix: the reported path was rebuilt
from the capability id, so on a case-insensitive filesystem it differed in case
from the file actually unlinked and git rejected the printed command; and a
capability directory symlinked to a sibling deleted one spec while naming
another. `retireSpec` now always returns the path it unlinked, and archive
reports that. Whether to print a command at all is decided against the REAL
repo root, so a symlink that stays inside the repo still gets a working command
and only a path that genuinely leaves it falls back to prose.

Also pins `!skipValidation` in isolation. The existing --no-validate test passed
for the wrong reason - its fixture was blocked by the content guard - so the
conjunct itself was unpinned.

Four regressions, all mutation-verified.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(archive): close remaining capability retirement gaps

* fix(archive): close final transaction safety gaps

* fix(archive): close retirement race windows

* fix(archive): preserve retirement authorization

* fix(archive): verify complete fallback copies

* fix(archive): preserve transactional safety

Reject structurally ambiguous or symlinked inputs before mutation, serialize archive claims safely, and preserve permissions during verified fallback moves.

Keep retired specs as inode-preserving backups until the archive commits, restore them on rollback, and retain any backup changed concurrently instead of deleting user data.

* fix(archive): preserve replaced claims on Windows

Add a per-claim nonce and verify stable claim contents before unlinking because Windows file IDs may not distinguish a replacement lock entry.

* test(archive): respect Windows deferred deletion

Skip the POSIX unlink-and-recreate claim simulation on Windows, where deletion of an open file remains pending until the original handle closes.

* test(archive): align symlink fixtures with path boundaries

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 19:00:25 +00:00
Clay Good 9cd845fc45 fix(security): keep paths on a short leash (#1499)
* fix(security): keep paths on a short leash

* fix(security): tighten linked path handling

* test(security): prove schema escape rejection

* fix(security): close remaining trust boundary gaps

* fix(security): close review-found read windows

* fix(security): preserve safe linked workflows

* fix(schema): preserve fork failure details
2026-08-04 18:28:03 +00:00
Clay Good 4e4c9e1ffd docs(workflows): visualize the OpenSpec lifecycle (#1507)
* docs(workflows): add lifecycle diagrams

* docs(workflows): clarify optional archive paths

* docs(workflows): correct lifecycle diagrams

* docs(website): render Mermaid diagrams

* fix(website): preserve Mermaid label text
2026-08-04 18:09:08 +00:00
dependabot[bot] 80ad1fbaef chore(deps): bump the website-dependencies group (#1496)
Bumps the website-dependencies group in /website with 9 updates:

| Package | From | To |
| --- | --- | --- |
| [fumadocs-core](https://github.com/fuma-nama/fumadocs) | `16.11.5` | `16.12.1` |
| [fumadocs-mdx](https://github.com/fuma-nama/fumadocs) | `15.2.0` | `15.2.1` |
| [fumadocs-ui](https://github.com/fuma-nama/fumadocs) | `16.11.5` | `16.12.1` |
| [lucide-react](https://github.com/lucide-icons/lucide/tree/HEAD/packages/lucide-react) | `1.25.0` | `1.27.0` |
| [next](https://github.com/vercel/next.js) | `16.2.11` | `16.2.12` |
| [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) | `26.1.1` | `26.1.2` |
| [@types/react](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react) | `19.2.17` | `19.2.18` |
| [@types/react-dom](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react-dom) | `19.2.3` | `19.2.4` |
| [postcss](https://github.com/postcss/postcss) | `8.5.22` | `8.5.25` |


Updates `fumadocs-core` from 16.11.5 to 16.12.1
- [Release notes](https://github.com/fuma-nama/fumadocs/releases)
- [Commits](https://github.com/fuma-nama/fumadocs/compare/fumadocs@16.11.5...fumadocs@16.12.1)

Updates `fumadocs-mdx` from 15.2.0 to 15.2.1
- [Release notes](https://github.com/fuma-nama/fumadocs/releases)
- [Commits](https://github.com/fuma-nama/fumadocs/compare/fumadocs-mdx@15.2.0...fumadocs-mdx@15.2.1)

Updates `fumadocs-ui` from 16.11.5 to 16.12.1
- [Release notes](https://github.com/fuma-nama/fumadocs/releases)
- [Commits](https://github.com/fuma-nama/fumadocs/compare/fumadocs@16.11.5...fumadocs@16.12.1)

Updates `lucide-react` from 1.25.0 to 1.27.0
- [Release notes](https://github.com/lucide-icons/lucide/releases)
- [Commits](https://github.com/lucide-icons/lucide/commits/1.27.0/packages/lucide-react)

Updates `next` from 16.2.11 to 16.2.12
- [Release notes](https://github.com/vercel/next.js/releases)
- [Commits](https://github.com/vercel/next.js/compare/v16.2.11...v16.2.12)

Updates `@types/node` from 26.1.1 to 26.1.2
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

Updates `@types/react` from 19.2.17 to 19.2.18
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react)

Updates `@types/react-dom` from 19.2.3 to 19.2.4
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react-dom)

Updates `postcss` from 8.5.22 to 8.5.25
- [Release notes](https://github.com/postcss/postcss/releases)
- [Changelog](https://github.com/postcss/postcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/postcss/postcss/compare/8.5.22...8.5.25)

---
updated-dependencies:
- dependency-name: fumadocs-core
  dependency-version: 16.12.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: website-dependencies
- dependency-name: fumadocs-mdx
  dependency-version: 15.2.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: website-dependencies
- dependency-name: fumadocs-ui
  dependency-version: 16.12.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: website-dependencies
- dependency-name: lucide-react
  dependency-version: 1.27.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: website-dependencies
- dependency-name: next
  dependency-version: 16.2.12
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: website-dependencies
- dependency-name: "@types/node"
  dependency-version: 26.1.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: website-dependencies
- dependency-name: "@types/react"
  dependency-version: 19.2.18
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: website-dependencies
- dependency-name: "@types/react-dom"
  dependency-version: 19.2.4
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: website-dependencies
- dependency-name: postcss
  dependency-version: 8.5.25
  dependency-type: direct:development
  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-08-04 17:39:57 +00:00
dependabot[bot]andClay Good 23c2787789 chore(deps-dev): bump eslint from 10.7.0 to 10.8.0 in the development-dependencies group (#1494)
* chore(deps-dev): bump eslint in the development-dependencies group

Bumps the development-dependencies group with 1 update: [eslint](https://github.com/eslint/eslint).


Updates `eslint` from 10.7.0 to 10.8.0
- [Release notes](https://github.com/eslint/eslint/releases)
- [Commits](https://github.com/eslint/eslint/compare/v10.7.0...v10.8.0)

---
updated-dependencies:
- dependency-name: eslint
  dependency-version: 10.8.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: development-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>

* fix(nix): refresh pnpm dependency hash

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Clay Good <hi@claygood.com>
2026-08-04 17:39:36 +00:00
Jun 690a27e649 fix(adapters): stop deleting the CoStrict and Junie commands on every run (#1492)
LEGACY_SLASH_COMMAND_PATHS lists artifacts older OpenSpec versions left
behind, so init and update remove whatever matches. Two entries named
paths the current adapters still write to.

`costrict` was a whole-directory entry for `.cospec/openspec/commands`,
the folder its adapter writes `opsx-<id>.md` into, so every run deleted
the directory and everything in it — including files the user put there
— under a heading reading 'No user content to preserve'. It is now a
file pattern for `.cospec/openspec/commands/openspec-*.md`: the only
files that folder ever held before the opsx rename were
openspec-proposal.md, openspec-apply.md and openspec-archive.md, written
by the slash configurator added in #240 and dropped in #565.

`junie` listed `.junie/commands/opsx-*.md`, its adapter's own output,
next to `openspec-*.md`. Both halves arrived in #853 one file apart, so
the entry has collided with itself since day one. Cleanup runs before
migrateIfNeeded, so on a config with no `profile` key yet — the state
after a first init — the deleted command files make inferDelivery read
the project as skills-only and write that to the global config. The
files are not regenerated, and the delivery preference changes for every
other project too.

The entry is removed rather than narrowed. Junie support landed in #853,
months after #565 deleted the slash configurators that wrote
`openspec-*` files, and no junie configurator ever existed — so
`.junie/commands/openspec-*.md` is a shape OpenSpec never produced. The
same reasoning already keeps `.devin/` off the list two lines above.

The regression test is an invariant rather than a fixture — it writes
every registered adapter's output for every workflow into a temp project
and asserts detection reports nothing — and it names codex, the one
legacy id with no adapter, instead of silently skipping it.

Nothing else changes. The surviving `openspec-*` globs stay as broad as
they have always been, since narrowing them to the three ids the pre-opsx
configurators actually wrote is a separate, uniform change, and qwen's
`opsx-*.toml` pair stays because its adapter emits Markdown now.
2026-08-04 17:39:09 +00:00
Clay GoodandClaude Opus 5 45cca5db61 fix(specs): warn before archiving deletes a note next to a requirement (#1490)
* fix(specs): keep content absorbed into a removed requirement

A requirement block's `raw` runs to the next header the parser RECOGNISES, so a
heading it does not - one indented by the 0-3 spaces CommonMark allows, or a
plain `### Notes` - is absorbed into the requirement above it. Removing that
requirement deleted the absorbed content with it. Silently: nothing counted it,
so nothing warned, and the spec left behind still validated.

Reproducible on main with no marker and no capability retirement involved.

Anything from the first `#`/`##`/`###` heading after a removed block's own
header is now kept in place. `####` is excluded deliberately - a requirement's
`#### Scenario:` headings are its own and go with it.

This replaces an earlier attempt on this branch that widened every heading
pattern in both parsers to accept indentation. That was wrong twice over. It
reclassified content, so a spec that was valid became invalid - commented-out
and indented examples started parsing as real requirements, taking `list` from
1 requirement to 3. And it did not even fix the bug: moving the line out of the
block only meant the reconstruction dropped it at a different step, since
`rebuilt` is assembled from `before + header + kept blocks + after` and anything
skipped is simply gone.

So nothing is reclassified now. An indented heading is still not a requirement,
exactly as before; it just survives its neighbour's removal, which is all this
ever needed to do. The repo's own corpus produces byte-identical `list`,
`validate --specs --strict` and `validate --changes --strict` output.

Four regressions, each mutation-verified: removing the salvage fails the three
absorbed-content cases, and counting `####` as a boundary fails the scenario
case.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(specs): keep notes absorbed into a modified or removed requirement

A slow audit of the previous commit found the fix covered one of three paths.

A requirement block absorbs anything below it that the parser does not read as
a new header - a note indented by the 0-3 spaces CommonMark allows, say - so
that content rides inside the block. The previous commit salvaged it when the
requirement was REMOVED and missed MODIFIED entirely: that path rebuilds the
block from the delta, which never carried the note, so it was dropped exactly as
before. Verified against the real CLI: main loses it on both paths.

RENAMED was the opposite trap. It rewrites the original block's header line in
place, so the note is already there - but it also deletes the original key from
the block map, which made the requirement look REMOVED to the salvage and
produced a duplicate. Tracking which operation applied is therefore not reliable
at this point in the merge, so the salvage now asks the assembled result
instead: re-insert a note only when nothing else in the rebuilt section already
carries it. That is correct for all three paths by construction.

Salvaged content also keeps its position now, next to the requirement it was
written beside, rather than being appended at the end of the section.

Six regressions, three of them mutation-verified against this logic: never
re-inserting fails four, always re-inserting duplicates on rename, and appending
at the end loses the position.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(specs): decide salvage by identity, not by matching text

Another audit pass, another defect in my own fix.

Deciding whether a note survived by searching the rebuilt section for its text
is wrong when two requirements carry the same note: the first copy is found,
and the second is dropped. Reproduced - two removed requirements each followed
by an identical `### Notes`, one note destroyed.

Survival is a question about the block, not about text. An untouched block is
the same object the parser produced and still carries its note; a replaced one
is a different object and does not. The RENAMED path previously blurred that by
copying the whole raw, so it now carries only the requirement's own lines and
the salvage puts the note back like every other path. With every replacement
uniformly lacking the tail, `replacement !== block` decides it exactly, and no
text is compared at all.

Four properties, each mutation-verified: matching text instead of identity
loses the duplicate note, always re-inserting doubles an untouched block's note,
letting RENAMED keep the tail doubles it on rename, and counting `####` as a
boundary severs a requirement from its scenarios.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(specs): warn when a note absorbed into a requirement will be deleted

An adversarial review found the previous approach was worse than the bug.

Salvaging the "foreign tail" out of a requirement block relied on a positional
rule: everything after the first heading-shaped line is not the requirement's.
That is not true. A `# comment` inside a scenario bullet, or a markdown example,
matches the same shape - and on MODIFIED the old text was then spliced back in
after the new, so the spec asserted both. The validator called the result valid,
and re-applying the same delta grew the file every time. Reproduced end to end.

It also turned a working archive into a hard abort: preserving an unindented
`### Notes` made the rebuilt spec fail validation as a scenario-less
requirement, so changes that archived cleanly on main stopped archiving, with an
error that never mentioned the note.

Measured before choosing: 3 of 742 requirement blocks in this repo contain a
heading-shaped line, and the repro shows those are false positives. Trading a
rare silent deletion for silent corruption on the most common operation is a bad
trade.

So the merge is left exactly as it was - byte-identical output, verified against
main - and the loss is reported instead. That fixes the part of the bug that
actually hurt: it was silent. A wrong warning costs a line of output; acting on
a wrong answer rewrites the spec.

Eight tests. Dropping the warning fails three; ignoring the fence mask fails
one - the fence case the previous version left unpinned.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(archive): warn before actual content loss

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 02:04:10 +00:00
Arda KılıçdağıandClay Good 1da6dfa8d7 Docs: add deno install instructions (#1079)
* docs: add deno install instructions

* chore(docs): address pr feedback

* chore(docs): address note feedback.

---------

Co-authored-by: Clay Good <hi@claygood.com>
2026-07-30 23:38:36 +00:00
Clay GoodandClaude Opus 5 2b3d368539 fix(archive): tell the caller which flag to pass when archive can't ask its questions (#1483)
* fix(archive): name the flag when a prompt has no terminal to answer it

An AI agent runs the CLI with stdin closed, so every confirmation
`openspec archive` asks rejects with @inquirer's "User force closed the
prompt with 0 null" - true, and useless: it names neither the question
nor the flag that answers it, so agents abort and guess (#1479).

Each confirmation now reports the same guidance JSON mode has always
given for that decision point, with a pasteable command. The change
picker got the opposite treatment: it swallowed the same failure,
printed "No change selected. Aborting." and exited 0, reporting success
for a run that archived nothing. It now exits 1 asking for a change
name, matching `openspec show` and `openspec validate`.

The detection is reactive - a prompt that already failed, at a stdin
that is not a terminal - so piped answers, --yes, --json and Ctrl-C at a
real terminal are untouched.

Closes #1479

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(archive): carry the caller's flags into the suggested rerun, and honor every non-interactive signal

Adversarial review of the first commit found four defects in it:

- The suggested rerun dropped the flags the caller had passed. For
  `archive x --skip-specs` it suggested a bare `--yes` rerun, and
  following it merged deltas into the main specs - the exact thing
  --skip-specs was passed to prevent.
- The change name went into that command unquoted, so a change named
  `my change` produced an unrunnable paste and one named `a;touch x`
  produced a paste that runs a second command.
- The predicate keyed on stdin.isTTY alone, so a CI runner that
  allocates a pty still got the raw @inquirer failure - #1479 unfixed
  under the very signals `isInteractive()` already treats as
  authoritative.
- A genuine Ctrl-C reaches a process whose stdin is a pipe, and that
  was reported as "this terminal is not interactive", telling a user
  who deliberately quit to rerun with --yes.

The signal is now `!isInteractive()` with SIGINT excluded, so the
terminal proves capability and the signal proves intent. Messages say
what happened ("no answer could be read from stdin") rather than
asserting a property of the terminal, which was false under MinTTY.

Mutation testing found four more gaps in the tests: an unconditional
`throw blocked()`, a stripped `withStoreFlag`, and either half of the
predicate's `||` all left the suite green. Each now has a test, along
with the flag carry-forward, the quoting, the pty-CI case, and the two
prompts that had no end-to-end coverage. docs/cli.md documents the
behavior without a terminal.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* test(archive): close two gaps CodeRabbit found in the new guards

The template guard required a space after `openspec archive`, so a
regression to a bare `openspec archive` line - which blocks agents
exactly as #1479 describes - would have passed it. Verified by
mutation: the widened pattern fails on that edit.

Expected filesystem paths in the new e2e assertions are built from
path segments, per the repo's testing guideline.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(archive): make the suggested rerun runnable for dashed names, stores, and Windows shells

A second adversarial pass, scoped to the previous two commits, found
three defects in the fix itself:

- A change named `--force` was emitted bare, and commander reads it as
  an option however it is quoted, so the suggested command failed with
  `unknown option`. Such changes do archive, so the case is reachable:
  the name now goes behind a `--`, with the store flag kept in front of
  it where it is still read as an option.
- The change-name-required path was the one blocked site left
  hard-coded, so `archive --skip-specs` with nothing to answer the
  picker suggested a rerun without `--skip-specs` - the same merge the
  previous commit set out to prevent.
- Quoting was POSIX-only: cmd.exe does not treat `'` as quoting at all,
  and PowerShell escapes an embedded quote by doubling it, so the
  emitted command was wrong on Windows. Names now use double quotes,
  which bash, zsh, PowerShell and cmd.exe all read the same way, and a
  name containing something with no portable spelling (a quote,
  backslash, `$`, backtick, newline) names the placeholder rather than
  emitting a command that could expand.

Two tests were pinning less than they claimed. The real-terminal
cancellation test had become a duplicate of the piped one, since the
SIGINT check short-circuits before the terminal is consulted; it now
covers the terminal leg with a non-SIGINT failure, which is the leg
nothing else guarded. The template guard iterated two identical strings
and could not see an indented invocation; it now sweeps every rendered
skill and command template, and both mutations were confirmed to fail
it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs(changeset): name the quoting form the fix actually emits

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(archive): stop quoting change names cmd.exe would expand anyway

`%USERNAME%` is a legal change directory name, and cmd.exe expands it
inside double quotes, so the suggested rerun `openspec archive
"%USERNAME%" --yes` targets a different change than the one that was
blocked. `!` has the same problem under cmd.exe's delayed expansion and
bash's interactive history expansion.

Both characters now fall back to the `<change-name>` placeholder, the
same path a `$`/backtick name already took: a rerun the reader has to
fill in beats one that silently archives something else.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(archive): stop a change directory from forging its own Fix line

Four adversarial reviews of this branch turned up one real defect and two
guards that were not actually pinned.

The human-mode message for an unanswerable incomplete-task confirmation
interpolated the change name raw, and archive resolves a change by stat-ing
its directory, so the name is attacker-influenceable. A newline in it added a
second, forged `Fix:` line - and because `quoteChangeName` degrades the real
fix to `<change-name>` for exactly those names, the forged line was the only
pasteable command on screen. Control characters are now collapsed.

Also pinned two mutations that passed the whole suite green: dropping
`withStoreFlag` from only the dash-leading branch of `rerunCommand`, and
dropping the `validate === false` leg of the `--no-validate` test - the one
leg Commander actually produces.

The --yes parity guard only saw invocations that opened a line, so a `$ `
prompt, a list marker or `openspec --store x archive` slipped past it. It now
matches those and names the onboarding floor instead of trusting `total > 0`.

Docs and spec catch up: a troubleshooting entry under the message people
actually search for, and cli-archive scenarios for the unanswerable-prompt
paths, including that Ctrl-C stays a cancellation.

* test(archive): tokenise the --yes guard instead of pattern-matching it

Accepting a global flag between `openspec` and `archive` needed nested
quantifiers, and CodeQL was right to call that a ReDoS shape (js/redos, high)
even in a test over our own templates. Splitting the line into tokens decides
the same question in linear time - a 20k-flag line now costs ~2ms - and reads
more plainly than the pattern did.

Same classifications as before, plus it correctly ignores `openspec list
archive`, where `archive` is an argument rather than the subcommand.

* test(archive): skip the forged-Fix-line case on Windows

Windows rejects control characters in a filename, so the change directory the
test needs cannot be created there - which is also why the hole it covers is
POSIX-only. Matches the existing `it.skipIf(process.platform === 'win32')`
idiom in the suite.

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 00:25:13 +00:00
Clay GoodandClaude Opus 5 427abf40ac fix(tasks): count indented sub-tasks in task progress (#1486)
Both checkbox parsers anchored the bullet at column 0, so an indented
sub-task was invisible to `openspec list`/`view` progress, to the apply
task list, and to archive's incomplete-task check. A change whose
sub-tasks were unfinished reported "✓ Complete" and archived with no
warning.

One shared `parseTaskLines()` now backs both surfaces and allows leading
whitespace. It matches every line the two patterns it replaces matched,
and more - including a tab or non-breaking space inside the brackets,
which the old counting pattern accepted - so task counts can rise but
never fall: no change starts reporting less work than before, and
archive's gate can only get stricter.

Checkboxes still count wherever they sit, including inside a code fence.
Skipping fenced ones was implemented and dropped: every rule for deciding
which fence is real has an input where a stray or unbalanced ``` swallows
genuine tasks, which is the silent failure this fix exists to remove.

Verified differentially against a build of main over hand-built fixtures
and the repo's own 120 tasks.md files: 0 files count fewer tasks, 0 lose
an incomplete-task warning.

Closes #1485

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-29 22:41:57 +00:00
Clay GoodandClaude Opus 5 84ebc57cb3 fix(validate): report scenarios a MODIFIED requirement would drop (#1482)
* fix(validate): report scenarios a MODIFIED requirement would drop

`openspec validate <change>` accepted a MODIFIED requirement that omits a
scenario the main spec still has, even with --strict. Archive refuses to
apply that block (a MODIFIED replaces the whole requirement, so the omitted
scenario would be lost), so the change could pass validation, be implemented
and reviewed, and fail only days later at archive time (#1477).

Validate now runs the same non-mutating check against the main specs and
reports each omitted scenario, naming the delta file. The comparison itself
moved to the parser module so archive and validate share one implementation
and cannot drift.

The check is silent when the main spec file or the requirement header is
absent — a MODIFIED written against a sister change still in flight is a
separate condition archive gates — so validate can only report what archive
already refuses.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* refactor(validate): tighten the scenario-loss check after review

Keep the moved scenario parser module-private, derive change validate's
main specs root from the changes root it already resolved, replace the
rename re-keying with a lookup fallback, and say at archive's call site
why it does not opt in.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(validate): follow rename chains when checking for dropped scenarios

A delta that renames A to B and then B to C leaves C holding A's block at
archive time. Walk the rename map instead of looking it up once, so the
chained case reports the same loss archive refuses.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(validate): close the gaps five adversarial reviews found

Code:
- An unreadable main spec was swallowed, so a change archive aborts on
  validated clean. Only ENOENT/ENOTDIR mean "no main spec" now; anything
  else is reported.
- A MODIFIED naming a header the same delta renames away no longer names
  scenarios from the block it would not land on. That contradiction is
  already reported on its own, and the scenario list pointed at the wrong
  requirement.

Guidance: the sync-specs skill told agents a MODIFIED block may carry only
the changed scenario, and its format reference showed one. Both validate and
archive reject that shape, so the template, the generated skill, and the
golden hashes are updated to match the schema's own rule.

Tests: the CLI wiring had no coverage at all — removing the argument that
turns the check on broke nothing. Adds end-to-end coverage of every entry
point and exit code, plus the non-strict default, a fenced scenario in the
delta, an unreadable main spec, the rename-away case, and a rename cycle.
Loose assertions now pin the scenario-loss issue itself.

Docs: a troubleshooting entry for the new message, and the changeset says
that a stale change will newly fail.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(validate): never turn a transient read error into a verdict

The unreadable-main-spec report added in the last commit fired for any
errno that was not ENOENT/ENOTDIR, which includes resource errors like
EMFILE that say nothing about the file. `validate --all` reads six changes
at once, so a busy process could have failed a change that is fine.

Reported now only for the codes that mean the file itself is unusable and
will be just as unusable when archive reads it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs(troubleshooting): label the example fence (MD040)

Every other fence in the file names its language.

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 22:41:51 +00:00
solanabandClay Good 1aa0f2abfc feat(init): add shared agents skills target (#1303)
Co-authored-by: Clay Good <hi@claygood.com>
2026-07-29 22:41:47 +00:00
Suhaib AslamandSuhaibAslam 1014c59ed1 docs: catalog intent-driven community schema (#1487)
Co-authored-by: SuhaibAslam <SuhaibAslam@users.noreply.github.com>
2026-07-29 20:26:12 +00:00
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
openspec-release-bot[bot]andgithub-actions[bot] 546224e00d Version Packages (#1248)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-28 13:02:43 +00:00
Tabish Bidiwale 96f6cacb20 chore: add changeset for stores beta and config JSON parsing (#1267)
* Add changeset for stores beta and config JSON parsing

* Remove leaked tool-wrapper lines from changeset
2026-06-28 12:44:00 +00:00
Tabish Bidiwale 737518b36f [codex] Refresh security dependency locks (#1249)
* fix: refresh security dependency locks

* fix: refresh nix pnpm dependency hash
2026-06-24 08:54:03 +00:00
zhangsan582andTabish Bidiwale f987cf3e29 Parse config JSON containers (#1216) (#1244)
Co-authored-by: Tabish Bidiwale <30385142+TabishB@users.noreply.github.com>
2026-06-24 08:06:42 +00:00
Zied JlassiandTabish Bidiwale cbf386bd68 fix(adapters): escape carriage returns in YAML frontmatter and dedupe escapeYamlValue (#1240)
escapeYamlValue detected \r as a character requiring quoting but never
escaped it, leaving a literal carriage return inside the double-quoted
scalar. A literal CR there is subject to YAML line folding/normalization
and could silently corrupt the round-tripped value (realistic with
CRLF-authored command descriptions).

- Escape \r as \r alongside the existing \, " and \n handling.
- Extract the helper, previously duplicated verbatim across five adapters
  (bob, claude, cursor, pi, windsurf), into a shared
  command-generation/yaml.ts module.
- Add unit tests covering the escaping rules and a round-trip through a
  real YAML parser.

Refs #1205, #1204

Co-authored-by: Tabish Bidiwale <30385142+TabishB@users.noreply.github.com>
2026-06-24 08:01:56 +00:00
bb1f18c483 docs: comprehensive overhaul — discoverability, explore-first, and closing recurring doc-request issues (#1237)
* docs: comprehensive documentation overhaul (home, mental model, command location, FAQ, glossary, troubleshooting, recipes)

Addresses #1228 (docs are fragmented and hard to discover). Additive,
docs-only. The single sharpest gap from the issue thread was that nobody
explains where slash commands run, hence the new "How Commands Work" page.

New docs:
- docs/README.md          documentation home / index that maps every doc
- docs/how-commands-work.md  where /opsx:* (chat) vs openspec (terminal) run; "interactive mode" answered
- docs/overview.md        core concepts at a glance, one page
- docs/faq.md             consolidated common questions
- docs/glossary.md        every term in one place
- docs/troubleshooting.md concrete fixes for concrete failures
- docs/examples.md        real changes start to finish (recipes)

Small additive edits:
- docs/getting-started.md  "where do I type this?" callout + first-five-minutes + richer Next Steps
- README.md                Docs list points at the new home and key new pages

Voice: warm, plain, bottom-line-up-front; no em-dashes in prose.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs: make /opsx:explore front and center, plus general polish

Per maintainer feedback (Tabish): "the docs in general need some work,
alongside making the explore option a lot more front and center."

explore ships in the default core profile but every doc led with propose
and treated explore as a footnote for "unclear requirements." This reframes
the canonical loop as explore -> propose -> apply -> archive and gives
explore real prominence.

- docs/explore.md (new): dedicated "Explore First" guide. When to use it,
  what it does/doesn't, a full transcript, handoff to propose, tradeoffs.
- getting-started.md: explore added to the flow and first-five-minutes,
  with a featured callout and Next Steps entry.
- overview.md: explore featured in the loop and next-links.
- docs/README.md: explore in the opening, pick-your-path, 30-second
  version, and the doc map.
- how-commands-work.md: explore leads the command list with a "good rhythm"
  note and an optional step in the clean-first-run example.
- workflows.md: new first-class "Start by exploring" pattern in the default
  section (was buried under expanded mode); quick-reference row strengthened.
- commands.md / faq.md / glossary.md: explore featured as the place to start.
- examples.md: top callout pointing at the explore recipe.
- README.md: explore opens the "See it in action" demo and Quick Start,
  and is added to the Docs list.

Docs-only and additive. No em-dashes in prose; links verified.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs: close recurring gaps (existing projects, editing changes, uninstall, context limits) + sync tool list

Sweep of open issues and discussions surfaced several questions good docs
should answer but didn't. This adds the missing guides and fixes a stale list.

New guides:
- docs/existing-projects.md: adopting OpenSpec on a large brownfield codebase
  without documenting everything up front (addresses #510, #1100, #176).
  Delta-first framing, first-change walkthrough, onboard, importing existing
  requirements docs, domain organization, monorepo/workspace pointers.
- docs/editing-changes.md: how to edit any artifact, update a proposal/spec
  after starting, go back after implementing, and reconcile manual code edits
  (addresses #684, #976, #355, #1188, #169, #1206).

Enhancements:
- installation.md: Updating + Uninstalling sections (addresses #308).
- faq.md: new entries for existing codebases, editing artifacts, going back,
  reconciling manual edits, context limits / long sessions, and uninstalling
  (addresses #257 among others).
- cli.md: --tools list now includes `vibe` and matches AI_TOOLS in
  src/core/config.ts, with a note pointing at the source (fixes #1213).
- Wired the new guides into the docs home, getting-started, and the README.

Docs-only and additive. No em-dashes in prose; links and section anchors verified.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs: reconcile coordinate-across-repos docs with the stores model

The merge with main pulled in the stores rename (#1190), which retired the
workspaces/initiatives/context-store vocabulary and deleted docs/workspaces-beta/.
This updates the three docs still describing the old model so they match the
new stores model, fixing the vocabulary-sweep test and dead links:

- glossary.md: Workspace/Link/Context store/Initiative -> Store/Reference/
  Working context/Workset; link to stores-beta/user-guide.md
- README.md: replace deleted workspaces-beta/* links with the Stores User
  Guide and Agent Contract
- existing-projects.md: reframe the multi-repo section as stores; drop the
  dead concepts.md#coordination-workspaces anchor

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: TabishB <tabishbidiwale@gmail.com>
Co-authored-by: Tabish Bidiwale <30385142+TabishB@users.noreply.github.com>
2026-06-24 06:52:59 +00:00
Tabish Bidiwale 41ceebe2d8 fix(ci+installers): harden permission checks and guard completion/profile writes (#1247)
* fix: harden permission checks in root CI

* fix: close permission guard review gaps

* test: address coderabbit installer comments
2026-06-24 06:18:54 +00:00
Tabish Bidiwale a0decbe3fa feat(stores)!: replace workspaces and initiatives with stores (#1190)
* Implement context store root parity

* Clarify simplified model roadmap

* Add roadmap progress checklists

* Number roadmap work items

* Add --store root selection for normal commands

Implements the store-root-selection slice (1.2, with 2.1 pulled forward):

- Add a shared OpenSpec-root resolver (src/core/root-selection.ts) behind
  new change, status, instructions, list, show, validate, and archive.
  --store <id> resolves a registered context store to an ordinary OpenSpec
  root; identity and root-health failures point to context-store doctor.
- Leftover workspace view state never wins root resolution for these
  commands, and a no-root directory with registered stores errors with a
  store-selection hint instead of scaffolding an implicit root.
- Selected-store runs print "Using OpenSpec root: <id> (<abs path>)" to
  stderr and JSON successes carry an additive shared root block.
- --store-path is rejected deliberately with context-store register
  guidance, including on show despite allowUnknownOption.
- new change is root selection only: initiative-link creation is removed,
  --initiative and --areas reject before any writes, --goal stays ordinary
  metadata. openspec set change is removed along with initiative-link.ts.
- archive gains --json: non-interactive, machine-readable diagnostics for
  blocked paths, and no prose or blank lines on stdout.
- list gains minimal --specs --json support so specs listing participates
  in the root reporting contract.
- context-store setup/register next steps show --store usage.

* Fix stream-purity and message bugs found in review

- archive --json: silence the REMOVED-deltas-on-new-spec warning from
  buildUpdatedSpec so the JSON payload stays pure.
- Resolver: wrap registry reads so a corrupt registry surfaces as a
  RootSelectionError; JSON mode now emits a machine-readable diagnostic
  instead of a blank stdout line.
- archive --store (human): per-spec update lines use the absolute store
  path, matching the cross-root absolute-paths contract.
- Noun-form spec show keeps its forward-slash relative not-found message
  on all platforms; root-aware show reports the absolute path.
- Tests: archive --json purity for REMOVED-delta and spec-update-failure
  paths, corrupt-registry JSON diagnostics, and running inside the
  standalone store repo without --store.

* Validate all rebuilt specs before writing any

The archive spec-update phase validated and wrote each rebuilt spec in a
single loop, so a later validation failure could leave earlier specs
already modified while reporting "No files were changed". Split it into
two passes: validate every rebuilt spec first, then write only after all
pass. Regression test covers a two-spec change where one rebuilt spec
fails validation and asserts no target spec was created or modified.

* Mark beta context-store and workspace docs as transition history

Rewrites the opening sections of the old initiative and workspace
reimplementation artifacts as transition evidence and beta history, and
adds the direction-git-native-work transition note. Readers are pointed
to openspec/work/simplify-context-and-workspace-model/ for the active
direction.

* Record store-root-selection slice artifacts and roadmap progress

Adds the slice 1.2 spec, plan, and decision-review evidence, and updates
the roadmap: 1.2 is implemented and tested on this branch, with review
follow-up and merge remaining.

* Record store-lifecycle-proof slice artifacts and roadmap progress

Spec and plan for slice 1.3 (prove the standalone repo lifecycle end to
end), with two review rounds folded in. Adds slice 1.4 to the roadmap,
parks archive browsability as L11, and records the single-branch
workflow for the whole roadmap.

* Prove the standalone store lifecycle end to end

Implements slice 1.3 (store-lifecycle-proof):
- Setup defaults to Git with a pathspec-limited initial commit of exactly
  the files it created, writes store.yaml before committing, anchors
  empty directories with .gitkeep, preflights commit identity via git var
  before creating anything, and requires an explicit --path (interactive
  setup prompts with a visible user path).
- Doctor reports read-only Git facts (commits, uncommitted changes,
  remote) and warns on commitless repos and clone-fragile directories.
- Register errors are terminal: one-checkout-per-id with the unregister
  escape, registration-aware id-mismatch fix text, and an empty-clone
  explanation on unhealthy roots.
- Selected-store hints carry --store, the root banner prints at
  resolution time so post-resolution failures keep it, new change names
  its next command, and status drops the workspace-era Planning home
  line.
- Adds the two-checkout journey e2e test (machine A lifecycle, machine B
  clone/register/continue) with fully isolated Git config and XDG state.

* Fix review findings in the store lifecycle slice

Two adversarial subagent reviews of the slice 1.3 implementation found
one spec violation and several correctness risks; all are fixed:

- Hints carry --store everywhere: validate/show non-interactive hints,
  archive blocked-path fix texts, and status JSON nextSteps now thread
  the selected store. Status JSON also drops the workspace-era
  planningHome field.
- Reruns of an already-registered store no longer git-init it (the CLI
  default is resolved against the registry via resolveSetupGitEnabled),
  keeping reruns strict no-ops.
- Failed initial commits unstage setup's files so a user repo is not
  left with a dirty index; once the commit lands, cleanup no longer
  deletes the committed files; fresh-dir cleanup is non-recursive again
  so it can never delete content setup did not create.
- Corrupt or fake .git dirs report Git facts as unknown instead of
  commitless, avoiding misleading empty-clone advice.
- The sharing next-step line only prints for actual repositories.
- Journey test: Windows-safe path assertions, telemetry opt-out, machine
  B now runs the full enumerated command set (instructions, validate),
  asserts register creates no commits, covers the banner-on-failure and
  store-carrying-hint contract, and doctor human output. Unit tests gain
  isolated git config, register error-text coverage for both mismatch
  branches, and a default-flags rerun no-op regression test.

Full suite: 93 files, 1729 tests, green.

* Keep validate and show hints inside the selected store

Follow-up review findings: the invalid-report next step pointed at the
deprecated cwd-based 'openspec change show <id>' and dropped --store; it
now names the supported top-level 'openspec show <id> --json
--deltas-only' with the actual change id and the store flag. The
nothing-to-show fallback hints and the ambiguous-item advice in validate
and show no longer suggest noun-form commands when a store is selected,
since those commands cannot reach a store root; store mode gets
--type-scoped top-level equivalents instead. No-store output is
unchanged.

* Derive setup's commit from the store shape and extract Git mechanics

Code-quality review follow-up:

- The initial commit was built from the rollback ledger, which is the
  wrong concept: for a converted (existing, non-Git) root it committed
  only the new anchors and identity file, leaving config and specs
  uncommitted and clones unhealthy. When setup initializes the repo
  itself, it now commits the full store shape (openspec/ plus
  .openspec-store/), while pre-existing repos keep the
  only-what-setup-created commit that protects user history and staged
  files. Old beta files outside the store shape are never swept in.
- Identity-file creation is now owned solely by setup; registration runs
  with writeMetadataIfMissing: false and verifies instead of writing,
  removing the split ownership that made the commit plan leaky.
- Git probing, init, identity preflight, and commit mechanics moved from
  operations.ts (1204 lines) into src/core/context-store/git.ts;
  operations.ts is back to 1077 lines and owns only the lifecycles.
- Git lifecycle tests split into test/commands/context-store-git.test.ts
  with shared fixtures in test/helpers/context-store-git.ts, including a
  new conversion test that proves a clone of a converted root is
  immediately healthy.

Spec and plan updated to lock the two commit modes. Full suite: 94
files, 1730 tests, green.

* Point the roadmap's next-item marker at slice 1.4

* Restructure the roadmap around root relationships

Fresh-eyes review outcome, settled in discussion: the layered
PM/architect-to-dev use case (high-level requirements in a standalone
store, implementation work in the app repo's own OpenSpec root) replaced
the rejected project-to-store binding idea with declared relationships
between roots and a fixed resolution precedence — explicit --store, then
nearest local root, then a declared default only when no local root
exists, then error with hint. References never change where commands
act.

- Slice 1.4 becomes one guidance pass (absorbs old 2.2; ~13 surfaces
  from research) gated on the context-store terminology decision
  promoted from L7.
- Phase 2 is fully absorbed: 2.1 shipped in 1.2, 2.2 into 1.4, 2.3 into
  4.1 (initiative selection is hardcoded into ~5,500 lines of opening
  machinery that 4.1 rebuilds; refactoring first is wasted motion).
- Phase 3 rewritten around relationships in both directions, references
  first: repo-references-stores, declared-store fallback, canonical
  remote in store identity, then store-level target declarations, local
  repo map, and relationship health reporting.
- Phase 4 reframed as context assembly; editor opening is one consumer,
  an agent session brief is another.
- New guardrails: references are repo-level config, never per-change
  lifecycle links; one change lives in one root.
- goal.md gains the layered reference experience.

* Lock the naming, Phase 3, and Phase 5 decisions

Decisions settled after parallel product-level and staff-engineer
analyses:

- Naming: the noun is 'store', defined as 'a standalone OpenSpec repo
  you've registered'. The context-store → store group rename plus the
  full machine-token rename (diagnostic codes, JSON keys, data dir) land
  first in slice 1.4; --store stays; committed store-repo formats are
  already aligned and stay. openspec repo/--repo rejected: the --repo
  prior means the code repo being operated on, colliding with target
  project repos.
- Phase 3: index-not-inline reference injection; references: and the
  fallback store: pointer both live in openspec/config.yaml (top-level
  marker rejected — .openspec.yaml is taken by change metadata); one
  typed id namespace with the kebab grammar locked for all id kinds;
  relationships are location, declaration, or citation — never managed
  per-artifact links, which is what initiative links were.
- Phase 5 criteria agreed: delete rather than hide, sequenced across
  1.4, a small command-group deletion slice, and 4.1; never auto-delete
  user data.

* Add the roadmap loop runbook

* Make the roadmap loop fully autonomous with layered reviews

No pause gates: unlocked decisions are made autonomously and recorded
as 'Decided autonomously (review me)' changelog lines; Phase 5
deletions proceed without confirmation. Review phases run as parallel
multi-agent Workflows plus the /code-review skill (high effort) and
codex CLI; /simplify runs serially after correctness fixes.

* Add the loop's parallelism policy

Serial across slices (single branch, shared junction files, mass
rename/deletion commits make cross-track rebases the riskiest
unattended operation); Workflow fan-outs within slices for mechanical
sweeps; read-only lookahead research for the next slice's code map.

* Switch the roadmap run driver from /loop to /goal

The docs position /loop as interval-based and /goal as the
condition-based counterpart: turns fire back-to-back until a verifiable
completion condition is met, with full main-loop tool and skill access
per turn and persistence across resume. That matches the queue's
semantics (next unit when the previous finishes, stop when done), so
loop.md becomes runbook.md, reframed around goal-driven turns with an
explicit per-turn status block for the goal evaluator and a declared
completion signal.

* Add the final acceptance capstone and standing quality bars

The goal condition previously checked activity (boxes ticked, suite
green); it now checks the product claim. Phase 6 / capstone 6.1: four
persona journeys including a cold-start agent dogfood, usability audits
(error catalog, vocabulary sweep, time-to-first-success), technical
audits (single-resolver invariant, dependency direction, dead code,
module sizes, agent-contract inventory, net LOC delta vs origin/main),
a whole-delta review gauntlet, and a committed release-readiness
report. Runbook gains standing per-slice quality bars: locked
vocabulary only, pasteable store-carrying errors, consistent agent
contracts, ~600-line module budget, no speculative abstractions, one
resolver.

* Bake the /goal invocation into the runbook header

* Write and review the store-rename-and-guidance slice spec

Two parallel adversarial reviews (subagent, codex CLI) converged on the
same flaw in the first draft: exempting the legacy groups from the token
rename contradicted the locked machine-token decision. The spec now
states one rule - total mechanical token rename, surgical prose rewrite,
behavior changes limited to the two riders - and folds the corrected
45-code token inventory, the missed guidance surfaces, and a
sweep-as-test acceptance criterion.

* Write and review the store-rename-and-guidance plan

Four green checkpoints: mechanical rename, the two riders, guidance
regeneration (three disjoint streams), and sweep/guards/dogfood. Both
parallel reviews (subagent, codex CLI) approved with fixes, all folded:
exact rider-1 deletion list with persisted path-bound views preserved,
Commander command:* error ownership, docs/concepts.md and beta-doc
runtime fixes, sweep roots excluding openspec/ history, old-data-dir
negative fixtures, pinned non-interactive dogfood init flags.

* Rename the context-store surface to store

Mechanical, total token rename per the slice spec: command group
(context-store -> store, subcommands unchanged), 45 diagnostic codes,
dotted context_store.* targets, JSON keys (context_store/context_stores
-> store/stores everywhere, legacy groups included), the machine-local
data dir (context-stores/ -> stores/), internal modules and symbols
(src/core/context-store -> src/core/store, ContextStore* -> Store*),
and every help/error/hint string. Committed store-repo formats are
untouched (.openspec-store/store.yaml, registry.yaml). The dead
getDefaultContextStoreRoot export is deleted; its negative path
assertion is kept inline. The --store flag description now carries the
locked definition, identical in Commander and completions metadata.

Full suite green (94 files, 1730 tests).

* Land the two store-rename riders

Rider 1: workspace open loses its legacy --store/--store-path initiative
selectors (the second live meaning of --store). The unreachable guard
branch and its workspace_open_store_without_initiative diagnostic are
deleted; --initiative keeps resolving through the cross-store scan, the
qualified <store>/<id> form, and the interactive picker; persisted
path-bound views still reopen and doctor (tests now write the view-state
fixture directly). Selector-advertising fix texts in initiative
resolution name only surviving forms.

Rider 2: the store group owns its unknown-subcommand path - the error
names the real subcommands (including ls) and points lifecycle-shaped
mistakes at the normal command with --store, same stderr text for human
and --json runs, exit 1. New tests cover the hint, the no-alias
negative, and the --help listing.

Full suite green (94 files, 1735 tests).

* Regenerate guidance around stores

Templates: every generated workflow skill (and its opsx command twin)
now carries a shared store-selection block - discover ids with
'openspec store list --json', carry --store <id> on every command,
hints keep the flag. The three out-of-guard workspace-planning prose
mentions reword to schema language; the five live workspace guards are
untouched. Parity hash tables updated deliberately and the test now
asserts the store teaching in all generated skills.

Docs accuracy pass: docs/cli.md store section renamed with the locked
vocabulary, removed workspace-open selector rows and example, stale
XDG-default setup text corrected; docs/concepts.md token renames;
workspaces-beta docs renamed plus correctness fixes (--path in setup
examples, current prompt-flow prose). Every documented invocation
smoke-ran against the built binary. The workspace and initiative group
one-liners are labeled legacy beta in Commander and completions.

Note: the .codex/skills/use-openspec guidance was also rewritten around
store discovery (beta reference deleted), but that directory is
git-ignored (the L8 ignored-local-skill), so those edits live on disk
only and cannot appear in this commit.

Full suite green (94 files, 1736 tests).

* Record the git-ignored .codex discovery in the slice artifacts

* Guard the rename with sweeps, format pins, and the dogfood proof

New tests: a vocabulary sweep over src/, test/, docs/, scripts/ (and
.codex/ when present) that fails on any reintroduction of the retired
tokens; committed-format pins (.openspec-store/store.yaml literals, the
stores/ data dir, pre-rename store registration); old-data-dir negative
fixtures (valid and corrupt old registries are ignored, never read or
migrated); a --store description exact-equality walk across every
lifecycle command; and a store:setup telemetry-path assertion.

Dogfood proof committed as dogfood-transcript.md: a fresh headless agent
session, one plain prompt naming the team store in words, discovered the
registered store via --help and store list and created the change with
--store - six tool calls, zero initiative/workspace invocations, local
root untouched.

Full suite green (95 files, 1742 tests).

* Fix the post-implementation review findings

Three parallel review mechanisms (spec-compliance agent: compliant with
findings; /code-review high: 10 verified findings; codex CLI: approve
with fixes) converged on two P2s and a set of cheap P3s, all fixed:

- The store group's unknown-subcommand hint no longer emits invalid
  suggestions: 'store new <id>' without 'change' falls back to the full
  form, flag-interleaved operands (which Commander cannot attribute)
  use the generic example, the lifecycle-redirect set derives from
  COMMAND_REGISTRY, and the subcommand list derives from the live
  Commander group instead of a hardcoded string.
- Store-selection guidance names the seven commands that accept --store
  instead of claiming every command does, and is removed from the
  feedback workflow (whose only command rejects the flag); presence
  coverage extended to all 11 opsx command templates; hash tables
  re-pinned.
- Pasteable hints: 'Run store unregister' fix texts now name
  'openspec store unregister <id>'; the empty-list setup hint carries
  the mandatory --path.
- STORE_OPTION_DESCRIPTION now imports the completions description
  instead of duplicating it; the path-bound view fixture persists
  through the production writeWorkspaceViewState; the pre-rename
  register test writes old-format bytes inline; the vocabulary sweep
  file carries no retired tokens and no longer self-exempts.

Full suite green (95 files, 1745 tests).

* Apply the simplify-pass cleanups

Test guards now iterate the production registries: store-selection
presence checks run over getSkillTemplates()/getCommandContents() (new
workflows are covered automatically) and assert full-constant
containment; the --store description walk pins the exact seven command
names and ties each to the guidance prose, so a stale taught surface
fails tests. The store group one-liner derives from the completions
registry entry; the command:* flag predicate is derived, not restated;
retired-token constants are hoisted once per file; a redundant
assertion and a dynamic import are gone.

Skipped deliberately: the sweep's hand-rolled walker (measured ~48ms,
works), a cross-file retired-token helper (two files only), and
pre-existing duplications on surfaces the next slices delete.

Full suite green (95 files, 1745 tests).

* Tick slice 1.4 in the roadmap and point at the deletion slice

* Write and review the delete-legacy-command-groups slice spec

Both parallel adversarial reviews rejected the first draft on verified
grounds and every finding is folded: the config command's
workspace-profile integration (which executes a dead command) is in
scope; binding.ts stays because the planning-home carve-out depends on
it through workspace/foundation.ts; a dead-export carve-out ledger owned
by 4.1 is specified; concepts.md loses its whole Coordination Workspaces
section; the surviving 'Use initiatives' constraint rewords to read-only
compatibility language. The locked 5.1 'opening machinery' wording is
narrowed (recorded as a reviewable autonomous decision): the state model
and workspace-planning mode die in 4.1; zero-consumer opening helpers
die with the command groups.

* Write and review the delete-legacy-command-groups plan

Five deletion waves with grep-before-delete discipline. Both parallel
plan reviews folded: the planning-home mode pin (nothing asserts
actionContext.mode today) and the docs pointer grep gate are new
explicit steps; docs/cli.md dead-command references outside the cited
ranges are mapped (agent-table rows, Stores summary cell, config
section); the config.ts map gained the interface and core-preset call
sites with full test ranges; the parity test's initiative carve-out
removal is a named fourth partial edit; the spec's byte-stable clause
now permits the new removal-coverage tests.

* Delete the workspace and initiative command groups

The legacy beta command groups stop existing, and everything only they
consumed goes with them: the command layer (workspace.ts, initiative.ts,
the 11-file workspace/ command dir), the orphaned core (workspace
registry/openers/open-surface/skills/link-input and the whole
collections tree), the completions entries, the config command's
workspace-profile integration (which executed a dead command), the
update command's workspace detection, the docs that documented nothing
else (cli.md sections, concepts.md Coordination Workspaces,
docs/workspaces-beta/), and the tests of all of it.

Kept deliberately: planning-home and its state model (foundation,
state-io, legacy-state, store binding types - 4.1 owns their end),
legacy initiative metadata display, the --initiative rejection, and
every byte of user data. The 'Use initiatives' constraint rewords to
read-only compatibility language.

Ground truth recorded: workspace-planning mode has been CLI-unreachable
since slice 1.2's resolver demotion (toPlanningHome hardcodes repo
kind); the spec scenario was corrected to pin the byte-stable repo-local
behavior plus the library contract.

New removal-coverage tests (7) pin unknown-command rejection, help
cleanliness, update fall-through, user-data byte-identity, legacy
display, and the library contract. deletion-ledger.md records the 41
removed diagnostic codes and the dead-export carve-outs owned by 4.1.

Full suite green (85 files, 1614 tests). Pointer grep gate clean.

* Fix the deletion-slice review findings

Three parallel review mechanisms (spec-compliance: compliant with
findings, no P1; /code-review high: surgery residue and test-robustness
items; codex CLI: three P3s) converged on a small list, all applied:
the dead hasRepoLocalOpenSpecProject helper and its orphaned import are
deleted; the maybeWarnConfigDrift pass-through wrapper is collapsed and
its stale awaits dropped; the byte-identity test asserts the update
spawn's exit code and snapshots directories (not just files) so empty
subdirectory deletions cannot pass; the frozen-legacy-bytes fixture is
documented as deliberate; the project-apply accept path regained
coverage (lost with the deleted workspace tests); a sweep test pins the
ledger's surviving-token claim so workspace/initiative token regrowth
fails fast; the ledger records the state-io dead-export carve-outs, the
EACCES error-fidelity collateral, and the L2 pointer for the accepted
spec library that still describes deleted behavior.

Full suite green (85 files, 1616 tests).

* Apply the deletion-slice simplify pass and tick the roadmap

Simplify: the redundant hand-written store.yaml fixtures are gone
(registerStore writes identical metadata), the update action lost its
vestigial path.resolve scaffolding, and the sweep's four token spellings
collapsed to one concatenation-built regex. Skipped deliberately:
cross-suite snapshot helper extraction, state-io trimming, and barrel
removal - none pay for themselves before 4.1 deletes that code.

Roadmap: Phase 5 first tranche recorded (-12,903 net lines, ledger,
~25 fewer modules per CLI invocation), the workspace-planning
CLI-unreachability ground truth logged as a reviewable decision, and
the pointer moved to 3.1.

Full suite green (85 files, 1616 tests).

* Write and review the store-references slice spec (3.1)

Two adversarial rounds folded. The subagent's P1s were both grounding
failures: parseSpec() throws on imperfect upstream specs, so the index
extracts summaries tolerantly; and apply instructions have a real human
surface, so the index lives in both surfaces and both modes. Codex
added the async command-boundary assembly (the sync generators receive
the index as input), the 50KB shared budget with order-preserving
truncation, and registry-corruption degradation. Five warning codes
degrade instructions instead of failing them; references parse raw and
validate in the assembler; the index is one level deep by rule.

* Write and review the store-references plan (3.1)

Two checkpoints (config + assembler core; instruction surfaces + docs).
Both plan reviews approved with fixes, all folded: pure renderers live
in core beside the assembler so the 50KB budget measures real output
(truncation stops before the cap, warning line exempt); the
inspectRegisteredStore extraction is pinned narrow - metadata/health
stages only, registry lookup stays in resolveStoreRoot and its seven
error codes stay byte-identical; config is read once at the command
boundary and suppresses the generator's internal read; the Purpose-line
scanner is self-contained; the test matrix gained symmetric --store,
boundary byte-identity, no-recursion, nothing-frozen, and not-inlined
assertions.

* Add the references config field and the index assembler core

openspec/config.yaml gains references: (raw strings kept, deduplicated,
order-preserving; grammar validation is the assembler's job so bad ids
surface as diagnostics). New src/core/references.ts assembles the
referenced-store index: one registry read per call, the narrow
inspectRegisteredStore extraction shared with resolveStoreRoot (whose
seven error codes stay byte-identical, pinned by the existing
root-selection tests), tolerant first-Purpose-line summaries, five
warning diagnostic codes, self-reference omission by id and path, and
the 50KB budget with order-preserving truncation measured by the pure
renderers that the command layer will print.

Full suite green (86 files, 1630 tests).

* Wire the referenced-store index into both instruction surfaces

The command layer reads the resolved root's config once (suppressing
the generator's internal read), assembles the index, and threads it
into generateInstructions and generateApplyInstructions. Artifact human
mode prints the <referenced_stores> XML block after project context;
apply human mode prints a '### Referenced Stores' markdown section.
JSON gains an additive references field, omitted when none are
declared. docs/cli.md gains the 'Referencing stores from a project'
subsection.

Seven new surface tests pin: both surfaces both modes, live (unfrozen)
summaries, field omission, symmetric --store declarations, the
one-level rule, non-instruction byte-identity with the store untouched,
and the full PM-to-dev layered flow including the verbatim fetch.

Full suite green (87 files, 1637 tests).

* Fix the 3.1 review findings

Three review mechanisms converged on six real issues, all fixed with
regression tests: extractFirstPurposeLine is fence-aware and accepts
CommonMark closing hashes; an index emptied by self-reference omission
now omits the JSON field (omitted-not-empty contract); truncation
renders its message as a Note line instead of an orphan fix; the budget
measures the real rendering in UTF-8 bytes (problem entries and
diagnostics included; only the truncation warning exempt) with a
binary-search prefix; registry-independent checks (invalid id,
self-reference) run before the corrupt-registry branch; the assembler
catches inspection throws and degrades them; the resolveStoreRoot
switch is explicit (return fromStoreError) with an exhaustiveness
guard; generateApplyInstructions takes an options bag instead of a
fifth positional; the dead config-read catch is gone; spec files read
concurrently.

Full suite green (87 files, 1641 tests).

* Apply the 3.1 simplify pass and tick the roadmap

Simplify: the two new test suites share test/helpers/openspec-fixtures
(createOpenSpecRoot/writeSpec); the dead canonicalize wrapper is gone
(canonicalizeExistingPath never throws); the 50KB cap is single-sourced
from project-config's exported MAX_CONTEXT_SIZE; the registry-unreadable
state collapsed into one nullable variable; spread and JSDoc nits.
Skipped with reasoning: renderer branch merge, binary-search
replacement (measured: cap self-bounds the cost), cross-suite snapshot
consolidation, and the remaining ~1ms duplicate config read (the
project's own perf note rejects that trade).

Roadmap: 3.1 boxes ticked, changelog round recorded, pointer moved to
3.2. Full suite green (88 files, 1641 tests).

* Write and review the declared-store-fallback slice spec (3.2)

Both adversarial reviews converged on the same P1: the spec claimed
declared roots behave exactly like --store roots while its own UX
example printed a relative path, and the scope named only two of the
seven source-keyed consumers. The fix is one store-selected predicate
(storeId set) adopted everywhere. Also folded: init refuses to bury a
pointer under a scaffold; malformed pointers error
(invalid_store_pointer) instead of silently flipping the write target;
one-hop pointer resolution; warning-silent resolver config reads;
directory-typed shape stats; the true-prefix declaredOrigin mechanism;
and the recorded amendment relocating the both-shapes warning from the
nonexistent project doctor to resolution stderr.

* Write and review the declared-store-fallback plan (3.2)

Both plan reviews approved with fixes, folded: the eighth
source==='store' check (show.ts printNonInteractiveHint) joins the
predicate inventory with a recorded spec amendment; the init guard
anchors immediately after validate() so legacy cleanup and the
global-config migration write cannot precede the refusal; the
declaration-origin prefix is a call-site rewrap (codes preserved, fix
unprefixed) covering the fromStoreError pass-throughs; the targeted
config read is a shared exported helper; the test matrix covers all
five prefixed taxonomy codes, the malformed-pointer no-write
assertion, deterministic byte-identity, and positive config-only
assertions.

* Add the declared-store fallback to root resolution

A config-only openspec/ directory with a store: pointer now resolves
the declared store: the nearest-root arm classifies the found dir with
two directory stats, reads the pointer via the new warning-silent
readStorePointer helper (malformed pointers error with
invalid_store_pointer - never a silent local write), and resolves
through the shared resolveStoreRoot pipeline with source 'declared'
and a declaration-origin rewrap (codes and fixes untouched). A real
root with a pointer warns once on stderr and stays nearest - fallback
never override. The new isStoreSelectedRoot predicate (storeId set)
replaces all eight source==='store' checks so declared roots get
identical cross-root behavior: banner, --store hints, absolute paths,
suppressed noun-form suggestions.

Nine new resolver tests cover the pointer, precedence, the both-shapes
warning, malformed pointers, all five prefixed taxonomy codes, one-hop
resolution, and .yml origins.

Full suite green (88 files, 1650 tests).

* Add the init pointer guard, externalized-planning e2e, and docs

openspec init now refuses to scaffold a config-only pointer directory,
anchored immediately after validate() so the refusal precedes legacy
cleanup, migration writes, and prompts - the test pins that nothing
changes on disk and that removing the store: line converts cleanly.
The e2e journey runs the full lifecycle (new change through archive)
in a pointer repo without --store anywhere: work lands in the store,
the pointer repo stays byte-identical, the banner and JSON root block
report declared, nextSteps hints carry --store, and the 3.1 references
composition surfaces the store's own upstream index. docs/cli.md gains
the 'Declaring a default store' subsection.

Full suite green (89 files, 1654 tests).

* Fix the 3.2 review findings

Three review mechanisms converged; all real findings fixed with
regression tests: empty or comments-only configs in config-only dirs
are plain roots again (the documented comment-out conversion path no
longer strands every command behind invalid_store_pointer; non-mapping
scalars carry no pointer); the malformed reason splits into
unparseable vs non-string with accurate messages and fixes; the init
guard now refuses malformed pointers too and walks ancestors so a
pointer-repo subdirectory cannot grow a nested root that silently
diverts work; resolver and init share one classifyOpenSpecDir (the
classification can never diverge); readProjectConfig and
readStorePointer share one .yaml/.yml probe; the fourth copy of the
snapshot test helper is consolidated into test/helpers/fs-snapshot.ts;
the resolver header documents invalid_store_pointer; the
absolute-path warning wording is recorded as a spec amendment.

Full suite green (89 files, 1656 tests).

* Apply the 3.2 simplify pass and tick the roadmap

Simplify: isStoreSelectedRoot is a type guard (three redundant
conjuncts gone); the malformed-pointer reason strings single-source
through storePointerProblem in project-config (init's copies were
unpinned and could drift); the init guard drops its ternary for the
walk that finds projectPath in extend mode anyway. Skipped with
reasoning: directoryExistsSync consolidation (four pre-existing private
copies, out of slice), the warnings-array altitude (3.6 owns the
structured surface), the classification's module home (revisit when
3.6 consumes it).

Roadmap: 3.2 boxes ticked, changelog round recorded (including the
detached-HEAD process note), pointer moved to 3.3.
Full suite green (89 files, 1656 tests).

* Write and review the store-canonical-remote slice spec (3.3)

Two adversarial reviews converged on the contract holes, all folded:
the setup-rerun origin-erasure P1 (probe in both flows so
storeBackendsMatch stays consistent and the 1.3 rerun no-op survives);
register's write contract stated precisely (never commits, never
modifies an existing store.yaml; conversion identity stays
remote-free); the one-way strict-schema compatibility recorded as a
standing constraint for 3.4; mixed references dedup semantics
(normalize, dedup by id, first remote wins); verbatim-pasteable clone
fixes via ~/openspec/<id>; setup --remote refuses to be silently
ignored; the doctor example redrawn from the real layout; the
no-network clause pinned testably.

* Write and review the store-canonical-remote plan (3.3)

Both plan reviews approved with fixes, folded: clone fixes render
absolute home paths (tilde never expands outside a shell; agent JSON
consumers execute argv directly) with the spec amended to match;
setup's origin probe reaches both backend-resolution sites so reruns
cannot re-introduce the erasure P1, and stays out of
resolveGitStoreBackendConfig's hot read paths; the sharing-guidance
plumbing is concrete (StoreMutationResult carries canonical/observed,
JSON drops them, printMutationHuman renders the preference chain); the
setup-JSON contradiction resolved for the unchanged StoreOutput shape;
getOriginUrl trims; the --remote-vs-existing refusal fires in
prepareStoreSetup before any prompt or write; fill-if-absent dedup
pinned; registry anchors and test filenames corrected; TEST-NET
fixtures via git remote add.

* Record canonical and observed store remotes (3.3 checkpoint 1)

store.yaml gains an optional remote (strict schema retained; pre-3.3
files parse; unknown keys and empty remotes still fail). setup --remote
writes it before the initial commit, fails on empty values before
creating anything, and refuses with the hand-edit fix when store.yaml
already exists - silent flag acceptance is the forbidden outcome. Both
setup backend-resolution sites and register probe the local git origin
(gitOriginUrl, config read only) into the machine-local registry entry,
so reruns stay no-ops that preserve the record and re-register
refreshes it; conversion-created identity stays {version, id}. Doctor
surfaces metadata.remote and git.origin_url, with one human Remote line
preferring canonical. Sharing guidance names the canonical remote, then
the observed origin, then keeps today's wording - threaded through
StoreMutationResult.remotes and dropped from JSON.

15 new tests; three additive pins updated (doctor git shape x2, the
completions flag registry friction pin).

Full suite green (90 files, 1671 tests).

* Carry clone sources in reference declarations (3.3 checkpoint 2)

references: entries now accept {id, remote} maps alongside plain ids,
normalized to ReferenceDeclaration[] (dedup by id keeps the first
position; the first remote seen fills a missing one, never overrides).
The unresolved-reference fix becomes a verbatim-pasteable
git clone <remote> <home>/openspec/<id> && openspec store register ...
- absolute home path because tilde never expands outside a shell and
agent JSON consumers execute argv directly. An invalid id still wins
over any declared remote. The e2e onboarding journey executes the
printed fix verbatim (scratch HOME, local-path remote, split on the
shell &&) and continues to a resolved index - including the clone-trap
lesson that the origin must track anchor files. docs/cli.md documents
--remote, the store.yaml field, and the reference-with-remote form.

Full suite green (90 files, 1674 tests).

* Fix the 3.3 review findings

Three review mechanisms converged; all real findings fixed with
regression tests: register (and both setup sites) no longer probe the
origin of a non-repo store folder nested inside another repository -
git -C walks up, so the enclosing repo's origin could be durably
recorded and printed as sharing guidance (the shared
resolveBackendWithObservedOrigin helper guards with an at-root check
and deduplicates the triplicated probe block); the clone fix quotes
the checkout path, separates the remote with --, and renders only
shell-inert remotes (a config-committed --upload-pack or
metacharacter-bearing remote falls back to the teammate wording -
agents execute these fixes verbatim); setupPreparedStore re-asserts
the hand-edit refusal so metadata materializing between prepare and
execute cannot silently swallow --remote; a same-checkout origin
backfill now reports already_registered: true while still refreshing
the entry (the 1.3 rerun-reporting contract); the references warnings
distinguish dropped entries from dropped remotes; the dead zod union
for references is gone (the manual parser is the documented single
source); foundation's duplicate empty-remote message names its layer.

New pins: setup-rerun remote preservation, origin-backfill reporting,
the nested-repo guard, and the shell-safety gate.

Full suite green (90 files, 1678 tests).

* Apply the 3.3 simplify pass and tick the roadmap

Simplify: the duplicated store_remote_requires_hand_edit throw is one
factory (the TOCTOU re-assert can no longer drift from the prepare
guard); commitStoreRegistration restructures around a normalized
sameCheckout predicate - three near-identical returns become one, and
a symlinked-path remote refresh no longer misreports as a fresh
registration. Skipped with reasoning: the test fixture consolidation
(near the option ceiling), the checkout-location prose/computed split
and the ext:: transport hardening (both recorded as capstone notes),
doctor divergence display (spec-locked quiet form).

Roadmap: 3.3 boxes ticked, changelog round recorded, pointer moved to
3.4. Full suite green (90 files, 1678 tests).

* Write and review the store-targets slice spec (3.4)

Both adversarial reviews approved with fixes, folded: the apply
surface's indirect metadata flow (assembly runs inside
generateApplyInstructions with store targets passed through the
options bag); empty narrowing treated as undeclared; status always in
the JSON shape so agents see degradation; remote inheritance under
narrowing; the change-level grammar cliff owned explicitly;
KebabIdentifierSchema as the named validator with a neutral shared
kebab predicate replacing store-flavored naming; declared-root
sessions and the inert pointer-dir wrong turn covered.

* Write and review the store-targets plan (3.4)

Both plan reviews approved with fixes, folded: the artifact human
rendering anchored to printInstructionsText (instruction-loader
renders nothing); the unknown-store and root-resolution pins added;
validateStoreId delegates to the neutral isKebabId so one kebab regex
remains; the label-factory call corrected; the apply options bag
carries the resolved config path for fix text; inline expected strings
replace snapshot wording; the e2e gains a second non-narrowed change.

* Add the targets declaration layer (3.4 checkpoint 1)

One shared declaration-list parser now backs both references: and the
new targets: config field (identical normalization, dedup, and split
warnings - the 3.1/3.3 references pins stay green untouched).
ChangeMetadataSchema gains targets as kebab-validated ordinary
metadata, and the kebab grammar finally has one source of truth: the
exported isKebabId in change-metadata/schema, which validateStoreId
now delegates to. The pure src/core/targets.ts assembles the effective
set (change narrowing replaces the store list with remote inheritance
by id join; empty narrowing means undeclared; target_invalid_id and
target_not_declared degradation) and renders the XML block and
markdown section with pinned provenance wording.

Full suite green (91 files, 1690 tests).

* Surface effective targets in instructions (3.4 checkpoint 2)

Both instruction surfaces in both modes now carry the effective target
set: the artifact path assembles in instructionsCommand (change
context and config both in hand) and threads through
GenerateInstructionsOptions; the apply path passes storeTargets and
the resolved config path through the options bag and assembles inside
generateApplyInstructions where the change metadata loads. JSON gets
{source, repos, status} omitted-when-none; human output renders the
target_repos XML block and the Target Repos markdown section after the
referenced-stores blocks. Six surface tests cover provenance on both
surfaces, narrowing with remote inheritance beside a non-narrowed
sibling change, vocabulary warnings in JSON and human at exit 0,
omitted-when-none, pointer sessions reading the resolved root (the
pointer dir's own targets are inert), the unknown-store pin for target
ids, and non-instruction byte-identity. docs/cli.md documents the
declaration and the targets-vs-affected_areas split.

Full suite green (92 files, 1696 tests).

* Fix the 3.4 review findings

Three review mechanisms converged on polish-level findings (no P1/P2),
all folded: change-level target duplicates dedup to a set (first
occurrence wins); the non-array config warning names repo ids for
targets instead of borrowing the references noun; both instruction
surfaces now share ONE wiring shape - the artifact path passes raw
storeTargets/storeConfigPath like apply and assembly happens inside
the generator where change metadata lives (the silently-degrading
asymmetry a second caller would have tripped on); the shared
declaration type is renamed DeclarationEntry (it backs repos and
stores alike) with the stale references-only comment gone; the dead
KEBAB_ID_REGEX export is private again; METADATA_FILENAME is exported
and reused instead of two string literals; the spec's severity-cliff
wording amended to the real blast radius (instructions/status read
metadata; show/validate/archive never did). Recorded for later: the
workspace kebab-regex copy dies with 4.1; the all-invalid-store-ids
empty-repos render is distinguishable by status and stays.

Full suite green (92 files, 1696 tests).

* Apply the 3.4 simplify pass and tick the roadmap

Simplify: the conditional spreads at both command boundaries collapse
to plain optional fields (internal options, not JSON output); the
loader falls back to the self-read config's targets so library callers
omitting the option agree with the CLI wiring; cosmetic blank-line and
spec-wrap leftovers fixed. Skipped with reasoning: a shared id.ts home
for the kebab grammar (3.5's natural move), the references barrel
export note and parseJson consolidation (capstone), import-statement
merges (trivia).

Roadmap: 3.4 boxes ticked, changelog round recorded, pointer moved to
3.5. Full suite green (92 files, 1696 tests).

* Write and review the repo-map slice spec (3.5)

Both adversarial reviews approved with fixes, folded. The P1: the four
registry state-rebuild sites would silently erase the new repos:
section on the next store write - preservation is a pinned scenario
naming the sites. Also folded: repo-check precedence over both
unknown-store branches with a non-looping zero-stores fix; path AND id
cross-section uniqueness with four claimant codes; invalid_repo_id
wording with the --id hint for default folder names; the kebab
predicate's neutral id.ts home; pinned JSON contracts; the honest
one-additional-read wiring; TargetRepoEntry; the recorded Unicode
arrow and corrupt-registry silence decisions.

* Write and review the repo-map plan (3.5)

Both plan reviews approved with fixes, folded: the cross-section check
lives inside assertNoRegisteredStoreConflict (four call sites incl.
three operations preflights - hooking only the write helper would let
setup scaffold files before failing, so an early-reject pin is
planned); getRepoPath reconciled as a dumb id lookup whose 3.5 caller
is repo unregister while the enrichment uses listRepoEntries on its
own read; six missing test mappings added (store list/doctor with both
sections, empty-list verbatim, repo_not_found, mixed-registry positive
resolution, directory-untouched unregister, both-surface enrichment);
two code-map anchors corrected.

* Add typed registry sections and the repo map core (3.5 checkpoint 1)

The machine-local registry gains an optional strict repos: section
beside stores:, carried through parse, serialize, and both store write
helpers (the preservation matrix is pinned - a schema-only change
would have silently erased every repo mapping on the next store
write). Cross-section uniqueness for ids AND paths lives inside
assertNoRegisteredStoreConflict (covering the three operations
preflights) and the new assertNoRegisteredRepoConflict, with the four
claimant codes plus in-section repo_id_conflict/repo_path_conflict.
registerRepo/unregisterRepo/listRepoEntries/getRepoPath form the core
API (rerun no-op, repo_not_found, corrupt-registry null). The kebab
grammar moves to its neutral src/core/id.ts home; change-metadata
re-exports, store foundation and targets consume it, and registry key
validation produces label-accurate wording.

Full suite green (93 files, 1705 tests).

* Add the repo command group, typed rejection, and path enrichment (3.5 checkpoint 2)

openspec repo register/unregister/list manage the machine-local repo
map with the pinned JSON contracts (folder-name default ids with the
--id fix when grammar fails; repo_path_missing/not_directory;
repo_not_found; rerun no-op; unregister never touches the checkout).
--store with a registered repo id now rejects with store_id_is_repo
before BOTH unknown-store branches - including zero-stores, whose fix
suggests a different id instead of looping into the cross-section
conflict - and propagates through the 3.2 pointer with the Declared-in
prefix. Effective-target entries gain a local path when the repo map
resolves them (TargetRepoEntry; arrow and combined renders; one
additional registry read in loadRootConfigContext; corrupt registry
yields bare entries silently). Completions registry, friction pins,
and docs updated; store setup with a repo-claimed id is pinned to
create nothing.

Full suite green (94 files, 1714 tests).

* Fix the 3.5 review findings

Three review mechanisms converged; all fixed with regression tests:
the library API enforces its own invariants (registerRepo validates
path-then-id with typed repo_path_missing/not_directory and
invalid_repo_id errors; unregisterRepo validates ids - a 4.1 caller
gets input errors, not serialize-time registry-corruption noise; the
command rewraps default-folder-name grammar failures with the --id
fix); no-op reruns never take the write lock or rewrite the registry
file (mtime/format churn pinned away); the stale getRepoPath pre-read
in unregister is gone (the locked removal is authoritative); the repo
map is read unconditionally so change-only targets enrich too; a
hand-edited registry with one id in both sections now fails clearly at
parse time instead of resolving ambiguously; store_id_is_repo embeds
its action in the message (human wrappers print message only - the
recorded family precedent); the register/unregister JSON shapes split
into total types; the docs Repo map heading no longer re-parents the
default-store subsection.

getRepoPath stays exported as recorded 4.1 groundwork (unit-tested,
no production caller yet - the 3.3 persisted-remote precedent).

Full suite green (94 files, 1718 tests).

* Apply the 3.5 simplify pass and tick the roadmap

Simplify: the third copy of the JSON/failure plumbing collapses into
commands/shared-output (one definition of the failure contract, used
by store and repo); the same-mapping predicate is hoisted in
registerRepo; the kebab grammar wording single-sources through
KEBAB_ID_DESCRIPTION; an unused test import and two docs nits fixed.
Skipped with reasoning: the registry-state builder quadruplication
(settled mirror territory), validator placement, the unconditional
registry read (measure-by-reasoning verdict: the only correct gate
needs data that arrives after the read on the apply path).

Roadmap: 3.5 boxes ticked, changelog round recorded, pointer moved to
3.6. Full suite green (94 files, 1718 tests).

* Write and review the relationship-health slice spec (3.6)

Both adversarial reviews approved with fixes (two P1s each,
converging), all folded: the exit-code rule now mirrors store
doctor's REAL contract (health findings exit 0; the draft cited a
nonexistent errors-exit-1 behavior); the JSON shape gains the lock's
separate store-metadata section and the 3.4-recorded inert-pointer
deferral lands as pointer_declarations_inert; a real
includeSpecs:false assembler mode replaces the strip-after hedge; the
assembler accepts a pre-read registry so one read feeds everything;
target_unmapped suppressed under unreadable registries;
grammar-invalid targets synthesize bare entries; the both-shapes
detection mechanism and stderr duplication recorded; the
STORE_SELECTION_GUIDANCE consequence scoped; missing scenarios added.

* Write and review the relationship-health plan (3.6)

Both plan reviews converged on three P1-grade holes, all folded: the
registry-injection option inverted the established null semantics (a
fresh machine with no registry file would have been marked unreadable
- the option is now registryEntries with [] = empty and null =
unreadable, mirroring the assembler's post-read variable);
resolveRootForCommand needs an additive allowImplicitRoot
pass-through (it forwards only store/storePath today); and the
invalid-target synthesis would have required parsing ids out of
message strings (the inspector receives raw declarations and uses
isKebabId). Plus: the inert-pointer re-walk named (the declared root
is the store; findRepoPlanningRootSync(cwd) finds the pointer dir);
the human-rendering contradiction resolved in favor of the spec
transcript; truncation-never and pass-through pins mapped; the dead
status key dropped from the failure payload.

* Add the health-mode assembler options and the relationship inspector (3.6 checkpoint 1)

assembleReferenceIndex gains includeSpecs:false (skipping the
spec-file reads AND the byte budget - health entries carry no
specs/fetch keys and the content-only truncation diagnostic can never
appear) and registryEntries injection with the [] -vs- null semantics
that mirror the assembler's own post-read variable (a naive raw-read
injection would mark every fresh machine unreadable). The pure
src/core/relationship-health.ts composes the doctor command's gathered
inputs into the lock's four separated categories, synthesizing
target_unmapped (suppressed under unreadable registries), structural
target_invalid_id entries from the raw declarations (never parsed from
messages), relationship_registry_unreadable, root_pointer_ignored,
pointer_declarations_inert, and the store_remote_divergence info note.

Full suite green (95 files, 1727 tests).

* Add openspec doctor (3.6 checkpoint 2)

The root-scoped relationship-health command: resolves like every
normal command (with the new additive allowImplicitRoot pass-through
on resolveRootForCommand and the null-shape failure payload), gathers
with ONE registry read feeding references, targets, and the unreadable
signal coherently, detects the both-shapes and inert-pointer wrong
turns (the latter via the cwd re-walk, working from subdirectories),
reads store facts for explicit and declared store-backed roots, and
renders the three-heading transcript voice with (none declared)
sections and Fix lines. Health findings of any severity exit 0; only
command failures exit 1. STORE_SELECTION_GUIDANCE gains doctor and the
skill-template parity hashes update deliberately; completions and the
--store description pins extended. Eight e2e tests cover the full
matrix incl. empty-vs-unreadable registries, divergence info, and the
read-only snapshot.

Full suite green (96 files, 1735 tests).

* Fix the 3.6 review findings

Three review mechanisms converged; all fixed with regression tests:
human-mode command failures now print the taxonomy Error/Fix lines
instead of a raw stack trace (the action gained the sibling-standard
try/catch); stale repo mappings surface as target_path_missing (the
lock's 'target checkout health' now actually stats mapped paths);
self-reference-emptied reference lists render '(declared references
all resolve to this root)' instead of the false '(none declared)'; a
malformed store: pointer on a real root surfaces as
root_pointer_invalid (the resolver is silent there); the synthesized
target_invalid_id fix carries the real config path; the inspector
reuses toRootOutput; instructions' registry read now feeds the
reference assembler through the 3.6 injection point (no more torn
snapshots between repoPaths and the index); the human renderer's
duplicated section loops collapse into shared helpers; the spec's
exit-1 list gains the recorded corrupt-store.yaml amendment (store
resolution rejects before doctor runs - a doctor-only resolution path
would break the one-resolver invariant).

Full suite green (96 files, 1739 tests).

* Apply the 3.6 simplify pass and tick the roadmap - Phase 3 complete

Simplify: readRegistrySnapshot extracts the torn-snapshot invariant
into one place (doctor and instructions both consume it); doctor's
catch routes through emitFailure, fixing a --json inconsistency where
post-resolution failures printed human lines without a JSON payload;
shared asStatus duck-types the diagnostic envelope so
RootSelectionError fixes survive; the inspector reuses
storePointerProblem (the fifth phrase copy dies); the existsSync sweep
stats only declared targets; the dead toRootOutput import removed.
Skipped with reasoning: the warning-factory extraction (the fourth
copy does not fit the shape), the config-path-fallback micro-helper.

Roadmap: 3.6 boxes ticked, Phase 3 marked complete on the branch,
changelog round recorded, pointer moved to 4.1.
Full suite green (96 files, 1739 tests).

* Trim the review profile for Phase 5 deletion slices

* Write and review the assemble-working-context slice spec (4.1)

Both adversarial reviews approved with fixes, converging on the
deletion-grounding P1s: binding.ts dies whole (5.1 kept it only for
workspace/foundation's import - with workspace/ gone it would be
exactly the hidden-not-deleted state the criteria reject) and the five
workflow-template workspace-planning guards 5.1 deeded here join the
deletion list with their parity churn named. Also folded: the
change-status-policy cascade enumerated; the shared doctor/context
data gather made mandatory with context recorded as silent on wrong
turns; the member-mapping table pinned; code-workspace write semantics
pinned; getRepoPath deleted rather than re-hidden; fetchRecipe
exported; the naming paragraph recorded.

* Write and review the assemble-working-context plan (4.1)

Both plan reviews approved with fixes, folded: the spec's
code_workspace_exists diagnostic collides with the vocabulary sweep's
workspace_* ban - amended to context_file_exists; the parity test's
workspace-planning guard assertion flips to absence; the policy
tranche names ChangeStatus.affectedAreas and the artifact-graph barrel
re-export; doctor-extraction weakened to behavior-identical; the
unresolved-members-stderr e2e mapped; the sweep guardrail reworded
honestly; stale hedges resolved. Both reviewers verified the deletion
order dependency-safe and every anchor accurate.

* Delete the workspace opening machinery (4.1 checkpoint 1)

The absorbed 2.3, executed leaves-first: the ten workspace-planning
template guards (parity test flipped to a no-residue assertion); the
change-status-policy cascade (summarizeAffectedAreas,
AffectedAreasSummary, affectedAreas plumbing, workspaceName, the
workspace-planning mode member, the workspace next-steps, the
artifact-graph barrel re-export); planning-home collapsed to repo-only
(PlanningHomeKind = 'repo'; the workspace state read and
workspace-planning default schema die); src/core/workspace/ whole
(897 lines) with its barrel line and tests; store/binding.ts whole
(~300 lines - 5.1 kept it only for workspace/foundation's import)
with its barrel line and binding tests; getRepoPath (its recorded
consumers evaporated). The library pins that froze the carve-outs die
with the behavior; the six legacy-groups CLI-surface pins stay green
untouched. The deletion ledger marks the carve-outs executed and the
workspace_skills vocabulary-allowlist entry is pruned. No
.openspec-workspace reads remain anywhere in src.

Net: 27 files, -2,196 lines / +40.

Full suite green (94 files, 1706 tests).

* Add openspec context, the assembled working set (4.1 checkpoint 2)

The working set a root's declarations describe, in one command: the
JSON agent brief (root + members with roles, absolute paths, fetch
recipes on available stores, and the existing fixes verbatim on
unavailable members), the human listing with the Not-available
section, and the --code-workspace editor view (available members only;
ref:/repo: folder prefixes; the pinned write matrix - typed
context_file_exists refusal, --force, no implicit mkdir, stderr
confirmation under --json; stale mapped paths excluded - reported, not
guessed). Assembly is presentation over the 3.6 composition through
the new shared command gather (doctor refactored onto it,
behavior-identical); fetchRecipe exported as the one recipe source.
STORE_SELECTION_GUIDANCE gains context with the parity hashes and
completions pins updated deliberately; docs add the section and the
project-context vs working-context disambiguation.

Full suite green (95 files, 1711 tests).

* Fix the 4.1 review findings

Three review mechanisms converged; all fixed with regression tests:
the --json + --code-workspace failure path now leaves exactly one JSON
document on stdout (the write runs before the brief is printed; both
failure modes pinned); context mirrors doctor's self-reference honesty
('Declared references all resolve to this root' instead of the false
'nothing declared'); the registry degradation is selected by
diagnostic code, never by array position (the fragile health.status[0]
coupling and the redundant boolean+diagnostic pair are gone); the
write summary names the skipped member ids instead of pointing JSON
users at a listing that is not there, with the count arithmetic in
plain form; the dead planningHome params on
buildNextSteps/buildActionContext inputs and their loader threading
are removed; the leftover binding imports in registry.test.ts and
three pieces of edit debris are swept; the ledger's Surviving-tokens
section is pruned; the doctor docs section cross-links context; and
the spec's working-set/builder unit-test bullet is fulfilled
(test/core/working-set.test.ts - the mapping table, ordering,
availability rule, by-code selection, and builder shape).

Skipped with reasoning: suppressing the resolver's both-shapes stderr
warning for context runs (codex P3) - that warning is 3.2 family
behavior for every command at resolution time; forking it per command
would fragment the one-resolver contract. Recorded for the capstone.

Full suite green (96 files, 1715 tests).

* Apply the 4.1 simplify pass and tick the roadmap - Phase 4 complete

Simplify: the stale-path stat sweep moves into shared-gather as
missingDeclaredRepoPaths (doctor and context both consume it; the
header comment now tells the truth); the dead Windows-path machinery
in planning-home dies with the stale workspace-kind test that was its
only exerciser (formatChangeLocation collapses to path.relative); the
garbled vocabulary-sweep comment is repaired; doctor's dead fs import
removed; the context_output_dir_missing code recorded as a plan
amendment instead of silent drift. Skipped with reasoning: the
printEntryDiagnostics extraction (net-zero lines, couples two
surfaces' voices); the three filter passes (readability beats a
one-pass accumulator at single-digit N); PlanningHomeSummary identity
(recorded for the capstone).

Roadmap: 4.1 boxes ticked, Phase 4 complete on the branch, pointer
moved to the Phase 5 remainder.
Full suite green (96 files, 1714 tests).

* Execute the Phase 5 remainder - 5.1 fully closed

Per the locked delete-don't-hide criteria, after 4.1 as queued
(decision record: slices/delete-legacy-command-groups/remainder.md):
schemas/workspace-planning/ deleted (openspec schemas still advertised
the dead workflow); the four workspace-* beta change folders deleted
(unimplemented relics - archiving would assert completion; git
preserves); L2 decided - the four wholly-workspace accepted specs
deleted (capability gone = spec gone) and the workspace requirements
excised from cli-config and cli-artifact-workflow (two requirements,
eight scenarios - bounded short of the docs rewrite the roadmap
forbids). Incidental mentions in five other specs recorded for the
capstone vocabulary audit. All 36 remaining accepted specs validate;
full suite green untouched (96 files, 1714 tests).

* Capstone: all four persona journeys pass (6.1)

Journeys 2 and 3 land as standing e2e in
test/cli-e2e/capstone-journeys.test.ts - the layered PM-to-dev flow
(an app-repo agent discovers the reference from config via openspec
context, cites the upstream spec by following the fetch recipe
verbatim, and writes its design change in the app repo's own root
while the store stays read-only) and externalized planning (a code
repo with only a store: pointer runs new-change through archive with
zero --store flags and never grows planning state). Journey 1 is the
standing store-lifecycle e2e. Journey 4 ran as a live cold-start
headless dogfood: a fresh codex session given only a vague prompt and
--help output assembled the full intended topology - store setup,
targets declaration, pointer config, repo mapping, and
doctor/context/validate self-verification. Results recorded in
capstone/journeys.md.

Full suite green (97 files, 1716 tests).

* Capstone: usability audits done (6.1)

Error-catalog walk: 55 wrong turns exercised live across 13 families
(human + JSON) against the actionable/store-carrying/correct-exit/
honest bar - 46 pass. The resolution-layer taxonomy held
(differentiated no-root hints, single-document JSON failures,
shell-parseable clone fixes, bidirectional namespace collisions). Nine
failures recorded and queued for the capstone fix round: 1 P1 (raw
YAML stack trace on unparseable real-root configs), 4 P2 (pathless
corrupt-registry fix that dead-ends through store doctor, instructions
dropping its Fix line, validate summaries without drill-down,
implicit scaffolding creating doctor-unhealthy roots), 4 P3.
Vocabulary sweep incl. docs/cli.md: clean except the legacy
ChangeStatus.initiative JSON passthrough (queued; the schema keeps
parsing user data). Time-to-first-success measured live: 2 commands,
2 concepts, each step printing the next command.

* Fix the capstone usability-audit findings

All nine error-catalog failures plus the vocabulary finding, with the
test pins updated deliberately:

P1 - unparseable real-root configs no longer dump a YAMLParseError
stack trace: readProjectConfig warns with one line naming the file and
the first error line only (pinned: single line, no node_modules).
P2 - the corrupt-registry fix names the actual registry file path; the
CLI's shared error wrapper (17 catch sites) now prints the diagnostic
fix line it used to drop, so instructions and every sibling carry the
pasteable next step; validate failure summaries print a drill-down
command carrying --store (derived from the resolved root); implicit
scaffolding (new change in a bare dir, non-interactive init) now
creates the complete healthy shape - specs/, changes/archive/, and a
minimal config.yaml - so doctor calls the result ok instead of
unhealthy.
P3 - the malformed-pointer warning on real roots names the file; the
declared-pointer unknown-store fix is reshaped for the actual mistake
(register the store or edit the named config - the user never passed
--store); the store-register-at-code-repo fix offers repo register;
archive not-found lists available changes like its status sibling.
Vocabulary - the legacy ChangeStatus.initiative passthrough is gone
from every surface (status JSON/human, instructions XML, apply text);
the metadata schema still PARSES stored links (user-data tolerance,
pinned by the flipped legacy tests: tolerated, not re-emitted).

Full suite green (97 files, 1716 tests).

* Capstone: technical audits done (6.1)

Single-resolver invariant HOLDS: one precedence implementation, nine
command entry points through it, doctor/init extra walks verified as
post-resolution diagnostics and scaffold guards; one latent
unreachable fallback queued for deletion. Dependency direction HOLDS:
zero core->commands/cli imports. Dead-code sweep over the 213-file
delta: no P2s, five P3s queued, four notes recorded (incl. the ext::
transport status: zero occurrences, the shell-safe gate and
team-committed trust boundary hold). Module sizes bounded (largest
1,160 lines). docs/agent-contract.md committed - every JSON shape,
the diagnostic envelope, failure payloads, the exit-code contract, and
the full diagnostic-code catalog verified against emitting code, with
14 consistency findings; the gauntlet-grade one (several --json
failure paths emit no JSON document) is queued for the gauntlet fix
round. Net LOC vs origin/main: src -4,478, test -325 - net-negative
as the roadmap expected.

* Capstone: whole-delta gauntlet run - findings ledger (6.1)

Four mechanisms over origin/main...HEAD: /code-review at max effort
(all 12 verified candidates CONFIRMED, most live-reproduced), a
32-agent adversarial Workflow (six lenses, refute-style verification:
25 confirmed + 7 completeness gaps), a codex whole-delta review
(FIX-FIRST), and the audits' queued items. Consolidated: 2 P1 (the
~/openspec layout turning $HOME into a phantom nearest root that
captures every lifecycle command under the home tree; status/
instructions --json errors emitting no JSON document), 13 P2 (the
JSON-failure-contract family, the --store-path seam, doctor's
up-walking origin probe, the stale registry lock, config-only
half-scaffolds, prompt-injection via verbatim hostile strings, five
more accepted specs requiring deleted behavior, stale planningHome
guidance in generated skills, a syntactically-broken zsh completion
script, store-remove delete-before-commit, the setup TOCTOU pair, the
orphaned-.git empty-clone path, the metadata rollback race), and a
triaged P3 set split into queued-cheap vs recorded-for-report. The
gauntlet box ticks only when every P1/P2 is fixed and re-verified.

* Fix every gauntlet P1/P2 plus the cheap P3 set (6.1)

P1: the nearest-root walk now skips openspec/ directories that are
neither planning-shaped nor configured - the recommended ~/openspec
store layout no longer turns $HOME into a phantom root that captures
every command under the home tree (regression test: the
registered-store hint fires instead). status/instructions/list/show/
validate --json failures all emit exactly one JSON status document
(JSON-aware shared failure helper; the stray blank stdout lines are
gone; store <unknown subcommand> --json emits a typed document; list
carries its null-shape).

P2: doctor/context gain the --store-path rejection seam; doctor's
origin probe is guarded by isGitRepositoryAtRoot (no more enclosing-
repo origins or spurious divergence notes); the registry lock steals
orphans older than 30s, names the lock path in the busy fix, and
reports permission problems as what they are; change scaffolding
completes the root shape for config-only roots and records the project
default schema, never a one-change --schema override; hostile-content
renders are sanitized at the index/render boundary (spec ids,
summaries at index time, remotes in targets and divergence messages -
control characters can no longer forge instruction lines); the five
remaining workspace-requiring accepted specs got the bounded excision
(all 36 validate); status JSON carries planningHome again (the
generated skills' published archive contract - restored rather than
rewriting eleven template references); the zsh completion generator
uses the correct quote idiom (generated script now passes zsh -n);
store remove commits the registry removal BEFORE deleting files (a
failed deletion degrades to a store_files_left_on_disk warning, never
a phantom registration); setup re-asserts directory facts at execute
(store_setup_path_changed) killing the stale-kind recursive-rm TOCTOU;
the half-made .git cleanup no longer hides behind the created-paths
ledger (no more commitless-store reruns); the metadata rollback
re-reads the registry and never deletes metadata a committed
registration depends on.

P3 (cheap set): CommonMark-correct fence tracking in purpose
extraction; the stale-target sweep requires a DIRECTORY; pretty JSON
for empty list; the declared-pointer repo-id fix names the config
file; absolute change location when the root is not the cwd; docs
fixes (affected_areas legacy wording, --remote in the setup table,
vibe in --tools, the real list output example); agent-contract.md
updated to match (planningHome restored, the failure-contract claim
now true).

Test pins updated deliberately: the store --json hint, the remove
ordering contract, the zsh escaper, the truncation corpus (summaries
now cap at index time, so the budget trips on count).

Full suite green (97 files, 1717 tests).

* Capstone complete: gauntlet passed, release-readiness report committed (6.1)

All 15 gauntlet P1/P2 fixes re-verified live (the JSON-contract codes
on show/validate/status/instructions/store, the --store-path seam on
doctor, the stale-lock steal, the config-only scaffold completion, the
phantom-root regression). The gauntlet ledger marks every finding
fixed. The release-readiness report lands with the five-minute
new-user story (2 commands, 2 concepts, proven cold by a headless
agent), the full audit results, the 18-entry autonomous-decision
ledger, and known gaps mapped to Later Ideas - no open P1/P2 findings
anywhere. Every queue item's roadmap boxes are ticked except Merged to
main, which this run deliberately does not perform.

Full suite green (97 files, 1717 tests); 36 accepted specs validate.

* Fix the phantom-root regression test's environment dependence

The G1 test omitted globalDataDir and never registered a store, so it
passed locally only by accident (it saw this machine's REAL registry)
and failed on the clean CI runner, where the empty registry correctly
fell through to the implicit root. The test now registers a store in
its isolated registry and passes globalDataDir, making the
no_root_with_registered_stores expectation deterministic everywhere.

Full suite green (97 files, 1717 tests).

* Record the user-directed workset correction (post-capstone review)

* Record 4.2 personal worksets with FR1; supersede the change-anchored direction

* Record 4.2 FR2: tool opening with the two-style extensible opener pattern

* Flesh out 4.2 personal worksets as a full roadmap item with its goal run

* Renumber personal worksets to Phase 7 item 7.1

* Add the 7.1 capstone dogfood and branch-push steps to the goal run

* Add the 7.1 personal-worksets research checkpoint

Evidence base for the spec: the f858c19^ opener archaeology (two-style
launch split, PATH/PATHEXT scan, cross-spawn handoff mechanics, the
not-to-inherit ledger), current-tree idioms (registry lock/atomic-write,
the pure .code-workspace builder, the @inquirer house rules, JSON
contracts), and live CLI verification of code/cursor/claude/codex flag
spellings and hazards.

* Windows-compatibility pass per test/AGENTS.md

A two-sided audit of the whole delta (production code and tests)
against the cross-platform rules, with every finding fixed:

Production: extractFirstPurposeLine splits on \r?\n (CRLF checkouts -
the Git-for-Windows default - previously got empty summaries for every
referenced spec); the clone-recipe fix quotes for the rendering
platform (single quotes are literal characters in cmd/PowerShell -
win32 now gets double quotes); the registry path-comparison fallback
resolves nonexistent paths instead of raw string-comparing them; the
manual-deletion fix drops its POSIX-only rm -rf; repo register expands
~ via the same expandUserPath every store command uses.

Tests: the onboarding e2e no longer depends on HOME (USERPROFILE set
alongside), declares its local remote in shell-safe forward-slash
form, pins the platform-correct quote style, and executes the fix via
argv arrays instead of split(' ') re-tokenization (paths with spaces);
the store-references normalizer matches the JSON-escaped needle
(serialized Windows paths double their backslashes); the
metadata-path assertion uses path.join; snapshot keys are
POSIX-normalized in the shared helper and both local copies.

Audited clean: registry/conflict path identity (canonicalized both
sides via realpathSync.native), cross-drive path.relative guards,
getGlobalDataDir's win32 branches, git invocations (argv arrays),
the lock and atomic-write semantics, XDG isolation, fetch-recipe
splits (no paths), and the deliberate-POSIX display literals.

CI: the OS test matrix (linux/macos/windows) previously ran ONLY on
push to main - it now also runs on workflow_dispatch so branches can
get a real Windows verification before merge.

Full suite green locally (97 files, 1717 tests).

* Make the clone-fix unit pin platform-aware

The references.test.ts pin asserted the POSIX single-quote form; the
implementation now deliberately renders double quotes on win32 - the
one remaining windows-pwsh matrix failure. The doctor/context pins are
quote-agnostic (stringContaining on the unquoted prefix) and the
onboarding e2e was already platform-aware.

* Write the 7.1 personal-worksets spec; fold the dual spec review

Subagent: approve-with-fixes; codex: reject (converging). The P1 -
attach-dirs argv now carries one attach pair per member, primary
included, per the locked FR2 wording. Folded: no-tool open path,
stale-saved-tool rule, signal exit contract, the hand-edit parse
contract, pinned JSON envelopes (incl. the open --json typed
rejection), derived-file locking with ENOENT-tolerant remove, the
teammate scenario, the win32 availability matrix, and opener-config
touchpoints. Research+spec roadmap box ticked; changelog entries added.

* Write the 7.1 personal-worksets plan; fold the dual plan review

Subagent: approve-with-fixes; codex: reject (converging). The shared P1:
open now regenerates the .code-workspace under the lock BEFORE tool
resolution, so every fallback names an existing current file. Also
folded: real busy-error factory sites with new byte-shape pins (the
suite never covered the lock mechanics), withWorksetsLock, cross-spawn
import shape, the --member collector, injectable-spawn units for
SIGINT/launch-failed, in-process cancellation coverage with enumerated
capstone carve-outs, the win32 stat-seam fixture strategy, the recorded
TOCTOU, anchor drift fixes, and the spec d12 amendment dropping the
dead workset_create_cancelled code.

* 7.1 CP1: worksets core, opener table, shared file-state mechanics

src/core/file-state.ts extracts writeFileAtomically and the lock-acquire
loop from store foundation (errors stay caller-owned via the injected
factory; store shapes pinned byte-identical by new tests - the suite
never covered the lock mechanics before). src/core/worksets.ts: the
saved-views file under <dataDir>/worksets/ on the registry idiom (strict
zod + version 1, hand-edit parse contract, withWorksetsLock
read-without-write, pure rebuilds, the .code-workspace builder).
src/core/openers.ts: the locked built-in table, per-field config merge
over built-ins, the PATH/PATHEXT availability scan with an injectable
stat seam, and the pure two-style launch-command builder (one attach
pair per member, never a positional). GlobalConfig gains the openers
key. 42 new unit tests; full suite green (99 files, 1759 tests).

* 7.1 CP2: the workset command group, registration, docs, and tests

src/commands/workset.ts: create (guided 3-step wizard / non-interactive
--member collector with name=path labels), list, open (regenerate the
.code-workspace under the lock before any tool resolution so every
fallback names a current file; cross-spawn handoff with honest
exit-code and 128+signal propagation; the Open manually: block on every
cannot-drive failure; hidden --json rejected as one typed JSON
document), remove (plan-then-confirm, --yes, ENOENT-tolerant derived
cleanup under the lock), and the command:* unknown-subcommand handler.
isPromptCancellationError extracted to shared-output (third copy).
CLI + completions registration, the docs/cli.md section and table rows,
the resurrected path-env helper, the fake-tool recorder, 34 command
tests (incl. in-process launch mechanics and interactive-cancellation
coverage via mocked prompts), and the two e2e journeys (no-footprint +
teammate isolation). Full suite green (101 files, 1795 tests).

* Tick 7.1 implementation and tests boxes; record the implementation round

* 7.1 review round: fix all converged P2s from the three review mechanisms

Spec-compliance (compliant-with-fixes), /code-review seven-angle
fan-out, and codex (approve-with-fixes) converged with no P1s.
Behavioral: structural open-fallback rule (surviving members, every
post-regeneration failure except cancellation), the primary-
reassignment note, honest zero-tools message, pasteable launch-failed
alternative, post-save Ctrl-C declines instead of cancelling, parent
signal guard during launch (the 128+n contract was unreachable for tty
SIGINT), sync spawn throws wrapped, tool.cmd PATHEXT double-append
removed, bare workset --json keeps the one-document contract,
deadline-bounded lock stat failures, remove cleanup after the durable
write, early flag-member validation, lazy cross-spawn (~6ms per CLI
invocation). Structure: command layer split (workset / prompts /
input); shared homes for formatZodIssues, folderStyleNameProblem,
KEBAB_ID_FIX, pathIs*; cancellation lifted into emitFailure with
store collapsed onto it. Tests: +6 cases, controlled PATH for the
in-process interactive suite, win32 path-env key fix. Spec amended to
the shipped contracts. Full suite green (101 files, 1799 tests).

* 7.1 simplify pass: collapse the parallel mechanisms the reviews queued

makeLockErrorFactory in file-state (both lock-error factories were
data-twins; store shapes stay byte-pinned), optsWithGlobals over the
hand-rolled group-option merge, the prompt preview ladder flattened
with one assertKnownTool spelling, asErrorMessage hoisted to
shared-output, formatMemberRows deduping three renderers, per-branch
opener resolution in open (dead branch + redundant re-scan gone),
serialize emits validated entries directly, toWorkset dedup, remove
--yes skips the duplicate pre-read, KEBAB_ID_FIX adopted, dead exports
trimmed. Skips recorded (store-group fallback convergence queued for
the next store touch). Full suite green (101 files, 1799 tests).

* 7.1 capstone dogfood passes; transcript committed, box ticked

Scripted walk (both launch styles, exact argv incl. the no-prompt
rule, strand-test fallback, missing-member skip, safe remove,
byte-untouched members), the interactive wizard from a real pty, live
cancellation, and the cold-start headless agent reaching an opened
workset from --help alone. No product findings. Full suite green
(101 files, 1799 tests).

* Close 7.1: pushed-branch box ticked, glance and pointer finalized

* Fix the Linux-only CI failure in the workset launch-failure test

The fixture was a shebang-less text file: macOS posix_spawn rejects it
with ENOEXEC (the spawn error the test wants), but glibc execvp
retries ENOEXEC via /bin/sh, so on Linux the child runs and exits 127
instead of erroring. A shebang pointing at a missing interpreter fails
ENOENT on every POSIX libc with no shell fallback; a garbage
claude.exe covers the win32 matrix leg the same way. Verified in a
node:20 Linux container (old fixture reproduces exit 127; full workset
file passes with the fix) and on macOS (full suite, 1799 tests).

* Add the stores beta user guide

A problem-first guide for the new surface (stores, references,
targets, repo map, doctor, context, worksets) under docs/stores-beta/,
mirroring the old workspaces-beta layout. Built around two team
stories — one team sharing a planning repo, and requirements crossing
team lines — with every command output captured from a live walk of
the current build in isolated scratch state. Carries the beta notice
(shapes may change), the verified resolution-precedence table, known
limitations including the one-checkout-per-store-id rule and the
commands that stay cwd-based, and the real on-disk state locations.
Linked from the README, getting-started, and the cli.md stores
section, which gains the same beta note.

* Carry the beta note on the worksets section of the CLI reference

The note at the top of the Stores section names worksets but is
invisible to a reader deep-linking straight to Personal worksets.

* Fix store --json missing-subcommand output

* Disable CLI-agent workset openers by default

* Remove targets and repo map commands

* Update simplify-context docs after removing targets

* Harden store-root test isolation

* Remove generated review HTML artifacts

* Refresh PR cleanup evidence
2026-06-23 16:53:23 +00:00
openspec-release-bot[bot]andgithub-actions[bot] 1b06fddd59 Version Packages (#1166)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-03 09:31:40 +00:00
Tabish Bidiwale 0a01146c18 [codex] Fix workspace.yaml collision detection (#1165)
* Fix workspace.yaml collision detection

* Store workspace view state under metadata

* Keep top-level update out of workspace updates

* Remove unused workspace root selector

* Allow repo updates below workspace roots

* Propagate repo state probe errors

* Generalize workspace yaml collision coverage
2026-06-03 09:19:54 +00:00
openspec-release-bot[bot]andgithub-actions[bot] bc7ab26650 Version Packages (#1023)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-01 21:23:33 +00:00
Tabish Bidiwale aa16080d16 Add changeset for Mistral Vibe support and validator/completion fixes (#1154) 2026-06-01 21:14:14 +00:00
Tabish Bidiwale 055957fbca clarify changeset release tracking (#1148) 2026-06-01 05:11:50 +00:00
Tabish BidiwaleandAlfred 9e78bcaa80 [codex] Document cross-platform path assertions (#1116)
* docs: document cross-platform path assertions

* docs: mention toPosixPath in path assertion guidance

* chore: remove changeset

---------

Co-authored-by: Alfred <alfred@Alfreds-Mac-mini.local>
2026-06-01 05:05:20 +00:00
e36463074d [codex] Add Mistral Vibe support with CI fix (#1144)
* feat: add mistral vibe support

Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>

* feat: add mistral vibe support

Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>

* chore: archive add-mistral-vibe-support change

Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>

* chore: sync delta specs from add-mistral-vibe-support change

Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>

* chore: fix archive directory date to match metadata

Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>

* fix: correct vibe detection paths and alphabetical ordering

- Remove detectionPaths from Mistral Vibe to prevent double-nested skills dir
- Fix lingma alphabetical position in tool IDs list

Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>

* chore: remove archived mistral vibe change files

Remove archive directory per PR review feedback to keep PR minimal

Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>

* chore: remove mistral vibe spec files per PR feedback

Remove new spec corpus (vibe-tool-config + Mistral Vibe scenario in ai-tool-paths)

Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>

* test: add Mistral Vibe detection regression test

Add focused regression test that proves Vibe initializes and detects
skills under .vibe/skills so the path semantics do not drift.

Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>

* test: tolerate workspace update help wrapping

---------

Co-authored-by: Thomas Betous <4435536+tbetous@users.noreply.github.com>
Co-authored-by: Mistral Vibe <vibe@mistral.ai>
Co-authored-by: tbetous <thomas.betous@doctolib.com>
2026-05-31 14:35:52 +00:00
9aded17af7 fix(validator): hint when SHALL/MUST appears only in requirement header (#1135)
When a change delta has a requirement whose body is missing SHALL/MUST but
whose header (the text after `### Requirement:`) already contains the
keyword, the validator emitted the generic error "must contain SHALL or
MUST". Authors then re-read the spec, see SHALL right there in the header,
and have no idea what the validator wants.

Per the OpenSpec conventions the keyword has to live on the requirement
body line (the line immediately after the header). When the keyword is
present in the header only, append guidance explaining exactly where to
move it. The fix is scoped to the two `validateChangeDeltaSpecs` call
sites (ADDED + MODIFIED) so behaviour for requirements that lack the
keyword everywhere stays unchanged.

Adds three vitest cases under `test/core/validation.test.ts`:
- ADDED block with header-only SHALL → enriched hint
- MODIFIED block with header-only MUST → enriched hint
- Neither header nor body contain SHALL/MUST → generic message preserved

Verified by reproducing the spec from #356, running
`openspec validate <change>` against the rebuilt CLI, and confirming the
new diagnostic guides the author to the fix. Reverting `validator.ts`
makes the two enriched-hint cases fail, so the tests guard the
regression.

Fixes #356

Co-authored-by: Pluviobyte <Pluviobyte@users.noreply.github.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Tabish Bidiwale <30385142+TabishB@users.noreply.github.com>
2026-05-31 08:03:50 +00:00
Tabish Bidiwale 0c5f0c6c48 Improve context-store setup and cleanup UX (#1137)
* Improve context-store setup and cleanup UX

* Address CodeRabbit context-store feedback

* Canonicalize cleanup registry test assertion
2026-05-28 18:02:07 +00:00
Tabish Bidiwale 21c1805d80 [codex] Polish beta context workspace flow (#1136)
* Polish beta context workspace flow

* Allow context-only initiative workspace open

* Add workspace beta compatibility review item
2026-05-28 15:49:13 +00:00
Tabish Bidiwale 11b2690618 test: split slow workspace open CI case (#1134) 2026-05-28 06:54:51 +00:00
Tabish Bidiwale fd92ccca74 [codex] Add context stores and initiative views (#1127)
* Document initiative-led workspace direction

* Add context stores and initiative change links

* Let workspaces open initiative views

* Add workspace root bundle artifacts

* Support legacy workspace roots in planning resolution

* Remove accidental workspace root bundle artifacts

* Bundle workspace reimplementation docs into roadmap

* Preserve workspace context store bindings

* Address review feedback for context store initiatives

* test: canonicalize context store path assertions

* Refine context store and workspace core boundaries

* Avoid initiative diagnostic regex backtracking
2026-05-27 08:06:02 +00:00
Tabish Bidiwale e441287b1f test: normalize workspace change path assertion (#1117) 2026-05-23 05:45:18 +00:00
Tabish Bidiwale 7fdb177158 [codex] Fix Windows workspace path CI failure (#1111)
* fix: handle canonical workspace paths

* docs: document path canonicalization pitfalls

* docs: scope canonicalization notes to tests

* docs: improve test agent guidance

* docs: shorten test agent guidance
2026-05-23 01:38:30 +00:00
Tabish Bidiwale 79303b5210 Update recommended high-reasoning models (#1107) 2026-05-20 18:36:09 +00:00
Tabish Bidiwale 8498042fe8 [codex] Add workspace change planning workflow (#1089)
* Propose workspace change planning

* Implement workspace setup skills phase

* Implement workspace skill updates

* Handle config profile workspace apply

* Implement workspace change creation phase

* Enrich planning context for workspace changes

* Update workflow skills for planning context

* Add workspace planning verification coverage

* Fix workspace update review issues

* Fix workspace skill drift comparison

* Clean up workspace change planning artifacts

* Archive workspace change planning

* Fix archived workspace planning spec purpose

* Address workspace planning review comments
2026-05-14 16:00:56 +00:00
Howard 053d8a59d5 docs(migration-guide): fix inconsistent /opsx:sync description (#1059)
Changed description from 'Preview/spec-merge without archiving' to 'Merge delta specs into main specs' to match commands.md and workflows.md
2026-05-07 02:20:51 +00:00
Tabish Bidiwale b642398bf3 Fix Windows workspace launch arg expectation (#1057) 2026-05-06 04:33:20 +00:00
Tabish Bidiwale ff506c347a Fix Windows workspace CI tests (#1056) 2026-05-06 04:15:18 +00:00
Tabish Bidiwale 1cdf0410df [codex] Propose workspace open agent context (#1054)
* Propose workspace open agent context

* Implement workspace open surface

* Address workspace open review feedback

* Archive workspace open agent context

* Fix workspace open Windows launcher args
2026-05-06 03:53:55 +00:00
Tabish Bidiwale d5c824d4cd archive workspace create and register repos (#1052) 2026-05-06 02:30:11 +00:00
Tabish Bidiwale 849ae2a976 Fix Windows workspace path test expectations (#1055) 2026-05-06 01:54:12 +00:00
Tabish Bidiwale f510581b6c Fix Windows workspace path aliases (#1050) 2026-05-05 17:22:23 +00:00
Tabish Bidiwale 7c3acccaf7 [codex] Add workspace setup commands (#1046)
* add workspace setup commands

* Address workspace review comments

* Address completion review nitpicks

* Improve workspace command UX

* Address workspace review comments
2026-05-04 14:06:40 +00:00
Tabish Bidiwale 435458be56 archive workspace foundation (#1045) 2026-05-04 05:32:18 +00:00
JiangWayandClaude Opus 4.7 76c80f80f3 docs: add Community Schemas section + README entry (#1043)
Adds a "Community Schemas" section to docs/customization.md cataloging
community-maintained schema bundles distributed via standalone
repositories. Modeled after github/spec-kit's community extension
catalog (https://github.com/github/spec-kit/tree/main/extensions).

The first entry is `superpowers-bridge` from JiangWay/openspec-schemas
— born from the proposal in PR #970 and now maintained externally.

Also adds a brief 4-line "Community schemas" introductory section in
README.md (between Docs and Why OpenSpec) pointing readers to the
catalog. Documentation only; no code or schema changes.

Refs: #970

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 01:58:25 +00:00
Tabish Bidiwale 0ca74762dc fix windows workspace data dir paths (#1038) 2026-05-01 23:59:20 +00:00
Tabish Bidiwale e6d81ba0f6 [codex] Complete workspace foundation and setup specs (#1029)
* docs: define workspace foundation and setup specs

* Complete workspace foundation

* Document workspace beta status

* Address workspace PR review comments
2026-05-01 17:36:38 +00:00
davseby 2d189ce5e0 fix: make requirement header parsing case-insensitive (#1031)
* fix: make requirement header parsing case-insensitive

* fix: add tests and cover the rest of requirement header places
2026-05-01 15:55:09 +00:00
Tabish Bidiwale 44e4beeee8 fix omz completion compinit setup (#1033) 2026-05-01 15:30:14 +00:00
Tabish Bidiwale a974c67986 docs: clarify Bun install still requires Node (#1032) 2026-05-01 15:29:51 +00:00
Tabish Bidiwale 485c97e97d [codex] Include sync in core workflow defaults (#1030)
* Include sync in core workflow defaults

* Add old core custom profile sync hint

* Update workflows sync default docs
2026-05-01 14:20:28 +00:00
Yousa 347f0277e3 docs: sync tool ID lists with AI_TOOLS source of truth (#1027)
* docs: sync tool ID lists with AI_TOOLS source of truth

Fixes missing tool IDs in docs/cli.md and docs/supported-tools.md that
drifted from src/core/config.ts (AI_TOOLS).

- docs/cli.md: add bob, forgecode, junie, lingma (25 -> 29)
- docs/supported-tools.md: add lingma, align order with config.ts (28 -> 29)

Follow-up to #1003.

* docs: address AICR feedback on tool ID ordering and table entry

Address review comments from Copilot and CodeRabbit on PR #1027:

- docs/cli.md: reorder lingma to match AI_TOOLS position (between qoder and qwen)
- docs/supported-tools.md: same reordering in the --tools list
- docs/supported-tools.md: add missing Lingma row to Tool Directory Reference
  table (inserted alphabetically between Kiro and OpenCode, matching existing
  table convention)

Verified all three documentation surfaces against AI_TOOLS (29 tools):
- cli.md list: order matches src/core/config.ts
- supported-tools.md list: order matches src/core/config.ts
- supported-tools.md table: set equals AI_TOOLS (alphabetical-by-display-name
  order preserved per existing convention).
2026-04-30 13:49:59 +00:00
Tabish Bidiwale cb9641a450 docs: add workspace reimplementation proposal slices (#1025)
* docs: propose workspace reimplementation slices

* docs: add workspace reimplementation roadmap readme

* docs: add workspace poc reference guide

* docs: add workspace reimplementation entrypoint
2026-04-30 11:03:17 +00:00
Yousa 342ed43e69 feat: add Kimi CLI skills-only support (#1003)
* feat: add Kimi CLI skills-only support

* test: relax Kimi adapterless log assertion
2026-04-30 07:38:48 +00:00
openspec-release-bot[bot]andgithub-actions[bot] 3c7a05c5dc Version Packages (#996)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-04-21 16:14:12 +00:00
Tabish Bidiwale d1f3861d9e Add changeset for v1.3.1 patch fixes (#995) 2026-04-21 16:09:49 +00:00
7a39e887bb fix: escape glob-special characters in directory paths (#984)
* fix: escape glob-special chars in directory paths (#974)

Parentheses and square brackets in project directory paths broke
fast-glob matching, causing glob-based artifact outputs to silently
return empty results. Escape these characters in the directory portion
before passing to fast-glob, preserving glob semantics in the generates
pattern.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: use cwd for artifact output globs

---------

Co-authored-by: furao <furao@didiglobal.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Tabish Bidiwale <30385142+TabishB@users.noreply.github.com>
Co-authored-by: TabishB <tabishbidiwale@gmail.com>
2026-04-21 16:00:10 +00:00
swithekandTabishB 18c445a48d fix: handle XDG_CONFIG_HOME and %APPDATA% in telemetry config path (#990)
* fix: handle XDG_CONFIG_HOME and %APPDATA% in telemetry config path

* fix: migrate telemetry config to resolved path

* fix: address telemetry config review feedback

---------

Co-authored-by: TabishB <tabishbidiwale@gmail.com>
2026-04-21 15:31:23 +00:00
Tabish Bidiwale 900174000b docs: remove teams slack mention from readme (#991) 2026-04-20 02:22:12 +00:00
Tabish Bidiwale f529b25968 test: align path assertions with canonical helper (#975) 2026-04-15 10:32:03 +00:00
Tabish Bidiwale 93f7b797cf fix: prefer native realpath for canonical paths (#972) 2026-04-14 07:43:43 +00:00
Tabish Bidiwale 7d07101363 fix: canonicalize workflow artifact paths (#971) 2026-04-14 07:14:11 +00:00
Alfred c0f29044f9 docs: clarify initiative-first workspace model (#969)
* docs: split workspace initiatives from repo-local changes

* docs: align roadmap and explore ux with initiatives
2026-04-13 12:20:24 +00:00
Tabish Bidiwale 7fe45ca330 Fix apply instructions for glob artifact outputs (#967)
* Fix glob artifact resolution in apply instructions

* Enforce file-only literal artifact outputs
2026-04-12 14:35:11 +00:00
Tabish Bidiwale c8e2072e3a fix: detect hidden requirements in main specs (#966)
* fix: detect hidden main spec requirements

* fix: tighten fenced code parsing
2026-04-12 13:59:10 +00:00
Tabish Bidiwale cd5e49346f docs: expand workspace planning explorations (#965)
* docs: add workspace ux explorations

* docs: update workspace architecture direction
2026-04-12 13:17:17 +00:00
Tabish Bidiwale a18d992fa1 fix: suppress ora spinner output when --json flag is used (#960)
When --json is passed, ora spinners wrote progress text to stderr, which
broke JSON parsing for AI agents that combine stdout+stderr. Conditionally
skip spinner creation in status, instructions, and templates commands.

Closes #957
2026-04-12 03:27:55 +00:00
Tabish Bidiwale 4df6a4889b fix: silence telemetry network errors in firewalled environments (#959)
* fix: silence telemetry network errors in firewalled environments

Wrap PostHog fetch with safeTelemetryFetch that catches all network
errors and non-2xx responses, returning a synthetic 204 so PostHog
never throws PostHogFetchNetworkError. Disable retries, remote config,
surveys, and feature flag preloading to eliminate extra network calls.
Add 1s request timeout. Surface telemetry opt-out docs earlier in
README, installation, and CLI reference.

Closes #895

* fix: clear CI env var in telemetry fetch tests

GitHub Actions sets CI=true which disables telemetry, causing PostHog
to never be instantiated and the fetch wrapper tests to fail.

* docs: add telemetry env vars to Environment Variables table

Addresses CodeRabbit review comment.

* docs: remove unnecessary firewall telemetry warnings

Telemetry now fails silently, so users don't need to proactively
disable it. The env vars are still documented in the reference table.

* docs: restore original config get/set examples in cli.md

These examples document how config works, not telemetry opt-out.
2026-04-12 03:21:11 +00:00
openspec-release-bot[bot]andgithub-actions[bot] 9b5007dbc3 Version Packages (#953)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-04-11 15:43:55 +00:00
Tabish Bidiwale cce787ec40 chore: add changeset for v1.3.0 (#952)
* Add changeset for new tool integrations and bug fixes

* Update changeset with IBM Bob support and pi.dev fix
2026-04-11 15:39:42 +00:00
94d651de8c feat: add support for IBM Bob coding assistant (#886)
* test: add comprehensive tests for Bob Shell adapter

- Add 7 tests covering toolId, file paths, formatting, and edge cases
- Include Bob Shell adapter in cross-platform path handling tests
- All 89 adapter tests now passing
- Ensures Bob Shell adapter works correctly with all 11 workflows

* feat: add Bob Shell adapter support

- Implement Bob Shell command adapter with YAML frontmatter
- Register adapter in CommandAdapterRegistry
- Export adapter from adapters/index.ts
- Add Bob Shell to AI_TOOLS configuration
- Generates commands in .bob/commands/opsx-<id>.md format
- Supports all 11 workflows via custom profile system

* docs: add Bob Shell to supported tools documentation

- Add Bob Shell to README.md supported tools list
- Update docs/supported-tools.md with Bob Shell entry
- Document .bob/commands/opsx-<id>.md command path pattern
- Note that Bob Shell uses commands, not Agent Skills spec

* docs: add Bob Shell support proposal and design documentation

- Add comprehensive proposal for Bob Shell integration
- Document command structure and file format
- Include implementation plan and success criteria
- Preserve change documentation for future reference

* chore: update dependencies and gitignore

- Update package-lock.json with latest dependencies
- Add .bob/ to gitignore (test output directory)

* chore: update gitignore to exclude .bob directory

* openspec change not needed for project, should be kept local

* Update reference from Bob Shell to IBM Bob Shell

* fix: transform command references and add argument-hint for Bob adapter

Bob derives command names from filenames (opsx-apply.md → /opsx-apply),
so body text referencing /opsx:apply is incorrect. Apply the same
transformToHyphenCommands rewrite that opencode.ts uses. Also add
argument-hint frontmatter to match peer adapters (auggie, codebuddy, etc).

---------

Co-authored-by: TabishB <tabishbidiwale@gmail.com>
Co-authored-by: Tabish Bidiwale <30385142+TabishB@users.noreply.github.com>
2026-04-11 15:31:45 +00:00
Tabish Bidiwale 040e382d64 test: fix windows powershell ci (#951) 2026-04-11 15:17:00 +00:00
Tabish Bidiwale caafd7c9bf fix: pi.dev command reference transforms and template args passing (#950)
* fix: pi.dev prompt naming and template args passing (#912)

Fix two pi.dev integration bugs:
- Use colon-based filenames (opsx:explore.md) so CLI commands render as
  /opsx:explore instead of /opsx-explore
- Inject $@ into template body so user arguments are passed through

Adds getLegacyFilePaths to ToolCommandAdapter for migration-safe cleanup
of old hyphenated files during init/update.

* fix: pi.dev command references and template args passing (#912)

- Transform /opsx: references to /opsx- in Pi command bodies and skills,
  matching the hyphenated filename convention (same approach as OpenCode)
- Inject $@ into template body so user arguments are passed through

Pi uses the filename (minus .md) as the slash command name, so
opsx-propose.md becomes /opsx-propose. This keeps filenames
cross-platform safe while ensuring command references in the body
match the actual command names.
2026-04-11 15:00:46 +00:00
Tabish Bidiwale 144528257d fix: make completion install opt-in, fix PowerShell encoding corruption (#949)
* fix: make shell completion install opt-in and fix PowerShell profile encoding corruption (#948)

The postinstall hook silently modified users' shell profiles and corrupted
UTF-16 LE PowerShell profiles by forcing all reads/writes through UTF-8.
Now postinstall only prints a tip, and the PowerShell installer preserves
file encoding via BOM detection on read/write.

* Address review: skip profile on any read error, log warnings, clean up UTF-16 BE handling

- configureProfile: skip profile on any non-ENOENT error instead of falling
  through with empty content (could overwrite real profile)
- removeProfileConfig: log warning on unexpected read errors instead of
  silently swallowing
- detectEncoding: throw directly for UTF-16 BE instead of using sentinel value
- Add test for UTF-16 BE profile rejection
2026-04-11 13:58:24 +00:00
Irina ChichikovaandTabishB af0b3418d0 Add support for Junie from JetBrains tool and command generation (#853)
* Add support for Junie from JetBrains tool and command generation

* Expand Junie support to include `opsx` file patterns and update documentation accordingly

---------

Co-authored-by: TabishB <tabishbidiwale@gmail.com>
2026-04-10 01:01:38 +00:00
7fd5417ed0 style: Fix formatting of user facing and agent facing diagrams and markdown tables (#892)
* style: Fix formatting of user facing and agent facing diagrams and markdown tables

* test: update template parity hashes for formatting changes

---------

Co-authored-by: Tabish Bidiwale <30385142+TabishB@users.noreply.github.com>
Co-authored-by: TabishB <tabishbidiwale@gmail.com>
2026-04-09 07:26:45 +00:00
mrack688andMarck 5ac1e12b83 feat: add Lingma IDE support to configuration (#864)
feat: add Lingma IDE support to configuration
feat: add Lingma IDE support to configuration

Co-authored-by: Marck <6992917@qq.com>
2026-04-09 06:25:27 +00:00
Gregor Albrecht fd7ad273c7 Fix formatting in concepts.md (#882) 2026-04-09 03:34:41 +00:00
Harry James Hall ea6f380fea feat: add ForgeCode tool support (#941) 2026-04-09 03:13:30 +00:00
XingxingandClaude Opus 4.6 765df47ad3 fix(init): prevent false GitHub Copilot auto-detection from bare .github/ directory (#917)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 03:13:04 +00:00
Alfred 64d476f8b9 ci: add merge_group support for merge queue (#918) 2026-04-05 06:56:59 +00:00
afdca0d5da fix(status): exit gracefully when no changes exist (#759)
* fix(status): exit gracefully when no changes exist (#714)

Extract `getAvailableChanges` as a public function from `validateChangeExists`
and use it in `statusCommand` to detect the no-changes case early. Returns a
friendly message (text and JSON modes) with exit code 0 instead of a fatal error.

Generated with Claude Code using claude-opus-4-6.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs: fix design risk description and proposal accuracy

Address CodeRabbit review feedback:
- Fix contradictory risk description in design.md (double-read happens
  when changes exist, not when they don't)
- Clarify in proposal.md that validateChangeExists was internally
  refactored to delegate to getAvailableChanges

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(status): narrow catch in getAvailableChanges to ENOENT only

Return [] only when the changes directory doesn't exist (ENOENT).
Rethrow other errors (EACCES, etc.) so real filesystem issues
surface instead of being silently masked as "no changes".

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Tabish Bidiwale <30385142+TabishB@users.noreply.github.com>
2026-02-27 00:52:22 -08:00
61eb999f7c fix(opencode): use plural commands/ directory to match OpenCode convention (#760)
* fix(opencode): use plural `commands/` directory to match OpenCode convention

The OpenCode adapter was using `.opencode/command/` (singular) but OpenCode's
official documentation specifies `.opencode/commands/` (plural). This aligns
with every other adapter in the codebase. Legacy cleanup updated to detect
old singular-path artifacts. Fixes #748.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(legacy): detect both opsx-* and openspec-* patterns, auto-cleanup in CI

- Extend LegacySlashCommandPattern.pattern to accept string | string[]
- OpenCode legacy entry now detects both opsx-*.md and openspec-*.md
- Auto-cleanup legacy artifacts in non-interactive mode instead of
  aborting with exit 1 (safe: slash commands are OpenSpec-managed,
  config cleanup only removes markers)
- Add 7 tests (6 legacy detection + 1 non-interactive init)
- Update spec with array pattern support and auto-cleanup scenario

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* chore: update task description to reflect dual-pattern support

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Tabish Bidiwale <30385142+TabishB@users.noreply.github.com>
2026-02-27 00:08:22 -08:00
3d3bf96061 docs: fix openspec status examples in cli.md (#761)
* docs: fix `openspec status` examples in cli.md to match actual CLI output

The text and JSON output examples for the status command used incorrect
field names, indicators, and structure. Updated to match real CLI output,
validated against a test project with partial artifacts.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* chore: remove spec change and changeset for docs-only fix

Per reviewer feedback — docs fixes don't need a spec change or
version bump.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Tabish Bidiwale <30385142+TabishB@users.noreply.github.com>
2026-02-26 23:37:28 -08:00
JabinGP d199dfa407 docs: fix docs/concepts nested code-block format (#763) 2026-02-26 10:15:27 +00:00
Tabish Bidiwale d7d186088e docs: realign defaults, profile workflows, and tool references (#746)
* docs: realign defaults, workflows, and tool references

* docs: resolve Trae wording and opsx diagram alignment

* chore: ignore codex workspace directory
2026-02-23 18:27:23 -08:00
openspec-release-bot[bot]andgithub-actions[bot] 6a3a1263fe Version Packages (#751)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-02-23 15:43:08 -08:00
Tabish Bidiwale 1e94443a35 Add changeset for profiles, Pi, Kiro, and bug fixes (#747) 2026-02-23 15:37:30 -08:00
Tabish Bidiwale a0608d0bab Sync update to prune deselected workflows (#741) 2026-02-22 05:35:01 -08:00
e4c32dbe07 feat: add support for Pi (pi.dev) coding agent (#735)
* feat: add support for Pi (pi.dev) coding agent

Add Pi as a supported tool in OpenSpec with full adapter implementation.

Changes:
- Create pi.ts adapter for command generation
- Register adapter in registry and export from index
- Add Pi to AI_TOOLS config with .pi skills directory
- Add tests for piAdapter following existing patterns
- Update supported-tools.md documentation

Pi uses:
- Skills: .pi/skills/ (Agent Skills standard)
- Prompts: .pi/prompts/*.md (with description frontmatter)

Closes #732

* fix: add Pi to LEGACY_SLASH_COMMAND_PATHS for test compliance

* style: add trailing newline to pi.ts

* fix: correct legacy cleanup pattern for Pi (opsx-*.md not openspec-*.md)

* fix: add YAML escaping for Pi adapter to handle special characters in descriptions

- Add escapeYamlValue() function to properly escape YAML special characters
- Apply escaping to description field in frontmatter
- Add tests for YAML special character escaping (colons, quotes, newlines)

This follows the same pattern used by cursor, claude, and windsurf adapters.

* fix: remove Pi from LEGACY_SLASH_COMMAND_PATHS

Pi was never supported in pre-1.0 versions, so no legacy cleanup is needed.
Per reviewer feedback: this is only for tools from pre-1.0 OpenSpec.

* test: relax legacy-cleanup registry coverage invariant

---------

Co-authored-by: Tabish Bidiwale <30385142+TabishB@users.noreply.github.com>
Co-authored-by: TabishB <tabishbidiwale@gmail.com>
2026-02-21 15:34:40 -08:00
Tabish Bidiwale 2d4c98e196 Simplify profile sync + strengthen commands-only coverage (#736)
* Improve profile sync flows and add coverage for commands-only edge cases

* Fix migration workflow preservation and add coverage
2026-02-21 05:48:50 -08:00
Tabish Bidiwale be6659cd39 Add OpenSpec proposals for stacking, install scope, and command surfaces (#733)
* Add OpenSpec change proposals for stacking and scope

* Address review feedback across change proposals

* Preserve legacy install-scope behavior with migration path

* Address remaining review threads across spec proposals

* Address latest review feedback on split and command-surface specs

* Clarify split and composition semantics from latest review
2026-02-21 00:42:15 -08:00
Tabish Bidiwale 4ba26902df feat: simplify skill installation with profiles and smart defaults (#726)
* feat: implement simplified skill installation with profiles and smart defaults

Introduces a profile system (core/custom) to reduce the default workflow
count from 10 to 4, auto-detects AI tools during init, adds a new
`propose` workflow combining new+ff, fixes multi-select keybindings,
and adds backwards-compatible migration for existing users.

* feat: harden update config drift, command-only detection, and init profile validation

Address post-implementation review findings: update now detects profile/delivery
drift even when template versions are current, recognizes command-only installs
as configured tools, init validates --profile values and applies delivery cleanup
on re-init. Specs and design docs updated with new scenarios and rationale.

* fix: address AI reviewer feedback on config, detection, and test cleanup

- Add error handling for execSync in config profile apply and use static import
- Fix config list showing "(explicit)" for core profile workflows misleadingly
- Add missing 'openspec-onboard' to SKILL_NAMES for parity with COMMAND_IDS
- Remove unused fsSync import in init tests
- Fix configTempDir leak in test afterEach cleanup

* docs: add qa smoke harness change proposal
2026-02-19 18:21:06 -08:00
Tabish Bidiwale 5fd8e9d66c feat: simplify skill installation with profiles and smart defaults init (#719)
* feat: add change proposal for simplified skill installation

Introduces a change proposal to simplify the init flow and skill installation:

- Zero-question init with sensible defaults (core profile, both delivery)
- Auto-detect AI tools from existing directories (.claude/, .cursor/, etc.)
- Profile system: core (4 workflows), extended (11 workflows), custom
- Delivery config: both, skills, commands
- New `propose` workflow combining new + ff
- Fix tool selection UX (space to select, enter to confirm)

Key design decisions:
- Extend existing global config (~/.config/openspec/config.json)
- Profile install/uninstall immediately mutates filesystem
- Safe deletion via SKILL_NAMES and COMMAND_IDS constant lookups
- Filesystem as truth for installed workflows

Also adds rules to openspec/config.yaml to prevent overengineering
(explicit lookups over pattern matching).

* chore: add missing .openspec.yaml metadata file

* fix: address PR review feedback

Issues fixed:
- Clarify workflow count: extended = existing 10 + new propose = 11
- Rename spec: tool-auto-detection → available-tools (matches proposal)
- Change "identical" to "functionally equivalent" in propose spec
- Add profile change notification when install/uninstall changes profile
- Specify edge case: uninstall workflow from current non-custom profile
- Specify behavior when --apply-profile confirmation is declined
- Fix section numbering in design.md (6, 6a, 6b, 8)
- Add scaffolding verification tasks (verify .openspec.yaml exists)
- Specify case sensitivity mechanism: use fs.existsSync, let OS handle it

* fix: address CodeRabbit review comments

- Add language specifiers to fenced code blocks in proposal.md
- Add COMMAND_IDS update for propose in modified files list
- Make init success message tool-aware (colon vs hyphen syntax)
- Fix grammar: "Skills-only" and "Commands-only" in delivery-config
- Specify config get delivery output when field absent: "both (default)"
- Add profile set scenarios: config-only vs --apply-profile with filesystem mutation
- Add error scenarios for invalid profile name and unknown workflow
- Add scenario for existing config without profile field
- Mark active profile in profile list output
- Enumerate artifacts in propose basic scenario
- Fix propose equivalence to use skill syntax consistently
- Specify continue/create new branches in propose
- Remove out-of-scope command assertion from skill-generation spec
- Reference SKILL_NAMES constant instead of vague "existing templates"
- Fix design.md: SKILL_NAMES AND COMMAND_IDS (not "only")
- Specify overwrite semantics for refresh/update
- Add task 6.8: propose to COMMAND_IDS
- Fix function name: getAvailableTools() not detectInstalledTools()

* refactor: simplify skill installation design based on review

- Update design to use existing CLAUDE.md mechanisms
- Add cli-update spec for managing skill updates
- Clarify profile system and user config interactions
- Add explorations directory with design notes
- Update docs with clearer concepts

* docs: rename zero-question init to smart defaults init

Clarify that init auto-detects tools and asks for confirmation,
rather than being completely question-free. Update examples to
show the tool confirmation UI.

* docs: add explore workflow tasks and UX exploration

- Add tasks to update explore.ts references to /opsx:propose
- Create exploration note for deeper explore → propose UX questions
- Captures open questions about exploration artifacts, lifecycle,
  context handoff, and transition smoothness

* fix: address PR review feedback from 1code-async

- Add ## Purpose sections to all 10 spec files (required by schema)
- Add specs/ to propose workflow's first-time user guidance scenario
- Add --tools flag scenario for interactive mode in cli-init/spec.md
- Clarify that profile changes take effect on next init/update
- Fix design snippet to use AI_TOOLS config instead of TOOL_DIRS constant
- Add explicit Windsurf detection scenario to available-tools/spec.md
- Mark tasks 10.2-10.3 as follow-up work (out of scope)
- Fix capability name: init → cli-init in proposal.md
2026-02-18 02:11:35 -08:00
Tabish Bidiwale 4108563731 Bulk archive completed changes and normalize source specs (#716)
* chore: bulk archive completed changes and normalize specs

* docs: finalize spec purposes and align init workflow scenarios

* test: guard source specs against placeholders and delta headers

* docs: resolve remaining spec review nits
2026-02-16 21:28:27 -08:00
anilkmr-a2zandTabish Bidiwale fbef555041 feat: add Kiro CLI support (#707)
Add Kiro (AWS AI IDE) as a supported tool with command adapter
that writes to .kiro/prompts/ with YAML frontmatter.

- Add Kiro to AI_TOOLS registry in config.ts
- Create kiro.ts adapter (GitHub Copilot pattern)
- Register adapter in index.ts and registry.ts
- Add legacy cleanup path for migration
- Update supported-tools.md documentation

Generated with Kiro CLI using Claude Opus 4.6

Co-authored-by: Tabish Bidiwale <30385142+TabishB@users.noreply.github.com>
2026-02-16 08:34:42 +00:00
Tabish Bidiwale 92731e2263 refactor: split skill templates into workflow modules (#698)
* refactor: split skill templates into workflow modules

* fix: align template index exports and parity docs

* fix: add standard metadata to feedback skill template

* fix: add ff command guardrail for context and rules

* spec: add unified template generation pipeline proposal
2026-02-15 23:13:52 -08:00
1code-async[bot]andClaude Opus 4.6 c574e7992d docs: clarify GitHub Copilot CLI limitation for custom prompts (#676)
* docs: clarify GitHub Copilot CLI does not support custom prompt files

GitHub Copilot's .github/prompts/*.prompt.md files are only recognized
as custom slash commands in IDE extensions (VS Code, JetBrains, Visual
Studio). The Copilot CLI does not support them (github/copilot-cli#618).
This updates the docs to clarify the limitation and point users to the
.github/agents/ workaround.

Closes #671

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs: use distinct footnote markers for Codex and Copilot

Addresses review feedback: the shared `*` marker was ambiguous across
Markdown renderers. Now uses `*` for Codex and `**` for Copilot.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-02-06 20:07:41 -08:00
pig 62d4391268 fix: improve Windows compatibility in tests (#646)
- Replace Unix 'cat' command with fs.readFile in spec.test.ts

- Replace 'mkdir -p' and 'bash' commands with fs.mkdir/writeFile in validate.enriched-output.test.ts

- Skip symlink test on Windows (requires admin privileges) in file-system.test.ts
2026-02-01 22:57:47 -08:00
Oleksandr Kruk 4573c28048 fix(docs): Update migration guide action diagram formatting (#644) 2026-02-01 16:03:06 -08:00
Tabish Bidiwale 0541f93ddd fix(onboard): add Windows PowerShell alternatives for shell commands (#638)
* fix(onboard): replace broken preflight check and add Windows compatibility

The onboarding preflight used `openspec status --json` to detect if a
project was initialized, but that command requires an existing change
to succeed. After a fresh `openspec init` (no changes yet), it always
failed — causing the onboarding to incorrectly tell users to run init
again.

Replace with `openspec --version` to verify the CLI is installed.

Also add Windows PowerShell alternatives for all platform-specific
shell commands in the onboarding skill:
- `2>&1 ||` → `; if ($LASTEXITCODE -ne 0) {}`
- `2>/dev/null` → `2>$null`
- `mkdir -p` → `New-Item -ItemType Directory -Force`

* fix(onboard): address PR review feedback for PowerShell commands

Use Get-Command for robust CLI detection instead of $LASTEXITCODE
(which stays stale when a command isn't found), and use forward slashes
in PowerShell paths for consistency with Unix commands.
2026-01-31 19:30:43 -08:00
Tabish Bidiwale be51bcbc6a fix(onboard): replace broken preflight check with direct config file test (#637)
The onboarding preflight used `openspec status --json` to detect if a
project was initialized, but that command requires an existing change
to succeed. After a fresh `openspec init` (no changes yet), it always
failed — causing the onboarding to incorrectly tell users to run init
again.

Replace with two targeted checks:
- `openspec --version` to verify the CLI is installed
- `test -f openspec/config.yaml` to verify project initialization
2026-01-31 18:46:50 -08:00
CodingVillainandClaude Opus 4.5 1d34e72f10 fix: use Skill tool for sync invocation in archive templates (#632)
* fix: use Skill tool for sync invocation in archive templates

Update archive skill templates to properly instruct the AI to use
the Skill tool to invoke sync commands instead of executing command
logic directly.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: use Task tool subagent for sync in archive templates

Skill tool terminates after completion and doesn't return control to the
caller, causing archive to not continue after sync. Changed to spawn a
subagent via Task tool which properly returns control after completion.

Also updated opsx:sync references to openspec-sync-specs for semantic
coherence across templates.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-31 18:23:44 -08:00
openspec-release-bot[bot]andgithub-actions[bot] 36fbc898da Version Packages (#628)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-01-30 14:55:43 -08:00
Tabish Bidiwale afb73cf9ec Add changeset for OpenCode command reference fix (#627) 2026-01-30 14:27:05 -08:00
Rodrigo Passos 697738bc9b fix(opencode): transform command references from colon to hyphen format (#626)
* Add OpenCode files to gitignore

* docs(changes): add opencode-command-references change artifacts

* fix(opencode): transform command references from colon to hyphen format
2026-01-30 13:51:47 -08:00
openspec-release-bot[bot]andgithub-actions[bot] 37686944a9 Version Packages (#606)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-01-30 02:27:45 -08:00
Tabish Bidiwale 53081fb2a2 Add changeset for combined v1.0.2+ fixes (#625) 2026-01-30 02:22:17 -08:00
Tabish Bidiwale 5e2e02c090 fix: use path.resolve in Codex adapter test for Windows compatibility (#624)
The test expected path.join('/custom/codex-home', ...) but the
implementation uses path.resolve() which adds the drive letter on
Windows (e.g. D:\). Align the test expectation with the implementation.
2026-01-30 02:14:17 -08:00
Tabish Bidiwale a3cee3c2f2 Revert "feat: add openspec dashboard command for web-based project browsing (#615)" (#623)
This reverts commit f45ba73a5f.
2026-01-30 02:06:17 -08:00
Tabish Bidiwale f27e5e809a feat: support global paths for Codex command generation (#622)
* feat: support global paths for Codex command generation

Codex custom prompts live in ~/.codex/prompts/ (global, not per-project).
Update the Codex adapter to return absolute paths via os.homedir(), handle
absolute paths in init/update writers, and update docs and specs to reflect
the change.

* fix: address review feedback on Codex global paths

- Guard against empty CODEX_HOME resolving to CWD by trimming the env var
- Loosen test regex to not depend on .codex prefix (resilient to custom CODEX_HOME)
- Clarify non-goal wording in design.md to avoid contradictory phrasing
2026-01-30 01:51:00 -08:00
6b545f6ebb fix: add slash command hints in workflow completion messages (#603)
When artifacts or tasks are complete, the command templates now
suggest specific slash commands (/opsx:apply, /opsx:archive) instead
of generic guidance, helping users discover the next workflow step.

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Co-authored-by: Tabish Bidiwale <30385142+TabishB@users.noreply.github.com>
2026-01-29 20:20:28 -08:00
yangjunandTabish Bidiwale 661059b54f Update Windsurf file path from commands to workflows (#610)
* fix windsurf workrules

* fix a missing update

---------

Co-authored-by: Tabish Bidiwale <tabishbidiwale@gmail.com>
2026-01-29 18:02:30 -08:00
Haven-SandTabish Bidiwale ddbfa529f4 docs: modernize opsx.md (#616)
Co-authored-by: Tabish Bidiwale <30385142+TabishB@users.noreply.github.com>
2026-01-29 17:38:23 -08:00
Costica Puntaru [Tica]andTabish Bidiwale d3c3d66e67 fix(archive): fall back to copy+remove on EPERM/EXDEV (fixes #197) (#605)
On Windows, fs.rename() often fails with EPERM when moving non-empty
directories. Fall back to recursive copy then rm when rename throws
EPERM or EXDEV so 'openspec archive' succeeds where Move-Item works.

Co-authored-by: Tabish Bidiwale <30385142+TabishB@users.noreply.github.com>
2026-01-29 17:26:58 -08:00
moe1214andTabish Bidiwale 277be194ef docs: support Trae AI (#601)
Co-authored-by: Tabish Bidiwale <30385142+TabishB@users.noreply.github.com>
2026-01-29 17:23:30 -08:00
Tabish Bidiwale f45ba73a5f feat: add openspec dashboard command for web-based project browsing (#615)
Implements a new `openspec dashboard` command that serves a local HTTP server with a web-based dashboard for exploring changes, specs, and archive. Features include:
- Three-tab navigation for Changes, Specifications, and Archive
- Click artifacts to view rendered markdown in a detail panel
- Domain-grouped specs with requirement counts
- Task progress tracking for active changes
- Artifact status indicators (proposal, specs, design, tasks)
- Archive pagination with reverse chronological sorting
- Zero external dependencies (Node.js built-in http module)
- Port auto-increment (3000-3010) with --port override
- Cross-platform browser opening (macOS, Linux, Windows)
- Path traversal prevention on artifact API

Includes comprehensive tests for markdown renderer, data gathering, and API security (43 tests, all passing).
2026-01-29 16:45:07 -08:00
Tabish Bidiwale 41305753b4 Fix schema listing command in migration guide (#608) 2026-01-27 19:59:47 -08:00
Jérôme BenoitandTabish Bidiwale 86d2e04cae chore(nix): improve flake with dynamic version and build optimization (#550)
* chore(nix): improve flake with dynamic version and source filtering

- Read version dynamically from package.json instead of hardcoding
- Add lib.fileset source filtering to exclude node_modules and build artifacts
- Update update-flake.sh to support dynamic version pattern
- Add hash change detection to skip unnecessary rebuilds
- Improve error handling with automatic rollback on failure
- Update specs to reflect dynamic version behavior

* chore(ci): bump Nix actions to latest versions

- nix-installer-action: v13 → v21
- magic-nix-cache-action: v8 → v13
- Update validation message for unchanged flake.nix

* chore: add changeset for Nix improvements

* fix(nix): make update-flake.sh portable to macOS

- Fix grep pattern on line 37 to include opening parenthesis
- Replace GNU grep -oP with portable sed alternatives (lines 53, 68, 70)
- Ensures script works on both Linux and macOS (BSD sed/grep)

* fix(nix): properly check build verification exit status

Fix logic bug where build failures were incorrectly reported as success.
The script now:
- Captures build exit code and output separately
- Fails fast if build returns non-zero exit code
- Only checks for 'dirty tree' warning if build succeeded

This addresses CodeRabbit review feedback on line 101-107.

---------

Co-authored-by: Tabish Bidiwale <30385142+TabishB@users.noreply.github.com>
2026-01-27 14:23:11 -08:00
openspec-release-bot[bot]andgithub-actions[bot] f6b415cb9b Version Packages (#597)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-01-26 19:33:25 -08:00
Tabish Bidiwale e91568deb9 Add changeset for spec naming clarification (#596) 2026-01-26 19:12:01 -08:00
Tabish Bidiwale fc0d798f93 fix: clarify spec naming convention and task checkbox format (#595)
* fix: clarify spec naming convention and task checkbox format

- Update docs, schema, and templates to clarify that specs should be
  named after capabilities (specs/<capability>/spec.md), not changes
- Emphasize that tasks MUST use checkbox format for apply phase tracking

* fix: clarify delta spec location for modified capabilities

Address review feedback: explicitly state that the delta spec is created
at specs/<capability>/spec.md, not in openspec/specs/<capability>/.
2026-01-26 18:12:56 -08:00
openspec-release-bot[bot]andgithub-actions[bot] d155126235 Version Packages (#588)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-01-26 02:51:24 -08:00
Tabish Bidiwale 943e0d4102 Add changeset for archive path fix (#587) 2026-01-26 02:47:28 -08:00
Tabish Bidiwale 12a7224dc6 chore: remove TDD schema and all references (#586)
* chore: remove TDD schema and all references

TDD was an internal test example that should not be in user-facing docs.
This removes:
- The schemas/tdd directory and all its templates
- All TDD references from documentation
- TDD examples from skill templates and source code comments

* test: update tests to remove TDD schema references

All tests that referenced the removed TDD schema have been updated
to use spec-driven or custom-schema names instead.

* chore: remove accidentally committed files
2026-01-26 02:31:48 -08:00
Tabish Bidiwale c773ef6feb fix: correct archive path in onboarding template (#585)
The onboarding template incorrectly documented the archive path as
`openspec/archive/YYYY-MM-DD--<name>/` when the actual implementation
uses `openspec/changes/archive/YYYY-MM-DD-<name>/`.

This fixes two issues:
- Missing `changes/` directory in the path
- Double dash `--` instead of single dash `-`

Fixes discussion #583
2026-01-26 02:04:46 -08:00
Tabish Bidiwale 0bfe1d4426 docs: rewrite customization guide to document schema commands (#582)
* docs: rewrite customization guide to document schema commands

The old guide described a painful manual process for schema customization
(mkdir, npm list, cp commands) and even listed "No scaffolding" as a
limitation. But the `openspec schema` commands have existed for a while:

- `schema fork` - copy existing schema to customize
- `schema init` - create new schema from scratch
- `schema validate` - check schema structure
- `schema which` - debug resolution precedence

Rewrote the guide to:
- Lead with the actual CLI commands instead of manual steps
- Remove the misleading "Current Limitations" section
- Add practical examples (TDD workflow, adding review artifact)
- Structure progressively: config → custom schemas → global overrides

* docs: add language tags to code blocks in customization guide

Address review feedback from CodeRabbit:
- Add 'text' language tag to directory tree code blocks
- Satisfies MD040 markdown lint rule
2026-01-25 23:37:46 -08:00
openspec-release-bot[bot]andgithub-actions[bot] 0e6f42c81c Version Packages (#579)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-01-25 16:09:16 -08:00
Tabish Bidiwale 0cc9d9025a chore: add changeset for 1.0.0 release (#578)
* Add changeset for v0.24.0 features

* Replace changeset with 1.0.0 release notes

Update from minor to major version bump. Rewrite release notes to
properly capture the OPSX workflow changes:

- Dynamic instructions based on artifact state
- Semantic spec syncing with ADDED/MODIFIED/REMOVED parsing
- Step-through artifacts with /opsx:continue
- Skills support alongside tool commands
- Breaking: old /openspec:* commands removed

* Rewrite 1.0.0 changeset with comprehensive release notes

Based on deep research into old vs new workflow:

Old workflow:
- 3 phase-locked commands (proposal → apply → archive)
- 8+ config files scattered at project root
- Static prompts, same instructions every time
- Text-based spec merging

New OPSX workflow:
- 10 action-based commands (do any action anytime)
- Dynamic instructions (context + rules + template layers)
- Artifact graph with dependency awareness
- Semantic spec syncing (ADDED/MODIFIED/REMOVED/RENAMED)
- Agent Skills standard for cross-editor compatibility
- 21 AI tools supported
- Onboarding skill for guided first experience
2026-01-25 16:04:10 -08:00
Tabish Bidiwale 3261ccf6dc feat: onboarding skill and comprehensive documentation overhaul (#574)
* feat(skills): add opsx:onboard guided workflow skill

Add a new onboard skill that walks users through their first complete
OpenSpec workflow cycle. The skill provides interactive guidance through
task selection, change creation, artifact building, implementation, and
archiving.

Also includes:
- New README with updated branding and workflow examples
- Documentation structure placeholders
- Change artifacts for the onboard skill feature

* test(skills): update skill-generation tests for onboard skill

Update test expectations from 9 to 10 skills after adding opsx:onboard.

* docs: update README links and add doc cleanup checklist

- Replace placeholder links in README_NEW.md with actual doc paths
- Add documentation cleanup checklist to README_RENEWAL_PROMPTS.md

* docs: overhaul documentation with new workflows, getting-started, and customization guides

- Rewrite workflows.md with action-based philosophy and workflow patterns
- Rewrite getting-started.md with clearer onboarding flow
- Rewrite customization.md with schema customization guidance
- Add cross-references between docs (Commands, Customization links)
- Remove obsolete docs: artifact_poc, experimental-release-plan, project-config-demo, schema-customization, schema-workflow-gaps
- Update README_RENEWAL_PROMPTS.md checklist

* docs: continue documentation overhaul with expanded guides and restructuring

- Expand cli.md, commands.md, and concepts.md with comprehensive content
- Add installation.md, multi-language.md, and supported-tools.md
- Rename experimental-workflow.md to opsx.md
- Remove i18n.md (replaced by multi-language.md)
- Update README links and cleanup prompts

* chore(assets): consolidate logo images

* docs: enhance README with badges, usage notes, and contributing guidelines

- Update Discord badge to show member count
- Add collapsible section with stars/downloads/contributors badges
- Add OpenSpec Dashboard preview section
- Add usage notes for model selection and context hygiene
- Expand contributing section with guidelines for small/large changes
- Clarify AI-generated code policy

* docs: remove misleading mid-flight update claims

The documentation claimed users could edit artifacts mid-implementation
and seamlessly continue, but no such mechanism exists. This removes:

- "Mid-Flight Correction" section from workflows.md
- Feedback arrows and "update as you learn" from all diagrams
- Mid-flight claims from commands.md, opsx.md, concepts.md
- Example blocks showing edit-then-continue workflow

Also adds a proposal for future artifact regeneration support that
would actually make this workflow possible.

* docs: fix PR review comments (markdown linting and accuracy)

- Add language tags to fenced code blocks (MD040)
- Remove blank line between blockquotes (MD028)
- Capitalize "Markdown" as proper noun
- Update deprecated command reference (experimental -> update)
- Update skill count from 9 to 10, add openspec-onboard
- Fix typo: fix-midlight -> fix-midflight

* chore: remove polish-release-notes CI workflow

Replaced with local /polish-release skill. The claude-code-action
doesn't work well with repository_dispatch triggers (no PR context).

* docs: clarify /opsx:sync is optional (archive prompts if needed)

Remove sync from main workflow flows and diagrams since archive
already prompts to sync when needed. Most users will never need
to call sync directly.

- Remove sync from completion flow diagrams
- Remove "Sync Specs Regularly" best practice section
- Update command descriptions to note it's optional
- Update "When to sync" to "When to use manually"

* docs: redesign README with simplified content and new OPSX callout

- Simplify badges and logo presentation
- Add collapsible "most loved" section
- Replace detailed explanation with concise philosophy
- Add prominent /opsx:onboard callout for new workflow
- Remove README_NEW.md (content merged into README.md)
- Remove renewal prompts documentation

* docs: add README_OLD.md as reference backup

* docs: fix command directory paths for multiple tools

Correct commands locations for Antigravity, Codex, Crush, OpenCode,
and Qoder in the supported tools table.
2026-01-25 15:43:52 -08:00
zhing2006andClaude Opus 4.5 26ed336a16 fix: correct regex trailing whitespace and add missing projectRoot param (#575)
- Add \s* to parseTasksFile regex to handle trailing whitespace in task lines
- Add missing projectRoot argument to resolveSchema call in generateApplyInstructions

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-24 18:48:05 -08:00
Tabish Bidiwale 847aa81c0f revert: undo README update from init merge (premature) (#567)
Reverts the README documentation changes that were included in PR #565.
These changes document features that aren't released yet.

A separate PR will be opened to re-apply these changes when the package
is released.
2026-01-23 20:06:22 -08:00
Tabish Bidiwale 39bebefcc4 feat(cli): merge init and experimental commands (#565)
* feat(core): add legacy cleanup detection functions for init migration

Implement src/core/legacy-cleanup.ts with detection and cleanup functions
for all legacy OpenSpec artifact types:

Detection functions:
- detectLegacyConfigFiles() - checks for config files with OpenSpec markers
  (CLAUDE.md, CLINE.md, CODEBUDDY.md, COSTRICT.md, QODER.md, IFLOW.md,
  AGENTS.md, QWEN.md)
- detectLegacySlashCommands() - checks for old /openspec:* command
  directories and files across all 21 tool integrations
- detectLegacyStructureFiles() - checks for openspec/AGENTS.md and
  openspec/project.md (project.md preserved for migration hint)
- detectLegacyArtifacts() - orchestrates all detection

Utility functions:
- hasOpenSpecMarkers() - checks if content has OpenSpec markers
- isOnlyOpenSpecContent() - checks if file is 100% OpenSpec content
- removeMarkerBlock() - surgically removes marker blocks from mixed content

Cleanup functions:
- cleanupLegacyArtifacts() - orchestrates removal with proper edge cases:
  - Deletes files that are 100% OpenSpec content
  - Removes marker blocks from files with mixed content
  - Deletes legacy slash command directories and files
  - Preserves openspec/project.md (shows migration hint only)

Formatting functions:
- formatDetectionSummary() - formats what was detected before cleanup
- formatCleanupSummary() - formats what was cleaned up after

This is task 1.1 for the merge-init-experimental change.

* feat(utils): add removeMarkerBlock() for surgically removing marker blocks

- Add removeMarkerBlock() function to file-system.ts that properly handles
  inline marker mentions by using findMarkerIndex/isMarkerOnOwnLine
- Refactor legacy-cleanup.ts to use the shared utility
- Export removeMarkerBlock from utils/index.ts for reusability
- Add comprehensive tests for inline marker mention edge cases
- Add tests for shell-style markers and various whitespace scenarios

The new implementation correctly ignores markers mentioned inline within
text and only removes actual marker blocks that are on their own lines.

* feat(core): add formatProjectMdMigrationHint() for migration messaging

- Add standalone formatProjectMdMigrationHint() function for reusable
  migration hint output directing users to migrate project.md content
  to config.yaml's "context:" field
- Update formatDetectionSummary() to include the migration hint when
  project.md is detected (not just in cleanup summary)
- Refactor formatCleanupSummary() to use the new function for
  consistency
- Add unit tests for the new function and updated behavior

* test(init): rewrite init tests for experimental workflow approach

Rewrites the init command tests to verify the new experimental workflow
implementation. The new tests cover:

- OpenSpec directory structure creation (specs, changes, archive)
- config.yaml generation with default schema
- 9 Agent Skills creation for various tools (Claude, Cursor, Windsurf, etc.)
- 9 slash commands generation using tool-specific adapters
- Multi-tool support (--tools all, --tools none, specific tools)
- Extend mode (re-running init)
- Tool-specific adapters (Gemini TOML, Continue .prompt, etc.)
- Error handling for invalid tools and permissions

Removes old tests for legacy config file generation (AGENTS.md, CLAUDE.md,
project.md, etc.) as the new init command uses Agent Skills instead.

* test(update): rewrite tests for skills/commands refresh behavior

Update the update command tests to match the new implementation that
refreshes skills and opsx commands instead of config files.

Changes:
- Remove old ToolRegistry import (deleted module)
- Rewrite tests to verify skill file updates
- Rewrite tests to verify opsx command generation
- Add tests for multi-tool support (Claude, Cursor, Qwen, Windsurf)
- Add tests for error handling and tool detection
- Fix test assertions to match actual skill template names

The update command now:
- Detects configured tools by checking skill directories
- Updates SKILL.md files with latest skill templates
- Generates opsx commands using tool-specific adapters

* docs(readme): update documentation for new init behavior

- Replace tool list with simplified supported tools section (skills-based)
- Update init instructions to document --tools flag, --force, and legacy cleanup
- Replace project.md with config.yaml documentation
- Update workflow examples to use /opsx:* commands instead of /openspec:*
- Add command reference table for slash commands
- Update Team Adoption and Updating sections for new workflow
- Replace Experimental Features with Workflow Customization section

* refactor(cli): remove legacy configurators and merge experimental into workflow

- Delete src/core/configurators/ directory (ToolRegistry, all config generators)
- Delete legacy templates (agents-template, claude-template, project-template, etc.)
- Move experimental commands to src/commands/workflow/ with cleaner structure
- Remove experimental setup.ts and index.ts (functionality merged into init)
- Update CLI to register workflow commands directly instead of through experimental
- Update openspec update command to refresh skills/commands instead of config files
- Update tests for new command structure

* refactor: extract shared modules and move AGENTS.md to root

- Move AGENTS.md from openspec/ to project root
- Add shared module with tool-detection and skill-generation utilities
- Update legacy-cleanup with improved cleanup logic
- Enhance update.ts with additional functionality
- Add comprehensive tests for shared modules

* fix(ui): update welcome screen tagline

Change from experimental reference to reflect the merged workflow.

* fix: improve Windows cross-platform compatibility

- Handle both forward and backward slashes in path parsing
- Normalize paths before regex matching for legacy artifact detection
- Use regex split for both path separators in tool directory extraction
- Handle CRLF line endings when cleaning up multiple blank lines
- Add retry logic for test file cleanup to handle Windows file locking

* fix(init): use dynamic counts for skills and commands in success message

Replace hard-coded "9 skills and 9 commands" with dynamic values from
getSkillTemplates().length and getCommandContents().length to prevent
the message from diverging from reality when skills/commands change.

* fix: various small improvements across init, cleanup, and file handling

- Remove shell prompt characters from README bash examples (MD014)
- Show actual config filename (config.yaml vs config.yml) in init output
- Include hasProjectMd in hasLegacyArtifacts to show migration hint
- Add existence check before AGENTS.md deletion to avoid spurious errors
- Preserve leading whitespace and original newline style in file operations
- Use dynamic tool list from CommandAdapterRegistry in tests
2026-01-23 19:51:31 -08:00
Tabish Bidiwale cf8b6212c8 feat(cli): merge init and experimental commands (#564)
* feat(cli): add change proposal to merge init and experimental commands

This change merges `openspec init` and `openspec experimental` into a
single command that uses the skill-based workflow as the default.

Key changes:
- BREAKING: init generates skills and /opsx:* commands instead of config files
- BREAKING: Config files (CLAUDE.md, .cursorrules, etc.) no longer generated
- BREAKING: Old slash commands (/openspec:proposal, etc.) no longer generated
- BREAKING: openspec/AGENTS.md and project.md no longer generated
- Add legacy detection and cleanup with Y/N confirmation
- Keep experimental as hidden alias for backward compatibility

Artifacts:
- proposal.md: Motivation and scope
- design.md: Architecture decisions and edge case handling
- specs/legacy-cleanup/spec.md: New capability for legacy artifact cleanup
- specs/cli-init/spec.md: Modified init spec with skill-based workflow
- tasks.md: 37 implementation tasks across 7 groups

* docs(change): preserve project.md with migration hint instead of deleting

Update merge-init-experimental change artifacts to preserve openspec/project.md
during legacy cleanup instead of auto-deleting it. Users will see a migration
hint directing them to move content to config.yaml's context field.

Changes:
- design.md: Add Decision 6 documenting rationale and migration path
- spec.md: Add project.md migration hint requirement and scenarios
- tasks.md: Add task 1.7 for migration hint output

This avoids losing user-written project documentation while guiding them
to the new config.yaml approach.

* docs(change): resolve open questions about update command and experimental labels
2026-01-22 23:15:18 -08:00
Tabish Bidiwale c157483685 feat(skills): add Agent Skills spec optional metadata fields (#563)
Add optional fields to SkillTemplate interface and all skill templates
to improve compliance with the Agent Skills specification:

- license: MIT (matching project license)
- compatibility: Requires openspec CLI.
- metadata: { author: openspec, version: 1.0 }

Updates skill file generation to include these fields in YAML frontmatter.
2026-01-22 22:25:46 -08:00
Tabish Bidiwale f90c7c3354 refactor(commands): modularize artifact workflow into separate files (#562)
* refactor(commands): modularize artifact workflow into separate files

Split the monolithic artifact-workflow.ts into separate modules under
src/commands/experimental/:
- index.ts: main exports and command registration
- status.ts: status display logic
- new-change.ts: change creation logic
- schemas.ts: Zod schemas
- setup.ts: setup command logic
- templates.ts: template generation
- shared.ts: shared utilities
- instructions.ts: instruction generation

Also extracted init wizard logic to src/core/init/wizard.ts.

* fix(commands): address code review feedback from PR #562

- Fix template source detection using path.relative instead of startsWith
  to prevent misclassification of paths with shared prefixes
- Fix config file log message to show actual file name (config.yaml vs config.yml)
- Fix selectedTools option to be honored when provided programmatically
2026-01-22 21:21:05 -08:00
Tabish Bidiwale 9381bd3b24 feat(cli): improve artifact experimental setup with refresh detection (#561)
* feat(cli): improve artifact experimental setup with refresh detection

- Add functions to detect which tools already have experimental skills configured
- Pre-select configured tools in interactive mode for easy refresh
- Sort configured tools to appear first in the selection list
- Show "(refresh)" indicator for already-configured tools in selection
- Distinguish between "Created" and "Refreshed" tools in output
- Streamline success output to be more concise and scannable

* test: update experimental command test assertions for new output format

The output format changed from '.claude/skills/' to '.claude/' (showing
summary counts instead of directory paths), so update assertions to match.

* feat(cli): rename artifact-experimental-setup to experimental

Shorter command name for better usability. Updates command registration,
documentation, and README references.
2026-01-22 20:13:06 -08:00
Tabish Bidiwale ae83b4e16d feat(cli): add interactive UI for artifact experimental setup (#560)
* feat(cli): add interactive UI for artifact experimental setup

Add animated welcome screen and searchable multi-select prompt when
running `openspec artifact-experimental-setup` without the --tool flag
in interactive mode. Users can now browse and select multiple tools
for setup instead of requiring the --tool flag.

- Add welcome screen with ASCII art animation
- Add searchable multi-select prompt component
- Support multi-tool setup in single command invocation

* fix(nix): update flake version and reset hash for rebuild

- Update version from 0.20.0 to 0.23.0 to match package.json
- Set pnpmDeps hash to empty string to trigger rebuild
- Fix update-flake.sh to work on macOS (use portable grep/sed)

CI will fail with correct hash which we'll then apply.

* fix(nix): set correct pnpmDeps hash

* feat(cli): improve error handling for multi-tool setup

- Continue setup for remaining tools when one fails
- Collect and report all failures at the end
- Only throw if all tools fail
- Show partial success summary (configured vs failed)
2026-01-22 18:39:38 -08:00
Tabish Bidiwale d48528134b feat(cli): add multi-provider skill generation support (#556)
* feat(cli): add multi-provider skill generation support

Add --tool flag to artifact-experimental-setup command to generate
skills and commands for different AI tools (Claude, Cursor, Windsurf).

- Add skillsDir field to AIToolOption interface
- Create command-generation module with tool-specific adapters
- Each adapter handles tool-specific file paths and frontmatter formats
- Add CommandAdapterRegistry for adapter lookup
- Update artifact-experimental-setup to use dynamic paths

* feat(config): add skillsDir for all supported AI tools

Add skillsDir mappings for tools that were missing:
- Amazon Q Developer (.amazonq)
- Antigravity (.agent)
- Auggie (.augment)
- Cline (.cline)
- CodeBuddy Code (.codebuddy)
- Continue (.continue)
- CoStrict (.cospec)
- Crush (.crush)
- iFlow (.iflow)
- Qoder (.qoder)
- Qwen Code (.qwen)

Fix RooCode path: .roocode → .roo

* feat(adapters): add command adapters for all supported AI tools

Add 18 new command adapters covering all supported AI tools in the
multi-provider skill generation system. Each adapter implements the
correct file path and frontmatter format for its respective tool.

New adapters: amazon-q, antigravity, auggie, cline, codex, codebuddy,
continue, costrict, crush, factory, gemini, github-copilot, iflow,
kilocode, opencode, qoder, qwen, roocode.

* fix(adapters): address PR review feedback

- Change .requiredOption to .option for custom error handling with tool list
- Add YAML escaping for special characters in all command adapters
- Normalize path separators in tests for cross-platform compatibility
- Update docs: --tool flag is required, not optional with default
- Add missing Windsurf adapter scenario to spec
- Fix spec headers and language specifiers
2026-01-21 23:32:06 -08:00
Tabish Bidiwale 54bd3f1ccd fix(docs): update invalid Discord link in experimental workflow (#555) 2026-01-21 15:04:09 -08:00
QraffaandTabish Bidiwale 675e870bf1 style: remove unnecessary whitespace (#554)
Co-authored-by: Tabish Bidiwale <30385142+TabishB@users.noreply.github.com>
2026-01-21 14:52:27 -08:00
Alexey StepanovandTabish Bidiwale 07eaf7b691 fix(claude): replace colon with dash in slash command frontmatter names (#553)
Claude Code's YAML parser fails when the `name` field contains a colon (e.g., `name: OpenSpec: Proposal`), causing it to fall back to the first content line as the description, showing `<!-- OPENSPEC:START -->`.

Changes:
  - Replace `OpenSpec: Proposal` with `OpenSpec - Proposal` in claude.ts
  - Add `shouldRefreshFrontmatter()` hook to allow updating existing files
  - Extract `buildContent()` helper method for DRY
  - Update test expectations for Claude configurator

Co-authored-by: Tabish Bidiwale <30385142+TabishB@users.noreply.github.com>
2026-01-21 14:36:16 -08:00
153721d14a Add /opsx:bulk-archive to experimental workflow setup command output (#551)
* Add /opsx:bulk-archive to experimental workflow setup command

* Update src/commands/artifact-workflow.ts

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Tabish Bidiwale <30385142+TabishB@users.noreply.github.com>
2026-01-21 13:03:24 -08:00
mingcao-devandpengjiahan.pjh 70c2e17525 chore: rename "Qoder (CLI)" to "Qoder" (#552)
- Update tool name in config.ts
- Update README.md documentation and docs link

Co-authored-by: pengjiahan.pjh <pengjiahan.pjh@antgroup.com>
2026-01-21 13:00:43 -08:00
Tabish Bidiwale e2c333e493 fix(instructions): separate context and rules from template in JSON output (#547)
* fix(instructions): separate context and rules from template in JSON output

Previously, project config context and rules were prepended to the template
field in artifact instructions. This caused AI assistants to literally copy
these constraint blocks into generated artifact files, rather than treating
them as instructions.

Changes:
- Add `context` and `rules` as separate fields in ArtifactInstructions interface
- Update generateInstructions() to populate these as distinct fields
- Update CLI output to display context/rules with clear "do not include" comments
- Rename <context> (dependencies) to <dependencies> to avoid confusion
- Update tests for new structure

The JSON output from `openspec instructions` now clearly separates:
- `context`: Project background (constraints for AI)
- `rules`: Artifact-specific rules (constraints for AI)
- `template`: The actual structure for the output file

* fix(skills): update skill templates with separate context/rules documentation

Update generated skill templates (continue-change, ff-change) to document
that context and rules are separate JSON fields that should NOT be copied
into artifact output files.

Users running `openspec update` will get the updated skill instructions.
2026-01-20 20:52:07 -08:00
Tabish Bidiwale e137dd3981 fix(ci): use repository_dispatch for polish release notes (#545)
The GitHub App token doesn't have actions:write permission, which is
required for workflow_dispatch. Switch to repository_dispatch which
works with existing contents:write permission.

Changes:
- release-prepare.yml: Use gh api to trigger repository_dispatch
- polish-release-notes.yml: Add repository_dispatch trigger type
- Delete test workflow (validation complete)

Tested via PR #542 - both workflow_dispatch and repository_dispatch
triggers work correctly with claude-code-action.
2026-01-20 20:03:56 -08:00
Tabish Bidiwale 2beb8e77e8 test: add temporary workflow to validate repository_dispatch (#542)
This is a dry-run workflow to test repository_dispatch before modifying
the release pipeline. Will be deleted after validation.
2026-01-20 19:01:23 -08:00
openspec-release-bot[bot]andgithub-actions[bot] 3b16b13613 Version Packages (#541)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-01-20 17:21:29 -08:00
Tabish Bidiwale c4cfdc7c49 Add changeset for bulk-archive skill and setup simplification (#540) 2026-01-20 17:17:31 -08:00
Tabish Bidiwale e0736807b4 refactor(setup): simplify config creation and fix test hanging (#537)
* refactor(setup): simplify config creation and fix test hanging

- Replace interactive config prompts with automatic config creation using
  default schema. The generated config includes helpful comments explaining
  context and rules options.
- Remove unused promptForConfig, promptForArtifactRules, and isExitPromptError
  functions from config-prompts.ts
- Add forceExit: true to vitest config to prevent worker processes from hanging
  after tests complete

* docs: add schema-alias-support change proposal

Proposal to add schema alias support so `openspec-default` and `spec-driven`
can be used interchangeably, enabling a rename without breaking existing configs.

* fix(test): remove invalid forceExit config and add proper teardown

- Remove `forceExit: true` from vitest.config.ts (Jest option, not Vitest)
- Add actual teardown logic in vitest.setup.ts that forces exit after 1s
  grace period if processes are still hanging
2026-01-20 14:32:56 -08:00
Tabish Bidiwale fdb05a723e feat(skills): add bulk-archive skill for archiving multiple changes (#527)
Add `/opsx:bulk-archive` skill that allows archiving multiple completed
changes in a single operation. Features include:

- Multi-select change selection via AskUserQuestion
- Batch validation of artifacts, tasks, and delta specs
- Spec conflict detection when multiple changes touch same capability
- Agentic conflict resolution by checking codebase for implementation
- Consolidated status table before confirmation
- Single confirmation for entire batch operation
- Comprehensive summary showing archived/skipped/failed changes

This is useful when working on multiple changes in parallel and wanting
to archive them together after implementation is complete.
2026-01-20 11:37:33 -08:00
Tabish Bidiwale 8332a09811 fix(ci): use workflow_dispatch for polish release notes (#533)
* fix(ci): use workflow_dispatch for polish release notes

The claude-code-action doesn't support the `release` event type.
Switch to workflow_dispatch which is supported, and have
release-prepare trigger it after publishing.

* fix: get tag from package.json instead of gh release list
2026-01-20 00:36:24 -08:00
Tabish Bidiwale d61a49f6d5 fix(changelog): convert markdown headers to bold text for proper formatting (#532)
Changeset descriptions that use markdown headers (### New Features) get
nested inside list items, causing poor rendering. This converts all
affected entries to use **Bold Text** instead, which renders correctly.

Also adds a brief summary line to each entry for better readability.
2026-01-20 00:09:49 -08:00
openspec-release-bot[bot]andgithub-actions[bot] 7d1237f00d Version Packages (#531)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-01-20 00:01:09 -08:00
Tabish Bidiwale 33466b1e2a Add changeset for project config and schema commands (#530) 2026-01-19 23:58:09 -08:00
Tabish Bidiwale 6c8c778043 fix(config): handle null rules field in project config (#529)
* feat(config): add project-level configuration via openspec/config.yaml

Adds openspec/config.yaml support for project-level customization without
forking schemas. Teams can now:

- Set a default schema (used when --schema flag not provided)
- Inject project context into all artifact instructions
- Add per-artifact rules (e.g., proposal rules, specs rules)

Key changes:
- New `src/core/project-config.ts` with Zod schema and resilient parsing
- New `src/core/config-prompts.ts` for interactive config creation
- Updated schema resolution order: CLI → change metadata → config → default
- Updated instruction generation to inject <context> and <rules> XML sections
- Integrated config creation prompts into `artifact-experimental-setup`

Schema resolution precedence:
1. --schema CLI flag (explicit override)
2. .openspec.yaml in change directory (change-specific)
3. openspec/config.yaml schema field (project default)
4. "spec-driven" (hardcoded fallback)

* test(config): add e2e tests and performance benchmarks for project config

- Add project config integration tests in artifact-workflow.test.ts:
  - Test new change uses schema from config
  - Test CLI schema overrides config schema
  - Test context and rules injection in instructions
  - Test backwards compatibility without config file
  - Test immediate reflection of config changes

- Add performance benchmark tests in project-config.test.ts:
  - Typical config (1KB): <20ms target
  - Large config (50KB): <50ms target
  - Repeated reads consistency check
  - Missing config fast-path test

- Add project configuration documentation in experimental-workflow.md:
  - Config fields reference (schema, context, rules)
  - Schema precedence explanation
  - Artifact IDs by schema
  - Troubleshooting guide

- Mark all tasks complete in tasks.md

* refactor(config): remove benchmark tests, document decision in source

Remove performance benchmark tests from project-config.test.ts and
document the results directly in the source code instead. Benchmarks
showed config reads are fast enough (~0.5ms typical) that caching
is unnecessary.

* fix(config): resolve three issues in project config feature

- Remove nested <template> tags: generateInstructions() no longer wraps
  template content since printInstructionsText() already handles XML
  structure
- Fix schema message accuracy: newChangeCommand() now uses the resolved
  schema returned from createChange() instead of hardcoded fallback
- Add TTY check: artifact-experimental-setup skips interactive prompts
  in non-TTY environments (CI, automation) to prevent hangs

* fix(config): handle null rules field in project config

Add null check when parsing rules field since YAML `rules:` with no
value parses to null, and `typeof null === 'object'` in JavaScript.
Without this check, Object.entries(null) would throw an error.
2026-01-19 23:49:11 -08:00
Tabish Bidiwale 43b01ad374 docs: update workflow docs and mark schema commands as experimental (#526)
* docs: update workflow docs for schema management CLI

Update documentation to reflect implemented schema management features:
- Document schema CLI commands (which, validate, fork, init)
- Update gap summary to show completed phases (PR #522, #525)
- Improve custom schema examples with actual CLI usage
- Update resolution order documentation

* feat(cli): mark schema commands as experimental

Add [experimental] tag to help description and runtime warning
for schema management commands to indicate they may change.
2026-01-19 20:41:15 -08:00
Tabish Bidiwale 3cdcdfca8e feat(cli): add schema management commands (#525)
Add `openspec schema` command group with subcommands for managing
workflow schemas:

- `schema which [name]` - Show where a schema resolves from with
  shadow detection across project/user/package locations
- `schema validate [name]` - Validate schema structure, templates,
  and dependency graph
- `schema fork <source> [name]` - Copy an existing schema to project
  for customization
- `schema init <name>` - Create a new project-local schema with
  interactive or CLI-driven configuration

All commands support `--json` output for scripting. The init command
supports interactive prompts for description and artifact selection.

Implements the schema-management-cli change proposal.
2026-01-19 19:42:37 -08:00
Tabish Bidiwale 32fc19a60d fix: Windows path compatibility in resolver tests (#524)
- Use path.join() in test expectations instead of hardcoded forward slashes
- Add openspec/config.yaml with cross-platform requirements to prevent
  similar issues in future proposals

The tests were failing on Windows because path.join() uses backslashes
on Windows, but the test expectations hardcoded forward slashes.
2026-01-19 18:42:00 -08:00
Tabish Bidiwale 84f372517f change(schema-management-cli): proposal for schema management commands (#523)
* change(schema-management-cli): add proposal for schema management commands

Propose new CLI commands to improve the UX of creating and managing project schemas:

- `openspec schema init <name>` - Interactive wizard to scaffold new schemas
- `openspec schema fork <source> [name]` - Copy existing schema for customization
- `openspec schema validate [name]` - Validate schema structure before runtime
- `openspec schema which <name>` - Debug schema resolution path

* change(schema-management-cli): add design, tasks, and specs artifacts

Add implementation artifacts for the schema management CLI feature:
- Design document with decisions and rationale
- Task breakdown for implementation
- Specs for schema init, fork, validate, and which commands
2026-01-19 18:22:44 -08:00
Tabish Bidiwale adda63e17a feat(resolver): add project-local schema support (#522)
Add 3-level schema resolution: project-local → user override → package built-in.

- Add `getProjectSchemasDir(projectRoot)` to resolve project schemas at `./openspec/schemas/<name>/`
- Extend `SchemaInfo.source` type to include `'project'`
- Update `getSchemaDir()`, `resolveSchema()`, `listSchemas()`, `listSchemasWithInfo()` with optional `projectRoot` parameter
- Update CLI commands to display schema source labels (project/user/package)
- Add `projectRoot` to `ChangeContext` interface for proper resolution throughout workflow
- Add 17 new tests covering project-local schema resolution

This enables projects to define custom schemas that override user and package schemas,
while maintaining backward compatibility when projectRoot is not provided.
2026-01-19 18:01:35 -08:00
Tabish Bidiwale 90d05b7115 docs: add project-config demo guide (#521)
Add a quick-reference demo guide for the project-config feature
(openspec/config.yaml). This consolidates the demo walkthrough
into a standalone document that's easier to use when presenting
the feature.

Includes:
- Summary of what project config does
- 6 numbered demo scenarios
- Quick all-in-one demo script
- Key points to emphasize
2026-01-19 16:56:12 -08:00
Tabish Bidiwale 20714c1c28 feat(config): add project-level configuration via openspec/config.yaml (#499)
* feat(config): add project-level configuration via openspec/config.yaml

Adds openspec/config.yaml support for project-level customization without
forking schemas. Teams can now:

- Set a default schema (used when --schema flag not provided)
- Inject project context into all artifact instructions
- Add per-artifact rules (e.g., proposal rules, specs rules)

Key changes:
- New `src/core/project-config.ts` with Zod schema and resilient parsing
- New `src/core/config-prompts.ts` for interactive config creation
- Updated schema resolution order: CLI → change metadata → config → default
- Updated instruction generation to inject <context> and <rules> XML sections
- Integrated config creation prompts into `artifact-experimental-setup`

Schema resolution precedence:
1. --schema CLI flag (explicit override)
2. .openspec.yaml in change directory (change-specific)
3. openspec/config.yaml schema field (project default)
4. "spec-driven" (hardcoded fallback)

* test(config): add e2e tests and performance benchmarks for project config

- Add project config integration tests in artifact-workflow.test.ts:
  - Test new change uses schema from config
  - Test CLI schema overrides config schema
  - Test context and rules injection in instructions
  - Test backwards compatibility without config file
  - Test immediate reflection of config changes

- Add performance benchmark tests in project-config.test.ts:
  - Typical config (1KB): <20ms target
  - Large config (50KB): <50ms target
  - Repeated reads consistency check
  - Missing config fast-path test

- Add project configuration documentation in experimental-workflow.md:
  - Config fields reference (schema, context, rules)
  - Schema precedence explanation
  - Artifact IDs by schema
  - Troubleshooting guide

- Mark all tasks complete in tasks.md

* refactor(config): remove benchmark tests, document decision in source

Remove performance benchmark tests from project-config.test.ts and
document the results directly in the source code instead. Benchmarks
showed config reads are fast enough (~0.5ms typical) that caching
is unnecessary.

* fix(config): resolve three issues in project config feature

- Remove nested <template> tags: generateInstructions() no longer wraps
  template content since printInstructionsText() already handles XML
  structure
- Fix schema message accuracy: newChangeCommand() now uses the resolved
  schema returned from createChange() instead of hardcoded fallback
- Add TTY check: artifact-experimental-setup skips interactive prompts
  in non-TTY environments (CI, automation) to prevent hangs
2026-01-19 16:39:43 -08:00
Tabish Bidiwale 2e51ae26d3 fix: auto-trigger polish release notes on release publish (#519)
* perf: add path filtering to Nix validation CI job

Skip Nix flake validation when no Nix-related files change. This speeds
up CI for PRs that only touch source code, docs, or tests.

Files that trigger Nix validation:
- flake.nix, flake.lock
- package.json, pnpm-lock.yaml
- scripts/update-flake.sh
- .github/workflows/ci.yml

Uses dorny/paths-filter@v3 for change detection. Required-checks jobs
updated to handle skipped status correctly.

* fix: auto-trigger polish release notes on release publish

The workflow was only set up for manual dispatch, requiring someone to
remember to run it after each release. This change adds an automatic
trigger on `release: [published]` events while keeping the manual
trigger as a fallback.

The Claude Code Action supports any GitHub event when using the `prompt`
parameter for custom automations.
2026-01-19 01:28:45 -08:00
Tabish Bidiwale dbd4ed7bfb perf: add path filtering to Nix validation CI job (#518)
Skip Nix flake validation when no Nix-related files change. This speeds
up CI for PRs that only touch source code, docs, or tests.

Files that trigger Nix validation:
- flake.nix, flake.lock
- package.json, pnpm-lock.yaml
- scripts/update-flake.sh
- .github/workflows/ci.yml

Uses dorny/paths-filter@v3 for change detection. Required-checks jobs
updated to handle skipped status correctly.
2026-01-19 01:22:33 -08:00
openspec-release-bot[bot]andgithub-actions[bot] 473093f885 Version Packages (#517)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-01-19 00:59:49 -08:00
Tabish Bidiwale b5a884748b Add changeset for v0.21 release (#516) 2026-01-19 00:56:17 -08:00
Tabish Bidiwale 690c75225c fix: prevent implementation during explore mode (#515)
Add explicit instructions to both the explore skill and slash command:
- Add IMPORTANT notice clarifying explore is for thinking, not implementing
- Add "Open threads, not interrogations" stance for user-driven exploration
- Add "Don't implement" guardrail to both templates

Creating OpenSpec artifacts is still allowed since that captures thinking.
2026-01-19 00:28:45 -08:00
Tabish Bidiwale dd53fb7736 OPSX apply: infer target change (#513)
* opsx: infer change for apply

* opsx: prompt when apply ambiguous

* opsx: use AskUserQuestion when ambiguous

* Simplify opsx:apply change selection instructions

Update all change selection instructions across all opsx commands
(apply, continue, sync, archive, verify, ff) to use consistent wording:

"If omitted, check if it can be inferred from conversation context.
If vague or ambiguous you MUST prompt for available changes."

For apply specifically, also simplifies Step 1 from ~180 to ~60 words
while preserving the same behavior: infer from conversation, auto-select
if single change, prompt via AskUserQuestion if ambiguous, always announce.

Removes micromanagement details (validation commands, recommendation
markers, presentation specifics) and trusts the LLM to figure out
reasonable defaults.
2026-01-18 19:44:56 -08:00
Tabish Bidiwale 2a441c472d Refine opsx archive sync assessment (#514)
* Refine opsx archive sync assessment

* Simplify opsx archive sync instructions

Replace verbose algorithmic instructions with intent-focused guidance.
Trust the agent to figure out comparison logic rather than specifying
the exact algorithm for each delta type (ADDED, MODIFIED, etc).

Reduces section 4 from ~31 lines to ~13 lines per template.

* Add explicit paths for delta spec locations

Make it clear where agents should look for delta specs and main specs:
- Delta specs: openspec/changes/<name>/specs/
- Main specs: openspec/specs/<capability>/spec.md
2026-01-18 19:35:04 -08:00
Pim SnelandTabish Bidiwale ed4d965208 feat: add nix flake support (sorry for this duplicate) (#459)
* add nix flake support

* feat: add Nix flake maintenance automation

* Add Nix Flake CI Validation

* fix updatescript, update flake

* make update-script compatible with macos

---------

Co-authored-by: Tabish Bidiwale <30385142+TabishB@users.noreply.github.com>
2026-01-16 13:00:11 -08:00
Tabish Bidiwale c86985d6ec feat: add feedback command for submitting user feedback (#509)
* feat: add feedback command for submitting user feedback

Implement `openspec feedback` command that creates GitHub Issues using the gh CLI.
Includes graceful fallback to manual submission when gh is not available or
not authenticated.

Features:
- Automatic gh CLI detection and authentication check
- Graceful fallback with pre-filled issue URLs for manual submission
- Automatic metadata inclusion (version, platform, timestamp)
- /feedback skill for agent-assisted feedback with context enrichment
- Comprehensive test coverage with mocked gh CLI calls

* fix: address PR review comments for feedback command

- Add Windows compatibility: use 'where gh' on Windows, 'which gh' on Unix/macOS
- Fix shell injection vulnerability: replace execSync with execFileSync and argument arrays
- Fix British English phrasing: "in future" → "in the future"
- Reduce code duplication: extract formatTitle/formatBody to single location
- Update tests to verify execFileSync usage and cross-platform command detection
- Add spec scenarios for safe command execution and cross-platform support
2026-01-14 15:48:29 -08:00
Tabish Bidiwale bf4bc2426f fix: add auto-approval for file writes in polish-release-notes workflow (#505)
The workflow was failing because Claude Code requested permission to write files
(release-title.txt and polished-notes.md) but there was no interactive user to approve.

Added claude_args: "--allowedTools Write,Read" to pre-approve file operations
in automation mode.
2026-01-13 23:09:37 -08:00
Tabish Bidiwale c57e421cc2 fix: update polish-release-notes workflow to use correct Claude Code action parameters (#504)
The workflow was failing due to two issues:
1. Using unsupported 'release' event trigger - Claude Code action doesn't support this event type
2. Using deprecated 'direct_prompt' parameter instead of 'prompt'

Changes:
- Switch from 'release' trigger to 'workflow_dispatch' for manual triggering
- Replace 'direct_prompt' with 'prompt' parameter (v1.0 breaking change)
- Update all tag references from github.event.release.tag_name to inputs.tag_name

The workflow now needs to be manually triggered from GitHub Actions UI after a release is published.
2026-01-13 23:02:22 -08:00
openspec-release-bot[bot]andgithub-actions[bot] ed2e832066 Version Packages (#503)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-01-13 22:50:11 -08:00
Tabish Bidiwale 9db74aa5ac chore: add changeset for opsx:verify skill and bug fixes (#502)
* Add changeset for opsx:verify skill and bug fixes

* Update changeset to follow template format
2026-01-13 22:45:23 -08:00
Tabish Bidiwale b5b7248610 feat(skills): implement /opsx:verify skill for validating change implementations (#501)
Add comprehensive verification skill that checks implementation completeness, correctness, and coherence against change artifacts. The skill validates task completion, spec coverage, requirement implementation, and design adherence.
2026-01-13 22:38:49 -08:00
Tabish Bidiwale 322bfd455a fix(vitest): cap worker parallelism to prevent process storms (#500)
Vitest v3 defaults to `pool: "forks"` and scales worker processes with
CPU count. This repo's tests spawn many Node processes (CLI invocations,
temp FS operations), which can cause runaway CPU/memory usage when
combined with high parallelism.

Changes:
- Explicitly set pool to 'forks' (tests rely on process isolation)
- Add resolveMaxWorkers() to cap workers at min(4, availableCPUs)
- Allow VITEST_MAX_WORKERS env override for CI/automation tuning
2026-01-13 21:41:25 -08:00
Tabish Bidiwale 08c349369a docs: add MAINTAINERS.md with core maintainers and advisors (#495) 2026-01-13 21:05:28 -08:00
Tabish Bidiwale 40afee643e chore(openspec): add feedback command change proposal (#496)
Add change proposal for `openspec feedback` CLI command that enables
users and agents to submit feedback via GitHub Issues.

Key features:
- Simple `openspec feedback <message>` command
- GitHub Device OAuth for authentication
- `/feedback` skill for agent-assisted feedback with context enrichment
- Anonymization of sensitive data before submission
- User confirmation required before submitting
2026-01-13 15:39:55 -08:00
Tabish Bidiwale 05023dab43 feat(skills): add /opsx:verify change proposal (#497)
Add change proposal for a new verification skill that validates
implementation matches change artifacts (specs, tasks, design).

The skill verifies three dimensions:
- Completeness: all tasks done, all specs addressed
- Correctness: implementation matches specs and scenarios
- Coherence: follows design decisions and project patterns

Produces prioritized report with actionable fix suggestions.
2026-01-13 15:26:31 -08:00
Tabish Bidiwale d7a928b4e9 fix(agents): add --no-interactive to validate commands in agent workflows (#494)
AI agents following OpenSpec workflows would hit interactive prompts when
running `openspec validate` because the commands in AGENTS.md and slash
command templates didn't include the --no-interactive flag.

This caused hangs in LLM tool execution since agents run in pseudo-TTY
environments where process.stdin.isTTY returns true, triggering
interactive mode.

Fixes #492
2026-01-13 14:50:43 -08:00
Nob ShinjoandTabish Bidiwale 07dd634986 fix(powershell-generator): remove trailing comma from last entry (#485)
Co-authored-by: Tabish Bidiwale <30385142+TabishB@users.noreply.github.com>
2026-01-13 13:50:11 -08:00
Tabish Bidiwale 36078b1947 chore: improve release notes pipeline (#481)
* chore: improve changelog generation with GitHub integration

- Switch to @changesets/changelog-github for PR/author links in CHANGELOG.md
- Add comprehensive changeset README with template and contributor guidance
- Remove release:local script (CI-only releases)

* ci: add AI-powered release notes polishing

Transforms raw changelog into user-friendly release notes when a
GitHub Release is published. Uses Claude Code Action to:
- Generate concise release title (e.g., "v0.18.0 - OPSX Workflow")
- Rewrite changelog as developer-friendly release notes
- Remove noise (commit hashes, PR numbers, internal changes)

Requires CLAUDE_CODE_OAUTH_TOKEN secret (from claude setup-token).
2026-01-10 22:33:43 -08:00
Tabish Bidiwale d0e1b076c2 chore: trigger release workflow for v0.19.0 (#480) 2026-01-10 18:20:30 -08:00
Tabish Bidiwale 2fbda520de ci: remove auto-merge to fix release triggering (#479)
GitHub's auto-merge feature uses an internal token that cannot trigger
workflows. Even with manual approval, the merge performed by auto-merge
doesn't trigger the release workflow.

Removing auto-merge means:
1. Version PR is created with CI running
2. User manually merges the PR (clicking "Merge")
3. The merge triggers the release workflow
4. Package is published

This aligns with industry standard practice for changesets.
2026-01-10 18:18:09 -08:00
github-actions[bot] 5633556b6d Version Packages (#475)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-01-10 18:04:55 -08:00
Tabish Bidiwale 2bb0ed36c5 ci: pass GitHub App token to checkout for CI triggering (#478)
Move the GitHub App token generation before checkout and pass it
to actions/checkout. This configures git to use the App's identity
for all operations, allowing pushes to trigger CI workflows.

Removes commitMode: github-api which doesn't support executable files.
2026-01-10 18:01:05 -08:00
Tabish Bidiwale 06097f9cb7 ci: add commitMode github-api to trigger CI on version PR (#477)
* ci: use GitHub App token to trigger CI on version PR

Replace GITHUB_TOKEN with a GitHub App token so that the version PR
can trigger CI workflows. GITHUB_TOKEN cannot trigger workflows by
design (to prevent infinite loops).

Requires APP_ID variable and APP_PRIVATE_KEY secret to be configured.

* ci: upgrade create-github-app-token to v2

* ci: add commitMode github-api to trigger CI on version PR
2026-01-10 17:50:59 -08:00
Tabish Bidiwale 8f5a526396 ci: use GitHub App token to trigger CI on version PR (#476)
* ci: use GitHub App token to trigger CI on version PR

Replace GITHUB_TOKEN with a GitHub App token so that the version PR
can trigger CI workflows. GITHUB_TOKEN cannot trigger workflows by
design (to prevent infinite loops).

Requires APP_ID variable and APP_PRIVATE_KEY secret to be configured.

* ci: upgrade create-github-app-token to v2
2026-01-10 17:38:57 -08:00
Tabish Bidiwale eb152eb2ca ci: auto-merge version PR to streamline releases (#474)
* Add changeset for Continue support, shell completions, and explore command

* ci: auto-merge version PR to streamline releases

Enable auto-merge on the changesets version PR so it merges
automatically once CI passes. This reduces the release process
from 2 manual PR merges to effectively 1.

Requires enabling "Allow auto-merge" in repository settings
and branch protection rules on main.
2026-01-10 16:22:25 -08:00
e987a5a327 Add Continue support (#402)
* OpenSpec 支持 Continue 插件

* add update

* Update spec.md

* fix: correct Continue frontmatter format and README ordering

- Fix Continue frontmatter to use proper YAML format with opening `---`
  and required `invokable: true` field for slash command availability
- Fix README table ordering: Continue should be after Codex alphabetically
- Remove unused TemplateManager import from continue.ts
- Fix trailing whitespace in update.test.ts
- Fix double blank line in init.test.ts
- Fix extra blank line in cli-init/spec.md
- Update tests to verify correct frontmatter format

---------

Co-authored-by: ajuanli <ajuanli@tencent.com>
Co-authored-by: Tabish Bidiwale <30385142+TabishB@users.noreply.github.com>
Co-authored-by: Tabish Bidiwale <tabishbidiwale@gmail.com>
2026-01-10 15:54:09 -08:00
Tabish Bidiwale 4971cda812 fix: use actionCommand for telemetry command tracking (#472)
The preAction hook receives (thisCommand, actionCommand) where thisCommand
is the root program and actionCommand is the actual subcommand being run.
Using thisCommand was incorrectly tracking the root command instead of
the actual subcommand executed by the user.
2026-01-10 15:10:12 -08:00
Tabish Bidiwale 4715138927 fix: set USERPROFILE for Windows compatibility in telemetry tests (#469)
On Windows, os.homedir() uses the USERPROFILE environment variable
instead of HOME. The telemetry config tests were only mocking HOME,
causing them to fail on Windows CI.
2026-01-09 23:36:54 -08:00
Tabish Bidiwale 940898c1c5 Add optional anonymous usage statistics (#468)
* chore: add proposal for PostHog analytics integration

Introduces the proposal artifact for adding opt-in telemetry to OpenSpec
using PostHog. Covers command tracking, feature adoption metrics, and
privacy-respecting consent management.

* feat: add optional anonymous usage statistics

Introduces privacy-first usage analytics to help understand how OpenSpec
is being used. Key privacy protections:

- Only tracks command names and version (no arguments, paths, or content)
- Opt-out via OPENSPEC_TELEMETRY=0 or DO_NOT_TRACK=1
- Auto-disabled in CI environments
- No IP address collection (explicitly disabled)
- Anonymous ID is a random UUID with no PII

Uses PostHog with a reverse proxy to avoid ad blockers. First-run shows
a one-line notice informing users about the collection.

* docs: make telemetry section collapsible and concise
2026-01-09 23:10:31 -08:00
Tabish Bidiwale d49a88c3bb feat: add /opsx:explore command for exploratory thinking (#467)
Wire up the explore skill and slash command templates to the
artifact-experimental-setup command. This adds /opsx:explore as
a thinking partner mode for exploring ideas, investigating problems,
and clarifying requirements before committing to a change.

Changes:
- Add imports for getExploreSkillTemplate and getOpsxExploreCommandTemplate
- Add explore skill to skills array (generates openspec-explore/SKILL.md)
- Add explore command to commands array (generates opsx/explore.md)
- Add /opsx:explore to CLI usage message
- Update docs/experimental-workflow.md with explore command
2026-01-09 20:15:26 -08:00
Tabish Bidiwale bb9f6ce0ea docs: add OPSX experimental workflow visibility to README (#460)
* docs: add OPSX experimental workflow visibility to README

Add a subtle banner near the top and an Experimental Features section
before Contributing to draw attention to the new OPSX workflow.

* docs: reframe OPSX messaging around fluid iteration

Update messaging to emphasize the core value proposition:
- No phases, just actions
- Dependencies are enablers, not gates
- Update artifacts as you learn during implementation

The previous "step-by-step artifact creation" framing incorrectly
suggested more bureaucracy. OPSX is about less rigidity, not more.

* docs: add architecture deep dive with ASCII diagrams

Add comprehensive comparison of standard vs OPSX workflow architecture:
- Philosophy: phases vs actions
- Component architecture diagrams
- Dependency graph model
- Information flow comparison
- Iteration model comparison
- Custom schema example

* docs: emphasize hackability and experimentation rationale

Add "Why We Built This" section explaining the meta-level motivation:
- Instructions were hardcoded, hard to improve
- Needed granular, testable artifacts
- Wanted to experiment with different workflows without code changes
- OPSX makes the instruction system itself hackable

Update README banner and experimental features section to highlight
schema-driven, hackable nature alongside fluid iteration benefits.

* docs: reframe hackability as user benefit, not just internal

OPSX isn't just for OpenSpec devs to experiment - it's for everyone:
- Teams can create workflows that match how they work
- Power users can tweak prompts to get better AI outputs
- Contributors can experiment without releases

Updated framing from "we needed" to "now anyone can".

* docs: add guidance on when to update vs. start fresh

Addresses a common question: when does "update as you learn" become
"this is different work"? Adds heuristics based on intent, scope
overlap, and completability to help users make the judgment call.
2026-01-09 20:09:53 -08:00
Tabish Bidiwale ae85a7229d fix: offer parent flags in Bash and PowerShell completions when subcommands exist (#466)
When a command has both flags and subcommands, the Bash and PowerShell
completion generators now check if the user is typing a flag (input
starts with `-`) before offering subcommand completions. This fixes the
issue where parent-level flags were never suggested.

Before: `openspec config --<TAB>` → Only showed subcommands
After: `openspec config --<TAB>` → Shows parent flags when input starts with `-`

Fixes #463
2026-01-09 19:40:13 -08:00
Tabish Bidiwale 504c93bdf1 fix: skip additional Windows-specific tests (#465)
* fix: skip additional Windows-specific tests

- fish-installer: skip uninstall permission test (chmod on directory)
- powershell-installer: skip "skip configuration when script line exists"
  test (Windows has dual profile paths so the second profile gets configured)

* refactor: use ENOTDIR approach for cross-platform install error tests

Instead of platform-specific invalid paths (Z:\ or /root), create a
temporary file and use it as homeDir. This guarantees deterministic
ENOTDIR failures when trying to create subdirectories on all platforms.
2026-01-09 16:26:26 -08:00
Tabish Bidiwale c4a54a8d54 fix: skip Windows-specific permission tests that rely on chmod() (#464)
fs.chmod() on directories doesn't restrict write access on Windows since
Windows uses ACLs that Node.js doesn't control. Additionally, admin users
and CI runners can bypass read-only attributes. Skip these tests on Windows
and use platform-specific invalid paths in cross-platform tests.

Fixes #401 (bash/pwsh completion commit breaking Windows e2e tests).
2026-01-09 16:06:15 -08:00
38d2356836 feature/bash_fish_power_shells_completions (#401)
* added CLI completions support for: bash, fish and powershell

* Add bash/fish/powershell completions

* Archive extend-shell-completions

* Archive extend-shell-completions

* Fix canWriteFile control flow and add tests

* Fix bash completion fallback and security escaping

  - Add _init_completion fallback for systems without bash-completion
  - Fix command injection escaping in Fish/PowerShell generators
  - Add Bash command name escaping for security
  - Add comprehensive security tests for all generators
  - Fix test placement issues in bash/powershell test files

* refactor: extract completion templates and standardize naming

Extract static template literals from generators into separate template files.
Standardize naming to {SHELL}_STATIC_HELPERS and {SHELL}_DYNAMIC_HELPERS.

- Create bash/fish/powershell/zsh template files
- Rename constants: BASH_HELPERS → BASH_DYNAMIC_HELPERS,
  FISH_HELPER_FUNCTIONS → FISH_STATIC_HELPERS,
  POWERSHELL_HELPERS → POWERSHELL_DYNAMIC_HELPERS
- Update generator imports
- Remove ~99 lines of boilerplate from generators

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

* docs: update spec to reflect multi-shell support

Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

* fix: add all shells to zsh completion suggestions

* feat: add --yes flag to completion uninstall

* fix: remove bash-completion dependency from fallback

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

* fix: use printf instead of echo for Fish tab output

Fish's echo doesn't interpret escape sequences, so \t outputs
literally instead of as a tab character. Use printf for proper
tab-separated completion output.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

* fix: make UX messages shell-aware

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

* fix: add Homebrew paths for bash-completion detection

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

* fix: support both PowerShell Core and Windows PS 5.1

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

* fix: preserve colon handling in bash completion

Add -n : option to _init_completion to prevent colons from being
treated as word separators. This is important for spec/change IDs
that may contain colons.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

* fix: update completion tests to match implementation changes

Updated bash-generator test to expect `-n :` flag in _init_completion call.
Updated powershell-installer tests to match refactored implementation that
supports both PowerShell Core and Windows PowerShell 5.1.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
Co-authored-by: Tabish Bidiwale <30385142+TabishB@users.noreply.github.com>
2026-01-09 15:49:50 -08:00
Neroyangandneroyang 3f67debf65 feat: change the frontmatter of the Codebuddy Slash Commands (#462)
* feat: change the frontmatter of the Codebuddy Slash Commands

* fix: fix the issue mentioned by coderabbitai

* feat: change the init.test

* feat: change the init.test

---------

Co-authored-by: neroyang <neroyang@tencent.com>
2026-01-09 10:08:59 -08:00
github-actions[bot]andTabish Bidiwale 533cb0fa87 chore(release): version packages (#458)
* Version Packages

* chore: trigger CI

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Tabish Bidiwale <tabishbidiwale@gmail.com>
2026-01-07 00:38:00 -08:00
Tabish Bidiwale 8dfd824477 Add changeset for OPSX experimental workflow commands (#457) 2026-01-07 00:35:04 -08:00
Tabish Bidiwale 3ed1270316 docs: add experimental workflow (OPSX) user guide (#456)
Adds documentation for the experimental artifact-based workflow:
- Setup instructions (Claude Code only for now)
- Command reference for all /opsx:* commands
- Usage examples and tips
- Comparison with standard workflow
- Feedback links to Discord and GitHub
2026-01-07 00:24:14 -08:00
Tabish Bidiwale eb15cdb983 chore: archive completed changes and clean up stale ones (#455)
Archive 5 completed changes:
- opsx-archive-command (synced specs)
- add-specs-apply-command
- add-per-change-schema-metadata
- make-apply-instructions-schema-aware (synced specs)
- add-agent-schema-selection

Delete 5 stale/abandoned changes:
- add-fingerprinting (no tasks, 7 weeks old)
- add-scaffold-command (0/7 tasks, 7 weeks old)
- add-proposal-frontmatter (no tasks, 8 weeks old)
- add-interactive-proposal-command (no tasks, 8 weeks old)
- make-validation-scope-aware (0/8 tasks, 4 months old)

Sync delta specs to main:
- Add opsx-archive-skill spec (new capability)
- Update cli-artifact-workflow spec with Schema Apply Block and
  Apply Instructions Command requirements
2026-01-06 23:52:11 -08:00
Tabish Bidiwale cd172a4427 feat: add smart sync check to /opsx:archive command (#452)
Instead of blindly asking "want to sync?", archive now performs a quick
check to see if delta specs actually need syncing:

- Extracts requirement names from delta specs
- Checks if corresponding main spec exists
- Checks if ADDED requirements appear in main spec
- Only prompts if sync appears needed

Also improves archive output to always show specs status:
- ✓ Synced to main specs
- No delta specs
- ⚠️ Not synced
2026-01-06 17:41:45 -08:00
Tabish Bidiwale b7f5a429de feat: add /opsx:archive command for archiving completed changes (#451)
Add `/opsx:archive` slash command to complete the OPSX workflow lifecycle.
This command archives completed changes in the experimental workflow with:

- Change selection prompt (if not specified)
- Artifact completion check using `openspec status --json`
- Task completion check (parsing tasks.md for `- [ ]`)
- Spec sync prompt (offers `/opsx:sync` before archiving if specs exist)
- Archive to `openspec/changes/archive/YYYY-MM-DD-<name>/`
- Clear output formatting for success, warnings, and errors

This completes the OPSX command suite:
- /opsx:new - Start a change
- /opsx:continue - Create next artifact
- /opsx:ff - Fast-forward all artifacts
- /opsx:apply - Implement tasks
- /opsx:sync - Sync delta specs
- /opsx:archive - Archive completed change (NEW)
2026-01-06 17:21:37 -08:00
Tabish Bidiwale a5c10ed5e7 feat: add /opsx:sync command for syncing delta specs to main specs (#450)
* feat: add /opsx:sync command for syncing delta specs to main specs

Add a new agent-driven skill that syncs delta specs from a change to main specs
without requiring archiving. This enables:

- Updating main specs during active development
- Intelligent merging (partial updates, adding scenarios)
- Idempotent operation (safe to run multiple times)

Implementation:
- Extract shared specs-apply logic from archive.ts to specs-apply.ts
- Add getSyncSpecsSkillTemplate and getOpsxSyncCommandTemplate
- Register /opsx:sync in artifact-experimental-setup
- Add specs-sync-skill main spec

* fix: rename specs apply to specs sync throughout change artifacts

Update all references:
- /opsx:specs → /opsx:sync
- specs-apply-skill → specs-sync-skill
- Remove cli-artifact-workflow delta spec (no CLI command)
- Update proposal, design, and tasks docs
2026-01-06 16:32:22 -08:00
Tabish Bidiwale 1bc849554c feat: add /opsx:ff command for fast-forward artifact creation (#448)
Adds a new fast-forward command that creates all artifacts needed for
implementation in one go, instead of stepping through them individually.

Changes:
- Add `applyRequires` field to status JSON output (shows which artifacts
  are required before the apply phase can begin)
- Add skill template for openspec-ff-change
- Add command template for /opsx:ff slash command
- Register templates in artifact-workflow setup command

The fast-forward command uses the schema's `apply.requires` configuration
to determine when to stop creating artifacts, making it schema-agnostic.
2026-01-06 11:49:03 -08:00
Tabish Bidiwale d73705736f feat: make apply instructions schema-aware (#444)
* feat: make apply instructions schema-aware

The `generateApplyInstructions` function was hardcoded to check for
`spec-driven` artifacts. This change makes it read artifact definitions
from the schema's new `apply` block, enabling support for different
workflows like TDD.

Changes:
- Add `ApplyPhaseSchema` Zod schema with `requires`, `tracks`, and
  `instruction` fields to types.ts
- Update `SchemaYamlSchema` to include optional `apply` field
- Add `apply` block to spec-driven and tdd schemas
- Refactor `generateApplyInstructions` to:
  - Load schema via `resolveSchema()`
  - Read `apply.requires` for required artifacts
  - Check artifact existence dynamically (supports glob patterns)
  - Use `apply.tracks` for progress tracking (or skip if null)
  - Use `apply.instruction` for custom guidance
  - Build `contextFiles` from all existing artifacts in schema
- Handle fallback when schema has no `apply` block (require all artifacts)
- Add 8 new tests for schema-aware apply behavior

* fix: improve apply instructions robustness and consistency

- Fix artifactOutputExists to properly handle glob patterns cross-platform
  by using path.sep and verifying actual file matches
- Remove redundant if/else in contextFiles loop
- Distinguish between missing tracks file vs empty tracks file
- Use consistent { error: } key in ApplyPhaseSchema validation
- Rename fallback test to accurately describe what it tests

* test: add fallback behavior tests for schemas without apply block

Add two tests that verify the fallback logic when a schema lacks an
apply block:
- Blocks when not all artifacts exist (requires ALL artifacts)
- Ready state with default instruction when all artifacts exist

Uses XDG_DATA_HOME to create temporary user schemas for testing.
2026-01-05 22:45:27 -08:00
Tabish Bidiwale ed924ffcff feat: add agent schema selection to experimental artifact workflow (#445)
- Add `openspec schemas` CLI command with `--json` option for agents
- Add `listSchemasWithInfo()` helper to get schema metadata (name, description, artifacts)
- Update `openspec-new-change` skill to prompt for schema selection
- Update `openspec-continue-change` skill to read schema dynamically from status
- Update `openspec-apply-change` skill to use schema-specific context files
- Update all slash commands (/opsx:new, /opsx:continue, /opsx:apply) to be schema-agnostic
- Add documentation for when to use each schema (spec-driven vs tdd)

Agents can now create changes with different workflow schemas and the skills
dynamically adapt based on the selected schema's artifact sequence.
2026-01-05 22:32:22 -08:00
Tabish Bidiwale 1786684af6 feat: add per-change schema metadata (.openspec.yaml) (#443)
This feature enables workflow schema auto-detection for changes:

- Add ChangeMetadataSchema Zod schema to types.ts
- Create change-metadata.ts with writeChangeMetadata(), readChangeMetadata()
- Update createChange() to accept optional schema param and write metadata
- Modify loadChangeContext() to auto-detect schema from .openspec.yaml
- Add --schema option to openspec new change command
- Update status/instructions commands to auto-detect schema from metadata

Schema resolution order:
1. Explicit --schema flag (if provided)
2. Schema from .openspec.yaml in change directory
3. Default 'spec-driven'
2026-01-05 17:13:52 -08:00
Tabish Bidiwale 51fb10db5e feat: add slash commands to artifact-experimental-setup (#442)
Extend the `openspec artifact-experimental-setup` command to also generate
slash commands alongside Agent Skills.

Changes:
- Add CommandTemplate interface and template functions for /opsx:new,
  /opsx:continue, and /opsx:apply commands
- Modify artifactExperimentalSetupCommand() to create slash command files
  at .claude/commands/opsx/
- Update success message to show both skills and slash commands created

The setup command now creates:
- 3 Agent Skills (.claude/skills/)
- 3 Slash Commands (.claude/commands/opsx/)
2026-01-05 17:08:46 -08:00
Tabish Bidiwale cac54042ce feat: add Agent Skills for experimental artifact workflow (#424)
* feat: add Agent Skills for experimental artifact workflow

Implements Task #3 from the experimental release plan:
- Create skill templates for openspec-new-change and openspec-continue-change
- Add artifact-experimental-setup CLI command
- Generate Agent Skills in .claude/skills/ directory
- Support cross-editor compatibility (Claude Code, Cursor, Windsurf)

Skills follow the Agent Skills specification and provide:
- Natural language invocation by AI assistants
- Step-by-step artifact creation workflow
- Dependency-driven change management

* fix: update GitHub issues URL to correct repository

Replace placeholder `https://github.com/your-org/openspec/issues`
with correct URL `https://github.com/Fission-AI/OpenSpec/issues` in:
- docs/experimental-release-plan.md
- src/commands/artifact-workflow.ts

Verified against package.json repository.url field.

* feat: add openspec-apply-change skill for task implementation

Add the apply skill to guide agents through implementing tasks from an
OpenSpec change:

- Add `openspec instructions apply` CLI command that parses tasks.md and
  returns context files, progress tracking, and dynamic instructions
- Add `getApplyChangeSkillTemplate()` to skill-templates.ts with full
  workflow guidance for implementing tasks
- Update artifact-experimental-setup to generate all three skills:
  openspec-new-change, openspec-continue-change, openspec-apply-change

The apply skill supports the fluid "actions on a change" model:
- Can be invoked anytime (if tasks.md exists)
- Handles blocked/ready/all_done states
- Guides agents to pause on issues and suggest artifact updates
- Tracks progress via task checkboxes

* docs: mark apply skill implementation steps as complete

* feat: add Capabilities section to proposal template

Enrich the proposal template to explicitly capture capability discovery:

- Add "Capabilities" section with "New Capabilities" and "Modified
  Capabilities" subsections to proposal template
- Update proposal instruction in schema.yaml to guide agents on
  researching existing specs and listing capabilities
- Update skill instructions with detailed guidance for the Capabilities
  section

This creates a clear contract between proposal and specs phases - each
capability listed in the proposal will need a corresponding spec file.

* feat: remove redundant `openspec next` command

The `next` command was redundant with `status` - both show which artifacts
are ready to create. The status command provides more context (done/ready/blocked)
and is the single source of truth for artifact state.

Changes:
- Remove nextCommand function and CLI registration from artifact-workflow.ts
- Update skill templates to use status instead of next
- Update docs and specs to reflect removal
- Add REMOVED Requirements section to cli-artifact-workflow spec
- Remove 7 tests for the next command

Migration: Use `openspec status --change <id> --json` and filter artifacts
with `status: "ready"` to find artifacts that can be created next.

* docs: clarify kebab-case naming in proposal template

Update HTML comments in the Capabilities section to explicitly instruct
using kebab-case identifiers with examples (user-auth, data-export,
api-rate-limiting).

* feat: add change proposals for per-change schema metadata

Add two related change proposals for enabling schema selection in the
experimental artifact workflow:

1. add-per-change-schema-metadata: Store schema choice in .openspec.yaml
   per change, enabling auto-detection in workflow commands. Includes
   Zod schema design and delta specs for cli-artifact-workflow.

2. add-agent-schema-selection: Follow-up to update agent skills to
   support dynamic schema selection (depends on metadata change).

Also includes:
- Example .openspec.yaml in add-frontmatter-to-openspec-artifact-files
- Clarify "Modified Capabilities" guidance in proposal template

* remove test change

* feat: fix design.md as optional, update docs, add schema-aware apply proposal

Changes:
- Fix generateApplyInstructions to treat design.md as optional (not required)
- Update experimental-release-plan.md CLI output to match implementation
- Add openspec-apply-change skill to docs (was missing)
- Fix test flow numbering after adding apply skill verification step
- Add change proposal for making apply instructions schema-aware

The schema-aware proposal introduces an `implementation` block in schema.yaml
to define when a change becomes implementable and how to track progress.
2026-01-05 16:42:17 -08:00
Tabish Bidiwale c47cdaafe2 fix: archive add-antigravity-support and fix-cline-workflows-implementation (#423)
Archives two changes that were missing complete requirement content in their
spec deltas, which would have caused data loss during archival:

1. add-antigravity-support: Adds Antigravity IDE support to cli-init and
   cli-update with proper workflow file generation in `.agent/workflows/`

2. fix-cline-workflows-implementation: Corrects Cline paths from `.clinerules/`
   to `.clinerules/workflows/` to match Cline's official workflow conventions

Both deltas were fixed to include all 15 slash command scenarios (14 existing +
1 new Antigravity) to prevent loss of existing requirements when archived.
2025-12-31 00:12:07 +11:00
Tabish Bidiwale ea5aa0e562 feat: enhance artifact instructions with inline guidance and XML output (#422)
* feat: enhance artifact instructions with inline guidance and XML output

Schema changes:
- Add `instruction` field to artifacts for inline creation guidance
- Include detailed instructions for proposal, specs, design, and tasks

Instruction loader enhancements:
- Return `instruction` field from schema
- Include `changeDir` for full path resolution
- Enrich dependency info with `path` and `description` fields

CLI output improvements:
- New XML-style format for `openspec instructions` (better for AI parsing)
- Structured tags: <artifact>, <task>, <context>, <output>, <template>
- Dependencies now show full paths for easy file reading
- JSON output includes all new fields

* docs: add schema customization and workflow gap documentation

- schema-customization.md: Guide for customizing artifact schemas
- schema-workflow-gaps.md: Analysis of current workflow limitations

* test: fix list test for new default sort order

Update test to explicitly use sort='name' since the default
changed from alphabetical to most-recently-modified.
2025-12-30 22:28:49 +11:00
Tabish Bidiwale 48b5ed9657 feat: enhance list command with last modified timestamps and sorting (#421)
- Add lastModified field showing when each change was last modified
- Default sort order is now "recent" (most recently modified first)
- Add --sort option to choose between "recent" and "name" ordering
- Add --json option for programmatic access with structured output
- Fall back to directory mtime for empty change directories
- Display relative time (e.g., "2h ago", "3d ago") in human output
2025-12-30 22:06:17 +11:00
Tabish Bidiwale fb7ff527a6 proposal: add artifact workflow CLI commands (Slice 4) (#415)
* proposal: add artifact workflow CLI commands (Slice 4)

Add CLI commands for artifact workflow operations:
- `openspec status --change <id>` - Show artifact completion state
- `openspec next --change <id>` - Show ready artifacts
- `openspec instructions <artifact> --change <id>` - Get enriched template
- `openspec templates --change <id>` - Show template paths
- `openspec new change <name>` - Create new change

Commands are top-level for fluid UX and implemented in isolation
for easy removal (experimental feature).

* fix: remove --change from templates command

Templates are schema-level, not change-level. The command now uses
--schema instead of --change for consistency with how templates
are actually resolved.

* rename: cli-workflow -> cli-artifact-workflow

More specific capability name that clarifies which workflow the CLI
commands are for.

* feat: implement artifact workflow CLI commands (Slice 4)

Add experimental CLI commands for artifact-based workflow management:
- `openspec status --change <id>` - display artifact completion status
- `openspec next --change <id>` - show artifacts ready to create
- `openspec instructions <artifact> --change <id>` - output enriched template
- `openspec templates [--schema <name>]` - show resolved template paths
- `openspec new change <name>` - create new change directory

Features:
- JSON output support (--json flag) for all commands
- Color-coded status indicators (green/yellow/red)
- Progress spinners during loading
- --no-color and NO_COLOR env support
- --schema option for custom schema selection
- Comprehensive error handling with helpful messages

All commands are isolated in src/commands/artifact-workflow.ts for easy
removal if the feature doesn't work out. Help text marks them as experimental.

* fix: update specs glob to match nested directory structure

The schema used specs/*.md but specs are stored as specs/<capability>/spec.md.
Updated to specs/**/*.md so openspec status/next correctly detect spec completion.

* test: update test to match new specs glob pattern

* chore: archive add-artifact-workflow-cli change

- Move change to archive/2025-12-28-add-artifact-workflow-cli
- Create cli-artifact-workflow spec

* feat: unify change state model for scaffolded changes

- Update artifact workflow commands to work with scaffolded changes
- Add draft changes section to dashboard view
- Fix completed changes to require tasks.total > 0
- Archive unify-change-state-model change

* fix: validate change name format to prevent path traversal

Add validation in validateChangeExists() to ensure --change parameter
is a valid kebab-case ID before constructing file paths. This prevents
path traversal attacks like --change "../foo" or --change "/etc/passwd".

- Reuses existing validateChangeName() from change-utils.ts
- Adds 3 tests for path traversal, absolute paths, and slashes
2025-12-29 16:55:56 +11:00
Tabish Bidiwale 11e195575f feat: add instruction loader for template loading and change context (#414)
* feat: add instruction loader for template loading and change context

Add the instruction-loader module that provides:
- loadTemplate: Load templates from schema directories
- loadChangeContext: Combine artifact graph with completion state
- generateInstructions: Enrich templates with change-specific context
- formatChangeStatus: Format change status as readable output

This is Slice 3 of the artifact-graph system, building on the graph
operations (Slice 1) and change creation utilities (Slice 2).

* chore: archive add-instruction-loader change

- Move change to archive as 2025-12-28-add-instruction-loader
- Create instruction-loader spec with 4 requirements

* docs: add purpose description to instruction-loader spec
2025-12-28 17:44:05 +11:00
Tabish Bidiwale ab47cc6b00 feat: restructure schemas as directories with templates (#411)
* feat: restructure schemas as directories with templates

Move built-in schemas from embedded TypeScript objects to a file-based
directory structure. This enables co-located templates alongside schemas.

Changes:
- Remove builtin-schemas.ts (replaced by file-based schemas)
- Add schemas/ directory at package root with spec-driven and tdd schemas
- Update resolveSchema() to load from directory structure
- Resolution checks user dir → package dir

* chore: archive restructure-schema-directories change

* docs: update artifact_poc.md for directory-based schema structure

Update documentation to reflect the new schema structure where schemas
are directories containing schema.yaml and co-located templates/ rather
than single .yaml files with separate template directories.
2025-12-28 17:00:15 +11:00
Tabish Bidiwale 8dcd1707ee proposal: add instruction loader and schema restructure (Slice 3) (#410)
* proposal: add instruction loader and schema restructure (Slice 3)

Adds two change proposals for implementing Slice 3 of the artifact POC:

1. restructure-schema-directories
   - Move schemas from embedded TS objects to self-contained directories
   - Each schema becomes a directory with schema.yaml + templates/
   - Enables co-located templates for user extensibility
   - 2-level resolution: user override → package built-in

2. add-instruction-loader (depends on #1)
   - Load templates from schema directories
   - Enrich templates with change context (dependencies, next steps)
   - Format change status for CLI output
   - New instruction-loader capability

These proposals complete Slice 3 from docs/artifact_poc.md.

* fix: include full requirement block in MODIFIED spec

Update the Schema Loading requirement to include all original scenarios
(modified as needed) per the MODIFIED requirement guidelines. The archiver
replaces the entire requirement with the provided content.
2025-12-26 23:45:36 +11:00
Tabish Bidiwale 4f4af5708d feat: add change creation utilities (#408)
* proposal: add change manager - extract + new functionality

Slice 2 of the artifact tracker POC. Creates ChangeManager module that:

**Extracts existing functionality:**
- `listChanges()` from ListCommand + ChangeCommand.getActiveChanges()
- `changeExists()` from inline fs.access() checks
- `getChangePath()` from inline path.join() calls
- `isInitialized()` from ListCommand directory check

**Adds new functionality:**
- `createChange(name, description?)` - create change directory + README
- `validateName(name)` - enforce kebab-case naming

**Refactors CLI commands to be thin wrappers:**
- ListCommand delegates to ChangeManager
- ChangeCommand delegates to ChangeManager

Also updates docs/artifact_poc.md to reflect XDG decisions from Slice 1.

* proposal: simplify to utility functions only

Remove extraction/refactor scope. Just add:
- createChange(projectRoot, name, description?)
- validateChangeName(name)

Simple utility functions in src/utils/change-utils.ts.
No class, no abstraction layer.

* docs: update artifact_poc.md for simplified Slice 2

- Rename ChangeManager to change-utils (simple utility functions)
- Remove extracted methods (listChanges, getChangePath, etc.)
- Keep only new functionality: createChange(), validateChangeName()
- Update component diagram and summary table

* docs: clarify existing vs new functionality in artifact_poc.md

Audit artifact_poc.md against existing codebase to mark what already
exists vs what's genuinely new:

- Slice 4 CLI table: Added Status column (NEW/EXISTS)
- Added "Existing CLI commands" section listing what's not in scope
- Updated Implementation Order Slice 4 with explicit new vs existing
- Summary table: Updated status + added "What already exists" section

Key finding: The document was already well-simplified. Only truly new
functionality (createChange, validateChangeName, InstructionLoader,
artifact graph CLI commands) is proposed.

* rename: change-manager -> change-creation capability

The capability name "change-manager" implied a manager class abstraction,
but the simplified proposal uses only utility functions. Renamed to
"change-creation" to accurately reflect what the capability provides.

* feat: implement change creation utilities

Add createChange() and validateChangeName() functions for programmatic
change directory creation with kebab-case validation.

- createChange(projectRoot, name) creates openspec/changes/<name>/
- validateChangeName() enforces kebab-case naming conventions
- Comprehensive test coverage (21 tests)

* chore: archive add-change-manager change

Move change to archive and create change-creation spec with
requirements for createChange() and validateChangeName().

* docs: update change-creation spec purpose
2025-12-26 22:14:56 +11:00
Tabish Bidiwale 9822576770 fix(artifact-graph): normalize paths for cross-platform glob compatibility (#407)
Add FileSystemUtils.toPosixPath() utility and consolidate scattered
path normalization patterns. This fixes Windows test failures where
fast-glob couldn't match paths containing backslashes.

Root cause: path.join() uses backslashes on Windows, but fast-glob
requires forward slashes for glob patterns on all platforms.

Changes:
- Add toPosixPath() to FileSystemUtils for cross-platform path handling
- Update artifact-graph/state.ts to normalize glob patterns
- Consolidate path normalization in update.ts, validator.ts, and
  json-converter.ts to use the new utility
2025-12-25 22:42:00 +11:00
Tabish Bidiwale af273b8e0b proposal: add artifact graph core query system (#400)
* proposal: add artifact graph core query system

Add OpenSpec change proposal for Slice 1 of the artifact POC - the core
"What's Ready?" query system. This implements:

- ArtifactGraph class for DAG-based dependency modeling
- Filesystem-based state detection (file existence = completion)
- Topological sort for build order calculation
- Ready/blocked artifact queries

This is a parallel module that will coexist with the current system.

* docs: specify Zod for schema validation in artifact graph proposal

- Add decision section for Zod schema validation in design.md
- Update data structures to show Zod schemas with z.infer<> types
- Update tasks to specify Zod usage for type definitions and parsing

* docs: add 2-level schema resolution and built-in schemas

- Add decision for global → built-in schema resolution pattern
- Add resolver.ts for schema lookup logic
- Add built-in schemas directory (spec-driven.yaml, tdd.yaml)
- Add schema resolution tests
- Follows ESLint/Prettier/Git patterns (defaults baked in package)

* experiment: add vertical slice version of artifact graph change

Creates add-artifact-graph-core-v2 with requirements organized as
vertical slices - each requirement file contains its spec, design
decisions, and tasks bundled together for comparison.

* feat(core): add getGlobalDataDir for XDG-compliant data directory

Add getGlobalDataDir() function following XDG Base Directory Specification
for storing user data like schema overrides:
- XDG_DATA_HOME takes precedence on all platforms
- Unix/macOS fallback: ~/.local/share/openspec/
- Windows fallback: %LOCALAPPDATA%/openspec/

* feat(artifact-graph): add core dependency graph module

Implement Slice 1 ("What's Ready?") of the artifact graph system:

- types.ts: Zod schemas for artifact definitions with derived TypeScript types
- schema.ts: YAML parsing with validation for duplicates, invalid refs, cycles
- graph.ts: ArtifactGraph class with Kahn's algorithm for topological sort
- state.ts: Filesystem-based completion detection with glob pattern support
- resolver.ts: Two-level schema resolution (global override → built-in)
- builtin-schemas.ts: spec-driven and tdd workflow definitions

Key design decisions:
- Filesystem as database (stateless, git-friendly)
- Cycle errors show full path (e.g., "A → B → C → A")
- Deterministic ordering via sorted queues

* test(artifact-graph): add comprehensive test suite

52 tests covering all artifact-graph functionality:

- schema.test.ts: Parsing, validation errors, cycle detection
- graph.test.ts: Build order, ready artifacts, blocked queries
- state.test.ts: File existence, glob patterns, missing directories
- resolver.test.ts: Schema resolution with global overrides

* docs(openspec): archive add-artifact-graph-core change

Archive completed change proposal and create artifact-graph spec with
6 requirements covering schema loading, build order, state detection,
ready queries, completion checks, and blocked queries.

* chore: remove experimental artifact-graph-core-v2 folder

Clean up experimental vertical slice proposal that is no longer needed.

* feat(artifact-graph): validate global schema overrides

Global schema overrides are now validated through the same pipeline as
built-in schemas, catching invalid schemas, cyclic dependencies, and
invalid requires references at load time. Added SchemaLoadError for
better error context with file paths.

* test(artifact-graph): add workflow integration tests

Add end-to-end integration tests that exercise the full artifact-graph
pipeline: resolveSchema → ArtifactGraph → detectCompleted → queries.

Tests cover:
- Complete spec-driven and tdd workflow progressions
- Out-of-order file creation handling
- Glob pattern matching with multiple files
- Build order consistency
- Edge cases (empty/missing directories, non-matching files)

* refactor(artifact-graph): adopt zod v4 error message format

Update custom error messages from string format to zod v4 object format
using `{ error: 'message' }` convention.

* fix(test): prevent hanging vitest threads after test runs

- Add teardownTimeout (3s) to vitest config for forced cleanup
- Add global teardown function to vitest.setup.ts
- Call child.unref() to prevent child processes from blocking event loop
- Explicitly destroy stdio streams on process close/error
2025-12-25 22:09:42 +11:00
Eunsong-Park 3ceef2db72 fix(archive): allow REMOVED requirements when creating new spec files (#403) (#404)
When creating a new spec file, REMOVED requirements are now ignored
with a warning instead of causing archive to fail. This enables
refactoring scenarios where old fields are removed while documenting
a capability for the first time.

Fixes #403
2025-12-25 03:44:34 +11:00
Tabish Bidiwale 2c2599b1f0 docs: add artifact POC analysis document (#398)
Add internal documentation for the artifact-based approach to OpenSpec
core. This document outlines design decisions, terminology, and the
philosophy behind treating dependencies as enablers rather than gates.
2025-12-23 22:21:11 +11:00
github-actions[bot]andTabish Bidiwale c08a53cb21 chore(release): version packages (#397)
* Version Packages

* chore: trigger CI

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Tabish Bidiwale <tabishbidiwale@gmail.com>
2025-12-23 12:59:10 +11:00
Tabish Bidiwale 455c65f3c4 Add changeset for --no-interactive flag fix (#396) 2025-12-23 12:45:56 +11:00
Tabish Bidiwale 9ac6330430 fix(cli): respect --no-interactive flag in validate command (#395)
* fix(cli): respect --no-interactive flag in validate command

The validate command's spinner was starting regardless of the
--no-interactive flag, causing hangs in pre-commit hooks.

Changes:
- Pass noInteractive option to runBulkValidation
- Handle Commander.js --no-* flag syntax (sets interactive=false)
- Only start ora spinner when in interactive mode
- Add CI environment variable check to isInteractive() for industry
  standard compliance

* test: add unit tests for interactive utilities and CLI flag

- Export resolveNoInteractive() helper for reuse
- Add InteractiveOptions type export for testing
- Refactor validate.ts to use resolveNoInteractive()
- Add 17 unit tests for isInteractive() and resolveNoInteractive()
- Add CLI integration test for --no-interactive flag

This prevents future regressions where Commander.js --no-* flag
parsing is not properly handled.
2025-12-23 12:42:25 +11:00
github-actions[bot]andTabish Bidiwale fb264bcbcd chore(release): version packages (#394)
* Version Packages

* chore: trigger CI

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Tabish Bidiwale <tabishbidiwale@gmail.com>
2025-12-23 10:00:01 +11:00
Tabish Bidiwale a2757e7856 Add changeset for config command dynamic import fix (#393) 2025-12-23 09:56:51 +11:00
Tabish Bidiwale 6d84924c18 fix(cli): use dynamic import for @inquirer/prompts in config command (#392)
* fix(cli): use dynamic import for @inquirer/prompts in config command

The config command (added in #382) reintroduced the pre-commit hook hang
issue that was fixed in #380. The static import of @inquirer/prompts at
module load time causes stdin event listeners to be registered even when
running non-interactive commands, preventing clean process exit when
stdin is piped (as pre-commit does).

Convert the static import to a dynamic import that only loads inquirer
when the `config reset` command is actually used interactively.

Fixes #367

* chore: add ESLint with no-restricted-imports rule for @inquirer

Add ESLint configuration that prevents static imports of @inquirer/*
modules. This prevents future regressions of the pre-commit hook hang
issue fixed in this PR.

The rule shows a helpful error message pointing to issue #367 for context.
init.ts is exempted since it's already dynamically imported from the CLI.

* ci: add ESLint step to lint job

Run `pnpm lint` in CI to enforce the no-restricted-imports rule
that prevents static @inquirer imports.
2025-12-23 09:50:54 +11:00
Tabish Bidiwale 6de04f3b2b feat(ci): migrate to npm OIDC trusted publishing (#390)
Replace classic npm token authentication with OIDC trusted publishing:

- Add `id-token: write` permission for OIDC token generation
- Upgrade to Node 24 (includes npm 11.5.1+ required for OIDC)
- Remove NPM_TOKEN/NODE_AUTH_TOKEN env vars (OIDC replaces them)

This eliminates the need for rotating npm access tokens and provides
cryptographically verified publisher identity with automatic provenance
attestation.

Requires configuring trusted publisher on npmjs.com:
- Organization: Fission-AI
- Repository: OpenSpec
- Workflow: release-prepare.yml
2025-12-22 20:10:27 +11:00
github-actions[bot]andTabish Bidiwale c2a1a4c807 chore(release): version packages (#389)
* Version Packages

* chore: trigger CI

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Tabish Bidiwale <tabishbidiwale@gmail.com>
2025-12-22 18:44:57 +11:00
Tabish Bidiwale 2e71835d23 Add changeset for config command and shell completions (#388) 2025-12-22 18:35:29 +11:00
Tabish Bidiwale 971f8ca4a3 feat(cli): add openspec config command for global configuration management (#382)
* feat(cli): add openspec config command for global configuration management

Implements the `openspec config` command with subcommands:
- `path`: Show config file location
- `list [--json]`: Show all current settings
- `get <key>`: Get a specific value (raw output for scripting)
- `set <key> <value> [--string]`: Set a value with auto type coercion
- `unset <key>`: Remove a key (revert to default)
- `reset --all [-y]`: Reset configuration to defaults
- `edit`: Open config in $EDITOR/$VISUAL

Key features:
- Dot notation for nested key access (e.g., featureFlags.someFlag)
- Auto type coercion (true/false → boolean, numbers → number)
- --string flag to force string storage
- Zod schema validation with unknown field passthrough
- Reserved --scope flag for future project-local config
- Windows-compatible editor spawning with proper path quoting
- Shell completion registry integration

* test(config): add additional unit tests for validation and coercion

- Add tests for unknown fields with various types
- Add test to verify error message path for featureFlags
- Add test for number values rejection in featureFlags
- Add config set simulation tests to verify full coerce → set → validate flow

* fix(config): avoid shell parsing in config edit to handle paths with spaces

Use spawn with shell: false and pass configPath as an argument instead
of building a shell command string. This correctly handles spaces in
both the EDITOR path and config file path on all platforms.

* chore(openspec): archive add-config-command and create cli-config spec

Move completed change to archive and apply spec deltas to create
the cli-config specification documenting the config command interface.

* Validate config keys on set
2025-12-22 18:29:25 +11:00
Tabish Bidiwale 68e0a7e68e fix(cli): prevent hang in pre-commit hooks by using dynamic imports (#380)
Fixes #367

The CLI was hanging when run as a pre-commit hook because @inquirer/prompts
was statically imported at module load time. Even when prompts were never
called (e.g., `openspec validate --specs --no-interactive`), the import
itself could set up stdin references that prevented clean process exit
when stdin was piped.

Changes:
- Convert all static `@inquirer/prompts` imports to dynamic imports
- Dynamically import `InitCommand` (which uses `@inquirer/core`)
- Update `isInteractive()` to accept options object with both
  `noInteractive` and Commander's negated `interactive` property
- Handle empty validation queue with proper exit code

Now when running in non-interactive mode, the inquirer modules are never
loaded, allowing the process to exit cleanly after completion.
2025-12-21 18:10:53 +11:00
Tabish Bidiwale f39cc5c1fb fix(global-config): respect XDG_CONFIG_HOME on all platforms (#378)
Prioritize XDG_CONFIG_HOME on Windows to fix test environment overrides.
Previously, Windows would always use APPDATA regardless of XDG_CONFIG_HOME,
causing tests to fail. Now XDG_CONFIG_HOME is checked first on all platforms
before falling back to platform-specific defaults.

Also update the Windows APPDATA test to explicitly clear XDG_CONFIG_HOME
when testing the fallback behavior.
2025-12-20 23:01:04 +11:00
Tabish Bidiwale 5129a8cf96 feat(core): implement global config directory with XDG support (#377)
* feat(core): implement global config directory with XDG support

Add new global-config module following XDG Base Directory Specification with platform-specific fallbacks (Unix: ~/.config/openspec, Windows: %APPDATA%/openspec). Includes config loading with defaults, config saving with directory creation, and full test coverage. Archive add-global-config-dir change.

* docs(spec): add Purpose section to global-config spec

Replace placeholder text with a concise description of what the spec governs, its scope, and high-level objectives.
2025-12-20 20:16:53 +11:00
Tabish Bidiwale 4ff893048d feat(spec): add XDG global config directory and config command proposals (#376)
Create two OpenSpec change proposals:
1. add-global-config-dir: Foundation for user-level configuration following XDG Base Directory Specification with cross-platform support
2. add-config-command: User-facing CLI command for viewing and managing global settings

Both proposals are minimal and focused on providing a clean, extensible base for OpenSpec settings and future feature flags.
2025-12-20 19:45:55 +11:00
Tabish Bidiwale cefb4719aa fix(completions): resolve Windows compatibility issues in zsh-installer tests (#373)
- Fix canWriteFile to use fs.access with W_OK flag instead of Unix-style
  permission bits (stats.mode & 0o222) which don't work on Windows
- Update test paths to use platform-specific invalid paths that fail on
  both Unix and Windows
- Use regex for path separator matching in test assertions
2025-12-19 23:37:59 +11:00
Tabish Bidiwale 5e1cef3b3b fix(spec): align cli-completion spec with implementation (#360)
Update the cli-completion spec to match the actual implementation:

- Change `completion zsh` to `completion generate [shell]` command structure
- Update uninstall behavior to reflect confirmation prompt cancels entire operation
- Change "not installed" uninstall exit code from 0 to 1
- Update shell detection error message to match implementation
- Replace Purpose placeholder with actual description
2025-12-12 22:13:52 +11:00
1adf3cea88 feature/oh-my-zsh-completions (#289)
* shell completions for zsh

* after code review changes

* expose only postinstall.js script

* Replace _openspec "$@" with compdef in zsh-generator.ts to prevent execution during load

* Update test/commands/completion.test.ts

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* - Fix dispatcher to use $words instead of $line for subcommand routing
  - Add __complete endpoint with tab-separated output for safe parsing
  - Replace brittle awk parsing with __complete in completion helpers
  - Add uninstall confirmation prompt with --yes flag to skip
  - Prefer $ZSH env var for Oh My Zsh detection before dir check
  - Add fpath verification guidance for OMZ installations
  - Update cli-completion spec to document generate subcommand

* improve shell detection and installation handling

  - Return structured result from detectShell() with shell and detected name
  - Detect already-installed completions and skip reinstall
  - Add update detection with automatic backup of previous version
  - Add debug logging to silent catch blocks for diagnostics
  - Quote fpath directories to handle paths with spaces
  - Verify Oh My Zsh fpath configuration and add to .zshrc if needed
  - Show helpful error for detected but unsupported shells
  - Update all tests for new detection API

---------

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Co-authored-by: Tabish Bidiwale <30385142+TabishB@users.noreply.github.com>
2025-12-12 21:30:40 +11:00
Tabish Bidiwale 6d3cfe0443 docs(readme): alphabetize AI tools list and make collapsible (#343)
- Sort supported AI tools alphabetically (A-Z)
- Wrap both "Native Slash Commands" and "AGENTS.md Compatible" sections
  in collapsible <details> tags to reduce visual clutter
2025-11-28 16:13:36 +11:00
Tabish Bidiwale 17d1e5db3f fix(opencode): remove hardcoded agent field from slash commands (#335)
Remove the `agent: build` field from OpenCode slash command templates
to allow OpenCode to use the current/custom agent instead of requiring
the build agent to be available.

Fixes #334
2025-11-25 12:07:51 +11:00
github-actions[bot]andTabish Bidiwale 3f5a66d3e4 chore(release): version packages (#327)
* Version Packages

* chore: trigger CI

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Tabish Bidiwale <tabishbidiwale@gmail.com>
2025-11-21 22:58:29 +11:00
Tabish Bidiwaleandcoderabbitai[bot] c08fbc1ba0 chore: add changeset for new features and improvements (#326)
* Add changeset for new features and improvements

* Update .changeset/new-features-and-improvements.md

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

---------

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2025-11-21 21:49:33 +11:00
Tabish Bidiwale 938d03be9a feat(init): add IDE restart instruction after init (#323)
Add prominent restart instruction in success message to inform users
they need to restart their IDE/coding tool for slash commands to appear.
Applies to all tools when slash commands are created or refreshed.

Also updates cli-init spec to document the restart instruction requirement.
2025-11-21 20:29:53 +11:00
1105 changed files with 204335 additions and 10242 deletions
+1
View File
@@ -0,0 +1 @@
-P ubuntu-latest=catthehacker/ubuntu:act-latest
@@ -0,0 +1,46 @@
---
name: draft-openspec-docs
description: Collaborative page-drafting mode for the OpenSpec docs. Builds a scratch plan inside the target page (purpose, structure, numbered draft steps), iterates on it with the user, then drafts one section per approved step and cleans up after itself. Use when a page needs a from-scratch rewrite or a new page is being shaped with the user in the loop.
argument-hint: target page
---
# Draft OpenSpec docs (scratch-plan workflow)
You are shaping a docs page with the user in the loop. The page is planned and reviewed inside the page itself, then drafted one section at a time. Load `write-openspec-docs` (the style authority) and `no-ai-slop` before drafting anything.
## 1. Set up the scratch section
Strip the page to its title and `>` goal line, then add a working section below them:
```md
## Scratch: page plan (delete before publish)
### Purpose
### Structure
```
- **Purpose**: 3-5 dot points. Who the reader is and what they come to look up, what the page covers, what it links out to. Check `docs-lab/README.md` (the page's goal line) and `docs-lab/message-map.md` (the questions routed here) before writing it.
- **Structure**: a numbered list of the page's sections, one line each naming the section and the shape of its content (table, fence, tree, bullets).
- Say what the page will do, never what it won't. Plain words and short bullets; the user reads this in their editor.
## 2. Iterate until the plan is approved
- Plan edits are cheap; page edits aren't. Reshape the plan as many times as the user asks before drafting.
- Record every decision in the plan itself, not only in chat. Add a `### Notes` list for follow-ups that belong to other pages and product observations found along the way.
- The user may edit the file directly between turns; their edits are decisions, not drift to revert.
- Surface one open call at a time, with a recommendation.
## 3. Add the draft plan, then draft step by step
Once the structure holds, add a `### Draft plan` below the notes: one step per page section, each with an ID and a readable title (`**D1. Goal line and intro**`), ending with a consolidation step (cross-page updates) and a cleanup step. Then:
- Wait for the user to call a step ID. Draft exactly that step, into the page above the scratch block.
- Verify each fact against source before writing it; a cheap grep beats trust. Reference content shows the raw contract (templates, instructions, config) verbatim in fences, linked to the file on GitHub, rather than paraphrasing it.
- Keep sibling sections on a repeatable sub-structure so the page scans as one system.
- Mark the step `(done)` in the plan, report what landed, and name the next step.
## 4. Consolidation and cleanup
- **Consolidation**: update everything that points at the page. The README goal line (verbatim match with the page's `>` line), the message map, the sync config (`website/docs.sync.config.mjs`), and any cross-links found by grepping the tree. Run `node website/scripts/sync-docs.mjs` to validate.
- **Cleanup**: delete the scratch block, run the retrievability and glance tests from `write-openspec-docs` at desktop and narrow widths, and flip the page's message-map row to Answered if its prose landed.
+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.
@@ -0,0 +1,49 @@
---
name: verify-openspec-docs
description: Fact-checks OpenSpec user documentation with a fresh-context subagent that re-runs commands and checks claims against source. Manually triggered; not part of the drafting loop. Use when the user asks to verify, fact-check, or accuracy-check a docs page, section, or set of changed claims.
argument-hint: page or section
---
# Verify OpenSpec docs
Check finished docs prose against reality. The point of a fresh context is that the reviewer hasn't watched the prose get written, so it can't be talked into the author's assumptions.
This skill runs only when the user asks for it. Drafting is owned by `write-openspec-docs`; don't invoke this from inside a drafting session unless the user requests a verification pass.
## Scope the run
1. Confirm the target: a page, one `##` section, or a list of changed claims. If invoked without a target, ask.
2. Read the README at the root of the docs tree the target lives in; its invariants and page map are part of what gets checked.
3. One subagent per unit (one `##` section, or the stated claim list). A full page is several subagents, run in parallel.
## Spawn the reviewer
General-purpose subagent. Subagents don't inherit skills, so the prompt hands the reviewer everything by path. Fill every placeholder, make every path absolute, and send:
```
You are reviewing one unit of OpenSpec's user documentation before it reaches the docs owner. Be the two hardest readers it will meet: a skeptical developer reading it cold, and a fact-checker with the repo open.
Repo root: <ABSOLUTE REPO ROOT>. Use absolute paths with every tool.
Read first:
1. <DOCS TREE ROOT>/README.md: the page map and standing invariants.
2. <ABSOLUTE REPO ROOT>/.agents/skills/write-openspec-docs/writing.md: the house writing rules.
3. <PAGE PATH>: review only <the section "<HEADING>" | these changed claims: <LIST>>; read the rest of the page for context.
Then check, in this order:
1. Facts. Every command, flag, path, config key, output block, default, and behavior claim. Re-run the terminal commands shown: read-only commands anywhere, anything that mutates state in a scratch directory or not at all. Commands for the AI chat surface (like /opsx:propose) can't run in a shell; verify their names and behavior against the skill sources this repo ships. Check names against src/ and the CLI's own --help. An output block must match what the command actually prints.
2. Examples. Any example spec or change must pass `openspec validate`. Run it when the example exists on disk.
3. Structure. Flag anything that re-explains a topic whose canonical home is another page, or breaks a rule the docs tree's README states.
4. Job fit. Does the unit serve the page's stated job (the one-line statement under the title, if present)? Does the arriving reader get what they came for quickly?
5. Trust and slop. Flag: hype or comfort adjectives (easy, simple, powerful, seamless), claims with no shown evidence, vague generalization where a specific fact belongs, binary contrasts ("not X, it's Y"), colon reveals, importance puffery, summary endings, em dashes, bullet lists that should be prose, and three parallel punchy sentences in a row.
Report findings only, most severe first. For each: quote the line, say what is wrong, and give the fix in one line. For every fact you verified, say how (the command you ran, or the file and line you checked). List any claim you could not verify and why. Do not rewrite the unit. If the unit is clean, say so and list exactly what you verified.
```
## Handle the report
- Default is report, not rewrite: show the user the findings ranked most severe first, each with the quoted line and one-line fix, plus what was verified and how, and any claim the reviewer couldn't verify.
- Apply fixes only when the user asked for a verify-and-fix run or approves the findings. A verifier can also be wrong: rejections go in the report with your reason, so the user can overrule you.
- If an applied fix changed a factual claim, verify again, scoped to the changed claims. Typo and wording fixes don't need a second pass.
- Two passes without converging means stop and take it to the user. Don't polish in a loop.
@@ -0,0 +1,34 @@
---
name: write-openspec-docs
description: Switches into OpenSpec docs-writing mode; loads the house style guide and drafts or revises pages in its voice (action-first, no preamble, scannable). Use when writing or editing pages in the OpenSpec docs tree.
argument-hint: page or section
---
# Write OpenSpec docs
You are now writing OpenSpec's user docs. Read [writing.md](writing.md); it is the style authority for everything drafted here. The short version, in effect immediately:
- A page is a retrieval surface, not an essay. Structure decides whether the reader finds the answer; prose only decides how it reads. Open every section with the answer, never a running story.
- Choose the page type before the outline. Guides follow the reader's task; reference mirrors the product's structure and uses exact field, command, and file names as scan anchors. Reference needs complete coverage without compressing several facts into one sentence, cell, or paragraph.
- Draft the shortest version that answers; expanding a spare page is cheap, cutting a bloated one is a rewrite. Plain words, the fewest of them: an idea that fits in one line takes one line. Depth most readers skip goes behind a link, and the payload (commands, real output, failures and fixes) stays whole.
- Dumb sentences, smart structure. Write the obvious sentence (actor, verb, object, stating the literal event); never compress extra facts in or take an angle. No hype adjectives, no preamble, no em dashes.
- One job per slot: one fact per sentence, list intros only announce the list, one reader question or lookup target per section. A related fact gets its own slot, never a ride in someone else's.
- Ground items in what the reader can verify: path or folder first, concept as the gloss, real output shown honestly.
- No house template. Inventories open with a list naming every item, then expand each in its own unit after the list, never inline. Sequences take numbered steps (numbers mean order; inventories take bullets). Single ideas and reasoning stay in short prose.
- Every load-bearing fact sits on a scan anchor: code fence, numbered bold lead-in, `**Term**: fact` bullet, table, file tree. Never only mid-paragraph.
- Before finishing, run two backstop tests. Retrievability: can each question or exact product name be found by scanning alone? The glance: inspect the rendered page as shapes; does it look finishable, or like work? Check table-heavy changes at desktop and narrow widths. A failure means a slot got written without being earned; fix it now, don't leave it for review.
## Ground rules
- Load the `no-ai-slop` skill before drafting; it owns the generic slop patterns, while [writing.md](writing.md) owns what OpenSpec's docs specifically look and sound like.
- Read the target page in full before editing it.
- Real facts only: flags, paths, and output as they exist in source. If a claim can't be checked cheaply, still write it, but name it as unchecked when you show the work; never bridge a gap with a plausible-sounding sentence.
- A fact lives on one page; everywhere else links to it. The docs tree's README owns the page map and structural invariants; check it before restructuring or adding pages.
- For reference pages, inventory the contract from source before drafting prose. Follow the reference process in [writing.md](writing.md#reference-pages).
- When unsure how something should scan or sound, match the exemplars: `docs-lab/start/setup.md` for section shape and inventories, `docs-lab/start/installation.md` (Uninstalling) for multi-step tasks.
## When done
Show the user what changed and name any unchecked claims.
If the user asks for the deep, evidence-first drafting session (run every command, one section per sitting, formal checkpoints), follow [full-process.md](full-process.md).
@@ -0,0 +1,49 @@
# Full drafting process (opt-in)
The evidence-first, checkpointed way to draft a page. Use this only when the user asks for the deep process; the default mode is SKILL.md alone.
## Orient (every session, before any writing)
1. Read the README at the root of the docs tree you're writing in. Where it states invariants or a page map, it wins over this skill.
2. Read the target page top to bottom, plus anything its comments cite.
3. Read the sibling pages this page links to or overlaps with, enough to know what must stay a link rather than become an explanation.
4. Write down the page's job in one line: who arrives, trying to do what, and what they leave able to do. If the page carries a job statement (docs-lab pages use a `>` blockquote under the title), test against it. If the unit you're about to write doesn't serve that job, stop and raise it instead of drafting around it.
## Work in small units
- The default unit is one `##` section. A page is several sittings, not one.
- For revisions to existing prose, the unit is the requested change, however many headings it touches.
- Draft the next unit only after the user has reviewed the current one. When the user asks for fixes, fix only that; don't smuggle in the next unit.
## Evidence before prose
Before drafting a unit, know where its claims come from.
- Run the terminal commands the unit will show when they're cheap: read-only commands anywhere, anything that mutates state in a scratch directory or not at all. Paste real output; trim it, never retouch it.
- Check names against source: flags, paths, config keys, and defaults come from `src/` and the CLI's own help, not from older docs. When docs and source disagree, source wins; note the conflict for the user.
- Never bridge a gap with a plausible-sounding sentence. A claim you couldn't check gets flagged at the checkpoint, not silently shipped. Don't let one expensive check stall the draft.
## Strip the slop
Invoke the `no-ai-slop` skill on the drafted unit and apply its edit pass. Docs prose gets no exemption: the patterns it names read as machine-written to exactly the audience these docs must win over.
## Does it do the job?
Reread the unit cold, as the reader the job line names, arriving with their actual problem. Answer three questions:
1. Can they act? Every step is runnable as written, and nothing depends on knowledge the page hasn't given or linked.
2. Do they know it worked? Where success could be in doubt, the unit shows something visible: output, a file, what the agent does next. Where the outcome is obvious, no success line is owed.
3. What can they do now that they couldn't before? If the honest answer is "they read some context", the unit is explaining instead of solving; cut it back to what serves the job or raise it with the user.
A unit that fails here gets fixed before the checkpoint, not annotated.
## Checkpoint
End the unit by showing the user:
- the file path and the unit written;
- the page's job in one line, and what this unit lets the reader do toward it;
- which claims you checked and how (commands run, files read);
- any claim still unchecked.
Then stop. The next unit starts when the user says so.
@@ -0,0 +1,204 @@
# OpenSpec docs: the style guide
Structure, voice, tone, and language for OpenSpec's user docs. This file is the primary style authority for the docs tree. The tree's own README owns structure (the page map and which page teaches what); when this file and that README disagree, the README wins. `no-ai-slop` owns the generic slop patterns; this file owns what OpenSpec's docs specifically look and sound like.
## Six principles
Every rule below applies one of these; when rules collide, the principles decide.
- **A page is a retrieval surface, not an essay.** The reader arrives mid-task with a question, scans for the answer, and leaves. Structure decides whether they find it; prose only decides how it reads. Structure wins.
- **The shortest version that answers is the right length, drafted that way from the start.** Expanding a spare page is cheap; cutting a bloated one is a rewrite. Plain words, and the fewest of them: an idea that fits in one line takes one line.
- **Dumb sentences, smart structure.** Write the obvious sentence: actor, verb, object, stating the literal event ("Running init creates two things in your project"). Never the version that compresses facts in or takes an angle ("Everything init creates is meant to be committed"). If a sentence needs unpacking, it failed.
- **One job per slot.** A sentence carries one fact (one carrying three hides two). A list intro only announces its list. A section owns one reader question or lookup target. Related facts get their own slot, never a ride in someone else's.
- **Ground everything in what the reader can verify.** Name things by path, file, or real output: what the reader could match against `ls`. The concept is the gloss, never the name.
- **No house template.** Shape follows content: inventories get overview-then-expand, sequences get numbered steps, a single idea gets short paragraphs, and reasoning lives in prose. The universal check is the retrievability test, not bullet count.
## The retrievability test
Name the questions or exact product terms a reader would bring to the section ("does init touch `.gitignore`?", "how do I add a tool later?", `generates`). Each answer or term must be findable by scanning, heading to anchor to fact, without reading paragraphs. If finding a fact means reading sentences, restructure; prose that passes needs no bullets, and no amount of bullets saves a section that fails.
## The shortest draft
Brevity happens at drafting time, not review. A page that needs heavy cutting in review gets rewritten, and a rewrite costs more than writing it spare the first time. Start from the shortest version that answers and expand only where a real reader question goes unanswered.
Every slot is earned before it's written:
- **The unit**: it answers a reader question or documents a lookup target, or it doesn't go in. Tight prose on the wrong scope is still the wrong scope.
- **The sentence**: would any reader come back for it? If not, it spends attention without buying anything; don't write it.
- **The depth**: an edge case or rationale most readers skip goes behind a link to its canonical page, not inline. The docs keep the depth; this page doesn't charge every reader for it.
- **The payload**: the command, the real output, the failure and its fix stay whole. Spare means no wind-up and no commentary, never fewer facts.
The glance test is the backstop, not the method. Scroll the rendered page and read it as shapes: short units, air between anchors, no screen-filling block of anything. A page that looks like work loses its reader before the first sentence; if yours does, something above got in without earning its slot. For table or layout changes, check both desktop and narrow widths; the Markdown source can't show cramped columns, poor wrapping, or horizontal scrolling.
## Shape of a section
- **Answer first**: open with the command, the inventory, or the fact in one line. Context and rationale come after, never first.
- **Inventory, then expand**: when a section covers several things (what init installs, what an uninstall leaves behind), open with a list naming every item in one line each, then expand each in its own unit after the list (a subsection or bold lead-in).
- **The overview only names**: a count ("two things:") is not an inventory, and expansion never happens inline in the list; the reader sees the whole footprint, then the detail.
- **Place first, meaning second**: name each on-disk item by its path or folder ("an `openspec/` folder at the repo root"), never by concept alone ("the planning folder"). The concept gloss can wait for the item's expansion. When a location varies (per tool, per OS), anchor it with a real folder or two ("`.agents/`, `.claude/`") and link the full list.
- **Core before nuance**: inside every unit, the answer, then what to expect, then edge cases last. A reader who stops early still leaves with the core.
- **One unit per target**: task pages separate different reader questions. Reference pages separate different product elements when readers look them up independently. Two facts with different targets get separate units, even when one elegant sentence could join them.
## Scan anchors
Every load-bearing fact sits on an anchor: something the eye lands on without reading. A fact a reader might come back for never lives only in the middle of a paragraph. The anchors these docs use:
- Code fences, for commands and real output.
- Numbered steps with bold lead-ins (`**1. Remove the package.**`) for multi-step tasks.
- `**Term**: fact` bullets for options, properties, and locations.
- Tables, when several items share the same attributes (mostly reference pages).
- File trees with inline annotations for layouts.
A full screen of content with no anchor is a wall, even when every sentence in it is true.
## Choosing the shape
No house template: facts go on anchors (enumerable content defaults to a list or table); explanation, reasoning, and judgment go in prose. The content picks the form:
- **Sequences**: numbered steps, one bounded action each. Numbers mean order of execution; an inventory of things takes bullets, never numbers. Restate any value a step needs rather than pointing back three steps.
- **Options, properties, locations**: `**Term**: fact` bullets.
- **Items sharing the same attributes**: a table.
- **A single idea** (why a store is worth it, what sync treats as drift): a couple of short paragraphs; that is the right shape.
- **Connected reasoning**: a paragraph. Shredding a thought into fragments makes it harder to read, not easier; a bullet list of full explanatory sentences is a paragraph in costume, so write the paragraph.
Across every form:
- Cap lists at about five items. Longer than that, split by priority: common path first, edge cases into their own list or a linked page.
- Keep items parallel: same internal order (name, fact, catch, link), same grammatical shape. Repetition across items is what makes scanning work; never vary structure between items for the sake of the prose.
- Uniformity is right when the content is uniform (a reference table, an install matrix). When every section on a varied page resolves to the same pattern, some of those lists are disguised paragraphs.
## Prose budget
Paragraphs are glue between anchors, not containers for facts.
- One to three lines. Three is the ceiling; a glue line between two anchors is often enough.
- The list intro has exactly one job: say what the list is ("Running init creates two things in your project:"). Never spend that slot on a different fact, however related; it gets its own line after the list.
- Parentheses and semicolon riders are for true asides only (a version caveat, a pointer). If a reader might return for the fact, it gets its own anchor.
- A section that is mostly paragraphs is misshaped, unless the page is genuinely conceptual (Concepts, explanations of the model). Even there, front-load each paragraph and leave air between them.
## Sentences
- Short sentences, active voice. Default subject is "you" or the tool by name.
- No preamble. The first sentence of any unit states the thing itself, never wind-up ("Before we get into...", "It's worth understanding that...").
- No em dashes anywhere in these docs. Use a colon, a comma, parentheses, or two sentences.
- Write the sentence you would say out loud. A draft that splices clauses with semicolons or colon-stacked fragments ("different documents: fewer of them, different names, different structure") gets rewritten as the spoken version ("when you want these to be different documents, whether that means fewer of them, different names, or a different structure"). Colons still introduce lists, fences, and labels. They don't splice prose.
- Contractions are fine ("you're set", "doesn't come along"). These docs talk, they don't proclaim.
- Inside narrative paragraphs, vary sentence length so the prose doesn't read staccato. Paragraphs only; list items stay parallel even when the cadence repeats.
## Voice
The narrator is a colleague who has run every command on the page, hit the failure modes personally, and is telling you what they know. Not a marketer, not a tutorial host, not a manual.
- Calm and specific. The reader wants the fact, the command, and the catch, in that order.
- Confidence comes from precision, not emphasis. Never "very", "extremely", "critical", bold-for-importance, or exclamation marks.
- Plain judgment is welcome. The docs may tell the reader what to do and what to skip: "The `openspec/` folder: pause first."
- Address the reader as "you". OpenSpec, the CLI, and init do things. "We" appears only for project decisions ("we say skills"), never as a tour guide.
- Dry beats chirpy. No cheerleading, no apologizing, no drama around failures. A failure is a fact with a fix.
## Structure and tone by page type
Same voice everywhere; structure and temperature shift:
- **Start pages**: numbered steps and short units, nothing assumed, every step ends in something visible. Warmest the docs get, which is still plain.
- **Guides**: peer to peer, skip re-orientation. The judgment calls are the reason guides exist; put them on anchors so they scan.
- **Reference**: mirror the product, use its exact names as anchors, and cover the contract without compressing it. Tables and fragments are common, but scan speed decides the shape. No motivation or persuasion; the reader is here to look something up and leave.
- **Troubleshooting**: symptom, cause, fix, in that order, one unit per symptom. Name the error the reader sees. Never "you may notice" or "sometimes it can happen that".
Guides and Customize pages share one skeleton: a one-line what, a link to the Quickstart (never a recap), the 80% path, then Advanced. The exception is the concepts page, which is an explanation, not a task guide; its shape follows its concerns.
## Reference pages
Reference describes the product for a reader who is already working with it. The outline follows the machinery: file, block, field; command group, command, option; object, property, value. Use the product's exact names as headings when readers will search for those names. Reader-question headings belong to task pages unless the question itself is the established lookup term.
Cover the full contract without packing several facts into one sentence, cell, or paragraph.
### Draft the contract first
Before writing prose:
1. Inventory the product elements from source.
2. Arrange them in the same hierarchy as the product.
3. Write the smallest complete contract for each element.
4. Expand only the elements whose behavior needs more room.
5. Add examples that illustrate one rule at a time.
6. Audit defaults, constraints, failures, ignored input, and validation gaps.
For each field, option, command, or file, record the identity facts that apply:
- Exact name and syntax
- Type or accepted values
- Required state and default
- Scope, location, or base path
Then record the behavior facts that apply:
- Behavior and side effects
- Constraints
- Failure and ignored-input behavior
- What validation catches and misses
Don't create empty sections or table columns for facts that don't apply.
### Inventory, then expand
Open with a complete table or list. Expand an item below the inventory only when its behavior can't fit cleanly in the overview. Put the expansion under the item's exact name so the table of contents works as an index.
For field and option references, `Field | Contract` is the safe table shape when definitions need sentences. Add more columns only when every cell is short and comparable. If columns split one coherent definition into fragments or wrap badly at a narrow width, use fewer columns or move the detail below the inventory.
### Examples and edge cases
An example illustrates one mapping, rule, or result. It doesn't become a sequence the reader follows or a narrative about completing a task. A complete example may follow the contract when seeing the elements together helps lookup.
Put the concrete default path or value in the primary slot. Put environment variables and uncommon overrides afterward.
State the observable consequence of a limit. If OpenSpec ignores a misspelled field, say that validation passes and the field has no effect. If a value falls back, name the value OpenSpec uses.
## Two surfaces
OpenSpec spans the terminal and the AI chat, and readers mix them up. Label every snippet:
```
In your terminal:
openspec init
In your AI chat:
/opsx:propose add-rate-limit
```
Where the reader could doubt it worked (a fresh install, a first run, a command with no output of its own), end with the concrete success signal: the line the command prints, the file that now exists, what the agent says next. Where the outcome is obvious, stop; an unneeded success line is noise.
## Authoring mechanics
Pages are plain markdown; GitHub and the site both render them. JSX components (`<Callout>`, `<Tabs>`, `<details>`) don't render; never use them.
- **Install commands**: write the global npm command once, in a fence whose language is `npm`; the site renders it as npm/pnpm/yarn/bun tabs with a copy button per tab (`remarkNpm` in `website/source.config.ts`, which also persists the reader's choice across blocks). On GitHub the fence degrades to the plain npm command.
- **Callouts**: GitHub-style blockquote alerts (`> [!NOTE]`, `> [!TIP]`, `> [!IMPORTANT]`, `> [!WARNING]`, `> [!CAUTION]`); GitHub styles them natively and the site renders them as callouts (`remarkGfmAlert` in `website/lib/remark-gfm-alert.ts`). Never place one directly under the page title: the sync lifts the leading blockquote into the page description.
## What earns a developer's trust
- Show the real command and its real output, trimmed honestly. A retouched output is a lie the reader catches on their first run.
- No hype and no comfort adjectives: easy, simple, just, powerful, seamless, robust. Three lines that show the thing beat any adjective about it.
- State limits plainly. A named limitation builds more trust than praise: "Your assistant does need to be able to run shell commands; a few IDE integrations can't."
- Don't generalize. Where you're tempted to write what OpenSpec "helps" with, write what actually happens: which file appears, what the diff shows, what the agent does next.
- Don't define what you can show. An unfamiliar term whose instances explain themselves (the workflow list: propose, explore, apply...) is introduced by showing the instances with one-phrase glosses; the abstraction can wait.
- Exact names: flags, paths, config keys, and versions as they exist in source, linked to their canonical page on first use.
## Naming and terms
- One term per concept, the glossary's term if the tree has one; today that means "skills", never "slash commands".
- No invented taxonomy. Product terms (spec, change, delta, profile, store) name real things; use them freely. Any other organizing word in a heading or goal ("layers", "levers", "pillars") must pass one test: would a reader use it to ask their own question? If not, write the reader's question or the plain enumeration ("What you can customize", never "The three layers").
- Examples invoke workflows by skill: the ask that triggers it ("ask your agent to propose a change") or the skill's name (`openspec-propose`), which is the same in every tool. A command spelling (`/opsx:propose`) appears only as a labeled per-tool example, never as the generic instruction; commands are headed for deprecation and their spellings vary per tool.
- Prefer the shared `.agents/` folder in file-path examples; a tool-specific folder (`.claude/`) appears only when the example is about that tool.
- Headings lead with a verb when the section is something the reader does ("Initialize your project"). Found content takes a plain noun phrase ("Install methods"). Never a vague verb ("Understand it") and never a pun.
- If the page carries a one-line job statement under the title (docs-lab uses a `>` blockquote the site lifts into the page description), keep it plain, concrete, and true of the finished page.
## One canonical home
A fact lives on exactly one page; everywhere else links to it. A second copy is a future contradiction. The tree's README says which page owns what; when in doubt, link.
## Exemplars
When unsure how something should scan or sound, match these:
- `docs-lab/start/setup.md`: section shape, inventory-then-expand, enumerable facts on bullets.
- `docs-lab/start/installation.md`, the Uninstalling section: multi-step tasks with bold numbered lead-ins.
+95 -4
View File
@@ -1,6 +1,97 @@
This directory is managed by Changesets.
# Changesets
- Add a changeset locally with `pnpm changeset`.
- The CI "Release (prepare)" workflow opens/updates a Version Packages PR.
- Publishing happens from a GitHub Release via the "Publish to npm" workflow.
This directory is managed by [Changesets](https://github.com/changesets/changesets).
## Quick Start
```bash
pnpm changeset
```
Follow the prompts to select version bump type and describe your changes.
## Workflow
1. **Choose the release path**: Maintainers decide whether a PR follows the normal release cadence or gets dedicated release tracking.
2. **Add dedicated release tracking**: When a maintainer asks for a changeset, run `pnpm changeset` locally before or after your PR.
3. **Version PR**: CI opens/updates a "Version Packages" PR when changesets merge to main.
4. **Release**: Merging the Version PR triggers npm publish and GitHub Release.
> **Note:** The default path is the normal release cadence. Add a changeset when a maintainer or release owner wants dedicated release notes and version tracking for the PR. Versioning (`changeset version`) and publishing happen automatically in CI.
## Template
Use this structure for your changeset content:
```markdown
---
"@fission-ai/openspec": patch
---
### New Features
- **Feature name** — What users can now do
### Bug Fixes
- Fixed issue where X happened when Y
### Breaking Changes
- `oldMethod()` has been removed, use `newMethod()` instead
### Deprecations
- `legacyOption` is deprecated and will be removed in v2.0
### Other
- Internal refactoring of X for better performance
```
Include only the sections relevant to your change.
## Version Bump Guide
| Type | When to use | Example |
|------|-------------|---------|
| `patch` | Release-tracked bug fixes, small improvements | Fixed crash when config missing |
| `minor` | New features, non-breaking additions | Added `--verbose` flag |
| `major` | Breaking changes, removed features | Renamed `init` to `setup` |
## When to Create a Changeset
**Use dedicated release tracking for:**
- New features or commands selected for release
- Notable bug fixes or hotfixes requested by a maintainer/release owner
- Breaking changes or deprecations
- Performance improvements users would notice and that are planned for release
**Use the normal release cadence for:**
- Routine bug fixes that fit the normal release cadence
- Documentation-only changes
- Test additions/fixes
- Internal refactoring that preserves user behavior
- CI/tooling changes
## Writing Good Descriptions
**Do:** Write for users, not developers
```markdown
- **Shell completions** — Tab completion now available for Bash, Fish, and PowerShell
```
**Don't:** Write implementation details
```markdown
- Added ShellCompletionGenerator class with Bash/Fish/PowerShell subclasses
```
**Do:** Explain the impact
```markdown
- Fixed config loading to respect `XDG_CONFIG_HOME` on Linux
```
**Don't:** Just reference the fix
```markdown
- Fixed #123
```
+4 -1
View File
@@ -1,6 +1,9 @@
{
"$schema": "https://unpkg.com/@changesets/config/schema.json",
"changelog": "@changesets/cli/changelog",
"changelog": [
"@changesets/changelog-github",
{ "repo": "Fission-AI/OpenSpec" }
],
"commit": false,
"fixed": [],
"linked": [],
+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
+106
View File
@@ -0,0 +1,106 @@
version: 2
# Dependabot does not manage two dependency surfaces in this repo:
# 1. pnpm `overrides` (pnpm-workspace.yaml, root and website) — transitive
# version pins that remediate advisories Dependabot can't otherwise reach.
# It never bumps or removes these; each carries an inline advisory comment
# noting the removal condition (see pnpm-workspace.yaml). They live in
# pnpm-workspace.yaml only, because Dependabot *does* rewrite a plain-name
# override (`postcss: ^8.5.26`) mirrored under package.json's `pnpm.overrides`
# — and that block replaces the workspace list rather than merging with it,
# so the mirror displaces the real pins. See #1812.
# 2. The Nix flake (flake.nix / flake.lock). Update nixpkgs manually with
# `nix flake update`; there is no Dependabot ecosystem for Nix. Note that the
# pnpmDeps FOD hash in flake.nix must be regenerated on any root lockfile change.
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
ignore:
- dependency-name: "@types/node"
update-types:
- version-update:semver-major
# Chalk 6 requires Node 22, while the published CLI supports Node 20.19.
- dependency-name: "chalk"
update-types:
- version-update:semver-major
- dependency-name: "typescript"
update-types:
- version-update:semver-major
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
ignore:
- dependency-name: "@types/node"
update-types:
- version-update:semver-major
- dependency-name: "typescript"
update-types:
- version-update:semver-major
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:
- "*"
+20
View File
@@ -0,0 +1,20 @@
# Github Workflows
## Testing CI Locally
Test GitHub Actions workflows locally using [act](https://nektosact.com/):
```bash
# Test all PR checks
act pull_request
# Test specific job
act pull_request -j nix-flake-validate
# Dry run to see what would execute
act pull_request --dryrun
```
The `.actrc` file configures act to use the appropriate Docker image.
+216 -82
View File
@@ -3,6 +3,8 @@ name: CI
on:
pull_request:
branches: [main]
merge_group:
branches: [main]
push:
branches: [main]
workflow_dispatch:
@@ -15,50 +17,41 @@ concurrency:
cancel-in-progress: true
jobs:
test_pr:
name: Test
# Detect which files changed to enable path-based filtering
changes:
name: Detect changes
runs-on: ubuntu-latest
timeout-minutes: 10
if: github.event_name == 'pull_request'
outputs:
nix: ${{ steps.filter.outputs.nix }}
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
- name: Check for Nix-related changes
uses: dorny/paths-filter@ceb8a2b8f2d89434be7ff52d3de7ec3738c5cc9d # v4
id: filter
with:
version: 9
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
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
filters: |
nix:
- 'flake.nix'
- 'flake.lock'
- 'package.json'
- 'pnpm-lock.yaml'
- 'pnpm-workspace.yaml'
- 'scripts/update-flake.sh'
- '.github/workflows/ci.yml'
# The Nix build runs `openspec completion generate`, so a change to
# the generator can break packaging without touching flake.nix.
- 'src/commands/completion.ts'
- 'src/core/completions/**'
test_matrix:
name: Test (${{ matrix.label }})
runs-on: ${{ matrix.os }}
timeout-minutes: 15
if: github.event_name != 'pull_request'
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:
@@ -66,12 +59,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:
@@ -79,19 +75,18 @@ 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@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0
- name: Setup Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: '20'
node-version: '20.19.0'
cache: 'pnpm'
- name: Print environment diagnostics
@@ -105,32 +100,48 @@ 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@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0
- name: Setup Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: '20'
node-version: '20.19.0'
cache: 'pnpm'
- name: Install dependencies
@@ -142,6 +153,9 @@ jobs:
- name: Type check
run: pnpm exec tsc --noEmit
- name: Lint
run: pnpm lint
- name: Check for build artifacts
run: |
if [ ! -d "dist" ]; then
@@ -153,61 +167,147 @@ jobs:
exit 1
fi
validate-changesets:
name: Validate Changesets
nix-flake-validate:
name: Nix Flake Validation
runs-on: ubuntu-latest
if: github.event_name == 'pull_request'
timeout-minutes: 10
needs: changes
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@3138316df39ed29be04236d7ffc686fa525866aa # v23
- name: Setup Nix cache
uses: DeterminateSystems/magic-nix-cache-action@84c0677f58dcedf3b91f8223ce36a9ea5b3c84b7 # v15
# Run the update script before `nix build`, not after. The script recomputes
# the pnpmDeps hash from pnpm-lock.yaml and rewrites flake.nix in place, so a
# stale hash is reported here as the exact value to paste. Built first, the
# same staleness surfaces as pnpm's ERR_PNPM_NO_OFFLINE_TARBALL — which names
# a missing tarball, not the hash — and the script never runs to say otherwise.
# Every root lockfile change needs this value, and Dependabot cannot produce it.
- name: Verify pnpmDeps hash matches the lockfile
run: |
bash scripts/update-flake.sh
if git diff --quiet flake.nix; then
echo "✅ flake.nix pnpmDeps hash is up to date"
exit 0
fi
# Scoped to the pnpmDeps block: a bare first-match would report some other
# FOD's hash if one is ever added above it.
HASH=$(sed -n '/pnpmDeps = /,/};/p' flake.nix \
| sed -nE 's/.*hash = "(sha256-[^"]+)".*/\1/p' | head -1)
git diff flake.nix
echo "::error file=flake.nix::Stale pnpmDeps hash. Set pnpmDeps.hash to $HASH and push."
exit 1
- name: Restore flake.nix
if: always()
run: git checkout -- flake.nix || true
- name: Build with Nix
run: nix build
- name: Verify build output
run: |
if [ ! -e "result" ]; then
echo "Error: Nix build output 'result' symlink not found"
exit 1
fi
if [ ! -f "result/bin/openspec" ]; then
echo "Error: openspec binary not found in build output"
exit 1
fi
for completion in \
"share/bash-completion/completions/openspec.bash" \
"share/fish/vendor_completions.d/openspec.fish" \
"share/zsh/site-functions/_openspec"; do
if [ ! -s "result/$completion" ]; then
echo "Error: completion script missing or empty: $completion"
exit 1
fi
done
if [ "$(head -1 result/share/zsh/site-functions/_openspec)" != "#compdef openspec" ]; then
echo "Error: zsh completion is not autoloadable (missing #compdef header)"
exit 1
fi
echo "✅ Build output verified"
- name: Test binary execution
run: |
VERSION=$(nix run . -- --version)
echo "OpenSpec version: $VERSION"
if [ -z "$VERSION" ]; then
echo "Error: Version command returned empty output"
exit 1
fi
echo "✅ Binary execution successful"
validate-changesets:
name: Validate Release Tracking
runs-on: ubuntu-latest
if: github.event_name == 'pull_request' || github.event_name == 'merge_group'
steps:
- name: Checkout code
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
persist-credentials: false
- name: Determine release tracking
id: changed-changesets
run: |
changed_changesets="$(git diff --name-only --diff-filter=ACMRT origin/main...HEAD -- '.changeset/*.md' ':!.changeset/README.md')"
if [[ -n "$changed_changesets" ]]; then
echo "has_changesets=true" >> "$GITHUB_OUTPUT"
# Run-unique delimiter: the value is a list of PR-authored paths, so a
# fixed "EOF" would let a crafted path close the block early and append
# its own key=value outputs.
delim="EOF_$(openssl rand -hex 16)"
{
echo "files<<$delim"
echo "$changed_changesets"
echo "$delim"
} >> "$GITHUB_OUTPUT"
else
echo "has_changesets=false" >> "$GITHUB_OUTPUT"
echo "This PR follows the normal release cadence; continuing with standard validation"
fi
- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: 9
if: steps.changed-changesets.outputs.has_changesets == 'true'
uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0
- name: Setup Node.js
uses: actions/setup-node@v4
if: steps.changed-changesets.outputs.has_changesets == 'true'
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: '20'
node-version: '24'
cache: 'pnpm'
- name: Install dependencies
if: steps.changed-changesets.outputs.has_changesets == 'true'
run: pnpm install --frozen-lockfile
- name: Validate changesets
- name: Validate release-tracked changesets
if: steps.changed-changesets.outputs.has_changesets == 'true'
env:
CHANGESET_FILES: ${{ steps.changed-changesets.outputs.files }}
run: |
if command -v changeset &> /dev/null; then
pnpm exec changeset status --since=origin/main
else
echo "Changesets not configured, skipping validation"
fi
echo "Validating changed changesets:"
printf '%s\n' "$CHANGESET_FILES"
pnpm exec changeset status --since=origin/main
required-checks-pr:
name: All checks passed
runs-on: ubuntu-latest
needs: [test_pr, lint]
if: always() && github.event_name == 'pull_request'
steps:
- name: Verify all checks passed
run: |
if [[ "${{ needs.test_pr.result }}" != "success" ]]; then
echo "Test job failed"
exit 1
fi
if [[ "${{ needs.lint.result }}" != "success" ]]; then
echo "Lint job failed"
exit 1
fi
echo "All required checks passed!"
required-checks-main:
name: All checks passed
runs-on: ubuntu-latest
needs: [test_matrix, lint]
if: always() && github.event_name != 'pull_request'
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: |
@@ -219,4 +319,38 @@ jobs:
echo "Lint job failed"
exit 1
fi
# Nix validation may be skipped if no Nix-related files changed
if [[ "${{ needs.nix-flake-validate.result }}" != "success" && "${{ needs.nix-flake-validate.result }}" != "skipped" ]]; then
echo "Nix flake validation job failed"
exit 1
fi
if [[ "${{ needs.nix-flake-validate.result }}" == "skipped" ]]; then
echo "Nix flake validation skipped (no Nix-related changes)"
fi
echo "All required checks passed!"
required-checks-main:
name: All checks passed
runs-on: ubuntu-latest
needs: [test_matrix, lint, nix-flake-validate]
if: always() && github.event_name == 'push'
steps:
- name: Verify all checks passed
run: |
if [[ "${{ needs.test_matrix.result }}" != "success" ]]; then
echo "Matrix test job failed"
exit 1
fi
if [[ "${{ needs.lint.result }}" != "success" ]]; then
echo "Lint job failed"
exit 1
fi
# Nix validation may be skipped if no Nix-related files changed
if [[ "${{ needs.nix-flake-validate.result }}" != "success" && "${{ needs.nix-flake-validate.result }}" != "skipped" ]]; then
echo "Nix flake validation job failed"
exit 1
fi
if [[ "${{ needs.nix-flake-validate.result }}" == "skipped" ]]; then
echo "Nix flake validation skipped (no Nix-related changes)"
fi
echo "All required checks passed!"
+160 -18
View File
@@ -1,12 +1,16 @@
name: Release (prepare)
name: Release
on:
push:
branches: [main]
workflow_dispatch: # manually cut a beta prerelease from main
# Floor for both jobs. The prepare job widens this to pull-requests: write for
# the Version Packages PR; the beta job only tags/releases + publishes via OIDC
# and needs no PR access, so it inherits this narrower default.
permissions:
contents: write
pull-requests: write
id-token: write # Required for npm OIDC trusted publishing
concurrency:
group: release-${{ github.ref }}
@@ -14,37 +18,175 @@ concurrency:
jobs:
prepare:
if: github.repository == 'Fission-AI/OpenSpec'
if: github.repository == 'Fission-AI/OpenSpec' && github.event_name == 'push'
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write # changesets opens/updates the Version Packages PR
id-token: write # Required for npm OIDC trusted publishing
steps:
- uses: actions/checkout@v4
# Generate GitHub App token first - used for checkout and changesets
# This allows git operations to trigger CI workflows on the version PR
# (GITHUB_TOKEN cannot trigger workflows by design)
- name: Generate GitHub App Token
id: app-token
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3
with:
app-id: ${{ vars.APP_ID }}
private-key: ${{ secrets.APP_PRIVATE_KEY }}
- 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@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0
- uses: actions/setup-node@v4
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: '20'
node-version: '24' # Node 24 includes npm 11.5.1+ required for OIDC
cache: 'pnpm'
registry-url: 'https://registry.npmjs.org'
scope: '@fission-ai'
always-auth: true
- run: pnpm install --frozen-lockfile
# Opens/updates the Version Packages PR; publishes when the Version PR merges
- name: Create/Update Version PR
uses: changesets/action@v1
id: changesets
uses: changesets/action@ae32849d5ba541f9ae29e40e22a623bc13562f51 # v2.1.2
with:
title: 'chore(release): version packages'
createGithubReleases: true
github-token: ${{ steps.app-token.outputs.token }}
pr-title: 'chore(release): version packages'
create-github-releases: true
# Preserve the v1 release path: pushes use the GitHub App token from
# checkout so version PR updates trigger their normal CI workflows.
push-with-git-cli: true
# Use CI-specific release script: relies on version PR having been merged
# so package.json already contains the bumped version.
publish: pnpm run release:ci
publish-script: pnpm run release:ci
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
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@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0
- 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
+120
View File
@@ -0,0 +1,120 @@
name: Security
on:
push:
branches: [main]
paths:
- '**/package.json'
- '**/pnpm-lock.yaml'
- '**/pnpm-workspace.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@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0
# No dependency cache: `pnpm audit` reads the lockfile, nothing is installed,
# so a cache-save step would fail on the missing store path.
- 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
# The website keeps its own lockfile and is never installed or built elsewhere
# in CI, so a website/package.json change — e.g. a security override — that is
# not reflected in website/pnpm-lock.yaml goes unnoticed: the override you think
# patches an advisory may not be in the committed graph at all, and `pnpm audit`
# would happily audit the stale (possibly still-vulnerable) tree. A frozen-lockfile
# install fails fast on that drift. Root drift is already caught by the
# `--frozen-lockfile` installs in ci.yml; this closes the same gap for the website.
# `--ignore-scripts` skips sharp's native build (irrelevant to lockfile validation
# and the usual source of install flake).
website-lockfile:
name: Website Lockfile Drift
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@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0
- name: Setup Node.js
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: '20.19.0'
- name: Verify website lockfile matches package.json
run: pnpm install --frozen-lockfile --ignore-scripts --dir website
+18 -2
View File
@@ -140,8 +140,6 @@ dist/
vite.config.js.timestamp-*
vite.config.ts.timestamp-*
# Internal Docs
docs/
# Claude
.claude/
@@ -150,3 +148,21 @@ CLAUDE.md
# Pnpm
.pnpm-store/
/package-lock.json
result
# OpenCode
.opencode/
opencode.json
# Codex
.codex/
# Bob
.bob/
# Trae
.trae/
# Cursor
.cursor/
-18
View File
@@ -1,18 +0,0 @@
<!-- OPENSPEC:START -->
# OpenSpec Instructions
These instructions are for AI assistants working in this project.
Always open `@/openspec/AGENTS.md` when the request:
- Mentions planning or proposals (words like proposal, spec, change, plan)
- Introduces new capabilities, breaking changes, architecture shifts, or big performance/security work
- Sounds ambiguous and you need the authoritative spec before coding
Use `@/openspec/AGENTS.md` to learn:
- How to create and apply change proposals
- Spec format and conventions
- Project structure and guidelines
Keep this managed block so 'openspec update' can refresh the instructions.
<!-- OPENSPEC:END -->
+1071
View File
File diff suppressed because it is too large Load Diff
+47
View File
@@ -0,0 +1,47 @@
# Contributing
Thanks for helping improve OpenSpec.
## 1. Open a discussion or an issue first
Every change starts here, including small ones.
- [Start a discussion](https://github.com/Fission-AI/OpenSpec/discussions) if it affects OpenSpec's core design.
- [Open an issue](https://github.com/Fission-AI/OpenSpec/issues) for bugs and everything else.
This is so we can agree on the approach before you spend time building. PRs without a linked issue or a prior discussion may be closed.
## 2. Decide whether it needs a change proposal
A bug fix, a typo, or a small improvement goes straight to a PR.
A new feature, a significant refactor, or anything that changes OpenSpec's architecture needs an OpenSpec change proposal first, so we can align on intent and goals before implementation begins. Open it as a PR containing only `openspec/changes/<name>/` and wait for it to be approved before you write the code.
When writing a proposal, keep the OpenSpec philosophy in mind: we serve a wide variety of users across different coding agents, models, and use cases. Changes should work well for everyone.
If you are not sure which side of the line your change falls on, ask in the discussion or issue from step 1.
## 3. Make your change
You need Node 20.19+ and pnpm.
```bash
pnpm install
pnpm build # tests run against the build output
pnpm test
pnpm exec tsc --noEmit
pnpm lint
```
Those four commands are what CI runs, so a green local run means a green CI run.
Run `pnpm changeset` if your change affects users, and commit the file it generates.
## 4. Open the PR
- Branch off `main` in your fork.
- Title it as a conventional commit: `type(scope): subject`, for example `fix(archive): keep authored Purpose`.
- Link what you opened in step 1: `Closes #123` for an issue, or a link to the discussion when there is no issue.
- If a coding agent wrote the code, say which agent and model, and confirm you tested it. AI-generated code is welcome when it has been verified.
Maintainers are listed in [MAINTAINERS.md](MAINTAINERS.md).
+24
View File
@@ -0,0 +1,24 @@
# Maintainers
People who maintain and guide OpenSpec.
## Core Maintainers
| 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
Advisors help shape technical direction and provide guidance to the project.
| Name | GitHub | Focus |
|------|--------|-------|
| Hari Krishnan | [@harikrishnan83](https://github.com/harikrishnan83) | Technical direction |
+195 -300
View File
@@ -1,373 +1,268 @@
<p align="center">
<a href="https://github.com/Fission-AI/OpenSpec">
<picture>
<source srcset="assets/openspec_pixel_dark.svg" media="(prefers-color-scheme: dark)">
<source srcset="assets/openspec_pixel_light.svg" media="(prefers-color-scheme: light)">
<img src="assets/openspec_pixel_light.svg" alt="OpenSpec logo" height="64">
<source srcset="assets/openspec_bg.png">
<img src="assets/openspec_bg.png" alt="OpenSpec logo">
</picture>
</a>
</p>
<p align="center">Spec-driven development for AI coding assistants.</p>
<p align="center">
<a href="https://github.com/Fission-AI/OpenSpec/actions/workflows/ci.yml"><img alt="CI" src="https://github.com/Fission-AI/OpenSpec/actions/workflows/ci.yml/badge.svg" /></a>
<a href="https://www.npmjs.com/package/@fission-ai/openspec"><img alt="npm version" src="https://img.shields.io/npm/v/@fission-ai/openspec?style=flat-square" /></a>
<a href="https://nodejs.org/"><img alt="node version" src="https://img.shields.io/node/v/@fission-ai/openspec?style=flat-square" /></a>
<a href="./LICENSE"><img alt="License: MIT" src="https://img.shields.io/badge/License-MIT-blue.svg?style=flat-square" /></a>
<a href="https://conventionalcommits.org"><img alt="Conventional Commits" src="https://img.shields.io/badge/Conventional%20Commits-1.0.0-yellow.svg?style=flat-square" /></a>
<a href="https://discord.gg/YctCnvvshC"><img alt="Discord" src="https://img.shields.io/badge/Discord-Join%20the%20community-5865F2?logo=discord&logoColor=white&style=flat-square" /></a>
</p>
<p align="center">
<img src="assets/openspec_dashboard.png" alt="OpenSpec dashboard preview" width="90%">
<a href="https://github.com/Fission-AI/OpenSpec/actions/workflows/ci.yml"><img alt="CI" src="https://github.com/Fission-AI/OpenSpec/actions/workflows/ci.yml/badge.svg" /></a>
<a href="https://www.npmjs.com/package/@fission-ai/openspec"><img alt="npm version" src="https://img.shields.io/npm/v/@fission-ai/openspec?style=flat-square" /></a>
<a href="./LICENSE"><img alt="License: MIT" src="https://img.shields.io/badge/License-MIT-blue.svg?style=flat-square" /></a>
<a href="https://discord.gg/YctCnvvshC"><img alt="Discord" src="https://img.shields.io/discord/1411657095639601154?style=flat-square&logo=discord&logoColor=white&label=Discord&suffix=%20online" /></a>
</p>
<details>
<summary><strong>The most loved spec framework.</strong></summary>
[![Stars](https://img.shields.io/github/stars/Fission-AI/OpenSpec?style=flat-square&label=Stars)](https://github.com/Fission-AI/OpenSpec/stargazers)
[![Downloads](https://img.shields.io/npm/dm/@fission-ai/openspec?style=flat-square&label=Downloads/mo)](https://www.npmjs.com/package/@fission-ai/openspec)
[![Contributors](https://img.shields.io/github/contributors/Fission-AI/OpenSpec?style=flat-square&label=Contributors)](https://github.com/Fission-AI/OpenSpec/graphs/contributors)
</details>
<p></p>
Our philosophy:
```text
→ fluid not rigid
→ iterative not waterfall
→ easy not complex
→ built for brownfield not just greenfield
→ scalable from personal projects to enterprises
```
> [!TIP]
> **New workflow now available!** We've rebuilt OpenSpec with a new artifact-guided workflow.
>
> Run `/opsx:propose "your idea"` to get started. → [Learn more here](docs/opsx.md)
<p align="center">
Follow <a href="https://x.com/0xTab">@0xTab on X</a> for updates · Join the <a href="https://discord.gg/YctCnvvshC">OpenSpec Discord</a> for help and questions.
</p>
# OpenSpec
<!-- TODO: Add GIF demo of /opsx:propose → /opsx:archive workflow -->
OpenSpec aligns humans and AI coding assistants with spec-driven development so you agree on what to build before any code is written. **No API keys required.**
## See it in action
## Why OpenSpec?
```text
You: /opsx:explore
AI: What would you like to explore?
You: I want dark mode but I'm not sure how to do it cleanly.
AI: Let me look at your styling setup...
Cleanest path here: CSS variables + a small theme context,
with system-preference detection. No new dependencies. Scope it?
You: Yes, let's do it.
AI coding assistants are powerful but unpredictable when requirements live in chat history. OpenSpec adds a lightweight specification workflow that locks intent before implementation, giving you deterministic, reviewable outputs.
You: /opsx:propose add-dark-mode
AI: Created openspec/changes/add-dark-mode/
✓ proposal.md — why we're doing this, what's changing
✓ specs/ — requirements and scenarios
✓ design.md — technical approach
✓ tasks.md — implementation checklist
Ready for implementation!
Key outcomes:
- Human and AI stakeholders agree on specs before work begins.
- Structured change folders (proposals, tasks, and spec updates) keep scope explicit and auditable.
- Shared visibility into what's proposed, active, or archived.
- Works with the AI tools you already use: custom slash commands where supported, context rules everywhere else.
You: /opsx:apply
AI: Implementing tasks...
✓ 1.1 Add theme context provider
✓ 1.2 Create toggle component
✓ 2.1 Add CSS variables
✓ 2.2 Wire up localStorage
All tasks complete!
## How OpenSpec compares (at a glance)
- **Lightweight**: simple workflow, no API keys, minimal setup.
- **Brownfield-first**: works great beyond 0→1. OpenSpec separates the source of truth from proposals: `openspec/specs/` (current truth) and `openspec/changes/` (proposed updates). This keeps diffs explicit and manageable across features.
- **Change tracking**: proposals, tasks, and spec deltas live together; archiving merges the approved updates back into specs.
- **Compared to spec-kit & Kiro**: those shine for brand-new features (0→1). OpenSpec also excels when modifying existing behavior (1→n), especially when updates span multiple specs.
See the full comparison in [How OpenSpec Compares](#how-openspec-compares).
## How It Works
```
┌────────────────────┐
│ Draft Change │
│ Proposal │
└────────┬───────────┘
│ share intent with your AI
▼
┌────────────────────┐
│ Review & Align │
│ (edit specs/tasks) │◀──── feedback loop ──────┐
└────────┬───────────┘ │
│ approved plan │
▼ │
┌────────────────────┐ │
│ Implement Tasks │──────────────────────────┘
│ (AI writes code) │
└────────┬───────────┘
│ ship the change
▼
┌────────────────────┐
│ Archive & Update │
│ Specs (source) │
└────────────────────┘
1. Draft a change proposal that captures the spec updates you want.
2. Review the proposal with your AI assistant until everyone agrees.
3. Implement tasks that reference the agreed specs.
4. Archive the change to merge the approved updates back into the source-of-truth specs.
You: /opsx:archive
AI: Archived to openspec/changes/archive/2025-01-23-add-dark-mode/
Specs updated. Ready for the next feature.
```
## Getting Started
<details>
<summary><strong>What do the specs actually look like?</strong></summary>
### Supported AI Tools
Plain Markdown — requirements with concrete scenarios, no special syntax to learn. Here's what goes in the `specs/` folder created above:
#### Native Slash Commands
These tools have built-in OpenSpec commands. Select the OpenSpec integration when prompted.
```markdown
## ADDED Requirements
| Tool | Commands |
|------|----------|
| **Claude Code** | `/openspec:proposal`, `/openspec:apply`, `/openspec:archive` |
| **CodeBuddy Code (CLI)** | `/openspec:proposal`, `/openspec:apply`, `/openspec:archive` (`.codebuddy/commands/`) — see [docs](https://www.codebuddy.ai/cli) |
| **CoStrict** | `/openspec-proposal`, `/openspec-apply`, `/openspec-archive` (`.cospec/openspec/commands/`) — see [docs](https://costrict.ai)|
| **Cursor** | `/openspec-proposal`, `/openspec-apply`, `/openspec-archive` |
| **Cline** | Workflows in `.clinerules/workflows/` directory (`.clinerules/workflows/openspec-*.md`) |
| **Crush** | `/openspec-proposal`, `/openspec-apply`, `/openspec-archive` (`.crush/commands/openspec/`) |
| **RooCode** | `/openspec-proposal`, `/openspec-apply`, `/openspec-archive` (`.roo/commands/`) |
| **Factory Droid** | `/openspec-proposal`, `/openspec-apply`, `/openspec-archive` (`.factory/commands/`) |
| **Gemini CLI** | `/openspec:proposal`, `/openspec:apply`, `/openspec:archive` (`.gemini/commands/openspec/`) |
| **OpenCode** | `/openspec-proposal`, `/openspec-apply`, `/openspec-archive` |
| **Kilo Code** | `/openspec-proposal.md`, `/openspec-apply.md`, `/openspec-archive.md` (`.kilocode/workflows/`) |
| **Qoder (CLI)** | `/openspec:proposal`, `/openspec:apply`, `/openspec:archive` (`.qoder/commands/openspec/`) — see [docs](https://qoder.com/cli) |
| **Antigravity** | `/openspec-proposal`, `/openspec-apply`, `/openspec-archive` (`.agent/workflows/`) |
| **Windsurf** | `/openspec-proposal`, `/openspec-apply`, `/openspec-archive` (`.windsurf/workflows/`) |
| **Codex** | `/openspec-proposal`, `/openspec-apply`, `/openspec-archive` (global: `~/.codex/prompts`, auto-installed) |
| **GitHub Copilot** | `/openspec-proposal`, `/openspec-apply`, `/openspec-archive` (`.github/prompts/`) |
| **Amazon Q Developer** | `@openspec-proposal`, `@openspec-apply`, `@openspec-archive` (`.amazonq/prompts/`) |
| **Auggie (Augment CLI)** | `/openspec-proposal`, `/openspec-apply`, `/openspec-archive` (`.augment/commands/`) |
| **Qwen Code** | `/openspec-proposal`, `/openspec-apply`, `/openspec-archive` (`.qwen/commands/`) |
| **iFlow (iflow-cli)** | `/openspec-proposal`, `/openspec-apply`, `/openspec-archive` (`.iflow/commands/`) |
### 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
```
Kilo Code discovers team workflows automatically. Save the generated files under `.kilocode/workflows/` and trigger them from the command palette with `/openspec-proposal.md`, `/openspec-apply.md`, or `/openspec-archive.md`.
Your AI writes these; you review the plan before any code is written.
#### AGENTS.md Compatible
These tools automatically read workflow instructions from `openspec/AGENTS.md`. Ask them to follow the OpenSpec workflow if they need a reminder. Learn more about the [AGENTS.md convention](https://agents.md/).
OpenSpec is built with OpenSpec — browse this repo's live [specs](openspec/specs) and in-flight [changes](openspec/changes) for real examples at scale.
| Tools |
|-------|
| Amp • Jules • Others |
</details>
### Install & Initialize
<details>
<summary><strong>OpenSpec Dashboard</strong></summary>
#### Prerequisites
- **Node.js >= 20.19.0** - Check your version with `node --version`
<p align="center">
<img src="assets/openspec_dashboard.png" alt="OpenSpec dashboard preview" width="90%">
</p>
#### Step 1: Install the CLI globally
</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.** Homebrew installs it as a dependency.
Install OpenSpec globally:
```bash
npm install -g @fission-ai/openspec@latest
```
Verify installation:
Or install the official [Homebrew formula](https://formulae.brew.sh/formula/openspec) on macOS or Linux:
```bash
openspec --version
brew install openspec
```
#### Step 2: Initialize OpenSpec in your project
Then navigate to your project directory and initialize:
Navigate to your project directory:
```bash
cd my-project
```
Run the initialization:
```bash
cd your-project
openspec init
```
**What happens during initialization:**
- You'll be prompted to pick any natively supported AI tools (Claude Code, CodeBuddy, Cursor, OpenCode, Qoder,etc.); other assistants always rely on the shared `AGENTS.md` stub
- OpenSpec automatically configures slash commands for the tools you choose and always writes a managed `AGENTS.md` hand-off at the project root
- A new `openspec/` directory structure is created in your project
> **Want your AI to do it?** Paste the [setup prompt](docs-lab/start/installation.md#install-with-your-ai-assistant) into your coding assistant — it installs the CLI, runs `openspec init`, and verifies the result.
**After setup:**
- Primary AI tools can trigger `/openspec` workflows without additional configuration
- Run `openspec list` to verify the setup and view any active changes
- If your coding assistant doesn't surface the new slash commands right away, restart it. Slash commands are loaded at startup,
so a fresh launch ensures they appear
Now talk to your AI:
### Optional: Populate Project Context
- **Not sure what to build yet?** Start with `/opsx:explore`, a no-stakes thinking partner that reads your code, weighs options, and shapes a plan before any code gets written. ([Explore guide](docs/explore.md))
- **Already know what you want?** Go straight to `/opsx:propose <what-you-want-to-build>`.
After `openspec init` completes, you'll receive a suggested prompt to help populate your project context:
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`.
```text
Populate your project context:
"Please read openspec/project.md and help me fill it out with details about my project, tech stack, and conventions"
```
`/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).
Use `openspec/project.md` to define project-level conventions, standards, architectural patterns, and other guidelines that should be followed across all changes.
> [!NOTE]
> Not sure if your tool is supported? [View the full list](docs/supported-tools.md) – we support 30+ tools and growing.
>
> Also works with Homebrew, pnpm, yarn, bun, and Nix. [See installation options](docs-lab/start/installation.md).
### Create Your First Change
## Docs
Here's a real example showing the complete OpenSpec workflow. This works with any AI tool. Those with native slash commands will recognize the shortcuts automatically.
**Start here:** the **[Documentation Home](docs/README.md)** maps everything. New to OpenSpec? Read [Getting Started](docs/getting-started.md), then [How Commands Work](docs/how-commands-work.md) (where you actually type `/opsx:propose`).
#### 1. Draft the Proposal
Start by asking your AI to create a change proposal:
→ **[Getting Started](docs/getting-started.md)**: first steps<br>
→ **[Explore First](docs/explore.md)**: think it through with `/opsx:explore` before you commit<br>
→ **[How Commands Work](docs/how-commands-work.md)**: where slash commands run vs the CLI<br>
→ **[Core Concepts at a Glance](docs/overview.md)**: the whole mental model, one page<br>
→ **[Examples & Recipes](docs/examples.md)**: real changes, start to finish<br>
→ **[Workflows](docs/workflows.md)**: combos and patterns<br>
→ **[Existing Projects](docs/existing-projects.md)**: adopt OpenSpec on a brownfield codebase<br>
→ **[Editing a Change](docs/editing-changes.md)**: update artifacts, go back, reconcile manual edits<br>
→ **[Commands](docs/commands.md)**: slash commands & skills<br>
→ **[CLI](docs/cli.md)**: terminal reference<br>
→ **[Stores](docs/stores-beta/user-guide.md)**: plan in a separate repo, shared across your team (beta)<br>
→ **[Supported Tools](docs/supported-tools.md)**: tool integrations & install paths<br>
→ **[Concepts](docs/concepts.md)**: how it all fits<br>
→ **[Multi-Language](docs/multi-language.md)**: multi-language support<br>
→ **[Customization](docs/customization.md)**: make it yours<br>
→ **[Community Showcase](docs/community.md)**: projects and resources built with and for OpenSpec<br>
→ **[FAQ](docs/faq.md)** · **[Troubleshooting](docs/troubleshooting.md)** · **[Glossary](docs/glossary.md)**: quick help
```text
You: Create an OpenSpec change proposal for adding profile search filters by role and team
(Shortcut for tools with slash commands: /openspec:proposal Add profile search filters)
AI: I'll create an OpenSpec change proposal for profile filters.
*Scaffolds openspec/changes/add-profile-filters/ with proposal.md, tasks.md, spec deltas.*
```
## Community schemas
#### 2. Verify & Review
Check that the change was created correctly and review the proposal:
Third-party schema bundles distributed via standalone repositories — these provide opinionated workflows that integrate OpenSpec with other tools, similar to how [github/spec-kit's community extension catalog](https://github.com/github/spec-kit/tree/main/extensions) handles tool integrations.
```bash
$ openspec list # Confirm the change folder exists
$ openspec validate add-profile-filters # Validate spec formatting
$ openspec show add-profile-filters # Review proposal, tasks, and spec delta
```
→ **[Browse the catalog](docs/customization.md#community-schemas)** in the customization docs.
#### 3. Refine the Specs
Iterate on the specifications until they match your needs:
```text
You: Can you add acceptance criteria for the role and team filters?
## Why OpenSpec?
AI: I'll update the spec delta with scenarios for role and team filters.
*Edits openspec/changes/add-profile-filters/specs/profile/spec.md and tasks.md.*
```
AI coding assistants are powerful but unpredictable when requirements live only in chat history. OpenSpec adds a lightweight spec layer so you agree on what to build before any code is written.
#### 4. Implement the Change
Once specs look good, start implementation:
- **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 30+ AI assistants via slash commands
```text
You: The specs look good. Let's implement this change.
(Shortcut for tools with slash commands: /openspec:apply add-profile-filters)
### How we compare
AI: I'll work through the tasks in the add-profile-filters change.
*Implements tasks from openspec/changes/add-profile-filters/tasks.md*
*Marks tasks complete: Task 1.1 ✓, Task 1.2 ✓, Task 2.1 ✓...*
```
**vs. [Spec Kit](https://github.com/github/spec-kit)** (GitHub) — Thorough but heavyweight. Rigid phase gates, lots of Markdown, Python setup. OpenSpec is lighter and lets you iterate freely.
#### 5. Archive the Completed Change
After implementation is complete, archive the change:
**vs. [Kiro](https://kiro.dev)** (AWS) — Powerful but you're locked into their IDE and limited to Claude models. OpenSpec works with the tools you already use.
```text
AI: All tasks are complete. The implementation is ready.
You: Please archive the change
(Shortcut for tools with slash commands: /openspec:archive add-profile-filters)
AI: I'll archive the add-profile-filters change.
*Runs: openspec archive add-profile-filters --yes*
✓ Change archived successfully. Specs updated. Ready for the next feature!
```
Or run the command yourself in terminal:
```bash
$ openspec archive add-profile-filters --yes # Archive the completed change without prompts
```
**Note:** Tools with native slash commands (Claude Code, CodeBuddy, Cursor, Codex, Qoder, RooCode) can use the shortcuts shown. All other tools work with natural language requests to "create an OpenSpec proposal", "apply the OpenSpec change", or "archive the change".
## Command Reference
```bash
openspec list # View active change folders
openspec view # Interactive dashboard of specs and changes
openspec show <change> # Display change details (proposal, tasks, spec updates)
openspec validate <change> # Check spec formatting and structure
openspec archive <change> [--yes|-y] # Move a completed change into archive/ (non-interactive with --yes)
```
## Example: How AI Creates OpenSpec Files
When you ask your AI assistant to "add two-factor authentication", it creates:
```
openspec/
├── specs/
│ └── auth/
│ └── spec.md # Current auth spec (if exists)
└── changes/
└── add-2fa/ # AI creates this entire structure
├── proposal.md # Why and what changes
├── tasks.md # Implementation checklist
├── design.md # Technical decisions (optional)
└── specs/
└── auth/
└── spec.md # Delta showing additions
```
### AI-Generated Spec (created in `openspec/specs/auth/spec.md`):
```markdown
# Auth Specification
## Purpose
Authentication and session management.
## Requirements
### Requirement: User Authentication
The system SHALL issue a JWT on successful login.
#### Scenario: Valid credentials
- WHEN a user submits valid credentials
- THEN a JWT is returned
```
### AI-Generated Change Delta (created in `openspec/changes/add-2fa/specs/auth/spec.md`):
```markdown
# Delta for Auth
## ADDED Requirements
### Requirement: Two-Factor Authentication
The system MUST require a second factor during login.
#### Scenario: OTP required
- WHEN a user submits valid credentials
- THEN an OTP challenge is required
```
### AI-Generated Tasks (created in `openspec/changes/add-2fa/tasks.md`):
```markdown
## 1. Database Setup
- [ ] 1.1 Add OTP secret column to users table
- [ ] 1.2 Create OTP verification logs table
## 2. Backend Implementation
- [ ] 2.1 Add OTP generation endpoint
- [ ] 2.2 Modify login flow to require OTP
- [ ] 2.3 Add OTP verification endpoint
## 3. Frontend Updates
- [ ] 3.1 Create OTP input component
- [ ] 3.2 Update login flow UI
```
**Important:** You don't create these files manually. Your AI assistant generates them based on your requirements and the existing codebase.
## Understanding OpenSpec Files
### Delta Format
Deltas are "patches" that show how specs change:
- **`## ADDED Requirements`** - New capabilities
- **`## MODIFIED Requirements`** - Changed behavior (include complete updated text)
- **`## REMOVED Requirements`** - Deprecated features
**Format requirements:**
- Use `### Requirement: <name>` for headers
- Every requirement needs at least one `#### Scenario:` block
- Use SHALL/MUST in requirement text
## How OpenSpec Compares
### vs. spec-kit
OpenSpec’s two-folder model (`openspec/specs/` for the current truth, `openspec/changes/` for proposed updates) keeps state and diffs separate. This scales when you modify existing features or touch multiple specs. spec-kit is strong for greenfield/0→1 but provides less structure for cross-spec updates and evolving features.
### vs. Kiro.dev
OpenSpec groups every change for a feature in one folder (`openspec/changes/feature-name/`), making it easy to track related specs, tasks, and designs together. Kiro spreads updates across multiple spec folders, which can make feature tracking harder.
### vs. No Specs
Without specs, AI coding assistants generate code from vague prompts, often missing requirements or adding unwanted features. OpenSpec brings predictability by agreeing on the desired behavior before any code is written.
## Team Adoption
1. **Initialize OpenSpec** – Run `openspec init` in your repo.
2. **Start with new features** – Ask your AI to capture upcoming work as change proposals.
3. **Grow incrementally** – Each change archives into living specs that document your system.
4. **Stay flexible** – Different teammates can use Claude Code, CodeBuddy, Cursor, or any AGENTS.md-compatible tool while sharing the same specs.
Run `openspec update` whenever someone switches tools so your agents pick up the latest instructions and slash-command bindings.
**vs. nothing** — AI coding without specs means vague prompts and unpredictable results. OpenSpec brings predictability without the ceremony.
## Updating OpenSpec
1. **Upgrade the package**
```bash
npm install -g @fission-ai/openspec@latest
```
2. **Refresh agent instructions**
- Run `openspec update` inside each project to regenerate AI guidance and ensure the latest slash commands are active.
**Upgrade the package**
```bash
npm install -g @fission-ai/openspec@latest
```
If you installed OpenSpec with Homebrew:
```bash
brew upgrade openspec
```
**Refresh agent instructions**
Run this inside each project to regenerate AI guidance and ensure the latest slash commands are active:
```bash
openspec update
```
## Usage Notes
**Model selection**: OpenSpec works best with high-reasoning models. We recommend Codex 5.5 and Opus 4.7 for both planning and implementation.
**Context hygiene**: OpenSpec benefits from a clean context window. Clear your context before starting implementation and maintain good context hygiene throughout your session.
## Contributing
- Install dependencies: `pnpm install`
- Build: `pnpm run build`
- Test: `pnpm test`
- Develop CLI locally: `pnpm run dev` or `pnpm run dev:cli`
- Conventional commits (one-line): `type(scope): subject`
Open a discussion (for core design changes) or an issue before you open a PR, and link the issue or discussion from the PR. New features, significant refactors, and architectural changes need an OpenSpec change proposal first.
→ **[CONTRIBUTING.md](CONTRIBUTING.md)**: the full process, from first issue to merged PR
## Other
<details>
<summary><strong>Telemetry</strong></summary>
OpenSpec collects anonymous usage stats.
We collect only command names and version to understand usage patterns. No arguments, paths, content, or PII. Automatically disabled in CI.
**Opt-out (any one is enough):**
- `openspec config set telemetry.enabled false` (global config; unset means on)
- `export OPENSPEC_TELEMETRY=0` or `export DO_NOT_TRACK=1` (env overrides config)
</details>
<details>
<summary><strong>Maintainers & Advisors</strong></summary>
See [MAINTAINERS.md](MAINTAINERS.md) for the list of core maintainers and advisors who help guide the project.
</details>
## License
+475
View File
@@ -0,0 +1,475 @@
<p align="center">
<a href="https://github.com/Fission-AI/OpenSpec">
<picture>
<source srcset="assets/openspec_pixel_dark.svg" media="(prefers-color-scheme: dark)">
<source srcset="assets/openspec_pixel_light.svg" media="(prefers-color-scheme: light)">
<img src="assets/openspec_pixel_light.svg" alt="OpenSpec logo" height="64">
</picture>
</a>
</p>
<p align="center">Spec-driven development for AI coding assistants.</p>
<p align="center">
<a href="https://github.com/Fission-AI/OpenSpec/actions/workflows/ci.yml"><img alt="CI" src="https://github.com/Fission-AI/OpenSpec/actions/workflows/ci.yml/badge.svg" /></a>
<a href="https://www.npmjs.com/package/@fission-ai/openspec"><img alt="npm version" src="https://img.shields.io/npm/v/@fission-ai/openspec?style=flat-square" /></a>
<a href="https://nodejs.org/"><img alt="node version" src="https://img.shields.io/node/v/@fission-ai/openspec?style=flat-square" /></a>
<a href="./LICENSE"><img alt="License: MIT" src="https://img.shields.io/badge/License-MIT-blue.svg?style=flat-square" /></a>
<a href="https://conventionalcommits.org"><img alt="Conventional Commits" src="https://img.shields.io/badge/Conventional%20Commits-1.0.0-yellow.svg?style=flat-square" /></a>
<a href="https://discord.gg/YctCnvvshC"><img alt="Discord" src="https://img.shields.io/badge/Discord-Join%20the%20community-5865F2?logo=discord&logoColor=white&style=flat-square" /></a>
</p>
<p align="center">
<img src="assets/openspec_dashboard.png" alt="OpenSpec dashboard preview" width="90%">
</p>
<p align="center">
Follow <a href="https://x.com/0xTab">@0xTab on X</a> for updates · Join the <a href="https://discord.gg/YctCnvvshC">OpenSpec Discord</a> for help and questions.
</p>
<p align="center">
<sub>🧪 <strong>New:</strong> <a href="docs/opsx.md">OPSX Workflow</a> — schema-driven, hackable, fluid. Iterate on workflows without code changes.</sub>
</p>
# OpenSpec
OpenSpec aligns humans and AI coding assistants with spec-driven development so you agree on what to build before any code is written. **No API keys required.**
## Why OpenSpec?
AI coding assistants are powerful but unpredictable when requirements live in chat history. OpenSpec adds a lightweight specification workflow that locks intent before implementation, giving you deterministic, reviewable outputs.
Key outcomes:
- Human and AI stakeholders agree on specs before work begins.
- Structured change folders (proposals, tasks, and spec updates) keep scope explicit and auditable.
- Shared visibility into what's proposed, active, or archived.
- Works with the AI tools you already use: custom slash commands where supported, context rules everywhere else.
## How OpenSpec compares (at a glance)
- **Lightweight**: simple workflow, no API keys, minimal setup.
- **Brownfield-first**: works great beyond 0→1. OpenSpec separates the source of truth from proposals: `openspec/specs/` (current truth) and `openspec/changes/` (proposed updates). This keeps diffs explicit and manageable across features.
- **Change tracking**: proposals, tasks, and spec deltas live together; archiving merges the approved updates back into specs.
- **Compared to spec-kit & Kiro**: those shine for brand-new features (0→1). OpenSpec also excels when modifying existing behavior (1→n), especially when updates span multiple specs.
See the full comparison in [How OpenSpec Compares](#how-openspec-compares).
## How It Works
```
┌────────────────────┐
│ Draft Change │
│ Proposal │
└────────┬───────────┘
│ share intent with your AI
▼
┌────────────────────┐
│ Review & Align │
│ (edit specs/tasks) │◀──── feedback loop ──────┐
└────────┬───────────┘ │
│ approved plan │
▼ │
┌────────────────────┐ │
│ Implement Tasks │──────────────────────────┘
│ (AI writes code) │
└────────┬───────────┘
│ ship the change
▼
┌────────────────────┐
│ Archive & Update │
│ Specs (source) │
└────────────────────┘
1. Draft a change proposal that captures the spec updates you want.
2. Review the proposal with your AI assistant until everyone agrees.
3. Implement tasks that reference the agreed specs.
4. Archive the change to merge the approved updates back into the source-of-truth specs.
```
## Getting Started
### Supported AI Tools
<details>
<summary><strong>Native Slash Commands</strong> (click to expand)</summary>
These tools have built-in OpenSpec commands. Select the OpenSpec integration when prompted.
| Tool | Commands |
|------|----------|
| **Amazon Q Developer** | `@openspec-proposal`, `@openspec-apply`, `@openspec-archive` (`.amazonq/prompts/`) |
| **Antigravity** | `/openspec-proposal`, `/openspec-apply`, `/openspec-archive` (`.agent/workflows/`) |
| **Auggie (Augment CLI)** | `/openspec-proposal`, `/openspec-apply`, `/openspec-archive` (`.augment/commands/`) |
| **Claude Code** | `/openspec:proposal`, `/openspec:apply`, `/openspec:archive` |
| **Cline** | Workflows in `.clinerules/workflows/` directory (`.clinerules/workflows/openspec-*.md`) |
| **CodeBuddy Code (CLI)** | `/openspec:proposal`, `/openspec:apply`, `/openspec:archive` (`.codebuddy/commands/`) — see [docs](https://www.codebuddy.ai/cli) |
| **Codex** | `/openspec-proposal`, `/openspec-apply`, `/openspec-archive` (global: `~/.codex/prompts`, auto-installed) |
| **Continue** | `/openspec-proposal`, `/openspec-apply`, `/openspec-archive` (`.continue/prompts/`) |
| **CoStrict** | `/openspec-proposal`, `/openspec-apply`, `/openspec-archive` (`.cospec/openspec/commands/`) — see [docs](https://costrict.ai)|
| **Crush** | `/openspec-proposal`, `/openspec-apply`, `/openspec-archive` (`.crush/commands/openspec/`) |
| **Cursor** | `/openspec-proposal`, `/openspec-apply`, `/openspec-archive` |
| **Factory Droid** | `/openspec-proposal`, `/openspec-apply`, `/openspec-archive` (`.factory/commands/`) |
| **Gemini CLI** | `/openspec:proposal`, `/openspec:apply`, `/openspec:archive` (`.gemini/commands/openspec/`) |
| **GitHub Copilot** | `/openspec-proposal`, `/openspec-apply`, `/openspec-archive` (`.github/prompts/`) |
| **iFlow (iflow-cli)** | `/openspec-proposal`, `/openspec-apply`, `/openspec-archive` (`.iflow/commands/`) |
| **Kilo Code** | `/openspec-proposal.md`, `/openspec-apply.md`, `/openspec-archive.md` (`.kilocode/workflows/`) |
| **OpenCode** | `/openspec-proposal`, `/openspec-apply`, `/openspec-archive` |
| **Qoder** | `/openspec:proposal`, `/openspec:apply`, `/openspec:archive` (`.qoder/commands/openspec/`) — see [docs](https://qoder.com) |
| **Qwen Code** | `/openspec-proposal`, `/openspec-apply`, `/openspec-archive` (`.qwen/commands/`) |
| **RooCode** | `/openspec-proposal`, `/openspec-apply`, `/openspec-archive` (`.roo/commands/`) |
| **Windsurf** | `/openspec-proposal`, `/openspec-apply`, `/openspec-archive` (`.windsurf/workflows/`) |
Kilo Code discovers team workflows automatically. Save the generated files under `.kilocode/workflows/` and trigger them from the command palette with `/openspec-proposal.md`, `/openspec-apply.md`, or `/openspec-archive.md`.
</details>
<details>
<summary><strong>AGENTS.md Compatible</strong> (click to expand)</summary>
These tools automatically read workflow instructions from `openspec/AGENTS.md`. Ask them to follow the OpenSpec workflow if they need a reminder. Learn more about the [AGENTS.md convention](https://agents.md/).
| Tools |
|-------|
| Amp • Jules • Others |
</details>
### Install & Initialize
#### Prerequisites
- **Node.js >= 20.19.0** - Check your version with `node --version`
#### Step 1: Install the CLI globally
**Option A: Using npm**
```bash
npm install -g @fission-ai/openspec@latest
```
Verify installation:
```bash
openspec --version
```
**Option B: Using Nix (NixOS and Nix package manager)**
Run OpenSpec directly without installation:
```bash
nix run github:Fission-AI/OpenSpec -- init
```
Or install to your profile:
```bash
nix profile install github:Fission-AI/OpenSpec
```
Or add to your development environment in `flake.nix`:
```nix
{
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
openspec.url = "github:Fission-AI/OpenSpec";
};
outputs = { nixpkgs, openspec, ... }: {
devShells.x86_64-linux.default = nixpkgs.legacyPackages.x86_64-linux.mkShell {
buildInputs = [ openspec.packages.x86_64-linux.default ];
};
};
}
```
Verify installation:
```bash
openspec --version
```
#### Step 2: Initialize OpenSpec in your project
Navigate to your project directory:
```bash
cd my-project
```
Run the initialization:
```bash
openspec init
```
**What happens during initialization:**
- You'll be prompted to pick any natively supported AI tools (Claude Code, CodeBuddy, Cursor, OpenCode, Qoder,etc.); other assistants always rely on the shared `AGENTS.md` stub
- OpenSpec automatically configures slash commands for the tools you choose and always writes a managed `AGENTS.md` hand-off at the project root
- A new `openspec/` directory structure is created in your project
**After setup:**
- Primary AI tools can trigger `/openspec` workflows without additional configuration
- Run `openspec list` to verify the setup and view any active changes
- If your coding assistant doesn't surface the new slash commands right away, restart it. Slash commands are loaded at startup,
so a fresh launch ensures they appear
### Optional: Populate Project Context
After `openspec init` completes, you'll receive a suggested prompt to help populate your project context:
```text
Populate your project context:
"Please read openspec/project.md and help me fill it out with details about my project, tech stack, and conventions"
```
Use `openspec/project.md` to define project-level conventions, standards, architectural patterns, and other guidelines that should be followed across all changes.
### Create Your First Change
Here's a real example showing the complete OpenSpec workflow. This works with any AI tool. Those with native slash commands will recognize the shortcuts automatically.
#### 1. Draft the Proposal
Start by asking your AI to create a change proposal:
```text
You: Create an OpenSpec change proposal for adding profile search filters by role and team
(Shortcut for tools with slash commands: /openspec:proposal Add profile search filters)
AI: I'll create an OpenSpec change proposal for profile filters.
*Scaffolds openspec/changes/add-profile-filters/ with proposal.md, tasks.md, spec deltas.*
```
#### 2. Verify & Review
Check that the change was created correctly and review the proposal:
```bash
$ openspec list # Confirm the change folder exists
$ openspec validate add-profile-filters # Validate spec formatting
$ openspec show add-profile-filters # Review proposal, tasks, and spec delta
```
#### 3. Refine the Specs
Iterate on the specifications until they match your needs:
```text
You: Can you add acceptance criteria for the role and team filters?
AI: I'll update the spec delta with scenarios for role and team filters.
*Edits openspec/changes/add-profile-filters/specs/profile/spec.md and tasks.md.*
```
#### 4. Implement the Change
Once specs look good, start implementation:
```text
You: The specs look good. Let's implement this change.
(Shortcut for tools with slash commands: /openspec:apply add-profile-filters)
AI: I'll work through the tasks in the add-profile-filters change.
*Implements tasks from openspec/changes/add-profile-filters/tasks.md*
*Marks tasks complete: Task 1.1 ✓, Task 1.2 ✓, Task 2.1 ✓...*
```
#### 5. Archive the Completed Change
After implementation is complete, archive the change:
```text
AI: All tasks are complete. The implementation is ready.
You: Please archive the change
(Shortcut for tools with slash commands: /openspec:archive add-profile-filters)
AI: I'll archive the add-profile-filters change.
*Runs: openspec archive add-profile-filters --yes*
✓ Change archived successfully. Specs updated. Ready for the next feature!
```
Or run the command yourself in terminal:
```bash
$ openspec archive add-profile-filters --yes # Archive the completed change without prompts
```
**Note:** Tools with native slash commands (Claude Code, CodeBuddy, Cursor, Codex, Qoder, RooCode) can use the shortcuts shown. All other tools work with natural language requests to "create an OpenSpec proposal", "apply the OpenSpec change", or "archive the change".
## Command Reference
```bash
openspec list # View active change folders
openspec view # Interactive dashboard of specs and changes
openspec show <change> # Display change details (proposal, tasks, spec updates)
openspec validate <change> # Check spec formatting and structure
openspec archive <change> [--yes|-y] # Move a completed change into archive/ (non-interactive with --yes)
```
## Example: How AI Creates OpenSpec Files
When you ask your AI assistant to "add two-factor authentication", it creates:
```
openspec/
├── specs/
│ └── auth/
│ └── spec.md # Current auth spec (if exists)
└── changes/
└── add-2fa/ # AI creates this entire structure
├── proposal.md # Why and what changes
├── tasks.md # Implementation checklist
├── design.md # Technical decisions (optional)
└── specs/
└── auth/
└── spec.md # Delta showing additions
```
### AI-Generated Spec (created in `openspec/specs/auth/spec.md`):
```markdown
# Auth Specification
## Purpose
Authentication and session management.
## Requirements
### Requirement: User Authentication
The system SHALL issue a JWT on successful login.
#### Scenario: Valid credentials
- WHEN a user submits valid credentials
- THEN a JWT is returned
```
### AI-Generated Change Delta (created in `openspec/changes/add-2fa/specs/auth/spec.md`):
```markdown
# Delta for Auth
## ADDED Requirements
### Requirement: Two-Factor Authentication
The system MUST require a second factor during login.
#### Scenario: OTP required
- WHEN a user submits valid credentials
- THEN an OTP challenge is required
```
### AI-Generated Tasks (created in `openspec/changes/add-2fa/tasks.md`):
```markdown
## 1. Database Setup
- [ ] 1.1 Add OTP secret column to users table
- [ ] 1.2 Create OTP verification logs table
## 2. Backend Implementation
- [ ] 2.1 Add OTP generation endpoint
- [ ] 2.2 Modify login flow to require OTP
- [ ] 2.3 Add OTP verification endpoint
## 3. Frontend Updates
- [ ] 3.1 Create OTP input component
- [ ] 3.2 Update login flow UI
```
**Important:** You don't create these files manually. Your AI assistant generates them based on your requirements and the existing codebase.
## Understanding OpenSpec Files
### Delta Format
Deltas are "patches" that show how specs change:
- **`## ADDED Requirements`** - New capabilities
- **`## MODIFIED Requirements`** - Changed behavior (include complete updated text)
- **`## REMOVED Requirements`** - Deprecated features
**Format requirements:**
- Use `### Requirement: <name>` for headers
- Every requirement needs at least one `#### Scenario:` block
- Use SHALL/MUST in requirement text
## How OpenSpec Compares
### vs. spec-kit
OpenSpec’s two-folder model (`openspec/specs/` for the current truth, `openspec/changes/` for proposed updates) keeps state and diffs separate. This scales when you modify existing features or touch multiple specs. spec-kit is strong for greenfield/0→1 but provides less structure for cross-spec updates and evolving features.
### vs. Kiro.dev
OpenSpec groups every change for a feature in one folder (`openspec/changes/feature-name/`), making it easy to track related specs, tasks, and designs together. Kiro spreads updates across multiple spec folders, which can make feature tracking harder.
### vs. No Specs
Without specs, AI coding assistants generate code from vague prompts, often missing requirements or adding unwanted features. OpenSpec brings predictability by agreeing on the desired behavior before any code is written.
## Team Adoption
1. **Initialize OpenSpec** – Run `openspec init` in your repo.
2. **Start with new features** – Ask your AI to capture upcoming work as change proposals.
3. **Grow incrementally** – Each change archives into living specs that document your system.
4. **Stay flexible** – Different teammates can use Claude Code, CodeBuddy, Cursor, or any AGENTS.md-compatible tool while sharing the same specs.
Run `openspec update` whenever someone switches tools so your agents pick up the latest instructions and slash-command bindings.
## Updating OpenSpec
1. **Upgrade the package**
```bash
npm install -g @fission-ai/openspec@latest
```
2. **Refresh agent instructions**
- Run `openspec update` inside each project to regenerate AI guidance and ensure the latest slash commands are active.
## Experimental Features
<details>
<summary><strong>🧪 OPSX: Fluid, Iterative Workflow</strong> (Claude Code only)</summary>
**Why this exists:**
- Standard workflow is locked down — you can't tweak instructions or customize
- When AI output is bad, you can't improve the prompts yourself
- Same workflow for everyone, no way to match how your team works
**What's different:**
- **Hackable** — edit templates and schemas yourself, test immediately, no rebuild
- **Granular** — each artifact has its own instructions, test and tweak individually
- **Customizable** — define your own workflows, artifacts, and dependencies
- **Fluid** — no phase gates, update any artifact anytime
```
You can always go back:
proposal ──→ specs ──→ design ──→ tasks ──→ implement
▲ ▲ ▲ │
└───────────┴──────────┴────────────────────┘
```
| Command | What it does |
|---------|--------------|
| `/opsx:new` | Start a new change |
| `/opsx:continue` | Create the next artifact (based on what's ready) |
| `/opsx:ff` | Fast-forward (all planning artifacts at once) |
| `/opsx:apply` | Implement tasks, updating artifacts as needed |
| `/opsx:archive` | Archive when done |
**Setup:** `openspec experimental`
[Full documentation →](docs/opsx.md)
</details>
<details>
<summary><strong>Telemetry</strong> – OpenSpec collects anonymous usage stats (opt-out: <code>OPENSPEC_TELEMETRY=0</code>)</summary>
We collect only command names and version to understand usage patterns. No arguments, paths, content, or PII. Automatically disabled in CI.
**Opt-out:** `export OPENSPEC_TELEMETRY=0` or `export DO_NOT_TRACK=1`
</details>
## Contributing
- Install dependencies: `pnpm install`
- Build: `pnpm run build`
- Test: `pnpm test`
- Develop CLI locally: `pnpm run dev` or `pnpm run dev:cli`
- Conventional commits (one-line): `type(scope): subject`
<details>
<summary><strong>Maintainers & Advisors</strong></summary>
See [MAINTAINERS.md](MAINTAINERS.md) for the list of core maintainers and advisors who help guide the project.
</details>
## License
MIT
+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/`, and `schemas/`. 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 scripts | The package ships no `preinstall`, `install`, or `postinstall` script, so installing it from the npm registry runs no code from OpenSpec. (`prepare` is still declared; npm runs it only for git and local-directory installs, where it builds from source.) Shell completions are opt-in via `openspec completion install`; the CLI prints a one-line tip about them on its first run. |
| 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.
Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

+3 -1
View File
@@ -1,3 +1,5 @@
#!/usr/bin/env node
import '../dist/cli/index.js';
import { runCli } from '../dist/cli/index.js';
runCli();
+94
View File
@@ -0,0 +1,94 @@
Ok these are my notes when reviewing the different sections/file in docs-lab.
I've written down my thoughts when looking at these sections so we can think about how to update these
docs with the feedback in mind.
## Start > Overview
Ok the following subtitle here is horrible:
> OpenSpec gives you and your coding agent a shared, reviewable plan before code is written.
This is not a strong value prop in the day and age of plan mode, but other than that i don't think it sells openspec hard enough
OpenSpec is not really about a shared plan for a single session it's about a keeping things on track and aligned for larger features.
what we focus on:
- making it work for teams
- git native / things checked into vcs
- intented behaviour matches the implemented behaviour
- we help you capture intendede behaviour and match it to the implemented behaviour.
- it's about correctness, coherence,
Control theory inspiration:
Instructors break down how a system measures its current state, compares it to a desired goal, and adjusts its actions to reduce the difference.
## Guides > Explore and idea
I think in this section we should mention:
This is about exploring the problem space, figuring out what problems you care about and diving deeper into them.
It was designed with a very different philiosphy in mind of giving you the freedom to explore the problem space and jump around to different ideas and sections.
It serves a similar purpose to other newer entrees in the fields like superpowers or matt pocock's skills.
We often do see people combining explore with
Often this is a matter of UX and personal preference. There is no single best skill or method to getting to
aligment with your agent.
Some people prefer a conversing with a thoughtful design partner, others might prefer being asked questions till they have a good understanding of a problem.
Feel free to customize the explore skills to your needs.
Unrelated to docs:
How do we solve the problem for PM's?
How do we give them a good home? - what is their job to be done?
How we efficiently help them achieve that?
- they're basically turning it into tickets?
What do we want to get across the line this week?
- The spec drift agent?
- The dashboard?
- figure out how we use agent session and traces better
## From docs-lab drafting (2026-08-19, project-config page)
Product issue, not docs: the installed skills in this repo are stale against the current
templates. `.claude/skills/openspec-archive-change/SKILL.md` has no `openspec instructions`
call at all, while `src/core/templates/workflows/archive-change.ts:40` instructs one; the
installed apply skill also doesn't mention the `context`/`operationGuidance` fields in the
JSON it reads. So config injection reaches the CLI output, but a stale skill never tells
the agent to consume it. Running `openspec update` should refresh them.
## From docs-lab drafting (2026-08-19, schemas page)
Product issues found while verifying the schema system (all file refs current as of today):
- `schema init` next-steps output prints a command that doesn't exist in that form:
"Use with: openspec new --schema <name>" (schema.ts:999); real syntax is
`openspec new change <name> --schema <name>`.
- `openspec new change` spinner prints the hardcoded default schema, not the resolved one
(new-change.ts:118): "Creating change 'x' with schema 'spec-driven'..." then "Schema: lite".
- `schema fork` re-serializes schema.yaml (literal `instruction: |` becomes folded `>`,
comments dropped), so diffing a fork against upstream is noisy (schema.ts:706-712).
- All `openspec schema` subcommands plus `openspec schemas`/`templates` use process.cwd()
and take no --store; they silently see nothing when run from a subdirectory, unlike
root-resolved commands (schema.ts:383/485/634/768).
- `suggestSchemas` fuzzy "did you mean" helper exists but is wired to nothing
(project-config.ts:420).
Docs follow-up: the community schema catalog lives only in legacy docs/customization.md
(#community-schemas); customize/schemas.md links to it on GitHub. When the old docs tree
retires, the catalog needs a docs-lab home.
+294
View File
@@ -0,0 +1,294 @@
# docs-lab: parallel rebuild of the OpenSpec docs
**Status: prose is landing page by page; the rest are skeletons** (real headings plus a
one-line `>` job statement the site lifts into the page description). The live site
builds from this tree: `website/docs.sync.config.mjs` maps these files to published
pages, and the old `docs/` tree is no longer used by the site.
This README owns the structure: which pages exist and which page teaches what. The
reverse view, from a job or message to the page that owns it, is
[message-map.md](message-map.md). How to
write them (style, voice, formatting) is the `write-openspec-docs` skill's
[writing.md](../.agents/skills/write-openspec-docs/writing.md).
## The bar for every page
Every page in docs-lab is written by hand, from scratch. The old `docs/` tree is source
material for facts, never text to carry over.
What we're after is that the reader gets the idea: every page reads well and makes sense
to anyone, whatever their level of skill, and above all it is simple. The worst thing we
can ship is documentation that is cognitively expensive to understand, and that cost
comes from complicated words, metaphors that don't make sense, random terminology that
isn't explained, and formatting that gets in the way of reading. Every sentence has a
purpose and is easy to read and comprehend. If a sentence doesn't pass that test, rewrite
it or cut it.
## Structure rules
**Folders are the areas.** Every page lives in its area's folder (`start/`,
`guides/`, `customize/`, `multi-repo/`, `reference/`, `help/`); the root holds only this
README, `message-map.md`, and `sources.md`. Most folders publish as one
sidebar group; `guides/` publishes as the Guides group, holding three collapsible
subgroups (Understanding OpenSpec, Using OpenSpec, Adopting OpenSpec), all expanded
by default (held back from the site until the pages are drafted: the whole section is
commented out in `website/docs.sync.config.mjs`, and links to a guide fall back to its
source on GitHub until it's re-listed). Reference holds three nested
folders (`reference/architecture/`, `reference/schemas/`, and
`reference/configuration/`), each publishing as a collapsible group with `index.md` as
its landing page; the spec-driven schema publishes as a single page
(`reference/schemas/spec-driven/index.md`) inside the Schemas group. Labels and URLs come from
`website/docs.sync.config.mjs`, so moving a file never moves a URL.
**Teach once.** The loop (propose, review, apply, archive) has one teacher; every other
page links, never re-teaches:
- `start/quickstart.md` teaches it as UX: how a human moves a change through the
lifecycle, including what archive does on disk.
- `start/overview.md` shows it as pitch: copy only, no explanation.
- `guides/concepts.md` stays out of it: the page explains the artifacts (specs, changes,
the delta) and links to the quickstart for the loop. Disk paths appear inline with the
concept that owns them, never as a layout section.
- `start/installation.md` owns install; `start/setup.md` owns init and what it writes.
The quickstart opens with one prerequisite line linking both and starts at explore.
**Guides vs reference.** `reference/skills.md` holds each skill's contract: arguments,
what it creates, and what it responds with. Guide pages
(the Using and Adopting subgroups) own the human judgment for a task, including when
to reach for each skill, may span several skills, and never restate skill mechanics.
`reference/architecture/` is the one exception to Reference's look-it-up bar: it's
explanation content, housed here as a pragmatic home while it's three pages. If it
grows (say, by absorbing contributor internals), consider giving it its own folder
and tab.
**Reference is lookup, and named for it.** `reference/schemas/` and
`reference/configuration/` are contracts: keys, values, types, defaults, and
locations, on tables and fences. Anything explanatory (what a schema is, what
to put in config.yaml) lives in Customize or Guides and is linked, never
restated. Naming follows three rules. A reference folder's landing
page is titled "Overview"; the folder label already names the group, and
repeating it double-nests the sidebar. A page documenting one file carries
concept and filename in the title, concept first, where the concept names the
file's use, never just its scope ("Project configuration (config.yaml)", "CLI
settings (config.json)"): the left edge is what the eye disambiguates in the
sidebar, and the filename keeps the title matching what readers search for and
see on disk. A file whose name is the term readers use keeps the filename
alone as the title (`schema.yaml`), and a page owning one product term takes
that term as the title (`spec-driven`), and
a page covering several files takes the concept alone (Stores), naming its
files in the job line.
**FAQ is one-liners.** Every FAQ entry is a short answer, a few lines at most, or a
router link to the page that owns the topic. How-to content never lives in the FAQ:
when an answer outgrows a one-liner, it moves to a guide or reference page and the FAQ
entry becomes a pointer.
## Page index: every page's job
Each goal below is the page's `>` blockquote verbatim, so the promise here is the promise
readers see. A page delivers exactly its goal: content that outgrows it means splitting
the page or rewriting the goal in both places, never letting them drift.
### Start: from "what is this?" to your first archived change
| Page | Goal |
|---|---|
| [Overview](start/overview.md) | _TODO: emptied 2026-08-21 for a from-scratch rewrite and pulled from the site (`/docs` redirects to Installation meanwhile); the old goal line was dropped as too weak a pitch. Brief in Notes.md._ |
| [Installation](start/installation.md) | Install the `openspec` CLI on your machine, update it, and uninstall it. |
| [Set up your project](start/setup.md) | Add OpenSpec to a project: run init, see what it wrote, and adjust it. |
| [Quickstart](start/quickstart.md) | Your first change in a new or existing project, from idea to archived. |
### Guides: understand the system, use it well, bring it to your codebase and team
| Page | Goal |
|---|---|
| [Understanding › Concepts](guides/concepts.md) | What the two artifacts are, and how a change describes a diff against current specs. |
| [Using › Explore an idea](guides/explore.md) | Think it through with the agent before you commit to a proposal. |
| [Using › Review the plan](guides/review-the-plan.md) | The two-minute pass that catches wrong turns before they're code. |
| [Using › Apply a change](guides/apply.md) | Run the plan: pacing, context windows, and picking up where you left off. |
| [Using › Change course](guides/change-course.md) | Revise a change in flight, or decide it's cleaner to start fresh. |
| [Adopting › Existing codebases](guides/existing-codebases.md) | Bring OpenSpec to a codebase with a lot of code and no specs: where to start, what to backfill, and how specs grow from there. |
| [Adopting › Teams](guides/teams.md) | Run OpenSpec as a team: what to commit, how a change rides its PR, and when to archive. |
### Customize: make the workflows fit your project
| Page | Goal |
|---|---|
| [Overview](customize/overview.md) | Your options for customizing OpenSpec. |
| [Profiles](customize/profiles.md) | Choose which workflows are installed, and whether they install as skills, commands, or both. |
| [Project configuration](customize/project-config.md) | Make the workflows plan changes the way you want with a few lines in config.yaml. |
| [Schemas](customize/schemas.md) | Change what OpenSpec produces: the artifacts, their order, and their templates. |
### Multi-repo (beta): plan across repository boundaries
| Page | Goal |
|---|---|
| [Stores (beta)](multi-repo/stores.md) | Plan changes that span repositories: one store, many repos. |
| [Worksets (beta)](multi-repo/worksets.md) | Open the store and the repos that use it in one editor window, so your agent sees both. |
### Reference: look it up, exact and complete
| Page | Goal |
|---|---|
| [Skills](reference/skills.md) | Every OpenSpec skill: arguments, what it creates, and what it responds with. |
| [CLI](reference/cli.md) | The `openspec` terminal commands. |
| [Schemas](reference/schemas/index.md) | Every available workflow schema and the artifacts it defines. |
| [Schemas › schema.yaml](reference/schemas/schema-yaml.md) | Every field of a schema definition, for reading or writing one. |
| [Schemas › spec-driven](reference/schemas/spec-driven/index.md) | The default workflow's artifacts: their order, their formats, and the change folder they produce. |
| [Configuration](reference/configuration/index.md) | Every file and setting that changes how OpenSpec behaves, and where each lives. |
| [Configuration › Project configuration (config.yaml)](reference/configuration/config-yaml.md) | Every field of openspec/config.yaml: the schema, context, and rules this project plans with. |
| [Configuration › Change metadata (.openspec.yaml)](reference/configuration/change-metadata.md) | The supported fields and validation rules for the metadata stored with each change. |
| [Configuration › CLI settings (config.json)](reference/configuration/config-json.md) | Every field of config.json: how the openspec CLI behaves on your machine. |
| [Configuration › Environment variables](reference/configuration/environment-variables.md) | Every environment variable OpenSpec reads. |
| [Configuration › Stores](reference/configuration/stores.md) | The files behind multi-repo stores: registry.yaml and store.yaml, and which root a command uses. |
| [Supported tools](reference/supported-tools.md) | Which AI coding tools OpenSpec supports, and each one's command syntax. |
| [Glossary](reference/glossary.md) | Every OpenSpec term, one line each. |
| [Architecture](reference/architecture/index.md) (held back from the site until drafted) | How OPSX is built: internals for the curious. |
| [Architecture › Workflow runs](reference/architecture/workflow-runs.md) | How a workflow run executes, from invocation to written artifacts. |
| [Architecture › Design decisions](reference/architecture/design-decisions.md) | Why OPSX works the way it does. |
### Help: get unstuck (held back from the site until drafted, see Open TODOs)
| Page | Goal |
|---|---|
| [FAQ](help/faq.md) | Short answers to the questions that don't need a page. |
| [Troubleshooting](help/troubleshooting.md) | When OpenSpec doesn't do what you expected: symptoms and their fixes. |
### Legacy: land the old workflow safely (held back from the site until drafted, see Open TODOs)
| Page | Goal |
|---|---|
| [Migrating from the legacy workflow](help/legacy/migration.md) | Moving from the legacy `/openspec:*` commands to OPSX. |
## Old docs
The `docs/` tree is legacy, and the plan is to remove it once docs-lab covers what it
owns. It has become a bit of an AI slop mess, so nothing from it is carried over as text
(see The bar for every page). Until it's removed it stays untouched: fixes land in
docs-lab, never in `docs/`.
[`sources.md`](sources.md) maps every current `docs/` page to its destination here: the
source material while drafting, the redirect list at cutover. Cutover steps are in that
file's [Cutover](sources.md#cutover) section.
## Open TODOs
- Not started: the Architecture pages (`reference/architecture/index.md`,
`workflow-runs.md`, `design-decisions.md`). All three are headings only, so we hid the
group from the site on 2026-08-21 (folder entry commented out in
`website/docs.sync.config.mjs`). The files stay on disk with a WIP comment. Published
pages that link to them (`reference/glossary.md` to the Overview,
`customize/project-config.md` to Workflow runs) fall back to the GitHub source until
the group is re-listed.
- Not started: the Help and Legacy pages (`help/faq.md`, `help/troubleshooting.md`,
`help/legacy/migration.md`). FAQ has one answer and the other two are headings only, so
we hid both sections from the site on 2026-08-21 (commented out in
`website/docs.sync.config.mjs`, same mechanism as Guides). The files stay on disk with
a WIP comment. Published pages that link to them (`start/setup.md` to FAQ,
`reference/glossary.md` to Migration) fall back to the GitHub source until the
sections are re-listed.
- Not started: `start/overview.md` is empty on purpose. We cleared the skeleton
(headings, narrative beats, diagram gallery) on 2026-08-21 to rewrite the landing page
from scratch. The old pitch ("a shared, reviewable plan before code is written")
undersells OpenSpec now that plan mode is everywhere; the rewrite should sell keeping
larger features on track and aligned (teams, git-native, intended vs implemented
behavior, control-loop framing). Brief in `Notes.md` ("Start > Overview"); the diagram
candidates went with the gallery and live in git history. Until the rewrite lands the
page is off the site: its entry is commented out in `website/docs.sync.config.mjs` and
`/docs` redirects to Installation (`website/public/_redirects` plus a fallback in the
docs page route). Restoring it is one uncomment plus removing the two redirects. The
Teach-once rule still applies: the loop appears here as pitch only.
- Product feedback, not a docs task: spec-driven's design `instruction` lists six
sections (including Migration Plan and Open Questions) but
`schemas/spec-driven/templates/design.md` carries only four headers. The docs show
both verbatim; the mismatch belongs upstream. Noted 2026-08-14 while consolidating
the spec-driven page.
- Product feedback, not a docs task: `openspec store setup --remote` writes the URL
into `store.yaml` but never configures a git `origin`, so "setup --remote, then
`git push -u origin main`" fails as written; the Stores page shows `git remote add`
instead. The pasteable missing-store fix in `openspec doctor` is powered by
`references:` remotes, not `store.yaml`. Noted 2026-08-21 while porting the Stores
page.
- Style guide follow-up (`.agents/skills/write-openspec-docs/writing.md`), from the
Stores page's review rounds, 2026-08-21: never use a term the page hasn't shown
(say "the `store:` line", not "the pointer"; define by showing the artifact first);
when behavior depends on the reader's starting state, enumerate the states and walk
each to its outcome; sentence subjects are you, OpenSpec, or your agent, never an
implementation unit ("the resolver picks") or a class of things ("store-only
projects make..."); when a defined term is reused a section later, re-gloss it in
one parenthetical at the point of use.
- Fence convention follow-up, 2026-08-21: the Stores page puts commands in `bash`
fences with a one-line `#` comment and OpenSpec output in a separate `yaml` fence.
`customize/schemas.md` still uses `console` fences with `$` prompts (lines 78, 114,
137, 145; prompts at 24 and 115); the style guide should name the convention and
that page should adopt it.
- Monorepo: message-map row 37 is still a Gap. "Packages treated as separate repos"
may land on the Stores page later; not part of the current page.
- `reference/cli.md` is fully drafted: the command table plus one section per real
command, facts captured from working-tree runs (2026-08-11). The `delivery` key that
start/setup.md's "Skills, commands, or both" section sets appears there only as
command output; its field-level home, `reference/configuration/config-json.md`, is
drafted (2026-08-14).
- Telemetry is undocumented. `OPENSPEC_TELEMETRY=0` appears nowhere in the tree; the
Deno install command grants `--allow-net=edge.openspec.dev` with no explanation (the
telemetry gloss was deliberately pulled pending a real home). The home now exists:
write `reference/configuration/environment-variables.md` (the env var, what's
collected, the opt-out, the CI auto-disable), then have the Deno section link to it
to explain the flag. Noted 2026-08-07; home settled 2026-08-10.
- Product feedback, not a docs task: init doesn't say when the global profile changed what
it wrote. A machine with `profile: custom` silently installs a different workflow set
than a stock machine, and nothing in the init output names the profile that shaped it.
Noted 2026-08-05 while verifying installation.md; track upstream, don't paper over in prose.
- Product feedback, not a docs task: drop the sync-specs skill from the default set; its
job reads as reference content, not a workflow, and it pads the skill list every reader
scans. Noted 2026-08-08 while writing start/setup.md's workflow tree.
- Product feedback, not a docs task: make the shared `.agents/` folder the default install
target for every tool, with tool-specific folders (`.claude/`, ...) the exception. The
docs already prefer `.agents/` in examples; the product should match. Noted 2026-08-08.
- `help/troubleshooting.md`'s skeleton has no section for install-time failures
(`command not found`, wrong Node version, PATH). Old `docs/troubleshooting.md` covered
them; `start/installation.md` carries caveats inline but there is no symptom-to-fix
home. Add a section or an installation.md anchor. Noted 2026-08-10 during the old-docs
message audit.
- Missing guide: the iterative flow. new/continue/fast-forward have no owner for the
judgment: what the flow is, when to pick it over propose, and ff vs continue. Old
`docs/workflows.md` covered it (Two Modes, When to Use What); `sources.md` routes that
page's mechanics to `guides/apply.md` and contracts to `reference/skills.md`, so the
choice itself landed nowhere. Likely a Guides › Using page slotted between Explore and
Review the plan, with a pointer to `customize/profiles.md` (the skills are optional
workflows outside the core set). Salvage only the ff-vs-continue rule of thumb; the rest of
workflows.md is unverified. Noted 2026-08-11. Related: message-map row 29 words
apply.md's pacing question as drafting-time pacing, the same creation-stage choice;
fix that row's wording or owner when this guide lands. Noted 2026-08-14.
- Missing guide: working with git. OpenSpec never touches git, so every git decision
lands on the reader with no page to answer it: do you branch before or after propose,
does a task get its own commit, what goes in the PR, where does the archive commit
land. `guides/teams.md` owns the archive-vs-PR ordering; the rest is unowned. Likely a
`guides/` file in the Adoption group. Noted 2026-08-08.
- `customize/skills.md` is parked: the skeleton stays on disk but is out of the page
index, the sidebar, and the sync config. Editing installed skill prompts has no good
answer yet (`openspec update` overwrites edits); the message map keeps the question as
a Gap. Revive when the product has a real story for surviving updates. Parked 2026-08-14.
- `guides/examples.md` is parked: the skeleton stays on disk but is out of the page
index, the sidebar, and the sync config. Contrived examples teach the wrong lesson for
this product; revive the page when real archived changes from actual usage can fill it.
The content plan (weak-vs-reviewed pairs, archived-changes gallery) is in the file's
comment. Parked 2026-08-11.
- Product feedback, not a docs task: "expanded" survives in product strings and the
update workflow is unlabeled in the picker. The only stored profile values are core
and custom, but `src/core/templates/workflows/update-change.ts` says "expanded-profile
workflow", and `WORKFLOW_PROMPT_META` (`src/commands/config.ts`) has no `update` entry,
so the `openspec config` workflow picker renders a core workflow as raw `update` /
"Workflow: update". Docs standardized on core/custom with "expand the set" as a verb
(2026-08-12). Noted 2026-08-12 during the glossary product sweep.
- Product feedback, not a docs task: converge on skills only, soon. A workflow's skill and
command are the same instructions, Claude Code has already merged commands into skills
upstream, and setup spends a whole subsection explaining why two forms exist. Every page
gets simpler when commands go. Noted 2026-08-08 while writing start/setup.md.
- Website QOL backlog, site build not prose: i18n; AI search layered on the stock keyword
search (an ask-the-docs answer box, not just matching); proper light/dark themes that
carry the Survey palette (DESIGN.md tokens) into both modes instead of a stock dark
theme. Candidates to bundle in the same pass: `llms.txt` plus a per-page "copy as
Markdown" button so agents can ingest pages, copy buttons on code blocks, and
"edit this page on GitHub" links. Noted 2026-08-11.
+32
View File
@@ -0,0 +1,32 @@
# Overview
> Your options for customizing OpenSpec.
OpenSpec supports multiple customization options. This page shows what each one changes and when to use it.
## What you can customize
| Option | What it changes | Use it when |
|---|---|---|
| [Profiles](profiles.md) | Which workflows are installed, and whether as skills, commands, or both | You want additional workflows and working patterns, or to remove workflows you don't need |
| [Project configuration](project-config.md) | The instructions injected into every workflow run: context, rules, and operation guidance (`config.yaml`) | You want changes planned your way, like tasks always including Playwright tests |
| [Schemas](schemas.md) | What OpenSpec produces: the artifacts, their order, and their templates | Changes should produce different planning files, sections, or formats |
## Not sure which to use?
Config and schemas are two levels of customization. Pick by how hands-on you want to get:
- **Start with [project configuration](project-config.md)**: it's lighter, and for most projects it's enough. You keep the standard artifacts and add your own context and rules on top.
- **Fork a [schema](schemas.md) when adding isn't enough**: config only adds on top of the core workflow. It can add a rule like "tasks always include tests," but it can't drop the design doc or rename a file. That's schema territory. Forking gives you your own copy to edit.
*"Fork" here means the `openspec schema fork` command, not forking a git repo. [Schemas](schemas.md) has the details.*
```mermaid
flowchart LR
a["The workflows should know my stack and conventions"] --> config
b["One artifact needs an extra rule, like tasks always including tests"] --> config
c["Different artifacts, file names, or document structure"] --> schema
d["The built-in instructions say things my team does differently"] --> schema
config["Project configuration<br/>(config.yaml)"]
schema["Fork a schema<br/>(openspec schema fork)"]
```
+88
View File
@@ -0,0 +1,88 @@
# Profiles
> Choose which workflows are installed, and whether they install as skills, commands, or both.
A profile is your preference for which OpenSpec workflows (the [skills and commands](../start/setup.md#the-workflow-files-skills-and-commands) in your AI tool) are installed across your machine. The default profile is `core`. Include or exclude workflows and your selection is saved as the `custom` profile.
## The core set
The `core` profile installs six workflows, covering the whole loop from idea to archive:
| Workflow | What it's for |
|---|---|
| [`explore`](../reference/skills.md#openspec-explore) | Think through an idea before it becomes a change proposal |
| [`propose`](../reference/skills.md#openspec-propose) | Create a change proposal and generate all its planning artifacts in one step |
| [`apply`](../reference/skills.md#openspec-apply-change) | Implement a change proposal's tasks |
| [`update`](../reference/skills.md#openspec-update-change) | Revise a change proposal's existing planning artifacts |
| [`sync`](../reference/skills.md#openspec-sync-specs) | Merge a change proposal's spec updates into `specs/` without archiving it |
| [`archive`](../reference/skills.md#openspec-archive-change) | Move a finished change proposal to the archive |
Each links to its full contract: arguments, what it creates, and what it responds with.
## Expanding the set: optional workflows
Six more workflows are available beyond the core set. Three of them (`new`, `continue`, `ff`) create a change proposal artifact by artifact, instead of all at once like `propose`.
| Workflow | What it's for |
|---|---|
| [`new`](../reference/skills.md#openspec-new-change) | Start a change proposal as an empty scaffold |
| [`continue`](../reference/skills.md#openspec-continue-change) | Create the next planning artifact in a change proposal, one at a time |
| [`ff`](../reference/skills.md#openspec-ff-change) | Create a change proposal and every planning artifact implementation needs, in one pass |
| [`verify`](../reference/skills.md#openspec-verify-change) | Check that the implementation matches the change proposal's artifacts |
| [`bulk-archive`](../reference/skills.md#openspec-bulk-archive-change) | Archive several change proposals at once |
| [`onboard`](../reference/skills.md#openspec-onboard) | Learn the workflow by doing one real change proposal end to end |
To change the set, run the interactive picker:
```bash
openspec config profile
```
The picker asks what to configure ([delivery](#delivery-skills-commands-or-both), workflows, or both), then lists all twelve workflows as checkboxes, with the installed ones checked. Any selection that isn't exactly the core six is saved as the `custom` profile, so you can also uncheck core workflows you don't use.
## Delivery: skills, commands, or both
Delivery is a profile setting that lets you choose to have only skills or only commands installed. The default is `both`. [Set up your project](../start/setup.md#the-workflow-files-skills-and-commands) explains the two forms and why both exist. The field's exact contract is in [CLI settings (config.json)](../reference/configuration/config-json.md#delivery).
Two ways to change it:
**Interactively**: run `openspec config profile` and choose "Delivery only". Here's switching to skills only:
```
Current profile settings
Delivery: both
? What do you want to configure? Delivery only
? Delivery mode (how workflows are installed): Skills only
Config changes:
delivery: both -> skills
? Apply changes to this project now? (Y/n) y
```
**Directly**: one command, no prompts:
```bash
openspec config set delivery skills # or: both, commands
```
Delivery never changes the profile name. `core` and `custom` describe the workflow set only, and switching back to `core` keeps your delivery setting.
## Switching profiles
Switching is two steps: change the profile on your machine, then update each project to apply it.
1. Change the profile:
```bash
openspec config profile # interactive
openspec config profile core # reset to the core six (keeps delivery)
```
2. Run the update in each project you work in:
```bash
openspec update
```
When your current directory is an existing OpenSpec project, the interactive flow offers to run step 2 there for you.
+122
View File
@@ -0,0 +1,122 @@
# Project configuration
> Make the workflows plan changes the way you want with a few lines in config.yaml.
`openspec/config.yaml` tells the workflows how you want changes planned.
For example, the following configuration updates the creation rules for the [tasks.md](../reference/schemas/spec-driven/index.md) artifact:
```yaml
rules:
tasks:
- End every task with a commit
```
When the agent runs, it pulls from these rules and ensures every task ends with a commit step.
Keep rules short. Everything here lands in the agent's context, and verbose rules can make the output worse.
## How it works
config.yaml holds instructions the agent receives when it creates artifacts or works through the workflow.
Here's what happens on every run:
1. You run a workflow (e.g. `/openspec-propose`).
2. The agent calls the [`openspec instructions`](../reference/cli.md) command.
3. The command reads your context and rules from config.yaml.
4. OpenSpec's built-in instructions and your customizations are combined into a single prompt for the agent.
5. The agent follows that prompt to write the artifact.
For example, with a `context` field and the rule from the top of this page, here's what [`openspec instructions`](../reference/cli.md) returns for tasks.md (trimmed and annotated):
```xml
<artifact id="tasks" change="add-dark-mode" schema="spec-driven">
<!-- From your config.yaml: context -->
<project_context>
Tech stack: TypeScript, Node.js
Domain: e-commerce platform
</project_context>
<!-- From your config.yaml: rules for tasks -->
<rules>
- End every task with a commit
</rules>
<!-- From OpenSpec: the built-in guidance -->
<instruction>
...how to write a good tasks.md...
</instruction>
<template>
...the tasks.md structure to fill in...
</template>
</artifact>
```
Your config arrives first, then OpenSpec's built-in instruction and template. Rules add to the built-ins and never replace them. Edits to config.yaml reach the agent on the next run.
## The fields
Three fields shape what the agent receives. Each field's exact contract (types, limits, validation) is in [Project configuration (config.yaml)](../reference/configuration/config-yaml.md).
| Field | What it does | Injected into |
|---|---|---|
| `context` | Instructions the agent always receives | Everything: every artifact, `apply`, `archive` |
| `rules` | Extra instructions for one artifact | Only that artifact's creation |
| `operations` | Guidance for how a workflow step is carried out | Only `apply` and `archive` |
config.yaml's other fields (`schema`, `store`, `references`) select which schema and which OpenSpec root a project uses. The contract page covers them.
The last column is exact, so a field reaches only the steps listed there. In particular, `verify` never receives `rules`. It checks the implementation against the artifacts as written.
### context
`context` is what the agent should know up front when planning a change, whether it's creating an artifact, applying tasks, or archiving:
```yaml
context: |
We ship cross-platform; designs and tasks must cover Windows, macOS, and Linux
Tech stack: TypeScript, Node.js, Commander.js
We use conventional commits
```
This is planning context, not project documentation. Add a fact when it should shape every plan, like the cross-platform line above. Leave out anything the agent can learn by reading the code.
**Another language**: because context reaches every artifact, it's also how you change the output language. One line, like `Write all artifacts in Spanish.`, switches every proposal, spec, and tasks file the workflows write.
### rules
`rules` attach to one artifact, keyed by artifact id. Each line is added to that artifact's built-in guidance:
```yaml
rules:
proposal:
- Keep proposals under 500 words
tasks:
- Every UI task includes a Playwright test
```
Proposals now stay short and tasks.md always plans browser tests. Every other artifact is untouched.
### operations
`operations` guides how the agent carries out `apply` and `archive`, rather than what artifacts say:
```yaml
operations:
apply:
guidance:
- Run the linter before marking a task complete
archive:
guidance:
- Summarize what shipped before archiving
```
During apply, the agent lints as it completes tasks. During archive, it closes with a summary.
## When config.yaml isn't enough
Config adds instructions on top of the standard workflow, but it can't change which artifacts exist or how they're structured. When you want that level of control, or rules aren't steering behavior consistently, [fork a schema](schemas.md).
+164
View File
@@ -0,0 +1,164 @@
# Schemas
> Change what OpenSpec produces: the artifacts, their order, and their templates.
A schema defines what a change proposal produces: which artifacts, in what order, from which templates. For example, [spec-driven](../reference/schemas/spec-driven/index.md), the default bundled schema, produces these four in roughly this order, each building on what came before:
```
proposal → specs → design → tasks
```
Fork a schema when you want these to be different documents, whether that means fewer of them, different names, or a different structure.
## Where schemas live
OpenSpec looks for a schema in three places, in order, and uses the first one it finds:
1. **Your project**: `openspec/schemas/`, committed with the repo so your whole team gets it.
2. **Your machine**: `~/.local/share/openspec/schemas` on macOS and Linux (or under `$XDG_DATA_HOME` if you set it), or `%LOCALAPPDATA%\openspec\schemas` on Windows. Schemas here are available in every project you work in.
3. **The package**: the built-ins, like `spec-driven`, ship inside openspec itself.
The same name can exist in more than one place, and the more specific location wins. `openspec schema which` shows which copy is in use:
```
$ openspec schema which spec-driven
Schema: spec-driven
Source: project
Path: /your-project/openspec/schemas/spec-driven
Shadows:
package: .../openspec/schemas/spec-driven
```
## What's in a schema
A schema is defined by a folder of plain files: one schema.yaml that declares the artifacts, and a template for each of them. Here's the built-in `spec-driven`:
```
spec-driven/
├── schema.yaml
└── templates/
├── proposal.md
├── spec.md
├── design.md
└── tasks.md
```
- **schema.yaml**: declares each artifact, the file it generates, the template it starts from, what it requires first, and the instruction the agent receives when creating it. Every field's contract is in [schema.yaml](../reference/schemas/schema-yaml.md).
- **templates/**: one markdown skeleton per artifact, which the agent fills in.
Here's the tasks artifact's entry in schema.yaml, trimmed:
```yaml
artifacts:
- id: tasks
generates: tasks.md
description: Implementation checklist with trackable tasks
template: tasks.md
instruction: |
...what the agent is told when creating tasks.md...
requires:
- specs
- design
```
The built-in schemas ship inside the openspec package, so you never edit them in place. You get your own copy by forking.
## Creating your own custom schema
There are two ways to get your own schema:
1. **Fork an existing schema** and edit your copy. Start here when an existing schema is close to what you want, because everything in it already works.
2. **Start from scratch** when none of them fit, scaffolding an empty schema with `openspec schema init`.
### Fork an existing schema
1. Fork the schema you want to start from, running from your project root:
```console
$ openspec schema fork spec-driven
Note: Schema commands are experimental and may change.
✔ Forked 'spec-driven' to 'spec-driven-custom'
Source: .../openspec/schemas/spec-driven (package)
Destination: /your-project/openspec/schemas/spec-driven-custom
```
Pass a second argument to pick the name (`openspec schema fork spec-driven team-flow`). Names are kebab-case.
2. Edit the copy: schema.yaml and the templates. [Editing your fork](#editing-your-fork) covers what to change.
3. Validate it:
```bash
openspec schema validate spec-driven-custom
```
This is the one command that catches a broken schema (missing templates, bad YAML, dependency cycles) before you're in the middle of a change.
4. Point your project at it in openspec/config.yaml. This step is yours to do because fork leaves config.yaml untouched:
```yaml
schema: spec-driven-custom
```
5. New change proposals now follow your schema. Changes created earlier keep the schema they started with.
To replace the default everywhere without touching config.yaml, fork to the same name: `openspec schema fork spec-driven spec-driven`. Your project's copy then shadows the built-in, as [Where schemas live](#where-schemas-live) explains.
### Start from scratch
`openspec schema init` scaffolds a new schema instead of copying one:
```console
$ openspec schema init lite --description "Lite flow" --artifacts proposal,tasks
✔ Created schema 'lite'
Schema created at: /your-project/openspec/schemas/lite
Artifacts: proposal, tasks
```
The scaffold is bare. Artifacts come from the built-in four ids only, and the generated templates carry no instructions, so the agent gets less guidance until you write your own. From there the fork steps apply unchanged: validate it, then point config.yaml at it.
## Editing your fork
A fork has two kinds of files to edit:
- **templates/** change the skeleton of each document. Add a section to the tasks template and every new tasks.md starts with it. Keep the `#` title on the first line: the artifact inherits it, so every generated file opens as a titled document.
- **schema.yaml** changes the workflow itself: which artifacts exist, what each one requires first, and the instruction the agent gets when creating it.
For example, to drop the design document for a leaner flow:
1. Delete the `design` entry from schema.yaml.
2. Remove `design` from the `requires` list of `tasks`.
3. Validate:
```console
$ openspec schema validate spec-driven-custom
✓ Schema 'spec-driven-custom' is valid
```
Skip step 2 and validate catches it:
```console
✗ Schema 'spec-driven-custom' has errors:
error: Invalid dependency reference in artifact 'tasks': 'design' does not exist
```
Validate after every hand-edit. A broken schema otherwise surfaces in the middle of a change, when a workflow asks for a file that isn't there. Like config.yaml, schema edits reach the agent on the next run.
## A fork is a snapshot
`openspec update` refreshes the installed skills and commands, and it never touches `openspec/schemas/`. Your fork keeps working exactly as you left it, which also means it stops receiving improvements when the built-in schema evolves. To pick those up later, fork the built-in again under a new name and port the differences across.
## Sharing schemas
Sharing a schema means copying its folder.
- **With your team**: commit `openspec/schemas/` and everyone on the repo uses it.
- **Across your projects**: put the folder in the user-level directory from [Where schemas live](#where-schemas-live).
- **From the community**: the [community catalog](https://github.com/Fission-AI/OpenSpec/blob/main/docs/customization.md#community-schemas) lists shared schemas. Copy one into `openspec/schemas/<name>` and it works like your own.
We're working on a schema registry, public and private, so schemas can be installed by name instead of copied by hand.
+14
View File
@@ -0,0 +1,14 @@
# Customizing skills
> Edit the installed skill prompts directly: what you can change, and what update overwrites.
<!-- PARKED 2026-08-14 (README TODO): out of the page index, sidebar, and sync config.
No good answer yet for edits surviving `openspec update`; the message map keeps
"How should a user edit the installed skill prompts?" as a Gap. Skeleton below is
the shape to revive. The honest page for the update-clobber caveat. -->
## What you can change
## What openspec update overwrites
## Supported alternatives
+11
View File
@@ -0,0 +1,11 @@
# Apply a change
> Run the plan: pacing, context windows, and picking up where you left off.
<!-- Skeleton: headings only. Promoted out of examples.md in review round 2. -->
## Task by task or all at once
## Managing the context window
## Continue and fast-forward
+11
View File
@@ -0,0 +1,11 @@
# Change course
> Revise a change in flight, or decide it's cleaner to start fresh.
<!-- Skeleton: headings only. -->
## Update or start fresh?
## Revising artifacts with openspec-update-change
## Advanced: revising mid-implementation
+9
View File
@@ -0,0 +1,9 @@
# Concepts
> What the two artifacts are, and how a change describes a diff against current specs.
<!-- Skeleton: headings only. Narrowed in review round 3 (dedup pass). The quickstart's archive file-steps absorbed the loop mechanics this page once planned to own, so the loop section is gone: the quickstart is the loop's only teacher, and core-vs-optional moved to customize/profiles.md. The fixed-vs-shapeable section was cut entirely (customize/overview.md dropped that framing in review, 2026-08-14: it opens straight on the options and never says what can't be changed). What remains is the one explanation this page owns: the artifacts. Specs absorbs "What a spec is"; Changes owns the delta concept: one worked delta block (ADDED/MODIFIED/REMOVED, from docs/concepts.md's core) showing a change as a diff against current specs; the file-format rules live in reference/schemas/spec-driven/index.md (Delta specs section). Disk paths appear inline with the concept that owns them (specs/ under Specs, changes/ under Changes), never as a layout section. Close with link lines: quickstart for the loop, customize/overview for the customization options. When to archive relative to a branch or PR stays in guides/teams.md. -->
## Specs: the system as built
## Changes: a folder of deltas
+16
View File
@@ -0,0 +1,16 @@
# Examples
> See what a good change looks like: real drafts, what review caught, and the fixes.
<!-- Skeleton: headings only. PARKED 2026-08-11: out of the README index and sync config. Contrived examples teach the wrong lesson for this product; the page waits for real archived changes from actual usage. Plan when revived: weak-vs-reviewed pairs (4) plus an archived-changes gallery. Earlier decisions still hold: slimmed in review round 2, purely example pairs; apply technique lives in guides/apply.md. -->
## Adding a feature to an undocumented codebase
## Changing an API without breaking clients
## A behavior-preserving refactor
## A database migration with rollback
## Real archived changes
+13
View File
@@ -0,0 +1,13 @@
# Existing codebases
> Bring OpenSpec to a codebase with a lot of code and no specs: where to start, what to backfill, and how specs grow from there.
<!-- Skeleton: headings only. Rescoped 2026-08-11: the page is the adoption guide for legacy codebases (a lot of code, no specs), not just the backfill task; "Specs for existing code" is now one section. The opening section owns "you don't need specs first": the quickstart runs on an existing repo as-is, and specs accumulate through changes. Source: docs/existing-projects.md. When prose lands, reshape to the Guides skeleton (one-line what, quickstart link, 80% path, Advanced). -->
## Start with a change
## Specs for existing code
## Working from a PRD
## Organizing specs as they grow
+17
View File
@@ -0,0 +1,17 @@
# Explore an idea
> Think it through with the agent before you commit to a proposal.
<!-- Skeleton: headings only. -->
## When to explore first
## A real explore session
## From exploration to proposal
## Advanced
### Exploring mid-change
### Challenging a draft plan
+15
View File
@@ -0,0 +1,15 @@
# Review the plan
> The two-minute pass that catches wrong turns before they're code.
<!-- Skeleton: headings only. -->
## The two-minute pass
## What good requirements look like
## What good scenarios look like
## Pushing back
## Advanced: verify after apply
+17
View File
@@ -0,0 +1,17 @@
# Teams
> Run OpenSpec as a team: what to commit, how a change rides its PR, and when to archive.
<!-- Skeleton: headings only. Retitled from "Teams & parallel changes" 2026-08-11: the page's job is running OpenSpec in a team; parallel changes stays as the touching-one-spec section, and the general parallel-changes answer (solo included) remains an unowned gap in message-map.md. Expanded in review: this page owns the git/PR conventions (the quickstart's archive step shows the mechanism; this page owns timing). Source for all three new sections: docs/team-workflow.md. "A change is a branch and a PR": one branch carries the change folder and the code, so the PR shows the plan and the diff together. "Review in pull requests": reviewer order proposal → spec delta → code; approach disagreements land on the proposal, not across 300 lines of diff. "When to archive": after the PR merges (recommended, specs/ only advances with shipped work) vs inside the PR (simpler, noisier diff); pick one and be consistent. Proposal-first PRs (plan reviewed before implementation starts) go under Advanced. -->
## Checking openspec/ into git
## A change is a branch and a PR
## Review in pull requests
## When to archive
## Parallel changes touching one spec
## Advanced: conventions for larger teams
+24
View File
@@ -0,0 +1,24 @@
# FAQ
> Short answers to the questions that don't need a page.
<!-- WIP, on the todo list: this page is not written yet and is held back from the
site (its section is commented out in website/docs.sync.config.mjs, 2026-08-21). The
file stays so the structure and inbound links survive; re-list it in the sync config
once the prose lands. -->
<!-- Every entry is a one-liner: a short answer or a router link to the page that owns the topic; how-to content never lives here (README's "FAQ is one-liners" rule). Update/uninstall moved to installation.md; the skills-missing fix and Getting help live in troubleshooting.md; the git question is a one-line yes routing to guides/teams.md, which owns the team conventions. Unfilled headings are skeletons. -->
## Should openspec/ be checked into git?
## What runs in the terminal, and what in chat?
## Does OpenSpec work with my tool?
If it has a row in the [support matrix](../reference/supported-tools.md), yes.
Pick its id at init. If it isn't listed but reads the shared `.agents/skills/`
folder, pick **Other / Universal** (`--tools agents`), covered by the support
matrix's Other / Universal section. If neither, request it in the
[OpenSpec repo](https://github.com/Fission-AI/OpenSpec/issues).
## Where did the old /openspec:* commands go?
+18
View File
@@ -0,0 +1,18 @@
# Migrating from the legacy workflow
> Moving from the legacy `/openspec:*` commands to OPSX.
<!-- WIP, on the todo list: this page is not written yet and is held back from the
site (its section is commented out in website/docs.sync.config.mjs, 2026-08-21). The
file stays so the structure and inbound links survive; re-list it in the sync config
once the prose lands. -->
<!-- Skeleton: headings only. -->
## What changed and why
## Command mapping
## Migrating a project
## Behavior differences
+20
View File
@@ -0,0 +1,20 @@
# Troubleshooting
> When OpenSpec doesn't do what you expected: symptoms and their fixes.
<!-- WIP, on the todo list: this page is not written yet and is held back from the
site (its section is commented out in website/docs.sync.config.mjs, 2026-08-21). The
file stays so the structure and inbound links survive; re-list it in the sync config
once the prose lands. -->
<!-- Skeleton: headings only. Canonical home for symptom-to-fix, including the skills-missing checklist. -->
## Skills don't appear in chat
## The agent ignores the workflow
## Validation failures
## Sync and archive issues
## Getting help
+65
View File
@@ -0,0 +1,65 @@
# Message map: the questions the docs must answer, and where
The [README](README.md) index runs page to job. This file runs the other way: one flat
list of the questions we need the docs to answer, each pointing at the group and page
that owns the answer. Status says whether that answer exists yet: **Answered** (the
owning page's prose has landed), **Skeleton** (owner assigned, page is headings only),
**Gap** (no owner), **Off-site** (answered outside these docs by decision). Flip a row
to Answered when its page's prose lands. Rows follow the sidebar order of the owning
page; gaps sit where their proposed home would fall, and off-site rows go last. Keep
rows coarse (question to page, never sentence to section) so this stays cheap to
maintain.
| Question | Answered by | Status |
|---|---|---|
| How do we pitch the core idea (keeping larger features on track and aligned, not just a plan before code)? | [Start › Overview](start/overview.md) (emptied 2026-08-21 for a from-scratch rewrite and pulled from the site until then; brief in Notes.md) | Skeleton |
| How does someone decide OpenSpec is worth their time? | [Start › Overview](start/overview.md) (emptied 2026-08-21, see row above) | Skeleton |
| How should a user install the CLI, update it, uninstall it? | [Start › Installation](start/installation.md) | Answered |
| How can a user hand install and setup to their AI assistant? | [Start › Installation](start/installation.md), the install.md prompt | Answered |
| How should a user add OpenSpec to their repo? | [Start › Set up your project](start/setup.md) | Answered |
| How do the workflows get into a user's tool, and why skills and commands both? | [Start › Set up your project](start/setup.md) | Answered |
| How do we teach the loop: propose, review, apply, archive? | [Start › Quickstart](start/quickstart.md) | Answered |
| How should a user run their first change end to end? | [Start › Quickstart](start/quickstart.md) | Answered |
| How does a user know which prompts go in the AI chat and which commands in the terminal? | [Start › Quickstart](start/quickstart.md) inline with each step, then [Help › FAQ](help/faq.md) | Answered |
| How do we explain what specs and changes are? | [Guides › Understanding › Concepts](guides/concepts.md) | Skeleton |
| How should a user think through an idea before proposing? | [Guides › Using › Explore an idea](guides/explore.md) | Skeleton |
| How should a user review a plan? | [Guides › Using › Review the plan](guides/review-the-plan.md) | Skeleton |
| How does a user check the implementation matches the plan before archiving? | [Guides › Using › Review the plan](guides/review-the-plan.md), the verify pass | Skeleton |
| How should a user run a plan across sessions and context limits? | [Guides › Using › Apply a change](guides/apply.md) | Skeleton |
| How should a user pace the plan: draft everything at once, or artifact by artifact? | [Guides › Using › Apply a change](guides/apply.md), continue and fast-forward | Skeleton |
| How do we explain the standard flow (propose drafts every artifact in one step) vs the iterative flow (new creates the change, continue drafts the next artifact, fast-forward catches up)? | [Start › Quickstart](start/quickstart.md) teaches only the standard flow; [Guides › Using › Apply a change](guides/apply.md) owns pacing once a change exists; [Reference › Skills](reference/skills.md) holds the new/continue/ff contracts; [Customize › Profiles](customize/profiles.md) covers installing them; a [README](README.md) TODO proposes a Using guide | Gap |
| How should a user change direction mid-change, or bail out? | [Guides › Using › Change course](guides/change-course.md) | Skeleton |
| How should a team run OpenSpec together? | [Guides › Adopting › Teams](guides/teams.md) | Skeleton |
| How should a user work on several changes at once? | [Guides › Adopting › Teams](guides/teams.md) owns the touching-one-spec collision case; the general answer (solo included, not just teams) has no owner yet | Gap |
| How should a user handle git across the loop: branching, commits, PRs? | Only archive-vs-PR ordering is owned, by [Guides › Adopting › Teams](guides/teams.md); README TODO proposes a guide | Gap |
| What does a good change look like? | `guides/examples.md` is parked until real archived changes can fill it (README TODO); no published owner | Gap |
| How should a user adopt OpenSpec on code that already exists? | [Guides › Adopting › Existing codebases](guides/existing-codebases.md) | Skeleton |
| How should a user run OpenSpec in a monorepo? | Legacy `docs/existing-projects.md` owned it (one `openspec/` at the repo root, domains map to packages); likely home is [Guides › Adopting › Existing codebases](guides/existing-codebases.md), with [Multi-repo › Stores](multi-repo/stores.md) taking packages treated as separate repos | Gap |
| How do we explain what's customizable in OpenSpec? | [Customize › Overview](customize/overview.md) | Answered |
| How does a user pick the right customization level, and when should they escalate from config to schemas? | [Customize › Overview](customize/overview.md), the "Not sure which to use?" section | Answered |
| How should a user choose which workflows are installed? | [Customize › Profiles](customize/profiles.md) | Answered |
| How does a user switch to skills only or commands only? | [Customize › Profiles](customize/profiles.md), Delivery section; [Start › Set up your project](start/setup.md) owns why both forms exist | Answered |
| How does a user make the workflows plan changes their way: context, rules, and guidance? | [Customize › Project configuration](customize/project-config.md) | Answered |
| How does a user get artifacts written in a language other than English? | [Customize › Project configuration](customize/project-config.md), the context section's "Another language" note | Answered |
| How should a user change what OpenSpec produces? | [Customize › Schemas](customize/schemas.md), with the fork walkthrough in "Creating your own custom schema" | Answered |
| How should a user edit the installed skill prompts? | No owner: `customize/skills.md` is parked (README TODO) until there's a good answer to `openspec update` overwriting edits | Gap |
| How should a user run OpenSpec across multiple repos? | [Multi-repo › Stores](multi-repo/stores.md); [Start › Set up your project](start/setup.md) routes there from "Pick where OpenSpec lives" | Answered |
| How should a user plan a change that spans repos? | [Multi-repo › Stores](multi-repo/stores.md) | Answered |
| What does each skill do, and when should a user reach for it? | [Reference › Skills](reference/skills.md) | Answered |
| Where does a user look up a terminal command? | [Reference › CLI](reference/cli.md) | Answered |
| How does a user learn what telemetry is collected, and opt out? | [Reference › Configuration › Environment variables](reference/configuration/environment-variables.md) owns the facts (was a README-TODO gap); [Help › FAQ](help/faq.md) routes searchers there | Skeleton |
| Where does a user look up an artifact's format, or a schema definition's fields? | [Reference › Schemas](reference/schemas/index.md) | Answered |
| Where does a user look up a setting or a file that changes OpenSpec's behavior? | [Reference › Configuration](reference/configuration/index.md) | Answered |
| Which openspec/ tree does a command operate on? | [Reference › Configuration › Stores](reference/configuration/stores.md) owns the whole resolution ladder, including the everyday case (nearest openspec/ wins); readers reach it from the Stores row of the [Configuration overview](reference/configuration/index.md) map | Skeleton |
| How should a user run a change with no spec impact, or retire a capability outright? | [Reference › Configuration › Change metadata](reference/configuration/change-metadata.md) owns the `skip_specs` and `retire_capabilities` contracts; [Reference › Schemas › spec-driven](reference/schemas/spec-driven/index.md), Delta specs section, owns their effect on deltas and archive; neither half has a guide owner | Gap |
| What is an initiative, and how does a change join one? | No owner: the `initiative` field's contract sits on [Reference › Configuration › Change metadata](reference/configuration/change-metadata.md), but no page teaches initiatives (multi-repo has only Stores) | Gap |
| What is a workset, and how does a user open one in their editor? | [Multi-repo › Worksets](multi-repo/worksets.md); the `openers` field's contract stays on [Reference › Configuration › CLI settings](reference/configuration/config-json.md) | Answered |
| Which AI tools work, and what's each one's syntax? | [Reference › Supported tools](reference/supported-tools.md) | Answered |
| My tool isn't listed, can I still use OpenSpec? | [Help › FAQ](help/faq.md) routes: the shared `.agents` target or an issue; [Reference › Supported tools](reference/supported-tools.md), Per-tool notes, holds the shared target's contract | Answered |
| Where does a user look up a term? | [Reference › Glossary](reference/glossary.md) | Answered |
| How is OPSX built? | [Reference › Architecture](reference/architecture/index.md) | Skeleton |
| How do we explain that the workflow is fluid, actions not phases? | [Start › Overview](start/overview.md) will carry the pitch once rewritten (the "shared map, not a plan up front" framing was in the cleared skeleton); [sources.md](sources.md) routes opsx.md's explanation to [Guides › Understanding › Concepts](guides/concepts.md), but that page narrowed to artifacts only in review round 3; likely home is Guides › Understanding, widening [Concepts](guides/concepts.md) or adding a sibling page, with [Reference › Architecture › Design decisions](reference/architecture/design-decisions.md) keeping the why | Gap |
| What should a user do when OpenSpec doesn't do what they expected? | [Help › Troubleshooting](help/troubleshooting.md), then [Help › FAQ](help/faq.md) | Skeleton |
| Where does a user go for help or to report a bug? | [Help › Troubleshooting](help/troubleshooting.md), Getting help | Skeleton |
| How should a user move off the legacy `/openspec:*` commands? | [Help › Migration](help/legacy/migration.md) | Skeleton |
| How does a script or CI drive the CLI programmatically? | Off-site by decision: repo-side contributor docs, per [sources.md](sources.md) | Off-site |
+341
View File
@@ -0,0 +1,341 @@
# Stores (beta)
> Plan changes that span repositories: one store, many repos.
OpenSpec normally lives inside one repo: an `openspec/` folder next to the code it plans. A store moves that folder into a repository of its own, and several code repos can share it.
After a one-time setup on each machine, commands like `status`, `new change`, and `archive` can work in the store from any directory.
```
team-plans (a store: OpenSpec in its own repo)
├── .openspec-store/store.yaml the store's name
└── openspec/
├── specs/
└── changes/
▲
│ set up once on each machine,
│ shared by pushing and cloning like any repo
┌─────────────┼─────────────┐
│ │ │
web-app api-server mobile-app
(code repo) (code repo) (code repo)
```
You share a store with git, the same way you share code: commit, push, pull, and review it yourself. Specs and changes get branches and pull requests the same way code does.
## When you need one
Two common reasons to use a store:
- **Frontend and backend in separate repos**: one feature touches both, and the plan needs a single home instead of two halves.
```
shop-plans (store)
└── openspec/changes/add-discounts/ one plan for the feature
▲
┌─────────┴─────────┐
│ │
storefront api
(frontend repo) (backend repo)
```
- **One product, several client repos**: Android, iOS, and web ship from their own repos but share one expected behavior. A spec describes behavior, not implementation, so one spec serves all three.
```
product-specs (store)
└── openspec/specs/checkout/spec.md the expected behavior
▲
┌─────────────┼─────────────┐
│ │ │
android-app ios-app web-app
(code repo) (code repo) (code repo)
```
You can have more than one store, though we recommend keeping the count low.
## Set up a store
One person creates the store, then everyone else joins it.
1. **Create the store** (one person, once per team). Run `openspec store setup` and answer the prompts:
```bash
# run from anywhere; it asks what to create and where
openspec store setup
```
It asks three questions:
- **Store name**: `team-plans`
- **Where should this store live?**: pre-filled with `~/openspec/<name>`, press Enter to accept it or type another path
- **Create this store?**: shows what it's about to make, answer `Yes`
Then it reports what it created:
```yaml
Store ready: team-plans
Location: ~/openspec/team-plans
OpenSpec root: ready
Registry: registered
Next: run normal OpenSpec commands against this store, for example:
openspec new change <change-id> --store team-plans
Share this store by committing and pushing it like any Git repo.
```
2. **Push it to your git host.** Create an empty `team-plans` repo on your host first. Setup doesn't add a git remote, so connect the store to that repo, then push:
```bash
# connect the store to the empty repo on your git host
cd ~/openspec/team-plans
git remote add origin git@github.com:acme/team-plans.git
# publish it
git push -u origin main
```
3. **Join the store** (every teammate, once per machine):
```bash
# get the store onto your machine
git clone git@github.com:acme/team-plans.git ~/openspec/team-plans
# tell OpenSpec where it lives
openspec store register ~/openspec/team-plans
```
```yaml
Store registered: team-plans
Location: /Users/you/openspec/team-plans
OpenSpec root: ready
Registry: registered
```
Registering tells your machine where this store lives. The store's name is already committed inside it, in `.openspec-store/store.yaml`. Setup registered the creator's copy, so only cloned copies need this step.
4. **Confirm it worked**, from any directory:
```bash
# any OpenSpec command reaches the store by name
openspec status --store team-plans
```
```yaml
Using OpenSpec root: team-plans (/Users/you/openspec/team-plans)
No active changes. Create one with: openspec new change <name> --store team-plans
```
## Types of setups
OpenSpec has three setups. The rest of this page uses these names:
- **repo-local**: OpenSpec inside your repo, no store. The default.
- **store-only**: your repo keeps no specs or changes of its own. Everything lives in the store.
- **store-optional**: your project has its own `openspec/` folder and also reaches a store when you ask.
### The default: OpenSpec inside your repo (`repo-local`)
`openspec init` puts an `openspec/` folder next to your code, and that repo's specs and changes live there. No store is involved. This is the setup [Set up your project](../start/setup.md) teaches, and most projects never need another.
```
web-app (code repo)
└── openspec/
├── specs/
└── changes/
```
### OpenSpec outside your repo, in a store (`store-only`)
The repo keeps no specs or changes of its own. Everything it plans lives in the store, and one line in the repo's config connects the two.
Common when one team builds all the repos and plans in one place. The [examples above](#when-you-need-one) all have this shape.
```
team-plans (store)
└── openspec/
├── specs/ the repo's specs live here
└── changes/ its changes too
▲
│ store: team-plans (the connecting line)
web-app (code repo)
└── openspec/
└── config.yaml nothing else
```
### OpenSpec in your repo and in a store (`store-optional`)
The repo stays repo-local for its own work, while the store holds the shared specs and changes. Inside the repo, OpenSpec uses your project's `openspec/` folder, and reaches the store only when you pass `--store`.
Common when a repo used OpenSpec before the store existed, or when a mostly independent repo only occasionally touches shared work.
```
team-plans (store)
└── openspec/ the shared specs and changes
▲
│ only when you pass --store team-plans
web-app (code repo)
└── openspec/ this repo's own
├── config.yaml
├── specs/
└── changes/
```
A repo can start repo-local and move its specs and changes into the store later. [Move a repo's specs and changes into the store](#move-a-repos-specs-and-changes-into-the-store) shows how.
## Where artifacts get created when using stores
When you use a store, OpenSpec also has to decide where the artifacts get created. It depends on your setup:
- **store-only** (your project only writes to the store): every artifact is created in the store. The `store:` line below records that.
- **store-optional** (your project has its own `openspec/` folder and also uses a store): artifacts are created in your project, unless you name the store in your request or pass `--store` for that change. Your agent then carries the flag through the rest of the workflow.
OpenSpec writes artifacts to one of two places: your project's `openspec/` folder, or the store's. It picks in this order, and the first option that applies wins:
1. **`--store <id>` on a command.** Always wins, from any directory.
2. **Your project's `openspec/` folder.** If your project has its own `specs/` or `changes/` folders, OpenSpec uses them.
3. **The `store:` line in your project.** How a store-only project records its store.
4. **`defaultStore` on your machine.** The fallback when none of the above applies.
When OpenSpec selects a store, it prints `Using OpenSpec root: ...` before the command output.
### The `store:` line (store-only projects)
Add one line to your project's `openspec/config.yaml`:
```yaml
# web-app/openspec/config.yaml
store: team-plans
```
Everything you or your agent run inside your project now uses the store, with no flag to type:
```bash
# inside web-app, connected
openspec status
```
```yaml
Using OpenSpec root: team-plans (/Users/you/openspec/team-plans)
No active changes. Create one with: openspec new change <name> --store team-plans
```
- **Without the line**: run a plain command in a store-only project and OpenSpec stops with an error listing your registered stores.
- **Commit it**: teammates who clone your project get the line too. They still need the store registered on their machine ([step 3 of Set up a store](#set-up-a-store)), or OpenSpec errors and tells them to register it.
- **Next to real folders**: if your project also has `specs/` or `changes/` folders, OpenSpec uses those and ignores the line, with a warning.
### `defaultStore` on your machine
Set it once if every project you work in uses the same store. OpenSpec falls back to it when it finds no flag, no local `openspec/` folder, and no `store:` line:
```bash
# use team-plans whenever nothing else names a store
openspec config set defaultStore team-plans
# undo it
openspec config unset defaultStore
```
**Commands that stay local.** `init`, `update`, `templates`, `schemas`, and the `openspec schema` subcommands act on the current directory only and take no `--store`.
## Move a repo's specs and changes into the store
To take a repo from repo-local to store-only:
1. Move everything in the repo's `openspec/specs/` and `openspec/changes/` into the same folders in the store.
2. Delete the now-empty folders, so the repo's `openspec/` folder holds only `config.yaml`.
3. Add the `store:` line to that `config.yaml`.
`openspec status` inside the repo now starts with `Using OpenSpec root: team-plans`.
## Work in the store
The workflows don't change, for you or for your agent. Propose, apply, and archive run the way they always do. The only difference is where the artifacts get created, and [the section above](#where-artifacts-get-created-when-using-stores) covers that.
Create a change from inside a store-only repo and it lands in the store:
```bash
# inside web-app; the store: line routes this to team-plans
openspec new change add-login
```
```yaml
Using OpenSpec root: team-plans (/Users/you/openspec/team-plans)
Created change 'add-login' at /Users/you/openspec/team-plans/openspec/changes/add-login/
Schema: spec-driven
Next: openspec status --change add-login --store team-plans
```
- **Where it went**: into the store repo, not next to your code.
- **Sharing it**: the change exists only in your checkout until you commit and push the store repo. Teammates see it when they pull. The same goes for every artifact the workflows write.
- **Paths in the docs**: wherever the docs show an `openspec/` path, in a store setup that folder is the store's.
When artifacts get created somewhere you didn't expect, `openspec doctor` checks your setup without changing anything and prints a fix for each finding:
```bash
# check the current root and its stores
openspec doctor
```
```yaml
Doctor
Root
Location: /Users/you/openspec/team-plans
OpenSpec root: ok
Store: team-plans (metadata ok)
References
(none declared)
```
`openspec context` lists the root and stores your current directory works with, when you want the same picture without the checks.
To open the store and a repo in one editor window, so your agent can read both, see [Worksets (beta)](worksets.md).
## Read specs from another store
Your repo can keep its own `openspec/` folder and still let your agent read another store's specs. Declare that store under `references:` in the repo's `openspec/config.yaml`:
```yaml
# api-server/openspec/config.yaml
references:
- team-plans
```
References are read-only. Your work stays in your repo, and the reference only changes what your agent is told.
When a workflow creates an artifact, its instructions gain an index of the referenced store's specs, each with a one-line summary and the exact command to fetch it:
```xml
<referenced_stores>
<!-- Read-only upstream context. Fetch what you need; cite what you use. -->
Store team-plans (/Users/you/openspec/team-plans):
- payments: Rules for charging and refunding customers.
Fetch: openspec show <spec-id> --type spec --store team-plans
</referenced_stores>
```
A reference can also carry the store's clone URL, for machines that don't have that store yet:
```yaml
references:
- team-plans
- { id: design-system, remote: "git@github.com:acme/design-system.git" }
```
With the URL declared, `openspec doctor` turns a missing store into a pasteable fix:
```yaml
# output wrapped to fit
References
- team-plans: ok (/Users/you/openspec/team-plans)
- design-system: Referenced store 'design-system' is not registered on this machine.
Fix: git clone -- git@github.com:acme/design-system.git '/Users/you/openspec/design-system' &&
openspec store register '/Users/you/openspec/design-system' --id design-system
```
## Beta limits
- **The shape may change**: command names, flags, and file formats can change between releases. Re-read this page after upgrading.
- **No sync, by design**: OpenSpec never clones, pulls, or pushes. A stale checkout shows stale specs until you pull, and references are read from whatever is on disk.
- **One checkout per store name**: registering a second folder under a name that's already registered fails, with a hint to run `openspec store unregister` first.
+62
View File
@@ -0,0 +1,62 @@
# Worksets (beta)
> Open the store and the repos that use it in one editor window, so your agent sees both.
With a store, the context your agent needs is split across folders. The specs and changes live in the store, and the code lives in each repo. An agent started in one repo can read and grep that repo and nothing else, so it works from half the picture.
Worksets are the utility OpenSpec provides for this. A workset is a saved, named list of folders you open together. This page assumes the store is already set up and registered on your machine. [Stores (beta)](stores.md) covers that.
## How it works
- **What it is**: a named list of folders, saved on your machine only. Nothing is written into the member folders, and nothing is committed.
- **What opening does**: OpenSpec generates a `.code-workspace` file from the list and launches your editor on it. Every member folder sits in one window.
- **What you get**: your editor's search, and any agent you run inside that window, can read every member folder. The agent can grep the store's specs and the repo's code in one session.
- **What it doesn't change**: which `openspec/` folder a command uses. That still follows [Where artifacts get created](stores.md#where-artifacts-get-created-when-using-stores).
## Set it up
1. **Save the workset** (once per machine). List the repo and the store as members, and the tool to open them with:
```bash
# save a named list of folders you open together
openspec workset create platform \
--member ~/src/web-app \
--member ~/openspec/team-plans \
--tool code
```
```yaml
Saved workset 'platform' (2 members) to your machine.
Open it any time with: openspec workset open platform
```
2. **Open it** whenever you start work:
```bash
# open every member in one VS Code window
openspec workset open platform
```
`openspec workset list` shows what you saved, and `openspec workset remove <name>` deletes a workset without touching the member folders:
```yaml
platform (opens in VS Code)
web-app /Users/you/src/web-app
team-plans /Users/you/openspec/team-plans
```
## Use it: one change, two folders
Say the `add-login` change lives in the `team-plans` store, and the code for it lives in `web-app`. Open the `platform` workset and ask your agent to implement the change. In that one session it can:
- read `team-plans/openspec/changes/add-login/` and the specs next to it
- edit the code in `web-app/`
- run `openspec` commands from inside `web-app`
Without the workset, the agent only sees whichever folder it was started in.
## Tools out of the box
- **VS Code** (`--tool code`) and **Cursor** (`--tool cursor`): built in. Each opens one window with every member folder.
- **Claude Code and Codex in the terminal**: temporarily disabled as workset openers while that flow is reworked. `--tool claude` or `--tool codex` stops with an error that says so and points you to VS Code or Cursor.
- **Other editors**: add them under the `openers` key in [CLI settings (config.json)](../reference/configuration/config-json.md).
@@ -0,0 +1,11 @@
# Design decisions
> Why OPSX works the way it does.
<!-- WIP, on the todo list: this page is not written yet and is held back from the
site (its entry is commented out in website/docs.sync.config.mjs, 2026-08-21). The file
stays so the structure and inbound links survive; re-list it in the sync config once the
prose lands. -->
<!-- Skeleton: heading only. Split from reference/architecture.md on 2026-08-10.
message-map.md routes "the workflow is fluid, actions not phases" here. -->
+19
View File
@@ -0,0 +1,19 @@
# Overview
> How OPSX is built: internals for the curious.
<!-- WIP, on the todo list: this page is not written yet and is held back from the
site (its entry is commented out in website/docs.sync.config.mjs, 2026-08-21). The file
stays so the structure and inbound links survive; re-list it in the sync config once the
prose lands. -->
<!-- Skeleton: headings only. Moved out of Legacy in review round 2 (it documents
the current system); split from a single architecture.md page into this folder
on 2026-08-10. -->
The pages in this section:
- [Workflow runs](workflow-runs.md): how a workflow run executes, from invocation to written artifacts.
- [Design decisions](design-decisions.md): why OPSX works the way it does.
## How the pieces fit
@@ -0,0 +1,10 @@
# Workflow runs
> How a workflow run executes, from invocation to written artifacts.
<!-- WIP, on the todo list: this page is not written yet and is held back from the
site (its entry is commented out in website/docs.sync.config.mjs, 2026-08-21). The file
stays so the structure and inbound links survive; re-list it in the sync config once the
prose lands. -->
<!-- Skeleton: heading only. Split from reference/architecture.md on 2026-08-10. -->
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,62 @@
# Change metadata (.openspec.yaml)
> The supported fields and validation rules for the metadata stored with each change.
## Location
Each change keeps its metadata at `openspec/changes/<change-name>/.openspec.yaml`, next to its artifacts. Creating a change writes the file with `schema` and `created` filled in.
## Fields
| Key | Type | Required | Effect |
| --- | --- | --- | --- |
| `schema` | string | Yes | The workflow schema this change follows |
| `created` | string, YYYY-MM-DD | No | Records the date the change was created |
| `goal` | string | No | Records what the change sets out to do |
| `affected_areas` | list of strings | No | Records the areas the change expects to touch |
| `initiative` | map: `store` and `id` | No | Records the initiative this change belongs to |
| `skip_specs` | boolean | No | Declares the change makes no spec deltas, so zero deltas validate |
| `retire_capabilities` | boolean | No | Authorizes archive to delete a capability this change empties |
### schema
The workflow schema this change follows. It is set when the change is created and wins over the project config, so a change keeps its schema even if `openspec/config.yaml` changes afterwards. Run [`openspec schemas`](../cli.md#openspec-schemas) to list the available schema names.
### initiative
The initiative this change belongs to, as a store id and an initiative id, both kebab-case:
```yaml
initiative:
store: platform-specs
id: unify-billing
```
Keys other than `store` and `id` are rejected. No command reads the link today.
### skip_specs
Declares the change intentionally makes no spec deltas: a pure refactor, tooling, or docs change. With it set, validation accepts zero deltas, and artifacts that would generate spec files count as complete. Setting it while spec files exist under specs/ is a validation error.
### retire_capabilities
Authorizes archive to retire a capability. When this change's REMOVED deltas take away the last requirement a capability has, archive deletes that capability's main spec instead of stopping. The flag exists because the deletion is only recoverable from git, so it stays the author's call.
## Example
A filled-in .openspec.yaml:
```yaml
schema: spec-driven
created: 2026-08-14
goal: Add magic-link login to the API
affected_areas:
- auth
- api
```
## Validation
The file is validated whenever a command writes or reads it. A write that fails validation throws and writes nothing. Reading an existing file fails on invalid YAML, a field that breaks its contract, or a schema name that is not available. A missing file is not an error, and the change is treated as having no metadata.
Unlike [config.yaml](config-yaml.md), bad values are never dropped with a warning. A metadata error stops the command. The one exception is unknown top-level keys, which are ignored rather than rejected.
@@ -0,0 +1,97 @@
# CLI settings (config.json)
> Every field of config.json: how the openspec CLI behaves on your machine.
## Location
The CLI keeps its machine-level settings at `~/.config/openspec/config.json` on macOS and Linux, and `%APPDATA%\openspec\config.json` on Windows. `$XDG_CONFIG_HOME` wins on every platform when set. The `openspec config` command reads and edits it.
## Fields
| Key | Type | Required | Effect |
| --- | --- | --- | --- |
| `profile` | string: `core` or `custom` | No | Picks the workflow set `openspec init` installs |
| `delivery` | string: `both`, `skills`, or `commands` | No | Whether init installs skills, slash commands, or both |
| `workflows` | list of strings | No | The workflow list a `custom` profile installs |
| `featureFlags` | map: flag → boolean | No | Boolean feature toggles |
| `defaultStore` | string | No | Machine-level fallback store for root resolution |
| `openers` | map: tool id → settings | No | The tools worksets open in, and how each is launched |
| `telemetry` | map | No | Telemetry opt-out, anonymous id, and notice-seen state |
### profile
Which workflow set `openspec init` installs. Defaults to `core`: propose, explore, apply, update, sync, and archive. Setting `custom` installs exactly the `workflows` list instead.
### delivery
Whether init installs workflows as skills, as slash commands, or both. Defaults to `both`.
### workflows
The workflows a `custom` profile installs; ignored when the profile is `core`. Valid ids: `propose`, `explore`, `new`, `continue`, `apply`, `update`, `ff`, `sync`, `archive`, `bulk-archive`, `verify`, `onboard`.
### featureFlags
Boolean toggles keyed by flag name, set with `openspec config set featureFlags.<flag> true`. No flag is read by the CLI today.
### defaultStore
The machine-level fallback store id for root resolution, consulted only when no `--store` flag, local `openspec/`, or project `store:` pointer resolves. The full ladder is [Root resolution](../../multi-repo/stores.md#where-artifacts-get-created-when-using-stores).
### openers
The tools a workset can open in, keyed by tool id. Edit `openers` in the global `config.json` with `openspec config edit` in your terminal.
| Field | Contract |
| --- | --- |
| `style` | `workspace-file` or `attach-dirs`. Required for a new tool; optional for a built-in. |
| `label` | Non-empty string shown in the tool picker. Defaults to the id for a new tool. |
| `command` | Non-empty executable name or path. Defaults to the id for a new tool. Put arguments in `args`, not in this string. |
| `args` | Array of strings passed before the workspace file or attach flags. Defaults to `[]` for a new tool. |
| `attach_flag` | Non-empty string paired with each member path for `attach-dirs`. Defaults to `--add-dir` for a new tool. Ignored for `workspace-file`. |
**Built-in overrides:** `code`, `cursor`, `claude`, and `codex` retain any fields you omit. Setting `args` replaces the entire argument list; `[]` clears it.
**Launch styles:** `workspace-file` passes the generated `.code-workspace` path to the executable. `attach-dirs` passes one flag/path pair per member, including the primary member.
**Availability:** `attach-dirs` openers, including Claude Code and Codex, are disabled by default. You cannot select or save them with `--tool`, and OpenSpec refuses to open a workset that already names one. Configuration overrides do not enable the `attach-dirs` launch style.
**Validation:** unknown fields, invalid types, and a new tool without `style` fail when a workset command reads the opener table.
This example adds VS Code Insiders and passes `--new-window` whenever the built-in VS Code opener launches:
```json
{
"openers": {
"code-insiders": {
"style": "workspace-file",
"label": "VS Code Insiders"
},
"code": {
"args": ["--new-window"]
}
}
}
```
The corresponding `code-insiders` or `code` executable must be installed and available on `PATH`.
### telemetry
The CLI stores your anonymous id and whether the first-run notice was shown. Set `telemetry.enabled` to `false` to disable telemetry. You can also opt out with `OPENSPEC_TELEMETRY=0` or `DO_NOT_TRACK=1` in your environment.
## Example
A filled-in config.json:
```json
{
"profile": "core",
"delivery": "both",
"featureFlags": {},
"telemetry": {
"anonymousId": "5f8a2c1e-4b6d-4f9a-9c3d-7e1b2a8d4c6f",
"noticeSeen": true
}
}
```
@@ -0,0 +1,102 @@
# Project configuration (config.yaml)
> Every field of openspec/config.yaml: the schema, context, and rules this project plans with.
## Location
Each OpenSpec project keeps its config file at `openspec/config.yaml`, in the project root.
## Fields
| Key | Type | Required | Effect |
| --- | --- | --- | --- |
| `schema` | string | Yes | The workflow schema this project's changes follow |
| `context` | string | No | Injected into every artifact's instructions |
| `rules` | map: artifact ID → list of strings | No | Extra rules added to one artifact's built-in guidance |
| `operations` | map: operation → guidance list | No | Advisory guidance for apply and archive work |
| `store` | string | No | Fallback OpenSpec root when this openspec/ is config-only |
| `references` | list | No | Stores whose specs are indexed into instructions |
Invalid fields never fail a command. Each field is validated on its own, and a bad value is dropped with a warning.
What to write in these fields is covered in [Project configuration](../../customize/project-config.md).
### schema
The workflow schema every change in this project follows. Valid values are `spec-driven` or a schema name the project defines. Run [`openspec schemas`](../cli.md#openspec-schemas) to list the available schema names.
### context
Free text injected into every artifact's instructions. The limit is 50KB, and a larger value is ignored with a warning.
### rules
Extra rules for one artifact, added to the schema's built-in guidance:
```yaml
rules:
proposal:
- Keep proposals under 500 words
```
Artifact IDs are not restricted to the built-in names, so artifacts from custom schemas work as keys.
### operations
Advisory guidance for how apply and archive work is conducted, separate from artifact rules:
```yaml
operations:
apply:
guidance:
- Keep test summaries concise
```
Only `apply` and `archive` are read.
### store
A store id used as the OpenSpec root, consulted only when this openspec/ directory is config-only (no specs/ or changes/). It is a fallback, never an override. The full ladder is [Root resolution](../../multi-repo/stores.md#where-artifacts-get-created-when-using-stores).
### references
Store ids whose specs this project's work draws on. An index of each store's specs (id, summary, fetch command) is added to instructions output. Spec content is never inlined, and root resolution is never affected. An entry is a store id or a map with `id` and an optional `remote` clone source:
```yaml
references:
- platform-specs
- id: billing-specs
remote: git@github.com:acme/billing-specs.git
```
## Example
A filled-in config.yaml:
```yaml
schema: spec-driven
context: |
Tech stack: TypeScript, React, Node.js
We use conventional commits
Domain: e-commerce platform
rules:
proposal:
- Keep proposals under 500 words
- Always include a "Non-goals" section
tasks:
- Break tasks into chunks of max 2 hours
operations:
apply:
guidance:
- Keep test summaries concise
archive:
guidance:
- Summarize the archive outcome before finishing
```
## Legacy names
`openspec/config.yml` is read as an alias when `config.yaml` does not exist. When both files exist, `config.yaml` wins and `config.yml` is ignored. `openspec init` creates `config.yaml`.
@@ -0,0 +1,14 @@
# Environment variables
> Every environment variable OpenSpec reads.
<!-- Skeleton: headings only. This page is the telemetry opt-out's home
(README TODO): OPENSPEC_TELEMETRY=0, DO_NOT_TRACK=1, auto-disabled in CI, plus
what's collected. start/installation.md's Deno section links here to explain
its network-permission flag. XDG vars move the config/data directories. -->
## OPENSPEC_TELEMETRY
## DO_NOT_TRACK
## XDG_CONFIG_HOME and XDG_DATA_HOME
+11
View File
@@ -0,0 +1,11 @@
# Overview
> Every file and setting that changes how OpenSpec behaves, and where each lives.
| File | Lives at | Controls |
| --- | --- | --- |
| [Project configuration (config.yaml)](config-yaml.md) | `openspec/config.yaml` | The schema, context, and rules this project plans with |
| [Change metadata (.openspec.yaml)](change-metadata.md) | `openspec/changes/<name>/.openspec.yaml` | The workflow schema, goal, scope, and spec exceptions for one change |
| [CLI settings (config.json)](config-json.md) | `~/.config/openspec/config.json` (Windows varies) | How the openspec CLI behaves on your machine |
| Environment variables | Your shell or CI environment | Telemetry opt-out, and where the config and data directories live |
| Stores | `~/.local/share/openspec/stores/` (Windows varies) | The registry and metadata behind multi-repo stores |
@@ -0,0 +1,22 @@
# Stores
> The files behind multi-repo stores: registry.yaml and store.yaml, and which root a command uses.
<!-- Skeleton: headings only. Beta, like the multi-repo group. Machine-maintained
rather than hand-edited; documented so readers can inspect and repair them. The
concept and workflow live in multi-repo/stores.md. Root resolution is the
contract from src/core/root-selection.ts: --store flag, else nearest ancestor
openspec/, else a config-only openspec/'s store: pointer, else the global
defaultStore, else error. This page owns the whole ladder including the
everyday case (nearest openspec/ wins); the section's Overview only links
here. Locations (store/foundation.ts): registry.yaml at <dataDir>/stores/
(~/.local/share/openspec/stores/); store.yaml at .openspec-store/store.yaml
inside each checkout. The glossary's "OpenSpec root" row links here. -->
## registry.yaml
## store.yaml
## Locations
## Root resolution
+40
View File
@@ -0,0 +1,40 @@
# Glossary
> Every OpenSpec term, one line each.
OpenSpec reuses words that mean something else in git, CI, and agent tooling. Each row gives the OpenSpec meaning. The last column links to more detail where available.
| Term | Definition | More |
|---|---|---|
| **Apply** | Implement the tasks in a change proposal. Skill: `openspec-apply-change`. | [Apply a change](skills.md#openspec-apply-change) |
| **Archive** | Complete a change proposal: merge its deltas into the main specs and move its folder to `openspec/changes/archive/`. | [Quickstart](../start/quickstart.md) |
| **Artifact** | A planning document inside a change proposal: `proposal.md`, delta specs, `design.md`, `tasks.md`. Not a build output. | [Artifacts](schemas/spec-driven/index.md#artifacts) |
| **Capability** | One behavior area of your system. Each has one spec at `openspec/specs/<capability>/spec.md`. | [Capabilities](schemas/spec-driven/index.md#proposalmd) |
| **Change proposal** | One unit of work: a folder under `openspec/changes/<name>/` holding its planning artifacts. Often shortened to "change". Not a git commit. | [Propose](../start/quickstart.md#step-2-propose) |
| **Command** | A typed entry point for a workflow. Spelling varies per tool (`/opsx:propose`, `/opsx-propose`). The docs name workflows by skill instead. | [Supported tools](supported-tools.md) |
| **Continue** | Create the next planning artifact for an existing change proposal. Skill: `openspec-continue-change`. | [Skills](skills.md) |
| **Delivery** | How workflows are installed: as skills, commands, or both. | [Set up your project](../start/setup.md) |
| **Delta spec** | A spec inside a change proposal listing only what changes, under `ADDED`, `MODIFIED`, `REMOVED`, and `RENAMED` headers. | [Delta specs](schemas/spec-driven/index.md#delta-specs-specmd) |
| **Explore** | Think an idea through with the agent before proposing. Writes no code. Skill: `openspec-explore`. | [Explore an idea](skills.md#openspec-explore) |
| **Fast-forward** | Create a change proposal with every planning artifact in one pass, ready to implement. Skill: `openspec-ff-change`. Not a git fast-forward. | [Skills](skills.md) |
| **Legacy workflow** | The pre-OPSX `/openspec:*` commands. | |
| **Loop** | The cycle a change proposal moves through: explore, propose, review, apply, archive. | [Quickstart](../start/quickstart.md) |
| **Main specs** | The `openspec/specs/` tree: the current, agreed behavior of your system. Archiving merges deltas into it. A capability with no spec yet gets one from its `ADDED` requirements. | [Archive](../start/quickstart.md#step-5-archive) |
| **OpenSpec root** | The `openspec/` tree a command resolves to and operates on: your repo's, or a store's. | [Stores](../multi-repo/stores.md#where-artifacts-get-created-when-using-stores) |
| **OPSX** | The current OpenSpec workflow system, and the command prefix it installs (`/opsx:`). | |
| **Profile** | Which workflows init installs: `core` or `custom`. | [Profiles](../customize/profiles.md) |
| **Propose** | Create a change proposal and generate all its planning artifacts in one step. Skill: `openspec-propose`. | [Quickstart](../start/quickstart.md) |
| **Registry** | The machine-level list of registered stores, in `registry.yaml`. Not a package registry. | [CLI](cli.md#openspec-store) |
| **Requirement** | One behavior the system must have, written with SHALL: `### Requirement:` in a spec. | [Delta specs](schemas/spec-driven/index.md#delta-specs-specmd) |
| **Scenario** | A testable example under a requirement, in WHEN/THEN form. | [Delta specs](schemas/spec-driven/index.md#delta-specs-specmd) |
| **Schema** | The definition of which artifacts a change proposal produces, and in what order. Not JSON Schema. | [Schemas](schemas/index.md) |
| **Skill** | A workflow's instructions, installed where your AI tool reads them (`.agents/skills/`, ...). | [Skills](skills.md) |
| **Spec** | A file describing how one capability behaves today, at `openspec/specs/<capability>/spec.md`. | [Archive](../start/quickstart.md#step-5-archive) |
| **spec-driven** | The default schema: proposal, then delta specs, then design, then tasks. | [spec-driven](schemas/spec-driven/index.md) |
| **Store** | A standalone OpenSpec repo registered on your machine, for planning that spans repositories. Not a data store. | [Stores (beta)](../multi-repo/stores.md) |
| **Sync** | Merge implemented deltas into the main specs without archiving. Skill: `openspec-sync-specs`. | [Skills](skills.md) |
| **Template** | The starting content a schema gives each artifact. | [Schemas](../customize/schemas.md) |
| **Update** | As a skill (`openspec-update-change`): revise a change proposal's planning artifacts. As a CLI command (`openspec update`): refresh OpenSpec's installed files. | [Update a change](skills.md#openspec-update-change), [CLI](cli.md) |
| **Verify** | Check the implementation matches a change proposal's artifacts before archiving. Skill: `openspec-verify-change`. | [Skills](skills.md) |
| **Workflow** | A named OpenSpec action (propose, apply, archive, ...), installed into your AI tool as a skill or command. | [Set up your project](../start/setup.md) |
| **Workset** | A personal, local group of folders opened together in one tool. Not a store, and nothing is shared. | [Worksets (beta)](../multi-repo/worksets.md) |
+20
View File
@@ -0,0 +1,20 @@
# Overview
> Every available workflow schema and the artifacts it defines.
<!-- This group states the formats. How schemas shape artifacts and how to
change or write one is customize/schemas.md's job. -->
A schema defines which artifacts a change proposal produces, and in what order. On disk it's a folder with a schema.yaml in it. Every field of that file is on the [schema.yaml](schema-yaml.md) page.
## Available schemas
One schema ships with the CLI:
| Schema | Artifacts |
|---|---|
| [spec-driven](spec-driven/index.md) (default) | `proposal`, `specs`, `design`, `tasks` |
A project can add its own schemas, and a machine can override globally. Where those folders live and which copy wins is in schema.yaml's [Location](schema-yaml.md#location) section.
In your terminal, [`openspec schemas`](../cli.md#openspec-schemas) prints every schema your project can see.
+228
View File
@@ -0,0 +1,228 @@
# schema.yaml
> Every field of a schema definition, for reading or writing one.
`schema.yaml` lists the planning files a workflow creates. It also defines their order and the handoff to implementation.
## Location
A project schema lives under `openspec/schemas/<name>/`:
```text
openspec/schemas/review-first/
├── schema.yaml
└── templates/
├── proposal.md
└── tasks.md
```
OpenSpec checks three places for that directory. The first match wins.
| Copy | Directory |
|---|---|
| **1. Project** | `<project>/openspec/schemas/<name>/` |
| **2. User, macOS and Linux** | `~/.local/share/openspec/schemas/<name>/` |
| **2. User, Windows** | `%LOCALAPPDATA%\openspec\schemas\<name>\` |
| **3. Package** | The schemas installed with the CLI |
If `XDG_DATA_HOME` is set, the user directory moves to `$XDG_DATA_HOME/openspec/schemas/<name>/` on every platform.
The directory name is the lookup key used by `--schema`, `config.yaml`, and [`.openspec.yaml`](../configuration/change-metadata.md#schema). If the `name` field differs from the directory name, OpenSpec still uses the directory name for lookup.
[`openspec schema which <name>`](../cli.md#openspec-schema-which) prints the active directory and any lower-priority copies it hides.
## Top-level fields
| Field | Contract |
|---|---|
| `name` | **Required.** A non-empty string stored as the schema name. Lookup still uses the directory name. |
| `version` | **Required.** A positive integer stored as the schema revision. The value doesn't change OpenSpec's behavior. |
| `description` | An optional string printed by `openspec schemas`. With no value, the schema has no description. |
| `artifacts` | **Required.** A non-empty list of [artifact entries](#artifact-fields). |
| `apply` | Optional [apply settings](#apply-fields). With no block, OpenSpec uses the [apply defaults](#apply-defaults). |
## Artifact fields
Each entry under `artifacts` defines one planning file or set of files.
| Field | Contract |
|---|---|
| `id` | **Required.** A unique, non-empty string used in dependencies, project rules, commands, and apply settings. |
| `generates` | **Required.** A relative path or glob telling the agent where to write the artifact inside the change folder. |
| `description` | **Required.** A string that labels the artifact in instructions sent to the agent. |
| `template` | **Required.** A relative path to the artifact's format in the schema's `templates/` folder. |
| `instruction` | Optional guidance telling the agent what content to produce. |
| `requires` | A list of artifact IDs that must be complete first. Default: `[]`. |
### `generates`
The path starts from the change folder. For a change named `add-auth`:
```yaml
generates: proposal.md
```
The artifact goes here:
```text
openspec/changes/add-auth/proposal.md
```
A glob can match several files:
```yaml
generates: specs/**/*.md
```
This matches Markdown files below `openspec/changes/add-auth/specs/`.
OpenSpec recognizes these glob forms in `generates`:
- **Wildcards and character classes**: values containing `*`, `?`, or `[`, such as `specs/**/*.md` and `review-[ab].md`.
- **Brace expansions**: alternatives such as `review-{api,ui}.md` and ranges such as `file-{1..3}.md`.
- **Extglobs**: patterns such as `@(proposal|design).md`, `+(proposal|design).md`, and `!(proposal|design).md`.
**Literal filenames**: a leading `!` alone does not make a glob. Use `generates: '!review.md'` to name that file. Plain parentheses such as `(proposal|design).md` and single-element braces such as `review-{api}.md` also remain literal.
OpenSpec rejects absolute paths and paths containing a `..` segment.
#### Completion
OpenSpec checks whether the output exists. It doesn't read the file to decide whether the artifact is complete.
| `generates` value | Complete when |
|---|---|
| `proposal.md` | That file exists. |
| `specs/**/*.md` | The glob matches at least one file. |
### `template`
The path starts from the schema's `templates/` folder. In the `review-first` schema:
```yaml
template: proposal.md
```
OpenSpec reads this file:
```text
openspec/schemas/review-first/templates/proposal.md
```
OpenSpec gives the template's contents to the agent as the output format. It doesn't copy the template into the change folder.
OpenSpec rejects absolute paths and paths containing a `..` segment.
### `requires`
- **Dependencies**: every ID in `requires` must name another artifact in the same schema.
- **Ready state**: an artifact becomes ready after all its dependencies are complete.
- **Invalid graphs**: missing IDs, duplicate IDs, and dependency cycles fail validation.
- **Ties**: when several artifacts are ready, their order in `artifacts` decides which one OpenSpec returns first.
## Apply fields
`apply` defines what must exist before implementation starts.
| Field | Contract |
|---|---|
| `requires` | **Required.** A non-empty list of artifacts that must exist before apply instructions become ready. |
| `tracks` | An optional relative path or glob for Markdown task files in the change folder. Default: `null`. |
| `instruction` | Optional guidance sent to the agent when apply is ready. OpenSpec uses built-in guidance by default. |
Artifact `requires` controls planning order. `apply.requires` controls when apply instructions become ready.
### `tracks`
The path starts from the change folder. For a change named `add-auth`, `tracks: tasks.md` reads:
```text
openspec/changes/add-auth/tasks.md
```
A glob such as `tracks: "**/tasks.md"` reads every matching file, such as `backend/tasks.md` and `frontend/tasks.md`. OpenSpec combines their tasks and progress. Use the same value for an artifact's `generates` field so status and list track the same files.
Apply stays blocked if no file matches or the matched files contain no checkbox with task text. OpenSpec counts these checkbox forms:
```markdown
- [ ] Pending task
- [x] Completed task
* [X] Completed task
+ [ ] Pending task
1. [ ] Pending task
2) [x] Completed task
```
Any Markdown list marker works: `-`, `*`, `+`, or a number of up to nine digits followed by `.` or `)`. Leading spaces are allowed. The [tasks.md section of the spec-driven page](spec-driven/index.md#tasksmd) defines the stricter format produced by the default schema.
The tracked files drive the apply state:
- **`blocked`**: no file matches, or no readable file has a checkbox with task text.
- **`ready`**: at least one task is pending, or a matched file could not be read while another provides tasks.
- **`all_done`**: every tracked task is checked and every matched file was read.
If a matched file cannot be read, apply keeps the tasks and progress from readable files but does not mark the change `all_done`. [Apply JSON output](../cli.md#openspec-instructions) identifies each unavailable file and the reason.
OpenSpec rejects absolute paths and paths containing a `..` segment.
### Apply defaults
| Behavior | Default |
|---|---|
| Required artifacts | Every artifact in the schema |
| Progress tracking | No tracked file |
| Agent guidance | Built-in apply guidance |
## Complete example
```yaml
name: review-first
version: 1
description: Proposal and implementation checklist
artifacts:
- id: proposal
generates: proposal.md
description: Why the change is needed and what it affects
template: proposal.md
instruction: |
Explain the problem, the proposed change, and its impact.
requires: []
- id: tasks
generates: tasks.md
description: Trackable implementation checklist
template: tasks.md
instruction: |
Break the approved proposal into ordered implementation tasks.
requires:
- proposal
apply:
requires:
- tasks
tracks: tasks.md
instruction: |
Work through the pending tasks and mark each one complete.
```
## Validation
[`openspec schema validate <name>`](../cli.md#openspec-schema-validate) checks:
- Field types and required fields
- Relative paths
- Artifact IDs, dependencies, and cycles
- `apply.requires` IDs: each must be an artifact in the schema
- Template files
A schema with an unknown `apply.requires` ID doesn't load, so every command that uses it reports the error.
Validation warns, without failing, when `apply.tracks` isn't exactly equal to some artifact's `generates` value. OpenSpec finds the tracked artifact by comparing those two strings, so anything else leaves it unable to tell which artifact's progress the file belongs to. That includes a typo like `task.md`, and also `tracks: tasks/main.md` against `generates: tasks/*.md`, where the glob does produce the file but the strings still differ. Apply keeps reading the file either way, but `openspec list` and `openspec status` count `tasks.md` instead.
Validation doesn't catch these mistakes:
| Mistake | What happens |
|---|---|
| A field is misspelled, such as `instrution` | OpenSpec ignores it. Validation doesn't report the typo. |
| `name` differs from the schema directory | Validation passes. OpenSpec still uses the directory name for lookup. |
@@ -0,0 +1,476 @@
# spec-driven
> The default workflow's artifacts: their order, their formats, and the change folder they produce.
`spec-driven` is OpenSpec's built-in default schema. [schema.yaml](../schema-yaml.md) defines the fields it sets.
## Artifacts
The workflow drafts four artifacts:
| Artifact | File | Purpose |
|---|---|---|
| [`proposal`](#proposalmd) | `proposal.md` | Why the change is needed |
| [`specs`](#delta-specs-specmd) | `specs/<capability-path>/spec.md`, one per capability | What behavior changes |
| [`design`](#designmd) | `design.md` | How to build it |
| [`tasks`](#tasksmd) | `tasks.md` | The implementation checklist |
## Drafting order
```text
┌─ specs ──┐
proposal ────┤ ├── tasks ── apply
└─ design ─┘
```
Proposal comes first. Specs and design follow in either order, and tasks needs both. Implementation ([apply](#apply)) starts once `tasks.md` is in place.
Two artifacts can be skipped:
- **`design`**: when none of [its conditions](#designmd) apply, the agent leaves it out and drafts `tasks` anyway.
- **`specs`**: set [`skip_specs: true`](../../configuration/change-metadata.md#skip_specs) in the change's `.openspec.yaml`.
## Example change folder
A change named `add-user-auth`, with every artifact drafted:
```text
openspec/changes/add-user-auth/
├── .openspec.yaml change metadata, written when the change is created
├── proposal.md
├── specs/
│ └── user-auth/
│ └── spec.md one delta spec per capability
├── design.md
└── tasks.md
```
## proposal.md
Establishes why the change is needed.
### Structure
The template the agent receives as the output format ([templates/proposal.md](https://github.com/Fission-AI/OpenSpec/blob/main/schemas/spec-driven/templates/proposal.md)):
```md
# Proposal
## Why
<!-- Explain the motivation for this change. What problem does this solve? Why now? -->
## What Changes
<!-- Describe what will change. Be specific about new capabilities, modifications, or removals. -->
## Capabilities
### New Capabilities
<!-- Capabilities being introduced. Use kebab-case for path segments you introduce
(e.g., user-auth or identity/user-auth) that follow the project's existing
spec organization. Each creates specs/<capability-path>/spec.md. -->
- `<capability-path>`: <brief description of what this capability covers>
### Modified Capabilities
<!-- Existing capabilities whose REQUIREMENTS are changing (not just implementation).
Only list here if spec-level behavior changes. Each needs a delta spec file.
Use the exact existing path under openspec/specs/. Leave empty if no requirement
changes. A change with no capabilities at all (pure refactor, tooling, docs)
must set `skip_specs: true` in its .openspec.yaml - openspec validate rejects
a zero-delta change without that marker. Do not invent a requirement just to
satisfy validation. -->
- `<existing-capability-path>`: <what requirement is changing>
## Impact
<!-- Affected code, APIs, dependencies, systems -->
```
### Instructions
The instruction sent to the agent when it drafts this artifact (from [schema.yaml](https://github.com/Fission-AI/OpenSpec/blob/main/schemas/spec-driven/schema.yaml)):
```md
Create the proposal document that establishes WHY this change is needed.
Sections:
- **Why**: 1-2 sentences on the problem or opportunity. What problem does this solve? Why now?
- **What Changes**: Bullet list of changes. Be specific about new capabilities, modifications, or removals. Mark breaking changes with **BREAKING**.
- **Capabilities**: Identify which specs will be created or modified:
- **New Capabilities**: List capabilities being introduced. Each becomes a new `specs/<capability-path>/spec.md`. Use kebab-case for path segments you introduce (e.g., `user-auth` or `identity/user-auth`) and follow the project's existing spec organization.
- **Modified Capabilities**: List existing capabilities whose REQUIREMENTS are changing. Only include if spec-level behavior changes (not just implementation details). Each needs a delta spec file. Use the exact existing path under `openspec/specs/`. Leave empty if no requirement changes.
- **Impact**: Affected code, APIs, dependencies, or systems.
IMPORTANT: The Capabilities section is critical. It creates the contract between
proposal and specs phases. Research existing specs before filling this in:
run `openspec list --specs` for the project's capability inventory, then
`openspec show "<spec-id>" --type spec --json --no-scenarios` for any that
look related - that returns a capability's purpose and requirement texts
without pulling whole spec files into context. Append `--store "<id>"` to
both commands only for a registered standalone store, and keep `--type
spec`: a change and a spec sharing a name is otherwise an ambiguous-item
error. `openspec list` without `--specs` lists in-flight changes, not
specs - it never shows what the project already covers. Reuse an existing
capability's exact path instead of introducing a near-duplicate name.
The filtered read is only an overview. Before deciding what is already
covered or what should change, read each relevant spec in full, including
scenarios, with `openspec show "<spec-id>" --type spec` (same `--store` rule).
Each capability listed here will need a corresponding spec file.
Every change must either declare at least one capability (new or
modified) or explicitly opt out of specs: `openspec validate` rejects a
change with zero deltas unless the change's `.openspec.yaml` sets
`skip_specs: true`. Use `skip_specs: true` only when no spec-level
behavior changes (pure refactor, tooling, docs) - specs describe
behavior, so if behavior does not change, no spec should change either.
Do not invent a requirement just to satisfy validation.
Keep it concise (1-2 pages). Focus on the "why" not the "how" -
implementation details belong in design.md.
This is the foundation - specs, design, and tasks all build on this.
```
## Delta specs (spec.md)
Defines what behavior changes, with one delta spec per capability the proposal lists.
Each delta spec is the `spec.md` inside its capability folder. `openspec validate` and `openspec archive` reject delta sections written in any other file under `specs/`, such as `specs/user-auth.md`, because archive never merges them.
### Structure
The template the agent receives as the output format ([templates/spec.md](https://github.com/Fission-AI/OpenSpec/blob/main/schemas/spec-driven/templates/spec.md)):
```md
# Spec Delta
## Purpose
<!-- New capabilities only: one or two sentences (50+ characters) on what this capability is for. Delete this section for an existing capability. -->
## ADDED Requirements
### Requirement: <!-- requirement name -->
<!-- requirement text -->
#### Scenario: <!-- scenario name -->
- **WHEN** <!-- condition -->
- **THEN** <!-- expected outcome -->
```
### Instructions
The instruction sent to the agent when it drafts this artifact (from [schema.yaml](https://github.com/Fission-AI/OpenSpec/blob/main/schemas/spec-driven/schema.yaml)):
````md
Create specification files that define WHAT the system should do.
A spec is a behavior contract, not an implementation plan.
Good spec content:
- Observable behavior users or downstream systems rely on
- Inputs, outputs, and error conditions
- External constraints (security, privacy, reliability, compatibility)
- Scenarios that can be tested or explicitly validated
Avoid in specs:
- Internal class/function names
- Library or framework choices
- Step-by-step implementation details
- Detailed execution plans (those belong in design.md or tasks.md)
Quick test: if the implementation can change without changing externally
visible behavior, it likely does not belong in the spec.
Create one spec file per capability listed in the proposal's Capabilities section.
`<capability-path>` is the spec directory relative to `specs/` (for example,
`user-auth` or `identity/user-auth`). Preserve the full path:
- New capabilities: use the exact path from the proposal at `specs/<capability-path>/spec.md`. Any path segment newly introduced in the proposal must be kebab-case. Follow the project's existing organization; do not add a new domain level when the project uses a flat layout.
- Modified capabilities: use the exact existing path from `openspec/specs/<capability-path>/` when creating the delta at `specs/<capability-path>/spec.md`. Run `openspec list --specs` to confirm that path before writing the delta, appending `--store "<id>"` only for a registered standalone store - a mistyped or invented path targets a capability that does not exist rather than the one you meant. Do not move or rename the capability.
There must be at least one spec file unless the change's `.openspec.yaml`
sets `skip_specs: true` (no spec-level behavior change) - `openspec validate`
rejects a zero-delta change without that marker. If the proposal lists no
capabilities and `skip_specs` is not set, revisit the proposal first.
Delta operations (use ## headers):
- **ADDED Requirements**: New capabilities
- **MODIFIED Requirements**: Changed behavior - MUST include full updated content
- **REMOVED Requirements**: Deprecated features - MUST include **Reason** and **Migration**
- **RENAMED Requirements**: Name changes only - use FROM:/TO: format
Format requirements:
- Each requirement: `### Requirement: <name>` followed by description
- Use SHALL/MUST for normative requirements (avoid should/may)
- Each scenario: `#### Scenario: <name>` with WHEN/THEN format
- **CRITICAL**: Scenarios MUST use exactly 4 hashtags (`####`). Using 3 hashtags or bullets will fail silently.
- Every requirement MUST have at least one scenario.
New capabilities only: the delta spec's first section is `## Purpose` -
one or two sentences (50+ characters, or `openspec validate --strict`
reports it as too brief) describing what the capability is for. Archive
copies it into the main spec it creates; without it the new main spec is
left with a `TBD ... Update Purpose after archive` placeholder to fill in
by hand. Do NOT add `## Purpose` to a delta for an existing capability -
that spec already has one and the delta's is ignored. To change an
existing capability's Purpose - including a leftover `TBD` placeholder -
edit `<planningHome.root>/openspec/specs/<capability-path>/spec.md`
directly. `planningHome.root` comes from the `openspec instructions ...
--json` response. Always use it rather than a repo-relative path: it
resolves to the store whenever the change lives in one - whether that
came from `--store`, a project `store:` pointer, or a global default
store - and to the current repository otherwise. Do not try to work out
which case applies; the field already has.
MODIFIED requirements workflow:
1. Locate the existing requirement in `<planningHome.root>/openspec/specs/<capability-path>/spec.md` (the same store-aware root as above)
2. Copy the ENTIRE requirement block (from `### Requirement:` through all scenarios)
3. Paste under `## MODIFIED Requirements` and edit to reflect new behavior
4. Ensure header text matches exactly (whitespace-insensitive)
Common pitfall: Using MODIFIED with partial content loses detail at archive time.
If adding new concerns without changing existing behavior, use ADDED instead.
Example (a new capability, so its first section is `## Purpose`):
```
# Spec Delta
## Purpose
Lets users take their data out of the product in a portable format.
## ADDED Requirements
### Requirement: User can export data
The system SHALL allow users to export their data in CSV format.
#### Scenario: Successful export
- **WHEN** user clicks "Export" button
- **THEN** system downloads a CSV file with all user data
## REMOVED Requirements
### Requirement: Legacy export
**Reason**: Replaced by new export system
**Migration**: Use new export endpoint at /api/v2/export
```
Specs should be testable - each scenario is a potential test case.
````
## design.md
Explains how to implement the change. Drafted only when the change needs one.
### Structure
The template the agent receives as the output format ([templates/design.md](https://github.com/Fission-AI/OpenSpec/blob/main/schemas/spec-driven/templates/design.md)):
```md
# Design
## Context
<!-- Current state and constraints that shape the approach. See proposal.md for motivation - don't restate it -->
## Goals / Non-Goals
**Goals:**
<!-- What this design aims to achieve -->
**Non-Goals:**
<!-- What is explicitly out of scope -->
## Decisions
<!-- Key design decisions with rationale and alternatives considered -->
## Risks / Trade-offs
<!-- Known risks and trade-offs -->
```
### Instructions
The instruction sent to the agent when it drafts this artifact (from [schema.yaml](https://github.com/Fission-AI/OpenSpec/blob/main/schemas/spec-driven/schema.yaml)):
```md
Create the design document that explains HOW to implement the change.
When to include design.md (create only if any apply):
- Cross-cutting change (multiple services/modules) or new architectural pattern
- New external dependency or significant data model changes
- Security, performance, or migration complexity
- Ambiguity that benefits from technical decisions before coding
Sections:
- **Context**: Only the current state and constraints needed to explain the approach. Reference the proposal for motivation instead of restating it (e.g., "See proposal.md - Why").
- **Goals / Non-Goals**: What this design achieves and explicitly excludes. Don't restate the proposal's scope - add only design-level boundaries.
- **Decisions**: Key technical choices with rationale (why X over Y?). Include alternatives considered for each decision.
- **Risks / Trade-offs**: Known limitations, things that could go wrong. Format: [Risk] → Mitigation
- **Migration Plan**: Steps to deploy, rollback strategy (if applicable)
- **Open Questions**: Unknowns that can safely be answered later without
changing the specs, the approach, or the task breakdown. Omit if none.
Open questions are for genuinely deferrable unknowns, not decisions you
skipped. If a question would change the specs, the chosen approach, or
the task breakdown, resolve it now - ask the user instead of guessing.
Focus on architecture and approach, not line-by-line implementation.
The proposal covers why and what; design covers how. Reference the
proposal for motivation and, once written, the specs for requirements -
if a section would only restate them, point to them instead.
Good design docs explain the "why" behind technical decisions.
```
## tasks.md
Breaks the implementation into checkable tasks. [apply](#apply) tracks progress here.
### Structure
The template the agent receives as the output format ([templates/tasks.md](https://github.com/Fission-AI/OpenSpec/blob/main/schemas/spec-driven/templates/tasks.md)):
```md
# Tasks
## 1. <!-- Task Group Name -->
- [ ] 1.1 <!-- Task description -->
- [ ] 1.2 <!-- Task description -->
## 2. <!-- Task Group Name -->
- [ ] 2.1 <!-- Task description -->
- [ ] 2.2 <!-- Task description -->
```
### Instructions
The instruction sent to the agent when it drafts this artifact (from [schema.yaml](https://github.com/Fission-AI/OpenSpec/blob/main/schemas/spec-driven/schema.yaml)):
````md
Create the task list that breaks down the implementation work.
Before writing tasks, check design.md for Open Questions. If any of them
would change what gets built, resolve them with the user first - do not
bake an unstated assumption into the task list.
**IMPORTANT: Follow the template below exactly.** The apply phase parses
checkbox format to track progress. A box holding only `x` counts as done,
upper or lower case and with any spacing, so `- [ x]` is done too. Every
other marker, including `- [~]`, `- [-]` and an empty `- []`, reads as
unfinished. A line with no checkbox is not tracked at all.
Guidelines:
- Group related tasks under ## numbered headings
- Each task MUST be a checkbox: `- [ ] X.Y Task description`
- Tasks should be small enough to complete in one session
- Order tasks by dependency (what must be done first?)
- Each task MUST state how to verify completion (a test, command,
observable behavior, or delivered artifact). Put the verification in
that task's checkbox description. Use a separate verification task only
when it checks broader integration or system behavior that spans
multiple implementation tasks.
- Each task group MUST land the tests and documentation its own work
calls for. Do NOT collect testing or documentation into a final group -
when a late group first exercises work from an early one, the failures
cascade back through every group in between and force rework. A group
whose work calls for neither, such as scaffolding or dependency setup,
carries neither. A final group is for integration checks only, not for
the tests and docs an earlier group owed.
Example:
```
# Tasks
## 1. Setup
- [ ] 1.1 Create new module structure and verify expected files are present
- [ ] 1.2 Add dependencies to package.json and verify package installation succeeds
## 2. Core Implementation
- [ ] 2.1 Implement data export function and verify the export test passes
- [ ] 2.2 Add CSV formatting utilities and verify unit tests cover quoting and delimiters
- [ ] 2.3 Document the export API in docs/export.md and verify the documented command runs as written
```
Reference specs for what needs to be built, design for how to build it.
````
## Apply
The handoff from planning to implementation. Apply is the phase that works through `tasks.md`, not an artifact.
- **Starts**: once `tasks.md` exists and lists at least one task.
- **Tracks**: the checkboxes in `tasks.md`. Checking them off is the progress record.
- **Ends**: every checkbox checked. OpenSpec then suggests archiving the change.
### Settings
The apply settings (from [schema.yaml](https://github.com/Fission-AI/OpenSpec/blob/main/schemas/spec-driven/schema.yaml)):
```yaml
apply:
requires: [tasks]
tracks: tasks.md
# instruction: shown below
```
### Instructions
The instruction sent to the agent when implementation starts (from [schema.yaml](https://github.com/Fission-AI/OpenSpec/blob/main/schemas/spec-driven/schema.yaml)):
```md
Read context files, work through pending tasks, mark complete as you go.
Pause if you hit blockers or need clarification.
```
## schema.yaml
The complete [schema.yaml](https://github.com/Fission-AI/OpenSpec/blob/main/schemas/spec-driven/schema.yaml), with instruction bodies elided. Each is shown in full in its section above.
```yaml
name: spec-driven
version: 1
description: Default OpenSpec workflow - proposal → specs → design → tasks
artifacts:
- id: proposal
generates: proposal.md
description: Initial proposal document outlining the change
template: proposal.md
# instruction: shown in full under proposal.md above
requires: []
- id: specs
generates: "specs/**/*.md"
description: Detailed specifications for the change
template: spec.md
# instruction: shown in full under Delta specs above
requires:
- proposal
- id: design
generates: design.md
description: Technical design document with implementation details
template: design.md
# instruction: shown in full under design.md above
requires:
- proposal
- id: tasks
generates: tasks.md
description: Implementation checklist with trackable tasks
template: tasks.md
# instruction: shown in full under tasks.md above
requires:
- specs
- design
apply:
requires: [tasks]
tracks: tasks.md
# instruction: shown in full under Apply above
```
+185
View File
@@ -0,0 +1,185 @@
# Skills
> Every OpenSpec skill: arguments, what it creates, and what it responds with.
<!-- Drafted 2026-08-11 via one subagent per entry, each verifying every claim against
its workflow template in src/core/templates/workflows/; assembled and uniformity-passed
by the main session. Terminology: "change proposal", never bare "change" (user call,
2026-08-11). Shape (user-reviewed): intro bullets define Core/Optional, then ONE index
table (Skill / Job / Type) and a flat run of H2 entries matching
cli.md's shape - no group sections. Recipe per entry: one-line job sentence, then a
two-column key-value table (header row "Contract | Description", uniform across
entries) holding the pure input/output contract,
one row per fact: Arguments (what you pass; each cell self-contains its
optional/ambiguous behavior) / Creates (exact paths written; always states the code
boundary) / Response (what the agent reports back and where it stops). No judgment
rows: no when-to-use beyond the job sentence, no Not-for routing, no guide links
(guides link here, not the reverse). The ff job says "create a change proposal"
because its template unconditionally scaffolds a new one (redirects if the name
exists), contradicting the old "remaining artifacts" framing. Paths shown are the
default single-repo layout, stated without a caveat: reference pages state defaults,
and the store-moves-the-planning-home fact is multi-repo/stores.md's to teach (the
per-tool command spelling story likewise stays with setup.md and supported-tools.md;
user cut the NOTE carrying both, 2026-08-11). H2 entries double as the site's
right-rail TOC and the anchors guides deep-link. No frontmatter in source: sync-docs.mjs lifts H1 to title and the > line to
description (README pins the > line verbatim in its page index). Deliberately
excluded, each with an owner elsewhere: per-tool command spellings and syntax
(reference/supported-tools.md), example transcripts (quickstart and guides), tips and
when-to-use judgment (guides own it), troubleshooting (help/troubleshooting.md),
legacy /openspec:* commands (help/legacy/migration.md). Source: old docs/commands.md
maps here per sources.md; its unsupported claims (apply "runs tests", bulk-archive
name arguments, fixed tasks.md filename) were checked against templates and dropped.
Skill names from WORKFLOW_TO_SKILL_DIR (src/core/profile-sync-drift.ts) and the
templates in src/core/templates/workflows/; core set src/core/profiles.ts:14. "Optional"
is the docs' set label (was "Expanded"; renamed 2026-08-12: the product's only stored
profile values are core and custom, so "expanded" reads as a third profile). -->
The skills come in two sets:
- **Core**: installed by default, the main planning loop.
- **Optional**: installed only when you add them, via [Profiles](../customize/profiles.md).
Every skill expects a project that already uses OpenSpec. Before its first step that writes anything, a skill checks for a resolved root. What happens when there is none depends on how the skill was reached:
- **Auto-selected**: your agent picked the skill on its own, without you naming OpenSpec. It drops OpenSpec and answers your request normally, the way it would with OpenSpec not installed.
- **Explicit OpenSpec request**: you named OpenSpec, named the skill, or ran its command. It stops before writing and asks how to proceed: run `openspec init` here, target a store with `--store <id>`, or continue without OpenSpec. It waits for your answer.
Commands are always the second case. A project whose `openspec/config.yaml` names a store this machine cannot resolve (not registered, or a malformed `store:` line) is not treated as uninitialized: the skill stops and shows the store error with its fix. No skill creates an `openspec/` directory on its own in either case. The entries below describe what each skill does once a root is in place.
| Skill | Job | Type |
|---|---|---|
| [openspec-explore](#openspec-explore) | Think through an idea before it becomes a change proposal | Core |
| [openspec-propose](#openspec-propose) | Create a change proposal with all its planning artifacts in one step | Core |
| [openspec-apply-change](#openspec-apply-change) | Implement a change proposal's tasks | Core |
| [openspec-update-change](#openspec-update-change) | Revise a change proposal's plan | Core |
| [openspec-sync-specs](#openspec-sync-specs) | Merge a change proposal's spec updates into `specs/` | Core |
| [openspec-archive-change](#openspec-archive-change) | Move a finished change proposal to the archive | Core |
| [openspec-new-change](#openspec-new-change) | Start a change proposal as an empty scaffold | Optional |
| [openspec-continue-change](#openspec-continue-change) | Create the next planning artifact, one at a time | Optional |
| [openspec-ff-change](#openspec-ff-change) | Create a change proposal with every artifact implementation needs, in one pass | Optional |
| [openspec-verify-change](#openspec-verify-change) | Check the implementation matches the plan | Optional |
| [openspec-bulk-archive-change](#openspec-bulk-archive-change) | Archive several change proposals at once | Optional |
| [openspec-onboard](#openspec-onboard) | Learn the workflow by doing one real change proposal end to end | Optional |
Each entry below names the skill that owns the next step. When your profile leaves that skill out, the installed files never name it: the handoff becomes the equivalent `openspec` command, or a plain request to you, and a line that exists only to point at a missing skill is not written at all. So the skills you have always hand off to skills you have. Which set you get is [Profiles](../customize/profiles.md).
## openspec-explore
Think through an idea before it becomes a change proposal.
| Contract | Description |
|---|---|
| **Arguments** | A topic: an idea, a problem, a comparison, or the name of an existing change proposal to explore in context. With nothing given it enters explore mode. |
| **Creates** | Nothing by default. It reads and investigates only. On request it captures insights: a new change proposal under `openspec/changes/<name>/`, or updates to an existing one's proposal, design, specs, or tasks. Never code. |
| **Response** | An open conversation with no required output. When thinking crystallizes it summarizes the problem, approach, open questions, and next steps, and offers to capture them. You decide. Implementation never starts here. |
## openspec-propose
Create a change proposal and generate all its planning artifacts in one step.
| Contract | Description |
|---|---|
| **Arguments** | A kebab-case name (`add-dark-mode`) or a plain description. Asks if you give neither. |
| **Creates** | `openspec/changes/<name>/` with every artifact the schema defines, in dependency order (spec-driven: proposal, spec deltas, design, tasks). Never code. |
| **Response** | The created artifacts, ready for review, and the next step. Stops there; implementation waits for `openspec-apply-change`. |
## openspec-apply-change
Implement a change proposal's tasks, working through the list until done or blocked.
| Contract | Description |
|---|---|
| **Arguments** | A change proposal name (`add-auth`), optional. If the target is ambiguous it lists the active change proposals and asks you to pick. |
| **Creates** | Code: the minimal changes each task calls for, in your project files. In the change proposal it touches only the tasks file, checking off each finished task (`- [ ]` to `- [x]`). |
| **Response** | Progress per task, then an overall count (N/M tasks complete). All done: suggests `openspec-archive-change`. Blocked by missing artifacts: points to `openspec-continue-change`, or to `openspec status` and `openspec instructions` when that skill is not installed (the core profile leaves it out). Unclear tasks or errors: pauses and asks. |
## openspec-update-change
Revise a change proposal's existing planning artifacts and keep them coherent with each
other.
| Contract | Description |
|---|---|
| **Arguments** | A change proposal name, optional, plus the revision you want. With no revision stated it runs a coherence review: artifacts checked against each other for contradictions, gaps, and duplication. |
| **Creates** | Edits artifact files that already exist. One exception: for an artifact written as a glob, such as `specs/**/*.md`, that already has at least one file, it can add a missing companion file once you confirm the path. An artifact with no files yet is `openspec-continue-change`'s job. Without that skill (the core profile leaves it out), it points to `openspec status` and `openspec instructions` instead. Never code. |
| **Response** | Shows each proposed revision and writes it only after you confirm, one artifact at a time. Ends with what was revised and the next step; implementation waits for `openspec-apply-change`. |
## openspec-sync-specs
Merge a change proposal's spec updates into `specs/` without archiving it.
| Contract | Description |
|---|---|
| **Arguments** | A change proposal name, optional. You can also name a subset of its delta specs, and only those sync. |
| **Creates** | Edits or creates `openspec/specs/<capability-path>/spec.md` for each delta spec, merging added, modified, removed, and renamed requirements into the main spec. Never code. |
| **Response** | A per-capability summary of requirements added, modified, removed, or renamed, after the updated specs validate. The change proposal stays active; archiving waits for `openspec-archive-change`. |
## openspec-archive-change
Move a finished change proposal to the archive.
| Contract | Description |
|---|---|
| **Arguments** | A change proposal name, optional. |
| **Creates** | Moves the change proposal folder to `openspec/changes/archive/YYYY-MM-DD-<name>/` (no date added if the name already starts with one). With your approval it first syncs outstanding delta specs via `openspec-sync-specs`. Never code. |
| **Response** | Warns and asks before archiving with incomplete artifacts or tasks, and asks whether to sync when delta specs exist. Ends with a summary: name, schema, archive location, spec sync status, and any warnings. |
## openspec-new-change
Start a change proposal as an empty scaffold.
| Contract | Description |
|---|---|
| **Arguments** | A kebab-case name (`add-user-auth`) or a plain description, plus a schema name only for a non-default workflow. Asks what you want to build if you give neither. |
| **Creates** | `openspec/changes/<name>/` as an empty scaffold: no artifacts yet, never code. |
| **Response** | The scaffold's name and location, the workflow's artifact sequence, status (0/N complete), and the first artifact's template. Drafting artifacts waits for `openspec-continue-change`. |
## openspec-continue-change
Create the next planning artifact in a change proposal, one at a time.
| Contract | Description |
|---|---|
| **Arguments** | A change proposal name, optional. If still ambiguous it asks you to pick from the most recently modified. |
| **Creates** | The single next ready artifact in the schema's sequence, written into the change proposal folder. One artifact per run, never code. |
| **Response** | The created artifact, progress (N of M complete), and which artifacts that unlocked. When planning is complete it says so; implementation moves to `openspec-apply-change`. |
## openspec-ff-change
Create a change proposal and every planning artifact implementation needs, in one pass.
| Contract | Description |
|---|---|
| **Arguments** | A kebab-case name or a plain description. Asks if you give neither. If the named change proposal already exists it suggests continuing it instead. |
| **Creates** | `openspec/changes/<name>/` and every planning artifact implementation requires, in dependency order (spec-driven: proposal, specs, design, tasks), leaving out only artifacts marked skipped or conditional. Never code. |
| **Response** | The change proposal's name and location, each artifact created, and any conditional artifact skipped and why. Stops there; implementation waits for `openspec-apply-change`. |
## openspec-verify-change
Check that the implementation matches the change proposal's artifacts.
| Contract | Description |
|---|---|
| **Arguments** | A change proposal name, optional. When ambiguous it asks, listing change proposals that have a tasks artifact. |
| **Creates** | Nothing. It reads the change proposal's artifacts and the codebase. Verification is report-only. |
| **Response** | A report: a scorecard for Completeness, Correctness, and Coherence, then CRITICAL, WARNING, and SUGGESTION issues with recommendations, and a final archive-readiness assessment. It changes nothing and does not archive. |
## openspec-bulk-archive-change
Archive several change proposals at once.
| Contract | Description |
|---|---|
| **Arguments** | None. It lists the active change proposals and asks you to select any number, with an option for all. If none are active it says so and stops. |
| **Creates** | `openspec/changes/archive/YYYY-MM-DD-<name>/` per archived change proposal (already-dated names keep their prefix). Each one's spec deltas sync first via `openspec-sync-specs`. Never code. |
| **Response** | A status table per change proposal and one confirmation for the whole batch, then a summary of archived, skipped, and failed, plus spec sync results. When two change proposals touch the same spec it checks the codebase and syncs implemented deltas oldest first. |
## openspec-onboard
Learn the workflow by doing one real change proposal end to end.
| Contract | Description |
|---|---|
| **Arguments** | None. It scans your codebase for small starter tasks and asks you to pick one or describe your own. |
| **Creates** | A real change proposal for the chosen task, one artifact at a time, then real code once you confirm implementation. Archives the change proposal at the end. |
| **Response** | A narrated walkthrough of the full cycle with pauses for your input: explore, create, build each artifact, implement, archive. Ends with a recap and a pointer to `openspec-propose`. Takes about 15 to 20 minutes. |
+144
View File
@@ -0,0 +1,144 @@
# Supported tools
> Which AI coding tools OpenSpec supports, and each one's command syntax.
Every tool in the matrix runs the same OpenSpec workflows. A skill and its command are
the same workflow instructions. The only difference is what you type. Which form init
installs is the delivery setting, covered in
[Set up your project](../start/setup.md#the-workflow-files-skills-and-commands).
## Support matrix
Invocations are shown for the apply workflow. Every workflow follows the same shape.
The id goes to `openspec init --tools <id>` to skip the picker ([CLI](cli.md)).
| Tool | `--tools` id | Skills | Skill invocation | Commands | Command invocation |
|---|---|---|---|---|---|
| Amazon Q Developer | `amazon-q` | `.amazonq/skills/` | `/openspec-apply-change` | `.amazonq/prompts/` | `@opsx-apply` |
| Antigravity | `antigravity` | `.agents/skills/` | `/openspec-apply-change` | `.agents/workflows/` | `/opsx-apply` |
| Auggie (Augment CLI) | `auggie` | `.augment/skills/` | `/openspec-apply-change` | `.augment/commands/` | `/opsx-apply` |
| Bob Shell | `bob` | `.bob/skills/` | `/openspec-apply-change` | `.bob/commands/` | `/opsx-apply` |
| Claude Code | `claude` | `.claude/skills/` | `/openspec-apply-change` | `.claude/commands/opsx/` | `/opsx:apply` |
| Cline | `cline` | `.cline/skills/` | `/openspec-apply-change` | `.clinerules/workflows/` | `/opsx-apply` |
| CodeArts | `codeartsagent` | `.codeartsdoer/skills/` | `/openspec-apply-change` | none | none |
| CodeBuddy Code (CLI) | `codebuddy` | `.codebuddy/skills/` | `/openspec-apply-change` | `.codebuddy/commands/opsx/` | `/opsx:apply` |
| Codex | `codex` | `.agents/skills/` | `$openspec-apply-change` | none | none |
| Continue | `continue` | `.continue/skills/` | `/openspec-apply-change` | `.continue/prompts/` | `/opsx-apply` |
| CoStrict | `costrict` | `.cospec/skills/` | `/openspec-apply-change` | `.cospec/openspec/commands/` | `/opsx-apply` |
| Crush | `crush` | `.crush/skills/` | `/openspec-apply-change` | `.crush/commands/opsx/` | `/opsx:apply` |
| Cursor | `cursor` | `.cursor/skills/` | `/openspec-apply-change` | `.cursor/commands/` | `/opsx-apply` |
| Devin Desktop (formerly Windsurf) | `devin` | `.devin/skills/` | `/openspec-apply-change` | `.devin/workflows/` | `/opsx-apply` |
| Factory Droid | `factory` | `.factory/skills/` | `/openspec-apply-change` | `.factory/commands/` | `/opsx-apply` |
| ForgeCode | `forgecode` | `.forge/skills/` | `/openspec-apply-change` | none | none |
| Gemini CLI | `gemini` | `.gemini/skills/` | `/openspec-apply-change` | `.gemini/commands/opsx/` | `/opsx:apply` |
| GitHub Copilot | `github-copilot` | `.github/skills/` | `/openspec-apply-change` | `.github/prompts/` | `/opsx-apply` |
| Hermes Agent | `hermes` | `.hermes/skills/` | `/openspec-apply-change` | none | none |
| iFlow | `iflow` | `.iflow/skills/` | `/openspec-apply-change` | `.iflow/commands/` | `/opsx-apply` |
| Junie | `junie` | `.junie/skills/` | `/openspec-apply-change` | `.junie/commands/` | `/opsx-apply` |
| Kilo Code | `kilocode` | `.kilocode/skills/` | `/openspec-apply-change` | `.kilo/command/` | `/opsx-apply` |
| Kimi Code | `kimi` | `.kimi-code/skills/` | `/skill:openspec-apply-change` | none | none |
| Kiro | `kiro` | `.kiro/skills/` | `/openspec-apply-change` | `.kiro/prompts/` | `/opsx-apply` |
| Lingma | `lingma` | `.lingma/skills/` | `/openspec-apply-change` | `.lingma/commands/opsx/` | `/opsx:apply` |
| MiniMax Code | `minimax-code` | `~/.minimax/skills/` (global) | `/openspec-apply-change` | none | none |
| Mistral Vibe | `vibe` | `.vibe/skills/` | `/openspec-apply-change` | none | none |
| Oh My Pi | `oh-my-pi` | `.omp/skills/` | `/openspec-apply-change` | `.omp/commands/` | `/opsx-apply` |
| OpenCode | `opencode` | `.opencode/skills/` | `/openspec-apply-change` | `.opencode/commands/` | `/opsx-apply` |
| Pi | `pi` | `.pi/skills/` | `/openspec-apply-change` | `.pi/prompts/` | `/opsx-apply` |
| Qoder | `qoder` | `.qoder/skills/` | `/openspec-apply-change` | `.qoder/commands/opsx/` | `/opsx:apply` |
| Qwen Code | `qwen` | `.qwen/skills/` | `/openspec-apply-change` | `.qwen/commands/` | `/opsx-apply` |
| Trae | `trae` | `.trae/skills/` | `/openspec-apply-change` | `.trae/commands/` | `/opsx-apply` |
| ZCode | `zcode` | `.zcode/skills/` | `/openspec-apply-change` | `.zcode/commands/opsx/` | `/opsx:apply` |
| Zoo Code | `roocode` | `.roo/skills/` | `/openspec-apply-change` | `.roo/commands/` | `/opsx-apply` |
| Other / Universal | `agents` | `.agents/skills/` | `/openspec-apply-change` | none | none |
- **Skill invocation**: whether a tool registers skills as typed entries is the tool's
own behavior. The column shows the spelling OpenSpec uses in generated files and in
the hint init prints. Check your tool's docs if typing it does nothing.
- **Command file formats**: most tools take `.md` command files. Gemini CLI takes
`.toml`, Continue `.prompt`, Kiro and GitHub Copilot `.prompt.md`. The spelling you
type is the same either way.
## Per-tool notes
A tool not listed here behaves exactly as its row reads.
### Antigravity
- **Current folder**: Antigravity v1.20.5 and later read workspace skills and
workflows from `.agents/`.
- **Legacy folder**: after OpenSpec writes replacements, it removes equivalent
generated files from `.agent/`. Custom files and changed generated files stay in
`.agent/` for you to review.
- **Shared skills**: Antigravity shares `.agents/skills/` with Codex, Zed Agent, and
the `agents` target. OpenSpec writes that skill tree once while still writing
Antigravity commands to `.agents/workflows/`.
### Cline
Cline reads commands from `.clinerules/workflows/`, not from its `.cline/` folder.
Skills stay in `.cline/skills/`.
### Codex
- **CLI and IDE extension**: mention `$openspec-propose` with your idea, or run
`/skills` to select the skill. Codex does not recognize `/openspec-propose`
([upstream issue](https://github.com/openai/codex/issues/11817)).
- **Desktop app**: open Skills in the sidebar and select `openspec-propose`.
[OpenAI's skills documentation](https://learn.chatgpt.com/docs/build-skills)
describes both interfaces.
- **No command files**: Codex runs skills directly, so init skips commands even when
delivery includes them and prints `Commands skipped for: codex (uses skills)`.
- **Shared folder**: Codex skills land in `.agents/skills/`, the same tree Antigravity,
Zed Agent, and the `agents` target use. Selecting more than one keeps a single
compatible tree, and its handoffs spell both `$openspec-*` and `/openspec-*` when
Codex owns it.
- **Legacy path**: skills installed under `.codex/skills/` by older versions are
migrated on the next `openspec update`.
### Devin Desktop (formerly Windsurf)
- **Two agents**: command files in `.devin/workflows/` work only in Devin Desktop.
Devin Local runs skills only, so generated skills reference `/openspec-<skill>`,
which works in both.
- **Rename**: `--tools windsurf` still resolves to `devin`. A project holding
OpenSpec files in the legacy `.windsurf/` folder is offered the move on the next
`openspec update`.
### GitHub Copilot
- **IDE extensions (command delivery)**: VS Code, JetBrains, and Visual Studio load
`.github/prompts/opsx-<id>.prompt.md` as `/opsx-<id>`. If a command disappears
while its file still exists, restart the IDE.
- **Copilot CLI (skill delivery)**: the CLI ignores `.github/prompts/` and loads
`.github/skills/openspec-*/SKILL.md` instead. Invoke a skill as
`/openspec-<skill>`. If a skill disappears while its file still exists, run
`/skills reload`, then `/skills info openspec-propose` to confirm discovery.
### Hermes Agent
Hermes loads skills only from `~/.hermes/skills/` by default. Add the project's
`.hermes/skills/` folder to `skills.external_dirs` in `~/.hermes/config.yaml`;
init prints this reminder after install.
### MiniMax Code
- **Global only**: skills go to `~/.minimax/skills/`. Nothing is written inside
the repo.
- **Safe across projects**: a commands-only delivery leaves the global skills in
place, so one project's setting cannot remove skills another project uses.
### Other / Universal (shared `.agents` skills)
- **When it fits**: any tool that reads the shared `.agents/skills/` folder,
including tools with no row in the matrix. It is the entry to pick when your
assistant is not listed. The init picker's search box finds it by `universal`,
`other`, `generic`, `custom`, `proprietary`, `unlisted`, `unsupported`,
`vendor-neutral`, or `agents.md`.
- **Alongside other targets**: Antigravity, Codex, Zed Agent, and this target share
one physical skill tree. OpenSpec records one writer in `.openspec-target` and
writes the tree once per run. Each tool's separate command files are still
generated.
- **What OpenSpec claims**: only the `openspec-*` folders and the
`.openspec-target` marker. Anything else under `.agents/` is left alone.
- **`AGENTS.md`**: not created or edited. The target is the `.agents/` folder, not
the file.
+45
View File
@@ -0,0 +1,45 @@
# Where every current page goes
The old-to-new mapping: the source material for each `docs-lab/` page while drafting,
and the redirect list at cutover. The target structure is the page index in
[README.md](README.md).
| Current (`docs/`) | Destination |
|---|---|
| README.md (index) | `start/overview.md`, rewritten as pitch and routing |
| getting-started.md | `start/quickstart.md` |
| installation.md | split: `start/installation.md` (machine-level: matrix, update, uninstall) · `start/setup.md` (project-level: init, what init writes, skills-vs-commands delivery, stores router) |
| how-commands-work.md | `start/quickstart.md` (inline labels) · `help/faq.md` · `help/troubleshooting.md` |
| existing-projects.md | `guides/existing-codebases.md` ("Existing codebases"); walkthrough half to `start/quickstart.md` |
| overview.md | `guides/concepts.md` |
| concepts.md | `guides/concepts.md` (core) · delta format to `reference/schemas/spec-driven/index.md` (Delta specs section) · embedded glossary table deleted |
| explore.md | `guides/explore.md` |
| workflows.md | `guides/apply.md` (execution patterns, continue/ff) · `reference/skills.md` |
| opsx.md | split four ways: config to `customize/project-config.md` · commands to `reference/skills.md` · philosophy to `guides/concepts.md` · architecture to `reference/architecture/` |
| reviewing-changes.md + writing-specs.md | `guides/review-the-plan.md` (merged) |
| editing-changes.md | `guides/change-course.md` |
| team-workflow.md | `guides/teams.md` |
| examples.md | parked: `guides/examples.md` skeleton kept off the index and sync config until real archived changes exist (see README TODOs) |
| customization.md | `customize/project-config.md` + `customize/schemas.md` + `customize/overview.md` (decision ladder) · schema.yaml fields to `reference/schemas/schema-yaml.md` |
| multi-language.md | `customize/project-config.md` §context, the "Another language" note |
| stores-beta/user-guide.md | `multi-repo/stores.md` · worksets section to `multi-repo/worksets.md` |
| commands.md | `reference/skills.md` (legacy `/openspec:*` section removed) |
| cli.md | `reference/cli.md` (minus install, which moves to `start/installation.md`) |
| supported-tools.md | `reference/supported-tools.md` |
| glossary.md | `reference/glossary.md` |
| faq.md | `help/faq.md` (unpublished-model claim deleted; update/uninstall to `start/installation.md`) |
| troubleshooting.md | `help/troubleshooting.md`, canonical home for all 5 copies, plus Getting help |
| migration-guide.md | `help/legacy/migration.md` (demoted) |
| agent-contract.md | **off-site**, to repo-side contributor docs |
New pages with no single current source: `customize/overview.md`, `customize/profiles.md`
(today: scattered two-line fragments across 12 pages), and the
`reference/schemas/` and `reference/configuration/` sections (which replaced the
planned `reference/file-formats.md`).
## Cutover
Point `website/docs.sync.config.mjs` here, add old-to-new redirects in
`website/public/_redirects`, and verify `llms.txt` / `llms-full.txt` /
per-page markdown routes. `docs/` stays in place, untouched. The site just
stops reading it.
+168
View File
@@ -0,0 +1,168 @@
# Installation
> Install the `openspec` CLI on your machine, update it, and uninstall it.
## Prerequisites
OpenSpec runs on Node.js 20.19.0 or newer. Homebrew installs Node.js as a
dependency, and the Nix package includes the runtime. Check your installed version
before using another install method.
In your terminal:
```bash
node --version
```
If that prints `v20.19.0` or higher, you're set. If not, install a newer Node from
[nodejs.org](https://nodejs.org) or through your version manager (nvm, fnm, asdf,
volta). You can skip this check when you install with Homebrew or Nix.
The workflow itself runs inside an AI coding tool: Claude Code, Cursor, or any other tool on the [supported list](../reference/supported-tools.md).
## Install with your AI assistant
Paste this into your AI chat:
```text
Fetch https://raw.githubusercontent.com/Fission-AI/OpenSpec/main/install.md and follow it.
```
Or, in your terminal, pipe it into a CLI agent (Claude Code shown):
```bash
curl -fsSL https://raw.githubusercontent.com/Fission-AI/OpenSpec/main/install.md | claude
```
That fetches [install.md at the repo root](https://github.com/Fission-AI/OpenSpec/blob/main/install.md), a prompt written for any agent that can run shell commands (a few IDE integrations can't). Expect your assistant to:
1. Check your Node version, and stop if it's older than 20.19.0.
2. Skip the install if the CLI is already on your machine. Otherwise, show you the install command and wait for your confirmation before running it.
3. Verify `openspec` is on your PATH.
4. Name the folder it thinks you mean, suggest the AI tool you're already talking to, and ask which others you use, then run `openspec init` there (the [project setup](setup.md) step).
5. Report what init created and the exact spelling to invoke OpenSpec in your tool.
It stops before anything privileged and never edits your shell startup files. The [manual methods below](#install-methods) are the source of truth, and the prompt runs them for you.
This install method is new and can have varying results depending on model used. Only use if you're comfortable correcting AI mistakes. Otherwise we recommend following the standard method below.
## Install methods
Install the CLI globally; [setting up your project](setup.md) comes after.
In your terminal:
```npm
npm install -g @fission-ai/openspec@latest
```
### Homebrew
Homebrew installs OpenSpec and its Node.js dependency on macOS or Linux. In your terminal:
```bash
brew install openspec
```
The formula is published in [homebrew-core](https://formulae.brew.sh/formula/openspec), so you don't need to add a tap.
### Yarn
`yarn global add` is Yarn Classic (1.x) only. Modern Yarn removed global installs, so use npm, pnpm, or bun instead. A global CLI doesn't have to share your project's package manager.
### Bun
Bun installs OpenSpec but doesn't run it, so you still need Node on your machine (the [prerequisite](#prerequisites) above). Without it, every command fails with `env: node: No such file or directory`. Bun treats [every Node CLI](https://bun.com/docs/pm/bunx#shebangs) this way.
### Deno
Deno installs the CLI from npm and needs explicit permission flags. In your terminal:
```bash
deno install --global \
--allow-read --allow-write --allow-env --allow-sys=cpus,homedir --allow-net=edge.openspec.dev \
npm:@fission-ai/openspec@latest
```
Some commands launch another program: [`openspec config edit`](../reference/cli.md) opens your editor. Deno interrupts those with a permission prompt on every run. To stop it asking, add a scoped `--allow-run=<program>` to the install command.
> [!NOTE]
> If Deno can't resolve `@latest`, pin a version range instead: `npm:@fission-ai/openspec@^1.7.0`.
### Nix
The OpenSpec repo ships a Nix flake. Install it into your profile. In your terminal:
```bash
nix profile install github:Fission-AI/OpenSpec
```
Or run a one-off command first, without installing:
```bash
nix run github:Fission-AI/OpenSpec -- --version
```
That leaves nothing on your PATH, so there's no install to check afterward.
To put OpenSpec in a project dev shell instead, add the flake as an input and use its default package; [flake.nix](https://github.com/Fission-AI/OpenSpec/blob/main/flake.nix) lists the outputs.
The Nix package ships the Bash, Fish, and Zsh completion scripts at the standard
locations (`share/bash-completion/completions`, `share/fish/vendor_completions.d`,
`share/zsh/site-functions`), so they load with the package and there is no need to run
`openspec completion install`.
### Check it worked
Whichever method you used, in your terminal:
```bash
openspec --version
```
If that prints a version number, the CLI is on your PATH. It installs once per machine.
Next, [set up your project](setup.md). If your assistant already ran init, that page shows what it wrote and how to adjust it.
## Updating
In your terminal, in each project where you ran init:
```bash
openspec update
```
When a newer CLI is out, [`openspec update`](../reference/cli.md#openspec-update) says so and can install it for you; that upgrade is once per machine. Every run refreshes the project's generated skills and commands, which never update on their own. A current project prints `✓ All 2 tool(s) up to date (v1.7.0)`.
> [!WARNING]
> On Homebrew, run `brew upgrade openspec`. On Deno, re-run the [Deno install](#deno) with `-f`; it won't overwrite the installed command without it. On Nix, use `nix profile upgrade openspec`.
> [!NOTE]
> A global npm install belongs to one Node installation. Switch Node versions with nvm and the `openspec` command doesn't come along, so install it again under the new version.
## Uninstalling
To uninstall OpenSpec, run through the steps below; none of them touch your source code. You can also point your agent at this section and let it handle the removal.
**1. Remove [shell completions](../reference/cli.md#openspec-completion)**, if you set them up, while the CLI can still do it. In your terminal:
```bash
openspec completion uninstall
```
**2. Remove the package.** In your terminal:
```npm
npm uninstall -g @fission-ai/openspec
```
On Homebrew: `brew uninstall openspec`. On Deno: `deno uninstall --global openspec`. On Nix: `nix profile remove openspec`. Your shell should no longer find `openspec`.
**3. Delete what's left, or keep it.**
- Generated agent files: `openspec-*` skills and `opsx` commands under directories like `.claude/` or `.agents/`, per project. [Supported tools](../reference/supported-tools.md) lists each tool's paths; MiniMax Code keeps skills in `~/.minimax/skills`.
- Leftovers from older versions: marker blocks in `CLAUDE.md` or `AGENTS.md` (delete the block, keep the file) and `opsx-*.md` prompts in `~/.codex/prompts`.
- The `openspec/` folder: pause first. `specs/` and `changes/archive/` are your record of the system, plain Markdown that reads fine without OpenSpec.
- Per-machine state: settings and the telemetry id in `~/.config/openspec/`; schema overrides and store registrations in `~/.local/share/openspec/` (Windows: `%APPDATA%\openspec`, `%LOCALAPPDATA%\openspec`). Registrations are pointers; the store repos they point to are untouched.
+14
View File
@@ -0,0 +1,14 @@
# Overview
TODO: this page is being rewritten from scratch.
<!-- Emptied 2026-08-21. The previous skeleton (section headings, narrative beats, and
the diagram-options gallery) was cleared so the page starts clean. The old goal line
("OpenSpec gives you and your coding agent a shared, reviewable plan before code is
written") was dropped as too weak a pitch: the rewrite should sell keeping larger
features on track and aligned (teams, git-native artifacts, intended behavior matching
implemented behavior, the control-loop framing). The brief is in docs-lab/Notes.md
under "Start > Overview". The diagram candidates (docs-lab/diagrams/) were deleted
with the gallery; recover them from git history if the rewrite wants a starting point.
README rules that still bind the rewrite: the loop appears here as pitch only, copy and
no explanation; the quickstart is its one teacher. -->
+174
View File
@@ -0,0 +1,174 @@
# Quickstart
> Your first change, from idea to archived, in a new or existing project.
Before you start, you need the CLI on your machine ([Installation](installation.md)) and OpenSpec initialized in your project ([Set up your project](setup.md)).
## Start from an empty project
You can start without a chosen stack or a complete architecture. Initialize OpenSpec in your project folder, then ask your agent to explore the options with you. In your AI chat:
```text
Help me explore a task tracker from scratch. I have not picked a stack. Compare the options and help me choose the first behavior to build.
```
Decide what the first change needs and leave later architecture choices open. Ask your agent to propose that one change, then follow the steps below. You can revisit the architecture as the project grows.
## The loop at a glance
Every change moves through the same five steps: you think the idea through with your agent, it drafts a plan, you correct the plan before any code exists, the agent builds from it, and archiving updates your specs with what shipped.
```mermaid
flowchart LR
explore["1 · Explore<br/>think it through together"] --> propose["2 · Propose<br/>agent drafts the plan"]
propose --> review["3 · Review<br/>you correct the plan"]
review --> apply["4 · Apply<br/>agent builds, task by task"]
apply --> archive["5 · Archive<br/>specs absorb the change"]
archive -. "next change" .-> explore
```
Every prompt below goes in your AI chat, the same place you ask for code. The examples use plain language so they work across tools. You can also invoke a skill directly; the syntax varies by tool ([supported tools](../reference/supported-tools.md)).
## Step 1: Explore
Think the idea through with your agent before you ask for a plan. In your AI chat:
```text
Help me explore how rate limiting should work in this app.
```
Explore is a thinking mode. The agent investigates your codebase, asks the questions that matter, sketches options, and challenges assumptions. It never writes code. It writes nothing else unless you ask it to capture what you decided, or say yes when it offers. The output is a sharper idea.
Stay here as long as the problem needs. When the shape feels right, hand it off:
```text
Propose the change we just discussed.
```
That line starts propose for you, carrying everything you settled. Skip the first prompt in step 2.
## Step 2: Propose
Propose turns the idea into a reviewable plan. Coming from explore, it's already running. Starting cold, when the change is clear in your head, ask directly. In your AI chat:
```text
Propose a change to add rate limiting.
```
The agent asks what it needs to, then writes a change folder:
```
openspec/changes/add-rate-limiting/
├── proposal.md why, and what changes
├── specs/ what "done" means, as testable requirements
├── design.md technical decisions (only when the change needs one)
└── tasks.md the implementation checklist
```
No code yet. Propose stops at the plan.
## Step 3: Review and correct the plan
Fix the plan while it's still words and nothing is built yet. Read in this order:
- **`proposal.md`**: is this the right problem, at the right size?
- **`specs/`**: the highest-value read. Would you accept these requirements as done?
- **`tasks.md`**: do the tasks cover the specs, and nothing more?
To fix something, either works:
- Edit the file yourself. The artifacts are plain markdown, and the files are the plan.
- Tell your agent what's wrong ("the spec is missing the unauthenticated case"). It revises the artifacts.
## Step 4: Apply
Apply turns the plan into code. Start a fresh chat session, since implementation goes better on a clean context window. In your AI chat:
```text
Apply the add-rate-limiting change.
```
The agent reads the change folder, then works through `tasks.md`, checking off each task as it lands.
- **Interrupted, or out of context?** Open a new session and ask it to apply again. It resumes at the first unchecked task.
- **Plan turned out wrong?** Fix the artifacts (either way from step 3), then continue applying.
- **Progress** lives in the `tasks.md` checkboxes. There is no hidden state.
## Step 5: Archive
Archiving does two things: it updates your main specs with the change's requirements, and it moves the change folder into the archive folder (in `/openspec/changes/archive/*`).
When every box in `tasks.md` is checked, in your AI chat:
```text
Archive the add-rate-limiting change.
```
Step through what archiving does:
```file-steps
## The finished change
> Implementation is done. The delta spec (what this change adds) still sits inside the change folder; specs/ doesn't know about rate limiting yet.
openspec/
├── specs/ (no rate-limiting spec yet)
└── changes/
└── add-rate-limiting/
├── proposal.md
├── tasks.md every box checked
└── specs/
└── rate-limiting/
└── spec.md the delta: ADDED requirements
## Requirements land in specs/
> Each requirement in the delta lands in the main spec: added ones append, modified ones replace their old version. A new capability gets a new spec file.
openspec/
├── specs/
+ │ └── rate-limiting/
+ │ └── spec.md gains "Requirement: Rate limiting"
└── changes/
└── add-rate-limiting/
└── specs/
└── rate-limiting/
└── spec.md the delta, source of the merge
## The folder moves to archive/
> The whole change folder, delta included, moves into the archive under a date prefix. Nothing is deleted.
openspec/
├── specs/
│ └── rate-limiting/
│ └── spec.md
└── changes/
- └── add-rate-limiting/
+ └── archive/
+ └── 2026-08-08-add-rate-limiting/
+ ├── proposal.md
+ ├── tasks.md
+ └── specs/rate-limiting/spec.md
## Specs describe the system as built
> changes/ is clear for the next change. specs/ is the source of truth for what the system does; archive/ is the history of how it got there.
openspec/
├── specs/
│ └── rate-limiting/
│ └── spec.md the spec as built
└── changes/
└── archive/
└── 2026-08-08-add-rate-limiting/
```
Git is a separate concern. Commit the change folder with the code, and nothing else about your workflow changes.
## Going further
- [Delta specs](../reference/schemas/spec-driven/index.md#delta-specs-specmd): how to write the behavior changes in a delta spec.
- [Profiles](../customize/profiles.md): optional workflows beyond the core set (verify before archive, incremental planning).
## Advanced guides
<!-- Planned pages, not yet written or in the README page map. Listed here so the quickstart routes to them once they exist. -->
Not written yet; guides we plan to add:
- **Prototype first**: spike the code before any spec, then backfill the proposal from what the prototype taught you.
- **Building iteratively**: a sequence of small changes instead of one big proposal.
- **Revising an implemented change**: the plan needs to move again after apply, but the change hasn't merged or archived yet.
+137
View File
@@ -0,0 +1,137 @@
# Set up your project
> Add OpenSpec to a project: run init, see what it wrote, and adjust it.
## Pick where OpenSpec lives
- **In your repo (the default)**: specs and changes sit next to the code they describe and are versioned with it. The rest of this page follows this path.
- **In a store**: a separate planning repo shared by the repos that use it, for multi-repo setups or keeping planning out of the repo entirely. [Stores (beta)](../multi-repo/stores.md) covers when that's worth it and how to set one up.
## Initialize your project
With the CLI installed ([Installation](installation.md)), run init at the root of your project. In your terminal:
```bash
cd <your-project>
openspec init
```
Init asks which AI tools you use, writes the workflow files for the ones you pick, and reports what you got:
```
OpenSpec Setup Complete
Created: Claude Code
6 skills and 6 commands in .claude/
Config: openspec/config.yaml (schema: spec-driven)
```
Restart your IDE for the new commands to take effect.
Re-running init is safe:
- Tools you already set up print `Refreshed` instead of `Created`.
- Running init again with a new tool selected adds that tool.
- The `--tools` flag skips the picker ([CLI reference](../reference/cli.md)).
## What init installs
Running init creates two things in your project:
- An `openspec/` folder at the repo root
- Workflow files (skills and commands) added to your AI tool's folder (`.agents/`, `.claude/`, etc.)
Commit all of it like the rest of your source. Init changes nothing else in your repo (if it finds leftovers from an older OpenSpec version, it asks before cleaning them up).
### The `openspec/` folder
Every OpenSpec artifact lives here, at the root of your project. Here's what that looks like:
```
openspec/
├── config.yaml project settings and context for the AI
├── specs/ your specs (empty for now)
└── changes/ in-motion changes (empty for now)
└── archive/ completed changes move here
```
[Project config](../customize/project-config.md) covers `config.yaml`.
### The workflow files (skills and commands)
These are the OpenSpec workflows, the actions you'll use as you work. Here they are as installed skills, in the shared `.agents/` folder most tools use:
```
.agents/skills/
├── openspec-explore/ think through an idea first
├── openspec-propose/ propose a change
├── openspec-apply-change/ implement a change's tasks
├── openspec-update-change/ revise a change's plan
├── openspec-sync-specs/ sync a change's spec updates into specs/
├── openspec-archive-change/ move a finished change to the archive
├── openspec-verify-change/ check the implementation matches the plan (not included by default)
└── openspec-bulk-archive-change/ archive several changes at once (not included by default)
```
This is the default set plus two optional workflows. [Profiles](../customize/profiles.md) lists all twelve.
By default each workflow installs in two forms:
- **Skill** (`openspec-apply-change`): instructions your agent picks up on its own when you ask for the work.
- **Command** (`/opsx:apply` in Claude Code): a typed entry point for the same workflow, under a shorter name.
The two are functionally identical. A workflow's skill and its command carry the same instructions.
Why two: commands came first, and every tool spells them its own way. Skills are the newer standard shared across tools, but not every tool can invoke a skill directly, so commands stay as those tools' entry point.
Some tools install in skill form only. Where the tool runs skills directly, init skips commands and says so (`Commands skipped for: codex (uses skills)`).
We prefer skills and expect to retire commands eventually.
#### Change what gets installed
The interactive picker changes the delivery form and the workflow set ([Profiles](../customize/profiles.md)). In your terminal:
```bash
openspec config profile
```
Here's switching to skills only:
```
Current profile settings
Delivery: both
? What do you want to configure? Delivery only
? Delivery mode (how workflows are installed): Skills only
Config changes:
delivery: both -> skills
? Apply changes to this project now? (Y/n) y
```
Answering yes applies it to the current project on the spot. Other projects pick it up on their next `openspec update`. The setting is global, per machine.
#### Claude Code doesn't show the workflows
Claude Code loads OpenSpec workflows from one or both of these project paths, based on your delivery setting:
- **Skills**: `.claude/skills/openspec-*/SKILL.md`
- **Commands**: `.claude/commands/opsx/<id>.md`
If the files are missing, refresh the project. In your terminal:
```bash
openspec update
```
If the command files exist but `/opsx:` shows no OpenSpec commands, update Claude Code and restart it. If commands still don't load, enable skills too. In your terminal:
```bash
openspec config set delivery both
openspec update
```
Restart Claude Code, then run `/openspec-propose` in its chat. If only some workflows are missing, [change your profile](../customize/profiles.md#expanding-the-set-optional-workflows).
Setup is done. The [Quickstart](quickstart.md) takes your first change from here.
+115
View File
@@ -0,0 +1,115 @@
# OpenSpec Documentation
Welcome. This is the home for everything OpenSpec.
OpenSpec helps you and your AI coding assistant **agree on what to build before any code is written.** You describe the change, the AI drafts a short spec and a task list, you both look at the same plan, and then the work happens. No more discovering halfway through that the AI built the wrong thing.
If you read nothing else, read these two pages:
1. [Getting Started](getting-started.md): install, initialize, and ship your first change.
2. [How Commands Work](how-commands-work.md): where you actually type `/opsx:propose` (hint: in your AI chat, not the terminal). This trips up almost everyone once.
That second one matters more than it looks. OpenSpec has two halves: a command line tool you run in your terminal, and slash commands you give to your AI assistant. Knowing which is which saves you the most common moment of confusion.
> **The best habit to build first: when you're not sure what to build, start with `/opsx:explore`.** It's a no-stakes thinking partner that reads your code, weighs options, and sharpens a fuzzy idea into a concrete plan before any code gets written. The [Explore First](explore.md) guide makes the case.
## Pick your path
**I'm brand new.** Start with [Getting Started](getting-started.md), then skim the [Core Concepts at a Glance](overview.md). When something feels mysterious, the [FAQ](faq.md) and [Glossary](glossary.md) are nearby.
**I have a problem but not a plan.** This is the common case, and it has a dedicated answer: [Explore First](explore.md). Use `/opsx:explore` to think it through with the AI before committing to anything.
**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. 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.
**Something's broken.** [Troubleshooting](troubleshooting.md) collects the failures people actually hit, with fixes.
## The whole map
### Start here
| Doc | What it gives you |
|-----|-------------------|
| [Getting Started](getting-started.md) | Install, initialize, and run your first change end to end |
| [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, a prompt that hands setup to your AI assistant, and how to verify it worked |
### Use it day to day
| Doc | What it gives you |
|-----|-------------------|
| [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 |
| [CLI](cli.md) | Reference for every `openspec` terminal command |
### Understand it deeply
| Doc | What it gives you |
|-----|-------------------|
| [Concepts](concepts.md) | The long-form explanation of specs, changes, artifacts, schemas, and archive |
| [OPSX Workflow](opsx.md) | Why the workflow is fluid instead of phase-locked, plus an architecture deep dive |
| [Glossary](glossary.md) | Every term defined in one place |
### Make it yours
| Doc | What it gives you |
|-----|-------------------|
| [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 30+ AI tools OpenSpec integrates with, and where files land |
| [Community Showcase](community.md) | Projects and resources built with and for OpenSpec |
### When you need help
| Doc | What it gives you |
|-----|-------------------|
| [FAQ](faq.md) | Quick answers to the questions people ask most |
| [Troubleshooting](troubleshooting.md) | Concrete fixes for concrete failures |
| [Migration Guide](migration-guide.md) | Moving from the legacy workflow to OPSX |
### Coordinate across repos (beta)
| Doc | What it gives you |
|-----|-------------------|
| [Stores: User Guide](stores-beta/user-guide.md) | Plan in its own repo when your work spans repos or teams |
| [Agent Contract](agent-contract.md) | The machine-readable CLI surfaces agents drive |
## The thirty-second version
```text
1. Install npm install -g @fission-ai/openspec@latest
2. Initialize cd your-project && openspec init
3. Explore (in your AI chat) /opsx:explore ← optional, but a great habit
4. Propose (in your AI chat) /opsx:propose add-dark-mode
5. Build (in your AI chat) /opsx:apply
6. Archive (in your AI chat) /opsx:archive
```
Steps 1 and 2 happen in your terminal. The rest happen in your AI assistant's chat. That split is the one thing worth memorizing, and [How Commands Work](how-commands-work.md) explains exactly why. Step 3 is optional, but starting with `/opsx:explore` when you're unsure is the habit most worth forming.
## Where else to get help
- **Discord:** [discord.gg/YctCnvvshC](https://discord.gg/YctCnvvshC) for questions, ideas, and help.
- **GitHub Issues:** [github.com/Fission-AI/OpenSpec/issues](https://github.com/Fission-AI/OpenSpec/issues) for bugs and feature requests.
- **`openspec feedback "your message"`** sends feedback straight from your terminal (it opens a GitHub issue).
Found something in these docs that's wrong, stale, or confusing? That's a bug. Open an issue or a PR. Documentation improvements are some of the most valuable contributions you can make.
+146
View File
@@ -0,0 +1,146 @@
# OpenSpec Agent Contract
Machine-readable surfaces of the `openspec` CLI, verified against `src/` (capstone audit, 2026-06-11). Every shape below is documented from the emitting code.
## 1. General conventions
- **One JSON document per invocation.** In `--json` mode, stdout carries exactly one JSON document (2-space pretty-printed). Human prose, spinners, and the store banner go to stderr.
- **Store banner.** In human mode, a store-selected root prints `Using OpenSpec root: <id> (<path>)` to stderr. Never printed in JSON mode.
- **Key casing is surface-dependent** (see Known inconsistencies): store/doctor/context payloads use `snake_case`; workflow payloads (`status`, `instructions`, `new change`, `validate`, `list`) use `camelCase`, except the embedded `root` object, which always uses `store_id`.
- **Optional keys are omitted, not null**, in most payloads (e.g. `root.store_id`, `member.path`). Exceptions that use explicit `null` are called out per shape (store doctor `git.*`, failure payloads).
## 2. The diagnostic envelope
One envelope shape is shared by every machine-readable diagnostic (`StoreDiagnostic`):
```json
{
"severity": "error" | "warning" | "info",
"code": "snake_case_string",
"message": "human sentence",
"target": "dotted.surface (optional)",
"fix": "one actionable sentence/command (optional)"
}
```
Diagnostics appear in two positions: **status arrays** (`status: StoreDiagnostic[]` at top level or per entry) for health findings, and **thrown errors** converted to a single-element `status` array on command failure.
## 3. Root selection and `RootOutput`
All root-resolving commands (`list`, `show`, `validate`, `status`, `instructions`, `instructions apply`, `instructions archive`, `new change`, `archive`, `doctor`, `context`, `schemas`) 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 + 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: commands may treat the cwd as `source: "implicit"`; `doctor`, `context`, `list`, and bulk `validate` instead fail with `no_openspec_root`. `list` preserves the implicit fallback for legacy projects with `openspec/project.md`.
Successful JSON payloads normally embed the root; successful `schemas --json`
deliberately remains the compatibility bare array documented in §4.13:
```json
"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.
## 4. Command JSON shapes
### 4.1 `list --json`
`{ "changes": [ { "name", "completedTasks", "totalTasks", "lastModified", "status": "no-tasks"|"complete"|"in-progress", "nested"?: ["<area>/<name>", ...] } ], "warnings"?: [ { "code", "name", "nested", "message" } ], "root": RootOutput }` — note the per-change `status` is a string enum here. `--specs`: `{ "specs": [ { "id", "requirementCount" } ], "root" }`.
`warnings` (omitted when empty) reports directories under `changes/` that are not changes. Today the only code is `nested_change_directory`: a namespace folder wrapping change directories, which OpenSpec cannot address because a change is always a directory directly under `changes/`. The same entry carries `nested` on the listed change, whose `status` is then meaningless. Do not treat such an entry as a change; report the message and leave the directories alone.
### 4.2 `show <item> --json`
Change: `{ "id", "title", "deltaCount", "deltas": [...], "root" }`. Spec: `{ "id", "title", "overview", "requirementCount", "requirements": [...], "metadata": { "version", "format", "sourcePath"? }, "root" }`.
### 4.3 `validate --json`
`{ "items": [ { "id", "type": "change"|"spec", "valid", "issues": [ { "level", "path", "message", "line"?, "column"? } ], "durationMs" } ], "summary": { "totals": {items,passed,failed}, "byType": {...} }, "version": "1.0", "root" }`. Exit 1 when any item fails.
### 4.4 `status --json`
`{ "changeName", "schemaName", "planningHome"?: { "kind", "root", "changesDir", "defaultSchema" }, "changeRoot", "artifactPaths": { "<id>": {outputPath, resolvedOutputPath, existingOutputPaths} }, "nextSteps": ["..."], "actionContext": { "mode": "repo-local", "sourceOfTruth": "repo", "planningArtifacts", "linkedContext", "allowedEditRoots", "requiresAffectedAreaSelection", "constraints" }, "isPlanningComplete", "isComplete", "applyRequires", "artifacts": [ {id, outputPath, status: "done"|"skipped"|"ready"|"blocked", requires, missingDeps?} ], "root" }`. `isPlanningComplete` means every non-skipped planning artifact exists; skipped artifacts count as satisfied without being created. It does not mean implementation tasks are complete. `isComplete` is retained as a compatibility alias with the same value. Each artifact's `requires` is its direct dependency ids (present for every status, so the transitive required set is computable even when the artifact is `done`); `missingDeps` appears only when `blocked`. The `artifacts` array is in dependency order, with the schema's `artifacts:` declaration order breaking ties between artifacts that become ready at the same time (never alphabetical), so the first `ready` entry is the artifact to write next; `missingDeps` uses that same order. `"skipped"` marks an artifact whose `generates` path is under `specs/` in a change whose `.openspec.yaml` declares `skip_specs: true`; it satisfies dependencies but must not be created. No active changes: `{ "changes": [], "message", "root" }`, exit 0.
`--all` (batch, mutually exclusive with `--change` — combining them is an error with the `{ "changes": [], "root": null, "status": [d] }` null-shape): `{ "changes": [ <per-change status object, no per-change root>, ... ], "root" }`, sorted by change name. A change that fails to load contributes `{ "changeName", "status": [d] }` in place; the sweep continues, preserves the complete envelope, and exits 1 in both text and JSON modes. An invalid `--schema` fails the whole invocation with the null-shape, even when no changes exist.
### 4.5 `instructions <artifact> --json`
`{ "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"?, "missingPrerequisites"?, "warnings"?, "instruction", "references"?, "context"?, "operationGuidance"?, "root" }`. `missingArtifacts` is what apply blocks on (the schema's `apply.requires`); `missingPrerequisites` is everything still to build before apply can run, in build order - the transitive closure of those requires, so it can be the longer list. `warnings` lists non-blocking problems with the change itself - today, a change that is ready to implement with no delta specs and no `skip_specs: true`, the state `openspec validate` rejects. Both optional root fields (`context`, `operationGuidance`) are read from the selected root on every invocation. `context` is a required prompt-level input whose relevant project facts, conventions, and constraints must be applied; `operationGuidance` is advisory input whose entries are followed only when applicable and compatible with the built-in workflow. Both remain separate from state, tasks, progress, context files, and the built-in instruction.
### 4.7 `instructions archive --json`
`{ "changeName", "context"?, "operationGuidance"?, "root" }`. Requires a valid `--change` in the resolved repo/store root and uses the same required-context/advisory-guidance semantics as apply. This is a read-only runtime-input surface: it does not return the static archive workflow, inspect or merge delta specs, write main specs, or move the change.
### 4.8 `new change <name> --json`
Success: `{ "change": { "id", "path", "metadataPath", "schema" }, "root" }`. Failure: `{ "change": null, "status": [d] }`, exit 1.
### 4.9 `archive <name> --json`
Success: `{ "archive": { "change", "archivedAs": "YYYY-MM-DD-name", "path", "specsUpdated", "totals"?, "warnings"? }, "root" }`. Failure: `{ "archive": null, "root"?, "status": [d] }`, exit 1. `specsUpdated` is true only when at least one spec file was written or retired (a capability whose last requirement the change removed has its spec deleted, which requires `retire_capabilities: true` in the change's `.openspec.yaml`; every retirement is named in `warnings`, with a pasteable Git recovery command only when the spec lived in the caller's checkout); an already-synced change archives with all-zero totals and the skips listed in `warnings`. JSON mode is strictly non-interactive: every prompt point becomes an `archive_*` code.
### 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.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.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.13 `schemas --json` / `templates --json`
`schemas`: success remains a bare array `[ {name, description, artifacts, source} ]`; it resolves the canonical root-selection precedence and accepts `--store <id>`. Root-selection failure: `{ "schemas": [], "root": null, "status": [d] }`, exit 1. `templates`: keyed object `{ "<artifactId>": {path, source} }`, still cwd-based with no root/status keys.
## 5. Exit-code contract
| Situation | Exit | Stdout |
|---|---|---|
| Success, incl. health findings (doctor/context/store doctor) | 0 | the payload |
| Command failure in `--json` mode | 1 | one JSON document with `status: [d]` and the command's null-shape |
| `validate` with failing items | 1 | full report |
| Prompt cancellation (`store` group, human mode) | 130 | stderr only |
## 6. Diagnostic code catalog
### Resolution
`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_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_root_pointer_declared`, `store_register_root_unhealthy`, `store_register_identity_confirmation_required`, `store_register_cancelled`, `store_remote_empty`, `store_remote_requires_hand_edit`, `store_remove_confirmation_required`, `store_remove_cancelled`, `store_remove_path_not_directory`, `store_remove_metadata_missing`, `store_remove_contains_registered_store`, `store_root_missing` (warning in remove, error in doctor), `store_root_not_directory`.
### Store git
`store_git_init_failed`, `store_git_identity_missing`, `store_git_commit_failed`, `store_git_no_commits` (warning), `store_clone_fragile_directories` (warning), `store_remote_divergence` (info, doctor), `store_checkout_drift` (info, doctor).
### References (warning)
`reference_invalid_id`, `reference_registry_unreadable`, `reference_unresolved`, `reference_root_unhealthy`, `reference_index_truncated`.
### Relationships (warning; doctor; context keeps only the registry one)
`relationship_registry_unreadable`, `root_pointer_ignored`, `root_pointer_invalid`, `pointer_declarations_inert`.
### Archive (JSON mode)
`archive_change_name_required`, `archive_change_not_found`, `archive_change_symlink`, `archive_validation_failed`, `archive_confirmation_required`, `archive_tasks_incomplete`, `archive_spec_update_failed`, `archive_spec_validation_failed`, `archive_target_exists`, `archive_error`.
### Context writes
`context_file_exists`, `context_output_dir_missing`.
### Fallbacks
`doctor_failed`, `context_failed`, `store_error`, `change_error`, `archive_error`.
## Known inconsistencies
Recorded by the capstone audit; published-key renames are product decisions deferred past this release:
1. ~~In `--json` mode, several failure paths printed stderr only with no JSON document.~~ Fixed in the capstone gauntlet round: `show`/`validate` unknown and ambiguous items emit `{status:[{code: unknown_item | ambiguous_item, ...}]}`; thrown errors in `status`/`instructions`/`list`/`show`/`validate` route through the JSON-aware failure helper (the command's null-shape + `status`); `store <unknown subcommand> --json` emits `{status:[{code: unknown_store_subcommand}]}`; `list` carries its `{changes|specs: [], root: null}` null-shape on resolution failures.
2. `store_root_missing` is emitted with two severities (warning in remove, error in store doctor) — context-dependent, documented above.
3. snake_case (store family) vs camelCase (workflow family) key casing; `root.store_id` is snake_case everywhere.
4. Four parallel envelope type declarations exist in src; archive diagnostics never carry `target`.
5. `list --json` reuses the `status` key as a string enum per change.
6. Only `validate` output carries a `version` field.
7. `templates` ignores root selection (cwd-based, no `--store`).
8. Deprecated noun forms (`change`/`spec` subcommands) emit unenveloped payloads without `root`/`status`.
+1342
View File
File diff suppressed because it is too large Load Diff
+779
View File
@@ -0,0 +1,779 @@
# 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, 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)
| Command | Purpose |
|---------|---------|
| `/opsx:propose` | Create a change and generate planning artifacts in one step |
| `/opsx:explore` | Think through ideas before committing to a change |
| `/opsx:apply` | Implement tasks from the change |
| `/opsx: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 |
### Expanded Workflow Commands (custom workflow selection)
| Command | Purpose |
|---------|---------|
| `/opsx:new` | Start a new change scaffold |
| `/opsx:continue` | Create the next artifact based on dependencies |
| `/opsx:ff` | Fast-forward: create all planning artifacts at once |
| `/opsx:verify` | Validate implementation matches artifacts |
| `/opsx:bulk-archive` | Archive multiple changes at once |
| `/opsx:onboard` | Guided tutorial through the complete workflow |
The default global profile is `core`. To enable expanded workflow commands, run `openspec config profile`, select workflows, then run `openspec update` in your project.
---
## Command Reference
### `/opsx:propose`
Create a new change and generate planning artifacts in one step. This is the default start command in the `core` profile.
**Syntax:**
```text
/opsx:propose [change-name-or-description]
```
**Arguments:**
| Argument | Required | Description |
|----------|----------|-------------|
| `change-name-or-description` | No | Kebab-case name or plain-language change description |
**What it does:**
- Creates `openspec/changes/<change-name>/`
- Generates artifacts needed before implementation (for `spec-driven`: proposal, specs, design, tasks)
- Stops when the change is ready for `/opsx:apply`
**Example:**
```text
You: /opsx:propose add-dark-mode
AI: Created openspec/changes/add-dark-mode/
✓ proposal.md
✓ specs/ui/spec.md
✓ design.md
✓ tasks.md
Ready for implementation. Run /opsx:apply.
```
**Tips:**
- Use this for the fastest end-to-end path
- If you want step-by-step artifact control, enable expanded workflows and use `/opsx:new` + `/opsx:continue`
---
### `/opsx:explore`
> **Start here when you're unsure.** Explore is a no-stakes thinking partner: it reads your codebase, compares options, and sharpens a fuzzy idea into a concrete plan before any code gets written. It ships in the default profile. For the full case and more examples, see the [Explore First](explore.md) guide.
Think through ideas, investigate problems, and clarify requirements before committing to a change.
**Syntax:**
```
/opsx:explore [topic]
```
**Arguments:**
| Argument | Required | Description |
|----------|----------|-------------|
| `topic` | No | What you want to explore or investigate |
**What it does:**
- Opens an exploratory conversation with no structure required
- Investigates the codebase to answer questions
- Compares options and approaches
- Creates visual diagrams to clarify thinking
- Captures the exploration when you ask, or when you say yes to its offer: scaffolds a change with `openspec new change` and writes the planning artifacts you name, or updates an existing change's artifacts
- Can transition to `/opsx:propose` (default) or `/opsx:new` (expanded workflow) when insights crystallize
**Example:**
```text
You: /opsx:explore
AI: What would you like to explore?
You: How should we handle authentication for the mobile app?
AI: Let me investigate your current auth setup...
[Analyzes codebase]
I see you're using session-based auth for the web app.
For mobile, we have three main options:
1. JWT tokens (stateless, works offline)
2. OAuth2 (if you need third-party login)
3. Extend current sessions (simpler, but requires connectivity)
Your API already has CORS configured. Which direction interests you?
You: Let's go with JWT.
AI: That's a decision worth keeping. Want me to start a change called
add-jwt-auth? Just the change folder, nothing else yet.
You: Yes.
AI: Started openspec/changes/add-jwt-auth/. Say the word and I'll
write the proposal, specs, and tasks from what we just worked out.
```
**Tips:**
- Use when requirements are unclear or you need to investigate
- It never writes code, and writes nothing else unless you ask, or say yes when it offers
- Good for comparing multiple approaches before deciding
- Can read files and search the codebase
---
### `/opsx:new`
Start a new change scaffold. Creates the change folder and waits for you to generate artifacts with `/opsx:continue` or `/opsx:ff`.
This command is part of the expanded workflow set (not included in the default `core` profile).
**Syntax:**
```
/opsx:new [change-name] [--schema <schema-name>]
```
**Arguments:**
| Argument | Required | Description |
|----------|----------|-------------|
| `change-name` | No | Name for the change folder (prompted if not provided) |
| `--schema` | No | Workflow schema to use (default: from config or `spec-driven`) |
**What it does:**
- Creates `openspec/changes/<change-name>/` directory
- Creates `.openspec.yaml` metadata file in the change folder
- Shows the first artifact template ready for creation
- Prompts for change name and schema if not provided
**What it creates:**
```
openspec/changes/<change-name>/
└── .openspec.yaml # Change metadata (schema, created date)
```
**Example:**
```
You: /opsx:new add-dark-mode
AI: Created openspec/changes/add-dark-mode/
Schema: spec-driven
Ready to create: proposal
Use /opsx:continue to create it, or /opsx:ff to create all artifacts.
```
**Tips:**
- Use descriptive names: `add-feature`, `fix-bug`, `refactor-module`
- Avoid generic names like `update`, `changes`, `wip`
- Schema can also be set in project config (`openspec/config.yaml`)
---
### `/opsx:continue`
Create the next artifact in the dependency chain. Creates one artifact at a time for incremental progress.
**Syntax:**
```
/opsx:continue [change-name]
```
**Arguments:**
| Argument | Required | Description |
|----------|----------|-------------|
| `change-name` | No | Which change to continue (inferred from context if not provided) |
**What it does:**
- Queries the artifact dependency graph
- Shows which artifacts are ready vs blocked
- Creates the first ready artifact
- Reads dependency files for context
- Shows what becomes available after creation
**Example:**
```
You: /opsx:continue
AI: Change: add-dark-mode
Artifact status:
✓ proposal (done)
◆ specs (ready)
◆ design (ready)
○ tasks (blocked - needs: specs, design)
Creating specs...
[Reads proposal.md, creates specs based on requirements]
✓ Created openspec/changes/add-dark-mode/specs/ui/spec.md
Now available: design
Run /opsx:continue to create the next artifact.
```
**Tips:**
- Use when you want to review each artifact before proceeding
- Good for complex changes where you want control
- Multiple artifacts may become ready simultaneously
- You can edit created artifacts before continuing
---
### `/opsx:ff`
Fast-forward through artifact creation. Creates all planning artifacts at once.
**Syntax:**
```
/opsx:ff [change-name]
```
**Arguments:**
| Argument | Required | Description |
|----------|----------|-------------|
| `change-name` | No | Which change to fast-forward (inferred from context if not provided) |
**What it does:**
- Creates all artifacts in dependency order
- Tracks progress via todo list
- Stops when all `apply-required` artifacts are complete
- Reads each dependency before creating the next artifact
**Example:**
```
You: /opsx:ff add-dark-mode
AI: Fast-forwarding add-dark-mode...
✓ Creating proposal.md
✓ Creating specs/ui/spec.md
✓ Creating design.md
✓ Creating tasks.md
All planning artifacts complete!
Ready for implementation. Run /opsx:apply to begin.
```
**Tips:**
- Use when you have a clear picture of what you're building
- Faster than `/opsx:continue` for straightforward changes
- You can still edit artifacts afterward
- Good for small to medium features
---
### `/opsx:apply`
Implement tasks from the change. Works through the task list, writing code and checking off items.
**Syntax:**
```
/opsx:apply [change-name]
```
**Arguments:**
| Argument | Required | Description |
|----------|----------|-------------|
| `change-name` | No | Which change to implement (inferred from context if not provided) |
**What it does:**
- Reads `tasks.md` and identifies incomplete tasks
- Works through tasks one by one
- Writes code, creates files, runs tests as needed
- Marks tasks complete with checkboxes `[x]`
**Example:**
```
You: /opsx:apply
AI: Implementing add-dark-mode...
Reading tasks.md:
- [ ] 1.1 Create ThemeContext
- [ ] 1.2 Add CSS custom properties
- [ ] 1.3 Implement localStorage persistence
Working on 1.1: Create ThemeContext...
[Creates src/context/ThemeContext.tsx]
✓ 1.1 Complete
Working on 1.2: Add CSS custom properties...
[Updates src/styles/globals.css]
✓ 1.2 Complete
...
```
**Tips:**
- Can resume where you left off if interrupted
- Use for parallel changes by specifying the change name
- Completion state is tracked in `tasks.md` checkboxes
---
### `/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` (unstarted artifacts), `/opsx:apply` (carry a revised plan into code), or `/opsx:archive` (all done)
**Missing files:**
- For a glob artifact such as `specs/**/*.md` with at least one existing file, update can propose a missing companion file. It uses the schema's instructions and asks you to confirm the concrete path before creating it.
- Artifacts with no files yet remain with `/opsx:continue`. Intentionally skipped artifacts stay untouched.
- New files must stay inside the change directory. If a file appears at the confirmed path before creation, update stops instead of overwriting it.
**Example:**
```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 start an artifact with no existing files. Enable `/opsx:continue` for that, or use `openspec status` and `openspec instructions` if that optional workflow isn't installed.
- If the change was already implemented, follow up with `/opsx:apply` so the code matches the revised plan
- If your revision changes the *intent* of the change, start fresh with a new change instead (see [When to Update vs. Start Fresh](opsx.md#when-to-update-vs-start-fresh))
---
### `/opsx:verify`
Validate that implementation matches your change artifacts. Checks completeness, correctness, and coherence.
**Syntax:**
```
/opsx:verify [change-name]
```
**Arguments:**
| Argument | Required | Description |
|----------|----------|-------------|
| `change-name` | No | Which change to verify (inferred from context if not provided) |
**What it does:**
- Checks three dimensions of implementation quality
- Searches codebase for implementation evidence
- Reports issues categorized as CRITICAL, WARNING, or SUGGESTION
- Does not block archive, but surfaces issues
**Verification dimensions:**
| Dimension | What it validates |
|-----------|-------------------|
| **Completeness** | All tasks done, all requirements implemented, scenarios covered |
| **Correctness** | Implementation matches spec intent, edge cases handled |
| **Coherence** | Design decisions reflected in code, patterns consistent |
**Example:**
```
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 "System preference detection" has no test coverage
CORRECTNESS
✓ Implementation matches spec intent
✓ Edge cases from scenarios are handled
✓ Error states match spec definitions
COHERENCE
✓ Design decisions reflected in code structure
✓ Naming conventions consistent with design.md
⚠ Design mentions "CSS variables" but implementation uses Tailwind classes
SUMMARY
─────────────────────────────
Critical issues: 0
Warnings: 2
Ready to archive: Yes (with warnings)
Recommendations:
1. Add test for system preference detection
2. Update design.md to reflect Tailwind usage, or refactor to use CSS variables
```
**Tips:**
- Run before archiving to catch mismatches early
- Warnings don't block archive but indicate potential issues
- Good for reviewing AI's work before committing
- Can reveal drift between artifacts and implementation
---
### `/opsx:sync`
**Optional command.** Merge delta specs from a change into main specs. Archive will prompt to sync if needed, so you typically don't need to run this manually.
**Syntax:**
```
/opsx:sync [change-name]
```
**Arguments:**
| Argument | Required | Description |
|----------|----------|-------------|
| `change-name` | No | Which change to sync (inferred from context if not provided) |
**What it does:**
- Reads delta specs from change folder
- Parses ADDED/MODIFIED/REMOVED/RENAMED sections
- Merges changes into main `openspec/specs/` directory
- Preserves existing content not mentioned in delta
- Does not archive the change (remains active)
**Example:**
```text
You: /opsx:sync
AI: Syncing add-dark-mode delta specs...
Reading: openspec/changes/add-dark-mode/specs/ui/spec.md
Target: openspec/specs/ui/spec.md
Changes to apply:
✓ ADDED: Theme Selection requirement (2 scenarios)
✓ ADDED: Accessibility requirement (1 scenario)
Merging...
✓ openspec/specs/ui/spec.md updated
Change remains active. Run /opsx:archive when ready to complete.
```
**When to use manually:**
| Scenario | Use sync? |
|----------|-----------|
| Long-running change, want specs in main before archiving | Yes |
| Multiple parallel changes need the updated base specs | Yes |
| Want to preview/review the merge separately | Yes |
| Quick change, going straight to archive | No (archive handles it) |
**Tips:**
- Sync is intelligent, not copy-paste
- Can add scenarios to existing requirements without duplicating
- Change stays active after sync (not archived)
- Most users will never need to call this directly—archive prompts if needed
---
### `/opsx:archive`
Archive a completed change. Finalizes the change and moves it to the archive folder.
**Syntax:**
```
/opsx:archive [change-name]
```
**Arguments:**
| Argument | Required | Description |
|----------|----------|-------------|
| `change-name` | No | Which change to archive (inferred from context if not provided) |
**What it does:**
- Checks artifact completion status
- Checks task completion (warns if incomplete)
- Offers to sync delta specs if not already synced
- Moves change folder to `openspec/changes/archive/YYYY-MM-DD-<name>/`
- Preserves all artifacts for audit trail
**Example:**
```text
You: /opsx:archive
AI: Archiving add-dark-mode...
Artifact status:
✓ proposal.md exists
✓ specs/ exists
✓ design.md exists
✓ tasks.md exists (8/8 tasks complete)
Delta specs: Not yet synced
→ Sync now? (recommended)
You: Yes
AI: ✓ Synced specs to openspec/specs/ui/spec.md
✓ Moved to openspec/changes/archive/2025-01-24-add-dark-mode/
Change archived successfully.
```
**Tips:**
- Archive won't block on incomplete tasks, but will warn
- Delta specs can be synced during archive or beforehand
- Archived changes are preserved for history
- Use `/opsx:verify` first to catch issues
---
### `/opsx:bulk-archive`
Archive multiple completed changes at once. Handles spec conflicts between changes.
**Syntax:**
```
/opsx:bulk-archive [change-names...]
```
**Arguments:**
| Argument | Required | Description |
|----------|----------|-------------|
| `change-names` | No | Specific changes to archive (prompts to select if not provided) |
**What it does:**
- Lists all completed changes
- Validates each change before archiving
- Detects spec conflicts across changes
- Resolves conflicts by checking what's actually implemented
- Archives in chronological order
**Example:**
```
You: /opsx:bulk-archive
AI: Found 3 completed changes:
- add-dark-mode (8/8 tasks complete)
- fix-login-bug (3/3 tasks complete)
- update-footer (2/2 tasks complete)
Checking for spec conflicts...
⚠ add-dark-mode and update-footer both touch specs/ui/
Inspecting codebase to resolve...
Both changes are implemented. Will merge in chronological order:
1. add-dark-mode (created Jan 20)
2. update-footer (created Jan 22)
Archive all 3 changes?
You: Yes
AI: ✓ Archived add-dark-mode
✓ Archived fix-login-bug
✓ Archived update-footer
Specs merged: add-dark-mode → update-footer
```
**Tips:**
- Good for parallel work streams
- Conflict resolution is agentic (checks codebase)
- Changes are archived in order of creation
- Prompts before overwriting spec content
---
### `/opsx:onboard`
Guided onboarding through the complete OpenSpec workflow. An interactive tutorial using your actual codebase.
**Syntax:**
```
/opsx:onboard
```
**What it does:**
- Walks through a complete workflow cycle with narration
- Scans your codebase for real improvement opportunities
- Creates an actual change with real artifacts
- Implements actual work (small, safe changes)
- Archives the completed change
- Explains each step as it happens
**Phases:**
1. Welcome and codebase analysis
2. Finding an improvement opportunity
3. Creating a change (`/opsx:new`)
4. Writing the proposal
5. Creating specs
6. Writing the design
7. Creating tasks
8. Implementing tasks (`/opsx:apply`)
9. Verifying implementation
10. Archiving the change
11. Summary and next steps
**Example:**
```
You: /opsx:onboard
AI: Welcome to OpenSpec!
I'll walk you through the complete workflow using your actual codebase.
We'll find something small to improve, create a proper change for it,
implement it, and archive it.
Let me scan your codebase for opportunities...
[Analyzes codebase]
I found a few things we could work on:
1. Add input validation to the contact form
2. Improve error messages in the auth flow
3. Add loading states to async buttons
Which interests you? (or suggest something else)
```
**Tips:**
- Best for new users learning the workflow
- Uses real code, not toy examples
- Creates a real change you can keep or discard
- Takes 15-30 minutes to complete
---
## Command Syntax by AI Tool
Different AI tools use slightly different command syntax. Use the format that matches your tool:
| 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, MiniMax Code, Mistral Vibe, Zed Agent, shared `.agents` |
| none — Kimi Code | `/skill:openspec-propose` | Kimi Code |
| none — Codex CLI | `$openspec-propose` | Codex |
> **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.
---
## Legacy Commands
These commands use the older "all-at-once" workflow. They still work but OPSX commands are recommended.
| Command | What it does |
|---------|--------------|
| `/openspec:proposal` | Create all artifacts at once (proposal, specs, design, tasks) |
| `/openspec:apply` | Implement the change |
| `/openspec:archive` | Archive the change |
**When to use legacy commands:**
- Existing projects using the old workflow
- Simple changes where you don't need incremental artifact creation
- Preference for the all-or-nothing approach
**Migrating to OPSX:**
Legacy changes can be continued with OPSX commands. The artifact structure is compatible.
---
## Troubleshooting
### "Change not found"
The command couldn't identify which change to work on.
**Solutions:**
- Specify the change name explicitly: `/opsx:apply add-dark-mode`
- Check that the change folder exists: `openspec list`
- Verify you're in the right project directory
### "No artifacts ready"
All artifacts are either complete or blocked by missing dependencies.
**Solutions:**
- Run `openspec status --change <name>` to see what's blocking
- Check if required artifacts exist
- Create missing dependency artifacts first
### "Schema not found"
The specified schema doesn't exist.
**Solutions:**
- List available schemas: `openspec schemas`
- Check spelling of schema name
- Create the schema if it's custom: `openspec schema init <name>`
### Commands not recognized
The AI tool doesn't recognize OpenSpec commands.
**Solutions:**
- Ensure OpenSpec is initialized: `openspec init`
- Regenerate skills: `openspec update`
- Check that `.claude/skills/` directory exists (for Claude Code)
- Restart your AI tool to pick up new skills
### Artifacts not generating properly
The AI creates incomplete or incorrect artifacts.
**Solutions:**
- Add project context in `openspec/config.yaml`
- Add per-artifact rules for specific guidance
- Provide more detail in your change description
- Use `/opsx:continue` instead of `/opsx:ff` for more control
---
## Next Steps
- [Workflows](workflows.md) - Common patterns and when to use each command
- [CLI](cli.md) - Terminal commands for management and validation
- [Customization](customization.md) - Create custom schemas and workflows
+20
View File
@@ -0,0 +1,20 @@
# Community Showcase
A community-owned awesome list of projects and resources built with and for OpenSpec. Tools, integrations, workflows, and learning resources are welcome. Community members grow and maintain this showcase through pull requests.
Listed projects are maintained independently. Inclusion does not imply official support or endorsement by OpenSpec. See each project's documentation and issue tracker for setup and support.
## Projects and resources
- **[OpenSpec Workbench](https://github.com/VeryComplexAndLongName/OpenSpec-UI)**: Running and supervising agents on OpenSpec changes.
- **[openspec-guard](https://github.com/guillaume-flambard/spec-guard)**: CLI and GitHub Action that reports which OpenSpec scenarios are covered by a Vitest or Jest test, without running the tests.
## Add your project
Open a pull request adding one line to this file with your project's name, a direct link, and a short description of how it relates to OpenSpec.
- Keep entries focused on something built with OpenSpec or supporting its use, rather than general product advertising.
- Describe what people can use. Avoid promotional claims, referral links, and tracking links.
- Disclose paid features or required accounts in the entry, if any.
Corrections and updates to existing entries are welcome too.
+631
View File
@@ -0,0 +1,631 @@
# Concepts
This guide explains the core ideas behind OpenSpec and how they fit together. For practical usage, see [Getting Started](getting-started.md) and [Workflows](workflows.md).
## Philosophy
OpenSpec is built around four principles:
```
fluid not rigid — no phase gates, work on what makes sense
iterative not waterfall — learn as you build, refine as you go
easy not complex — lightweight setup, minimal ceremony
brownfield-first — works with existing codebases, not just greenfield
```
### Why These Principles Matter
**Fluid not rigid.** Traditional spec systems lock you into phases: first you plan, then you implement, then you're done. OpenSpec is more flexible — you can create artifacts in any order that makes sense for your work.
**Iterative not waterfall.** Requirements change. Understanding deepens. What seemed like a good approach at the start might not hold up after you see the codebase. OpenSpec embraces this reality.
**Easy not complex.** Some spec frameworks require extensive setup, rigid formats, or heavyweight processes. OpenSpec stays out of your way. Initialize in seconds, start working immediately, customize only if you need to.
**Brownfield-first.** Most software work isn't building from scratch — it's modifying existing systems. OpenSpec's delta-based approach makes it easy to specify changes to existing behavior, not just describe new systems.
## The Big Picture
OpenSpec organizes your work into two main areas:
```
┌────────────────────────────────────────────────────────────────────┐
│ openspec/ │
│ │
│ ┌─────────────────────┐ ┌───────────────────────────────┐ │
│ │ specs/ │ │ changes/ │ │
│ │ │ │ │ │
│ │ Source of truth │◄─────│ Proposed modifications │ │
│ │ How your system │ merge│ Each change = one folder │ │
│ │ currently works │ │ Contains artifacts + deltas │ │
│ │ │ │ │ │
│ └─────────────────────┘ └───────────────────────────────┘ │
│ │
└────────────────────────────────────────────────────────────────────┘
```
**Specs** are the source of truth — they describe how your system currently behaves.
**Changes** are proposed modifications — they live in separate folders until you're ready to merge them.
This separation is key. You can work on multiple changes in parallel without conflicts. You can review a change before it affects the main specs. And when you archive a change, its deltas merge cleanly into the source of truth.
## Specs
Specs describe your system's behavior using structured requirements and scenarios.
### Structure
```
openspec/specs/
├── auth/
│ └── spec.md # Authentication behavior
├── payments/
│ └── spec.md # Payment processing
├── notifications/
│ └── spec.md # Notification system
└── ui/
└── spec.md # UI behavior and themes
```
Organize specs by domain — logical groupings that make sense for your system. Common patterns:
- **By feature area**: `auth/`, `payments/`, `search/`
- **By component**: `api/`, `frontend/`, `workers/`
- **By bounded context**: `ordering/`, `fulfillment/`, `inventory/`
### Spec Format
A spec contains requirements, and each requirement has scenarios:
```markdown
# Auth Specification
## Purpose
Authentication and session management for the application.
## Requirements
### Requirement: User Authentication
The system SHALL issue a JWT token upon successful login.
#### Scenario: Valid credentials
- GIVEN a user with valid credentials
- WHEN the user submits login form
- THEN a JWT token is returned
- AND the user is redirected to dashboard
#### Scenario: Invalid credentials
- GIVEN invalid credentials
- WHEN the user submits login form
- THEN an error message is displayed
- AND no token is issued
### Requirement: Session Expiration
The system MUST expire sessions after 30 minutes of inactivity.
#### Scenario: Idle timeout
- GIVEN an authenticated session
- WHEN 30 minutes pass without activity
- THEN the session is invalidated
- AND the user must re-authenticate
```
**Key elements:**
| Element | Purpose |
|---------|---------|
| `## Purpose` | High-level description of this spec's domain |
| `### Requirement:` | A specific behavior the system must have |
| `#### Scenario:` | A concrete example of the requirement in action |
| SHALL/MUST/SHOULD | RFC 2119 keywords indicating requirement strength |
### Why Structure Specs This Way
**Requirements are the "what"** — they state what the system should do without specifying implementation.
**Scenarios are the "when"** — they provide concrete examples that can be verified. Good scenarios:
- Are testable (you could write an automated test for them)
- Cover both happy path and edge cases
- Use Given/When/Then or similar structured format
**RFC 2119 keywords** (SHALL, MUST, SHOULD, MAY) communicate intent:
- **MUST/SHALL** — absolute requirement
- **SHOULD** — recommended, but exceptions exist
- **MAY** — optional
### What a Spec Is (and Is Not)
A spec is a **behavior contract**, not an implementation plan.
Good spec content:
- Observable behavior users or downstream systems rely on
- Inputs, outputs, and error conditions
- External constraints (security, privacy, reliability, compatibility)
- Scenarios that can be tested or explicitly validated
Avoid in specs:
- Internal class/function names
- Library or framework choices
- Step-by-step implementation details
- Detailed execution plans (those belong in `design.md` or `tasks.md`)
Quick test:
- If implementation can change without changing externally visible behavior, it likely does not belong in the spec.
### Keep It Lightweight: Progressive Rigor
OpenSpec aims to avoid bureaucracy. Use the lightest level that still makes the change verifiable.
**Lite spec (default):**
- Short behavior-first requirements
- Clear scope and non-goals
- A few concrete acceptance checks
**Full spec (for higher risk):**
- Cross-team or cross-repo changes
- API/contract changes, migrations, security/privacy concerns
- Changes where ambiguity is likely to cause expensive rework
Most changes should stay in Lite mode.
### Human + Agent Collaboration
In many teams, humans explore and agents draft artifacts. The intended loop is:
1. Human provides intent, context, and constraints.
2. Agent converts this into behavior-first requirements and scenarios.
3. Agent keeps implementation detail in `design.md` and `tasks.md`, not `spec.md`.
4. Validation confirms structure and clarity before implementation.
This keeps specs readable for humans and consistent for agents.
## Changes
A change is a proposed modification to your system, packaged as a folder with everything needed to understand and implement it.
### Change Structure
```
openspec/changes/add-dark-mode/
├── proposal.md # Why and what
├── design.md # How (technical approach)
├── tasks.md # Implementation checklist
├── .openspec.yaml # Change metadata (optional): schema, created, skip_specs, retire_capabilities
└── specs/ # Delta specs
└── ui/
└── spec.md # What's changing in ui/spec.md
```
Each change is self-contained. It has:
- **Artifacts** — documents that capture intent, design, and tasks
- **Delta specs** — specifications for what's being added, modified, or removed
- **Metadata** — optional configuration for this specific change
### Why Changes Are Folders
Packaging a change as a folder has several benefits:
1. **Everything together.** Proposal, design, tasks, and specs live in one place. No hunting through different locations.
2. **Parallel work.** Multiple changes can exist simultaneously without conflicting. Work on `add-dark-mode` while `fix-auth-bug` is also in progress.
3. **Clean history.** When archived, changes move to `changes/archive/` with their full context preserved. You can look back and understand not just what changed, but why.
4. **Review-friendly.** A change folder is easy to review — open it, read the proposal, check the design, see the spec deltas.
## Artifacts
Artifacts are the documents within a change that guide the work.
### The Artifact Flow
```
proposal ──────► specs ──────► design ──────► tasks ──────► implement
│ │ │ │
why what how steps
+ scope changes approach to take
```
Artifacts build on each other. Each artifact provides context for the next.
### Artifact Types
#### Proposal (`proposal.md`)
The proposal captures **intent**, **scope**, and **approach** at a high level.
```markdown
# Proposal: Add Dark Mode
## Intent
Users have requested a dark mode option to reduce eye strain
during nighttime usage and match system preferences.
## Scope
In scope:
- Theme toggle in settings
- System preference detection
- Persist preference in localStorage
Out of scope:
- Custom color themes (future work)
- Per-page theme overrides
## Approach
Use CSS custom properties for theming with a React context
for state management. Detect system preference on first load,
allow manual override.
```
**When to update the proposal:**
- Scope changes (narrowing or expanding)
- Intent clarifies (better understanding of the problem)
- Approach fundamentally shifts
#### Specs (delta specs in `specs/`)
Delta specs describe **what's changing** relative to the current specs. See [Delta Specs](#delta-specs) below.
#### Design (`design.md`)
The design captures **technical approach** and **architecture decisions**.
````markdown
# Design: Add Dark Mode
## Technical Approach
Theme state managed via React Context to avoid prop drilling.
CSS custom properties enable runtime switching without class toggling.
## Architecture Decisions
### Decision: Context over Redux
Using React Context for theme state because:
- Simple binary state (light/dark)
- No complex state transitions
- Avoids adding Redux dependency
### Decision: CSS Custom Properties
Using CSS variables instead of CSS-in-JS because:
- Works with existing stylesheet
- No runtime overhead
- Browser-native solution
## Data Flow
```
ThemeProvider (context)
│
▼
ThemeToggle ◄──► localStorage
│
▼
CSS Variables (applied to :root)
```
## File Changes
- `src/contexts/ThemeContext.tsx` (new)
- `src/components/ThemeToggle.tsx` (new)
- `src/styles/globals.css` (modified)
````
**When to update the design:**
- Implementation reveals the approach won't work
- Better solution discovered
- Dependencies or constraints change
#### Tasks (`tasks.md`)
Tasks are the **implementation checklist** — concrete steps with checkboxes.
```markdown
# Tasks
## 1. Theme Infrastructure
- [ ] 1.1 Create ThemeContext with light/dark state
- [ ] 1.2 Add CSS custom properties for colors
- [ ] 1.3 Implement localStorage persistence
- [ ] 1.4 Add system preference detection
## 2. UI Components
- [ ] 2.1 Create ThemeToggle component
- [ ] 2.2 Add toggle to settings page
- [ ] 2.3 Update Header to include quick toggle
## 3. Styling
- [ ] 3.1 Define dark theme color palette
- [ ] 3.2 Update components to use CSS variables
- [ ] 3.3 Test contrast ratios for accessibility
```
**Task best practices:**
- Group related tasks under headings
- Use hierarchical numbering (1.1, 1.2, etc.)
- Keep tasks small enough to complete in one session
- State how each task is verified (a test, command, or observable result)
- Land the tests and documentation each group's work calls for inside that group, not in a final catch-up group
- Check tasks off as you complete them
## Delta Specs
Delta specs are the key concept that makes OpenSpec work for brownfield development. They describe **what's changing** rather than restating the entire spec.
### The Format
```markdown
# Delta for Auth
## ADDED Requirements
### Requirement: Two-Factor Authentication
The system MUST support TOTP-based two-factor authentication.
#### Scenario: 2FA enrollment
- GIVEN a user without 2FA enabled
- WHEN the user enables 2FA in settings
- THEN a QR code is displayed for authenticator app setup
- AND the user must verify with a code before activation
#### Scenario: 2FA login
- GIVEN a user with 2FA enabled
- WHEN the user submits valid credentials
- THEN an OTP challenge is presented
- AND login completes only after valid OTP
## MODIFIED Requirements
### Requirement: Session Expiration
The system MUST expire sessions after 15 minutes of inactivity.
(Previously: 30 minutes)
#### Scenario: Idle timeout
- GIVEN an authenticated session
- WHEN 15 minutes pass without activity
- THEN the session is invalidated
## REMOVED Requirements
### Requirement: Remember Me
(Deprecated in favor of 2FA. Users should re-authenticate each session.)
```
### Delta Sections
| Section | Meaning | What Happens on Archive |
|---------|---------|------------------------|
| `## ADDED Requirements` | New behavior | Appended to main spec |
| `## MODIFIED Requirements` | Changed behavior | Replaces existing requirement |
| `## REMOVED Requirements` | Deprecated behavior | Deleted from main spec; removing the last requirement retires the capability and deletes its spec file, when the change declares `retire_capabilities: true` |
| `## 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
**Clarity.** A delta shows exactly what's changing. Reading a full spec, you'd have to diff it mentally against the current version.
**Conflict avoidance.** Two changes can touch the same spec file without conflicting, as long as they modify different requirements.
**Review efficiency.** Reviewers see the change, not the unchanged context. Focus on what matters.
**Brownfield fit.** Most work modifies existing behavior. Deltas make modifications first-class, not an afterthought.
## Schemas
Schemas define the artifact types and their dependencies for a workflow.
### How Schemas Work
```yaml
# openspec/schemas/spec-driven/schema.yaml
name: spec-driven
artifacts:
- id: proposal
generates: proposal.md
requires: [] # No dependencies, can create first
- id: specs
generates: specs/**/*.md
requires: [proposal] # Needs proposal before creating
- id: design
generates: design.md
requires: [proposal] # Can create in parallel with specs
- id: tasks
generates: tasks.md
requires: [specs, design] # Needs both specs and design first
```
**Artifacts form a dependency graph:**
```
proposal
(root node)
│
┌─────────────┴─────────────┐
│ │
▼ ▼
specs design
(requires: (requires:
proposal) proposal)
│ │
└─────────────┬─────────────┘
│
▼
tasks
(requires:
specs, design)
```
**Dependencies are enablers, not gates.** They show what's possible to create, not what you must create next. You can skip design if you don't need it. You can create specs before or after design — both depend only on proposal.
### Built-in Schemas
**spec-driven** (default)
The standard workflow for spec-driven development:
```
proposal → specs → design → tasks → implement
```
Best for: Most feature work where you want to agree on specs before implementation.
### Custom Schemas
Create custom schemas for your team's workflow:
```bash
# Create from scratch
openspec schema init research-first
# Or fork an existing one
openspec schema fork spec-driven research-first
```
**Example custom schema:**
```yaml
# openspec/schemas/research-first/schema.yaml
name: research-first
artifacts:
- id: research
generates: research.md
requires: [] # Do research first
- id: proposal
generates: proposal.md
requires: [research] # Proposal informed by research
- id: tasks
generates: tasks.md
requires: [proposal] # Skip specs/design, go straight to tasks
```
See [Customization](customization.md) for full details on creating and using custom schemas.
## Archive
Archiving completes a change by merging its delta specs into the main specs and preserving the change for history.
### What Happens When You Archive
```
Before archive:
openspec/
├── specs/
│ └── auth/
│ └── spec.md ◄────────────────┐
└── changes/ │
└── add-2fa/ │
├── proposal.md │
├── design.md │ merge
├── tasks.md │
└── specs/ │
└── auth/ │
└── spec.md ─────────┘
After archive:
openspec/
├── specs/
│ └── auth/
│ └── spec.md # Now includes 2FA requirements
└── changes/
└── archive/
└── 2025-01-24-add-2fa/ # Preserved for history
├── proposal.md
├── design.md
├── tasks.md
└── specs/
└── auth/
└── spec.md
```
### The Archive Process
1. **Merge deltas.** Each delta spec section (ADDED/MODIFIED/REMOVED) is applied to the corresponding main spec.
2. **Move to archive.** The change folder moves to `changes/archive/` with a date prefix for chronological ordering.
3. **Preserve context.** All artifacts remain intact in the archive. You can always look back to understand why a change was made.
### Why Archive Matters
**Clean state.** Active changes (`changes/`) shows only work in progress. Completed work moves out of the way.
**Audit trail.** The archive preserves the full context of every change — not just what changed, but the proposal explaining why, the design explaining how, and the tasks showing the work done.
**Spec evolution.** Specs grow organically as changes are archived. Each archive merges its deltas, building up a comprehensive specification over time.
## How It All Fits Together
```
┌──────────────────────────────────────────────────────────────────────────────┐
│ OPENSPEC FLOW │
│ │
│ ┌────────────────┐ │
│ │ 1. START │ /opsx:propose (core) or /opsx:new (expanded) │
│ │ CHANGE │ │
│ └───────┬────────┘ │
│ │ │
│ ▼ │
│ ┌────────────────┐ │
│ │ 2. CREATE │ /opsx:ff or /opsx:continue (expanded workflow) │
│ │ ARTIFACTS │ Creates proposal → specs → design → tasks │
│ │ │ (based on schema dependencies) │
│ └───────┬────────┘ │
│ │ │
│ ▼ │
│ ┌────────────────┐ │
│ │ 3. IMPLEMENT │ /opsx:apply │
│ │ TASKS │ Work through tasks, checking them off │
│ │ │◄──── Update artifacts as you learn │
│ └───────┬────────┘ │
│ │ │
│ ▼ │
│ ┌────────────────┐ │
│ │ 4. VERIFY │ /opsx:verify (optional) │
│ │ WORK │ Check implementation matches specs │
│ └───────┬────────┘ │
│ │ │
│ ▼ │
│ ┌────────────────┐ ┌──────────────────────────────────────────────┐ │
│ │ 5. ARCHIVE │────►│ Delta specs merge into main specs │ │
│ │ CHANGE │ │ Change folder moves to archive/ │ │
│ └────────────────┘ │ Specs are now the updated source of truth │ │
│ └──────────────────────────────────────────────┘ │
│ │
└──────────────────────────────────────────────────────────────────────────────┘
```
**The virtuous cycle:**
1. Specs describe current behavior
2. Changes propose modifications (as deltas)
3. Implementation makes the changes real
4. Archive merges deltas into specs
5. Specs now describe the new behavior
6. Next change builds on updated specs
## Glossary
| Term | Definition |
|------|------------|
| **Artifact** | A document within a change (proposal, design, tasks, or delta specs) |
| **Archive** | The process of completing a change and merging its deltas into main specs |
| **Change** | A proposed modification to the system, packaged as a folder with artifacts |
| **Delta spec** | A spec that describes changes (ADDED/MODIFIED/REMOVED) relative to current specs |
| **Domain** | A logical grouping for specs (e.g., `auth/`, `payments/`) |
| **Requirement** | A specific behavior the system must have |
| **Scenario** | A concrete example of a requirement, typically in Given/When/Then format |
| **Schema** | A definition of artifact types and their dependencies |
| **Spec** | A specification describing system behavior, containing requirements and scenarios |
| **Source of truth** | The `openspec/specs/` directory, containing the current agreed-upon behavior |
## Next Steps
- [Getting Started](getting-started.md) - Practical first steps
- [Workflows](workflows.md) - Common patterns and when to use each
- [Commands](commands.md) - Full command reference
- [Customization](customization.md) - Create custom schemas and configure your project
+433
View File
@@ -0,0 +1,433 @@
# Customization
OpenSpec provides three levels of customization:
| Level | What it does | Best for |
|-------|--------------|----------|
| **Project Config** | Set defaults, inject context/rules | Most teams |
| **Custom Schemas** | Define your own workflow artifacts | Teams with unique processes |
| **Global Overrides** | Share schemas across all projects | Power users |
---
## Project Configuration
The `openspec/config.yaml` file is the easiest way to customize OpenSpec for your team. It lets 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
- **Remember integration choices** - e.g. the [GitHub Copilot cloud coding agent](supported-tools.md#github-copilot-cloud-coding-agent) opt-in
### Quick Setup
```bash
openspec init
```
This walks you through creating a config interactively. Or create one manually:
```yaml
# openspec/config.yaml
schema: spec-driven
context: |
Tech stack: TypeScript, React, Node.js, PostgreSQL
API style: RESTful, documented in docs/api.md
Testing: Jest + React Testing Library
We value backwards compatibility for all public APIs
rules:
proposal:
- Include rollback plan
- Identify affected teams
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
# Set by `openspec init` when you choose (or decline) the GitHub Copilot
# cloud coding agent; controls whether `init`/`update` generate its files.
githubCopilot:
cloudAgent: false
```
### How It Works
**Default schema:**
```bash
# Without config
openspec new change my-feature --schema spec-driven
# With config - schema is automatic
openspec new change my-feature
```
**Context and rules injection:**
When generating any artifact, your context and rules are injected into the AI prompt:
```xml
<context>
Tech stack: TypeScript, React, Node.js, PostgreSQL
...
</context>
<rules>
- Include rollback plan
- Identify affected teams
</rules>
<template>
[Schema's built-in template]
</template>
```
- **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:
1. CLI flag: `--schema <name>`
2. Change metadata (`.openspec.yaml` in the change folder)
3. Project config (`openspec/config.yaml`)
4. Default (`spec-driven`)
---
## Custom Schemas
When project config isn't enough, create your own schema with a completely custom workflow. Custom schemas live in your project's `openspec/schemas/` directory and are version-controlled with your code.
```text
your-project/
├── openspec/
│ ├── config.yaml # Project config
│ ├── schemas/ # Custom schemas live here
│ │ └── my-workflow/
│ │ ├── schema.yaml
│ │ └── templates/
│ └── changes/ # Your changes
└── src/
```
### Fork an Existing Schema
The fastest way to customize is to fork a built-in schema:
```bash
openspec schema fork spec-driven my-workflow
```
This copies the entire `spec-driven` schema to `openspec/schemas/my-workflow/` where you can edit it freely.
**What you get:**
```text
openspec/schemas/my-workflow/
├── schema.yaml # Workflow definition
└── templates/
├── proposal.md # Template for proposal artifact
├── spec.md # Template for specs
├── design.md # Template for design
└── tasks.md # Template for tasks
```
Now edit `schema.yaml` to change the workflow, or edit templates to change what AI generates.
### Create a Schema from Scratch
For a completely fresh workflow:
```bash
# Interactive
openspec schema init research-first
# Non-interactive
openspec schema init rapid \
--description "Rapid iteration workflow" \
--artifacts "proposal,tasks" \
--default
```
### Schema Structure
A schema defines the artifacts in your workflow and how they depend on each other:
```yaml
# openspec/schemas/my-workflow/schema.yaml
name: my-workflow
version: 1
description: My team's custom workflow
artifacts:
- id: proposal
generates: proposal.md
description: Initial proposal document
template: proposal.md
instruction: |
Create a proposal that explains WHY this change is needed.
Focus on the problem, not the solution.
requires: []
- id: design
generates: design.md
description: Technical design
template: design.md
instruction: |
Create a design document explaining HOW to implement.
requires:
- proposal # Can't create design until proposal exists
- id: tasks
generates: tasks.md
description: Implementation checklist
template: tasks.md
requires:
- design
apply:
requires: [tasks]
tracks: tasks.md
```
**Key fields:**
| Field | Purpose |
|-------|---------|
| `id` | Unique identifier, used in commands and rules |
| `generates` | Output filename (supports globs like `specs/**/*.md`) |
| `template` | Template file in `templates/` directory |
| `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.
```markdown
<!-- templates/proposal.md -->
## Why
<!-- Explain the motivation for this change. What problem does this solve? -->
## What Changes
<!-- Describe what will change. Be specific about new capabilities or modifications. -->
## Impact
<!-- Affected code, APIs, dependencies, systems -->
```
Templates can include:
- Section headers the AI should fill in
- HTML comments with guidance for the AI
- Example formats showing expected structure
### Validate Your Schema
Before using a custom schema, validate it:
```bash
openspec schema validate my-workflow
```
This checks:
- `schema.yaml` syntax is correct
- All referenced templates exist
- No circular dependencies
- Artifact IDs are valid
### Use Your Custom Schema
Once created, use your schema with:
```bash
# Specify on command
openspec new change feature --schema my-workflow
# Or set as default in config.yaml
schema: my-workflow
```
### Debug Schema Resolution
Not sure which schema is being used? Check with:
```bash
# See where a specific schema resolves from
openspec schema which my-workflow
# List all available schemas
openspec schema which --all
```
Output shows whether it's from your project, user directory, or the package:
```text
Schema: my-workflow
Source: project
Path: /path/to/project/openspec/schemas/my-workflow
```
---
> **Note:** OpenSpec also supports user-level schemas at `~/.local/share/openspec/schemas/` for sharing across projects, but project-level schemas in `openspec/schemas/` are recommended since they're version-controlled with your code.
---
## Examples
### Rapid Iteration Workflow
A minimal workflow for quick iterations:
```yaml
# openspec/schemas/rapid/schema.yaml
name: rapid
version: 1
description: Fast iteration with minimal overhead
artifacts:
- id: proposal
generates: proposal.md
description: Quick proposal
template: proposal.md
instruction: |
Create a brief proposal for this change.
Focus on what and why, skip detailed specs.
requires: []
- id: tasks
generates: tasks.md
description: Implementation checklist
template: tasks.md
requires: [proposal]
apply:
requires: [tasks]
tracks: tasks.md
```
### Adding a Review Artifact
Fork the default and add a review step:
```bash
openspec schema fork spec-driven with-review
```
Then edit `schema.yaml` to add:
```yaml
- id: review
generates: review.md
description: Pre-implementation review checklist
template: review.md
instruction: |
Create a review checklist based on the design.
Include security, performance, and testing considerations.
requires:
- design
- id: tasks
# ... existing tasks config ...
requires:
- specs
- design
- review # Now tasks require review too
```
---
## Community Schemas
OpenSpec also supports community-maintained schemas distributed via standalone repositories. These provide opinionated workflows that integrate OpenSpec with other tools or systems, similar to how [github/spec-kit's community extension catalog](https://github.com/github/spec-kit/tree/main/extensions) works for spec-kit.
Community schemas are not vendored into OpenSpec core — they live in their own repositories with their own release cadence. To use one, copy the schema bundle into your project's `openspec/schemas/<schema-name>/` directory (each repo's README has install instructions).
| Schema | Maintainer | Repository | Description |
|--------|-----------|-----------|-------------|
| `intent-driven` | @harikrishnan83 | [intent-driven-dev/openspec-schemas](https://github.com/intent-driven-dev/openspec-schemas/tree/main/openspec/schemas/intent-driven) | Captures change intent, observable behaviour, technical design, and durable architectural decisions before implementation. Adds a change-local ADR review manifest and writes qualifying long-lived decisions as immutable, supersedable ADRs. |
| `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.
---
## See Also
- [CLI Reference: Schema Commands](cli.md#schema-commands) - Full command documentation
+91
View File
@@ -0,0 +1,91 @@
# Editing & Iterating on a Change
**Every artifact in a change is just a Markdown file you can edit at any time.** There is no locked "planning phase," no approval gate, no special edit mode to enter. Want to change the proposal after you've started building? Open `proposal.md` and change it. Realized the design is wrong mid-implementation? Fix `design.md` and keep going. That's the whole answer, and it's by design.
This page is for the moment you think "wait, can I go back and change that?" Yes. Here's how, for each common case.
## Two ways to edit anything
You always have both:
1. **Edit the file directly.** Artifacts are plain Markdown in `openspec/changes/<name>/`. Open `proposal.md`, `design.md`, `tasks.md`, or a delta spec under `specs/` in your editor and change it. Nothing else is required.
2. **Ask your AI to revise it.** In chat, just say what you want: "Update the proposal to drop the caching idea and add a rate-limit section," or "the design should use a queue, not polling." The AI edits the artifact for you, using the rest of the change as context.
Use whichever fits the moment. Small wording tweak? Edit the file. Substantive rethink? Let the AI revise with full context.
## "How do I update the proposal (or specs) after I've started?"
Just update it. Same change, refined.
If you're using the expanded commands, the natural flow is: edit the artifact, then run `/opsx:continue` to pick up from the new state, or `/opsx:apply` to keep implementing against the updated plan. If you're on the default `core` commands, edit the artifact and run `/opsx:apply`; it reads the current files, so it builds against whatever the artifacts now say.
The mental model: artifacts are the live plan, not a signed contract. The AI always works from their current contents, so editing them steers the work.
```text
You: I want to change the approach in this change.
You: [edit design.md, or tell the AI:]
Update design.md to use a background job instead of a synchronous call.
AI: Updated design.md. The task list still fits; want me to continue applying?
You: /opsx:apply
```
This answers a very common question: there's no separate "update proposal" command because you don't need one. The file is the source of truth, and editing it (by hand or via the AI) is the update.
## "How do I go back to review after implementing?"
You don't have to "go back," because you never left. The workflow is fluid: review, edit, and implementation aren't sequential phases you're trapped in.
Concretely, after some `/opsx:apply` work:
- Want to re-examine the plan? Open the artifacts and read them, or run `openspec show <change>` in your terminal for a consolidated view.
- Found something to change? Edit the artifact (or ask the AI to), then continue.
- Want a structured check that the code matches the plan? Run `/opsx:verify` (expanded command). It reports completeness, correctness, and coherence without blocking anything. See [Workflows: Verify](workflows.md#verify-check-your-work).
There's no "review phase" to return to, because review is something you can do at any point, including after implementation.
## "I edited the code by hand. How do I reconcile that with OpenSpec?"
This happens constantly and it's fine. You tweaked something in your editor, and now the code and the artifacts disagree. Bring them back in sync in whichever direction is true:
- **The code is now correct, the spec is stale.** Update the delta spec (and tasks, if relevant) to describe the behavior you actually shipped. The spec should match reality before you archive, because archiving merges the spec into your source of truth.
- **The spec is correct, the code drifted.** Keep building or fixing until the code matches the spec.
A fast way to surface mismatches is `/opsx:verify`: it reads your artifacts and your code and tells you where they diverge. Treat its output as a to-do list for reconciliation, then archive once they agree.
The principle: at archive time, your specs become the truth of record. So before you archive, make the specs honest about what the code does. Manual edits are welcome; just don't let them quietly desync the spec.
## Refining a proposal you're not happy with
If a generated proposal misses the mark, you have three good moves:
- **Iterate in place.** Tell the AI what's off ("the scope is too broad, drop the admin features") and let it revise. Cheapest and usually right.
- **Explore first, then re-propose.** If the problem is that the idea itself is unclear, step back to `/opsx:explore`, think it through, and let a sharper proposal come out of that. See [Explore First](explore.md).
- **Start fresh.** If the intent has fundamentally changed, a new change can be clearer than patching the old one.
That last move has its own decision guide, next.
## When to update vs. start a new change
Short version: **update when it's the same work refined; start new when the intent fundamentally changed or the scope exploded into different work.**
- Same goal, better approach? Update.
- Scope narrowing (ship the MVP now, more later)? Update, then archive, then a new change for phase two.
- The problem itself changed ("add dark mode" became "build a full theming system")? New change.
There's a full flowchart and worked examples in [Workflows: When to Update vs Start Fresh](workflows.md#when-to-update-vs-start-fresh) and a deeper treatment in [OPSX: When to Update vs. Start Fresh](opsx.md#when-to-update-vs-start-fresh).
## A note on tasks
`tasks.md` is a living checklist, not a frozen plan. As you implement, you can add tasks you discover, remove ones that turned out unnecessary, or reorder them. The AI checks items off as it completes them during `/opsx:apply`, and it resumes from the first unchecked task if you come back later. Editing the list mid-flight is expected.
## 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
+224
View File
@@ -0,0 +1,224 @@
# 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`, `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.
> **Not sure what you're building yet?** Most of these recipes are sharper if you start with `/opsx:explore` to think it through first. [Recipe 3](#recipe-3-exploring-before-you-commit) shows it in action, and the [Explore First](explore.md) guide makes the full case.
## Recipe 1: A small feature, the fast path
**When to use it:** you know what you want, and it's a contained piece of work. This is the most common recipe.
The whole thing is three commands. Propose, build, archive.
```text
You: /opsx:propose add-logout-button
AI: Created openspec/changes/add-logout-button/
✓ proposal.md — why and what
✓ specs/ — the new requirement and its scenarios
✓ design.md — technical approach
✓ tasks.md — implementation checklist
Ready for implementation. Run /opsx:apply.
```
Now read the plan. Open the proposal and the delta spec. This is the moment OpenSpec is built for: catching a wrong assumption while it's still one paragraph, not 400 lines of code. Edit any artifact directly if something's off, then continue.
```text
You: /opsx:apply
AI: Working through tasks...
✓ 1.1 Add logout button to header
✓ 1.2 Clear session on click
✓ 1.3 Add confirmation dialog
All tasks complete!
You: /opsx:archive
AI: Archiving add-logout-button...
✓ Merged specs into openspec/specs/auth/spec.md
✓ Moved to openspec/changes/archive/2026-06-22-add-logout-button/
Done. Ready for the next change.
```
That's it. The logout behavior is now part of your specs, and the change is filed away with its full context.
## Recipe 2: A bug fix
**When to use it:** something is broken and you want the fix recorded as a deliberate change to behavior, not a mystery commit.
Bug fixes work exactly like features. The difference is in how you frame the proposal: describe the *correct* behavior, not just "fix the bug."
```text
You: /opsx:propose fix-login-redirect-loop
AI: Created openspec/changes/fix-login-redirect-loop/
The proposal captures the current broken behavior and the
intended behavior. The delta spec MODIFIES the redirect
requirement so it's now precise and testable.
Ready for implementation.
```
Because the fix lands as a `MODIFIED` requirement with a fresh scenario, the next person (or the next AI session) sees not just that you fixed it, but what "correct" means. Then `/opsx:apply` and `/opsx:archive` as usual.
Tip: for a fix, a good scenario is the regression test in prose. "GIVEN a logged-out user, WHEN they submit valid credentials, THEN they land on the dashboard and are not redirected again." Write that, and the implementation has a clear target.
## Recipe 3: Exploring before you commit
**When to use it:** you have a problem but not yet a plan. You're not sure what to build, or which approach is right.
Start with `/opsx:explore`. It's a thinking partner with no structure. It never writes code, and writes nothing else unless you ask it to capture what you decided, or say yes when it offers. It reads your codebase and helps you decide.
```text
You: /opsx:explore
AI: What would you like to explore?
You: Page loads feel slow but I don't know where the time goes.
AI: Let me look... I found three likely culprits:
1. Large unoptimized images on the product list
2. Synchronous data fetching that blocks render
3. Re-renders triggered by context changes
Which should we tackle first?
You: The data fetching one.
You: /opsx:propose optimize-product-list-fetching
AI: Created the change, with a proposal informed by what we just
found. Ready for implementation.
```
Exploration clarifies your thinking *before* you spend a change on it. When the insight crystallizes, propose, and the AI carries the context forward.
## Recipe 4: Juggling two changes at once
**When to use it:** you're mid-feature and an urgent fix jumps the queue.
Changes are independent folders, so parallel work doesn't conflict. Start the fix, ship it, then return to the feature right where you left off.
```text
You: I'm partway through add-dark-mode, but I need to fix a login bug now.
You: /opsx:propose fix-login-redirect
AI: Created openspec/changes/fix-login-redirect/. Ready for implementation.
You: /opsx:apply
AI: Implemented the fix. All tasks complete.
You: /opsx:archive
AI: ✓ Archived fix-login-redirect.
You: Back to dark mode.
You: /opsx:apply add-dark-mode
AI: Resuming add-dark-mode. Picking up at task 2.3: Update Header...
```
Naming the change in `/opsx:apply add-dark-mode` is how you point the AI at a specific change when more than one is active. Because tasks track completion in `tasks.md`, the AI knows exactly where you stopped.
When several changes are done at once, the expanded `/opsx:bulk-archive` files them together and resolves spec conflicts by checking what's actually implemented. See [Workflows](workflows.md#parallel-changes).
## Recipe 5: A refactor with no behavior change
**When to use it:** you're restructuring code, and externally visible behavior should stay identical.
This is the interesting case, because a pure refactor has *nothing to add to your specs*. The behavior contract doesn't change; only the implementation does. So the work lives in the design and tasks, and the spec delta is empty or absent.
```text
You: /opsx:propose refactor-payment-module
AI: Created the change. The proposal states the goal (split the
payment module, no behavior change) and the design captures
the new structure. No spec changes, since behavior is identical.
Ready for implementation.
```
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
```
The same flag is handy for tooling, CI, and docs-only changes. The principle: specs describe behavior, so if behavior didn't change, the spec shouldn't either. See [Concepts](concepts.md#what-a-spec-is-and-is-not).
## Recipe 6: Step-by-step control (expanded commands)
**When to use it:** a complex or risky change where you want to review each artifact before moving on.
The core `/opsx:propose` drafts everything at once. When you'd rather go one step at a time, turn on the expanded commands:
```bash
$ openspec config profile # select the expanded workflows
$ openspec update # apply them to this project
```
Now you can scaffold and build incrementally:
```text
You: /opsx:new add-2fa
AI: Created openspec/changes/add-2fa/. Ready to create: proposal.
You: /opsx:continue
AI: Created proposal.md. Now available: specs, design.
You: /opsx:continue
AI: Created specs/auth/spec.md. Now available: design.
```
Review each artifact as it lands, edit freely, and continue when you're happy. When you want the rest drafted in one go, `/opsx:ff` fast-forwards through the remaining planning artifacts. Before archiving, `/opsx:verify` checks that the implementation actually matches the specs. See [Workflows](workflows.md#opsxff-vs-opsxcontinue).
## Recipe 7: Learning the whole loop hands-on
**When to use it:** you've installed OpenSpec and want to *feel* the workflow on your own code, not a toy example.
Turn on the expanded commands (see Recipe 6), then:
```text
You: /opsx:onboard
AI: Welcome to OpenSpec! I'll walk you through a complete change
using your actual codebase. Let me scan for a small, safe
improvement we can make together...
```
`/opsx:onboard` finds a real (small) improvement, creates a change for it, implements it, and archives it, narrating every step. It takes 15 to 30 minutes and leaves you with a real change you can keep or discard. It's the gentlest way to learn. See [Commands](commands.md#opsxonboard).
## Checking your work from the terminal
Any time, from your terminal, you can inspect the state of things:
```bash
$ openspec list # active changes
$ openspec show add-dark-mode # one change in detail
$ openspec validate add-dark-mode # check structure
$ openspec view # interactive dashboard
```
These are read-and-inspect tools. The proposing and building still happen through slash commands in chat. Full details in the [CLI reference](cli.md).
## Where to go next
- [Explore First](explore.md): the recommended way to start when you're unsure
- [Workflows](workflows.md): the patterns above, with decision guidance on when to use each
- [Commands](commands.md): every slash command in detail
- [Getting Started](getting-started.md): the canonical first-change walkthrough
- [Concepts](concepts.md): why the pieces fit together the way they do
+134
View File
@@ -0,0 +1,134 @@
# Using OpenSpec in an Existing Project
**You do not document your whole codebase to start. You write specs only for what you're about to change.** That's the single most important thing to know about adopting OpenSpec on an existing project, and it's why OpenSpec is built brownfield-first.
A common worry sounds like this: "My app is 80,000 lines old. Do I have to write specs for all of it before OpenSpec is useful?" No. You'd hate that, and so would we. OpenSpec grows your specs one change at a time. Your first change documents the slice it touches, the next change documents its slice, and over months your specs fill in naturally around the work you actually do.
This guide shows how to start on day one without boiling the ocean.
## The thirty-second version
```bash
$ cd your-existing-project
$ openspec init # adds openspec/ and your AI tool's commands
```
Then, in your AI chat:
```text
/opsx:explore # optional: have the AI read the area you'll touch
/opsx:propose <a real, small change you actually need>
/opsx:apply
/opsx:archive
```
Your specs now describe exactly the part of the system that change touched, and nothing more. That's correct. You're done worrying about the other 80,000 lines.
## Why delta-first is the whole trick
OpenSpec changes are written as **deltas**: `ADDED`, `MODIFIED`, `REMOVED`. A delta describes what's changing relative to current behavior, not the entire system.
This is exactly what brownfield work needs. You're rarely building from nothing. You're adding a field, fixing a redirect, tightening a timeout. A delta lets you specify that one change precisely without first writing a 40-page spec of everything around it.
So your `openspec/specs/` directory doesn't start full and complete. It starts nearly empty and accumulates. Each archived change merges its delta in. The spec for `auth/` becomes thorough only after you've made several auth changes, which is exactly when you want it thorough.
If you want the deeper mechanics, see [Concepts: Delta Specs](concepts.md#delta-specs).
## Your first change on a real codebase
Pick something small and real. Not a toy, not a rewrite. A change you were going to make this week anyway. Small first changes teach you the workflow with low stakes.
**Step 1: Let the AI read the relevant area.** This is where `/opsx:explore` earns its keep on an unfamiliar or large codebase. Point it at the part you're about to touch and let it map how things work before proposing anything.
```text
You: /opsx:explore
AI: What would you like to explore?
You: I need to add rate limiting to our public API, but I'm not sure
how requests currently flow through the middleware.
AI: Let me trace it... [reads the router, middleware stack, and config]
Requests hit Express, pass through auth middleware, then your
controllers. There's no rate-limiting layer today. The cleanest
insertion point is a middleware right after auth. Want me to scope it?
```
Notice the AI now understands your actual structure, so the proposal it writes will fit your code, not a generic template. On a big codebase, this single habit saves the most pain. See [Explore First](explore.md).
**Step 2: Propose the change.** The proposal and its delta spec capture just this change.
```text
You: /opsx:propose add-api-rate-limiting
```
**Step 3: Build and archive** with `/opsx:apply` and `/opsx:archive`, same as any change. After archiving, you have a real spec for your rate-limiting behavior, born from a change you needed anyway.
## Prefer a guided tour? Use onboard
If you'd rather watch the whole loop happen on your own code with narration, the expanded command `/opsx:onboard` does exactly that: it scans your codebase for a small, safe improvement, then walks you through proposing, building, and archiving it, explaining each step.
Turn on the expanded commands first:
```bash
$ openspec config profile # select the expanded workflows
$ openspec update # apply them to this project
```
Then in chat:
```text
/opsx:onboard
```
It's the gentlest possible introduction on a real project, and it leaves you with a genuine (small) change you can keep or discard. See [Commands: `/opsx:onboard`](commands.md#opsxonboard).
## "But I already have requirements docs"
Maybe you have a PRD, an SRS, a formal spec, even TLA+ models. Good. You don't import them wholesale, and you don't throw them away either.
Treat existing docs as **source material for exploration**, not as specs to convert. When you start a change, paste or point the AI at the relevant section, and let it shape a focused OpenSpec delta from it. The delta captures the behavior you're changing now, in OpenSpec's testable requirement-and-scenario form. Your original documents stay where they are as background.
The honest reason: OpenSpec specs are deliberately behavior-first and scoped to changes. A 40-page PRD is a different artifact with a different job. Forcing a one-time bulk conversion tends to produce a large, stale spec nobody trusts. Letting specs grow from real changes keeps them accurate.
```text
You: /opsx:explore
You: Here's the section of our PRD about checkout. I'm implementing the
"guest checkout" requirement next.
[paste the relevant requirement]
AI: [reads it, asks clarifying questions, then helps scope a change]
You: /opsx:propose add-guest-checkout
```
## Organizing specs in a big codebase
Specs live under `openspec/specs/`, grouped by **domain**: a logical area that matches how your team thinks about the system. You don't have to design the whole taxonomy up front. Create a domain folder when your first change in that area needs one.
Common ways to slice domains:
- **By feature area:** `auth/`, `payments/`, `search/`
- **By component:** `api/`, `frontend/`, `workers/`
- **By bounded context:** `ordering/`, `fulfillment/`, `inventory/`
Pick whatever makes a newcomer nod. You can refine later. See [Concepts: Specs](concepts.md#specs).
## Monorepos and work that spans repos
For a monorepo, the simplest model is one `openspec/` directory at the repo root, with domains that map to your packages or services. That covers most teams.
If your work genuinely spans **multiple repositories** (or several packages you treat as separate), OpenSpec has a beta **stores** feature: planning lives in its own standalone repo that any of your code repos can reference, so the plan does not have to live inside one repo's `openspec/` folder. It's beta, so treat its commands and state as evolving. Start with the [Stores User Guide](stores-beta/user-guide.md) for the mental model and the smallest useful path.
## A few honest cautions
- **Resist the urge to back-fill everything.** Writing specs for code you aren't changing feels productive and usually isn't. Those specs go stale, because nothing forces them to track reality. Let real changes drive your specs.
- **Keep early changes small.** Your first few changes are as much about learning the rhythm as shipping. A tight scope makes the loop fast and the lessons cheap.
- **Commit `openspec/` to git.** Your specs and archive belong in version control alongside the code they describe.
- **Give the AI context.** On a large codebase with strong conventions, fill in `openspec/config.yaml`'s `context:` so every proposal respects your stack and patterns. See [Customization](customization.md#project-configuration).
## Where to go next
- [Explore First](explore.md) - the key habit for understanding code before you change it
- [Getting Started](getting-started.md) - the full first-change walkthrough
- [Editing & Iterating on a Change](editing-changes.md) - adjusting a change as you learn
- [Concepts: Delta Specs](concepts.md#delta-specs) - why deltas make brownfield work clean
- [Customization](customization.md) - teach OpenSpec your project's conventions
+127
View File
@@ -0,0 +1,127 @@
# Explore First
**`/opsx:explore` is your thinking partner. Reach for it whenever you have a problem but not yet a plan.** It investigates your codebase, weighs options with you, and clarifies what you actually want, all before a line of code is written. When the picture is clear, it hands off to `/opsx:propose`.
If you take one habit from these docs, take this one: **when you're not sure, explore before you propose.**
Here's why that matters. AI coding assistants are eager. Ask vaguely and they'll confidently build *something*, just maybe not the thing you needed. Explore is the cure. It's a no-stakes conversation where you and the AI figure out the right move together, so that by the time you propose, you're proposing the right thing.
## When to explore
Explore is the right first step more often than people expect. Use it when any of these is true:
- You know the *problem* but not the *solution*. ("Pages feel slow." "Auth is a mess." "We keep getting duplicate orders.")
- You're choosing between approaches and want the tradeoffs laid out against your actual code.
- You're new to a codebase and need to understand how something works before you change it.
- The requirements are fuzzy and you want to sharpen them before committing.
- You suspect the work is bigger or smaller than it looks and want to scope it honestly.
Skip explore only when you already know exactly what you want and how. In that case go straight to [`/opsx:propose`](commands.md#opsxpropose).
## What it does (and doesn't)
Explore is a **conversation**, not a generator.
**It does:**
- Read and search your codebase to answer real questions.
- Compare options and name the tradeoffs of each.
- Draw diagrams to make a design legible.
- Help you narrow a vague idea into a concrete, buildable scope.
- Capture the exploration when you ask, or when you accept its offer: it scaffolds the change with `openspec new change` and writes the planning artifacts you named, or updates an existing change's artifacts.
- Transition to `/opsx:propose` when you're ready.
**It does not:**
- Write or modify code. Explore never writes code, on any path, capture included.
- Design or edit your schemas or templates. Shaping those is a change, not thinking.
- Start a change or write an artifact on its own. It writes nothing unless you ask, or say yes when it offers, and then only what you agreed to, plus the setup files starting a change needs (see below).
- Push you toward capturing. It offers when the thinking crystallizes; you decide.
That's the point. Exploring costs you nothing and commits you to nothing until you say so. You can explore three dead ends, learn something from each, and only then propose the path that survived.
## It's already installed
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
Watch how a vague worry becomes a sharp, buildable change.
```text
You: /opsx:explore
AI: What would you like to explore?
You: Our checkout sometimes creates duplicate orders. I don't know why
and I don't know how to fix it safely.
AI: Let me dig into the checkout path...
[reads the order service, the payment webhook handler, and the
client submit logic]
Here's what I found. There are two ways a duplicate can happen:
1. The client can submit twice (double-click, retry on slow network).
Nothing debounces it.
2. The payment webhook can fire more than once, and the handler
creates an order each time. There's no idempotency key.
Cause #2 is the dangerous one: it can charge and fulfill twice.
Three ways to fix it, roughly in order of robustness:
- Idempotency key on the order-creation endpoint (handles both causes)
- Dedup on the webhook by payment-intent id (handles #2 only)
- Client-side debounce (handles #1 only, papers over the real bug)
Given your code already stores the payment-intent id, the
idempotency-key approach is a small change with the biggest payoff.
Want me to scope that?
You: Yes, let's do the idempotency key.
You: /opsx:propose add-order-idempotency-key
AI: Created openspec/changes/add-order-idempotency-key/, with a proposal
and delta spec grounded in what we just found. Ready for implementation.
```
Notice what happened. The starting point was "something is wrong and I'm scared to touch it." Twenty seconds of exploration turned that into a named root cause, three ranked options, a recommendation tied to the existing code, and a precise change. The proposal that follows is sharp because the thinking happened first.
## Handing off to propose
Explore doesn't archive into anything. When you're ready, you simply start a change, and the AI carries the context from your conversation into the artifacts.
```text
explore ──► propose ──► apply ──► archive
(think) (agree) (build) (record)
```
You can say it in plain language ("let's turn this into a change") or run `/opsx:propose <name>` directly. Either way, the exploration you just did becomes the foundation of the proposal, not throwaway chat.
You can also ask explore to capture the change itself, without leaving the conversation: "start a change for this" scaffolds the folder, and "write the proposal too" writes exactly the artifacts you named. Scaffolding also lays down the change's own metadata, and fills in anything your project is missing at the top level (`openspec/specs/`, `openspec/changes/archive/`, a `config.yaml`).
That's the same destination as handing off, with one difference: propose writes the whole set your schema requires to reach implementation, while capture writes only the artifacts you named.
If you use the expanded command set, explore can hand off to `/opsx:new` instead, for step-by-step artifact creation. See [Workflows](workflows.md).
## Tips for a good exploration
- **Bring the problem, not the solution.** "Logins feel slow" gives the AI room to investigate. "Add a Redis cache" pre-commits you to an answer you haven't tested yet.
- **Ask for the tradeoffs out loud.** "What are the downsides of each option?" gets you a more honest comparison.
- **Let it read first.** The best explorations start with the AI actually looking at your code, not guessing. Point it at the relevant area if it helps.
- **It's okay to bail.** If exploration reveals the idea isn't worth it, that's a win. You learned it cheaply.
- **Explore again mid-change.** Stuck during `/opsx:apply`? You can step back and explore a sub-problem, then return.
## The honest tradeoffs
**What you gain:** explore catches wrong turns at the cheapest possible moment, before you've committed to anything. It's especially powerful in unfamiliar code, where the AI's ability to read and summarize the system saves you an afternoon of spelunking.
**What it costs:** a little patience. Explore is a conversation, so it's slower than firing off `/opsx:propose` and hoping. For work you genuinely understand already, that extra step is pure overhead, and you should skip it.
The rule of thumb: the fuzzier the task, the more explore pays off. The clearer the task, the more you can skip straight to proposing.
## Where to go next
- [Commands: `/opsx:explore`](commands.md#opsxexplore): the precise reference
- [Workflows](workflows.md): explore as part of the everyday loop
- [Examples & Recipes](examples.md#recipe-3-exploring-before-you-commit): explore in a full walkthrough
- [Getting Started](getting-started.md): the first-change guide, exploration included
+155
View File
@@ -0,0 +1,155 @@
# FAQ
Quick answers to the questions people ask most. If your question is really a "something is broken" question, [Troubleshooting](troubleshooting.md) is the better page. If you want a term defined, see the [Glossary](glossary.md).
## The basics
### What is OpenSpec, in one sentence?
A lightweight layer that gets you and your AI coding assistant to agree on what to build, in writing, before any code is written.
### Why would I want that?
Because AI assistants are confident even when they're wrong. When the requirements live only in a chat thread, the AI fills gaps with guesses, and you find out after the code exists. OpenSpec moves the agreement earlier, where mistakes are cheap to fix. See [Core Concepts at a Glance](overview.md) for the full case.
### Do I have to use it for everything?
No. Use it where agreement matters, which is most non-trivial work. For a one-character typo fix, the ceremony probably isn't worth it, and that's fine.
### Can I use it on a big existing codebase, or only new projects?
Existing codebases are the main event. OpenSpec is brownfield-first: you do not document your whole app up front. You write specs only for what each change touches, and your specs fill in over time around the work you actually do. There's a dedicated guide: [Using OpenSpec in an Existing Project](existing-projects.md).
### Is it tied to one AI tool?
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
### Where do I type `/opsx:propose`?
In your AI assistant's chat, not your terminal. This is the single most common point of confusion, so it has its own page: [How Commands Work](how-commands-work.md). Short version: `openspec ...` runs in the terminal, `/opsx:...` runs in chat.
### How do I "start interactive mode"?
There isn't a separate mode to start. You open your AI assistant like normal and type a slash command into its chat. The slash command is how you "enter" OpenSpec. (The one genuinely interactive terminal feature is `openspec view`, a dashboard for browsing specs and changes.) Full explanation in [How Commands Work](how-commands-work.md).
### I typed a slash command and nothing happened. Why?
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, 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?
Both are files OpenSpec writes so your assistant can run the workflow. Skills (`.../skills/openspec-*/SKILL.md`) are the newer cross-tool standard; commands (`.../commands/opsx-*`) are the older per-tool slash files. You don't need to pick. You just type the slash command, and OpenSpec installs whichever your tool uses.
## The workflow
### Where should I start if I'm not sure what to build?
With `/opsx:explore`. It's a no-stakes thinking partner that reads your codebase, lays out options, and turns a fuzzy problem into a concrete plan, all before any code gets written. It's in the default profile, so it's always available. When the plan is clear, it hands off to `/opsx:propose`. This is the single best habit to form, because it stops an eager AI from confidently building the wrong thing. See [Explore First](explore.md).
### What's the simplest possible flow?
```text
/opsx:explore (optional) then /opsx:propose <what you want> then /opsx:apply then /opsx:archive
```
Explore to think it through, propose to draft the plan, apply to build it, archive to file it away. Skip explore when you already know exactly what you want.
### What's the difference between `/opsx:propose` and `/opsx:new`?
`/opsx:propose` is the default one-step command: it creates the change and drafts all the planning artifacts at once. `/opsx:new` is part of the expanded command set and only scaffolds an empty change, leaving you to create artifacts one at a time with `/opsx:continue` (or all at once with `/opsx:ff`). Use propose unless you want step-by-step control. See [Commands](commands.md).
### What are `core` and expanded profiles?
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`?
Usually not. Sync merges a change's delta specs into your main specs, and `/opsx:archive` will offer to do it for you. Run sync manually only when you want the specs merged before archiving, for example on a long-running change. See [Commands](commands.md#opsxsync).
### How do I edit a proposal, spec, or task after I've started?
Just edit the file. Every artifact is plain Markdown in `openspec/changes/<name>/`, and there's no locked phase or special edit mode. Change it by hand, or ask your AI to revise it ("update the design to use a queue"), then keep going. The AI always works from the current file contents. Full guide: [Editing & Iterating on a Change](editing-changes.md).
### Can I go back and change the plan after implementing some of it?
Yes, at any time. The workflow is fluid, so review and editing aren't phases you get locked out of. Edit the artifact, then continue. If you want a structured check that the code still matches the plan, run `/opsx:verify`. See [Editing & Iterating on a Change](editing-changes.md#how-do-i-go-back-to-review-after-implementing).
### I edited the code by hand. How do I reconcile it with the spec?
Bring them back in sync before you archive, since archiving makes your specs the record of truth. If the code is now correct, update the delta spec to match what you shipped; if the spec is correct, keep building until the code agrees. `/opsx:verify` surfaces the mismatches. See [Editing & Iterating on a Change](editing-changes.md#i-edited-the-code-by-hand-how-do-i-reconcile-that-with-openspec).
### When should I update an existing change versus start a new one?
Update when it's the same work, refined. Start fresh when the intent fundamentally changed or the scope exploded into different work. There's a decision flowchart and examples in [Workflows](workflows.md#when-to-update-vs-start-fresh).
### What if my session runs out of context, or requirements change mid-implementation?
This is where specs earn their keep. Because the plan lives in files (not only in chat history), you can clear your context, start a fresh AI session, and pick up with `/opsx:apply`; it reads the artifacts and resumes from the first unchecked task. If requirements change, edit the artifacts to match the new reality and continue. Keeping a clean context window also produces better results; clear it before implementation.
### Should I commit the `openspec/` folder to git?
Yes. Your specs, active changes, and archive are part of your project's history. Commit them like any other source. The archive in particular becomes a durable record of why your system works the way it does.
## Specs and changes
### What goes in a spec versus a design?
A spec describes observable behavior: what the system does, its inputs, outputs, and error conditions. A design describes how you'll build it: the technical approach, architecture decisions, file changes. If implementation could change without changing externally visible behavior, it belongs in the design, not the spec. [Concepts](concepts.md#what-a-spec-is-and-is-not) goes deeper.
### What's a delta spec?
A spec that describes only what's changing, using `ADDED`, `MODIFIED`, and `REMOVED` sections, rather than restating the whole spec. It's how OpenSpec handles edits to existing systems cleanly. See [Concepts](concepts.md#delta-specs).
### Where do archived changes go?
To `openspec/changes/archive/YYYY-MM-DD-<name>/`, with all change artifacts preserved. The change moves out of your active list. A change that explicitly declares `retire_capabilities: true` can also delete a main capability spec when it removes that capability's final requirement.
## Configuration and customization
### How do I tell the AI about my tech stack?
Put it in `openspec/config.yaml` under `context:`. That text is injected into every planning request, so the AI always knows your stack and conventions. See [Customization](customization.md#project-configuration).
### Can I generate specs in a language other than English?
Yes. Add a language instruction to your config's `context:`. [Multi-Language](multi-language.md) has copy-paste snippets for several languages.
### Can I change the workflow itself?
Yes, with custom schemas. A schema defines which artifacts exist and how they depend on each other. Fork the default with `openspec schema fork spec-driven my-workflow`, then edit it. See [Customization](customization.md#custom-schemas).
## Models, privacy, and upgrades
### Which AI model should I use?
OpenSpec works best with high-reasoning models. The README recommends models like Codex 5.5 and Opus 4.7 for both planning and implementation. Also keep your context window clean: clear it before implementation for best results.
### Does OpenSpec collect data?
It collects anonymous usage stats: command names and version only. No arguments, paths, content, or personal data, and it's off automatically in CI. Opt out with `export OPENSPEC_TELEMETRY=0` or `export DO_NOT_TRACK=1`.
### How do I upgrade?
Two steps. Upgrade the package (`npm install -g @fission-ai/openspec@latest`), then run `openspec update` inside each project to refresh the generated skills and commands.
### How do I uninstall OpenSpec?
There's no uninstall command, because it's just a global package plus files in your project. Remove the package (`npm uninstall -g @fission-ai/openspec`), and optionally delete the `openspec/` directory and the generated tool files. Step-by-step, including what's safe to keep, is in [Installation: Uninstalling](installation.md#uninstalling).
## Getting help
### Where do I ask questions or report bugs?
- **Discord:** [discord.gg/YctCnvvshC](https://discord.gg/YctCnvvshC)
- **GitHub Issues:** [github.com/Fission-AI/OpenSpec/issues](https://github.com/Fission-AI/OpenSpec/issues)
- **From your terminal:** `openspec feedback "your message"` opens a GitHub issue for you.
### These docs are wrong or confusing. What do I do?
Tell us, or fix it. Documentation PRs are welcome and valued. Open an issue or send a pull request.
+291
View File
@@ -0,0 +1,291 @@
# Getting Started
This guide explains how OpenSpec works after you've installed and initialized it. For installation instructions, see the [main README](../README.md#quick-start) or the [Installation guide](installation.md). New to the whole docs set? The [documentation home](README.md) maps everything.
> **Where do I type these commands?** Two places, and mixing them up is the most common early stumble.
>
> - `openspec ...` commands (like `openspec init`) run in your **terminal**.
> - `/opsx:...` commands (like `/opsx:propose`) run in your **AI assistant's chat**, the same box where you'd ask it to write code.
>
> There's no separate "interactive mode" to start. You just type the slash command in chat and your assistant takes it from there. Full explanation: [How Commands Work](how-commands-work.md).
## Your First Five Minutes
The whole loop, with each step labeled by where it happens:
```text
TERMINAL $ npm install -g @fission-ai/openspec@latest
TERMINAL $ cd your-project && openspec init
AI CHAT /opsx:explore (optional: think it through first)
AI CHAT /opsx:propose add-dark-mode (AI drafts the plan; you review it)
AI CHAT /opsx:apply (AI builds it)
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 code gets written. When the picture is clear, it hands off to `/opsx:propose`. This is the single best habit for working with an AI that will otherwise confidently build the wrong thing. See the [Explore guide](explore.md).
## How It Works
OpenSpec helps you and your AI coding assistant agree on what to build before any code is written.
**Default quick path (core profile):**
```text
/opsx:explore ──► /opsx:propose ──► /opsx:apply ──► /opsx:sync ──► /opsx:archive
(optional)
```
Start with `/opsx:explore` when you're figuring out what to do, or jump straight to `/opsx:propose` when you already know. Explore is in the default profile, so it's always there when you want it.
**Expanded path (custom workflow selection):**
```text
/opsx:new ──► /opsx:ff or /opsx:continue ──► /opsx:apply ──► /opsx:verify ──► /opsx:archive
```
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
After running `openspec init`, your project has this structure:
```
openspec/
├── specs/ # Source of truth (your system's behavior)
│ └── <domain>/
│ └── spec.md
├── changes/ # Proposed updates (one folder per change)
│ └── <change-name>/
│ ├── proposal.md
│ ├── design.md
│ ├── tasks.md
│ └── specs/ # Delta specs (what's changing)
│ └── <domain>/
│ └── spec.md
└── config.yaml # Project configuration (optional)
```
**Two key directories:**
- **`specs/`** - The source of truth. These specs describe how your system currently behaves. Organized by domain (e.g., `specs/auth/`, `specs/payments/`).
- **`changes/`** - Proposed modifications. Each change gets its own folder with all related artifacts. When a change is complete, its specs merge into the main `specs/` directory.
## Understanding Artifacts
Each change folder contains artifacts that guide the work:
| Artifact | Purpose |
|----------|---------|
| `proposal.md` | The "why" and "what" - captures intent, scope, and approach |
| `specs/` | Delta specs showing ADDED/MODIFIED/REMOVED requirements |
| `design.md` | The "how" - technical approach and architecture decisions |
| `tasks.md` | Implementation checklist with checkboxes |
**Artifacts build on each other:**
```
proposal ──► specs ──► design ──► tasks ──► implement
▲ ▲ ▲ │
└───────────┴──────────┴────────────────────┘
update as you learn
```
You can always go back and refine earlier artifacts as you learn more during implementation.
## How Delta Specs Work
Delta specs are the key concept in OpenSpec. They show what's changing relative to your current specs.
### The Format
Delta specs use sections to indicate the type of change:
```markdown
# Delta for Auth
## ADDED Requirements
### Requirement: Two-Factor Authentication
The system MUST require a second factor during login.
#### Scenario: OTP required
- GIVEN a user with 2FA enabled
- WHEN the user submits valid credentials
- THEN an OTP challenge is presented
## MODIFIED Requirements
### Requirement: Session Timeout
The system SHALL expire sessions after 30 minutes of inactivity.
(Previously: 60 minutes)
#### Scenario: Idle timeout
- GIVEN an authenticated session
- WHEN 30 minutes pass without activity
- THEN the session is invalidated
## REMOVED Requirements
### Requirement: Remember Me
(Deprecated in favor of 2FA)
```
### What Happens on Archive
When you archive a change:
1. **ADDED** requirements are appended to the main spec
2. **MODIFIED** requirements replace the existing version
3. **REMOVED** requirements are deleted from the main spec
The change folder moves to `openspec/changes/archive/` for audit history.
## Example: Your First Change
Let's walk through adding dark mode to an application.
### 1. Start the Change (Default)
```text
You: /opsx:propose add-dark-mode
AI: Created openspec/changes/add-dark-mode/
✓ proposal.md — why we're doing this, what's changing
✓ specs/ — requirements and scenarios
✓ design.md — technical approach
✓ tasks.md — implementation checklist
Ready for implementation!
```
If you've enabled the expanded workflow profile, you can also do this as two steps: `/opsx:new` then `/opsx:ff` (or `/opsx:continue` incrementally).
### 2. What Gets Created
**proposal.md** - Captures the intent:
```markdown
# Proposal: Add Dark Mode
## Intent
Users have requested a dark mode option to reduce eye strain
during nighttime usage.
## Scope
- Add theme toggle in settings
- Support system preference detection
- Persist preference in localStorage
## Approach
Use CSS custom properties for theming with a React context
for state management.
```
**specs/ui/spec.md** - Delta showing new requirements:
```markdown
# Delta for UI
## ADDED Requirements
### Requirement: Theme Selection
The system SHALL allow users to choose between light and dark themes.
#### Scenario: Manual toggle
- GIVEN a user on any page
- WHEN the user clicks the theme toggle
- THEN the theme switches immediately
- AND the preference persists across sessions
#### Scenario: System preference
- GIVEN a user with no saved preference
- WHEN the application loads
- THEN the system's preferred color scheme is used
```
**tasks.md** - Implementation checklist:
```markdown
# Tasks
## 1. Theme Infrastructure
- [ ] 1.1 Create ThemeContext with light/dark state
- [ ] 1.2 Add CSS custom properties for colors
- [ ] 1.3 Implement localStorage persistence
## 2. UI Components
- [ ] 2.1 Create ThemeToggle component
- [ ] 2.2 Add toggle to settings page
- [ ] 2.3 Update Header to include quick toggle
## 3. Styling
- [ ] 3.1 Define dark theme color palette
- [ ] 3.2 Update components to use CSS variables
```
### 3. Implement
```
You: /opsx:apply
AI: Working through tasks...
✓ 1.1 Created ThemeContext with light/dark state
✓ 1.2 Added CSS custom properties to globals.css
✓ 1.3 Implemented localStorage persistence
✓ 2.1 Created ThemeToggle component
...
All tasks complete!
```
During implementation, if you discover the design needs adjustment, just update the artifact and continue.
### 4. Archive
```
You: /opsx:archive
AI: Archiving add-dark-mode...
✓ Merged specs into openspec/specs/ui/spec.md
✓ Moved to openspec/changes/archive/2025-01-24-add-dark-mode/
Done! Ready for the next feature.
```
Your delta specs are now part of the main specs, documenting how your system works.
## Verifying and Reviewing
Use the CLI to check on your changes:
```bash
# List active changes
openspec list
# View change details
openspec show add-dark-mode
# Validate spec formatting
openspec validate add-dark-mode
# Interactive dashboard
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
- [Examples & Recipes](examples.md) - Real changes, start to finish
- [Workflows](workflows.md) - Common patterns and when to use each command
- [Commands](commands.md) - Full reference for all slash commands
- [Concepts](concepts.md) - Deeper understanding of specs, changes, and schemas
- [Customization](customization.md) - Make OpenSpec work your way
- [Stores](stores-beta/user-guide.md) - Planning that spans repos or teams? Keep it in its own repo (beta)
- [FAQ](faq.md) and [Troubleshooting](troubleshooting.md) - When you get stuck
+91
View File
@@ -0,0 +1,91 @@
# Glossary
Every OpenSpec term in one place, defined in plain language. Skim it once and the rest of the docs read faster.
Terms are grouped by topic, then alphabetized within each group.
## The core nouns
**Spec.** A document describing how part of your system behaves. Specs live in `openspec/specs/`, are organized by domain, and are made of requirements and scenarios. The spec is the agreed-upon answer to "what does this software do?" See [Concepts](concepts.md#specs).
**Source of truth.** The `openspec/specs/` directory as a whole. It holds the current, agreed-upon behavior of your system. Changes propose edits to it; archiving applies them.
**Change.** One unit of work, packaged as a folder under `openspec/changes/<name>/`. A change holds everything about that work: its proposal, design, tasks, and the spec edits it introduces. One change, one feature or fix.
**Artifact.** A document inside a change. The standard artifacts are the proposal, the delta specs, the design, and the tasks. They're created in dependency order and feed into each other.
**Delta spec.** A spec inside a change that describes only what's changing, using `ADDED`, `MODIFIED`, and `REMOVED` sections, rather than restating the entire spec. This is what lets OpenSpec edit existing systems cleanly. See [Concepts](concepts.md#delta-specs).
**Domain.** A logical grouping for specs, like `auth/`, `payments/`, or `ui/`. You choose domains that match how you think about your system.
## Inside a spec
**Requirement.** A single behavior the system must have, usually written with an RFC 2119 keyword: "The system SHALL expire sessions after 30 minutes." Requirements state the *what*, not the *how*.
**Scenario.** A concrete, testable example of a requirement in action, typically in Given/When/Then form. Scenarios make a requirement verifiable: you could write an automated test from one.
**RFC 2119 keywords.** The words MUST, SHALL, SHOULD, and MAY, which carry standardized meaning about how strict a requirement is. MUST and SHALL are absolute. SHOULD is recommended with room for exceptions. MAY is optional. The name comes from the internet standards document that defined them.
## The artifacts
**Proposal (`proposal.md`).** The *why* and *what* of a change: its intent, scope, and high-level approach. The first artifact you create.
**Design (`design.md`).** The *how*: technical approach, architecture decisions, and the files you expect to touch. Optional for simple changes.
**Tasks (`tasks.md`).** The implementation checklist, with checkboxes. The AI works through it during `/opsx:apply` and checks items off as it goes.
## The lifecycle
**Archive.** The act of finishing a change. Its delta specs merge into the main specs, and the change folder moves to `openspec/changes/archive/YYYY-MM-DD-<name>/`. After archiving, your specs describe the new reality. See [Concepts](concepts.md#archive).
**Sync.** Merging a change's delta specs into the main specs *without* archiving the change. Usually automatic (archive offers to do it), but available on its own as `/opsx:sync` for long-running changes. See [Commands](commands.md#opsxsync).
## Workflow and commands
**OPSX.** The current standard OpenSpec workflow, built around fluid actions instead of rigid phases. Its slash commands all start with `/opsx:`. See [OPSX Workflow](opsx.md).
**Slash command.** A command you type into your AI assistant's chat, like `/opsx:propose`. Slash commands drive the workflow. They are not terminal commands. See [How Commands Work](how-commands-work.md).
**Explore (`/opsx:explore`).** The thinking-partner command. It reads your codebase, compares options, and clarifies a fuzzy idea into a concrete plan. It never writes code, and writes nothing else unless you ask it to capture the exploration as a change, or say yes when it offers. The recommended starting point whenever you have a problem but not yet a plan. See [Explore First](explore.md).
**CLI.** The `openspec` program you run in your terminal. It sets up projects, lists and validates changes, opens the dashboard, and archives. The terminal half of OpenSpec. See [CLI](cli.md).
**Skill.** A folder of instructions (`.../skills/openspec-*/SKILL.md`) that your AI assistant auto-detects and follows. Skills are the emerging cross-tool standard for delivering the OpenSpec workflow to your assistant.
**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`, `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`.
## Customization
**Schema.** The definition of which artifacts a workflow has and how they depend on one another. The built-in default is `spec-driven` (proposal → specs → design → tasks). You can fork it or write your own. See [Customization](customization.md#custom-schemas).
**Template.** A Markdown file inside a schema that shapes what the AI generates for a given artifact. Editing a template changes the AI's output immediately, with no rebuild.
**Project config (`openspec/config.yaml`).** Per-project settings: the default schema, the `context:` injected into every planning request, and per-artifact `rules:`. The easiest way to teach OpenSpec about your stack and conventions. See [Customization](customization.md#project-configuration).
**Context injection.** Putting project background in `config.yaml`'s `context:` field so it's automatically added to every artifact the AI generates. More reliable than hoping the AI reads a separate file.
**Dependency graph.** The directed graph formed by artifact `requires:` relationships. It's a DAG (directed acyclic graph: arrows only point forward, never in a loop), and OpenSpec uses it to know what you can create next.
**Enablers, not gates.** The principle that artifact dependencies show what becomes *possible* next, not what's *required* next. You can revisit and edit any artifact at any time. See [Core Concepts at a Glance](overview.md#enablers-not-gates).
## Coordination across repos (beta)
These terms apply only if your planning spans more than one repo. They're in beta. Most users can ignore them. See the [Stores User Guide](stores-beta/user-guide.md).
**Store.** A standalone repo whose whole job is planning. It has the same `openspec/` shape you already know (specs and changes) plus a small identity file. You register it on your machine once, by name, and then any OpenSpec command can work in it from anywhere.
**Reference.** A declaration, in a code repo's `openspec/config.yaml`, of a store that repo draws on. References are read-only: the repo keeps its own root, and `openspec instructions` gains an index of the referenced store's specs, each with the exact command to fetch it.
**Working context.** What `openspec context` assembles for the current repo: its OpenSpec root plus every store it references, each with how to fetch it. The answer to "what am I working with?"
**Workset.** A personal, machine-local set of folders you open together (a store alongside the code repos you work on). Created explicitly with `openspec workset create`; nothing about those local paths is committed to the shared planning repo.
## See also
- [Core Concepts at a Glance](overview.md): the five ideas, on one page
- [Concepts](concepts.md): the long-form explanation
- [How Commands Work](how-commands-work.md): slash commands versus the CLI
+173
View File
@@ -0,0 +1,173 @@
# How Commands Work
**The one thing to know: OpenSpec has two kinds of commands, and they run in two different places.**
- `openspec ...` commands run in your **terminal**. (Example: `openspec init`.)
- `/opsx:...` commands run in your **AI assistant's chat**. (Example: `/opsx:propose`.)
If you ever type `/opsx:propose` into your terminal and nothing happens, this page is why. You are talking to the wrong half of OpenSpec. Slash commands are not terminal commands. They are instructions you give to your AI coding assistant, in the same chat box where you'd normally type "add a login form."
That single distinction is the most common stumbling block for new users, so let's make it crystal clear.
## The two halves
OpenSpec is one project wearing two hats.
**The CLI (terminal half).** A program named `openspec` that you install and run from your shell. It sets up your project, lists and validates changes, shows a dashboard, and archives finished work. You type these into iTerm, the VS Code terminal, PowerShell, anywhere you'd run `git` or `npm`.
```bash
openspec init # set up OpenSpec in this project
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, Devin Desktop, Copilot, or whichever assistant you use.
```text
/opsx:propose add-dark-mode (typed in your AI chat)
/opsx:apply (typed in your AI chat)
/opsx:archive (typed in your AI chat)
```
Here's the mental model in one picture:
```text
YOUR TERMINAL YOUR AI ASSISTANT'S CHAT
┌──────────────────────┐ ┌──────────────────────────────┐
│ $ openspec init │ installs │ /opsx:propose add-dark-mode │
│ $ openspec list │ ──────────► │ /opsx:apply │
│ $ openspec view │ commands │ /opsx:archive │
└──────────────────────┘ & skills └──────────────────────────────┘
run openspec here run /opsx:* here
```
Notice the arrow. Running `openspec init` in your terminal is what *installs* the slash commands into your AI tool. The terminal half sets up the chat half. After that, day-to-day driving mostly happens in chat.
## "How do I start interactive mode?"
**There is no separate interactive mode to start.** This question comes up a lot, so it deserves a plain answer.
You don't enter a special OpenSpec mode. You just open your AI coding assistant like you always do, and type a slash command into the chat. The slash command *is* how you "enter" OpenSpec. Your assistant recognizes it, loads the matching OpenSpec skill, and starts following the workflow.
So the real instructions are:
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.
That's it. No mode to toggle, no daemon to launch, no separate window.
One thing that *is* genuinely interactive lives in the terminal: `openspec view`. It opens a dashboard for browsing your specs and changes. But that's a viewer, not the thing you propose and build with. The building happens through slash commands in chat.
## Why this split exists
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 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 spelling follows the file your tool loads.
| 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, Zed Agent, shared `.agents` |
| none — Kimi Code | `/skill:openspec-propose` | Kimi Code |
| none — Codex CLI | `$openspec-propose` | Codex |
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.
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 `.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 `.agents/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.
See [Supported Tools](supported-tools.md) for the exact paths per tool, and [Migration Guide](migration-guide.md) for how skills replaced the older command-only approach.
## Confirming it's installed
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. On a skills-only tool (Codex, Kimi Code, CodeArts, ForgeCode, Hermes, Mistral Vibe, Zed Agent, or the shared `.agents` target) `/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.
## Which commands do I even have?
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
A good default rhythm: `explore` when you're figuring out what to do, then `propose`, `apply`, `archive`. The [Explore First](explore.md) guide explains why that opening step pays off.
There's also an **expanded** set for people who want finer control (`/opsx:new`, `/opsx:continue`, `/opsx:ff`, `/opsx:verify`, `/opsx:bulk-archive`, `/opsx:onboard`). You turn it on with `openspec config profile`, then apply it with `openspec update`.
New to all of this? `/opsx:onboard` (in the expanded set) walks you through a complete change on your own codebase, narrating each step. It's the friendliest possible introduction.
For what each command does in detail, see [Commands](commands.md). For when to reach for which, see [Workflows](workflows.md).
## A clean first run
Putting it together, here is the whole sequence with each step labeled by where it happens.
```text
TERMINAL $ npm install -g @fission-ai/openspec@latest
TERMINAL $ cd your-project
TERMINAL $ openspec init
(installs slash commands into your AI tool)
AI CHAT /opsx:explore
(optional: think the idea through with the AI first)
AI CHAT /opsx:propose add-dark-mode
(AI drafts proposal, specs, design, tasks)
AI CHAT /opsx:apply
(AI builds it, checking off tasks)
AI CHAT /opsx:archive
(change is merged into your specs and filed away)
```
Two terminal steps to set up. Then you live in chat. That's the rhythm.
## Related
- [Getting Started](getting-started.md): the full first-change walkthrough
- [Commands](commands.md): every slash command in detail
- [CLI](cli.md): every terminal command in detail
- [Supported Tools](supported-tools.md): per-tool syntax and file locations
- [FAQ](faq.md): more quick answers
- [Troubleshooting](troubleshooting.md): fixes when commands don't show up
+206
View File
@@ -0,0 +1,206 @@
# Installation
## Prerequisites
- **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
```bash
npm install -g @fission-ai/openspec@latest
```
### pnpm
```bash
pnpm add -g @fission-ai/openspec@latest
```
### yarn
```bash
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.
### deno
Deno sometimes has issues parsing the @latest tag, but we can specify a version while installing initially.
If that happens, you could try to change the @latest tag with the version, something like `@^1.3.1`
```bash
deno install --global \
--allow-read --allow-write --allow-env --allow-sys=cpus,homedir --allow-net=edge.openspec.dev \
npm:@fission-ai/openspec@latest
# or
deno install --global \
--allow-read --allow-write --allow-env --allow-sys=cpus,homedir --allow-net=edge.openspec.dev \
npm:@fission-ai/openspec@^1.3.1
```
Note: If your subcommands launch external tools, like config edit, feedback, or workspace open, you may need a scoped --allow-run=<program>.
### bun
Bun can install OpenSpec globally, but OpenSpec currently runs on Node.js.
You still need Node.js 20.19.0 or higher available on `PATH`.
```bash
bun add -g @fission-ai/openspec@latest
```
## Nix
Run OpenSpec directly without installation:
```bash
nix run github:Fission-AI/OpenSpec -- init
```
Or install to your profile:
```bash
nix profile install github:Fission-AI/OpenSpec
```
Or add to your development environment in `flake.nix`:
```nix
{
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
openspec.url = "github:Fission-AI/OpenSpec";
};
outputs = { nixpkgs, openspec, ... }: {
devShells.x86_64-linux.default = nixpkgs.legacyPackages.x86_64-linux.mkShell {
buildInputs = [ openspec.packages.x86_64-linux.default ];
};
};
}
```
## Verify Installation
```bash
openspec --version
```
## Updating
Upgrade the package, then refresh each project's generated files:
```bash
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. 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
There's no `openspec uninstall` command, because OpenSpec is just a global package plus some files in your project. Removing it is a few manual steps, and nothing here touches your source code.
**1. Remove the global package:**
```bash
npm uninstall -g @fission-ai/openspec # or: pnpm rm -g / yarn global remove / bun rm -g
```
**2. Remove OpenSpec from a project (optional).** Delete the `openspec/` directory if you no longer want its specs and changes:
```bash
rm -rf openspec/
```
Think before you do this: `openspec/specs/` and `openspec/changes/archive/` are your record of how the system behaves and why it changed. If you might want that history, keep the folder (or keep it in git) even after uninstalling.
**3. Remove generated AI tool files (optional).** OpenSpec writes skill and command files into per-tool directories like `.claude/skills/openspec-*/`, `.cursor/commands/opsx-*`, and so on. Delete the `openspec-*` skills and `opsx-*` commands for whichever tools you configured. The exact paths per tool are listed in [Supported Tools](supported-tools.md).
If you also have OpenSpec marker blocks in files like `CLAUDE.md` or `AGENTS.md`, remove those blocks by hand; your own content in those files is yours to keep.
## Next Steps
After installing, initialize OpenSpec in your project:
```bash
cd your-project
openspec init
```
See [Getting Started](getting-started.md) for a full walkthrough.
+604
View File
@@ -0,0 +1,604 @@
# Migrating to OPSX
This guide helps you transition from the legacy OpenSpec workflow to OPSX. The migration is designed to be smooth—your existing work is preserved, and the new system offers more flexibility.
## What's Changing?
OPSX replaces the old phase-locked workflow with a fluid, action-based approach. Here's the key shift:
| Aspect | Legacy | OPSX |
|--------|--------|------|
| **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 |
| **Configuration** | `CLAUDE.md` with markers + `project.md` | Clean config in `openspec/config.yaml` |
**The philosophy change:** Work isn't linear. OPSX stops pretending it is.
---
## Before You Begin
### Your Existing Work Is Safe
The migration process is designed with preservation in mind:
- **Active changes in `openspec/changes/`** — Completely preserved. You can continue them with OPSX commands.
- **Archived changes** — Untouched. Your history remains intact.
- **Main specs in `openspec/specs/`** — Untouched. These are your source of truth.
- **Your content in CLAUDE.md, AGENTS.md, etc.** — Preserved. Only the OpenSpec marker blocks are removed; everything you wrote stays.
### What Gets Removed
Only OpenSpec-managed files that are being replaced:
| What | Why |
|------|-----|
| Legacy slash command directories/files | Replaced by the new skills system |
| `openspec/AGENTS.md` | Obsolete workflow trigger |
| OpenSpec markers in `CLAUDE.md`, `AGENTS.md`, etc. | No longer needed |
**Legacy command locations by tool** (examples—your tool may vary):
- Claude Code: `.claude/commands/openspec/`
- Cursor: `.cursor/commands/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 the canonical `.agents/skills/openspec-*` path. OpenSpec-managed `SKILL.md` files under the former `.codex/skills` path are reconciled only after replacements exist; custom files and divergent copies stay in place. If an unmarked `.agents` tree already contains OpenSpec skills, OpenSpec preserves its existing Codex (`$openspec-*`) or generic (`/openspec-*`) rendering instead of guessing from the legacy directory. Select `codex` explicitly with `openspec init` to switch ownership. Legacy prompt cleanup still targets only OpenSpec's allowlisted filenames in `$CODEX_HOME/prompts` or `~/.codex/prompts`.
- And others (Augment, Continue, Amazon Q, etc.)
The migration detects whichever tools you have configured and cleans up their legacy files.
The removal list may seem long, but these are all files that OpenSpec originally created. Your own content is never deleted.
### What Needs Your Attention
One file requires manual migration:
**`openspec/project.md`** — This file isn't deleted automatically because it may contain project context you've written. You'll need to:
1. Review its contents
2. Move useful context to `openspec/config.yaml` (see guidance below)
3. Delete the file when ready
**Why we made this change:**
The old `project.md` was passive—agents might read it, might not, might forget what they read. We found reliability was inconsistent.
The new `config.yaml` context is **actively injected into every OpenSpec planning request**. This means your project conventions, tech stack, and rules are always present when the AI is creating artifacts. Higher reliability.
**The tradeoff:**
Because context is injected into every request, you'll want to be concise. Focus on what really matters:
- Tech stack and key conventions
- Non-obvious constraints the AI needs to know
- Rules that frequently got ignored before
Don't worry about getting it perfect. We're still learning what works best here, and we'll be improving how context injection works as we experiment.
---
## Running the Migration
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`, `update`, `sync`, `archive`).
- Migrated installs preserve your previously installed workflows by writing a `custom` profile when needed.
### Using `openspec init`
Run this if you want to add new tools or reconfigure which tools are set up:
```bash
openspec init
```
The init command detects legacy files and guides you through cleanup:
```
Upgrading to the new OpenSpec
OpenSpec now uses agent skills, the emerging standard across coding
agents. This simplifies your setup while keeping everything working
as before.
Files to remove
No user content to preserve:
• .claude/commands/openspec/
• openspec/AGENTS.md
Files to update
OpenSpec markers will be removed, your content preserved:
• CLAUDE.md
• AGENTS.md
Needs your attention
• openspec/project.md
We won't delete this file. It may contain useful project context.
The new openspec/config.yaml has a "context:" section for planning
context. This is included in every OpenSpec request and works more
reliably than the old project.md approach.
Review project.md, move any useful content to config.yaml's context
section, then delete the file when ready.
? Upgrade and clean up legacy files? (Y/n)
```
**What happens when you say yes:**
1. Legacy slash command directories are removed
2. OpenSpec markers are stripped from `CLAUDE.md`, `AGENTS.md`, etc. (your content stays)
3. `openspec/AGENTS.md` is deleted
4. New skills are installed in `.claude/skills/`
5. `openspec/config.yaml` is created with a default schema
### Using `openspec update`
Run this if you just want to migrate and refresh your existing tools to the latest version:
```bash
openspec update
```
The update command also detects and cleans up legacy artifacts, then refreshes generated skills/commands to match your current profile and delivery settings.
### Non-Interactive / CI Environments
For scripted migrations:
```bash
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 `.agents/skills/openspec-*` skills exist, and preserves all other files.
---
## Migrating project.md to config.yaml
The old `openspec/project.md` was a freeform markdown file for project context. The new `openspec/config.yaml` is structured and—critically—**injected into every planning request** so your conventions are always present when the AI works.
### Before (project.md)
```markdown
# Project Context
This is a TypeScript monorepo using React and Node.js.
We use Jest for testing and follow strict ESLint rules.
Our API is RESTful and documented in docs/api.md.
## Conventions
- All public APIs must maintain backwards compatibility
- New features should include tests
- Use Given/When/Then format for specifications
```
### After (config.yaml)
```yaml
schema: spec-driven
context: |
Tech stack: TypeScript, React, Node.js
Testing: Jest with React Testing Library
API: RESTful, documented in docs/api.md
We maintain backwards compatibility for all public APIs
rules:
proposal:
- Include rollback plan for risky changes
specs:
- Use Given/When/Then format for scenarios
- Reference existing patterns before inventing new ones
design:
- Include sequence diagrams for complex flows
```
### Key Differences
| project.md | config.yaml |
|------------|-------------|
| Freeform markdown | Structured YAML |
| One blob of text | Separate context and per-artifact rules |
| Unclear when it's used | Context appears in ALL artifacts; rules appear in matching artifacts only |
| No schema selection | Explicit `schema:` field sets default workflow |
### What to Keep, What to Drop
When migrating, be selective. Ask yourself: "Does the AI need this for *every* planning request?"
**Good candidates for `context:`**
- Tech stack (languages, frameworks, databases)
- Key architectural patterns (monorepo, microservices, etc.)
- Non-obvious constraints ("we can't use library X because...")
- Critical conventions that often get ignored
**Move to `rules:` instead**
- Artifact-specific formatting ("use Given/When/Then in specs")
- Review criteria ("proposals must include rollback plans")
- These only appear for the matching artifact, keeping other requests lighter
**Leave out entirely**
- General best practices the AI already knows
- Verbose explanations that could be summarized
- Historical context that doesn't affect current work
### Migration Steps
1. **Create config.yaml** (if not already created by init):
```yaml
schema: spec-driven
```
2. **Add your context** (be concise—this goes into every request):
```yaml
context: |
Your project background goes here.
Focus on what the AI genuinely needs to know.
```
3. **Add per-artifact rules** (optional):
```yaml
rules:
proposal:
- Your proposal-specific guidance
specs:
- Your spec-writing rules
```
4. **Delete project.md** once you've moved everything useful.
**Don't overthink it.** Start with the essentials and iterate. If you notice the AI missing something important, add it. If context feels bloated, trim it. This is a living document.
### Need Help? Use This Prompt
If you're unsure how to distill your project.md, ask your AI assistant:
```
I'm migrating from OpenSpec's old project.md to the new config.yaml format.
Here's my current project.md:
[paste your project.md content]
Please help me create a config.yaml with:
1. A concise `context:` section (this gets injected into every planning request, so keep it tight—focus on tech stack, key constraints, and conventions that often get ignored)
2. `rules:` for specific artifacts if any content is artifact-specific (e.g., "use Given/When/Then" belongs in specs rules, not global context)
Leave out anything generic that AI models already know. Be ruthless about brevity.
```
The AI will help you identify what's essential vs. what can be trimmed.
---
## The New Commands
Command availability is profile-dependent:
**Default (`core` profile):**
| Command | Purpose |
|---------|---------|
| `/opsx:propose` | Create a change and generate planning artifacts in one step |
| `/opsx:explore` | Think through ideas with no structure |
| `/opsx:apply` | Implement tasks from tasks.md |
| `/opsx: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):**
| Command | Purpose |
|---------|---------|
| `/opsx:new` | Start a new change scaffold |
| `/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:bulk-archive` | Archive multiple changes at once |
| `/opsx:onboard` | Guided end-to-end onboarding workflow |
Enable expanded commands with `openspec config profile`, then run `openspec update`.
### Command Mapping from Legacy
| Legacy | OPSX Equivalent |
|--------|-----------------|
| `/openspec:proposal` | `/opsx:propose` (default) or `/opsx:new` then `/opsx:ff` (expanded) |
| `/openspec:apply` | `/opsx:apply` |
| `/openspec:archive` | `/opsx:archive` |
### New Capabilities
These capabilities are part of the expanded workflow command set.
**Granular artifact creation:**
```
/opsx:continue
```
Creates one artifact at a time based on dependencies. Use this when you want to review each step.
**Exploration mode:**
```
/opsx:explore
```
Think through ideas with a partner before committing to a change.
---
## Understanding the New Architecture
### From Phase-Locked to Fluid
The legacy workflow forced linear progression:
```
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ PLANNING │ ───► │ IMPLEMENTING │ ───► │ ARCHIVING │
│ PHASE │ │ PHASE │ │ PHASE │
└──────────────┘ └──────────────┘ └──────────────┘
If you're in implementation and realize the design is wrong?
Too bad. Phase gates don't let you go back easily.
```
OPSX uses actions, not phases:
```
┌───────────────────────────────────────────────┐
│ ACTIONS (not phases) │
│ │
│ new ◄──► continue ◄──► apply ◄──► archive │
│ │ │ │ │ │
│ └──────────┴───────────┴─────────────┘ │
│ any order │
└───────────────────────────────────────────────┘
```
### Dependency Graph
Artifacts form a directed graph. Dependencies are enablers, not gates:
```
proposal
(root node)
│
┌─────────────┴─────────────┐
│ │
▼ ▼
specs design
(requires: (requires:
proposal) proposal)
│ │
└─────────────┬─────────────┘
│
▼
tasks
(requires:
specs, design)
```
When you run `/opsx:continue`, it checks what's ready and offers the next artifact. You can also create multiple ready artifacts in any order.
### Skills vs Commands
The legacy system used tool-specific command files:
```
.claude/commands/openspec/
├── proposal.md
├── apply.md
└── archive.md
```
OPSX uses the emerging **skills** standard:
```
.claude/skills/
├── openspec-explore/SKILL.md
├── openspec-new-change/SKILL.md
├── openspec-continue-change/SKILL.md
├── openspec-apply-change/SKILL.md
└── ...
```
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 `.agents/skills/openspec-*` directories instead.
---
## Continuing Existing Changes
Your in-progress changes work seamlessly with OPSX commands.
**Have an active change from the legacy workflow?**
```
/opsx:apply add-my-feature
```
OPSX reads the existing artifacts and continues from where you left off.
**Want to add more artifacts to an existing change?**
```
/opsx:continue add-my-feature
```
Shows what's ready to create based on what already exists.
**Need to see status?**
```bash
openspec status --change add-my-feature
```
---
## The New Config System
### config.yaml Structure
```yaml
# Required: Default schema for new changes
schema: spec-driven
# Optional: Project context (max 50KB)
# Injected into ALL artifact instructions
context: |
Your project background, tech stack,
conventions, and constraints.
# Optional: Per-artifact rules
# Only injected into matching artifacts
rules:
proposal:
- Include rollback plan
specs:
- Use Given/When/Then format
design:
- Document fallback strategies
tasks:
- Break into 2-hour maximum chunks
```
### Schema Resolution
When determining which schema to use, OPSX checks in order:
1. **CLI flag**: `--schema <name>` (highest priority)
2. **Change metadata**: `.openspec.yaml` in the change directory
3. **Project config**: `openspec/config.yaml`
4. **Default**: `spec-driven`
### Available Schemas
| Schema | Artifacts | Best For |
|--------|-----------|----------|
| `spec-driven` | proposal → specs → design → tasks | Most projects |
List all available schemas:
```bash
openspec schemas
```
### Custom Schemas
Create your own workflow:
```bash
openspec schema init my-workflow
```
Or fork an existing one:
```bash
openspec schema fork spec-driven my-workflow
```
See [Customization](customization.md) for details.
---
## Troubleshooting
### "Legacy files detected in non-interactive mode"
You're running in a CI or non-interactive environment. Use:
```bash
openspec init --force
```
### Commands not appearing after migration
Restart your IDE. Skills are detected at startup.
### "Unknown artifact ID in rules"
Check that your `rules:` keys match your schema's artifact IDs:
- **spec-driven**: `proposal`, `specs`, `design`, `tasks`
Run this to see valid artifact IDs:
```bash
openspec schemas --json
```
### Config not being applied
1. Ensure the file is at `openspec/config.yaml` (not `.yml`)
2. Validate YAML syntax
3. Config changes take effect immediately—no restart needed
### project.md not migrated
The system intentionally preserves `project.md` because it may contain your custom content. Review it manually, move useful parts to `config.yaml`, then delete it.
### Want to see what would be cleaned up?
Run init and decline the cleanup prompt—you'll see the full detection summary without any changes being made.
---
## Quick Reference
### Files After Migration
```
project/
├── openspec/
│ ├── specs/ # Unchanged
│ ├── changes/ # Unchanged
│ │ └── archive/ # Unchanged
│ └── config.yaml # NEW: Project configuration
├── .claude/
│ └── skills/ # NEW: OPSX skills
│ ├── 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
```
### What's Gone
- `.claude/commands/openspec/` — replaced by `.claude/skills/`
- `openspec/AGENTS.md` — obsolete
- `openspec/project.md` — migrate to `config.yaml`, then delete
- OpenSpec marker blocks in `CLAUDE.md`, `AGENTS.md`, etc.
### Command Cheatsheet
```text
/opsx:propose Start quickly (default core profile)
/opsx:apply Implement tasks
/opsx:archive Finish and archive
# Expanded workflow (if enabled):
/opsx:new Scaffold a change
/opsx:continue Create next artifact
/opsx:ff Create planning artifacts
```
---
## Getting Help
- **Discord**: [discord.gg/YctCnvvshC](https://discord.gg/YctCnvvshC)
- **GitHub Issues**: [github.com/Fission-AI/OpenSpec/issues](https://github.com/Fission-AI/OpenSpec/issues)
- **Documentation**: [docs/opsx.md](opsx.md) for the full OPSX reference
+132
View File
@@ -0,0 +1,132 @@
# Multi-Language Guide
Configure OpenSpec to generate artifacts in languages other than English.
## Quick Setup
For a new project, set the language during initialization:
```bash
openspec init --language "Portuguese (pt-BR)"
```
This writes the language instruction to `openspec/config.yaml`. If the project
already has a config, edit its `context` field directly so existing project
guidance is preserved.
You can also configure the same behavior manually:
Add a language instruction to your `openspec/config.yaml`:
```yaml
schema: spec-driven
context: |
Language: Portuguese (pt-BR)
All artifacts must be written in Brazilian Portuguese.
Keep OpenSpec structural headings and SHALL/MUST keywords in English.
# Your other project context below...
Tech stack: TypeScript, React, Node.js
```
That's it. All generated artifacts will now be in Portuguese.
OpenSpec's document structure and normative `SHALL`/`MUST` keywords remain in
English because validation relies on them. The surrounding requirement and
scenario prose can use your selected language.
## Language Examples
### Portuguese (Brazil)
```yaml
context: |
Language: Portuguese (pt-BR)
All artifacts must be written in Brazilian Portuguese.
```
### Spanish
```yaml
context: |
Idioma: Español
Todos los artefactos deben escribirse en español.
```
### Chinese (Simplified)
```yaml
context: |
语言:中文(简体)
所有产出物必须用简体中文撰写。
```
### Japanese
```yaml
context: |
言語:日本語
すべての成果物は日本語で作成してください。
```
### French
```yaml
context: |
Langue : Français
Tous les artefacts doivent être rédigés en français.
```
### German
```yaml
context: |
Sprache: Deutsch
Alle Artefakte müssen auf Deutsch verfasst werden.
```
## Tips
### Handle Technical Terms
Decide how to handle technical terminology:
```yaml
context: |
Language: Japanese
Write in Japanese, but:
- Keep technical terms like "API", "REST", "GraphQL" in English
- Code examples and file paths remain in English
```
### Combine with Other Context
Language settings work alongside your other project context:
```yaml
schema: spec-driven
context: |
Language: Portuguese (pt-BR)
All artifacts must be written in Brazilian Portuguese.
Tech stack: TypeScript, React 18, Node.js 20
Database: PostgreSQL with Prisma ORM
```
## Verification
To verify your language config is working:
```bash
# Check the instructions - should show your language context
openspec instructions proposal --change my-change
# Output will include your language context
```
## Related Documentation
- [Customization Guide](./customization.md) - Project configuration options
- [Workflows Guide](./workflows.md) - Full workflow documentation
+675
View File
@@ -0,0 +1,675 @@
# OPSX Workflow
> Feedback welcome on [Discord](https://discord.gg/YctCnvvshC).
## What Is It?
OPSX is now the standard workflow for OpenSpec.
It's a **fluid, iterative workflow** for OpenSpec changes. No more rigid phases — just actions you can take anytime.
## Why This Exists
The legacy OpenSpec workflow works, but it's **locked down**:
- **Instructions are hardcoded** — buried in TypeScript, you can't change them
- **All-or-nothing** — one big command creates everything, can't test individual pieces
- **Fixed structure** — same workflow for everyone, no customization
- **Black box** — when AI output is bad, you can't tweak the prompts
**OPSX opens it up.** Now anyone can:
1. **Experiment with instructions** — edit a template, see if the AI does better
2. **Test granularly** — validate each artifact's instructions independently
3. **Customize workflows** — define your own artifacts and dependencies
4. **Iterate quickly** — change a template, test immediately, no rebuild
```
Legacy workflow: OPSX:
┌────────────────────────┐ ┌────────────────────────┐
│ Hardcoded in package │ │ schema.yaml │◄── You edit this
│ (can't change) │ │ templates/*.md │◄── Or this
│ ↓ │ │ ↓ │
│ Wait for new release │ │ Instant effect │
│ ↓ │ │ ↓ │
│ Hope it's better │ │ Test it yourself │
└────────────────────────┘ └────────────────────────┘
```
**This is for everyone:**
- **Teams** — create workflows that match how you actually work
- **Power users** — tweak prompts to get better AI outputs for your codebase
- **OpenSpec contributors** — experiment with new approaches without releases
We're all still learning what works best. OPSX lets us learn together.
## The User Experience
**The problem with linear workflows:**
You're "in planning phase", then "in implementation phase", then "done". But real work doesn't work that way. You implement something, realize your design was wrong, need to update specs, continue implementing. Linear phases fight against how work actually happens.
**OPSX approach:**
- **Actions, not phases** — create, implement, update, archive — do any of them anytime
- **Dependencies are enablers** — they show what's possible, not what's required next
```
proposal ──→ specs ──→ design ──→ tasks ──→ implement
```
## Setup
```bash
# Make sure you have openspec installed — skills are automatically generated
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`, `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.
## Project Configuration
Project config lets you set defaults and inject project-specific context into all artifacts.
### Creating Config
Config is created during `openspec init`, or manually:
```yaml
# openspec/config.yaml
schema: spec-driven
context: |
Tech stack: TypeScript, React, Node.js
API conventions: RESTful, JSON responses
Testing: Vitest for unit tests, Playwright for e2e
Style: ESLint with Prettier, strict TypeScript
rules:
proposal:
- Include rollback plan
- Identify affected teams
specs:
- Use Given/When/Then format for scenarios
design:
- Include sequence diagrams for complex flows
```
### Config Fields
| Field | Type | Description |
|-------|------|-------------|
| `schema` | string | Default schema for new changes (e.g., `spec-driven`) |
| `context` | string | Project context injected into all artifact instructions |
| `rules` | object | Per-artifact rules, keyed by artifact ID |
### How It Works
**Schema precedence** (highest to lowest):
1. CLI flag (`--schema <name>`)
2. Change metadata (`.openspec.yaml` in change directory)
3. Project config (`openspec/config.yaml`)
4. Default (`spec-driven`)
**Context injection:**
- Context is prepended to every artifact's instructions
- Wrapped in `<context>...</context>` tags
- Helps AI understand your project's conventions
**Rules injection:**
- Rules are only injected for matching artifacts
- Wrapped in `<rules>...</rules>` tags
- Appear after context, before the template
### Artifact IDs by Schema
**spec-driven** (default):
- `proposal` — Change proposal
- `specs` — Specifications
- `design` — Technical design
- `tasks` — Implementation tasks
### Config Validation
- Unknown artifact IDs in `rules` generate warnings
- Schema names are validated against available schemas
- Context has a 50KB size limit
- Invalid YAML is reported with line numbers
### Troubleshooting
**"Unknown artifact ID in rules: X"**
- Check artifact IDs match your schema (see list above)
- Run `openspec schemas --json` to see artifact IDs for each schema
**Config not being applied:**
- Ensure file is at `openspec/config.yaml` (not `.yml`)
- Check YAML syntax with a validator
- Config changes take effect immediately (no restart needed)
**Context too large:**
- Context is limited to 50KB
- Summarize or link to external docs instead
## Commands
| Command | What it does |
|---------|--------------|
| `/opsx:propose` | Create a change and generate planning artifacts in one step (default quick path) |
| `/opsx:explore` | Think through ideas, investigate problems, clarify requirements |
| `/opsx:new` | Start a new change scaffold (expanded workflow) |
| `/opsx:continue` | Create the next artifact (expanded workflow) |
| `/opsx:ff` | Fast-forward planning artifacts (expanded workflow) |
| `/opsx: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` | Merge delta specs into main specs (optional) |
| `/opsx:archive` | Archive when done |
| `/opsx:bulk-archive` | Archive multiple completed changes (expanded workflow) |
| `/opsx:onboard` | Guided walkthrough of an end-to-end change (expanded workflow) |
## Usage
### Explore an idea
```
/opsx:explore
```
Think through ideas, investigate problems, compare options. No structure required - just a thinking partner. When insights crystallize, transition to `/opsx:propose` (default) or `/opsx:new`/`/opsx:ff` (expanded).
### Start a new change
```
/opsx:propose
```
Creates the change and generates planning artifacts needed before implementation.
If you've enabled expanded workflows, you can instead use:
```text
/opsx:new # scaffold only
/opsx:continue # create one artifact at a time
/opsx:ff # create all planning artifacts at once
```
### Create artifacts
```
/opsx:continue
```
Shows what's ready to create based on dependencies, then creates one artifact. Use repeatedly to build up your change incrementally.
```
/opsx:ff add-dark-mode
```
Creates all planning artifacts at once. Use when you have a clear picture of what you're building.
### Implement (the fluid part)
```
/opsx:apply
```
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). It never edits code. Every edit is confirmed with you first. See [the update reference](commands.md#opsxupdate) for how it handles missing files without starting a new artifact.
If the change was already implemented, it recommends `/opsx:apply` so the code catches up with the revised plan. If your revision changes the change's *intent*, start fresh instead. See [When to Update vs. Start Fresh](#when-to-update-vs-start-fresh).
### Sync delta specs
```text
/opsx:sync
```
Merges the current change's delta specs into your main `openspec/specs/` without archiving — the change stays active. It applies the whole delta: a requirement under `## REMOVED` is deleted from the main spec and a renamed one is retitled in place, while content the delta doesn't mention is left untouched. Syncing is optional — archive prompts you to sync first if you haven't. Reach for it when you want main specs updated before archiving, when a parallel change needs to build on specs this one just added, or when you want to review the merged main spec before archiving.
### Finish up
```
/opsx:archive # Move to archive when done (prompts to sync specs if needed)
```
## When to Update vs. Start Fresh
You can always edit your proposal or specs before implementation. But when does refining become "this is different work"?
### What a Proposal Captures
A proposal defines three things:
1. **Intent** — What problem are you solving?
2. **Scope** — What's in/out of bounds?
3. **Approach** — How will you solve it?
The question is: which changed, and by how much?
### Update the Existing Change When:
**Same intent, refined execution**
- You discover edge cases you didn't consider
- The approach needs tweaking but the goal is unchanged
- Implementation reveals the design was slightly off
**Scope narrows**
- You realize full scope is too big, want to ship MVP first
- "Add dark mode" → "Add dark mode toggle (system preference in v2)"
**Learning-driven corrections**
- Codebase isn't structured how you thought
- A dependency doesn't work as expected
- "Use CSS variables" → "Use Tailwind's dark: prefix instead"
### Start a New Change When:
**Intent fundamentally changed**
- The problem itself is different now
- "Add dark mode" → "Add comprehensive theme system with custom colors, fonts, spacing"
**Scope exploded**
- Change grew so much it's essentially different work
- Original proposal would be unrecognizable after updates
- "Fix login bug" → "Rewrite auth system"
**Original is completable**
- The original change can be marked "done"
- New work stands alone, not a refinement
- Complete "Add dark mode MVP" → Archive → New change "Enhance dark mode"
### The Heuristics
```
┌─────────────────────────────────────┐
│ Is this the same work? │
└──────────────┬──────────────────────┘
│
┌──────────────────┼──────────────────┐
│ │ │
▼ ▼ ▼
Same intent? >50% overlap? Can original
Same problem? Same scope? be "done" without
│ │ these changes?
│ │ │
┌────────┴────────┐ ┌──────┴──────┐ ┌───────┴───────┐
│ │ │ │ │ │
YES NO YES NO NO YES
│ │ │ │ │ │
▼ ▼ ▼ ▼ ▼ ▼
UPDATE NEW UPDATE NEW UPDATE NEW
```
| Test | Update | New Change |
|------|--------|------------|
| **Identity** | "Same thing, refined" | "Different work" |
| **Scope overlap** | >50% overlaps | <50% overlaps |
| **Completion** | Can't be "done" without changes | Can finish original, new work stands alone |
| **Story** | Update chain tells coherent story | Patches would confuse more than clarify |
### The Principle
> **Update preserves context. New change provides clarity.**
>
> Choose update when the history of your thinking is valuable.
> Choose new when starting fresh would be clearer than patching.
Think of it like git branches:
- Keep committing while working on the same feature
- Start a new branch when it's genuinely new work
- Sometimes merge a partial feature and start fresh for phase 2
## What's Different?
| | Legacy (`/openspec:proposal`) | OPSX (`/opsx:*`) |
|---|---|---|
| **Structure** | One big proposal document | Discrete artifacts with dependencies |
| **Workflow** | Linear phases: plan → implement → archive | Fluid actions — do anything anytime |
| **Iteration** | Awkward to go back | Update artifacts as you learn |
| **Customization** | Fixed structure | Schema-driven (define your own artifacts) |
**The key insight:** work isn't linear. OPSX stops pretending it is.
## Architecture Deep Dive
This section explains how OPSX works under the hood and how it compares to the legacy workflow.
Examples in this section use the expanded command set (`new`, `continue`, etc.); default `core` users can map the same flow to `propose → apply → sync → archive`.
### Philosophy: Phases vs Actions
```
┌─────────────────────────────────────────────────────────────────────────────┐
│ LEGACY WORKFLOW │
│ (Phase-Locked, All-or-Nothing) │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ PLANNING │ ───► │ IMPLEMENTING │ ───► │ ARCHIVING │ │
│ │ PHASE │ │ PHASE │ │ PHASE │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ /openspec:proposal /openspec:apply /openspec:archive │
│ │
│ • Creates ALL artifacts at once │
│ • Can't go back to update specs during implementation │
│ • Phase gates enforce linear progression │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────────┐
│ OPSX WORKFLOW │
│ (Fluid Actions, Iterative) │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌────────────────────────────────────────────┐ │
│ │ ACTIONS (not phases) │ │
│ │ │ │
│ │ new ◄──► continue ◄──► apply ◄──► archive │ │
│ │ │ │ │ │ │ │
│ │ └──────────┴───────────┴───────────┘ │ │
│ │ any order │ │
│ └────────────────────────────────────────────┘ │
│ │
│ • Create artifacts one at a time OR fast-forward │
│ • Update specs/design/tasks during implementation │
│ • Dependencies enable progress, phases don't exist │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
```
### Component Architecture
**Legacy workflow** uses hardcoded templates in TypeScript:
```
┌─────────────────────────────────────────────────────────────────────────────┐
│ LEGACY WORKFLOW COMPONENTS │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ Hardcoded Templates (TypeScript strings) │
│ │ │
│ ▼ │
│ Tool-specific configurators/adapters │
│ │ │
│ ▼ │
│ Generated Command Files (.claude/commands/openspec/*.md) │
│ │
│ • Fixed structure, no artifact awareness │
│ • Change requires code modification + rebuild │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
```
**OPSX** uses external schemas and a dependency graph engine:
```
┌─────────────────────────────────────────────────────────────────────────────┐
│ OPSX COMPONENTS │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ Schema Definitions (YAML) │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ name: spec-driven │ │
│ │ artifacts: │ │
│ │ - id: proposal │ │
│ │ generates: proposal.md │ │
│ │ requires: [] ◄── Dependencies │ │
│ │ - id: specs │ │
│ │ generates: specs/**/*.md ◄── Glob patterns │ │
│ │ requires: [proposal] ◄── Enables after proposal │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ Artifact Graph Engine │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ • Topological sort (dependency ordering) │ │
│ │ • State detection (filesystem existence) │ │
│ │ • Rich instruction generation (templates + context) │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ Skill Files (.claude/skills/openspec-*/SKILL.md) │
│ │
│ • Cross-editor compatible (Claude Code, Cursor, Devin) │
│ • Skills query CLI for structured data │
│ • Fully customizable via schema files │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
```
### Dependency Graph Model
Artifacts form a directed acyclic graph (DAG). Dependencies are **enablers**, not gates:
```
proposal
(root node)
│
┌─────────────┴─────────────┐
│ │
▼ ▼
specs design
(requires: (requires:
proposal) proposal)
│ │
└─────────────┬─────────────┘
│
▼
tasks
(requires:
specs, design)
│
▼
┌──────────────┐
│ APPLY PHASE │
│ (requires: │
│ tasks) │
└──────────────┘
```
**State transitions:**
```
BLOCKED ────────────────► READY ────────────────► DONE
│ │ │
Missing All deps File exists
dependencies are DONE on filesystem
```
### Information Flow
**Legacy workflow** — agent receives static instructions:
```
User: "/openspec:proposal"
│
▼
┌─────────────────────────────────────────┐
│ Static instructions: │
│ • Create proposal.md │
│ • Create tasks.md │
│ • Create design.md │
│ • Create delta spec files │
│ │
│ No awareness of what exists or │
│ dependencies between artifacts │
└─────────────────────────────────────────┘
│
▼
Agent creates ALL artifacts in one go
```
**OPSX** — agent queries for rich context:
```
User: "/opsx:continue"
│
▼
┌──────────────────────────────────────────────────────────────────────────┐
│ Step 1: Query current state │
│ ┌────────────────────────────────────────────────────────────────────┐ │
│ │ $ openspec status --change "add-auth" --json │ │
│ │ │ │
│ │ { │ │
│ │ "artifacts": [ │ │
│ │ {"id": "proposal", "status": "done"}, │ │
│ │ {"id": "specs", "status": "ready"}, ◄── First ready │ │
│ │ {"id": "design", "status": "ready"}, │ │
│ │ {"id": "tasks", "status": "blocked", │ │
│ │ "missingDeps": ["specs", "design"]} │ │
│ │ ] │ │
│ │ } │ │
│ └────────────────────────────────────────────────────────────────────┘ │
│ │
│ Step 2: Get rich instructions for ready artifact │
│ ┌────────────────────────────────────────────────────────────────────┐ │
│ │ $ openspec instructions specs --change "add-auth" --json │ │
│ │ │ │
│ │ { │ │
│ │ "template": "# Specification\n\n## ADDED Requirements...", │ │
│ │ "dependencies": [{"id": "proposal", "path": "...", "done": true}│ │
│ │ "unlocks": ["tasks"] │ │
│ │ } │ │
│ └────────────────────────────────────────────────────────────────────┘ │
│ │
│ Step 3: Read dependencies → Create ONE artifact → Show what's unlocked │
└──────────────────────────────────────────────────────────────────────────┘
```
### Iteration Model
**Legacy workflow** — awkward to iterate:
```
┌─────────┐ ┌─────────┐ ┌─────────┐
│/proposal│ ──► │ /apply │ ──► │/archive │
└─────────┘ └─────────┘ └─────────┘
│ │
│ ├── "Wait, the design is wrong"
│ │
│ ├── Options:
│ │ • Edit files manually (breaks context)
│ │ • Abandon and start over
│ │ • Push through and fix later
│ │
│ └── No official "go back" mechanism
│
└── Creates ALL artifacts at once
```
**OPSX** — natural iteration:
```
/opsx:new ───► /opsx:continue ───► /opsx:apply ───► /opsx:archive
│ │ │
│ │ ├── "The design is wrong"
│ │ │
│ │ ▼
│ │ Just edit design.md
│ │ and continue!
│ │ │
│ │ ▼
│ │ /opsx:apply picks up
│ │ where you left off
│ │
│ └── Creates ONE artifact, shows what's unlocked
│
└── Scaffolds change, waits for direction
```
### Custom Schemas
Create custom workflows using the schema management commands:
```bash
# Create a new schema from scratch (interactive)
openspec schema init my-workflow
# Or fork an existing schema as a starting point
openspec schema fork spec-driven my-workflow
# Validate your schema structure
openspec schema validate my-workflow
# See where a schema resolves from (useful for debugging)
openspec schema which my-workflow
```
Schemas are stored in `openspec/schemas/` (project-local, version controlled) or `~/.local/share/openspec/schemas/` (user global).
**Schema structure:**
```
openspec/schemas/research-first/
├── schema.yaml
└── templates/
├── research.md
├── proposal.md
└── tasks.md
```
**Example schema.yaml:**
```yaml
name: research-first
artifacts:
- id: research # Added before proposal
generates: research.md
requires: []
- id: proposal
generates: proposal.md
requires: [research] # Now depends on research
- id: tasks
generates: tasks.md
requires: [proposal]
```
**Dependency Graph:**
```
research ──► proposal ──► tasks
```
### Summary
| Aspect | Legacy | OPSX |
|--------|----------|------|
| **Templates** | Hardcoded TypeScript | External YAML + Markdown |
| **Dependencies** | None (all at once) | DAG with topological sort |
| **State** | Phase-based mental model | Filesystem existence |
| **Customization** | Edit source, rebuild | Create schema.yaml |
| **Iteration** | Phase-locked | Fluid, edit anything |
| **Editor Support** | Tool-specific configurator/adapters | Single skills directory |
## Schemas
Schemas define what artifacts exist and their dependencies. Currently available:
- **spec-driven** (default): proposal → specs → design → tasks
```bash
# List available schemas
openspec schemas
# See all schemas with their resolution sources
openspec schema which --all
# Create a new schema interactively
openspec schema init my-workflow
# Fork an existing schema for customization
openspec schema fork spec-driven my-workflow
# Validate schema structure before use
openspec schema validate my-workflow
```
## Tips
- Use `/opsx:explore` to think through an idea before committing to a change
- `/opsx:ff` when you know what you want, `/opsx:continue` when exploring
- During `/opsx:apply`, if something's wrong — fix the artifact, then continue
- Tasks track progress via checkboxes in `tasks.md`
- Check status anytime: `openspec status --change "name"`
## Feedback
This is rough. That's intentional — we're learning what works.
Found a bug? Have ideas? Join us on [Discord](https://discord.gg/YctCnvvshC) or open an issue on [GitHub](https://github.com/Fission-AI/openspec/issues).
+91
View File
@@ -0,0 +1,91 @@
# Core Concepts at a Glance
**OpenSpec is a lightweight agreement layer between you and your AI.** You write down what a change should do, the AI drafts the details, you both look at the same plan, and only then does code get written. This page is the whole mental model on one screen. When you want the long version, [Concepts](concepts.md) has it.
Here's the entire idea in five words: **agree first, then build confidently.**
## The five ideas
Everything in OpenSpec is built from five concepts. Learn these and the rest is detail.
**1. Specs are the truth.** A spec describes how your system behaves *right now*. It lives in `openspec/specs/`, organized by domain (`auth/`, `payments/`, `ui/`). Specs are made of requirements ("the system SHALL expire sessions after 30 minutes") and scenarios (concrete given/when/then examples). Think of specs as the single agreed-upon answer to "what does this software do?"
**2. A change is one unit of work.** When you want to add, modify, or remove behavior, you create a change: a folder in `openspec/changes/` holding everything about that work in one place. A proposal, a design, a task list, and the spec edits. One change, one folder, one feature.
**3. Delta specs describe what's changing, not the whole world.** Inside a change, you don't rewrite the entire spec. You write a small delta: `ADDED` this requirement, `MODIFIED` that one, `REMOVED` this other one. This is the trick that makes OpenSpec good at editing existing systems, not just green-field ones. You describe the diff, not the destination.
**4. Artifacts build on each other.** A change contains a few documents, created in a natural order, each feeding the next:
```text
proposal ──► specs ──► design ──► tasks ──► implement
why what how steps do it
```
You can revisit any of them at any time. They're enablers, not gates. (More on that below.)
**5. Archiving folds the change back into the truth.** When the work is done, you archive the change. Its delta specs merge into your main specs, and the change folder moves to `changes/archive/` with a date stamp. Now your specs describe the new reality, and you're ready for the next change. The cycle closes.
## The picture
```text
┌─────────────────────────────────────────────────────────────────┐
│ openspec/ │
│ │
│ ┌──────────────────┐ ┌──────────────────────────┐ │
│ │ specs/ │ │ changes/ │ │
│ │ │ ◄───── │ │ │
│ │ source of truth │ merge │ one folder per change │ │
│ │ how things work │ on │ proposal · design · │ │
│ │ today │ archive │ tasks · delta specs │ │
│ └──────────────────┘ └──────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
```
Two folders. `specs/` is what's true. `changes/` is what you're proposing. Archiving moves a proposal into truth.
## The loop you'll actually run
In the default setup, your day looks like this. Optionally think it through first; then one command drafts the plan, you read it, the next builds it, and the last files it away.
```text
/opsx:explore → (optional) think it through with the AI first
/opsx:propose add-dark-mode → AI drafts proposal, specs, design, tasks
(you read and adjust the plan)
/opsx:apply → AI builds it, checking off tasks
/opsx:archive → specs updated, change archived
```
**When in doubt, start by exploring.** `/opsx:explore` is a no-stakes thinking partner: it reads your code, lays out options, and turns a fuzzy idea into a concrete plan before any code gets written. It's the best antidote to an AI that will otherwise build *something* from a vague prompt. Already know exactly what you want? Skip straight to `/opsx:propose`. Either way, explore ships in the default profile, so it's always there. See the [Explore guide](explore.md).
Those are slash commands, typed in your AI assistant's chat. Setup (`openspec init`) happens in your terminal. If that split is new to you, read [How Commands Work](how-commands-work.md) first; it's the most common point of confusion.
## "Enablers, not gates"
This phrase shows up everywhere in OpenSpec, so here's what it means in plain terms.
Old-school spec processes are waterfalls: finish planning, *then* you're allowed to implement, and going back is painful. OpenSpec refuses that. The order `proposal → specs → design → tasks` shows what becomes *possible* next, not what you're *forced* to do next.
Discover during implementation that the design was wrong? Edit `design.md` and keep going. Realize the scope should shrink? Update the proposal. Nothing locks. The dependencies exist only so the AI has the context it needs (you can't write good tasks without specs to base them on), not to box you in.
The strength here is honesty: real work is messy and iterative, and OpenSpec lets it be. The tradeoff is discipline: because nothing forces you forward, it's on you to keep a change focused rather than letting it sprawl. The [Workflows](workflows.md) guide has good habits for that.
## Why this is worth the small overhead
Plain truth: OpenSpec adds a step. You write a short plan before building. So what do you get for it?
- **You catch wrong turns before they cost you.** Fixing a misunderstanding in a one-paragraph proposal is free. Fixing it after the AI wrote 400 lines is not.
- **The plan and the code stay in the same repo.** Six months later, the spec tells you (and the next AI session) why the system works the way it does.
- **Changes are reviewable.** A change folder is a tidy package: read the proposal, skim the deltas, check the tasks. No archaeology through chat history.
- **It fits existing codebases.** Deltas mean you can specify a change to a 50,000-line app without first documenting the whole thing.
And the honest tradeoff: for a truly trivial one-line fix, the ceremony may not pay off, and that's fine. OpenSpec is designed to be lightweight, but it isn't free. Use it where agreement matters, which turns out to be most of the time once you're working with an AI that will confidently build whatever you vaguely asked for.
## Where to go next
- New here? [Getting Started](getting-started.md) walks the first change in full.
- Not sure what to build yet? [Explore First](explore.md) is the place to start.
- Confused about where commands run? [How Commands Work](how-commands-work.md).
- Want the deep version of everything above? [Concepts](concepts.md).
- Learn by example? [Examples & Recipes](examples.md).
- Need a term defined? [Glossary](glossary.md).
+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.
+462
View File
@@ -0,0 +1,462 @@
# Stores: Plan in Its Own Repo
> **Beta.** Stores, references, working context, and worksets are
> new. Command names, flags, file formats, and JSON output may still change
> shape between releases. Every walkthrough below was run against the
> current build, but re-read this guide after upgrading.
## The problem this solves
OpenSpec normally lives inside one code repo: an `openspec/` folder next to
your code, holding specs and changes for that repo.
That stops fitting the moment your planning is bigger than one repo:
- Your work spans several repos — one feature touches the API server, the
web app, and a shared library. Whose `openspec/` folder does the plan
live in?
- Your team plans before code exists, or plans things that never become
code in *this* repo.
- Requirements are owned by one team and consumed by others. The wiki
version drifts, and your coding agent can't read it anyway.
A **store** is the answer: a standalone repo whose whole job is planning.
It has the same `openspec/` shape you already know — specs and changes —
plus a small identity file. You register it on your machine once, by name,
and then every normal OpenSpec command can work in it from anywhere.
## The shape
```
team-plans (a store: planning in its own repo)
├── .openspec-store/store.yaml identity: "I am team-plans"
└── openspec/
├── specs/ what is true
└── changes/ what is in motion
▲
│ registered on each machine by name;
│ shared by pushing/cloning like any repo
┌─────────────┼─────────────┐
│ │ │
web-app api-server mobile-app
(code repo) (code repo) (code repo)
```
Two rules keep this simple:
1. **A store is just a git repo.** You commit, push, pull, and review it
yourself. OpenSpec never clones, syncs, or pushes anything on its own.
2. **Declarations, not machinery.** Repos can *declare* how they relate to
stores (shown below). Declarations change what OpenSpec can tell you —
never where your commands act.
## Five minutes to your first store
Two commands take you from nothing to a working, store-scoped change:
```bash
openspec store setup team-plans --path ~/openspec/team-plans
```
```
Store ready: team-plans
Location: /Users/you/openspec/team-plans
OpenSpec root: ready
Registry: registered
Next: run normal OpenSpec commands against this store, for example:
openspec new change <change-id> --store team-plans
Share this store by committing and pushing it like any Git repo.
```
```bash
openspec new change add-login --store team-plans
```
```
Using OpenSpec root: team-plans (/Users/you/openspec/team-plans)
Created change 'add-login' at /Users/you/openspec/team-plans/openspec/changes/add-login/
Schema: spec-driven
Next: openspec status --change add-login --store team-plans
```
That's the whole model. From here the lifecycle is exactly what you know —
`status`, `instructions`, `validate`, `archive` — with `--store team-plans`
on each command, and every printed hint carries the flag for you. The
`Using OpenSpec root:` line always tells you where a command is acting.
## Story: one team, one planning repo
A team keeps its specs and changes in `team-plans` instead of scattering
them across code repos.
**Day one (whoever sets it up):**
```bash
openspec store setup team-plans --path ~/openspec/team-plans \
--remote git@github.com:acme/team-plans.git
git -C ~/openspec/team-plans push -u origin main
```
Passing `--remote` records the clone URL inside the store's own identity
file (`.openspec-store/store.yaml`), in the initial commit. Every future
clone is born knowing where it came from, so health checks and error
messages can print a complete, pasteable fix for teammates who don't have
it yet.
**Every teammate (once per machine):**
```bash
git clone git@github.com:acme/team-plans.git ~/openspec/team-plans
openspec store register ~/openspec/team-plans
```
From then on, everyone works in the same planning repo by name:
```bash
openspec status --store team-plans --change add-login
openspec show add-login --store team-plans
```
**Sharing work is git, on purpose.** A change you create exists only in
your checkout until you commit and push it — same as code. Plans get
branches, pull requests, and review for free, because a store is an
ordinary repo.
**Connecting the team's code repos.** A code repo whose planning is fully
externalized needs exactly one line, in `openspec/config.yaml`:
```yaml
# web-app/openspec/config.yaml
store: team-plans
```
Now every OpenSpec command run inside `web-app` acts on `team-plans` with
no flags at all:
```bash
cd ~/src/web-app
openspec status --change add-login
```
```
Using OpenSpec root: team-plans (/Users/you/openspec/team-plans)
...
```
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.
## Example: one feature, two component repos
Suppose `add-checkout-promo` changes both `checkout-api` and
`checkout-web`. The team wants one shared product contract, while each code
repo still needs its own implementation tasks, branch, and review.
Use two layers:
1. Keep the shared behavior in `team-plans`.
2. Keep implementation plans in each component repo and reference the store
as read-only upstream context.
First, plan the shared contract in the store:
```bash
openspec new change add-checkout-promo --store team-plans
openspec status --change add-checkout-promo --store team-plans
```
The proposal and specs should describe the behavior at the boundary between
the components — for example, the promotion fields returned by the service
and how the frontend handles an ineligible checkout. Review this change in
the store repo like any other branch and pull request.
### What context does planning see?
Selecting a store changes the OpenSpec root; it does not discover or read
every code repo that uses that store. Store instructions see the artifacts
and configured context in the store. They see component code only when those
folders are also available to the agent or editor and the agent reads them.
A workset is a convenient way to open the planning store and both code repos
together:
```bash
openspec workset create checkout-promo \
--member ~/openspec/team-plans \
--member ~/src/checkout-api \
--member ~/src/checkout-web \
--tool code
openspec workset open checkout-promo
```
This makes the folders visible in one IDE workspace. It does not copy source
context into the store, select affected repos, or grant an agent permission
to edit them. Put durable cross-component facts in the shared specs; do not
rely on a planner remembering source it happened to inspect.
### How does implementation start in each repo?
When no explicit `--store` or nearer `openspec/` root applies, a
`store: team-plans` pointer routes commands to that store. It does not split
one store task list by the directory from which `apply` was invoked. OpenSpec
currently does not route tasks to repos.
When each component needs an independently scoped apply/review cycle, give it
a local OpenSpec root and reference the central store instead of pointing at
it:
```yaml
# checkout-api/openspec/config.yaml (and likewise in checkout-web)
schema: spec-driven
references:
- team-plans
```
After the shared contract is approved and available in the store's main
specs, create a small local change for the component's part:
```bash
cd ~/src/checkout-api
openspec new change implement-checkout-promo-api
cd ~/src/checkout-web
openspec new change implement-checkout-promo-ui
```
The reference index in each repo's instructions supplies the store spec's
summary and exact `openspec show ... --store team-plans` fetch command. Each
local proposal cites that shared contract, and its tasks describe only work
in that component. Then run `/opsx:apply` in each repo separately; root
resolution keeps the artifacts and implementation edits scoped to that repo.
The service and frontend changes can now be tested, reviewed, merged, and
archived independently.
If implementation must begin while the shared store change is still active,
fetch it explicitly with
`openspec show add-checkout-promo --store team-plans`; reference indexes list
canonical store specs, not active store changes. Keep the store branch and
component branches linked in their pull-request descriptions so reviewers
can see which version of the contract each implementation follows.
## Story: requirements that cross team lines
A platform team owns the requirements. Product teams build against them,
in their own repos, with their own designs. A reference describes that
relationship without moving anyone's work.
```
platform-reqs (store) api-server (code repo)
owned by the platform team owned by a product team
┌──────────────────────────┐ ┌──────────────────────────┐
│ openspec/specs/ │ ◀────────│ openspec/config.yaml │
│ payments/spec.md │ reads │ references: │
│ auth/spec.md │ │ - platform-reqs │
│ │ │ openspec/specs/ │
│ openspec/changes/ │ │ (their own designs) │
│ platform work │ │ openspec/changes/ │
│ │ │ (their own work) │
│ │ └──────────────────────────┘
└──────────────────────────┘
```
**The product team declares what it draws on** in its repo's
`openspec/config.yaml`:
```yaml
references:
- platform-reqs
```
References are read-only context. The repo keeps its own `openspec/` root;
work stays there. What changes: `openspec instructions` in that repo now
includes an index of the referenced store's specs — each with a one-line
summary and the exact fetch command (`openspec show <spec-id> --type spec
--store platform-reqs`). An agent working in `api-server` can find the
upstream payment requirements, cite them, and write its low-level design in
the repo's own root — without anyone pasting context around.
A reference can carry its clone source, so teammates who don't have the
store yet get a complete fix instead of a dead end:
```yaml
references:
- { id: platform-reqs, remote: "git@github.com:acme/platform-reqs.git" }
```
**When you want the plan and code open together, make a workset.** This is
personal and explicit: each person chooses the folders they actually work
with on their machine. Nothing about those local checkout paths is
committed to the shared planning repo.
```bash
openspec workset create platform \
--member ~/openspec/platform-reqs \
--member ~/src/api-server \
--member ~/src/web-app
```
## Two questions you can always ask
**"Is my setup healthy?"** — `openspec doctor` checks the current root and
its referenced stores, read-only, with a pasteable fix per finding:
```
Doctor
Root
Location: /Users/you/src/api-server
OpenSpec root: ok
References
- platform-reqs: ok (/Users/you/openspec/platform-reqs)
- design-system: Referenced store 'design-system' is not registered on this machine.
Fix: git clone -- git@github.com:acme/design-system.git '/Users/you/openspec/design-system' && openspec store register '/Users/you/openspec/design-system' --id design-system
```
**"What am I working with?"** — `openspec context` assembles the working
set from OpenSpec declarations: the root and the stores it references.
```
Working context for api-server (/Users/you/src/api-server)
OpenSpec root
api-server /Users/you/src/api-server
Referenced stores
platform-reqs /Users/you/openspec/platform-reqs
Fetch: openspec show <spec-id> --type spec --store platform-reqs
```
Both support `--json` for agents. `openspec context --code-workspace
<path>` additionally writes a VS Code workspace file containing the whole
set — the only write this command performs.
## Worksets: reopen the folders you work on together
Separate from all of the above: most people open the same few folders
together every session — the planning repo plus two or three code repos.
A **workset** is a personal, named view of exactly that, reopened with one
command in your tool of choice.
```
workset "platform" openspec workset open platform
├── team-plans ~/openspec/team-plans │
├── api-server ~/src/api-server ▼
└── web-app ~/src/web-app all three open in your tool
```
```bash
openspec workset create platform \
--member ~/openspec/team-plans --member ~/src/api-server \
--tool code
openspec workset list
```
```
platform (opens in VS Code)
team-plans /Users/you/openspec/team-plans
api-server /Users/you/src/api-server
```
`openspec workset open platform` then launches the saved tool: editors
(VS Code, Cursor) open one window with every member and return. The first
member is the primary. Override the tool any time with `--tool <id>`.
Worksets are deliberately *not* shared state. They live on your machine,
are never committed, and make no claims about the work — they only record
what you like open together. Removing one never touches the member
folders. New tools are configuration, not code: anything launched via a
workspace file or per-folder attach flags can be added under the `openers`
key in the global config (`openspec config edit`).
## How commands decide where to act
Every normal command resolves its root the same way, in this order:
```
1. --store <id> you said so explicitly → that store
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. 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
(classic behavior)
```
The `Using OpenSpec root:` line (and the `root` block in `--json` output)
tells you which case you're in.
## Known limitations
- **Beta shape.** Everything on this page may change between releases —
names, flags, file formats, JSON keys.
- **One checkout per store id per machine.** Registering a second checkout
under the same id fails with a hint to `store unregister` first.
- **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.** `templates` and the
deprecated noun forms (`openspec change show`, ...) act on the current
directory only — no `--store`. `schemas` follows the canonical root-selection
precedence and accepts `--store <id>` while keeping its successful JSON array
shape unchanged.
- **Per-machine state is per-machine.** The store registry and worksets
are local settings. Nothing about your machine's layout is
ever committed to shared planning.
- **Two launch styles for worksets.** A tool that can't be launched with a
workspace file or per-folder attach flags can't be added as an opener.
- **Agent JSON has a known casing split** (store-family keys are
snake_case, workflow-family camelCase). Documented in the
[agent contract](../agent-contract.md); unifying it is deferred to a
versioned release.
## Where things live
| What | Where | Shared? |
|---|---|---|
| A store's planning | `<store>/openspec/` (specs, changes) | Yes — commit and push it |
| A store's identity | `<store>/.openspec-store/store.yaml` | Yes — committed with the store |
| The store registry | `<data dir>/openspec/stores/registry.yaml` | No — this machine only |
| Worksets | `<data dir>/openspec/worksets/` | No — this machine only |
`<data dir>` is `~/.local/share/openspec` on macOS and Linux (or
`$XDG_DATA_HOME/openspec` when set), and `%LOCALAPPDATA%\openspec` on
Windows.
## Reference
Exact flags and JSON shapes for every command on this page:
[CLI reference](../cli.md) (Stores, Doctor, Working context, Personal
worksets) and the [agent contract](../agent-contract.md).
+257
View File
@@ -0,0 +1,257 @@
# Supported Tools
OpenSpec works with many AI coding assistants. When you run `openspec init`, OpenSpec configures selected tools using your active profile/workflow selection and delivery mode.
## How It Works
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 `.agents/skills/openspec-*/SKILL.md` for Codex even when delivery is set to `commands`, and it does not generate Codex custom prompt files. Existing OpenSpec-managed skills under the legacy `.codex/skills` path are reconciled after their replacements are written; custom and divergent files are preserved.
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, MiniMax Code, Mistral Vibe, Zed Agent, shared `.agents` |
| 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 |
|-----------|---------------------|----------------------|
| Amazon Q Developer (`amazon-q`) | `.amazonq/skills/openspec-*/SKILL.md` | `.amazonq/prompts/opsx-<id>.md` |
| Antigravity (`antigravity`) | `.agent/skills/openspec-*/SKILL.md` | `.agent/workflows/opsx-<id>.md` |
| Auggie (`auggie`) | `.augment/skills/openspec-*/SKILL.md` | `.augment/commands/opsx-<id>.md` |
| IBM Bob Shell (`bob`) | `.bob/skills/openspec-*/SKILL.md` | `.bob/commands/opsx-<id>.md` |
| Claude Code (`claude`) | `.claude/skills/openspec-*/SKILL.md` | `.claude/commands/opsx/<id>.md` |
| Cline (`cline`) | `.cline/skills/openspec-*/SKILL.md` | `.clinerules/workflows/opsx-<id>.md` |
| Command Code (`command-code`) | `.commandcode/skills/openspec-*/SKILL.md` | `.commandcode/commands/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`) | `.agents/skills/openspec-*/SKILL.md` | Not generated (skills-only; use `$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` |
| Crush (`crush`) | `.crush/skills/openspec-*/SKILL.md` | `.crush/commands/opsx/<id>.md` |
| Cursor (`cursor`) | `.cursor/skills/openspec-*/SKILL.md` | `.cursor/commands/opsx-<id>.md` |
| Factory Droid (`factory`) | `.factory/skills/openspec-*/SKILL.md` | `.factory/commands/opsx-<id>.md` |
| Gemini CLI (`gemini`) | `.gemini/skills/openspec-*/SKILL.md` | `.gemini/commands/opsx/<id>.toml` |
| GitHub Copilot (`github-copilot`) | `.github/skills/openspec-*/SKILL.md` | `.github/prompts/opsx-<id>.prompt.md`\*\* |
| 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` | `.kilo/command/opsx-<id>.md` |
| Kimi Code (`kimi`) | `.kimi-code/skills/openspec-*/SKILL.md` | Not generated (no command adapter; use skill-based `/skill:openspec-*` invocations) |
| Kiro (`kiro`) | `.kiro/skills/openspec-*/SKILL.md` | `.kiro/prompts/opsx-<id>.prompt.md` |
| Lingma (`lingma`) | `.lingma/skills/openspec-*/SKILL.md` | `.lingma/commands/opsx/<id>.md` |
| MiniMax Code (`minimax-code`) | `~/.minimax/skills/openspec-*/SKILL.md` | Not generated (no command adapter; use MiniMax Code skills) |
| 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` |
| SourceCraft Code Assistant for VS Code (`codeassistant`) | `.codeassistant/skills/openspec-*/SKILL.md` | `.codeassistant/commands/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>.md` |
| [Rovo Dev CLI](https://support.atlassian.com/rovo/docs/use-rovo-dev-cli/) (`rovodev`) | `.rovodev/skills/openspec-*/SKILL.md` | Not generated. Rovo has no slash-command surface — it matches skills automatically or by prompt (e.g. "use the openspec-propose skill"); `/skills` only manages them. Generated content references skills by name, never as `/openspec-*` commands. |
| [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` |
| [Zed Agent](https://zed.dev/docs/ai/skills) (`zed`) | `.agents/skills/openspec-*/SKILL.md` | Not generated (skills-only; use `/openspec-*` or `@openspec-*`) |
| ZCode (`zcode`) | `.zcode/skills/openspec-*/SKILL.md` | `.zcode/commands/opsx/<id>.md` |
| Shared `.agents` skills (`agents`) | `.agents/skills/openspec-*/SKILL.md` | Not generated (no command adapter; use skill-based `/openspec-*` invocations) |
\*\* 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. Selecting `github-copilot` can also set up the GitHub-hosted **cloud coding agent** — see [GitHub Copilot cloud coding agent](#github-copilot-cloud-coding-agent) below.
\*\*\* 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-*`.
SourceCraft Code Assistant support targets its VS Code extension. Its [custom commands](https://sourcecraft.dev/portal/docs/en/code-assistant/operations/agent/slash-commands) and [skills](https://sourcecraft.dev/portal/docs/ru/code-assistant/operations/agent/skills) are available only in VS Code. This integration does not configure SourceCraft web or JetBrains.
With skills-only delivery, ask Code Assistant to use the `openspec-propose` skill with your idea. Skills activate through request matching; OpenSpec does not generate `/openspec-*` commands for this tool.
MiniMax Code is a global skills-only integration. OpenSpec writes only its
`openspec-*` directories under `~/.minimax/skills/`; it does not create
repo-local `.minimax` or `.mavis` directories. Commands-only delivery leaves
existing global MiniMax Code skills untouched so one project's delivery setting
cannot remove skills used by another project.
### GitHub Copilot cloud coding agent
GitHub's [Copilot coding agent](https://docs.github.com/en/copilot/using-github-copilot/coding-agent) runs on GitHub in a GitHub Actions environment — separate from Copilot in your editor. OpenSpec can set it up to use the OpenSpec CLI by generating two files:
- `.github/workflows/copilot-setup-steps.yml` — installs `@fission-ai/openspec` in the agent's environment
- `.github/agents/openspec.agent.md` — tells the agent how to drive OpenSpec
Because this writes a GitHub Actions workflow into your repository, it is **opt-in**:
| How | Behavior |
|-----|----------|
| `openspec init` (interactive) | Asks whether to set up cloud files. Default is **No**. |
| `openspec init --copilot-cloud` | Sets them up without prompting (for scripts/CI). |
| `openspec init --no-copilot-cloud` | Skips them without prompting, and removes any previously generated ones. |
| `openspec update` | Never prompts. Refreshes the files only if you opted in (or the project already has them). If you opted out, it removes OpenSpec-managed cloud files. |
Your choice is saved in `openspec/config.yaml` as `githubCopilot.cloudAgent: true|false`, so non-interactive updates honor it. OpenSpec only ever writes or removes files whose content it generated — if you customize `copilot-setup-steps.yml` or `openspec.agent.md`, or already have your own, it is left untouched (and `init`/`update` tell you so).
### When to pick the shared `.agents` target
`agents` is the vendor-neutral option: it writes skills to `.agents/skills/`, the
shared root many agent tools read, instead of a tool-specific directory.
| Situation | Pick |
|-----------|------|
| Your tool has its own row above | Its own ID — you get that tool's integration, including slash commands where it supports them |
| Several agents on one repo, all reading `.agents/skills` | `agents` — one skill tree instead of one per tool |
| Your tool isn't listed yet but reads `.agents/skills` | `agents` |
Selecting it alongside a tool-specific ID is fine; each normally writes to its
own root. Codex and Zed Agent are the exceptions because they use the same canonical
`.agents` root. If Codex is selected with Zed or `agents`, OpenSpec keeps one
Codex-led tree. Its handoffs name both `$openspec-*` for Codex and
`/openspec-*` for other agents, so `--tools all` and existing multi-agent
setups keep working without two writers overwriting the same files.
OpenSpec also offers it automatically once a project has a `.agents/skills/`
directory — a bare `.agents/` is not enough, since tools use that root for rules
and subagent definitions too. Note `.agents` is not `.agent`: the singular
directory belongs to Antigravity.
Two things to know:
- **Skills only.** No command adapter exists, so no `opsx-*` command files are
written; with a commands-inclusive delivery mode `openspec init` lists `agents`
among the tools it reports under `Commands skipped for: … (no adapter)`.
Invoke the workflows by skill name —
most assistants that read `.agents/skills` spell that `/openspec-propose`, the form
OpenSpec's setup hint prints. The target is vendor-neutral, so check your
assistant's own docs if it uses another form.
- **No `AGENTS.md` is created or edited.** The target is the `.agents/` directory.
If your root `AGENTS.md` still carries OpenSpec marker blocks from an older
version, `openspec update` strips them — see the [Migration Guide](migration-guide.md).
Zed support here is for the built-in Zed Agent. Zed External Agents and Terminal
Threads use their own integrations. Agent Skills require
[Zed v1.4.2](https://github.com/zed-industries/zed/releases/tag/v1.4.2) or newer.
Project-local skills are unavailable in an untrusted worktree until you
[grant trust](https://zed.dev/docs/worktree-trust).
Because `.agents/skills/` is shared by Codex, Zed Agent, and the vendor-neutral target,
it is worth knowing what OpenSpec claims there:
it writes, refreshes, and removes only the `openspec-*` skill directories for your
selected workflows, plus an `.openspec-target` marker that records whether Codex,
Zed Agent, or the vendor-neutral target rendered that shared tree. Anything else in that
directory is left alone. Treat the `openspec-*` names and marker as OpenSpec's —
edits inside them are replaced on the next `openspec update`, the same as for
every other tool.
For pre-marker projects, OpenSpec infers ownership from managed skill references:
`$openspec-*` means Codex and `/openspec-*` means the vendor-neutral target. A
generic canonical tree alongside legacy `.codex/skills` is treated as an older
dual-target install and consolidated into the compatible shared tree.
`openspec update` honors this ownership too. If a project owns `.agents` as the
vendor-neutral target and a leftover Codex install is detected only from stray
prompt files, the update leaves the established `agents` tree in place instead of
rewriting it with Codex syntax, and preserves those legacy prompt files rather
than deleting them. To hand the shared tree to Codex, run `openspec init --tools
codex` explicitly.
## Non-Interactive Setup
For CI/CD or scripted setup, use `--tools` (and optionally `--profile`):
```bash
# Configure specific tools
openspec init --tools claude,cursor
# Configure all supported tools
openspec init --tools all
# Skip tool configuration
openspec init --tools none
# Override profile for this init run
openspec init --profile core
```
**Available tool IDs (`--tools`)** — `windsurf` is also accepted, as an alias for `devin`: `amazon-q`, `antigravity`, `auggie`, `bob`, `claude`, `cline`, `command-code`, `codeartsagent`, `codex`, `devin`, `forgecode`, `codebuddy`, `continue`, `costrict`, `crush`, `cursor`, `factory`, `gemini`, `github-copilot`, `hermes`, `iflow`, `junie`, `kilocode`, `kimi`, `kiro`, `lingma`, `minimax-code`, `vibe`, `oh-my-pi`, `opencode`, `pi`, `qoder`, `qwen`, `roocode`, `codeassistant`, `trae`, `zed`, `zcode`, `agents`
## Workflow-Dependent Installation
OpenSpec installs workflow artifacts based on selected workflows:
- **Core profile (default):** `propose`, `explore`, `apply`, `update`, `sync`, `archive`
- **Custom selection:** any subset of all workflow IDs:
`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.
## Generated Skill Names
When selected by profile/workflow config, OpenSpec generates these skills:
- `openspec-propose`
- `openspec-explore`
- `openspec-new-change`
- `openspec-continue-change`
- `openspec-apply-change`
- `openspec-update-change`
- `openspec-ff-change`
- `openspec-sync-specs`
- `openspec-archive-change`
- `openspec-bulk-archive-change`
- `openspec-verify-change`
- `openspec-onboard`
See [Commands](commands.md) for command behavior and [CLI](cli.md) for `init`/`update` options.
## Related
- [CLI Reference](cli.md) — Terminal commands
- [Commands](commands.md) — Slash commands and skills
- [Getting Started](getting-started.md) — First-time setup
+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.
+195
View File
@@ -0,0 +1,195 @@
# Troubleshooting
Concrete fixes for concrete problems. Each entry names a symptom, explains the likely cause in a sentence, and gives you the fix. If you don't see your issue here, the [FAQ](faq.md) may help, and the [Discord](https://discord.gg/YctCnvvshC) definitely will.
## Installation and setup
### `openspec: command not found`
The CLI isn't installed, or your shell can't find it. Install it globally and check:
```bash
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 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"
OpenSpec runs on Node 20.19.0+. Check your version and upgrade if needed:
```bash
node --version
```
If you use bun to install OpenSpec, note that OpenSpec still *runs* on Node, so you need Node 20.19.0+ available on your `PATH` regardless. See [Installation](installation.md).
### `openspec init` didn't configure my AI tool
Init asks which tools to set up. If you skipped your tool or want to add another, just run it again, or use the non-interactive form:
```bash
openspec init --tools claude,cursor
```
The full list of tool IDs is in [Supported Tools](supported-tools.md). Use `--tools all` for everything, `--tools none` to skip tool setup.
## Commands don't show up
If `/opsx:propose` (or your tool's equivalent) doesn't appear or doesn't do anything, work down this list. They're ordered fastest-to-check first.
1. **You may be in the wrong place.** Slash commands go in your AI assistant's chat, not your terminal. If you typed `/opsx:propose` into your shell, that's the issue. See [How Commands Work](how-commands-work.md).
2. **Regenerate the files.** From your project root:
```bash
openspec update
```
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.** Codex, CodeArts, ForgeCode, Hermes, Kimi Code, Mistral Vibe, Zed Agent, and the shared `.agents` target 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. The shared `.agents` target is vendor-neutral, so `/openspec-propose` is the common form rather than a guaranteed one — if your assistant does not answer to it, check its own docs for how it invokes a skill. 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
### "Change not found"
The command couldn't tell which change you meant. Name it explicitly, or check what exists:
```bash
openspec list # see active changes
/opsx:apply add-dark-mode # name the change in chat
```
Also confirm you're in the right project directory.
### "No artifacts ready"
Every artifact is either already created or blocked waiting on a dependency. See what's blocking:
```bash
openspec status --change <name>
```
Then create the missing dependency first. Remember the order: proposal enables specs and design; specs and design together enable tasks.
### `openspec validate` reports warnings or errors
Validation checks your specs and changes for structural problems. Read the message: it names the file and the issue.
```bash
openspec validate <name> # validate one item
openspec validate --all # validate everything
openspec validate --all --strict # stricter checks, good for CI
openspec validate --archived # fail if archived changes have unchecked tasks
```
Common causes are a missing required section (like a spec with no scenarios) or a malformed delta header. Fix the file and re-run. The [CLI reference](cli.md#openspec-validate) documents the output format.
One message deserves its own note:
```text
MODIFIED "<requirement>" omits scenario(s) the current spec still has: "<scenario>"
```
A `MODIFIED` requirement replaces the whole requirement block, so it has to carry every scenario that survives the change, not only the ones you edited. Copy the named scenarios from `openspec/specs/<capability-path>/spec.md` back into the delta, preserving any domain directories in the path. This often appears on an older change after someone else's change added a scenario to the same requirement — archive refuses that change either way, and validation now says so before you implement it.
### The AI created incomplete or wrong artifacts
The AI didn't have enough context. A few levers help:
- Add project context in `openspec/config.yaml` so your stack and conventions are injected into every request. See [Customization](customization.md#project-configuration).
- Add per-artifact `rules:` for guidance that only applies to, say, specs.
- Give a more detailed description when you propose.
- Use the expanded `/opsx:continue` to create one artifact at a time and review each, instead of `/opsx:ff` doing them all at once.
### Archive won't finish, or warns about incomplete tasks
Archive won't *block* on incomplete tasks, but it warns you, because archiving usually means the work is done. If tasks remain on purpose (you're filing a partial change), proceed. Otherwise finish the tasks first. Archive will also offer to sync your delta specs into the main specs if you haven't synced yet; say yes unless you have a reason not to.
### "User force closed the prompt with 0 null"
Something ran `openspec archive` where nothing can answer a question — an AI agent calling it from a tool, a CI job, or any shell with stdin closed. Archive asks up to three confirmations, and an unanswerable one used to fail with that raw message.
Pass `--yes` to answer them up front:
```bash
openspec archive <change-name> --yes
```
Keep any flags you were already passing — `--skip-specs` and `--no-validate` change what archive does, so a bare `--yes` rerun is not the same command. Current versions name the flag for you and print a `Fix:` line you can paste. If you meant to pick from a list, pass the change name explicitly: the picker needs an answer too.
If you instead ran archive with its output redirected to a file or captured by a tool and *did* pipe an answer (`printf 'y\n' | openspec archive …`), older versions wrote terminal escape codes into that capture while drawing the prompt — in some environments enough to bloat the file badly. Current versions read the confirmation prompts as plain text whenever stdout is not a terminal, and a no-argument `openspec archive` (which would otherwise draw an interactive change picker) asks you to pass a change name up front instead of rendering a menu into the capture. Either way, redirected and agent runs stay clean; passing `--yes` (with a change name) skips the prompts entirely.
## Configuration
### My `config.yaml` isn't being applied
Three usual suspects:
1. **Wrong filename.** It must be `openspec/config.yaml`, not `.yml`.
2. **Invalid YAML.** Run it through any YAML validator; the CLI also reports syntax errors with line numbers.
3. **You expected a restart.** You don't need one. Config changes take effect immediately.
### "Unknown artifact ID in rules: X"
A key under `rules:` doesn't match any artifact in your schema. For the default `spec-driven` schema the valid IDs are `proposal`, `specs`, `design`, `tasks`. To see the IDs for any schema:
```bash
openspec schemas --json
```
### "Context too large"
The `context:` field is capped at 50KB, on purpose, because it's injected into every request. Summarize it, or link out to longer docs instead of pasting them. Lean context also produces better, faster results.
### "Schema not found"
The schema name you referenced doesn't exist. List what's available and check spelling:
```bash
openspec schemas # list available schemas
openspec schema which <name> # see where a schema resolves from
openspec schema init <name> # create a custom one
```
See [Customization](customization.md#custom-schemas).
## Migration from the legacy workflow
### "Legacy files detected in non-interactive mode"
You're in CI or a non-interactive shell, and OpenSpec found old files to clean up but can't prompt you. Approve automatically:
```bash
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 `.agents/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).
### My old `project.md` wasn't migrated
That's intentional. OpenSpec never deletes `project.md` automatically because it may hold context you wrote. Move the useful parts into `config.yaml`'s `context:` section, then delete it yourself. The [Migration Guide](migration-guide.md#migrating-projectmd-to-configyaml) walks through this, including a prompt you can hand to your AI to do the distilling.
## Still stuck?
- **Discord:** [discord.gg/YctCnvvshC](https://discord.gg/YctCnvvshC)
- **GitHub Issues:** [github.com/Fission-AI/OpenSpec/issues](https://github.com/Fission-AI/OpenSpec/issues)
- **From your terminal:** `openspec feedback "what went wrong"` opens an issue for you.
When you report a problem, include your OpenSpec version (`openspec --version`), your Node version (`node --version`), your AI tool, and the exact command and output. It makes help much faster.
+552
View File
@@ -0,0 +1,552 @@
# Workflows
This guide covers common workflow patterns for OpenSpec and when to use each one. For basic setup, see [Getting Started](getting-started.md). For command reference, see [Commands](commands.md).
## Philosophy: Actions, Not Phases
Traditional workflows force you through phases: planning, then implementation, then done. But real work doesn't fit neatly into boxes.
OPSX takes a different approach:
```text
Traditional (phase-locked):
PLANNING ────────► IMPLEMENTING ────────► DONE
│ │
│ "Can't go back" │
└────────────────────┘
OPSX (fluid actions):
proposal ──► specs ──► design ──► tasks ──► implement
```
**Key principles:**
- **Actions, not phases** - Commands are things you can do, not stages you're stuck in
- **Dependencies are enablers** - They show what's possible, not what's required next
> **Customization:** OPSX workflows are driven by schemas that define artifact sequences. See [Customization](customization.md) for details on creating custom schemas.
## Workflow at a Glance
The default workflow stays fluid: exploration and verification are optional, and
you can update planning artifacts whenever implementation reveals something new.
```mermaid
flowchart TD
Idea["Idea or problem"] --> Explore["/opsx:explore<br/>(optional)"]
Idea --> Propose["/opsx:propose"]
Explore --> Propose
Propose --> Review{"Planning artifacts<br/>ready?"}
Review -->|"Refine"| Update["/opsx:update"]
Update --> Review
Review -->|"Implement"| Apply["/opsx:apply"]
Apply -->|"Plan changed"| Update
Apply --> Archive["/opsx:archive"]
Apply --> Verify["/opsx:verify<br/>(optional, custom selection)"]
Apply --> Sync["/opsx:sync<br/>(optional before archive)"]
Verify --> Verified{"Ready to archive?"}
Verified -->|"Fix implementation"| Apply
Verified -->|"Revise plan"| Update
Verified -->|"Ready"| Sync
Verified -->|"Ready"| Archive
Sync --> Archive
```
The AI assistant drives the workflow, while the CLI provides deterministic
scaffolding, status, and artifact instructions:
```mermaid
sequenceDiagram
actor Human
participant Assistant as AI assistant
participant CLI as OpenSpec CLI
participant Files as Planning and implementation files
Human->>Assistant: /opsx:propose "change"
Assistant->>CLI: openspec new change
CLI->>Files: Scaffold change metadata
Assistant->>CLI: Request status and artifact instructions
CLI-->>Assistant: Build order, paths, and templates
Assistant->>Files: Write schema-defined planning artifacts
Assistant-->>Human: Present artifacts for review
Human->>Assistant: /opsx:apply
Assistant->>CLI: Request apply instructions
CLI-->>Assistant: Context files and task state
Assistant->>Files: Implement tasks and update checkboxes
Assistant-->>Human: Report implementation status
Human->>Assistant: /opsx:archive
Assistant->>CLI: Request archive inputs and artifact status
CLI-->>Assistant: Planning paths and artifact completion
Assistant->>Files: Read task state and compare delta specs
opt Delta specs exist
Assistant-->>Human: Offer to sync before archiving
alt Sync accepted
Human->>Assistant: Confirm sync
Assistant->>Files: Merge delta specs into main specs
else Sync skipped
Human->>Assistant: Archive without syncing
end
end
Assistant->>Files: Move the change into the archive
Assistant-->>Human: Report archive location and sync result
Note over Human,CLI: CLI alternative: openspec archive change-name --yes skips confirmation prompts. It still validates, then applies any delta specs and archives
```
## Two Modes
### Default Quick Path (`core` profile)
New installs default to `core`, which provides:
- `/opsx:explore`
- `/opsx:propose`
- `/opsx:apply`
- `/opsx:update`
- `/opsx:sync`
- `/opsx:archive`
Typical flow:
```text
/opsx:explore ──► /opsx:propose ──► /opsx:apply ──► /opsx:sync ──► /opsx:archive
(optional)
```
#### Start by exploring (the habit worth forming)
`/opsx:explore` is part of the default profile, not an advanced add-on. It's the move to make whenever you have a problem but not yet a plan, which, with an AI assistant, is most of the time.
```text
You: /opsx:explore
AI: What would you like to explore?
You: Our search results feel stale. I'm not sure if it's caching,
indexing, or the query itself.
AI: Let me look... [reads the search service and cache layer]
It's the index: it only rebuilds nightly, so edits don't show
up until the next day. Two options: rebuild on write (simple,
more load) or a short-TTL cache invalidation (more moving parts,
less load). Given your write volume is low, rebuild-on-write is
the cleaner fix. Want to scope it?
You: Yes.
You: /opsx:propose rebuild-search-index-on-write
```
Explore never writes code, and writes nothing else unless you ask, or say yes when it offers. It's a free, no-stakes conversation that turns a vague worry into a precise change, so the proposal that follows is sharp. Already know exactly what you want? Skip it and go straight to `/opsx:propose`. Full guide: [Explore First](explore.md).
### Expanded/Full Workflow (custom selection)
If you want explicit scaffold-and-build commands (`/opsx:new`, `/opsx:continue`, `/opsx:ff`, `/opsx:verify`, `/opsx:bulk-archive`, `/opsx:onboard`), enable them with:
```bash
openspec config profile
openspec update
```
## Workflow Patterns (Expanded Mode)
### Quick Feature
When you know what you want to build and just need to execute:
```text
/opsx:new ──► /opsx:ff ──► /opsx:apply ──► /opsx:verify ──► /opsx:archive
```
**Example conversation:**
```text
You: /opsx:new add-logout-button
AI: Created openspec/changes/add-logout-button/
Ready to create: proposal
You: /opsx:ff
AI: Creating all planning artifacts...
✓ proposal.md
✓ specs/
✓ design.md
✓ tasks.md
Ready for implementation!
You: /opsx:apply
AI: Working through tasks...
✓ Added logout button to header
✓ Implemented session clearing
✓ Added confirmation dialog
All tasks complete!
You: /opsx:archive
AI: ✓ Merged specs
✓ Archived change
```
**Best for:** Small to medium features, bug fixes, straightforward changes.
### Exploratory
When requirements are unclear or you need to investigate first:
```text
/opsx:explore ──► /opsx:new ──► /opsx:continue ──► ... ──► /opsx:apply
```
**Example conversation:**
```text
You: /opsx:explore
AI: What would you like to explore?
You: I want to improve page load performance but I'm not sure
where the bottlenecks are.
AI: Let me help investigate...
[Analyzes bundle sizes, identifies slow queries, reviews
component render patterns]
I found three main bottlenecks:
1. Large unoptimized images
2. Synchronous data fetching in ProductList
3. Re-renders from context changes
Which would you like to address first?
You: Let's tackle the data fetching issue.
You: /opsx:new optimize-product-list-fetching
AI: Created change. Ready to create: proposal
You: /opsx:continue
AI: Creating proposal based on our exploration...
[Writes proposal incorporating investigation insights]
Ready to create: specs, design
Which would you like next?
You: /opsx:continue
AI: Creating specs...
```
**Best for:** Performance optimization, debugging, architectural decisions, unclear requirements.
### Parallel Changes
Work on multiple changes at once:
```text
Change A: /opsx:new ──► /opsx:ff ──► /opsx:apply (in progress)
│
context switch
│
Change B: /opsx:new ──► /opsx:ff ──────► /opsx:apply
```
**Example conversation:**
```text
You: I'm working on add-dark-mode but got pulled into a bug fix
You: /opsx:new fix-login-redirect
AI: Created openspec/changes/fix-login-redirect/
You: /opsx:ff
AI: ✓ Created planning artifacts
You: /opsx:apply
AI: Working on fix-login-redirect...
[Implements the fix]
All tasks complete!
You: /opsx:archive
AI: ✓ Archived fix-login-redirect
You: Let's get back to dark mode
You: /opsx:apply add-dark-mode
AI: Resuming add-dark-mode...
Picking up at task 2.3: Update Header...
```
**Best for:** Parallel work streams, urgent interrupts, team collaboration.
When you have multiple completed changes, use `/opsx:bulk-archive`:
```text
You: /opsx:bulk-archive
AI: Found 3 completed changes:
- add-dark-mode (all tasks done)
- fix-login-redirect (all tasks done)
- update-footer (all tasks done)
Checking for spec conflicts...
⚠ add-dark-mode and update-footer both touch specs/ui/
Inspecting codebase to resolve...
Both changes are implemented. Will apply in chronological order.
Archive all 3 changes?
You: Yes
AI: ✓ Archived add-dark-mode
✓ Archived fix-login-redirect
✓ Archived update-footer
Specs merged in order: add-dark-mode → update-footer
```
Bulk archive detects when multiple changes touch the same specs and resolves conflicts by checking what's actually implemented.
### Completing a Change
The recommended completion flow:
```text
/opsx:apply ──► /opsx:verify ──► /opsx:archive
│ │
validates prompts to sync
implementation if needed
```
#### Verify: Check Your Work
`/opsx:verify` validates implementation against your artifacts across three dimensions:
```text
You: /opsx:verify
AI: Verifying add-auth...
COMPLETENESS
✓ All 12 tasks in tasks.md are checked
✓ All requirements in specs have corresponding code
⚠ Scenario "Session timeout after inactivity" not tested
CORRECTNESS
✓ Implementation matches spec intent
✓ Edge cases from scenarios are handled
✓ Error states match spec definitions
COHERENCE
✓ Design decisions reflected in code structure
✓ Naming conventions consistent with design.md
⚠ Design mentions "event-driven" but implementation uses polling
SUMMARY
─────────────────────────────
Critical issues: 0
Warnings: 2
Ready to archive: Yes (with warnings)
Recommendations:
1. Add test for session timeout scenario
2. Consider refactoring to event-driven as designed, or update design.md
```
**What verify checks:**
| Dimension | What it validates |
|-----------|------------------|
| Completeness | All tasks done, all requirements implemented, scenarios covered |
| Correctness | Implementation matches spec intent, edge cases handled |
| Coherence | Design decisions reflected in code, patterns consistent |
Verify won't block archive, but it surfaces issues you might want to address first.
#### Archive: Finalize the Change
`/opsx:archive` completes the change and moves it to the archive:
```text
You: /opsx:archive
AI: Archiving add-auth...
Artifact status:
✓ proposal.md exists
✓ specs/ exists
✓ design.md exists
✓ tasks.md exists (12/12 tasks complete)
Delta specs: Not yet synced to main
→ Would you like to sync now?
You: Yes
AI: ✓ Synced specs to openspec/specs/auth/spec.md
✓ Moved to openspec/changes/archive/2025-01-24-add-auth/
Change archived successfully.
```
Archive will prompt if specs aren't synced. It won't block on incomplete tasks, but it will warn you.
## When to Use What
### `/opsx:ff` vs `/opsx:continue`
| Situation | Use |
|-----------|-----|
| Clear requirements, ready to build | `/opsx:ff` |
| Exploring, want to review each step | `/opsx:continue` |
| Want to iterate on proposal before specs | `/opsx:continue` |
| Time pressure, need to move fast | `/opsx:ff` |
| Complex change, want control | `/opsx:continue` |
**Rule of thumb:** If you can describe the full scope upfront, use `/opsx:ff`. If you're figuring it out as you go, use `/opsx:continue`.
### When to Update vs Start Fresh
A common question: when is updating an existing change okay, and when should you start a new one?
**Update the existing change when:**
- Same intent, refined execution
- Scope narrows (MVP first, rest later)
- Learning-driven corrections (codebase isn't what you expected)
- Design tweaks based on implementation discoveries
**Start a new change when:**
- Intent fundamentally changed
- Scope exploded to different work entirely
- Original change can be marked "done" standalone
- Patches would confuse more than clarify
```text
┌─────────────────────────────────────┐
│ Is this the same work? │
└──────────────┬──────────────────────┘
│
┌──────────────────┼──────────────────┐
│ │ │
▼ ▼ ▼
Same intent? >50% overlap? Can original
Same problem? Same scope? be "done" without
│ │ these changes?
│ │ │
┌────────┴────────┐ ┌──────┴──────┐ ┌───────┴───────┐
│ │ │ │ │ │
YES NO YES NO NO YES
│ │ │ │ │ │
▼ ▼ ▼ ▼ ▼ ▼
UPDATE NEW UPDATE NEW UPDATE NEW
```
**Example: "Add dark mode"**
- "Need to also support custom themes" → New change (scope exploded)
- "System preference detection is harder than expected" → Update (same intent)
- "Let's ship toggle first, add preferences later" → Update then archive, then new change
## Best Practices
### Keep Changes Focused
One logical unit of work per change. If you're doing "add feature X and also refactor Y", consider two separate changes.
**Why it matters:**
- Easier to review and understand
- Cleaner archive history
- Can ship independently
- Simpler rollback if needed
### Use `/opsx:explore` for Unclear Requirements
Before committing to a change, explore the problem space:
```text
You: /opsx:explore
AI: What would you like to explore?
You: How should we handle rate limiting for our API?
AI: Let me investigate your current setup and options...
[Researches codebase, considers patterns]
I see three approaches we could take:
1. Middleware-based (simple, per-endpoint)
2. Redis-backed (scalable, shared state)
3. Token bucket per user (fair, complex)
Your current stack suggests #1 or #2. What's your scale?
```
Exploration clarifies thinking before any code gets written.
### Verify Before Archiving
Use `/opsx:verify` to check implementation matches artifacts:
```text
You: /opsx:verify
AI: Verifying add-dark-mode...
✓ All tasks in tasks.md are checked
✓ Requirements in specs have corresponding code
✓ Design decisions are reflected in implementation
Ready to archive!
```
Catches mismatches before you close out the change.
### Name Changes Clearly
Good names make `openspec list` useful:
```text
Good: Avoid:
add-dark-mode feature-1
fix-login-redirect update
optimize-product-query changes
implement-2fa wip
```
## Command Quick Reference
For full command details and options, see [Commands](commands.md).
| Command | Purpose | When to Use |
|---------|---------|-------------|
| `/opsx:propose` | Create change + planning artifacts | Fast default path (`core` profile) |
| `/opsx:explore` | Think through ideas with the AI | Start here when unsure: unclear requirements, investigation, comparing options |
| `/opsx:new` | Start a change scaffold | Expanded mode, explicit artifact control |
| `/opsx:continue` | Create next artifact | Expanded mode, step-by-step artifact creation |
| `/opsx:ff` | Create all planning artifacts | Expanded mode, clear scope |
| `/opsx:apply` | Implement tasks | Ready to write code |
| `/opsx:verify` | Validate implementation | Expanded mode, before archiving |
| `/opsx:sync` | Merge delta specs | Expanded mode, optional |
| `/opsx:archive` | Complete the change | All work finished |
| `/opsx:bulk-archive` | Archive multiple changes | Expanded mode, parallel work |
## 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 dropped from it. Remove the last requirement a capability has and you retire it: rather than leave a spec with nothing in it, archive deletes `openspec/specs/<capability>/spec.md`. Because that is the one archive step that removes a file, it has to be asked for — add `retire_capabilities: true` to the change's `.openspec.yaml`, alongside the `schema:` that file already needs. Without it the archive aborts and tells you so. Retirement deletes the whole file, so it is also refused while the spec holds anything outside its title, `## Purpose`, and its requirement blocks — a `## Notes` section, a comment under a requirement. The abort names those lines; move them into `## Purpose` or a requirement, or delete the spec by hand. For a spec in the caller's checkout, the archive output also names the `git checkout` that restores a committed file; selected stores receive checkout-scoped recovery guidance instead. 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-path>/spec.md` directly to change one. Here, `<capability-path>` is the directory relative to `specs/`, such as `user-auth` in a flat project or `identity/user-auth` in a project organized by domain.
## 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.
+42
View File
@@ -0,0 +1,42 @@
import tseslint from 'typescript-eslint';
export default tseslint.config(
{
files: ['src/**/*.ts'],
extends: [...tseslint.configs.recommended],
rules: {
// Prevent static imports of @inquirer modules to avoid pre-commit hook hangs.
// These modules have side effects that can keep the Node.js event loop alive
// when stdin is piped. Use dynamic import() instead.
// See: https://github.com/Fission-AI/OpenSpec/issues/367
'no-restricted-imports': [
'error',
{
patterns: [
{
group: ['@inquirer/*'],
message:
'Use dynamic import() for @inquirer modules to prevent pre-commit hook hangs. See #367.',
},
],
},
],
// Disable rules that need broader cleanup - focus on critical issues only
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-unused-vars': 'off',
'no-empty': 'off',
'prefer-const': 'off',
},
},
{
// init.ts is dynamically imported from cli/index.ts, so static @inquirer
// imports there are safe - they won't be loaded at CLI startup
files: ['src/core/init.ts'],
rules: {
'no-restricted-imports': 'off',
},
},
{
ignores: ['dist/**', 'node_modules/**', '*.js', '*.mjs'],
}
);
Generated
+27
View File
@@ -0,0 +1,27 @@
{
"nodes": {
"nixpkgs": {
"locked": {
"lastModified": 1767640445,
"narHash": "sha256-UWYqmD7JFBEDBHWYcqE6s6c77pWdcU/i+bwD6XxMb8A=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "9f0c42f8bc7151b8e7e5840fb3bd454ad850d8c5",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"nixpkgs": "nixpkgs"
}
}
},
"root": "root",
"version": 7
}

Some files were not shown because too many files have changed in this diff Show More