OpenSpec dev workflow
How a developer (or an AI agent driving on their behalf) actually ships a behavior change through OpenSpec in this repo. Grounded in the real change we shipped recently: remove-show-underlying-data-sales-dashboard — a one-line revert that pulls Action.ShowUnderlyingData out of the sales dashboard's visibleActions allowlist.
If you only remember three things:
- Four slash commands drive the loop:
/opsx-explore(unclear scope) →/opsx-propose→/opsx-apply→/opsx-archive(with/opsx-verifyas a pre-archive sweep). Most tickets start with explore. - Four artifacts per change, built in dependency order:
proposal → design → specs → tasks. - The CLI is the source of truth. Slash commands are just thin wrappers around
npx openspec ...— when something looks weird, drop down to the CLI.
1. Mental model
OpenSpec splits "what we want to change" from "what the system permanently behaves like":
openspec/
config.yaml # repo-wide context + rules injected into every artifact prompt
specs/<capability>/spec.md # the living spec — what the system does today
changes/<change-name>/ # an in-flight change proposal
.openspec.yaml # schema marker (spec-driven)
proposal.md # WHY + scope
design.md # HOW (only if there's a real trade-off)
specs/<capability>/spec.md # the DELTA — ADDED / MODIFIED / REMOVED requirements
tasks.md # the implementation checklist (- [ ] / - [x])
changes/archive/YYYY-MM-DD-<name>/ # past changes, kept for history
openspec/config.yaml is the prompt header injected into every artifact prompt. In this repo it deliberately stays thin: it points the agent at AGENTS.md at the repo root for the bulk of the project context (full stack — Next.js 15 / React 19 / React Query v5 / MUI v6 / Tailwind / next-intl; auth flow; container pattern; server vs client component split; Playwright E2E as primary verification; OpenAPI-generated types in generated/; base path /analytics) and keeps only the residual rules — telemetry preservation and explicit non-goals — inline. You do not paste any of this yourself — config.yaml injects the pointer, the agent reads AGENTS.md on its own. Just write your artifact.
A change always has exactly one proposal.md, optionally one design.md, one tasks.md, and one delta spec file per affected capability under specs/<capability>/spec.md.
Change lifecycle
A change moves through seven states. Most tickets enter at Exploring, not Drafted — unclear requirements are the norm. The slash commands transition between them; the CLI does the actual work.
2. When to use OpenSpec, when to skip it
Use it for:
- Behavior changes in
src/app/**,src/containers/**,src/middleware.ts. - Anything user-visible (UI states, routes, role gates, embed configuration like the change we just shipped).
- Anything that affects auth, role gates, token-verification flows, or i18n message keys.
Skip it for:
- Type-only / codegen-only changes (
npm run codegenoutput). - Pure styling / Tailwind class adjustments with no behavior change.
- CI-only or tooling-only PRs.
- Renames, refactors, dependency bumps that don't change behavior.
Verbosity scales with change size. A one-line allowlist edit warrants a ~15-line proposal, often no design.md at all (or a single paragraph if there's a real trade-off), and a tasks file with 4–6 actionable bullets. Don't pad. The rules in openspec/config.yaml are floors, not quotas.
Decision flowchart
3. The slash commands
These live as Cursor commands in .cursor/commands/ and as Claude Code commands in .claude/commands/. Both directories are gitignored — each developer regenerates them locally with:
Naming convention differs by tool. Cursor invokes these as
/opsx-<cmd>(hyphen — e.g./opsx-propose). Claude Code invokes them as/opsx:<cmd>(colon — e.g./opsx:propose). The commands and their behavior are identical; only the invocation syntax differs. This doc uses the Cursor hyphen form throughout.
npx openspec update # regenerate for already-configured tools
npx openspec update --force # re-emit even when up to date
Heads-up — there is no
--tools <list>flag onupdate. That option only exists onopenspec init. The set of slash commandsupdatewrites is driven entirely by your global OpenSpec config at~/.config/openspec/config.json(profile,workflows,delivery). Earlier revisions of this doc told you to runnpx openspec update --tools cursor claude-code— that is wrong on the pinned@fission-ai/openspec ^1.3.1and will no-op or error.
The default core profile ships exactly four workflows: propose, explore, apply, archive. The other seven (new, continue, ff, sync, bulk-archive, verify, onboard) are only emitted when profile: "custom". To enable them — verify chief among them — run:
npx openspec config profile # interactive picker: switch to `custom`, tick workflows
npx openspec update # regenerate slash commands per the new profile
Re-run openspec update after every @fission-ai/openspec bump in package.json, and once after a fresh git clone. The slash commands are just thin wrappers around the CLI; when one drifts, drop to npx openspec ... directly.
/opsx-explore [topic or ticket description]
Use this when the ticket is vague, requirements conflict, or you don't yet know what capability is actually affected. Explore is a thinking-partner session — it does not scaffold any files.
Bring in:
- The ticket description or Jira/Linear link
- Any relevant existing spec (
openspec/specs/<capability>/spec.md) if you suspect it's in scope - Screenshots, error logs, or stakeholder notes
What it produces:
- A clearer problem statement
- Candidate capabilities affected
- Enough scope to write a focused
proposal.md
When to stop: When you can answer "what are we changing and why?" in two sentences. That answer becomes the description you hand to /opsx-propose.
Explore does not create a change folder. If exploration reveals no change is needed, nothing is left behind.
/opsx-propose <description-or-name>
Scaffolds the change folder and walks proposal → design → specs → tasks in order, asking for clarification only when context is genuinely unclear.
Under the hood it runs:
npx openspec new change "<name>" # scaffolds folder + .openspec.yaml
npx openspec status --change "<name>" --json # gets the artifact graph
npx openspec instructions <artifact> --change "<name>" --json # for each artifact, in dep order
Each instructions call returns a JSON envelope with:
instruction— schema-specific guidance for that artifact typetemplate— the section structure to fill incontext— repo-wide background (do not paste into the file; it's a constraint for you)rules— artifact-specific rules (also a constraint, not content)dependencies— completed artifacts you should read for context firstoutputPath— where to write
For MODIFIED Requirements, you must paste the entire existing requirement block from openspec/specs/<capability>/spec.md and edit it in place. Partial-content MODIFIED blocks lose detail at archive time — this is the single most common authoring mistake.
/opsx-apply [name]
Reads context (proposal, design, specs, tasks), then loops through - [ ] checkboxes in tasks.md, making the code edits and flipping each box to - [x] as it goes. It pauses on:
- An unclear task (asks for clarification)
- A blocker (e.g. missing dependency, broken types)
- A discovered design issue (suggests amending an artifact rather than guessing)
Tasks that are PR/QA hygiene (manual click-through, "note in PR description", "archive after merge") stay unchecked — those belong to the human, not the agent.
/opsx-verify (recommended, non-blocking)
Prerequisite — opt-in workflow.
verifyis not part of the defaultcoreprofile. Until you runnpx openspec config profile, switch tocustom, tickverify, and thennpx openspec update, the slash command will not exist on your machine. See §3 above.
Run between /opsx-apply and /opsx-archive. It sweeps the change against three axes:
- Completeness — all tasks done, requirements coded, scenarios tested.
- Correctness — implementation matches spec intent; edge cases handled.
- Coherence — design decisions reflected in code structure and naming.
It is non-blocking by design (upstream workflows doc): it surfaces issues at the point they are cheapest to fix. For high-risk changes (auth, middleware, role gates, anything that ships across multiple capabilities) treat it as mandatory; for trivial allowlist edits and config flips it remains a habit.
/opsx-archive [name]
After the PR merges:
- Verifies all artifacts are
done(warns if not, asks before continuing). - Counts unchecked tasks (warns if > 0, asks before continuing — the unchecked PR/QA tasks are normal).
- Diffs the change's delta specs against
openspec/specs/<capability>/spec.mdand offers to sync them. - Renames
openspec/changes/<name>/→openspec/changes/archive/YYYY-MM-DD-<name>/.
The actual work is one CLI call: npx openspec archive <name> -y. That command applies the delta into the main spec and moves the folder atomically. Do not move the folder by hand — you'll skip the spec sync.
Expanded workflow commands (custom profile only)
The seven commands below are not available on the default core profile. Enable them by running npx openspec config profile, switching to custom, ticking the workflows you want, then npx openspec update.
/opsx-new [name]
Creates the change scaffold (openspec/changes/<name>/ + .openspec.yaml) with no artifacts. Use this as the starting point when you want to generate and review one artifact at a time rather than the whole plan in one shot — follow it with /opsx-continue.
/opsx-continue [name]
Generates the next artifact in the dependency chain, one at a time, then stops so you can review before proceeding. Reads all completed artifacts for context, shows what is ready vs blocked, and tells you what to run next.
Order: proposal.md → design.md → specs/<capability>/spec.md → tasks.md.
Use this when:
- The change is large enough that you want a human sign-off on the proposal before the design is written.
- You are iterating on the design doc before locking in tasks.
- You interrupted a
/opsx-proposesession mid-way and need to resume from the last completed artifact.
/opsx-ff [name]
Fast-forward — generates all remaining artifacts at once, respecting dependencies, but reading each completed artifact before writing the next. Faster than /opsx-continue because it doesn't pause; slower than /opsx-propose because it surfaces each artifact as it goes.
Use when:
- You used
/opsx-newto start the change but now want to complete planning quickly. - You want to see the artifacts as they are produced without waiting for interactive review.
Equivalent to running /opsx-continue in a tight loop until all artifacts are done.
/opsx-sync [name]
Merges the change's delta specs (openspec/changes/<name>/specs/<cap>/spec.md) into the living spec (openspec/specs/<cap>/spec.md) without archiving the change. The change folder stays active.
Use this for long-running branches where other changes have already archived and updated the main spec. Sync your delta forward before archiving so you're not overwriting recent updates.
Normally you won't need this directly.
/opsx-archiveoffers to sync as part of its flow. Only reach for/opsx-syncexplicitly when you need an up-to-date base spec mid-flight (e.g. before rebasing a long branch).
/opsx-bulk-archive
Archives multiple completed changes in one pass. Detects spec conflicts across the batch and resolves them by inspecting the current codebase state rather than guessing from artifact order.
Use when:
- You have been running parallel feature branches (
sales-dashboard+leads-dashboardchanges) and both landed around the same time. - You let several small changes pile up and want to clean up the
openspec/changes/folder in one go.
Still run
npx openspec validate --all --strictafter bulk-archiving to verify the merged main specs are consistent.
/opsx-onboard
Interactive tutorial that walks a new developer through the complete OpenSpec cycle on the real codebase — finding an improvement opportunity, creating a change, implementing it, and archiving it — with narration at each step.
Intended for:
- New teammates being introduced to the spec-driven workflow in this repo.
- Developers familiar with OpenSpec in general but unfamiliar with how it is configured here (artifact shapes,
config.yamlcontext injection, naming conventions).
Not intended for active feature work.
Choosing a path
| Situation | Path |
|---|---|
| Scope is clear, change is self-contained | /opsx-propose → /opsx-apply → /opsx-archive |
| Requirements are fuzzy | /opsx-explore → /opsx-propose → /opsx-apply → /opsx-archive |
| Complex change; want to review each artifact before the next | /opsx-new → /opsx-continue × N → /opsx-apply → /opsx-verify → /opsx-archive |
| Confident on scope, want artifacts visible but fast | /opsx-new → /opsx-ff → /opsx-apply → /opsx-verify → /opsx-archive |
| Long-running branch; base spec has drifted | … → /opsx-sync → rebase → /opsx-apply → /opsx-archive |
| Multiple parallel branches just merged | /opsx-bulk-archive |
| Pre-archive safety check on auth / middleware / multi-cap changes | … → /opsx-verify → /opsx-archive |
| Onboarding a new developer | /opsx-onboard (standalone — not during active feature work) |
The core profile covers the first two rows. Anything below requires switching to custom (npx openspec config profile → npx openspec update).
4. End-to-end walkthrough
This is the change we shipped recently, command by command.
4.0 The whole journey at a glance
4.1 Propose
/opsx-propose remove show underlying data for sales dashboard from visible actions allowlist
What happened:
- Scaffolded
openspec/changes/remove-show-underlying-data-sales-dashboard/with.openspec.yaml. - Built
proposal.md— Why (data owner reported aggregation-hidden rows getting exposed), What (delete one line + spec delta), in-scope / out-of-scope, Capabilities (sales-dashboardmodified), Impact (route, container, rollback). - Built
design.md— kept it short because the change is trivial. Recorded the one real trade-off (remove outright vs. flag) and a comment-hygiene note (don't leave a// Action.ShowUnderlyingData,placeholder). - Built the delta spec at
specs/sales-dashboard/spec.md. Started with aREMOVED Requirementssection by mistake, then realized the doomed item was a Scenario underLiveboard embed configuration, not a top-level Requirement — so it's just aMODIFIED Requirementsblock with the Scenario dropped and the visibleActions list shrunk from 10 → 9 entries. Lesson: a Scenario disappearing inside a Requirement is aMODIFIEDoperation, not aREMOVEDone. - Built
tasks.md— 4 numbered groups (Pre-merge / Code / Verify / Archive), 6 bullets total, all checkboxes. - Validated:
npx openspec validate remove-show-underlying-data-sales-dashboard --strict→ passed.
4.2 Apply
/opsx-apply
What happened:
- Resolved the change name from conversation context (no need to type it).
- Read
tasks.mdand worked through it:- 2.1 — Deleted the
Action.ShowUnderlyingData,line fromsrc/containers/sales-dashboard/SalesDashboard.client.tsx. Did not leave a placeholder comment behind. - 3.1 —
npm run typecheck && npm run lintboth pass; the 21 ESLint warnings are pre-existing. - 4.1 —
npx openspec validate ... --strictpasses.
- 2.1 — Deleted the
- Tasks 1.1 (PR description note), 3.2 (manual
en+nlclick-through), and 4.2 (the archive itself) were left unchecked — they need a human / a real PR / a running app, and that's expected. - Showed status: 3/6 complete, paused with the remaining-tasks list.
4.3 Verify (recommended)
/opsx-verify
For this change it would have flagged: "Tests: empty section under §3 — is there a Vitest or Playwright test that should land?" Answer: no — the verification note Scenario already documents that the iframe DOM isn't observable, so the array literal change is verified by code review + validate --strict. That's the kind of question /opsx-verify is designed to surface so the author can answer it explicitly rather than implicitly.
4.4 Archive
/opsx-archive
What happened:
npx openspec status --change ... --jsonconfirmed all 4 artifactsdone.- Counted 3 unchecked tasks — confirmed proceeding (those tasks are PR/QA hygiene).
- Diffed
specs/sales-dashboard/spec.md(delta) againstopenspec/specs/sales-dashboard/spec.md(main) — confirmed sync. - Ran
npx openspec archive remove-show-underlying-data-sales-dashboard -y. Output:Specs to update:sales-dashboard: updateApplying changes to openspec/specs/sales-dashboard/spec.md:~ 1 modifiedTotals: + 0, ~ 1, - 0, → 0Specs updated successfully.Change '...' archived as '2026-04-27-remove-show-underlying-data-sales-dashboard'. npx openspec validate sales-dashboard --strictconfirmed the synced main spec is clean.
4.5 Commit
The implementation commit (the one-line code edit) is separate from the archive commit. Two commits, in this order:
- Code change with a short opsx-flavored message:
disable show underlying data for sales dashboard using opsx. - Archive + main-spec sync:
archive remove-show-underlying-data-sales-dashboard opsx change.
This split keeps git blame on the source line clean and lets you revert behavior without reverting the openspec history.
5. CLI cheatsheet
The slash commands wrap these. Use them directly when scripting, when something goes wrong, or when you just want to look around.
# Discover
npx openspec list # active changes
npx openspec list --specs # capabilities (living specs)
npx openspec show <change-or-spec> # render a change or spec
# Propose / inspect
npx openspec new change "<name>" # scaffold (kebab-case)
npx openspec status --change "<name>" --json # artifact graph + applyRequires
npx openspec instructions <artifact> --change "<name>" --json
# artifact: proposal | design | specs | tasks
npx openspec instructions apply --change "<name>" --json
# implementation context + task list
# Validate
npx openspec validate <name> --strict # validate a change
npx openspec validate <capability> --strict # validate a living spec
npx openspec validate --all --strict # everything
# Archive
npx openspec archive <name> # interactive (prompts)
npx openspec archive <name> -y # skip prompts
npx openspec archive <name> --skip-specs # tooling/doc-only changes
npx openspec archive <name> --no-validate # dry-archive against a throwaway branch
# Tooling
npx openspec update # regenerate slash commands for configured tools
# (run after every @fission-ai/openspec bump
# or any `openspec config profile` change)
npx openspec update --force # re-emit even when up to date
npx openspec config profile # interactive: pick profile + workflows + delivery
# (use this to enable `verify`, `new`, `continue`,
# `ff`, `sync`, `bulk-archive`, `onboard`)
npx openspec config list # show current profile + workflow set + delivery
6. Gotchas we hit (or nearly hit)
MODIFIEDvsREMOVEDRequirements.REMOVEDis for whole### Requirement:blocks. Dropping a#### Scenario:from inside a Requirement is part of aMODIFIEDblock — paste the full Requirement, then edit. We almost wrote a stray## REMOVED Requirementssection for a Scenario; caught it beforevalidate.- Don't paste
<context>or<rules>into the artifact file. Theopenspec instructions ... --jsonoutput gives you those as JSON fields. They are constraints for the writer, not content for the file. Templates also include<!-- HTML comments -->as authoring hints; strip them. - Don't sync specs by hand.
npx openspec archivedoes the sync atomically. Editingopenspec/specs/<capability>/spec.mddirectly and then archiving will produce confusing diffs and may leave the spec inconsistent. - Don't add a Vitest test for
visibleActionsunless you actually have one. The tasks.md template is a floor for normal changes; for a one-line allowlist edit there's no test layer to update. List only actionable items, neverN/A — not modifiedplaceholder bullets. NB:AGENTS.mdlists Vitest alongside Playwright as part of the verification substrate, but the vast majority of behavior coverage in this repo lives in Playwright. Don't manufacture a unit test just to satisfy the template. - PR-hygiene tasks stay unchecked. "Note in PR description", "manual click-through", and "after merge, archive" are real tasks — they just don't belong to the apply-loop. The
--yesarchive flag is the right way to acknowledge them at archive time. -yskips warnings, not validation.npx openspec archive ... -ywill still fail if the strict validation fails. If you want to bypass validation (almost never), use--no-validateseparately.validate --strictdoes NOT guaranteearchivewill succeed. Per the upstream parallel-merge plan, there are reported cases where validation passes but archive fails downstream (parser miscount under fenced code blocks; deltas that are valid in isolation but reference requirements no longer present after a sibling archive). For high-risk changes, dry-archive first in a throwaway worktree:npx openspec archive <name> --no-validate, inspect the merged spec, then run the real archive.- Parallel changes can silently corrupt main specs. OpenSpec's archive replaces entire requirement blocks without conflict detection. One active change per
### Requirement:block at a time. When parallel work is unavoidable, the second author MUST rebase their delta against the first change's merged main spec before archiving. - No
###headers inside fenced code blocks. The OpenSpec parser has been reported to mis-count requirement headers when they appear inside fenced blocks. Quote with inline code (`### Requirement: X`) or leading whitespace instead. - The
coreprofile silently ignores theworkflowsarray. Your global~/.config/openspec/config.jsonmay list any subset of the 11 available workflows underworkflows, but ifprofile: "core"is set, that list is ignored — thecoreprofile is hard-coded to exactlypropose,explore,apply,archive(seenode_modules/@fission-ai/openspec/dist/core/profiles.js). To enableverify,new,continue,ff,sync,bulk-archive, oronboard, switch toprofile: "custom"(cleanest vianpx openspec config profile), thennpx openspec update. There is also no--toolsflag onupdate— that option lives oninit. Tool selection (Cursor / Claude Code / both) is decided bydeliveryin the same global config. - Two commits, not one. Keep the source-code change and the archive (rename + spec sync) in separate commits, in that order. Revertability matters more than commit count.
7. When things go wrong
| Symptom | Likely cause | Fix |
|---|---|---|
validate ... --strict fails on MODIFIED Requirements | Pasted only the changed Scenario, not the whole Requirement | Copy the full ### Requirement: block from the main spec, paste under ## MODIFIED Requirements, edit in place |
archive says "Specs to update: <capability>: update" but main spec doesn't change | Delta is identical to main (already synced) | Safe to ignore; you may have hand-edited earlier |
archive reports "Target archive directory already exists" | Re-archiving on the same date | Rename / delete the old archive folder, or wait until tomorrow |
archive succeeds but a Scenario silently disappeared from the main spec | A sibling change archived first and replaced the requirement block | Re-author the delta against the current main spec; consider --no-validate dry-archive next time |
| Slash command refuses to start | Missing artifact dependency | Run openspec status --change ... --json and look at missingDeps |
| Slash command behaves differently from CLI docs | Local slash files lag the CLI version | npx openspec update (or --force) — there is no --tools flag on update |
npx openspec update ran cleanly but /opsx-verify (or another non-core workflow) still isn't installed | Global config has profile: "core", which silently ignores the workflows array | npx openspec config profile → choose custom → tick the workflow → npx openspec update |
| Apply loop guesses wrong | Task description was vague | Pause, edit the task in tasks.md with the right specifics, re-run /opsx-apply |
| Code change shipped but spec wasn't updated | Skipped archive, or used git mv instead | Re-run npx openspec archive <name> from the original (un-renamed) folder |
8. The one-page TL;DR
That's the whole loop.