diff --git a/.changeset/stores-upstream-work.md b/.changeset/stores-upstream-work.md new file mode 100644 index 0000000000..d29ba5e6c7 --- /dev/null +++ b/.changeset/stores-upstream-work.md @@ -0,0 +1,29 @@ +--- +"@fission-ai/openspec": minor +--- + +### New Features (stores beta) + +- **Upstream links** — `openspec new change --serves /` links a change to the store work it implements; the link wires context automatically (the store is referenced in the repo's config, the repo is recorded machine-locally for rollups), and the change's instructions carry the upstream artifacts to read first +- **Downstream rollup** — `openspec list --downstream [--store ]` shows each of the root's changes and every change on this machine that serves it, live from task checkboxes; archived upstream work stays visible and resolvable +- **Built-in `requirements` schema** — a documentation-only workflow (proposal → specs); fresh stores default to it, so a store is ready for shared planning with zero configuration +- **`openspec schema init` upgrades** — accepts `--store ` to scaffold into a registered store, and seeds per-stage `instructions/.md` files (guidance agents receive, editable per stage) alongside templates +- **Schema inheritance** — repos that declare `references: []` resolve the store's schemas (project → referenced stores → user → built-in); `openspec schemas` labels inherited entries with their source store and accepts `--store ` +- **Schema `notes:` and instruction files** — a schema's top-level `notes:` are surfaced verbatim on every instruction surface; `instructions/.md` beside the schema wins over inline `instruction:` +- **`structure:` config, now executable** — declare any folders or files in a root's layout (keys ending `/` are folders, other keys files); surfaced in `openspec context` and the references index, materialized by `store setup ` (markdown files seeded with their purpose), and drift is flagged by `store doctor` +- **`/opsx:store` — one door** — a generated skill (core profile) that reads machine state and routes the whole loop conversationally: set up, draft, link with `--serves`, show status, archive; every mechanic stays a deterministic CLI command underneath +- **One-command store setup** — `openspec store setup ` no longer requires `--path` (defaults to `~/openspec/`) and its output teaches the draft → serve → rollup loop +- **Stateless CI rollups** — `openspec list --downstream --scan ` scans a directory of checkouts with no per-machine state, so a CI job can publish team-wide status +- **Whole-change rollup status** — a serving change only counts as complete when its tasks AND its schema's artifacts are done; half-done work renders both counts +- **Custom stages in `schema init`** — `--artifacts` accepts custom kebab-case stage names in workflow order (each stage requires the previous); `schema validate` and `schema which` accept `--store ` + +- **Intent stays honest** — a serving change's instructions carry a divergence rule (flag upstream instead of silently diverging), and the built-in `requirements` workflow now asks for explicit non-goals and measurable success signals, with requirement-traceability enforced in its specs guidance + +### Fixes + +- Legacy `initiative:` change metadata is tolerated in both the object and string forms (read-only, never re-emitted) instead of failing `status` +- `openspec new change` no longer names the wrong schema in its progress line when the root's config sets a default +- `openspec schema init --default` now writes the `schema:` key the config reader uses (previously wrote `defaultSchema:`, which nothing read), and its next-step hint prints a valid command +- `openspec validate` no longer demands delta specs from changes whose workflow schema defines no specs artifact (deliberately spec-less workflows validated with a spurious error) +- New specs created by archiving seed their Purpose from an explicit `## Purpose` in the delta spec, then the proposal's Why section, instead of a TBD placeholder +- Instruction output no longer ships an empty `` placeholder; `status` prints a glyph legend when artifacts are blocked; archiving a change whose schema has no tasks phase says "not tracked by this schema" instead of "No tasks" diff --git a/docs/README.md b/docs/README.md index 2e3df67b1c..1d7c93a7c9 100644 --- a/docs/README.md +++ b/docs/README.md @@ -90,6 +90,7 @@ That second one matters more than it looks. OpenSpec has two halves: a command l | Doc | What it gives you | |-----|-------------------| | [Stores: User Guide](stores-beta/user-guide.md) | Plan in its own repo when your work spans repos or teams | +| [Upstream Work](stores-beta/upstream-work.md) | Upstream work in a store: one door, your workflows, serves links, live status | | [Agent Contract](agent-contract.md) | The machine-readable CLI surfaces agents drive | ## The thirty-second version diff --git a/docs/agent-contract.md b/docs/agent-contract.md index dae386b9f7..90acd56a32 100644 --- a/docs/agent-contract.md +++ b/docs/agent-contract.md @@ -45,7 +45,7 @@ Successful JSON payloads embed the root: ## 4. Command JSON shapes ### 4.1 `list --json` -`{ "changes": [ { "name", "completedTasks", "totalTasks", "lastModified", "status": "no-tasks"|"complete"|"in-progress" } ], "root": RootOutput }` — note the per-change `status` is a string enum here. `--specs`: `{ "specs": [ { "id", "requirementCount" } ], "root" }`. +`{ "changes": [ { "name", "completedTasks", "totalTasks", "lastModified", "status": "no-tasks"|"complete"|"in-progress" } ], "root": RootOutput }` — note the per-change `status` is a string enum here. `--specs`: `{ "specs": [ { "id", "requirementCount" } ], "root" }`. `--downstream`: `{ "downstream": { "path", "upstream": [ { "id", "exists", "archived", "changes": [ { "id", "store"?, "repo"?, "completedTasks", "totalTasks", "completedArtifacts"?, "totalArtifacts"?, "state": "complete"|"in-progress"|"no-tasks" } ], "changesComplete", "changesTotal", "tasksComplete", "tasksTotal" } ] } | null, "root" }` — `downstream` is null when the root has no `openspec/changes/` folder; each `upstream` entry is one of the root's changes plus every change on this machine that serves it. Serving changes are discovered by scanning `.openspec.yaml` files for `serves: ` (the root's own changes) and `serves: /` (changes in other registered roots and linked repos pointing at this store's change); `"exists": false` marks a ref whose target change is not on disk, `"archived": true` one whose target has been archived; `state: "complete"` requires tasks checked off AND (when the serving change's schema is readable) all artifacts present — `completedArtifacts`/`totalArtifacts` carry the artifact-graph progress and are absent when the schema cannot be resolved; `changes[].store`/`changes[].repo` name where a serving change lives, absent = the rolled-up root. `--scan ` (repeatable, with `--downstream`) additionally scans the directory and its immediate subdirectories for serving changes — stateless, for machines with no linked-root records (CI). Outside any root (and without `--store`), the command answers with registered store rollups instead: `{ "downstream": null, "stores": [ { "store", "rollup": } ], "root": null }`. ### 4.2 `show --json` Change: `{ "id", "title", "deltaCount", "deltas": [...], "root" }`. Spec: `{ "id", "title", "overview", "requirementCount", "requirements": [...], "metadata": { "version", "format", "sourcePath"? }, "root" }`. @@ -57,15 +57,15 @@ Change: `{ "id", "title", "deltaCount", "deltas": [...], "root" }`. Spec: `{ "id `{ "changeName", "schemaName", "planningHome"?: { "kind", "root", "changesDir", "defaultSchema" }, "changeRoot", "artifactPaths": { "": {outputPath, resolvedOutputPath, existingOutputPaths} }, "nextSteps": ["..."], "actionContext": { "mode": "repo-local", "sourceOfTruth": "repo", "planningArtifacts", "linkedContext", "allowedEditRoots", "requiresAffectedAreaSelection", "constraints" }, "isComplete", "applyRequires", "artifacts": [ {id, outputPath, status: "done"|"ready"|"blocked", missingDeps?} ], "root" }`. No active changes: `{ "changes": [], "message", "root" }`, exit 0. ### 4.5 `instructions --json` -`{ "changeName", "artifactId", "schemaName", "changeDir", "planningHome"?, "outputPath", "resolvedOutputPath", "existingOutputPaths", "description", "instruction"?, "context"?, "rules"?, "references"?: ReferenceIndexEntry[], "template", "dependencies": [{id,done,path,description}], "unlocks", "root" }`. +`{ "changeName", "artifactId", "schemaName", "changeDir", "planningHome"?, "outputPath", "resolvedOutputPath", "existingOutputPaths", "description", "instruction"?, "schemaNotes"?, "context"?, "rules"?, "references"?: ReferenceIndexEntry[], "upstream"?, "template", "dependencies": [{id,done,path,description}], "unlocks", "root" }` — `schemaNotes` carries the schema's top-level `notes:` verbatim; `upstream` is present when the change's metadata declares `serves:` and carries `{ "ref", "changeId", "store"?, "path": string|null, "archived" }` (path null = link is stale or the store is not registered here). -`ReferenceIndexEntry`: `{ "store_id", "root"?, "specs"?: [{id,summary}], "fetch"?, "status": [] }` — resolved entries carry root/specs/fetch; unresolved carry store_id + warning status. Index capped at 50KB (`reference_index_truncated`). +`ReferenceIndexEntry`: `{ "store_id", "root"?, "specs"?: [{id,summary}], "schemas"?: [{id,summary,artifacts}], "changes"?: [string], "structure"?: {"": ""}, "fetch"?, "status": [] }` — resolved entries carry root/specs/fetch; `schemas` (the store's own project-local artifact types), `changes` (its in-motion change ids), and `structure` (its config-declared folder purposes) are present only when the store defines them; unresolved carry store_id + warning status. Index capped at 50KB (`reference_index_truncated`). ### 4.6 `instructions apply --json` -`{ "changeName", "changeDir", "schemaName", "contextFiles": { "": ["/abs", ...] }, "progress": {total,complete,remaining}, "tasks": [{id,description,done}], "state": "blocked"|"all_done"|"ready", "missingArtifacts"?, "instruction", "references"?, "root" }`. +`{ "changeName", "changeDir", "schemaName", "contextFiles": { "": ["/abs", ...] }, "progress": {total,complete,remaining}, "tasks": [{id,description,done}], "state": "blocked"|"all_done"|"ready", "missingArtifacts"?, "instruction", "schemaNotes"?, "references"?, "upstream"?, "root" }` — `schemaNotes` and `upstream` as in 4.5. ### 4.7 `new change --json` -Success: `{ "change": { "id", "path", "metadataPath", "schema" }, "root" }`. Failure: `{ "change": null, "status": [d] }`, exit 1. +Success: `{ "change": { "id", "path", "metadataPath", "schema" }, "serves"?, "root" }` — `serves` is present when `--serves ` was passed and carries `{ "ref", "resolved", "reference_wiring"?: "added"|"already"|"skipped" }` (`resolved` = the upstream change was found on disk at link time; `reference_wiring` reports the automatic `references:` config edit). Failure: `{ "change": null, "status": [d] }`, exit 1. ### 4.8 `archive --json` Success: `{ "archive": { "change", "archivedAs": "YYYY-MM-DD-name", "path", "specsUpdated", "totals"? }, "root" }`. Failure: `{ "archive": null, "root"?, "status": [d] }`, exit 1. JSON mode is strictly non-interactive: every prompt point becomes an `archive_*` code. @@ -74,13 +74,13 @@ Success: `{ "archive": { "change", "archivedAs": "YYYY-MM-DD-name", "path", "spe `{ "root": { "path", "source", "store_id"?, "healthy", "status": [] }, "store": { "id", "metadata": {present,valid,remote?}, "origin_url"?, "status": [] } | null, "references": [...], "status": [] }`. Health findings of any severity exit 0. Failure payload: `{ "root": null, "store": null, "references": [], "status": [d] }`, exit 1. ### 4.10 `context --json` -`{ "root": { "path", "source", "store_id"?, "role": "openspec_root" }, "members": [ { "role": "referenced_store", "id", "path"?, "remote"?, "fetch"?, "status": [] } ], "status": [] }`. AVAILABLE = path present AND status empty. `--code-workspace ` writes `{folders:[{name,path}]}` (available referenced stores only, `ref:` prefixes); in JSON mode the write runs before printing so stdout holds exactly one document even on write failure. Failure: `{ "root": null, "members": [], "status": [d] }`, exit 1. +`{ "root": { "path", "source", "store_id"?, "role": "openspec_root" }, "members": [ { "role": "referenced_store", "id", "path"?, "remote"?, "fetch"?, "artifactTypes"?: [string], "changes"?: [string], "structure"?: {"": ""}, "status": [] } ], "status": [] }`. AVAILABLE = path present AND status empty. `artifactTypes` (the store's own project-local schema names), `changes` (its in-motion change ids), and `structure` (its config-declared layout — keys ending `/` are folders, other keys files) are present only on available members whose store defines them. `--code-workspace ` writes `{folders:[{name,path}]}` (available referenced stores only, `ref:` prefixes); in JSON mode the write runs before printing so stdout holds exactly one document even on write failure. Failure: `{ "root": null, "members": [], "status": [d] }`, exit 1. ### 4.11 `store ... --json` setup/register: `{ "store": {id, root, metadata_path?}, "registry": {path, registered, already_registered}, "git": {is_repository, initialized, committed}, "created_files": [], "status": [] }`. unregister/remove: `{ "store", "registry": {path, removed}, "files": {deleted, deleted_path, left_on_disk}, "status": [] }`. list: `{ "stores": [{id, root}], "status": [] }`. doctor: `{ "stores": [ { id, root, metadata_path?, openspec_root: {...healthy, status}, metadata: {present, valid, id?, remote}, git: {is_repository, has_commits, has_uncommitted_changes, has_remote, origin_url}, status } ], "status": [] }` (`null` = unknown/not probed). Health findings exit 0; failures exit 1 with the matching null-shape. Prompt cancellation exits 130. ### 4.12 `schemas --json` / `templates --json` -`schemas`: bare array `[ {name, description, artifacts, source} ]`. `templates`: keyed object `{ "": {path, source} }`. Both cwd-based, no root/status keys. +`schemas`: bare array `[ {name, description, artifacts, source, store?} ]` — `source` is `project|store|user|package`; `store` names the referenced store an inherited schema comes from. Supports `--store ` to list a store's schemas (success stays a bare array; a store-resolution failure emits `{status: [...]}` like other commands). `templates`: keyed object `{ "": {path, source} }`, cwd-based, no `--store`. ## 5. Exit-code contract @@ -133,5 +133,5 @@ Recorded by the capstone audit; published-key renames are product decisions defe 4. Four parallel envelope type declarations exist in src; archive diagnostics never carry `target`. 5. `list --json` reuses the `status` key as a string enum per change. 6. Only `validate` output carries a `version` field. -7. `schemas`/`templates` ignore root selection (cwd-based, no `--store`). +7. `schemas` honors root selection (`--store ` lists a store's schemas); `templates` is still cwd-based (no `--store`). 8. Deprecated noun forms (`change`/`spec` subcommands) emit unenveloped payloads without `root`/`status`. diff --git a/docs/cli.md b/docs/cli.md index fb591f2bcc..3e97680528 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -44,20 +44,20 @@ These commands support `--json` output for programmatic use by AI agents and scr | Command | Human Use | Agent Use | |---------|-----------|-----------| -| `openspec list` | Browse changes/specs | `--json` for structured data | +| `openspec list` | Browse changes/specs; `--downstream` for the upstream rollup | `--json` for structured data | | `openspec show ` | Read content | `--json` for parsing | | `openspec validate` | Check for issues | `--all --json` for bulk validation | | `openspec status` | See artifact progress | `--json` for structured status | | `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 | +| `openspec schemas` | List available schemas (including ones inherited from referenced stores) | `--json` for schema discovery | | `openspec store setup ` | Create and register a local store | `--json` with explicit inputs for structured setup output | | `openspec store register ` | Register an existing store | `--json` for structured registration output | | `openspec store unregister ` | Forget a local store registration | `--json` for structured cleanup output | | `openspec store remove ` | Delete a registered local store folder | `--yes --json` for non-interactive deletion | | `openspec store list` | Browse registered stores | `--json` for structured registrations | | `openspec store doctor` | Check local store setup | `--json` for structured diagnostics | -| `openspec new change ` | Create repo-local change scaffolding | `--json`, plus `--store ` to use a registered store as the OpenSpec root | +| `openspec new change ` | Create change scaffolding; `--serves ` links it to upstream work | `--json`, plus `--store ` to use a registered store as the OpenSpec root | | `openspec workset create [name]` | Compose a personal working view | `--member --json` for non-interactive composition | | `openspec workset list` | Browse saved worksets | `--json` for structured views | | `openspec workset remove ` | Delete a saved view | `--yes --json` for non-interactive removal | @@ -202,15 +202,15 @@ openspec store setup [id] [options] | `--no-init-git` | Skip every Git action: no init, no initial commit | | `--json` | Output JSON | -Non-interactive runs (`--json`, scripts, agents) must pass both the store id and `--path`. In an interactive terminal, setup prompts for the location with an editable suggestion in a visible, user-owned place (for example `~/openspec/`); it never defaults to OpenSpec's managed data directory. +`--path` is optional: setup defaults to `~/openspec/` (interactive runs prompt with that suggestion prefilled). A fresh store's changes default to the built-in `requirements` workflow — see the [upstream work guide](stores-beta/upstream-work.md). Examples: ```bash openspec store setup openspec store setup team-context -openspec store setup team-context --path ~/openspec/team-context --no-init-git -openspec store setup team-context --path ~/openspec/team-context --no-init-git --json +openspec store setup team-context --no-init-git +openspec store setup team-context --path ~/plans/team-context --json ``` ### `openspec store register` @@ -390,7 +390,10 @@ openspec list [options] |--------|-------------| | `--specs` | List specs instead of changes | | `--changes` | List changes (default) | +| `--downstream` | Show the root's changes and every change on this machine that serves them | +| `--scan ` | With `--downstream`: also scan a directory of checkouts — stateless, for CI (repeatable) | | `--sort ` | Sort by `recent` (default) or `name` | +| `--store ` | Store id to use as the OpenSpec root | | `--json` | Output as JSON | **Examples:** @@ -402,6 +405,9 @@ openspec list # List all specs openspec list --specs +# Where does the team's upstream work stand? +openspec list --downstream --store team-plans + # JSON output for scripts openspec list --json ``` @@ -639,14 +645,21 @@ prefix it with a word, for example `ticket-123-add-notifications` instead of | `--description ` | Description to add to `README.md` | | `--goal ` | Optional goal metadata to store with the change | | `--schema ` | Workflow schema to use | +| `--serves ` | Link this change to the work it serves: `` or `/` | | `--store ` | Store id to use as the OpenSpec root (a store is a standalone OpenSpec repo you've registered) | | `--json` | Output JSON | +`--serves` records the link in the change's metadata and wires everything +else automatically: the repo is recorded (machine-local) so +`openspec list --downstream` finds it, and the store is added to the repo's +`references:` so agents there see its context. + Examples: ```bash openspec new change add-billing-api openspec new change add-billing-api --store team-context --json +openspec new change add-billing-ui --serves team-context/billing-revamp ``` ### `openspec status` @@ -804,6 +817,8 @@ Templates: ### `openspec schemas` List available workflow schemas with their descriptions and artifact flows. +Includes schemas inherited from referenced stores, labeled with their +source store. ``` openspec schemas [options] @@ -813,6 +828,7 @@ openspec schemas [options] | Option | Description | |--------|-------------| +| `--store ` | Store id to use as the OpenSpec root | | `--json` | Output as JSON | **Example:** @@ -860,12 +876,19 @@ openspec schema init [options] | Option | Description | |--------|-------------| | `--description ` | Schema description | -| `--artifacts ` | Comma-separated artifact IDs (default: `proposal,specs,design,tasks`) | +| `--artifacts ` | Comma-separated stage ids **in workflow order** — built-in (`proposal,specs,design,tasks`) or custom kebab-case names; each stage requires the previous | | `--default` | Set as project default schema | | `--no-default` | Don't prompt to set as default | | `--force` | Overwrite existing schema | +| `--store ` | Store id to use as the OpenSpec root (scaffold into a registered store) | | `--json` | Output as JSON | +Each artifact is one stage of the workflow, ordered by `requires:` in +`schema.yaml`. The scaffold includes `instructions/.md` files — +per-stage guidance agents receive, seeded from the built-in workflow — and +`templates/` for each stage's output shape. `--default` sets `schema: ` +in the root's `openspec/config.yaml`. + **Examples:** ```bash diff --git a/docs/customization.md b/docs/customization.md index 3c20a1d657..e4ea9fb623 100644 --- a/docs/customization.md +++ b/docs/customization.md @@ -269,7 +269,7 @@ 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. +> **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. Repos that declare `references:` to a store additionally inherit the store's schemas (resolution order: project → referenced stores → user → built-in) — see the [upstream work guide](stores-beta/upstream-work.md). --- diff --git a/docs/opsx.md b/docs/opsx.md index e396890add..87159087f1 100644 --- a/docs/opsx.md +++ b/docs/opsx.md @@ -158,6 +158,7 @@ rules: | Command | What it does | |---------|--------------| | `/opsx:propose` | Create a change and generate planning artifacts in one step (default quick path) | +| `/opsx:store` | One door to shared upstream work: set up a store, draft, link with `--serves`, status, archive (stores beta) | | `/opsx:explore` | Think through ideas, investigate problems, clarify requirements | | `/opsx:new` | Start a new change scaffold (expanded workflow) | | `/opsx:continue` | Create the next artifact (expanded workflow) | diff --git a/docs/stores-beta/upstream-work.md b/docs/stores-beta/upstream-work.md new file mode 100644 index 0000000000..98e95ff854 --- /dev/null +++ b/docs/stores-beta/upstream-work.md @@ -0,0 +1,175 @@ +# Upstream work: typed, linked, and live in a store + +> Specs are what is true. Changes are what is in motion. **Upstream work is +> just a change in the store — typed by a schema the store defines.** + +A team that plans before code moves (product briefs, platform contracts, +requirements handoffs) does not need a new folder convention. It needs the +two primitives OpenSpec already has, working one level up: + +| Need | Mechanism | +|------|-----------| +| "Our upstream work has its own shape" | a **schema in the store** (`openspec/schemas//`) | +| "Engineers' work should trace to ours" | a **serves link** (`openspec new change --serves /`) | +| "What is true, standing, forever" | the store's **specs/** — archive syncs them, as always | +| "Where does everything stand" | `openspec list --downstream --store ` | + +**And there is one door for all of it:** repos with OpenSpec skills get +`/opsx:store` — a conversational entry point that reads the machine's +state, routes to the one thing that makes sense (set up, draft, link, +status, archive), runs the commands itself, and ends every reply with +numbered next moves. Users talk; the agent drives; the commands below are +what it drives. + +All output below is from a real run. + +## The store defines the workflow + +A fresh store needs no setup: its changes default to the built-in +`requirements` workflow (proposal → specs, documentation-only — its `notes:` +tell agents there is no implementation phase). To encode your own chain, +scaffold a schema and add one artifact per handoff: + +```text +$ openspec schema init product-flow --artifacts proposal,specs --store product-team +✔ Created schema 'product-flow' + +Each artifact is a stage — order them with requires: in schema.yaml. +Guidance agents get per stage lives in instructions/.md. +``` + +A schema is a folder. Long-form guidance lives in files beside it, and a +top-level `notes:` tells agents how this workflow differs from the default: + +```text +product-hub/openspec/ + config.yaml # schema: product-brief + structure: map + schemas/product-brief/ + schema.yaml # brief → requirements, notes: no implementation phase + instructions/brief.md # per-artifact guidance as files, like templates + instructions/requirements.md + templates/… + research/ # declared in config `structure:` so agents know + decisions/ # what these folders are for +``` + +Planning work is then an ordinary change, with review gates and artifact +dependencies enforced by the graph: + +```text +$ openspec new change onboarding-revamp --store product-hub +Schema: product-brief +``` + +There is no apply phase in this schema — the schema's `notes:` say so, and +every instruction surface repeats it to the agent verbatim +(`` in `openspec instructions`). When the requirements are +approved, archiving syncs them into the store's `specs/`. + +## Downstream repos inherit and link + +A repo that references the store sees its schemas the same way it already +sees its specs — resolution order is project → referenced stores → user → +package: + +```text +$ openspec schemas # in web-app, which references product-hub + product-brief (from store 'product-hub') +``` + +Linking a change to the work it serves does all the wiring in one flag: +it records the checkout so rollups find this repo (machine-local, nothing +committed), and references the store so agents here see its context: + +```text +$ openspec new change add-welcome-tour --serves product-hub/onboarding-revamp +Serves: product-hub/onboarding-revamp (rollup: openspec list --downstream --store product-hub) +Referenced store 'product-hub' in openspec/config.yaml — agents here now see its context. +``` + +The serving change's instructions open with the upstream context on disk — +the intent travels without anyone pasting it, along with one standing rule: +if implementation shows an upstream requirement is wrong, flag it upstream +instead of silently diverging, so intent stays honest: + +```text + +This change serves the change 'onboarding-revamp' in store 'product-hub'. +Upstream context: ~/openspec/product-hub/openspec/changes/onboarding-revamp +Before working, read its artifacts (proposal, specs, and any others its schema defines); … + +``` + +## Status flows back live + +Rollup is discovered from facts on disk (one `serves:` line per change, task +checkboxes, artifact presence), never from prose, and works from anywhere. +A ✓ means the whole change is done — checked-off tasks with missing +artifacts render as in-progress with both counts, so nobody trusts work +that skipped its own workflow: + +```text +$ openspec list --downstream --store product-hub + +onboarding-revamp 1/2 serving changes complete + ✓ add-invite-endpoint api 2/2 tasks + · add-welcome-tour web-app 1/2 tasks +``` + +Truth flows up: archive the upstream change when its requirements land, and +the link keeps resolving — marked so downstream agents trace against specs: + +```text +onboarding-revamp 1/2 serving changes complete (archived — its requirements now live in specs/) +``` + +## Any format, one opinion + +Artifacts are fully open — any filename, any document format, any template. +A team's existing product-spec format is a 15-line schema away, and a +change made of nothing but that document creates, gates, and archives +cleanly. OpenSpec holds exactly one format opinion, at the truth boundary: +deltas you want merged into standing `specs/` at archive time use the +requirement/scenario format. Freeform artifacts archive as-is, and +validation is schema-aware: a workflow that defines no specs artifact is +never asked for deltas. + +## Team-wide status + +The rollup reads checkouts on the machine it runs on — pull a teammate's +repo and their work appears. For an always-current team view, run the same +command somewhere that already has every repo. `--scan ` rolls up a +directory of checkouts with no per-machine state at all, so a CI job is +four lines (run against a real fresh environment): + +```text +$ git clone plans +$ git clone checkouts/… +$ openspec store register ./plans --id team-plans +$ openspec list --downstream --store team-plans --scan ./checkouts + +onboarding-revamp 0/2 serving changes complete + · api-implements api 1/2 tasks (1/4 artifacts) + · web-app-implements web-app 1/2 tasks (1/4 artifacts) +``` + +Publish the `--json` form of that output and the whole team has one answer +without any new machinery. + +Schemas version the same way everything in a store does: git. Pulling the +store updates the workflow for every repo that references it; a repo that +needs to pin or diverge defines a project-local schema with the same name, +which wins. + +## Reference + +| Surface | What it carries | +|---------|-----------------| +| `.openspec.yaml` | `serves: ` or `serves: /` | +| `schema.yaml` | optional `notes:` (workflow guidance, surfaced verbatim to agents) | +| `/instructions/.md` | per-artifact instruction files; a file wins over inline `instruction:` | +| `config.yaml` | `structure:` — layout map, surfaced in `context` and the references index; keys ending `/` are folders, other keys are files; `store setup ` materializes anything missing (markdown files are seeded with their purpose) and `store doctor` flags drift | +| `openspec schema init [--store ]` | scaffold a workflow: schema.yaml + instructions/ + templates/ | +| `openspec schemas [--store ]` | includes inherited schemas with their source store | +| `openspec list --downstream [--store ]` | the rollup; outside any root it shows every registered store's | +| `openspec context` | referenced stores show artifact types, in-motion changes, and layout | diff --git a/docs/stores-beta/user-guide.md b/docs/stores-beta/user-guide.md index 3711777e15..b946fdd72b 100644 --- a/docs/stores-beta/user-guide.md +++ b/docs/stores-beta/user-guide.md @@ -55,18 +55,19 @@ Two rules keep this simple: Two commands take you from nothing to a working, store-scoped change: ```bash -openspec store setup team-plans --path ~/openspec/team-plans +openspec store setup team-plans # defaults to ~/openspec/team-plans ``` ``` Store ready: team-plans -Location: /Users/you/openspec/team-plans +Location: ~/openspec/team-plans OpenSpec root: ready Registry: registered -Next: run normal OpenSpec commands against this store, for example: - openspec new change --store team-plans -Share this store by committing and pushing it like any Git repo. +Next: + openspec new change --store team-plans # draft requirements here + openspec new change --serves team-plans/ # in any repo: link work to them + openspec list --downstream --store team-plans # where everything stands ``` ```bash @@ -76,7 +77,7 @@ openspec new change add-login --store team-plans ``` Using OpenSpec root: team-plans (/Users/you/openspec/team-plans) Created change 'add-login' at /Users/you/openspec/team-plans/openspec/changes/add-login/ -Schema: spec-driven +Schema: requirements Next: openspec status --change add-login --store team-plans ``` @@ -85,6 +86,23 @@ That's the whole model. From here the lifecycle is exactly what you know — on each command, and every printed hint carries the flag for you. The `Using OpenSpec root:` line always tells you where a command is acting. +Prefer to skip the commands entirely? Repos with OpenSpec skills get +`/opsx:store` — one conversational door that sets up, drafts, links, and +archives by running all of this for you. + +Two defaults worth knowing: + +- A fresh store's changes use the built-in **`requirements`** workflow + (proposal → specs, nothing to implement) — a store holds shared planning, + not code work. One line in the store's `openspec/config.yaml` changes it, + and `openspec schema init --store team-plans` scaffolds a workflow + of your own. +- Any repo can link a change to the store work it implements: + `openspec new change --serves team-plans/`. Context wires + itself, and `openspec list --downstream --store team-plans` shows where + everything stands. The full loop lives in + [Upstream Work](upstream-work.md). + ## Story: one team, one planning repo A team keeps its specs and changes in `team-plans` instead of scattering @@ -93,8 +111,7 @@ them across code repos. **Day one (whoever sets it up):** ```bash -openspec store setup team-plans --path ~/openspec/team-plans \ - --remote git@github.com:acme/team-plans.git +openspec store setup team-plans --remote git@github.com:acme/team-plans.git git -C ~/openspec/team-plans push -u origin main ``` @@ -185,6 +202,13 @@ summary and the exact fetch command (`openspec show --type spec upstream payment requirements, cite them, and write its low-level design in the repo's own root — without anyone pasting context around. +References are the read-only half. When a product team's change +*implements* something the platform team is drafting, link it — +`openspec new change --serves platform-reqs/` — and the +platform team sees live progress with +`openspec list --downstream --store platform-reqs`. See +[Upstream Work](upstream-work.md). + A reference can carry its clone source, so teammates who don't have the store yet get a complete fix instead of a dead end: @@ -316,12 +340,16 @@ tells you which case you're in. `openspec/config.yaml` declares `store: ` is treated as externalized planning, not as a store checkout to register. Remove the `store:` line first if you intentionally want to convert that repo into a local store root. -- **Some commands stay where they are.** `view`, `templates`, `schemas`, - and the deprecated noun forms (`openspec change show`, ...) act on the - current directory only — no `--store`. -- **Per-machine state is per-machine.** The store registry and worksets - are local settings. Nothing about your machine's layout is - ever committed to shared planning. +- **Some commands stay where they are.** `view`, `templates`, and the + deprecated noun forms (`openspec change show`, ...) act on the current + directory only — no `--store`. (`schemas` now accepts `--store` — see + [Upstream Work](upstream-work.md).) +- **Per-machine state is per-machine.** The store registry, worksets, and + the upstream-link records that power `list --downstream` are local + settings. The rollup sees the checkouts on *this* machine — pull your + teammates' repos to see their work, or point `list --downstream --scan + ` at a directory of clones (stateless; how CI gets the team view). + Nothing about your machine's layout is ever committed to shared work. - **Two launch styles for worksets.** A tool that can't be launched with a workspace file or per-folder attach flags can't be added as an opener. - **Agent JSON has a known casing split** (store-family keys are @@ -336,6 +364,7 @@ tells you which case you're in. | A store's planning | `/openspec/` (specs, changes) | Yes — commit and push it | | A store's identity | `/.openspec-store/store.yaml` | Yes — committed with the store | | The store registry | `/openspec/stores/registry.yaml` | No — this machine only | +| Repos linked via `--serves` | `/openspec/stores/linked-roots.yaml` | No — this machine only | | Worksets | `/openspec/worksets/` | No — this machine only | `` is `~/.local/share/openspec` on macOS and Linux (or diff --git a/openspec/changes/add-upstream-links/.openspec.yaml b/openspec/changes/add-upstream-links/.openspec.yaml new file mode 100644 index 0000000000..b119b63505 --- /dev/null +++ b/openspec/changes/add-upstream-links/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-07-13 diff --git a/openspec/changes/add-upstream-links/design.md b/openspec/changes/add-upstream-links/design.md new file mode 100644 index 0000000000..19d5374e74 --- /dev/null +++ b/openspec/changes/add-upstream-links/design.md @@ -0,0 +1,64 @@ +# Design + +## The one decision + +Upstream work is not a new artifact kind. It is a change in a store, typed +by the store's schema. Everything else follows from refusing the parallel +ontology: + +| Need | Not this (initiatives experiment) | This | +|---|---|---| +| A finite piece of work above code | `initiatives//` folder | a change in the store | +| An ordered workflow with handoffs | numbered stage folders | schema artifacts with `requires:` | +| Guidance per stage | (none — freeform) | `instructions/.md` + review gates | +| Standing truths | unnumbered "evergreen" files | the store's `specs/`, synced by archive | +| The store's own layout | (nothing) | `structure:` map — materialized by setup, checked by doctor | +| Traceability | `initiative:` line to a folder | `serves:` line to a change | +| Status | folder scan + task counts | rollup grouped by upstream change; done = tasks AND artifacts | +| Entry point | a skill that owned the mechanics | a thin skill routing deterministic commands | + +The pull-based rule the experiment liked ("read everything lower-numbered, +produce what your stage owes the next") is the artifact dependency graph: +your artifact's `requires` are satisfied → read them → draft yours. + +## Deterministic rails, judgment in agents + +Every mechanic is a CLI command computed from disk state: refs validated +before anything is written, status derived from checkboxes and artifact +presence (never recorded), rollup discovered from one metadata line (no +manifest), missing/archived targets rendered visibly. The `/opsx:store` +skill is deliberately mechanics-free — it reads three JSON commands, +routes to one situation, and ends with numbered next moves — so agents +cannot drift and humans/CI can run the same rails directly. `--scan ` +exists because linked-root records are per-machine: a stateless CI clone +directory gets the identical rollup. + +## Schema resolution order + +`project → referenced stores (declaration order, one hop) → user → package`. +The resolver reads the registry synchronously and degrades to "no inherited +schemas" on any unreadable state, because resolution runs deep in sync call +chains and must never turn a broken registry into a failed command. The +same tolerance pattern governs everything schema-aware added here +(whole-change rollup status, delta-less validation, archive phrasing): an +unresolvable schema always falls back to the stricter, older behavior. + +## The truth boundary + +Archive remains the only place OpenSpec holds a format opinion: deltas +merged into standing `specs/` use the requirement/scenario format; +everything else archives as-is. New specs seed Purpose from the best +available intent (delta `## Purpose` → proposal `## Why` → placeholder), +and the divergence rule in the upstream block catches drift while work is +still in motion instead of only at archive time. + +## Compatibility + +- Legacy `initiative:` metadata stays readable in both shapes the beta + wrote (object and string), never re-emitted; `--initiative` stays + registered as a hidden removed option pointing at `--serves`. +- `schemas --json` stays a bare array; entries gain `source: 'store'` + + `store` for inherited schemas. `structure:` JSON surfaces stay + folder/file → purpose strings. +- Existing commands keep their shapes; every addition is a new key, flag, + or command. diff --git a/openspec/changes/add-upstream-links/proposal.md b/openspec/changes/add-upstream-links/proposal.md new file mode 100644 index 0000000000..ffbaa4679b --- /dev/null +++ b/openspec/changes/add-upstream-links/proposal.md @@ -0,0 +1,68 @@ +# Upstream work in stores: links, rollups, workflows, one door + +## Why + +Teams decide what to build before code exists, and that work spans repos. +Stores gave it a home; nothing gave it motion — no link from implementing +changes to the upstream work they serve, no cross-repo status, no way for +a store to define its own workflow or layout, and no simple entry point. +The initiatives experiment answered with a new untyped layer; users with +custom schemas showed the better shape: upstream work is a change in a +store, and everything else is the existing machinery working one level up. + +## What Changes + +- **Serves links**: `serves: ` / `/` in change + metadata via `new change --serves ` — validated up front, and the + one flag wires everything: records the checkout for rollups + (machine-local) and auto-adds `references:` to the repo's config. +- **Live rollup**: `list --downstream [--store ]` groups every change + on the machine under the upstream change it serves; complete means the + WHOLE change (tasks and schema artifacts); missing refs stay visible, + archived targets resolve and are marked; `--scan ` rolls up a + directory of checkouts statelessly (CI needs no linked-root records). +- **Context flows down**: instructions for a linked change open with an + `` block — the upstream change's on-disk path, a directive to + trace to a requirement, and the divergence rule (flag upstream instead + of silently diverging). +- **Workflows are data**: schema `notes:` surfaced verbatim to agents; + per-stage `instructions/.md` files; inheritance through + `references:` (project → referenced stores → user → built-in); + `schema init` scaffolds custom stage names in the order given, into a + store via `--store`; a built-in `requirements` workflow (proposal → + specs with non-goals, success signals, and requirement traceability) + is the default for fresh stores. +- **Layout is data**: `structure:` in config declares any folders or + files (keys ending `/` are folders); `store setup` materializes what is + missing (markdown seeded with its purpose, never overwriting) and + `store doctor` flags drift. +- **One door**: a generated core-profile skill (`/opsx:store`) reads + machine state via JSON commands, routes to the one applicable move + (set up / draft / continue / link / status / archive / custom schema), + and ends every reply with numbered next steps — zero mechanics of its + own. +- **Truth boundary made schema-aware**: `validate` requires deltas only + from schemas that define a specs artifact; new specs seed Purpose from + the delta's own Purpose section or the change's stated intent; archive + says "not tracked by this schema" instead of implying a missing + tasks.md is a defect. +- **Removed**: the initiatives folder convention, stages, evergreen + files, `list --initiatives`, and the initiatives skill; the noun + returns to retired vocabulary, legacy metadata stays readable in both + shapes it ever had. + +## Capabilities + +### New Capabilities +- `upstream-links`: the serves link, downstream rollup (including `--scan` + and whole-change completion), and upstream context block +- `schema-openness`: notes, instruction files, inheritance, custom-stage + scaffolding, executable structure declarations + +## Impact + +CLI (`new change --serves`, `list --downstream --scan`, `schema init/ +validate/which --store`, store setup/doctor), core (`src/core/upstream.ts` +new; resolver, project config, references, archive, validate), generated +skills (new `store` workflow in the core profile), stores-beta docs and +the agent contract. diff --git a/openspec/changes/add-upstream-links/specs/schema-openness/spec.md b/openspec/changes/add-upstream-links/specs/schema-openness/spec.md new file mode 100644 index 0000000000..11b7465765 --- /dev/null +++ b/openspec/changes/add-upstream-links/specs/schema-openness/spec.md @@ -0,0 +1,46 @@ +# Schema Openness + +## ADDED Requirements + +### Requirement: Schemas inherit through references + +Schema resolution SHALL consult the stores a root references, in declaration +order, between project-local and user-level schemas. Unreadable config, +registry, or checkouts SHALL degrade to "no inherited schemas", never an +error. `openspec schemas` SHALL label inherited schemas with their store. + +#### Scenario: A consumer repo uses the store's workflow +- **GIVEN** a repo whose config declares `references: [product-hub]` and a store defining `schemas/product-brief/` +- **WHEN** the user runs `openspec new change spike --schema product-brief` in the repo +- **THEN** the change is created with the store's schema and its artifact graph gates work as usual + +### Requirement: Schema authors can speak to agents + +A schema MAY carry top-level `notes:`; every instruction surface SHALL +render it verbatim. Per-artifact guidance MAY live in +`instructions/.md` beside the schema (and `instructions/apply.md` +for the apply phase); a file SHALL win over an inline `instruction:` value. + +#### Scenario: A documentation-only workflow steers the agent +- **GIVEN** a schema whose notes say it has no implementation phase +- **WHEN** any artifact's instructions render +- **THEN** the notes appear in a `` block before the artifact guidance + +### Requirement: A store can declare its layout, and the layout is real + +Config MAY carry `structure:` — a map of folders (keys ending `/`) and +files to their purpose. `openspec context` and the references index SHALL +surface it; `openspec store setup ` SHALL materialize declared entries +that are missing (seeding markdown files with their purpose, never +overwriting, confined to `openspec/`); `openspec store doctor` SHALL flag +declared entries missing on disk. + +#### Scenario: Downstream agents see the store's layout +- **GIVEN** a store whose config declares `structure: { research/: raw inputs }` +- **WHEN** a referencing repo runs `openspec context` +- **THEN** the store's member entry lists `research/` with its purpose + +#### Scenario: Declared layout materializes on setup +- **GIVEN** a store whose config declares a folder and a markdown file that do not exist +- **WHEN** `openspec store setup ` runs again +- **THEN** the folder exists, the file exists seeded with its purpose, and nothing pre-existing was overwritten diff --git a/openspec/changes/add-upstream-links/specs/upstream-links/spec.md b/openspec/changes/add-upstream-links/specs/upstream-links/spec.md new file mode 100644 index 0000000000..e3a88ee878 --- /dev/null +++ b/openspec/changes/add-upstream-links/specs/upstream-links/spec.md @@ -0,0 +1,71 @@ +# Upstream Links + +## ADDED Requirements + +### Requirement: Changes declare the work they serve + +A change's metadata MAY carry `serves: ` (same root) or +`serves: /` (a registered store). `openspec new change +--serves ` SHALL validate the ref before creating anything, write the +metadata line, record the repo as a linked root, and add the store to the +repo's `references:` when it can do so safely. + +#### Scenario: Linking wires context automatically +- **GIVEN** a repo with no reference to store `product-hub` +- **WHEN** the user runs `openspec new change add-tour --serves product-hub/onboarding-revamp` +- **THEN** the change is created with `serves: product-hub/onboarding-revamp`, the repo is recorded machine-locally for rollups, and `references: [product-hub]` is added to `openspec/config.yaml` + +#### Scenario: A malformed ref creates nothing +- **GIVEN** any repo +- **WHEN** the user passes `--serves a/b/c` +- **THEN** the command fails with a validation message and no change directory is left behind + +### Requirement: The downstream rollup is discovered, not maintained + +`openspec list --downstream` SHALL show each of the root's changes with +every change on this machine that serves it, with task progress, scanning +the root, other registered stores, and linked roots. Refs whose target is +missing SHALL render visibly; archived targets SHALL be marked as archived. + +#### Scenario: Cross-repo rollup from one metadata line +- **GIVEN** changes in two repos whose metadata serves `product-hub/onboarding-revamp` +- **WHEN** the user runs `openspec list --downstream --store product-hub` +- **THEN** both serving changes appear under `onboarding-revamp` with their repo names and task counts + +#### Scenario: Archived upstream stays traceable +- **GIVEN** the upstream change has been archived +- **WHEN** the rollup or a serving change's instructions render +- **THEN** the link resolves to the archived copy and states that its requirements now live in specs + +### Requirement: Rollups work without per-machine state + +`openspec list --downstream --scan ` SHALL also scan the directory +and its immediate subdirectories for serving changes, so a machine with no +linked-root records (CI) can produce the team rollup from clones alone. + +#### Scenario: A CI machine rolls up from fresh clones +- **GIVEN** a machine with no OpenSpec state, a cloned store registered by path, and code repos cloned under `./checkouts` +- **WHEN** `openspec list --downstream --store --scan ./checkouts` runs +- **THEN** serving changes from the scanned clones appear in the rollup + +### Requirement: Complete means the whole change + +A serving change SHALL count as complete only when its tasks are checked +off AND (when its schema is resolvable) all of its schema's artifacts +exist; half-done work SHALL render both counts. + +#### Scenario: Checked tasks with missing artifacts is not done +- **GIVEN** a serving change with all tasks checked but only one of four schema artifacts present +- **WHEN** the rollup renders +- **THEN** the change shows as in-progress with both task and artifact counts + +### Requirement: Instructions carry upstream context + +Instruction surfaces for a serving change SHALL open with an `` +block naming the upstream change, its on-disk path (or that it is missing), +and the rollup command. + +#### Scenario: Intent travels without pasting +- **GIVEN** a change with `serves: product-hub/onboarding-revamp` and the store registered locally +- **WHEN** the user runs `openspec instructions --change ` +- **THEN** the output contains the upstream change's absolute path and directs the agent to read its artifacts before working diff --git a/openspec/changes/add-upstream-links/tasks.md b/openspec/changes/add-upstream-links/tasks.md new file mode 100644 index 0000000000..db4a814e23 --- /dev/null +++ b/openspec/changes/add-upstream-links/tasks.md @@ -0,0 +1,43 @@ +# Tasks + +## Remove the parallel ontology +- [x] Delete initiatives module, skill, CLI mode, and folder conventions +- [x] Restore retired-vocabulary policing; keep legacy `initiative:` metadata readable + +## Upstream links +- [x] `serves:` metadata + `new change --serves` with full wiring (linked roots, auto-references) +- [x] `openspec list --downstream` rollup (own root, stores, linked repos; missing/archived visible) +- [x] `` context block in instructions and apply instructions + +## Schema openness +- [x] Top-level `notes:` parsed and surfaced on every instruction surface +- [x] `instructions/.md` and `instructions/apply.md` files beside schemas +- [x] Inheritance through `references:` in resolver, listings, and `schemas --store` labels +- [x] `structure:` config declarations surfaced in context and the references index + +## Happy path +- [x] Built-in `requirements` schema; fresh stores default to it +- [x] `schema init`: `--store` support, per-stage instruction files, `--default` key fix +- [x] Store setup output teaches the loop; skills carry `--serves` guidance + +## Hardening from independent sessions +- [x] Whole-change rollup status: complete = tasks AND schema artifacts (half-done shows both counts) +- [x] `--scan `: stateless rollup for CI (no linked-root records needed) +- [x] `schema init` accepts custom stage names, wired sequentially in the order given +- [x] `schema validate` / `schema which` accept `--store` +- [x] Divergence rule in the `` block (flag upstream, never silently diverge) +- [x] Schema-aware validate (spec-less workflows exempt from delta requirement) +- [x] Purpose seeding: delta `## Purpose` → proposal `## Why` → placeholder +- [x] Requirements workflow sharpened: non-goals, success signals, requirement traceability +- [x] Legacy string-form `initiative:` refs readable; rollup noise cut + +## Layout and the one door +- [x] `structure:` executable: folders and files materialized by `store setup`, drift flagged by `store doctor` +- [x] `/opsx:store` skill (core profile): thin state-routed entry point, generated on plain init + +## Verification +- [x] Unit tests: upstream module, resolver inheritance, structure parsing, references index +- [x] CLI tests: --serves validation and wiring, downstream rollup, context enrichment +- [x] Full suite green; golden skill hashes regenerated +- [x] Real end-to-end runs captured for docs and PR body (single-repo, cross-repo on a real product, CI-machine rollup) +- [x] Cold-start session (help + guides only) completed the loop first attempt; multi-stage custom chain driven through every gate diff --git a/openspec/initiatives/context-store-and-initiatives/.initiative.yaml b/openspec/initiatives/context-store-and-initiatives/.initiative.yaml deleted file mode 100644 index c67efbeda8..0000000000 --- a/openspec/initiatives/context-store-and-initiatives/.initiative.yaml +++ /dev/null @@ -1,27 +0,0 @@ -version: 1 -id: context-store-and-initiatives -title: Context Store And Initiatives Direction -status: exploring -summary: > - Define the direction for a synced context store, mounted collections, - initiatives, local workspaces, and repo-local changes. -owners: [] -artifacts: - readme: README.md - direction: direction.md - roadmap: roadmap.md - tasks: tasks.md - decisions: decisions.md - questions: questions.md - work_items: work-items/ -linked_changes: - - change: workspace-reimplementation-roadmap - relationship: informs - - change: workspace-agent-guidance - relationship: reframes - - change: workspace-apply-repo-slice - relationship: reframes - - change: workspace-verify-and-archive - relationship: reframes -links: [] -metadata: {} diff --git a/openspec/initiatives/context-store-and-initiatives/README.md b/openspec/initiatives/context-store-and-initiatives/README.md deleted file mode 100644 index 6574a7d4ca..0000000000 --- a/openspec/initiatives/context-store-and-initiatives/README.md +++ /dev/null @@ -1,58 +0,0 @@ -# Context Store And Initiatives - -Status: transition evidence / beta history. - -This folder preserves the beta context-store and workspace direction, the -decisions made while exploring it, and the evidence that led to the simpler -Git-native model. - -It is not the active product roadmap or implementation queue. For current -direction, start with: - -1. `openspec/work/simplify-context-and-workspace-model/goal.md` -2. `openspec/work/simplify-context-and-workspace-model/roadmap.md` - -The `direction-git-native-work.md` note is the transition note that led to the -current goal. If it conflicts with the current `goal.md`, the current `goal.md` -wins. - -## Reading Order - -Use this reading order when researching the beta history: - -1. `direction-git-native-work.md` explains the transition from the old beta - model toward Git-native specs and work. -2. `direction.md` preserves the earlier context-store and initiative direction. -3. `roadmap.md` preserves the historical beta roadmap snapshot. -4. `tasks.md` preserves historical initiative-wide progress. -5. `decisions.md` records accepted decisions made during the beta. -6. `questions.md` tracks questions that were open at the time. -7. `work-items//` contains execution notes for one historical roadmap item. - -## Boundary - -These artifacts preserve product intent, roadmap decisions, and beta evidence -from the old model. OpenSpec specs describe the current behavioral contract -behind the code. - -Do not rewrite specs for future intent until behavior changes with an -implementation slice. - -The earlier product boundary was: - -```text -Context stores sync truth. -Collections shape truth. -Initiatives coordinate work. -Workspaces open local views. -Changes implement repo-owned slices. -``` - -The newer direction is: - -```text -OpenSpec is a Git-native artifact format for specs and work. - -Specs are what is true. -Work is what is in motion. -``` diff --git a/openspec/initiatives/context-store-and-initiatives/decisions.md b/openspec/initiatives/context-store-and-initiatives/decisions.md deleted file mode 100644 index e2aa873d6d..0000000000 --- a/openspec/initiatives/context-store-and-initiatives/decisions.md +++ /dev/null @@ -1,225 +0,0 @@ -# Context Store And Initiatives Decisions - -## 2026-05-20: Track Roadmap Execution Inside The Initiative - -Decision: Track initiative roadmap implementation inside -`openspec/initiatives/context-store-and-initiatives/` rather than creating an -OpenSpec change for each roadmap item. - -Why: The initiative is the durable coordination object for this work. Repo-local -OpenSpec changes should be reserved for implementation slices owned by a repo or -team. Roadmap-item tracking belongs with the initiative until a task needs a -repo-owned implementation plan. - -Implications: - -- Use `tasks.md` as the initiative-wide progress dashboard. -- Use `work-items//` for detailed execution notes on one roadmap item. -- Link repo-local OpenSpec changes back to the initiative later when - implementation moves into a repo-owned slice. - -## 2026-05-20: Lock Workspace-To-Initiative Product Boundary - -Decision: Workspaces are local working views, not durable shared planning -objects. Durable coordination belongs to context stores and initiatives. Repo -local changes own implementation. - -Implications: - -- Preserve workspace setup, link, relink, list, open, update, and doctor as - beta local-view infrastructure. -- Treat workspace-planning behavior as beta or transitional compatibility. -- Defer workspace apply, verify, and archive until initiative-linked repo-local - changes exist. - -## 2026-05-21: Leave Specs Alone Until Behavior Changes - -Decision: Do not use the initial direction lock to rewrite OpenSpec specs. -Specs should describe the current behavioral contract behind the code. The -initiative artifacts should carry product intent, roadmap decisions, and future -direction until a later implementation change deliberately updates behavior and -its specs together. - -Implications: - -- Initial Item 1 cleanup should focus on initiative docs, historical roadmap - artifacts, active proposal disposition, and user-facing docs. -- Existing workspace-planning specs and schemas may continue to describe current - implemented behavior. -- Future changes to specs should happen with the behavior they govern. - -## 2026-05-21: Keep Deferred Workspace Changes As Reference Placeholders - -Decision: Keep the active workspace changes for agent guidance, repo-slice -apply, verify/archive, and the reimplementation roadmap as deferred reference -placeholders. - -Why: These areas are still expected to matter after context stores, initiatives, -and initiative-linked repo-local changes exist. Archiving or deleting them now -would lose useful research and continuity. - -Implications: - -- Do not pick them up as the immediate next implementation focus. -- Treat their current proposals as historical/deferred direction. -- Revisit and reframe them after initiative-linked repo-local changes define the - durable handoff model. - -## 2026-05-21: Generated Workspace Guidance Routes Work By Ownership - -Decision: Generated workspace guidance should describe workspaces as local -working views and route durable work to the owning artifact: initiatives own -cross-team or cross-repo intent, repo-local OpenSpec changes own implementation -plans, and linked repos or folders own their implementation. - -Why: The initiative direction supersedes the older model where a workspace-level -`changes/` tree owned the canonical shared cross-repo plan. New agent guidance -should not reinforce that old model. - -Implications: - -- Remove guidance that tells agents to use workspace-level `changes/` as the - planning home for coordinated work. -- Keep legacy or beta workspace-planning files readable as compatibility - context when present. -- Update generated workspace guidance before broad user-facing docs or specs. -- Leave specs untouched until the corresponding behavior intentionally changes. - -## 2026-05-21: Workspace Action Context Is Local Compatibility Context - -Decision: Workspace-planning action context should no longer describe -workspace-level artifacts as the source of truth. It should report -`sourceOfTruth: "workspace-local"` and describe workspace-local planning -artifacts as compatibility context for the current local view. - -Why: Workspace-planning artifacts can still exist in the beta workflow, but the -initiative direction assigns durable coordination to initiatives and -implementation planning to repo-local changes. - -Implications: - -- Keep `actionContext.mode: "workspace-planning"` for compatibility. -- Keep `allowedEditRoots: []` until an explicit edit root is selected. -- Keep linked repos and folders as context, not implicit edit roots. -- Route durable coordination to initiatives when initiative context exists. - -## 2026-05-21: Reorder Roadmap Around Agent-First Initiative Handoff - -Decision: Treat initiatives as an agent-first workflow. Users should be able to -prompt an agent with intent like "using initiative X, explore Y and create a -proposal"; OpenSpec should provide small CLI primitives the agent can compose. - -Why: The practical UX is not a human manually typing every coordination command. -Agents need reliable structured answers about where canonical initiative context -lives and how repo-local changes reference it. Local paths come from workspace -state, not from an initiative command. - -Implications: - -- Promote minimal context-store setup, registration, listing, and doctoring - before workspace initiative opening. -- Add `initiative show --json` before broader progress/status concepts. -- Connect repo-local changes with checked-in initiative metadata, not checked-in - snapshots of initiative prose. -- Do not add `initiative resolve`; workspace local-view state owns local path - mapping. -- Teach workspace opening about initiatives after show and repo-change linkage - semantics exist. - -## 2026-05-26: Workspace Initiative Opening Uses Generated Runtime Files - -Decision: Treat workspace initiative opening as a private local view record plus -generated runtime files. The workspace does not contain the work. It remembers -how this runtime opens the work. - -Why: Initiative context is shared truth in the context store, repo-local changes -own implementation, and agent/editor affordances need to exist in the runtime -where the agent actually runs. Persisting generated files as workspace truth -would blur local view state with shared coordination and create stale or -privacy-sensitive artifacts. - -Implications: - -- Persist only tiny private local view choices: selected store, selected - initiative, selected local links, opener, and selected tools. -- Preserve the selected context-store selector inside the private workspace - record, so a runtime-local `--store-path` open can be reopened without writing - machine-local paths into checked-in repo metadata. -- Generate agent guidance, skills, launch prompts, and editor workspace files as - runtime support when opening or preparing a view. -- Open existing local paths only; do not clone, branch, create worktrees, use - submodules, or infer local repos in Item 10. -- Treat generated runtime files as disposable and regenerable. -- Allow context-only initiative open; linked repos are optional local view - choices. -- Keep edit boundaries advisory in Item 10 until enforcement is designed. - -## 2026-05-26: Workspace Storage Is Keyed By Workspace Name - -Decision: Store private workspace views under -`getGlobalDataDir()/workspaces//`. The workspace name is the -local identity. The selected context store and initiative, if any, live inside -one durable private `workspace.yaml` record. - -Why: Workspaces are generic local views, not initiative-owned directories. A -user may want a custom workspace with linked repos and folders but no initiative, -or multiple personal workspaces over the same initiative. Keying storage by -store and initiative would overfit the filesystem layout to one workflow. - -Implications: - -- Keep initiative references optional inside `workspace.yaml`. -- Store initiative context with an explicit context-store binding rather than a - flat store id, because workspace state may need to remember a registry selector - or a runtime-local path selector. -- Generate `AGENTS.md`, opener workspace files, and tool-specific skills at the - managed workspace root. -- Keep `workspace.yaml` as the only view file for Item 10; do not add a separate - machine-readable view file. -- Do not introduce a separate generated-output directory for Item 10. -- If the user opens an initiative without a workspace name, derive a friendly - default workspace name from the initiative id when that is unambiguous. -- On workspace-name collisions or multiple workspaces pointing at the same - initiative, ask the human to choose or require an explicit workspace name in - non-interactive mode. - -## 2026-05-26: Item 10 Workspace Open UX Decisions - -Decision: Close the remaining Item 10 product decisions around runtime identity, -JSON output, Codex Desktop, edit boundaries, and implementation scope. - -Implications: - -- Use `getGlobalDataDir()` as the cross-platform runtime-local boundary. Do not - add path translation or a separate runtime id in Item 10. -- Keep `workspace open --json` as a machine-facing receipt for the same open - operation. It should return useful generated paths, selected context, opened - roots, skipped roots, opener, launch status, and warnings. -- Do not add `--prepare-only` for Item 10. -- For Codex Desktop, open the generated workspace root as the project and expose - attached initiative and repo/folder paths through generated guidance and - `workspace open --json` output. -- Emit advisory edit boundaries only; do not enforce write restrictions. -- Continue to open known existing local paths only. Do not clone, branch, create - worktrees, use submodules, or infer local repos in Item 10. - -## 2026-05-30: Defer Hardcoded Agent Handoff Guidance - -Decision: Skip Item 13, agent handoff output and delivery polish, as an -implementation item for now. - -Why: The underlying beta pain is real: users and agents need better receipts -after setup, initiative creation, workspace opening, and repo-local change -creation. However, fixed "Next for your agent" guidance assumes a linear -workflow path and may not fit dynamic agentic work, where the agent should -inspect current state and choose the next move. - -Implications: - -- Do not implement hardcoded next-step blocks yet. -- Preserve Item 13 as research context for a future receipt or affordance model. -- Prefer future output that reports what exists, where it lives, and what - actions are available, rather than prescribing one next command. -- Deterministic receipt improvements such as direct `created_paths` fields may - be split into a smaller implementation slice if they remain clearly useful. -- Delivery terminology concerns may be handled separately from handoff output. diff --git a/openspec/initiatives/context-store-and-initiatives/direction-git-native-work.md b/openspec/initiatives/context-store-and-initiatives/direction-git-native-work.md deleted file mode 100644 index a24b346d02..0000000000 --- a/openspec/initiatives/context-store-and-initiatives/direction-git-native-work.md +++ /dev/null @@ -1,472 +0,0 @@ -# Git-Native Specs And Work Direction - -This note captures the current product direction after the initiative, -workspace, context-store, and multi-repo planning discussion. - -The positive shape is: - -```text -OpenSpec is a Git-native artifact format for specs and work. - -Specs are what is true. -Work is what is in motion. -``` - -OpenSpec artifacts live as files in Git. That Git repo may be the code repo, a -planning repo, or a contracts repo. OpenSpec should not introduce a separate -authoritative state system outside those files. - -## Core Shape - -The preferred future shape is: - -```text -openspec/ - README.md - openspec.yml - specs/ - work/ -``` - -- `specs/` describes accepted behavior. -- `work/` describes intended effort in motion. - -This shape should be the same whether the OpenSpec root lives beside code or in -a dedicated planning or contracts repo. - -```text -app-repo/ - openspec/ - specs/ - work/ - -planning-repo/ - openspec/ - specs/ - work/ -``` - -There is no separate product mode for "repo-local", "external", "workspace", -"context store", or "multi-repo" artifacts. The placement choice is simply -which Git repo contains the OpenSpec files. - -## Vocabulary - -Use a small vocabulary first: - -```text -Spec current accepted behavior -Work intended effort in motion -Change work that applies concrete deltas to targets -Initiative work that coordinates or decomposes other work -Target repo, service, package, path, or system where work lands -``` - -Users should not need to learn `context store`, `project`, `workspace`, -`artifact home`, or `index` as primary product nouns. - -## Domain Terms - -Use these terms when explaining the near-term product: - -```text -OpenSpec root - The `openspec/` directory that contains specs, changes, work, and config. - -In-project OpenSpec - OpenSpec initialized inside the project repo it helps describe. - -Standalone OpenSpec repo - A separate Git repo whose main purpose is to hold OpenSpec artifacts. - -Target project repo - A code repo that a change or work item applies to. - -Local repo map - Private local resolution from a target repo id to a checkout path. - -Workspace view - Legacy or beta local-view language. In the new direction, this should reduce - to a local repo map plus an optional focused OpenSpec root or work item. -``` - -Examples: - -```text -In-project OpenSpec: - -app-repo/ - openspec/ - specs/ - changes/ - -Standalone OpenSpec repo: - -app-openspec-repo/ - openspec/ - specs/ - changes/ - -Target project repo: - -app-repo/ - src/ - tests/ -``` - -The product should avoid the term `repo-local` for this distinction. It is too -easy to confuse "OpenSpec lives in this project repo" with "this work targets -this repo." - -The product should also avoid making `workspace` a primary user-facing noun. -The job that remains is simpler: map target repo ids to local checkout paths so -agents and commands can assemble the relevant Git repos on this machine. - -## Work Is The Primitive - -`work/` is one canonical area for units of work at different scales. - -```text -openspec/ - specs/ - auth/session-limits.md - work/ - add-login-rate-limit/ - work.yaml - proposal.md - tasks.md - deltas/ - checkout-modernization/ - work.yaml - README.md -``` - -A change is work with change capabilities: - -```yaml -id: add-login-rate-limit -kind: change -status: proposed -targets: - - repo: app -``` - -An initiative is also work: - -```yaml -id: checkout-modernization -kind: initiative -status: active -children: - - work: add-login-rate-limit - - work: add-checkout-tax -``` - -The distinction between a change and an initiative should not come from which -top-level folder the artifact lives in. It should come from metadata and -capabilities: - -- Work with targets and deltas can validate and archive those deltas into - `specs/`. -- Work with children, dependencies, and context can coordinate and roll up other - work. -- Some work may be both change-shaped and coordination-shaped. - -## Git Is The Source Of Truth - -OpenSpec should stay Git-native: - -- History comes from Git. -- Review uses normal Git and forge workflows. -- Diffs are normal file diffs. -- External planning means another Git repo, not another state system. -- Indexes, dashboards, status rollups, and orchestration are derived views. - -Forge-specific status such as pull request state, CI, review approvals, or -merge status may be read by adapters. That status should not become a competing -OpenSpec truth. - -## Targets - -Filesystem location should not imply implementation target. Work declares where -it lands. - -```yaml -targets: - - repo: api - - repo: web -``` - -Targets may later address repos, services, packages, paths, external systems, -or monorepo subtrees. Use plural `targets` in the format early, even if some MVP -lifecycle commands only support one target. - -## Nesting And References - -The rule is: - -```text -Nest within a repo. -Reference across repos. -``` - -Within one Git repo, work can nest when that is the real relationship: - -```text -app-repo/ - openspec/ - work/ - checkout-modernization/ - work.yaml - work/ - add-login-rate-limit/ -``` - -Across Git repo boundaries, work references other work by stable identity: - -```yaml -id: checkout-modernization -kind: initiative -children: - - repo: api - work: add-tax-api - - repo: web - work: update-checkout-ui -``` - -This keeps each repo's executable work close to the code it affects while still -allowing a planning or contracts repo to coordinate the larger effort. - -Work identity must come from metadata, not from the path. Folder paths can help -humans browse; they should not be the durable identity of the work. - -## Dependency And Sequencing - -Multi-repo complexity is mostly about sequencing, not folder placement. - -OpenSpec should be able to record dependency intent in Git: - -```yaml -depends_on: - - work: publish-tax-contract -``` - -Future views can answer: - -- How does this large effort decompose? -- What has to happen first? -- Which targets are affected? -- Which teams own the slices? -- What surrounding context does an agent need? - -The free artifact format should be able to describe ordering and dependencies. -Automation that enforces sequencing, gates merges, or rolls up live forge status -can remain a derived orchestration layer. - -## MVP Implication - -The immediate release path should keep the current OpenSpec baseline working: - -```text -openspec/ - README.md - openspec.yml - specs/ - changes/ -``` - -The first mental model is: - -```text -Specs = what is true. -Changes = what should change. -``` - -Near-term work should not require the future `work/` layout. `change` remains -important because a change applies deltas. The `work/` model is the future -layout direction, not a prerequisite for making standalone OpenSpec repos -useful. - -## Roadmap - -### 1. Preserve The Current Baseline - -Keep the existing in-project OpenSpec flow working and understandable: - -```text -app-repo/ - openspec/ - specs/ - changes/ -``` - -The first release goal is not to rename everything. It is to make the current -model boring and reliable. - -### 2. Make The Placement Choice Explicit - -Teach the product language: - -```text -OpenSpec can live inside your project repo, -or in its own Git repo. -``` - -Use: - -- `in-project OpenSpec` for `app-repo/openspec/` -- `standalone OpenSpec repo` for `app-openspec-repo/openspec/` - -Avoid `repo-local` as the user-facing term for this split. - -### 3. Support Standalone OpenSpec Repos - -Allow OpenSpec to be initialized and validated in a Git repo that does not hold -application code: - -```text -app-openspec-repo/ - openspec/ - specs/ - changes/ -``` - -This should use the same parser, templates, validation, and archive concepts as -in-project OpenSpec. A standalone repo is not a new state system. - -### 4. Add Target Project Repo Resolution - -Standalone OpenSpec repos need to describe where changes land: - -```yaml -targets: - - repo: app -``` - -The first slice can keep target resolution simple: - -- register local target repos -- validate that referenced targets exist -- report unresolved targets clearly -- let agents know which OpenSpec repo and target repos are involved - -Do not clone, branch, sync, orchestrate, or infer complex repo state yet. - -This is the simplified successor to the larger workspace-view concept. Existing -workspace beta behavior may remain as compatibility, but new direction should -use local repo mapping as the product shape. - -### 5. Add Cross-Repo Context And Doctoring - -Once standalone OpenSpec repos can target project repos, add read-oriented -support for relevant context: - -- doctor checks for missing target repo mappings -- local path mapping for agents -- read-only references to other OpenSpec repos when needed -- clear output showing which Git repo owns each artifact - -Remote Git URL support, pull/push helpers, status dashboards, and sequencing -enforcement can come later. - -### 6. Evolve Toward `work/` - -After the baseline and standalone repo flow are solid, introduce the future -layout direction: - -```text -openspec/ - specs/ - work/ -``` - -At that point: - -- existing `changes/` can be supported as legacy or migrated -- changes become change-shaped work -- initiatives become coordination-shaped work -- dependency and sequencing views can build on stable work identity - -Do not make `/work` block the standalone OpenSpec repo release. - -## Decisions Considered - -### Separate `changes/` And `initiatives/` - -Rejected as the preferred future shape: - -```text -openspec/ - changes/ - initiatives/ -``` - -This uses folders as the type system and makes changes and initiatives feel -artificially unrelated. The cleaner model is one `work/` tree where change and -initiative are shapes of work. - -### Initiative-Owned Change Folders - -Rejected as canonical storage: - -```text -openspec/ - initiatives/ - checkout-modernization/ - changes/ - add-tax-api/ -``` - -This makes initiative ownership look like lifecycle ownership. A larger unit of -work may coordinate a smaller one, but the smaller unit still has its own -identity, targets, deltas, and lifecycle. - -### Project Or Repo Buckets As Lifecycle Roots - -Rejected as the default: - -```text -projects/ - api/ - openspec/ - changes/ - web/ - openspec/ - changes/ -``` - -Repo buckets work when each artifact cleanly belongs to one repo, but they get -awkward for cross-repo work, shared contracts, monorepos, and initiatives that -span several targets. Repos should be targets, not mandatory lifecycle roots. - -### Stateful Context Store As Core Primitive - -Rejected as the core framing. - -A dedicated planning or contracts repo may hold OpenSpec artifacts, but it is -still a Git repo. OpenSpec should not create a separate authoritative store that -can disagree with Git. - -### Configurable Layout Modes - -Rejected as an MVP product shape. - -Custom layout modes force every tool, doc, and agent instruction to branch. -Prefer one opinionated layout and let users choose which Git repo contains it. - -### Workspace As A Primary Product Object - -Rejected as the new user-facing shape. - -The useful part of workspace-view behavior is local resolution: knowing where -the OpenSpec repo and target project repos are checked out on this machine. That -should be treated as a local repo map, not as a planning container, lifecycle -owner, or durable source of truth. - -## Supersession Note - -This direction supersedes the older product boundary that centered context -stores, collections, initiatives, workspaces, and repo-local changes as separate -primary nouns. Those artifacts remain useful historical context and describe -implemented beta behavior, but new product direction should start from the -Git-native `specs/` and `work/` shape. diff --git a/openspec/initiatives/context-store-and-initiatives/direction.md b/openspec/initiatives/context-store-and-initiatives/direction.md deleted file mode 100644 index c7bf117d01..0000000000 --- a/openspec/initiatives/context-store-and-initiatives/direction.md +++ /dev/null @@ -1,458 +0,0 @@ -# Context Store And Initiatives Direction - -Status: historical beta direction. - -This document preserves the earlier context-store and workspace direction from -the workspace/initiative discussion. It is useful transition evidence, but it -is not the current product authority for the simplification work. - -For current direction, start with: - -1. `openspec/work/simplify-context-and-workspace-model/goal.md` -2. `openspec/work/simplify-context-and-workspace-model/roadmap.md` - -The main historical shift captured here was that "workspace" should not be the -durable shared planning object. In this earlier model, the durable shared -object was a synced context store, and initiatives were one opinionated -collection inside it. - -## Historical Core Model - -```text -Context Store - = synced shared content container - -Collection - = mounted content system inside a store - -Initiatives - = first major collection for cross-team implementation context - -Workspace - = local working view over context stores and repos - -Change - = repo/team-owned implementation plan -``` - -The clean rule: - -```text -Context stores sync truth. -Collections shape truth. -Initiatives coordinate work. -Workspaces open local views. -Changes implement repo-owned slices. -``` - -## Historical Locked Product Boundary - -The workspace-to-initiative pivot was the product boundary for this beta -coordination work: - -- A workspace is a regenerable, machine-local working view. It maps context - stores, initiatives, projects, repos, and folders to paths the current user can - open. -- A context store is the durable synced container for shared files. -- An initiative is the durable coordination object for cross-team or cross-repo - implementation context. -- A repo-local change remains the implementation plan owned by the repo or team - doing the work. - -This supersedes the older model where a workspace-level `changes/` tree owned -the canonical shared plan for cross-repo work. Existing workspace-planning -behavior can remain as beta or legacy infrastructure, but it should not steer -new lifecycle design. - -Workspace roadmap disposition: - -- Keep setup, link, relink, list, open, update, and doctor. -- Keep linked repos and folders visible for exploration before a change exists. -- Keep workspace-local agent guidance as local view setup, refreshed by - `workspace update`. -- Defer workspace apply, verify, and archive until initiatives can link to - repo-owned OpenSpec changes. -- Defer branch/worktree orchestration, multi-repo apply, strong cross-repo - validation, and dependency graph enforcement. - -## Agent-First UX - -The primary user experience for initiatives is expected to be agent-driven: - -```text -Using initiative billing-launch, explore the API work and create a proposal. -``` - -The user should not need to know every command. OpenSpec should expose small, -structured CLI primitives that an agent can use to: - -- find the intended initiative across registered context stores -- read canonical initiative files from the context store -- create or link a repo-local OpenSpec change -- use workspace state for local repo and folder views -- respect edit boundaries instead of treating every opened folder as editable - -The CLI is therefore the agent's tool surface, not the whole user workflow. -Prefer explicit, machine-readable commands such as `initiative show --json`, -`new change --initiative ...`, and workspace local-view commands over broad -interactive flows as the first slice. - -Canonical initiative context should stay in the context store. Repo-local -changes should reference the initiative rather than checking in copied snapshots -of initiative prose. If an agent needs a compact context pack, OpenSpec can -generate that as command output from the live initiative context. - -## Context Store - -A context store is the shared/synced folder of files. It is content-agnostic. -It should not know what an initiative is. - -Example: - -```text -acme-context/ - initiatives/ - decisions/ - api-catalog/ - playbooks/ -``` - -The first backend should be Git: - -```text -create/update/delete files - -> commit - -> push - -> other users pull - -> local views update -``` - -But the application should talk to a store abstraction, not directly to Git, so -the backend can later become a cloud database. - -## Backend - -A backend provides persistence and sync for a context store. - -Examples: - -- `git` backend: local clone, pull, commit, push, watch -- `cloud` backend: database records, subscriptions, hosted sync -- `memory` backend: tests and local prototypes - -The backend should expose generic file/object operations: - -```text -read -write -delete -list -sync -watch -``` - -It should not contain initiative-specific behavior. - -## Collections - -A collection is a mounted content system inside a context store. It is -plugin-like, but "collection" is the user-facing term. - -Each collection owns: - -- a folder namespace -- a content model -- templates -- validation/rules -- optional agent guidance -- optional UI views - -Example: - -```text -context-store/ - initiatives/ # Initiative collection - decisions/ # Decision collection - api-catalog/ # API catalog collection -``` - -Core should enforce that a collection only writes inside its mount. - -## Initiative Collection - -The initiative collection is the first enterprise-oriented collection. - -An initiative is shared, agent-consumable implementation context for a -coordinated outcome. It can span teams, repos, services, APIs, contracts, and -capabilities. - -Default shape: - -```text -initiatives/ - launch-billing-flow/ - initiative.yaml - requirements.md - design.md - contracts/ - decisions.md - questions.md - tasks.md -``` - -This describes the runtime initiative collection shape in context stores. This -roadmap folder may still contain legacy `.initiative.yaml` progress metadata -while the initiative itself is being used to manage the migration; that legacy -tracker is not the model new context-store initiatives should copy. - -The default structure should be opinionated for the enterprise design -partnership, but the collection system should allow other structures later. - -## Initiative Responsibilities - -Initiatives should own implementation-relevant shared context: - -- product/program intent -- accepted requirements -- high-level technical coordination -- capability and ownership maps -- API/event/schema contracts -- dependency assumptions -- decisions and open questions -- workspace-readable context for repo-local implementation work - -Initiatives should not try to become all of Jira or Confluence. The focused -positioning is: - -```text -OpenSpec stores agreed implementation context. -Jira tracks work. -Confluence stores broad prose. -GitHub/GitLab store code. -``` - -## Initiative And Change Scope - -An initiative can span one or many OpenSpec changes. - -Those changes may live: - -- in the same repo as the initiative -- in different repos -- in multiple context stores or OpenSpec roots later - -The initiative stores shared coordination context. Workspace views can associate -that context with local repos and repo-owned changes without making the -initiative store machine-local checkout links. - -This keeps grouping separate from storage: - -```text -Initiative = shared grouping/context -Change = execution artifact -Workspace = local opened view of initiative + repos -``` - -## Workspace - -A workspace is a local working view, not the source of truth. - -It can map context stores and project identifiers to local paths, configure an -opener, and launch coding agents with the right folders visible. - -A workspace can open an initiative by resolving: - -- the initiative's context store -- locally selected repo-local changes -- local checkout paths for participating repos - -The durable workspace record should stay tiny and private. It records this -runtime's local view choices, not generated agent files or shared initiative -content. - -```text -getGlobalDataDir()/workspaces// - workspace.yaml -``` - -The workspace name is the local identity. The workspace record can optionally -store a selected context store and initiative, plus stable link names to local -paths and opener preferences. Initiative references are data inside the record, -not path segments. - -Opening a workspace materializes opener-specific runtime files at the managed -workspace root. Those files can contain generated agent guidance, skills, -and editor workspace files. Machine-readable context is returned by JSON command -output. These are regenerated local support, not source of truth. - -```text -private local view record - -> generated runtime files - -> opener-specific launch - -> initiative context + selected local repos/folders -``` - -Workspaces should be regenerable and runtime-specific. They should not be the -canonical home for initiative content, checked-in collaboration state, branches, -worktrees, clones, or implementation progress. - -## Repo Changes - -Repo-local changes remain the team-owned implementation plan. - -An engineering team should be able to pull relevant initiative context into a -repo and create a linked OpenSpec change. - -Example: - -```text -repo/ - openspec/ - changes/ - add-billing-api/ - .openspec.yaml - proposal.md - design.md - specs/ - tasks.md -``` - -The local change should reference the initiative in metadata, for example: - -```yaml -initiative: - store: platform - id: billing-launch -``` - -This metadata is durable repo context and should be checked in. It should not -contain machine-local paths. Agents should read the initiative's canonical files -from the registered context store when they need the shared context. - -## Relationship Between Concepts - -```text -Context Store - contains Collections - -Collection - defines structure/rules for a mounted folder - -Initiative Collection - defines initiatives/ - -Initiative - coordinates one shared outcome - -Workspace - opens local views of context stores and repos - -Repo Change - implements one team's/repo's part of an initiative -``` - -End-to-end flow: - -```text -Product/program/architect creates initiative - -> initiative syncs through context store - -> engineers open local workspace - -> repo team pulls relevant initiative context - -> repo team creates linked OpenSpec change - -> repo team implements locally - -> workspace view surfaces local progress alongside initiative context -``` - -## Local API Direction - -The app should use dependency injection: - -```ts -const store = createStore({ - id: "acme-context", - backend: gitBackend({ - remote: "git@github.com:acme/context.git", - localPath: "~/.openspec/stores/acme-context", - autoSync: true, - }), - collections: [ - initiativeCollection({ mount: "initiatives" }), - ], -}); -``` - -Usage: - -```ts -const initiatives = store.collection("initiatives"); - -await initiatives.create({ id: "launch-billing-flow" }); -await initiatives.update("launch-billing-flow", patch); -await store.sync(); -``` - -Important separation: - -```text -Git backend knows Git. -Store knows sync/lifecycle/events. -Collection knows content structure. -Initiative collection knows initiatives. -``` - -## UI Direction - -The UI should be content-agnostic at the core: - -- browse folders/files -- edit Markdown/YAML -- preview content -- search -- show diffs/history -- sync status - -Collections can add richer views: - -- initiative status view -- contract table -- owner/dependency graph -- linked repo-change view - -The UI should work no matter which collections are mounted. - -## Open Questions - -- What is the first concrete context store command surface? -- Should stores be called `context`, `store`, or something more product-facing? -- Where should enterprise context stores live by default: customer GitHub, - OpenSpec-managed Git, or later hosted cloud? -- How do non-technical users edit Git-backed content without feeling Git? -- What is the minimum viable auto-sync behavior before conflict handling gets - painful? -- How does an initiative contract graduate into a canonical owner repo contract? -- How should linked repo changes report status back into an initiative without - becoming Jira? -- How should monorepos map capabilities, folders, and repo-local changes? -- What should the first repo-change linking command be called? -- Which initiative progress/status signals are useful after linked changes - exist? - -## Suggested Next Direction - -After the initial store, collection, and initiative create/list foundations, -build the next slices in this order: - -1. Reconcile the Initiative MVP around create/list, validation, templates, and - explicit deferral of read/update/delete policy. -2. Add minimal context-store UX for setup, registration, listing, and doctoring. -3. Add agent-first initiative discovery with `initiative show --json` and - registered-store lookup. -4. Add repo-local change metadata and an agent-friendly create/link flow for - `--initiative`. -5. Reject standalone `initiative resolve`; local path mapping belongs to - workspaces, not initiative commands. -6. Let workspaces open initiative-aware local views once show/link semantics - exist. -7. Add local-to-initiative escalation UX. -8. Harden team-shared coordination, sync, conflict guidance, and progress - status after real usage shapes those needs. diff --git a/openspec/initiatives/context-store-and-initiatives/questions.md b/openspec/initiatives/context-store-and-initiatives/questions.md deleted file mode 100644 index 79c42ebc8b..0000000000 --- a/openspec/initiatives/context-store-and-initiatives/questions.md +++ /dev/null @@ -1,23 +0,0 @@ -# Context Store And Initiatives Questions - -## Open - -- Should the user-facing command vocabulary say `context`, `store`, or - something more product-facing? -- What migration or compatibility path should existing workspace-planning - changes get once initiatives exist? -- How should linked repo changes report progress back into an initiative without - becoming a Jira clone? -- How should monorepos map capabilities, folders, and repo-local changes? -- Should OpenSpec support configurable change homes across context stores and - local OpenSpec repos, and what ownership rules keep that model safe? - -## Resolved - -- Workspaces should not be the durable shared planning object. -- Initiative roadmap implementation should be tracked inside the initiative - until repo-owned implementation changes are needed. -- The first concrete context store command surface is `context-store setup`, - `context-store register`, `context-store list`/`ls`, and - `context-store doctor`. Sync, push/pull, remotes, and conflict handling are - future work. diff --git a/openspec/initiatives/context-store-and-initiatives/roadmap.md b/openspec/initiatives/context-store-and-initiatives/roadmap.md deleted file mode 100644 index 1f8d345ce7..0000000000 --- a/openspec/initiatives/context-store-and-initiatives/roadmap.md +++ /dev/null @@ -1,775 +0,0 @@ -# Context Store And Initiatives Roadmap - -Status: historical beta roadmap snapshot. - -This roadmap preserves the implementation queue that existed while the -context-store and workspace model was being explored. It is not the active -roadmap for current simplification work. - -For current direction, start with: - -1. `openspec/work/simplify-context-and-workspace-model/goal.md` -2. `openspec/work/simplify-context-and-workspace-model/roadmap.md` - -The historical product decision underneath this roadmap was: - -```text -Context stores sync truth. -Collections shape truth. -Initiatives coordinate work. -Workspaces open local views. -Changes implement repo-owned slices. -``` - -## Historical Beta Priority Snapshot - -At the time, the manual beta pass pulled first-run friction forward. This was -the historical working order before investing in deeper schema or lifecycle -machinery: - -1. Finish the manual beta reality pass enough to keep the next slices grounded. -2. Item 12, context-store first-run and cleanup UX: interactive no-argument setup, - target-path safety, and a supported unregister/remove path. -3. Skip Item 13 as an implementation item for now. Preserve the handoff - findings, but avoid hardcoding linear "next step" guidance until the agent - handoff model is clearer. -4. Item 14, workspaces beta guide split: make user docs match the interactive - setup path and keep exact flags in the agent playbook. -5. Item 15, context store project roots and schema-led initiatives: sparse initiative - creation and store-local schemas. - -Escalation UX, team-sharing hardening, and initiative-hosted target-bound -changes remain important, but they should wait until the first-run path feels -boring in the good way. - -Before workspaces become public/stable, run Item 19 as a late beta cleanup pass -so beta compatibility code is reviewed intentionally instead of treated as a -permanent contract. - -## 1. Lock The Direction - -Goal: make the workspace-to-initiative pivot explicit so future workspace work -does not keep implementing the older "workspace owns the plan" model. - -Ship: - -- Record that workspaces are local working views, not durable shared planning - objects. -- Record that initiatives are the durable coordination object for cross-team or - cross-repo work. -- Mark the current workspace apply, verify, and archive direction as deferred or - superseded until initiative-linked repo changes exist. -- Keep the already-built workspace setup, link, open, update, and doctor - behavior as useful beta infrastructure. - -Done when: - -- Fresh agents can tell which workspace ideas still apply and which ones should - not steer implementation. - -Locked disposition: - -- Keep workspace setup, link, relink, list, open, update, and doctor as beta - local-view infrastructure. -- Keep "workspace visibility is not change commitment" as a safety rule for - linked repos and folders. -- Supersede "workspace is the durable planning home" with "initiatives are the - durable coordination object." -- Supersede workspace-level planning artifacts as the canonical shared - cross-repo plan. -- Defer workspace apply, verify, and archive as first-class lifecycle commands - until initiative-linked repo-local changes exist. -- Defer branch/worktree orchestration, strong cross-repo validation, dependency - graph enforcement, and shared contract governance. - -Fresh-agent historical reading rule: - -- Start from `openspec/work/simplify-context-and-workspace-model/goal.md` and - `openspec/work/simplify-context-and-workspace-model/roadmap.md` for current - product authority. -- Use `openspec/initiatives/context-store-and-initiatives/direction.md` as - historical beta direction, not as the current product authority. -- Treat `openspec/changes/workspace-reimplementation-roadmap/HISTORICAL_DIRECTION.md` and - `openspec/changes/workspace-reimplementation-roadmap/` as historical reference - material for preserved local-view behavior and POC lessons. -- Do not pick up `workspace-apply-repo-slice` or - `workspace-verify-and-archive` as the next implementation slice unless a later - initiative-linked repo-change design explicitly reactivates them. - -## 2. Stabilize Workspace As Local View - -Goal: keep workspaces useful without making them the source of truth. - -Ship: - -- Workspace guidance that routes durable coordination to initiatives, - implementation planning to repo-local changes, and linked repos or folders to - local context until an edit root is selected. -- Workspace-open behavior that launches the local planning view with linked - folders visible. -- Workspace doctor/status output that explains local path mappings, unresolved - links, installed agent skills, and repair steps. -- Clear docs that `workspace update` refreshes local agent guidance and does not - modify linked repos. - -Done when: - -- A user can set up a workspace, link repos, open an agent, and understand that - the workspace is a local view over context, not the canonical shared plan. - -## 3. Add Context Store Foundation - -Goal: create the generic local context-store foundation that can later hold -initiatives and other shared context collections. Sync/watch behavior remains a -future hardening slice. - -Ship: - -- A context store abstraction with generic local operations: read, write, - delete, and list. -- A first Git-shaped backend model that can point at a local store root. -- A test/memory backend for fast tests and prototypes. -- A store configuration model that does not contain initiative-specific logic. - -Done when: - -- OpenSpec can create and manipulate files inside a local context store without - the core store layer knowing what those files mean. Pull, push, watch, - remote creation, and conflict handling are tracked as future sync work. - -## 4. Add Collection Foundation - -Goal: let product-specific content systems live inside a context store without -hardcoding every future concept into the store layer. - -Ship: - -- A collection interface with a mounted folder namespace. -- Rules that keep a collection's writes inside its mount. -- Basic collection validation and template hooks. -- A way for collections to expose optional agent guidance or UI metadata later. - -Done when: - -- The context store can host a mounted `initiatives/` collection while staying - generic enough for future collections like decisions, API catalogs, or - playbooks. - -## 5. Ship Initiative MVP - -Goal: give coordinated work a durable, shared, agent-consumable home. - -Ship: - -- Initiative creation and listing. -- A default initiative file shape: - -```text -initiatives// - initiative.yaml - requirements.md - design.md - decisions.md - questions.md - tasks.md -``` - -- Templates for product intent, accepted requirements, design decisions, open - questions, and coordination tasks. -- Validation for required initiative metadata. -- Explicit deferral of full read/show, update, and delete policy until the - agent-first discovery and lifecycle needs are clearer. - -Done when: - -- A user or agent can create and list initiatives as shared planning objects - before any repo has committed to implementation details. - -## 6. Add Minimal Context Store UX - -Goal: make shared initiative storage usable before repo handoff or workspace -opening depends on it. - -Ship: - -- `context-store setup ` for creating a local Git-backed store folder with - portable store metadata and local registration. -- `context-store register ` for registering an existing clone or folder, - defaulting the store id from the repo or folder name. -- `context-store list` and `context-store doctor` for local visibility and - non-mutating diagnostics. -- `initiative list` defaulting to all registered stores, with `--store` as a - filter and `--store-path` as an escape hatch. -- Minimal human output and JSON output suitable for agents. - -Done when: - -- A single developer or teammate can create or register a shared context store, - list initiatives across registered stores, and diagnose missing or broken - local store setup without learning the internal registry layout. - -## 7. Add Agent-First Initiative Discovery - -Goal: let an agent resolve the initiative the user named and read canonical -initiative context from the source of truth. - -Ship: - -- `initiative show ` that searches registered stores by default. -- Ambiguity handling when the same initiative id exists in multiple stores. -- JSON output with canonical initiative metadata, store identity, initiative - root path, and metadata path. -- Human output focused on identity and available files, not work progress. - -Done when: - -- An agent can answer, "Which initiative did the user mean, where is the - canonical context, and where is the initiative metadata?" - -## 8. Connect Repo-Local Changes To Initiatives - -Goal: split shared coordination from repo-owned implementation plans cleanly. - -Discussion points to confirm before implementation: - -- Should the create/link flow explicitly report where the change lives, which - initiative it references, and the next suggested command? -- Should `--initiative ` search registered stores by default, or should it - require `--store` when more than one store is registered? -- What should the command do when the initiative exists but the current repo has - no obvious ownership match? - -Ship: - -- Repo-local change metadata that can reference an initiative by store id and - initiative id. -- An agent-friendly create or link flow such as - `new change --initiative /`. -- Guidance that repo-local changes remain responsible for implementation, - validation, and archive. -- No checked-in `initiative.md` snapshot by default; agents read canonical - initiative files live from the context store. - -Done when: - -- One initiative can coordinate several repo-local changes without copying the - shared plan into every repo, storing machine-local links in the initiative, or - making the initiative own implementation artifacts. - -## 9. Reject Initiative Resolve - -Decision: do not add `openspec initiative resolve`, now or later. - -Rationale: - -- `initiative show` already resolves canonical shared initiative context. -- A workspace is the local view over repos, folders, context stores, and - initiatives. -- Repo-local changes already carry durable initiative links in checked-in - `.openspec.yaml` metadata. -- Repo-local status already reports work progress. -- A standalone resolve command would either duplicate workspace local-view state - or produce weak output when no workspace is present. - -Do not ship: - -- `openspec initiative resolve ` -- all-workspace or all-repo scans for initiative availability -- explicit path scanning as an initiative command -- Git remote matching for initiative participation -- repo ownership inference -- cloning, branch creation, or worktree creation as part of initiative - resolution -- initiative backlinks -- local availability or progress dashboards under the initiative command - -Done when: - -- Future agents can see that "initiative resolve" is intentionally rejected and - should not be revived under another command name. - -## Proposed Discussion Point: Add Initiative Next / Agent Handoff UX - -Status: candidate work item, not locked into the numbered roadmap yet. - -Question to confirm: - -- Should this become a roadmap item before "Let Workspaces Open Initiatives"? - -Goal: give agents and users a small "what now?" command after initiative -discovery from the current repo or workspace, without turning it into a -dashboard or progress/status surface. - -Possible shape: - -```bash -openspec initiative next billing-launch --json -``` - -Possible JSON answer: - -```json -{ - "initiative": "billing-launch", - "next_action": "create_repo_change", - "reason": "initiative found, no linked local change exists for this repo", - "suggested_command": "openspec new change add-billing-api --initiative billing-launch" -} -``` - -Discussion points to confirm before implementation: - -- Is `initiative next` the right command name, or should this guidance belong - inside workspace initiative opening or repo-local status? -- Should it return exactly one suggested next action, or a ranked set of options? -- Should it ever inspect work progress, or stay limited to handoff/readiness? -- How should it behave when no stores are registered, the initiative is - ambiguous, or the local repo is unrelated? - -Done when, if accepted: - -- An agent can answer "what should I do next for this initiative from here?" - without guessing across `show`, workspace state, and repo-local - change metadata. - -## 10. Let Workspaces Open Initiatives - -Goal: connect durable initiative context to this runtime's local working view -after initiative show and repo-change linkage exist. - -Locked direction: - -- A workspace does not contain the work. It remembers how this runtime opens the - work. -- Persist only tiny private local view choices. -- Generate opener-specific runtime files on open. -- Attach initiative context and selected existing local repos or folders. -- Do not clone, branch, create worktrees, use submodules, or infer local repos in - this slice. -- Context-only open is valid. - -Product decision status: - -- No remaining Item 10 product decisions are open. Implementation may still - uncover mechanical details, but the intended UX shape is locked. - -Command UX decision: - -- Use `openspec workspace open --initiative `. -- Support `/` and ` --store `. -- Support `openspec workspace open --initiative ` - when the user wants to choose the local workspace identity explicitly. -- If only `` is provided, proceed when exactly one registered - context store has that initiative id. -- On ambiguity, list exact matches and require an explicit store selector. -- On no exact match, show likely matches when available and suggest `openspec - initiative list`; do not silently open a fuzzy match. -- If the user omits a workspace name, derive a friendly default from the - initiative id when that is unambiguous; otherwise require the user to pick an - explicit workspace name. - -Open target decision: - -- Open the initiative directory by default, not the whole context store. -- Generated guidance and JSON output should still report the context store root - and that broader context is available. -- A later explicit option may open the whole context store, but broad store - scope is not the Item 10 default. - -Local view record decision: - -- Use one private local view record for initiative-aware local views. -- Store initiative-view state in the root `workspace.yaml` file. -- The record stores selected context-store binding, initiative, local links, - opener, and selected tools. The binding may preserve a registry selector or a - runtime-local path selector. -- The context binding is optional, so a workspace can also be a custom local view - with linked folders and no initiative. - -Workspace storage decision: - -- Store each private workspace view under - `getGlobalDataDir()/workspaces//`. -- The workspace name is the local identity. Selected store and initiative, if - any, are data inside the private record rather than path segments. -- Use one durable `workspace.yaml` at the workspace root. -- Generate `AGENTS.md`, opener workspace files, and tool-specific skills at the - workspace root. -- Do not introduce a separate generated-output directory for Item 10. - -Runtime identity decision: - -- Use `getGlobalDataDir()` as the cross-platform runtime-local boundary. -- Local paths are valid only in the runtime that wrote the private - `workspace.yaml`. -- Do not add path translation or a separate `` path segment in Item - 10. - -Prepare/JSON decision: - -- Keep `workspace open --json` as a machine-facing receipt for the same open - operation. -- Do not add `--prepare-only` for Item 10. -- JSON should return useful generated paths, selected context, opened roots, - skipped roots, opener, launch status, and warnings rather than a bare success - response. - -Codex Desktop decision: - -- Open the generated workspace root as the Codex Desktop project. -- Expose attached initiative and linked repo/folder paths through generated - guidance and `workspace open --json` output. -- Defer Desktop multi-root automation until there is a clearer Desktop contract. - -Edit-boundary decision: - -- Emit advisory boundaries only. -- Label initiative/context-store files as shared coordination context and linked - repos/folders as local implementation context when selected. -- Do not enforce write restrictions in Item 10. - -Ship: - -- Private local view state that can remember the selected context store, - selected initiative, selected local links, opener, and selected tools for this - runtime. -- `workspace open` support for generating opener-specific runtime files and - opening initiative context plus locally resolved linked repos/folders. -- Agent guidance and machine-readable `workspace open --json` output that - explain the current initiative, opened roots, skipped roots, local paths, and - advisory edit boundaries. -- Workspace-name reuse behavior that avoids silently repointing an existing - workspace to a different initiative. -- Open-time warnings that skip missing linked repos/folders while failing when - the selected initiative or context store cannot be resolved. -- Continued support for custom non-initiative workspaces as first-class local - views. -- Doctor guidance for missing context stores, missing linked repos/folders, and - stale local view records. - -Done when: - -- A teammate can open the same initiative in their runtime while using their own - local paths and selected repo subset. -- Generated runtime files are clearly derived and can be regenerated without - losing the user's local view choices. - -## 11. Manual Beta Reality Pass - -Status: proposed immediate beta-learning item. - -Goal: manually run what exists and use the friction to update initiative notes -before designing more surface area. - -Ship: - -- A fresh-user walkthrough of context-store setup, initiative creation, - workspace opening, repo linking, doctor output, and repo-local linked change - creation. -- Notes on what felt clear, what felt odd, where prompts were missing, and where - docs pushed too many flags onto the user. -- A short disposition that separates docs-only fixes from follow-on - implementation slices. - -Done when: - -- The initiative contains concrete notes from trying the current beta flow by - hand. -- The next implementation or docs slice is grounded in observed friction rather - than guessed workflow shape. - -## 12. Context Store First-Run And Cleanup UX - -Goal: make context-store setup and cleanup feel like a normal local workflow, -without adding sync, remote, or governance automation. - -Work item: -`work-items/12-context-store-first-run-and-cleanup-ux/` - -Ship: - -- Interactive no-argument `context-store setup` for terminal users. -- Deterministic non-interactive and JSON behavior when required setup choices - are missing. -- Target-path safety output for managed defaults, explicit paths, existing Git - repos, and non-empty directories. -- A supported local cleanup path for unregistering or removing a context store - without hand-editing the registry. -- Setup output that explains local registry state and Git state, including - uncommitted shared-store files after `--init-git`. - -Done when: - -- A fresh user can set up or clean up a local context store without knowing - hidden registry paths, environment variables, or manual file edits. - -## 13. Agent Handoff Output And Delivery Polish - -Status: deferred as an implementation item. - -Goal, if revisited: define an agent handoff receipt model that reports what -exists, where it lives, and which affordances are available without prescribing -one linear next step. - -Work item: -`work-items/13-agent-handoff-output-and-delivery-polish/` - -Ship: - -Do not ship fixed "Next for your agent" guidance yet. The current shape assumes -that users and agents move through the beta flow linearly, but real agentic -workflows may inspect, branch, skip steps, or start from existing context. - -Preserve for future exploration: - -- Whether command output should include context receipts, available affordances, - or nothing beyond deterministic paths. -- Whether direct path fields like `created_paths` are a small standalone receipt - improvement rather than part of a broader handoff model. -- How delivery wording should distinguish baseline OpenSpec guidance from - workflow entrypoints without coupling it to this handoff item. - -## 14. Workspaces Beta Guide Split - -Status: proposed immediate beta-learning item. - -Goal: make the beta docs reflect the intended division of labor: - -```text -Users make local choices. -Agents run OpenSpec work commands. -``` - -Ship: - -- A user-facing guide that prefers interactive terminal setup for local choices - such as context-store location, opener, and local repo paths. -- An agent-facing CLI playbook that keeps explicit commands, JSON output, - current-directory rules, and caveats. -- A clear rule for which flags are normal user-facing escape hatches and which - are mostly agent-facing precision. - -Done when: - -- A new user can get to a working beta setup without reading a flag-heavy CLI - tutorial. -- A coding agent can still find the exact commands needed to create initiatives, - link repo-local changes, and inspect state safely. - -## 15. Context Store Project Roots And Schema-Led Initiatives - -Goal: let context stores behave like OpenSpec roots for shared planning config -and schemas, while keeping implementation changes repo-owned by default. - -Work item: -`work-items/15-context-store-project-roots-and-schema-led-initiatives/` - -Product decision to confirm: - -- A context store can have `openspec/config.yaml` and `openspec/schemas/` like a - repo after `openspec init`. -- That project-like shape is for shared context configuration and initiative - schemas. It must not silently make the context store an implementation repo. -- `initiative create` should create a sparse shell and let reviewed initiative - artifacts grow through schema-led status/instructions. - -Ship: - -- Context-store setup that creates or supports store-local OpenSpec config. -- A default initiative schema for high-level requirements and design artifacts. -- Sparse initiative creation: `initiative.yaml` plus a short `brief.md`, with no - `TBD` placeholders and no default `tasks.md`. -- Initiative artifact status and instructions output rooted in the initiative - directory. -- Guardrails so `openspec new change` does not accidentally create executable - repo-local changes inside a context store just because the store has an - `openspec/` directory. -- Compatibility for existing six-file MVP initiatives. - -Done when: - -- A context store can resolve store-local initiative schemas. -- Agents can iteratively create initiative requirements and design artifacts - from CLI instructions. -- Existing MVP initiatives continue to list and show. -- Docs stop presenting initiative creation as "fill every markdown file now." - -## 16. Add Escalation UX - -Goal: let users start locally and upgrade only when coordination is actually -needed. - -Work item: -`work-items/16-add-escalation-ux/` - -Ship: - -- Explore/propose guidance that starts in the current repo by default. -- A recommendation path when work spans multiple owned areas: - -```text -This appears to span multiple owned areas. -OpenSpec can upgrade it into a coordinated initiative and carry the current -planning context forward. -``` - -- Carry-forward behavior for the current change name, product goal, notes, - inferred areas, and relevant questions. -- Clear prompts that ask about concrete affected areas rather than abstract - storage models. - -Done when: - -- Coordinated planning feels like a continuation of local planning, not a - workflow restart. - -## 17. Harden Team-Shared Coordination - -Goal: make initiatives practical for teams without turning setup into an admin -ceremony. - -Work item: -`work-items/17-harden-team-shared-coordination/` - -Ship: - -- A recommended Git-backed shared context store pattern. -- Lightweight teammate onboarding: - -```text -Clone the context store. -Run openspec workspace doctor. -Open the initiative with your agent. -``` - -- Repair flows for local path mappings. -- Sync status and conflict guidance. -- Clear separation between committed initiative state and machine-local - workspace state. - -Done when: - -- Several teammates can share the same initiative while each keeps their own - local checkout layout. - -## 18. Explore Initiative-Hosted Target-Bound Change Artifacts - -Goal: decide whether shared initiative artifacts can graduate into executable -OpenSpec changes only after they are bound to a target repo or spec root, -without blurring initiative coordination, repo ownership, and workspace -local-view boundaries. - -Work item: -`work-items/18-explore-initiative-hosted-target-bound-change-artifacts/` - -Discussion points to confirm before exploration: - -- Should "change home" stay internal resolver language, with user-facing - phrasing like "where should this plan live?" and "editable target"? -- What is the difference between initiative work items, briefs, target-bound - changes, and repo-local changes? -- What portable target metadata is required before an initiative-hosted artifact - can be considered implementation-ready? -- Should shared target-bound changes require explicit opt-in, or can - initiative/store policy select them? -- What user/team scenario would justify an initiative-hosted target-bound change - instead of a repo-local linked change? - -Ship: - -- Audit commands, templates, validation, archive, apply, completion, and docs - for repo-local `openspec/changes/` assumptions. -- Define the concepts of artifact home, implementation target, allowed edit - roots, and action context. -- Decide how initiative-hosted target-bound changes bind to repo specs, - implementation roots, branches, validation, archive, and sync/conflict - behavior. -- Define agent-readable JSON output for work target, artifact home, - implementation target, initiative link, edit boundaries, unsupported - lifecycle commands, and next commands. -- Record compatibility behavior for existing repo-local and workspace-local - changes. -- Recommend whether this should become an implementation slice, remain deferred, - start as initiative work items only, or be limited to specific schemas or - workflows first. - -Done when: - -- The initiative has a concrete recommendation, opt-in/config examples, affected - command list, and go/no-go criteria for implementation. - -## 19. Review Workspace Beta Compatibility Before Public Release - -Goal: decide which workspace beta compatibility behavior should survive into the -public workspace contract, and remove or migrate the rest while workspaces are -still unpublished. - -Work item: -`work-items/19-review-workspace-beta-compatibility-before-public-release/` - -Why this is late: - -- Workspaces are still beta and not public/stable yet. -- We do not need to preserve every intermediate beta file shape forever. -- Early cleanup risks churn while first-run UX and initiative behavior are still - changing. -- The right compatibility contract is easier to define after manual beta usage - shows which local workspace artifacts real users have actually created. - -Ship: - -- Inventory workspace compatibility code, including legacy split state readers, - registry fallbacks, `codex` to `codex-cli` aliases, generated `.gitignore` - cleanup, and empty compatibility shims. -- Classify each path as public contract, beta migration, test-only shim, or - removable dead weight. -- Remove beta-only shims that only support unpublished intermediate workspace - shapes. -- Define any migration behavior worth keeping for people who tried the beta. -- Update docs, tests, generated guidance, and release notes so the public - workspace compatibility promise is explicit. - -Done when: - -- The workspace compatibility surface is intentionally small. -- Public docs do not imply support for beta-only workspace internals. -- Any remaining migration code has a clear owner, reason, and removal policy. - -## Later, Not First - -These are important, but should wait until the initiative model has real usage: - -- Workspace apply, verify, and archive as first-class lifecycle commands. -- Branch or worktree orchestration. -- Strong cross-repo validation. -- Dependency graph enforcement. -- Shared contract ownership workflows. -- Sponsor/driver governance flows. -- Initiative progress/status dashboards. -- Cloud-hosted context stores. - -## Suggested Shipping Sequence - -1. Lock the direction and defer old workspace lifecycle slices. -2. Stabilize workspace as local view and agent launcher. -3. Add context store foundation. -4. Add collection foundation. -5. Ship initiative MVP. -6. Add minimal context-store UX. -7. Add agent-first initiative discovery. -8. Link repo-local changes to initiatives. -9. Keep initiative resolve rejected; use workspace local-view mapping instead. -10. Let workspaces open initiatives. -11. Manual beta reality pass. -12. Context store first-run and cleanup UX. -13. Skip agent handoff output and delivery polish until the handoff model is - clearer. -14. Workspaces beta guide split. -15. Context store project roots and schema-led initiatives. -16. Add local-to-initiative escalation UX. -17. Harden team-shared coordination. -18. Explore initiative-hosted target-bound change artifacts. -19. Review workspace beta compatibility before public release. - -Pending discussion: revisit handoff receipts after the beta guide and sparse -initiative model clarify what context agents actually need. diff --git a/openspec/initiatives/context-store-and-initiatives/tasks.md b/openspec/initiatives/context-store-and-initiatives/tasks.md deleted file mode 100644 index 1acbd6f60f..0000000000 --- a/openspec/initiatives/context-store-and-initiatives/tasks.md +++ /dev/null @@ -1,321 +0,0 @@ -# Context Store And Initiatives Tasks - -Status: historical beta progress snapshot. - -This file preserves the task state from the old context-store and workspace -initiative. It is not the active implementation queue for current -simplification work. - -For current direction, start with: - -1. `openspec/work/simplify-context-and-workspace-model/goal.md` -2. `openspec/work/simplify-context-and-workspace-model/roadmap.md` - -Historical roadmap items live in `roadmap.md`; detailed working notes live -under `work-items/`. - -## Historical Beta Priority Snapshot - -At the time, the manual beta pass prioritized the things a fresh user hit while -getting started before deeper model work: - -1. Finish Item 11 observations enough to keep implementation grounded. -2. Item 12: no-argument context-store setup, path safety, and - cleanup. -3. Skip Item 13 as an implementation item for now. Preserve the findings, but - do not hardcode linear "next step" guidance until the agent handoff shape is - better understood. -4. Item 14: update the beta guide so it matches the improved first-run flow. -5. Item 15: context-store project roots and sparse schema-led - initiatives. -6. Items 16-18: leave escalation, team hardening, and initiative-hosted - target-bound changes until after the onboarding path feels sane. -7. Item 19: review beta workspace compatibility near the end, before workspace - behavior becomes public/stable. - -## 1. Lock The Direction - -Work item: `work-items/01-lock-the-direction/` - -- [x] Record the workspace-to-initiative product boundary in initiative docs. -- [x] Mark the old workspace reimplementation roadmap as historical reference. -- [x] Defer workspace apply, verify, and archive until initiative-linked repo - changes exist. -- [x] Complete a non-spec direction pass so roadmap, work items, docs, and - active change artifacts point to the initiative as product intent. -- [x] Decide whether user-facing workspace docs need any change now; default to - no unless they misrepresent current behavior. -- [x] Decide how to handle active no-task workspace changes after the - disposition pass. -- [x] Record final evidence and remaining risks for Item 1. - -## 2. Stabilize Workspace As Local View - -Work item: `work-items/02-stabilize-workspace-as-local-view/` - -- [x] Re-anchor generated workspace guidance in the initiative direction. -- [x] Decide that generated guidance should stop recommending workspace-level - `changes/` as the planning home for coordinated work. -- [x] Decide that `workspace update` should refresh generated workspace - guidance for existing workspaces. -- [x] Decide that workspace-planning action context should treat beta workspace - artifacts as local compatibility context. -- [x] Decide to defer doctor installed-skill summaries and only update stale - `workspace update` wording for now. -- [x] Define exact local-view behavior to preserve. -- [x] Review current workspace setup, link, relink, list, open, update, and - doctor behavior against that definition. -- [x] Identify any product wording or guidance gaps left after Item 1. - -## 3. Add Context Store Foundation - -Work item: `work-items/03-add-context-store-foundation/` - -- [x] Define the initial store/backend data model. -- [x] Decide that the first slice is core API only, with no CLI surface yet. -- [x] Decide that the first backend is Git/local checkout config only. -- [x] Decide where context store roots, local registry YAML, and portable store - metadata YAML live. -- [x] Implement context-store foundation helpers and tests. - -## 4. Add Collection Foundation - -Work item: `work-items/04-add-collection-foundation/` - -- [x] Define collection mount rules. -- [x] Decide validation/template hooks stay inert extension fields for this - slice. -- [x] Prove `initiatives/` can mount without store-specific logic. - -## 5. Ship Initiative MVP - -Work item: `work-items/05-ship-initiative-mvp/` - -- [x] Define initiative file shape and validation. -- [x] Add templates for requirements, design, decisions, questions, and tasks. -- [x] Implement create/list mounted collection operations and CLI adapter. -- [x] Decide full read/show, update, and delete policy should move to later - agent-first discovery and lifecycle work. - -## 6. Add Minimal Context Store UX - -Work item: `work-items/06-add-minimal-context-store-ux/` - -- [x] Create Item 6 work-item tracking notes. -- [x] Define high-level `context-store setup`, `register`, `list`, and `doctor` - UX direction. -- [x] Decide exact checked-in store metadata and machine-local registry - behavior. -- [x] Decide setup/register/list/doctor human behavior and responsibility split. -- [x] Decide `initiative list` partial-success behavior across registered - stores. -- [x] Decide final Item 6 edge cases: id inference, non-empty setup folders, - registry conflicts, empty states, JSON exit behavior, and static completions. -- [x] Update `initiative list` to default across registered stores, with - `--store` as a filter and `--store-path` as an escape hatch. -- [x] Add focused tests and verification for context-store CLI behavior. - -## 7. Add Agent-First Initiative Discovery - -- [x] Define `initiative show ` human and JSON output. -- [x] Search registered stores by default and handle ambiguous initiative ids. -- [x] Return canonical initiative metadata, store identity, root path, and - metadata path for agent reads. -- [x] Keep work-progress status out of this command. - -## 8. Connect Repo-Local Changes To Initiatives - -Work item: `work-items/08-connect-repo-local-changes-to-initiatives/` - -- [x] Decide that the initiative link lives in repo-local `.openspec.yaml`. -- [x] Add repo-local initiative metadata. -- [x] Add an agent-friendly create or link flow for repo-local changes. -- [x] Decide command naming for `--initiative` linking on new change creation. -- [x] Confirm whether create/link output should report where the change lives, - which initiative it references, and the next suggested command. -- [x] Confirm whether `--initiative ` searches registered stores by default - or requires explicit store selection in multi-store setups. -- [x] Keep canonical initiative context in the context store; do not add a - checked-in `initiative.md` snapshot by default. - -## 9. Reject Initiative Resolve - -Work item: `work-items/09-add-initiative-resolve/` - -- [x] Pressure-test whether a standalone `initiative resolve` command is needed. -- [x] Decide not to add `openspec initiative resolve`, now or later. -- [x] Keep canonical initiative discovery in `initiative show`. -- [x] Keep local path mapping in workspace behavior. -- [x] Keep implementation progress in repo-local status. -- [x] Reject all-repo scans, all-workspace scans, explicit path scanning as an - initiative command, Git remote matching, cloning, worktree creation, and - initiative backlinks. - -## Proposed Discussion: Initiative Next / Agent Handoff UX - -Work item draft: -`work-items/proposed-initiative-next-agent-handoff-ux/` - -- [ ] Decide whether to add this as a numbered roadmap item between Item 9 and - Item 10. -- [ ] Decide whether the surface is `initiative next`, workspace initiative - opening, or repo-local status guidance. -- [ ] Decide whether it suggests one next action or multiple ranked options. -- [ ] Decide that progress/status stays out of scope, unless we explicitly want - this command to grow into a broader status surface. - -## 10. Let Workspaces Open Initiatives - -- [x] Create Item 10 work-item tracking notes. -- [x] Lock the command UX for opening an initiative as a local workspace view. -- [x] Define the private local view record for selected context store, - initiative, local links, opener, and selected tools. -- [x] Decide the private local view record storage namespace and keying. -- [x] Decide the default open target: initiative directory versus full context - store. -- [x] Decide where generated runtime files live and how they are regenerated. -- [x] Define runtime identity rules for macOS, Codespaces, WSL, SSH, and - containers without path translation. -- [x] Decide the prepare/JSON surface for agents and desktop integrations. -- [x] Decide the Codex Desktop behavior for generated workspace roots and attached - paths. -- [x] Define advisory edit-boundary output for Item 10. -- [x] Confirm this slice opens known local paths only and does not create - clones, branches, worktrees, or submodules. - -## 11. Manual Beta Reality Pass - -Work item: `work-items/11-manual-beta-reality-pass/` - -- [ ] Manually run the current context-store, initiative, workspace, and - repo-local change flows from a fresh user's point of view. -- [ ] Capture notes on confusing commands, missing prompts, unclear output, and - places where the docs over-explain or under-explain. -- [ ] Update initiative notes as observations come in. -- [ ] Decide which findings should become implementation slices versus docs-only - fixes. - -## 12. Context Store First-Run And Cleanup UX - -Work item: `work-items/12-context-store-first-run-and-cleanup-ux/` - -- [x] Decide and implement interactive no-argument `context-store setup`. -- [x] Define target-path safety behavior for managed defaults, explicit paths, - Git repos, and non-empty directories. -- [x] Add local cleanup support for unregistering or removing a context store. -- [x] Make setup and cleanup output report the agreed human-facing summary and - exact JSON state without workflow `next_commands`. -- [x] Update docs and tests for first-run setup and cleanup behavior. - -## 13. Agent Handoff Output And Delivery Polish - -Work item: `work-items/13-agent-handoff-output-and-delivery-polish/` - -Status: deferred. Do not implement fixed "Next for your agent" output from this -item yet. - -- [ ] Revisit the handoff model after Item 14/15 clarify the beta guide and - sparse initiative flow. -- [ ] If needed, split deterministic receipt improvements such as direct - `created_paths` into a smaller future implementation slice. -- [ ] Avoid prescribing one linear workflow path; future handoff output should - report state, paths, and possible affordances that agents can compose. - -## 14. Workspaces Beta Guide Split - -Work item: `work-items/14-workspaces-beta-guide-split/` - -- [ ] Update the user-facing guide to prefer interactive terminal setup for - local choices. -- [ ] Move initiative creation, initiative editing, and repo-local change - creation into "ask your coding agent" guidance. -- [ ] Keep explicit flags, JSON output, cwd rules, and caveats in the - agent-facing CLI playbook. -- [ ] Decide which flags remain useful in user docs as escape hatches for - ambiguity. -- [ ] Record any interactive prompt gaps found while writing the guide. - -## 15. Context Store Project Roots And Schema-Led Initiatives - -Work item: -`work-items/15-context-store-project-roots-and-schema-led-initiatives/` - -- [x] Create Item 15 work-item tracking notes. -- [ ] Update initiative direction language so context stores are OpenSpec-aware - shared project roots, not only cross-team/cross-repo coordination folders. -- [ ] Decide the minimal context-store OpenSpec structure: - `.openspec-store/store.yaml`, `openspec/config.yaml`, - `openspec/schemas/`, and collection mounts. -- [ ] Decide the store-local config shape for initiative collection defaults, - including whether to use `collections.initiatives.schema`. -- [ ] Decide how context-store setup creates, preserves, or repairs - store-local `openspec/config.yaml`. -- [ ] Define the built-in high-level initiative schema and its initial - artifacts. -- [ ] Decide whether `initiative create` creates only `initiative.yaml`, or - `initiative.yaml` plus one schema-selected seed artifact such as `brief.md`. -- [ ] Replace eager six-file initiative scaffolding with sparse iterative - creation. -- [ ] Add initiative artifact status/instructions behavior rooted at the - initiative directory. -- [ ] Reuse project-local schema resolution with the context-store root as the - project root for initiative commands. -- [ ] Decide whether schema CLI commands need `--store` or `--store-path` - selectors. -- [ ] Guard planning-home resolution so context stores with `openspec/config.yaml` - do not accidentally make the store an implementation repo. -- [ ] Preserve existing six-file beta initiatives as readable valid - initiatives. -- [ ] Update docs, generated agent guidance, and tests for the project-like - context-store model. - -## 16. Add Escalation UX - -Work item: `work-items/16-add-escalation-ux/` - -- [ ] Define local-to-initiative recommendation triggers. -- [ ] Carry current planning context into a new initiative. -- [ ] Keep prompts grounded in affected areas. - -## 17. Harden Team-Shared Coordination - -Work item: `work-items/17-harden-team-shared-coordination/` - -- [ ] Document recommended Git-backed store setup. -- [ ] Define teammate onboarding and repair flows. -- [ ] Add sync status and conflict guidance. - -## 18. Explore Initiative-Hosted Target-Bound Change Artifacts - -Work item: `work-items/18-explore-initiative-hosted-target-bound-change-artifacts/` - -- [ ] Confirm "change home" stays internal language and user-facing wording is - closer to "where should this plan live?" -- [ ] Define user-facing naming for initiative work items, briefs, - target-bound changes, artifact homes, and editable targets. -- [ ] Decide whether initiative-hosted artifacts can graduate into executable - changes, and which target metadata is required first. -- [ ] Decide the configuration or opt-in surface for repo-local versus - initiative-hosted artifacts. -- [ ] Define how `openspec new change` selects and reports the artifact home, - implementation target, initiative link, and action context. -- [ ] Decide how initiative-hosted target-bound changes bind to repo specs, - implementation roots, validation, archive, and sync behavior. -- [ ] Record compatibility behavior for existing repo-local and - workspace-local changes. -- [ ] Identify follow-on implementation slices and risks. - -## 19. Review Workspace Beta Compatibility Before Public Release - -Work item: -`work-items/19-review-workspace-beta-compatibility-before-public-release/` - -- [ ] Inventory workspace beta compatibility code and tests. -- [ ] Decide which beta-only compatibility paths should be removed before - public release. -- [ ] Decide which compatibility paths need explicit migration behavior or - release notes. -- [ ] Remove low-value shims that only support unpublished beta workspace - shapes. -- [ ] Update docs, tests, and agent guidance to match the chosen public - workspace compatibility contract. diff --git a/openspec/initiatives/context-store-and-initiatives/work-items/01-lock-the-direction/evidence.md b/openspec/initiatives/context-store-and-initiatives/work-items/01-lock-the-direction/evidence.md deleted file mode 100644 index d9483abe69..0000000000 --- a/openspec/initiatives/context-store-and-initiatives/work-items/01-lock-the-direction/evidence.md +++ /dev/null @@ -1,154 +0,0 @@ -# Work Item 01 Evidence - -## 2026-05-20 Initial Direction Lock - -Completed before this work item folder was created: - -- Added locked disposition to `roadmap.md`. -- Added locked product boundary to `direction.md`. -- Marked `openspec/changes/workspace-reimplementation-roadmap/START_HERE.md` as historical reference. -- Marked `openspec/changes/workspace-reimplementation-roadmap/HISTORICAL_DIRECTION.md` as historical reference. -- Marked `openspec/changes/workspace-reimplementation-roadmap/` as historical - reference. -- Marked `workspace-apply-repo-slice` and `workspace-verify-and-archive` as - deferred until initiative-linked repo-local changes exist. - -Research findings: - -- Current workspace setup, link, relink, list, open, update, and doctor behavior - is useful beta local-view infrastructure and should be preserved. -- Live specs describe current workspace-planning behavior. They should not be - rewritten during the initial direction lock; initiative artifacts should carry - future product intent until behavior changes. -- Existing runtime behavior should remain intact until initiatives and linked - repo-local changes can replace workspace-level planning. - -Verification: - -- `git diff --check` passed after the initial direction-lock edits. -- `openspec validate workspace-reimplementation-roadmap --no-interactive`, - `openspec validate workspace-apply-repo-slice --no-interactive`, and - `openspec validate workspace-verify-and-archive --no-interactive` failed - because those existing active changes have no spec deltas. That predates the - disposition wording and is tracked as an active-change cleanup question. - -## 2026-05-21 Initiative Entry Point - -Added `README.md` as the initiative entry point and linked it from -`.initiative.yaml`. - -The README explains: - -- this initiative is the source of product intent -- the reading order for direction, roadmap, tasks, decisions, questions, and - work items -- specs remain the current behavioral contract behind the code -- specs should not be rewritten for future intent until behavior changes - -Updated `work-items/01-lock-the-direction/tasks.md` to mark the initiative -source-of-intent review complete. - -## 2026-05-21 Historical Workspace Roadmap Review - -Reviewed the historical workspace reimplementation entry points: - -- `openspec/changes/workspace-reimplementation-roadmap/START_HERE.md` -- `openspec/changes/workspace-reimplementation-roadmap/HISTORICAL_DIRECTION.md` -- `openspec/changes/workspace-reimplementation-roadmap/README.md` -- `openspec/changes/workspace-reimplementation-roadmap/proposal.md` -- `openspec/changes/workspace-reimplementation-roadmap/POC_REFERENCE_GUIDE.md` - -Added a guard near the top of -`openspec/changes/workspace-reimplementation-roadmap/HISTORICAL_DIRECTION.md` stating -that the remaining sections are historical POC follow-up direction and should -not be treated as active implementation guidance. - -The roadmap README and handoff prompt already direct agents to the initiative -direction first and warn not to continue the old flat sibling queue unless a -later initiative-linked repo-change design reactivates it. - -## 2026-05-21 Active Workspace Proposal Review - -Reviewed active workspace proposal artifacts: - -- `workspace-reimplementation-roadmap` -- `workspace-agent-guidance` -- `workspace-apply-repo-slice` -- `workspace-verify-and-archive` - -Added small notes to `workspace-apply-repo-slice` and -`workspace-verify-and-archive` clarifying that the remaining proposal sections -are preserved for later reference, not discarded, and should become relevant -again after initiatives and initiative-linked repo-local changes exist. - -Left `workspace-agent-guidance` untouched because it already has unrelated -worktree edits and should be handled as a separate active-change disposition -decision. - -## 2026-05-21 User-Facing Docs Decision - -Decision: Do not update `docs/cli.md` as part of the initial direction lock -unless it misrepresents current user-facing behavior. - -Reasoning: - -- The direction lock is for contributors and agents deciding what to build next. -- User-facing docs should describe current CLI behavior, not future initiative - intent. -- Initiatives do not have a CLI surface yet, so announcing the pivot in user - docs would draw attention to an internal product direction before users can act - on it. - -Revisit user-facing docs when initiative or context-store commands exist, or if -current docs promise unavailable workspace apply, verify, or archive behavior. - -Verification: - -- `git diff --check` passed. -- No files under `openspec/specs/` or `schemas/workspace-planning/` were - modified in this pass. - -## 2026-05-21 Active Change Disposition - -Decision: Keep the active workspace changes as deferred reference placeholders. - -Rationale: - -- Workspace agent guidance, apply, verify, and archive are still expected to - matter after initiative infrastructure exists. -- The immediate focus should be context stores, initiatives, and - initiative-linked repo-local changes. -- Keeping the proposals preserves research and continuity without making them - the next implementation queue. - -Follow-up: - -- Revisit the deferred workspace changes after initiative-linked repo-local - changes define the durable handoff model. - -## Final Item 1 State - -Item 1 is complete. - -What is locked: - -- Initiative artifacts are the source of product intent for context stores, - collections, initiatives, workspaces, and repo-local changes. -- Specs and schemas remain the current behavioral contract and were not edited - for future intent. -- Historical workspace roadmap artifacts remain available as reference, not as - the active shipping queue. -- Deferred workspace changes remain active reference placeholders because their - domains are expected to matter after initiative infrastructure exists. -- User-facing docs were intentionally left unchanged unless they misrepresent - current behavior. - -Remaining risks: - -- `openspec list` still shows deferred workspace changes as active no-task - changes. This is intentional for now but may remain visually noisy. -- `workspace-agent-guidance` has unrelated worktree edits and should be handled - carefully before any future commit or archive decision. -- Future agents still need to read the initiative README first; the historical - workspace docs are safer now, but still contain useful old lifecycle details - deeper in the file. diff --git a/openspec/initiatives/context-store-and-initiatives/work-items/01-lock-the-direction/plan.md b/openspec/initiatives/context-store-and-initiatives/work-items/01-lock-the-direction/plan.md deleted file mode 100644 index 05a15c5a82..0000000000 --- a/openspec/initiatives/context-store-and-initiatives/work-items/01-lock-the-direction/plan.md +++ /dev/null @@ -1,90 +0,0 @@ -# Work Item 01: Lock The Direction - -## Goal - -Make the workspace-to-initiative pivot explicit enough that future agents and -contributors do not continue implementing the older "workspace owns the plan" -model. - -The locked model is: - -```text -Context stores sync truth. -Collections shape truth. -Initiatives coordinate work. -Workspaces open local views. -Changes implement repo-owned slices. -``` - -## Direction - -This work item is a non-spec direction pass, not a runtime removal. - -Specs should continue to describe the current behavioral contract behind the -code. Product intent, roadmap decisions, and future direction should live in the -initiative artifacts until a later implementation change intentionally updates -behavior and its specs together. - -Keep: - -- workspace setup, link, relink, list, open, update, and doctor -- linked repos and folders as local planning context -- workspace-local skills as local agent guidance -- "workspace visibility is not change commitment" - -Mark as transitional: - -- workspace-level `changes/` planning -- `workspace-planning` schema -- workspace-scoped status/instructions compatibility - -Defer: - -- workspace apply, verify, and archive as first-class lifecycle commands -- branch/worktree orchestration -- strong cross-repo validation -- dependency graph enforcement - -Supersede: - -- workspace as the durable shared planning home -- workspace-level planning artifacts as the canonical cross-repo plan -- workspace change planning as the long-term source of truth - -## Files To Review Now - -- `openspec/initiatives/context-store-and-initiatives/*.md` -- `openspec/initiatives/context-store-and-initiatives/work-items/**/*.md` -- `openspec/changes/workspace-reimplementation-roadmap/START_HERE.md` -- `openspec/changes/workspace-reimplementation-roadmap/HISTORICAL_DIRECTION.md` -- `openspec/changes/workspace-reimplementation-roadmap/*` -- active `openspec/changes/workspace-*` proposals -- `docs/cli.md` - -## Files To Leave Alone For Now - -- `openspec/specs/**/*.md` -- `schemas/workspace-planning/**` - -Those files should change only when we intentionally change behavior or create a -repo-owned implementation change that updates the relevant behavioral contract. - -## Non-Goals - -- Do not remove current workspace-planning runtime behavior. -- Do not delete the `workspace-planning` schema. -- Do not add CLI deprecation warnings until the initiative replacement exists. -- Do not implement context stores in this work item. -- Do not edit OpenSpec specs as part of the initial direction lock. - -## Done When - -- Initiative artifacts clearly carry the product intent and roadmap decisions. -- Historical workspace roadmap artifacts no longer read as the active shipping - queue. -- User-facing docs describe current workspaces as local views where that does - not contradict current behavior. -- Existing workspace-planning behavior is clearly treated as current behavior, - not the future product model, in initiative and roadmap artifacts. -- Workspace apply, verify, and archive are clearly deferred. -- Fresh agents can identify the initiative direction as the source of truth. diff --git a/openspec/initiatives/context-store-and-initiatives/work-items/01-lock-the-direction/tasks.md b/openspec/initiatives/context-store-and-initiatives/work-items/01-lock-the-direction/tasks.md deleted file mode 100644 index d04b60a620..0000000000 --- a/openspec/initiatives/context-store-and-initiatives/work-items/01-lock-the-direction/tasks.md +++ /dev/null @@ -1,44 +0,0 @@ -# Work Item 01 Tasks - -## Tracking Setup - -- [x] Create initiative-level `tasks.md`, `decisions.md`, and `questions.md`. -- [x] Create `work-items/01-lock-the-direction/`. -- [x] Record why roadmap implementation is tracked inside the initiative instead - of creating a new OpenSpec change. - -## Direction Lock Already Captured - -- [x] Add locked disposition to `roadmap.md`. -- [x] Add locked product boundary to `direction.md`. -- [x] Mark `openspec/changes/workspace-reimplementation-roadmap/START_HERE.md` as historical reference. -- [x] Mark `openspec/changes/workspace-reimplementation-roadmap/HISTORICAL_DIRECTION.md` as historical reference. -- [x] Mark `workspace-reimplementation-roadmap` as historical reference. -- [x] Mark `workspace-apply-repo-slice` as deferred. -- [x] Mark `workspace-verify-and-archive` as deferred. - -## Non-Spec Direction Pass - -- [x] Keep OpenSpec specs unchanged until behavior changes. -- [x] Review initiative artifacts for a clear source-of-intent story. -- [x] Review historical workspace roadmap artifacts for any remaining language - that tells agents to continue the old shipping queue. -- [x] Review active workspace proposal artifacts for any remaining language that - presents workspace apply, verify, or archive as next. -- [x] Decide whether user-facing docs need changes now; default to no unless - they misrepresent current behavior. -- [x] Record a decision that specs remain current behavioral contracts, while - initiative docs carry future product intent. - -## Active Change Disposition - -- [x] Decide whether `workspace-agent-guidance` should be reframed, closed, or - kept as a local-view guidance item. -- [x] Decide whether no-task deferred workspace changes should stay active, - move to archive, or be represented only by initiative work items. - -## Verification - -- [x] Run `git diff --check`. -- [x] Confirm no OpenSpec specs were modified in this pass. -- [x] Record evidence in `evidence.md`. diff --git a/openspec/initiatives/context-store-and-initiatives/work-items/02-stabilize-workspace-as-local-view/evidence.md b/openspec/initiatives/context-store-and-initiatives/work-items/02-stabilize-workspace-as-local-view/evidence.md deleted file mode 100644 index 85d0b0a03b..0000000000 --- a/openspec/initiatives/context-store-and-initiatives/work-items/02-stabilize-workspace-as-local-view/evidence.md +++ /dev/null @@ -1,68 +0,0 @@ -# Stabilize Workspace As Local View Evidence - -## Direction Evidence - -`direction.md` says the durable shared object is a synced context store, with -initiatives as the first major collection. It defines workspaces as local -working views over context stores and repos, and repo changes as repo/team-owned -implementation plans. - -The locked product boundary supersedes the older model where a workspace-level -`changes/` tree owned the canonical shared cross-repo plan. Existing -workspace-planning behavior can remain as beta or legacy infrastructure, but it -should not steer new lifecycle design. - -## Subagent Research - -Implementation research found that workspace setup, link, relink, list, open, -update, and doctor already mostly behave like local-view infrastructure: - -- shared link names live in workspace state -- machine-local paths and opener/skill state live in local state -- `workspace open` launches linked folders as a local working set -- linked repos are treated as context for workspace-planning commands -- `workspace update` refreshes workspace-local skills and leaves linked repos - untouched - -Guidance research found that the generated `AGENTS.md` block is the most -important mismatch because it still frames the workspace as planning across -linked repos and says to use `changes/` for workspace-level planning. - -Test research found strong current coverage for setup/list/doctor, link/relink, -open, update, artifact placement, and workspace-planning guards. The targeted -workspace/artifact test slice passed, as did the skill-template parity test. - -## Main Risk - -If generated workspace guidance continues to recommend workspace-level -`changes/`, agents may treat the workspace as the durable shared planning -object even though the initiative direction assigns durable coordination to -initiatives and implementation planning to repo-local changes. - -## Implementation Evidence - -The first implementation slice updates the generated workspace `AGENTS.md` -guidance and makes `workspace update` refresh the workspace-local open surface. -It also updates workspace-planning action context so beta workspace artifacts are -reported as `workspace-local` compatibility context instead of the source of -truth. - -Doctor/status review found that local path mappings, unresolved links, repair -steps, malformed local state, missing local state, repo specs paths, and skill -drift warnings are already covered. Normal installed-skill summaries are -deferred for now; the current slice only updates stale `workspace update` -wording so it matches the guidance refresh behavior. - -Verification: - -- `pnpm run build` -- `pnpm exec vitest run test/commands/workspace.test.ts test/commands/artifact-workflow.test.ts test/core/workspace/foundation.test.ts` -- `pnpm run lint` -- `git diff --check` - -## Closeout Evidence - -Live docs no longer describe workspaces as durable planning homes or as the -canonical place for cross-repo planning. Historical and deferred workspace -artifacts remain as reference material, with active deferred proposals labeled -so they do not steer the next implementation slice. diff --git a/openspec/initiatives/context-store-and-initiatives/work-items/02-stabilize-workspace-as-local-view/plan.md b/openspec/initiatives/context-store-and-initiatives/work-items/02-stabilize-workspace-as-local-view/plan.md deleted file mode 100644 index 147b94556a..0000000000 --- a/openspec/initiatives/context-store-and-initiatives/work-items/02-stabilize-workspace-as-local-view/plan.md +++ /dev/null @@ -1,80 +0,0 @@ -# Stabilize Workspace As Local View - -## Status - -Complete for the current local-view stabilization slice. Remaining workspace -planning/apply/verify/archive behavior stays deferred until initiative-linked -repo-local changes exist. - -## Source Of Truth - -Start from `../direction.md`. - -The relevant model is: - -```text -Context stores sync truth. -Collections shape truth. -Initiatives coordinate work. -Workspaces open local views. -Changes implement repo-owned slices. -``` - -## Goal - -Keep workspace setup, link, relink, list, open, update, and doctor useful while -making it clear that a workspace is a regenerable machine-local view, not the -durable coordination object. - -## Agreed Guidance Direction - -Generated workspace guidance should route agents by ownership: - -- Use the workspace to open the local view of coordinated work. -- Use initiatives for durable cross-team or cross-repo intent, decisions, - requirements, and coordination context. -- Use repo-local OpenSpec changes for implementation plans owned by a repo or - team. -- Use linked repos and folders to inspect context, understand ownership, and - make edits in the place that owns the work. -- Keep workspace-local files focused on local paths, opener state, agent setup, - and other machine-specific view state. -- Use OpenSpec workspace commands instead of hand-editing - `.openspec-workspace/*.yaml`. -- If a workspace contains legacy or beta workspace-level planning files, treat - them as compatibility context unless the user explicitly asks to use that beta - flow. - -## Guidance To Stop Reinforcing - -Do not tell agents to use workspace-level `changes/` as the planning home for -coordinated work. That reinforces the superseded model where a workspace-level -`changes/` tree owned the canonical shared cross-repo plan. - -Existing workspace-planning behavior may remain as beta or legacy -infrastructure, but it should not steer new lifecycle design. - -## Likely Repo Slice - -- Reword generated workspace guidance in - `src/core/workspace/open-surface.ts`. -- Update focused guidance tests. -- Make `workspace update` refresh the guidance block for existing workspaces. -- Keep specs untouched until a behavior change intentionally updates them. - -## Closeout - -Implemented: - -- generated workspace guidance now routes work by ownership -- `workspace update` refreshes workspace-local guidance/open-surface files and - managed agent skills -- workspace-planning action context treats beta workspace artifacts as - `workspace-local` compatibility context -- live docs describe workspaces as local views instead of durable planning homes - -Deferred: - -- normal doctor installed-skill inventory -- workspace apply, verify, and archive -- initiative-linked repo-local change orchestration diff --git a/openspec/initiatives/context-store-and-initiatives/work-items/02-stabilize-workspace-as-local-view/tasks.md b/openspec/initiatives/context-store-and-initiatives/work-items/02-stabilize-workspace-as-local-view/tasks.md deleted file mode 100644 index 5a07a1f579..0000000000 --- a/openspec/initiatives/context-store-and-initiatives/work-items/02-stabilize-workspace-as-local-view/tasks.md +++ /dev/null @@ -1,23 +0,0 @@ -# Stabilize Workspace As Local View Tasks - -- [x] Research current workspace runtime, guidance, and test coverage. -- [x] Re-anchor guidance direction in `direction.md`. -- [x] Decide that generated guidance should route durable coordination to - initiatives and implementation planning to repo-local changes. -- [x] Decide that generated guidance should stop recommending workspace-level - `changes/` as the planning home. -- [x] Decide that `workspace update` refreshes the generated guidance block - for existing workspaces. -- [x] Update workspace-planning action context so beta workspace artifacts are - compatibility context, not the source of truth. -- [x] Decide to defer normal doctor skill summaries until users need an - installed-skill inventory. -- [x] Update `workspace update` wording to include workspace-local guidance and - agent skills. -- [x] Define the minimal doctor/status improvement for local paths, unresolved - links, and installed agent skills. -- [x] Identify the focused code/test files for the implementation slice. -- [x] Run the targeted workspace and artifact workflow test slice before - landing implementation. -- [x] Close out live docs wording that still framed workspaces as durable - planning homes. diff --git a/openspec/initiatives/context-store-and-initiatives/work-items/03-add-context-store-foundation/evidence.md b/openspec/initiatives/context-store-and-initiatives/work-items/03-add-context-store-foundation/evidence.md deleted file mode 100644 index 402c2f8fbe..0000000000 --- a/openspec/initiatives/context-store-and-initiatives/work-items/03-add-context-store-foundation/evidence.md +++ /dev/null @@ -1,43 +0,0 @@ -# Add Context Store Foundation Evidence - -## Research Summary - -Existing OpenSpec patterns point toward a small explicit foundation: - -- Global data uses XDG/platform locations from `getGlobalDataDir()`. -- Workspace registries are machine-local convenience indexes under global data. -- Workspace portable state uses versioned YAML and strict Zod validation. -- Existing read/write helpers validate state before writing and use - `FileSystemUtils.writeFile()` to create parent directories. -- Schema/backend-style code favors small explicit adapters and registries over - heavy framework abstractions. - -## Decisions - -- The first context-store backend is Git/local checkout config only. -- OpenSpec records where the local checkout lives; it does not decide where real - team stores are cloned by default. -- The local registry is not source of truth. It is a machine-local index. -- Store-root metadata is portable source-of-identity for the synced store. -- Initiatives and collections are later consumers, not part of the store - foundation. -- A thin facade should hide raw registry/metadata writes before initiative CLI - wiring. - -## Implementation Evidence - -- `src/core/context-store/registry.ts` registers Git/local context stores, - lists local registry entries, and resolves registered stores with metadata id - validation. -- `src/core/context-store/index.ts` exports the facade. -- `test/core/context-store/registry.test.ts` covers registration, registry - merge/update, metadata mismatch rejection, listing, resolution, missing or - mismatched metadata, and initiative collection mounting from a resolved root. - -## Verification - -- `pnpm exec vitest run test/core/context-store/foundation.test.ts` -- `pnpm exec vitest run test/core/context-store/registry.test.ts` -- `pnpm run build` -- `pnpm run lint` -- `git diff --check` diff --git a/openspec/initiatives/context-store-and-initiatives/work-items/03-add-context-store-foundation/plan.md b/openspec/initiatives/context-store-and-initiatives/work-items/03-add-context-store-foundation/plan.md deleted file mode 100644 index 2ea0c27afd..0000000000 --- a/openspec/initiatives/context-store-and-initiatives/work-items/03-add-context-store-foundation/plan.md +++ /dev/null @@ -1,85 +0,0 @@ -# Add Context Store Foundation - -## Status - -Registration/resolution facade implemented. - -## Source Of Truth - -Start from `../direction.md`. - -The relevant model is: - -```text -Context stores sync truth. -Collections shape truth. -Initiatives coordinate work. -Workspaces open local views. -Changes implement repo-owned slices. -``` - -## Goal - -Add the smallest core foundation for context stores without making the store -layer know about initiatives, collections, workspaces, or repo-local changes. - -## Locked Direction - -- Support one backend for the first slice: a Git/local checkout backend. -- Treat the actual context store root as a user-chosen local Git checkout or - synced folder. -- Do not hide real team context stores under XDG data by default. -- Store the machine-local registry under global data: - `$XDG_DATA_HOME/openspec/context-stores/registry.yaml`. -- Store portable context-store identity inside the store root: - `/.openspec-store/store.yaml`. -- Start with backend identity/config, strict validation, path helpers, and - registry/metadata read-write helpers. -- Add a thin registration/resolution facade before initiative CLI wiring so - callers do not manipulate raw registry and metadata YAML directly. -- Do not reimplement the TypeScript or Node filesystem APIs as the public store - interface. -- Do not add initiative, collection, workspace-open, sync, pull, push, or CLI - behavior in this slice. - -## Initial Shape - -Machine-local registry: - -```yaml -version: 1 -stores: - acme-context: - backend: - type: git - local_path: /Users/me/repos/acme-context - remote: git@github.com:acme/context.git - branch: main -``` - -Portable metadata in the store root: - -```yaml -version: 1 -id: acme-context -``` - -## Likely Repo Slice - -- Add `src/core/context-store/foundation.ts`. -- Add `src/core/context-store/registry.ts`. -- Add `src/core/context-store/index.ts`. -- Export the core context-store foundation from `src/core/index.ts`. -- Add focused tests under `test/core/context-store/`. -- Keep specs untouched until a behavior/API contract is deliberately surfaced. - -## Implemented Facade Slice - -- Added `registerContextStore(...)`. -- Added `listRegisteredContextStores(...)`. -- Added `resolveRegisteredContextStore(...)`. -- Registration writes portable store metadata when missing, validates existing - metadata when present, and merges/updates the machine-local registry. -- Resolution validates that the registry id matches the store-root metadata id. -- No Git clone, pull, push, sync, workspace state, collection manifest, or CLI - behavior was added. diff --git a/openspec/initiatives/context-store-and-initiatives/work-items/03-add-context-store-foundation/tasks.md b/openspec/initiatives/context-store-and-initiatives/work-items/03-add-context-store-foundation/tasks.md deleted file mode 100644 index 4aeddc285d..0000000000 --- a/openspec/initiatives/context-store-and-initiatives/work-items/03-add-context-store-foundation/tasks.md +++ /dev/null @@ -1,17 +0,0 @@ -# Add Context Store Foundation Tasks - -- [x] Research existing config, registry, file-system, and schema/backend - patterns. -- [x] Decide to start with Git/local backend identity only, not a generic file - API. -- [x] Decide that real context store roots are user-chosen Git checkouts or - synced folders. -- [x] Decide that the local registry lives under global data and portable store - metadata lives inside the store root. -- [x] Add context-store foundation types, path helpers, parse/serialize, and - read/write helpers. -- [x] Add focused tests for validation, paths, registry roundtrip, metadata - roundtrip, and Git/local backend path resolution. -- [x] Run targeted verification. -- [x] Decide registration/resolution facade should precede initiative CLI. -- [x] Add context-store registration/list/resolve facade and tests. diff --git a/openspec/initiatives/context-store-and-initiatives/work-items/04-add-collection-foundation/evidence.md b/openspec/initiatives/context-store-and-initiatives/work-items/04-add-collection-foundation/evidence.md deleted file mode 100644 index fe751b6b91..0000000000 --- a/openspec/initiatives/context-store-and-initiatives/work-items/04-add-collection-foundation/evidence.md +++ /dev/null @@ -1,77 +0,0 @@ -# Add Collection Foundation Evidence - -## Research Summary - -Subagent and local review converged on the same direction: - -- Item 4 should define the boundary between store identity and product-specific - content meaning. -- The collection layer should own mounted namespaces and logical path fences. -- The context-store layer should stay content-agnostic. -- Initiative CRUD and initiative file shape belong to Item 5. -- A runtime injected registry is enough for now; persisted manifests and dynamic - plugins are premature. -- A thin registration facade should hide metadata and local registry writes, but - Item 4 should not depend on that facade. - -## Clean-Code Notes - -- Use module boundaries and mounted objects to carry context. -- Prefer `validateMount`, `parseCollectionPath`, `createCollectionRegistry`, - and `mountCollections` inside the collection module. -- Avoid public helper names that stack every concept together, such as - `validateContextStoreCollectionRelativePath`. -- Keep path resolution pure and lexical until a future write-capable layer - deliberately handles symlinks, canonical parent paths, and backend behavior. -- Keep persisted YAML shape below the public setup surface. Runtime/public - handles should use camelCase fields such as `storeRoot`; persisted backend - state can continue to use `local_path`. - -## Chosen Pattern - -Use a two-step pattern: - -```ts -const store = await registerContextStore({ - id: "acme-context", - backend: gitLocalBackend({ - localPath: "/Users/me/repos/acme-context", - remote: "git@github.com:acme/context.git", - branch: "main", - }), -}); - -const collections = createCollectionRegistry([ - { id: "initiatives", mount: "initiatives" }, -]); - -const mounted = mountCollections({ - storeRoot: store.storeRoot, - collections, -}); -``` - -For Item 4 itself, `mountCollections({ storeRoot, collections })` is the -canonical API. One-call setup facades, store lifecycle objects, builder DSLs, -and initiative-specific setup presets are deferred. - -## Implementation Evidence - -- `src/core/collections/runtime.ts` defines runtime collection - definitions, registries, mounted collection contexts, logical path parsing, - and mount/path resolution. -- `src/core/collections/index.ts` exports the collection module, and - `src/core/index.ts` re-exports it for core consumers. -- `test/core/collections/runtime.test.ts` covers mount and id validation, - logical path parsing, duplicate id/mount rejection, Windows-style roots, - `createHandle(context)`, no filesystem creation, and generic `initiatives/` - mounting. - -## Verification - -- `pnpm exec vitest run test/core/collections/runtime.test.ts` -- `pnpm run build` -- `pnpm exec vitest run test/core/collections/runtime.test.ts test/core/context-store/foundation.test.ts test/core/planning-home.test.ts` -- `pnpm exec vitest run test/utils/file-system.test.ts` -- `pnpm run lint` -- `git diff --check` diff --git a/openspec/initiatives/context-store-and-initiatives/work-items/04-add-collection-foundation/plan.md b/openspec/initiatives/context-store-and-initiatives/work-items/04-add-collection-foundation/plan.md deleted file mode 100644 index 6475df3a88..0000000000 --- a/openspec/initiatives/context-store-and-initiatives/work-items/04-add-collection-foundation/plan.md +++ /dev/null @@ -1,198 +0,0 @@ -# Add Collection Foundation - -## Status - -First implementation slice implemented. - -## Source Of Truth - -Start from `../../direction.md`. - -The relevant model is: - -```text -Context stores sync truth. -Collections shape truth. -Initiatives coordinate work. -Workspaces open local views. -Changes implement repo-owned slices. -``` - -## Goal - -Add the smallest collection foundation that lets product-specific content -systems mount inside a context store without making the context-store layer know -what those systems mean. - -## Locked Direction So Far - -- Treat Item 4 as a mount/path foundation, not a collection runtime. -- Keep collection composition runtime-only and dependency-injected. -- Keep context-store registration separate from runtime collection mounting. -- Use a future thin registration facade for metadata/registry setup instead of - showing raw registry or metadata state writes in public examples. -- Do not add a persisted collection manifest yet. -- Do not add CLI behavior yet. -- Do not add generic `read`, `write`, `list`, or `delete` helpers. -- Do not add initiative file shape, initiative CRUD, or initiative validation - yet. -- Prove `initiatives/` can mount through generic collection definitions, not - through initiative-specific context-store logic. - -## Naming Direction - -Use the module/object boundary to carry context instead of growing helper names. - -Use a focused generic module such as `src/core/collections/runtime.ts` with -short names: - -```ts -validateCollectionId(id); -validateMount(mount); -parseCollectionPath(input); - -createCollectionRegistry(...); -mountCollections(...); -``` - -Prefer mounted objects for context-aware operations: - -```ts -const mounted = collections.require("initiatives"); - -mounted.resolvePath("launch-billing-flow/initiative.yaml"); -mounted.toStorePath("launch-billing-flow/initiative.yaml"); -``` - -Avoid names like `validateContextStoreCollectionRelativePath`. They indicate -that too much context has leaked into a standalone helper name. - -## Minimal API Shape - -The first slice should stay close to this: - -```ts -interface CollectionDefinition { - id: string; - mount: string; - metadata?: CollectionMetadata; - hooks?: CollectionHooks; - createHandle?: (context: MountedCollectionContext) => THandle; -} - -interface MountedCollectionContext { - storeRoot: string; - collectionId: string; - mount: string; - mountRoot: string; - resolvePath(relativePath?: string): string; - toStorePath(relativePath?: string): string; -} - -interface MountedCollection { - collectionId: string; - mount: string; - mountRoot: string; - context: MountedCollectionContext; - handle: THandle | undefined; -} -``` - -Use `id` on definitions, but `collectionId` on mounted handles and contexts so -domain object IDs such as initiative IDs do not collide with collection type IDs. - -## Setup And Mounting Pattern - -Use two separate layers: - -1. A context-store registration facade for setup. -2. A pure runtime collection mounting API for Item 4. - -Registration should hide persisted YAML details: - -```ts -const store = await registerContextStore({ - id: "acme-context", - backend: gitLocalBackend({ - localPath: "/Users/me/repos/acme-context", - remote: "git@github.com:acme/context.git", - branch: "main", - }), -}); -``` - -The registration facade can call lower-level helpers such as backend config -normalization, metadata writes, and local registry writes internally. Public -examples should not call raw `writeContextStoreMetadataState(...)`, -`writeContextStoreRegistryState(...)`, or expose persisted snake_case backend -state such as `local_path`. - -Item 4 mounting should stay independent of registration and accept only the -authority it needs: - -```ts -const collections = createCollectionRegistry([ - { id: "initiatives", mount: "initiatives" }, -]); - -const mounted = mountCollections({ - storeRoot: store.storeRoot, - collections, -}); - -mounted.require("initiatives").resolvePath( - "launch-billing-flow/initiative.yaml" -); -``` - -Prefer `mountCollections({ storeRoot, collections })` as the canonical first -API. Passing a whole store handle can wait until there is a real need. - -## Path Direction - -- Mount names are single-segment kebab-case folder names such as `initiatives`, - `decisions`, or `api-catalog`. -- Collection-relative paths are logical portable paths inside a mount. -- The path resolver is lexical only. It proves that a logical path belongs under - a collection mount; it does not claim to be a filesystem security sandbox. -- Future write-capable helpers must revisit symlink and canonical parent-path - handling before touching disk. - -Reject: - -- empty mounts -- `.` -- `..` -- hidden/reserved mounts such as `.openspec-store` -- absolute paths -- Windows drive paths -- UNC paths -- NUL bytes -- traversal segments -- sibling-prefix escapes - -## Deferred - -- Store-level collection config files. -- Dynamic plugin loading. -- One-call `setupContextStore({ id, backend, collections })` APIs. -- `createStore(...).setup()` lifecycle APIs. -- Builder-style setup DSLs. -- Initiative-specific setup presets in the generic context-store layer. -- Template override search paths. -- Rich validation execution. -- Agent guidance generation. -- Workspace integration. -- Git sync, commits, pull, push, watch, or conflict behavior. - -## Implemented Slice - -- Added a pure runtime collection module at - `src/core/collections/runtime.ts`. -- Exported the module through `src/core/collections/index.ts` and - `src/core/index.ts`. -- Added focused tests under `test/core/collections/runtime.test.ts`. -- Proved a generic `{ id: "initiatives", mount: "initiatives" }` definition can - mount and resolve paths without initiative-specific store logic. -- Kept validation/template hooks as inert extension fields for now; rich hook - execution remains deferred. diff --git a/openspec/initiatives/context-store-and-initiatives/work-items/04-add-collection-foundation/tasks.md b/openspec/initiatives/context-store-and-initiatives/work-items/04-add-collection-foundation/tasks.md deleted file mode 100644 index 215b8092da..0000000000 --- a/openspec/initiatives/context-store-and-initiatives/work-items/04-add-collection-foundation/tasks.md +++ /dev/null @@ -1,14 +0,0 @@ -# Add Collection Foundation Tasks - -- [x] Research what Item 4 needs to decide. -- [x] Compare collection model options. -- [x] Run clean-code and design-pattern review. -- [x] Decide to keep Item 4 as a runtime mount/path foundation. -- [x] Decide to avoid long context-stacked helper names. -- [x] Decide to separate context-store registration from runtime collection - mounting. -- [x] Define exact collection mount and path rules. -- [x] Define the minimal runtime registry and mounted collection API. -- [x] Implement collection foundation helpers and tests. -- [x] Prove `initiatives/` can mount without store-specific initiative logic. -- [x] Run targeted verification. diff --git a/openspec/initiatives/context-store-and-initiatives/work-items/05-ship-initiative-mvp/evidence.md b/openspec/initiatives/context-store-and-initiatives/work-items/05-ship-initiative-mvp/evidence.md deleted file mode 100644 index 7b9e7ab036..0000000000 --- a/openspec/initiatives/context-store-and-initiatives/work-items/05-ship-initiative-mvp/evidence.md +++ /dev/null @@ -1,99 +0,0 @@ -# Ship Initiative MVP Evidence - -## Research Summary - -- Initiative code should live in `src/core/collections/initiatives/`, outside - `src/core/context-store/`. -- Initiative APIs should consume a mounted `initiatives` collection from Item 4 - rather than raw context-store roots. -- The first coding slice should lock metadata and templates before mounted - create/list operations. -- Visible `initiative.yaml` is preferred for the new shared initiative model. -- `links.yaml` should not exist in the initiative MVP. Repo-change wiring is a - workspace/local coordination concern to revisit later. -- Read/show, update, and delete have extra policy risk, so create/list should - come before broader lifecycle behavior. -- The first mounted operation slice should do create/list only. A full - `readInitiative` API is deferred until the return shape is clearer. - -## Decisions - -- Use `src/core/collections/initiatives/` for initiative-domain code. -- Do not put initiative semantics into `src/core/context-store/`. -- Add `initiative.yaml` strict parse/serialize helpers. -- Generate Markdown files up front, but do not validate Markdown content beyond - existence/templates in the first pass. -- Defer workspace opening, repo resolution, status dashboards, sync, linked - change lifecycle, `links.yaml`, `contracts/`, and CLI behavior. -- Detect initiatives by valid `initiative.yaml`: missing means ignore, invalid - means fail loudly, and the YAML `id` must match the folder name. - -## Suggested First Coding Slice - -Add: - -- `src/core/collections/initiatives/schema.ts` -- `src/core/collections/initiatives/templates.ts` -- `src/core/collections/initiatives/operations.ts` -- `src/core/collections/initiatives/index.ts` -- focused tests under `test/core/collections/initiatives/` - -Cover: - -- constants for initiative file names -- `validateInitiativeId` -- strict `initiative.yaml` parse/serialize -- create/list operations through a mounted `initiatives` collection -- template builders for `requirements.md`, `design.md`, `decisions.md`, - `questions.md`, and `tasks.md` -- tests for valid and invalid metadata, invalid IDs, unknown YAML fields, - required `created`, and generated template names/content shape - -## Implementation Evidence - -- `src/core/collections/initiatives/schema.ts` defines initiative constants, - strict persisted `initiative.yaml` parsing/serialization, required - `created`, bounded JSON-like metadata, statuses, and portable kebab-case - initiative IDs. -- `src/core/collections/initiatives/templates.ts` defines deterministic default - Markdown file builders for requirements, design, decisions, questions, and - tasks. -- `src/core/collections/initiatives/index.ts` exports the initiative - schema/template surface inside the initiative module only. -- `src/core/collections/initiatives/operations.ts` creates MVP initiative - folders and lists initiative states using the valid-`initiative.yaml` - detection rule. -- `src/core/collections/index.ts` exports the initiative module now that it has - a mounted operation API. -- `test/core/collections/initiatives/schema.test.ts` covers file constants, - no `links.yaml`, ID validation, strict YAML behavior, required `created`, - default owners/metadata, metadata validation, and serialization round trips. -- `test/core/collections/initiatives/templates.test.ts` covers generated - Markdown file names, deterministic ordering, trailing newlines, and expected - section headings. -- `test/core/collections/initiatives/operations.test.ts` covers create, list, - duplicate protection, cleanup on partial write failure, missing - `initiative.yaml` ignored, invalid `initiative.yaml` failure, and folder/id - mismatch failure. -- `src/core/context-store/registry.ts` was added as the next integration - enabler before CLI wiring. -- `src/commands/initiative.ts` adds `openspec initiative create/list` as a thin - CLI adapter over the context-store facade and mounted initiatives collection. -- `src/cli/index.ts` registers the initiative command. -- `src/core/completions/command-registry.ts` registers static completion - metadata for `initiative create/list/ls`. -- `test/commands/initiative.test.ts` covers JSON create, `--store-path` list, - human output, selector errors, duplicate create errors, and completion - registry entries. - -## Verification - -- `pnpm exec vitest run test/core/collections/initiatives/schema.test.ts test/core/collections/initiatives/templates.test.ts` -- `pnpm exec vitest run test/core/collections/initiatives/operations.test.ts` -- `pnpm exec vitest run test/core/collections/initiatives/schema.test.ts test/core/collections/initiatives/templates.test.ts test/core/collections/initiatives/operations.test.ts test/core/collections/runtime.test.ts test/core/context-store/foundation.test.ts test/core/planning-home.test.ts` -- `pnpm exec vitest run test/commands/initiative.test.ts` -- `pnpm exec vitest run test/core/context-store/registry.test.ts test/core/collections/initiatives/operations.test.ts test/core/collections/initiatives/schema.test.ts test/core/collections/initiatives/templates.test.ts test/core/collections/runtime.test.ts` -- `pnpm exec vitest run test/commands/workspace.test.ts` -- `pnpm run build` -- `pnpm run lint` -- `git diff --check` diff --git a/openspec/initiatives/context-store-and-initiatives/work-items/05-ship-initiative-mvp/plan.md b/openspec/initiatives/context-store-and-initiatives/work-items/05-ship-initiative-mvp/plan.md deleted file mode 100644 index d6463765cb..0000000000 --- a/openspec/initiatives/context-store-and-initiatives/work-items/05-ship-initiative-mvp/plan.md +++ /dev/null @@ -1,236 +0,0 @@ -# Ship Initiative MVP - -## Status - -Create/list operation and CLI adapter slices complete. Full read/show, update, -and delete policy is deferred to later agent-first discovery and lifecycle -work. - -## Source Of Truth - -Start from `../../direction.md`. - -The relevant model is: - -```text -Context stores sync truth. -Collections shape truth. -Initiatives coordinate work. -Workspaces open local views. -Changes implement repo-owned slices. -``` - -## Goal - -Give coordinated work a durable, shared, agent-consumable home inside an -`initiatives/` collection. - -## Roadmap Shape - -Default initiative shape: - -```text -initiatives// - initiative.yaml - requirements.md - design.md - decisions.md - questions.md - tasks.md -``` - -Direction also leaves room for later `contracts/` content: - -```text -initiatives// - contracts/ -``` - -## Initial Boundaries - -- Initiative code should live outside `src/core/context-store/`. -- Context-store core should not know initiative semantics. -- Initiative APIs should consume a mounted `initiatives` collection from Item 4. -- Repo-local OpenSpec changes remain the implementation artifacts; initiatives - coordinate intent, decisions, questions, and tasks. -- Do not implement workspace opening, repo resolution, status dashboards, sync, - or linked change lifecycle in this item. - -## Locked Direction So Far - -- Put initiative code under `src/core/collections/initiatives/`. -- Export initiatives from `src/core/index.ts` only after a real API exists. -- Use visible `initiative.yaml`, not hidden `.initiative.yaml`, for the runtime - context-store initiative model. Existing roadmap folders may still carry - legacy `.initiative.yaml` progress metadata until that tracker is migrated or - retired. -- Use strict YAML parsing and validation, following the existing foundation - patterns. -- Do not create `links.yaml` in the initiative MVP. Repo-change wiring belongs - to workspace/local coordination work later. -- Keep Markdown validation light; generate useful structure but do not validate - prose content yet. -- Start implementation with initiative schema and template helpers before - mounted collection operations. -- For the first mounted operation slice, add create and list only. Avoid a - broad `readInitiative` API until the shape of "full initiative" is clearer. -- Treat a child folder as an initiative only when it contains a valid - `initiative.yaml`. Missing `initiative.yaml` means "not an initiative"; - invalid `initiative.yaml` means broken shared state and should fail loudly. - -## Deferred From Item 5 - -- Full initiative show/read behavior belongs in agent-first initiative discovery - once the return shape is clearer. -- Metadata update and guarded delete belong in later lifecycle work after - create/list usage has shaped the policy. - -## Initial `initiative.yaml` - -Recommended shape: - -```yaml -version: 1 -id: launch-billing-flow -title: Launch Billing Flow -summary: > - Coordinate the billing launch across product, API, and client surfaces. -status: exploring -created: "2026-05-21" -owners: [] -metadata: {} -``` - -Required: - -- `version` -- `id` -- `title` -- `summary` -- `status` -- `created` - -Defaulted or optional: - -- `owners` -- `metadata` - -Initial statuses: - -- `exploring` -- `active` -- `complete` -- `archived` - -## Initial Markdown Templates - -Create these files up front: - -- `requirements.md`: product intent, accepted requirements, out of scope. -- `design.md`: context, approach, affected areas, dependencies, risks. -- `decisions.md`: accepted decisions with date/title/decision/why/implications. -- `questions.md`: open and resolved questions. -- `tasks.md`: coordination tasks only, not repo implementation tasks. - -Defer `contracts/`, `README.md`, milestones, dependency graphs, external issue -links, workspace path mappings, status dashboards, `links.yaml`, and Markdown -content validation. - -## Likely Repo Slice - -- Add `src/core/collections/initiatives/schema.ts`. -- Add `src/core/collections/initiatives/templates.ts`. -- Add `src/core/collections/initiatives/index.ts`. -- Add focused tests under `test/core/collections/initiatives/`. -- Add types, constants, ID validation, strict `initiative.yaml` - parse/serialize helpers, and default template builders. -- Add create/list mounted collection operations after schema and templates are - locked. -- Keep context-store collection APIs unchanged unless a real integration gap is - found. - -## Implemented Slice - -- Added `src/core/collections/initiatives/schema.ts`. -- Added `src/core/collections/initiatives/templates.ts`. -- Added `src/core/collections/initiatives/index.ts`. -- Added focused tests under `test/core/collections/initiatives/`. -- Exported initiatives through `src/core/collections/index.ts` now that a - mounted operation API exists. -- Kept `links.yaml` out of the initiative MVP file contract. - -## Operation Slice Direction - -- Add `src/core/collections/initiatives/operations.ts`. -- Export initiatives through `src/core/collections/index.ts` now that a mounted - operation API exists. -- `createInitiative` should create exactly the MVP file shape: - `initiative.yaml`, `requirements.md`, `design.md`, `decisions.md`, - `questions.md`, and `tasks.md`. -- `createInitiative` should generate `created` through an injectable date - provider, fail if the initiative folder already exists, and clean up a - partially created folder on write failure. -- `listInitiatives` should inspect immediate child directories under the - mounted `initiatives` collection, ignore folders without `initiative.yaml`, - parse and validate folders with `initiative.yaml`, require - `initiative.yaml.id` to match the folder name, and return initiative states - sorted by id. - -## Implemented Operation Slice - -- Added `src/core/collections/initiatives/operations.ts`. -- Added `createInitiative` for creating the MVP folder shape through a mounted - `initiatives` collection. -- Added `listInitiatives` using the valid-`initiative.yaml` detection rule. -- Exported initiatives through `src/core/collections/index.ts`. -- Added focused operation tests under - `test/core/collections/initiatives/operations.test.ts`. - -## Next Integration Enabler - -Before adding `openspec initiative create/list`, add a context-store -registration/resolution facade so CLI code can resolve a named store and mount -the initiatives collection without exposing raw registry or metadata YAML. - -## CLI Adapter Direction - -Add the first initiative CLI surface as a thin adapter over the mounted -collection operations: - -```bash -openspec initiative create --store --title --summary <summary> -openspec initiative create <id> --store-path <path> --title <title> --summary <summary> -openspec initiative list --store <store-id> -openspec initiative list --store-path <path> -``` - -Use `initiative create/list` as a deliberate noun namespace, similar to -`workspace` and `schema`, even though newer OpenSpec conventions generally -prefer verb-first top-level commands. The stricter alternative would spread -initiative behavior across `new initiative` and global `list` flags, which is a -larger surface for this slice because initiative commands must resolve a -context store. - -Keep store selection explicit in the first CLI slice. Require either -`--store <id>` or `--store-path <path>`, reject both together, and do not add -current-directory discovery, single-store auto-selection, an interactive picker, -a global default store, or workspace selected-store state yet. - -Because shell completions are manually registered, adding the runtime command -also requires adding `initiative create/list/ls` to `COMMAND_REGISTRY`. Keep -completion support static for now: command names and flags only, with no dynamic -store-id or initiative-id completion. - -## Implemented CLI Adapter Slice - -- Added `src/commands/initiative.ts`. -- Registered `openspec initiative create` and `openspec initiative list` from - the top-level CLI. -- Added `openspec initiative ls` as an alias for list. -- Required explicit context-store selection through `--store <id>` or - `--store-path <path>`. -- Rejected conflicting `--store` and `--store-path` selectors. -- Returned workspace-style JSON payloads with a top-level `status` diagnostics - array. -- Added static shell completion metadata for `initiative create/list/ls`. -- Added focused command tests under `test/commands/initiative.test.ts`. diff --git a/openspec/initiatives/context-store-and-initiatives/work-items/05-ship-initiative-mvp/tasks.md b/openspec/initiatives/context-store-and-initiatives/work-items/05-ship-initiative-mvp/tasks.md deleted file mode 100644 index 197a333bf1..0000000000 --- a/openspec/initiatives/context-store-and-initiatives/work-items/05-ship-initiative-mvp/tasks.md +++ /dev/null @@ -1,21 +0,0 @@ -# Ship Initiative MVP Tasks - -- [x] Create Item 5 work-item tracking notes. -- [x] Research initiative shape, API, module placement, and first slice. -- [x] Decide where initiative code lives. -- [x] Decide required `initiative.yaml` metadata. -- [x] Decide no initiative `links.yaml` in the MVP. -- [x] Decide first coding slice starts with initiative schema/templates before operations. -- [x] Add initiative schema helpers and tests. -- [x] Add default initiative templates. -- [x] Run targeted verification for schema/templates. -- [x] Decide create/list-only operation slice. -- [x] Add create/list mounted initiative operations and tests. -- [x] Run targeted verification for operations. -- [x] Research initiative CLI adapter gaps. -- [x] Decide explicit context-store selection for first CLI slice. -- [x] Document noun-command and manual-completion tradeoffs. -- [x] Add `openspec initiative create/list` CLI adapter. -- [x] Register static shell completions for initiative commands. -- [x] Add focused CLI tests for create/list, selection errors, and completions. -- [x] Run targeted verification for the initiative CLI adapter. diff --git a/openspec/initiatives/context-store-and-initiatives/work-items/06-add-minimal-context-store-ux/evidence.md b/openspec/initiatives/context-store-and-initiatives/work-items/06-add-minimal-context-store-ux/evidence.md deleted file mode 100644 index 6b0bc32b2e..0000000000 --- a/openspec/initiatives/context-store-and-initiatives/work-items/06-add-minimal-context-store-ux/evidence.md +++ /dev/null @@ -1,97 +0,0 @@ -# Add Minimal Context Store UX Evidence - -## Conversation Decisions - -- The next roadmap step should not jump straight to repo-local change linking - or workspace initiative opening. -- Teams first need a simple way to create or register the shared context store - that holds initiatives. -- The workflow is agent-first: the user prompts an agent, and the agent uses CLI - primitives to discover stores and initiatives. -- `context-store` should be the top-level command namespace for now. It is more - explicit for agents than `store`, and `store` can remain shorthand in scoped - flags such as `initiative list --store <id>`. -- A store can start as a local Git-backed folder. OpenSpec can help create the - folder, write metadata, register it locally, and optionally initialize Git. -- When setup does not receive `--path`, it should create or use `./<id>`. This - keeps the real shared store visible and avoids hiding it under global data. -- Using the current directory should require explicit `--path .`. -- If a user registers an existing folder or clone, the default store id can be - the repo or folder name. -- Portable `.openspec-store/store.yaml` metadata should be checked in and should - not include local paths. -- `.openspec-store/store.yaml` is the identity file itself, not a bundle beside - another checked-in metadata file. It should contain only `version` and `id` - for now. -- Future backend, sync, collection, permission, or policy config should not be - added to `store.yaml` by default. -- The local registry maps store ids to local paths on one machine. -- Remote-url clone/setup sugar is useful but can wait. -- `initiative list` should list all registered stores by default; `--store` - should filter. -- Interactive setup should prompt for Git initialization and default to yes - when no explicit Git flag is provided. -- Non-interactive, JSON, `--init-git`, and `--no-init-git` setup should not - prompt. -- `context-store register` should be idempotent for the same id/path and fail - for the same id with a different path until a future explicit replacement - option exists. -- `context-store list` should stay a simple registry index and should not show - health warnings. -- `context-store doctor` owns health diagnostics. The first slice should check - registry/path/metadata and cheap Git repository presence, not dirty state, - branch, remote, sync, pull/push, or conflicts. -- `initiative list` should allow partial success in all-store mode: show - initiatives from readable stores and print one small warning pointing to - `context-store doctor` when other registered stores cannot be read. -- Filtered `initiative list --store` and explicit `--store-path` should fail - directly when the selected store cannot be read. -- Partial success should exit 0 with warning diagnostics in JSON. Total failure - should exit nonzero. -- Register id inference should use the repo/folder name as-is with normal - context-store id validation. Do not add normalization in this slice. -- Setup should reject non-empty folders without context-store metadata for now. -- Registry conflicts should fail when the same id points at a different path or - the same path is already registered under a different id. -- Empty states should stay simple: no stores registered for `context-store list` - and `doctor`; no initiatives found because no stores are registered for - `initiative list`. -- Static shell completion metadata is now part of the shipped command surface; - dynamic store-id and initiative-id completions remain deferred. - -## Risks To Check Before Implementation - -- Existing command naming conventions may prefer verb-first flows, while - context-store commands are naturally noun namespaced. -- Shell completions are manually registered; keep future command additions in - `src/core/completions/command-registry.ts` with focused registry tests. -- Human output should match existing compact CLI output patterns. -- JSON output should be stable enough for agents without over-modeling future - sync or remote behavior. - -## Implementation Evidence - -- `src/commands/context-store.ts` adds the `context-store` command namespace - with setup, register, list, and doctor subcommands. -- `src/cli/index.ts` registers the context-store command. -- `src/commands/context-store.ts` keeps strict CLI setup/register policy in the - command layer while reusing context-store foundation helpers. -- `src/commands/initiative.ts` now lets `initiative list` search all registered - stores by default, keeps `--store` as a filter, preserves `--store-path`, and - reports all-store partial success with warning diagnostics. -- `src/core/completions/command-registry.ts` registers static completion - metadata for the context-store command surface. -- `test/commands/context-store.test.ts` covers setup, register, list, doctor, - conflict handling, non-empty setup rejection, and interactive Git init. -- `test/commands/initiative.test.ts` covers all-store initiative listing, - compact human output, empty registered-store state, partial success, and all - unreadable stores. - -## Verification - -- `pnpm run build` -- `pnpm exec vitest run test/commands/context-store.test.ts test/commands/initiative.test.ts` -- `pnpm exec vitest run test/core/context-store/foundation.test.ts - test/core/context-store/registry.test.ts - test/core/collections/initiatives/operations.test.ts` -- `pnpm run lint` diff --git a/openspec/initiatives/context-store-and-initiatives/work-items/06-add-minimal-context-store-ux/plan.md b/openspec/initiatives/context-store-and-initiatives/work-items/06-add-minimal-context-store-ux/plan.md deleted file mode 100644 index 35f64ceb5c..0000000000 --- a/openspec/initiatives/context-store-and-initiatives/work-items/06-add-minimal-context-store-ux/plan.md +++ /dev/null @@ -1,333 +0,0 @@ -# Add Minimal Context Store UX - -## Status - -Minimal context-store CLI and all-store initiative listing implemented. - -## Source Of Truth - -Start from `../../direction.md`. - -The current roadmap order is: - -```text -Context stores sync truth. -Collections shape truth. -Initiatives coordinate work. -Workspaces open local views. -Changes implement repo-owned slices. -``` - -This item exists because agent-first initiative workflows need a usable shared -store before repo-local handoff and workspace opening can feel coherent. - -## Goal - -Let a user or agent create, register, list, and diagnose local context stores -without knowing the internal registry layout. - -## Agent-First Framing - -The expected user prompt is closer to: - -```text -Using initiative billing-launch, explore the API work and create a proposal. -``` - -Before an agent can do that, it needs to answer: - -- Which context stores are registered locally? -- Which store contains the named initiative? -- Is the registered store path valid? -- Is store metadata present and consistent? -- If no store exists yet, how should one be created? - -This work item should provide those primitives. It should not implement -repo-local initiative linking, initiative resolution, workspace opening, or -progress/status dashboards. - -## Locked Direction So Far - -- Keep the user-facing term `store` for now; naming polish is deferred. -- Use `context-store` as the top-level CLI namespace for this slice. It is more - explicit for agents and avoids overloading a broad top-level `store` command. - Keep `store` as shorthand only when the context is already scoped, such as - `initiative list --store <id>`. -- `context-store setup <id>` should create or use a local folder, write portable - store metadata, register the local path, and optionally initialize Git. -- When `--path` is omitted, `context-store setup <id>` should default to - `./<id>`. -- Using the current directory should be explicit with `--path .`; setup should - not silently turn the current repo into a context store. -- The actual shared context store should be visible on disk, not hidden under - XDG/global data. XDG/global data is only for the machine-local registry. -- `context-store register <path>` should register an existing clone or folder. -- Registration means "this folder already exists on my machine; remember it as - a known context store." It should not create the folder, initialize Git, pull, - push, commit, or create remotes. -- Default the store id from the repo or folder name when metadata is missing. -- Portable store metadata is exactly `.openspec-store/store.yaml`. It should be - checked into the context-store repo and contain only portable identity for - now: - -```yaml -version: 1 -id: team-context -``` - -- Do not put backend config, local paths, remote URLs, collection config, sync - policy, or permissions in `store.yaml`. -- If future collection/store config is needed, add a separate explicit file - rather than expanding the identity file by default. -- Machine-local registry state should stay outside the checked-in store and map - store ids to local paths. -- Registration should not pull, push, commit, or create remote repositories. -- Remote-url registration or clone sugar can come later. -- `initiative list` should default to all registered stores. `--store` should - filter to one store, and `--store-path` should remain an explicit escape - hatch. -- Human output should stay compact and avoid a `Status` column for now. - -## Suggested Command Shape - -```bash -openspec context-store setup <id> [--path <path>] [--init-git|--no-init-git] [--json] -openspec context-store register <path> [--id <id>] [--json] -openspec context-store list [--json] -openspec context-store doctor [id] [--json] -openspec initiative list [--store <id>] [--store-path <path>] [--json] -``` - -## Command Behavior - -### `context-store setup` - -`context-store setup <id>` creates or uses a visible local store root and -registers it on the current machine. - -Locked behavior: - -- Default path is `./<id>` when `--path` is omitted. -- Current-directory setup is allowed only with explicit `--path .`. -- Missing folders are created. -- Existing folders are allowed when metadata is missing or matches the requested - id. -- Non-empty folders without context-store metadata are not supported for setup - in this slice. -- Existing metadata with a different id fails. -- File paths fail. -- `.openspec-store/store.yaml` is written when missing. -- The store is registered in the machine-local registry. -- Interactive TTY mode prompts for Git initialization when neither - `--init-git` nor `--no-init-git` is provided; the default answer is yes. -- `--json`, non-TTY execution, `--init-git`, and `--no-init-git` do not prompt. -- Git is initialized only when the prompt answer is yes or `--init-git` is - passed. -- Setup does not commit, push, pull, create remotes, or create hosted repos. -- If a user wants to initialize an existing non-empty folder, fail with a clear - message and suggest filing the use case or using `context-store register` for - an existing context store. - -Suggested human output: - -```text -Context store setup complete - -ID: team-context -Location: /Users/me/work/team-context -Metadata: /Users/me/work/team-context/.openspec-store/store.yaml -Registry: /Users/me/.local/share/openspec/context-stores/registry.yaml -Git: initialized -``` - -### `context-store register` - -`context-store register <path>` records an existing local folder or clone as a -known context store on the current machine. - -Locked behavior: - -- Path must already exist and be a directory. -- If `.openspec-store/store.yaml` exists, use its id. -- `--id` may confirm the metadata id but cannot conflict with it. -- If metadata is missing, infer the id from the folder or repo name unless - `--id` is passed. -- Inference uses the folder or repo name as-is and then applies normal context - store id validation. Do not do clever normalization in this slice. -- Missing metadata is written. -- The machine-local registry is updated. -- Same id and same path is an idempotent success. -- Same id and different path fails for now; a future `--replace` can make - replacement explicit. -- Same path already registered under a different id fails for now. -- Register does not create the folder, initialize Git, pull, push, commit, - create remotes, or clone. - -Suggested human output: - -```text -Context store registered - -ID: team-context -Location: /Users/me/src/team-context -Metadata: /Users/me/src/team-context/.openspec-store/store.yaml -Registry: /Users/me/.local/share/openspec/context-stores/registry.yaml -``` - -### `context-store list` - -`context-store list` is an index view of the local registry. - -Locked behavior: - -- Reads the local registry. -- Shows registered id and location only. -- Sorts by store id. -- Does not check metadata, path health, Git, sync, remote, dirty state, or - conflicts. -- Does not mutate anything. -- Prints no health warnings; health belongs to `context-store doctor`. - -Suggested human output: - -```text -OpenSpec context stores (2) - -ID Location -platform /Users/me/src/platform-context -team-context /Users/me/src/team-context -``` - -Empty output: - -```text -No context stores registered. - -Next: - openspec context-store setup team-context - openspec context-store register /path/to/context-store -``` - -### `context-store doctor` - -`context-store doctor [id]` is the non-mutating health and repair surface. - -Locked behavior: - -- Checks all registered stores by default. -- Checks one store when `id` is passed. -- Checks registry presence, path existence, directory shape, metadata presence, - metadata parsing, and metadata id matching. -- Includes a cheap Git repository presence check. -- Does not check dirty state, branch, remote, sync, pull/push, or conflicts in - this slice. -- Does not mutate anything. - -Empty output: - -```text -No context stores registered. -``` - -Suggested human output: - -```text -Context store doctor - -team-context - Location: /Users/me/src/team-context - Metadata: ok - Git: repository detected - Issues: none -``` - -### `initiative list` - -`initiative list` becomes the agent-friendly discovery command across -registered stores. - -Locked behavior: - -- Without `--store` or `--store-path`, list initiatives from all readable - registered stores. -- If no context stores are registered, print a concise empty message. -- Sort by store id, then initiative id. -- Do not show a `Status` column in human output. -- Do not print detailed health diagnostics. -- If some stores cannot be read, still show initiatives from readable stores - and print one small warning that points to `context-store doctor`. -- If all registered stores are unreadable, print a concise failure/empty message - and point to `context-store doctor`. -- With `--store <id>`, filter to one registered store. -- With `--store-path <path>`, list from that explicit store path. -- Filtered `--store` or `--store-path` mode fails directly if that store cannot - be read, because there are no fallback stores. - -Suggested all-store output: - -```text -OpenSpec initiatives (3 across 2 stores) - -ID Store Title -billing-launch platform Billing Launch -docs-refresh platform Docs Refresh -api-cleanup team API Cleanup - -Some registered context stores could not be read. -Run: openspec context-store doctor -``` - -No registered stores output: - -```text -No initiatives found because no context stores are registered. -``` - -Suggested filtered output: - -```text -OpenSpec initiatives in platform (2) - -ID Title -billing-launch Billing Launch -docs-refresh Docs Refresh - -Location: /Users/me/src/platform-context -``` - -## Boundaries - -Do not implement in this item: - -- initiative `show` -- repo-local change metadata -- `new change --initiative` -- initiative local resolution -- workspace initiative opening -- sync, pull, push, remote repository creation, or conflict handling - -## Remaining Decisions - -None before implementation. JSON shapes can follow the existing command pattern: -top-level result objects plus a `status` diagnostics array. Partial success -returns exit code 0 with warning diagnostics; total failure returns nonzero. - -## Implemented Slice - -- Added `openspec context-store setup/register/list/doctor`. -- Registered the `context-store` command from the top-level CLI. -- Initially kept shell completion metadata out of scope; static metadata was - added later with the shipped command surface. -- Implemented strict CLI registration policy without changing the permissive - lower-level registry facade. -- Added setup behavior for default `./<id>`, explicit `--path .`, interactive - Git init prompt, non-interactive/JSON no-prompt behavior, non-empty directory - rejection, and metadata writing. -- Added register behavior for existing folders, id inference from folder name, - metadata writing, id/path conflict rejection, and registry updates. -- Added list behavior as a registry index only. -- Added doctor behavior for registry/path/metadata health and cheap Git - presence. -- Updated `initiative list` so no selector lists across registered stores, - `--store` filters, `--store-path` remains an escape hatch, human output is - compact, and all-store partial success returns warning diagnostics. diff --git a/openspec/initiatives/context-store-and-initiatives/work-items/06-add-minimal-context-store-ux/tasks.md b/openspec/initiatives/context-store-and-initiatives/work-items/06-add-minimal-context-store-ux/tasks.md deleted file mode 100644 index e17b34dd7a..0000000000 --- a/openspec/initiatives/context-store-and-initiatives/work-items/06-add-minimal-context-store-ux/tasks.md +++ /dev/null @@ -1,29 +0,0 @@ -# Add Minimal Context Store UX Tasks - -- [x] Create Item 6 work-item tracking notes. -- [x] Capture agent-first setup and discovery direction. -- [x] Decide `context-store` is the first CLI namespace. -- [x] Decide setup defaults to `./<id>` when `--path` is omitted. -- [x] Decide current-directory setup requires explicit `--path .`. -- [x] Record that checked-in store metadata stays minimal. -- [x] Decide checked-in store metadata is exactly `.openspec-store/store.yaml` - and contains portable identity only. -- [x] Record that machine-local registry state stays outside the store. -- [x] Record that `initiative list` should default across registered stores. -- [x] Decide setup interactive and non-interactive behavior. -- [x] Decide register behavior. -- [x] Decide context-store list is registry index only. -- [x] Decide doctor owns health checks. -- [x] Decide initiative list partial-success behavior. -- [x] Decide JSON and exit behavior for partial success and total failure. -- [x] Decide id inference uses folder/repo name as-is with normal validation. -- [x] Decide setup rejects non-empty folders without context-store metadata. -- [x] Decide registry path/id conflicts fail for now. -- [x] Decide empty states for list, doctor, and initiative list. -- [x] Initially defer completion metadata; later add static metadata with the - rest of the shipped command surface. -- [x] Finalize exact JSON payload fields for setup, register, list, doctor, and - all-store initiative list. -- [x] Implement `context-store setup/register/list/doctor`. -- [x] Update `initiative list` all-store behavior and output. -- [x] Add focused tests and verification evidence. diff --git a/openspec/initiatives/context-store-and-initiatives/work-items/07-add-agent-first-initiative-discovery/evidence.md b/openspec/initiatives/context-store-and-initiatives/work-items/07-add-agent-first-initiative-discovery/evidence.md deleted file mode 100644 index a08d1fe17d..0000000000 --- a/openspec/initiatives/context-store-and-initiatives/work-items/07-add-agent-first-initiative-discovery/evidence.md +++ /dev/null @@ -1,97 +0,0 @@ -# Add Agent-First Initiative Discovery Evidence - -## Conversation Decisions - -- `initiative show <id>` should be a locator/discovery command for agents. -- The command should answer which initiative the user meant, where the - canonical context lives, and where the initiative metadata is. -- The command should not concatenate markdown, summarize initiative contents, - compute work progress, resolve local repos, list linked changes, or open a - workspace. -- Default lookup should search all registered context stores. -- `--store <id>` should disambiguate or filter to one registered store. -- `--store-path <path>` should remain the explicit local-path escape hatch. -- Duplicate initiative ids across stores should fail with an ambiguity error. -- Default all-store lookup should fail when any registered store is unreadable, - because uniqueness is unknowable. -- Explicit `--store` and `--store-path` lookup should only care about the - selected store. -- `initiative.status` should be omitted from the v1 output projection. -- `owners` should be omitted from the v1 output projection. -- Arbitrary `metadata` should be omitted from the v1 output projection. -- `version` and `created` should stay in the v1 initiative projection. -- `files` should be omitted from v1. -- `initiative.metadata_path` should point to the validated `initiative.yaml`. -- `initiative.root` is enough for an agent to inspect the folder with normal - filesystem tools. -- Top-level `matches` should be omitted. Ambiguity and incomplete-lookup - candidates should live under the diagnostic that needs them, for example - `status[0].details.matches`. -- `context_store.source` should be omitted from `initiative show` v1 because it - is selector provenance, not context-store identity. -- A top-level `resolution` field is not needed in v1. -- Existing `initiative create/list` output can keep `context_store.source` for - now; this item should not refactor old output shapes. -- `readInitiative` should return `null` when the exact initiative is absent and - throw when `initiative.yaml` exists but is invalid or has the wrong id. -- In default all-store lookup, any unreadable registered store should make the - primary error `initiative_lookup_incomplete`, even when readable stores have - partial matches. -- If `initiatives/<id>/initiative.yaml` exists but is invalid or has the wrong - id, `initiative show` should fail as broken initiative state instead of - treating that store as not found. -- Human output should be a compact locator view on success: title, id, summary, - context store, location, and canonical filenames. -- Human ambiguity and incomplete-lookup errors should show matching or partial - matching stores inline, then point to the next command. -- Static shell completion metadata should ship for `initiative show`. -- Dynamic completions for store ids and initiative ids should remain deferred. - -## Research Notes - -- Current initiative create/list output spreads the full parsed - `initiative.yaml` state, which is useful for MVP but too broad for the first - `show` contract. -- A focused per-initiative read operation is preferred over implementing `show` - through `listInitiatives`, because exact lookup should not fail due to an - unrelated malformed initiative folder. -- Other initiative files are schema/config dependent and should not be - hardcoded into `show`. -- Keeping candidates inside diagnostic details follows the same general shape as - GraphQL-style responses: successful data stays clean, while error-specific - context travels with the error. -- If selector provenance is needed later, add a separate explicit field such as - `resolution` rather than putting provenance inside `context_store`. -- Human output should stay compact: title, id, summary, context store, - location, and metadata path. - -## Implementation Evidence - -- `src/core/collections/initiatives/operations.ts` adds `readInitiative` for - exact initiative lookup. -- `src/commands/initiative.ts` adds `initiative show <id>` with all-store - default lookup, `--store`, `--store-path`, JSON output, compact human output, - ambiguity diagnostics, and incomplete-lookup diagnostics. -- `src/core/completions/command-registry.ts` adds static completion metadata for - `initiative show`. -- `test/core/collections/initiatives/operations.test.ts` covers exact read, - absent initiatives, invalid exact initiatives, id mismatches, and unrelated - invalid folders. -- `test/commands/initiative.test.ts` covers `initiative show` success, - `--store-path`, human output, ambiguity, incomplete lookup, not found, - invalid exact initiative state, no `context_store.source`, no `files`, no - top-level `matches`, and static completions. - -## Verification - -- `pnpm run build` -- `pnpm exec vitest run test/core/collections/initiatives/operations.test.ts` -- `pnpm exec vitest run test/commands/initiative.test.ts` -- `pnpm exec vitest run test/commands/context-store.test.ts - test/commands/initiative.test.ts test/core/context-store/foundation.test.ts - test/core/context-store/registry.test.ts - test/core/collections/initiatives/operations.test.ts` -- `pnpm run lint` -- `git diff --check` -- Markdown line-length check for the initiative roadmap, task tracker, and Item - 7 work-item notes. diff --git a/openspec/initiatives/context-store-and-initiatives/work-items/07-add-agent-first-initiative-discovery/plan.md b/openspec/initiatives/context-store-and-initiatives/work-items/07-add-agent-first-initiative-discovery/plan.md deleted file mode 100644 index d59c23bc88..0000000000 --- a/openspec/initiatives/context-store-and-initiatives/work-items/07-add-agent-first-initiative-discovery/plan.md +++ /dev/null @@ -1,184 +0,0 @@ -# Add Agent-First Initiative Discovery - -## Status - -Implementation complete; verification in progress. - -## Source Of Truth - -Start from `../../direction.md`. - -This item exists because the expected workflow is agent-first: - -```text -Using initiative billing-launch, explore the API work and create a proposal. -``` - -Before repo-local linking, local resolution, or workspace opening can work, the -agent needs a small command that answers: - -- Which initiative did the user mean? -- Which context store contains the canonical initiative? -- Where is the initiative metadata, and what root should the agent inspect? - -## Goal - -Add agent-first initiative discovery without turning `show` into a reader, -progress dashboard, repo resolver, or workspace launcher. - -## Locked Direction So Far - -- `initiative show <id>` is a locator/discovery command. -- It should return identity, context-store location, initiative location, and - the initiative metadata path. -- It should not concatenate markdown, summarize file contents, compute progress, - resolve repos, list linked changes, or open workspaces. -- Default lookup searches all locally registered context stores. -- `--store <id>` filters to one registered store. -- `--store-path <path>` remains the explicit local-path escape hatch. -- Duplicate initiative ids across stores are ambiguous. The command should not - auto-pick a match. -- In default all-store lookup, unreadable stores make the lookup incomplete. - The command should fail rather than silently returning a possibly false - unique match. -- Explicit `--store` and `--store-path` modes only consider the selected store. - -## Output Contract Direction - -The first JSON contract should be a resolver/read-pointer projection, not a -full serialization of `initiative.yaml`. - -Suggested success shape: - -```json -{ - "context_store": { - "id": "platform", - "root": "/path/to/platform-context" - }, - "initiative": { - "version": 1, - "id": "billing-launch", - "title": "Billing Launch", - "summary": "Coordinate billing launch work.", - "created": "2026-05-21", - "root": "/path/to/platform-context/initiatives/billing-launch", - "store_path": "initiatives/billing-launch", - "metadata_path": "/path/to/platform-context/initiatives/billing-launch/initiative.yaml" - }, - "status": [] -} -``` - -Locked field decisions: - -- Keep `initiative.version`. -- Keep `initiative.created`. -- Keep `initiative.id`, `title`, `summary`, `root`, `store_path`, and - `metadata_path`. -- Keep `context_store.id` and `root`. -- Omit `context_store.source` from `initiative show` v1. It is selector - provenance, not context-store identity. Existing create/list output can remain - unchanged for now. -- Omit a top-level `resolution` field from v1. -- Omit `initiative.status` from the v1 projection. -- Omit `initiative.owners` from the v1 projection. -- Omit arbitrary `initiative.metadata` from the v1 projection. -- Omit a `files` list from the v1 projection. -- Omit top-level `matches`. -- Put ambiguity and incomplete-lookup candidates under the relevant diagnostic - entry, such as `status[0].details.matches`. -- Keep top-level `status` as command diagnostics only, not initiative work - progress. - -## Still To Decide - -- Nothing for the minimal v1 slice. - -## Human Output Direction - -Success output should stay locator-focused: - -```text -OpenSpec initiative: Billing Launch - -ID: billing-launch -Summary: Coordinate billing launch work. -Context store: platform -Location: /path/to/platform-context/initiatives/billing-launch - -Files: - Metadata: /path/to/platform-context/initiatives/billing-launch/initiative.yaml -``` - -Error output should stay plain: - -- Not found: say the initiative was not found in registered context stores and - suggest `openspec initiative list`. -- Ambiguous: show matching stores and paths, then suggest - `openspec initiative show <id> --store <store>`. -- Incomplete lookup: say some context stores could not be read, include partial - matches when present, then suggest `openspec context-store doctor`. - -## File Listing Direction - -`initiative show` should not list initiative folder contents in v1. - -Only `initiative.yaml` is required to identify and validate the initiative. All -other files are schema/config dependent and may differ across teams. Once the -command has resolved `initiative.root`, agents can use normal filesystem tools -to inspect the folder. Later schema-aware views can expose important files -without hardcoding today's default template filenames. - -## Completion Direction - -Add static shell completion metadata for: - -```text -initiative show <id> --store <id> --store-path <path> --json -``` - -Do not add dynamic completions for registered store ids or initiative ids in -this slice. - -## Core Read Operation Direction - -Add a focused `readInitiative` operation for exact lookup. - -Behavior: - -- Return `null` when the initiative folder or `initiative.yaml` is absent. -- Throw when `initiative.yaml` exists but is invalid. -- Throw when the parsed `initiative.yaml` id does not match the folder id. -- Do not scan unrelated initiative folders. - -## Lookup Error Precedence - -For default all-store lookup, any unreadable registered store makes lookup -incomplete. - -If one or more readable stores contain the initiative and one or more other -stores cannot be read, the primary error should still be -`initiative_lookup_incomplete`, not success or ambiguity. Include any readable -partial matches under the diagnostic details. - -Explicit `--store` and `--store-path` modes are scoped to the selected store and -do not check unrelated registered stores. - -Invalid exact initiative folders are broken shared state, not "not found". - -If `initiatives/<id>/initiative.yaml` exists but is invalid or has a mismatched -id, `initiative show` should fail with an invalid-initiative diagnostic. In -default all-store lookup, unreadable stores still take precedence as -`initiative_lookup_incomplete` because the full candidate set is unknowable. - -## Explicitly Out Of Scope - -- Top-level `openspec show` integration. -- Markdown content bundles or generated context packs. -- Checked-in initiative snapshots in repo-local changes. -- Repo-local change linking. -- Local repo/workspace resolution. -- Workspace opening. -- Git sync status, dirty state, remotes, pull, push, or conflicts. -- Initiative progress or status dashboards. diff --git a/openspec/initiatives/context-store-and-initiatives/work-items/07-add-agent-first-initiative-discovery/tasks.md b/openspec/initiatives/context-store-and-initiatives/work-items/07-add-agent-first-initiative-discovery/tasks.md deleted file mode 100644 index 2bf8440d78..0000000000 --- a/openspec/initiatives/context-store-and-initiatives/work-items/07-add-agent-first-initiative-discovery/tasks.md +++ /dev/null @@ -1,27 +0,0 @@ -# Add Agent-First Initiative Discovery Tasks - -- [x] Create Item 7 work-item tracking notes. -- [x] Decide `initiative show <id>` is a locator/discovery command. -- [x] Decide default lookup searches all registered context stores. -- [x] Decide `--store` and `--store-path` remain the narrowing selectors. -- [x] Decide duplicate initiative ids are ambiguity errors. -- [x] Decide unreadable stores make default all-store lookup incomplete. -- [x] Decide the v1 projection omits `initiative.status`, `owners`, and - arbitrary `metadata`. -- [x] Decide the v1 projection keeps `initiative.version` and `created`. -- [x] Decide v1 omits `files` and only returns initiative root plus metadata - path. -- [x] Decide ambiguity and incomplete-lookup candidates live under diagnostic - details, not top-level `matches`. -- [x] Decide exact human output direction for success and error states. -- [x] Decide `initiative show` omits `context_store.source`. -- [x] Decide `initiative show` omits a top-level `resolution` field. -- [x] Decide static completion metadata ships with Item 7. -- [x] Decide `readInitiative` returns `null` for absent and throws for invalid. -- [x] Decide incomplete lookup takes precedence over success or ambiguity in - default all-store mode. -- [x] Decide invalid exact initiative folders are errors, not not-found. -- [x] Implement a focused per-initiative read operation. -- [x] Implement `initiative show`. -- [x] Register static completion metadata for `initiative show`. -- [x] Add focused tests and verification evidence. diff --git a/openspec/initiatives/context-store-and-initiatives/work-items/08-connect-repo-local-changes-to-initiatives/evidence.md b/openspec/initiatives/context-store-and-initiatives/work-items/08-connect-repo-local-changes-to-initiatives/evidence.md deleted file mode 100644 index 917c121652..0000000000 --- a/openspec/initiatives/context-store-and-initiatives/work-items/08-connect-repo-local-changes-to-initiatives/evidence.md +++ /dev/null @@ -1,239 +0,0 @@ -# Connect Repo-Local Changes To Initiatives Evidence - -## Decision 1: Initiative Link Location - -The initiative link should live in the repo-local change `.openspec.yaml`. - -Example: - -```yaml -schema: spec-driven -created: 2026-05-22 -initiative: - store: platform - id: billing-launch -``` - -This keeps repo implementation ownership in the repo while preserving a durable -reference to canonical initiative context. - -The link should not include local paths, copied initiative prose, or backlinks -inside the initiative store. - -## Research Notes - -- `createChange()` already writes `.openspec.yaml` for every change. -- `ChangeMetadataSchema` currently allows schema, created, goal, and - affected-area fields. Item 8 can extend that schema with `initiative`. -- Archive moves the whole change directory, so the initiative link will move - with archived changes. -- Apply, validate, and archive should not require context-store availability in - this slice. - -## Decision 2: Create Command Shape - -Initiative-linked creation should use `openspec new change` with `--initiative`. - -Supported first-slice forms: - -```bash -openspec new change add-billing-api --initiative billing-launch --json -openspec new change add-billing-api --initiative platform/billing-launch --json -openspec new change add-billing-api --initiative billing-launch --store platform --json -``` - -This keeps the operation repo-owned. The initiative is a reference on the -change, not the actor that creates or owns the change. - -The first slice should also add `--json` to `new change` so agents can capture -the created change path, metadata path, and initiative reference. - -## Decision 3: Initiative Lookup Behavior - -Bare `--initiative <id>` should reuse `initiative show` lookup semantics. - -It searches all registered context stores and succeeds only when the lookup is -complete and exactly one readable store contains the initiative. - -Explicit store selectors narrow lookup: - -```bash -openspec new change add-billing-api --initiative platform/billing-launch -openspec new change add-billing-api --initiative billing-launch --store platform -openspec new change add-billing-api --initiative billing-launch --store-path ./context -``` - -`--store-path` validates the explicit path and reads its store id, but does not -auto-register the store. Metadata still stores only the portable store id and -initiative id. - -Repo-local metadata should not be written until initiative lookup is complete -and unambiguous. - -## Decision 4: Repo-Local Only For V1 - -Item 8 should support initiative links only on repo-local changes. - -If `openspec new change <id> --initiative ...` runs from a workspace planning -home, v1 should refuse and tell the user to run the command from the repo that -owns the implementation plan. - -Existing workspace-planning changes remain compatibility behavior and should not -gain initiative linkage in this slice. - -This preserves the boundary that initiatives coordinate shared context, -repo-local changes own implementation plans, and workspaces open local views. - -## Decision 5: No Repo Ownership Matching In V1 - -Item 8 should not verify that the current repo is named by, owned by, or inferred -from the initiative. - -Creating a repo-local change with an initiative link records participation in the -initiative. It does not prove ownership, repo impact, or coverage of an -initiative area. - -Repo ownership matching can be revisited after initiative resolution or explicit -initiative metadata has a real repo/area model. - -## Decision 6: JSON And Human Output - -Create output should stay factual and minimal. - -Human output should confirm: - -- the created change id and location -- the schema -- the initiative link `{ store, id }` - -JSON output should include: - -```json -{ - "change": { - "id": "add-billing-api", - "path": "/repo/openspec/changes/add-billing-api", - "metadataPath": "/repo/openspec/changes/add-billing-api/.openspec.yaml", - "schema": "spec-driven" - }, - "initiative": { - "store": "platform", - "id": "billing-launch" - } -} -``` - -The output should not include `next` or other suggested workflow actions. API -responses should report operation results or errors; choosing the next action is -the agent's responsibility and depends on broader context. - -## Decision 7: Existing Change Recovery - -Item 8 should include a friendly recovery command for existing repo-local -changes: - -```bash -openspec set change add-billing-api --initiative billing-launch --json -openspec set change add-billing-api --initiative platform/billing-launch --json -openspec set change add-billing-api --initiative billing-launch --store platform --json -openspec set change add-billing-api --initiative billing-launch --store-path ../context --json -``` - -This command is a validated setter for checked-in repo-local change metadata. In -Item 8, the only supported settable field is the initiative link, and the only -file it may mutate is `openspec/changes/<id>/.openspec.yaml`. - -The command should not edit proposal, design, tasks, specs, or initiative-store -files. It should not store local paths or write backlinks into the initiative. - -If the requested initiative link already exists, the command should succeed as -an idempotent no-op. If a different initiative link already exists, the command -should fail without writing. Replacement, relink, unlink, and dry-run behavior -are deferred. - -Rationale: - -- Agents can forget to link a change during creation, so a first-class recovery - path is useful. -- `set change` matches the actual side effect: writing validated change metadata - to `.openspec.yaml`. -- Keeping the command scoped to `.openspec.yaml` avoids creating a broad change - editing surface. -- `openspec change ...` is currently deprecated, `edit` implies opening an - editor, and `update` already means refreshing local OpenSpec tooling or - guidance. - -## Decision 8: Status And Instructions Visibility - -Status and instructions should surface that the repo-local change is linked to -an initiative, but should not display or resolve the initiative itself. - -Human status output should show the stored initiative reference, and JSON status -output should include the stored initiative `{ store, id }`. Instructions output -should include a concise factual note that the change is linked to the -initiative. - -Status and instructions should not read, summarize, validate, or resolve the -initiative from the context store in v1. Missing or unavailable context stores -should not make repo-local status or instructions fail. - -This keeps the relationship visible during ordinary repo-local workflows while -preserving the boundary that initiative lookup and context reading belong to -initiative-specific commands. - -## Latest Open-Decision Notes - -Date: 2026-05-23. - -All decisions for Item 8 are now confirmed for implementation. - -Implementation should keep the first slice small: - -- The light release should test whether initiative-linked repo-local changes are - useful before adding gating, ownership inference, or broader workflow - integration. -- Standalone `initiative resolve` was later rejected; workspace local-view state - owns local path mapping. -- Source provenance, history/export, contract maps, and target-bound - initiative-hosted changes remain useful future discussion points, but should - not block this initial slice. - -## Implementation Evidence - -Date: 2026-05-23. - -Implemented: - -- `openspec new change <id> --initiative ...` for repo-local changes, with - `--json`, `--store`, and `--store-path` support. -- `openspec set change <id> --initiative ...` for existing repo-local changes. -- Portable checked-in metadata under `initiative: { store, id }`. -- Status and instructions visibility from stored metadata only. -- Workspace refusal, lookup-failure no-write behavior, same-link idempotency, - and different-link conflict protection. - -Verification: - -```bash -pnpm run build -``` - -Result: passed. - -```bash -pnpm exec eslint src/commands/workflow/new-change.ts src/commands/workflow/set-change.ts src/commands/workflow/initiative-link.ts src/commands/workflow/instructions.ts src/commands/workflow/status.ts src/commands/workflow/shared.ts src/commands/initiative.ts src/core/artifact-graph/types.ts src/core/artifact-graph/instruction-loader.ts src/utils/change-utils.ts src/cli/index.ts -``` - -Result: passed. - -```bash -pnpm exec vitest run test/utils/change-metadata.test.ts test/commands/change-initiative-link.test.ts -``` - -Result: passed, 39 tests. - -```bash -pnpm exec vitest run test/commands/artifact-workflow.test.ts test/commands/initiative.test.ts test/core/artifact-graph/instruction-loader.test.ts -``` - -Result: passed, 110 tests. diff --git a/openspec/initiatives/context-store-and-initiatives/work-items/08-connect-repo-local-changes-to-initiatives/plan.md b/openspec/initiatives/context-store-and-initiatives/work-items/08-connect-repo-local-changes-to-initiatives/plan.md deleted file mode 100644 index 6019549f95..0000000000 --- a/openspec/initiatives/context-store-and-initiatives/work-items/08-connect-repo-local-changes-to-initiatives/plan.md +++ /dev/null @@ -1,279 +0,0 @@ -# Connect Repo-Local Changes To Initiatives - -## Status - -Implemented. The original decision text below is preserved as design record; -current completion evidence lives in `tasks.md` and `evidence.md`. - -## Source Of Truth - -Start from `../../direction.md` and the Item 8 roadmap entry. - -The relevant boundary is: - -```text -Initiatives coordinate shared context. -Repo-local changes own implementation plans. -Workspaces open local views. -``` - -## Goal - -Let an agent create or link a repo-local OpenSpec change to a shared -initiative without copying initiative prose, storing machine-local paths, or -making the initiative own repo implementation artifacts. - -Example user prompt: - -```text -Using initiative billing-launch, create a proposal for API work. -``` - -## Decisions - -### 1. Initiative Link Location - -Decision: Store the initiative link in the repo-local change `.openspec.yaml`. - -Suggested metadata shape: - -```yaml -schema: spec-driven -created: 2026-05-22 -initiative: - store: platform - id: billing-launch -``` - -Rules: - -- Store only the context store id and initiative id. -- Do not store local context-store paths. -- Do not store local repo paths. -- Do not create a checked-in `initiative.md` snapshot by default. -- Do not write backlinks into the initiative. - -Rationale: - -- `.openspec.yaml` is already the per-change machine-readable metadata file. -- The link is durable repo context and should be checked in with the change. -- The canonical initiative context remains in the context store. -- The metadata stays portable across teammates and machines. - -### 2. Create Command Shape - -Decision: Add initiative linking to the repo-local change creation command with -`--initiative`. - -Supported first-slice forms: - -```bash -openspec new change add-billing-api --initiative billing-launch --json -openspec new change add-billing-api --initiative platform/billing-launch --json -openspec new change add-billing-api --initiative billing-launch --store platform --json -``` - -Rules: - -- The command starts from `new change` because the change is repo-owned. -- `--initiative` modifies repo-local change creation; it does not make the - initiative create or own the change. -- `--json` should be added to `new change` for agent-readable handoff output. -- A separate initiative-owned create command is not part of the first slice. - -Rationale: - -- The expected user flow is agent-first: "using initiative X, create a proposal - for repo work." -- Agents need one normal repo-local create command that can also write the - initiative reference. -- Keeping the verb rooted in `new change` preserves the boundary that changes - implement repo-owned slices. - -### 3. Initiative Lookup Behavior - -Decision: Reuse `initiative show` lookup semantics for `--initiative`. - -Rules: - -- Bare `--initiative <id>` searches all registered context stores. -- Bare lookup succeeds only when exactly one readable registered store contains - the initiative id. -- Duplicate initiative ids across stores fail as ambiguous. -- Any unreadable registered store makes bare lookup incomplete and fails before - writing change metadata. -- `--initiative <store>/<id>` selects one registered store by id. -- `--initiative <id> --store <store>` also selects one registered store by id. -- `--initiative <id> --store-path <path>` validates the explicit local context - store path, reads its store id, and writes only `{ store, id }` to metadata. -- `--store-path` does not auto-register the context store. -- Do not write repo-local initiative metadata until lookup is complete and - unambiguous. - -Rationale: - -- Agents can use the short form when it is safe. -- Durable repo-local links should not be created from partial knowledge. -- The behavior matches existing agent-first discovery semantics. - -### 4. Repo-Local Only For V1 - -Decision: Item 8 supports initiative links only on repo-local changes. - -Rules: - -- `openspec new change <id> --initiative ...` creates an initiative-linked - change only when the current planning home is repo-local. -- If the command runs from a workspace planning home, v1 refuses with clear - guidance to run the command from the repo that owns the implementation plan. -- Existing workspace-planning changes remain compatibility behavior and are not - extended with initiative linkage in this slice. - -Rationale: - -- The current product boundary assigns implementation plans to repo-local - OpenSpec changes. -- Workspaces are local views, not the durable planning owner for initiative - work. -- Extending workspace-planning changes would revive the superseded - workspace-owns-the-plan model. - -### 5. Repo Ownership Matching - -Decision: Do not attempt repo ownership matching in v1. - -Rules: - -- Creating a repo-local change with an initiative link records participation in - the initiative. -- The link does not claim that OpenSpec verified repo ownership, repo impact, or - initiative area coverage. -- The command should not block or warn solely because the current repo is absent - from initiative content. - -Rationale: - -- Item 8 should not invent repo ownership or monorepo area semantics. -- Ownership matching belongs with later initiative resolution or explicit - initiative metadata. -- Keeping v1 small lets teams test whether linked repo-local changes are useful - before adding policy gates. - -### 6. JSON And Human Output - -Decision: Keep create output factual and minimal. - -Rules: - -- Output should report what the command did, not recommend workflow next steps. -- Human output should confirm the created change location, schema, and initiative - link. -- JSON output should include stable fields for the created change and initiative - link. -- JSON output should not include a `next` command or suggested workflow action. -- Output should not include initiative summaries, repo ownership claims, - resolved local context-store paths, or progress/status-like fields. - -Suggested JSON shape: - -```json -{ - "change": { - "id": "add-billing-api", - "path": "/repo/openspec/changes/add-billing-api", - "metadataPath": "/repo/openspec/changes/add-billing-api/.openspec.yaml", - "schema": "spec-driven" - }, - "initiative": { - "store": "platform", - "id": "billing-launch" - } -} -``` - -Rationale: - -- CLI/API-style responses should state operation results or errors. -- Accurately choosing the next action depends on agent context and should remain - the agent's responsibility. -- Keeping output factual avoids coupling change creation to later lifecycle - design. - -### 7. Existing Change Recovery - -Decision: Include a recovery command for setting the initiative link on an -existing repo-local change. - -Command shape: - -```bash -openspec set change add-billing-api --initiative billing-launch --json -openspec set change add-billing-api --initiative platform/billing-launch --json -openspec set change add-billing-api --initiative billing-launch --store platform --json -openspec set change add-billing-api --initiative billing-launch --store-path ../context --json -``` - -Rules: - -- `openspec set change <id> --initiative ...` is a validated setter for - repo-local change metadata. -- In Item 8, the only supported settable field is the initiative link. -- The command only mutates `openspec/changes/<id>/.openspec.yaml`. -- The command does not edit proposal, design, tasks, specs, or initiative-store - files. -- The command uses the same initiative lookup semantics as - `openspec new change <id> --initiative ...`. -- If the same initiative link already exists, the command succeeds as an - idempotent no-op. -- If a different initiative link already exists, the command fails without - writing. Replacement, relink, unlink, and dry-run behavior are not part of v1. -- If the command runs from a workspace planning home, it refuses for the same - reason as initiative-linked `new change`. - -Rationale: - -- Agents can forget to pass `--initiative` during change creation; v1 needs a - friendly recovery path. -- `set change` describes the real operation: setting checked-in change metadata, - not creating an initiative-owned relationship. -- Keeping the command limited to `.openspec.yaml` avoids a broad edit surface. -- Avoid `openspec change ...` because that namespace is currently deprecated. -- Avoid `edit` because it implies opening an editor, and avoid `update` because - OpenSpec already uses update for local guidance/tool refresh. - -### 8. Status And Instructions Visibility - -Decision: Surface the initiative link in status and instructions output without -resolving or displaying the initiative itself. - -Rules: - -- Human status output should show that the change is linked to an initiative. -- JSON status output should include the stored initiative `{ store, id }`. -- Instructions output should include a concise factual note that the change is - linked to the initiative. -- Status and instructions must not read, summarize, validate, or resolve the - initiative from the context store in v1. -- Missing or unavailable context stores must not make repo-local status or - instructions fail. -- Output should not add next-step recommendations. - -Rationale: - -- The initiative link should be visible in normal repo-local workflow output so - users and agents do not miss the relationship. -- Keeping visibility to stored metadata avoids introducing context-store - availability as a dependency for repo-local workflow commands. -- Initiative resolution belongs to initiative-specific commands, not status or - instructions in this slice. - -## Open Decisions - -None. Decision pass complete; confirm the decisions before implementation. - -## Latest Suggested Resolutions - -These were the suggested answers carried into implementation: - -- Surface the stored initiative link in status and instructions without reading - or displaying the initiative itself. diff --git a/openspec/initiatives/context-store-and-initiatives/work-items/08-connect-repo-local-changes-to-initiatives/tasks.md b/openspec/initiatives/context-store-and-initiatives/work-items/08-connect-repo-local-changes-to-initiatives/tasks.md deleted file mode 100644 index 926e34ebbd..0000000000 --- a/openspec/initiatives/context-store-and-initiatives/work-items/08-connect-repo-local-changes-to-initiatives/tasks.md +++ /dev/null @@ -1,22 +0,0 @@ -# Connect Repo-Local Changes To Initiatives Tasks - -## Decisions - -- [x] Decide where the initiative link lives. -- [x] Decide command shape for creating initiative-linked changes. -- [x] Decide initiative lookup behavior for `--initiative`. -- [x] Decide whether workspace-scoped changes are allowed in this slice. -- [x] Decide whether repo ownership matching is attempted in v1. -- [x] Decide JSON and human output shape. -- [x] Decide whether Item 8 includes linking existing changes. -- [x] Decide whether status/instructions surface initiative links. -- [x] Confirm latest suggested resolutions in `plan.md` before implementation. - -## Implementation - -- [x] Extend change metadata schema with an optional initiative link. -- [x] Persist initiative metadata when creating repo-local changes. -- [x] Add command support for creating initiative-linked changes. -- [x] Add tests for metadata validation and persistence. -- [x] Add tests for command output and lookup failures. -- [x] Add status/instruction visibility for stored initiative links. diff --git a/openspec/initiatives/context-store-and-initiatives/work-items/09-add-initiative-resolve/decision-review.md b/openspec/initiatives/context-store-and-initiatives/work-items/09-add-initiative-resolve/decision-review.md deleted file mode 100644 index a7bf4c51a4..0000000000 --- a/openspec/initiatives/context-store-and-initiatives/work-items/09-add-initiative-resolve/decision-review.md +++ /dev/null @@ -1,64 +0,0 @@ -# Item 9 Decision: Reject Initiative Resolve - -## Final Decision - -Do not implement a standalone `openspec initiative resolve <id>` command, now -or later. - -The command is unnecessary because it tries to do work that already belongs to -other concepts: - -- `initiative show` finds the canonical initiative. -- A workspace is the local view over repos and folders. -- Repo-local changes link themselves to initiatives. -- Repo-local status reports work progress. - -## Decision 1: No Command - -No separate initiative command is needed. - -If the user only has a context store, `initiative show` is enough. If the user -has a workspace, the local view is already represented by that workspace. If the -user is inside a repo, repo-local commands are enough. - -## Decision 2: Local Resolution Belongs To Workspace - -A workspace maps local repos and folders to paths on one machine. Future -initiative-aware local opening belongs in workspace behavior. - -## Decision 3: Agent Behavior - -Agents should: - -- Use `openspec initiative show <id> --json` for shared context. -- Use the current workspace view when the user is working in a workspace. -- Use repo-local commands when the user is working in a repo. -- Let the user decide which repos are present locally. - -## Decision 4: Rejected Scope - -Remove all standalone resolve behavior: - -- no `initiative resolve` -- no all-repo scan -- no all-workspace scan -- no `--path` search roots -- no Git remote matching -- no cloning -- no worktree or branch creation -- no initiative backlinks -- no local availability dashboard - -## Decision 5: Roadmap Update - -Convert Item 9 into a decision-only checkpoint. - -Replacement: - -```text -Item 9. Reject Initiative Resolve - -Decision: do not add `openspec initiative resolve`, now or later. Initiative -discovery belongs to `initiative show`; local path mapping belongs to -workspaces; implementation progress belongs to repo-local changes. -``` diff --git a/openspec/initiatives/context-store-and-initiatives/work-items/09-add-initiative-resolve/evidence.md b/openspec/initiatives/context-store-and-initiatives/work-items/09-add-initiative-resolve/evidence.md deleted file mode 100644 index 26ce8ca83d..0000000000 --- a/openspec/initiatives/context-store-and-initiatives/work-items/09-add-initiative-resolve/evidence.md +++ /dev/null @@ -1,106 +0,0 @@ -# Reject Initiative Resolve Evidence - -## Decision Summary - -Date: 2026-05-25. - -After review, the standalone `openspec initiative resolve <id>` command should -not be implemented, now or later. - -The useful distinction is already covered by existing concepts: - -- `initiative show` resolves canonical shared initiative context. -- A workspace is the local view over repos and folders. -- Repo-local changes link themselves to initiatives through checked-in metadata. -- Repo-local status reports implementation progress. - -A standalone resolve command would mostly duplicate workspace local-view state -or provide weak output when no workspace is present. - -## Pressure Test - -Scenario: - -```bash -git clone git@github.com:acme/context.git -openspec context-store register ./context --id platform -openspec initiative show billing-launch --json -``` - -This can locate: - -```text -platform/billing-launch -./context/initiatives/billing-launch -./context/initiatives/billing-launch/initiative.yaml -``` - -It cannot know: - -```text -which implementation repos should exist locally -where those repos are on this machine -which repos the user intends to work in -which repos should be cloned -which workspace view the user wants -``` - -That knowledge belongs to the user and the workspace, not the initiative. - -## Why Workspace Changes The Answer - -When a user has a workspace, the local view is already resolved by the -workspace: - -```text -workspace -> link names -> machine-local paths -``` - -The agent can operate from the workspace context. A separate -`initiative resolve` command would add another layer that mostly reprints what -the workspace already owns. - -If future UX needs initiative-aware opening, it should be part of workspace -behavior, such as opening or preparing a workspace around a selected initiative. -It should not be a standalone initiative command pretending to infer local repo -availability. - -## Research Notes Retained - -The earlier investigation is still useful as background: - -- `initiative show` already has correct context-store lookup behavior, - ambiguity handling, incomplete lookup handling, and JSON locator output. -- Item 8 stores initiative links in repo-local `.openspec.yaml` as - `{ store, id }`. -- Workspace state owns local path mappings and generated open surfaces. -- Existing repo-local status and instructions expose initiative links but do not - resolve or summarize the initiative. - -Those findings support the final decision: do not add a standalone command; keep -each responsibility in its existing owner. - -## Rejected Scope - -Rejected for Item 9: - -- `openspec initiative resolve <id>` -- path-resolution dashboards -- progress dashboards -- all-workspace scans -- all-repo scans -- explicit path scanning as an initiative command -- Git remote matching -- repo ownership inference -- cloning or branch/worktree orchestration -- initiative backlinks - -## Verification - -This pass updates decision artifacts only. - -```bash -git diff --check -``` - -Result: passed after this revision. diff --git a/openspec/initiatives/context-store-and-initiatives/work-items/09-add-initiative-resolve/plan.md b/openspec/initiatives/context-store-and-initiatives/work-items/09-add-initiative-resolve/plan.md deleted file mode 100644 index 6a5a482f47..0000000000 --- a/openspec/initiatives/context-store-and-initiatives/work-items/09-add-initiative-resolve/plan.md +++ /dev/null @@ -1,141 +0,0 @@ -# Reject Initiative Resolve - -## Status - -Final decision: do not implement a standalone `openspec initiative resolve` -command, now or later. - -## Source Of Truth - -Start from `../../direction.md` and the boundary: - -```text -Context stores sync truth. -Collections shape truth. -Initiatives coordinate work. -Workspaces open local views. -Changes implement repo-owned slices. -``` - -Item 8 already established that repo-local changes may reference initiatives -through portable checked-in metadata: - -```yaml -initiative: - store: platform - id: billing-launch -``` - -## Final Decision - -Do not ship `openspec initiative resolve <id>` as a user-facing command in this -slice or any future slice. - -The earlier command framing was too broad. It tried to join initiative identity, -workspace local paths, explicit repo roots, and linked repo-local changes into a -new CLI surface. That makes the command look authoritative even though the -initiative does not own local repo paths, repo participation, or implementation -state. - -## Why The Command Is Not Needed - -If a user only has a context store clone, OpenSpec can already resolve the -canonical initiative with: - -```bash -openspec initiative show billing-launch --json -``` - -That answers: - -```text -What initiative is this, which context store contains it, and where is the -canonical initiative folder? -``` - -It cannot answer: - -```text -Which local implementation repos should exist on this machine? -``` - -because that information is not in the context store. - -If a user has a workspace, the workspace is already the local view. It already -maps local repos and folders to paths on this machine. A separate -`initiative resolve` command would mostly re-describe the workspace the user is -already using. - -If a user is in a repo, the repo-local change commands and status commands -already operate from that repo. The user or agent can inspect the current repo's -changes directly. - -## Product Rule - -Do not create a new command whose main job is to discover local paths that the -workspace already represents. - -Rules: - -- `initiative show` remains the command for canonical initiative discovery. -- Workspaces remain the local view over repos, folders, context stores, and - initiatives. -- Repo-local changes remain the implementation artifacts. -- Agents should use the current workspace or current repo context rather than - asking a standalone initiative command to infer local availability. -- OpenSpec should not infer repo ownership, scan arbitrary repos, clone repos, - create worktrees, or write backlinks to make resolve appear smarter than it - is. - -## What To Do Instead - -Keep the pieces separate: - -- Use `openspec initiative show <id> --json` to locate canonical shared context. -- Use workspace commands to set up, link, relink, list, open, update, and doctor - local views. -- Use repo-local `openspec new change ... --initiative ...` and - `openspec set change ... --initiative ...` to create durable links from repo - work to initiative context. -- Use `openspec status --change <id> --json` inside the owning repo to inspect - implementation progress. - -If a future workspace workflow needs to open an initiative-specific view, it -should be designed under workspace behavior, not as a standalone initiative -resolve command. - -## Deferred Or Replaced Scope - -The following ideas are not part of Item 9 implementation: - -- `openspec initiative resolve <id>` -- scanning all registered workspaces -- scanning all repos on disk -- explicit `--path` based initiative resolution -- Git remote matching -- repo ownership inference -- cloning, fetching, pulling, pushing -- branch or worktree creation -- initiative backlinks -- progress dashboards -- local availability dashboards - -## Roadmap Disposition - -Item 9 is a decision-only checkpoint. It records that standalone initiative -resolution is rejected permanently. - -Roadmap framing: - -```text -Item 9. Reject Initiative Resolve - -Decision: do not add `openspec initiative resolve`, now or later. Initiative -discovery belongs to `initiative show`; local path mapping belongs to -workspaces; implementation progress belongs to repo-local changes. -``` - -## Next Useful Work - -The next useful implementation slice is workspace initiative opening, without a -standalone resolve prerequisite. diff --git a/openspec/initiatives/context-store-and-initiatives/work-items/09-add-initiative-resolve/tasks.md b/openspec/initiatives/context-store-and-initiatives/work-items/09-add-initiative-resolve/tasks.md deleted file mode 100644 index f442d86bd9..0000000000 --- a/openspec/initiatives/context-store-and-initiatives/work-items/09-add-initiative-resolve/tasks.md +++ /dev/null @@ -1,22 +0,0 @@ -# Reject Initiative Resolve Tasks - -## Decisions - -- [x] Create Item 9 work-item tracking notes. -- [x] Pressure-test whether a standalone `initiative resolve` command is needed. -- [x] Decide that a standalone user-facing `initiative resolve` command should - not be implemented now or later. -- [x] Decide `initiative show` remains sufficient for canonical initiative - discovery. -- [x] Decide workspace local-view state is the right place for local repo/path - mapping. -- [x] Decide repo-local status remains the right place for work progress. -- [x] Decide not to add all-repo scanning, all-workspace scanning, Git remote - matching, cloning, worktree creation, or initiative backlinks. - -## Follow-Up - -- [x] Update the central roadmap entry for Item 9. -- [x] Update the initiative task tracker. -- [x] Record workspace initiative opening as the next useful implementation - slice. diff --git a/openspec/initiatives/context-store-and-initiatives/work-items/10-let-workspaces-open-initiatives/plan.md b/openspec/initiatives/context-store-and-initiatives/work-items/10-let-workspaces-open-initiatives/plan.md deleted file mode 100644 index 42444b568b..0000000000 --- a/openspec/initiatives/context-store-and-initiatives/work-items/10-let-workspaces-open-initiatives/plan.md +++ /dev/null @@ -1,430 +0,0 @@ -# Let Workspaces Open Initiatives - -## Status - -Product decisions are locked. The remaining work is implementation design and -delivery. - -## Source Of Truth - -Start from `../../direction.md` and the boundary: - -```text -Context stores sync truth. -Collections shape truth. -Initiatives coordinate work. -Workspaces open local views. -Changes implement repo-owned slices. -``` - -Item 9 rejected standalone initiative resolution. Initiative discovery belongs -to `initiative show`; local path mapping belongs to workspace local-view state. - -## Locked Direction - -A workspace does not contain the work. It remembers how this runtime opens the -work. - -```text -private local view record - -> generated runtime files - -> opener-specific launch - -> initiative context + selected local repos/folders -``` - -The durable part is the user's private local view choice. The generated part is -runtime support for agents and editors. - -## Product Goal - -Let a user open a shared initiative in their own local runtime with the context -and repos they care about. - -Examples: - -- A Team A developer opens `platform/billing-launch` with local Repo A and Repo - B. -- A Team B developer opens the same initiative with local Repo C only. -- A user opens the initiative context only, links repos later, and still gets - useful agent guidance. - -## Non-Goals - -- Do not clone repos. -- Do not create branches or worktrees. -- Do not use Git submodules as the workspace primitive. -- Do not infer all participating repos from Git remotes or disk scans. -- Do not write generated agent files into linked repos or context stores. -- Do not make workspace-level `changes/` the durable planning model. -- Do not enforce edit permissions in Item 10. - -## Decision Register - -### Command UX - -Status: decided. - -Use `workspace open` for initiative local-view realization: - -```bash -openspec workspace open --initiative platform/billing-launch -openspec workspace open --initiative billing-launch --store platform -openspec workspace open --initiative billing-launch -openspec workspace open team-a-billing --initiative platform/billing-launch -``` - -Rationale: the action being performed is local view realization, so the command -belongs under `workspace open` rather than `initiative open`. - -Lookup behavior: - -- If the user provides `<store>/<initiative>`, use that exact store selector. -- If the user provides `<initiative> --store <store>`, use that exact store - selector. -- If the user provides only `<initiative>`, search registered context stores and - proceed when there is exactly one exact match. -- If multiple stores contain the same initiative id, stop and show the matching - stores with a hint to retry using `<store>/<initiative>` or `--store`. -- If no exact match exists, do not silently open the closest match. Show a small - list of likely matches when available, plus a hint to run `openspec - initiative list`. -- If some registered stores cannot be read, keep the result conservative. Do not - choose a match that could be ambiguous behind an unreadable store unless the - user supplied an explicit store selector. - -Interactive UX may let a human choose from suggestions. JSON and non-interactive -UX should return structured errors and suggestions without prompting. - -Workspace-name behavior: - -- The optional positional workspace name remains the local view identity. -- If the user provides a workspace name with `--initiative`, create or reuse that - named local view. -- If the user omits a workspace name, create or reuse a friendly default derived - from the initiative id when that is unambiguous. -- On name collisions or multiple existing local views for the same initiative, - let the human choose interactively or require an explicit workspace name in - non-interactive mode. - -### Open Target - -Status: decided. - -Default to opening the initiative directory, not the whole context store. - -User-facing behavior: - -```bash -openspec workspace open --initiative billing-launch -``` - -opens a focused local view: - -```text -generated files in the workspace root -context-store/initiatives/billing-launch/ -selected local repos/folders -``` - -It should not open the entire context store by default. - -Rationale: - -- The user asked for one initiative, so the opened context should be focused on - that initiative. -- Agents receive less unrelated shared context. -- Unrelated initiatives and shared files are not exposed by default. -- The local view stays easier to understand: generated workspace root plus this - initiative plus selected implementation roots. - -Generated guidance and JSON output should still report the context store root -and that broader context exists. A later explicit option may open the full -context store, for example `--context-scope store` or `--include-store`, but -broad store scope is not the default for Item 10. - -### Local View Record - -Status: decided. - -Use one private local view record: the root `workspace.yaml` file. - -```yaml -version: 1 -name: billing-launch -context: - kind: initiative - store: - id: platform - selector: - kind: registry - id: platform - initiative: - id: billing-launch -links: - repo-a: /Users/me/repos/repo-a - repo-b: /Users/me/repos/repo-b -preferred_opener: codex -tools: - - codex -``` - -This decision covers the conceptual record shape and the fact that generated -runtime files are not durable state. - -If the user selected a context store by local path, the private workspace record -can keep that runtime-local selector without changing checked-in repo metadata: - -```yaml -context: - kind: initiative - store: - id: platform - selector: - kind: path - path: /Users/me/context/platform - observed_id: platform - initiative: - id: billing-launch -``` - -The context binding is optional. A user can also create a workspace that is not -linked to any initiative: - -```yaml -version: 1 -name: team-a-local -context: null -links: - repo-a: /Users/me/repos/repo-a - repo-b: /Users/me/repos/repo-b -preferred_opener: codex -tools: - - codex -``` - -This is a first-class workspace shape, not only an edge case for initiative -opening. Item 10 should preserve custom non-initiative workspaces while adding -initiative-aware opening. - -### Workspace Storage And Generated Files - -Status: decided. - -Store each private workspace view under the user's OpenSpec global data -directory, keyed by workspace name: - -```text -getGlobalDataDir()/workspaces/<workspace-name>/ -``` - -The workspace name is the local identity. The selected store and initiative, if -any, are data inside the private record; they do not define the storage path. -This keeps the workspace API generic enough for custom local views that are not -initiative-linked. - -Initial shape: - -```text -getGlobalDataDir()/workspaces/<workspace-name>/ - workspace.yaml - AGENTS.md - <workspace-name>.code-workspace - .codex/ - skills/ - .claude/ - skills/ -``` - -`workspace.yaml` is the durable private view record and the only view file in -Item 10. The other files are generated runtime support owned by OpenSpec. They -may be overwritten by `workspace open`, `workspace update`, or a future explicit -preparation surface. - -Do not add a separate generated-output directory for Item 10. The managed -workspace root is already the private generated view. - -Initiative open defaults: - -- If the user provides a workspace name and no workspace exists, create that - workspace bound to the selected initiative. -- If the user provides a workspace name and it already points at the same - initiative, reuse it and regenerate runtime files. -- If the user provides a workspace name and it has no context binding, bind it - to the selected initiative only after clear user confirmation; in - non-interactive mode, fail and require an explicit future rebind/update - surface. -- If the user provides a workspace name and it points at a different initiative - or context, do not silently repoint it. Stop with a clear error and require an - explicit future rebind/update surface. -- If the user omits a workspace name and exactly one existing workspace points at - the selected initiative, reuse it. -- If the user omits a workspace name and no existing workspace points at the - selected initiative, create a friendly default workspace name derived from the - initiative id only when that name is unused. -- If the derived workspace name collides with another workspace, ask for an - explicit workspace name or show matching workspace choices instead of hiding - the collision behind a path convention. -- If multiple workspaces point at the same initiative, let the user choose or - require an explicit workspace name in non-interactive mode. - -### Generated Runtime Files - -Status: decided. - -Generate runtime files at the workspace root, next to `workspace.yaml`. - -```text -getGlobalDataDir()/workspaces/<workspace-name>/ -``` - -The generated files can contain `AGENTS.md`, skills, launch prompts, and -generated editor workspace files. - -Regeneration behavior: - -- `workspace open` regenerates the managed runtime files before launching the - opener. -- `workspace update` regenerates the managed runtime files without changing - durable local view choices unless the user asked for a state change. -- Generated files are OpenSpec-owned and may be overwritten each time. -- `workspace.yaml` is not generated output and should not be overwritten except - when the local view record itself changes. - -### Runtime Identity - -Status: decided. - -Use `getGlobalDataDir()` as the runtime-local boundary. It is already -cross-platform and resolves to the appropriate user data directory for macOS, -Linux, Windows, Codespaces, WSL, SSH hosts, and containers. - -Local paths in `workspace.yaml` are valid only in the runtime that wrote them. -If the same user opens the same initiative from another runtime, they create or -relink that runtime's workspace there. Item 10 should not add path translation, -shared machine identities, or an extra `<runtime-id>` path segment. - -### Prepare/JSON Surface - -Status: decided. - -Keep `workspace open --json` as a machine-facing receipt for the same open -operation. Do not add `--prepare-only` for Item 10. - -The JSON response should be useful to agents and desktop integrations, not just -a success boolean. It should include the workspace name, workspace root, -generated file paths, selected context, opened roots, skipped or missing roots, -opener, launch status, and warnings. - -Human-facing behavior remains the normal `workspace open` output. JSON mode is -for tools that need structured facts after OpenSpec has prepared the workspace -root and attempted the requested open. - -### Missing Paths At Open Time - -Status: decided. - -Workspace opening should be strict about the selected initiative/context and -forgiving about optional linked local paths. - -- If the selected initiative cannot be resolved, fail before launch. -- If the context store or initiative path is unavailable, fail before launch and - point to context-store registration/doctor guidance. -- If a linked repo or folder is missing, warn and skip that root; do not block a - context-only or partially linked open. -- Human output should name skipped links and suggest `workspace doctor` or - relink guidance. -- JSON output should include skipped or missing roots and warnings. - -### Codex Desktop - -Status: decided. - -Open the generated workspace root as the Codex Desktop project. Surface the -attached initiative path and linked repo/folder paths through generated guidance -and the `workspace open --json` response. - -Do not depend on Desktop multi-root automation for Item 10. If Desktop later has -a clearer multi-root contract, it can become an enhancement without changing the -workspace storage model. - -### Edit Boundaries - -Status: decided. - -Item 10 emits advisory boundaries only. Generated context should distinguish -coordination context from implementation targets, but it should not enforce -write restrictions. - -The generated view should label initiative/context-store files as shared -coordination context and linked repos/folders as local implementation context -when selected. Strong enforcement can come later. - -## First-Run UX Sketch - -Status: deferred beyond the first implementation slice. - -This sketch captures the eventual human interactive flow. Item 10 should not -depend on building a full guided setup wizard; the first implementation may use -explicit flags and structured errors first. - -```text -Found initiative: platform/billing-launch -No local workspace view exists for this runtime. - -Create a local view? -> Open context only - Link existing local repos/folders - Cancel -``` - -No option in this first-run flow should clone, branch, create worktrees, or -create submodules. - -## Machine-Readable Open Contract - -`workspace open --json` is the machine-readable contract for the generated -runtime context. Item 10 should not create a separate machine-readable view -file; the durable view record is `workspace.yaml`. - -The JSON response should tell agents: - -- schema version -- workspace name and workspace root -- selected initiative id, title, and path -- selected context store id and path -- generated file paths -- opened roots -- skipped or missing roots -- linked repo-local changes when known -- advisory edit boundaries -- next repair commands -- warnings and launch status when produced by `workspace open --json` - -If no implementation target is selected, `allowedEditRoots` should be empty or -explicitly advisory. - -The exact schema can evolve during implementation, but the JSON response should -make the generated view self-describing enough for agents and desktop -integrations without scraping human output. - -## Forward Compatibility - -The initial `context` record supports the selected context store and initiative. -Do not design the YAML parser so narrowly that future records cannot add fields -for configurable change homes, artifact homes, target bindings, or other -collection/view metadata. - -## Compatibility Notes - -The current beta workspace implementation creates a managed root with -`changes/`, `AGENTS.md`, `.gitignore`, -`.openspec-workspace/workspace.yaml`, `.openspec-workspace/local.yaml`, and a -durable `.code-workspace` file. - -Item 10's intended new shape is a root `workspace.yaml` plus generated runtime -files at the managed workspace root. Existing beta workspaces should be treated -as compatibility inputs. Migration or removal of all beta internals is deferred -unless the implementation slice intentionally scopes that migration. - -For the initiative-opening model, generated runtime files are derived artifacts, -not workspace truth. diff --git a/openspec/initiatives/context-store-and-initiatives/work-items/10-let-workspaces-open-initiatives/tasks.md b/openspec/initiatives/context-store-and-initiatives/work-items/10-let-workspaces-open-initiatives/tasks.md deleted file mode 100644 index a44944c96c..0000000000 --- a/openspec/initiatives/context-store-and-initiatives/work-items/10-let-workspaces-open-initiatives/tasks.md +++ /dev/null @@ -1,43 +0,0 @@ -# Let Workspaces Open Initiatives Tasks - -## Decisions - -- [x] Create Item 10 work-item tracking notes. -- [x] Lock the high-level direction: private local view record plus generated - runtime files. -- [x] Decide command UX. -- [x] Decide default open target. -- [x] Decide private local view record shape. -- [x] Decide private local view record storage namespace and keying. -- [x] Decide generated runtime file location and lifetime. -- [x] Decide runtime identity rules. -- [x] Decide prepare/JSON surface. -- [x] Decide Codex Desktop behavior. -- [x] Decide Item 10 edit-boundary semantics. - -## Implementation Scope To Confirm Later - -- [x] Add or adapt workspace local-view state for initiative opening. -- [x] Preserve non-initiative custom workspaces as first-class local views. -- [x] Resolve initiative context through existing `initiative show` semantics. -- [x] Implement workspace-name reuse and collision behavior for initiative open. -- [x] Generate opener-specific runtime files. -- [x] Return explicit machine-readable view context from `workspace open --json`. -- [x] Launch agent/editor with generated workspace root plus initiative context and - selected local repos/folders. -- [x] Warn and skip missing linked repos/folders at open time while failing on - missing selected initiative/context. -- [x] Add doctor guidance for missing context stores, missing local links, stale - view records, and advisory edit boundaries. -- [x] Ensure Item 10 opens known local paths only and does not clone, branch, - create worktrees, or use submodules. - -## Deferred - -- [ ] Multiple saved views per initiative. -- [ ] Shared/exported workspace templates. -- [ ] Repo auto-discovery or Git remote matching. -- [ ] Strong edit-boundary enforcement. -- [ ] Codex Desktop multi-root automation if the Desktop contract is not clear - enough for Item 10. -- [ ] Migration or removal of all existing beta workspace root artifacts. diff --git a/openspec/initiatives/context-store-and-initiatives/work-items/11-manual-beta-reality-pass/notes.md b/openspec/initiatives/context-store-and-initiatives/work-items/11-manual-beta-reality-pass/notes.md deleted file mode 100644 index 317aaac695..0000000000 --- a/openspec/initiatives/context-store-and-initiatives/work-items/11-manual-beta-reality-pass/notes.md +++ /dev/null @@ -1,289 +0,0 @@ -# Manual Beta Reality Pass Notes - -Use this as the scratchpad while trying the beta flow. - -## What Worked - -- Manual beta pass caught the bad default before building more surface area. -- After changing the default, rerunning - `openspec context-store setup team-context --init-git` from inside the - OpenSpec repo created the store at - `~/.local/share/openspec/context-stores/team-context` instead of nesting it in - the repo. -- Minimal fresh-agent handoff worked for initiative creation. A subagent given - only the store id and a loose topic created `agent-trace-hooks` in the correct - context-store location: - `~/.local/share/openspec/context-stores/team-context/initiatives/agent-trace-hooks`. - -## What Felt Weird - -- Fresh-user guidance immediately drifted into sandbox/environment setup - (`XDG_CONFIG_HOME`, `XDG_DATA_HOME`) instead of letting the user just run the - beta locally. Strong reaction: this should work as a normal local workflow. -- `openspec context-store setup` with no args feels like it should start an - interactive setup, but it does not. The command name itself creates that - expectation. -- `openspec context-store setup team-context --init-git` created - `team-context/` inside the current OpenSpec repo because the default path is - `./<id>`. User expected a default outside the current repo, not a new Git repo - nested in whatever directory they happened to run from. -- Cleaning up the accidental store had no obvious CLI path. `context-store` - exposes setup/register/list/doctor, but no unregister/remove command, so - cleanup required removing the folder and editing the registry manually. -- `context-store setup --init-git` initializes Git, but leaves - `.openspec-store/` and new initiatives untracked. That may be fine, but the - beta flow does not tell the user or agent whether to stage/commit the shared - context store. -- `openspec workspace open` with no arguments prompts only for known local - workspace views. It does not show registered context stores or initiatives, so - `team-context` is absent even though the next guide step is opening an - initiative from that store. This is technically consistent with the current - implementation, but confusing in the beta flow because the command name reads - like the broad "open something OpenSpec-related" entrypoint. -- The post-initiative step has the wrong first-run verb. After creating a - context store and an initiative, the user is conceptually creating a local - workspace view for that initiative. "Open" implies the workspace already - exists, so the beta guide and CLI make the user infer a hidden create-or-open - behavior. - -## Missing Prompts Or Too Many Flags - -- Need clearer guidance for whether a beta pass should use existing local - OpenSpec state or create a normal local test context store. Avoid requiring - environment variables as the default manual path. -- Missing prompt: when no context-store id is provided, ask for the store id, - path, and Git initialization choice instead of requiring the user to know the - positional argument/flags. -- Missing prompt/safety check: before creating a default context store under - the current directory, show the target path and ask for confirmation or offer - a managed default location. -- Missing handoff guidance: after context-store setup, the guide tells the user - to ask an agent to create an initiative, but a fresh agent may not know the - beta initiative CLI or where to find the agent playbook. -- Missing prompt: `workspace open` should either offer an "open initiative from - context store" path when registered initiatives exist, or make the zero-arg - prompt text explicit that it is selecting an existing local workspace view - only. If it offers initiatives, it should likely list references like - `team-context/agent-trace-hooks`, not just the store id. -- Missing first-run workspace creation flow: after an initiative exists, the - user should be guided through creating the local workspace view. A simple - interactive path could ask what to set up, list registered initiatives such as - `team-context/agent-trace-hooks`, suggest a workspace name from the initiative - id, optionally link existing repo/folder paths, choose an opener, then create - the workspace view. -- Better minimal beta path: keep lazy workspace creation, but make bare - interactive `openspec workspace open` initiative-aware. The picker should show - existing local workspace views and registered initiatives that can create a - local view on selection, with labels that preserve the distinction between - "workspace" and "initiative." -- The generated initiative file contract is underexplained. The CLI creates - exactly `initiative.yaml`, `requirements.md`, `design.md`, `decisions.md`, - `questions.md`, and `tasks.md`, but docs describe the Markdown files as - "typical" or "then edit" rather than naming the contract clearly. -- A user looking at the generated initiative tree may reasonably ask where that - structure came from. The exact six-file contract is clear in code and the - internal MVP work item, but public beta docs do not make it explicit and the - broader direction doc still mentions future `contracts/` content. - -## Agent Handoff Notes - -- The first agent step has a bootstrapping problem. `context-store setup` does - not create repo-local guidance, and `workspace open --initiative` cannot run - until the initiative exists. A fresh agent needs either an explicit pasted - mini-playbook, installed OpenSpec skills, or CLI output that prints the exact - next agent prompt/command. -- In the manual subagent test, the agent ran `initiative create --help`, then - created the initiative with `--store team-context --title ... --summary ... - --json`. It correctly resolved the store and did not create files in the - OpenSpec repo. -- The subagent replaced generated `TBD` placeholders with useful short content, - which suggests the templates give enough structure but not enough guidance. - There is no CLI option to seed richer content beyond title and summary. -- `initiative create --json` reports `created_files` as relative names. Agents - have to combine those with the returned root to get absolute paths. -- "Commands only" is product-ambiguous for this beta. The implementation treats - it as "remove all skills and install only slash command files," but users may - read it as "I prefer slash commands for workflow entry points." They still - likely expect their coding agent to understand OpenSpec concepts, context - stores, initiatives, and workspace handoff. - -## Delivery UX Model - -- Split the concept into two layers: - - Baseline OpenSpec literacy: "Does the agent understand OpenSpec concepts and - know how to inspect context stores, initiatives, workspaces, and repo-local - changes?" - - Workflow entrypoints: "How does the user invoke workflow actions such as - propose/apply/archive?" -- Current `delivery` acts like a generated-artifact cleanup switch. That is too - low-level for the user-facing choice. -- Better meaning: - - `skills`: install the baseline guide skill plus workflow skills. - - `commands`: install the baseline guide skill plus workflow slash commands. - - `both`: install the baseline guide skill plus workflow skills and workflow - slash commands. -- In UI copy, avoid "commands only" if it implies no skills at all. Prefer - labels like "Slash commands as workflow entrypoints" or "Workflow commands - only" with helper text that baseline OpenSpec guidance is still installed - when the selected agent supports skills. -- For tools without a command adapter, commands-oriented delivery should warn - clearly that workflow slash commands are unavailable for that tool. The tool - should still receive the baseline guide skill if it supports skills, so the - selected agent is not left with nothing. - -## Initiative Placement UX - -- A fresh agent also needs to know whether a new planning object belongs in a - context store or in the current repo. This should not be left to vibes. -- Product distinction: - - Initiatives in context stores are durable planning and coordination context - that intentionally lives outside implementation repos: product intent, - decisions, questions, roadmap notes, and tasks that should not necessarily - be checked into the code repo. - - Repo-local OpenSpec changes are implementation plans owned by the repo that - will change: proposal/design/spec deltas/tasks/validation. - - Workspaces are local views that connect shared context to local repos; they - should not become a third durable planning home. -- Agent guidance should not assume repo-local is preferred just because work - touches one repo. Use or create a context-store initiative when the user wants - OpenSpec artifacts outside the repo, when a monorepo has multiple teams with - separate planning contexts, when repo policy discourages planning artifacts, - when work is cross-repo/team-coordinated, long-lived, pre-implementation - discovery, or already tied to an existing context store. -- If a request is ambiguous, the agent should inspect first: - `openspec initiative list --json`, `openspec list --json`, and workspace - state when available. If still ambiguous, ask: "Should these OpenSpec - artifacts live outside the repo in a context store, or inside this repo as a - repo-local implementation change?" -- CLI/skill copy should make the linked flow explicit: create/read initiative - in the context store, then create repo-local changes from the owning repo with - `--initiative <store>/<initiative>`. - -## Initiative Creation Rethink - -- `openspec initiative create` currently creates a full six-file planning - packet with `TBD` placeholders. That is too eager for the intended audience: - PMs, designers, architects, and agents facilitating early product/architecture - thinking. -- Initial creation should register the initiative shell, not invent the plan. - The most conservative first slice is `initiative.yaml` plus either: - - a short `brief.md` seeded from title/summary/current understanding; or - - a lightweight `requirements.md` with no `TBD` placeholders and no claims of - accepted requirements until the content has been reviewed. -- Follow-up artifacts should be created iteratively when they become real: - - `requirements.md`: accepted high-level requirements, goals, non-goals, - unresolved product questions. - - `design.md`: reviewed product/UX/architecture direction and tradeoffs. - - `questions.md`: optional question log when questions need tracking. - - `decisions.md`: optional decision log appended only after decisions happen. - - Avoid default `tasks.md`; implementation tasks belong in repo-local changes. - If initiative-level coordination is needed later, use clearer language like - `workstreams.md`, `milestones.md`, or `coordination.md`. -- This should ideally become schema-led. Reuse the artifact-graph idea - (artifact ids, generated paths, templates, dependencies, status/instructions), - but root it at the initiative directory instead of repo-local changes. -- A minimal initiative schema could start with only `requirements` and `design`, - where design depends on requirements. `decisions` and `questions` are living - logs, so file-existence completion semantics may not fit them. -- For next-release safety, avoid a strict top-level `schema:` field in - `initiative.yaml` until metadata compatibility is designed. If a schema hint - needs persistence, store it under `metadata` or keep the default implicit. - -## Docs Fixes - -- The beta guide says "This creates a local context store" but does not explain - that the default location is `./<id>` relative to the current working - directory. That needs to be explicit if the default remains. -- Immediate docs/code fix changed the default away from `./<id>` and documented - the managed local data location instead. -- Step 2 should not assume the agent already knows the beta initiative command. - Include a copy-paste bootstrap prompt or link/inline excerpt from the agent - CLI playbook. -- Step 3 says "Open Your Local Workbench," but the command is actually - create-or-open when `--initiative` is passed. The guide should make that - explicit: "Create or open a local workspace view for the initiative." It - should also warn that bare `openspec workspace open` selects existing - workspace views only and will not list context stores like `team-context`. -- Better: change the user-facing flow so the first-time path is explicitly - creation/setup. The guide should send humans to an interactive workspace setup - path for the initiative, then reserve `workspace open` for reopening an - existing workspace view. -- Subagent UX/model passes recommended a leaner beta change: keep - `workspace open --initiative <store>/<initiative>` as the explicit - create-or-reuse path, but make bare interactive `workspace open` show - initiatives as selectable targets. Selecting an initiative should say it is - creating/opening a local workspace view. - -## Possible Implementation Slices - -- Make `openspec context-store setup` interactive when no id is provided: - prompt for store id, default path, and Git initialization; keep `--json` / - non-interactive behavior deterministic with a helpful fix message. -- Reconsider the default context-store setup path. Options: use the managed - OpenSpec data directory by default, or keep `./<id>` only after an interactive - confirmation that names the full target path. - - Implemented during the pass: use the managed OpenSpec data directory by - default and keep `--path` for explicit locations. -- Add `openspec context-store unregister <id>` or `remove <id>` for local - registry cleanup, with an explicit choice about whether to delete files or - only forget the local registration. -- Add a first-run handoff affordance after context-store setup, such as printing - "Next for your agent" guidance or adding a command that emits the agent - playbook for shared context/initiative setup. -- Add interactive workspace creation for initiative views. Candidate surfaces: - extend `openspec workspace setup` with initiative selection, add - `openspec workspace setup --initiative <store>/<initiative>`, or introduce a - clearer `workspace create` command. The key UX requirement is that a fresh - user can run an interactive command after initiative creation and be led to - "create a local workspace view for this initiative" without knowing - `--initiative` or the derived workspace-name convention. -- Add an initiative-aware `workspace open` picker as the smallest product fix: - on bare interactive open, list local workspaces plus registered initiatives. - If the user selects an initiative, feed it through the existing - `--initiative` create/reuse path. Do not auto-create workspaces during - context-store setup or initiative creation, and do not make workspaces 1:1 - with initiatives. - - Implemented during the pass: bare interactive `workspace open` now shows - registered initiatives that do not already have a known local view, and - selecting one creates/reuses the initiative-bound workspace view. - - Follow-up fix: when that lazy initiative view is new, `workspace open` - now runs the same repo/folder link prompt as `workspace setup` before - creating the workspace view. This avoids opening an empty workspace and - makes the first-run path collect implementation roots at the moment the - user expects it. -- Consider splitting baseline OpenSpec literacy from workflow delivery. A - small default `use-openspec` skill could be installed whenever a selected - agent supports skills, even if workflow delivery is set to commands-only, so - "commands only" means "workflow actions are slash commands" rather than "the - agent gets no OpenSpec context." -- Simpler possible slice: treat `use-openspec` as a normal managed skill bundled - with the configurator and installed by default. Keep it skill-only even if it - is presented as part of the default profile, so it does not create a slash - command, workflow artifact, or user-facing workflow action. -- Rethink `openspec initiative create` as a sparse, schema-led container - instead of a fully scaffolded planning packet. Initial create should likely - write only `initiative.yaml` plus a short `brief.md` seeded from title and - summary. Follow-up agent/CLI actions can add `requirements.md`, `design.md`, - `questions.md`, `decisions.md`, or coordination artifacts when there is - reviewed content to capture. Avoid default `TBD` sections, fake decisions, - and default initiative-level `tasks.md` that may be confused with repo-local - implementation tasks. -- Manual follow-up converted the test `agent-trace-hooks` initiative to the - proposed sparse shape: kept `initiative.yaml`, added `brief.md`, and removed - the eager generated planning files. `initiative show` still resolves because - current initiative identity depends on `initiative.yaml`. -- Promoted the broader fix into - `work-items/15-context-store-project-roots-and-schema-led-initiatives/`: - context stores should behave like OpenSpec roots for shared context, with - store-local config, schemas, and sparse schema-led initiative artifacts. -- Workspace shape correction: managed workspace views should not look like - repos. New workspace views should contain the generated root files - (`AGENTS.md`, `workspace.yaml`, and `<workspace>.code-workspace`) without a - default `changes/` directory or generated `.gitignore`; VS Code multi-root - views should show linked repos first, then initiative context, then the small - OpenSpec workspace folder. -- Guide correction: after opening a workspace, the user should ask the agent to - explore or draft using the initiative. The agent should resolve workspace - state, initiative context, and linked repo ownership, then run repo-local - OpenSpec commands from the owning repo. The user-facing flow should not make - humans type `openspec new change` or `cd` into implementation repos. diff --git a/openspec/initiatives/context-store-and-initiatives/work-items/11-manual-beta-reality-pass/plan.md b/openspec/initiatives/context-store-and-initiatives/work-items/11-manual-beta-reality-pass/plan.md deleted file mode 100644 index 93ca446d80..0000000000 --- a/openspec/initiatives/context-store-and-initiatives/work-items/11-manual-beta-reality-pass/plan.md +++ /dev/null @@ -1,39 +0,0 @@ -# Manual Beta Reality Pass - -## Status - -Proposed next work item. - -## Goal - -Try the current beta flow by hand and use the friction as product input before -building more surface area. - -## Pass Shape - -Start from a fresh local setup and walk through: - -- context store setup or registration -- initiative creation and editing through an agent -- workspace open -- workspace link or relink -- workspace doctor -- repo-local change creation linked to an initiative -- handoff back to an agent - -## Output - -The output should be notes, not polish: - -- what felt easy -- what felt weird -- where flags leaked into user-facing docs -- where prompts were missing -- what an agent needed to be told explicitly -- what should become a follow-on implementation slice - -## Non-Goals - -- Do not require a clean public tutorial state. -- Do not solve every issue found during the pass. -- Do not turn the beta flow into a progress dashboard. diff --git a/openspec/initiatives/context-store-and-initiatives/work-items/11-manual-beta-reality-pass/tasks.md b/openspec/initiatives/context-store-and-initiatives/work-items/11-manual-beta-reality-pass/tasks.md deleted file mode 100644 index 1d7f4133c0..0000000000 --- a/openspec/initiatives/context-store-and-initiatives/work-items/11-manual-beta-reality-pass/tasks.md +++ /dev/null @@ -1,8 +0,0 @@ -# Manual Beta Reality Pass Tasks - -- [ ] Run the current beta flow from a fresh user's point of view. -- [ ] Capture notes in the initiative as the pass happens. -- [ ] Mark where the user should type commands versus prompt an agent. -- [ ] Record confusing output, missing prompts, and unclear command names. -- [ ] Update beta docs with immediate findings. -- [ ] Split larger findings into proposed implementation work items. diff --git a/openspec/initiatives/context-store-and-initiatives/work-items/12-context-store-first-run-and-cleanup-ux/evidence.md b/openspec/initiatives/context-store-and-initiatives/work-items/12-context-store-first-run-and-cleanup-ux/evidence.md deleted file mode 100644 index 214921ad2d..0000000000 --- a/openspec/initiatives/context-store-and-initiatives/work-items/12-context-store-first-run-and-cleanup-ux/evidence.md +++ /dev/null @@ -1,45 +0,0 @@ -# Context Store First-Run And Cleanup UX Evidence - -## Manual Beta Source Notes - -The manual beta pass found: - -- no-argument `openspec context-store setup` feels like it should start an - interactive setup; -- accidental setup previously created a store under the current repo before the - managed default was corrected; -- cleanup had no CLI path and required deleting files plus editing the registry - manually; -- Git initialization left shared files untracked without telling the user or - agent what to do next. - -## Initial Recommendation - -Keep context-store first-run UX small and local: - -- prompt only for local setup choices; -- never push, pull, commit, create remotes, or delete files implicitly; -- keep JSON output explicit enough for agents to continue safely; -- leave team sync policy to the later shared-coordination hardening work. - -## Implementation Result - -- `openspec context-store setup` now runs a guided setup in interactive - terminals when no id is provided. -- Non-interactive and `--json` setup require explicit inputs and fail with a - structured setup-id diagnostic when the id is missing. -- Explicit setup paths inside another Git repository are blocked - non-interactively and require explicit confirmation interactively. -- `context-store unregister <id>` removes only the local registry entry. -- `context-store remove <id>` removes the local registry entry and deletes the - local folder only after confirmation or `--yes`; it refuses to delete folders - without matching context-store metadata. -- Human success output is intentionally compact; JSON output carries exact - registry, file, and Git state without `next_commands`. - -Verification: - -- `pnpm build` -- `pnpm lint` -- `pnpm vitest run test/commands/context-store.test.ts test/core/context-store/registry.test.ts` -- `pnpm test` diff --git a/openspec/initiatives/context-store-and-initiatives/work-items/12-context-store-first-run-and-cleanup-ux/plan.md b/openspec/initiatives/context-store-and-initiatives/work-items/12-context-store-first-run-and-cleanup-ux/plan.md deleted file mode 100644 index 39b4326c3e..0000000000 --- a/openspec/initiatives/context-store-and-initiatives/work-items/12-context-store-first-run-and-cleanup-ux/plan.md +++ /dev/null @@ -1,150 +0,0 @@ -# Context Store First-Run And Cleanup UX - -## Status - -Implemented. - -This work item covers the context-store setup and cleanup gaps that were not -fully captured by later docs, schema, or handoff work. - -## Source Of Truth - -Manual beta notes: - -- `../11-manual-beta-reality-pass/notes.md`, especially the findings around - no-argument setup, cleanup, target path safety, and shared-store Git guidance. - -Preserve the current boundary: - -```text -Context stores sync truth. -Collections shape truth. -Initiatives coordinate work. -Workspaces open local views. -Changes implement repo-owned slices. -``` - -## Why This Exists - -The beta pass found that `openspec context-store setup` feels like a first-run -entrypoint, but no-argument setup currently does not guide the user through the -choices they need to make. The pass also found that recovering from a mistaken -store setup requires manual registry edits and file deletion. - -These are local lifecycle problems, not shared coordination model problems. -They should be solved before asking new users or teammates to trust context -stores as normal local workflow. - -## Goals - -- Make no-argument `context-store setup` a friendly interactive setup path in a - terminal. -- Keep non-interactive and JSON behavior deterministic and agent-safe. -- Make the target store path explicit before creation. -- Provide a supported local cleanup command for removing or unregistering a - context store from this machine. -- Keep Git setup limited to optional local initialization, without staging, - committing, pushing, creating remotes, or choosing team workflow. - -## Non-Goals - -- Do not add remote creation, clone, pull, push, watch, or sync automation. -- Do not make setup choose team governance, branching, or review policy. -- Do not delete shared files without an explicit user choice. -- Do not make context stores implementation repos. - -## UX Direction - -Locked decisions from the product pass: - -- `openspec context-store setup` with no arguments should start a guided setup - when run in an interactive terminal. Agents, scripts, CI, and `--json` callers - should pass the equivalent explicit inputs instead of relying on prompts. -- The guided setup should ask only for values that map to existing setup flags: - context store id, context store path, and whether to initialize Git. -- User-facing prompt copy should stay direct: - `Context store name`, `Where should this context store live?`, - `Initialize Git in this context store?`, then a final - `Create this context store?` confirmation after showing the resolved summary. -- The default location should be the managed OpenSpec context-store directory, - not the current working directory. Users can still choose any explicit safe - local path; OpenSpec stores that machine-local path in the local registry, not - in shared context-store metadata. -- Setup should be protective around risky paths: create missing paths, accept - empty directories, treat matching context-store metadata as idempotent, stop - on metadata/id conflicts, stop on files, and stop or explicitly warn before - using a non-empty unmarked directory or a path inside another Git repository. -- Cleanup should expose two explicit intents: `context-store unregister <id>` - forgets the machine-local registry entry and leaves files alone, while - `context-store remove <id>` unregisters the store and deletes the local folder - only after showing the exact path and receiving confirmation. -- Happy-path human output should stay small: show the context store id, its - location, and the next user-facing step. Do not show Git state, metadata - paths, registry paths, or created-file lists unless there is a warning, - failure, `--json`, or `context-store doctor` output. -- JSON output should report exact resulting state, not workflow guidance. Include - ids, roots, metadata paths, registry state, Git facts, created/deleted files, - and warnings/errors where present, but do not include `next_commands`. Empty - `status: []` can be preserved where existing JSON compatibility needs it, but - new behavior should not rely on blank status arrays for meaning. -- Git initialization is an optional local convenience only. When requested, - OpenSpec may run `git init`, but it must not stage, commit, push, create - remotes, create branches, or define team Git policy. - -Interactive setup should cover the minimum choices: - -```text -Store id -Target path, defaulting to the managed OpenSpec context-store location -Whether to initialize Git -``` - -Before writing files, output should show the resolved target path. If an -explicit path is inside another Git repo or an existing non-empty directory, -the command should either ask for confirmation with clear wording or fail with -a fix message in non-interactive mode. - -Cleanup should distinguish local registration from file deletion: - -```bash -openspec context-store unregister team-context -openspec context-store remove team-context -``` - -The command names are explicit because the user intents are different: - -- forget this local registry entry only -- delete this local context-store folder too - -If Git initialization fails, setup should explain that the user can install Git -or rerun setup without Git. Successful Git initialization stays out of the -happy-path human output. - -## Agent / JSON Contract - -JSON setup output should report: - -- store id -- root path -- metadata path -- whether Git was initialized -- whether files were created or already existed -- local registry path or registry entry identity - -JSON cleanup output should report: - -- store id -- removed local registry entry, if any -- deleted root path, if requested -- files left on disk, if deletion was not requested -- warnings for missing, ambiguous, or already-removed state - -## Done When - -- A fresh user can run `openspec context-store setup` in a terminal and be led - through the normal local setup path without knowing flags. -- Non-interactive and JSON setup still fail predictably when required choices - are missing. -- A mistaken local store registration can be removed through the CLI without - hand-editing the registry. -- Setup and cleanup output make local file, registry, and Git state explicit. diff --git a/openspec/initiatives/context-store-and-initiatives/work-items/12-context-store-first-run-and-cleanup-ux/tasks.md b/openspec/initiatives/context-store-and-initiatives/work-items/12-context-store-first-run-and-cleanup-ux/tasks.md deleted file mode 100644 index 95245a1406..0000000000 --- a/openspec/initiatives/context-store-and-initiatives/work-items/12-context-store-first-run-and-cleanup-ux/tasks.md +++ /dev/null @@ -1,23 +0,0 @@ -# Context Store First-Run And Cleanup UX Tasks - -- [x] Decide exact no-argument `context-store setup` behavior for TTY, - non-TTY, and `--json` invocations. -- [x] Design the interactive setup prompts for store id, target path, and Git - initialization. -- [x] Define target-path safety behavior for managed defaults, explicit paths, - paths inside existing Git repos, and non-empty directories. -- [x] Implement the interactive setup flow without changing deterministic - non-interactive behavior. -- [x] Decide whether the cleanup surface is `unregister`, `remove`, or both. -- [x] Define cleanup semantics for "forget local registration" versus "delete - local files too". -- [x] Implement local registry cleanup with explicit confirmation before file - deletion. -- [x] Add human output that stays small and JSON output that reports exact setup - and cleanup state without `next_commands`. -- [x] Keep Git initialization scoped to local `git init` with no auto-staging, - committing, pushing, remote creation, or team policy. -- [x] Add focused tests for setup prompts, non-interactive failures, path - safety, registry cleanup, and JSON output. -- [x] Update beta docs and agent playbook references for first-run setup and - cleanup. diff --git a/openspec/initiatives/context-store-and-initiatives/work-items/13-agent-handoff-output-and-delivery-polish/evidence.md b/openspec/initiatives/context-store-and-initiatives/work-items/13-agent-handoff-output-and-delivery-polish/evidence.md deleted file mode 100644 index 40b8808512..0000000000 --- a/openspec/initiatives/context-store-and-initiatives/work-items/13-agent-handoff-output-and-delivery-polish/evidence.md +++ /dev/null @@ -1,41 +0,0 @@ -# Agent Handoff Output And Delivery Polish Evidence - -## Manual Beta Source Notes - -The manual beta pass found: - -- after context-store setup, the user is told to ask an agent to create an - initiative, but a fresh agent may not know the beta CLI or where to find the - playbook; -- `initiative create --json` reports `created_files` as relative names, so - agents must combine them with the returned root before writing; -- "commands only" can sound like "the agent gets no OpenSpec guidance," even - though users may only mean slash commands as workflow entrypoints; -- tools without command adapters need a clear warning when workflow slash - commands cannot be installed. - -## Initial Recommendation - -Treat this as output polish, not a new workflow engine: - -- add direct path fields rather than breaking existing relative fields; -- keep handoff guidance concrete and command-sized; -- keep baseline OpenSpec literacy separate from workflow entrypoints; -- leave the broader "what should I do next?" command to the proposed handoff - work item. - -## Reassessment - -After reviewing practical examples, the proposed "Next for your agent" shape -looked too prescriptive. It assumes a fixed linear beta path, but agents may -inspect state, branch, skip setup, continue an existing change, or use context in -a different order. - -Conclusion on 2026-05-30: - -- Skip Item 13 as an implementation item for now. -- Preserve the evidence because the handoff pain is real. -- Do not hardcode fixed next-step guidance until the product has a clearer - receipt or affordance model. -- Consider splitting deterministic `created_paths` style receipt fields into a - smaller future slice if they remain useful. diff --git a/openspec/initiatives/context-store-and-initiatives/work-items/13-agent-handoff-output-and-delivery-polish/plan.md b/openspec/initiatives/context-store-and-initiatives/work-items/13-agent-handoff-output-and-delivery-polish/plan.md deleted file mode 100644 index af578e2de4..0000000000 --- a/openspec/initiatives/context-store-and-initiatives/work-items/13-agent-handoff-output-and-delivery-polish/plan.md +++ /dev/null @@ -1,106 +0,0 @@ -# Agent Handoff Output And Delivery Polish - -## Status - -Deferred as an implementation item. - -This work item captures a real beta pain, but the current "Next for your agent" -shape should not be built yet. It assumes a linear workflow path and risks -hardcoding guidance that does not fit dynamic agentic work. - -Decision on 2026-05-30: skip this item for now. Keep the notes as research -input for a future handoff receipt model. - -## Source Of Truth - -Manual beta notes: - -- `../11-manual-beta-reality-pass/notes.md`, especially the findings around - post-setup agent guidance, relative `created_files`, and commands-oriented - delivery warnings. - -Related work: - -- `../proposed-initiative-next-agent-handoff-ux/` -- `../14-workspaces-beta-guide-split/` -- `../15-context-store-project-roots-and-schema-led-initiatives/` - -## Why This Was Proposed - -The beta pass showed that agents can succeed if they know which command to run, -but the first handoff is still too implicit. Setup output, JSON receipts, docs, -and generated delivery artifacts should make the next move obvious without -requiring the user to paste tribal knowledge. - -That pain is still valid. The uncertain part is the product shape. A fixed next -step may be wrong when the agent can inspect current state, discover existing -initiatives, skip workspace setup, continue from a repo-local change, or choose -a different planning route. - -## Future Direction - -If this is revisited, frame it as a receipt or affordance model: - -- report what now exists; -- report where canonical context and created artifacts live; -- report relevant state and selected local bindings; -- optionally report available actions, not a required next command; -- avoid a single `next_command` unless the next action is genuinely - deterministic. - -Small deterministic output improvements, such as absolute `created_paths`, may -still be worth splitting into a narrower implementation slice. - -## Non-Goals - -- Do not implement an `initiative next` command in this slice. -- Do not add progress dashboards or work-status rollups. -- Do not create initiatives, changes, or workspaces automatically as part of - setup output. -- Do not make every relative path field disappear if existing compatibility - requires it; add direct absolute path fields instead. -- Do not hardcode a single user or agent journey. - -## Deferred Output Sketch - -Avoid this prescriptive shape for now: - -```text -Next for your agent: - Ask your coding agent to create or update an initiative in team-context. -``` - -If a future model exists, prefer contextual receipts: - -```json -{ - "created_files": ["brief.md"], - "created_paths": [ - "/path/to/store/initiatives/billing-launch/brief.md" - ], - "handoff_context": { - "store": "team-context", - "initiative": "billing-launch", - "workspace": null - }, - "available_actions": [ - "inspect_initiative", - "open_workspace_view", - "create_repo_local_change" - ] -} -``` - -Delivery copy may still need separate work to distinguish: - -- baseline OpenSpec guidance or literacy; -- workflow entrypoints such as skills or slash commands. - -## Revisit When - -- Item 14 clarifies the human guide versus agent playbook split. -- Item 15 clarifies sparse initiative artifacts and context-store project-root - behavior. -- There is enough beta evidence to decide whether command output should expose - state receipts, available affordances, direct paths only, or no special - handoff block. diff --git a/openspec/initiatives/context-store-and-initiatives/work-items/13-agent-handoff-output-and-delivery-polish/tasks.md b/openspec/initiatives/context-store-and-initiatives/work-items/13-agent-handoff-output-and-delivery-polish/tasks.md deleted file mode 100644 index 243af6d171..0000000000 --- a/openspec/initiatives/context-store-and-initiatives/work-items/13-agent-handoff-output-and-delivery-polish/tasks.md +++ /dev/null @@ -1,38 +0,0 @@ -# Agent Handoff Output And Delivery Polish Tasks - -Status: deferred. Do not implement these tasks until the handoff model is -redesigned as contextual receipts or affordances rather than fixed linear -"next step" guidance. - -- [ ] Revisit after Item 14 and Item 15 clarify the beta docs, sparse - initiative flow, and context-store project-root model. -- [ ] Decide whether any deterministic receipt improvements, such as - `created_paths`, should be split into a smaller independent slice. -- [ ] Decide whether delivery terminology belongs in a separate - command-surface/delivery item. - -## Deferred Original Tasks - -- [ ] Decide which existing commands should print a "Next for your agent" - handoff block. -- [ ] Define the minimal handoff content for context-store setup, initiative - creation, workspace opening, and repo-local linked change creation. -- [ ] Add direct created-path fields, such as `created_paths`, where JSON output - currently forces agents to combine relative file names with returned - roots. -- [ ] Preserve compatibility for existing relative `created_files` fields where - callers may already depend on them. -- [ ] Update `initiative create --json` and sparse initiative creation output - from Item 15 to include direct artifact paths and next commands. -- [ ] Decide how generated docs or setup output points to the agent CLI - playbook without requiring a pasted mini-playbook in every guide step. -- [ ] Clarify delivery terminology so commands-oriented delivery means workflow - commands as entrypoints, not absence of baseline OpenSpec guidance. -- [ ] Add warnings when a selected tool does not support workflow slash command - delivery. -- [ ] Define how baseline OpenSpec guidance is reported when commands-oriented - delivery is selected for a tool that still supports skills. -- [ ] Add tests or fixtures for human output, JSON output, and delivery-warning - behavior. -- [ ] Update beta docs and generated agent guidance with the polished handoff - and delivery language. diff --git a/openspec/initiatives/context-store-and-initiatives/work-items/14-workspaces-beta-guide-split/plan.md b/openspec/initiatives/context-store-and-initiatives/work-items/14-workspaces-beta-guide-split/plan.md deleted file mode 100644 index c4e1d8882e..0000000000 --- a/openspec/initiatives/context-store-and-initiatives/work-items/14-workspaces-beta-guide-split/plan.md +++ /dev/null @@ -1,37 +0,0 @@ -# Workspaces Beta Guide Split - -## Status - -Proposed next work item. - -## Goal - -Make the beta docs match how people should actually use the feature: - -- humans use terminal prompts for local setup and local paths -- coding agents use explicit CLI commands for OpenSpec work - -## Working Model - -User-facing docs should be light on flags and heavy on agent prompts. The agent -CLI playbook should carry the exact commands, JSON surfaces, cwd rules, and -current caveats. - -Manual beta clarification: after a workspace is opened, the user should ask the -agent to explore or draft from the workspace. The agent should resolve the -workspace and initiative context, identify the owning linked repo, and run -repo-local OpenSpec commands from that repo. The workspace is the conversation -surface, not the artifact home. - -## Scope - -- Revise `docs/workspaces-beta/user-guide.md`. -- Revise `docs/workspaces-beta/agent-cli-playbook.md`. -- Keep the docs minimal until the flow has been tried manually. -- Record command or prompt gaps found during the doc pass. - -## Non-Goals - -- Do not change CLI behavior in this work item. -- Do not promise sync, cloning, branching, worktrees, progress dashboards, or - enforced edit boundaries. diff --git a/openspec/initiatives/context-store-and-initiatives/work-items/14-workspaces-beta-guide-split/tasks.md b/openspec/initiatives/context-store-and-initiatives/work-items/14-workspaces-beta-guide-split/tasks.md deleted file mode 100644 index 00d86c8c71..0000000000 --- a/openspec/initiatives/context-store-and-initiatives/work-items/14-workspaces-beta-guide-split/tasks.md +++ /dev/null @@ -1,9 +0,0 @@ -# Workspaces Beta Guide Split Tasks - -- [x] Identify which setup steps should be typed by the user. -- [x] Identify which initiative and change steps should be delegated to a coding - agent. -- [x] Update the user guide around interactive setup and agent prompts. -- [x] Update the agent CLI playbook around explicit commands and cwd rules. -- [x] Add a tiny caveat section that reflects shipped beta behavior. -- [ ] Capture any product gaps exposed by the docs pass. diff --git a/openspec/initiatives/context-store-and-initiatives/work-items/15-context-store-project-roots-and-schema-led-initiatives/evidence.md b/openspec/initiatives/context-store-and-initiatives/work-items/15-context-store-project-roots-and-schema-led-initiatives/evidence.md deleted file mode 100644 index 0d52781bba..0000000000 --- a/openspec/initiatives/context-store-and-initiatives/work-items/15-context-store-project-roots-and-schema-led-initiatives/evidence.md +++ /dev/null @@ -1,140 +0,0 @@ -# Context Store Project Roots And Schema-Led Initiatives Evidence - -## Manual Beta Findings - -- A fresh-agent style prompt successfully created `agent-trace-hooks` in the - registered `team-context` context store. -- The current CLI created this hardcoded file set: - -```text -initiative.yaml -requirements.md -design.md -decisions.md -questions.md -tasks.md -``` - -- The generated markdown templates started with `TBD` placeholders. -- The agent filled those documents with plausible but unreviewed planning - content. -- We manually reduced the test initiative to a sparse shape: - -```text -initiative.yaml -brief.md -``` - -- `openspec initiative show team-context/agent-trace-hooks --json` and - `openspec initiative list --store team-context --json` continued to resolve, - which proves current identity/listing logic does not require the six-file - packet. - -## Code Observations - -- Initiative file names are hardcoded in - `src/core/collections/initiatives/schema.ts`. -- Initiative markdown templates are hardcoded in - `src/core/collections/initiatives/templates.ts`. -- `createInitiative` writes `initiative.yaml` and then all default template - files in `src/core/collections/initiatives/operations.ts`. -- `initiative create --json` reports `created_files` from - `INITIATIVE_FILE_NAMES` in `src/commands/initiative.ts`. -- Initiative list/show read only `initiative.yaml`. -- Project-local schema resolution already uses - `<projectRoot>/openspec/schemas/<name>/schema.yaml` in - `src/core/artifact-graph/resolver.ts`. -- Project config already reads `<projectRoot>/openspec/config.yaml` in - `src/core/project-config.ts`. -- Change artifact status/instructions are coupled to repo-local change context - through `src/core/artifact-graph/instruction-loader.ts`. -- Planning-home detection currently treats an ancestor containing `openspec/` - as a possible repo planning root, so adding config to context stores needs a - safety check. - -## UX/Product Pass - -Recommended user meaning: - -```text -context store = shared OpenSpec context project -initiative = iterative high-level planning object -repo change = implementation plan -workspace = local view -``` - -Docs should avoid saying initiatives are only for cross-repo or cross-team -work. A user may choose a context store simply because they want OpenSpec -artifacts outside the implementation repo. - -`initiative create` should make the smallest useful shared object and then -teach the agent how to continue through status/instructions. It should not -pretend requirements, decisions, and tasks exist before review. - -## Architecture Pass - -Feasible minimal path: - -1. Treat the context store root as a project root for config/schema resolution. -2. Create `openspec/config.yaml` during context-store setup. -3. Resolve initiative schemas with `projectRoot = contextStoreRoot`. -4. Add initiative-specific status/instructions helpers using artifact graph - primitives. -5. Change initiative creation to write a sparse shell. - -Main risks: - -- strict `initiative.yaml` parsing if a new top-level `schema` field is added -- tests currently asserting the six-file MVP contract -- docs and generated agent guidance currently telling agents to edit the five - generated Markdown files -- ambiguity between initiative planning artifacts and repo-local implementation - tasks -- context-store roots becoming accidental repo planning homes after they gain - `openspec/config.yaml` - -## Subagent / Research Notes - -Three focused passes converged on the same direction. - -Architecture pass: - -- Model a context store as an OpenSpec planning root: - -```text -context-store/ - .openspec-store/store.yaml - openspec/config.yaml - openspec/schemas/ - initiatives/ -``` - -- Keep `.openspec-store/store.yaml` as store identity and - `openspec/config.yaml` as behavior/configuration. -- Reuse project-local config and schema resolution with the context-store root - as the project root. -- Add an initiative-specific artifact context instead of forcing initiatives - through repo-local change context. -- Guard planning-home discovery so a context store with `openspec/config.yaml` - does not become an accidental implementation repo. - -UX/product pass: - -- Describe a context store as an OpenSpec-managed planning home. It may be used - for cross-repo coordination, but also simply to keep OpenSpec artifacts out of - an implementation repo. -- Make `initiative create` sparse: `initiative.yaml` plus a seed artifact such - as `brief.md`. -- Add status/instructions output so agents create requirements and design - artifacts only when there is reviewed content to capture. -- Stop treating default initiative artifacts as files the user or agent should - immediately fill in. - -Release-risk pass: - -- Keep old six-file beta initiatives readable. -- Update tests that assert the old generated file list. -- Avoid strict top-level additions to `initiative.yaml` until metadata - versioning is designed. -- Defer context-store-hosted executable changes to the configurable change-home - work instead of bundling them into this slice. diff --git a/openspec/initiatives/context-store-and-initiatives/work-items/15-context-store-project-roots-and-schema-led-initiatives/plan.md b/openspec/initiatives/context-store-and-initiatives/work-items/15-context-store-project-roots-and-schema-led-initiatives/plan.md deleted file mode 100644 index 22a01ff2fe..0000000000 --- a/openspec/initiatives/context-store-and-initiatives/work-items/15-context-store-project-roots-and-schema-led-initiatives/plan.md +++ /dev/null @@ -1,344 +0,0 @@ -# Context Store Project Roots And Schema-Led Initiatives - -## Status - -Proposed from the manual beta reality pass. - -This work item replaces the current "initiative create writes a full hardcoded -six-file packet" model with a project-like context-store root and an iterative, -schema-led initiative artifact flow. - -## Source Of Truth - -Start from `../../direction.md` and preserve the current boundary: - -```text -Context stores sync truth. -Collections shape truth. -Initiatives coordinate work. -Workspaces open local views. -Changes implement repo-owned slices. -``` - -Manual beta evidence: agents can create the current MVP initiative shape, but -the generated `requirements.md`, `design.md`, `decisions.md`, `questions.md`, -and `tasks.md` invite premature, unreviewed planning content. - -## Why This Exists - -`openspec initiative create` currently creates a complete-looking planning -packet from hardcoded TypeScript constants and `TBD` templates. That made the -MVP tangible, but it is the wrong default for real initiative work. - -Initiatives are high-level shared planning surfaces for PMs, designers, -architects, and agents. They should capture intent, reviewed requirements, -design direction, open questions, and decisions as those artifacts become real. -They should not create empty or fake documents that look finished just because -the folder exists. - -The broader product shape is that a context store should feel like an OpenSpec -root in the same way a repo does after `openspec init`: it can have local -OpenSpec configuration and project-local schemas. The difference is lifecycle: -a context store is the shared context root, not an implementation repo. - -## Product Model - -Repo after `openspec init`: - -```text -repo/ - openspec/ - config.yaml - schemas/ - changes/ - specs/ -``` - -Context store after setup: - -```text -context-store/ - .openspec-store/ - store.yaml - openspec/ - config.yaml - schemas/ - initiatives/ -``` - -The `openspec/` directory inside a context store exists for OpenSpec config and -schema resolution. It does not by itself make the context store an executable -implementation planning home. - -## Goals - -- Let context stores carry OpenSpec config, including a default initiative - schema. -- Let context stores carry project-local schemas under `openspec/schemas/`. -- Replace hardcoded initiative file creation with a schema-led artifact model. -- Make `initiative create` sparse and safe by default. -- Let agents grow initiative artifacts one step at a time through status and - instructions output. -- Keep existing six-file MVP initiatives readable. -- Keep repo-local changes as the default implementation artifact. - -## Non-Goals - -- Do not make context-store-hosted executable changes part of this slice. That - remains Item 18. -- Do not add cross-repo apply, archive, validation, or spec-sync orchestration. -- Do not make workspace-local artifacts the shared planning source of truth. -- Do not migrate existing initiatives automatically. -- Do not install AI-tool runtime files into context stores by default unless a - later UX decision explicitly opts into that. - -## Default Initiative Shape - -New initiative creation should create a shell, not the whole plan: - -```text -initiatives/<id>/ - initiative.yaml - brief.md -``` - -`brief.md` is a seed document, not a completion marker for reviewed -requirements or design. It should contain the title, summary, and a short -"current understanding" section with no `TBD` placeholders. - -Reviewed planning artifacts are created later through the initiative schema. - -Default built-in schema, conceptually: - -```yaml -name: product-initiative -version: 1 -description: High-level initiative planning for PMs, designers, architects, and agents -usage: initiative -artifacts: - - id: requirements - generates: requirements.md - description: Product intent, goals, non-goals, requirements, and open questions - template: requirements.md - requires: [] - - - id: design - generates: design.md - description: Product, UX, and architecture direction with constraints and tradeoffs - template: design.md - requires: - - requirements -``` - -Do not include `tasks.md` in the default initiative schema. Implementation -tasks belong in repo-local changes. Coordination tasks, workstreams, decision -logs, and question logs can be separate later schemas or explicit artifacts once -the usage pattern is clearer. - -## UX Direction - -Store setup should make the store project-like enough for schemas: - -```bash -openspec context-store setup team-context --init-git -``` - -Expected created shape: - -```text -team-context/ - .openspec-store/store.yaml - openspec/config.yaml - initiatives/ -``` - -Preferred config direction: - -```yaml -initiative_schema: product-initiative -``` - -This avoids overloading the existing repo-local `schema` field, which currently -means "default change schema." If implementation chooses to reuse `schema` -instead, docs and JSON output must make the context-store scope explicit. - -Then initiative creation stays small: - -```bash -openspec initiative create agent-trace-hooks \ - --store team-context \ - --title "Agent Trace Hooks" \ - --summary "Explore lightweight capture of agent trace events and hook outcomes." -``` - -Expected next action: - -```bash -openspec initiative status team-context/agent-trace-hooks --json -openspec initiative instructions requirements team-context/agent-trace-hooks --json -``` - -The agent writes `requirements.md` only when the conversation has enough -reviewed content. `design.md` becomes ready after requirements exist. - -## Technical Approach - -Reuse the artifact graph primitives, but add an initiative-specific loader -instead of forcing initiatives through `loadChangeContext`. - -Current reusable pieces: - -- `src/core/artifact-graph/graph.ts` -- `src/core/artifact-graph/state.ts` -- `src/core/artifact-graph/outputs.ts` -- `src/core/artifact-graph/resolver.ts` -- `src/core/artifact-graph/instruction-loader.ts` template loading -- `src/core/project-config.ts` - -New initiative-specific pieces: - -- a context-store OpenSpec-root helper that treats the store root as the - `projectRoot` for config and schema lookup -- an initiative artifact context loader rooted at - `context-store/initiatives/<id>/` -- initiative `status` and `instructions` commands that mirror the repo-local - artifact workflow but return initiative-specific fields -- a sparse `initiative create` path that writes `initiative.yaml` and `brief.md` - only - -Do not use the existing repo planning-home resolver unchanged. Once context -stores contain `openspec/config.yaml`, the current "nearest `openspec/` folder -means repo planning home" heuristic can accidentally make a context store look -like an implementation repo. This work must either: - -- teach planning-home resolution to detect `.openspec-store/store.yaml` and - return or reject a context-store kind for implementation commands, or -- explicitly reject `openspec new change` from a context-store root until Item - 15 defines target-bound executable changes. - -## Schema And Config Compatibility - -Prefer a next-release-safe config path: - -- Add `initiative_schema` to project config, or an equivalent - collection-specific config field. -- Continue using existing `schema` as the default repo-local change schema. -- Store per-initiative schema overrides in existing `metadata` if needed. -- Avoid adding a new top-level `schema` field to `initiative.yaml` until - initiative metadata versioning is designed. - -Why: `initiative.yaml` is currently strict and versioned as `version: 1`. -Adding a top-level field would make older CLIs reject new initiatives. Existing -`metadata` can carry forward-compatible fields without breaking old readers. - -Schema namespace needs one explicit decision: - -- Either add a `usage: change | initiative` discriminator to schema files and - filter commands accordingly, or -- use a separate initiative schema namespace while reusing the same artifact - graph format. - -The simplest user-facing model is still `openspec/schemas/`, but commands must -avoid listing `product-initiative` as a valid repo-local change workflow. - -## JSON Contract - -`initiative create --json` should report the shell and next actions: - -```json -{ - "context_store": { - "id": "team-context", - "root": "/path/to/store" - }, - "initiative": { - "id": "agent-trace-hooks", - "root": "/path/to/store/initiatives/agent-trace-hooks", - "metadata_path": "/path/to/store/initiatives/agent-trace-hooks/initiative.yaml", - "schema": "product-initiative" - }, - "created_files": [ - "initiative.yaml", - "brief.md" - ], - "next_commands": { - "status": "openspec initiative status team-context/agent-trace-hooks --json", - "requirements": "openspec initiative instructions requirements team-context/agent-trace-hooks --json" - }, - "status": [] -} -``` - -`initiative status --json` should include: - -- context store identity and root -- initiative identity, root, metadata path, and selected schema -- artifact paths keyed by artifact id -- artifact statuses: `done`, `ready`, `blocked` -- next steps -- action context that says this is shared planning context, not an editable - implementation target - -`initiative instructions --json` should include: - -- resolved output path -- existing output paths -- schema instruction -- template content -- dependencies and unlocks -- store config context/rules if supported - -## Release Risk And Migration - -This is compatible if implemented as a sparse, additive layer: - -- Existing six-file initiatives remain readable because list/show only require - `initiative.yaml`. -- Existing optional markdown files can remain in old initiative folders. -- New status/instructions can ignore files outside the selected schema. -- Old CLIs can still read new initiatives if schema data stays under - `metadata` or store config rather than new strict top-level fields. - -High-risk areas: - -- Planning-home detection after context stores gain `openspec/config.yaml`. -- Tests and docs that assert the six-file MVP initiative shape. -- Schema lists and completions if initiative schemas share the same namespace - as change schemas. -- Agent guidance that still tells agents to edit every generated initiative - markdown file after creation. - -## Test And Doc Touch Points - -Likely tests to update or add: - -- `test/core/collections/initiatives/schema.test.ts` -- `test/core/collections/initiatives/templates.test.ts` -- `test/core/collections/initiatives/operations.test.ts` -- `test/commands/initiative.test.ts` -- `test/commands/context-store.test.ts` -- `test/commands/artifact-workflow.test.ts` -- planning-home tests for context-store roots with `openspec/config.yaml` -- schema listing/completion tests if schema usage filtering is added - -Likely docs to update: - -- `docs/workspaces-beta/agent-cli-playbook.md` -- `docs/workspaces-beta/user-guide.md` -- `docs/cli.md` -- schema docs if `usage` or `initiative_schema` is added -- `openspec/initiatives/context-store-and-initiatives/work-items/05-ship-initiative-mvp/` - with a note that the MVP shape was superseded by this work item - -## Done When - -- A new context store has OpenSpec config and can resolve project-local - initiative schemas. -- `initiative create` creates only the sparse initiative shell. -- Agents can use initiative status/instructions to create high-level planning - artifacts iteratively. -- `openspec new change` does not accidentally treat a context store as a normal - implementation repo just because the store has `openspec/config.yaml`. -- Existing MVP initiatives continue to list and show. -- Docs describe initiative artifacts as reviewed, iterative context rather than - files to fill in immediately. diff --git a/openspec/initiatives/context-store-and-initiatives/work-items/15-context-store-project-roots-and-schema-led-initiatives/tasks.md b/openspec/initiatives/context-store-and-initiatives/work-items/15-context-store-project-roots-and-schema-led-initiatives/tasks.md deleted file mode 100644 index dace62c162..0000000000 --- a/openspec/initiatives/context-store-and-initiatives/work-items/15-context-store-project-roots-and-schema-led-initiatives/tasks.md +++ /dev/null @@ -1,39 +0,0 @@ -# Context Store Project Roots And Schema-Led Initiatives Tasks - -- [x] Create Item 15 work-item tracking notes. -- [ ] Record the product decision that context stores should behave like - OpenSpec roots for config and schema resolution, but not as implementation - repos by default. -- [ ] Define the context-store root layout, including `.openspec-store/`, - `openspec/config.yaml`, `openspec/schemas/`, and `initiatives/`. -- [ ] Decide the config key for the default initiative schema, with - `initiative_schema` as the preferred next-release-safe direction. -- [ ] Decide whether initiative schemas share `openspec/schemas/` with a - `usage: initiative` discriminator or use a separate namespace while - reusing the artifact graph format. -- [ ] Add or design the built-in `product-initiative` schema for high-level - requirements and design artifacts. -- [ ] Define `brief.md` as the sparse creation seed and decide whether it sits - outside the artifact graph or is represented as an already-complete - artifact. -- [ ] Change `initiative create` from hardcoded six-file generation to sparse - `initiative.yaml` plus `brief.md` creation. -- [ ] Add initiative artifact status resolution rooted at - `context-store/initiatives/<id>/`. -- [ ] Add initiative artifact instructions output that returns schema guidance, - template content, dependencies, output path, and existing paths. -- [ ] Ensure store-local config context and rules can be read for initiative - artifact instructions without confusing repo-local change config. -- [ ] Guard planning-home resolution so context stores with - `openspec/config.yaml` do not silently become repo-local implementation - homes. -- [ ] Update `initiative create --json`, human output, and next-command guidance - for sparse creation and iterative artifacts. -- [ ] Update tests that currently assert the MVP six-file initiative shape. -- [ ] Add compatibility tests proving old six-file initiatives still list and - show. -- [ ] Add tests for context-store local schemas and store config defaults. -- [ ] Update beta docs and agent guidance to stop telling agents to edit every - initiative markdown file immediately after creation. -- [ ] Record migration behavior and a note that Item 5's six-file MVP shape has - been superseded by this schema-led sparse model. diff --git a/openspec/initiatives/context-store-and-initiatives/work-items/16-add-escalation-ux/plan.md b/openspec/initiatives/context-store-and-initiatives/work-items/16-add-escalation-ux/plan.md deleted file mode 100644 index 12ddd5d3bb..0000000000 --- a/openspec/initiatives/context-store-and-initiatives/work-items/16-add-escalation-ux/plan.md +++ /dev/null @@ -1,26 +0,0 @@ -# Add Escalation UX - -## Status - -Future work item. Kept after first-run setup, handoff output, guide cleanup, -and schema-led initiatives because escalation should build on a sane onboarding -path. - -## Goal - -Let users start locally and upgrade into a coordinated initiative only when the -work actually needs shared context. - -## Ship - -- Explore/propose guidance that starts in the current repo by default. -- Recommendation triggers when work spans multiple owned areas. -- Carry-forward behavior for current change name, product goal, notes, inferred - areas, and relevant questions. -- Prompts grounded in concrete affected areas instead of abstract storage - models. - -## Done When - -- Coordinated planning feels like a continuation of local planning, not a - workflow restart. diff --git a/openspec/initiatives/context-store-and-initiatives/work-items/16-add-escalation-ux/tasks.md b/openspec/initiatives/context-store-and-initiatives/work-items/16-add-escalation-ux/tasks.md deleted file mode 100644 index 6dcd465915..0000000000 --- a/openspec/initiatives/context-store-and-initiatives/work-items/16-add-escalation-ux/tasks.md +++ /dev/null @@ -1,7 +0,0 @@ -# Add Escalation UX Tasks - -- [ ] Define local-to-initiative recommendation triggers. -- [ ] Carry current planning context into a new initiative. -- [ ] Keep prompts grounded in affected areas. -- [ ] Decide where escalation guidance appears in agent instructions, command - output, or interactive prompts. diff --git a/openspec/initiatives/context-store-and-initiatives/work-items/17-harden-team-shared-coordination/plan.md b/openspec/initiatives/context-store-and-initiatives/work-items/17-harden-team-shared-coordination/plan.md deleted file mode 100644 index c36f9230f3..0000000000 --- a/openspec/initiatives/context-store-and-initiatives/work-items/17-harden-team-shared-coordination/plan.md +++ /dev/null @@ -1,25 +0,0 @@ -# Harden Team-Shared Coordination - -## Status - -Future work item. This should follow the first-run UX and schema-led initiative -work so team guidance is built on the stable beta path. - -## Goal - -Make initiatives practical for several teammates without turning setup into an -admin ceremony. - -## Ship - -- Recommended Git-backed shared context-store setup. -- Lightweight teammate onboarding. -- Repair flows for local path mappings. -- Sync status and conflict guidance. -- Clear separation between committed initiative state and machine-local - workspace state. - -## Done When - -- Several teammates can share the same initiative while each keeps their own - local checkout layout. diff --git a/openspec/initiatives/context-store-and-initiatives/work-items/17-harden-team-shared-coordination/tasks.md b/openspec/initiatives/context-store-and-initiatives/work-items/17-harden-team-shared-coordination/tasks.md deleted file mode 100644 index fa51ad070a..0000000000 --- a/openspec/initiatives/context-store-and-initiatives/work-items/17-harden-team-shared-coordination/tasks.md +++ /dev/null @@ -1,7 +0,0 @@ -# Harden Team-Shared Coordination Tasks - -- [ ] Document recommended Git-backed store setup. -- [ ] Define teammate onboarding and repair flows. -- [ ] Add sync status and conflict guidance. -- [ ] Define how committed initiative state and machine-local workspace state - should be explained in docs and generated guidance. diff --git a/openspec/initiatives/context-store-and-initiatives/work-items/18-explore-initiative-hosted-target-bound-change-artifacts/evidence.md b/openspec/initiatives/context-store-and-initiatives/work-items/18-explore-initiative-hosted-target-bound-change-artifacts/evidence.md deleted file mode 100644 index eaaea23940..0000000000 --- a/openspec/initiatives/context-store-and-initiatives/work-items/18-explore-initiative-hosted-target-bound-change-artifacts/evidence.md +++ /dev/null @@ -1,397 +0,0 @@ -# Explore Initiative-Hosted Target-Bound Change Artifacts Evidence - -## Initial Research Notes - -- Current product direction says context stores sync shared truth, initiatives - coordinate work, and repo-local changes own implementation planning. -- Roadmap Item 8 currently assumes repo-local changes linked to initiatives. -- Existing planning-home behavior distinguishes repo-local and workspace - planning homes, but does not have a context-store-backed change home. -- New artifact workflow commands already consume resolved planning paths in - some places, which may be a useful seam for future change-home resolution. -- Older command surfaces still assume `openspec/changes/` under a local - OpenSpec project and need an explicit audit before any implementation slice. - -## Initial Framing - -Configurable change homes are a product-boundary question, not just a path -change. A context-store-hosted artifact would still need a clear target repo or -spec root before validation, apply, archive, or spec sync can run safely. - -The future exploration should keep "change home" as internal resolver language -and use clearer product language around initiative-hosted planning artifacts, -target-bound changes, implementation targets, and editable roots. - -## Agent-First Team UX Research Pass - -Date: 2026-05-23. - -Question explored: - -```text -What does a great agent-first developer experience look like for teams using -context stores, initiatives, workspaces, and repo-local changes? -``` - -### External Pattern Notes - -- Linear uses initiatives as higher-level coordination objects that group - projects and expose health, ownership, and active project rollups. -- Jira planning commonly uses initiatives above epics or other child work - items for multi-team planning. -- GitLab roadmaps show higher-level epics and milestones across groups or - projects. -- GitHub Projects emphasize flexible planning that stays connected to issues - and repo work. - -These patterns point toward a common split: - -```text -Higher-level object = coordination and rollup -Execution item = work owned closer to a team, project, repo, or issue -``` - -OpenSpec should keep that separation while making the agent handoff sharper -than human project-management tools can. - -### Clean Mental Model - -The strongest mental model from the research pass: - -```text -Initiative = shared coordination truth -Workspace = local lens over initiative + repos -Repo change = executable implementation plan -``` - -Expanded product rule: - -```text -Context stores remember. -Initiatives coordinate. -Workspaces open. -Repo-local changes implement. -``` - -The key invariant: - -```text -Work identity is not storage location. -Storage location is not edit permission. -``` - -This keeps three decisions separate for agents: - -- What work is the user talking about? -- Where should the planning artifact live? -- Which files or repos may be edited now? - -### Suggested Artifact Types - -Repo context: - -```text -openspec/changes/<change-id>/ -``` - -Use for repo-owned implementation plans. A repo-local change may reference an -initiative through portable metadata: - -```yaml -initiative: - store: platform - id: billing-launch -``` - -Workspace context: - -```text -<store>/initiatives/<initiative-id>/work-items/<work-id>/ -``` - -Use for shared initiative planning before repo ownership or implementation -targets are clear. These should be called initiative work items, planning -briefs, or proposals, not executable OpenSpec changes, until Item 18 defines a -full lifecycle for context-store-backed changes. - -Workspace-local changes: - -```text -<workspace>/changes/<change-id>/ -``` - -Keep as legacy or beta compatibility unless the user explicitly opts into the -workspace-planning flow. - -### Agent-First UX Scenarios - -Single repo team: - -- User asks the agent to create a proposal from inside the repo. -- Agent resolves the initiative if named. -- Agent creates a repo-local change linked to the initiative. -- Apply, validate, sync, and archive stay repo-local. - -Monorepo: - -- One repo-local change can cover several packages or capabilities. -- The agent may need an area or package hint. -- The repo remains the implementation owner; areas clarify scope but do not - become separate change homes. - -Multi-repo platform: - -- Workspace opens the shared initiative context plus local repo clones. -- The initiative coordinates the platform outcome. -- Each owning repo gets its own linked repo-local change when implementation - ownership is known. -- Workspace state should report available local repos, missing local paths, and - edit boundaries. - -Central architecture team: - -- Architects may update initiative requirements, designs, contracts, decisions, - and questions without owning implementation. -- The agent should offer to draft shared initiative context or ask for the - owning repo before creating a repo-local change. - -Ownership unknown: - -- The agent should not create an implementation change. -- It should add or update initiative-level questions, or return target options - with a request for a repo or area decision. - -Teammate onboarding: - -```text -Clone or register the context store. -Run context-store doctor. -Open or resolve the initiative. -Link local repos through workspace mappings. -Ask the agent to continue from the initiative. -``` - -### Ideal Agent JSON Blocks - -Agents need stable routing vocabulary across create, status, instructions, -resolve, and list: - -```json -{ - "workTarget": { - "kind": "repo-change | initiative-work-item | workspace-change", - "id": "add-billing-api", - "root": "/absolute/path", - "storePath": "initiatives/billing-launch/work-items/add-billing-api" - }, - "initiativeLink": { - "store": "platform", - "id": "billing-launch", - "root": "/absolute/store/initiatives/billing-launch" - }, - "invocationContext": { - "kind": "repo | workspace", - "root": "/absolute/current/context" - }, - "actionContext": { - "mode": "implementation-ready | planning-only | target-selection-required", - "sourceOfTruth": "repo | context-store | workspace-local", - "allowedEditRoots": [], - "requiresTargetSelection": true, - "constraints": [ - "Use resolved output paths from the CLI.", - "Do not infer editable repos from the current working directory." - ] - }, - "nextCommands": {} -} -``` - -The important fields are: - -- `workTarget`: the object the agent is acting on. -- `initiativeLink`: the canonical shared coordination context, when present. -- `invocationContext`: where the command was run. -- `actionContext`: what the agent may edit. -- `nextCommands`: follow-up commands the agent should run instead of inventing - paths. - -### Lifecycle Rules - -- Repo-local changes are implementation-ready when the repo is the allowed edit - root. -- Initiative work items are planning-only until they select or link repo-local - implementation changes. -- Workspace-local changes are compatibility artifacts, not the preferred new - shared planning model. -- Apply, archive, repo spec sync, and repo delta validation should remain - repo-local until context-store-backed change lifecycle is explicitly designed. -- If `allowedEditRoots` is empty or target selection is required, agents should - stop before editing implementation files. - -### Edge Cases To Design For - -- Same initiative id exists in multiple stores. -- Some registered stores are unreadable or out of sync. -- A workspace can see a repo path but the user has not selected it as an edit - target. -- The terminal is inside a workspace, but the intended work belongs in a linked - repo. -- The terminal is inside a linked repo, but the user wants shared initiative - planning first. -- A repo-local change references an initiative store that is not registered on - the current machine. -- A context-store work item uses a schema that another teammate does not have. -- A change id exists both as a repo-local change and an initiative work item. -- A central team edits initiative context while implementation teams edit - linked repo-local changes. - -### Suggested Direction From The Pass - -Keep Item 8 narrow: - -- Add initiative metadata to repo-local changes. -- Add `new change <id> --initiative <store>/<initiative> --json`. -- Use `initiative show` plus workspace/repo context as the agent handoff - backbone. -- Do not implement context-store-backed OpenSpec changes in Item 8. - -Use Item 18 to decide the larger model: - -- Whether initiative work items should become a first-class artifact. -- Whether "change home" remains internal language. -- How context-store-hosted work binds to repo targets, specs, validation, - apply, archive, and sync. -- How skills and generated guidance teach agents to trust CLI JSON instead of - hardcoded paths or current working directory assumptions. - -## Target-Bound Reframe Subagent Pass - -Date: 2026-05-23. - -Question explored: - -```text -Given the product tension around central versus repo-local change storage, how -should Item 18 be reframed before implementation work begins? -``` - -Three subagent passes reviewed Item 18 from product semantics, agent-first UX, -and lifecycle/implementation angles. - -### Product Semantics Findings - -- The visible work item should not be framed as generic configurable storage. - That makes the hard question sound like path plumbing. -- The sharper product question is whether initiative-hosted artifacts can - become executable OpenSpec changes after they are bound to a target repo or - spec root. -- Repo-local changes remain the default executable implementation artifact. -- Initiative-hosted artifacts start as planning-only work items, briefs, or - proposals. -- "Change home" can stay as internal resolver language, but should not be the - main user-facing concept. - -Recommended naming: - -```text -Explore Initiative-Hosted Target-Bound Change Artifacts -``` - -### Agent-First UX Findings - -Agents need stable CLI output that separates the artifact from the thing the -agent may edit: - -```text -Plan lives in: repo-local OpenSpec | initiative context -Editable target: selected repo path | none yet -Linked initiative: platform/billing-launch -``` - -Commands should report structured action context rather than making generated -skills infer paths: - -```json -{ - "workTarget": { - "kind": "repo-change | initiative-work-item | initiative-hosted-change", - "id": "add-billing-api", - "root": "/absolute/path" - }, - "initiativeLink": { - "store": "platform", - "id": "billing-launch" - }, - "implementationTarget": { - "kind": "repo", - "id": "billing-api", - "specRoot": "openspec" - }, - "actionContext": { - "mode": "implementation-ready | planning-only | target-selection-required | unsupported", - "sourceOfTruth": "repo | context-store | workspace-local", - "allowedEditRoots": [], - "constraints": [ - "Use CLI-reported paths.", - "Do not infer editable repos from the current working directory." - ] - }, - "nextCommands": {} -} -``` - -If `allowedEditRoots` is empty, the agent should stop before editing -implementation files. If target selection is required, the command should return -next-step options rather than silently creating an ambiguous implementation -change. - -### Lifecycle And Implementation Findings - -Local code still has strong repo-local assumptions: - -- `src/core/planning-home.ts` models planning homes as `repo | workspace`. -- `src/commands/workflow/new-change.ts` resolves storage from the current - planning home and does not yet expose `--initiative` or `--json`. -- `src/commands/validate.ts` validates changes and specs from - `process.cwd()/openspec/...`. -- `src/core/archive.ts` archives by reading `openspec/changes`, applying deltas - to `openspec/specs`, and moving the change into `openspec/changes/archive`. -- `src/core/artifact-graph/types.ts` metadata does not yet model initiative - links, target repo identity, artifact home, or edit boundaries. -- Generated skills and workflow templates still contain repo-local path - assumptions such as `openspec/changes/<name>/`. - -These are not bugs in the current repo-local model. They are evidence that an -initiative-hosted executable change is a lifecycle design, not a small path -switch. - -### Updated Recommendation - -Keep Item 8 narrow: - -- Create or link repo-local changes with initiative metadata. -- Add JSON output for the agent handoff. -- Do not implement context-store-hosted executable changes in Item 8. - -Use Item 18 to answer the bigger question: - -- What initiative-hosted artifacts exist before an implementation target is - known? -- What target metadata lets a shared artifact graduate into an executable - change? -- How do local workspace and registry mappings resolve target repo identity to - machine-local paths? -- Which lifecycle commands should refuse, hand off to a repo-local change, or - operate directly against a resolved target? -- How should command and skill output teach agents to trust CLI-reported paths, - edit roots, and next commands? - -Go/no-go criterion: - -```text -Do not implement initiative-hosted executable changes until create/link, -show/status/list/instructions, validate, apply, archive, spec sync, workspace -resolution, generated skills, and JSON output all share one target-resolution -model. -``` diff --git a/openspec/initiatives/context-store-and-initiatives/work-items/18-explore-initiative-hosted-target-bound-change-artifacts/plan.md b/openspec/initiatives/context-store-and-initiatives/work-items/18-explore-initiative-hosted-target-bound-change-artifacts/plan.md deleted file mode 100644 index 9ae6faabcb..0000000000 --- a/openspec/initiatives/context-store-and-initiatives/work-items/18-explore-initiative-hosted-target-bound-change-artifacts/plan.md +++ /dev/null @@ -1,180 +0,0 @@ -# Explore Initiative-Hosted Target-Bound Change Artifacts - -## Status - -Not started. Added as a future exploratory work item. Framing updated from -generic "configurable change homes" to the sharper question of when shared -initiative artifacts can become executable, target-bound OpenSpec changes. - -## Source Of Truth - -Start from `../../direction.md`, especially the current boundary: - -```text -Context stores sync truth. -Collections shape truth. -Initiatives coordinate work. -Workspaces open local views. -Changes implement repo-owned slices. -``` - -## Why This Exists - -The current initiative direction assumes OpenSpec changes usually live in the -local repo that owns implementation. That keeps validation, archive, and spec -sync close to the code that will change. - -Some coordinated work may need a shared home before the owning repo is obvious. -A team may want initiative-hosted planning artifacts, and later may want some -of those artifacts to become implementation-ready plans for a specific repo or -spec root. - -This is not just a storage preference. A shared artifact is planning-only until -it has an explicit portable target binding and lifecycle rules for validate, -apply, archive, spec sync, and conflict handling. - -## Goal - -Decide whether OpenSpec should support initiative-hosted artifacts that can -graduate into executable changes only after they are bound to an implementation -target. - -Repo-local changes remain the default executable implementation artifact. Item -18 should decide if, when, and how a context-store-hosted artifact can safely be -treated as a change. - -The answer should preserve three boundaries: - -- Initiatives coordinate shared context. -- Changes describe executable implementation plans. -- Workspaces open local views and must not imply edit permission. - -## Model To Explore - -```text -Initiative artifact - -> planning-only by default - -> may become target-bound later - -Repo-local change - -> home: repo/openspec/changes/<id>/ - -> target: implicit current repo/spec root - -> lifecycle: validate/apply/archive/spec sync are repo-local - -Initiative-hosted target-bound change - -> home: context-store/initiatives/<initiative>/changes/<id>/ - -> target: explicit repo/spec root identity - -> lifecycle: unsupported until target resolution is designed - -Agent output - -> reports the work target - -> reports where the artifact lives - -> reports the implementation target, if any - -> reports allowed edit roots for this machine -``` - -Keep "change home" as internal resolver language. User-facing and agent-facing -output should prefer clearer phrases like "plan lives in repo-local OpenSpec", -"plan lives with the initiative", and "editable target". - -## Core Invariants - -- Storage location does not imply ownership, edit permission, or lifecycle. -- Work identity, artifact home, execution target, and allowed edit roots are - separate decisions. -- Shared context-store files must not store machine-local checkout paths. -- A targetless initiative artifact is a brief, work item, or proposal, not an - implementation-ready OpenSpec change. -- A context-store-hosted artifact can be considered executable only after it has - explicit target metadata and lifecycle command support. -- Item 8 remains repo-local: `new change <id> --initiative ...` creates or links - a repo-local change only. - -## Questions To Answer - -- What exact artifact types exist under an initiative: work items, briefs, - target-bound changes, or something else? -- What portable target metadata is required before an initiative-hosted artifact - can be executable? -- How does local resolution map a target repo identity to a checkout path, - OpenSpec root, branch, and allowed edit roots? -- Should central target-bound changes require explicit opt-in such as - `--home initiative`, or can initiative/store policy choose that behavior? -- If config exists, what is the deterministic precedence across explicit CLI - flags, repo config, initiative preference, context-store default, user default, - and built-in repo-local behavior? -- How does `openspec new change` report work target, artifact home, - implementation target, initiative link, action context, and next commands in - JSON? -- How do validate, apply, archive, and spec sync behave when the artifact lives - in a context store but the target specs live in a repo? -- Should archive for an initiative-hosted target-bound change archive centrally, - materialize a repo-local handoff change, or refuse until a repo-local change - exists? -- Which command and skill surfaces still hardcode `openspec/changes/`, current - working directory, or repo-local edit assumptions? -- What compatibility behavior preserves existing repo-local and workspace-local - changes? - -## Agent-First Output Contract - -Any future command that creates, reads, or resolves this work should make the -agent's next move explicit: - -```json -{ - "workTarget": { - "kind": "repo-change | initiative-work-item | initiative-hosted-change", - "id": "add-billing-api", - "root": "/absolute/path/reported/by/cli" - }, - "initiativeLink": { - "store": "platform", - "id": "billing-launch" - }, - "implementationTarget": { - "kind": "repo", - "id": "billing-api", - "specRoot": "openspec" - }, - "actionContext": { - "mode": "implementation-ready | planning-only | target-selection-required | unsupported", - "sourceOfTruth": "repo | context-store | workspace-local", - "allowedEditRoots": [], - "constraints": [ - "Use CLI-reported paths.", - "Do not infer editable repos from the current working directory." - ] - }, - "nextCommands": {} -} -``` - -If `allowedEditRoots` is empty, the agent should not edit implementation files. -If target selection is required, the command should return options or next -commands instead of creating an ambiguous implementation plan. - -## Explicitly Out Of Scope - -- Implementing context-store-hosted executable changes before the model is - decided. -- Moving existing repo-local changes into a context store automatically. -- Making initiatives own implementation artifacts by default. -- Making workspace-level changes the new shared planning model. -- Cross-repo apply, archive, or validation orchestration. -- Storing machine-local checkout paths in shared context-store files. -- Adding global defaults that can surprise ordinary repo-local commands into - writing shared artifacts. - -## Go/No-Go Criteria - -Do not implement initiative-hosted executable changes until OpenSpec has one -target-resolution model that can cover: - -- create and link output -- status, show, list, and instructions output -- validate, apply, archive, and spec sync behavior -- workspace registry and local repo mapping behavior -- generated skill guidance and command examples -- JSON output for work target, artifact home, implementation target, edit roots, - unsupported lifecycle commands, and next commands diff --git a/openspec/initiatives/context-store-and-initiatives/work-items/18-explore-initiative-hosted-target-bound-change-artifacts/tasks.md b/openspec/initiatives/context-store-and-initiatives/work-items/18-explore-initiative-hosted-target-bound-change-artifacts/tasks.md deleted file mode 100644 index 6218f63bf1..0000000000 --- a/openspec/initiatives/context-store-and-initiatives/work-items/18-explore-initiative-hosted-target-bound-change-artifacts/tasks.md +++ /dev/null @@ -1,28 +0,0 @@ -# Explore Initiative-Hosted Target-Bound Change Artifacts Tasks - -- [x] Create Item 18 work-item tracking notes. -- [x] Reframe Item 18 from generic change-home configuration to - initiative-hosted target-bound change artifacts. -- [ ] Audit commands, templates, validation, archive, apply, completion, and - docs for repo-local `openspec/changes/` assumptions. -- [ ] Define user-facing naming for initiative work items, briefs, - target-bound changes, artifact homes, and editable targets. -- [ ] Decide whether initiative-hosted artifacts can graduate into executable - changes, and which target metadata is required first. -- [ ] Decide the configuration or opt-in surface for repo-local versus - initiative-hosted artifacts. -- [ ] Define how `openspec new change` selects and reports the artifact home, - implementation target, initiative link, and action context. -- [ ] Define how initiative linking and workspace guidance discover artifact - homes and target repo mappings. -- [ ] Decide how initiative-hosted target-bound changes bind to repo specs, - implementation roots, branches, and local checkout paths. -- [ ] Decide validation, apply, archive, sync, and conflict behavior for - initiative-hosted target-bound changes. -- [ ] Define the agent JSON contract for work target, artifact home, - implementation target, allowed edit roots, unsupported lifecycle commands, and - next commands. -- [ ] Record compatibility behavior for existing repo-local and workspace-local - changes. -- [ ] Produce a recommendation, opt-in/config examples, affected command list, - and go/no-go criteria for implementation. diff --git a/openspec/initiatives/context-store-and-initiatives/work-items/19-review-workspace-beta-compatibility-before-public-release/plan.md b/openspec/initiatives/context-store-and-initiatives/work-items/19-review-workspace-beta-compatibility-before-public-release/plan.md deleted file mode 100644 index 04c4354a48..0000000000 --- a/openspec/initiatives/context-store-and-initiatives/work-items/19-review-workspace-beta-compatibility-before-public-release/plan.md +++ /dev/null @@ -1,62 +0,0 @@ -# Review Workspace Beta Compatibility Before Public Release - -## Goal - -Before workspaces become public/stable, decide what beta workspace -compatibility behavior is actually worth carrying forward. - -This is intentionally late-stage work. Workspaces have not been publicly -released yet, so unpublished beta internals should not automatically become a -permanent compatibility contract. - -## Background - -The beta currently contains a few compatibility paths: - -- Legacy split workspace state readers for `.openspec-workspace/workspace.yaml` - and `.openspec-workspace/local.yaml`. -- Managed workspace registry fallback behavior. -- `codex` to `codex-cli` opener normalization. -- Generated `.gitignore` cleanup for old workspace `.code-workspace` ignore - rules. -- Empty or deprecated helper shims that exist only because previous workspace - slices exposed them internally. - -Some of these may be useful for local beta testers. Others may be safer to -delete before public release. - -## Scope - -Review workspace compatibility only. Do not use this item to reopen unrelated -legacy migration systems such as old slash-command cleanup, telemetry config -migration, or deprecated `change`/`spec` command aliases. - -## Decisions To Make - -- Which workspace compatibility paths are part of the public contract? -- Which paths are beta-only migration helpers and can be removed after one - release note or cleanup pass? -- Which paths are only test compatibility and can be deleted before release? -- Should beta workspace roots be migrated automatically, left readable, or - intentionally unsupported? -- Should old generated `.gitignore` cleanup exist at all, given workspaces are - managed local folders rather than repos? - -## Implementation Notes - -- Prefer deletion over preserving compatibility for unpublished intermediate - beta states. -- If a compatibility path remains, document why it exists and what would allow - it to be removed later. -- Keep user-owned files safe. Do not clean or rewrite ambiguous local files - unless OpenSpec can prove it owns them. -- Update tests so they describe the chosen public contract rather than the - accidental beta history. - -## Done When - -- Workspace compatibility code is inventoried and classified. -- Low-value beta-only shims are removed. -- Remaining compatibility behavior has focused tests and release-note language. -- Public docs and generated agent guidance do not mention unsupported beta - internals. diff --git a/openspec/initiatives/context-store-and-initiatives/work-items/19-review-workspace-beta-compatibility-before-public-release/tasks.md b/openspec/initiatives/context-store-and-initiatives/work-items/19-review-workspace-beta-compatibility-before-public-release/tasks.md deleted file mode 100644 index 4ef391fa8a..0000000000 --- a/openspec/initiatives/context-store-and-initiatives/work-items/19-review-workspace-beta-compatibility-before-public-release/tasks.md +++ /dev/null @@ -1,16 +0,0 @@ -# Tasks - -- [ ] Inventory workspace compatibility code paths and tests. -- [ ] Classify each path as public contract, beta migration, test-only shim, or - removable dead weight. -- [ ] Decide whether legacy split workspace state remains readable after public - release. -- [ ] Decide whether old generated `.gitignore` cleanup should remain, become - more conservative, or be removed entirely. -- [ ] Decide how long `codex` should remain accepted as an alias for - `codex-cli`. -- [ ] Remove beta-only compatibility paths that do not need to survive public - release. -- [ ] Update tests to encode the chosen compatibility contract. -- [ ] Update docs, generated guidance, and release notes with the final public - behavior. diff --git a/openspec/initiatives/context-store-and-initiatives/work-items/proposed-initiative-next-agent-handoff-ux/evidence.md b/openspec/initiatives/context-store-and-initiatives/work-items/proposed-initiative-next-agent-handoff-ux/evidence.md deleted file mode 100644 index 884feca24d..0000000000 --- a/openspec/initiatives/context-store-and-initiatives/work-items/proposed-initiative-next-agent-handoff-ux/evidence.md +++ /dev/null @@ -1,47 +0,0 @@ -# Proposed Initiative Next / Agent Handoff UX Evidence - -## Source - -This discussion item came from the GSD workspace comparison. - -GSD's useful lesson was not its storage model. It was the simple user loop: -create context, move to the next concrete step, and keep the agent from guessing -where it is in the workflow. - -OpenSpec should keep the current boundary: - -```text -Context stores sync truth. -Initiatives coordinate work. -Workspaces open local views. -Changes implement repo-owned slices. -``` - -The possible gap is that `initiative show`, repo-local change linking, and -workspace opening may still require an agent to stitch together the next action -by hand. - -## Current Recommendation - -Keep this as a discussion draft until workspace initiative opening is clearer. -If accepted, the first version should be a small handoff/readiness command, not -status, progress, dashboarding, or workspace orchestration. - -## Manual Beta Pass Addition - -The 2026-05-28 manual beta pass found that command-level handoff is not the -only missing layer. A fresh agent also needs a small, tool-readable guide for -how to use OpenSpec at all: - -- inspect context stores, initiatives, workspaces, and repo-local changes before - guessing; -- understand that context stores can be artifact homes outside implementation - repos, not only cross-team coordination spaces; -- understand that repo-local changes own implementation planning when the user - wants artifacts in the repo; -- treat workspaces as local views, not durable planning homes; -- route to narrower OpenSpec workflow skills when available. - -As a temporary beta aid, a manual Codex skill was created at -`.codex/skills/use-openspec/` with references for shared context and artifact -placement. This is not yet productized in the configurator. diff --git a/openspec/initiatives/context-store-and-initiatives/work-items/proposed-initiative-next-agent-handoff-ux/plan.md b/openspec/initiatives/context-store-and-initiatives/work-items/proposed-initiative-next-agent-handoff-ux/plan.md deleted file mode 100644 index ca7b13b24b..0000000000 --- a/openspec/initiatives/context-store-and-initiatives/work-items/proposed-initiative-next-agent-handoff-ux/plan.md +++ /dev/null @@ -1,90 +0,0 @@ -# Proposed Initiative Next / Agent Handoff UX - -## Status - -Discussion draft. Not locked into the numbered roadmap yet. - -## Why This Exists - -The GSD workspace comparison highlighted a UX gap: OpenSpec has increasingly -good discovery primitives, but agents still need to infer the next useful step -from several commands. - -The candidate idea is a tiny "what now?" handoff command after initiative -discovery from the current repo or workspace. It should not become a dashboard, -work-progress status view, or replacement for workspace local-view behavior. - -The manual beta pass surfaced a second, related handoff gap: before a command -like `initiative next` exists, a fresh coding agent still needs baseline -OpenSpec literacy. It needs to understand context stores, initiatives, -workspaces, repo-local changes, and where artifacts should live. A small -`use-openspec` skill may be the simplest first slice. - -## Candidate Goal - -Help an agent answer: - -```text -What should I do next for this initiative from the current repo or workspace? -``` - -## Possible Command Shape - -```bash -openspec initiative next <id> --json -``` - -Possible response: - -```json -{ - "initiative": "billing-launch", - "next_action": "create_repo_change", - "reason": "initiative found, no linked local change exists for this repo", - "suggested_command": "openspec new change add-billing-api --initiative billing-launch" -} -``` - -## Possible Skill Shape - -```text -use-openspec/ - SKILL.md - references/ - shared-context-beta.md - artifact-placement.md -``` - -This would be a baseline guide skill, not a workflow action. It should not -produce `/opsx:use-openspec`, should not appear as an implementation workflow, -and should not imply that workflow command delivery is unavailable. - -Open design question: whether this is literally part of the default profile, a -separate always-on bundled skill, or a managed guide skill installed by default -whenever the selected agent supports skills. - -## Discussion Points To Review - -- Should this become a numbered roadmap item before workspace initiative - opening? -- Is `initiative next` the right command name, or should this guidance live - inside workspace initiative opening or repo-local status? -- Should the command suggest exactly one next action, or return a ranked set of - possible actions? -- Should it inspect actual work progress, or stay limited to handoff readiness? -- How should it behave when no stores are registered, the initiative is - ambiguous, the local repo is unrelated, or linked changes already exist? -- Should baseline OpenSpec guidance be modeled as a default skill, a profile - member, or a separate managed guide? -- How should the guide skill interact with commands-oriented delivery? -- How should it teach artifact placement: context-store initiative vs - repo-local change vs workspace view? - -## Boundaries - -- Do not add progress/status semantics in the first version. -- Do not create changes, clone repos, or mutate workspace state. -- Do not make workspace opening a prerequisite. -- Prefer agent-readable JSON over broad interactive UX in the first slice. -- Do not turn baseline guidance into a new slash command unless a separate - workflow need emerges. diff --git a/openspec/initiatives/context-store-and-initiatives/work-items/proposed-initiative-next-agent-handoff-ux/tasks.md b/openspec/initiatives/context-store-and-initiatives/work-items/proposed-initiative-next-agent-handoff-ux/tasks.md deleted file mode 100644 index 188dbe726d..0000000000 --- a/openspec/initiatives/context-store-and-initiatives/work-items/proposed-initiative-next-agent-handoff-ux/tasks.md +++ /dev/null @@ -1,18 +0,0 @@ -# Proposed Initiative Next / Agent Handoff UX Tasks - -These are discussion tasks only. Do not implement until the roadmap position and -scope are confirmed. - -- [ ] Decide whether to add this as a numbered roadmap item. -- [ ] Decide whether the command is `initiative next`, workspace initiative - opening guidance, or repo-local status guidance. -- [ ] Decide the minimal JSON output contract for agent handoff. -- [ ] Decide whether the command returns one next action or multiple options. -- [ ] Decide the error and empty-state behavior. -- [ ] Decide whether actual work progress/status is explicitly out of scope. -- [ ] Decide whether to ship `use-openspec` as a managed default skill. -- [ ] Decide whether `use-openspec` is a default-profile member or a separate - always-on guide skill. -- [ ] Decide how `use-openspec` interacts with commands-oriented delivery. -- [ ] Decide the minimal artifact-placement guidance for context-store - initiatives, repo-local changes, and workspace views. diff --git a/schemas/requirements/instructions/proposal.md b/schemas/requirements/instructions/proposal.md new file mode 100644 index 0000000000..0bd121d0aa --- /dev/null +++ b/schemas/requirements/instructions/proposal.md @@ -0,0 +1,27 @@ +Create the proposal that establishes WHAT is changing and WHY, in plain +language a non-engineer can review. Do NOT describe implementation — no API +names, wire formats, or code structure. That belongs to the repos that will +serve this change. + +Sections: +- **Why**: 1-3 sentences on the problem or opportunity. Who benefits? Why now? +- **What Changes**: bullet list. Be specific about new capabilities, + modifications, or removals. Mark breaking changes with **BREAKING**. +- **Capabilities**: which specs this creates or modifies: + - **New Capabilities**: each becomes a new `specs/<name>/spec.md`. + Use kebab-case names (e.g., `user-auth`, `data-export`). + - **Modified Capabilities**: existing capabilities whose REQUIREMENTS + change. Check `openspec/specs/` for existing names. Leave empty if none. +- **Non-goals**: what this deliberately does NOT include or defers. The + explicit deferral list is the single best guard against downstream work + building the wrong thing. +- **Success signals**: 1-3 measurable outcomes — numbers and time bounds, + not adjectives. +- **Impact**: affected systems, teams, or downstream repos. + +IMPORTANT: The Capabilities section is the contract with the specs phase — +research existing specs before filling it in. Each capability listed needs a +corresponding spec file. + +Keep it to 1-2 pages. Do NOT proceed to specs until the proposal has been +reviewed and approved. diff --git a/schemas/requirements/instructions/specs.md b/schemas/requirements/instructions/specs.md new file mode 100644 index 0000000000..d141565553 --- /dev/null +++ b/schemas/requirements/instructions/specs.md @@ -0,0 +1,29 @@ +Create specification files that define WHAT should be true — testable +requirements with scenarios, written from the actor's perspective. Cover the +primary success path AND meaningful edge/failure cases. + +Create one spec file per capability listed in the proposal's Capabilities +section: +- New capabilities: `specs/<capability>/spec.md` with the exact kebab-case + name from the proposal. +- Modified capabilities: use the existing folder name from + `openspec/specs/<capability>/`. + +Delta operations (use `##` headers): **ADDED Requirements**, **MODIFIED +Requirements** (MUST include the full updated requirement block), **REMOVED +Requirements** (MUST include **Reason** and **Migration**), **RENAMED +Requirements** (FROM:/TO:). + +Format: +- Each requirement: `### Requirement: <name>`, SHALL/MUST language. +- Each scenario: `#### Scenario: <name>` with GIVEN/WHEN/THEN. +- **CRITICAL**: scenarios use exactly 4 hashtags (`####`); every requirement + needs at least one scenario. + +Specs should be testable — each scenario is a potential acceptance check for +the downstream work that serves this change. Every requirement MUST trace to +something the proposal states; a requirement that traces to nothing is scope +creep — flag it instead of writing it. + +This is the final artifact: there is no design or tasks phase here. Once the +specs are approved, archive the change to sync them into `specs/`. diff --git a/schemas/requirements/schema.yaml b/schemas/requirements/schema.yaml new file mode 100644 index 0000000000..358fe31c1a --- /dev/null +++ b/schemas/requirements/schema.yaml @@ -0,0 +1,21 @@ +name: requirements +version: 1 +description: Documentation-only workflow — proposal → specs. Draft and agree on requirements here; implementation happens in the repos that serve this change. +notes: > + This workflow has no implementation phase: there is no design.md or + tasks.md, and nothing to apply. When the specs are approved, archive the + change to sync its delta specs into specs/ — those become the standing + truths that downstream work traces against. Repos implement this change + by linking to it: openspec new change <name> --serves <store-id>/<change>. +artifacts: + - id: proposal + generates: proposal.md + description: Bounded statement of what is changing and why, in plain language + template: proposal.md + requires: [] + - id: specs + generates: specs/**/*.md + description: Testable requirements with scenarios, one spec per capability + template: spec.md + requires: + - proposal diff --git a/schemas/requirements/templates/proposal.md b/schemas/requirements/templates/proposal.md new file mode 100644 index 0000000000..503f23da0a --- /dev/null +++ b/schemas/requirements/templates/proposal.md @@ -0,0 +1,31 @@ +## Why + +<!-- Explain the motivation for this change. What problem does this solve? Why now? --> + +## What Changes + +<!-- Describe what will change. Be specific about new capabilities, modifications, or removals. --> + +## Capabilities + +### New Capabilities +<!-- Capabilities being introduced. Replace <name> with kebab-case identifier (e.g., user-auth, data-export, api-rate-limiting). Each creates specs/<name>/spec.md --> +- `<name>`: <brief description of what this capability covers> + +### Modified Capabilities +<!-- Existing capabilities whose REQUIREMENTS are changing (not just implementation). + Only list here if spec-level behavior changes. Each needs a delta spec file. + Use existing spec names from openspec/specs/. Leave empty if no requirement changes. --> +- `<existing-name>`: <what requirement is changing> + +## Non-goals + +<!-- What this deliberately does not include or defers --> + +## Success Signals + +<!-- 1-3 measurable outcomes: numbers and time bounds, not adjectives --> + +## Impact + +<!-- Affected code, APIs, dependencies, systems --> diff --git a/schemas/requirements/templates/spec.md b/schemas/requirements/templates/spec.md new file mode 100644 index 0000000000..095d711c8f --- /dev/null +++ b/schemas/requirements/templates/spec.md @@ -0,0 +1,8 @@ +## ADDED Requirements + +### Requirement: <!-- requirement name --> +<!-- requirement text --> + +#### Scenario: <!-- scenario name --> +- **WHEN** <!-- condition --> +- **THEN** <!-- expected outcome --> diff --git a/src/cli/index.ts b/src/cli/index.ts index 98505b02fe..38bc5f05bc 100644 --- a/src/cli/index.ts +++ b/src/cli/index.ts @@ -8,9 +8,15 @@ import { promises as fs } from 'fs'; import { AI_TOOLS } from '../core/config.js'; import { UpdateCommand } from '../core/update.js'; import { ListCommand } from '../core/list.js'; +import { + expandScanDirs, + rollupDownstream, + rollupRegisteredStores, + type DownstreamRollup, +} from '../core/upstream.js'; import { ArchiveCommand, type ArchiveOptions } from '../core/archive.js'; import { ViewCommand } from '../core/view.js'; -import { resolveRootForCommand, toRootOutput } from '../core/root-selection.js'; +import { resolveRootForCommand, toRootOutput, isStoreSelectedRoot, type ResolvedOpenSpecRoot } from '../core/root-selection.js'; import { registerSpecCommand } from '../commands/spec.js'; import { ChangeCommand } from '../commands/change.js'; import { ValidateCommand } from '../commands/validate.js'; @@ -53,6 +59,133 @@ function hiddenStorePathOption(): Option { ).hideHelp(); } +function printRollupBody( + rollup: DownstreamRollup, + indent: string, + // Store rollups need the store-qualified ref in the link hint — a bare + // <change> would not resolve from a consumer repo. + refPrefix = '' +): void { + if (rollup.upstream.length === 0) { + // The empty state must teach the whole move: a first-time user with a + // fresh store has no other way to learn it from the CLI. + console.log(`${indent}(no changes yet — upstream work is just a change here:`); + console.log( + `${indent} openspec new change <name>${refPrefix ? ` --store ${refPrefix.slice(0, -1)}` : ''}, then link work to it from any repo` + ); + console.log( + `${indent} with: openspec new change <name> --serves ${refPrefix}<change>)` + ); + return; + } + + let anyServing = false; + for (const entry of rollup.upstream) { + const summary = + entry.changesTotal === 0 + ? 'nothing serves it yet' + : `${entry.changesComplete}/${entry.changesTotal} serving changes complete`; + const marker = !entry.exists + ? ' (no such change)' + : entry.archived + ? ' (archived — its requirements now live in specs/)' + : ''; + console.log(''); + console.log(`${indent}${entry.id} ${summary}${marker}`); + if (entry.changes.length === 0) continue; + anyServing = true; + const changeWidth = Math.max(...entry.changes.map((c) => c.id.length)); + for (const change of entry.changes) { + const mark = + change.state === 'complete' ? '✓' : change.state === 'no-tasks' ? '–' : '·'; + const where = change.store ?? change.repo ?? 'here'; + const artifactsKnown = change.totalArtifacts !== undefined; + const artifactsDone = + artifactsKnown && change.completedArtifacts === change.totalArtifacts; + let progressText = + change.totalTasks === 0 + ? artifactsKnown + ? `${change.completedArtifacts}/${change.totalArtifacts} artifacts` + : 'no tasks' + : `${change.completedTasks}/${change.totalTasks} tasks`; + // Checked-off tasks with missing artifacts is the half-done state + // that must not read as done — surface both numbers. + if (change.totalTasks > 0 && artifactsKnown && !artifactsDone) { + progressText += ` (${change.completedArtifacts}/${change.totalArtifacts} artifacts)`; + } + console.log( + `${indent} ${mark} ${change.id.padEnd(changeWidth)} ${where.padEnd(14)} ${progressText}` + ); + } + // Truth flows up: upstream work whose serving changes are all complete + // is ready to archive, which syncs its requirements into specs. + if (entry.changesTotal > 0 && entry.changesComplete === entry.changesTotal && !entry.archived) { + console.log( + `${indent} all serving changes complete — archive it to sync its specs` + ); + } + } + if (!anyServing) { + console.log(''); + console.log( + `${indent}Link work to a change here: openspec new change <name> --serves ${refPrefix}<change>` + ); + } +} + +async function renderDownstream( + root: ResolvedOpenSpecRoot, + options: { json?: boolean; scan?: string[] } +): Promise<void> { + const downstream = await rollupDownstream(root.path, { + ...(options.scan && options.scan.length > 0 + ? { extraRoots: await expandScanDirs(options.scan) } + : {}), + }); + + if (options.json) { + console.log( + JSON.stringify({ downstream, root: toRootOutput(root) }, null, 2) + ); + return; + } + + if (downstream === null) { + console.log('No changes folder found at openspec/changes/.'); + console.log('Create the first change: openspec new change <name>'); + return; + } + + console.log(`Downstream rollup: ${downstream.path}`); + printRollupBody(downstream, '', isStoreSelectedRoot(root) ? `${root.storeId}/` : ''); +} + +/** + * Outside any root, the rollup question is still answerable: show the + * downstream work of every registered store that has changes. + */ +async function renderStoreRollups(options: { json?: boolean }): Promise<void> { + const stores = await rollupRegisteredStores(); + + if (options.json) { + console.log(JSON.stringify({ downstream: null, stores, root: null }, null, 2)); + return; + } + + if (stores.length === 0) { + console.log('No local OpenSpec root, and no registered store has changes.'); + console.log('Run inside a repo, or pass --store <id>.'); + return; + } + + console.log('No local OpenSpec root — showing registered store rollups.'); + for (const { store, rollup } of stores) { + console.log(''); + console.log(`${store} (${rollup.path})`); + printRollupBody(rollup, ' ', `${store}/`); + } +} + function failWithError( error: unknown, json?: { enabled: boolean | undefined; payload?: Record<string, unknown>; fallbackCode?: string } @@ -214,22 +347,59 @@ program program .command('list') - .description('List items (changes by default). Use --specs to list specs.') + .description('List items (changes by default). Use --specs to list specs, --downstream to see the upstream rollup.') .option('--specs', 'List specs instead of changes') .option('--changes', 'List changes explicitly (default)') + .option('--downstream', "Show the root's changes and every change on this machine that serves them") + .option( + '--scan <dir>', + 'With --downstream: also scan this directory (and its immediate subdirectories) for serving changes — no registration needed (repeatable)', + (value: string, previous: string[]) => [...previous, value], + [] as string[] + ) .option('--sort <order>', 'Sort order: "recent" (default) or "name"', 'recent') .option('--json', 'Output as JSON (for programmatic use)') .option('--store <id>', STORE_OPTION_DESCRIPTION) .addOption(hiddenStorePathOption()) - .action(async (options?: { specs?: boolean; changes?: boolean; sort?: string; json?: boolean; store?: string; storePath?: string }) => { + .action(async (options?: { specs?: boolean; changes?: boolean; downstream?: boolean; scan?: string[]; sort?: string; json?: boolean; store?: string; storePath?: string }) => { try { + const failurePayload = options?.downstream + ? { downstream: null, root: null } + : options?.specs + ? { specs: [], root: null } + : { changes: [], root: null }; + // The rollup question is the one `list` answer that still makes + // sense outside any root: fall back to registered store rollups + // instead of failing root resolution. + if ( + options?.downstream && + options.store === undefined && + options.storePath === undefined + ) { + try { + const root = await resolveRootForCommand(options ?? {}); + if (root) { + await renderDownstream(root, { json: options?.json, scan: options?.scan }); + } + return; + } catch (error) { + const code = (error as { diagnostic?: { code?: string } }).diagnostic?.code; + if (code !== 'no_root_with_registered_stores') throw error; + await renderStoreRollups({ json: options?.json }); + return; + } + } const root = await resolveRootForCommand(options ?? {}, { json: options?.json, - failurePayload: options?.specs ? { specs: [], root: null } : { changes: [], root: null }, + failurePayload, }); if (!root) { return; } + if (options?.downstream) { + await renderDownstream(root, { json: options?.json, scan: options?.scan }); + return; + } const listCommand = new ListCommand(); const mode: 'changes' | 'specs' = options?.specs ? 'specs' : 'changes'; const sort = options?.sort === 'name' ? 'name' : 'recent'; @@ -241,7 +411,11 @@ program } catch (error) { failWithError(error, { enabled: options?.json, - payload: options?.specs ? { specs: [], root: null } : { changes: [], root: null }, + payload: options?.downstream + ? { downstream: null, root: null } + : options?.specs + ? { specs: [], root: null } + : { changes: [], root: null }, fallbackCode: 'list_error', }); process.exit(1); @@ -544,6 +718,8 @@ program .command('schemas') .description('List available workflow schemas with descriptions') .option('--json', 'Output as JSON (for agent use)') + .option('--store <id>', STORE_OPTION_DESCRIPTION) + .addOption(hiddenStorePathOption()) .action(async (options: SchemasOptions) => { try { await schemasCommand(options); @@ -562,6 +738,7 @@ newCmd .option('--description <text>', 'Description to add to README.md') .option('--goal <text>', 'Optional goal metadata to store with the change') .option('--schema <name>', `Workflow schema to use (default: ${DEFAULT_SCHEMA})`) + .option('--serves <ref>', 'Link this change to the work it serves: <change> or <store-id>/<change>') .option('--json', 'Output as JSON') .option('--store <id>', STORE_OPTION_DESCRIPTION) .addOption(hiddenStorePathOption()) diff --git a/src/commands/context.ts b/src/commands/context.ts index 1a4b4a8312..14a687eeb4 100644 --- a/src/commands/context.ts +++ b/src/commands/context.ts @@ -26,6 +26,10 @@ import { COMMAND_REGISTRY } from '../core/completions/command-registry.js'; import { COMMON_FLAGS } from '../core/completions/shared-flags.js'; import { emitFailure, printJson } from './shared-output.js'; import { gatherRelationshipData } from './shared-gather.js'; +import { listSchemasWithInfo } from '../core/artifact-graph/index.js'; +import { listActiveChangeIds } from '../core/upstream.js'; +import { readProjectConfig } from '../core/project-config.js'; +import { schemasFetchRecipe, changesFetchRecipe } from '../core/references.js'; const FAILURE_PAYLOAD = { root: null, members: [] }; @@ -59,6 +63,52 @@ function memberLine(member: WorkingSetMember): string { return ` ${member.id} ${member.path}`; } +/** + * Enrich available referenced-store members with the store's own custom + * artifact types and in-motion changes, so `context` reports what a repo + * draws on beyond specs. Read-only; failures degrade to an unenriched member. + */ +async function enrichMembersWithStoreArtifacts( + workingSet: WorkingSet +): Promise<void> { + for (const member of workingSet.members) { + if (member.role !== 'referenced_store' || !isAvailableMember(member)) { + continue; + } + const storeRoot = member.path as string; + try { + const artifactTypes = listSchemasWithInfo(storeRoot) + .filter((schema) => schema.source === 'project') + .map((schema) => schema.name); + if (artifactTypes.length > 0) { + member.artifactTypes = artifactTypes; + } + } catch { + // Unreadable schemas dir: leave the member unenriched. + } + try { + // In-motion change ids: specs say what is true; these say what the + // team there is currently deciding. + const changeIds = await listActiveChangeIds(storeRoot); + if (changeIds.length > 0) { + member.changes = changeIds; + } + } catch { + // Unreadable changes dir: leave the member unenriched. + } + try { + // The store's declared layout (config `structure:`), so agents know + // what its non-reserved folders are for without spelunking. + const structure = readProjectConfig(storeRoot)?.structure; + if (structure && Object.keys(structure).length > 0) { + member.structure = structure; + } + } catch { + // Unreadable config: leave the member unenriched. + } + } +} + function printHumanWorkingSet(workingSet: WorkingSet, declaredReferenceCount: number): void { const rootLabel = workingSet.root.store_id ?? path.basename(workingSet.root.path); console.log(`Working context for ${rootLabel} (${workingSet.root.path})`); @@ -79,6 +129,21 @@ function printHumanWorkingSet(workingSet: WorkingSet, declaredReferenceCount: nu if (member.fetch) { console.log(` Fetch: ${member.fetch}`); } + if (member.artifactTypes && member.artifactTypes.length > 0) { + console.log( + ` Artifact types: ${member.artifactTypes.join(', ')} (${schemasFetchRecipe(member.id)})` + ); + } + if (member.changes && member.changes.length > 0) { + console.log( + ` In motion: ${member.changes.join(', ')} (${changesFetchRecipe(member.id)})` + ); + } + if (member.structure && Object.keys(member.structure).length > 0) { + for (const [folder, purpose] of Object.entries(member.structure)) { + console.log(` Layout: ${folder} — ${purpose}`); + } + } } } @@ -190,6 +255,7 @@ export function registerContextCommand(program: Command): void { } const { workingSet, declaredReferenceCount } = await gatherWorkingSet(root); + await enrichMembersWithStoreArtifacts(workingSet); if (options.json) { // The write runs FIRST: a write failure must leave stdout diff --git a/src/commands/schema.ts b/src/commands/schema.ts index 7f8d0b7888..245b021e18 100644 --- a/src/commands/schema.ts +++ b/src/commands/schema.ts @@ -9,7 +9,9 @@ import { getUserSchemasDir, getPackageSchemasDir, listSchemas, + resolveSchema, } from '../core/artifact-graph/resolver.js'; +import { resolveRootForCommand } from '../core/root-selection.js'; import { parseSchema, SchemaValidationError } from '../core/artifact-graph/schema.js'; import type { SchemaYaml, Artifact } from '../core/artifact-graph/types.js'; @@ -303,9 +305,18 @@ export function registerSchemaCommand(program: Command): void { .description('Show where a schema resolves from') .option('--json', 'Output as JSON') .option('--all', 'List all schemas with their resolution sources') - .action(async (name?: string, options?: { json?: boolean; all?: boolean }) => { + .option('--store <id>', 'Store id to use as the OpenSpec root (a store is a standalone OpenSpec repo you\'ve registered)') + .action(async (name?: string, options?: { json?: boolean; all?: boolean; store?: string }) => { try { - const projectRoot = process.cwd(); + let projectRoot = process.cwd(); + if (options?.store) { + const root = await resolveRootForCommand( + { store: options.store }, + { json: options?.json } + ); + if (!root) return; + projectRoot = root.path; + } if (options?.all) { // List all schemas @@ -405,9 +416,18 @@ export function registerSchemaCommand(program: Command): void { .description('Validate a schema structure and templates') .option('--json', 'Output as JSON') .option('--verbose', 'Show detailed validation steps') - .action(async (name?: string, options?: { json?: boolean; verbose?: boolean }) => { + .option('--store <id>', 'Store id to use as the OpenSpec root (a store is a standalone OpenSpec repo you\'ve registered)') + .action(async (name?: string, options?: { json?: boolean; verbose?: boolean; store?: string }) => { try { - const projectRoot = process.cwd(); + let projectRoot = process.cwd(); + if (options?.store) { + const root = await resolveRootForCommand( + { store: options.store }, + { json: options?.json } + ); + if (!root) return; + projectRoot = root.path; + } if (!name) { // Validate all project schemas @@ -667,10 +687,11 @@ export function registerSchemaCommand(program: Command): void { .description('Create a new project-local schema') .option('--json', 'Output as JSON') .option('--description <text>', 'Schema description') - .option('--artifacts <list>', 'Comma-separated artifact IDs (proposal,specs,design,tasks)') + .option('--artifacts <list>', 'Comma-separated stage ids, in workflow order (built-in or custom kebab-case names)') .option('--default', 'Set as project default schema') .option('--no-default', 'Do not prompt to set as default') .option('--force', 'Overwrite existing schema') + .option('--store <id>', 'Store id to use as the OpenSpec root (a store is a standalone OpenSpec repo you\'ve registered)') .action(async ( name: string, options?: { @@ -679,12 +700,23 @@ export function registerSchemaCommand(program: Command): void { artifacts?: string; default?: boolean; force?: boolean; + store?: string; } ) => { const spinner = options?.json ? null : ora(); try { - const projectRoot = process.cwd(); + // --store scaffolds into a registered store's root; without it the + // command keeps its classic cwd behavior. + let projectRoot = process.cwd(); + if (options?.store) { + const root = await resolveRootForCommand( + { store: options.store }, + { json: options?.json, failurePayload: { created: false } } + ); + if (!root) return; + projectRoot = root.path; + } // Validate name if (!isValidSchemaName(name)) { @@ -776,19 +808,19 @@ export function registerSchemaCommand(program: Command): void { if (options?.artifacts) { selectedArtifactIds = options.artifacts.split(',').map((a) => a.trim()); - // Validate artifact IDs - const validIds = DEFAULT_ARTIFACTS.map((a) => a.id); + // Any kebab-case id is a valid stage — custom names are the + // point of a custom workflow. Built-in ids (proposal, specs, + // design, tasks) get richer seeded content. for (const id of selectedArtifactIds) { - if (!validIds.includes(id)) { + if (!isValidSchemaName(id)) { if (options?.json) { console.log(JSON.stringify({ created: false, - error: `Unknown artifact '${id}'`, - valid: validIds, + error: `Invalid artifact id '${id}' — use kebab-case (e.g. design-note)`, }, null, 2)); } else { - console.error(`Error: Unknown artifact '${id}'`); - console.error(`Valid artifacts: ${validIds.join(', ')}`); + console.error(`Error: Invalid artifact id '${id}'`); + console.error('Artifact ids must be kebab-case (e.g. design-note)'); } process.exitCode = 1; return; @@ -804,29 +836,29 @@ export function registerSchemaCommand(program: Command): void { if (spinner) spinner.start(`Creating schema '${name}'...`); fs.mkdirSync(schemaDir, { recursive: true }); - // Build artifacts array with proper dependencies - const selectedArtifacts = selectedArtifactIds.map((id) => { - const template = DEFAULT_ARTIFACTS.find((a) => a.id === id)!; - const artifact: Artifact = { - id: template.id, - generates: template.generates, - description: template.description, - template: template.template, - requires: [], - }; - - // Set up dependencies based on typical workflow - if (id === 'specs' && selectedArtifactIds.includes('proposal')) { - artifact.requires = ['proposal']; - } else if (id === 'design' && selectedArtifactIds.includes('specs')) { - artifact.requires = ['specs']; - } else if (id === 'tasks') { - const requires: string[] = []; - if (selectedArtifactIds.includes('design')) requires.push('design'); - else if (selectedArtifactIds.includes('specs')) requires.push('specs'); - artifact.requires = requires; + // Build the artifacts as a sequential chain in the order given — + // the order you pass IS your workflow. Built-in ids keep their + // known shapes; custom ids get a generic single-file shape. + const selectedArtifacts = selectedArtifactIds.map((id, index) => { + const known = DEFAULT_ARTIFACTS.find((a) => a.id === id); + const artifact: Artifact = known + ? { + id: known.id, + generates: known.generates, + description: known.description, + template: known.template, + requires: [], + } + : { + id, + generates: `${id}.md`, + description: `The ${id} stage`, + template: `${id}.md`, + requires: [], + }; + if (index > 0) { + artifact.requires = [selectedArtifactIds[index - 1]]; } - return artifact; }); @@ -861,11 +893,29 @@ export function registerSchemaCommand(program: Command): void { fs.mkdirSync(templateDir, { recursive: true }); } - // Create default template content + // Create default template content (custom stages get a minimal + // heading to replace) const templateContent = createDefaultTemplate(artifact.id); fs.writeFileSync(templatePath, templateContent); } + // Seed per-stage guidance as instructions/<artifact>.md files — + // the built-in workflow's guidance is the starting point, and a + // file beside the schema is what teams edit (it wins over any + // inline instruction). + const builtin = resolveSchema('spec-driven'); + const instructionsDir = path.join(schemaDir, 'instructions'); + fs.mkdirSync(instructionsDir, { recursive: true }); + for (const artifact of selectedArtifacts) { + const seed = + builtin.artifacts.find((a) => a.id === artifact.id)?.instruction ?? + `Write the ${artifact.id} stage.\n\n<replace with your guidance: required sections, what belongs here vs. later stages, and any review gate — e.g. "Do NOT proceed to the next stage until this is approved.">`; + fs.writeFileSync( + path.join(instructionsDir, `${artifact.id}.md`), + `${seed.trim()}\n` + ); + } + // Update config if --default if (options?.default) { const configPath = path.join(projectRoot, 'openspec', 'config.yaml'); @@ -874,7 +924,7 @@ export function registerSchemaCommand(program: Command): void { const { parse: parseYaml, stringify: stringifyYaml2 } = await import('yaml'); const configContent = fs.readFileSync(configPath, 'utf-8'); const config = parseYaml(configContent) || {}; - config.defaultSchema = name; + config.schema = name; fs.writeFileSync(configPath, stringifyYaml2(config)); } else { // Create config file @@ -882,7 +932,7 @@ export function registerSchemaCommand(program: Command): void { if (!fs.existsSync(configDir)) { fs.mkdirSync(configDir, { recursive: true }); } - fs.writeFileSync(configPath, stringifyYaml({ defaultSchema: name })); + fs.writeFileSync(configPath, stringifyYaml({ schema: name })); } } @@ -902,10 +952,14 @@ export function registerSchemaCommand(program: Command): void { if (options?.default) { console.log(`\nSet as project default schema.`); } + console.log(`\nEach artifact is a stage — order them with requires: in schema.yaml.`); + console.log(`Guidance agents get per stage lives in instructions/<artifact>.md.`); console.log(`\nNext steps:`); - console.log(` 1. Edit ${schemaDir}/schema.yaml to customize artifacts`); - console.log(` 2. Modify templates in the schema directory`); - console.log(` 3. Use with: openspec new --schema ${name}`); + console.log(` 1. Edit ${schemaDir}/schema.yaml to customize the stages`); + console.log(` 2. Edit instructions/ and templates/ to match your workflow`); + console.log( + ` 3. Use it: openspec new change <name> --schema ${name}${options?.store ? ` --store ${options.store}` : ''}` + ); } } catch (error) { if (spinner) spinner.fail(`Creation failed`); diff --git a/src/commands/store.ts b/src/commands/store.ts index 1a91d89984..6d9fba82f8 100644 --- a/src/commands/store.ts +++ b/src/commands/store.ts @@ -269,25 +269,18 @@ async function resolveSetupInput( ); } - if (options.path === undefined && !interactive) { - throw new StoreError( - 'Pass --path with the folder where this store should live.', - 'store_setup_path_required', - { - target: 'store.root', - fix: `openspec store setup ${id ?? '<id>'} --path ~/openspec/${id ?? '<id>'}`, - } - ); - } - const resolvedId = id ? validateStoreId(id) : await promptStoreId(); - const promptedPath = options.path === undefined - ? await promptStorePath(resolvedId) - : undefined; + // No --path needed: interactive prompts with ~/openspec/<id> prefilled; + // non-interactive defaults to the same place, so setup is one command. + const resolvedPath = + options.path ?? + (interactive + ? await promptStorePath(resolvedId) + : ['~', 'openspec', resolvedId].join('/')); return { id: resolvedId, - path: options.path ?? promptedPath, + path: resolvedPath, ...(options.remote !== undefined ? { remote: options.remote } : {}), }; } @@ -405,8 +398,10 @@ function printMutationHuman( console.log(`${status.severity === 'error' ? 'Issue' : 'Note'}: ${status.message}`); } console.log(''); - console.log('Next: run normal OpenSpec commands against this store, for example:'); - console.log(` openspec new change <change-id> --store ${payload.store.id}`); + console.log('Next:'); + console.log(` openspec new change <name> --store ${payload.store.id} # draft requirements here`); + console.log(` openspec new change <name> --serves ${payload.store.id}/<change> # in any repo: link work to them`); + console.log(` openspec list --downstream --store ${payload.store.id} # where everything stands`); if (payload.git.is_repository) { const shareRemote = remotes?.canonical ?? remotes?.observed; console.log( @@ -673,7 +668,7 @@ export function registerStoreCommand(program: Command): void { .option('--path <path>', 'Folder where the store should live (for example ~/openspec/<id>)') .option('--init-git', 'Initialize a Git repository with an initial commit (default)') .option('--no-init-git', 'Skip every Git action: no init, no initial commit') - .option('--remote <url>', 'Canonical clone source recorded in store.yaml') + .option('--remote <url>', 'Canonical clone source recorded in .openspec-store/store.yaml') .option('--json', 'Output as JSON') .action(async (id: string | undefined, options: StoreSetupOptions) => { await storeCommand.setup(id, options); diff --git a/src/commands/workflow/instructions.ts b/src/commands/workflow/instructions.ts index 10a5fac166..434da6871c 100644 --- a/src/commands/workflow/instructions.ts +++ b/src/commands/workflow/instructions.ts @@ -35,6 +35,10 @@ import { } from '../../core/references.js'; import { readRegistrySnapshot } from '../../core/store/registry.js'; import { readProjectConfig, type ProjectConfig } from '../../core/project-config.js'; +import { + resolveUpstreamLink, + type ResolvedUpstreamLink, +} from '../../core/upstream.js'; import { validateChangeExists, validateSchemaExists, @@ -97,6 +101,44 @@ async function loadRootConfigContext(root: ResolvedOpenSpecRoot): Promise<{ }; } +/** + * The upward join: a linked change's instructions hand the agent the + * upstream change it serves and where that context lives on disk, so + * intent reaches the working agent without anyone pasting it. + */ +function renderUpstreamBlock(link: ResolvedUpstreamLink): string { + const lines = [ + `<upstream ref="${link.ref}">`, + `This change serves the change '${link.changeId}'${link.store ? ` in store '${link.store}'` : ''}.`, + ]; + if (link.path && link.archived) { + lines.push(`Upstream context (archived): ${link.path}`); + lines.push( + "The upstream change has been archived — its requirements were synced into that root's openspec/specs/, which is now the standing truth to trace against." + ); + lines.push( + 'If implementation shows a synced requirement is wrong or incomplete, do not silently diverge — propose a change against that spec upstream so the divergence is recorded before it becomes drift.' + ); + } else if (link.path) { + lines.push(`Upstream context: ${link.path}`); + lines.push( + "Before working, read its artifacts (proposal, specs, and any others its schema defines); this change should trace to a requirement there. Standing truths live beside it in that root's openspec/specs/." + ); + lines.push( + 'If implementation shows an upstream requirement is wrong or incomplete, do not silently diverge — flag it upstream (or edit the upstream change, if that is your team\'s call to make) so intent stays honest.' + ); + } else { + lines.push( + 'The upstream change was not found on disk — the link may be stale, or the store may not be registered on this machine.' + ); + } + lines.push( + `Status rollup: openspec list --downstream${link.store ? ` --store ${link.store}` : ''}` + ); + lines.push('</upstream>'); + return lines.join('\n'); +} + export async function instructionsCommand( artifactId: string | undefined, options: InstructionsOptions @@ -154,22 +196,37 @@ export async function instructionsCommand( references, }); const isBlocked = instructions.dependencies.some((d) => !d.done); + const upstream = await resolveUpstreamLink(context.changeDir, projectRoot); spinner?.stop(); if (options.json) { - console.log(JSON.stringify({ ...instructions, root: toRootOutput(root) }, null, 2)); + console.log( + JSON.stringify( + { + ...instructions, + ...(upstream ? { upstream } : {}), + root: toRootOutput(root), + }, + null, + 2 + ) + ); return; } - printInstructionsText(instructions, isBlocked); + printInstructionsText(instructions, isBlocked, upstream); } catch (error) { spinner?.stop(); throw error; } } -export function printInstructionsText(instructions: ArtifactInstructions, isBlocked: boolean): void { +export function printInstructionsText( + instructions: ArtifactInstructions, + isBlocked: boolean, + upstream?: ResolvedUpstreamLink | null +): void { const { artifactId, changeName, @@ -206,6 +263,15 @@ export function printInstructionsText(instructions: ArtifactInstructions, isBloc console.log('</task>'); console.log(); + // Schema-level workflow guidance (the schema author speaking to agents) + if (instructions.schemaNotes) { + console.log('<schema_notes>'); + console.log('<!-- How this schema\'s workflow differs from the default. Follow it. -->'); + console.log(instructions.schemaNotes.trim()); + console.log('</schema_notes>'); + console.log(); + } + // Project context (AI constraint - do not include in output) if (context) { console.log('<project_context>'); @@ -215,6 +281,12 @@ export function printInstructionsText(instructions: ArtifactInstructions, isBloc console.log(); } + // The upstream change this change serves (read-only upstream context) + if (upstream) { + console.log(renderUpstreamBlock(upstream)); + console.log(); + } + // Referenced-store index (read-only upstream context) if (instructions.references && instructions.references.length > 0) { console.log(renderReferencedStoresBlock(instructions.references)); @@ -270,12 +342,6 @@ export function printInstructionsText(instructions: ArtifactInstructions, isBloc console.log('</template>'); console.log(); - // Success criteria placeholder - console.log('<success_criteria>'); - console.log('<!-- To be defined in schema validation rules -->'); - console.log('</success_criteria>'); - console.log(); - // Unlocks if (unlocks.length > 0) { console.log('<unlocks>'); @@ -393,19 +459,25 @@ export async function generateApplyInstructions( let state: ApplyInstructions['state']; let instruction: string; + // Blocked-state hints must point at the always-available CLI path — a + // named skill may not be generated under every profile. + const tracksArtifactId = tracksFile + ? (schema.artifacts.find((a) => a.generates === tracksFile)?.id ?? + path.parse(path.basename(tracksFile)).name) + : null; if (missingArtifacts.length > 0) { state = 'blocked'; - instruction = `Cannot apply this change yet. Missing artifacts: ${missingArtifacts.join(', ')}.\nUse the openspec-continue-change skill to create the missing artifacts first.`; + instruction = `Cannot apply this change yet. Missing artifacts: ${missingArtifacts.join(', ')}.\nCreate each one first: openspec instructions <artifact> --change ${changeName}`; } else if (tracksFile && !tracksFileExists) { // Tracking file configured but doesn't exist yet const tracksFilename = path.basename(tracksFile); state = 'blocked'; - instruction = `The ${tracksFilename} file is missing and must be created.\nUse openspec-continue-change to generate the tracking file.`; + instruction = `The ${tracksFilename} file is missing and must be created.\nGenerate it: openspec instructions ${tracksArtifactId} --change ${changeName}`; } else if (tracksFile && tracksFileExists && total === 0) { // Tracking file exists but contains no tasks const tracksFilename = path.basename(tracksFile); state = 'blocked'; - instruction = `The ${tracksFilename} file exists but contains no tasks.\nAdd tasks to ${tracksFilename} or regenerate it with openspec-continue-change.`; + instruction = `The ${tracksFilename} file exists but contains no tasks.\nAdd tasks to ${tracksFilename}, or regenerate it: openspec instructions ${tracksArtifactId} --change ${changeName}`; } else if (tracksFile && remaining === 0 && total > 0) { state = 'all_done'; instruction = 'All tasks are complete! This change is ready to be archived.\nConsider running tests and reviewing the changes before archiving.'; @@ -428,6 +500,7 @@ export async function generateApplyInstructions( state, missingArtifacts: missingArtifacts.length > 0 ? missingArtifacts : undefined, instruction, + ...(schema.notes ? { schemaNotes: schema.notes } : {}), ...(references !== undefined ? { references } : {}), }; } @@ -462,28 +535,56 @@ export async function applyInstructionsCommand(options: ApplyInstructionsOptions planningHome, references, }); + const upstream = await resolveUpstreamLink(instructions.changeDir, projectRoot); spinner?.stop(); if (options.json) { - console.log(JSON.stringify({ ...instructions, root: toRootOutput(root) }, null, 2)); + console.log( + JSON.stringify( + { + ...instructions, + ...(upstream ? { upstream } : {}), + root: toRootOutput(root), + }, + null, + 2 + ) + ); return; } - printApplyInstructionsText(instructions); + printApplyInstructionsText(instructions, upstream); } catch (error) { spinner?.stop(); throw error; } } -export function printApplyInstructionsText(instructions: ApplyInstructions): void { +export function printApplyInstructionsText( + instructions: ApplyInstructions, + upstream?: ResolvedUpstreamLink | null +): void { const { changeName, schemaName, contextFiles, progress, tasks, state, missingArtifacts, instruction } = instructions; console.log(`## Apply: ${changeName}`); console.log(`Schema: ${schemaName}`); console.log(); + // Schema-level workflow guidance (the schema author speaking to agents) + if (instructions.schemaNotes) { + console.log('### Schema Notes'); + console.log(); + console.log(instructions.schemaNotes.trim()); + console.log(); + } + + // The upstream change this change serves (read-only upstream context) + if (upstream) { + console.log(renderUpstreamBlock(upstream)); + console.log(); + } + if (instructions.references && instructions.references.length > 0) { console.log(renderReferencedStoresSection(instructions.references)); console.log(); @@ -494,7 +595,9 @@ export function printApplyInstructionsText(instructions: ApplyInstructions): voi console.log('### ⚠️ Blocked'); console.log(); console.log(`Missing artifacts: ${missingArtifacts.join(', ')}`); - console.log('Use the openspec-continue-change skill to create these first.'); + console.log( + `Create each one first: openspec instructions <artifact> --change ${changeName}` + ); console.log(); } diff --git a/src/commands/workflow/new-change.ts b/src/commands/workflow/new-change.ts index 3e059242dc..2f18e819f0 100644 --- a/src/commands/workflow/new-change.ts +++ b/src/commands/workflow/new-change.ts @@ -2,13 +2,16 @@ * New Change Command * * Creates a new change directory with optional description and schema in the - * resolved OpenSpec root. `--store <id>` selects a registered store's - * root; initiative linking and workspace affected areas are no longer part of - * this command. + * resolved OpenSpec root. `--store <id>` selects a registered store's root; + * `--serves <ref>` links the change upward to the work it serves. Workspace + * affected areas are no longer part of this command. */ import ora from 'ora'; import path from 'path'; +import { ServesRefSchema } from '../../core/change-metadata/schema.js'; +import { recordLinkedRoot, resolveUpstreamLink } from '../../core/upstream.js'; +import { addReferenceToProjectConfig, readProjectConfig } from '../../core/project-config.js'; import { createChange, validateChangeName } from '../../utils/change-utils.js'; import { formatChangeLocation } from '../../core/planning-home.js'; import { @@ -31,6 +34,8 @@ export interface NewChangeOptions { description?: string; goal?: string; schema?: string; + /** Link the change to the work it serves: `<change>` or `<store-id>/<change>`. */ + serves?: string; store?: string; storePath?: string; initiative?: string; @@ -45,6 +50,14 @@ interface NewChangeOutput { metadataPath: string; schema: string; }; + /** Present when the change was born serving upstream work. */ + serves?: { + ref: string; + /** Whether the upstream change was found on disk at link time. */ + resolved: boolean; + /** Whether linking auto-added the store to `references:` in config. */ + reference_wiring?: 'added' | 'already' | 'skipped'; + }; root: RootOutput; } @@ -55,7 +68,7 @@ interface NewChangeOutput { function assertRemovedOptionsAbsent(options: NewChangeOptions): void { if (options.initiative !== undefined) { throw new RootSelectionError( - '--initiative is no longer supported. Normal changes no longer attach to initiatives; --store <id> selects the OpenSpec root.', + '--initiative is no longer supported. Upstream work is a change in its store; link to it with --serves <store-id>/<change>.', 'initiative_option_removed', { target: 'change.options' } ); @@ -82,6 +95,28 @@ function printCreatedChangeHuman( : payload.change.path; console.log(`Created change '${payload.change.id}' at ${location}/`); console.log(`Schema: ${payload.change.schema}`); + if (payload.serves) { + const { ref, resolved, reference_wiring: wiring } = payload.serves; + const storeId = ref.includes('/') ? ref.split('/')[0] : null; + const rollup = storeId + ? `openspec list --downstream --store ${storeId}` + : 'openspec list --downstream'; + console.log(`Serves: ${ref} (rollup: ${rollup})`); + if (!resolved) { + console.log( + `Warning: '${ref}' was not found on this machine — the link will show as missing in rollups until that change exists.` + ); + } + if (wiring === 'added') { + console.log( + `Referenced store '${storeId}' in openspec/config.yaml — agents here now see its context.` + ); + } else if (wiring === 'skipped') { + console.log( + `Tip: add 'references: [${storeId}]' to openspec/config.yaml so agents here see that store's context.` + ); + } + } console.log(`Next: ${withStoreFlag(root, `openspec status --change ${payload.change.id}`)}`); } @@ -100,6 +135,15 @@ export async function newChangeCommand(name: string | undefined, options: NewCha assertRemovedOptionsAbsent(options); + // Validate the ref before anything is created: a bad value must not + // leave a half-written change behind. + if (options.serves !== undefined) { + const ref = ServesRefSchema.safeParse(options.serves); + if (!ref.success) { + throw new Error(ref.error.issues[0].message); + } + } + const root = await resolveRootForCommand(options, { json: options.json, failurePayload: { change: null }, @@ -115,7 +159,16 @@ export async function newChangeCommand(name: string | undefined, options: NewCha validateSchemaExists(options.schema, projectRoot); } - const resolvedSchema = options.schema ?? root.defaultSchema; + // Resolve the schema the same way createChange will (option → config → + // default) so the progress line never names the wrong schema. + let resolvedSchema = options.schema; + if (!resolvedSchema) { + try { + resolvedSchema = readProjectConfig(projectRoot)?.schema ?? root.defaultSchema; + } catch { + resolvedSchema = root.defaultSchema; + } + } if (spinner) { spinner.start(`Creating change '${name}' with schema '${resolvedSchema}'...`); } @@ -126,6 +179,7 @@ export async function newChangeCommand(name: string | undefined, options: NewCha changesDir: root.changesDir, metadata: { ...(options.goal ? { goal: options.goal } : {}), + ...(options.serves ? { serves: options.serves } : {}), }, }); @@ -136,6 +190,31 @@ export async function newChangeCommand(name: string | undefined, options: NewCha await fs.writeFile(readmePath, `# ${name}\n\n${options.description}\n`, 'utf-8'); } + // A store-qualified link makes this repo part of that store's rollup. + // Linking does ALL the wiring: record the checkout so rollups scan it, + // and reference the store so agents here see its context — neither + // failure may fail the created change. + let referenceWiring: 'added' | 'already' | 'skipped' | null = null; + if (options.serves?.includes('/')) { + await recordLinkedRoot(projectRoot).catch(() => undefined); + const storeId = options.serves.split('/')[0]; + try { + referenceWiring = addReferenceToProjectConfig(projectRoot, storeId); + } catch { + referenceWiring = 'skipped'; + } + } + + // Resolve (never block on) the upstream link so a typo is visible at + // creation time instead of surfacing later as a dangling rollup entry. + let servesResolved = false; + if (options.serves) { + const link = await resolveUpstreamLink(result.changeDir, projectRoot).catch( + () => null + ); + servesResolved = link?.path !== null && link !== null; + } + const payload: NewChangeOutput = { change: { id: name, @@ -143,6 +222,15 @@ export async function newChangeCommand(name: string | undefined, options: NewCha metadataPath: path.join(result.changeDir, '.openspec.yaml'), schema: result.schema, }, + ...(options.serves + ? { + serves: { + ref: options.serves, + resolved: servesResolved, + ...(referenceWiring ? { reference_wiring: referenceWiring } : {}), + }, + } + : {}), root: toRootOutput(root), }; diff --git a/src/commands/workflow/schemas.ts b/src/commands/workflow/schemas.ts index b9af74a677..fd2b548600 100644 --- a/src/commands/workflow/schemas.ts +++ b/src/commands/workflow/schemas.ts @@ -6,6 +6,7 @@ import chalk from 'chalk'; import { listSchemasWithInfo } from '../../core/artifact-graph/index.js'; +import { resolveRootForCommand } from '../../core/root-selection.js'; // ----------------------------------------------------------------------------- // Types @@ -13,6 +14,8 @@ import { listSchemasWithInfo } from '../../core/artifact-graph/index.js'; export interface SchemasOptions { json?: boolean; + store?: string; + storePath?: string; } // ----------------------------------------------------------------------------- @@ -20,8 +23,18 @@ export interface SchemasOptions { // ----------------------------------------------------------------------------- export async function schemasCommand(options: SchemasOptions): Promise<void> { - const projectRoot = process.cwd(); - const schemas = listSchemasWithInfo(projectRoot); + // Resolve the OpenSpec root the same way normal commands do, so + // `--store <id>` lists a store's schemas and a repo's own schemas are + // still found when no store is selected. The JSON shape stays a bare + // array (per the agent contract); only which root is read changes. + const root = await resolveRootForCommand(options, { + json: options.json, + failurePayload: {}, + }); + if (!root) { + return; + } + const schemas = listSchemasWithInfo(root.path); if (options.json) { console.log(JSON.stringify(schemas, null, 2)); @@ -35,6 +48,8 @@ export async function schemasCommand(options: SchemasOptions): Promise<void> { let sourceLabel = ''; if (schema.source === 'project') { sourceLabel = chalk.cyan(' (project)'); + } else if (schema.source === 'store') { + sourceLabel = chalk.cyan(` (from store '${schema.store}')`); } else if (schema.source === 'user') { sourceLabel = chalk.dim(' (user override)'); } diff --git a/src/commands/workflow/shared.ts b/src/commands/workflow/shared.ts index f4350a6d9c..c3471f1d37 100644 --- a/src/commands/workflow/shared.ts +++ b/src/commands/workflow/shared.ts @@ -45,6 +45,8 @@ export interface ApplyInstructions { state: 'blocked' | 'all_done' | 'ready'; missingArtifacts?: string[]; instruction: string; + /** Workflow-level guidance from the schema's `notes:` field */ + schemaNotes?: string; /** Referenced-store index (read-only upstream context; omitted when none declared) */ references?: ReferenceIndexEntry[]; } diff --git a/src/commands/workflow/status.ts b/src/commands/workflow/status.ts index 4374744bf5..bd2cca53e3 100644 --- a/src/commands/workflow/status.ts +++ b/src/commands/workflow/status.ts @@ -145,6 +145,11 @@ export function printStatusText(status: ChangeStatus): void { console.log(line); } + if (status.artifacts.some((a) => a.status === 'blocked')) { + console.log(); + console.log(chalk.dim('[x] done · [ ] ready · [-] blocked')); + } + if (status.isComplete) { console.log(); console.log(chalk.green('All artifacts complete!')); diff --git a/src/core/archive.ts b/src/core/archive.ts index 850d4cd5ba..d9d5d96724 100644 --- a/src/core/archive.ts +++ b/src/core/archive.ts @@ -1,6 +1,8 @@ import { promises as fs } from 'fs'; import path from 'path'; import { getTaskProgressForChange, formatTaskStatus } from '../utils/task-progress.js'; +import { resolveSchemaForChange } from '../utils/change-metadata.js'; +import { resolveSchema } from './artifact-graph/resolver.js'; import { Validator } from './validation/validator.js'; import chalk from 'chalk'; import { @@ -345,7 +347,12 @@ export class ArchiveCommand { // Show progress and check for incomplete tasks const progress = await getTaskProgressForChange(changesDir, changeName, path.resolve(changesDir, '..', '..')); if (!json) { - const status = formatTaskStatus(progress); + // "No tasks" reads like a warning when the schema deliberately has + // no tasks phase — say so instead. + const status = + progress.total === 0 && !schemaTracksTasks(path.join(changesDir, changeName)) + ? 'not tracked by this schema' + : formatTaskStatus(progress); console.log(`Task status: ${status}`); } @@ -569,3 +576,23 @@ export class ArchiveCommand { return new Date().toISOString().split('T')[0]; } } + +/** + * Whether the change's workflow schema has a tasks phase at all — an + * artifact generating tasks.md or an apply phase tracking one. Tolerant: + * unresolvable schemas count as tracking, so phrasing can only soften for + * schemas we can actually read. + */ +function schemaTracksTasks(changeDir: string): boolean { + try { + const projectRoot = path.dirname(path.dirname(path.dirname(changeDir))); + const schemaName = resolveSchemaForChange(changeDir, undefined, projectRoot); + const schema = resolveSchema(schemaName, projectRoot); + return ( + schema.artifacts.some((artifact) => artifact.generates.endsWith('tasks.md')) || + Boolean(schema.apply?.tracks) + ); + } catch { + return true; + } +} diff --git a/src/core/artifact-graph/instruction-loader.ts b/src/core/artifact-graph/instruction-loader.ts index aa4a78e6be..6c641e52ea 100644 --- a/src/core/artifact-graph/instruction-loader.ts +++ b/src/core/artifact-graph/instruction-loader.ts @@ -51,6 +51,8 @@ export interface ChangeContext { changeDir: string; /** Project root directory */ projectRoot: string; + /** Workflow-level guidance from the schema's `notes:` field */ + schemaNotes?: string; /** Resolved planning home for this change */ planningHome?: PlanningHome; /** Parsed change metadata, when present */ @@ -86,6 +88,8 @@ export interface ArtifactInstructions { description: string; /** Guidance on how to create this artifact (from schema instruction field) */ instruction: string | undefined; + /** Workflow-level guidance from the schema's `notes:` field */ + schemaNotes?: string; /** Project context from config (constraints/background for AI, not to be included in output) */ context: string | undefined; /** Artifact-specific rules from config (constraints for AI, not to be included in output) */ @@ -244,6 +248,7 @@ export function loadChangeContext( changeName, changeDir, projectRoot, + ...(schema.notes ? { schemaNotes: schema.notes } : {}), ...(options.planningHome ? { planningHome: options.planningHome } : {}), ...(metadata ? { metadata } : {}), }; @@ -332,6 +337,7 @@ export function generateInstructions( existingOutputPaths: resolveArtifactOutputs(context.changeDir, artifact.generates), description: artifact.description, instruction: artifact.instruction, + ...(context.schemaNotes ? { schemaNotes: context.schemaNotes } : {}), context: configContext, rules: configRules, ...(options.references !== undefined ? { references: options.references } : {}), diff --git a/src/core/artifact-graph/resolver.ts b/src/core/artifact-graph/resolver.ts index 9ccd48abaf..abf281b7c3 100644 --- a/src/core/artifact-graph/resolver.ts +++ b/src/core/artifact-graph/resolver.ts @@ -2,6 +2,12 @@ import * as fs from 'node:fs'; import * as path from 'node:path'; import { fileURLToPath } from 'node:url'; import { getGlobalDataDir } from '../global-config.js'; +import { readProjectConfig } from '../project-config.js'; +import { + getStoreRegistryPath, + parseStoreRegistryState, +} from '../store/foundation.js'; +import { getStoreRootForBackend } from '../store/registry.js'; import { parseSchema, SchemaValidationError } from './schema.js'; import type { SchemaYaml } from './types.js'; @@ -45,13 +51,72 @@ export function getProjectSchemasDir(projectRoot: string): string { return path.join(projectRoot, 'openspec', 'schemas'); } +/** A referenced store whose schemas this root inherits. */ +export interface ReferencedSchemaSource { + storeId: string; + /** The store's openspec/schemas directory. */ + dir: string; +} + +/** + * The schema directories of every store this root references, in + * declaration order — a repo that declares `references: [team-hub]` + * inherits team-hub's schemas the way it already sees its specs. + * + * Deliberately tolerant and synchronous: schema resolution runs deep in + * sync call chains, and a missing config, registry, or checkout must + * degrade to "no inherited schemas", never an error. One hop only — + * references of references are not followed. + */ +export function getReferencedSchemaSources( + projectRoot: string +): ReferencedSchemaSource[] { + let references; + try { + references = readProjectConfig(projectRoot)?.references; + } catch { + return []; + } + if (!references || references.length === 0) return []; + + let registry; + try { + registry = parseStoreRegistryState( + fs.readFileSync(getStoreRegistryPath({}), 'utf-8') + ); + } catch { + return []; + } + + const resolvedRoot = path.resolve(projectRoot); + const sources: ReferencedSchemaSource[] = []; + for (const reference of references) { + const entry = registry.stores[reference.id]; + if (!entry) continue; + let storeRoot; + try { + storeRoot = getStoreRootForBackend(entry.backend); + } catch { + continue; + } + // A store referencing itself must not shadow its own project dir. + if (path.resolve(storeRoot) === resolvedRoot) continue; + sources.push({ + storeId: reference.id, + dir: getProjectSchemasDir(storeRoot), + }); + } + return sources; +} + /** * Resolves a schema name to its directory path. * * Resolution order (when projectRoot is provided): * 1. Project-local: <projectRoot>/openspec/schemas/<name>/schema.yaml - * 2. User override: ${XDG_DATA_HOME}/openspec/schemas/<name>/schema.yaml - * 3. Package built-in: <package>/schemas/<name>/schema.yaml + * 2. Referenced stores (declaration order): <storeRoot>/openspec/schemas/<name>/schema.yaml + * 3. User override: ${XDG_DATA_HOME}/openspec/schemas/<name>/schema.yaml + * 4. Package built-in: <package>/schemas/<name>/schema.yaml * * When projectRoot is not provided, only user override and package built-in are checked * (backward compatible behavior). @@ -71,6 +136,15 @@ export function getSchemaDir( if (fs.existsSync(projectSchemaPath)) { return projectDir; } + + // 1b. Check referenced stores (declaration order): schemas inherit + // through `references:` the way specs already surface through them. + for (const source of getReferencedSchemaSources(projectRoot)) { + const storeDir = path.join(source.dir, name); + if (fs.existsSync(path.join(storeDir, 'schema.yaml'))) { + return storeDir; + } + } } // 2. Check user override directory @@ -134,7 +208,7 @@ export function resolveSchema(name: string, projectRoot?: string): SchemaYaml { } try { - return parseSchema(content); + return applyInstructionFiles(parseSchema(content), schemaDir); } catch (err) { if (err instanceof SchemaValidationError) { throw new SchemaLoadError( @@ -152,6 +226,45 @@ export function resolveSchema(name: string, projectRoot?: string): SchemaYaml { } } +/** + * Long-form guidance can live beside the schema instead of inline YAML: + * `<schemaDir>/instructions/<artifact-id>.md` supplies an artifact's + * instruction (and `instructions/apply.md` the apply phase's), the same + * way templates already live in `<schemaDir>/templates/`. A file wins + * over an inline `instruction:` value. + */ +function applyInstructionFiles(schema: SchemaYaml, schemaDir: string): SchemaYaml { + const instructionsDir = path.join(schemaDir, 'instructions'); + if (!fs.existsSync(instructionsDir)) { + return schema; + } + + const readInstruction = (id: string): string | undefined => { + const filePath = path.join(instructionsDir, `${id}.md`); + try { + return fs.existsSync(filePath) + ? fs.readFileSync(filePath, 'utf-8').trim() + : undefined; + } catch { + return undefined; + } + }; + + for (const artifact of schema.artifacts) { + const fromFile = readInstruction(artifact.id); + if (fromFile) { + artifact.instruction = fromFile; + } + } + if (schema.apply) { + const fromFile = readInstruction('apply'); + if (fromFile) { + schema.apply.instruction = fromFile; + } + } + return schema; +} + /** * Lists all available schema names. * Combines project-local, user override, and package built-in schemas. @@ -187,13 +300,17 @@ export function listSchemas(projectRoot?: string): string[] { } } - // Add project-local schemas (if projectRoot provided) + // Add project-local and referenced-store schemas (if projectRoot provided) if (projectRoot) { - const projectDir = getProjectSchemasDir(projectRoot); - if (fs.existsSync(projectDir)) { - for (const entry of fs.readdirSync(projectDir, { withFileTypes: true })) { + const dirs = [ + getProjectSchemasDir(projectRoot), + ...getReferencedSchemaSources(projectRoot).map((source) => source.dir), + ]; + for (const dir of dirs) { + if (!fs.existsSync(dir)) continue; + for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { if (entry.isDirectory()) { - const schemaPath = path.join(projectDir, entry.name, 'schema.yaml'); + const schemaPath = path.join(dir, entry.name, 'schema.yaml'); if (fs.existsSync(schemaPath)) { schemas.add(entry.name); } @@ -212,7 +329,9 @@ export interface SchemaInfo { name: string; description: string; artifacts: string[]; - source: 'project' | 'user' | 'package'; + source: 'project' | 'store' | 'user' | 'package'; + /** The referenced store the schema is inherited from (source: 'store'). */ + store?: string; } /** @@ -225,78 +344,44 @@ export function listSchemasWithInfo(projectRoot?: string): SchemaInfo[] { const schemas: SchemaInfo[] = []; const seenNames = new Set<string>(); - // Add project-local schemas first (highest priority, if projectRoot provided) - if (projectRoot) { - const projectDir = getProjectSchemasDir(projectRoot); - if (fs.existsSync(projectDir)) { - for (const entry of fs.readdirSync(projectDir, { withFileTypes: true })) { - if (entry.isDirectory()) { - const schemaPath = path.join(projectDir, entry.name, 'schema.yaml'); - if (fs.existsSync(schemaPath)) { - try { - const schema = parseSchema(fs.readFileSync(schemaPath, 'utf-8')); - schemas.push({ - name: entry.name, - description: schema.description || '', - artifacts: schema.artifacts.map((a) => a.id), - source: 'project', - }); - seenNames.add(entry.name); - } catch { - // Skip invalid schemas - } - } - } + const collectFromDir = ( + dir: string, + source: SchemaInfo['source'], + store?: string + ): void => { + if (!fs.existsSync(dir)) return; + for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { + if (!entry.isDirectory() || seenNames.has(entry.name)) continue; + const schemaPath = path.join(dir, entry.name, 'schema.yaml'); + if (!fs.existsSync(schemaPath)) continue; + try { + const schema = parseSchema(fs.readFileSync(schemaPath, 'utf-8')); + schemas.push({ + name: entry.name, + description: schema.description || '', + artifacts: schema.artifacts.map((a) => a.id), + source, + ...(store !== undefined ? { store } : {}), + }); + seenNames.add(entry.name); + } catch { + // Skip invalid schemas } } - } + }; - // Add user override schemas (if not overridden by project) - const userDir = getUserSchemasDir(); - if (fs.existsSync(userDir)) { - for (const entry of fs.readdirSync(userDir, { withFileTypes: true })) { - if (entry.isDirectory() && !seenNames.has(entry.name)) { - const schemaPath = path.join(userDir, entry.name, 'schema.yaml'); - if (fs.existsSync(schemaPath)) { - try { - const schema = parseSchema(fs.readFileSync(schemaPath, 'utf-8')); - schemas.push({ - name: entry.name, - description: schema.description || '', - artifacts: schema.artifacts.map((a) => a.id), - source: 'user', - }); - seenNames.add(entry.name); - } catch { - // Skip invalid schemas - } - } - } + // Add project-local schemas first, then schemas inherited from + // referenced stores (declaration order) — mirrors getSchemaDir. + if (projectRoot) { + collectFromDir(getProjectSchemasDir(projectRoot), 'project'); + for (const source of getReferencedSchemaSources(projectRoot)) { + collectFromDir(source.dir, 'store', source.storeId); } } - // Add package built-in schemas (if not overridden by project or user) - const packageDir = getPackageSchemasDir(); - if (fs.existsSync(packageDir)) { - for (const entry of fs.readdirSync(packageDir, { withFileTypes: true })) { - if (entry.isDirectory() && !seenNames.has(entry.name)) { - const schemaPath = path.join(packageDir, entry.name, 'schema.yaml'); - if (fs.existsSync(schemaPath)) { - try { - const schema = parseSchema(fs.readFileSync(schemaPath, 'utf-8')); - schemas.push({ - name: entry.name, - description: schema.description || '', - artifacts: schema.artifacts.map((a) => a.id), - source: 'package', - }); - } catch { - // Skip invalid schemas - } - } - } - } - } + // Add user override schemas (if not shadowed), then package built-ins. + collectFromDir(getUserSchemasDir(), 'user'); + collectFromDir(getPackageSchemasDir(), 'package'); return schemas.sort((a, b) => a.name.localeCompare(b.name)); } diff --git a/src/core/artifact-graph/types.ts b/src/core/artifact-graph/types.ts index c2d2128e45..87353fe466 100644 --- a/src/core/artifact-graph/types.ts +++ b/src/core/artifact-graph/types.ts @@ -25,6 +25,10 @@ export const SchemaYamlSchema = z.object({ name: z.string().min(1, { error: 'Schema name is required' }), version: z.number().int().positive({ error: 'Version must be a positive integer' }), description: z.string().optional(), + // Workflow-level guidance surfaced verbatim to agents on every + // instruction surface — how this schema's lifecycle differs from the + // default (e.g. "no implementation phase; archive after specs"). + notes: z.string().optional(), artifacts: z.array(ArtifactSchema).min(1, { error: 'At least one artifact required' }), // Optional apply phase configuration (for schema-aware apply instructions) apply: ApplyPhaseSchema.optional(), diff --git a/src/core/change-metadata/schema.ts b/src/core/change-metadata/schema.ts index d97d9a9a3f..2086f6e2e7 100644 --- a/src/core/change-metadata/schema.ts +++ b/src/core/change-metadata/schema.ts @@ -20,6 +20,23 @@ export const InitiativeLinkSchema = z.object({ export type InitiativeLink = z.infer<typeof InitiativeLinkSchema>; +// The upward link to the work this change serves: `<change>` for a change +// in the same root, or `<store-id>/<change>` for one in that registered +// store. +export const ServesRefSchema = z.string().superRefine((value, ctx) => { + const parts = value.split('/'); + const valid = + (parts.length === 1 || parts.length === 2) && + parts.every((part) => isKebabId(part)); + if (!valid) { + ctx.addIssue({ + code: 'custom', + message: + 'serves must be a kebab-case change id, optionally prefixed with a store id: <change> or <store-id>/<change>', + }); + } +}); + // Per-change metadata schema. The schema field is validated against available // workflow schemas when metadata is read or written. export const ChangeMetadataSchema = z.object({ @@ -32,7 +49,11 @@ export const ChangeMetadataSchema = z.object({ .optional(), goal: z.string().min(1).optional(), affected_areas: z.array(z.string().min(1)).optional(), - initiative: InitiativeLinkSchema.optional(), + serves: ServesRefSchema.optional(), + // Legacy initiative links are tolerated in BOTH shapes the beta wrote — + // the `{store, id}` object and the `<store>/<name>` string — read-only, + // never re-emitted. A change that carries one must never fail to load. + initiative: z.union([InitiativeLinkSchema, z.string().min(1)]).optional(), }); export type ChangeMetadata = z.infer<typeof ChangeMetadataSchema>; diff --git a/src/core/completions/command-registry.ts b/src/core/completions/command-registry.ts index 76f2a28587..c80c66d839 100644 --- a/src/core/completions/command-registry.ts +++ b/src/core/completions/command-registry.ts @@ -50,6 +50,15 @@ export const COMMAND_REGISTRY: CommandDefinition[] = [ name: 'changes', description: 'List changes explicitly (default)', }, + { + name: 'downstream', + description: "Show the root's changes and every change on this machine that serves them", + }, + { + name: 'scan', + description: 'With --downstream: also scan this directory (and its immediate subdirectories) for serving changes — no registration needed (repeatable)', + takesValue: true, + }, { name: 'sort', description: 'Sort order: "recent" (default) or "name"', @@ -213,6 +222,7 @@ export const COMMAND_REGISTRY: CommandDefinition[] = [ description: 'List available workflow schemas with descriptions', flags: [ COMMON_FLAGS.json, + COMMON_FLAGS.store, ], }, { @@ -241,6 +251,11 @@ export const COMMAND_REGISTRY: CommandDefinition[] = [ description: 'Workflow schema to use', takesValue: true, }, + { + name: 'serves', + description: 'Link this change to the work it serves: <change> or <store-id>/<change>', + takesValue: true, + }, COMMON_FLAGS.json, COMMON_FLAGS.store, ], @@ -693,6 +708,7 @@ export const COMMAND_REGISTRY: CommandDefinition[] = [ name: 'all', description: 'List all schemas with their resolution sources', }, + COMMON_FLAGS.store, ], }, { @@ -707,6 +723,7 @@ export const COMMAND_REGISTRY: CommandDefinition[] = [ name: 'verbose', description: 'Show detailed validation steps', }, + COMMON_FLAGS.store, ], }, { @@ -740,7 +757,7 @@ export const COMMAND_REGISTRY: CommandDefinition[] = [ }, { name: 'artifacts', - description: 'Comma-separated artifact IDs', + description: 'Comma-separated stage ids, in workflow order (built-in or custom kebab-case names)', takesValue: true, }, { @@ -755,6 +772,7 @@ export const COMMAND_REGISTRY: CommandDefinition[] = [ name: 'force', description: 'Overwrite existing schema', }, + COMMON_FLAGS.store, ], }, ], diff --git a/src/core/init.ts b/src/core/init.ts index b6ab31ab77..acd9ffffb6 100644 --- a/src/core/init.ts +++ b/src/core/init.ts @@ -75,6 +75,7 @@ const WORKFLOW_TO_SKILL_DIR: Record<string, string> = { 'verify': 'openspec-verify-change', 'onboard': 'openspec-onboard', 'propose': 'openspec-propose', + 'store': 'openspec-store', }; // ----------------------------------------------------------------------------- diff --git a/src/core/openspec-root.ts b/src/core/openspec-root.ts index d65882ee21..25f89fd44d 100644 --- a/src/core/openspec-root.ts +++ b/src/core/openspec-root.ts @@ -2,6 +2,7 @@ import * as fs from 'node:fs/promises'; import * as path from 'node:path'; import { FileSystemUtils } from '../utils/file-system.js'; +import { readProjectConfig } from './project-config.js'; import { serializeConfig } from './config-prompts.js'; import { makeStoreDiagnostic, @@ -15,6 +16,19 @@ export const OPENSPEC_SPECS_DIR = 'openspec/specs'; export const OPENSPEC_CHANGES_DIR = 'openspec/changes'; export const OPENSPEC_ARCHIVE_DIR = 'openspec/changes/archive'; export const DEFAULT_OPENSPEC_SCHEMA = 'spec-driven'; + +// A store's changes are shared upstream work — requirements drafted before +// code moves — so a fresh store defaults to the documentation-only workflow. +// One line to change; `structure:` stays a commented example until a team +// wants to declare what its folders are for. +export const STORE_DEFAULT_CONFIG_CONTENT = `schema: requirements + +# Optional: tell agents what this store's folders are for. Surfaced to +# every repo that references this store (openspec context). +# structure: +# research/: raw inputs — interviews, transcripts, meeting notes +# decisions/: standing decisions and their rationale +`; export const DIRECTORY_ANCHOR_FILE_NAME = '.gitkeep'; // Git cannot track empty directories, so setup anchors otherwise-empty @@ -248,7 +262,8 @@ async function ensureDirectory( async function ensureDefaultConfig( storeRoot: string, - ledger: CreatedPathLedgerEntry[] + ledger: CreatedPathLedgerEntry[], + configContent?: string ): Promise<void> { const configYamlPath = path.join(storeRoot, OPENSPEC_CONFIG_YAML); const configYmlPath = path.join(storeRoot, OPENSPEC_CONFIG_YML); @@ -262,7 +277,7 @@ async function ensureDefaultConfig( await FileSystemUtils.writeFile( configYamlPath, - serializeConfig({ schema: DEFAULT_OPENSPEC_SCHEMA }) + configContent ?? serializeConfig({ schema: DEFAULT_OPENSPEC_SCHEMA }) ); ledger.push({ relativePath: relativeArtifact(OPENSPEC_CONFIG_YAML, 'file'), @@ -289,8 +304,60 @@ async function ensureDirectoryAnchor( }); } +/** + * Materialize the root's declared layout: every `structure:` entry in + * config becomes real. Keys ending in `/` are folders (created, anchored + * for git when requested); other keys are files (seeded with their purpose + * when markdown, created empty otherwise). Never overwrites anything that + * exists — declaring is safe, and re-running `store setup <id>` heals a + * layout that drifted. Keys are confined to the root; anything escaping it + * is skipped. + */ +async function ensureDeclaredStructure( + storeRoot: string, + ledger: CreatedPathLedgerEntry[], + anchor?: boolean +): Promise<void> { + let structure; + try { + structure = readProjectConfig(storeRoot)?.structure; + } catch { + return; + } + if (!structure) return; + + const openspecDir = path.resolve(storeRoot, OPENSPEC_ROOT_DIR); + for (const [key, purpose] of Object.entries(structure)) { + const target = path.resolve(openspecDir, key); + if (target !== openspecDir && !target.startsWith(openspecDir + path.sep)) { + continue; // Escapes the root — declared paths must stay inside openspec/. + } + const relative = `${OPENSPEC_ROOT_DIR}/${path.relative(openspecDir, target)}`; + if (key.endsWith('/')) { + await ensureDirectory(storeRoot, relative, ledger); + if (anchor) { + await ensureDirectoryAnchor(storeRoot, relative, ledger); + } + } else { + if ((await pathKind(target)) !== 'missing') continue; + await fs.mkdir(path.dirname(target), { recursive: true }); + const seed = key.endsWith('.md') + ? `# ${path.basename(key, '.md')}\n\n<!-- ${purpose} -->\n` + : ''; + await fs.writeFile(target, seed, 'utf-8'); + ledger.push({ + relativePath: relativeArtifact(relative, 'file'), + absolutePath: target, + kind: 'file', + }); + } + } +} + export interface EnsureOpenSpecRootOptions { anchorEmptyDirectories?: boolean; + /** Seed content for a config that does not exist yet (never overwrites). */ + defaultConfigContent?: string; } export async function ensureOpenSpecRoot( @@ -310,7 +377,8 @@ export async function ensureOpenSpecRoot( await ensureDirectory(storeRoot, OPENSPEC_SPECS_DIR, ledger); await ensureDirectory(storeRoot, OPENSPEC_CHANGES_DIR, ledger); await ensureDirectory(storeRoot, OPENSPEC_ARCHIVE_DIR, ledger); - await ensureDefaultConfig(storeRoot, ledger); + await ensureDefaultConfig(storeRoot, ledger, options.defaultConfigContent); + await ensureDeclaredStructure(storeRoot, ledger, options.anchorEmptyDirectories); if (options.anchorEmptyDirectories) { for (const relativeDir of ANCHORED_OPENSPEC_DIRS) { diff --git a/src/core/profile-sync-drift.ts b/src/core/profile-sync-drift.ts index 488d16cfdc..4d81b6660f 100644 --- a/src/core/profile-sync-drift.ts +++ b/src/core/profile-sync-drift.ts @@ -24,6 +24,7 @@ export const WORKFLOW_TO_SKILL_DIR: Record<WorkflowId, string> = { 'verify': 'openspec-verify-change', 'onboard': 'openspec-onboard', 'propose': 'openspec-propose', + 'store': 'openspec-store', }; function toKnownWorkflows(workflows: readonly string[]): WorkflowId[] { diff --git a/src/core/profiles.ts b/src/core/profiles.ts index acdc3ec953..b1fcaee19f 100644 --- a/src/core/profiles.ts +++ b/src/core/profiles.ts @@ -11,7 +11,7 @@ import type { Profile } from './global-config.js'; * Core workflows included in the 'core' profile. * These provide the streamlined experience for new users. */ -export const CORE_WORKFLOWS = ['propose', 'explore', 'apply', 'update', 'sync', 'archive'] as const; +export const CORE_WORKFLOWS = ['propose', 'explore', 'apply', 'update', 'sync', 'archive', 'store'] as const; /** * All available workflows in the system. @@ -29,6 +29,7 @@ export const ALL_WORKFLOWS = [ 'bulk-archive', 'verify', 'onboard', + 'store', ] as const; export type WorkflowId = (typeof ALL_WORKFLOWS)[number]; diff --git a/src/core/project-config.ts b/src/core/project-config.ts index 5d1b70e3aa..3b25f5548d 100644 --- a/src/core/project-config.ts +++ b/src/core/project-config.ts @@ -1,6 +1,6 @@ -import { existsSync, readFileSync, statSync } from 'fs'; +import { existsSync, readFileSync, statSync, writeFileSync } from 'fs'; import path from 'path'; -import { parse as parseYaml } from 'yaml'; +import { parse as parseYaml, parseDocument, isSeq } from 'yaml'; import { z } from 'zod'; /** @@ -39,6 +39,15 @@ export const ProjectConfigSchema = z.object({ .optional() .describe('Per-artifact rules, keyed by artifact ID'), + // Optional: what this root's non-reserved folders are for, keyed by + // folder path relative to openspec/ (e.g. "research/": "raw inputs"). + // Purely declarative — surfaced to agents in context and the + // references index so a store can explain its own layout. + structure: z + .record(z.string(), z.string()) + .optional() + .describe('Folder purposes, keyed by path relative to openspec/'), + // Note: the `references` field (id strings or {id, remote} maps) is // deliberately absent here — readProjectConfig parses and normalizes // it by hand (see DeclarationEntry below); a schema entry nothing @@ -233,6 +242,29 @@ export function readProjectConfig(projectRoot: string): ProjectConfig | null { } } + // Parse structure field: folder → purpose map, both non-empty strings. + if (raw.structure !== undefined) { + if (typeof raw.structure === 'object' && raw.structure !== null && !Array.isArray(raw.structure)) { + const parsedStructure: Record<string, string> = {}; + let droppedFolders = false; + for (const [folder, purpose] of Object.entries(raw.structure)) { + if (typeof purpose === 'string' && purpose.length > 0 && folder.length > 0) { + parsedStructure[folder] = purpose; + } else { + droppedFolders = true; + } + } + if (droppedFolders) { + console.warn(`Some 'structure' entries are invalid (must map folder to a non-empty string), ignoring them`); + } + if (Object.keys(parsedStructure).length > 0) { + config.structure = parsedStructure; + } + } else { + console.warn(`Invalid 'structure' field in config (must be a folder → purpose map)`); + } + } + const references = parseDeclarationList(raw.references); if (references) { config.references = references; @@ -417,6 +449,56 @@ export function readStorePointer(projectRoot: string): StorePointerRead { } } +/** + * Adds a store id to the config's `references:` list, preserving the file's + * comments and formatting. Conservative on purpose: any state it cannot + * edit with certainty (missing config, parse errors, a non-list + * `references:`) returns 'skipped' so the caller falls back to telling the + * user instead of guessing. Returns 'already' when the reference exists. + */ +export function addReferenceToProjectConfig( + projectRoot: string, + storeId: string +): 'added' | 'already' | 'skipped' { + const configPath = resolveConfigFilePath(projectRoot); + if (configPath === null) return 'skipped'; + + let doc; + try { + doc = parseDocument(readFileSync(configPath, 'utf-8')); + } catch { + return 'skipped'; + } + if (doc.errors.length > 0) return 'skipped'; + + const existing = doc.get('references'); + if (existing === undefined) { + doc.set('references', [storeId]); + } else if (isSeq(existing)) { + const values = existing.toJSON() as unknown[]; + const present = + Array.isArray(values) && + values.some( + (value) => + value === storeId || + (value !== null && + typeof value === 'object' && + (value as { id?: unknown }).id === storeId) + ); + if (present) return 'already'; + existing.add(doc.createNode(storeId)); + } else { + return 'skipped'; + } + + try { + writeFileSync(configPath, doc.toString(), 'utf-8'); + } catch { + return 'skipped'; + } + return 'added'; +} + /** Shared .yaml/.yml probe used by readProjectConfig and readStorePointer. */ export function resolveConfigFilePath(projectRoot: string): string | null { const yamlPath = path.join(projectRoot, 'openspec', 'config.yaml'); diff --git a/src/core/references.ts b/src/core/references.ts index 7edb8a225b..21858330d8 100644 --- a/src/core/references.ts +++ b/src/core/references.ts @@ -21,18 +21,33 @@ import { import { getStoreRootForBackend } from './store/registry.js'; import { inspectRegisteredStore, type ResolvedOpenSpecRoot } from './root-selection.js'; import { getSpecIds } from '../utils/item-discovery.js'; +import { listActiveChangeIds } from './upstream.js'; import { FileSystemUtils } from '../utils/file-system.js'; -import { MAX_CONTEXT_SIZE, type DeclarationEntry } from './project-config.js'; +import { MAX_CONTEXT_SIZE, readProjectConfig, type DeclarationEntry } from './project-config.js'; +import { listSchemasWithInfo } from './artifact-graph/index.js'; export interface ReferenceSpecEntry { id: string; summary: string; } +/** A store's own custom artifact type (schema), for the reference index. */ +export interface ReferenceSchemaEntry { + id: string; + summary: string; + artifacts: string[]; +} + export interface ReferenceIndexEntry { store_id: string; root?: string; specs?: ReferenceSpecEntry[]; + schemas?: ReferenceSchemaEntry[]; + /** The store's in-motion change ids — the work currently being drafted + * there. */ + changes?: string[]; + /** The store's declared folder purposes (config `structure:`). */ + structure?: Record<string, string>; fetch?: string; status: StoreDiagnostic[]; } @@ -140,10 +155,71 @@ async function collectSpecEntries(referencedRoot: string): Promise<ReferenceSpec ); } +/** + * A store's own custom artifact types — its project-local schemas. Package + * built-ins (spec-driven) are excluded: they are the same everywhere and + * would be noise in every referenced store's index. + */ +function collectSchemaEntries(referencedRoot: string): ReferenceSchemaEntry[] { + let schemas; + try { + schemas = listSchemasWithInfo(referencedRoot); + } catch { + return []; + } + return schemas + .filter((schema) => schema.source === 'project') + .map((schema) => ({ + id: sanitizeInline(schema.name, 100), + summary: sanitizeInline(schema.description), + artifacts: schema.artifacts.map((artifact) => sanitizeInline(artifact, 60)), + })); +} + +/** + * A store's declared layout for the index: the config's `structure:` map, + * sanitized — the store explaining its own non-reserved folders. + */ +function collectStructure(referencedRoot: string): Record<string, string> { + let structure; + try { + structure = readProjectConfig(referencedRoot)?.structure; + } catch { + return {}; + } + if (!structure) return {}; + const sanitized: Record<string, string> = {}; + for (const [folder, purpose] of Object.entries(structure)) { + sanitized[sanitizeInline(folder, 100)] = sanitizeInline(purpose, 200); + } + return sanitized; +} + +/** + * A store's work in motion for the index: its active change ids. Specs say + * what is true; these say what the team is currently deciding. + */ +async function collectActiveChangeIds(referencedRoot: string): Promise<string[]> { + try { + const ids = await listActiveChangeIds(referencedRoot); + return ids.map((id) => sanitizeInline(id, 60)); + } catch { + return []; + } +} + export function fetchRecipe(storeId: string): string { return `openspec show <spec-id> --type spec --store ${storeId}`; } +export function schemasFetchRecipe(storeId: string): string { + return `openspec schemas --store ${storeId}`; +} + +export function changesFetchRecipe(storeId: string): string { + return `openspec list --downstream --store ${storeId}`; +} + function specLine(spec: ReferenceSpecEntry): string { // Ids are raw directory names from cloned content; summaries are // sanitized at index time (collectSpecEntries). @@ -205,6 +281,30 @@ function renderEntryLines(entry: ReferenceIndexEntry): string[] { for (const spec of entry.specs ?? []) { lines.push(specLine(spec)); } + if (entry.schemas && entry.schemas.length > 0) { + lines.push(` Artifact types (${schemasFetchRecipe(entry.store_id)}):`); + for (const schema of entry.schemas) { + const graph = schema.artifacts.length > 0 + ? ` [${schema.artifacts.join(' → ')}]` + : ''; + lines.push( + schema.summary + ? ` - ${schema.id}: ${schema.summary}${graph}` + : ` - ${schema.id}${graph}` + ); + } + } + if (entry.changes && entry.changes.length > 0) { + lines.push( + ` In motion: ${entry.changes.join(', ')} (${changesFetchRecipe(entry.store_id)})` + ); + } + if (entry.structure && Object.keys(entry.structure).length > 0) { + lines.push(' Layout:'); + for (const [folder, purpose] of Object.entries(entry.structure)) { + lines.push(` - ${folder}: ${purpose}`); + } + } if (entry.fetch) { lines.push(` Fetch: ${entry.fetch}`); } @@ -368,10 +468,16 @@ export async function assembleReferenceIndex( } const specs = await collectSpecEntries(inspection.canonicalRoot); + const schemas = collectSchemaEntries(inspection.canonicalRoot); + const changes = await collectActiveChangeIds(inspection.canonicalRoot); + const structure = collectStructure(inspection.canonicalRoot); const entry: ReferenceIndexEntry = { store_id: id, root: inspection.canonicalRoot, specs, + ...(schemas.length > 0 ? { schemas } : {}), + ...(changes.length > 0 ? { changes } : {}), + ...(Object.keys(structure).length > 0 ? { structure } : {}), fetch: fetchRecipe(id), status: [], }; diff --git a/src/core/root-selection.ts b/src/core/root-selection.ts index aeb4e0a350..1f5bb618ff 100644 --- a/src/core/root-selection.ts +++ b/src/core/root-selection.ts @@ -383,12 +383,15 @@ export async function resolveOpenSpecRoot( : []; if (registeredIds.length > 0) { + // Lead with init: for work that belongs to THIS repo (like a change + // serving upstream work in a store), --store would land it in the + // wrong place — the store, not the repo. throw new RootSelectionError( - `No OpenSpec root found in the current directory or its ancestors. Registered stores: ${registeredIds.join(', ')}. Pass --store <id> to use one, or run openspec init to create a local root.`, + `No OpenSpec root found in the current directory or its ancestors. Run openspec init to create a local root here, or pass --store <id> to work in a registered store (registered: ${registeredIds.join(', ')}).`, 'no_root_with_registered_stores', { target: 'openspec.root', - fix: `Rerun with --store <id> (registered: ${registeredIds.join(', ')}) or run openspec init.`, + fix: `Run openspec init here, or rerun with --store <id> (registered: ${registeredIds.join(', ')}).`, } ); } diff --git a/src/core/shared/skill-generation.ts b/src/core/shared/skill-generation.ts index f671b4de73..682b50b50e 100644 --- a/src/core/shared/skill-generation.ts +++ b/src/core/shared/skill-generation.ts @@ -17,6 +17,8 @@ import { getVerifyChangeSkillTemplate, getOnboardSkillTemplate, getOpsxProposeSkillTemplate, + getStoreSkillTemplate, + getOpsxStoreCommandTemplate, getOpsxExploreCommandTemplate, getOpsxNewCommandTemplate, getOpsxContinueCommandTemplate, @@ -70,6 +72,7 @@ export function getSkillTemplates(workflowFilter?: readonly string[]): SkillTemp { template: getVerifyChangeSkillTemplate(), dirName: 'openspec-verify-change', workflowId: 'verify' }, { template: getOnboardSkillTemplate(), dirName: 'openspec-onboard', workflowId: 'onboard' }, { template: getOpsxProposeSkillTemplate(), dirName: 'openspec-propose', workflowId: 'propose' }, + { template: getStoreSkillTemplate(), dirName: 'openspec-store', workflowId: 'store' }, ]; if (!workflowFilter) return all; @@ -97,6 +100,7 @@ export function getCommandTemplates(workflowFilter?: readonly string[]): Command { template: getOpsxVerifyCommandTemplate(), id: 'verify' }, { template: getOpsxOnboardCommandTemplate(), id: 'onboard' }, { template: getOpsxProposeCommandTemplate(), id: 'propose' }, + { template: getOpsxStoreCommandTemplate(), id: 'store' }, ]; if (!workflowFilter) return all; diff --git a/src/core/shared/tool-detection.ts b/src/core/shared/tool-detection.ts index 30622209dc..71066d5e59 100644 --- a/src/core/shared/tool-detection.ts +++ b/src/core/shared/tool-detection.ts @@ -44,6 +44,7 @@ export const COMMAND_IDS = [ 'verify', 'onboard', 'propose', + 'store', ] as const; export type CommandId = (typeof COMMAND_IDS)[number]; diff --git a/src/core/specs-apply.ts b/src/core/specs-apply.ts index 3cf81222af..bf6889f8fe 100644 --- a/src/core/specs-apply.ts +++ b/src/core/specs-apply.ts @@ -227,7 +227,11 @@ export async function buildUpdatedSpec( ); } isNewSpec = true; - targetContent = buildSpecSkeleton(specName, changeName); + targetContent = buildSpecSkeleton( + specName, + changeName, + (await readDeltaPurpose(update.source)) ?? (await readProposalWhy(update.source)) + ); } const structureIssues = findMainSpecStructureIssues(targetContent); @@ -385,11 +389,62 @@ export async function writeUpdatedSpec( } /** - * Build a skeleton spec for new capabilities. + * Build a skeleton spec for new capabilities. The Purpose is seeded from + * the change's own proposal when one exists — the intent was already + * written once; a placeholder would just ask for it twice. */ -export function buildSpecSkeleton(specFolderName: string, changeName: string): string { +export function buildSpecSkeleton( + specFolderName: string, + changeName: string, + purpose?: string | null +): string { const titleBase = specFolderName; - return `# ${titleBase} Specification\n\n## Purpose\nTBD - created by archiving change ${changeName}. Update Purpose after archive.\n\n## Requirements\n`; + const purposeText = + purpose ?? + `TBD - created by archiving change ${changeName}. Update Purpose after archive.`; + return `# ${titleBase} Specification\n\n## Purpose\n${purposeText}\n\n## Requirements\n`; +} + +/** + * An explicit `## Purpose` section in the delta spec itself — the highest- + * fidelity source for a NEW spec's Purpose, written by whoever wrote the + * requirements. Delta operations ignore the section, so without this it + * would be silently dropped. + */ +async function readDeltaPurpose(deltaSource: string): Promise<string | null> { + try { + const content = await fs.readFile(deltaSource, 'utf-8'); + const match = content + .replace(/\r\n?/g, '\n') + .match(/(?:^|\n)##\s+Purpose\s*\n+([\s\S]*?)(?=\n##\s|$)/); + const text = match?.[1]?.trim(); + return text && text.length > 0 ? text : null; + } catch { + return null; + } +} + +/** + * The first paragraph of the change proposal's `## Why` section, flattened + * to one line — the natural Purpose for a spec this change creates. The + * delta source lives at `<changeDir>/specs/<name>/spec.md`, so the + * proposal sits two directories up. Tolerant: any missing or shapeless + * proposal means "no seed" and the placeholder is used. + */ +async function readProposalWhy(deltaSource: string): Promise<string | null> { + const changeDir = path.dirname(path.dirname(path.dirname(deltaSource))); + try { + const proposal = await fs.readFile(path.join(changeDir, 'proposal.md'), 'utf-8'); + const match = proposal + .replace(/\r\n?/g, '\n') + .match(/(?:^|\n)##\s+Why\s*\n+([\s\S]*?)(?=\n##\s|$)/); + const text = match?.[1]?.trim(); + if (!text) return null; + const paragraph = text.split(/\n\s*\n/)[0].replace(/\s*\n\s*/g, ' ').trim(); + return paragraph.length > 0 ? paragraph : null; + } catch { + return null; + } } function findMissingCurrentScenarios(current: RequirementBlock, incoming: RequirementBlock): string[] { diff --git a/src/core/store/operations.ts b/src/core/store/operations.ts index ccdc976377..e65575352c 100644 --- a/src/core/store/operations.ts +++ b/src/core/store/operations.ts @@ -13,6 +13,7 @@ import { ANCHORED_OPENSPEC_DIRS, DIRECTORY_ANCHOR_FILE_NAME, OPENSPEC_ROOT_DIR, + STORE_DEFAULT_CONFIG_CONTENT, ensureOpenSpecRoot, inspectOpenSpecRoot, rollbackCreatedPaths, @@ -34,6 +35,7 @@ import { type StorePathOptions, type StoreRegistryState, } from './foundation.js'; +import { readProjectConfig } from '../project-config.js'; import { StoreError, type StoreDiagnostic, makeStoreDiagnostic } from './errors.js'; import { assertGitCommitIdentity, @@ -626,6 +628,9 @@ export async function setupPreparedStore( try { const root = await ensureOpenSpecRoot(storeRoot, { anchorEmptyDirectories: !alreadyRegisteredHere, + // Stores hold shared upstream work, so a fresh one defaults to the + // documentation-only workflow instead of the code workflow. + defaultConfigContent: STORE_DEFAULT_CONFIG_CONTENT, }); createdFiles.push(...root.createdArtifacts); createdPaths = root.createdPaths; @@ -1087,6 +1092,20 @@ async function inspectStore(entry: { openspecRoot = await inspectOpenSpecRoot(root); diagnostics.push(...openspecRoot.diagnostics); + // Declared layout drift: every `structure:` entry should exist on + // disk. Setup materializes them, so the fix is one re-run. + for (const missing of await findMissingDeclaredStructure(root)) { + diagnostics.push(makeStoreDiagnostic( + 'warning', + 'store_structure_missing', + `Declared structure entry 'openspec/${missing}' is missing on disk.`, + { + target: 'store.structure', + fix: `Run openspec store setup ${entry.id} to materialize the declared layout.`, + } + )); + } + try { const parsed = await readOptionalStoreMetadataState(root); if (!parsed) { @@ -1191,6 +1210,31 @@ async function inspectStore(entry: { }; } +/** Declared `structure:` entries (relative to openspec/) missing on disk. */ +async function findMissingDeclaredStructure(root: string): Promise<string[]> { + let structure; + try { + structure = readProjectConfig(root)?.structure; + } catch { + return []; + } + if (!structure) return []; + const openspecDir = path.resolve(root, OPENSPEC_ROOT_DIR); + const missing: string[] = []; + for (const key of Object.keys(structure)) { + const target = path.resolve(openspecDir, key); + if (target !== openspecDir && !target.startsWith(openspecDir + path.sep)) continue; + try { + const stat = await nodeFs.promises.stat(target); + const wantDir = key.endsWith('/'); + if (wantDir !== stat.isDirectory()) missing.push(key); + } catch { + missing.push(key); + } + } + return missing.sort(); +} + export async function doctorStores(id?: string): Promise<StoreDoctorResult> { const selectedId = id !== undefined ? validateStoreId(id) : undefined; const registry = await readStoreRegistryState(); diff --git a/src/core/templates/skill-templates.ts b/src/core/templates/skill-templates.ts index 598fcc4465..ffb84e942f 100644 --- a/src/core/templates/skill-templates.ts +++ b/src/core/templates/skill-templates.ts @@ -18,4 +18,5 @@ export { getBulkArchiveChangeSkillTemplate, getOpsxBulkArchiveCommandTemplate } export { getVerifyChangeSkillTemplate, getOpsxVerifyCommandTemplate } from './workflows/verify-change.js'; export { getOnboardSkillTemplate, getOpsxOnboardCommandTemplate } from './workflows/onboard.js'; export { getOpsxProposeSkillTemplate, getOpsxProposeCommandTemplate } from './workflows/propose.js'; +export { getStoreSkillTemplate, getOpsxStoreCommandTemplate } from './workflows/store.js'; export { getFeedbackSkillTemplate } from './workflows/feedback.js'; diff --git a/src/core/templates/workflows/apply-change.ts b/src/core/templates/workflows/apply-change.ts index a08b24ddd0..3dd91eb43d 100644 --- a/src/core/templates/workflows/apply-change.ts +++ b/src/core/templates/workflows/apply-change.ts @@ -50,7 +50,7 @@ ${STORE_SELECTION_GUIDANCE} - Dynamic instruction based on current state **Handle states:** - - If \`state: "blocked"\` (missing artifacts): show message, suggest using openspec-continue-change + - If \`state: "blocked"\` (missing artifacts): show message, create each missing artifact via \`openspec instructions <artifact> --change <name>\` (never point at a skill that may not be installed) - If \`state: "all_done"\`: congratulate, suggest archive - Otherwise: proceed to implementation @@ -210,7 +210,7 @@ ${STORE_SELECTION_GUIDANCE} - Dynamic instruction based on current state **Handle states:** - - If \`state: "blocked"\` (missing artifacts): show message, suggest using \`/opsx:continue\` + - If \`state: "blocked"\` (missing artifacts): show message, create each missing artifact via \`openspec instructions <artifact> --change <name>\` (never point at a skill that may not be installed) - If \`state: "all_done"\`: congratulate, suggest archive - Otherwise: proceed to implementation diff --git a/src/core/templates/workflows/store-selection.ts b/src/core/templates/workflows/store-selection.ts index d40ed7d94d..92af634085 100644 --- a/src/core/templates/workflows/store-selection.ts +++ b/src/core/templates/workflows/store-selection.ts @@ -4,4 +4,4 @@ * Interpolated into every workflow's instructions so generated skills * consistently teach how to target a registered store with `--store <id>`. */ -export const STORE_SELECTION_GUIDANCE = `**Store selection:** If the user names a store (a store is a standalone OpenSpec repo registered on this machine) or the work lives in one, run \`openspec store list --json\` to discover registered store ids, then pass \`--store <id>\` on the commands that read or write specs and changes (\`new change\`, \`status\`, \`instructions\`, \`list\`, \`show\`, \`validate\`, \`archive\`, \`doctor\`, \`context\`). Other commands do not take the flag. Hints printed by commands already carry the flag; keep it on follow-ups. Without a store, commands act on the nearest local \`openspec/\` root.`; +export const STORE_SELECTION_GUIDANCE = `**Store selection:** If the user names a store (a store is a standalone OpenSpec repo registered on this machine) or the work lives in one, run \`openspec store list --json\` to discover registered store ids, then pass \`--store <id>\` on the commands that act on a selected root (\`new change\`, \`schema init\`, \`schema validate\`, \`schema which\`, \`status\`, \`instructions\`, \`list\`, \`show\`, \`validate\`, \`archive\`, \`doctor\`, \`context\`, \`schemas\`). Other commands do not take the flag. Hints printed by commands already carry the flag; keep it on follow-ups. Without a store, commands act on the nearest local \`openspec/\` root. **Upstream links:** when new work implements or serves work tracked in a store (the user names it, or \`openspec context\` lists it under "In motion"), create the change with \`--serves <store-id>/<change>\` — the link wires context and status automatically, and the change's instructions will carry the upstream artifacts to read first.`; diff --git a/src/core/templates/workflows/store.ts b/src/core/templates/workflows/store.ts new file mode 100644 index 0000000000..b7d42679ea --- /dev/null +++ b/src/core/templates/workflows/store.ts @@ -0,0 +1,60 @@ +/** + * Skill Template Workflow Modules + * + * The one door to shared upstream work: a thin, state-routed skill. Every + * mechanic lives in deterministic CLI commands with JSON output; this + * skill only reads the world, routes to exactly one situation, and ends + * every reply with numbered next moves computed from disk state. + */ +import type { SkillTemplate, CommandTemplate } from '../types.js'; +import { STORE_SELECTION_GUIDANCE } from './store-selection.js'; + +const STORE_DOOR_INSTRUCTIONS = `The one door to shared upstream work: set it up, draft requirements, link implementing work, see status, archive into standing truth. The user should never need to know the commands — you run them. + +--- + +${STORE_SELECTION_GUIDANCE} + +**Read the world FIRST — never ask the user anything a command can answer:** + +1. \`openspec store list --json\` — which stores exist on this machine +2. \`openspec context --json\` — what the current repo references and what is in motion upstream +3. If a store is relevant: \`openspec list --downstream --store <id> --json\` — where everything stands + +**Route to exactly ONE situation:** + +- **No store registered** → offer to create one: \`openspec store setup <kebab-id>\` (no other flags needed — it picks ~/openspec/<id>, seeds a working requirements workflow, and prints the whole loop). Derive the id from the team or product name. +- **Store exists, nothing drafted** → capture: ask ONE question at most ("what's the work?"), derive a kebab-case name, run \`openspec new change <name> --store <id>\`, then follow \`openspec instructions <artifact> --change <name> --store <id>\` stage by stage — read the schema_notes and instruction blocks, author each artifact from the conversation, and STOP at each review gate the instructions declare. +- **Upstream change in motion** → continue its next incomplete artifact via \`openspec status --change <name> --store <id>\`, then \`openspec instructions <next-artifact> ...\`. +- **User is implementing upstream work in a code repo** → \`openspec new change <impl-name> --serves <store-id>/<change>\` — this wires context and status automatically; their instructions will open with the upstream block. +- **The rollup shows "all serving changes complete — archive it"** → confirm with the user, then \`openspec archive <change> --store <id> --yes\`, and show the standing spec it produced (\`openspec list --specs --store <id>\`). +- **The team wants its own workflow stages** → \`openspec schema init <name> --artifacts <their,stage,names> --store <id>\` (their order IS the chain), then edit the generated instructions/ and templates/ files with them. +- **Anything else / just checking in** → show the rollup and read it back plainly. + +**Rules:** + +- Quote real command output for any status claim — never estimate progress. +- Ask at most ONE question per message, and only when no command can answer it. +- Respect the schema: read \`schema_notes\` and per-artifact instructions verbatim; do not assume proposal/design/tasks exist — the workflow is whatever the schema says. +- If implementation shows an upstream requirement is wrong, flag it upstream instead of silently diverging. +- End EVERY reply with 2-4 numbered next moves computed from the current disk state, exactly one marked **(Recommended)**, each naming the real command it runs.`; + +export function getStoreSkillTemplate(): SkillTemplate { + return { + name: 'openspec-store', + description: + "One door to shared upstream work (stores): set up a team store, draft requirements under the team's workflow, link implementing changes with --serves, show live cross-repo status, and archive approved work into standing specs. Use when the user mentions team requirements, shared or upstream work, a store, cross-repo status, or handing work to other repos.", + instructions: STORE_DOOR_INSTRUCTIONS, + }; +} + +export function getOpsxStoreCommandTemplate(): CommandTemplate { + return { + name: 'OPSX: Store', + description: + 'One door to shared upstream work: set up, draft, link, status, archive', + category: 'Workflow', + tags: ['workflow', 'stores', 'experimental'], + content: STORE_DOOR_INSTRUCTIONS, + }; +} diff --git a/src/core/upstream.ts b/src/core/upstream.ts new file mode 100644 index 0000000000..75710b0deb --- /dev/null +++ b/src/core/upstream.ts @@ -0,0 +1,554 @@ +/** + * Upstream links (stores beta). + * + * A change can serve work that lives one level up — usually a change in a + * shared store where a team drafts requirements before code moves. The link + * is one metadata line in the change's `.openspec.yaml`: + * `serves: <change>` (a change in the same root) or + * `serves: <store-id>/<change>` (a change in that registered store). + * + * There is no manifest — rollup discovers serving changes by scanning for + * that line, so teams tag their own work and nothing central needs + * maintaining. When the upstream change archives, its requirements live on + * in the store's specs; the link keeps resolving to the archived copy. + */ + +import { promises as fs } from 'fs'; +import path from 'path'; +import { parse as parseYaml } from 'yaml'; + +import { getTaskProgressForChange } from '../utils/task-progress.js'; +import { resolveSchemaForChange } from '../utils/change-metadata.js'; +import { ArtifactGraph } from './artifact-graph/graph.js'; +import { detectCompleted } from './artifact-graph/state.js'; +import { resolveSchema } from './artifact-graph/resolver.js'; +import { writeFileAtomically } from './file-state.js'; +import { + getStoresDir, + listStoreRegistryEntries, + readStoreRegistryState, + type StorePathOptions, +} from './store/foundation.js'; +import { getStoreRootForBackend } from './store/registry.js'; + +export interface ServingChangeStatus { + id: string; + /** Registered store the serving change lives in; absent = the rolled-up root. */ + store?: string; + /** Linked (non-store) repo the serving change lives in, by directory name. */ + repo?: string; + completedTasks: number; + totalTasks: number; + /** Artifact-graph progress; absent when the change's schema is unreadable. */ + completedArtifacts?: number; + totalArtifacts?: number; + state: 'complete' | 'in-progress' | 'no-tasks'; +} + +/** + * Artifact-graph progress for one serving change, so the rollup's "done" + * means the whole change — a checked-off tasks.md with no approved + * proposal must not read as complete. Tolerant: an unreadable schema + * yields null and the rollup falls back to task counts alone. + */ +function readArtifactProgress( + changeDir: string, + scanRoot: string +): { completed: number; total: number } | null { + try { + const schemaName = resolveSchemaForChange(changeDir, undefined, scanRoot); + const schema = resolveSchema(schemaName, scanRoot); + const graph = ArtifactGraph.fromSchema(schema); + const completed = detectCompleted(graph, changeDir); + return { completed: completed.size, total: schema.artifacts.length }; + } catch { + return null; + } +} + +export interface UpstreamChangeInfo { + id: string; + /** False when serving changes reference this id but no change exists on disk. */ + exists: boolean; + /** True when the upstream change has been archived (requirements synced to specs). */ + archived: boolean; + changes: ServingChangeStatus[]; + changesComplete: number; + changesTotal: number; + tasksComplete: number; + tasksTotal: number; +} + +export interface DownstreamRollup { + path: string; + upstream: UpstreamChangeInfo[]; +} + +function changesDirOf(root: string): string { + return path.join(root, 'openspec', 'changes'); +} + +/** + * Reads the `serves:` line from a change's `.openspec.yaml`. Tolerant: any + * unreadable or unlinked metadata simply means "serves nothing upstream". + */ +export async function readServesRef(changeDir: string): Promise<string | null> { + try { + const raw = await fs.readFile(path.join(changeDir, '.openspec.yaml'), 'utf-8'); + const parsed = parseYaml(raw) as { serves?: unknown } | null; + return typeof parsed?.serves === 'string' && parsed.serves.length > 0 + ? parsed.serves + : null; + } catch { + return null; + } +} + +/** + * A root's active change ids: every non-archive directory under + * openspec/changes/, regardless of which schema's artifacts it holds. + * (Unlike proposal-based discovery, this stays correct for custom + * schemas whose first artifact is not proposal.md.) + */ +export async function listActiveChangeIds(root: string): Promise<string[]> { + let entries; + try { + entries = await fs.readdir(changesDirOf(root), { withFileTypes: true }); + } catch { + return []; + } + return entries + .filter( + (entry) => + entry.isDirectory() && !entry.name.startsWith('.') && entry.name !== 'archive' + ) + .map((entry) => entry.name) + .sort(); +} + +// --------------------------------------------------------------------------- +// Linked roots +// +// Rollup can only scan checkouts it knows about. Store roots come from the +// store registry; plain code repos become known the moment they link a change +// to a store's change (`new change --serves <store>/<change>` records the +// repo's path here). Linking IS the registration — no extra command, and +// nothing is written into the repo itself. +// --------------------------------------------------------------------------- + +const LINKED_ROOTS_FILE_NAME = 'linked-roots.yaml'; + +function getLinkedRootsPath(options: StorePathOptions = {}): string { + return path.join(getStoresDir(options), LINKED_ROOTS_FILE_NAME); +} + +/** Known linked roots. Tolerant: unreadable state means "none". */ +export async function readLinkedRoots( + options: StorePathOptions = {} +): Promise<string[]> { + try { + const raw = await fs.readFile(getLinkedRootsPath(options), 'utf-8'); + const parsed = parseYaml(raw) as { roots?: unknown } | null; + if (!Array.isArray(parsed?.roots)) return []; + return parsed.roots.filter( + (entry): entry is string => typeof entry === 'string' && entry.length > 0 + ); + } catch { + return []; + } +} + +/** + * Records a repo root so rollups scan it. Idempotent; stale entries are + * harmless (a missing directory scans as empty). + */ +export async function recordLinkedRoot( + root: string, + options: StorePathOptions = {} +): Promise<void> { + const resolved = path.resolve(root); + const existing = await readLinkedRoots(options); + if (existing.includes(resolved)) return; + const roots = [...existing, resolved].sort(); + await fs.mkdir(getStoresDir(options), { recursive: true }); + await writeFileAtomically( + getLinkedRootsPath(options), + `version: 1\nroots:\n${roots.map((entry) => ` - ${JSON.stringify(entry)}`).join('\n')}\n` + ); +} + +/** + * Locates an upstream change inside a root: active changes first, then the + * archive (archived copies are named `YYYY-MM-DD-<id>`). Returns null when + * the id matches neither. + */ +async function locateChange( + root: string, + changeId: string +): Promise<{ path: string; archived: boolean } | null> { + const active = path.join(changesDirOf(root), changeId); + try { + if ((await fs.stat(active)).isDirectory()) { + return { path: active, archived: false }; + } + } catch { + // Not active — fall through to the archive. + } + const archiveDir = path.join(changesDirOf(root), 'archive'); + let entries; + try { + entries = await fs.readdir(archiveDir, { withFileTypes: true }); + } catch { + return null; + } + const suffix = `-${changeId}`; + const match = entries + .filter((entry) => entry.isDirectory() && entry.name.endsWith(suffix)) + .map((entry) => entry.name) + .sort() + .pop(); + return match ? { path: path.join(archiveDir, match), archived: true } : null; +} + +/** + * Scans one root's changes and returns, per matching upstream change id, the + * serving changes that point at it. `toId` maps a raw ref to the upstream + * change id it addresses in the root being rolled up — or null when the ref + * points elsewhere. + */ +async function collectServingChanges( + root: string, + toId: (ref: string) => string | null, + label: { store?: string; repo?: string } | undefined +): Promise<Map<string, ServingChangeStatus[]>> { + const changesDir = changesDirOf(root); + const found = new Map<string, ServingChangeStatus[]>(); + let entries; + try { + entries = await fs.readdir(changesDir, { withFileTypes: true }); + } catch { + return found; + } + + for (const entry of entries) { + if (!entry.isDirectory() || entry.name === 'archive') continue; + const ref = await readServesRef(path.join(changesDir, entry.name)); + if (ref === null) continue; + const id = toId(ref); + if (id === null) continue; + + const progress = await getTaskProgressForChange(changesDir, entry.name, root); + const artifacts = readArtifactProgress(path.join(changesDir, entry.name), root); + const tasksDone = progress.total > 0 && progress.completed === progress.total; + const artifactsDone = artifacts !== null && artifacts.completed === artifacts.total; + // Done means the WHOLE change: tasks checked off AND (when the schema + // is readable) every artifact present. Without tasks, artifacts alone + // can carry it; without either signal, the state stays 'no-tasks'. + const state: ServingChangeStatus['state'] = + artifacts === null + ? progress.total === 0 + ? 'no-tasks' + : tasksDone + ? 'complete' + : 'in-progress' + : (progress.total === 0 ? artifactsDone : tasksDone && artifactsDone) + ? 'complete' + : progress.total === 0 && artifacts.total === 0 + ? 'no-tasks' + : 'in-progress'; + const status: ServingChangeStatus = { + id: entry.name, + ...(label?.store ? { store: label.store } : {}), + ...(label?.repo ? { repo: label.repo } : {}), + completedTasks: progress.completed, + totalTasks: progress.total, + ...(artifacts !== null + ? { completedArtifacts: artifacts.completed, totalArtifacts: artifacts.total } + : {}), + state, + }; + const bucket = found.get(id); + if (bucket) { + bucket.push(status); + } else { + found.set(id, [status]); + } + } + return found; +} + +function mergeChanges( + target: Map<string, ServingChangeStatus[]>, + source: Map<string, ServingChangeStatus[]> +): void { + for (const [id, changes] of source) { + const bucket = target.get(id); + if (bucket) { + bucket.push(...changes); + } else { + target.set(id, changes); + } + } +} + +/** + * Rolls up a root's downstream work: the root's upstream candidates, plus + * every change on this machine that serves one of them. Local serving + * changes match `serves: <id>` (or `<own-store-id>/<id>`); when the root is + * a registered store, other registered roots and linked repos are scanned + * for `<that id>/<id>`. A change that itself serves something is downstream + * work — it only gets its own row when something serves IT, so rollups of + * busy repos are not noise. Ids referenced by serving changes but missing + * on disk are included with `exists: false` — a bad reference should be + * visible, not silently dropped; archived upstream changes are included + * with `archived: true`. Returns null when the root has no changes folder. + */ +export async function rollupDownstream( + root: string, + options: { globalDataDir?: string; extraRoots?: string[] } = {} +): Promise<DownstreamRollup | null> { + const changesDir = changesDirOf(root); + let ownEntries; + try { + ownEntries = await fs.readdir(changesDir, { withFileTypes: true }); + } catch { + return null; + } + const allOwnChanges = ownEntries + .filter((entry) => entry.isDirectory() && entry.name !== 'archive') + .map((entry) => entry.name) + .sort(); + const servingOwnChanges = new Set<string>(); + for (const id of allOwnChanges) { + if ((await readServesRef(path.join(changesDir, id))) !== null) { + servingOwnChanges.add(id); + } + } + + const registry = await readStoreRegistryState( + options.globalDataDir ? { globalDataDir: options.globalDataDir } : {} + ).catch(() => null); + const registered = registry ? listStoreRegistryEntries(registry) : []; + + // Which registered store, if any, is this root? + const resolvedRoot = path.resolve(root); + let ownStoreId: string | undefined; + const storeRoots: Array<{ id: string; root: string }> = []; + for (const entry of registered) { + try { + const entryRoot = path.resolve(getStoreRootForBackend(entry.backend)); + storeRoots.push({ id: entry.id, root: entryRoot }); + if (entryRoot === resolvedRoot) { + ownStoreId = entry.id; + } + } catch { + // Unusable backend — skipped; doctor reports it. + } + } + + // A ref addresses this root as `<id>` from its own changes, or as + // `<own-store-id>/<id>` from anywhere. + const ownPrefix = ownStoreId !== undefined ? `${ownStoreId}/` : null; + const byId = await collectServingChanges( + root, + (ref) => { + if (!ref.includes('/')) return ref; + if (ownPrefix !== null && ref.startsWith(ownPrefix)) { + return ref.slice(ownPrefix.length); + } + return null; + }, + undefined + ); + + if (ownStoreId !== undefined && ownPrefix !== null) { + const toOwnId = (ref: string) => + ref.startsWith(ownPrefix) ? ref.slice(ownPrefix.length) : null; + const scanned = new Set([resolvedRoot]); + for (const store of storeRoots) { + if (scanned.has(store.root)) continue; + scanned.add(store.root); + mergeChanges( + byId, + await collectServingChanges(store.root, toOwnId, { store: store.id }) + ); + } + // Plain code repos that linked a change here (recorded at link time), + // plus any roots handed in explicitly (--scan on a machine with no + // per-machine state, like CI). + const passedRoots = (options.extraRoots ?? []).map((entry) => path.resolve(entry)); + for (const linkedRoot of [ + ...(await readLinkedRoots( + options.globalDataDir ? { globalDataDir: options.globalDataDir } : {} + )), + ...passedRoots, + ]) { + if (scanned.has(linkedRoot)) continue; + scanned.add(linkedRoot); + mergeChanges( + byId, + await collectServingChanges(linkedRoot, toOwnId, { + repo: path.basename(linkedRoot), + }) + ); + } + } + + const upstream: UpstreamChangeInfo[] = []; + const addUpstream = (id: string, exists: boolean, archived: boolean) => { + const changes = (byId.get(id) ?? []).sort((a, b) => a.id.localeCompare(b.id)); + let changesComplete = 0; + let tasksComplete = 0; + let tasksTotal = 0; + for (const change of changes) { + tasksComplete += change.completedTasks; + tasksTotal += change.totalTasks; + if (change.state === 'complete') changesComplete += 1; + } + upstream.push({ + id, + exists, + archived, + changes, + changesComplete, + changesTotal: changes.length, + tasksComplete, + tasksTotal, + }); + }; + + for (const id of allOwnChanges) { + // Downstream work only earns an upstream row when something serves it. + if (servingOwnChanges.has(id) && !byId.has(id)) continue; + addUpstream(id, true, false); + } + const known = new Set(allOwnChanges); + for (const id of [...byId.keys()].sort()) { + if (known.has(id)) continue; + const located = await locateChange(root, id); + addUpstream(id, located !== null, located?.archived ?? false); + } + + return { path: changesDir, upstream }; +} + +/** + * The rollups of every registered store that has one. This is the + * outside-a-root answer to "where does everything stand": shared planning + * sits above repos, so asking it should not require standing in one. + */ +export async function rollupRegisteredStores( + options: { globalDataDir?: string } = {} +): Promise<Array<{ store: string; rollup: DownstreamRollup }>> { + const registry = await readStoreRegistryState( + options.globalDataDir ? { globalDataDir: options.globalDataDir } : {} + ).catch(() => null); + const registered = registry ? listStoreRegistryEntries(registry) : []; + + const rollups: Array<{ store: string; rollup: DownstreamRollup }> = []; + for (const entry of registered) { + let root; + try { + root = getStoreRootForBackend(entry.backend); + } catch { + continue; // Unusable backend — skipped; doctor reports it. + } + const rollup = await rollupDownstream(root, options); + if (rollup !== null && rollup.upstream.length > 0) { + rollups.push({ store: entry.id, rollup }); + } + } + return rollups; +} + +/** + * Expands `--scan` arguments into candidate roots: the directory itself + * when it is an OpenSpec root, plus every immediate subdirectory that is + * one. Stateless by design — a CI checkout directory needs no + * registration to be rolled up. + */ +export async function expandScanDirs(dirs: string[]): Promise<string[]> { + const roots: string[] = []; + for (const dir of dirs) { + const resolved = path.resolve(dir); + const isRoot = async (candidate: string): Promise<boolean> => { + try { + return (await fs.stat(path.join(candidate, 'openspec', 'changes'))).isDirectory(); + } catch { + return false; + } + }; + if (await isRoot(resolved)) { + roots.push(resolved); + } + let entries; + try { + entries = await fs.readdir(resolved, { withFileTypes: true }); + } catch { + continue; + } + for (const entry of entries) { + if (!entry.isDirectory() || entry.name.startsWith('.')) continue; + const candidate = path.join(resolved, entry.name); + if (await isRoot(candidate)) { + roots.push(candidate); + } + } + } + return roots; +} + +export interface ResolvedUpstreamLink { + /** The reference as written: `<change>` or `<store-id>/<change>`. */ + ref: string; + changeId: string; + /** Present when the ref is store-qualified. */ + store?: string; + /** Absolute path to the upstream change; null when not found on disk. */ + path: string | null; + /** True when the upstream change has been archived. */ + archived: boolean; +} + +/** + * Resolves a change's `serves:` link to the upstream change it points at, so + * instruction surfaces can hand the agent the actual upstream context. A ref + * that resolves to no change still returns (with `path: null`) — a stale + * link should be visible, not silently dropped. + */ +export async function resolveUpstreamLink( + changeDir: string, + root: string, + options: StorePathOptions = {} +): Promise<ResolvedUpstreamLink | null> { + const ref = await readServesRef(changeDir); + if (ref === null) return null; + + const slash = ref.indexOf('/'); + const store = slash === -1 ? undefined : ref.slice(0, slash); + const changeId = slash === -1 ? ref : ref.slice(slash + 1); + + let upstreamRoot: string | null = slash === -1 ? root : null; + if (store !== undefined) { + const registry = await readStoreRegistryState(options).catch(() => null); + const entry = registry?.stores[store]; + if (entry) { + try { + upstreamRoot = getStoreRootForBackend(entry.backend); + } catch { + // Unusable backend — the link renders with path: null. + } + } + } + + const located = + upstreamRoot !== null ? await locateChange(upstreamRoot, changeId) : null; + + return { + ref, + changeId, + ...(store !== undefined ? { store } : {}), + path: located?.path ?? null, + archived: located?.archived ?? false, + }; +} diff --git a/src/core/validation/validator.ts b/src/core/validation/validator.ts index 511662f294..04eedd2000 100644 --- a/src/core/validation/validator.ts +++ b/src/core/validation/validator.ts @@ -11,6 +11,8 @@ import { VALIDATION_MESSAGES } from './constants.js'; import { parseDeltaSpec, normalizeRequirementName, extractRequirementsSection } from '../parsers/requirement-blocks.js'; +import { resolveSchemaForChange } from '../../utils/change-metadata.js'; +import { resolveSchema } from '../artifact-graph/resolver.js'; import { extractRequirementBody as extractRequirementBodyShared, containsShallOrMust as containsShallOrMustShared, @@ -90,7 +92,12 @@ export class Validator { const result = ChangeSchema.safeParse(change); if (!result.success) { - issues.push(...this.convertZodErrors(result.error)); + const converted = this.convertZodErrors(result.error).filter( + (issue) => + !issue.message.startsWith(VALIDATION_MESSAGES.CHANGE_NO_DELTAS) || + this.changeSchemaExpectsDeltas(changeDir) + ); + issues.push(...converted); } issues.push(...this.applyChangeRules(change, content)); @@ -303,13 +310,34 @@ export class Validator { }); } - if (totalDeltas === 0) { + if (totalDeltas === 0 && this.changeSchemaExpectsDeltas(changeDir)) { issues.push({ level: 'ERROR', path: 'file', message: this.enrichTopLevelError('change', VALIDATION_MESSAGES.CHANGE_NO_DELTAS) }); } return this.createReport(issues); } + /** + * Whether the change's workflow schema defines a specs-generating + * artifact. A schema without one is deliberately spec-less (a + * documentation-only or freeform workflow), and demanding deltas from + * it would flag the schema author's intent as an error. Tolerant: any + * resolution failure falls back to requiring deltas, so the guard can + * only ever relax validation for schemas we can actually read. + */ + private changeSchemaExpectsDeltas(changeDir: string): boolean { + try { + const projectRoot = path.dirname(path.dirname(path.dirname(changeDir))); + const schemaName = resolveSchemaForChange(changeDir, undefined, projectRoot); + const schema = resolveSchema(schemaName, projectRoot); + return schema.artifacts.some((artifact) => + artifact.generates.startsWith('specs/') + ); + } catch { + return true; + } + } + /** * Recursively collect every delta `spec.md` under a change's specs directory, * so both the one-level (specs/<capability>/spec.md) and nested multi-area diff --git a/src/core/working-set.ts b/src/core/working-set.ts index 007c336801..58b1ae9b41 100644 --- a/src/core/working-set.ts +++ b/src/core/working-set.ts @@ -18,6 +18,13 @@ export interface WorkingSetMember { path?: string; remote?: string; fetch?: string; + /** A referenced store's own custom artifact types (project schema names). + * Present only when the store defines some and it is available. */ + artifactTypes?: string[]; + /** A referenced store's in-motion change ids. */ + changes?: string[]; + /** A referenced store's declared folder purposes (config `structure:`). */ + structure?: Record<string, string>; status: StoreDiagnostic[]; } diff --git a/src/utils/change-utils.ts b/src/utils/change-utils.ts index c47a4a3efe..b4a6a40e24 100644 --- a/src/utils/change-utils.ts +++ b/src/utils/change-utils.ts @@ -17,7 +17,7 @@ export interface CreateChangeOptions { /** Directory that should contain the change directories */ changesDir?: string; /** Additional metadata to persist in the change's .openspec.yaml */ - metadata?: Partial<Pick<ChangeMetadata, 'goal' | 'affected_areas' | 'initiative'>>; + metadata?: Partial<Pick<ChangeMetadata, 'goal' | 'affected_areas' | 'serves'>>; } /** diff --git a/test/commands/artifact-workflow.test.ts b/test/commands/artifact-workflow.test.ts index a286422a82..52d07ba596 100644 --- a/test/commands/artifact-workflow.test.ts +++ b/test/commands/artifact-workflow.test.ts @@ -355,6 +355,32 @@ describe('artifact-workflow CLI commands', () => { }); }); + it('writes the upward serves link for --serves', async () => { + const result = await runCLI( + ['new', 'change', 'serving-change', '--serves', 'billing-launch'], + { cwd: tempDir } + ); + expect(result.exitCode).toBe(0); + + const metadata = await fs.readFile( + path.join(changesDir, 'serving-change', '.openspec.yaml'), + 'utf-8' + ); + expect(metadata).toContain('serves: billing-launch'); + }); + + it('rejects a malformed --serves ref and writes no change', async () => { + const result = await runCLI( + ['new', 'change', 'bad-serves', '--serves', 'a/b/c'], + { cwd: tempDir } + ); + expect(result.exitCode).toBe(1); + expect(getOutput(result)).toContain('serves must be a kebab-case change id'); + await expect(fs.stat(path.join(changesDir, 'bad-serves'))).rejects.toMatchObject({ + code: 'ENOENT', + }); + }); + it('rejects --areas and writes no affected-area metadata', async () => { const result = await runCLI(['new', 'change', 'area-change', '--areas', 'api'], { cwd: tempDir, diff --git a/test/commands/change-initiative-link.test.ts b/test/commands/change-initiative-link.test.ts index c1a7797b57..e22ba2b1eb 100644 --- a/test/commands/change-initiative-link.test.ts +++ b/test/commands/change-initiative-link.test.ts @@ -48,7 +48,7 @@ describe('legacy repo-local change initiative metadata', () => { return path.join(tempDir, 'openspec', 'changes', id); } - function createLegacyLinkedChange(id: string): string { + function createLegacyLinkedChange(id: string, metadataLine?: string): string { const dir = changeDir(id); fs.mkdirSync(dir, { recursive: true }); fs.writeFileSync( @@ -57,11 +57,26 @@ describe('legacy repo-local change initiative metadata', () => { ); fs.writeFileSync( path.join(dir, '.openspec.yaml'), - 'schema: spec-driven\ninitiative:\n store: platform\n id: billing-launch\n' + metadataLine ?? + 'schema: spec-driven\ninitiative:\n store: platform\n id: billing-launch\n' ); return dir; } + it('keeps reading the string-form legacy link the beta wrote', async () => { + createLegacyLinkedChange( + 'legacy-string-change', + 'schema: spec-driven\ninitiative: platform/billing-launch\n' + ); + + const status = await runCLI( + ['status', '--change', 'legacy-string-change', '--json'], + { cwd: tempDir, env } + ); + expect(status.exitCode).toBe(0); + expect('initiative' in parseJson(status)).toBe(false); + }); + it('keeps reading existing initiative metadata without modifying it', async () => { const dir = createLegacyLinkedChange('legacy-change'); const metadataPath = path.join(dir, '.openspec.yaml'); diff --git a/test/commands/config-profile.test.ts b/test/commands/config-profile.test.ts index 679e89a547..cda3a04dce 100644 --- a/test/commands/config-profile.test.ts +++ b/test/commands/config-profile.test.ts @@ -78,7 +78,7 @@ describe('deriveProfileFromWorkflowSelection', () => { it('returns core when selection has exactly core workflows in different order', async () => { const { deriveProfileFromWorkflowSelection } = await import('../../src/commands/config.js'); - expect(deriveProfileFromWorkflowSelection(['archive', 'sync', 'update', 'apply', 'explore', 'propose'])).toBe('core'); + expect(deriveProfileFromWorkflowSelection(['store', 'archive', 'sync', 'update', 'apply', 'explore', 'propose'])).toBe('core'); }); }); @@ -107,6 +107,7 @@ describe('config profile interactive flow', () => { 'openspec-update-change', 'openspec-sync-specs', 'openspec-archive-change', + 'openspec-store', ]; for (const dirName of coreSkillDirs) { const skillPath = path.join(projectDir, '.claude', 'skills', dirName, 'SKILL.md'); @@ -114,7 +115,7 @@ describe('config profile interactive flow', () => { fs.writeFileSync(skillPath, `name: ${dirName}\n`, 'utf-8'); } - const coreCommands = ['propose', 'explore', 'apply', 'update', 'sync', 'archive']; + const coreCommands = ['propose', 'explore', 'apply', 'update', 'sync', 'archive', 'store']; for (const commandId of coreCommands) { const commandPath = path.join(projectDir, '.claude', 'commands', 'opsx', `${commandId}.md`); fs.mkdirSync(path.dirname(commandPath), { recursive: true }); @@ -408,7 +409,7 @@ describe('config profile interactive flow', () => { const config = getGlobalConfig(); expect(config.profile).toBe('core'); expect(config.delivery).toBe('skills'); - expect(config.workflows).toEqual(['propose', 'explore', 'apply', 'update', 'sync', 'archive']); + expect(config.workflows).toEqual(['propose', 'explore', 'apply', 'update', 'sync', 'archive', 'store']); expect(select).not.toHaveBeenCalled(); expect(checkbox).not.toHaveBeenCalled(); expect(confirm).not.toHaveBeenCalled(); diff --git a/test/commands/config.test.ts b/test/commands/config.test.ts index 9d3541b686..381a6a499b 100644 --- a/test/commands/config.test.ts +++ b/test/commands/config.test.ts @@ -250,7 +250,7 @@ describe('config profile command', () => { const result = getGlobalConfig(); expect(result.profile).toBe('core'); expect(result.delivery).toBe('skills'); // preserved - expect(result.workflows).toEqual(['propose', 'explore', 'apply', 'update', 'sync', 'archive']); + expect(result.workflows).toEqual(['propose', 'explore', 'apply', 'update', 'sync', 'archive', 'store']); }); it('custom workflow selection should set profile to custom', async () => { diff --git a/test/commands/context.test.ts b/test/commands/context.test.ts index 14471afadc..edc8100ff4 100644 --- a/test/commands/context.test.ts +++ b/test/commands/context.test.ts @@ -106,6 +106,53 @@ describe('openspec context (4.1)', () => { expect(parseJson(declared).members).toHaveLength(2); }); + it("surfaces a referenced store's artifact types, work in motion, and layout", async () => { + // The upstream store defines a custom artifact type, active changes, + // and a declared layout. + const schemaDir = path.join(upstream, 'openspec', 'schemas', 'team-brief'); + fs.mkdirSync(path.join(schemaDir, 'templates'), { recursive: true }); + fs.writeFileSync( + path.join(schemaDir, 'schema.yaml'), + 'name: team-brief\nversion: 1\ndescription: Our own artifacts.\n' + + 'artifacts:\n - id: brief\n generates: brief.md\n description: x\n' + + ' template: brief.md\n requires: []\n instruction: y\n' + ); + fs.writeFileSync(path.join(schemaDir, 'templates', 'brief.md'), '# Brief\n'); + fs.mkdirSync(path.join(upstream, 'openspec', 'changes', 'smoother-setup'), { + recursive: true, + }); + fs.mkdirSync(path.join(upstream, 'openspec', 'changes', 'q3-payments'), { + recursive: true, + }); + fs.writeFileSync( + path.join(upstream, 'openspec', 'config.yaml'), + 'schema: spec-driven\nstructure:\n research/: raw inputs\n' + ); + + const json = await runCLI(['context', '--json', '--store', 'team-context'], { + cwd: tempDir, + env, + }); + const upstreamMember = parseJson(json).members.find( + (member: any) => member.id === 'upstream-context' + ); + expect(upstreamMember.artifactTypes).toEqual(['team-brief']); + expect(upstreamMember.changes).toEqual(['q3-payments', 'smoother-setup']); + expect(upstreamMember.structure).toEqual({ 'research/': 'raw inputs' }); + + const human = await runCLI(['context', '--store', 'team-context'], { + cwd: tempDir, + env, + }); + expect(human.stdout).toContain( + 'Artifact types: team-brief (openspec schemas --store upstream-context)' + ); + expect(human.stdout).toContain( + 'In motion: q3-payments, smoother-setup (openspec list --downstream --store upstream-context)' + ); + expect(human.stdout).toContain('Layout: research/ — raw inputs'); + }); + it('distinguishes self-reference omission from nothing declared', async () => { fs.writeFileSync( path.join(storeRoot, 'openspec', 'config.yaml'), diff --git a/test/commands/schema-init.test.ts b/test/commands/schema-init.test.ts new file mode 100644 index 0000000000..974cfe9277 --- /dev/null +++ b/test/commands/schema-init.test.ts @@ -0,0 +1,108 @@ +import { afterEach, beforeEach, describe, expect, it } from 'vitest'; +import * as fs from 'node:fs'; +import * as os from 'node:os'; +import * as path from 'node:path'; + +import { runCLI, type RunCLIResult } from '../helpers/run-cli.js'; + +describe('openspec schema init (CLI)', () => { + let tempDir: string; + let env: NodeJS.ProcessEnv; + + beforeEach(() => { + tempDir = fs.realpathSync.native( + fs.mkdtempSync(path.join(os.tmpdir(), 'openspec-schema-init-')) + ); + env = { + XDG_DATA_HOME: path.join(tempDir, 'data'), + XDG_CONFIG_HOME: path.join(tempDir, 'config'), + OPEN_SPEC_INTERACTIVE: '0', + OPENSPEC_TELEMETRY: '0', + }; + fs.mkdirSync(path.join(tempDir, 'openspec', 'changes'), { recursive: true }); + fs.writeFileSync( + path.join(tempDir, 'openspec', 'config.yaml'), + 'schema: spec-driven\n' + ); + }); + + afterEach(() => { + fs.rmSync(tempDir, { recursive: true, force: true }); + }); + + function parseJson(result: RunCLIResult): any { + return JSON.parse(result.stdout); + } + + it('scaffolds instruction files beside templates, seeded from the built-in guidance', async () => { + const result = await runCLI( + ['schema', 'init', 'team-flow', '--artifacts', 'proposal,specs', '--json'], + { cwd: tempDir, env } + ); + expect(result.exitCode).toBe(0); + expect(parseJson(result)).toMatchObject({ + created: true, + artifacts: ['proposal', 'specs'], + }); + + const schemaDir = path.join(tempDir, 'openspec', 'schemas', 'team-flow'); + for (const file of [ + 'schema.yaml', + path.join('instructions', 'proposal.md'), + path.join('instructions', 'specs.md'), + ]) { + expect(fs.existsSync(path.join(schemaDir, file)), file).toBe(true); + } + // Seeded from the built-in workflow's guidance, not a placeholder. + expect( + fs.readFileSync(path.join(schemaDir, 'instructions', 'proposal.md'), 'utf-8') + ).toContain('Capabilities'); + + // A change created with it resolves the file-based instructions. + const change = await runCLI( + ['new', 'change', 'try-flow', '--schema', 'team-flow', '--json'], + { cwd: tempDir, env } + ); + expect(change.exitCode).toBe(0); + expect(parseJson(change).change.schema).toBe('team-flow'); + }); + + it('writes the schema: key the config reader actually uses for --default', async () => { + const result = await runCLI( + ['schema', 'init', 'team-flow', '--artifacts', 'proposal', '--default', '--json'], + { cwd: tempDir, env } + ); + expect(result.exitCode).toBe(0); + + const config = fs.readFileSync( + path.join(tempDir, 'openspec', 'config.yaml'), + 'utf-8' + ); + expect(config).toContain('schema: team-flow'); + expect(config).not.toContain('defaultSchema'); + }); + + it('scaffolds into a registered store with --store', async () => { + const setup = await runCLI( + ['store', 'setup', 'team-hub', '--path', path.join(tempDir, 'team-hub'), '--no-init-git', '--json'], + { cwd: tempDir, env } + ); + expect(setup.exitCode).toBe(0); + + const result = await runCLI( + ['schema', 'init', 'product-flow', '--artifacts', 'proposal,specs', '--store', 'team-hub', '--json'], + { cwd: tempDir, env } + ); + expect(result.exitCode).toBe(0); + expect(parseJson(result).path).toBe( + path.join(tempDir, 'team-hub', 'openspec', 'schemas', 'product-flow') + ); + + const schemas = await runCLI(['schemas', '--store', 'team-hub', '--json'], { + cwd: tempDir, + env, + }); + const entry = parseJson(schemas).find((s: any) => s.name === 'product-flow'); + expect(entry).toMatchObject({ source: 'project' }); + }); +}); diff --git a/test/commands/store-root-selection.test.ts b/test/commands/store-root-selection.test.ts index e50147e0d9..bf6f41e6e3 100644 --- a/test/commands/store-root-selection.test.ts +++ b/test/commands/store-root-selection.test.ts @@ -724,7 +724,7 @@ describe('store root selection for normal commands', () => { { cwd: appRepo, env } ); expect(result.exitCode).toBe(0); - expect(result.stdout).toContain('openspec new change <change-id> --store fresh-context'); + expect(result.stdout).toContain('openspec new change <name> --store fresh-context'); }); it('shows --store usage after register', async () => { @@ -740,7 +740,7 @@ describe('store root selection for normal commands', () => { env, }); expect(result.exitCode).toBe(0); - expect(result.stdout).toContain('openspec new change <change-id> --store register-context'); + expect(result.stdout).toContain('openspec new change <name> --store register-context'); }); }); }); diff --git a/test/commands/store.test.ts b/test/commands/store.test.ts index 41a17a6377..839037f089 100644 --- a/test/commands/store.test.ts +++ b/test/commands/store.test.ts @@ -164,8 +164,10 @@ describe('store command', () => { ]); expect(payload.status).toEqual([]); expectHealthyOpenSpecRoot(storeRoot); + // A fresh store defaults to the documentation-only workflow: its + // changes are shared upstream requirements, not code changes. expect(fs.readFileSync(path.join(storeRoot, 'openspec', 'config.yaml'), 'utf-8')).toContain( - `schema: ${DEFAULT_OPENSPEC_SCHEMA}` + 'schema: requirements' ); expectNoGeneratedAgentOrBetaArtifacts(storeRoot); await expect(readStoreMetadataState(storeRoot)).resolves.toEqual({ @@ -229,18 +231,23 @@ describe('store command', () => { expect(process.exitCode).toBeUndefined(); }); - it('requires an explicit path for non-interactive JSON setup', async () => { - const result = await runCLI(['store', 'setup', 'team-context', '--json'], { - cwd: tempDir, - env, - }); + it('defaults the path to ~/openspec/<id> for non-interactive JSON setup', async () => { + const result = await runCLI( + ['store', 'setup', 'team-context', '--no-init-git', '--json'], + // Both spellings: os.homedir() reads HOME on POSIX, USERPROFILE on Windows. + { cwd: tempDir, env: { ...env, HOME: tempDir, USERPROFILE: tempDir } } + ); - expect(result.exitCode).toBe(1); - expect(parseJson(result).status[0]).toEqual( - expect.objectContaining({ - code: 'store_setup_path_required', - }) + expect(result.exitCode).toBe(0); + const storeRoot = path.join( + expectedExistingPath(tempDir), + 'openspec', + 'team-context' ); + expect(parseJson(result).store.root).toBe(storeRoot); + expect(fs.existsSync(getStoreMetadataPath(storeRoot))).toBe(true); + expectHealthyOpenSpecRoot(storeRoot); + // The default is a visible user-owned location, never the managed data dir. expect( fs.existsSync(path.join(getStoresDir({ globalDataDir }), 'team-context')) ).toBe(false); diff --git a/test/commands/validate-schema-aware.test.ts b/test/commands/validate-schema-aware.test.ts new file mode 100644 index 0000000000..2a4609faaf --- /dev/null +++ b/test/commands/validate-schema-aware.test.ts @@ -0,0 +1,84 @@ +import { afterEach, beforeEach, describe, expect, it } from 'vitest'; +import * as fs from 'node:fs'; +import * as os from 'node:os'; +import * as path from 'node:path'; + +import { runCLI } from '../helpers/run-cli.js'; + +describe('schema-aware delta validation', () => { + let tempDir: string; + let env: NodeJS.ProcessEnv; + + beforeEach(() => { + tempDir = fs.realpathSync.native( + fs.mkdtempSync(path.join(os.tmpdir(), 'openspec-validate-aware-')) + ); + env = { + XDG_DATA_HOME: path.join(tempDir, 'data'), + XDG_CONFIG_HOME: path.join(tempDir, 'config'), + OPEN_SPEC_INTERACTIVE: '0', + OPENSPEC_TELEMETRY: '0', + }; + fs.mkdirSync(path.join(tempDir, 'openspec', 'changes'), { recursive: true }); + fs.writeFileSync( + path.join(tempDir, 'openspec', 'config.yaml'), + 'schema: spec-driven\n' + ); + // A deliberately spec-less workflow: one freeform document, no specs/. + const schemaDir = path.join(tempDir, 'openspec', 'schemas', 'doc-only'); + fs.mkdirSync(path.join(schemaDir, 'templates'), { recursive: true }); + fs.writeFileSync( + path.join(schemaDir, 'schema.yaml'), + 'name: doc-only\nversion: 1\nartifacts:\n - id: document\n generates: document.md\n description: The document\n template: document.md\n requires: []\n' + ); + fs.writeFileSync(path.join(schemaDir, 'templates', 'document.md'), '# Doc\n'); + }); + + afterEach(() => { + fs.rmSync(tempDir, { recursive: true, force: true }); + }); + + function makeChange(id: string, schema: string): string { + const dir = path.join(tempDir, 'openspec', 'changes', id); + fs.mkdirSync(dir, { recursive: true }); + fs.writeFileSync(path.join(dir, '.openspec.yaml'), `schema: ${schema}\n`); + fs.writeFileSync( + path.join(dir, 'proposal.md'), + '## Why\nBecause.\n\n## What Changes\n- **thing:** Something\n' + ); + return dir; + } + + it('does not demand deltas from a change whose schema is spec-less', async () => { + const dir = makeChange('write-the-doc', 'doc-only'); + fs.writeFileSync(path.join(dir, 'document.md'), '# The document\n'); + + const result = await runCLI(['validate', 'write-the-doc', '--type', 'change'], { + cwd: tempDir, + env, + }); + expect(result.stdout + result.stderr).not.toContain('at least one delta'); + expect(result.exitCode).toBe(0); + }); + + it('still demands deltas when the schema defines a specs artifact', async () => { + makeChange('needs-deltas', 'spec-driven'); + + const result = await runCLI(['validate', 'needs-deltas', '--type', 'change'], { + cwd: tempDir, + env, + }); + expect(result.exitCode).toBe(1); + expect(result.stdout + result.stderr).toContain('at least one delta'); + }); + + it('falls back to demanding deltas when the schema cannot be resolved', async () => { + makeChange('unknown-schema', 'no-such-schema'); + + const result = await runCLI(['validate', 'unknown-schema', '--type', 'change'], { + cwd: tempDir, + env, + }); + expect(result.exitCode).toBe(1); + }); +}); diff --git a/test/core/archive.test.ts b/test/core/archive.test.ts index 0d039f2026..2eb3df9fdf 100644 --- a/test/core/archive.test.ts +++ b/test/core/archive.test.ts @@ -190,6 +190,60 @@ Then expected result happens`; expect(updatedContent).toContain('#### Scenario: Basic test'); }); + it('seeds a new spec\'s Purpose from the proposal\'s Why section', async () => { + const changeName = 'seeded-purpose'; + const changeDir = path.join(tempDir, 'openspec', 'changes', changeName); + const changeSpecDir = path.join(changeDir, 'specs', 'seeded-capability'); + await fs.mkdir(changeSpecDir, { recursive: true }); + + await fs.writeFile( + path.join(changeDir, 'proposal.md'), + '## Why\n\nAuditors need one answer for retention.\nToday there is none.\n\nSecond paragraph is not the Purpose.\n\n## What Changes\n- Things.\n' + ); + await fs.writeFile( + path.join(changeSpecDir, 'spec.md'), + '## ADDED Requirements\n\n### Requirement: The system SHALL retain events\n\n#### Scenario: Basic\nGiven a\nWhen b\nThen c' + ); + + await archiveCommand.execute(changeName, { yes: true, noValidate: true }); + + const mainSpecPath = path.join(tempDir, 'openspec', 'specs', 'seeded-capability', 'spec.md'); + const updatedContent = await fs.readFile(mainSpecPath, 'utf-8'); + // First Why paragraph, flattened to one line — no TBD placeholder. + expect(updatedContent).toContain( + '## Purpose\nAuditors need one answer for retention. Today there is none.' + ); + expect(updatedContent).not.toContain('TBD'); + expect(updatedContent).not.toContain('Second paragraph'); + }); + + it('prefers an explicit Purpose section in the delta over the proposal Why', async () => { + const changeName = 'delta-purpose'; + const changeDir = path.join(tempDir, 'openspec', 'changes', changeName); + const changeSpecDir = path.join(changeDir, 'specs', 'purposeful'); + await fs.mkdir(changeSpecDir, { recursive: true }); + + await fs.writeFile( + path.join(changeDir, 'proposal.md'), + '## Why\n\nProposal-level why.\n' + ); + await fs.writeFile( + path.join(changeSpecDir, 'spec.md'), + '## Purpose\nThe authoritative purpose, written with the requirements.\n\n## ADDED Requirements\n\n### Requirement: The system SHALL work\n\n#### Scenario: Basic\nGiven a\nWhen b\nThen c' + ); + + await archiveCommand.execute(changeName, { yes: true, noValidate: true }); + + const updatedContent = await fs.readFile( + path.join(tempDir, 'openspec', 'specs', 'purposeful', 'spec.md'), + 'utf-8' + ); + expect(updatedContent).toContain( + '## Purpose\nThe authoritative purpose, written with the requirements.' + ); + expect(updatedContent).not.toContain('Proposal-level why'); + }); + it('should allow REMOVED requirements when creating new spec file (issue #403)', async () => { const changeName = 'new-spec-with-removed'; const changeDir = path.join(tempDir, 'openspec', 'changes', changeName); diff --git a/test/core/artifact-graph/resolver.test.ts b/test/core/artifact-graph/resolver.test.ts index 484cc3b406..6115c3bef4 100644 --- a/test/core/artifact-graph/resolver.test.ts +++ b/test/core/artifact-graph/resolver.test.ts @@ -648,4 +648,113 @@ artifacts: expect(sharedSchema!.description).toBe('Project shared'); // project version wins }); }); + describe('schema inheritance through references', () => { + function writeSchema(dir: string, name: string, description: string): void { + fs.mkdirSync(dir, { recursive: true }); + fs.writeFileSync( + path.join(dir, 'schema.yaml'), + `name: ${name}\nversion: 1\ndescription: ${description}\nartifacts:\n - id: brief\n generates: brief.md\n description: What and why\n template: brief.md\n` + ); + } + + function setUpReferencedStore(): { projectRoot: string; storeRoot: string } { + process.env.XDG_DATA_HOME = tempDir; + const storeRoot = path.join(tempDir, 'team-hub'); + const projectRoot = path.join(tempDir, 'web-app'); + writeSchema(path.join(storeRoot, 'openspec', 'schemas', 'team-brief'), 'team-brief', 'From the store'); + fs.mkdirSync(path.join(projectRoot, 'openspec'), { recursive: true }); + fs.writeFileSync( + path.join(projectRoot, 'openspec', 'config.yaml'), + 'schema: spec-driven\nreferences:\n - team-hub\n' + ); + const storesDir = path.join(tempDir, 'openspec', 'stores'); + fs.mkdirSync(storesDir, { recursive: true }); + fs.writeFileSync( + path.join(storesDir, 'registry.yaml'), + `version: 1\nstores:\n team-hub:\n backend:\n type: git\n local_path: ${JSON.stringify(storeRoot)}\n` + ); + return { projectRoot, storeRoot }; + } + + it('resolves a schema from a referenced store when the project has none', () => { + const { projectRoot, storeRoot } = setUpReferencedStore(); + + expect(getSchemaDir('team-brief', projectRoot)).toBe( + path.join(storeRoot, 'openspec', 'schemas', 'team-brief') + ); + expect(listSchemas(projectRoot)).toContain('team-brief'); + }); + + it('lets a project-local schema shadow an inherited one', () => { + const { projectRoot } = setUpReferencedStore(); + writeSchema( + path.join(projectRoot, 'openspec', 'schemas', 'team-brief'), + 'team-brief', + 'Local override' + ); + + expect(getSchemaDir('team-brief', projectRoot)).toBe( + path.join(projectRoot, 'openspec', 'schemas', 'team-brief') + ); + }); + + it('reports inherited schemas with their store id', () => { + const { projectRoot } = setUpReferencedStore(); + + const info = listSchemasWithInfo(projectRoot).find((s) => s.name === 'team-brief'); + expect(info?.source).toBe('store'); + expect(info?.store).toBe('team-hub'); + }); + + it('degrades to no inherited schemas without a registry', () => { + const { projectRoot } = setUpReferencedStore(); + fs.rmSync(path.join(tempDir, 'openspec', 'stores', 'registry.yaml')); + + expect(getSchemaDir('team-brief', projectRoot)).toBeNull(); + }); + }); + + describe('schema folder extras', () => { + it('parses top-level notes', () => { + process.env.XDG_DATA_HOME = tempDir; + const dir = path.join(tempDir, 'openspec', 'schemas', 'noted'); + fs.mkdirSync(dir, { recursive: true }); + fs.writeFileSync( + path.join(dir, 'schema.yaml'), + 'name: noted\nversion: 1\nnotes: No implementation phase; archive after specs.\nartifacts:\n - id: brief\n generates: brief.md\n description: x\n template: brief.md\n' + ); + + const schema = resolveSchema('noted'); + expect(schema.notes).toBe('No implementation phase; archive after specs.'); + }); + + it('loads instructions/<artifact>.md over an inline instruction, and instructions/apply.md', () => { + process.env.XDG_DATA_HOME = tempDir; + const dir = path.join(tempDir, 'openspec', 'schemas', 'filed'); + fs.mkdirSync(path.join(dir, 'instructions'), { recursive: true }); + fs.writeFileSync( + path.join(dir, 'schema.yaml'), + 'name: filed\nversion: 1\nartifacts:\n - id: brief\n generates: brief.md\n description: x\n template: brief.md\n instruction: inline version\n - id: tasks\n generates: tasks.md\n description: y\n template: tasks.md\napply:\n requires: [brief]\n tracks: tasks.md\n instruction: inline apply\n' + ); + fs.writeFileSync(path.join(dir, 'instructions', 'brief.md'), 'From the file.\n'); + fs.writeFileSync(path.join(dir, 'instructions', 'apply.md'), 'Apply from the file.\n'); + + const schema = resolveSchema('filed'); + expect(schema.artifacts.find((a) => a.id === 'brief')?.instruction).toBe('From the file.'); + // No file for tasks: inline (absent) stays untouched. + expect(schema.artifacts.find((a) => a.id === 'tasks')?.instruction).toBeUndefined(); + expect(schema.apply?.instruction).toBe('Apply from the file.'); + }); + }); + describe('built-in requirements schema', () => { + it('resolves as a documentation-only workflow with notes and file instructions', () => { + const schema = resolveSchema('requirements'); + expect(schema.artifacts.map((a) => a.id)).toEqual(['proposal', 'specs']); + expect(schema.notes).toContain('no implementation phase'); + // Guidance ships as instructions/<artifact>.md files beside the schema. + expect(schema.artifacts[0].instruction).toContain('non-engineer'); + expect(schema.artifacts[1].instruction).toContain('#### Scenario'); + expect(schema.apply).toBeUndefined(); + }); + }); }); diff --git a/test/core/completions/command-registry.test.ts b/test/core/completions/command-registry.test.ts index 8ac1e0775b..0299845af5 100644 --- a/test/core/completions/command-registry.test.ts +++ b/test/core/completions/command-registry.test.ts @@ -171,6 +171,10 @@ describe('command completion registry', () => { 'instructions', 'list', 'new change', + 'schema init', + 'schema validate', + 'schema which', + 'schemas', 'show', 'status', 'validate', @@ -206,6 +210,7 @@ describe('command completion registry', () => { 'description', 'goal', 'schema', + 'serves', 'json', 'store', ]); diff --git a/test/core/openspec-root.test.ts b/test/core/openspec-root.test.ts index d2059f31e0..b41e157fc3 100644 --- a/test/core/openspec-root.test.ts +++ b/test/core/openspec-root.test.ts @@ -11,6 +11,36 @@ import { } from '../../src/core/index.js'; describe('OpenSpec root helper', () => { + it('materializes declared structure: folders and seeded files, never overwriting', async () => { + const root = path.join(tempDir, 'structured-store'); + fs.mkdirSync(path.join(root, 'openspec'), { recursive: true }); + fs.writeFileSync( + path.join(root, 'openspec', 'config.yaml'), + [ + 'schema: spec-driven', + 'structure:', + ' research/: raw inputs', + ' decisions/adr-template.md: the shape of a decision record', + ' ../escape/: must be skipped', + ].join('\n') + '\n' + ); + fs.mkdirSync(path.join(root, 'openspec', 'research'), { recursive: true }); + fs.writeFileSync(path.join(root, 'openspec', 'research', 'kept.md'), 'existing\n'); + + const result = await ensureOpenSpecRoot(root, { anchorEmptyDirectories: true }); + + // Declared file seeded with its purpose; existing content untouched; + // escaping keys ignored. + expect( + fs.readFileSync(path.join(root, 'openspec', 'decisions', 'adr-template.md'), 'utf-8') + ).toContain('the shape of a decision record'); + expect(fs.readFileSync(path.join(root, 'openspec', 'research', 'kept.md'), 'utf-8')).toBe( + 'existing\n' + ); + expect(fs.existsSync(path.join(root, 'escape'))).toBe(false); + expect(result.createdArtifacts).toContain('openspec/decisions/adr-template.md'); + }); + let tempDir: string; beforeEach(() => { diff --git a/test/core/profiles.test.ts b/test/core/profiles.test.ts index b06456e016..a0badab354 100644 --- a/test/core/profiles.test.ts +++ b/test/core/profiles.test.ts @@ -9,7 +9,7 @@ import { describe('profiles', () => { describe('CORE_WORKFLOWS', () => { it('should contain the default core workflows', () => { - expect(CORE_WORKFLOWS).toEqual(['propose', 'explore', 'apply', 'update', 'sync', 'archive']); + expect(CORE_WORKFLOWS).toEqual(['propose', 'explore', 'apply', 'update', 'sync', 'archive', 'store']); }); it('should include update in the core profile (default install, not expanded-only)', () => { @@ -24,14 +24,14 @@ describe('profiles', () => { }); describe('ALL_WORKFLOWS', () => { - it('should contain all 12 workflows', () => { - expect(ALL_WORKFLOWS).toHaveLength(12); + it('should contain all 13 workflows', () => { + expect(ALL_WORKFLOWS).toHaveLength(13); }); it('should contain expected workflow IDs', () => { const expected = [ 'propose', 'explore', 'new', 'continue', 'apply', 'update', - 'ff', 'sync', 'archive', 'bulk-archive', 'verify', 'onboard', + 'ff', 'sync', 'archive', 'bulk-archive', 'verify', 'onboard', 'store', ]; expect([...ALL_WORKFLOWS]).toEqual(expected); }); diff --git a/test/core/project-config.test.ts b/test/core/project-config.test.ts index 02173d5691..3951c2d922 100644 --- a/test/core/project-config.test.ts +++ b/test/core/project-config.test.ts @@ -3,6 +3,7 @@ import * as fs from 'node:fs'; import * as path from 'node:path'; import * as os from 'node:os'; import { + addReferenceToProjectConfig, readProjectConfig, validateConfigRules, suggestSchemas, @@ -693,4 +694,77 @@ rules: expect(message).toContain('Available schemas:'); }); }); + + describe('structure field', () => { + it('parses a folder → purpose map and drops invalid entries', () => { + const configPath = path.join(tempDir, 'openspec', 'config.yaml'); + fs.mkdirSync(path.dirname(configPath), { recursive: true }); + fs.writeFileSync( + configPath, + 'schema: spec-driven\nstructure:\n research/: raw inputs\n decisions/: standing decisions\n bad/: 42\n', + 'utf-8' + ); + + expect(readProjectConfig(tempDir)?.structure).toEqual({ + 'research/': 'raw inputs', + 'decisions/': 'standing decisions', + }); + }); + + it('ignores a non-map structure field', () => { + const configPath = path.join(tempDir, 'openspec', 'config.yaml'); + fs.mkdirSync(path.dirname(configPath), { recursive: true }); + fs.writeFileSync(configPath, 'schema: spec-driven\nstructure: nope\n', 'utf-8'); + + expect(readProjectConfig(tempDir)?.structure).toBeUndefined(); + }); + }); + + describe('addReferenceToProjectConfig', () => { + function writeConfig(content: string): string { + const configPath = path.join(tempDir, 'openspec', 'config.yaml'); + fs.mkdirSync(path.dirname(configPath), { recursive: true }); + fs.writeFileSync(configPath, content, 'utf-8'); + return configPath; + } + + it('adds a references list when none exists, preserving comments', () => { + const configPath = writeConfig('# my config\nschema: spec-driven\n'); + + expect(addReferenceToProjectConfig(tempDir, 'team-plans')).toBe('added'); + + const content = fs.readFileSync(configPath, 'utf-8'); + expect(content).toContain('# my config'); + expect(readProjectConfig(tempDir)?.references).toEqual([ + { id: 'team-plans' }, + ]); + }); + + it('appends to an existing list and is idempotent', () => { + writeConfig('schema: spec-driven\nreferences:\n - other-store\n'); + + expect(addReferenceToProjectConfig(tempDir, 'team-plans')).toBe('added'); + expect(addReferenceToProjectConfig(tempDir, 'team-plans')).toBe('already'); + + expect(readProjectConfig(tempDir)?.references).toEqual([ + { id: 'other-store' }, + { id: 'team-plans' }, + ]); + }); + + it('recognizes an existing {id, remote} entry', () => { + writeConfig( + 'schema: spec-driven\nreferences:\n - id: team-plans\n remote: git@example.com:plans.git\n' + ); + + expect(addReferenceToProjectConfig(tempDir, 'team-plans')).toBe('already'); + }); + + it('skips instead of guessing: missing config or a non-list references', () => { + expect(addReferenceToProjectConfig(tempDir, 'team-plans')).toBe('skipped'); + + writeConfig('schema: spec-driven\nreferences: not-a-list\n'); + expect(addReferenceToProjectConfig(tempDir, 'team-plans')).toBe('skipped'); + }); + }); }); diff --git a/test/core/references.test.ts b/test/core/references.test.ts index c129e9e420..a790cc9f25 100644 --- a/test/core/references.test.ts +++ b/test/core/references.test.ts @@ -130,6 +130,63 @@ describe('reference index assembly', () => { expect(entries[0].status).toEqual([]); }); + it("indexes a store's own artifact types, work in motion, and layout", async () => { + const storeRoot = await registerStore('team-context'); + // A project-local schema (custom artifact types) in the store. + const schemaDir = path.join(storeRoot, 'openspec', 'schemas', 'team-brief'); + fs.mkdirSync(path.join(schemaDir, 'templates'), { recursive: true }); + fs.writeFileSync( + path.join(schemaDir, 'schema.yaml'), + 'name: team-brief\nversion: 1\ndescription: Our own planning artifacts.\n' + + 'artifacts:\n - id: brief\n generates: brief.md\n description: What and why\n' + + ' template: brief.md\n requires: []\n instruction: Write it.\n' + ); + fs.writeFileSync(path.join(schemaDir, 'templates', 'brief.md'), '# Brief\n'); + // Work in motion: active changes in the store. + const changesDir = path.join(storeRoot, 'openspec', 'changes'); + fs.mkdirSync(path.join(changesDir, 'smoother-setup'), { recursive: true }); + fs.mkdirSync(path.join(changesDir, 'q3-payments'), { recursive: true }); + // Declared layout in the store's config. + fs.writeFileSync( + path.join(storeRoot, 'openspec', 'config.yaml'), + 'schema: team-brief\nstructure:\n research/: raw inputs — interviews, transcripts\n' + ); + + const [entry] = await assemble(['team-context']); + + expect(entry.schemas).toEqual([ + { + id: 'team-brief', + summary: 'Our own planning artifacts.', + artifacts: ['brief'], + }, + ]); + expect(entry.changes).toEqual(['q3-payments', 'smoother-setup']); + expect(entry.structure).toEqual({ + 'research/': 'raw inputs — interviews, transcripts', + }); + + const block = renderReferencedStoresBlock([entry]); + expect(block).toContain( + 'Artifact types (openspec schemas --store team-context):' + ); + expect(block).toContain('- team-brief: Our own planning artifacts. [brief]'); + expect(block).toContain( + 'In motion: q3-payments, smoother-setup (openspec list --downstream --store team-context)' + ); + expect(block).toContain('- research/: raw inputs — interviews, transcripts'); + }); + + it('omits schemas/changes/structure keys for a store that has none', async () => { + await registerStore('bare-context'); + + const [entry] = await assemble(['bare-context']); + + expect(entry.schemas).toBeUndefined(); + expect(entry.changes).toBeUndefined(); + expect(entry.structure).toBeUndefined(); + }); + it('degrades an unregistered reference to reference_unresolved with a pasteable fix', async () => { const entries = await assemble(['missing-context']); diff --git a/test/core/shared/skill-generation.test.ts b/test/core/shared/skill-generation.test.ts index 5f4bba9d55..c806c906b3 100644 --- a/test/core/shared/skill-generation.test.ts +++ b/test/core/shared/skill-generation.test.ts @@ -8,9 +8,9 @@ import { describe('skill-generation', () => { describe('getSkillTemplates', () => { - it('should return all 12 skill templates', () => { + it('should return all 13 skill templates', () => { const templates = getSkillTemplates(); - expect(templates).toHaveLength(12); + expect(templates).toHaveLength(13); }); it('should have unique directory names', () => { @@ -89,9 +89,9 @@ describe('skill-generation', () => { }); describe('getCommandTemplates', () => { - it('should return all 12 command templates', () => { + it('should return all 13 command templates', () => { const templates = getCommandTemplates(); - expect(templates).toHaveLength(12); + expect(templates).toHaveLength(13); }); it('should have unique IDs', () => { @@ -144,9 +144,9 @@ describe('skill-generation', () => { }); describe('getCommandContents', () => { - it('should return all 12 command contents', () => { + it('should return all 13 command contents', () => { const contents = getCommandContents(); - expect(contents).toHaveLength(12); + expect(contents).toHaveLength(13); }); it('should have valid content structure', () => { diff --git a/test/core/templates/skill-templates-parity.test.ts b/test/core/templates/skill-templates-parity.test.ts index 65202e9e6e..ea23ea3ab8 100644 --- a/test/core/templates/skill-templates-parity.test.ts +++ b/test/core/templates/skill-templates-parity.test.ts @@ -37,46 +37,46 @@ import { import { STORE_SELECTION_GUIDANCE } from '../../../src/core/templates/workflows/store-selection.js'; const EXPECTED_FUNCTION_HASHES: Record<string, string> = { - getExploreSkillTemplate: '7d2f54e74fffcb36aaaa4498a4a8b033142bb25945fb9b2de532354acbe76b9c', - getNewChangeSkillTemplate: '39663a6d2037e6697020393a66f6327506e3e3bc573b7a3556dcb7f9457dc51d', - getContinueChangeSkillTemplate: '1bb28875d6e5946ea2ec5f12e90f55d9784c2fa1f6e4c4e2d0eda53d861d4c75', - getApplyChangeSkillTemplate: '0f5a15fc7fb9ad6059a5643d0e01365d27642637a4aaebf182f9eabb45348197', - getFfChangeSkillTemplate: '9f4c12a1c58c723c9c45a139307eb90caf39cedd93c435bc960d0817328875e2', - getSyncSpecsSkillTemplate: '75abb20572256e2b8a647e77befae99f109ab5c4dc954a9c3c184829b5fcaa40', - getOnboardSkillTemplate: 'e871d8ce172bb805ae62a7611aee7a3154d89414f427ad5ef31721c903f13002', - getOpsxExploreCommandTemplate: '37e53590aae7ac6621d4393aa80a5b8af21881323887fa924ed329199fda27e0', - getOpsxNewCommandTemplate: '57c600cce318d16b9b4308a18d0d983ea3c0673034e606a7cceec07b4c705e87', - getOpsxContinueCommandTemplate: '418108b417107a87019d4020b26c105792d2ef0110fe6920445e255889216716', - getOpsxApplyCommandTemplate: 'daeb507206707169de73c828e199648dde5732cbc17791ef2a027adffd028574', - getOpsxFfCommandTemplate: '36973ae0dd00ab169fbaaa42bf565f97e1bc97cf63ae7c07307734cc1ca8c1fd', - getArchiveChangeSkillTemplate: 'c511a1c943bcfc5f9f3833b8c0ff284b22d34864a08f5f553cec471ee485d38f', - getBulkArchiveChangeSkillTemplate: '0f635913757ae3d1609e111f4a8f699443ca47cbaaf8a1b21eb652f7b96a1d13', - getOpsxSyncCommandTemplate: '86cf706886d0f18069e2cfa16948b7357028fd348210efb58588c88c416d8622', - getVerifyChangeSkillTemplate: 'd718c79aad649223a73fdb11036c93fb3842ac5a780f4934d50bfa03c9692683', - getOpsxArchiveCommandTemplate: '6985bddb310cb45b6b50350bfcebe31bf67146135ca0084c94930920280970a4', - getOpsxOnboardCommandTemplate: '0673f34a0f81fd173bcfb8c3ac83e2b1c617f7b7564e24e5298d3bd5665a05a9', - getOpsxBulkArchiveCommandTemplate: '9f444fc7b27a5b788077b5e3aa4f61af45aa8c8004ac8d899d204fa362ff89b7', - getOpsxVerifyCommandTemplate: '011509480a20a60342c993906f0f9280c0e9ba5d019d335bdc1ef4d53213a5a8', - getOpsxProposeSkillTemplate: '8dfb5e9c719d5ba547aff0d3953c076dca6b33d7223be98cbffc396b8f1e0048', - getOpsxProposeCommandTemplate: '7cd569beb32d99cdabd0b49615a8245160a8e152b6ea67a99fc4dd71e3f39f50', + getExploreSkillTemplate: '3bfeae699013c46c0ad0018afd85b0ca931c5a7ddf8ce3c18aab22f06d1d354d', + getNewChangeSkillTemplate: '63d2eb80b6e746a562c9273df4048d228d3aa025f9a921632a5855f751e727ba', + getContinueChangeSkillTemplate: '09a58562acebd99b6738c45095bc423c3ce48388335e349aa59c7cb83ffab426', + getApplyChangeSkillTemplate: '76cc8caf6a58e946b03d14eb81dd02af45a209d040e249b8e12811e2423012e8', + getFfChangeSkillTemplate: '61389a5c3b9aa7405ac9e355269bd35f6a48415c041a3abd5cda1210afb4a7f7', + getSyncSpecsSkillTemplate: 'd8a19a90678d84ddfbcb3cd98e06806efe3e32f2965a7a29fd970e29f50f0522', + getOnboardSkillTemplate: 'fd10e5d08562888fbb95eec3bf1b765c90d24bbadfd886b9ae0eb821423eb4ab', + getOpsxExploreCommandTemplate: '152372aee423777e9d5637199d194bdba224fc3caff6dc3643c0d9b6ad4fbf3d', + getOpsxNewCommandTemplate: '14a3027294b0117b653c3d03f04ec89b951b5fe59153be3ae00377cf7cc6d6f1', + getOpsxContinueCommandTemplate: '332a4d8c47f1d477bab725946454551de5031e3c4d6551f040b6806c81a53b9e', + getOpsxApplyCommandTemplate: 'ab2efb2a9c3e47cfd12cf777575af93971f424343e8a318cbcfc4ed465d6e3ea', + getOpsxFfCommandTemplate: 'edb5e1a9ecff5be9eb7f952356531fb899a3c88fa70663037ed4bdeeb30aa0a8', + getArchiveChangeSkillTemplate: 'a630a79be1c563d313f75ed3f038b157cd9ad33e28b2805dd7fbb7dc766e2f86', + getBulkArchiveChangeSkillTemplate: 'b251e738bc06d7e8e9e55e8c3e7591c452e3ca42a185965c5e7beeb2c6870bec', + getOpsxSyncCommandTemplate: 'b25f62a21ad9b3eda0af79f3e8730814a44b9baa0ea8da6276125e845f59e823', + getVerifyChangeSkillTemplate: '72d800b35725c65e1d11144a4000b74bb2c6c1809c351958b193e0502b83d3f4', + getOpsxArchiveCommandTemplate: 'dc8c8ada7e068986bd83c4ba3755f111fb7abf2b234d3c42b744e552276407b3', + getOpsxOnboardCommandTemplate: 'a0a3a0bd3005757cf8c64f2a4f5b80e90aca6179723ae793843baaab822ebe7f', + getOpsxBulkArchiveCommandTemplate: 'fa10bb635ad5df4a42963b333a9d42d5da098b2577321a08c96de75bb83fb0eb', + getOpsxVerifyCommandTemplate: 'dd00c7601f579e65968035776b28e75193d713300e9776f7bcdd14dbecd2fb63', + getOpsxProposeSkillTemplate: 'a96cb461f59a562cfda0acecd6361d5e70497a1800d6a3c73afc35f2b18dae53', + getOpsxProposeCommandTemplate: '64d236912ec56432c34b7ee810bca47e20a4cc3befbf1c9570909163f4f05fcd', getFeedbackSkillTemplate: 'd7d83c5f7fc2b92fe8f4588a5bf2d9cb315e4c73ec19bcd5ef28270906319a0d', - getUpdateChangeSkillTemplate: 'fe2e8edaf973d42dc7fc7dfd846105c4c3cfec0437606e582ec644985cd4e81d', - getOpsxUpdateCommandTemplate: 'e55ac5774203a7d9037d2d588889c97c53f3f930da49497cc79e865375920da7', + getUpdateChangeSkillTemplate: '84cf42664f863b89447c4fadeb666f5b16936d10f143139e7bbbe6846da87f2e', + getOpsxUpdateCommandTemplate: 'c58d3534fa477e67bd35400848620784a2a69749c2e1703ab11cf7dca28b211a', }; const EXPECTED_GENERATED_SKILL_CONTENT_HASHES: Record<string, string> = { - 'openspec-explore': 'ba099821631ce75ee70af370917bbddbc88d0882ad0e50e91ed687d2185102ef', - 'openspec-new-change': 'd5b8909bea70a33b7a312b38ce204a91f40b6bb2bff12c4c06b3e11641b6a689', - 'openspec-continue-change': '39b4467a4873cde7c97d52c80d53ac647b220bf7c9d96f4e6505f3188e1a1642', - 'openspec-apply-change': '09c0e1cdf5ccc82416d0969d6bd715cc70616bdbc3531358a5c36057f78be55a', - 'openspec-ff-change': '8d5a8890eccbd97d714fbab1d73472f79ad9104b519e000264ae43d752cdf631', - 'openspec-sync-specs': 'f6a1581eb11a30061795c42582db6fa4f5e1f213b4b7cad9f3cbfbe3e9fb2d97', - 'openspec-archive-change': '1821aee5a06afd895d59d1e1d16495e484b6087ecf59ec93460d7d5e7851e772', - 'openspec-bulk-archive-change': '7b09b04a440809dd7dbf0b1d7b695cbb8c41184d8d104eb32e82d7cdfb476d18', - 'openspec-verify-change': '9a8735eaaa34c278d2193eb32fa736f4b111d1c47e675971c8df40f81d20c8c3', - 'openspec-onboard': 'b1b6fc9a1b3ff64dafe9b8c39a761ee1bd001b542d47b4e4deaf058e0aa21256', - 'openspec-propose': '0cfc9278123d973929cb4da3ea7ac8ae1b6c84b472eed4fb753657b8347eaeb9', - 'openspec-update-change': '77ff4d1f1cd08a57649cce1f25e0ebc4f55d6d032dfde5c301d1b479561b72fa', + 'openspec-explore': '74bd0a2abc09559af858195ddc187800e70686569a8a9e283cd72a0607442b35', + 'openspec-new-change': '986235a54f883526a3cd876348f30c5c4929371f257b84e1370dfafc910713c1', + 'openspec-continue-change': 'e82cf91e7833c21de29e537c256ee7acca9d92fb29275a3e126975cf2d8e2b91', + 'openspec-apply-change': '3f8eae937f9fc87967fbed44a9f20aabc6668ddad2aa53762a5621591cdaec8f', + 'openspec-ff-change': '9e997859748f54b75399c21c0896792912f719b9ddd1eabae3746153d31d311c', + 'openspec-sync-specs': 'be314dea2f1e26aca3d8fbc37dad1077b6d83fdb9cb6f59b2415f5eb91e9eca3', + 'openspec-archive-change': 'f99716c8c02a0dcdbc148e5cba64cb188952f1a0933ee4ebd8fb1aba701e2d8f', + 'openspec-bulk-archive-change': 'a2518c0cd3eebe817338be87743a1be07ab28f17e183c7eb499f705af0c863c2', + 'openspec-verify-change': 'c8e32ad379f5ab120ee2df54d17b1fb2d24ffde4b774e06e66e2eaafb2ffc150', + 'openspec-onboard': '34ca708adb63d6b011d65021b2807ca0f6b5078d10026fd4606df043a9e1d608', + 'openspec-propose': '21bc7e1756fd7a58a9776ea272a46510361e99b84ba5435956e74700ea25c8ac', + 'openspec-update-change': '9507860650a2f3fa13d3eab4299e2b56bdf90263f49b457dc5d641e28afdaa75', }; // Intentionally excludes getFeedbackSkillTemplate: this list only models templates diff --git a/test/core/upstream.test.ts b/test/core/upstream.test.ts new file mode 100644 index 0000000000..b466e714ae --- /dev/null +++ b/test/core/upstream.test.ts @@ -0,0 +1,357 @@ +import { afterEach, beforeEach, describe, expect, it } from 'vitest'; +import * as fs from 'node:fs'; +import * as os from 'node:os'; +import * as path from 'node:path'; + +import { + readLinkedRoots, + readServesRef, + recordLinkedRoot, + resolveUpstreamLink, + rollupDownstream, + rollupRegisteredStores, +} from '../../src/core/upstream.js'; +import { + readStoreRegistryState, + writeStoreRegistryState, +} from '../../src/core/store/foundation.js'; + +describe('upstream links', () => { + let tempDir: string; + let globalDataDir: string; + let savedXdgDataHome: string | undefined; + + beforeEach(() => { + tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'openspec-upstream-')); + globalDataDir = path.join(tempDir, 'data', 'openspec'); + savedXdgDataHome = process.env.XDG_DATA_HOME; + process.env.XDG_DATA_HOME = path.join(tempDir, 'xdg'); + }); + + afterEach(() => { + if (savedXdgDataHome === undefined) { + delete process.env.XDG_DATA_HOME; + } else { + process.env.XDG_DATA_HOME = savedXdgDataHome; + } + fs.rmSync(tempDir, { recursive: true, force: true }); + }); + + function write(relativePath: string, content: string): void { + const full = path.join(tempDir, relativePath); + fs.mkdirSync(path.dirname(full), { recursive: true }); + fs.writeFileSync(full, content); + } + + // Fixtures use a schema name that resolves nowhere on purpose: artifact + // progress degrades to null and the aggregation tests stay about tasks. + // Artifact-aware completion has its own test below. + function change(root: string, id: string, ref: string | null, tasks: string): void { + const metadata = + ref === null + ? 'schema: fixture-workflow\n' + : `schema: fixture-workflow\nserves: ${ref}\n`; + write(`${root}/openspec/changes/${id}/.openspec.yaml`, metadata); + write(`${root}/openspec/changes/${id}/tasks.md`, tasks); + } + + async function registerStore(id: string, root: string): Promise<void> { + const existing = await readStoreRegistryState({ globalDataDir }).catch(() => null); + await writeStoreRegistryState( + { + version: 1, + stores: { + ...(existing?.stores ?? {}), + [id]: { backend: { type: 'git', local_path: path.join(tempDir, root) } }, + }, + }, + { globalDataDir } + ); + } + + describe('readServesRef', () => { + it('reads the serves line and tolerates absent or unreadable metadata', async () => { + change('app', 'add-search', 'team-hub/onboarding-revamp', '- [ ] task\n'); + change('app', 'unlinked', null, '- [ ] task\n'); + + const changesDir = path.join(tempDir, 'app', 'openspec', 'changes'); + expect(await readServesRef(path.join(changesDir, 'add-search'))).toBe( + 'team-hub/onboarding-revamp' + ); + expect(await readServesRef(path.join(changesDir, 'unlinked'))).toBeNull(); + expect(await readServesRef(path.join(changesDir, 'missing'))).toBeNull(); + }); + }); + + describe('linked roots', () => { + it('records roots idempotently and reads them back sorted', async () => { + const options = { globalDataDir }; + await recordLinkedRoot(path.join(tempDir, 'b-repo'), options); + await recordLinkedRoot(path.join(tempDir, 'a-repo'), options); + await recordLinkedRoot(path.join(tempDir, 'b-repo'), options); + + expect(await readLinkedRoots(options)).toEqual([ + path.resolve(tempDir, 'a-repo'), + path.resolve(tempDir, 'b-repo'), + ]); + }); + + it('reads unreadable state as none', async () => { + expect(await readLinkedRoots({ globalDataDir })).toEqual([]); + }); + }); + + describe('rollupDownstream in a plain repo', () => { + it('groups local serving changes under the changes they serve', async () => { + change('app', 'onboarding-revamp', null, ''); + change('app', 'add-search', 'onboarding-revamp', '- [x] one\n- [ ] two\n'); + change('app', 'add-tour', 'onboarding-revamp', '- [x] only\n'); + change('app', 'unrelated', null, '- [ ] task\n'); + + const rollup = await rollupDownstream(path.join(tempDir, 'app'), { globalDataDir }); + + const entry = rollup?.upstream.find((u) => u.id === 'onboarding-revamp'); + expect(entry?.exists).toBe(true); + expect(entry?.archived).toBe(false); + expect(entry?.changes.map((c) => c.id)).toEqual(['add-search', 'add-tour']); + expect(entry?.changesComplete).toBe(1); + expect(entry?.changesTotal).toBe(2); + expect(entry?.tasksComplete).toBe(2); + expect(entry?.tasksTotal).toBe(3); + + const unrelated = rollup?.upstream.find((u) => u.id === 'unrelated'); + expect(unrelated?.changesTotal).toBe(0); + }); + + it('does not call a change complete on tasks alone when artifacts are missing', async () => { + change('app', 'onboarding-revamp', null, ''); + // spec-driven resolves (built-in): tasks done, but only tasks.md of + // four artifacts exists — the whole change is not done. + write( + 'app/openspec/changes/half-done/.openspec.yaml', + 'schema: spec-driven\nserves: onboarding-revamp\n' + ); + write('app/openspec/changes/half-done/tasks.md', '- [x] all checked\n'); + + const rollup = await rollupDownstream(path.join(tempDir, 'app'), { globalDataDir }); + const entry = rollup?.upstream.find((u) => u.id === 'onboarding-revamp'); + const serving = entry?.changes.find((c) => c.id === 'half-done'); + + expect(serving?.state).toBe('in-progress'); + expect(serving?.completedTasks).toBe(1); + expect(serving?.totalArtifacts).toBe(4); + expect(serving?.completedArtifacts).toBe(1); + expect(entry?.changesComplete).toBe(0); + }); + + it('hides serving-only changes from upstream rows', async () => { + change('app', 'onboarding-revamp', null, ''); + change('app', 'add-search', 'onboarding-revamp', '- [ ] one\n'); + + const rollup = await rollupDownstream(path.join(tempDir, 'app'), { globalDataDir }); + + // add-search is downstream work: it serves something and nothing + // serves it, so it appears under its upstream, not as its own row. + expect(rollup?.upstream.map((u) => u.id)).toEqual(['onboarding-revamp']); + }); + + it('keeps refs to missing changes visible instead of dropping them', async () => { + change('app', 'add-search', 'no-such-change', '- [ ] task\n'); + + const rollup = await rollupDownstream(path.join(tempDir, 'app'), { globalDataDir }); + + const entry = rollup?.upstream.find((u) => u.id === 'no-such-change'); + expect(entry?.exists).toBe(false); + expect(entry?.changes.map((c) => c.id)).toEqual(['add-search']); + }); + + it('resolves refs to archived changes and marks them archived', async () => { + write( + 'app/openspec/changes/archive/2026-07-01-onboarding-revamp/.openspec.yaml', + 'schema: spec-driven\n' + ); + change('app', 'add-search', 'onboarding-revamp', '- [x] done\n'); + + const rollup = await rollupDownstream(path.join(tempDir, 'app'), { globalDataDir }); + + const entry = rollup?.upstream.find((u) => u.id === 'onboarding-revamp'); + expect(entry?.exists).toBe(true); + expect(entry?.archived).toBe(true); + }); + + it('returns null when the root has no changes folder', async () => { + fs.mkdirSync(path.join(tempDir, 'app', 'openspec'), { recursive: true }); + expect(await rollupDownstream(path.join(tempDir, 'app'), { globalDataDir })).toBeNull(); + }); + }); + + describe('rollupDownstream for a registered store', () => { + it('scans other stores and linked repos for store-qualified refs', async () => { + await registerStore('team-hub', 'team-hub'); + await registerStore('other-store', 'other-store'); + change('team-hub', 'onboarding-revamp', null, ''); + // A change in another registered store serving team-hub's change. + change('other-store', 'update-docs', 'team-hub/onboarding-revamp', '- [x] a\n'); + // A change in a plain linked repo. + change('web-app', 'add-tour', 'team-hub/onboarding-revamp', '- [ ] b\n'); + await recordLinkedRoot(path.join(tempDir, 'web-app'), { globalDataDir }); + // A self-qualified ref inside the store itself. + change('team-hub', 'refine-copy', 'team-hub/onboarding-revamp', '- [x] c\n'); + + const rollup = await rollupDownstream(path.join(tempDir, 'team-hub'), { + globalDataDir, + }); + + const entry = rollup?.upstream.find((u) => u.id === 'onboarding-revamp'); + expect(entry?.changes).toEqual([ + expect.objectContaining({ id: 'add-tour', repo: 'web-app' }), + expect.objectContaining({ id: 'refine-copy' }), + expect.objectContaining({ id: 'update-docs', store: 'other-store' }), + ]); + expect(entry?.changesComplete).toBe(2); + expect(entry?.changesTotal).toBe(3); + }); + + it('ignores refs qualified for a different store', async () => { + await registerStore('team-hub', 'team-hub'); + change('team-hub', 'onboarding-revamp', null, ''); + change('web-app', 'add-tour', 'elsewhere/onboarding-revamp', '- [ ] b\n'); + await recordLinkedRoot(path.join(tempDir, 'web-app'), { globalDataDir }); + + const rollup = await rollupDownstream(path.join(tempDir, 'team-hub'), { + globalDataDir, + }); + + const entry = rollup?.upstream.find((u) => u.id === 'onboarding-revamp'); + expect(entry?.changes).toEqual([]); + }); + }); + + describe('scan roots (stateless CI path)', () => { + it('rolls up serving changes from extraRoots without any linked-root state', async () => { + await registerStore('team-hub', 'team-hub'); + change('team-hub', 'onboarding-revamp', null, ''); + change('checkouts/web-app', 'add-tour', 'team-hub/onboarding-revamp', '- [x] a\n'); + change('checkouts/api', 'add-endpoint', 'team-hub/onboarding-revamp', '- [ ] b\n'); + + const { expandScanDirs } = await import('../../src/core/upstream.js'); + const extraRoots = await expandScanDirs([path.join(tempDir, 'checkouts')]); + expect(extraRoots).toHaveLength(2); + + const rollup = await rollupDownstream(path.join(tempDir, 'team-hub'), { + globalDataDir, + extraRoots, + }); + const entry = rollup?.upstream.find((u) => u.id === 'onboarding-revamp'); + expect(entry?.changes.map((c) => `${c.repo}/${c.id}`)).toEqual([ + 'api/add-endpoint', + 'web-app/add-tour', + ]); + }); + }); + + describe('rollupRegisteredStores', () => { + it('returns rollups for every registered store with changes', async () => { + await registerStore('team-hub', 'team-hub'); + await registerStore('empty-store', 'empty-store'); + change('team-hub', 'onboarding-revamp', null, ''); + fs.mkdirSync(path.join(tempDir, 'empty-store', 'openspec'), { recursive: true }); + + const rollups = await rollupRegisteredStores({ globalDataDir }); + + expect(rollups.map((r) => r.store)).toEqual(['team-hub']); + expect(rollups[0].rollup.upstream.map((u) => u.id)).toEqual([ + 'onboarding-revamp', + ]); + }); + }); + + describe('resolveUpstreamLink', () => { + it('resolves a same-root ref to the upstream change directory', async () => { + change('app', 'onboarding-revamp', null, ''); + change('app', 'add-search', 'onboarding-revamp', '- [ ] a\n'); + + const link = await resolveUpstreamLink( + path.join(tempDir, 'app', 'openspec', 'changes', 'add-search'), + path.join(tempDir, 'app'), + { globalDataDir } + ); + + expect(link).toEqual({ + ref: 'onboarding-revamp', + changeId: 'onboarding-revamp', + path: path.join(tempDir, 'app', 'openspec', 'changes', 'onboarding-revamp'), + archived: false, + }); + }); + + it('resolves a store-qualified ref through the registry', async () => { + await registerStore('team-hub', 'team-hub'); + change('team-hub', 'onboarding-revamp', null, ''); + change('web-app', 'add-tour', 'team-hub/onboarding-revamp', '- [ ] a\n'); + + const link = await resolveUpstreamLink( + path.join(tempDir, 'web-app', 'openspec', 'changes', 'add-tour'), + path.join(tempDir, 'web-app'), + { globalDataDir } + ); + + expect(link?.store).toBe('team-hub'); + expect(link?.path).toBe( + path.join(tempDir, 'team-hub', 'openspec', 'changes', 'onboarding-revamp') + ); + }); + + it('resolves an archived upstream change and marks it archived', async () => { + await registerStore('team-hub', 'team-hub'); + write( + 'team-hub/openspec/changes/archive/2026-07-01-onboarding-revamp/proposal.md', + '# done\n' + ); + change('web-app', 'add-tour', 'team-hub/onboarding-revamp', '- [ ] a\n'); + + const link = await resolveUpstreamLink( + path.join(tempDir, 'web-app', 'openspec', 'changes', 'add-tour'), + path.join(tempDir, 'web-app'), + { globalDataDir } + ); + + expect(link?.archived).toBe(true); + expect(link?.path).toBe( + path.join( + tempDir, + 'team-hub', + 'openspec', + 'changes', + 'archive', + '2026-07-01-onboarding-revamp' + ) + ); + }); + + it('keeps a stale link visible with path null', async () => { + change('web-app', 'add-tour', 'unregistered/onboarding-revamp', '- [ ] a\n'); + + const link = await resolveUpstreamLink( + path.join(tempDir, 'web-app', 'openspec', 'changes', 'add-tour'), + path.join(tempDir, 'web-app'), + { globalDataDir } + ); + + expect(link?.path).toBeNull(); + expect(link?.ref).toBe('unregistered/onboarding-revamp'); + }); + + it('returns null for a change that serves nothing', async () => { + change('app', 'plain', null, '- [ ] a\n'); + + expect( + await resolveUpstreamLink( + path.join(tempDir, 'app', 'openspec', 'changes', 'plain'), + path.join(tempDir, 'app'), + { globalDataDir } + ) + ).toBeNull(); + }); + }); +});