github Fission-AI/OpenSpec @fission-ai/openspec@1.13.1

55 minutes ago

Patch Changes

  • #1864 767d63c Thanks @dwin-gharibi! - Stop archive adding a second copy of an existing requirement under a name that differs only in case or spacing. ADDED and the RENAMED target compared requirement names exactly, while REMOVED and the RENAMED source already treated a case or whitespace variant as a mistyped header, so an ADDED late fees beside an existing Late Fees, or a rename to LATE FEES, archived cleanly and left two contradicting requirements in the main spec, which validate then accepted. Both now refuse with an error naming the existing requirement, in the same form REMOVED already used. The exact-duplicate error is unchanged, a case-only rename of a requirement to its own name still works, and a variant of a requirement the same delta removes or renames away is still allowed, because ADDED is checked against the spec as it stands after the earlier operations, as the exact check already was.

  • #1872 72bf760 Thanks @dwin-gharibi! - Make openspec completion uninstall bash hand .bashrc back exactly as completion install bash found it. Install adds the OpenSpec block at the top of the file followed by a blank separator line; uninstall removed the block but kept that blank line at the top, then stripped every trailing blank line and wrote the file back without its final newline. The byte count happened to come out unchanged, but the next tool to append to .bashrc with >> (the nvm, conda and rustup installers all do) merged its first line into the user's last line and broke both. Uninstall now also drops the separator line install added when the block sits at the top of the file, and leaves the rest untouched: the final newline, trailing blank lines and CRLF line endings all survive the round trip. A block the user moved elsewhere in the file is still removed, and the zsh, fish and PowerShell installers are unchanged.

  • #1829 e67ac47 Thanks @choi138! - Fix bulk archive nesting a change inside an existing archive target. The workflow now checks every archive target before it writes any main spec, the same order openspec archive uses. A change whose target already exists, or that shares a target with another selected change, is reported as failed and is never synced or moved, while the rest of the batch continues. The check runs again just before each move.

  • #1878 2ef6fbd Thanks @dwin-gharibi! - Let openspec config edit run an EDITOR or VISUAL that carries arguments. The whole value was passed to spawn as the program name, so common settings such as code --wait, subl -w or emacsclient -t failed with spawn code --wait ENOENT, and because that error was never caught the command died with a raw Node stack trace. The value is now split into a program and its arguments, honoring quoted paths with spaces, and the config path is appended as its own argument. No shell is involved, so shell metacharacters in the value are passed through literally. On Windows, .cmd shims such as code.cmd are found. A value that is itself the absolute path of an existing file is still run as-is, so an unquoted editor path containing spaces keeps working. An editor that cannot be started, exits non-zero or is killed is now reported as a one-line error naming the editor, with an install hint when the program was not found, and the command exits 1 instead of throwing. EDITOR still takes precedence over VISUAL, and the file is still validated after the editor closes.

  • #1773 11a9691 Thanks @clay-good! - Stop dropping checkbox lines whose marker the task parser does not recognise. A tasks.md whose remaining work used a marker other than [ ]/[x]/[X], for example - [~] 1.2 Deferred, reported ✓ Complete in openspec list/status and archived with no incomplete-task warning, because unmatched lines counted toward neither the numerator nor the denominator. An empty [] and a padded [ x] were lost the same way. Only a box holding x or X means done (spacing inside the brackets is ignored, so [ x] is done), and every other marker now reads as unfinished, across progress, the apply task list, archive's gate and validate's task-numbering check. The archive, bulk-archive and verify workflows now tell agents the same rule, so a hand-counted tally cannot disagree with the CLI, and the tasks instruction in the spec-driven schema states it where agents author the file. Markdown link bullets stay out of the count: - [Some doc](./doc.md) and the one-character - [A](https://example.com) are not tasks.

  • #1701 92fb72d Thanks @clay-good! - Agent-driven archive and sync workflows now create a missing main spec from ADDED requirements instead of treating it as already synced. They block sync rather than inventing MODIFIED or RENAMED requirements or writing an empty spec for a REMOVED-only delta, while preserving the user's explicit choice to archive without syncing. A REMOVED-only delta with retire_capabilities: true remains already synced when its main spec is gone. Fixes #1222 and #1264.

  • #1804 a5bf5c6 Thanks @dwin-gharibi! - Say so when a requirement in a delta sits outside every delta section. A well-formed ### Requirement: block written under ## Notes, under a misspelled header such as ## Add Requirements, or above the first ## header was dropped with no diagnostic: openspec validate reported the change valid and openspec archive exited 0 without applying it. openspec validate now reports each one as a WARNING naming the section and line, and archive prints the same warning. Nothing else changes: the block is still not applied, the verdict stays valid outside --strict, and requirements shown inside a code fence are not reported. Fixes #1803.

  • #1832 4c369e0 Thanks @clay-good! - Resolve the contradiction that left explore mode's capture branch without a governing rule. Explore states twice that the agent must ask a direct yes/no question and wait for confirmation in a separate user message before its first write-capable action, naming openspec new change as an example, while the capture branch tells the agent to transition "seamlessly" into running openspec new change and creating artifacts with no confirmation step. Both readings were defensible from the text, so the same "capture this as a change" request either wrote .openspec.yaml plus several artifacts immediately or stopped and asked, depending on which passage the agent weighed, which made the #1715 guarantee unenforceable in the one explore path that writes files. An explicit capture request is now stated to be that confirmation, covering the change and the artifacts the request names and nothing else. The guardrail keeps its teeth for the case #1715 actually reported: when the agent is the one proposing the capture, or when the work would go beyond the requested scope, it still asks first, and answers to design or clarifying questions are still never consent to write. Both explore delivery surfaces and the committed skill carry the same wording. Fixes #1828.

  • #1788 62106f4 Thanks @clay-good! - Name the workflow where explore hands off. Explore mode refuses to implement, but every place it said what to do instead described the next step as prose ("create a change proposal") without naming the workflow that does it: the refusal itself, the "flow into a proposal" ending, the closing summary, and the do-not-implement guardrail. Its seamless capture path was worse: it scaffolded a change, wrote artifacts, and then said nothing at all about what came next. With no named exit, agents finished the discovery questions and started writing code, which is the failure reported through GitHub Copilot in #869, and which the docs already promised would not happen ("when the picture is clear, it hands off to /opsx:propose").

    The explore skill and command now name /opsx:propose at all four prose handoffs, and the capture path ends by naming /opsx:propose for the remaining planning artifacts and /opsx:apply for implementation, with an explicit note that capturing artifacts is not permission to implement them. The references are written in the canonical /opsx:<id> form so each tool renders the invocation it actually registers (/openspec-propose for skills-only delivery, /opsx-propose, /opsx:propose, or @opsx-propose for command surfaces). The handoffs follow the installed workflow set: a custom profile without propose or apply gets explore's own capture path and the openspec instructions apply CLI instead of a command it never installed. Fixes #869.

  • #1787 9827762 Thanks @clay-good! - ### Bug Fixes

    • Generated skills and commands no longer adopt a project that never ran openspec init. Every workflow now checks root from openspec list --json before its first write, and "root": null means the project is not set up. What happens next depends on how the workflow was reached. A skill the agent picked on its own drops OpenSpec and answers the request normally, without asking about setup. A workflow the user asked for by name, or ran as a slash command, stops and asks whether to initialize the project, target a store, or handle the request without OpenSpec. A project whose openspec/config.yaml names a store this machine cannot resolve (not registered, or a malformed store: line) is not mistaken for an uninitialized one: the workflow stops and shows the store error. Neither path lets openspec new change create openspec/ in the current directory as a side effect. Skill descriptions now name OpenSpec so hosts stop offering these workflows in unrelated repositories. openspec new change also says when it had to create the root itself, so a directory that was never set up no longer picks up an openspec/ directory in silence (human output only; --json is unchanged).
  • #1902 eb03b9e Thanks @clay-good! - Harden the CLI against repositories you have cloned but not yet read (#1835).

    • A config.yaml value can no longer close the project context block and inject its own directives into the instructions an agent receives.
    • A crafted delta or skill file no longer stalls openspec update or openspec archive with catastrophic regex backtracking.
    • A repository's .npmrc can no longer point the update check at a cleartext or attacker-controlled registry; a rejected registry now disables the check instead of falling back.
    • openspec update now notices a generated SKILL.md that was edited by hand and restores it, instead of reporting every tool as up to date.
    • DO_NOT_TRACK=true and other common spellings of an opt-out now turn telemetry off, and nothing is sent until the first-run notice has been shown.
    • Shell-completion installs quote directory paths safely, git probes run with bounded time and output, and dependencies are cleared of known advisories.
  • #1874 388d344 Thanks @dwin-gharibi! - Stop legacy cleanup deleting the user's own files. The six pre-skills tools that kept their commands in a <tool>/commands/openspec/ folder (Claude Code, CodeBuddy, Qoder, Lingma, Crush and Gemini CLI) had that whole folder removed recursively whenever it existed, so a command the user kept there, such as a team review checklist, was deleted along with OpenSpec's files, and the summary named only the folder. Because openspec init cleans up automatically when there is no TTY, an agent or CI running plain openspec init did this without --force and without a prompt, and openspec update --force did the same. Cleanup now deletes only the files OpenSpec wrote there: proposal, apply and archive files that still carry the OpenSpec markers every legacy command was generated with, so a same-named file the user wrote is kept. It never follows a symlinked command folder, removes the folder only once nothing else is left in it, and lists each thing it kept. A folder holding nothing OpenSpec wrote is no longer reported as legacy at all. A folder holding only OpenSpec's files, or nothing, is still removed exactly as before, with the same summary line.

  • #1866 8146be5 Thanks @dwin-gharibi! - Stop one unresolvable file from breaking openspec list. To sort changes by recency, list stats every file inside each change, and any entry it could not stat failed the whole command: a dangling symlink, such as the .#tasks.md lock Emacs keeps beside every file with unsaved edits, or a symlink loop made list exit 1 and list --json report "changes": [], so agents discovering work through it saw no changes at all. An entry that no longer resolves (removed mid-walk, a dangling symlink, or a loop) is now skipped when computing a change's last-modified time. Valid symlinks are dated as before, and any other error, such as a permission failure, still fails the listing.

  • #1849 09a999b Thanks @clay-good! - Report a change directory nested in a namespace folder instead of silently listing the folder around it as a change. Specs can be nested by domain (specs/mobile/tutorial-videos/spec.md), so it looks reasonable to lay changes out the same way, but a change is only ever a directory directly under changes/: changes/mobile/refresh-token/ left the real change invisible while mobile was reported as a task-less change everywhere. openspec archive mobile then moved the unfinished change into the archive under the namespace's name and applied none of its deltas. openspec list now marks the folder not a change and names the nested directories and a flat alternative, openspec show, openspec status --change and openspec status --all say the same instead of reporting a missing proposal or a full artifact plan, openspec validate reports it instead of "must have at least one delta", openspec list --json carries a warnings entry, and openspec archive refuses the folder outright. Detection looks up to three directory levels below changes/, which covers every namespace layout seen in practice; a change buried deeper than that behaves as it did before. Fixes #1846.

  • #1902 eb03b9e Thanks @clay-good! - Install shell completions with the Nix flake package (#1785). The package now ships bash, zsh and fish completions in their standard share/ locations, so Nix users get tab completion without running openspec completion install against their home directory.

  • #1775 626269e Thanks @clay-good! - Generated skills and commands no longer point at workflows the active profile does not install. On the default core profile, the update workflow told agents to hand off to /opsx:continue for missing artifacts and to /opsx:new for a change of intent, neither of which core generates. Every cross-workflow handoff is now decided at generation time against the installed workflow set, and renders a concrete CLI fallback (openspec status, openspec instructions, openspec archive) when the workflow it would name is absent, rather than relying on a runtime availability check the agent had to perform. The onboarding tutorial's command tables are likewise built from the workflows you actually have.

    Also folds in #1735, which fixed the same issue (#1734) by removing the optional handoffs outright. The CLI's own runtime instructions no longer name the openspec-continue-change skill either, since those strings are chosen at run time and cannot be resolved against a profile; and the blocked-state fallback now carries the full CLI recovery (select the next ready artifact from openspec status, read its rules with openspec instructions, keep the selected --store) rather than a one-line pointer.

  • #1870 e01ed07 Thanks @dwin-gharibi! - Stop archiving a change whose delta was written somewhere archive never reads. validate and archive read a change's deltas only from specs/<capability-path>/spec.md, but the spec-driven artifact graph counts any markdown file under specs/ as the specs being written, so a delta at specs/user-auth.md, or in a second file beside a capability's spec.md, was reported done by status and ready by instructions apply with no warning, rejected by validate only as "no deltas found", and then archived with exit 0 and nothing merged into openspec/specs/. A markdown file that carries delta sections but is not a capability's spec.md is now a validation error naming the file and the spec.md its requirements belong in; archive runs that validation and refuses the change instead of archiving it unmerged, and instructions apply lists each such file in its warnings. --no-validate still archives as before, a change with no spec files still archives, and notes without delta sections under specs/ are not affected.

  • #1806 6e62b1d Thanks @dwin-gharibi! - Refuse a ## RENAMED Requirements section whose FROM: and TO: lines do not pair up, instead of guessing. The reader kept one pending pair and dropped whatever did not fit: a TO: before its FROM:, a FROM: displaced by a second FROM:, or a trailing FROM: vanished with no diagnostic. Listing the old names and then the new ones paired the second FROM: with the first TO:, so openspec archive renamed a requirement the delta never named, under a name written for a different one, and exited 0. openspec validate now reports each unpaired line as an ERROR with its line number, and archive refuses the change until the pairing is fixed. Well-formed renames, including several consecutive pairs, are unchanged. A change that used to archive with a malformed RENAMED section is now rejected. Fixes #1805.

  • #1860 4b5c07a Thanks @dwin-gharibi! - Read a requirement heading written with a CommonMark closing sequence, such as ### Requirement: Late Fees ###, as the requirement it renders as. The trailing # run stayed in the name, so a REMOVED written that way looked for "Late Fees ###", missed the requirement, and archive exited 0 with a false "treating it as already removed" warning while the requirement stayed in the spec; a closed MODIFIED or RENAMED heading failed as "not found", and a closed and an open heading of one requirement were not reported as duplicates. Requirement names now drop the closing run wherever they are read, exactly as scenario names already did: only a run preceded by a space or tab counts, so a name such as C# keeps its #. Headings without a closing run are unaffected.

  • #1868 7090e16 Thanks @dwin-gharibi! - Reject a schema whose apply.requires names an artifact that does not exist. parseSchema checked every artifact's requires but never apply.requires, so openspec schema validate passed a one-character typo there, and apply then skipped the unknown id: apply.requires: [desgin] turned the apply gate off and told the agent "Proceed with implementation" with only a proposal written. That is now a schema error, raised wherever the schema is loaded, exactly like an unknown artifact requires, and it names the bad id and the artifacts the schema declares. openspec schema validate also warns, without failing, when apply.tracks isn't exactly equal to some artifact's generates value, because OpenSpec finds the tracked artifact by comparing those two strings and can otherwise not tell which artifact's progress the file belongs to. That covers a typo such as task.md and also tracks: tasks/main.md against generates: tasks/*.md, where the glob does produce the file but the strings still differ. Apply reads that path as written either way, so schemas that track a hand-written file keep loading and working. Every built-in schema parses as before.

  • #1856 46ff91f Thanks @dwin-gharibi! - Make openspec show --json --deltas-only report the deltas archive applies. ChangeParser, which backs show --json, the change list delta counts and archive's proposal warnings, read delta specs with its own section lookup instead of parseDeltaSpec, the reader archive uses, and the two disagreed. A REMOVED written in the bullet form (- `### Requirement: X`) was invisible to it, so it fell back to the proposal's "What Changes" prose and reported an invented MODIFIED while archive deleted the requirement; a repeated section header was read only once; and a RENAMED line written with * or + was dropped. The inspection command OpenSpec's own error text recommends therefore misreported a deletion. ChangeParser now derives every operation from parseDeltaSpec, and a change whose delta spec files carry a delta section is described by them alone, so proposal prose is never reported in place of what archive applies. Requirement text and scenarios are read exactly as before, header-form deltas produce the same output, and a change with no delta spec files, or a legacy change whose spec files carry no delta section, still falls back to the "What Changes" bullets.

  • #1786 8b99c07 Thanks @clay-good! - openspec status now names the command that moves the change forward.

    The text output reported state and stopped there, so picking a change back up (after a lost session, or on a change you did not start) meant already knowing which command came next. The JSON surface had carried that command all along in nextSteps; the text surface never printed it.

    Status now ends with a Next: line: the next ready artifact's openspec instructions command while planning is unfinished, and openspec instructions apply once every planning artifact exists. It carries --store <id> when the resolved root is a store, and it is built from the same source as the JSON nextSteps sentence, so the two surfaces cannot name different commands.

  • #1882 208b5b5 Thanks @dwin-gharibi! - Stop a store named specs or changes from taking over root selection. Stores are placed at ~/openspec/<id>, so a store with one of those ids is itself ~/openspec/specs or ~/openspec/changes, and that made $HOME look like a planning root. Every command run anywhere under the home directory then resolved $HOME as the nearest root: the global defaultStore was never consulted, and new change wrote into ~/openspec/changes, outside any store. A specs/ or changes/ directory that carries store metadata no longer counts as planning content of the directory above it, so these stores resolve like any other. A real project's openspec/specs/ and openspec/changes/ are unaffected.

  • #1880 9f8dec5 Thanks @dwin-gharibi! - Stop openspec store remove deleting a store the user did not name. Remove deletes the target's folder recursively, but it checked only the target's own metadata, so any other registered store living inside that folder was deleted with it, uncommitted planning work included, while its registry entry was left pointing at a path that no longer existed. The natural way to get there is a shared store vendored into another as a git submodule, a layout store register accepts. Remove now refuses when another registration points inside the folder, checked under the same registry lock that commits the removal, and the error names each nested store with the openspec store unregister command to run first. Removing a store whose other registrations are siblings is unchanged, and store register still accepts nested checkouts.

  • #1884 5d22145 Thanks @dwin-gharibi! - Let openspec store setup --no-init-git create a store inside an existing Git repository. Setup refuses a path inside another repository because initializing the store there would nest one repository in another, but it ran that check even with --no-init-git, which creates no repository at all. Users who keep their home directory as a dotfiles repository therefore could not set up a store at the recommended ~/openspec/<id> path with any flag. With --no-init-git the check is now skipped, and the store never records the enclosing repository's remote. The default setup and an explicit --init-git still refuse a path inside another repository.

  • #1862 8fc65b7 Thanks @dwin-gharibi! - Count task checkboxes under every CommonMark list marker. The task counter shared by list, status, view, instructions apply, validate --archived and archive's incomplete-task check recognized only - and * bullets, so a task written as an ordered item (1. [ ], 1) [ ]) or under a + bullet was invisible to all of them: a change with unfinished ordered tasks reported "✓ Complete", and openspec archive archived it without its incomplete-task warning. Task lines under + and ordered markers (. or ), up to nine digits, as CommonMark allows) now count exactly like - and * ones, including nested sub-tasks, CRLF files and the existing tolerance of a missing space after the marker, and task-numbering checks now see them too. Ordered and + items without a checkbox are still ignored, and - and * tasks count as before.

  • #1777 3312af4 Thanks @clay-good! - Start generated proposal, spec, design, and tasks files with a top-level heading, so artifacts are complete markdown documents instead of files whose first line is a section header. Editors that run markdownlint no longer flag every OpenSpec artifact with MD041. openspec schema init scaffolds custom templates the same way.

    openspec show --json and openspec change list --json keep naming a change by its id when its proposal opens with the template's bare # Proposal title.

  • #1778 7de2404 Thanks @clay-good! - Make the vendor-neutral tool target findable when your assistant is not on the list. openspec init now shows it as "Other / Universal (shared .agents skills)"; the picker's search box matches it on universal, other, generic, custom, proprietary, unlisted, unsupported, vendor-neutral and agents.md; a search that matches nothing points at it instead of ending at "No matches"; and --tools <unknown> names it in the error. The search box also accepts punctuation, so .agents and amazon-q filter instead of silently dropping their . and -.

  • #1876 605d9e7 Thanks @dwin-gharibi! - Stop OpenSpec rewriting a global config file it cannot parse. After a hand edit left a typo such as a trailing comma in config.json, the next command of any kind, including read-only ones like openspec list, read the fallback defaults as telemetry consent, minted a new anonymous ID and wrote it back, replacing the whole file: a telemetry.enabled false opt-out, the chosen profile and the workflow list were all lost, and usage events were sent. A config file that exists but does not hold a JSON object, whether it failed to parse or its root is something else such as null, an array or a string, is now never written implicitly, and telemetry and the update check treat it as opted out. config set, config unset and config profile refuse with an error that names the file and points to openspec config edit, and openspec config reset --all still replaces it. The existing "Invalid JSON" warning is unchanged, and valid or missing config files behave exactly as before.

  • #1840 fede536 Thanks @clay-good! - Resolve the contradiction that left /opsx:update's only write path without a governing rule. Step 4 told the agent to "Apply the requested edit", while step 5 and the guardrails told it to write only after the user confirms each revision, so the same /opsx:update "the design now uses X" either wrote immediately or stopped and showed the proposed revision first, depending on which passage the agent weighed. Step 4 now drafts the edit in the conversation and step 5 owns every artifact write, matching the workflow's own specified behavior: propose each revision and apply it only after user confirmation. Fixes #1836.

  • #1858 db560ae Thanks @dwin-gharibi! - Stop validate accepting a requirement whose only scenario is a bare header. The delta scenario counter counted every #### header, while the spec path that archive uses to validate the rebuilt spec keeps a scenario only when its body has content, so validate called such a change valid and archive then refused it with a generic "Requirement must have at least one scenario" that did not name the requirement. Both paths now share one rule, hasScenarioBody, and read a scenario's body up to the same boundary, so validate rejects exactly what archive rejects, naming the requirement and saying that a header with no body under it does not count. A scenario whose body is only a fenced block or a deeper header still counts, a requirement with one real scenario is still accepted even when another is empty, and main-spec validation is unchanged.

  • #1774 09984b8 Thanks @clay-good! - ### Bug Fixes

    • Task lists without checkboxes are now caught: a tasks.md written as plain bullets or a numbered list counts as zero tasks, so openspec list and openspec status reported "No tasks" and openspec archive had no unfinished work to warn about. openspec validate now warns when a change's tracked task files contain list items but no checkbox at all, and points at the first offending line.
  • #1852 5f5914e Thanks @clay-good! - Match the natural "openspec " phrasing to the workflow it names. Users and agents say "openspec propose" or "do an openspec apply", but no workflow skill's description contained that phrasing (and a skill's description is what an agent matches on), so the phrase read as an invitation to hand-build the artifacts with the CLI instead of running the workflow. Every workflow skill's description now names the phrasings a user actually types ("openspec propose", "opsx apply", and so on). Run openspec update to pick it up. openspec update itself is deliberately left unclaimed: it is a real CLI command that refreshes generated files, unrelated to the update-change workflow, which claims "openspec update change" instead. Commands-only installs write no skills and are unchanged. Fixes #1221.

Don't miss a new OpenSpec release

NewReleases is sending notifications on new releases.