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>
* 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>
* 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>
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>
* 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>
* 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>
* 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>
* 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>
* 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>
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>
* 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>
* 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>
* 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>
* 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>
* 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>
* 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>
* 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>
* 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>
* 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>
* 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>
* 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>
* 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>
* 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>
* 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>
* 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>
* 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>
* 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>
* 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>
* 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>
* 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.
* fix(propose): stop before implementation
* fix(propose): require explicit implementation request
* fix(propose): hand implementation to apply
* test(propose): align parity hashes after rebase
* 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>
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.
* 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>
* 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>
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>
* 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>
* 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>
* 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>
* 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>
* 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#1307Closes#727Closes#1379Closes#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>
* 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>
* 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>
* 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>
* 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>
* 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>
* 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>
* 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#692Closes#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>
* 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>
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#920Fixes#717
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* 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>
* 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>
* 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>
* 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>
* 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>
* 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>
* 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#850Closes#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>
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>
* 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>
* 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>
* 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#1413Closes#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>
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>
* 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>
* 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>
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>
* 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>
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>
* 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>
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>
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.
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>
* 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.
* 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>
* 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>
* 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>
* 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>
`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>
* 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>
* 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>
* 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>
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.
* 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
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
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.
* 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#881Closes#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.
* 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>
* 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>
* 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
* 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>
* 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>
* 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>
* 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>
* 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>
* 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>
* 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>
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>
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>
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>
* 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>
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>
* 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>
* 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>
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>
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>
* 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>
* 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>
* 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>
* 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>
* 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.
* 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>
* 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>
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>
* 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>
* 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
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>
* 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
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>
* 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).
* 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>
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
* 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.
* 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>
* 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.
* 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
* 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>
* 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>
* 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>
* 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>
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.
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 \
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:
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.
- 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;
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 \
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.
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.
@@ -12,11 +12,12 @@ Follow the prompts to select version bump type and describe your changes.
## Workflow
1.**Add a changeset** — Run `pnpm changeset` locally before or after your PR
2.**Version PR** — CI opens/updates a "Version Packages" PR when changesets merge to main
3.**Release** — Merging the Version PR triggers npm publish and GitHub Release
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:** Contributors only need to run `pnpm changeset`. Versioning (`changeset version`) and publishing happen automatically in CI.
> **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
@@ -54,22 +55,23 @@ Include only the sections relevant to your change.
| Type | When to use | Example |
|------|-------------|---------|
| `patch` | Bug fixes, small improvements | Fixed crash when config missing |
| `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
**Create one for:**
- New features or commands
-Bug fixes that affect users
**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
- Performance improvements users would notice and that are planned for release
**Skip for:**
**Use the normal release cadence for:**
- Routine bug fixes that fit the normal release cadence
- Documentation-only changes
- Test additions/fixes
- Internal refactoring with no user impact
- Internal refactoring that preserves user behavior
- [#1622](https://github.com/Fission-AI/OpenSpec/pull/1622) [`59c16a4`](https://github.com/Fission-AI/OpenSpec/commit/59c16a4461254ed984d1d5e29d00af1a5610035a) Thanks [@clay-good](https://github.com/clay-good)! - ### New Features
- **Command Code command adapter** — Command Code is now a first-class, adapter-backed tool. `openspec init` generates OpenSpec commands under `.commandcode/commands/opsx-<id>.md` (invoked as `/opsx-<id>`) alongside the skills under `.commandcode/skills/`, matching Command Code's documented custom-slash-command surface.
- [#1613](https://github.com/Fission-AI/OpenSpec/pull/1613) [`42d7f67`](https://github.com/Fission-AI/OpenSpec/commit/42d7f673bc5f13378451267c8a9d0c23f63a2d1a) Thanks [@Angelthebestone](https://github.com/Angelthebestone)! - ### New Features
- **Command Code support** — `openspec init` now supports Command Code as an adapterless skills-only tool. It installs the OpenSpec skills under `.commandcode/skills/` and invokes them as `/openspec-*` commands, matching Command Code's native skill surface.
- [#1604](https://github.com/Fission-AI/OpenSpec/pull/1604) [`83be9d1`](https://github.com/Fission-AI/OpenSpec/commit/83be9d113e8310789c281f7c8a00ed4fad191dd5) Thanks [@clay-good](https://github.com/clay-good)! - Add `openspec validate --archived`: an opt-in check that every change under `changes/archive/` has all of its `tasks.md` checkboxes ticked, exiting non-zero if any are unchecked. This surfaces changes that were archived with unfinished work — which the normal validate flow never catches, because it only looks at active changes — and is meant for a pre-commit or CI hook ([#205](https://github.com/Fission-AI/OpenSpec/issues/205)). It is a standalone scope: it does not alter any existing `validate` invocation and does not re-validate already-applied spec deltas.
### Patch Changes
- [#1530](https://github.com/Fission-AI/OpenSpec/pull/1530) [`bf5099e`](https://github.com/Fission-AI/OpenSpec/commit/bf5099e39fdb5d7bde2adc84f49ea93afd7463e9) Thanks [@clay-good](https://github.com/clay-good)! - Apply workflow now tells agents to surface unexpected scope instead of hiding it. When a task needs work beyond what the spec describes, the `/opsx:apply` skill and command guidance direct the agent to pause and report the added scope rather than silently narrowing, deferring, or simplifying away specified behavior, and to mark a task complete only when its specified behavior is fully implemented. Fixes [#1529](https://github.com/Fission-AI/OpenSpec/issues/1529).
- [#1603](https://github.com/Fission-AI/OpenSpec/pull/1603) [`9ae75c8`](https://github.com/Fission-AI/OpenSpec/commit/9ae75c86efe5d326ffa7ca5a3fd64b1f1e7728c2) Thanks [@clay-good](https://github.com/clay-good)! - `openspec archive` no longer writes terminal escape codes to a redirected or captured stdout. Its confirmation prompts and the no-argument change picker drew their live UI with ANSI cursor-move sequences even when stdout was not a terminal — noise in a redirected log, and in some non-interactive hosts an unbounded render loop that could grow the captured output until the disk filled. When stdout (or stdin) is not a terminal, archive now reads the confirmations as plain text, and a no-argument run asks you to pass a change name up front instead of drawing a menu. Piped answers (`printf 'y\n' | openspec archive …`) and `--yes` behave as before, and interactive terminals are unchanged. Fixes [#1526](https://github.com/Fission-AI/OpenSpec/issues/1526).
- [#1528](https://github.com/Fission-AI/OpenSpec/pull/1528) [`9425897`](https://github.com/Fission-AI/OpenSpec/commit/942589741de35f1b8896b410d7ea70295bb137c0) Thanks [@Marzx13](https://github.com/Marzx13)! - Canonicalize rebuilt specs to end with exactly one final LF. Previously a spec whose `## Requirements` section was last was rebuilt with a trailing blank line (`\n\n`), which failed Markdown whitespace checks after sync or archive. Internal spacing and content after the Requirements section are unchanged.
- [#1640](https://github.com/Fission-AI/OpenSpec/pull/1640) [`610b78f`](https://github.com/Fission-AI/OpenSpec/commit/610b78f6554e8aabfa294df53962428ff85c8b76) Thanks [@clay-good](https://github.com/clay-good)! - Preserve the blank lines around a spec's `## Requirements` heading when syncing a delta. `openspec archive` rebuilt `openspec/specs/<capability>/spec.md` by joining its slices with a bare newline, so the blank lines that surround the heading were dropped and the resulting file failed Markdown whitespace checks. The rebuild now keeps that spacing intact. Fixes [#1625](https://github.com/Fission-AI/OpenSpec/issues/1625). Thanks [@jwang513](https://github.com/jwang513)! ([#1637](https://github.com/Fission-AI/OpenSpec/pull/1637))
- [#1640](https://github.com/Fission-AI/OpenSpec/pull/1640) [`610b78f`](https://github.com/Fission-AI/OpenSpec/commit/610b78f6554e8aabfa294df53962428ff85c8b76) Thanks [@clay-good](https://github.com/clay-good)! - `openspec validate --all` and `openspec list --json` no longer silently pass when run outside an OpenSpec project. From a directory with no root they used to resolve the current directory as an implicit root, exit 0, and report empty results — a false pass for CI and agents. Bulk validation (`--all`, `--changes`, `--specs`) and `list` now require an existing root (the `openspec/project.md` fallback for legacy projects is kept), while direct validation and other intentional implicit-root workflows are unchanged. Thanks [@clay-good](https://github.com/clay-good)! ([#1612](https://github.com/Fission-AI/OpenSpec/pull/1612))
- [#1640](https://github.com/Fission-AI/OpenSpec/pull/1640) [`610b78f`](https://github.com/Fission-AI/OpenSpec/commit/610b78f6554e8aabfa294df53962428ff85c8b76) Thanks [@clay-good](https://github.com/clay-good)! - Label the `update` workflow in the `openspec config` workflow picker. The checklist had friendly labels for 11 of the 12 workflows but was missing `update`, so that row — one of the six core workflows every user sees — fell back to its raw id with a placeholder description. The update-change template's stale "expanded-profile" wording is also reworded to "optional". Fixes [#1627](https://github.com/Fission-AI/OpenSpec/issues/1627). Thanks [@clay-good](https://github.com/clay-good)! ([#1632](https://github.com/Fission-AI/OpenSpec/pull/1632))
- [#1640](https://github.com/Fission-AI/OpenSpec/pull/1640) [`610b78f`](https://github.com/Fission-AI/OpenSpec/commit/610b78f6554e8aabfa294df53962428ff85c8b76) Thanks [@clay-good](https://github.com/clay-good)! - `openspec schema fork` now preserves the source schema's YAML formatting. Renaming a forked `schema.yaml` round-tripped through a parse/re-serialize step that dropped comments, could rewrite block-scalar style (a literal `|` folded to `>`), and reordered keys, so the fork no longer matched its source. The rename now edits the document in place via the YAML Document API, leaving comments, scalar style, and key order untouched. Thanks [@clay-good](https://github.com/clay-good)! ([#1607](https://github.com/Fission-AI/OpenSpec/pull/1607))
- [#1640](https://github.com/Fission-AI/OpenSpec/pull/1640) [`610b78f`](https://github.com/Fission-AI/OpenSpec/commit/610b78f6554e8aabfa294df53962428ff85c8b76) Thanks [@clay-good](https://github.com/clay-good)! - `openspec schemas` now resolves through the canonical OpenSpec root-selection precedence instead of always reading from the current directory. It accepts `--store <id>`, rejects `--store-path` like the other store-aware commands, and returns the shared machine-readable diagnostics on JSON failures, while preserving the existing human output and bare JSON array on success. Thanks [@Patodo](https://github.com/Patodo)! ([#1616](https://github.com/Fission-AI/OpenSpec/pull/1616))
- [#1640](https://github.com/Fission-AI/OpenSpec/pull/1640) [`610b78f`](https://github.com/Fission-AI/OpenSpec/commit/610b78f6554e8aabfa294df53962428ff85c8b76) Thanks [@clay-good](https://github.com/clay-good)! - `openspec validate` now warns on ambiguous task numbering in `spec-driven` changes: a task ID duplicated at full depth (including across resolved task files), or a task whose leading number disagrees with its enclosing `## N.` group. Numeric-looking text outside numbered groups is ignored, and custom schemas are unchanged until they opt in. The checks run across direct, bulk, and deprecated change validation. Closes [#1520](https://github.com/Fission-AI/OpenSpec/issues/1520). Thanks [@alectimison-maker](https://github.com/alectimison-maker)! ([#1523](https://github.com/Fission-AI/OpenSpec/pull/1523))
- **Don't let a legacy Codex upgrade hijack the vendor-neutral `agents` target** — `openspec update` no longer overwrites an existing `.agents` skills tree (and its ownership marker) when Codex is detected only from leftover global `~/.codex/prompts`. Because Codex and the vendor-neutral `agents` target share `.agents/skills`, a project that used the `agents` target could have its generic skills silently rewritten with Codex-specific syntax and its target flipped to Codex on the next `update --force`. The legacy-upgrade path now respects the established owner of a shared skills directory, matching the one-writer rule `openspec init` already applies. When an upgrade is skipped this way, that tool's repo-local legacy files (e.g. `.codex/prompts/openspec-*.md`) are also preserved rather than cleaned up, since no replacement was written to take their place. A genuine first-time Codex upgrade (no `.agents` tree yet) is unaffected.
- **Stop silently dropping unlabeled scenarios on archive** — `openspec validate` and `openspec archive` now recognize every level-4 (`####` followed by whitespace) child of a requirement as a scenario, matching how the spec is counted elsewhere. Before, the scenario-loss guard only recognized headers written exactly as `#### Scenario:`, so a `MODIFIED` requirement that dropped a differently-labeled child (for example `#### Edge case`) passed validation and was then permanently deleted by archive with no warning. Both paths now agree, so the loss is caught at authoring time. Scenario names are normalized when comparing (an optional `Scenario:` prefix and a CommonMark closing `#` run are ignored), so simply relabeling a scenario is not mistaken for dropping one.
-`openspec init` now suggests an IDE restart only when an IDE-resident tool such as Cursor, GitHub Copilot, Continue, or Cline was configured. CLI tools like Claude Code, Codex, and Gemini CLI no longer show the hint, since their commands work as soon as the files exist.
- [#1609](https://github.com/Fission-AI/OpenSpec/pull/1609) [`804427b`](https://github.com/Fission-AI/OpenSpec/commit/804427b6ff3f3b35b542365ba8b32e183fce3287) Thanks [@clay-good](https://github.com/clay-good)! - Suppress the first-run telemetry disclosure notice when `--json` is used. On a
first-ever run the notice was written to stdout and could break `--json`
consumers; it is now deferred to the first later non-JSON run, keeping `--json`
output valid while still guaranteeing the disclosure.
## 1.8.0
### Minor Changes
- [#1303](https://github.com/Fission-AI/OpenSpec/pull/1303) [`1aa0f2a`](https://github.com/Fission-AI/OpenSpec/commit/1aa0f2abfc19f2487f5b8566e6eb3bf15f41c20a) Thanks [@solanab](https://github.com/solanab)! - Add the vendor-neutral `agents` target: `openspec init --tools agents` installs the workflow skills to `.agents/skills/openspec-*/SKILL.md`, the shared location AGENTS.md-compatible assistants read. It is skills-only, so no slash commands are generated. Because `agents` is now a real target, `--tools all` includes it and creates `.agents/skills/` where it previously did not.
- [#1274](https://github.com/Fission-AI/OpenSpec/pull/1274) [`7a4a745`](https://github.com/Fission-AI/OpenSpec/commit/7a4a745d803b698c34947eda6d73b5a24aebb58c) Thanks [@NicoAvanzDev](https://github.com/NicoAvanzDev)! - Generate GitHub Copilot coding agent setup and custom agent files during `openspec init` and keep them synchronized during `openspec update`.
- [#1214](https://github.com/Fission-AI/OpenSpec/pull/1214) [`161f945`](https://github.com/Fission-AI/OpenSpec/commit/161f9454a372aab67c495d780928bba89c829f3e) Thanks [@showms](https://github.com/showms)! - Add MiniMax Code as a global skills-only tool target.
- [#1518](https://github.com/Fission-AI/OpenSpec/pull/1518) [`568e56c`](https://github.com/Fission-AI/OpenSpec/commit/568e56c67231dbe2447aca4f0e7995c05ada95a3) Thanks [@clay-good](https://github.com/clay-good)! - ### New Features
- **Atlassian Rovo Dev CLI** — `openspec init --tools rovodev` installs the OpenSpec workflow skills for Atlassian's Rovo Dev CLI. It is skills-only (no slash commands), written to `.rovodev`.
### Bug Fixes
- **Codex skills now live in the shared `.agents` directory** — `openspec init` and `openspec update` install Codex skills under `.agents/skills/` (the canonical location assistants read) and migrate an existing `.codex` skills directory in place. Files you customized are preserved, not overwritten.
- **`openspec status` separates planning from implementation** — status now reports `isPlanningComplete` (every non-skipped planning artifact exists; skipped artifacts count as satisfied without being written) distinctly from overall progress, and its messages no longer imply a change is finished before it has been implemented. `isComplete` is kept as a compatibility alias, so existing scripts keep working.
- [#1517](https://github.com/Fission-AI/OpenSpec/pull/1517) [`73207a6`](https://github.com/Fission-AI/OpenSpec/commit/73207a6f2cd235729ac3fe3cb1e44152b8f63f12) Thanks [@clay-good](https://github.com/clay-good)! - Make GitHub Copilot cloud coding-agent files opt-in. Selecting the `github-copilot` tool no longer silently writes a GitHub Actions workflow into `.github/`; `openspec init` now asks first (default No) and remembers the choice in `openspec/config.yaml` (`githubCopilot.cloudAgent`). Use `--copilot-cloud` / `--no-copilot-cloud` to decide non-interactively.
-`openspec update` never prompts — it only refreshes cloud files for projects that opted in (or that already have generated cloud files, so existing setups keep working).
- Opting out (`--no-copilot-cloud` or `cloudAgent: false`) removes OpenSpec-managed cloud files; a user-customized file is always preserved, never overwritten or deleted.
-`init` and `update` now report whether cloud files were written, skipped, or left untouched — and if you already have your own `copilot-setup-steps.yml`, they say it was preserved and that you need to add the OpenSpec install step by hand.
- [#1484](https://github.com/Fission-AI/OpenSpec/pull/1484) [`521ee33`](https://github.com/Fission-AI/OpenSpec/commit/521ee33e6ece269241b45e08017ee60f13fdef08) Thanks [@clay-good](https://github.com/clay-good)! - Retire a capability when a change removes its last requirement. A change that declares `retire_capabilities: true` in its `.openspec.yaml` (alongside the `schema:` that file requires) may now be archived even when its REMOVED entries take a capability's last requirement: `openspec archive` deletes that capability's main spec instead of aborting with "Spec must have at least one requirement". Without the marker nothing changes — the archive aborts exactly as before, except the message now names the marker as the way out. Retirement happens only when the emptied spec could not have been written at all, every one is named in the archive output, a pasteable `git checkout` is included when the spec lived in the caller's checkout, and `--no-validate` never retires. Archive now also rejects a main spec with duplicate canonical requirement names instead of letting delta reconciliation collapse one of the duplicate blocks. One thing to know before retiring: a capability's spec is the base another change's MODIFIED block is checked against, so an in-flight change that modifies the capability you just retired will keep validating clean and then refuse to archive ("target spec does not exist; only ADDED requirements are allowed for new specs") — close or rework that change alongside the retirement.
### Patch Changes
- [#1502](https://github.com/Fission-AI/OpenSpec/pull/1502) [`ece8660`](https://github.com/Fission-AI/OpenSpec/commit/ece8660d44bd19b86440376327752cda3d7b0717) Thanks [@clay-good](https://github.com/clay-good)! - `openspec validate` now treats the English `SHALL`/`MUST` convention as guidance in normal mode, so requirements written in other languages can validate. Strict mode continues to enforce the convention.
- [#1483](https://github.com/Fission-AI/OpenSpec/pull/1483) [`2b3d368`](https://github.com/Fission-AI/OpenSpec/commit/2b3d368539132be6311e55db58899abbf5306b81) Thanks [@clay-good](https://github.com/clay-good)! - Tell the caller which flag to pass when `openspec archive` cannot ask its confirmation questions. An AI agent (or any script) runs the CLI with stdin closed, so every prompt rejects with `@inquirer`'s `User force closed the prompt with 0 null` — the archive aborted with an error that named neither the question nor the flag, and agents burned a turn guessing ([#1479](https://github.com/Fission-AI/OpenSpec/issues/1479)). Each confirmation now reports what it needed and a pasteable rerun that carries the flags you already passed: `openspec archive <name> --skip-specs --yes` stays a `--skip-specs` run, so following the suggestion cannot merge specs you opted out of merging, and a change name that needs quoting gets double quotes, the one form bash, zsh, PowerShell and cmd.exe all read the same way (a name no shell reads literally even quoted — one containing `$`, a backtick, or the `%`/`!` that cmd.exe still expands inside quotes — is left as a `<change-name>` placeholder rather than a command that would target something else). `openspec archive` with no change name used to swallow the same failure, print `No change selected. Aborting.` and exit 0 — success for a run that archived nothing; it now exits 1 asking for a change name, matching how `openspec show` and `openspec validate` already behave without a terminal. The check is reactive — it inspects a prompt that already failed — so answers piped into the command, `--yes`, `--json`, and Ctrl-C all behave exactly as before, and a run that OpenSpec already considers non-interactive (`CI`, `OPEN_SPEC_INTERACTIVE=0`, `--no-interactive`) gets the guidance even when the runner allocated a pty. The onboarding walkthrough, the only generated guidance that tells an agent to run `openspec archive`, now shows `--yes`.
- [#1486](https://github.com/Fission-AI/OpenSpec/pull/1486) [`427abf4`](https://github.com/Fission-AI/OpenSpec/commit/427abf40ac45a9a44f78eb74c81f53f9f4197ccf) Thanks [@clay-good](https://github.com/clay-good)! - Task progress now counts indented sub-tasks. A `tasks.md` whose sub-tasks were unfinished reported `✓ Complete` in `openspec list` and `openspec view`, was missing those tasks from the `openspec instructions apply` list, and archived with no incomplete-task warning, because both checkbox parsers only matched checkboxes at column 0.
Progress counting and the apply task list now share one parser, so `list`, `view`, `archive` and `apply` agree about which lines of a tasks file are tasks. A checkbox with no text after it is left out of the apply list, which has nothing to act on, but still counts toward every progress number; a file of nothing but such checkboxes now asks to be rewritten rather than reporting itself done. The shared pattern matches every line the two it replaced matched, and more, so task counts can rise but never fall: no change starts reporting less work than before, and archive's incomplete-task warning can only become stricter. Checkboxes are still counted wherever they appear, including inside a code fence, an HTML comment or an indented block, so a `tasks.md` that shows a checklist as a format example can now count that example as work — remove it from the file, or pass `--yes` to archive.
- [#1500](https://github.com/Fission-AI/OpenSpec/pull/1500) [`26bd1d4`](https://github.com/Fission-AI/OpenSpec/commit/26bd1d4e5c6c6ba75bd7d6136424019b2bf89ced) Thanks [@clay-good](https://github.com/clay-good)! - Keep generated workflows on the selected store, handle optional workflow fallbacks safely, and validate synced specs before reporting success.
- [#1490](https://github.com/Fission-AI/OpenSpec/pull/1490) [`45cca5d`](https://github.com/Fission-AI/OpenSpec/commit/45cca5db6137ed209117cc70510eb3e057fb981b) Thanks [@clay-good](https://github.com/clay-good)! - Say before confirmation when archiving a change will delete a note written next to a requirement. A requirement absorbs anything below it that OpenSpec doesn't recognize as a new heading — a note indented by the one to three spaces Markdown allows, for example — so removing or modifying that requirement took the note with it, silently. `openspec archive` now names content the rebuilt spec would actually drop and where to move it to keep it. The merge itself is unchanged: nothing is relocated, because a `#` line inside a scenario looks identical to a note and moving one of those would rewrite the spec wrongly.
- [#1492](https://github.com/Fission-AI/OpenSpec/pull/1492) [`690a27e`](https://github.com/Fission-AI/OpenSpec/commit/690a27e649c4a3325daeb0f6667ebe0f82792179) Thanks [@mc856](https://github.com/mc856)! - `openspec init` and `openspec update` no longer delete the CoStrict and Junie command files they just generated. Legacy cleanup removes artifacts older OpenSpec versions left behind, and two of its patterns named paths the current adapters still write to. CoStrict's was a whole-directory removal of `.cospec/openspec/commands/`, the folder the adapter writes `opsx-<id>.md` into, so every run wiped the directory — including any file the user kept there — while the banner above it read `No user content to preserve`. Junie's `.junie/commands/opsx-*.md` listed its own current output. Cleanup runs before the config migration, so on a config that has no `profile` key yet the missing command files make delivery detection read the project as skills-only and persist that to the global config: the files are not regenerated, and the preference changes for every other project too.
CoStrict is now a file pattern, `.cospec/openspec/commands/openspec-*.md`, matching the three commands the pre-`opsx` CoStrict integration wrote there (`openspec-proposal.md`, `openspec-apply.md`, `openspec-archive.md`) and the same shape every other file-based tool already uses. Junie's entry is removed outright: Junie support arrived after the slash configurators that wrote `openspec-*` files were deleted, so no OpenSpec version ever created those files there. Genuinely legacy files are still detected and removed, and no other tool's patterns change — they never overlapped their adapter's current output.
- [#1501](https://github.com/Fission-AI/OpenSpec/pull/1501) [`0b20ae3`](https://github.com/Fission-AI/OpenSpec/commit/0b20ae3964283bdcb4e34ea7380770857f6a339c) Thanks [@clay-good](https://github.com/clay-good)! - Keep the propose workflow focused on planning, clarify material ambiguities before creating a change, and hand implementation off to the apply workflow.
- [#1503](https://github.com/Fission-AI/OpenSpec/pull/1503) [`8a3850d`](https://github.com/Fission-AI/OpenSpec/commit/8a3850da735e241c14ad94935463f879b33f21a9) Thanks [@clay-good](https://github.com/clay-good)! - When exploration turns into a new change, generated explore guidance now instructs agents to run `openspec new change` before writing requested artifacts. This preserves the required `.openspec.yaml` metadata instead of letting an agent create an incomplete change directory by hand. After the user accepts a capture, explore also creates the requested artifacts without requiring another workflow command.
- [#1513](https://github.com/Fission-AI/OpenSpec/pull/1513) [`622c509`](https://github.com/Fission-AI/OpenSpec/commit/622c509a1349c3ad9c52cd1a4ee007bd47549204) Thanks [@FasterPHP](https://github.com/FasterPHP)! - Honor `telemetry.enabled` in global config. `false` disables anonymous telemetry and `openspec update` version checks; unset keeps telemetry enabled, and env/CI opt-outs still take precedence.
- [#1499](https://github.com/Fission-AI/OpenSpec/pull/1499) [`9cd845f`](https://github.com/Fission-AI/OpenSpec/commit/9cd845fc459b71486d9f2424c2e1f38e2ca8766e) Thanks [@clay-good](https://github.com/clay-good)! - Keep generated files, specs, archive moves, and local state inside their intended security boundaries without breaking linked monorepo workflows.
- [#1482](https://github.com/Fission-AI/OpenSpec/pull/1482) [`84ebc57`](https://github.com/Fission-AI/OpenSpec/commit/84ebc57cb3f0e91b93484484092fdc2f9fcf39e6) Thanks [@clay-good](https://github.com/clay-good)! - `openspec validate <change>` now reports a MODIFIED requirement that omits a scenario the main spec still has — the same loss archive already refuses to apply — so the change fails at authoring time instead of at archive time. A change carrying a stale MODIFIED block will start failing validation; it was already unarchivable, and the message names the scenarios to copy back in.
- [#1475](https://github.com/Fission-AI/OpenSpec/pull/1475) [`17af60c`](https://github.com/Fission-AI/OpenSpec/commit/17af60c66e4c049e3986fdbafcdc16b202cda59f) Thanks [@clay-good](https://github.com/clay-good)! - Add Hermes Agent as a supported AI tool: `openspec init --tools hermes` installs the workflow skills (Hermes is skills-only and invokes them directly).
- [#1475](https://github.com/Fission-AI/OpenSpec/pull/1475) [`17af60c`](https://github.com/Fission-AI/OpenSpec/commit/17af60c66e4c049e3986fdbafcdc16b202cda59f) Thanks [@clay-good](https://github.com/clay-good)! - Add ZCode as a supported AI tool: `openspec init --tools zcode` generates its skills and `/opsx:*` commands.
- [#1475](https://github.com/Fission-AI/OpenSpec/pull/1475) [`17af60c`](https://github.com/Fission-AI/OpenSpec/commit/17af60c66e4c049e3986fdbafcdc16b202cda59f) Thanks [@clay-good](https://github.com/clay-good)! - Codex is now skills-only: workflows install as `$openspec-*` skills and previously managed custom prompts are retired (existing ones are cleaned up on update).
- [#1062](https://github.com/Fission-AI/OpenSpec/pull/1062) [`eac2973`](https://github.com/Fission-AI/OpenSpec/commit/eac2973819037727b10214f70db2f54d82f2d891) Thanks [@showms](https://github.com/showms)! - Add current project context and per-operation guidance to apply and archive workflows. Projects can configure `operations.apply.guidance` and `operations.archive.guidance`; `openspec instructions apply` returns apply inputs, and the new read-only `openspec instructions archive` surface returns archive inputs for the selected root.
Archive, bulk archive, and sync skills now load current archive inputs and `specs` artifact rules at execution time, fail before writes or moves when required instruction lookups fail, and reuse specs-rule snapshots during inline sync.
- [#1475](https://github.com/Fission-AI/OpenSpec/pull/1475) [`17af60c`](https://github.com/Fission-AI/OpenSpec/commit/17af60c66e4c049e3986fdbafcdc16b202cda59f) Thanks [@clay-good](https://github.com/clay-good)! - Publish the workflow skills as static `skills/<name>/SKILL.md` files so `npx skills add Fission-AI/OpenSpec` works.
- [#1399](https://github.com/Fission-AI/OpenSpec/pull/1399) [`27b22ab`](https://github.com/Fission-AI/OpenSpec/commit/27b22ab4cbf530fa00e17f0f6b75a44d56777542) Thanks [@clay-good](https://github.com/clay-good)! - Add `skip_specs: true` change metadata for work with no spec-level behavior change (pure refactors, tooling, docs). `openspec validate` accepts a zero-delta change that declares the marker (honored only when the metadata parses under the shared change-metadata schema and names a schema that loads) and errors when the marker and delta specs are both present, the artifact graph no longer blocks `tasks` on spec files for such changes, `openspec status` renders the specs stage as explicitly skipped, and the propose/specs guidance points to the marker instead of contradicting the validator.
- [#1475](https://github.com/Fission-AI/OpenSpec/pull/1475) [`17af60c`](https://github.com/Fission-AI/OpenSpec/commit/17af60c66e4c049e3986fdbafcdc16b202cda59f) Thanks [@clay-good](https://github.com/clay-good)! - Resolve symlinked schema directories so schemas shared via symlink (e.g. from a dotfiles repo) are discovered.
- [#1470](https://github.com/Fission-AI/OpenSpec/pull/1470) [`6295515`](https://github.com/Fission-AI/OpenSpec/commit/6295515d4da4f7c76eaed00b7f1926771eae92de) Thanks [@clay-good](https://github.com/clay-good)! - `openspec update` now offers to upgrade the CLI when yours is behind the published one. Instruction files are generated by the installed CLI, so a stale install reported `✓ All 1 tool(s) up to date (v1.6.0)` while the workflows added in newer releases were never written:
```text
A newer OpenSpec CLI is available (v1.6.0 → v1.7.0).
Say yes and it upgrades, confirms the new version is the one that answers, then re-runs the update so the new workflows arrive in the same command. Say no and it prints the command matching how you installed OpenSpec, and updates with what you have. Nothing happens to your machine that you did not agree to: the offer appears only in an interactive terminal and only where `npm install -g` would help, and the check is skipped in CI or when `OPENSPEC_NO_UPDATE_CHECK`, `DO_NOT_TRACK=1`, or `OPENSPEC_TELEMETRY=0` is set.
See [CLI reference → `openspec update`](https://github.com/Fission-AI/OpenSpec/blob/main/docs/cli.md#openspec-update) for the per-install-method behavior and every opt-out.
### Patch Changes
- [#1404](https://github.com/Fission-AI/OpenSpec/pull/1404) [`a84ae70`](https://github.com/Fission-AI/OpenSpec/commit/a84ae70e8c6ef6ffaab56599d6f91fa39873e63d) Thanks [@clay-good](https://github.com/clay-good)! - Generated skills for tools without a command adapter (Kimi Code, Mistral Vibe, Hermes, ForgeCode, CodeArts) no longer reference `/opsx:*` commands that were never generated: skill cross-references, the init getting-started hint, and the profile-migration message now use each tool's documented skill invocation (Kimi Code: `/skill:openspec-*`; others: `/openspec-*`), and Codex — skills-invocable with no slash surface — gets a syntax-neutral hint that names the skill. Selections that mix invocation syntaxes print one labeled hint per distinct form, so every advertised instruction is usable by the tool it names. When `delivery: commands` would generate nothing for a selected tool, init prints a configuration correction naming that tool, even when other tools did get commands or skills. The committed skills.sh distribution is regenerated with skill references (default `/openspec-*` form, as that channel installs skills only).
- [#1363](https://github.com/Fission-AI/OpenSpec/pull/1363) [`5199f41`](https://github.com/Fission-AI/OpenSpec/commit/5199f41a5d523b9212dd2854ec5e505d2f80e2e7) Thanks [@clay-good](https://github.com/clay-good)! - ### Features
- **One default store for every repo on your machine** — `openspec config set defaultStore <id>` sets a machine-level fallback root: any command run outside a planning root, with no `--store` flag and no project `store:` pointer, resolves to that store. It sits at the bottom of the precedence list, so `--store`, a local root, and a project pointer all still win. The root banner and JSON `root` block report the distinct provenance `source: "global_default"`, so users and tooling can tell a machine-wide default from a repo's own pointer. A stale id degrades to the underlying store error with a fix that names `openspec config unset defaultStore`.
- [#1435](https://github.com/Fission-AI/OpenSpec/pull/1435) [`6a5171e`](https://github.com/Fission-AI/OpenSpec/commit/6a5171e18630db4ed8e78c9edfaae4be532e2af6) Thanks [@clay-good](https://github.com/clay-good)! - `openspec new change` now accepts numeric-prefixed names like `100-add-feature` or `00001-add-auth`, useful for ordering or tiering changes. Change names now use the same kebab-case grammar as store ids and change metadata (a leading digit is allowed); `archive` already treated date-prefixed names as a supported convention. Uppercase, spaces, underscores, and leading/trailing or consecutive hyphens are still rejected, and every previously valid name stays valid.
- [#1425](https://github.com/Fission-AI/OpenSpec/pull/1425) [`040a869`](https://github.com/Fission-AI/OpenSpec/commit/040a86931f5398167137a483b2e8081aec13016e) Thanks [@clay-good](https://github.com/clay-good)! - Compare config key guards literally instead of through a helper.
`setNestedValue` and `deleteNestedValue` rejected prototype-reaching key segments through a helper that did a `Set` lookup. That is correct, but static analysis could not follow it, so CodeQL kept reporting prototype-pollution on the very assignments the guard protects. The segments are now compared literally in the same function, still checked across the whole path before anything is written. Behavior is unchanged for every input, verified against the previous implementation across 400,000 generated cases.
- [#1431](https://github.com/Fission-AI/OpenSpec/pull/1431) [`6a4f0d7`](https://github.com/Fission-AI/OpenSpec/commit/6a4f0d7f3384486132cb9c516b635c23cadc1fa2) Thanks [@clay-good](https://github.com/clay-good)! - A delta spec that introduces a brand-new capability can now open with a `## Purpose`, and `openspec archive` uses it as the Purpose of the main spec it creates instead of writing the `TBD - created by archiving change <name>. Update Purpose after archive.` placeholder over it. The `specs` artifact instruction, its example, the delta template and the `openspec-sync-specs` skill all tell authors and agents to write one, so the CLI and agent-driven sync paths produce the same main spec.
Archive keeps the placeholder when the delta has no usable `## Purpose`:
- no `## Purpose` header outside a code fence or HTML comment, or a body that is only a code fence or only a comment
- a body that would leave a spec its own parser cannot read — a heading or requirement header that truncates a section, an unterminated fence, or any HTML comment
- in the second case archive also says why, and still completes rather than aborting
A carried Purpose under 50 characters is kept but warned about, since `openspec validate --strict` reports it as too brief. The Purpose of an existing main spec is never touched; archive warns when it ignores a delta's Purpose there.
- [#1437](https://github.com/Fission-AI/OpenSpec/pull/1437) [`19d4171`](https://github.com/Fission-AI/OpenSpec/commit/19d41714c8b790488732687443713e406ef5aeef) Thanks [@clay-good](https://github.com/clay-good)! - `openspec archive` no longer aborts when a REMOVED delta's requirement is already gone from the main spec (the early-sync pattern the sync skill teaches): it warns, treats the removal as already applied, and reports applied-only totals. In `--json` mode those warnings are carried in a new optional `warnings` array on the archive result. When every operation for a spec was already synced, archive skips rewriting that file instead of churning normalization differences into it. A delta that both RENAMEs and REMOVEs the same requirement is now rejected explicitly, by both `validate` and `archive` — the two spellings are compared case- and whitespace-insensitively — and a REMOVED header that differs only in case or whitespace from an existing requirement still aborts (that is a typo, not an early sync). Also fixed: the archive delta gate matches section headers case-insensitively like the parser; symlinked `specs/<capability>/spec.md` files are discovered instead of silently dropped; `openspec show <change>` no longer prints a spurious "scenarios" flag warning; files generated for qwen and bob reference commands by their real hyphenated names (`/opsx-<id>`), and init's getting-started hint follows suit; apply/update/onboard guidance names the CLI fallback for profiles that don't install `/opsx:continue` or `/opsx:new`.
- [#1411](https://github.com/Fission-AI/OpenSpec/pull/1411) [`c439a4e`](https://github.com/Fission-AI/OpenSpec/commit/c439a4ee48ef02dcdae6ac8101b7d12924695e7e) Thanks [@clay-good](https://github.com/clay-good)! - Fix phantom requirements parsed from delta specs, which made `openspec archive` warn about problems `openspec validate` never reported.
A header inside a delta section that is not a `### Requirement:` header — a divider such as `### Documentation Requirements` — was read as a requirement with no scenario. `openspec archive` warned that it was missing a scenario, and `openspec show <change> --json` and `openspec change list` counted it as an extra delta. The change parser now ignores those headers, matching the delta reader, so the phantom is gone from the warnings and from the JSON. Main spec parsing is unchanged.
`openspec archive` also no longer repeats requirement-level issues from the delta specs in its non-blocking "Proposal warnings in proposal.md" block. Each defect was printed twice there, and a `## REMOVED Requirements` entry — names-only by design — was reported as missing a scenario on every correct removal. Delta spec validation still reports and blocks on genuine defects, and proposal-level warnings are unchanged.
- **Archive no longer races the spec sync, or reports a sync that never landed** — the generated `openspec-archive-change` skill (and the matching `opsx:archive` command) handed the spec sync to a background task and then moved the change folder immediately. The archive could move the delta specs out from under the running sync: the change ended up archived, `openspec/specs/` was never updated, and the summary still reported `Specs: ✓ Synced`. The sync now runs inline, and the archive only proceeds once every capability with a delta spec has been checked against it — ADDED present, MODIFIED changes applied, REMOVED gone, RENAMED under the new name and not the old. If the sync fails or a capability doesn't match, the archive stops and reports what differs instead of claiming success; nothing has moved, so you can fix it and retry.
- [#1475](https://github.com/Fission-AI/OpenSpec/pull/1475) [`17af60c`](https://github.com/Fission-AI/OpenSpec/commit/17af60c66e4c049e3986fdbafcdc16b202cda59f) Thanks [@clay-good](https://github.com/clay-good)! - Apply profile changes with the installed CLI instead of shelling out to `npx`, which could run a different version.
- [#1475](https://github.com/Fission-AI/OpenSpec/pull/1475) [`17af60c`](https://github.com/Fission-AI/OpenSpec/commit/17af60c66e4c049e3986fdbafcdc16b202cda59f) Thanks [@clay-good](https://github.com/clay-good)! - Delta and main-spec parsers strip a UTF-8 BOM, so files saved by Windows editors or PowerShell redirects no longer fail with "No delta sections found".
- **Bulk archive now stops when you pick "Cancel"** — the generated `openspec-bulk-archive-change` skill (and the matching `opsx:bulk-archive` command) offered a "Cancel" option at the confirmation prompt but never told the agent what to do with it, so the next step archived every selected change anyway. The prompt now routes each answer by intent: "Cancel" stops without archiving anything, the archive options proceed (the ready-only option archives just the changes the status table marks `Ready` or `Ready*`), and any other answer re-asks instead of archiving. The single-change archive skill already routes Cancel this way; this brings the bulk variant in line.
- [#1375](https://github.com/Fission-AI/OpenSpec/pull/1375) [`52a8bce`](https://github.com/Fission-AI/OpenSpec/commit/52a8bce1fd2bc98c51fa35cf0cfa05e799eb4404) Thanks [@clay-good](https://github.com/clay-good)! - `--change` now accepts any change name that exists on disk (e.g. date-prefixed names like `2026-07-04-voice-copilot-v1`), matching what `list`, `validate`, and `archive` already resolve. Lookup still rejects unsafe names (path separators, `..`, hidden entries); the kebab-case naming rule still applies when creating a change.
- [#1475](https://github.com/Fission-AI/OpenSpec/pull/1475) [`17af60c`](https://github.com/Fission-AI/OpenSpec/commit/17af60c66e4c049e3986fdbafcdc16b202cda59f) Thanks [@clay-good](https://github.com/clay-good)! - `openspec new change` rejects names over 200 characters with a validation message instead of surfacing a raw ENAMETOOLONG filesystem error.
- [#1447](https://github.com/Fission-AI/OpenSpec/pull/1447) [`fb19699`](https://github.com/Fission-AI/OpenSpec/commit/fb196995dad017074415a638824eb546f3321cbc) Thanks [@hsusul](https://github.com/hsusul)! - Generated tool command files now carry valid YAML frontmatter for every supported tool. Command names ship as `OPSX: Explore`, and the unquoted `name: OPSX: Explore` that adapters emitted is not parseable YAML — strict parsers rejected the whole file, so the command failed to load. Several adapters also re-implemented their own escaping, and a few interpolated descriptions in raw.
Escaping now lives in one place (`escapeYamlValue` / `formatTagsArray`) and every adapter uses it. String frontmatter values are always double-quoted, which also keeps values like `true`, `null` and `123` from round-tripping as booleans, nulls and numbers. Non-string fields such as `allowed-tools` and `invokable` are unchanged. Expect the first `openspec update` after upgrading to rewrite the frontmatter lines of your generated command files.
Archive workflow guidance also gets two corrections: bulk archive now carries its per-delta include/exclude decisions into execution, so a delta whose implementation was not found is reported as `sync skipped` instead of being synced anyway, and both archive workflows verify the main specs before moving the change directory.
- [#1471](https://github.com/Fission-AI/OpenSpec/pull/1471) [`9a937cb`](https://github.com/Fission-AI/OpenSpec/commit/9a937cb9b36fb1040bdbde3bab3fa3903944ef10) Thanks [@clay-good](https://github.com/clay-good)! - Reference slash commands by the name each tool actually registers. Command bodies, generated `SKILL.md` cross-references, and the `init`/`update`/migration hints all advertised `/opsx:<id>`, but only 7 of the 28 tools with a command adapter register that name — the ones whose files sit in an `opsx/` directory. The other 21 write `.../opsx-<id>.md`, where the filename is the command, so tools such as Cursor, GitHub Copilot, Windsurf and Kilo Code were told to type a command their palette never had; a single generated Cursor file named itself `/opsx-apply` in frontmatter and then told the reader to run `/opsx:apply`. The command _name_ is now derived from the command file each adapter writes rather than a hand-maintained tool list, so a newly added adapter cannot drift, and the _wrapper_ around it is adapter metadata: Amazon Q loads its files into a prompt library invoked with `@`, so it now gets `@opsx-<id>` in command bodies, skills, and the onboarding hint instead of a slash command it never registers. Codex, which generates no command files at all, now gets `$openspec-<skill>` — the syntax its CLI actually accepts — everywhere it previously advertised `/opsx:*`, superseding the syntax-neutral hint described in the pending `adapterless-skill-references` note. Command filenames and paths are unchanged, and Claude Code output is byte-identical.
- [#1364](https://github.com/Fission-AI/OpenSpec/pull/1364) [`f58b445`](https://github.com/Fission-AI/OpenSpec/commit/f58b4456925b6331f3e5902a1c57905afe7edbf5) Thanks [@clay-good](https://github.com/clay-good)! - Fix `openspec completion install` detecting the wrong shell for fish (and other)
users whose interactive shell differs from their login shell. Detection now
consults the parent process before falling back to `$SHELL`, so running the
command from fish installs fish completions instead of defaulting to bash.
- Config `rules:` keys are no longer reported as `Unknown artifact ID` when they belong to a different schema. The global rules map is now validated against the union of artifact IDs across every available schema, so multi-schema projects stop seeing spurious warnings on every command ([#1322](https://github.com/Fission-AI/OpenSpec/issues/1322)).
- [#1401](https://github.com/Fission-AI/OpenSpec/pull/1401) [`b33b15d`](https://github.com/Fission-AI/OpenSpec/commit/b33b15d98ae929624c991632c7382ebc234d4ca7) Thanks [@clay-good](https://github.com/clay-good)! - Stop `design.md` from restating the proposal. In the default `spec-driven` schema, the design instruction asked for "Background, current state, constraints, stakeholders" and "What this design achieves and excludes" without saying that motivation and scope already live in `proposal.md`, so agents restated the proposal's Why and What Changes instead of adding the design's own value - approach, alternatives, and trade-offs. The instruction and the design template now state the boundary explicitly (the proposal covers why and what, design covers how) and tell the agent to reference those documents rather than repeat them ([#1382](https://github.com/Fission-AI/OpenSpec/issues/1382)).
- [#1167](https://github.com/Fission-AI/OpenSpec/pull/1167) [`1637856`](https://github.com/Fission-AI/OpenSpec/commit/1637856c423f2e84457652d1ab58885fe9744fb2) Thanks [@mehdishahdoost](https://github.com/mehdishahdoost)! - **Windsurf is now Devin Desktop.** Windsurf was rebranded on June 2, 2026 and its config directory moved: `.devin/` is the preferred read + write location, `.windsurf/` a legacy read-only fallback that the Devin Local agent does not read at all. OpenSpec follows the rename rather than carrying two ids for one product — the tool id is `devin`, writing `.devin/workflows/opsx-<id>.md` and `.devin/skills/openspec-*/SKILL.md`, and it is detected from either directory.
- `--tools windsurf` still resolves, so existing setup scripts keep working; it now configures `.devin/`.
- If your OpenSpec files are still in `.windsurf/`, `openspec update` explains the rebrand and offers to move them. `--force` and non-interactive runs take the move; declining leaves every file exactly where it is. Only the files OpenSpec generates move — each skill's `SKILL.md` and commands named `opsx-*`. A hand-written Cascade workflow, a reference file you keep beside a `SKILL.md`, a command file you edited, and `.devin/rules/` all stay exactly where they are.
- Devin skills and the getting-started hint reference `/openspec-*` skills rather than `/opsx-*` workflows, because only Devin Desktop reads workflows; the `/openspec-*` form works on both agents. Workflow bodies still use `/opsx-<id>`, the name Devin registers for a workflow file.
- [#1475](https://github.com/Fission-AI/OpenSpec/pull/1475) [`17af60c`](https://github.com/Fission-AI/OpenSpec/commit/17af60c66e4c049e3986fdbafcdc16b202cda59f) Thanks [@clay-good](https://github.com/clay-good)! - `openspec doctor` now notes when a store checkout is behind its upstream ref.
- [#1475](https://github.com/Fission-AI/OpenSpec/pull/1475) [`17af60c`](https://github.com/Fission-AI/OpenSpec/commit/17af60c66e4c049e3986fdbafcdc16b202cda59f) Thanks [@clay-good](https://github.com/clay-good)! - Make the archive scenario-drift check multiplicity-aware: a MODIFIED block that keeps only one of two same-named scenarios no longer silently drops the other.
- [#1408](https://github.com/Fission-AI/OpenSpec/pull/1408) [`378d468`](https://github.com/Fission-AI/OpenSpec/commit/378d468ad348dc1e973ed30c5cfa458fb77c9de3) Thanks [@clay-good](https://github.com/clay-good)! - Explore now reads the project's context and rules from `openspec/config.yaml` (or `config.yml`) at the start of a session, so it reasons with the same tech stack and conventions the artifact-creating workflows already receive.
- [#1475](https://github.com/Fission-AI/OpenSpec/pull/1475) [`17af60c`](https://github.com/Fission-AI/OpenSpec/commit/17af60c66e4c049e3986fdbafcdc16b202cda59f) Thanks [@clay-good](https://github.com/clay-good)! - `openspec feedback` shows the formatted text and a pre-filled submission URL on any gh failure (issues disabled, network, rate limit), not only when gh is missing or unauthenticated.
- [#1396](https://github.com/Fission-AI/OpenSpec/pull/1396) [`60f720c`](https://github.com/Fission-AI/OpenSpec/commit/60f720c43acd94de7645ac8629c614ede4682b6a) Thanks [@clay-good](https://github.com/clay-good)! - Fix `openspec feedback` failing when the repository does not define the `feedback` label. The command now retries without the label and notes that it was not applied, instead of exiting with an error and discarding the feedback.
- Ignore Markdown structure (requirement headers, delta sections, scenarios, REMOVED/RENAMED entries) that appears inside fenced code blocks when parsing delta specs. Previously a fenced `### Requirement:` example was parsed as a real (phantom) requirement, producing spurious `validate` errors and risking incorrect `archive` output. Fenced-code detection is now shared across the Markdown parsers so `validate` and `archive` behave consistently.
- [#1475](https://github.com/Fission-AI/OpenSpec/pull/1475) [`17af60c`](https://github.com/Fission-AI/OpenSpec/commit/17af60c66e4c049e3986fdbafcdc16b202cda59f) Thanks [@clay-good](https://github.com/clay-good)! - The archive scenario-drift check now ignores `#### Scenario:` lines inside fenced code blocks, matching validate: a fenced example no longer false-aborts an archive, and a fenced name no longer masks a genuinely dropped scenario.
- **`archive` no longer stacks a second date prefix** — archiving a change whose name already starts with a `YYYY-MM-DD-` prefix (a common authoring convention) keeps the name as-is instead of prepending today's date. Previously `openspec archive 2026-07-04-voice-copilot-v1 --yes` produced `2026-07-06-2026-07-04-voice-copilot-v1`, and when run on a later day the folder sorted under a day on which the change did not happen. Names without a full date prefix (including partial dates like `2026-07-feature`) are dated as before, and the naming is now idempotent.
- [#1374](https://github.com/Fission-AI/OpenSpec/pull/1374) [`da3907b`](https://github.com/Fission-AI/OpenSpec/commit/da3907b8a9170711c8b7f63e18352e8577cf7df5) Thanks [@clay-good](https://github.com/clay-good)! - fix(completion): make the PowerShell completion script parse and load again
The generated `OpenSpecCompletion.ps1` contained 18 empty `switch ($positionalIndex) { }` blocks — emitted for commands whose positionals are all `path`-typed (PowerShell completes paths natively, so those cases produce no clauses). A switch with no clauses is a PowerShell parse error ("Missing condition in switch statement clause"), and PowerShell parses the whole file before running it, so the script never loaded and completions never registered. The generator now skips the positional-index block entirely when no positional produces completions, so the script parses clean (18 → 0 errors) and tab completion works.
- **Archive workflow templates no longer teach agents to stack a second date prefix** — the `openspec-archive-change` and `openspec-bulk-archive-change` skill/command templates (and the onboarding walkthrough's archived-path example) now mirror the `openspec archive` rule: a change whose name already starts with a `YYYY-MM-DD-` prefix is archived under its own name, while other names get the current date prepended as before. Previously an agent following the workflow instructions on a change named `2026-07-04-voice-copilot-v1` produced `archive/2026-07-07-2026-07-04-voice-copilot-v1`, whatever the CLI did.
- [#1475](https://github.com/Fission-AI/OpenSpec/pull/1475) [`17af60c`](https://github.com/Fission-AI/OpenSpec/commit/17af60c66e4c049e3986fdbafcdc16b202cda59f) Thanks [@clay-good](https://github.com/clay-good)! - Gemini command files escape TOML-active characters (quotes, backslashes, control characters) in the description and prompt, so a template value containing them can no longer produce an invalid `.toml` file.
- [#1464](https://github.com/Fission-AI/OpenSpec/pull/1464) [`5bcf057`](https://github.com/Fission-AI/OpenSpec/commit/5bcf05766a70ec0163c3e700a3029b1c1da895d8) Thanks [@clay-good](https://github.com/clay-good)! - Workflow skills and commands no longer tell agents to use the Claude Code-only AskUserQuestion tool. The same templates are generated for every supported tool, and agents without that tool (OpenCode, Factory Droid, Codex, and others) errored or stalled on the instruction. The guidance is now runtime-neutral: agents are simply told to ask the user.
- **Propose and fast-forward skills no longer name the Claude-only TodoWrite tool** — the generated `openspec-propose` and `openspec-ff-change` skills (and their `/opsx:propose` / `/opsx:ff` commands) told every agent to "Use the **TodoWrite tool**", which only exists in Claude Code. Codex, Cursor, Gemini, Copilot, and the other supported tools have no such tool, so agents either errored or stalled looking for it. The instruction is now runtime-neutral ("Use a todo list to track progress"), which works everywhere — including Claude Code.
- [#1415](https://github.com/Fission-AI/OpenSpec/pull/1415) [`e2f748c`](https://github.com/Fission-AI/OpenSpec/commit/e2f748c64f05efaeac720f83c71fb6f1b6f6e18d) Thanks [@clay-good](https://github.com/clay-good)! - Reject config key paths that reach the prototype chain, and update the bundled `yaml` dependency.
`openspec config set --allow-unknown __proto__.polluted <value>` reported success and assigned onto `Object.prototype` for the rest of the process. `--allow-unknown` was meant to relax the known-key check only, but it skipped every key check, so `__proto__`, `constructor`, and `prototype` segments reached the nested-write helper. Those segments are now rejected in `config set` whether or not `--allow-unknown` is passed, and `setNestedValue` / `deleteNestedValue` refuse them regardless of caller. Ordinary keys such as `featureFlags.myFlag` behave exactly as before.
The `yaml` runtime dependency moves from 2.8.2 to 2.9.0, picking up the fix for a stack overflow on deeply nested input (GHSA / advisory patched in 2.8.3).
- **Archive after early sync** — `openspec archive` no longer fails with `ADDED failed … already exists` when a change's specs were already synced to the main specs before archiving (the early-sync pattern from the `sync` workflow). If an ADDED requirement already exists in the target spec with identical content, applying it is treated as a no-op; a same-named requirement with different content still aborts the archive as a genuine conflict ([#1332](https://github.com/Fission-AI/OpenSpec/issues/1332)).
- **Archive after early sync (RENAMED)** — `openspec archive` no longer fails with `RENAMED failed … source not found` when a change's renames were already synced to the main specs before archiving (the early-sync pattern from the `sync` workflow). If a RENAMED requirement's source header is gone but the target header exists in the spec, applying the rename is treated as a no-op; a rename whose source and target are both missing still aborts the archive as a genuine error, and reported counts reflect only renames actually applied.
- [#1462](https://github.com/Fission-AI/OpenSpec/pull/1462) [`ebf66c7`](https://github.com/Fission-AI/OpenSpec/commit/ebf66c7ee1df3f7465d7f480753f952483133a73) Thanks [@clay-good](https://github.com/clay-good)! - Respect reduced-motion preferences in `openspec init`: the welcome animation is skipped when the OS reduced-motion setting is on (macOS Reduce Motion, GNOME animations disabled), when `OPENSPEC_NO_ANIMATION` is set, or when the new `--no-animation` flag is passed. The static welcome screen is shown instead.
- **Custom schema instructions are no longer overridden by hard-coded spec-driven patterns** — the `openspec-continue-change` skill/command embedded one-line "common artifact patterns" for proposal.md, specs, design.md, and tasks.md, so agents followed those shortcuts instead of the schema's `instruction` field whenever a custom schema reused familiar artifact names. The templates now state that the `instruction` field is the authoritative guidance, and the `propose`, `continue`, and `ff` workflows direct the agent — both in the artifact-creation step and in the guidelines — to invoke a skill when the instruction delegates artifact creation to one, verifying the artifact exists afterward (fixes [#777](https://github.com/Fission-AI/OpenSpec/issues/777)).
- [#1475](https://github.com/Fission-AI/OpenSpec/pull/1475) [`17af60c`](https://github.com/Fission-AI/OpenSpec/commit/17af60c66e4c049e3986fdbafcdc16b202cda59f) Thanks [@clay-good](https://github.com/clay-good)! - Follow the Kimi CLI rename to Kimi Code: new install paths with automatic migration of existing `.kimi` setups.
- [#1415](https://github.com/Fission-AI/OpenSpec/pull/1415) [`e2f748c`](https://github.com/Fission-AI/OpenSpec/commit/e2f748c64f05efaeac720f83c71fb6f1b6f6e18d) Thanks [@clay-good](https://github.com/clay-good)! - Parse spec headings in linear time when the title is padded with whitespace.
Building the reference index read the first Purpose line with a regex that backtracked quadratically on a heading full of spaces: 10,000 characters of padding took 60ms, and 100,000 would have taken roughly six seconds. The heading scan is now hand-rolled and linear. Behavior is unchanged — the replacement was checked against the old implementation across 303,000 generated inputs, including CommonMark closing sequences (`## Purpose ##`), seven-hash lines, and headings with no space after the hashes.
- [#1475](https://github.com/Fission-AI/OpenSpec/pull/1475) [`17af60c`](https://github.com/Fission-AI/OpenSpec/commit/17af60c66e4c049e3986fdbafcdc16b202cda59f) Thanks [@clay-good](https://github.com/clay-good)! - Use local dates for CLI date-only values (archive names, timestamps) instead of UTC, so late-evening archives no longer get tomorrow's date.
- [#1475](https://github.com/Fission-AI/OpenSpec/pull/1475) [`17af60c`](https://github.com/Fission-AI/OpenSpec/commit/17af60c66e4c049e3986fdbafcdc16b202cda59f) Thanks [@clay-good](https://github.com/clay-good)! - `openspec update` warns when a custom profile is missing core workflows instead of silently generating a partial install.
- [#1428](https://github.com/Fission-AI/OpenSpec/pull/1428) [`81d5109`](https://github.com/Fission-AI/OpenSpec/commit/81d5109b86f16537deb99f84a772a83235dc9e09) Thanks [@taltas](https://github.com/taltas)! - Update current Roo Code product references to its community successor, Zoo Code.
- [#1475](https://github.com/Fission-AI/OpenSpec/pull/1475) [`17af60c`](https://github.com/Fission-AI/OpenSpec/commit/17af60c66e4c049e3986fdbafcdc16b202cda59f) Thanks [@clay-good](https://github.com/clay-good)! - Archive treats a MODIFIED delta whose content already matches the main spec as a no-op: a fully early-synced change now reports "Specs already in sync" instead of rewriting the file and claiming modifications.
- [#1475](https://github.com/Fission-AI/OpenSpec/pull/1475) [`17af60c`](https://github.com/Fission-AI/OpenSpec/commit/17af60c66e4c049e3986fdbafcdc16b202cda59f) Thanks [@clay-good](https://github.com/clay-good)! - Render multi-select prompts with `[x]`/`[ ]` checkbox markers instead of radio-button icons.
- [#1475](https://github.com/Fission-AI/OpenSpec/pull/1475) [`17af60c`](https://github.com/Fission-AI/OpenSpec/commit/17af60c66e4c049e3986fdbafcdc16b202cda59f) Thanks [@clay-good](https://github.com/clay-good)! - Discover nested spec paths like `specs/<area>/<capability>/spec.md` recursively and consistently across parse, apply, and archive.
- [#1410](https://github.com/Fission-AI/OpenSpec/pull/1410) [`b3b05e1`](https://github.com/Fission-AI/OpenSpec/commit/b3b05e1abeb312caefd57e60be799aeb466c1d0e) Thanks [@clay-good](https://github.com/clay-good)! - Only advertise onboarding commands that will actually exist. The `openspec init` welcome screen and the `openspec update` "Getting started" summary listed `/opsx:new` and `/opsx:continue`, which the default `core` profile never generates, so users were told to run commands that did not exist. Both surfaces now list the commands for the installed workflows. The `init` and `update` completion hints also name the skill (`/openspec-propose`) instead of a command for tools that receive no command files — Codex, and any tool under skills-only delivery.
- **`/opsx:propose` and `/opsx:ff` no longer finish a change with no spec written.** The workflows listed only `proposal`/`design`/`tasks` and treated the apply phase's `tasks` artifact as the stop condition — but `status` marks an artifact `done` as soon as a matching file exists, so writing `tasks.md` early satisfied the loop while `specs/<capability>/spec.md` was never created (a spec-less change in a spec-driven tool). The loop now derives the full required set — every apply dependency plus everything it transitively `requires` — from a single `status` call, creates each missing artifact, and only skips one when its own `instruction` field marks it conditional. ([#1260](https://github.com/Fission-AI/OpenSpec/issues/1260), [#788](https://github.com/Fission-AI/OpenSpec/issues/788))
### Changed
- **`openspec status --json` now reports each artifact's `requires` edges.** Every entry in the `artifacts` array carries a `requires` array of the ids it directly depends on, present for every status (including `done`) so agents can compute the transitive required set from `status` alone. Additive and backward-compatible — existing fields are unchanged.
- [#1191](https://github.com/Fission-AI/OpenSpec/pull/1191) [`7704702`](https://github.com/Fission-AI/OpenSpec/commit/7704702d61fa71e4f553c21a06bdf8e4ee803b4a) Thanks [@mc856](https://github.com/mc856)! - Generate Markdown commands for Qwen Code instead of deprecated TOML format. Qwen Code now recommends Markdown custom commands with YAML frontmatter; the old `.qwen/commands/opsx-*.toml` files are cleaned up as legacy artifacts on update.
- [#1475](https://github.com/Fission-AI/OpenSpec/pull/1475) [`17af60c`](https://github.com/Fission-AI/OpenSpec/commit/17af60c66e4c049e3986fdbafcdc16b202cda59f) Thanks [@clay-good](https://github.com/clay-good)! - An already-synced RENAMED delta aborts when a case/whitespace variant of the source requirement still exists — the same typo guard REMOVED deltas have.
- **Regenerated artifacts now pick up your manual edits** — the continue, propose, and fast-forward workflows (and the `openspec instructions` dependency block) now tell the agent to re-read dependency artifacts from disk before creating the next one, instead of trusting whatever version it saw earlier in the conversation. Previously, editing `spec.md` and deleting `design.md`/`tasks.md` to regenerate them could silently produce artifacts based on the stale, pre-edit content.
- [#1475](https://github.com/Fission-AI/OpenSpec/pull/1475) [`17af60c`](https://github.com/Fission-AI/OpenSpec/commit/17af60c66e4c049e3986fdbafcdc16b202cda59f) Thanks [@clay-good](https://github.com/clay-good)! - Proposal guidance now resolves blocking open questions with the user instead of deferring them to design.md.
- Stop a delta spec written directly at a change's `specs/` root from being silently dropped. `validate` accepted `specs/spec.md` and counted its deltas, but the apply/archive merge only reads capability folders (`specs/<capability>/spec.md`), so the change could pass validation and be archived while its requirements never reached `openspec/specs/`. `validate` now uses the same discovery rules as the merge path and reports the misplaced file with a fix hint, and `archive` blocks instead of completing.
- [#1465](https://github.com/Fission-AI/OpenSpec/pull/1465) [`f917b8b`](https://github.com/Fission-AI/OpenSpec/commit/f917b8be5e1100189ef62320ba9322763053640e) Thanks [@clay-good](https://github.com/clay-good)! - Order artifacts by the schema's declaration order instead of alphabetically.
`specs` and `design` both require only `proposal`, so both become ready at once - and the tie used to be broken alphabetically, which put `design` first. `openspec status` listed design above specs and `nextSteps` recommended writing `design.md` before any spec existed, contradicting the spec-driven schema's own documented `proposal → specs → design → tasks` sequence.
Ties now follow the order the schema declares its artifacts, so `openspec status`, `status --json`, `nextSteps`, `blocked by:` lists, and an artifact's `unlocks` all agree. No dependency edges changed, so nothing newly blocks and `design.md` stays optional - only the order of equally-ready artifacts moved. Custom schemas get the same guarantee: dependency order still comes first, but wherever your schema leaves two artifacts equally ready, the order of its `artifacts:` list now decides which one the CLI recommends - so reorder that list if it was never deliberate.
- Preserve an existing project-local schema when `openspec schema init --force` rejects an unknown artifact ID. Forced replacement now begins only after artifact validation succeeds.
- [#1433](https://github.com/Fission-AI/OpenSpec/pull/1433) [`26f009d`](https://github.com/Fission-AI/OpenSpec/commit/26f009d940f311b99db7f310816bb166a99fb3ef) Thanks [@clay-good](https://github.com/clay-good)! - Change lookup no longer requires `proposal.md`. `openspec show`, `openspec change list/show/validate`, and shell completion now resolve a change by its directory, matching `openspec list`, `status`, `instructions`, and `validate`.
Previously a change created by `openspec new change` — which scaffolds only `.openspec.yaml` — was reported as `Unknown item` by `openspec show` and was missing from completions and `openspec change list` until a proposal was written, and a change from a schema with no proposal artifact was never resolvable. `openspec change list` now reports the same set as `openspec list`, keeps task counts for a change that has no proposal yet, and labels it `(no proposal.md yet)` rather than `(unable to read)`. Showing such a change explains that the proposal is not written yet and points at `openspec status --change <name>`.
- [#1468](https://github.com/Fission-AI/OpenSpec/pull/1468) [`fc886af`](https://github.com/Fission-AI/OpenSpec/commit/fc886af7f93068482bbf2c66fd1eb76b40c6a22f) Thanks [@clay-good](https://github.com/clay-good)! - The continue, update, verify, sync, and archive workflow skills now select a change the same way apply does: use the provided name, infer it from conversation context, auto-select when exactly one active change exists, and only prompt when the choice is genuinely ambiguous. Previously these workflows were told to always prompt ("Do NOT guess or auto-select"), so invoking them with a single active change stalled on a question with only one possible answer. The selection is always announced ("Using change: <name>") with how to override, and bulk archive still always prompts.
- [#1194](https://github.com/Fission-AI/OpenSpec/pull/1194) [`b7c85c7`](https://github.com/Fission-AI/OpenSpec/commit/b7c85c741ca56748a4ae095b573fe4550c5c977f) Thanks [@mc856](https://github.com/mc856)! - Fix skills-only delivery emitting `/opsx:*` command references. SKILL.md files generated by init, update, and workspace skill setup now reference the corresponding skills (e.g. `/openspec-apply-change`) when `delivery: 'skills'` is configured, instead of commands that were never generated.
- [#1475](https://github.com/Fission-AI/OpenSpec/pull/1475) [`17af60c`](https://github.com/Fission-AI/OpenSpec/commit/17af60c66e4c049e3986fdbafcdc16b202cda59f) Thanks [@clay-good](https://github.com/clay-good)! - Specs instructions include the spec content guidance from the concepts docs, so generated specs follow the requirement/scenario format.
- [#1475](https://github.com/Fission-AI/OpenSpec/pull/1475) [`17af60c`](https://github.com/Fission-AI/OpenSpec/commit/17af60c66e4c049e3986fdbafcdc16b202cda59f) Thanks [@clay-good](https://github.com/clay-good)! - The static welcome screen (reduced motion, `--no-animation`, narrow terminals) now waits for the Enter it asks for instead of letting the keystroke submit the tool picker unseen.
- [#1475](https://github.com/Fission-AI/OpenSpec/pull/1475) [`17af60c`](https://github.com/Fission-AI/OpenSpec/commit/17af60c66e4c049e3986fdbafcdc16b202cda59f) Thanks [@clay-good](https://github.com/clay-good)! - Sync and archive workflows resolve main specs through the store-aware root instead of assuming `openspec/specs` in the repo.
- [#1402](https://github.com/Fission-AI/OpenSpec/pull/1402) [`0da5f98`](https://github.com/Fission-AI/OpenSpec/commit/0da5f98e147543a44379e32295e2e9798d775d83) Thanks [@clay-good](https://github.com/clay-good)! - Show the main spec format in the sync-specs skill so agents stop leaving delta operation headers (`## ADDED/MODIFIED Requirements`) in `openspec/specs/` — merged main specs with those headers parse as 0 requirements in `openspec view` ([#1120](https://github.com/Fission-AI/OpenSpec/issues/1120)).
- [#1476](https://github.com/Fission-AI/OpenSpec/pull/1476) [`8731290`](https://github.com/Fission-AI/OpenSpec/commit/87312900f532c6c13ea556d4badaff2efdfa9602) Thanks [@clay-good](https://github.com/clay-good)! - Telemetry no longer depends on `posthog-node`: the single usage event is sent with a plain fetch to the same endpoint. Installing OpenSpec no longer pulls the fast-publishing `posthog-node`/`@posthog/core`/`@posthog/types` tree, which broke downstream installs under supply-chain age policies like pnpm's `minimumReleaseAge` ([#1390](https://github.com/Fission-AI/OpenSpec/issues/1390)).
- [#1475](https://github.com/Fission-AI/OpenSpec/pull/1475) [`17af60c`](https://github.com/Fission-AI/OpenSpec/commit/17af60c66e4c049e3986fdbafcdc16b202cda59f) Thanks [@clay-good](https://github.com/clay-good)! - The stale-CLI check hardens its install detection: a directory merely named `volta` no longer changes the upgrade hint, the Windows npm-ownership check corroborates against the `openspec.cmd` shim npm actually writes, and a registry redirect from https to plain http is no longer followed.
- [#1475](https://github.com/Fission-AI/OpenSpec/pull/1475) [`17af60c`](https://github.com/Fission-AI/OpenSpec/commit/17af60c66e4c049e3986fdbafcdc16b202cda59f) Thanks [@clay-good](https://github.com/clay-good)! - The stale-CLI check tears down a redirected registry connection when its time budget expires instead of leaving the socket open.
- [#1442](https://github.com/Fission-AI/OpenSpec/pull/1442) [`10fa39b`](https://github.com/Fission-AI/OpenSpec/commit/10fa39b1c3a3e88c02ae7d3053864c03a793ff47) Thanks [@hsusul](https://github.com/hsusul)! - `openspec update` now refreshes tools that are configured with command files but no skills (delivery `commands`). Previously it read the generating version only from skill files, so such a tool was reported as "up to date" forever and its command files were never regenerated after a CLI upgrade. Command files carry no version stamp, so OpenSpec compares their contents against what it would generate now — including removing a command file left behind by a workflow you have since deselected. CRLF line endings and a UTF-8 BOM are treated as checkout artifacts rather than drift, so a Windows clone does not report a spurious update.
- [#1475](https://github.com/Fission-AI/OpenSpec/pull/1475) [`17af60c`](https://github.com/Fission-AI/OpenSpec/commit/17af60c66e4c049e3986fdbafcdc16b202cda59f) Thanks [@clay-good](https://github.com/clay-good)! - `openspec update` with `delivery: commands` prints the same configuration correction as init when it removes the skills of a tool that supports only skills, instead of deleting them silently.
- [#1475](https://github.com/Fission-AI/OpenSpec/pull/1475) [`17af60c`](https://github.com/Fission-AI/OpenSpec/commit/17af60c66e4c049e3986fdbafcdc16b202cda59f) Thanks [@clay-good](https://github.com/clay-good)! - `openspec validate` reports an unreadable specs/ directory as the error it is instead of misdiagnosing it as "no deltas found".
- [#1455](https://github.com/Fission-AI/OpenSpec/pull/1455) [`6b3623a`](https://github.com/Fission-AI/OpenSpec/commit/6b3623a39e96f49995d38d642738b31f68e92039) Thanks [@c4patino](https://github.com/c4patino)! - `openspec view` now resolves the configured OpenSpec root instead of always reading the current directory, and accepts `--store <id>` like its sibling commands. Projects whose `openspec/config.yaml` points at an external store saw an empty dashboard — 0 specs, 0 requirements — while `openspec list` read the same store correctly.
- [#1475](https://github.com/Fission-AI/OpenSpec/pull/1475) [`17af60c`](https://github.com/Fission-AI/OpenSpec/commit/17af60c66e4c049e3986fdbafcdc16b202cda59f) Thanks [@clay-good](https://github.com/clay-good)! - Preserve keyboard input on Windows after the welcome screen instead of dropping the first keystrokes.
- [#1475](https://github.com/Fission-AI/OpenSpec/pull/1475) [`17af60c`](https://github.com/Fission-AI/OpenSpec/commit/17af60c66e4c049e3986fdbafcdc16b202cda59f) Thanks [@clay-good](https://github.com/clay-good)! - zsh completion install honors `$ZSH` and `$ZSH_CUSTOM`, so Oh My Zsh setups at custom locations get the completion where their shell actually loads it.
## 1.6.0
### Minor Changes
- [#1090](https://github.com/Fission-AI/OpenSpec/pull/1090) [`3f0ca3f`](https://github.com/Fission-AI/OpenSpec/commit/3f0ca3f6ce6f2ec41260c5cbe7954b7e46adcf43) Thanks [@jjxyxsjr](https://github.com/jjxyxsjr)! - ### New Features
- **TRAE command adapter** — Added command adapter for Trae IDE, enabling generation of `.trae/commands/opsx-<id>.md` files for custom slash commands
- [#1340](https://github.com/Fission-AI/OpenSpec/pull/1340) [`1552731`](https://github.com/Fission-AI/OpenSpec/commit/15527310f9be13cc9a4035ea01b93ba85873d956) Thanks [@TabishB](https://github.com/TabishB)! - ### New Features
- **Oh My Pi support** — Generate native OPSX commands and skills for Oh My Pi projects, including tool detection and the expected `.omp` directory layout.
- **Update planning artifacts in place** — Use `/opsx:update` to revise an existing change's planning artifacts, reconcile related artifacts, and keep implementation work delegated to `/opsx:apply`.
### Bug Fixes
- **Fresh store registration** — Register and use newly created stores before their empty changes, specs, or archive directories have been committed.
- **Safer requirement archiving** — Stop stale `MODIFIED` requirements from silently deleting scenarios that were added by an earlier archive.
### Patch Changes
- [#1300](https://github.com/Fission-AI/OpenSpec/pull/1300) [`a5bfeda`](https://github.com/Fission-AI/OpenSpec/commit/a5bfedafc8b3d914fe01d05eb36ad9ad3fbe35a2) Thanks [@clay-good](https://github.com/clay-good)! - ### Features
- **Auto-approve the OpenSpec CLI in generated skills and commands** — every generated `SKILL.md` (all tools) and every Claude Code `/opsx:*` slash command now carries `allowed-tools: Bash(openspec:*)` in its frontmatter, so agents that honor the Agent Skills standard run `openspec` commands without prompting for approval on each call; tools that don't recognize the field ignore it. Scope is limited to the `openspec` CLI; because `allowed-tools` pre-approves rather than restricts, every other tool a skill or command uses stays available under your normal permission settings.
- **`archive` exits non-zero when blocked in human mode** — `openspec archive <change> -y` (and any non-`--json` invocation) no longer returns exit code 0 when validation fails and nothing is archived. The three blocking paths in human mode — delta-spec validation failure, spec rebuild failure, and rebuilt-spec validation failure — now set `process.exitCode = 1`, matching the existing `--json` behavior. Previously the command printed "Validation failed" (or "Aborted. No files were changed.") and exited 0, letting scripts and CI believe the archive succeeded. Aligns `archive` with the same exit-code guarantee already approved for `apply` instructions (#1250).
- **`validate` resolves changes like `status`** — `openspec validate <change>` (and `--all`/`--changes` and the interactive selector) now resolves a change by directory existence, matching `status`/`instructions`, instead of requiring `proposal.md`. A scaffolded or still-authoring change is validated rather than reported as `Unknown item`, and a resolved-but-invalid change now exits non-zero. Delta discovery also recurses the nested `specs/<area>/<capability>/spec.md` layout. (#1182)
- **Task progress reads nested/glob `tasks.md`** — `openspec view`, `list`, and the `archive` incomplete-task gate now resolve task progress through the tracked-tasks artifact's `generates` glob (the same file-resolution `status` uses), so a change whose tasks live in nested `tasks.md` files is classified correctly and can no longer archive while unfinished. (#1202)
- **SHALL/MUST body-keyword hint applies to main specs** — A main-spec requirement whose normative keyword sits only in the `### Requirement:` header now receives the same targeted "move it to the body line" remediation as a change delta, emitted exactly once. (#1156)
- **Requirement reading fidelity** — The requirement reader used by `validate <change>`, `validate <spec>`, and `archive` is now unified into one fence-, metadata-, and multi-line-aware extraction, closing the known divergences between the change-delta path and the main-spec path (the remaining ones are documented in the change's design doc):
- A `SHALL`/`MUST` keyword that wraps onto a later body line is detected instead of dropped (#361).
- Metadata lines (`**ID**:`, `**Priority**:`) before the description are skipped on the spec path, matching the change path (#418). A requirement written entirely as metadata (e.g. `**Constraint**: The system MUST ...`) keeps that line as its text instead of being emptied.
- A fenced code block before the prose line no longer becomes the requirement text (#312).
- A `#### Scenario:` inside a fenced example no longer counts as a real scenario in `validate <change>`, matching `validate <spec>`.
- `SHALL`/`MUST` detection uses one whole-word predicate across all readers, and a requirement with no body text falls back to its header title on both paths.
Displayed requirement text (e.g. in JSON output and delta descriptions) now reflects the full requirement body rather than only its first line. Archived spec content is unchanged — the archive rebuild reads raw `### Requirement:` blocks, not the parsed text.
- **Surface non-canonical delta headers** — `validate <change>` now emits an INFO note when an `## ADDED`/`## MODIFIED Requirements` section contains a level-3 header that is not a canonical `### Requirement:` header (one the delta reader silently skips, such as a stray `### Documentation Requirements` divider). The note never changes the `valid` result, including under `--strict` (#498).
## 1.5.0
### Minor Changes
- [#1267](https://github.com/Fission-AI/OpenSpec/pull/1267) [`96f6cac`](https://github.com/Fission-AI/OpenSpec/commit/96f6cacb206c65bee30066f6a1f4e9b855a0d783) Thanks [@TabishB](https://github.com/TabishB)! - ### New Features
- **Stores (very early beta)** — Introduces stores as a simpler way to organize specs and changes, replacing the workspace and initiative model. This feature is in very early beta — expect rough edges and breaking changes in upcoming releases.
### Bug Fixes
- **Config parsing** — Configuration values wrapped in JSON containers are now parsed correctly.
`escapeYamlValue` flagged `\r` as a character requiring quoting but never escaped it, leaving a literal carriage return inside the double-quoted scalar where YAML line folding/normalization could silently corrupt the value (realistic with CRLF-authored command descriptions). Carriage returns are now escaped as `\r`. The helper — previously duplicated verbatim across five adapters (bob, claude, cursor, pi, windsurf) — is extracted into a shared `command-generation/yaml.ts` module so the behavior stays consistent and is fixed in one place.
## 1.4.1
### Patch Changes
- [#1165](https://github.com/Fission-AI/OpenSpec/pull/1165) [`0a01146`](https://github.com/Fission-AI/OpenSpec/commit/0a01146c181a3af8dbf645547bcbe20c0d48d615) Thanks [@TabishB](https://github.com/TabishB)! - Move beta workspace view state to `.openspec-workspace/view.yaml`, stop top-level `openspec update` from routing into workspace updates, and ignore foreign root `workspace.yaml` files so Dagster projects keep updating normally.
## 1.4.0
### Minor Changes
- [#1003](https://github.com/Fission-AI/OpenSpec/pull/1003) [`342ed43`](https://github.com/Fission-AI/OpenSpec/commit/342ed43e694abba65a3ea275f94ba3b77df85da3) Thanks [@Miss-you](https://github.com/Miss-you)! - ### New Features
- **Kimi CLI support** — OpenSpec can now initialize Kimi CLI as a supported skills-only tool using `.kimi/skills/`
### Other
- Added Kimi-specific docs and init coverage aligned with skill-based `/skill:openspec-*` usage
- [#1154](https://github.com/Fission-AI/OpenSpec/pull/1154) [`aa16080`](https://github.com/Fission-AI/OpenSpec/commit/aa16080d16b70f7b26cebd465334b2e16c0e7a43) Thanks [@TabishB](https://github.com/TabishB)! - ### New Features
- **Mistral Vibe support** — OpenSpec can now initialize Mistral Vibe as a supported skills-only tool using `.vibe/skills/`
### Bug Fixes
- **Case-insensitive requirement headers** — Requirement headers are now parsed regardless of capitalization, so specs no longer fail to parse over header casing
- **Zsh completions on oh-my-zsh** — Fixed shell completion setup so tab completion installs correctly under oh-my-zsh's `compinit`
### Other
- **Clearer validation hints** — When a requirement has SHALL/MUST only in its header, `openspec validate` now points you to move the keyword onto the requirement body line instead of showing the generic error
- [#1030](https://github.com/Fission-AI/OpenSpec/pull/1030) [`485c97e`](https://github.com/Fission-AI/OpenSpec/commit/485c97e97d766e35dd16c02370baee2044abc4f4) Thanks [@TabishB](https://github.com/TabishB)! - ### New Features
- Include the sync workflow in the default core profile so new installs generate `/opsx:sync` skills and commands by default.
- **Canonical artifact paths** — Workflow artifact paths are now resolved via the native `realpath`, so symlinks and case-insensitive filesystems no longer cause path mismatches during apply and archive.
- **Glob apply instructions** — Apply instructions with glob artifact outputs now resolve correctly, and literal artifact outputs are enforced to be file paths.
- **Hidden main spec requirements** — Requirements nested inside fenced code blocks or otherwise hidden in main specs are now detected during validation.
- **Clean `--json` output** — Spinner progress text no longer leaks into stderr when `--json` is passed, so AI agents that combine stdout and stderr can parse the JSON reliably.
- **Silent telemetry in firewalled environments** — PostHog network errors are now swallowed with a 1s timeout and retries/remote config disabled, so OpenSpec no longer surfaces `PostHogFetchNetworkError` in locked-down networks. Telemetry opt-out is documented earlier in the README, installation guide, and CLI reference.
## 1.3.0
### Minor Changes
- [#952](https://github.com/Fission-AI/OpenSpec/pull/952) [`cce787e`](https://github.com/Fission-AI/OpenSpec/commit/cce787ec4083da2b27781f6786f5ce0002909a7b) Thanks [@TabishB](https://github.com/TabishB)! - ### New Features
- **Junie support** — Added tool and command generation for JetBrains Junie
- **Lingma IDE support** — Added configuration support for Lingma IDE
- **ForgeCode support** — Added tool support for ForgeCode
- **IBM Bob support** — Added support for IBM Bob coding assistant
### Bug Fixes
- **Shell completions opt-in** — Completion install is now opt-in, fixing PowerShell encoding corruption
- **Copilot auto-detection** — Prevented false GitHub Copilot detection from a bare `.github/` directory
- [#760](https://github.com/Fission-AI/OpenSpec/pull/760) [`61eb999`](https://github.com/Fission-AI/OpenSpec/commit/61eb999f7c6c0fc98d2e7f3678756fce6a3f4378) Thanks [@fsilvaortiz](https://github.com/fsilvaortiz)! - fix: OpenCode adapter now uses `.opencode/commands/` (plural) to match OpenCode's official directory convention. Fixes #748.
- [#759](https://github.com/Fission-AI/OpenSpec/pull/759) [`afdca0d`](https://github.com/Fission-AI/OpenSpec/commit/afdca0d5dab1aa109cfd8848b2512333ccad60c3) Thanks [@fsilvaortiz](https://github.com/fsilvaortiz)! - fix: `openspec status` now exits gracefully when no changes exist instead of throwing a fatal error. Fixes #714.
## 1.2.0
### Minor Changes
- [#747](https://github.com/Fission-AI/OpenSpec/pull/747) [`1e94443`](https://github.com/Fission-AI/OpenSpec/commit/1e94443a3551b228eecbc89e95d96d3b9600a192) Thanks [@TabishB](https://github.com/TabishB)! - ### New Features
- **Profile system** — Choose between `core` (4 essential workflows) and `custom` (pick any subset) profiles to control which skills get installed. Manage profiles with the new `openspec config profile` command
- **Propose workflow** — New one-step workflow creates a complete change proposal with design, specs, and tasks from a single request — no need to run `new` then `ff` separately
- **AI tool auto-detection** — `openspec init` now scans your project for existing tool directories (`.claude/`, `.cursor/`, etc.) and pre-selects detected tools
- **Pi (pi.dev) support** — Pi coding agent is now a supported tool with prompt and skill generation
- **Kiro support** — AWS Kiro IDE is now a supported tool with prompt and skill generation
- **Sync prunes deselected workflows** — `openspec update` now removes command files and skill directories for workflows you've deselected, keeping your project clean
- **Config drift warning** — `openspec config list` warns when global config is out of sync with the current project
### Bug Fixes
- Fixed onboard preflight giving a false "not initialized" error on freshly initialized projects
- Fixed archive workflow stopping mid-way when syncing — it now properly resumes after sync completes
- Added Windows PowerShell alternatives for onboard shell commands
> **New workflow now available!** We've rebuilt OpenSpec with a new artifact-guided workflow.
>
> Run `/opsx:onboard` to get started. → [Learn more here](docs/opsx.md)
> 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>
### Teams
Using OpenSpec in a team? [Email here](mailto:teams@openspec.dev) for access to our Slack channel.
@@ -75,6 +76,29 @@ AI: Archived to openspec/changes/archive/2025-01-23-add-dark-mode/
Specs updated. Ready for the next feature.
```
<details>
<summary><strong>What do the specs actually look like?</strong></summary>
Plain Markdown — requirements with concrete scenarios, no special syntax to learn. Here's what goes in the `specs/` folder created above:
```markdown
## ADDED Requirements
### Requirement: Theme selection
The app SHALL let users switch between light and dark themes,
defaulting to the system preference.
#### Scenario: User toggles dark mode
- **WHEN** the user clicks the theme toggle
- **THEN** the app switches to dark mode and persists the choice
```
Your AI writes these; you review the plan before any code is written.
OpenSpec is built with OpenSpec — browse this repo's live [specs](openspec/specs) and in-flight [changes](openspec/changes) for real examples at scale.
@@ -84,6 +108,18 @@ AI: Archived to openspec/changes/archive/2025-01-23-add-dark-mode/
</details>
## Why teams adopt OpenSpec
Solo, OpenSpec keeps you and your AI honest on a single repo. On a team, the hard part moves: a feature spans the API server, the web app, and a shared library; requirements are owned by one team and consumed by others; planning starts before any code exists.
**[Stores](docs/stores-beta/user-guide.md)** are the answer — planning in a repo of its own. The same `openspec/` shape you already know (specs and changes), shared by `git push` like anything else. One source of truth your whole team and every coding agent can read, across every repo.
- **Cross-repo features** — one change, one plan, even when the code lands in three repos.
- **Shared requirements** — a platform team owns the specs; product teams reference them read-only, right where their coding agent can read them. No drifting wiki.
- **Plan before code** — capture the plan in the store now; the code repos catch up later.
> Stores are in **beta**. Start with the [Stores User Guide](docs/stores-beta/user-guide.md).
## Quick Start
**Requires Node.js 20.19.0 or higher.**
@@ -101,23 +137,49 @@ cd your-project
openspec init
```
Now tell your AI: `/opsx:new <what-you-want-to-build>`
> **Want your AI to do it?** Paste the [setup prompt](docs/installation.md#install-with-your-ai-assistant) into your coding assistant — it installs the CLI, runs `openspec init`, and verifies the result.
Now talk to your AI:
- **Not sure what to build yet?** Start with `/opsx:explore`, a no-stakes thinking partner that reads your code, weighs options, and shapes a plan before anything is written. ([Explore guide](docs/explore.md))
- **Already know what you want?** Go straight to `/opsx:propose <what-you-want-to-build>`.
Both are in the default profile. If you want the expanded workflow (`/opsx:new`, `/opsx:continue`, `/opsx:ff`, `/opsx:verify`, `/opsx:bulk-archive`, `/opsx:onboard`), select it with `openspec config profile` and apply with `openspec update`.
`/opsx:propose` is the canonical name; your tool may spell it `/opsx-propose` (Cursor, GitHub Copilot), `@opsx-propose` (Amazon Q) or `$openspec-propose` (Codex). `openspec init` prints the right form for the tools you picked — see [How To Invoke](docs/supported-tools.md#how-to-invoke).
> [!NOTE]
> Not sure if your tool is supported? [View the full list](docs/supported-tools.md) – we support 20+ tools and growing.
> Not sure if your tool is supported? [View the full list](docs/supported-tools.md) – we support 30+ tools and growing.
>
> Also works with pnpm, yarn, bun, and nix. [See installation options](docs/installation.md).
## Docs
**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`).
→ **[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>
→ **[Customization](docs/customization.md)**: make it yours
→ **[Customization](docs/customization.md)**: make it yours<br>
→ **[FAQ](docs/faq.md)** · **[Troubleshooting](docs/troubleshooting.md)** · **[Glossary](docs/glossary.md)**: quick help
## Community schemas
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.
→ **[Browse the catalog](docs/customization.md#community-schemas)** in the customization docs.
## Why OpenSpec?
@@ -127,7 +189,7 @@ AI coding assistants are powerful but unpredictable when requirements live only
- **Agree before you build** — human and AI align on specs before code gets written
- **Stay organized** — each change gets its own folder with proposal, specs, design, and tasks
- **Work fluidly** — update any artifact anytime, no rigid phase gates
- **Use your tools** — works with 20+ AI assistants via slash commands
- **Use your tools** — works with 30+ AI assistants via slash commands
### How we compare
@@ -155,7 +217,7 @@ openspec update
## Usage Notes
**Model selection**: OpenSpec works best with high-reasoning models. We recommend Opus 4.5 and GPT 5.2 for both planning and implementation.
**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.
Report privately through [GitHub Security Advisories](https://github.com/Fission-AI/OpenSpec/security/advisories/new). Please don't open a public issue for a suspected vulnerability.
Include what you can: affected version, reproduction steps, and the impact you believe it has. We aim to acknowledge within 3 business days and to ship a fix or a decision within 30 days. Valid reports are credited in the advisory unless you'd rather stay anonymous.
## Supported versions
Fixes ship in the latest published version on npm. Older versions are not patched — upgrade to pick up a fix.
## Threat model
OpenSpec is a local command-line tool. It has no server, no network listener, and no privileged daemon. It reads and writes markdown under the directory you run it in, using paths you supply, with your own user permissions. It can offer to upgrade itself during `openspec update`, and only with your say-so. It sends anonymous usage telemetry, which you can disable with `OPENSPEC_TELEMETRY=0`.
That shapes what is and isn't a vulnerability here:
| In scope | Out of scope |
| --- | --- |
| Code execution triggered by parsing a spec, config, or template file | Reading or writing a file path you passed to the CLI yourself |
| Escaping the directory OpenSpec was pointed at, via untrusted input | Static-analysis findings on file-path joins with no untrusted input |
| Leaking credentials or file contents through telemetry or logs | Vulnerabilities in devDependencies that don't ship in the published package |
| Prototype pollution or injection reachable from a config or spec file | Denial of service against your own machine using your own input |
If you think something sits on the boundary, report it and we'll work it out together.
## Published package contents
The `openspec` npm package publishes `dist/`, `bin/`, `schemas/`, and `scripts/postinstall.js`. Build and test tooling (vite, rollup, vitest, eslint, and their transitive dependencies) is not published. Scanners that read `pnpm-lock.yaml` without separating dependency scope will report advisories for packages that never reach an installed copy of OpenSpec.
You do not have to take that on trust — install the package and look:
```sh
npm install @fission-ai/openspec
ls node_modules | grep -E '^(vite|rollup|vitest|eslint|js-yaml|minimatch)$'# no matches
```
`pnpm audit --prod` in this repository reports the same scope, and CI runs it on every pull request.
## What the CLI does on your machine
| Surface | Behavior |
| --- | --- |
| Install script | `scripts/postinstall.js` prints one line suggesting shell completions. It makes no network request, writes no files, and runs no shell. Completions are opt-in via `openspec completion install`. |
| Running other programs | Every call that goes through a shell uses a fixed literal (`which gh`, `gh auth status`). Anything carrying your input — issue text, editor paths, workset commands, the path passed to `openspec update` — uses an argument array, never string interpolation into a shell. On Windows, `.cmd` shims are launched through `cross-spawn`, which escapes arguments rather than concatenating them. |
| Installing software | `openspec update` can run `npm install -g @fission-ai/openspec@latest` and then re-run `openspec update` with the upgraded CLI. It does this only after you answer yes to a prompt, only for the OpenSpec package itself, only when npm owns the install, and never in CI or a non-interactive shell. A global install lives outside your project, so it runs with your permissions there and executes whatever lifecycle scripts the published package ships. It then reads the installed binary's version back rather than assuming the upgrade took. Decline and it prints the command for you to run yourself. |
| Telemetry | Command name, OpenSpec version, and a locally generated random UUID. No file paths, no file contents, no environment, no hostname, and IP capture is explicitly disabled. Opt out with `OPENSPEC_TELEMETRY=0` or `DO_NOT_TRACK=1`; it is off in CI automatically. |
| Network | Telemetry when enabled, and one npm registry request during `openspec update` to check whether a newer CLI has been published. That request sends no data about you beyond what any HTTP request reveals, runs once per `openspec update` with nothing cached, and is skipped when `CI` is set to anything but an explicit off-value, under `NODE_ENV=test`, or when `OPENSPEC_NO_UPDATE_CHECK`, `DO_NOT_TRACK=1`, or `OPENSPEC_TELEMETRY=0` is set. Reading, writing, and validating specs is entirely local. |
## Automated checks
| Tool | Covers |
| --- | --- |
| [CodeQL](https://github.com/Fission-AI/OpenSpec/security/code-scanning) | Static analysis on every push and pull request to `main` |
| [Dependabot](https://github.com/Fission-AI/OpenSpec/security/dependabot) | Dependency advisories plus weekly update pull requests for the CLI, the docs site, and CI actions |
| Dependency review | Blocks a pull request that introduces a high-severity dependency |
| Secret scanning | Enabled on the repository, including push protection |
| `pnpm audit` | Published dependencies are audited on every pull request, on pushes to `main`, and weekly. Advisory on pull requests so an unrelated change is not blocked; failing elsewhere, so a new advisory surfaces even when no dependency changed. Build tooling is always advisory. |
| Pinned actions | Every GitHub Action runs from a commit SHA, so a moved tag cannot change what CI executes |
Alerts are triaged against the threat model above, so a finding in build-only tooling is fixed on the normal update cadence rather than treated as an incident.
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 artifact or code exists. 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 |
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.
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`):
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.
`{ "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.
### 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.
`{ "changeName", "changeDir", "schemaName", "contextFiles": { "<artifactId>": ["/abs", ...] }, "progress": {total,complete,remaining}, "tasks": [{id,description,done}], "state": "blocked"|"all_done"|"ready", "missingArtifacts"?, "instruction", "references"?, "context"?, "operationGuidance"?, "root" }`. Both optional fields are read from the selected root on every invocation. `context` is a required prompt-level input whose relevant project facts, conventions, and constraints must be applied; `operationGuidance` is advisory input whose entries are followed only when applicable and compatible with the built-in workflow. Both remain separate from state, tasks, progress, context files, and the built-in instruction.
### 4.7 `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.
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.
`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.
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`.
The OpenSpec CLI (`openspec`) provides terminal commands for project setup, validation, status inspection, and management. These commands complement the AI slash commands (like `/opsx:new`) documented in [Commands](commands.md).
The OpenSpec CLI (`openspec`) provides terminal commands for project setup, validation, status inspection, and management. These commands complement the AI slash commands (like `/opsx:propose`) documented in [Commands](commands.md).
## Summary
| Category | Commands | Purpose |
|----------|----------|---------|
| **Setup** | `init`, `update` | Initialize and update OpenSpec in your project |
| **Health** | `doctor` | Report relationship health for the resolved root |
| **Working context** | `context` | Assemble the working set (root + referenced stores) |
| **Personal worksets** | `workset create`, `workset list`, `workset open`, `workset remove` | Keep and open personal, local working views in your tool |
`--profile custom` uses whatever workflows are currently selected in global config (`openspec config profile`).
The welcome animation is also skipped when the `OPENSPEC_NO_ANIMATION` environment variable is set (any value, including empty), when `NO_COLOR` is set to a non-empty value, or when the OS reduced-motion preference is enabled (macOS Reduce Motion, GNOME animations disabled).
**Supported tool IDs (`--tools`)** — `windsurf` is also accepted, as an alias for `devin`: `amazon-q`, `antigravity`, `auggie`, `bob`, `claude`, `cline`, `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`, `trae`, `zcode`, `agents`
> This list mirrors `AI_TOOLS` in `src/core/config.ts`. See [Supported Tools](supported-tools.md) for each tool's skill and command paths.
**Examples:**
@@ -98,9 +125,15 @@ openspec init ./my-project
# Non-interactive: configure for Claude and Cursor
openspec init --tools claude,cursor
# Non-interactive: configure global MiniMax Code skills
openspec init --tools minimax-code
# Configure for all supported tools
openspec init --tools all
# Override profile for this run
openspec init --profile core
# Skip prompts and auto-cleanup legacy files
openspec init --force
```
@@ -113,8 +146,10 @@ openspec/
├── changes/ # Proposed changes
└── config.yaml # Project configuration
.claude/skills/ # Claude Code skill files (if claude selected)
.cursor/commands/ # Cursor OPSX commands (if delivery includes commands)
.agents/skills/ # Shared skills for AGENTS.md-compatible tools (if agents selected)
... (other tool configs)
```
@@ -122,7 +157,7 @@ openspec/
### `openspec update`
Update OpenSpec instruction files after upgrading the CLI. Re-generates AI tool configuration files.
Update OpenSpec instruction files after upgrading the CLI. Re-generates AI tool configuration files using your current global profile, selected workflows, and delivery mode.
Upgrade the package first. Instruction files are generated by the installed CLI, so running `openspec update` against a stale install reports everything up to date without adding the workflows newer releases ship.
To make that visible, `openspec update` asks the npm registry whether a newer CLI has been published. When yours is behind, it offers to upgrade:
```text
A newer OpenSpec CLI is available (v1.6.0 → v1.7.0).
Answer yes and it runs `npm install -g @fission-ai/openspec@latest`, then re-runs the update with the new CLI so the new workflows land in the same command. It confirms the upgrade by asking the installed binary its version rather than trusting npm's exit code, so if another install earlier on your `PATH` is still answering, it tells you instead of claiming success. Answer no and it prints the command and updates with the CLI you have. Ctrl-C stops the command.
The offer appears only in an interactive terminal, and only when npm owns the install — the one case `npm install -g` actually fixes. Everything else gets the command that matches how it was installed instead:
| How OpenSpec is installed | What you get |
|---------------------------|--------------|
| Global npm install | The prompt, and the upgrade run for you — in an interactive terminal; piped output gets the printed command instead |
| Global pnpm, bun, yarn, or volta install | That manager's own command: `pnpm add -g …@latest`, `bun add -g …@latest`, `yarn global add …@latest`, or `volta install …@latest` |
| A dependency of the project | A note to update the dependency, since its package manager owns the lockfile |
| An `npx` / `dlx` cache | `npx @fission-ai/openspec@latest update` — that command is the update, so there is no second step |
| A git clone | Nothing — your version is whatever the branch says |
Whenever anything is printed, it names the directory the running CLI was loaded from — the thing to check when you did upgrade but a stale shim still owns your `PATH`.
It asks the registry in `npm_config_registry` when npm exports it, and `https://registry.npmjs.org` otherwise. No `.npmrc` is read: letting file contents choose where an outbound request goes is a flow worth avoiding, and a project's `.npmrc` travels with the repository. On a private mirror, export `npm_config_registry` — or set `OPENSPEC_NO_UPDATE_CHECK` to skip the check entirely. The check is skipped when `CI` is set to anything but an explicit off-value (`false`, `0`, `no`, `off`, or empty), under `NODE_ENV=test`, and whenever `OPENSPEC_NO_UPDATE_CHECK` (any value), `DO_NOT_TRACK=1`, or `OPENSPEC_TELEMETRY=0` is set. It runs before the update and can delay it by at most 1.5 seconds — it gives up after that even when the network drops packets silently, and stays quiet when the registry is unreachable.
**How "up to date" is decided:** skill files record the version that generated
them, so OpenSpec compares that against the installed CLI. Command files carry no
version stamp, so for a tool that has commands but no skills (delivery
`commands`), OpenSpec compares the file contents against what it would generate
now — edits to those files count as drift and are overwritten. With delivery
`skills` or `both`, only the recorded version is checked, so a hand-edited file
whose version still matches is left alone; use `--force` to rewrite it. Either
way, generated files are OpenSpec's to own — keep your own instructions
elsewhere.
---
## Stores (standalone OpenSpec repos)
> **Beta.** Stores and the features built on them (references, working context, worksets) are new; command names, flags, file formats, and JSON output may change shape between releases. For the problem-first walkthrough, see the [stores guide](stores-beta/user-guide.md).
A store is a standalone OpenSpec repo you've registered on this machine — for example a planning repo or a contracts repo. Registering a store lets normal commands (`list`, `show`, `status`, `validate`, `new change`, `archive`, ...) act in it from anywhere by passing `--store <id>`.
### `openspec store setup`
Create and register a local store. With no arguments in a terminal,
OpenSpec guides the user through setup. Agents and scripts should pass explicit
inputs and use `--json`.
```bash
openspec store setup [id][options]
```
**Options:**
| Option | Description |
|--------|-------------|
| `--path <path>` | Folder where the store should live (for example `~/openspec/<id>`) |
| `--remote <url>` | Record the canonical remote in the new store's `store.yaml` |
| `--init-git` | Initialize a Git repository with an initial commit (default) |
| `--no-init-git` | Skip every Git action: no init, no initial commit |
| `--json` | Output JSON |
Non-interactive runs (`--json`, scripts, agents) must pass both the store id and `--path`. In an interactive terminal, setup prompts for the location with an editable suggestion in a visible, user-owned place (for example `~/openspec/<id>`); it never defaults to OpenSpec's managed data directory.
Examples:
```bash
openspec store setup
openspec store setup team-context
openspec store setup team-context --path ~/openspec/team-context --no-init-git
openspec store setup team-context --path ~/openspec/team-context --no-init-git --json
```
### `openspec store register`
Register an existing local store folder. During the stores beta, a root may be
registered before any changes exist, specs have been applied, or changes have
been archived; in that case `openspec/changes/`, `openspec/specs/`, and
`openspec/changes/archive/` may be absent until normal commands create them.
A config-only repo that declares `store: <id>` remains a pointer to another
store and is not registered as a store root unless that pointer is removed.
```bash
openspec store register [path][options]
```
**Options:**
| Option | Description |
|--------|-------------|
| `--id <id>` | Store id; defaults to store metadata or folder name |
| `--yes` | Confirm creating store identity metadata for a healthy OpenSpec root |
| `--json` | Output JSON |
### `openspec store unregister`
Forget a local store registration without deleting files.
```bash
openspec store unregister <id> [--json]
```
Use this when a store was moved, cloned somewhere else, or should no longer be
shown by OpenSpec on this machine.
### `openspec store remove`
Forget a local store registration and delete its local folder.
```bash
openspec store remove <id> [--yes][--json]
```
`remove` shows the exact folder before deleting in an interactive terminal.
Agents, scripts, and JSON callers must pass `--yes` to confirm deletion.
OpenSpec refuses to delete a folder that does not contain matching
store metadata.
### `openspec store list`
List locally registered stores.
```bash
openspec store list [--json]
openspec store ls [--json]
```
### `openspec store doctor`
Check local store registration, metadata, and Git presence.
```bash
openspec store doctor [id][--json]
```
Doctor is diagnostic-only; it reports missing roots, metadata mismatches, and invalid local registry state without modifying the store.
### Referencing stores from a project
A project repo can declare which stores its work draws on in `openspec/config.yaml`:
```yaml
schema:spec-driven
references:
- team-context
```
From then on, `openspec instructions` output in that repo (both the per-artifact and `apply` surfaces, JSON and human modes) carries an index of each referenced store's specs — spec ids, a one-line summary from each spec's Purpose section, and the fetch command (`openspec show <spec-id> --type spec --store <id>`). The index is built live from the registered checkout on every run; spec content is never copied into the output.
References are read-only context. They never change where commands act: work stays in the repo's own root, and writing to a referenced store remains an explicit `--store` action. A reference that cannot be resolved (for example, a store not registered on this machine) degrades to a warning in the index with the exact fix, and instructions still generate. `openspec doctor` reports reference health in one place.
### Recording where a store is cloned from
A store can record its canonical clone source in its committed identity file, so onboarding never dead-ends at "register the store":
```bash
openspec store setup team-context --path ~/openspec/team-context \
--remote git@github.com:acme/team-context.git
```
The remote lands in `.openspec-store/store.yaml` inside the initial commit, so every clone is born knowing it. For an existing store, edit `store.yaml` by hand and commit. `store doctor` shows the recorded remote (and the checkout's observed Git origin); setup/register sharing guidance names it; and register records the checkout's origin in the machine-local registry.
A reference declaration can carry the clone source too, so a teammate who doesn't have the store yet gets a complete, pasteable fix (`git clone <remote> <path> && openspec store register <path> --id <id>`):
Recording a remote is not sync: OpenSpec never clones, pulls, or pushes on its own.
### Declaring a default store
A repo whose planning is fully externalized — no local `openspec/specs/` or `openspec/changes/` — can declare its store once instead of passing `--store` on every command:
```yaml
# openspec/config.yaml (the only file under openspec/)
store:team-context
```
Normal commands then resolve to the declared store automatically; the root banner and JSON `root` block report `source: "declared"` with the store id, and printed hints still carry `--store <id>`. The declaration is a fallback, never an override: explicit `--store` always wins, and a directory with real planning folders ignores the pointer (with a warning). To convert a pointer repo into a local OpenSpec root, remove the `store:` line and run `openspec init` — init refuses to scaffold while the declaration is present.
A machine-level variant covers every repo at once: `openspec config set defaultStore <id>` (see Configuration). It is consulted only after `--store`, a local root, and a project pointer have all failed to resolve; the root banner and JSON `root` block then report `source: "global_default"`.
## Doctor (relationship health)
One read-only question, one place: is the OpenSpec root healthy, and are the stores it references available on this machine?
```bash
openspec doctor [--store <id>][--json]
```
The report separates root health, store metadata health (including a note when the recorded remote and the checkout's origin diverge, and a note when the store checkout has drifted behind its last-fetched upstream tracking ref), and reference health (the same diagnostics instructions show, with clone fixes for unresolved references). Health findings of any severity exit 0 — agents read the `status` arrays; only command failures (no root, unknown store) exit 1. Doctor never clones, syncs, or repairs. To get the assembled set itself rather than its health, use `openspec context`.
## Working context (the assembled set)
Everything this work relates to through OpenSpec declarations, in one working set: the OpenSpec root and the stores it references.
The JSON brief is agent-consumable (each available referenced store carries its fetch recipe; unresolved members carry the same fixes instructions and doctor show). `--code-workspace` additionally writes a VS Code workspace file containing the root plus the available referenced stores (`ref:<id>` folders) — the one write this command performs, refused without `--force` if the file exists. Unavailable members are reported, never guessed at.
"Working context" is the assembled set; the `context:` field in `openspec/config.yaml` is project background injected into instructions — two different things. `openspec doctor` answers whether the set is healthy; `openspec context` answers what the set is.
## Personal worksets
> **Beta.** Worksets are part of the new beta surface; commands, flags, and file formats may change shape between releases. For the walkthrough, see the [stores guide](stores-beta/user-guide.md#worksets-reopen-the-folders-you-work-on-together).
A workset is a personal, named view of the folders you work on together — a planning root plus whatever else you choose — kept on your machine and reopened by name in your tool. It is purely local: never committed, never shared, never derived from declarations, and removing one never touches a member folder.
`create` runs a short guided flow (or takes `--member` flags non-interactively; the first member is the primary — sessions start there). `open` launches the chosen tool: editors (VS Code, Cursor) open a window with every member and return; CLI agents (Claude Code, codex) take over this terminal as a session with every member attached and no prompt pre-filled, ending when you exit. A member folder missing at open time is skipped with a note; the rest opens. The saved tool preference is overridable per open with `--tool`.
Supporting a new tool is configuration, not code. Every tool is one of two launch styles — `workspace-file` (launched with the generated `.code-workspace`) or `attach-dirs` (one attach flag per member) — and the `openers` key in the global `config.json` (open it with `openspec config edit`) adds tools or adjusts built-ins per field:
```json
{
"openers":{
"zed":{"style":"workspace-file"},
"claude":{"attach_flag":"--dir"}
}
}
```
All workset state lives under the global data dir's `worksets/` folder (the saved views plus the generated `<name>.code-workspace` files, regenerated on every open); deleting that folder removes every trace.
---
## Browsing Commands
@@ -185,9 +456,8 @@ openspec list --json
**Output (text):**
```
Active changes:
add-dark-mode UI theme switching support
fix-login-bug Session timeout handling
Changes:
add-dark-mode No tasks just now
```
---
@@ -262,12 +532,14 @@ openspec show add-dark-mode --json
### `openspec validate`
Validate changes and specs for structural issues.
Validate changes and specs for structural issues, and check a change's MODIFIED requirements against the main specs they would replace.
```
openspec validate [item-name] [options]
```
A change with zero spec deltas fails validation unless its `.openspec.yaml` declares `skip_specs: true` (for pure refactors, tooling, or docs work — see [Recipe 5](examples.md#recipe-5-a-refactor-with-no-behavior-change)).
| `--archived` | Validate that archived changes have all tasks completed (for pre-commit linting) |
| `--type <type>` | Specify type when name is ambiguous: `change` or `spec` |
| `--strict` | Enable strict validation mode |
| `--json` | Output as JSON |
| `--concurrency <n>` | Max parallel validations (default: 6, or `OPENSPEC_CONCURRENCY` env) |
| `--no-interactive` | Disable prompts |
`--archived` is its own scope: it does not validate spec deltas (already applied at archive time), it verifies that every change under `changes/archive/` has all of its `tasks.md` checkboxes ticked, exiting non-zero if any are unchecked. This catches changes that were archived with unfinished work — handy in a pre-commit hook.
| `-y, --yes` | Skip confirmation prompts. Required when nothing can answer them — an AI agent, a CI job, or any run with stdin closed |
| `--skip-specs` | Skip spec updates for one archive run. A change that permanently has no spec deltas should declare `skip_specs: true` in its `.openspec.yaml` instead — it archives with no flag |
| `--no-validate` | Skip validation (requires confirmation). Also disables capability retirement — with no validator verdict, nothing is retired |
**Examples:**
```bash
# Interactive archive
# Interactive archive (asks which change, then confirms)
openspec archive
# Archive specific change
openspec archive add-dark-mode
# Archive without prompts (CI/scripts)
# Archive without prompts (agents, CI, scripts)
openspec archive add-dark-mode --yes
# Archive a tooling change that doesn't affect specs
4.Moves change folder to `openspec/changes/archive/YYYY-MM-DD-<name>/`
3.Claims the archive destination before changing any main spec
4.Validates and merges the active delta specs into `openspec/specs/` — a capability whose last requirement the change removes is retired, and its spec file deleted, but only when the change's `.openspec.yaml` declares `retire_capabilities: true` next to its `schema:`
5. Moves the change folder to `openspec/changes/archive/YYYY-MM-DD-<name>/`
6. If a spec mutation or final move fails before a complete archive is secured, restores the specs and leaves or returns the change at its active path
7. If a verified fallback copy completes but staged-source cleanup fails, retains the complete archive and committed spec state for recovery
**Without a terminal:** an AI agent, a CI job, or any run with stdin closed cannot
answer step 2, so archive stops before touching anything, exits 1, and names the
command to rerun — `openspec archive <name> --yes`, carrying whatever other flags
you passed. Pass `--yes` (and the change name) up front to skip the round trip.
These commands support the artifact-driven OPSX workflow. They're useful for both humans checking progress and agents determining next steps.
### `openspec new change`
Create a change directory and optional checked-in metadata in the resolved OpenSpec root.
```bash
openspec new change <name> [options]
```
Change names must use lowercase kebab-case: lowercase letters, numbers, and
single hyphens. They cannot contain spaces, underscores, uppercase letters,
consecutive hyphens, or leading/trailing hyphens. A leading number is allowed,
so you can prefix names to order or tier changes, for example `100-add-feature`
or `00001-add-auth`.
**Options:**
| Option | Description |
|--------|-------------|
| `--description <text>` | Description to add to `README.md` |
| `--goal <text>` | Optional goal metadata to store with the change |
| `--schema <name>` | Workflow schema to use |
| `--store <id>` | Store id to use as the OpenSpec root (a store is a standalone OpenSpec repo you've registered) |
| `--json` | Output JSON |
Examples:
```bash
openspec new change add-billing-api
openspec new change add-billing-api --store team-context --json
```
### `openspec status`
Display artifact completion status for a change.
@@ -428,32 +745,45 @@ openspec status --change add-dark-mode --json
```
Change: add-dark-mode
Schema: spec-driven
Progress: 2/4 artifacts complete
Artifacts:
✓ proposal proposal.md exists
✓ specs specs/ exists
◆ design ready (requires: specs)
○ tasks blocked (requires: design)
Next: Create design using /opsx:continue
[x] proposal
[x] specs
[ ] design
[-] tasks (blocked by: design)
```
A change that declares `skip_specs: true` shows its specs stage as `[~] specs (skipped: change declares skip_specs)` and excludes it from the progress count.
- Current project context and matching operation guidance for `apply`/`archive`
Operation inputs are read from the resolved repo or selected store on every
invocation. Project context is a required prompt-level input: agents read it and
apply relevant project facts, conventions, and constraints. Operation guidance is
optional additive advice: agents consider every entry and follow only entries that
are applicable and compatible with the built-in workflow. Both fields remain
separate from explicit user choices, CLI-controlled state, built-in instructions,
and artifact rules. Conflicting context is reported; conflicting or inapplicable
guidance is not followed and the reason is explained. These are behavioral
contracts for generated agents, not enforceable CLI checks. `instructions archive`
returns only the selected change, optional inputs, and root metadata; it does not
include the static archive workflow.
For an artifact skipped via `skip_specs: true`, the output is a warning only (JSON adds `skipped`/`warning` fields) — the artifact must not be created.
---
@@ -560,6 +910,7 @@ openspec schemas [options]
| Option | Description |
|--------|-------------|
| `--json` | Output as JSON |
| `--store <id>` | Use a registered store as the OpenSpec root |
**Example:**
@@ -781,7 +1132,7 @@ openspec config list
# Get a specific value
openspec config get telemetry.enabled
# Set a value
# Set a value (disable anonymous usage telemetry)
openspec config set telemetry.enabled false
# Set a string value explicitly
@@ -790,6 +1141,10 @@ openspec config set user.name "My Name" --string
# Remove a custom setting
openspec config unset user.name
# Set a machine-level default store (fallback root when no --store,
# local root, or project store: pointer resolves)
openspec config set defaultStore team-plans
# Reset all configuration
openspec config reset --all --yes
@@ -803,6 +1158,11 @@ openspec config profile
openspec config profile core
```
**Telemetry opt-out:**`telemetry.enabled` defaults to on when unset (opt-out model).
Set it to `false` to disable anonymous usage stats and the `openspec update` version check.
Environment variables take precedence over config: `OPENSPEC_TELEMETRY=0`, `DO_NOT_TRACK=1`,
and a truthy `CI` value (e.g. `true`/`1`/`yes`) always disable telemetry regardless of the config value.
`openspec config profile` starts with a current-state summary, then lets you choose:
- Change delivery + workflows
- Change delivery only
@@ -810,7 +1170,7 @@ openspec config profile core
- Keep current settings (exit)
If you keep current settings, no changes are written and no update prompt is shown.
If there are no config changes but the current project files are out of sync with your global profile/delivery, OpenSpec will show a warning and suggest running `openspec update`.
If there are no config changes but the current project files are out of sync with your global profile/delivery, OpenSpec will show a warning and suggest `openspec update`.
Pressing `Ctrl+C` also cancels the flow cleanly (no stack trace) and exits with code `130`.
In the workflow checklist, `[x]` means the workflow is selected in global config. To apply those selections to project files, run `openspec update` (or choose `Apply changes to this project now?` when prompted inside a project).
| `EDITOR` or `VISUAL` | Editor for `openspec config edit` |
| `NO_COLOR` | Disable color output when set |
| `OPENSPEC_NO_ANIMATION` | Disable the `openspec init` welcome animation when set |
| `OPENSPEC_NO_UPDATE_CHECK` | Disable the `openspec update` check for a newer published CLI when set (any value, including empty). Also skipped when `CI` is set (unless `false`/`0`/`no`/`off`) or `NODE_ENV=test` |
| `npm_config_registry` | Registry the `openspec update` version check asks. Must be an `http(s)` URL or it falls back to `https://registry.npmjs.org`. No `.npmrc` file is read |
---
## Related Documentation
- [Commands](commands.md) - AI slash commands (`/opsx:new`, `/opsx:apply`, etc.)
- [Commands](commands.md) - AI slash commands (`/opsx:propose`, `/opsx:apply`, etc.)
- [Workflows](workflows.md) - Common patterns and when to use each command
- [Customization](customization.md) - Create custom schemas and templates
This is the reference for OpenSpec's slash commands. These commands are invoked in your AI coding assistant's chat interface (e.g., Claude Code, Cursor, Windsurf).
This is the reference for OpenSpec's slash commands. These commands are invoked in your AI coding assistant's chat interface (e.g., Claude Code, Cursor, Devin Desktop).
For workflow patterns and when to use each command, see [Workflows](workflows.md). For CLI commands, see [CLI](cli.md).
These pages use `/opsx:<command>` as the canonical name. Some tools spell it
differently — Cursor and GitHub Copilot register `/opsx-propose`, Codex uses
`$openspec-propose` — so check [How To Invoke](supported-tools.md#how-to-invoke)
for your tool. The files OpenSpec generates already use the right form.
## Quick Reference
### Default Quick Path (`core` profile)
| 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:new` | Start a new change |
| `/opsx:continue` | Create the next artifact based on dependencies |
| `/opsx:ff` | Fast-forward: create all planning artifacts at once |
| `/opsx:apply` | Implement tasks from the change |
| `/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 |
- 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 change exists. 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:**
@@ -42,7 +97,7 @@ Think through ideas, investigate problems, and clarify requirements before commi
- Investigates the codebase to answer questions
- Compares options and approaches
- Creates visual diagrams to clarify thinking
- Can transition to `/opsx:new` when insights crystallize
- Can transition to `/opsx:propose` (default) or `/opsx:new` (expanded workflow) when insights crystallize
**Example:**
```text
@@ -66,7 +121,7 @@ AI: Let me investigate your current auth setup...
You: Let's go with JWT. Can we start a change for that?
AI: Ready when you are. Run /opsx:new add-jwt-auth to begin.
AI: Ready when you are. Run /opsx:propose add-jwt-auth to begin.
```
**Tips:**
@@ -79,7 +134,9 @@ AI: Ready when you are. Run /opsx:new add-jwt-auth to begin.
### `/opsx:new`
Start a new change. Creates the change folder structure and scaffolds it with the selected schema.
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:**
```
@@ -153,7 +210,7 @@ AI: Change: add-dark-mode
✓ proposal (done)
◆ specs (ready)
◆ design (ready)
○ tasks (blocked - needs: specs)
○ tasks (blocked - needs: specs, design)
Creating specs...
@@ -161,7 +218,7 @@ AI: Change: add-dark-mode
✓ Created openspec/changes/add-dark-mode/specs/ui/spec.md
Revise a change's existing planning artifacts and keep them coherent with one another. Planning artifacts only - it never edits code.
**Syntax:**
```text
/opsx:update [change-name]
```
**Arguments:**
| Argument | Required | Description |
|----------|----------|-------------|
| `change-name` | No | Which change to update (inferred from context if not provided) |
**What it does:**
- Reads the change's artifacts via `openspec status --change <name> --json`
- Applies your requested revision, or reviews the artifacts for contradictions if you didn't name one
- Reconciles the other existing artifacts in any direction (a design edit may ripple back to the proposal)
- Confirms every edit with you before writing, one artifact at a time
- Ends by recommending the next step: `/opsx:continue` (artifacts missing), `/opsx:apply` (carry a revised plan into code), or `/opsx:archive` (all done)
**Example:**
```text
You: /opsx:update add-dark-mode - we're storing the theme in a cookie now, not localStorage
AI: Reading add-dark-mode artifacts...
The design references localStorage in two places; tasks 1.3 covers
localStorage persistence; the proposal doesn't mention storage.
Proposed revisions:
1. design.md - swap localStorage decision for cookie storage
2. tasks.md - reword task 1.3 to cookie persistence
Apply revision 1? (design.md)
```
**Tips:**
- It won't create missing artifacts - that's `/opsx:continue`
- If the change was already implemented, follow up with `/opsx:apply` so the code matches the revised plan
- If your revision changes the *intent* of the change, start fresh with a new change instead (see [When to Update vs. Start Fresh](opsx.md#when-to-update-vs-start-fresh))
---
### `/opsx:verify`
Validate that implementation matches your change artifacts. Checks completeness, correctness, and coherence.
@@ -563,15 +669,20 @@ AI: Welcome to OpenSpec!
Different AI tools use slightly different command syntax. Use the format that matches your tool:
| none — Kimi Code | `/skill:openspec-propose` | Kimi Code |
| none — Codex CLI |`$openspec-propose` | Codex |
The functionality is identical regardless of syntax.
> **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.
| `## REMOVED Requirements` | Deprecated behavior | Deleted from main spec |
| `## 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 |
Both surfaces return current project `context` and matching
`operationGuidance` as separate optional fields. Each invocation reads a fresh
snapshot from the resolved root. When `--store <id>` is selected, the change,
context, and guidance all come from that store rather than the current repository.
The archive instruction command is read-only: it does not inspect or merge delta
specs, write main specs, move the change, or run the static archive workflow.
Project context is a required prompt-level input. Generated workflows read it and
apply relevant project facts, conventions, and constraints. Operation guidance is
optional additive advice: workflows consider every entry and follow entries that
are applicable and compatible with the built-in workflow.
Both fields remain separate from CLI-controlled state, resolved paths, built-in
steps, explicit user choices, and artifact rules. A workflow reports context
conflicts while preserving the controlling value. It does not follow inapplicable
or conflicting guidance and explains why. Neither field is an enforceable check,
and workflows do not copy their text into implementation files, specs, change
artifacts, or summaries unless the user separately requests that content.
**Archive and spec-sync input safety:**
Archive, bulk archive, and standalone sync use
`artifactPaths.specs.existingOutputPaths` from `openspec status --json` as the
only delta-spec source. A schema without a `specs` artifact, or a change whose
concrete output list is empty, has nothing to sync; other artifacts are not used
to infer delta specs.
Before a semantic merge writes a main spec, the workflow consumes current
`openspec instructions specs --change <name> --json` output. The returned
`specs` rules constrain only the main specs produced by that merge. Single archive
passes that snapshot into inline sync, standalone sync fetches it directly, and
bulk archive obtains every required snapshot before its first spec write. A
non-zero or invalid JSON archive/specs instruction response is a lookup failure,
not an empty input: the workflow stops before the affected spec write or change
move (for bulk archive, before any batch write or move).
This configuration does not change archive execution phases, user prompts,
filesystem operations, semantic merge ownership, the direct `openspec archive`
command, or the structure and output of artifact `rules`.
### Schema Resolution Order
When OpenSpec needs a schema, it checks in this order:
@@ -197,6 +266,10 @@ apply:
| `instruction` | AI instructions for creating this artifact |
| `requires` | Dependencies - which artifacts must exist first |
List artifacts in the order you want them written. `requires` decides what is
possible; the order of the `artifacts:` list decides what comes first when
several artifacts are ready at once.
### Templates
Templates are markdown files that guide the AI. They're injected into the prompt when creating that artifact.
@@ -337,6 +410,24 @@ Then edit `schema.yaml` to add:
---
## 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).
| `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
**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
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 and no artifacts created. 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:
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
**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.
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
**`/opsx:explore` is your thinking partner. Reach for it whenever you have a problem but not yet a plan.** It investigates your codebase, weighs options with you, and clarifies what you actually want, all before a single artifact or line of code is created. When the picture is clear, it hands off to `/opsx:propose`.
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.
- Transition to `/opsx:propose` when you're ready.
**It does not:**
- Create a change folder.
- Write any artifacts (no proposal, specs, design, or tasks).
- Write or modify code.
That's the point. Exploring costs you nothing and commits you to nothing. You can explore three dead ends, learn something from each, and only then propose the path that survived.
## 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.
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 any artifact exists. It's especially powerful in unfamiliar code, where the AI's ability to read and summarize the system saves you an afternoon of spelunking.
**What 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
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 change or code exists. It's in the default profile, so it's always available. When the plan is clear, it hands off to `/opsx:propose`. This is the single best habit to form, because it stops an eager AI from confidently building the wrong thing. See [Explore First](explore.md).
### 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).
This guide explains how OpenSpec works after you've installed and initialized it. For installation instructions, see the [main README](../README.md#quick-start).
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:
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 artifact or code exists. When the picture is clear, it hands off to `/opsx:propose`. This is the single best habit for working with an AI that will otherwise confidently build the wrong thing. See the [Explore guide](explore.md).
## How It Works
OpenSpec helps you and your AI coding assistant agree on what to build before any code is written. The workflow follows a simple pattern:
OpenSpec helps you and your AI coding assistant agree on what to build before any code is written.
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.
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:
@@ -131,23 +149,12 @@ The change folder moves to `openspec/changes/archive/` for audit history.
Let's walk through adding dark mode to an application.
### 1. Start the Change
### 1. Start the Change (Default)
```
You: /opsx:new add-dark-mode
```text
You: /opsx:propose add-dark-mode
AI: Created openspec/changes/add-dark-mode/
Ready to create: proposal
```
### 2. Create Artifacts
Use `/opsx:ff` (fast-forward) to create all planning artifacts at once:
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, creating no artifacts and writing no code. The recommended starting point whenever you have a problem but not yet a plan. See [Explore First](explore.md).
**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
**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)
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 |
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, 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.
- **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.
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`
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
```
@@ -67,6 +161,39 @@ Or add to your development environment in `flake.nix`:
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:
- 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.
@@ -84,6 +85,9 @@ Don't worry about getting it perfect. We're still learning what works best here,
Both `openspec init` and `openspec update` detect legacy files and guide you through the same cleanup process. Use whichever fits your situation:
- New installs default to profile `core` (`propose`, `explore`, `apply`, `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:
@@ -141,7 +145,7 @@ Run this if you just want to migrate and refresh your existing tools to the late
openspec update
```
The update command also detects and cleans up legacy artifacts, then refreshes your skills to the latest version.
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
@@ -153,6 +157,8 @@ openspec init --force --tools claude
The `--force` flag skips prompts and auto-accepts cleanup.
This includes cleanup of OpenSpec-managed Codex prompt files in the global Codex prompt directory. Cleanup only targets OpenSpec's allowlisted legacy Codex prompt filenames, removes them only after replacement `.agents/skills/openspec-*` skills exist, and preserves all other files.
---
## Migrating project.md to config.yaml
@@ -275,30 +281,44 @@ The AI will help you identify what's essential vs. what can be trimmed.
## The New Commands
After migration, you have 9 OPSX commands instead of 3:
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:new` | Start a new change |
| `/opsx:continue` | Create the next artifact (one at a time) |
| `/opsx:ff` | Fast-forward—create all planning artifacts at once |
Enable expanded commands with `openspec config profile`, then run `openspec update`.
### Command Mapping from Legacy
| Legacy | OPSX Equivalent |
|--------|-----------------|
| `/openspec:proposal` | `/opsx:new` then `/opsx:ff` |
| `/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
@@ -391,6 +411,8 @@ OPSX uses the emerging **skills** standard:
Skills are recognized across multiple AI coding tools and provide richer metadata.
Codex is skills-only in OPSX. OpenSpec no longer generates Codex custom prompt files; use the generated `.agents/skills/openspec-*` directories instead.
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
@@ -155,13 +157,18 @@ rules:
| Command | What it does |
|---------|--------------|
| `/opsx:propose` | Create a change and generate planning artifacts in one step (default quick path) |
| `/opsx:onboard` | Guided walkthrough of an end-to-end change (expanded workflow) |
## Usage
@@ -169,13 +176,21 @@ rules:
```
/opsx:explore
```
Think through ideas, investigate problems, compare options. No structure required - just a thinking partner. When insights crystallize, transition to `/opsx:new` or `/opsx:ff`.
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:new
/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
```
You'll be asked what you want to build and which workflow schema to use.
### Create artifacts
```
@@ -194,6 +209,18 @@ Creates all planning artifacts at once. Use when you have a clear picture of wha
```
Works through tasks, checking them off as you go. If you're juggling multiple changes, you can run `/opsx:apply <name>`; otherwise it should infer from the conversation and prompt you to choose if it can't tell.
### Updating a change
```
/opsx:update add-dark-mode - we're storing the theme in a cookie now
```
Revises the change's existing planning artifacts and keeps them coherent - in any direction (a design edit may ripple back to the proposal). Planning artifacts only: it never edits code, and it never creates missing artifacts (that's `/opsx:continue`). Every edit is confirmed with you first. If the change was already implemented, it recommends `/opsx:apply` so the code catches up with the revised plan. If your revision changes the change's *intent*, start fresh instead - see [When to Update vs. Start Fresh](#when-to-update-vs-start-fresh).
### 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)
@@ -299,6 +326,7 @@ Think of it like git branches:
## 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
@@ -356,7 +384,7 @@ This section explains how OPSX works under the hood and how it compares to the l
**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:
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.
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 artifact exists. It's the best antidote to an AI that will otherwise build *something* from a vague prompt. Already know exactly what you want? Skip straight to `/opsx:propose`. Either way, explore ships in the default profile, so it's always there. See the [Explore guide](explore.md).
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).
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.
OpenSpec works with 20+ AI coding assistants. When you run `openspec init`, you'll be prompted to select which tools you use, and OpenSpec will configure the appropriate integrations.
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 tool you select, OpenSpec installs:
For each selected tool, OpenSpec can install:
1.**Skills**— Reusable instruction files that power the `/opsx:*` workflow commands
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 |
| [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. |
| Shared `.agents` skills (`agents`) | `.agents/skills/openspec-*/SKILL.md` | Not generated (no command adapter; use skill-based `/openspec-*` invocations) |
\* Codex commands are installed to the global home directory (`~/.codex/prompts/` or `$CODEX_HOME/prompts/`), not the project directory.
\*\* 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.
\*\* GitHub Copilot's `.github/prompts/*.prompt.md` files are recognized as custom slash commands in **IDE extensions only** (VS Code, JetBrains, Visual Studio). GitHub Copilot CLI does not currently support custom prompts from this directory — see [github/copilot-cli#618](https://github.com/github/copilot-cli/issues/618). If you use Copilot CLI, you may need to manually set up [custom agents](https://docs.github.com/en/copilot/how-tos/use-copilot-agents/coding-agent/create-custom-agents) in `.github/agents/` as a workaround.
\*\*\* 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-*`.
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 is the exception because it uses the same canonical `.agents`
root. If both `codex` and `agents` are selected, 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).
Because `.agents/skills/` is shared, 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
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 the `--tools`flag:
For CI/CD or scripted setup, use `--tools`(and optionally `--profile`):
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.
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 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.
- **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.
> **Customization:** OPSX workflows are driven by schemas that define artifact sequences. See [Customization](customization.md) for details on creating custom schemas.
## Workflow Patterns
## 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)"]
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
`/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 creates no artifacts and writes no code. It's a free, no-stakes conversation that turns a vague worry into a precise change, so the proposal that follows is sharp. Already know exactly what you want? Skip it and go straight to `/opsx:propose`. Full guide: [Explore First](explore.md).
### 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
@@ -408,18 +531,22 @@ For full command details and options, see [Commands](commands.md).
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:
- **`## 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. 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.
- Windsurf has been [rebranded to **Devin Desktop**](https://docs.devin.ai/desktop/devin-desktop-faq) as of June 2, 2026. Same IDE, same editor, new brand.
- The rebrand moved the config directory: `.devin/` is now the preferred read + write location and `.windsurf/` the legacy read-only fallback, for `rules/`, `workflows/`, `skills/`, and `plans/`. OpenSpec writes only `.windsurf/`, so every Devin install lands in the deprecated path.
- Devin ships two agents. Devin Desktop (Cascade) reads workflows; the [Devin Local agent does not](https://docs.devin.ai/desktop/devin-local) — its docs say to migrate workflows to skills, and it does not read `.windsurf/` at all. An existing Windsurf user's OpenSpec files are therefore invisible to Devin Local entirely.
- Adding `devin` as a *second* tool id alongside `windsurf` would list one product twice in the picker and leave existing users with two parallel installs. This follows the rename instead, matching what OpenSpec already did for Kimi CLI → Kimi Code.
## What Changes
- **Rename the tool, don't duplicate it.** `windsurf` is retired as a tool id; `devin` (Devin Desktop) takes its place with `skillsDir: '.devin'` and `detectionPaths: ['.devin', '.windsurf']`. The Windsurf adapter is replaced by a Devin adapter writing `.devin/workflows/opsx-<id>.md`.
- **Keep `--tools windsurf` working.** A `TOOL_ID_ALIASES` map resolves retired ids, so existing setup scripts and CI keep running; they now configure `.devin/`.
- **Migrate existing installs, with consent.** OpenSpec-managed skills (`openspec-*`) and command files (`opsx-*`) under `.windsurf/` move to `.devin/`. `openspec update` explains the rebrand and asks first; `--force` and non-interactive runs take the move. Selecting the tool during `openspec init` is itself consent. Files the user wrote are never touched.
- Route Devin's **skill** bodies and the getting-started hint through the skill-reference transformer so they say `/openspec-*`, the one invocation both Devin agents accept.
- Update the tool reference, invocation, and command-syntax tables in `docs/`, plus the website tool list.
- **Who could be affected:** a user still on a pre-rebrand Windsurf build reads only `.windsurf/`. That is why the move is offered rather than taken — declining leaves every file where it is. Declining does mean `.windsurf/` stops being refreshed, which the prompt says plainly.
- The `.devin/` directory also covers `rules/` and `plans/`. OpenSpec writes neither, so they are out of scope and untouched.
### Requirement: Migrating OpenSpec content out of a renamed tool's former directory
When a tool's directory is renamed, OpenSpec-managed content left in the former
location SHALL be moved to the current one. Content the user wrote SHALL never
be moved or deleted.
Some renames are safe to apply silently and some are not, so each former root
declares whether leaving it needs the user's consent. Kimi CLI is gone, so
`.kimi` can be vacated without asking. Windsurf's `.windsurf` cannot: a
pre-rebrand Windsurf build reads only that directory, and nothing on disk
distinguishes that user from one who took the rebrand.
#### Scenario: Moving a former directory that needs no consent
- **WHEN** `openspec init` or `openspec update` runs and OpenSpec-managed content is found under a former root marked as needing no consent, such as `.kimi`
- **THEN** move it to the tool's current directory without prompting
- **AND** report what moved
#### Scenario: Offering a move that needs consent
- **GIVEN** OpenSpec skills or command files under `.windsurf/`
- **WHEN** `openspec update` runs interactively without `--force`
- **THEN** explain that Windsurf is now Devin Desktop, that `.devin/` is the current directory, and that Devin Local does not read `.windsurf/` at all
- **AND** ask before moving anything
- **AND** on decline, leave every file untouched and state that `.windsurf/` will no longer be refreshed until it is moved
#### Scenario: Unattended runs take the move
- **WHEN** `openspec update` runs with `--force`, or non-interactively
- **THEN** perform the move without prompting, reporting what moved
#### Scenario: Selecting a renamed tool is consent
- **WHEN** `openspec init` configures a tool that has OpenSpec content under a former root
- **THEN** move that content as part of setup, rather than leaving the user with two installs of one tool
#### Scenario: Both directories already hold OpenSpec content
- **GIVEN** the same OpenSpec-managed skill or command exists under both the former and the current root
- **WHEN** the move runs
- **THEN** the copy under the current root SHALL win, rather than being merged or overwritten
- **AND** only the file OpenSpec generated SHALL be removed from the former root — for a skill directory that is `SKILL.md` alone, never the directory and whatever else it holds
- **AND** one rule SHALL govern skills and command files alike: the former copy SHALL be removed only when it is byte-identical to the surviving one
- **AND** a former copy that differs SHALL be left where it is, since the difference may be a customization
- **AND** files left behind for that reason SHALL be reported, so the user knows two copies now exist
#### Scenario: Every former file differs, so nothing is movable
- **GIVEN** every OpenSpec-managed file under the former root differs from its counterpart under the current one
- **WHEN** the move runs
- **THEN** report the files left in place, rather than staying silent because nothing moved
- **AND** NOT offer to move anything, since there is nothing movable to consent to
- **AND** NOT report a migration that did not happen
#### Scenario: One root is a symbolic link to the other
- **GIVEN** the former and current roots resolve to the same directory, as when a user symlinks one at the other to straddle the rename
- **WHEN** the move runs
- **THEN** recognize that source and destination are the same file and change nothing, rather than deleting the only copy
#### Scenario: User files survive the move
- **GIVEN** a former root also holds files the user wrote, such as a hand-written workflow beside the generated ones
- **WHEN** the move runs
- **THEN** move only the files OpenSpec generates — each skill's `SKILL.md` and command files named `opsx-*`
- **AND** delete the former directory only when the move leaves it empty
#### Scenario: A user file beside a generated skill is not carried into a directory OpenSpec prunes
- **GIVEN** a former skill directory holds `SKILL.md` alongside a file the user wrote
- **AND** OpenSpec removes whole skill directories it owns, as under commands-only delivery or for a workflow outside the active profile
- **WHEN** the move runs
- **THEN** move `SKILL.md` alone and leave the user's file under the former root
- **AND** never move the enclosing directory, which would hand that file to a later removal
#### Scenario: The move is idempotent
- **WHEN** `openspec update` runs again after a completed move
- **THEN** find nothing to migrate and report nothing
## MODIFIED Requirements
### Requirement: Path configuration for supported tools
The `AI_TOOLS` array SHALL include `skillsDir` for tools that support the Agent Skills specification.
#### Scenario: Claude Code paths defined
- **WHEN** looking up the `claude` tool
- **THEN** `skillsDir` SHALL be `.claude`
#### Scenario: Cursor paths defined
- **WHEN** looking up the `cursor` tool
- **THEN** `skillsDir` SHALL be `.cursor`
#### Scenario: Windsurf paths defined
- **GIVEN** RETIRED — Windsurf was rebranded to Devin Desktop and `windsurf` is no longer a tool id
- **WHEN** looking up the `windsurf` tool
- **THEN** no `AI_TOOLS` entry SHALL exist for it
- **AND** the id SHALL resolve to `devin`, whose `skillsDir` is `.devin` and whose `detectionPaths` still include the legacy `.windsurf`
#### Scenario: Kimi Code paths defined
- **WHEN** looking up the `kimi` tool
- **THEN** `skillsDir` SHALL be `.kimi-code`
- **AND** OpenSpec-managed skills remaining under the legacy `.kimi/skills` directory SHALL be migrated to `.kimi-code/skills` during init and update, preserving user files
#### Scenario: Hermes Agent paths defined
- **WHEN** looking up the `hermes` tool
- **THEN** `skillsDir` SHALL be `.hermes`
- **AND** `setupNote` SHALL explain that project `.hermes/skills` must be added to `skills.external_dirs` in `~/.hermes/config.yaml`
- **AND** `openspec init` and `openspec update` SHALL display the note whenever `hermes` is configured
#### Scenario: Devin Desktop paths defined
- **WHEN** looking up the `devin` tool
- **THEN** `skillsDir` SHALL be `.devin`
- **AND** workflow files SHALL be written to `.devin/workflows/opsx-<id>.md`
- **AND** `detectionPaths` SHALL include both `.devin` and the legacy `.windsurf`, so a project set up before the rebrand is still recognized
#### Scenario: Retired tool ids resolve on the command line
- **WHEN** a retired brand is named on the command line, such as `--tools windsurf`
- **THEN** it SHALL resolve to the current tool id `devin` rather than erroring as unknown
- **AND** generation SHALL write the current directory `.devin/`, not the retired one
#### Scenario: Tools without skillsDir
- **WHEN** a tool has no `skillsDir` defined
- **THEN** skill generation SHALL error with message indicating the tool is not supported
The command SHALL generate Agent Skills for selected AI tools.
#### Scenario: Generating skills for a tool
- **WHEN** a tool is selected during initialization
- **THEN** create 9 skill directories under `.<tool>/skills/`:
-`openspec-explore/SKILL.md`
-`openspec-new-change/SKILL.md`
-`openspec-continue-change/SKILL.md`
-`openspec-apply-change/SKILL.md`
-`openspec-ff-change/SKILL.md`
-`openspec-verify-change/SKILL.md`
-`openspec-sync-specs/SKILL.md`
-`openspec-archive-change/SKILL.md`
-`openspec-bulk-archive-change/SKILL.md`
- **AND** each SKILL.md SHALL contain YAML frontmatter with name and description
- **AND** each SKILL.md SHALL contain the skill instructions
#### Scenario: Devin skills reference skills rather than workflows
- **GIVEN** the Devin Local agent does not support workflows and its documentation directs users to skills instead
- **WHEN** generating skills for the `devin` tool
- **THEN** rewrite `/opsx:<id>` references in the skill body to the matching `/openspec-<skill>` invocation, which both Devin agents accept
- **AND** the getting-started hint SHALL name `/openspec-propose` rather than a workflow
- **AND** under commands-only delivery, where no Devin skills are written, both the workflow bodies and the hint SHALL fall back to `/opsx-<id>`
### Requirement: Slash Command Generation
The command SHALL generate opsx slash commands only for selected tools that have a registered command adapter, while keeping adapterless tools valid for skill generation.
#### Scenario: Generating slash commands for a tool with a registered adapter
- **WHEN** a tool with a registered command adapter is selected during initialization
- **THEN** create 9 slash command files using the tool's command adapter:
-`/opsx:explore`
-`/opsx:new`
-`/opsx:continue`
-`/opsx:apply`
-`/opsx:ff`
-`/opsx:verify`
-`/opsx:sync`
-`/opsx:archive`
-`/opsx:bulk-archive`
- **AND** use tool-specific path conventions (e.g., `.claude/commands/opsx/` for Claude)
- **AND** include tool-specific frontmatter format
#### Scenario: Selected tool has no command adapter
- **GIVEN** a selected tool has `skillsDir` configured but no registered command adapter
- **WHEN** initialization includes command generation
- **THEN** skill generation for that tool SHALL still remain valid
- **AND** command-file generation SHALL be skipped for that tool
- **AND** the command output SHALL include `Commands skipped for: <tool-id> (no adapter)`
#### Scenario: Kimi Code skips command-file generation
- **WHEN** the user selects Kimi Code during initialization
- **THEN** OpenSpec SHALL treat it as a supported tool with `skillsDir: '.kimi-code'`
- **AND** command-file generation SHALL be skipped because no Kimi adapter is registered
#### Scenario: Generating workflows for Devin Desktop
- **WHEN** the user selects Devin Desktop during initialization
- **THEN** create one workflow file per profile workflow at `.devin/workflows/opsx-<id>.md`
- **AND** include frontmatter with `name`, `description`, `category`, and `tags`
- **AND** rewrite `/opsx:<id>` references in the body to `/opsx-<id>`, the name Devin registers for a workflow file
The update command SHALL refresh existing slash command files for configured tools without creating new ones, and ensure the OpenCode archive command accepts change ID arguments.
#### Scenario: Updating slash commands for Antigravity
- **WHEN** `.agent/workflows/` contains `openspec-proposal.md`, `openspec-apply.md`, and `openspec-archive.md`
- **THEN** refresh the OpenSpec-managed portion of each file so the workflow copy matches other tools while preserving the existing single-field `description` frontmatter
- **AND** skip creating any missing workflow files during update, mirroring the behavior for Devin Desktop and other IDEs
#### Scenario: Updating slash commands for Claude Code
- **WHEN** `.claude/commands/openspec/` contains `proposal.md`, `apply.md`, and `archive.md`
- **THEN** refresh each file using shared templates
- **AND** ensure templates include instructions for the relevant workflow stage
#### Scenario: Updating slash commands for CodeBuddy Code
- **WHEN** `.codebuddy/commands/openspec/` contains `proposal.md`, `apply.md`, and `archive.md`
- **THEN** refresh each file using the shared CodeBuddy templates that include YAML frontmatter for the `description` and `argument-hint` fields
- **AND** use square bracket format for `argument-hint` parameters (e.g., `[change-id]`)
- **AND** preserve any user customizations outside the OpenSpec managed markers
#### Scenario: Updating slash commands for Cline
- **WHEN** `.clinerules/workflows/` contains `openspec-proposal.md`, `openspec-apply.md`, and `openspec-archive.md`
- **THEN** refresh each file using shared templates
- **AND** include Cline-specific Markdown heading frontmatter
- **AND** ensure templates include instructions for the relevant workflow stage
#### Scenario: Updating slash commands for Continue
- **WHEN** `.continue/prompts/` contains `openspec-proposal.prompt`, `openspec-apply.prompt`, and `openspec-archive.prompt`
- **THEN** refresh each file using shared templates
- **AND** ensure templates include instructions for the relevant workflow stage
#### Scenario: Updating slash commands for Crush
- **WHEN** `.crush/commands/` contains `openspec/proposal.md`, `openspec/apply.md`, and `openspec/archive.md`
- **THEN** refresh each file using shared templates
- **AND** include Crush-specific frontmatter with OpenSpec category and tags
- **AND** ensure templates include instructions for the relevant workflow stage
#### Scenario: Updating slash commands for Cursor
- **WHEN** `.cursor/commands/` contains `openspec-proposal.md`, `openspec-apply.md`, and `openspec-archive.md`
- **THEN** refresh each file using shared templates
- **AND** ensure templates include instructions for the relevant workflow stage
#### Scenario: Updating slash commands for Factory Droid
- **WHEN** `.factory/commands/` contains `openspec-proposal.md`, `openspec-apply.md`, and `openspec-archive.md`
- **THEN** refresh each file using the shared Factory templates that include YAML frontmatter for the `description` and `argument-hint` fields
- **AND** ensure the template body retains the `$ARGUMENTS` placeholder so user input keeps flowing into droid
- **AND** update only the content inside the OpenSpec managed markers, leaving any unmanaged notes untouched
- **AND** skip creating missing files during update
#### Scenario: Updating slash commands for OpenCode
- **WHEN** `.opencode/commands/` contains OpenSpec-managed `opsx-*.md` command files for the configured profile (for example `opsx-propose.md`, `opsx-apply.md`, and `opsx-archive.md`)
- **THEN** refresh each file using shared templates
- **AND** transform command references to hyphen form (for example `/opsx-propose`), as for every tool whose command files are named `opsx-<id>`
- **AND** ensure templates include instructions for the relevant workflow stage
- **AND** ensure the archive command includes `$ARGUMENTS` placeholder in frontmatter for accepting change ID arguments
- **WHEN** a project still has command files under the legacy singular path `.opencode/command/` (for example `opsx-*.md` or `openspec-*.md`)
- **THEN** `openspec init` or legacy cleanup SHALL remove those files and generate replacements under `.opencode/commands/`
- **AND** `openspec update` SHALL NOT refresh files that remain only under `.opencode/command/`
#### Scenario: Updating slash commands for Windsurf
- **WHEN** the legacy Windsurf location `.windsurf/workflows/`, now Devin's, contains `openspec-proposal.md`, `openspec-apply.md`, and `openspec-archive.md`
- **THEN** refresh each file using shared templates wrapped in OpenSpec markers
- **AND** ensure templates include instructions for the relevant workflow stage
- **AND** skip creating missing files (the update command only refreshes what already exists)
#### Scenario: Updating workflows for Devin Desktop
- **WHEN** Devin Desktop is a configured tool (its `.devin/` directory exists)
- **THEN** write `.devin/workflows/opsx-<id>.md` for each workflow in the active profile, from shared templates
- **AND** emit frontmatter with `name`, `description`, `category`, and `tags`
- **AND** transform command references to hyphen form (for example `/opsx-propose`), the name Devin registers for a workflow file
- **AND** refresh `.devin/skills/openspec-*/SKILL.md` with `/openspec-*` skill references, the one invocation both Devin agents accept
#### Scenario: Updating slash commands for Kilo Code
- **WHEN** `.kilocode/workflows/` contains `openspec-proposal.md`, `openspec-apply.md`, and `openspec-archive.md`
- **THEN** refresh each file using shared templates wrapped in OpenSpec markers
- **AND** ensure templates include instructions for the relevant workflow stage
- **AND** skip creating missing files (the update command only refreshes what already exists)
#### Scenario: Updating slash commands for Codex
- **GIVEN** the global Codex prompt directory contains `openspec-proposal.md`, `openspec-apply.md`, and `openspec-archive.md`
- **WHEN** a user runs `openspec update`
- **THEN** refresh each file using the shared slash-command templates (including placeholder guidance)
- **AND** preserve any unmanaged content outside the OpenSpec marker block
- **AND** skip creation when a Codex prompt file is missing
#### Scenario: Updating slash commands for GitHub Copilot
- **WHEN** `.github/prompts/` contains `openspec-proposal.prompt.md`, `openspec-apply.prompt.md`, and `openspec-archive.prompt.md`
- **THEN** refresh each file using shared templates while preserving the YAML frontmatter
- **AND** update only the OpenSpec-managed block between markers
- **AND** ensure templates include instructions for the relevant workflow stage
#### Scenario: Updating slash commands for Gemini CLI
- **WHEN** `.gemini/commands/openspec/` contains `proposal.toml`, `apply.toml`, and `archive.toml`
- **THEN** refresh the body of each file using the shared proposal/apply/archive templates
- **AND** replace only the content between `<!-- OPENSPEC:START -->` and `<!-- OPENSPEC:END -->` markers inside the `prompt = """` block so the TOML framing (`description`, `prompt`) stays intact
- **AND** skip creating any missing `.toml` files during update; only pre-existing Gemini commands are refreshed
#### Scenario: Updating slash commands for iFlow CLI
- **WHEN** `.iflow/commands/` contains `openspec-proposal.md`, `openspec-apply.md`, and `openspec-archive.md`
- **THEN** refresh each file using shared templates
- **AND** preserve the YAML frontmatter with `name`, `id`, `category`, and `description` fields
- **AND** update only the OpenSpec-managed block between markers
- **AND** ensure templates include instructions for the relevant workflow stage
- [x] 1.1 Add `src/core/command-generation/adapters/devin.ts`: `.devin/workflows/opsx-<id>.md`, frontmatter `name`/`description`/`category`/`tags` via the shared helpers in `command-generation/yaml.ts`.
- [x] 1.2 Keep the adapter a pure formatter: the `opsx-` filename prefix makes Devin a flat invocation, so the generator rewrites `/opsx:<id>` body references to `/opsx-<id>` — the name Devin registers for a workflow file.
- [x] 1.3 Delete `adapters/windsurf.ts` and its registry/barrel entries; register `devinAdapter` in their place.
## 2. Tool wiring
- [x] 2.1 Replace the `windsurf` row in `AI_TOOLS` with `devin` (`skillsDir: '.devin'`, `detectionPaths: ['.devin', '.windsurf']`). Detection, the init picker, `--tools` validation, update, and profile sync all derive from this row.
- [x] 2.2 Add `TOOL_ID_ALIASES` / `resolveToolIdAlias` in `src/core/config.ts` and apply it when parsing `--tools`, so `--tools windsurf` still resolves.
- [x] 2.3 Re-key the pre-opsx `.windsurf/workflows/openspec-*.md` entry in `LEGACY_SLASH_COMMAND_PATHS` to `devin` — that map's keys are tool ids.
- [x] 2.4 In `getTransformerForTool`, give `devin` the skill-reference transformer whenever skills are generated, so skill bodies and the getting-started hint say `/openspec-*` — the Devin Local agent has no workflows. Under commands-only delivery, fall through to the invocation rewrite.
## 3. Migration
- [x] 3.1 Replace `LEGACY_SKILLS_DIRS` with `LEGACY_TOOL_ROOTS`, each root carrying whether leaving it needs consent (`.kimi` no, `.windsurf` yes).
- [x] 3.2 Extend the move to command files, deriving the legacy path from the adapter's own `getFilePath` so no layout is hard-coded. Skip absolute paths.
- [x] 3.3 Split find from apply (`findLegacyToolMigrations` / `migrateLegacyToolDirs`) so a consent-gated move can be described before it happens.
- [x] 3.4 `openspec update`: explain the rebrand, prompt interactively, migrate under `--force` or non-interactively, and say plainly what declining costs.
- [x] 3.5 `openspec init`: treat selecting the tool as consent and migrate for the selected tools only.
## 4. Documentation
- [x] 4.1 `docs/supported-tools.md`: give Devin its own row in the authoritative "How To Invoke" table — the catch-all row would otherwise claim `/opsx-<id>` for both agents. Replace the Windsurf directory row and rewrite the footnote to cover the rename, the alias, and the migration.
- [x] 4.2 Drop `windsurf` from the `--tools` ID lists in `docs/cli.md` and `docs/supported-tools.md`, noting it is still accepted as an alias.
- [x] 4.3 Update the command-syntax tables in `docs/commands.md` and `docs/how-commands-work.md`, plus prose mentions in `faq.md`, `migration-guide.md`, `opsx.md`, and the website tool list.
## 5. Tests
- [x] 5.1 Adapter: tool id, `getFilePath`, and frontmatter. Hyphen rewriting is asserted end to end in the `generateCommand` flat-tool loop, and YAML escaping by the registry-derived parity matrix — both enroll Devin automatically.
- [x] 5.2 Detection: `.devin` and legacy `.windsurf` both resolve to `devin`; neither present means not detected.
- [x] 5.3 Alias: `--tools windsurf` writes `.devin/` and leaves no `.windsurf/`.
- [x] 5.4 Migration: skills and workflows move, user-authored files in `.windsurf/` survive, and a second run migrates nothing.
- [x] 5.5 `init`/`update`: both surfaces — `.devin/workflows/opsx-*.md` carry `/opsx-*`, `.devin/skills/openspec-*/SKILL.md` carry `/openspec-*`, and neither carries `/opsx:`.
- [x] 5.6 `getTransformerForTool` returns the skill transformer for Devin under `both`/`skills` delivery and the hyphen form under `commands`.
`.agents/skills` has become the shared, vendor-neutral location modern agent tools read. OpenSpec already carried an `agents` entry in `AI_TOOLS`, but with `available: false` and no `skillsDir` it was unreachable — every real gate keys off `skillsDir`. Teams running several agents on one repo, or a tool with no first-class integration yet, had to generate for some other tool and move the files by hand (#1480), or pick a vendor target they do not use (#1104, #653).
## What Changes
- Enable `agents` in `AI_TOOLS` with `skillsDir: '.agents'`, making it selectable interactively and via `--tools agents`.
- Scope detection to `detectionPaths: ['.agents/skills']` so a bare `.agents/` written by another framework does not select — or silently install into — the target.
- Rename the entry to `Shared .agents skills`. The old label said "AGENTS.md", but OpenSpec writes no `AGENTS.md` — it strips its markers out of one.
- Document the target, including when to prefer it over a tool-specific integration.
## Capabilities
### New Capabilities
_None._
### Modified Capabilities
-`ai-tool-paths`: define the `.agents` skills root and its scoped detection path
-`cli-init`: record that the shared target installs skills and skips command generation
## Impact
-`src/core/config.ts` - enable the `agents` entry, scope detection, correct the label
-`.changeset/add-agents-tool.md` - minor release note, including the `--tools all` behavior change
-`docs/supported-tools.md`, `docs/cli.md`, `docs/commands.md`, `docs/how-commands-work.md`, `docs/troubleshooting.md` - list `agents` among skills-only tools and explain when to choose it
-`test/core/*`, `test/commands/*`, `test/cli-e2e/*` - cover init, update, detection, and the deprecated alias
## Non-Goals
- No command adapter for `agents`. There is no cross-vendor slash-command format, so commands stay skills-only (the Kimi/Hermes pattern).
- No `.pi`, `.codex`, or `.agent` migration into `.agents`. Moving vendor tools to the shared root is separate work (#830, #1157).
OpenSpec SHALL provide a vendor-neutral `agents` tool target rooted at the shared `.agents` directory, for assistants that read skills from the shared location rather than a vendor-specific one.
#### Scenario: Shared agents target paths defined
- **WHEN** looking up the `agents` tool
- **THEN** `skillsDir` SHALL be `.agents`
#### Scenario: Detection keys off the shared skills subtree
- **WHEN** a project contains a `.agents/skills` path
- **THEN** OpenSpec SHALL detect `agents` as an available target
#### Scenario: A bare shared root does not select the target
- **GIVEN** a project contains `.agents` but no `.agents/skills` path
- **WHEN** OpenSpec detects available tools
- **THEN** `agents` SHALL NOT be reported as available
- [x] 1.1 Cover `agents` init, update, detection, and the deprecated `experimental --tool` alias
- [x] 1.2 Assert a bare `.agents/` directory does not select the target
## 2. Registry
- [x] 2.1 Enable `agents` in `src/core/config.ts` with `skillsDir: '.agents'`
- [x] 2.2 Scope detection with `detectionPaths: ['.agents/skills']`
- [x] 2.3 Rename the entry to `Shared .agents skills` so it names the directory instead of a file OpenSpec never writes
## 3. Docs
- [x] 3.1 Add `agents` to the tool ID lists in `docs/cli.md` and `docs/supported-tools.md`
- [x] 3.2 Add the Tool Directory row and the skills-only invocation rows across `docs/supported-tools.md`, `docs/commands.md`, `docs/how-commands-work.md`, and `docs/troubleshooting.md`
- [x] 3.3 Document when to choose the shared target over a tool-specific integration
## 4. Verification
- [x] 4.1 Run `pnpm run build` and the full Vitest suite
- [x] 4.2 Validate with `openspec validate --strict`, and confirm `openspec archive` applies cleanly against a scratch copy of `openspec/`
Every generated OpenSpec skill drives the `openspec` CLI (`openspec list`, `status`, `instructions`, …). Today the skill frontmatter never pre-approves those calls, so agents that gate Bash on permission prompt the user on every single `openspec` invocation. The workflow stalls on approvals for a first-party, read-mostly CLI the user already opted into by installing OpenSpec.
The Agent Skills standard already solves this: an `allowed-tools` frontmatter field pre-approves listed tools while a skill is active. We just aren't emitting it.
## What Changes
- Every generated `SKILL.md` gains `allowed-tools: Bash(openspec:*)` in its YAML frontmatter, so agents run `openspec` commands from the skill without prompting. Emitted centrally in `generateSkillContent`, so `init`, `update`, every tool's skills directory, and every current and future skill get it uniformly.
- Claude Code slash commands (`.claude/commands/opsx/*.md`) gain the same field — commands share the skill frontmatter contract, so the same pre-approval applies when a user runs `/opsx:*`.
- Scope is deliberately narrow: only the `openspec` CLI is pre-approved. Per the standard, `allowed-tools` pre-approves rather than restricts — so any other tool a skill or command uses (Read, Write, or arbitrary Bash for builds/tests in `apply`/`onboard`) stays available under the user's normal permission settings, still prompting as before.
- Cross-tool: skills go to every supported tool's skills directory, and `allowed-tools` is an Agent Skills standard field — tools that implement the standard honor it; tools that don't ignore the unknown key. Only the Claude command adapter changes, because no other tool's slash-command format defines a per-command pre-approval field.
## Capabilities
### Modified Capabilities
-`cli-init`: the Skill Generation requirement now specifies the `allowed-tools` pre-approval in generated skill frontmatter.
-`command-generation`: the Claude adapter frontmatter now includes the `allowed-tools` field.
## Impact
-`src/core/shared/allowed-tools.ts` — the shared `OPENSPEC_CLI_ALLOWED_TOOLS` constant (single source for both surfaces).
-`src/core/shared/skill-generation.ts` — emit `allowed-tools` in the SKILL.md frontmatter.
-`src/core/command-generation/adapters/claude.ts` — emit `allowed-tools` in the slash-command frontmatter.
- Tests: regenerated golden skill-content hashes; new assertions that every deployed skill and the Claude command format pre-approve the CLI.
- No behavior change for agents that ignore `allowed-tools`; pure upside for agents that honor it.
The command SHALL generate Agent Skills for selected AI tools.
#### Scenario: Generating skills for a tool
- **WHEN** a tool is selected during initialization
- **THEN** create 9 skill directories under `.<tool>/skills/`:
-`openspec-explore/SKILL.md`
-`openspec-new-change/SKILL.md`
-`openspec-continue-change/SKILL.md`
-`openspec-apply-change/SKILL.md`
-`openspec-ff-change/SKILL.md`
-`openspec-verify-change/SKILL.md`
-`openspec-sync-specs/SKILL.md`
-`openspec-archive-change/SKILL.md`
-`openspec-bulk-archive-change/SKILL.md`
- **AND** each SKILL.md SHALL contain YAML frontmatter with name and description
- **AND** each SKILL.md SHALL contain the skill instructions
#### Scenario: Pre-approving the OpenSpec CLI in skill frontmatter
- **WHEN** generating a skill's YAML frontmatter
- **THEN** the frontmatter SHALL include an `allowed-tools` field with the value `Bash(openspec:*)`
- **AND** an agent that honors `allowed-tools` SHALL run `openspec` commands from the skill without prompting for approval
- **AND** because `allowed-tools` pre-approves rather than restricts, any other tool the skill uses SHALL remain available under the user's existing permission settings
-`getFilePath(commandId: string)`: returns file path for command (relative from project root, or absolute for global-scoped tools like Codex)
-`formatFile(content: CommandContent)`: returns complete file content with frontmatter
#### Scenario: Claude adapter formatting
- **WHEN** formatting a command for Claude Code
- **THEN** the adapter SHALL output YAML frontmatter with `name`, `description`, `allowed-tools`, `category`, `tags` fields
- **AND** the `allowed-tools` field SHALL have the value `Bash(openspec:*)` so Claude Code runs `openspec` commands from the slash command without prompting for approval
- [x] 1.1 Add the shared `OPENSPEC_CLI_ALLOWED_TOOLS = 'Bash(openspec:*)'` constant (`src/core/shared/allowed-tools.ts`) and emit `allowed-tools` in the frontmatter built by `generateSkillContent`
- [x] 1.2 Emit the same `allowed-tools` field in the Claude command adapter's frontmatter (`src/core/command-generation/adapters/claude.ts`); other adapters unchanged — no other tool defines a per-command pre-approval field
## 2. Tests
- [x] 2.1 Regenerate the golden generated-content hashes in `skill-templates-parity.test.ts`
- [x] 2.2 Add a test asserting every deployed skill's generated content contains `allowed-tools: Bash(openspec:*)` (iterates the registry so new skills are covered)
- [x] 2.3 Assert the Claude adapter output contains the field (`adapters.test.ts`)
- [x] 2.4 Verify end-to-end: `openspec init --tools claude` emits the field in both SKILL.md and `.claude/commands/opsx/*.md`, and it parses as the YAML string `Bash(openspec:*)`
## 3. Release
- [x] 3.1 Add a changeset describing the auto-approval
OpenSpec currently assumes command delivery maps directly to command adapters. That assumption does not hold for all tools.
Trae is a concrete example: it invokes OpenSpec workflows via skill entries (for example `/openspec-new-change`) rather than adapter-generated command files. In this model, skills are the command surface.
Some tools expose OpenSpec workflows via skill entries rather than adapter-generated command files. Kimi CLI is a concrete example: it invokes skills with forms such as `/skill:openspec-new-change`. In this model, skills are the command surface.
Today, this creates a behavior gap:
-`delivery=commands` can remove skills
- tools without adapters skip command generation
- result: selected tools like Trae can end up with no invocable workflow artifacts
- result: selected tools like Kimi CLI, ForgeCode, or Mistral Vibe can end up with no invocable workflow artifacts
This is more than a prompt UX issue because non-interactive and CI flows bypass interactive guidance. We need a capability-aware model in core generation logic.
@@ -25,9 +25,13 @@ Add an optional field in tool metadata to describe how a tool exposes commands:
Field should be optional. Default behavior is inferred from adapter registry presence: tools with a registered adapter resolve to `adapter`; tools with no adapter registration and no explicit annotation resolve to `none`.
Capability values use kebab-case string tokens for consistency with serialized metadata conventions.
Initial explicit override:
Initial explicit overrides:
-Trae -> `skills-invocable`
-ForgeCode -> `skills-invocable`
- Kimi CLI -> `skills-invocable`
- Mistral Vibe -> `skills-invocable`
Trae no longer belongs in this override set once its `.trae/commands/opsx-<id>.md` adapter is available; it should resolve to `adapter` like other file-backed command integrations.
### 2. Make delivery behavior capability-aware
@@ -62,12 +66,12 @@ Update summaries to show effective delivery outcomes per tool (for example, when
### 4. Update docs and tests
- document capability model and Trae behavior under delivery modes
- document capability model and skills-invocable behavior under delivery modes
- ensure CLI docs and supported-tools docs reflect effective behavior
- add test coverage for:
-`init --tools trae` with `delivery=commands`
-`update` with Trae configured under `delivery=commands`
- mixed selections (`claude + trae`) across all delivery modes
-`init --tools kimi` with `delivery=commands`
-`update` with Kimi CLI configured under `delivery=commands`
- mixed selections (`claude + kimi`) across all delivery modes
- explicit error path for tools with no command surface under `delivery=commands`
### 5. Coordinate with install-scope behavior
@@ -94,7 +98,7 @@ Implementation tests should cover mixed-tool matrices to ensure deterministic be
## Impact
-`src/core/config.ts` - add optional command-surface metadata and Trae override
-`src/core/config.ts` - add optional command-surface metadata and skills-invocable tool overrides
-`src/core/command-generation/registry.ts` (or shared helper) - capability inference from adapter presence
OPSX models a change as a small DAG of planning artifacts. Each schema declares artifacts with `requires` edges ([schemas/spec-driven/schema.yaml](../../../schemas/spec-driven/schema.yaml)); `ArtifactGraph` ([src/core/artifact-graph/graph.ts](../../../src/core/artifact-graph/graph.ts)) topologically sorts them, and `openspec status --change <id> --json` already reports, per artifact: its `status` (`done`/`ready`/`blocked`), its `outputPath`, and — via the top-level `artifactPaths` map — its `resolvedOutputPath` and `existingOutputPaths`, plus the change's `schemaName` and `isComplete`. The two path fields differ in a way that matters for a write operation: `existingOutputPaths` is the concrete files that exist on disk (for a glob artifact such as `specs/**/*.md`, the glob already expanded to real files); `resolvedOutputPath` is the change-dir-joined declared path, which for a glob artifact is still the glob (`.../specs/**/*.md`) and is therefore **not** a write target. `/opsx:update` edits the files in `existingOutputPaths`. `openspec list --json` lists changes by recency.
That is everything an update skill needs. The artifacts are a handful of markdown files on disk; the agent can read them. So `/opsx:update` is built as a thin skill over the **existing** CLI, in the same shape as `continue-change.ts` (select change → `openspec status --json` → act).
This proposal began larger — a reverse-dependency graph API, content digests, a baseline ledger, a `reconcile` write op, a `status --impact` selector. Review feedback ([PR #1278](https://github.com/Fission-AI/OpenSpec/pull/1278)) was that this over-builds: coding agents tend to over-complicate skills, and the feature should work off the existing `status` command with as little new code as possible. This design follows that steer.
## Goals / Non-Goals
**Goals**
- A `/opsx:update` action that revises a change's existing planning artifacts and keeps them coherent with one another.
- Drive it from the artifact set and paths the CLI already reports — zero hardcoded artifact names — so custom schemas work.
- Edit planning artifacts only; never touch code. Confirm every edit with the user.
- Add as little code as possible: one skill template, no changes to the graph engine, the `status` command, or the metadata schema.
**Non-Goals**
- A new top-level `openspec update*` CLI verb (name is taken; see Naming).
- Automatic, unattended regeneration (the user always confirms).
- Content digests, a drift/staleness signal, a baseline ledger, a `reconcile` op, or a `status --impact` selector (see "Why not the heavier machinery").
- Regenerating *code* from updated artifacts — that is `/opsx:apply`'s job; `/opsx:update` stops at the plan and hands off.
- Cross-change audit ([#247](https://github.com/Fission-AI/OpenSpec/issues/247) in full) — a later proposal; this change is intra-change.
- Updating anything other than a change's planning artifacts. v1 is specific to change proposals; generalizing "update" to other graph types is deferred until such a graph exists (see Naming).
## The skill, written by hand
Working backwards from "what is the minimal instruction set," here is the skill body in sketch form. It is short on purpose — few tokens, few commands:
```
Revise a change's planning artifacts and keep them coherent. Never edit code.
1. Resolve the change.
- If named, use it. Else infer from context, or auto-select the only active change;
if still unclear, run `openspec list --json` and ask the user to choose
(most-recently-modified first). Announce the selection and how to override.
2. Get the artifacts.
- Run `openspec status --change "<id>" --json`.
- Read `artifacts[]` (ids + status) and the `artifactPaths` map. These come from the
active schema — do not assume the artifact ids or paths.
- The files to edit are `artifactPaths.<id>.existingOutputPaths` (already glob-expanded
for artifacts like `specs/**/*.md`). Do not write to `resolvedOutputPath`: for a glob
artifact it is still the glob pattern, not a real file.
3. Understand the request.
- If the user named a change ("the design now uses X"), that is the starting edit.
- If they only said "update" / "make this coherent," treat it as a coherence review.
4. Read and reconcile.
- Read the artifact(s) the request touches and the other existing artifacts in the change.
- Apply the requested edit. Then check every other existing artifact against it — in any
direction (an edit to design may require revising the proposal, not only the tasks) —
and note what is now inconsistent, missing, or contradictory.
- Do not invent artifacts that don't exist yet; point the user to `/opsx:continue` to create them.
5. Confirm and apply, one artifact at a time.
- Show each proposed revision and why. Write only after the user confirms.
- When a substantial rewrite is needed, `openspec instructions <artifact> --change "<id>" --json`
gives that artifact's rules/template to follow.
6. Point to the next step (guidance only — never act on it).
checked off / applied) → the code may no longer match the revised plan; suggest
`/opsx:apply` to carry the delta. Fully done and implemented → suggest `/opsx:archive`.
Guardrails:
- Planning artifacts only. If the plan now implies code changes, stop and point to `/opsx:apply`.
- Use artifact ids/paths from `openspec status`; never branch on literal proposal/specs/design/tasks names.
- If the request changes the change's *intent* rather than refining it, recommend `/opsx:new`
(the "Update vs. Start Fresh" heuristic, docs/opsx.md).
```
The `spec-driven` artifact names may appear once, as a worked *example* of how to apply step 4, exactly as `continue-change.ts` does today — but the control flow reads ids from the CLI, so the skill never branches on those names. A template test asserts there is no name-based branching (the anti-[#777](https://github.com/Fission-AI/OpenSpec/issues/777) guard).
## Decisions
### 1. Bidirectional coherence, not downstream propagation
The artifact graph has a build *order*, but "what needs updating after an edit" is not strictly downstream. If `design` changes, the `proposal` it elaborates may need to change too; if `tasks` reveal a missing capability, the `specs` may need a new requirement. The skill therefore reads the change's artifacts and reconciles them in whatever direction the edit demands. Build order is still useful as a default *reading* order and for presenting fixes, but it is not a constraint on which artifacts may be revised. This is why the design does not add a one-directional `getDownstream` / `--impact` primitive: it would encode the wrong model.
### 2. Lean on the existing `status` command
`openspec status --change <id> --json` already returns the artifact set, per-artifact status, and, in the `artifactPaths` map, the on-disk paths. The skill writes to `artifactPaths.<id>.existingOutputPaths` — the concrete files, glob-expanded — and deliberately not to `resolvedOutputPath`, which for a glob artifact is the pattern itself and not a file. That is everything the skill needs to know what exists and where it lives; no new CLI field is required. Picking the change reuses `openspec list --json`, exactly like `/opsx:continue`. No new CLI surface is introduced.
### 3. Why not the heavier machinery (digests, ledger, reconcile, impact)
The first draft proposed SHA-256 content digests, a per-change baseline ledger in `.openspec.yaml`, an `openspec reconcile` write op, a derived drift signal, and a `status --impact` selector — so the CLI could tell the agent *which* artifacts are stale without the agent reading them.
Rejected for v1, because the cost outweighs the need:
- The artifacts are a few markdown files. An agent that is going to *rewrite* them must read them anyway, so computing staleness for it saves little and adds a stateful subsystem (a ledger that `status` must not mutate, a separate write verb, scheme-versioning for forward-compat, cross-platform digest canonicalization, and the round-trip tests for all of it).
- A digest/ledger only earns its keep when something must judge staleness *without* reading content — e.g. unattended drift detection across many changes ([#247](https://github.com/Fission-AI/OpenSpec/issues/247) cross-change, [#846](https://github.com/Fission-AI/OpenSpec/issues/846) tracking files). Those are out of scope here. When one of them becomes concrete, this machinery can be designed against that real need.
So `/opsx:update` v1 has the agent read the change's artifacts and judge coherence directly. If, after using it, a deterministic signal proves necessary, the smallest first step is to expose the schema's `requires` edges on `status --json` (a single additive field, no new command) — and only then consider digests.
### 4. Naming: `/opsx:update` skill, not `openspec update` CLI
`openspec update [path]` already regenerates AI tool/skill files ([src/cli/index.ts](../../../src/cli/index.ts)). Overloading it would give one verb two unrelated meanings. The artifact-update action is therefore the **skill**`/opsx:update`, with no new `openspec` verb at all. Considered and rejected: `openspec regen --from <artifact>` ([#705](https://github.com/Fission-AI/OpenSpec/issues/705)) — a mutating CLI verb that rewrites artifacts duplicates the skill's job and bypasses user confirmation; the value is in the agent's semantic revision, not a CLI rewrite.
Review feedback flagged that "update" alone is generic — could it apply to any graph? The resolution: the skill is scoped to **change proposals only**, and the specific name carries that scope. The skill is `openspec-update-change`, following the `openspec-<verb>-change` naming of its siblings (`openspec-continue-change`, `openspec-new-change`, …). The command is `/opsx:update` because every verb in the `/opsx:` family operates on a change (`continue`, `apply`, `archive` — none says `-change`); a change-scoped meaning is what the namespace already promises. If a future graph type needs its own update action, it gets its own specific skill name then — nothing here blocks or breaks that.
### 5. Guardrails (the part that makes it the requested command)
- **Planning artifacts only.** The skill's write targets are the artifact paths from `status`; if a revision implies code changes it stops and points to `/opsx:apply`. This directly answers [#1188](https://github.com/Fission-AI/OpenSpec/issues/1188)'s complaint that the manual workaround edits code.
- **Schema-driven.** Ids and paths come from `status`; no branching on literal `proposal`/`specs`/`design`/`tasks`. Works for custom schemas ([#777](https://github.com/Fission-AI/OpenSpec/issues/777), [#666](https://github.com/Fission-AI/OpenSpec/issues/666)).
- **Confirm each edit.** One artifact at a time, shown before writing.
- **Intent guard.** A revision that changes intent rather than refining it is redirected to `/opsx:new` (the "Update vs. Start Fresh" heuristic, [docs/opsx.md](../../../docs/opsx.md)).
### 6. Next-step guidance, especially for already-implemented changes
A change can be revised after it was built — tasks checked off, `/opsx:apply` already run. The update itself behaves identically (planning artifacts only), but stopping silently would strand the user: the code and the revised plan now disagree. So the skill ends by reporting where the change stands (from the status JSON and the tasks checklist) and recommending the next command — `/opsx:continue` if artifacts are missing, `/opsx:apply` to carry a revised plan into code, `/opsx:archive` when everything is done. Guidance only: the skill never implements, mirroring the "All artifacts created! You can now implement this change with `/opsx:apply`" hand-off that `continue-change.ts` already uses.
## Risks / Trade-offs
- **No deterministic staleness signal.** With no digest/ledger, the skill relies on the agent reading the artifacts to spot incoherence. Trade-off accepted: an agent that rewrites prose must read it anyway, and a content-blind signal earns its cost only for use cases this change excludes (Decision 3).
- **Coherence quality depends on the agent.** Mitigated by confirming every edit and by keeping scope to one change's artifacts (a small, readable set).
- **Skill drifts back to hardcoding artifact names.** Mitigated by a template test asserting the control flow reads ids from `status` JSON and contains no name-based branching.
## Migration Plan
Additive and backward-compatible. One new skill template, installed with the default `core` profile (maintainer call on the PR: update is part of the default happy path, not expanded-only); one docs row. No existing command changes behavior; no schema or graph changes. The superseded stub (`add-artifact-regeneration-support`) is removed or folded in the same PR to avoid two competing proposals in the tree.
OPSX names **four** first-class actions — "create, implement, **update**, archive — do any of them anytime" ([docs/opsx.md:52](../../../docs/opsx.md)). Three ship as commands. **`update` does not exist.** The only mechanism offered is *"edit the files manually"* — and when you edit one artifact, nothing helps you keep the rest of the change coherent. Worse, the manual workaround lets the agent edit **code** when the user only wanted to revise the **plan** ([#1188](https://github.com/Fission-AI/OpenSpec/issues/1188)).
This is the most-requested missing capability in the tracker. It is one gap with several faces, and the fix is small: a thin `/opsx:update` skill that revises a change's planning artifacts and keeps them coherent with each other, built on the **existing**`openspec status` / `openspec list` commands. No new graph engine, no digests, no ledger — just an agent that reads the change's artifacts and updates what needs updating, with the user's confirmation.
## What Changes
The whole feature is a single new workflow skill, `/opsx:update`. The skill is deliberately change-scoped — `openspec-update-change`, following the `openspec-<verb>-change` naming of its siblings — and applies to change proposals only, not arbitrary artifact graphs (see design, Naming). Written by hand, its instruction set is short:
1.**Understand the request** — what the user wants to revise (or, with no specific ask, "review this change for coherence").
2.**Get the artifacts** — run `openspec status --change <id> --json`. Its `artifactPaths` map reports, per artifact, which files exist and where: `existingOutputPaths` is the concrete file list to edit — already expanded for glob artifacts like `specs/**/*.md`. (`openspec list --json` to pick the change when it isn't given.)
3.**Read and revise** — read the relevant artifacts, make the requested edit, then check the change's **other** artifacts against it and propose any follow-on edits needed to keep the plan coherent.
4.**Confirm and apply** — show each proposed revision, write only after the user confirms.
5.**Point to the next step** — report where the change now stands and recommend what comes next: artifacts still missing → `/opsx:continue`; plan revised after the change was already implemented → `/opsx:apply` to carry the delta into code; everything done and implemented → `/opsx:archive`. Guidance only — the skill never acts on it.
Two guardrails make it the command the cluster asked for:
- **Planning artifacts only, never code.** If a revised plan implies code changes, it hands off to `/opsx:apply` ([#1188](https://github.com/Fission-AI/OpenSpec/issues/1188)).
- **Schema-driven, not name-driven.** Artifact ids and paths come from `openspec status`, so the skill works for custom schemas, not just the default `proposal → specs → design → tasks` ([#777](https://github.com/Fission-AI/OpenSpec/issues/777), [#666](https://github.com/Fission-AI/OpenSpec/issues/666)).
**Coherence is bidirectional.** Earlier framing treated update as strictly "downstream" propagation. That is wrong: in `proposal → specs → design → tasks`, editing `design` can require revising `proposal` too. The skill reads the change's artifacts and reconciles them in whatever direction the edit demands, rather than assuming a fixed flow.
### Deliberately not built (yet)
Per the steer to introduce as little code as possible, and only when there is a defined need, this change does **not** add: a reverse-dependency graph API, content digests / staleness signals, a `.openspec.yaml` baseline ledger, an `openspec reconcile` write op, a drift report, or a `status --impact` selector. The agent reads the change's artifacts directly — a handful of markdown files — which is enough to judge coherence. If a future, concrete need emerges (e.g. unattended drift detection across many changes), exposing the schema's `requires` edges on `openspec status --json` is a one-field additive follow-up. It is out of scope here.
## Capabilities
### New Capabilities
-`opsx-update-skill`: A new `/opsx:update` workflow skill that revises a change's existing planning artifacts and keeps them coherent with one another. It reads the artifact set and paths from `openspec status`, reviews related artifacts in any direction (not only downstream), edits planning artifacts only and never code, and confirms each edit with the user. It ends with next-step guidance — recommending `/opsx:continue`, `/opsx:apply`, or `/opsx:archive` based on the change's state — without acting on it.
## Impact
-`src/core/templates/workflows/update-change.ts` (**new**) — the `openspec-update-change` skill template and the `/opsx:update` command template, mirroring the structure of `continue-change.ts`. Reads artifact ids and paths from `openspec status --json`; embeds no artifact-name patterns.
- Skill/command registration + [src/core/profiles.ts](../../../src/core/profiles.ts) — add `update` to `ALL_WORKFLOWS`**and to the default `core` profile** (`propose`, `explore`, `apply`, `sync`, `archive`), so `/opsx:update` is part of the default install rather than expanded-only (maintainer call on the PR).
-`docs/opsx.md` — add a `/opsx:update` row to the command table and a short "Updating a change" usage note.
-`openspec/changes/add-artifact-regeneration-support/` — the in-repo proposal-only stub for this gap is superseded; retire it or fold its notes into design.
- No changes to `src/core/artifact-graph/*`, `src/commands/workflow/status.ts`, or `ChangeMetadataSchema`. The skill uses `openspec status` / `openspec list` as they exist today.
## Issues addressed
Verified against `Fission-AI/OpenSpec` on 2026-06-30.
Closes (the missing-update-action family):
- [#1188](https://github.com/Fission-AI/OpenSpec/issues/1188) — "Add a command to update proposal, design and task" (and stop it editing code). Delivered as `/opsx:update`, planning-artifacts-only.
- [#705](https://github.com/Fission-AI/OpenSpec/issues/705) — "Rebuild downstream artifacts from a modified upstream." Delivered as the skill's read-and-reconcile pass over the change's artifacts.
- [#673](https://github.com/Fission-AI/OpenSpec/issues/673) — "clarify": update existing artifacts without auto-advancing the build frontier. `/opsx:update` revises in place and never creates the next artifact.
- [#247](https://github.com/Fission-AI/OpenSpec/issues/247) — "review and update all change proposals." Delivered as the within-a-change coherence review; cross-change audit is a separate, later proposal.
Answers (questions whose honest answer today is "no command exists"):
- [#694](https://github.com/Fission-AI/OpenSpec/issues/694), [#684](https://github.com/Fission-AI/OpenSpec/issues/684), [#618](https://github.com/Fission-AI/OpenSpec/issues/618) — "which command regenerates a document after the flow progressed / after apply?" → `/opsx:update`.
- Discussion [#1206](https://github.com/Fission-AI/OpenSpec/discussions/1206) — the official answer becomes `/opsx:update`.
Supersedes:
-`openspec/changes/add-artifact-regeneration-support` (in-repo, proposal-only stub) — same problem, replaced by this skill. Its hardcoded-filename dependency tracking and metadata-file staleness mechanism are dropped in favor of letting the agent read the artifacts.
Delineated from adjacent commands (distinct surfaces — coordinate, don't collide):
- [#702](https://github.com/Fission-AI/OpenSpec/pull/702) `/opsx:clarify` — resolves ambiguity *within one artifact* via Q&A; a complementary upstream step. `/opsx:update` then reconciles the change's artifacts with each other.
- [#1251](https://github.com/Fission-AI/OpenSpec/pull/1251) `/opsx:review`, [#880](https://github.com/Fission-AI/OpenSpec/issues/880) — review the *implementation (code)* against the plan. `/opsx:update` is the mirror image: it keeps the *plan* coherent and never touches code.
- [#783](https://github.com/Fission-AI/OpenSpec/issues/783) — cross-artifact quality review. The skill's coherence pass is the lightweight form of this; a deterministic `validate`-side check is a separate proposal.
The system SHALL provide a `/opsx:update` workflow skill that revises a change's existing planning artifacts in place. It SHALL NOT advance the build frontier (it does not create a not-yet-started artifact) and SHALL edit planning artifacts only, never implementation code.
#### Scenario: Select the change to update
- **WHEN** the user invokes `/opsx:update` without a change name
- **THEN** the skill infers the change from conversation context if possible, or auto-selects the change when only one active change exists
- **AND** if it is still ambiguous, it lists available changes (most-recently-modified first) via `openspec list --json` and asks the user to choose
- **AND** it announces which change was selected and how to override
#### Scenario: Revise without advancing the frontier
- **WHEN** the user asks `/opsx:update` to revise an existing artifact
- **THEN** the skill updates that artifact and reconciles the change's other existing artifacts with it
- **AND** it does NOT create any artifact that does not yet exist (that remains the job of `/opsx:continue`/`/opsx:propose`)
#### Scenario: Missing artifacts are deferred to continue
- **WHEN** keeping the change coherent would require an artifact that has not been created yet
- **THEN** the skill revises only the artifacts that currently exist
- **AND** it notes the not-yet-created artifacts and points the user to `/opsx:continue` to create them
#### Scenario: Update stays within the plan
- **WHEN** revising artifacts would imply changes to implementation code
- **THEN** the skill updates the planning artifacts only
- **AND** it directs the user to `/opsx:apply` to carry the revised plan into code, rather than editing code itself
The `/opsx:update` skill SHALL learn which artifacts exist and where they live by reading the change's status from the CLI, and SHALL NOT rely on hardcoded artifact names or assumed path separators. This makes the skill correct for custom schemas and on every platform, not only the default `spec-driven` schema.
#### Scenario: Reads the artifact set from status
- **WHEN** the skill needs to know which artifacts a change has and where they are
- **THEN** it runs `openspec status --change <id> --json` and uses the reported artifact ids, statuses, and the `artifactPaths` map (`existingOutputPaths` for the files to edit)
- **AND** it does not assume the artifact ids or output paths
#### Scenario: Does not branch on hardcoded artifact names
- **WHEN** the skill decides which artifacts to read and revise
- **THEN** its control flow uses the ids reported by the CLI
- **AND** it does not branch on literal `proposal`/`specs`/`design`/`tasks` names
#### Scenario: Works for a custom schema
- **WHEN** the active change uses a custom schema whose artifact ids are not `proposal`/`specs`/`design`/`tasks`
- **THEN** the skill uses the artifact ids and paths reported by the CLI
- **AND** it works without any change to the skill
- **WHEN** the skill reads or writes an artifact on macOS, Linux, or Windows
- **THEN** it uses the `existingOutputPaths` provided by the CLI status output
- **AND** it does not assume forward-slash separators
#### Scenario: Edit the concrete files of a glob artifact
- **WHEN** an artifact's declared output path is a glob (for example `specs/**/*.md`)
- **THEN** the skill edits the concrete files reported in that artifact's `existingOutputPaths`
- **AND** it does not write to `resolvedOutputPath`, which for a glob artifact remains the glob pattern rather than a real file
#### Scenario: A new file under a glob artifact is deferred to continue
- **WHEN** keeping the change coherent would require a new file under a glob artifact that does not exist yet (for example a spec for a not-yet-captured capability)
- **THEN** the skill revises only the files already present in `existingOutputPaths`
- **AND** it points the user to `/opsx:continue`/`/opsx:propose` to create the new file rather than inventing a path from the glob
### Requirement: Bidirectional Coherence Review
The `/opsx:update` skill SHALL keep a change's existing planning artifacts coherent with one another after a revision, reviewing affected artifacts in any direction rather than assuming a fixed downstream flow.
#### Scenario: Reconcile related artifacts after an edit
- **WHEN** the user revises one artifact
- **THEN** the skill reviews the change's other existing artifacts against the revision
- **AND** it proposes follow-on edits to any artifact that is now inconsistent, whether that artifact is upstream or downstream of the edited one
#### Scenario: Upstream artifact may be revised
- **WHEN** an edit to a later artifact (for example design) contradicts an earlier one (for example the proposal)
- **THEN** the skill may propose revising the earlier artifact to restore coherence
- **AND** it does not treat propagation as downstream-only
#### Scenario: Coherence review with no specific edit
- **WHEN** the user invokes `/opsx:update` without a specific revision in mind ("make this change coherent")
- **THEN** the skill reads the change's existing artifacts and reviews them against each other for contradictions, gaps, and duplication
- **AND** it presents any findings for the user to confirm before editing
#### Scenario: Coherent change yields no changes
- **WHEN** the skill finds the change's artifacts already coherent
- **THEN** it reports the change as coherent and makes no edits
### Requirement: Next-Step Guidance
After applying confirmed revisions (or finding none needed), the `/opsx:update` skill SHALL report where the change stands and recommend the next command, without acting on the recommendation itself.
#### Scenario: Updating an already-implemented change
- **WHEN** the user updates a change whose implementation already happened (for example tasks are checked off or `/opsx:apply` was already run)
- **THEN** the skill still revises planning artifacts only
- **AND** it notes that the implementation may no longer match the revised plan and recommends `/opsx:apply` to carry the delta into code
- **AND** it does not implement anything itself
#### Scenario: Next step when artifacts are incomplete
- **WHEN** the update finishes and the change still has not-yet-created artifacts
- **THEN** the skill recommends `/opsx:continue` to create them
#### Scenario: Next step when the change is fully done
- **WHEN** the update finishes and the change's artifacts are complete and already implemented
> The whole feature is one new skill template over the existing `openspec status` / `openspec list` commands. No changes to the graph engine, the `status` command, or the metadata schema.
## 1. The `/opsx:update` skill
- [x] 1.1 Create `src/core/templates/workflows/update-change.ts` with `getUpdateChangeSkillTemplate()` (skill) and `getOpsxUpdateCommandTemplate()` (command), mirroring `continue-change.ts`. The skill name is `openspec-update-change` — change-scoped, per the `openspec-<verb>-change` convention (see design, Naming).
- [x] 1.2 Instruction body (see design "The skill, written by hand"): resolve the change (infer / `openspec list --json` / ask) → `openspec status --change <id> --json` → read the relevant artifacts → apply the requested edit → reconcile the change's other existing artifacts in any direction → confirm and apply one artifact at a time → end with next-step guidance (`/opsx:continue` / `/opsx:apply` / `/opsx:archive` based on the change's state; see design Decision 6), never acting on it. Read artifact ids from the status JSON only, and write to `artifactPaths.<id>.existingOutputPaths` (never to a glob `resolvedOutputPath`).
- [x] 1.3 Encode the guardrails: (a) planning artifacts only — never edit code, hand off to `/opsx:apply`; (b) schema-driven — no branching on literal `proposal`/`specs`/`design`/`tasks`; ids/paths come from `openspec status`; (c) revise only existing files (`existingOutputPaths`) — defer not-yet-created artifacts, and new files under a glob artifact, to `/opsx:continue`; (d) intent change → recommend `/opsx:new` (the "Update vs. Start Fresh" heuristic in `docs/opsx.md`).
- [x] 1.4 Register the skill/command and add `update` to `ALL_WORKFLOWS`**and the default `core` profile** in `src/core/profiles.ts` (maintainer call: default install, not expanded-only).
## 2. Docs & supersede the stub
- [x] 2.1 Add a `/opsx:update` row to the command table in `docs/opsx.md`, plus a short "Updating a change" usage note.
- [x] 2.2 Remove (or fold) `openspec/changes/add-artifact-regeneration-support/` so the tree has a single update proposal.
- [x] 2.3 Update any generated-skill manifests/fixtures that enumerate workflow skills so `openspec-update-change` is included.
## 3. Tests
- [x] 3.1 Template generation snapshot for the skill and command templates.
- [x] 3.2 Assert the template's control flow contains NO hardcoded artifact-name branching (the anti-#777 guard): artifact ids must be read from `openspec status` JSON.
- [x] 3.3 Assert the template instructs planning-artifacts-only with a hand-off to `/opsx:apply` for code, and never advances the build frontier.
- [x] 3.4 Assert the template instructs writing to `existingOutputPaths` (the glob-expanded concrete files) and not to a glob `resolvedOutputPath`.
- [x] 3.5 Assert the template ends with next-step guidance (`/opsx:continue`/`/opsx:apply`/`/opsx:archive`) and instructs the agent never to act on it.
- [x] 3.6 Assert `update` is included in the `core` profile's workflows (profiles test).
## 4. End-to-end verification
- [x] 4.1 `openspec validate add-update-workflow --strict` passes; `openspec status --change add-update-workflow` shows all artifacts complete.
- [x] 4.2 Manual walk-through: on a `spec-driven` change, edit `design`, run `/opsx:update`, confirm it proposes coherence edits to other existing artifacts (including upstream where warranted) and never touches code.
This matches Kimi CLI's project-local skills root and lets existing init/update detection paths treat it as a supported tool.
### 2. Do not add a Kimi command adapter
No `src/core/command-generation/adapters/kimi.ts` file will be added, and the command adapter registry will remain unchanged.
Rationale:
- Kimi CLI exposes skills dynamically as `/skill:<name>`
- the previous upstream PR stalled specifically because no legitimate adapter target was available
- adding a fake `.kimi/commands/...` path would create behavior OpenSpec cannot justify against upstream Kimi CLI behavior
### 3. Document Kimi by its real invocation surface
Kimi documentation in OpenSpec must use Kimi's actual skill invocation form:
- supported-tools: no generated command files, use `/skill:openspec-*`
- commands doc: examples such as `/skill:openspec-propose`
The docs must not claim generated `opsx-*` files or `/openspec-*` direct invocations for Kimi.
### 4. Keep the change compatible with existing Trae-style behavior
This change intentionally follows the current adapterless-tool behavior already present in the codebase:
- skills are created whenever delivery includes skills
- command generation is skipped when no adapter exists
- init output reports `Commands skipped for: kimi (no adapter)`
This keeps the Kimi change small and avoids overlapping implementation work already captured in `add-tool-command-surface-capabilities`.
## Test Strategy
Add one focused regression test in `test/core/init.test.ts`:
- configure `delivery=both`
- run init with `--tools kimi`
- verify Kimi skills are created under `.kimi/skills/...`
- verify init reports the skipped command generation path for `kimi`
That test is enough for this narrow change because:
- adapterless update behavior already has generic coverage
- CLI tool-id rendering is derived from `AI_TOOLS`
- no command adapter or path formatting logic is being introduced
## Risks / Trade-offs
The main trade-off is scope: Kimi will inherit the current adapterless-tool behavior, including the broader limitation that `delivery=commands` is not yet capability-aware for skills-invocable tools. That is acceptable for this change because it matches the existing Trae/ForgeCode model and keeps the implementation aligned with verified Kimi CLI behavior.
OpenSpec already has user demand for Kimi CLI support, but the previous upstream attempt stalled because it assumed Kimi needed a command adapter. Local review of the Kimi CLI codebase shows a different integration surface: Kimi discovers `SKILL.md` files from `.kimi/skills/` and exposes them through `/skill:<name>`, but it does not provide a stable, file-based custom command directory like Claude Code or Codex.
OpenSpec already supports tools that install skills without a command adapter. Trae and ForgeCode are the existing examples. Kimi should follow the same pattern instead of introducing undocumented `.kimi/commands/...` behavior.
## What Changes
- Add Kimi CLI as a supported tool in `AI_TOOLS` with `skillsDir: '.kimi'`
- Document Kimi CLI as a skills-only integration in supported tools and command usage docs
- Align change specs so `cli-init` explicitly allows selected tools with `skillsDir` but no registered command adapter
## Capabilities
### New Capabilities
_None._
### Modified Capabilities
-`ai-tool-paths`: define the `.kimi` skills root for Kimi CLI
-`cli-init`: clarify that adapterless tools remain valid selections and skip command-file generation with an informational message
## Impact
-`src/core/config.ts` - add Kimi CLI tool metadata
-`docs/supported-tools.md` - add Kimi CLI row and tool id
-`docs/commands.md` - document `/skill:openspec-*` usage for Kimi CLI
-`docs/cli.md` - include `kimi` in the supported `--tools` list
-`test/core/init.test.ts` - cover Kimi CLI as an adapterless tool during init
- Changing the broader delivery model for adapterless tools under `delivery=commands`
That broader capability-aware delivery work is already being explored separately in `add-tool-command-surface-capabilities`. This change stays narrow and follows the existing Trae/ForgeCode pattern.
The command SHALL generate opsx slash commands only for selected tools that have a registered command adapter, while keeping adapterless tools valid for skill generation.
#### Scenario: Generating slash commands for a tool with a registered adapter
- **WHEN** a tool with a registered command adapter is selected during initialization
- **THEN** create 9 slash command files using the tool's command adapter:
-`/opsx:explore`
-`/opsx:new`
-`/opsx:continue`
-`/opsx:apply`
-`/opsx:ff`
-`/opsx:verify`
-`/opsx:sync`
-`/opsx:archive`
-`/opsx:bulk-archive`
- **AND** use tool-specific path conventions (e.g., `.claude/commands/opsx/` for Claude)
- **AND** include tool-specific frontmatter format
#### Scenario: Selected tool has no command adapter
- **GIVEN** a selected tool has `skillsDir` configured but no registered command adapter
- **WHEN** initialization includes command generation
- **THEN** skill generation for that tool SHALL still remain valid
- **AND** command-file generation SHALL be skipped for that tool
- **AND** the command output SHALL include `Commands skipped for: <tool-id> (no adapter)`
#### Scenario: Kimi CLI skips command-file generation
- **WHEN** the user selects Kimi CLI during initialization
- **THEN** OpenSpec SHALL treat it as a supported tool with `skillsDir: '.kimi'`
- **AND** command-file generation SHALL be skipped because no Kimi adapter is registered
An OpenSpec workspace is the durable planning home for work that spans multiple repos or folders.
It should feel like this:
```text
workspace = where related changes live
link = a named repo or folder the workspace can plan against
change = one feature, fix, project, or other planned piece of work
```
The foundation intentionally avoids the rest of the workflow. It only defines how OpenSpec recognizes a workspace, where managed workspaces live, how linked paths are represented, and how shared state differs from local state.
A workspace is not a feature. It can hold many changes over time. The linked repos or folders provide planning context, while the code stays where it is.
repo-local project -> repo-owned specs and implementation planning
```
Users should not run repo-local `openspec init` inside the workspace root. A workspace is already an OpenSpec coordination surface; it is not a product repo adopting repo-local OpenSpec.
## Workspace Names
A workspace name is a simple folder-style identifier, not a display name.
The name must be usable as a folder name in the current runtime. It must not be empty, must not be `.` or `..`, and must not contain path separators.
OpenSpec should not maintain a cross-platform reserved-name list in this slice. Setup/create flows should let filesystem creation surface OS-specific invalid folder names, then report that failure clearly.
The same workspace name is stored in `.openspec-workspace/workspace.yaml`, used as the default managed workspace folder name, and used as the local registry name.
## Shared And Local State
Workspace state follows a simple sharing rule:
```text
share stable link names and planning
keep local checkout paths local
```
Expected shared state:
```yaml
version:1
name:platform
links:
api:{}
web:{}
```
Expected local state:
```yaml
version:1
paths:
api:/repos/api
web:/repos/web
```
Later slices can expand these shapes, but the product rule should stay stable: a shared workspace should not commit one user's absolute checkout paths.
OpenSpec-created workspaces should include an ignore rule for `.openspec-workspace/local.yaml` so local checkout paths are not accidentally shared. `.openspec-workspace/workspace.yaml` remains the portable workspace identity and link-name state.
## Workspace Location
OpenSpec should create managed workspaces in one standard place:
```text
getGlobalDataDir()/workspaces
```
That reuses existing OpenSpec data-directory behavior:
-`$XDG_DATA_HOME/openspec/workspaces` when `XDG_DATA_HOME` is set
-`~/.local/share/openspec/workspaces` on Unix/macOS fallback
-`%LOCALAPPDATA%\openspec\workspaces` on native Windows fallback
This slice intentionally does not define a workspace-specific environment-variable, command, or configuration override for managed workspace storage. Tests should rely on existing global data-directory controls and test helpers instead of a separate workspace-home override.
This is deliberately quiet. The product should not ask most users where workspaces should live.
OpenSpec should show the resolved workspace path after setup. Quiet defaults should avoid a prompt, not hide where planning files were created.
## Local Workspace Registry
OpenSpec should keep a lightweight local registry of known workspaces:
The registry is a local index, not the source of truth. It exists so workspace commands can work from anywhere, show a picker when multiple workspaces exist, and list known workspaces without scanning arbitrary folders.
Each workspace folder remains authoritative for its own `.openspec-workspace/workspace.yaml` and `.openspec-workspace/local.yaml`. If a registry entry points at a missing or invalid workspace, later check/list flows can report that and suggest a repair.
## Windows And WSL2
Path behavior is runtime-local:
- PowerShell/native Windows uses Windows paths and Windows data-directory fallback.
- WSL2 uses Linux paths and Linux/XDG fallback inside WSL.
- Local repo paths are stored as the user supplied them for the current runtime.
Examples:
```text
PowerShell:
default base -> %LOCALAPPDATA%\openspec\workspaces
WSL2:
default base -> ~/.local/share/openspec/workspaces
```
This slice should not translate between `D:\repo`, `/mnt/d/repo`, and `\\wsl$` paths. Cross-runtime translation can be reconsidered later if an agent-launch workflow requires it.
## Link Names
A link name is the stable way to refer to a repo or folder inside workspace planning.
The local path can vary by machine:
```text
shared link name: landing
Tabish path: /Users/tabish/repos/landing
Windows path: D:\repos\landing
WSL2 path: /mnt/d/repos/landing
```
Later workflows should refer to `landing` in workspace planning, status, and apply context. The local path is only how the current machine finds that repo or folder.
Link names are intentionally minimal: they must be non-empty, must not be `.` or `..`, must not contain path separators, and must be unique within the workspace.
The owning repo or folder remains the home of canonical specs and implementation work. The workspace makes the cross-boundary plan legible; it does not take ownership away from the linked repos or folders.
Link names are normally inferred from the folder basename in guided flows. Direct flows can allow an explicit name when the default would conflict or be unclear.
## Linked Repos And Folders
Workspace planning visibility should not require repo-local OpenSpec state.
That matters for two common cases:
- a repo has not adopted OpenSpec yet, but still needs to be considered in planning
- a large monorepo has folders such as packages, services, or apps that should be planned like separate areas, without each folder having its own `openspec/`
Foundation should allow the link model to describe both:
```text
multi-repo:
api -> /repos/api
web -> /repos/web
large monorepo:
billing -> /repos/platform/services/billing
checkout -> /repos/platform/apps/checkout
```
Later apply/verify/archive workflows can decide what extra readiness is needed for implementation. Planning should be able to start before that.
Linking only records the relationship between a workspace link name and a local path. It must not create, copy, move, initialize, or edit files inside the linked repo or folder.
Repo-local spec availability is computed when needed. For example, `repo_specs_path` can be reported by a later doctor command when a linked path contains `openspec/specs`, but that path should not be treated as required workspace state.
## Later Slices
This foundation stops before user-facing workspace workflows:
-`workspace-create-and-register-repos` owns setup, link, relink, list, and doctor behavior.
Users need a workspace to feel like the obvious home for planning across multiple repos or folders.
They should be able to think:
```text
I have repos or folders that are often planned together.
I create an OpenSpec workspace.
That workspace is where changes live.
My code stays where it is.
OpenSpec links the workspace to those local paths.
```
A workspace is not a feature. It is the durable planning home. Individual features, fixes, and projects are changes inside the workspace.
Users should not have to choose a storage location, create a change early, or understand internal workspace state before OpenSpec can orient itself.
The POC proved that workspace state is useful. This reimplementation should turn that into a simple product model that users and agents can explain without special-case vocabulary.
## What Changes
This change defines the user-facing foundation for OpenSpec workspaces.
An OpenSpec workspace has a recognizable planning home:
```text
workspace-root/
changes/
.openspec-workspace/
```
`changes/` is where workspace-level planning lives. `.openspec-workspace/` identifies the directory as an OpenSpec workspace and stores workspace state.
OpenSpec-managed workspaces live in one standard location:
```text
<global-data-dir>/workspaces/
```
Users should not need to choose that location. OpenSpec still shows the workspace path after setup so users know where planning files live. This foundation slice does not provide a workspace-specific environment-variable or configuration override for managed workspace storage.
OpenSpec also keeps a lightweight local registry of known workspaces on the current machine. The registry powers global commands, pickers, and listing, but each workspace folder remains the source of truth.
Workspace state is split by user expectation:
- shared workspace information can move between machines
- local checkout paths stay local to each machine
- linked repos and folders are referred to by stable link names, not by absolute paths
A linked path can be a full repo, a folder inside a monorepo, or another existing folder the workspace should plan against. A linked path does not need repo-local `openspec/` state before it can be included in workspace planning. Repo-local OpenSpec state may still matter later for implementation, verification, or archive workflows, but it is not a prerequisite for planning visibility.
Native Windows/PowerShell and WSL2 are both supported. Each runtime uses its own path conventions. OpenSpec does not translate paths between Windows and WSL in this foundation slice.
## Outcome
After this change, later workspace features can rely on one clear product contract:
- OpenSpec can tell when the user is inside a workspace.
- OpenSpec knows where to create managed workspaces by default.
- OpenSpec can keep a local registry of known workspaces.
- A workspace has one visible planning area: `changes/`.
- Workspace state is distinguishable from repo-local `openspec/` state.
- Shared workspace state does not force one user's local paths onto another user.
- Workspace planning can reference existing repos or folders by stable link names.
- Linked repos or folders do not need repo-local OpenSpec state for workspace planning.
- Multi-repo and large-monorepo work can use the same workspace planning model.
- Repo-owned specs and implementation remain owned by their repos or source areas.
- Windows, PowerShell, and WSL2 path behavior is predictable.
This change does not deliver the full workspace workflow. It gives `workspace-create-and-register-repos` the foundation it needs to add the first user-facing commands.
## POC Findings
Behavior to preserve:
- A workspace is a durable coordination home for cross-repo planning.
- The workspace has a visible `changes/` directory at its root.
- Linked repos and folders provide the context the workspace can plan against.
- Stable link names matter more than local checkout paths.
- Local machine paths should not become shared workspace state.
- Canonical specs and implementation still belong to the owning repos.
Lessons to carry forward:
- The POC's hidden `.openspec/` workspace metadata shape made workspace state too easy to confuse with repo-local OpenSpec state.
- Users should not need to run repo-local `openspec init` inside the workspace root.
- The POC's requirement that registered repos already have `openspec/` is too strict for planning. Repos and folders should be linkable before they adopt repo-local OpenSpec state.
- Repo or folder visibility should not depend on creating a change.
- Workspace setup should not imply repo-local implementation, branch, worktree, apply, verify, or archive behavior.
-`add-repo` is too narrow for the user-facing model. Linking an existing repo or folder is clearer.
- [x] 1.1 Capture the foundation POC findings in the proposal/design artifacts
- [x] 1.2 Settle `.openspec-workspace/` as the workspace metadata directory
- [x] 1.3 Define the minimal workspace root shape and root marker
- [x] 1.4 Define committed workspace state versus machine-local workspace state
- [x] 1.5 Capture that workspace setup is useful only after at least one repo or folder is linked
- [x] 1.6 Capture that repo-owned specs and implementation remain owned by repos
- [x] 1.7 Capture that planning can include repos or monorepo folders without repo-local OpenSpec state
- [x] 1.8 Capture that workspaces hold many changes and are not feature containers
- [x] 1.9 Capture `link`/`relink` as the user-facing model instead of `add-repo`/`update-repo`
## 2. Foundation Helpers
- [x] 2.1 Add workspace path constants and helpers for `.openspec-workspace/`, `workspace.yaml`, `local.yaml`, and root `changes/`
- [x] 2.2 Add workspace root detection from an arbitrary starting directory
- [x] 2.3 Add typed parsing and validation for minimal shared workspace state
- [x] 2.4 Add typed parsing and validation for minimal machine-local workspace state
- [x] 2.5 Ensure repo-local `openspec/` projects are not mistaken for coordination workspaces
- [x] 2.6 Add a standard workspace location resolver using `getGlobalDataDir()/workspaces`
- [x] 2.7 Ensure workspace path helpers use platform path APIs and avoid hardcoded POSIX separators
- [x] 2.8 Add local workspace registry path constants and helpers
## 3. Metadata And Local State
- [x] 3.1 Define the versioned shared-state shape with workspace name and stable link map
- [x] 3.2 Define the versioned local-state shape with stable link names mapped to local paths
- [x] 3.3 Ensure local-state files are treated as machine-local and OpenSpec-created workspaces exclude `.openspec-workspace/local.yaml` from portable collaboration state
- [x] 3.4 Add validation for invalid versions, invalid link names, malformed link maps, and malformed local path maps
- [x] 3.5 Preserve native Windows and WSL2 path strings when reading and writing local path state
- [x] 3.6 Define the versioned local registry shape with workspace names mapped to workspace roots
- [x] 3.7 Ensure the local registry is treated as a convenience index, not the workspace source of truth
## 4. Documentation And Guidance
- [x] 4.1 Document the coordination workspace mental model
- [x] 4.2 Document how `.openspec-workspace/` differs from repo-local `openspec/`
- [x] 4.3 Document stable link names as the way to refer to linked repos and folders
- [x] 4.4 Document which behavior is intentionally deferred to later workspace slices
- [x] 4.5 Document native Windows/PowerShell and WSL2 path behavior for managed workspace storage
- [x] 4.6 Document linked repos/folders without repo-local OpenSpec and large-monorepo planning behavior
- [x] 4.7 Document the local workspace registry and global command model
## 5. Verification
- [x] 5.1 Add unit tests for root detection and non-detection cases
- [x] 5.2 Add unit tests for shared-state and local-state parsing
- [x] 5.3 Add unit tests for standard workspace location resolution with XDG/Linux fallback and native Windows fallback
- [x] 5.4 Add unit tests that local-state parsing preserves native Windows and WSL2-style paths
- [x] 5.5 Add unit tests for repo-local compatibility boundaries
- [x] 5.6 Add tests or docs coverage that linked repos/folders do not require repo-local `openspec/`
- [x] 5.7 Add tests or docs coverage for monorepo folder links under the same workspace model
- [x] 5.8 Add tests for local registry parsing and stale registry entries
- [x] 5.9 Add tests or docs coverage for `.openspec-workspace/local.yaml` exclusion in OpenSpec-created workspaces
- [x] 5.10 Run `openspec validate workspace-foundation --strict`
- [x] 5.11 Run targeted test coverage for the new workspace foundation helpers
This slice is the first user-facing step after `workspace-foundation`.
The user experience should be:
```text
I set up a workspace.
I link the repos or folders it should know about.
I can list my workspaces later.
I can ask OpenSpec what is broken and how to fix it.
```
No change proposal is required yet.
## Links
A workspace link is a stable name plus a local path on the current machine.
Examples:
```text
api -> /repos/api
web -> /repos/web
checkout -> /repos/platform/apps/checkout
billing -> /repos/platform/services/billing
```
The path may point at a full repo or a folder inside a large monorepo. It may point at a repo or folder that has not adopted repo-local OpenSpec yet.
The product language should say "repos or folders". It should avoid "working set", "code area", "entry", "alias", and "local overlay" in user-facing output.
Path handling should behave like a folder picker. The user may type a relative or absolute path, but OpenSpec should verify that it points to an existing folder, convert it to an absolute path relative to the command's current working directory when needed, and store that verified absolute path in local workspace state. OpenSpec should not store the raw string the user typed.
Path conversion stays in the current runtime. Native Windows paths, WSL2 paths, and Unix paths should not be translated across runtimes. Where duplicate-path detection needs canonical comparisons, OpenSpec may compare canonical existing paths internally, but it should store and display the verified absolute path for the current runtime.
## Names
Workspace names should be kebab-case:
```text
platform
checkout-web
api2
```
Invalid workspace names include uppercase letters, underscores, dots, spaces, leading hyphens, trailing hyphens, empty names, dot names, and path separators. Interactive setup should explain the expected form and let the user retry. Non-interactive setup should fail with the same expectation in the error message.
Link names should keep the folder-style validation from `workspace-foundation`: they must not be empty, must not be `.` or `..`, must not contain path separators, and must be unique inside the workspace. This lets inferred link names match existing folder basenames without forcing users to rename local folders for workspace planning.
Link names are normally inferred from the folder basename:
```text
/repos/api -> api
/repos/platform/apps/checkout -> checkout
```
If the inferred name conflicts, interactive setup should show the conflicting name and the existing path it maps to, then ask for a different name. Non-interactive setup and direct `workspace link` should fail with a clear message instead of silently overwriting.
Duplicate-name errors should be specific:
```text
Cannot use link name 'api' because another link already uses that name.
Existing link:
api -> /repos/api
Choose a different name:
openspec workspace link archived-api /archive/api
If you meant to change the existing link path:
openspec workspace relink api /archive/api
```
This slice does not add a separate link-rename command. Renaming a link can be considered later if users need it, but v1 should keep the command model crisp: `link` adds a new link, and `relink` changes the local path for an existing link.
## Commands
### `workspace setup`
Guided onboarding:
- create a workspace in the standard workspace location
- ask for a workspace name
- require at least one existing repo or folder path
- infer link names from folder names
- let the user add more repos or folders with a simple repeated prompt
- record the workspace in the local workspace registry
- run `workspace doctor`
- print the workspace location, planning path, linked repos or folders, and next useful commands
This slice should not ask for preferred agent or open the workspace with an agent. Those belong to `workspace-open-agent-context`.
Setup should support a non-interactive mode for automation:
In non-interactive mode, setup should fail cleanly unless the user provides a valid workspace name and at least one valid link. `--link` should accept either a path, which infers the name from the folder basename, or `name=path`.
There is no public `workspace create` command in this slice. Setup is the creation flow.
### `workspace list`
Show known OpenSpec-managed workspaces from the local workspace registry.
`workspace ls` should behave the same way.
The output should answer what exists and what each workspace links to:
```yaml
workspaces:
- name:platform
location:/.../openspec/workspaces/platform
links:
- name:api
path:/repos/api
- name:web
path:/repos/web
- name:checkout
location:/.../openspec/workspaces/checkout
links:
- name:app
path:/repos/platform/apps/checkout
```
List should keep deep validation for `workspace doctor`. It can still report obviously stale workspace registry entries if a known workspace location no longer exists. Stale registry entries are report-only in this slice: `workspace list` should not delete, rewrite, or repair registry entries, and this slice should not add a `workspace forget` command.
For JSON output, list should use typed workspace objects with a structured `status` array for issues:
```json
{
"workspaces":[
{
"name":"platform",
"root":"/.../openspec/workspaces/platform",
"links":[
{
"name":"api",
"path":"/repos/api",
"status":[]
}
],
"status":[]
},
{
"name":"old-platform",
"root":"/.../openspec/workspaces/old-platform",
"links":[],
"status":[
{
"severity":"error",
"code":"workspace_root_missing",
"message":"Workspace location does not exist.",
"fix":"Remove or repair the local registry entry."
}
]
}
],
"status":[]
}
```
### `workspace link [name] <path>`
Record an existing repo or folder path for the selected workspace.
Supported forms:
```bash
openspec workspace link /path/to/api
openspec workspace link api-service /path/to/api
```
The one-argument form infers the link name from the folder basename. The two-argument form lets the user choose the link name.
The path must exist. The command should accept:
- full repo roots
- monorepo folders such as packages, services, and apps
- repos or folders without repo-local `openspec/`
If the user passes a relative path, OpenSpec should resolve it against the command's current working directory before writing local state.
If the path has repo-local OpenSpec state, OpenSpec can report the repo specs path in doctor output. If it does not, OpenSpec should still allow workspace planning.
`workspace link` only records the link. It must not create, copy, move, initialize, or edit files in the linked repo or folder.
### `workspace relink <name> <path>`
Repair or change the local path for an existing link.
Relink should use the same path handling as link: require an existing folder, resolve relative inputs to absolute runtime-local paths, and store the verified path.
This slice should keep relink focused on path repair. It should not include owner or handoff metadata; that language was too process-heavy in the POC and can be revisited later if users need contact or notes fields.
### `workspace doctor`
Explain one selected workspace from the user's machine. If the command is run from a workspace folder or subdirectory and `--workspace <name>` is not provided, doctor should use that current workspace. Otherwise it should follow the normal workspace-selection rules.
Doctor should inspect:
- workspace location
- workspace planning path
- linked repos and folders
- whether each local path exists
- repo-local specs path when present
- missing local paths
- local names that are not in shared workspace state
- shared link names that are missing local paths
- suggested fixes for each issue
Doctor should not scan every known workspace in the local registry by default. Broad registry visibility belongs to `workspace list`. A future `workspace doctor --all` can be considered later if users need global workspace diagnostics.
Doctor should report issues and suggested fixes. It should not repair anything automatically.
Registry cleanup remains out of scope. If doctor cannot inspect the selected workspace because the registry points at a missing or invalid workspace location, it should report that selected-workspace issue through status entries and stop before inspecting links. Other stale registry entries should be surfaced by `workspace list`, not by selected-workspace doctor.
Human output should be readable by default: a short workspace summary, linked repo or folder rows, and a clear issues section when anything needs attention. It should not be raw JSON or a rigid YAML dump.
JSON output should follow the object/status pattern: primary data lives in typed objects, and diagnostics live in `status` arrays. A healthy object has `status: []`. Status entries should include `severity`, `code`, `message`, and optional `target` and `fix` fields.
"fix":"openspec workspace relink web /path/to/web"
}
]
}
],
"status":[]
},
"status":[]
}
```
## Workspace Selection
Workspace commands should work from anywhere.
Commands that do not need one workspace:
-`workspace setup`
-`workspace list`
-`workspace ls`
Commands that need one workspace:
-`workspace link`
-`workspace relink`
-`workspace doctor`
If the current command needs one workspace and `--workspace <name>` is not provided:
- use the current workspace when running from inside a workspace
- otherwise show an interactive picker when multiple known workspaces exist
- otherwise select the only known workspace
- otherwise explain that no workspaces exist and suggest `openspec workspace setup`
The current workspace wins even if it is not in the local workspace registry. This supports manually created or shared workspace folders. In that case commands should continue and include a non-fatal warning status:
```json
{
"severity":"warning",
"code":"workspace_not_in_local_registry",
"message":"This workspace is not recorded in the local workspace registry.",
"target":"workspace.root",
"fix":"Run a mutating workspace command from this workspace, such as workspace link or workspace relink, to record it locally."
}
```
For human output, this should be a short warning rather than a blocking error. Successful mutating commands that use an unregistered current workspace, such as `workspace link` or `workspace relink`, should record the workspace name and location in the local registry after the mutation succeeds. Non-mutating commands such as `workspace doctor` should not write registry state; they should only report the warning. This slice should not add a standalone `workspace register` or `workspace join` command.
In non-interactive mode, commands that need one workspace should fail when selection is ambiguous and suggest `--workspace <name>`.
`--json` should also suppress prompting for commands that need one workspace. If a command would otherwise show a picker, JSON mode should fail with a structured status error and suggest `--workspace <name>`.
## Machine-Local Files
Workspace creation should make machine-local state safe by default.
The workspace should ignore:
```text
/.openspec-workspace/local.yaml
```
The local workspace registry should also be machine-local:
```text
<global-data-dir>/workspaces/registry.yaml
```
Generated agent launch surfaces can be ignored by `workspace-open-agent-context` when that slice creates them.
## JSON Output
Interactive setup does not need JSON output as its primary contract. Non-interactive setup and direct commands should support JSON output for scripting:
-`workspace setup --no-interactive --json`
-`workspace list --json`
-`workspace link --json`
-`workspace relink --json`
-`workspace doctor --json`
`workspace setup --json` should require `--no-interactive`. If a user runs `workspace setup --json` without `--no-interactive`, setup should fail clearly because an interactive wizard cannot produce clean JSON. Direct commands such as `workspace list --json`, `workspace link --json`, `workspace relink --json`, and `workspace doctor --json` do not require `--no-interactive`, but JSON mode should disable prompts and fail on ambiguous workspace selection.
JSON output should use object/status structure across commands:
- primary entities such as `workspace`, `workspaces`, or `link` carry the durable data
-`status` arrays carry warnings, errors, and suggested fixes
- status entries use stable `code` values plus human-readable `message` text
- command-level `status` describes the whole response
- object-level `status` describes that specific workspace or link
## POC Adjustments
Keep:
- guided setup as the default first run
- direct list/link/check commands
- shared state separate from local paths
- clean non-interactive failure when required setup inputs are missing
- JSON output for non-interactive/direct commands
Change:
- do not expose public `workspace create` in the first release
- do not require repo-local OpenSpec state to link a repo or folder
- use `workspace link` instead of `workspace add-repo`
- use `workspace relink` instead of `workspace update-repo`
- do not save a preferred agent during setup
- do not offer to open the workspace from setup
- require setup to link at least one existing repo or folder
- keep relink behavior focused on path repair rather than owner or handoff metadata
- do not use "working set", "code area", "entry", "alias", or "local overlay" in human-facing output
Note: the change id keeps the older "register repos" wording for continuity. User-facing product language in this slice is `workspace setup`, `workspace link`, `workspace relink`, and "linked repos or folders."
Users start workspace work by creating a planning home and linking the repos or folders OpenSpec should know about.
They should not have to create a change before OpenSpec can see the relevant repos, monorepo folders, packages, services, or apps.
The product rule is:
```text
Workspace visibility is not change commitment.
```
A workspace is the durable planning home. A change is a feature, fix, project, or other planned piece of work inside that workspace.
## What Changes
Add the first user-facing workspace setup flow:
```text
Set up a workspace.
Link existing repos or folders.
List known workspaces and what they link to.
Check what OpenSpec can resolve and how to fix problems.
`workspace setup` is the creation path for users. It should ask for the workspace name first, create the workspace in the standard location, require at least one existing repo or folder path, infer link names from folder names, show the workspace location, and run a check at the end so the user knows what OpenSpec can see.
Workspace names should be kebab-case so they are clean managed-folder names and stable registry identifiers. Link names should keep the folder-style validation from `workspace-foundation` because they are often inferred directly from existing repo or folder basenames.
`workspace setup --no-interactive` is the automation path. It should require enough flags to create a useful workspace, including a workspace name and at least one link.
`workspace list` shows known OpenSpec-managed workspaces from the local workspace registry, including each workspace location and linked repos or folders.
`workspace link` records an existing local repo or folder path for the selected workspace. It should support a simple form that infers the link name from the folder name and an explicit-name form for conflicts or clarity. Linking does not create, copy, move, initialize, or edit files in the linked repo or folder.
Linking should behave like selecting a folder from a picker: OpenSpec verifies the folder exists, resolves relative inputs to an absolute path in the current runtime, and stores that verified path instead of the raw input string.
When a link name is already in use, OpenSpec should preserve the existing link and show the conflicting name with the existing path. The error should suggest choosing a different link name, or using `workspace relink <name> <path>` if the user intended to change the existing link's path.
`workspace relink` lets users repair or change the local path for an existing link without recreating the workspace. It should not introduce owner or handoff metadata in this slice.
`workspace doctor` explains what the current machine can resolve for one selected workspace: the workspace location, the workspace planning path, linked repos or folders, missing paths, repo-local specs paths when present, and suggested fixes. It should infer the current workspace when run from inside a workspace. It reports issues but does not repair them automatically.
Workspace commands should work globally. When a command needs one workspace and the user did not specify it, OpenSpec should use the local registry to show an interactive picker. In non-interactive mode, it should fail with a clear message and suggest `--workspace <name>`.
When a command runs from inside a valid workspace that is not in the local registry, OpenSpec should still use that current workspace. It should surface a non-fatal warning status that the workspace is not known locally, and successful mutating commands such as `workspace link` or `workspace relink` should record that workspace in the local registry after they update workspace state.
Machine-readable output should separate workspace or link objects from status entries. Status should be an array of structured issues instead of scattering fields such as `root_status`, `issue`, or `fix` through the primary object shape.
Interactive behavior should be disabled whenever output must be script-safe. `--no-interactive` means no prompts, and `--json` should fail instead of prompting when selection or setup inputs are ambiguous. `workspace setup --json` should require `--no-interactive` so JSON setup always uses the explicit automation path.
Planning dependency:
- Depends on `workspace-foundation`.
## POC Findings
Behavior to preserve:
-`workspace setup` was the friendly onboarding path.
-`workspace list` made managed workspaces discoverable.
- A direct automation path is still useful, but it should live under `workspace setup --no-interactive`.
- Link repair is useful, but owner or handoff metadata should not carry forward in this slice.
-`workspace doctor` was the right place to answer "what does OpenSpec know about this workspace?"
- Shared workspace state and local paths were stored separately.
- Setup failed cleanly when non-interactive inputs were incomplete.
- Created workspaces excluded machine-local path state from portable workspace state.
Behavior to change:
- The POC required linked repo paths to already contain repo-local `openspec/`. This should become an implementation-readiness signal, not a planning prerequisite.
- The POC used repo-only language. This slice should use "repos or folders" for user-facing text.
- The public command should be `workspace link`, not `workspace add-repo`.
- The repair command should be `workspace relink`, not `workspace update-repo`.
- Public `workspace create` should be removed for the first release. Setup should be the creation flow.
- The POC's `setup` flow stored preferred agent and open behavior. Agent launch preferences belong to `workspace-open-agent-context`, not this slice.
- Human output should avoid implementation terms such as working set, code area, entry, alias, or local overlay.
-`setup` should require at least one linked repo or folder so the created workspace is immediately useful.
## Non-Goals
- No public `openspec workspace create` command in this first release.
- No agent launch or workspace open behavior.
- No preferred agent prompts or saved agent preference.
- No owner or handoff metadata fields.
- No workspace change creation or target selection.
- No apply, verify, archive, branch, or worktree behavior.
- No requirement that linked repos or folders have repo-local OpenSpec state.
- No automatic repair behavior in `workspace doctor`.
- No registry cleanup command such as `workspace forget`; stale registry entries are report-only in this slice.
- No standalone `workspace register` or `workspace join` command; unregistered current workspaces are usable, and mutating workspace commands can record them locally.
## Capabilities
### New Capabilities
-`workspace-links`: Lets users set up a workspace, link repos or folders, list known workspaces, and check workspace resolution before change creation.
### Modified Capabilities
-`cli-artifact-workflow`: Introduces workspace setup commands that happen before change creation.
-`workspace-foundation`: Tightens workspace names to kebab-case while keeping folder-style link names.
## Impact
-`openspec workspace setup`
-`openspec workspace list`
-`openspec workspace ls`
-`openspec workspace link`
-`openspec workspace relink`
-`openspec workspace doctor`
- Local workspace registry usage from `workspace-foundation`.
- Docs and generated guidance that explain linked repos or folders as planning context, not implementation commitment.
OpenSpec SHALL use one kebab-case workspace name across workspace identity, managed storage, and the local registry.
#### Scenario: Using one workspace name
- **WHEN** OpenSpec creates or records a managed workspace
- **THEN** the workspace name SHALL be stored in `.openspec-workspace/workspace.yaml`
- **AND** the same name SHALL be used as the default managed workspace folder name
- **AND** the same name SHALL be used as the local registry name
#### Scenario: Rejecting invalid workspace names
- **WHEN** OpenSpec accepts a workspace name
- **THEN** it SHALL require kebab-case names using lowercase letters, numbers, and single hyphen separators
- **AND** it SHALL reject empty names, dot names, names with leading or trailing hyphens, names with repeated hyphens, uppercase letters, spaces, underscores, dots, and path separators
- [x] 3.3 Support `--link <path>` with inferred names
- [x] 3.4 Support `--link <name>=<path>` with explicit names
- [x] 3.5 Fail cleanly when non-interactive setup is missing a name or at least one link
- [x] 3.6 Resolve relative link paths to verified absolute runtime-local paths before storing local state
- [x] 3.7 Require `--no-interactive` when `workspace setup --json` is used
- [x] 3.8 Add `--json` output for non-interactive setup
- [x] 3.9 Preserve the interactive setup UX when `--no-interactive` is not passed
## 4. Workspace Listing
- [x] 4.1 Implement `openspec workspace list`
- [x] 4.2 Add `workspace ls` as an alias for `workspace list`
- [x] 4.3 List known OpenSpec-managed workspaces from the local workspace registry
- [x] 4.4 Handle the no-workspaces case with a clear next step
- [x] 4.5 Show each workspace location and linked repos or folders
- [x] 4.6 Report stale registry entries with status entries without deleting, rewriting, or repairing registry state
- [x] 4.7 Add JSON output with typed workspace objects and structured status arrays
## 5. Workspace Selection
- [x] 5.1 Make workspace commands work from outside workspace directories
- [x] 5.2 Add `--workspace <name>` to commands that need one workspace
- [x] 5.3 Use the current workspace when running from inside a workspace
- [x] 5.4 Use unregistered current workspaces with a non-fatal warning status
- [x] 5.5 Record unregistered current workspaces in the local registry after successful `workspace link` or `workspace relink`
- [x] 5.6 Keep `workspace doctor` diagnostic-only when the current workspace is unregistered
- [x] 5.7 Show an interactive picker when multiple known workspaces exist and no workspace is specified
- [x] 5.8 Select the only known workspace automatically when there is exactly one
- [x] 5.9 Fail clearly in non-interactive mode when workspace selection is ambiguous
- [x] 5.10 Fail with structured status output instead of prompting when `--json` workspace selection is ambiguous
- [x] 5.11 Use the local workspace registry for workspace lookup
## 6. Workspace Links
- [x] 6.1 Implement `openspec workspace link <path>` with inferred link names
- [x] 6.2 Implement `openspec workspace link <name> <path>` with explicit link names
- [x] 6.3 Accept full repo roots and monorepo package/service/app folder paths
- [x] 6.4 Require linked paths to exist
- [x] 6.5 Allow links without repo-local `openspec/`
- [x] 6.6 Store stable link names in shared state and local paths in machine-local state
- [x] 6.7 Keep link names folder-style, and detect duplicate link names with a specific error that shows the existing link path and suggests a different name or `workspace relink`
- [x] 6.8 Resolve relative linked paths to verified absolute runtime-local paths before storing local state
- [x] 6.9 Preserve native Windows and WSL2-style paths as local path values without cross-runtime translation
- [x] 6.10 Ensure link only records state and does not edit the linked repo/folder
- [x] 6.11 Add `--json` output for `workspace link`
`workspace open` should feel like opening a multi-root working set.
The user model is:
```text
workspace setup = create the planning home and choose the default opener
workspace links = the repos or folders OpenSpec can plan across
workspace open = open that linked working set
--agent = use a different agent for this one session
--editor = open the working set as an editor workspace
```
Repo or folder visibility supports exploration and planning. Opening a workspace gives the agent or editor access to linked paths, and implementation starts through an explicit later workflow.
## Command Surface
Supported v1 forms:
```bash
openspec workspace open
openspec workspace open platform
openspec workspace open --agent codex
openspec workspace open platform --agent github-copilot
openspec workspace open --editor
```
The positional workspace name is the primary explicit selection surface for `open`. User-facing docs should prefer the positional form because a flag such as `--workspace <name>` repeats the noun.
For consistency with other workspace commands and scripts, `workspace open` may also support `--workspace <name>` as an alias for the positional name:
```bash
openspec workspace open platform
openspec workspace open --workspace platform
```
User-facing docs should prefer the positional form. If both are provided and they differ, OpenSpec should fail with a clear conflict error.
`--prepare-only` should not be included. The POC used it to build and print launch surfaces without starting the external tool, but that does not map cleanly to a user-facing intent.
`--json` should not be included in this slice. If a future integration needs a machine-readable resolved-open context, design that as a separate context/query surface instead of overloading the launching command.
`--change` should be deferred. Change-scoped open depends on workspace change planning and target semantics that this slice should not invent.
## Workspace Selection
Selection should follow this order:
1. If a positional workspace name is provided, open that known workspace.
2. Otherwise, if the command runs from inside a workspace, open the current workspace.
3. Otherwise, if exactly one workspace is known locally, open it.
4. Otherwise, if multiple workspaces are known and the terminal is interactive, present a picker.
5. Otherwise, fail with a clear message that names the known workspaces and asks the user to pass the workspace name.
This keeps the common cases direct while still supporting global use.
## Preferred Opener
Workspace setup should ask which opener the user wants by default. The answer is machine-local state because different machines may have different installed agents or editors.
`workspace open` uses the saved opener when no override is passed.
`--agent <tool>` is a one-session override that leaves the saved preference unchanged. Persisting a changed default should require an explicit preference/config action in a later slice if users need it.
This slice should not add global workspace opener config. OpenSpec already has a global config system, and workspace-level defaults can be added there later if repeated setup makes the local prompt feel noisy.
The local preference should be shaped so a future global default can fit underneath it with smooth migration. The intended precedence is:
```text
command override
-> workspace-local preferred opener
-> future global workspace default opener
-> interactive prompt or built-in fallback
```
In future config terms, that global default might look like `workspace.defaultOpener`; this slice documents the precedence for later implementation.
Store the preferred opener as a structured object in `.openspec-workspace/local.yaml`:
```yaml
preferred_opener:
kind:agent
id:codex
```
```yaml
preferred_opener:
kind:editor
id:vscode
```
Allowed initial values:
```text
kind: agent, id: codex
kind: agent, id: claude
kind: agent, id: github-copilot
kind: editor, id: vscode
```
The structure keeps the agent/editor distinction clear and leaves room for future opener variants without changing the local-state shape.
Interactive setup should show all supported opener choices, but it should order detected/available openers first. Unavailable choices should still be visible with a note such as `not found on PATH`.
Setup should prefer the plain editor option over an agent when a fallback default is needed for an interactive picker.
Non-interactive setup stores a preferred opener when the caller explicitly passes an opener option. Otherwise, it leaves opener selection for a later interactive `workspace open` prompt or a non-interactive error that explains how to choose an opener.
`--opener <id>` sets the stored preference. It is different from `workspace open --agent <id>` and `workspace open --editor`, which are one-session runtime overrides.
Initial opener detection should stay simple and executable-based:
```text
VS Code editor: code
Codex: codex
Claude: claude
GitHub Copilot in VS Code: code
```
Keep initial detection scoped to executable availability in this slice.
Supported agent values for the initial open surface should be limited to tools with a real launch or attachment mechanism:
```text
claude
codex
github-copilot
```
Plain editor open should be represented by `--editor` with an explicit editor kind.
For this slice, `--editor` means VS Code editor. The `.code-workspace` format is VS Code-specific, so prompts and errors should call this `VS Code editor` rather than implying generic editor support.
`github-copilot` means the VS Code Copilot experience. It should open the maintained `.code-workspace` in VS Code because that is the product surface where this Copilot mode is available.
If OpenSpec later supports a Copilot CLI agent, it should use a distinct value such as `github-copilot-cli` and launch the CLI agent directly. VS Code Copilot and a CLI agent have different opener mechanics, so they should remain distinct opener values.
## Opener Availability
`workspace open` should fail with a clear error when the selected opener is unavailable on the current machine.
The selected opener remains required because it represents user intent, whether it came from local preference or a command-line override.
Errors should name the missing executable or unavailable opener and suggest a concrete next step. For editor-based open, the error should include the `.code-workspace` path so the user can open it manually if needed.
When no preferred opener is stored and no command-line override is provided, `workspace open` should prompt in interactive mode. In non-interactive mode, it should fail and tell the user to pass either an agent override or the editor option.
## Editor Open
`--editor` opens the workspace root plus every linked repo or folder with a valid local path.
For VS Code-style editor support, OpenSpec should create and maintain a `.code-workspace` file as part of the workspace setup/link/relink lifecycle. `workspace open` should launch against existing workspace state.
Expected local workspace shape:
```text
workspace-root/
changes/
<workspace-name>.code-workspace
.openspec-workspace/
workspace.yaml
local.yaml
```
The `.code-workspace` file should include the workspace root and each linked repo or folder with a valid local path. Because linked paths come from machine-local workspace state, OpenSpec-created workspaces should ignore the maintained `.code-workspace` file by default.
The ignore rule should target the specific maintained file and leave other `*.code-workspace` files available for user-authored tracking:
```text
<workspace-name>.code-workspace
```
This lets teams add a separate user-authored portable `.code-workspace` later if they have a shared relative-path layout.
`workspace setup`, `workspace link`, and `workspace relink` should all run the same open-surface sync after mutating workspace state. That sync owns:
-`AGENTS.md`
-`<workspace-name>.code-workspace`
- workspace ignore rules for machine-local files
Even when a command only changes local state, such as `workspace relink`, it should refresh the full openable workspace surface so user-facing files do not drift.
`--agent github-copilot` may use the same editor workspace mechanics, but it also needs Copilot prompt context. Plain `--editor` keeps a normal editor-workspace intent.
`--agent github-copilot` should still open VS Code. The distinction from `--editor` is intent: `--editor` opens the workspace as a normal editor workspace, while `--agent github-copilot` opens the same editor workspace for the user to work with the VS Code Copilot agent experience.
## Workspace Guidance
Workspace setup should install stable guidance in the workspace root, preferably `AGENTS.md`.
The guidance should explain durable workspace rules:
- the workspace root is the planning home
-`changes/` contains workspace-level planning
- linked repos and folders are available for exploration and planning
- visibility supports exploration and planning
- implementation edits start after the user explicitly asks for implementation work
The managed `AGENTS.md` text should stay short and durable, covering stable workspace guidance while runtime details remain discoverable from workspace state. A starting shape:
```markdown
# OpenSpec Workspace Guidance
This directory is an OpenSpec workspace for planning across linked repos or folders.
- Use `changes/` for workspace-level planning.
- Linked repos and folders are available for exploration and planning.
- Repo or folder visibility supports exploration and planning.
- Make implementation edits after the user explicitly asks for implementation work.
- Treat linked repos and folders as the implementation homes for their owned code.
- Use OpenSpec workspace commands instead of hand-editing `.openspec-workspace/*.yaml`.
```
`workspace open` is a launching feature. It should launch the selected opener against existing workspace files.
For Claude and Codex, `workspace open` may still need to pass workspace and linked directory arguments to the agent process at launch because those tools do not consume `.code-workspace` directly. If an opener requires an initial prompt argument, it should be minimal, such as `Open this OpenSpec workspace.`
Dynamic workspace facts should normally be discoverable from existing files:
- linked paths: `.openspec-workspace/local.yaml`
- stable link names: `.openspec-workspace/workspace.yaml`
- active workspace changes: `changes/`
- editor working set: `<workspace-name>.code-workspace`
Report a command file or prompt file path only when the file is actually written and used.
OpenSpec should own a marked workspace-guidance block inside `AGENTS.md`:
```markdown
<!-- OPENSPEC:WORKSPACE-GUIDANCE:START -->
# OpenSpec Workspace Guidance
...
<!-- OPENSPEC:WORKSPACE-GUIDANCE:END -->
```
`workspace setup`, `workspace link`, and `workspace relink` may rewrite that marked block during open-surface sync. Content outside the marked block should be preserved so users can keep their own workspace notes in the same file.
If `AGENTS.md` is missing, OpenSpec should recreate it. If `AGENTS.md` exists and the markers are absent, OpenSpec should append the managed block while preserving existing content.
## Linked Paths
Root workspace open should attach every linked repo or folder with a valid local path.
Broken links are skipped during workspace open. OpenSpec should surface clear status in human output, with `openspec workspace doctor` as the repair path.
Links with repo-local `openspec/` state absent remain valid for workspace open. Missing repo-local OpenSpec state can matter later for implementation readiness while still allowing visibility for exploration and planning.
## Safety Boundary
The opening prompt or editor guidance should say:
```text
Linked repos and folders are visible for exploration and planning.
Make implementation edits after the user explicitly asks for implementation work.
```
Prompt guidance is acceptable for this slice because apply/verify/archive sit outside the open surface. Later implementation workflows should enforce mode and scope through explicit context providers as well as prompt wording.
After a user creates a workspace and links repos or folders, they need to open that workspace with their preferred agent or editor and have the working set available immediately.
The workspace should provide repo and folder locations, link names, and the context that distinguishes planning from implementation.
## What Changes
Add the workspace-open experience:
```text
Open this workspace.
Use my preferred opener by default and honor explicit opener overrides.
The opener sees the workspace location, linked repos or folders, current changes, and relevant instructions.
```
Links are the planning context. The local registry serves as a workspace-discovery index for finding known workspaces on the current machine.
Expected user surface:
```bash
openspec workspace open
openspec workspace open platform
openspec workspace open --agent codex
openspec workspace open platform --agent github-copilot
openspec workspace open --editor
```
`workspace open` should open the current workspace when run from inside one, auto-select the only known workspace when run outside a workspace, and present an interactive picker when multiple known workspaces are available. Users can pass a workspace name as the positional argument when they want to choose explicitly.
Workspace setup should ask for and store a preferred opener in machine-local workspace state. `workspace open` uses that preference by default. `--agent <tool>` is a one-session override that leaves the saved preference unchanged.
`--editor` opens the workspace as an editor workspace. This is related to, but distinct from, `--agent github-copilot`: GitHub Copilot needs editor workspace support plus agent prompt context, while plain editor open should focus on opening the linked working set.
Workspace guidance should live in durable workspace files where possible:
- stable behavior belongs in workspace-level `AGENTS.md`
- opener-specific launch prompts stay minimal when required
- linked repos or folders are visible for exploration and planning before a change exists
This slice supports root workspace launching through the documented opener forms. Public preview (`--prepare-only`) and machine-readable context (`--json`) surfaces belong in a future context/query design if a clear user need appears.
This slice focuses on root workspace open behavior. Change-scoped sessions need the target model from workspace change planning before they can be specified cleanly.
Planning dependency:
- Depends on `workspace-create-and-register-repos`.
## Capabilities
### New Capabilities
-`workspace-open`: Opens a workspace through a preferred agent or VS Code editor with linked repos or folders available for exploration and planning.
### Modified Capabilities
-`workspace-foundation`: Extends machine-local workspace state and setup/link/relink behavior with a preferred opener and maintained openable workspace surface.
## Impact
-`openspec workspace open`
- Workspace setup preferred opener prompt and local preference storage.
- Workspace prompt, editor workspace, and agent-launch context.
- Generated or committed agent guidance for workspace mode.
- Tests for opening inside a workspace, auto-selecting one known workspace, picking among multiple known workspaces, opening by workspace name, one-session agent overrides, and editor open.
Workspace setup already creates a planning home, records linked repos or folders, stores a preferred opener, and maintains the root open surface. For workspace change planning to work in practice, the opened agent also needs OpenSpec workflow skills available from that workspace root.
Repo-local `openspec init` and `openspec update` already provide the user model for choosing agent surfaces and generating skills. Workspace setup should feel similar, but the installation target is the workspace root rather than any linked repo or folder.
The existing artifact workflow assumes a change lives under a repo-local `openspec/changes/<id>` path. Workspace planning needs the same workflow vocabulary, but the planning home may be a workspace root and the implementation homes may be linked repos or folders.
## Goals / Non-Goals
**Goals:**
- Install OpenSpec agent skills into the workspace root during workspace setup.
- Use the active global profile to select which workflow skills are installed in the workspace.
- Let users choose which agents receive skills with familiar `--tools` semantics.
- Persist workspace-local agent skill selection so update can refresh the same agents later.
- Let users refresh, add, or remove workspace-local skills later through `workspace update`.
- Detect and report workspace-local skill drift from the active global profile.
- Let `openspec config profile` offer to apply changed profile settings to the current workspace when run from inside a workspace.
- Redirect workspace users from repo-local `openspec update` to `openspec workspace update`.
- Add a built-in workspace planning schema for workspace-scoped changes.
- Create workspace changes under the workspace planning path.
- Represent affected areas without forcing implementation artifacts into linked repos.
- Give agents machine-readable planning context through status/instructions output.
- Preserve the workspace boundary: linked repos and folders remain untouched during setup/update.
**Non-Goals:**
- Generating slash commands as part of workspace setup.
- Honoring global `delivery: commands` by generating workspace command files.
- Installing skills into linked repos or folders.
- Adding workspace-local workflow profiles separate from global config.
- Solving workspace-scoped artifact path discovery in the first setup-skill step.
- Adding a separate artifact-context CLI command in the first version.
- Implementing workspace apply, verify, or archive semantics end to end.
- Changing repo-local `openspec init` or `openspec update` behavior.
## Decisions
### Use agent-skill language in workspace UX
Workspace setup should ask, "Which agents should get OpenSpec skills in this workspace?" rather than using the broader "AI tools" wording. The user-visible action is installing skills for coding agents, and the target is the workspace planning home.
Alternative considered: reuse the exact `init` wording. That would be familiar, but it hides the important distinction between opening a workspace and installing skills into it.
### Reuse the existing tool id model
The CLI should use the existing `--tools all|none|<ids>` grammar for non-interactive setup and update. Reusing the existing tool IDs avoids inventing a second naming system for the same configured agents.
Alternative considered: add `--agents`. That reads better in isolation, but it creates unnecessary parallel vocabulary next to `openspec init --tools`.
### Let profile choose workflows and tools choose agents
Workspace setup/update should use the active global profile to decide which OpenSpec workflow skills are installed. The profile answers "which actions are available?" while `--tools` answers "which agents get those actions?" Keeping those concerns separate preserves the existing profile model and avoids adding workspace-local workflow selection in this slice.
If global profile is `core`, workspace skills should include the core workflow set. If global profile is `custom`, workspace skills should include only the configured custom workflows. `--tools none` should still mean no agent skills are installed, regardless of profile.
Alternative considered: add a workspace-local profile file. That might be useful later for team-shared workspace defaults, but this slice already stores machine-local agent paths and should avoid introducing another config authority before the global profile behavior works.
### Preselect the preferred opener when possible
Interactive setup should preselect the preferred opener when that opener maps to a skill-capable agent. The user can accept the default, add more agents, or deselect it.
Alternative considered: install skills only for the preferred opener. That is simpler, but opener choice means "how should I open this workspace" while skill selection means "which agents should understand OpenSpec here."
Workspace setup should store the selected skill-capable agents in `.openspec-workspace/local.yaml` because agent paths and installed tool surfaces are machine-local. Workspace update should use that stored selection when the user does not pass `--tools` or make a new interactive selection.
Explicit `--tools` on workspace setup/update should replace the stored selection. `--tools none` should store an empty selection and remove only known OpenSpec-managed workspace skill directories.
The local state should also record enough last-applied information to support drift detection, such as the workflow IDs installed for each selected agent and the effective global profile/delivery at the time of the last successful sync. This is diagnostic state, not a second source of truth.
Alternative considered: infer selected agents by scanning `.codex/skills/`, `.claude/skills/`, and similar directories. Scanning is useful as a fallback, but persisted selection gives predictable update behavior and avoids treating unrelated user-authored files as OpenSpec-managed state.
`openspec workspace setup --no-interactive` should not require `--tools`. If `--tools` is omitted, setup should create the workspace and skip skill installation, preserving existing scripted workspace setup behavior. Human and JSON output should say that no workspace skills were installed and that `openspec workspace update --tools <ids>` can add them later.
`openspec workspace update --no-interactive` without `--tools` should refresh the stored workspace skill agent selection. If no selection is stored, it should complete without installing skills and report a clear no-op with guidance to pass `--tools`.
Alternative considered: require `--tools` whenever workspace setup/update is non-interactive. That mirrors repo-local init, but it would break existing workspace setup scripts that predate workspace-local skill installation.
### Generate workspace-local skills only
Workspace setup/update should generate skills under the workspace root, such as `.codex/skills/` or `.claude/skills/`. It should not generate slash commands in this slice because some command adapters resolve to global locations, and workspace setup should remain local and predictable.
When global delivery is `commands` or `both`, workspace setup/update should still generate only skills and report that workspace command generation is not part of this slice. This keeps profile workflow selection useful without making workspace setup perform global or repo-local command writes.
Alternative considered: mirror `init` exactly and generate both skills and commands. That risks surprising global writes and makes the setup boundary harder to explain.
### Add `workspace update` for skill refresh
`openspec workspace update` should refresh, add, or remove workspace-local OpenSpec skills after setup. It should resolve the current workspace when run from inside a workspace, and also support named and non-interactive forms.
Workspace update should compare the active global profile's workflow selection with the last applied workspace skill state. If they differ, update should add/remove only OpenSpec-managed workflow skill directories for the selected agents. Workspace doctor/list/status surfaces may report the drift as a warning, and `openspec config profile` no-op inside a workspace should use the same drift check for guidance.
Alternative considered: reuse `openspec update` from inside the workspace. That command currently means repo/project update, while workspace update needs workspace selection, workspace JSON/status behavior, and linked-repo safety rules.
### Make `config profile` workspace-aware
`openspec config profile` should remain a global configuration command. When it runs inside a repo-local OpenSpec project and the user chooses to apply changes, it should continue to run `openspec update`.
When it runs inside an OpenSpec workspace and the profile or delivery settings actually change, it should prompt to apply changes to the current workspace. If confirmed, it should run `openspec workspace update` for that workspace. If declined, it should explain that the global config changed and the user can run `openspec workspace update` later.
The preset shortcut `openspec config profile core` should keep its non-interactive character and not launch an apply prompt. When run from inside a workspace, it should save global config and print workspace-specific follow-up guidance to run `openspec workspace update`. When run inside a repo-local project, it should keep the existing repo-local guidance.
For this slice, automatic workspace context should come from the workspace planning home and its own subdirectories. Running a command from inside a linked repo or folder should keep that location's repo-local behavior unless the user explicitly selects the workspace with a workspace command option. This avoids surprising repo-local commands merely because the repo is registered as a workspace link.
If a directory is both inside a workspace planning home and inside a repo-local OpenSpec project, the nearest planning home should determine the apply prompt. This avoids applying a workspace profile change to a linked repo when the user is intentionally operating from the workspace planning home.
Alternative considered: make `openspec config profile` update all known workspaces. That would be convenient in small setups, but global config changes should not fan out into multiple planning homes without an explicit per-workspace action.
### Resolve a planning home before acting
Workflow commands should resolve whether the current change belongs to a repo-local planning home or a workspace planning home before computing paths. The resolver should identify the planning root, change root, linked areas when present, and whether implementation edits are allowed. Linked repos are not implicitly treated as workspace planning homes just because they are registered in a workspace; workspace-scoped behavior is selected from the workspace planning home or through explicit workspace selection.
Alternative considered: add workspace-specific command branches wherever paths are used. That would make the workspace model leak into every workflow and make generated skills more fragile.
### Store workspace changes in the workspace planning path
Workspace changes should live under the workspace planning path, initially `changes/<id>` at the workspace root. Creating the workspace change should capture shared intent once and may record affected areas, but it should not create repo-local `openspec/changes/<id>` directories in linked repos.
Alternative considered: materialize a repo-local change in every affected repo during workspace change creation. That was easy to reason about in the POC, but it commits too early and makes exploration look like implementation.
### Add a workspace planning schema
Workspace-scoped changes should use a built-in `workspace-planning` schema by default. This keeps the workflow verbs familiar while letting workspace changes have a structure that fits cross-area planning.
Initial artifact shape:
```text
changes/<id>/
.openspec.yaml # schema: workspace-planning
proposal.md # shared goal and scope
design.md # cross-area decisions
tasks.md # coordination tasks, optionally grouped by affected area
specs/
<area-or-repo>/
<capability>/spec.md
```
The first schema should stay intentionally close to the normal OpenSpec artifact shape: proposal, specs, design, and tasks. Area-specific requirements live under `specs/` and area-specific work can be represented as sections in `tasks.md`. This slice does not introduce another area manifest beside those normal planning artifacts.
Alternative considered: reuse `spec-driven` unchanged and make all workspace differences implicit in status output. That hides the fact that workspace planning needs different instructions for organizing requirements and tasks by affected area.
Alternative considered: create separate workspace workflow skills instead of a schema. That would duplicate workflow guidance and make workspace mode feel like a different product.
### Support nested workspace spec paths in the schema
The `workspace-planning` schema should define its specs artifact so nested workspace paths are first-class, not accidental. The intended output pattern is `specs/**/*.md`, and the schema instructions should explicitly describe `specs/<area-or-repo>/<capability>/spec.md` as the default convention for area-specific requirements.
Status and instructions output should preserve the concrete nested paths it discovers. Repo-local spec sync, archive, and validation paths that assume `specs/<capability>/spec.md` should not treat workspace-scoped specs as repo-local capability specs until a later explicit implementation, sync, or archive workflow selects an affected area and defines the destination.
### Use affected areas, not targets or repo slices
The planning model should call ownership or implementation boundaries "affected areas." Affected areas can start with registered workspace link names, but the language should leave room for folders, packages, services, apps, or docs sites. Delivery breakdown remains a separate concept and should not be called an area.
Alternative considered: keep "targets" because it maps to the old POC flag. That term is implementation-first and encourages users to choose repos before the plan is clear.
### Make status JSON the agent context contract
`openspec status --change <id> --json` should become the primary source of machine-readable action context. It should include the planning home, change root, concrete artifact paths, affected areas, next steps, and constraints such as allowed edit roots when implementation is later in scope.
Alternative considered: create a separate context command immediately. Status is already used by generated workflow skills, so enriching it first gives agents a single place to look.
### Keep generated skills path-agnostic
Generated workflow skills should ask OpenSpec where artifacts live instead of embedding repo-local paths such as `openspec/changes/<name>`. The standard skill pattern should be:
```text
1. Run `openspec status --change "<name>" --json`.
2. Use the returned planning home, artifacts, next steps, and action context.
3. Run `openspec instructions <artifact> --change "<name>" --json` before writing an artifact.
4. Write to the resolved path returned by the CLI.
```
This keeps the same skill usable in repo-local and workspace-scoped changes. If status/instructions output later becomes too crowded, a separate context command can be introduced in a future change without changing the high-level skill rule.
Alternative considered: add a new `openspec context` command now. That may become useful, but it adds a new surface before we have proven that enriched status/instructions are insufficient.
### Guard unsupported workspace workflow actions
The global profile may select workflows whose workspace-scoped behavior is not implemented in this slice, such as full workspace apply, verify, or archive. Generated workspace-local skills for those workflows should be safe: they should inspect status/instructions, explain the unsupported workspace action, and avoid editing linked repos unless a later explicit implementation workflow supplies an allowed edit root.
This keeps the workspace skill set aligned with the user's profile while preventing repo-local fallbacks from pretending to implement workspace semantics.
Alternative considered: filter unsupported workflows out of workspace skill generation. That would avoid unsupported commands, but it would make the workspace skill set silently diverge from the user's profile and make drift harder to explain.
### Redirect repo update from workspace roots
`openspec update` should remain the repo/project update command. When it is run from an OpenSpec workspace planning home, it should not try to treat the workspace as a repo-local project. It should fail or redirect with clear guidance to run `openspec workspace update`.
Alternative considered: make `openspec update` polymorphic and perform workspace update inside workspaces. That would be convenient, but it blurs the repo/project versus workspace boundary this change is trying to make explicit.
### Update docs, help, and completions
The CLI help, command registry/completions, and user docs should include `openspec workspace update`, its `--tools` behavior, the global-profile relationship, and the skills-only workspace delivery rule.
Alternative considered: document this only after implementation. Because profile/update behavior is easy to confuse with repo-local update, the docs and help updates are part of the user-facing feature.
### Treat manual acceptance and UX review as phase gates
Each phase should produce a user-testable increment, even when most of the work is internal. The phase is not done until a user can exercise the named behavior through the CLI, inspect the resulting output or files, and understand what changed.
Each implementation phase should include a manual acceptance pass in addition to automated tests. The manual pass should exercise the real CLI flow, inspect the generated files or output, and confirm linked repos or folders stay untouched where that is part of the contract.
Each phase should also include a lightweight UX review of prompts, command forms, human output, JSON output, artifact paths, and next-step guidance. Any confusing UX found during review should be fixed in the same phase or recorded as an intentional follow-up before the phase is considered done.
Alternative considered: keep manual review only in the final verification phase. That would catch end-to-end issues late, but workspace planning is mostly workflow and agent-facing UX, so each phase needs its own human check while the behavior is still fresh.
### Reduce self-validation bias with evidence-based review
Implementation should define acceptance evidence before marking tasks done. For each phase, the implementer should capture the exact manual commands or interaction path, expected observations, and actual observations. A task is not complete merely because the implementer believes the code matches the design.
When practical, a separate reviewer or fresh agent context should run the manual acceptance checklist and UX review using only the change artifacts, CLI output, and observed filesystem state. If a separate reviewer is not available, the implementer should rerun the checklist from a clean temporary workspace and record the evidence in the change notes or final implementation summary.
Alternative considered: rely on automated tests plus the implementer's final review. Automated tests are necessary, but this change is workflow-heavy and agent-facing, so independent evidence is more useful than confidence alone.
## Deferred Direction
The earlier product notes pointed at a richer workspace model than this slice ships. Keep that direction as follow-up material, not competing current scope.
- Full workspace apply should select or confirm one work focus before implementation. The first work focus should be an affected area with an allowed edit root; later work may add an optional delivery phase when a large change needs sequencing. Until that model exists, workspace apply/verify/archive skills remain guarded.
- Workspace verify and archive should wait for a clear model of partial area completion, final whole-change completion, and how workspace-scoped specs become repo-local canonical specs.
- Scoped plan files may eventually attach at the change, phase, affected-area, or work-focus level. This slice intentionally keeps the first workspace schema close to normal OpenSpec artifacts: proposal, specs, design, and tasks.
- Affected areas can start as registered workspace link names, but future flows may refine or derive them from planning artifacts. That derivation should avoid reintroducing target-first or repo-slice language.
- Workflow skills may later separate generic OpenSpec workflow semantics from agent-specific affordances such as asking questions, tracking todos, or delegating work. This slice only makes generated workflow skills path-agnostic.
- OpenSpec may need a named exploratory-notes convention for preserving unsettled thinking before it is promoted into proposal, design, specs, or tasks. This cleanup keeps the current change folder focused on standard artifacts.
## Risks / Trade-offs
- Skill generation logic may drift from `init/update` → share the same template generation and tool validation helpers where practical.
- Removing unselected skills could remove user-modified files → remove only known OpenSpec-managed workflow skill directories by explicit workflow list.
-`--tools` is less precise than `--agents` in workspace UX → keep `--tools` for CLI consistency, but use "agents" in prompts and human output.
- Global delivery can say `commands` while workspace update remains skills-only → report this explicitly so users know command generation is deferred, not silently broken.
-`config profile` may run from a linked repo inside an opened workspace → resolve the current planning home carefully and apply only to that home.
- Stored workspace skill state can become stale or hand-edited → treat it as diagnostic machine-local state and always reconcile managed files from the active global profile during update.
- Profile-selected workflows may not yet have full workspace semantics → generated skills must guard unsupported actions and avoid repo-local fallbacks.
- Existing generated skills still contain repo-local path assumptions → handle that as a later artifact-context step after workspace-local skills can be installed.
- Status JSON may become too broad → keep fields plain and action-oriented, such as `planningHome`, `artifacts`, `affectedAreas`, `nextSteps`, and `actionContext`.
- Affected area discovery may be ambiguous → start with explicit registered workspace links and allow later refinement instead of parsing free-form Markdown headings as the only source of truth.
- A new schema can drift from repo-local workflow expectations → keep artifact IDs plain and make status/instructions carry the schema-specific paths.
- Skill instructions may lag behind CLI behavior → audit source workflow templates for hardcoded repo-local paths and replace them with the path-agnostic status/instructions pattern.
Once repos are visible and the agent has workspace context, the user should be able to plan a cross-repo change without creating repo-local artifacts before implementation starts.
The user goal is:
```text
Explore the product goal across repos.
Decide the scope.
Create one workspace-level proposal that identifies the affected areas.
```
Planning should be the commitment point. Repo visibility alone should remain lightweight.
## What Changes
Add workspace-level change planning:
- install and refresh OpenSpec agent skills from the workspace root so agents can operate from the planning home
- use the active global workflow profile to decide which workflow skills are installed in the workspace
- keep `--tools` focused on which agents receive those workspace-local skills
- add a workspace-specific planning schema for workspace changes
- create a workspace change from the coordination root
- capture the product goal once
- identify affected areas by registered workspace link name where applicable
- let the agent explore before committing to affected areas or delivery slices
- keep the workspace as the planning source of truth
- update workflow skill instructions to use CLI-reported artifact paths instead of hardcoded repo-local paths
This slice should avoid creating repo-local artifacts as a side effect of planning. Repo-local artifacts should not be created merely because a workspace change exists.
Workspace setup and update may write agent skill files into the workspace root, such as `.codex/skills/` or `.claude/skills/`, because those files make the workspace planning home usable by agents. That setup work must not write OpenSpec artifacts or agent skill files into linked repos or folders.
Interactive setup should ask which agents should get OpenSpec skills in the workspace, preselecting the preferred opener when that opener supports skills. Workspace update should let users refresh or change those installed agent skills later, including when run from inside the workspace.
Workspace setup and update should treat the global profile as the workflow selection source. For this slice, workspace setup and update are skills-only even when global delivery is `commands` or `both`; command generation for workspaces is deferred.
`openspec config profile` should remain global, but when it runs from inside an OpenSpec workspace and changes the global profile or delivery settings, it should offer to apply the new workflow selection to the current workspace by running `openspec workspace update`.
Workspace-local skill selection should be machine-local state: setup records which agents received skills, update refreshes that stored selection by default, and explicit `--tools` changes the stored selection. OpenSpec should detect when workspace-local skills drift from the current global profile and give clear update guidance.
Selected profile workflows that are not yet fully implemented for workspace-scoped changes should still be safe. Generated skills and CLI guidance must guard unsupported workspace actions instead of falling back to repo-local behavior or editing linked repos implicitly.
Workspace help, docs, and completions should make the distinction legible: `openspec update` remains repo/project sync, while `openspec workspace update` syncs workspace-local agent skills.
Planning dependency:
- Depends on `workspace-open-agent-context`.
## Capabilities
### New Capabilities
-`workspace-change-planning`: Creates and manages workspace-level proposals for cross-repo goals.
### Modified Capabilities
-`workspace-links`: Adds workspace setup/update behavior for workspace-local agent skill installation.
-`cli-config`: Makes `openspec config profile` aware of workspace roots and able to apply global profile changes to the current workspace.
-`change-creation`: Adds workspace-aware change creation semantics and affected area identification.
-`cli-artifact-workflow`: Enriches workflow status and instructions so agents can discover planning context and artifact paths without hardcoded repo-local assumptions.
-`artifact-graph`: Adds a built-in workspace planning schema for workspace-scoped changes.
-`schema-resolution`: Ensures workspace-scoped change creation and workflow commands can resolve the workspace planning schema.
-`openspec-conventions`: Defines the relationship between workspace-level planning and repo-local implementation work.
## Impact
- Workspace change creation.
- Workspace-specific planning schema and templates.
- Affected area metadata and validation.
- Workspace setup and update behavior for installing or refreshing agent skills in the workspace root.
- Global profile integration for workspace-local skill workflow selection.
- **WHEN** repo-local implementation work is needed for a workspace change
- **THEN** OpenSpec SHALL require an explicit implementation workflow with a selected affected area
- **AND** it SHALL expose the allowed edit root for that selected area before implementation edits begin
Some files were not shown because too many files have changed in this diff
Show More
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.