Patch Changes
-
#1864
767d63cThanks @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 ADDEDlate feesbeside an existingLate Fees, or a rename toLATE FEES, archived cleanly and left two contradicting requirements in the main spec, whichvalidatethen 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
72bf760Thanks @dwin-gharibi! - Makeopenspec completion uninstall bashhand.bashrcback exactly ascompletion install bashfound 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.bashrcwith>>(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
e67ac47Thanks @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 orderopenspec archiveuses. 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
2ef6fbdThanks @dwin-gharibi! - Letopenspec config editrun anEDITORorVISUALthat carries arguments. The whole value was passed tospawnas the program name, so common settings such ascode --wait,subl -woremacsclient -tfailed withspawn 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,.cmdshims such ascode.cmdare 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.EDITORstill takes precedence overVISUAL, and the file is still validated after the editor closes. -
#1773
11a9691Thanks @clay-good! - Stop dropping checkbox lines whose marker the task parser does not recognise. Atasks.mdwhose remaining work used a marker other than[ ]/[x]/[X], for example- [~] 1.2 Deferred, reported✓ Completeinopenspec list/statusand 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 holdingxorXmeans 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 thetasksinstruction in thespec-drivenschema 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
92fb72dThanks @clay-good! - Agent-driven archive and sync workflows now create a missing main spec fromADDEDrequirements instead of treating it as already synced. They block sync rather than inventingMODIFIEDorRENAMEDrequirements or writing an empty spec for aREMOVED-only delta, while preserving the user's explicit choice to archive without syncing. A REMOVED-only delta withretire_capabilities: trueremains already synced when its main spec is gone. Fixes #1222 and #1264. -
#1804
a5bf5c6Thanks @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 validatereported the change valid andopenspec archiveexited 0 without applying it.openspec validatenow 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
4c369e0Thanks @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, namingopenspec new changeas an example, while the capture branch tells the agent to transition "seamlessly" into runningopenspec new changeand 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.yamlplus 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
62106f4Thanks @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:proposeat all four prose handoffs, and the capture path ends by naming/opsx:proposefor the remaining planning artifacts and/opsx:applyfor 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-proposefor skills-only delivery,/opsx-propose,/opsx:propose, or@opsx-proposefor command surfaces). The handoffs follow the installed workflow set: a custom profile withoutproposeorapplygets explore's own capture path and theopenspec instructions applyCLI instead of a command it never installed. Fixes #869. -
#1787
9827762Thanks @clay-good! - ### Bug Fixes- Generated skills and commands no longer adopt a project that never ran
openspec init. Every workflow now checksrootfromopenspec list --jsonbefore its first write, and"root": nullmeans 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 whoseopenspec/config.yamlnames a store this machine cannot resolve (not registered, or a malformedstore:line) is not mistaken for an uninitialized one: the workflow stops and shows the store error. Neither path letsopenspec new changecreateopenspec/in the current directory as a side effect. Skill descriptions now name OpenSpec so hosts stop offering these workflows in unrelated repositories.openspec new changealso says when it had to create the root itself, so a directory that was never set up no longer picks up anopenspec/directory in silence (human output only;--jsonis unchanged).
- Generated skills and commands no longer adopt a project that never ran
-
#1902
eb03b9eThanks @clay-good! - Harden the CLI against repositories you have cloned but not yet read (#1835).- A
config.yamlvalue 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 updateoropenspec archivewith catastrophic regex backtracking. - A repository's
.npmrccan 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 updatenow notices a generatedSKILL.mdthat was edited by hand and restores it, instead of reporting every tool as up to date.DO_NOT_TRACK=trueand 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.
- A
-
#1874
388d344Thanks @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. Becauseopenspec initcleans up automatically when there is no TTY, an agent or CI running plainopenspec initdid this without--forceand without a prompt, andopenspec update --forcedid the same. Cleanup now deletes only the files OpenSpec wrote there:proposal,applyandarchivefiles 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
8146be5Thanks @dwin-gharibi! - Stop one unresolvable file from breakingopenspec list. To sort changes by recency,liststats every file inside each change, and any entry it could not stat failed the whole command: a dangling symlink, such as the.#tasks.mdlock Emacs keeps beside every file with unsaved edits, or a symlink loop madelistexit 1 andlist --jsonreport"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
09a999bThanks @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 underchanges/:changes/mobile/refresh-token/left the real change invisible whilemobilewas reported as a task-less change everywhere.openspec archive mobilethen moved the unfinished change into the archive under the namespace's name and applied none of its deltas.openspec listnow marks the foldernot a changeand names the nested directories and a flat alternative,openspec show,openspec status --changeandopenspec status --allsay the same instead of reporting a missing proposal or a full artifact plan,openspec validatereports it instead of "must have at least one delta",openspec list --jsoncarries awarningsentry, andopenspec archiverefuses the folder outright. Detection looks up to three directory levels belowchanges/, which covers every namespace layout seen in practice; a change buried deeper than that behaves as it did before. Fixes #1846. -
#1902
eb03b9eThanks @clay-good! - Install shell completions with the Nix flake package (#1785). The package now ships bash, zsh and fish completions in their standardshare/locations, so Nix users get tab completion without runningopenspec completion installagainst their home directory. -
#1775
626269eThanks @clay-good! - Generated skills and commands no longer point at workflows the active profile does not install. On the defaultcoreprofile, the update workflow told agents to hand off to/opsx:continuefor missing artifacts and to/opsx:newfor a change of intent, neither of whichcoregenerates. 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-changeskill 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 nextreadyartifact fromopenspec status, read its rules withopenspec instructions, keep the selected--store) rather than a one-line pointer. -
#1870
e01ed07Thanks @dwin-gharibi! - Stop archiving a change whose delta was written somewherearchivenever reads.validateandarchiveread a change's deltas only fromspecs/<capability-path>/spec.md, but the spec-driven artifact graph counts any markdown file underspecs/as the specs being written, so a delta atspecs/user-auth.md, or in a second file beside a capability'sspec.md, was reported done bystatusand ready byinstructions applywith no warning, rejected byvalidateonly as "no deltas found", and then archived with exit 0 and nothing merged intoopenspec/specs/. A markdown file that carries delta sections but is not a capability'sspec.mdis now a validation error naming the file and thespec.mdits requirements belong in;archiveruns that validation and refuses the change instead of archiving it unmerged, andinstructions applylists each such file in itswarnings.--no-validatestill archives as before, a change with no spec files still archives, and notes without delta sections underspecs/are not affected. -
#1806
6e62b1dThanks @dwin-gharibi! - Refuse a## RENAMED Requirementssection whoseFROM:andTO:lines do not pair up, instead of guessing. The reader kept one pending pair and dropped whatever did not fit: aTO:before itsFROM:, aFROM:displaced by a secondFROM:, or a trailingFROM:vanished with no diagnostic. Listing the old names and then the new ones paired the secondFROM:with the firstTO:, soopenspec archiverenamed a requirement the delta never named, under a name written for a different one, and exited 0.openspec validatenow 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
4b5c07aThanks @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 asC#keeps its#. Headings without a closing run are unaffected. -
#1868
7090e16Thanks @dwin-gharibi! - Reject a schema whoseapply.requiresnames an artifact that does not exist.parseSchemachecked every artifact'srequiresbut neverapply.requires, soopenspec schema validatepassed 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 artifactrequires, and it names the bad id and the artifacts the schema declares.openspec schema validatealso warns, without failing, whenapply.tracksisn't exactly equal to some artifact'sgeneratesvalue, 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 astask.mdand alsotracks: tasks/main.mdagainstgenerates: 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
46ff91fThanks @dwin-gharibi! - Makeopenspec show --json --deltas-onlyreport the deltas archive applies.ChangeParser, which backsshow --json, thechange listdelta counts and archive's proposal warnings, read delta specs with its own section lookup instead ofparseDeltaSpec, 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.ChangeParsernow derives every operation fromparseDeltaSpec, 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
8b99c07Thanks @clay-good! -openspec statusnow 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'sopenspec instructionscommand while planning is unfinished, andopenspec instructions applyonce 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 JSONnextStepssentence, so the two surfaces cannot name different commands. -
#1882
208b5b5Thanks @dwin-gharibi! - Stop a store namedspecsorchangesfrom taking over root selection. Stores are placed at~/openspec/<id>, so a store with one of those ids is itself~/openspec/specsor~/openspec/changes, and that made$HOMElook like a planning root. Every command run anywhere under the home directory then resolved$HOMEas the nearest root: the globaldefaultStorewas never consulted, andnew changewrote into~/openspec/changes, outside any store. Aspecs/orchanges/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'sopenspec/specs/andopenspec/changes/are unaffected. -
#1880
9f8dec5Thanks @dwin-gharibi! - Stopopenspec store removedeleting 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 layoutstore registeraccepts. 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 theopenspec store unregistercommand to run first. Removing a store whose other registrations are siblings is unchanged, andstore registerstill accepts nested checkouts. -
#1884
5d22145Thanks @dwin-gharibi! - Letopenspec store setup --no-init-gitcreate 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-gitthe check is now skipped, and the store never records the enclosing repository's remote. The default setup and an explicit--init-gitstill refuse a path inside another repository. -
#1862
8fc65b7Thanks @dwin-gharibi! - Count task checkboxes under every CommonMark list marker. The task counter shared bylist,status,view,instructions apply,validate --archivedand 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", andopenspec archivearchived 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
3312af4Thanks @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 initscaffolds custom templates the same way.openspec show --jsonandopenspec change list --jsonkeep naming a change by its id when its proposal opens with the template's bare# Proposaltitle. -
#1778
7de2404Thanks @clay-good! - Make the vendor-neutral tool target findable when your assistant is not on the list.openspec initnow shows it as "Other / Universal (shared .agents skills)"; the picker's search box matches it onuniversal,other,generic,custom,proprietary,unlisted,unsupported,vendor-neutralandagents.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.agentsandamazon-qfilter instead of silently dropping their.and-. -
#1876
605d9e7Thanks @dwin-gharibi! - Stop OpenSpec rewriting a global config file it cannot parse. After a hand edit left a typo such as a trailing comma inconfig.json, the next command of any kind, including read-only ones likeopenspec list, read the fallback defaults as telemetry consent, minted a new anonymous ID and wrote it back, replacing the whole file: atelemetry.enabled falseopt-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 asnull, an array or a string, is now never written implicitly, and telemetry and the update check treat it as opted out.config set,config unsetandconfig profilerefuse with an error that names the file and points toopenspec config edit, andopenspec config reset --allstill replaces it. The existing "Invalid JSON" warning is unchanged, and valid or missing config files behave exactly as before. -
#1840
fede536Thanks @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
db560aeThanks @dwin-gharibi! - Stopvalidateaccepting 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, sovalidatecalled such a change valid andarchivethen 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, sovalidaterejects 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
09984b8Thanks @clay-good! - ### Bug Fixes- Task lists without checkboxes are now caught: a
tasks.mdwritten as plain bullets or a numbered list counts as zero tasks, soopenspec listandopenspec statusreported "No tasks" andopenspec archivehad no unfinished work to warn about.openspec validatenow warns when a change's tracked task files contain list items but no checkbox at all, and points at the first offending line.
- Task lists without checkboxes are now caught: a
-
#1852
5f5914eThanks @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). Runopenspec updateto pick it up.openspec updateitself 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.