mirror of
https://github.com/Fission-AI/OpenSpec.git
synced 2026-10-02 05:24:34 +08:00
db2309783547a14e150dbcbfc19120e4028446c3
913
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
db23097835 |
Version Packages (#1953)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>@fission-ai/openspec@1.13.2 v1.13.2 |
||
|
|
7ac58dc790 |
chore(changeset): track Kilo Code and Continue fixes for 1.13.2 (#1964)
* chore(changeset): track Kilo Code and Continue fixes for 1.13.2 #1938 and #1944 merged without changesets, so their user-visible fixes would be missing from the 1.13.2 release notes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * chore(changeset): describe Kilo cleanup by file name Cleanup deletes the known legacy file names without checking content, so an edited copy is removed too; drop the claim that user files are kept. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
336414665f |
fix(verify): stop reporting removed requirements as missing (#1962)
* fix(verify): stop reporting removed requirements as missing Verify walked every "### Requirement:" in the delta specs and looked for an implementation of each one, whatever section it sat under. A REMOVED requirement that the change had removed correctly came back as CRITICAL "Requirement not found", with a recommendation to implement it. An agent that follows the report puts back the behavior the change just deleted. Verify now notes the delta section of each requirement first. ADDED and MODIFIED keep the existing checks. REMOVED is checked the other way round: finding nothing is the expected result, and it is only critical while the behavior is still in the code. RENAMED only changes a name, so the old name is not reported as missing. Scenario coverage skips removed requirements, since there is nothing left to cover. Archive and sync already handle each section on its own terms; verify was the one step in the loop that did not. Closes #1959 * docs(specs): scope the general verify scenarios to ADDED and MODIFIED The Spec coverage, Requirement implementation mapping and Scenario coverage scenarios still told the verifier to check every requirement in the delta specs, which contradicts the Removed requirement scenario added in the previous commit. A verifier following them would repeat the #1959 failure. * fix(verify): report a removal-only change as ready when nothing remains With #1732 merged, a change whose delta specs only remove or rename requirements left Requirement Implementation Mapping and Scenario Coverage with nothing to check. The "no usable requirements" rule then marked them not verified, so verify never reported the change ready, the exact case #1959 describes. Those two checks are now not applicable when the readable delta specs hold REMOVED or RENAMED requirements and no ADDED or MODIFIED ones. An empty or unparseable delta still marks them not verified. Keyword matches in openspec/ artifacts, docs, or code that serves only the Migration note or an ADDED requirement are no longer evidence by themselves that a removed requirement is still implemented; a code path that still delivers the removed behavior is reported even when shared. The summary counts removals separately from covered requirements. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(verify): check a renamed requirement's behavior against its baseline A RENAMED entry only told verify not to report the FROM name as missing, and a rename-only change marked the correctness checks not applicable. Nothing checked that the renamed requirement's behavior was still implemented, so verify could report readiness unchecked. Spec Coverage now reads the baseline requirement from the main spec (under the FROM name, or the TO name once synced) and checks that its behavior is still implemented, without requiring code symbols to be renamed. A missing behavior is CRITICAL "Renamed requirement not found"; an unreadable baseline marks the entry not verified. A TO name that also appears under MODIFIED is still checked there. Regression tests cover the skill template, the command template, and the committed skills/ mirror. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Clay Good <hi@claygood.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
072de6bc39 |
fix(verify): do not report unverified dimensions as passing (#1732)
* fix(verify): do not report unverified dimensions as passing Step 5 gates task and spec coverage on `contextFiles.tasks` and `contextFiles.specs`. `contextFiles` is an artifact-id map and artifact ids come from the active schema, so on a schema that defines neither, both branches are no-ops: nothing is checked, no issues are raised, and step 8 concludes "All checks passed. Ready for archive." The Graceful Degradation guardrail already asks the agent to note skipped checks, but nothing stopped the all-clear verdict. Mark an unchecked dimension `Not verified` in the scorecard and require the final assessment to name it. * fix(verify): map skipped checks to report outcomes * fix(verify): retain no-task and skipped-check context * fix(verify): harden evidence gaps and final assessments * fix(verify): preserve optional workflows and task artifact fallback * fix(apply): resolve tracked task globs by schema path * fix(verify): preserve unavailable task evidence * fix(verify): distinguish untracked tasks from missing evidence * docs(apply): document tracked globs and JSON evidence * test(parity): regenerate hashes after merging #1940 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * test(parity): restore the #1837 regression tests dropped in the merge The earlier conflict resolution took our whole side of the parity file, which discarded the two threshold tests main gained in #1940. Take main's file verbatim and regenerate the hashes instead. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * test(parity): regenerate hashes after merging #1955 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * test(parity): regenerate hashes after merging #1795 and #1926 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * test(parity): regenerate hashes after merging #1731 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Clay Good <hi@claygood.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
d6bdef6577 |
fix(workflows): stop reading schema from list output (#1731)
* fix(workflows): resolve the change picker's schema label from status The update and continue templates tell the agent to read a `schema` field from `openspec list --json` and fall back to "spec-driven" when it is absent. `list --json` returns only `name`, `completedTasks`, `totalTasks`, `lastModified`, and `status` (docs/agent-contract.md 4.1), so the field is never present and the fallback fires every time: a change on a custom schema is shown to the user as `spec-driven`. Make the schema line optional and, when shown, resolve it from `openspec status --change "<name>" --json` (`schemaName`). * fix(workflows): align list prompts with JSON fields * test(workflows): verify list and status schema contracts * fix(workflows): keep bulk archive sync available in custom profiles * test(parity): regenerate hashes after merging #1940 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * test(parity): restore the #1837 regression tests dropped in the merge The earlier conflict resolution took our whole side of the parity file, which discarded the two threshold tests main gained in #1940. Take main's file verbatim and regenerate the hashes instead. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * test(parity): regenerate hashes after merging #1955 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * test(parity): regenerate hashes after merging #1733 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * test(archive-progress): resolve optional-workflow blocks before generating This change makes the bulk-archive surfaces conditional on the sync workflow being installed. #1795's task-progress test built them from the raw templates, which leaves the [[opsx:if-workflow ...]] markers in the text and makes skill generation throw. Build both surfaces through getSkillTemplates/getCommandTemplates, which resolve the blocks against an installed set, and name sync in that set so the assertions keep testing the wording they were written for. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Clay Good <hi@claygood.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
fb1b87613b |
fix(archive): use schema-aware task progress in workflows (#1795)
* fix(archive): use schema-aware task progress in workflows * test(archive): verify task lookup follows the selected store * fix(archive): reject invalid task progress in workflows * test(parity): regenerate hashes after merging #1940 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * test(parity): restore the #1837 regression tests dropped in the merge The earlier conflict resolution took our whole side of the parity file, which discarded the two threshold tests main gained in #1940. Take main's file verbatim and regenerate the hashes instead. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * test(parity): regenerate hashes after merging #1955 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Clay Good <hi@claygood.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
f2812f6d18 |
fix(archive): copy without staging when Windows EPERM blocks rename (#1926)
* fix(archive): copy without staging when Windows EPERM blocks rename fs.rename of a non-leaf change directory fails with EPERM on Windows when a watcher holds a handle. The fallback required a staging rename of the same directory, so it never ran: specs were rolled back after printing success, and a newly created capability was left as an empty folder git cannot see. Copy from the original source when staging also fails with EPERM/EXDEV, and prune empty capability dirs on rollback. Closes #1895 AI-assisted (Grok) * fix(archive): bound the unstaged cleanup to what it verified Addresses both review findings on the copy fallback. The staging rename was what claimed the source before it was deleted. Falling back without it means copy-then-remove now runs against the live change directory, which the archive claim does not cover, and a recursive remove deletes whatever is there at that moment - including a file written after the final fingerprint, which never reached the destination. Cleanup now removes a named set: the entries listed after the last verification, deepest first. A later arrival is not in that set, so it is never deleted, and the rmdir of its parent fails with ENOTEMPTY, which the caller already reports as a retained destination. The move fails loudly rather than completing with data missing. Rollback of a created spec pruned the capability directory unconditionally, which also removed one the user already had, along with its mode and ACLs. The snapshot now records whether that parent existed, and only a directory this write created is pruned. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(archive): close the listing window and the nested prune boundary Both follow-ups from CodeRabbit's second pass, and both are right. The removal listing was taken after the final fingerprint, which left the window it was meant to close: a file arriving between the fingerprint and the listing landed in the set and was deleted, having never reached the destination. Listing before the verification makes the two orderings exhaustive - an arrival either changes the fingerprint and aborts the move, or is absent from the set and survives. The one case this cannot cover, an edit to an already-listed file, is now stated in the comment. `parentExisted` only described the target's direct parent, so a nested capability id whose intermediate directory already existed still lost it: the prune walked to the specs root. The snapshot now records the deepest pre-existing ancestor and passes it as the prune boundary, which pruneEmptyDirs never removes. That one mechanism covers the flat case too. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(archive): claim each entry before removing it in the unstaged fallback An editor could rewrite an already-verified file between the final fingerprint and its removal. The destination held the older bytes, the newer ones were deleted with the source, and archive reported success. Cleanup now renames each entry to a private claim name before reading it. rename is atomic, so a rewrite that lands after the claim creates a new file at the original path, which is not in the verified set, is never deleted, and makes the parent rmdir fail. A rewrite that lands first is caught by comparing the claimed entry against the copy, which puts the file back and abandons the move with both trees intact. Symlinks are compared by their target rather than by reading them, since a link to a directory is not a directory entry and reading one is EISDIR. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(archive): draw the cleanup claim suffix per move A fixed '.openspec-claim' suffix collided with a source file that legitimately ends in it: claiming 'collision' renamed it over a real 'collision.openspec-claim', and that file's own turn then failed with ENOENT after part of the live source had already been removed. A valid tree could not archive, and its source was damaged for nothing. The suffix is now drawn per move and checked against the entries being removed, so no claim of one entry can land on another. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Clay Good <hi@claygood.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
72fbe4c904 |
fix(update): close the dead end for partially populated glob artifacts (#1733)
* fix(update): close the dead end for partially populated glob artifacts `artifactOutputExists` returns true as soon as a single file matches a glob `generates`, so a `specs/**/*.md` artifact is `done` after the first delta spec. `/opsx:continue` selects only `ready` artifacts and never revisits it. The update templates forbid creating new files under a glob artifact and point the user to `/opsx:continue` instead, which cannot act on it. A capability spec the coherence review finds missing therefore has no supported way to be created. Allow update to write that file: concrete path only, rules fetched from `openspec instructions`, and the same confirm-before-write rule as every other revision. Creating an artifact that has no files at all stays out of scope - that one is genuinely `/opsx:continue`'s job. * fix(update): tighten glob gap write guardrails * fix(update): narrow continue handoff to empty artifacts * fix(update): harden glob gap creation guidance * fix(update): preserve creation safeguards for glob companions * fix(update): integrate glob guidance with current workflows * docs(skills): note update's glob companion-file exception docs-lab/reference/skills.md said update creates nothing new, which this change makes false for glob artifacts. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * test(parity): restore the #1837 regression tests dropped in the merge The earlier conflict resolution took our whole side of the parity file, which discarded the two threshold tests main gained in #1940. Take main's file verbatim and regenerate the hashes instead. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * test(explore-docs): allow the update guidance line that says "no files yet" main's #1833 guard flags any docs line containing "no files". The new /opsx:update guidance describes which artifacts update leaves to continue, not what explore writes, so allow that exact line. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Clay Good <hi@claygood.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
ed5d386a55 |
fix(tasks): keep tests and docs inside each task group (#1955)
* fix(tasks): keep tests and docs inside each task group Closes #1952 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(tasks): carry the per-group rule to onboarding and the docs Teach the same rule where a user first meets task groups, and stop the published schema reference from quoting instruction text that drifted two revisions behind schema.yaml. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(tasks): scope the per-group rule to the work each group does The MUST read as an absolute per-group requirement while the worked example's Setup group carries neither tests nor docs. Scope the rule to what a group's work calls for and name the scaffolding case explicitly, so the rule and its example agree. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
1d35e90880 |
fix(windows): preserve a file's existing line endings on rewrite (#1958)
* fix(windows): preserve a file's existing line endings on rewrite The parsers normalize CRLF to LF on read, but nothing restored it on write. On a Windows checkout (core.autocrlf=true) that turned every rewrite into a whole-file change: applying a delta that added one requirement produced a diff of 21 insertions and 14 deletions, burying the real change. Archiving the same spec now writes 7 insertions and 0 deletions. - specs-apply: write an updated spec back with the convention the file already used; a spec that does not exist yet stays LF. - file-system: same fix for updateFileWithMarkers, so installing shell completions into a CRLF .bashrc/.zshrc no longer leaves mixed endings, which bash reports as "$'\r': command not found". - pack-version-check: spawn npm through cross-spawn, since execFile cannot resolve npm.cmd on Windows. Adds src/utils/line-endings.ts for the detect/restore pair, plus tests pinning the CRLF round trip through the real write paths. Also adds regression tests for path containment under Windows case variance: path.win32.relative already folds case, and those tests pin both halves of the contract so a future "case-insensitive" change cannot quietly loosen the traversal guard. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(windows): keep removeMarkerBlock on the file's own newline Addresses the two review points and one more instance of the same bug. `removeMarkerBlock` collapses a run of blank lines, and rebuilt the separator as a bare '\n' regardless of the file it came from. Removing a managed block from a CRLF CLAUDE.md or rc file therefore left a lone LF behind - the mixed ending this PR exists to prevent. It now uses the newline it already detects for the trailing ending. Test fixes: - `marker-updates.test.ts`: close `describe('line endings')` so `removeMarkerBlock` is no longer nested inside `updateFileWithMarkers`. - `path-containment.test.ts`: exercise `FileSystemUtils.assertPathWithin` and `resolveProjectArtifactPath` instead of a private copy of the containment logic, which passed whatever the production guard did. The guard had no coverage at all; a prefix-comparison regression now fails the sibling case. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(windows): read the file's convention consistently, and only ENOENT as absent Three follow-ups from CodeRabbit's pass on the superseding PR. `writeUpdatedSpec` turned every read error into "no previous file", so an existing but unreadable spec was treated as absent and rewritten as LF. Only ENOENT means absent now; everything else propagates. `removeMarkerBlock` chose CRLF whenever the content held one anywhere, so a single stray CRLF in an otherwise-LF file pulled the whole rewrite to CRLF. It now uses detectLineEnding, the same dominant-ending reading matchLineEnding uses, so both write paths agree. Added the alias-path case the containment suite was missing: a directory link inside the root that resolves outside it. That exercises the canonicalization half of the guard, which a lexical check cannot do - the link's own path looks contained. Skipped where creating a directory link needs a privilege the runner lacks. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Travis James <travis@tribehealthsolutions.com> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
8826c0c4a1 |
fix(validate): require marker punctuation after a leading TBD/TODO (#1912)
* fix(validate): require marker punctuation after a leading TBD/TODO Closes #1897 * fix(validate): keep a shouted TODO a placeholder marker Requiring marker punctuation after a leading TBD/TODO fixed the Spanish and Portuguese false positive, but it also stopped reporting the plainest unwritten Purpose there is: `TODO write this once the capability settles down.` Case is what actually separates the marker from the word. In capitals it is the marker whatever follows it. In any other case it is a marker only when punctuation or the end of the line says so, which is how the lowercase forms an agent leaves behind are written (`todo - `, `tbd.`) and is not how a Spanish sentence opens. Covers `todo el ...` in lowercase too, which the capitals-only reading of the original fix would have reported. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs(changeset): drop the trailing space inside a code span markdownlint MD038. Changeset text only; no behaviour change. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Clay Good <hi@claygood.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
f179ed4e40 |
chore(deps): bump @inquirer/core to 12.0.0 and @changesets/cli to 3.0.3 (#1954)
* chore(deps): bump @inquirer/core to 12.0.0 and @changesets/cli to 3.0.3 Consolidates Dependabot #1930 and #1931 into one PR so the flake.nix pnpmDeps hash is computed once against the final lockfile. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * chore(nix): refresh pnpmDeps hash for the new lockfile Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
1515edbbbd |
docs(community): add openspec-guard (#1845)
* docs(community): add openspec-guard A CLI and GitHub Action that reports which OpenSpec scenarios are covered by a Vitest or Jest test, without running the tests. Closes #1844. * docs(community): update spec-guard link --------- Co-authored-by: Clay Good <hi@claygood.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
02d8c243c4 |
docs(troubleshooting): explain missing workflow commands (#1941)
* docs(troubleshooting): explain missing workflow commands * test(docs): guard command troubleshooting guidance * docs(setup): move workflow recovery to docs lab * test(docs): scope core workflow assertions |
||
|
|
fd56e12c9e |
fix(artifact-graph): support brace expansion and extglob output patterns (#1885)
* fix(artifact-graph): support brace expansion and extglob output patterns (#1854) * fix(artifact-graph): preserve literal output filenames * fix(artifact-graph): confine expanded brace patterns * test(artifact-graph): cover later brace ranges and specify glob contract * test(artifact-graph): resolve brace globs with Windows separators --------- Co-authored-by: 胥寅 <xuuyin@dingtalk.com> Co-authored-by: Clay Good <hi@claygood.com> |
||
|
|
0b5ce44b55 |
fix(templates): align approval thresholds (#1940)
* fix(templates): align approval thresholds * fix(onboard): preserve implementation choice * test(onboard): reject premature implementation prompt |
||
|
|
fe81461d51 |
chore(deps): bump the production-dependencies group with 2 updates (#1929)
* chore(deps): bump the production-dependencies group with 2 updates Bumps the production-dependencies group with 2 updates: [yaml](https://github.com/eemeli/yaml) and [zod](https://github.com/colinhacks/zod). Updates `yaml` from 2.9.0 to 2.9.1 - [Release notes](https://github.com/eemeli/yaml/releases) - [Commits](https://github.com/eemeli/yaml/compare/v2.9.0...v2.9.1) Updates `zod` from 4.5.4 to 4.6.5 - [Release notes](https://github.com/colinhacks/zod/releases) - [Commits](https://github.com/colinhacks/zod/compare/v4.5.4...v4.6.5) --- updated-dependencies: - dependency-name: yaml dependency-version: 2.9.1 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: production-dependencies - dependency-name: zod dependency-version: 4.6.5 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: production-dependencies ... Signed-off-by: dependabot[bot] <support@github.com> * chore(nix): update pnpm dependency hash Matches the production dependency lockfile update and fixes Nix Flake Validation. --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Clay Good <hi@claygood.com> |
||
|
|
2a8500a849 |
chore(deps): bump the website-dependencies group across 1 directory with 3 updates (#1932)
* chore(deps): bump the website-dependencies group across 1 directory with 3 updates Bumps the website-dependencies group with 3 updates in the /website directory: [fumadocs-core](https://github.com/fuma-nama/fumadocs), [fumadocs-ui](https://github.com/fuma-nama/fumadocs) and [next](https://github.com/vercel/next.js). Updates `fumadocs-core` from 16.15.5 to 16.15.11 - [Release notes](https://github.com/fuma-nama/fumadocs/releases) - [Commits](https://github.com/fuma-nama/fumadocs/compare/fumadocs@16.15.5...fumadocs@16.15.11) Updates `fumadocs-ui` from 16.15.5 to 16.15.11 - [Release notes](https://github.com/fuma-nama/fumadocs/releases) - [Commits](https://github.com/fuma-nama/fumadocs/compare/fumadocs@16.15.5...fumadocs@16.15.11) Updates `next` from 16.3.4 to 16.3.5 - [Release notes](https://github.com/vercel/next.js/releases) - [Commits](https://github.com/vercel/next.js/compare/v16.3.4...v16.3.5) --- updated-dependencies: - dependency-name: fumadocs-core dependency-version: 16.15.11 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: website-dependencies - dependency-name: fumadocs-ui dependency-version: 16.15.11 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: website-dependencies - dependency-name: next dependency-version: 16.3.5 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: website-dependencies ... Signed-off-by: dependabot[bot] <support@github.com> * fix(website): await llms index generation Adapts the llms.txt route to the asynchronous Fumadocs 16.15.11 API and restores the production build. --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Clay Good <hi@claygood.com> |
||
|
|
1d2f8f2b75 | OpenSpec-UI -> OpenSpec Workbench (#1934) | ||
|
|
fe429a13dc |
fix(kilocode): generate commands in canonical directory (#1938)
* fix(kilocode): generate commands in canonical directory * fix(kilocode): preserve unrelated legacy workflows |
||
|
|
5b55263775 |
fix(init): clarify Codex desktop skill usage (#1744)
* fix(init): clarify Codex desktop skill usage * fix(init): clarify Codex desktop skill usage * test(init): cover Codex hints across delivery modes --------- Co-authored-by: Clay Good <hi@claygood.com> |
||
|
|
a5ceea32cf |
fix(validate): report what a MODIFIED block adds, not only what it drops (#1809)
* fix(validate): report what a MODIFIED block adds, not only what it drops When the scenario-loss guard fires it names the scenarios the block omits, which is the whole message. A reader's next question is what the block put there instead, and answering it separates the two cases the guard cannot: a block that omits two names and introduces two is shaped like a rename, one that omits two and introduces none is shaped like a truncation. Today that costs opening both files. Add the counts and the introduced names to the message. This decides nothing. Intent is not recoverable from structure, the guard fires exactly as before, and the exit code is unchanged. The comparison already walked one direction, so the other is the same pass run the other way. Both directions now come from diffScenarioNames, and both commands print one shared sentence, so archive and validate cannot drift on what they report any more than they can on what they catch. findMissingCurrentScenarios stays as its missing half. Also names the antecedent in validate's fix instruction, which became ambiguous once a second list of scenarios appeared before it. * chore(changeset): track the scenario balance in the loss guard message * fix(validate): clarify scenario balance diagnostic * test(validate): cover one-to-two scenario replacement --------- Co-authored-by: Clay Good <hi@claygood.com> |
||
|
|
d3d770736f |
fix: release archive lock on Windows (#1769)
* fix: release archive lock on Windows * test(archive): make the Windows claim-release regression actually fail The new test compared the lstat target against the temp-dir claim path verbatim. The command stats the resolved real path, so on macOS (/var -> /private/var) the comparison never matched, `dev: 0n` was never injected, and the test only asserted that an ordinary archive releases its claim — which already passed before the fix. Verified: it passed with the source change reverted. Match the claim by file name instead, and count the interceptions so the test fails loudly if the mock ever goes inert again rather than silently passing. With the source change reverted the test now fails as intended. Also add the missing changeset for the user-visible fix. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * test(archive): keep replaced claims with absent device ids * test(archive): retain claim when zero-device path identity changes --------- Co-authored-by: Clay Good <hi@claygood.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
e09916e232 |
docs(start): explain the first change in a new project (#1146)
* docs: add beginner workflow guide * docs(start): guide the first change in a new project * docs(start): make greenfield guidance tool-neutral * docs(start): use portable first-change prompts --------- Co-authored-by: Clay Good <hi@claygood.com> |
||
|
|
c681df7058 |
docs(install): document Homebrew (#1946)
* docs(install): document Homebrew * docs(install): clarify the Node prerequisite |
||
|
|
91f2925c63 |
docs(config): document opener settings (#1945)
* docs(config): document opener settings * test(config): harden opener argument contract |
||
|
|
416599a85e |
docs(copilot): document workflow rediscovery (#1943)
* docs(copilot): document workflow rediscovery * test(copilot): pair discovery claims * docs(copilot): harden workflow recovery guidance |
||
|
|
0dde57b401 | fix(continue): keep active prompts from becoming tool calls (#1944) | ||
|
|
a64303fe1e |
fix(update): report legacy Codex bootstrap failures (#1939)
* fix(update): report legacy Codex bootstrap failures * fix(update): preserve failures after declined migrations * test(update): cover every bootstrap failure exit |
||
|
|
02fade2077 |
ci: bump the github-actions group across 1 directory with 4 updates (#1928)
Bumps the github-actions group with 4 updates in the / directory: [pnpm/action-setup](https://github.com/pnpm/action-setup), [DeterminateSystems/nix-installer-action](https://github.com/determinatesystems/nix-installer-action), [DeterminateSystems/magic-nix-cache-action](https://github.com/determinatesystems/magic-nix-cache-action) and [changesets/action](https://github.com/changesets/action). Updates `pnpm/action-setup` from 6.0.10 to 6.1.0 - [Release notes](https://github.com/pnpm/action-setup/releases) - [Commits](https://github.com/pnpm/action-setup/compare/0977fd99725f1db4007ccb2928dbb4e90d06cc86...ea17c68df8912ef543352723c149a84f56e3d413) Updates `DeterminateSystems/nix-installer-action` from 22 to 23 - [Release notes](https://github.com/determinatesystems/nix-installer-action/releases) - [Commits](https://github.com/determinatesystems/nix-installer-action/compare/ef8a148080ab6020fd15196c2084a2eea5ff2d25...3138316df39ed29be04236d7ffc686fa525866aa) Updates `DeterminateSystems/magic-nix-cache-action` from 14 to 15 - [Release notes](https://github.com/determinatesystems/magic-nix-cache-action/releases) - [Commits](https://github.com/determinatesystems/magic-nix-cache-action/compare/908b263ff629f4cc17666315b7fd3ec127c6244d...84c0677f58dcedf3b91f8223ce36a9ea5b3c84b7) Updates `changesets/action` from 2.1.1 to 2.1.2 - [Release notes](https://github.com/changesets/action/releases) - [Changelog](https://github.com/changesets/action/blob/main/CHANGELOG.md) - [Commits](https://github.com/changesets/action/compare/8488615a623b1b9c987934bb89eae8af6a946ac1...ae32849d5ba541f9ae29e40e22a623bc13562f51) --- updated-dependencies: - dependency-name: pnpm/action-setup dependency-version: 6.1.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: github-actions - dependency-name: DeterminateSystems/nix-installer-action dependency-version: '23' dependency-type: direct:production update-type: version-update:semver-major dependency-group: github-actions - dependency-name: DeterminateSystems/magic-nix-cache-action dependency-version: '15' dependency-type: direct:production update-type: version-update:semver-major dependency-group: github-actions - dependency-name: changesets/action dependency-version: 2.1.2 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: github-actions ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> |
||
|
|
518e1a0124 |
fix(legacy-cleanup): read a legacy command through the handle it checks (#1905)
isGeneratedLegacyCommand lstat'ed a path and then re-read it by path, so the file judged "generated" could differ from the file read (CodeQL js/file-system-race, alert #524, added by #1874). Open once with O_NOFOLLOW|O_NONBLOCK, fstat that handle, and read from it. Windows lacks O_NOFOLLOW, so links are still refused there via lstat. Co-authored-by: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
bae58cf614 | docs: fix Docslab links to unfinished pages (#1903) | ||
|
|
634c557bd0 |
Version Packages (#1896)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>@fission-ai/openspec@1.13.1 v1.13.1 |
||
|
|
eb03b9e933 |
chore(changeset): track #1835 security hardening and #1785 nix completions (#1902)
Co-authored-by: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
3312af4799 |
fix(templates): open generated artifacts with a top-level heading (#1777)
* fix(templates): open generated artifacts with a top-level heading
Generated proposal.md, design.md, spec.md and tasks.md started on a
section header, so every OpenSpec artifact tripped markdownlint MD041
("first line in a file should be a top-level heading") in editors that
run it. The files were also, literally, documents without a title.
Each packaged template now opens with `# Proposal`, `# Design`,
`# Spec Delta` or `# Tasks` followed by a blank line, and
`openspec schema init` scaffolds custom templates the same way. The
schema's own examples and the customization docs match.
Titles are inert to every reader downstream: the parsers anchor on `##`
and `###`, and archive builds a new main spec from the delta's sections,
so the main spec keeps its own generated `# <capability> Specification`
and only that one.
Closes #1138
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* test(templates): compare template headings on normalized line endings
The Windows runner checks out CRLF, so splitting the template on "\n"
left the blank second line as "\r" and the new guards failed there while
passing everywhere else. Normalize before splitting; verified against a
CRLF copy of the templates locally.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* test(templates): pin each template to its own title
Review feedback: the guards accepted any top-level heading, so a wrong
title would have passed, and the archive check counted `# ` lines only,
so a demoted `## Spec Delta` would have slipped through. Assert the exact
heading per artifact, the blank line under it in both guards, and that no
heading of any level named "Spec Delta" survives archive.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(workflows): teach the artifact titles everywhere the shape is shown
The templates were only half the story. The onboarding walkthrough drafts
each artifact in the conversation and then saves what it drafted, so its
previews would have written untitled proposal.md, spec.md, design.md and
tasks.md whatever the template said. The sync workflow's delta format
reference had the same gap, sitting directly beneath a main-spec
reference that does carry a title. Both now show the template's title,
and `docs/opsx.md` no longer documents a `template` value the CLI never
returned.
Guards added:
- The template guard now enumerates every artifact of every packaged
schema from schema.yaml rather than a hardcoded list of four, and the
exact-title table must name every artifact the schema declares.
- A drift guard reads the titles out of the packaged templates and
requires the onboard and sync surfaces to show those same titles, so
guidance and template cannot part ways again.
- Parser tests pin the claim the fix rests on: a title is inert, and a
spec or proposal parses identically with and without one.
Regenerated the skill mirrors and parity hashes for the two workflows
touched; no other hash moved.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* docs: move the artifact-title update to the canonical docs-lab tree
The live site builds from docs-lab/, so the exact-format page there is the
one readers see. It still showed all four templates opening on a section
header and described the delta as starting with `## Purpose`.
- reference/schemas/spec-driven/index.md: each template block now matches
the shipped template byte for byte, and the quoted spec/tasks
instructions match schema.yaml again.
- customize/schemas.md: one line telling fork authors to keep the `#`
title on the first line.
Reverts the edits to docs/customization.md and docs/opsx.md: that tree is
no longer published and docs-lab/README.md keeps it as source material
only, so editing it would leave two versions of the same fact.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(show): keep the change id as title for a bare `# Proposal` heading
The packaged proposal template now opens with `# Proposal`, which
`extractTitle` read as the change's title, so `show --json` and
`change list --json/--long` titled every templated change "Proposal"
instead of its id. Treat that bare heading as untitled.
Also re-quote the proposal and specs instructions in the canonical
docs-lab schema page after #1700 changed schema.yaml.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
5f5914e7f7 |
fix(skills): match natural "openspec <verb>" phrasing to its workflow (#1852)
* feat(skills): match natural "openspec <verb>" phrasing to its workflow Users and agents say "openspec propose" / "openspec apply", but no workflow skill description contained that phrasing, so an agent hearing it had nothing to match and routinely hand-built the artifacts with the CLI instead of running the workflow. Each workflow skill's description now names the phrasings that should route to it. `openspec update` is deliberately left unclaimed: it is a real CLI command that refreshes generated files, unrelated to the update-change workflow, so that skill claims "openspec update change" instead. Descriptions are emitted as unquoted YAML plain scalars, so the new tests also pin that the generated frontmatter still parses and the description round-trips. Closes #1221 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(skills): derive the CLI-collision guard instead of hardcoding it Review found the guard codified the one exception rather than the rule, so it could never catch the next collision. It now reads every command name the CLI registers and fails on any claimed phrase that shadows one, unless the phrase is listed in DELIBERATE_CLI_PHRASE_CLAIMS with a reason. Two routing fixes fall out of stating the rule: - bulk-archive also claims "openspec archive all", so an exact-phrase match on "openspec archive" no longer pulls a multi-change request to the single-change skill. - update-change now disclaims the openspec update CLI command in prose, not only by avoiding the string. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(skills): drop the trailing clause and close the plural-archive hole Review found the "- follow this skill rather than doing the work by hand" trailer was decoration that contradicted two of the skills it was appended to: sync-specs opens "This is an agent-driven operation - you will read delta specs and directly edit main specs", and explore says "This is a stance, not a workflow. There are no fixed steps." A description is read at selection time, so the clause could not reach the hand-building it targeted anyway; the bodies already carry that guidance. Removing it from all 12 also drops ~800 chars of identical boilerplate that made update-change's CLI redirect read as filler. Routing fixes: - bulk-archive claims the plural phrasings that do not contain "all", so "openspec archive these three changes" no longer loses to the single-change skill on the bare literal. - update-change redirects to the CLI command positively instead of negating ("run that command instead"), which routers honor far better than "not for". - apply also claims "openspec implement", the natural English verb for it, which shadows no CLI command. Corrects the recorded reason for claiming "openspec archive": the CLI command does merge delta specs (docs/cli.md:631, src/core/archive.ts:1402). The real reason is that the workflow confirms and verifies the merge before anything moves, where the bare command does it in one shot. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs(quickstart): name the verb phrasing that now routes to a workflow Also rewrites the changeset to house style: links the issue, names the commands-only scope limit, and tells a reader they need `openspec update` to pick it up. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(skills): walk the real command tree instead of scanning the entrypoint Mutation testing found the collision guard was a strict subset of reality, not the superset its comment claimed. It scanned src/cli/index.ts for `.command('…')`, but seven groups — spec, config, schema, store, doctor, context, workset — are registered from their own modules, so 23 real command names were invisible. A description claiming "openspec doctor" or "openspec spec" passed 18/18 green. It now walks the commander tree from the exported `program` (importing it does not parse argv; runCli does that), and a sanity test pins the seven delegated groups so the blind spot cannot come back. Three more holes the same pass found, all confirmed by re-running the mutations that previously slipped through: - phrase extraction was case-sensitive and double-quote-only, so "Openspec update" and `openspec update` in backticks both evaded every guard. Matching is now case-insensitive and accepts either delimiter. Unquoted prose stays excluded on purpose: the update-change redirect names the CLI command in prose, and prose is not a routing trigger. - prefix shadowing was unguarded, which is the exact shape of the archive/bulk-archive tension. A shorter phrase contained in another skill's longer phrase must now be declared in DELIBERATE_PHRASE_SHADOWING. - both allowlists accepted an empty reason and never flagged stale entries. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * test(skills): let the CLI-collision guard ignore hidden workflow-verb hints PR #1776 registers the workflow verbs (explore, propose, apply, ...) as hidden CLI commands that only point the user at the workflow. Walking the commander tree then saw "openspec explore" as a real command and failed the collision guard for every skill trigger. Skip a subcommand only when it is hidden AND named after a workflow. Visible commands and hidden non-workflow commands are still guarded, pinned by a synthetic commander tree. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs(changeset): drop em dashes from the release note Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(skills): drop the generic by-hand clause from the explore description The other eleven descriptions dropped it; explore is a stance, not a workflow, so telling the agent to follow it instead of doing the work contradicts it. Adds a regression over every workflow description. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
9827762d2d |
fix(skills): stop workflows from adopting a project that never ran init (#1787)
* fix(skills): stop workflows from adopting a project that never ran init Generated skills and commands are installed once per machine and offered in every repository the agent opens, including ones with no OpenSpec at all. Nothing stopped the workflow there: root resolution falls back to an implicit root at the current directory, so `openspec new change` quietly creates `openspec/` in whatever repo the agent happened to be standing in (#1645). Two changes, both in the generated instructions: - Every workflow now carries a shared project check. Before the first step that writes, the agent reads `root.source` from `openspec status --json`; `implicit` (or a `No OpenSpec root found` error) means the project is not set up, and the agent stops and asks the user whether to run `openspec init`, target a store, or drop OpenSpec for that request. It may not initialize the project on its own or let a command create the root as a side effect. - Every deployed skill description now names OpenSpec. Hosts pick skills by description, and "Enter explore mode - a thinking partner..." reads as a generic offer in a repository that has never heard of OpenSpec. Closes #1645 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * chore(changeset): note the uninitialized-project guard Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(skills): let onboarding run init once the user asks for it The guard read as an absolute ban on `openspec init`, which contradicts the option it offers one sentence earlier and the onboard workflow's job. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs(troubleshooting): explain an OpenSpec workflow starting in an unset-up project Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(skills): check the root with a command that never fabricates one `openspec status --json` demands --change once a project has changes, so the guard's own check could fail in exactly the projects it should wave through. `openspec list --json` answers in one shape everywhere: a root object when the project is set up, `root: null` both when nothing is set up and when only stores are registered. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * test(skills): pin the guard against every write, not the first fence CodeRabbit's point: checking only the first ```bash fence would miss a write outside a fence. Assert instead that nothing preceding the guard runs a command or writes, and that the guard sits directly under the store-selection guidance - both fail when the guard is moved down a workflow. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * feat(new-change): say when the command had to create the root itself The generated workflows now check for a root before writing, but the guard is instructions - an agent that ignores it, or a human running the CLI directly, still turned an unset-up directory into an OpenSpec project without a word. Creating the root stays zero-config; it is no longer silent. Human output only: --json is unchanged, and `root.source` already carried the same fact for programmatic callers. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(skills): tell agents the root check's non-zero exit is the answer `openspec list --json` exits 1 when there is no root. An agent that reads that as a broken CLI is one step from hand-creating `openspec/` instead, which is the failure the guard exists to prevent. Also drops a vacuous assertion: the notice test now checks that the note names the directory it created and that the change really landed there. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * style(skills): read the guard back and untangle its two 'in that case' clauses Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(skills): carry the store flag into the root check; pin the notice path exactly CodeRabbit, both valid: - With a store selected the store IS the root, so the check has to run as `openspec list --json --store <id>`. The store-selection paragraph above already says to append the flag to every command it lists, but leaving it implicit here invited a check against the wrong directory. - The notice assertion matched any `openspec/` suffix. It now pins the exact rendered path, and a new case runs the command from a subdirectory to show the note names the directory actually adopted (and that the repo above it is left alone). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs: move the no-root contract to the canonical docs-lab pages alfred-openspec on #1787: docs-lab/README.md makes docs-lab/ canonical and the old docs/ tree legacy, and the canonical pages were stale in the two places the review named. - docs-lab/reference/cli.md, 'openspec new': documents the implicit-root notice after the 'Next:' line, with the exact output the CLI prints, that it goes to stdout and never appears with --json, and that JSON carries the same fact as root.source: implicit. Verified against a real run in an empty directory with an isolated HOME. - docs-lab/reference/skills.md: states the shared response and stop behavior once, above the index table, since it now holds for every skill: confirm the resolved root before the first write, stop when there is none, offer init, a store, or dropping OpenSpec, wait for the answer, never create openspec/ on its own. Drops the legacy docs/troubleshooting.md addition. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(skills): make the no-root answer depend on how the workflow was reached alfred-openspec's product call on #1787. One answer could not serve both arrivals: #1645 asks the workflow to get out of the way ('it can go through the normal general propose not the openspec'), while a user who typed the skill's name is owed an answer about OpenSpec. The guard now branches after the same `openspec list --json` check: - Auto-selected: the model picked this workflow without the user naming OpenSpec, naming the skill, or running its command. Drop OpenSpec and answer the request normally, with no setup question and no mention of OpenSpec. - Explicit OpenSpec request: stop before writing and ask whether to run `openspec init`, target a store, or continue without OpenSpec, then wait. Neither branch may create the root as a side effect, stated once for both. One text serves both surfaces rather than a command-only variant, because apply-change and onboard render a single body into the skill and the command alike; a command-only constant would mean threading a surface flag through bodies that deliberately have none (#1515). The bullets scope themselves instead, and a slash command is an explicit invocation, so only the ask branch can apply there. A test pins that branch reaching every generated opsx command. Four regressions: the auto-selected branch (asserting it does not mention `openspec init` or `--store`), the explicit branch, the explicit branch's presence in every command file, and the shared no-side-effect rule. docs-lab/reference/skills.md said every no-root invocation asks. It now carries the same two branches as scan anchors. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(skills): keep the root guard off store-only projects and fix its docs A store-only project whose `store:` line names a store this machine has not registered reports `"root": null` from `openspec list --json`, so the guard read a real OpenSpec project as uninitialized. The guard now checks for the `Declared in` status message first and shows the store error instead. A stale global defaultStore reports the same codes in unrelated repositories, which is why the message prefix, not the code, decides. Propose's context step from #1657 offered `openspec init` on `no_openspec_root` regardless of how the workflow was reached. It now defers to the project check, so an auto-selected propose skill stays silent. The changeset names both no-root branches, and the `new change --json` docs separate the initialized example from the verified `implicit` one. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(skills): keep the root guard off projects with a malformed store line A config-only project whose `store:` line is malformed reports `"root": null` with an `Invalid store declaration in` message, not `Declared in`, so the project check read it as never initialized and would drop OpenSpec or offer `openspec init` there. The guard now names both prefixes, and a root-selection test pins that every declaration failure starts with one of them while a stale global defaultStore starts with neither. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * test(skills): check guard ordering in the skill body, not its frontmatter A skill's YAML frontmatter is metadata a host reads to choose the skill, not instructions the agent runs, so a description that quotes a command name must not trip the ordering check. Scope the scan to the text after the closing frontmatter delimiter; a command injected into the body ahead of the guard still fails. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
11a9691524 |
fix(tasks): count unrecognised checkbox markers as not done (#1773)
* fix(tasks): count unrecognised checkbox markers as not done A checkbox marker the task parser did not recognise was dropped from progress entirely: it counted toward neither the numerator nor the denominator. A tasks.md whose remaining work was written `- [~] ...` therefore reported `✓ Complete` in `openspec list`/`status`, and `openspec archive` raised no incomplete-task warning for it. Marking open items with such a marker *shrank* the denominator instead of leaving them counted as not-done. Widen the marker to any single non-`]` character and keep `x`/`X` as the only done state, so an unrecognised marker reads as not-done - the fail-safe direction the pattern's own docblock argues for. OpenSpec adopts no new marker semantics; it just stops losing the line. Closes #1761 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(tasks): close the remaining silent-loss holes in checkbox parsing Hardening pass on the #1761 fix. Parser: an empty `[]` and a padded `[ x]` were dropped exactly the way `[~]` was - checkbox-like lines counting toward neither the numerator nor the denominator, so archive stopped warning about them. The marker is now "at most one non-whitespace token", which covers all three. Multi-character brackets stay unmatched on purpose: widening to `[^\]]*` would match `- [Some doc](./doc.md)`, whose `]` is followed by `(` rather than a space, and turn every Markdown link list into phantom unfinished work. The docblock states that boundary and a test pins it. Guidance: archive, bulk-archive and verify told agents to count `- [ ]` vs `- [x]` by hand, which reproduced the same bug one layer up - an agent following it saw no incomplete tasks in a `[~]` file even with the CLI fixed. All six bodies (skill + command per workflow) now state the rule the parser implements; skills/ mirror and parity hashes regenerated. Coverage now spans every consumer of the shared counter: `openspec list`, archive's gate, the apply task list, validate's task-numbering check and the parser itself, including the reported 42-done/17-deferred ratio. Reverting only src/utils/task-progress.ts fails 7 of them. Verified empirically: the widened pattern newly matches 0 lines across all 1090 .md/.ts files in the repo, templates and docs included. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(tasks): state the padded-tick rule in workflow guidance too CodeRabbit review, verified and valid: the new guidance named only exact `[x]`/`[X]` as complete, while the parser also counts `[ x]`, `[x ]` and `[ x ]`. An agent hand-counting by that wording would have reported a padded tick as unfinished work and disagreed with `openspec list` - the same guidance/CLI split this PR set out to close. All six bodies now phrase it as the parser implements it: complete means the box holds only `x`/`X`, spacing inside the brackets ignored; every other marker, including an empty box, is incomplete. skills/ mirror and parity hashes regenerated. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(tasks): keep one-character link bullets out of the task count alfred-openspec on #1773: the widened marker class let a one-character Markdown link label parse as a checkbox, so `- [A](https://example.com)` and `- [1](./one)` read as unfinished tasks and made progress and archive report phantom work. The multi-character guard did not cover them: a one-character label is one token. The closing bracket may now not be followed by `(` or `[`, the only two characters that continue Markdown link syntax. Nothing that used to count is lost: a checkbox is followed by its description or by end of line, and `- [x]done` still parses. Regressions cover both link forms, the reference form, and a link inside a real task description. Also updates the canonical contract, which said tasks not written `- [ ]` are not tracked while this change deliberately tracks `[~]`, `[-]`, `[]` and a padded `x`. Fixed in schemas/spec-driven/schema.yaml and in the docs-lab page that quotes it, so the quote stays faithful. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs: say that a done checkbox is case-insensitive CodeRabbit on #1773: the parser lowercases the marker before comparing it with `x`, so `- [X]` is done, but the contract I added said only `- [x]` counts. Corrected in both copies, which are the same text. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(tasks): keep empty boxes followed by link syntax in the task count The one-character link guard also dropped `- [ ](...)` and `- [ ][...]`, which the strict pre-#1761 pattern counted as unfinished tasks. A line the parser drops is one archive stops warning about, so a whitespace-only box now bypasses the guard. The canonical tasks instruction also names `[-]` and the padded `x` rule, in schema.yaml and docs-lab in lockstep. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs(changeset): drop em dashes and state the padded-x done rule Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
62106f40e3 |
fix(explore): name the propose workflow at every handoff (#1788)
* fix(explore): name the propose workflow at every handoff Explore mode refuses to implement, but nowhere named the workflow that turns the discussion into a change. The refusal, the "flow into a proposal" ending, the closing summary, and the do-not-implement guardrail all described the next step as prose. With no named exit, agents answered the discovery questions and then started writing code (#869). All four handoff points now point at `/opsx:propose`, written in the canonical `/opsx:<id>` form so each tool renders the invocation it actually registers. Skill and command bodies are patched together, the skills.sh mirror is regenerated, and parity hashes are refreshed. Closes #869 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(explore): name the continuation after a seamless capture The capture path let explore scaffold a change and write artifacts, then said nothing about what came next. An agent holding a fresh proposal inside explore mode has an obvious wrong next move, and it is the one #869 reported. The capture now ends by naming `/opsx:propose` for the remaining planning artifacts and `/opsx:apply` for implementation, and says plainly that capturing artifacts is not permission to implement them. Widen the rendering guard to walk the real registries instead of a hand-picked few: every registered command adapter and every entry in AI_TOOLS must rewrite every canonical reference in both bodies, with no `/opsx:` form surviving on any skills surface. A new adapter or a changed invocation shape now fails here rather than shipping a command nobody answers to. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs(changeset): cover the capture-path handoff Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(explore): resolve handoffs against the installed workflow set A custom profile can install explore without propose or apply, and the explore skill and command still named both. Handoffs are now authored with optionalWorkflow() and resolved in getSkillTemplates()/getCommandTemplates() against the workflow filter every init/update path already passes. Missing workflows fall back to explore's own capture path and the openspec instructions apply CLI. Output with every workflow installed is unchanged. Uses the same API and marker syntax as #1775 so the two compose. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs(changeset): drop em dashes from the release note Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
e67ac47f3a |
fix(bulk-archive): check the archive target before moving changeRoot (#1829)
* fix(bulk-archive): check the archive target before moving changeRoot Step 8c ran `mv` with no existence check. POSIX `mv` moves changeRoot inside an existing target directory and exits 0, so a same-day name collision produced archive/<target>/<target>/ and was recorded as a successful archive. The workflow already promised the opposite in three places: the guardrail "If archive target exists, fail that change but continue with others" and both failure output templates listing "Archive directory already exists". The single-change archive workflow implements the check; the bulk path did not. Closes #1827 * fix(bulk-archive): check archive targets before the first spec write The existence check at the move ran after step 8a had already synced the change's delta specs, so a collision still left main specs rewritten for a change that stayed active. `openspec archive` settles the destination before touching any spec; the batch now does the same in step 3 (including two selected changes that resolve to the same target), keeps blocked changes out of sync, conflict resolution, and the archive-everything option, and re-checks just before the move. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs(changeset): describe the pre-sync archive target check Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(bulk-archive): undo a nested move and keep blocked changes failed The last target check and `mv` are separate steps, so a target created in between still nested the change with exit 0. Step 8c now confirms the move did not land inside the target and moves the change back, recording `Archive directory already exists`, instead of reporting success. The ready-only option recorded every non-Ready change as Skipped, which misreported archive collisions; Blocked changes now stay Failed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * test(bulk-archive): require the move before asserting the nest check follows it Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(bulk-archive): say the guardrail's dated target is the one step 3d recorded The guardrail still read 'uses current date', which invites recomputing the name at the move. It now points to the step 3d value, matching step 8c. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: choi138 <dev@silviahealth.com> Co-authored-by: Clay Good <hi@claygood.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
605d9e7a2b |
fix(config): leave an unparseable global config untouched (#1876)
* fix(config): leave an unparseable global config untouched A typo in config.json made getGlobalConfig() fall back to defaults, which telemetry read as consent: any command, even a read-only list, minted a new anonymous id and wrote it over the whole file, dropping a telemetry.enabled false opt-out and every other setting. config set, unset and profile likewise saved the defaults over it. saveGlobalConfig() and telemetry's writeConfig() now refuse to overwrite a file they cannot parse, telemetry and the update check treat such a file as opted out, and config set, unset and profile exit with an error pointing to config edit. config reset --all can still replace the file, and the existing warning is unchanged. * fix(config): treat a non-object global config as unreadable Valid JSON that is not an object (null, an array, a string) also makes getGlobalConfig() fall back to defaults, silently, so `config set` still saved those defaults over the user's file. isGlobalConfigUnreadable() now reports such a file as unreadable, which keeps telemetry off and routes every save through the same refusal as a parse failure. This matches how completion-tip already treats a non-object config. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs(config): document the refusal to rewrite an unparseable config docs-lab/reference/cli.md said `config unset` always exits 0. With an unparseable global config, `config set`, `config unset` and `config profile` now exit 1 and leave the file unchanged; say so, show the message and the two fixes, and note telemetry and the update check stay off until it is fixed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(config): warn about an unparseable global config once per command Telemetry, the update check and the command each read the global config, and now that none of them rewrites the broken file, the "Invalid JSON" warning printed two or three times per command. Warn once per path. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(migration): skip profile migration for a config that is not a JSON object A global config holding [] reached saveGlobalConfig, which now refuses it, so init and update failed. null already crashed on a property read. migrateIfNeeded now skips such a file, as it does for a parse failure. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(telemetry): refuse to write over a non-object global config The telemetry writer had its own notion of an unreadable config: only a JSON parse failure counted. Valid JSON that is not an object slipped through, so updateTelemetryConfig() merged into it and replaced the file -- an array, a number or a boolean became a bare telemetry object, a string spread into numeric character keys, and null threw a TypeError instead of the actionable refusal every other writer reports. Funnel both notions through one predicate: isConfigRootObject() in core/global-config.ts now backs isGlobalConfigUnreadable() and the telemetry reader, so every shape the global guard rejects is classified invalid on read and refused on write. Both writers report the same one-line message via unreadableGlobalConfigMessage(). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(config): read a non-object global config as plain defaults getGlobalConfig() spread the parsed root into its result before the unreadable predicate was consulted, so the shape of the root leaked to every caller: a config of "abc" returned defaults plus the numeric character keys 0, 1 and 2. Check isConfigRootObject() right after parsing and answer with plain defaults, as for a file that did not parse at all. Reported by CodeRabbit as an outside-the-diff finding. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(config): stop `config list` crashing on a null config root `config list` re-reads the raw file to mark each value explicit or default, and assigned JSON.parse() straight to rawConfig. A root of `null` then crashed the command with a TypeError stack trace, the one failure mode this PR is meant to remove, and it did so on a read-only command. Normalize a non-object root to {} through the shared isConfigRootObject() predicate so the listing shows plain defaults. Reported by CodeRabbit as an outside-the-diff finding. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * test(config): show the telemetry notice before the first-run write check Since #1835, nothing is tracked until the notice has been shown, so the first-run test must show it before tracking the command. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Clay Good <hi@claygood.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
626269ed73 |
fix(templates): stop generated skills naming workflows the profile omits (#1775)
* fix(templates): stop generated skills naming workflows the profile omits The `core` profile installs six of the twelve workflows, but the update and apply templates named `/opsx:continue` (6 times) and `/opsx:new` (twice) regardless. On a default install those became `/openspec-continue-change` and `/openspec-new-change` — skills that were never written — so `update-change` refused to create a missing artifact and handed off to a dead end. The only guard was a sentence asking the model to check availability at runtime, 70 lines above the two places it hits the wall. `command-references.ts` decides how a reference is spelled; nothing decided whether it should be emitted at all. Add that: templates author both wordings with `optionalWorkflow()`, and `getSkillTemplates()` / `getCommandTemplates()` — the one place every generation path already funnels the resolved workflow set through — pick a branch before the reference transformers run. A profile that omits a workflow now gets a concrete `openspec status` / `openspec instructions` fallback instead of a reference to a skill that does not exist. Closes #1734 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * chore(changeset): describe profile-aware workflow references Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * test(init): assert the core profile names no uninstalled workflow The end-to-end init test pinned the runtime availability hedging that #1734 is about, and asserted `/opsx:continue` appears in the default profile's generated update workflow — the bug itself. Assert the fixed behavior instead: neither `/opsx:continue` nor `/opsx:new` appears, and the CLI fallback is stated outright, for both the update and apply surfaces. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(templates): resolve every cross-workflow reference, not just core's The first commit fixed the two templates the default profile broke on. Every other cross-workflow handoff had the same shape, and arbitrary subsets are reachable: a `custom` profile is whatever the user picked, and `openspec update` re-derives a workflow set from what it finds on disk (legacy tool overrides, inferred Codex workflows) without passing it through getProfileWorkflows. So resolve all of them: - `apply` -> archive; `continue` -> apply, archive; `ff` -> apply; `new` -> continue; `propose` -> apply; `update` -> apply, archive; `archive` and `bulk-archive` -> sync. - `onboard`'s two command-reference tables are built from the installed set rather than printed in full with an "only if installed" caveat, and its explore, resume and next-step prompts are resolved the same way. Two supporting changes: - `onlyWithWorkflow()` plus a whole-line rule in the resolver: a conditional that owns its line takes the line with it when it resolves to empty, so a dropped table row cannot leave a blank line that ends the table in markdown. - `generateSkillContent()` and `generateCommand()` now throw on an unresolved marker. A generation path that skips the choke point fails loudly instead of writing `[[opsx:...]]` into a user's SKILL.md. The propose and ff surfaces keep their deliberate wording difference (#258): the command surface never invites "ask me to implement", so its missing-`apply` fallback names the CLI rather than a conversation. The guard test now runs the property over every subset that could expose a reference — each workflow alone, everything but one, the empty set, and the two shipped profiles — for skills and commands, in both spellings. Twenty-plus of those cases fail against the previous commit. Only `openspec-onboard` changes in the skills/ mirror: with every workflow installed, all other templates render byte for byte as before. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * chore(changeset): cover the full cross-workflow reference fix Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(templates): validate conditionals before choosing a branch Resolution discarded the unselected branch and only then checked for residual markers, so a truncated block inside the *missing* branch was accepted for a profile that installs the workflow and rejected for one that does not. Profile-dependent authoring errors are exactly what this module exists to remove. Validate the authored text up front instead: every marker must be one of the three recognized forms, and they must appear as a flat sequence of if / else / end. A malformed block now throws identically for every profile. The post-resolution check stays as a backstop. Caught by CodeRabbit on #1775. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs: state the profile-aware handoff rule in the skills reference alfred-openspec on #1775: docs-lab/reference/skills.md described several handoffs as unconditional while this change deliberately emits a CLI or conversational fallback when the profile omits the target. Stated once, above the entries, rather than as a caveat on each of the eleven affected Response and Creates rows: the page's recipe is one fact per row, and repeating the same conditional eleven times would bury the contracts it exists to state. The rows keep naming the skill that owns the next step, which is the fact a reader looks up; the rule above them says what happens when that skill is not installed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(workflows): fold #1735 into the profile-aware references #1735 fixes the same issue (#1734) by removing the optional handoffs outright. This PR resolves them at generation time instead, which is strictly better for the template layer: an install that has `continue` still gets told about it. So the mechanism here wins and #1735's content is folded in, rather than the two competing for the same lines. What #1735 had that this did not: - src/commands/workflow/instructions.ts. The CLI's own runtime strings named the openspec-continue-change skill. Those are chosen at run time, so optionalWorkflow() cannot reach them; taken from #1735 verbatim. - The blocked-state fallback. It was a one-line pointer; it now carries #1735's full CLI recovery (select the next `ready` artifact, not `skipped` or `blocked`, read its rules with `openspec instructions`, keep the selected `--store` on both commands) plus the tracking-file repair path and the `missingArtifacts` field it branches on. The installed branch still names `/opsx:continue`, so neither audience loses. #1735's update-change.ts rewrite is not carried over: this PR already covers all six of those sites conditionally, which is the better answer. Both of #1735's test suites come across, and they are worth more here than there. test/core/templates/profile-handoffs.test.ts asserts that no generated file names an uninstalled workflow across every tool and all three delivery modes, which is the property this PR's mechanism exists to provide, and it passes against it. test/commands/profile-handoffs.test.ts covers the runtime CLI strings. The two guards are complementary: that one is broad on tools and deliveries, this PR's own profile-workflow-references.test.ts is broad on workflow subsets. #1735's command-references.test.ts assertions could not be carried as written, since they assume the reference is gone unconditionally. Replaced with a case that resolves the template against a set without `continue` and asserts the fallback carries the whole recovery. Verified it fails when the fallback is shortened. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs: name the core-profile fallback on the two rows that hit it The rule above the entries covers every profile, but apply-change and update-change are Core skills whose rows name openspec-continue-change, which the core profile never installs. On the default install those rows now say what the generated skill points to instead: openspec status and openspec instructions. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs(changeset): drop em dashes from the release note Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
086c93b40b |
chore(deps-dev): bump changesets, eslint and typescript-eslint (#1901)
Applies dependabot #1898 (lockfile-only: @changesets/changelog-github 1.0.1, @changesets/cli 3.0.2, eslint 10.10.0, typescript-eslint 8.70.0) plus the two follow-ups its CI needed: the transitive esbuild 0.28.1 -> 0.28.2 bump in allowBuilds, and the pnpmDeps hash in flake.nix computed by the Nix job. Co-authored-by: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
7090e16d74 |
fix(schema): validate the apply block against declared artifacts (#1868)
* fix(schema): validate the apply block against declared artifacts parseSchema checked each artifact's requires but never the apply block, so schema validate passed a schema whose apply.requires named an artifact that does not exist or whose apply.tracks named a file no artifact generates. At run time apply skipped the unknown id, turning the apply gate off, or blocked forever on a tracked file nothing produces. Reject both in parseSchema, naming the bad value and what the schema declares. tracks is compared to generates by exact string, the same comparison the tracked-tasks lookups use, so any schema that parses is one they can resolve. * fix(schema): warn instead of failing the load on an unmatched apply.tracks apply.tracks is a path that apply reads as written, not an artifact id. A schema that tracks a hand-written TODO.md, or one file under a glob generates such as tasks/main.md, loads and applies correctly on main. Rejecting it in parseSchema made every command on that schema fail. Keep the unknown apply.requires id as a load error, the same as an unknown artifact requires. Report an apply.tracks path that matches no artifact's generates as a warning from `openspec schema validate`, which still exits 0. Move the docs note from legacy docs/customization.md into docs-lab (schema-yaml.md validation section, cli.md schema validate). The schema-yaml.md table had said unknown apply.requires IDs go unreported. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(schema): describe apply.tracks as a generates mismatch, not as ungenerated The apply.tracks check compares `tracks` to each artifact's `generates` with exact string equality, but its warning said the tracked file "is not generated by any artifact". That is false for the case this PR deliberately supports: `tracks: tasks/main.md` under `generates: tasks/*.md`, where the glob really does generate the file and only the strings differ. The diagnostic now names the real condition (the `tracks` value does not exactly match any `generates` value, so OpenSpec cannot tell which artifact's progress it tracks) and keeps both remedies. Both docs-lab pages, the changeset and the JSDoc that repeated the claim are corrected the same way, and a new CLI test pins that the glob case is described as a mismatch and never as ungenerated. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * test(schema): pin apply.tracks matching across Windows path separators The tracked-tasks lookup compares apply.tracks and generates as plain strings, so a backslash on one side and a forward slash on the other must warn, and the same backslash spelling on both sides must not. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Clay Good <hi@claygood.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
a5bf5c6844 |
fix(validate): report requirements outside delta sections (#1804)
* fix(validate): report requirements outside delta sections * docs(test): document orphaned-requirement test helpers * test(parser): pin orphan reporting to the reader's section rule Cover a header the reader does not match exactly (`## ADDED Requirements`), which must be reported, and a repeated `## ADDED Requirements` header with a non-delta section between the copies, which must not. Add a patch changeset matching the other parser fixes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Clay Good <hi@claygood.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
6a87a514ec |
test(store): give the git probe cleanup hook the setup's timeout (#1900)
Deleting the 12000 fixture files on the Windows runner outlasts vitest's default 10s hook timeout, so the suite failed after every test passed and knocked PRs out of the merge queue. Co-authored-by: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
fede536c27 |
fix(update-change): draft the requested edit in step 4, write only in step 5 (#1840)
* fix(update-change): draft the requested edit in step 4, write only in step 5 Step 4 told the agent to "Apply the requested edit" while step 5 and the guardrails told it to write only after the user confirms each revision. "Apply" is a write verb in this very document - step 5 is titled "Confirm and apply" - so the same request either wrote immediately or stopped and showed the revision first, depending on which passage the agent weighed. Step 5 is the workflow's only write path, so its confirmation guarantee was unenforceable whenever step 4 governed. Step 4 now drafts and says explicitly that it writes nothing; step 5 claims every write and shows the drafted edit for confirmation. Both delivery surfaces and the committed skill carry the same wording, and a regression test slices step 4 out of each body so no write verb can reappear there. Closes #1836 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(update-change): keep every edit verb out of the write-free step Adversarial review of the first commit found three gaps. Step 4 still opened a bullet with "Revise only files that already exist" - the same shape as the bug, an imperative edit verb inside the step that now declares it writes nothing. It reads as a scoping rule, but "revise" is the write verb everywhere else in this body ("proposed revision", "Which artifacts were revised"). It now says "Propose revisions only to files that already exist". The guard's `not.toMatch(/\bWrite\b/)` was inert and inverted: it was case-sensitive, so it never matched the wording it was meant to pin, and it could not be made case-insensitive because the fix's own text says "do not write anything yet". It now strips that one sanctioned sentence and rejects any remaining form of write or apply, case-insensitively - so lowercase "write the drafted edit now", the dangerous case, is caught. A second assertion rejects any step 4 bullet opening with Revise/Edit/Update/Rewrite. The changeset claimed no write verb could reappear in step 4, which was not what the old guard did. It now states what the guard checks. Also passes the surface label into section() so a marker drift names the surface that broke. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(update-change): let no passage outside step 5 authorize a write Mutation-testing the first guard found it defeatable: 20 of 25 mutations broke the #1836 contract and still passed. The two worst were structural, not lexical - the guard only sliced steps 4 and 5, so an authorization placed in the intro, in step 3, in the Guardrails or in the Output section governed the agent while no assertion ever saw it; and step 5 carried only positive assertions, so its gate could be kept and then exempted in the next sentence, or the whole-body confirmation guardrail deleted outright, with nothing failing. The guard now pins step 4's draft rule and the whole of step 5 verbatim, requires the whole-body guardrail to survive, and scans every other passage for write verbs and their synonyms (commit/save/persist/ overwrite/reapply/flush/emit), verb-free equivalents (perform, carry out, in place, to disk), consent-bypass phrasing, and imperative edit bullets including ones led by an adverb. All 22 mutations are now caught. Two wording corrections came out of the same review. "nothing earlier writes to disk" was false - every openspec invocation persists a telemetry id via the root preAction hook - so step 5 now claims every artifact write instead. Step 4 says "in the conversation, not in files", borrowing explore.ts's phrasing, so "draft" cannot be read as writing a draft file. docs/commands.md carried the same apply-vs-confirm collision two lines above the confirmation bullet, contradicting the worked example directly below it; it now says "Drafts your requested revision". Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * test(update-change): catch more imperative edit verbs outside step 5 CodeRabbit noted `- Modify the artifact now` slipped past the imperative-bullet guard. Added Modify, Amend, Patch and Replace, each verified to trip the guard. `Change` is deliberately excluded: step 6 already opens a bullet with "Change already implemented ...". Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * test(update-change): prove the write-gate scan trips in every section The outside-step-5 scan was only ever checked against the real body, so nothing showed it could fail. Injecting a verb-free authorization into the intro, Input, steps 1-2, Output or Guardrails passed on both surfaces (7 of 10 sections). The bare "already applied" allowlist entry also erased "treat the requested edit as already applied" before any check ran. - Move the scan into a function and add a mutation table: one injected authorization per section, asserted on skill and command bodies. - Spell every sanctioned mention in full context. - Flag any mention of the requested edit outside the pinned draft rule. - Widen the consent-bypass filter (needs no / exempt from / skip confirm). Also revert the legacy docs/commands.md edit: docs-lab reference/skills.md is the published page and already states the confirm-then-write contract. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs(changeset): drop em dashes from the release note Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
8fc65b7f70 |
fix(tasks): count checkboxes under every list marker (#1862)
* fix(tasks): count checkboxes under every list marker The task counter behind list, status, view, instructions apply, validate --archived and archive's incomplete-task check read only `-` and `*` bullets. A GFM task is a list item, and CommonMark also allows `+` and ordered `1.` / `1)` markers, so unchecked ordered or plus tasks were dropped from every count: the change read as complete and archive skipped its incomplete-task warning. Accept every CommonMark list marker in TASK_LINE_PATTERN, keeping its existing tolerances (indentation, CRLF, a missing space after the marker). Every consumer goes through parseTaskLines, so this one change fixes them all, and task-numbering validation now sees these tasks too. * docs(tasks): list every counted checkbox marker in schema.yaml reference The apply.tracks section listed only - and * checkbox forms. Task lines under + and ordered markers now count, so show them and name the marker rule. Also use American spelling in the changeset. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Clay Good <hi@claygood.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
92fb72d1dc |
fix(workflows): create the main spec when a capability is new (#1701)
* fix(workflows): create the main spec when a capability is new The agent-driven archive workflow told agents to "compare each delta spec with its corresponding main spec" and said nothing about the case where that main spec does not exist yet. Comparing against nothing reads as "already synced", so the agent took the archive branch and the new capability's main spec was never written — the change landed in changes/archive/ with openspec/specs/ still empty. `openspec archive` already handles this: buildUpdatedSpec creates the spec from the delta's ADDED requirements, rejects MODIFIED/RENAMED with "only ADDED requirements are allowed for new specs", and warns past REMOVED. The guidance now says the same thing, so the agent path and the CLI path agree: - archive-change: a missing main spec counts as changes needed and is named in the summary as a spec the sync will create — never as already synced. - sync-specs: MODIFIED and RENAMED have no requirement to act on when the main spec is absent, so the sync stops and reports rather than inventing one; REMOVED is skipped with a warning. Guidance text only — no CLI, parser, or archive behavior changes. Closes #1222 Closes #1264 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(sync): never create a main spec with nothing to put in it CodeRabbit caught a gap in the previous commit: step 4b now tells the agent a REMOVED-only delta has nothing to remove, but step 4d still read as "create the main spec if the capability doesn't exist yet" unconditionally. Following both would write a spec whose `## Requirements` section is empty. Verified against the CLI on a REMOVED-only delta targeting a capability with no main spec: Specs to update: parking: create ⚠️ Warning: parking - 1 REMOVED requirement(s) ignored for new spec. Validation errors in rebuilt spec for parking (will not write changes): ✗ Spec must have at least one requirement Aborted. No files were changed. So step 4d is now gated on the delta having ADDED requirements to seed the spec with, and says what the CLI reports when it does not. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs: define "main spec" and close the no-ADDED archive loop Hardening pass over the two fixes in this branch. Guidance: the archive step's verification pass re-runs the same comparison the fix touched, so a delta that can create nothing — no ADDED requirements, no main spec to merge into — would have been reported as "still needs sync" after a sync that correctly created nothing, and an agent could loop on it. That case now short-circuits with the reason, matching `openspec archive`, which refuses it with "Spec must have at least one requirement". Docs: the glossary defined "delta spec" but never "main spec", which is half of #1647's terminology complaint. It now defines the term and says that for a new capability the main spec is created by the archive rather than written up front; concepts.md says the same in the delta-section table and the archive process. The docs site generates from docs/ at build time, so no website files change. Changeset rewritten in the house style (prose, no commit header; the changelog-github action supplies attribution) and renamed descriptively. All three CLI branches this guidance describes were verified end to end: ADDED against a greenfield repo creates the spec and carries its Purpose; MODIFIED reports "target spec does not exist; only ADDED requirements are allowed for new specs"; REMOVED-only aborts with "Spec must have at least one requirement" and writes nothing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs(glossary): a standalone sync creates the main spec too The "Main spec" entry said the spec is created "by the archive", but the "Sync" entry two sections down says /opsx:sync creates it as well, without archiving — and specs-sync-skill's "New capability spec" scenario is the sync's own behavior. Names both paths so the two entries agree. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(workflows): clarify removed deltas for missing specs * fix(workflows): preserve explicitly retired missing specs * fix(archive): preserve explicit skip-sync choice for blocked deltas --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
4c369e022b |
fix(explore): make the capture request the write confirmation (#1832)
* fix(explore): make the capture request the write confirmation
Explore's write-confirmation rule named `openspec new change` as an action
requiring a separate yes/no, while the capture branch told the agent to
transition "seamlessly" into running it. Both readings were defensible, so
the same request either wrote files immediately or stopped and asked.
State the resolution in all three places: an explicit capture request is
the confirmation, for the change and artifacts that request names. The
guardrail keeps its teeth where #1715 reported the problem — an
agent-proposed capture, or work beyond the requested scope, still asks.
Closes #1828
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* test(explore): guard the capture branch against a re-added confirmation gate
Also disambiguate the scope fence in the IMPORTANT block: "the artifacts
that request names" parses as a relative clause, and it is the sentence an
agent weighs first. Match the article used by both restatements.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(explore): put the capture discriminators where the decision is made
Four hardening findings from review of the first pass:
- A yes to an offer the agent made looks identical to a user-initiated
capture request at the point the branch decides. The discriminator sat
190 lines away in Guardrails. Move it into the branch, and require the
offer to name what it would create.
- "Do not ask for a second confirmation" contradicted step 2 nine lines
below it, which requires asking before expanding the capture. Narrow it
to re-asking for what was already asked for.
- Scope the carve-out to change artifacts, so it cannot be read to reach
the workflow configuration #1715 reported an agent editing.
- The Guardrails bullet restated the whole contract a third time, in a
quick-reference list whose next-longest entry is 43 words. Replace with
a pointer to the branch that owns it.
Tests: the three not.toContain guards could not see a gate phrased in
words they did not anticipate. Replace with a structural check that
collects every consent-bearing sentence and requires each to be sanctioned
— inside the capture branch with no topic filter, since a gate written
there is about the capture whether or not it says so. Mutation testing:
kills 6 of 8 contradiction mutations that survived before, and all three
sites stay independently pinned. The two survivors reverse the resolution
without any consent word and are noted as review-only.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(explore): keep the contact clause in the scope fence
The previous commit reintroduced "the change artifacts that request
names", the garden-path parse that
|