openspec/work/simplify-context-and-workspace-model/slices/store-lifecycle-proof/plan.md
Spec locked 2026-06-11 (including same-day review findings: tracked placeholders, Git identity preflight, interactive location prompt, and the enumerated second-checkout journey). Plan drafted 2026-06-11. Implementation not started.
This plan implements spec.md for slice 1.3. The main product move:
Setup leaves a real, clonable Git repo, and the proof is a two-checkout
journey against the built CLI.
Start from spec.md.
Also keep nearby:
../../goal.md../../roadmap.md../store-root-parity/spec.md (root shape, doctor, setup/register safety)../store-root-selection/spec.md (selector semantics, root reporting)Sequencing: this slice changes setup behavior from slice 1.1 and hint/banner
behavior from slice 1.2, so it must stack on that work. The whole roadmap
is being built on the single codex/store-root-parity branch (PR #1190),
whose tip already contains both prerequisite implementations — implement
this slice directly on that branch. Merge to main is deferred until the
work lands as a whole; the old codex/store-root-selection branch is a
stale ancestor of the tip.
What the human wants:
What the agent needs to know:
How the user knows it worked:
context-store setup Git defaults: init on by default, initial
commit of exactly the files setup created, tracked placeholders in
otherwise-empty store directories.--path in non-interactive/JSON mode; an
interactive prompt whose editable suggestion is a user-visible path.unregister escape, named missing root pieces, empty-clone hint).--store <id>, banner prints on
post-resolution failures, new change names a next command, status
drops the Planning home line.test/cli-e2e/.git init plus one initial commit at setup is the entire Git write
surface; doctor reporting is read-only.--fix.view changes (Phase 4), no agent guidance or help one-liners
(slice 1.4), no terminology renames (L7), no archive browsing (L11).openspec init baseline roots (their clone fragility is an L9
baseline quirk, out of scope here).Setup, register, doctor internals:
src/core/context-store/operations.ts (916 lines) owns setup/register/
doctor operations. initGitRepository (line ~277) runs git init;
input.initGit ?? false (line ~472) is the default to flip. Today
.openspec-store/store.yaml is written inside
commitContextStoreRegistration (writeMetadataIfMissing: true,
line ~483) — after Git init — so the metadata write must be decoupled
and moved before the new commit step, or the initial commit will not
contain store.yaml and clones will hit the register conversion prompt.
Register errors live here: requires an existing healthy OpenSpec root
(line ~555), metadata id mismatch (line ~569), and already registered at this path (line ~190). Git inspection currently reports only
isRepository.src/core/context-store/registry.ts raises already registered at <path> (line ~99) with the circular "choose a different context store
id" fix text, and path is already registered as '<id>' (line ~110).src/core/context-store/foundation.ts provides
getDefaultContextStoreRoot (XDG data dir + context-stores/), used as
the silent default path and the interactive prompt suggestion.src/commands/context-store.ts (738 lines) is the command surface:
resolveSetupInput (line ~287) only errors non-interactively when the
id is missing — the path silently defaults; promptContextStorePath
(line ~276) already prompts interactively but suggests the XDG data path;
doctor human/JSON mapping (is_repository, line ~67/146/474); next-steps
output (line ~424).Hint, banner, and status surfaces:
src/core/root-selection.ts has emitStoreRootBanner (line ~300) and
the shared resolver from slice 1.2. Banner emission currently happens on
command success paths; the spec requires it after successful resolution
even when the command then fails.src/commands/workflow/status.ts prints Planning home: <label>
(line ~131) and the storeless hint No active changes. Create one with: openspec new change <name> (line ~75).src/commands/workflow/shared.ts throws storeless hints at lines ~148
and ~169 (No changes found. Create one with: openspec new change <name>).src/commands/workflow/new-change.ts prints the created-change output;
it already knows the schema, so it can name the first artifact's
instructions command as the next step.Test harness:
test/helpers/run-cli.ts spawns the built dist/cli/index.js with cwd
and env injection.test/cli-e2e/basic.test.ts shows the e2e pattern (mkdtemp fixtures,
runCLI, afterAll cleanup).test/commands/context-store.test.ts covers setup/register/doctor and
asserts current defaults (silent XDG path, git off) — these assertions
change.test/commands/store-root-selection.test.ts (32 tests) covers selector
semantics; hint/banner changes touch a few of its expectations.Order of operations inside setup (replaces the current create-then-init sequence):
--path fails with new
diagnostic context_store_setup_path_required, naming example --path
usage. Interactive without --path prompts (existing prompt), with the
editable suggestion changed from getDefaultContextStoreRoot(id) to a
user-visible path such as ~/openspec/<id>. Setup never silently picks
the XDG data directory.initGit defaulted to true
and not opted out, or the target is already a Git repo and a commit will
be attempted): verify git is available (existing error) and that a
commit identity resolves via git var GIT_COMMITTER_IDENT and
git var GIT_AUTHOR_IDENT — these honor config, GIT_*_NAME/EMAIL
environment variables, and fail exactly when git commit would fail.
Do not use git config user.*, which is blind to env-var identity.
Probe cwd: the target directory when it exists, otherwise its existing
parent (safe because nested-Git targets are already rejected, so
repo-local config can only matter when the target itself is a repo). On
failure: new diagnostic context_store_git_identity_missing naming the
exact git config --global user.name/user.email commands. Nothing is
created before this point..gitkeep) inside openspec/specs/ and openspec/changes/archive/
when they end up empty (whether setup created the directories or first
accepted an existing healthy root with empty ones), and
.openspec-store/store.yaml when missing. This requires restructuring:
today the metadata file is written inside
commitContextStoreRegistration (writeMetadataIfMissing: true,
operations.ts line ~483), i.e. after Git init — write it explicitly
in this step instead, so the commit in step 5 can include it. A clone
without committed store.yaml would hit the register conversion
prompt instead of registering without ceremony. All created files,
including placeholders and metadata, join created_files.git init when needed, then an index-preserving pathspec commit
(git add -- <pathspecs> followed by git commit -m "Initialize OpenSpec context store <id>" -- <pathspecs>). The commit set depends
on who owns the repository: when setup initialized it, the pathspecs
are the full store shape (openspec/ plus .openspec-store/) so a
clone of a converted root is healthy; when the repository pre-existed,
the pathspecs are exactly the files setup created, and the pathspec on
commit is what keeps the user's pre-staged files out of setup's commit
and still staged afterward. Old beta files outside the store shape are
never swept in..git/ directory created by this operation is removed on failure too.--no-init-git skips steps 3 and 5 entirely (no identity requirement, no
commit). JSON output gains nothing new beyond created_files accuracy and
the existing git block reporting initialized plus a new committed
boolean.
Placeholder boundaries: placeholders are created by setup when it creates the directories or first accepts an existing unregistered root — never by reruns on an already-registered store (those stay strict no-ops, so pre-slice stores are not retrofitted) and never by register, which stays thin and commit-free. Clone-fragile converted or pre-slice stores are doctor's job to flag (below), not setup's job to repair.
Next-steps output (setup and register success): keep the --store usage
example and add one line: sharing the store is committing and pushing it
like any Git repo.
operations.ts with read-only probes:
git rev-parse --verify HEAD (has commits), git status --porcelain
(uncommitted changes), git remote (remote configured). All three are
nullable when the root is not a repo or Git is unavailable.git section with has_commits,
has_uncommitted_changes, has_remote.has_commits === false: clones of this
repo will be empty until an initial commit exists.openspec/specs/ or openspec/changes/archive/ exists but
contains no tracked files (git ls-files per directory): clones will
lose those directories until they contain a tracked file. This is the
visibility net for converted and pre-slice stores that setup
deliberately does not retrofit.registry.ts already-registered error: replace "choose a different
context store id" with the one-checkout rule and the escape hatch —
names the registered path and openspec context-store unregister <id>
as the way to switch checkouts.operations.ts id-mismatch error: before suggesting --id <metadata-id>,
check whether that metadata id is already registered to another path; if
so, emit the one-checkout guidance instead. Following any register
error's fix text must not land on another register error for the same
situation.root-selection.ts) that formats a
follow-up openspec ... suggestion and appends --store <id> when the
resolved root came from a store. Thread the resolved root into the hint
sites: status.ts (line ~75), shared.ts (lines ~148, ~169), and any
other supported-command hint found by grepping for openspec new change
/ openspec literals in supported command paths.emitStoreRootBanner calls to immediately after successful
resolution in each supported command entry point, so post-resolution
failures still print it.new change: after the created-change lines, print a next-step line
naming the first artifact's instructions command
(openspec instructions <artifact> --change <id> --store <id> when
selected); fall back to openspec status --change <id> if the first
artifact is not cheaply known.status: delete the Planning home: human line; audit status JSON for
workspace vocabulary while keeping the slice 1.2 root block as the
machine-readable source of truth.New file test/cli-e2e/store-lifecycle.test.ts, using runCLI with two
simulated machines (separate XDG_CONFIG_HOME/XDG_DATA_HOME/etc. env
sets) and real git via execFile:
Machine A (project repo without its own root):
context-store setup team-context --path <tmp>/store --json (no Git
flags) — created_files exists only in JSON output, so this step runs
--json and asserts it there (placeholders and store.yaml listed);
repo existence, exactly-one-commit, and committed content (including
store.yaml and placeholders, via git show --name-only) are asserted
on the filesystem. Human-mode next-steps and sharing-line text is
covered in test/commands/context-store.test.ts, not here.context-store list, context-store doctor --json — healthy, git
facts present, no-remote reported as fact not error.new change, status, instructions (write
artifacts via the test as the simulated agent), validate, list,
show, archive — all with --store team-context.changes/archive/, spec promoted into
openspec/specs/, project repo byte-identical (hash the tree before and
after), banners on stderr, stdout payloads clean.Machine B (separate global state):
git clone machine A's store; context-store register <clone> —
succeeds without ceremony; doctor healthy.list --specs / show see the spec promoted by machine A's archived
change (no archive browsing).End-state assertions:
openspec/ artifacts,
.openspec-store/store.yaml, placeholders, and Git state. No
initiative or workspace planning files anywhere, including both
machines' global state; global state holds only registry/config
metadata.Test hygiene: set GIT_CONFIG_GLOBAL/GIT_CONFIG_SYSTEM to isolated
files (or env identity vars) so user gitconfig (signing, hooks, templates)
cannot leak in; configure identity explicitly for the journey, and add one
focused test that unsets identity to cover the preflight failure.
test/commands/context-store.test.ts: setup now errors without --path
non-interactively (was: silent XDG default — my 2026-06-11 probe confirmed
current behavior); git on by default with commit and placeholders;
the initial commit contains store.yaml; a pre-staged unrelated file in
the existing-repo case stays staged and out of setup's commit;
placeholders added when first accepting an existing root with empty
dirs; --no-init-git opt-out; identity preflight failure creates
nothing, and env-var identity (GIT_AUTHOR_*/GIT_COMMITTER_*) passes
the preflight; rerun no-op includes no new commit and no placeholder
retrofit; doctor git facts, commitless warning, and clone-fragile
empty-directory warning; reworked register error texts (assert
non-circular fix text).test/commands/store-root-selection.test.ts: hint expectations gain
--store; banner-on-failure coverage (e.g. instructions apply with no
changes); status output no longer contains Planning home.test/commands/artifact-workflow.test.ts (or wherever status human
output is asserted): drop Planning home expectations; new change
next-step line.test/core/context-store/ for placeholder
creation, staged-paths commit, and identity preflight if the logic is
factored into testable functions.Run order during implementation:
pnpm test -- test/commands/context-store.test.ts
pnpm test -- test/commands/store-root-selection.test.ts
pnpm test -- test/commands/artifact-workflow.test.ts
pnpm run build
pnpm test -- test/cli-e2e/store-lifecycle.test.ts
pnpm test
committed JSON field.store.yaml metadata write from registration so it
happens before the commit; move the machine-local registry write to
last..gitkeep placeholders to empty directories setup creates or
first accepts; include them in created_files and the commit; never on
reruns or via register.git var GIT_COMMITTER_IDENT/GIT_AUTHOR_IDENT with
context_store_git_identity_missing before any file creation; exempt
--no-init-git.--path non-interactively
(context_store_setup_path_required); change the interactive prompt
suggestion to a user-visible path.has_commits,
has_uncommitted_changes, has_remote, plus the commitless warning and
the clone-fragile empty-directory warning.status.ts,
shared.ts, and other supported-command hint sites.new change next-step line.Planning home line from status; audit status output for
workspace vocabulary.test/cli-e2e/store-lifecycle.test.ts (two-checkout journey).init.defaultBranch prompts. Isolate Git config in
every test that touches Git, and keep setup's own Git invocations free of
assumptions about branch names.git add <paths> followed by a bare
git commit would sweep the user's pre-staged unrelated files into
setup's commit. The commit itself must be pathspec-limited
(git commit -- <created paths>) or built on a temporary index; test
with a pre-staged file in the repo.store.yaml is currently written during
registration, after Git init. If it is not written before the commit
step, the initial commit silently omits it and clones lose the
no-ceremony register path — the journey's git show --name-only
assertion is the regression net..git/. If the
commit fails after the registry write is reordered to last, no registry
entry exists to clean up.--store;
deprecated noun-form commands stay untouched (slice 1.2 boundary).--no-init-git needs no identity.status prints no workspace-era vocabulary.