This guide is for engineers, architects, and tooling maintainers who need to understand OpenSpec deeply — how it is built, how its subsystems fit together, how it stores and validates data, what its security and privacy posture is, and how to extend it (new schemas, new AI-tool adapters, new shells). It is the companion to the User Guide; end-user task instructions live there.
OpenSpec is a Node.js/TypeScript command-line application distributed on npm as @fission-ai/openspec (current version 1.4.1, MIT licensed). It is intentionally infrastructure-light: no server, no database, no network dependency for its core workflow. State is plain Markdown and small JSON/YAML files on disk. That design choice — "just files" — is the central architectural principle and the reason the tool is portable and reviewable.
Table of Contents
- Executive Summary
- System Architecture
- Requirements & Runtime
- Repository Structure
- Subsystem Deep Dive
- The Artifact Graph & Schema Engine
- Data Model & On-Disk State
- Configuration Reference
- Security Analysis
- Privacy & Telemetry
- Extending OpenSpec
- Build, Test & Release
- Operations & Troubleshooting
- Code Quality & Conventions
- Potential Improvements
- Quick Reference
1. Executive Summary
OpenSpec implements spec-driven development as a thin CLI over a Markdown filesystem. Its job is to give AI coding agents (and humans) a deterministic, reviewable workflow:
proposal → specs → design → tasks → implement → archive (specs sync)
The CLI does five things well:
- Scaffolds change directories and artifact files from schema-defined templates.
- Validates specs and change deltas against a strict structural grammar (Zod-backed).
- Tracks artifact and task completion by reading the filesystem.
- Generates AI-tool integrations — "skills" and
/opsx-*slash commands — for 30+ assistants via a pluggable adapter registry. - Archives completed changes by applying their spec deltas back into the canonical specs.
Architecturally it is a layered command application:
- A commander-based CLI layer (
src/cli/index.ts) wires global hooks (telemetry, color) and registers commands. - A commands layer (
src/commands/) holds command implementations and the artifact-workflow commands (src/commands/workflow/). - A core layer (
src/core/) holds the domain logic: init/update, archive, validation, the artifact-graph/schema engine, command generation, completions, workspaces, context stores, and global config. - Cross-cutting utils (
src/utils/) and telemetry (src/telemetry/).
Key non-functional properties: cross-platform (explicit Windows handling for moves and config paths), offline-first, privacy-respecting (opt-out anonymous telemetry only), and forward-compatible (config schemas use passthrough() to preserve unknown fields).
2. System Architecture
Layered view
┌──────────────────────────────────────────────────────────────┐
│ Entry: bin/openspec.js → dist/cli/index.js (runCli) │
├──────────────────────────────────────────────────────────────┤
│ CLI LAYER src/cli/index.ts │
│ • commander Program: name/version/global --no-color │
│ • preAction hook → NO_COLOR, telemetry notice, trackCmd │
│ • postAction hook → telemetry shutdown/flush │
│ • registers all commands & subcommands │
├───────────────┬───────────────────────────┬──────────────────┤
│ COMMANDS │ WORKFLOW COMMANDS │ REGISTER-style │
│ src/commands/ │ src/commands/workflow/ │ commands │
│ change, spec │ status, instructions, │ config, schema, │
│ show, │ templates, schemas, │ workspace, │
│ validate, │ new-change, set-change, │ context-store, │
│ completion, │ apply-instructions │ initiative, spec │
│ feedback │ │ │
├───────────────┴───────────────────────────┴──────────────────┤
│ CORE LAYER src/core/ │
│ init • update • archive • list • view • validation/ │
│ artifact-graph/ (schema engine) • command-generation/ │
│ completions/ • workspace/ • context-store/ • collections/ │
│ global-config • config • config-schema • planning-home │
│ specs-apply • change-metadata/ • templates/ • shared/ │
├──────────────────────────────────────────────────────────────┤
│ UTILS src/utils/ • TELEMETRY src/telemetry/ (PostHog) │
├──────────────────────────────────────────────────────────────┤
│ FILESYSTEM (the only persistence) │
│ project: openspec/{specs,changes,changes/archive,config.yaml}│
│ global: $XDG_CONFIG_HOME/openspec/config.json │
│ tools: .claude/ .cursor/ … (generated skills & commands) │
└──────────────────────────────────────────────────────────────┘
Request lifecycle (every invocation)
bin/openspec.jsloads the compileddist/cli/index.jsand callsrunCli(process.argv).- commander parses argv. The root program's
preActionhook fires first: it applies--no-color(settingNO_COLOR=1), shows the one-time telemetry notice, and records the command path (e.g.change:show) viatrackCommand. - The matched command's
.action()executes the domain logic, usually delegating to a class or function insrc/core/. - Errors bubble to a per-command
try/catchthat prints viaora().fail(...)and sets a non-zero exit code. - The
postActionhook flushes and shuts down the PostHog telemetry client.
Notable design decisions
- Verb-first command surface (
list,show,validate,archive) with deprecated noun-first aliases (change ...,spec ...) kept for backward compatibility and emitting deprecation warnings via their ownpreActionhooks. - Lazy imports of heavy or rarely-used modules (
await import('../core/init.js'),@inquirer/prompts) keep cold-start fast and avoid loading interactive deps in non-interactive paths. - Schema-driven everything: artifact requirements, templates, and the apply phase are data (
schema.yaml), not code, so workflows are customizable without forking the binary.
3. Requirements & Runtime
| Item | Value |
|---|---|
| Runtime | Node.js >= 20.19.0 (enforced via package.json engines) |
| Language | TypeScript ^5.9.3, compiled to ESM ("type": "module") |
| Module system | ESM throughout; imports use .js extensions on TS source |
| Package manager (dev) | pnpm (build/release scripts assume pnpm) |
| Executable | openspec → bin/openspec.js |
| Published files | dist/, bin/, schemas/, scripts/postinstall.js |
| OS support | macOS, Linux, Windows (first-class, with platform-specific code paths) |
Runtime dependencies
| Dependency | Version | Role |
|---|---|---|
commander |
^14.0.0 |
CLI command/option parsing, hooks |
@inquirer/core, @inquirer/prompts |
^10, ^7.8 |
Interactive prompts (confirm, select, custom searchable multi-select) |
chalk |
^5.5.0 |
Terminal coloring (honors NO_COLOR) |
ora |
^8.2.0 |
Spinners and success/fail status lines |
zod |
^4.0.17 |
Schema validation for config, change/spec data models, workspace state |
yaml |
^2.8.2 |
Parse/serialize schema.yaml, config.yaml, metadata |
fast-glob |
^3.3.3 |
Glob resolution for artifact outputs (e.g. specs/**/*.md) |
cross-spawn |
7.0.6 |
Cross-platform process spawning (editors, agents, openers) |
posthog-node |
^5.20.0 |
Anonymous telemetry transport |
Dev tooling: vitest (tests + coverage + UI), eslint + typescript-eslint, @changesets/cli (versioning/release), @types/node.
Note
Because the project is pure ESM,
import.meta.urlandcreateRequireare used to locate the package's ownpackage.json(for the version string) and to load JSON. There is no CommonJS entry point.
4. Repository Structure
OpenSpec/
├── bin/openspec.js # Published CLI shim → dist/cli/index.js
├── build.js # Build script (invoked by `pnpm build`)
├── package.json # name, bin, engines, scripts, deps
├── src/
│ ├── index.ts # Public exports (cli + core)
│ ├── cli/index.ts # commander program, hooks, command wiring
│ ├── commands/ # Command implementations
│ │ ├── change.ts spec.ts show.ts validate.ts completion.ts feedback.ts
│ │ ├── config.ts schema.ts workspace.ts context-store.ts initiative.ts
│ │ └── workflow/ # Artifact-driven workflow commands
│ │ ├── status.ts instructions.ts templates.ts schemas.ts
│ │ ├── new-change.ts set-change.ts initiative-link.ts shared.ts
│ ├── core/
│ │ ├── init.ts update.ts archive.ts list.ts view.ts
│ │ ├── config.ts config-schema.ts config-prompts.ts global-config.ts
│ │ ├── available-tools.ts legacy-cleanup.ts migration.ts profiles.ts
│ │ ├── planning-home.ts change-status-policy.ts specs-apply.ts
│ │ ├── validation/ # Spec/change validators
│ │ ├── artifact-graph/ # Schema engine (see §6)
│ │ ├── command-generation/ # Tool adapters + generator (see §5)
│ │ │ └── adapters/ # 27 per-tool adapters
│ │ ├── completions/ # Shell completion generators/installers
│ │ ├── workspace/ # Workspace foundation, registry, openers
│ │ ├── context-store/ # Context store foundation/registry/ops
│ │ ├── collections/ # Collections + initiatives runtime
│ │ ├── change-metadata/ # Per-change metadata schema
│ │ ├── schemas/ # Zod models: spec, change, base
│ │ ├── shared/ # Tool detection + skill generation
│ │ ├── templates/ # Skill templates facade
│ │ └── styles/ # Palette / ANSI styling
│ ├── ui/ # Welcome screen, ascii patterns
│ ├── prompts/ # Custom searchable multi-select prompt
│ ├── telemetry/ # PostHog client + config
│ └── utils/ # change-utils, file-system, item-discovery…
├── schemas/ # Built-in schemas (published)
│ ├── spec-driven/ # Default schema + templates
│ └── workspace-planning/ # Workspace planning schema + templates
├── scripts/ # postinstall.js, pack-version-check, update-flake
├── openspec/ # OpenSpec dogfooding its own specs/changes
└── test/ # vitest suites (mirrors src/ layout)
Tip
The project dogfoods itself: its own
openspec/directory holds the specs and changes that drove OpenSpec's development. The archived changes underopenspec/changes/archive/are an excellent source of historical design rationale.
5. Subsystem Deep Dive
5.1 Init (src/core/init.ts)
InitCommand.execute(targetPath) is the most involved flow:
- Validate the target path and check write permissions (
FileSystemUtils.ensureWritePermissions); throwsInsufficient permissions to write to <path>on failure. Detects extend mode ifopenspec/already exists. - Legacy cleanup:
detectLegacyArtifactsscans for files from older OpenSpec layouts; in interactive mode it prompts ("Upgrade and clean up legacy files?"), in--force/non-interactive mode it cleans automatically. Cleanup only removes OpenSpec-managed markers/commands — it never deletes user content. - Tool detection:
getAvailableTools(src/core/available-tools.ts) scans for each tool'sskillsDir(or customdetectionPaths) to pre-select tools. - Migration: in extend mode,
migrateIfNeededupgrades the project to the profile system. - Tool selection:
--tools all|none|<ids>resolves non-interactively; otherwise a searchable multi-select. Validation rejects unknown IDs and the combination of reserved (all/none) with specific IDs. - Directory structure: creates
openspec/{specs,changes,changes/archive}. - Generate skills & commands per tool (see 5.2).
- Create config (
openspec/config.yaml) and print a success summary.
The catalog of supported tools is the AI_TOOLS array in src/core/config.ts — each entry maps a display name, stable value ID, and skillsDir (e.g. claude → .claude, cursor → .cursor, github-copilot → .github with explicit detectionPaths).
5.2 Command generation (src/core/command-generation/)
A small registry/adapter pattern turns abstract command/skill content into tool-specific files:
CommandAdapterRegistry(registry.ts) maps a tool ID to aToolCommandAdapter.generateCommand/generateCommands(generator.ts) renderCommandContent[]through an adapter intoGeneratedCommand[]ready to write.adapters/contains 27 adapters (claude.ts,cursor.ts,windsurf.ts,codex.ts,gemini.ts,github-copilot.ts,opencode.ts,roocode.ts, …). Each encodes that tool's command file format, directory layout, and slash-command naming (e.g. Claude-style/opsx:foovs hyphen-style/opsx-foo).
The set of installed workflows is profile-driven (src/core/profiles.ts): CORE_WORKFLOWS vs ALL_WORKFLOWS. Workflow IDs map to skill directories via WORKFLOW_TO_SKILL_DIR in init.ts: explore→openspec-explore, new→openspec-new-change, continue→openspec-continue-change, apply→openspec-apply-change, ff→openspec-ff-change, sync→openspec-sync-specs, archive→openspec-archive-change, bulk-archive→openspec-bulk-archive-change, verify→openspec-verify-change, onboard→openspec-onboard, propose→openspec-propose.
Note
Several community issues track per-tool command formatting (e.g. Codex generating a deprecated
/opsx:*form, Cursor/Windsurf body references). The adapter pattern is precisely the seam where these per-tool quirks are fixed.
5.3 Archive (src/core/archive.ts)
ArchiveCommand.execute(changeName, options):
- Verifies
openspec/changes/exists and the named change directory exists (or prompts aselectpicker that shows per-change task progress). - Unless
--no-validate, validates the proposal (informational, non-blocking) and any delta spec files (blocking on errors). - Reports task progress; warns on incomplete tasks (confirm unless
--yes). - Unless
--skip-specs, computes spec updates (findSpecUpdates), then in a prepare-then-write pattern: builds every updated spec in memory and validates it; only if all pass does it write. On any failure it aborts with "No files were changed." - Moves the change directory to
changes/archive/<YYYY-MM-DD>-<name>/viamoveDirectory, which falls back to copy-then-remove onEPERM/EXDEV(Windows-safe).
This transactional discipline (validate-all-before-write) is the key safety property of archiving.
5.4 Spec application (src/core/specs-apply.ts)
Implements the delta grammar that turns a change's future-state spec files into edits on canonical specs. It recognizes ## ADDED|MODIFIED|REMOVED|RENAMED Requirements sections, rebuilds the full spec, and reports counts (+ added, ~ modified, - removed, → renamed). RENAMED uses FROM:/TO: header matching; failures raise precise errors (e.g. <specName> RENAMED failed for header ...).
5.5 Validation (src/core/validation/)
Validator validates change proposals, change delta specs, and full rebuilt specs. The structural rules enforced (surfaced in templates and errors): requirements as ### Requirement:, scenarios as exactly #### Scenario: (four hashes — three is silently skipped, which validation flags), at least one scenario per requirement, and SHALL/MUST normative phrasing. Bulk validation (src/commands/validate.ts) runs items concurrently, bounded by --concurrency / OPENSPEC_CONCURRENCY (default 6).
5.6 Completions (src/core/completions/)
A factory (CompletionFactory) builds per-shell generators and installers for bash, zsh, fish, powershell. COMMAND_REGISTRY (command-registry.ts) is a declarative description of every command, subcommand, flag, and positional type — the single source for generating static completion scripts. CompletionProvider supplies dynamic completions (change IDs, spec IDs, schema names) with a 2-second TTL cache to avoid hammering the filesystem during tab completion. The hidden __complete <type> command is the runtime hook shells call.
5.7 Workspaces, context stores, initiatives (beta)
These three subsystems implement cross-repo coordination and share a Zod-validated, file-backed registry pattern:
workspace/—foundation.ts(naming/validation),registry.ts,state-io.ts,openers.ts(launch Codex/Claude/Copilot/editor viacross-spawn),open-surface.ts(inject guidance markers intoAGENTS.md),skills.ts.context-store/—foundation.ts,registry.ts,binding.ts,operations.ts,errors.ts(typedContextStoreError). Stores can be Git-initialized (--init-git).collections/initiatives/— initiative collection, schema, operations, and resolution;planning-home.tsresolves whether the current directory is a repo-local or workspace planning home.
change-status-policy.ts adapts behavior between repo-local and workspace-planning modes (allowed edit roots, affected-area requirements, agent constraints).
Warning
The workspace subsystem carries the bulk of the project's defensive validation (path-traversal guards in
collections/runtime.tsandworkspace/foundation.ts). It is also the area most actively in flux — see theworkspace-reimplementation-roadmapchange and thecontext-store-and-initiativesinitiative in the dogfoodedopenspec/.
6. The Artifact Graph & Schema Engine
The schema engine (src/core/artifact-graph/) is the conceptual core of OpenSpec. A schema is a schema.yaml describing artifacts and their dependencies; the engine loads it, builds a dependency graph, detects completion state from the filesystem, and generates per-artifact instructions.
Schema shape (the default spec-driven)
name: spec-driven
version: 1
description: Default OpenSpec workflow - proposal → specs → design → tasks
artifacts:
- id: proposal
generates: proposal.md
template: proposal.md
requires: []
- id: specs
generates: "specs/**/*.md" # glob — resolved via fast-glob
template: spec.md
requires: [proposal]
- id: design
generates: design.md
template: design.md
requires: [proposal]
- id: tasks
generates: tasks.md
template: tasks.md
requires: [specs, design]
apply:
requires: [tasks]
tracks: tasks.md
instruction: |
Read context files, work through pending tasks, mark complete as you go.
Engine components
| Module | Responsibility |
|---|---|
types.ts |
Zod schemas: ArtifactSchema, SchemaYamlSchema, Artifact, SchemaYaml |
schema.ts |
loadSchema/parseSchema; raises SchemaValidationError on invalid schema, duplicate artifact IDs, or cyclic dependencies |
graph.ts |
ArtifactGraph — build from schema, query artifacts and dependencies |
state.ts |
detectCompleted — which artifacts exist on disk |
outputs.ts |
artifactOutputExists, resolveArtifactOutputs, isGlobPattern (glob vs literal) |
resolver.ts |
resolveSchema, listSchemas, getSchemaDir, project/user/package dir resolution; SchemaLoadError |
instruction-loader.ts |
loadChangeContext, generateInstructions, formatChangeStatus — produce the enriched, XML-tagged instructions the agent consumes |
Schema resolution precedence
project-local schema (openspec/… in repo) ← highest priority
▲
user override schema ($XDG_DATA_HOME/openspec/…)
▲
package schema (schemas/ shipped in the npm package) ← default
openspec schemas labels the winning source ((project), (user override), or none for package). A change records its schema in metadata, so downstream commands auto-detect it.
How commands consume the graph
status→loadChangeContext+formatChangeStatus→ per-artifactdone/ready/blocked(blocked artifacts list their missing deps).instructions <artifact>→generateInstructionsemits a structured block (<task>,<project_context>,<rules>,<dependencies>,<output>,<template>,<unlocks>) the agent fills in. Blocked artifacts emit a<warning>with missing dependencies.instructions apply→ reads the schema'sapplyblock (requires,tracks,instruction), parsestasks.mdcheckboxes, and reportsblocked/ready/all_donewith progress counts.templates→ resolves each artifact's template path and its source tier.
7. Data Model & On-Disk State
OpenSpec persists only to the filesystem. There are three storage scopes.
Project scope (committed to the repo)
openspec/
├── config.yaml # project config (default schema, context, rules)
├── specs/<capability>/spec.md # canonical specs (what IS)
└── changes/
├── <change-name>/
│ ├── proposal.md
│ ├── design.md # optional
│ ├── tasks.md # tracked checkbox list
│ ├── README.md # optional (from --description)
│ ├── .openspec.yaml # per-change metadata (schema, initiative, areas)
│ └── specs/<capability>/spec.md# future-state delta spec
└── archive/<YYYY-MM-DD>-<name>/ # archived changes
- Change metadata (
.openspec.yaml, modeled insrc/core/change-metadata/): records the change'sschema, optionalinitiativelink ({store, id}),affected_areas, andgoal. Read/written viareadChangeMetadata/writeChangeMetadatainsrc/utils/change-metadata.ts. - Tasks are parsed from
- [ ]/- [x]checkbox lines; only properly formatted checkboxes are tracked. - Specs follow the requirement/scenario grammar enforced by validation.
Global scope (user-level)
global-config.ts resolves a config.json under the XDG config dir:
| Platform | Config dir |
|---|---|
Any, if XDG_CONFIG_HOME set |
$XDG_CONFIG_HOME/openspec/ |
| macOS/Linux | ~/.config/openspec/ |
| Windows | %APPDATA%\openspec\ (fallback ~/AppData/Roaming/openspec/) |
A separate data dir holds user schema overrides (XDG_DATA_HOME → ~/.local/share/openspec/ → %LOCALAPPDATA%\openspec\). Config loading merges with defaults (deep-merging featureFlags) and tolerates missing/invalid files by falling back to defaults (logging only on JSON syntax errors).
Tool scope (generated)
Per selected AI tool, OpenSpec writes skills and slash commands into that tool's directory (.claude/, .cursor/, .github/, etc.). These are OpenSpec-managed and regenerated by update.
8. Configuration Reference
Global config (config.json) — schema in config-schema.ts
The Zod GlobalConfigSchema uses .passthrough() to preserve unknown keys for forward compatibility.
| Key | Type | Default | Meaning |
|---|---|---|---|
profile |
"core" | "custom" |
core |
Which workflow set to install |
delivery |
"both" | "skills" | "commands" |
both |
Install skills, slash commands, or both |
featureFlags |
Record<string, boolean> |
{} |
Boolean feature toggles |
workflows |
string[] |
(unset) | Explicit workflow list (advanced) |
config set validates key paths (validateConfigKeyPath): unknown top-level keys are rejected unless --allow-unknown; featureFlags.<flag> is the only nested form allowed (booleans, no deeper nesting). Values auto-coerce (coerceValue): "true"/"false" → boolean, numeric strings → number, else string; --string forces string storage.
Project config (openspec/config.yaml)
Serialized by config-prompts.ts with helpful comments. Holds the project's default schema (required) plus optional context (project background shown to the AI) and per-artifact rules.
Environment variables
| Variable | Read at | Effect |
|---|---|---|
OPENSPEC_TELEMETRY |
src/telemetry/index.ts |
0 disables telemetry |
DO_NOT_TRACK |
src/telemetry/index.ts |
1 disables telemetry (standard) |
CI |
src/telemetry/index.ts |
true auto-disables telemetry |
NO_COLOR |
src/cli/index.ts, shared |
Disable ANSI color (also --no-color) |
OPENSPEC_CONCURRENCY |
src/commands/validate.ts |
Default bulk-validation parallelism (else 6) |
XDG_CONFIG_HOME |
global-config.ts |
Override config dir |
XDG_DATA_HOME |
global-config.ts |
Override data dir (schema overrides) |
APPDATA / LOCALAPPDATA |
global-config.ts |
Windows config/data dir resolution |
EDITOR / VISUAL |
src/commands/config.ts |
Editor for config edit |
CODEX_HOME |
adapters/codex.ts |
Codex adapter target dir |
9. Security Analysis
OpenSpec's attack surface is small (a local CLI over local files, no server, no auth), but it does spawn processes, write files into project and tool directories, validate untrusted Markdown, and (optionally) make network calls for telemetry. Assessment by concern:
Filesystem & path traversal
- Change-name validation (
validateChangeName,src/utils/change-utils.ts) is applied before any path is built from a change name, explicitly to prevent path traversal.validateChangeExistsre-validates the name format before touching the filesystem. - The collections/workspace runtime (
collections/runtime.ts,workspace/foundation.ts) enforces a strict path policy: no NUL bytes, no empty/./..segments, no path separators in IDs, forward-slashes only, no Windows drive paths, and "path escapes mount" rejection. This is the most defensively coded area of the project. - Write permissions are checked before init writes anything (
Insufficient permissions to write to <path>).
Process execution
- Editors and agent openers are launched via
cross-spawn(workspaceopeners.ts,config edit). Inputs are tool IDs/paths from local config rather than arbitrary remote strings, limiting injection risk, but operators integrating new openers should keep argument arrays (not shell strings).
Untrusted input parsing
- All structured inputs (
schema.yaml,config.json, change/spec metadata, workspace state) are parsed through Zod schemas with explicit error messages, so malformed input fails loudly rather than corrupting state. - Spec/change Markdown is parsed by the validator with a strict grammar; malformed deltas are rejected at validate/archive time.
Transactional safety
- Archive uses validate-all-then-write and copy-then-remove fallbacks, minimizing the chance of half-applied state even on Windows
EPERM.
Secrets
- The embedded PostHog key (
phc_...insrc/telemetry/index.ts) is a public, write-only ingest key (documented in-code as safe to embed; it can send events, not read data). There are no other credentials in the codebase.
Caution
The generated skills and
/opsx-*commands are instructions executed by AI agents with whatever permissions you grant those agents. The security boundary for actions taken is your AI tool's permission model, not OpenSpec. Review what your agent is allowed to do (file writes, shell execution) independently.
Tip
For locked-down/air-gapped environments, set
OPENSPEC_TELEMETRY=0(orDO_NOT_TRACK=1) globally. With telemetry off, OpenSpec's core workflow makes no outbound network connections.
10. Privacy & Telemetry
Telemetry lives entirely in src/telemetry/ and is privacy-first by design:
- What is sent: only the command name (e.g.
change:show), the OpenSpec version, and asurface: 'cli'tag — keyed to a random UUID generated on first run and stored in telemetry config.$ipis explicitly set tonull. - What is never sent: command arguments, file paths, file contents, repo names, or your IP.
- Transport:
posthog-nodetohttps://edge.openspec.dev(a reverse proxy on the project's own domain to avoid ad-blocker breakage), with hardened settings —flushAt: 1,flushInterval: 0,fetchRetryCount: 0,requestTimeout: 1000ms, remote config/surveys disabled. - Failure isolation: a
safeTelemetryFetchwrapper swallows all network errors and returns a synthetic204, so telemetry can never surface noise or break the CLI.shutdown()flushes on exit and never throws. - Opt-out:
OPENSPEC_TELEMETRY=0,DO_NOT_TRACK=1, orCI=true. A one-time notice is printed on first run.
11. Extending OpenSpec
Add or customize a workflow schema
openspec schema fork spec-driven my-flow # copy into project to edit
openspec schema init lite --artifacts proposal,tasks --default
openspec schema validate my-flow --verbose # check structure & templates
Edit the resulting schema.yaml (artifacts, requires, generates, template, apply) and the template files under its templates/ directory. The engine validates for duplicate IDs and dependency cycles on load. Project-local schemas override user and package schemas.
Add a new AI-tool adapter
- Add an entry to
AI_TOOLSinsrc/core/config.ts(name,valueID,skillsDir, optionaldetectionPaths). - Create
src/core/command-generation/adapters/<tool>.tsexporting aToolCommandAdapterthat encodes the tool's file format and slash-command convention. - Register it in
adapters/index.tsand theCommandAdapterRegistry. - Add tests under
test/core/command-generation/. Existing adapters (claude, cursor, codex…) are the templates to copy.
Note
A tool can be skills-only (no command adapter) — Kimi CLI is the reference example. In that case give it a
skillsDirbut no adapter.
Add a new shell for completions
Implement a generator (CompletionGenerator) and installer (CompletionInstaller) under src/core/completions/{generators,installers}/, then wire both into CompletionFactory and add the shell to SUPPORTED_SHELLS. The declarative COMMAND_REGISTRY already supplies the command metadata.
12. Build, Test & Release
Scripts (package.json)
| Script | Command | Purpose |
|---|---|---|
build |
node build.js |
Compile TS → dist/ (ESM) |
dev |
tsc --watch |
Type-check/compile on save |
dev:cli |
pnpm build && node bin/openspec.js |
Build then run the CLI |
lint |
eslint src/ |
Lint source |
test |
vitest run |
Run the full test suite |
test:watch / test:ui / test:coverage |
vitest variants | Iterative testing / UI / coverage |
prepare / prepublishOnly |
pnpm run build |
Ensure a fresh build before pack/publish |
postinstall |
node scripts/postinstall.js |
Runs after install (setup hints) |
check:pack-version |
node scripts/pack-version-check.mjs |
Verify packaged version consistency |
release / release:ci |
changesets publish | Version + publish (gated by pack-version check) |
Testing
Tests use vitest and mirror the src/ layout under test/ (test/core/, test/commands/, test/commands/workflow/, test/core/completions/, test/telemetry/, test/cli-e2e/). E2E tests build the CLI and invoke dist/cli/index.js via test/helpers/run-cli.ts (which errors CLI entry point missing after build. Expected dist/cli/index.js if you forgot to build). The telemetry suite mocks posthog-node and asserts opt-out behavior and the silent-failure fetch wrapper.
Versioning & release
Versioning is managed by Changesets (@changesets/cli with @changesets/changelog-github). The release flow runs check:pack-version first, then changeset publish. Tests, build, and publish all assume pnpm.
Important
Because the package ships
schemas/andbin/alongsidedist/, any change to schema files or the CLI shim must be reflected in thefilesallow-list inpackage.jsonor it won't be published.
13. Operations & Troubleshooting
Health & sanity checks
openspec --version # confirm installed version
openspec schemas # confirm schema resolution
openspec schema validate spec-driven --verbose
openspec validate --all --strict # validate all project artifacts
openspec workspace doctor # workspace resolvability (beta)
openspec context-store doctor # context-store registration (beta)
Common operational issues
| Symptom | Likely cause | Resolution |
|---|---|---|
No OpenSpec changes directory found |
Not in a project root / no openspec/ |
cd to root or openspec init |
Insufficient permissions to write to <path> |
Init target not writable | Fix perms; run from a writable location |
| Archive aborts: "No files were changed" | Delta spec validation failed | validate <change> --strict, fix Markdown, retry |
update insists on workspace update |
Run inside a workspace | Use openspec workspace update |
update crashes with foreign workspace.yaml |
Filename collision (e.g. Dagster) — issue #1164 | Upgrade; report via openspec feedback |
| Scenario ignored during validation | Scenario not using #### |
Use exactly four hashes |
| Slow/hanging bulk validation | Concurrency mis-tuned for machine | Set --concurrency / OPENSPEC_CONCURRENCY |
| Generated slash command wrong format | Per-tool adapter quirk (issues #1108/#1129) | Upgrade; the adapter is the fix point |
Cross-platform notes
- Windows: directory moves fall back to copy+remove on
EPERM/EXDEV; config/data dirs use%APPDATA%/%LOCALAPPDATA%. Shell detection covers PowerShell (PSModulePath,COMSPEC,WT_SESSION). - Color: honor
NO_COLORin scripts and CI to keep logs clean.
14. Code Quality & Conventions
- TypeScript strictness with ESM and
.jsimport specifiers on source files;import.meta/createRequirefor package introspection. - Validation-first: Zod is the gatekeeper for every structured input; errors carry actionable messages.
- Registry/adapter patterns for the two most extensible surfaces (AI tools, shells), keeping per-target quirks isolated and testable.
- Lazy imports for interactive deps and heavyweight command modules.
- Defensive filesystem code: path-policy enforcement, write-permission checks, transactional archive, Windows fallbacks.
- Privacy/robustness in telemetry: silent failure, bounded timeouts, explicit IP suppression.
- Deprecation discipline: noun-first commands remain but warn; deprecated flags (
--requirements-only) alias their replacements. - Self-hosting: the project manages its own development with OpenSpec, so specs/changes double as living architecture docs and regression context.
The main complexity hotspots are init.ts (multi-phase setup with many branches) and the workspace/context-store/initiative subsystems (large, beta, rapidly evolving). These are the areas to read tests alongside source.
15. Potential Improvements
Observations from the source, offered as forward-looking suggestions (not defects):
- Stabilize the workspace/context-store/initiative beta. It is the largest, most in-flux area, with deferred lifecycle commands (apply/verify/archive) and an explicit reimplementation roadmap. Locking the data model and promoting it out of beta would reduce the per-tool and per-mode special-casing in
change-status-policy.ts. - Consolidate per-tool slash-command formatting. Multiple community issues (#1108, #1129, #1072, #1007) stem from per-adapter naming/format drift. A shared conformance test matrix across all adapters would catch these before release.
- Surface
--areas/workspace constraints earlier. Several workflow commands validate workspace-only options (--areas) only after resolving the planning home; earlier guards with clearer messages would improve agent ergonomics. - Numbered change names (issue #1169) and custom output directories (#1147) are requested workflow flexibility that the schema engine could accommodate via additional
generates/naming policy. - Document the schema authoring contract publicly. The
schema.yamlshape (artifacts,requires,apply.tracks) is powerful but currently best learned by readingspec-driven/schema.yaml; a reference page would lower the barrier to custom workflows.
16. Quick Reference
Architecture at a glance
| Layer | Location | Responsibility |
|---|---|---|
| CLI | src/cli/index.ts |
commander program, global hooks, command wiring |
| Commands | src/commands/ (+workflow/) |
Command implementations |
| Core | src/core/ |
Domain logic, schema engine, generators |
| Utils/Telemetry | src/utils/, src/telemetry/ |
Cross-cutting helpers, analytics |
| Persistence | filesystem | openspec/, global config.json, tool dirs |
Key files
| File | Why it matters |
|---|---|
src/cli/index.ts |
The command map and lifecycle hooks |
src/core/init.ts |
Project setup + tool integration |
src/core/archive.ts |
Transactional change archival + spec sync |
src/core/artifact-graph/ |
Schema engine (the conceptual core) |
src/core/command-generation/adapters/ |
27 AI-tool adapters |
src/core/config.ts (AI_TOOLS) |
Supported-tool catalog |
schemas/spec-driven/schema.yaml |
Default workflow definition |
src/telemetry/index.ts |
Privacy model + opt-out logic |
Default schema artifacts
proposal → specs → design → tasks → (apply: tracks tasks.md)
Storage locations
| Scope | Path |
|---|---|
| Project | openspec/{config.yaml,specs/,changes/,changes/archive/} |
| Global config | $XDG_CONFIG_HOME/openspec/config.json (Win: %APPDATA%) |
| User schema overrides | $XDG_DATA_HOME/openspec/ (Win: %LOCALAPPDATA%) |
| Tool integrations | .claude/, .cursor/, .github/, … |
Opt out of telemetry
export OPENSPEC_TELEMETRY=0 # or DO_NOT_TRACK=1 ; auto-off when CI=true