Commit Graph
283 Commits
Author SHA1 Message Date
openspec-release-bot[bot]andgithub-actions[bot] 94ca9c1eb1 Version Packages (#2005)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-09-30 22:39:18 +00:00
Clay GoodandClaude Opus 5.5 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>
2026-09-30 22:11:42 +00:00
JinandClaude Opus 5.5 cd4f9e4a5f fix(completion): restore .zshrc byte for byte on zsh uninstall (#2016)
removeZshrcConfig stripped every blank line at the top of .zshrc after
removing the OpenSpec block, so a file that started with blank lines
lost them in an install/uninstall round trip, even when the user had
moved the block further down. Drop only the separator line install
added, and only when the block sits at the top, as #1872 did for bash.

Closes #2015

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 21:22:12 +00:00
Clay GoodandClaude Opus 5.5 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>
2026-09-30 21:22:09 +00:00
c879d13d5f feat: add support for code studio AI-powered coding agent (#883)
* feat: add Code Studio tool integration

* feat: add Code Studio tool integration

* feat: add Code Studio tool integration

* feat: add Code Studio tool integration

---------

Co-authored-by: AshokkumarKaruppasamy <ashokkumar.karuppasamy@syncfusion.com>
Co-authored-by: Clay Good <hi@claygood.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 21:42:57 +00:00
e232080d09 feat(grok): add skills-only support for grok build (#1349)
* feat: add Grok Build skills-only support

* test(grok): cover skills-only installation and update lifecycle

---------

Co-authored-by: Clay Good <hi@claygood.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 21:42:53 +00:00
Clay GoodandClaude Opus 5 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>
2026-09-29 20:24:15 +00:00
d1642cb58c feat(easycode): add project skills and commands (#1352)
* feat: add Easy Code as a supported AI tool

- Register 'easycode' in AI_TOOLS (config.ts) with skillsDir '.easycode'
- Add EasycodeAdapter (easycode.ts): generates TOML commands at
  .easycode/commands/opsx/<id>.toml, matching Easy Code's native format
- Export easycodeAdapter from adapters/index.ts
- Register easycodeAdapter in CommandAdapterRegistry

Easy Code (https://easycode.ai) is a terminal-based AI coding assistant.
Its commands use TOML with a description field and a prompt multiline
literal string, distinct from the Markdown/YAML frontmatter format used
by most other tools.

Tested locally: `openspec init --tools easycode` generates 5 SKILL.md
files and 5 .toml command files in the expected directory structure.

* fix: robust TOML serialization for Easy Code adapter

Per code review: the original formatFile had unsafe manual escaping
that would corrupt output for descriptions containing backslashes or
control characters, and prompt bodies containing triple-single-quotes.

Changes:
- Add src/core/command-generation/toml.ts with two helpers:
    escapeTOMLBasicString  — escapes \, ", \n, \r, \t for TOML
                              basic strings (double-quoted)
    escapeTOMLMultilineString — escapes \ and \r, and breaks any
                              run of 3+ consecutive " (lookahead match)
                              for TOML basic multiline strings
- Switch prompt block from triple-single-quote literal string (''')
  to triple-double-quote basic multiline string ("""), which allows
  full escape sequence support and handles arbitrary body content
- Update easycode.ts to use both helpers

* fix: escape disallowed control characters in TOML helpers

Per code review: TOML basic strings forbid U+0000-U+0008, U+000B-U+000C,
U+000E-U+001F, and U+007F. Add escapeControlChars() helper that replaces
these with \uXXXX sequences, and apply it in both escapeTOMLBasicString
and escapeTOMLMultilineString after their named-escape passes.

* feat(easycode): harden project skills and command integration

---------

Co-authored-by: Trae <konghaifeng@cmcm.com>
Co-authored-by: Clay Good <hi@claygood.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 20:24:09 +00:00
a7f08b8a46 feat(tools): add GSD skills support (#1082)
Co-authored-by: Clay Good <hi@claygood.com>
Co-authored-by: Storm-Chaser <Storm-Chaser@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 20:24:04 +00:00
f197804a38 feat(nix): expose openspec as a consumable overlay (#1439)
* feat(nix): expose openspec as a consumable overlay

Refactor the flake so the package derivation is defined once, in
`overlays.default`, and every other output consumes it. Previously the
derivation lived inline in `packages.default`, so anyone who wanted
`openspec` in their own package set had to copy the derivation rather than
import it.

What changed:
- Add `overlays.default`, a standard `final: _prev:` overlay that exposes
  `pkgs.openspec`. The derivation is written against `final`, so downstream
  overlay composition and `overrideAttrs` behave as expected.
- Route `packages.{default,openspec}` through the overlay via a `pkgsFor`
  helper (`import nixpkgs { overlays = [ self.overlays.default ]; }`), so the
  flake's own package resolves exactly as a consumer's would. No more
  duplicated build definition.
- Refresh the nixpkgs pin in `flake.lock`.

`apps` and `devShells` are unchanged in behaviour.

Usage — a downstream flake:

    nixpkgs.overlays = [ openspec.overlays.default ];
    # -> pkgs.openspec

or devenv, via `devenv.yaml`:

    inputs:
      openspec:
        url: github:Fission-AI/OpenSpec
        overlays:
          - default

Assisted-by: Claude Opus 4.8 <noreply@anthropic.com>

* test(nix): cover downstream overlay composition and updater warnings

* fix(ci): compare Nix dependencies across multiple outputs

* chore: add Nix overlay changeset

---------

Co-authored-by: John Muchovej <jmuchovej@users.noreply.github.com>
Co-authored-by: Clay Good <hi@claygood.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 20:23:57 +00:00
ded99e27de feat(cli): show archived changes in list and view (#399)
* feat: show archived changes in list and view commands

Add --archived and --all flags to the list command to display archived changes.
The view dashboard now includes an "Archived Changes" section with a count in
the summary.

- list --archived: shows only archived changes
- list --all: shows both active and archived changes
- view: displays archived changes section in dashboard

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(list): reject malformed archive parents on Windows

* fix(list): preserve browsing of archived symbolic links

* fix(list,view): keep archived entries off nested findings and render past a file archive

An archived change that shares a name with an active namespace folder was
reported as nested. view now treats an archive path that is a file as no
archived changes, as it did before, instead of failing mid-dashboard.
The archive symlink test now uses a portable link and checks the link itself.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Co-authored-by: Clay Good <hi@claygood.com>
2026-09-29 20:23:52 +00:00
Clay GoodandClaude Opus 5.5 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>
2026-09-29 20:23:49 +00:00
Clay Good 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
2026-09-29 18:26:04 +00:00
3de7c72c26 feat(atomcode): add project skills and commands (#1211)
* feat: add AtomCode command adapter support

Add AtomCode as a supported tool with its own command adapter.
AtomCode is an open-source terminal AI coding assistant that uses
the same Agent Skills spec as Claude Code.

Changes:
- New adapter: src/core/command-generation/adapters/atomcode.ts
  - File path: .atomcode/commands/opsx-<id>.md
  - Frontmatter: description only
  - Command references transformed from colon to hyphen format
- Register adapter in index.ts, registry.ts, and config.ts
- Update docs (supported-tools.md, cli.md) with AtomCode entry
- Add 6 test cases for atomcodeAdapter
- Add changeset for version tracking

Closes #1210

Co-Authored-By: hu-qi, AtomCode (GLM-5.1) <huqi1024@gmail.com>

* docs(atomcode): correct parser compatibility comments

* test(atomcode): cover detection and registry registration

The adapter shipped without the two per-tool checks its siblings carry:
a detection test asserting `.atomcode` surfaces the tool from
getAvailableTools, and registry assertions that the adapter is reachable
by id. Also adds the missing AtomCode row to the docs-lab support matrix.

Verified non-vacuous: removing the registry registration and the
AI_TOOLS entry fails all four new assertions.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* test(atomcode): lock the literal name/args invariant

AtomCode's custom-command loader scans for `key:` and takes the rest of
the line verbatim, with no YAML unquoting, then matches `args` against
the exact strings "required"/"optional". A quoted `args: "optional"`
falls through to ArgsRequirement::None and silently drops every
argument, so this adapter cannot use the shared escapeYamlValue helper
the way its siblings do.

That made the deviation look like an oversight inviting a "consistency"
refactor. Document why it exists and add a case proving a description
that needs quoting does not drag name/args into quotes.

Also drops the literal `description:` assertion. It passed only because
no current description needs quoting; the first one containing ": "
would have failed it, looking like an adapter bug rather than a
description edit. The parsed-frontmatter toEqual already covers it.

Verified non-vacuous: routing all three fields through a quoting helper
fails 13 tests, including the new one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(atomcode): declare args: none for workflows that take no input

Every generated command shipped `args: optional`, including `onboard`,
which reads no invocation input. AtomCode treats the two differently at
the slash menu: `none` executes the command immediately, while
`optional` completes to `/name ` and waits for a second Enter. Onboard
therefore cost every AtomCode user an extra keystroke to start, and its
body carried a `**Provided arguments**:` line that was always empty.

Gate both on the `**Input**:` contract the workflow bodies already
declare, matching how the Command Code adapter decides the same thing.

Output for the other 11 workflows is byte-identical; only `onboard`
changes. Adds the same tripwire assertion Command Code carries, so a
future workflow cannot silently lose its arguments.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs(atomcode): describe the args contract in the changeset

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs(atomcode): keep updates in docs-lab

---------

Co-authored-by: Clay Good <hi@claygood.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-29 18:26:02 +00:00
297092cb25 feat(tools): add DeepSeek Harness support (#1672)
* feat(tools): add DeepSeek Harness support

* docs(cli): clarify dsh tool id shorthand

* docs(changeset): clarify dsh skill invocations vs command files

* fix(tools): harden deepseek harness integration

---------

Co-authored-by: Clay Good <hi@claygood.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-29 18:26:00 +00:00
c21d897261 feat(view): display workflow status in active changes (#807)
* feat(view): display workflow status in active changes

Active changes now show workflow artifact status below each entry,
indicating schema name and completion state of each artifact (done✓,
ready→, blocked). Powered by loadChangeContext and formatChangeStatus.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>

* chore(release): track dashboard workflow status

* fix(view): neutralize controls in workflow output

* docs(view): move workflow status docs to docs-lab

* docs(view): preserve store in status guidance

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Clay Good <hi@claygood.com>
2026-09-29 18:25:57 +00:00
070de01dfa feat(init): add Amp skills support (#420)
* proposal: add Amp skill support for OpenSpec workflows

Amp-Thread-ID: https://ampcode.com/threads/T-019b6a90-6107-755b-8087-942d9b5460ac

* feat(init): add Amp skills support for OpenSpec workflows

Add AmpSlashCommandConfigurator that generates Amp-native skill files
at .agents/skills/openspec-{proposal,apply,archive}/SKILL.md with YAML
frontmatter containing name and description fields.

- Register Amp in the native tool picker for init and update commands
- Include comprehensive test coverage for init and update scenarios
- Mark all add-amp-support tasks as complete

Amp-Thread-ID: https://ampcode.com/threads/T-019b6a90-6107-755b-8087-942d9b5460ac

* proposal: rename SlashCommandConfigurator to WorkflowConfigurator

Fix semantic mismatch with diverse tool terminology (skills, prompts,
commands). Old names kept as deprecated aliases for compatibility.

Amp-Thread-ID: https://ampcode.com/threads/T-019b6a90-6107-755b-8087-942d9b5460ac

* feat(init): add Amp skills support

---------

Co-authored-by: Jean du Plessis <jean@upbound.io>
Co-authored-by: Clay Good <hi@claygood.com>
2026-09-29 18:25:55 +00:00
5a360c2088 feat(veai): add skills-only support (#848)
* feat: Add support for Veai coding agent

* chore(changeset): track Veai support

* docs(changeset): clarify Veai user impact

---------

Co-authored-by: TabishB <tabishbidiwale@gmail.com>
Co-authored-by: Clay Good <hi@claygood.com>
2026-09-29 18:25:53 +00:00
3c3e6e3d42 feat: add GigaCode as a supported --tools target (#1961)
* feat: add GigaCode as a supported --tools target

GigaCode is Sber's CLI coding agent, a fork of Qwen Code that reuses
its config directory shape (.gigacode/ vs .qwen/) and file formats.
Register a gigacodeAdapter mirroring the qwen adapter (Markdown
commands with a description frontmatter field at
.gigacode/commands/opsx-<id>.md), wire it into the config tool list
and command-adapter registry, and document it in
docs/supported-tools.md, docs/cli.md and docs-lab/reference/supported-tools.md.

Verified: GigaCode's fork relationship to Qwen Code and its .gigacode
config directory are confirmed by the task's own primary-source brief;
I could not independently find a GigaCode-authored doc page that
enumerates its custom-command file format (Markdown vs TOML), so this
mirrors Qwen Code's current (post-deprecation) Markdown spec on the
stated fork/format-compatibility basis. Flagging this assumption for
review.

Generated with Claude Code (claude-sonnet-5); tested with the full
vitest suite (5879 tests passing across 199 files), pnpm build,
tsc --noEmit, and eslint, all green.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(gigacode): use current documentation sources

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-authored-by: Clay Good <hi@claygood.com>
2026-09-29 18:25:51 +00:00
Casey GollanandClay Good 817cdb64be fix(tools): identify Bob by product name (#1722)
* chore(openspec): add change for IBM Bob rename and skills-only

* feat: rename Bob Shell to IBM Bob and make skills-only

- Rename AI_TOOLS entry from 'Bob Shell' to 'IBM Bob' (name + successLabel)
- Remove bobAdapter from CommandAdapterRegistry and adapters/index.ts
- Add 'bob' to skills-invocable capability path in command-surface.ts
- Add cleanupLegacyBobCommandFiles() to migration.ts for .bob/commands/ cleanup
- Call cleanup from init.ts and update.ts after skills are generated
- Update docs/supported-tools.md: IBM Bob, mark commands as not generated
- Remove bobAdapter tests; update flat-invocation and all-adapters lists

* chore: delete dead bob command adapter file

* fix: address CodeRabbit review comments on Bob command cleanup

- Guard bobCommandsDir and each commandFile against symlink escape
  using areProjectArtifacts() before deletion
- Only remove files containing 'argument-hint:' frontmatter (OpenSpec
  marker), leaving user-authored files with matching names intact
- Run cleanupLegacyBobCommandFiles on the up-to-date early-return
  path in update.ts so stale .bob/commands/ files are cleaned even
  when no tools need a version update

* fix: scope argument-hint check to YAML frontmatter block only

* fix(tools): identify Bob by product name

* chore(changeset): track IBM Bob naming fix

* docs(changeset): clarify IBM Bob user impact

---------

Co-authored-by: Clay Good <hi@claygood.com>
2026-09-29 18:25:45 +00:00
88692b3bb4 fix(change-metadata): warn on unrecognized .openspec.yaml keys (#1925)
* fix(change-metadata): warn on unrecognized .openspec.yaml keys

Unknown keys such as skip_design were stripped with no signal, so status
still demanded design and validate --strict exited 0. Warn on the shared
status/validate/archive read path without rejecting the file.

Closes #1920

AI-assisted (Grok)

* fix(change-metadata): sanitize unknown keys before they are printed

A quoted YAML key can carry a terminal control sequence, and the warning
printed it as it was written. The listed keys now go through
sanitizeInline, which also flattens C1 controls from now on.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* test(change-metadata): cover sanitized archive warnings

* fix(change-metadata): keep warnings safe and structured

* fix(change-metadata): label key names as untrusted

* test(change-metadata): keep known keys in step with the schema

CHANGE_METADATA_KNOWN_KEYS is a hand-kept copy of ChangeMetadataSchema's
keys. A key added to the schema but not the list would warn on, and fail
validate --strict for, every change that uses it. Pin the two together.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Co-authored-by: Clay Good <hi@claygood.com>
2026-09-29 18:25:42 +00:00
47thandClay Good 9557b43aaf fix(init): install integrations with external stores (#1969)
* fix(init): install integrations with external stores

* Canonicalize pointer root and skip config writes in integrations-only mode

* test(init): cover external-store integration choices

* docs(init): document store-only integration setup

* fix(init): preserve pointer-repo planning files

---------

Co-authored-by: Clay Good <hi@claygood.com>
2026-09-29 18:25:36 +00:00
Ryan de Melo d28fb49c1c fix(show): include requirement and scenario names in JSON (#1972)
show --json described each requirement as its SHALL sentence and each
scenario as its bullets. The parser read both headers and dropped them,
so a JSON reader could not name a requirement the way archive matches it.

Requirements and scenarios now carry a name, normalized by the same
helpers archive and the MODIFIED scenario loss check use. The field is
additive and optional in the schema.

Closes #1971
2026-09-29 18:25:33 +00:00
Clay Good 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
2026-09-29 18:25:31 +00:00
Clay Good 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
2026-09-29 18:25:26 +00:00
Clay Good e70dcc7c82 fix(propose): guide capability naming (#1997)
* fix(propose): guide capability naming

* test(propose): cover published naming guidance
2026-09-29 18:25:24 +00:00
Clay GoodandClaude Opus 5.5 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>
2026-09-29 18:25:22 +00:00
42671df890 fix(config): name the offending rules item when a list is malformed (#1984)
* fix(config): name the offending rules item when a list is malformed

A rule item containing an unquoted ": " is valid-looking YAML but parses as
a mapping, so the artifact's whole rule set is dropped. The warning named only
the artifact, so the bad item had to be found by bisecting the list by hand.

Name every offending index with the shape YAML produced there, and point at the
quoting fix when a mapping is the cause. Parsing behavior is unchanged.

* test(config): cover remaining malformed rule shapes

---------

Co-authored-by: Yi-111-a <41823681+Yi-111-a@users.noreply.github.com>
Co-authored-by: Clay Good <hi@claygood.com>
2026-09-29 18:25:21 +00:00
Clay Goodand胥寅 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>
2026-09-28 21:46:37 +00:00
openspec-release-bot[bot]andgithub-actions[bot] db23097835 Version Packages (#1953)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-09-23 21:33:22 +00:00
Clay GoodandClaude Opus 5.5 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>
2026-09-23 20:56:54 +00:00
336414665f fix(verify): stop reporting removed requirements as missing (#1962)
* fix(verify): stop reporting removed requirements as missing

Verify walked every "### Requirement:" in the delta specs and looked for
an implementation of each one, whatever section it sat under. A REMOVED
requirement that the change had removed correctly came back as CRITICAL
"Requirement not found", with a recommendation to implement it. An agent
that follows the report puts back the behavior the change just deleted.

Verify now notes the delta section of each requirement first. ADDED and
MODIFIED keep the existing checks. REMOVED is checked the other way
round: finding nothing is the expected result, and it is only critical
while the behavior is still in the code. RENAMED only changes a name, so
the old name is not reported as missing. Scenario coverage skips removed
requirements, since there is nothing left to cover.

Archive and sync already handle each section on its own terms; verify
was the one step in the loop that did not.

Closes #1959

* docs(specs): scope the general verify scenarios to ADDED and MODIFIED

The Spec coverage, Requirement implementation mapping and Scenario
coverage scenarios still told the verifier to check every requirement
in the delta specs, which contradicts the Removed requirement scenario
added in the previous commit. A verifier following them would repeat
the #1959 failure.

* fix(verify): report a removal-only change as ready when nothing remains

With #1732 merged, a change whose delta specs only remove or rename
requirements left Requirement Implementation Mapping and Scenario
Coverage with nothing to check. The "no usable requirements" rule then
marked them not verified, so verify never reported the change ready,
the exact case #1959 describes. Those two checks are now not applicable
when the readable delta specs hold REMOVED or RENAMED requirements and
no ADDED or MODIFIED ones. An empty or unparseable delta still marks
them not verified.

Keyword matches in openspec/ artifacts, docs, or code that serves only
the Migration note or an ADDED requirement are no longer evidence by
themselves that a removed requirement is still implemented; a code path
that still delivers the removed behavior is reported even when shared.
The summary counts removals separately from covered requirements.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(verify): check a renamed requirement's behavior against its baseline

A RENAMED entry only told verify not to report the FROM name as
missing, and a rename-only change marked the correctness checks not
applicable. Nothing checked that the renamed requirement's behavior was
still implemented, so verify could report readiness unchecked.

Spec Coverage now reads the baseline requirement from the main spec
(under the FROM name, or the TO name once synced) and checks that its
behavior is still implemented, without requiring code symbols to be
renamed. A missing behavior is CRITICAL "Renamed requirement not
found"; an unreadable baseline marks the entry not verified. A TO name
that also appears under MODIFIED is still checked there.

Regression tests cover the skill template, the command template, and
the committed skills/ mirror.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Clay Good <hi@claygood.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-23 19:28:55 +00:00
072de6bc39 fix(verify): do not report unverified dimensions as passing (#1732)
* fix(verify): do not report unverified dimensions as passing

Step 5 gates task and spec coverage on `contextFiles.tasks` and
`contextFiles.specs`. `contextFiles` is an artifact-id map and artifact
ids come from the active schema, so on a schema that defines neither, both
branches are no-ops: nothing is checked, no issues are raised, and step 8
concludes "All checks passed. Ready for archive."

The Graceful Degradation guardrail already asks the agent to note skipped
checks, but nothing stopped the all-clear verdict. Mark an unchecked
dimension `Not verified` in the scorecard and require the final assessment
to name it.

* fix(verify): map skipped checks to report outcomes

* fix(verify): retain no-task and skipped-check context

* fix(verify): harden evidence gaps and final assessments

* fix(verify): preserve optional workflows and task artifact fallback

* fix(apply): resolve tracked task globs by schema path

* fix(verify): preserve unavailable task evidence

* fix(verify): distinguish untracked tasks from missing evidence

* docs(apply): document tracked globs and JSON evidence

* test(parity): regenerate hashes after merging #1940

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* test(parity): restore the #1837 regression tests dropped in the merge

The earlier conflict resolution took our whole side of the parity file,
which discarded the two threshold tests main gained in #1940. Take main's
file verbatim and regenerate the hashes instead.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* test(parity): regenerate hashes after merging #1955

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* test(parity): regenerate hashes after merging #1795 and #1926

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* test(parity): regenerate hashes after merging #1731

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Clay Good <hi@claygood.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-23 17:38:11 +00:00
d6bdef6577 fix(workflows): stop reading schema from list output (#1731)
* fix(workflows): resolve the change picker's schema label from status

The update and continue templates tell the agent to read a `schema` field
from `openspec list --json` and fall back to "spec-driven" when it is
absent. `list --json` returns only `name`, `completedTasks`,
`totalTasks`, `lastModified`, and `status` (docs/agent-contract.md 4.1),
so the field is never present and the fallback fires every time: a change on
a custom schema is shown to the user as `spec-driven`.

Make the schema line optional and, when shown, resolve it from
`openspec status --change "<name>" --json` (`schemaName`).

* fix(workflows): align list prompts with JSON fields

* test(workflows): verify list and status schema contracts

* fix(workflows): keep bulk archive sync available in custom profiles

* test(parity): regenerate hashes after merging #1940

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* test(parity): restore the #1837 regression tests dropped in the merge

The earlier conflict resolution took our whole side of the parity file,
which discarded the two threshold tests main gained in #1940. Take main's
file verbatim and regenerate the hashes instead.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* test(parity): regenerate hashes after merging #1955

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* test(parity): regenerate hashes after merging #1733

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* test(archive-progress): resolve optional-workflow blocks before generating

This change makes the bulk-archive surfaces conditional on the sync
workflow being installed. #1795's task-progress test built them from the
raw templates, which leaves the [[opsx:if-workflow ...]] markers in the
text and makes skill generation throw.

Build both surfaces through getSkillTemplates/getCommandTemplates, which
resolve the blocks against an installed set, and name sync in that set so
the assertions keep testing the wording they were written for.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Clay Good <hi@claygood.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-23 16:59:45 +00:00
fb1b87613b fix(archive): use schema-aware task progress in workflows (#1795)
* fix(archive): use schema-aware task progress in workflows

* test(archive): verify task lookup follows the selected store

* fix(archive): reject invalid task progress in workflows

* test(parity): regenerate hashes after merging #1940

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* test(parity): restore the #1837 regression tests dropped in the merge

The earlier conflict resolution took our whole side of the parity file,
which discarded the two threshold tests main gained in #1940. Take main's
file verbatim and regenerate the hashes instead.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* test(parity): regenerate hashes after merging #1955

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Clay Good <hi@claygood.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-23 16:25:32 +00:00
f2812f6d18 fix(archive): copy without staging when Windows EPERM blocks rename (#1926)
* fix(archive): copy without staging when Windows EPERM blocks rename

fs.rename of a non-leaf change directory fails with EPERM on Windows
when a watcher holds a handle. The fallback required a staging rename
of the same directory, so it never ran: specs were rolled back after
printing success, and a newly created capability was left as an empty
folder git cannot see. Copy from the original source when staging also
fails with EPERM/EXDEV, and prune empty capability dirs on rollback.

Closes #1895

AI-assisted (Grok)

* fix(archive): bound the unstaged cleanup to what it verified

Addresses both review findings on the copy fallback.

The staging rename was what claimed the source before it was deleted.
Falling back without it means copy-then-remove now runs against the live
change directory, which the archive claim does not cover, and a recursive
remove deletes whatever is there at that moment - including a file
written after the final fingerprint, which never reached the destination.

Cleanup now removes a named set: the entries listed after the last
verification, deepest first. A later arrival is not in that set, so it is
never deleted, and the rmdir of its parent fails with ENOTEMPTY, which
the caller already reports as a retained destination. The move fails
loudly rather than completing with data missing.

Rollback of a created spec pruned the capability directory unconditionally,
which also removed one the user already had, along with its mode and ACLs.
The snapshot now records whether that parent existed, and only a directory
this write created is pruned.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(archive): close the listing window and the nested prune boundary

Both follow-ups from CodeRabbit's second pass, and both are right.

The removal listing was taken after the final fingerprint, which left the
window it was meant to close: a file arriving between the fingerprint and
the listing landed in the set and was deleted, having never reached the
destination. Listing before the verification makes the two orderings
exhaustive - an arrival either changes the fingerprint and aborts the
move, or is absent from the set and survives. The one case this cannot
cover, an edit to an already-listed file, is now stated in the comment.

`parentExisted` only described the target's direct parent, so a nested
capability id whose intermediate directory already existed still lost it:
the prune walked to the specs root. The snapshot now records the deepest
pre-existing ancestor and passes it as the prune boundary, which
pruneEmptyDirs never removes. That one mechanism covers the flat case too.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(archive): claim each entry before removing it in the unstaged fallback

An editor could rewrite an already-verified file between the final
fingerprint and its removal. The destination held the older bytes, the
newer ones were deleted with the source, and archive reported success.

Cleanup now renames each entry to a private claim name before reading it.
rename is atomic, so a rewrite that lands after the claim creates a new
file at the original path, which is not in the verified set, is never
deleted, and makes the parent rmdir fail. A rewrite that lands first is
caught by comparing the claimed entry against the copy, which puts the
file back and abandons the move with both trees intact.

Symlinks are compared by their target rather than by reading them, since
a link to a directory is not a directory entry and reading one is EISDIR.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(archive): draw the cleanup claim suffix per move

A fixed '.openspec-claim' suffix collided with a source file that
legitimately ends in it: claiming 'collision' renamed it over a real
'collision.openspec-claim', and that file's own turn then failed with
ENOENT after part of the live source had already been removed. A valid
tree could not archive, and its source was damaged for nothing.

The suffix is now drawn per move and checked against the entries being
removed, so no claim of one entry can land on another.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Clay Good <hi@claygood.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-23 16:11:13 +00:00
72fbe4c904 fix(update): close the dead end for partially populated glob artifacts (#1733)
* fix(update): close the dead end for partially populated glob artifacts

`artifactOutputExists` returns true as soon as a single file matches a glob
`generates`, so a `specs/**/*.md` artifact is `done` after the first
delta spec. `/opsx:continue` selects only `ready` artifacts and never
revisits it.

The update templates forbid creating new files under a glob artifact and
point the user to `/opsx:continue` instead, which cannot act on it. A
capability spec the coherence review finds missing therefore has no
supported way to be created.

Allow update to write that file: concrete path only, rules fetched from
`openspec instructions`, and the same confirm-before-write rule as every
other revision. Creating an artifact that has no files at all stays out of
scope - that one is genuinely `/opsx:continue`'s job.

* fix(update): tighten glob gap write guardrails

* fix(update): narrow continue handoff to empty artifacts

* fix(update): harden glob gap creation guidance

* fix(update): preserve creation safeguards for glob companions

* fix(update): integrate glob guidance with current workflows

* docs(skills): note update's glob companion-file exception

docs-lab/reference/skills.md said update creates nothing new, which this
change makes false for glob artifacts.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* test(parity): restore the #1837 regression tests dropped in the merge

The earlier conflict resolution took our whole side of the parity file,
which discarded the two threshold tests main gained in #1940. Take main's
file verbatim and regenerate the hashes instead.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* test(explore-docs): allow the update guidance line that says "no files yet"

main's #1833 guard flags any docs line containing "no files". The new
/opsx:update guidance describes which artifacts update leaves to
continue, not what explore writes, so allow that exact line.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Clay Good <hi@claygood.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-23 15:55:52 +00:00
Clay GoodandClaude Opus 5 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>
2026-09-23 15:04:37 +00:00
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>
2026-09-23 14:30:24 +00:00
8826c0c4a1 fix(validate): require marker punctuation after a leading TBD/TODO (#1912)
* fix(validate): require marker punctuation after a leading TBD/TODO

Closes #1897

* fix(validate): keep a shouted TODO a placeholder marker

Requiring marker punctuation after a leading TBD/TODO fixed the Spanish
and Portuguese false positive, but it also stopped reporting the plainest
unwritten Purpose there is: `TODO write this once the capability settles
down.`

Case is what actually separates the marker from the word. In capitals it
is the marker whatever follows it. In any other case it is a marker only
when punctuation or the end of the line says so, which is how the
lowercase forms an agent leaves behind are written (`todo - `, `tbd.`)
and is not how a Spanish sentence opens.

Covers `todo el ...` in lowercase too, which the capitals-only reading of
the original fix would have reported.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs(changeset): drop the trailing space inside a code span

markdownlint MD038. Changeset text only; no behaviour change.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Clay Good <hi@claygood.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-23 14:30:07 +00:00
fd56e12c9e fix(artifact-graph): support brace expansion and extglob output patterns (#1885)
* fix(artifact-graph): support brace expansion and extglob output patterns (#1854)

* fix(artifact-graph): preserve literal output filenames

* fix(artifact-graph): confine expanded brace patterns

* test(artifact-graph): cover later brace ranges and specify glob contract

* test(artifact-graph): resolve brace globs with Windows separators

---------

Co-authored-by: 胥寅 <xuuyin@dingtalk.com>
Co-authored-by: Clay Good <hi@claygood.com>
2026-09-22 18:09:53 +00:00
Clay Good 0b5ce44b55 fix(templates): align approval thresholds (#1940)
* fix(templates): align approval thresholds

* fix(onboard): preserve implementation choice

* test(onboard): reject premature implementation prompt
2026-09-22 17:30:19 +00:00
Javier GomezandClay Good 5b55263775 fix(init): clarify Codex desktop skill usage (#1744)
* fix(init): clarify Codex desktop skill usage

* fix(init): clarify Codex desktop skill usage

* test(init): cover Codex hints across delivery modes

---------

Co-authored-by: Clay Good <hi@claygood.com>
2026-09-22 17:29:07 +00:00
Ryan de MeloandClay Good a5ceea32cf fix(validate): report what a MODIFIED block adds, not only what it drops (#1809)
* fix(validate): report what a MODIFIED block adds, not only what it drops

When the scenario-loss guard fires it names the scenarios the block omits,
which is the whole message. A reader's next question is what the block put
there instead, and answering it separates the two cases the guard cannot:
a block that omits two names and introduces two is shaped like a rename,
one that omits two and introduces none is shaped like a truncation. Today
that costs opening both files.

Add the counts and the introduced names to the message. This decides
nothing. Intent is not recoverable from structure, the guard fires exactly
as before, and the exit code is unchanged.

The comparison already walked one direction, so the other is the same pass
run the other way. Both directions now come from diffScenarioNames, and
both commands print one shared sentence, so archive and validate cannot
drift on what they report any more than they can on what they catch.
findMissingCurrentScenarios stays as its missing half.

Also names the antecedent in validate's fix instruction, which became
ambiguous once a second list of scenarios appeared before it.

* chore(changeset): track the scenario balance in the loss guard message

* fix(validate): clarify scenario balance diagnostic

* test(validate): cover one-to-two scenario replacement

---------

Co-authored-by: Clay Good <hi@claygood.com>
2026-09-22 17:29:05 +00:00
d3d770736f fix: release archive lock on Windows (#1769)
* fix: release archive lock on Windows

* test(archive): make the Windows claim-release regression actually fail

The new test compared the lstat target against the temp-dir claim path
verbatim. The command stats the resolved real path, so on macOS
(/var -> /private/var) the comparison never matched, `dev: 0n` was never
injected, and the test only asserted that an ordinary archive releases its
claim — which already passed before the fix. Verified: it passed with the
source change reverted.

Match the claim by file name instead, and count the interceptions so the
test fails loudly if the mock ever goes inert again rather than silently
passing. With the source change reverted the test now fails as intended.

Also add the missing changeset for the user-visible fix.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* test(archive): keep replaced claims with absent device ids

* test(archive): retain claim when zero-device path identity changes

---------

Co-authored-by: Clay Good <hi@claygood.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 17:29:03 +00:00
Clay Good 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
2026-09-22 17:28:51 +00:00
openspec-release-bot[bot]andgithub-actions[bot] 634c557bd0 Version Packages (#1896)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-09-17 01:01:33 +00:00
Clay GoodandClaude Opus 5 eb03b9e933 chore(changeset): track #1835 security hardening and #1785 nix completions (#1902)
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-17 00:39:28 +00:00
Clay GoodandClaude Opus 5 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>
2026-09-17 00:07:26 +00:00
Clay GoodandClaude Opus 5 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>
2026-09-16 23:34:57 +00:00