docs: rebuild docs site from docs-lab (#1649)

* docs: rebuild docs site from docs-lab

Replace the docs site's source tree with docs-lab, a page-by-page rebuild
of the OpenSpec docs (40 pages: Start / Guides / Customize / Multi-repo /
Reference / Help).

- Point website/docs.sync.config.mjs at ../docs-lab and restructure the
  sidebar into nested groups; sync script gains nested meta.json emission,
  leading-quote descriptions, idempotent writes, and diagram asset copying
- Remove the marketing landing page; / now redirects to /docs
  (meta-refresh page + Cloudflare _redirects)
- Add remark plugins (faq, file-steps, gfm-alert) and the FileSteps
  component backing the new page formats
- Add install.md at the repo root, curled by docs-lab/start/installation.md
  as an agent-executable install prompt
- Add the docs authoring skills (.agents/skills/{write,draft,verify}-
  openspec-docs); docs-lab/README.md links into write-openspec-docs

The old docs/ tree is now unused by the site and left for a follow-up.

Claude-Session: https://claude.ai/code/session_01BMMLYNJQPKXx1QHpnDn4ho

* docs: hold back unwritten pages, add worksets, drop diagram drafts

- website: comment out Overview, Guides, Architecture, Help, Legacy in
  docs.sync.config.mjs until those pages are written; temporary
  /docs -> /docs/installation redirect (Cloudflare _redirects + static
  export meta-refresh fallback in page.tsx)
- docs-lab: new multi-repo/worksets.md page, published under Multi-repo
- docs-lab: content revisions across start/, customize/, reference/,
  help/, multi-repo/; add review notes (Notes.md)
- remove docs-lab/diagrams option-* drafts and their website copies
- write-openspec-docs skill: add spoken-flow sentence rule

* docs: address review on PR #1649

- sync-docs: read the existing output directly instead of exists-then-read
  (CodeQL TOCTOU alert)
- hold back the headings-only Environment variables and Stores reference
  pages until written; links to them fall back to their GitHub source
- sources.md: cutover keeps docs/ in place and points at public/_redirects
- setup.md: label the workflow tree as the default set plus two optional ones

* docs: two review nits (spoken-flow rule, XDG_DATA_HOME note)
This commit is contained in:
Tabish Bidiwale
2026-08-21 20:45:19 +00:00
committed by GitHub
parent 1ebddd17f4
commit f1b521dffa
70 changed files with 6660 additions and 818 deletions
@@ -0,0 +1,46 @@
---
name: draft-openspec-docs
description: Collaborative page-drafting mode for the OpenSpec docs. Builds a scratch plan inside the target page (purpose, structure, numbered draft steps), iterates on it with the user, then drafts one section per approved step and cleans up after itself. Use when a page needs a from-scratch rewrite or a new page is being shaped with the user in the loop.
argument-hint: target page
---
# Draft OpenSpec docs (scratch-plan workflow)
You are shaping a docs page with the user in the loop. The page is planned and reviewed inside the page itself, then drafted one section at a time. Load `write-openspec-docs` (the style authority) and `no-ai-slop` before drafting anything.
## 1. Set up the scratch section
Strip the page to its title and `>` goal line, then add a working section below them:
```md
## Scratch: page plan (delete before publish)
### Purpose
### Structure
```
- **Purpose**: 3-5 dot points. Who the reader is and what they come to look up, what the page covers, what it links out to. Check `docs-lab/README.md` (the page's goal line) and `docs-lab/message-map.md` (the questions routed here) before writing it.
- **Structure**: a numbered list of the page's sections, one line each naming the section and the shape of its content (table, fence, tree, bullets).
- Say what the page will do, never what it won't. Plain words and short bullets; the user reads this in their editor.
## 2. Iterate until the plan is approved
- Plan edits are cheap; page edits aren't. Reshape the plan as many times as the user asks before drafting.
- Record every decision in the plan itself, not only in chat. Add a `### Notes` list for follow-ups that belong to other pages and product observations found along the way.
- The user may edit the file directly between turns; their edits are decisions, not drift to revert.
- Surface one open call at a time, with a recommendation.
## 3. Add the draft plan, then draft step by step
Once the structure holds, add a `### Draft plan` below the notes: one step per page section, each with an ID and a readable title (`**D1. Goal line and intro**`), ending with a consolidation step (cross-page updates) and a cleanup step. Then:
- Wait for the user to call a step ID. Draft exactly that step, into the page above the scratch block.
- Verify each fact against source before writing it; a cheap grep beats trust. Reference content shows the raw contract (templates, instructions, config) verbatim in fences, linked to the file on GitHub, rather than paraphrasing it.
- Keep sibling sections on a repeatable sub-structure so the page scans as one system.
- Mark the step `(done)` in the plan, report what landed, and name the next step.
## 4. Consolidation and cleanup
- **Consolidation**: update everything that points at the page. The README goal line (verbatim match with the page's `>` line), the message map, the sync config (`website/docs.sync.config.mjs`), and any cross-links found by grepping the tree. Run `node website/scripts/sync-docs.mjs` to validate.
- **Cleanup**: delete the scratch block, run the retrievability and glance tests from `write-openspec-docs` at desktop and narrow widths, and flip the page's message-map row to Answered if its prose landed.
@@ -0,0 +1,49 @@
---
name: verify-openspec-docs
description: Fact-checks OpenSpec user documentation with a fresh-context subagent that re-runs commands and checks claims against source. Manually triggered; not part of the drafting loop. Use when the user asks to verify, fact-check, or accuracy-check a docs page, section, or set of changed claims.
argument-hint: page or section
---
# Verify OpenSpec docs
Check finished docs prose against reality. The point of a fresh context is that the reviewer hasn't watched the prose get written, so it can't be talked into the author's assumptions.
This skill runs only when the user asks for it. Drafting is owned by `write-openspec-docs`; don't invoke this from inside a drafting session unless the user requests a verification pass.
## Scope the run
1. Confirm the target: a page, one `##` section, or a list of changed claims. If invoked without a target, ask.
2. Read the README at the root of the docs tree the target lives in; its invariants and page map are part of what gets checked.
3. One subagent per unit (one `##` section, or the stated claim list). A full page is several subagents, run in parallel.
## Spawn the reviewer
General-purpose subagent. Subagents don't inherit skills, so the prompt hands the reviewer everything by path. Fill every placeholder, make every path absolute, and send:
```
You are reviewing one unit of OpenSpec's user documentation before it reaches the docs owner. Be the two hardest readers it will meet: a skeptical developer reading it cold, and a fact-checker with the repo open.
Repo root: <ABSOLUTE REPO ROOT>. Use absolute paths with every tool.
Read first:
1. <DOCS TREE ROOT>/README.md: the page map and standing invariants.
2. <ABSOLUTE REPO ROOT>/.agents/skills/write-openspec-docs/writing.md: the house writing rules.
3. <PAGE PATH>: review only <the section "<HEADING>" | these changed claims: <LIST>>; read the rest of the page for context.
Then check, in this order:
1. Facts. Every command, flag, path, config key, output block, default, and behavior claim. Re-run the terminal commands shown: read-only commands anywhere, anything that mutates state in a scratch directory or not at all. Commands for the AI chat surface (like /opsx:propose) can't run in a shell; verify their names and behavior against the skill sources this repo ships. Check names against src/ and the CLI's own --help. An output block must match what the command actually prints.
2. Examples. Any example spec or change must pass `openspec validate`. Run it when the example exists on disk.
3. Structure. Flag anything that re-explains a topic whose canonical home is another page, or breaks a rule the docs tree's README states.
4. Job fit. Does the unit serve the page's stated job (the one-line statement under the title, if present)? Does the arriving reader get what they came for quickly?
5. Trust and slop. Flag: hype or comfort adjectives (easy, simple, powerful, seamless), claims with no shown evidence, vague generalization where a specific fact belongs, binary contrasts ("not X, it's Y"), colon reveals, importance puffery, summary endings, em dashes, bullet lists that should be prose, and three parallel punchy sentences in a row.
Report findings only, most severe first. For each: quote the line, say what is wrong, and give the fix in one line. For every fact you verified, say how (the command you ran, or the file and line you checked). List any claim you could not verify and why. Do not rewrite the unit. If the unit is clean, say so and list exactly what you verified.
```
## Handle the report
- Default is report, not rewrite: show the user the findings ranked most severe first, each with the quoted line and one-line fix, plus what was verified and how, and any claim the reviewer couldn't verify.
- Apply fixes only when the user asked for a verify-and-fix run or approves the findings. A verifier can also be wrong: rejections go in the report with your reason, so the user can overrule you.
- If an applied fix changed a factual claim, verify again, scoped to the changed claims. Typo and wording fixes don't need a second pass.
- Two passes without converging means stop and take it to the user. Don't polish in a loop.
@@ -0,0 +1,34 @@
---
name: write-openspec-docs
description: Switches into OpenSpec docs-writing mode; loads the house style guide and drafts or revises pages in its voice (action-first, no preamble, scannable). Use when writing or editing pages in the OpenSpec docs tree.
argument-hint: page or section
---
# Write OpenSpec docs
You are now writing OpenSpec's user docs. Read [writing.md](writing.md); it is the style authority for everything drafted here. The short version, in effect immediately:
- A page is a retrieval surface, not an essay. Structure decides whether the reader finds the answer; prose only decides how it reads. Open every section with the answer, never a running story.
- Choose the page type before the outline. Guides follow the reader's task; reference mirrors the product's structure and uses exact field, command, and file names as scan anchors. Reference needs complete coverage without compressing several facts into one sentence, cell, or paragraph.
- Draft the shortest version that answers; expanding a spare page is cheap, cutting a bloated one is a rewrite. Plain words, the fewest of them: an idea that fits in one line takes one line. Depth most readers skip goes behind a link, and the payload (commands, real output, failures and fixes) stays whole.
- Dumb sentences, smart structure. Write the obvious sentence (actor, verb, object, stating the literal event); never compress extra facts in or take an angle. No hype adjectives, no preamble, no em dashes.
- One job per slot: one fact per sentence, list intros only announce the list, one reader question or lookup target per section. A related fact gets its own slot, never a ride in someone else's.
- Ground items in what the reader can verify: path or folder first, concept as the gloss, real output shown honestly.
- No house template. Inventories open with a list naming every item, then expand each in its own unit after the list, never inline. Sequences take numbered steps (numbers mean order; inventories take bullets). Single ideas and reasoning stay in short prose.
- Every load-bearing fact sits on a scan anchor: code fence, numbered bold lead-in, `**Term**: fact` bullet, table, file tree. Never only mid-paragraph.
- Before finishing, run two backstop tests. Retrievability: can each question or exact product name be found by scanning alone? The glance: inspect the rendered page as shapes; does it look finishable, or like work? Check table-heavy changes at desktop and narrow widths. A failure means a slot got written without being earned; fix it now, don't leave it for review.
## Ground rules
- Load the `no-ai-slop` skill before drafting; it owns the generic slop patterns, while [writing.md](writing.md) owns what OpenSpec's docs specifically look and sound like.
- Read the target page in full before editing it.
- Real facts only: flags, paths, and output as they exist in source. If a claim can't be checked cheaply, still write it, but name it as unchecked when you show the work; never bridge a gap with a plausible-sounding sentence.
- A fact lives on one page; everywhere else links to it. The docs tree's README owns the page map and structural invariants; check it before restructuring or adding pages.
- For reference pages, inventory the contract from source before drafting prose. Follow the reference process in [writing.md](writing.md#reference-pages).
- When unsure how something should scan or sound, match the exemplars: `docs-lab/start/setup.md` for section shape and inventories, `docs-lab/start/installation.md` (Uninstalling) for multi-step tasks.
## When done
Show the user what changed and name any unchecked claims.
If the user asks for the deep, evidence-first drafting session (run every command, one section per sitting, formal checkpoints), follow [full-process.md](full-process.md).
@@ -0,0 +1,49 @@
# Full drafting process (opt-in)
The evidence-first, checkpointed way to draft a page. Use this only when the user asks for the deep process; the default mode is SKILL.md alone.
## Orient (every session, before any writing)
1. Read the README at the root of the docs tree you're writing in. Where it states invariants or a page map, it wins over this skill.
2. Read the target page top to bottom, plus anything its comments cite.
3. Read the sibling pages this page links to or overlaps with, enough to know what must stay a link rather than become an explanation.
4. Write down the page's job in one line: who arrives, trying to do what, and what they leave able to do. If the page carries a job statement (docs-lab pages use a `>` blockquote under the title), test against it. If the unit you're about to write doesn't serve that job, stop and raise it instead of drafting around it.
## Work in small units
- The default unit is one `##` section. A page is several sittings, not one.
- For revisions to existing prose, the unit is the requested change, however many headings it touches.
- Draft the next unit only after the user has reviewed the current one. When the user asks for fixes, fix only that; don't smuggle in the next unit.
## Evidence before prose
Before drafting a unit, know where its claims come from.
- Run the terminal commands the unit will show when they're cheap: read-only commands anywhere, anything that mutates state in a scratch directory or not at all. Paste real output; trim it, never retouch it.
- Check names against source: flags, paths, config keys, and defaults come from `src/` and the CLI's own help, not from older docs. When docs and source disagree, source wins; note the conflict for the user.
- Never bridge a gap with a plausible-sounding sentence. A claim you couldn't check gets flagged at the checkpoint, not silently shipped. Don't let one expensive check stall the draft.
## Strip the slop
Invoke the `no-ai-slop` skill on the drafted unit and apply its edit pass. Docs prose gets no exemption: the patterns it names read as machine-written to exactly the audience these docs must win over.
## Does it do the job?
Reread the unit cold, as the reader the job line names, arriving with their actual problem. Answer three questions:
1. Can they act? Every step is runnable as written, and nothing depends on knowledge the page hasn't given or linked.
2. Do they know it worked? Where success could be in doubt, the unit shows something visible: output, a file, what the agent does next. Where the outcome is obvious, no success line is owed.
3. What can they do now that they couldn't before? If the honest answer is "they read some context", the unit is explaining instead of solving; cut it back to what serves the job or raise it with the user.
A unit that fails here gets fixed before the checkpoint, not annotated.
## Checkpoint
End the unit by showing the user:
- the file path and the unit written;
- the page's job in one line, and what this unit lets the reader do toward it;
- which claims you checked and how (commands run, files read);
- any claim still unchecked.
Then stop. The next unit starts when the user says so.
@@ -0,0 +1,204 @@
# OpenSpec docs: the style guide
Structure, voice, tone, and language for OpenSpec's user docs. This file is the primary style authority for the docs tree. The tree's own README owns structure (the page map and which page teaches what); when this file and that README disagree, the README wins. `no-ai-slop` owns the generic slop patterns; this file owns what OpenSpec's docs specifically look and sound like.
## Six principles
Every rule below applies one of these; when rules collide, the principles decide.
- **A page is a retrieval surface, not an essay.** The reader arrives mid-task with a question, scans for the answer, and leaves. Structure decides whether they find it; prose only decides how it reads. Structure wins.
- **The shortest version that answers is the right length, drafted that way from the start.** Expanding a spare page is cheap; cutting a bloated one is a rewrite. Plain words, and the fewest of them: an idea that fits in one line takes one line.
- **Dumb sentences, smart structure.** Write the obvious sentence: actor, verb, object, stating the literal event ("Running init creates two things in your project"). Never the version that compresses facts in or takes an angle ("Everything init creates is meant to be committed"). If a sentence needs unpacking, it failed.
- **One job per slot.** A sentence carries one fact (one carrying three hides two). A list intro only announces its list. A section owns one reader question or lookup target. Related facts get their own slot, never a ride in someone else's.
- **Ground everything in what the reader can verify.** Name things by path, file, or real output: what the reader could match against `ls`. The concept is the gloss, never the name.
- **No house template.** Shape follows content: inventories get overview-then-expand, sequences get numbered steps, a single idea gets short paragraphs, and reasoning lives in prose. The universal check is the retrievability test, not bullet count.
## The retrievability test
Name the questions or exact product terms a reader would bring to the section ("does init touch `.gitignore`?", "how do I add a tool later?", `generates`). Each answer or term must be findable by scanning, heading to anchor to fact, without reading paragraphs. If finding a fact means reading sentences, restructure; prose that passes needs no bullets, and no amount of bullets saves a section that fails.
## The shortest draft
Brevity happens at drafting time, not review. A page that needs heavy cutting in review gets rewritten, and a rewrite costs more than writing it spare the first time. Start from the shortest version that answers and expand only where a real reader question goes unanswered.
Every slot is earned before it's written:
- **The unit**: it answers a reader question or documents a lookup target, or it doesn't go in. Tight prose on the wrong scope is still the wrong scope.
- **The sentence**: would any reader come back for it? If not, it spends attention without buying anything; don't write it.
- **The depth**: an edge case or rationale most readers skip goes behind a link to its canonical page, not inline. The docs keep the depth; this page doesn't charge every reader for it.
- **The payload**: the command, the real output, the failure and its fix stay whole. Spare means no wind-up and no commentary, never fewer facts.
The glance test is the backstop, not the method. Scroll the rendered page and read it as shapes: short units, air between anchors, no screen-filling block of anything. A page that looks like work loses its reader before the first sentence; if yours does, something above got in without earning its slot. For table or layout changes, check both desktop and narrow widths; the Markdown source can't show cramped columns, poor wrapping, or horizontal scrolling.
## Shape of a section
- **Answer first**: open with the command, the inventory, or the fact in one line. Context and rationale come after, never first.
- **Inventory, then expand**: when a section covers several things (what init installs, what an uninstall leaves behind), open with a list naming every item in one line each, then expand each in its own unit after the list (a subsection or bold lead-in).
- **The overview only names**: a count ("two things:") is not an inventory, and expansion never happens inline in the list; the reader sees the whole footprint, then the detail.
- **Place first, meaning second**: name each on-disk item by its path or folder ("an `openspec/` folder at the repo root"), never by concept alone ("the planning folder"). The concept gloss can wait for the item's expansion. When a location varies (per tool, per OS), anchor it with a real folder or two ("`.agents/`, `.claude/`") and link the full list.
- **Core before nuance**: inside every unit, the answer, then what to expect, then edge cases last. A reader who stops early still leaves with the core.
- **One unit per target**: task pages separate different reader questions. Reference pages separate different product elements when readers look them up independently. Two facts with different targets get separate units, even when one elegant sentence could join them.
## Scan anchors
Every load-bearing fact sits on an anchor: something the eye lands on without reading. A fact a reader might come back for never lives only in the middle of a paragraph. The anchors these docs use:
- Code fences, for commands and real output.
- Numbered steps with bold lead-ins (`**1. Remove the package.**`) for multi-step tasks.
- `**Term**: fact` bullets for options, properties, and locations.
- Tables, when several items share the same attributes (mostly reference pages).
- File trees with inline annotations for layouts.
A full screen of content with no anchor is a wall, even when every sentence in it is true.
## Choosing the shape
No house template: facts go on anchors (enumerable content defaults to a list or table); explanation, reasoning, and judgment go in prose. The content picks the form:
- **Sequences**: numbered steps, one bounded action each. Numbers mean order of execution; an inventory of things takes bullets, never numbers. Restate any value a step needs rather than pointing back three steps.
- **Options, properties, locations**: `**Term**: fact` bullets.
- **Items sharing the same attributes**: a table.
- **A single idea** (why a store is worth it, what sync treats as drift): a couple of short paragraphs; that is the right shape.
- **Connected reasoning**: a paragraph. Shredding a thought into fragments makes it harder to read, not easier; a bullet list of full explanatory sentences is a paragraph in costume, so write the paragraph.
Across every form:
- Cap lists at about five items. Longer than that, split by priority: common path first, edge cases into their own list or a linked page.
- Keep items parallel: same internal order (name, fact, catch, link), same grammatical shape. Repetition across items is what makes scanning work; never vary structure between items for the sake of the prose.
- Uniformity is right when the content is uniform (a reference table, an install matrix). When every section on a varied page resolves to the same pattern, some of those lists are disguised paragraphs.
## Prose budget
Paragraphs are glue between anchors, not containers for facts.
- One to three lines. Three is the ceiling; a glue line between two anchors is often enough.
- The list intro has exactly one job: say what the list is ("Running init creates two things in your project:"). Never spend that slot on a different fact, however related; it gets its own line after the list.
- Parentheses and semicolon riders are for true asides only (a version caveat, a pointer). If a reader might return for the fact, it gets its own anchor.
- A section that is mostly paragraphs is misshaped, unless the page is genuinely conceptual (Concepts, explanations of the model). Even there, front-load each paragraph and leave air between them.
## Sentences
- Short sentences, active voice. Default subject is "you" or the tool by name.
- No preamble. The first sentence of any unit states the thing itself, never wind-up ("Before we get into...", "It's worth understanding that...").
- No em dashes anywhere in these docs. Use a colon, a comma, parentheses, or two sentences.
- Write the sentence you would say out loud. A draft that splices clauses with semicolons or colon-stacked fragments ("different documents: fewer of them, different names, different structure") gets rewritten as the spoken version ("when you want these to be different documents, whether that means fewer of them, different names, or a different structure"). Colons still introduce lists, fences, and labels. They don't splice prose.
- Contractions are fine ("you're set", "doesn't come along"). These docs talk, they don't proclaim.
- Inside narrative paragraphs, vary sentence length so the prose doesn't read staccato. Paragraphs only; list items stay parallel even when the cadence repeats.
## Voice
The narrator is a colleague who has run every command on the page, hit the failure modes personally, and is telling you what they know. Not a marketer, not a tutorial host, not a manual.
- Calm and specific. The reader wants the fact, the command, and the catch, in that order.
- Confidence comes from precision, not emphasis. Never "very", "extremely", "critical", bold-for-importance, or exclamation marks.
- Plain judgment is welcome. The docs may tell the reader what to do and what to skip: "The `openspec/` folder: pause first."
- Address the reader as "you". OpenSpec, the CLI, and init do things. "We" appears only for project decisions ("we say skills"), never as a tour guide.
- Dry beats chirpy. No cheerleading, no apologizing, no drama around failures. A failure is a fact with a fix.
## Structure and tone by page type
Same voice everywhere; structure and temperature shift:
- **Start pages**: numbered steps and short units, nothing assumed, every step ends in something visible. Warmest the docs get, which is still plain.
- **Guides**: peer to peer, skip re-orientation. The judgment calls are the reason guides exist; put them on anchors so they scan.
- **Reference**: mirror the product, use its exact names as anchors, and cover the contract without compressing it. Tables and fragments are common, but scan speed decides the shape. No motivation or persuasion; the reader is here to look something up and leave.
- **Troubleshooting**: symptom, cause, fix, in that order, one unit per symptom. Name the error the reader sees. Never "you may notice" or "sometimes it can happen that".
Guides and Customize pages share one skeleton: a one-line what, a link to the Quickstart (never a recap), the 80% path, then Advanced. The exception is the concepts page, which is an explanation, not a task guide; its shape follows its concerns.
## Reference pages
Reference describes the product for a reader who is already working with it. The outline follows the machinery: file, block, field; command group, command, option; object, property, value. Use the product's exact names as headings when readers will search for those names. Reader-question headings belong to task pages unless the question itself is the established lookup term.
Cover the full contract without packing several facts into one sentence, cell, or paragraph.
### Draft the contract first
Before writing prose:
1. Inventory the product elements from source.
2. Arrange them in the same hierarchy as the product.
3. Write the smallest complete contract for each element.
4. Expand only the elements whose behavior needs more room.
5. Add examples that illustrate one rule at a time.
6. Audit defaults, constraints, failures, ignored input, and validation gaps.
For each field, option, command, or file, record the identity facts that apply:
- Exact name and syntax
- Type or accepted values
- Required state and default
- Scope, location, or base path
Then record the behavior facts that apply:
- Behavior and side effects
- Constraints
- Failure and ignored-input behavior
- What validation catches and misses
Don't create empty sections or table columns for facts that don't apply.
### Inventory, then expand
Open with a complete table or list. Expand an item below the inventory only when its behavior can't fit cleanly in the overview. Put the expansion under the item's exact name so the table of contents works as an index.
For field and option references, `Field | Contract` is the safe table shape when definitions need sentences. Add more columns only when every cell is short and comparable. If columns split one coherent definition into fragments or wrap badly at a narrow width, use fewer columns or move the detail below the inventory.
### Examples and edge cases
An example illustrates one mapping, rule, or result. It doesn't become a sequence the reader follows or a narrative about completing a task. A complete example may follow the contract when seeing the elements together helps lookup.
Put the concrete default path or value in the primary slot. Put environment variables and uncommon overrides afterward.
State the observable consequence of a limit. If OpenSpec ignores a misspelled field, say that validation passes and the field has no effect. If a value falls back, name the value OpenSpec uses.
## Two surfaces
OpenSpec spans the terminal and the AI chat, and readers mix them up. Label every snippet:
```
In your terminal:
openspec init
In your AI chat:
/opsx:propose add-rate-limit
```
Where the reader could doubt it worked (a fresh install, a first run, a command with no output of its own), end with the concrete success signal: the line the command prints, the file that now exists, what the agent says next. Where the outcome is obvious, stop; an unneeded success line is noise.
## Authoring mechanics
Pages are plain markdown; GitHub and the site both render them. JSX components (`<Callout>`, `<Tabs>`, `<details>`) don't render; never use them.
- **Install commands**: write the global npm command once, in a fence whose language is `npm`; the site renders it as npm/pnpm/yarn/bun tabs with a copy button per tab (`remarkNpm` in `website/source.config.ts`, which also persists the reader's choice across blocks). On GitHub the fence degrades to the plain npm command.
- **Callouts**: GitHub-style blockquote alerts (`> [!NOTE]`, `> [!TIP]`, `> [!IMPORTANT]`, `> [!WARNING]`, `> [!CAUTION]`); GitHub styles them natively and the site renders them as callouts (`remarkGfmAlert` in `website/lib/remark-gfm-alert.ts`). Never place one directly under the page title: the sync lifts the leading blockquote into the page description.
## What earns a developer's trust
- Show the real command and its real output, trimmed honestly. A retouched output is a lie the reader catches on their first run.
- No hype and no comfort adjectives: easy, simple, just, powerful, seamless, robust. Three lines that show the thing beat any adjective about it.
- State limits plainly. A named limitation builds more trust than praise: "Your assistant does need to be able to run shell commands; a few IDE integrations can't."
- Don't generalize. Where you're tempted to write what OpenSpec "helps" with, write what actually happens: which file appears, what the diff shows, what the agent does next.
- Don't define what you can show. An unfamiliar term whose instances explain themselves (the workflow list: propose, explore, apply...) is introduced by showing the instances with one-phrase glosses; the abstraction can wait.
- Exact names: flags, paths, config keys, and versions as they exist in source, linked to their canonical page on first use.
## Naming and terms
- One term per concept, the glossary's term if the tree has one; today that means "skills", never "slash commands".
- No invented taxonomy. Product terms (spec, change, delta, profile, store) name real things; use them freely. Any other organizing word in a heading or goal ("layers", "levers", "pillars") must pass one test: would a reader use it to ask their own question? If not, write the reader's question or the plain enumeration ("What you can customize", never "The three layers").
- Examples invoke workflows by skill: the ask that triggers it ("ask your agent to propose a change") or the skill's name (`openspec-propose`), which is the same in every tool. A command spelling (`/opsx:propose`) appears only as a labeled per-tool example, never as the generic instruction; commands are headed for deprecation and their spellings vary per tool.
- Prefer the shared `.agents/` folder in file-path examples; a tool-specific folder (`.claude/`) appears only when the example is about that tool.
- Headings lead with a verb when the section is something the reader does ("Initialize your project"). Found content takes a plain noun phrase ("Install methods"). Never a vague verb ("Understand it") and never a pun.
- If the page carries a one-line job statement under the title (docs-lab uses a `>` blockquote the site lifts into the page description), keep it plain, concrete, and true of the finished page.
## One canonical home
A fact lives on exactly one page; everywhere else links to it. A second copy is a future contradiction. The tree's README says which page owns what; when in doubt, link.
## Exemplars
When unsure how something should scan or sound, match these:
- `docs-lab/start/setup.md`: section shape, inventory-then-expand, enumerable facts on bullets.
- `docs-lab/start/installation.md`, the Uninstalling section: multi-step tasks with bold numbered lead-ins.
+98
View File
@@ -0,0 +1,98 @@
Ok these are my notes when reviewing the different sections/file in docs-lab.
I've written down my thoughts when looking at these sections so we can think about how to update these
docs with the feedback in mind.
## Start > Overview
Ok the following subtitle here is horrible:
> OpenSpec gives you and your coding agent a shared, reviewable plan before code is written.
This is not a strong value prop in the day and age of plan mode, but other than that i don't think it sells openspec hard enough
OpenSpec is not really about a shared plan for a single session it's about a keeping things on track and aligned for larger features.
what we focus on:
- making it work for teams
- git native / things checked into vcs
- intented behaviour matches the implemented behaviour
- we help you capture intendede behaviour and match it to the implemented behaviour.
- it's about correctness, coherence,
Control theory inspiration:
Instructors break down how a system measures its current state, compares it to a desired goal, and adjusts its actions to reduce the difference.
## Guides > Explore and idea
I think in this section we should mention:
This is about exploring the problem space, figuring out what problems you care about and diving deeper into them.
It was designed with a very different philiosphy in mind of giving you the freedom to explore the problem space and jump around to different ideas and sections.
It serves a similar purpose to other newer entrees in the fields like superpowers or matt pocock's skills.
We often do see people combining explore with
Often this is a matter of UX and personal preference. There is no single best skill or method to getting to
aligment with your agent.
Some people prefer a conversing with a thoughtful design partner, others might prefer being asked questions till they have a good understanding of a problem.
Feel free to customize the explore skills to your needs.
Unrelated to docs:
How do we solve the problem for PM's?
How do we give them a good home? - what is their job to be done?
How we efficiently help them achieve that?
- they're basically turning it into tickets?
What do we want to get across the line this week?
- The spec drift agent?
- The dashboard?
- figure out how we use agent session and traces better
## From docs-lab drafting (2026-08-19, project-config page)
Product issue, not docs: the installed skills in this repo are stale against the current
templates. `.claude/skills/openspec-archive-change/SKILL.md` has no `openspec instructions`
call at all, while `src/core/templates/workflows/archive-change.ts:40` instructs one; the
installed apply skill also doesn't mention the `context`/`operationGuidance` fields in the
JSON it reads. So config injection reaches the CLI output, but a stale skill never tells
the agent to consume it. Running `openspec update` should refresh them.
## From docs-lab drafting (2026-08-19, schemas page)
Product issues found while verifying the schema system (all file refs current as of today):
- `schema init --default` writes a `defaultSchema:` key to openspec/config.yaml that nothing
reads (schema.ts:961-978; readProjectConfig parses only schema/context/rules/operations/
references/store). The flag should write `schema:` or be removed. The docs now say to set
`schema:` by hand.
- `schema init` next-steps output prints a command that doesn't exist in that form:
"Use with: openspec new --schema <name>" (schema.ts:999); real syntax is
`openspec new change <name> --schema <name>`.
- `openspec new change` spinner prints the hardcoded default schema, not the resolved one
(new-change.ts:118): "Creating change 'x' with schema 'spec-driven'..." then "Schema: lite".
- `schema fork` re-serializes schema.yaml (literal `instruction: |` becomes folded `>`,
comments dropped), so diffing a fork against upstream is noisy (schema.ts:706-712).
- All `openspec schema` subcommands plus `openspec schemas`/`templates` use process.cwd()
and take no --store; they silently see nothing when run from a subdirectory, unlike
root-resolved commands (schema.ts:383/485/634/768).
- `suggestSchemas` fuzzy "did you mean" helper exists but is wired to nothing
(project-config.ts:420).
Docs follow-up: the community schema catalog lives only in legacy docs/customization.md
(#community-schemas); customize/schemas.md links to it on GitHub. When the old docs tree
retires, the catalog needs a docs-lab home.
+294
View File
@@ -0,0 +1,294 @@
# docs-lab: parallel rebuild of the OpenSpec docs
**Status: prose is landing page by page; the rest are skeletons** (real headings plus a
one-line `>` job statement the site lifts into the page description). The live site
builds from this tree: `website/docs.sync.config.mjs` maps these files to published
pages, and the old `docs/` tree is no longer used by the site.
This README owns the structure: which pages exist and which page teaches what. The
reverse view, from a job or message to the page that owns it, is
[message-map.md](message-map.md). How to
write them (style, voice, formatting) is the `write-openspec-docs` skill's
[writing.md](../.agents/skills/write-openspec-docs/writing.md).
## The bar for every page
Every page in docs-lab is written by hand, from scratch. The old `docs/` tree is source
material for facts, never text to carry over.
What we're after is that the reader gets the idea: every page reads well and makes sense
to anyone, whatever their level of skill, and above all it is simple. The worst thing we
can ship is documentation that is cognitively expensive to understand, and that cost
comes from complicated words, metaphors that don't make sense, random terminology that
isn't explained, and formatting that gets in the way of reading. Every sentence has a
purpose and is easy to read and comprehend. If a sentence doesn't pass that test, rewrite
it or cut it.
## Structure rules
**Folders are the areas.** Every page lives in its area's folder (`start/`,
`guides/`, `customize/`, `multi-repo/`, `reference/`, `help/`); the root holds only this
README, `message-map.md`, and `sources.md`. Most folders publish as one
sidebar group; `guides/` publishes as the Guides group, holding three collapsible
subgroups (Understanding OpenSpec, Using OpenSpec, Adopting OpenSpec), all expanded
by default (held back from the site until the pages are drafted: the whole section is
commented out in `website/docs.sync.config.mjs`, and links to a guide fall back to its
source on GitHub until it's re-listed). Reference holds three nested
folders (`reference/architecture/`, `reference/schemas/`, and
`reference/configuration/`), each publishing as a collapsible group with `index.md` as
its landing page; the spec-driven schema publishes as a single page
(`reference/schemas/spec-driven/index.md`) inside the Schemas group. Labels and URLs come from
`website/docs.sync.config.mjs`, so moving a file never moves a URL.
**Teach once.** The loop (propose, review, apply, archive) has one teacher; every other
page links, never re-teaches:
- `start/quickstart.md` teaches it as UX: how a human moves a change through the
lifecycle, including what archive does on disk.
- `start/overview.md` shows it as pitch: copy only, no explanation.
- `guides/concepts.md` stays out of it: the page explains the artifacts (specs, changes,
the delta) and links to the quickstart for the loop. Disk paths appear inline with the
concept that owns them, never as a layout section.
- `start/installation.md` owns install; `start/setup.md` owns init and what it writes.
The quickstart opens with one prerequisite line linking both and starts at explore.
**Guides vs reference.** `reference/skills.md` holds each skill's contract: arguments,
what it creates, and what it responds with. Guide pages
(the Using and Adopting subgroups) own the human judgment for a task, including when
to reach for each skill, may span several skills, and never restate skill mechanics.
`reference/architecture/` is the one exception to Reference's look-it-up bar: it's
explanation content, housed here as a pragmatic home while it's three pages. If it
grows (say, by absorbing contributor internals), consider giving it its own folder
and tab.
**Reference is lookup, and named for it.** `reference/schemas/` and
`reference/configuration/` are contracts: keys, values, types, defaults, and
locations, on tables and fences. Anything explanatory (what a schema is, what
to put in config.yaml) lives in Customize or Guides and is linked, never
restated. Naming follows three rules. A reference folder's landing
page is titled "Overview"; the folder label already names the group, and
repeating it double-nests the sidebar. A page documenting one file carries
concept and filename in the title, concept first, where the concept names the
file's use, never just its scope ("Project configuration (config.yaml)", "CLI
settings (config.json)"): the left edge is what the eye disambiguates in the
sidebar, and the filename keeps the title matching what readers search for and
see on disk. A file whose name is the term readers use keeps the filename
alone as the title (`schema.yaml`), and a page owning one product term takes
that term as the title (`spec-driven`), and
a page covering several files takes the concept alone (Stores), naming its
files in the job line.
**FAQ is one-liners.** Every FAQ entry is a short answer, a few lines at most, or a
router link to the page that owns the topic. How-to content never lives in the FAQ:
when an answer outgrows a one-liner, it moves to a guide or reference page and the FAQ
entry becomes a pointer.
## Page index: every page's job
Each goal below is the page's `>` blockquote verbatim, so the promise here is the promise
readers see. A page delivers exactly its goal: content that outgrows it means splitting
the page or rewriting the goal in both places, never letting them drift.
### Start: from "what is this?" to your first archived change
| Page | Goal |
|---|---|
| [Overview](start/overview.md) | _TODO: emptied 2026-08-21 for a from-scratch rewrite and pulled from the site (`/docs` redirects to Installation meanwhile); the old goal line was dropped as too weak a pitch. Brief in Notes.md._ |
| [Installation](start/installation.md) | Install the `openspec` CLI on your machine, update it, and uninstall it. |
| [Set up your project](start/setup.md) | Add OpenSpec to a project: run init, see what it wrote, and adjust it. |
| [Quickstart](start/quickstart.md) | Your first change on your existing repo, from idea to archived. |
### Guides: understand the system, use it well, bring it to your codebase and team
| Page | Goal |
|---|---|
| [Understanding › Concepts](guides/concepts.md) | What the two artifacts are, and how a change describes a diff against current specs. |
| [Using › Explore an idea](guides/explore.md) | Think it through with the agent before you commit to a proposal. |
| [Using › Review the plan](guides/review-the-plan.md) | The two-minute pass that catches wrong turns before they're code. |
| [Using › Apply a change](guides/apply.md) | Run the plan: pacing, context windows, and picking up where you left off. |
| [Using › Change course](guides/change-course.md) | Revise a change in flight, or decide it's cleaner to start fresh. |
| [Adopting › Existing codebases](guides/existing-codebases.md) | Bring OpenSpec to a codebase with a lot of code and no specs: where to start, what to backfill, and how specs grow from there. |
| [Adopting › Teams](guides/teams.md) | Run OpenSpec as a team: what to commit, how a change rides its PR, and when to archive. |
### Customize: make the workflows fit your project
| Page | Goal |
|---|---|
| [Overview](customize/overview.md) | Your options for customizing OpenSpec. |
| [Profiles](customize/profiles.md) | Choose which workflows are installed, and whether they install as skills, commands, or both. |
| [Project configuration](customize/project-config.md) | Make the workflows plan changes the way you want with a few lines in config.yaml. |
| [Schemas](customize/schemas.md) | Change what OpenSpec produces: the artifacts, their order, and their templates. |
### Multi-repo (beta): plan across repository boundaries
| Page | Goal |
|---|---|
| [Stores (beta)](multi-repo/stores.md) | Plan changes that span repositories: one store, many repos. |
| [Worksets (beta)](multi-repo/worksets.md) | Open the store and the repos that use it in one editor window, so your agent sees both. |
### Reference: look it up, exact and complete
| Page | Goal |
|---|---|
| [Skills](reference/skills.md) | Every OpenSpec skill: arguments, what it creates, and what it responds with. |
| [CLI](reference/cli.md) | The `openspec` terminal commands. |
| [Schemas](reference/schemas/index.md) | Every available workflow schema and the artifacts it defines. |
| [Schemas › schema.yaml](reference/schemas/schema-yaml.md) | Every field of a schema definition, for reading or writing one. |
| [Schemas › spec-driven](reference/schemas/spec-driven/index.md) | The default workflow's artifacts: their order, their formats, and the change folder they produce. |
| [Configuration](reference/configuration/index.md) | Every file and setting that changes how OpenSpec behaves, and where each lives. |
| [Configuration › Project configuration (config.yaml)](reference/configuration/config-yaml.md) | Every field of openspec/config.yaml: the schema, context, and rules this project plans with. |
| [Configuration › Change metadata (.openspec.yaml)](reference/configuration/change-metadata.md) | The supported fields and validation rules for the metadata stored with each change. |
| [Configuration › CLI settings (config.json)](reference/configuration/config-json.md) | Every field of config.json: how the openspec CLI behaves on your machine. |
| [Configuration › Environment variables](reference/configuration/environment-variables.md) | Every environment variable OpenSpec reads. |
| [Configuration › Stores](reference/configuration/stores.md) | The files behind multi-repo stores: registry.yaml and store.yaml, and which root a command uses. |
| [Supported tools](reference/supported-tools.md) | Which AI coding tools OpenSpec supports, and each one's command syntax. |
| [Glossary](reference/glossary.md) | Every OpenSpec term, one line each. |
| [Architecture](reference/architecture/index.md) (held back from the site until drafted) | How OPSX is built: internals for the curious. |
| [Architecture › Workflow runs](reference/architecture/workflow-runs.md) | How a workflow run executes, from invocation to written artifacts. |
| [Architecture › Design decisions](reference/architecture/design-decisions.md) | Why OPSX works the way it does. |
### Help: get unstuck (held back from the site until drafted, see Open TODOs)
| Page | Goal |
|---|---|
| [FAQ](help/faq.md) | Short answers to the questions that don't need a page. |
| [Troubleshooting](help/troubleshooting.md) | When OpenSpec doesn't do what you expected: symptoms and their fixes. |
### Legacy: land the old workflow safely (held back from the site until drafted, see Open TODOs)
| Page | Goal |
|---|---|
| [Migrating from the legacy workflow](help/legacy/migration.md) | Moving from the legacy `/openspec:*` commands to OPSX. |
## Old docs
The `docs/` tree is legacy, and the plan is to remove it once docs-lab covers what it
owns. It has become a bit of an AI slop mess, so nothing from it is carried over as text
(see The bar for every page). Until it's removed it stays untouched: fixes land in
docs-lab, never in `docs/`.
[`sources.md`](sources.md) maps every current `docs/` page to its destination here: the
source material while drafting, the redirect list at cutover. Cutover steps are in that
file's [Cutover](sources.md#cutover) section.
## Open TODOs
- Not started: the Architecture pages (`reference/architecture/index.md`,
`workflow-runs.md`, `design-decisions.md`). All three are headings only, so we hid the
group from the site on 2026-08-21 (folder entry commented out in
`website/docs.sync.config.mjs`). The files stay on disk with a WIP comment. Published
pages that link to them (`reference/glossary.md` to the Overview,
`customize/project-config.md` to Workflow runs) fall back to the GitHub source until
the group is re-listed.
- Not started: the Help and Legacy pages (`help/faq.md`, `help/troubleshooting.md`,
`help/legacy/migration.md`). FAQ has one answer and the other two are headings only, so
we hid both sections from the site on 2026-08-21 (commented out in
`website/docs.sync.config.mjs`, same mechanism as Guides). The files stay on disk with
a WIP comment. Published pages that link to them (`start/setup.md` to FAQ,
`reference/glossary.md` to Migration) fall back to the GitHub source until the
sections are re-listed.
- Not started: `start/overview.md` is empty on purpose. We cleared the skeleton
(headings, narrative beats, diagram gallery) on 2026-08-21 to rewrite the landing page
from scratch. The old pitch ("a shared, reviewable plan before code is written")
undersells OpenSpec now that plan mode is everywhere; the rewrite should sell keeping
larger features on track and aligned (teams, git-native, intended vs implemented
behavior, control-loop framing). Brief in `Notes.md` ("Start > Overview"); the diagram
candidates went with the gallery and live in git history. Until the rewrite lands the
page is off the site: its entry is commented out in `website/docs.sync.config.mjs` and
`/docs` redirects to Installation (`website/public/_redirects` plus a fallback in the
docs page route). Restoring it is one uncomment plus removing the two redirects. The
Teach-once rule still applies: the loop appears here as pitch only.
- Product feedback, not a docs task: spec-driven's design `instruction` lists six
sections (including Migration Plan and Open Questions) but
`schemas/spec-driven/templates/design.md` carries only four headers. The docs show
both verbatim; the mismatch belongs upstream. Noted 2026-08-14 while consolidating
the spec-driven page.
- Product feedback, not a docs task: `openspec store setup --remote` writes the URL
into `store.yaml` but never configures a git `origin`, so "setup --remote, then
`git push -u origin main`" fails as written; the Stores page shows `git remote add`
instead. The pasteable missing-store fix in `openspec doctor` is powered by
`references:` remotes, not `store.yaml`. Noted 2026-08-21 while porting the Stores
page.
- Style guide follow-up (`.agents/skills/write-openspec-docs/writing.md`), from the
Stores page's review rounds, 2026-08-21: never use a term the page hasn't shown
(say "the `store:` line", not "the pointer"; define by showing the artifact first);
when behavior depends on the reader's starting state, enumerate the states and walk
each to its outcome; sentence subjects are you, OpenSpec, or your agent, never an
implementation unit ("the resolver picks") or a class of things ("store-only
projects make..."); when a defined term is reused a section later, re-gloss it in
one parenthetical at the point of use.
- Fence convention follow-up, 2026-08-21: the Stores page puts commands in `bash`
fences with a one-line `#` comment and OpenSpec output in a separate `yaml` fence.
`customize/schemas.md` still uses `console` fences with `$` prompts (lines 78, 114,
137, 145; prompts at 24 and 115); the style guide should name the convention and
that page should adopt it.
- Monorepo: message-map row 37 is still a Gap. "Packages treated as separate repos"
may land on the Stores page later; not part of the current page.
- `reference/cli.md` is fully drafted: the command table plus one section per real
command, facts captured from working-tree runs (2026-08-11). The `delivery` key that
start/setup.md's "Skills, commands, or both" section sets appears there only as
command output; its field-level home, `reference/configuration/config-json.md`, is
drafted (2026-08-14).
- Telemetry is undocumented. `OPENSPEC_TELEMETRY=0` appears nowhere in the tree; the
Deno install command grants `--allow-net=edge.openspec.dev` with no explanation (the
telemetry gloss was deliberately pulled pending a real home). The home now exists:
write `reference/configuration/environment-variables.md` (the env var, what's
collected, the opt-out, the CI auto-disable), then have the Deno section link to it
to explain the flag. Noted 2026-08-07; home settled 2026-08-10.
- Product feedback, not a docs task: init doesn't say when the global profile changed what
it wrote. A machine with `profile: custom` silently installs a different workflow set
than a stock machine, and nothing in the init output names the profile that shaped it.
Noted 2026-08-05 while verifying installation.md; track upstream, don't paper over in prose.
- Product feedback, not a docs task: drop the sync-specs skill from the default set; its
job reads as reference content, not a workflow, and it pads the skill list every reader
scans. Noted 2026-08-08 while writing start/setup.md's workflow tree.
- Product feedback, not a docs task: make the shared `.agents/` folder the default install
target for every tool, with tool-specific folders (`.claude/`, ...) the exception. The
docs already prefer `.agents/` in examples; the product should match. Noted 2026-08-08.
- `help/troubleshooting.md`'s skeleton has no section for install-time failures
(`command not found`, wrong Node version, PATH). Old `docs/troubleshooting.md` covered
them; `start/installation.md` carries caveats inline but there is no symptom-to-fix
home. Add a section or an installation.md anchor. Noted 2026-08-10 during the old-docs
message audit.
- Missing guide: the iterative flow. new/continue/fast-forward have no owner for the
judgment: what the flow is, when to pick it over propose, and ff vs continue. Old
`docs/workflows.md` covered it (Two Modes, When to Use What); `sources.md` routes that
page's mechanics to `guides/apply.md` and contracts to `reference/skills.md`, so the
choice itself landed nowhere. Likely a Guides › Using page slotted between Explore and
Review the plan, with a pointer to `customize/profiles.md` (the skills are optional
workflows outside the core set). Salvage only the ff-vs-continue rule of thumb; the rest of
workflows.md is unverified. Noted 2026-08-11. Related: message-map row 29 words
apply.md's pacing question as drafting-time pacing, the same creation-stage choice;
fix that row's wording or owner when this guide lands. Noted 2026-08-14.
- Missing guide: working with git. OpenSpec never touches git, so every git decision
lands on the reader with no page to answer it: do you branch before or after propose,
does a task get its own commit, what goes in the PR, where does the archive commit
land. `guides/teams.md` owns the archive-vs-PR ordering; the rest is unowned. Likely a
`guides/` file in the Adoption group. Noted 2026-08-08.
- `customize/skills.md` is parked: the skeleton stays on disk but is out of the page
index, the sidebar, and the sync config. Editing installed skill prompts has no good
answer yet (`openspec update` overwrites edits); the message map keeps the question as
a Gap. Revive when the product has a real story for surviving updates. Parked 2026-08-14.
- `guides/examples.md` is parked: the skeleton stays on disk but is out of the page
index, the sidebar, and the sync config. Contrived examples teach the wrong lesson for
this product; revive the page when real archived changes from actual usage can fill it.
The content plan (weak-vs-reviewed pairs, archived-changes gallery) is in the file's
comment. Parked 2026-08-11.
- Product feedback, not a docs task: "expanded" survives in product strings and the
update workflow is unlabeled in the picker. The only stored profile values are core
and custom, but `src/core/templates/workflows/update-change.ts` says "expanded-profile
workflow", and `WORKFLOW_PROMPT_META` (`src/commands/config.ts`) has no `update` entry,
so the `openspec config` workflow picker renders a core workflow as raw `update` /
"Workflow: update". Docs standardized on core/custom with "expand the set" as a verb
(2026-08-12). Noted 2026-08-12 during the glossary product sweep.
- Product feedback, not a docs task: converge on skills only, soon. A workflow's skill and
command are the same instructions, Claude Code has already merged commands into skills
upstream, and setup spends a whole subsection explaining why two forms exist. Every page
gets simpler when commands go. Noted 2026-08-08 while writing start/setup.md.
- Website QOL backlog, site build not prose: i18n; AI search layered on the stock keyword
search (an ask-the-docs answer box, not just matching); proper light/dark themes that
carry the Survey palette (DESIGN.md tokens) into both modes instead of a stock dark
theme. Candidates to bundle in the same pass: `llms.txt` plus a per-page "copy as
Markdown" button so agents can ingest pages, copy buttons on code blocks, and
"edit this page on GitHub" links. Noted 2026-08-11.
+32
View File
@@ -0,0 +1,32 @@
# Overview
> Your options for customizing OpenSpec.
OpenSpec supports multiple customization options. This page shows what each one changes and when to use it.
## What you can customize
| Option | What it changes | Use it when |
|---|---|---|
| [Profiles](profiles.md) | Which workflows are installed, and whether as skills, commands, or both | You want additional workflows and working patterns, or to remove workflows you don't need |
| [Project configuration](project-config.md) | The instructions injected into every workflow run: context, rules, and operation guidance (`config.yaml`) | You want changes planned your way, like tasks always including Playwright tests |
| [Schemas](schemas.md) | What OpenSpec produces: the artifacts, their order, and their templates | Changes should produce different planning files, sections, or formats |
## Not sure which to use?
Config and schemas are two levels of customization. Pick by how hands-on you want to get:
- **Start with [project configuration](project-config.md)**: it's lighter, and for most projects it's enough. You keep the standard artifacts and add your own context and rules on top.
- **Fork a [schema](schemas.md) when adding isn't enough**: config only adds on top of the core workflow. It can add a rule like "tasks always include tests," but it can't drop the design doc or rename a file. That's schema territory. Forking gives you your own copy to edit.
*"Fork" here means the `openspec schema fork` command, not forking a git repo. [Schemas](schemas.md) has the details.*
```mermaid
flowchart LR
a["The workflows should know my stack and conventions"] --> config
b["One artifact needs an extra rule, like tasks always including tests"] --> config
c["Different artifacts, file names, or document structure"] --> schema
d["The built-in instructions say things my team does differently"] --> schema
config["Project configuration<br/>(config.yaml)"]
schema["Fork a schema<br/>(openspec schema fork)"]
```
+88
View File
@@ -0,0 +1,88 @@
# Profiles
> Choose which workflows are installed, and whether they install as skills, commands, or both.
A profile is your preference for which OpenSpec workflows (the [skills and commands](../start/setup.md#the-workflow-files-skills-and-commands) in your AI tool) are installed across your machine. The default profile is `core`. Include or exclude workflows and your selection is saved as the `custom` profile.
## The core set
The `core` profile installs six workflows, covering the whole loop from idea to archive:
| Workflow | What it's for |
|---|---|
| [`explore`](../reference/skills.md#openspec-explore) | Think through an idea before it becomes a change proposal |
| [`propose`](../reference/skills.md#openspec-propose) | Create a change proposal and generate all its planning artifacts in one step |
| [`apply`](../reference/skills.md#openspec-apply-change) | Implement a change proposal's tasks |
| [`update`](../reference/skills.md#openspec-update-change) | Revise a change proposal's existing planning artifacts |
| [`sync`](../reference/skills.md#openspec-sync-specs) | Merge a change proposal's spec updates into `specs/` without archiving it |
| [`archive`](../reference/skills.md#openspec-archive-change) | Move a finished change proposal to the archive |
Each links to its full contract: arguments, what it creates, and what it responds with.
## Expanding the set: optional workflows
Six more workflows are available beyond the core set. Three of them (`new`, `continue`, `ff`) create a change proposal artifact by artifact, instead of all at once like `propose`.
| Workflow | What it's for |
|---|---|
| [`new`](../reference/skills.md#openspec-new-change) | Start a change proposal as an empty scaffold |
| [`continue`](../reference/skills.md#openspec-continue-change) | Create the next planning artifact in a change proposal, one at a time |
| [`ff`](../reference/skills.md#openspec-ff-change) | Create a change proposal and every planning artifact implementation needs, in one pass |
| [`verify`](../reference/skills.md#openspec-verify-change) | Check that the implementation matches the change proposal's artifacts |
| [`bulk-archive`](../reference/skills.md#openspec-bulk-archive-change) | Archive several change proposals at once |
| [`onboard`](../reference/skills.md#openspec-onboard) | Learn the workflow by doing one real change proposal end to end |
To change the set, run the interactive picker:
```bash
openspec config profile
```
The picker asks what to configure ([delivery](#delivery-skills-commands-or-both), workflows, or both), then lists all twelve workflows as checkboxes, with the installed ones checked. Any selection that isn't exactly the core six is saved as the `custom` profile, so you can also uncheck core workflows you don't use.
## Delivery: skills, commands, or both
Delivery is a profile setting that lets you choose to have only skills or only commands installed. The default is `both`. [Set up your project](../start/setup.md#the-workflow-files-skills-and-commands) explains the two forms and why both exist. The field's exact contract is in [CLI settings (config.json)](../reference/configuration/config-json.md#delivery).
Two ways to change it:
**Interactively**: run `openspec config profile` and choose "Delivery only". Here's switching to skills only:
```
Current profile settings
Delivery: both
? What do you want to configure? Delivery only
? Delivery mode (how workflows are installed): Skills only
Config changes:
delivery: both -> skills
? Apply changes to this project now? (Y/n) y
```
**Directly**: one command, no prompts:
```bash
openspec config set delivery skills # or: both, commands
```
Delivery never changes the profile name. `core` and `custom` describe the workflow set only, and switching back to `core` keeps your delivery setting.
## Switching profiles
Switching is two steps: change the profile on your machine, then update each project to apply it.
1. Change the profile:
```bash
openspec config profile # interactive
openspec config profile core # reset to the core six (keeps delivery)
```
2. Run the update in each project you work in:
```bash
openspec update
```
When your current directory is an existing OpenSpec project, the interactive flow offers to run step 2 there for you.
+124
View File
@@ -0,0 +1,124 @@
# Project configuration
> Make the workflows plan changes the way you want with a few lines in config.yaml.
`openspec/config.yaml` tells the workflows how you want changes planned.
For example, the following configuration updates the creation rules for the [tasks.md](../reference/schemas/spec-driven/index.md) artifact:
```yaml
rules:
tasks:
- End every task with a commit
```
When the agent runs, it pulls from these rules and ensures every task ends with a commit step.
Keep rules short. Everything here lands in the agent's context, and verbose rules can make the output worse.
## How it works
config.yaml holds instructions the agent receives when it creates artifacts or works through the workflow.
Here's what happens on every run:
1. You run a workflow (e.g. `/openspec-propose`).
2. The agent calls the [`openspec instructions`](../reference/cli.md) command.
3. The command reads your context and rules from config.yaml.
4. OpenSpec's built-in instructions and your customizations are combined into a single prompt for the agent.
5. The agent follows that prompt to write the artifact.
For example, with a `context` field and the rule from the top of this page, here's what [`openspec instructions`](../reference/cli.md) returns for tasks.md (trimmed and annotated):
```xml
<artifact id="tasks" change="add-dark-mode" schema="spec-driven">
<!-- From your config.yaml: context -->
<project_context>
Tech stack: TypeScript, Node.js
Domain: e-commerce platform
</project_context>
<!-- From your config.yaml: rules for tasks -->
<rules>
- End every task with a commit
</rules>
<!-- From OpenSpec: the built-in guidance -->
<instruction>
...how to write a good tasks.md...
</instruction>
<template>
...the tasks.md structure to fill in...
</template>
</artifact>
```
Your config arrives first, then OpenSpec's built-in instruction and template. Rules add to the built-ins and never replace them. Edits to config.yaml reach the agent on the next run.
[Workflow runs](../reference/architecture/workflow-runs.md) covers the full run, from invocation to written artifacts.
## The fields
Three fields shape what the agent receives. Each field's exact contract (types, limits, validation) is in [Project configuration (config.yaml)](../reference/configuration/config-yaml.md).
| Field | What it does | Injected into |
|---|---|---|
| `context` | Instructions the agent always receives | Everything: every artifact, `apply`, `archive` |
| `rules` | Extra instructions for one artifact | Only that artifact's creation |
| `operations` | Guidance for how a workflow step is carried out | Only `apply` and `archive` |
config.yaml's other fields (`schema`, `store`, `references`) select which schema and which OpenSpec root a project uses. The contract page covers them.
The last column is exact, so a field reaches only the steps listed there. In particular, `verify` never receives `rules`. It checks the implementation against the artifacts as written.
### context
`context` is what the agent should know up front when planning a change, whether it's creating an artifact, applying tasks, or archiving:
```yaml
context: |
We ship cross-platform; designs and tasks must cover Windows, macOS, and Linux
Tech stack: TypeScript, Node.js, Commander.js
We use conventional commits
```
This is planning context, not project documentation. Add a fact when it should shape every plan, like the cross-platform line above. Leave out anything the agent can learn by reading the code.
**Another language**: because context reaches every artifact, it's also how you change the output language. One line, like `Write all artifacts in Spanish.`, switches every proposal, spec, and tasks file the workflows write.
### rules
`rules` attach to one artifact, keyed by artifact id. Each line is added to that artifact's built-in guidance:
```yaml
rules:
proposal:
- Keep proposals under 500 words
tasks:
- Every UI task includes a Playwright test
```
Proposals now stay short and tasks.md always plans browser tests. Every other artifact is untouched.
### operations
`operations` guides how the agent carries out `apply` and `archive`, rather than what artifacts say:
```yaml
operations:
apply:
guidance:
- Run the linter before marking a task complete
archive:
guidance:
- Summarize what shipped before archiving
```
During apply, the agent lints as it completes tasks. During archive, it closes with a summary.
## When config.yaml isn't enough
Config adds instructions on top of the standard workflow, but it can't change which artifacts exist or how they're structured. When you want that level of control, or rules aren't steering behavior consistently, [fork a schema](schemas.md).
+164
View File
@@ -0,0 +1,164 @@
# Schemas
> Change what OpenSpec produces: the artifacts, their order, and their templates.
A schema defines what a change proposal produces: which artifacts, in what order, from which templates. For example, [spec-driven](../reference/schemas/spec-driven/index.md), the default bundled schema, produces these four in roughly this order, each building on what came before:
```
proposal → specs → design → tasks
```
Fork a schema when you want these to be different documents, whether that means fewer of them, different names, or a different structure.
## Where schemas live
OpenSpec looks for a schema in three places, in order, and uses the first one it finds:
1. **Your project**: `openspec/schemas/`, committed with the repo so your whole team gets it.
2. **Your machine**: `~/.local/share/openspec/schemas` on macOS and Linux (or under `$XDG_DATA_HOME` if you set it), or `%LOCALAPPDATA%\openspec\schemas` on Windows. Schemas here are available in every project you work in.
3. **The package**: the built-ins, like `spec-driven`, ship inside openspec itself.
The same name can exist in more than one place, and the more specific location wins. `openspec schema which` shows which copy is in use:
```
$ openspec schema which spec-driven
Schema: spec-driven
Source: project
Path: /your-project/openspec/schemas/spec-driven
Shadows:
package: .../openspec/schemas/spec-driven
```
## What's in a schema
A schema is defined by a folder of plain files: one schema.yaml that declares the artifacts, and a template for each of them. Here's the built-in `spec-driven`:
```
spec-driven/
├── schema.yaml
└── templates/
├── proposal.md
├── spec.md
├── design.md
└── tasks.md
```
- **schema.yaml**: declares each artifact, the file it generates, the template it starts from, what it requires first, and the instruction the agent receives when creating it. Every field's contract is in [schema.yaml](../reference/schemas/schema-yaml.md).
- **templates/**: one markdown skeleton per artifact, which the agent fills in.
Here's the tasks artifact's entry in schema.yaml, trimmed:
```yaml
artifacts:
- id: tasks
generates: tasks.md
description: Implementation checklist with trackable tasks
template: tasks.md
instruction: |
...what the agent is told when creating tasks.md...
requires:
- specs
- design
```
The built-in schemas ship inside the openspec package, so you never edit them in place. You get your own copy by forking.
## Creating your own custom schema
There are two ways to get your own schema:
1. **Fork an existing schema** and edit your copy. Start here when an existing schema is close to what you want, because everything in it already works.
2. **Start from scratch** when none of them fit, scaffolding an empty schema with `openspec schema init`.
### Fork an existing schema
1. Fork the schema you want to start from, running from your project root:
```console
$ openspec schema fork spec-driven
Note: Schema commands are experimental and may change.
✔ Forked 'spec-driven' to 'spec-driven-custom'
Source: .../openspec/schemas/spec-driven (package)
Destination: /your-project/openspec/schemas/spec-driven-custom
```
Pass a second argument to pick the name (`openspec schema fork spec-driven team-flow`). Names are kebab-case.
2. Edit the copy: schema.yaml and the templates. [Editing your fork](#editing-your-fork) covers what to change.
3. Validate it:
```bash
openspec schema validate spec-driven-custom
```
This is the one command that catches a broken schema (missing templates, bad YAML, dependency cycles) before you're in the middle of a change.
4. Point your project at it in openspec/config.yaml. This step is yours to do because fork leaves config.yaml untouched:
```yaml
schema: spec-driven-custom
```
5. New change proposals now follow your schema. Changes created earlier keep the schema they started with.
To replace the default everywhere without touching config.yaml, fork to the same name: `openspec schema fork spec-driven spec-driven`. Your project's copy then shadows the built-in, as [Where schemas live](#where-schemas-live) explains.
### Start from scratch
`openspec schema init` scaffolds a new schema instead of copying one:
```console
$ openspec schema init lite --description "Lite flow" --artifacts proposal,tasks
✔ Created schema 'lite'
Schema created at: /your-project/openspec/schemas/lite
Artifacts: proposal, tasks
```
The scaffold is bare. Artifacts come from the built-in four ids only, and the generated templates carry no instructions, so the agent gets less guidance until you write your own. From there the fork steps apply unchanged: validate it, then point config.yaml at it.
## Editing your fork
A fork has two kinds of files to edit:
- **templates/** change the skeleton of each document. Add a section to the tasks template and every new tasks.md starts with it.
- **schema.yaml** changes the workflow itself: which artifacts exist, what each one requires first, and the instruction the agent gets when creating it.
For example, to drop the design document for a leaner flow:
1. Delete the `design` entry from schema.yaml.
2. Remove `design` from the `requires` list of `tasks`.
3. Validate:
```console
$ openspec schema validate spec-driven-custom
✓ Schema 'spec-driven-custom' is valid
```
Skip step 2 and validate catches it:
```console
✗ Schema 'spec-driven-custom' has errors:
error: Invalid dependency reference in artifact 'tasks': 'design' does not exist
```
Validate after every hand-edit. A broken schema otherwise surfaces in the middle of a change, when a workflow asks for a file that isn't there. Like config.yaml, schema edits reach the agent on the next run.
## A fork is a snapshot
`openspec update` refreshes the installed skills and commands, and it never touches `openspec/schemas/`. Your fork keeps working exactly as you left it, which also means it stops receiving improvements when the built-in schema evolves. To pick those up later, fork the built-in again under a new name and port the differences across.
## Sharing schemas
Sharing a schema means copying its folder.
- **With your team**: commit `openspec/schemas/` and everyone on the repo uses it.
- **Across your projects**: put the folder in the user-level directory from [Where schemas live](#where-schemas-live).
- **From the community**: the [community catalog](https://github.com/Fission-AI/OpenSpec/blob/main/docs/customization.md#community-schemas) lists shared schemas. Copy one into `openspec/schemas/<name>` and it works like your own.
We're working on a schema registry, public and private, so schemas can be installed by name instead of copied by hand.
+14
View File
@@ -0,0 +1,14 @@
# Customizing skills
> Edit the installed skill prompts directly: what you can change, and what update overwrites.
<!-- PARKED 2026-08-14 (README TODO): out of the page index, sidebar, and sync config.
No good answer yet for edits surviving `openspec update`; the message map keeps
"How should a user edit the installed skill prompts?" as a Gap. Skeleton below is
the shape to revive. The honest page for the update-clobber caveat. -->
## What you can change
## What openspec update overwrites
## Supported alternatives
+11
View File
@@ -0,0 +1,11 @@
# Apply a change
> Run the plan: pacing, context windows, and picking up where you left off.
<!-- Skeleton: headings only. Promoted out of examples.md in review round 2. -->
## Task by task or all at once
## Managing the context window
## Continue and fast-forward
+11
View File
@@ -0,0 +1,11 @@
# Change course
> Revise a change in flight, or decide it's cleaner to start fresh.
<!-- Skeleton: headings only. -->
## Update or start fresh?
## Revising artifacts with openspec-update-change
## Advanced: revising mid-implementation
+9
View File
@@ -0,0 +1,9 @@
# Concepts
> What the two artifacts are, and how a change describes a diff against current specs.
<!-- Skeleton: headings only. Narrowed in review round 3 (dedup pass). The quickstart's archive file-steps absorbed the loop mechanics this page once planned to own, so the loop section is gone: the quickstart is the loop's only teacher, and core-vs-optional moved to customize/profiles.md. The fixed-vs-shapeable section was cut entirely (customize/overview.md dropped that framing in review, 2026-08-14: it opens straight on the options and never says what can't be changed). What remains is the one explanation this page owns: the artifacts. Specs absorbs "What a spec is"; Changes owns the delta concept: one worked delta block (ADDED/MODIFIED/REMOVED, from docs/concepts.md's core) showing a change as a diff against current specs; the file-format rules live in reference/schemas/spec-driven/index.md (Delta specs section). Disk paths appear inline with the concept that owns them (specs/ under Specs, changes/ under Changes), never as a layout section. Close with link lines: quickstart for the loop, customize/overview for the customization options. When to archive relative to a branch or PR stays in guides/teams.md. -->
## Specs: the system as built
## Changes: a folder of deltas
+16
View File
@@ -0,0 +1,16 @@
# Examples
> See what a good change looks like: real drafts, what review caught, and the fixes.
<!-- Skeleton: headings only. PARKED 2026-08-11: out of the README index and sync config. Contrived examples teach the wrong lesson for this product; the page waits for real archived changes from actual usage. Plan when revived: weak-vs-reviewed pairs (4) plus an archived-changes gallery. Earlier decisions still hold: slimmed in review round 2, purely example pairs; apply technique lives in guides/apply.md. -->
## Adding a feature to an undocumented codebase
## Changing an API without breaking clients
## A behavior-preserving refactor
## A database migration with rollback
## Real archived changes
+13
View File
@@ -0,0 +1,13 @@
# Existing codebases
> Bring OpenSpec to a codebase with a lot of code and no specs: where to start, what to backfill, and how specs grow from there.
<!-- Skeleton: headings only. Rescoped 2026-08-11: the page is the adoption guide for legacy codebases (a lot of code, no specs), not just the backfill task; "Specs for existing code" is now one section. The opening section owns "you don't need specs first": the quickstart runs on an existing repo as-is, and specs accumulate through changes. Source: docs/existing-projects.md. When prose lands, reshape to the Guides skeleton (one-line what, quickstart link, 80% path, Advanced). -->
## Start with a change
## Specs for existing code
## Working from a PRD
## Organizing specs as they grow
+17
View File
@@ -0,0 +1,17 @@
# Explore an idea
> Think it through with the agent before you commit to a proposal.
<!-- Skeleton: headings only. -->
## When to explore first
## A real explore session
## From exploration to proposal
## Advanced
### Exploring mid-change
### Challenging a draft plan
+15
View File
@@ -0,0 +1,15 @@
# Review the plan
> The two-minute pass that catches wrong turns before they're code.
<!-- Skeleton: headings only. -->
## The two-minute pass
## What good requirements look like
## What good scenarios look like
## Pushing back
## Advanced: verify after apply
+17
View File
@@ -0,0 +1,17 @@
# Teams
> Run OpenSpec as a team: what to commit, how a change rides its PR, and when to archive.
<!-- Skeleton: headings only. Retitled from "Teams & parallel changes" 2026-08-11: the page's job is running OpenSpec in a team; parallel changes stays as the touching-one-spec section, and the general parallel-changes answer (solo included) remains an unowned gap in message-map.md. Expanded in review: this page owns the git/PR conventions (the quickstart's archive step shows the mechanism; this page owns timing). Source for all three new sections: docs/team-workflow.md. "A change is a branch and a PR": one branch carries the change folder and the code, so the PR shows the plan and the diff together. "Review in pull requests": reviewer order proposal → spec delta → code; approach disagreements land on the proposal, not across 300 lines of diff. "When to archive": after the PR merges (recommended, specs/ only advances with shipped work) vs inside the PR (simpler, noisier diff); pick one and be consistent. Proposal-first PRs (plan reviewed before implementation starts) go under Advanced. -->
## Checking openspec/ into git
## A change is a branch and a PR
## Review in pull requests
## When to archive
## Parallel changes touching one spec
## Advanced: conventions for larger teams
+23
View File
@@ -0,0 +1,23 @@
# FAQ
> Short answers to the questions that don't need a page.
<!-- WIP, on the todo list: this page is not written yet and is held back from the
site (its section is commented out in website/docs.sync.config.mjs, 2026-08-21). The
file stays so the structure and inbound links survive; re-list it in the sync config
once the prose lands. -->
<!-- Every entry is a one-liner: a short answer or a router link to the page that owns the topic; how-to content never lives here (README's "FAQ is one-liners" rule). Update/uninstall moved to installation.md; the skills-missing fix and Getting help live in troubleshooting.md; the git question is a one-line yes routing to guides/teams.md, which owns the team conventions. Unfilled headings are skeletons. -->
## Should openspec/ be checked into git?
## What runs in the terminal, and what in chat?
## Does OpenSpec work with my tool?
If it has a row in the [support matrix](../reference/supported-tools.md), yes.
Pick its id at init. If it isn't listed but reads the shared `.agents/skills/`
folder, pick **Shared `.agents` skills** (`--tools agents`). If neither, request
it in the [OpenSpec repo](https://github.com/Fission-AI/OpenSpec/issues).
## Where did the old /openspec:* commands go?
+18
View File
@@ -0,0 +1,18 @@
# Migrating from the legacy workflow
> Moving from the legacy `/openspec:*` commands to OPSX.
<!-- WIP, on the todo list: this page is not written yet and is held back from the
site (its section is commented out in website/docs.sync.config.mjs, 2026-08-21). The
file stays so the structure and inbound links survive; re-list it in the sync config
once the prose lands. -->
<!-- Skeleton: headings only. -->
## What changed and why
## Command mapping
## Migrating a project
## Behavior differences
+20
View File
@@ -0,0 +1,20 @@
# Troubleshooting
> When OpenSpec doesn't do what you expected: symptoms and their fixes.
<!-- WIP, on the todo list: this page is not written yet and is held back from the
site (its section is commented out in website/docs.sync.config.mjs, 2026-08-21). The
file stays so the structure and inbound links survive; re-list it in the sync config
once the prose lands. -->
<!-- Skeleton: headings only. Canonical home for symptom-to-fix, including the skills-missing checklist. -->
## Skills don't appear in chat
## The agent ignores the workflow
## Validation failures
## Sync and archive issues
## Getting help
+65
View File
@@ -0,0 +1,65 @@
# Message map: the questions the docs must answer, and where
The [README](README.md) index runs page to job. This file runs the other way: one flat
list of the questions we need the docs to answer, each pointing at the group and page
that owns the answer. Status says whether that answer exists yet: **Answered** (the
owning page's prose has landed), **Skeleton** (owner assigned, page is headings only),
**Gap** (no owner), **Off-site** (answered outside these docs by decision). Flip a row
to Answered when its page's prose lands. Rows follow the sidebar order of the owning
page; gaps sit where their proposed home would fall, and off-site rows go last. Keep
rows coarse (question to page, never sentence to section) so this stays cheap to
maintain.
| Question | Answered by | Status |
|---|---|---|
| How do we pitch the core idea (keeping larger features on track and aligned, not just a plan before code)? | [Start › Overview](start/overview.md) (emptied 2026-08-21 for a from-scratch rewrite and pulled from the site until then; brief in Notes.md) | Skeleton |
| How does someone decide OpenSpec is worth their time? | [Start › Overview](start/overview.md) (emptied 2026-08-21, see row above) | Skeleton |
| How should a user install the CLI, update it, uninstall it? | [Start › Installation](start/installation.md) | Answered |
| How can a user hand install and setup to their AI assistant? | [Start › Installation](start/installation.md), the install.md prompt | Answered |
| How should a user add OpenSpec to their repo? | [Start › Set up your project](start/setup.md) | Answered |
| How do the workflows get into a user's tool, and why skills and commands both? | [Start › Set up your project](start/setup.md) | Answered |
| How do we teach the loop: propose, review, apply, archive? | [Start › Quickstart](start/quickstart.md) | Answered |
| How should a user run their first change end to end? | [Start › Quickstart](start/quickstart.md) | Answered |
| How does a user know which prompts go in the AI chat and which commands in the terminal? | [Start › Quickstart](start/quickstart.md) inline with each step, then [Help › FAQ](help/faq.md) | Answered |
| How do we explain what specs and changes are? | [Guides › Understanding › Concepts](guides/concepts.md) | Skeleton |
| How should a user think through an idea before proposing? | [Guides › Using › Explore an idea](guides/explore.md) | Skeleton |
| How should a user review a plan? | [Guides › Using › Review the plan](guides/review-the-plan.md) | Skeleton |
| How does a user check the implementation matches the plan before archiving? | [Guides › Using › Review the plan](guides/review-the-plan.md), the verify pass | Skeleton |
| How should a user run a plan across sessions and context limits? | [Guides › Using › Apply a change](guides/apply.md) | Skeleton |
| How should a user pace the plan: draft everything at once, or artifact by artifact? | [Guides › Using › Apply a change](guides/apply.md), continue and fast-forward | Skeleton |
| How do we explain the standard flow (propose drafts every artifact in one step) vs the iterative flow (new creates the change, continue drafts the next artifact, fast-forward catches up)? | [Start › Quickstart](start/quickstart.md) teaches only the standard flow; [Guides › Using › Apply a change](guides/apply.md) owns pacing once a change exists; [Reference › Skills](reference/skills.md) holds the new/continue/ff contracts; [Customize › Profiles](customize/profiles.md) covers installing them; a [README](README.md) TODO proposes a Using guide | Gap |
| How should a user change direction mid-change, or bail out? | [Guides › Using › Change course](guides/change-course.md) | Skeleton |
| How should a team run OpenSpec together? | [Guides › Adopting › Teams](guides/teams.md) | Skeleton |
| How should a user work on several changes at once? | [Guides › Adopting › Teams](guides/teams.md) owns the touching-one-spec collision case; the general answer (solo included, not just teams) has no owner yet | Gap |
| How should a user handle git across the loop: branching, commits, PRs? | Only archive-vs-PR ordering is owned, by [Guides › Adopting › Teams](guides/teams.md); README TODO proposes a guide | Gap |
| What does a good change look like? | `guides/examples.md` is parked until real archived changes can fill it (README TODO); no published owner | Gap |
| How should a user adopt OpenSpec on code that already exists? | [Guides › Adopting › Existing codebases](guides/existing-codebases.md) | Skeleton |
| How should a user run OpenSpec in a monorepo? | Legacy `docs/existing-projects.md` owned it (one `openspec/` at the repo root, domains map to packages); likely home is [Guides › Adopting › Existing codebases](guides/existing-codebases.md), with [Multi-repo › Stores](multi-repo/stores.md) taking packages treated as separate repos | Gap |
| How do we explain what's customizable in OpenSpec? | [Customize › Overview](customize/overview.md) | Answered |
| How does a user pick the right customization level, and when should they escalate from config to schemas? | [Customize › Overview](customize/overview.md), the "Not sure which to use?" section | Answered |
| How should a user choose which workflows are installed? | [Customize › Profiles](customize/profiles.md) | Answered |
| How does a user switch to skills only or commands only? | [Customize › Profiles](customize/profiles.md), Delivery section; [Start › Set up your project](start/setup.md) owns why both forms exist | Answered |
| How does a user make the workflows plan changes their way: context, rules, and guidance? | [Customize › Project configuration](customize/project-config.md) | Answered |
| How does a user get artifacts written in a language other than English? | [Customize › Project configuration](customize/project-config.md), the context section's "Another language" note | Answered |
| How should a user change what OpenSpec produces? | [Customize › Schemas](customize/schemas.md), with the fork walkthrough in "Creating your own custom schema" | Answered |
| How should a user edit the installed skill prompts? | No owner: `customize/skills.md` is parked (README TODO) until there's a good answer to `openspec update` overwriting edits | Gap |
| How should a user run OpenSpec across multiple repos? | [Multi-repo › Stores](multi-repo/stores.md); [Start › Set up your project](start/setup.md) routes there from "Pick where OpenSpec lives" | Answered |
| How should a user plan a change that spans repos? | [Multi-repo › Stores](multi-repo/stores.md) | Answered |
| What does each skill do, and when should a user reach for it? | [Reference › Skills](reference/skills.md) | Answered |
| Where does a user look up a terminal command? | [Reference › CLI](reference/cli.md) | Answered |
| How does a user learn what telemetry is collected, and opt out? | [Reference › Configuration › Environment variables](reference/configuration/environment-variables.md) owns the facts (was a README-TODO gap); [Help › FAQ](help/faq.md) routes searchers there | Skeleton |
| Where does a user look up an artifact's format, or a schema definition's fields? | [Reference › Schemas](reference/schemas/index.md) | Answered |
| Where does a user look up a setting or a file that changes OpenSpec's behavior? | [Reference › Configuration](reference/configuration/index.md) | Answered |
| Which openspec/ tree does a command operate on? | [Reference › Configuration › Stores](reference/configuration/stores.md) owns the whole resolution ladder, including the everyday case (nearest openspec/ wins); readers reach it from the Stores row of the [Configuration overview](reference/configuration/index.md) map | Skeleton |
| How should a user run a change with no spec impact, or retire a capability outright? | [Reference › Configuration › Change metadata](reference/configuration/change-metadata.md) owns the `skip_specs` and `retire_capabilities` contracts; [Reference › Schemas › spec-driven](reference/schemas/spec-driven/index.md), Delta specs section, owns their effect on deltas and archive; neither half has a guide owner | Gap |
| What is an initiative, and how does a change join one? | No owner: the `initiative` field's contract sits on [Reference › Configuration › Change metadata](reference/configuration/change-metadata.md), but no page teaches initiatives (multi-repo has only Stores) | Gap |
| What is a workset, and how does a user open one in their editor? | [Multi-repo › Worksets](multi-repo/worksets.md); the `openers` field's contract stays on [Reference › Configuration › CLI settings](reference/configuration/config-json.md) | Answered |
| Which AI tools work, and what's each one's syntax? | [Reference › Supported tools](reference/supported-tools.md) | Answered |
| My tool isn't listed, can I still use OpenSpec? | [Help › FAQ](help/faq.md) routes: the shared `.agents` target or an issue; [Reference › Supported tools](reference/supported-tools.md), Per-tool notes, holds the shared target's contract | Answered |
| Where does a user look up a term? | [Reference › Glossary](reference/glossary.md) | Answered |
| How is OPSX built? | [Reference › Architecture](reference/architecture/index.md) | Skeleton |
| How do we explain that the workflow is fluid, actions not phases? | [Start › Overview](start/overview.md) will carry the pitch once rewritten (the "shared map, not a plan up front" framing was in the cleared skeleton); [sources.md](sources.md) routes opsx.md's explanation to [Guides › Understanding › Concepts](guides/concepts.md), but that page narrowed to artifacts only in review round 3; likely home is Guides › Understanding, widening [Concepts](guides/concepts.md) or adding a sibling page, with [Reference › Architecture › Design decisions](reference/architecture/design-decisions.md) keeping the why | Gap |
| What should a user do when OpenSpec doesn't do what they expected? | [Help › Troubleshooting](help/troubleshooting.md), then [Help › FAQ](help/faq.md) | Skeleton |
| Where does a user go for help or to report a bug? | [Help › Troubleshooting](help/troubleshooting.md), Getting help | Skeleton |
| How should a user move off the legacy `/openspec:*` commands? | [Help › Migration](help/legacy/migration.md) | Skeleton |
| How does a script or CI drive the CLI programmatically? | Off-site by decision: repo-side contributor docs, per [sources.md](sources.md) | Off-site |
+341
View File
@@ -0,0 +1,341 @@
# Stores (beta)
> Plan changes that span repositories: one store, many repos.
OpenSpec normally lives inside one repo: an `openspec/` folder next to the code it plans. A store moves that folder into a repository of its own, and several code repos can share it.
After a one-time setup on each machine, commands like `status`, `new change`, and `archive` can work in the store from any directory.
```
team-plans (a store: OpenSpec in its own repo)
├── .openspec-store/store.yaml the store's name
└── openspec/
├── specs/
└── changes/
▲
│ set up once on each machine,
│ shared by pushing and cloning like any repo
┌─────────────┼─────────────┐
│ │ │
web-app api-server mobile-app
(code repo) (code repo) (code repo)
```
You share a store with git, the same way you share code: commit, push, pull, and review it yourself. Specs and changes get branches and pull requests the same way code does.
## When you need one
Two common reasons to use a store:
- **Frontend and backend in separate repos**: one feature touches both, and the plan needs a single home instead of two halves.
```
shop-plans (store)
└── openspec/changes/add-discounts/ one plan for the feature
▲
┌─────────┴─────────┐
│ │
storefront api
(frontend repo) (backend repo)
```
- **One product, several client repos**: Android, iOS, and web ship from their own repos but share one expected behavior. A spec describes behavior, not implementation, so one spec serves all three.
```
product-specs (store)
└── openspec/specs/checkout/spec.md the expected behavior
▲
┌─────────────┼─────────────┐
│ │ │
android-app ios-app web-app
(code repo) (code repo) (code repo)
```
You can have more than one store, though we recommend keeping the count low.
## Set up a store
One person creates the store, then everyone else joins it.
1. **Create the store** (one person, once per team). Run `openspec store setup` and answer the prompts:
```bash
# run from anywhere; it asks what to create and where
openspec store setup
```
It asks three questions:
- **Store name**: `team-plans`
- **Where should this store live?**: pre-filled with `~/openspec/<name>`, press Enter to accept it or type another path
- **Create this store?**: shows what it's about to make, answer `Yes`
Then it reports what it created:
```yaml
Store ready: team-plans
Location: ~/openspec/team-plans
OpenSpec root: ready
Registry: registered
Next: run normal OpenSpec commands against this store, for example:
openspec new change <change-id> --store team-plans
Share this store by committing and pushing it like any Git repo.
```
2. **Push it to your git host.** Create an empty `team-plans` repo on your host first. Setup doesn't add a git remote, so connect the store to that repo, then push:
```bash
# connect the store to the empty repo on your git host
cd ~/openspec/team-plans
git remote add origin git@github.com:acme/team-plans.git
# publish it
git push -u origin main
```
3. **Join the store** (every teammate, once per machine):
```bash
# get the store onto your machine
git clone git@github.com:acme/team-plans.git ~/openspec/team-plans
# tell OpenSpec where it lives
openspec store register ~/openspec/team-plans
```
```yaml
Store registered: team-plans
Location: /Users/you/openspec/team-plans
OpenSpec root: ready
Registry: registered
```
Registering tells your machine where this store lives. The store's name is already committed inside it, in `.openspec-store/store.yaml`. Setup registered the creator's copy, so only cloned copies need this step.
4. **Confirm it worked**, from any directory:
```bash
# any OpenSpec command reaches the store by name
openspec status --store team-plans
```
```yaml
Using OpenSpec root: team-plans (/Users/you/openspec/team-plans)
No active changes. Create one with: openspec new change <name> --store team-plans
```
## Types of setups
OpenSpec has three setups. The rest of this page uses these names:
- **repo-local**: OpenSpec inside your repo, no store. The default.
- **store-only**: your repo keeps no specs or changes of its own. Everything lives in the store.
- **store-optional**: your project has its own `openspec/` folder and also reaches a store when you ask.
### The default: OpenSpec inside your repo (`repo-local`)
`openspec init` puts an `openspec/` folder next to your code, and that repo's specs and changes live there. No store is involved. This is the setup [Set up your project](../start/setup.md) teaches, and most projects never need another.
```
web-app (code repo)
└── openspec/
├── specs/
└── changes/
```
### OpenSpec outside your repo, in a store (`store-only`)
The repo keeps no specs or changes of its own. Everything it plans lives in the store, and one line in the repo's config connects the two.
Common when one team builds all the repos and plans in one place. The [examples above](#when-you-need-one) all have this shape.
```
team-plans (store)
└── openspec/
├── specs/ the repo's specs live here
└── changes/ its changes too
▲
│ store: team-plans (the connecting line)
web-app (code repo)
└── openspec/
└── config.yaml nothing else
```
### OpenSpec in your repo and in a store (`store-optional`)
The repo stays repo-local for its own work, while the store holds the shared specs and changes. Inside the repo, OpenSpec uses your project's `openspec/` folder, and reaches the store only when you pass `--store`.
Common when a repo used OpenSpec before the store existed, or when a mostly independent repo only occasionally touches shared work.
```
team-plans (store)
└── openspec/ the shared specs and changes
▲
│ only when you pass --store team-plans
web-app (code repo)
└── openspec/ this repo's own
├── config.yaml
├── specs/
└── changes/
```
A repo can start repo-local and move its specs and changes into the store later. [Move a repo's specs and changes into the store](#move-a-repos-specs-and-changes-into-the-store) shows how.
## Where artifacts get created when using stores
When you use a store, OpenSpec also has to decide where the artifacts get created. It depends on your setup:
- **store-only** (your project only writes to the store): every artifact is created in the store. The `store:` line below records that.
- **store-optional** (your project has its own `openspec/` folder and also uses a store): artifacts are created in your project, unless you name the store in your request or pass `--store` for that change. Your agent then carries the flag through the rest of the workflow.
OpenSpec writes artifacts to one of two places: your project's `openspec/` folder, or the store's. It picks in this order, and the first option that applies wins:
1. **`--store <id>` on a command.** Always wins, from any directory.
2. **Your project's `openspec/` folder.** If your project has its own `specs/` or `changes/` folders, OpenSpec uses them.
3. **The `store:` line in your project.** How a store-only project records its store.
4. **`defaultStore` on your machine.** The fallback when none of the above applies.
Whichever applied, OpenSpec's first output line names the folder it acted on (`Using OpenSpec root: ...`). The exact rules, including the error cases, are in [Configuration › Stores](../reference/configuration/stores.md).
### The `store:` line (store-only projects)
Add one line to your project's `openspec/config.yaml`:
```yaml
# web-app/openspec/config.yaml
store: team-plans
```
Everything you or your agent run inside your project now uses the store, with no flag to type:
```bash
# inside web-app, connected
openspec status
```
```yaml
Using OpenSpec root: team-plans (/Users/you/openspec/team-plans)
No active changes. Create one with: openspec new change <name> --store team-plans
```
- **Without the line**: run a plain command in a store-only project and OpenSpec stops with an error listing your registered stores.
- **Commit it**: teammates who clone your project get the line too. They still need the store registered on their machine ([step 3 of Set up a store](#set-up-a-store)), or OpenSpec errors and tells them to register it.
- **Next to real folders**: if your project also has `specs/` or `changes/` folders, OpenSpec uses those and ignores the line, with a warning.
### `defaultStore` on your machine
Set it once if every project you work in uses the same store. OpenSpec falls back to it when it finds no flag, no local `openspec/` folder, and no `store:` line:
```bash
# use team-plans whenever nothing else names a store
openspec config set defaultStore team-plans
# undo it
openspec config unset defaultStore
```
**Commands that stay local.** `init`, `update`, `templates`, `schemas`, and the `openspec schema` subcommands act on the current directory only and take no `--store`.
## Move a repo's specs and changes into the store
To take a repo from repo-local to store-only:
1. Move everything in the repo's `openspec/specs/` and `openspec/changes/` into the same folders in the store.
2. Delete the now-empty folders, so the repo's `openspec/` folder holds only `config.yaml`.
3. Add the `store:` line to that `config.yaml`.
`openspec status` inside the repo now starts with `Using OpenSpec root: team-plans`.
## Work in the store
The workflows don't change, for you or for your agent. Propose, apply, and archive run the way they always do. The only difference is where the artifacts get created, and [the section above](#where-artifacts-get-created-when-using-stores) covers that.
Create a change from inside a store-only repo and it lands in the store:
```bash
# inside web-app; the store: line routes this to team-plans
openspec new change add-login
```
```yaml
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
Next: openspec status --change add-login --store team-plans
```
- **Where it went**: into the store repo, not next to your code.
- **Sharing it**: the change exists only in your checkout until you commit and push the store repo. Teammates see it when they pull. The same goes for every artifact the workflows write.
- **Paths in the docs**: wherever the docs show an `openspec/` path, in a store setup that folder is the store's.
When artifacts get created somewhere you didn't expect, `openspec doctor` checks your setup without changing anything and prints a fix for each finding:
```bash
# check the current root and its stores
openspec doctor
```
```yaml
Doctor
Root
Location: /Users/you/openspec/team-plans
OpenSpec root: ok
Store: team-plans (metadata ok)
References
(none declared)
```
`openspec context` lists the root and stores your current directory works with, when you want the same picture without the checks.
To open the store and a repo in one editor window, so your agent can read both, see [Worksets (beta)](worksets.md).
## Read specs from another store
Your repo can keep its own `openspec/` folder and still let your agent read another store's specs. Declare that store under `references:` in the repo's `openspec/config.yaml`:
```yaml
# api-server/openspec/config.yaml
references:
- team-plans
```
References are read-only. Your work stays in your repo, and the reference only changes what your agent is told.
When a workflow creates an artifact, its instructions gain an index of the referenced store's specs, each with a one-line summary and the exact command to fetch it:
```xml
<referenced_stores>
<!-- Read-only upstream context. Fetch what you need; cite what you use. -->
Store team-plans (/Users/you/openspec/team-plans):
- payments: Rules for charging and refunding customers.
Fetch: openspec show <spec-id> --type spec --store team-plans
</referenced_stores>
```
A reference can also carry the store's clone URL, for machines that don't have that store yet:
```yaml
references:
- team-plans
- { id: design-system, remote: "git@github.com:acme/design-system.git" }
```
With the URL declared, `openspec doctor` turns a missing store into a pasteable fix:
```yaml
# output wrapped to fit
References
- team-plans: ok (/Users/you/openspec/team-plans)
- design-system: Referenced store 'design-system' is not registered on this machine.
Fix: git clone -- git@github.com:acme/design-system.git '/Users/you/openspec/design-system' &&
openspec store register '/Users/you/openspec/design-system' --id design-system
```
## Beta limits
- **The shape may change**: command names, flags, and file formats can change between releases. Re-read this page after upgrading.
- **No sync, by design**: OpenSpec never clones, pulls, or pushes. A stale checkout shows stale specs until you pull, and references are read from whatever is on disk.
- **One checkout per store name**: registering a second folder under a name that's already registered fails, with a hint to run `openspec store unregister` first.
+62
View File
@@ -0,0 +1,62 @@
# Worksets (beta)
> Open the store and the repos that use it in one editor window, so your agent sees both.
With a store, the context your agent needs is split across folders. The specs and changes live in the store, and the code lives in each repo. An agent started in one repo can read and grep that repo and nothing else, so it works from half the picture.
Worksets are the utility OpenSpec provides for this. A workset is a saved, named list of folders you open together. This page assumes the store is already set up and registered on your machine. [Stores (beta)](stores.md) covers that.
## How it works
- **What it is**: a named list of folders, saved on your machine only. Nothing is written into the member folders, and nothing is committed.
- **What opening does**: OpenSpec generates a `.code-workspace` file from the list and launches your editor on it. Every member folder sits in one window.
- **What you get**: your editor's search, and any agent you run inside that window, can read every member folder. The agent can grep the store's specs and the repo's code in one session.
- **What it doesn't change**: which `openspec/` folder a command uses. That still follows [Where artifacts get created](stores.md#where-artifacts-get-created-when-using-stores).
## Set it up
1. **Save the workset** (once per machine). List the repo and the store as members, and the tool to open them with:
```bash
# save a named list of folders you open together
openspec workset create platform \
--member ~/src/web-app \
--member ~/openspec/team-plans \
--tool code
```
```yaml
Saved workset 'platform' (2 members) to your machine.
Open it any time with: openspec workset open platform
```
2. **Open it** whenever you start work:
```bash
# open every member in one VS Code window
openspec workset open platform
```
`openspec workset list` shows what you saved, and `openspec workset remove <name>` deletes a workset without touching the member folders:
```yaml
platform (opens in VS Code)
web-app /Users/you/src/web-app
team-plans /Users/you/openspec/team-plans
```
## Use it: one change, two folders
Say the `add-login` change lives in the `team-plans` store, and the code for it lives in `web-app`. Open the `platform` workset and ask your agent to implement the change. In that one session it can:
- read `team-plans/openspec/changes/add-login/` and the specs next to it
- edit the code in `web-app/`
- run `openspec` commands from inside `web-app`
Without the workset, the agent only sees whichever folder it was started in.
## Tools out of the box
- **VS Code** (`--tool code`) and **Cursor** (`--tool cursor`): built in. Each opens one window with every member folder.
- **Claude Code and Codex in the terminal**: temporarily disabled as workset openers while that flow is reworked. `--tool claude` or `--tool codex` stops with an error that says so and points you to VS Code or Cursor.
- **Other editors**: add them under the `openers` key in [CLI settings (config.json)](../reference/configuration/config-json.md).
@@ -0,0 +1,11 @@
# Design decisions
> Why OPSX works the way it does.
<!-- WIP, on the todo list: this page is not written yet and is held back from the
site (its entry is commented out in website/docs.sync.config.mjs, 2026-08-21). The file
stays so the structure and inbound links survive; re-list it in the sync config once the
prose lands. -->
<!-- Skeleton: heading only. Split from reference/architecture.md on 2026-08-10.
message-map.md routes "the workflow is fluid, actions not phases" here. -->
+19
View File
@@ -0,0 +1,19 @@
# Overview
> How OPSX is built: internals for the curious.
<!-- WIP, on the todo list: this page is not written yet and is held back from the
site (its entry is commented out in website/docs.sync.config.mjs, 2026-08-21). The file
stays so the structure and inbound links survive; re-list it in the sync config once the
prose lands. -->
<!-- Skeleton: headings only. Moved out of Legacy in review round 2 (it documents
the current system); split from a single architecture.md page into this folder
on 2026-08-10. -->
The pages in this section:
- [Workflow runs](workflow-runs.md): how a workflow run executes, from invocation to written artifacts.
- [Design decisions](design-decisions.md): why OPSX works the way it does.
## How the pieces fit
@@ -0,0 +1,10 @@
# Workflow runs
> How a workflow run executes, from invocation to written artifacts.
<!-- WIP, on the todo list: this page is not written yet and is held back from the
site (its entry is commented out in website/docs.sync.config.mjs, 2026-08-21). The file
stays so the structure and inbound links survive; re-list it in the sync config once the
prose lands. -->
<!-- Skeleton: heading only. Split from reference/architecture.md on 2026-08-10. -->
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,62 @@
# Change metadata (.openspec.yaml)
> The supported fields and validation rules for the metadata stored with each change.
## Location
Each change keeps its metadata at `openspec/changes/<change-name>/.openspec.yaml`, next to its artifacts. Creating a change writes the file with `schema` and `created` filled in.
## Fields
| Key | Type | Required | Effect |
| --- | --- | --- | --- |
| `schema` | string | Yes | The workflow schema this change follows |
| `created` | string, YYYY-MM-DD | No | Records the date the change was created |
| `goal` | string | No | Records what the change sets out to do |
| `affected_areas` | list of strings | No | Records the areas the change expects to touch |
| `initiative` | map: `store` and `id` | No | Records the initiative this change belongs to |
| `skip_specs` | boolean | No | Declares the change makes no spec deltas, so zero deltas validate |
| `retire_capabilities` | boolean | No | Authorizes archive to delete a capability this change empties |
### schema
The workflow schema this change follows. It is set when the change is created and wins over the project config, so a change keeps its schema even if `openspec/config.yaml` changes afterwards. Valid names are listed in [Schemas](../schemas/index.md).
### initiative
The initiative this change belongs to, as a store id and an initiative id, both kebab-case:
```yaml
initiative:
store: platform-specs
id: unify-billing
```
Keys other than `store` and `id` are rejected. No command reads the link today.
### skip_specs
Declares the change intentionally makes no spec deltas: a pure refactor, tooling, or docs change. With it set, validation accepts zero deltas, and artifacts that would generate spec files count as complete. Setting it while spec files exist under specs/ is a validation error. Its effect on deltas and archive is on [spec-driven](../schemas/spec-driven/index.md).
### retire_capabilities
Authorizes archive to retire a capability. When this change's REMOVED deltas take away the last requirement a capability has, archive deletes that capability's main spec instead of stopping. The flag exists because the deletion is only recoverable from git, so it stays the author's call. The archive behavior is on [spec-driven](../schemas/spec-driven/index.md).
## Example
A filled-in .openspec.yaml:
```yaml
schema: spec-driven
created: 2026-08-14
goal: Add magic-link login to the API
affected_areas:
- auth
- api
```
## Validation
The file is validated whenever a command writes or reads it. A write that fails validation throws and writes nothing. Reading an existing file fails on invalid YAML, a field that breaks its contract, or a schema name that is not available. A missing file is not an error, and the change is treated as having no metadata.
Unlike [config.yaml](config-yaml.md), bad values are never dropped with a warning. A metadata error stops the command. The one exception is unknown top-level keys, which are ignored rather than rejected.
@@ -0,0 +1,63 @@
# CLI settings (config.json)
> Every field of config.json: how the openspec CLI behaves on your machine.
## Location
The CLI keeps its machine-level settings at `~/.config/openspec/config.json` on macOS and Linux, and `%APPDATA%\openspec\config.json` on Windows. `$XDG_CONFIG_HOME` wins on every platform when set. The `openspec config` command reads and edits it.
## Fields
| Key | Type | Required | Effect |
| --- | --- | --- | --- |
| `profile` | string: `core` or `custom` | No | Picks the workflow set `openspec init` installs |
| `delivery` | string: `both`, `skills`, or `commands` | No | Whether init installs skills, slash commands, or both |
| `workflows` | list of strings | No | The workflow list a `custom` profile installs |
| `featureFlags` | map: flag → boolean | No | Boolean feature toggles |
| `defaultStore` | string | No | Machine-level fallback store for root resolution |
| `openers` | list | No | The tools worksets open in, and how each is launched |
| `telemetry` | map | No | State the CLI keeps: anonymous id and notice-seen |
### profile
Which workflow set `openspec init` installs. Defaults to `core`: propose, explore, apply, update, sync, and archive. Setting `custom` installs exactly the `workflows` list instead.
### delivery
Whether init installs workflows as skills, as slash commands, or both. Defaults to `both`.
### workflows
The workflows a `custom` profile installs; ignored when the profile is `core`. Valid ids: `propose`, `explore`, `new`, `continue`, `apply`, `update`, `ff`, `sync`, `archive`, `bulk-archive`, `verify`, `onboard`.
### featureFlags
Boolean toggles keyed by flag name, set with `openspec config set featureFlags.<flag> true`. No flag is read by the CLI today.
### defaultStore
The machine-level fallback store id for root resolution, consulted only when no `--store` flag, local `openspec/`, or project `store:` pointer resolves. The full ladder is [Root resolution](stores.md#root-resolution).
### openers
The tools a workset can open in, and how each is launched. Entries are hand-edited and validated on use. Each may set `style` (`workspace-file` or `attach-dirs`), `label`, `command`, `args`, and `attach_flag`, and is merged over the built-in defaults.
### telemetry
State the CLI writes for telemetry: your anonymous id and whether the first-run notice was shown. It is not the opt-out. Disabling telemetry is an environment variable, on [Environment variables](environment-variables.md).
## Example
A filled-in config.json:
```json
{
"profile": "core",
"delivery": "both",
"featureFlags": {},
"telemetry": {
"anonymousId": "5f8a2c1e-4b6d-4f9a-9c3d-7e1b2a8d4c6f",
"noticeSeen": true
}
}
```
@@ -0,0 +1,102 @@
# Project configuration (config.yaml)
> Every field of openspec/config.yaml: the schema, context, and rules this project plans with.
## Location
Each OpenSpec project keeps its config file at `openspec/config.yaml`, in the project root.
## Fields
| Key | Type | Required | Effect |
| --- | --- | --- | --- |
| `schema` | string | Yes | The workflow schema this project's changes follow |
| `context` | string | No | Injected into every artifact's instructions |
| `rules` | map: artifact ID → list of strings | No | Extra rules added to one artifact's built-in guidance |
| `operations` | map: operation → guidance list | No | Advisory guidance for apply and archive work |
| `store` | string | No | Fallback OpenSpec root when this openspec/ is config-only |
| `references` | list | No | Stores whose specs are indexed into instructions |
Invalid fields never fail a command. Each field is validated on its own, and a bad value is dropped with a warning.
What to write in these fields is covered in [Project configuration](../../customize/project-config.md).
### schema
The workflow schema every change in this project follows. Valid values are `spec-driven` or a schema name the project defines. The names are listed in [Schemas](../schemas/index.md).
### context
Free text injected into every artifact's instructions. The limit is 50KB, and a larger value is ignored with a warning.
### rules
Extra rules for one artifact, added to the schema's built-in guidance:
```yaml
rules:
proposal:
- Keep proposals under 500 words
```
Artifact IDs are not restricted to the built-in names, so artifacts from custom schemas work as keys.
### operations
Advisory guidance for how apply and archive work is conducted, separate from artifact rules:
```yaml
operations:
apply:
guidance:
- Keep test summaries concise
```
Only `apply` and `archive` are read.
### store
A store id used as the OpenSpec root, consulted only when this openspec/ directory is config-only (no specs/ or changes/). It is a fallback, never an override. The full ladder is [Root resolution](stores.md#root-resolution).
### references
Store ids whose specs this project's work draws on. An index of each store's specs (id, summary, fetch command) is added to instructions output. Spec content is never inlined, and root resolution is never affected. An entry is a store id or a map with `id` and an optional `remote` clone source:
```yaml
references:
- platform-specs
- id: billing-specs
remote: git@github.com:acme/billing-specs.git
```
## Example
A filled-in config.yaml:
```yaml
schema: spec-driven
context: |
Tech stack: TypeScript, React, Node.js
We use conventional commits
Domain: e-commerce platform
rules:
proposal:
- Keep proposals under 500 words
- Always include a "Non-goals" section
tasks:
- Break tasks into chunks of max 2 hours
operations:
apply:
guidance:
- Keep test summaries concise
archive:
guidance:
- Summarize the archive outcome before finishing
```
## Legacy names
`openspec/config.yml` is read as an alias when `config.yaml` does not exist. When both files exist, `config.yaml` wins and `config.yml` is ignored. `openspec init` creates `config.yaml`.
@@ -0,0 +1,14 @@
# Environment variables
> Every environment variable OpenSpec reads.
<!-- Skeleton: headings only. This page is the telemetry opt-out's home
(README TODO): OPENSPEC_TELEMETRY=0, DO_NOT_TRACK=1, auto-disabled in CI, plus
what's collected. start/installation.md's Deno section links here to explain
its network-permission flag. XDG vars move the config/data directories. -->
## OPENSPEC_TELEMETRY
## DO_NOT_TRACK
## XDG_CONFIG_HOME and XDG_DATA_HOME
+11
View File
@@ -0,0 +1,11 @@
# Overview
> Every file and setting that changes how OpenSpec behaves, and where each lives.
| File | Lives at | Controls |
| --- | --- | --- |
| [Project configuration (config.yaml)](config-yaml.md) | `openspec/config.yaml` | The schema, context, and rules this project plans with |
| [Change metadata (.openspec.yaml)](change-metadata.md) | `openspec/changes/<name>/.openspec.yaml` | The workflow schema, goal, scope, and spec exceptions for one change |
| [CLI settings (config.json)](config-json.md) | `~/.config/openspec/config.json` (Windows varies) | How the openspec CLI behaves on your machine |
| [Environment variables](environment-variables.md) | Your shell or CI environment | Telemetry opt-out, and where the config and data directories live |
| [Stores](stores.md) | `~/.local/share/openspec/stores/` (Windows varies) | The registry and metadata behind multi-repo stores |
@@ -0,0 +1,22 @@
# Stores
> The files behind multi-repo stores: registry.yaml and store.yaml, and which root a command uses.
<!-- Skeleton: headings only. Beta, like the multi-repo group. Machine-maintained
rather than hand-edited; documented so readers can inspect and repair them. The
concept and workflow live in multi-repo/stores.md. Root resolution is the
contract from src/core/root-selection.ts: --store flag, else nearest ancestor
openspec/, else a config-only openspec/'s store: pointer, else the global
defaultStore, else error. This page owns the whole ladder including the
everyday case (nearest openspec/ wins); the section's Overview only links
here. Locations (store/foundation.ts): registry.yaml at <dataDir>/stores/
(~/.local/share/openspec/stores/); store.yaml at .openspec-store/store.yaml
inside each checkout. The glossary's "OpenSpec root" row links here. -->
## registry.yaml
## store.yaml
## Locations
## Root resolution
+40
View File
@@ -0,0 +1,40 @@
# Glossary
> Every OpenSpec term, one line each.
OpenSpec reuses words that mean something else in git, CI, and agent tooling. Each row gives the OpenSpec meaning, and the last column links to the page that teaches the term.
| Term | Definition | More |
|---|---|---|
| **Apply** | Implement the tasks in a change proposal. Skill: `openspec-apply-change`. | [Apply a change](../guides/apply.md) |
| **Archive** | Complete a change proposal: merge its deltas into the main specs and move its folder to `openspec/changes/archive/`. | [Quickstart](../start/quickstart.md) |
| **Artifact** | A planning document inside a change proposal: `proposal.md`, delta specs, `design.md`, `tasks.md`. Not a build output. | [Concepts](../guides/concepts.md) |
| **Capability** | One behavior area of your system. Each has one spec at `openspec/specs/<capability>/spec.md`. | [Concepts](../guides/concepts.md) |
| **Change proposal** | One unit of work: a folder under `openspec/changes/<name>/` holding its planning artifacts. Often shortened to "change". Not a git commit. | [Concepts](../guides/concepts.md) |
| **Command** | A typed entry point for a workflow. Spelling varies per tool (`/opsx:propose`, `/opsx-propose`). The docs name workflows by skill instead. | [Supported tools](supported-tools.md) |
| **Continue** | Create the next planning artifact for an existing change proposal. Skill: `openspec-continue-change`. | [Skills](skills.md) |
| **Delivery** | How workflows are installed: as skills, commands, or both. | [Set up your project](../start/setup.md) |
| **Delta spec** | A spec inside a change proposal listing only what changes, under `ADDED`, `MODIFIED`, `REMOVED`, and `RENAMED` headers. | [Delta specs](schemas/spec-driven/index.md#delta-specs-specmd) |
| **Explore** | Think an idea through with the agent before proposing. Writes no code. Skill: `openspec-explore`. | [Explore an idea](../guides/explore.md) |
| **Fast-forward** | Create a change proposal with every planning artifact in one pass, ready to implement. Skill: `openspec-ff-change`. Not a git fast-forward. | [Skills](skills.md) |
| **Legacy workflow** | The pre-OPSX `/openspec:*` commands. | [Migration](../help/legacy/migration.md) |
| **Loop** | The cycle a change proposal moves through: explore, propose, review, apply, archive. | [Quickstart](../start/quickstart.md) |
| **Main specs** | The `openspec/specs/` tree: the current, agreed behavior of your system. Archiving merges deltas into it. | [Concepts](../guides/concepts.md) |
| **OpenSpec root** | The `openspec/` tree a command resolves to and operates on: your repo's, or a store's. | [Stores](configuration/stores.md) |
| **OPSX** | The current OpenSpec workflow system, and the command prefix it installs (`/opsx:`). | [Architecture](architecture/index.md) |
| **Profile** | Which workflows init installs: `core` or `custom`. | [Profiles](../customize/profiles.md) |
| **Propose** | Create a change proposal and generate all its planning artifacts in one step. Skill: `openspec-propose`. | [Quickstart](../start/quickstart.md) |
| **Registry** | The machine-level list of registered stores, in `registry.yaml`. Not a package registry. | [Stores](configuration/stores.md) |
| **Requirement** | One behavior the system must have, written with SHALL: `### Requirement:` in a spec. | [Delta specs](schemas/spec-driven/index.md#delta-specs-specmd) |
| **Scenario** | A testable example under a requirement, in WHEN/THEN form. | [Delta specs](schemas/spec-driven/index.md#delta-specs-specmd) |
| **Schema** | The definition of which artifacts a change proposal produces, and in what order. Not JSON Schema. | [Schemas](schemas/index.md) |
| **Skill** | A workflow's instructions, installed where your AI tool reads them (`.agents/skills/`, ...). | [Skills](skills.md) |
| **Spec** | A file describing how one capability behaves today, at `openspec/specs/<capability>/spec.md`. | [Concepts](../guides/concepts.md) |
| **spec-driven** | The default schema: proposal, then delta specs, then design, then tasks. | [spec-driven](schemas/spec-driven/index.md) |
| **Store** | A standalone OpenSpec repo registered on your machine, for planning that spans repositories. Not a data store. | [Stores (beta)](../multi-repo/stores.md) |
| **Sync** | Merge implemented deltas into the main specs without archiving. Skill: `openspec-sync-specs`. | [Skills](skills.md) |
| **Template** | The starting content a schema gives each artifact. | [Schemas](../customize/schemas.md) |
| **Update** | As a skill (`openspec-update-change`): revise a change proposal's planning artifacts. As a CLI command (`openspec update`): refresh OpenSpec's installed files. | [Change course](../guides/change-course.md), [CLI](cli.md) |
| **Verify** | Check the implementation matches a change proposal's artifacts before archiving. Skill: `openspec-verify-change`. | [Skills](skills.md) |
| **Workflow** | A named OpenSpec action (propose, apply, archive, ...), installed into your AI tool as a skill or command. | [Set up your project](../start/setup.md) |
| **Workset** | A personal, local group of folders opened together in one tool. Not a store, and nothing is shared. | [Worksets (beta)](../multi-repo/worksets.md) |
+20
View File
@@ -0,0 +1,20 @@
# Overview
> Every available workflow schema and the artifacts it defines.
<!-- This group states the formats. How schemas shape artifacts and how to
change or write one is customize/schemas.md's job. -->
A schema defines which artifacts a change proposal produces, and in what order. On disk it's a folder with a schema.yaml in it. Every field of that file is on the [schema.yaml](schema-yaml.md) page.
## Available schemas
One schema ships with the CLI:
| Schema | Artifacts |
|---|---|
| [spec-driven](spec-driven/index.md) (default) | `proposal`, `specs`, `design`, `tasks` |
A project can add its own schemas, and a machine can override globally. Where those folders live and which copy wins is in schema.yaml's [Location](schema-yaml.md#location) section.
In your terminal, [`openspec schemas`](../cli.md#openspec-schemas) prints every schema your project can see.
+209
View File
@@ -0,0 +1,209 @@
# schema.yaml
> Every field of a schema definition, for reading or writing one.
`schema.yaml` lists the planning files a workflow creates. It also defines their order and the handoff to implementation.
## Location
A project schema lives under `openspec/schemas/<name>/`:
```text
openspec/schemas/review-first/
├── schema.yaml
└── templates/
├── proposal.md
└── tasks.md
```
OpenSpec checks three places for that directory. The first match wins.
| Copy | Directory |
|---|---|
| **1. Project** | `<project>/openspec/schemas/<name>/` |
| **2. User, macOS and Linux** | `~/.local/share/openspec/schemas/<name>/` |
| **2. User, Windows** | `%LOCALAPPDATA%\openspec\schemas\<name>\` |
| **3. Package** | The schemas installed with the CLI |
If `XDG_DATA_HOME` is set, the user directory moves to `$XDG_DATA_HOME/openspec/schemas/<name>/` on every platform.
The directory name is the lookup key used by `--schema`, `config.yaml`, and [`.openspec.yaml`](../configuration/change-metadata.md#schema). If the `name` field differs from the directory name, OpenSpec still uses the directory name for lookup.
[`openspec schema which <name>`](../cli.md#openspec-schema-which) prints the active directory and any lower-priority copies it hides.
## Top-level fields
| Field | Contract |
|---|---|
| `name` | **Required.** A non-empty string stored as the schema name. Lookup still uses the directory name. |
| `version` | **Required.** A positive integer stored as the schema revision. The value doesn't change OpenSpec's behavior. |
| `description` | An optional string printed by `openspec schemas`. With no value, the schema has no description. |
| `artifacts` | **Required.** A non-empty list of [artifact entries](#artifact-fields). |
| `apply` | Optional [apply settings](#apply-fields). With no block, OpenSpec uses the [apply defaults](#apply-defaults). |
## Artifact fields
Each entry under `artifacts` defines one planning file or set of files.
| Field | Contract |
|---|---|
| `id` | **Required.** A unique, non-empty string used in dependencies, project rules, commands, and apply settings. |
| `generates` | **Required.** A relative path or glob telling the agent where to write the artifact inside the change folder. |
| `description` | **Required.** A string that labels the artifact in instructions sent to the agent. |
| `template` | **Required.** A relative path to the artifact's format in the schema's `templates/` folder. |
| `instruction` | Optional guidance telling the agent what content to produce. |
| `requires` | A list of artifact IDs that must be complete first. Default: `[]`. |
### `generates`
The path starts from the change folder. For a change named `add-auth`:
```yaml
generates: proposal.md
```
The artifact goes here:
```text
openspec/changes/add-auth/proposal.md
```
A glob can match several files:
```yaml
generates: specs/**/*.md
```
This matches Markdown files below `openspec/changes/add-auth/specs/`. OpenSpec treats a value containing `*`, `?`, or `[` as a glob.
OpenSpec rejects absolute paths and paths containing a `..` segment.
#### Completion
OpenSpec checks whether the output exists. It doesn't read the file to decide whether the artifact is complete.
| `generates` value | Complete when |
|---|---|
| `proposal.md` | That file exists. |
| `specs/**/*.md` | The glob matches at least one file. |
### `template`
The path starts from the schema's `templates/` folder. In the `review-first` schema:
```yaml
template: proposal.md
```
OpenSpec reads this file:
```text
openspec/schemas/review-first/templates/proposal.md
```
OpenSpec gives the template's contents to the agent as the output format. It doesn't copy the template into the change folder.
OpenSpec rejects absolute paths and paths containing a `..` segment.
### `requires`
- **Dependencies**: every ID in `requires` must name another artifact in the same schema.
- **Ready state**: an artifact becomes ready after all its dependencies are complete.
- **Invalid graphs**: missing IDs, duplicate IDs, and dependency cycles fail validation.
- **Ties**: when several artifacts are ready, their order in `artifacts` decides which one OpenSpec returns first.
## Apply fields
`apply` defines what must exist before implementation starts.
| Field | Contract |
|---|---|
| `requires` | **Required.** A non-empty list of artifacts that must exist before apply instructions become ready. |
| `tracks` | An optional relative path to a Markdown task file in the change folder. Default: `null`. |
| `instruction` | Optional guidance sent to the agent when apply is ready. OpenSpec uses built-in guidance by default. |
Artifact `requires` controls planning order. `apply.requires` controls when apply instructions become ready.
### `tracks`
The path starts from the change folder. For a change named `add-auth`, `tracks: tasks.md` reads:
```text
openspec/changes/add-auth/tasks.md
```
Apply stays blocked if that file is missing or contains no checkbox with task text. OpenSpec counts these checkbox forms:
```markdown
- [ ] Pending task
- [x] Completed task
* [X] Completed task
```
Leading spaces are allowed. The [tasks.md section of the spec-driven page](spec-driven/index.md#tasksmd) defines the stricter format produced by the default schema.
The tracked file drives the apply state:
- **`blocked`**: the file is missing, or no checkbox has task text.
- **`ready`**: at least one tracked task is pending.
- **`all_done`**: every tracked task is checked.
OpenSpec rejects absolute paths and paths containing a `..` segment.
### Apply defaults
| Behavior | Default |
|---|---|
| Required artifacts | Every artifact in the schema |
| Progress tracking | No tracked file |
| Agent guidance | Built-in apply guidance |
## Complete example
```yaml
name: review-first
version: 1
description: Proposal and implementation checklist
artifacts:
- id: proposal
generates: proposal.md
description: Why the change is needed and what it affects
template: proposal.md
instruction: |
Explain the problem, the proposed change, and its impact.
requires: []
- id: tasks
generates: tasks.md
description: Trackable implementation checklist
template: tasks.md
instruction: |
Break the approved proposal into ordered implementation tasks.
requires:
- proposal
apply:
requires:
- tasks
tracks: tasks.md
instruction: |
Work through the pending tasks and mark each one complete.
```
## Validation
[`openspec schema validate <name>`](../cli.md#openspec-schema-validate) checks:
- Field types and required fields
- Relative paths
- Artifact IDs, dependencies, and cycles
- Template files
Validation doesn't catch these mistakes:
| Mistake | What happens |
|---|---|
| A field is misspelled, such as `instrution` | OpenSpec ignores it. Validation doesn't report the typo. |
| `apply.requires` names an unknown artifact ID | Validation doesn't report the unknown ID. |
| `name` differs from the schema directory | Validation passes. OpenSpec still uses the directory name for lookup. |
@@ -0,0 +1,429 @@
# spec-driven
> The default workflow's artifacts: their order, their formats, and the change folder they produce.
`spec-driven` is OpenSpec's built-in default schema. [schema.yaml](../schema-yaml.md) defines the fields it sets.
## Artifacts
The workflow drafts four artifacts:
| Artifact | File | Purpose |
|---|---|---|
| [`proposal`](#proposalmd) | `proposal.md` | Why the change is needed |
| [`specs`](#delta-specs-specmd) | `specs/<capability-path>/spec.md`, one per capability | What behavior changes |
| [`design`](#designmd) | `design.md` | How to build it |
| [`tasks`](#tasksmd) | `tasks.md` | The implementation checklist |
## Drafting order
```text
┌─ specs ──┐
proposal ────┤ ├── tasks ── apply
└─ design ─┘
```
Proposal comes first. Specs and design follow in either order, and tasks needs both. Implementation ([apply](#apply)) starts once `tasks.md` is in place.
Two artifacts can be skipped:
- **`design`**: when none of [its conditions](#designmd) apply, the agent leaves it out and drafts `tasks` anyway.
- **`specs`**: set [`skip_specs: true`](../../configuration/change-metadata.md#skip_specs) in the change's `.openspec.yaml`.
## Example change folder
A change named `add-user-auth`, with every artifact drafted:
```text
openspec/changes/add-user-auth/
├── .openspec.yaml change metadata, written when the change is created
├── proposal.md
├── specs/
│ └── user-auth/
│ └── spec.md one delta spec per capability
├── design.md
└── tasks.md
```
## proposal.md
Establishes why the change is needed.
### Structure
The template the agent receives as the output format ([templates/proposal.md](https://github.com/Fission-AI/OpenSpec/blob/main/schemas/spec-driven/templates/proposal.md)):
```md
## 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. Use kebab-case for path segments you introduce
(e.g., user-auth or identity/user-auth) that follow the project's existing
spec organization. Each creates specs/<capability-path>/spec.md. -->
- `<capability-path>`: <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 the exact existing path under openspec/specs/. Leave empty if no requirement
changes. A change with no capabilities at all (pure refactor, tooling, docs)
must set `skip_specs: true` in its .openspec.yaml - openspec validate rejects
a zero-delta change without that marker. Do not invent a requirement just to
satisfy validation. -->
- `<existing-capability-path>`: <what requirement is changing>
## Impact
<!-- Affected code, APIs, dependencies, systems -->
```
### Instructions
The instruction sent to the agent when it drafts this artifact (from [schema.yaml](https://github.com/Fission-AI/OpenSpec/blob/main/schemas/spec-driven/schema.yaml)):
```md
Create the proposal document that establishes WHY this change is needed.
Sections:
- **Why**: 1-2 sentences on the problem or opportunity. What problem does this solve? Why now?
- **What Changes**: Bullet list of changes. Be specific about new capabilities, modifications, or removals. Mark breaking changes with **BREAKING**.
- **Capabilities**: Identify which specs will be created or modified:
- **New Capabilities**: List capabilities being introduced. Each becomes a new `specs/<capability-path>/spec.md`. Use kebab-case for path segments you introduce (e.g., `user-auth` or `identity/user-auth`) and follow the project's existing spec organization.
- **Modified Capabilities**: List existing capabilities whose REQUIREMENTS are changing. Only include if spec-level behavior changes (not just implementation details). Each needs a delta spec file. Use the exact existing path under `openspec/specs/`. Leave empty if no requirement changes.
- **Impact**: Affected code, APIs, dependencies, or systems.
IMPORTANT: The Capabilities section is critical. It creates the contract between
proposal and specs phases. Research existing specs before filling this in.
Each capability listed here will need a corresponding spec file.
Every change must either declare at least one capability (new or
modified) or explicitly opt out of specs: `openspec validate` rejects a
change with zero deltas unless the change's `.openspec.yaml` sets
`skip_specs: true`. Use `skip_specs: true` only when no spec-level
behavior changes (pure refactor, tooling, docs) - specs describe
behavior, so if behavior does not change, no spec should change either.
Do not invent a requirement just to satisfy validation.
Keep it concise (1-2 pages). Focus on the "why" not the "how" -
implementation details belong in design.md.
This is the foundation - specs, design, and tasks all build on this.
```
## Delta specs (spec.md)
Defines what behavior changes, with one delta spec per capability the proposal lists.
### Structure
The template the agent receives as the output format ([templates/spec.md](https://github.com/Fission-AI/OpenSpec/blob/main/schemas/spec-driven/templates/spec.md)):
```md
## Purpose
<!-- New capabilities only: one or two sentences (50+ characters) on what this capability is for. Delete this section for an existing capability. -->
## ADDED Requirements
### Requirement: <!-- requirement name -->
<!-- requirement text -->
#### Scenario: <!-- scenario name -->
- **WHEN** <!-- condition -->
- **THEN** <!-- expected outcome -->
```
### Instructions
The instruction sent to the agent when it drafts this artifact (from [schema.yaml](https://github.com/Fission-AI/OpenSpec/blob/main/schemas/spec-driven/schema.yaml)):
````md
Create specification files that define WHAT the system should do.
A spec is a behavior contract, not an implementation plan.
Good spec content:
- Observable behavior users or downstream systems rely on
- Inputs, outputs, and error conditions
- External constraints (security, privacy, reliability, compatibility)
- Scenarios that can be tested or explicitly validated
Avoid in specs:
- Internal class/function names
- Library or framework choices
- Step-by-step implementation details
- Detailed execution plans (those belong in design.md or tasks.md)
Quick test: if the implementation can change without changing externally
visible behavior, it likely does not belong in the spec.
Create one spec file per capability listed in the proposal's Capabilities section.
`<capability-path>` is the spec directory relative to `specs/` (for example,
`user-auth` or `identity/user-auth`). Preserve the full path:
- New capabilities: use the exact path from the proposal at `specs/<capability-path>/spec.md`. Any path segment newly introduced in the proposal must be kebab-case. Follow the project's existing organization; do not add a new domain level when the project uses a flat layout.
- Modified capabilities: use the exact existing path from `openspec/specs/<capability-path>/` when creating the delta at `specs/<capability-path>/spec.md`. Do not move or rename the capability.
There must be at least one spec file unless the change's `.openspec.yaml`
sets `skip_specs: true` (no spec-level behavior change) - `openspec validate`
rejects a zero-delta change without that marker. If the proposal lists no
capabilities and `skip_specs` is not set, revisit the proposal first.
Delta operations (use ## headers):
- **ADDED Requirements**: New capabilities
- **MODIFIED Requirements**: Changed behavior - MUST include full updated content
- **REMOVED Requirements**: Deprecated features - MUST include **Reason** and **Migration**
- **RENAMED Requirements**: Name changes only - use FROM:/TO: format
Format requirements:
- Each requirement: `### Requirement: <name>` followed by description
- Use SHALL/MUST for normative requirements (avoid should/may)
- Each scenario: `#### Scenario: <name>` with WHEN/THEN format
- **CRITICAL**: Scenarios MUST use exactly 4 hashtags (`####`). Using 3 hashtags or bullets will fail silently.
- Every requirement MUST have at least one scenario.
New capabilities only: start the delta spec with a `## Purpose` section -
one or two sentences (50+ characters, or `openspec validate --strict`
reports it as too brief) describing what the capability is for. Archive
copies it into the main spec it creates; without it the new main spec is
left with a `TBD ... Update Purpose after archive` placeholder to fill in
by hand. Do NOT add `## Purpose` to a delta for an existing capability -
that spec already has one and the delta's is ignored. To change an
existing capability's Purpose - including a leftover `TBD` placeholder -
edit `openspec/specs/<capability-path>/spec.md` directly.
MODIFIED requirements workflow:
1. Locate the existing requirement in openspec/specs/<capability-path>/spec.md
2. Copy the ENTIRE requirement block (from `### Requirement:` through all scenarios)
3. Paste under `## MODIFIED Requirements` and edit to reflect new behavior
4. Ensure header text matches exactly (whitespace-insensitive)
Common pitfall: Using MODIFIED with partial content loses detail at archive time.
If adding new concerns without changing existing behavior, use ADDED instead.
Example (a new capability, so it opens with `## Purpose`):
```
## Purpose
Lets users take their data out of the product in a portable format.
## ADDED Requirements
### Requirement: User can export data
The system SHALL allow users to export their data in CSV format.
#### Scenario: Successful export
- **WHEN** user clicks "Export" button
- **THEN** system downloads a CSV file with all user data
## REMOVED Requirements
### Requirement: Legacy export
**Reason**: Replaced by new export system
**Migration**: Use new export endpoint at /api/v2/export
```
Specs should be testable - each scenario is a potential test case.
````
## design.md
Explains how to implement the change. Drafted only when the change needs one.
### Structure
The template the agent receives as the output format ([templates/design.md](https://github.com/Fission-AI/OpenSpec/blob/main/schemas/spec-driven/templates/design.md)):
```md
## Context
<!-- Current state and constraints that shape the approach. See proposal.md for motivation - don't restate it -->
## Goals / Non-Goals
**Goals:**
<!-- What this design aims to achieve -->
**Non-Goals:**
<!-- What is explicitly out of scope -->
## Decisions
<!-- Key design decisions with rationale and alternatives considered -->
## Risks / Trade-offs
<!-- Known risks and trade-offs -->
```
### Instructions
The instruction sent to the agent when it drafts this artifact (from [schema.yaml](https://github.com/Fission-AI/OpenSpec/blob/main/schemas/spec-driven/schema.yaml)):
```md
Create the design document that explains HOW to implement the change.
When to include design.md (create only if any apply):
- Cross-cutting change (multiple services/modules) or new architectural pattern
- New external dependency or significant data model changes
- Security, performance, or migration complexity
- Ambiguity that benefits from technical decisions before coding
Sections:
- **Context**: Only the current state and constraints needed to explain the approach. Reference the proposal for motivation instead of restating it (e.g., "See proposal.md - Why").
- **Goals / Non-Goals**: What this design achieves and explicitly excludes. Don't restate the proposal's scope - add only design-level boundaries.
- **Decisions**: Key technical choices with rationale (why X over Y?). Include alternatives considered for each decision.
- **Risks / Trade-offs**: Known limitations, things that could go wrong. Format: [Risk] → Mitigation
- **Migration Plan**: Steps to deploy, rollback strategy (if applicable)
- **Open Questions**: Unknowns that can safely be answered later without
changing the specs, the approach, or the task breakdown. Omit if none.
Open questions are for genuinely deferrable unknowns, not decisions you
skipped. If a question would change the specs, the chosen approach, or
the task breakdown, resolve it now - ask the user instead of guessing.
Focus on architecture and approach, not line-by-line implementation.
The proposal covers why and what; design covers how. Reference the
proposal for motivation and, once written, the specs for requirements -
if a section would only restate them, point to them instead.
Good design docs explain the "why" behind technical decisions.
```
## tasks.md
Breaks the implementation into checkable tasks. [apply](#apply) tracks progress here.
### Structure
The template the agent receives as the output format ([templates/tasks.md](https://github.com/Fission-AI/OpenSpec/blob/main/schemas/spec-driven/templates/tasks.md)):
```md
## 1. <!-- Task Group Name -->
- [ ] 1.1 <!-- Task description -->
- [ ] 1.2 <!-- Task description -->
## 2. <!-- Task Group Name -->
- [ ] 2.1 <!-- Task description -->
- [ ] 2.2 <!-- Task description -->
```
### Instructions
The instruction sent to the agent when it drafts this artifact (from [schema.yaml](https://github.com/Fission-AI/OpenSpec/blob/main/schemas/spec-driven/schema.yaml)):
````md
Create the task list that breaks down the implementation work.
Before writing tasks, check design.md for Open Questions. If any of them
would change what gets built, resolve them with the user first - do not
bake an unstated assumption into the task list.
**IMPORTANT: Follow the template below exactly.** The apply phase parses
checkbox format to track progress. Tasks not using `- [ ]` won't be tracked.
Guidelines:
- Group related tasks under ## numbered headings
- Each task MUST be a checkbox: `- [ ] X.Y Task description`
- Tasks should be small enough to complete in one session
- Order tasks by dependency (what must be done first?)
Example:
```
## 1. Setup
- [ ] 1.1 Create new module structure
- [ ] 1.2 Add dependencies to package.json
## 2. Core Implementation
- [ ] 2.1 Implement data export function
- [ ] 2.2 Add CSV formatting utilities
```
Reference specs for what needs to be built, design for how to build it.
Each task should be verifiable - you know when it's done.
````
## Apply
The handoff from planning to implementation. Apply is the phase that works through `tasks.md`, not an artifact.
- **Starts**: once `tasks.md` exists and lists at least one task.
- **Tracks**: the checkboxes in `tasks.md`. Checking them off is the progress record.
- **Ends**: every checkbox checked. OpenSpec then suggests archiving the change.
### Settings
The apply settings (from [schema.yaml](https://github.com/Fission-AI/OpenSpec/blob/main/schemas/spec-driven/schema.yaml)):
```yaml
apply:
requires: [tasks]
tracks: tasks.md
# instruction: shown below
```
### Instructions
The instruction sent to the agent when implementation starts (from [schema.yaml](https://github.com/Fission-AI/OpenSpec/blob/main/schemas/spec-driven/schema.yaml)):
```md
Read context files, work through pending tasks, mark complete as you go.
Pause if you hit blockers or need clarification.
```
## schema.yaml
The complete [schema.yaml](https://github.com/Fission-AI/OpenSpec/blob/main/schemas/spec-driven/schema.yaml), with instruction bodies elided. Each is shown in full in its section above.
```yaml
name: spec-driven
version: 1
description: Default OpenSpec workflow - proposal → specs → design → tasks
artifacts:
- id: proposal
generates: proposal.md
description: Initial proposal document outlining the change
template: proposal.md
# instruction: shown in full under proposal.md above
requires: []
- id: specs
generates: "specs/**/*.md"
description: Detailed specifications for the change
template: spec.md
# instruction: shown in full under Delta specs above
requires:
- proposal
- id: design
generates: design.md
description: Technical design document with implementation details
template: design.md
# instruction: shown in full under design.md above
requires:
- proposal
- id: tasks
generates: tasks.md
description: Implementation checklist with trackable tasks
template: tasks.md
# instruction: shown in full under tasks.md above
requires:
- specs
- design
apply:
requires: [tasks]
tracks: tasks.md
# instruction: shown in full under Apply above
```
+176
View File
@@ -0,0 +1,176 @@
# Skills
> Every OpenSpec skill: arguments, what it creates, and what it responds with.
<!-- Drafted 2026-08-11 via one subagent per entry, each verifying every claim against
its workflow template in src/core/templates/workflows/; assembled and uniformity-passed
by the main session. Terminology: "change proposal", never bare "change" (user call,
2026-08-11). Shape (user-reviewed): intro bullets define Core/Optional, then ONE index
table (Skill / Job / Type) and a flat run of H2 entries matching
cli.md's shape - no group sections. Recipe per entry: one-line job sentence, then a
two-column key-value table (header row "Contract | Description", uniform across
entries) holding the pure input/output contract,
one row per fact: Arguments (what you pass; each cell self-contains its
optional/ambiguous behavior) / Creates (exact paths written; always states the code
boundary) / Response (what the agent reports back and where it stops). No judgment
rows: no when-to-use beyond the job sentence, no Not-for routing, no guide links
(guides link here, not the reverse). The ff job says "create a change proposal"
because its template unconditionally scaffolds a new one (redirects if the name
exists), contradicting the old "remaining artifacts" framing. Paths shown are the
default single-repo layout, stated without a caveat: reference pages state defaults,
and the store-moves-the-planning-home fact is multi-repo/stores.md's to teach (the
per-tool command spelling story likewise stays with setup.md and supported-tools.md;
user cut the NOTE carrying both, 2026-08-11). H2 entries double as the site's
right-rail TOC and the anchors guides deep-link. No frontmatter in source: sync-docs.mjs lifts H1 to title and the > line to
description (README pins the > line verbatim in its page index). Deliberately
excluded, each with an owner elsewhere: per-tool command spellings and syntax
(reference/supported-tools.md), example transcripts (quickstart and guides), tips and
when-to-use judgment (guides own it), troubleshooting (help/troubleshooting.md),
legacy /openspec:* commands (help/legacy/migration.md). Source: old docs/commands.md
maps here per sources.md; its unsupported claims (apply "runs tests", bulk-archive
name arguments, fixed tasks.md filename) were checked against templates and dropped.
Skill names from WORKFLOW_TO_SKILL_DIR (src/core/profile-sync-drift.ts) and the
templates in src/core/templates/workflows/; core set src/core/profiles.ts:14. "Optional"
is the docs' set label (was "Expanded"; renamed 2026-08-12: the product's only stored
profile values are core and custom, so "expanded" reads as a third profile). -->
The skills come in two sets:
- **Core**: installed by default, the main planning loop.
- **Optional**: installed only when you add them, via [Profiles](../customize/profiles.md).
| Skill | Job | Type |
|---|---|---|
| [openspec-explore](#openspec-explore) | Think through an idea before it becomes a change proposal | Core |
| [openspec-propose](#openspec-propose) | Create a change proposal with all its planning artifacts in one step | Core |
| [openspec-apply-change](#openspec-apply-change) | Implement a change proposal's tasks | Core |
| [openspec-update-change](#openspec-update-change) | Revise a change proposal's plan | Core |
| [openspec-sync-specs](#openspec-sync-specs) | Merge a change proposal's spec updates into `specs/` | Core |
| [openspec-archive-change](#openspec-archive-change) | Move a finished change proposal to the archive | Core |
| [openspec-new-change](#openspec-new-change) | Start a change proposal as an empty scaffold | Optional |
| [openspec-continue-change](#openspec-continue-change) | Create the next planning artifact, one at a time | Optional |
| [openspec-ff-change](#openspec-ff-change) | Create a change proposal with every artifact implementation needs, in one pass | Optional |
| [openspec-verify-change](#openspec-verify-change) | Check the implementation matches the plan | Optional |
| [openspec-bulk-archive-change](#openspec-bulk-archive-change) | Archive several change proposals at once | Optional |
| [openspec-onboard](#openspec-onboard) | Learn the workflow by doing one real change proposal end to end | Optional |
## openspec-explore
Think through an idea before it becomes a change proposal.
| Contract | Description |
|---|---|
| **Arguments** | A topic: an idea, a problem, a comparison, or the name of an existing change proposal to explore in context. With nothing given it enters explore mode. |
| **Creates** | Nothing by default. It reads and investigates only. On request it captures insights: a new change proposal under `openspec/changes/<name>/`, or updates to an existing one's proposal, design, specs, or tasks. Never code. |
| **Response** | An open conversation with no required output. When thinking crystallizes it summarizes the problem, approach, open questions, and next steps, and offers to capture them. You decide. Implementation never starts here. |
## openspec-propose
Create a change proposal and generate all its planning artifacts in one step.
| Contract | Description |
|---|---|
| **Arguments** | A kebab-case name (`add-dark-mode`) or a plain description. Asks if you give neither. |
| **Creates** | `openspec/changes/<name>/` with every artifact the schema defines, in dependency order (spec-driven: proposal, spec deltas, design, tasks). Never code. |
| **Response** | The created artifacts, ready for review, and the next step. Stops there; implementation waits for `openspec-apply-change`. |
## openspec-apply-change
Implement a change proposal's tasks, working through the list until done or blocked.
| Contract | Description |
|---|---|
| **Arguments** | A change proposal name (`add-auth`), optional. If the target is ambiguous it lists the active change proposals and asks you to pick. |
| **Creates** | Code: the minimal changes each task calls for, in your project files. In the change proposal it touches only the tasks file, checking off each finished task (`- [ ]` to `- [x]`). |
| **Response** | Progress per task, then an overall count (N/M tasks complete). All done: suggests `openspec-archive-change`. Blocked by missing artifacts: points to `openspec-continue-change`. Unclear tasks or errors: pauses and asks. |
## openspec-update-change
Revise a change proposal's existing planning artifacts and keep them coherent with each
other.
| Contract | Description |
|---|---|
| **Arguments** | A change proposal name, optional, plus the revision you want. With no revision stated it runs a coherence review: artifacts checked against each other for contradictions, gaps, and duplication. |
| **Creates** | Nothing new. Edits only artifact files that already exist. Missing artifacts are `openspec-continue-change`'s job. Never code. |
| **Response** | Shows each proposed revision and writes it only after you confirm, one artifact at a time. Ends with what was revised and the next step; implementation waits for `openspec-apply-change`. |
## openspec-sync-specs
Merge a change proposal's spec updates into `specs/` without archiving it.
| Contract | Description |
|---|---|
| **Arguments** | A change proposal name, optional. You can also name a subset of its delta specs, and only those sync. |
| **Creates** | Edits or creates `openspec/specs/<capability-path>/spec.md` for each delta spec, merging added, modified, removed, and renamed requirements into the main spec. Never code. |
| **Response** | A per-capability summary of requirements added, modified, removed, or renamed, after the updated specs validate. The change proposal stays active; archiving waits for `openspec-archive-change`. |
## openspec-archive-change
Move a finished change proposal to the archive.
| Contract | Description |
|---|---|
| **Arguments** | A change proposal name, optional. |
| **Creates** | Moves the change proposal folder to `openspec/changes/archive/YYYY-MM-DD-<name>/` (no date added if the name already starts with one). With your approval it first syncs outstanding delta specs via `openspec-sync-specs`. Never code. |
| **Response** | Warns and asks before archiving with incomplete artifacts or tasks, and asks whether to sync when delta specs exist. Ends with a summary: name, schema, archive location, spec sync status, and any warnings. |
## openspec-new-change
Start a change proposal as an empty scaffold.
| Contract | Description |
|---|---|
| **Arguments** | A kebab-case name (`add-user-auth`) or a plain description, plus a schema name only for a non-default workflow. Asks what you want to build if you give neither. |
| **Creates** | `openspec/changes/<name>/` as an empty scaffold: no artifacts yet, never code. |
| **Response** | The scaffold's name and location, the workflow's artifact sequence, status (0/N complete), and the first artifact's template. Drafting artifacts waits for `openspec-continue-change`. |
## openspec-continue-change
Create the next planning artifact in a change proposal, one at a time.
| Contract | Description |
|---|---|
| **Arguments** | A change proposal name, optional. If still ambiguous it asks you to pick from the most recently modified. |
| **Creates** | The single next ready artifact in the schema's sequence, written into the change proposal folder. One artifact per run, never code. |
| **Response** | The created artifact, progress (N of M complete), and which artifacts that unlocked. When planning is complete it says so; implementation moves to `openspec-apply-change`. |
## openspec-ff-change
Create a change proposal and every planning artifact implementation needs, in one pass.
| Contract | Description |
|---|---|
| **Arguments** | A kebab-case name or a plain description. Asks if you give neither. If the named change proposal already exists it suggests continuing it instead. |
| **Creates** | `openspec/changes/<name>/` and every planning artifact implementation requires, in dependency order (spec-driven: proposal, specs, design, tasks), leaving out only artifacts marked skipped or conditional. Never code. |
| **Response** | The change proposal's name and location, each artifact created, and any conditional artifact skipped and why. Stops there; implementation waits for `openspec-apply-change`. |
## openspec-verify-change
Check that the implementation matches the change proposal's artifacts.
| Contract | Description |
|---|---|
| **Arguments** | A change proposal name, optional. When ambiguous it asks, listing change proposals that have a tasks artifact. |
| **Creates** | Nothing. It reads the change proposal's artifacts and the codebase. Verification is report-only. |
| **Response** | A report: a scorecard for Completeness, Correctness, and Coherence, then CRITICAL, WARNING, and SUGGESTION issues with recommendations, and a final archive-readiness assessment. It changes nothing and does not archive. |
## openspec-bulk-archive-change
Archive several change proposals at once.
| Contract | Description |
|---|---|
| **Arguments** | None. It lists the active change proposals and asks you to select any number, with an option for all. If none are active it says so and stops. |
| **Creates** | `openspec/changes/archive/YYYY-MM-DD-<name>/` per archived change proposal (already-dated names keep their prefix). Each one's spec deltas sync first via `openspec-sync-specs`. Never code. |
| **Response** | A status table per change proposal and one confirmation for the whole batch, then a summary of archived, skipped, and failed, plus spec sync results. When two change proposals touch the same spec it checks the codebase and syncs implemented deltas oldest first. |
## openspec-onboard
Learn the workflow by doing one real change proposal end to end.
| Contract | Description |
|---|---|
| **Arguments** | None. It scans your codebase for small starter tasks and asks you to pick one or describe your own. |
| **Creates** | A real change proposal for the chosen task, one artifact at a time, then real code once you confirm implementation. Archives the change proposal at the end. |
| **Response** | A narrated walkthrough of the full cycle with pauses for your input: explore, create, build each artifact, implement, archive. Ends with a recap and a pointer to `openspec-propose`. Takes about 15 to 20 minutes. |
+118
View File
@@ -0,0 +1,118 @@
# Supported tools
> Which AI coding tools OpenSpec supports, and each one's command syntax.
Every tool in the matrix runs the same OpenSpec workflows. A skill and its command are
the same workflow instructions. The only difference is what you type. Which form init
installs is the delivery setting, covered in
[Set up your project](../start/setup.md#the-workflow-files-skills-and-commands).
## Support matrix
Invocations are shown for the apply workflow. Every workflow follows the same shape.
The id goes to `openspec init --tools <id>` to skip the picker ([CLI](cli.md)).
| Tool | `--tools` id | Skills | Skill invocation | Commands | Command invocation |
|---|---|---|---|---|---|
| Amazon Q Developer | `amazon-q` | `.amazonq/skills/` | `/openspec-apply-change` | `.amazonq/prompts/` | `@opsx-apply` |
| Antigravity | `antigravity` | `.agent/skills/` | `/openspec-apply-change` | `.agent/workflows/` | `/opsx-apply` |
| Auggie (Augment CLI) | `auggie` | `.augment/skills/` | `/openspec-apply-change` | `.augment/commands/` | `/opsx-apply` |
| Bob Shell | `bob` | `.bob/skills/` | `/openspec-apply-change` | `.bob/commands/` | `/opsx-apply` |
| Claude Code | `claude` | `.claude/skills/` | `/openspec-apply-change` | `.claude/commands/opsx/` | `/opsx:apply` |
| Cline | `cline` | `.cline/skills/` | `/openspec-apply-change` | `.clinerules/workflows/` | `/opsx-apply` |
| CodeArts | `codeartsagent` | `.codeartsdoer/skills/` | `/openspec-apply-change` | none | none |
| CodeBuddy Code (CLI) | `codebuddy` | `.codebuddy/skills/` | `/openspec-apply-change` | `.codebuddy/commands/opsx/` | `/opsx:apply` |
| Codex | `codex` | `.agents/skills/` | `$openspec-apply-change` | none | none |
| Continue | `continue` | `.continue/skills/` | `/openspec-apply-change` | `.continue/prompts/` | `/opsx-apply` |
| CoStrict | `costrict` | `.cospec/skills/` | `/openspec-apply-change` | `.cospec/openspec/commands/` | `/opsx-apply` |
| Crush | `crush` | `.crush/skills/` | `/openspec-apply-change` | `.crush/commands/opsx/` | `/opsx:apply` |
| Cursor | `cursor` | `.cursor/skills/` | `/openspec-apply-change` | `.cursor/commands/` | `/opsx-apply` |
| Devin Desktop (formerly Windsurf) | `devin` | `.devin/skills/` | `/openspec-apply-change` | `.devin/workflows/` | `/opsx-apply` |
| Factory Droid | `factory` | `.factory/skills/` | `/openspec-apply-change` | `.factory/commands/` | `/opsx-apply` |
| ForgeCode | `forgecode` | `.forge/skills/` | `/openspec-apply-change` | none | none |
| Gemini CLI | `gemini` | `.gemini/skills/` | `/openspec-apply-change` | `.gemini/commands/opsx/` | `/opsx:apply` |
| GitHub Copilot | `github-copilot` | `.github/skills/` | `/openspec-apply-change` | `.github/prompts/` | `/opsx-apply` |
| Hermes Agent | `hermes` | `.hermes/skills/` | `/openspec-apply-change` | none | none |
| iFlow | `iflow` | `.iflow/skills/` | `/openspec-apply-change` | `.iflow/commands/` | `/opsx-apply` |
| Junie | `junie` | `.junie/skills/` | `/openspec-apply-change` | `.junie/commands/` | `/opsx-apply` |
| Kilo Code | `kilocode` | `.kilocode/skills/` | `/openspec-apply-change` | `.kilocode/workflows/` | `/opsx-apply` |
| Kimi Code | `kimi` | `.kimi-code/skills/` | `/skill:openspec-apply-change` | none | none |
| Kiro | `kiro` | `.kiro/skills/` | `/openspec-apply-change` | `.kiro/prompts/` | `/opsx-apply` |
| Lingma | `lingma` | `.lingma/skills/` | `/openspec-apply-change` | `.lingma/commands/opsx/` | `/opsx:apply` |
| MiniMax Code | `minimax-code` | `~/.minimax/skills/` (global) | `/openspec-apply-change` | none | none |
| Mistral Vibe | `vibe` | `.vibe/skills/` | `/openspec-apply-change` | none | none |
| Oh My Pi | `oh-my-pi` | `.omp/skills/` | `/openspec-apply-change` | `.omp/commands/` | `/opsx-apply` |
| OpenCode | `opencode` | `.opencode/skills/` | `/openspec-apply-change` | `.opencode/commands/` | `/opsx-apply` |
| Pi | `pi` | `.pi/skills/` | `/openspec-apply-change` | `.pi/prompts/` | `/opsx-apply` |
| Qoder | `qoder` | `.qoder/skills/` | `/openspec-apply-change` | `.qoder/commands/opsx/` | `/opsx:apply` |
| Qwen Code | `qwen` | `.qwen/skills/` | `/openspec-apply-change` | `.qwen/commands/` | `/opsx-apply` |
| Trae | `trae` | `.trae/skills/` | `/openspec-apply-change` | `.trae/commands/` | `/opsx-apply` |
| ZCode | `zcode` | `.zcode/skills/` | `/openspec-apply-change` | `.zcode/commands/opsx/` | `/opsx:apply` |
| Zoo Code | `roocode` | `.roo/skills/` | `/openspec-apply-change` | `.roo/commands/` | `/opsx-apply` |
| Shared `.agents` skills | `agents` | `.agents/skills/` | `/openspec-apply-change` | none | none |
- **Skill invocation**: whether a tool registers skills as typed entries is the tool's
own behavior. The column shows the spelling OpenSpec uses in generated files and in
the hint init prints. Check your tool's docs if typing it does nothing.
- **Command file formats**: most tools take `.md` command files. Gemini CLI takes
`.toml`, Continue `.prompt`, Kiro and GitHub Copilot `.prompt.md`. The spelling you
type is the same either way.
## Per-tool notes
A tool not listed here behaves exactly as its row reads.
### Cline
Cline reads commands from `.clinerules/workflows/`, not from its `.cline/` folder.
Skills stay in `.cline/skills/`.
### Codex
- **Invocation**: type `$openspec-<skill>`. Codex does not recognize the
`/openspec-<skill>` form ([upstream issue](https://github.com/openai/codex/issues/11817)).
- **No command files**: Codex runs skills directly, so init skips commands even when
delivery includes them and prints `Commands skipped for: codex (uses skills)`.
- **Shared folder**: Codex skills land in `.agents/skills/`, the same tree the shared
`agents` target uses. Selecting both keeps one tree, and its handoffs spell both
`$openspec-*` and `/openspec-*`.
- **Legacy path**: skills installed under `.codex/skills/` by older versions are
migrated on the next `openspec update`.
### Devin Desktop (formerly Windsurf)
- **Two agents**: command files in `.devin/workflows/` work only in Devin Desktop.
Devin Local runs skills only, so generated skills reference `/openspec-<skill>`,
which works in both.
- **Rename**: `--tools windsurf` still resolves to `devin`. A project holding
OpenSpec files in the legacy `.windsurf/` folder is offered the move on the next
`openspec update`.
### GitHub Copilot
Prompt files register as slash commands in the Copilot IDE extensions (VS Code,
JetBrains, Visual Studio). Copilot CLI does not read `.github/prompts/`.
### Hermes Agent
Hermes loads skills only from `~/.hermes/skills/` by default. Add the project's
`.hermes/skills/` folder to `skills.external_dirs` in `~/.hermes/config.yaml`;
init prints this reminder after install.
### MiniMax Code
- **Global only**: skills go to `~/.minimax/skills/`. Nothing is written inside
the repo.
- **Safe across projects**: a commands-only delivery leaves the global skills in
place, so one project's setting cannot remove skills another project uses.
### Shared `.agents` skills
- **When it fits**: any tool that reads the shared `.agents/skills/` folder,
including tools with no row in the matrix.
- **Alongside other targets**: fine, since each target writes its own folder. Codex
shares this one; see the [Codex note](#codex).
- **What OpenSpec claims**: only the `openspec-*` folders and the
`.openspec-target` marker. Anything else under `.agents/` is left alone.
- **`AGENTS.md`**: not created or edited. The target is the `.agents/` folder, not
the file.
+45
View File
@@ -0,0 +1,45 @@
# Where every current page goes
The old-to-new mapping: the source material for each `docs-lab/` page while drafting,
and the redirect list at cutover. The target structure is the page index in
[README.md](README.md).
| Current (`docs/`) | Destination |
|---|---|
| README.md (index) | `start/overview.md`, rewritten as pitch and routing |
| getting-started.md | `start/quickstart.md` |
| installation.md | split: `start/installation.md` (machine-level: matrix, update, uninstall) · `start/setup.md` (project-level: init, what init writes, skills-vs-commands delivery, stores router) |
| how-commands-work.md | `start/quickstart.md` (inline labels) · `help/faq.md` · `help/troubleshooting.md` |
| existing-projects.md | `guides/existing-codebases.md` ("Existing codebases"); walkthrough half to `start/quickstart.md` |
| overview.md | `guides/concepts.md` |
| concepts.md | `guides/concepts.md` (core) · delta format to `reference/schemas/spec-driven/index.md` (Delta specs section) · embedded glossary table deleted |
| explore.md | `guides/explore.md` |
| workflows.md | `guides/apply.md` (execution patterns, continue/ff) · `reference/skills.md` |
| opsx.md | split four ways: config to `customize/project-config.md` · commands to `reference/skills.md` · philosophy to `guides/concepts.md` · architecture to `reference/architecture/` |
| reviewing-changes.md + writing-specs.md | `guides/review-the-plan.md` (merged) |
| editing-changes.md | `guides/change-course.md` |
| team-workflow.md | `guides/teams.md` |
| examples.md | parked: `guides/examples.md` skeleton kept off the index and sync config until real archived changes exist (see README TODOs) |
| customization.md | `customize/project-config.md` + `customize/schemas.md` + `customize/overview.md` (decision ladder) · schema.yaml fields to `reference/schemas/schema-yaml.md` |
| multi-language.md | `customize/project-config.md` §context, the "Another language" note |
| stores-beta/user-guide.md | `multi-repo/stores.md` · worksets section to `multi-repo/worksets.md` |
| commands.md | `reference/skills.md` (legacy `/openspec:*` section removed) |
| cli.md | `reference/cli.md` (minus install, which moves to `start/installation.md`) |
| supported-tools.md | `reference/supported-tools.md` |
| glossary.md | `reference/glossary.md` |
| faq.md | `help/faq.md` (unpublished-model claim deleted; update/uninstall to `start/installation.md`) |
| troubleshooting.md | `help/troubleshooting.md`, canonical home for all 5 copies, plus Getting help |
| migration-guide.md | `help/legacy/migration.md` (demoted) |
| agent-contract.md | **off-site**, to repo-side contributor docs |
New pages with no single current source: `customize/overview.md`, `customize/profiles.md`
(today: scattered two-line fragments across 12 pages), and the
`reference/schemas/` and `reference/configuration/` sections (which replaced the
planned `reference/file-formats.md`).
## Cutover
Point `website/docs.sync.config.mjs` here, add old-to-new redirects in
`website/public/_redirects`, and verify `llms.txt` / `llms-full.txt` /
per-page markdown routes. `docs/` stays in place, untouched. The site just
stops reading it.
+149
View File
@@ -0,0 +1,149 @@
# Installation
> Install the `openspec` CLI on your machine, update it, and uninstall it.
## Prerequisites
OpenSpec is a Node.js CLI. You need version 20.19.0 or newer.
In your terminal:
```bash
node --version
```
If that prints `v20.19.0` or higher, you're set. If not, install a newer Node from [nodejs.org](https://nodejs.org) or through your version manager (nvm, fnm, asdf, volta).
The workflow itself runs inside an AI coding tool: Claude Code, Cursor, or any other tool on the [supported list](../reference/supported-tools.md).
## Install with your AI assistant
Paste this into your AI chat:
```text
Fetch https://raw.githubusercontent.com/Fission-AI/OpenSpec/main/install.md and follow it.
```
Or, in your terminal, pipe it into a CLI agent (Claude Code shown):
```bash
curl -fsSL https://raw.githubusercontent.com/Fission-AI/OpenSpec/main/install.md | claude
```
That fetches [install.md at the repo root](https://github.com/Fission-AI/OpenSpec/blob/main/install.md), a prompt written for any agent that can run shell commands (a few IDE integrations can't). Expect your assistant to:
1. Check your Node version, and stop if it's older than 20.19.0.
2. Skip the install if the CLI is already on your machine. Otherwise, show you the install command and wait for your confirmation before running it.
3. Verify `openspec` is on your PATH.
4. Name the folder it thinks you mean, suggest the AI tool you're already talking to, and ask which others you use, then run `openspec init` there (the [project setup](setup.md) step).
5. Report what init created and the exact spelling to invoke OpenSpec in your tool.
It stops before anything privileged and never edits your shell startup files. The [manual methods below](#install-methods) are the source of truth, and the prompt runs them for you.
This install method is new and can have varying results depending on model used. Only use if you're comfortable correcting AI mistakes. Otherwise we recommend following the standard method below.
## Install methods
Install the CLI globally; [setting up your project](setup.md) comes after.
In your terminal:
```npm
npm install -g @fission-ai/openspec@latest
```
### Yarn
`yarn global add` is Yarn Classic (1.x) only. Modern Yarn removed global installs, so use npm, pnpm, or bun instead. A global CLI doesn't have to share your project's package manager.
### Bun
Bun installs OpenSpec but doesn't run it, so you still need Node on your machine (the [prerequisite](#prerequisites) above). Without it, every command fails with `env: node: No such file or directory`. Bun treats [every Node CLI](https://bun.com/docs/pm/bunx#shebangs) this way.
### Deno
Deno installs the CLI from npm and needs explicit permission flags. In your terminal:
```bash
deno install --global \
--allow-read --allow-write --allow-env --allow-sys=cpus,homedir --allow-net=edge.openspec.dev \
npm:@fission-ai/openspec@latest
```
Some commands launch another program: [`openspec config edit`](../reference/cli.md) opens your editor. Deno interrupts those with a permission prompt on every run. To stop it asking, add a scoped `--allow-run=<program>` to the install command.
> [!NOTE]
> If Deno can't resolve `@latest`, pin a version range instead: `npm:@fission-ai/openspec@^1.7.0`.
### Nix
The OpenSpec repo ships a Nix flake. Install it into your profile. In your terminal:
```bash
nix profile install github:Fission-AI/OpenSpec
```
Or run a one-off command first, without installing:
```bash
nix run github:Fission-AI/OpenSpec -- --version
```
That leaves nothing on your PATH, so there's no install to check afterward.
To put OpenSpec in a project dev shell instead, add the flake as an input and use its default package; [flake.nix](https://github.com/Fission-AI/OpenSpec/blob/main/flake.nix) lists the outputs.
### Check it worked
Whichever method you used, in your terminal:
```bash
openspec --version
```
If that prints a version number, the CLI is on your PATH. It installs once per machine.
Next, [set up your project](setup.md). If your assistant already ran init, that page shows what it wrote and how to adjust it.
## Updating
In your terminal, in each project where you ran init:
```bash
openspec update
```
When a newer CLI is out, [`openspec update`](../reference/cli.md#openspec-update) says so and can install it for you; that upgrade is once per machine. Every run refreshes the project's generated skills and commands, which never update on their own. A current project prints `✓ All 2 tool(s) up to date (v1.7.0)`.
> [!WARNING]
> On Deno, re-run the [Deno install](#deno) with `-f`; it won't overwrite the installed command without it. On Nix, use `nix profile upgrade openspec`.
> [!NOTE]
> A global npm install belongs to one Node installation. Switch Node versions with nvm and the `openspec` command doesn't come along, so install it again under the new version.
## Uninstalling
To uninstall OpenSpec, run through the steps below; none of them touch your source code. You can also point your agent at this section and let it handle the removal.
**1. Remove [shell completions](../reference/cli.md#openspec-completion)**, if you set them up, while the CLI can still do it. In your terminal:
```bash
openspec completion uninstall
```
**2. Remove the package.** In your terminal:
```npm
npm uninstall -g @fission-ai/openspec
```
On Deno: `deno uninstall --global openspec`. On Nix: `nix profile remove openspec`. Your shell should no longer find `openspec`.
**3. Delete what's left, or keep it.**
- Generated agent files: `openspec-*` skills and `opsx` commands under directories like `.claude/` or `.agents/`, per project. [Supported tools](../reference/supported-tools.md) lists each tool's paths; MiniMax Code keeps skills in `~/.minimax/skills`.
- Leftovers from older versions: marker blocks in `CLAUDE.md` or `AGENTS.md` (delete the block, keep the file) and `opsx-*.md` prompts in `~/.codex/prompts`.
- The `openspec/` folder: pause first. `specs/` and `changes/archive/` are your record of the system, plain Markdown that reads fine without OpenSpec.
- Per-machine state: settings and the telemetry id in `~/.config/openspec/`; schema overrides and store registrations in `~/.local/share/openspec/` (Windows: `%APPDATA%\openspec`, `%LOCALAPPDATA%\openspec`). Registrations are pointers; the store repos they point to are untouched.
+14
View File
@@ -0,0 +1,14 @@
# Overview
TODO: this page is being rewritten from scratch.
<!-- Emptied 2026-08-21. The previous skeleton (section headings, narrative beats, and
the diagram-options gallery) was cleared so the page starts clean. The old goal line
("OpenSpec gives you and your coding agent a shared, reviewable plan before code is
written") was dropped as too weak a pitch: the rewrite should sell keeping larger
features on track and aligned (teams, git-native artifacts, intended behavior matching
implemented behavior, the control-loop framing). The brief is in docs-lab/Notes.md
under "Start > Overview". The diagram candidates (docs-lab/diagrams/) were deleted
with the gallery; recover them from git history if the rewrite wants a starting point.
README rules that still bind the rewrite: the loop appears here as pitch only, copy and
no explanation; the quickstart is its one teacher. -->
+167
View File
@@ -0,0 +1,167 @@
# Quickstart
> Your first change on your existing repo, from idea to archived.
Before you start, you need the CLI on your machine ([Installation](installation.md)) and OpenSpec initialized in your project ([Set up your project](setup.md)).
## The loop at a glance
Every change moves through the same five steps: you think the idea through with your agent, it drafts a plan, you correct the plan before any code exists, the agent builds from it, and archiving updates your specs with what shipped.
```mermaid
flowchart LR
explore["1 · Explore<br/>think it through together"] --> propose["2 · Propose<br/>agent drafts the plan"]
propose --> review["3 · Review<br/>you correct the plan"]
review --> apply["4 · Apply<br/>agent builds, task by task"]
apply --> archive["5 · Archive<br/>specs absorb the change"]
archive -. "next change" .-> explore
```
Every prompt below goes in your AI chat, the same place you ask for code. Each invokes an OpenSpec skill by name, the same spelling in every tool. A plain ask works too ("propose a change to add rate limiting"). Some tools add shorter command aliases (`/opsx:propose` in Claude Code, [other tools vary](../reference/supported-tools.md)).
## Step 1: Explore
Think the idea through with your agent before you ask for a plan. In your AI chat:
```text
/openspec-explore how rate limiting should work in this app
```
Explore is a thinking mode. The agent investigates your codebase, asks the questions that matter, sketches options, and challenges assumptions. It writes no code and no files. The output is a sharper idea.
Stay here as long as the problem needs. When the shape feels right, hand it off:
```text
/openspec-propose
```
That line starts propose for you, carrying everything you settled. Skip the first prompt in step 2.
## Step 2: Propose
Propose turns the idea into a reviewable plan. Coming from explore, it's already running. Starting cold, when the change is clear in your head, ask directly. In your AI chat:
```text
/openspec-propose add rate limiting
```
The agent asks what it needs to, then writes a change folder:
```
openspec/changes/add-rate-limiting/
├── proposal.md why, and what changes
├── specs/ what "done" means, as testable requirements
├── design.md technical decisions (only when the change needs one)
└── tasks.md the implementation checklist
```
No code yet. Propose stops at the plan.
## Step 3: Review and correct the plan
Fix the plan while it's still words and nothing is built yet. Read in this order:
- **`proposal.md`**: is this the right problem, at the right size?
- **`specs/`**: the highest-value read. Would you accept these requirements as done?
- **`tasks.md`**: do the tasks cover the specs, and nothing more?
To fix something, either works:
- Edit the file yourself. The artifacts are plain markdown, and the files are the plan.
- Tell your agent what's wrong ("the spec is missing the unauthenticated case"). It revises the artifacts.
## Step 4: Apply
Apply turns the plan into code. Start a fresh chat session, since implementation goes better on a clean context window. In your AI chat:
```text
/openspec-apply-change add-rate-limiting
```
The agent reads the change folder, then works through `tasks.md`, checking off each task as it lands.
- **Interrupted, or out of context?** Open a new session and ask it to apply again. It resumes at the first unchecked task.
- **Plan turned out wrong?** Fix the artifacts (either way from step 3), then continue applying.
- **Progress** lives in the `tasks.md` checkboxes. There is no hidden state.
## Step 5: Archive
Archiving does two things: it updates your main specs with the change's requirements, and it moves the change folder into the archive folder (in `/openspec/changes/archive/*`).
When every box in `tasks.md` is checked, in your AI chat:
```text
/openspec-archive-change add-rate-limiting
```
Step through what archiving does:
```file-steps
## The finished change
> Implementation is done. The delta spec (what this change adds) still sits inside the change folder; specs/ doesn't know about rate limiting yet.
openspec/
├── specs/ (no rate-limiting spec yet)
└── changes/
└── add-rate-limiting/
├── proposal.md
├── tasks.md every box checked
└── specs/
└── rate-limiting/
└── spec.md the delta: ADDED requirements
## Requirements land in specs/
> Each requirement in the delta lands in the main spec: added ones append, modified ones replace their old version. A new capability gets a new spec file.
openspec/
├── specs/
+ │ └── rate-limiting/
+ │ └── spec.md gains "Requirement: Rate limiting"
└── changes/
└── add-rate-limiting/
└── specs/
└── rate-limiting/
└── spec.md the delta, source of the merge
## The folder moves to archive/
> The whole change folder, delta included, moves into the archive under a date prefix. Nothing is deleted.
openspec/
├── specs/
│ └── rate-limiting/
│ └── spec.md
└── changes/
- └── add-rate-limiting/
+ └── archive/
+ └── 2026-08-08-add-rate-limiting/
+ ├── proposal.md
+ ├── tasks.md
+ └── specs/rate-limiting/spec.md
## Specs describe the system as built
> changes/ is clear for the next change. specs/ is the source of truth for what the system does; archive/ is the history of how it got there.
openspec/
├── specs/
│ └── rate-limiting/
│ └── spec.md the spec as built
└── changes/
└── archive/
└── 2026-08-08-add-rate-limiting/
```
Git is a separate concern. Commit the change folder with the code, and nothing else about your workflow changes. When to archive relative to a PR is a team convention; the [Teams](../guides/teams.md) guide has the tradeoff.
## Going further
- [Concepts](../guides/concepts.md): what the two artifacts are, and how a delta describes a change.
- [Explore](../guides/explore.md): getting more out of explore mode.
- [Apply](../guides/apply.md): pacing, context windows, resuming long changes.
- [Review the plan](../guides/review-the-plan.md): what to look for in specs before you build.
- [Profiles](../customize/profiles.md): optional workflows beyond the core set (verify before archive, incremental planning).
## Advanced guides
<!-- Planned pages, not yet written or in the README page map. Listed here so the quickstart routes to them once they exist. -->
Not written yet; guides we plan to add:
- **Prototype first**: spike the code before any spec, then backfill the proposal from what the prototype taught you.
- **Building iteratively**: a sequence of small changes instead of one big proposal.
- **Revising an implemented change**: the plan needs to move again after apply, but the change hasn't merged or archived yet.
+115
View File
@@ -0,0 +1,115 @@
# Set up your project
> Add OpenSpec to a project: run init, see what it wrote, and adjust it.
## Pick where OpenSpec lives
- **In your repo (the default)**: specs and changes sit next to the code they describe and are versioned with it. The rest of this page follows this path.
- **In a store**: a separate planning repo shared by the repos that use it, for multi-repo setups or keeping planning out of the repo entirely. [Stores (beta)](../multi-repo/stores.md) covers when that's worth it and how to set one up.
## Initialize your project
With the CLI installed ([Installation](installation.md)), run init at the root of your project. In your terminal:
```bash
cd <your-project>
openspec init
```
Init asks which AI tools you use, writes the workflow files for the ones you pick, and reports what you got:
```
OpenSpec Setup Complete
Created: Claude Code
6 skills and 6 commands in .claude/
Config: openspec/config.yaml (schema: spec-driven)
```
Restart your IDE for the new commands to take effect.
Re-running init is safe:
- Tools you already set up print `Refreshed` instead of `Created`.
- Running init again with a new tool selected adds that tool.
- The `--tools` flag skips the picker ([CLI reference](../reference/cli.md)).
## What init installs
Running init creates two things in your project:
- An `openspec/` folder at the repo root
- Workflow files (skills and commands) added to your AI tool's folder (`.agents/`, `.claude/`, etc.)
Commit all of it like the rest of your source ([FAQ](../help/faq.md) covers why). Init changes nothing else in your repo (if it finds leftovers from an older OpenSpec version, it asks before cleaning them up).
### The `openspec/` folder
Every OpenSpec artifact lives here, at the root of your project. Here's what that looks like:
```
openspec/
├── config.yaml project settings and context for the AI
├── specs/ your specs (empty for now)
└── changes/ in-motion changes (empty for now)
└── archive/ completed changes move here
```
[Concepts](../guides/concepts.md) explains both artifacts; [Project config](../customize/project-config.md) covers `config.yaml`.
### The workflow files (skills and commands)
These are the OpenSpec workflows, the actions you'll use as you work. Here they are as installed skills, in the shared `.agents/` folder most tools use:
```
.agents/skills/
├── openspec-explore/ think through an idea first
├── openspec-propose/ propose a change
├── openspec-apply-change/ implement a change's tasks
├── openspec-update-change/ revise a change's plan
├── openspec-sync-specs/ sync a change's spec updates into specs/
├── openspec-archive-change/ move a finished change to the archive
├── openspec-verify-change/ check the implementation matches the plan (not included by default)
└── openspec-bulk-archive-change/ archive several changes at once (not included by default)
```
This is the default set plus two optional workflows. [Profiles](../customize/profiles.md) lists all twelve.
By default each workflow installs in two forms:
- **Skill** (`openspec-apply-change`): instructions your agent picks up on its own when you ask for the work.
- **Command** (`/opsx:apply` in Claude Code): a typed entry point for the same workflow, under a shorter name.
The two are functionally identical. A workflow's skill and its command carry the same instructions.
Why two: commands came first, and every tool spells them its own way. Skills are the newer standard shared across tools, but not every tool can invoke a skill directly, so commands stay as those tools' entry point.
Some tools install in skill form only. Where the tool runs skills directly, init skips commands and says so (`Commands skipped for: codex (uses skills)`).
We prefer skills and expect to retire commands eventually.
#### Change what gets installed
The interactive picker changes the delivery form and the workflow set ([Profiles](../customize/profiles.md)). In your terminal:
```bash
openspec config profile
```
Here's switching to skills only:
```
Current profile settings
Delivery: both
? What do you want to configure? Delivery only
? Delivery mode (how workflows are installed): Skills only
Config changes:
delivery: both -> skills
? Apply changes to this project now? (Y/n) y
```
Answering yes applies it to the current project on the spot. Other projects pick it up on their next `openspec update`. The setting is global, per machine.
Setup is done. The [Quickstart](quickstart.md) takes your first change from here.
+70
View File
@@ -0,0 +1,70 @@
# openspec
> OpenSpec gives you and your coding agent a shared, reviewable plan before code is written. This file installs its CLI and sets up your project.
I want you to install OpenSpec and set up my project. Follow the steps below in order, and stop where a step tells you to stop.
OBJECTIVE: Install the OpenSpec CLI globally and initialize the user's project.
DONE WHEN:
- `openspec --version` prints a version number
- the project the user confirmed has an `openspec/` folder created or refreshed by `openspec init`
- the user has been told the exact spelling to invoke OpenSpec in their AI coding tool
TODO:
- [ ] Check the Node.js runtime
- [ ] Install the CLI globally, with the user's confirmation
- [ ] Verify `openspec` is on PATH
- [ ] Confirm which folder to set up, then run `openspec init --tools <ids>` there
- [ ] Report what init actually created and how to invoke it
## 1. Runtime
Run `node --version`. OpenSpec needs Node.js 20.19.0 or higher. If Node is missing or older, say so and stop, but leave the user with their next move: point them at https://nodejs.org, or if they use a version manager like nvm, name the command that would switch to a newer version. Don't install Node, switch versions, or reconfigure the user's version manager yourself.
## 2. Install
Run `openspec --version` first. If it already prints a version, the CLI is installed: say so, offer to update it with the install command below, and if the user declines skip ahead to step 4.
Use whichever package manager is already on PATH, preferring npm:
```bash
npm install -g @fission-ai/openspec@latest
pnpm add -g @fission-ai/openspec@latest
bun add --global @fission-ai/openspec@latest
yarn global add @fission-ai/openspec@latest # Yarn 1.x only
```
Don't pick based on this project's lockfile: a global install has nothing to do with how this repo's own dependencies are installed. If none of those four is available, stop and say so; don't improvise an install. (On Nix, point the user at the Nix section of https://openspec.dev/docs/installation instead.)
Show the exact command and get confirmation before running it; this installs software outside the project, and the user may want a different package manager to own it.
Stop and ask again if the install needs sudo or admin rights, fails with a permissions error, or reports that its global bin directory is missing or unconfigured. Never edit shell startup files (.bashrc, .zshrc, .profile, fish, PowerShell profile), and never run a setup command that edits them; show the change and let the user make it.
## 3. PATH
Run `openspec --version`. If the command isn't found, it may only be missing from this shell: say where the package manager installed it and how to add that directory to PATH for the user's shell and OS, then stop until they confirm. If it prints an older version than the one the install just reported, an earlier copy is shadowing it on PATH; report both versions instead of continuing. If the user uses a version manager, say so rather than editing PATH around it: with nvm or fnm the CLI is tied to the Node version that was active at install time, and with asdf or volta a shim may need regenerating.
## 4. Initialize
Work out where `openspec/` should go, and lead with your best guess rather than an open question: the root of the project the user is working in is almost always right. Name the folder you picked and let them correct it, for example "you're in ~/code/acme-api, so I'll set OpenSpec up there". Prefer the version control root over the current directory, and in a monorepo say which package you chose and why. To target a folder other than the current one, pass it: `openspec init <path> --tools <ids>`.
init creates `openspec/` wherever you point it and won't warn you when that's wrong. If the folder is a home directory, a temp directory, or holds no project at all, stop and ask where the project is.
Then work out which AI coding tools the user works with, and again lead with an inference instead of an open question: you are probably running inside one of them, so name it and ask what else they use, suggesting a few common options (Claude Code, Cursor, Copilot, Codex). Say what the answer changes: each tool named gets its own skill and command files in the project, and re-running init later adds more, so a short list now costs nothing. Map each tool to an id from `openspec init --help` (Copilot is `github-copilot`, Zoo Code is `roocode`). `--tools` takes a comma-separated list, so name all of them.
`openspec init --tools <ids>` deletes leftovers from older OpenSpec versions automatically, without asking, including `opsx-*.md` prompt files in the home directory (Codex keeps them in `~/.codex/prompts`). Before running it, look for those: `.../commands/openspec/` folders, OpenSpec marker blocks in files like CLAUDE.md or AGENTS.md, and home-directory `opsx-*.md` prompts. List whatever you find, say plainly that init will delete these, and wait for a go-ahead; if you find nothing, say so and carry on without asking. An existing `openspec/` folder is not a problem: init refreshes it and leaves specs and changes alone.
Then run: `openspec init --tools <ids>`, adding the path when it isn't the current folder.
## 5. Report
Don't assume what should exist; relay what init actually printed: how many skills and/or commands it created and where, the config file line, any "Setup required" note, and what to restart or reload. Some tools are skills-only and correctly create zero command files, so missing commands is not a failure on its own. If init said nothing was generated, relay the fix it suggested instead of retrying.
Finish by telling the user how to invoke OpenSpec in their tool, taking the exact spelling from the files init created rather than from its summary line: the punctuation differs per tool (`/opsx:propose` in some, `/opsx-propose` in others, `@opsx-propose` in Amazon Q), and tools that get skills instead of commands are invoked by skill name (`/openspec-propose`, or `$openspec-propose` in Codex, or `/skill:openspec-propose` in Kimi Code).
EXECUTE NOW: Complete the TODO list above to achieve: the OpenSpec CLI installed, this project initialized, and the user told exactly how to invoke OpenSpec in their AI coding tool.
More context for agents: https://openspec.dev/llms.txt
-6
View File
@@ -1,6 +0,0 @@
import { HomeLayout } from 'fumadocs-ui/layouts/home';
import { baseOptions } from '@/lib/layout.shared';
export default function Layout({ children }: LayoutProps<'/'>) {
return <HomeLayout {...baseOptions()}>{children}</HomeLayout>;
}
-651
View File
@@ -1,651 +0,0 @@
import Link from 'next/link';
import {
ArrowRight,
Boxes,
Check,
Clock,
Compass,
FileText,
GitBranch,
Hammer,
Archive,
Layers,
ListChecks,
Share2,
Sparkles,
} from 'lucide-react';
import { docsRoute, links } from '@/lib/shared';
export default function HomePage() {
return (
<main className="flex flex-col">
<Hero />
<Philosophy />
<ToolStrip />
<TwoFolders />
<Anatomy />
<FiveIdeas />
<TheLoop />
<Teams />
<Why />
<Comparison />
<FinalCta />
</main>
);
}
function Hero() {
return (
<section className="relative overflow-hidden border-b border-fd-border">
<div
className="absolute inset-0 -z-10"
style={{
background:
'radial-gradient(ellipse at top, color-mix(in oklab, var(--color-fd-primary) 9%, transparent), transparent 60%)',
}}
/>
<div className="mx-auto flex max-w-5xl flex-col items-center px-4 py-20 text-center sm:py-28">
<span className="mb-5 inline-flex items-center gap-2 rounded-full border border-fd-border bg-fd-card px-3 py-1 text-xs font-medium text-fd-muted-foreground">
<Sparkles className="size-3.5 text-fd-primary" />
The lightweight spec layer for AI coding
</span>
<h1 className="max-w-3xl text-balance text-4xl font-bold tracking-tight sm:text-6xl">
Agree first.
<br />
Then build confidently.
</h1>
<p className="mt-6 max-w-2xl text-balance text-lg text-fd-muted-foreground">
OpenSpec is a tiny agreement layer between you and your AI. You write
down what a change should do, the AI drafts the details, you both look
at the same plan, and <em>only then</em> does code get written. No more
discovering halfway through that it built the wrong thing.
</p>
<div className="mt-9 flex flex-col gap-3 sm:flex-row">
<Link
href={`${docsRoute}/getting-started`}
className="inline-flex items-center justify-center gap-2 rounded-lg bg-fd-primary px-5 py-2.5 text-sm font-semibold text-fd-primary-foreground transition-opacity hover:opacity-90"
>
Get started <ArrowRight className="size-4" />
</Link>
<Link
href={links.github}
className="inline-flex items-center justify-center gap-2 rounded-lg border border-fd-border bg-fd-card px-5 py-2.5 text-sm font-semibold transition-colors hover:bg-fd-accent"
>
<GitBranch className="size-4" /> Star on GitHub
</Link>
</div>
<Terminal />
</div>
</section>
);
}
function Terminal() {
return (
<div className="mt-14 w-full max-w-2xl text-left">
<div className="overflow-hidden rounded-xl border border-fd-border bg-fd-card shadow-sm">
<div className="flex items-center gap-1.5 border-b border-fd-border px-4 py-3">
<span className="size-3 rounded-full bg-red-400/80" />
<span className="size-3 rounded-full bg-yellow-400/80" />
<span className="size-3 rounded-full bg-green-400/80" />
<span className="ml-3 text-xs text-fd-muted-foreground">
your-project — AI chat
</span>
</div>
<pre className="overflow-x-auto p-4 text-sm leading-relaxed">
<code>
<span className="text-fd-primary">/opsx:propose</span> add-dark-mode
{'\n'}
<span className="text-fd-muted-foreground">
{' '}✓ proposal.md — why we are doing this, what changes{'\n'}
{' '}✓ specs/ — requirements and scenarios{'\n'}
{' '}✓ design.md — technical approach{'\n'}
{' '}✓ tasks.md — implementation checklist{'\n'}
</span>
{'\n'}
<span className="text-fd-primary">/opsx:apply</span>
{'\n'}
<span className="text-fd-muted-foreground">
{' '}✓ working through tasks, checking each one off…{'\n'}
</span>
{'\n'}
<span className="text-fd-primary">/opsx:archive</span>
{'\n'}
<span className="text-fd-muted-foreground">
{' '}✓ specs updated · change filed away · ready for the next one
</span>
</code>
</pre>
</div>
</div>
);
}
const PHILOSOPHY = [
['fluid', 'not rigid'],
['iterative', 'not waterfall'],
['easy', 'not complex'],
['brownfield', 'not just greenfield'],
];
function Philosophy() {
return (
<section className="border-b border-fd-border bg-fd-card/30">
<div className="mx-auto grid max-w-5xl grid-cols-2 gap-px px-4 py-3 sm:grid-cols-4">
{PHILOSOPHY.map(([a, b]) => (
<div key={a} className="px-4 py-4 text-center">
<div className="text-lg font-semibold tracking-tight">{a}</div>
<div className="text-sm text-fd-muted-foreground">{b}</div>
</div>
))}
</div>
</section>
);
}
function TwoFolders() {
return (
<section className="mx-auto max-w-5xl px-4 py-20">
<div className="mx-auto max-w-2xl text-center">
<h2 className="text-3xl font-bold tracking-tight">
The whole idea, in two folders
</h2>
<p className="mt-4 text-fd-muted-foreground">
OpenSpec lives in one <code className="text-fd-primary">openspec/</code>{' '}
directory in your repo. Two folders inside it carry the entire mental
model.
</p>
</div>
<div className="mt-12 grid gap-6 md:grid-cols-2">
<div className="rounded-xl border border-fd-border bg-fd-card p-6">
<div className="mb-3 inline-flex size-10 items-center justify-center rounded-lg bg-fd-primary/10 text-fd-primary">
<FileText className="size-5" />
</div>
<h3 className="text-lg font-semibold">
<code>specs/</code> — what is true
</h3>
<p className="mt-2 text-sm text-fd-muted-foreground">
The source of truth. Plain-language requirements and scenarios that
describe how your system behaves <em>right now</em>, organized by
domain. This is the agreed-upon answer to &ldquo;what does this
software do?&rdquo;
</p>
</div>
<div className="rounded-xl border border-fd-border bg-fd-card p-6">
<div className="mb-3 inline-flex size-10 items-center justify-center rounded-lg bg-fd-primary/10 text-fd-primary">
<GitBranch className="size-5" />
</div>
<h3 className="text-lg font-semibold">
<code>changes/</code> — what you are proposing
</h3>
<p className="mt-2 text-sm text-fd-muted-foreground">
One folder per change. Each holds a proposal, a design, a task list,
and a small spec delta. When the work is done, you archive it and the
delta folds into the truth. The cycle closes.
</p>
</div>
</div>
</section>
);
}
const IDEAS = [
{
icon: FileText,
title: 'Specs are the truth',
body: 'Requirements and scenarios describe how your system behaves today. One agreed-upon answer, in your repo, readable by humans and AI alike.',
},
{
icon: GitBranch,
title: 'A change is one unit of work',
body: 'One feature, one folder. Proposal, design, tasks, and spec edits all live together. Easy to review, easy to reason about.',
},
{
icon: Layers,
title: 'Deltas, not rewrites',
body: 'You describe what is changing — ADDED, MODIFIED, REMOVED — not the whole world. That is the trick that makes OpenSpec great at brownfield code.',
},
{
icon: Compass,
title: 'Enablers, not gates',
body: 'Artifacts build on each other in a natural order, but nothing locks. Learn something mid-build? Edit the plan and keep going.',
},
];
function FiveIdeas() {
return (
<section className="border-y border-fd-border bg-fd-card/30">
<div className="mx-auto max-w-5xl px-4 py-20">
<div className="mx-auto max-w-2xl text-center">
<h2 className="text-3xl font-bold tracking-tight">
Learn four ideas, and the rest is detail
</h2>
<p className="mt-4 text-fd-muted-foreground">
Everything in OpenSpec is built from a handful of simple concepts.
</p>
</div>
<div className="mt-12 grid gap-6 sm:grid-cols-2">
{IDEAS.map(({ icon: Icon, title, body }) => (
<div
key={title}
className="rounded-xl border border-fd-border bg-fd-card p-6"
>
<Icon className="size-5 text-fd-primary" />
<h3 className="mt-3 font-semibold">{title}</h3>
<p className="mt-2 text-sm text-fd-muted-foreground">{body}</p>
</div>
))}
</div>
</div>
</section>
);
}
const STEPS = [
{
icon: Compass,
cmd: '/opsx:explore',
label: 'optional',
body: 'A no-stakes thinking partner. It reads your code, weighs options, and turns a fuzzy idea into a concrete plan.',
},
{
icon: FileText,
cmd: '/opsx:propose',
body: 'The AI drafts the proposal, spec deltas, design, and a task list. You read it and adjust before any code is written.',
},
{
icon: Hammer,
cmd: '/opsx:apply',
body: 'The AI builds it, working through the tasks and checking each one off as it goes.',
},
{
icon: Archive,
cmd: '/opsx:archive',
body: 'Spec deltas merge into the truth and the change is filed away with a date stamp. Ready for the next one.',
},
];
function TheLoop() {
return (
<section className="mx-auto max-w-5xl px-4 py-20">
<div className="mx-auto max-w-2xl text-center">
<h2 className="text-3xl font-bold tracking-tight">The loop you run</h2>
<p className="mt-4 text-fd-muted-foreground">
Two terminal commands to set up. After that, you live in your AI chat.
</p>
</div>
<ol className="mt-12 grid gap-4 md:grid-cols-4">
{STEPS.map(({ icon: Icon, cmd, label, body }, i) => (
<li
key={cmd}
className="relative rounded-xl border border-fd-border bg-fd-card p-5"
>
<div className="flex items-center justify-between">
<Icon className="size-5 text-fd-primary" />
<span className="text-xs font-medium text-fd-muted-foreground">
{label ?? `step ${i + 1}`}
</span>
</div>
<code className="mt-3 block text-sm font-semibold text-fd-primary">
{cmd}
</code>
<p className="mt-2 text-sm text-fd-muted-foreground">{body}</p>
</li>
))}
</ol>
</section>
);
}
function Why() {
return (
<section className="border-y border-fd-border bg-fd-card/30">
<div className="mx-auto max-w-5xl px-4 py-20">
<div className="mx-auto max-w-2xl text-center">
<h2 className="text-3xl font-bold tracking-tight">
Why bother with the extra step?
</h2>
<p className="mt-4 text-fd-muted-foreground">
OpenSpec adds one small step — a short plan before building. Here is
what you get for it.
</p>
</div>
<div className="mx-auto mt-12 grid max-w-3xl gap-5 sm:grid-cols-2">
{[
[
'Catch wrong turns early',
'Fixing a misunderstanding in a one-paragraph proposal is free. Fixing it after 400 lines of code is not.',
],
[
'The plan lives with the code',
'Six months later, the spec tells you and the next AI session why the system works the way it does.',
],
[
'Changes are reviewable',
'A change folder is a tidy package: read the proposal, skim the deltas, check the tasks. No chat archaeology.',
],
[
'It fits existing codebases',
'Deltas mean you can specify a change to a 50,000-line app without first documenting the whole thing.',
],
].map(([title, body]) => (
<div key={title} className="flex gap-3">
<ArrowRight className="mt-1 size-4 shrink-0 text-fd-primary" />
<div>
<div className="font-semibold">{title}</div>
<p className="mt-1 text-sm text-fd-muted-foreground">{body}</p>
</div>
</div>
))}
</div>
</div>
</section>
);
}
const TEAM_SCENARIOS = [
{
icon: Share2,
title: 'Cross-repo features',
body: 'One change, one plan — even when the code lands in the API server, the web app, and a shared library. No more "whose openspec/ folder does this live in?"',
},
{
icon: Boxes,
title: 'Shared requirements',
body: 'A platform team owns the specs; product teams reference them read-only, right where their coding agent can read them. No more drifting wiki.',
},
{
icon: Clock,
title: 'Plan before code',
body: 'Capture the plan in the store now, while it is just an idea. The code repos catch up later — the thinking is already recorded and reviewed.',
},
];
function Teams() {
return (
<section className="border-y border-fd-border bg-fd-primary/5">
<div className="mx-auto max-w-5xl px-4 py-20">
<div className="mx-auto max-w-2xl text-center">
<p className="text-sm font-medium uppercase tracking-wide text-fd-primary">
For teams
</p>
<h2 className="mt-2 text-3xl font-bold tracking-tight sm:text-4xl">
Why teams adopt OpenSpec
</h2>
<p className="mt-4 text-fd-muted-foreground">
Solo, OpenSpec keeps you and your AI honest on one repo. On a team,
the hard part moves: work spans repos, requirements cross team lines,
and planning starts before code exists. OpenSpec{' '}
<Link href={`${docsRoute}/stores`} className="font-medium text-fd-primary underline">
stores
</Link>{' '}
put planning in a repo of its own — one source of truth your whole
team and every coding agent can read, shared by{' '}
<code>git push</code> like anything else.
</p>
</div>
<div className="mt-12 grid gap-5 md:grid-cols-3">
{TEAM_SCENARIOS.map(({ icon: Icon, title, body }) => (
<div
key={title}
className="rounded-xl border border-fd-border bg-fd-card p-6"
>
<div className="mb-3 inline-flex size-10 items-center justify-center rounded-lg bg-fd-primary/10 text-fd-primary">
<Icon className="size-5" />
</div>
<h3 className="font-semibold">{title}</h3>
<p className="mt-2 text-sm text-fd-muted-foreground">{body}</p>
</div>
))}
</div>
<div className="mt-10 text-center">
<Link
href={`${docsRoute}/stores`}
className="inline-flex items-center justify-center gap-2 rounded-lg bg-fd-primary px-5 py-2.5 text-sm font-semibold text-fd-primary-foreground transition-opacity hover:opacity-90"
>
Explore stores <ArrowRight className="size-4" />
</Link>
<span className="ml-3 rounded-full border border-fd-border bg-fd-card px-2.5 py-1 text-xs font-medium text-fd-muted-foreground">
Beta
</span>
</div>
</div>
</section>
);
}
const TOOLS = [
'Claude Code',
'Cursor',
'Codex',
'Devin Desktop',
'Gemini CLI',
'GitHub Copilot',
'Cline',
'Zoo Code',
'Kilo Code',
'Amazon Q',
'OpenCode',
'Qwen Code',
'Kiro',
'Continue',
'Factory Droid',
];
function ToolStrip() {
return (
<section className="mx-auto max-w-5xl px-4 py-16 text-center">
<p className="text-sm font-medium uppercase tracking-wide text-fd-muted-foreground">
Works with the tools you already use
</p>
<div className="mt-6 flex flex-wrap items-center justify-center gap-2.5">
{TOOLS.map((t) => (
<span
key={t}
className="rounded-full border border-fd-border bg-fd-card px-3.5 py-1.5 text-sm text-fd-foreground/80"
>
{t}
</span>
))}
<span className="rounded-full px-3.5 py-1.5 text-sm font-medium text-fd-primary">
+ 15 more
</span>
</div>
</section>
);
}
const ARTIFACTS = [
{
icon: FileText,
file: 'proposal.md',
caption: 'The why and what',
code: `# Proposal: Add Dark Mode
## Intent
Reduce eye strain at night and
match the user's system theme.
## Scope
- Theme toggle in settings
- System-preference detection
- Persist the choice`,
},
{
icon: Layers,
file: 'specs/ui/spec.md',
caption: 'The delta — what changes',
code: `# Delta for UI
## ADDED Requirements
### Requirement: Theme Selection
The system SHALL let users choose
light or dark.
#### Scenario: Manual toggle
- WHEN the toggle is clicked
- THEN the theme switches at once`,
},
{
icon: ListChecks,
file: 'tasks.md',
caption: 'The checklist',
code: `# Tasks
## 1. Theme Infrastructure
- [ ] 1.1 ThemeContext + state
- [ ] 1.2 CSS custom properties
- [ ] 1.3 localStorage persistence
## 2. UI
- [ ] 2.1 ThemeToggle component`,
},
];
function Anatomy() {
return (
<section className="mx-auto max-w-5xl px-4 py-20">
<div className="mx-auto max-w-2xl text-center">
<h2 className="text-3xl font-bold tracking-tight">
What a change actually looks like
</h2>
<p className="mt-4 text-fd-muted-foreground">
Plain Markdown files your AI drafts and you review. No new formats to
learn, nothing you cannot read at a glance.
</p>
</div>
<div className="mt-12 grid gap-5 md:grid-cols-3">
{ARTIFACTS.map(({ icon: Icon, file, caption, code }) => (
<div
key={file}
className="overflow-hidden rounded-xl border border-fd-border bg-fd-card"
>
<div className="flex items-center gap-2 border-b border-fd-border px-4 py-2.5">
<Icon className="size-4 text-fd-primary" />
<code className="text-xs font-medium">{file}</code>
</div>
<pre className="overflow-x-auto p-4 text-xs leading-relaxed text-fd-muted-foreground">
<code>{code}</code>
</pre>
<div className="border-t border-fd-border px-4 py-2 text-xs text-fd-muted-foreground">
{caption}
</div>
</div>
))}
</div>
</section>
);
}
const ROWS = [
{
name: 'Spec Kit',
by: 'GitHub',
good: 'Thorough and structured',
catch: 'Rigid phase gates, lots of Markdown, Python setup',
us: false,
},
{
name: 'Kiro',
by: 'AWS',
good: 'Powerful and integrated',
catch: 'Locked into their IDE and a limited set of models',
us: false,
},
{
name: 'No specs',
by: 'the default',
good: 'Zero overhead',
catch: 'Vague prompts, unpredictable results, no record of why',
us: false,
},
{
name: 'OpenSpec',
by: '',
good: 'Lightweight, fluid, lives in your repo',
catch: 'Adds one small step — worth it whenever agreement matters',
us: true,
},
];
function Comparison() {
return (
<section className="mx-auto max-w-5xl px-4 py-20">
<div className="mx-auto max-w-2xl text-center">
<h2 className="text-3xl font-bold tracking-tight">The honest middle</h2>
<p className="mt-4 text-fd-muted-foreground">
Heavier tools exist. So does doing nothing. OpenSpec aims for the
spot where the value clearly beats the cost.
</p>
</div>
<div className="mx-auto mt-12 max-w-3xl divide-y divide-fd-border overflow-hidden rounded-xl border border-fd-border">
{ROWS.map((r) => (
<div
key={r.name}
className={
'grid grid-cols-1 gap-1 px-5 py-4 sm:grid-cols-[10rem_1fr] ' +
(r.us ? 'bg-fd-primary/5' : 'bg-fd-card')
}
>
<div className="flex items-center gap-2 font-semibold">
{r.us && <Check className="size-4 text-fd-primary" />}
<span className={r.us ? 'text-fd-primary' : ''}>{r.name}</span>
{r.by && (
<span className="text-xs font-normal text-fd-muted-foreground">
{r.by}
</span>
)}
</div>
<div className="text-sm">
<span className="text-fd-foreground/90">{r.good}.</span>{' '}
<span className="text-fd-muted-foreground">{r.catch}.</span>
</div>
</div>
))}
</div>
</section>
);
}
function FinalCta() {
return (
<section className="mx-auto max-w-5xl px-4 py-24 text-center">
<h2 className="text-3xl font-bold tracking-tight sm:text-4xl">
Ship your first change in five minutes
</h2>
<p className="mx-auto mt-4 max-w-xl text-fd-muted-foreground">
Works with 30+ AI assistants — Claude Code, Cursor, Codex, Devin Desktop,
Gemini CLI, and more.
</p>
<div className="mt-8 inline-flex flex-col gap-1 rounded-lg border border-fd-border bg-fd-card px-4 py-3 text-left font-mono text-sm">
<div className="flex items-center gap-2">
<span className="text-fd-muted-foreground">$</span>
npm install -g @fission-ai/openspec@latest
</div>
<div className="flex items-center gap-2">
<span className="text-fd-muted-foreground">$</span>
cd your-project &amp;&amp; openspec init
</div>
</div>
<p className="mt-4 text-sm text-fd-muted-foreground">
Or{' '}
<Link
href={`${docsRoute}/installation#install-with-your-ai-assistant`}
className="underline underline-offset-4 hover:text-fd-foreground"
>
let your AI assistant install it for you
</Link>
.
</p>
<div className="mt-8">
<Link
href={`${docsRoute}/getting-started`}
className="inline-flex items-center justify-center gap-2 rounded-lg bg-fd-primary px-6 py-3 text-sm font-semibold text-fd-primary-foreground transition-opacity hover:opacity-90"
>
Read the getting-started guide <ArrowRight className="size-4" />
</Link>
</div>
</section>
);
}
+39 -11
View File
@@ -1,34 +1,54 @@
import { getPageImage, getPageMarkdownUrl, source } from '@/lib/source';
import {
DocsBody,
DocsDescription,
DocsPage,
DocsTitle,
MarkdownCopyButton,
ViewOptionsPopover,
} from 'fumadocs-ui/layouts/docs/page';
} from 'fumadocs-ui/layouts/notebook/page';
import { notFound } from 'next/navigation';
import { getMDXComponents } from '@/components/mdx';
import type { Metadata } from 'next';
import { createRelativeLink } from 'fumadocs-ui/mdx';
import { gitConfig } from '@/lib/shared';
// TEMPORARY (2026-08-21): the Overview page (the docs index, served at /docs)
// is pulled from docs.sync.config.mjs while it's rewritten, so there is no
// index page. Cloudflare redirects /docs via public/_redirects; this
// meta-refresh page is the fallback for local dev and the static export (which
// can't issue HTTP redirects), mirroring app/page.tsx. Remove this constant,
// the two uses below, and the `{ slug: [] }` param once the Overview is back.
const TEMP_DOCS_INDEX_REDIRECT = '/docs/installation';
export default async function Page(props: PageProps<'/docs/[[...slug]]'>) {
const params = await props.params;
const page = source.getPage(params.slug);
if (!page) notFound();
if (!page) {
if (!params.slug?.length) {
return (
<>
<meta httpEquiv="refresh" content={`0; url=${TEMP_DOCS_INDEX_REDIRECT}`} />
<p>
Redirecting to <a href={TEMP_DOCS_INDEX_REDIRECT}>installation</a>…
</p>
</>
);
}
notFound();
}
const MDX = page.data.body;
const markdownUrl = getPageMarkdownUrl(page).url;
return (
<DocsPage toc={page.data.toc} full={page.data.full}>
<DocsPage
toc={page.data.toc}
full={page.data.full}
breadcrumb={{ enabled: true, includeRoot: false, includePage: false }}
>
<DocsTitle>{page.data.title}</DocsTitle>
{/*
The frontmatter `description` is derived from the page's first paragraph
(see scripts/sync-docs.mjs), so rendering it here as a subtitle would
just duplicate the opening paragraph of the body below. We keep it in
`generateMetadata` for SEO/OG, but omit the on-page <DocsDescription>.
*/}
<DocsDescription>{page.data.description}</DocsDescription>
<div className="flex flex-row gap-2 items-center border-b pb-6">
<MarkdownCopyButton markdownUrl={markdownUrl} />
<ViewOptionsPopover
@@ -51,13 +71,21 @@ export default async function Page(props: PageProps<'/docs/[[...slug]]'>) {
}
export async function generateStaticParams() {
return source.generateParams();
const params: { slug: string[] }[] = source.generateParams();
// TEMPORARY: emit /docs even without an index page so the static export
// carries the meta-refresh fallback above.
if (!params.some((p) => !p.slug?.length)) params.push({ slug: [] });
return params;
}
export async function generateMetadata(props: PageProps<'/docs/[[...slug]]'>): Promise<Metadata> {
const params = await props.params;
const page = source.getPage(params.slug);
if (!page) notFound();
if (!page) {
// TEMPORARY: metadata for the /docs redirect fallback (see Page above).
if (!params.slug?.length) return { title: 'Documentation', robots: { index: false } };
notFound();
}
return {
title: page.data.title,
+9 -3
View File
@@ -1,10 +1,16 @@
import { source } from '@/lib/source';
import { DocsLayout } from 'fumadocs-ui/layouts/docs';
import { getSidebarTree } from '@/lib/source';
import { baseOptions } from '@/lib/layout.shared';
import { DocsLayout } from 'fumadocs-ui/layouts/notebook';
export default function Layout({ children }: LayoutProps<'/docs'>) {
const { nav, ...base } = baseOptions();
return (
<DocsLayout tree={source.getPageTree()} {...baseOptions()}>
<DocsLayout
tree={getSidebarTree()}
{...base}
nav={{ ...nav, mode: 'top' }}
tabMode="navbar"
>
{children}
</DocsLayout>
);
+1 -10
View File
@@ -1,16 +1,7 @@
@import 'tailwindcss';
@import 'fumadocs-ui/css/neutral.css';
@import 'fumadocs-ui/css/black.css';
@import 'fumadocs-ui/css/preset.css';
/* OpenSpec brand accent — a confident indigo that reads well on light and dark. */
:root {
--color-fd-primary: #4f46e5;
}
.dark {
--color-fd-primary: #818cf8;
}
html {
scrollbar-gutter: stable;
}
+3 -3
View File
@@ -14,12 +14,12 @@ const description =
export const metadata: Metadata = {
metadataBase: new URL(siteUrl),
title: {
default: `${appName} — Agree first, then build confidently`,
template: `%s — ${appName}`,
default: `${appName} | Agree first, then build confidently`,
template: `%s | ${appName}`,
},
description,
openGraph: {
title: `${appName} — Agree first, then build confidently`,
title: `${appName} | Agree first, then build confidently`,
description,
siteName: appName,
type: 'website',
+14
View File
@@ -0,0 +1,14 @@
// This site is documentation-only; the marketing/landing page lives in a
// separate repo. The static export can't issue HTTP redirects itself, so
// Cloudflare Pages handles `/` via public/_redirects; this meta-refresh page
// is the fallback for local previews and hosts that ignore _redirects.
export default function Home() {
return (
<>
<meta httpEquiv="refresh" content="0; url=/docs" />
<p>
Redirecting to <a href="/docs">documentation</a>…
</p>
</>
);
}
+4 -11
View File
@@ -7,18 +7,11 @@ export const revalidate = false;
export default function sitemap(): MetadataRoute.Sitemap {
const base = siteUrl.replace(/\/$/, '');
const docs = source.getPages().map((page) => ({
// `/` redirects to /docs, so the docs pages are the whole sitemap; the
// docs index gets top priority.
return source.getPages().map((page) => ({
url: `${base}${page.url}`,
changeFrequency: 'weekly' as const,
priority: 0.7,
priority: page.url === '/docs' ? 1 : 0.7,
}));
return [
{
url: `${base}/`,
changeFrequency: 'weekly',
priority: 1,
},
...docs,
];
}
+188
View File
@@ -0,0 +1,188 @@
'use client';
import { useId, useMemo, useRef, useState } from 'react';
// Renders a `file-steps` fence (see lib/remark-file-steps.ts) as a
// click-through stepper: numbered steps, a note explaining the step, and an
// annotated file tree. Added lines (`+ ` gutter) carry the accent; removed
// lines (`- `) are struck. Inline annotations are anything after 3+ spaces.
// All steps render stacked in one grid cell so the tallest step fixes the
// height; arrow keys (plus Home/End) step through once the figure has focus.
interface StepLine {
marker: '+' | '-' | ' ';
text: string;
note: string;
}
interface StepData {
title: string;
caption: string[];
lines: StepLine[];
}
const ACCENT = 'text-[#A64F2C] dark:text-[#D89074]';
function parseSteps(content: string): StepData[] {
const steps: StepData[] = [];
for (const raw of content.split('\n')) {
if (raw.startsWith('## ')) {
steps.push({ title: raw.slice(3).trim(), caption: [], lines: [] });
continue;
}
const step = steps[steps.length - 1];
if (!step) continue;
if (raw.startsWith('> ')) {
step.caption.push(raw.slice(2).trim());
continue;
}
if (!raw.trim()) {
if (step.lines.length > 0) step.lines.push({ marker: ' ', text: '', note: '' });
continue;
}
const marker = raw.startsWith('+ ') ? '+' : raw.startsWith('- ') ? '-' : ' ';
const body = marker === ' ' ? (raw.startsWith(' ') ? raw.slice(2) : raw) : raw.slice(2);
const split = body.match(/^(.*?\S)(\s{3,})(.*)$/);
step.lines.push({
marker,
text: split ? split[1] + split[2] : body,
note: split ? split[3] : '',
});
}
for (const step of steps) {
while (step.lines.length > 0 && step.lines[step.lines.length - 1].text === '') {
step.lines.pop();
}
}
return steps;
}
export function FileSteps({ content }: { content: string }) {
const steps = useMemo(() => parseSteps(content), [content]);
const [index, setIndex] = useState(0);
const id = useId();
const tabRefs = useRef<(HTMLButtonElement | null)[]>([]);
if (steps.length === 0) return null;
const select = (next: number, focusTab: boolean) => {
const clamped = Math.max(0, Math.min(steps.length - 1, next));
setIndex(clamped);
if (focusTab) tabRefs.current[clamped]?.focus();
};
const onKeyDown = (e: React.KeyboardEvent) => {
const target = e.target as HTMLElement;
if (target.tagName === 'PRE') return; // leave keyboard scrolling of the tree alone
let next: number | null = null;
if (e.key === 'ArrowLeft') next = index - 1;
else if (e.key === 'ArrowRight') next = index + 1;
else if (e.key === 'Home') next = 0;
else if (e.key === 'End') next = steps.length - 1;
if (next === null) return;
e.preventDefault();
select(next, target.closest('[role="tablist"]') !== null);
};
return (
<figure
tabIndex={0}
onKeyDown={onKeyDown}
aria-label="File steps, use arrow keys to change step"
className="my-6 rounded-none border border-fd-border font-mono focus-visible:outline focus-visible:outline-1 focus-visible:outline-[#A64F2C] dark:focus-visible:outline-[#D89074]"
>
<div className="flex items-center justify-between gap-4 border-b border-fd-border px-4 py-2">
<div className="flex items-center gap-1 text-xs" role="tablist" aria-label="Steps">
{steps.map((s, i) => (
<span key={i} className="flex items-center">
{i > 0 && <span aria-hidden="true" className="px-1 text-fd-muted-foreground">/</span>}
<button
type="button"
role="tab"
id={`${id}-tab-${i}`}
aria-controls={`${id}-panel-${i}`}
aria-selected={i === index}
aria-label={`Step ${i + 1}: ${s.title}`}
tabIndex={i === index ? 0 : -1}
ref={(el) => {
tabRefs.current[i] = el;
}}
onClick={() => select(i, false)}
className={`rounded-none px-1.5 py-0.5 tabular-nums ${
i === index ? `${ACCENT} font-semibold` : 'text-fd-muted-foreground hover:text-fd-foreground'
}`}
>
{i + 1}
</button>
</span>
))}
</div>
<div className="flex items-center gap-2 text-[0.65rem] uppercase tracking-widest">
<button
type="button"
onClick={() => select(index - 1, false)}
disabled={index === 0}
className="rounded-none border border-fd-border px-2 py-0.5 text-fd-muted-foreground hover:text-fd-foreground disabled:cursor-default disabled:opacity-40"
>
Prev
</button>
<button
type="button"
onClick={() => select(index + 1, false)}
disabled={index === steps.length - 1}
className="rounded-none border border-fd-border px-2 py-0.5 text-fd-muted-foreground hover:text-fd-foreground disabled:cursor-default disabled:opacity-40"
>
Next
</button>
</div>
</div>
<div className="grid">
{steps.map((step, i) => (
<div
key={i}
role="tabpanel"
id={`${id}-panel-${i}`}
aria-labelledby={`${id}-tab-${i}`}
aria-hidden={i !== index}
className={`col-start-1 row-start-1 px-4 py-3 ${i === index ? '' : 'invisible'}`}
>
<div className="text-[0.7rem] font-semibold uppercase tracking-[0.18em]">
<span className={ACCENT}>Step {i + 1}</span>
<span aria-hidden="true" className="px-2 text-fd-muted-foreground">·</span>
<span className="text-fd-foreground">{step.title}</span>
</div>
{step.caption.length > 0 && (
<p className="mt-2 max-w-prose text-[0.8rem] leading-6 text-fd-foreground">
{step.caption.join(' ')}
</p>
)}
<pre className="mt-3 overflow-x-auto text-[0.8rem] leading-6">
{step.lines.map((line, j) => (
<div
key={j}
className={
line.marker === '+'
? ACCENT
: line.marker === '-'
? 'text-fd-muted-foreground line-through'
: 'text-fd-foreground'
}
>
<span aria-hidden="true" className="select-none pr-2 opacity-70">
{line.marker === ' ' ? ' ' : line.marker}
</span>
{line.text}
{line.note && <span className="text-fd-muted-foreground">{line.note}</span>}
</div>
))}
</pre>
</div>
))}
</div>
</figure>
);
}
+2
View File
@@ -3,6 +3,7 @@ import { Tab, Tabs } from 'fumadocs-ui/components/tabs';
import { Step, Steps } from 'fumadocs-ui/components/steps';
import { Accordion, Accordions } from 'fumadocs-ui/components/accordion';
import { Mermaid } from '@/components/mermaid';
import { FileSteps } from '@/components/file-steps';
import type { MDXComponents } from 'mdx/types';
export function getMDXComponents(components?: MDXComponents) {
@@ -15,6 +16,7 @@ export function getMDXComponents(components?: MDXComponents) {
Accordion,
Accordions,
Mermaid,
FileSteps,
...components,
} satisfies MDXComponents;
}
+144 -47
View File
@@ -1,76 +1,173 @@
// Single source of truth for the documentation site's content.
//
// The pages under `content/docs/` are NOT authored by hand. They are generated
// from the repository's `docs/*.md` files by `scripts/sync-docs.mjs` (which runs
// as the first step of `npm run build` / `npm run dev`). Edit the docs in
// `../docs`, and the site mirrors them automatically — locally and in CI.
// from the repository's `docs-lab/**/*.md` files by `scripts/sync-docs.mjs`
// (which runs as the first step of `npm run build` / `npm run dev`). Edit the
// docs in `../docs-lab`, and the site mirrors them automatically, both locally
// and in CI.
//
// This manifest is the only place that decides which docs are published, their
// slug/URL, their sidebar section and order, and their sidebar icon.
// slug/URL, and their sidebar section and order.
//
// `source` is a path relative to the repo root's `docs/` directory.
// `slug` is the page path under `/docs/` (may contain a folder, e.g. reference/cli).
// `icon` is any lucide-react icon name (unknown names simply render no icon).
export const docsDir = '../docs';
// `source` is a path relative to the repo root's `docs-lab/` directory.
// `slug` is the page path under `/docs/`.
//
// A section's `pages` list may also hold a folder entry
// (`{ folder, label, pages }`): its pages publish under `<folder>/...` slugs
// and the sidebar shows them as a collapsible group inside the section. A page
// with slug `<folder>/index` is the folder's landing page (served at
// `/docs/<folder>`). Folder entries may nest: a folder's `pages` list may hold
// another folder entry (`folder` is always the full path, e.g.
// `schemas/spec-driven`), rendered as a collapsible group inside the group.
//
// Page descriptions come from each page's leading `> ...` blockquote, lifted
// into frontmatter by sync-docs.mjs. Don't duplicate them here.
export const docsDir = '../docs-lab';
/** Ordered sections; each becomes a labeled group in the sidebar. */
export const sections = [
{
label: 'Start here',
label: 'Start',
pages: [
{ source: 'README.md', slug: 'index', icon: 'Sparkles' },
{ source: 'installation.md', slug: 'installation', icon: 'Download' },
{ source: 'getting-started.md', slug: 'getting-started', icon: 'Rocket' },
{ source: 'how-commands-work.md', slug: 'how-commands-work', icon: 'Terminal' },
],
},
{
label: 'Understand it',
pages: [
{ source: 'overview.md', slug: 'overview', icon: 'Map' },
{ source: 'concepts.md', slug: 'core-concepts', icon: 'Boxes' },
{ source: 'workflows.md', slug: 'the-workflow', icon: 'Workflow' },
{ source: 'opsx.md', slug: 'opsx', icon: 'GitBranch' },
{ source: 'explore.md', slug: 'explore', icon: 'Compass' },
// TEMPORARY (2026-08-21): the Overview page is pulled from the site while
// docs-lab/start/overview.md is rewritten from scratch (it's a TODO stub).
// Until it returns, /docs redirects to Installation: see public/_redirects
// (Cloudflare) and the empty-slug fallback in app/docs/[[...slug]]/page.tsx
// (local dev and static export). To restore: uncomment the line below and
// remove both redirects. The `index` slug is a router requirement (it
// serves /docs); the authored source file is overview.md.
// { source: 'start/overview.md', slug: 'index' },
{ source: 'start/installation.md', slug: 'installation' },
{ source: 'start/setup.md', slug: 'setup' },
{ source: 'start/quickstart.md', slug: 'quickstart' },
],
},
// Guides are held back until the pages are drafted. Re-publish one by moving
// its entry out of this comment, keeping its folder wrapper so the slug stays
// `<folder>/<name>`. Links to a held-back guide fall back to its source on
// GitHub (see rewriteLinks in scripts/sync-docs.mjs). The Guides navbar tab
// returns on its own once this section exists again (lib/source.ts).
/*
{
label: 'Guides',
pages: [
{ source: 'examples.md', slug: 'examples', icon: 'ListChecks' },
{ source: 'writing-specs.md', slug: 'writing-specs', icon: 'PenLine' },
{ source: 'reviewing-changes.md', slug: 'reviewing-changes', icon: 'SearchCheck' },
{ source: 'existing-projects.md', slug: 'existing-projects', icon: 'FolderGit2' },
{ source: 'editing-changes.md', slug: 'editing-changes', icon: 'Pencil' },
{ source: 'customization.md', slug: 'customization', icon: 'Settings2' },
{ source: 'multi-language.md', slug: 'multi-language', icon: 'Languages' },
{ source: 'team-workflow.md', slug: 'team-workflow', icon: 'GitPullRequest' },
{ source: 'stores-beta/user-guide.md', slug: 'stores', icon: 'Store' },
{
folder: 'understanding',
label: 'Understanding OpenSpec',
defaultOpen: true,
pages: [{ source: 'guides/concepts.md', slug: 'understanding/concepts' }],
},
{
folder: 'using',
label: 'Using OpenSpec',
defaultOpen: true,
pages: [
{ source: 'guides/explore.md', slug: 'using/explore' },
{ source: 'guides/review-the-plan.md', slug: 'using/review-the-plan' },
{ source: 'guides/apply.md', slug: 'using/apply' },
{ source: 'guides/change-course.md', slug: 'using/change-course' },
],
},
{
folder: 'adopting',
label: 'Adopting OpenSpec',
defaultOpen: true,
pages: [
{ source: 'guides/existing-codebases.md', slug: 'adopting/existing-codebases' },
{ source: 'guides/teams.md', slug: 'adopting/teams' },
],
},
],
},
*/
{
label: 'Customize',
pages: [
{ source: 'customize/overview.md', slug: 'customize' },
{ source: 'customize/profiles.md', slug: 'profiles' },
{ source: 'customize/project-config.md', slug: 'project-config' },
{ source: 'customize/schemas.md', slug: 'customize-schemas' },
],
},
{
// Rendered as a collapsible folder (its own meta.json) rather than a label.
label: 'Reference',
folder: 'reference',
icon: 'BookMarked',
label: 'Multi-repo (beta)',
pages: [
{ source: 'commands.md', slug: 'reference/slash-commands', icon: 'SquareSlash' },
{ source: 'cli.md', slug: 'reference/cli', icon: 'SquareTerminal' },
{ source: 'supported-tools.md', slug: 'reference/supported-tools', icon: 'Wrench' },
{ source: 'agent-contract.md', slug: 'reference/agents', icon: 'Bot' },
{ source: 'multi-repo/stores.md', slug: 'stores' },
{ source: 'multi-repo/worksets.md', slug: 'worksets' },
],
},
{
label: 'Reference',
pages: [
{ source: 'reference/skills.md', slug: 'skills' },
{ source: 'reference/cli.md', slug: 'cli' },
{
folder: 'schemas',
label: 'Schemas',
pages: [
{ source: 'reference/schemas/index.md', slug: 'schemas/index' },
{ source: 'reference/schemas/schema-yaml.md', slug: 'schemas/schema-yaml' },
{ source: 'reference/schemas/spec-driven/index.md', slug: 'schemas/spec-driven' },
],
},
{
folder: 'configuration',
label: 'Configuration',
pages: [
{ source: 'reference/configuration/index.md', slug: 'configuration/index' },
{ source: 'reference/configuration/config-yaml.md', slug: 'configuration/config-yaml' },
{ source: 'reference/configuration/change-metadata.md', slug: 'configuration/change-metadata' },
{ source: 'reference/configuration/config-json.md', slug: 'configuration/config-json' },
// TODO (held back 2026-08-21): Environment variables and Stores are
// headings only, so they stay out of the nav until written. The
// markdown stays in docs-lab/reference/configuration/. Links to them
// from published pages fall back to their GitHub source. Re-publish
// by moving the lines out of this comment.
// { source: 'reference/configuration/environment-variables.md', slug: 'configuration/environment-variables' },
// { source: 'reference/configuration/stores.md', slug: 'configuration/stores' },
],
},
{ source: 'reference/supported-tools.md', slug: 'supported-tools' },
{ source: 'reference/glossary.md', slug: 'glossary' },
// TODO (held back 2026-08-21): Architecture is not written yet (all three
// pages are headings only), so the group is hidden until we get to it.
// The markdown stays in docs-lab/reference/architecture/. Links to these
// pages from published pages fall back to their GitHub source. Re-publish
// by moving the folder entry out of this comment.
/*
{
folder: 'architecture',
label: 'Architecture',
pages: [
{ source: 'reference/architecture/index.md', slug: 'architecture/index' },
{ source: 'reference/architecture/workflow-runs.md', slug: 'architecture/workflow-runs' },
{ source: 'reference/architecture/design-decisions.md', slug: 'architecture/design-decisions' },
],
},
*/
],
},
// TODO (held back 2026-08-21): Help and Legacy are not written yet (FAQ has one
// answer, Troubleshooting and Migration are headings only), so both sections
// are hidden from the site until we get to them. The markdown stays in
// docs-lab/help/. Links to these pages from published pages fall back to
// their GitHub source (rewriteLinks in scripts/sync-docs.mjs). Re-publish by
// moving the entries out of this comment, same as Guides above.
/*
{
label: 'Help',
pages: [
{ source: 'faq.md', slug: 'faq', icon: 'CircleHelp' },
{ source: 'troubleshooting.md', slug: 'troubleshooting', icon: 'LifeBuoy' },
{ source: 'glossary.md', slug: 'glossary', icon: 'BookA' },
{ source: 'migration-guide.md', slug: 'migration-guide', icon: 'ArrowLeftRight' },
{ source: 'help/faq.md', slug: 'faq' },
{ source: 'help/troubleshooting.md', slug: 'troubleshooting' },
],
},
{
label: 'Legacy',
pages: [{ source: 'help/legacy/migration.md', slug: 'migration' }],
},
*/
];
/** Flat list of every published page, in sidebar order. */
export const pages = sections.flatMap((section) => section.pages);
/** Flat list of every published route (folder entries expanded recursively). */
const expandEntry = (entry) => (entry.folder ? entry.pages.flatMap(expandEntry) : [entry]);
export const pages = sections.flatMap((section) => section.pages.flatMap(expandEntry));
+11 -13
View File
@@ -1,30 +1,28 @@
import type { BaseLayoutProps } from 'fumadocs-ui/layouts/shared';
import { appName, links } from './shared';
/**
* Shared layout options for both the home (marketing) layout and the docs
* layout. Keeping nav links in one place means the header stays consistent
* everywhere.
*/
/** Shared layout options for the docs layout. */
export function baseOptions(): BaseLayoutProps {
return {
nav: {
// The site is documentation-only, so the logo links to the docs index
// rather than `/` (which just redirects there).
url: '/docs',
title: (
<span className="font-semibold tracking-tight">
Open<span className="text-fd-primary">Spec</span>
</span>
<img
src="/openspec-pixel.svg"
alt={appName}
className="h-3 w-auto dark:invert [#nd-sidebar_&]:ml-2"
/>
),
},
// No "Documentation" link here: the navbar layout tabs already cover it.
links: [
{
text: 'Documentation',
url: '/docs',
active: 'nested-url',
},
{
text: 'Discord',
url: links.discord,
external: true,
on: 'nav',
},
],
githubUrl: links.github,
+74
View File
@@ -0,0 +1,74 @@
// Turns the FAQ page's `##` question sections into Fumadocs Accordion
// elements, the same mdxJsxFlowElement injection remarkGfmAlert and
// remarkFileSteps use, so the doc stays plain headings on GitHub while the
// site renders a collapsible FAQ.
//
// Applies only to files named `faq` (the synced content/docs/faq.md); every
// other page keeps its headings. Each accordion gets a GitHub-style slug id so
// existing `#heading-anchor` deep links still open the right question.
// getLLMText (lib/source.ts) round-trips the accordions back to `##` headings.
interface Node {
type: string;
depth?: number;
value?: string;
children?: Node[];
[key: string]: unknown;
}
function toText(node: Node): string {
if (node.type === 'text' || node.type === 'inlineCode') return node.value ?? '';
return (node.children ?? []).map(toText).join('');
}
// Matches github-slugger for plain-text titles, which is what the sync'd
// heading anchors used.
function slugify(title: string): string {
return title
.toLowerCase()
.replace(/[^a-z0-9 -]/g, '')
.trim()
.replace(/\s+/g, '-');
}
function accordion(title: string, children: Node[]): Node {
return {
type: 'mdxJsxFlowElement',
name: 'Accordion',
attributes: [
{ type: 'mdxJsxAttribute', name: 'title', value: title },
{ type: 'mdxJsxAttribute', name: 'id', value: slugify(title) },
],
children,
};
}
export function remarkFaq() {
return (tree: Node, file: { stem?: string | null }) => {
if (file.stem !== 'faq' || !tree.children) return;
const first = tree.children.findIndex(
(child) => child.type === 'heading' && child.depth === 2,
);
if (first === -1) return;
const accordions: Node[] = [];
let title: string | undefined;
let body: Node[] = [];
for (const child of tree.children.slice(first)) {
if (child.type === 'heading' && child.depth === 2) {
if (title !== undefined) accordions.push(accordion(title, body));
title = toText(child);
body = [];
} else {
body.push(child);
}
}
if (title !== undefined) accordions.push(accordion(title, body));
tree.children = [
...tree.children.slice(0, first),
{ type: 'mdxJsxFlowElement', name: 'Accordions', attributes: [], children: accordions },
];
};
}
+38
View File
@@ -0,0 +1,38 @@
// Turns `file-steps` fences into the interactive <FileSteps> stepper, the
// same mdxJsxFlowElement injection remarkMdxMermaid and remarkGfmAlert use.
// The fence body stays readable on GitHub: `## ` lines start a step, `> `
// lines are the step's caption, and the remaining lines are a file tree
// whose two-character gutter (`+ ` added, `- ` removed, ` ` unchanged)
// reads like a diff.
interface Node {
type: string;
lang?: string | null;
value?: string;
children?: Node[];
[key: string]: unknown;
}
function transform(node: Node): void {
if (!node.children) return;
node.children.forEach((child, index) => {
transform(child);
if (child.type !== 'code' || child.lang !== 'file-steps') return;
node.children![index] = {
type: 'mdxJsxFlowElement',
name: 'FileSteps',
attributes: [
{ type: 'mdxJsxAttribute', name: 'content', value: child.value ?? '' },
],
children: [],
};
});
}
export function remarkFileSteps() {
return (tree: Node) => {
transform(tree);
};
}
+61
View File
@@ -0,0 +1,61 @@
// Turns GitHub-style blockquote alerts (`> [!NOTE]`) into Fumadocs Callout
// elements, the same mdxJsxFlowElement injection remarkMdxMermaid uses, so
// docs keep GitHub-native syntax while the site renders styled callouts.
//
// Marker-to-Callout mapping is bijective so getLLMText (lib/source.ts) can
// round-trip a Callout placeholder back to the original blockquote syntax.
const MARKER_TO_TYPE: Record<string, string> = {
NOTE: 'info',
TIP: 'idea',
IMPORTANT: 'warn',
WARNING: 'warning',
CAUTION: 'error',
};
export const TYPE_TO_MARKER: Record<string, string> = Object.fromEntries(
Object.entries(MARKER_TO_TYPE).map(([marker, type]) => [type, marker]),
);
const MARKER_RE = /^\[!(NOTE|TIP|IMPORTANT|WARNING|CAUTION)\]\s*/;
interface Node {
type: string;
value?: string;
children?: Node[];
[key: string]: unknown;
}
function transform(node: Node): void {
if (!node.children) return;
node.children.forEach((child, index) => {
transform(child);
if (child.type !== 'blockquote') return;
const para = child.children?.[0];
const text = para?.type === 'paragraph' ? para.children?.[0] : undefined;
if (!text || text.type !== 'text' || typeof text.value !== 'string') return;
const match = MARKER_RE.exec(text.value);
if (!match) return;
text.value = text.value.slice(match[0].length);
if (!text.value) para!.children!.shift();
if (para!.children!.length === 0) child.children!.shift();
node.children![index] = {
type: 'mdxJsxFlowElement',
name: 'Callout',
attributes: [
{ type: 'mdxJsxAttribute', name: 'type', value: MARKER_TO_TYPE[match[1]] },
],
children: child.children,
};
});
}
export function remarkGfmAlert() {
return (tree: Node) => {
transform(tree);
};
}
+91 -8
View File
@@ -1,23 +1,85 @@
import { docs } from 'collections/server';
import { renderPlaceholder } from 'fumadocs-core/mdx-plugins/remark-llms.runtime';
import type * as PageTree from 'fumadocs-core/page-tree';
import { loader } from 'fumadocs-core/source';
import { icons } from 'lucide-react';
import { createElement } from 'react';
import { TYPE_TO_MARKER } from './remark-gfm-alert';
import { docsContentRoute, docsImageRoute, docsRoute } from './shared';
// See https://fumadocs.dev/docs/headless/source-api for more info
export const source = loader({
baseUrl: docsRoute,
source: docs.toFumadocsSource(),
// Render a lucide icon in the sidebar when a page sets `icon:` in frontmatter.
icon(icon) {
if (icon && icon in icons) {
return createElement(icons[icon as keyof typeof icons]);
}
},
plugins: [],
});
// The synced content is flat (meta.json separators, flat /docs/* URLs), which
// renders sections as fixed labels. Regroup each separator's pages into a
// folder node so the sidebar sections collapse, without changing any URLs.
export function getSidebarTree(): PageTree.Root {
const tree = source.getPageTree();
const children: PageTree.Node[] = [];
let section: PageTree.Folder | undefined;
for (const node of tree.children) {
if (node.type === 'separator') {
section = {
$id: node.$id ?? `section-${children.length}`,
type: 'folder',
name: node.name,
defaultOpen: true,
children: [],
};
children.push(section);
} else if (section) {
section.children.push(node);
} else {
children.push(node);
}
}
return { ...tree, children: splitIntoTabs(children) };
}
function firstPage(nodes: PageTree.Node[]): PageTree.Item | undefined {
for (const node of nodes) {
if (node.type === 'page') return node;
if (node.type === 'folder') {
const found = firstPage(node.children);
if (found) return found;
}
}
}
// Split the sidebar into two layout tabs ("Documentation" and "Guides") by
// wrapping the sections in `root: true` folders. Fumadocs derives the tab bar
// from root folders and shows only the active root's subtree in the sidebar;
// URLs are unaffected. `index` is required for a root folder without direct
// page children — it becomes the tab's link target.
function splitIntoTabs(sections: PageTree.Node[]): PageTree.Node[] {
const guides = sections.find(
(node): node is PageTree.Folder => node.type === 'folder' && node.name === 'Guides'
);
if (!guides) return sections;
const rest = sections.filter((node) => node !== guides);
const docsTab: PageTree.Folder = {
$id: 'tab-documentation',
type: 'folder',
name: 'Documentation',
root: true,
index: firstPage(rest),
children: rest,
};
const guidesTab: PageTree.Folder = {
...guides,
$id: 'tab-guides',
root: true,
index: firstPage(guides.children),
};
return [docsTab, guidesTab];
}
export function getPageImage(page: (typeof source)['$inferPage']) {
const segments = [...page.slugs, 'image.png'];
@@ -46,6 +108,27 @@ export async function getLLMText(page: (typeof source)['$inferPage']) {
${attributes.chart}
\`\`\``;
},
FileSteps({ attributes }) {
if (typeof attributes.content !== 'string') return '';
return `\`\`\`file-steps
${attributes.content}
\`\`\``;
},
Callout({ attributes, children }) {
const marker = TYPE_TO_MARKER[String(attributes.type)] ?? 'NOTE';
const body = String(children ?? '').trim();
return [`> [!${marker}]`, ...body.split('\n').map((line) => `> ${line}`)].join('\n');
},
Accordions({ children }) {
return String(children ?? '').trim();
},
Accordion({ attributes, children }) {
const body = String(children ?? '').trim();
return [`## ${attributes.title}`, body].filter(Boolean).join('\n\n') + '\n\n';
},
});
return `# ${page.data.title} (${page.url})
+3
View File
@@ -6,6 +6,9 @@ const withMDX = createMDX();
const config = {
// Static HTML export — the `out/` directory deploys directly to Cloudflare Pages.
output: 'export',
// Static export has no Image Optimization API; serve images as-is. Required
// for the diagram images the docs pipeline embeds via next/image.
images: { unoptimized: true },
reactStrictMode: true,
// This site has its own lockfile and lives inside the OpenSpec monorepo, so
// pin the workspace root to silence Next's multi-lockfile inference warning.
+2 -1
View File
@@ -5,8 +5,9 @@
"description": "Documentation site for OpenSpec, built with Fumadocs and deployable to Cloudflare Pages.",
"scripts": {
"sync:docs": "node scripts/sync-docs.mjs",
"sync:docs:watch": "node --watch-path=../docs --watch-path=../docs-lab --watch-path=docs.sync.config.mjs --watch-preserve-output scripts/sync-docs.mjs",
"build": "pnpm run sync:docs && fumadocs-mdx && next build",
"dev": "pnpm run sync:docs && next dev",
"dev": "pnpm run sync:docs && (pnpm run sync:docs:watch & next dev)",
"start": "serve out",
"types:check": "pnpm run sync:docs && fumadocs-mdx && next typegen && tsc --noEmit"
},
+7
View File
@@ -0,0 +1,7 @@
# Cloudflare Pages redirects (evaluated before static assets).
# The root of this deploy is documentation-only; the landing page is a
# separate repo/deploy.
/ /docs 302
# TEMPORARY (2026-08-21): no docs index page while the Overview is rewritten
# (see docs.sync.config.mjs). Remove this line when the Overview is re-listed.
/docs /docs/installation 302
+89
View File
@@ -0,0 +1,89 @@
<svg xmlns="http://www.w3.org/2000/svg" width="640" height="80" viewBox="0 0 640 80">
<rect x="16" y="0" width="16" height="16" fill="black" />
<rect x="32" y="0" width="16" height="16" fill="black" />
<rect x="0" y="16" width="16" height="16" fill="black" />
<rect x="48" y="16" width="16" height="16" fill="black" />
<rect x="0" y="32" width="16" height="16" fill="black" />
<rect x="48" y="32" width="16" height="16" fill="black" />
<rect x="0" y="48" width="16" height="16" fill="black" />
<rect x="48" y="48" width="16" height="16" fill="black" />
<rect x="16" y="64" width="16" height="16" fill="black" />
<rect x="32" y="64" width="16" height="16" fill="black" />
<rect x="80" y="0" width="16" height="16" fill="black" />
<rect x="96" y="0" width="16" height="16" fill="black" />
<rect x="112" y="0" width="16" height="16" fill="black" />
<rect x="80" y="16" width="16" height="16" fill="black" />
<rect x="128" y="16" width="16" height="16" fill="black" />
<rect x="80" y="32" width="16" height="16" fill="black" />
<rect x="96" y="32" width="16" height="16" fill="black" />
<rect x="112" y="32" width="16" height="16" fill="black" />
<rect x="128" y="32" width="16" height="16" fill="black" />
<rect x="80" y="48" width="16" height="16" fill="black" />
<rect x="80" y="64" width="16" height="16" fill="black" />
<rect x="160" y="0" width="16" height="16" fill="black" />
<rect x="176" y="0" width="16" height="16" fill="black" />
<rect x="192" y="0" width="16" height="16" fill="black" />
<rect x="208" y="0" width="16" height="16" fill="black" />
<rect x="160" y="16" width="16" height="16" fill="black" />
<rect x="160" y="32" width="16" height="16" fill="black" />
<rect x="176" y="32" width="16" height="16" fill="black" />
<rect x="192" y="32" width="16" height="16" fill="black" />
<rect x="160" y="48" width="16" height="16" fill="black" />
<rect x="160" y="64" width="16" height="16" fill="black" />
<rect x="176" y="64" width="16" height="16" fill="black" />
<rect x="192" y="64" width="16" height="16" fill="black" />
<rect x="208" y="64" width="16" height="16" fill="black" />
<rect x="240" y="0" width="16" height="16" fill="black" />
<rect x="288" y="0" width="16" height="16" fill="black" />
<rect x="240" y="16" width="16" height="16" fill="black" />
<rect x="256" y="16" width="16" height="16" fill="black" />
<rect x="288" y="16" width="16" height="16" fill="black" />
<rect x="240" y="32" width="16" height="16" fill="black" />
<rect x="272" y="32" width="16" height="16" fill="black" />
<rect x="288" y="32" width="16" height="16" fill="black" />
<rect x="240" y="48" width="16" height="16" fill="black" />
<rect x="288" y="48" width="16" height="16" fill="black" />
<rect x="240" y="64" width="16" height="16" fill="black" />
<rect x="288" y="64" width="16" height="16" fill="black" />
<rect x="336" y="0" width="16" height="16" fill="black" />
<rect x="352" y="0" width="16" height="16" fill="black" />
<rect x="368" y="0" width="16" height="16" fill="black" />
<rect x="320" y="16" width="16" height="16" fill="black" />
<rect x="336" y="32" width="16" height="16" fill="black" />
<rect x="352" y="32" width="16" height="16" fill="black" />
<rect x="368" y="48" width="16" height="16" fill="black" />
<rect x="320" y="64" width="16" height="16" fill="black" />
<rect x="336" y="64" width="16" height="16" fill="black" />
<rect x="352" y="64" width="16" height="16" fill="black" />
<rect x="400" y="0" width="16" height="16" fill="black" />
<rect x="416" y="0" width="16" height="16" fill="black" />
<rect x="432" y="0" width="16" height="16" fill="black" />
<rect x="400" y="16" width="16" height="16" fill="black" />
<rect x="448" y="16" width="16" height="16" fill="black" />
<rect x="400" y="32" width="16" height="16" fill="black" />
<rect x="416" y="32" width="16" height="16" fill="black" />
<rect x="432" y="32" width="16" height="16" fill="black" />
<rect x="448" y="32" width="16" height="16" fill="black" />
<rect x="400" y="48" width="16" height="16" fill="black" />
<rect x="400" y="64" width="16" height="16" fill="black" />
<rect x="480" y="0" width="16" height="16" fill="black" />
<rect x="496" y="0" width="16" height="16" fill="black" />
<rect x="512" y="0" width="16" height="16" fill="black" />
<rect x="528" y="0" width="16" height="16" fill="black" />
<rect x="480" y="16" width="16" height="16" fill="black" />
<rect x="480" y="32" width="16" height="16" fill="black" />
<rect x="496" y="32" width="16" height="16" fill="black" />
<rect x="512" y="32" width="16" height="16" fill="black" />
<rect x="480" y="48" width="16" height="16" fill="black" />
<rect x="480" y="64" width="16" height="16" fill="black" />
<rect x="496" y="64" width="16" height="16" fill="black" />
<rect x="512" y="64" width="16" height="16" fill="black" />
<rect x="528" y="64" width="16" height="16" fill="black" />
<rect x="576" y="0" width="16" height="16" fill="black" />
<rect x="592" y="0" width="16" height="16" fill="black" />
<rect x="560" y="16" width="16" height="16" fill="black" />
<rect x="560" y="32" width="16" height="16" fill="black" />
<rect x="560" y="48" width="16" height="16" fill="black" />
<rect x="576" y="64" width="16" height="16" fill="black" />
<rect x="592" y="64" width="16" height="16" fill="black" />
</svg>

After

Width:  |  Height:  |  Size: 5.1 KiB

+137 -51
View File
@@ -1,38 +1,82 @@
#!/usr/bin/env node
// Generate the Fumadocs content set (`content/docs/**`) from the repository's
// canonical Markdown in `../docs`. This is the mechanical mirror: docs/*.md is
// the single source of truth, and the site is a faithful, always-current view
// of it. Runs as the first step of `build`/`dev`, and on a cadence in CI.
// Generate the Fumadocs content set (`content/docs/**`) as a mechanical mirror
// of the repository's `docs-lab/**/*.md` files.
// Runs as the first step of `build`/`dev`, and on a cadence in CI.
//
// For each published doc (see docs.sync.config.mjs) it:
// - derives the page title from the leading `# H1` (and strips that H1),
// - derives a short description from the first paragraph,
// - injects Fumadocs frontmatter (title / description / icon / githubSource),
// - lifts the leading `> ...` blockquote into the frontmatter description,
// - injects Fumadocs frontmatter (title / description / githubSource),
// - rewrites internal `*.md` links to their `/docs/...` routes,
// - writes the result as a `.md` file (Fumadocs parses `.md` as plain
// Markdown, so `<placeholders>` and `{braces}` in the docs stay literal),
// - and emits `meta.json` sidebar ordering for the root and the reference folder.
// - and emits `meta.json` sidebar ordering.
//
// Generated files live under content/docs/ and are git-ignored — never edit
// them by hand; edit ../docs instead.
// them by hand; edit ../docs-lab instead.
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
import {
copyFileSync,
existsSync,
mkdirSync,
readdirSync,
readFileSync,
rmSync,
writeFileSync,
} from 'node:fs';
import { dirname, join, posix, relative, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import { docsDir, pages, sections } from '../docs.sync.config.mjs';
const websiteRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..');
const docsRoot = resolve(websiteRoot, docsDir);
const outRoot = join(websiteRoot, 'content', 'docs');
const sourceRoot = resolve(websiteRoot, docsDir);
// The source directory's path from the repo root (e.g. `docs-lab`), for
// GitHub links.
const repoDocsDir = posix.normalize(docsDir).replace(/^\.\.\//, '');
const gitBranch = 'main';
const gitBlobBase = 'https://github.com/Fission-AI/OpenSpec/blob';
// Map every source path (relative to docs/, normalized) -> its /docs route,
// so cross-doc `.md` links resolve to on-site pages.
// Map every source file -> its /docs route, so cross-doc Markdown links
// resolve.
const routeBySource = new Map();
for (const page of pages) {
const normalized = posix.normalize(page.source);
routeBySource.set(normalized, page.slug === 'index' ? '/docs' : `/docs/${page.slug}`);
const key = posix.normalize(page.source);
if (!routeBySource.has(key)) {
// An `index` slug (root or `<folder>/index`) serves its parent path.
const route = page.slug === 'index' ? '' : `/${page.slug.replace(/\/index$/, '')}`;
routeBySource.set(key, `/docs${route}`);
}
}
// Every output file goes through here. Skipping identical writes keeps mtimes
// stable so the fumadocs-mdx dev watcher only rebuilds pages that changed;
// `written` records the full expected output set for stale-file cleanup.
const written = new Set();
function writeOutputFile(path, content) {
written.add(path);
let current = null;
try {
current = readFileSync(path, 'utf8');
} catch {
// Missing (or unreadable) file: write it.
}
if (current === content) return;
mkdirSync(dirname(path), { recursive: true });
writeFileSync(path, content, 'utf8');
}
function removeStaleOutputs(dir) {
if (!existsSync(dir)) return;
for (const entry of readdirSync(dir, { withFileTypes: true })) {
const path = join(dir, entry.name);
if (entry.isDirectory()) {
removeStaleOutputs(path);
if (readdirSync(path).length === 0) rmSync(path, { recursive: true });
} else if (!written.has(path)) {
rmSync(path);
}
}
}
function yamlQuote(value) {
@@ -53,6 +97,24 @@ function extractTitle(markdown, fallback) {
return { title: fallback, rest: markdown };
}
// Authoring convention: a `> ...` blockquote directly after the H1 is the
// page's one-line description. Lift it into frontmatter and strip it from
// the body so the sentence doesn't render twice (Fumadocs already shows the
// description under the title).
function extractLeadingQuote(markdown) {
const lines = markdown.split('\n');
let i = 0;
while (i < lines.length && lines[i].trim() === '') i++;
if (i >= lines.length || !lines[i].startsWith('>')) return { quote: '', rest: markdown };
const buffer = [];
while (i < lines.length && lines[i].startsWith('>')) {
buffer.push(lines[i].replace(/^>\s?/, '').trim());
i++;
}
const quote = buffer.join(' ').replace(/[*_`]/g, '').replace(/\s+/g, ' ').trim();
return { quote, rest: lines.slice(i).join('\n').replace(/^\n+/, '') };
}
// First real paragraph, flattened to a one-line meta description.
function extractDescription(markdown) {
const lines = markdown.split('\n');
@@ -83,91 +145,110 @@ function extractDescription(markdown) {
}
// Rewrite internal Markdown links that point at other docs.
// `sourceRel` is the current doc's path relative to docs/ (for resolving ../).
// `sourceRel` is the current doc's path relative to the source directory.
function rewriteLinks(markdown, sourceRel) {
const sourceDir = posix.dirname(sourceRel);
const sourceFileDir = posix.dirname(sourceRel);
return markdown.replace(/\]\(([^)]+)\)/g, (whole, target) => {
// Leave external, anchor-only, and non-.md links untouched.
if (/^(https?:|mailto:|#|\/)/.test(target)) return whole;
const [rawPath, hash] = target.split('#');
if (!/\.md$/i.test(rawPath)) return whole;
const resolved = posix.normalize(posix.join(sourceDir, rawPath)).replace(/^\.\//, '');
const resolved = posix.normalize(posix.join(sourceFileDir, rawPath)).replace(/^\.\//, '');
const route = routeBySource.get(resolved);
const suffix = hash ? `#${hash}` : '';
if (route) return `](${route}${suffix})`;
// A link we don't publish (e.g. the repo-root README) — fall back to the
// source on GitHub, normalizing any `../` that escapes the docs/ folder.
const repoPath = posix.normalize(`docs/${resolved}`);
// source on GitHub, normalizing any `../` that escapes the source folder.
const repoPath = posix.join(repoDocsDir, resolved);
return `](${gitBlobBase}/${gitBranch}/${repoPath}${suffix})`;
});
}
function buildFrontmatter({ title, description, icon, source }) {
function buildFrontmatter({ title, description, repoSource }) {
const fm = [`title: ${yamlQuote(title)}`];
if (description) fm.push(`description: ${yamlQuote(description)}`);
if (icon) fm.push(`icon: ${icon}`);
fm.push(`githubSource: ${yamlQuote(`docs/${source}`)}`);
fm.push(`githubSource: ${yamlQuote(repoSource)}`);
return `---\n${fm.join('\n')}\n---\n`;
}
function generatePage(page) {
const srcPath = join(docsRoot, page.source);
const repoSource = posix.join(repoDocsDir, posix.normalize(page.source));
const srcPath = join(sourceRoot, page.source);
if (!existsSync(srcPath)) {
throw new Error(`Missing source doc: docs/${page.source} (referenced by slug "${page.slug}")`);
throw new Error(`Missing source doc: ${repoSource} (referenced by slug "${page.slug}")`);
}
const raw = readFileSync(srcPath, 'utf8');
const fallbackTitle = page.slug.split('/').pop().replace(/-/g, ' ');
const { title, rest } = extractTitle(raw, fallbackTitle);
const description = extractDescription(rest);
const body = rewriteLinks(rest, posix.normalize(page.source));
const { quote, rest: dequoted } = extractLeadingQuote(rest);
const description = page.description ?? (quote || extractDescription(dequoted));
const body = rewriteLinks(dequoted, posix.normalize(page.source));
const frontmatter = buildFrontmatter({
title,
description,
icon: page.icon,
source: posix.normalize(page.source),
repoSource,
});
const outPath = join(outRoot, `${page.slug}.md`);
mkdirSync(dirname(outPath), { recursive: true });
writeFileSync(outPath, `${frontmatter}\n${body.replace(/\s*$/, '')}\n`, 'utf8');
writeOutputFile(outPath, `${frontmatter}\n${body.replace(/\s*$/, '')}\n`);
return outPath;
}
// meta.json for the docs root: labeled section separators + page slugs, with
// the reference folder inserted as a single entry.
// meta.json for the docs root: labeled section separators + page slugs. A
// folder entry contributes its folder name; the folder's own meta.json
// (written below) labels it and orders its pages.
function writeRootMeta() {
const items = [];
for (const section of sections) {
items.push(`---${section.label}---`);
if (section.folder) {
items.push(section.folder);
} else {
for (const page of section.pages) items.push(page.slug);
}
for (const entry of section.pages) items.push(entry.folder ?? entry.slug);
}
const meta = { title: 'Documentation', root: true, pages: items };
writeFileSync(join(outRoot, 'meta.json'), `${JSON.stringify(meta, null, 2)}\n`, 'utf8');
writeOutputFile(join(outRoot, 'meta.json'), `${JSON.stringify(meta, null, 2)}\n`);
}
// meta.json for each folder section (e.g. reference/).
function writeFolderMetas() {
for (const section of sections) {
if (!section.folder) continue;
// meta.json for each folder entry: the sidebar renders it as a collapsible
// group (collapsed by default) labeled with the entry's `label`. Folder
// entries nest, so recurse into each folder's pages; a nested folder shows up
// in its parent's `pages` list by its base name.
function writeFolderMetasFor(entries) {
for (const entry of entries) {
if (!entry.folder) continue;
const meta = {
title: section.label,
...(section.icon ? { icon: section.icon } : {}),
pages: section.pages.map((page) => page.slug.split('/').pop()),
title: entry.label,
defaultOpen: entry.defaultOpen ?? false,
pages: entry.pages.map((page) => posix.basename(page.folder ?? page.slug)),
};
const dir = join(outRoot, section.folder);
mkdirSync(dir, { recursive: true });
writeFileSync(join(dir, 'meta.json'), `${JSON.stringify(meta, null, 2)}\n`, 'utf8');
writeOutputFile(
join(outRoot, entry.folder, 'meta.json'),
`${JSON.stringify(meta, null, 2)}\n`
);
writeFolderMetasFor(entry.pages);
}
}
function writeFolderMetas() {
for (const section of sections) writeFolderMetasFor(section.pages);
}
// Diagram images: docs-lab/diagrams/*.png|svg is copied to public/diagrams/
// so the markdown can embed them as /diagrams/<name>.png.
function copyDiagramAssets() {
const srcDir = join(sourceRoot, 'diagrams');
if (!existsSync(srcDir)) return 0;
const outDir = join(websiteRoot, 'public', 'diagrams');
mkdirSync(outDir, { recursive: true });
let count = 0;
for (const name of readdirSync(srcDir)) {
if (!/\.(png|svg)$/i.test(name)) continue;
copyFileSync(join(srcDir, name), join(outDir, name));
count++;
}
return count;
}
function main() {
// Start clean so removed/renamed docs don't leave stale pages behind.
rmSync(outRoot, { recursive: true, force: true });
mkdirSync(outRoot, { recursive: true });
let count = 0;
@@ -177,9 +258,14 @@ function main() {
}
writeRootMeta();
writeFolderMetas();
// Removed/renamed docs must not leave stale pages behind. Deleting only the
// leftovers (rather than starting from an empty dir) keeps the untouched
// files' mtimes stable for the dev watcher.
removeStaleOutputs(outRoot);
const assets = copyDiagramAssets();
const rel = relative(process.cwd(), outRoot);
console.log(`sync-docs: generated ${count} pages from ${docsDir} into ${rel}/`);
console.log(`sync-docs: generated ${count} pages into ${rel}/ (${assets} diagram assets)`);
}
main();
+9 -3
View File
@@ -1,6 +1,9 @@
import { defineConfig, defineDocs } from 'fumadocs-mdx/config';
import { metaSchema, pageSchema } from 'fumadocs-core/source/schema';
import { remarkMdxMermaid } from 'fumadocs-core/mdx-plugins';
import { remarkMdxMermaid, remarkNpm } from 'fumadocs-core/mdx-plugins';
import { remarkGfmAlert } from './lib/remark-gfm-alert';
import { remarkFileSteps } from './lib/remark-file-steps';
import { remarkFaq } from './lib/remark-faq';
import { z } from 'zod';
// You can customize Zod schemas for frontmatter and `meta.json` here
@@ -14,7 +17,7 @@ export const docs = defineDocs({
schema: pageSchema.extend({ githubSource: z.string().optional() }),
postprocess: {
includeProcessedMarkdown: {
mdxAsPlaceholder: ['Mermaid'],
mdxAsPlaceholder: ['Mermaid', 'Callout', 'FileSteps', 'Accordions', 'Accordion'],
},
},
},
@@ -25,6 +28,9 @@ export const docs = defineDocs({
export default defineConfig({
mdxOptions: {
remarkPlugins: [remarkMdxMermaid],
// `npm`-language fences become package-manager tabs (npm/pnpm/yarn/bun) with
// per-tab copy buttons; persist remembers the reader's choice across blocks.
// `remarkGfmAlert` renders GitHub-style `> [!NOTE]` blockquotes as callouts.
remarkPlugins: [remarkMdxMermaid, remarkGfmAlert, remarkFileSteps, remarkFaq, [remarkNpm, { persist: { id: 'package-manager' } }]],
},
});