From de970e6efa0117b4b2809ecc7d0e0f3f8b98c05a Mon Sep 17 00:00:00 2001 From: Clay Good Date: Fri, 4 Sep 2026 12:03:02 -0500 Subject: [PATCH 01/15] 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 --- skills/openspec-apply-change/SKILL.md | 2 + skills/openspec-archive-change/SKILL.md | 4 +- skills/openspec-bulk-archive-change/SKILL.md | 4 +- skills/openspec-continue-change/SKILL.md | 2 + skills/openspec-explore/SKILL.md | 4 +- skills/openspec-ff-change/SKILL.md | 2 + skills/openspec-new-change/SKILL.md | 2 + skills/openspec-onboard/SKILL.md | 2 + skills/openspec-propose/SKILL.md | 4 +- skills/openspec-sync-specs/SKILL.md | 4 +- skills/openspec-update-change/SKILL.md | 2 + skills/openspec-verify-change/SKILL.md | 4 +- src/core/templates/workflows/apply-change.ts | 3 + .../templates/workflows/archive-change.ts | 7 +- .../workflows/bulk-archive-change.ts | 7 +- .../templates/workflows/continue-change.ts | 5 + src/core/templates/workflows/explore.ts | 7 +- src/core/templates/workflows/ff-change.ts | 5 + src/core/templates/workflows/new-change.ts | 5 + src/core/templates/workflows/onboard.ts | 3 + src/core/templates/workflows/project-root.ts | 14 +++ src/core/templates/workflows/propose.ts | 7 +- src/core/templates/workflows/sync-specs.ts | 7 +- src/core/templates/workflows/update-change.ts | 5 + src/core/templates/workflows/verify-change.ts | 7 +- .../core/templates/project-root-guard.test.ts | 93 +++++++++++++++++++ .../templates/skill-templates-parity.test.ts | 72 +++++++------- 27 files changed, 235 insertions(+), 48 deletions(-) create mode 100644 src/core/templates/workflows/project-root.ts create mode 100644 test/core/templates/project-root-guard.test.ts diff --git a/skills/openspec-apply-change/SKILL.md b/skills/openspec-apply-change/SKILL.md index 098f63fecb..b122feb305 100644 --- a/skills/openspec-apply-change/SKILL.md +++ b/skills/openspec-apply-change/SKILL.md @@ -13,6 +13,8 @@ Implement tasks from an OpenSpec change. **Store selection:** If the user names a store (a store is a standalone OpenSpec repo registered on this machine) or the work lives in one, run `openspec store list --json` to discover registered store ids, then pass `--store ` on the commands that read or write specs and changes (`new change`, `status`, `instructions`, `list`, `show`, `validate`, `archive`, `doctor`, `context`, `schemas`, `view`). Once selected, treat `--store ` as sticky for the rest of the workflow. Every unscoped example of those commands below is shorthand: before running it, append the flag. For example, run `openspec status --change "" --json --store ""`, not the unscoped form shown below. Other commands do not take the flag. Hints printed by commands already carry the flag; keep it on follow-ups. Without a store, commands act on the nearest local `openspec/` root. +**Project check:** These steps expect a project that already uses OpenSpec. Before the first step that writes anything (`new change`, `archive`, `sync specs`, or authoring an artifact file), confirm the project has a root: run `openspec status --json` and read `root.source`. Any value other than `implicit` means the project is set up; `"source": "implicit"` means it is not - there is no `openspec/` directory here, and the next write would create one. An error saying `No OpenSpec root found` means the same thing. In that case stop before writing and ask the user how to proceed: set this project up (`openspec init`), target a store they already have (`--store `), or drop OpenSpec for this request and help them directly. Wait for their answer. Do not run `openspec init` on your own, do not hand-create `openspec/` files, and do not let a command create the root as a side effect. + **Input**: Optionally specify a change name (e.g., `/openspec-apply-change add-auth`). If omitted, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes. **Steps** diff --git a/skills/openspec-archive-change/SKILL.md b/skills/openspec-archive-change/SKILL.md index 5f34ed53a7..5128710111 100644 --- a/skills/openspec-archive-change/SKILL.md +++ b/skills/openspec-archive-change/SKILL.md @@ -1,6 +1,6 @@ --- name: openspec-archive-change -description: Archive a completed change in the experimental workflow. Use when the user wants to finalize and archive a change after implementation is complete. +description: Archive a completed OpenSpec change in the experimental workflow. Use when the user wants to finalize and archive a change after implementation is complete. allowed-tools: Bash(openspec:*) license: MIT compatibility: Requires openspec CLI. @@ -13,6 +13,8 @@ Archive a completed change in the experimental workflow. **Store selection:** If the user names a store (a store is a standalone OpenSpec repo registered on this machine) or the work lives in one, run `openspec store list --json` to discover registered store ids, then pass `--store ` on the commands that read or write specs and changes (`new change`, `status`, `instructions`, `list`, `show`, `validate`, `archive`, `doctor`, `context`, `schemas`, `view`). Once selected, treat `--store ` as sticky for the rest of the workflow. Every unscoped example of those commands below is shorthand: before running it, append the flag. For example, run `openspec status --change "" --json --store ""`, not the unscoped form shown below. Other commands do not take the flag. Hints printed by commands already carry the flag; keep it on follow-ups. Without a store, commands act on the nearest local `openspec/` root. +**Project check:** These steps expect a project that already uses OpenSpec. Before the first step that writes anything (`new change`, `archive`, `sync specs`, or authoring an artifact file), confirm the project has a root: run `openspec status --json` and read `root.source`. Any value other than `implicit` means the project is set up; `"source": "implicit"` means it is not - there is no `openspec/` directory here, and the next write would create one. An error saying `No OpenSpec root found` means the same thing. In that case stop before writing and ask the user how to proceed: set this project up (`openspec init`), target a store they already have (`--store `), or drop OpenSpec for this request and help them directly. Wait for their answer. Do not run `openspec init` on your own, do not hand-create `openspec/` files, and do not let a command create the root as a side effect. + `` is the spec directory relative to `specs/` (for example, `user-auth` or `identity/user-auth`). Preserve the full path from each delta spec when resolving its main spec. **Input**: Optionally specify a change name. If omitted, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes. diff --git a/skills/openspec-bulk-archive-change/SKILL.md b/skills/openspec-bulk-archive-change/SKILL.md index 252e1dd155..d2b529f3b9 100644 --- a/skills/openspec-bulk-archive-change/SKILL.md +++ b/skills/openspec-bulk-archive-change/SKILL.md @@ -1,6 +1,6 @@ --- name: openspec-bulk-archive-change -description: Archive multiple completed changes at once. Use when archiving several parallel changes. +description: Archive multiple completed OpenSpec changes at once. Use when archiving several parallel changes. allowed-tools: Bash(openspec:*) license: MIT compatibility: Requires openspec CLI. @@ -15,6 +15,8 @@ This skill allows you to batch-archive changes, handling spec conflicts intellig **Store selection:** If the user names a store (a store is a standalone OpenSpec repo registered on this machine) or the work lives in one, run `openspec store list --json` to discover registered store ids, then pass `--store ` on the commands that read or write specs and changes (`new change`, `status`, `instructions`, `list`, `show`, `validate`, `archive`, `doctor`, `context`, `schemas`, `view`). Once selected, treat `--store ` as sticky for the rest of the workflow. Every unscoped example of those commands below is shorthand: before running it, append the flag. For example, run `openspec status --change "" --json --store ""`, not the unscoped form shown below. Other commands do not take the flag. Hints printed by commands already carry the flag; keep it on follow-ups. Without a store, commands act on the nearest local `openspec/` root. +**Project check:** These steps expect a project that already uses OpenSpec. Before the first step that writes anything (`new change`, `archive`, `sync specs`, or authoring an artifact file), confirm the project has a root: run `openspec status --json` and read `root.source`. Any value other than `implicit` means the project is set up; `"source": "implicit"` means it is not - there is no `openspec/` directory here, and the next write would create one. An error saying `No OpenSpec root found` means the same thing. In that case stop before writing and ask the user how to proceed: set this project up (`openspec init`), target a store they already have (`--store `), or drop OpenSpec for this request and help them directly. Wait for their answer. Do not run `openspec init` on your own, do not hand-create `openspec/` files, and do not let a command create the root as a side effect. + `` is the spec directory relative to `specs/` (for example, `user-auth` or `identity/user-auth`). Preserve the full path from each delta spec when resolving its main spec. **Input**: None required (prompts for selection) diff --git a/skills/openspec-continue-change/SKILL.md b/skills/openspec-continue-change/SKILL.md index 5991b06891..92e91c4005 100644 --- a/skills/openspec-continue-change/SKILL.md +++ b/skills/openspec-continue-change/SKILL.md @@ -13,6 +13,8 @@ Continue working on a change by creating the next artifact. **Store selection:** If the user names a store (a store is a standalone OpenSpec repo registered on this machine) or the work lives in one, run `openspec store list --json` to discover registered store ids, then pass `--store ` on the commands that read or write specs and changes (`new change`, `status`, `instructions`, `list`, `show`, `validate`, `archive`, `doctor`, `context`, `schemas`, `view`). Once selected, treat `--store ` as sticky for the rest of the workflow. Every unscoped example of those commands below is shorthand: before running it, append the flag. For example, run `openspec status --change "" --json --store ""`, not the unscoped form shown below. Other commands do not take the flag. Hints printed by commands already carry the flag; keep it on follow-ups. Without a store, commands act on the nearest local `openspec/` root. +**Project check:** These steps expect a project that already uses OpenSpec. Before the first step that writes anything (`new change`, `archive`, `sync specs`, or authoring an artifact file), confirm the project has a root: run `openspec status --json` and read `root.source`. Any value other than `implicit` means the project is set up; `"source": "implicit"` means it is not - there is no `openspec/` directory here, and the next write would create one. An error saying `No OpenSpec root found` means the same thing. In that case stop before writing and ask the user how to proceed: set this project up (`openspec init`), target a store they already have (`--store `), or drop OpenSpec for this request and help them directly. Wait for their answer. Do not run `openspec init` on your own, do not hand-create `openspec/` files, and do not let a command create the root as a side effect. + **Input**: Optionally specify a change name. If omitted, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes. **Steps** diff --git a/skills/openspec-explore/SKILL.md b/skills/openspec-explore/SKILL.md index b597796fb1..1bc51e6af7 100644 --- a/skills/openspec-explore/SKILL.md +++ b/skills/openspec-explore/SKILL.md @@ -1,6 +1,6 @@ --- name: openspec-explore -description: Enter explore mode - a thinking partner for exploring ideas, investigating problems, and clarifying requirements. Use when the user wants to think through something before or during a change. +description: Enter OpenSpec explore mode - a thinking partner for exploring ideas, investigating problems, and clarifying requirements in a project that uses OpenSpec. Use when the user wants to think through something before or during an OpenSpec change. allowed-tools: Bash(openspec:*) license: MIT compatibility: Requires openspec CLI. @@ -17,6 +17,8 @@ Enter explore mode. Think deeply. Visualize freely. Follow the conversation wher **Store selection:** If the user names a store (a store is a standalone OpenSpec repo registered on this machine) or the work lives in one, run `openspec store list --json` to discover registered store ids, then pass `--store ` on the commands that read or write specs and changes (`new change`, `status`, `instructions`, `list`, `show`, `validate`, `archive`, `doctor`, `context`, `schemas`, `view`). Once selected, treat `--store ` as sticky for the rest of the workflow. Every unscoped example of those commands below is shorthand: before running it, append the flag. For example, run `openspec status --change "" --json --store ""`, not the unscoped form shown below. Other commands do not take the flag. Hints printed by commands already carry the flag; keep it on follow-ups. Without a store, commands act on the nearest local `openspec/` root. +**Project check:** These steps expect a project that already uses OpenSpec. Before the first step that writes anything (`new change`, `archive`, `sync specs`, or authoring an artifact file), confirm the project has a root: run `openspec status --json` and read `root.source`. Any value other than `implicit` means the project is set up; `"source": "implicit"` means it is not - there is no `openspec/` directory here, and the next write would create one. An error saying `No OpenSpec root found` means the same thing. In that case stop before writing and ask the user how to proceed: set this project up (`openspec init`), target a store they already have (`--store `), or drop OpenSpec for this request and help them directly. Wait for their answer. Do not run `openspec init` on your own, do not hand-create `openspec/` files, and do not let a command create the root as a side effect. + --- ## The Stance diff --git a/skills/openspec-ff-change/SKILL.md b/skills/openspec-ff-change/SKILL.md index 72a95620bb..28162c1a7d 100644 --- a/skills/openspec-ff-change/SKILL.md +++ b/skills/openspec-ff-change/SKILL.md @@ -13,6 +13,8 @@ Fast-forward through artifact creation - generate everything needed to start imp **Store selection:** If the user names a store (a store is a standalone OpenSpec repo registered on this machine) or the work lives in one, run `openspec store list --json` to discover registered store ids, then pass `--store ` on the commands that read or write specs and changes (`new change`, `status`, `instructions`, `list`, `show`, `validate`, `archive`, `doctor`, `context`, `schemas`, `view`). Once selected, treat `--store ` as sticky for the rest of the workflow. Every unscoped example of those commands below is shorthand: before running it, append the flag. For example, run `openspec status --change "" --json --store ""`, not the unscoped form shown below. Other commands do not take the flag. Hints printed by commands already carry the flag; keep it on follow-ups. Without a store, commands act on the nearest local `openspec/` root. +**Project check:** These steps expect a project that already uses OpenSpec. Before the first step that writes anything (`new change`, `archive`, `sync specs`, or authoring an artifact file), confirm the project has a root: run `openspec status --json` and read `root.source`. Any value other than `implicit` means the project is set up; `"source": "implicit"` means it is not - there is no `openspec/` directory here, and the next write would create one. An error saying `No OpenSpec root found` means the same thing. In that case stop before writing and ask the user how to proceed: set this project up (`openspec init`), target a store they already have (`--store `), or drop OpenSpec for this request and help them directly. Wait for their answer. Do not run `openspec init` on your own, do not hand-create `openspec/` files, and do not let a command create the root as a side effect. + **Input**: The user's request should include a change name (kebab-case) OR a description of what they want to build. **Steps** diff --git a/skills/openspec-new-change/SKILL.md b/skills/openspec-new-change/SKILL.md index 9aea11d391..81221cc8f4 100644 --- a/skills/openspec-new-change/SKILL.md +++ b/skills/openspec-new-change/SKILL.md @@ -13,6 +13,8 @@ Start a new change using the experimental artifact-driven approach. **Store selection:** If the user names a store (a store is a standalone OpenSpec repo registered on this machine) or the work lives in one, run `openspec store list --json` to discover registered store ids, then pass `--store ` on the commands that read or write specs and changes (`new change`, `status`, `instructions`, `list`, `show`, `validate`, `archive`, `doctor`, `context`, `schemas`, `view`). Once selected, treat `--store ` as sticky for the rest of the workflow. Every unscoped example of those commands below is shorthand: before running it, append the flag. For example, run `openspec status --change "" --json --store ""`, not the unscoped form shown below. Other commands do not take the flag. Hints printed by commands already carry the flag; keep it on follow-ups. Without a store, commands act on the nearest local `openspec/` root. +**Project check:** These steps expect a project that already uses OpenSpec. Before the first step that writes anything (`new change`, `archive`, `sync specs`, or authoring an artifact file), confirm the project has a root: run `openspec status --json` and read `root.source`. Any value other than `implicit` means the project is set up; `"source": "implicit"` means it is not - there is no `openspec/` directory here, and the next write would create one. An error saying `No OpenSpec root found` means the same thing. In that case stop before writing and ask the user how to proceed: set this project up (`openspec init`), target a store they already have (`--store `), or drop OpenSpec for this request and help them directly. Wait for their answer. Do not run `openspec init` on your own, do not hand-create `openspec/` files, and do not let a command create the root as a side effect. + **Input**: The user's request should include a change name (kebab-case) OR a description of what they want to build. **Steps** diff --git a/skills/openspec-onboard/SKILL.md b/skills/openspec-onboard/SKILL.md index fb3f13bec7..e46a4e0b9c 100644 --- a/skills/openspec-onboard/SKILL.md +++ b/skills/openspec-onboard/SKILL.md @@ -13,6 +13,8 @@ Guide the user through their first complete OpenSpec workflow cycle. This is a t **Store selection:** If the user names a store (a store is a standalone OpenSpec repo registered on this machine) or the work lives in one, run `openspec store list --json` to discover registered store ids, then pass `--store ` on the commands that read or write specs and changes (`new change`, `status`, `instructions`, `list`, `show`, `validate`, `archive`, `doctor`, `context`, `schemas`, `view`). Once selected, treat `--store ` as sticky for the rest of the workflow. Every unscoped example of those commands below is shorthand: before running it, append the flag. For example, run `openspec status --change "" --json --store ""`, not the unscoped form shown below. Other commands do not take the flag. Hints printed by commands already carry the flag; keep it on follow-ups. Without a store, commands act on the nearest local `openspec/` root. +**Project check:** These steps expect a project that already uses OpenSpec. Before the first step that writes anything (`new change`, `archive`, `sync specs`, or authoring an artifact file), confirm the project has a root: run `openspec status --json` and read `root.source`. Any value other than `implicit` means the project is set up; `"source": "implicit"` means it is not - there is no `openspec/` directory here, and the next write would create one. An error saying `No OpenSpec root found` means the same thing. In that case stop before writing and ask the user how to proceed: set this project up (`openspec init`), target a store they already have (`--store `), or drop OpenSpec for this request and help them directly. Wait for their answer. Do not run `openspec init` on your own, do not hand-create `openspec/` files, and do not let a command create the root as a side effect. + --- ## Preflight diff --git a/skills/openspec-propose/SKILL.md b/skills/openspec-propose/SKILL.md index 2d5709dfe1..e4a079231d 100644 --- a/skills/openspec-propose/SKILL.md +++ b/skills/openspec-propose/SKILL.md @@ -1,6 +1,6 @@ --- name: openspec-propose -description: Propose a new change with all artifacts generated in one step. Use when the user wants to quickly describe what they want to build and get a complete proposal with design, specs, and tasks ready for implementation. +description: Propose a new OpenSpec change with all artifacts generated in one step. Use when the user wants to quickly describe what they want to build and get a complete proposal with design, specs, and tasks ready for implementation. allowed-tools: Bash(openspec:*) license: MIT compatibility: Requires openspec CLI. @@ -27,6 +27,8 @@ When the user is ready to implement, they must start the apply workflow explicit **Store selection:** If the user names a store (a store is a standalone OpenSpec repo registered on this machine) or the work lives in one, run `openspec store list --json` to discover registered store ids, then pass `--store ` on the commands that read or write specs and changes (`new change`, `status`, `instructions`, `list`, `show`, `validate`, `archive`, `doctor`, `context`, `schemas`, `view`). Once selected, treat `--store ` as sticky for the rest of the workflow. Every unscoped example of those commands below is shorthand: before running it, append the flag. For example, run `openspec status --change "" --json --store ""`, not the unscoped form shown below. Other commands do not take the flag. Hints printed by commands already carry the flag; keep it on follow-ups. Without a store, commands act on the nearest local `openspec/` root. +**Project check:** These steps expect a project that already uses OpenSpec. Before the first step that writes anything (`new change`, `archive`, `sync specs`, or authoring an artifact file), confirm the project has a root: run `openspec status --json` and read `root.source`. Any value other than `implicit` means the project is set up; `"source": "implicit"` means it is not - there is no `openspec/` directory here, and the next write would create one. An error saying `No OpenSpec root found` means the same thing. In that case stop before writing and ask the user how to proceed: set this project up (`openspec init`), target a store they already have (`--store `), or drop OpenSpec for this request and help them directly. Wait for their answer. Do not run `openspec init` on your own, do not hand-create `openspec/` files, and do not let a command create the root as a side effect. + **Input**: The user's request should include a change name (kebab-case) OR a description of what they want to build. **Steps** diff --git a/skills/openspec-sync-specs/SKILL.md b/skills/openspec-sync-specs/SKILL.md index d12d56b857..244aa44aba 100644 --- a/skills/openspec-sync-specs/SKILL.md +++ b/skills/openspec-sync-specs/SKILL.md @@ -1,6 +1,6 @@ --- name: openspec-sync-specs -description: Sync delta specs from a change to main specs. Use when the user wants to update main specs with changes from a delta spec, without archiving the change. +description: Sync delta specs from an OpenSpec change to main specs. Use when the user wants to update main specs with changes from a delta spec, without archiving the change. allowed-tools: Bash(openspec:*) license: MIT compatibility: Requires openspec CLI. @@ -15,6 +15,8 @@ This is an **agent-driven** operation - you will read delta specs and directly e **Store selection:** If the user names a store (a store is a standalone OpenSpec repo registered on this machine) or the work lives in one, run `openspec store list --json` to discover registered store ids, then pass `--store ` on the commands that read or write specs and changes (`new change`, `status`, `instructions`, `list`, `show`, `validate`, `archive`, `doctor`, `context`, `schemas`, `view`). Once selected, treat `--store ` as sticky for the rest of the workflow. Every unscoped example of those commands below is shorthand: before running it, append the flag. For example, run `openspec status --change "" --json --store ""`, not the unscoped form shown below. Other commands do not take the flag. Hints printed by commands already carry the flag; keep it on follow-ups. Without a store, commands act on the nearest local `openspec/` root. +**Project check:** These steps expect a project that already uses OpenSpec. Before the first step that writes anything (`new change`, `archive`, `sync specs`, or authoring an artifact file), confirm the project has a root: run `openspec status --json` and read `root.source`. Any value other than `implicit` means the project is set up; `"source": "implicit"` means it is not - there is no `openspec/` directory here, and the next write would create one. An error saying `No OpenSpec root found` means the same thing. In that case stop before writing and ask the user how to proceed: set this project up (`openspec init`), target a store they already have (`--store `), or drop OpenSpec for this request and help them directly. Wait for their answer. Do not run `openspec init` on your own, do not hand-create `openspec/` files, and do not let a command create the root as a side effect. + `` is the spec directory relative to `specs/` (for example, `user-auth` or `identity/user-auth`). Preserve the full path from each delta spec when resolving its main spec. **Input**: Optionally specify a change name. If omitted, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes. diff --git a/skills/openspec-update-change/SKILL.md b/skills/openspec-update-change/SKILL.md index 24c9f88367..819a535534 100644 --- a/skills/openspec-update-change/SKILL.md +++ b/skills/openspec-update-change/SKILL.md @@ -13,6 +13,8 @@ Revise a change's existing planning artifacts and keep them coherent. Never edit **Store selection:** If the user names a store (a store is a standalone OpenSpec repo registered on this machine) or the work lives in one, run `openspec store list --json` to discover registered store ids, then pass `--store ` on the commands that read or write specs and changes (`new change`, `status`, `instructions`, `list`, `show`, `validate`, `archive`, `doctor`, `context`, `schemas`, `view`). Once selected, treat `--store ` as sticky for the rest of the workflow. Every unscoped example of those commands below is shorthand: before running it, append the flag. For example, run `openspec status --change "" --json --store ""`, not the unscoped form shown below. Other commands do not take the flag. Hints printed by commands already carry the flag; keep it on follow-ups. Without a store, commands act on the nearest local `openspec/` root. +**Project check:** These steps expect a project that already uses OpenSpec. Before the first step that writes anything (`new change`, `archive`, `sync specs`, or authoring an artifact file), confirm the project has a root: run `openspec status --json` and read `root.source`. Any value other than `implicit` means the project is set up; `"source": "implicit"` means it is not - there is no `openspec/` directory here, and the next write would create one. An error saying `No OpenSpec root found` means the same thing. In that case stop before writing and ask the user how to proceed: set this project up (`openspec init`), target a store they already have (`--store `), or drop OpenSpec for this request and help them directly. Wait for their answer. Do not run `openspec init` on your own, do not hand-create `openspec/` files, and do not let a command create the root as a side effect. + **Input**: Optionally specify a change name. If omitted, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes. `/openspec-continue-change` is an optional workflow and may not be installed. Before suggesting it anywhere below, verify that it is available. If it is unavailable, `openspec status --change "" --json` shows the next artifact and `openspec instructions "" --change "" --json` explains how to create it. diff --git a/skills/openspec-verify-change/SKILL.md b/skills/openspec-verify-change/SKILL.md index 2165a6a910..156a729710 100644 --- a/skills/openspec-verify-change/SKILL.md +++ b/skills/openspec-verify-change/SKILL.md @@ -1,6 +1,6 @@ --- name: openspec-verify-change -description: Verify implementation matches change artifacts. Use when the user wants to validate that implementation is complete, correct, and coherent before archiving. +description: Verify implementation matches OpenSpec change artifacts. Use when the user wants to validate that implementation is complete, correct, and coherent before archiving. allowed-tools: Bash(openspec:*) license: MIT compatibility: Requires openspec CLI. @@ -13,6 +13,8 @@ Verify that an implementation matches the change artifacts (specs, tasks, design **Store selection:** If the user names a store (a store is a standalone OpenSpec repo registered on this machine) or the work lives in one, run `openspec store list --json` to discover registered store ids, then pass `--store ` on the commands that read or write specs and changes (`new change`, `status`, `instructions`, `list`, `show`, `validate`, `archive`, `doctor`, `context`, `schemas`, `view`). Once selected, treat `--store ` as sticky for the rest of the workflow. Every unscoped example of those commands below is shorthand: before running it, append the flag. For example, run `openspec status --change "" --json --store ""`, not the unscoped form shown below. Other commands do not take the flag. Hints printed by commands already carry the flag; keep it on follow-ups. Without a store, commands act on the nearest local `openspec/` root. +**Project check:** These steps expect a project that already uses OpenSpec. Before the first step that writes anything (`new change`, `archive`, `sync specs`, or authoring an artifact file), confirm the project has a root: run `openspec status --json` and read `root.source`. Any value other than `implicit` means the project is set up; `"source": "implicit"` means it is not - there is no `openspec/` directory here, and the next write would create one. An error saying `No OpenSpec root found` means the same thing. In that case stop before writing and ask the user how to proceed: set this project up (`openspec init`), target a store they already have (`--store `), or drop OpenSpec for this request and help them directly. Wait for their answer. Do not run `openspec init` on your own, do not hand-create `openspec/` files, and do not let a command create the root as a side effect. + **Input**: Optionally specify a change name. If omitted, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes. **Steps** diff --git a/src/core/templates/workflows/apply-change.ts b/src/core/templates/workflows/apply-change.ts index d7ebe2f4eb..2c44184612 100644 --- a/src/core/templates/workflows/apply-change.ts +++ b/src/core/templates/workflows/apply-change.ts @@ -6,6 +6,7 @@ */ import type { SkillTemplate, CommandTemplate } from '../types.js'; import { STORE_SELECTION_GUIDANCE } from './store-selection.js'; +import { PROJECT_ROOT_GUARD } from './project-root.js'; /** * The apply workflow instructions, authored once and rendered by both the @@ -21,6 +22,8 @@ export function getApplyInstructions(): string { ${STORE_SELECTION_GUIDANCE} +${PROJECT_ROOT_GUARD} + **Input**: Optionally specify a change name (e.g., \`/opsx:apply add-auth\`). If omitted, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes. **Steps** diff --git a/src/core/templates/workflows/archive-change.ts b/src/core/templates/workflows/archive-change.ts index 2dae74d436..c2b07a8cfc 100644 --- a/src/core/templates/workflows/archive-change.ts +++ b/src/core/templates/workflows/archive-change.ts @@ -6,15 +6,18 @@ */ import type { SkillTemplate, CommandTemplate } from '../types.js'; import { STORE_SELECTION_GUIDANCE } from './store-selection.js'; +import { PROJECT_ROOT_GUARD } from './project-root.js'; export function getArchiveChangeSkillTemplate(): SkillTemplate { return { name: 'openspec-archive-change', - description: 'Archive a completed change in the experimental workflow. Use when the user wants to finalize and archive a change after implementation is complete.', + description: 'Archive a completed OpenSpec change in the experimental workflow. Use when the user wants to finalize and archive a change after implementation is complete.', instructions: `Archive a completed change in the experimental workflow. ${STORE_SELECTION_GUIDANCE} +${PROJECT_ROOT_GUARD} + \`\` is the spec directory relative to \`specs/\` (for example, \`user-auth\` or \`identity/user-auth\`). Preserve the full path from each delta spec when resolving its main spec. **Input**: Optionally specify a change name. If omitted, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes. @@ -197,6 +200,8 @@ export function getOpsxArchiveCommandTemplate(): CommandTemplate { ${STORE_SELECTION_GUIDANCE} +${PROJECT_ROOT_GUARD} + \`\` is the spec directory relative to \`specs/\` (for example, \`user-auth\` or \`identity/user-auth\`). Preserve the full path from each delta spec when resolving its main spec. **Input**: Optionally specify a change name after \`/opsx:archive\` (e.g., \`/opsx:archive add-auth\`). If omitted, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes. diff --git a/src/core/templates/workflows/bulk-archive-change.ts b/src/core/templates/workflows/bulk-archive-change.ts index cacede2543..6beeaa74bf 100644 --- a/src/core/templates/workflows/bulk-archive-change.ts +++ b/src/core/templates/workflows/bulk-archive-change.ts @@ -6,17 +6,20 @@ */ import type { SkillTemplate, CommandTemplate } from '../types.js'; import { STORE_SELECTION_GUIDANCE } from './store-selection.js'; +import { PROJECT_ROOT_GUARD } from './project-root.js'; export function getBulkArchiveChangeSkillTemplate(): SkillTemplate { return { name: 'openspec-bulk-archive-change', - description: 'Archive multiple completed changes at once. Use when archiving several parallel changes.', + description: 'Archive multiple completed OpenSpec changes at once. Use when archiving several parallel changes.', instructions: `Archive multiple completed changes in a single operation. This skill allows you to batch-archive changes, handling spec conflicts intelligently by checking the codebase to determine what's actually implemented. ${STORE_SELECTION_GUIDANCE} +${PROJECT_ROOT_GUARD} + \`\` is the spec directory relative to \`specs/\` (for example, \`user-auth\` or \`identity/user-auth\`). Preserve the full path from each delta spec when resolving its main spec. **Input**: None required (prompts for selection) @@ -356,6 +359,8 @@ This skill allows you to batch-archive changes, handling spec conflicts intellig ${STORE_SELECTION_GUIDANCE} +${PROJECT_ROOT_GUARD} + \`\` is the spec directory relative to \`specs/\` (for example, \`user-auth\` or \`identity/user-auth\`). Preserve the full path from each delta spec when resolving its main spec. **Input**: None required (prompts for selection) diff --git a/src/core/templates/workflows/continue-change.ts b/src/core/templates/workflows/continue-change.ts index 14b3109e43..387662e8d8 100644 --- a/src/core/templates/workflows/continue-change.ts +++ b/src/core/templates/workflows/continue-change.ts @@ -6,6 +6,7 @@ */ import type { SkillTemplate, CommandTemplate } from '../types.js'; import { STORE_SELECTION_GUIDANCE } from './store-selection.js'; +import { PROJECT_ROOT_GUARD } from './project-root.js'; export function getContinueChangeSkillTemplate(): SkillTemplate { return { @@ -15,6 +16,8 @@ export function getContinueChangeSkillTemplate(): SkillTemplate { ${STORE_SELECTION_GUIDANCE} +${PROJECT_ROOT_GUARD} + **Input**: Optionally specify a change name. If omitted, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes. **Steps** @@ -133,6 +136,8 @@ export function getOpsxContinueCommandTemplate(): CommandTemplate { ${STORE_SELECTION_GUIDANCE} +${PROJECT_ROOT_GUARD} + **Input**: Optionally specify a change name after \`/opsx:continue\` (e.g., \`/opsx:continue add-auth\`). If omitted, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes. **Steps** diff --git a/src/core/templates/workflows/explore.ts b/src/core/templates/workflows/explore.ts index 8954a2cce4..49068b78e5 100644 --- a/src/core/templates/workflows/explore.ts +++ b/src/core/templates/workflows/explore.ts @@ -6,6 +6,7 @@ */ import type { SkillTemplate, CommandTemplate } from '../types.js'; import { STORE_SELECTION_GUIDANCE } from './store-selection.js'; +import { PROJECT_ROOT_GUARD } from './project-root.js'; const PLANNING_GUIDANCE = `## Planning a Change @@ -32,7 +33,7 @@ adding a service to operate; shared state would need a separate sync design. export function getExploreSkillTemplate(): SkillTemplate { return { name: 'openspec-explore', - description: 'Enter explore mode - a thinking partner for exploring ideas, investigating problems, and clarifying requirements. Use when the user wants to think through something before or during a change.', + description: 'Enter OpenSpec explore mode - a thinking partner for exploring ideas, investigating problems, and clarifying requirements in a project that uses OpenSpec. Use when the user wants to think through something before or during an OpenSpec change.', instructions: `Enter explore mode. Think deeply. Visualize freely. Follow the conversation wherever it goes. **IMPORTANT: Explore mode is for thinking, not implementing.** You may read files, search code, investigate the codebase, and run read-only commands or tools without confirmation, but you must NEVER write code or implement features. If the user asks you to implement something, remind them to exit explore mode first and create a change proposal. You MAY create or update OpenSpec change artifacts (proposals, designs, specs) within a confirmed scope—that's capturing thinking, not implementing. Answering design or clarifying questions is never consent to write. Before the first write-capable action, name the artifacts or files you would change and what you would do, ask a direct yes/no question, and wait for the user's confirmation in a separate message. Confirmation covers only the scope you described; ask again before expanding it. For a new change, scaffold it first as described below. @@ -41,6 +42,8 @@ export function getExploreSkillTemplate(): SkillTemplate { ${STORE_SELECTION_GUIDANCE} +${PROJECT_ROOT_GUARD} + --- ## The Stance @@ -356,6 +359,8 @@ export function getOpsxExploreCommandTemplate(): CommandTemplate { ${STORE_SELECTION_GUIDANCE} +${PROJECT_ROOT_GUARD} + **Input**: The argument after \`/opsx:explore\` is whatever the user wants to think about. Could be: - A vague idea: "real-time collaboration" - A specific problem: "the auth system is getting unwieldy" diff --git a/src/core/templates/workflows/ff-change.ts b/src/core/templates/workflows/ff-change.ts index 843fb79c87..435b9b2548 100644 --- a/src/core/templates/workflows/ff-change.ts +++ b/src/core/templates/workflows/ff-change.ts @@ -6,6 +6,7 @@ */ import type { SkillTemplate, CommandTemplate } from '../types.js'; import { STORE_SELECTION_GUIDANCE } from './store-selection.js'; +import { PROJECT_ROOT_GUARD } from './project-root.js'; export function getFfChangeSkillTemplate(): SkillTemplate { return { @@ -15,6 +16,8 @@ export function getFfChangeSkillTemplate(): SkillTemplate { ${STORE_SELECTION_GUIDANCE} +${PROJECT_ROOT_GUARD} + **Input**: The user's request should include a change name (kebab-case) OR a description of what they want to build. **Steps** @@ -132,6 +135,8 @@ export function getOpsxFfCommandTemplate(): CommandTemplate { ${STORE_SELECTION_GUIDANCE} +${PROJECT_ROOT_GUARD} + **Input**: The argument after \`/opsx:ff\` is the change name (kebab-case), OR a description of what the user wants to build. **Steps** diff --git a/src/core/templates/workflows/new-change.ts b/src/core/templates/workflows/new-change.ts index e45858abbc..4df17c25ee 100644 --- a/src/core/templates/workflows/new-change.ts +++ b/src/core/templates/workflows/new-change.ts @@ -6,6 +6,7 @@ */ import type { SkillTemplate, CommandTemplate } from '../types.js'; import { STORE_SELECTION_GUIDANCE } from './store-selection.js'; +import { PROJECT_ROOT_GUARD } from './project-root.js'; export function getNewChangeSkillTemplate(): SkillTemplate { return { @@ -15,6 +16,8 @@ export function getNewChangeSkillTemplate(): SkillTemplate { ${STORE_SELECTION_GUIDANCE} +${PROJECT_ROOT_GUARD} + **Input**: The user's request should include a change name (kebab-case) OR a description of what they want to build. **Steps** @@ -92,6 +95,8 @@ export function getOpsxNewCommandTemplate(): CommandTemplate { ${STORE_SELECTION_GUIDANCE} +${PROJECT_ROOT_GUARD} + **Input**: The argument after \`/opsx:new\` is the change name (kebab-case), OR a description of what the user wants to build. **Steps** diff --git a/src/core/templates/workflows/onboard.ts b/src/core/templates/workflows/onboard.ts index 414c6e18b5..1e28d10a81 100644 --- a/src/core/templates/workflows/onboard.ts +++ b/src/core/templates/workflows/onboard.ts @@ -6,6 +6,7 @@ */ import type { SkillTemplate, CommandTemplate } from '../types.js'; import { STORE_SELECTION_GUIDANCE } from './store-selection.js'; +import { PROJECT_ROOT_GUARD } from './project-root.js'; export function getOnboardSkillTemplate(): SkillTemplate { return { @@ -23,6 +24,8 @@ function getOnboardInstructions(): string { ${STORE_SELECTION_GUIDANCE} +${PROJECT_ROOT_GUARD} + --- ## Preflight diff --git a/src/core/templates/workflows/project-root.ts b/src/core/templates/workflows/project-root.ts new file mode 100644 index 0000000000..444c411482 --- /dev/null +++ b/src/core/templates/workflows/project-root.ts @@ -0,0 +1,14 @@ +/** + * Shared project-root guidance for skill template workflows. + * + * Generated skills and commands are installed once per machine, so they are + * offered in every repository the agent opens - including repositories that + * never ran `openspec init`. Nothing stops the workflow there: `openspec new + * change` falls back to an implicit root and creates `openspec/` in whatever + * directory the agent happens to be in. + * + * This guidance is interpolated into every workflow so the agent checks + * `root.source` before writing and hands the decision back to the user + * instead of setting the project up on its own. + */ +export const PROJECT_ROOT_GUARD = `**Project check:** These steps expect a project that already uses OpenSpec. Before the first step that writes anything (\`new change\`, \`archive\`, \`sync specs\`, or authoring an artifact file), confirm the project has a root: run \`openspec status --json\` and read \`root.source\`. Any value other than \`implicit\` means the project is set up; \`"source": "implicit"\` means it is not - there is no \`openspec/\` directory here, and the next write would create one. An error saying \`No OpenSpec root found\` means the same thing. In that case stop before writing and ask the user how to proceed: set this project up (\`openspec init\`), target a store they already have (\`--store \`), or drop OpenSpec for this request and help them directly. Wait for their answer. Do not run \`openspec init\` on your own, do not hand-create \`openspec/\` files, and do not let a command create the root as a side effect.`; diff --git a/src/core/templates/workflows/propose.ts b/src/core/templates/workflows/propose.ts index 54ceda16c7..283f755a00 100644 --- a/src/core/templates/workflows/propose.ts +++ b/src/core/templates/workflows/propose.ts @@ -6,11 +6,12 @@ */ import type { SkillTemplate, CommandTemplate } from '../types.js'; import { STORE_SELECTION_GUIDANCE } from './store-selection.js'; +import { PROJECT_ROOT_GUARD } from './project-root.js'; export function getOpsxProposeSkillTemplate(): SkillTemplate { return { name: 'openspec-propose', - description: 'Propose a new change with all artifacts generated in one step. Use when the user wants to quickly describe what they want to build and get a complete proposal with design, specs, and tasks ready for implementation.', + description: 'Propose a new OpenSpec change with all artifacts generated in one step. Use when the user wants to quickly describe what they want to build and get a complete proposal with design, specs, and tasks ready for implementation.', instructions: `Propose a new change - create the change and generate all artifacts in one step. **Planning boundary**: This workflow creates planning artifacts only. The user request that selected or triggered this workflow authorizes planning only, even if it asks to build or fix something. Do not edit project code. After the planning artifacts are complete, stop. Do not start implementation in the same response, even if the initial request asks for it. Wait for a new user request after the artifacts are presented; then start the apply workflow. @@ -29,6 +30,8 @@ When the user is ready to implement, they must start the apply workflow explicit ${STORE_SELECTION_GUIDANCE} +${PROJECT_ROOT_GUARD} + **Input**: The user's request should include a change name (kebab-case) OR a description of what they want to build. **Steps** @@ -182,6 +185,8 @@ When the user is ready to implement, they must start the apply workflow explicit ${STORE_SELECTION_GUIDANCE} +${PROJECT_ROOT_GUARD} + **Input**: The argument after \`/opsx:propose\` is the change name (kebab-case), OR a description of what the user wants to build. **Steps** diff --git a/src/core/templates/workflows/sync-specs.ts b/src/core/templates/workflows/sync-specs.ts index bedbaa7164..03e8b23167 100644 --- a/src/core/templates/workflows/sync-specs.ts +++ b/src/core/templates/workflows/sync-specs.ts @@ -6,17 +6,20 @@ */ import type { SkillTemplate, CommandTemplate } from '../types.js'; import { STORE_SELECTION_GUIDANCE } from './store-selection.js'; +import { PROJECT_ROOT_GUARD } from './project-root.js'; export function getSyncSpecsSkillTemplate(): SkillTemplate { return { name: 'openspec-sync-specs', - description: 'Sync delta specs from a change to main specs. Use when the user wants to update main specs with changes from a delta spec, without archiving the change.', + description: 'Sync delta specs from an OpenSpec change to main specs. Use when the user wants to update main specs with changes from a delta spec, without archiving the change.', instructions: `Sync delta specs from a change to main specs. This is an **agent-driven** operation - you will read delta specs and directly edit main specs to apply the changes. This allows intelligent merging (e.g., adding a scenario without copying the entire requirement). ${STORE_SELECTION_GUIDANCE} +${PROJECT_ROOT_GUARD} + \`\` is the spec directory relative to \`specs/\` (for example, \`user-auth\` or \`identity/user-auth\`). Preserve the full path from each delta spec when resolving its main spec. **Input**: Optionally specify a change name. If omitted, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes. @@ -279,6 +282,8 @@ This is an **agent-driven** operation - you will read delta specs and directly e ${STORE_SELECTION_GUIDANCE} +${PROJECT_ROOT_GUARD} + \`\` is the spec directory relative to \`specs/\` (for example, \`user-auth\` or \`identity/user-auth\`). Preserve the full path from each delta spec when resolving its main spec. **Input**: Optionally specify a change name after \`/opsx:sync\` (e.g., \`/opsx:sync add-auth\`). If omitted, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes. diff --git a/src/core/templates/workflows/update-change.ts b/src/core/templates/workflows/update-change.ts index 7700cd8d7e..e0a62c9fcd 100644 --- a/src/core/templates/workflows/update-change.ts +++ b/src/core/templates/workflows/update-change.ts @@ -6,6 +6,7 @@ */ import type { SkillTemplate, CommandTemplate } from '../types.js'; import { STORE_SELECTION_GUIDANCE } from './store-selection.js'; +import { PROJECT_ROOT_GUARD } from './project-root.js'; export function getUpdateChangeSkillTemplate(): SkillTemplate { return { @@ -15,6 +16,8 @@ export function getUpdateChangeSkillTemplate(): SkillTemplate { ${STORE_SELECTION_GUIDANCE} +${PROJECT_ROOT_GUARD} + **Input**: Optionally specify a change name. If omitted, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes. \`/opsx:continue\` is an optional workflow and may not be installed. Before suggesting it anywhere below, verify that it is available. If it is unavailable, \`openspec status --change "" --json\` shows the next artifact and \`openspec instructions "" --change "" --json\` explains how to create it. @@ -106,6 +109,8 @@ export function getOpsxUpdateCommandTemplate(): CommandTemplate { ${STORE_SELECTION_GUIDANCE} +${PROJECT_ROOT_GUARD} + **Input**: Optionally specify a change name after \`/opsx:update\` (e.g., \`/opsx:update add-auth\`). If omitted, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes. \`/opsx:continue\` is an optional workflow and may not be installed. Before suggesting it anywhere below, verify that it is available. If it is unavailable, \`openspec status --change "" --json\` shows the next artifact and \`openspec instructions "" --change "" --json\` explains how to create it. diff --git a/src/core/templates/workflows/verify-change.ts b/src/core/templates/workflows/verify-change.ts index 1aa540c76b..99f5e8f676 100644 --- a/src/core/templates/workflows/verify-change.ts +++ b/src/core/templates/workflows/verify-change.ts @@ -6,15 +6,18 @@ */ import type { SkillTemplate, CommandTemplate } from '../types.js'; import { STORE_SELECTION_GUIDANCE } from './store-selection.js'; +import { PROJECT_ROOT_GUARD } from './project-root.js'; export function getVerifyChangeSkillTemplate(): SkillTemplate { return { name: 'openspec-verify-change', - description: 'Verify implementation matches change artifacts. Use when the user wants to validate that implementation is complete, correct, and coherent before archiving.', + description: 'Verify implementation matches OpenSpec change artifacts. Use when the user wants to validate that implementation is complete, correct, and coherent before archiving.', instructions: `Verify that an implementation matches the change artifacts (specs, tasks, design). ${STORE_SELECTION_GUIDANCE} +${PROJECT_ROOT_GUARD} + **Input**: Optionally specify a change name. If omitted, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes. **Steps** @@ -190,6 +193,8 @@ export function getOpsxVerifyCommandTemplate(): CommandTemplate { ${STORE_SELECTION_GUIDANCE} +${PROJECT_ROOT_GUARD} + **Input**: Optionally specify a change name after \`/opsx:verify\` (e.g., \`/opsx:verify add-auth\`). If omitted, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes. **Steps** diff --git a/test/core/templates/project-root-guard.test.ts b/test/core/templates/project-root-guard.test.ts new file mode 100644 index 0000000000..6ecce15c5c --- /dev/null +++ b/test/core/templates/project-root-guard.test.ts @@ -0,0 +1,93 @@ +import { describe, expect, it } from 'vitest'; + +import { PROJECT_ROOT_GUARD } from '../../../src/core/templates/workflows/project-root.js'; +import { getFeedbackSkillTemplate } from '../../../src/core/templates/skill-templates.js'; +import { + generateSkillContent, + getCommandContents, + getSkillTemplates, +} from '../../../src/core/shared/skill-generation.js'; + +/** + * Regression coverage for #1645. + * + * Skills and commands are installed once per machine and offered in every + * repository, including ones that never ran `openspec init`. Nothing in the + * CLI stops the workflow there - `openspec new change` falls back to an + * implicit root and creates `openspec/` wherever the agent is standing - so + * the guard has to live in the instructions themselves, in every workflow. + */ +describe('project root guard', () => { + it('warns about an uninitialized project in every deployed skill', () => { + for (const { template, dirName } of getSkillTemplates()) { + const content = generateSkillContent(template, 'PARITY-BASELINE'); + expect(content, dirName).toContain(PROJECT_ROOT_GUARD); + } + }); + + it('warns about an uninitialized project in every deployed opsx command', () => { + for (const entry of getCommandContents()) { + expect(entry.body, entry.id).toContain(PROJECT_ROOT_GUARD); + } + }); + + // Feedback files a GitHub issue through `openspec feedback`; it never reads + // or writes a root, so it ships outside both registries and carries neither + // the store teaching nor this guard. + it('leaves the rootless feedback skill alone', () => { + expect(getFeedbackSkillTemplate().instructions).not.toContain('**Project check:**'); + }); + + it('names the machine-readable signal rather than a guess', () => { + expect(PROJECT_ROOT_GUARD).toContain('openspec status --json'); + expect(PROJECT_ROOT_GUARD).toContain('root.source'); + expect(PROJECT_ROOT_GUARD).toContain('"source": "implicit"'); + expect(PROJECT_ROOT_GUARD).toContain('No OpenSpec root found'); + }); + + it('hands the decision to the user instead of setting the project up', () => { + expect(PROJECT_ROOT_GUARD).toContain('stop before writing and ask the user how to proceed'); + expect(PROJECT_ROOT_GUARD).toContain('drop OpenSpec for this request and help them directly'); + expect(PROJECT_ROOT_GUARD).toContain('Do not run `openspec init` on your own'); + expect(PROJECT_ROOT_GUARD).toContain('do not hand-create `openspec/` files'); + expect(PROJECT_ROOT_GUARD).toContain( + 'do not let a command create the root as a side effect' + ); + }); + + // A guard printed after the workflow has already scaffolded a change is no + // guard at all: it has to precede the first command the workflow runs. + it('precedes the first openspec command in every deployed skill and command', () => { + const bodies: Array<[string, string]> = [ + ...getSkillTemplates().map( + ({ template, dirName }): [string, string] => [ + `skill ${dirName}`, + generateSkillContent(template, 'PARITY-BASELINE'), + ] + ), + ...getCommandContents().map( + (entry): [string, string] => [`command ${entry.id}`, entry.body] + ), + ]; + + for (const [label, body] of bodies) { + const guardStart = body.indexOf(PROJECT_ROOT_GUARD); + expect(guardStart, label).toBeGreaterThanOrEqual(0); + + const guardEnd = guardStart + PROJECT_ROOT_GUARD.length; + const firstCommand = body.indexOf('```bash'); + if (firstCommand >= 0) { + expect(firstCommand, label).toBeGreaterThan(guardEnd); + } + } + }); + + // The other half of #1645: a host picks skills by description, so a + // description that never says "OpenSpec" reads as a generic offer to + // explore or propose and wins in repositories that have no OpenSpec at all. + it('scopes every deployed skill description to OpenSpec', () => { + for (const { template, dirName } of getSkillTemplates()) { + expect(template.description, dirName).toContain('OpenSpec'); + } + }); +}); diff --git a/test/core/templates/skill-templates-parity.test.ts b/test/core/templates/skill-templates-parity.test.ts index 3f309bdaa3..f37f3458eb 100644 --- a/test/core/templates/skill-templates-parity.test.ts +++ b/test/core/templates/skill-templates-parity.test.ts @@ -38,46 +38,46 @@ import { import { STORE_SELECTION_GUIDANCE } from '../../../src/core/templates/workflows/store-selection.js'; const EXPECTED_FUNCTION_HASHES: Record = { - getExploreSkillTemplate: '6315fcc5c2eb848963bc8bca4c23e657412a99608e610daee59fb4e58cd21fd4', - getNewChangeSkillTemplate: 'eabd1e895c5881dcb17dcbaa3fb26098dd59e8eacb318e400820b4dc811ef781', - getContinueChangeSkillTemplate: '012136f6411a99c8fa228e2f9444cb64b0a89e0f56fdeac2fe03b2f5bee0c5d7', - getApplyChangeSkillTemplate: 'd1e7d5ceb85193c0964057dbb88e9651526754bd33f84020e2440ff0621d5dbb', - getFfChangeSkillTemplate: 'efa6a70c111b18b61a7720250b9622afa9a212fb64edf609cf80e2182a9bdf8c', - getSyncSpecsSkillTemplate: 'b099e2ff31859c9b10d928066e662524f9aad9ecf2be12fceacb732d718c4146', - getOnboardSkillTemplate: '3a836faae463d88c289a1c129cb7ee556a563b7e53e1a52a4711ff152a3b51f7', - getOpsxExploreCommandTemplate: 'b4706a5b8fd280f7929eea610ecc9d41676b2d2dd6653d259cbbc2bfe01813d9', - getOpsxNewCommandTemplate: 'f2d30e569798a4c92ba932859d6ba4e0ad10e18feccbade1cfee0957597b3463', - getOpsxContinueCommandTemplate: 'e50e50266efa1b8e64ff9b6274ee8254f0a240d6adc1b862d126e2f1c9d3a559', - getOpsxApplyCommandTemplate: 'e3579ac78f2e2c75fa3d3a7ac7dc3e49c395e96f7323398f0f041d94f8de9bb0', - getOpsxFfCommandTemplate: '21132fc9c6d3b3ab2d2295d6bbd72d1e0052eb35ea1be0258c8b1ab3e200c4db', - getArchiveChangeSkillTemplate: '56bfada1a5f35a127791b70de9d428a75b5aedd1584d6c9803a1ecb1fd1b4a23', - getBulkArchiveChangeSkillTemplate: '93875998cade5322d95b43299fba794bc1da754e917dd63a770406386a6d295d', - getOpsxSyncCommandTemplate: '0d2427efb79986e8fff3f96bd075a739c80d45eb29159fae717e950030da8202', - getVerifyChangeSkillTemplate: '223b7ffd99299a7d430e13092b9a0a3421b39f0d3217232f46c39d79b5f619ff', - getOpsxArchiveCommandTemplate: '9f973c819b11620985b03322945f0e0a92a02a2ef455b94e74482f5e6292ac5d', - getOpsxOnboardCommandTemplate: 'ee99aa99252c602720fbb8c63fb3ac438a5bd4e952fd961ddf1ae956cbfc2c8f', - getOpsxBulkArchiveCommandTemplate: '9fa8cdebe2f5667ebfc37bdc023396762c59d5b038c771dac2d8fd2c19e2627b', - getOpsxVerifyCommandTemplate: '1efcf7eff0671f48e9d9420f50865c563dd3079ee60f8c380bb7a90dd0102696', - getOpsxProposeSkillTemplate: '9c0fbf0137151bd03ec30c45180f83daec96e8976ceaf517c63147f84b803446', - getOpsxProposeCommandTemplate: 'b3c145f541dcc13d9859eae8f7bedbe4553371477ed2c5ac07a4a80f82c46f52', + getExploreSkillTemplate: '5a15a8b69043633b72d8d84568b876c25ae31fe2d80159866e848418010f4d2c', + getNewChangeSkillTemplate: 'ab0c86bd0865297ffe567842961eb0c054050c0a658c63fa62971003242ae91a', + getContinueChangeSkillTemplate: '91db17a4f25257a03660006916c05b02fe3aaee003c8b3a3422e25878e1c78b8', + getApplyChangeSkillTemplate: '1b91edb905faf7bdc3dece7a0f11045196a07588aff993bc13b7d2033b81616f', + getFfChangeSkillTemplate: '1f6e63424492398ea9256bc106a4f9fa4fec0e8b6c82ba22cac5e08ac302273e', + getSyncSpecsSkillTemplate: 'b0859fbc99decde4c8cfcbe6c2954b61f04d42bd28e6087eae7806b5e6a042f7', + getOnboardSkillTemplate: '44bc5da670f227b824a9258964888f57e4295dec044cee3d227056ee362845a0', + getOpsxExploreCommandTemplate: '19eb801771f076b61464ab372249c4bd215994945403361f2e475c2e89d73aab', + getOpsxNewCommandTemplate: '8915b72135554de9f78d0274c2b111681fcf2ddc613058a4f78a63201f57c176', + getOpsxContinueCommandTemplate: '1f41c50b7b8a609a044c8f2d995dd4f8cb9bf6498853c00723ba10a458cace70', + getOpsxApplyCommandTemplate: '8d03568ee988d154ef6cde1bedf0ae3401aefe9ab558b8c1fa3a745fcd72cff3', + getOpsxFfCommandTemplate: '2a88278926e2a2ea3ab66e538b1ed344ea30050329626b875c7744995617970f', + getArchiveChangeSkillTemplate: '5a9fd7040c463f359c33239a12143e0767c1f8dcf6123e4861d9bbbc7db40dc4', + getBulkArchiveChangeSkillTemplate: 'c9aac2f8d7debd7990f581a840acbfe39aa80a44dc373646a1864ad6e4f23250', + getOpsxSyncCommandTemplate: '9267cb4ffa20a9c2886221636be07a62b7bd986843c713f77a87da0e4c6f975f', + getVerifyChangeSkillTemplate: 'e3f4f50eea12d758e6c0229922b467592bed95f1d8d8195ab43057272cd70811', + getOpsxArchiveCommandTemplate: '96f1e3a2d139cff703de3a8cea8727439db4413818ba5561b309d662c91cc7c1', + getOpsxOnboardCommandTemplate: '192de384df43e7a50ca413b947c861a0b6300ff7a9f4d12c06bcdc8cbc236414', + getOpsxBulkArchiveCommandTemplate: '4200eb1cb89a16b3dc99e2336731a9ada82767c98cba01b540ff33e783ac9555', + getOpsxVerifyCommandTemplate: '37aebc24996843e90868e012a5aa5ed535f793fa4e1cd26dcc63a0d6fd33a3f5', + getOpsxProposeSkillTemplate: '18ff3de460a02772f1ff15866c45ded16c74e50a5aa0fb007f8df7950777f389', + getOpsxProposeCommandTemplate: '822c134a40daa96d520fc5da439e03da47d1532dfe3d8e0287d4931b9530df0c', getFeedbackSkillTemplate: 'dabeb5e825b9349abc8156c3e7b8608f27987912a6d9bf47ef29addde6138133', - getUpdateChangeSkillTemplate: '7dc8abc6f64c58bf34d7581ed4ab095a3b7a53cb372349bee2d840db58622819', - getOpsxUpdateCommandTemplate: 'e2388521b22f92f74561df9a0c2f98e1fa4d265af93b5ba26f42fb47a6c5bfed', + getUpdateChangeSkillTemplate: 'd1b3e5ab9f2d84860a12af23d4ebc88e25967af15e9419fa7e27daff3c103e4a', + getOpsxUpdateCommandTemplate: '98b7a5ca79fdcd1876b34809994a2e00113b883ed5e7fa8f300f9ebb76e6291a', }; const EXPECTED_GENERATED_SKILL_CONTENT_HASHES: Record = { - 'openspec-explore': 'dd84af68d3c93b40659dcdd8d383423b25b443cacdc4b514cd70614ae10c5cac', - 'openspec-new-change': 'ec4529beef978e34634a6f7286fab55d68fad8fb374dceb45691d52caab33fbb', - 'openspec-continue-change': 'bb6194a16c54891cdb253678e8f70ce53b2af86735243980f366ce551d37e42e', - 'openspec-apply-change': '81ea96d9fa6ec8536cd23c1fe561ed28e1cc1cad0a8ceb700588e08974cc0e49', - 'openspec-ff-change': '31355250514bce51b16ff37ee2b833bc9d475cd0dbd4b1f68fe2041694575623', - 'openspec-sync-specs': 'd933d8856584d6c1253de91e652e7aee9e85c77ad4d3531f6476f79d84e6e5e8', - 'openspec-archive-change': '7c65053d674ba4e1e20e2bf73ba7e5a7f94baef2eaa9b33cee48d4cadea51b7a', - 'openspec-bulk-archive-change': '2039b9ecf6e64339dffe0e16272507a386d9fe326f419ff758315aa736fdd96c', - 'openspec-verify-change': 'af9be013dcbe8c6d8f6d9ab10c893fbd03f4c62933c384d82f63894dd0ceb84f', - 'openspec-onboard': 'f6f59476acaf5e4d65dbb180da4cef62432612f3cecf207d471a951295e2003a', - 'openspec-propose': 'e358b45102a88082cf20f5c4441cba02533724ad6eef8ed15ba174e3496cb6ed', - 'openspec-update-change': '586547406aca94422dfeb3ffedce6c01049429b743f57ce829baa79ebc714d51', + 'openspec-explore': '9fc548a718cf555f411f9d4981723556bc52ffef2c117ef8444fd86a2b0ef27f', + 'openspec-new-change': '0f05ae5d75148a5b5cef9397697b6cfcef88cea8331682c44213e44674cb3fbb', + 'openspec-continue-change': '70411b49d0c1b73383c742016b13710e22bbd3ecd3fc3592b112e7c343ffef5c', + 'openspec-apply-change': 'ac12fbfaee2898cc0051962d422c450c835916f4a3b7fd4796cb1c82187c11ea', + 'openspec-ff-change': '8ebb799188c8d4b75bec852e2e70ce86fe8e2ceb6f52f214bd74523169593dde', + 'openspec-sync-specs': '14724b88c194197fb5c80a31b1eb9c8a5a89a94e97818fd63e59fdfa4b8a17dc', + 'openspec-archive-change': '1ac1cfae5918de243afa3502ed0cd3a54bb4ec0cde2e6468df1563a82bc59679', + 'openspec-bulk-archive-change': 'a021488b41fae7b9296656e62cf50a47fd04a3c0de3641953eafd3edc9d63bfa', + 'openspec-verify-change': 'd9b1dc240650d33ebb7358b8deb202c93b7cc0ff89187e5345624f2ee1a6910d', + 'openspec-onboard': '4dcc17be6b883457f264e417a57eb96c1aa92a91f2afaa8223658a220e7c51f5', + 'openspec-propose': 'b5be3ba1f342a047d0b1d8b7c4a0d9b30fb24112e7a16a18dcbc0f4026f9aa1a', + 'openspec-update-change': 'a62eefcd70b6e239c377ce73c8eae224b5413b634bdd80658e10594e42115022', }; // Intentionally excludes getFeedbackSkillTemplate: this list only models templates From c5b7fb84a32ddad3ec4ebc6d2616707d9e24b5d3 Mon Sep 17 00:00:00 2001 From: Clay Good Date: Fri, 4 Sep 2026 12:03:20 -0500 Subject: [PATCH 02/15] chore(changeset): note the uninitialized-project guard Co-Authored-By: Claude Opus 5 --- .changeset/guard-uninitialized-projects.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .changeset/guard-uninitialized-projects.md diff --git a/.changeset/guard-uninitialized-projects.md b/.changeset/guard-uninitialized-projects.md new file mode 100644 index 0000000000..af612f048d --- /dev/null +++ b/.changeset/guard-uninitialized-projects.md @@ -0,0 +1,7 @@ +--- +"@fission-ai/openspec": patch +--- + +### Bug Fixes + +- Generated skills and commands no longer adopt a project that never ran `openspec init`. Every workflow now checks `root.source` from `openspec status --json` before its first write: an implicit root (or a `No OpenSpec root found` error) means the project is not set up, so the agent stops and asks whether to initialize it, target a store, or handle the request without OpenSpec, instead of letting `openspec new change` create `openspec/` in the current directory as a side effect. Skill descriptions now name OpenSpec so hosts stop offering these workflows in unrelated repositories. From 3ae88ca3890fb6ce30a8f86249596a92c2031011 Mon Sep 17 00:00:00 2001 From: Clay Good Date: Fri, 4 Sep 2026 12:04:34 -0500 Subject: [PATCH 03/15] 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 --- skills/openspec-apply-change/SKILL.md | 2 +- skills/openspec-archive-change/SKILL.md | 2 +- skills/openspec-bulk-archive-change/SKILL.md | 2 +- skills/openspec-continue-change/SKILL.md | 2 +- skills/openspec-explore/SKILL.md | 2 +- skills/openspec-ff-change/SKILL.md | 2 +- skills/openspec-new-change/SKILL.md | 2 +- skills/openspec-onboard/SKILL.md | 2 +- skills/openspec-propose/SKILL.md | 2 +- skills/openspec-sync-specs/SKILL.md | 2 +- skills/openspec-update-change/SKILL.md | 2 +- skills/openspec-verify-change/SKILL.md | 2 +- src/core/templates/workflows/project-root.ts | 2 +- .../core/templates/project-root-guard.test.ts | 2 +- .../templates/skill-templates-parity.test.ts | 72 +++++++++---------- 15 files changed, 50 insertions(+), 50 deletions(-) diff --git a/skills/openspec-apply-change/SKILL.md b/skills/openspec-apply-change/SKILL.md index b122feb305..aaee85d684 100644 --- a/skills/openspec-apply-change/SKILL.md +++ b/skills/openspec-apply-change/SKILL.md @@ -13,7 +13,7 @@ Implement tasks from an OpenSpec change. **Store selection:** If the user names a store (a store is a standalone OpenSpec repo registered on this machine) or the work lives in one, run `openspec store list --json` to discover registered store ids, then pass `--store ` on the commands that read or write specs and changes (`new change`, `status`, `instructions`, `list`, `show`, `validate`, `archive`, `doctor`, `context`, `schemas`, `view`). Once selected, treat `--store ` as sticky for the rest of the workflow. Every unscoped example of those commands below is shorthand: before running it, append the flag. For example, run `openspec status --change "" --json --store ""`, not the unscoped form shown below. Other commands do not take the flag. Hints printed by commands already carry the flag; keep it on follow-ups. Without a store, commands act on the nearest local `openspec/` root. -**Project check:** These steps expect a project that already uses OpenSpec. Before the first step that writes anything (`new change`, `archive`, `sync specs`, or authoring an artifact file), confirm the project has a root: run `openspec status --json` and read `root.source`. Any value other than `implicit` means the project is set up; `"source": "implicit"` means it is not - there is no `openspec/` directory here, and the next write would create one. An error saying `No OpenSpec root found` means the same thing. In that case stop before writing and ask the user how to proceed: set this project up (`openspec init`), target a store they already have (`--store `), or drop OpenSpec for this request and help them directly. Wait for their answer. Do not run `openspec init` on your own, do not hand-create `openspec/` files, and do not let a command create the root as a side effect. +**Project check:** These steps expect a project that already uses OpenSpec. Before the first step that writes anything (`new change`, `archive`, `sync specs`, or authoring an artifact file), confirm the project has a root: run `openspec status --json` and read `root.source`. Any value other than `implicit` means the project is set up; `"source": "implicit"` means it is not - there is no `openspec/` directory here, and the next write would create one. An error saying `No OpenSpec root found` means the same thing. In that case stop before writing and ask the user how to proceed: set this project up (`openspec init`), target a store they already have (`--store `), or drop OpenSpec for this request and help them directly. Wait for their answer. Do not run `openspec init` until they ask for it, do not hand-create `openspec/` files, and do not let a command create the root as a side effect. **Input**: Optionally specify a change name (e.g., `/openspec-apply-change add-auth`). If omitted, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes. diff --git a/skills/openspec-archive-change/SKILL.md b/skills/openspec-archive-change/SKILL.md index 5128710111..31b7b9914a 100644 --- a/skills/openspec-archive-change/SKILL.md +++ b/skills/openspec-archive-change/SKILL.md @@ -13,7 +13,7 @@ Archive a completed change in the experimental workflow. **Store selection:** If the user names a store (a store is a standalone OpenSpec repo registered on this machine) or the work lives in one, run `openspec store list --json` to discover registered store ids, then pass `--store ` on the commands that read or write specs and changes (`new change`, `status`, `instructions`, `list`, `show`, `validate`, `archive`, `doctor`, `context`, `schemas`, `view`). Once selected, treat `--store ` as sticky for the rest of the workflow. Every unscoped example of those commands below is shorthand: before running it, append the flag. For example, run `openspec status --change "" --json --store ""`, not the unscoped form shown below. Other commands do not take the flag. Hints printed by commands already carry the flag; keep it on follow-ups. Without a store, commands act on the nearest local `openspec/` root. -**Project check:** These steps expect a project that already uses OpenSpec. Before the first step that writes anything (`new change`, `archive`, `sync specs`, or authoring an artifact file), confirm the project has a root: run `openspec status --json` and read `root.source`. Any value other than `implicit` means the project is set up; `"source": "implicit"` means it is not - there is no `openspec/` directory here, and the next write would create one. An error saying `No OpenSpec root found` means the same thing. In that case stop before writing and ask the user how to proceed: set this project up (`openspec init`), target a store they already have (`--store `), or drop OpenSpec for this request and help them directly. Wait for their answer. Do not run `openspec init` on your own, do not hand-create `openspec/` files, and do not let a command create the root as a side effect. +**Project check:** These steps expect a project that already uses OpenSpec. Before the first step that writes anything (`new change`, `archive`, `sync specs`, or authoring an artifact file), confirm the project has a root: run `openspec status --json` and read `root.source`. Any value other than `implicit` means the project is set up; `"source": "implicit"` means it is not - there is no `openspec/` directory here, and the next write would create one. An error saying `No OpenSpec root found` means the same thing. In that case stop before writing and ask the user how to proceed: set this project up (`openspec init`), target a store they already have (`--store `), or drop OpenSpec for this request and help them directly. Wait for their answer. Do not run `openspec init` until they ask for it, do not hand-create `openspec/` files, and do not let a command create the root as a side effect. `` is the spec directory relative to `specs/` (for example, `user-auth` or `identity/user-auth`). Preserve the full path from each delta spec when resolving its main spec. diff --git a/skills/openspec-bulk-archive-change/SKILL.md b/skills/openspec-bulk-archive-change/SKILL.md index d2b529f3b9..48797a9145 100644 --- a/skills/openspec-bulk-archive-change/SKILL.md +++ b/skills/openspec-bulk-archive-change/SKILL.md @@ -15,7 +15,7 @@ This skill allows you to batch-archive changes, handling spec conflicts intellig **Store selection:** If the user names a store (a store is a standalone OpenSpec repo registered on this machine) or the work lives in one, run `openspec store list --json` to discover registered store ids, then pass `--store ` on the commands that read or write specs and changes (`new change`, `status`, `instructions`, `list`, `show`, `validate`, `archive`, `doctor`, `context`, `schemas`, `view`). Once selected, treat `--store ` as sticky for the rest of the workflow. Every unscoped example of those commands below is shorthand: before running it, append the flag. For example, run `openspec status --change "" --json --store ""`, not the unscoped form shown below. Other commands do not take the flag. Hints printed by commands already carry the flag; keep it on follow-ups. Without a store, commands act on the nearest local `openspec/` root. -**Project check:** These steps expect a project that already uses OpenSpec. Before the first step that writes anything (`new change`, `archive`, `sync specs`, or authoring an artifact file), confirm the project has a root: run `openspec status --json` and read `root.source`. Any value other than `implicit` means the project is set up; `"source": "implicit"` means it is not - there is no `openspec/` directory here, and the next write would create one. An error saying `No OpenSpec root found` means the same thing. In that case stop before writing and ask the user how to proceed: set this project up (`openspec init`), target a store they already have (`--store `), or drop OpenSpec for this request and help them directly. Wait for their answer. Do not run `openspec init` on your own, do not hand-create `openspec/` files, and do not let a command create the root as a side effect. +**Project check:** These steps expect a project that already uses OpenSpec. Before the first step that writes anything (`new change`, `archive`, `sync specs`, or authoring an artifact file), confirm the project has a root: run `openspec status --json` and read `root.source`. Any value other than `implicit` means the project is set up; `"source": "implicit"` means it is not - there is no `openspec/` directory here, and the next write would create one. An error saying `No OpenSpec root found` means the same thing. In that case stop before writing and ask the user how to proceed: set this project up (`openspec init`), target a store they already have (`--store `), or drop OpenSpec for this request and help them directly. Wait for their answer. Do not run `openspec init` until they ask for it, do not hand-create `openspec/` files, and do not let a command create the root as a side effect. `` is the spec directory relative to `specs/` (for example, `user-auth` or `identity/user-auth`). Preserve the full path from each delta spec when resolving its main spec. diff --git a/skills/openspec-continue-change/SKILL.md b/skills/openspec-continue-change/SKILL.md index 92e91c4005..c6b16083eb 100644 --- a/skills/openspec-continue-change/SKILL.md +++ b/skills/openspec-continue-change/SKILL.md @@ -13,7 +13,7 @@ Continue working on a change by creating the next artifact. **Store selection:** If the user names a store (a store is a standalone OpenSpec repo registered on this machine) or the work lives in one, run `openspec store list --json` to discover registered store ids, then pass `--store ` on the commands that read or write specs and changes (`new change`, `status`, `instructions`, `list`, `show`, `validate`, `archive`, `doctor`, `context`, `schemas`, `view`). Once selected, treat `--store ` as sticky for the rest of the workflow. Every unscoped example of those commands below is shorthand: before running it, append the flag. For example, run `openspec status --change "" --json --store ""`, not the unscoped form shown below. Other commands do not take the flag. Hints printed by commands already carry the flag; keep it on follow-ups. Without a store, commands act on the nearest local `openspec/` root. -**Project check:** These steps expect a project that already uses OpenSpec. Before the first step that writes anything (`new change`, `archive`, `sync specs`, or authoring an artifact file), confirm the project has a root: run `openspec status --json` and read `root.source`. Any value other than `implicit` means the project is set up; `"source": "implicit"` means it is not - there is no `openspec/` directory here, and the next write would create one. An error saying `No OpenSpec root found` means the same thing. In that case stop before writing and ask the user how to proceed: set this project up (`openspec init`), target a store they already have (`--store `), or drop OpenSpec for this request and help them directly. Wait for their answer. Do not run `openspec init` on your own, do not hand-create `openspec/` files, and do not let a command create the root as a side effect. +**Project check:** These steps expect a project that already uses OpenSpec. Before the first step that writes anything (`new change`, `archive`, `sync specs`, or authoring an artifact file), confirm the project has a root: run `openspec status --json` and read `root.source`. Any value other than `implicit` means the project is set up; `"source": "implicit"` means it is not - there is no `openspec/` directory here, and the next write would create one. An error saying `No OpenSpec root found` means the same thing. In that case stop before writing and ask the user how to proceed: set this project up (`openspec init`), target a store they already have (`--store `), or drop OpenSpec for this request and help them directly. Wait for their answer. Do not run `openspec init` until they ask for it, do not hand-create `openspec/` files, and do not let a command create the root as a side effect. **Input**: Optionally specify a change name. If omitted, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes. diff --git a/skills/openspec-explore/SKILL.md b/skills/openspec-explore/SKILL.md index 1bc51e6af7..edb483d90f 100644 --- a/skills/openspec-explore/SKILL.md +++ b/skills/openspec-explore/SKILL.md @@ -17,7 +17,7 @@ Enter explore mode. Think deeply. Visualize freely. Follow the conversation wher **Store selection:** If the user names a store (a store is a standalone OpenSpec repo registered on this machine) or the work lives in one, run `openspec store list --json` to discover registered store ids, then pass `--store ` on the commands that read or write specs and changes (`new change`, `status`, `instructions`, `list`, `show`, `validate`, `archive`, `doctor`, `context`, `schemas`, `view`). Once selected, treat `--store ` as sticky for the rest of the workflow. Every unscoped example of those commands below is shorthand: before running it, append the flag. For example, run `openspec status --change "" --json --store ""`, not the unscoped form shown below. Other commands do not take the flag. Hints printed by commands already carry the flag; keep it on follow-ups. Without a store, commands act on the nearest local `openspec/` root. -**Project check:** These steps expect a project that already uses OpenSpec. Before the first step that writes anything (`new change`, `archive`, `sync specs`, or authoring an artifact file), confirm the project has a root: run `openspec status --json` and read `root.source`. Any value other than `implicit` means the project is set up; `"source": "implicit"` means it is not - there is no `openspec/` directory here, and the next write would create one. An error saying `No OpenSpec root found` means the same thing. In that case stop before writing and ask the user how to proceed: set this project up (`openspec init`), target a store they already have (`--store `), or drop OpenSpec for this request and help them directly. Wait for their answer. Do not run `openspec init` on your own, do not hand-create `openspec/` files, and do not let a command create the root as a side effect. +**Project check:** These steps expect a project that already uses OpenSpec. Before the first step that writes anything (`new change`, `archive`, `sync specs`, or authoring an artifact file), confirm the project has a root: run `openspec status --json` and read `root.source`. Any value other than `implicit` means the project is set up; `"source": "implicit"` means it is not - there is no `openspec/` directory here, and the next write would create one. An error saying `No OpenSpec root found` means the same thing. In that case stop before writing and ask the user how to proceed: set this project up (`openspec init`), target a store they already have (`--store `), or drop OpenSpec for this request and help them directly. Wait for their answer. Do not run `openspec init` until they ask for it, do not hand-create `openspec/` files, and do not let a command create the root as a side effect. --- diff --git a/skills/openspec-ff-change/SKILL.md b/skills/openspec-ff-change/SKILL.md index 28162c1a7d..d94f7edb6e 100644 --- a/skills/openspec-ff-change/SKILL.md +++ b/skills/openspec-ff-change/SKILL.md @@ -13,7 +13,7 @@ Fast-forward through artifact creation - generate everything needed to start imp **Store selection:** If the user names a store (a store is a standalone OpenSpec repo registered on this machine) or the work lives in one, run `openspec store list --json` to discover registered store ids, then pass `--store ` on the commands that read or write specs and changes (`new change`, `status`, `instructions`, `list`, `show`, `validate`, `archive`, `doctor`, `context`, `schemas`, `view`). Once selected, treat `--store ` as sticky for the rest of the workflow. Every unscoped example of those commands below is shorthand: before running it, append the flag. For example, run `openspec status --change "" --json --store ""`, not the unscoped form shown below. Other commands do not take the flag. Hints printed by commands already carry the flag; keep it on follow-ups. Without a store, commands act on the nearest local `openspec/` root. -**Project check:** These steps expect a project that already uses OpenSpec. Before the first step that writes anything (`new change`, `archive`, `sync specs`, or authoring an artifact file), confirm the project has a root: run `openspec status --json` and read `root.source`. Any value other than `implicit` means the project is set up; `"source": "implicit"` means it is not - there is no `openspec/` directory here, and the next write would create one. An error saying `No OpenSpec root found` means the same thing. In that case stop before writing and ask the user how to proceed: set this project up (`openspec init`), target a store they already have (`--store `), or drop OpenSpec for this request and help them directly. Wait for their answer. Do not run `openspec init` on your own, do not hand-create `openspec/` files, and do not let a command create the root as a side effect. +**Project check:** These steps expect a project that already uses OpenSpec. Before the first step that writes anything (`new change`, `archive`, `sync specs`, or authoring an artifact file), confirm the project has a root: run `openspec status --json` and read `root.source`. Any value other than `implicit` means the project is set up; `"source": "implicit"` means it is not - there is no `openspec/` directory here, and the next write would create one. An error saying `No OpenSpec root found` means the same thing. In that case stop before writing and ask the user how to proceed: set this project up (`openspec init`), target a store they already have (`--store `), or drop OpenSpec for this request and help them directly. Wait for their answer. Do not run `openspec init` until they ask for it, do not hand-create `openspec/` files, and do not let a command create the root as a side effect. **Input**: The user's request should include a change name (kebab-case) OR a description of what they want to build. diff --git a/skills/openspec-new-change/SKILL.md b/skills/openspec-new-change/SKILL.md index 81221cc8f4..728cf41fe1 100644 --- a/skills/openspec-new-change/SKILL.md +++ b/skills/openspec-new-change/SKILL.md @@ -13,7 +13,7 @@ Start a new change using the experimental artifact-driven approach. **Store selection:** If the user names a store (a store is a standalone OpenSpec repo registered on this machine) or the work lives in one, run `openspec store list --json` to discover registered store ids, then pass `--store ` on the commands that read or write specs and changes (`new change`, `status`, `instructions`, `list`, `show`, `validate`, `archive`, `doctor`, `context`, `schemas`, `view`). Once selected, treat `--store ` as sticky for the rest of the workflow. Every unscoped example of those commands below is shorthand: before running it, append the flag. For example, run `openspec status --change "" --json --store ""`, not the unscoped form shown below. Other commands do not take the flag. Hints printed by commands already carry the flag; keep it on follow-ups. Without a store, commands act on the nearest local `openspec/` root. -**Project check:** These steps expect a project that already uses OpenSpec. Before the first step that writes anything (`new change`, `archive`, `sync specs`, or authoring an artifact file), confirm the project has a root: run `openspec status --json` and read `root.source`. Any value other than `implicit` means the project is set up; `"source": "implicit"` means it is not - there is no `openspec/` directory here, and the next write would create one. An error saying `No OpenSpec root found` means the same thing. In that case stop before writing and ask the user how to proceed: set this project up (`openspec init`), target a store they already have (`--store `), or drop OpenSpec for this request and help them directly. Wait for their answer. Do not run `openspec init` on your own, do not hand-create `openspec/` files, and do not let a command create the root as a side effect. +**Project check:** These steps expect a project that already uses OpenSpec. Before the first step that writes anything (`new change`, `archive`, `sync specs`, or authoring an artifact file), confirm the project has a root: run `openspec status --json` and read `root.source`. Any value other than `implicit` means the project is set up; `"source": "implicit"` means it is not - there is no `openspec/` directory here, and the next write would create one. An error saying `No OpenSpec root found` means the same thing. In that case stop before writing and ask the user how to proceed: set this project up (`openspec init`), target a store they already have (`--store `), or drop OpenSpec for this request and help them directly. Wait for their answer. Do not run `openspec init` until they ask for it, do not hand-create `openspec/` files, and do not let a command create the root as a side effect. **Input**: The user's request should include a change name (kebab-case) OR a description of what they want to build. diff --git a/skills/openspec-onboard/SKILL.md b/skills/openspec-onboard/SKILL.md index e46a4e0b9c..19602194cb 100644 --- a/skills/openspec-onboard/SKILL.md +++ b/skills/openspec-onboard/SKILL.md @@ -13,7 +13,7 @@ Guide the user through their first complete OpenSpec workflow cycle. This is a t **Store selection:** If the user names a store (a store is a standalone OpenSpec repo registered on this machine) or the work lives in one, run `openspec store list --json` to discover registered store ids, then pass `--store ` on the commands that read or write specs and changes (`new change`, `status`, `instructions`, `list`, `show`, `validate`, `archive`, `doctor`, `context`, `schemas`, `view`). Once selected, treat `--store ` as sticky for the rest of the workflow. Every unscoped example of those commands below is shorthand: before running it, append the flag. For example, run `openspec status --change "" --json --store ""`, not the unscoped form shown below. Other commands do not take the flag. Hints printed by commands already carry the flag; keep it on follow-ups. Without a store, commands act on the nearest local `openspec/` root. -**Project check:** These steps expect a project that already uses OpenSpec. Before the first step that writes anything (`new change`, `archive`, `sync specs`, or authoring an artifact file), confirm the project has a root: run `openspec status --json` and read `root.source`. Any value other than `implicit` means the project is set up; `"source": "implicit"` means it is not - there is no `openspec/` directory here, and the next write would create one. An error saying `No OpenSpec root found` means the same thing. In that case stop before writing and ask the user how to proceed: set this project up (`openspec init`), target a store they already have (`--store `), or drop OpenSpec for this request and help them directly. Wait for their answer. Do not run `openspec init` on your own, do not hand-create `openspec/` files, and do not let a command create the root as a side effect. +**Project check:** These steps expect a project that already uses OpenSpec. Before the first step that writes anything (`new change`, `archive`, `sync specs`, or authoring an artifact file), confirm the project has a root: run `openspec status --json` and read `root.source`. Any value other than `implicit` means the project is set up; `"source": "implicit"` means it is not - there is no `openspec/` directory here, and the next write would create one. An error saying `No OpenSpec root found` means the same thing. In that case stop before writing and ask the user how to proceed: set this project up (`openspec init`), target a store they already have (`--store `), or drop OpenSpec for this request and help them directly. Wait for their answer. Do not run `openspec init` until they ask for it, do not hand-create `openspec/` files, and do not let a command create the root as a side effect. --- diff --git a/skills/openspec-propose/SKILL.md b/skills/openspec-propose/SKILL.md index e4a079231d..2c4514c1d8 100644 --- a/skills/openspec-propose/SKILL.md +++ b/skills/openspec-propose/SKILL.md @@ -27,7 +27,7 @@ When the user is ready to implement, they must start the apply workflow explicit **Store selection:** If the user names a store (a store is a standalone OpenSpec repo registered on this machine) or the work lives in one, run `openspec store list --json` to discover registered store ids, then pass `--store ` on the commands that read or write specs and changes (`new change`, `status`, `instructions`, `list`, `show`, `validate`, `archive`, `doctor`, `context`, `schemas`, `view`). Once selected, treat `--store ` as sticky for the rest of the workflow. Every unscoped example of those commands below is shorthand: before running it, append the flag. For example, run `openspec status --change "" --json --store ""`, not the unscoped form shown below. Other commands do not take the flag. Hints printed by commands already carry the flag; keep it on follow-ups. Without a store, commands act on the nearest local `openspec/` root. -**Project check:** These steps expect a project that already uses OpenSpec. Before the first step that writes anything (`new change`, `archive`, `sync specs`, or authoring an artifact file), confirm the project has a root: run `openspec status --json` and read `root.source`. Any value other than `implicit` means the project is set up; `"source": "implicit"` means it is not - there is no `openspec/` directory here, and the next write would create one. An error saying `No OpenSpec root found` means the same thing. In that case stop before writing and ask the user how to proceed: set this project up (`openspec init`), target a store they already have (`--store `), or drop OpenSpec for this request and help them directly. Wait for their answer. Do not run `openspec init` on your own, do not hand-create `openspec/` files, and do not let a command create the root as a side effect. +**Project check:** These steps expect a project that already uses OpenSpec. Before the first step that writes anything (`new change`, `archive`, `sync specs`, or authoring an artifact file), confirm the project has a root: run `openspec status --json` and read `root.source`. Any value other than `implicit` means the project is set up; `"source": "implicit"` means it is not - there is no `openspec/` directory here, and the next write would create one. An error saying `No OpenSpec root found` means the same thing. In that case stop before writing and ask the user how to proceed: set this project up (`openspec init`), target a store they already have (`--store `), or drop OpenSpec for this request and help them directly. Wait for their answer. Do not run `openspec init` until they ask for it, do not hand-create `openspec/` files, and do not let a command create the root as a side effect. **Input**: The user's request should include a change name (kebab-case) OR a description of what they want to build. diff --git a/skills/openspec-sync-specs/SKILL.md b/skills/openspec-sync-specs/SKILL.md index 244aa44aba..08fac5e448 100644 --- a/skills/openspec-sync-specs/SKILL.md +++ b/skills/openspec-sync-specs/SKILL.md @@ -15,7 +15,7 @@ This is an **agent-driven** operation - you will read delta specs and directly e **Store selection:** If the user names a store (a store is a standalone OpenSpec repo registered on this machine) or the work lives in one, run `openspec store list --json` to discover registered store ids, then pass `--store ` on the commands that read or write specs and changes (`new change`, `status`, `instructions`, `list`, `show`, `validate`, `archive`, `doctor`, `context`, `schemas`, `view`). Once selected, treat `--store ` as sticky for the rest of the workflow. Every unscoped example of those commands below is shorthand: before running it, append the flag. For example, run `openspec status --change "" --json --store ""`, not the unscoped form shown below. Other commands do not take the flag. Hints printed by commands already carry the flag; keep it on follow-ups. Without a store, commands act on the nearest local `openspec/` root. -**Project check:** These steps expect a project that already uses OpenSpec. Before the first step that writes anything (`new change`, `archive`, `sync specs`, or authoring an artifact file), confirm the project has a root: run `openspec status --json` and read `root.source`. Any value other than `implicit` means the project is set up; `"source": "implicit"` means it is not - there is no `openspec/` directory here, and the next write would create one. An error saying `No OpenSpec root found` means the same thing. In that case stop before writing and ask the user how to proceed: set this project up (`openspec init`), target a store they already have (`--store `), or drop OpenSpec for this request and help them directly. Wait for their answer. Do not run `openspec init` on your own, do not hand-create `openspec/` files, and do not let a command create the root as a side effect. +**Project check:** These steps expect a project that already uses OpenSpec. Before the first step that writes anything (`new change`, `archive`, `sync specs`, or authoring an artifact file), confirm the project has a root: run `openspec status --json` and read `root.source`. Any value other than `implicit` means the project is set up; `"source": "implicit"` means it is not - there is no `openspec/` directory here, and the next write would create one. An error saying `No OpenSpec root found` means the same thing. In that case stop before writing and ask the user how to proceed: set this project up (`openspec init`), target a store they already have (`--store `), or drop OpenSpec for this request and help them directly. Wait for their answer. Do not run `openspec init` until they ask for it, do not hand-create `openspec/` files, and do not let a command create the root as a side effect. `` is the spec directory relative to `specs/` (for example, `user-auth` or `identity/user-auth`). Preserve the full path from each delta spec when resolving its main spec. diff --git a/skills/openspec-update-change/SKILL.md b/skills/openspec-update-change/SKILL.md index 819a535534..1ec1530dea 100644 --- a/skills/openspec-update-change/SKILL.md +++ b/skills/openspec-update-change/SKILL.md @@ -13,7 +13,7 @@ Revise a change's existing planning artifacts and keep them coherent. Never edit **Store selection:** If the user names a store (a store is a standalone OpenSpec repo registered on this machine) or the work lives in one, run `openspec store list --json` to discover registered store ids, then pass `--store ` on the commands that read or write specs and changes (`new change`, `status`, `instructions`, `list`, `show`, `validate`, `archive`, `doctor`, `context`, `schemas`, `view`). Once selected, treat `--store ` as sticky for the rest of the workflow. Every unscoped example of those commands below is shorthand: before running it, append the flag. For example, run `openspec status --change "" --json --store ""`, not the unscoped form shown below. Other commands do not take the flag. Hints printed by commands already carry the flag; keep it on follow-ups. Without a store, commands act on the nearest local `openspec/` root. -**Project check:** These steps expect a project that already uses OpenSpec. Before the first step that writes anything (`new change`, `archive`, `sync specs`, or authoring an artifact file), confirm the project has a root: run `openspec status --json` and read `root.source`. Any value other than `implicit` means the project is set up; `"source": "implicit"` means it is not - there is no `openspec/` directory here, and the next write would create one. An error saying `No OpenSpec root found` means the same thing. In that case stop before writing and ask the user how to proceed: set this project up (`openspec init`), target a store they already have (`--store `), or drop OpenSpec for this request and help them directly. Wait for their answer. Do not run `openspec init` on your own, do not hand-create `openspec/` files, and do not let a command create the root as a side effect. +**Project check:** These steps expect a project that already uses OpenSpec. Before the first step that writes anything (`new change`, `archive`, `sync specs`, or authoring an artifact file), confirm the project has a root: run `openspec status --json` and read `root.source`. Any value other than `implicit` means the project is set up; `"source": "implicit"` means it is not - there is no `openspec/` directory here, and the next write would create one. An error saying `No OpenSpec root found` means the same thing. In that case stop before writing and ask the user how to proceed: set this project up (`openspec init`), target a store they already have (`--store `), or drop OpenSpec for this request and help them directly. Wait for their answer. Do not run `openspec init` until they ask for it, do not hand-create `openspec/` files, and do not let a command create the root as a side effect. **Input**: Optionally specify a change name. If omitted, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes. diff --git a/skills/openspec-verify-change/SKILL.md b/skills/openspec-verify-change/SKILL.md index 156a729710..5e9a2ed293 100644 --- a/skills/openspec-verify-change/SKILL.md +++ b/skills/openspec-verify-change/SKILL.md @@ -13,7 +13,7 @@ Verify that an implementation matches the change artifacts (specs, tasks, design **Store selection:** If the user names a store (a store is a standalone OpenSpec repo registered on this machine) or the work lives in one, run `openspec store list --json` to discover registered store ids, then pass `--store ` on the commands that read or write specs and changes (`new change`, `status`, `instructions`, `list`, `show`, `validate`, `archive`, `doctor`, `context`, `schemas`, `view`). Once selected, treat `--store ` as sticky for the rest of the workflow. Every unscoped example of those commands below is shorthand: before running it, append the flag. For example, run `openspec status --change "" --json --store ""`, not the unscoped form shown below. Other commands do not take the flag. Hints printed by commands already carry the flag; keep it on follow-ups. Without a store, commands act on the nearest local `openspec/` root. -**Project check:** These steps expect a project that already uses OpenSpec. Before the first step that writes anything (`new change`, `archive`, `sync specs`, or authoring an artifact file), confirm the project has a root: run `openspec status --json` and read `root.source`. Any value other than `implicit` means the project is set up; `"source": "implicit"` means it is not - there is no `openspec/` directory here, and the next write would create one. An error saying `No OpenSpec root found` means the same thing. In that case stop before writing and ask the user how to proceed: set this project up (`openspec init`), target a store they already have (`--store `), or drop OpenSpec for this request and help them directly. Wait for their answer. Do not run `openspec init` on your own, do not hand-create `openspec/` files, and do not let a command create the root as a side effect. +**Project check:** These steps expect a project that already uses OpenSpec. Before the first step that writes anything (`new change`, `archive`, `sync specs`, or authoring an artifact file), confirm the project has a root: run `openspec status --json` and read `root.source`. Any value other than `implicit` means the project is set up; `"source": "implicit"` means it is not - there is no `openspec/` directory here, and the next write would create one. An error saying `No OpenSpec root found` means the same thing. In that case stop before writing and ask the user how to proceed: set this project up (`openspec init`), target a store they already have (`--store `), or drop OpenSpec for this request and help them directly. Wait for their answer. Do not run `openspec init` until they ask for it, do not hand-create `openspec/` files, and do not let a command create the root as a side effect. **Input**: Optionally specify a change name. If omitted, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes. diff --git a/src/core/templates/workflows/project-root.ts b/src/core/templates/workflows/project-root.ts index 444c411482..86d30c16e0 100644 --- a/src/core/templates/workflows/project-root.ts +++ b/src/core/templates/workflows/project-root.ts @@ -11,4 +11,4 @@ * `root.source` before writing and hands the decision back to the user * instead of setting the project up on its own. */ -export const PROJECT_ROOT_GUARD = `**Project check:** These steps expect a project that already uses OpenSpec. Before the first step that writes anything (\`new change\`, \`archive\`, \`sync specs\`, or authoring an artifact file), confirm the project has a root: run \`openspec status --json\` and read \`root.source\`. Any value other than \`implicit\` means the project is set up; \`"source": "implicit"\` means it is not - there is no \`openspec/\` directory here, and the next write would create one. An error saying \`No OpenSpec root found\` means the same thing. In that case stop before writing and ask the user how to proceed: set this project up (\`openspec init\`), target a store they already have (\`--store \`), or drop OpenSpec for this request and help them directly. Wait for their answer. Do not run \`openspec init\` on your own, do not hand-create \`openspec/\` files, and do not let a command create the root as a side effect.`; +export const PROJECT_ROOT_GUARD = `**Project check:** These steps expect a project that already uses OpenSpec. Before the first step that writes anything (\`new change\`, \`archive\`, \`sync specs\`, or authoring an artifact file), confirm the project has a root: run \`openspec status --json\` and read \`root.source\`. Any value other than \`implicit\` means the project is set up; \`"source": "implicit"\` means it is not - there is no \`openspec/\` directory here, and the next write would create one. An error saying \`No OpenSpec root found\` means the same thing. In that case stop before writing and ask the user how to proceed: set this project up (\`openspec init\`), target a store they already have (\`--store \`), or drop OpenSpec for this request and help them directly. Wait for their answer. Do not run \`openspec init\` until they ask for it, do not hand-create \`openspec/\` files, and do not let a command create the root as a side effect.`; diff --git a/test/core/templates/project-root-guard.test.ts b/test/core/templates/project-root-guard.test.ts index 6ecce15c5c..4ff172051d 100644 --- a/test/core/templates/project-root-guard.test.ts +++ b/test/core/templates/project-root-guard.test.ts @@ -48,7 +48,7 @@ describe('project root guard', () => { it('hands the decision to the user instead of setting the project up', () => { expect(PROJECT_ROOT_GUARD).toContain('stop before writing and ask the user how to proceed'); expect(PROJECT_ROOT_GUARD).toContain('drop OpenSpec for this request and help them directly'); - expect(PROJECT_ROOT_GUARD).toContain('Do not run `openspec init` on your own'); + expect(PROJECT_ROOT_GUARD).toContain('Do not run `openspec init` until they ask for it'); expect(PROJECT_ROOT_GUARD).toContain('do not hand-create `openspec/` files'); expect(PROJECT_ROOT_GUARD).toContain( 'do not let a command create the root as a side effect' diff --git a/test/core/templates/skill-templates-parity.test.ts b/test/core/templates/skill-templates-parity.test.ts index f37f3458eb..45863159b5 100644 --- a/test/core/templates/skill-templates-parity.test.ts +++ b/test/core/templates/skill-templates-parity.test.ts @@ -38,46 +38,46 @@ import { import { STORE_SELECTION_GUIDANCE } from '../../../src/core/templates/workflows/store-selection.js'; const EXPECTED_FUNCTION_HASHES: Record = { - getExploreSkillTemplate: '5a15a8b69043633b72d8d84568b876c25ae31fe2d80159866e848418010f4d2c', - getNewChangeSkillTemplate: 'ab0c86bd0865297ffe567842961eb0c054050c0a658c63fa62971003242ae91a', - getContinueChangeSkillTemplate: '91db17a4f25257a03660006916c05b02fe3aaee003c8b3a3422e25878e1c78b8', - getApplyChangeSkillTemplate: '1b91edb905faf7bdc3dece7a0f11045196a07588aff993bc13b7d2033b81616f', - getFfChangeSkillTemplate: '1f6e63424492398ea9256bc106a4f9fa4fec0e8b6c82ba22cac5e08ac302273e', - getSyncSpecsSkillTemplate: 'b0859fbc99decde4c8cfcbe6c2954b61f04d42bd28e6087eae7806b5e6a042f7', - getOnboardSkillTemplate: '44bc5da670f227b824a9258964888f57e4295dec044cee3d227056ee362845a0', - getOpsxExploreCommandTemplate: '19eb801771f076b61464ab372249c4bd215994945403361f2e475c2e89d73aab', - getOpsxNewCommandTemplate: '8915b72135554de9f78d0274c2b111681fcf2ddc613058a4f78a63201f57c176', - getOpsxContinueCommandTemplate: '1f41c50b7b8a609a044c8f2d995dd4f8cb9bf6498853c00723ba10a458cace70', - getOpsxApplyCommandTemplate: '8d03568ee988d154ef6cde1bedf0ae3401aefe9ab558b8c1fa3a745fcd72cff3', - getOpsxFfCommandTemplate: '2a88278926e2a2ea3ab66e538b1ed344ea30050329626b875c7744995617970f', - getArchiveChangeSkillTemplate: '5a9fd7040c463f359c33239a12143e0767c1f8dcf6123e4861d9bbbc7db40dc4', - getBulkArchiveChangeSkillTemplate: 'c9aac2f8d7debd7990f581a840acbfe39aa80a44dc373646a1864ad6e4f23250', - getOpsxSyncCommandTemplate: '9267cb4ffa20a9c2886221636be07a62b7bd986843c713f77a87da0e4c6f975f', - getVerifyChangeSkillTemplate: 'e3f4f50eea12d758e6c0229922b467592bed95f1d8d8195ab43057272cd70811', - getOpsxArchiveCommandTemplate: '96f1e3a2d139cff703de3a8cea8727439db4413818ba5561b309d662c91cc7c1', - getOpsxOnboardCommandTemplate: '192de384df43e7a50ca413b947c861a0b6300ff7a9f4d12c06bcdc8cbc236414', - getOpsxBulkArchiveCommandTemplate: '4200eb1cb89a16b3dc99e2336731a9ada82767c98cba01b540ff33e783ac9555', - getOpsxVerifyCommandTemplate: '37aebc24996843e90868e012a5aa5ed535f793fa4e1cd26dcc63a0d6fd33a3f5', - getOpsxProposeSkillTemplate: '18ff3de460a02772f1ff15866c45ded16c74e50a5aa0fb007f8df7950777f389', - getOpsxProposeCommandTemplate: '822c134a40daa96d520fc5da439e03da47d1532dfe3d8e0287d4931b9530df0c', + getExploreSkillTemplate: '8fbadbc603d929a4db626d277fdee6a27a46fcb4f01fde354bbaf3cd717c3f28', + getNewChangeSkillTemplate: '77bdf20d2d58a49491f9a78c7a7081bcb26b9a12a3a07358b4abe59a5c6621b9', + getContinueChangeSkillTemplate: 'ad87fe61f9cee29e0490de71192e76d42a5af035b38e068e8847376562d41667', + getApplyChangeSkillTemplate: '5444afd511ed70300e6e155700d5ea4d10b12e865f115e9c1356245a14988d0d', + getFfChangeSkillTemplate: '5a0f84b517a7bae25f53c3d6a228e8302c763d20cad4518e1e3caade54c16918', + getSyncSpecsSkillTemplate: '7c11770beb47888c684668593c9b6b9054ab828f5c65f9a0de165156af009cf3', + getOnboardSkillTemplate: 'a198b904818bcea66cc0ffa0d9750fc6dd12fe2d8af8b31e5b98a187aa99bf67', + getOpsxExploreCommandTemplate: '58a68aadc1b3f108a4a16abfb9820698c8f9fe9af41c8b6fd1fcb8f3a3ba4d90', + getOpsxNewCommandTemplate: '74cfc988862ae23a883375c3b703754c398d14e2554b9b3bf89e71eb017905cb', + getOpsxContinueCommandTemplate: 'e1d6b4473233d6f33fef0f97a96d33fef92881a8cc27a8db485872cd2b716c99', + getOpsxApplyCommandTemplate: '20a9b8c7868bd67a25c2296aef41e12982821c1c2af3385d5a008a1188ee98d0', + getOpsxFfCommandTemplate: 'e62ce1913654bdcf61a0eb5056992b3d4b1a057f4c20f72002da2476c2182717', + getArchiveChangeSkillTemplate: 'f71012ff02663afa01c2ee6d8235d66139572cb1b40db5a6372a3b5b52facc60', + getBulkArchiveChangeSkillTemplate: 'de2f38934910271786025303d5110b1cbefd4d66fc31dfc704abe1e1e111fe8d', + getOpsxSyncCommandTemplate: '005939610270e9fe48abddf79e5521c4a9d92adea244e5e093217445279c3f4b', + getVerifyChangeSkillTemplate: '548feb55b4b4651c1b7a0ddf06d362375597aded97aac98b4fc479bd3277ff07', + getOpsxArchiveCommandTemplate: '607c6afc4366a7c502ba2bf5aa95440c2c803c0ed8e8fe9a80a2824db0b27446', + getOpsxOnboardCommandTemplate: '1c3d0e3d5bcf02476b36b9a6c8d6192e4c6389916a41ad39b349c61b4ec85078', + getOpsxBulkArchiveCommandTemplate: '1e447cdbf8b47ecd64f08a6bfb2e74e1c36186e57bc2b0088f6e1f0765a26c82', + getOpsxVerifyCommandTemplate: 'b55d27b4e6915718305175154700306d746be1e1d0716e13ab5bf0d613e46239', + getOpsxProposeSkillTemplate: '73cde4b2becc257e0270b9fdd28f018f48a9669bc90ba7056b8c4c940c305fca', + getOpsxProposeCommandTemplate: '5194c2d0ca41f0ab70bcd3fcb1d0259fa84418352b85bcdeb64028cc348ee045', getFeedbackSkillTemplate: 'dabeb5e825b9349abc8156c3e7b8608f27987912a6d9bf47ef29addde6138133', - getUpdateChangeSkillTemplate: 'd1b3e5ab9f2d84860a12af23d4ebc88e25967af15e9419fa7e27daff3c103e4a', - getOpsxUpdateCommandTemplate: '98b7a5ca79fdcd1876b34809994a2e00113b883ed5e7fa8f300f9ebb76e6291a', + getUpdateChangeSkillTemplate: '5502735e4a8a2fef9ba2469a742922f45de42a9b6dbde075d6a181d1f8f8a1f6', + getOpsxUpdateCommandTemplate: 'fa903cce48116b86e913044781237716117e4a941614233783f6054e7cb56ff1', }; const EXPECTED_GENERATED_SKILL_CONTENT_HASHES: Record = { - 'openspec-explore': '9fc548a718cf555f411f9d4981723556bc52ffef2c117ef8444fd86a2b0ef27f', - 'openspec-new-change': '0f05ae5d75148a5b5cef9397697b6cfcef88cea8331682c44213e44674cb3fbb', - 'openspec-continue-change': '70411b49d0c1b73383c742016b13710e22bbd3ecd3fc3592b112e7c343ffef5c', - 'openspec-apply-change': 'ac12fbfaee2898cc0051962d422c450c835916f4a3b7fd4796cb1c82187c11ea', - 'openspec-ff-change': '8ebb799188c8d4b75bec852e2e70ce86fe8e2ceb6f52f214bd74523169593dde', - 'openspec-sync-specs': '14724b88c194197fb5c80a31b1eb9c8a5a89a94e97818fd63e59fdfa4b8a17dc', - 'openspec-archive-change': '1ac1cfae5918de243afa3502ed0cd3a54bb4ec0cde2e6468df1563a82bc59679', - 'openspec-bulk-archive-change': 'a021488b41fae7b9296656e62cf50a47fd04a3c0de3641953eafd3edc9d63bfa', - 'openspec-verify-change': 'd9b1dc240650d33ebb7358b8deb202c93b7cc0ff89187e5345624f2ee1a6910d', - 'openspec-onboard': '4dcc17be6b883457f264e417a57eb96c1aa92a91f2afaa8223658a220e7c51f5', - 'openspec-propose': 'b5be3ba1f342a047d0b1d8b7c4a0d9b30fb24112e7a16a18dcbc0f4026f9aa1a', - 'openspec-update-change': 'a62eefcd70b6e239c377ce73c8eae224b5413b634bdd80658e10594e42115022', + 'openspec-explore': '508e93e4831d6866a523cc10c732018f1133e0da68f2113e32b346c71add83a5', + 'openspec-new-change': '8c09c677488dc567d6810ac44d1beaa4263159c95a1d2164a8945f45670c1fee', + 'openspec-continue-change': '5ebcfafb024e9033bac8d9369fe004ee8db03654ca0fc46fb8d068a2a71b7409', + 'openspec-apply-change': 'be1326080d11a1a4db3e916bfcc0b35680e582706633ef407d396e03564252e6', + 'openspec-ff-change': '5a87b3827bb420f06235a1fe8aaaa133ddffd3fb83d1ef70ae4f8ed65a00cad3', + 'openspec-sync-specs': '9902dbcc38b03bdaff06c1a4c1c2c1c4c8a9c96fdc24e53ee366063716a2e12c', + 'openspec-archive-change': '659f729e27764bed59f8ef11b2dadf928797b44d14c6ae9e5a72d37b19dc5686', + 'openspec-bulk-archive-change': '2ce95b813ef3cc6b05f40cda1eafeea9553dc573f4e02356d98cfbf9bfbc5f35', + 'openspec-verify-change': 'c7aeca640011957f319636a217e3a43b73e5160639b29ff65f7945b30eb38120', + 'openspec-onboard': 'c0448d0070d4628e6168ed09da17eaf442e043f357095966eb00c0c63f2a34d9', + 'openspec-propose': 'f808354d0a779718651275af4337ec941d7445cd49cfe31198f41a178b19d1c0', + 'openspec-update-change': '6dd12af78bf32f19779026d76de5ace961360985b462c2a237d5b9260cf4470c', }; // Intentionally excludes getFeedbackSkillTemplate: this list only models templates From 382032e9d6537096a0807625ffb1e335e168061c Mon Sep 17 00:00:00 2001 From: Clay Good Date: Fri, 4 Sep 2026 12:05:24 -0500 Subject: [PATCH 04/15] docs(troubleshooting): explain an OpenSpec workflow starting in an unset-up project Co-Authored-By: Claude Opus 5 --- docs/troubleshooting.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index f35ada883a..65d3739ddf 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -37,6 +37,12 @@ openspec init --tools claude,cursor The full list of tool IDs is in [Supported Tools](supported-tools.md). Use `--tools all` for everything, `--tools none` to skip tool setup. +### The AI started an OpenSpec workflow in a project that doesn't use OpenSpec + +Skills and commands are installed once per tool, so they are offered in every project you open. If you ask for a proposal in a project that never ran `openspec init`, the assistant stops and asks whether to set the project up, work against a store (`--store `), or drop OpenSpec for that request. + +If it went ahead anyway, it is running older instruction files: upgrade the CLI, run `openspec update` in your OpenSpec projects, and restart your assistant. To check a directory yourself, run `openspec status --json` — a `root.source` of `implicit` means there is no `openspec/` directory here and the next write would create one. An `openspec/` directory created this way is safe to delete if you didn't want it. + ## Commands don't show up If `/opsx:propose` (or your tool's equivalent) doesn't appear or doesn't do anything, work down this list. They're ordered fastest-to-check first. From 30bb18a8b9582449720a5c12e7fd2703bae6a78d Mon Sep 17 00:00:00 2001 From: Clay Good Date: Fri, 4 Sep 2026 12:07:51 -0500 Subject: [PATCH 05/15] 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 --- .changeset/guard-uninitialized-projects.md | 2 +- docs/troubleshooting.md | 2 +- skills/openspec-apply-change/SKILL.md | 2 +- skills/openspec-archive-change/SKILL.md | 2 +- skills/openspec-bulk-archive-change/SKILL.md | 2 +- skills/openspec-continue-change/SKILL.md | 2 +- skills/openspec-explore/SKILL.md | 2 +- skills/openspec-ff-change/SKILL.md | 2 +- skills/openspec-new-change/SKILL.md | 2 +- skills/openspec-onboard/SKILL.md | 2 +- skills/openspec-propose/SKILL.md | 2 +- skills/openspec-sync-specs/SKILL.md | 2 +- skills/openspec-update-change/SKILL.md | 2 +- skills/openspec-verify-change/SKILL.md | 2 +- src/core/templates/workflows/project-root.ts | 10 +-- test/commands/store-root-selection.test.ts | 22 ++++++ .../core/templates/project-root-guard.test.ts | 9 +-- .../templates/skill-templates-parity.test.ts | 72 +++++++++---------- 18 files changed, 83 insertions(+), 58 deletions(-) diff --git a/.changeset/guard-uninitialized-projects.md b/.changeset/guard-uninitialized-projects.md index af612f048d..01b1a5dad8 100644 --- a/.changeset/guard-uninitialized-projects.md +++ b/.changeset/guard-uninitialized-projects.md @@ -4,4 +4,4 @@ ### Bug Fixes -- Generated skills and commands no longer adopt a project that never ran `openspec init`. Every workflow now checks `root.source` from `openspec status --json` before its first write: an implicit root (or a `No OpenSpec root found` error) means the project is not set up, so the agent stops and asks whether to initialize it, target a store, or handle the request without OpenSpec, instead of letting `openspec new change` create `openspec/` in the current directory as a side effect. Skill descriptions now name OpenSpec so hosts stop offering these workflows in unrelated repositories. +- Generated skills and commands no longer adopt a project that never ran `openspec init`. Every workflow now checks `root` from `openspec list --json` before its first write: `"root": null` means the project is not set up, so the agent stops and asks whether to initialize it, target a store, or handle the request without OpenSpec, instead of letting `openspec new change` create `openspec/` in the current directory as a side effect. Skill descriptions now name OpenSpec so hosts stop offering these workflows in unrelated repositories. diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index 65d3739ddf..37aac1bcd1 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -41,7 +41,7 @@ The full list of tool IDs is in [Supported Tools](supported-tools.md). Use `--to Skills and commands are installed once per tool, so they are offered in every project you open. If you ask for a proposal in a project that never ran `openspec init`, the assistant stops and asks whether to set the project up, work against a store (`--store `), or drop OpenSpec for that request. -If it went ahead anyway, it is running older instruction files: upgrade the CLI, run `openspec update` in your OpenSpec projects, and restart your assistant. To check a directory yourself, run `openspec status --json` — a `root.source` of `implicit` means there is no `openspec/` directory here and the next write would create one. An `openspec/` directory created this way is safe to delete if you didn't want it. +If it went ahead anyway, it is running older instruction files: upgrade the CLI, run `openspec update` in your OpenSpec projects, and restart your assistant. To check a directory yourself, run `openspec list --json` — `"root": null` means there is no `openspec/` directory here, and a command like `openspec new change` would create one. An `openspec/` directory created this way is safe to delete if you didn't want it. ## Commands don't show up diff --git a/skills/openspec-apply-change/SKILL.md b/skills/openspec-apply-change/SKILL.md index aaee85d684..945615acf4 100644 --- a/skills/openspec-apply-change/SKILL.md +++ b/skills/openspec-apply-change/SKILL.md @@ -13,7 +13,7 @@ Implement tasks from an OpenSpec change. **Store selection:** If the user names a store (a store is a standalone OpenSpec repo registered on this machine) or the work lives in one, run `openspec store list --json` to discover registered store ids, then pass `--store ` on the commands that read or write specs and changes (`new change`, `status`, `instructions`, `list`, `show`, `validate`, `archive`, `doctor`, `context`, `schemas`, `view`). Once selected, treat `--store ` as sticky for the rest of the workflow. Every unscoped example of those commands below is shorthand: before running it, append the flag. For example, run `openspec status --change "" --json --store ""`, not the unscoped form shown below. Other commands do not take the flag. Hints printed by commands already carry the flag; keep it on follow-ups. Without a store, commands act on the nearest local `openspec/` root. -**Project check:** These steps expect a project that already uses OpenSpec. Before the first step that writes anything (`new change`, `archive`, `sync specs`, or authoring an artifact file), confirm the project has a root: run `openspec status --json` and read `root.source`. Any value other than `implicit` means the project is set up; `"source": "implicit"` means it is not - there is no `openspec/` directory here, and the next write would create one. An error saying `No OpenSpec root found` means the same thing. In that case stop before writing and ask the user how to proceed: set this project up (`openspec init`), target a store they already have (`--store `), or drop OpenSpec for this request and help them directly. Wait for their answer. Do not run `openspec init` until they ask for it, do not hand-create `openspec/` files, and do not let a command create the root as a side effect. +**Project check:** These steps expect a project that already uses OpenSpec. Before the first step that writes anything (`new change`, `archive`, `sync specs`, or authoring an artifact file), confirm the project has a root: run `openspec list --json` and read `root`. A root object means the project is set up. `"root": null` means it is not - there is no `openspec/` directory here, and a write such as `openspec new change` would create one as a side effect. In that case stop before writing and ask the user how to proceed: set this project up (`openspec init`), target a store they already have (`--store `), or drop OpenSpec for this request and help them directly. Wait for their answer. Do not run `openspec init` until they ask for it, do not hand-create `openspec/` files, and do not let a command create the root as a side effect. **Input**: Optionally specify a change name (e.g., `/openspec-apply-change add-auth`). If omitted, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes. diff --git a/skills/openspec-archive-change/SKILL.md b/skills/openspec-archive-change/SKILL.md index 31b7b9914a..8936eb96cc 100644 --- a/skills/openspec-archive-change/SKILL.md +++ b/skills/openspec-archive-change/SKILL.md @@ -13,7 +13,7 @@ Archive a completed change in the experimental workflow. **Store selection:** If the user names a store (a store is a standalone OpenSpec repo registered on this machine) or the work lives in one, run `openspec store list --json` to discover registered store ids, then pass `--store ` on the commands that read or write specs and changes (`new change`, `status`, `instructions`, `list`, `show`, `validate`, `archive`, `doctor`, `context`, `schemas`, `view`). Once selected, treat `--store ` as sticky for the rest of the workflow. Every unscoped example of those commands below is shorthand: before running it, append the flag. For example, run `openspec status --change "" --json --store ""`, not the unscoped form shown below. Other commands do not take the flag. Hints printed by commands already carry the flag; keep it on follow-ups. Without a store, commands act on the nearest local `openspec/` root. -**Project check:** These steps expect a project that already uses OpenSpec. Before the first step that writes anything (`new change`, `archive`, `sync specs`, or authoring an artifact file), confirm the project has a root: run `openspec status --json` and read `root.source`. Any value other than `implicit` means the project is set up; `"source": "implicit"` means it is not - there is no `openspec/` directory here, and the next write would create one. An error saying `No OpenSpec root found` means the same thing. In that case stop before writing and ask the user how to proceed: set this project up (`openspec init`), target a store they already have (`--store `), or drop OpenSpec for this request and help them directly. Wait for their answer. Do not run `openspec init` until they ask for it, do not hand-create `openspec/` files, and do not let a command create the root as a side effect. +**Project check:** These steps expect a project that already uses OpenSpec. Before the first step that writes anything (`new change`, `archive`, `sync specs`, or authoring an artifact file), confirm the project has a root: run `openspec list --json` and read `root`. A root object means the project is set up. `"root": null` means it is not - there is no `openspec/` directory here, and a write such as `openspec new change` would create one as a side effect. In that case stop before writing and ask the user how to proceed: set this project up (`openspec init`), target a store they already have (`--store `), or drop OpenSpec for this request and help them directly. Wait for their answer. Do not run `openspec init` until they ask for it, do not hand-create `openspec/` files, and do not let a command create the root as a side effect. `` is the spec directory relative to `specs/` (for example, `user-auth` or `identity/user-auth`). Preserve the full path from each delta spec when resolving its main spec. diff --git a/skills/openspec-bulk-archive-change/SKILL.md b/skills/openspec-bulk-archive-change/SKILL.md index 48797a9145..143cb8c2ee 100644 --- a/skills/openspec-bulk-archive-change/SKILL.md +++ b/skills/openspec-bulk-archive-change/SKILL.md @@ -15,7 +15,7 @@ This skill allows you to batch-archive changes, handling spec conflicts intellig **Store selection:** If the user names a store (a store is a standalone OpenSpec repo registered on this machine) or the work lives in one, run `openspec store list --json` to discover registered store ids, then pass `--store ` on the commands that read or write specs and changes (`new change`, `status`, `instructions`, `list`, `show`, `validate`, `archive`, `doctor`, `context`, `schemas`, `view`). Once selected, treat `--store ` as sticky for the rest of the workflow. Every unscoped example of those commands below is shorthand: before running it, append the flag. For example, run `openspec status --change "" --json --store ""`, not the unscoped form shown below. Other commands do not take the flag. Hints printed by commands already carry the flag; keep it on follow-ups. Without a store, commands act on the nearest local `openspec/` root. -**Project check:** These steps expect a project that already uses OpenSpec. Before the first step that writes anything (`new change`, `archive`, `sync specs`, or authoring an artifact file), confirm the project has a root: run `openspec status --json` and read `root.source`. Any value other than `implicit` means the project is set up; `"source": "implicit"` means it is not - there is no `openspec/` directory here, and the next write would create one. An error saying `No OpenSpec root found` means the same thing. In that case stop before writing and ask the user how to proceed: set this project up (`openspec init`), target a store they already have (`--store `), or drop OpenSpec for this request and help them directly. Wait for their answer. Do not run `openspec init` until they ask for it, do not hand-create `openspec/` files, and do not let a command create the root as a side effect. +**Project check:** These steps expect a project that already uses OpenSpec. Before the first step that writes anything (`new change`, `archive`, `sync specs`, or authoring an artifact file), confirm the project has a root: run `openspec list --json` and read `root`. A root object means the project is set up. `"root": null` means it is not - there is no `openspec/` directory here, and a write such as `openspec new change` would create one as a side effect. In that case stop before writing and ask the user how to proceed: set this project up (`openspec init`), target a store they already have (`--store `), or drop OpenSpec for this request and help them directly. Wait for their answer. Do not run `openspec init` until they ask for it, do not hand-create `openspec/` files, and do not let a command create the root as a side effect. `` is the spec directory relative to `specs/` (for example, `user-auth` or `identity/user-auth`). Preserve the full path from each delta spec when resolving its main spec. diff --git a/skills/openspec-continue-change/SKILL.md b/skills/openspec-continue-change/SKILL.md index c6b16083eb..ee1ee84088 100644 --- a/skills/openspec-continue-change/SKILL.md +++ b/skills/openspec-continue-change/SKILL.md @@ -13,7 +13,7 @@ Continue working on a change by creating the next artifact. **Store selection:** If the user names a store (a store is a standalone OpenSpec repo registered on this machine) or the work lives in one, run `openspec store list --json` to discover registered store ids, then pass `--store ` on the commands that read or write specs and changes (`new change`, `status`, `instructions`, `list`, `show`, `validate`, `archive`, `doctor`, `context`, `schemas`, `view`). Once selected, treat `--store ` as sticky for the rest of the workflow. Every unscoped example of those commands below is shorthand: before running it, append the flag. For example, run `openspec status --change "" --json --store ""`, not the unscoped form shown below. Other commands do not take the flag. Hints printed by commands already carry the flag; keep it on follow-ups. Without a store, commands act on the nearest local `openspec/` root. -**Project check:** These steps expect a project that already uses OpenSpec. Before the first step that writes anything (`new change`, `archive`, `sync specs`, or authoring an artifact file), confirm the project has a root: run `openspec status --json` and read `root.source`. Any value other than `implicit` means the project is set up; `"source": "implicit"` means it is not - there is no `openspec/` directory here, and the next write would create one. An error saying `No OpenSpec root found` means the same thing. In that case stop before writing and ask the user how to proceed: set this project up (`openspec init`), target a store they already have (`--store `), or drop OpenSpec for this request and help them directly. Wait for their answer. Do not run `openspec init` until they ask for it, do not hand-create `openspec/` files, and do not let a command create the root as a side effect. +**Project check:** These steps expect a project that already uses OpenSpec. Before the first step that writes anything (`new change`, `archive`, `sync specs`, or authoring an artifact file), confirm the project has a root: run `openspec list --json` and read `root`. A root object means the project is set up. `"root": null` means it is not - there is no `openspec/` directory here, and a write such as `openspec new change` would create one as a side effect. In that case stop before writing and ask the user how to proceed: set this project up (`openspec init`), target a store they already have (`--store `), or drop OpenSpec for this request and help them directly. Wait for their answer. Do not run `openspec init` until they ask for it, do not hand-create `openspec/` files, and do not let a command create the root as a side effect. **Input**: Optionally specify a change name. If omitted, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes. diff --git a/skills/openspec-explore/SKILL.md b/skills/openspec-explore/SKILL.md index edb483d90f..c1287d98ac 100644 --- a/skills/openspec-explore/SKILL.md +++ b/skills/openspec-explore/SKILL.md @@ -17,7 +17,7 @@ Enter explore mode. Think deeply. Visualize freely. Follow the conversation wher **Store selection:** If the user names a store (a store is a standalone OpenSpec repo registered on this machine) or the work lives in one, run `openspec store list --json` to discover registered store ids, then pass `--store ` on the commands that read or write specs and changes (`new change`, `status`, `instructions`, `list`, `show`, `validate`, `archive`, `doctor`, `context`, `schemas`, `view`). Once selected, treat `--store ` as sticky for the rest of the workflow. Every unscoped example of those commands below is shorthand: before running it, append the flag. For example, run `openspec status --change "" --json --store ""`, not the unscoped form shown below. Other commands do not take the flag. Hints printed by commands already carry the flag; keep it on follow-ups. Without a store, commands act on the nearest local `openspec/` root. -**Project check:** These steps expect a project that already uses OpenSpec. Before the first step that writes anything (`new change`, `archive`, `sync specs`, or authoring an artifact file), confirm the project has a root: run `openspec status --json` and read `root.source`. Any value other than `implicit` means the project is set up; `"source": "implicit"` means it is not - there is no `openspec/` directory here, and the next write would create one. An error saying `No OpenSpec root found` means the same thing. In that case stop before writing and ask the user how to proceed: set this project up (`openspec init`), target a store they already have (`--store `), or drop OpenSpec for this request and help them directly. Wait for their answer. Do not run `openspec init` until they ask for it, do not hand-create `openspec/` files, and do not let a command create the root as a side effect. +**Project check:** These steps expect a project that already uses OpenSpec. Before the first step that writes anything (`new change`, `archive`, `sync specs`, or authoring an artifact file), confirm the project has a root: run `openspec list --json` and read `root`. A root object means the project is set up. `"root": null` means it is not - there is no `openspec/` directory here, and a write such as `openspec new change` would create one as a side effect. In that case stop before writing and ask the user how to proceed: set this project up (`openspec init`), target a store they already have (`--store `), or drop OpenSpec for this request and help them directly. Wait for their answer. Do not run `openspec init` until they ask for it, do not hand-create `openspec/` files, and do not let a command create the root as a side effect. --- diff --git a/skills/openspec-ff-change/SKILL.md b/skills/openspec-ff-change/SKILL.md index d94f7edb6e..3a8fc2a1cc 100644 --- a/skills/openspec-ff-change/SKILL.md +++ b/skills/openspec-ff-change/SKILL.md @@ -13,7 +13,7 @@ Fast-forward through artifact creation - generate everything needed to start imp **Store selection:** If the user names a store (a store is a standalone OpenSpec repo registered on this machine) or the work lives in one, run `openspec store list --json` to discover registered store ids, then pass `--store ` on the commands that read or write specs and changes (`new change`, `status`, `instructions`, `list`, `show`, `validate`, `archive`, `doctor`, `context`, `schemas`, `view`). Once selected, treat `--store ` as sticky for the rest of the workflow. Every unscoped example of those commands below is shorthand: before running it, append the flag. For example, run `openspec status --change "" --json --store ""`, not the unscoped form shown below. Other commands do not take the flag. Hints printed by commands already carry the flag; keep it on follow-ups. Without a store, commands act on the nearest local `openspec/` root. -**Project check:** These steps expect a project that already uses OpenSpec. Before the first step that writes anything (`new change`, `archive`, `sync specs`, or authoring an artifact file), confirm the project has a root: run `openspec status --json` and read `root.source`. Any value other than `implicit` means the project is set up; `"source": "implicit"` means it is not - there is no `openspec/` directory here, and the next write would create one. An error saying `No OpenSpec root found` means the same thing. In that case stop before writing and ask the user how to proceed: set this project up (`openspec init`), target a store they already have (`--store `), or drop OpenSpec for this request and help them directly. Wait for their answer. Do not run `openspec init` until they ask for it, do not hand-create `openspec/` files, and do not let a command create the root as a side effect. +**Project check:** These steps expect a project that already uses OpenSpec. Before the first step that writes anything (`new change`, `archive`, `sync specs`, or authoring an artifact file), confirm the project has a root: run `openspec list --json` and read `root`. A root object means the project is set up. `"root": null` means it is not - there is no `openspec/` directory here, and a write such as `openspec new change` would create one as a side effect. In that case stop before writing and ask the user how to proceed: set this project up (`openspec init`), target a store they already have (`--store `), or drop OpenSpec for this request and help them directly. Wait for their answer. Do not run `openspec init` until they ask for it, do not hand-create `openspec/` files, and do not let a command create the root as a side effect. **Input**: The user's request should include a change name (kebab-case) OR a description of what they want to build. diff --git a/skills/openspec-new-change/SKILL.md b/skills/openspec-new-change/SKILL.md index 728cf41fe1..97eabc5fed 100644 --- a/skills/openspec-new-change/SKILL.md +++ b/skills/openspec-new-change/SKILL.md @@ -13,7 +13,7 @@ Start a new change using the experimental artifact-driven approach. **Store selection:** If the user names a store (a store is a standalone OpenSpec repo registered on this machine) or the work lives in one, run `openspec store list --json` to discover registered store ids, then pass `--store ` on the commands that read or write specs and changes (`new change`, `status`, `instructions`, `list`, `show`, `validate`, `archive`, `doctor`, `context`, `schemas`, `view`). Once selected, treat `--store ` as sticky for the rest of the workflow. Every unscoped example of those commands below is shorthand: before running it, append the flag. For example, run `openspec status --change "" --json --store ""`, not the unscoped form shown below. Other commands do not take the flag. Hints printed by commands already carry the flag; keep it on follow-ups. Without a store, commands act on the nearest local `openspec/` root. -**Project check:** These steps expect a project that already uses OpenSpec. Before the first step that writes anything (`new change`, `archive`, `sync specs`, or authoring an artifact file), confirm the project has a root: run `openspec status --json` and read `root.source`. Any value other than `implicit` means the project is set up; `"source": "implicit"` means it is not - there is no `openspec/` directory here, and the next write would create one. An error saying `No OpenSpec root found` means the same thing. In that case stop before writing and ask the user how to proceed: set this project up (`openspec init`), target a store they already have (`--store `), or drop OpenSpec for this request and help them directly. Wait for their answer. Do not run `openspec init` until they ask for it, do not hand-create `openspec/` files, and do not let a command create the root as a side effect. +**Project check:** These steps expect a project that already uses OpenSpec. Before the first step that writes anything (`new change`, `archive`, `sync specs`, or authoring an artifact file), confirm the project has a root: run `openspec list --json` and read `root`. A root object means the project is set up. `"root": null` means it is not - there is no `openspec/` directory here, and a write such as `openspec new change` would create one as a side effect. In that case stop before writing and ask the user how to proceed: set this project up (`openspec init`), target a store they already have (`--store `), or drop OpenSpec for this request and help them directly. Wait for their answer. Do not run `openspec init` until they ask for it, do not hand-create `openspec/` files, and do not let a command create the root as a side effect. **Input**: The user's request should include a change name (kebab-case) OR a description of what they want to build. diff --git a/skills/openspec-onboard/SKILL.md b/skills/openspec-onboard/SKILL.md index 19602194cb..d22dac0c0e 100644 --- a/skills/openspec-onboard/SKILL.md +++ b/skills/openspec-onboard/SKILL.md @@ -13,7 +13,7 @@ Guide the user through their first complete OpenSpec workflow cycle. This is a t **Store selection:** If the user names a store (a store is a standalone OpenSpec repo registered on this machine) or the work lives in one, run `openspec store list --json` to discover registered store ids, then pass `--store ` on the commands that read or write specs and changes (`new change`, `status`, `instructions`, `list`, `show`, `validate`, `archive`, `doctor`, `context`, `schemas`, `view`). Once selected, treat `--store ` as sticky for the rest of the workflow. Every unscoped example of those commands below is shorthand: before running it, append the flag. For example, run `openspec status --change "" --json --store ""`, not the unscoped form shown below. Other commands do not take the flag. Hints printed by commands already carry the flag; keep it on follow-ups. Without a store, commands act on the nearest local `openspec/` root. -**Project check:** These steps expect a project that already uses OpenSpec. Before the first step that writes anything (`new change`, `archive`, `sync specs`, or authoring an artifact file), confirm the project has a root: run `openspec status --json` and read `root.source`. Any value other than `implicit` means the project is set up; `"source": "implicit"` means it is not - there is no `openspec/` directory here, and the next write would create one. An error saying `No OpenSpec root found` means the same thing. In that case stop before writing and ask the user how to proceed: set this project up (`openspec init`), target a store they already have (`--store `), or drop OpenSpec for this request and help them directly. Wait for their answer. Do not run `openspec init` until they ask for it, do not hand-create `openspec/` files, and do not let a command create the root as a side effect. +**Project check:** These steps expect a project that already uses OpenSpec. Before the first step that writes anything (`new change`, `archive`, `sync specs`, or authoring an artifact file), confirm the project has a root: run `openspec list --json` and read `root`. A root object means the project is set up. `"root": null` means it is not - there is no `openspec/` directory here, and a write such as `openspec new change` would create one as a side effect. In that case stop before writing and ask the user how to proceed: set this project up (`openspec init`), target a store they already have (`--store `), or drop OpenSpec for this request and help them directly. Wait for their answer. Do not run `openspec init` until they ask for it, do not hand-create `openspec/` files, and do not let a command create the root as a side effect. --- diff --git a/skills/openspec-propose/SKILL.md b/skills/openspec-propose/SKILL.md index 2c4514c1d8..4587e39472 100644 --- a/skills/openspec-propose/SKILL.md +++ b/skills/openspec-propose/SKILL.md @@ -27,7 +27,7 @@ When the user is ready to implement, they must start the apply workflow explicit **Store selection:** If the user names a store (a store is a standalone OpenSpec repo registered on this machine) or the work lives in one, run `openspec store list --json` to discover registered store ids, then pass `--store ` on the commands that read or write specs and changes (`new change`, `status`, `instructions`, `list`, `show`, `validate`, `archive`, `doctor`, `context`, `schemas`, `view`). Once selected, treat `--store ` as sticky for the rest of the workflow. Every unscoped example of those commands below is shorthand: before running it, append the flag. For example, run `openspec status --change "" --json --store ""`, not the unscoped form shown below. Other commands do not take the flag. Hints printed by commands already carry the flag; keep it on follow-ups. Without a store, commands act on the nearest local `openspec/` root. -**Project check:** These steps expect a project that already uses OpenSpec. Before the first step that writes anything (`new change`, `archive`, `sync specs`, or authoring an artifact file), confirm the project has a root: run `openspec status --json` and read `root.source`. Any value other than `implicit` means the project is set up; `"source": "implicit"` means it is not - there is no `openspec/` directory here, and the next write would create one. An error saying `No OpenSpec root found` means the same thing. In that case stop before writing and ask the user how to proceed: set this project up (`openspec init`), target a store they already have (`--store `), or drop OpenSpec for this request and help them directly. Wait for their answer. Do not run `openspec init` until they ask for it, do not hand-create `openspec/` files, and do not let a command create the root as a side effect. +**Project check:** These steps expect a project that already uses OpenSpec. Before the first step that writes anything (`new change`, `archive`, `sync specs`, or authoring an artifact file), confirm the project has a root: run `openspec list --json` and read `root`. A root object means the project is set up. `"root": null` means it is not - there is no `openspec/` directory here, and a write such as `openspec new change` would create one as a side effect. In that case stop before writing and ask the user how to proceed: set this project up (`openspec init`), target a store they already have (`--store `), or drop OpenSpec for this request and help them directly. Wait for their answer. Do not run `openspec init` until they ask for it, do not hand-create `openspec/` files, and do not let a command create the root as a side effect. **Input**: The user's request should include a change name (kebab-case) OR a description of what they want to build. diff --git a/skills/openspec-sync-specs/SKILL.md b/skills/openspec-sync-specs/SKILL.md index 08fac5e448..d324ac83a3 100644 --- a/skills/openspec-sync-specs/SKILL.md +++ b/skills/openspec-sync-specs/SKILL.md @@ -15,7 +15,7 @@ This is an **agent-driven** operation - you will read delta specs and directly e **Store selection:** If the user names a store (a store is a standalone OpenSpec repo registered on this machine) or the work lives in one, run `openspec store list --json` to discover registered store ids, then pass `--store ` on the commands that read or write specs and changes (`new change`, `status`, `instructions`, `list`, `show`, `validate`, `archive`, `doctor`, `context`, `schemas`, `view`). Once selected, treat `--store ` as sticky for the rest of the workflow. Every unscoped example of those commands below is shorthand: before running it, append the flag. For example, run `openspec status --change "" --json --store ""`, not the unscoped form shown below. Other commands do not take the flag. Hints printed by commands already carry the flag; keep it on follow-ups. Without a store, commands act on the nearest local `openspec/` root. -**Project check:** These steps expect a project that already uses OpenSpec. Before the first step that writes anything (`new change`, `archive`, `sync specs`, or authoring an artifact file), confirm the project has a root: run `openspec status --json` and read `root.source`. Any value other than `implicit` means the project is set up; `"source": "implicit"` means it is not - there is no `openspec/` directory here, and the next write would create one. An error saying `No OpenSpec root found` means the same thing. In that case stop before writing and ask the user how to proceed: set this project up (`openspec init`), target a store they already have (`--store `), or drop OpenSpec for this request and help them directly. Wait for their answer. Do not run `openspec init` until they ask for it, do not hand-create `openspec/` files, and do not let a command create the root as a side effect. +**Project check:** These steps expect a project that already uses OpenSpec. Before the first step that writes anything (`new change`, `archive`, `sync specs`, or authoring an artifact file), confirm the project has a root: run `openspec list --json` and read `root`. A root object means the project is set up. `"root": null` means it is not - there is no `openspec/` directory here, and a write such as `openspec new change` would create one as a side effect. In that case stop before writing and ask the user how to proceed: set this project up (`openspec init`), target a store they already have (`--store `), or drop OpenSpec for this request and help them directly. Wait for their answer. Do not run `openspec init` until they ask for it, do not hand-create `openspec/` files, and do not let a command create the root as a side effect. `` is the spec directory relative to `specs/` (for example, `user-auth` or `identity/user-auth`). Preserve the full path from each delta spec when resolving its main spec. diff --git a/skills/openspec-update-change/SKILL.md b/skills/openspec-update-change/SKILL.md index 1ec1530dea..2295f3f1dd 100644 --- a/skills/openspec-update-change/SKILL.md +++ b/skills/openspec-update-change/SKILL.md @@ -13,7 +13,7 @@ Revise a change's existing planning artifacts and keep them coherent. Never edit **Store selection:** If the user names a store (a store is a standalone OpenSpec repo registered on this machine) or the work lives in one, run `openspec store list --json` to discover registered store ids, then pass `--store ` on the commands that read or write specs and changes (`new change`, `status`, `instructions`, `list`, `show`, `validate`, `archive`, `doctor`, `context`, `schemas`, `view`). Once selected, treat `--store ` as sticky for the rest of the workflow. Every unscoped example of those commands below is shorthand: before running it, append the flag. For example, run `openspec status --change "" --json --store ""`, not the unscoped form shown below. Other commands do not take the flag. Hints printed by commands already carry the flag; keep it on follow-ups. Without a store, commands act on the nearest local `openspec/` root. -**Project check:** These steps expect a project that already uses OpenSpec. Before the first step that writes anything (`new change`, `archive`, `sync specs`, or authoring an artifact file), confirm the project has a root: run `openspec status --json` and read `root.source`. Any value other than `implicit` means the project is set up; `"source": "implicit"` means it is not - there is no `openspec/` directory here, and the next write would create one. An error saying `No OpenSpec root found` means the same thing. In that case stop before writing and ask the user how to proceed: set this project up (`openspec init`), target a store they already have (`--store `), or drop OpenSpec for this request and help them directly. Wait for their answer. Do not run `openspec init` until they ask for it, do not hand-create `openspec/` files, and do not let a command create the root as a side effect. +**Project check:** These steps expect a project that already uses OpenSpec. Before the first step that writes anything (`new change`, `archive`, `sync specs`, or authoring an artifact file), confirm the project has a root: run `openspec list --json` and read `root`. A root object means the project is set up. `"root": null` means it is not - there is no `openspec/` directory here, and a write such as `openspec new change` would create one as a side effect. In that case stop before writing and ask the user how to proceed: set this project up (`openspec init`), target a store they already have (`--store `), or drop OpenSpec for this request and help them directly. Wait for their answer. Do not run `openspec init` until they ask for it, do not hand-create `openspec/` files, and do not let a command create the root as a side effect. **Input**: Optionally specify a change name. If omitted, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes. diff --git a/skills/openspec-verify-change/SKILL.md b/skills/openspec-verify-change/SKILL.md index 5e9a2ed293..c1cc8a34ad 100644 --- a/skills/openspec-verify-change/SKILL.md +++ b/skills/openspec-verify-change/SKILL.md @@ -13,7 +13,7 @@ Verify that an implementation matches the change artifacts (specs, tasks, design **Store selection:** If the user names a store (a store is a standalone OpenSpec repo registered on this machine) or the work lives in one, run `openspec store list --json` to discover registered store ids, then pass `--store ` on the commands that read or write specs and changes (`new change`, `status`, `instructions`, `list`, `show`, `validate`, `archive`, `doctor`, `context`, `schemas`, `view`). Once selected, treat `--store ` as sticky for the rest of the workflow. Every unscoped example of those commands below is shorthand: before running it, append the flag. For example, run `openspec status --change "" --json --store ""`, not the unscoped form shown below. Other commands do not take the flag. Hints printed by commands already carry the flag; keep it on follow-ups. Without a store, commands act on the nearest local `openspec/` root. -**Project check:** These steps expect a project that already uses OpenSpec. Before the first step that writes anything (`new change`, `archive`, `sync specs`, or authoring an artifact file), confirm the project has a root: run `openspec status --json` and read `root.source`. Any value other than `implicit` means the project is set up; `"source": "implicit"` means it is not - there is no `openspec/` directory here, and the next write would create one. An error saying `No OpenSpec root found` means the same thing. In that case stop before writing and ask the user how to proceed: set this project up (`openspec init`), target a store they already have (`--store `), or drop OpenSpec for this request and help them directly. Wait for their answer. Do not run `openspec init` until they ask for it, do not hand-create `openspec/` files, and do not let a command create the root as a side effect. +**Project check:** These steps expect a project that already uses OpenSpec. Before the first step that writes anything (`new change`, `archive`, `sync specs`, or authoring an artifact file), confirm the project has a root: run `openspec list --json` and read `root`. A root object means the project is set up. `"root": null` means it is not - there is no `openspec/` directory here, and a write such as `openspec new change` would create one as a side effect. In that case stop before writing and ask the user how to proceed: set this project up (`openspec init`), target a store they already have (`--store `), or drop OpenSpec for this request and help them directly. Wait for their answer. Do not run `openspec init` until they ask for it, do not hand-create `openspec/` files, and do not let a command create the root as a side effect. **Input**: Optionally specify a change name. If omitted, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes. diff --git a/src/core/templates/workflows/project-root.ts b/src/core/templates/workflows/project-root.ts index 86d30c16e0..be18f9474d 100644 --- a/src/core/templates/workflows/project-root.ts +++ b/src/core/templates/workflows/project-root.ts @@ -7,8 +7,10 @@ * change` falls back to an implicit root and creates `openspec/` in whatever * directory the agent happens to be in. * - * This guidance is interpolated into every workflow so the agent checks - * `root.source` before writing and hands the decision back to the user - * instead of setting the project up on its own. + * This guidance is interpolated into every workflow so the agent checks for a + * root before writing and hands the decision back to the user instead of + * setting the project up on its own. `openspec list --json` is the check + * because it refuses to fabricate an implicit root: it reports `root: null` + * both when nothing is set up and when only stores are registered. */ -export const PROJECT_ROOT_GUARD = `**Project check:** These steps expect a project that already uses OpenSpec. Before the first step that writes anything (\`new change\`, \`archive\`, \`sync specs\`, or authoring an artifact file), confirm the project has a root: run \`openspec status --json\` and read \`root.source\`. Any value other than \`implicit\` means the project is set up; \`"source": "implicit"\` means it is not - there is no \`openspec/\` directory here, and the next write would create one. An error saying \`No OpenSpec root found\` means the same thing. In that case stop before writing and ask the user how to proceed: set this project up (\`openspec init\`), target a store they already have (\`--store \`), or drop OpenSpec for this request and help them directly. Wait for their answer. Do not run \`openspec init\` until they ask for it, do not hand-create \`openspec/\` files, and do not let a command create the root as a side effect.`; +export const PROJECT_ROOT_GUARD = `**Project check:** These steps expect a project that already uses OpenSpec. Before the first step that writes anything (\`new change\`, \`archive\`, \`sync specs\`, or authoring an artifact file), confirm the project has a root: run \`openspec list --json\` and read \`root\`. A root object means the project is set up. \`"root": null\` means it is not - there is no \`openspec/\` directory here, and a write such as \`openspec new change\` would create one as a side effect. In that case stop before writing and ask the user how to proceed: set this project up (\`openspec init\`), target a store they already have (\`--store \`), or drop OpenSpec for this request and help them directly. Wait for their answer. Do not run \`openspec init\` until they ask for it, do not hand-create \`openspec/\` files, and do not let a command create the root as a side effect.`; diff --git a/test/commands/store-root-selection.test.ts b/test/commands/store-root-selection.test.ts index 314ce44da8..201fcf7f4f 100644 --- a/test/commands/store-root-selection.test.ts +++ b/test/commands/store-root-selection.test.ts @@ -683,6 +683,28 @@ operations: } }); + // The generated workflows read `root` from `openspec list --json` to decide + // whether a project is set up (#1645). That answer has to stay honest when + // stores are registered but this directory has no root of its own - + // an implicit root here would read as "set up" and the workflow would + // scaffold a change into an unrelated repository. + it('reports a missing root as JSON when only stores are registered', async () => { + const result = await runCLI(['list', '--json'], { cwd: appRepo, env }); + + expect(result.exitCode).toBe(1); + expect(result.stderr).toBe(''); + + const json = parseJson(result); + expect(json.root).toBeNull(); + expect(json.changes).toEqual([]); + expect(json.status[0]).toEqual( + expect.objectContaining({ + severity: 'error', + code: 'no_root_with_registered_stores', + }) + ); + }); + it('still accepts an existing root with no items', async () => { const isolatedEnv = { ...env, diff --git a/test/core/templates/project-root-guard.test.ts b/test/core/templates/project-root-guard.test.ts index 4ff172051d..3d14918c1b 100644 --- a/test/core/templates/project-root-guard.test.ts +++ b/test/core/templates/project-root-guard.test.ts @@ -38,11 +38,12 @@ describe('project root guard', () => { expect(getFeedbackSkillTemplate().instructions).not.toContain('**Project check:**'); }); + // The CLI contract behind this check - `list` reporting `root: null` instead + // of fabricating an implicit root - is pinned in + // test/commands/store-root-selection.test.ts. it('names the machine-readable signal rather than a guess', () => { - expect(PROJECT_ROOT_GUARD).toContain('openspec status --json'); - expect(PROJECT_ROOT_GUARD).toContain('root.source'); - expect(PROJECT_ROOT_GUARD).toContain('"source": "implicit"'); - expect(PROJECT_ROOT_GUARD).toContain('No OpenSpec root found'); + expect(PROJECT_ROOT_GUARD).toContain('openspec list --json'); + expect(PROJECT_ROOT_GUARD).toContain('`"root": null`'); }); it('hands the decision to the user instead of setting the project up', () => { diff --git a/test/core/templates/skill-templates-parity.test.ts b/test/core/templates/skill-templates-parity.test.ts index 45863159b5..a66d01639a 100644 --- a/test/core/templates/skill-templates-parity.test.ts +++ b/test/core/templates/skill-templates-parity.test.ts @@ -38,46 +38,46 @@ import { import { STORE_SELECTION_GUIDANCE } from '../../../src/core/templates/workflows/store-selection.js'; const EXPECTED_FUNCTION_HASHES: Record = { - getExploreSkillTemplate: '8fbadbc603d929a4db626d277fdee6a27a46fcb4f01fde354bbaf3cd717c3f28', - getNewChangeSkillTemplate: '77bdf20d2d58a49491f9a78c7a7081bcb26b9a12a3a07358b4abe59a5c6621b9', - getContinueChangeSkillTemplate: 'ad87fe61f9cee29e0490de71192e76d42a5af035b38e068e8847376562d41667', - getApplyChangeSkillTemplate: '5444afd511ed70300e6e155700d5ea4d10b12e865f115e9c1356245a14988d0d', - getFfChangeSkillTemplate: '5a0f84b517a7bae25f53c3d6a228e8302c763d20cad4518e1e3caade54c16918', - getSyncSpecsSkillTemplate: '7c11770beb47888c684668593c9b6b9054ab828f5c65f9a0de165156af009cf3', - getOnboardSkillTemplate: 'a198b904818bcea66cc0ffa0d9750fc6dd12fe2d8af8b31e5b98a187aa99bf67', - getOpsxExploreCommandTemplate: '58a68aadc1b3f108a4a16abfb9820698c8f9fe9af41c8b6fd1fcb8f3a3ba4d90', - getOpsxNewCommandTemplate: '74cfc988862ae23a883375c3b703754c398d14e2554b9b3bf89e71eb017905cb', - getOpsxContinueCommandTemplate: 'e1d6b4473233d6f33fef0f97a96d33fef92881a8cc27a8db485872cd2b716c99', - getOpsxApplyCommandTemplate: '20a9b8c7868bd67a25c2296aef41e12982821c1c2af3385d5a008a1188ee98d0', - getOpsxFfCommandTemplate: 'e62ce1913654bdcf61a0eb5056992b3d4b1a057f4c20f72002da2476c2182717', - getArchiveChangeSkillTemplate: 'f71012ff02663afa01c2ee6d8235d66139572cb1b40db5a6372a3b5b52facc60', - getBulkArchiveChangeSkillTemplate: 'de2f38934910271786025303d5110b1cbefd4d66fc31dfc704abe1e1e111fe8d', - getOpsxSyncCommandTemplate: '005939610270e9fe48abddf79e5521c4a9d92adea244e5e093217445279c3f4b', - getVerifyChangeSkillTemplate: '548feb55b4b4651c1b7a0ddf06d362375597aded97aac98b4fc479bd3277ff07', - getOpsxArchiveCommandTemplate: '607c6afc4366a7c502ba2bf5aa95440c2c803c0ed8e8fe9a80a2824db0b27446', - getOpsxOnboardCommandTemplate: '1c3d0e3d5bcf02476b36b9a6c8d6192e4c6389916a41ad39b349c61b4ec85078', - getOpsxBulkArchiveCommandTemplate: '1e447cdbf8b47ecd64f08a6bfb2e74e1c36186e57bc2b0088f6e1f0765a26c82', - getOpsxVerifyCommandTemplate: 'b55d27b4e6915718305175154700306d746be1e1d0716e13ab5bf0d613e46239', - getOpsxProposeSkillTemplate: '73cde4b2becc257e0270b9fdd28f018f48a9669bc90ba7056b8c4c940c305fca', - getOpsxProposeCommandTemplate: '5194c2d0ca41f0ab70bcd3fcb1d0259fa84418352b85bcdeb64028cc348ee045', + getExploreSkillTemplate: 'f0b94c622a03ee90b285d2111b80ece4e0bc036f20b470b4f2c0d7199fa07e2b', + getNewChangeSkillTemplate: '1f0bd4bd92e510f207b469fa3dd678bed6d796cbd1f0565b70f9c4eb909976f3', + getContinueChangeSkillTemplate: 'cb0e32b760953fd1ab00c12baf1a87da68394ecd9263c952a7e049c55147161c', + getApplyChangeSkillTemplate: 'f22566f746cbcd7562f546b6e48d6cd89eaaf0daa60ba62cc91545c9d5226219', + getFfChangeSkillTemplate: '43a85a3b66fab64b7b2ff78f8e23cc66df22a81c647c1f2f2db1c15b3167f026', + getSyncSpecsSkillTemplate: '655f4352561f50b703c8475e332ca6910f1ab217ade28c510c7b5a625a89acbd', + getOnboardSkillTemplate: 'a8389574768c44bb5b3300b6374a8c3f0bbfad963709e81b04773bbc3e4defc8', + getOpsxExploreCommandTemplate: 'c3d3590d5975428d96236731259451f05016e2ebe733072094eea7bd5d190084', + getOpsxNewCommandTemplate: 'b9d8823cfd6dc912eb5472d501178ef53fa3fcb6af45f01955ba2ac59fa9de59', + getOpsxContinueCommandTemplate: '58b33b64f2c6990902d6a1d77f3280c92ed38b74e4d3b046b7200e0dba4fdec2', + getOpsxApplyCommandTemplate: '1b3fee846ce08a5eda6efe52f9a22d506162927949ea0ac2331a3e0b8e2679b0', + getOpsxFfCommandTemplate: '388b291a4916f9230ec7fe392253d435ca74b72e7fbabdaf3cfabf879382d5bd', + getArchiveChangeSkillTemplate: '5069e40df61aee97806226a8ef309fa51d1bd98f973893cc860359fa395865b3', + getBulkArchiveChangeSkillTemplate: 'b64b15d33815f7c50215ad4ecfe069c65beddc5b6c56a671daa8610f2b57debe', + getOpsxSyncCommandTemplate: 'b2576135394c0d61c380f591d00eeb628063233bd58e409c14bd9bed4918adf3', + getVerifyChangeSkillTemplate: '31d1a92554a4d664d586d88615c302f6507ae48cd234dd7067f67c148ee88967', + getOpsxArchiveCommandTemplate: '6e034773cb0df424e2ce4b3b5018993cbc3eb732da58c219931306cf879db9cc', + getOpsxOnboardCommandTemplate: '88387b452ca5409af6510e5944e976119a350069529bc719d24887dd7011c60e', + getOpsxBulkArchiveCommandTemplate: '68f584b4dc3b42219e5c98cbe07c38bc47e9795a85e201fa53c8015df89ffbd5', + getOpsxVerifyCommandTemplate: 'eb6a8b746b50791de251111bc7767870af18b70935125cfd714178eefa6894bd', + getOpsxProposeSkillTemplate: '0778c431d3c8feaedeb84870314eabb9636cd43042a4a9dc8e848ccd2ae8b7aa', + getOpsxProposeCommandTemplate: 'bcf0767b51795fb4a95871c0e1dcc025ab54a2386af9a8d42099d1726ef2c307', getFeedbackSkillTemplate: 'dabeb5e825b9349abc8156c3e7b8608f27987912a6d9bf47ef29addde6138133', - getUpdateChangeSkillTemplate: '5502735e4a8a2fef9ba2469a742922f45de42a9b6dbde075d6a181d1f8f8a1f6', - getOpsxUpdateCommandTemplate: 'fa903cce48116b86e913044781237716117e4a941614233783f6054e7cb56ff1', + getUpdateChangeSkillTemplate: '528bf852a3aea7854cc30714a987a2de8484a8996fcb46fa18912c2dcdf98311', + getOpsxUpdateCommandTemplate: '8f9454edc53e58f1974e8360db51b9e9f85e2d94003d5ef7376e469363913852', }; const EXPECTED_GENERATED_SKILL_CONTENT_HASHES: Record = { - 'openspec-explore': '508e93e4831d6866a523cc10c732018f1133e0da68f2113e32b346c71add83a5', - 'openspec-new-change': '8c09c677488dc567d6810ac44d1beaa4263159c95a1d2164a8945f45670c1fee', - 'openspec-continue-change': '5ebcfafb024e9033bac8d9369fe004ee8db03654ca0fc46fb8d068a2a71b7409', - 'openspec-apply-change': 'be1326080d11a1a4db3e916bfcc0b35680e582706633ef407d396e03564252e6', - 'openspec-ff-change': '5a87b3827bb420f06235a1fe8aaaa133ddffd3fb83d1ef70ae4f8ed65a00cad3', - 'openspec-sync-specs': '9902dbcc38b03bdaff06c1a4c1c2c1c4c8a9c96fdc24e53ee366063716a2e12c', - 'openspec-archive-change': '659f729e27764bed59f8ef11b2dadf928797b44d14c6ae9e5a72d37b19dc5686', - 'openspec-bulk-archive-change': '2ce95b813ef3cc6b05f40cda1eafeea9553dc573f4e02356d98cfbf9bfbc5f35', - 'openspec-verify-change': 'c7aeca640011957f319636a217e3a43b73e5160639b29ff65f7945b30eb38120', - 'openspec-onboard': 'c0448d0070d4628e6168ed09da17eaf442e043f357095966eb00c0c63f2a34d9', - 'openspec-propose': 'f808354d0a779718651275af4337ec941d7445cd49cfe31198f41a178b19d1c0', - 'openspec-update-change': '6dd12af78bf32f19779026d76de5ace961360985b462c2a237d5b9260cf4470c', + 'openspec-explore': '9ec52ca89d750c451fec090ccc81738eba67f4f64f4ca63d8beaf6d50ca46037', + 'openspec-new-change': 'bcbaf1d7da7caa616e2fa219f753a87458906d0f7bcbe359a11cccad727fbc24', + 'openspec-continue-change': '01f40217fae9d644471f00ba751dcb61a66fc42cd62c88fa5f5366932a0a2031', + 'openspec-apply-change': '629d3d9c9ecf7d7abfc41379fb7c0b0364834b4b5e49f449a2e66b454653b73e', + 'openspec-ff-change': 'a47c7adbc079731e981e4d5aa4da926cd0fa411263df4de3c40dec71c161f85c', + 'openspec-sync-specs': '5662d165ace17ca9f860973a009ecb2172456151da5b73ac1b7778c27a724ab8', + 'openspec-archive-change': '5102469cf8cd65e2dee9b1917755c894d10bf6314dd06779753d9ddd6b13686c', + 'openspec-bulk-archive-change': 'bd1e74c8c0baac58061a6ed44aa9aa63cd75c8f10c97575bf24145262a3f410b', + 'openspec-verify-change': '275eda987e1dff2dd93c94e81dbfcf112f7536676eaf2fdeaf2ac443d74d4e6f', + 'openspec-onboard': '6f64d9e70901a8f7c22f8cce576d67634232a9c7f74ecffb7a8bdee311775856', + 'openspec-propose': '9a7143fca06276c36f84096f6d37d02a029e078f5cd19519402bd6a358ca5b79', + 'openspec-update-change': 'd420226c3b6c7dc9d63e4890fa244642bcf5efa839d61acb65a8f32a0322fa95', }; // Intentionally excludes getFeedbackSkillTemplate: this list only models templates From 72043e8fb2bda8c11f8b6f6107f0e5ded86025c6 Mon Sep 17 00:00:00 2001 From: Clay Good Date: Fri, 4 Sep 2026 12:20:19 -0500 Subject: [PATCH 06/15] 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 --- .../core/templates/project-root-guard.test.ts | 64 ++++++++++++++----- 1 file changed, 47 insertions(+), 17 deletions(-) diff --git a/test/core/templates/project-root-guard.test.ts b/test/core/templates/project-root-guard.test.ts index 3d14918c1b..4df1762ca3 100644 --- a/test/core/templates/project-root-guard.test.ts +++ b/test/core/templates/project-root-guard.test.ts @@ -1,6 +1,7 @@ import { describe, expect, it } from 'vitest'; import { PROJECT_ROOT_GUARD } from '../../../src/core/templates/workflows/project-root.js'; +import { STORE_SELECTION_GUIDANCE } from '../../../src/core/templates/workflows/store-selection.js'; import { getFeedbackSkillTemplate } from '../../../src/core/templates/skill-templates.js'; import { generateSkillContent, @@ -18,6 +19,21 @@ import { * the guard has to live in the instructions themselves, in every workflow. */ describe('project root guard', () => { + // Both surfaces, rendered exactly as they ship. + function renderedBodies(): Array<[string, string]> { + return [ + ...getSkillTemplates().map( + ({ template, dirName }): [string, string] => [ + `skill ${dirName}`, + generateSkillContent(template, 'PARITY-BASELINE'), + ] + ), + ...getCommandContents().map( + (entry): [string, string] => [`command ${entry.id}`, entry.body] + ), + ]; + } + it('warns about an uninitialized project in every deployed skill', () => { for (const { template, dirName } of getSkillTemplates()) { const content = generateSkillContent(template, 'PARITY-BASELINE'); @@ -57,32 +73,46 @@ describe('project root guard', () => { }); // A guard printed after the workflow has already scaffolded a change is no - // guard at all: it has to precede the first command the workflow runs. - it('precedes the first openspec command in every deployed skill and command', () => { - const bodies: Array<[string, string]> = [ - ...getSkillTemplates().map( - ({ template, dirName }): [string, string] => [ - `skill ${dirName}`, - generateSkillContent(template, 'PARITY-BASELINE'), - ] - ), - ...getCommandContents().map( - (entry): [string, string] => [`command ${entry.id}`, entry.body] - ), + // guard at all, so nothing that runs a command or writes an artifact may + // appear before it. Asserting on the text *preceding* the guard catches a + // stray write wherever it sits - inside a fence or in bare prose - which + // looking only at the first fenced block would miss. + it('precedes every command block and write instruction it guards', () => { + const writeMarkers = [ + '```', // any command block, whatever the language tag + 'openspec new change', + 'openspec archive', + 'openspec sync', + 'openspec instructions', + 'openspec validate', ]; - for (const [label, body] of bodies) { + for (const [label, body] of renderedBodies()) { const guardStart = body.indexOf(PROJECT_ROOT_GUARD); expect(guardStart, label).toBeGreaterThanOrEqual(0); - const guardEnd = guardStart + PROJECT_ROOT_GUARD.length; - const firstCommand = body.indexOf('```bash'); - if (firstCommand >= 0) { - expect(firstCommand, label).toBeGreaterThan(guardEnd); + const beforeGuard = body.slice(0, guardStart); + for (const marker of writeMarkers) { + expect(beforeGuard, `${label} runs "${marker}" before the project check`).not.toContain( + marker + ); } } }); + // The guard is worthless if it sits at the end of a long workflow, so pin + // where it lives: directly under the store-selection guidance, in the + // header every workflow reads before it starts. + it('sits directly under the store-selection guidance', () => { + for (const [label, body] of renderedBodies()) { + const storeStart = body.indexOf(STORE_SELECTION_GUIDANCE); + expect(storeStart, label).toBeGreaterThanOrEqual(0); + expect(body.indexOf(PROJECT_ROOT_GUARD), label).toBe( + storeStart + STORE_SELECTION_GUIDANCE.length + '\n\n'.length + ); + } + }); + // The other half of #1645: a host picks skills by description, so a // description that never says "OpenSpec" reads as a generic offer to // explore or propose and wins in repositories that have no OpenSpec at all. From 3d5851bbe0bae58f75b907db22cce8a0572a5c9d Mon Sep 17 00:00:00 2001 From: Clay Good Date: Fri, 4 Sep 2026 12:52:32 -0500 Subject: [PATCH 07/15] 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 --- .changeset/guard-uninitialized-projects.md | 2 +- docs/troubleshooting.md | 2 +- src/commands/workflow/new-change.ts | 29 ++++++++++++++ test/commands/store-root-selection.test.ts | 46 ++++++++++++++++++++++ 4 files changed, 77 insertions(+), 2 deletions(-) diff --git a/.changeset/guard-uninitialized-projects.md b/.changeset/guard-uninitialized-projects.md index 01b1a5dad8..cd66817bfa 100644 --- a/.changeset/guard-uninitialized-projects.md +++ b/.changeset/guard-uninitialized-projects.md @@ -4,4 +4,4 @@ ### Bug Fixes -- Generated skills and commands no longer adopt a project that never ran `openspec init`. Every workflow now checks `root` from `openspec list --json` before its first write: `"root": null` means the project is not set up, so the agent stops and asks whether to initialize it, target a store, or handle the request without OpenSpec, instead of letting `openspec new change` create `openspec/` in the current directory as a side effect. Skill descriptions now name OpenSpec so hosts stop offering these workflows in unrelated repositories. +- Generated skills and commands no longer adopt a project that never ran `openspec init`. Every workflow now checks `root` from `openspec list --json` before its first write: `"root": null` means the project is not set up, so the agent stops and asks whether to initialize it, target a store, or handle the request without OpenSpec, instead of letting `openspec new change` create `openspec/` in the current directory as a side effect. Skill descriptions now name OpenSpec so hosts stop offering these workflows in unrelated repositories. `openspec new change` also says when it had to create the root itself, so a directory that was never set up no longer picks up an `openspec/` directory in silence (human output only; `--json` is unchanged). diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index 37aac1bcd1..b1d9561b7d 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -41,7 +41,7 @@ The full list of tool IDs is in [Supported Tools](supported-tools.md). Use `--to Skills and commands are installed once per tool, so they are offered in every project you open. If you ask for a proposal in a project that never ran `openspec init`, the assistant stops and asks whether to set the project up, work against a store (`--store `), or drop OpenSpec for that request. -If it went ahead anyway, it is running older instruction files: upgrade the CLI, run `openspec update` in your OpenSpec projects, and restart your assistant. To check a directory yourself, run `openspec list --json` — `"root": null` means there is no `openspec/` directory here, and a command like `openspec new change` would create one. An `openspec/` directory created this way is safe to delete if you didn't want it. +If it went ahead anyway, it is running older instruction files: upgrade the CLI, run `openspec update` in your OpenSpec projects, and restart your assistant. To check a directory yourself, run `openspec list --json` — `"root": null` means there is no `openspec/` directory here, and a command like `openspec new change` would create one. `openspec new change` also prints a note when it had to create the root itself. An `openspec/` directory created this way is safe to delete if you didn't want it. ## Commands don't show up diff --git a/src/commands/workflow/new-change.ts b/src/commands/workflow/new-change.ts index 3e059242dc..9dc57fd69d 100644 --- a/src/commands/workflow/new-change.ts +++ b/src/commands/workflow/new-change.ts @@ -7,6 +7,7 @@ * this command. */ +import chalk from 'chalk'; import ora from 'ora'; import path from 'path'; import { createChange, validateChangeName } from '../../utils/change-utils.js'; @@ -85,6 +86,33 @@ function printCreatedChangeHuman( console.log(`Next: ${withStoreFlag(root, `openspec status --change ${payload.change.id}`)}`); } +/** + * An implicit root is the fallback taken when no `openspec/` directory was + * found: creating a change there materializes OpenSpec in whatever directory + * the caller happened to be in, which is how an agent ends up adopting a + * project that never ran `openspec init` (#1645). The creation itself stays + * zero-config; this only makes it visible. + */ +function printImplicitRootNotice(root: ResolvedOpenSpecRoot): void { + if (root.source !== 'implicit') { + return; + } + + const openspecDir = path.dirname(root.changesDir); + const relative = path.relative(process.cwd(), openspecDir); + const location = relative && !relative.startsWith('..') ? relative : openspecDir; + + console.log(); + console.log( + chalk.dim(`Note: no OpenSpec root was found here, so one was created at ${location}/.`) + ); + console.log( + chalk.dim( + 'Run `openspec init` to finish setting this project up, or delete that directory if you meant a different project.' + ) + ); +} + export async function newChangeCommand(name: string | undefined, options: NewChangeOptions): Promise { const spinner = options.json ? undefined : ora(); @@ -153,6 +181,7 @@ export async function newChangeCommand(name: string | undefined, options: NewCha spinner?.stop(); printCreatedChangeHuman(payload, root); + printImplicitRootNotice(root); } catch (error) { spinner?.stop(); if (options.json) { diff --git a/test/commands/store-root-selection.test.ts b/test/commands/store-root-selection.test.ts index 201fcf7f4f..dcef6ff81b 100644 --- a/test/commands/store-root-selection.test.ts +++ b/test/commands/store-root-selection.test.ts @@ -604,6 +604,52 @@ operations: expect(json.root.source).toBe('implicit'); }); + // Creating a change in a directory that was never set up silently + // materializes `openspec/` there (#1645). The creation stays zero-config, + // but a human who did not mean to adopt this directory has to be told. + it('says so when the first change creates the root in an unset-up directory', async () => { + const isolatedEnv = { + ...env, + XDG_DATA_HOME: path.join(tempDir, 'data-empty'), + }; + + const created = await runCLI(['new', 'change', 'adopt-me'], { + cwd: appRepo, + env: isolatedEnv, + }); + expect(created.exitCode).toBe(0); + const firstOutput = created.stdout + created.stderr; + expect(firstOutput).toContain('no OpenSpec root was found here'); + expect(firstOutput).toContain('openspec'); + expect(firstOutput).toContain('openspec init'); + + // The root exists now, so the notice must not repeat on every change. + const second = await runCLI(['new', 'change', 'already-adopted'], { + cwd: appRepo, + env: isolatedEnv, + }); + expect(second.exitCode).toBe(0); + expect(second.stdout + second.stderr).not.toContain('no OpenSpec root was found here'); + }); + + it('keeps the notice out of JSON output', async () => { + const isolatedEnv = { + ...env, + XDG_DATA_HOME: path.join(tempDir, 'data-empty'), + }; + + const result = await runCLI(['new', 'change', 'adopt-me-quietly', '--json'], { + cwd: appRepo, + env: isolatedEnv, + }); + expect(result.exitCode).toBe(0); + expect(result.stdout).not.toContain('no OpenSpec root was found here'); + + // `root.source` is how a caller in JSON mode learns the same fact. + const json = parseJson(result); + expect(json.root.source).toBe('implicit'); + }); + it('keeps list working for a legacy project.md root when no stores are registered', async () => { const isolatedEnv = { ...env, From b623f1da2c3c7bab97732d00c8869142ef88fcfc Mon Sep 17 00:00:00 2001 From: Clay Good Date: Fri, 4 Sep 2026 12:54:39 -0500 Subject: [PATCH 08/15] 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 --- skills/openspec-apply-change/SKILL.md | 2 +- skills/openspec-archive-change/SKILL.md | 2 +- skills/openspec-bulk-archive-change/SKILL.md | 2 +- skills/openspec-continue-change/SKILL.md | 2 +- skills/openspec-explore/SKILL.md | 2 +- skills/openspec-ff-change/SKILL.md | 2 +- skills/openspec-new-change/SKILL.md | 2 +- skills/openspec-onboard/SKILL.md | 2 +- skills/openspec-propose/SKILL.md | 2 +- skills/openspec-sync-specs/SKILL.md | 2 +- skills/openspec-update-change/SKILL.md | 2 +- skills/openspec-verify-change/SKILL.md | 2 +- src/core/templates/workflows/project-root.ts | 2 +- test/commands/store-root-selection.test.ts | 5 +- .../core/templates/project-root-guard.test.ts | 3 + .../templates/skill-templates-parity.test.ts | 72 +++++++++---------- 16 files changed, 56 insertions(+), 50 deletions(-) diff --git a/skills/openspec-apply-change/SKILL.md b/skills/openspec-apply-change/SKILL.md index 945615acf4..b727e5f065 100644 --- a/skills/openspec-apply-change/SKILL.md +++ b/skills/openspec-apply-change/SKILL.md @@ -13,7 +13,7 @@ Implement tasks from an OpenSpec change. **Store selection:** If the user names a store (a store is a standalone OpenSpec repo registered on this machine) or the work lives in one, run `openspec store list --json` to discover registered store ids, then pass `--store ` on the commands that read or write specs and changes (`new change`, `status`, `instructions`, `list`, `show`, `validate`, `archive`, `doctor`, `context`, `schemas`, `view`). Once selected, treat `--store ` as sticky for the rest of the workflow. Every unscoped example of those commands below is shorthand: before running it, append the flag. For example, run `openspec status --change "" --json --store ""`, not the unscoped form shown below. Other commands do not take the flag. Hints printed by commands already carry the flag; keep it on follow-ups. Without a store, commands act on the nearest local `openspec/` root. -**Project check:** These steps expect a project that already uses OpenSpec. Before the first step that writes anything (`new change`, `archive`, `sync specs`, or authoring an artifact file), confirm the project has a root: run `openspec list --json` and read `root`. A root object means the project is set up. `"root": null` means it is not - there is no `openspec/` directory here, and a write such as `openspec new change` would create one as a side effect. In that case stop before writing and ask the user how to proceed: set this project up (`openspec init`), target a store they already have (`--store `), or drop OpenSpec for this request and help them directly. Wait for their answer. Do not run `openspec init` until they ask for it, do not hand-create `openspec/` files, and do not let a command create the root as a side effect. +**Project check:** These steps expect a project that already uses OpenSpec. Before the first step that writes anything (`new change`, `archive`, `sync specs`, or authoring an artifact file), confirm the project has a root: run `openspec list --json` and read `root`. A root object means the project is set up. `"root": null` means it is not - there is no `openspec/` directory here, and a write such as `openspec new change` would create one as a side effect. The command exits non-zero in that case: that is the answer to the question, not a broken CLI, so read the JSON rather than retrying or working around it. In that case stop before writing and ask the user how to proceed: set this project up (`openspec init`), target a store they already have (`--store `), or drop OpenSpec for this request and help them directly. Wait for their answer. Do not run `openspec init` until they ask for it, do not hand-create `openspec/` files, and do not let a command create the root as a side effect. **Input**: Optionally specify a change name (e.g., `/openspec-apply-change add-auth`). If omitted, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes. diff --git a/skills/openspec-archive-change/SKILL.md b/skills/openspec-archive-change/SKILL.md index 8936eb96cc..4de7fe870f 100644 --- a/skills/openspec-archive-change/SKILL.md +++ b/skills/openspec-archive-change/SKILL.md @@ -13,7 +13,7 @@ Archive a completed change in the experimental workflow. **Store selection:** If the user names a store (a store is a standalone OpenSpec repo registered on this machine) or the work lives in one, run `openspec store list --json` to discover registered store ids, then pass `--store ` on the commands that read or write specs and changes (`new change`, `status`, `instructions`, `list`, `show`, `validate`, `archive`, `doctor`, `context`, `schemas`, `view`). Once selected, treat `--store ` as sticky for the rest of the workflow. Every unscoped example of those commands below is shorthand: before running it, append the flag. For example, run `openspec status --change "" --json --store ""`, not the unscoped form shown below. Other commands do not take the flag. Hints printed by commands already carry the flag; keep it on follow-ups. Without a store, commands act on the nearest local `openspec/` root. -**Project check:** These steps expect a project that already uses OpenSpec. Before the first step that writes anything (`new change`, `archive`, `sync specs`, or authoring an artifact file), confirm the project has a root: run `openspec list --json` and read `root`. A root object means the project is set up. `"root": null` means it is not - there is no `openspec/` directory here, and a write such as `openspec new change` would create one as a side effect. In that case stop before writing and ask the user how to proceed: set this project up (`openspec init`), target a store they already have (`--store `), or drop OpenSpec for this request and help them directly. Wait for their answer. Do not run `openspec init` until they ask for it, do not hand-create `openspec/` files, and do not let a command create the root as a side effect. +**Project check:** These steps expect a project that already uses OpenSpec. Before the first step that writes anything (`new change`, `archive`, `sync specs`, or authoring an artifact file), confirm the project has a root: run `openspec list --json` and read `root`. A root object means the project is set up. `"root": null` means it is not - there is no `openspec/` directory here, and a write such as `openspec new change` would create one as a side effect. The command exits non-zero in that case: that is the answer to the question, not a broken CLI, so read the JSON rather than retrying or working around it. In that case stop before writing and ask the user how to proceed: set this project up (`openspec init`), target a store they already have (`--store `), or drop OpenSpec for this request and help them directly. Wait for their answer. Do not run `openspec init` until they ask for it, do not hand-create `openspec/` files, and do not let a command create the root as a side effect. `` is the spec directory relative to `specs/` (for example, `user-auth` or `identity/user-auth`). Preserve the full path from each delta spec when resolving its main spec. diff --git a/skills/openspec-bulk-archive-change/SKILL.md b/skills/openspec-bulk-archive-change/SKILL.md index 143cb8c2ee..28b13e12ab 100644 --- a/skills/openspec-bulk-archive-change/SKILL.md +++ b/skills/openspec-bulk-archive-change/SKILL.md @@ -15,7 +15,7 @@ This skill allows you to batch-archive changes, handling spec conflicts intellig **Store selection:** If the user names a store (a store is a standalone OpenSpec repo registered on this machine) or the work lives in one, run `openspec store list --json` to discover registered store ids, then pass `--store ` on the commands that read or write specs and changes (`new change`, `status`, `instructions`, `list`, `show`, `validate`, `archive`, `doctor`, `context`, `schemas`, `view`). Once selected, treat `--store ` as sticky for the rest of the workflow. Every unscoped example of those commands below is shorthand: before running it, append the flag. For example, run `openspec status --change "" --json --store ""`, not the unscoped form shown below. Other commands do not take the flag. Hints printed by commands already carry the flag; keep it on follow-ups. Without a store, commands act on the nearest local `openspec/` root. -**Project check:** These steps expect a project that already uses OpenSpec. Before the first step that writes anything (`new change`, `archive`, `sync specs`, or authoring an artifact file), confirm the project has a root: run `openspec list --json` and read `root`. A root object means the project is set up. `"root": null` means it is not - there is no `openspec/` directory here, and a write such as `openspec new change` would create one as a side effect. In that case stop before writing and ask the user how to proceed: set this project up (`openspec init`), target a store they already have (`--store `), or drop OpenSpec for this request and help them directly. Wait for their answer. Do not run `openspec init` until they ask for it, do not hand-create `openspec/` files, and do not let a command create the root as a side effect. +**Project check:** These steps expect a project that already uses OpenSpec. Before the first step that writes anything (`new change`, `archive`, `sync specs`, or authoring an artifact file), confirm the project has a root: run `openspec list --json` and read `root`. A root object means the project is set up. `"root": null` means it is not - there is no `openspec/` directory here, and a write such as `openspec new change` would create one as a side effect. The command exits non-zero in that case: that is the answer to the question, not a broken CLI, so read the JSON rather than retrying or working around it. In that case stop before writing and ask the user how to proceed: set this project up (`openspec init`), target a store they already have (`--store `), or drop OpenSpec for this request and help them directly. Wait for their answer. Do not run `openspec init` until they ask for it, do not hand-create `openspec/` files, and do not let a command create the root as a side effect. `` is the spec directory relative to `specs/` (for example, `user-auth` or `identity/user-auth`). Preserve the full path from each delta spec when resolving its main spec. diff --git a/skills/openspec-continue-change/SKILL.md b/skills/openspec-continue-change/SKILL.md index ee1ee84088..5ae6f7560b 100644 --- a/skills/openspec-continue-change/SKILL.md +++ b/skills/openspec-continue-change/SKILL.md @@ -13,7 +13,7 @@ Continue working on a change by creating the next artifact. **Store selection:** If the user names a store (a store is a standalone OpenSpec repo registered on this machine) or the work lives in one, run `openspec store list --json` to discover registered store ids, then pass `--store ` on the commands that read or write specs and changes (`new change`, `status`, `instructions`, `list`, `show`, `validate`, `archive`, `doctor`, `context`, `schemas`, `view`). Once selected, treat `--store ` as sticky for the rest of the workflow. Every unscoped example of those commands below is shorthand: before running it, append the flag. For example, run `openspec status --change "" --json --store ""`, not the unscoped form shown below. Other commands do not take the flag. Hints printed by commands already carry the flag; keep it on follow-ups. Without a store, commands act on the nearest local `openspec/` root. -**Project check:** These steps expect a project that already uses OpenSpec. Before the first step that writes anything (`new change`, `archive`, `sync specs`, or authoring an artifact file), confirm the project has a root: run `openspec list --json` and read `root`. A root object means the project is set up. `"root": null` means it is not - there is no `openspec/` directory here, and a write such as `openspec new change` would create one as a side effect. In that case stop before writing and ask the user how to proceed: set this project up (`openspec init`), target a store they already have (`--store `), or drop OpenSpec for this request and help them directly. Wait for their answer. Do not run `openspec init` until they ask for it, do not hand-create `openspec/` files, and do not let a command create the root as a side effect. +**Project check:** These steps expect a project that already uses OpenSpec. Before the first step that writes anything (`new change`, `archive`, `sync specs`, or authoring an artifact file), confirm the project has a root: run `openspec list --json` and read `root`. A root object means the project is set up. `"root": null` means it is not - there is no `openspec/` directory here, and a write such as `openspec new change` would create one as a side effect. The command exits non-zero in that case: that is the answer to the question, not a broken CLI, so read the JSON rather than retrying or working around it. In that case stop before writing and ask the user how to proceed: set this project up (`openspec init`), target a store they already have (`--store `), or drop OpenSpec for this request and help them directly. Wait for their answer. Do not run `openspec init` until they ask for it, do not hand-create `openspec/` files, and do not let a command create the root as a side effect. **Input**: Optionally specify a change name. If omitted, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes. diff --git a/skills/openspec-explore/SKILL.md b/skills/openspec-explore/SKILL.md index c1287d98ac..6887ea50aa 100644 --- a/skills/openspec-explore/SKILL.md +++ b/skills/openspec-explore/SKILL.md @@ -17,7 +17,7 @@ Enter explore mode. Think deeply. Visualize freely. Follow the conversation wher **Store selection:** If the user names a store (a store is a standalone OpenSpec repo registered on this machine) or the work lives in one, run `openspec store list --json` to discover registered store ids, then pass `--store ` on the commands that read or write specs and changes (`new change`, `status`, `instructions`, `list`, `show`, `validate`, `archive`, `doctor`, `context`, `schemas`, `view`). Once selected, treat `--store ` as sticky for the rest of the workflow. Every unscoped example of those commands below is shorthand: before running it, append the flag. For example, run `openspec status --change "" --json --store ""`, not the unscoped form shown below. Other commands do not take the flag. Hints printed by commands already carry the flag; keep it on follow-ups. Without a store, commands act on the nearest local `openspec/` root. -**Project check:** These steps expect a project that already uses OpenSpec. Before the first step that writes anything (`new change`, `archive`, `sync specs`, or authoring an artifact file), confirm the project has a root: run `openspec list --json` and read `root`. A root object means the project is set up. `"root": null` means it is not - there is no `openspec/` directory here, and a write such as `openspec new change` would create one as a side effect. In that case stop before writing and ask the user how to proceed: set this project up (`openspec init`), target a store they already have (`--store `), or drop OpenSpec for this request and help them directly. Wait for their answer. Do not run `openspec init` until they ask for it, do not hand-create `openspec/` files, and do not let a command create the root as a side effect. +**Project check:** These steps expect a project that already uses OpenSpec. Before the first step that writes anything (`new change`, `archive`, `sync specs`, or authoring an artifact file), confirm the project has a root: run `openspec list --json` and read `root`. A root object means the project is set up. `"root": null` means it is not - there is no `openspec/` directory here, and a write such as `openspec new change` would create one as a side effect. The command exits non-zero in that case: that is the answer to the question, not a broken CLI, so read the JSON rather than retrying or working around it. In that case stop before writing and ask the user how to proceed: set this project up (`openspec init`), target a store they already have (`--store `), or drop OpenSpec for this request and help them directly. Wait for their answer. Do not run `openspec init` until they ask for it, do not hand-create `openspec/` files, and do not let a command create the root as a side effect. --- diff --git a/skills/openspec-ff-change/SKILL.md b/skills/openspec-ff-change/SKILL.md index 3a8fc2a1cc..f0964b9b8a 100644 --- a/skills/openspec-ff-change/SKILL.md +++ b/skills/openspec-ff-change/SKILL.md @@ -13,7 +13,7 @@ Fast-forward through artifact creation - generate everything needed to start imp **Store selection:** If the user names a store (a store is a standalone OpenSpec repo registered on this machine) or the work lives in one, run `openspec store list --json` to discover registered store ids, then pass `--store ` on the commands that read or write specs and changes (`new change`, `status`, `instructions`, `list`, `show`, `validate`, `archive`, `doctor`, `context`, `schemas`, `view`). Once selected, treat `--store ` as sticky for the rest of the workflow. Every unscoped example of those commands below is shorthand: before running it, append the flag. For example, run `openspec status --change "" --json --store ""`, not the unscoped form shown below. Other commands do not take the flag. Hints printed by commands already carry the flag; keep it on follow-ups. Without a store, commands act on the nearest local `openspec/` root. -**Project check:** These steps expect a project that already uses OpenSpec. Before the first step that writes anything (`new change`, `archive`, `sync specs`, or authoring an artifact file), confirm the project has a root: run `openspec list --json` and read `root`. A root object means the project is set up. `"root": null` means it is not - there is no `openspec/` directory here, and a write such as `openspec new change` would create one as a side effect. In that case stop before writing and ask the user how to proceed: set this project up (`openspec init`), target a store they already have (`--store `), or drop OpenSpec for this request and help them directly. Wait for their answer. Do not run `openspec init` until they ask for it, do not hand-create `openspec/` files, and do not let a command create the root as a side effect. +**Project check:** These steps expect a project that already uses OpenSpec. Before the first step that writes anything (`new change`, `archive`, `sync specs`, or authoring an artifact file), confirm the project has a root: run `openspec list --json` and read `root`. A root object means the project is set up. `"root": null` means it is not - there is no `openspec/` directory here, and a write such as `openspec new change` would create one as a side effect. The command exits non-zero in that case: that is the answer to the question, not a broken CLI, so read the JSON rather than retrying or working around it. In that case stop before writing and ask the user how to proceed: set this project up (`openspec init`), target a store they already have (`--store `), or drop OpenSpec for this request and help them directly. Wait for their answer. Do not run `openspec init` until they ask for it, do not hand-create `openspec/` files, and do not let a command create the root as a side effect. **Input**: The user's request should include a change name (kebab-case) OR a description of what they want to build. diff --git a/skills/openspec-new-change/SKILL.md b/skills/openspec-new-change/SKILL.md index 97eabc5fed..60163bd47d 100644 --- a/skills/openspec-new-change/SKILL.md +++ b/skills/openspec-new-change/SKILL.md @@ -13,7 +13,7 @@ Start a new change using the experimental artifact-driven approach. **Store selection:** If the user names a store (a store is a standalone OpenSpec repo registered on this machine) or the work lives in one, run `openspec store list --json` to discover registered store ids, then pass `--store ` on the commands that read or write specs and changes (`new change`, `status`, `instructions`, `list`, `show`, `validate`, `archive`, `doctor`, `context`, `schemas`, `view`). Once selected, treat `--store ` as sticky for the rest of the workflow. Every unscoped example of those commands below is shorthand: before running it, append the flag. For example, run `openspec status --change "" --json --store ""`, not the unscoped form shown below. Other commands do not take the flag. Hints printed by commands already carry the flag; keep it on follow-ups. Without a store, commands act on the nearest local `openspec/` root. -**Project check:** These steps expect a project that already uses OpenSpec. Before the first step that writes anything (`new change`, `archive`, `sync specs`, or authoring an artifact file), confirm the project has a root: run `openspec list --json` and read `root`. A root object means the project is set up. `"root": null` means it is not - there is no `openspec/` directory here, and a write such as `openspec new change` would create one as a side effect. In that case stop before writing and ask the user how to proceed: set this project up (`openspec init`), target a store they already have (`--store `), or drop OpenSpec for this request and help them directly. Wait for their answer. Do not run `openspec init` until they ask for it, do not hand-create `openspec/` files, and do not let a command create the root as a side effect. +**Project check:** These steps expect a project that already uses OpenSpec. Before the first step that writes anything (`new change`, `archive`, `sync specs`, or authoring an artifact file), confirm the project has a root: run `openspec list --json` and read `root`. A root object means the project is set up. `"root": null` means it is not - there is no `openspec/` directory here, and a write such as `openspec new change` would create one as a side effect. The command exits non-zero in that case: that is the answer to the question, not a broken CLI, so read the JSON rather than retrying or working around it. In that case stop before writing and ask the user how to proceed: set this project up (`openspec init`), target a store they already have (`--store `), or drop OpenSpec for this request and help them directly. Wait for their answer. Do not run `openspec init` until they ask for it, do not hand-create `openspec/` files, and do not let a command create the root as a side effect. **Input**: The user's request should include a change name (kebab-case) OR a description of what they want to build. diff --git a/skills/openspec-onboard/SKILL.md b/skills/openspec-onboard/SKILL.md index d22dac0c0e..2f21f48770 100644 --- a/skills/openspec-onboard/SKILL.md +++ b/skills/openspec-onboard/SKILL.md @@ -13,7 +13,7 @@ Guide the user through their first complete OpenSpec workflow cycle. This is a t **Store selection:** If the user names a store (a store is a standalone OpenSpec repo registered on this machine) or the work lives in one, run `openspec store list --json` to discover registered store ids, then pass `--store ` on the commands that read or write specs and changes (`new change`, `status`, `instructions`, `list`, `show`, `validate`, `archive`, `doctor`, `context`, `schemas`, `view`). Once selected, treat `--store ` as sticky for the rest of the workflow. Every unscoped example of those commands below is shorthand: before running it, append the flag. For example, run `openspec status --change "" --json --store ""`, not the unscoped form shown below. Other commands do not take the flag. Hints printed by commands already carry the flag; keep it on follow-ups. Without a store, commands act on the nearest local `openspec/` root. -**Project check:** These steps expect a project that already uses OpenSpec. Before the first step that writes anything (`new change`, `archive`, `sync specs`, or authoring an artifact file), confirm the project has a root: run `openspec list --json` and read `root`. A root object means the project is set up. `"root": null` means it is not - there is no `openspec/` directory here, and a write such as `openspec new change` would create one as a side effect. In that case stop before writing and ask the user how to proceed: set this project up (`openspec init`), target a store they already have (`--store `), or drop OpenSpec for this request and help them directly. Wait for their answer. Do not run `openspec init` until they ask for it, do not hand-create `openspec/` files, and do not let a command create the root as a side effect. +**Project check:** These steps expect a project that already uses OpenSpec. Before the first step that writes anything (`new change`, `archive`, `sync specs`, or authoring an artifact file), confirm the project has a root: run `openspec list --json` and read `root`. A root object means the project is set up. `"root": null` means it is not - there is no `openspec/` directory here, and a write such as `openspec new change` would create one as a side effect. The command exits non-zero in that case: that is the answer to the question, not a broken CLI, so read the JSON rather than retrying or working around it. In that case stop before writing and ask the user how to proceed: set this project up (`openspec init`), target a store they already have (`--store `), or drop OpenSpec for this request and help them directly. Wait for their answer. Do not run `openspec init` until they ask for it, do not hand-create `openspec/` files, and do not let a command create the root as a side effect. --- diff --git a/skills/openspec-propose/SKILL.md b/skills/openspec-propose/SKILL.md index 4587e39472..c6deeb1939 100644 --- a/skills/openspec-propose/SKILL.md +++ b/skills/openspec-propose/SKILL.md @@ -27,7 +27,7 @@ When the user is ready to implement, they must start the apply workflow explicit **Store selection:** If the user names a store (a store is a standalone OpenSpec repo registered on this machine) or the work lives in one, run `openspec store list --json` to discover registered store ids, then pass `--store ` on the commands that read or write specs and changes (`new change`, `status`, `instructions`, `list`, `show`, `validate`, `archive`, `doctor`, `context`, `schemas`, `view`). Once selected, treat `--store ` as sticky for the rest of the workflow. Every unscoped example of those commands below is shorthand: before running it, append the flag. For example, run `openspec status --change "" --json --store ""`, not the unscoped form shown below. Other commands do not take the flag. Hints printed by commands already carry the flag; keep it on follow-ups. Without a store, commands act on the nearest local `openspec/` root. -**Project check:** These steps expect a project that already uses OpenSpec. Before the first step that writes anything (`new change`, `archive`, `sync specs`, or authoring an artifact file), confirm the project has a root: run `openspec list --json` and read `root`. A root object means the project is set up. `"root": null` means it is not - there is no `openspec/` directory here, and a write such as `openspec new change` would create one as a side effect. In that case stop before writing and ask the user how to proceed: set this project up (`openspec init`), target a store they already have (`--store `), or drop OpenSpec for this request and help them directly. Wait for their answer. Do not run `openspec init` until they ask for it, do not hand-create `openspec/` files, and do not let a command create the root as a side effect. +**Project check:** These steps expect a project that already uses OpenSpec. Before the first step that writes anything (`new change`, `archive`, `sync specs`, or authoring an artifact file), confirm the project has a root: run `openspec list --json` and read `root`. A root object means the project is set up. `"root": null` means it is not - there is no `openspec/` directory here, and a write such as `openspec new change` would create one as a side effect. The command exits non-zero in that case: that is the answer to the question, not a broken CLI, so read the JSON rather than retrying or working around it. In that case stop before writing and ask the user how to proceed: set this project up (`openspec init`), target a store they already have (`--store `), or drop OpenSpec for this request and help them directly. Wait for their answer. Do not run `openspec init` until they ask for it, do not hand-create `openspec/` files, and do not let a command create the root as a side effect. **Input**: The user's request should include a change name (kebab-case) OR a description of what they want to build. diff --git a/skills/openspec-sync-specs/SKILL.md b/skills/openspec-sync-specs/SKILL.md index d324ac83a3..8813b4e3be 100644 --- a/skills/openspec-sync-specs/SKILL.md +++ b/skills/openspec-sync-specs/SKILL.md @@ -15,7 +15,7 @@ This is an **agent-driven** operation - you will read delta specs and directly e **Store selection:** If the user names a store (a store is a standalone OpenSpec repo registered on this machine) or the work lives in one, run `openspec store list --json` to discover registered store ids, then pass `--store ` on the commands that read or write specs and changes (`new change`, `status`, `instructions`, `list`, `show`, `validate`, `archive`, `doctor`, `context`, `schemas`, `view`). Once selected, treat `--store ` as sticky for the rest of the workflow. Every unscoped example of those commands below is shorthand: before running it, append the flag. For example, run `openspec status --change "" --json --store ""`, not the unscoped form shown below. Other commands do not take the flag. Hints printed by commands already carry the flag; keep it on follow-ups. Without a store, commands act on the nearest local `openspec/` root. -**Project check:** These steps expect a project that already uses OpenSpec. Before the first step that writes anything (`new change`, `archive`, `sync specs`, or authoring an artifact file), confirm the project has a root: run `openspec list --json` and read `root`. A root object means the project is set up. `"root": null` means it is not - there is no `openspec/` directory here, and a write such as `openspec new change` would create one as a side effect. In that case stop before writing and ask the user how to proceed: set this project up (`openspec init`), target a store they already have (`--store `), or drop OpenSpec for this request and help them directly. Wait for their answer. Do not run `openspec init` until they ask for it, do not hand-create `openspec/` files, and do not let a command create the root as a side effect. +**Project check:** These steps expect a project that already uses OpenSpec. Before the first step that writes anything (`new change`, `archive`, `sync specs`, or authoring an artifact file), confirm the project has a root: run `openspec list --json` and read `root`. A root object means the project is set up. `"root": null` means it is not - there is no `openspec/` directory here, and a write such as `openspec new change` would create one as a side effect. The command exits non-zero in that case: that is the answer to the question, not a broken CLI, so read the JSON rather than retrying or working around it. In that case stop before writing and ask the user how to proceed: set this project up (`openspec init`), target a store they already have (`--store `), or drop OpenSpec for this request and help them directly. Wait for their answer. Do not run `openspec init` until they ask for it, do not hand-create `openspec/` files, and do not let a command create the root as a side effect. `` is the spec directory relative to `specs/` (for example, `user-auth` or `identity/user-auth`). Preserve the full path from each delta spec when resolving its main spec. diff --git a/skills/openspec-update-change/SKILL.md b/skills/openspec-update-change/SKILL.md index 2295f3f1dd..417c444aef 100644 --- a/skills/openspec-update-change/SKILL.md +++ b/skills/openspec-update-change/SKILL.md @@ -13,7 +13,7 @@ Revise a change's existing planning artifacts and keep them coherent. Never edit **Store selection:** If the user names a store (a store is a standalone OpenSpec repo registered on this machine) or the work lives in one, run `openspec store list --json` to discover registered store ids, then pass `--store ` on the commands that read or write specs and changes (`new change`, `status`, `instructions`, `list`, `show`, `validate`, `archive`, `doctor`, `context`, `schemas`, `view`). Once selected, treat `--store ` as sticky for the rest of the workflow. Every unscoped example of those commands below is shorthand: before running it, append the flag. For example, run `openspec status --change "" --json --store ""`, not the unscoped form shown below. Other commands do not take the flag. Hints printed by commands already carry the flag; keep it on follow-ups. Without a store, commands act on the nearest local `openspec/` root. -**Project check:** These steps expect a project that already uses OpenSpec. Before the first step that writes anything (`new change`, `archive`, `sync specs`, or authoring an artifact file), confirm the project has a root: run `openspec list --json` and read `root`. A root object means the project is set up. `"root": null` means it is not - there is no `openspec/` directory here, and a write such as `openspec new change` would create one as a side effect. In that case stop before writing and ask the user how to proceed: set this project up (`openspec init`), target a store they already have (`--store `), or drop OpenSpec for this request and help them directly. Wait for their answer. Do not run `openspec init` until they ask for it, do not hand-create `openspec/` files, and do not let a command create the root as a side effect. +**Project check:** These steps expect a project that already uses OpenSpec. Before the first step that writes anything (`new change`, `archive`, `sync specs`, or authoring an artifact file), confirm the project has a root: run `openspec list --json` and read `root`. A root object means the project is set up. `"root": null` means it is not - there is no `openspec/` directory here, and a write such as `openspec new change` would create one as a side effect. The command exits non-zero in that case: that is the answer to the question, not a broken CLI, so read the JSON rather than retrying or working around it. In that case stop before writing and ask the user how to proceed: set this project up (`openspec init`), target a store they already have (`--store `), or drop OpenSpec for this request and help them directly. Wait for their answer. Do not run `openspec init` until they ask for it, do not hand-create `openspec/` files, and do not let a command create the root as a side effect. **Input**: Optionally specify a change name. If omitted, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes. diff --git a/skills/openspec-verify-change/SKILL.md b/skills/openspec-verify-change/SKILL.md index c1cc8a34ad..54a8047603 100644 --- a/skills/openspec-verify-change/SKILL.md +++ b/skills/openspec-verify-change/SKILL.md @@ -13,7 +13,7 @@ Verify that an implementation matches the change artifacts (specs, tasks, design **Store selection:** If the user names a store (a store is a standalone OpenSpec repo registered on this machine) or the work lives in one, run `openspec store list --json` to discover registered store ids, then pass `--store ` on the commands that read or write specs and changes (`new change`, `status`, `instructions`, `list`, `show`, `validate`, `archive`, `doctor`, `context`, `schemas`, `view`). Once selected, treat `--store ` as sticky for the rest of the workflow. Every unscoped example of those commands below is shorthand: before running it, append the flag. For example, run `openspec status --change "" --json --store ""`, not the unscoped form shown below. Other commands do not take the flag. Hints printed by commands already carry the flag; keep it on follow-ups. Without a store, commands act on the nearest local `openspec/` root. -**Project check:** These steps expect a project that already uses OpenSpec. Before the first step that writes anything (`new change`, `archive`, `sync specs`, or authoring an artifact file), confirm the project has a root: run `openspec list --json` and read `root`. A root object means the project is set up. `"root": null` means it is not - there is no `openspec/` directory here, and a write such as `openspec new change` would create one as a side effect. In that case stop before writing and ask the user how to proceed: set this project up (`openspec init`), target a store they already have (`--store `), or drop OpenSpec for this request and help them directly. Wait for their answer. Do not run `openspec init` until they ask for it, do not hand-create `openspec/` files, and do not let a command create the root as a side effect. +**Project check:** These steps expect a project that already uses OpenSpec. Before the first step that writes anything (`new change`, `archive`, `sync specs`, or authoring an artifact file), confirm the project has a root: run `openspec list --json` and read `root`. A root object means the project is set up. `"root": null` means it is not - there is no `openspec/` directory here, and a write such as `openspec new change` would create one as a side effect. The command exits non-zero in that case: that is the answer to the question, not a broken CLI, so read the JSON rather than retrying or working around it. In that case stop before writing and ask the user how to proceed: set this project up (`openspec init`), target a store they already have (`--store `), or drop OpenSpec for this request and help them directly. Wait for their answer. Do not run `openspec init` until they ask for it, do not hand-create `openspec/` files, and do not let a command create the root as a side effect. **Input**: Optionally specify a change name. If omitted, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes. diff --git a/src/core/templates/workflows/project-root.ts b/src/core/templates/workflows/project-root.ts index be18f9474d..5191b55890 100644 --- a/src/core/templates/workflows/project-root.ts +++ b/src/core/templates/workflows/project-root.ts @@ -13,4 +13,4 @@ * because it refuses to fabricate an implicit root: it reports `root: null` * both when nothing is set up and when only stores are registered. */ -export const PROJECT_ROOT_GUARD = `**Project check:** These steps expect a project that already uses OpenSpec. Before the first step that writes anything (\`new change\`, \`archive\`, \`sync specs\`, or authoring an artifact file), confirm the project has a root: run \`openspec list --json\` and read \`root\`. A root object means the project is set up. \`"root": null\` means it is not - there is no \`openspec/\` directory here, and a write such as \`openspec new change\` would create one as a side effect. In that case stop before writing and ask the user how to proceed: set this project up (\`openspec init\`), target a store they already have (\`--store \`), or drop OpenSpec for this request and help them directly. Wait for their answer. Do not run \`openspec init\` until they ask for it, do not hand-create \`openspec/\` files, and do not let a command create the root as a side effect.`; +export const PROJECT_ROOT_GUARD = `**Project check:** These steps expect a project that already uses OpenSpec. Before the first step that writes anything (\`new change\`, \`archive\`, \`sync specs\`, or authoring an artifact file), confirm the project has a root: run \`openspec list --json\` and read \`root\`. A root object means the project is set up. \`"root": null\` means it is not - there is no \`openspec/\` directory here, and a write such as \`openspec new change\` would create one as a side effect. The command exits non-zero in that case: that is the answer to the question, not a broken CLI, so read the JSON rather than retrying or working around it. In that case stop before writing and ask the user how to proceed: set this project up (\`openspec init\`), target a store they already have (\`--store \`), or drop OpenSpec for this request and help them directly. Wait for their answer. Do not run \`openspec init\` until they ask for it, do not hand-create \`openspec/\` files, and do not let a command create the root as a side effect.`; diff --git a/test/commands/store-root-selection.test.ts b/test/commands/store-root-selection.test.ts index dcef6ff81b..2511381e8c 100644 --- a/test/commands/store-root-selection.test.ts +++ b/test/commands/store-root-selection.test.ts @@ -620,8 +620,11 @@ operations: expect(created.exitCode).toBe(0); const firstOutput = created.stdout + created.stderr; expect(firstOutput).toContain('no OpenSpec root was found here'); - expect(firstOutput).toContain('openspec'); + // Naming the directory it created is the point: the reader has to know + // what to delete if this was not the project they meant. + expect(firstOutput).toMatch(/created at .*openspec\//); expect(firstOutput).toContain('openspec init'); + expect(fs.existsSync(path.join(appRepo, 'openspec', 'changes', 'adopt-me'))).toBe(true); // The root exists now, so the notice must not repeat on every change. const second = await runCLI(['new', 'change', 'already-adopted'], { diff --git a/test/core/templates/project-root-guard.test.ts b/test/core/templates/project-root-guard.test.ts index 4df1762ca3..da28110d73 100644 --- a/test/core/templates/project-root-guard.test.ts +++ b/test/core/templates/project-root-guard.test.ts @@ -60,6 +60,9 @@ describe('project root guard', () => { it('names the machine-readable signal rather than a guess', () => { expect(PROJECT_ROOT_GUARD).toContain('openspec list --json'); expect(PROJECT_ROOT_GUARD).toContain('`"root": null`'); + // An agent that reads the non-zero exit as a broken CLI is one step from + // hand-creating `openspec/` instead, which is the failure being guarded. + expect(PROJECT_ROOT_GUARD).toContain('exits non-zero in that case'); }); it('hands the decision to the user instead of setting the project up', () => { diff --git a/test/core/templates/skill-templates-parity.test.ts b/test/core/templates/skill-templates-parity.test.ts index a66d01639a..33447934dd 100644 --- a/test/core/templates/skill-templates-parity.test.ts +++ b/test/core/templates/skill-templates-parity.test.ts @@ -38,46 +38,46 @@ import { import { STORE_SELECTION_GUIDANCE } from '../../../src/core/templates/workflows/store-selection.js'; const EXPECTED_FUNCTION_HASHES: Record = { - getExploreSkillTemplate: 'f0b94c622a03ee90b285d2111b80ece4e0bc036f20b470b4f2c0d7199fa07e2b', - getNewChangeSkillTemplate: '1f0bd4bd92e510f207b469fa3dd678bed6d796cbd1f0565b70f9c4eb909976f3', - getContinueChangeSkillTemplate: 'cb0e32b760953fd1ab00c12baf1a87da68394ecd9263c952a7e049c55147161c', - getApplyChangeSkillTemplate: 'f22566f746cbcd7562f546b6e48d6cd89eaaf0daa60ba62cc91545c9d5226219', - getFfChangeSkillTemplate: '43a85a3b66fab64b7b2ff78f8e23cc66df22a81c647c1f2f2db1c15b3167f026', - getSyncSpecsSkillTemplate: '655f4352561f50b703c8475e332ca6910f1ab217ade28c510c7b5a625a89acbd', - getOnboardSkillTemplate: 'a8389574768c44bb5b3300b6374a8c3f0bbfad963709e81b04773bbc3e4defc8', - getOpsxExploreCommandTemplate: 'c3d3590d5975428d96236731259451f05016e2ebe733072094eea7bd5d190084', - getOpsxNewCommandTemplate: 'b9d8823cfd6dc912eb5472d501178ef53fa3fcb6af45f01955ba2ac59fa9de59', - getOpsxContinueCommandTemplate: '58b33b64f2c6990902d6a1d77f3280c92ed38b74e4d3b046b7200e0dba4fdec2', - getOpsxApplyCommandTemplate: '1b3fee846ce08a5eda6efe52f9a22d506162927949ea0ac2331a3e0b8e2679b0', - getOpsxFfCommandTemplate: '388b291a4916f9230ec7fe392253d435ca74b72e7fbabdaf3cfabf879382d5bd', - getArchiveChangeSkillTemplate: '5069e40df61aee97806226a8ef309fa51d1bd98f973893cc860359fa395865b3', - getBulkArchiveChangeSkillTemplate: 'b64b15d33815f7c50215ad4ecfe069c65beddc5b6c56a671daa8610f2b57debe', - getOpsxSyncCommandTemplate: 'b2576135394c0d61c380f591d00eeb628063233bd58e409c14bd9bed4918adf3', - getVerifyChangeSkillTemplate: '31d1a92554a4d664d586d88615c302f6507ae48cd234dd7067f67c148ee88967', - getOpsxArchiveCommandTemplate: '6e034773cb0df424e2ce4b3b5018993cbc3eb732da58c219931306cf879db9cc', - getOpsxOnboardCommandTemplate: '88387b452ca5409af6510e5944e976119a350069529bc719d24887dd7011c60e', - getOpsxBulkArchiveCommandTemplate: '68f584b4dc3b42219e5c98cbe07c38bc47e9795a85e201fa53c8015df89ffbd5', - getOpsxVerifyCommandTemplate: 'eb6a8b746b50791de251111bc7767870af18b70935125cfd714178eefa6894bd', - getOpsxProposeSkillTemplate: '0778c431d3c8feaedeb84870314eabb9636cd43042a4a9dc8e848ccd2ae8b7aa', - getOpsxProposeCommandTemplate: 'bcf0767b51795fb4a95871c0e1dcc025ab54a2386af9a8d42099d1726ef2c307', + getExploreSkillTemplate: '16999a190ca45039accc5f35962c9af2730915871b595d8315b16a5de5bb2708', + getNewChangeSkillTemplate: 'f92768e5cf19a9ae09ce3baf35ef5c7d8a116d1690e8a91f89d50dbc79971798', + getContinueChangeSkillTemplate: 'd554a621582785beb18ee47cbb7f7cd401b24d88fd880147540a7deec8f98088', + getApplyChangeSkillTemplate: '9949cc0fbbf9f70e3b3598936700563617c2bc1cbc00417e9861102c81f21a11', + getFfChangeSkillTemplate: 'ee47c4c7c198c08c072b7ce83474d0fa5dcb261ad520a4b03ad39e0c21e397bd', + getSyncSpecsSkillTemplate: '1f0dc5a1b899ee58dea9c0ba1d2e1c8f36696ce0ce4cc9feb4695fca5f5611e5', + getOnboardSkillTemplate: '83fca17d6d8b67b8fb8d55f42cbd94052208e588b915f9534821c2ed5cfa81e8', + getOpsxExploreCommandTemplate: '3a80e9180834eb39b8870bf0040f847a161ea89e2904fef20f361aeef271305c', + getOpsxNewCommandTemplate: 'd8124a085bb36840a3ced2dd1a0dd109c281feecd5892524a426d1769d8ede92', + getOpsxContinueCommandTemplate: '3c5b8c50ff15e4e5dd526591531e25458df079069e4d2e60d9ba5de3c159d097', + getOpsxApplyCommandTemplate: '583a72caf2f31c830984110ad40b06fb8e77137851dece0c1c146b6b93867496', + getOpsxFfCommandTemplate: '34f6f4d28734e3571f41c8790e0a5dc18cf50c6ca63a4d3605c5f1909316e2d6', + getArchiveChangeSkillTemplate: '47d8817f7e1b4433e639bccfadac667dcac07b79bdcf6f3a34987b6ab20cb601', + getBulkArchiveChangeSkillTemplate: 'caae7ade139efead2cb501fd20ef1772d8937664add541d9d91b3867965722d8', + getOpsxSyncCommandTemplate: '9b77ae927f0decfbbf0a3ddf02bd50c3e4642190402318447ab449414fd219d6', + getVerifyChangeSkillTemplate: '7fa735e9d9aca1ea7d0fea9be74406f9cd60fca78191fb72bbd2e26c1c3893e6', + getOpsxArchiveCommandTemplate: 'de3e85e14c2e382c44a652c62883423bcfc99c728cee574f2f5c35f0d6cb9241', + getOpsxOnboardCommandTemplate: 'fcb626fa8a4d4017821b3720413279fb8f9973433b08bb2ebe5582c2e3b8c428', + getOpsxBulkArchiveCommandTemplate: '11a9260721583c188ee886a3e98aa6009f2bb684bc78e851a33056854542af57', + getOpsxVerifyCommandTemplate: 'b3df65f24c51fb606c92e88d304d0603b6a2ed78bce3b6893a10815ceaa6dc9d', + getOpsxProposeSkillTemplate: '58701e927038d200afbff4bad556b8bc1955a99911f7638cf9d147e2d15012ce', + getOpsxProposeCommandTemplate: '8b928db81b048b91a18c89231e698f2e41d6d7877349de8297710b13bae17b21', getFeedbackSkillTemplate: 'dabeb5e825b9349abc8156c3e7b8608f27987912a6d9bf47ef29addde6138133', - getUpdateChangeSkillTemplate: '528bf852a3aea7854cc30714a987a2de8484a8996fcb46fa18912c2dcdf98311', - getOpsxUpdateCommandTemplate: '8f9454edc53e58f1974e8360db51b9e9f85e2d94003d5ef7376e469363913852', + getUpdateChangeSkillTemplate: '2e93cbbc3599a39601017b8df98ac1b56037fa7c75f3afb6e528dd1a1cecd1dc', + getOpsxUpdateCommandTemplate: '9859b917b4e91720c7c764aa852f6acb59971b2a223537f894eaa4a413d49a0a', }; const EXPECTED_GENERATED_SKILL_CONTENT_HASHES: Record = { - 'openspec-explore': '9ec52ca89d750c451fec090ccc81738eba67f4f64f4ca63d8beaf6d50ca46037', - 'openspec-new-change': 'bcbaf1d7da7caa616e2fa219f753a87458906d0f7bcbe359a11cccad727fbc24', - 'openspec-continue-change': '01f40217fae9d644471f00ba751dcb61a66fc42cd62c88fa5f5366932a0a2031', - 'openspec-apply-change': '629d3d9c9ecf7d7abfc41379fb7c0b0364834b4b5e49f449a2e66b454653b73e', - 'openspec-ff-change': 'a47c7adbc079731e981e4d5aa4da926cd0fa411263df4de3c40dec71c161f85c', - 'openspec-sync-specs': '5662d165ace17ca9f860973a009ecb2172456151da5b73ac1b7778c27a724ab8', - 'openspec-archive-change': '5102469cf8cd65e2dee9b1917755c894d10bf6314dd06779753d9ddd6b13686c', - 'openspec-bulk-archive-change': 'bd1e74c8c0baac58061a6ed44aa9aa63cd75c8f10c97575bf24145262a3f410b', - 'openspec-verify-change': '275eda987e1dff2dd93c94e81dbfcf112f7536676eaf2fdeaf2ac443d74d4e6f', - 'openspec-onboard': '6f64d9e70901a8f7c22f8cce576d67634232a9c7f74ecffb7a8bdee311775856', - 'openspec-propose': '9a7143fca06276c36f84096f6d37d02a029e078f5cd19519402bd6a358ca5b79', - 'openspec-update-change': 'd420226c3b6c7dc9d63e4890fa244642bcf5efa839d61acb65a8f32a0322fa95', + 'openspec-explore': 'a6c0b69f6b46df5fac8631f81cc01277740807e54bb4d6fa2483ca89fa4d700d', + 'openspec-new-change': 'caaae53e3ad10bfaf8b59c11344918d4691ee9a112a7d6422ed7f74ca58fe906', + 'openspec-continue-change': 'ea9138711d13027646a20f9be6d96a39790be951f4283dd1d58a36f7633dc209', + 'openspec-apply-change': '229735de3079455172862e4db4efcba37c3894ceaf8c23604444ba98d43ffecd', + 'openspec-ff-change': '74ea6ccf191f0cd9007ae61913471ea0f9945201fab7fbfca20f2ba22c8e4704', + 'openspec-sync-specs': '1c54e0d16e205da7ed9b08f36b49ff3236c2c362cfc7db82ccf5ae87143da0eb', + 'openspec-archive-change': 'b8a7948e7f1a8b821e995b69bb942cba6d82ff2d3d4f7a60b08100e4bb5eff21', + 'openspec-bulk-archive-change': 'd24c38080a5590db9959e484ad1d3a5516af3208eecd33ea07a0e339e832e2fd', + 'openspec-verify-change': '838727cf999b11466942fc60420d34b1ebfa18b808e5c7c2571a0e32187104f3', + 'openspec-onboard': '178a1053ae03a5b8d954b2df4c3a5a93ded539c963cb89e4846fd0197c003825', + 'openspec-propose': 'ec1d61d20a46c5015145bfe0a1318feb2adf5376ec38130c875b1819443ca975', + 'openspec-update-change': '764dac81fd09a18b5d903c9f1f5712f4de068f30cac77054d533f2917222d598', }; // Intentionally excludes getFeedbackSkillTemplate: this list only models templates From 78d202789695cb0c9da23d7ff953ead304c7d742 Mon Sep 17 00:00:00 2001 From: Clay Good Date: Fri, 4 Sep 2026 12:56:12 -0500 Subject: [PATCH 09/15] style(skills): read the guard back and untangle its two 'in that case' clauses Co-Authored-By: Claude Opus 5 --- skills/openspec-apply-change/SKILL.md | 2 +- skills/openspec-archive-change/SKILL.md | 2 +- skills/openspec-bulk-archive-change/SKILL.md | 2 +- skills/openspec-continue-change/SKILL.md | 2 +- skills/openspec-explore/SKILL.md | 2 +- skills/openspec-ff-change/SKILL.md | 2 +- skills/openspec-new-change/SKILL.md | 2 +- skills/openspec-onboard/SKILL.md | 2 +- skills/openspec-propose/SKILL.md | 2 +- skills/openspec-sync-specs/SKILL.md | 2 +- skills/openspec-update-change/SKILL.md | 2 +- skills/openspec-verify-change/SKILL.md | 2 +- src/core/templates/workflows/project-root.ts | 2 +- .../core/templates/project-root-guard.test.ts | 2 +- .../templates/skill-templates-parity.test.ts | 72 +++++++++---------- 15 files changed, 50 insertions(+), 50 deletions(-) diff --git a/skills/openspec-apply-change/SKILL.md b/skills/openspec-apply-change/SKILL.md index b727e5f065..4165807edc 100644 --- a/skills/openspec-apply-change/SKILL.md +++ b/skills/openspec-apply-change/SKILL.md @@ -13,7 +13,7 @@ Implement tasks from an OpenSpec change. **Store selection:** If the user names a store (a store is a standalone OpenSpec repo registered on this machine) or the work lives in one, run `openspec store list --json` to discover registered store ids, then pass `--store ` on the commands that read or write specs and changes (`new change`, `status`, `instructions`, `list`, `show`, `validate`, `archive`, `doctor`, `context`, `schemas`, `view`). Once selected, treat `--store ` as sticky for the rest of the workflow. Every unscoped example of those commands below is shorthand: before running it, append the flag. For example, run `openspec status --change "" --json --store ""`, not the unscoped form shown below. Other commands do not take the flag. Hints printed by commands already carry the flag; keep it on follow-ups. Without a store, commands act on the nearest local `openspec/` root. -**Project check:** These steps expect a project that already uses OpenSpec. Before the first step that writes anything (`new change`, `archive`, `sync specs`, or authoring an artifact file), confirm the project has a root: run `openspec list --json` and read `root`. A root object means the project is set up. `"root": null` means it is not - there is no `openspec/` directory here, and a write such as `openspec new change` would create one as a side effect. The command exits non-zero in that case: that is the answer to the question, not a broken CLI, so read the JSON rather than retrying or working around it. In that case stop before writing and ask the user how to proceed: set this project up (`openspec init`), target a store they already have (`--store `), or drop OpenSpec for this request and help them directly. Wait for their answer. Do not run `openspec init` until they ask for it, do not hand-create `openspec/` files, and do not let a command create the root as a side effect. +**Project check:** These steps expect a project that already uses OpenSpec. Before the first step that writes anything (`new change`, `archive`, `sync specs`, or authoring an artifact file), confirm the project has a root: run `openspec list --json` and read `root`. A root object means the project is set up. `"root": null` means it is not - there is no `openspec/` directory here, and a write such as `openspec new change` would create one as a side effect. The command also exits non-zero, which is that answer rather than a broken CLI, so read the JSON instead of retrying or working around it. Then stop before writing and ask the user how to proceed: set this project up (`openspec init`), target a store they already have (`--store `), or drop OpenSpec for this request and help them directly. Wait for their answer. Do not run `openspec init` until they ask for it, do not hand-create `openspec/` files, and do not let a command create the root as a side effect. **Input**: Optionally specify a change name (e.g., `/openspec-apply-change add-auth`). If omitted, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes. diff --git a/skills/openspec-archive-change/SKILL.md b/skills/openspec-archive-change/SKILL.md index 4de7fe870f..6ba0cdc96e 100644 --- a/skills/openspec-archive-change/SKILL.md +++ b/skills/openspec-archive-change/SKILL.md @@ -13,7 +13,7 @@ Archive a completed change in the experimental workflow. **Store selection:** If the user names a store (a store is a standalone OpenSpec repo registered on this machine) or the work lives in one, run `openspec store list --json` to discover registered store ids, then pass `--store ` on the commands that read or write specs and changes (`new change`, `status`, `instructions`, `list`, `show`, `validate`, `archive`, `doctor`, `context`, `schemas`, `view`). Once selected, treat `--store ` as sticky for the rest of the workflow. Every unscoped example of those commands below is shorthand: before running it, append the flag. For example, run `openspec status --change "" --json --store ""`, not the unscoped form shown below. Other commands do not take the flag. Hints printed by commands already carry the flag; keep it on follow-ups. Without a store, commands act on the nearest local `openspec/` root. -**Project check:** These steps expect a project that already uses OpenSpec. Before the first step that writes anything (`new change`, `archive`, `sync specs`, or authoring an artifact file), confirm the project has a root: run `openspec list --json` and read `root`. A root object means the project is set up. `"root": null` means it is not - there is no `openspec/` directory here, and a write such as `openspec new change` would create one as a side effect. The command exits non-zero in that case: that is the answer to the question, not a broken CLI, so read the JSON rather than retrying or working around it. In that case stop before writing and ask the user how to proceed: set this project up (`openspec init`), target a store they already have (`--store `), or drop OpenSpec for this request and help them directly. Wait for their answer. Do not run `openspec init` until they ask for it, do not hand-create `openspec/` files, and do not let a command create the root as a side effect. +**Project check:** These steps expect a project that already uses OpenSpec. Before the first step that writes anything (`new change`, `archive`, `sync specs`, or authoring an artifact file), confirm the project has a root: run `openspec list --json` and read `root`. A root object means the project is set up. `"root": null` means it is not - there is no `openspec/` directory here, and a write such as `openspec new change` would create one as a side effect. The command also exits non-zero, which is that answer rather than a broken CLI, so read the JSON instead of retrying or working around it. Then stop before writing and ask the user how to proceed: set this project up (`openspec init`), target a store they already have (`--store `), or drop OpenSpec for this request and help them directly. Wait for their answer. Do not run `openspec init` until they ask for it, do not hand-create `openspec/` files, and do not let a command create the root as a side effect. `` is the spec directory relative to `specs/` (for example, `user-auth` or `identity/user-auth`). Preserve the full path from each delta spec when resolving its main spec. diff --git a/skills/openspec-bulk-archive-change/SKILL.md b/skills/openspec-bulk-archive-change/SKILL.md index 28b13e12ab..419d77bc19 100644 --- a/skills/openspec-bulk-archive-change/SKILL.md +++ b/skills/openspec-bulk-archive-change/SKILL.md @@ -15,7 +15,7 @@ This skill allows you to batch-archive changes, handling spec conflicts intellig **Store selection:** If the user names a store (a store is a standalone OpenSpec repo registered on this machine) or the work lives in one, run `openspec store list --json` to discover registered store ids, then pass `--store ` on the commands that read or write specs and changes (`new change`, `status`, `instructions`, `list`, `show`, `validate`, `archive`, `doctor`, `context`, `schemas`, `view`). Once selected, treat `--store ` as sticky for the rest of the workflow. Every unscoped example of those commands below is shorthand: before running it, append the flag. For example, run `openspec status --change "" --json --store ""`, not the unscoped form shown below. Other commands do not take the flag. Hints printed by commands already carry the flag; keep it on follow-ups. Without a store, commands act on the nearest local `openspec/` root. -**Project check:** These steps expect a project that already uses OpenSpec. Before the first step that writes anything (`new change`, `archive`, `sync specs`, or authoring an artifact file), confirm the project has a root: run `openspec list --json` and read `root`. A root object means the project is set up. `"root": null` means it is not - there is no `openspec/` directory here, and a write such as `openspec new change` would create one as a side effect. The command exits non-zero in that case: that is the answer to the question, not a broken CLI, so read the JSON rather than retrying or working around it. In that case stop before writing and ask the user how to proceed: set this project up (`openspec init`), target a store they already have (`--store `), or drop OpenSpec for this request and help them directly. Wait for their answer. Do not run `openspec init` until they ask for it, do not hand-create `openspec/` files, and do not let a command create the root as a side effect. +**Project check:** These steps expect a project that already uses OpenSpec. Before the first step that writes anything (`new change`, `archive`, `sync specs`, or authoring an artifact file), confirm the project has a root: run `openspec list --json` and read `root`. A root object means the project is set up. `"root": null` means it is not - there is no `openspec/` directory here, and a write such as `openspec new change` would create one as a side effect. The command also exits non-zero, which is that answer rather than a broken CLI, so read the JSON instead of retrying or working around it. Then stop before writing and ask the user how to proceed: set this project up (`openspec init`), target a store they already have (`--store `), or drop OpenSpec for this request and help them directly. Wait for their answer. Do not run `openspec init` until they ask for it, do not hand-create `openspec/` files, and do not let a command create the root as a side effect. `` is the spec directory relative to `specs/` (for example, `user-auth` or `identity/user-auth`). Preserve the full path from each delta spec when resolving its main spec. diff --git a/skills/openspec-continue-change/SKILL.md b/skills/openspec-continue-change/SKILL.md index 5ae6f7560b..ef446f29fb 100644 --- a/skills/openspec-continue-change/SKILL.md +++ b/skills/openspec-continue-change/SKILL.md @@ -13,7 +13,7 @@ Continue working on a change by creating the next artifact. **Store selection:** If the user names a store (a store is a standalone OpenSpec repo registered on this machine) or the work lives in one, run `openspec store list --json` to discover registered store ids, then pass `--store ` on the commands that read or write specs and changes (`new change`, `status`, `instructions`, `list`, `show`, `validate`, `archive`, `doctor`, `context`, `schemas`, `view`). Once selected, treat `--store ` as sticky for the rest of the workflow. Every unscoped example of those commands below is shorthand: before running it, append the flag. For example, run `openspec status --change "" --json --store ""`, not the unscoped form shown below. Other commands do not take the flag. Hints printed by commands already carry the flag; keep it on follow-ups. Without a store, commands act on the nearest local `openspec/` root. -**Project check:** These steps expect a project that already uses OpenSpec. Before the first step that writes anything (`new change`, `archive`, `sync specs`, or authoring an artifact file), confirm the project has a root: run `openspec list --json` and read `root`. A root object means the project is set up. `"root": null` means it is not - there is no `openspec/` directory here, and a write such as `openspec new change` would create one as a side effect. The command exits non-zero in that case: that is the answer to the question, not a broken CLI, so read the JSON rather than retrying or working around it. In that case stop before writing and ask the user how to proceed: set this project up (`openspec init`), target a store they already have (`--store `), or drop OpenSpec for this request and help them directly. Wait for their answer. Do not run `openspec init` until they ask for it, do not hand-create `openspec/` files, and do not let a command create the root as a side effect. +**Project check:** These steps expect a project that already uses OpenSpec. Before the first step that writes anything (`new change`, `archive`, `sync specs`, or authoring an artifact file), confirm the project has a root: run `openspec list --json` and read `root`. A root object means the project is set up. `"root": null` means it is not - there is no `openspec/` directory here, and a write such as `openspec new change` would create one as a side effect. The command also exits non-zero, which is that answer rather than a broken CLI, so read the JSON instead of retrying or working around it. Then stop before writing and ask the user how to proceed: set this project up (`openspec init`), target a store they already have (`--store `), or drop OpenSpec for this request and help them directly. Wait for their answer. Do not run `openspec init` until they ask for it, do not hand-create `openspec/` files, and do not let a command create the root as a side effect. **Input**: Optionally specify a change name. If omitted, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes. diff --git a/skills/openspec-explore/SKILL.md b/skills/openspec-explore/SKILL.md index 6887ea50aa..df66c6319e 100644 --- a/skills/openspec-explore/SKILL.md +++ b/skills/openspec-explore/SKILL.md @@ -17,7 +17,7 @@ Enter explore mode. Think deeply. Visualize freely. Follow the conversation wher **Store selection:** If the user names a store (a store is a standalone OpenSpec repo registered on this machine) or the work lives in one, run `openspec store list --json` to discover registered store ids, then pass `--store ` on the commands that read or write specs and changes (`new change`, `status`, `instructions`, `list`, `show`, `validate`, `archive`, `doctor`, `context`, `schemas`, `view`). Once selected, treat `--store ` as sticky for the rest of the workflow. Every unscoped example of those commands below is shorthand: before running it, append the flag. For example, run `openspec status --change "" --json --store ""`, not the unscoped form shown below. Other commands do not take the flag. Hints printed by commands already carry the flag; keep it on follow-ups. Without a store, commands act on the nearest local `openspec/` root. -**Project check:** These steps expect a project that already uses OpenSpec. Before the first step that writes anything (`new change`, `archive`, `sync specs`, or authoring an artifact file), confirm the project has a root: run `openspec list --json` and read `root`. A root object means the project is set up. `"root": null` means it is not - there is no `openspec/` directory here, and a write such as `openspec new change` would create one as a side effect. The command exits non-zero in that case: that is the answer to the question, not a broken CLI, so read the JSON rather than retrying or working around it. In that case stop before writing and ask the user how to proceed: set this project up (`openspec init`), target a store they already have (`--store `), or drop OpenSpec for this request and help them directly. Wait for their answer. Do not run `openspec init` until they ask for it, do not hand-create `openspec/` files, and do not let a command create the root as a side effect. +**Project check:** These steps expect a project that already uses OpenSpec. Before the first step that writes anything (`new change`, `archive`, `sync specs`, or authoring an artifact file), confirm the project has a root: run `openspec list --json` and read `root`. A root object means the project is set up. `"root": null` means it is not - there is no `openspec/` directory here, and a write such as `openspec new change` would create one as a side effect. The command also exits non-zero, which is that answer rather than a broken CLI, so read the JSON instead of retrying or working around it. Then stop before writing and ask the user how to proceed: set this project up (`openspec init`), target a store they already have (`--store `), or drop OpenSpec for this request and help them directly. Wait for their answer. Do not run `openspec init` until they ask for it, do not hand-create `openspec/` files, and do not let a command create the root as a side effect. --- diff --git a/skills/openspec-ff-change/SKILL.md b/skills/openspec-ff-change/SKILL.md index f0964b9b8a..e623cab936 100644 --- a/skills/openspec-ff-change/SKILL.md +++ b/skills/openspec-ff-change/SKILL.md @@ -13,7 +13,7 @@ Fast-forward through artifact creation - generate everything needed to start imp **Store selection:** If the user names a store (a store is a standalone OpenSpec repo registered on this machine) or the work lives in one, run `openspec store list --json` to discover registered store ids, then pass `--store ` on the commands that read or write specs and changes (`new change`, `status`, `instructions`, `list`, `show`, `validate`, `archive`, `doctor`, `context`, `schemas`, `view`). Once selected, treat `--store ` as sticky for the rest of the workflow. Every unscoped example of those commands below is shorthand: before running it, append the flag. For example, run `openspec status --change "" --json --store ""`, not the unscoped form shown below. Other commands do not take the flag. Hints printed by commands already carry the flag; keep it on follow-ups. Without a store, commands act on the nearest local `openspec/` root. -**Project check:** These steps expect a project that already uses OpenSpec. Before the first step that writes anything (`new change`, `archive`, `sync specs`, or authoring an artifact file), confirm the project has a root: run `openspec list --json` and read `root`. A root object means the project is set up. `"root": null` means it is not - there is no `openspec/` directory here, and a write such as `openspec new change` would create one as a side effect. The command exits non-zero in that case: that is the answer to the question, not a broken CLI, so read the JSON rather than retrying or working around it. In that case stop before writing and ask the user how to proceed: set this project up (`openspec init`), target a store they already have (`--store `), or drop OpenSpec for this request and help them directly. Wait for their answer. Do not run `openspec init` until they ask for it, do not hand-create `openspec/` files, and do not let a command create the root as a side effect. +**Project check:** These steps expect a project that already uses OpenSpec. Before the first step that writes anything (`new change`, `archive`, `sync specs`, or authoring an artifact file), confirm the project has a root: run `openspec list --json` and read `root`. A root object means the project is set up. `"root": null` means it is not - there is no `openspec/` directory here, and a write such as `openspec new change` would create one as a side effect. The command also exits non-zero, which is that answer rather than a broken CLI, so read the JSON instead of retrying or working around it. Then stop before writing and ask the user how to proceed: set this project up (`openspec init`), target a store they already have (`--store `), or drop OpenSpec for this request and help them directly. Wait for their answer. Do not run `openspec init` until they ask for it, do not hand-create `openspec/` files, and do not let a command create the root as a side effect. **Input**: The user's request should include a change name (kebab-case) OR a description of what they want to build. diff --git a/skills/openspec-new-change/SKILL.md b/skills/openspec-new-change/SKILL.md index 60163bd47d..1c4f0a5a72 100644 --- a/skills/openspec-new-change/SKILL.md +++ b/skills/openspec-new-change/SKILL.md @@ -13,7 +13,7 @@ Start a new change using the experimental artifact-driven approach. **Store selection:** If the user names a store (a store is a standalone OpenSpec repo registered on this machine) or the work lives in one, run `openspec store list --json` to discover registered store ids, then pass `--store ` on the commands that read or write specs and changes (`new change`, `status`, `instructions`, `list`, `show`, `validate`, `archive`, `doctor`, `context`, `schemas`, `view`). Once selected, treat `--store ` as sticky for the rest of the workflow. Every unscoped example of those commands below is shorthand: before running it, append the flag. For example, run `openspec status --change "" --json --store ""`, not the unscoped form shown below. Other commands do not take the flag. Hints printed by commands already carry the flag; keep it on follow-ups. Without a store, commands act on the nearest local `openspec/` root. -**Project check:** These steps expect a project that already uses OpenSpec. Before the first step that writes anything (`new change`, `archive`, `sync specs`, or authoring an artifact file), confirm the project has a root: run `openspec list --json` and read `root`. A root object means the project is set up. `"root": null` means it is not - there is no `openspec/` directory here, and a write such as `openspec new change` would create one as a side effect. The command exits non-zero in that case: that is the answer to the question, not a broken CLI, so read the JSON rather than retrying or working around it. In that case stop before writing and ask the user how to proceed: set this project up (`openspec init`), target a store they already have (`--store `), or drop OpenSpec for this request and help them directly. Wait for their answer. Do not run `openspec init` until they ask for it, do not hand-create `openspec/` files, and do not let a command create the root as a side effect. +**Project check:** These steps expect a project that already uses OpenSpec. Before the first step that writes anything (`new change`, `archive`, `sync specs`, or authoring an artifact file), confirm the project has a root: run `openspec list --json` and read `root`. A root object means the project is set up. `"root": null` means it is not - there is no `openspec/` directory here, and a write such as `openspec new change` would create one as a side effect. The command also exits non-zero, which is that answer rather than a broken CLI, so read the JSON instead of retrying or working around it. Then stop before writing and ask the user how to proceed: set this project up (`openspec init`), target a store they already have (`--store `), or drop OpenSpec for this request and help them directly. Wait for their answer. Do not run `openspec init` until they ask for it, do not hand-create `openspec/` files, and do not let a command create the root as a side effect. **Input**: The user's request should include a change name (kebab-case) OR a description of what they want to build. diff --git a/skills/openspec-onboard/SKILL.md b/skills/openspec-onboard/SKILL.md index 2f21f48770..0323181bde 100644 --- a/skills/openspec-onboard/SKILL.md +++ b/skills/openspec-onboard/SKILL.md @@ -13,7 +13,7 @@ Guide the user through their first complete OpenSpec workflow cycle. This is a t **Store selection:** If the user names a store (a store is a standalone OpenSpec repo registered on this machine) or the work lives in one, run `openspec store list --json` to discover registered store ids, then pass `--store ` on the commands that read or write specs and changes (`new change`, `status`, `instructions`, `list`, `show`, `validate`, `archive`, `doctor`, `context`, `schemas`, `view`). Once selected, treat `--store ` as sticky for the rest of the workflow. Every unscoped example of those commands below is shorthand: before running it, append the flag. For example, run `openspec status --change "" --json --store ""`, not the unscoped form shown below. Other commands do not take the flag. Hints printed by commands already carry the flag; keep it on follow-ups. Without a store, commands act on the nearest local `openspec/` root. -**Project check:** These steps expect a project that already uses OpenSpec. Before the first step that writes anything (`new change`, `archive`, `sync specs`, or authoring an artifact file), confirm the project has a root: run `openspec list --json` and read `root`. A root object means the project is set up. `"root": null` means it is not - there is no `openspec/` directory here, and a write such as `openspec new change` would create one as a side effect. The command exits non-zero in that case: that is the answer to the question, not a broken CLI, so read the JSON rather than retrying or working around it. In that case stop before writing and ask the user how to proceed: set this project up (`openspec init`), target a store they already have (`--store `), or drop OpenSpec for this request and help them directly. Wait for their answer. Do not run `openspec init` until they ask for it, do not hand-create `openspec/` files, and do not let a command create the root as a side effect. +**Project check:** These steps expect a project that already uses OpenSpec. Before the first step that writes anything (`new change`, `archive`, `sync specs`, or authoring an artifact file), confirm the project has a root: run `openspec list --json` and read `root`. A root object means the project is set up. `"root": null` means it is not - there is no `openspec/` directory here, and a write such as `openspec new change` would create one as a side effect. The command also exits non-zero, which is that answer rather than a broken CLI, so read the JSON instead of retrying or working around it. Then stop before writing and ask the user how to proceed: set this project up (`openspec init`), target a store they already have (`--store `), or drop OpenSpec for this request and help them directly. Wait for their answer. Do not run `openspec init` until they ask for it, do not hand-create `openspec/` files, and do not let a command create the root as a side effect. --- diff --git a/skills/openspec-propose/SKILL.md b/skills/openspec-propose/SKILL.md index c6deeb1939..fd45b7ed61 100644 --- a/skills/openspec-propose/SKILL.md +++ b/skills/openspec-propose/SKILL.md @@ -27,7 +27,7 @@ When the user is ready to implement, they must start the apply workflow explicit **Store selection:** If the user names a store (a store is a standalone OpenSpec repo registered on this machine) or the work lives in one, run `openspec store list --json` to discover registered store ids, then pass `--store ` on the commands that read or write specs and changes (`new change`, `status`, `instructions`, `list`, `show`, `validate`, `archive`, `doctor`, `context`, `schemas`, `view`). Once selected, treat `--store ` as sticky for the rest of the workflow. Every unscoped example of those commands below is shorthand: before running it, append the flag. For example, run `openspec status --change "" --json --store ""`, not the unscoped form shown below. Other commands do not take the flag. Hints printed by commands already carry the flag; keep it on follow-ups. Without a store, commands act on the nearest local `openspec/` root. -**Project check:** These steps expect a project that already uses OpenSpec. Before the first step that writes anything (`new change`, `archive`, `sync specs`, or authoring an artifact file), confirm the project has a root: run `openspec list --json` and read `root`. A root object means the project is set up. `"root": null` means it is not - there is no `openspec/` directory here, and a write such as `openspec new change` would create one as a side effect. The command exits non-zero in that case: that is the answer to the question, not a broken CLI, so read the JSON rather than retrying or working around it. In that case stop before writing and ask the user how to proceed: set this project up (`openspec init`), target a store they already have (`--store `), or drop OpenSpec for this request and help them directly. Wait for their answer. Do not run `openspec init` until they ask for it, do not hand-create `openspec/` files, and do not let a command create the root as a side effect. +**Project check:** These steps expect a project that already uses OpenSpec. Before the first step that writes anything (`new change`, `archive`, `sync specs`, or authoring an artifact file), confirm the project has a root: run `openspec list --json` and read `root`. A root object means the project is set up. `"root": null` means it is not - there is no `openspec/` directory here, and a write such as `openspec new change` would create one as a side effect. The command also exits non-zero, which is that answer rather than a broken CLI, so read the JSON instead of retrying or working around it. Then stop before writing and ask the user how to proceed: set this project up (`openspec init`), target a store they already have (`--store `), or drop OpenSpec for this request and help them directly. Wait for their answer. Do not run `openspec init` until they ask for it, do not hand-create `openspec/` files, and do not let a command create the root as a side effect. **Input**: The user's request should include a change name (kebab-case) OR a description of what they want to build. diff --git a/skills/openspec-sync-specs/SKILL.md b/skills/openspec-sync-specs/SKILL.md index 8813b4e3be..9c03b62872 100644 --- a/skills/openspec-sync-specs/SKILL.md +++ b/skills/openspec-sync-specs/SKILL.md @@ -15,7 +15,7 @@ This is an **agent-driven** operation - you will read delta specs and directly e **Store selection:** If the user names a store (a store is a standalone OpenSpec repo registered on this machine) or the work lives in one, run `openspec store list --json` to discover registered store ids, then pass `--store ` on the commands that read or write specs and changes (`new change`, `status`, `instructions`, `list`, `show`, `validate`, `archive`, `doctor`, `context`, `schemas`, `view`). Once selected, treat `--store ` as sticky for the rest of the workflow. Every unscoped example of those commands below is shorthand: before running it, append the flag. For example, run `openspec status --change "" --json --store ""`, not the unscoped form shown below. Other commands do not take the flag. Hints printed by commands already carry the flag; keep it on follow-ups. Without a store, commands act on the nearest local `openspec/` root. -**Project check:** These steps expect a project that already uses OpenSpec. Before the first step that writes anything (`new change`, `archive`, `sync specs`, or authoring an artifact file), confirm the project has a root: run `openspec list --json` and read `root`. A root object means the project is set up. `"root": null` means it is not - there is no `openspec/` directory here, and a write such as `openspec new change` would create one as a side effect. The command exits non-zero in that case: that is the answer to the question, not a broken CLI, so read the JSON rather than retrying or working around it. In that case stop before writing and ask the user how to proceed: set this project up (`openspec init`), target a store they already have (`--store `), or drop OpenSpec for this request and help them directly. Wait for their answer. Do not run `openspec init` until they ask for it, do not hand-create `openspec/` files, and do not let a command create the root as a side effect. +**Project check:** These steps expect a project that already uses OpenSpec. Before the first step that writes anything (`new change`, `archive`, `sync specs`, or authoring an artifact file), confirm the project has a root: run `openspec list --json` and read `root`. A root object means the project is set up. `"root": null` means it is not - there is no `openspec/` directory here, and a write such as `openspec new change` would create one as a side effect. The command also exits non-zero, which is that answer rather than a broken CLI, so read the JSON instead of retrying or working around it. Then stop before writing and ask the user how to proceed: set this project up (`openspec init`), target a store they already have (`--store `), or drop OpenSpec for this request and help them directly. Wait for their answer. Do not run `openspec init` until they ask for it, do not hand-create `openspec/` files, and do not let a command create the root as a side effect. `` is the spec directory relative to `specs/` (for example, `user-auth` or `identity/user-auth`). Preserve the full path from each delta spec when resolving its main spec. diff --git a/skills/openspec-update-change/SKILL.md b/skills/openspec-update-change/SKILL.md index 417c444aef..6430310067 100644 --- a/skills/openspec-update-change/SKILL.md +++ b/skills/openspec-update-change/SKILL.md @@ -13,7 +13,7 @@ Revise a change's existing planning artifacts and keep them coherent. Never edit **Store selection:** If the user names a store (a store is a standalone OpenSpec repo registered on this machine) or the work lives in one, run `openspec store list --json` to discover registered store ids, then pass `--store ` on the commands that read or write specs and changes (`new change`, `status`, `instructions`, `list`, `show`, `validate`, `archive`, `doctor`, `context`, `schemas`, `view`). Once selected, treat `--store ` as sticky for the rest of the workflow. Every unscoped example of those commands below is shorthand: before running it, append the flag. For example, run `openspec status --change "" --json --store ""`, not the unscoped form shown below. Other commands do not take the flag. Hints printed by commands already carry the flag; keep it on follow-ups. Without a store, commands act on the nearest local `openspec/` root. -**Project check:** These steps expect a project that already uses OpenSpec. Before the first step that writes anything (`new change`, `archive`, `sync specs`, or authoring an artifact file), confirm the project has a root: run `openspec list --json` and read `root`. A root object means the project is set up. `"root": null` means it is not - there is no `openspec/` directory here, and a write such as `openspec new change` would create one as a side effect. The command exits non-zero in that case: that is the answer to the question, not a broken CLI, so read the JSON rather than retrying or working around it. In that case stop before writing and ask the user how to proceed: set this project up (`openspec init`), target a store they already have (`--store `), or drop OpenSpec for this request and help them directly. Wait for their answer. Do not run `openspec init` until they ask for it, do not hand-create `openspec/` files, and do not let a command create the root as a side effect. +**Project check:** These steps expect a project that already uses OpenSpec. Before the first step that writes anything (`new change`, `archive`, `sync specs`, or authoring an artifact file), confirm the project has a root: run `openspec list --json` and read `root`. A root object means the project is set up. `"root": null` means it is not - there is no `openspec/` directory here, and a write such as `openspec new change` would create one as a side effect. The command also exits non-zero, which is that answer rather than a broken CLI, so read the JSON instead of retrying or working around it. Then stop before writing and ask the user how to proceed: set this project up (`openspec init`), target a store they already have (`--store `), or drop OpenSpec for this request and help them directly. Wait for their answer. Do not run `openspec init` until they ask for it, do not hand-create `openspec/` files, and do not let a command create the root as a side effect. **Input**: Optionally specify a change name. If omitted, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes. diff --git a/skills/openspec-verify-change/SKILL.md b/skills/openspec-verify-change/SKILL.md index 54a8047603..aaddf7ac41 100644 --- a/skills/openspec-verify-change/SKILL.md +++ b/skills/openspec-verify-change/SKILL.md @@ -13,7 +13,7 @@ Verify that an implementation matches the change artifacts (specs, tasks, design **Store selection:** If the user names a store (a store is a standalone OpenSpec repo registered on this machine) or the work lives in one, run `openspec store list --json` to discover registered store ids, then pass `--store ` on the commands that read or write specs and changes (`new change`, `status`, `instructions`, `list`, `show`, `validate`, `archive`, `doctor`, `context`, `schemas`, `view`). Once selected, treat `--store ` as sticky for the rest of the workflow. Every unscoped example of those commands below is shorthand: before running it, append the flag. For example, run `openspec status --change "" --json --store ""`, not the unscoped form shown below. Other commands do not take the flag. Hints printed by commands already carry the flag; keep it on follow-ups. Without a store, commands act on the nearest local `openspec/` root. -**Project check:** These steps expect a project that already uses OpenSpec. Before the first step that writes anything (`new change`, `archive`, `sync specs`, or authoring an artifact file), confirm the project has a root: run `openspec list --json` and read `root`. A root object means the project is set up. `"root": null` means it is not - there is no `openspec/` directory here, and a write such as `openspec new change` would create one as a side effect. The command exits non-zero in that case: that is the answer to the question, not a broken CLI, so read the JSON rather than retrying or working around it. In that case stop before writing and ask the user how to proceed: set this project up (`openspec init`), target a store they already have (`--store `), or drop OpenSpec for this request and help them directly. Wait for their answer. Do not run `openspec init` until they ask for it, do not hand-create `openspec/` files, and do not let a command create the root as a side effect. +**Project check:** These steps expect a project that already uses OpenSpec. Before the first step that writes anything (`new change`, `archive`, `sync specs`, or authoring an artifact file), confirm the project has a root: run `openspec list --json` and read `root`. A root object means the project is set up. `"root": null` means it is not - there is no `openspec/` directory here, and a write such as `openspec new change` would create one as a side effect. The command also exits non-zero, which is that answer rather than a broken CLI, so read the JSON instead of retrying or working around it. Then stop before writing and ask the user how to proceed: set this project up (`openspec init`), target a store they already have (`--store `), or drop OpenSpec for this request and help them directly. Wait for their answer. Do not run `openspec init` until they ask for it, do not hand-create `openspec/` files, and do not let a command create the root as a side effect. **Input**: Optionally specify a change name. If omitted, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes. diff --git a/src/core/templates/workflows/project-root.ts b/src/core/templates/workflows/project-root.ts index 5191b55890..fc794d49b0 100644 --- a/src/core/templates/workflows/project-root.ts +++ b/src/core/templates/workflows/project-root.ts @@ -13,4 +13,4 @@ * because it refuses to fabricate an implicit root: it reports `root: null` * both when nothing is set up and when only stores are registered. */ -export const PROJECT_ROOT_GUARD = `**Project check:** These steps expect a project that already uses OpenSpec. Before the first step that writes anything (\`new change\`, \`archive\`, \`sync specs\`, or authoring an artifact file), confirm the project has a root: run \`openspec list --json\` and read \`root\`. A root object means the project is set up. \`"root": null\` means it is not - there is no \`openspec/\` directory here, and a write such as \`openspec new change\` would create one as a side effect. The command exits non-zero in that case: that is the answer to the question, not a broken CLI, so read the JSON rather than retrying or working around it. In that case stop before writing and ask the user how to proceed: set this project up (\`openspec init\`), target a store they already have (\`--store \`), or drop OpenSpec for this request and help them directly. Wait for their answer. Do not run \`openspec init\` until they ask for it, do not hand-create \`openspec/\` files, and do not let a command create the root as a side effect.`; +export const PROJECT_ROOT_GUARD = `**Project check:** These steps expect a project that already uses OpenSpec. Before the first step that writes anything (\`new change\`, \`archive\`, \`sync specs\`, or authoring an artifact file), confirm the project has a root: run \`openspec list --json\` and read \`root\`. A root object means the project is set up. \`"root": null\` means it is not - there is no \`openspec/\` directory here, and a write such as \`openspec new change\` would create one as a side effect. The command also exits non-zero, which is that answer rather than a broken CLI, so read the JSON instead of retrying or working around it. Then stop before writing and ask the user how to proceed: set this project up (\`openspec init\`), target a store they already have (\`--store \`), or drop OpenSpec for this request and help them directly. Wait for their answer. Do not run \`openspec init\` until they ask for it, do not hand-create \`openspec/\` files, and do not let a command create the root as a side effect.`; diff --git a/test/core/templates/project-root-guard.test.ts b/test/core/templates/project-root-guard.test.ts index da28110d73..54a56b5fd9 100644 --- a/test/core/templates/project-root-guard.test.ts +++ b/test/core/templates/project-root-guard.test.ts @@ -62,7 +62,7 @@ describe('project root guard', () => { expect(PROJECT_ROOT_GUARD).toContain('`"root": null`'); // An agent that reads the non-zero exit as a broken CLI is one step from // hand-creating `openspec/` instead, which is the failure being guarded. - expect(PROJECT_ROOT_GUARD).toContain('exits non-zero in that case'); + expect(PROJECT_ROOT_GUARD).toContain('also exits non-zero, which is that answer rather than a broken CLI'); }); it('hands the decision to the user instead of setting the project up', () => { diff --git a/test/core/templates/skill-templates-parity.test.ts b/test/core/templates/skill-templates-parity.test.ts index 33447934dd..60351abcb2 100644 --- a/test/core/templates/skill-templates-parity.test.ts +++ b/test/core/templates/skill-templates-parity.test.ts @@ -38,46 +38,46 @@ import { import { STORE_SELECTION_GUIDANCE } from '../../../src/core/templates/workflows/store-selection.js'; const EXPECTED_FUNCTION_HASHES: Record = { - getExploreSkillTemplate: '16999a190ca45039accc5f35962c9af2730915871b595d8315b16a5de5bb2708', - getNewChangeSkillTemplate: 'f92768e5cf19a9ae09ce3baf35ef5c7d8a116d1690e8a91f89d50dbc79971798', - getContinueChangeSkillTemplate: 'd554a621582785beb18ee47cbb7f7cd401b24d88fd880147540a7deec8f98088', - getApplyChangeSkillTemplate: '9949cc0fbbf9f70e3b3598936700563617c2bc1cbc00417e9861102c81f21a11', - getFfChangeSkillTemplate: 'ee47c4c7c198c08c072b7ce83474d0fa5dcb261ad520a4b03ad39e0c21e397bd', - getSyncSpecsSkillTemplate: '1f0dc5a1b899ee58dea9c0ba1d2e1c8f36696ce0ce4cc9feb4695fca5f5611e5', - getOnboardSkillTemplate: '83fca17d6d8b67b8fb8d55f42cbd94052208e588b915f9534821c2ed5cfa81e8', - getOpsxExploreCommandTemplate: '3a80e9180834eb39b8870bf0040f847a161ea89e2904fef20f361aeef271305c', - getOpsxNewCommandTemplate: 'd8124a085bb36840a3ced2dd1a0dd109c281feecd5892524a426d1769d8ede92', - getOpsxContinueCommandTemplate: '3c5b8c50ff15e4e5dd526591531e25458df079069e4d2e60d9ba5de3c159d097', - getOpsxApplyCommandTemplate: '583a72caf2f31c830984110ad40b06fb8e77137851dece0c1c146b6b93867496', - getOpsxFfCommandTemplate: '34f6f4d28734e3571f41c8790e0a5dc18cf50c6ca63a4d3605c5f1909316e2d6', - getArchiveChangeSkillTemplate: '47d8817f7e1b4433e639bccfadac667dcac07b79bdcf6f3a34987b6ab20cb601', - getBulkArchiveChangeSkillTemplate: 'caae7ade139efead2cb501fd20ef1772d8937664add541d9d91b3867965722d8', - getOpsxSyncCommandTemplate: '9b77ae927f0decfbbf0a3ddf02bd50c3e4642190402318447ab449414fd219d6', - getVerifyChangeSkillTemplate: '7fa735e9d9aca1ea7d0fea9be74406f9cd60fca78191fb72bbd2e26c1c3893e6', - getOpsxArchiveCommandTemplate: 'de3e85e14c2e382c44a652c62883423bcfc99c728cee574f2f5c35f0d6cb9241', - getOpsxOnboardCommandTemplate: 'fcb626fa8a4d4017821b3720413279fb8f9973433b08bb2ebe5582c2e3b8c428', - getOpsxBulkArchiveCommandTemplate: '11a9260721583c188ee886a3e98aa6009f2bb684bc78e851a33056854542af57', - getOpsxVerifyCommandTemplate: 'b3df65f24c51fb606c92e88d304d0603b6a2ed78bce3b6893a10815ceaa6dc9d', - getOpsxProposeSkillTemplate: '58701e927038d200afbff4bad556b8bc1955a99911f7638cf9d147e2d15012ce', - getOpsxProposeCommandTemplate: '8b928db81b048b91a18c89231e698f2e41d6d7877349de8297710b13bae17b21', + getExploreSkillTemplate: '7919571d9b846b6f51fe0ee1544b649f1650071e5a8eb3d5795eeda9ee185dc2', + getNewChangeSkillTemplate: 'b6d9c999c664a4ca95b0b9ddf5be45a7595bce6644bd95e5e23a93db5353524d', + getContinueChangeSkillTemplate: '690b75890111d9b4f3cb44327cf7a86e8cd46d1b7812136e4b58f416b3b25b38', + getApplyChangeSkillTemplate: 'fd7467b24f4bb7051044237ed6d82357773ee77df659e64f28c331677ff28de6', + getFfChangeSkillTemplate: '1dc289a3de6999810f2d936b82706ff23402d33b79087636b4afc984f5006c41', + getSyncSpecsSkillTemplate: '23420a9bc829e4c2334a766b8f3f0498eb48293664e8c8e84685ed15865e0cdf', + getOnboardSkillTemplate: '0f15e46aae290322cb979033bf7575f7a42e9c009e148cbcfc3b487b6e2a8fcd', + getOpsxExploreCommandTemplate: '6167af7300894b85ef58e17178f941adadf5629865c21a6a79a32c78d1a90f6b', + getOpsxNewCommandTemplate: '700d80a18653bd1d37ba9017ca870049561bfff521241ef5c489a5174b7b451a', + getOpsxContinueCommandTemplate: 'bff75b037596be5bddd9f9499380427bd82c9092f1f613148af33a6bf8b0178b', + getOpsxApplyCommandTemplate: 'fffda846cf919e34a0a09abd4b177a385dc922d1073a6b36e6064135b39486db', + getOpsxFfCommandTemplate: 'c67f5b41a60bbbecaa9b9ad978e53d85c919225bbca89884ad8c49efed490680', + getArchiveChangeSkillTemplate: 'baaf840bd1eddce4fea1f8d9d0bd7710c548500201994481d42c748d0265f3f2', + getBulkArchiveChangeSkillTemplate: 'c23025010cdf629a3f45d6f351b803bedb048612420297f389752b420ead8949', + getOpsxSyncCommandTemplate: '82f5892c1a8c94bee2bcd25da8a1c8f883f5c97056ac436508dfc549b12ce052', + getVerifyChangeSkillTemplate: 'ba7eb2736abf1ea1b93a1bf949afb46b71c92414cf137497d70f650ecd71f74d', + getOpsxArchiveCommandTemplate: 'fc2b4ca635437f997b3ad1e78aa0d3a64eee78a5541199dd47d30ca5a03ca743', + getOpsxOnboardCommandTemplate: 'f387de3e2121c22734544382f8353a342bb306cae24dd292b626c9b6337dd30b', + getOpsxBulkArchiveCommandTemplate: '105b829eddd7acdcf414ebe0c77be65fe0e9b75d5ea72a73621e36675368c752', + getOpsxVerifyCommandTemplate: '5ca651adc9d78e43ff3b6ba7184ca64364b8a9e2b99488aba3ae6c7b36605f2f', + getOpsxProposeSkillTemplate: '1ff05e257ac308f690095dd276eb751b7e7825d981596b72cad44e8a8c2abf66', + getOpsxProposeCommandTemplate: 'bb16658833068ac1fde9b1e1aeadbd09347a95738e5e08736bbfdfdbb0314fed', getFeedbackSkillTemplate: 'dabeb5e825b9349abc8156c3e7b8608f27987912a6d9bf47ef29addde6138133', - getUpdateChangeSkillTemplate: '2e93cbbc3599a39601017b8df98ac1b56037fa7c75f3afb6e528dd1a1cecd1dc', - getOpsxUpdateCommandTemplate: '9859b917b4e91720c7c764aa852f6acb59971b2a223537f894eaa4a413d49a0a', + getUpdateChangeSkillTemplate: 'b09beffde628370223ad6e36524c76d023ac9a1720939bffdb318334e2d757eb', + getOpsxUpdateCommandTemplate: 'd2561642635a36af2544dfb98c03c3838cb5e8c7b574a9f95327aebb2a1eb4ae', }; const EXPECTED_GENERATED_SKILL_CONTENT_HASHES: Record = { - 'openspec-explore': 'a6c0b69f6b46df5fac8631f81cc01277740807e54bb4d6fa2483ca89fa4d700d', - 'openspec-new-change': 'caaae53e3ad10bfaf8b59c11344918d4691ee9a112a7d6422ed7f74ca58fe906', - 'openspec-continue-change': 'ea9138711d13027646a20f9be6d96a39790be951f4283dd1d58a36f7633dc209', - 'openspec-apply-change': '229735de3079455172862e4db4efcba37c3894ceaf8c23604444ba98d43ffecd', - 'openspec-ff-change': '74ea6ccf191f0cd9007ae61913471ea0f9945201fab7fbfca20f2ba22c8e4704', - 'openspec-sync-specs': '1c54e0d16e205da7ed9b08f36b49ff3236c2c362cfc7db82ccf5ae87143da0eb', - 'openspec-archive-change': 'b8a7948e7f1a8b821e995b69bb942cba6d82ff2d3d4f7a60b08100e4bb5eff21', - 'openspec-bulk-archive-change': 'd24c38080a5590db9959e484ad1d3a5516af3208eecd33ea07a0e339e832e2fd', - 'openspec-verify-change': '838727cf999b11466942fc60420d34b1ebfa18b808e5c7c2571a0e32187104f3', - 'openspec-onboard': '178a1053ae03a5b8d954b2df4c3a5a93ded539c963cb89e4846fd0197c003825', - 'openspec-propose': 'ec1d61d20a46c5015145bfe0a1318feb2adf5376ec38130c875b1819443ca975', - 'openspec-update-change': '764dac81fd09a18b5d903c9f1f5712f4de068f30cac77054d533f2917222d598', + 'openspec-explore': '3976176121cefd78024b3eac88901d505f6494c5e871461a32d17b64098c4ec7', + 'openspec-new-change': 'a1cfdc950f9016cacb2fe0c29d4dbbb331dc2b9e64682844f47b2a9b2fe27eaa', + 'openspec-continue-change': 'a5cffd1d9a8f62623f66110ff959bfa7c419209114fcc2fcc4ab9a208714595c', + 'openspec-apply-change': '58ca8f83d9ff08a1c0b61aad42921bcb2e707899cc30fda0eeea430503dd26da', + 'openspec-ff-change': '8d7806a063f04fe4cb6b115ac198bb190c20979af30239f9082f287802f5c016', + 'openspec-sync-specs': 'cd22186ac839d09ab370cc16457f9f06905fb4eeca881d25af19bcc6b1d1c5a5', + 'openspec-archive-change': '7c3fc3eeb5a18d50927368dc51ac7ef27d341b9253ed3e7fcf83031c42b1884a', + 'openspec-bulk-archive-change': '2979576072c4ab851c9e4bd00e6d02d1005feb9a2afa83793381d8f10bd172a4', + 'openspec-verify-change': 'da28c468d7ffb1245cc4b6222e87733109187bc78336a99163e379256af45f5c', + 'openspec-onboard': '65e44559221053472b696ee41c0ee24003557cb518b392f84a0c4c64bd3db16e', + 'openspec-propose': '50892baa28106968042605dd63ec12c5f8801b617020c58508c16fa895a74ef7', + 'openspec-update-change': '586bf934b24d671e545e7fd44f5eec0858decb5403eb5884ffd95bc15d2db4b0', }; // Intentionally excludes getFeedbackSkillTemplate: this list only models templates From 47917d339edff6c136d10a29b4da9bfa3954ed53 Mon Sep 17 00:00:00 2001 From: Clay Good Date: Fri, 4 Sep 2026 13:06:45 -0500 Subject: [PATCH 10/15] 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 `. 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 --- skills/openspec-apply-change/SKILL.md | 2 +- skills/openspec-archive-change/SKILL.md | 2 +- skills/openspec-bulk-archive-change/SKILL.md | 2 +- skills/openspec-continue-change/SKILL.md | 2 +- skills/openspec-explore/SKILL.md | 2 +- skills/openspec-ff-change/SKILL.md | 2 +- skills/openspec-new-change/SKILL.md | 2 +- skills/openspec-onboard/SKILL.md | 2 +- skills/openspec-propose/SKILL.md | 2 +- skills/openspec-sync-specs/SKILL.md | 2 +- skills/openspec-update-change/SKILL.md | 2 +- skills/openspec-verify-change/SKILL.md | 2 +- src/core/templates/workflows/project-root.ts | 2 +- test/commands/store-root-selection.test.ts | 28 +++++++- .../core/templates/project-root-guard.test.ts | 3 + .../templates/skill-templates-parity.test.ts | 72 +++++++++---------- 16 files changed, 78 insertions(+), 51 deletions(-) diff --git a/skills/openspec-apply-change/SKILL.md b/skills/openspec-apply-change/SKILL.md index 4165807edc..b822d56f45 100644 --- a/skills/openspec-apply-change/SKILL.md +++ b/skills/openspec-apply-change/SKILL.md @@ -13,7 +13,7 @@ Implement tasks from an OpenSpec change. **Store selection:** If the user names a store (a store is a standalone OpenSpec repo registered on this machine) or the work lives in one, run `openspec store list --json` to discover registered store ids, then pass `--store ` on the commands that read or write specs and changes (`new change`, `status`, `instructions`, `list`, `show`, `validate`, `archive`, `doctor`, `context`, `schemas`, `view`). Once selected, treat `--store ` as sticky for the rest of the workflow. Every unscoped example of those commands below is shorthand: before running it, append the flag. For example, run `openspec status --change "" --json --store ""`, not the unscoped form shown below. Other commands do not take the flag. Hints printed by commands already carry the flag; keep it on follow-ups. Without a store, commands act on the nearest local `openspec/` root. -**Project check:** These steps expect a project that already uses OpenSpec. Before the first step that writes anything (`new change`, `archive`, `sync specs`, or authoring an artifact file), confirm the project has a root: run `openspec list --json` and read `root`. A root object means the project is set up. `"root": null` means it is not - there is no `openspec/` directory here, and a write such as `openspec new change` would create one as a side effect. The command also exits non-zero, which is that answer rather than a broken CLI, so read the JSON instead of retrying or working around it. Then stop before writing and ask the user how to proceed: set this project up (`openspec init`), target a store they already have (`--store `), or drop OpenSpec for this request and help them directly. Wait for their answer. Do not run `openspec init` until they ask for it, do not hand-create `openspec/` files, and do not let a command create the root as a side effect. +**Project check:** These steps expect a project that already uses OpenSpec. Before the first step that writes anything (`new change`, `archive`, `sync specs`, or authoring an artifact file), confirm the project has a root: run `openspec list --json` (with `--store ` when a store is selected, since the store is then the root) and read `root`. A root object means the project is set up. `"root": null` means it is not - there is no `openspec/` directory here, and a write such as `openspec new change` would create one as a side effect. The command also exits non-zero, which is that answer rather than a broken CLI, so read the JSON instead of retrying or working around it. Then stop before writing and ask the user how to proceed: set this project up (`openspec init`), target a store they already have (`--store `), or drop OpenSpec for this request and help them directly. Wait for their answer. Do not run `openspec init` until they ask for it, do not hand-create `openspec/` files, and do not let a command create the root as a side effect. **Input**: Optionally specify a change name (e.g., `/openspec-apply-change add-auth`). If omitted, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes. diff --git a/skills/openspec-archive-change/SKILL.md b/skills/openspec-archive-change/SKILL.md index 6ba0cdc96e..3e717138ba 100644 --- a/skills/openspec-archive-change/SKILL.md +++ b/skills/openspec-archive-change/SKILL.md @@ -13,7 +13,7 @@ Archive a completed change in the experimental workflow. **Store selection:** If the user names a store (a store is a standalone OpenSpec repo registered on this machine) or the work lives in one, run `openspec store list --json` to discover registered store ids, then pass `--store ` on the commands that read or write specs and changes (`new change`, `status`, `instructions`, `list`, `show`, `validate`, `archive`, `doctor`, `context`, `schemas`, `view`). Once selected, treat `--store ` as sticky for the rest of the workflow. Every unscoped example of those commands below is shorthand: before running it, append the flag. For example, run `openspec status --change "" --json --store ""`, not the unscoped form shown below. Other commands do not take the flag. Hints printed by commands already carry the flag; keep it on follow-ups. Without a store, commands act on the nearest local `openspec/` root. -**Project check:** These steps expect a project that already uses OpenSpec. Before the first step that writes anything (`new change`, `archive`, `sync specs`, or authoring an artifact file), confirm the project has a root: run `openspec list --json` and read `root`. A root object means the project is set up. `"root": null` means it is not - there is no `openspec/` directory here, and a write such as `openspec new change` would create one as a side effect. The command also exits non-zero, which is that answer rather than a broken CLI, so read the JSON instead of retrying or working around it. Then stop before writing and ask the user how to proceed: set this project up (`openspec init`), target a store they already have (`--store `), or drop OpenSpec for this request and help them directly. Wait for their answer. Do not run `openspec init` until they ask for it, do not hand-create `openspec/` files, and do not let a command create the root as a side effect. +**Project check:** These steps expect a project that already uses OpenSpec. Before the first step that writes anything (`new change`, `archive`, `sync specs`, or authoring an artifact file), confirm the project has a root: run `openspec list --json` (with `--store ` when a store is selected, since the store is then the root) and read `root`. A root object means the project is set up. `"root": null` means it is not - there is no `openspec/` directory here, and a write such as `openspec new change` would create one as a side effect. The command also exits non-zero, which is that answer rather than a broken CLI, so read the JSON instead of retrying or working around it. Then stop before writing and ask the user how to proceed: set this project up (`openspec init`), target a store they already have (`--store `), or drop OpenSpec for this request and help them directly. Wait for their answer. Do not run `openspec init` until they ask for it, do not hand-create `openspec/` files, and do not let a command create the root as a side effect. `` is the spec directory relative to `specs/` (for example, `user-auth` or `identity/user-auth`). Preserve the full path from each delta spec when resolving its main spec. diff --git a/skills/openspec-bulk-archive-change/SKILL.md b/skills/openspec-bulk-archive-change/SKILL.md index 419d77bc19..a20aaeeee1 100644 --- a/skills/openspec-bulk-archive-change/SKILL.md +++ b/skills/openspec-bulk-archive-change/SKILL.md @@ -15,7 +15,7 @@ This skill allows you to batch-archive changes, handling spec conflicts intellig **Store selection:** If the user names a store (a store is a standalone OpenSpec repo registered on this machine) or the work lives in one, run `openspec store list --json` to discover registered store ids, then pass `--store ` on the commands that read or write specs and changes (`new change`, `status`, `instructions`, `list`, `show`, `validate`, `archive`, `doctor`, `context`, `schemas`, `view`). Once selected, treat `--store ` as sticky for the rest of the workflow. Every unscoped example of those commands below is shorthand: before running it, append the flag. For example, run `openspec status --change "" --json --store ""`, not the unscoped form shown below. Other commands do not take the flag. Hints printed by commands already carry the flag; keep it on follow-ups. Without a store, commands act on the nearest local `openspec/` root. -**Project check:** These steps expect a project that already uses OpenSpec. Before the first step that writes anything (`new change`, `archive`, `sync specs`, or authoring an artifact file), confirm the project has a root: run `openspec list --json` and read `root`. A root object means the project is set up. `"root": null` means it is not - there is no `openspec/` directory here, and a write such as `openspec new change` would create one as a side effect. The command also exits non-zero, which is that answer rather than a broken CLI, so read the JSON instead of retrying or working around it. Then stop before writing and ask the user how to proceed: set this project up (`openspec init`), target a store they already have (`--store `), or drop OpenSpec for this request and help them directly. Wait for their answer. Do not run `openspec init` until they ask for it, do not hand-create `openspec/` files, and do not let a command create the root as a side effect. +**Project check:** These steps expect a project that already uses OpenSpec. Before the first step that writes anything (`new change`, `archive`, `sync specs`, or authoring an artifact file), confirm the project has a root: run `openspec list --json` (with `--store ` when a store is selected, since the store is then the root) and read `root`. A root object means the project is set up. `"root": null` means it is not - there is no `openspec/` directory here, and a write such as `openspec new change` would create one as a side effect. The command also exits non-zero, which is that answer rather than a broken CLI, so read the JSON instead of retrying or working around it. Then stop before writing and ask the user how to proceed: set this project up (`openspec init`), target a store they already have (`--store `), or drop OpenSpec for this request and help them directly. Wait for their answer. Do not run `openspec init` until they ask for it, do not hand-create `openspec/` files, and do not let a command create the root as a side effect. `` is the spec directory relative to `specs/` (for example, `user-auth` or `identity/user-auth`). Preserve the full path from each delta spec when resolving its main spec. diff --git a/skills/openspec-continue-change/SKILL.md b/skills/openspec-continue-change/SKILL.md index ef446f29fb..9139a25673 100644 --- a/skills/openspec-continue-change/SKILL.md +++ b/skills/openspec-continue-change/SKILL.md @@ -13,7 +13,7 @@ Continue working on a change by creating the next artifact. **Store selection:** If the user names a store (a store is a standalone OpenSpec repo registered on this machine) or the work lives in one, run `openspec store list --json` to discover registered store ids, then pass `--store ` on the commands that read or write specs and changes (`new change`, `status`, `instructions`, `list`, `show`, `validate`, `archive`, `doctor`, `context`, `schemas`, `view`). Once selected, treat `--store ` as sticky for the rest of the workflow. Every unscoped example of those commands below is shorthand: before running it, append the flag. For example, run `openspec status --change "" --json --store ""`, not the unscoped form shown below. Other commands do not take the flag. Hints printed by commands already carry the flag; keep it on follow-ups. Without a store, commands act on the nearest local `openspec/` root. -**Project check:** These steps expect a project that already uses OpenSpec. Before the first step that writes anything (`new change`, `archive`, `sync specs`, or authoring an artifact file), confirm the project has a root: run `openspec list --json` and read `root`. A root object means the project is set up. `"root": null` means it is not - there is no `openspec/` directory here, and a write such as `openspec new change` would create one as a side effect. The command also exits non-zero, which is that answer rather than a broken CLI, so read the JSON instead of retrying or working around it. Then stop before writing and ask the user how to proceed: set this project up (`openspec init`), target a store they already have (`--store `), or drop OpenSpec for this request and help them directly. Wait for their answer. Do not run `openspec init` until they ask for it, do not hand-create `openspec/` files, and do not let a command create the root as a side effect. +**Project check:** These steps expect a project that already uses OpenSpec. Before the first step that writes anything (`new change`, `archive`, `sync specs`, or authoring an artifact file), confirm the project has a root: run `openspec list --json` (with `--store ` when a store is selected, since the store is then the root) and read `root`. A root object means the project is set up. `"root": null` means it is not - there is no `openspec/` directory here, and a write such as `openspec new change` would create one as a side effect. The command also exits non-zero, which is that answer rather than a broken CLI, so read the JSON instead of retrying or working around it. Then stop before writing and ask the user how to proceed: set this project up (`openspec init`), target a store they already have (`--store `), or drop OpenSpec for this request and help them directly. Wait for their answer. Do not run `openspec init` until they ask for it, do not hand-create `openspec/` files, and do not let a command create the root as a side effect. **Input**: Optionally specify a change name. If omitted, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes. diff --git a/skills/openspec-explore/SKILL.md b/skills/openspec-explore/SKILL.md index df66c6319e..db3f0cf0d1 100644 --- a/skills/openspec-explore/SKILL.md +++ b/skills/openspec-explore/SKILL.md @@ -17,7 +17,7 @@ Enter explore mode. Think deeply. Visualize freely. Follow the conversation wher **Store selection:** If the user names a store (a store is a standalone OpenSpec repo registered on this machine) or the work lives in one, run `openspec store list --json` to discover registered store ids, then pass `--store ` on the commands that read or write specs and changes (`new change`, `status`, `instructions`, `list`, `show`, `validate`, `archive`, `doctor`, `context`, `schemas`, `view`). Once selected, treat `--store ` as sticky for the rest of the workflow. Every unscoped example of those commands below is shorthand: before running it, append the flag. For example, run `openspec status --change "" --json --store ""`, not the unscoped form shown below. Other commands do not take the flag. Hints printed by commands already carry the flag; keep it on follow-ups. Without a store, commands act on the nearest local `openspec/` root. -**Project check:** These steps expect a project that already uses OpenSpec. Before the first step that writes anything (`new change`, `archive`, `sync specs`, or authoring an artifact file), confirm the project has a root: run `openspec list --json` and read `root`. A root object means the project is set up. `"root": null` means it is not - there is no `openspec/` directory here, and a write such as `openspec new change` would create one as a side effect. The command also exits non-zero, which is that answer rather than a broken CLI, so read the JSON instead of retrying or working around it. Then stop before writing and ask the user how to proceed: set this project up (`openspec init`), target a store they already have (`--store `), or drop OpenSpec for this request and help them directly. Wait for their answer. Do not run `openspec init` until they ask for it, do not hand-create `openspec/` files, and do not let a command create the root as a side effect. +**Project check:** These steps expect a project that already uses OpenSpec. Before the first step that writes anything (`new change`, `archive`, `sync specs`, or authoring an artifact file), confirm the project has a root: run `openspec list --json` (with `--store ` when a store is selected, since the store is then the root) and read `root`. A root object means the project is set up. `"root": null` means it is not - there is no `openspec/` directory here, and a write such as `openspec new change` would create one as a side effect. The command also exits non-zero, which is that answer rather than a broken CLI, so read the JSON instead of retrying or working around it. Then stop before writing and ask the user how to proceed: set this project up (`openspec init`), target a store they already have (`--store `), or drop OpenSpec for this request and help them directly. Wait for their answer. Do not run `openspec init` until they ask for it, do not hand-create `openspec/` files, and do not let a command create the root as a side effect. --- diff --git a/skills/openspec-ff-change/SKILL.md b/skills/openspec-ff-change/SKILL.md index e623cab936..dd5353c1a1 100644 --- a/skills/openspec-ff-change/SKILL.md +++ b/skills/openspec-ff-change/SKILL.md @@ -13,7 +13,7 @@ Fast-forward through artifact creation - generate everything needed to start imp **Store selection:** If the user names a store (a store is a standalone OpenSpec repo registered on this machine) or the work lives in one, run `openspec store list --json` to discover registered store ids, then pass `--store ` on the commands that read or write specs and changes (`new change`, `status`, `instructions`, `list`, `show`, `validate`, `archive`, `doctor`, `context`, `schemas`, `view`). Once selected, treat `--store ` as sticky for the rest of the workflow. Every unscoped example of those commands below is shorthand: before running it, append the flag. For example, run `openspec status --change "" --json --store ""`, not the unscoped form shown below. Other commands do not take the flag. Hints printed by commands already carry the flag; keep it on follow-ups. Without a store, commands act on the nearest local `openspec/` root. -**Project check:** These steps expect a project that already uses OpenSpec. Before the first step that writes anything (`new change`, `archive`, `sync specs`, or authoring an artifact file), confirm the project has a root: run `openspec list --json` and read `root`. A root object means the project is set up. `"root": null` means it is not - there is no `openspec/` directory here, and a write such as `openspec new change` would create one as a side effect. The command also exits non-zero, which is that answer rather than a broken CLI, so read the JSON instead of retrying or working around it. Then stop before writing and ask the user how to proceed: set this project up (`openspec init`), target a store they already have (`--store `), or drop OpenSpec for this request and help them directly. Wait for their answer. Do not run `openspec init` until they ask for it, do not hand-create `openspec/` files, and do not let a command create the root as a side effect. +**Project check:** These steps expect a project that already uses OpenSpec. Before the first step that writes anything (`new change`, `archive`, `sync specs`, or authoring an artifact file), confirm the project has a root: run `openspec list --json` (with `--store ` when a store is selected, since the store is then the root) and read `root`. A root object means the project is set up. `"root": null` means it is not - there is no `openspec/` directory here, and a write such as `openspec new change` would create one as a side effect. The command also exits non-zero, which is that answer rather than a broken CLI, so read the JSON instead of retrying or working around it. Then stop before writing and ask the user how to proceed: set this project up (`openspec init`), target a store they already have (`--store `), or drop OpenSpec for this request and help them directly. Wait for their answer. Do not run `openspec init` until they ask for it, do not hand-create `openspec/` files, and do not let a command create the root as a side effect. **Input**: The user's request should include a change name (kebab-case) OR a description of what they want to build. diff --git a/skills/openspec-new-change/SKILL.md b/skills/openspec-new-change/SKILL.md index 1c4f0a5a72..91b1a85819 100644 --- a/skills/openspec-new-change/SKILL.md +++ b/skills/openspec-new-change/SKILL.md @@ -13,7 +13,7 @@ Start a new change using the experimental artifact-driven approach. **Store selection:** If the user names a store (a store is a standalone OpenSpec repo registered on this machine) or the work lives in one, run `openspec store list --json` to discover registered store ids, then pass `--store ` on the commands that read or write specs and changes (`new change`, `status`, `instructions`, `list`, `show`, `validate`, `archive`, `doctor`, `context`, `schemas`, `view`). Once selected, treat `--store ` as sticky for the rest of the workflow. Every unscoped example of those commands below is shorthand: before running it, append the flag. For example, run `openspec status --change "" --json --store ""`, not the unscoped form shown below. Other commands do not take the flag. Hints printed by commands already carry the flag; keep it on follow-ups. Without a store, commands act on the nearest local `openspec/` root. -**Project check:** These steps expect a project that already uses OpenSpec. Before the first step that writes anything (`new change`, `archive`, `sync specs`, or authoring an artifact file), confirm the project has a root: run `openspec list --json` and read `root`. A root object means the project is set up. `"root": null` means it is not - there is no `openspec/` directory here, and a write such as `openspec new change` would create one as a side effect. The command also exits non-zero, which is that answer rather than a broken CLI, so read the JSON instead of retrying or working around it. Then stop before writing and ask the user how to proceed: set this project up (`openspec init`), target a store they already have (`--store `), or drop OpenSpec for this request and help them directly. Wait for their answer. Do not run `openspec init` until they ask for it, do not hand-create `openspec/` files, and do not let a command create the root as a side effect. +**Project check:** These steps expect a project that already uses OpenSpec. Before the first step that writes anything (`new change`, `archive`, `sync specs`, or authoring an artifact file), confirm the project has a root: run `openspec list --json` (with `--store ` when a store is selected, since the store is then the root) and read `root`. A root object means the project is set up. `"root": null` means it is not - there is no `openspec/` directory here, and a write such as `openspec new change` would create one as a side effect. The command also exits non-zero, which is that answer rather than a broken CLI, so read the JSON instead of retrying or working around it. Then stop before writing and ask the user how to proceed: set this project up (`openspec init`), target a store they already have (`--store `), or drop OpenSpec for this request and help them directly. Wait for their answer. Do not run `openspec init` until they ask for it, do not hand-create `openspec/` files, and do not let a command create the root as a side effect. **Input**: The user's request should include a change name (kebab-case) OR a description of what they want to build. diff --git a/skills/openspec-onboard/SKILL.md b/skills/openspec-onboard/SKILL.md index 0323181bde..4803b91ef1 100644 --- a/skills/openspec-onboard/SKILL.md +++ b/skills/openspec-onboard/SKILL.md @@ -13,7 +13,7 @@ Guide the user through their first complete OpenSpec workflow cycle. This is a t **Store selection:** If the user names a store (a store is a standalone OpenSpec repo registered on this machine) or the work lives in one, run `openspec store list --json` to discover registered store ids, then pass `--store ` on the commands that read or write specs and changes (`new change`, `status`, `instructions`, `list`, `show`, `validate`, `archive`, `doctor`, `context`, `schemas`, `view`). Once selected, treat `--store ` as sticky for the rest of the workflow. Every unscoped example of those commands below is shorthand: before running it, append the flag. For example, run `openspec status --change "" --json --store ""`, not the unscoped form shown below. Other commands do not take the flag. Hints printed by commands already carry the flag; keep it on follow-ups. Without a store, commands act on the nearest local `openspec/` root. -**Project check:** These steps expect a project that already uses OpenSpec. Before the first step that writes anything (`new change`, `archive`, `sync specs`, or authoring an artifact file), confirm the project has a root: run `openspec list --json` and read `root`. A root object means the project is set up. `"root": null` means it is not - there is no `openspec/` directory here, and a write such as `openspec new change` would create one as a side effect. The command also exits non-zero, which is that answer rather than a broken CLI, so read the JSON instead of retrying or working around it. Then stop before writing and ask the user how to proceed: set this project up (`openspec init`), target a store they already have (`--store `), or drop OpenSpec for this request and help them directly. Wait for their answer. Do not run `openspec init` until they ask for it, do not hand-create `openspec/` files, and do not let a command create the root as a side effect. +**Project check:** These steps expect a project that already uses OpenSpec. Before the first step that writes anything (`new change`, `archive`, `sync specs`, or authoring an artifact file), confirm the project has a root: run `openspec list --json` (with `--store ` when a store is selected, since the store is then the root) and read `root`. A root object means the project is set up. `"root": null` means it is not - there is no `openspec/` directory here, and a write such as `openspec new change` would create one as a side effect. The command also exits non-zero, which is that answer rather than a broken CLI, so read the JSON instead of retrying or working around it. Then stop before writing and ask the user how to proceed: set this project up (`openspec init`), target a store they already have (`--store `), or drop OpenSpec for this request and help them directly. Wait for their answer. Do not run `openspec init` until they ask for it, do not hand-create `openspec/` files, and do not let a command create the root as a side effect. --- diff --git a/skills/openspec-propose/SKILL.md b/skills/openspec-propose/SKILL.md index fd45b7ed61..da016c1e80 100644 --- a/skills/openspec-propose/SKILL.md +++ b/skills/openspec-propose/SKILL.md @@ -27,7 +27,7 @@ When the user is ready to implement, they must start the apply workflow explicit **Store selection:** If the user names a store (a store is a standalone OpenSpec repo registered on this machine) or the work lives in one, run `openspec store list --json` to discover registered store ids, then pass `--store ` on the commands that read or write specs and changes (`new change`, `status`, `instructions`, `list`, `show`, `validate`, `archive`, `doctor`, `context`, `schemas`, `view`). Once selected, treat `--store ` as sticky for the rest of the workflow. Every unscoped example of those commands below is shorthand: before running it, append the flag. For example, run `openspec status --change "" --json --store ""`, not the unscoped form shown below. Other commands do not take the flag. Hints printed by commands already carry the flag; keep it on follow-ups. Without a store, commands act on the nearest local `openspec/` root. -**Project check:** These steps expect a project that already uses OpenSpec. Before the first step that writes anything (`new change`, `archive`, `sync specs`, or authoring an artifact file), confirm the project has a root: run `openspec list --json` and read `root`. A root object means the project is set up. `"root": null` means it is not - there is no `openspec/` directory here, and a write such as `openspec new change` would create one as a side effect. The command also exits non-zero, which is that answer rather than a broken CLI, so read the JSON instead of retrying or working around it. Then stop before writing and ask the user how to proceed: set this project up (`openspec init`), target a store they already have (`--store `), or drop OpenSpec for this request and help them directly. Wait for their answer. Do not run `openspec init` until they ask for it, do not hand-create `openspec/` files, and do not let a command create the root as a side effect. +**Project check:** These steps expect a project that already uses OpenSpec. Before the first step that writes anything (`new change`, `archive`, `sync specs`, or authoring an artifact file), confirm the project has a root: run `openspec list --json` (with `--store ` when a store is selected, since the store is then the root) and read `root`. A root object means the project is set up. `"root": null` means it is not - there is no `openspec/` directory here, and a write such as `openspec new change` would create one as a side effect. The command also exits non-zero, which is that answer rather than a broken CLI, so read the JSON instead of retrying or working around it. Then stop before writing and ask the user how to proceed: set this project up (`openspec init`), target a store they already have (`--store `), or drop OpenSpec for this request and help them directly. Wait for their answer. Do not run `openspec init` until they ask for it, do not hand-create `openspec/` files, and do not let a command create the root as a side effect. **Input**: The user's request should include a change name (kebab-case) OR a description of what they want to build. diff --git a/skills/openspec-sync-specs/SKILL.md b/skills/openspec-sync-specs/SKILL.md index 9c03b62872..404bd2cda9 100644 --- a/skills/openspec-sync-specs/SKILL.md +++ b/skills/openspec-sync-specs/SKILL.md @@ -15,7 +15,7 @@ This is an **agent-driven** operation - you will read delta specs and directly e **Store selection:** If the user names a store (a store is a standalone OpenSpec repo registered on this machine) or the work lives in one, run `openspec store list --json` to discover registered store ids, then pass `--store ` on the commands that read or write specs and changes (`new change`, `status`, `instructions`, `list`, `show`, `validate`, `archive`, `doctor`, `context`, `schemas`, `view`). Once selected, treat `--store ` as sticky for the rest of the workflow. Every unscoped example of those commands below is shorthand: before running it, append the flag. For example, run `openspec status --change "" --json --store ""`, not the unscoped form shown below. Other commands do not take the flag. Hints printed by commands already carry the flag; keep it on follow-ups. Without a store, commands act on the nearest local `openspec/` root. -**Project check:** These steps expect a project that already uses OpenSpec. Before the first step that writes anything (`new change`, `archive`, `sync specs`, or authoring an artifact file), confirm the project has a root: run `openspec list --json` and read `root`. A root object means the project is set up. `"root": null` means it is not - there is no `openspec/` directory here, and a write such as `openspec new change` would create one as a side effect. The command also exits non-zero, which is that answer rather than a broken CLI, so read the JSON instead of retrying or working around it. Then stop before writing and ask the user how to proceed: set this project up (`openspec init`), target a store they already have (`--store `), or drop OpenSpec for this request and help them directly. Wait for their answer. Do not run `openspec init` until they ask for it, do not hand-create `openspec/` files, and do not let a command create the root as a side effect. +**Project check:** These steps expect a project that already uses OpenSpec. Before the first step that writes anything (`new change`, `archive`, `sync specs`, or authoring an artifact file), confirm the project has a root: run `openspec list --json` (with `--store ` when a store is selected, since the store is then the root) and read `root`. A root object means the project is set up. `"root": null` means it is not - there is no `openspec/` directory here, and a write such as `openspec new change` would create one as a side effect. The command also exits non-zero, which is that answer rather than a broken CLI, so read the JSON instead of retrying or working around it. Then stop before writing and ask the user how to proceed: set this project up (`openspec init`), target a store they already have (`--store `), or drop OpenSpec for this request and help them directly. Wait for their answer. Do not run `openspec init` until they ask for it, do not hand-create `openspec/` files, and do not let a command create the root as a side effect. `` is the spec directory relative to `specs/` (for example, `user-auth` or `identity/user-auth`). Preserve the full path from each delta spec when resolving its main spec. diff --git a/skills/openspec-update-change/SKILL.md b/skills/openspec-update-change/SKILL.md index 6430310067..d9b723b52a 100644 --- a/skills/openspec-update-change/SKILL.md +++ b/skills/openspec-update-change/SKILL.md @@ -13,7 +13,7 @@ Revise a change's existing planning artifacts and keep them coherent. Never edit **Store selection:** If the user names a store (a store is a standalone OpenSpec repo registered on this machine) or the work lives in one, run `openspec store list --json` to discover registered store ids, then pass `--store ` on the commands that read or write specs and changes (`new change`, `status`, `instructions`, `list`, `show`, `validate`, `archive`, `doctor`, `context`, `schemas`, `view`). Once selected, treat `--store ` as sticky for the rest of the workflow. Every unscoped example of those commands below is shorthand: before running it, append the flag. For example, run `openspec status --change "" --json --store ""`, not the unscoped form shown below. Other commands do not take the flag. Hints printed by commands already carry the flag; keep it on follow-ups. Without a store, commands act on the nearest local `openspec/` root. -**Project check:** These steps expect a project that already uses OpenSpec. Before the first step that writes anything (`new change`, `archive`, `sync specs`, or authoring an artifact file), confirm the project has a root: run `openspec list --json` and read `root`. A root object means the project is set up. `"root": null` means it is not - there is no `openspec/` directory here, and a write such as `openspec new change` would create one as a side effect. The command also exits non-zero, which is that answer rather than a broken CLI, so read the JSON instead of retrying or working around it. Then stop before writing and ask the user how to proceed: set this project up (`openspec init`), target a store they already have (`--store `), or drop OpenSpec for this request and help them directly. Wait for their answer. Do not run `openspec init` until they ask for it, do not hand-create `openspec/` files, and do not let a command create the root as a side effect. +**Project check:** These steps expect a project that already uses OpenSpec. Before the first step that writes anything (`new change`, `archive`, `sync specs`, or authoring an artifact file), confirm the project has a root: run `openspec list --json` (with `--store ` when a store is selected, since the store is then the root) and read `root`. A root object means the project is set up. `"root": null` means it is not - there is no `openspec/` directory here, and a write such as `openspec new change` would create one as a side effect. The command also exits non-zero, which is that answer rather than a broken CLI, so read the JSON instead of retrying or working around it. Then stop before writing and ask the user how to proceed: set this project up (`openspec init`), target a store they already have (`--store `), or drop OpenSpec for this request and help them directly. Wait for their answer. Do not run `openspec init` until they ask for it, do not hand-create `openspec/` files, and do not let a command create the root as a side effect. **Input**: Optionally specify a change name. If omitted, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes. diff --git a/skills/openspec-verify-change/SKILL.md b/skills/openspec-verify-change/SKILL.md index aaddf7ac41..b71e9f3003 100644 --- a/skills/openspec-verify-change/SKILL.md +++ b/skills/openspec-verify-change/SKILL.md @@ -13,7 +13,7 @@ Verify that an implementation matches the change artifacts (specs, tasks, design **Store selection:** If the user names a store (a store is a standalone OpenSpec repo registered on this machine) or the work lives in one, run `openspec store list --json` to discover registered store ids, then pass `--store ` on the commands that read or write specs and changes (`new change`, `status`, `instructions`, `list`, `show`, `validate`, `archive`, `doctor`, `context`, `schemas`, `view`). Once selected, treat `--store ` as sticky for the rest of the workflow. Every unscoped example of those commands below is shorthand: before running it, append the flag. For example, run `openspec status --change "" --json --store ""`, not the unscoped form shown below. Other commands do not take the flag. Hints printed by commands already carry the flag; keep it on follow-ups. Without a store, commands act on the nearest local `openspec/` root. -**Project check:** These steps expect a project that already uses OpenSpec. Before the first step that writes anything (`new change`, `archive`, `sync specs`, or authoring an artifact file), confirm the project has a root: run `openspec list --json` and read `root`. A root object means the project is set up. `"root": null` means it is not - there is no `openspec/` directory here, and a write such as `openspec new change` would create one as a side effect. The command also exits non-zero, which is that answer rather than a broken CLI, so read the JSON instead of retrying or working around it. Then stop before writing and ask the user how to proceed: set this project up (`openspec init`), target a store they already have (`--store `), or drop OpenSpec for this request and help them directly. Wait for their answer. Do not run `openspec init` until they ask for it, do not hand-create `openspec/` files, and do not let a command create the root as a side effect. +**Project check:** These steps expect a project that already uses OpenSpec. Before the first step that writes anything (`new change`, `archive`, `sync specs`, or authoring an artifact file), confirm the project has a root: run `openspec list --json` (with `--store ` when a store is selected, since the store is then the root) and read `root`. A root object means the project is set up. `"root": null` means it is not - there is no `openspec/` directory here, and a write such as `openspec new change` would create one as a side effect. The command also exits non-zero, which is that answer rather than a broken CLI, so read the JSON instead of retrying or working around it. Then stop before writing and ask the user how to proceed: set this project up (`openspec init`), target a store they already have (`--store `), or drop OpenSpec for this request and help them directly. Wait for their answer. Do not run `openspec init` until they ask for it, do not hand-create `openspec/` files, and do not let a command create the root as a side effect. **Input**: Optionally specify a change name. If omitted, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes. diff --git a/src/core/templates/workflows/project-root.ts b/src/core/templates/workflows/project-root.ts index fc794d49b0..d4c92aaab4 100644 --- a/src/core/templates/workflows/project-root.ts +++ b/src/core/templates/workflows/project-root.ts @@ -13,4 +13,4 @@ * because it refuses to fabricate an implicit root: it reports `root: null` * both when nothing is set up and when only stores are registered. */ -export const PROJECT_ROOT_GUARD = `**Project check:** These steps expect a project that already uses OpenSpec. Before the first step that writes anything (\`new change\`, \`archive\`, \`sync specs\`, or authoring an artifact file), confirm the project has a root: run \`openspec list --json\` and read \`root\`. A root object means the project is set up. \`"root": null\` means it is not - there is no \`openspec/\` directory here, and a write such as \`openspec new change\` would create one as a side effect. The command also exits non-zero, which is that answer rather than a broken CLI, so read the JSON instead of retrying or working around it. Then stop before writing and ask the user how to proceed: set this project up (\`openspec init\`), target a store they already have (\`--store \`), or drop OpenSpec for this request and help them directly. Wait for their answer. Do not run \`openspec init\` until they ask for it, do not hand-create \`openspec/\` files, and do not let a command create the root as a side effect.`; +export const PROJECT_ROOT_GUARD = `**Project check:** These steps expect a project that already uses OpenSpec. Before the first step that writes anything (\`new change\`, \`archive\`, \`sync specs\`, or authoring an artifact file), confirm the project has a root: run \`openspec list --json\` (with \`--store \` when a store is selected, since the store is then the root) and read \`root\`. A root object means the project is set up. \`"root": null\` means it is not - there is no \`openspec/\` directory here, and a write such as \`openspec new change\` would create one as a side effect. The command also exits non-zero, which is that answer rather than a broken CLI, so read the JSON instead of retrying or working around it. Then stop before writing and ask the user how to proceed: set this project up (\`openspec init\`), target a store they already have (\`--store \`), or drop OpenSpec for this request and help them directly. Wait for their answer. Do not run \`openspec init\` until they ask for it, do not hand-create \`openspec/\` files, and do not let a command create the root as a side effect.`; diff --git a/test/commands/store-root-selection.test.ts b/test/commands/store-root-selection.test.ts index 2511381e8c..c2b98be46f 100644 --- a/test/commands/store-root-selection.test.ts +++ b/test/commands/store-root-selection.test.ts @@ -621,8 +621,9 @@ operations: const firstOutput = created.stdout + created.stderr; expect(firstOutput).toContain('no OpenSpec root was found here'); // Naming the directory it created is the point: the reader has to know - // what to delete if this was not the project they meant. - expect(firstOutput).toMatch(/created at .*openspec\//); + // what to delete if this was not the project they meant. The path is + // relative to where the command ran, so it reads the same on Windows. + expect(firstOutput).toContain('created at openspec/.'); expect(firstOutput).toContain('openspec init'); expect(fs.existsSync(path.join(appRepo, 'openspec', 'changes', 'adopt-me'))).toBe(true); @@ -635,6 +636,29 @@ operations: expect(second.stdout + second.stderr).not.toContain('no OpenSpec root was found here'); }); + // Run from a subdirectory and the subdirectory is what gets adopted - the + // note has to name that directory, not the repository above it. + it('names the directory it actually adopted when run from a subdirectory', async () => { + const isolatedEnv = { + ...env, + XDG_DATA_HOME: path.join(tempDir, 'data-empty'), + }; + const nested = path.join(appRepo, 'services', 'billing'); + fs.mkdirSync(nested, { recursive: true }); + + const created = await runCLI(['new', 'change', 'adopt-the-subdir'], { + cwd: nested, + env: isolatedEnv, + }); + expect(created.exitCode).toBe(0); + expect(created.stdout + created.stderr).toContain('created at openspec/.'); + + expect(fs.existsSync(path.join(nested, 'openspec', 'changes', 'adopt-the-subdir'))).toBe( + true + ); + expect(fs.existsSync(path.join(appRepo, 'openspec'))).toBe(false); + }); + it('keeps the notice out of JSON output', async () => { const isolatedEnv = { ...env, diff --git a/test/core/templates/project-root-guard.test.ts b/test/core/templates/project-root-guard.test.ts index 54a56b5fd9..f84cda0e88 100644 --- a/test/core/templates/project-root-guard.test.ts +++ b/test/core/templates/project-root-guard.test.ts @@ -59,6 +59,9 @@ describe('project root guard', () => { // test/commands/store-root-selection.test.ts. it('names the machine-readable signal rather than a guess', () => { expect(PROJECT_ROOT_GUARD).toContain('openspec list --json'); + // A selected store is a root, so the check has to carry the flag or it + // answers a question about the wrong directory. + expect(PROJECT_ROOT_GUARD).toContain('with `--store ` when a store is selected'); expect(PROJECT_ROOT_GUARD).toContain('`"root": null`'); // An agent that reads the non-zero exit as a broken CLI is one step from // hand-creating `openspec/` instead, which is the failure being guarded. diff --git a/test/core/templates/skill-templates-parity.test.ts b/test/core/templates/skill-templates-parity.test.ts index 60351abcb2..9e56aa7a11 100644 --- a/test/core/templates/skill-templates-parity.test.ts +++ b/test/core/templates/skill-templates-parity.test.ts @@ -38,46 +38,46 @@ import { import { STORE_SELECTION_GUIDANCE } from '../../../src/core/templates/workflows/store-selection.js'; const EXPECTED_FUNCTION_HASHES: Record = { - getExploreSkillTemplate: '7919571d9b846b6f51fe0ee1544b649f1650071e5a8eb3d5795eeda9ee185dc2', - getNewChangeSkillTemplate: 'b6d9c999c664a4ca95b0b9ddf5be45a7595bce6644bd95e5e23a93db5353524d', - getContinueChangeSkillTemplate: '690b75890111d9b4f3cb44327cf7a86e8cd46d1b7812136e4b58f416b3b25b38', - getApplyChangeSkillTemplate: 'fd7467b24f4bb7051044237ed6d82357773ee77df659e64f28c331677ff28de6', - getFfChangeSkillTemplate: '1dc289a3de6999810f2d936b82706ff23402d33b79087636b4afc984f5006c41', - getSyncSpecsSkillTemplate: '23420a9bc829e4c2334a766b8f3f0498eb48293664e8c8e84685ed15865e0cdf', - getOnboardSkillTemplate: '0f15e46aae290322cb979033bf7575f7a42e9c009e148cbcfc3b487b6e2a8fcd', - getOpsxExploreCommandTemplate: '6167af7300894b85ef58e17178f941adadf5629865c21a6a79a32c78d1a90f6b', - getOpsxNewCommandTemplate: '700d80a18653bd1d37ba9017ca870049561bfff521241ef5c489a5174b7b451a', - getOpsxContinueCommandTemplate: 'bff75b037596be5bddd9f9499380427bd82c9092f1f613148af33a6bf8b0178b', - getOpsxApplyCommandTemplate: 'fffda846cf919e34a0a09abd4b177a385dc922d1073a6b36e6064135b39486db', - getOpsxFfCommandTemplate: 'c67f5b41a60bbbecaa9b9ad978e53d85c919225bbca89884ad8c49efed490680', - getArchiveChangeSkillTemplate: 'baaf840bd1eddce4fea1f8d9d0bd7710c548500201994481d42c748d0265f3f2', - getBulkArchiveChangeSkillTemplate: 'c23025010cdf629a3f45d6f351b803bedb048612420297f389752b420ead8949', - getOpsxSyncCommandTemplate: '82f5892c1a8c94bee2bcd25da8a1c8f883f5c97056ac436508dfc549b12ce052', - getVerifyChangeSkillTemplate: 'ba7eb2736abf1ea1b93a1bf949afb46b71c92414cf137497d70f650ecd71f74d', - getOpsxArchiveCommandTemplate: 'fc2b4ca635437f997b3ad1e78aa0d3a64eee78a5541199dd47d30ca5a03ca743', - getOpsxOnboardCommandTemplate: 'f387de3e2121c22734544382f8353a342bb306cae24dd292b626c9b6337dd30b', - getOpsxBulkArchiveCommandTemplate: '105b829eddd7acdcf414ebe0c77be65fe0e9b75d5ea72a73621e36675368c752', - getOpsxVerifyCommandTemplate: '5ca651adc9d78e43ff3b6ba7184ca64364b8a9e2b99488aba3ae6c7b36605f2f', - getOpsxProposeSkillTemplate: '1ff05e257ac308f690095dd276eb751b7e7825d981596b72cad44e8a8c2abf66', - getOpsxProposeCommandTemplate: 'bb16658833068ac1fde9b1e1aeadbd09347a95738e5e08736bbfdfdbb0314fed', + getExploreSkillTemplate: 'c201b946d28d690b42795a366e6a3d8763085386cf88abad5e4e100abcd66149', + getNewChangeSkillTemplate: 'dc2951d473aab531be6706705ce54971465f5943e66bfc31d3d7c6ee4ac80688', + getContinueChangeSkillTemplate: '84349c6a1bad53c2df7209cbe8d64318892beb83d1ea7aa302c8cc0097e5621a', + getApplyChangeSkillTemplate: '4aff0843213e93830f6fe2285d3395514e94864581c63e8d3ccf6dcf1b4d20fd', + getFfChangeSkillTemplate: '1973d7d1c7fbe210006a9f74a9dae6048f231773fc52fbd83363846401bcd5dd', + getSyncSpecsSkillTemplate: '44ef2db76bedaccf47f0cba3634cb51e4a820fb6946f34fa953355579b401ba2', + getOnboardSkillTemplate: 'bda1ebe2b6d316a8d17e2f7d4d2d2b259d0b587840b3e5e4793defd36ad84f99', + getOpsxExploreCommandTemplate: '76c7ed340902cd72a366857ec12331bb0039f4bc207d7033ef49d953a67c1feb', + getOpsxNewCommandTemplate: '5bdef93e9690b913f87c77b044256533865264822f72e65d31980ec064e03a7b', + getOpsxContinueCommandTemplate: '1893be6ab61be7b0addcf659acc324dca6d8e35ef2bb856b7049975ca97a039e', + getOpsxApplyCommandTemplate: 'c7a7979f655d2c09c883c3c99d9bb97cb6780aa578a96b3bbd2aa86a901ad7b8', + getOpsxFfCommandTemplate: '05033b1b32db175fcd9af41904681e73b7140623c3574b67ca46df5a264f5a49', + getArchiveChangeSkillTemplate: '7095dd5fc3863e13e05496891ea434d12fe90d3331eeb2fdc118e9c3af690fb9', + getBulkArchiveChangeSkillTemplate: '542ffe560de39bacc62ca85fd68b89f33b919f03bf23d51c8aebeb47f92fa171', + getOpsxSyncCommandTemplate: '3130fae9252e0a2a33c1476c650eec9da01ee0526b57d5434aee18eb44b1ed3b', + getVerifyChangeSkillTemplate: '7c86f4214ce8b1bf791d78b77c0a8123973369a7f9a396cdd8439cd40da4a8dd', + getOpsxArchiveCommandTemplate: 'c3698dfb3e8e82c385406263d905169d0bee72f188bb4839c1288feafba4db68', + getOpsxOnboardCommandTemplate: 'd747de9d729a8c86420f7deda1aa848ce51372916a03c87a51dd49701ab86d62', + getOpsxBulkArchiveCommandTemplate: '6ab10f00c182f52baebc5888f527ace0e8723f90e6950e927cea5a4945b2134b', + getOpsxVerifyCommandTemplate: '0e8dc5783b1a2fe6b7f385f73b26ff04dc9ca86c3e836e0d8c6d8c7ef37dde66', + getOpsxProposeSkillTemplate: '13038af568ee251adb775147a09dd3ee74069855b34ec5faebb2ecd4e412bbf4', + getOpsxProposeCommandTemplate: 'f42a9120d55d43f519f3a7635e25b86ed6b5632c8b62fe6997190080e9130753', getFeedbackSkillTemplate: 'dabeb5e825b9349abc8156c3e7b8608f27987912a6d9bf47ef29addde6138133', - getUpdateChangeSkillTemplate: 'b09beffde628370223ad6e36524c76d023ac9a1720939bffdb318334e2d757eb', - getOpsxUpdateCommandTemplate: 'd2561642635a36af2544dfb98c03c3838cb5e8c7b574a9f95327aebb2a1eb4ae', + getUpdateChangeSkillTemplate: '90377a82f628e9891e6f72a78db68551e32896c404a55408628e44dbc80848ac', + getOpsxUpdateCommandTemplate: 'e391160a9e77a2cf13b5c346b571d127188741e8a748530e46a24734ac85c4f1', }; const EXPECTED_GENERATED_SKILL_CONTENT_HASHES: Record = { - 'openspec-explore': '3976176121cefd78024b3eac88901d505f6494c5e871461a32d17b64098c4ec7', - 'openspec-new-change': 'a1cfdc950f9016cacb2fe0c29d4dbbb331dc2b9e64682844f47b2a9b2fe27eaa', - 'openspec-continue-change': 'a5cffd1d9a8f62623f66110ff959bfa7c419209114fcc2fcc4ab9a208714595c', - 'openspec-apply-change': '58ca8f83d9ff08a1c0b61aad42921bcb2e707899cc30fda0eeea430503dd26da', - 'openspec-ff-change': '8d7806a063f04fe4cb6b115ac198bb190c20979af30239f9082f287802f5c016', - 'openspec-sync-specs': 'cd22186ac839d09ab370cc16457f9f06905fb4eeca881d25af19bcc6b1d1c5a5', - 'openspec-archive-change': '7c3fc3eeb5a18d50927368dc51ac7ef27d341b9253ed3e7fcf83031c42b1884a', - 'openspec-bulk-archive-change': '2979576072c4ab851c9e4bd00e6d02d1005feb9a2afa83793381d8f10bd172a4', - 'openspec-verify-change': 'da28c468d7ffb1245cc4b6222e87733109187bc78336a99163e379256af45f5c', - 'openspec-onboard': '65e44559221053472b696ee41c0ee24003557cb518b392f84a0c4c64bd3db16e', - 'openspec-propose': '50892baa28106968042605dd63ec12c5f8801b617020c58508c16fa895a74ef7', - 'openspec-update-change': '586bf934b24d671e545e7fd44f5eec0858decb5403eb5884ffd95bc15d2db4b0', + 'openspec-explore': '27bbe34bc9254d76210a06d540ff6b2caa4c4962e9e62c6cdc0cb49293293c52', + 'openspec-new-change': '651028f7a0f72e0505404798f48fd0f34efbf773bf25bcdfc627b2bea11a7790', + 'openspec-continue-change': 'a1a336bfd50539a00c279f4176e88a9a311b772908479788b1d94a69e70d3b42', + 'openspec-apply-change': '5cc0e8d5be257227c5bad9eac771ce2527ed555f5152f0618b958d3ed0a9523f', + 'openspec-ff-change': '890162ee129104b727949b3345acf229865f59c8d3107e0299ef69631a9c5b17', + 'openspec-sync-specs': 'f833aa14447079f7465fc8ecfd4766de123d1e9675c86dab8490554d965e9459', + 'openspec-archive-change': '295d50f79ef186999ae088857479834550cc2574253d21580b87937be93326a5', + 'openspec-bulk-archive-change': '4f38ad54d54caea1afcb94468192f5bdd8905f01e32d0b763796252ca9547965', + 'openspec-verify-change': 'd7f71e25be20839d01cfe2b3e9c0a88aa3a997aed0e2b85f1325d079a309d587', + 'openspec-onboard': '5dd2569c661d5032e1e89f5e5b06da7a099511e600d12b4e89bb1d49b9591968', + 'openspec-propose': '18f1c017a4c0ce1d8ce589522bed33f5bc6d915798358574b4c3116207ecf982', + 'openspec-update-change': 'b3dfd0dbdc05e77b8e385b5c3a1f67fd9182ace7201fe7bcac947320d4abc5c4', }; // Intentionally excludes getFeedbackSkillTemplate: this list only models templates From 5bfe9053353215ece57c440b98c026479e2ce2b9 Mon Sep 17 00:00:00 2001 From: Clay Good Date: Mon, 7 Sep 2026 11:34:53 -0500 Subject: [PATCH 11/15] 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 --- docs-lab/reference/cli.md | 13 +++++++++++++ docs-lab/reference/skills.md | 2 ++ docs/troubleshooting.md | 6 ------ 3 files changed, 15 insertions(+), 6 deletions(-) diff --git a/docs-lab/reference/cli.md b/docs-lab/reference/cli.md index e9e4eb14fd..2c3760852f 100644 --- a/docs-lab/reference/cli.md +++ b/docs-lab/reference/cli.md @@ -999,6 +999,19 @@ Schema: spec-driven Next: openspec status --change add-caching ``` +When no `openspec/` directory was found, `new change` creates one where you are and says so: + +``` +Created change 'add-caching' at openspec/changes/add-caching/ +Schema: spec-driven +Next: openspec status --change add-caching + +Note: no OpenSpec root was found here, so one was created at openspec/. +Run `openspec init` to finish setting this project up, or delete that directory if you meant a different project. +``` + +The notice goes to stdout with the rest of the human output, and never appears with `--json`. In JSON, `root.source` reads `implicit` for the same case. + With `--json`: ```json diff --git a/docs-lab/reference/skills.md b/docs-lab/reference/skills.md index 74437a0dd6..755a1eafc4 100644 --- a/docs-lab/reference/skills.md +++ b/docs-lab/reference/skills.md @@ -39,6 +39,8 @@ The skills come in two sets: - **Core**: installed by default, the main planning loop. - **Optional**: installed only when you add them, via [Profiles](../customize/profiles.md). +Every skill expects a project that already uses OpenSpec. Before its first step that writes anything, a skill confirms the resolved root and stops when there is none, then asks how to proceed: set this project up with `openspec init`, target a store you already have with `--store `, or drop OpenSpec for that request. It waits for your answer, and never creates an `openspec/` directory on its own. The entries below describe what each skill does once a root is in place. + | Skill | Job | Type | |---|---|---| | [openspec-explore](#openspec-explore) | Think through an idea before it becomes a change proposal | Core | diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index b1d9561b7d..f35ada883a 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -37,12 +37,6 @@ openspec init --tools claude,cursor The full list of tool IDs is in [Supported Tools](supported-tools.md). Use `--tools all` for everything, `--tools none` to skip tool setup. -### The AI started an OpenSpec workflow in a project that doesn't use OpenSpec - -Skills and commands are installed once per tool, so they are offered in every project you open. If you ask for a proposal in a project that never ran `openspec init`, the assistant stops and asks whether to set the project up, work against a store (`--store `), or drop OpenSpec for that request. - -If it went ahead anyway, it is running older instruction files: upgrade the CLI, run `openspec update` in your OpenSpec projects, and restart your assistant. To check a directory yourself, run `openspec list --json` — `"root": null` means there is no `openspec/` directory here, and a command like `openspec new change` would create one. `openspec new change` also prints a note when it had to create the root itself. An `openspec/` directory created this way is safe to delete if you didn't want it. - ## Commands don't show up If `/opsx:propose` (or your tool's equivalent) doesn't appear or doesn't do anything, work down this list. They're ordered fastest-to-check first. From ce18b10cd851af8dbb1d1504232d033e8ac837bc Mon Sep 17 00:00:00 2001 From: Clay Good Date: Wed, 9 Sep 2026 07:43:03 -0500 Subject: [PATCH 12/15] 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 --- docs-lab/reference/skills.md | 7 +- skills/openspec-apply-change/SKILL.md | 9 ++- skills/openspec-archive-change/SKILL.md | 9 ++- skills/openspec-bulk-archive-change/SKILL.md | 9 ++- skills/openspec-continue-change/SKILL.md | 9 ++- skills/openspec-explore/SKILL.md | 9 ++- skills/openspec-ff-change/SKILL.md | 9 ++- skills/openspec-new-change/SKILL.md | 9 ++- skills/openspec-onboard/SKILL.md | 9 ++- skills/openspec-propose/SKILL.md | 9 ++- skills/openspec-sync-specs/SKILL.md | 9 ++- skills/openspec-update-change/SKILL.md | 9 ++- skills/openspec-verify-change/SKILL.md | 9 ++- src/core/templates/workflows/project-root.ts | 29 ++++++-- .../core/templates/project-root-guard.test.ts | 57 +++++++++++++-- .../templates/skill-templates-parity.test.ts | 72 +++++++++---------- 16 files changed, 212 insertions(+), 61 deletions(-) diff --git a/docs-lab/reference/skills.md b/docs-lab/reference/skills.md index 755a1eafc4..ab6474cab9 100644 --- a/docs-lab/reference/skills.md +++ b/docs-lab/reference/skills.md @@ -39,7 +39,12 @@ The skills come in two sets: - **Core**: installed by default, the main planning loop. - **Optional**: installed only when you add them, via [Profiles](../customize/profiles.md). -Every skill expects a project that already uses OpenSpec. Before its first step that writes anything, a skill confirms the resolved root and stops when there is none, then asks how to proceed: set this project up with `openspec init`, target a store you already have with `--store `, or drop OpenSpec for that request. It waits for your answer, and never creates an `openspec/` directory on its own. The entries below describe what each skill does once a root is in place. +Every skill expects a project that already uses OpenSpec. Before its first step that writes anything, a skill checks for a resolved root. What happens when there is none depends on how the skill was reached: + +- **Auto-selected**: your agent picked the skill on its own, without you naming OpenSpec. It drops OpenSpec and answers your request normally, the way it would with OpenSpec not installed. +- **Explicit OpenSpec request**: you named OpenSpec, named the skill, or ran its command. It stops before writing and asks how to proceed: run `openspec init` here, target a store with `--store `, or continue without OpenSpec. It waits for your answer. + +Commands are always the second case. No skill creates an `openspec/` directory on its own in either case. The entries below describe what each skill does once a root is in place. | Skill | Job | Type | |---|---|---| diff --git a/skills/openspec-apply-change/SKILL.md b/skills/openspec-apply-change/SKILL.md index b822d56f45..1599cb2195 100644 --- a/skills/openspec-apply-change/SKILL.md +++ b/skills/openspec-apply-change/SKILL.md @@ -13,7 +13,14 @@ Implement tasks from an OpenSpec change. **Store selection:** If the user names a store (a store is a standalone OpenSpec repo registered on this machine) or the work lives in one, run `openspec store list --json` to discover registered store ids, then pass `--store ` on the commands that read or write specs and changes (`new change`, `status`, `instructions`, `list`, `show`, `validate`, `archive`, `doctor`, `context`, `schemas`, `view`). Once selected, treat `--store ` as sticky for the rest of the workflow. Every unscoped example of those commands below is shorthand: before running it, append the flag. For example, run `openspec status --change "" --json --store ""`, not the unscoped form shown below. Other commands do not take the flag. Hints printed by commands already carry the flag; keep it on follow-ups. Without a store, commands act on the nearest local `openspec/` root. -**Project check:** These steps expect a project that already uses OpenSpec. Before the first step that writes anything (`new change`, `archive`, `sync specs`, or authoring an artifact file), confirm the project has a root: run `openspec list --json` (with `--store ` when a store is selected, since the store is then the root) and read `root`. A root object means the project is set up. `"root": null` means it is not - there is no `openspec/` directory here, and a write such as `openspec new change` would create one as a side effect. The command also exits non-zero, which is that answer rather than a broken CLI, so read the JSON instead of retrying or working around it. Then stop before writing and ask the user how to proceed: set this project up (`openspec init`), target a store they already have (`--store `), or drop OpenSpec for this request and help them directly. Wait for their answer. Do not run `openspec init` until they ask for it, do not hand-create `openspec/` files, and do not let a command create the root as a side effect. +**Project check:** These steps expect a project that already uses OpenSpec. Before the first step that writes anything (`new change`, `archive`, `sync specs`, or authoring an artifact file), confirm the project has a root: run `openspec list --json` (with `--store ` when a store is selected, since the store is then the root) and read `root`. A root object means the project is set up. `"root": null` means it is not - there is no `openspec/` directory here, and a write such as `openspec new change` would create one as a side effect. The command also exits non-zero, which is that answer rather than a broken CLI, so read the JSON instead of retrying or working around it. + +With no root, what happens next depends on how this workflow was reached: + +- **Auto-selected**: you chose this workflow yourself, without the user naming OpenSpec, naming this skill, or running its slash command. Stop using OpenSpec and answer the request normally, as you would with no OpenSpec installed. Do not ask them to set anything up and do not mention OpenSpec setup. +- **Explicit OpenSpec request**: the user named OpenSpec, named this skill, or ran its slash command. Stop before writing and ask how to proceed: set this project up (`openspec init`), target a store they already have (`--store `), or continue without OpenSpec for this request. Wait for their answer. + +In both branches, never create the root as a side effect: do not run `openspec init` until the user asks for it, do not hand-create `openspec/` files, and do not let a command create it. **Input**: Optionally specify a change name (e.g., `/openspec-apply-change add-auth`). If omitted, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes. diff --git a/skills/openspec-archive-change/SKILL.md b/skills/openspec-archive-change/SKILL.md index 3e717138ba..3a244a0c0c 100644 --- a/skills/openspec-archive-change/SKILL.md +++ b/skills/openspec-archive-change/SKILL.md @@ -13,7 +13,14 @@ Archive a completed change in the experimental workflow. **Store selection:** If the user names a store (a store is a standalone OpenSpec repo registered on this machine) or the work lives in one, run `openspec store list --json` to discover registered store ids, then pass `--store ` on the commands that read or write specs and changes (`new change`, `status`, `instructions`, `list`, `show`, `validate`, `archive`, `doctor`, `context`, `schemas`, `view`). Once selected, treat `--store ` as sticky for the rest of the workflow. Every unscoped example of those commands below is shorthand: before running it, append the flag. For example, run `openspec status --change "" --json --store ""`, not the unscoped form shown below. Other commands do not take the flag. Hints printed by commands already carry the flag; keep it on follow-ups. Without a store, commands act on the nearest local `openspec/` root. -**Project check:** These steps expect a project that already uses OpenSpec. Before the first step that writes anything (`new change`, `archive`, `sync specs`, or authoring an artifact file), confirm the project has a root: run `openspec list --json` (with `--store ` when a store is selected, since the store is then the root) and read `root`. A root object means the project is set up. `"root": null` means it is not - there is no `openspec/` directory here, and a write such as `openspec new change` would create one as a side effect. The command also exits non-zero, which is that answer rather than a broken CLI, so read the JSON instead of retrying or working around it. Then stop before writing and ask the user how to proceed: set this project up (`openspec init`), target a store they already have (`--store `), or drop OpenSpec for this request and help them directly. Wait for their answer. Do not run `openspec init` until they ask for it, do not hand-create `openspec/` files, and do not let a command create the root as a side effect. +**Project check:** These steps expect a project that already uses OpenSpec. Before the first step that writes anything (`new change`, `archive`, `sync specs`, or authoring an artifact file), confirm the project has a root: run `openspec list --json` (with `--store ` when a store is selected, since the store is then the root) and read `root`. A root object means the project is set up. `"root": null` means it is not - there is no `openspec/` directory here, and a write such as `openspec new change` would create one as a side effect. The command also exits non-zero, which is that answer rather than a broken CLI, so read the JSON instead of retrying or working around it. + +With no root, what happens next depends on how this workflow was reached: + +- **Auto-selected**: you chose this workflow yourself, without the user naming OpenSpec, naming this skill, or running its slash command. Stop using OpenSpec and answer the request normally, as you would with no OpenSpec installed. Do not ask them to set anything up and do not mention OpenSpec setup. +- **Explicit OpenSpec request**: the user named OpenSpec, named this skill, or ran its slash command. Stop before writing and ask how to proceed: set this project up (`openspec init`), target a store they already have (`--store `), or continue without OpenSpec for this request. Wait for their answer. + +In both branches, never create the root as a side effect: do not run `openspec init` until the user asks for it, do not hand-create `openspec/` files, and do not let a command create it. `` is the spec directory relative to `specs/` (for example, `user-auth` or `identity/user-auth`). Preserve the full path from each delta spec when resolving its main spec. diff --git a/skills/openspec-bulk-archive-change/SKILL.md b/skills/openspec-bulk-archive-change/SKILL.md index a20aaeeee1..8d74167dae 100644 --- a/skills/openspec-bulk-archive-change/SKILL.md +++ b/skills/openspec-bulk-archive-change/SKILL.md @@ -15,7 +15,14 @@ This skill allows you to batch-archive changes, handling spec conflicts intellig **Store selection:** If the user names a store (a store is a standalone OpenSpec repo registered on this machine) or the work lives in one, run `openspec store list --json` to discover registered store ids, then pass `--store ` on the commands that read or write specs and changes (`new change`, `status`, `instructions`, `list`, `show`, `validate`, `archive`, `doctor`, `context`, `schemas`, `view`). Once selected, treat `--store ` as sticky for the rest of the workflow. Every unscoped example of those commands below is shorthand: before running it, append the flag. For example, run `openspec status --change "" --json --store ""`, not the unscoped form shown below. Other commands do not take the flag. Hints printed by commands already carry the flag; keep it on follow-ups. Without a store, commands act on the nearest local `openspec/` root. -**Project check:** These steps expect a project that already uses OpenSpec. Before the first step that writes anything (`new change`, `archive`, `sync specs`, or authoring an artifact file), confirm the project has a root: run `openspec list --json` (with `--store ` when a store is selected, since the store is then the root) and read `root`. A root object means the project is set up. `"root": null` means it is not - there is no `openspec/` directory here, and a write such as `openspec new change` would create one as a side effect. The command also exits non-zero, which is that answer rather than a broken CLI, so read the JSON instead of retrying or working around it. Then stop before writing and ask the user how to proceed: set this project up (`openspec init`), target a store they already have (`--store `), or drop OpenSpec for this request and help them directly. Wait for their answer. Do not run `openspec init` until they ask for it, do not hand-create `openspec/` files, and do not let a command create the root as a side effect. +**Project check:** These steps expect a project that already uses OpenSpec. Before the first step that writes anything (`new change`, `archive`, `sync specs`, or authoring an artifact file), confirm the project has a root: run `openspec list --json` (with `--store ` when a store is selected, since the store is then the root) and read `root`. A root object means the project is set up. `"root": null` means it is not - there is no `openspec/` directory here, and a write such as `openspec new change` would create one as a side effect. The command also exits non-zero, which is that answer rather than a broken CLI, so read the JSON instead of retrying or working around it. + +With no root, what happens next depends on how this workflow was reached: + +- **Auto-selected**: you chose this workflow yourself, without the user naming OpenSpec, naming this skill, or running its slash command. Stop using OpenSpec and answer the request normally, as you would with no OpenSpec installed. Do not ask them to set anything up and do not mention OpenSpec setup. +- **Explicit OpenSpec request**: the user named OpenSpec, named this skill, or ran its slash command. Stop before writing and ask how to proceed: set this project up (`openspec init`), target a store they already have (`--store `), or continue without OpenSpec for this request. Wait for their answer. + +In both branches, never create the root as a side effect: do not run `openspec init` until the user asks for it, do not hand-create `openspec/` files, and do not let a command create it. `` is the spec directory relative to `specs/` (for example, `user-auth` or `identity/user-auth`). Preserve the full path from each delta spec when resolving its main spec. diff --git a/skills/openspec-continue-change/SKILL.md b/skills/openspec-continue-change/SKILL.md index 9139a25673..1fce8351fe 100644 --- a/skills/openspec-continue-change/SKILL.md +++ b/skills/openspec-continue-change/SKILL.md @@ -13,7 +13,14 @@ Continue working on a change by creating the next artifact. **Store selection:** If the user names a store (a store is a standalone OpenSpec repo registered on this machine) or the work lives in one, run `openspec store list --json` to discover registered store ids, then pass `--store ` on the commands that read or write specs and changes (`new change`, `status`, `instructions`, `list`, `show`, `validate`, `archive`, `doctor`, `context`, `schemas`, `view`). Once selected, treat `--store ` as sticky for the rest of the workflow. Every unscoped example of those commands below is shorthand: before running it, append the flag. For example, run `openspec status --change "" --json --store ""`, not the unscoped form shown below. Other commands do not take the flag. Hints printed by commands already carry the flag; keep it on follow-ups. Without a store, commands act on the nearest local `openspec/` root. -**Project check:** These steps expect a project that already uses OpenSpec. Before the first step that writes anything (`new change`, `archive`, `sync specs`, or authoring an artifact file), confirm the project has a root: run `openspec list --json` (with `--store ` when a store is selected, since the store is then the root) and read `root`. A root object means the project is set up. `"root": null` means it is not - there is no `openspec/` directory here, and a write such as `openspec new change` would create one as a side effect. The command also exits non-zero, which is that answer rather than a broken CLI, so read the JSON instead of retrying or working around it. Then stop before writing and ask the user how to proceed: set this project up (`openspec init`), target a store they already have (`--store `), or drop OpenSpec for this request and help them directly. Wait for their answer. Do not run `openspec init` until they ask for it, do not hand-create `openspec/` files, and do not let a command create the root as a side effect. +**Project check:** These steps expect a project that already uses OpenSpec. Before the first step that writes anything (`new change`, `archive`, `sync specs`, or authoring an artifact file), confirm the project has a root: run `openspec list --json` (with `--store ` when a store is selected, since the store is then the root) and read `root`. A root object means the project is set up. `"root": null` means it is not - there is no `openspec/` directory here, and a write such as `openspec new change` would create one as a side effect. The command also exits non-zero, which is that answer rather than a broken CLI, so read the JSON instead of retrying or working around it. + +With no root, what happens next depends on how this workflow was reached: + +- **Auto-selected**: you chose this workflow yourself, without the user naming OpenSpec, naming this skill, or running its slash command. Stop using OpenSpec and answer the request normally, as you would with no OpenSpec installed. Do not ask them to set anything up and do not mention OpenSpec setup. +- **Explicit OpenSpec request**: the user named OpenSpec, named this skill, or ran its slash command. Stop before writing and ask how to proceed: set this project up (`openspec init`), target a store they already have (`--store `), or continue without OpenSpec for this request. Wait for their answer. + +In both branches, never create the root as a side effect: do not run `openspec init` until the user asks for it, do not hand-create `openspec/` files, and do not let a command create it. **Input**: Optionally specify a change name. If omitted, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes. diff --git a/skills/openspec-explore/SKILL.md b/skills/openspec-explore/SKILL.md index db3f0cf0d1..c2d937cdd2 100644 --- a/skills/openspec-explore/SKILL.md +++ b/skills/openspec-explore/SKILL.md @@ -17,7 +17,14 @@ Enter explore mode. Think deeply. Visualize freely. Follow the conversation wher **Store selection:** If the user names a store (a store is a standalone OpenSpec repo registered on this machine) or the work lives in one, run `openspec store list --json` to discover registered store ids, then pass `--store ` on the commands that read or write specs and changes (`new change`, `status`, `instructions`, `list`, `show`, `validate`, `archive`, `doctor`, `context`, `schemas`, `view`). Once selected, treat `--store ` as sticky for the rest of the workflow. Every unscoped example of those commands below is shorthand: before running it, append the flag. For example, run `openspec status --change "" --json --store ""`, not the unscoped form shown below. Other commands do not take the flag. Hints printed by commands already carry the flag; keep it on follow-ups. Without a store, commands act on the nearest local `openspec/` root. -**Project check:** These steps expect a project that already uses OpenSpec. Before the first step that writes anything (`new change`, `archive`, `sync specs`, or authoring an artifact file), confirm the project has a root: run `openspec list --json` (with `--store ` when a store is selected, since the store is then the root) and read `root`. A root object means the project is set up. `"root": null` means it is not - there is no `openspec/` directory here, and a write such as `openspec new change` would create one as a side effect. The command also exits non-zero, which is that answer rather than a broken CLI, so read the JSON instead of retrying or working around it. Then stop before writing and ask the user how to proceed: set this project up (`openspec init`), target a store they already have (`--store `), or drop OpenSpec for this request and help them directly. Wait for their answer. Do not run `openspec init` until they ask for it, do not hand-create `openspec/` files, and do not let a command create the root as a side effect. +**Project check:** These steps expect a project that already uses OpenSpec. Before the first step that writes anything (`new change`, `archive`, `sync specs`, or authoring an artifact file), confirm the project has a root: run `openspec list --json` (with `--store ` when a store is selected, since the store is then the root) and read `root`. A root object means the project is set up. `"root": null` means it is not - there is no `openspec/` directory here, and a write such as `openspec new change` would create one as a side effect. The command also exits non-zero, which is that answer rather than a broken CLI, so read the JSON instead of retrying or working around it. + +With no root, what happens next depends on how this workflow was reached: + +- **Auto-selected**: you chose this workflow yourself, without the user naming OpenSpec, naming this skill, or running its slash command. Stop using OpenSpec and answer the request normally, as you would with no OpenSpec installed. Do not ask them to set anything up and do not mention OpenSpec setup. +- **Explicit OpenSpec request**: the user named OpenSpec, named this skill, or ran its slash command. Stop before writing and ask how to proceed: set this project up (`openspec init`), target a store they already have (`--store `), or continue without OpenSpec for this request. Wait for their answer. + +In both branches, never create the root as a side effect: do not run `openspec init` until the user asks for it, do not hand-create `openspec/` files, and do not let a command create it. --- diff --git a/skills/openspec-ff-change/SKILL.md b/skills/openspec-ff-change/SKILL.md index dd5353c1a1..815c1bfb6e 100644 --- a/skills/openspec-ff-change/SKILL.md +++ b/skills/openspec-ff-change/SKILL.md @@ -13,7 +13,14 @@ Fast-forward through artifact creation - generate everything needed to start imp **Store selection:** If the user names a store (a store is a standalone OpenSpec repo registered on this machine) or the work lives in one, run `openspec store list --json` to discover registered store ids, then pass `--store ` on the commands that read or write specs and changes (`new change`, `status`, `instructions`, `list`, `show`, `validate`, `archive`, `doctor`, `context`, `schemas`, `view`). Once selected, treat `--store ` as sticky for the rest of the workflow. Every unscoped example of those commands below is shorthand: before running it, append the flag. For example, run `openspec status --change "" --json --store ""`, not the unscoped form shown below. Other commands do not take the flag. Hints printed by commands already carry the flag; keep it on follow-ups. Without a store, commands act on the nearest local `openspec/` root. -**Project check:** These steps expect a project that already uses OpenSpec. Before the first step that writes anything (`new change`, `archive`, `sync specs`, or authoring an artifact file), confirm the project has a root: run `openspec list --json` (with `--store ` when a store is selected, since the store is then the root) and read `root`. A root object means the project is set up. `"root": null` means it is not - there is no `openspec/` directory here, and a write such as `openspec new change` would create one as a side effect. The command also exits non-zero, which is that answer rather than a broken CLI, so read the JSON instead of retrying or working around it. Then stop before writing and ask the user how to proceed: set this project up (`openspec init`), target a store they already have (`--store `), or drop OpenSpec for this request and help them directly. Wait for their answer. Do not run `openspec init` until they ask for it, do not hand-create `openspec/` files, and do not let a command create the root as a side effect. +**Project check:** These steps expect a project that already uses OpenSpec. Before the first step that writes anything (`new change`, `archive`, `sync specs`, or authoring an artifact file), confirm the project has a root: run `openspec list --json` (with `--store ` when a store is selected, since the store is then the root) and read `root`. A root object means the project is set up. `"root": null` means it is not - there is no `openspec/` directory here, and a write such as `openspec new change` would create one as a side effect. The command also exits non-zero, which is that answer rather than a broken CLI, so read the JSON instead of retrying or working around it. + +With no root, what happens next depends on how this workflow was reached: + +- **Auto-selected**: you chose this workflow yourself, without the user naming OpenSpec, naming this skill, or running its slash command. Stop using OpenSpec and answer the request normally, as you would with no OpenSpec installed. Do not ask them to set anything up and do not mention OpenSpec setup. +- **Explicit OpenSpec request**: the user named OpenSpec, named this skill, or ran its slash command. Stop before writing and ask how to proceed: set this project up (`openspec init`), target a store they already have (`--store `), or continue without OpenSpec for this request. Wait for their answer. + +In both branches, never create the root as a side effect: do not run `openspec init` until the user asks for it, do not hand-create `openspec/` files, and do not let a command create it. **Input**: The user's request should include a change name (kebab-case) OR a description of what they want to build. diff --git a/skills/openspec-new-change/SKILL.md b/skills/openspec-new-change/SKILL.md index 91b1a85819..be1305e992 100644 --- a/skills/openspec-new-change/SKILL.md +++ b/skills/openspec-new-change/SKILL.md @@ -13,7 +13,14 @@ Start a new change using the experimental artifact-driven approach. **Store selection:** If the user names a store (a store is a standalone OpenSpec repo registered on this machine) or the work lives in one, run `openspec store list --json` to discover registered store ids, then pass `--store ` on the commands that read or write specs and changes (`new change`, `status`, `instructions`, `list`, `show`, `validate`, `archive`, `doctor`, `context`, `schemas`, `view`). Once selected, treat `--store ` as sticky for the rest of the workflow. Every unscoped example of those commands below is shorthand: before running it, append the flag. For example, run `openspec status --change "" --json --store ""`, not the unscoped form shown below. Other commands do not take the flag. Hints printed by commands already carry the flag; keep it on follow-ups. Without a store, commands act on the nearest local `openspec/` root. -**Project check:** These steps expect a project that already uses OpenSpec. Before the first step that writes anything (`new change`, `archive`, `sync specs`, or authoring an artifact file), confirm the project has a root: run `openspec list --json` (with `--store ` when a store is selected, since the store is then the root) and read `root`. A root object means the project is set up. `"root": null` means it is not - there is no `openspec/` directory here, and a write such as `openspec new change` would create one as a side effect. The command also exits non-zero, which is that answer rather than a broken CLI, so read the JSON instead of retrying or working around it. Then stop before writing and ask the user how to proceed: set this project up (`openspec init`), target a store they already have (`--store `), or drop OpenSpec for this request and help them directly. Wait for their answer. Do not run `openspec init` until they ask for it, do not hand-create `openspec/` files, and do not let a command create the root as a side effect. +**Project check:** These steps expect a project that already uses OpenSpec. Before the first step that writes anything (`new change`, `archive`, `sync specs`, or authoring an artifact file), confirm the project has a root: run `openspec list --json` (with `--store ` when a store is selected, since the store is then the root) and read `root`. A root object means the project is set up. `"root": null` means it is not - there is no `openspec/` directory here, and a write such as `openspec new change` would create one as a side effect. The command also exits non-zero, which is that answer rather than a broken CLI, so read the JSON instead of retrying or working around it. + +With no root, what happens next depends on how this workflow was reached: + +- **Auto-selected**: you chose this workflow yourself, without the user naming OpenSpec, naming this skill, or running its slash command. Stop using OpenSpec and answer the request normally, as you would with no OpenSpec installed. Do not ask them to set anything up and do not mention OpenSpec setup. +- **Explicit OpenSpec request**: the user named OpenSpec, named this skill, or ran its slash command. Stop before writing and ask how to proceed: set this project up (`openspec init`), target a store they already have (`--store `), or continue without OpenSpec for this request. Wait for their answer. + +In both branches, never create the root as a side effect: do not run `openspec init` until the user asks for it, do not hand-create `openspec/` files, and do not let a command create it. **Input**: The user's request should include a change name (kebab-case) OR a description of what they want to build. diff --git a/skills/openspec-onboard/SKILL.md b/skills/openspec-onboard/SKILL.md index 4803b91ef1..fc0122a17a 100644 --- a/skills/openspec-onboard/SKILL.md +++ b/skills/openspec-onboard/SKILL.md @@ -13,7 +13,14 @@ Guide the user through their first complete OpenSpec workflow cycle. This is a t **Store selection:** If the user names a store (a store is a standalone OpenSpec repo registered on this machine) or the work lives in one, run `openspec store list --json` to discover registered store ids, then pass `--store ` on the commands that read or write specs and changes (`new change`, `status`, `instructions`, `list`, `show`, `validate`, `archive`, `doctor`, `context`, `schemas`, `view`). Once selected, treat `--store ` as sticky for the rest of the workflow. Every unscoped example of those commands below is shorthand: before running it, append the flag. For example, run `openspec status --change "" --json --store ""`, not the unscoped form shown below. Other commands do not take the flag. Hints printed by commands already carry the flag; keep it on follow-ups. Without a store, commands act on the nearest local `openspec/` root. -**Project check:** These steps expect a project that already uses OpenSpec. Before the first step that writes anything (`new change`, `archive`, `sync specs`, or authoring an artifact file), confirm the project has a root: run `openspec list --json` (with `--store ` when a store is selected, since the store is then the root) and read `root`. A root object means the project is set up. `"root": null` means it is not - there is no `openspec/` directory here, and a write such as `openspec new change` would create one as a side effect. The command also exits non-zero, which is that answer rather than a broken CLI, so read the JSON instead of retrying or working around it. Then stop before writing and ask the user how to proceed: set this project up (`openspec init`), target a store they already have (`--store `), or drop OpenSpec for this request and help them directly. Wait for their answer. Do not run `openspec init` until they ask for it, do not hand-create `openspec/` files, and do not let a command create the root as a side effect. +**Project check:** These steps expect a project that already uses OpenSpec. Before the first step that writes anything (`new change`, `archive`, `sync specs`, or authoring an artifact file), confirm the project has a root: run `openspec list --json` (with `--store ` when a store is selected, since the store is then the root) and read `root`. A root object means the project is set up. `"root": null` means it is not - there is no `openspec/` directory here, and a write such as `openspec new change` would create one as a side effect. The command also exits non-zero, which is that answer rather than a broken CLI, so read the JSON instead of retrying or working around it. + +With no root, what happens next depends on how this workflow was reached: + +- **Auto-selected**: you chose this workflow yourself, without the user naming OpenSpec, naming this skill, or running its slash command. Stop using OpenSpec and answer the request normally, as you would with no OpenSpec installed. Do not ask them to set anything up and do not mention OpenSpec setup. +- **Explicit OpenSpec request**: the user named OpenSpec, named this skill, or ran its slash command. Stop before writing and ask how to proceed: set this project up (`openspec init`), target a store they already have (`--store `), or continue without OpenSpec for this request. Wait for their answer. + +In both branches, never create the root as a side effect: do not run `openspec init` until the user asks for it, do not hand-create `openspec/` files, and do not let a command create it. --- diff --git a/skills/openspec-propose/SKILL.md b/skills/openspec-propose/SKILL.md index da016c1e80..a654084a55 100644 --- a/skills/openspec-propose/SKILL.md +++ b/skills/openspec-propose/SKILL.md @@ -27,7 +27,14 @@ When the user is ready to implement, they must start the apply workflow explicit **Store selection:** If the user names a store (a store is a standalone OpenSpec repo registered on this machine) or the work lives in one, run `openspec store list --json` to discover registered store ids, then pass `--store ` on the commands that read or write specs and changes (`new change`, `status`, `instructions`, `list`, `show`, `validate`, `archive`, `doctor`, `context`, `schemas`, `view`). Once selected, treat `--store ` as sticky for the rest of the workflow. Every unscoped example of those commands below is shorthand: before running it, append the flag. For example, run `openspec status --change "" --json --store ""`, not the unscoped form shown below. Other commands do not take the flag. Hints printed by commands already carry the flag; keep it on follow-ups. Without a store, commands act on the nearest local `openspec/` root. -**Project check:** These steps expect a project that already uses OpenSpec. Before the first step that writes anything (`new change`, `archive`, `sync specs`, or authoring an artifact file), confirm the project has a root: run `openspec list --json` (with `--store ` when a store is selected, since the store is then the root) and read `root`. A root object means the project is set up. `"root": null` means it is not - there is no `openspec/` directory here, and a write such as `openspec new change` would create one as a side effect. The command also exits non-zero, which is that answer rather than a broken CLI, so read the JSON instead of retrying or working around it. Then stop before writing and ask the user how to proceed: set this project up (`openspec init`), target a store they already have (`--store `), or drop OpenSpec for this request and help them directly. Wait for their answer. Do not run `openspec init` until they ask for it, do not hand-create `openspec/` files, and do not let a command create the root as a side effect. +**Project check:** These steps expect a project that already uses OpenSpec. Before the first step that writes anything (`new change`, `archive`, `sync specs`, or authoring an artifact file), confirm the project has a root: run `openspec list --json` (with `--store ` when a store is selected, since the store is then the root) and read `root`. A root object means the project is set up. `"root": null` means it is not - there is no `openspec/` directory here, and a write such as `openspec new change` would create one as a side effect. The command also exits non-zero, which is that answer rather than a broken CLI, so read the JSON instead of retrying or working around it. + +With no root, what happens next depends on how this workflow was reached: + +- **Auto-selected**: you chose this workflow yourself, without the user naming OpenSpec, naming this skill, or running its slash command. Stop using OpenSpec and answer the request normally, as you would with no OpenSpec installed. Do not ask them to set anything up and do not mention OpenSpec setup. +- **Explicit OpenSpec request**: the user named OpenSpec, named this skill, or ran its slash command. Stop before writing and ask how to proceed: set this project up (`openspec init`), target a store they already have (`--store `), or continue without OpenSpec for this request. Wait for their answer. + +In both branches, never create the root as a side effect: do not run `openspec init` until the user asks for it, do not hand-create `openspec/` files, and do not let a command create it. **Input**: The user's request should include a change name (kebab-case) OR a description of what they want to build. diff --git a/skills/openspec-sync-specs/SKILL.md b/skills/openspec-sync-specs/SKILL.md index 404bd2cda9..1a0be4b911 100644 --- a/skills/openspec-sync-specs/SKILL.md +++ b/skills/openspec-sync-specs/SKILL.md @@ -15,7 +15,14 @@ This is an **agent-driven** operation - you will read delta specs and directly e **Store selection:** If the user names a store (a store is a standalone OpenSpec repo registered on this machine) or the work lives in one, run `openspec store list --json` to discover registered store ids, then pass `--store ` on the commands that read or write specs and changes (`new change`, `status`, `instructions`, `list`, `show`, `validate`, `archive`, `doctor`, `context`, `schemas`, `view`). Once selected, treat `--store ` as sticky for the rest of the workflow. Every unscoped example of those commands below is shorthand: before running it, append the flag. For example, run `openspec status --change "" --json --store ""`, not the unscoped form shown below. Other commands do not take the flag. Hints printed by commands already carry the flag; keep it on follow-ups. Without a store, commands act on the nearest local `openspec/` root. -**Project check:** These steps expect a project that already uses OpenSpec. Before the first step that writes anything (`new change`, `archive`, `sync specs`, or authoring an artifact file), confirm the project has a root: run `openspec list --json` (with `--store ` when a store is selected, since the store is then the root) and read `root`. A root object means the project is set up. `"root": null` means it is not - there is no `openspec/` directory here, and a write such as `openspec new change` would create one as a side effect. The command also exits non-zero, which is that answer rather than a broken CLI, so read the JSON instead of retrying or working around it. Then stop before writing and ask the user how to proceed: set this project up (`openspec init`), target a store they already have (`--store `), or drop OpenSpec for this request and help them directly. Wait for their answer. Do not run `openspec init` until they ask for it, do not hand-create `openspec/` files, and do not let a command create the root as a side effect. +**Project check:** These steps expect a project that already uses OpenSpec. Before the first step that writes anything (`new change`, `archive`, `sync specs`, or authoring an artifact file), confirm the project has a root: run `openspec list --json` (with `--store ` when a store is selected, since the store is then the root) and read `root`. A root object means the project is set up. `"root": null` means it is not - there is no `openspec/` directory here, and a write such as `openspec new change` would create one as a side effect. The command also exits non-zero, which is that answer rather than a broken CLI, so read the JSON instead of retrying or working around it. + +With no root, what happens next depends on how this workflow was reached: + +- **Auto-selected**: you chose this workflow yourself, without the user naming OpenSpec, naming this skill, or running its slash command. Stop using OpenSpec and answer the request normally, as you would with no OpenSpec installed. Do not ask them to set anything up and do not mention OpenSpec setup. +- **Explicit OpenSpec request**: the user named OpenSpec, named this skill, or ran its slash command. Stop before writing and ask how to proceed: set this project up (`openspec init`), target a store they already have (`--store `), or continue without OpenSpec for this request. Wait for their answer. + +In both branches, never create the root as a side effect: do not run `openspec init` until the user asks for it, do not hand-create `openspec/` files, and do not let a command create it. `` is the spec directory relative to `specs/` (for example, `user-auth` or `identity/user-auth`). Preserve the full path from each delta spec when resolving its main spec. diff --git a/skills/openspec-update-change/SKILL.md b/skills/openspec-update-change/SKILL.md index d9b723b52a..b898ddff98 100644 --- a/skills/openspec-update-change/SKILL.md +++ b/skills/openspec-update-change/SKILL.md @@ -13,7 +13,14 @@ Revise a change's existing planning artifacts and keep them coherent. Never edit **Store selection:** If the user names a store (a store is a standalone OpenSpec repo registered on this machine) or the work lives in one, run `openspec store list --json` to discover registered store ids, then pass `--store ` on the commands that read or write specs and changes (`new change`, `status`, `instructions`, `list`, `show`, `validate`, `archive`, `doctor`, `context`, `schemas`, `view`). Once selected, treat `--store ` as sticky for the rest of the workflow. Every unscoped example of those commands below is shorthand: before running it, append the flag. For example, run `openspec status --change "" --json --store ""`, not the unscoped form shown below. Other commands do not take the flag. Hints printed by commands already carry the flag; keep it on follow-ups. Without a store, commands act on the nearest local `openspec/` root. -**Project check:** These steps expect a project that already uses OpenSpec. Before the first step that writes anything (`new change`, `archive`, `sync specs`, or authoring an artifact file), confirm the project has a root: run `openspec list --json` (with `--store ` when a store is selected, since the store is then the root) and read `root`. A root object means the project is set up. `"root": null` means it is not - there is no `openspec/` directory here, and a write such as `openspec new change` would create one as a side effect. The command also exits non-zero, which is that answer rather than a broken CLI, so read the JSON instead of retrying or working around it. Then stop before writing and ask the user how to proceed: set this project up (`openspec init`), target a store they already have (`--store `), or drop OpenSpec for this request and help them directly. Wait for their answer. Do not run `openspec init` until they ask for it, do not hand-create `openspec/` files, and do not let a command create the root as a side effect. +**Project check:** These steps expect a project that already uses OpenSpec. Before the first step that writes anything (`new change`, `archive`, `sync specs`, or authoring an artifact file), confirm the project has a root: run `openspec list --json` (with `--store ` when a store is selected, since the store is then the root) and read `root`. A root object means the project is set up. `"root": null` means it is not - there is no `openspec/` directory here, and a write such as `openspec new change` would create one as a side effect. The command also exits non-zero, which is that answer rather than a broken CLI, so read the JSON instead of retrying or working around it. + +With no root, what happens next depends on how this workflow was reached: + +- **Auto-selected**: you chose this workflow yourself, without the user naming OpenSpec, naming this skill, or running its slash command. Stop using OpenSpec and answer the request normally, as you would with no OpenSpec installed. Do not ask them to set anything up and do not mention OpenSpec setup. +- **Explicit OpenSpec request**: the user named OpenSpec, named this skill, or ran its slash command. Stop before writing and ask how to proceed: set this project up (`openspec init`), target a store they already have (`--store `), or continue without OpenSpec for this request. Wait for their answer. + +In both branches, never create the root as a side effect: do not run `openspec init` until the user asks for it, do not hand-create `openspec/` files, and do not let a command create it. **Input**: Optionally specify a change name. If omitted, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes. diff --git a/skills/openspec-verify-change/SKILL.md b/skills/openspec-verify-change/SKILL.md index b71e9f3003..288e4ed272 100644 --- a/skills/openspec-verify-change/SKILL.md +++ b/skills/openspec-verify-change/SKILL.md @@ -13,7 +13,14 @@ Verify that an implementation matches the change artifacts (specs, tasks, design **Store selection:** If the user names a store (a store is a standalone OpenSpec repo registered on this machine) or the work lives in one, run `openspec store list --json` to discover registered store ids, then pass `--store ` on the commands that read or write specs and changes (`new change`, `status`, `instructions`, `list`, `show`, `validate`, `archive`, `doctor`, `context`, `schemas`, `view`). Once selected, treat `--store ` as sticky for the rest of the workflow. Every unscoped example of those commands below is shorthand: before running it, append the flag. For example, run `openspec status --change "" --json --store ""`, not the unscoped form shown below. Other commands do not take the flag. Hints printed by commands already carry the flag; keep it on follow-ups. Without a store, commands act on the nearest local `openspec/` root. -**Project check:** These steps expect a project that already uses OpenSpec. Before the first step that writes anything (`new change`, `archive`, `sync specs`, or authoring an artifact file), confirm the project has a root: run `openspec list --json` (with `--store ` when a store is selected, since the store is then the root) and read `root`. A root object means the project is set up. `"root": null` means it is not - there is no `openspec/` directory here, and a write such as `openspec new change` would create one as a side effect. The command also exits non-zero, which is that answer rather than a broken CLI, so read the JSON instead of retrying or working around it. Then stop before writing and ask the user how to proceed: set this project up (`openspec init`), target a store they already have (`--store `), or drop OpenSpec for this request and help them directly. Wait for their answer. Do not run `openspec init` until they ask for it, do not hand-create `openspec/` files, and do not let a command create the root as a side effect. +**Project check:** These steps expect a project that already uses OpenSpec. Before the first step that writes anything (`new change`, `archive`, `sync specs`, or authoring an artifact file), confirm the project has a root: run `openspec list --json` (with `--store ` when a store is selected, since the store is then the root) and read `root`. A root object means the project is set up. `"root": null` means it is not - there is no `openspec/` directory here, and a write such as `openspec new change` would create one as a side effect. The command also exits non-zero, which is that answer rather than a broken CLI, so read the JSON instead of retrying or working around it. + +With no root, what happens next depends on how this workflow was reached: + +- **Auto-selected**: you chose this workflow yourself, without the user naming OpenSpec, naming this skill, or running its slash command. Stop using OpenSpec and answer the request normally, as you would with no OpenSpec installed. Do not ask them to set anything up and do not mention OpenSpec setup. +- **Explicit OpenSpec request**: the user named OpenSpec, named this skill, or ran its slash command. Stop before writing and ask how to proceed: set this project up (`openspec init`), target a store they already have (`--store `), or continue without OpenSpec for this request. Wait for their answer. + +In both branches, never create the root as a side effect: do not run `openspec init` until the user asks for it, do not hand-create `openspec/` files, and do not let a command create it. **Input**: Optionally specify a change name. If omitted, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes. diff --git a/src/core/templates/workflows/project-root.ts b/src/core/templates/workflows/project-root.ts index d4c92aaab4..84f79a8964 100644 --- a/src/core/templates/workflows/project-root.ts +++ b/src/core/templates/workflows/project-root.ts @@ -8,9 +8,28 @@ * directory the agent happens to be in. * * This guidance is interpolated into every workflow so the agent checks for a - * root before writing and hands the decision back to the user instead of - * setting the project up on its own. `openspec list --json` is the check - * because it refuses to fabricate an implicit root: it reports `root: null` - * both when nothing is set up and when only stores are registered. + * root before writing. `openspec list --json` is the check because it refuses + * to fabricate an implicit root: it reports `root: null` both when nothing is + * set up and when only stores are registered. + * + * What follows the check depends on how the workflow was reached, because the + * two cases want opposite things (#1645). A skill the model picked on its own + * in an unrelated repository must get out of the way: the user asked for help, + * not for OpenSpec, and answering with a setup menu is the reported bug. A + * user who named OpenSpec, named the skill, or ran its slash command is owed + * an answer about OpenSpec, so that case stops and asks. + * + * One text serves both surfaces. `apply-change` and `onboard` render a single + * body into the skill and the command alike, so a command-only variant would + * mean threading a surface flag through bodies that deliberately have none. + * The bullets scope themselves instead: a slash command is an explicit + * invocation, so its branch is the only one that can apply there. */ -export const PROJECT_ROOT_GUARD = `**Project check:** These steps expect a project that already uses OpenSpec. Before the first step that writes anything (\`new change\`, \`archive\`, \`sync specs\`, or authoring an artifact file), confirm the project has a root: run \`openspec list --json\` (with \`--store \` when a store is selected, since the store is then the root) and read \`root\`. A root object means the project is set up. \`"root": null\` means it is not - there is no \`openspec/\` directory here, and a write such as \`openspec new change\` would create one as a side effect. The command also exits non-zero, which is that answer rather than a broken CLI, so read the JSON instead of retrying or working around it. Then stop before writing and ask the user how to proceed: set this project up (\`openspec init\`), target a store they already have (\`--store \`), or drop OpenSpec for this request and help them directly. Wait for their answer. Do not run \`openspec init\` until they ask for it, do not hand-create \`openspec/\` files, and do not let a command create the root as a side effect.`; +export const PROJECT_ROOT_GUARD = `**Project check:** These steps expect a project that already uses OpenSpec. Before the first step that writes anything (\`new change\`, \`archive\`, \`sync specs\`, or authoring an artifact file), confirm the project has a root: run \`openspec list --json\` (with \`--store \` when a store is selected, since the store is then the root) and read \`root\`. A root object means the project is set up. \`"root": null\` means it is not - there is no \`openspec/\` directory here, and a write such as \`openspec new change\` would create one as a side effect. The command also exits non-zero, which is that answer rather than a broken CLI, so read the JSON instead of retrying or working around it. + +With no root, what happens next depends on how this workflow was reached: + +- **Auto-selected**: you chose this workflow yourself, without the user naming OpenSpec, naming this skill, or running its slash command. Stop using OpenSpec and answer the request normally, as you would with no OpenSpec installed. Do not ask them to set anything up and do not mention OpenSpec setup. +- **Explicit OpenSpec request**: the user named OpenSpec, named this skill, or ran its slash command. Stop before writing and ask how to proceed: set this project up (\`openspec init\`), target a store they already have (\`--store \`), or continue without OpenSpec for this request. Wait for their answer. + +In both branches, never create the root as a side effect: do not run \`openspec init\` until the user asks for it, do not hand-create \`openspec/\` files, and do not let a command create it.`; diff --git a/test/core/templates/project-root-guard.test.ts b/test/core/templates/project-root-guard.test.ts index f84cda0e88..c5b25e0ec8 100644 --- a/test/core/templates/project-root-guard.test.ts +++ b/test/core/templates/project-root-guard.test.ts @@ -19,6 +19,14 @@ import { * the guard has to live in the instructions themselves, in every workflow. */ describe('project root guard', () => { + /** One bullet of the no-root branch table, from its anchor to the next. */ + function branch(anchor: string): string { + const start = PROJECT_ROOT_GUARD.indexOf(anchor); + expect(start, `${anchor} is missing`).toBeGreaterThanOrEqual(0); + const next = PROJECT_ROOT_GUARD.indexOf('\n- ', start); + return PROJECT_ROOT_GUARD.slice(start, next === -1 ? undefined : next); + } + // Both surfaces, rendered exactly as they ship. function renderedBodies(): Array<[string, string]> { return [ @@ -68,14 +76,49 @@ describe('project root guard', () => { expect(PROJECT_ROOT_GUARD).toContain('also exits non-zero, which is that answer rather than a broken CLI'); }); - it('hands the decision to the user instead of setting the project up', () => { - expect(PROJECT_ROOT_GUARD).toContain('stop before writing and ask the user how to proceed'); - expect(PROJECT_ROOT_GUARD).toContain('drop OpenSpec for this request and help them directly'); - expect(PROJECT_ROOT_GUARD).toContain('Do not run `openspec init` until they ask for it'); + // #1645 asks for the workflow to get out of the way, not to interrogate the + // user: "if not exist it can go through the normal general propose not the + // openspec". So the two ways of arriving here get opposite answers, and both + // have to be pinned or the guard drifts back to one of them. + it('gets out of the way when it selected itself', () => { + const autoSelected = branch('**Auto-selected**'); + + expect(autoSelected).toContain('without the user naming OpenSpec'); + expect(autoSelected).toContain('answer the request normally'); + // The reported bug is being asked to choose a setup path for a project the + // user never said was an OpenSpec project. + expect(autoSelected).toContain('Do not ask them to set anything up'); + expect(autoSelected).not.toContain('openspec init'); + expect(autoSelected).not.toContain('--store '); + }); + + it('asks when the user named OpenSpec, this skill, or its command', () => { + const explicit = branch('**Explicit OpenSpec request**'); + + expect(explicit).toContain('named OpenSpec, named this skill, or ran its slash command'); + expect(explicit).toContain('Stop before writing and ask how to proceed'); + expect(explicit).toContain('`openspec init`'); + expect(explicit).toContain('`--store `'); + expect(explicit).toContain('continue without OpenSpec'); + expect(explicit).toContain('Wait for their answer'); + }); + + // A slash command is an explicit invocation, so the ask branch is the one + // that applies there. The guard ships whole into command files, which is what + // keeps that branch reachable from a command surface. + it('carries the explicit branch into every deployed opsx command', () => { + for (const [label, body] of renderedBodies()) { + if (!label.startsWith('command ')) continue; + expect(body, label).toContain('**Explicit OpenSpec request**'); + expect(body, label).toContain('Stop before writing and ask how to proceed'); + } + }); + + it('never lets any branch create the root as a side effect', () => { + expect(PROJECT_ROOT_GUARD).toContain('In both branches, never create the root as a side effect'); + expect(PROJECT_ROOT_GUARD).toContain('do not run `openspec init` until the user asks for it'); expect(PROJECT_ROOT_GUARD).toContain('do not hand-create `openspec/` files'); - expect(PROJECT_ROOT_GUARD).toContain( - 'do not let a command create the root as a side effect' - ); + expect(PROJECT_ROOT_GUARD).toContain('do not let a command create it'); }); // A guard printed after the workflow has already scaffolded a change is no diff --git a/test/core/templates/skill-templates-parity.test.ts b/test/core/templates/skill-templates-parity.test.ts index 9e56aa7a11..5f211aba26 100644 --- a/test/core/templates/skill-templates-parity.test.ts +++ b/test/core/templates/skill-templates-parity.test.ts @@ -38,46 +38,46 @@ import { import { STORE_SELECTION_GUIDANCE } from '../../../src/core/templates/workflows/store-selection.js'; const EXPECTED_FUNCTION_HASHES: Record = { - getExploreSkillTemplate: 'c201b946d28d690b42795a366e6a3d8763085386cf88abad5e4e100abcd66149', - getNewChangeSkillTemplate: 'dc2951d473aab531be6706705ce54971465f5943e66bfc31d3d7c6ee4ac80688', - getContinueChangeSkillTemplate: '84349c6a1bad53c2df7209cbe8d64318892beb83d1ea7aa302c8cc0097e5621a', - getApplyChangeSkillTemplate: '4aff0843213e93830f6fe2285d3395514e94864581c63e8d3ccf6dcf1b4d20fd', - getFfChangeSkillTemplate: '1973d7d1c7fbe210006a9f74a9dae6048f231773fc52fbd83363846401bcd5dd', - getSyncSpecsSkillTemplate: '44ef2db76bedaccf47f0cba3634cb51e4a820fb6946f34fa953355579b401ba2', - getOnboardSkillTemplate: 'bda1ebe2b6d316a8d17e2f7d4d2d2b259d0b587840b3e5e4793defd36ad84f99', - getOpsxExploreCommandTemplate: '76c7ed340902cd72a366857ec12331bb0039f4bc207d7033ef49d953a67c1feb', - getOpsxNewCommandTemplate: '5bdef93e9690b913f87c77b044256533865264822f72e65d31980ec064e03a7b', - getOpsxContinueCommandTemplate: '1893be6ab61be7b0addcf659acc324dca6d8e35ef2bb856b7049975ca97a039e', - getOpsxApplyCommandTemplate: 'c7a7979f655d2c09c883c3c99d9bb97cb6780aa578a96b3bbd2aa86a901ad7b8', - getOpsxFfCommandTemplate: '05033b1b32db175fcd9af41904681e73b7140623c3574b67ca46df5a264f5a49', - getArchiveChangeSkillTemplate: '7095dd5fc3863e13e05496891ea434d12fe90d3331eeb2fdc118e9c3af690fb9', - getBulkArchiveChangeSkillTemplate: '542ffe560de39bacc62ca85fd68b89f33b919f03bf23d51c8aebeb47f92fa171', - getOpsxSyncCommandTemplate: '3130fae9252e0a2a33c1476c650eec9da01ee0526b57d5434aee18eb44b1ed3b', - getVerifyChangeSkillTemplate: '7c86f4214ce8b1bf791d78b77c0a8123973369a7f9a396cdd8439cd40da4a8dd', - getOpsxArchiveCommandTemplate: 'c3698dfb3e8e82c385406263d905169d0bee72f188bb4839c1288feafba4db68', - getOpsxOnboardCommandTemplate: 'd747de9d729a8c86420f7deda1aa848ce51372916a03c87a51dd49701ab86d62', - getOpsxBulkArchiveCommandTemplate: '6ab10f00c182f52baebc5888f527ace0e8723f90e6950e927cea5a4945b2134b', - getOpsxVerifyCommandTemplate: '0e8dc5783b1a2fe6b7f385f73b26ff04dc9ca86c3e836e0d8c6d8c7ef37dde66', - getOpsxProposeSkillTemplate: '13038af568ee251adb775147a09dd3ee74069855b34ec5faebb2ecd4e412bbf4', - getOpsxProposeCommandTemplate: 'f42a9120d55d43f519f3a7635e25b86ed6b5632c8b62fe6997190080e9130753', + getExploreSkillTemplate: '1d7c2c7e73f1cb057ef00386afe2db14e7d0cd77ceebf13c9d49abb702542fab', + getNewChangeSkillTemplate: '22d6f37bd4407a159fc0e4df56535b9f8ec41db2bcf4b8838213c4583dab7508', + getContinueChangeSkillTemplate: 'b9d5e71fb1ea27fc0e886be778360d69ea5bad8ba2a00fd44c7b0aef6fb420ac', + getApplyChangeSkillTemplate: '8548ac443f5f9789589cdf57e15bd954b3fc0ba8285ffc846b0906225c8cdadc', + getFfChangeSkillTemplate: '54842368356da8da31fa0052c90c6bc326f894575761c3d627bc8a100d8752ad', + getSyncSpecsSkillTemplate: '5636031e95bb822f0b5f7bafda826b267e02826346be809510c3006021834ac8', + getOnboardSkillTemplate: '66da6835a1f7d38bec464f7eb4223ca56400745c02656ca9a09937d0cb0041f3', + getOpsxExploreCommandTemplate: 'b96740b619563945e87772deddc145f4e6f73cf14f4294b864e312bc90c4175d', + getOpsxNewCommandTemplate: '387bc291d002001fde8fbdf59309a0d2dc101b9538b1badb8603cb4828ad88f1', + getOpsxContinueCommandTemplate: '500e83a8af8a5e9af53a4b6b075135e09470b9df88c8fb99febec4a1679b67ce', + getOpsxApplyCommandTemplate: '98216b03669f1da6f03932cb00b60bad28af9a8688b54f7e3c76f85999fa2836', + getOpsxFfCommandTemplate: '0a488f0d11b950544577f5509fc5d35118ac9a7e483f65f8a3d523082a078966', + getArchiveChangeSkillTemplate: 'ba9916c7326c5c5c44e8230d778230156d2f14235ae9bc799ce3a869c53b28d1', + getBulkArchiveChangeSkillTemplate: '255fd206da93271e1ecc4cf042afb5d93141c88e342ff5eafce07749d3356df5', + getOpsxSyncCommandTemplate: 'e8b1314e1a72c9c8222fcfc2b553556d9ac58161ff3721f971dcfe2076eeca2a', + getVerifyChangeSkillTemplate: '860d997ab8e4a114f55ef40268729e8f5ea577f32b260fc31df72a13e3f99308', + getOpsxArchiveCommandTemplate: '4a335710a79d0e9fb913fafffebec6f318712533b1808d6677fa720208a64704', + getOpsxOnboardCommandTemplate: '74ec3635a3add7dc7ebeaebc1dddbdd94b13aa54bcb0dbbd62823bf84f7ab9dc', + getOpsxBulkArchiveCommandTemplate: '3f71ce2d80c011e115ddbaa97e2785719c7439448e7d3db96d683e0a1dd5065e', + getOpsxVerifyCommandTemplate: 'feb3c643f95d32b66d44e624426de0dd4ab84c77dea863314332bf58f1497254', + getOpsxProposeSkillTemplate: '1c161aee76732be64a2d410af0030bc77a359c6bddaf827430d49c12a4ceb0f1', + getOpsxProposeCommandTemplate: 'c8ac6cc7c6f753c3ff4094321caba3cdb02aa20b6152c565e032f516a76b061a', getFeedbackSkillTemplate: 'dabeb5e825b9349abc8156c3e7b8608f27987912a6d9bf47ef29addde6138133', - getUpdateChangeSkillTemplate: '90377a82f628e9891e6f72a78db68551e32896c404a55408628e44dbc80848ac', - getOpsxUpdateCommandTemplate: 'e391160a9e77a2cf13b5c346b571d127188741e8a748530e46a24734ac85c4f1', + getUpdateChangeSkillTemplate: '934a05834851e88f1f9d6e2a4d969832c29282feaabc3f96fe26bf7e50955a14', + getOpsxUpdateCommandTemplate: 'b3fafee176b50630000cc0235ccb14aa04da68cdbc6747548b5f739043493b1f', }; const EXPECTED_GENERATED_SKILL_CONTENT_HASHES: Record = { - 'openspec-explore': '27bbe34bc9254d76210a06d540ff6b2caa4c4962e9e62c6cdc0cb49293293c52', - 'openspec-new-change': '651028f7a0f72e0505404798f48fd0f34efbf773bf25bcdfc627b2bea11a7790', - 'openspec-continue-change': 'a1a336bfd50539a00c279f4176e88a9a311b772908479788b1d94a69e70d3b42', - 'openspec-apply-change': '5cc0e8d5be257227c5bad9eac771ce2527ed555f5152f0618b958d3ed0a9523f', - 'openspec-ff-change': '890162ee129104b727949b3345acf229865f59c8d3107e0299ef69631a9c5b17', - 'openspec-sync-specs': 'f833aa14447079f7465fc8ecfd4766de123d1e9675c86dab8490554d965e9459', - 'openspec-archive-change': '295d50f79ef186999ae088857479834550cc2574253d21580b87937be93326a5', - 'openspec-bulk-archive-change': '4f38ad54d54caea1afcb94468192f5bdd8905f01e32d0b763796252ca9547965', - 'openspec-verify-change': 'd7f71e25be20839d01cfe2b3e9c0a88aa3a997aed0e2b85f1325d079a309d587', - 'openspec-onboard': '5dd2569c661d5032e1e89f5e5b06da7a099511e600d12b4e89bb1d49b9591968', - 'openspec-propose': '18f1c017a4c0ce1d8ce589522bed33f5bc6d915798358574b4c3116207ecf982', - 'openspec-update-change': 'b3dfd0dbdc05e77b8e385b5c3a1f67fd9182ace7201fe7bcac947320d4abc5c4', + 'openspec-explore': '42752221a911ea3420e34c9ca044beec4c572b002983518975407818bf3af26a', + 'openspec-new-change': '9fd65ab06abaa5da56ebdb596adcac45015cae8d4818ab7b9c622f7e0556d6e0', + 'openspec-continue-change': '98a5b8f2af2cec6bec3e8da9c410603dd5df0d90b1c9caf633abd4e86881bf25', + 'openspec-apply-change': '27d857f848714372a9e1012082c42052d59093803dc7c858333a945e15429309', + 'openspec-ff-change': '6d3cc61eadc6b738c8ed5bf2f6165df347950431e8da97895934aa87f4810b9e', + 'openspec-sync-specs': '2d78ec7675715959841ea7242960f202c112d23ecbaaf12c2740525d69904830', + 'openspec-archive-change': '61ac661788c84193ec0b1478598ccc743d741baf30fcc8c6926e7ad4814672e3', + 'openspec-bulk-archive-change': '09d120c08ec1726923874efb0308b69196ad054108159c0144a7f631bc0e8742', + 'openspec-verify-change': 'f8bbf1243eb5dcde3b9d909bdb1c83c7d4ef9be0d873cb1d88bf81835faffdfa', + 'openspec-onboard': '0045322427a1be84f0d9c7caa7b2668f2a913c86bb1ac229c9ffd8ce0b7bf8c2', + 'openspec-propose': 'f0aa8a5e27ce2afbb1a96bdb3786ddece12e3f5d6f306d246d1a3d55e05e041c', + 'openspec-update-change': 'affc5639381b220113094bc87dd932c118c8cf74a4abb9fe40049fce1f85a8c7', }; // Intentionally excludes getFeedbackSkillTemplate: this list only models templates From a84dba5d9a3fdeeb95b4b55db913d809b8fbbb4e Mon Sep 17 00:00:00 2001 From: Clay Good Date: Tue, 15 Sep 2026 07:58:42 -0500 Subject: [PATCH 13/15] 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 --- .changeset/guard-uninitialized-projects.md | 2 +- docs-lab/reference/cli.md | 13 +++- docs-lab/reference/skills.md | 2 +- skills/openspec-apply-change/SKILL.md | 4 +- skills/openspec-archive-change/SKILL.md | 4 +- skills/openspec-bulk-archive-change/SKILL.md | 4 +- skills/openspec-continue-change/SKILL.md | 4 +- skills/openspec-explore/SKILL.md | 4 +- skills/openspec-ff-change/SKILL.md | 4 +- skills/openspec-new-change/SKILL.md | 4 +- skills/openspec-onboard/SKILL.md | 4 +- skills/openspec-propose/SKILL.md | 6 +- skills/openspec-sync-specs/SKILL.md | 4 +- skills/openspec-update-change/SKILL.md | 4 +- skills/openspec-verify-change/SKILL.md | 4 +- src/core/templates/workflows/project-root.ts | 4 +- src/core/templates/workflows/propose.ts | 4 +- .../core/templates/project-root-guard.test.ts | 16 +++++ .../templates/skill-templates-parity.test.ts | 72 +++++++++---------- 19 files changed, 107 insertions(+), 56 deletions(-) diff --git a/.changeset/guard-uninitialized-projects.md b/.changeset/guard-uninitialized-projects.md index cd66817bfa..0f2dd0ef13 100644 --- a/.changeset/guard-uninitialized-projects.md +++ b/.changeset/guard-uninitialized-projects.md @@ -4,4 +4,4 @@ ### Bug Fixes -- Generated skills and commands no longer adopt a project that never ran `openspec init`. Every workflow now checks `root` from `openspec list --json` before its first write: `"root": null` means the project is not set up, so the agent stops and asks whether to initialize it, target a store, or handle the request without OpenSpec, instead of letting `openspec new change` create `openspec/` in the current directory as a side effect. Skill descriptions now name OpenSpec so hosts stop offering these workflows in unrelated repositories. `openspec new change` also says when it had to create the root itself, so a directory that was never set up no longer picks up an `openspec/` directory in silence (human output only; `--json` is unchanged). +- Generated skills and commands no longer adopt a project that never ran `openspec init`. Every workflow now checks `root` from `openspec list --json` before its first write, and `"root": null` means the project is not set up. What happens next depends on how the workflow was reached. A skill the agent picked on its own drops OpenSpec and answers the request normally, without asking about setup. A workflow the user asked for by name, or ran as a slash command, stops and asks whether to initialize the project, target a store, or handle the request without OpenSpec. A project whose `openspec/config.yaml` names a store this machine has not registered is not mistaken for an uninitialized one: the workflow stops and shows the store error. Neither path lets `openspec new change` create `openspec/` in the current directory as a side effect. Skill descriptions now name OpenSpec so hosts stop offering these workflows in unrelated repositories. `openspec new change` also says when it had to create the root itself, so a directory that was never set up no longer picks up an `openspec/` directory in silence (human output only; `--json` is unchanged). diff --git a/docs-lab/reference/cli.md b/docs-lab/reference/cli.md index 2c3760852f..b89ca062e3 100644 --- a/docs-lab/reference/cli.md +++ b/docs-lab/reference/cli.md @@ -1010,9 +1010,9 @@ Note: no OpenSpec root was found here, so one was created at openspec/. Run `openspec init` to finish setting this project up, or delete that directory if you meant a different project. ``` -The notice goes to stdout with the rest of the human output, and never appears with `--json`. In JSON, `root.source` reads `implicit` for the same case. +The notice goes to stdout with the rest of the human output, and never appears with `--json`. -With `--json`: +With `--json`, in a project that already has `openspec/`: ```json { @@ -1029,6 +1029,15 @@ With `--json`: } ``` +When no `openspec/` directory was found and `new change` created one, the JSON has the same shape. `root.path` is the directory you ran it from, and `root.source` reads `implicit`: + +```json +"root": { + "path": "/Users/you/projects/my-app", + "source": "implicit" +} +``` + **Exit codes** - `0`: change created. diff --git a/docs-lab/reference/skills.md b/docs-lab/reference/skills.md index ab6474cab9..3e7cfd65aa 100644 --- a/docs-lab/reference/skills.md +++ b/docs-lab/reference/skills.md @@ -44,7 +44,7 @@ Every skill expects a project that already uses OpenSpec. Before its first step - **Auto-selected**: your agent picked the skill on its own, without you naming OpenSpec. It drops OpenSpec and answers your request normally, the way it would with OpenSpec not installed. - **Explicit OpenSpec request**: you named OpenSpec, named the skill, or ran its command. It stops before writing and asks how to proceed: run `openspec init` here, target a store with `--store `, or continue without OpenSpec. It waits for your answer. -Commands are always the second case. No skill creates an `openspec/` directory on its own in either case. The entries below describe what each skill does once a root is in place. +Commands are always the second case. A project whose `openspec/config.yaml` names a store this machine has not registered is not treated as uninitialized: the skill stops and shows the store error with its fix. No skill creates an `openspec/` directory on its own in either case. The entries below describe what each skill does once a root is in place. | Skill | Job | Type | |---|---|---| diff --git a/skills/openspec-apply-change/SKILL.md b/skills/openspec-apply-change/SKILL.md index 1599cb2195..9b1618bc34 100644 --- a/skills/openspec-apply-change/SKILL.md +++ b/skills/openspec-apply-change/SKILL.md @@ -15,7 +15,9 @@ Implement tasks from an OpenSpec change. **Project check:** These steps expect a project that already uses OpenSpec. Before the first step that writes anything (`new change`, `archive`, `sync specs`, or authoring an artifact file), confirm the project has a root: run `openspec list --json` (with `--store ` when a store is selected, since the store is then the root) and read `root`. A root object means the project is set up. `"root": null` means it is not - there is no `openspec/` directory here, and a write such as `openspec new change` would create one as a side effect. The command also exits non-zero, which is that answer rather than a broken CLI, so read the JSON instead of retrying or working around it. -With no root, what happens next depends on how this workflow was reached: +One `"root": null` is not about setup: when a `status` error message starts with `Declared in` and names this project's `openspec/config.yaml`, the project does use OpenSpec through a store this machine has not registered. Do not treat it as uninitialized and skip the branches below: stop before writing and show the user that error's `message` and `fix`. + +Otherwise, with no root, what happens next depends on how this workflow was reached: - **Auto-selected**: you chose this workflow yourself, without the user naming OpenSpec, naming this skill, or running its slash command. Stop using OpenSpec and answer the request normally, as you would with no OpenSpec installed. Do not ask them to set anything up and do not mention OpenSpec setup. - **Explicit OpenSpec request**: the user named OpenSpec, named this skill, or ran its slash command. Stop before writing and ask how to proceed: set this project up (`openspec init`), target a store they already have (`--store `), or continue without OpenSpec for this request. Wait for their answer. diff --git a/skills/openspec-archive-change/SKILL.md b/skills/openspec-archive-change/SKILL.md index 3a244a0c0c..8bd1a25cf6 100644 --- a/skills/openspec-archive-change/SKILL.md +++ b/skills/openspec-archive-change/SKILL.md @@ -15,7 +15,9 @@ Archive a completed change in the experimental workflow. **Project check:** These steps expect a project that already uses OpenSpec. Before the first step that writes anything (`new change`, `archive`, `sync specs`, or authoring an artifact file), confirm the project has a root: run `openspec list --json` (with `--store ` when a store is selected, since the store is then the root) and read `root`. A root object means the project is set up. `"root": null` means it is not - there is no `openspec/` directory here, and a write such as `openspec new change` would create one as a side effect. The command also exits non-zero, which is that answer rather than a broken CLI, so read the JSON instead of retrying or working around it. -With no root, what happens next depends on how this workflow was reached: +One `"root": null` is not about setup: when a `status` error message starts with `Declared in` and names this project's `openspec/config.yaml`, the project does use OpenSpec through a store this machine has not registered. Do not treat it as uninitialized and skip the branches below: stop before writing and show the user that error's `message` and `fix`. + +Otherwise, with no root, what happens next depends on how this workflow was reached: - **Auto-selected**: you chose this workflow yourself, without the user naming OpenSpec, naming this skill, or running its slash command. Stop using OpenSpec and answer the request normally, as you would with no OpenSpec installed. Do not ask them to set anything up and do not mention OpenSpec setup. - **Explicit OpenSpec request**: the user named OpenSpec, named this skill, or ran its slash command. Stop before writing and ask how to proceed: set this project up (`openspec init`), target a store they already have (`--store `), or continue without OpenSpec for this request. Wait for their answer. diff --git a/skills/openspec-bulk-archive-change/SKILL.md b/skills/openspec-bulk-archive-change/SKILL.md index 8d74167dae..79488a5ed2 100644 --- a/skills/openspec-bulk-archive-change/SKILL.md +++ b/skills/openspec-bulk-archive-change/SKILL.md @@ -17,7 +17,9 @@ This skill allows you to batch-archive changes, handling spec conflicts intellig **Project check:** These steps expect a project that already uses OpenSpec. Before the first step that writes anything (`new change`, `archive`, `sync specs`, or authoring an artifact file), confirm the project has a root: run `openspec list --json` (with `--store ` when a store is selected, since the store is then the root) and read `root`. A root object means the project is set up. `"root": null` means it is not - there is no `openspec/` directory here, and a write such as `openspec new change` would create one as a side effect. The command also exits non-zero, which is that answer rather than a broken CLI, so read the JSON instead of retrying or working around it. -With no root, what happens next depends on how this workflow was reached: +One `"root": null` is not about setup: when a `status` error message starts with `Declared in` and names this project's `openspec/config.yaml`, the project does use OpenSpec through a store this machine has not registered. Do not treat it as uninitialized and skip the branches below: stop before writing and show the user that error's `message` and `fix`. + +Otherwise, with no root, what happens next depends on how this workflow was reached: - **Auto-selected**: you chose this workflow yourself, without the user naming OpenSpec, naming this skill, or running its slash command. Stop using OpenSpec and answer the request normally, as you would with no OpenSpec installed. Do not ask them to set anything up and do not mention OpenSpec setup. - **Explicit OpenSpec request**: the user named OpenSpec, named this skill, or ran its slash command. Stop before writing and ask how to proceed: set this project up (`openspec init`), target a store they already have (`--store `), or continue without OpenSpec for this request. Wait for their answer. diff --git a/skills/openspec-continue-change/SKILL.md b/skills/openspec-continue-change/SKILL.md index 1fce8351fe..4d4ae59421 100644 --- a/skills/openspec-continue-change/SKILL.md +++ b/skills/openspec-continue-change/SKILL.md @@ -15,7 +15,9 @@ Continue working on a change by creating the next artifact. **Project check:** These steps expect a project that already uses OpenSpec. Before the first step that writes anything (`new change`, `archive`, `sync specs`, or authoring an artifact file), confirm the project has a root: run `openspec list --json` (with `--store ` when a store is selected, since the store is then the root) and read `root`. A root object means the project is set up. `"root": null` means it is not - there is no `openspec/` directory here, and a write such as `openspec new change` would create one as a side effect. The command also exits non-zero, which is that answer rather than a broken CLI, so read the JSON instead of retrying or working around it. -With no root, what happens next depends on how this workflow was reached: +One `"root": null` is not about setup: when a `status` error message starts with `Declared in` and names this project's `openspec/config.yaml`, the project does use OpenSpec through a store this machine has not registered. Do not treat it as uninitialized and skip the branches below: stop before writing and show the user that error's `message` and `fix`. + +Otherwise, with no root, what happens next depends on how this workflow was reached: - **Auto-selected**: you chose this workflow yourself, without the user naming OpenSpec, naming this skill, or running its slash command. Stop using OpenSpec and answer the request normally, as you would with no OpenSpec installed. Do not ask them to set anything up and do not mention OpenSpec setup. - **Explicit OpenSpec request**: the user named OpenSpec, named this skill, or ran its slash command. Stop before writing and ask how to proceed: set this project up (`openspec init`), target a store they already have (`--store `), or continue without OpenSpec for this request. Wait for their answer. diff --git a/skills/openspec-explore/SKILL.md b/skills/openspec-explore/SKILL.md index e40c8c0376..a3910ffae6 100644 --- a/skills/openspec-explore/SKILL.md +++ b/skills/openspec-explore/SKILL.md @@ -19,7 +19,9 @@ Enter explore mode. Think deeply. Visualize freely. Follow the conversation wher **Project check:** These steps expect a project that already uses OpenSpec. Before the first step that writes anything (`new change`, `archive`, `sync specs`, or authoring an artifact file), confirm the project has a root: run `openspec list --json` (with `--store ` when a store is selected, since the store is then the root) and read `root`. A root object means the project is set up. `"root": null` means it is not - there is no `openspec/` directory here, and a write such as `openspec new change` would create one as a side effect. The command also exits non-zero, which is that answer rather than a broken CLI, so read the JSON instead of retrying or working around it. -With no root, what happens next depends on how this workflow was reached: +One `"root": null` is not about setup: when a `status` error message starts with `Declared in` and names this project's `openspec/config.yaml`, the project does use OpenSpec through a store this machine has not registered. Do not treat it as uninitialized and skip the branches below: stop before writing and show the user that error's `message` and `fix`. + +Otherwise, with no root, what happens next depends on how this workflow was reached: - **Auto-selected**: you chose this workflow yourself, without the user naming OpenSpec, naming this skill, or running its slash command. Stop using OpenSpec and answer the request normally, as you would with no OpenSpec installed. Do not ask them to set anything up and do not mention OpenSpec setup. - **Explicit OpenSpec request**: the user named OpenSpec, named this skill, or ran its slash command. Stop before writing and ask how to proceed: set this project up (`openspec init`), target a store they already have (`--store `), or continue without OpenSpec for this request. Wait for their answer. diff --git a/skills/openspec-ff-change/SKILL.md b/skills/openspec-ff-change/SKILL.md index 815c1bfb6e..ebab88af9b 100644 --- a/skills/openspec-ff-change/SKILL.md +++ b/skills/openspec-ff-change/SKILL.md @@ -15,7 +15,9 @@ Fast-forward through artifact creation - generate everything needed to start imp **Project check:** These steps expect a project that already uses OpenSpec. Before the first step that writes anything (`new change`, `archive`, `sync specs`, or authoring an artifact file), confirm the project has a root: run `openspec list --json` (with `--store ` when a store is selected, since the store is then the root) and read `root`. A root object means the project is set up. `"root": null` means it is not - there is no `openspec/` directory here, and a write such as `openspec new change` would create one as a side effect. The command also exits non-zero, which is that answer rather than a broken CLI, so read the JSON instead of retrying or working around it. -With no root, what happens next depends on how this workflow was reached: +One `"root": null` is not about setup: when a `status` error message starts with `Declared in` and names this project's `openspec/config.yaml`, the project does use OpenSpec through a store this machine has not registered. Do not treat it as uninitialized and skip the branches below: stop before writing and show the user that error's `message` and `fix`. + +Otherwise, with no root, what happens next depends on how this workflow was reached: - **Auto-selected**: you chose this workflow yourself, without the user naming OpenSpec, naming this skill, or running its slash command. Stop using OpenSpec and answer the request normally, as you would with no OpenSpec installed. Do not ask them to set anything up and do not mention OpenSpec setup. - **Explicit OpenSpec request**: the user named OpenSpec, named this skill, or ran its slash command. Stop before writing and ask how to proceed: set this project up (`openspec init`), target a store they already have (`--store `), or continue without OpenSpec for this request. Wait for their answer. diff --git a/skills/openspec-new-change/SKILL.md b/skills/openspec-new-change/SKILL.md index be1305e992..42eded7492 100644 --- a/skills/openspec-new-change/SKILL.md +++ b/skills/openspec-new-change/SKILL.md @@ -15,7 +15,9 @@ Start a new change using the experimental artifact-driven approach. **Project check:** These steps expect a project that already uses OpenSpec. Before the first step that writes anything (`new change`, `archive`, `sync specs`, or authoring an artifact file), confirm the project has a root: run `openspec list --json` (with `--store ` when a store is selected, since the store is then the root) and read `root`. A root object means the project is set up. `"root": null` means it is not - there is no `openspec/` directory here, and a write such as `openspec new change` would create one as a side effect. The command also exits non-zero, which is that answer rather than a broken CLI, so read the JSON instead of retrying or working around it. -With no root, what happens next depends on how this workflow was reached: +One `"root": null` is not about setup: when a `status` error message starts with `Declared in` and names this project's `openspec/config.yaml`, the project does use OpenSpec through a store this machine has not registered. Do not treat it as uninitialized and skip the branches below: stop before writing and show the user that error's `message` and `fix`. + +Otherwise, with no root, what happens next depends on how this workflow was reached: - **Auto-selected**: you chose this workflow yourself, without the user naming OpenSpec, naming this skill, or running its slash command. Stop using OpenSpec and answer the request normally, as you would with no OpenSpec installed. Do not ask them to set anything up and do not mention OpenSpec setup. - **Explicit OpenSpec request**: the user named OpenSpec, named this skill, or ran its slash command. Stop before writing and ask how to proceed: set this project up (`openspec init`), target a store they already have (`--store `), or continue without OpenSpec for this request. Wait for their answer. diff --git a/skills/openspec-onboard/SKILL.md b/skills/openspec-onboard/SKILL.md index fc0122a17a..05bda93b06 100644 --- a/skills/openspec-onboard/SKILL.md +++ b/skills/openspec-onboard/SKILL.md @@ -15,7 +15,9 @@ Guide the user through their first complete OpenSpec workflow cycle. This is a t **Project check:** These steps expect a project that already uses OpenSpec. Before the first step that writes anything (`new change`, `archive`, `sync specs`, or authoring an artifact file), confirm the project has a root: run `openspec list --json` (with `--store ` when a store is selected, since the store is then the root) and read `root`. A root object means the project is set up. `"root": null` means it is not - there is no `openspec/` directory here, and a write such as `openspec new change` would create one as a side effect. The command also exits non-zero, which is that answer rather than a broken CLI, so read the JSON instead of retrying or working around it. -With no root, what happens next depends on how this workflow was reached: +One `"root": null` is not about setup: when a `status` error message starts with `Declared in` and names this project's `openspec/config.yaml`, the project does use OpenSpec through a store this machine has not registered. Do not treat it as uninitialized and skip the branches below: stop before writing and show the user that error's `message` and `fix`. + +Otherwise, with no root, what happens next depends on how this workflow was reached: - **Auto-selected**: you chose this workflow yourself, without the user naming OpenSpec, naming this skill, or running its slash command. Stop using OpenSpec and answer the request normally, as you would with no OpenSpec installed. Do not ask them to set anything up and do not mention OpenSpec setup. - **Explicit OpenSpec request**: the user named OpenSpec, named this skill, or ran its slash command. Stop before writing and ask how to proceed: set this project up (`openspec init`), target a store they already have (`--store `), or continue without OpenSpec for this request. Wait for their answer. diff --git a/skills/openspec-propose/SKILL.md b/skills/openspec-propose/SKILL.md index dae11a4d58..c012cff58d 100644 --- a/skills/openspec-propose/SKILL.md +++ b/skills/openspec-propose/SKILL.md @@ -29,7 +29,9 @@ When the user is ready to implement, they must start the apply workflow explicit **Project check:** These steps expect a project that already uses OpenSpec. Before the first step that writes anything (`new change`, `archive`, `sync specs`, or authoring an artifact file), confirm the project has a root: run `openspec list --json` (with `--store ` when a store is selected, since the store is then the root) and read `root`. A root object means the project is set up. `"root": null` means it is not - there is no `openspec/` directory here, and a write such as `openspec new change` would create one as a side effect. The command also exits non-zero, which is that answer rather than a broken CLI, so read the JSON instead of retrying or working around it. -With no root, what happens next depends on how this workflow was reached: +One `"root": null` is not about setup: when a `status` error message starts with `Declared in` and names this project's `openspec/config.yaml`, the project does use OpenSpec through a store this machine has not registered. Do not treat it as uninitialized and skip the branches below: stop before writing and show the user that error's `message` and `fix`. + +Otherwise, with no root, what happens next depends on how this workflow was reached: - **Auto-selected**: you chose this workflow yourself, without the user naming OpenSpec, naming this skill, or running its slash command. Stop using OpenSpec and answer the request normally, as you would with no OpenSpec installed. Do not ask them to set anything up and do not mention OpenSpec setup. - **Explicit OpenSpec request**: the user named OpenSpec, named this skill, or ran its slash command. Stop before writing and ask how to proceed: set this project up (`openspec init`), target a store they already have (`--store `), or continue without OpenSpec for this request. Wait for their answer. @@ -53,7 +55,7 @@ In both branches, never create the root as a side effect: do not run `openspec i 2. **Load project context** - Run `openspec context --json` from the current working directory (or `openspec context --json --store ""` when a registered store was explicitly selected). Use the returned `root.path` as the authoritative OpenSpec root. If context reports `no_openspec_root`, stop without creating or changing any files. Offer `openspec init` and wait for the user to request initialization. Do not initialize automatically or run `openspec new change`. After initialization, rerun this context check before continuing. For any other context failure, stop and report the error; do not fall back to the current directory or run later OpenSpec commands without the selected store. + Run `openspec context --json` from the current working directory (or `openspec context --json --store ""` when a registered store was explicitly selected). Use the returned `root.path` as the authoritative OpenSpec root. If context reports `no_openspec_root`, stop without creating or changing any files and follow the **Project check** above for how this workflow was reached. Offer `openspec init` only for an explicit OpenSpec request, and wait for the user to request initialization. Do not initialize automatically or run `openspec new change`. After initialization, rerun this context check before continuing. For any other context failure, stop and report the error; do not fall back to the current directory or run later OpenSpec commands without the selected store. Only when context returns a resolved `root.path`, read `/openspec/config.yaml`. Use `config.yml` only when `config.yaml` does not exist. If neither file exists, continue without project context. Do not fall back to `config.yml` if `config.yaml` is unreadable or invalid. diff --git a/skills/openspec-sync-specs/SKILL.md b/skills/openspec-sync-specs/SKILL.md index 1a0be4b911..ce994b4d35 100644 --- a/skills/openspec-sync-specs/SKILL.md +++ b/skills/openspec-sync-specs/SKILL.md @@ -17,7 +17,9 @@ This is an **agent-driven** operation - you will read delta specs and directly e **Project check:** These steps expect a project that already uses OpenSpec. Before the first step that writes anything (`new change`, `archive`, `sync specs`, or authoring an artifact file), confirm the project has a root: run `openspec list --json` (with `--store ` when a store is selected, since the store is then the root) and read `root`. A root object means the project is set up. `"root": null` means it is not - there is no `openspec/` directory here, and a write such as `openspec new change` would create one as a side effect. The command also exits non-zero, which is that answer rather than a broken CLI, so read the JSON instead of retrying or working around it. -With no root, what happens next depends on how this workflow was reached: +One `"root": null` is not about setup: when a `status` error message starts with `Declared in` and names this project's `openspec/config.yaml`, the project does use OpenSpec through a store this machine has not registered. Do not treat it as uninitialized and skip the branches below: stop before writing and show the user that error's `message` and `fix`. + +Otherwise, with no root, what happens next depends on how this workflow was reached: - **Auto-selected**: you chose this workflow yourself, without the user naming OpenSpec, naming this skill, or running its slash command. Stop using OpenSpec and answer the request normally, as you would with no OpenSpec installed. Do not ask them to set anything up and do not mention OpenSpec setup. - **Explicit OpenSpec request**: the user named OpenSpec, named this skill, or ran its slash command. Stop before writing and ask how to proceed: set this project up (`openspec init`), target a store they already have (`--store `), or continue without OpenSpec for this request. Wait for their answer. diff --git a/skills/openspec-update-change/SKILL.md b/skills/openspec-update-change/SKILL.md index b898ddff98..b9fe0978e0 100644 --- a/skills/openspec-update-change/SKILL.md +++ b/skills/openspec-update-change/SKILL.md @@ -15,7 +15,9 @@ Revise a change's existing planning artifacts and keep them coherent. Never edit **Project check:** These steps expect a project that already uses OpenSpec. Before the first step that writes anything (`new change`, `archive`, `sync specs`, or authoring an artifact file), confirm the project has a root: run `openspec list --json` (with `--store ` when a store is selected, since the store is then the root) and read `root`. A root object means the project is set up. `"root": null` means it is not - there is no `openspec/` directory here, and a write such as `openspec new change` would create one as a side effect. The command also exits non-zero, which is that answer rather than a broken CLI, so read the JSON instead of retrying or working around it. -With no root, what happens next depends on how this workflow was reached: +One `"root": null` is not about setup: when a `status` error message starts with `Declared in` and names this project's `openspec/config.yaml`, the project does use OpenSpec through a store this machine has not registered. Do not treat it as uninitialized and skip the branches below: stop before writing and show the user that error's `message` and `fix`. + +Otherwise, with no root, what happens next depends on how this workflow was reached: - **Auto-selected**: you chose this workflow yourself, without the user naming OpenSpec, naming this skill, or running its slash command. Stop using OpenSpec and answer the request normally, as you would with no OpenSpec installed. Do not ask them to set anything up and do not mention OpenSpec setup. - **Explicit OpenSpec request**: the user named OpenSpec, named this skill, or ran its slash command. Stop before writing and ask how to proceed: set this project up (`openspec init`), target a store they already have (`--store `), or continue without OpenSpec for this request. Wait for their answer. diff --git a/skills/openspec-verify-change/SKILL.md b/skills/openspec-verify-change/SKILL.md index 288e4ed272..99abbc1107 100644 --- a/skills/openspec-verify-change/SKILL.md +++ b/skills/openspec-verify-change/SKILL.md @@ -15,7 +15,9 @@ Verify that an implementation matches the change artifacts (specs, tasks, design **Project check:** These steps expect a project that already uses OpenSpec. Before the first step that writes anything (`new change`, `archive`, `sync specs`, or authoring an artifact file), confirm the project has a root: run `openspec list --json` (with `--store ` when a store is selected, since the store is then the root) and read `root`. A root object means the project is set up. `"root": null` means it is not - there is no `openspec/` directory here, and a write such as `openspec new change` would create one as a side effect. The command also exits non-zero, which is that answer rather than a broken CLI, so read the JSON instead of retrying or working around it. -With no root, what happens next depends on how this workflow was reached: +One `"root": null` is not about setup: when a `status` error message starts with `Declared in` and names this project's `openspec/config.yaml`, the project does use OpenSpec through a store this machine has not registered. Do not treat it as uninitialized and skip the branches below: stop before writing and show the user that error's `message` and `fix`. + +Otherwise, with no root, what happens next depends on how this workflow was reached: - **Auto-selected**: you chose this workflow yourself, without the user naming OpenSpec, naming this skill, or running its slash command. Stop using OpenSpec and answer the request normally, as you would with no OpenSpec installed. Do not ask them to set anything up and do not mention OpenSpec setup. - **Explicit OpenSpec request**: the user named OpenSpec, named this skill, or ran its slash command. Stop before writing and ask how to proceed: set this project up (`openspec init`), target a store they already have (`--store `), or continue without OpenSpec for this request. Wait for their answer. diff --git a/src/core/templates/workflows/project-root.ts b/src/core/templates/workflows/project-root.ts index 84f79a8964..c6adafe9b7 100644 --- a/src/core/templates/workflows/project-root.ts +++ b/src/core/templates/workflows/project-root.ts @@ -27,7 +27,9 @@ */ export const PROJECT_ROOT_GUARD = `**Project check:** These steps expect a project that already uses OpenSpec. Before the first step that writes anything (\`new change\`, \`archive\`, \`sync specs\`, or authoring an artifact file), confirm the project has a root: run \`openspec list --json\` (with \`--store \` when a store is selected, since the store is then the root) and read \`root\`. A root object means the project is set up. \`"root": null\` means it is not - there is no \`openspec/\` directory here, and a write such as \`openspec new change\` would create one as a side effect. The command also exits non-zero, which is that answer rather than a broken CLI, so read the JSON instead of retrying or working around it. -With no root, what happens next depends on how this workflow was reached: +One \`"root": null\` is not about setup: when a \`status\` error message starts with \`Declared in\` and names this project's \`openspec/config.yaml\`, the project does use OpenSpec through a store this machine has not registered. Do not treat it as uninitialized and skip the branches below: stop before writing and show the user that error's \`message\` and \`fix\`. + +Otherwise, with no root, what happens next depends on how this workflow was reached: - **Auto-selected**: you chose this workflow yourself, without the user naming OpenSpec, naming this skill, or running its slash command. Stop using OpenSpec and answer the request normally, as you would with no OpenSpec installed. Do not ask them to set anything up and do not mention OpenSpec setup. - **Explicit OpenSpec request**: the user named OpenSpec, named this skill, or ran its slash command. Stop before writing and ask how to proceed: set this project up (\`openspec init\`), target a store they already have (\`--store \`), or continue without OpenSpec for this request. Wait for their answer. diff --git a/src/core/templates/workflows/propose.ts b/src/core/templates/workflows/propose.ts index 2b0c5d3294..61af81d9a9 100644 --- a/src/core/templates/workflows/propose.ts +++ b/src/core/templates/workflows/propose.ts @@ -49,7 +49,7 @@ ${PROJECT_ROOT_GUARD} 2. **Load project context** - Run \`openspec context --json\` from the current working directory (or \`openspec context --json --store ""\` when a registered store was explicitly selected). Use the returned \`root.path\` as the authoritative OpenSpec root. If context reports \`no_openspec_root\`, stop without creating or changing any files. Offer \`openspec init\` and wait for the user to request initialization. Do not initialize automatically or run \`openspec new change\`. After initialization, rerun this context check before continuing. For any other context failure, stop and report the error; do not fall back to the current directory or run later OpenSpec commands without the selected store. + Run \`openspec context --json\` from the current working directory (or \`openspec context --json --store ""\` when a registered store was explicitly selected). Use the returned \`root.path\` as the authoritative OpenSpec root. If context reports \`no_openspec_root\`, stop without creating or changing any files and follow the **Project check** above for how this workflow was reached. Offer \`openspec init\` only for an explicit OpenSpec request, and wait for the user to request initialization. Do not initialize automatically or run \`openspec new change\`. After initialization, rerun this context check before continuing. For any other context failure, stop and report the error; do not fall back to the current directory or run later OpenSpec commands without the selected store. Only when context returns a resolved \`root.path\`, read \`/openspec/config.yaml\`. Use \`config.yml\` only when \`config.yaml\` does not exist. If neither file exists, continue without project context. Do not fall back to \`config.yml\` if \`config.yaml\` is unreadable or invalid. @@ -214,7 +214,7 @@ ${PROJECT_ROOT_GUARD} 2. **Load project context** - Run \`openspec context --json\` from the current working directory (or \`openspec context --json --store ""\` when a registered store was explicitly selected). Use the returned \`root.path\` as the authoritative OpenSpec root. If context reports \`no_openspec_root\`, stop without creating or changing any files. Offer \`openspec init\` and wait for the user to request initialization. Do not initialize automatically or run \`openspec new change\`. After initialization, rerun this context check before continuing. For any other context failure, stop and report the error; do not fall back to the current directory or run later OpenSpec commands without the selected store. + Run \`openspec context --json\` from the current working directory (or \`openspec context --json --store ""\` when a registered store was explicitly selected). Use the returned \`root.path\` as the authoritative OpenSpec root. If context reports \`no_openspec_root\`, stop without creating or changing any files and follow the **Project check** above for how this workflow was reached. Offer \`openspec init\` only for an explicit OpenSpec request, and wait for the user to request initialization. Do not initialize automatically or run \`openspec new change\`. After initialization, rerun this context check before continuing. For any other context failure, stop and report the error; do not fall back to the current directory or run later OpenSpec commands without the selected store. Only when context returns a resolved \`root.path\`, read \`/openspec/config.yaml\`. Use \`config.yml\` only when \`config.yaml\` does not exist. If neither file exists, continue without project context. Do not fall back to \`config.yml\` if \`config.yaml\` is unreadable or invalid. diff --git a/test/core/templates/project-root-guard.test.ts b/test/core/templates/project-root-guard.test.ts index c5b25e0ec8..0c42f151ca 100644 --- a/test/core/templates/project-root-guard.test.ts +++ b/test/core/templates/project-root-guard.test.ts @@ -76,6 +76,22 @@ describe('project root guard', () => { expect(PROJECT_ROOT_GUARD).toContain('also exits non-zero, which is that answer rather than a broken CLI'); }); + // A store-only project whose `store:` line names a store this machine has not + // registered (a teammate's fresh clone) also reports `root: null`, with + // `unknown_store` or `no_registered_stores`. A stale global `defaultStore` + // reports the same codes in unrelated repositories, so only the message + // prefix pinned in test/core/root-selection.test.ts tells them apart. Treating + // that project as uninitialized would silently drop OpenSpec, or offer + // `openspec init`, in a project that is already set up. + it('does not mistake an unregistered declared store for an uninitialized project', () => { + expect(PROJECT_ROOT_GUARD).toContain('starts with `Declared in`'); + expect(PROJECT_ROOT_GUARD).toContain('Do not treat it as uninitialized and skip the branches below'); + expect(PROJECT_ROOT_GUARD).toContain("show the user that error's `message` and `fix`"); + expect(PROJECT_ROOT_GUARD.indexOf('starts with `Declared in`')).toBeLessThan( + PROJECT_ROOT_GUARD.indexOf('**Auto-selected**') + ); + }); + // #1645 asks for the workflow to get out of the way, not to interrogate the // user: "if not exist it can go through the normal general propose not the // openspec". So the two ways of arriving here get opposite answers, and both diff --git a/test/core/templates/skill-templates-parity.test.ts b/test/core/templates/skill-templates-parity.test.ts index 6e618a86b3..834c0599d7 100644 --- a/test/core/templates/skill-templates-parity.test.ts +++ b/test/core/templates/skill-templates-parity.test.ts @@ -38,46 +38,46 @@ import { import { STORE_SELECTION_GUIDANCE } from '../../../src/core/templates/workflows/store-selection.js'; const EXPECTED_FUNCTION_HASHES: Record = { - getExploreSkillTemplate: 'ee22cf5bdca13df2e62f9269cc89b97a54414148ee62d96561a3468b88239f88', - getNewChangeSkillTemplate: '22d6f37bd4407a159fc0e4df56535b9f8ec41db2bcf4b8838213c4583dab7508', - getContinueChangeSkillTemplate: 'b9d5e71fb1ea27fc0e886be778360d69ea5bad8ba2a00fd44c7b0aef6fb420ac', - getApplyChangeSkillTemplate: '8548ac443f5f9789589cdf57e15bd954b3fc0ba8285ffc846b0906225c8cdadc', - getFfChangeSkillTemplate: '54842368356da8da31fa0052c90c6bc326f894575761c3d627bc8a100d8752ad', - getSyncSpecsSkillTemplate: '5636031e95bb822f0b5f7bafda826b267e02826346be809510c3006021834ac8', - getOnboardSkillTemplate: '66da6835a1f7d38bec464f7eb4223ca56400745c02656ca9a09937d0cb0041f3', - getOpsxExploreCommandTemplate: 'b14495754388b5576090b47b90756511716fa8ef828df8c2eee907f8b6dd4687', - getOpsxNewCommandTemplate: '387bc291d002001fde8fbdf59309a0d2dc101b9538b1badb8603cb4828ad88f1', - getOpsxContinueCommandTemplate: '500e83a8af8a5e9af53a4b6b075135e09470b9df88c8fb99febec4a1679b67ce', - getOpsxApplyCommandTemplate: '98216b03669f1da6f03932cb00b60bad28af9a8688b54f7e3c76f85999fa2836', - getOpsxFfCommandTemplate: '0a488f0d11b950544577f5509fc5d35118ac9a7e483f65f8a3d523082a078966', - getArchiveChangeSkillTemplate: 'ba9916c7326c5c5c44e8230d778230156d2f14235ae9bc799ce3a869c53b28d1', - getBulkArchiveChangeSkillTemplate: '255fd206da93271e1ecc4cf042afb5d93141c88e342ff5eafce07749d3356df5', - getOpsxSyncCommandTemplate: 'e8b1314e1a72c9c8222fcfc2b553556d9ac58161ff3721f971dcfe2076eeca2a', - getVerifyChangeSkillTemplate: '860d997ab8e4a114f55ef40268729e8f5ea577f32b260fc31df72a13e3f99308', - getOpsxArchiveCommandTemplate: '4a335710a79d0e9fb913fafffebec6f318712533b1808d6677fa720208a64704', - getOpsxOnboardCommandTemplate: '74ec3635a3add7dc7ebeaebc1dddbdd94b13aa54bcb0dbbd62823bf84f7ab9dc', - getOpsxBulkArchiveCommandTemplate: '3f71ce2d80c011e115ddbaa97e2785719c7439448e7d3db96d683e0a1dd5065e', - getOpsxVerifyCommandTemplate: 'feb3c643f95d32b66d44e624426de0dd4ab84c77dea863314332bf58f1497254', - getOpsxProposeSkillTemplate: 'ab8cf7af5b2149b52d3317a1dc425daeb87bec61c2ca0e539875006e87070ac2', - getOpsxProposeCommandTemplate: 'b8c59ffa4f5c92a3bc11a2e7ff4a49808f3c8a64ebe2558ea525bf86b89ff0a8', + getExploreSkillTemplate: '8da228370cc68abe2ce4772289c9672b4426789100ca7798feddf011c178b684', + getNewChangeSkillTemplate: '37d6ab63360da7d25990a4f29eee1d393d6a6c4ec0020dfaf4e3880475984882', + getContinueChangeSkillTemplate: '021be741f29b5e06e92e1f53386655ceb87ad672189080892c6dd75640f63ec1', + getApplyChangeSkillTemplate: '8120ae05c3ff6257eafd90bce21395961006d534067a3f59651d0493a42dfa13', + getFfChangeSkillTemplate: '7b14213aeb80590c5bb85ffc1b0b46b8cf3d1a6ef255cbbdc6ce73671fbd8992', + getSyncSpecsSkillTemplate: '9321f1cc4facdaf4cd2a7755614d6252cec173caf6c2ac6456ee03aaafe1a55d', + getOnboardSkillTemplate: '3a37b2ada4752069402df7b597202d1e1c38627b1ca68063b2f7b041d78315e1', + getOpsxExploreCommandTemplate: 'b00cf1851f25fd6d05b57b61f77f15e7d68a02ed6f2e66176129d8ac81694574', + getOpsxNewCommandTemplate: '9740a3edf2ce01114caf78d6e12e98e6e9db95cafaf295a8aeae49de3b63b665', + getOpsxContinueCommandTemplate: '73f809e9b6e453f097f7ebbbc0052c8fbbb6860935cfa21821b65ad1bb9d5255', + getOpsxApplyCommandTemplate: '3a0bfdc0a22e0784552cdc90a869fc82678883fb1753c34d8d8bb71682b94a0c', + getOpsxFfCommandTemplate: '9b42a0fec7a26e6937766b2115b80b337a32c03330d1bebb06fcdc051a2a6e80', + getArchiveChangeSkillTemplate: '183cf866ce8d084b2780e259470c7c84a8f3115ba242c1a5363fba17df1d1741', + getBulkArchiveChangeSkillTemplate: '4b41aacfb0e856ca2a943df9c1eef6e640bdbac3b71607039f301af7e90205f0', + getOpsxSyncCommandTemplate: '042ba1d78fd7449a6754d1762636b0593f536ee6bfa27c146e3463db996eb3a9', + getVerifyChangeSkillTemplate: '3ee730d8e0d051ebb0c4dc47e45af916e6b86694929a5e57c38c9e29bff8cc74', + getOpsxArchiveCommandTemplate: '1f96f0649b08724b233638f03bd3e48bbf5be19ab7eaf0ce1e61bbdc5d55fb01', + getOpsxOnboardCommandTemplate: '41e7470d77664565ea41c788ac2617ac3e2b4dd7fb2354dda4d2dbb60e9c85c6', + getOpsxBulkArchiveCommandTemplate: 'a24814a93ac8ae582a1cc75cad5b89cd4f396c11a2c56a530183f6e8ac6a5035', + getOpsxVerifyCommandTemplate: '729a403fc9f5d4d39a4f37775e069075005af056351ecb4ad6269251e96bd9cb', + getOpsxProposeSkillTemplate: '39b7c59c29821064f4e76babd2a8661cb1d63c2e84f02242640aff50fed4857a', + getOpsxProposeCommandTemplate: 'ce381e54d3a865034dcc02be5e6b8af6e19dbd374ba2f6db4c32f011bf5c4405', getFeedbackSkillTemplate: 'dabeb5e825b9349abc8156c3e7b8608f27987912a6d9bf47ef29addde6138133', - getUpdateChangeSkillTemplate: '934a05834851e88f1f9d6e2a4d969832c29282feaabc3f96fe26bf7e50955a14', - getOpsxUpdateCommandTemplate: 'b3fafee176b50630000cc0235ccb14aa04da68cdbc6747548b5f739043493b1f', + getUpdateChangeSkillTemplate: '3b81d34ad0d9b0761da380ae46adff62a05b6bdfec51499dac076783d7dd61af', + getOpsxUpdateCommandTemplate: 'a140dbd3a0bdad7246b946f15b488711e7bd7a053c8f37ad4d6ffa5f3055c327', }; const EXPECTED_GENERATED_SKILL_CONTENT_HASHES: Record = { - 'openspec-explore': '143cd6283eac1e8b487d63390e4c583abd9708bcbae6abee75e6bcf1397e4b36', - 'openspec-new-change': '9fd65ab06abaa5da56ebdb596adcac45015cae8d4818ab7b9c622f7e0556d6e0', - 'openspec-continue-change': '98a5b8f2af2cec6bec3e8da9c410603dd5df0d90b1c9caf633abd4e86881bf25', - 'openspec-apply-change': '27d857f848714372a9e1012082c42052d59093803dc7c858333a945e15429309', - 'openspec-ff-change': '6d3cc61eadc6b738c8ed5bf2f6165df347950431e8da97895934aa87f4810b9e', - 'openspec-sync-specs': '2d78ec7675715959841ea7242960f202c112d23ecbaaf12c2740525d69904830', - 'openspec-archive-change': '61ac661788c84193ec0b1478598ccc743d741baf30fcc8c6926e7ad4814672e3', - 'openspec-bulk-archive-change': '09d120c08ec1726923874efb0308b69196ad054108159c0144a7f631bc0e8742', - 'openspec-verify-change': 'f8bbf1243eb5dcde3b9d909bdb1c83c7d4ef9be0d873cb1d88bf81835faffdfa', - 'openspec-onboard': '0045322427a1be84f0d9c7caa7b2668f2a913c86bb1ac229c9ffd8ce0b7bf8c2', - 'openspec-propose': 'bf9870c0fc46b1b9d1ca51a79ab11fbc62f60f3c4f5796ccc6400ca738701ebf', - 'openspec-update-change': 'affc5639381b220113094bc87dd932c118c8cf74a4abb9fe40049fce1f85a8c7', + 'openspec-explore': 'eda0b87e39f88e058847435ea695a678add131785859713c980c878a15e2c185', + 'openspec-new-change': '24fceefc3ee83410f149785f3ed3d06158a9033d9ad061aa48751865fd61a50a', + 'openspec-continue-change': '5acedb5c7354f1a271d9a24c43c767b6d1acf6d0108acf44fd5882be044ee4c5', + 'openspec-apply-change': '654b648009488e3208d8b575a22801bc4708d1bef4ccfa672864a7cace4e5359', + 'openspec-ff-change': '232bdfb9722d46864ab6945cba376966e9de4758a404bf91844c4a9212b3099f', + 'openspec-sync-specs': '14325d79a9a513bdf6d06ad9777556bae91b3dedfb72f010d46a823423402c36', + 'openspec-archive-change': '23c542396acf76295879ad9359efc07cb79a6ae554b7bbf08481f67d902bacb2', + 'openspec-bulk-archive-change': '08363c6efb445292eb071b23cd7ba56f52b736d0db8d2c0612d63d80e720af1a', + 'openspec-verify-change': 'ecfc092f113c69f56607c9a8cc29f417c324842667f64badb4695ce6fdae71d4', + 'openspec-onboard': 'b0b6e4679bce217a9e162d7e041af6c323ee50b8c9e939932279abbc3c034a07', + 'openspec-propose': 'cfea2589c5aa3374fb33aa047e54e48b97f89c1363bd4c998fc6a6ea1a605a78', + 'openspec-update-change': '399ef2c1c4596fd14301601ac2181600b15a42fa17e2c11ea4455ed6d6789a9c', }; // Intentionally excludes getFeedbackSkillTemplate: this list only models templates From 080d8bbc6cbc8b0c0a49a3926dd054fe90670dcd Mon Sep 17 00:00:00 2001 From: Clay Good Date: Tue, 15 Sep 2026 08:20:08 -0500 Subject: [PATCH 14/15] 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 --- .changeset/guard-uninitialized-projects.md | 2 +- docs-lab/reference/skills.md | 2 +- skills/openspec-apply-change/SKILL.md | 2 +- skills/openspec-archive-change/SKILL.md | 2 +- skills/openspec-bulk-archive-change/SKILL.md | 2 +- skills/openspec-continue-change/SKILL.md | 2 +- skills/openspec-explore/SKILL.md | 2 +- skills/openspec-ff-change/SKILL.md | 2 +- skills/openspec-new-change/SKILL.md | 2 +- skills/openspec-onboard/SKILL.md | 2 +- skills/openspec-propose/SKILL.md | 2 +- skills/openspec-sync-specs/SKILL.md | 2 +- skills/openspec-update-change/SKILL.md | 2 +- skills/openspec-verify-change/SKILL.md | 2 +- src/core/templates/workflows/project-root.ts | 2 +- test/core/root-selection.test.ts | 40 +++++++++++ .../templates/skill-templates-parity.test.ts | 72 +++++++++---------- 17 files changed, 91 insertions(+), 51 deletions(-) diff --git a/.changeset/guard-uninitialized-projects.md b/.changeset/guard-uninitialized-projects.md index 0f2dd0ef13..231794467c 100644 --- a/.changeset/guard-uninitialized-projects.md +++ b/.changeset/guard-uninitialized-projects.md @@ -4,4 +4,4 @@ ### Bug Fixes -- Generated skills and commands no longer adopt a project that never ran `openspec init`. Every workflow now checks `root` from `openspec list --json` before its first write, and `"root": null` means the project is not set up. What happens next depends on how the workflow was reached. A skill the agent picked on its own drops OpenSpec and answers the request normally, without asking about setup. A workflow the user asked for by name, or ran as a slash command, stops and asks whether to initialize the project, target a store, or handle the request without OpenSpec. A project whose `openspec/config.yaml` names a store this machine has not registered is not mistaken for an uninitialized one: the workflow stops and shows the store error. Neither path lets `openspec new change` create `openspec/` in the current directory as a side effect. Skill descriptions now name OpenSpec so hosts stop offering these workflows in unrelated repositories. `openspec new change` also says when it had to create the root itself, so a directory that was never set up no longer picks up an `openspec/` directory in silence (human output only; `--json` is unchanged). +- Generated skills and commands no longer adopt a project that never ran `openspec init`. Every workflow now checks `root` from `openspec list --json` before its first write, and `"root": null` means the project is not set up. What happens next depends on how the workflow was reached. A skill the agent picked on its own drops OpenSpec and answers the request normally, without asking about setup. A workflow the user asked for by name, or ran as a slash command, stops and asks whether to initialize the project, target a store, or handle the request without OpenSpec. A project whose `openspec/config.yaml` names a store this machine cannot resolve (not registered, or a malformed `store:` line) is not mistaken for an uninitialized one: the workflow stops and shows the store error. Neither path lets `openspec new change` create `openspec/` in the current directory as a side effect. Skill descriptions now name OpenSpec so hosts stop offering these workflows in unrelated repositories. `openspec new change` also says when it had to create the root itself, so a directory that was never set up no longer picks up an `openspec/` directory in silence (human output only; `--json` is unchanged). diff --git a/docs-lab/reference/skills.md b/docs-lab/reference/skills.md index 3e7cfd65aa..9247cb4ebc 100644 --- a/docs-lab/reference/skills.md +++ b/docs-lab/reference/skills.md @@ -44,7 +44,7 @@ Every skill expects a project that already uses OpenSpec. Before its first step - **Auto-selected**: your agent picked the skill on its own, without you naming OpenSpec. It drops OpenSpec and answers your request normally, the way it would with OpenSpec not installed. - **Explicit OpenSpec request**: you named OpenSpec, named the skill, or ran its command. It stops before writing and asks how to proceed: run `openspec init` here, target a store with `--store `, or continue without OpenSpec. It waits for your answer. -Commands are always the second case. A project whose `openspec/config.yaml` names a store this machine has not registered is not treated as uninitialized: the skill stops and shows the store error with its fix. No skill creates an `openspec/` directory on its own in either case. The entries below describe what each skill does once a root is in place. +Commands are always the second case. A project whose `openspec/config.yaml` names a store this machine cannot resolve (not registered, or a malformed `store:` line) is not treated as uninitialized: the skill stops and shows the store error with its fix. No skill creates an `openspec/` directory on its own in either case. The entries below describe what each skill does once a root is in place. | Skill | Job | Type | |---|---|---| diff --git a/skills/openspec-apply-change/SKILL.md b/skills/openspec-apply-change/SKILL.md index 9b1618bc34..d4b2ed6970 100644 --- a/skills/openspec-apply-change/SKILL.md +++ b/skills/openspec-apply-change/SKILL.md @@ -15,7 +15,7 @@ Implement tasks from an OpenSpec change. **Project check:** These steps expect a project that already uses OpenSpec. Before the first step that writes anything (`new change`, `archive`, `sync specs`, or authoring an artifact file), confirm the project has a root: run `openspec list --json` (with `--store ` when a store is selected, since the store is then the root) and read `root`. A root object means the project is set up. `"root": null` means it is not - there is no `openspec/` directory here, and a write such as `openspec new change` would create one as a side effect. The command also exits non-zero, which is that answer rather than a broken CLI, so read the JSON instead of retrying or working around it. -One `"root": null` is not about setup: when a `status` error message starts with `Declared in` and names this project's `openspec/config.yaml`, the project does use OpenSpec through a store this machine has not registered. Do not treat it as uninitialized and skip the branches below: stop before writing and show the user that error's `message` and `fix`. +One `"root": null` is not about setup: when a `status` error message starts with `Declared in` or `Invalid store declaration in` and names this project's `openspec/config.yaml` (or `config.yml`), the project does use OpenSpec through a store it declares, which this machine cannot resolve (the store is not registered, or the `store:` line is malformed). Do not treat it as uninitialized and skip the branches below: stop before writing and show the user that error's `message` and `fix`. Otherwise, with no root, what happens next depends on how this workflow was reached: diff --git a/skills/openspec-archive-change/SKILL.md b/skills/openspec-archive-change/SKILL.md index 8bd1a25cf6..531cb4e670 100644 --- a/skills/openspec-archive-change/SKILL.md +++ b/skills/openspec-archive-change/SKILL.md @@ -15,7 +15,7 @@ Archive a completed change in the experimental workflow. **Project check:** These steps expect a project that already uses OpenSpec. Before the first step that writes anything (`new change`, `archive`, `sync specs`, or authoring an artifact file), confirm the project has a root: run `openspec list --json` (with `--store ` when a store is selected, since the store is then the root) and read `root`. A root object means the project is set up. `"root": null` means it is not - there is no `openspec/` directory here, and a write such as `openspec new change` would create one as a side effect. The command also exits non-zero, which is that answer rather than a broken CLI, so read the JSON instead of retrying or working around it. -One `"root": null` is not about setup: when a `status` error message starts with `Declared in` and names this project's `openspec/config.yaml`, the project does use OpenSpec through a store this machine has not registered. Do not treat it as uninitialized and skip the branches below: stop before writing and show the user that error's `message` and `fix`. +One `"root": null` is not about setup: when a `status` error message starts with `Declared in` or `Invalid store declaration in` and names this project's `openspec/config.yaml` (or `config.yml`), the project does use OpenSpec through a store it declares, which this machine cannot resolve (the store is not registered, or the `store:` line is malformed). Do not treat it as uninitialized and skip the branches below: stop before writing and show the user that error's `message` and `fix`. Otherwise, with no root, what happens next depends on how this workflow was reached: diff --git a/skills/openspec-bulk-archive-change/SKILL.md b/skills/openspec-bulk-archive-change/SKILL.md index 79488a5ed2..fd3990dadc 100644 --- a/skills/openspec-bulk-archive-change/SKILL.md +++ b/skills/openspec-bulk-archive-change/SKILL.md @@ -17,7 +17,7 @@ This skill allows you to batch-archive changes, handling spec conflicts intellig **Project check:** These steps expect a project that already uses OpenSpec. Before the first step that writes anything (`new change`, `archive`, `sync specs`, or authoring an artifact file), confirm the project has a root: run `openspec list --json` (with `--store ` when a store is selected, since the store is then the root) and read `root`. A root object means the project is set up. `"root": null` means it is not - there is no `openspec/` directory here, and a write such as `openspec new change` would create one as a side effect. The command also exits non-zero, which is that answer rather than a broken CLI, so read the JSON instead of retrying or working around it. -One `"root": null` is not about setup: when a `status` error message starts with `Declared in` and names this project's `openspec/config.yaml`, the project does use OpenSpec through a store this machine has not registered. Do not treat it as uninitialized and skip the branches below: stop before writing and show the user that error's `message` and `fix`. +One `"root": null` is not about setup: when a `status` error message starts with `Declared in` or `Invalid store declaration in` and names this project's `openspec/config.yaml` (or `config.yml`), the project does use OpenSpec through a store it declares, which this machine cannot resolve (the store is not registered, or the `store:` line is malformed). Do not treat it as uninitialized and skip the branches below: stop before writing and show the user that error's `message` and `fix`. Otherwise, with no root, what happens next depends on how this workflow was reached: diff --git a/skills/openspec-continue-change/SKILL.md b/skills/openspec-continue-change/SKILL.md index 4d4ae59421..4a0d5e2f23 100644 --- a/skills/openspec-continue-change/SKILL.md +++ b/skills/openspec-continue-change/SKILL.md @@ -15,7 +15,7 @@ Continue working on a change by creating the next artifact. **Project check:** These steps expect a project that already uses OpenSpec. Before the first step that writes anything (`new change`, `archive`, `sync specs`, or authoring an artifact file), confirm the project has a root: run `openspec list --json` (with `--store ` when a store is selected, since the store is then the root) and read `root`. A root object means the project is set up. `"root": null` means it is not - there is no `openspec/` directory here, and a write such as `openspec new change` would create one as a side effect. The command also exits non-zero, which is that answer rather than a broken CLI, so read the JSON instead of retrying or working around it. -One `"root": null` is not about setup: when a `status` error message starts with `Declared in` and names this project's `openspec/config.yaml`, the project does use OpenSpec through a store this machine has not registered. Do not treat it as uninitialized and skip the branches below: stop before writing and show the user that error's `message` and `fix`. +One `"root": null` is not about setup: when a `status` error message starts with `Declared in` or `Invalid store declaration in` and names this project's `openspec/config.yaml` (or `config.yml`), the project does use OpenSpec through a store it declares, which this machine cannot resolve (the store is not registered, or the `store:` line is malformed). Do not treat it as uninitialized and skip the branches below: stop before writing and show the user that error's `message` and `fix`. Otherwise, with no root, what happens next depends on how this workflow was reached: diff --git a/skills/openspec-explore/SKILL.md b/skills/openspec-explore/SKILL.md index a3910ffae6..63ae843665 100644 --- a/skills/openspec-explore/SKILL.md +++ b/skills/openspec-explore/SKILL.md @@ -19,7 +19,7 @@ Enter explore mode. Think deeply. Visualize freely. Follow the conversation wher **Project check:** These steps expect a project that already uses OpenSpec. Before the first step that writes anything (`new change`, `archive`, `sync specs`, or authoring an artifact file), confirm the project has a root: run `openspec list --json` (with `--store ` when a store is selected, since the store is then the root) and read `root`. A root object means the project is set up. `"root": null` means it is not - there is no `openspec/` directory here, and a write such as `openspec new change` would create one as a side effect. The command also exits non-zero, which is that answer rather than a broken CLI, so read the JSON instead of retrying or working around it. -One `"root": null` is not about setup: when a `status` error message starts with `Declared in` and names this project's `openspec/config.yaml`, the project does use OpenSpec through a store this machine has not registered. Do not treat it as uninitialized and skip the branches below: stop before writing and show the user that error's `message` and `fix`. +One `"root": null` is not about setup: when a `status` error message starts with `Declared in` or `Invalid store declaration in` and names this project's `openspec/config.yaml` (or `config.yml`), the project does use OpenSpec through a store it declares, which this machine cannot resolve (the store is not registered, or the `store:` line is malformed). Do not treat it as uninitialized and skip the branches below: stop before writing and show the user that error's `message` and `fix`. Otherwise, with no root, what happens next depends on how this workflow was reached: diff --git a/skills/openspec-ff-change/SKILL.md b/skills/openspec-ff-change/SKILL.md index ebab88af9b..5607d7bc69 100644 --- a/skills/openspec-ff-change/SKILL.md +++ b/skills/openspec-ff-change/SKILL.md @@ -15,7 +15,7 @@ Fast-forward through artifact creation - generate everything needed to start imp **Project check:** These steps expect a project that already uses OpenSpec. Before the first step that writes anything (`new change`, `archive`, `sync specs`, or authoring an artifact file), confirm the project has a root: run `openspec list --json` (with `--store ` when a store is selected, since the store is then the root) and read `root`. A root object means the project is set up. `"root": null` means it is not - there is no `openspec/` directory here, and a write such as `openspec new change` would create one as a side effect. The command also exits non-zero, which is that answer rather than a broken CLI, so read the JSON instead of retrying or working around it. -One `"root": null` is not about setup: when a `status` error message starts with `Declared in` and names this project's `openspec/config.yaml`, the project does use OpenSpec through a store this machine has not registered. Do not treat it as uninitialized and skip the branches below: stop before writing and show the user that error's `message` and `fix`. +One `"root": null` is not about setup: when a `status` error message starts with `Declared in` or `Invalid store declaration in` and names this project's `openspec/config.yaml` (or `config.yml`), the project does use OpenSpec through a store it declares, which this machine cannot resolve (the store is not registered, or the `store:` line is malformed). Do not treat it as uninitialized and skip the branches below: stop before writing and show the user that error's `message` and `fix`. Otherwise, with no root, what happens next depends on how this workflow was reached: diff --git a/skills/openspec-new-change/SKILL.md b/skills/openspec-new-change/SKILL.md index 42eded7492..ddb822386b 100644 --- a/skills/openspec-new-change/SKILL.md +++ b/skills/openspec-new-change/SKILL.md @@ -15,7 +15,7 @@ Start a new change using the experimental artifact-driven approach. **Project check:** These steps expect a project that already uses OpenSpec. Before the first step that writes anything (`new change`, `archive`, `sync specs`, or authoring an artifact file), confirm the project has a root: run `openspec list --json` (with `--store ` when a store is selected, since the store is then the root) and read `root`. A root object means the project is set up. `"root": null` means it is not - there is no `openspec/` directory here, and a write such as `openspec new change` would create one as a side effect. The command also exits non-zero, which is that answer rather than a broken CLI, so read the JSON instead of retrying or working around it. -One `"root": null` is not about setup: when a `status` error message starts with `Declared in` and names this project's `openspec/config.yaml`, the project does use OpenSpec through a store this machine has not registered. Do not treat it as uninitialized and skip the branches below: stop before writing and show the user that error's `message` and `fix`. +One `"root": null` is not about setup: when a `status` error message starts with `Declared in` or `Invalid store declaration in` and names this project's `openspec/config.yaml` (or `config.yml`), the project does use OpenSpec through a store it declares, which this machine cannot resolve (the store is not registered, or the `store:` line is malformed). Do not treat it as uninitialized and skip the branches below: stop before writing and show the user that error's `message` and `fix`. Otherwise, with no root, what happens next depends on how this workflow was reached: diff --git a/skills/openspec-onboard/SKILL.md b/skills/openspec-onboard/SKILL.md index 05bda93b06..2e923cecfd 100644 --- a/skills/openspec-onboard/SKILL.md +++ b/skills/openspec-onboard/SKILL.md @@ -15,7 +15,7 @@ Guide the user through their first complete OpenSpec workflow cycle. This is a t **Project check:** These steps expect a project that already uses OpenSpec. Before the first step that writes anything (`new change`, `archive`, `sync specs`, or authoring an artifact file), confirm the project has a root: run `openspec list --json` (with `--store ` when a store is selected, since the store is then the root) and read `root`. A root object means the project is set up. `"root": null` means it is not - there is no `openspec/` directory here, and a write such as `openspec new change` would create one as a side effect. The command also exits non-zero, which is that answer rather than a broken CLI, so read the JSON instead of retrying or working around it. -One `"root": null` is not about setup: when a `status` error message starts with `Declared in` and names this project's `openspec/config.yaml`, the project does use OpenSpec through a store this machine has not registered. Do not treat it as uninitialized and skip the branches below: stop before writing and show the user that error's `message` and `fix`. +One `"root": null` is not about setup: when a `status` error message starts with `Declared in` or `Invalid store declaration in` and names this project's `openspec/config.yaml` (or `config.yml`), the project does use OpenSpec through a store it declares, which this machine cannot resolve (the store is not registered, or the `store:` line is malformed). Do not treat it as uninitialized and skip the branches below: stop before writing and show the user that error's `message` and `fix`. Otherwise, with no root, what happens next depends on how this workflow was reached: diff --git a/skills/openspec-propose/SKILL.md b/skills/openspec-propose/SKILL.md index c012cff58d..6332280526 100644 --- a/skills/openspec-propose/SKILL.md +++ b/skills/openspec-propose/SKILL.md @@ -29,7 +29,7 @@ When the user is ready to implement, they must start the apply workflow explicit **Project check:** These steps expect a project that already uses OpenSpec. Before the first step that writes anything (`new change`, `archive`, `sync specs`, or authoring an artifact file), confirm the project has a root: run `openspec list --json` (with `--store ` when a store is selected, since the store is then the root) and read `root`. A root object means the project is set up. `"root": null` means it is not - there is no `openspec/` directory here, and a write such as `openspec new change` would create one as a side effect. The command also exits non-zero, which is that answer rather than a broken CLI, so read the JSON instead of retrying or working around it. -One `"root": null` is not about setup: when a `status` error message starts with `Declared in` and names this project's `openspec/config.yaml`, the project does use OpenSpec through a store this machine has not registered. Do not treat it as uninitialized and skip the branches below: stop before writing and show the user that error's `message` and `fix`. +One `"root": null` is not about setup: when a `status` error message starts with `Declared in` or `Invalid store declaration in` and names this project's `openspec/config.yaml` (or `config.yml`), the project does use OpenSpec through a store it declares, which this machine cannot resolve (the store is not registered, or the `store:` line is malformed). Do not treat it as uninitialized and skip the branches below: stop before writing and show the user that error's `message` and `fix`. Otherwise, with no root, what happens next depends on how this workflow was reached: diff --git a/skills/openspec-sync-specs/SKILL.md b/skills/openspec-sync-specs/SKILL.md index ce994b4d35..2f61d20394 100644 --- a/skills/openspec-sync-specs/SKILL.md +++ b/skills/openspec-sync-specs/SKILL.md @@ -17,7 +17,7 @@ This is an **agent-driven** operation - you will read delta specs and directly e **Project check:** These steps expect a project that already uses OpenSpec. Before the first step that writes anything (`new change`, `archive`, `sync specs`, or authoring an artifact file), confirm the project has a root: run `openspec list --json` (with `--store ` when a store is selected, since the store is then the root) and read `root`. A root object means the project is set up. `"root": null` means it is not - there is no `openspec/` directory here, and a write such as `openspec new change` would create one as a side effect. The command also exits non-zero, which is that answer rather than a broken CLI, so read the JSON instead of retrying or working around it. -One `"root": null` is not about setup: when a `status` error message starts with `Declared in` and names this project's `openspec/config.yaml`, the project does use OpenSpec through a store this machine has not registered. Do not treat it as uninitialized and skip the branches below: stop before writing and show the user that error's `message` and `fix`. +One `"root": null` is not about setup: when a `status` error message starts with `Declared in` or `Invalid store declaration in` and names this project's `openspec/config.yaml` (or `config.yml`), the project does use OpenSpec through a store it declares, which this machine cannot resolve (the store is not registered, or the `store:` line is malformed). Do not treat it as uninitialized and skip the branches below: stop before writing and show the user that error's `message` and `fix`. Otherwise, with no root, what happens next depends on how this workflow was reached: diff --git a/skills/openspec-update-change/SKILL.md b/skills/openspec-update-change/SKILL.md index b9fe0978e0..4735c8f0e5 100644 --- a/skills/openspec-update-change/SKILL.md +++ b/skills/openspec-update-change/SKILL.md @@ -15,7 +15,7 @@ Revise a change's existing planning artifacts and keep them coherent. Never edit **Project check:** These steps expect a project that already uses OpenSpec. Before the first step that writes anything (`new change`, `archive`, `sync specs`, or authoring an artifact file), confirm the project has a root: run `openspec list --json` (with `--store ` when a store is selected, since the store is then the root) and read `root`. A root object means the project is set up. `"root": null` means it is not - there is no `openspec/` directory here, and a write such as `openspec new change` would create one as a side effect. The command also exits non-zero, which is that answer rather than a broken CLI, so read the JSON instead of retrying or working around it. -One `"root": null` is not about setup: when a `status` error message starts with `Declared in` and names this project's `openspec/config.yaml`, the project does use OpenSpec through a store this machine has not registered. Do not treat it as uninitialized and skip the branches below: stop before writing and show the user that error's `message` and `fix`. +One `"root": null` is not about setup: when a `status` error message starts with `Declared in` or `Invalid store declaration in` and names this project's `openspec/config.yaml` (or `config.yml`), the project does use OpenSpec through a store it declares, which this machine cannot resolve (the store is not registered, or the `store:` line is malformed). Do not treat it as uninitialized and skip the branches below: stop before writing and show the user that error's `message` and `fix`. Otherwise, with no root, what happens next depends on how this workflow was reached: diff --git a/skills/openspec-verify-change/SKILL.md b/skills/openspec-verify-change/SKILL.md index 99abbc1107..03276ce818 100644 --- a/skills/openspec-verify-change/SKILL.md +++ b/skills/openspec-verify-change/SKILL.md @@ -15,7 +15,7 @@ Verify that an implementation matches the change artifacts (specs, tasks, design **Project check:** These steps expect a project that already uses OpenSpec. Before the first step that writes anything (`new change`, `archive`, `sync specs`, or authoring an artifact file), confirm the project has a root: run `openspec list --json` (with `--store ` when a store is selected, since the store is then the root) and read `root`. A root object means the project is set up. `"root": null` means it is not - there is no `openspec/` directory here, and a write such as `openspec new change` would create one as a side effect. The command also exits non-zero, which is that answer rather than a broken CLI, so read the JSON instead of retrying or working around it. -One `"root": null` is not about setup: when a `status` error message starts with `Declared in` and names this project's `openspec/config.yaml`, the project does use OpenSpec through a store this machine has not registered. Do not treat it as uninitialized and skip the branches below: stop before writing and show the user that error's `message` and `fix`. +One `"root": null` is not about setup: when a `status` error message starts with `Declared in` or `Invalid store declaration in` and names this project's `openspec/config.yaml` (or `config.yml`), the project does use OpenSpec through a store it declares, which this machine cannot resolve (the store is not registered, or the `store:` line is malformed). Do not treat it as uninitialized and skip the branches below: stop before writing and show the user that error's `message` and `fix`. Otherwise, with no root, what happens next depends on how this workflow was reached: diff --git a/src/core/templates/workflows/project-root.ts b/src/core/templates/workflows/project-root.ts index c6adafe9b7..2815844210 100644 --- a/src/core/templates/workflows/project-root.ts +++ b/src/core/templates/workflows/project-root.ts @@ -27,7 +27,7 @@ */ export const PROJECT_ROOT_GUARD = `**Project check:** These steps expect a project that already uses OpenSpec. Before the first step that writes anything (\`new change\`, \`archive\`, \`sync specs\`, or authoring an artifact file), confirm the project has a root: run \`openspec list --json\` (with \`--store \` when a store is selected, since the store is then the root) and read \`root\`. A root object means the project is set up. \`"root": null\` means it is not - there is no \`openspec/\` directory here, and a write such as \`openspec new change\` would create one as a side effect. The command also exits non-zero, which is that answer rather than a broken CLI, so read the JSON instead of retrying or working around it. -One \`"root": null\` is not about setup: when a \`status\` error message starts with \`Declared in\` and names this project's \`openspec/config.yaml\`, the project does use OpenSpec through a store this machine has not registered. Do not treat it as uninitialized and skip the branches below: stop before writing and show the user that error's \`message\` and \`fix\`. +One \`"root": null\` is not about setup: when a \`status\` error message starts with \`Declared in\` or \`Invalid store declaration in\` and names this project's \`openspec/config.yaml\` (or \`config.yml\`), the project does use OpenSpec through a store it declares, which this machine cannot resolve (the store is not registered, or the \`store:\` line is malformed). Do not treat it as uninitialized and skip the branches below: stop before writing and show the user that error's \`message\` and \`fix\`. Otherwise, with no root, what happens next depends on how this workflow was reached: diff --git a/test/core/root-selection.test.ts b/test/core/root-selection.test.ts index 3a09d2ee55..31bfbb3cc8 100644 --- a/test/core/root-selection.test.ts +++ b/test/core/root-selection.test.ts @@ -500,6 +500,46 @@ describe('resolveOpenSpecRoot', () => { ); expect(error.message).toContain(path.join(dir, 'openspec', 'config.yml')); }); + + // The generated workflows' project check (#1645) reads `root: null` as + // "never initialized" unless the status message starts with a prefix it + // names. Every failure of a project's own `store:` declaration must carry + // one of those prefixes, or the guard drops OpenSpec (or offers `openspec + // init`) in a project that already uses it. A stale global defaultStore in + // an unrelated repository must carry neither. + it('starts every declaration failure with a prefix the project check names', async () => { + const { PROJECT_ROOT_GUARD } = await import( + '../../src/core/templates/workflows/project-root.js' + ); + const prefixes = ['Declared in ', 'Invalid store declaration in ']; + for (const prefix of prefixes) { + expect(PROJECT_ROOT_GUARD).toContain(`\`${prefix.trim()}\``); + } + const namedByGuard = (message: string) => + prefixes.some((prefix) => message.startsWith(prefix)); + + const failures = [ + createPointerDir('guard-unregistered', 'store: ghost\n'), + createPointerDir('guard-bad-type', 'store: [a, b]\n'), + createPointerDir('guard-bad-yaml', 'store: [unclosed'), + createPointerDir('guard-bad-id', 'store: "BAD ID"\n'), + ]; + for (const dir of failures) { + const error = await resolveOpenSpecRoot({ startPath: dir, globalDataDir }).catch( + (caught: unknown) => caught + ); + expect(error, dir).toBeInstanceOf(RootSelectionError); + expect(namedByGuard((error as RootSelectionError).message), dir).toBe(true); + } + + setDefaultStore('ghost-plans'); + const stale = await resolveOpenSpecRoot({ + startPath: mkdir('guard-unrelated-repo'), + globalDataDir, + }).catch((caught: unknown) => caught); + expect(stale).toBeInstanceOf(RootSelectionError); + expect(namedByGuard((stale as RootSelectionError).message)).toBe(false); + }); }); it('skips openspec/ directories that are neither planning-shaped nor configured (the ~/openspec layout)', async () => { diff --git a/test/core/templates/skill-templates-parity.test.ts b/test/core/templates/skill-templates-parity.test.ts index 834c0599d7..10941bb012 100644 --- a/test/core/templates/skill-templates-parity.test.ts +++ b/test/core/templates/skill-templates-parity.test.ts @@ -38,46 +38,46 @@ import { import { STORE_SELECTION_GUIDANCE } from '../../../src/core/templates/workflows/store-selection.js'; const EXPECTED_FUNCTION_HASHES: Record = { - getExploreSkillTemplate: '8da228370cc68abe2ce4772289c9672b4426789100ca7798feddf011c178b684', - getNewChangeSkillTemplate: '37d6ab63360da7d25990a4f29eee1d393d6a6c4ec0020dfaf4e3880475984882', - getContinueChangeSkillTemplate: '021be741f29b5e06e92e1f53386655ceb87ad672189080892c6dd75640f63ec1', - getApplyChangeSkillTemplate: '8120ae05c3ff6257eafd90bce21395961006d534067a3f59651d0493a42dfa13', - getFfChangeSkillTemplate: '7b14213aeb80590c5bb85ffc1b0b46b8cf3d1a6ef255cbbdc6ce73671fbd8992', - getSyncSpecsSkillTemplate: '9321f1cc4facdaf4cd2a7755614d6252cec173caf6c2ac6456ee03aaafe1a55d', - getOnboardSkillTemplate: '3a37b2ada4752069402df7b597202d1e1c38627b1ca68063b2f7b041d78315e1', - getOpsxExploreCommandTemplate: 'b00cf1851f25fd6d05b57b61f77f15e7d68a02ed6f2e66176129d8ac81694574', - getOpsxNewCommandTemplate: '9740a3edf2ce01114caf78d6e12e98e6e9db95cafaf295a8aeae49de3b63b665', - getOpsxContinueCommandTemplate: '73f809e9b6e453f097f7ebbbc0052c8fbbb6860935cfa21821b65ad1bb9d5255', - getOpsxApplyCommandTemplate: '3a0bfdc0a22e0784552cdc90a869fc82678883fb1753c34d8d8bb71682b94a0c', - getOpsxFfCommandTemplate: '9b42a0fec7a26e6937766b2115b80b337a32c03330d1bebb06fcdc051a2a6e80', - getArchiveChangeSkillTemplate: '183cf866ce8d084b2780e259470c7c84a8f3115ba242c1a5363fba17df1d1741', - getBulkArchiveChangeSkillTemplate: '4b41aacfb0e856ca2a943df9c1eef6e640bdbac3b71607039f301af7e90205f0', - getOpsxSyncCommandTemplate: '042ba1d78fd7449a6754d1762636b0593f536ee6bfa27c146e3463db996eb3a9', - getVerifyChangeSkillTemplate: '3ee730d8e0d051ebb0c4dc47e45af916e6b86694929a5e57c38c9e29bff8cc74', - getOpsxArchiveCommandTemplate: '1f96f0649b08724b233638f03bd3e48bbf5be19ab7eaf0ce1e61bbdc5d55fb01', - getOpsxOnboardCommandTemplate: '41e7470d77664565ea41c788ac2617ac3e2b4dd7fb2354dda4d2dbb60e9c85c6', - getOpsxBulkArchiveCommandTemplate: 'a24814a93ac8ae582a1cc75cad5b89cd4f396c11a2c56a530183f6e8ac6a5035', - getOpsxVerifyCommandTemplate: '729a403fc9f5d4d39a4f37775e069075005af056351ecb4ad6269251e96bd9cb', - getOpsxProposeSkillTemplate: '39b7c59c29821064f4e76babd2a8661cb1d63c2e84f02242640aff50fed4857a', - getOpsxProposeCommandTemplate: 'ce381e54d3a865034dcc02be5e6b8af6e19dbd374ba2f6db4c32f011bf5c4405', + getExploreSkillTemplate: '653abc239a47cc7e50a511e7866b5f6be5336cfc0c9e66c912061dccd562d20c', + getNewChangeSkillTemplate: 'c7baff552ed6adcde8603741311c01aa56e4d548ccc46e4e8139fc3bc751ec23', + getContinueChangeSkillTemplate: '281ffd3876e8d3e4c2f5a2bdd640116abf617b900b57d8144ada7975dd10bf49', + getApplyChangeSkillTemplate: '8b02641490b6cf622b67a0e9d3257b980b93aef70df452bc3a862c01aeb7128b', + getFfChangeSkillTemplate: '2f2c9a40145ba7385762e33915203bbfcd9f1949c8a017f260ee8e5599feab7a', + getSyncSpecsSkillTemplate: '0d24c686630767a0640b91606866ea9f739634989c244420c6a344adbc2a3b07', + getOnboardSkillTemplate: '686aeb8c531f08a17200ddc10939c458784593bad76b88ae804889b81c0b611a', + getOpsxExploreCommandTemplate: '9c049c21c3d205215c9be82a4fde7ab1349a90cecdc0acf1eb10af856543f88b', + getOpsxNewCommandTemplate: '494d06f205891c0ea193d8359601f93a845c768169378540a2129a691540fb80', + getOpsxContinueCommandTemplate: 'dee57ffaa00ed1252f7cfdffe5b51a55badb27c00eb8b6afe8a8f3a589af5266', + getOpsxApplyCommandTemplate: 'b1a166aa233412bdb2568bbe596b656b3e128c3ca6dc9717c17e8da1a933f908', + getOpsxFfCommandTemplate: 'a0b1dc92988decaa216f48665654d3451af12ba1e781039a340577aaa98ed1c9', + getArchiveChangeSkillTemplate: '13e9e5ff0a216f9e99c00d1e041d70d886df09a00cd47571d6659c08c9997dce', + getBulkArchiveChangeSkillTemplate: '691763ea077370eaaeac86815912f34c410355fbde37f72d52cb6adf8ac50d60', + getOpsxSyncCommandTemplate: 'c65fb8a16a053b736f2273c23419a2909564f33759e3756c9e657d8c22983f3f', + getVerifyChangeSkillTemplate: '67cfb0cc3e235088d2252821bb079fdc4c8a76d212485f96f4501df626b91858', + getOpsxArchiveCommandTemplate: '9802eaf96658d20b64c66da589a259576d4f8972695af66932d11842fd5064da', + getOpsxOnboardCommandTemplate: 'ae2b3f0c8d9d9abd0230cb8d0fb2fb16657a7334bc52c4a80bffdbd39163634e', + getOpsxBulkArchiveCommandTemplate: '825803b6371e4f1c4094a0cac4070557be6aea2882d5b3257a06aa89b7c01ba4', + getOpsxVerifyCommandTemplate: '88cf94f36a2ee40c023a6fecf1d726346a3aba35ccf0df26661aa645077e9aa6', + getOpsxProposeSkillTemplate: 'a305a17b78c15d75ab8a07edbd4918509f9a10b6c8d74be344b598afefd1888a', + getOpsxProposeCommandTemplate: '2d3ea5361674742adb494ab95ccc7398ec9a658b713aaa797cdc01ce58efd0db', getFeedbackSkillTemplate: 'dabeb5e825b9349abc8156c3e7b8608f27987912a6d9bf47ef29addde6138133', - getUpdateChangeSkillTemplate: '3b81d34ad0d9b0761da380ae46adff62a05b6bdfec51499dac076783d7dd61af', - getOpsxUpdateCommandTemplate: 'a140dbd3a0bdad7246b946f15b488711e7bd7a053c8f37ad4d6ffa5f3055c327', + getUpdateChangeSkillTemplate: 'f1d86841879599206205ecab5e5755fcce4adfd55c627ec912ddcd9cc1ca6d61', + getOpsxUpdateCommandTemplate: 'c8c75a96d58372aa6bb2fe7a5974dfc3faa3f9efa2a0d1bcbfe5e94f0bc9c583', }; const EXPECTED_GENERATED_SKILL_CONTENT_HASHES: Record = { - 'openspec-explore': 'eda0b87e39f88e058847435ea695a678add131785859713c980c878a15e2c185', - 'openspec-new-change': '24fceefc3ee83410f149785f3ed3d06158a9033d9ad061aa48751865fd61a50a', - 'openspec-continue-change': '5acedb5c7354f1a271d9a24c43c767b6d1acf6d0108acf44fd5882be044ee4c5', - 'openspec-apply-change': '654b648009488e3208d8b575a22801bc4708d1bef4ccfa672864a7cace4e5359', - 'openspec-ff-change': '232bdfb9722d46864ab6945cba376966e9de4758a404bf91844c4a9212b3099f', - 'openspec-sync-specs': '14325d79a9a513bdf6d06ad9777556bae91b3dedfb72f010d46a823423402c36', - 'openspec-archive-change': '23c542396acf76295879ad9359efc07cb79a6ae554b7bbf08481f67d902bacb2', - 'openspec-bulk-archive-change': '08363c6efb445292eb071b23cd7ba56f52b736d0db8d2c0612d63d80e720af1a', - 'openspec-verify-change': 'ecfc092f113c69f56607c9a8cc29f417c324842667f64badb4695ce6fdae71d4', - 'openspec-onboard': 'b0b6e4679bce217a9e162d7e041af6c323ee50b8c9e939932279abbc3c034a07', - 'openspec-propose': 'cfea2589c5aa3374fb33aa047e54e48b97f89c1363bd4c998fc6a6ea1a605a78', - 'openspec-update-change': '399ef2c1c4596fd14301601ac2181600b15a42fa17e2c11ea4455ed6d6789a9c', + 'openspec-explore': '17dd543e9f5c6423b5a49e072cf892a4407222471ea9c078fd3cf7d5fdd6f7c6', + 'openspec-new-change': '81fe69229f937533174416b3d7d4198efbd38fd0f6f156e38802c13897dda18b', + 'openspec-continue-change': '7fe5bd31eeecc7903b151f6f2570dde5bb22fabf332fd0f07c2f9c18da683bcb', + 'openspec-apply-change': '995c743167b444ee7ab7b5d31617ec52e76932e8f3849614520a76b3c3b51d67', + 'openspec-ff-change': 'b64d7524aa6186d5cd901025bd249b69ba5a6bb6dfbd3f375743f1f826301654', + 'openspec-sync-specs': 'c18400af26bfe0b50519fc15857a51da2b3c1c0a6e26613c9ad9990752e5cef3', + 'openspec-archive-change': '277192140e429ac8a63bd862bbfacdcbc5fc77ad1e5a1db285ab20275c8a4cf3', + 'openspec-bulk-archive-change': '936cfe0674a2ddb2886b51eb47c0c5dad925876e62ae96a96e3e8e914032dd8b', + 'openspec-verify-change': '090ad09a9f74d52e47159cd4f8831f91f87302575283e950aa24936575842b12', + 'openspec-onboard': '282463283b5f6cd4857fdcad0098de2fa08ab6136cd7e306bf9142d46697fee3', + 'openspec-propose': '54a623ded66f025dbd3a4a0c30ea3e832bf7c3dd1ef1f36e280f99a0fdea93df', + 'openspec-update-change': '07b4273f432d658cbfe1a6700f49ca80707a0b19f853ddc6d36aab197a0d3db9', }; // Intentionally excludes getFeedbackSkillTemplate: this list only models templates From c3f581c920d372b50bb6cc8448b5d93e527cbfe9 Mon Sep 17 00:00:00 2001 From: Clay Good Date: Tue, 15 Sep 2026 08:56:23 -0500 Subject: [PATCH 15/15] 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 --- test/core/templates/project-root-guard.test.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/test/core/templates/project-root-guard.test.ts b/test/core/templates/project-root-guard.test.ts index 0c42f151ca..05f417fd62 100644 --- a/test/core/templates/project-root-guard.test.ts +++ b/test/core/templates/project-root-guard.test.ts @@ -156,7 +156,11 @@ describe('project root guard', () => { const guardStart = body.indexOf(PROJECT_ROOT_GUARD); expect(guardStart, label).toBeGreaterThanOrEqual(0); - const beforeGuard = body.slice(0, guardStart); + // A skill's YAML frontmatter is metadata a host reads to pick the skill, + // not instructions the agent runs, so a description may quote a command + // name without running it. Only the body after the frontmatter is guarded. + const frontmatter = /^---\n[\s\S]*?\n---\n/.exec(body)?.[0] ?? ''; + const beforeGuard = body.slice(frontmatter.length, guardStart); for (const marker of writeMarkers) { expect(beforeGuard, `${label} runs "${marker}" before the project check`).not.toContain( marker