Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/share-schema-stores.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@fission-ai/openspec": minor
---

### New Features

- **Schema Store sources** — Select a registered Store as the shared source for workflow schemas and templates, independently from the local or Store-backed planning root, with optional exact-name visibility controls.
2 changes: 1 addition & 1 deletion docs/agent-contract.md
Original file line number Diff line number Diff line change
Expand Up @@ -137,5 +137,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. `schemas`/`templates` do not accept `--store`; they resolve schemas from the nearest consumer config, including its `schemaStore`.
8. Deprecated noun forms (`change`/`spec` subcommands) emit unenveloped payloads without `root`/`status`.
26 changes: 26 additions & 0 deletions docs/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -361,6 +361,24 @@ store: team-context

Normal commands then resolve to the declared store automatically; the root banner and JSON `root` block report `source: "declared"` with the store id, and printed hints still carry `--store <id>`. The declaration is a fallback, never an override: explicit `--store` always wins, and a directory with real planning folders ignores the pointer (with a warning). To convert a pointer repo into a local OpenSpec root, remove the `store:` line and run `openspec init` — init refuses to scaffold while the declaration is present.

Select a registered Store as the schema and template source without changing
the planning root:

```yaml
schema: qeda-sdd
schemaStore:
id: department-schemas
schemas: [qeda-sdd, frontend-sdd]
```

`schemaStore: department-schemas`, an omitted `schemas` field, and
`schemas: ["*"]` all expose every schema. An exact list exposes only those
Store schemas; hidden names can still fall back to user or package schemas.
Schema Store entries take precedence over user and package entries, and replace
consumer-local schemas while configured. `schemas`, `schema which`, schema
validation/forking, templates, and workflow commands all apply the same filter.
JSON source reporting uses `source: "store"` plus `storeId`.

A machine-level variant covers every repo at once: `openspec config set defaultStore <id>` (see Configuration). It is consulted only after `--store`, a local root, and a project pointer have all failed to resolve; the root banner and JSON `root` block then report `source: "global_default"`.

## Doctor (relationship health)
Expand Down Expand Up @@ -915,6 +933,10 @@ Commands for creating and managing custom workflow schemas.

Create a new project-local schema.

This command refuses to create an invisible project-local schema when
`schemaStore` is configured. Edit the registered Schema Store directly, or
remove `schemaStore` first.

```
openspec schema init <name> [options]
```
Expand Down Expand Up @@ -967,6 +989,10 @@ openspec/schemas/<name>/

Copy an existing schema to your project for customization.

This command refuses to create an invisible project-local schema when
`schemaStore` is configured. Edit the registered Schema Store directly, or
remove `schemaStore` first.

```
openspec schema fork <source> [name] [options]
```
Expand Down
47 changes: 47 additions & 0 deletions docs/customization.md
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,53 @@ your-project/
└── src/
```

### Share Schemas from a Store

For schemas maintained by another team or department, register that repository
as a normal OpenSpec Store and select it independently from the location that
owns changes and specs:

```yaml
# openspec/config.yaml
schema: qeda-sdd
schemaStore:
id: department-schemas
schemas:
- qeda-sdd
- frontend-sdd
```

The scalar form exposes every schema in the Store:

```yaml
schemaStore: department-schemas
```

Omitting `schemas`, or writing `schemas: ["*"]`, has the same all-visible
behavior. Otherwise the list contains exact schema names; `*` cannot be mixed
with names.

When `schemaStore` is configured, its visible schemas replace the project's
local schema layer. Resolution order is:

1. visible schemas from the configured Schema Store;
2. user schemas;
3. package schemas.

The visibility list filters only the Store. A hidden Store schema can still
resolve from the user or package layer. Without `schemaStore`, existing
project → user → package behavior is unchanged.

OpenSpec reads the registered checkout's current files. It does not fetch,
pull, pin, or otherwise synchronize that repository during schema or workflow
commands. Update the checkout with normal Git commands; one registered checkout
is shared by every local consumer that names that Store.

Because a configured Schema Store replaces the project-local schema layer,
`openspec schema init` and `openspec schema fork` do not create local schemas
in that consumer. Edit the registered Store directly, or remove `schemaStore`
before creating a project-local schema.

### Fork an Existing Schema

The fastest way to customize is to fork a built-in schema:
Expand Down
50 changes: 47 additions & 3 deletions docs/stores-beta/user-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,49 @@ The pointer is a fallback, never an override: an explicit `--store` always
wins, and if the repo grows real planning folders of its own, those win
(with a warning to remove the stale pointer).

**Sharing workflow schemas independently.** A Store can also be selected only
for schemas and templates. This keeps changes and specs in the local repo:

```yaml
# web-app/openspec/config.yaml
schema: qeda-sdd
schemaStore:
id: department-schemas
schemas: [qeda-sdd, frontend-sdd]
```

Or combine two Store roles explicitly:

```yaml
store: department-planning
schema: qeda-sdd
schemaStore: department-schemas
```

Here `department-planning` owns specs, changes, and archives, while
`department-schemas` contributes the allowed schemas. Use
`schemaStore: department-schemas` when every schema should be visible; the
object form also defaults to `*` when `schemas` is omitted.

Set up or clone the schema repository normally, then register it on each
machine:

```bash
git clone git@github.com:acme/department-schemas.git
openspec store register ./department-schemas --id department-schemas
```

Schema resolution never runs Git commands. Pull or switch the registered
checkout yourself; its current working tree is immediately visible to every
local consumer. OpenSpec does not pin separate commits per project. Use
`openspec schema which <name>` to see the winning source and
`openspec store doctor department-schemas` when Store identity is unhealthy.

Use `openspec schema init` and `openspec schema fork` only when the consumer
owns its project-local schema layer; while `schemaStore` is configured, those
commands fail before writing an invisible local schema. Edit the registered
Schema Store directly, or remove `schemaStore` first.

**One default for every repo on your machine.** If you work across many
code repos that all plan into the same store, set it once, globally,
instead of adding the `store:` line to each repo:
Expand Down Expand Up @@ -335,9 +378,10 @@ tells you which case you're in.
`openspec/config.yaml` declares `store: <id>` 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`.
- **Schema inspection follows the consumer config.** `templates` and `schemas`
do not accept `--store`; they resolve the nearest consumer project's
`schemaStore`. Deprecated noun forms (`openspec change show`, ...) remain
current-directory commands.
- **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.
Expand Down
2 changes: 2 additions & 0 deletions openspec/changes/add-schema-store-sources/.openspec.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-07-28
203 changes: 203 additions & 0 deletions openspec/changes/add-schema-store-sources/design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
## Context

OpenSpec currently resolves one active planning root. That root owns `openspec/specs`, `openspec/changes`, project configuration, and project-local schemas. A config-only consumer repository may redirect planning to a registered Store with `store: <id>`, but schema resolution then follows the planning Store as part of the same root.

The department use case needs two independent roles:

- a planning root that owns specs, changes, and archives; and
- a schema root that owns reusable workflow schemas and templates.

Both roots can already be represented by registered Store checkouts. Store checkout synchronization deliberately remains a normal Git responsibility. The design must therefore reuse Store identity and registry resolution, preserve synchronous schema parsing, and avoid the Git fetch, lockfile, cache, and integrity machinery of a separate remote-source subsystem.

## Goals / Non-Goals

**Goals:**

- Allow a consumer project to select one registered Store as its schema source without redirecting planning.
- Allow a project to combine local planning, a planning Store, and a different schema Store.
- Let the consumer restrict which schemas from the schema Store participate in discovery and resolution.
- Keep current schema behavior byte-compatible when `schemaStore` is absent.
- Resolve Store IDs once through the existing registry and pass canonical local paths to synchronous schema code.
- Produce clear, machine-readable diagnostics for invalid declarations and unavailable Store checkouts.

**Non-Goals:**

- Fetching, cloning, pulling, committing, or pushing Store repositories.
- Pinning a schema Store to a commit per consumer project.
- Content-addressed caches, schema lockfiles, or bundle integrity hashes.
- Combining schemas from multiple schema Stores in one consumer.
- Glob matching beyond the special all-visible token `*`.
- Schema merging or inheritance across roots.
- Changing how Store Git drift is detected or repaired.

## Decisions

### 1. Use role-specific Store declarations

The existing `store` field continues to select the planning Store. A new `schemaStore` field selects the schema Store:

```yaml
store: department-planning
schema: qeda-sdd
schemaStore: department-schemas
```

Scalar `schemaStore` is shorthand for the object form with every schema visible:

```yaml
schemaStore:
id: department-schemas
schemas:
- "*"
```

This is preferred over overloading `store` with a mode flag because the configuration states both roles directly and remains reproducible for humans, agents, and CI.

Alternative considered: a command-only `--schema-store` flag. Rejected as the primary contract because every lifecycle command would need the flag and omissions could resolve a different schema. A future CLI override can be added independently if a concrete use case appears.

Alternative considered: a general array of mounted Stores with arbitrary roles. Rejected because the current requirement has exactly two roles and a generalized mount graph would add ordering, conflict, and diagnostic complexity without a demonstrated need.

### 2. Normalize one strict visibility model

The normalized declaration is:

```ts
interface SchemaStoreDeclaration {
id: string;
schemas: '*' | string[];
}
```

Rules:

- a scalar declaration normalizes to `{ id, schemas: '*' }`;
- an object without `schemas` also defaults to `'*'`;
- `schemas: ["*"]` is the explicit all-visible form;
- otherwise `schemas` is a non-empty, duplicate-free list of exact valid schema names;
- `*` cannot be combined with names;
- empty lists, unsupported fields, invalid Store IDs, invalid schema names, and non-string values are invalid declarations.

The visibility filter applies only to schemas contributed by the schema Store. User and package schemas retain their existing behavior. A hidden Store schema does not participate in discovery, resolution, shadow reporting, or suggestions.

Exact names are preferred over general globs because schema names are already a finite discoverable set and exact matching avoids platform-dependent pattern behavior.

### 3. Fail closed for an explicitly invalid or unavailable schema Store

Generic project-config loading remains resilient and warns field-by-field. Schema context resolution additionally reads the declaration as an authority-bearing field:

- malformed `schemaStore` fails schema-context resolution instead of silently falling back;
- an unknown Store ID points to `openspec store register`;
- missing or mismatched Store identity points to `openspec store doctor <id>`;
- a missing `openspec/schemas` directory is treated as an empty schema Store, so a newly created Store can be populated incrementally;
- a configured schema that is absent or hidden reports the visible Store schemas and normal fallback candidates.

Failing closed prevents a typo in `schemaStore` from silently selecting a user or package schema with the same name.

### 4. Resolve Store registry state before synchronous schema lookup

Store registry APIs are asynchronous, while schema directory loading is intentionally synchronous. Root selection already occurs asynchronously for workflow commands.

Introduce a resolved command context with three explicit ownership locations:

```ts
interface ResolvedOpenSpecRoot {
path: string; // planning root
consumerRoot: string; // config owner
schemaContext: {
root: string; // consumer root or registered schema Store root
source: 'project' | 'store';
storeId?: string;
visibleSchemas: '*' | readonly string[];
};
// existing changes/specs/archive fields
}
```

Resolution sequence:

1. Canonicalize the command start path.
2. Find the consumer repository containing the controlling config, when present.
3. Resolve the planning root using existing `--store`, local-root, `store:`, and global-default precedence.
4. Read `schemaStore` from the consumer root, falling back to the planning root only when no consumer root exists.
5. Resolve the schema Store ID through the existing registry and validate Store identity.
6. Return canonical local paths and normalized visibility to downstream synchronous schema resolution.

Schema-only commands use the same schema-context resolver rather than duplicating registry lookup.

Alternative considered: make `getSchemaDir`, `resolveSchema`, and every caller asynchronous. Rejected because registry lookup is the only asynchronous requirement and can be completed at the command boundary.

Operational configuration remains backward-compatible with Planning Store
selection. When planning is redirected and the consumer does not declare
`schemaStore`, commands continue to use the Planning Store's configuration.
When the consumer does declare `schemaStore`, its configuration overlays the
Planning Store configuration: consumer-authored schema choices and rules can
target the selected schema authority, while omitted fields such as
`references`, context, and operation guidance remain inherited from the
Planning Store.

### 5. Treat a schema Store as the project schema layer

When `schemaStore` is configured, its visible schemas replace the consumer repository's project-local schema layer. Resolution precedence becomes:

1. visible schema Store schema;
2. user schema;
3. package schema.

When `schemaStore` is absent, precedence remains:

1. consumer/project-local schema;
2. user schema;
3. package schema.

The planning Store is never searched for schemas merely because it owns the active changes. If a project wants the same Store for both roles, it declares the same ID in `store` and `schemaStore`.

This avoids implicit coupling and makes the schema authority visible in consumer configuration.

Because the Store replaces the consumer-local project layer, `schema init` and
`schema fork` MUST NOT write into the consumer repository while `schemaStore`
is configured. Such files would be immediately invisible to resolution.
Instead, both commands fail before mutation, identify the configured Store, and
direct the user to edit that Store or remove `schemaStore` before creating a
project-local schema.

### 6. Report Store provenance consistently

Schema discovery records extend the source union with `store`. Store-backed results include the Store ID and canonical schema directory path.

The following surfaces use the same resolved schema context and visibility:

- `openspec schemas`;
- `openspec schema which <name>`;
- `openspec schema which --all`;
- template reporting;
- schema validation;
- change creation, status, instructions, apply, verify, and archive.

Human output labels Store schemas with the Store ID. JSON output adds `source: "store"` and `storeId` without changing existing fields for project, user, or package sources.

### 7. Keep Git synchronization external

The schema Store is an ordinary registered Store checkout. OpenSpec reads its current working tree and never contacts its remote during normal commands. Teams update it with normal Git operations and can use existing Store doctor output to inspect Git drift.

This is an explicit trade-off: consumers do not get per-project commit pinning, but the implementation remains aligned with the existing Store contract and the stated departmental workflow.

## Risks / Trade-offs

- **One checkout serves every consumer on a machine** → Document that updating the registered schema Store changes its schemas for all local consumers; teams that need version isolation must register differently named Store checkouts.
- **A dirty schema Store can affect consumers immediately** → Preserve normal Git ownership and surface the canonical Store/path in `schema which`; do not imply OpenSpec has pinned or synchronized it.
- **Root context still touches workflow commands** → Pass one resolved schema context through existing command boundaries and cover local, planning-Store, and split-Store journeys with integration tests.
- **Visibility can hide a schema that remains available elsewhere** → Treat the Store filter as source-specific, label the winning source, and include available-source diagnostics.
- **Invalid authority could otherwise fall back silently** → Strict schema-context resolution fails closed whenever the `schemaStore` field is present but unusable.
- **Windows path and case behavior differs** → Use existing canonicalization and Store registry helpers, `path.join`, and platform-neutral temporary-directory tests.

## Migration Plan

1. Add parsing and normalization while leaving absent-field behavior unchanged.
2. Add schema-context resolution and tests without changing existing command output for non-users.
3. Route schema consumers through the resolved context and add Store provenance.
4. Document creation, registration, Git update, visibility, and split planning/schema examples.
5. Release as an additive experimental capability. Rollback consists of removing `schemaStore`; planning and schema resolution then use their previous roots and precedence.

## Open Questions

None for the initial scope. Multiple schema Stores, CLI overrides, per-consumer commit pinning, and pattern visibility require separate proposals.
Loading
Loading