The security overrides were declared twice: in pnpm-workspace.yaml, with
the advisory comments explaining each pin, and again under
package.json's pnpm.overrides. The copies are not additive — pnpm 10
uses package.json's block instead of the workspace list when both are
present — and Dependabot rewrites plain-name entries in package.json
whenever it bumps the same package. So a routine bump silently
displaces the pins that patch advisories, and fails the equality test
that guards them (#1812).
Keeps one declaration, in the file that carries the reasoning, and
asserts the mirror stays gone.
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
* fix: handle npm git dep installation for GitHub installs
npm v11's git dep preparation runs `prepare` before node_modules exist
in the temp clone directory, causing TypeScript compilation to fail.
Changes:
- build.js: skip build gracefully when node_modules absent
- package.json: use `node build.js` directly in prepare/prepack for
npm compatibility (avoids pnpm dependency during git dep install)
Note: postinstall.js already handles all errors internally via
main().catch(() => process.exit(0)), so no `|| true` wrapper needed.
Install from GitHub with:
npm pack github:user/repo#branch
npm install -g ./fission-ai-openspec-x.y.z.tgz
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(build): prepare npm git installs without pnpm
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Clay Good <hi@claygood.com>
* Proposed feature: openspec show --diff to see changed requirements more clearly
* Implementation of the --diff feature, which led to some spec changes during implementation.
* Update the proposal to be clearer. Thanks coderabbit
* Fix a bug identified by coderabbit with excessive trimming, and add a testcase for it.
* fix(show): harden --diff for review feedback
Keeps `openspec show <change>` without `--diff` a raw proposal
passthrough, reports when a change has no delta specs instead of
returning silently, preserves the Reason/Migration body of a REMOVED
requirement, and resolves main specs through the command's root so
`--store <id>` diffs against that store.
Text mode and JSON mode now render from one shared collection pass, the
CLI tests drive argv arrays from a mkdtemp project instead of
interpolated shell strings, `--diff` is registered for shell
completions, and the stray package-lock.json is gone.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* build(deps): declare the diff dependency and refresh the flake hash
Adds the `diff` runtime dependency that requirement-diff.ts imports,
updates pnpm-lock.yaml, and regenerates the flake's pnpmDeps hash so
`nix build` matches the new lockfile.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(show): diff nested capabilities too
collectSpecDiffs enumerated only top-level directories under the
change's specs/, so a nested capability (specs/<area>/<id>/spec.md) was
skipped: text mode printed nothing for it and its MODIFIED deltas came
back from --json with no diff. It now uses the same discoverSpecFiles()
helper ChangeParser uses, so the capability ids match the `spec` field
of the JSON deltas.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(show): report header mismatches instead of hiding them
Two cases where --diff quietly showed something misleading:
A MODIFIED requirement whose capability has no main spec was rendered as
all-additions, which reads like a new capability. It is an authoring
error archive will reject, so it now prints the raw text with a warning
naming the missing spec.
A header that differs from the main spec only in case or interior
spacing found no match at all under exact lookup, or matched under a
lowercase-only comparison that let a real mismatch through silently.
Lookup is now exact first, then the shared foldRequirementName fallback,
and a folded match prints the diff the author meant alongside a warning
that archive matches names exactly.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* refactor(show): name the main spec as such in collectSpecDiffs
Comment and locals still called the main spec the "base" spec, and the
no-main-spec comment described the old all-additions behavior.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* chore(deps-dev): bump the development-dependencies group with 2 updates
Bumps the development-dependencies group with 2 updates: [smol-toml](https://github.com/squirrelchat/smol-toml) and [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint).
Updates `smol-toml` from 1.7.1 to 1.8.0
- [Release notes](https://github.com/squirrelchat/smol-toml/releases)
- [Commits](https://github.com/squirrelchat/smol-toml/compare/v1.7.1...v1.8.0)
Updates `typescript-eslint` from 8.66.0 to 8.67.0
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/typescript-eslint/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.67.0/packages/typescript-eslint)
---
updated-dependencies:
- dependency-name: smol-toml
dependency-version: 1.8.0
dependency-type: direct:development
update-type: version-update:semver-minor
dependency-group: development-dependencies
- dependency-name: typescript-eslint
dependency-version: 8.67.0
dependency-type: direct:development
update-type: version-update:semver-minor
dependency-group: development-dependencies
...
Signed-off-by: dependabot[bot] <support@github.com>
* chore(nix): invalidate pnpm dependency hash
* fix(nix): update pnpm dependency hash
* fix(show): harden requirement diff output
* build(nix): pin combined dependency hash
* test(show): assert proposal precedes diffs
* docs(show): clarify JSON diff diagnostics
* fix(show): retain unified diff hunk headers
---------
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: Clay Good <hi@claygood.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
* fix(packaging): print the completions tip from the CLI, not a postinstall script
The package's only install script existed to print one line suggesting
`openspec completion install`. Shipping it made every `npm install -g`
emit an npm allow-scripts warning, and `npm approve-scripts` then failed
with ENOMATCH because it looks in the local project, not a global install
— so the warning looked like a packaging fault with no way to clear it.
The tip now prints once on the CLI's first run, recorded via a
`completionTipSeen` flag in the existing global config alongside the
telemetry notice's `noticeSeen`. It writes to stderr so it can never
contaminate piped stdout, and is suppressed under CI,
OPENSPEC_NO_COMPLETIONS=1, `--json` runs, and `openspec completion`
itself. The published package now ships no lifecycle scripts at all.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(completions): stop the first-run tip from corrupting global config
Adversarial review of the previous commit found it wrote a defaults-merged
config: `saveGlobalConfig({ ...getGlobalConfig(), completionTipSeen: true })`
stamped `profile: "core"` into every user's config.json on first run.
`migrateIfNeeded` treats a raw `profile` as "already migrated", so the
one-time profile migration would never run again — and `openspec update`
then deleted the user's installed workflow skills. Reproduced: 2 skill
directories removed where main reports "Migrated: custom profile with 8
workflows". The same write also overwrote an unparsable config with
defaults and made `openspec config list` report defaults as explicit.
The tip now reads and writes the raw config file and touches only its own
key, leaving an unreadable config strictly alone.
Other hardening from the same review:
- Suppress the tip for the hidden `__complete` resolver. Generated
completion scripts call it on every Tab press with stderr discarded, so
the one-shot tip was consumed where nobody could see it.
- Defer, never consume, when stderr is not a terminal. Agents and pipes
drive this CLI far more often than humans do and would otherwise spend
the tip into a log nobody opens.
- Skip the tip when completions are already installed. Previously the CLI
advertised `completion install` to users who had run it — including on
the very next command after installing. Adds `isInstalled()` to the
bash/fish/powershell installers, mirroring the zsh one.
- Use the repo's `isCiEnvironment()` instead of a `CI === 'true'` string
check, so `CI=yes`/`True`/`on` are as quiet as telemetry is.
- Move the call to `postAction` so the tip trails the command's output
instead of pushing errors and `init`'s setup summary down the screen.
- Record before printing, so an unwritable config dir means silence rather
than nagging on every run.
Tests: assert the message literal (mutation testing showed the message text
was the one unguarded behavior), the raw-write shape, corrupt-config
safety, the already-installed path, the defer policy, and an e2e case
pinning the non-TTY contract.
Docs: SECURITY.md no longer claims zero lifecycle scripts — `prepare` is
still declared and runs for git/directory installs; the registry-install
claim is the accurate one. `OPENSPEC_NO_COMPLETIONS` is now documented.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* test(completions): make the unwritable-config case portable to Windows
fs.chmodSync(dir, 0o555) does not stop a write on Windows, so this test's
unwritable condition never existed there: markTipSeen succeeded, the tip
printed, and windows-pwsh was the only failing job.
Occupy the config directory's path with a file instead. mkdirSync with
recursive: true tolerates an existing directory but throws on an existing
file on every platform, so the persist fails where a real permission error
would - before anything is printed. Also asserts the path is still a file,
so a partial write through the failure would be caught.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(completions): retire the first-run tip instead of advising a dead end
Second adversarial pass over the tip, covering the hardening commit itself.
- An undetected or unsupported shell now retires the tip quietly. It used
to print, but `openspec completion install` exits 1 for exactly those
users ("Shell 'tcsh' is not supported yet" / "Could not auto-detect
shell"), so the one message they would ever get about completions sent
them to a command that fails.
- `markTipSeen` re-reads the config immediately before writing and swaps
the file in by rename. Deciding whether to show the tip costs a `ps`
spawn plus a stat, and a sibling process writing config in that window
got clobbered — on a first run that is exactly when telemetry mints
`anonymousId`. Concurrent-process loss drops from 15/40 to ~2/40, and
what now usually loses is the tip's own flag (it simply shows once
more) rather than telemetry identity. The residual is the non-atomic
read-modify-write shape shared with telemetry's own writer.
- `isInstalled()` uses stat().isFile(), so a directory at the install
path no longer counts as an installed completion script.
- Documented what `isInstalled()` actually promises: the script file, not
the profile sourcing line that bash and PowerShell also need. Callers
deciding whether to *advertise* completions want the loose reading — a
user whose profile config failed has already met the installer.
- Corrected a comment claiming the probe costs "one stat": detectShell()
forks `ps` to read the parent process on every non-Windows run.
Tests: mutation testing found four surviving mutants — dropping
isCompletionRun from the defer policy, reverting isCiEnvironment to a
CI==='true' string check, failing closed on an undetected shell, and
neutering the non-object config guard (which lets a JSON array config be
rewritten as {"0":...}). All four now fail a test. Adds direct coverage
for the three new isInstalled() implementations, which had none.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(validate): stop `change validate` exiting past commander's postAction
`change validate` on a failing change called process.exit(exitCode). That
tears down before commander's postAction hook, which is the same trap the
`update` command documents 165 lines earlier: "exiting here would skip
commander's postAction hook, killing the telemetry flush mid-request".
A change that fails validation is a routine outcome, not an error, so this
silently dropped the telemetry flush and — since the completions tip moved
to postAction — the first-run tip for anyone whose first command was a
failing validate. Verified under a pty: before, the tip never printed and
completionTipSeen was never recorded; after, both happen and the exit code
is still 1 (validate() already sets process.exitCode, which Node honours at
natural exit — top-level `validate --all` has always relied on exactly
that). The existing e2e in validate-scenario-loss.test.ts pins the exit
code.
Also wraps the postAction tip in try/finally so the telemetry flush runs
even if the hint throws: program.parse() is synchronous, so a rejection
there has no catch above it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
* chore(deps): migrate to @inquirer/prompts v8 + @inquirer/core v11
Bumps both packages together. The two Dependabot attempts each moved one
half (#1450 prompts->8, #1422 core->11) and failed: prompts@8 pulls
checkbox@5 -> core@^11, while package.json depends on core@^10 directly
for two custom prompts, so a one-sided bump leaves two copies of
@inquirer/core in the tree — custom prompts on v10 internals alongside
bundled prompts on v11.
Resolves the `instructions` removal in checkbox v5 by dropping the
option: the built-in keys help tip now renders a superset of the hint
that was being passed, so no theme override is needed.
Closes#1458
* fix(nix): regenerate pnpmDeps hash for the inquirer v8 lockfile
The pnpmDeps fixed-output hash is pinned to the contents of pnpm-lock.yaml,
so the inquirer v8/core v11 migration invalidated it and Nix Flake Validation
failed with 'pnpm failed to install dependencies'.
Regenerated against this branch's lockfile and verified: nix build .#default
completes (exit 0) through openspec-1.9.0.drv, not just past the fetch.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: Clay Good <hi@claygood.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
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>
* 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(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>
* 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>
* 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>
* 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>
* 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
* chore: add proposal for PostHog analytics integration
Introduces the proposal artifact for adding opt-in telemetry to OpenSpec
using PostHog. Covers command tracking, feature adoption metrics, and
privacy-respecting consent management.
* feat: add optional anonymous usage statistics
Introduces privacy-first usage analytics to help understand how OpenSpec
is being used. Key privacy protections:
- Only tracks command names and version (no arguments, paths, or content)
- Opt-out via OPENSPEC_TELEMETRY=0 or DO_NOT_TRACK=1
- Auto-disabled in CI environments
- No IP address collection (explicitly disabled)
- Anonymous ID is a random UUID with no PII
Uses PostHog with a reverse proxy to avoid ad blockers. First-run shows
a one-line notice informing users about the collection.
* docs: make telemetry section collapsible and concise
* feat: restructure schemas as directories with templates
Move built-in schemas from embedded TypeScript objects to a file-based
directory structure. This enables co-located templates alongside schemas.
Changes:
- Remove builtin-schemas.ts (replaced by file-based schemas)
- Add schemas/ directory at package root with spec-driven and tdd schemas
- Update resolveSchema() to load from directory structure
- Resolution checks user dir → package dir
* chore: archive restructure-schema-directories change
* docs: update artifact_poc.md for directory-based schema structure
Update documentation to reflect the new schema structure where schemas
are directories containing schema.yaml and co-located templates/ rather
than single .yaml files with separate template directories.
* proposal: add artifact graph core query system
Add OpenSpec change proposal for Slice 1 of the artifact POC - the core
"What's Ready?" query system. This implements:
- ArtifactGraph class for DAG-based dependency modeling
- Filesystem-based state detection (file existence = completion)
- Topological sort for build order calculation
- Ready/blocked artifact queries
This is a parallel module that will coexist with the current system.
* docs: specify Zod for schema validation in artifact graph proposal
- Add decision section for Zod schema validation in design.md
- Update data structures to show Zod schemas with z.infer<> types
- Update tasks to specify Zod usage for type definitions and parsing
* docs: add 2-level schema resolution and built-in schemas
- Add decision for global → built-in schema resolution pattern
- Add resolver.ts for schema lookup logic
- Add built-in schemas directory (spec-driven.yaml, tdd.yaml)
- Add schema resolution tests
- Follows ESLint/Prettier/Git patterns (defaults baked in package)
* experiment: add vertical slice version of artifact graph change
Creates add-artifact-graph-core-v2 with requirements organized as
vertical slices - each requirement file contains its spec, design
decisions, and tasks bundled together for comparison.
* feat(core): add getGlobalDataDir for XDG-compliant data directory
Add getGlobalDataDir() function following XDG Base Directory Specification
for storing user data like schema overrides:
- XDG_DATA_HOME takes precedence on all platforms
- Unix/macOS fallback: ~/.local/share/openspec/
- Windows fallback: %LOCALAPPDATA%/openspec/
* feat(artifact-graph): add core dependency graph module
Implement Slice 1 ("What's Ready?") of the artifact graph system:
- types.ts: Zod schemas for artifact definitions with derived TypeScript types
- schema.ts: YAML parsing with validation for duplicates, invalid refs, cycles
- graph.ts: ArtifactGraph class with Kahn's algorithm for topological sort
- state.ts: Filesystem-based completion detection with glob pattern support
- resolver.ts: Two-level schema resolution (global override → built-in)
- builtin-schemas.ts: spec-driven and tdd workflow definitions
Key design decisions:
- Filesystem as database (stateless, git-friendly)
- Cycle errors show full path (e.g., "A → B → C → A")
- Deterministic ordering via sorted queues
* test(artifact-graph): add comprehensive test suite
52 tests covering all artifact-graph functionality:
- schema.test.ts: Parsing, validation errors, cycle detection
- graph.test.ts: Build order, ready artifacts, blocked queries
- state.test.ts: File existence, glob patterns, missing directories
- resolver.test.ts: Schema resolution with global overrides
* docs(openspec): archive add-artifact-graph-core change
Archive completed change proposal and create artifact-graph spec with
6 requirements covering schema loading, build order, state detection,
ready queries, completion checks, and blocked queries.
* chore: remove experimental artifact-graph-core-v2 folder
Clean up experimental vertical slice proposal that is no longer needed.
* feat(artifact-graph): validate global schema overrides
Global schema overrides are now validated through the same pipeline as
built-in schemas, catching invalid schemas, cyclic dependencies, and
invalid requires references at load time. Added SchemaLoadError for
better error context with file paths.
* test(artifact-graph): add workflow integration tests
Add end-to-end integration tests that exercise the full artifact-graph
pipeline: resolveSchema → ArtifactGraph → detectCompleted → queries.
Tests cover:
- Complete spec-driven and tdd workflow progressions
- Out-of-order file creation handling
- Glob pattern matching with multiple files
- Build order consistency
- Edge cases (empty/missing directories, non-matching files)
* refactor(artifact-graph): adopt zod v4 error message format
Update custom error messages from string format to zod v4 object format
using `{ error: 'message' }` convention.
* fix(test): prevent hanging vitest threads after test runs
- Add teardownTimeout (3s) to vitest config for forced cleanup
- Add global teardown function to vitest.setup.ts
- Call child.unref() to prevent child processes from blocking event loop
- Explicitly destroy stdio streams on process close/error