diff --git a/.agents/skills/draft-openspec-docs/SKILL.md b/.agents/skills/draft-openspec-docs/SKILL.md new file mode 100644 index 0000000000..dcd28cbc7f --- /dev/null +++ b/.agents/skills/draft-openspec-docs/SKILL.md @@ -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. diff --git a/.agents/skills/verify-openspec-docs/SKILL.md b/.agents/skills/verify-openspec-docs/SKILL.md new file mode 100644 index 0000000000..66b81d3b56 --- /dev/null +++ b/.agents/skills/verify-openspec-docs/SKILL.md @@ -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: . Use absolute paths with every tool. + +Read first: +1. /README.md: the page map and standing invariants. +2. /.agents/skills/write-openspec-docs/writing.md: the house writing rules. +3. : review only " | these changed claims: >; 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. diff --git a/.agents/skills/write-openspec-docs/SKILL.md b/.agents/skills/write-openspec-docs/SKILL.md new file mode 100644 index 0000000000..a0290b7cb2 --- /dev/null +++ b/.agents/skills/write-openspec-docs/SKILL.md @@ -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). diff --git a/.agents/skills/write-openspec-docs/full-process.md b/.agents/skills/write-openspec-docs/full-process.md new file mode 100644 index 0000000000..ded76e4086 --- /dev/null +++ b/.agents/skills/write-openspec-docs/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. diff --git a/.agents/skills/write-openspec-docs/writing.md b/.agents/skills/write-openspec-docs/writing.md new file mode 100644 index 0000000000..3daaef5a4f --- /dev/null +++ b/.agents/skills/write-openspec-docs/writing.md @@ -0,0 +1,203 @@ +# 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. +- 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 (``, ``, `
`) 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. diff --git a/docs-lab/README.md b/docs-lab/README.md new file mode 100644 index 0000000000..a12fa1432f --- /dev/null +++ b/docs-lab/README.md @@ -0,0 +1,227 @@ +# 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). + +## 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`, `sources.md`, and `diagrams/`. 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. 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) | OpenSpec gives you and your coding agent a shared, reviewable plan before code is written. | +| [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) | Teach OpenSpec your project in config.yaml: context, rules, and conventions. | +| [Schemas](customize/schemas.md) | Change what the workflows produce: artifact schemas and 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. | + +### 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) | 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 + +| 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 + +| Page | Goal | +|---|---| +| [Migrating from the legacy workflow](help/legacy/migration.md) | Moving from the legacy `/openspec:*` commands to OPSX. | + +## Old 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 + +- 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. + +- `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. diff --git a/docs-lab/customize/overview.md b/docs-lab/customize/overview.md new file mode 100644 index 0000000000..d514ad81c5 --- /dev/null +++ b/docs-lab/customize/overview.md @@ -0,0 +1,13 @@ +# 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 context, rules, and conventions OpenSpec plans with (`config.yaml`) | You have planning conventions specific to your project, like tasks always including Playwright tests | +| [Schemas](schemas.md) | What the workflows produce: the artifacts and their templates | Changes should produce different planning files, sections, or formats | diff --git a/docs-lab/customize/profiles.md b/docs-lab/customize/profiles.md new file mode 100644 index 0000000000..1fe42766ad --- /dev/null +++ b/docs-lab/customize/profiles.md @@ -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. diff --git a/docs-lab/customize/project-config.md b/docs-lab/customize/project-config.md new file mode 100644 index 0000000000..a6a500574d --- /dev/null +++ b/docs-lab/customize/project-config.md @@ -0,0 +1,12 @@ +# Project configuration + +> Teach OpenSpec your project in config.yaml: context, rules, and conventions. + + + +## config.yaml at a glance + +## Context and rules + +## Language and conventions diff --git a/docs-lab/customize/schemas.md b/docs-lab/customize/schemas.md new file mode 100644 index 0000000000..a282f3f808 --- /dev/null +++ b/docs-lab/customize/schemas.md @@ -0,0 +1,17 @@ +# Schemas + +> Change what the workflows produce: artifact schemas and templates. + + + +## How schemas shape artifacts + +## Editing a schema + +## Templates + +## Per-artifact overrides + +## Community schemas diff --git a/docs-lab/customize/skills.md b/docs-lab/customize/skills.md new file mode 100644 index 0000000000..6a54510c97 --- /dev/null +++ b/docs-lab/customize/skills.md @@ -0,0 +1,14 @@ +# Customizing skills + +> Edit the installed skill prompts directly: what you can change, and what update overwrites. + + + +## What you can change + +## What openspec update overwrites + +## Supported alternatives diff --git a/docs-lab/diagrams/option-a-drift.excalidraw b/docs-lab/diagrams/option-a-drift.excalidraw new file mode 100644 index 0000000000..7b1826947d --- /dev/null +++ b/docs-lab/diagrams/option-a-drift.excalidraw @@ -0,0 +1,39 @@ +{ + "type": "excalidraw", + "version": 2, + "source": "https://excalidraw.com", + "appState": { "viewBackgroundColor": "#ffffff", "gridSize": 20 }, + "files": {}, + "elements": [ + { "type": "text", "id": "title_left", "x": 70, "y": 52, "width": 262, "height": 25, "text": "Without a shared plan", "originalText": "Without a shared plan", "fontSize": 20, "fontFamily": 3, "textAlign": "left", "verticalAlign": "top", "strokeColor": "#dc2626", "backgroundColor": "transparent", "fillStyle": "solid", "strokeWidth": 1, "strokeStyle": "solid", "roughness": 0, "opacity": 100, "angle": 0, "seed": 100001, "version": 1, "versionNonce": 100002, "isDeleted": false, "groupIds": [], "boundElements": null, "link": null, "locked": false, "containerId": null, "lineHeight": 1.25 }, + { "type": "text", "id": "title_right", "x": 690, "y": 52, "width": 362, "height": 25, "text": "With a shared, updatable plan", "originalText": "With a shared, updatable plan", "fontSize": 20, "fontFamily": 3, "textAlign": "left", "verticalAlign": "top", "strokeColor": "#047857", "backgroundColor": "transparent", "fillStyle": "solid", "strokeWidth": 1, "strokeStyle": "solid", "roughness": 0, "opacity": 100, "angle": 0, "seed": 100003, "version": 1, "versionNonce": 100004, "isDeleted": false, "groupIds": [], "boundElements": null, "link": null, "locked": false, "containerId": null, "lineHeight": 1.25 }, + { "type": "line", "id": "legend_dash", "x": 694, "y": 96, "width": 36, "height": 0, "strokeColor": "#64748b", "backgroundColor": "transparent", "fillStyle": "solid", "strokeWidth": 2, "strokeStyle": "dashed", "roughness": 0, "opacity": 100, "angle": 0, "seed": 100005, "version": 1, "versionNonce": 100006, "isDeleted": false, "groupIds": [], "boundElements": null, "link": null, "locked": false, "points": [[0, 0], [36, 0]] }, + { "type": "text", "id": "legend_dash_t", "x": 742, "y": 88, "width": 205, "height": 17, "text": "the spec: one shared route", "originalText": "the spec: one shared route", "fontSize": 13, "fontFamily": 3, "textAlign": "left", "verticalAlign": "top", "strokeColor": "#64748b", "backgroundColor": "transparent", "fillStyle": "solid", "strokeWidth": 1, "strokeStyle": "solid", "roughness": 0, "opacity": 100, "angle": 0, "seed": 100007, "version": 1, "versionNonce": 100008, "isDeleted": false, "groupIds": [], "boundElements": null, "link": null, "locked": false, "containerId": null, "lineHeight": 1.25 }, + { "type": "ellipse", "id": "legend_dot", "x": 706, "y": 112, "width": 10, "height": 10, "strokeColor": "#3b82f6", "backgroundColor": "#3b82f6", "fillStyle": "solid", "strokeWidth": 1, "strokeStyle": "solid", "roughness": 0, "opacity": 100, "angle": 0, "seed": 100009, "version": 1, "versionNonce": 100010, "isDeleted": false, "groupIds": [], "boundElements": null, "link": null, "locked": false }, + { "type": "text", "id": "legend_dot_t", "x": 742, "y": 108, "width": 195, "height": 17, "text": "correction: review / sync", "originalText": "correction: review / sync", "fontSize": 13, "fontFamily": 3, "textAlign": "left", "verticalAlign": "top", "strokeColor": "#64748b", "backgroundColor": "transparent", "fillStyle": "solid", "strokeWidth": 1, "strokeStyle": "solid", "roughness": 0, "opacity": 100, "angle": 0, "seed": 100011, "version": 1, "versionNonce": 100012, "isDeleted": false, "groupIds": [], "boundElements": null, "link": null, "locked": false, "containerId": null, "lineHeight": 1.25 }, + { "type": "ellipse", "id": "start_l", "x": 86, "y": 286, "width": 16, "height": 16, "strokeColor": "#c2410c", "backgroundColor": "#fed7aa", "fillStyle": "solid", "strokeWidth": 2, "strokeStyle": "solid", "roughness": 0, "opacity": 100, "angle": 0, "seed": 100013, "version": 1, "versionNonce": 100014, "isDeleted": false, "groupIds": [], "boundElements": null, "link": null, "locked": false }, + { "type": "ellipse", "id": "goal_l", "x": 470, "y": 118, "width": 28, "height": 28, "strokeColor": "#047857", "backgroundColor": "transparent", "fillStyle": "solid", "strokeWidth": 2, "strokeStyle": "dashed", "roughness": 0, "opacity": 100, "angle": 0, "seed": 100017, "version": 1, "versionNonce": 100018, "isDeleted": false, "groupIds": [], "boundElements": null, "link": null, "locked": false }, + { "type": "text", "id": "goal_l_t", "x": 452, "y": 92, "width": 70, "height": 18, "text": "the goal", "originalText": "the goal", "fontSize": 14, "fontFamily": 3, "textAlign": "left", "verticalAlign": "top", "strokeColor": "#047857", "backgroundColor": "transparent", "fillStyle": "solid", "strokeWidth": 1, "strokeStyle": "solid", "roughness": 0, "opacity": 100, "angle": 0, "seed": 100019, "version": 1, "versionNonce": 100020, "isDeleted": false, "groupIds": [], "boundElements": null, "link": null, "locked": false, "containerId": null, "lineHeight": 1.25 }, + { "type": "arrow", "id": "traj_you", "x": 104, "y": 290, "width": 346, "height": 112, "strokeColor": "#1e3a5f", "backgroundColor": "transparent", "fillStyle": "solid", "strokeWidth": 2, "strokeStyle": "solid", "roughness": 0, "opacity": 100, "angle": 0, "seed": 100021, "version": 1, "versionNonce": 100022, "isDeleted": false, "groupIds": [], "boundElements": null, "link": null, "locked": false, "points": [[0, 0], [170, -50], [346, -112]], "startBinding": null, "endBinding": null, "startArrowhead": null, "endArrowhead": "arrow" }, + { "type": "text", "id": "traj_you_t", "x": 458, "y": 170, "width": 30, "height": 17, "text": "you", "originalText": "you", "fontSize": 13, "fontFamily": 3, "textAlign": "left", "verticalAlign": "top", "strokeColor": "#1e3a5f", "backgroundColor": "transparent", "fillStyle": "solid", "strokeWidth": 1, "strokeStyle": "solid", "roughness": 0, "opacity": 100, "angle": 0, "seed": 100023, "version": 1, "versionNonce": 100024, "isDeleted": false, "groupIds": [], "boundElements": null, "link": null, "locked": false, "containerId": null, "lineHeight": 1.25 }, + { "type": "arrow", "id": "traj_a1", "x": 104, "y": 288, "width": 350, "height": 228, "strokeColor": "#6d28d9", "backgroundColor": "transparent", "fillStyle": "solid", "strokeWidth": 2, "strokeStyle": "solid", "roughness": 0, "opacity": 100, "angle": 0, "seed": 100025, "version": 1, "versionNonce": 100026, "isDeleted": false, "groupIds": [], "boundElements": null, "link": null, "locked": false, "points": [[0, 0], [160, -90], [350, -228]], "startBinding": null, "endBinding": null, "startArrowhead": null, "endArrowhead": "arrow" }, + { "type": "text", "id": "traj_a1_t", "x": 462, "y": 44, "width": 130, "height": 17, "text": "agent, session 1", "originalText": "agent, session 1", "fontSize": 13, "fontFamily": 3, "textAlign": "left", "verticalAlign": "top", "strokeColor": "#6d28d9", "backgroundColor": "transparent", "fillStyle": "solid", "strokeWidth": 1, "strokeStyle": "solid", "roughness": 0, "opacity": 100, "angle": 0, "seed": 100027, "version": 1, "versionNonce": 100028, "isDeleted": false, "groupIds": [], "boundElements": null, "link": null, "locked": false, "containerId": null, "lineHeight": 1.25 }, + { "type": "arrow", "id": "traj_a2", "x": 104, "y": 296, "width": 360, "height": 58, "strokeColor": "#6d28d9", "backgroundColor": "transparent", "fillStyle": "solid", "strokeWidth": 2, "strokeStyle": "solid", "roughness": 0, "opacity": 100, "angle": 0, "seed": 100029, "version": 1, "versionNonce": 100030, "isDeleted": false, "groupIds": [], "boundElements": null, "link": null, "locked": false, "points": [[0, 0], [180, 12], [360, 58]], "startBinding": null, "endBinding": null, "startArrowhead": null, "endArrowhead": "arrow" }, + { "type": "text", "id": "traj_a2_t", "x": 470, "y": 346, "width": 130, "height": 17, "text": "agent, session 2", "originalText": "agent, session 2", "fontSize": 13, "fontFamily": 3, "textAlign": "left", "verticalAlign": "top", "strokeColor": "#6d28d9", "backgroundColor": "transparent", "fillStyle": "solid", "strokeWidth": 1, "strokeStyle": "solid", "roughness": 0, "opacity": 100, "angle": 0, "seed": 100031, "version": 1, "versionNonce": 100032, "isDeleted": false, "groupIds": [], "boundElements": null, "link": null, "locked": false, "containerId": null, "lineHeight": 1.25 }, + { "type": "arrow", "id": "traj_tm", "x": 102, "y": 298, "width": 340, "height": 166, "strokeColor": "#64748b", "backgroundColor": "transparent", "fillStyle": "solid", "strokeWidth": 2, "strokeStyle": "solid", "roughness": 0, "opacity": 100, "angle": 0, "seed": 100033, "version": 1, "versionNonce": 100034, "isDeleted": false, "groupIds": [], "boundElements": null, "link": null, "locked": false, "points": [[0, 0], [150, 62], [340, 166]], "startBinding": null, "endBinding": null, "startArrowhead": null, "endArrowhead": "arrow" }, + { "type": "text", "id": "traj_tm_t", "x": 450, "y": 456, "width": 75, "height": 17, "text": "teammate", "originalText": "teammate", "fontSize": 13, "fontFamily": 3, "textAlign": "left", "verticalAlign": "top", "strokeColor": "#64748b", "backgroundColor": "transparent", "fillStyle": "solid", "strokeWidth": 1, "strokeStyle": "solid", "roughness": 0, "opacity": 100, "angle": 0, "seed": 100035, "version": 1, "versionNonce": 100036, "isDeleted": false, "groupIds": [], "boundElements": null, "link": null, "locked": false, "containerId": null, "lineHeight": 1.25 }, + { "type": "text", "id": "cap_l", "x": 70, "y": 500, "width": 480, "height": 17, "text": "aligned at the start, small differences compound each session", "originalText": "aligned at the start, small differences compound each session", "fontSize": 13, "fontFamily": 3, "textAlign": "left", "verticalAlign": "top", "strokeColor": "#64748b", "backgroundColor": "transparent", "fillStyle": "solid", "strokeWidth": 1, "strokeStyle": "solid", "roughness": 0, "opacity": 100, "angle": 0, "seed": 100037, "version": 1, "versionNonce": 100038, "isDeleted": false, "groupIds": [], "boundElements": null, "link": null, "locked": false, "containerId": null, "lineHeight": 1.25 }, + { "type": "line", "id": "divider", "x": 620, "y": 60, "width": 0, "height": 440, "strokeColor": "#64748b", "backgroundColor": "transparent", "fillStyle": "solid", "strokeWidth": 1, "strokeStyle": "dashed", "roughness": 0, "opacity": 100, "angle": 0, "seed": 100039, "version": 1, "versionNonce": 100040, "isDeleted": false, "groupIds": [], "boundElements": null, "link": null, "locked": false, "points": [[0, 0], [0, 440]] }, + { "type": "ellipse", "id": "start_r", "x": 706, "y": 286, "width": 16, "height": 16, "strokeColor": "#c2410c", "backgroundColor": "#fed7aa", "fillStyle": "solid", "strokeWidth": 2, "strokeStyle": "solid", "roughness": 0, "opacity": 100, "angle": 0, "seed": 100041, "version": 1, "versionNonce": 100042, "isDeleted": false, "groupIds": [], "boundElements": null, "link": null, "locked": false }, + { "type": "line", "id": "spec_line", "x": 722, "y": 292, "width": 360, "height": 100, "strokeColor": "#64748b", "backgroundColor": "transparent", "fillStyle": "solid", "strokeWidth": 2, "strokeStyle": "dashed", "roughness": 0, "opacity": 100, "angle": 0, "seed": 100043, "version": 1, "versionNonce": 100044, "isDeleted": false, "groupIds": [], "boundElements": null, "link": null, "locked": false, "points": [[0, 0], [360, -100]] }, + { "type": "ellipse", "id": "corr1", "x": 836, "y": 252, "width": 10, "height": 10, "strokeColor": "#3b82f6", "backgroundColor": "#3b82f6", "fillStyle": "solid", "strokeWidth": 1, "strokeStyle": "solid", "roughness": 0, "opacity": 100, "angle": 0, "seed": 100045, "version": 1, "versionNonce": 100046, "isDeleted": false, "groupIds": [], "boundElements": null, "link": null, "locked": false }, + { "type": "ellipse", "id": "corr2", "x": 950, "y": 220, "width": 10, "height": 10, "strokeColor": "#3b82f6", "backgroundColor": "#3b82f6", "fillStyle": "solid", "strokeWidth": 1, "strokeStyle": "solid", "roughness": 0, "opacity": 100, "angle": 0, "seed": 100047, "version": 1, "versionNonce": 100048, "isDeleted": false, "groupIds": [], "boundElements": null, "link": null, "locked": false }, + { "type": "arrow", "id": "wob_you", "x": 722, "y": 290, "width": 358, "height": 100, "strokeColor": "#1e3a5f", "backgroundColor": "transparent", "fillStyle": "solid", "strokeWidth": 2, "strokeStyle": "solid", "roughness": 0, "opacity": 100, "angle": 0, "seed": 100049, "version": 1, "versionNonce": 100050, "isDeleted": false, "groupIds": [], "boundElements": null, "link": null, "locked": false, "points": [[0, 0], [90, -45], [180, -30], [270, -85], [358, -100]], "startBinding": null, "endBinding": null, "startArrowhead": null, "endArrowhead": "arrow" }, + { "type": "arrow", "id": "wob_a1", "x": 722, "y": 292, "width": 360, "height": 98, "strokeColor": "#6d28d9", "backgroundColor": "transparent", "fillStyle": "solid", "strokeWidth": 2, "strokeStyle": "solid", "roughness": 0, "opacity": 100, "angle": 0, "seed": 100051, "version": 1, "versionNonce": 100052, "isDeleted": false, "groupIds": [], "boundElements": null, "link": null, "locked": false, "points": [[0, 0], [90, -10], [180, -62], [270, -55], [360, -98]], "startBinding": null, "endBinding": null, "startArrowhead": null, "endArrowhead": "arrow" }, + { "type": "arrow", "id": "wob_a2", "x": 722, "y": 294, "width": 356, "height": 104, "strokeColor": "#6d28d9", "backgroundColor": "transparent", "fillStyle": "solid", "strokeWidth": 2, "strokeStyle": "solid", "roughness": 0, "opacity": 100, "angle": 0, "seed": 100053, "version": 1, "versionNonce": 100054, "isDeleted": false, "groupIds": [], "boundElements": null, "link": null, "locked": false, "points": [[0, 0], [80, -38], [170, -16], [260, -78], [356, -104]], "startBinding": null, "endBinding": null, "startArrowhead": null, "endArrowhead": "arrow" }, + { "type": "arrow", "id": "wob_tm", "x": 720, "y": 296, "width": 354, "height": 106, "strokeColor": "#64748b", "backgroundColor": "transparent", "fillStyle": "solid", "strokeWidth": 2, "strokeStyle": "solid", "roughness": 0, "opacity": 100, "angle": 0, "seed": 100055, "version": 1, "versionNonce": 100056, "isDeleted": false, "groupIds": [], "boundElements": null, "link": null, "locked": false, "points": [[0, 0], [100, -58], [190, -46], [280, -62], [354, -106]], "startBinding": null, "endBinding": null, "startArrowhead": null, "endArrowhead": "arrow" }, + { "type": "ellipse", "id": "goal_r", "x": 1076, "y": 168, "width": 26, "height": 26, "strokeColor": "#047857", "backgroundColor": "#a7f3d0", "fillStyle": "solid", "strokeWidth": 2, "strokeStyle": "solid", "roughness": 0, "opacity": 100, "angle": 0, "seed": 100057, "version": 1, "versionNonce": 100058, "isDeleted": false, "groupIds": [], "boundElements": null, "link": null, "locked": false }, + { "type": "text", "id": "goal_r_t", "x": 1058, "y": 140, "width": 70, "height": 18, "text": "the goal", "originalText": "the goal", "fontSize": 14, "fontFamily": 3, "textAlign": "left", "verticalAlign": "top", "strokeColor": "#047857", "backgroundColor": "transparent", "fillStyle": "solid", "strokeWidth": 1, "strokeStyle": "solid", "roughness": 0, "opacity": 100, "angle": 0, "seed": 100059, "version": 1, "versionNonce": 100060, "isDeleted": false, "groupIds": [], "boundElements": null, "link": null, "locked": false, "containerId": null, "lineHeight": 1.25 }, + { "type": "text", "id": "cap_r", "x": 690, "y": 500, "width": 300, "height": 17, "text": "drift is corrected before it compounds", "originalText": "drift is corrected before it compounds", "fontSize": 13, "fontFamily": 3, "textAlign": "left", "verticalAlign": "top", "strokeColor": "#64748b", "backgroundColor": "transparent", "fillStyle": "solid", "strokeWidth": 1, "strokeStyle": "solid", "roughness": 0, "opacity": 100, "angle": 0, "seed": 100061, "version": 1, "versionNonce": 100062, "isDeleted": false, "groupIds": [], "boundElements": null, "link": null, "locked": false, "containerId": null, "lineHeight": 1.25 } + ] +} diff --git a/docs-lab/diagrams/option-a-drift.png b/docs-lab/diagrams/option-a-drift.png new file mode 100644 index 0000000000..671e325348 Binary files /dev/null and b/docs-lab/diagrams/option-a-drift.png differ diff --git a/docs-lab/diagrams/option-b-shared-map.excalidraw b/docs-lab/diagrams/option-b-shared-map.excalidraw new file mode 100644 index 0000000000..167221c0a2 --- /dev/null +++ b/docs-lab/diagrams/option-b-shared-map.excalidraw @@ -0,0 +1,42 @@ +{ + "type": "excalidraw", + "version": 2, + "source": "https://excalidraw.com", + "appState": { "viewBackgroundColor": "#ffffff", "gridSize": 20 }, + "files": {}, + "elements": [ + { "type": "text", "id": "title_left", "x": 70, "y": 52, "width": 350, "height": 25, "text": "Everyone navigates from memory", "originalText": "Everyone navigates from memory", "fontSize": 20, "fontFamily": 3, "textAlign": "left", "verticalAlign": "top", "strokeColor": "#dc2626", "backgroundColor": "transparent", "fillStyle": "solid", "strokeWidth": 1, "strokeStyle": "solid", "roughness": 0, "opacity": 100, "angle": 0, "seed": 200001, "version": 1, "versionNonce": 200002, "isDeleted": false, "groupIds": [], "boundElements": null, "link": null, "locked": false, "containerId": null, "lineHeight": 1.25 }, + { "type": "rectangle", "id": "card1", "x": 70, "y": 110, "width": 200, "height": 140, "strokeColor": "#1e3a5f", "backgroundColor": "transparent", "fillStyle": "solid", "strokeWidth": 2, "strokeStyle": "solid", "roughness": 0, "opacity": 100, "angle": 0, "seed": 200003, "version": 1, "versionNonce": 200004, "isDeleted": false, "groupIds": [], "boundElements": null, "link": null, "locked": false, "roundness": { "type": 3 } }, + { "type": "line", "id": "card1_road1", "x": 82, "y": 180, "width": 176, "height": 0, "strokeColor": "#93c5fd", "backgroundColor": "transparent", "fillStyle": "solid", "strokeWidth": 1, "strokeStyle": "solid", "roughness": 0, "opacity": 100, "angle": 0, "seed": 200005, "version": 1, "versionNonce": 200006, "isDeleted": false, "groupIds": [], "boundElements": null, "link": null, "locked": false, "points": [[0, 0], [176, 0]] }, + { "type": "line", "id": "card1_road2", "x": 150, "y": 122, "width": 30, "height": 116, "strokeColor": "#93c5fd", "backgroundColor": "transparent", "fillStyle": "solid", "strokeWidth": 1, "strokeStyle": "solid", "roughness": 0, "opacity": 100, "angle": 0, "seed": 200007, "version": 1, "versionNonce": 200008, "isDeleted": false, "groupIds": [], "boundElements": null, "link": null, "locked": false, "points": [[0, 0], [30, 116]] }, + { "type": "ellipse", "id": "card1_pin", "x": 224, "y": 132, "width": 14, "height": 14, "strokeColor": "#b91c1c", "backgroundColor": "#fecaca", "fillStyle": "solid", "strokeWidth": 2, "strokeStyle": "solid", "roughness": 0, "opacity": 100, "angle": 0, "seed": 200009, "version": 1, "versionNonce": 200010, "isDeleted": false, "groupIds": [], "boundElements": null, "link": null, "locked": false }, + { "type": "text", "id": "card1_t", "x": 70, "y": 258, "width": 160, "height": 17, "text": "what you remember", "originalText": "what you remember", "fontSize": 13, "fontFamily": 3, "textAlign": "left", "verticalAlign": "top", "strokeColor": "#64748b", "backgroundColor": "transparent", "fillStyle": "solid", "strokeWidth": 1, "strokeStyle": "solid", "roughness": 0, "opacity": 100, "angle": 0, "seed": 200011, "version": 1, "versionNonce": 200012, "isDeleted": false, "groupIds": [], "boundElements": null, "link": null, "locked": false, "containerId": null, "lineHeight": 1.25 }, + { "type": "rectangle", "id": "card2", "x": 320, "y": 110, "width": 200, "height": 140, "strokeColor": "#1e3a5f", "backgroundColor": "transparent", "fillStyle": "solid", "strokeWidth": 2, "strokeStyle": "solid", "roughness": 0, "opacity": 100, "angle": 0, "seed": 200013, "version": 1, "versionNonce": 200014, "isDeleted": false, "groupIds": [], "boundElements": null, "link": null, "locked": false, "roundness": { "type": 3 } }, + { "type": "line", "id": "card2_road1", "x": 332, "y": 160, "width": 176, "height": 0, "strokeColor": "#93c5fd", "backgroundColor": "transparent", "fillStyle": "solid", "strokeWidth": 1, "strokeStyle": "solid", "roughness": 0, "opacity": 100, "angle": 0, "seed": 200015, "version": 1, "versionNonce": 200016, "isDeleted": false, "groupIds": [], "boundElements": null, "link": null, "locked": false, "points": [[0, 0], [176, 0]] }, + { "type": "line", "id": "card2_road2", "x": 420, "y": 122, "width": -20, "height": 116, "strokeColor": "#93c5fd", "backgroundColor": "transparent", "fillStyle": "solid", "strokeWidth": 1, "strokeStyle": "solid", "roughness": 0, "opacity": 100, "angle": 0, "seed": 200017, "version": 1, "versionNonce": 200018, "isDeleted": false, "groupIds": [], "boundElements": null, "link": null, "locked": false, "points": [[0, 0], [-20, 116]] }, + { "type": "ellipse", "id": "card2_pin", "x": 344, "y": 210, "width": 14, "height": 14, "strokeColor": "#b91c1c", "backgroundColor": "#fecaca", "fillStyle": "solid", "strokeWidth": 2, "strokeStyle": "solid", "roughness": 0, "opacity": 100, "angle": 0, "seed": 200019, "version": 1, "versionNonce": 200020, "isDeleted": false, "groupIds": [], "boundElements": null, "link": null, "locked": false }, + { "type": "text", "id": "card2_t", "x": 320, "y": 258, "width": 210, "height": 17, "text": "the agent's map, session 1", "originalText": "the agent's map, session 1", "fontSize": 13, "fontFamily": 3, "textAlign": "left", "verticalAlign": "top", "strokeColor": "#64748b", "backgroundColor": "transparent", "fillStyle": "solid", "strokeWidth": 1, "strokeStyle": "solid", "roughness": 0, "opacity": 100, "angle": 0, "seed": 200021, "version": 1, "versionNonce": 200022, "isDeleted": false, "groupIds": [], "boundElements": null, "link": null, "locked": false, "containerId": null, "lineHeight": 1.25 }, + { "type": "rectangle", "id": "card3", "x": 195, "y": 310, "width": 200, "height": 140, "strokeColor": "#1e3a5f", "backgroundColor": "transparent", "fillStyle": "solid", "strokeWidth": 2, "strokeStyle": "solid", "roughness": 0, "opacity": 100, "angle": 0, "seed": 200023, "version": 1, "versionNonce": 200024, "isDeleted": false, "groupIds": [], "boundElements": null, "link": null, "locked": false, "roundness": { "type": 3 } }, + { "type": "line", "id": "card3_road1", "x": 207, "y": 400, "width": 176, "height": 0, "strokeColor": "#93c5fd", "backgroundColor": "transparent", "fillStyle": "solid", "strokeWidth": 1, "strokeStyle": "solid", "roughness": 0, "opacity": 100, "angle": 0, "seed": 200025, "version": 1, "versionNonce": 200026, "isDeleted": false, "groupIds": [], "boundElements": null, "link": null, "locked": false, "points": [[0, 0], [176, 0]] }, + { "type": "line", "id": "card3_road2", "x": 280, "y": 322, "width": 24, "height": 116, "strokeColor": "#93c5fd", "backgroundColor": "transparent", "fillStyle": "solid", "strokeWidth": 1, "strokeStyle": "solid", "roughness": 0, "opacity": 100, "angle": 0, "seed": 200027, "version": 1, "versionNonce": 200028, "isDeleted": false, "groupIds": [], "boundElements": null, "link": null, "locked": false, "points": [[0, 0], [24, 116]] }, + { "type": "ellipse", "id": "card3_pin", "x": 352, "y": 416, "width": 14, "height": 14, "strokeColor": "#b91c1c", "backgroundColor": "#fecaca", "fillStyle": "solid", "strokeWidth": 2, "strokeStyle": "solid", "roughness": 0, "opacity": 100, "angle": 0, "seed": 200029, "version": 1, "versionNonce": 200030, "isDeleted": false, "groupIds": [], "boundElements": null, "link": null, "locked": false }, + { "type": "text", "id": "card3_t", "x": 195, "y": 458, "width": 210, "height": 17, "text": "the agent's map, session 2", "originalText": "the agent's map, session 2", "fontSize": 13, "fontFamily": 3, "textAlign": "left", "verticalAlign": "top", "strokeColor": "#64748b", "backgroundColor": "transparent", "fillStyle": "solid", "strokeWidth": 1, "strokeStyle": "solid", "roughness": 0, "opacity": 100, "angle": 0, "seed": 200031, "version": 1, "versionNonce": 200032, "isDeleted": false, "groupIds": [], "boundElements": null, "link": null, "locked": false, "containerId": null, "lineHeight": 1.25 }, + { "type": "text", "id": "cap_l", "x": 70, "y": 505, "width": 330, "height": 17, "text": "the goal moved; nobody's map did", "originalText": "the goal moved; nobody's map did", "fontSize": 13, "fontFamily": 3, "textAlign": "left", "verticalAlign": "top", "strokeColor": "#dc2626", "backgroundColor": "transparent", "fillStyle": "solid", "strokeWidth": 1, "strokeStyle": "solid", "roughness": 0, "opacity": 100, "angle": 0, "seed": 200033, "version": 1, "versionNonce": 200034, "isDeleted": false, "groupIds": [], "boundElements": null, "link": null, "locked": false, "containerId": null, "lineHeight": 1.25 }, + { "type": "line", "id": "divider", "x": 590, "y": 60, "width": 0, "height": 460, "strokeColor": "#64748b", "backgroundColor": "transparent", "fillStyle": "solid", "strokeWidth": 1, "strokeStyle": "dashed", "roughness": 0, "opacity": 100, "angle": 0, "seed": 200035, "version": 1, "versionNonce": 200036, "isDeleted": false, "groupIds": [], "boundElements": null, "link": null, "locked": false, "points": [[0, 0], [0, 460]] }, + { "type": "text", "id": "title_right", "x": 650, "y": 52, "width": 180, "height": 25, "text": "One shared map", "originalText": "One shared map", "fontSize": 20, "fontFamily": 3, "textAlign": "left", "verticalAlign": "top", "strokeColor": "#047857", "backgroundColor": "transparent", "fillStyle": "solid", "strokeWidth": 1, "strokeStyle": "solid", "roughness": 0, "opacity": 100, "angle": 0, "seed": 200037, "version": 1, "versionNonce": 200038, "isDeleted": false, "groupIds": [], "boundElements": null, "link": null, "locked": false, "containerId": null, "lineHeight": 1.25 }, + { "type": "rectangle", "id": "bigmap", "x": 650, "y": 100, "width": 480, "height": 340, "strokeColor": "#1e3a5f", "backgroundColor": "transparent", "fillStyle": "solid", "strokeWidth": 2, "strokeStyle": "solid", "roughness": 0, "opacity": 100, "angle": 0, "seed": 200039, "version": 1, "versionNonce": 200040, "isDeleted": false, "groupIds": [], "boundElements": null, "link": null, "locked": false, "roundness": { "type": 3 } }, + { "type": "line", "id": "bigmap_road1", "x": 662, "y": 200, "width": 456, "height": 0, "strokeColor": "#93c5fd", "backgroundColor": "transparent", "fillStyle": "solid", "strokeWidth": 1, "strokeStyle": "solid", "roughness": 0, "opacity": 100, "angle": 0, "seed": 200041, "version": 1, "versionNonce": 200042, "isDeleted": false, "groupIds": [], "boundElements": null, "link": null, "locked": false, "points": [[0, 0], [456, 0]] }, + { "type": "line", "id": "bigmap_road2", "x": 900, "y": 112, "width": -40, "height": 316, "strokeColor": "#93c5fd", "backgroundColor": "transparent", "fillStyle": "solid", "strokeWidth": 1, "strokeStyle": "solid", "roughness": 0, "opacity": 100, "angle": 0, "seed": 200043, "version": 1, "versionNonce": 200044, "isDeleted": false, "groupIds": [], "boundElements": null, "link": null, "locked": false, "points": [[0, 0], [-40, 316]] }, + { "type": "ellipse", "id": "start_dot", "x": 690, "y": 380, "width": 16, "height": 16, "strokeColor": "#c2410c", "backgroundColor": "#fed7aa", "fillStyle": "solid", "strokeWidth": 2, "strokeStyle": "solid", "roughness": 0, "opacity": 100, "angle": 0, "seed": 200045, "version": 1, "versionNonce": 200046, "isDeleted": false, "groupIds": [], "boundElements": null, "link": null, "locked": false }, + { "type": "line", "id": "route_travelled", "x": 706, "y": 384, "width": 144, "height": 122, "strokeColor": "#1e3a5f", "backgroundColor": "transparent", "fillStyle": "solid", "strokeWidth": 3, "strokeStyle": "solid", "roughness": 0, "opacity": 100, "angle": 0, "seed": 200047, "version": 1, "versionNonce": 200048, "isDeleted": false, "groupIds": [], "boundElements": null, "link": null, "locked": false, "points": [[0, 0], [70, -70], [144, -122]] }, + { "type": "line", "id": "route_old", "x": 850, "y": 262, "width": 150, "height": 96, "strokeColor": "#64748b", "backgroundColor": "transparent", "fillStyle": "solid", "strokeWidth": 2, "strokeStyle": "dashed", "roughness": 0, "opacity": 100, "angle": 0, "seed": 200049, "version": 1, "versionNonce": 200050, "isDeleted": false, "groupIds": [], "boundElements": null, "link": null, "locked": false, "points": [[0, 0], [150, -96]] }, + { "type": "ellipse", "id": "pin_old", "x": 996, "y": 152, "width": 16, "height": 16, "strokeColor": "#64748b", "backgroundColor": "transparent", "fillStyle": "solid", "strokeWidth": 2, "strokeStyle": "dashed", "roughness": 0, "opacity": 100, "angle": 0, "seed": 200051, "version": 1, "versionNonce": 200052, "isDeleted": false, "groupIds": [], "boundElements": null, "link": null, "locked": false }, + { "type": "text", "id": "pin_old_t", "x": 1020, "y": 150, "width": 90, "height": 17, "text": "v1 goal", "originalText": "v1 goal", "fontSize": 13, "fontFamily": 3, "textAlign": "left", "verticalAlign": "top", "strokeColor": "#64748b", "backgroundColor": "transparent", "fillStyle": "solid", "strokeWidth": 1, "strokeStyle": "solid", "roughness": 0, "opacity": 100, "angle": 0, "seed": 200053, "version": 1, "versionNonce": 200054, "isDeleted": false, "groupIds": [], "boundElements": null, "link": null, "locked": false, "containerId": null, "lineHeight": 1.25 }, + { "type": "arrow", "id": "route_new", "x": 850, "y": 262, "width": 180, "height": 62, "strokeColor": "#1e3a5f", "backgroundColor": "transparent", "fillStyle": "solid", "strokeWidth": 3, "strokeStyle": "solid", "roughness": 0, "opacity": 100, "angle": 0, "seed": 200055, "version": 1, "versionNonce": 200056, "isDeleted": false, "groupIds": [], "boundElements": null, "link": null, "locked": false, "points": [[0, 0], [90, 20], [180, 62]], "startBinding": null, "endBinding": null, "startArrowhead": null, "endArrowhead": "arrow" }, + { "type": "ellipse", "id": "pin_new", "x": 1030, "y": 318, "width": 18, "height": 18, "strokeColor": "#047857", "backgroundColor": "#a7f3d0", "fillStyle": "solid", "strokeWidth": 2, "strokeStyle": "solid", "roughness": 0, "opacity": 100, "angle": 0, "seed": 200057, "version": 1, "versionNonce": 200058, "isDeleted": false, "groupIds": [], "boundElements": null, "link": null, "locked": false }, + { "type": "text", "id": "pin_new_t", "x": 1030, "y": 344, "width": 90, "height": 17, "text": "v2 goal", "originalText": "v2 goal", "fontSize": 13, "fontFamily": 3, "textAlign": "left", "verticalAlign": "top", "strokeColor": "#047857", "backgroundColor": "transparent", "fillStyle": "solid", "strokeWidth": 1, "strokeStyle": "solid", "roughness": 0, "opacity": 100, "angle": 0, "seed": 200059, "version": 1, "versionNonce": 200060, "isDeleted": false, "groupIds": [], "boundElements": null, "link": null, "locked": false, "containerId": null, "lineHeight": 1.25 }, + { "type": "text", "id": "reroute_t", "x": 736, "y": 236, "width": 200, "height": 17, "text": "goal changed: reroute", "originalText": "goal changed: reroute", "fontSize": 13, "fontFamily": 3, "textAlign": "left", "verticalAlign": "top", "strokeColor": "#b45309", "backgroundColor": "transparent", "fillStyle": "solid", "strokeWidth": 1, "strokeStyle": "solid", "roughness": 0, "opacity": 100, "angle": 0, "seed": 200061, "version": 1, "versionNonce": 200062, "isDeleted": false, "groupIds": [], "boundElements": null, "link": null, "locked": false, "containerId": null, "lineHeight": 1.25 }, + { "type": "text", "id": "cap_r", "x": 650, "y": 460, "width": 470, "height": 17, "text": "you, the agent (every session), your team: one map", "originalText": "you, the agent (every session), your team: one map", "fontSize": 13, "fontFamily": 3, "textAlign": "left", "verticalAlign": "top", "strokeColor": "#64748b", "backgroundColor": "transparent", "fillStyle": "solid", "strokeWidth": 1, "strokeStyle": "solid", "roughness": 0, "opacity": 100, "angle": 0, "seed": 200063, "version": 1, "versionNonce": 200064, "isDeleted": false, "groupIds": [], "boundElements": null, "link": null, "locked": false, "containerId": null, "lineHeight": 1.25 }, + { "type": "text", "id": "cap_r2", "x": 650, "y": 505, "width": 400, "height": 17, "text": "update the goal once; everyone reroutes", "originalText": "update the goal once; everyone reroutes", "fontSize": 13, "fontFamily": 3, "textAlign": "left", "verticalAlign": "top", "strokeColor": "#047857", "backgroundColor": "transparent", "fillStyle": "solid", "strokeWidth": 1, "strokeStyle": "solid", "roughness": 0, "opacity": 100, "angle": 0, "seed": 200065, "version": 1, "versionNonce": 200066, "isDeleted": false, "groupIds": [], "boundElements": null, "link": null, "locked": false, "containerId": null, "lineHeight": 1.25 } + ] +} diff --git a/docs-lab/diagrams/option-b-shared-map.png b/docs-lab/diagrams/option-b-shared-map.png new file mode 100644 index 0000000000..138b1075bd Binary files /dev/null and b/docs-lab/diagrams/option-b-shared-map.png differ diff --git a/docs-lab/diagrams/option-c-control-loop.excalidraw b/docs-lab/diagrams/option-c-control-loop.excalidraw new file mode 100644 index 0000000000..599dd3b850 --- /dev/null +++ b/docs-lab/diagrams/option-c-control-loop.excalidraw @@ -0,0 +1,40 @@ +{ + "type": "excalidraw", + "version": 2, + "source": "https://esm.sh", + "appState": { "viewBackgroundColor": "#ffffff", "gridSize": 20 }, + "files": {}, + "elements": [ + { "type": "text", "id": "title_open", "x": 70, "y": 50, "width": 120, "height": 25, "text": "Open loop", "originalText": "Open loop", "fontSize": 20, "fontFamily": 3, "textAlign": "left", "verticalAlign": "top", "strokeColor": "#dc2626", "backgroundColor": "transparent", "fillStyle": "solid", "strokeWidth": 1, "strokeStyle": "solid", "roughness": 0, "opacity": 100, "angle": 0, "seed": 300001, "version": 1, "versionNonce": 300002, "isDeleted": false, "groupIds": [], "boundElements": null, "link": null, "locked": false, "containerId": null, "lineHeight": 1.25 }, + { "type": "rectangle", "id": "prompt_box", "x": 70, "y": 100, "width": 150, "height": 60, "strokeColor": "#c2410c", "backgroundColor": "#fed7aa", "fillStyle": "solid", "strokeWidth": 2, "strokeStyle": "solid", "roughness": 0, "opacity": 100, "angle": 0, "seed": 300003, "version": 1, "versionNonce": 300004, "isDeleted": false, "groupIds": [], "boundElements": [{ "id": "prompt_t", "type": "text" }], "link": null, "locked": false, "roundness": { "type": 3 } }, + { "type": "text", "id": "prompt_t", "x": 100, "y": 118, "width": 90, "height": 25, "text": "prompt", "originalText": "prompt", "fontSize": 16, "fontFamily": 3, "textAlign": "center", "verticalAlign": "middle", "strokeColor": "#374151", "backgroundColor": "transparent", "fillStyle": "solid", "strokeWidth": 1, "strokeStyle": "solid", "roughness": 0, "opacity": 100, "angle": 0, "seed": 300005, "version": 1, "versionNonce": 300006, "isDeleted": false, "groupIds": [], "boundElements": null, "link": null, "locked": false, "containerId": "prompt_box", "lineHeight": 1.25 }, + { "type": "rectangle", "id": "agent_box1", "x": 300, "y": 100, "width": 150, "height": 60, "strokeColor": "#6d28d9", "backgroundColor": "#ddd6fe", "fillStyle": "solid", "strokeWidth": 2, "strokeStyle": "solid", "roughness": 0, "opacity": 100, "angle": 0, "seed": 300007, "version": 1, "versionNonce": 300008, "isDeleted": false, "groupIds": [], "boundElements": [{ "id": "agent_t1", "type": "text" }], "link": null, "locked": false, "roundness": { "type": 3 } }, + { "type": "text", "id": "agent_t1", "x": 330, "y": 118, "width": 90, "height": 25, "text": "agent", "originalText": "agent", "fontSize": 16, "fontFamily": 3, "textAlign": "center", "verticalAlign": "middle", "strokeColor": "#374151", "backgroundColor": "transparent", "fillStyle": "solid", "strokeWidth": 1, "strokeStyle": "solid", "roughness": 0, "opacity": 100, "angle": 0, "seed": 300009, "version": 1, "versionNonce": 300010, "isDeleted": false, "groupIds": [], "boundElements": null, "link": null, "locked": false, "containerId": "agent_box1", "lineHeight": 1.25 }, + { "type": "rectangle", "id": "code_box1", "x": 530, "y": 100, "width": 150, "height": 60, "strokeColor": "#1e3a5f", "backgroundColor": "#3b82f6", "fillStyle": "solid", "strokeWidth": 2, "strokeStyle": "solid", "roughness": 0, "opacity": 100, "angle": 0, "seed": 300011, "version": 1, "versionNonce": 300012, "isDeleted": false, "groupIds": [], "boundElements": [{ "id": "code_t1", "type": "text" }], "link": null, "locked": false, "roundness": { "type": 3 } }, + { "type": "text", "id": "code_t1", "x": 560, "y": 118, "width": 90, "height": 25, "text": "code", "originalText": "code", "fontSize": 16, "fontFamily": 3, "textAlign": "center", "verticalAlign": "middle", "strokeColor": "#ffffff", "backgroundColor": "transparent", "fillStyle": "solid", "strokeWidth": 1, "strokeStyle": "solid", "roughness": 0, "opacity": 100, "angle": 0, "seed": 300013, "version": 1, "versionNonce": 300014, "isDeleted": false, "groupIds": [], "boundElements": null, "link": null, "locked": false, "containerId": "code_box1", "lineHeight": 1.25 }, + { "type": "arrow", "id": "oarr1", "x": 222, "y": 130, "width": 76, "height": 0, "strokeColor": "#c2410c", "backgroundColor": "transparent", "fillStyle": "solid", "strokeWidth": 2, "strokeStyle": "solid", "roughness": 0, "opacity": 100, "angle": 0, "seed": 300015, "version": 1, "versionNonce": 300016, "isDeleted": false, "groupIds": [], "boundElements": null, "link": null, "locked": false, "points": [[0, 0], [76, 0]], "startBinding": null, "endBinding": null, "startArrowhead": null, "endArrowhead": "arrow" }, + { "type": "arrow", "id": "oarr2", "x": 452, "y": 130, "width": 76, "height": 0, "strokeColor": "#6d28d9", "backgroundColor": "transparent", "fillStyle": "solid", "strokeWidth": 2, "strokeStyle": "solid", "roughness": 0, "opacity": 100, "angle": 0, "seed": 300017, "version": 1, "versionNonce": 300018, "isDeleted": false, "groupIds": [], "boundElements": null, "link": null, "locked": false, "points": [[0, 0], [76, 0]], "startBinding": null, "endBinding": null, "startArrowhead": null, "endArrowhead": "arrow" }, + { "type": "arrow", "id": "oarr3", "x": 682, "y": 130, "width": 100, "height": 0, "strokeColor": "#64748b", "backgroundColor": "transparent", "fillStyle": "solid", "strokeWidth": 2, "strokeStyle": "dashed", "roughness": 0, "opacity": 100, "angle": 0, "seed": 300019, "version": 1, "versionNonce": 300020, "isDeleted": false, "groupIds": [], "boundElements": null, "link": null, "locked": false, "points": [[0, 0], [100, 0]], "startBinding": null, "endBinding": null, "startArrowhead": null, "endArrowhead": "arrow" }, + { "type": "text", "id": "hope_t", "x": 794, "y": 121, "width": 115, "height": 18, "text": "ship and hope", "originalText": "ship and hope", "fontSize": 14, "fontFamily": 3, "textAlign": "left", "verticalAlign": "top", "strokeColor": "#64748b", "backgroundColor": "transparent", "fillStyle": "solid", "strokeWidth": 1, "strokeStyle": "solid", "roughness": 0, "opacity": 100, "angle": 0, "seed": 300021, "version": 1, "versionNonce": 300022, "isDeleted": false, "groupIds": [], "boundElements": null, "link": null, "locked": false, "containerId": null, "lineHeight": 1.25 }, + { "type": "text", "id": "open_note", "x": 70, "y": 196, "width": 450, "height": 17, "text": "nothing to check against: drift compounds at agent speed", "originalText": "nothing to check against: drift compounds at agent speed", "fontSize": 13, "fontFamily": 3, "textAlign": "left", "verticalAlign": "top", "strokeColor": "#dc2626", "backgroundColor": "transparent", "fillStyle": "solid", "strokeWidth": 1, "strokeStyle": "solid", "roughness": 0, "opacity": 100, "angle": 0, "seed": 300023, "version": 1, "versionNonce": 300024, "isDeleted": false, "groupIds": [], "boundElements": null, "link": null, "locked": false, "containerId": null, "lineHeight": 1.25 }, + { "type": "line", "id": "hdiv", "x": 70, "y": 250, "width": 1010, "height": 0, "strokeColor": "#64748b", "backgroundColor": "transparent", "fillStyle": "solid", "strokeWidth": 1, "strokeStyle": "dashed", "roughness": 0, "opacity": 100, "angle": 0, "seed": 300025, "version": 1, "versionNonce": 300026, "isDeleted": false, "groupIds": [], "boundElements": null, "link": null, "locked": false, "points": [[0, 0], [1010, 0]] }, + { "type": "text", "id": "title_closed", "x": 70, "y": 278, "width": 390, "height": 25, "text": "Closed loop: the OpenSpec shape", "originalText": "Closed loop: the OpenSpec shape", "fontSize": 20, "fontFamily": 3, "textAlign": "left", "verticalAlign": "top", "strokeColor": "#047857", "backgroundColor": "transparent", "fillStyle": "solid", "strokeWidth": 1, "strokeStyle": "solid", "roughness": 0, "opacity": 100, "angle": 0, "seed": 300027, "version": 1, "versionNonce": 300028, "isDeleted": false, "groupIds": [], "boundElements": null, "link": null, "locked": false, "containerId": null, "lineHeight": 1.25 }, + { "type": "rectangle", "id": "spec_box", "x": 70, "y": 380, "width": 190, "height": 70, "strokeColor": "#047857", "backgroundColor": "#a7f3d0", "fillStyle": "solid", "strokeWidth": 2, "strokeStyle": "solid", "roughness": 0, "opacity": 100, "angle": 0, "seed": 300029, "version": 1, "versionNonce": 300030, "isDeleted": false, "groupIds": [], "boundElements": [{ "id": "spec_t", "type": "text" }], "link": null, "locked": false, "roundness": { "type": 3 } }, + { "type": "text", "id": "spec_t", "x": 80, "y": 398, "width": 170, "height": 34, "text": "the spec: what\nright looks like", "originalText": "the spec: what\nright looks like", "fontSize": 13, "fontFamily": 3, "textAlign": "center", "verticalAlign": "middle", "strokeColor": "#374151", "backgroundColor": "transparent", "fillStyle": "solid", "strokeWidth": 1, "strokeStyle": "solid", "roughness": 0, "opacity": 100, "angle": 0, "seed": 300031, "version": 1, "versionNonce": 300032, "isDeleted": false, "groupIds": [], "boundElements": null, "link": null, "locked": false, "containerId": "spec_box", "lineHeight": 1.25 }, + { "type": "diamond", "id": "cmp", "x": 330, "y": 370, "width": 150, "height": 90, "strokeColor": "#b45309", "backgroundColor": "#fef3c7", "fillStyle": "solid", "strokeWidth": 2, "strokeStyle": "solid", "roughness": 0, "opacity": 100, "angle": 0, "seed": 300033, "version": 1, "versionNonce": 300034, "isDeleted": false, "groupIds": [], "boundElements": [{ "id": "cmp_t", "type": "text" }], "link": null, "locked": false, "roundness": null }, + { "type": "text", "id": "cmp_t", "x": 360, "y": 402, "width": 90, "height": 25, "text": "aligned?", "originalText": "aligned?", "fontSize": 14, "fontFamily": 3, "textAlign": "center", "verticalAlign": "middle", "strokeColor": "#374151", "backgroundColor": "transparent", "fillStyle": "solid", "strokeWidth": 1, "strokeStyle": "solid", "roughness": 0, "opacity": 100, "angle": 0, "seed": 300035, "version": 1, "versionNonce": 300036, "isDeleted": false, "groupIds": [], "boundElements": null, "link": null, "locked": false, "containerId": "cmp", "lineHeight": 1.25 }, + { "type": "rectangle", "id": "agent_box2", "x": 550, "y": 380, "width": 150, "height": 70, "strokeColor": "#6d28d9", "backgroundColor": "#ddd6fe", "fillStyle": "solid", "strokeWidth": 2, "strokeStyle": "solid", "roughness": 0, "opacity": 100, "angle": 0, "seed": 300037, "version": 1, "versionNonce": 300038, "isDeleted": false, "groupIds": [], "boundElements": [{ "id": "agent_t2", "type": "text" }], "link": null, "locked": false, "roundness": { "type": 3 } }, + { "type": "text", "id": "agent_t2", "x": 570, "y": 402, "width": 110, "height": 25, "text": "agent builds", "originalText": "agent builds", "fontSize": 14, "fontFamily": 3, "textAlign": "center", "verticalAlign": "middle", "strokeColor": "#374151", "backgroundColor": "transparent", "fillStyle": "solid", "strokeWidth": 1, "strokeStyle": "solid", "roughness": 0, "opacity": 100, "angle": 0, "seed": 300039, "version": 1, "versionNonce": 300040, "isDeleted": false, "groupIds": [], "boundElements": null, "link": null, "locked": false, "containerId": "agent_box2", "lineHeight": 1.25 }, + { "type": "rectangle", "id": "code_box2", "x": 770, "y": 380, "width": 170, "height": 70, "strokeColor": "#1e3a5f", "backgroundColor": "#3b82f6", "fillStyle": "solid", "strokeWidth": 2, "strokeStyle": "solid", "roughness": 0, "opacity": 100, "angle": 0, "seed": 300041, "version": 1, "versionNonce": 300042, "isDeleted": false, "groupIds": [], "boundElements": [{ "id": "code_t2", "type": "text" }], "link": null, "locked": false, "roundness": { "type": 3 } }, + { "type": "text", "id": "code_t2", "x": 785, "y": 402, "width": 140, "height": 25, "text": "code + artifacts", "originalText": "code + artifacts", "fontSize": 14, "fontFamily": 3, "textAlign": "center", "verticalAlign": "middle", "strokeColor": "#ffffff", "backgroundColor": "transparent", "fillStyle": "solid", "strokeWidth": 1, "strokeStyle": "solid", "roughness": 0, "opacity": 100, "angle": 0, "seed": 300043, "version": 1, "versionNonce": 300044, "isDeleted": false, "groupIds": [], "boundElements": null, "link": null, "locked": false, "containerId": "code_box2", "lineHeight": 1.25 }, + { "type": "arrow", "id": "carr1", "x": 262, "y": 415, "width": 66, "height": 0, "strokeColor": "#047857", "backgroundColor": "transparent", "fillStyle": "solid", "strokeWidth": 2, "strokeStyle": "solid", "roughness": 0, "opacity": 100, "angle": 0, "seed": 300045, "version": 1, "versionNonce": 300046, "isDeleted": false, "groupIds": [], "boundElements": null, "link": null, "locked": false, "points": [[0, 0], [66, 0]], "startBinding": null, "endBinding": null, "startArrowhead": null, "endArrowhead": "arrow" }, + { "type": "arrow", "id": "carr2", "x": 482, "y": 415, "width": 66, "height": 0, "strokeColor": "#b45309", "backgroundColor": "transparent", "fillStyle": "solid", "strokeWidth": 2, "strokeStyle": "solid", "roughness": 0, "opacity": 100, "angle": 0, "seed": 300047, "version": 1, "versionNonce": 300048, "isDeleted": false, "groupIds": [], "boundElements": null, "link": null, "locked": false, "points": [[0, 0], [66, 0]], "startBinding": null, "endBinding": null, "startArrowhead": null, "endArrowhead": "arrow" }, + { "type": "text", "id": "yes_t", "x": 492, "y": 392, "width": 40, "height": 16, "text": "yes", "originalText": "yes", "fontSize": 12, "fontFamily": 3, "textAlign": "left", "verticalAlign": "top", "strokeColor": "#64748b", "backgroundColor": "transparent", "fillStyle": "solid", "strokeWidth": 1, "strokeStyle": "solid", "roughness": 0, "opacity": 100, "angle": 0, "seed": 300049, "version": 1, "versionNonce": 300050, "isDeleted": false, "groupIds": [], "boundElements": null, "link": null, "locked": false, "containerId": null, "lineHeight": 1.25 }, + { "type": "arrow", "id": "carr3", "x": 702, "y": 415, "width": 66, "height": 0, "strokeColor": "#6d28d9", "backgroundColor": "transparent", "fillStyle": "solid", "strokeWidth": 2, "strokeStyle": "solid", "roughness": 0, "opacity": 100, "angle": 0, "seed": 300051, "version": 1, "versionNonce": 300052, "isDeleted": false, "groupIds": [], "boundElements": null, "link": null, "locked": false, "points": [[0, 0], [66, 0]], "startBinding": null, "endBinding": null, "startArrowhead": null, "endArrowhead": "arrow" }, + { "type": "arrow", "id": "feedback", "x": 855, "y": 452, "width": 450, "height": 66, "strokeColor": "#64748b", "backgroundColor": "transparent", "fillStyle": "solid", "strokeWidth": 2, "strokeStyle": "solid", "roughness": 0, "opacity": 100, "angle": 0, "seed": 300053, "version": 1, "versionNonce": 300054, "isDeleted": false, "groupIds": [], "boundElements": null, "link": null, "locked": false, "points": [[0, 0], [0, 56], [-450, 56], [-450, 10]], "startBinding": null, "endBinding": null, "startArrowhead": null, "endArrowhead": "arrow" }, + { "type": "text", "id": "feedback_t", "x": 470, "y": 516, "width": 400, "height": 17, "text": "review / verify: measure the work against the spec", "originalText": "review / verify: measure the work against the spec", "fontSize": 13, "fontFamily": 3, "textAlign": "left", "verticalAlign": "top", "strokeColor": "#64748b", "backgroundColor": "transparent", "fillStyle": "solid", "strokeWidth": 1, "strokeStyle": "solid", "roughness": 0, "opacity": 100, "angle": 0, "seed": 300055, "version": 1, "versionNonce": 300056, "isDeleted": false, "groupIds": [], "boundElements": null, "link": null, "locked": false, "containerId": null, "lineHeight": 1.25 }, + { "type": "arrow", "id": "archive_arr", "x": 855, "y": 378, "width": 690, "height": 48, "strokeColor": "#047857", "backgroundColor": "transparent", "fillStyle": "solid", "strokeWidth": 2, "strokeStyle": "solid", "roughness": 0, "opacity": 100, "angle": 0, "seed": 300057, "version": 1, "versionNonce": 300058, "isDeleted": false, "groupIds": [], "boundElements": null, "link": null, "locked": false, "points": [[0, 0], [0, -46], [-690, -46], [-690, 0]], "startBinding": null, "endBinding": null, "startArrowhead": null, "endArrowhead": "arrow" }, + { "type": "text", "id": "archive_t", "x": 480, "y": 308, "width": 370, "height": 17, "text": "archive: the spec absorbs the shipped change", "originalText": "archive: the spec absorbs the shipped change", "fontSize": 13, "fontFamily": 3, "textAlign": "left", "verticalAlign": "top", "strokeColor": "#047857", "backgroundColor": "transparent", "fillStyle": "solid", "strokeWidth": 1, "strokeStyle": "solid", "roughness": 0, "opacity": 100, "angle": 0, "seed": 300059, "version": 1, "versionNonce": 300060, "isDeleted": false, "groupIds": [], "boundElements": null, "link": null, "locked": false, "containerId": null, "lineHeight": 1.25 }, + { "type": "text", "id": "closed_note", "x": 70, "y": 560, "width": 480, "height": 17, "text": "drift is measured and corrected every cycle", "originalText": "drift is measured and corrected every cycle", "fontSize": 13, "fontFamily": 3, "textAlign": "left", "verticalAlign": "top", "strokeColor": "#047857", "backgroundColor": "transparent", "fillStyle": "solid", "strokeWidth": 1, "strokeStyle": "solid", "roughness": 0, "opacity": 100, "angle": 0, "seed": 300061, "version": 1, "versionNonce": 300062, "isDeleted": false, "groupIds": [], "boundElements": null, "link": null, "locked": false, "containerId": null, "lineHeight": 1.25 } + ] +} diff --git a/docs-lab/diagrams/option-c-control-loop.png b/docs-lab/diagrams/option-c-control-loop.png new file mode 100644 index 0000000000..e719a979e7 Binary files /dev/null and b/docs-lab/diagrams/option-c-control-loop.png differ diff --git a/docs-lab/diagrams/option-d-sessions.excalidraw b/docs-lab/diagrams/option-d-sessions.excalidraw new file mode 100644 index 0000000000..1ab48db5cd --- /dev/null +++ b/docs-lab/diagrams/option-d-sessions.excalidraw @@ -0,0 +1,35 @@ +{ + "type": "excalidraw", + "version": 2, + "source": "https://excalidraw.com", + "appState": { "viewBackgroundColor": "#ffffff", "gridSize": 20 }, + "files": {}, + "elements": [ + { "type": "text", "id": "title", "x": 70, "y": 44, "width": 420, "height": 25, "text": "Sessions end. The map survives.", "originalText": "Sessions end. The map survives.", "fontSize": 20, "fontFamily": 3, "textAlign": "left", "verticalAlign": "top", "strokeColor": "#1e40af", "backgroundColor": "transparent", "fillStyle": "solid", "strokeWidth": 1, "strokeStyle": "solid", "roughness": 0, "opacity": 100, "angle": 0, "seed": 400001, "version": 1, "versionNonce": 400002, "isDeleted": false, "groupIds": [], "boundElements": null, "link": null, "locked": false, "containerId": null, "lineHeight": 1.25 }, + { "type": "rectangle", "id": "s1", "x": 70, "y": 120, "width": 260, "height": 90, "strokeColor": "#6d28d9", "backgroundColor": "#ddd6fe", "fillStyle": "solid", "strokeWidth": 2, "strokeStyle": "solid", "roughness": 0, "opacity": 100, "angle": 0, "seed": 400003, "version": 1, "versionNonce": 400004, "isDeleted": false, "groupIds": [], "boundElements": [{ "id": "s1_t", "type": "text" }], "link": null, "locked": false, "roundness": { "type": 3 } }, + { "type": "text", "id": "s1_t", "x": 85, "y": 148, "width": 230, "height": 34, "text": "session 1:\npropose the change", "originalText": "session 1:\npropose the change", "fontSize": 14, "fontFamily": 3, "textAlign": "center", "verticalAlign": "middle", "strokeColor": "#374151", "backgroundColor": "transparent", "fillStyle": "solid", "strokeWidth": 1, "strokeStyle": "solid", "roughness": 0, "opacity": 100, "angle": 0, "seed": 400005, "version": 1, "versionNonce": 400006, "isDeleted": false, "groupIds": [], "boundElements": null, "link": null, "locked": false, "containerId": "s1", "lineHeight": 1.25 }, + { "type": "rectangle", "id": "s2", "x": 460, "y": 120, "width": 260, "height": 90, "strokeColor": "#6d28d9", "backgroundColor": "#ddd6fe", "fillStyle": "solid", "strokeWidth": 2, "strokeStyle": "solid", "roughness": 0, "opacity": 100, "angle": 0, "seed": 400007, "version": 1, "versionNonce": 400008, "isDeleted": false, "groupIds": [], "boundElements": [{ "id": "s2_t", "type": "text" }], "link": null, "locked": false, "roundness": { "type": 3 } }, + { "type": "text", "id": "s2_t", "x": 475, "y": 148, "width": 230, "height": 34, "text": "session 2:\napply tasks 1-3", "originalText": "session 2:\napply tasks 1-3", "fontSize": 14, "fontFamily": 3, "textAlign": "center", "verticalAlign": "middle", "strokeColor": "#374151", "backgroundColor": "transparent", "fillStyle": "solid", "strokeWidth": 1, "strokeStyle": "solid", "roughness": 0, "opacity": 100, "angle": 0, "seed": 400009, "version": 1, "versionNonce": 400010, "isDeleted": false, "groupIds": [], "boundElements": null, "link": null, "locked": false, "containerId": "s2", "lineHeight": 1.25 }, + { "type": "rectangle", "id": "s3", "x": 850, "y": 120, "width": 260, "height": 90, "strokeColor": "#6d28d9", "backgroundColor": "#ddd6fe", "fillStyle": "solid", "strokeWidth": 2, "strokeStyle": "solid", "roughness": 0, "opacity": 100, "angle": 0, "seed": 400011, "version": 1, "versionNonce": 400012, "isDeleted": false, "groupIds": [], "boundElements": [{ "id": "s3_t", "type": "text" }], "link": null, "locked": false, "roundness": { "type": 3 } }, + { "type": "text", "id": "s3_t", "x": 865, "y": 148, "width": 230, "height": 34, "text": "session 3:\nfinish and archive", "originalText": "session 3:\nfinish and archive", "fontSize": 14, "fontFamily": 3, "textAlign": "center", "verticalAlign": "middle", "strokeColor": "#374151", "backgroundColor": "transparent", "fillStyle": "solid", "strokeWidth": 1, "strokeStyle": "solid", "roughness": 0, "opacity": 100, "angle": 0, "seed": 400013, "version": 1, "versionNonce": 400014, "isDeleted": false, "groupIds": [], "boundElements": null, "link": null, "locked": false, "containerId": "s3", "lineHeight": 1.25 }, + { "type": "line", "id": "break1", "x": 395, "y": 108, "width": 0, "height": 116, "strokeColor": "#dc2626", "backgroundColor": "transparent", "fillStyle": "solid", "strokeWidth": 1, "strokeStyle": "dashed", "roughness": 0, "opacity": 100, "angle": 0, "seed": 400015, "version": 1, "versionNonce": 400016, "isDeleted": false, "groupIds": [], "boundElements": null, "link": null, "locked": false, "points": [[0, 0], [0, 116]] }, + { "type": "text", "id": "break1_t", "x": 344, "y": 86, "width": 105, "height": 16, "text": "context lost", "originalText": "context lost", "fontSize": 12, "fontFamily": 3, "textAlign": "left", "verticalAlign": "top", "strokeColor": "#dc2626", "backgroundColor": "transparent", "fillStyle": "solid", "strokeWidth": 1, "strokeStyle": "solid", "roughness": 0, "opacity": 100, "angle": 0, "seed": 400017, "version": 1, "versionNonce": 400018, "isDeleted": false, "groupIds": [], "boundElements": null, "link": null, "locked": false, "containerId": null, "lineHeight": 1.25 }, + { "type": "line", "id": "break2", "x": 785, "y": 108, "width": 0, "height": 116, "strokeColor": "#dc2626", "backgroundColor": "transparent", "fillStyle": "solid", "strokeWidth": 1, "strokeStyle": "dashed", "roughness": 0, "opacity": 100, "angle": 0, "seed": 400019, "version": 1, "versionNonce": 400020, "isDeleted": false, "groupIds": [], "boundElements": null, "link": null, "locked": false, "points": [[0, 0], [0, 116]] }, + { "type": "text", "id": "break2_t", "x": 734, "y": 86, "width": 105, "height": 16, "text": "context lost", "originalText": "context lost", "fontSize": 12, "fontFamily": 3, "textAlign": "left", "verticalAlign": "top", "strokeColor": "#dc2626", "backgroundColor": "transparent", "fillStyle": "solid", "strokeWidth": 1, "strokeStyle": "solid", "roughness": 0, "opacity": 100, "angle": 0, "seed": 400021, "version": 1, "versionNonce": 400022, "isDeleted": false, "groupIds": [], "boundElements": null, "link": null, "locked": false, "containerId": null, "lineHeight": 1.25 }, + { "type": "line", "id": "spine", "x": 70, "y": 340, "width": 1030, "height": 0, "strokeColor": "#047857", "backgroundColor": "transparent", "fillStyle": "solid", "strokeWidth": 3, "strokeStyle": "solid", "roughness": 0, "opacity": 100, "angle": 0, "seed": 400023, "version": 1, "versionNonce": 400024, "isDeleted": false, "groupIds": [], "boundElements": null, "link": null, "locked": false, "points": [[0, 0], [1030, 0]] }, + { "type": "text", "id": "spine_t", "x": 70, "y": 366, "width": 430, "height": 18, "text": "openspec/ on disk: the specs and the change (the map)", "originalText": "openspec/ on disk: the specs and the change (the map)", "fontSize": 14, "fontFamily": 3, "textAlign": "left", "verticalAlign": "top", "strokeColor": "#047857", "backgroundColor": "transparent", "fillStyle": "solid", "strokeWidth": 1, "strokeStyle": "solid", "roughness": 0, "opacity": 100, "angle": 0, "seed": 400025, "version": 1, "versionNonce": 400026, "isDeleted": false, "groupIds": [], "boundElements": null, "link": null, "locked": false, "containerId": null, "lineHeight": 1.25 }, + { "type": "ellipse", "id": "goal", "x": 1096, "y": 330, "width": 20, "height": 20, "strokeColor": "#047857", "backgroundColor": "#a7f3d0", "fillStyle": "solid", "strokeWidth": 2, "strokeStyle": "solid", "roughness": 0, "opacity": 100, "angle": 0, "seed": 400027, "version": 1, "versionNonce": 400028, "isDeleted": false, "groupIds": [], "boundElements": null, "link": null, "locked": false }, + { "type": "arrow", "id": "w1", "x": 200, "y": 212, "width": 0, "height": 124, "strokeColor": "#64748b", "backgroundColor": "transparent", "fillStyle": "solid", "strokeWidth": 2, "strokeStyle": "solid", "roughness": 0, "opacity": 100, "angle": 0, "seed": 400029, "version": 1, "versionNonce": 400030, "isDeleted": false, "groupIds": [], "boundElements": null, "link": null, "locked": false, "points": [[0, 0], [0, 124]], "startBinding": null, "endBinding": null, "startArrowhead": null, "endArrowhead": "arrow" }, + { "type": "text", "id": "w1_t", "x": 210, "y": 262, "width": 55, "height": 16, "text": "writes", "originalText": "writes", "fontSize": 12, "fontFamily": 3, "textAlign": "left", "verticalAlign": "top", "strokeColor": "#64748b", "backgroundColor": "transparent", "fillStyle": "solid", "strokeWidth": 1, "strokeStyle": "solid", "roughness": 0, "opacity": 100, "angle": 0, "seed": 400031, "version": 1, "versionNonce": 400032, "isDeleted": false, "groupIds": [], "boundElements": null, "link": null, "locked": false, "containerId": null, "lineHeight": 1.25 }, + { "type": "arrow", "id": "r2", "x": 500, "y": 336, "width": 0, "height": 122, "strokeColor": "#64748b", "backgroundColor": "transparent", "fillStyle": "solid", "strokeWidth": 2, "strokeStyle": "solid", "roughness": 0, "opacity": 100, "angle": 0, "seed": 400033, "version": 1, "versionNonce": 400034, "isDeleted": false, "groupIds": [], "boundElements": null, "link": null, "locked": false, "points": [[0, 0], [0, -122]], "startBinding": null, "endBinding": null, "startArrowhead": null, "endArrowhead": "arrow" }, + { "type": "text", "id": "r2_t", "x": 510, "y": 262, "width": 50, "height": 16, "text": "reads", "originalText": "reads", "fontSize": 12, "fontFamily": 3, "textAlign": "left", "verticalAlign": "top", "strokeColor": "#64748b", "backgroundColor": "transparent", "fillStyle": "solid", "strokeWidth": 1, "strokeStyle": "solid", "roughness": 0, "opacity": 100, "angle": 0, "seed": 400035, "version": 1, "versionNonce": 400036, "isDeleted": false, "groupIds": [], "boundElements": null, "link": null, "locked": false, "containerId": null, "lineHeight": 1.25 }, + { "type": "arrow", "id": "w2", "x": 640, "y": 212, "width": 0, "height": 124, "strokeColor": "#64748b", "backgroundColor": "transparent", "fillStyle": "solid", "strokeWidth": 2, "strokeStyle": "solid", "roughness": 0, "opacity": 100, "angle": 0, "seed": 400037, "version": 1, "versionNonce": 400038, "isDeleted": false, "groupIds": [], "boundElements": null, "link": null, "locked": false, "points": [[0, 0], [0, 124]], "startBinding": null, "endBinding": null, "startArrowhead": null, "endArrowhead": "arrow" }, + { "type": "text", "id": "w2_t", "x": 650, "y": 262, "width": 55, "height": 16, "text": "writes", "originalText": "writes", "fontSize": 12, "fontFamily": 3, "textAlign": "left", "verticalAlign": "top", "strokeColor": "#64748b", "backgroundColor": "transparent", "fillStyle": "solid", "strokeWidth": 1, "strokeStyle": "solid", "roughness": 0, "opacity": 100, "angle": 0, "seed": 400039, "version": 1, "versionNonce": 400040, "isDeleted": false, "groupIds": [], "boundElements": null, "link": null, "locked": false, "containerId": null, "lineHeight": 1.25 }, + { "type": "arrow", "id": "r3", "x": 890, "y": 336, "width": 0, "height": 122, "strokeColor": "#64748b", "backgroundColor": "transparent", "fillStyle": "solid", "strokeWidth": 2, "strokeStyle": "solid", "roughness": 0, "opacity": 100, "angle": 0, "seed": 400041, "version": 1, "versionNonce": 400042, "isDeleted": false, "groupIds": [], "boundElements": null, "link": null, "locked": false, "points": [[0, 0], [0, -122]], "startBinding": null, "endBinding": null, "startArrowhead": null, "endArrowhead": "arrow" }, + { "type": "text", "id": "r3_t", "x": 900, "y": 262, "width": 50, "height": 16, "text": "reads", "originalText": "reads", "fontSize": 12, "fontFamily": 3, "textAlign": "left", "verticalAlign": "top", "strokeColor": "#64748b", "backgroundColor": "transparent", "fillStyle": "solid", "strokeWidth": 1, "strokeStyle": "solid", "roughness": 0, "opacity": 100, "angle": 0, "seed": 400043, "version": 1, "versionNonce": 400044, "isDeleted": false, "groupIds": [], "boundElements": null, "link": null, "locked": false, "containerId": null, "lineHeight": 1.25 }, + { "type": "arrow", "id": "w3", "x": 1030, "y": 212, "width": 0, "height": 124, "strokeColor": "#047857", "backgroundColor": "transparent", "fillStyle": "solid", "strokeWidth": 2, "strokeStyle": "solid", "roughness": 0, "opacity": 100, "angle": 0, "seed": 400045, "version": 1, "versionNonce": 400046, "isDeleted": false, "groupIds": [], "boundElements": null, "link": null, "locked": false, "points": [[0, 0], [0, 124]], "startBinding": null, "endBinding": null, "startArrowhead": null, "endArrowhead": "arrow" }, + { "type": "text", "id": "w3_t", "x": 1040, "y": 262, "width": 65, "height": 16, "text": "archive", "originalText": "archive", "fontSize": 12, "fontFamily": 3, "textAlign": "left", "verticalAlign": "top", "strokeColor": "#047857", "backgroundColor": "transparent", "fillStyle": "solid", "strokeWidth": 1, "strokeStyle": "solid", "roughness": 0, "opacity": 100, "angle": 0, "seed": 400047, "version": 1, "versionNonce": 400048, "isDeleted": false, "groupIds": [], "boundElements": null, "link": null, "locked": false, "containerId": null, "lineHeight": 1.25 }, + { "type": "text", "id": "note", "x": 70, "y": 430, "width": 480, "height": 17, "text": "every new session picks up exactly where the last left off", "originalText": "every new session picks up exactly where the last left off", "fontSize": 13, "fontFamily": 3, "textAlign": "left", "verticalAlign": "top", "strokeColor": "#64748b", "backgroundColor": "transparent", "fillStyle": "solid", "strokeWidth": 1, "strokeStyle": "solid", "roughness": 0, "opacity": 100, "angle": 0, "seed": 400049, "version": 1, "versionNonce": 400050, "isDeleted": false, "groupIds": [], "boundElements": null, "link": null, "locked": false, "containerId": null, "lineHeight": 1.25 }, + { "type": "text", "id": "note2", "x": 940, "y": 366, "width": 175, "height": 17, "text": "specs match reality", "originalText": "specs match reality", "fontSize": 13, "fontFamily": 3, "textAlign": "left", "verticalAlign": "top", "strokeColor": "#047857", "backgroundColor": "transparent", "fillStyle": "solid", "strokeWidth": 1, "strokeStyle": "solid", "roughness": 0, "opacity": 100, "angle": 0, "seed": 400051, "version": 1, "versionNonce": 400052, "isDeleted": false, "groupIds": [], "boundElements": null, "link": null, "locked": false, "containerId": null, "lineHeight": 1.25 } + ] +} diff --git a/docs-lab/diagrams/option-d-sessions.png b/docs-lab/diagrams/option-d-sessions.png new file mode 100644 index 0000000000..104b23cfc6 Binary files /dev/null and b/docs-lab/diagrams/option-d-sessions.png differ diff --git a/docs-lab/diagrams/option-h-drift-animated.svg b/docs-lab/diagrams/option-h-drift-animated.svg new file mode 100644 index 0000000000..9f459780d0 --- /dev/null +++ b/docs-lab/diagrams/option-h-drift-animated.svg @@ -0,0 +1,109 @@ + + + + + Without a shared plan + + + + + the goal + + + you + + + agent, session 1 + + + agent, session 2 + + + teammate + + aligned at the start, small differences compound each session + + + + + + With a shared, updatable plan + + + + + dashed: the spec, one shared route + + + + + + + + + review + sync + + + the goal + + drift is corrected before it compounds + diff --git a/docs-lab/diagrams/render/render_template_umd.html b/docs-lab/diagrams/render/render_template_umd.html new file mode 100644 index 0000000000..8c2b91c4ff --- /dev/null +++ b/docs-lab/diagrams/render/render_template_umd.html @@ -0,0 +1,54 @@ + + + + + + + + + + + +
+ + + diff --git a/docs-lab/diagrams/render/render_umd.py b/docs-lab/diagrams/render/render_umd.py new file mode 100644 index 0000000000..511a11daa7 --- /dev/null +++ b/docs-lab/diagrams/render/render_umd.py @@ -0,0 +1,62 @@ +"""Render .excalidraw to PNG via the UMD template (esm.sh workaround).""" +import json +import sys +from pathlib import Path +from playwright.sync_api import sync_playwright + +TEMPLATE = Path(__file__).parent / "render_template_umd.html" + + +def bbox(elements): + mnx = mny = float("inf") + mxx = mxy = float("-inf") + for el in elements: + if el.get("isDeleted"): + continue + x, y = el.get("x", 0), el.get("y", 0) + if el.get("type") in ("arrow", "line") and "points" in el: + for px, py in el["points"]: + mnx, mny = min(mnx, x + px), min(mny, y + py) + mxx, mxy = max(mxx, x + px), max(mxy, y + py) + else: + w, h = abs(el.get("width", 0)), abs(el.get("height", 0)) + mnx, mny = min(mnx, x), min(mny, y) + mxx, mxy = max(mxx, x + w), max(mxy, y + h) + if mnx == float("inf"): + return 0, 0, 800, 600 + return mnx, mny, mxx, mxy + + +def main(): + src = Path(sys.argv[1]) + data = json.loads(src.read_text()) + els = [e for e in data["elements"] if not e.get("isDeleted")] + mnx, mny, mxx, mxy = bbox(els) + pad = 80 + vw = min(int(mxx - mnx + pad * 2), 1920) + vh = max(int(mxy - mny + pad * 2), 600) + out = src.with_suffix(".png") + + with sync_playwright() as p: + browser = p.chromium.launch(headless=True) + page = browser.new_page(viewport={"width": vw, "height": vh}, device_scale_factor=2) + page.goto(TEMPLATE.as_uri()) + page.wait_for_function("window.__moduleReady === true || window.__moduleError", timeout=60000) + err = page.evaluate("window.__moduleError || null") + if err: + print(f"ERROR: {err}", file=sys.stderr) + sys.exit(1) + result = page.evaluate(f"window.renderDiagram({json.dumps(data)})") + if not result or not result.get("success"): + print(f"ERROR: {result.get('error') if result else 'null result'}", file=sys.stderr) + sys.exit(1) + page.wait_for_function("window.__renderComplete === true", timeout=15000) + page.wait_for_timeout(500) + svg = page.query_selector("#root svg") + svg.screenshot(path=str(out)) + browser.close() + print(out) + + +if __name__ == "__main__": + main() diff --git a/docs-lab/guides/apply.md b/docs-lab/guides/apply.md new file mode 100644 index 0000000000..dacb1027d1 --- /dev/null +++ b/docs-lab/guides/apply.md @@ -0,0 +1,11 @@ +# Apply a change + +> Run the plan: pacing, context windows, and picking up where you left off. + + + +## Task by task or all at once + +## Managing the context window + +## Continue and fast-forward diff --git a/docs-lab/guides/change-course.md b/docs-lab/guides/change-course.md new file mode 100644 index 0000000000..f12a8d8733 --- /dev/null +++ b/docs-lab/guides/change-course.md @@ -0,0 +1,11 @@ +# Change course + +> Revise a change in flight, or decide it's cleaner to start fresh. + + + +## Update or start fresh? + +## Revising artifacts with openspec-update-change + +## Advanced: revising mid-implementation diff --git a/docs-lab/guides/concepts.md b/docs-lab/guides/concepts.md new file mode 100644 index 0000000000..f203e5da31 --- /dev/null +++ b/docs-lab/guides/concepts.md @@ -0,0 +1,9 @@ +# Concepts + +> What the two artifacts are, and how a change describes a diff against current specs. + + + +## Specs: the system as built + +## Changes: a folder of deltas diff --git a/docs-lab/guides/examples.md b/docs-lab/guides/examples.md new file mode 100644 index 0000000000..6e5745eb45 --- /dev/null +++ b/docs-lab/guides/examples.md @@ -0,0 +1,16 @@ +# Examples + +> See what a good change looks like: real drafts, what review caught, and the fixes. + + + + +## 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 diff --git a/docs-lab/guides/existing-codebases.md b/docs-lab/guides/existing-codebases.md new file mode 100644 index 0000000000..e589c55386 --- /dev/null +++ b/docs-lab/guides/existing-codebases.md @@ -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. + + + +## Start with a change + +## Specs for existing code + +## Working from a PRD + +## Organizing specs as they grow diff --git a/docs-lab/guides/explore.md b/docs-lab/guides/explore.md new file mode 100644 index 0000000000..230d2c0b8d --- /dev/null +++ b/docs-lab/guides/explore.md @@ -0,0 +1,17 @@ +# Explore an idea + +> Think it through with the agent before you commit to a proposal. + + + +## When to explore first + +## A real explore session + +## From exploration to proposal + +## Advanced + +### Exploring mid-change + +### Challenging a draft plan diff --git a/docs-lab/guides/review-the-plan.md b/docs-lab/guides/review-the-plan.md new file mode 100644 index 0000000000..c6c0354a05 --- /dev/null +++ b/docs-lab/guides/review-the-plan.md @@ -0,0 +1,15 @@ +# Review the plan + +> The two-minute pass that catches wrong turns before they're code. + + + +## The two-minute pass + +## What good requirements look like + +## What good scenarios look like + +## Pushing back + +## Advanced: verify after apply diff --git a/docs-lab/guides/teams.md b/docs-lab/guides/teams.md new file mode 100644 index 0000000000..1285ddfea3 --- /dev/null +++ b/docs-lab/guides/teams.md @@ -0,0 +1,17 @@ +# Teams + +> Run OpenSpec as a team: what to commit, how a change rides its PR, and when to archive. + + + +## 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 diff --git a/docs-lab/help/faq.md b/docs-lab/help/faq.md new file mode 100644 index 0000000000..534a9f1b93 --- /dev/null +++ b/docs-lab/help/faq.md @@ -0,0 +1,18 @@ +# FAQ + +> Short answers to the questions that don't need a page. + + + +## 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? diff --git a/docs-lab/help/legacy/migration.md b/docs-lab/help/legacy/migration.md new file mode 100644 index 0000000000..60fc2f0a39 --- /dev/null +++ b/docs-lab/help/legacy/migration.md @@ -0,0 +1,13 @@ +# Migrating from the legacy workflow + +> Moving from the legacy `/openspec:*` commands to OPSX. + + + +## What changed and why + +## Command mapping + +## Migrating a project + +## Behavior differences diff --git a/docs-lab/help/troubleshooting.md b/docs-lab/help/troubleshooting.md new file mode 100644 index 0000000000..eebbc55f01 --- /dev/null +++ b/docs-lab/help/troubleshooting.md @@ -0,0 +1,15 @@ +# Troubleshooting + +> When OpenSpec doesn't do what you expected: symptoms and their fixes. + + + +## Skills don't appear in chat + +## The agent ignores the workflow + +## Validation failures + +## Sync and archive issues + +## Getting help diff --git a/docs-lab/message-map.md b/docs-lab/message-map.md new file mode 100644 index 0000000000..0782db6e6c --- /dev/null +++ b/docs-lab/message-map.md @@ -0,0 +1,64 @@ +# 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, a reviewable plan before code? | [Start › Overview](start/overview.md) | Answered | +| How does someone decide OpenSpec is worth their time? | [Start › Overview](start/overview.md) | Answered | +| 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 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 should a user teach OpenSpec their project's context and rules? | [Customize › Project configuration](customize/project-config.md) | Skeleton | +| How does a user get artifacts written in a language other than English? | [Customize › Project configuration](customize/project-config.md) | Skeleton | +| How should a user change what the workflows produce? | [Customize › Schemas](customize/schemas.md) | Skeleton | +| 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" | Skeleton | +| How should a user plan a change that spans repos? | [Multi-repo › Stores](multi-repo/stores.md) | Skeleton | +| 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? | No owner: the `openers` field's contract sits on [Reference › Configuration › CLI settings](reference/configuration/config-json.md), but no page teaches worksets (workspace feature, pre-release) | Gap | +| 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) carries the pitch ("a shared map, not a plan up front"); [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 | diff --git a/docs-lab/multi-repo/stores.md b/docs-lab/multi-repo/stores.md new file mode 100644 index 0000000000..4c620762ec --- /dev/null +++ b/docs-lab/multi-repo/stores.md @@ -0,0 +1,19 @@ +# Stores (beta) + +> Plan changes that span repositories: one store, many repos. + + + +## What a store is + +## When you need one (and when you don't) + +## Setup + +## The daily flow + +## Beta limits diff --git a/docs-lab/reference/architecture/design-decisions.md b/docs-lab/reference/architecture/design-decisions.md new file mode 100644 index 0000000000..5f267efb7c --- /dev/null +++ b/docs-lab/reference/architecture/design-decisions.md @@ -0,0 +1,6 @@ +# Design decisions + +> Why OPSX works the way it does. + + diff --git a/docs-lab/reference/architecture/index.md b/docs-lab/reference/architecture/index.md new file mode 100644 index 0000000000..583369155f --- /dev/null +++ b/docs-lab/reference/architecture/index.md @@ -0,0 +1,14 @@ +# Overview + +> How OPSX is built: internals for the curious. + + + +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 diff --git a/docs-lab/reference/architecture/workflow-runs.md b/docs-lab/reference/architecture/workflow-runs.md new file mode 100644 index 0000000000..484c48240f --- /dev/null +++ b/docs-lab/reference/architecture/workflow-runs.md @@ -0,0 +1,5 @@ +# Workflow runs + +> How a workflow run executes, from invocation to written artifacts. + + diff --git a/docs-lab/reference/cli.md b/docs-lab/reference/cli.md new file mode 100644 index 0000000000..8f892fe73a --- /dev/null +++ b/docs-lab/reference/cli.md @@ -0,0 +1,2034 @@ +# CLI + +> The `openspec` terminal commands. + + + +## Commands + +**Set up** + +| Command | What it does | +|---|---| +| [`openspec init`](#openspec-init) | Initialize OpenSpec in a project. | +| [`openspec update`](#openspec-update) | Update OpenSpec's installed instruction files. | +| [`openspec config`](#openspec-config) | View and change global configuration. | + +**Changes and specs** + +| Command | What it does | +|---|---| +| [`openspec list`](#openspec-list) | List changes, or specs with `--specs`. | +| [`openspec show`](#openspec-show) | Print a change or spec, as markdown or JSON. | +| [`openspec view`](#openspec-view) | One-screen dashboard of specs and changes. | +| [`openspec validate`](#openspec-validate) | Check changes and specs for structural issues. | +| [`openspec archive`](#openspec-archive) | Move a completed change to the archive and update the main specs. | + +**Workflows and schemas** + +Your agent runs most of these during the workflow. + +| Command | What it does | +|---|---| +| [`openspec new`](#openspec-new) | Create a new change directory. | +| [`openspec status`](#openspec-status) | Artifact completion status for a change. | +| [`openspec instructions`](#openspec-instructions) | Instructions for creating an artifact, applying, or archiving. | +| [`openspec templates`](#openspec-templates) | Resolved template paths for a schema's artifacts. | +| [`openspec schemas`](#openspec-schemas) | List available workflow schemas. | +| [`openspec schema`](#openspec-schema) | Inspect, fork, or create a schema (experimental). | + +**Multi-repo (beta)** + +| Command | What it does | +|---|---| +| [`openspec store`](#openspec-store) | Create and manage stores: standalone OpenSpec repos registered on your machine. | +| [`openspec doctor`](#openspec-doctor) | Report relationship health for the resolved OpenSpec root. | +| [`openspec context`](#openspec-context) | Print the working context for the resolved OpenSpec root. | +| [`openspec workset`](#openspec-workset) | Compose, keep, and open personal working views. | + +**Utilities** + +| Command | What it does | +|---|---| +| [`openspec feedback`](#openspec-feedback) | Submit feedback about OpenSpec. | +| [`openspec completion`](#openspec-completion) | Install or generate shell completions. | + +**Deprecated** + +| Command | What it does | +|---|---| +| [`openspec change`](#openspec-change) | Noun form of show, list, and validate for changes; the CLI warns and points to the verb-first commands. | +| [`openspec spec`](#openspec-spec) | Noun form of show, list, and validate for specs; same warning. | + +Every command takes `-h, --help`. The bare `openspec` command also takes: + +- `-V, --version`: print the CLI version. +- `--no-color`: disable colored output. + +## openspec init + +Initializes OpenSpec in a project. + +```bash +openspec init # current directory, interactive tool picker +openspec init --tools claude,cursor # set up specific tools, no prompts +openspec init --tools none # openspec/ structure only, no tool files +``` + +With no `--tools`, init prompts you to pick tools in an interactive terminal. Outside one, it sets up the tools it detects in the project; with none detected it exits 1 and lists the valid ids. + +**Arguments** + +| Argument | What it is | +|---|---| +| `path` | The project directory to initialize. Default: current directory. Created if missing. | + +**Options** + +| Flag | Effect | +|---|---| +| `--tools ` | Comma-separated tool ids, `all`, or `none`. Skips the picker. Ids are listed in [Supported tools](supported-tools.md). | +| `--force` | Remove files from older OpenSpec layouts without asking. Interactive runs otherwise confirm the cleanup first. | +| `--profile ` | Override the global config profile for this run: `core` (the standard workflow set) or `custom` (the workflows saved in global config). | +| `--no-animation` | Show a static welcome screen instead of the animated one. | + +**Output** + +Each selected tool gets OpenSpec's skills and commands in its own directory: + +``` +▌ OpenSpec structure created +✔ Setup complete for Claude Code + +OpenSpec Setup Complete + +Created: Claude Code +6 skills and 6 commands in .claude/ +Config: openspec/config.yaml (schema: spec-driven) + +Getting started: + Start your first change: /opsx:propose "your idea" + +Restart your IDE for the new commands to take effect. +``` + +`--tools none` creates only `openspec/config.yaml`. On an already-initialized project, init rewrites the installed files in place and the summary reads `Refreshed: Claude Code` with `Config: openspec/config.yaml (exists)`. + +**Exit codes** + +- `0`: setup completed. +- `1`: invalid `--tools` or `--profile` value, or a non-interactive run with no tools detected and no `--tools`. + +## openspec update + +Updates OpenSpec's installed instruction files. + +```bash +openspec update # refresh tools whose files are older than the CLI +openspec update --force # rewrite files even when they're current +``` + +update finds the tools init configured and compares their generated files against the CLI's version. When a newer OpenSpec release exists, it first offers to upgrade the CLI, then reruns with the upgraded version; set `OPENSPEC_NO_UPDATE_CHECK=1` to skip the check. + +**Arguments** + +| Argument | What it is | +|---|---| +| `path` | The project directory to update. Default: current directory. | + +**Options** + +| Flag | Effect | +|---|---| +| `--force` | Rewrite every configured tool's files even when they're up to date. | + +**Output** + +When every tool's files match the CLI version: + +``` +✓ All 1 tool(s) up to date (v1.7.0) + Tools: claude + +Use --force to refresh files anyway. +``` + +When a tool's files came from an older CLI (or with `--force`, which prints `Force updating 1 tool(s): claude` instead): + +``` +Updating 1 tool(s): claude (1.6.0 → 1.7.0) + +✔ Updated Claude Code + +✓ Updated: Claude Code (v1.7.0) +Tools: Claude Code + +Restart your IDE for changes to take effect. +``` + +In a directory without OpenSpec, update refuses: + +``` +✖ Error: No OpenSpec directory found. Run 'openspec init' first. +``` + +**Exit codes** + +- `0`: files updated, or everything already up to date. +- `1`: no OpenSpec directory at the path, or the update failed. + +## openspec config + +Views and changes global configuration. + +```bash +openspec config list # see current settings +openspec config set delivery skills # change one value +openspec config profile # interactive workflow picker +``` + +| Subcommand | What it does | +|---|---| +| `path` | Print the config file location. | +| `list` | Show all current settings. | +| `get ` | Print one value, raw and scriptable. | +| `set ` | Set a value, coercing its type. | +| `unset ` | Remove a key so its default applies. | +| `reset` | Reset all configuration to defaults. | +| `edit` | Open the config file in `$EDITOR`. | +| `profile [preset]` | Configure delivery mode and workflows. | + +Config is global to your machine, stored as JSON where `config path` points: `$XDG_CONFIG_HOME/openspec/config.json` if set, else `~/.config/openspec/config.json` (macOS, Linux) or `%APPDATA%\openspec\config.json` (Windows). Every subcommand accepts `--scope `, but only `global` works today; any other scope exits 1 with `Error: Project-local config is not yet implemented`. + +### openspec config path + +```bash +openspec config path +``` + +``` +/Users/you/.config/openspec/config.json +``` + +### openspec config list + +```bash +openspec config list # readable settings plus profile summary +openspec config list --json # raw config as JSON +``` + +**Options** + +| Flag | Effect | +|---|---| +| `--json` | Print the config object as JSON. | + +**Output** + +Each setting, then a profile summary that marks values as explicit or default: + +``` +featureFlags: {} +profile: core +delivery: both + +Profile settings: + profile: core (default) + delivery: both (default) + workflows: propose, explore, apply, update, sync, archive (from core profile) +``` + +### openspec config get + +```bash +openspec config get delivery +``` + +**Arguments** + +| Argument | What it is | +|---|---| +| `key` | The key to read. Dots reach nested values (`featureFlags.workspaces`). | + +**Output** + +The bare value, ready for scripts. Objects print as compact JSON: + +``` +both +``` + +**Exit codes** + +- `0`: value printed. +- `1`: key has no value; nothing is printed. + +### openspec config set + +```bash +openspec config set delivery skills +openspec config set featureFlags.workspaces true +``` + +**Arguments** + +| Argument | What it is | +|---|---| +| `key` | The key to write, dotted for nested values. | +| `value` | The new value. `true`/`false` become booleans, numeric strings become numbers. | + +**Options** + +| Flag | Effect | +|---|---| +| `--string` | Store the value as a string, skipping type coercion. | +| `--allow-unknown` | Permit keys the schema doesn't know. | + +**Output** + +``` +Set featureFlags.workspaces = true +``` + +Unknown keys and invalid values fail with exit 1 before anything is saved: + +``` +Error: Invalid configuration key "bogus.key". Unknown top-level key "bogus". +Use "openspec config list" to see available keys. +Pass --allow-unknown to bypass this check. +``` + +``` +Error: Invalid configuration - delivery: Invalid option: expected one of "both"|"skills"|"commands" +``` + +### openspec config unset + +```bash +openspec config unset delivery +``` + +Removes the key so the default applies again. Keys with built-in defaults always count as set, so this reports success even if you never set them: + +``` +Unset delivery (reverted to default) +``` + +A key with no value at all prints `Key "featureFlags.nothere" was not set`. Both cases exit 0. + +### openspec config reset + +```bash +openspec config reset --all # asks for confirmation +openspec config reset --all -y # no prompt +``` + +**Options** + +| Flag | Effect | +|---|---| +| `--all` | Required. Reset everything. | +| `-y, --yes` | Skip the confirmation prompt. | + +**Output** + +``` +Configuration reset to defaults +``` + +Without `--all` it exits 1 and prints the usage line. + +**Exit codes** + +- `0`: reset done, or you answered no at the prompt. +- `1`: `--all` missing. +- `130`: prompt cancelled with Ctrl-C. + +### openspec config edit + +```bash +openspec config edit +``` + +Opens the config file in `$EDITOR` (falling back to `$VISUAL`), creating it with defaults first if missing. When the editor closes, the file is validated; invalid JSON or an invalid config exits 1. With no editor configured it exits 1: + +``` +Error: No editor configured +Set the EDITOR or VISUAL environment variable to your preferred editor +Example: export EDITOR=vim +``` + +### openspec config profile + +```bash +openspec config profile # interactive picker (needs a terminal) +openspec config profile core # apply the core preset directly +``` + +**Arguments** + +| Argument | What it is | +|---|---| +| `preset` | Optional preset name. Only `core` exists; it selects the core workflows and keeps your delivery setting. | + +With no preset, an interactive picker shows your current delivery and workflows, lets you change either or both (delivery: both, skills only, or commands only; workflows: a checkbox list), prints the diff, and inside an OpenSpec project offers to run `openspec update` for you. Outside a terminal it exits 1: + +``` +Interactive mode required. Use `openspec config profile core` or set config via environment/flags. +``` + +**Output** + +Changed config doesn't reach projects until they update: + +``` +Config updated. Run `openspec update` in your projects to apply. +``` + +**Exit codes** + +- `0`: profile saved, or you kept current settings. +- `1`: unknown preset, no terminal, or the offered `openspec update` failed. +- `130`: picker cancelled with Ctrl-C. + +## openspec list + +Lists changes, or specs with `--specs`. + +```bash +openspec list # changes, most recently modified first +openspec list --specs # specs with requirement counts +openspec list --json # machine-readable, includes the resolved root +``` + +Rows come from `openspec/changes/` and `openspec/specs/` under the resolved root. The `archive/` folder is skipped. + +**Options** + +| Flag | Effect | +|---|---| +| `--specs` | List specs instead of changes. | +| `--changes` | List changes. This is the default. | +| `--sort ` | `recent` (last modified first) or `name`. Default: `recent`. Specs always sort by name. | +| `--json` | Print JSON instead of the table. | +| `--store ` | Use a registered store as the OpenSpec root instead of the current project. | + +**Output** + +One row per change: name, task status, last modified. The status column reads `No tasks`, `2/5 tasks`, or `✓ Complete`. + +``` +Changes: + add-rate-limit No tasks just now +``` + +``` +Specs: + api requirements 1 +``` + +`--json` adds task counts and a `status` of `no-tasks`, `in-progress`, or `complete`: + +```json +{ + "changes": [ + { + "name": "add-rate-limit", + "completedTasks": 0, + "totalTasks": 0, + "lastModified": "2026-08-11T13:44:40.171Z", + "status": "no-tasks" + } + ], + "root": { + "path": "/Users/you/projects/my-app", + "source": "nearest" + } +} +``` + +An empty listing prints `No active changes found.` or `No specs found.` and still exits 0. + +**Exit codes** + +- `0`: listing printed, even when empty. +- `1`: no OpenSpec root found (outside a project, no `--store`). + +## openspec show + +Prints a change or spec, as markdown or JSON. + +```bash +openspec show add-rate-limit # change: prints proposal.md +openspec show api # spec: prints spec.md +openspec show api --json --no-scenarios # spec JSON without scenario text +``` + +With no name, show asks change or spec, then lists items to pick from. Outside an interactive terminal it exits 1 and prints the direct forms instead. + +**Arguments** + +| Argument | What it is | +|---|---| +| `item-name` | The change or spec to show, by folder name (`add-rate-limit`, `api`). | + +**Options** + +| Flag | Effect | +|---|---| +| `--json` | Print structured JSON instead of raw markdown. | +| `--type ` | Pick the type when a change and a spec share a name. | +| `--no-interactive` | Never prompt: a missing name becomes an error. | +| `--deltas-only` | JSON, change: restrict output to deltas. Change JSON is already delta-only, so output matches plain `--json`. | +| `--requirements-only` | Deprecated alias for `--deltas-only`. Warns on stderr. | +| `--requirements` | JSON, spec: keep requirement text, empty the `scenarios` arrays. | +| `--no-scenarios` | JSON, spec: same output as `--requirements`. | +| `-r, --requirement ` | JSON, spec: output one requirement by 1-based position. Can't combine with `--requirements`. | +| `--store ` | Use a registered store as the OpenSpec root instead of the current project. | + +Flags that don't apply to the resolved type are ignored with a warning on stderr. + +**Output** + +Text mode is a raw passthrough: a change prints its `proposal.md`, a spec prints its `spec.md`. + +``` +# Add rate limiting + +## Why +Unauthenticated clients can exhaust the API. + +## What Changes +- Add per-client rate limiting to the public API. +``` + +A change with `--json` is delta-shaped: + +```json +{ + "id": "add-rate-limit", + "title": "Add rate limiting", + "deltaCount": 1, + "deltas": [ + { + "spec": "api", + "operation": "ADDED", + "description": "Add requirement: The API SHALL limit each client to 100 requests per minute.", + "requirement": { + "text": "The API SHALL limit each client to 100 requests per minute.", + "scenarios": [ + { + "rawText": "- **WHEN** a client sends its 101st request within a minute\n- **THEN** the API responds 429" + } + ] + }, + ... + } + ], + "root": { + "path": "/Users/you/projects/my-app", + "source": "nearest" + } +} +``` + +A spec with `--json` lists its requirements with scenarios: + +```json +{ + "id": "api", + "title": "api", + "overview": "Public HTTP API behavior.", + "requirementCount": 1, + "requirements": [ + { + "text": "The API SHALL expose a health endpoint.", + "scenarios": [ + { + "rawText": "- **WHEN** a client requests GET /health\n- **THEN** the API responds 200" + } + ] + } + ], + "metadata": { + "version": "1.0.0", + "format": "openspec" + }, + "root": { + "path": "/Users/you/projects/my-app", + "source": "nearest" + } +} +``` + +An unknown name suggests near matches: `Unknown item 'does-not-exist'. Did you mean: add-rate-limit, api?`. A name that matches both a change and a spec errors and asks for `--type`. + +**Exit codes** + +- `0`: item printed. +- `1`: unknown or ambiguous name, no name outside a terminal, an out-of-range `-r` index, or `--requirements` combined with `-r`. + +## openspec view + +Prints a one-screen dashboard of specs and changes. + +```bash +openspec view # project summary in one screen +``` + +view prints the dashboard once and exits; it reads no keystrokes. Changes group by task progress: Draft (no tasks yet), Active (tasks underway, with a progress bar and percent), Completed (every task checked). Specs list with requirement counts, largest first. + +**Options** + +| Flag | Effect | +|---|---| +| `--store ` | Use a registered store as the OpenSpec root instead of the current project. | + +**Output** + +``` +OpenSpec Dashboard + +════════════════════════════════════════════════════════════ +Summary: + ● Specifications: 1 specs, 1 requirements + ● Draft Changes: 1 + ● Active Changes: 0 in progress + ● Completed Changes: 0 + +Draft Changes +──────────────────────────────────────────────────────────── + ○ add-rate-limit + +Specifications +──────────────────────────────────────────────────────────── + ▪ api 1 requirement + +════════════════════════════════════════════════════════════ + +Use openspec list --changes or openspec list --specs for detailed views +``` + +A `Task Progress` summary line appears when any change has tasks underway. + +**Exit codes** + +- `0`: dashboard printed. +- `1`: no OpenSpec root found (outside a project, no `--store`). + +## openspec validate + +Checks changes and specs for structural issues. + +```bash +openspec validate add-rate-limit # one change or spec, by name +openspec validate --all # every change and spec +``` + +With no name and no bulk flag, validate prompts you to pick items. Outside an interactive terminal it exits 1 and prints the bulk flags instead. + +**Arguments** + +| Argument | What it is | +|---|---| +| `item-name` | The change or spec to validate, by folder name (`add-rate-limit`, `api`). | + +**Options** + +| Flag | Effect | +|---|---| +| `--all` | Validate every change and spec. | +| `--changes` | Validate every change. | +| `--specs` | Validate every spec. | +| `--strict` | Treat warnings as failures. | +| `--type ` | Pick the type when a change and a spec share a name. | +| `--json` | Print a structured report instead of text. | +| `--concurrency ` | Max parallel validations in bulk runs. Default: `OPENSPEC_CONCURRENCY`, else 6. | +| `--no-interactive` | Never prompt: a missing or ambiguous name becomes an error. | +| `--store ` | Use a registered store as the OpenSpec root instead of the current project. | + +**Output** + +One line per item; bulk runs end with totals: + +``` +✓ change/add-rate-limit +✓ spec/api +Totals: 2 passed, 0 failed (2 items) +``` + +A failing item lists each issue and the fix: + +``` +Change 'add-rate-limit' has issues +✗ [ERROR] api/spec.md: ADDED "Rate limiting" must include at least one scenario +Next steps: + - Ensure change has deltas in specs/: use headers ## ADDED/MODIFIED/REMOVED/RENAMED Requirements + - Each requirement MUST include at least one #### Scenario: block + - Debug parsed deltas: openspec show add-rate-limit --json --deltas-only +``` + +`--json` prints one report for the run: + +```json +{ + "items": [ + { + "id": "add-rate-limit", + "type": "change", + "valid": true, + "issues": [], + "durationMs": 2 + } + ], + "summary": { + "totals": { + "items": 1, + "passed": 1, + "failed": 0 + }, + "byType": { + "change": { + "items": 1, + "passed": 1, + "failed": 0 + } + } + }, + "version": "1.0", + "root": { + "path": "/Users/you/projects/my-app", + "source": "nearest" + } +} +``` + +`issues` entries carry a `level` of `ERROR`, `WARNING`, or `INFO`. + +**Exit codes** + +- `0`: every validated item passed. +- `1`: an item failed, or the run couldn't validate anything (unknown name, nothing to validate). + +## openspec archive + +Moves a completed change to the archive and updates the main specs. + +```bash +openspec archive add-rate-limit -y # archive one change, merge its deltas +openspec archive add-rate-limit -y --skip-specs # archive without touching the specs +``` + +With no name, archive prompts you to pick a change. Outside an interactive terminal it exits 1 and prints the rerun command instead. + +**Arguments** + +| Argument | What it is | +|---|---| +| `change-name` | The change to archive, by folder name (`add-rate-limit`). | + +**Options** + +| Flag | Effect | +|---|---| +| `-y, --yes` | Answer yes to every confirmation: spec updates, incomplete tasks, skipped validation. | +| `--skip-specs` | Archive without touching the main specs (infrastructure, tooling, or doc-only changes). | +| `--no-validate` | Skip validation. Archive asks you to confirm first; `-y` answers it. | +| `--json` | Print a structured result instead of text. Needs `--yes` to confirm spec updates. | +| `--store ` | Use a registered store as the OpenSpec root instead of the current project. | + +**Output** + +A successful run reports task status, previews the spec updates, applies them, and names the archive folder: + +``` +Task status: ✓ Complete + +Specs to update: + api: update +Applying changes to openspec/specs/api/spec.md: + + 1 added +Totals: + 1, ~ 0, - 0, → 0 +Specs updated successfully. +Change 'add-rate-limit' archived as '2026-08-11-add-rate-limit'. +``` + +The change folder moves whole to `openspec/changes/archive/2026-08-11-add-rate-limit/`, today's date prefixed to its name. Each delta merges into its main spec: the ADDED requirement above was appended to `openspec/specs/api/spec.md`. Without `-y`, archive shows the preview and asks before updating; declining still archives the change and leaves the specs alone. + +With `--json --yes`: + +```json +{ + "archive": { + "change": "add-rate-limit", + "archivedAs": "2026-08-11-add-rate-limit", + "path": "/Users/you/projects/my-app/openspec/changes/archive/2026-08-11-add-rate-limit", + "specsUpdated": true, + "totals": { + "added": 1, + "modified": 0, + "removed": 0, + "renamed": 0 + } + }, + "root": { + "path": "/Users/you/projects/my-app", + "source": "nearest" + } +} +``` + +Archive validates the change first and refuses one that fails: + +``` +Validation errors in change delta specs: + ✗ ADDED "Rate limiting" must include at least one scenario + +Validation failed. Please fix the errors before archiving. +To skip validation (not recommended), use --no-validate flag. +``` + +Incomplete tasks warn but don't block. Interactively archive asks whether to continue; `-y` continues on its own: + +``` +Task status: 1/2 tasks +Warning: 1 incomplete task(s) found. Continuing due to --yes flag. +``` + +**Exit codes** + +- `0`: the change was archived, with or without spec updates. +- `1`: validation failed, the change name is unknown, or a confirmation was needed and no answer could be read. + +## openspec new + +Creates a new change directory. + +```bash +openspec new change add-caching # metadata only +openspec new change add-search --goal "Users can search docs" # record a goal +``` + +`new` has one subcommand, `new change `. It creates `openspec/changes//` containing a single [`.openspec.yaml` metadata file](configuration/change-metadata.md): + +```yaml +schema: spec-driven +created: 2026-08-11 +``` + +Artifacts (proposal, specs, design, tasks) aren't scaffolded here. You write them later, and `openspec status` tells you which one is next. + +**Arguments** + +| Argument | What it is | +|---|---| +| `name` | Folder name for the change (`add-caching`). | + +**Options** + +| Flag | Effect | +|---|---| +| `--description ` | Also create a `README.md` in the change directory with this text. | +| `--goal ` | Store a `goal:` line in `.openspec.yaml`. | +| `--schema ` | Workflow schema for the change. Default: `spec-driven`, the only schema that ships. | +| `--json` | Print the created change as JSON instead of text. | +| `--store ` | Use a registered store as the OpenSpec root instead of the current project. | + +**Output** + +``` +Created change 'add-caching' at openspec/changes/add-caching/ +Schema: spec-driven +Next: openspec status --change add-caching +``` + +With `--json`: + +```json +{ + "change": { + "id": "add-caching", + "path": "/Users/you/projects/my-app/openspec/changes/add-caching", + "metadataPath": "/Users/you/projects/my-app/openspec/changes/add-caching/.openspec.yaml", + "schema": "spec-driven" + }, + "root": { + "path": "/Users/you/projects/my-app", + "source": "nearest" + } +} +``` + +**Exit codes** + +- `0`: change created. +- `1`: the change already exists, or the schema is unknown. + +## openspec status + +Reports artifact completion status for a change. + +```bash +openspec status --change add-rate-limit # checklist view +openspec status --change add-rate-limit --json # structured report +``` + +`--change` is required. Without it, status exits 1 and lists the available changes, even when only one exists: + +``` +✖ Error: Missing required option --change. Available changes: + add-rate-limit +``` + +**Options** + +| Flag | Effect | +|---|---| +| `--change ` | The change to report on, by folder name. | +| `--schema ` | Override the schema auto-detected from `openspec/config.yaml`. An unknown name is an error. | +| `--json` | Print a structured report instead of text. | +| `--store ` | Use a registered store as the OpenSpec root instead of the current project. | + +**Output** + +A checklist of the schema's artifacts: `[x]` done, `[ ]` ready to write, `[-]` blocked until the artifacts it depends on exist. + +``` +Change: add-rate-limit +Schema: spec-driven +Change root: /Users/you/projects/my-app/openspec/changes/add-rate-limit +Progress: 2/4 artifacts complete + +[x] proposal +[x] specs +[ ] design +[-] tasks (blocked by: design) +``` + +`--json` adds per-artifact dependencies, resolved file paths, and a suggested next step. Trimmed: + +```json +{ + "changeName": "add-rate-limit", + "schemaName": "spec-driven", + "isComplete": false, + "nextSteps": [ + "Run openspec instructions design --change \"add-rate-limit\" --json before writing that artifact." + ], + "artifacts": [ + { + "id": "proposal", + "outputPath": "proposal.md", + "status": "done", + "requires": [] + }, + { + "id": "design", + "outputPath": "design.md", + "status": "ready", + "requires": [ + "proposal" + ] + }, + { + "id": "tasks", + "outputPath": "tasks.md", + "status": "blocked", + "requires": [ + "specs", + "design" + ], + "missingDeps": [ + "design" + ] + } + ] +} +``` + +**Exit codes** + +- `0`: status printed. +- `1`: `--change` missing, the change doesn't exist, or the schema override is unknown. + +## openspec instructions + +Prints instructions for creating an artifact, applying, or archiving. Your agent runs this during the workflow to fetch the instruction text for its next step. + +```bash +openspec instructions proposal --change add-rate-limit # how to write one artifact +openspec instructions apply --change add-rate-limit # how to implement the change +openspec instructions archive --change add-rate-limit # inputs for archiving +``` + +**Arguments** + +| Argument | What it is | +|---|---| +| `artifact` | An artifact id from the schema (`proposal`, `specs`, `design`, `tasks` in `spec-driven`), or the reserved words `apply` and `archive`. | + +**Options** + +| Flag | Effect | +|---|---| +| `--change ` | The change to generate instructions for. Required. | +| `--schema ` | Override the schema. Auto-detected from `config.yaml` otherwise. | +| `--json` | Print a structured object instead of text. | +| `--store ` | Use a registered store as the OpenSpec root instead of the current project. | + +**Output** + +The artifact form prints one instruction block: the task, the file to write, how to write it, the artifact's template, and what completing it unlocks. + +``` + + + +Create the proposal artifact for change "add-rate-limit". +Initial proposal document outlining the change + + + +Write to: /Users/you/projects/my-app/openspec/changes/add-rate-limit/proposal.md + + + +Create the proposal document that establishes WHY this change is needed. +... +``` + +`apply` prints context files, task progress, and the working instruction: + +``` +## Apply: add-rate-limit +Schema: spec-driven + +### Context Files +- proposal: /Users/you/projects/my-app/openspec/changes/add-rate-limit/proposal.md +- specs: /Users/you/projects/my-app/openspec/changes/add-rate-limit/specs/api/spec.md +- tasks: /Users/you/projects/my-app/openspec/changes/add-rate-limit/tasks.md + +### Progress +1/3 complete + +### Tasks +- [x] 1.1 Add rate limit middleware +- [ ] 1.2 Return 429 with Retry-After header +- [ ] 1.3 Add tests for burst traffic + +### Instruction +Read context files, work through pending tasks, mark complete as you go. +Pause if you hit blockers or need clarification. + +No project context or operation guidance configured. +``` + +When required artifacts are missing, `apply` reports `### ⚠️ Blocked` and names them instead. `archive` prints the change name plus any project context and operation guidance from config; with none configured it says so and nothing more. + +With `--json`, each form returns one object. The artifact form starts: + +```json +{ + "changeName": "add-rate-limit", + "artifactId": "proposal", + "schemaName": "spec-driven", + "changeDir": "/Users/you/projects/my-app/openspec/changes/add-rate-limit", + ... +``` + +and continues with `outputPath`, `existingOutputPaths`, the full `instruction` and `template` strings, `dependencies`, `unlocks`, and `root`. The `apply` form carries `contextFiles`, `progress`, `tasks`, `state` (`blocked`, `ready`, `all_done`), and `instruction`. + +**Exit codes** + +- `0`: instructions printed. +- `1`: unknown artifact, unknown change, unknown schema, or missing `--change`. Each error lists the valid values. + +## openspec templates + +Prints the resolved template paths for a schema's artifacts. + +```bash +openspec templates # default schema: spec-driven +openspec templates --json # map of artifact ids to paths +``` + +**Options** + +| Flag | Effect | +|---|---| +| `--schema ` | Schema to resolve. Default: `spec-driven`. | +| `--json` | Print a JSON map of artifact ids to template paths. | + +**Output** + +``` +Schema: spec-driven +Source: package + +proposal: + /usr/local/lib/node_modules/@fission-ai/openspec/schemas/spec-driven/templates/proposal.md +specs: + /usr/local/lib/node_modules/@fission-ai/openspec/schemas/spec-driven/templates/spec.md +design: + /usr/local/lib/node_modules/@fission-ai/openspec/schemas/spec-driven/templates/design.md +tasks: + /usr/local/lib/node_modules/@fission-ai/openspec/schemas/spec-driven/templates/tasks.md +``` + +`Source` names where the schema resolved from: `project` (`openspec/schemas/` in your project), `user` (a global override), or `package` (built into the CLI). Project wins over user, user over package. + +```json +{ + "proposal": { + "path": "/usr/local/lib/node_modules/@fission-ai/openspec/schemas/spec-driven/templates/proposal.md", + "source": "package" + }, + "specs": { + "path": "/usr/local/lib/node_modules/@fission-ai/openspec/schemas/spec-driven/templates/spec.md", + "source": "package" + }, + "design": { + "path": "/usr/local/lib/node_modules/@fission-ai/openspec/schemas/spec-driven/templates/design.md", + "source": "package" + }, + "tasks": { + "path": "/usr/local/lib/node_modules/@fission-ai/openspec/schemas/spec-driven/templates/tasks.md", + "source": "package" + } +} +``` + +**Exit codes** + +- `0`: paths printed. +- `1`: unknown schema. The error lists available schemas. + +## openspec schemas + +Lists available workflow schemas. + +```bash +openspec schemas # names, descriptions, artifact order +openspec schemas --json # machine-readable, for agent use +``` + +**Options** + +| Flag | Effect | +|---|---| +| `--json` | Output as JSON (for agent use). | + +**Output** + +``` +Available schemas: + + spec-driven + Default OpenSpec workflow - proposal → specs → design → tasks + Artifacts: proposal → specs → design → tasks +``` + +Schemas from your project are labeled `(project)`; global overrides are labeled `(user override)`. + +```json +[ + { + "name": "spec-driven", + "description": "Default OpenSpec workflow - proposal → specs → design → tasks", + "artifacts": [ + "proposal", + "specs", + "design", + "tasks" + ], + "source": "package" + } +] +``` + +**Exit codes** + +- `0`: schemas listed. +- `1`: the schema list couldn't be read. + +## openspec schema + +Inspects, forks, or creates a schema (experimental). The CLI marks the whole group experimental: every subcommand first prints `Note: Schema commands are experimental and may change.` on stderr. + +```bash +openspec schema which spec-driven # where a schema resolves from +openspec schema fork spec-driven my-flow # copy a schema into the project +openspec schema init my-schema # create a schema from scratch +``` + +| Subcommand | What it does | +|---|---| +| `which` | Show where a schema resolves from. | +| `validate` | Check a schema's structure and templates. | +| `fork` | Copy an existing schema into the project for customization. | +| `init` | Create a new project-local schema. | + +Schemas resolve from three locations. The first match wins: + +| Source | Location | +|---|---| +| `project` | `openspec/schemas/` in the current project. | +| `user` | `~/.local/share/openspec/schemas/` (`XDG_DATA_HOME` and Windows `%LOCALAPPDATA%` respected). | +| `package` | The schemas shipped with the CLI. `spec-driven` lives here. | + +### openspec schema which + +Shows which copy of a schema the CLI will use. + +```bash +openspec schema which spec-driven +openspec schema which --all # every schema, grouped by source +``` + +**Arguments** + +| Argument | What it is | +|---|---| +| `name` | The schema to look up. Required unless `--all` is set; without either, which exits 1. | + +**Options** + +| Flag | Effect | +|---|---| +| `--all` | List every schema with its resolution source. | +| `--json` | Print the resolution as JSON. | + +**Output** + +``` +Schema: spec-driven +Source: package +Path: /usr/local/lib/node_modules/@fission-ai/openspec/schemas/spec-driven +``` + +When a higher-priority copy hides another, a `Shadows:` section lists the hidden copies. With `--json`: + +```json +{ + "name": "my-flow", + "source": "project", + "path": "/Users/you/projects/my-app/openspec/schemas/my-flow", + "shadows": [] +} +``` + +An unknown name exits 1 and lists the available schemas. + +### openspec schema validate + +Checks a schema's structure and templates. + +```bash +openspec schema validate spec-driven # one schema, from any source +openspec schema validate # every project-local schema +``` + +It verifies that `schema.yaml` exists and parses, that the structure matches the schema format, that every artifact's template file exists inside the schema's `templates/` directory, and that the dependency graph has no cycles or unknown references. + +**Options** + +| Flag | Effect | +|---|---| +| `--json` | Print a structured report instead of text. | +| `--verbose` | Print each validation step. | + +**Output** + +``` +✓ Schema 'spec-driven' is valid +``` + +With no name, each project schema gets one line under a `Validation Results:` header. A failing schema lists its issues and the run exits 1: + +``` +✗ Schema 'my-schema' has errors: + error: Template file 'tasks.md' not found for artifact 'tasks' +``` + +### openspec schema fork + +Copies an existing schema into the project so you can customize it. + +```bash +openspec schema fork spec-driven my-flow +``` + +**Arguments** + +| Argument | What it is | +|---|---| +| `source` | The schema to copy, from any source location. | +| `name` | Name for the copy. Kebab-case (`my-workflow`). Default: `-custom`. | + +**Options** + +| Flag | Effect | +|---|---| +| `--force` | Overwrite an existing destination schema. | +| `--json` | Print the result as JSON. | + +**Output** + +``` +✔ Forked 'spec-driven' to 'my-flow' + +Source: /usr/local/lib/node_modules/@fission-ai/openspec/schemas/spec-driven (package) +Destination: /Users/you/projects/my-app/openspec/schemas/my-flow +``` + +The fork lands in `openspec/schemas/`, and the `name:` field in its `schema.yaml` is rewritten to the new name: + +``` +openspec/schemas/my-flow/ +├── schema.yaml +└── templates/ + ├── design.md + ├── proposal.md + ├── spec.md + └── tasks.md +``` + +An existing destination is an error unless you pass `--force`. A fork that keeps the source's name shadows the original. + +### openspec schema init + +Creates a new project-local schema with starter templates. + +```bash +openspec schema init my-schema --description "Lightweight flow" --artifacts proposal,tasks +``` + +With no `--description` and no `--artifacts` in an interactive terminal, init prompts for a description, an artifact checklist, and whether to make the schema the project default. Outside a terminal it uses the defaults below. + +**Arguments** + +| Argument | What it is | +|---|---| +| `name` | Name for the new schema. Kebab-case (`my-workflow`). | + +**Options** + +| Flag | Effect | +|---|---| +| `--description ` | Schema description. Default: `Custom workflow schema for `. | +| `--artifacts ` | Comma-separated artifact IDs from `proposal`, `specs`, `design`, `tasks`. Default: all four. | +| `--default` | Make the schema the project default: writes `defaultSchema` to `openspec/config.yaml`. | +| `--no-default` | Skip the prompt about the default. | +| `--force` | Overwrite an existing schema with the same name. | +| `--json` | Print the result as JSON. | + +**Output** + +``` +✔ Created schema 'my-schema' + +Schema created at: /Users/you/projects/my-app/openspec/schemas/my-schema + +Artifacts: proposal, tasks +``` + +The layout on disk: + +``` +openspec/schemas/my-schema/ +├── schema.yaml +└── templates/ + ├── proposal.md + └── tasks.md +``` + +`schema.yaml` wires the selected artifacts with their dependencies. When `tasks` is included it also gets an `apply` phase that tracks `tasks.md`. Use the schema with `openspec new --schema my-schema`. + +## openspec store + +Creates and manages stores: standalone OpenSpec repos registered on your machine. + +```bash +openspec store setup team-context --path ~/openspec/team-context # create and register +openspec store register ~/stores/design-system # register an existing checkout +openspec store list # see what's registered +``` + +Registrations live in a per-machine registry: `~/.local/share/openspec/stores/registry.yaml`, or `$XDG_DATA_HOME/openspec/stores/registry.yaml` when `XDG_DATA_HOME` is set. Every subcommand takes `--json` to print a structured report instead of text. Running `openspec store` with a missing or unknown subcommand exits 1 and lists the subcommands. + +| Subcommand | What it does | +|---|---| +| `setup [id]` | Create a store folder and register it. | +| `register [path]` | Register an existing store folder. | +| `unregister ` | Forget the registration. The folder stays on disk. | +| `remove ` | Forget the registration and delete the folder. | +| `list` (alias `ls`) | List registered stores. | +| `doctor [id]` | Check registration, metadata, and Git state for registered stores. | + +### openspec store setup + +Creates a store folder and registers it. + +```bash +openspec store setup team-context --path ~/openspec/team-context +``` + +In an interactive terminal, setup prompts for a missing name and location and confirms before creating anything. Outside one, a missing name or `--path` exits 1 with the flag to pass. Rerunning setup for a registered store reports `Registry: already registered`. + +**Arguments** + +| Argument | What it is | +|---|---| +| `id` | The store name. It becomes the id you pass to `--store`. | + +**Options** + +| Flag | Effect | +|---|---| +| `--path ` | Folder where the store should live (`~` expands). | +| `--init-git` | Initialize a Git repository with an initial commit. Default. | +| `--no-init-git` | Skip every Git action: no init, no initial commit. | +| `--remote ` | Canonical clone source recorded in `store.yaml`. | + +**Output** + +``` +Store ready: team-context +Location: /Users/you/stores/team-context +OpenSpec root: ready +Registry: registered + +Next: run normal OpenSpec commands against this store, for example: + openspec new change --store team-context +Share this store by committing and pushing it like any Git repo. +``` + +`--json` reports what was created and where it was registered: + +```json +{ + "store": { + "id": "design-system", + "root": "/Users/you/stores/design-system", + "metadata_path": "/Users/you/stores/design-system/.openspec-store/store.yaml" + }, + "registry": { + "path": "/Users/you/.local/share/openspec/stores/registry.yaml", + "registered": true, + "already_registered": false + }, + "git": { + "is_repository": true, + "initialized": true, + "committed": true + }, + "created_files": [ + "openspec/", + "openspec/specs/", + "openspec/changes/", + "openspec/changes/archive/", + "openspec/config.yaml", + "openspec/specs/.gitkeep", + "openspec/changes/archive/.gitkeep", + ".openspec-store/store.yaml" + ], + "status": [] +} +``` + +### openspec store register + +Registers an existing store folder, for example a teammate's store you cloned. + +```bash +openspec store register ~/stores/design-system +``` + +The folder must contain a healthy `openspec/` root. With `.openspec-store/store.yaml` present, register reuses the recorded id. Without it, register asks before creating that metadata; outside an interactive terminal that needs `--yes`. One checkout per store id on a machine: a second path under the same id, or the same path under a second id, exits 1. + +**Arguments** + +| Argument | What it is | +|---|---| +| `path` | The store folder to register (`~` expands). Required. | + +**Options** + +| Flag | Effect | +|---|---| +| `--id ` | Store id; defaults to metadata or folder name. | +| `--yes` | Confirm creating store identity metadata for a healthy OpenSpec root. | + +**Output** + +``` +Store registered: design-system +Location: /Users/you/stores/design-system +OpenSpec root: ready +Registry: registered +``` + +`--json` prints the same document shape as `store setup --json`. + +### openspec store unregister + +Forgets the registration. The folder stays on disk. + +```bash +openspec store unregister design-system +``` + +``` +Unregistered store: design-system +Files kept at: /Users/you/stores/design-system +``` + +### openspec store remove + +Forgets the registration and deletes the folder. + +```bash +openspec store remove design-system --yes +``` + +Interactively, remove asks before deleting. With `--json` or outside an interactive terminal, deletion requires `--yes`: + +``` +Error: Pass --yes to delete store files non-interactively. +Fix: openspec store remove design-system --yes +``` + +**Options** + +| Flag | Effect | +|---|---| +| `--yes` | Confirm local store folder deletion. | + +**Output** + +``` +Removed store: design-system +Deleted: /Users/you/stores/design-system +``` + +### openspec store list + +Lists registered stores. `ls` is an alias. + +```bash +openspec store list +``` + +``` +OpenSpec stores (2) + +ID Location +design-system /Users/you/stores/design-system +team-context /Users/you/stores/team-context +``` + +With nothing registered, list prints `No stores registered.` and the setup and register commands to run next. + +### openspec store doctor + +Checks registration, metadata, and Git state for registered stores. + +```bash +openspec store doctor # every registered store +openspec store doctor team-context # one store +``` + +**Output** + +``` +Store doctor + +team-context + Location: /Users/you/stores/team-context + OpenSpec root: ok + Metadata: ok + Git: repository detected (commits: yes, uncommitted changes: no, remote: none) + Issues: none +``` + +**Exit codes** + +- `0`: the report printed, even when a store reports issues. +- `1`: the report couldn't run (for example an unknown store id). + +## openspec doctor + +Reports relationship health for the resolved OpenSpec root. + +```bash +openspec doctor # nearest openspec/ root above your cwd +openspec doctor --store team-context # a registered store as the root +``` + +Doctor is read-only: it never clones, syncs, or repairs. It reports whether the root is healthy and whether each reference declared in `openspec/config.yaml` resolves on this machine. With no root above your cwd and no `--store`, it exits 1 and names your registered stores. + +**Options** + +| Flag | Effect | +|---|---| +| `--store ` | Use a registered store as the OpenSpec root instead of the current project. | +| `--json` | Print the health report as JSON. | + +**Output** + +``` +Doctor + +Root + Location: /Users/you/projects/my-app + OpenSpec root: ok + +References + - team-context: ok (/Users/you/stores/team-context) +``` + +With `--store`, the root is the store and the report adds a store line: + +``` +Using OpenSpec root: team-context (/Users/you/stores/team-context) +Doctor + +Root + Location: /Users/you/stores/team-context + OpenSpec root: ok + Store: team-context (metadata ok) + +References + (none declared) +``` + +```json +{ + "root": { + "path": "/Users/you/projects/my-app", + "source": "nearest", + "healthy": true, + "status": [] + }, + "store": null, + "references": [ + { + "store_id": "team-context", + "root": "/Users/you/stores/team-context", + "status": [] + } + ], + "status": [] +} +``` + +**Exit codes** + +- `0`: the report printed, including when it lists issues. +- `1`: no root resolved (no `openspec/` above your cwd and no `--store`), or an unknown `--store` id. + +## openspec context + +Prints the working context for the resolved OpenSpec root: the root plus every referenced store declared in `openspec/config.yaml`, each with a fetch command. + +```bash +openspec context # nearest openspec/ root above your cwd +openspec context --store team-context # a registered store as the root +openspec context --json # agent brief +``` + +References that don't resolve on this machine land in a `Not available on this machine` section, each with a fix. + +**Options** + +| Flag | Effect | +|---|---| +| `--store ` | Use a registered store as the OpenSpec root instead of the current project. | +| `--json` | Print the agent brief as JSON. | +| `--code-workspace ` | Also write a VS Code workspace file for the set. | +| `--force` | Overwrite an existing `--code-workspace` file. | + +**Output** + +``` +Working context for my-app (/Users/you/projects/my-app) + +OpenSpec root + my-app /Users/you/projects/my-app + +Referenced stores + team-context /Users/you/stores/team-context + Fetch: openspec show --type spec --store team-context +``` + +With `--store`, the store is the whole set: + +``` +Using OpenSpec root: team-context (/Users/you/stores/team-context) +Working context for team-context (/Users/you/stores/team-context) + +OpenSpec root + team-context /Users/you/stores/team-context + +No references declared; the working set is this root alone. +``` + +```json +{ + "root": { + "path": "/Users/you/projects/my-app", + "source": "nearest", + "role": "openspec_root" + }, + "members": [ + { + "role": "referenced_store", + "id": "team-context", + "path": "/Users/you/stores/team-context", + "fetch": "openspec show --type spec --store team-context", + "status": [] + } + ], + "status": [] +} +``` + +**Writing a workspace file** + +`--code-workspace` writes a VS Code workspace file at the path you give: one folder for the root, one `ref:` folder per available referenced store. Unavailable references are skipped and named in the summary line, `Wrote /Users/you/projects/my-app/openspec.code-workspace (2 folders)`. The summary prints on stderr, so `--json` stdout stays one JSON document. An existing file exits 1 unless you pass `--force`. + +```json +{ + "folders": [ + { + "name": "my-app", + "path": "/Users/you/projects/my-app" + }, + { + "name": "ref:team-context", + "path": "/Users/you/stores/team-context" + } + ] +} +``` + +**Exit codes** + +- `0`: the report printed. +- `1`: no root resolved (no `openspec/` above your cwd and no `--store`), or the `--code-workspace` write was refused. + +## openspec workset + +Composes, keeps, and opens personal working views. A workset is a saved, named list of folders you work across together. + +```bash +openspec workset create checkout --member ~/projects/checkout-api --member web=~/projects/checkout-web +openspec workset list +openspec workset remove checkout --yes +``` + +| Subcommand | What it does | +|---|---| +| `create [name]` | Compose and save a named working view of folders you choose. | +| `list`, `ls` | Show saved worksets with their members. | +| `open ` | Open a saved workset in your tool (editor window or agent session). | +| `remove ` | Delete a saved workset (member folders are never touched). | + +A workset is purely local: + +- Its state lives in one folder: `~/.local/share/openspec/worksets/` (`$XDG_DATA_HOME/openspec/worksets/` when set; `%LOCALAPPDATA%\openspec\worksets\` on Windows). +- Nothing is written into the member folders, and nothing is committed or shared. +- Deleting that one folder removes every trace. + +### openspec workset create + +Saves a named working view of folders. + +```bash +openspec workset create checkout \ + --member ~/projects/checkout-api \ + --member web=~/projects/checkout-web +``` + +In an interactive terminal, create prompts for whatever the flags didn't provide: the name, folders one at a time, a tool, then an offer to open the workset now. Outside one, a missing name or member is an error. A name that's already saved is always an error; remove it first. + +**Arguments** + +| Argument | What it is | +|---|---| +| `name` | The workset name. Kebab-case: lowercase letters, numbers, single hyphens. Required outside an interactive terminal. | + +**Options** + +| Flag | Effect | +|---|---| +| `--member ` | Member folder as `` or `=`; repeatable, the first is the primary. The path must be an existing folder; the label defaults to the folder's own name. | +| `--tool ` | Preferred tool to open this workset with. Built-in ids: `code` (VS Code), `cursor` (Cursor); `claude` and `codex` are temporarily disabled. | +| `--json` | Print the saved workset as JSON. | + +**Output** + +``` +Saved workset 'checkout' (2 members) to your machine. +Open it any time with: openspec workset open checkout +``` + +### openspec workset list + +Shows saved worksets with their members, sorted by name. + +```bash +openspec workset list # alias: ls +``` + +**Options** + +| Flag | Effect | +|---|---| +| `--json` | Print the worksets as JSON. | + +**Output** + +One block per workset: the name, its tool when it has one, then one `name path` row per member. With nothing saved, list prints `No worksets saved. Create one with: openspec workset create`. + +``` +checkout + checkout-api /Users/you/projects/checkout-api + web /Users/you/projects/checkout-web +checkout-tool (opens in VS Code) + checkout-api /Users/you/projects/checkout-api +``` + +With `--json`: + +```json +{ + "worksets": [ + { + "name": "checkout", + "members": [ + { + "name": "checkout-api", + "path": "/Users/you/projects/checkout-api" + }, + { + "name": "web", + "path": "/Users/you/projects/checkout-web" + } + ] + } + ], + "status": [] +} +``` + +### openspec workset open + +Opens a saved workset in your tool. Editor tools (`code`, `cursor`) get a generated `.code-workspace` file: a window opens and the command returns. CLI agent tools (`claude`, `codex`) would take over this terminal with every member attached; they are temporarily disabled while that flow is reworked, so worksets open in an IDE for now. + +```bash +openspec workset open checkout # saved tool, or a prompt +openspec workset open checkout --tool cursor # this tool just this once +``` + +**Arguments** + +| Argument | What it is | +|---|---| +| `name` | The workset to open. | + +**Options** + +| Flag | Effect | +|---|---| +| `--tool ` | Open with this tool just this once. | + +With no `--tool` and no saved tool, open prompts you to pick an installed tool; outside an interactive terminal it exits 1 instead. + +- A member folder that no longer exists is skipped with a warning. When the primary is missing, the next surviving member becomes the primary for this open. When no member folder exists, the open fails. +- `--json` is rejected: open hands the terminal to the tool and has no JSON mode. +- When the launch fails, the error ends with the manual route: the workspace file's path and the member list. + +**Exit codes** + +- Mirrors the tool: the command exits with the tool's own exit code, and a signal becomes `128+n` (`130` after Ctrl-C). +- `1`: unknown workset, no member folder available, or no usable tool. + +### openspec workset remove + +Deletes a saved workset and its generated `.code-workspace` file. Member folders are never touched. + +```bash +openspec workset remove checkout --yes +``` + +In an interactive terminal, remove shows the workset and asks you to confirm. With `--json`, or outside a terminal, it requires `--yes` and exits 1 without it. + +**Options** + +| Flag | Effect | +|---|---| +| `--yes` | Confirm removal non-interactively. | +| `--json` | Print the removal as JSON. | + +**Output** + +``` +Removed workset 'checkout'. Member folders were not touched. +``` + +## openspec feedback + +Submits feedback about OpenSpec. + +```bash +openspec feedback "Validate output is hard to scan" +openspec feedback "Archive fails on Windows" --body "Steps: init, propose, archive. Error: EPERM." +``` + +The CLI files your message as a GitHub issue on the `Fission-AI/OpenSpec` repo through your `gh` CLI. The title becomes `Feedback: `. The body holds your `--body` text plus a footer with CLI version, platform, and timestamp. The issue gets the `feedback` label; if the repo doesn't define that label, the CLI retries without it and says so. + +**Arguments** + +| Argument | What it is | +|---|---| +| `message` | One-line summary. Becomes the issue title. Required. | + +**Options** + +| Flag | Effect | +|---|---| +| `--body ` | Longer description added to the issue body. | + +**Output** + +On success: + +``` +✓ Feedback submitted successfully! +Issue URL: https://github.com/Fission-AI/OpenSpec/issues/1234 +``` + +Without `gh` installed, or with `gh` not logged in, nothing is submitted. The CLI prints your formatted feedback between `--- FORMATTED FEEDBACK ---` markers, then a prefilled new-issue URL to open in the browser. The not-logged-in path adds `To auto-submit in the future: gh auth login`. + +**Exit codes** + +- `0`: issue created, or the manual-submission fallback ran (no `gh`, or `gh` not logged in). +- `1`: no message given. +- `gh`'s own code: `gh` failed after authentication (network, rate limit, issues disabled). The CLI reprints your feedback and the manual-submission URL first. + +## openspec completion + +Installs or generates shell completions. + +```bash +openspec completion install # detect your shell, install, wire up config +openspec completion generate zsh # print the script to stdout +``` + +Supported shells: `zsh`, `bash`, `fish`, `powershell`. Every subcommand takes an optional shell argument; omit it and the CLI detects your shell from the environment. + +| Subcommand | What it does | +|---|---| +| `generate [shell]` | Print the completion script to stdout. | +| `install [shell]` | Write the script and configure your shell startup file. | +| `uninstall [shell]` | Remove the script and the config block. | + +### openspec completion generate + +Prints the script; nothing is written. + +``` +#compdef openspec + +# Zsh completion script for OpenSpec CLI +# Auto-generated - do not edit manually + +_openspec() { + local context state line + typeset -A opt_args +... +``` + +### openspec completion install + +Writes the script and edits your shell config. Config edits sit between `# OPENSPEC:START` and `# OPENSPEC:END` markers. An existing script is backed up first (`.backup-` copy). + +| Shell | Script location | Config edited | +|---|---|---| +| zsh | `~/.zsh/completions/_openspec` | `~/.zshrc` | +| bash | `~/.local/share/bash-completion/completions/openspec` | `~/.bashrc` | +| fish | `~/.config/fish/completions/openspec.fish` | None: fish auto-loads it. | +| powershell | `OpenSpecCompletion.ps1` beside your profile | `$PROFILE` | + +With Oh My Zsh installed, the script lands in `$ZSH_CUSTOM/completions/_openspec` instead (default `~/.oh-my-zsh/custom/completions/_openspec`). + +**Options** + +| Flag | Effect | +|---|---| +| `--verbose` | Also print the installed path, any backup path, and which config file was edited. | + +**Output** + +``` +✓ Completion script installed and .zshrc configured successfully + +Restart your shell or run: exec zsh +``` + +### openspec completion uninstall + +Removes the script and the marked config block. It asks before touching your config (default: No). + +**Options** + +| Flag | Effect | +|---|---| +| `-y, --yes` | Skip confirmation prompts. | + +**Output** + +``` +✓ Completion script removed from /Users/you/.zsh/completions/_openspec. Removed OpenSpec configuration from ~/.zshrc +``` + +**Exit codes** + +- `0`: script generated, installed, or removed; also a cancelled uninstall. +- `1`: shell not supported or not detected, or an install or uninstall step failed. + +## openspec change + +Deprecated noun form of `show`, `list`, and `validate`. Every run warns and points to the verb-first commands, then runs anyway: + +``` +Warning: The "openspec change ..." commands are deprecated. Prefer verb-first commands (e.g., "openspec list", "openspec validate --changes"). +Warning: "openspec change list" is deprecated. Use "openspec list". +add-rate-limit +``` + +| Deprecated | Use instead | +|---|---| +| `openspec change show ` | `openspec show ` | +| `openspec change list` | `openspec list` | +| `openspec change validate ` | `openspec validate ` (all changes: `openspec validate --changes`) | + +The verb-first sections document the flags. + +## openspec spec + +Deprecated noun form of `show`, `list`, and `validate`. Every run warns and points to the verb-first commands, then runs anyway: + +``` +Warning: The "openspec spec ..." commands are deprecated. Prefer verb-first commands (e.g., "openspec show", "openspec validate --specs"). +api +``` + +| Deprecated | Use instead | +|---|---| +| `openspec spec show ` | `openspec show ` | +| `openspec spec list` | `openspec list --specs` | +| `openspec spec validate ` | `openspec validate ` (all specs: `openspec validate --specs`) | + +The verb-first sections document the flags. diff --git a/docs-lab/reference/configuration/change-metadata.md b/docs-lab/reference/configuration/change-metadata.md new file mode 100644 index 0000000000..5795a8605f --- /dev/null +++ b/docs-lab/reference/configuration/change-metadata.md @@ -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//.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: 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. Unknown top-level keys are the one exception; they are ignored rather than rejected. diff --git a/docs-lab/reference/configuration/config-json.md b/docs-lab/reference/configuration/config-json.md new file mode 100644 index 0000000000..0af65a161a --- /dev/null +++ b/docs-lab/reference/configuration/config-json.md @@ -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. 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 + } +} +``` diff --git a/docs-lab/reference/configuration/config-yaml.md b/docs-lab/reference/configuration/config-yaml.md new file mode 100644 index 0000000000..54da4db3ba --- /dev/null +++ b/docs-lab/reference/configuration/config-yaml.md @@ -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; 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`. diff --git a/docs-lab/reference/configuration/environment-variables.md b/docs-lab/reference/configuration/environment-variables.md new file mode 100644 index 0000000000..dc1c608ef8 --- /dev/null +++ b/docs-lab/reference/configuration/environment-variables.md @@ -0,0 +1,14 @@ +# Environment variables + +> Every environment variable OpenSpec reads. + + + +## OPENSPEC_TELEMETRY + +## DO_NOT_TRACK + +## XDG_CONFIG_HOME and XDG_DATA_HOME diff --git a/docs-lab/reference/configuration/index.md b/docs-lab/reference/configuration/index.md new file mode 100644 index 0000000000..fdbe54a7b3 --- /dev/null +++ b/docs-lab/reference/configuration/index.md @@ -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//.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 | diff --git a/docs-lab/reference/configuration/stores.md b/docs-lab/reference/configuration/stores.md new file mode 100644 index 0000000000..1d843f51f5 --- /dev/null +++ b/docs-lab/reference/configuration/stores.md @@ -0,0 +1,22 @@ +# Stores + +> The files behind multi-repo stores: registry.yaml and store.yaml, and which root a command uses. + + + +## registry.yaml + +## store.yaml + +## Locations + +## Root resolution diff --git a/docs-lab/reference/glossary.md b/docs-lab/reference/glossary.md new file mode 100644 index 0000000000..c34b443b67 --- /dev/null +++ b/docs-lab/reference/glossary.md @@ -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; 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//spec.md`. | [Concepts](../guides/concepts.md) | +| **Change proposal** | One unit of work: a folder under `openspec/changes//` 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//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; nothing is shared. | [CLI](cli.md) | diff --git a/docs-lab/reference/schemas/index.md b/docs-lab/reference/schemas/index.md new file mode 100644 index 0000000000..b7631b4b89 --- /dev/null +++ b/docs-lab/reference/schemas/index.md @@ -0,0 +1,20 @@ +# Overview + +> Every available workflow schema and the artifacts it defines. + + + +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. diff --git a/docs-lab/reference/schemas/schema-yaml.md b/docs-lab/reference/schemas/schema-yaml.md new file mode 100644 index 0000000000..9f63cd7239 --- /dev/null +++ b/docs-lab/reference/schemas/schema-yaml.md @@ -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//`: + +```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** | `/openspec/schemas//` | +| **2. User, macOS and Linux** | `~/.local/share/openspec/schemas//` | +| **2. User, Windows** | `%LOCALAPPDATA%\openspec\schemas\\` | +| **3. Package** | The schemas installed with the CLI | + +If `XDG_DATA_HOME` is set, the user directory moves to `$XDG_DATA_HOME/openspec/schemas//` 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 `](../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 `](../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. | diff --git a/docs-lab/reference/schemas/spec-driven/index.md b/docs-lab/reference/schemas/spec-driven/index.md new file mode 100644 index 0000000000..748e8e5cfe --- /dev/null +++ b/docs-lab/reference/schemas/spec-driven/index.md @@ -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//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 + + + +## What Changes + + + +## Capabilities + +### New Capabilities + +- ``: + +### Modified Capabilities + +- ``: + +## Impact + + +``` + +### 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//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: 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 + + +## ADDED Requirements + +### Requirement: + + +#### Scenario: +- **WHEN** +- **THEN** +``` + +### 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. +`` 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//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//` when creating the delta at `specs//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: ` followed by description +- Use SHALL/MUST for normative requirements (avoid should/may) +- Each scenario: `#### Scenario: ` 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//spec.md` directly. + +MODIFIED requirements workflow: +1. Locate the existing requirement in openspec/specs//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 + + + +## Goals / Non-Goals + +**Goals:** + + +**Non-Goals:** + + +## Decisions + + + +## Risks / 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. + +- [ ] 1.1 +- [ ] 1.2 + +## 2. + +- [ ] 2.1 +- [ ] 2.2 +``` + +### 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 isn't an artifact; it's the phase that works through `tasks.md`. + +- **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 +``` diff --git a/docs-lab/reference/skills.md b/docs-lab/reference/skills.md new file mode 100644 index 0000000000..6c6296f2ec --- /dev/null +++ b/docs-lab/reference/skills.md @@ -0,0 +1,176 @@ +# Skills + +> Every OpenSpec skill: arguments, what it creates, and what it responds with. + + + +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 just enters explore mode. | +| **Creates** | Nothing by default; it reads and investigates only. On request it captures insights: a new change proposal under `openspec/changes//`, 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//` 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; only those sync. | +| **Creates** | Edits or creates `openspec/specs//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-/` (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//` 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//` 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-/` 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. | diff --git a/docs-lab/reference/supported-tools.md b/docs-lab/reference/supported-tools.md new file mode 100644 index 0000000000..ec94dbf085 --- /dev/null +++ b/docs-lab/reference/supported-tools.md @@ -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 ` 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-`. Codex does not recognize the + `/openspec-` 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-`, + 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; 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. diff --git a/docs-lab/sources.md b/docs-lab/sources.md new file mode 100644 index 0000000000..abec00c00a --- /dev/null +++ b/docs-lab/sources.md @@ -0,0 +1,44 @@ +# 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` §Language and conventions | +| stores-beta/user-guide.md | `multi-repo/stores.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/cloudflare/router/worker.js`, verify `llms.txt` / `llms-full.txt` / +per-page markdown routes, then this folder replaces `docs/`. diff --git a/docs-lab/start/installation.md b/docs-lab/start/installation.md new file mode 100644 index 0000000000..c23ae89b41 --- /dev/null +++ b/docs-lab/start/installation.md @@ -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; 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 reccomend 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: 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=` 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; 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. diff --git a/docs-lab/start/overview.md b/docs-lab/start/overview.md new file mode 100644 index 0000000000..a1dbff85b7 --- /dev/null +++ b/docs-lab/start/overview.md @@ -0,0 +1,87 @@ +# Overview + +> OpenSpec gives you and your coding agent a shared, reviewable plan before code is written. + + + +## Building the right thing, and building it right + +## Agents make it go wrong faster + +## A shared map, not a plan up front + +## The loop in 60 seconds + +## Choose your path + +## Diagram options under review + + + +**A. Drift:** aligned at the start, small differences compound each session. + +![Option A: drift trajectories](/diagrams/option-a-drift.png) + +**B. Shared map:** private maps go stale; one shared map reroutes everyone. + +![Option B: shared map with reroute](/diagrams/option-b-shared-map.png) + +**C. Control loop:** the open loop ships and hopes; the closed loop measures every cycle against the spec. + +![Option C: open vs closed control loop](/diagrams/option-c-control-loop.png) + +**D. Sessions:** chat context dies between sessions; the plan on disk survives. + +![Option D: sessions over a persistent spine](/diagrams/option-d-sessions.png) + +**E. Native Mermaid, the loop:** the smallest possible statement of the cycle. Theme-aware, renders from text in this file. + +```mermaid +flowchart LR + p[propose] --> r[review] --> a[apply] --> ar[archive] + ar -- "specs absorb the change" --> p +``` + +**F. Native Mermaid, the closed loop (C, redrawn):** same argument as option C, drawn by Mermaid. + +```mermaid +flowchart LR + spec["the spec:
what right looks like"] --> check{aligned?} + check -- yes --> agent[agent builds] + agent --> code[code + artifacts] + code -- "review / verify" --> check + code -- "archive: the spec absorbs the change" --> spec +``` + +**G. Native Mermaid, sessions (D, redrawn):** same argument as option D, as a sequence diagram. + +```mermaid +sequenceDiagram + participant S1 as session 1 + participant S2 as session 2 + participant S3 as session 3 + participant M as openspec/ (the map) + S1->>M: propose: writes the plan + Note over S1: context lost + M->>S2: reads the plan + S2->>M: apply: checks off tasks 1-3 + Note over S2: context lost + M->>S3: reads the plan + S3->>M: archive: specs absorb the change +``` + +**H. Animated SVG (A, animated):** option A redrawn as a hand-authored SVG with CSS keyframes inside the file. The trajectories draw themselves and the loop repeats. No libraries, ships as a plain image, colors picked to read on light and dark. + +![Option H: animated drift](/diagrams/option-h-drift-animated.svg) diff --git a/docs-lab/start/quickstart.md b/docs-lab/start/quickstart.md new file mode 100644 index 0000000000..654762f02d --- /dev/null +++ b/docs-lab/start/quickstart.md @@ -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
think it through together"] --> propose["2 · Propose
agent drafts the plan"] + propose --> review["3 · Review
you correct the plan"] + review --> apply["4 · Apply
agent builds, task by task"] + apply --> archive["5 · Archive
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 + +Don't start with a plan. Start by thinking the idea through with your agent. In your AI chat: + +```text +/openspec-explore how rate limiting should work in this app +``` + +Explore is a thinking mode, not a writing 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 is the handoff: it 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: 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; 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 + + + +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. diff --git a/docs-lab/start/setup.md b/docs-lab/start/setup.md new file mode 100644 index 0000000000..c440adfdec --- /dev/null +++ b/docs-lab/start/setup.md @@ -0,0 +1,113 @@ +# 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 +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) +``` + +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. diff --git a/install.md b/install.md new file mode 100644 index 0000000000..a37c0fe402 --- /dev/null +++ b/install.md @@ -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 ` 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 --tools `. + +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 ` 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 `, 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 diff --git a/website/app/(home)/layout.tsx b/website/app/(home)/layout.tsx deleted file mode 100644 index 77379fac3f..0000000000 --- a/website/app/(home)/layout.tsx +++ /dev/null @@ -1,6 +0,0 @@ -import { HomeLayout } from 'fumadocs-ui/layouts/home'; -import { baseOptions } from '@/lib/layout.shared'; - -export default function Layout({ children }: LayoutProps<'/'>) { - return {children}; -} diff --git a/website/app/(home)/page.tsx b/website/app/(home)/page.tsx deleted file mode 100644 index 60433e7ec4..0000000000 --- a/website/app/(home)/page.tsx +++ /dev/null @@ -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 ( -
- - - - - - - - - - - -
- ); -} - -function Hero() { - return ( -
-
-
- - - The lightweight spec layer for AI coding - -

- Agree first. -
- Then build confidently. -

-

- 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 only then does code get written. No more - discovering halfway through that it built the wrong thing. -

-
- - Get started - - - Star on GitHub - -
- -
-
- ); -} - -function Terminal() { - return ( -
-
-
- - - - - your-project — AI chat - -
-
-          
-            /opsx:propose add-dark-mode
-            {'\n'}
-            
-              {'  '}✓ 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'}
-            
-            {'\n'}
-            /opsx:apply
-            {'\n'}
-            
-              {'  '}✓ working through tasks, checking each one off…{'\n'}
-            
-            {'\n'}
-            /opsx:archive
-            {'\n'}
-            
-              {'  '}✓ specs updated · change filed away · ready for the next one
-            
-          
-        
-
-
- ); -} - -const PHILOSOPHY = [ - ['fluid', 'not rigid'], - ['iterative', 'not waterfall'], - ['easy', 'not complex'], - ['brownfield', 'not just greenfield'], -]; - -function Philosophy() { - return ( -
-
- {PHILOSOPHY.map(([a, b]) => ( -
-
{a}
-
{b}
-
- ))} -
-
- ); -} - -function TwoFolders() { - return ( -
-
-

- The whole idea, in two folders -

-

- OpenSpec lives in one openspec/{' '} - directory in your repo. Two folders inside it carry the entire mental - model. -

-
-
-
-
- -
-

- specs/ — what is true -

-

- The source of truth. Plain-language requirements and scenarios that - describe how your system behaves right now, organized by - domain. This is the agreed-upon answer to “what does this - software do?” -

-
-
-
- -
-

- changes/ — what you are proposing -

-

- 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. -

-
-
-
- ); -} - -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 ( -
-
-
-

- Learn four ideas, and the rest is detail -

-

- Everything in OpenSpec is built from a handful of simple concepts. -

-
-
- {IDEAS.map(({ icon: Icon, title, body }) => ( -
- -

{title}

-

{body}

-
- ))} -
-
-
- ); -} - -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 ( -
-
-

The loop you run

-

- Two terminal commands to set up. After that, you live in your AI chat. -

-
-
    - {STEPS.map(({ icon: Icon, cmd, label, body }, i) => ( -
  1. -
    - - - {label ?? `step ${i + 1}`} - -
    - - {cmd} - -

    {body}

    -
  2. - ))} -
-
- ); -} - -function Why() { - return ( -
-
-
-

- Why bother with the extra step? -

-

- OpenSpec adds one small step — a short plan before building. Here is - what you get for it. -

-
-
- {[ - [ - '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]) => ( -
- -
-
{title}
-

{body}

-
-
- ))} -
-
-
- ); -} - -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 ( -
-
-
-

- For teams -

-

- Why teams adopt OpenSpec -

-

- 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{' '} - - stores - {' '} - put planning in a repo of its own — one source of truth your whole - team and every coding agent can read, shared by{' '} - git push like anything else. -

-
-
- {TEAM_SCENARIOS.map(({ icon: Icon, title, body }) => ( -
-
- -
-

{title}

-

{body}

-
- ))} -
-
- - Explore stores - - - Beta - -
-
-
- ); -} - -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 ( -
-

- Works with the tools you already use -

-
- {TOOLS.map((t) => ( - - {t} - - ))} - - + 15 more - -
-
- ); -} - -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 ( -
-
-

- What a change actually looks like -

-

- Plain Markdown files your AI drafts and you review. No new formats to - learn, nothing you cannot read at a glance. -

-
-
- {ARTIFACTS.map(({ icon: Icon, file, caption, code }) => ( -
-
- - {file} -
-
-              {code}
-            
-
- {caption} -
-
- ))} -
-
- ); -} - -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 ( -
-
-

The honest middle

-

- Heavier tools exist. So does doing nothing. OpenSpec aims for the - spot where the value clearly beats the cost. -

-
-
- {ROWS.map((r) => ( -
-
- {r.us && } - {r.name} - {r.by && ( - - {r.by} - - )} -
-
- {r.good}.{' '} - {r.catch}. -
-
- ))} -
-
- ); -} - -function FinalCta() { - return ( -
-

- Ship your first change in five minutes -

-

- Works with 30+ AI assistants — Claude Code, Cursor, Codex, Devin Desktop, - Gemini CLI, and more. -

-
-
- $ - npm install -g @fission-ai/openspec@latest -
-
- $ - cd your-project && openspec init -
-
-

- Or{' '} - - let your AI assistant install it for you - - . -

-
- - Read the getting-started guide - -
-
- ); -} diff --git a/website/app/docs/[[...slug]]/page.tsx b/website/app/docs/[[...slug]]/page.tsx index 1d2d034421..a1fd94ae2f 100644 --- a/website/app/docs/[[...slug]]/page.tsx +++ b/website/app/docs/[[...slug]]/page.tsx @@ -1,11 +1,12 @@ 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'; @@ -21,14 +22,13 @@ export default async function Page(props: PageProps<'/docs/[[...slug]]'>) { const markdownUrl = getPageMarkdownUrl(page).url; return ( - + {page.data.title} - {/* - 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 . - */} + {page.data.description}
) { + const { nav, ...base } = baseOptions(); return ( - + {children} ); diff --git a/website/app/global.css b/website/app/global.css index f9eb064351..a127bc3903 100644 --- a/website/app/global.css +++ b/website/app/global.css @@ -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; } diff --git a/website/app/layout.tsx b/website/app/layout.tsx index 243b59f38c..c8e5172f95 100644 --- a/website/app/layout.tsx +++ b/website/app/layout.tsx @@ -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', diff --git a/website/app/page.tsx b/website/app/page.tsx new file mode 100644 index 0000000000..8dd5f32fd9 --- /dev/null +++ b/website/app/page.tsx @@ -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 ( + <> + +

+ Redirecting to documentation… +

+ + ); +} diff --git a/website/app/sitemap.ts b/website/app/sitemap.ts index 5ed32ce8b7..83a2dc9b2e 100644 --- a/website/app/sitemap.ts +++ b/website/app/sitemap.ts @@ -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, - ]; } diff --git a/website/components/file-steps.tsx b/website/components/file-steps.tsx new file mode 100644 index 0000000000..17e998664a --- /dev/null +++ b/website/components/file-steps.tsx @@ -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 ( +
+
+
+ {steps.map((s, i) => ( + + {i > 0 && } + + + ))} +
+
+ + +
+
+ +
+ {steps.map((step, i) => ( +
+
+ Step {i + 1} + + {step.title} +
+ + {step.caption.length > 0 && ( +

+ {step.caption.join(' ')} +

+ )} + +
+              {step.lines.map((line, j) => (
+                
+ + {line.text} + {line.note && {line.note}} +
+ ))} +
+
+ ))} +
+
+ ); +} diff --git a/website/components/mdx.tsx b/website/components/mdx.tsx index d638e730f4..68345699a4 100644 --- a/website/components/mdx.tsx +++ b/website/components/mdx.tsx @@ -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; } diff --git a/website/docs.sync.config.mjs b/website/docs.sync.config.mjs index 0c7c220104..b739eaacf2 100644 --- a/website/docs.sync.config.mjs +++ b/website/docs.sync.config.mjs @@ -1,76 +1,138 @@ // 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 `/...` slugs +// and the sidebar shows them as a collapsible group inside the section. A page +// with slug `/index` is the folder's landing page (served at +// `/docs/`). 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' }, + // 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' }, ], }, { - label: 'Understand it', + label: 'Guides', 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' }, + { + 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: 'Guides', + label: 'Customize', 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' }, + { 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: 'Multi-repo (beta)', + pages: [{ source: 'multi-repo/stores.md', slug: 'stores' }], + }, + { label: 'Reference', - folder: 'reference', - icon: 'BookMarked', 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: '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' }, + { 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' }, + { + 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' }, + ], + }, ], }, { 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)); diff --git a/website/lib/layout.shared.tsx b/website/lib/layout.shared.tsx index 0ec454ff9b..e854e28870 100644 --- a/website/lib/layout.shared.tsx +++ b/website/lib/layout.shared.tsx @@ -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: ( - - OpenSpec - + {appName} ), }, + // 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, diff --git a/website/lib/remark-faq.ts b/website/lib/remark-faq.ts new file mode 100644 index 0000000000..1fcb5ff12b --- /dev/null +++ b/website/lib/remark-faq.ts @@ -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 }, + ]; + }; +} diff --git a/website/lib/remark-file-steps.ts b/website/lib/remark-file-steps.ts new file mode 100644 index 0000000000..248986c2bb --- /dev/null +++ b/website/lib/remark-file-steps.ts @@ -0,0 +1,38 @@ +// Turns `file-steps` fences into the interactive 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); + }; +} diff --git a/website/lib/remark-gfm-alert.ts b/website/lib/remark-gfm-alert.ts new file mode 100644 index 0000000000..0d6bfd1875 --- /dev/null +++ b/website/lib/remark-gfm-alert.ts @@ -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 = { + NOTE: 'info', + TIP: 'idea', + IMPORTANT: 'warn', + WARNING: 'warning', + CAUTION: 'error', +}; + +export const TYPE_TO_MARKER: Record = 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); + }; +} diff --git a/website/lib/source.ts b/website/lib/source.ts index e40147f089..19591939c6 100644 --- a/website/lib/source.ts +++ b/website/lib/source.ts @@ -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}) diff --git a/website/next.config.mjs b/website/next.config.mjs index d56c03567c..80c1cfd0ca 100644 --- a/website/next.config.mjs +++ b/website/next.config.mjs @@ -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. diff --git a/website/package.json b/website/package.json index 4b05a82a8d..80e28cfcb4 100644 --- a/website/package.json +++ b/website/package.json @@ -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" }, diff --git a/website/public/_redirects b/website/public/_redirects new file mode 100644 index 0000000000..da16935b3a --- /dev/null +++ b/website/public/_redirects @@ -0,0 +1,4 @@ +# 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 diff --git a/website/public/diagrams/option-a-drift.png b/website/public/diagrams/option-a-drift.png new file mode 100644 index 0000000000..671e325348 Binary files /dev/null and b/website/public/diagrams/option-a-drift.png differ diff --git a/website/public/diagrams/option-b-shared-map.png b/website/public/diagrams/option-b-shared-map.png new file mode 100644 index 0000000000..138b1075bd Binary files /dev/null and b/website/public/diagrams/option-b-shared-map.png differ diff --git a/website/public/diagrams/option-c-control-loop.png b/website/public/diagrams/option-c-control-loop.png new file mode 100644 index 0000000000..e719a979e7 Binary files /dev/null and b/website/public/diagrams/option-c-control-loop.png differ diff --git a/website/public/diagrams/option-d-sessions.png b/website/public/diagrams/option-d-sessions.png new file mode 100644 index 0000000000..104b23cfc6 Binary files /dev/null and b/website/public/diagrams/option-d-sessions.png differ diff --git a/website/public/diagrams/option-h-drift-animated.svg b/website/public/diagrams/option-h-drift-animated.svg new file mode 100644 index 0000000000..9f459780d0 --- /dev/null +++ b/website/public/diagrams/option-h-drift-animated.svg @@ -0,0 +1,109 @@ + + + + + Without a shared plan + + + + + the goal + + + you + + + agent, session 1 + + + agent, session 2 + + + teammate + + aligned at the start, small differences compound each session + + + + + + With a shared, updatable plan + + + + + dashed: the spec, one shared route + + + + + + + + + review + sync + + + the goal + + drift is corrected before it compounds + diff --git a/website/public/openspec-pixel.svg b/website/public/openspec-pixel.svg new file mode 100644 index 0000000000..3dda379ff4 --- /dev/null +++ b/website/public/openspec-pixel.svg @@ -0,0 +1,89 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/website/scripts/sync-docs.mjs b/website/scripts/sync-docs.mjs index 2db71402cb..81d7e0e199 100644 --- a/website/scripts/sync-docs.mjs +++ b/website/scripts/sync-docs.mjs @@ -1,38 +1,76 @@ #!/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 `` 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 `/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); + if (existsSync(path) && readFileSync(path, 'utf8') === 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 +91,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 +139,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/.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 +252,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(); diff --git a/website/source.config.ts b/website/source.config.ts index b3c9a76773..50bef2bc9a 100644 --- a/website/source.config.ts +++ b/website/source.config.ts @@ -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' } }]], }, });