From 8d3415eef69a096054d263c3da4faf5958d4896e Mon Sep 17 00:00:00 2001 From: Patodo Date: Sun, 9 Aug 2026 20:39:10 +0900 Subject: [PATCH 1/5] docs(openspec): propose schemas root selection fix --- .../fix-schemas-root-selection/.openspec.yaml | 2 + .../fix-schemas-root-selection/design.md | 79 ++++++++++++++++++ .../fix-schemas-root-selection/proposal.md | 28 +++++++ .../specs/schema-resolution/spec.md | 80 +++++++++++++++++++ .../fix-schemas-root-selection/tasks.md | 22 +++++ 5 files changed, 211 insertions(+) create mode 100644 openspec/changes/fix-schemas-root-selection/.openspec.yaml create mode 100644 openspec/changes/fix-schemas-root-selection/design.md create mode 100644 openspec/changes/fix-schemas-root-selection/proposal.md create mode 100644 openspec/changes/fix-schemas-root-selection/specs/schema-resolution/spec.md create mode 100644 openspec/changes/fix-schemas-root-selection/tasks.md diff --git a/openspec/changes/fix-schemas-root-selection/.openspec.yaml b/openspec/changes/fix-schemas-root-selection/.openspec.yaml new file mode 100644 index 0000000000..d77f64e53a --- /dev/null +++ b/openspec/changes/fix-schemas-root-selection/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-08-09 diff --git a/openspec/changes/fix-schemas-root-selection/design.md b/openspec/changes/fix-schemas-root-selection/design.md new file mode 100644 index 0000000000..a63c69ca99 --- /dev/null +++ b/openspec/changes/fix-schemas-root-selection/design.md @@ -0,0 +1,79 @@ +## Context + +See `proposal.md` for motivation and `specs/schema-resolution/spec.md` for the behavioral contract. + +The pre-fix CLI has two already-compatible pieces that are not connected: + +- `schemasCommand()` passes `process.cwd()` directly to `listSchemasWithInfo()`. +- `listSchemasWithInfo(projectRoot)` already lists the correct project-local, user, and package schemas when given an authoritative project root. +- Normal root-scoped commands already call `resolveRootForCommand()`, which implements explicit store, nearest root, local `store:` pointer, global `defaultStore`, rootless fallback, canonicalization, and shared diagnostics. + +The mismatch was reproduced against the built CLI with distinct `local-only` and `store-only` schemas. From the local project, `schemas --json` returned `local-only` and omitted `store-only`, while `context --json --store team-context` resolved the operation root to the store. The relevant pre-fix test baseline passes (110 tests), so the reproduction is not caused by an existing failing suite. + +## Goals / Non-Goals + +**Goals:** + +- Make schema discovery and schema consumption resolve the same root. +- Carry explicit store selection through a supported CLI flag. +- Reuse the canonical root-selection implementation and its diagnostics. +- Preserve successful schema-list output compatibility and cross-platform path handling. + +**Non-Goals:** + +- Change schema resolution precedence within a resolved root. +- Change schema descriptions, semantic selection policy, or generated workflow skills. +- Add a raw filesystem-root flag or expose a resolved path in successful JSON output. +- Modify `context`, `templates`, change creation, or the root resolver itself. +- Refactor the existing `propose` workflow workaround in this fix. + +## Decisions + +### 1. Resolve the root at the CLI command boundary + +`schemasCommand()` will accept the standard store selector fields and call `resolveRootForCommand()` before invoking `listSchemasWithInfo(root.path)`. This is the same boundary used by `status` and other root-scoped workflow commands. + +Resolving inside `listSchemasWithInfo()` was rejected because that function is also a programmatic API with intentional backward-compatible behavior when `projectRoot` is omitted. Root selection is a CLI/session concern; schema enumeration should remain a pure operation over the root it receives. + +### 2. Add the standard store option and rejection path + +The Commander registration for `schemas` will add `--store ` using `COMMON_FLAGS.store` and the shared hidden `--store-path` option. `SchemasOptions` will carry `store` and `storePath`, and command-completion metadata will add the same common store flag. + +A raw `--root` or `--cwd` flag was rejected because it would bypass registry validation, store identity checks, canonicalization, and existing diagnostics. Asking an Agent to run `cd && openspec schemas` was rejected because generated tool permissions and working-directory support differ across Agents. + +### 3. Preserve canonical root precedence without a schemas-specific fallback + +The command will use `resolveRootForCommand()` unchanged: + +1. Explicit `--store`. +2. Nearest OpenSpec root, including resolution of a config-only `store:` pointer. +3. Global `defaultStore` when no nearer root exists. +4. An implicit current-directory root only when no root or registered-store selection is available. + +Invalid pointers, stale defaults, unknown stores, and the presence of unselected registered stores remain fail-closed. Adding a schemas-only catch-and-fallback path was rejected because it would recreate the mismatch this change removes. + +### 4. Preserve success output; use the existing JSON failure contract + +Successful human output remains the current listing, and successful JSON remains the top-level schema array. No root metadata is added, avoiding a breaking output-shape change. + +When root resolution fails under `--json`, the existing command adapter will emit one machine-readable failure document with an empty schema list, null root, and the shared status diagnostic. Human mode keeps the standard root banner and error/fix presentation used by other commands. + +### 5. Test the user-visible command, not an implementation mock + +A focused CLI suite will construct real temporary roots and registered stores with distinct valid project-local schemas. It will exercise explicit store selection, local pointers, global defaults, nearest-root precedence, rootless compatibility, fail-closed errors, paths with spaces, and the hidden removed option. Completion metadata gets a focused registry assertion. + +The tests will use Node path utilities and canonical fixture helpers, following `test/AGENTS.md`; no path identity assertion will compare non-canonical spellings. + +## Risks / Trade-offs + +- **Users with registered stores but no selected root can no longer use `schemas` as an unscoped built-in-only listing.** → Return the same actionable selection diagnostic as other root-scoped commands; selecting a store or entering a root makes the result authoritative. +- **Adding root resolution introduces new JSON failure paths.** → Assert one-document failure output and non-zero exit behavior explicitly. +- **Store roots containing spaces or platform-specific separators could expose path assumptions.** → Resolve paths internally and add a real CLI fixture with a spaced store path; never compose a shell command. +- **The feature PR still needs to pass `--store` when it explicitly selects one.** → Keep that integration change in the feature PR after this independent CLI fix merges; do not mix generated skill changes into this branch. + +## Migration Plan + +1. Ship the root-aware `schemas` command and `--store` option. +2. Update dependent workflow guidance in its own branch to pass `--store` when selected. +3. Existing successful unscoped output remains compatible; scripts targeting a registered store should add `--store `. +4. Rollback removes the option and returns `schemasCommand()` to `process.cwd()` without changing schema files or registered-store state. diff --git a/openspec/changes/fix-schemas-root-selection/proposal.md b/openspec/changes/fix-schemas-root-selection/proposal.md new file mode 100644 index 0000000000..b59bffaef6 --- /dev/null +++ b/openspec/changes/fix-schemas-root-selection/proposal.md @@ -0,0 +1,28 @@ +## Why + +`openspec schemas` discovers project-local schemas from the shell's current directory, while commands that consume a schema resolve an authoritative OpenSpec root first. When an explicit store, a local `store:` pointer, or `defaultStore` selects a different root, discovery can recommend a schema that is unavailable where the change will actually be created. Agents currently have to work around this mismatch by resolving a path and trying to change their shell working directory, which is not reliable across supported tools. + +## What Changes + +- Make `openspec schemas` resolve its project root through the same root-selection contract used by normal OpenSpec commands before listing schemas. +- Add `--store ` to `openspec schemas`, including the standard hidden `--store-path` rejection path, so explicit store selection is carried directly by the CLI. +- Honor nearest roots, local `store:` pointers, and global `defaultStore` using existing precedence and diagnostics; do not add a parallel schema-specific root resolver. +- Preserve the successful human and JSON schema-list output shapes and the existing rootless fallback when no root or registered store exists. +- Add CLI regression coverage for explicit stores, declared pointers, global defaults, nearest-root precedence, error handling, and completion metadata. + +## Capabilities + +### New Capabilities + +None. + +### Modified Capabilities + +- `schema-resolution`: `openspec schemas` resolves and lists schemas from the authoritative OpenSpec root, including explicitly selected and configured stores. + +## Impact + +- Affected CLI surface: `openspec schemas [--json] [--store ]`. +- Affected code: workflow schemas command, CLI option registration, and command completion metadata. +- Affected tests: a focused schemas command suite plus CLI/completion regression coverage. +- No schema format, selection policy, generated skill, or change-creation behavior is modified in this fix. diff --git a/openspec/changes/fix-schemas-root-selection/specs/schema-resolution/spec.md b/openspec/changes/fix-schemas-root-selection/specs/schema-resolution/spec.md new file mode 100644 index 0000000000..eedbba0758 --- /dev/null +++ b/openspec/changes/fix-schemas-root-selection/specs/schema-resolution/spec.md @@ -0,0 +1,80 @@ +## ADDED Requirements + +### Requirement: Schemas command SHALL honor authoritative root selection + +`openspec schemas` SHALL resolve the authoritative OpenSpec root with the same precedence and diagnostics as other root-scoped commands, then list schemas using that root. The command SHALL accept `--store ` for explicit registered-store selection. Successful human output and successful `--json` output SHALL retain their existing formats. + +#### Scenario: Nearest project root supplies schemas + +- **GIVEN** the current directory is inside an OpenSpec root containing a project-local schema +- **WHEN** the user runs `openspec schemas --json` +- **THEN** the result SHALL include that root's project-local schema + +#### Scenario: Explicit store overrides the current project + +- **GIVEN** the current project and a registered store contain different project-local schemas +- **WHEN** the user runs `openspec schemas --json --store ` +- **THEN** the result SHALL include schemas from the selected store root +- **AND** it SHALL NOT include schemas that exist only in the current project + +#### Scenario: Local store pointer supplies schemas + +- **GIVEN** the nearest `openspec/config.yaml` is a config-only root declaring `store: ` +- **WHEN** the user runs `openspec schemas --json` without an explicit store flag +- **THEN** the result SHALL include schemas from the declared store root + +#### Scenario: Global default store supplies schemas + +- **GIVEN** no nearer OpenSpec root or pointer exists +- **AND** global configuration declares `defaultStore: ` +- **WHEN** the user runs `openspec schemas --json` +- **THEN** the result SHALL include schemas from the default store root + +#### Scenario: Explicit store preserves root-selection precedence + +- **GIVEN** a nearest project root, a global default store, and an explicitly selected registered store all exist +- **WHEN** the user runs `openspec schemas --json --store ` +- **THEN** the explicitly selected store SHALL supply the project-local schemas + +#### Scenario: Nearest root precedes the global default + +- **GIVEN** a nearest project root and a global default store contain different schemas +- **WHEN** the user runs `openspec schemas --json` without `--store` +- **THEN** the nearest project root SHALL supply the project-local schemas + +#### Scenario: Rootless listing remains available without registered stores + +- **GIVEN** no OpenSpec root, pointer, global default, or registered store exists +- **WHEN** the user runs `openspec schemas --json` +- **THEN** the command SHALL list user and package schemas using the current directory as its implicit root, as before + +#### Scenario: Registered stores require an authoritative selection + +- **GIVEN** no OpenSpec root, pointer, or global default exists +- **AND** one or more stores are registered +- **WHEN** the user runs `openspec schemas --json` without `--store` +- **THEN** the command SHALL fail with the standard root-selection diagnostic that asks the user to select a registered store +- **AND** it SHALL NOT silently list schemas from the current directory + +#### Scenario: Invalid or unavailable store fails closed + +- **WHEN** explicit, declared, or global-default store resolution fails +- **THEN** `openspec schemas` SHALL report the existing root-selection diagnostic and exit non-zero +- **AND** it SHALL NOT fall back to schemas from the current directory + +#### Scenario: Removed store-path option is rejected deliberately + +- **WHEN** the user runs `openspec schemas --store-path ` +- **THEN** the command SHALL reject the removed option with the standard instruction to register the store and use `--store ` + +#### Scenario: Success output remains compatible + +- **WHEN** root resolution succeeds +- **THEN** human output SHALL retain the existing schema listing and source labels +- **AND** `--json` output SHALL remain the existing top-level array of schema information + +#### Scenario: Store path works across supported platforms + +- **GIVEN** the selected store root uses a valid platform-native path, including a path containing spaces +- **WHEN** the user runs `openspec schemas --json --store ` +- **THEN** the command SHALL list schemas from that store without requiring the user or an Agent to compose a shell `cd` command diff --git a/openspec/changes/fix-schemas-root-selection/tasks.md b/openspec/changes/fix-schemas-root-selection/tasks.md new file mode 100644 index 0000000000..d25def4f99 --- /dev/null +++ b/openspec/changes/fix-schemas-root-selection/tasks.md @@ -0,0 +1,22 @@ +## 1. Lock the root-selection regression with CLI tests + +- [ ] 1.1 Add `test/commands/schemas.test.ts` with real temporary local and registered-store roots, valid distinct project schemas, isolated XDG data/config homes, canonical cleanup, and a store path containing spaces. +- [ ] 1.2 Add failing cases proving `schemas --json --store ` returns the store-only schema rather than the cwd-only schema, and `schemas --store-path ` reaches the deliberate removed-option diagnostic. +- [ ] 1.3 Add failing cases proving config-only `store:` and global `defaultStore` roots supply schemas without a flag, while a nearest real root wins over `defaultStore`. +- [ ] 1.4 Add failing cases for rootless compatibility, unselected registered-store failure, invalid/unavailable store failure, one-document JSON diagnostics, and unchanged successful array output. +- [ ] 1.5 Extend `test/core/completions/command-registry.test.ts` to require the common `store` flag on the `schemas` definition. +- [ ] 1.6 Run `pnpm exec vitest run test/commands/schemas.test.ts test/core/completions/command-registry.test.ts` and verify the new tests fail only because `schemas` lacks authoritative root selection and `--store` support. + +## 2. Implement canonical schemas root selection + +- [ ] 2.1 Extend `SchemasOptions` in `src/commands/workflow/schemas.ts` with `store` and `storePath`, resolve through `resolveRootForCommand()`, return on a JSON resolution failure, and pass `root.path` to `listSchemasWithInfo()`. +- [ ] 2.2 Update the `schemas` registration in `src/cli/index.ts` with `--store `, the shared hidden `--store-path` option, and JSON-aware failure handling without changing successful output shapes. +- [ ] 2.3 Add `COMMON_FLAGS.store` to the `schemas` entry in `src/core/completions/command-registry.ts`; do not modify generated workflow templates or skills. +- [ ] 2.4 Run `pnpm run build`, then rerun `pnpm exec vitest run test/commands/schemas.test.ts test/core/completions/command-registry.test.ts` and verify all root, error, output-compatibility, and completion cases pass. + +## 3. Regression and cross-platform verification + +- [ ] 3.1 Run `pnpm exec vitest run test/cli-e2e/basic.test.ts test/commands/context.test.ts test/commands/global-default-store.test.ts test/core/root-selection.test.ts test/core/artifact-graph/resolver.test.ts` to verify adjacent root and schema behavior. +- [ ] 3.2 Run `pnpm run lint`, `pnpm run build`, and `pnpm test`; confirm no successful `schemas` output regression and no changes outside the scoped CLI, tests, and proposal files. +- [ ] 3.3 Run `pnpm exec openspec validate fix-schemas-root-selection --strict` and `git diff --check`. +- [ ] 3.4 Verify the focused schemas suite on Windows CI, specifically the spaced native store path and absence of hard-coded path separators. From bfa7a8ef6878503e06fe8e48d371c6f4e7a288dc Mon Sep 17 00:00:00 2001 From: Patodo Date: Sun, 9 Aug 2026 21:09:01 +0900 Subject: [PATCH 2/5] fix(schemas): honor canonical root selection --- docs/agent-contract.md | 9 +- docs/cli.md | 3 +- docs/stores-beta/user-guide.md | 8 +- .../fix-schemas-root-selection/design.md | 10 +- .../fix-schemas-root-selection/proposal.md | 5 +- .../fix-schemas-root-selection/tasks.md | 26 +-- 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 | 4 +- skills/openspec-sync-specs/SKILL.md | 2 +- skills/openspec-update-change/SKILL.md | 2 +- skills/openspec-verify-change/SKILL.md | 2 +- src/cli/index.ts | 8 +- src/commands/workflow/schemas.ts | 14 +- src/core/completions/command-registry.ts | 1 + src/core/templates/workflows/propose.ts | 4 +- .../templates/workflows/store-selection.ts | 2 +- test/commands/schemas.test.ts | 216 ++++++++++++++++++ .../core/completions/command-registry.test.ts | 12 +- test/core/templates/propose.test.ts | 5 +- 26 files changed, 300 insertions(+), 49 deletions(-) create mode 100644 test/commands/schemas.test.ts diff --git a/docs/agent-contract.md b/docs/agent-contract.md index 63e469e48f..7c870f3a12 100644 --- a/docs/agent-contract.md +++ b/docs/agent-contract.md @@ -27,7 +27,7 @@ Diagnostics appear in two positions: **status arrays** (`status: StoreDiagnostic ## 3. Root selection and `RootOutput` -All root-resolving commands (`list`, `show`, `validate`, `status`, `instructions`, `instructions apply`, `instructions archive`, `new change`, `archive`, `doctor`, `context`) resolve one OpenSpec root with one precedence: +All root-resolving commands (`list`, `show`, `validate`, `status`, `instructions`, `instructions apply`, `instructions archive`, `new change`, `archive`, `doctor`, `context`, `schemas`) resolve one OpenSpec root with one precedence: 1. `--store ` → the registered store's root (`source: "store"`). 2. Otherwise, nearest ancestor with `openspec/`: planning shape → `source: "nearest"` (a `store:` pointer is ignored with a stderr warning); config-only dir with a valid `store:` pointer → that store, `source: "declared"`. @@ -35,7 +35,8 @@ All root-resolving commands (`list`, `show`, `validate`, `status`, `instructions 4. No nearest root, no default + registered stores exist → error `no_root_with_registered_stores`. 5. No root, no default, no stores: scaffolding commands treat the cwd as `source: "implicit"`; diagnostic commands (`doctor`, `context`) fail with `no_openspec_root` instead — they inspect, never scaffold. -Successful JSON payloads embed the root: +Successful JSON payloads normally embed the root; successful `schemas --json` +deliberately remains the compatibility bare array documented in §4.13: ```json "root": { "path": "/abs/path", "source": "store" | "declared" | "global_default" | "nearest" | "implicit", "store_id": "id (only when store-selected)" } @@ -84,7 +85,7 @@ Success: `{ "archive": { "change", "archivedAs": "YYYY-MM-DD-name", "path", "spe setup/register: `{ "store": {id, root, metadata_path?}, "registry": {path, registered, already_registered}, "git": {is_repository, initialized, committed}, "created_files": [], "status": [] }`. unregister/remove: `{ "store", "registry": {path, removed}, "files": {deleted, deleted_path, left_on_disk}, "status": [] }`. list: `{ "stores": [{id, root}], "status": [] }`. doctor: `{ "stores": [ { id, root, metadata_path?, openspec_root: {...healthy, status}, metadata: {present, valid, id?, remote}, git: {is_repository, has_commits, has_uncommitted_changes, has_remote, origin_url}, status } ], "status": [] }` (`null` = unknown/not probed). Health findings exit 0; failures exit 1 with the matching null-shape. Prompt cancellation exits 130. ### 4.13 `schemas --json` / `templates --json` -`schemas`: bare array `[ {name, description, artifacts, source} ]`. `templates`: keyed object `{ "": {path, source} }`. Both cwd-based, no root/status keys. +`schemas`: success remains a bare array `[ {name, description, artifacts, source} ]`; it resolves the canonical root-selection precedence and accepts `--store `. Root-selection failure: `{ "schemas": [], "root": null, "status": [d] }`, exit 1. `templates`: keyed object `{ "": {path, source} }`, still cwd-based with no root/status keys. ## 5. Exit-code contract @@ -137,5 +138,5 @@ Recorded by the capstone audit; published-key renames are product decisions defe 4. Four parallel envelope type declarations exist in src; archive diagnostics never carry `target`. 5. `list --json` reuses the `status` key as a string enum per change. 6. Only `validate` output carries a `version` field. -7. `schemas`/`templates` ignore root selection (cwd-based, no `--store`). +7. `templates` ignores root selection (cwd-based, no `--store`). 8. Deprecated noun forms (`change`/`spec` subcommands) emit unenveloped payloads without `root`/`status`. diff --git a/docs/cli.md b/docs/cli.md index d00d30382b..d17c6d662f 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -50,7 +50,7 @@ These commands support `--json` output for programmatic use by AI agents and scr | `openspec status` | See artifact progress | `--json` for structured status | | `openspec instructions` | Get next steps | `--json` for agent instructions | | `openspec templates` | Find template paths | `--json` for path resolution | -| `openspec schemas` | List available schemas | `--json` for schema discovery | +| `openspec schemas` | List available schemas | `--json` for schema discovery; `--store ` to select a registered root | | `openspec store setup ` | Create and register a local store | `--json` with explicit inputs for structured setup output | | `openspec store register ` | Register an existing store | `--json` for structured registration output | | `openspec store unregister ` | Forget a local store registration | `--json` for structured cleanup output | @@ -910,6 +910,7 @@ openspec schemas [options] | Option | Description | |--------|-------------| | `--json` | Output as JSON | +| `--store ` | Use a registered store as the OpenSpec root | **Example:** diff --git a/docs/stores-beta/user-guide.md b/docs/stores-beta/user-guide.md index 4a4db4ccc3..6353e14022 100644 --- a/docs/stores-beta/user-guide.md +++ b/docs/stores-beta/user-guide.md @@ -427,9 +427,11 @@ tells you which case you're in. `openspec/config.yaml` declares `store: ` is treated as externalized planning, not as a store checkout to register. Remove the `store:` line first if you intentionally want to convert that repo into a local store root. -- **Some commands stay where they are.** `view`, `templates`, `schemas`, - and the deprecated noun forms (`openspec change show`, ...) act on the - current directory only — no `--store`. +- **Some commands stay where they are.** `view`, `templates`, and the + deprecated noun forms (`openspec change show`, ...) act on the current + directory only — no `--store`. `schemas` follows the canonical root-selection + precedence and accepts `--store ` while keeping its successful JSON array + shape unchanged. - **Per-machine state is per-machine.** The store registry and worksets are local settings. Nothing about your machine's layout is ever committed to shared planning. diff --git a/openspec/changes/fix-schemas-root-selection/design.md b/openspec/changes/fix-schemas-root-selection/design.md index a63c69ca99..d3dafa37b5 100644 --- a/openspec/changes/fix-schemas-root-selection/design.md +++ b/openspec/changes/fix-schemas-root-selection/design.md @@ -22,10 +22,10 @@ The mismatch was reproduced against the built CLI with distinct `local-only` and **Non-Goals:** - Change schema resolution precedence within a resolved root. -- Change schema descriptions, semantic selection policy, or generated workflow skills. +- Change schema descriptions, semantic selection policy, or workflow-specific behavior beyond correcting stale `schemas --store` guidance. - Add a raw filesystem-root flag or expose a resolved path in successful JSON output. - Modify `context`, `templates`, change creation, or the root resolver itself. -- Refactor the existing `propose` workflow workaround in this fix. +- Refactor the existing `propose` compatibility sequence; only its stale flag-support claim changes. ## Decisions @@ -37,7 +37,7 @@ Resolving inside `listSchemasWithInfo()` was rejected because that function is a ### 2. Add the standard store option and rejection path -The Commander registration for `schemas` will add `--store ` using `COMMON_FLAGS.store` and the shared hidden `--store-path` option. `SchemasOptions` will carry `store` and `storePath`, and command-completion metadata will add the same common store flag. +The Commander registration for `schemas` will add `--store ` using `COMMON_FLAGS.store` and the shared hidden `--store-path` option. `SchemasOptions` will carry `store` and `storePath`, and command-completion metadata will add the same common store flag. Because the repository enforces that every command exposing `--store` is named by the shared store-selection guidance, that shared command list, committed generated skill snapshots, and generated-content parity hashes will be updated to include `schemas`. Formal CLI/JSON agent-contract references will be synchronized, and the existing `propose` compatibility flow will only lose its now-false assertion that `schemas` cannot accept the flag; its root-resolution sequence remains unchanged. A raw `--root` or `--cwd` flag was rejected because it would bypass registry validation, store identity checks, canonicalization, and existing diagnostics. Asking an Agent to run `cd && openspec schemas` was rejected because generated tool permissions and working-directory support differ across Agents. @@ -69,11 +69,11 @@ The tests will use Node path utilities and canonical fixture helpers, following - **Users with registered stores but no selected root can no longer use `schemas` as an unscoped built-in-only listing.** → Return the same actionable selection diagnostic as other root-scoped commands; selecting a store or entering a root makes the result authoritative. - **Adding root resolution introduces new JSON failure paths.** → Assert one-document failure output and non-zero exit behavior explicitly. - **Store roots containing spaces or platform-specific separators could expose path assumptions.** → Resolve paths internally and add a real CLI fixture with a spaced store path; never compose a shell command. -- **The feature PR still needs to pass `--store` when it explicitly selects one.** → Keep that integration change in the feature PR after this independent CLI fix merges; do not mix generated skill changes into this branch. +- **The feature PR still needs to integrate its schema-selection flow with explicit store choice.** → This fix synchronizes shared guidance and the existing `propose` compatibility wording, but leaves feature-specific selection/confirmation behavior to that branch after this independent CLI fix merges. ## Migration Plan 1. Ship the root-aware `schemas` command and `--store` option. -2. Update dependent workflow guidance in its own branch to pass `--store` when selected. +2. Update dependent feature-specific schema-selection guidance in its own branch; the shared store-capable command list and existing `propose` compatibility wording already support `schemas --store` after this fix. 3. Existing successful unscoped output remains compatible; scripts targeting a registered store should add `--store `. 4. Rollback removes the option and returns `schemasCommand()` to `process.cwd()` without changing schema files or registered-store state. diff --git a/openspec/changes/fix-schemas-root-selection/proposal.md b/openspec/changes/fix-schemas-root-selection/proposal.md index b59bffaef6..7350e52560 100644 --- a/openspec/changes/fix-schemas-root-selection/proposal.md +++ b/openspec/changes/fix-schemas-root-selection/proposal.md @@ -9,6 +9,7 @@ - Honor nearest roots, local `store:` pointers, and global `defaultStore` using existing precedence and diagnostics; do not add a parallel schema-specific root resolver. - Preserve the successful human and JSON schema-list output shapes and the existing rootless fallback when no root or registered store exists. - Add CLI regression coverage for explicit stores, declared pointers, global defaults, nearest-root precedence, error handling, and completion metadata. +- Update the shared store-capable command guidance, committed generated skill snapshots, and generated-content parity hashes to name `schemas`, plus the formal CLI and JSON agent-contract references; preserve the existing `propose` compatibility flow while removing its now-false claim that `schemas` cannot accept `--store`. ## Capabilities @@ -23,6 +24,6 @@ None. ## Impact - Affected CLI surface: `openspec schemas [--json] [--store ]`. -- Affected code: workflow schemas command, CLI option registration, and command completion metadata. +- Affected code: workflow schemas command, CLI option registration, command completion metadata, the shared store-capable command list, and directly affected command-contract documentation. - Affected tests: a focused schemas command suite plus CLI/completion regression coverage. -- No schema format, selection policy, generated skill, or change-creation behavior is modified in this fix. +- No schema format, selection policy, workflow-specific flow, or change-creation behavior is modified; the only workflow-specific wording change corrects the stale claim that `schemas` cannot accept `--store`. diff --git a/openspec/changes/fix-schemas-root-selection/tasks.md b/openspec/changes/fix-schemas-root-selection/tasks.md index d25def4f99..17d4197804 100644 --- a/openspec/changes/fix-schemas-root-selection/tasks.md +++ b/openspec/changes/fix-schemas-root-selection/tasks.md @@ -1,22 +1,22 @@ ## 1. Lock the root-selection regression with CLI tests -- [ ] 1.1 Add `test/commands/schemas.test.ts` with real temporary local and registered-store roots, valid distinct project schemas, isolated XDG data/config homes, canonical cleanup, and a store path containing spaces. -- [ ] 1.2 Add failing cases proving `schemas --json --store ` returns the store-only schema rather than the cwd-only schema, and `schemas --store-path ` reaches the deliberate removed-option diagnostic. -- [ ] 1.3 Add failing cases proving config-only `store:` and global `defaultStore` roots supply schemas without a flag, while a nearest real root wins over `defaultStore`. -- [ ] 1.4 Add failing cases for rootless compatibility, unselected registered-store failure, invalid/unavailable store failure, one-document JSON diagnostics, and unchanged successful array output. -- [ ] 1.5 Extend `test/core/completions/command-registry.test.ts` to require the common `store` flag on the `schemas` definition. -- [ ] 1.6 Run `pnpm exec vitest run test/commands/schemas.test.ts test/core/completions/command-registry.test.ts` and verify the new tests fail only because `schemas` lacks authoritative root selection and `--store` support. +- [x] 1.1 Add `test/commands/schemas.test.ts` with real temporary local and registered-store roots, valid distinct project schemas, isolated XDG data/config homes, canonical cleanup, and a store path containing spaces. +- [x] 1.2 Add failing cases proving `schemas --json --store ` returns the store-only schema rather than the cwd-only schema, and `schemas --store-path ` reaches the deliberate removed-option diagnostic. +- [x] 1.3 Add failing cases proving config-only `store:` and global `defaultStore` roots supply schemas without a flag, while a nearest real root wins over `defaultStore`. +- [x] 1.4 Add failing cases for rootless compatibility, unselected registered-store failure, invalid/unavailable store failure, one-document JSON diagnostics, and unchanged successful array output. +- [x] 1.5 Extend `test/core/completions/command-registry.test.ts` to require the common `store` flag on the `schemas` definition and require the shared store-selection guidance to name it. +- [x] 1.6 Run `pnpm exec vitest run test/commands/schemas.test.ts test/core/completions/command-registry.test.ts` and verify the new tests fail only because `schemas` lacks authoritative root selection and `--store` support. ## 2. Implement canonical schemas root selection -- [ ] 2.1 Extend `SchemasOptions` in `src/commands/workflow/schemas.ts` with `store` and `storePath`, resolve through `resolveRootForCommand()`, return on a JSON resolution failure, and pass `root.path` to `listSchemasWithInfo()`. -- [ ] 2.2 Update the `schemas` registration in `src/cli/index.ts` with `--store `, the shared hidden `--store-path` option, and JSON-aware failure handling without changing successful output shapes. -- [ ] 2.3 Add `COMMON_FLAGS.store` to the `schemas` entry in `src/core/completions/command-registry.ts`; do not modify generated workflow templates or skills. -- [ ] 2.4 Run `pnpm run build`, then rerun `pnpm exec vitest run test/commands/schemas.test.ts test/core/completions/command-registry.test.ts` and verify all root, error, output-compatibility, and completion cases pass. +- [x] 2.1 Extend `SchemasOptions` in `src/commands/workflow/schemas.ts` with `store` and `storePath`, resolve through `resolveRootForCommand()`, return on a JSON resolution failure, and pass `root.path` to `listSchemasWithInfo()`. +- [x] 2.2 Update the `schemas` registration in `src/cli/index.ts` with `--store `, the shared hidden `--store-path` option, and JSON-aware failure handling without changing successful output shapes. +- [x] 2.3 Add `COMMON_FLAGS.store` to the `schemas` entry in `src/core/completions/command-registry.ts`, add `schemas` to the shared store-capable command guidance, synchronize committed generated skill snapshots and the formal CLI/JSON agent-contract references, remove the stale `propose` claim without changing its compatibility flow, and refresh generated-content parity hashes. +- [x] 2.4 Run `pnpm run build`, then rerun `pnpm exec vitest run test/commands/schemas.test.ts test/core/completions/command-registry.test.ts` and verify all root, error, output-compatibility, and completion cases pass. ## 3. Regression and cross-platform verification -- [ ] 3.1 Run `pnpm exec vitest run test/cli-e2e/basic.test.ts test/commands/context.test.ts test/commands/global-default-store.test.ts test/core/root-selection.test.ts test/core/artifact-graph/resolver.test.ts` to verify adjacent root and schema behavior. -- [ ] 3.2 Run `pnpm run lint`, `pnpm run build`, and `pnpm test`; confirm no successful `schemas` output regression and no changes outside the scoped CLI, tests, and proposal files. -- [ ] 3.3 Run `pnpm exec openspec validate fix-schemas-root-selection --strict` and `git diff --check`. +- [x] 3.1 Run `pnpm exec vitest run test/cli-e2e/basic.test.ts test/commands/context.test.ts test/commands/global-default-store.test.ts test/core/root-selection.test.ts test/core/artifact-graph/resolver.test.ts` to verify adjacent root and schema behavior. +- [x] 3.2 Run `pnpm run lint`, `pnpm run build`, and `pnpm test`; confirm no successful `schemas` output regression and no changes outside the scoped CLI, tests, generated guidance/documentation, and proposal files. +- [x] 3.3 Run `pnpm exec openspec validate fix-schemas-root-selection --strict` and `git diff --check`. - [ ] 3.4 Verify the focused schemas suite on Windows CI, specifically the spaced native store path and absence of hard-coded path separators. diff --git a/skills/openspec-apply-change/SKILL.md b/skills/openspec-apply-change/SKILL.md index e9dc213b6b..098f63fecb 100644 --- a/skills/openspec-apply-change/SKILL.md +++ b/skills/openspec-apply-change/SKILL.md @@ -11,7 +11,7 @@ metadata: 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`, `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. +**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. **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 80991b7fe9..5f34ed53a7 100644 --- a/skills/openspec-archive-change/SKILL.md +++ b/skills/openspec-archive-change/SKILL.md @@ -11,7 +11,7 @@ metadata: 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`, `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. +**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. `` 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 415b40396e..252e1dd155 100644 --- a/skills/openspec-bulk-archive-change/SKILL.md +++ b/skills/openspec-bulk-archive-change/SKILL.md @@ -13,7 +13,7 @@ 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:** 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`, `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. +**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. `` 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 37201adf90..5991b06891 100644 --- a/skills/openspec-continue-change/SKILL.md +++ b/skills/openspec-continue-change/SKILL.md @@ -11,7 +11,7 @@ metadata: 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`, `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. +**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. **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 b886a44dcb..327c261489 100644 --- a/skills/openspec-explore/SKILL.md +++ b/skills/openspec-explore/SKILL.md @@ -15,7 +15,7 @@ Enter explore mode. Think deeply. Visualize freely. Follow the conversation wher **This is a stance, not a workflow.** There are no fixed steps, no required sequence, no mandatory outputs. You're a thinking partner helping the user explore. -**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`, `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. +**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. --- diff --git a/skills/openspec-ff-change/SKILL.md b/skills/openspec-ff-change/SKILL.md index e88c416a16..c63aeee13b 100644 --- a/skills/openspec-ff-change/SKILL.md +++ b/skills/openspec-ff-change/SKILL.md @@ -11,7 +11,7 @@ metadata: Fast-forward through artifact creation - generate everything needed to start implementation in one go. -**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`, `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. +**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. **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 a103bb0748..9aea11d391 100644 --- a/skills/openspec-new-change/SKILL.md +++ b/skills/openspec-new-change/SKILL.md @@ -11,7 +11,7 @@ metadata: 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`, `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. +**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. **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 0d78693254..5619596ea1 100644 --- a/skills/openspec-onboard/SKILL.md +++ b/skills/openspec-onboard/SKILL.md @@ -11,7 +11,7 @@ metadata: Guide the user through their first complete OpenSpec workflow cycle. This is a teaching experience—you'll do real work in their codebase while explaining each step. -**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`, `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. +**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. --- diff --git a/skills/openspec-propose/SKILL.md b/skills/openspec-propose/SKILL.md index 0f4ec8a0c5..ccf7b22334 100644 --- a/skills/openspec-propose/SKILL.md +++ b/skills/openspec-propose/SKILL.md @@ -25,7 +25,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`, `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. +**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. **Input**: The user's request should include a change name (kebab-case) OR a description of what they want to build. @@ -48,7 +48,7 @@ When the user is ready to implement, they must start the apply workflow explicit **Use a different schema only if the user:** - Explicitly requests a specific schema by name → use `--schema ` - - Asks to "show workflows" or asks "what workflows" exist → resolve the authoritative root by running `openspec context --json` from the current working directory. If the user explicitly selected a registered store, use `openspec context --json --store ""`. Then run `openspec schemas --json` with its working directory set to the returned `root.path` and let them choose. This preserves roots selected by a local `store:` pointer or the global `defaultStore`; `schemas` does not accept `--store`. If context reports only `no_openspec_root`, run `openspec schemas --json` from the current working directory instead. Do not use this fallback for invalid or unavailable stores. + - Asks to "show workflows" or asks "what workflows" exist → resolve the authoritative root by running `openspec context --json` from the current working directory. If the user explicitly selected a registered store, use `openspec context --json --store ""`. Then run `openspec schemas --json` with its working directory set to the returned `root.path` and let them choose. This preserves roots selected by a local `store:` pointer or the global `defaultStore`; when a registered store was explicitly selected, append `--store ""` to `openspec schemas --json` as well. If context reports only `no_openspec_root`, run `openspec schemas --json` from the current working directory instead. Do not use this fallback for invalid or unavailable stores. Otherwise, omit `--schema` to preserve the configured default. diff --git a/skills/openspec-sync-specs/SKILL.md b/skills/openspec-sync-specs/SKILL.md index e0a36880a0..d12d56b857 100644 --- a/skills/openspec-sync-specs/SKILL.md +++ b/skills/openspec-sync-specs/SKILL.md @@ -13,7 +13,7 @@ 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:** 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`, `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. +**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. `` 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 77d2ed27b3..10ac7dd24c 100644 --- a/skills/openspec-update-change/SKILL.md +++ b/skills/openspec-update-change/SKILL.md @@ -11,7 +11,7 @@ metadata: Revise a change's existing planning artifacts and keep them coherent. Never edit code. -**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`, `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. +**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. **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 8e62355d05..2165a6a910 100644 --- a/skills/openspec-verify-change/SKILL.md +++ b/skills/openspec-verify-change/SKILL.md @@ -11,7 +11,7 @@ metadata: 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`, `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. +**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. **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/cli/index.ts b/src/cli/index.ts index 6772643c68..23898b843c 100644 --- a/src/cli/index.ts +++ b/src/cli/index.ts @@ -629,11 +629,17 @@ program .command('schemas') .description('List available workflow schemas with descriptions') .option('--json', 'Output as JSON (for agent use)') + .option('--store ', STORE_OPTION_DESCRIPTION) + .addOption(hiddenStorePathOption()) .action(async (options: SchemasOptions) => { try { await schemasCommand(options); } catch (error) { - failWithError(error); + failWithError(error, { + enabled: options.json, + payload: { schemas: [], root: null }, + fallbackCode: 'schemas_error', + }); process.exit(1); } }); diff --git a/src/commands/workflow/schemas.ts b/src/commands/workflow/schemas.ts index b9af74a677..0880e20c9a 100644 --- a/src/commands/workflow/schemas.ts +++ b/src/commands/workflow/schemas.ts @@ -6,6 +6,7 @@ import chalk from 'chalk'; import { listSchemasWithInfo } from '../../core/artifact-graph/index.js'; +import { resolveRootForCommand } from '../../core/root-selection.js'; // ----------------------------------------------------------------------------- // Types @@ -13,6 +14,8 @@ import { listSchemasWithInfo } from '../../core/artifact-graph/index.js'; export interface SchemasOptions { json?: boolean; + store?: string; + storePath?: string; } // ----------------------------------------------------------------------------- @@ -20,8 +23,15 @@ export interface SchemasOptions { // ----------------------------------------------------------------------------- export async function schemasCommand(options: SchemasOptions): Promise { - const projectRoot = process.cwd(); - const schemas = listSchemasWithInfo(projectRoot); + const root = await resolveRootForCommand(options, { + json: options.json, + failurePayload: { schemas: [], root: null }, + }); + if (!root) { + return; + } + + const schemas = listSchemasWithInfo(root.path); if (options.json) { console.log(JSON.stringify(schemas, null, 2)); diff --git a/src/core/completions/command-registry.ts b/src/core/completions/command-registry.ts index 0fd3c02bd9..8e6231499d 100644 --- a/src/core/completions/command-registry.ts +++ b/src/core/completions/command-registry.ts @@ -231,6 +231,7 @@ export const COMMAND_REGISTRY: CommandDefinition[] = [ description: 'List available workflow schemas with descriptions', flags: [ COMMON_FLAGS.json, + COMMON_FLAGS.store, ], }, { diff --git a/src/core/templates/workflows/propose.ts b/src/core/templates/workflows/propose.ts index a47c8ea64c..8e700c703d 100644 --- a/src/core/templates/workflows/propose.ts +++ b/src/core/templates/workflows/propose.ts @@ -50,7 +50,7 @@ ${STORE_SELECTION_GUIDANCE} **Use a different schema only if the user:** - Explicitly requests a specific schema by name → use \`--schema \` - - Asks to "show workflows" or asks "what workflows" exist → resolve the authoritative root by running \`openspec context --json\` from the current working directory. If the user explicitly selected a registered store, use \`openspec context --json --store ""\`. Then run \`openspec schemas --json\` with its working directory set to the returned \`root.path\` and let them choose. This preserves roots selected by a local \`store:\` pointer or the global \`defaultStore\`; \`schemas\` does not accept \`--store\`. If context reports only \`no_openspec_root\`, run \`openspec schemas --json\` from the current working directory instead. Do not use this fallback for invalid or unavailable stores. + - Asks to "show workflows" or asks "what workflows" exist → resolve the authoritative root by running \`openspec context --json\` from the current working directory. If the user explicitly selected a registered store, use \`openspec context --json --store ""\`. Then run \`openspec schemas --json\` with its working directory set to the returned \`root.path\` and let them choose. This preserves roots selected by a local \`store:\` pointer or the global \`defaultStore\`; when a registered store was explicitly selected, append \`--store ""\` to \`openspec schemas --json\` as well. If context reports only \`no_openspec_root\`, run \`openspec schemas --json\` from the current working directory instead. Do not use this fallback for invalid or unavailable stores. Otherwise, omit \`--schema\` to preserve the configured default. @@ -199,7 +199,7 @@ ${STORE_SELECTION_GUIDANCE} **Use a different schema only if the user:** - Explicitly requests a specific schema by name → use \`--schema \` - - Asks to "show workflows" or asks "what workflows" exist → resolve the authoritative root by running \`openspec context --json\` from the current working directory. If the user explicitly selected a registered store, use \`openspec context --json --store ""\`. Then run \`openspec schemas --json\` with its working directory set to the returned \`root.path\` and let them choose. This preserves roots selected by a local \`store:\` pointer or the global \`defaultStore\`; \`schemas\` does not accept \`--store\`. If context reports only \`no_openspec_root\`, run \`openspec schemas --json\` from the current working directory instead. Do not use this fallback for invalid or unavailable stores. + - Asks to "show workflows" or asks "what workflows" exist → resolve the authoritative root by running \`openspec context --json\` from the current working directory. If the user explicitly selected a registered store, use \`openspec context --json --store ""\`. Then run \`openspec schemas --json\` with its working directory set to the returned \`root.path\` and let them choose. This preserves roots selected by a local \`store:\` pointer or the global \`defaultStore\`; when a registered store was explicitly selected, append \`--store ""\` to \`openspec schemas --json\` as well. If context reports only \`no_openspec_root\`, run \`openspec schemas --json\` from the current working directory instead. Do not use this fallback for invalid or unavailable stores. Otherwise, omit \`--schema\` to preserve the configured default. diff --git a/src/core/templates/workflows/store-selection.ts b/src/core/templates/workflows/store-selection.ts index 586ca156d9..dfa132c3e5 100644 --- a/src/core/templates/workflows/store-selection.ts +++ b/src/core/templates/workflows/store-selection.ts @@ -4,4 +4,4 @@ * Interpolated into every workflow's instructions so generated skills * consistently teach how to target a registered store with `--store `. */ -export const STORE_SELECTION_GUIDANCE = `**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\`, \`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.`; +export const STORE_SELECTION_GUIDANCE = `**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.`; diff --git a/test/commands/schemas.test.ts b/test/commands/schemas.test.ts new file mode 100644 index 0000000000..7c01f15d1b --- /dev/null +++ b/test/commands/schemas.test.ts @@ -0,0 +1,216 @@ +import { afterEach, beforeEach, describe, expect, it } from 'vitest'; +import * as fs from 'node:fs'; +import * as os from 'node:os'; +import * as path from 'node:path'; + +import { getGlobalDataDir, registerStore } from '../../src/core/index.js'; +import { runCLI, type RunCLIResult } from '../helpers/run-cli.js'; +import { createOpenSpecRoot } from '../helpers/openspec-fixtures.js'; +import { cleanupTempPath } from '../helpers/temp-cleanup.js'; + +interface SchemaOutput { + name: string; + source: 'project' | 'user' | 'package'; +} + +interface FailureOutput { + schemas: unknown[]; + root: null; + status: Array<{ code: string; message: string; fix?: string }>; +} + +const SCHEMAS_MATRIX_TIMEOUT_MS = 30_000; + +describe('openspec schemas root selection', () => { + let tempDir: string; + let env: NodeJS.ProcessEnv; + let globalDataDir: string; + let localRoot: string; + let storeRoot: string; + let scratch: string; + + beforeEach(async () => { + tempDir = fs.realpathSync.native( + fs.mkdtempSync(path.join(os.tmpdir(), 'openspec-schemas-root-')) + ); + env = isolatedEnv('selected'); + globalDataDir = getGlobalDataDir({ env }); + + localRoot = path.join(tempDir, 'local-project'); + createOpenSpecRoot(localRoot); + writeProjectSchema(localRoot, 'local-only', 'Local-only workflow'); + + // A native path containing spaces catches shell-composition and separator assumptions. + storeRoot = path.join(tempDir, 'team store root'); + createOpenSpecRoot(storeRoot); + writeProjectSchema(storeRoot, 'store-only', 'Store-only workflow'); + await registerStore({ id: 'team-context', localPath: storeRoot, globalDataDir }); + + scratch = path.join(tempDir, 'scratch'); + fs.mkdirSync(scratch, { recursive: true }); + }); + + afterEach(() => { + cleanupTempPath(tempDir); + }); + + function isolatedEnv(name: string): NodeJS.ProcessEnv { + return { + XDG_DATA_HOME: path.join(tempDir, `${name}-data`), + XDG_CONFIG_HOME: path.join(tempDir, `${name}-config`), + OPEN_SPEC_INTERACTIVE: '0', + OPENSPEC_TELEMETRY: '0', + }; + } + + function writeProjectSchema(root: string, name: string, description: string): void { + const schemaDir = path.join(root, 'openspec', 'schemas', name); + fs.mkdirSync(schemaDir, { recursive: true }); + fs.writeFileSync( + path.join(schemaDir, 'schema.yaml'), + `name: ${name}\n` + + 'version: 1\n' + + `description: ${description}\n` + + 'artifacts:\n' + + ' - id: proposal\n' + + ' generates: proposal.md\n' + + ' description: Proposal\n' + + ' template: proposal.md\n' + ); + fs.writeFileSync(path.join(schemaDir, 'proposal.md'), '# Proposal\n'); + } + + function parseSchemas(result: RunCLIResult): SchemaOutput[] { + const parsed: unknown = JSON.parse(result.stdout); + expect(Array.isArray(parsed)).toBe(true); + return parsed as SchemaOutput[]; + } + + function parseFailure(result: RunCLIResult): FailureOutput { + const parsed = JSON.parse(result.stdout) as FailureOutput; + expect(Object.keys(parsed).sort()).toEqual(['root', 'schemas', 'status']); + expect(parsed.schemas).toEqual([]); + expect(parsed.root).toBeNull(); + expect(parsed.status).toHaveLength(1); + return parsed; + } + + function schemaNames(schemas: SchemaOutput[]): string[] { + return schemas.map((schema) => schema.name); + } + + function setDefaultStore(id: string): void { + const configDir = path.join(env.XDG_CONFIG_HOME as string, 'openspec'); + fs.mkdirSync(configDir, { recursive: true }); + fs.writeFileSync( + path.join(configDir, 'config.json'), + `${JSON.stringify({ defaultStore: id })}\n` + ); + } + + it('uses an explicit store instead of the cwd root and preserves success output shapes', async () => { + const json = await runCLI(['schemas', '--json', '--store', 'team-context'], { + cwd: localRoot, + env, + }); + + expect(json.exitCode).toBe(0); + expect(json.stderr).toBe(''); + const names = schemaNames(parseSchemas(json)); + expect(names).toContain('store-only'); + expect(names).not.toContain('local-only'); + + const human = await runCLI(['schemas', '--store', 'team-context'], { + cwd: localRoot, + env, + }); + expect(human.exitCode).toBe(0); + expect(human.stdout).toContain('Available schemas:'); + expect(human.stdout).toContain('store-only'); + expect(human.stdout).not.toContain('local-only'); + expect(human.stderr).toContain('Using OpenSpec root: team-context'); + expect(human.stderr).toContain(fs.realpathSync.native(storeRoot)); + }, SCHEMAS_MATRIX_TIMEOUT_MS); + + it('rejects --store-path through the standard machine-readable diagnostic', async () => { + const result = await runCLI(['schemas', '--json', '--store-path', storeRoot], { + cwd: localRoot, + env, + }); + + expect(result.exitCode).toBe(1); + expect(parseFailure(result).status[0].code).toBe('store_path_not_supported'); + }); + + it('honors declared pointers and global defaults while keeping nearest-root precedence', async () => { + const pointerRoot = path.join(tempDir, 'pointer-project'); + fs.mkdirSync(path.join(pointerRoot, 'openspec'), { recursive: true }); + fs.writeFileSync( + path.join(pointerRoot, 'openspec', 'config.yaml'), + 'store: team-context\n' + ); + writeProjectSchema(pointerRoot, 'pointer-only', 'Pointer-only workflow'); + + const declared = await runCLI(['schemas', '--json'], { cwd: pointerRoot, env }); + expect(declared.exitCode).toBe(0); + const declaredNames = schemaNames(parseSchemas(declared)); + expect(declaredNames).toContain('store-only'); + expect(declaredNames).not.toContain('pointer-only'); + + setDefaultStore('team-context'); + const globalDefault = await runCLI(['schemas', '--json'], { cwd: scratch, env }); + expect(globalDefault.exitCode).toBe(0); + expect(schemaNames(parseSchemas(globalDefault))).toContain('store-only'); + + const nearest = await runCLI(['schemas', '--json'], { cwd: localRoot, env }); + expect(nearest.exitCode).toBe(0); + const nearestNames = schemaNames(parseSchemas(nearest)); + expect(nearestNames).toContain('local-only'); + expect(nearestNames).not.toContain('store-only'); + }, SCHEMAS_MATRIX_TIMEOUT_MS); + + it('keeps rootless schema listing compatible when no stores are registered', async () => { + const rootlessEnv = isolatedEnv('rootless'); + const rootlessDir = path.join(tempDir, 'rootless-project'); + fs.mkdirSync(rootlessDir, { recursive: true }); + + const result = await runCLI(['schemas', '--json'], { cwd: rootlessDir, env: rootlessEnv }); + + expect(result.exitCode).toBe(0); + expect(schemaNames(parseSchemas(result))).toContain('spec-driven'); + }); + + it('fails closed with one JSON document when stores exist but none is selected', async () => { + const result = await runCLI(['schemas', '--json'], { cwd: scratch, env }); + + expect(result.exitCode).toBe(1); + const failure = parseFailure(result); + expect(failure.status[0].code).toBe('no_root_with_registered_stores'); + expect(failure.status[0].fix).toContain('--store '); + }); + + it('reports unknown and unavailable stores through canonical diagnostics', async () => { + const unknown = await runCLI(['schemas', '--json', '--store', 'ghost-context'], { + cwd: localRoot, + env, + }); + expect(unknown.exitCode).toBe(1); + expect(parseFailure(unknown).status[0].code).toBe('unknown_store'); + + const unavailableRoot = path.join(tempDir, 'unavailable-store'); + createOpenSpecRoot(unavailableRoot); + await registerStore({ + id: 'unavailable-context', + localPath: unavailableRoot, + globalDataDir, + }); + cleanupTempPath(unavailableRoot); + + const unavailable = await runCLI( + ['schemas', '--json', '--store', 'unavailable-context'], + { cwd: localRoot, env } + ); + expect(unavailable.exitCode).toBe(1); + expect(parseFailure(unavailable).status[0].code).toBe('store_identity_mismatch'); + }, SCHEMAS_MATRIX_TIMEOUT_MS); +}); diff --git a/test/core/completions/command-registry.test.ts b/test/core/completions/command-registry.test.ts index 1137ff94ad..c3ada9fb79 100644 --- a/test/core/completions/command-registry.test.ts +++ b/test/core/completions/command-registry.test.ts @@ -171,6 +171,7 @@ describe('command completion registry', () => { 'instructions', 'list', 'new change', + 'schemas', 'show', 'status', 'validate', @@ -219,7 +220,16 @@ describe('command completion registry', () => { }); it('advertises --store on the supported root-selection commands', () => { - for (const name of ['list', 'show', 'validate', 'archive', 'status', 'instructions', 'view']) { + for (const name of [ + 'list', + 'show', + 'validate', + 'archive', + 'status', + 'instructions', + 'schemas', + 'view', + ]) { const entry = command(name); const store = entry?.flags.find((flag) => flag.name === 'store'); expect(store, `${name} --store flag`).toBeDefined(); diff --git a/test/core/templates/propose.test.ts b/test/core/templates/propose.test.ts index e88c8d7786..aeff3089af 100644 --- a/test/core/templates/propose.test.ts +++ b/test/core/templates/propose.test.ts @@ -203,7 +203,10 @@ describe('propose schema selection', () => { expect(schemaSection, label).toContain('returned `root.path`'); expect(schemaSection, label).toContain('local `store:` pointer'); expect(schemaSection, label).toContain('global `defaultStore`'); - expect(schemaSection, label).toContain('`schemas` does not accept `--store`'); + expect(schemaSection, label).toContain( + 'append `--store ""` to `openspec schemas --json` as well' + ); + expect(schemaSection, label).not.toContain('`schemas` does not accept `--store`'); expect(schemaSection, label).toContain('context reports only `no_openspec_root`'); expect(schemaSection, label).toContain( 'run `openspec schemas --json` from the current working directory instead' From 8f5e3c1f5dd0483bf6e4b2d7dee33dbb7b9155e7 Mon Sep 17 00:00:00 2001 From: Patodo Date: Sun, 9 Aug 2026 21:40:53 +0900 Subject: [PATCH 3/5] test(schemas): assert complete JSON schema shape --- test/commands/schemas.test.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/test/commands/schemas.test.ts b/test/commands/schemas.test.ts index 7c01f15d1b..9b4abcc233 100644 --- a/test/commands/schemas.test.ts +++ b/test/commands/schemas.test.ts @@ -10,6 +10,8 @@ import { cleanupTempPath } from '../helpers/temp-cleanup.js'; interface SchemaOutput { name: string; + description: string; + artifacts: string[]; source: 'project' | 'user' | 'package'; } @@ -116,9 +118,16 @@ describe('openspec schemas root selection', () => { expect(json.exitCode).toBe(0); expect(json.stderr).toBe(''); - const names = schemaNames(parseSchemas(json)); + const schemas = parseSchemas(json); + const names = schemaNames(schemas); expect(names).toContain('store-only'); expect(names).not.toContain('local-only'); + expect(schemas).toContainEqual({ + name: 'store-only', + description: 'Store-only workflow', + artifacts: ['proposal'], + source: 'project', + }); const human = await runCLI(['schemas', '--store', 'team-context'], { cwd: localRoot, From 33e2f4f30f0c8ac51d8f2708dad2dde271bf1aa0 Mon Sep 17 00:00:00 2001 From: Clay Good Date: Tue, 11 Aug 2026 16:00:10 -0500 Subject: [PATCH 4/5] docs(stores): drop view from the cwd-only, no --store list view already accepts --store (registered in src/cli/index.ts), so listing it among the commands that act on the current directory only was incorrect. Remove it; templates and the deprecated noun forms remain. Co-Authored-By: Claude Opus 4.8 --- docs/stores-beta/user-guide.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/stores-beta/user-guide.md b/docs/stores-beta/user-guide.md index 6353e14022..e34a8af16a 100644 --- a/docs/stores-beta/user-guide.md +++ b/docs/stores-beta/user-guide.md @@ -427,7 +427,7 @@ tells you which case you're in. `openspec/config.yaml` declares `store: ` is treated as externalized planning, not as a store checkout to register. Remove the `store:` line first if you intentionally want to convert that repo into a local store root. -- **Some commands stay where they are.** `view`, `templates`, and the +- **Some commands stay where they are.** `templates` and the deprecated noun forms (`openspec change show`, ...) act on the current directory only — no `--store`. `schemas` follows the canonical root-selection precedence and accepts `--store ` while keeping its successful JSON array From cb4f468b02ea39f73ef5d1c95a744193573fd9f7 Mon Sep 17 00:00:00 2001 From: Clay Good Date: Tue, 11 Aug 2026 16:25:10 -0500 Subject: [PATCH 5/5] chore: regenerate skills and parity hashes after rebase onto main Co-Authored-By: Claude Opus 4.8 --- .../templates/skill-templates-parity.test.ts | 74 +++++++++---------- 1 file changed, 37 insertions(+), 37 deletions(-) diff --git a/test/core/templates/skill-templates-parity.test.ts b/test/core/templates/skill-templates-parity.test.ts index 8c267c617a..05c8db1345 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: 'fec38ba01c5c20695aca0ec7eff78c26e278ead21459cab8ec1562af51053427', - getNewChangeSkillTemplate: '935f6335e2d4b7d1bd4f0538c88386350c25e8b16e11b627556262229583ca51', - getContinueChangeSkillTemplate: 'ed41e2356af7aad6ef760f60fad19c6843cefe436d8f90084dcba4dbc6bf7272', - getApplyChangeSkillTemplate: 'a8d2529741849723ef160726648173e8ca8b42f5bb6f3d89ec547268adce2846', - getFfChangeSkillTemplate: 'fc2a45a08533ee9c7ab30fdab5f832b7d440070048e2a153f03db1620dc379bb', - getSyncSpecsSkillTemplate: 'd43b112a3c74bc951b094d220c8e75cca26bb00640d404b78af0752af1ff7bd9', - getOnboardSkillTemplate: 'a9f6134b187ec4f3a5aa6c7c181e51a15fec11b7ac1044a076fdfe79b47fbc80', - getOpsxExploreCommandTemplate: 'e2d470148708a9070675edddd1e783f1c71c96625d08cff4fe7a9994e0d292c0', - getOpsxNewCommandTemplate: '08e784e52ac2c146975a874257c589d88e93efbd83dc4d79253c8525f5c3064f', - getOpsxContinueCommandTemplate: 'ae964cd00f6ca332fd7f9428a577ade75be279f50431d5f60ece8172e8d1a4b1', - getOpsxApplyCommandTemplate: '860b55e4ffc055bb6f7339eeb65eaa695e09be7738dff2260726403647a57a18', - getOpsxFfCommandTemplate: '012610f85576a7055dfec2aaabba6bfc245454ce91fb6214587ae9316dc2b864', - getArchiveChangeSkillTemplate: '5ef19163f73997fdda1c69dc8bca710c16c50b052b481821d916f4084bb42a64', - getBulkArchiveChangeSkillTemplate: '03cc44a0ce9bdb3ba2668a9d43946596308901600aa29a728c4a71fc76e86de3', - getOpsxSyncCommandTemplate: '361c9e6e063116ae454ecbc9fac90dc44d876f909e2bdd9c4904580a73ce790c', - getVerifyChangeSkillTemplate: 'eb2c0f1b46c1be12750965a3a122efd5944d2b25781d714224c6e62a0efdc7fd', - getOpsxArchiveCommandTemplate: 'e94cbee572231c4a876177bc1cd88b326beeb989c51ee662c703e7b59166f5bb', - getOpsxOnboardCommandTemplate: '3e0da93fb03cec2a8583c47d05359ffefce5e88cb0148ac3686c2ec49a289045', - getOpsxBulkArchiveCommandTemplate: '7d415e6b1ebb5da93bf74bc3d667cf7a5e7f3ec7031d7a61d525b7950ef91863', - getOpsxVerifyCommandTemplate: 'ce0ee05b7a6b332e29db2298b9d5a928a1932caf516e35fd88f163154ffd43f4', - getOpsxProposeSkillTemplate: '16822ea0f2405962a585ebc2ef470cbe7f6990f7fbcd553ad68b145580d393ff', - getOpsxProposeCommandTemplate: '69e1d017765695612bdeb9b3e0ae10986d18f5c3f9305014b79720eef797a951', + getExploreSkillTemplate: '3efc37cddf342318ac37be7bb4ff5915f454b4c5bb127294ebdc7534ee21aa23', + getNewChangeSkillTemplate: 'eabd1e895c5881dcb17dcbaa3fb26098dd59e8eacb318e400820b4dc811ef781', + getContinueChangeSkillTemplate: '012136f6411a99c8fa228e2f9444cb64b0a89e0f56fdeac2fe03b2f5bee0c5d7', + getApplyChangeSkillTemplate: 'd1e7d5ceb85193c0964057dbb88e9651526754bd33f84020e2440ff0621d5dbb', + getFfChangeSkillTemplate: '5501740e7ec36ab23ab8c3a0d6dd0655a5e2f35433c7b90e82904fef5e7a326a', + getSyncSpecsSkillTemplate: 'b099e2ff31859c9b10d928066e662524f9aad9ecf2be12fceacb732d718c4146', + getOnboardSkillTemplate: '29b1d825179cff92fbc7b790694c1baef138575ea3de56848715e27d7e367946', + getOpsxExploreCommandTemplate: 'd2f70d11588f902c15c1e5ce9908cc4124c6b82fe78dc766ac5c3599c9e2a6f1', + getOpsxNewCommandTemplate: 'f2d30e569798a4c92ba932859d6ba4e0ad10e18feccbade1cfee0957597b3463', + getOpsxContinueCommandTemplate: 'e50e50266efa1b8e64ff9b6274ee8254f0a240d6adc1b862d126e2f1c9d3a559', + getOpsxApplyCommandTemplate: 'e3579ac78f2e2c75fa3d3a7ac7dc3e49c395e96f7323398f0f041d94f8de9bb0', + getOpsxFfCommandTemplate: 'e603bc0996604e6c17a3140943ea642a32d0fc65565e25424bf956e124c55772', + getArchiveChangeSkillTemplate: '56bfada1a5f35a127791b70de9d428a75b5aedd1584d6c9803a1ecb1fd1b4a23', + getBulkArchiveChangeSkillTemplate: '93875998cade5322d95b43299fba794bc1da754e917dd63a770406386a6d295d', + getOpsxSyncCommandTemplate: '0d2427efb79986e8fff3f96bd075a739c80d45eb29159fae717e950030da8202', + getVerifyChangeSkillTemplate: '223b7ffd99299a7d430e13092b9a0a3421b39f0d3217232f46c39d79b5f619ff', + getOpsxArchiveCommandTemplate: '9f973c819b11620985b03322945f0e0a92a02a2ef455b94e74482f5e6292ac5d', + getOpsxOnboardCommandTemplate: '7e251da66e2fdf539a09326463ee3ed0d01fe665ecb1d8f36f941fed00a01891', + getOpsxBulkArchiveCommandTemplate: '9fa8cdebe2f5667ebfc37bdc023396762c59d5b038c771dac2d8fd2c19e2627b', + getOpsxVerifyCommandTemplate: '1efcf7eff0671f48e9d9420f50865c563dd3079ee60f8c380bb7a90dd0102696', + getOpsxProposeSkillTemplate: '24623c066f97e34b957d448d1f9a9e8b8a13da3dfce45d45671f6226a2534848', + getOpsxProposeCommandTemplate: 'e67ba591efb0fecacb2229d06dfa84af18b825fab8a7b01377279e4f09a06ce4', getFeedbackSkillTemplate: 'd7d83c5f7fc2b92fe8f4588a5bf2d9cb315e4c73ec19bcd5ef28270906319a0d', - getUpdateChangeSkillTemplate: 'e50b6cd5d38f0d8974172fd7ebd6e2139f3fe3782c71584d8a61cfdb54edff8e', - getOpsxUpdateCommandTemplate: '4f1530486fbe118d9d7d469083c5517b8ec341ed8e92282e0b6c5155fb945bfe', + getUpdateChangeSkillTemplate: '7aa7351aca25fe2d8d29df4339ca2c768e32c6a59596316bb451831447fa15fc', + getOpsxUpdateCommandTemplate: 'fdc8ba0502910f74e85044e450906f189e939ffd70d5e85b7307a6ec33805148', }; const EXPECTED_GENERATED_SKILL_CONTENT_HASHES: Record = { - 'openspec-explore': '80109dec3abf1505ab1037f7196baac4fcdf175ca954411e8d439e5da881bf62', - 'openspec-new-change': '579d432771703f947a331a6ed288bf9c6660ca015fcd376d76f19b6ac7683082', - 'openspec-continue-change': '5c34be8194cdb4c5158335e47aece71143e8a22bfb4179dba47fd8aaf436d395', - 'openspec-apply-change': '2709759f101b455dbd93779005f6692966a37c7c71336af9dc5752906046d4f8', - 'openspec-ff-change': '19315644df7c582d920acfb67f3c500ca4e06fccc900265b3ac39621d85f7cdb', - 'openspec-sync-specs': '6e85521de10858bb020885eb657aa843e5746b2f09c846aa44545694f456cda9', - 'openspec-archive-change': '019d580a13eee5892cc9233a899919b572a3abfc6a05c1f0aabf9c4ba9bf3d4d', - 'openspec-bulk-archive-change': '6082df91e91fa57fbb88f05ca7834437bfad51561e72657a67a41c355d557646', - 'openspec-verify-change': '7cd65897d126f7c948620c0672ca62418620dbcb82ee73d890f758fb666a4ff8', - 'openspec-onboard': 'c104afb286e7c274a6914cb2042047705e42468a2df16246ff6337692828e12a', - 'openspec-propose': '2414a289c9541b233b80e4a5dcfe75a128bd4c37db421a1f066bf54788afaa97', - 'openspec-update-change': '8654fc3ea1eb2f03e1dba3eaf1e8c884b1c71cc949294a070c2f966fb13c8e2a', + 'openspec-explore': '4d9736372cc1faf8a5d8a66395a95bf77b9f3fcd2cda40411ad1db6927e8066a', + 'openspec-new-change': 'ec4529beef978e34634a6f7286fab55d68fad8fb374dceb45691d52caab33fbb', + 'openspec-continue-change': 'bb6194a16c54891cdb253678e8f70ce53b2af86735243980f366ce551d37e42e', + 'openspec-apply-change': '81ea96d9fa6ec8536cd23c1fe561ed28e1cc1cad0a8ceb700588e08974cc0e49', + 'openspec-ff-change': '217c78da2b6e8358f609ac57dcd02266aaec3354ce26dc6ec2fc9c2174673ab4', + 'openspec-sync-specs': 'd933d8856584d6c1253de91e652e7aee9e85c77ad4d3531f6476f79d84e6e5e8', + 'openspec-archive-change': '7c65053d674ba4e1e20e2bf73ba7e5a7f94baef2eaa9b33cee48d4cadea51b7a', + 'openspec-bulk-archive-change': '2039b9ecf6e64339dffe0e16272507a386d9fe326f419ff758315aa736fdd96c', + 'openspec-verify-change': 'af9be013dcbe8c6d8f6d9ab10c893fbd03f4c62933c384d82f63894dd0ceb84f', + 'openspec-onboard': 'd53403b4910ab64307862ccf97e70bd8f7174ee44508088fb239c880f0939331', + 'openspec-propose': '25d08ed4f031770cea219604167d76bca9f3e89fe0c2f545263674482c6f13f0', + 'openspec-update-change': '5ee000a8bf5507a553fbb9ed666625d11986f9c0bf14edb495648554a4d1c53c', }; // Intentionally excludes getFeedbackSkillTemplate: this list only models templates @@ -215,7 +215,7 @@ describe('skill templates split parity', () => { expect(STORE_SELECTION_GUIDANCE).toContain( 'openspec status --change "" --json --store ""' ); - expect(STORE_SELECTION_GUIDANCE).toContain('`context`, `view`'); + expect(STORE_SELECTION_GUIDANCE).toContain('`context`, `schemas`, `view`'); }); it('validates synced main specs before reporting success', () => {