Hi!
I was reading the stores-beta user guide and wanted to check one assumption about how teammates stay in sync.
A code repo points at a store by id (store: team-plans in openspec/config.yaml), and each machine maps that id to a local checkout with openspec store register. The guide is clear that "OpenSpec never clones, pulls, or pushes": teammates move the store around with plain git themselves.
The part I could not find is anything that records which version of the store a code repo expects. There is no lockfile, and the registry entry stores only a path. So when two teammates have the store checked out at different commits, the same store: team-plans could resolve to different specs on each machine, and nothing flags the divergence.
Concretely, this means that two people can legitimately read directly opposite requirements from what is nominally the same spec.
Reproduction
Version
We'll use scoped XDG folders to emulate the store and three separate users - the author and two consumers:
Shared Store
Let's create a straightforward shared store and commit a spec.
export XDG_DATA_HOME=/tmp/openspec-store-drift/author/data
export XDG_CONFIG_HOME=/tmp/openspec-store-drift/author/config
export STORE=/tmp/openspec-store-drift/store
# New store
openspec store setup team-plans --path $STORE
# New change
openspec new change add-auth-tokens --store team-plans
mkdir -p $STORE/openspec/changes/add-auth-tokens/specs/auth-tokens/
cat <<'EOSPEC' >$STORE/openspec/changes/add-auth-tokens/specs/auth-tokens/spec.md
## ADDED Requirements
### Requirement: Token lifetime
Access tokens MUST remain valid for 24 hours and MAY be reused.
#### Scenario: A token issued an hour ago is replayed
- **WHEN** a client presents a token issued 1 hour ago
- **THEN** the API accepts it
EOSPEC
# Archive change
openspec archive add-auth-tokens --store team-plans --yes
# Commit
git -C $STORE add -A
git -C $STORE commit -qm "auth-tokens: 24h, reusable"
Store ready: team-plans
Location: /private/tmp/openspec-store-drift/store
OpenSpec root: ready
Registry: already registered
Note: Store 'team-plans' is already registered at this path.
Next: run normal OpenSpec commands against this store, for example:
openspec new change <change-id> --store team-plans
Share this store by committing and pushing it like any Git repo.
Created change 'add-auth-tokens' at /private/tmp/openspec-store-drift/store/openspec/changes/add-auth-tokens/
Schema: spec-driven
Next: openspec status --change add-auth-tokens --store team-plans
Task status: No tasks
Specs to update:
auth-tokens: update
auth-tokens ADDED failed for header "### Requirement: Token lifetime" - already exists
Aborted. No files were changed.
Project
Let's also create a project to consume said shared spec:
mkdir -p /tmp/openspec-store-drift/project/openspec/
cat <<'EOSPEC' >/tmp/openspec-store-drift/project/openspec/config.yaml
store: team-plans
EOSPEC
User 1 - Alice
Alice sets up the store as it stands today - clone it, register it, read the spec:
export XDG_DATA_HOME=/tmp/openspec-store-drift/alice/data
export XDG_CONFIG_HOME=/tmp/openspec-store-drift/alice/config
# Clone and register the shared store
git clone -q /tmp/openspec-store-drift/store /tmp/openspec-store-drift/alice/store
openspec store register /tmp/openspec-store-drift/alice/store --id team-plans
cd /tmp/openspec-store-drift/project
openspec show auth-tokens --type spec
Store registered: team-plans
Location: /private/tmp/openspec-store-drift/alice/store
OpenSpec root: ready
Registry: registered
Next: run normal OpenSpec commands against this store, for example:
openspec new change <change-id> --store team-plans
Share it: teammates clone /tmp/openspec-store-drift/store and run openspec store register <path>.
# auth-tokens Specification
## Purpose
TBD - created by archiving change add-auth-tokens. Update Purpose after archive.
## Requirements
### Requirement: Token lifetime
Access tokens MUST remain valid for 24 hours and MAY be reused.
#### Scenario: A token issued an hour ago is replayed
- **WHEN** a client presents a token issued 1 hour ago
- **THEN** the API accepts it
Spec change
We then radically change one requirement: tokens become single-use, 60s:
export XDG_DATA_HOME=/tmp/openspec-store-drift/author/data
export XDG_CONFIG_HOME=/tmp/openspec-store-drift/author/config
export STORE=/tmp/openspec-store-drift/store
# New change
openspec new change tighten-tokens --store team-plans
mkdir -p $STORE/openspec/changes/tighten-tokens/specs/auth-tokens
cat <<'EOSPEC' > $STORE/openspec/changes/tighten-tokens/specs/auth-tokens/spec.md
## MODIFIED Requirements
### Requirement: Token lifetime
Access tokens MUST be single-use and expire 60 seconds after issuance.
#### Scenario: A token issued an hour ago is replayed
- **WHEN** a client presents a token issued 1 hour ago
- **THEN** the API rejects it with 401
EOSPEC
openspec archive tighten-tokens --store team-plans --yes
git -C $STORE add -A
git -C $STORE commit -qm "auth-tokens: reverse to 60s, single-use"
Created change 'tighten-tokens' at /private/tmp/openspec-store-drift/store/openspec/changes/tighten-tokens/
Schema: spec-driven
Next: openspec status --change tighten-tokens --store team-plans
Task status: No tasks
Specs to update:
auth-tokens: update
Applying changes to /private/tmp/openspec-store-drift/store/openspec/specs/auth-tokens/spec.md:
~ 1 modified
Totals: + 0, ~ 1, - 0, → 0
Specs updated successfully.
User 2 - Bob
Bob sets up the store in the exact same way:
export XDG_DATA_HOME=/tmp/openspec-store-drift/bob/data
export XDG_CONFIG_HOME=/tmp/openspec-store-drift/bob/config
# Clone and register the shared store
git clone -q /tmp/openspec-store-drift/store /tmp/openspec-store-drift/bob/store
openspec store register /tmp/openspec-store-drift/bob/store --id team-plans
cd /tmp/openspec-store-drift/project
openspec show auth-tokens --type spec
Store registered: team-plans
Location: /private/tmp/openspec-store-drift/bob/store
OpenSpec root: ready
Registry: already registered
Note: Store 'team-plans' is already registered at this path.
Next: run normal OpenSpec commands against this store, for example:
openspec new change <change-id> --store team-plans
Share it: teammates clone /tmp/openspec-store-drift/store and run openspec store register <path>.
# auth-tokens Specification
## Purpose
TBD - created by archiving change add-auth-tokens. Update Purpose after archive.
## Requirements
### Requirement: Token lifetime
Access tokens MUST be single-use and expire 60 seconds after issuance.
#### Scenario: A token issued an hour ago is replayed
- **WHEN** a client presents a token issued 1 hour ago
- **THEN** the API rejects it with 401
Investigation
The only thing I found that differs between the two "machines"/users is the local checkout path. There is no commit, tag, or hash recorded anywhere.
git diff --no-index /tmp/openspec-store-drift/alice/data/openspec/ /tmp/openspec-store-drift/bob/data/openspec/
diff --git a/tmp/openspec-store-drift/alice/data/openspec/stores/registry.yaml b/tmp/openspec-store-drift/bob/data/openspec/stores/registry.yaml
index e392408..f374350 100644
--- a/tmp/openspec-store-drift/alice/data/openspec/stores/registry.yaml
+++ b/tmp/openspec-store-drift/bob/data/openspec/stores/registry.yaml
@@ -3,5 +3,5 @@ stores:
team-plans:
backend:
type: git
- local_path: /private/tmp/openspec-store-drift/alice/store
+ local_path: /private/tmp/openspec-store-drift/bob/store
remote: /tmp/openspec-store-drift/store
branch field
I also tried setting a SHA on the optional branch field to verify if openspec would warn us that the stored branch and the store's state had diverged
export XDG_DATA_HOME=/tmp/openspec-store-drift/alice/data
export XDG_CONFIG_HOME=/tmp/openspec-store-drift/alice/config
# Pin Alice's registry to Bob's branch via the branch field
B=$(git -C /tmp/openspec-store-drift/bob/store rev-parse HEAD)
sed -i "s#^ type: git# type: git\n branch: $B#" /tmp/openspec-store-drift/alice/data/openspec/stores/registry.yaml
cd /tmp/openspec-store-drift/project
openspec show auth-tokens --type spec | grep -A1 '^### Requirement'
### Requirement: Token lifetime
Access tokens MUST remain valid for 24 hours and MAY be reused.
Alice still resolves to the original checkout (24h), different from the explicitly pinned "branch" - and behind latest. There were no alerts.
Did I miss a setting somewhere? How would we uniquely determine exactly against which shared/parent spec was a given derived spec/feature built?
Thanks for OpenSpec!
Hi!
I was reading the stores-beta user guide and wanted to check one assumption about how teammates stay in sync.
A code repo points at a store by id (
store: team-plansinopenspec/config.yaml), and each machine maps that id to a local checkout withopenspec store register. The guide is clear that "OpenSpec never clones, pulls, or pushes": teammates move the store around with plain git themselves.The part I could not find is anything that records which version of the store a code repo expects. There is no lockfile, and the registry entry stores only a path. So when two teammates have the store checked out at different commits, the same
store: team-planscould resolve to different specs on each machine, and nothing flags the divergence.Concretely, this means that two people can legitimately read directly opposite requirements from what is nominally the same spec.
Reproduction
Version
We'll use scoped XDG folders to emulate the store and three separate users - the author and two consumers:
Shared Store
Let's create a straightforward shared store and commit a spec.
Project
Let's also create a project to consume said shared spec:
User 1 - Alice
Alice sets up the store as it stands today - clone it, register it, read the spec:
Spec change
We then radically change one requirement: tokens become single-use, 60s:
User 2 - Bob
Bob sets up the store in the exact same way:
Investigation
The only thing I found that differs between the two "machines"/users is the local checkout path. There is no commit, tag, or hash recorded anywhere.
branchfieldI also tried setting a SHA on the optional
branchfield to verify ifopenspecwould warn us that the stored branch and the store's state had divergedAlice still resolves to the original checkout (24h), different from the explicitly pinned "branch" - and behind latest. There were no alerts.
Did I miss a setting somewhere? How would we uniquely determine exactly against which shared/parent spec was a given derived spec/feature built?
Thanks for OpenSpec!