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
5 changes: 5 additions & 0 deletions .changeset/remote-schema-sources.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@fission-ai/openspec": minor
---

Add Git-backed project schema sources with explicit `schema sync`, deterministic lockfiles, integrity-verified local caching, and network-free normal resolution.
120 changes: 110 additions & 10 deletions docs/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ The OpenSpec CLI (`openspec`) provides terminal commands for project setup, vali
| **Validation** | `validate` | Check changes and specs for issues |
| **Lifecycle** | `archive` | Finalize completed changes |
| **Workflow** | `new change`, `status`, `instructions`, `templates`, `schemas` | Artifact-driven workflow support |
| **Schemas** | `schema init`, `schema fork`, `schema validate`, `schema which` | Create and manage custom workflows |
| **Schemas** | `schema init`, `schema fork`, `schema validate`, `schema which`, `schema sync` | Create, inspect, and synchronize custom workflows |
| **Config** | `config` | View and modify settings |
| **Utility** | `feedback`, `completion` | Feedback and shell integration |

Expand Down Expand Up @@ -51,6 +51,7 @@ These commands support `--json` output for programmatic use by AI agents and scr
| `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; `--store <id>` to select a registered root |
| `openspec schema sync [name]` | Synchronize declared Git schemas | `--json` for structured sync results |
| `openspec store setup <id>` | Create and register a local store | `--json` with explicit inputs for structured setup output |
| `openspec store register <path>` | Register an existing store | `--json` for structured registration output |
| `openspec store unregister <id>` | Forget a local store registration | `--json` for structured cleanup output |
Expand Down Expand Up @@ -892,14 +893,21 @@ openspec templates --json

```
Schema: spec-driven
Source: package

Templates:
proposal → ~/.openspec/schemas/spec-driven/templates/proposal.md
specs → ~/.openspec/schemas/spec-driven/templates/specs.md
design → ~/.openspec/schemas/spec-driven/templates/design.md
tasks → ~/.openspec/schemas/spec-driven/templates/tasks.md
proposal:
/path/to/openspec/schemas/spec-driven/templates/proposal.md
specs:
/path/to/openspec/schemas/spec-driven/templates/specs.md
design:
/path/to/openspec/schemas/spec-driven/templates/design.md
tasks:
/path/to/openspec/schemas/spec-driven/templates/tasks.md
```

The source is one of `project`, `remote`, `user`, or `package`. Synchronized
remote templates report `remote` in both text and JSON output.

---

### `openspec schemas`
Expand Down Expand Up @@ -943,6 +951,93 @@ Available schemas:

Commands for creating and managing custom workflow schemas.

### `openspec schema sync`

Synchronize Git schema sources declared in `openspec/config.yaml`.

```text
openspec schema sync [name] [options]
```

| Option | Description |
|--------|-------------|
| `--locked` | Restore or verify the exact commit and digest already in the lockfile |
| `--json` | Emit exactly one JSON result document |

Omit `name` to synchronize every declared source. Update mode resolves a
configured branch or tag to an immutable commit, validates the complete bundle,
installs it in the local content-addressed cache, and atomically updates
`openspec/schemas.lock.yaml`.

The command searches upward from the current directory for the nearest
consumer repository containing `openspec/`. Its `config.yaml` and
`schemas.lock.yaml` remain authoritative even when the repository selects a
planning store. Concurrent schema sync processes for the same consumer
repository are serialized so named updates cannot overwrite each other.
Runtime coordination lives beneath a self-ignored
`openspec/.schemas.lock/` directory. OpenSpec publishes participant records
atomically and recovers aged malformed records, so interrupted syncs neither
dirty Git status nor require manual lock cleanup.

```bash
# Update one source to the current configured ref
openspec schema sync qeda-sdd

# Update every source
openspec schema sync

# CI: restore the exact committed lock state
openspec schema sync --locked --json
```

Public HTTPS and private SSH declarations:

```yaml
schemaSources:
public-flow:
git: https://github.com/example/team-schemas.git
ref: v1.2.0
path: schemas/public-flow
private-flow:
git: git@github.com:acme/private-schemas.git
ref: main
path: schemas/private-flow
```

Private access reuses system Git SSH and credential helpers. OpenSpec preserves
an existing `GIT_SSH_COMMAND` while enforcing non-interactive SSH with
`BatchMode=yes`. An explicit `StrictHostKeyChecking` value is preserved;
`accept-new` is added only when the inherited command has no host-key policy.
Never embed a token in a URL. Commit the lockfile, but do not commit the global
cache. Ordinary commands are network-free and continue to use the old locked
version after a remote branch advances. Run update mode explicitly to upgrade.
A missing or corrupt cache reports a `schema sync --locked` fix; CI must either
restore the global cache or run that command while the source is reachable
before entering an offline phase.

Example JSON success:

```json
{
"synced": true,
"locked": false,
"lockfile": "/workspace/openspec/schemas.lock.yaml",
"schemas": [
{
"name": "qeda-sdd",
"git": "https://github.com/example/team-schemas.git",
"requestedRef": "v1.2.0",
"resolvedCommit": "0123456789abcdef0123456789abcdef01234567",
"bundlePath": "schemas/qeda-sdd",
"integrity": "sha256:...",
"cachePath": "...",
"restored": false
}
],
"status": []
}
```
Comment thread
coderabbitai[bot] marked this conversation as resolved.

### `openspec schema init`

Create a new project-local schema.
Expand Down Expand Up @@ -1094,11 +1189,16 @@ spec-driven resolves from: package
Source: /usr/local/lib/node_modules/@fission-ai/openspec/schemas/spec-driven
```

**Schema precedence:**
**Schema authority and precedence:**

For a name without a remote declaration, precedence remains project, user, then
package. A `schemaSources.<name>` declaration owns that name: a same-named
project schema is a configuration conflict, a valid lock/cache resolves as
remote, and an unavailable remote never falls through to user or package.

1. Project: `openspec/schemas/<name>/`
2. User: `~/.local/share/openspec/schemas/<name>/`
3. Package: Built-in schemas
Declared remote sources fail closed when unavailable and never trigger an
implicit network request. `schema which --all` reports an unavailable remote as
one structured entry and continues listing healthy schemas.

---

Expand Down
97 changes: 95 additions & 2 deletions docs/customization.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
# Customization

OpenSpec provides three levels of customization:
OpenSpec provides four levels of customization:

| Level | What it does | Best for |
|-------|--------------|----------|
| **Project Config** | Set defaults, inject context/rules | Most teams |
| **Custom Schemas** | Define your own workflow artifacts | Teams with unique processes |
| **Remote Schemas** | Pin a team-owned Git schema across repositories | Multi-repository teams |
| **Global Overrides** | Share schemas across all projects | Power users |

---
Expand Down Expand Up @@ -344,6 +345,98 @@ Path: /path/to/project/openspec/schemas/my-workflow

> **Note:** OpenSpec also supports user-level schemas at `~/.local/share/openspec/schemas/` for sharing across projects, but project-level schemas in `openspec/schemas/` are recommended since they're version-controlled with your code.

## Remote Team Schemas

A remote schema is a complete schema bundle maintained in a Git repository and
declared by a project. It solves a different problem from the other locations:

- A **project-local schema** is copied into one repository and can evolve there.
- A **remote schema** is shared by many repositories and pinned by each consumer.
- A **user-level schema** is a machine-local override and is not reproducible for a team.
- A **package schema** ships with the installed OpenSpec version.

Declare the source without changing the existing string-valued `schema` field:

```yaml
# openspec/config.yaml
schema: qeda-sdd

schemaSources:
qeda-sdd:
git: https://github.com/example/QEDASDD.git
ref: v1.0.0
path: schemas/qeda-sdd
```

Then synchronize explicitly:

```bash
# Resolve the configured ref and update the lock
openspec schema sync qeda-sdd

# Synchronize every declared source
openspec schema sync

# Restore/verify the exact lockfile state, for example in CI
openspec schema sync --locked
```

Commit `openspec/schemas.lock.yaml`. It records the requested ref, resolved
commit SHA, bundle path, and SHA-256 content integrity. Do not commit the
machine cache under the OpenSpec global data directory. Normal OpenSpec
commands never fetch: they only use a matching lock entry and verified cache.
If the cache is absent, run `schema sync --locked` while the Git source is
reachable, then ordinary commands work offline.

The consumer repository owns both `openspec/config.yaml` and
`openspec/schemas.lock.yaml`. Running sync from a nested directory searches
upward for that repository. A configured planning store does not own or redirect
remote schema sources. Sync processes for one consumer repository are
serialized, so concurrent named updates cannot lose lockfile entries.
The `openspec/.schemas.lock/` coordination directory ignores its own runtime
files. Participant records are published atomically, and aged malformed
records left by an interrupted process are reclaimed without manual cleanup.

Branches and tags are allowed, but they move only when `schema sync` is run
without `--locked`. A remote update therefore cannot change a normal command's
workflow unexpectedly.

Private repositories use the system Git client's existing SSH agent or
credential helper:

```yaml
schemaSources:
private-flow:
git: git@github.com:acme/private-schemas.git
ref: main
path: schemas/private-flow
```

Do not put credentials or tokens in configuration. Credential-bearing HTTPS
URLs are rejected, and lockfiles contain no authentication material. OpenSpec
preserves existing `GIT_SSH_COMMAND` options while enforcing `BatchMode=yes`.
An explicit `StrictHostKeyChecking` value is preserved; OpenSpec adds
`StrictHostKeyChecking=accept-new` only when no host-key policy is present.

Schema authority is name-based:

- Without a remote declaration, precedence remains project-local, user-level,
then package built-in.
- Once `schemaSources.<name>` is declared, the remote owns that name.
- A same-named project-local bundle is a configuration conflict; OpenSpec does
not silently choose or shadow either bundle.

A declared remote source fails closed when its lock or cache is missing,
stale, or corrupt; OpenSpec does not silently select a same-named user or
package schema. `schema which --all` reports each unavailable remote separately
while continuing to list healthy schemas. Bundle paths must stay inside the Git
repository. Absolute paths, traversal, symlinks, submodules, case-colliding
paths, invalid names, incomplete schemas, and bundles over 1,000 files or
10 MiB are rejected. These portable fail-closed checks apply to remote bundles;
existing project-local schema validation retains its legacy path and symlink
behavior. Remote schemas are complete bundles; inheritance and schema merging
are not supported.

---

## Examples
Expand Down Expand Up @@ -414,7 +507,7 @@ Then edit `schema.yaml` to add:

OpenSpec also supports community-maintained schemas distributed via standalone repositories. These provide opinionated workflows that integrate OpenSpec with other tools or systems, similar to how [github/spec-kit's community extension catalog](https://github.com/github/spec-kit/tree/main/extensions) works for spec-kit.

Community schemas are not vendored into OpenSpec core — they live in their own repositories with their own release cadence. To use one, copy the schema bundle into your project's `openspec/schemas/<schema-name>/` directory (each repo's README has install instructions).
Community schemas are not vendored into OpenSpec core — they live in their own repositories with their own release cadence. You can either declare one as a remote schema and pin it with `openspec schema sync`, or copy its bundle into your project's `openspec/schemas/<schema-name>/` directory.

| Schema | Maintainer | Repository | Description |
|--------|-----------|-----------|-------------|
Expand Down
2 changes: 2 additions & 0 deletions openspec/changes/add-remote-schema-sources/.openspec.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-07-26
Loading
Loading