openspec/work/simplify-context-and-workspace-model/slices/store-references/spec.md
A project repo can declare, once, which stores its work draws on — and from then on, every agent session in that repo sees an index of those stores' specs inside the instructions it already reads: what exists, one line about each, and the exact command to fetch any of them. Upstream truth stays in the store; downstream work stays in the repo's own root; the connection is a declaration plus citations, never redirection, copy-paste, or per-change links.
This is the headline PM/architect-to-dev layering flow: requirements
live in team-context, the dev's agent writing a low-level design in
the app repo discovers them from config, fetches what it needs with
--store, and cites them.
--store) built live from
the registered checkout at assembly time; the agent fetches what it
needs. Inlining would freeze upstream content at generation time —
the copy-paste failure this effort exists to kill.openspec/config.yaml. A references: list
of store ids, sharing the one id namespace (kebab grammar) locked for
Phase 3.--store action and a separate
change in that store. The fixed precedence (explicit --store →
nearest local root → declared fallback (3.2) → error) gains nothing
from this slice.openspec instructions <artifact> --change ...) and apply instructions (openspec instructions apply) both
carry it, built by one shared assembler. Artifact human mode prints
the <referenced_stores> XML block (mirroring <project_context>);
apply human mode prints a ### Referenced Stores markdown section
matching its existing markdown style (printApplyInstructionsText
is a real human surface — instructions.ts:429-484). No other
command changes.parseSpec() — that
throws on a missing Purpose or Requirements section
(src/core/parsers/markdown-parser.ts:80-86) and the index must
never fail on an imperfect upstream spec. The assembler scans
sections directly; a spec with no Purpose, an unreadable file, or
an unparseable file indexes with an empty summary (rendered as the
bare - <id> line, no dangling colon). No new authoring
requirement on stores.severity/code/message/fix diagnostic shape (severity
warning for all reference codes — JSON consumers must be able to
distinguish degraded context from errors). New codes:
reference_unresolved — the id has no registry entry; fix names
the id concretely: "get a checkout from a teammate and run:
openspec store register <path> --id <the-referenced-id>" (naming
a clone source is 3.3's job).reference_invalid_id — entry fails the kebab id grammar; fix:
use kebab-case ids in references:. (Deliberately distinct from
the CLI's hard-error invalid_store_id: same grammar, different
contract — the index degrades where the CLI refuses.)reference_root_unhealthy — the registry resolved the id but
anything after that failed (missing checkout path, missing or
mismatched store metadata, unhealthy OpenSpec root per
inspectOpenSpecRoot().healthy === false); fix:
openspec store doctor <id>.
A self-reference (the resolved root IS the referenced store, by
canonicalized-path equality or matching resolved store_id) is
omitted with no diagnostic — referencing yourself is meaningless; a
root whose only reference is itself simply gets an empty index.references:, and a session running
--store team-context sees that store's upstream references. But a
referenced store's own references: are never followed: no
recursion, so circular declarations (A↔B) are structurally
harmless.resolveStoreRoot (src/core/root-selection.ts:134-218) owns, via
a non-throwing read-only variant extracted from it — never a
re-implementation. Because that pipeline is async while
generateInstructions is sync (instruction-loader.ts:271), the
index is assembled by an async core helper invoked from the command
layer after root resolution, and passed into the (still-sync)
generators as an input — no async-ification of the instruction
loader. A registry that cannot be read or parsed at all degrades the
same way as everything else: each declared reference indexes with a
reference_registry_unreadable warning (fix:
openspec store doctor).specs: [] (the agent learns the store resolved and holds nothing).
The rendered index shares the spirit of the existing 50KB
project-context cap (project-config.ts:45): if the rendered index
would exceed 50KB, per-store spec lists are truncated
(order-preserving) and the entry carries a
reference_index_truncated warning naming the cap — the agent can
still fetch anything by listing the store directly.references (matching the config key). No workflow
template changes in this slice — templates already direct agents to
read instructions output, and the index is self-describing.readProjectConfig accepts references as an optional array,
keeping string-typed entries (deduplicated, order-preserving) and
dropping only non-strings per the existing resilient style; id
grammar is the assembler's job so invalid ids surface as index
diagnostics instead of being silently dropped at parse time.A PM keeps requirements in the team store. The app repo declares the relationship once:
# app-repo/openspec/config.yaml
schema: spec-driven
references:
- team-context
A dev tells their agent "write the low-level design for billing invoicing". The agent runs the instructions command it already uses:
$ openspec instructions design --change billing-rework
...
<referenced_stores>
<!-- Read-only upstream context. Fetch what you need; cite what you use. -->
Store team-context (/Users/dev/src/team-context):
- billing: Billing must support usage-based invoicing across regions
- auth-sso: Single sign-on requirements for enterprise tenants
Fetch: openspec show <spec-id> --type spec --store team-context
</referenced_stores>
The agent fetches openspec show billing --type spec --store team-context, writes the design in the app repo's own root, and cites
team-context/billing in prose. Nothing redirected the change to the
store; nothing copied the requirement into the repo.
When the store is not registered on this machine, the agent (and the human) see exactly what to do instead of silently missing context:
<referenced_stores>
Store team-context: not registered on this machine.
Fix: get a checkout from a teammate and run: openspec store register <path> --id team-context
</referenced_stores>
In scope:
references: (optional array of store ids) in
ProjectConfigSchema (src/core/project-config.ts:19-41), parsed
with the existing resilient field-by-field style; invalid entries
surface through the index diagnostics, valid entries survive.src/core/, e.g.
references.ts): resolve each id through the shared non-throwing
resolution variant (decision 5), enumerate the referenced root's
openspec/specs/, extract first-line summaries tolerantly
(decision 2), and emit per-store entries
{store_id, root, specs: [{id, summary}], fetch, status: [...]}
(root absolute; fetch the per-store recipe string).generateInstructions
(src/core/artifact-graph/instruction-loader.ts:271-339) gains a
references field; human mode prints the <referenced_stores> block
in the fixed position after the (conditional) <project_context>
block (src/commands/workflow/instructions.ts:171-178) — when
context: is absent, the references block prints in that same slot.generateApplyInstructions
(src/commands/workflow/instructions.ts:282-381) gains the same
field; printApplyInstructionsText gains a ### Referenced Stores
markdown section in its existing style.reference_unresolved, reference_invalid_id,
reference_root_unhealthy, reference_registry_unreadable,
reference_index_truncated), in the established shape.instructions output
carries the index, and the printed fetch command runs verbatim
against the built binary.docs/cli.md's Stores section documenting the references: config
key (no such config-key reference exists today — this subsection is
created, not extended).Out of scope:
store: pointer for rootless repos (3.2).--store flag, or write paths.context: field changes; docs rewrites beyond docs/cli.md's
config-reference section gaining the references: key.openspec/config.yaml with references: [team-context, team-context, BAD ID, other-context, 7][team-context, BAD ID, other-context] (deduplicated, order-preserving, string entries only
— grammar validation is the assembler's job, decision 8)reference_invalid_id warning
naming BAD ID with the kebab-grammar fixreferences: key behaves exactly as todaybilling and auth-ssoopenspec instructions <artifact> --change <id> --json runs
in the app reporeferences: [{store_id: "team-context", root: <absolute path>, specs: [{id, summary}, ...], fetch: "openspec show <spec-id> --type spec --store team-context"}]<referenced_stores> block with the
same informationinstructions apply --change <id> --json carries the same
references fieldreference_unresolved (severity warning) with a fix naming the
referenced id: openspec store register <path> --id <id>reference_root_unhealthy with the openspec store doctor <id> fixreferences: are never followed
(one level deep; A↔B circular declarations cause no recursion)references: [team-context]new change, status, validate, or archive run without
--storereferences: [upstream-context]instructions ... --store team-context --json runsteam-context's references (resolved
root's config, not the cwd's)billing spec carrying a Purpose
section, and an app repo with its own root and a references
declarationinstructions design --change billing-rework --json in the app repo → reads the index → runs the
fetch command → writes a design artifact in the app repo citing
team-context/billing → validate and statusopenspec/changes/, the
store is untouched, and the citation is plain prose in the artifact