mirror of
https://github.com/Fission-AI/OpenSpec.git
synced 2026-10-02 05:24:34 +08:00
main
100
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
760584ba9a |
fix(validate): fail --strict on requirements over the length limit (#2020)
* fix(validate): fail --strict on requirements over the length limit A requirement description over 500 characters was an INFO finding, so `openspec validate --all --strict` still exited 0 and CI could not hold the limit. It is now a WARNING: normal validation and archive still pass, while strict mode fails, the same split the SHALL/MUST keyword warning already uses. The specs instruction and its docs page say so. Closes #1976 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(validate): check ADDED requirement length and document splitting With the length finding now failing --strict, a change could still add an overlong requirement and pass `validate <change> --strict`; CI only went red after archive merged it into the main spec. ADDED requirements now get the same warning, using the shared body reader so the limit matches the main spec exactly. MODIFIED is left alone, since its text is the existing requirement the instruction says to keep whole. The specs instruction (and its docs-lab page) now says how to split an existing long requirement in a dedicated change: keep the MODIFIED header and every scenario, cut the description to one behavior, and add each removed behavior as its own ADDED requirement. Verified end to end: the split change validates strict, archives, and the main spec then passes --strict. Closes the rest of #1976. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
3a34ea309d |
fix(website): pin brace-expansion and fast-uri past new advisories (#2019)
Security's docs-site audit went red on main after four advisories landed in the site's dev-only serve dependency chain. Raise the site's existing overrides so they resolve patched versions. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
81c2f9fce3 |
chore(changeset): track apply, archive and view fixes for 1.14.0 (#2018)
#1994, #1759 and #1987 merged without changesets, so the 1.14.0 release notes would omit them. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
cf2859a520 |
fix(status): include the declaring repo in store-backed edit roots (#2014)
* fix(status): include the declaring repo in store-backed edit roots For a store-selected root, actionContext.allowedEditRoots listed only the store and claimed implementation edits were scoped to it, so the apply skill reported a conflict and refused to implement tasks. The project whose store: pointer names the store is now listed first; with no declaring project, the constraint asks the agent to confirm the target repo instead. Repo-local output is byte-identical. Closes #2013 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(status): keep actionContext key order and scope the store wording Adversarial review of the #2013 fix found: - The editScope spread moved `constraints` ahead of `requiresAffectedAreaSelection`, changing repo-local JSON key order. toEqual could not see it; the byte-stable test now pins the serialized form, and the keys are written out in contract order. - "Implementation edits belong to <repo>" claimed task routing OpenSpec does not do (a store change can span repos). It now names the current declaring project and asks before editing any other repository. - The no-declaring-project text was false when a pointer exists but is ignored, and did not say the user's answer is where edits go. New tests cover a subdirectory cwd, an ignored pointer on a real planning root, and status --all. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(status): pin the declaring repo's canonical path through an alias test/AGENTS.md asks for an alias-path regression when touching path identity logic (raised by CodeRabbit). The new case reaches the declaring repo through a symlink (a junction on Windows) and asserts the canonical path, both through the CLI and through a direct findDeclaringProjectRoot call whose start path keeps the alias spelling on every platform. The helper's own canonicalizeExistingPath call was redundant: the root walk already returns canonical paths, and removing it changes no output. The alias test fails only when that walk stops resolving aliases. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
781c7f9447 |
feat(warp): add project skills support (#1738)
* feat(warp): add project skills support * docs(warp): align integration contract and references * docs(warp): remove changes to frozen legacy docs --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
772819417a |
fix(archive): stop sending the archive skill to an uninstalled sync skill (#1977)
* fix(archive): stop sending the archive skill to an uninstalled sync skill The archive skill always told the agent to run the `openspec-sync-specs` skill, even when that skill was not installed, so an agent following it stalled at the sync step. The archive command already chose between the sync workflow and an inline merge based on what was installed; the skill now does the same. Closes #1975 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs(skills): document archive sync fallback --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
e923d05c8f |
feat(github): add issue forms and a PR template (#1847)
* feat(github): add issue forms and a PR template CONTRIBUTING asks every change to start with an issue or a discussion, but the repository had no `.github/ISSUE_TEMPLATE/` and no PR template, so a contributor who clones the repo and opens a PR meets a blank box and, later, a review comment asking them to go file the issue they did not know they needed. - `ISSUE_TEMPLATE/bug_report.yml` asks for expected, actual, a minimal repro, `openspec --version`, and the agent and model, and points users who already have OpenSpec installed at `openspec feedback`. - `ISSUE_TEMPLATE/feature_request.yml` asks for the problem, who it affects, and what was tried, and routes core-design topics to Discussions. - `ISSUE_TEMPLATE/config.yml` keeps blank issues enabled, so the pre-filled URL that `openspec feedback` prints when `gh` is unavailable still works, and links Discussions and Discord. - `PULL_REQUEST_TEMPLATE.md` puts `Closes #` on the first line with the "no issue yet?" path directly under it, catching the PR-first contributor at the moment they need it — no bot and no CI gate. Labels referenced by the forms (`bug`, `enhancement`, `needs-triage`) all exist in this repository. Part of #1834 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(github): clarify issue submission and verification guidance * fix(github): point agent disclosure to notes section --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
de4141f0fd | docs(schemas): document Superpowers community bridge (#2002) | ||
|
|
56528ea454 |
feat(cli): report version and update metadata (#2001)
* docs(openspec): propose version reporting command * docs(openspec): clarify update guidance availability * feat(cli): report version and update metadata * fix(cli): harden version install detection |
||
|
|
bda85565ef |
fix(init): guide project.md migration (#1999)
* feat(init): offer project.md migration * fix(config): preserve context newline state * test(init): prove migration preserves files * docs(setup): document project.md migration * fix(init): guide project.md migration * fix(init): cover migration destinations |
||
|
|
9a40b58928 | docs(archive): document retention options (#1998) | ||
|
|
baad4494b4 |
fix(config): clarify project context guidance (#1995)
* fix(config): clarify project context guidance Generated config comments and canonical docs now steer agents toward constraints that shape OpenSpec artifacts and away from discoverable codebase facts.\n\nPart of #1966 * test(config): cover generated context examples * docs(config): harden context examples * docs(config): correct context scope |
||
|
|
e70dcc7c82 |
fix(propose): guide capability naming (#1997)
* fix(propose): guide capability naming * test(propose): cover published naming guidance |
||
|
|
187298289d |
fix(schemas): tell agents the requirement length limit (#1978)
* fix(schemas): tell agents the requirement length limit The validator flags requirement text over 500 characters, but the specs instruction never mentioned the limit, so agents kept writing requirements that tripped it. State the limit and how to split, and make the validator message say how to fix it. Part of #1976 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(schemas): match requirement length boundary * test(validation): lock requirement length boundary * fix(schemas): keep MODIFIED requirements whole under the length hint The 500-character check is an INFO hint, not an error. Splitting or trimming an existing requirement under MODIFIED drops scenarios the main spec still has, which validate and archive reject. Say so, and scope the splitting advice to new requirements. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
e7a9512d7c |
fix(apply): include task source locations (#1994)
* fix(apply): include task source locations * fix(apply): verify task locations before updates * docs(apply): document task source locations |
||
|
|
d4e1c77eba |
fix(cleanup): clarify legacy file deletion warning (#2004)
Adapt #1820 to current cleanup behavior and docs-lab; cover directory, file, and marker-only summaries. Co-authored-by: 胥寅 <xuuyin@dingtalk.com> |
||
|
|
79b6aa9c98 |
test: stop two Windows subprocess tests timing out at 10s (#1981)
* test(flake): give the bash-spawning scope test a 60s timeout The Windows runner took 13.1s to spawn bash three times on the Version Packages push to main, tripping the 10s default. The same test ran in 0.3s and 4.2s on the two previous main runs; nothing in the code changed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(e2e): give the git-clone init test a 60s timeout Timed out at the 10s default on windows-pwsh three times (#1953 merge queue, two changeset-release runs); it normally takes ~2.6s there. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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 |
||
|
|
0b5ce44b55 |
fix(templates): align approval thresholds (#1940)
* fix(templates): align approval thresholds * fix(onboard): preserve implementation choice * test(onboard): reject premature implementation prompt |
||
|
|
fe429a13dc |
fix(kilocode): generate commands in canonical directory (#1938)
* fix(kilocode): generate commands in canonical directory * fix(kilocode): preserve unrelated legacy workflows |
||
|
|
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 |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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
|
||
|
|
e571b5b9ae |
fix(security): harden CLI against hostile-repository inputs and clear dependency advisories (#1835)
* fix(deps): upgrade vitest to 4.1.11 and override fflate
Clears GHSA-82fw-gwwq-j7x9 (path traversal / arbitrary file read via
@vitest/mocker redirect mock), the subject of all three open Dependabot
alerts. No patched 3.x exists — 4.1.11 is the first fixed release — so the
major bump is unavoidable.
Two things the plain Dependabot bump (#1823) got wrong, which is why its
tests failed on every platform:
- It left @vitest/ui on 3.x, which dragged vite/esbuild to 0.28.2 and broke
the allowBuilds pin assertion in pnpm-workspace-config.test.ts. Upgrading
@vitest/ui in lockstep keeps esbuild on 0.28.1.
- Vitest 4 no longer lets an arrow function stand in as a constructor
implementation, so the ZshInstaller module mock threw "is not a
constructor" across 8 completion tests. Converted the three mock factories
to function expressions.
Also adds a pnpm override for fflate (GHSA-px8p-9vwx-vf98, infinite loop on
malformed ZIP64), which @vitest/ui 4.1.11 still pulls at 0.8.2.
`pnpm audit` is now clean: 0 vulnerabilities across all severities.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(core): bound schema size and stop prototype keys shadowing worksets
Two low-severity robustness defects found during the security review.
Workset lookups tested membership with `state.worksets[name] !== undefined`
on a plain-prototype object. `constructor`, `toString`, `valueof` and
friends are all valid kebab ids, so `openspec workset add constructor`
reported "already exists" against empty state, `getWorkset` returned a
function off the prototype, and `withoutWorkset` took the found branch for a
workset that was never there. All three sites now use `hasOwnProperty`.
This was never prototype *pollution* — nothing is written through these
keys and Zod's `z.record` drops `__proto__` — only a correctness defect.
`SchemaYamlSchema.artifacts` was unbounded while `validateNoCycles` walks it
with a recursive DFS, so a project-local schema declaring a long `requires`
chain crashed the CLI with an uncaught `RangeError: Maximum call stack size
exceeded` instead of a validation error. Capped at 1000 artifacts, which
also bounds the reference-resolution and graph work.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(security): stop repo-supplied text forging agent instructions
OpenSpec prints a pseudo-XML envelope that an AI coding agent consumes as
instructions, and interpolated repo content went in raw. The tags carry
authority - <project_context> means "background only", <task> means "do
this" - so a value that closes its own block is promoted from data to
directive.
Confirmed against a fresh build: a config.yaml `context:` value containing
`</project_context><system_override priority="critical">` landed a top-level
override block outside every "do NOT treat as instructions" guard. The same
breakout worked from `rules`, `description`, a dependency description, and a
schema `instruction`. A change directory name containing a quote forged
attributes on the <artifact> tag. In markdown output, a `context` line
starting with `##` forged a peer of the printer's own headings.
src/core/references.ts already had sanitizeInline written for exactly this
threat, documented as such, and simply was not applied here - it also only
flattened newlines, which one line of markup is enough to defeat. Extended it
and added three siblings beside it: escapeEnvelopeText, escapeEnvelopeAttribute,
and escapeEnvelopeCloseTags for content that must stay verbatim.
Template bodies deliberately get only their closing tags neutralized: the
shipped templates are full of `<!-- ... -->` comments and <placeholder>
markers that are copied into the generated artifact, so blanket escaping
would write <!-- into every file. A block can only end at a closing tag,
so that is the load-bearing control.
Rules and operation guidance are flattened but explicitly not truncated -
they are instructions an agent must follow in full.
Separately, `openspec update` decided skill freshness from the generatedBy:
line alone and never compared bodies, so appending a step to a generated
SKILL.md still printed "All 1 tool(s) up to date". Skills now get the same
byte-comparison command files already had, and the plan names the reason.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(security): remove super-linear scans and tighten write containment
Three regexes ran over whole repository files with a `\s` class that crosses
newlines under the `m` flag, so `^\s*` re-scanned from every line start. The
blowup is in the *failing* scan - a file with no `generatedBy:` line at all -
which is also the realistic attack file. Measured on this machine:
extractGeneratedByVersion, 63 KB whitespace SKILL.md 3,103 ms -> <5 ms
legacy-skill compare, 63 KB whitespace frontmatter 8,131 ms -> <5 ms
buildUpdatedSpec, 195 KB of `<!--` openers 6,817 ms -> ~90 ms
The first two are reached by `openspec update`, the first command run after
cloning. The third is reached from extractPurposeSection during `openspec
archive`, on the write path.
The scans now use `[ \t]` and walk lines, and maskHtmlComments is an indexOf
scan that visits each character once instead of a lazy regex that re-scans to
EOF from every `<!--`. Both rewrites were fuzzed against the originals -
200,000 random inputs each, 0 mismatches - so the `--!>` terminator and the
"unterminated comment runs to EOF" rule from #1413 are preserved exactly.
resolveTrustedSpecPath treated a failed containment check as permission to
re-root trust on the symlink's own target, on the theory that monorepo
symlinks may be intentional. A repo shipping openspec/specs/<cap> as a link
out of the tree therefore got `openspec archive` to write attacker-controlled
markdown to <external>/spec.md while printing the in-project path. The
fallback root must now still be inside the project, matching retireSpec,
which already refused to delete an external target.
Also: `validate <id> --type spec|change` short-circuited the name guard that
`show` applies, so a traversing id reached a bare path.join; and markTipSeen
wrote the global config through a predictable <config>.<pid>.tmp at default
0644 instead of the repo's existing writeFileAtomically (random name, 0600).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(security): harden subprocess and shell-config write paths
Defense in depth. The audit confirmed there is no shell injection anywhere in
src/ - no `shell: true`, no user value concatenated into a command line - so
none of these are live exploits; they are the sharp edges next to that line.
Completion install wrote the completions directory into .bashrc/.zshrc inside
double quotes, so a `$(...)` or backtick in HOME/XDG_DATA_HOME became command
execution on every future shell start. Both installers now single-quote the
path through a shared helper.
`feedback` shelled out for two probes (`which gh`, `gh auth status`) directly
alongside free-form user title and body text - the most plausible site for a
future injection regression. Both are execFileSync now, behavior unchanged.
Git subprocesses inherited the default 1 MB maxBuffer with no timeout, so a
large dirty tree made `git status --porcelain` throw ENOBUFS, which gitProbe's
bare catch turned into "no git facts" - `openspec doctor` then silently stopped
reporting uncommitted changes. They now share GIT_EXEC_OPTIONS (15s timeout,
16 MB buffer) the way readCliVersion already did, and the catch distinguishes
a resource failure from "git absent" so the degraded path is no longer silent.
The GITHUB_OUTPUT heredoc in validate-changesets used a fixed EOF delimiter
over a list of PR-authored paths; it is now run-unique.
Finally, both package.json files still carried a `pnpm` block. pnpm 10 uses
that block *instead of* pnpm-workspace.yaml rather than merging with it, which
is exactly the override-displacement trap dependabot.yml documents as #1812 -
and it is where Dependabot writes when it bumps an overridden package. The
block only duplicated `allowBuilds`, so removing it leaves both lockfiles
byte-identical with every advisory override intact, and denies Dependabot the
block to write into. The workspace test now asserts `pnpm` is absent entirely.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(security): pin the update check to TLS and honor telemetry opt-out
The update check asked whatever `npm_config_registry` named, over any
protocol. A code comment asserted that file contents deliberately cannot
choose the destination; that was not true. npm exports every config source it
reads, including a `registry=` line in a repository-local .npmrc that travels
with a clone - reproduced: `registry=http://169.254.169.254/` came straight
through `npm run`.
That is a cleartext GET at an address of the repository's choosing, and it
escalates. The attacker's reply says `{"version":"99.0.0"}`, which triggers
the upgrade prompt whose default is Yes; accepting runs `npm install -g`,
which npm resolves against that same attacker registry. Cloning a repository
and answering one prompt installs an attacker-chosen global package.
Both halves are now closed. The registry override is honored only over https,
falling back to the public registry otherwise, and canSelfUpgrade() refuses
when the resolved registry is not the public origin - a private mirror can
still inform the check but can never drive an install prompt. Redirects must
stay https and on the origin resolved up front, not merely the previous hop,
so no single reply can steer the request elsewhere. The comment now describes
what the code actually guarantees.
Separately, the opt-out env vars were exact-string matches, so DO_NOT_TRACK=true
and OPENSPEC_TELEMETRY=false both silently left telemetry ON - the spellings a
user is most likely to reach for, and inconsistent with the tolerant
isCiEnvironment() helper beside them. Parsing is now tolerant, shared between
both call sites, and fails safe: an unparseable value suppresses the request.
The first --json run also sent an event before the disclosure was ever shown -
the notice is correctly deferred so it cannot corrupt machine-readable output,
but trackCommand fired regardless, and agent-driven --json may be a user's only
mode. No event is sent and no anonymous id is created until the notice has
actually been printed.
No existing guard was weakened: the 256 KB body cap, 3-redirect cap, single
budget timer, strict version regex, argv-based spawn, CI/test guards and the
four-field payload are untouched.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(security): make the close-tag escape linear
CodeQL js/polynomial-redos on the escape added in
|
||
|
|
3b8e5b6616 |
fix(nix): install shell completions with the flake package (#1785)
* fix(nix): install shell completions with the flake package The flake exposed `openspec completion generate SHELL` but installed no completion scripts, so a Nix install had no completions at the standard locations. Generate the Bash, Fish, and Zsh scripts during postInstall and place them with installShellCompletion. Closes #1740 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs: note that cross-compiled Nix builds omit completions Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * ci: run the Nix job when the completion generator changes The Nix build now runs `openspec completion generate`, so a change under src/core/completions can break packaging without touching flake.nix. Also drops the cross-compilation caveat from the Nix install docs: every package this flake exposes is native (`legacyPackages.<system>` has buildPlatform == hostPlatform), so `canExecute` is always true and the completions are never omitted. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs: move the Nix completions note to the canonical pages docs-lab/README.md makes docs-lab/ canonical and the old docs/ tree legacy, so the fact is recorded in the two docs-lab pages that own it and docs/cli.md and docs/installation.md are back to their state on main. - docs-lab/start/installation.md, Nix: the package ships the scripts at the standard locations, so completion install is not needed. - docs-lab/reference/cli.md, openspec completion: the same exception, stated where a reader looking up the command will hit it, linking to the Nix section rather than repeating the paths. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
7de24044ef |
fix(init): make the universal tool target findable in the picker (#1778)
* fix(init): make the universal tool target findable in the picker Closes #653 `openspec init`'s tool picker is a searchable list of product names. The vendor-neutral target every unlisted assistant is meant to use was named "Shared .agents skills" — after the directory it writes, which is not a word anyone in that position searches for. Typing "universal", "other" or "generic" returned "No matches", so the escape hatch was unreachable and the reporter had to open an issue to find it. Rename the entry to "Other / Universal (shared .agents skills)" and give choices optional `searchAliases` the filter also matches. The picker also dropped every non-alphanumeric keystroke: readline reports punctuation only in `key.sequence`, leaving `key.name` undefined, so ".agents" and "amazon-q" could not be typed at all. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(init): point at the universal target when a tool search matches nothing "No matches" is where someone whose assistant is not on the list gives up — the picker knows the answer and does not say it. Add an optional `emptyHint` to the searchable multi-select, and have init name the vendor-neutral entry there. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(init): close every dead end that hides the universal tool target Hardening pass over the same defect. Reviewing the first fix turned up four more places the answer was withheld: - `openspec update`'s tool picker builds its own choices and never passed searchAliases through, so the same search failed there. - `--tools <unknown>` printed a bare list of ids. It now names the fallback, the scripted counterpart of the picker's empty hint. The hint I first put on validateTools sat on an unreachable branch; the path users actually hit is the "Invalid tool(s)" parse error, and a test now pins it. - The search box dropped pasted text as well as punctuation. Any sequence whose characters are all printable is now accepted, which also lets a space reach the box so "claude code" filters. Escape sequences carry control characters and are still rejected, and the `name` fallback stays single-character so readline names like 'tab' are never typed. - docs-lab still taught the old label in two places. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs: keep the FAQ a router and finish the alias list - help/faq.md is a one-liner surface (README's "FAQ is one-liners" rule), so the answer points at the support matrix's Other / Universal section instead of restating the picker's search terms. Drops the em dash that writing.md forbids. - reference/supported-tools.md keeps the search terms, now all nine the picker actually matches: `vendor-neutral` and `agents.md` were added to searchAliases after the first draft of this page. Same correction in the legacy docs/supported-tools.md paragraph. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * chore(init): keep the tool-not-listed hint ASCII The non-interactive fallback hint is new terminal output and carried an em dash, an ambiguous-width glyph in the class #983 covered. A colon reads the same and cannot misalign a terminal. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs: drop the legacy docs/ tool-matrix edit docs-lab/reference/supported-tools.md already carries the label and search terms. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * test(init): prove picker punctuation through real readline key events The prompt tests fed a multi-character sequence that readline never emits: it splits a paste into one key per character, so a pasted space still toggles. Drive the handler with real emitKeypressEvents output (fails on main with 'amazonq'), pin the space limit, and stop claiming multi-word paste in the changeset and code comment. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
8b99c07bd0 |
fix(status): name the command that resumes a change (#1786)
* fix(status): name the command that resumes a change `openspec status` printed the artifact checklist and stopped. The command that moves the change forward was computed already and shipped in the JSON `nextSteps` sentence, but the text surface never rendered it, so anyone resuming a change had to know the next command by heart. Extract `resolveNextStep` so the command and the published sentence come from one place, and print it as a `Next:` line — matching the idiom `openspec new change` already uses to hand off to `openspec status`. The completion case matters most: "All planning artifacts complete!" reads as "you are done" even while implementation tasks remain, and it is now followed by the `openspec instructions apply` command that resumes the work. Closes #906 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * style(status): keep the loadStatus comment on loadStatus The store-flag note landed between the "single definition" comment and the declaration it describes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * test(status): cover the next-step line, and record it in the spec The behavior change shipped without the OpenSpec change this repo requires of user-facing work, and without coverage for the paths a reviewer would reasonably ask about. Adds `add-status-next-step`, whose delta modifies `Next Artifact Discovery` in `cli-artifact-workflow` - the requirement that already says status is how you learn what comes next. It now also says status names the command. Coverage added: - Unit tests for `resolveNextStep`, pinning the published `nextSteps` sentences verbatim. Confirmed byte-identical to main's output, so the split into command + sentence provably did not reword the contract. - Custom-schema case: the line is built from the resolved artifact id, so a project with neither proposal/specs/design/tasks still gets a usable command. - Skipped artifacts are never named - they satisfy dependents but must not be created. - `--json` stays parseable and carries no `Next:` line. - `--all` gives every change its own line, and a failed entry none. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * test(status): assert the next-step line closes the output The spec delta says the text output ends with the `Next:` line, but the assertions used toContain, which a later line would still satisfy. Compare the last non-empty line instead, across all four ready/complete cases and the skipped one. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * test(status): read the closing line CRLF-safely Split on /\r?\n/ so a CRLF stream cannot leave a carriage return attached to the line under comparison, and route the parity check through the same helper instead of its own scan. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs: move the status Next: line to the canonical CLI page docs-lab/README.md makes docs-lab/ canonical and the old docs/ tree legacy, so the entry now lives under 'openspec status' in docs-lab/reference/cli.md and docs/cli.md is back to its state on main. Both documented outputs were captured from real runs rather than written by hand: the blocked case in a change with proposal and specs, and the complete case with all four artifacts. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs(status): drop em dashes from the changeset and proposal Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
09a999bbb2 |
fix(changes): report a change nested in a namespace folder (#1849)
* fix(changes): report a change nested in a namespace folder Specs can be nested by domain (`specs/mobile/tutorial-videos/spec.md`), so laying changes out the same way looks reasonable - but a change is only ever a directory directly under `changes/`. `changes/mobile/refresh-token/` left the real change invisible to every command while `mobile`, the folder around it, was reported as an ordinary task-less change. Nothing said so, and `openspec archive mobile` moved the unfinished change into the archive under the namespace's name with none of its deltas applied. A directory under `changes/` is now recognised as a namespace folder when it carries no change-root artifact of its own and wraps a directory that does. The probe is deliberately conservative: a miss behaves exactly as before, and an ordinary change - including a scaffolded one with no artifacts yet, and one whose only content is a root delta spec - is never reported as a folder. - `openspec list` marks it `not a change` and explains the flat-only layout instead of printing `No tasks`; `--json` gains an additive `warnings` entry. - `openspec show` and `openspec status --change` say the same rather than reporting a change that has no proposal yet. - `openspec validate` reports the nesting instead of "must have at least one delta", with next steps that name the fix rather than delta authoring. - `openspec archive` refuses it outright - burying an active change is data loss, not something to warn about. Closes #1846. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs(agent-contract): document the list --json nesting warning Agents read 4.1 as the shape contract, so the additive `warnings` array and per-entry `nested` field have to appear there, along with the instruction not to treat a flagged entry as a change. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(changes): harden the nested-change probe and cover status --all Adversarial review of the first commit found three holes. A custom schema may generate every root artifact into a subdirectory (`generates: rfc/proposal.md`). Created by hand, such a change carries no `.openspec.yaml`, so the marker probe read it as a namespace folder wrapping a change called `rfc` - a legitimate change that validated clean before, now refused by validate, status, instructions and archive, with no flag to get past it. The same probe missed the case it was written for whenever the nested change started from its delta specs rather than a proposal: `mobile/refresh-token/` holding only `specs/auth/spec.md` still archived as `mobile`, deltas dropped. A nested change is always hand-made - `openspec new change` rejects a name with a separator - so "mkdir the tree, write the deltas first" is a common way to arrive here. Both come from asking one question. A directory is now recognised as a change by a root artifact OR a populated `specs/`, and a directory holding any file of its own is never a namespace folder. The `specs/**/*.md` shape is fixed by the delta format rather than by the schema, which is what makes it safe to lean on. `change show archive` also reached the probe - it has no reserved-name guard - and offered to rename every dated archive entry into an active change. The reserved name is rejected in the probe itself, so no caller can repeat it. `status --all` read each directory straight through `loadStatus`, bypassing the lookup guard, and printed a whole artifact plan for work that is not there. It now carries the same per-change diagnostic a malformed change does, and the sweep continues past it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * chore(changeset): state the nesting-detection bound CodeRabbit asked for the three-level limit in the release note. Stated as a closing sentence rather than a qualifier on the headline, so the note still leads with what changed for users. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(changes): trust the resolved schema before calling a folder a namespace A hand-made change under a custom schema that generates into a subdirectory (rfc/proposal.md), with no .openspec.yaml and no delta specs yet, was read as a namespace folder and refused by status, show, validate, instructions and archive. The probe now also counts a file at any path the resolved schema generates, the same check status uses to mark an artifact done. Move the flat-change-folder docs from legacy docs/ to docs-lab reference/cli.md. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
09984b8242 |
fix(validate): warn when tracked tasks have no checkboxes (#1774)
* fix(validate): warn when tracked tasks have no checkboxes Progress counts checkboxes and nothing else, so a tasks.md written as plain bullets or a numbered list is worse than an empty one: `openspec list` and `openspec status` report "No tasks", and `openspec archive` has no incomplete task to warn about. The file reads as finished to the tool and unfinished to a human. `openspec validate` now warns when every task file the change's schema tracks contains list items but not one checkbox, pointing at the first offending line. Reported per change, not per file, so a checklist alongside a prose file stays silent, and only files an artifact actually declares are linted - a bare tasks.md no schema tracks is left alone. Closes #354 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(validate): honor fence delimiter length and width CommonMark closes a fence only on the same character, a run at least as long as the opener's, and no info string. Comparing the first character alone let an inner ``` end an outer ```` block, exposing the bullets of a nested code sample as a task list. The delimiter pattern also loses its end anchor: `.` does not match `\r`, so an anchored info-string group matched nothing in a CRLF file and blinded the scan to fences. Adds the nested-fence, annotated-closer, tilde/backtick, longer-closer and CRLF cases, plus an e2e change whose nested task files are all bullets, asserting both reported paths stay POSIX-separated. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(validate): scan rendered content and complete evidence only Hardening pass over the checkbox warning. The scan for the offending line now skips YAML front matter and HTML comment blocks alongside fenced code. A list under `tags:` is metadata about the file rather than the work it tracks, and a commented-out list is not work either; each exclusion can only silence a warning, never drop a real task, which is the opposite trade from the task parser. An unterminated `---` opener rewinds to the top, because that is a thematic break and everything below it is still content. Only a comment opening its own line hides that line, so the template's `## 1. <!-- Task Group Name -->` heading cannot swallow the checklist beneath it. A tracked file that exists but cannot be read now withdraws the warning entirely: "no file here holds a checkbox" is a claim about the whole tracked set, and the checkboxes may be in exactly the file that would not open. `validate --archived` stays the surface that reports an unreadable task file loudly (#205). The message leads with the consequence rather than an accusation, since a file may legitimately carry a bulleted note and no tasks yet. New coverage: every packaged tasks template is asserted checkbox-shaped (the guard fails if a template loses its boxes), a schema tracking tasks by artifact id with no `apply` block, the deprecated `change validate` text output, and an unreadable tracked file. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(validate): match CommonMark on fence indent and front matter Two block-scanning defects, both of which hid list items. A fence indented four spaces is an indented code block, not an opener. Accepting it left the scan inside a block that never began, so every list below it went unseen. Fence recognition now stops at three spaces. `----` is a thematic break, not a YAML front-matter delimiter. Matching three-or-more dashes let one open a block that swallowed the list under it until the next `---`. Front matter is now exactly three dashes. Two test defects alongside them. The deprecated-command test claimed to assert the reported line, but the text renderer prints no line for any issue; it now asserts the level and path prefix that surface actually emits, with the line left to the JSON assertion that already covers it. The unreadable-file fixture would have passed for the wrong reason had the mode not taken, since the checkbox it hides would have silenced the warning by itself; the read failure is now asserted first. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(validate): name task files relative to a canonical change dir Windows CI caught the report naming a task file `../../../../../../../../runneradmin/AppData/.../tasks.md` instead of `tasks.md`. `resolveArtifactOutputs` hands back real paths while `changeDir` carries whatever spelling the caller resolved, and a short 8.3 alias against its expanded form is a difference in spelling, not in location, so the relative path escaped the change. A symlinked project directory reproduces it off Windows. Canonicalizing both sides recovers the relationship. A path that still escapes falls back to the file name, so no report can leak an absolute filesystem path. Numbering issues are named through the same helper and gain the same fix. The deprecated-command test now asserts the `[WARNING] tasks.md:` prefix that exposed this, and the Windows job is its regression guard: the mismatch cannot be staged on POSIX, where the spawned CLI's cwd is already physical. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(validate): keep indented code out of the uncheckboxed-task scan alfred-openspec on #1774: the scan said it reads rendered content, but LIST_ITEM began with \s* and so reported top-level four-space-indented code such as ' - example output' as an uncheckboxed task list. Under --strict that false positive failed validation on a correct file. A list-shaped line is now taken only below four visual columns of indent, the same cut the fence logic already applies, with tabs counting as four. Genuine nested lists are untouched: this scan reports the first list item it finds and a nested item always sits under a shallower parent, so the parent is reported exactly as before. A list-shaped line four columns deep with nothing shallower above it is not nested under anything, which is what makes it code. Regressions cover space-indented, tab-indented and numbered code samples, and pin both the nested-list case (parent still reported) and three-space indent (not code). Verified the guard bites: removing the column test fails them. Also moves the documentation to its canonical home. docs-lab/README.md says the old docs/ tree is legacy and must stay untouched, so the docs/concepts.md line is dropped and the warning is documented under 'openspec validate' in docs-lab/reference/cli.md, beside the archive merge findings. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(validate): skip BOM-prefixed front matter and cap ordered markers at nine digits A prose-only task file that opened with a UTF-8 BOM before its front matter was warned about, because the opener never matched and the tags list was scanned. A number longer than nine digits followed by a period also matched as a list item, which CommonMark does not allow. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs(changeset): drop the em dash Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * test(tasks): use a multi-character marker for the unrecognised-checkbox case A single-character marker such as `[~]` becomes a task once #1773 lands, which would flip this expectation. `[ab]` is not a task under either parser, so the test keeps asserting that checkbox-looking list items that count as no task still warn, whichever PR merges first. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
b928165276 |
docs(explore): stop claiming explore never writes files (#1838)
* docs(explore): stop claiming explore never writes files Sixteen lines across both documentation trees told users that `/opsx:explore` creates no artifacts and writes no files, full stop. That has been false since explore shipped (#467): its capture branch writes the planning artifacts the user asked for, and can edit an existing change's artifacts. #1503 later made it scaffold with `openspec new change` first, closing #668 and #720. The claim appeared in two shapes. Six lines denied the capability outright ("Explore creates no artifacts and writes no code"). Ten more said the same thing as a timing claim ("before any artifact exists"), which reads as ordinary pitch copy and is what escaped the first pass. Every site now carries one guarantee, worded the same way: explore never writes code, and writes nothing else unless you ask, or say yes when it offers. Four sites described only the user-initiated trigger, which left the offer path - the one a reader actually hits - looking like it did not exist. docs/explore.md and docs/commands.md also gain a positive description of capture where the denial used to sit, including what scaffolding creates beyond the artifacts you named, and how capture differs from handing off to propose (propose writes the set your schema requires; capture writes only what you named). Closes #1833 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * test(docs): keep the retired explore wording retired A flat list of the phrasings that actually carried the claim, swept over the eleven pages that pitch explore. Fails on main with all sixteen offenders; clean on this branch. Modeled on test/vocabulary-sweep.test.ts, and deliberately a list rather than a grammar. An earlier draft built the grammar - section splitting, code-fence tracking, a conditional-marker exemption so "creates no artifacts unless you ask" would pass - and measured against realistic prose it was imprecise in both directions while returning the same verdict on the real input. The list has no exemption logic to get wrong, and any maintainer can extend it. Phrasings that are only wrong in the absolute ("writes nothing", "creates nothing") are left to review, since the conditional form of each is the wording the failure message recommends. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * test(docs): pin the explore capture contract, not the word The guide check matched any "capture", so "explore automatically captures every artifact" passed. Both explore.md and commands.md now must name the user trigger, `openspec new change`, and the named-artifacts scope, with no capture line claiming it happens unprompted, and keep "never writes code". Also scope the explore.md guarantee to the setup files a new change needs, and make the commands.md offer name the change and its scope, which the template asks for on main and after #1832. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * test(docs): accept negated unprompted-capture wording, catch non-capture verbs The unprompted check matched "automatically" on any capture line, so the correct "Explore does not automatically capture artifacts" failed, while "Explore automatically writes planning artifacts" was never scanned because it lacks the word capture. Check each clause of lines naming explore or capture for an unprompted write verb with no preceding negation. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
e4e112d94f |
chore(deps): declare pnpm overrides only in pnpm-workspace.yaml (#1816)
The security overrides were declared twice: in pnpm-workspace.yaml, with the advisory comments explaining each pin, and again under package.json's pnpm.overrides. The copies are not additive — pnpm 10 uses package.json's block instead of the workspace list when both are present — and Dependabot rewrites plain-name entries in package.json whenever it bumps the same package. So a routine bump silently displaces the pins that patch advisories, and fails the equality test that guards them (#1812). Keeps one declaration, in the file that carries the reasoning, and asserts the mirror stays gone. Co-authored-by: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
3915db763a |
fix(guidance): teach the spec-inventory verb to generated guidance (#1700)
* fix(guidance): teach the spec-inventory verb to generated guidance `openspec list --specs` appeared in no generated skill, command, or artifact instruction, while `openspec list --json` — the in-flight CHANGE list — appeared throughout. An agent asked to read the existing specs first reached for the one enumeration verb it had been taught, got the change list, found it plausible, and reported the step complete against the wrong object. Explore now lists the spec inventory alongside the change list and says which is which. The spec-driven `proposal` and `specs` instructions name the command at the two points that need it: researching existing capabilities before filling in the Capabilities section, and confirming a delta's path matches an existing capability. Guidance text only — no CLI, parser, or archive behavior changes. Closes #1689 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(guidance): carry the store qualifier wherever the command is named A bare `openspec list --specs` reads the local inventory, so under a selected store it confirms a capability path against the wrong root. The proposal instruction carried the qualifier; the modified-capability instruction did not. All four sites now use the same wording, and the guard is scoped to the passage that names the command — every explore body already carries the qualifier in its unrelated capture steps, so a whole-body assertion would pass with it dropped here. Addresses CodeRabbit review on #1700. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(guidance): read a listed capability with the store-aware command The read step I added defeated the fix under a store. It told the agent to list the inventory with `--store "<id>"`, then read the result back from `openspec/specs/<capability-path>/spec.md` — a local path. Verified against a registered store: `list --specs --store mystore` returns `store-only-capability`, and the corresponding local read fails outright (or, when a local capability happens to share the name, silently returns a different one). That is the same wrong-object failure #1689 is about, reintroduced one line later. Capabilities are now read with `openspec show "<spec-id>" --type spec --json --no-scenarios`, which resolves against the same root the listing came from and returns purpose plus requirement texts without pulling whole spec files into context. `--type spec` is load-bearing: a change and a spec sharing a name is an ambiguous_item error, and change names routinely mirror capability names. Also documents `--store` on `list` and `show` in docs/cli.md. Both already accepted the flag — the prose at line 228 says so — but neither options table listed it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix: Use ASCII arrows instead of unicode This fixes the issue of ambiguous unicode character width when visualizing on terminals * fix: Update remaining docs within explore to use ASCII * fix(explore): finish the ASCII conversion and guard it Rebase onto main and close the gaps in the original fix: - Regenerate skills/openspec-explore/SKILL.md. The static skills/ mirror landed after this branch was cut, so the parity test would have failed with the template and the mirror out of sync. - Regenerate the three parity hashes through scripts/regen-parity-hashes.mjs. - Convert the ambiguous-width glyphs the first pass missed: the bullets in the CLI-storage example, and the check/cross marks in its comparison table, which sat in the column-aligned block the bug is about. - Tighten the ASCII guidance to two lines. It ships into every user project on both delivery surfaces, so the paragraph was pure overhead. - Add regression tests (#983): every fenced example in both the skill and the command body must be free of box-drawing, arrow, bullet, and check/cross glyphs, and the guidance must state the rule and the reason. - Add a patch changeset. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * test(explore): cover every check/cross dingbat in the ASCII guard The matcher listed U+2713 and U+2717 only, so a fenced example could use ✕ (U+2715) or ✘ (U+2718) — same ambiguous width, same misalignment — and still pass. Widen to the U+2713-U+2718 run. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(explore): require explicit confirmation before writing files * test(explore): harden write confirmation guardrail * fix(explore): scope write confirmation precisely * test(guidance): pin store-aware spec reads * test(templates): regenerate explore parity hashes The explore template now carries three independent guidance edits: the spec-inventory verb, the ASCII diagram conversion, and the write confirmation contract. Each pinned its own hash constants, so the pinned values no longer describe the combined template. Regenerate them from the merged source with `regen:parity-hashes` rather than hand-editing, and confirm the committed skills mirror still matches byte-for-byte. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(guidance): read complete specs before coverage decisions * docs: drop the redundant legacy docs/cli.md edit docs-lab/README.md makes docs-lab/ canonical and the old docs/ tree legacy. docs-lab/reference/cli.md already documents `--store <id>` for both `openspec list` and `openspec show`, so this branch's docs/cli.md rows added a third copy in the stale tree and nothing else. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * chore(changeset): drop the docs claim this PR no longer makes alfred-openspec on #1700: the release note still said docs/cli.md now documents --store on list and show, but that legacy-tree edit was removed from this head and the diff does not touch docs/cli.md. The canonical docs-lab/reference/cli.md already documented the flag on both commands, which is why the edit went. Removing the sentence rather than repointing it at docs-lab: nothing in docs-lab changed either, so there is no documentation change to announce. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> Co-authored-by: Shooks <justanormalme@gmail.com> Co-authored-by: Ayman D. <ayman.bacc@gmail.com> |
||
|
|
c170dc77ad |
fix(archive): read a wrapped scenario bullet as one bullet (#1782)
* fix(archive): read a wrapped scenario bullet as one bullet A repository that wraps its prose at a column limit writes most scenario bullets over two lines. The retirement guard read the continuation line as content the merge could not account for, so `retire_capabilities` refused every such spec - and because the hint that names the marker is gated on that same count, an unmarked author got the bare "must have at least one requirement" abort and never learned the retirement path exists. A line indented to the content column of the item above it, with no blank line between, is part of that item. It is accounted for when the item was and already reported when it was not, so nothing is deleted unmentioned either way. A blank line still ends the item, so a note written below the scenarios is still the author's own however it is indented. Closes #1780 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(archive): keep an indented heading out of a bullet's continuation Continuation is for wrapped prose. A raw HTML heading indented under a scenario bullet was absorbed by it, so indenting a section one level would have smuggled it past the audit and deleted it with the file. ATX headings were already excluded; HTML ones now are too, matching how the pass above the requirements section reads them. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(archive): flag a setext heading indented under a bullet A setext underline turns the line above it into a heading, so indenting the pair one level under a scenario bullet let a whole section be absorbed as continuation and deleted with the file. Checked ahead of the continuation branch now, the same way the ATX and raw HTML forms already are. Found by CodeRabbit on this PR. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(archive): read an unindented wrapped bullet as one bullet too Not every wrap indents its continuation, and the indent-only rule left the reported bug fixed for one spelling and live for the other: a hand-wrapped scenario bullet still refused the retirement. Inside a scenario's unbroken bullet run a lazy continuation is now read as part of the bullet above it. This widens nothing - a sibling bullet written in that same position is already read as the scenario's own, and a lazy line is part of the bullet where a sibling is merely next to it. Past the blank line that ends the run the indent is still required, so a note bulleted below the scenarios and the line that wraps it stay the author's. Also covers CRLF specs, and asserts the refusal report names only the real leftover in a wrapped multi-requirement spec rather than burying it under continuations. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(archive): stop a lazy continuation at anything that opens a block CommonMark lets a blockquote, thematic break, table, list item or raw HTML interrupt a paragraph, so one written flush against a scenario bullet starts something new rather than continuing it. The lazy allowance absorbed all of them, which would have deleted an author's note with the file and named nothing. The bullet's paragraph is now tracked as its own state: opened by a bullet, closed by a blank line, a fence, a heading, or a line that opens a block - including one indented inside the item, whose own paragraph ends the bullet's. Lazy continuation applies only while it is open. Indented continuation is unaffected: a nested list or quote sitting inside the item is still the item's own content. Each of the six holes is pinned by a test proven to fail with the narrower rule removed. Found by CodeRabbit on this PR. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(archive): classify a line as the list item sees it A marker as wide as `100. ` puts the item's content past the three columns a Markdown construct is allowed at the file's left margin, so `## Retention` written inside such an item read as five spaces of nothing and was absorbed as continuation - a regression against the behavior before continuation existed, which named it. Every syntax test in the audit now reads the line with the item's indent removed, so a heading, a setext underline or a block start is recognized wherever the item sits. Found by CodeRabbit on this PR. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * chore: drop test scratch directory committed by mistake `test-spec-command-tmp/` is a fixture a test run leaves behind, swept up by `git add -A` in the previous commit. It is not part of the change. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(archive): share one list-marker definition with the paragraph rule Folds in the marker coverage from the duplicate PR #1789, which fixes the same issue (#1780) with a shallower model. The audit named `-`, `*` and ordered items as list markers, while INTERRUPTS_PARAGRAPH, added in this same PR, already named `+` and capped an ordered marker at CommonMark's nine digits. The two disagreed, so a line one called a bullet and the other did not was read as both at once. Both now use one LIST_ITEM constant: - `+` is the behavior fix. A spec bulleted with `+` validates like any other, and every one of its scenario bullets was reported as unaccounted content, so that capability could not be retired at all. Regression added, verified to fail against the old marker set. - The nine-digit cap changes no verdict in this design, since a line the pattern rejects is weighed by the same rules either way. It is here for the consistency, and the comment says so rather than claiming a fix. The case is pinned so a later change cannot start deleting such a note. LIST_ITEM also no longer requires content after the marker, so an empty `- ` reads as the bullet it is instead of falling through to the leftovers, which is what the surrounding indent tracking already assumed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
8ba4ac1b16 |
fix(apply): warn when a change is ready to implement with no specs (#1783)
* fix(apply): warn when a change is ready to implement with no specs Apply gates on the schema's `apply.requires` (tasks) alone, so a change whose tasks file was written ahead of its specs read as ready even though it had no delta specs at all — the state `openspec validate` rejects. Apply was the one surface that green-lit a change every other surface flags, which is how agents end up implementing before the specs exist. Report it as a warning, in the text output and in `--json`, naming both ways out: write the specs, or declare `skip_specs: true`. Blocking would be a policy change; naming the gap is not. Changes that have specs, declare `skip_specs`, or are still blocked on their own required artifacts are unaffected. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * refactor(apply): name the metadata file from its shared constant Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * test(apply): cover custom schemas in the no-specs warning A schema with no spec-producing artifact must stay quiet, and one whose spec artifact is not called `specs` must still warn - the rule keys off the output path, not the artifact id. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * test(apply): stop asserting an absolute temp path on Windows os.tmpdir() hands back the short form (C:\Users\RUNNER~1) while the CLI resolves the long one, so the assertion pinned a path that never matched on windows-pwsh. Assert the change-relative tail instead. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(apply): name the whole chain a blocked change still needs Apply blocks on the schema's `apply.requires` alone, so its message stopped at the first hop: a change holding only a proposal was told "Missing artifacts: tasks" while the specs `tasks` depends on were missing too. Taken literally that is an instruction to write the tracking file straight from the proposal and skip everything between — the failure reported in #834 and #869. Walk `requires` and report the whole set, in build order, as `missingPrerequisites` (text and `--json`). What apply blocks on is unchanged, and the wording leaves conditional artifacts to the schema rather than demanding them. The remedies these messages give are now CLI commands rather than the `openspec-continue-change` skill: `continue` is not in CORE_WORKFLOWS, so on the default profile the old advice named a skill that is never installed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(apply): name the schema's own spec artifact in the warning alfred-openspec on #1783: collectApplyWarnings() discovers spec-producing artifacts by output path, so it correctly fires for a schema whose artifact id is `contracts`, but the remediation text then hardcoded `openspec instructions specs`. That names an artifact such a schema does not declare, so the advertised custom-schema support dead-ended at the exact step meant to resolve the warning. The command now derives its target from specArtifacts: the artifact's own id when the schema declares one spec-producing artifact, and `<artifact-id>` as a placeholder when it declares several, since there is no single right answer there and a guess would read as an instruction. The renamed-artifact test now asserts the command names `contracts` and rejects the hardcoded `specs` spelling, and a new test pins the two-artifact placeholder. Verified both fail against the hardcoded string. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * chore(changeset): bump apply warnings to minor This adds `missingPrerequisites` and `warnings` to the documented `instructions apply --json` contract in docs/agent-contract.md. New fields are backward compatible, but they are new capability an agent can consume, which is a minor under semver rather than a patch. Taking the conservative direction deliberately: shipping new API surface as a patch is the violation, since a consumer pinned to a patch range would receive it without opting in. A minor costs nothing if the fields turn out to be uninteresting. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
3c6d318b83 |
fix(init): name the workflows the profile left out (#1779)
* fix(init): name the workflows the profile left out Setup output listed the workflows it installed but never mentioned the ones it did not, so a user on the default core profile who typed /opsx:ff saw nothing and read it as a broken install. The docs explain profiles; nobody reads them before typing a command that should be there. init now closes with the missing workflows by name and the two commands that add them. The note is skipped when nothing was generated at all, where the existing delivery correction is the whole story, and when the profile already installs everything. Also adds a troubleshooting entry for the "only some /opsx: commands show up" symptom, which the existing list did not cover. Relates to #1076 (the optional-workflow discoverability half; the Windows command-discovery repro in that thread is not addressed here) Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * chore: drop a test scratch directory committed by mistake test-show-command-tmp/ is created by a test run and does not exist on main; it was picked up by a `git add -A`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(init): keep the workflow note off runs that generate nothing With no tools selected (or only tools that could not receive a surface), `openspec config profile` followed by `openspec update` writes nothing, so naming the missing workflows pointed at the wrong problem. Reported by CodeRabbit on this PR. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * refactor(init): drop the redundant update step from the workflow note `openspec config profile` offers to apply to the current project before it exits, and prints the `openspec update` guidance itself when the user declines, so naming a second command was one step too many. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs(troubleshooting): match the profile steps to what the CLI does `openspec config profile` applies to the current project itself, so listing `openspec update` as a second required step was wrong; it is the fallback for declining the prompt or for other projects. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(update): name the workflows the profile left out `openspec update` is what the troubleshooting checklist tells a user to run when a command they read about never appeared, and it is what people run after upgrading the CLI. Neither of its existing profile notes fires on the default `core` profile, so that user reached "All tools up to date" and still learned nothing about the six workflows they don't have. The note is the fallback pointer: silent when the extra-workflow or missing-core note already named `openspec config profile`, and when no configured tool can receive a workflow surface under the active delivery. Reading the two existing notes as one short-circuited `||` would have swallowed whichever ran second; they are evaluated separately. Relates to #1076 (the optional-workflow discoverability half; the Windows command-discovery repro in that thread is not addressed here) Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * refactor(update): gather the profile notes behind one call The two call sites had grown identical six-line blocks. One displayProfileNotes() keeps the ordering and the single-pointer rule in one place, where the "evaluate every note, never chain them with ||" constraint can be stated once. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs: drop the legacy troubleshooting entry alfred-openspec on #1779: docs-lab/README.md says the old docs/ tree is legacy, is no longer used by the site, and must stay untouched. The canonical docs-lab/customize/profiles.md already lists the six optional workflows and the 'openspec config profile' command that adds them, and the root README already calls out the expanded set, so this entry was a third copy in a stale tree. The docs-lab troubleshooting page is a heading-only skeleton held back from the site, so there is nothing to move it to; this PR is now source and tests only. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
6d2dbe62d3 |
fix(propose): load project context before planning (#1657)
* fix(propose): load project context before planning * test(propose): assert project context is applied * fix(propose): honor project context limits * fix(propose): fail closed on unsafe context * fix(propose): skip config without a root * chore(parity): regenerate hashes after merging main * fix(propose): harden early context loading guidance * fix(propose): require initialization before planning in bare repos --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
1c0ee701e5 |
docs: add CONTRIBUTING.md (#1781)
* docs: add CONTRIBUTING.md Require a discussion (core design changes) or an issue before a PR is opened, and require every PR to link its issue. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs: add setup and PR steps to CONTRIBUTING.md Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs: make CONTRIBUTING.md the single source for the process The README's Contributing section said small fixes could go straight to a PR, which contradicts the new discussion/issue requirement. Point it at CONTRIBUTING.md and carry over the conventional-commit and AI-disclosure policies so nothing is lost. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs: close the three process gaps in CONTRIBUTING.md alfred-openspec on #1781: 1. The OpenSpec-proposal rule was dropped from the README with nothing replacing it, recreating the gap in #1727. New step 2 carries the threshold over verbatim from the README (new features, significant refactors, architectural changes) plus the philosophy paragraph, says to open the proposal as its own PR and wait for approval, and tells anyone unsure to ask in the issue from step 1. 2. The discussion path contradicted itself: step 1 accepted a prior discussion while step 3 required 'Closes #123'. The PR step now says to link what you opened in step 1, 'Closes #123' for an issue or a link to the discussion when there is no issue. CodeRabbit's thread on README.md:227 is the same defect, so the README sentence says 'the issue or discussion' too. 3. The local setup was missing 'pnpm exec tsc --noEmit', which CI runs, and the README called the guide a development setup after 'pnpm run dev' and 'dev:cli' were removed. The command is added, the guide states that those four commands are exactly what CI runs, and the README pointer now describes the guide as the full process rather than a setup. Verified each documented command against this checkout: build, tsc --noEmit and lint all pass as written. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
0b60a0ac1f |
chore(deps): bump the website-dependencies group in /website with 5 updates (#1815)
Applies dependabot's website bumps (#1812) and syncs the postcss override in website/pnpm-workspace.yaml, which dependabot does not know about, keeping the three override declarations in agreement. Co-authored-by: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
6981c84df0 |
chore(deps): bump zod to 4.5.4 and eslint to 10.9.1 (#1814)
Consolidates the two open root-lockfile dependabot bumps (#1810, #1811) into one PR so the pinned flake.nix pnpmDeps hash only has to be regenerated once. Co-authored-by: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
63666c8bb2 |
ci: report the correct pnpmDeps hash when flake.nix is stale (#1817)
* ci: report the correct pnpmDeps hash when flake.nix is stale Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * ci: scope the reported hash to the pnpmDeps block Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * ci(flake): scope every hash rewrite to the pnpmDeps block alfred-openspec on #1817: the workflow read is scoped now, but the script it runs is not. update-flake.sh read CURRENT_HASH from the first hash assignment anywhere in flake.nix, and all three in-place rewrites matched every hash assignment. flake.nix holds one fixed-output derivation today, so that lands on the right line by luck; add a second and the script stamps the placeholder over both, reads back whichever mismatch Nix reported first, and writes pnpmDeps' hash into the other derivation. Scoping only the workflow left that path fragile, as the review says. The address range is declared once as PNPM_DEPS_BLOCK and used by the read and all three rewrites, so the scoping cannot drift between call sites. Also guards the read: an unmatched block previously left CURRENT_HASH empty, and the failure path would then restore hash = "". It now exits before touching the file. Verified against a three-derivation fixture with pnpmDeps in the middle, which catches both shapes of the bug: the scoped read returns the pnpmDeps hash while an unscoped read returns the first derivation's, the placeholder is written once rather than three times, and the neighbouring hashes survive the restore. That fixture is the new test, alongside a static check that no hash read or rewrite in the script is missing the range. Verified the static check fails when any one call site is unscoped. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * test(flake): run the scoping fixture on its own volume The new test failed on windows-pwsh with 'sed: cannot rename ./sedKaAflu: Invalid cross-device link'. sed -i writes its temp file in the working directory and renames it over the target; on a GitHub Windows runner the repo is on D: and os.tmpdir() is on C:, so that rename crosses volumes. bash now runs with cwd set to the fixture directory and addresses the file by name, which keeps the temp file and its rename on one volume. The assertions are unchanged. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
9ec0a090b8 | fix(security): patch fast-uri advisories (#1768) | ||
|
|
954d4796a4 |
docs(community): add a community showcase (#1739)
* docs(readme): list the independent openspec ui project * docs(community): move the showcase out of the readme |
||
|
|
98bf53e59e | fix(workflows): ground proposals in relevant project code (#1737) | ||
|
|
d0071d7326 | docs(archive): show how to retire capabilities (#1751) | ||
|
|
7da3f34fb6 |
fix(tasks): include verification in generated plans (#1660)
* fix(tasks): include verification in generated plans * test(tasks): enforce inline verification guidance * fix(tasks): harden verification guidance * test(tasks): verify every onboarding checkbox |
||
|
|
7276c6c268 |
fix(packaging): print the completions tip from the CLI, not a postinstall script (#1704)
* fix(packaging): print the completions tip from the CLI, not a postinstall script The package's only install script existed to print one line suggesting `openspec completion install`. Shipping it made every `npm install -g` emit an npm allow-scripts warning, and `npm approve-scripts` then failed with ENOMATCH because it looks in the local project, not a global install — so the warning looked like a packaging fault with no way to clear it. The tip now prints once on the CLI's first run, recorded via a `completionTipSeen` flag in the existing global config alongside the telemetry notice's `noticeSeen`. It writes to stderr so it can never contaminate piped stdout, and is suppressed under CI, OPENSPEC_NO_COMPLETIONS=1, `--json` runs, and `openspec completion` itself. The published package now ships no lifecycle scripts at all. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(completions): stop the first-run tip from corrupting global config Adversarial review of the previous commit found it wrote a defaults-merged config: `saveGlobalConfig({ ...getGlobalConfig(), completionTipSeen: true })` stamped `profile: "core"` into every user's config.json on first run. `migrateIfNeeded` treats a raw `profile` as "already migrated", so the one-time profile migration would never run again — and `openspec update` then deleted the user's installed workflow skills. Reproduced: 2 skill directories removed where main reports "Migrated: custom profile with 8 workflows". The same write also overwrote an unparsable config with defaults and made `openspec config list` report defaults as explicit. The tip now reads and writes the raw config file and touches only its own key, leaving an unreadable config strictly alone. Other hardening from the same review: - Suppress the tip for the hidden `__complete` resolver. Generated completion scripts call it on every Tab press with stderr discarded, so the one-shot tip was consumed where nobody could see it. - Defer, never consume, when stderr is not a terminal. Agents and pipes drive this CLI far more often than humans do and would otherwise spend the tip into a log nobody opens. - Skip the tip when completions are already installed. Previously the CLI advertised `completion install` to users who had run it — including on the very next command after installing. Adds `isInstalled()` to the bash/fish/powershell installers, mirroring the zsh one. - Use the repo's `isCiEnvironment()` instead of a `CI === 'true'` string check, so `CI=yes`/`True`/`on` are as quiet as telemetry is. - Move the call to `postAction` so the tip trails the command's output instead of pushing errors and `init`'s setup summary down the screen. - Record before printing, so an unwritable config dir means silence rather than nagging on every run. Tests: assert the message literal (mutation testing showed the message text was the one unguarded behavior), the raw-write shape, corrupt-config safety, the already-installed path, the defer policy, and an e2e case pinning the non-TTY contract. Docs: SECURITY.md no longer claims zero lifecycle scripts — `prepare` is still declared and runs for git/directory installs; the registry-install claim is the accurate one. `OPENSPEC_NO_COMPLETIONS` is now documented. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * test(completions): make the unwritable-config case portable to Windows fs.chmodSync(dir, 0o555) does not stop a write on Windows, so this test's unwritable condition never existed there: markTipSeen succeeded, the tip printed, and windows-pwsh was the only failing job. Occupy the config directory's path with a file instead. mkdirSync with recursive: true tolerates an existing directory but throws on an existing file on every platform, so the persist fails where a real permission error would - before anything is printed. Also asserts the path is still a file, so a partial write through the failure would be caught. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(completions): retire the first-run tip instead of advising a dead end Second adversarial pass over the tip, covering the hardening commit itself. - An undetected or unsupported shell now retires the tip quietly. It used to print, but `openspec completion install` exits 1 for exactly those users ("Shell 'tcsh' is not supported yet" / "Could not auto-detect shell"), so the one message they would ever get about completions sent them to a command that fails. - `markTipSeen` re-reads the config immediately before writing and swaps the file in by rename. Deciding whether to show the tip costs a `ps` spawn plus a stat, and a sibling process writing config in that window got clobbered — on a first run that is exactly when telemetry mints `anonymousId`. Concurrent-process loss drops from 15/40 to ~2/40, and what now usually loses is the tip's own flag (it simply shows once more) rather than telemetry identity. The residual is the non-atomic read-modify-write shape shared with telemetry's own writer. - `isInstalled()` uses stat().isFile(), so a directory at the install path no longer counts as an installed completion script. - Documented what `isInstalled()` actually promises: the script file, not the profile sourcing line that bash and PowerShell also need. Callers deciding whether to *advertise* completions want the loose reading — a user whose profile config failed has already met the installer. - Corrected a comment claiming the probe costs "one stat": detectShell() forks `ps` to read the parent process on every non-Windows run. Tests: mutation testing found four surviving mutants — dropping isCompletionRun from the defer policy, reverting isCiEnvironment to a CI==='true' string check, failing closed on an undetected shell, and neutering the non-object config guard (which lets a JSON array config be rewritten as {"0":...}). All four now fail a test. Adds direct coverage for the three new isInstalled() implementations, which had none. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(validate): stop `change validate` exiting past commander's postAction `change validate` on a failing change called process.exit(exitCode). That tears down before commander's postAction hook, which is the same trap the `update` command documents 165 lines earlier: "exiting here would skip commander's postAction hook, killing the telemetry flush mid-request". A change that fails validation is a routine outcome, not an error, so this silently dropped the telemetry flush and — since the completions tip moved to postAction — the first-run tip for anyone whose first command was a failing validate. Verified under a pty: before, the tip never printed and completionTipSeen was never recorded; after, both happen and the exit code is still 1 (validate() already sets process.exitCode, which Node honours at natural exit — top-level `validate --all` has always relied on exactly that). The existing e2e in validate-scenario-loss.test.ts pins the exit code. Also wraps the postAction tip in try/finally so the telemetry flush runs even if the hint throws: program.parse() is synchronous, so a rejection there has no catch above it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
9643888a75 |
fix(schema): resolve main-spec reads against the store-aware root (#1703)
* fix(schema): resolve main-spec reads against the store-aware root
The spec-driven `specs` instruction named
`openspec/specs/<capability-path>/spec.md` — a cwd-relative path — for the
two operations that touch a capability's main spec: step 1 of the MODIFIED
workflow ("locate the existing requirement") and the edit that fixes a
leftover TBD Purpose.
When the change lives in a registered store, the main spec is under the
store root. Verified against one: `openspec instructions specs --store
mystore --json` returns `planningHome.root` pointing at the store while the
instruction sent the read to the working repo, where the capability does
not exist. Where a local capability happens to share the name it is worse
than a miss — the read succeeds against a different capability and step 2
copies the wrong requirement block into the delta, silently.
Both now use `<planningHome.root>/openspec/specs/...`, the root the same
JSON already returns, matching what sync-specs.ts and archive-change.ts
have said since they were written: use the store-aware root, not a
hardcoded repo path.
Guidance text only — no CLI, parser, or archive behavior changes. The two
remaining `openspec/specs/` mentions describe the shape of a capability
path rather than a file operation, and are left alone.
Closes #1702
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(schema): make the store-aware root unconditional, and prove it resolves
Two hardening findings.
The wording said the root "points at the store when a store is selected."
Verified across all four root configurations, that undersells it: a project
`store:` pointer (source `declared`) and a global default store (source
`global_default`) both resolve to the store with no `--store` flag passed.
An agent reading the old sentence could conclude the case did not apply to
it and fall back to a repo-relative path. It now says to always use the
field and not to reason about which case applies.
The test only pinned the placeholder text, which would still pass if
`planningHome.root` were renamed or the suffix were wrong. Added a guard
that substitutes the placeholder with a real resolved planning home and
asserts the composed path lands on an actual main spec. Mutation-tested:
inserting a path segment and renaming the field each fail it.
Verified end to end that the composed path exists under all three
store-selecting configurations, and under a plain local repo.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* test: compose the main-spec path from segments, not string substitution
The guard substituted `planningHome.root` into a template spelled with
forward slashes. On Windows that yields a mixed-separator path, so the
assertion passed because Node accepts forward slashes there rather than
because the path was built correctly. Windows CI was green either way;
this makes the construction right instead of merely tolerated.
The suffix is now captured on its own and joined to the root with
path.join, so the assertion uses native separators everywhere. All three
mutations (cwd-relative path, extra segment, renamed field) still fail
the guard.
Addresses CodeRabbit review on #1703.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
18688c8b27 |
fix(archive): never dead-end a capability retirement (#1699)
* fix(archive): never dead-end a capability retirement A change whose delta removes the last requirement a capability has rebuilds the main spec empty, which can never validate. Archive already knows retiring is the fix and names the `retire_capabilities: true` marker that authorises deleting the spec - but only when the marker is the single thing missing. If the spec also holds a line the merge cannot account for (a `## Notes` section, a comment under a requirement - both ordinary), that hint was suppressed, and the hint that names such lines only spoke to authors who had already set the marker. Neither fired, so the archive aborted on "Spec must have at least one requirement" with no guidance at all: the exact dead end the marker exists to close. Archive now names the blocking content in that case. It deliberately does not name the marker there - adding it would not have let this run through, and the marker is only ever named when it really is the one thing missing. Once the content is resolved, the rerun names the marker. Closes #1696 * fix(archive): harden the blocked-retirement abort Three follow-ups to the same message. The blocking lines are authored spec content printed verbatim to a terminal, so they now get the treatment `describeChangeName` already gives a change directory name: control characters replaced, since a raw CR could forge a line of its own and an ESC could redraw the screen. Each line is bounded too - one very long line would push the way out of the abort off the reader's screen - and the cut counts code points so it can never leave half a surrogate pair. Both the declared and undeclared branches share the helper, so the marker-declared abort that shipped with #1484 is hardened with it. The wording no longer claims retiring is "the way through". It is not, in the one case this fires on that has a live requirement hiding in a second `## Requirements` section: merging the sections fixes that spec without deleting anything. `openspec/specs/cli-archive/spec.md` records the behavior change - the blocking lines are named whether or not the marker was declared, and the marker is still named only when adding it would let the archive through. * refactor(archive): drop a helper the revised wording made single-use The marker sentence is said in one place again, so it goes back inline rather than through a function that now has one caller. Also corrects the comment above `emptiedByThisRun`: retiring is not the only fix in every case it covers, which is exactly why the message stopped saying so. * docs(openspec): record the change as a delta, not a direct spec edit Both conventions exist in this repo's history, but the two most recent behavior fixes (#1609, #1616) carry an `openspec/changes/` delta rather than editing the main spec in place, which is also the workflow this project asks of everyone else. The delta reproduces the whole Capability Retirement requirement, so archiving it drops no scenario. Verified by archiving into a scratch copy of `openspec/`: the merged main spec differs from today's by exactly the three added bullets. * fix(archive): report an unhonorable marker alongside the blocking content An author who set `retire_capabilities: yes-please` believes they have authorised the deletion. Clearing the blocking content first, only to then learn the marker was never read, is two aborts for one mistake. The abort still never invites the marker to be added while content blocks the retirement - it only reports the one already there. The spec delta records that distinction, which the old bullet ("say nothing about the marker") did not draw. * style(archive): use one sentence for an unhonorable marker in both aborts * fix(metadata): strip control characters from an unhonorable marker reason Every reason a boolean change-metadata marker gives quotes something the author wrote - a schema name, a parser message carrying one, a filesystem error carrying a path - and two commands print it straight to a terminal. A schema name carrying a raw ESC, with the marker set, put that ESC on screen through `openspec archive`; `openspec validate` prints the same reason. Fixed at the source in `readBooleanMarker` rather than at either call site, so no consumer has to remember. The reason still quotes the name recognisably; only control characters are replaced. Reported by CodeRabbit on #1699. Pre-existing on main, and this PR would have added a second place it reaches the terminal. * test(archive): fix a comment left behind by the reworded abort |
||
|
|
c747ed1f34 |
feat(init): add language option (#1685)
* feat(init): add language option * fix(init): harden language configuration * fix(init): fail when language config cannot be written |
||
|
|
15e50d6889 |
fix(opencode): pass command arguments to workflows (#1664)
* fix(opencode): pass command arguments to workflows * test(opencode): recognize existing argument placeholders * test(opencode): harden argument generation * test(opencode): cover commands-only upgrades * test(opencode): verify repaired command content |
||
|
|
cf06d45f91 |
fix(profiles): include sync with archive workflows (#1663)
* fix(profiles): install sync with archive workflows * test(profiles): harden archive dependency coverage * fix(config): preserve custom profile ownership |
||
|
|
f3aa167d6e |
feat(tools): add Zed Agent support (#1659)
* feat(tools): add Zed Agent support * fix(tools): detect Zed projects |
||
|
|
a72a74de65 |
fix(update): only suggest IDE restarts when needed (#1656)
* fix(update): only suggest IDE restarts when needed * test(update): cover restart hint edge cases |
||
|
|
a2b965aa5e |
fix(workflow): keep no-spec schema changes valid (#1655)
* fix(workflow): scaffold valid no-spec changes * fix(workflow): normalize specs artifact paths |
||
|
|
98c79324ac | docs(workflows): fix sequence diagram rendering (#1654) | ||
|
|
fc0fec1250 |
fix(feedback): keep full reports in issue bodies (#1653)
* fix(feedback): keep full reports in issue bodies * fix(feedback): preserve report formatting |
||
|
|
610b78f655 |
chore(changeset): add catch-up changesets for 6 untracked fixes (#1640)
Six user-facing fixes merged after v1.8.0 without a changeset, so they would ship in v1.9.0 with no changelog entry and their authors uncredited. All are patch fixes; the release target stays at 1.9.0. Covers: #1637, #1607, #1632, #1616, #1612, #1523. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
8127c7b7cc |
fix(schema): preserve YAML formatting when forking a schema (#1607)
* fix(schema): preserve YAML formatting when forking a schema Rename a forked schema via yaml's Document API (parseDocument + doc.set) instead of round-tripping through parseSchema/stringifyYaml, so block scalars, comments, and key order in the source schema.yaml survive the fork. Keep the structural parseSchema validation before the document mutation so an invalid source is still rejected (addresses PR #1130 review). Adds fork-level regression coverage for both formatting preservation and invalid-source rejection. Co-Authored-By: JinzeLin <linjinze999@users.noreply.github.com> Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * harden(schema): clean up partial fork when validation fails If the source schema is structurally invalid, parseSchema throws after copyDirRecursive has already created the destination directory, leaving a broken half-schema on disk that made the next fork report "already exists". Wrap the read/validate/rename in a try/catch that removes the just-created destination on any failure and rethrows so the original error still drives the JSON/exit-code reporting. The cleanup can only ever delete a directory this run created: the no-force existing-dest path returns before the copy, and the --force path removes the prior directory first. This also closes a mid-write truncation window for free. Adds regression coverage: cleanup + retryability on invalid source, the pre-existing-destination-is-never-touched invariant, and a lock-in that YAML-ambiguous names (true/false/null/off) round-trip as strings. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * harden(schema): validate fork source up front; never mask fork errors Second hardening pass on the fork command, from an adversarial review of the previously-added cleanup. 1. Atomicity: validate the source's schema.yaml up front, immediately after assertSchemaTreeCanBeCopied and BEFORE the --force removal of an existing destination. Previously the source was validated only after the copy, so `fork --force <invalid-source> <existing-valid-dest>` destroyed the existing destination and then failed, leaving nothing. This matches `schema init`, which already validates before it overwrites. Behavior is unchanged for valid sources, and the redundant post-copy validation is dropped. 2. Never mask the real error: the failure-cleanup rmSync is now wrapped in its own try/catch. fs.rmSync's `force` only suppresses ENOENT, not EPERM/EBUSY/ENOTEMPTY (e.g. a locked file on Windows or a concurrent process), so a failed cleanup could previously replace the real "Invalid schema" diagnostic with a confusing filesystem error. The original error is now always rethrown. Adds regression coverage: --force with an invalid source leaves a valid destination intact; the pre-existing-destination test now uses a valid source so it exercises the no-force "already exists" guard directly. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * harden(schema): reject self-fork and stage fork before replacing destination Two data-loss defects in `schema fork --force` (per alfred-openspec review): 1. Self-fork: forking a schema onto itself removed the destination (which IS the source) before the copy, so the copy then read a directory it had just deleted — destroying the only copy. Now rejected up front by comparing the real (symlink-resolved) source and destination paths before any removal. 2. Non-atomic replacement: an existing destination was removed before the new fork was fully copied and name-updated, so a mid-copy failure left the user with nothing. The fork is now staged in a temporary sibling directory and only swapped into place once complete; any failure while staging leaves both the source and the existing destination untouched. Adds regressions: self-fork is rejected with the source intact; a forced fork whose copy fails leaves the existing destination byte-identical with no staging leftovers. Co-Authored-By: JinzeLin <linjinze999@users.noreply.github.com> Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * harden(schema): back up destination before installing fork so a failed final move restores The stage-then-swap still removed the destination and then renamed staging into place; if that final rename failed (e.g. a Windows lock) the destination was gone with no restore. Now, when a destination exists, `fork --force` moves it to a sibling backup, installs the staged fork, and only then discards the backup. If the install rename throws, the backup is moved back so the original destination is never lost. Non-existing destinations keep the simple staging rename. Adds a regression: forcing the final staging->destination move to fail leaves the pre-existing destination byte-identical with no staging/backup leftovers. Co-Authored-By: JinzeLin <linjinze999@users.noreply.github.com> Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * harden(schema): surface unrecoverable fork restore + hide fork temp dirs from discovery Two more edge cases from alfred's review: 1. A failed backup->destination restore was silently swallowed, so if the final install AND the restore both failed the user lost the destination with no clue the backup existed. Now that case throws an error naming the backup directory and how to move it back, with the original install error attached as cause. 2. The transient `.fork-staging-*` / `<name>.fork-backup-*` directories live inside the schemas dir, so a concurrent scan could surface them as real schemas. isSchemaDir (the single discovery chokepoint) now excludes them; real schema names are kebab-case (no dots) so this can never hide a schema. Adds regressions: an unrecoverable restore surfaces the backup path (and the rescued content is really there); fork temp dirs are excluded from listSchemas and listSchemasWithInfo. Co-Authored-By: JinzeLin <linjinze999@users.noreply.github.com> Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * harden(schema): fingerprint fork destination to abort on concurrent edits A concurrent process could edit an existing fork destination between the moment --force authorized the overwrite and the moment the destructive swap ran, and those edits were silently destroyed (reproduced by alfred: mutate destination schema.yaml during copy; --force completed and deleted the newer content). Now, when overwriting an existing destination, the fork: - fingerprints the authorized destination (SHA-256 over every file's relative path and bytes) BEFORE staging; - re-fingerprints and compares immediately before moving the destination aside; on mismatch it ABORTS without touching the destination, preserving the concurrent changes and telling the user to re-run; - re-fingerprints the backup before discarding it on the success path; if it changed during the install window it is kept, not deleted, and its location is surfaced. All prior guarantees remain: self-fork rejection, stage-then-swap, backup/restore on failed install with the backup path surfaced, and the temp-dir discovery filter. Adds regressions: a destination edited concurrently during staging aborts the fork and preserves the edit; a backup modified during the install window is kept and its location surfaced. Co-Authored-By: JinzeLin <linjinze999@users.noreply.github.com> Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(schema): avoid stat-then-read in fork fingerprint (CodeQL js/file-system-race) fingerprintDir called fs.lstatSync then fs.readFileSync on the same path, which CodeQL flags as a file-system race (the file may change between the check and the read). Use the Dirent type already returned by readdirSync ({ withFileTypes: true }) instead of a separate lstat, and read files directly, deriving the size from the bytes read. Behavior is unchanged (13/13 fork-fidelity tests, incl. the concurrent-edit race regressions, still pass); one fewer syscall per entry. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * harden(schema): validate the completed staged fork before any destructive step The up-front parseSchema only checks the SOURCE, but copyDirRecursive reads source files that can change mid-copy, so the staged result can be invalid even though the source was valid at the pre-check (reproduced by alfred: mutate source schema.yaml to invalid inside copyFileSync; --force installed the invalid fork and deleted the valid destination). Now, after copying and the Document-API name edit, the fork validates the COMPLETED staged schema.yaml (the exact bytes about to be installed) with parseSchema BEFORE any destination displacement. On failure it aborts, cleans up staging, and rethrows a clear error ("the staged fork of '<source>' is not a valid schema ...; aborted, '<dest>' was not modified") chaining the parse error. The up-front source parseSchema stays as a fail-fast; this is the authoritative gate. Order before the swap: validate staged -> fingerprint-revalidate dest -> rename dest->backup -> rename staging->dest -> revalidate+rm backup. Adds a regression: a source that becomes structurally invalid during staging aborts the fork and leaves the valid destination byte-identical, no leftovers. Co-Authored-By: JinzeLin <linjinze999@users.noreply.github.com> Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: JinzeLin <linjinze999@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
3281f1f068 |
fix(deps): patch js-yaml and nanoid advisories via pnpm overrides (#1635)
Resolve all three open Dependabot alerts (all high severity): - GHSA-5p4m-2wfm-xmqj — js-yaml quadratic-CPU !!omap DoS (#96, #97). Root tree carried js-yaml 3.15.0 (via read-yaml-file) and 4.3.0 (via @changesets/parse). Dev-only; never in the published CLI, which uses `yaml`, not `js-yaml`. Pinned to >=3.15.1 / >=4.3.1. - GHSA-2v37-7h3g-55p8 / CVE-2026-67213 — nanoid size=0 infinite loop (#99). Present in both root (dev, via postcss<-vitest) and website (build-time, via postcss<-next) trees. Pinned to >=3.3.17 (resolves to 3.3.18). Overrides added to all four override surfaces (pnpm-workspace.yaml + package.json, root and website) to keep them in sync, each YAML entry annotated with its advisory id and removal condition. flake.nix pnpmDeps FOD hash regenerated for the root lockfile change (verified via nix build; hash-mismatch-count 0). dependabot.yml gains a note documenting the two surfaces Dependabot cannot manage (pnpm overrides + the Nix flake). Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
b96b3e85cd |
chore(deps): bump safe website-dependencies subset (#1634)
* chore(deps): bump safe website-dependencies subset (defer fumadocs 16.14 / ZBSearch migration) Ships the non-breaking bumps from dependabot PR #1631, holding back the fumadocs 16.14 major that requires a website source migration. Bumped: - fumadocs-mdx ^15.2.1 -> ^15.2.2 (resolves 15.2.3; builds cleanly) - lucide-react ^1.27.0 -> ^1.28.0 (resolves 1.31.0) - next 16.2.12 -> 16.3.0 - postcss ^8.5.25 -> ^8.5.26 (override ^8.5.22 governs resolution) Held (defer to a dedicated migration PR): - fumadocs-core ^16.12.1 (16.14 replaces Orama with ZBSearch) - fumadocs-ui ^16.12.1 (pairs with core) fumadocs-mdx 15.2.3 does NOT pull core 16.14 transitively; type-check and next build both pass. esbuild stays 0.28.1, so allowBuilds is unchanged. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * chore(deps): apply the postcss bump for real (align override + lock to 8.5.26) The manifest declared postcss ^8.5.26 but the pnpm override stayed ^8.5.22, so the importer resolved 8.5.25 and the declared bump had no effect. Raise the override (website/pnpm-workspace.yaml + website/package.json pnpm.overrides) to ^8.5.26 and re-lock so postcss resolves 8.5.26 everywhere. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
207f3cc515 |
fix(config): label the update workflow in the picker; drop "expanded-profile" wording (#1632)
* fix(config): label the update workflow in the picker and drop "expanded-profile" wording The config workflow picker builds each row's label from WORKFLOW_PROMPT_META in src/commands/config.ts. The table had entries for 11 of the 12 workflows but not `update`, so `openspec config` rendered that row as the raw id `update` with a `Workflow: update` placeholder description. Since `update` is one of the six core workflows, every user who opens the picker saw it. Add the missing `update` entry so the row reads "Update change / Revise the planning artifacts of an existing change". Also reword the update-change workflow template, which called `/opsx:continue` and `/opsx:new` "expanded-profile" workflows. There is no "expanded" profile; the only profile values the product stores are `core` and `custom`. They are now described as "optional" workflows. Regenerated the committed skills.sh mirror and parity hashes accordingly. Harden with a regression test asserting every ALL_WORKFLOWS id has real picker metadata (no raw-id name, no "Workflow:" placeholder), so a future workflow addition can't silently reintroduce the fallback. Closes #1627 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * chore: regenerate skills and parity hashes after rebase onto main Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
4b114aade9 |
chore(deps-dev): bump development-dependencies group + refresh flake hash (#1633)
* chore(deps-dev): bump development-dependencies group + refresh flake pnpmDeps hash Supersedes #1630. Dependabot's PR bumps two dev dependencies within their existing package.json semver ranges (eslint 10.8.0 -> 10.8.1, typescript-eslint 8.65.0 -> 8.66.0), touching only pnpm-lock.yaml. That lockfile change invalidates the flake's fixed-output pnpmDeps.hash, so #1630 fails Nix Flake Validation ("pnpm failed to install dependencies"). Dependabot cannot update the Nix FOD hash, so this PR carries the same bump together with the refreshed hash. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(nix): set pnpmDeps hash for updated lockfile Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
804427b6ff |
fix(telemetry): suppress first-run notice in --json mode (#1609)
* fix(telemetry): suppress first-run notice in --json mode The first-run telemetry disclosure notice was written to stdout from the global preAction hook. On a user's first-ever command with --json this polluted stdout and could break JSON parsers. Read the executing command's --json flag (actionCommand.opts().json) and, when set, skip the notice and leave noticeSeen unset so the disclosure is deferred to the first later non-JSON run rather than lost. Spinner suppression, new-change --json output, and structured JSON errors already landed on main (#960, #1190); this closes the one remaining stdout writer in --json mode. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * harden: detect --json from argv to cover all invocation forms The preAction guard read actionCommand.opts().json, which only sees a declared leaf option. That missed two supported --json forms that emit a single JSON document to stdout: - openspec store --json (permissive group reads --json from residual args; never declares the option, so opts().json is undefined) - openspec workset --json <sub> (--json on the parent group, consumed before the leaf; leaf opts().json is undefined) Both would still print the first-run telemetry notice ahead of their JSON. Detect --json from process.argv instead: it covers leaf, parent, and residual-arg forms uniformly. Suppressing is always safe (the disclosure defers to the next non-JSON run, never lost), so a broad argv check is the correct, conservative signal. Also add a direct assertion that noticeSeen stays unset after a silent run, and note the pre-existing raw-stdout commands (completion generate, config get/path, __complete) as out of scope. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * refactor: derive --json from parsed command state + regression test Replace the process.argv check with isJsonRun(command), an exported pure helper that reads Commander's parsed state: optsWithGlobals().json (leaf and parent-group forms) OR command.args (residual --json on permissive bare groups like store). This is tied to the actually-parsed command rather than raw args, and — unlike process.argv — is unit-testable in-process. Add test/core/cli-is-json-run.test.ts: a synthetic program reproducing all three registration patterns proves isJsonRun returns true for status --json, store --json, workset --json list, and workset list --json, and false otherwise. This locks in the store/workset coverage against future regressions (an e2e test can't: telemetry is disabled under CI, so the notice never fires there). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * docs(spec): qualify first-command notice scenario as non-JSON The generic 'First command execution' scenario asserted the notice displays on every first command, contradicting the JSON scenario that says it does not. Qualify it as 'without --json' so the required behavior is unambiguous. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
17581c11ed |
fix(init): only show 'Restart your IDE' hint for IDE-embedded tools (#1610)
Reconstructed on current main (the original branch predated the Codex .agents rename, Command Code, Rovo Dev, Antigravity, Zoo Code, and the Kimi/Windsurf changes, so a direct rebase conflicted heavily in config.ts/init.ts/init.test.ts). Adds requiresIdeRestart to AIToolOption and gates the success-screen restart hint so it shows only when an IDE-resident tool actually received a surface. Wording follows that tool's own surface. Closes #1067. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
1a10dd5820 |
docs(opsx): clarify /opsx:sync description and add usage section (#1606)
* docs(opsx): fix /opsx:sync description and add detailed documentation - Changed description from 'Sync delta specs to main' to 'Merge delta specs into main specs' - Added detailed Usage section for /opsx:sync command - Now consistent with commands.md and migration-guide.md - Improves documentation completeness and clarity * docs(opsx): harden Sync delta specs section for accuracy and house style Fold the /opsx:sync usage entry into a single prose paragraph to match the six sibling Usage sections (heading -> fence -> paragraph), and fix two accuracy issues found against src/core/templates/workflows/sync-specs.ts: - Drop the invented "changes see each other's specs" and "test integration" use cases (no cross-change propagation or test step exists). - State that sync applies the whole delta -- a REMOVED requirement is deleted from the main spec and a RENAMED one retitled -- so the section no longer reads as additive-only. - Use the file's spaced em-dash convention. Docs-site build verified: sync-docs + fumadocs next build compile and render /docs/opsx end-to-end. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Howard <yhwelcome1981@gmail.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
137404b423 |
fix(cli): reject missing roots for list and validate (#1612)
* fix(cli): reject missing roots for list and validate * test(cli): cover legacy list root fallback |
||
|
|
144901ca74 | chore(dependabot): ignore unsupported major updates (#1623) | ||
|
|
c751b3da52 |
fix(validate): count every level-4 header as a scenario in the loss guard (#1521)
* fix(validate): count every level-4 header as a scenario in the loss guard The scenario-loss guard (#1482) recognized only `#### Scenario:` headers, but the spec path (SCENARIO_HEADER / countScenarios) counts every `#### ` child of a requirement as a scenario. A MODIFIED block that dropped a differently-labeled level-4 child (e.g. `#### Edge case`) therefore passed validate and was silently deleted by archive. Align parseScenarioBlocks with the spec path so both agree. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * test(validate): guard scenario-header parity; reuse SCENARIO_HEADER Harden the scenario-loss parity fix after a multi-agent review: - Export SCENARIO_HEADER from requirement-text.ts and reuse it in the delta path (scenarioHeaderAt/scenarioNameAt) so parity is guaranteed by construction, not two matching literals plus a comment. - Add boundary tests for the widened matcher: a level-5 (#####) header must not count, an unlabeled #### inside a fence must not count, an optional Scenario: label normalizes (relabel is not a loss), and unlabeled scenarios are counted by multiplicity. Plus an integration case: a dropped labeled scenario is caught even when an unlabeled sibling is kept (validate/archive parity, both directions). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * test(validate): harden scenario-name folding + incoming-fence parity Adversarial review of the scenario-loss guard surfaced one over-strict nit and one untested symmetry: - scenarioNameAt now also strips a CommonMark closing `#` run, so `#### Foo` and `#### Foo ####` fold to the same scenario name. Without this, relabeling a scenario's header on one side (ATX-open vs ATX-closed) read as a dropped scenario — a false-abort. Safe direction only: a genuine drop still lowers a folded name's count and is caught. - Add unit tests for the untested incoming-side fence mask (a fenced `####` in the MODIFIED block must not satisfy a real scenario), lowercase `scenario:` label normalization, and the ATX-closed header fold. Behavior for conventional `#### Scenario:` headers is unchanged; parser, validation, and archive suites stay green (269 tests). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(validate): match CommonMark whitespace in ATX-close strip; changeset nit Second adversarial-review round follow-ups: - scenarioNameAt's ATX-closing-sequence strip now matches only a space/tab before the trailing `#` run (`[ \t]` not `\s`), exactly as CommonMark defines a closing sequence. A looser `\s` could strip a `#` run after an exotic space (e.g. NBSP) that CommonMark keeps rendered, folding two distinct scenario names into one and masking a real loss. Correct-direction hardening for a data-loss guard; no behavior change for real space/tab-authored headers. - Changeset: describe the header whitespace outside the code span to satisfy markdownlint MD038 (no trailing space inside `#### `). Resolves CodeRabbit. Parser/validation/archive suites green (243 tests). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
07dea6ed2f |
fix(update): don't hijack the agents target on legacy Codex upgrade (#1522)
* fix(update): don't hijack the agents target on legacy Codex upgrade Codex and the vendor-neutral `agents` target share `.agents/skills`. In upgradeLegacyTools, a Codex install inferred only from global ~/.codex/prompts wrote Codex skills into `.agents` and flipped the ownership marker agents -> codex, silently rewriting an existing agents-owned tree. The main generation path reconciles shared-target ownership first; this legacy-upgrade path did not. Add sharedSkillRootOwnedByOther() and skip generation when a different tool already owns the shared root (marker or existing tree), while still allowing a genuine first-time Codex upgrade with no `.agents` yet. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * test(update): cover the hijack guard end-to-end; name the owner on skip Harden the agents-target ownership fix after a multi-agent review: - Add an integration test that runs the real update flow for the bug scenario (agents-owned .agents + a legacy global Codex prompt) and asserts the marker stays `agents` and skills keep generic `/openspec-` syntax. A unit test of the predicate can't catch a future refactor that stops calling it; this can. - Name the owning tool in the skip message ("...managed by another tool (Shared .agents skills)") via a new sharedSkillRootOwner() helper that sharedSkillRootOwnedByOther now delegates to. - Add a unit case for the ambiguous-tree branch (existing skills, no marker, no inferable syntax) and one asserting sharedSkillRootOwner names agents. - Document the known, harmless re-offer tradeoff (a skipped tool isn't recorded as configured, so a persistent legacy prompt re-offers it). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(update): preserve skipped tool's legacy files; add upgrade-path tests Adversarial review of the shared-root ownership guard surfaced one real integration defect and the review asks from alfred/CodeRabbit. Defect: when the guard skips a legacy Codex upgrade because the `.agents` root is owned by another tool, the caller's immediate legacy cleanup still deleted Codex's repo-local `.codex/prompts/openspec-*.md`. That violates the cleanup contract (remove X only because replacement Y was written): no replacement is written for a skipped tool, so its legacy files must stay. `upgradeLegacyTools` now reports `skippedSharedSkillTools`, and `performImmediateLegacyCleanup` exempts those tools' repo-local artifacts via a new `omitToolLegacyArtifacts` helper. Refactored the per-artifact tool matching out of `getToolsFromLegacyArtifacts` so both share one matcher. Tests (addressing the review + the defect): - update.test.ts: hijack test now asserts Codex is absent from the persisted configured-tool set and that the skip names the established owner. - update.test.ts: inverse no-root case proves a first-time Codex upgrade still writes the `codex` marker via the real UpdateCommand path. - update.test.ts: a skipped tool's repo-local `.codex/prompts` is preserved. - legacy-cleanup.test.ts: unit coverage for omitToolLegacyArtifacts. - shared-skill-target.test.ts: assert sharedSkillRootOwner resolves 'agents'. Docs + changeset updated to describe the preserve-on-skip behavior. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * test(update): lock in legacy-prompt preservation on skipped Codex upgrade Address the outstanding CodeRabbit review notes on #1522. The fix itself is confirmed correct by three independent adversarial reviews — these are test-only hardening that locks in the guarantees the fix promises: - Assert the global ~/.codex/prompts survives (byte-for-byte) in the hijack scenario. Previously the test set the prompt up but never checked it was preserved; on unfixed code Codex would be generated, its 'explore' workflow would read as installed, and the deferred global cleanup would delete the prompt — so this assertion fails without the fix. - Assert the repo-local .codex/prompts is preserved by content, not mere existence (distinguishes 'left untouched' from 'deleted+rewritten'). - Restore the stdout/stderr spies in a finally so a throw can't swallow output for the rest of the suite. - Cover backslash-delimited (Windows) paths in omitToolLegacyArtifacts. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
bf5099e39f |
fix(apply): surface deferred scope instead of silently simplifying tasks (#1530)
* fix(apply): surface deferred scope instead of silently simplifying tasks The /opsx:apply guidance told agents to keep going through tasks but never told them what to do when a task turned out harder than the spec assumed. Agents absorbed the extra scope silently — narrowing, deferring, or declaring partial work done — and marked the task complete anyway (#1529). Add a pause trigger and two guardrails to the shared apply instructions (rendered identically by the skill and command surfaces): surface the added scope and ask rather than simplify to fit, and mark a task complete only when it is fully implemented as specified. Regenerate the static skill and parity-hash pins. Guidance text only — no behavioral code paths change. Fixes #1529 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(apply): anchor deferred-scope guidance to spec scope, not effort Adversarial review flagged that "more complex than the spec assumed" could be read as "takes more effort than I guessed," which would make an agent pause on nearly every task. Retie the pause trigger and guardrail to a change in scope — work beyond what the spec/tasks describe, or dropping / narrowing / deferring specified behavior — so normal implementation effort does not trip it. Regenerate the static skill and parity pins; update the regression test and changeset to match. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(apply): name the "accept exceptions" pattern in deferred-scope guidance Issue #1529's concrete example is an agent that found three exceptions to a "zero writes on the main thread" task, declared them "accepted," and moved on. Add "accept exceptions to" to the pause trigger's verb list so the guidance names that exact failure mode, not just drop/narrow/defer. Behavior is otherwise unchanged; regenerate the static skill and parity pins. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * test(apply): assert the deferred-scope guidance requires pausing CodeRabbit noted the guardrail test checked that added scope is surfaced but not that the agent pauses, so it could pass if the workflow reported scope and kept going. Assert the exact "surface the added scope and pause" phrasing. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
9ae75c86ef |
fix(archive): don't write ANSI escape codes to a redirected (non-TTY) stdout (#1603)
* fix(archive): stop non-TTY confirm prompts from writing ANSI escapes to stdout `openspec archive` asks up to three yes/no questions through @inquirer's `confirm`, which renders by writing ANSI cursor-movement escape sequences — and emits them even when stdout is not a TTY. When archive runs with its output captured to a file or pipe (an agent's background task, CI), those escapes are noise, and in some non-TTY hosts the render loop never settles and repeats `ESC[NNG` moves until the disk fills (reporter hit 19.8 GB). Add `confirmPrompt` in interactive.ts: a real terminal (stdin AND stdout TTY) still gets @inquirer's rich prompt; every other case reads one plain line via node:readline with `terminal:false`, emitting no escapes. Parsing mirrors @inquirer/confirm exactly (prefix match on y/yes and n/no, else the default), and an unreadable stdin rejects with an ExitPromptError-shaped error so the existing #1479 "rerun with --yes" guidance is unchanged. archive's confirmOrBlock now calls confirmPrompt. Closes #1526 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * test(interactive): cover Windows CRLF and drained-stdin paths; doc note Adds two regression tests surfaced by adversarial review of the #1526 fix: - Windows CRLF piped input (`y\r\n`) parses as a clean yes with no ANSI — the reporter's platform, previously untested (all inputs used `\n`). - A second prompt after stdin was already drained blocks with an ExitPromptError instead of hanging, exercising the readableEnded guard. Also documents in troubleshooting.md that a redirected/agent archive run that pipes an answer no longer writes terminal escape codes into the capture. Refs #1526 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(interactive): align non-interactive classification and handle readline errors Addresses two review findings on the #1526 confirm-prompt fix: - confirmPrompt drops to the plain reader whenever either stream is not a TTY, but isNonInteractivePromptError only checked stdin. A stdin-TTY / stdout-redirected run that hit EOF leaked the raw ExitPromptError instead of the #1479 "rerun with --yes" guidance. Classification now also counts a redirected stdout, matching how the prompt mode is chosen. (isInteractive, used broadly elsewhere, is left untouched.) - readYesNo never listened for the readline/input 'error' event, so a stdin error would hang the promise (and go unhandled). It now settles with the underlying fault, guarded so the promise resolves or rejects exactly once. Tests: TTY-stdin/redirected-stdout EOF is classified non-interactive; an erroring input stream rejects instead of hanging; the archive usable-terminal test now models a full terminal (both streams TTY). Refs #1526 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(archive): gate the change picker on a TTY and tidy the reader Follow-ups from a second review round: - selectChange (the no-argument change picker) called @inquirer's `select` unconditionally. `select` writes ANSI escapes to stdout even when redirected — the same #1526 mechanism the confirm prompts were fixed for — so `openspec archive > log.txt` with no change name still spewed cursor moves into the capture before blocking. Refuse before rendering when either stream is not a TTY, with the same "pass a change name / --yes" guidance the caught ExitPromptError already gives. A new test asserts the picker is never reached in a non-terminal run. - readYesNo now removes its input-stream 'error' listener on every settle path (it lives on the long-lived process.stdin) and closes the readline interface on error too, so nothing accumulates across archive's sequential prompts. - troubleshooting.md now notes the picker also stays clean. Refs #1526 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * chore(changeset): add patch changeset for the archive non-TTY fix (#1526) User-facing patch note for the archive ANSI/disk-fill fix. Also drops an unnecessary optional-chain on the non-nullable readline handle in readYesNo (the listener is only attached after the interface exists). Refs #1526 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
83be9d113e |
feat(validate): add --archived to lint task completion of archived changes (#1604)
* feat(validate): add --archived to lint task completion of archived changes `openspec validate --archived` scans every change under changes/archive/ and fails (exit 1) if any has unchecked tasks in tasks.md. This catches changes archived with unfinished work — which the normal validate flow never sees, since it only looks at active changes — and is meant for a pre-commit or CI hook. It is a standalone, opt-in scope: it returns before any existing bulk path, so no current `validate` invocation changes behavior, and it does not re-validate already-applied spec deltas. Reuses getTaskProgressForChange (the same counter status/list/archive use) so task counting never forks, and reads root.archiveDir so it is store-aware. Closes #205 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(validate): fail loudly on archive read errors and unreadable task files Address adversarial + CodeRabbit review of `validate --archived`: - listArchivedChangeIds now returns [] only for ENOENT (missing archive dir) and rethrows permission/I/O/ENOTDIR errors, so a real archive-read failure exits 1 instead of silently reading as "no archived changes". - Add getTaskProgressDetailForChange, which reports task files that exist but cannot be read; --archived turns those into an ERROR (naming the file) rather than silently counting them as zero tasks. The shared getTaskProgressForChange now wraps it and drops the detail, so status/list/archive totals are byte-identical. - Start the spinner after listing so a thrown listing error never leaves a spinner running. Adds regression tests (archive path is a file; archived tasks.md is unreadable) and unit tests for the new detail variant. Docs: align the --archived table verb and add a troubleshooting one-liner. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * test(validate): address CodeRabbit nits on archived-task tests - Build the unreadable-fixture path from separate path.join components instead of a hard-coded Unix-separator string. - Assert the reported unreadable path (canonicalized with realpathSync.native), not just the count, so a wrong path can't pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * refactor(validate): address round-3 review of --archived From three fresh adversarial reviews (scale/perf, flag/output-shape, filesystem/security): - perf: memoize schema→glob resolution across archived changes via a run-scoped SchemaGlobCache, so the same schema.yaml isn't re-parsed once per change (the archive is append-only and can hold thousands). Threaded as an optional arg; existing callers are unchanged. Loop stays sequential by design (per-change work is synchronous) — now documented. - output shape: issue `path` now follows validate's convention — 'tasks.md' for incomplete tasks, and the POSIX root-relative file path for an unreadable file (one issue per file) instead of the bare 'tasks'. - plain output: print `change/<id>` (matching the JSON `type` and bulk validation) instead of `archived/<id>`. - docs: correct the "Never throws" docstrings (glob resolution can throw on a malformed/unsafe schema; the caller guards it) and note the load-bearing projectRoot override for the archive path depth. Store-mode resolution confirmed correct by review. Tests updated + a memo regression test added. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
59c16a4461 |
feat(tools): add Command Code command adapter for /opsx-* commands (#1622)
* feat(tools): add Command Code command adapter for /opsx-* commands Command Code documents custom slash commands under `.commandcode/commands/`, where the command name is the markdown filename without its `.md` extension (see https://commandcode.ai/docs/reference/slash-commands). That is the same flat naming Cursor and OpenCode use, so a standard flat adapter writing `.commandcode/commands/opsx-<id>.md` registers `/opsx-<id>`. Registering the adapter flips Command Code from `none` to `adapter-backed`, so with the default `both` delivery `openspec init` now generates OpenSpec commands alongside the skills it already installs under `.commandcode/skills/`. Builds on #1613, which registered Command Code as a skills-only tool. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(tools): preserve Command Code command arguments * test(command-code): cover commands-only delivery and openspec update Addresses review: prove the Command Code adapter survives both the commands-only init path and the update path, not just default delivery. - init: delivery=commands generates .commandcode/commands/opsx-explore.md and installs no skills. - update: a detected .commandcode install regenerates the flat opsx-<id>.md command (plain Markdown, $ARGUMENTS injected). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
568e56c672 |
chore(release): add catch-up changeset for Rovo, Codex dir, status (#1518)
* chore(release): add catch-up changeset for Rovo, Codex dir, status Cover three user-facing PRs that merged without changesets so they appear in the v1.8.0 CHANGELOG: - #1516 Atlassian Rovo Dev CLI (new tool) - #1511 Codex skills move to shared .agents directory - #1505 openspec status separates planning from implementation Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * chore(release): correct isPlanningComplete wording in changeset Skipped planning artifacts count as satisfied without being written; say "every non-skipped planning artifact exists" to match the CLI and agent-contract docs (alfred/CodeRabbit review). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
73207a6f2c |
feat(copilot): make cloud coding-agent files opt-in (#1517)
* feat(copilot): make cloud coding-agent files opt-in Selecting the `github-copilot` tool auto-generated a GitHub Actions workflow (.github/workflows/copilot-setup-steps.yml) plus an agent file. Writing into a user's CI on init/update is invasive, benefits only the narrow set of Copilot *cloud* coding-agent users, and couples us to GitHub's externally-owned custom-agent format. Cloud files are now opt-in: - `openspec init` prompts before generating them (default No) and records the choice in openspec/config.yaml (`githubCopilot.cloudAgent`). - `--copilot-cloud` / `--no-copilot-cloud` decide non-interactively. - `openspec update` never prompts; it only refreshes files for projects that opted in, or that already have generated cloud files (so existing setups keep working — the migration path). The pre-existing content-matching guarantees are unchanged and now proven by regression tests: a user-customized cloud file is never overwritten or deleted. Opt-in state is persisted via the YAML document model so the user's hand-authored config comments and formatting survive untouched. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(copilot): polish the cloud opt-in — safety, UX, and docs Follow-up hardening driven by a five-agent review swarm over the opt-in. Correctness: - persistCopilotCloudOptIn no longer throws on a scalar/`null` config file (reproduced crash); it starts a fresh map while preserving comment-only and empty files. - Explicit opt-out (`--no-copilot-cloud` / `cloudAgent: false`) now removes OpenSpec-managed cloud files on both init and update, instead of orphaning them. Customized files are still never touched. - `--copilot-cloud` / `--no-copilot-cloud` warns when github-copilot isn't among the selected tools, instead of silently no-opping. UX / discoverability: - init prints whether cloud files were written or, when skipped for want of a signal, how to enable them (`--copilot-cloud`). - When the user opts in but already has their own copilot-setup-steps.yml or agent file, init/update say it was left untouched and that the OpenSpec install step must be added by hand — the direct answer to "will this affect my existing Copilot cloud agent?". - Clearer interactive prompt (names both files; distinguishes the GitHub-hosted cloud agent from Copilot in the editor); a dim, interactive-only, decision- gated hint on `openspec update`; tightened flag help text. Docs (the feature was undocumented): new "GitHub Copilot cloud coding agent" section in supported-tools.md; init flags in cli.md; the githubCopilot.cloudAgent key in customization.md. Tests: interactive prompt (accept/decline), opt-out removal + customized-file preservation, config.yml variant, scalar-config regression, collision reporting, flag-ignored warning, re-init honoring persisted opt-in, and the config parse/warn branches. 2763 tests pass; the only failures are pre-existing and unrelated (completion mocks, adapters loader, one config-profile PATH case, one experimental-alias case), verified identical on clean main. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(copilot): make init cloud-file output honest; harden config guard Final hardening pass (adversarial review of the opt-in polish). - init's success line listed both cloud-file paths from the *decision* to write, not from what was written — so it claimed files that a write skipped (user already owns them) or that the alternate-agent path removed. It now lists only OpenSpec-managed files that actually exist after the write (listManagedCloudFiles), keeps the "left untouched" caveat for user-owned files, and reports opt-out removals in the normal output block. - persistCopilotCloudOptIn's non-map guard used isCollection, which is also true for sequences, so a YAML list at the config root still made setIn throw. Gate on isMap so scalars AND sequences fall back to a fresh document; empty/comment-only files still round-trip with comments intact. - Fixed a misleading catch comment on the opt-out removal path. Tests: success-line accuracy over a user-owned file, sequence-root config regression, and listManagedCloudFiles coverage. 318 tests pass across the touched suites; build + lint clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(copilot): replace a non-map githubCopilot node before setIn Addresses alfred review on #1517. The prior guard only fixed a non-map config *root*; a valid top-level map whose `githubCopilot` value is itself a scalar/null/sequence (`githubCopilot: false`, `null`, or a list) still made `setIn(['githubCopilot','cloudAgent'], ...)` throw, which init swallowed — so the explicit opt-in/out was never saved. Now the intermediate node is replaced with an empty map before descending, keeping the rest of the config and its comments intact. Regression covers all three reproduced cases (false/null/sequence). Full suite: 2770 pass; only the pre-existing unrelated failures remain. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(copilot): never throw persisting into an unparseable config Deeper pass on persistCopilotCloudOptIn (the function alfred flagged), driven by an exhaustive input-shape check. Two malformed inputs still threw at toString(): a multi-document YAML stream and a tab-indented (syntactically invalid) file. Such a file can't be edited without corrupting it, so persist now detects parse errors and leaves it untouched (no throw, no clobber) — it is already invalid, so readProjectConfig ignores it regardless. With this the function is throw-free across every shape exercised: empty, comment-only, scalar/sequence root, a non-map githubCopilot value, anchors, CRLF, BOM, and the two malformed cases (now skipped byte-identical). Regression added for the multi-document case. Touched suites: 314 pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
13e213e00f |
feat(tools): add Atlassian Rovo Dev CLI as a first-class tool (#1516)
* feat(tools): add Atlassian Rovo Dev CLI as a first-class tool Rovo Dev CLI loads project Agent Skills from `.rovodev/skills/<name>/SKILL.md` (Atlassian docs), the same SKILL.md format OpenSpec generates. It was usable only via the generic "Shared .agents skills" fallback; this makes it a named, selectable target in `openspec init`. Rovo has no slash-command surface, so it is registered as an adapterless skills-only tool (like CodeArts/ForgeCode/Hermes) — no command adapter. Closes #212 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(tools): reference Rovo skills by natural language, not dead slash commands Rovo Dev CLI has no slash-command surface — it matches skills automatically or by prompt, and `/skills` only manages them. The generated skills and the getting-started hint still advertised `/openspec-*` slash commands (18 references across the skill bodies plus the "Start your first change" hint), so every one was a dead command. Adds a natural-language skill-reference path for no-slash tools: `/opsx:<id>` now renders as "the openspec-<skill> skill" for rovodev, in both skill bodies and the init hint. Other tools are unchanged. - src/utils/command-references.ts: NATURAL_LANGUAGE_SKILL_TOOLS + usesNaturalLanguageSkillReferences(); getSkillReferenceTransformer returns the prose transformer for rovodev. - src/core/init.ts: phrase the skills-only hint as an instruction for no-slash tools ("ask Rovo Dev CLI to use the openspec-propose skill…"). - docs/supported-tools.md: correct the Rovo row (was "use skill-based /openspec-* invocations"). - tests: assert generated Rovo skills contain no /openspec-* or /opsx slash tokens, the hint advertises no dead command, and the transformer emits prose. Addresses alfred-openspec review on #1516. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
96a6548664 |
refactor(templates): share one apply instruction body across skill and command (#1515)
* refactor(templates): share one apply instruction body across skill and command The apply skill and command templates each carried a full ~150-line copy of the same instruction body, differing in exactly one line (the `contextFiles` note). Two near-identical copies invite silent drift. Author the body once in `getApplyInstructions(contextFilesNote)` and render it per surface, passing each surface's own note. The single intentional wording difference stays explicit as a named constant, and further per-surface parameters can be added here as the surfaces evolve — the skill and command remain distinct templates. Pure refactor: the generated skill and command output is byte-identical to before (SKILL.md and all parity hashes unchanged). Added a contract test that fails both if the shared body drifts between surfaces and if the intentional contextFiles difference is flattened away. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * refactor(templates): unify apply instruction body into one shared core Builds on the shared-core extraction: the apply skill and command still each carried a slightly different `contextFiles` note (skill spelled out example artifact sets, command said only "varies by schema"). That difference was long-standing accidental drift between the two copies, not an intentional surface distinction — the surfaces are meant to differ only in how they are invoked, which the generation transformers already handle downstream by rewriting `/opsx:<id>` tokens per surface. Resolve the drift by unifying on the more informative note, so both surfaces render one shared `getApplyInstructions()` body with no per-surface text. Skill output is unchanged; the command's contextFiles note gains the example artifact sets. Updated the contract test to assert both surfaces render the shared core (no silent template-level drift), and regenerated the command function hash accordingly. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
06b310bf57 |
fix(templates): restore intentional apply skill/command separation (#1514)
* fix(templates): restore intentional apply skill/command separation Revert the deduplication from #1153. Skills and commands are different ways to invoke the apply workflow: commands reference /opsx:*, while skills reference other skills by name and avoid /opsx: (a skill may be installed without the commands). Teams choose skills-only, commands-only, or both through profiles, so generating both is intentional, not drift. #1153 collapsed getApplyChangeSkillTemplate() and getOpsxApplyCommandTemplate() into one shared body and added a test asserting they are byte-identical, erasing four deliberate differences (change-name example, contextFiles note, blocked-state pointer, and completion hint). This restores the two separate templates and removes the identical-body assertion. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(templates): keep apply skill invocations transformable per target Address alfred's review on #1514. A plain revert of #1153 restored the skill template's bare `openspec-continue-change` prose and dropped the archive/input invocations. The generator only rewrites canonical `/opsx:<id>` tokens, so bare prose is dead text for skills-only targets: skills.sh, Codex, and Kimi lost valid continue/apply/archive invocations. Keep the skill and command templates split (no shared constant, no identical-body assertion — the design separation #1153 erased stays reverted), but author the skill's three invocation references as transformable `/opsx:*` tokens. The generator now emits the correct per-target skill invocation: `/openspec-continue-change` (default), `$openspec-continue-change` (Codex), `/skill:openspec-continue-change` (Kimi) — i.e. "invoked as skills," spelled for each tool. Regenerated the static SKILL.md and parity hashes, and added default/Codex/Kimi generation regressions that pin the apply skill's per-target invocations so this break can't recur silently. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
59bfb27a76 |
fix(codex): install skills in canonical agents directory (#1511)
* fix(codex): install skills in canonical agents directory * fix(codex): preserve shared agents compatibility * fix(codex): harden shared skill migration * fix(codex): preserve customized legacy skills * fix(codex): reject malformed generated versions |
||
|
|
02b124e6b6 |
fix(security): patch fast-uri, postcss, and brace-expansion advisories (#1510)
* fix(security): patch fast-uri, postcss, and brace-expansion advisories Resolve the two open Dependabot alerts plus a third high-severity advisory the repo's own audit surfaces but Dependabot had not filed, all via version-ranged pnpm overrides (they lapse once the upstream tree moves past them): - fast-uri 3.1.4 -> 3.1.5 (website): GHSA-7p8r-x3mc-p8w7, high. Host confusion via backslash authority introducer. Pulled in transitively by ajv@8.18.0; bounded to ^3.1.5 so it stays on the 3.x line ajv expects. - postcss 8.5.22 -> 8.5.25 (root): GHSA-fxqj-rqcc-2cmp, moderate. Arbitrary .map file read via attacker-controlled sourceMappingURL. Pulled in by vite (dev/test tooling). - brace-expansion 5.0.8 -> 5.0.9 (website): GHSA-rgw5-rvv9-x895, high. DoS via unbounded recursion. The existing override capped at >=5.0.8, and 5.0.8 is itself vulnerable under this newer advisory; the root already resolved to 5.0.9. Root and website audits are clean at --audit-level high (and any-severity for the website). Full test suite: 3662 passing. * harden(security): bound overrides, scope release perms, add website lockfile drift check, document archive TOCTOU intent Hardening pass over the security fixes, from a parallel review of the dependency, CI, archive, and adjacent-code surfaces. Each item is low-risk and verified; resolved dependency versions are unchanged. - deps: bound the three security overrides to their current major (brace-expansion ">=5.0.9 <6", postcss ">=8.5.23 <9"). A bare ">=X" pin would take a future major on the next lockfile regen without review; the website already models the caret-bounded idiom. - ci: scope release-prepare.yml permissions per job. The top-level block dropped "pull-requests: write"; only the "prepare" job (which opens the Version Packages PR) now holds it. The "beta" job only tags/releases and publishes via OIDC, so it inherits the narrower default (least privilege). - ci: add a "Website Lockfile Drift" job to security.yml. The website keeps its own lockfile and is never installed in CI, so a website override that stops resolving would go unnoticed and `pnpm audit` would scan a stale graph. A `pnpm install --frozen-lockfile --ignore-scripts --dir website` fails fast on that drift (root drift is already caught in ci.yml). - archive: add intent comments at the 7 js/file-system-race sites in src/core/archive.ts. The stat->read->re-stat pattern is a deliberate concurrent-change detector; the comments record why, so no future refactor (human or scanner-driven) collapses it to fd I/O and blinds the guard. Verified: 3662 tests pass, build clean, website build clean, root+website audits clean at --audit-level high, and the new frozen-lockfile check passes locally. * chore(nix): refresh pnpmDeps hash for the lockfile change The root pnpm-lock.yaml changed (postcss + brace-expansion overrides), which stales the fixed-output pnpmDeps hash and fails Nix Flake Validation. Repin to the value CI computed from the new lockfile. |
||
|
|
3d0701f871 |
fix(workflows): preserve nested spec paths (#1508)
* fix(workflows): preserve nested spec paths * fix(workflows): key conflicts by capability path * fix(workflows): preserve full paths in examples * fix(workflows): clarify nested path inputs * test(workflows): align parity hashes after rebase |