OpenSpec assumed one repo.
Every command asked the same implicit question: which repo am I in? Changes were authored under one repo, specs resolved locally, apply and archive were repo-local lifecycle steps. For normal single-repo work this was fine.
But real features don't always stay in one repo. A behavior change can span a service, a client, and a shared-contract repo. There was nowhere honest to write that one plan down. So it scattered — into Slack threads, side docs, and three half-plans in three repos, none of them the source of truth.
One plan per repo. Either one repo pretends to own it — dishonest — or three parallel plans drift. The cross-repo change is legible nowhere.
A single planning home that doesn't collapse repo ownership. One place to see the whole thing, with each repo still the source of truth for its own specs and execution.
Plan centrally. Execute locally. Don't touch ownership.
The POC committed to a small, opinionated thesis before any code got written:
This ruled out some tempting shortcuts. No new primitive. Users still think in change, not some new "initiative" concept. No new methodology schema. Workspace behavior is a topology concern, not a fork of spec-driven. No fully multi-root-aware CLI across every command. Just the minimum surface to prove the idea.
A workspace is the set of repos, not a feature folder.
The important mental model: one workspace holds many changes over time, across the same set of registered repos. You name a workspace after the coordination surface (a product, a team, a logical slice), not after an individual feature. Changes come and go inside it.
It lives in a managed location so users don't pick a home: $XDG_DATA_HOME/openspec/workspaces/<name>, or ~/.local/share/openspec/workspaces/<name> as the Unix fallback. Inside, committed metadata is deliberately split from local paths:
There's no inner openspec/ directory and no repo-local state anywhere in the workspace. The workspace is a planning root, not a repo. Repo-local openspec/changes/ only shows up inside a target repo once a change is materialized there.
26 phases. One day. A deliberate cadence.
The POC shipped on 2026-04-17 across 26 numbered phases. The order is not an accident — you can read the rhythm off a bar:
Every feature had a test phase right after it. Every risky design decision had a research phase right before it. The four DECISION.md files (phases 07, 10, 13, 18) are short but load-bearing — they pin what's in and what's out of v0 so implementation doesn't drift.
The fixtures came before the feature.
Phase 00 is the tell. Before any workspace create code existed, a sandbox harness shipped — a helper that clones workspace fixtures into temp roots and canonicalizes absolute paths at runtime. That one choice forced everything later to stay honest about what's committed vs. what's local.
Phases 01–04 built on top: workspace create (reusing the existing setup bootstrap instead of forking it), then add-repo and doctor. The registry split is the big idea:
version: 1 name: platform repos: app: description: Application repo api: description: API repo docs: description: Docs repo
version: 1 repoPaths: app: /Users/me/code/app api: /Users/me/code/api docs: /Users/me/code/docs
doctor reports drift (missing mappings, stale paths, extra aliases) and exits non-zero — but never mutates files. The user fixes it.
The central command: new change --targets.
Inside an existing workspace, openspec new change add-auth --targets app,api doesn't just create a change — it partitions it. You get a shared proposal and design at the top, plus a per-target slice underneath, ready to be worked independently. The change lives alongside any other changes already in the workspace.
Key invariant: no repo-local openspec/changes/ gets written yet. The target folders are planning drafts inside the workspace. The repos haven't been touched. That's important for the next act.
When a target becomes a repo-local change.
This is the most interesting decision in the POC. Phase 10 spent an entire research phase locking down the contract in a DECISION.md before any code:
apply is create-only. No overwrite. No refresh. Explicit failure on repeat-apply. A minimal sidecar trace in .openspec.materialization.yaml for later roll-up.
notes/workspace-poc/phase-10-materialization-contract-research/DECISION.md
Then phase 11 shipped it. openspec apply --change add-auth --repo service-a stages the bundle under openspec/changes/ with an atomic rename, writes the sidecar, and says so explicitly in the CLI output. The handoff isn't implicit:
The workspace target slice is the planning truth for that repo. Edits happen in changes/add-auth/targets/service-a/.
The repo-local change is the execution truth for that repo. Edits happen in service-a/openspec/changes/add-auth/. The workspace slice is no longer authoritative.
Create-only is a constraint, not a limitation. It means v0 never has to answer "what gets overwritten and what gets preserved on refresh" — a question the team explicitly deferred until someone has used the create-only version first.
Five states, derived from what's actually on disk.
Phase 13's research made a contrarian call: don't reuse the artifact-graph status primitive. Workspace changes don't have repo-local artifact paths to infer from — you'd be making things up. Instead, derive state from two concrete signals: task checkboxes, and materialization provenance from the sidecar.
Blocked is where the honesty lives. Missing overlay entries, stale repo paths, invalid materialization traces, unreadable task files — all reported. No "unknown" fallbacks that pretend things are fine.
And soft-done ≠ hard-done on purpose. Soft-done means the work looks finished. Hard-done requires an explicit human action. That action is the next chapter.
Hard-done is a marker, not an accumulation.
A workspace change becomes hard-done when, and only when, the user runs openspec archive <id> --workspace. That writes a single field to the workspace change's metadata:
Repo-local archive — running archive inside a target repo — has zero effect on workspace completion. Without that rule, hard-done would silently accumulate from repo-local actions taken by different people at different times. The workspace owner would lose the one place they can say "this cross-repo effort is done."
Phase 20 was an audit. It found three gaps.
After Archive, it looked done. Phase 19 even ran a consolidated acceptance suite — happy path, interruption/re-entry, failure recovery — all passing. Signoff was next.
Instead, phase 20 audited the shipped POC against the PRD line by line and came back with three concrete product gaps:
Four remediation phases got inserted before sign-off: owner metadata + docs + an update-repo command (21–22), and workspace targets --add/--remove with a guardrail that blocks removal after repo-local execution (23–24). Phase 25 did the final audit and flipped the PRD from Draft to Signed Off.
Nine commands. One coherent flow.
This is what a user actually types. The first three lines are one-time workspace setup. Everything after runs inside the same persistent workspace, and you'd run the change → apply → status → archive block again per cross-repo feature, reusing the same workspace over and over:
No new primitive. No new methodology. The workspace is the durable surface that knows how to coordinate a set of repos; change is still what the user reaches for, one per feature.
The POC is signed off. Here's what was left open.
Phase 18 deferred three real questions to post-POC work — shared-contract promotion, stable project IDs, team-shared workspaces — and designed a backward-compatible migration seam so v0's alias-keyed contract can grow into them later:
If you want to go deeper, each take-away prompt below is a handoff back into Claude against this repo's actual notes: