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> C
Clay Good committed
5f5914e7f7a817262c7564ac92694db833564978
Parent: 9827762
Committed by GitHub <noreply@github.com>
on 9/16/2026, 11:34:57 PM