Jesse A. Green / notebook

OpenSpec Admin & Architecture Guide

Generated June 2026 from Fission-AI/OpenSpec at 1b06fdd

AI-generated documentation. The library is checked by sampling (three repositories so far); this guide was not individually reviewed. Verify against the project before relying on it.

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

  1. Executive Summary
  2. System Architecture
  3. Requirements & Runtime
  4. Repository Structure
  5. Subsystem Deep Dive
  6. The Artifact Graph & Schema Engine
  7. Data Model & On-Disk State
  8. Configuration Reference
  9. Security Analysis
  10. Privacy & Telemetry
  11. Extending OpenSpec
  12. Build, Test & Release
  13. Operations & Troubleshooting
  14. Code Quality & Conventions
  15. Potential Improvements
  16. 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:

  1. Scaffolds change directories and artifact files from schema-defined templates.
  2. Validates specs and change deltas against a strict structural grammar (Zod-backed).
  3. Tracks artifact and task completion by reading the filesystem.
  4. Generates AI-tool integrations — "skills" and /opsx-* slash commands — for 30+ assistants via a pluggable adapter registry.
  5. 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)

  1. bin/openspec.js loads the compiled dist/cli/index.js and calls runCli(process.argv).
  2. commander parses argv. The root program's preAction hook fires first: it applies --no-color (setting NO_COLOR=1), shows the one-time telemetry notice, and records the command path (e.g. change:show) via trackCommand.
  3. The matched command's .action() executes the domain logic, usually delegating to a class or function in src/core/.
  4. Errors bubble to a per-command try/catch that prints via ora().fail(...) and sets a non-zero exit code.
  5. The postAction hook 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 own preAction hooks.
  • 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.url and createRequire are used to locate the package's own package.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 under openspec/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:

  1. Validate the target path and check write permissions (FileSystemUtils.ensureWritePermissions); throws Insufficient permissions to write to <path> on failure. Detects extend mode if openspec/ already exists.
  2. Legacy cleanup: detectLegacyArtifacts scans 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.
  3. Tool detection: getAvailableTools (src/core/available-tools.ts) scans for each tool's skillsDir (or custom detectionPaths) to pre-select tools.
  4. Migration: in extend mode, migrateIfNeeded upgrades the project to the profile system.
  5. 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.
  6. Directory structure: creates openspec/{specs,changes,changes/archive}.
  7. Generate skills & commands per tool (see 5.2).
  8. 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 a ToolCommandAdapter.
  • generateCommand/generateCommands (generator.ts) render CommandContent[] through an adapter into GeneratedCommand[] 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:foo vs 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):

  1. Verifies openspec/changes/ exists and the named change directory exists (or prompts a select picker that shows per-change task progress).
  2. Unless --no-validate, validates the proposal (informational, non-blocking) and any delta spec files (blocking on errors).
  3. Reports task progress; warns on incomplete tasks (confirm unless --yes).
  4. 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."
  5. Moves the change directory to changes/archive/<YYYY-MM-DD>-<name>/ via moveDirectory, which falls back to copy-then-remove on EPERM/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 via cross-spawn), open-surface.ts (inject guidance markers into AGENTS.md), skills.ts.
  • context-store/ — foundation.ts, registry.ts, binding.ts, operations.ts, errors.ts (typed ContextStoreError). Stores can be Git-initialized (--init-git).
  • collections/initiatives/ — initiative collection, schema, operations, and resolution; planning-home.ts resolves 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.ts and workspace/foundation.ts). It is also the area most actively in flux — see the workspace-reimplementation-roadmap change and the context-store-and-initiatives initiative in the dogfooded openspec/.


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-artifact done/ready/blocked (blocked artifacts list their missing deps).
  • instructions <artifact> → generateInstructions emits 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's apply block (requires, tracks, instruction), parses tasks.md checkboxes, and reports blocked / ready / all_done with 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 in src/core/change-metadata/): records the change's schema, optional initiative link ({store, id}), affected_areas, and goal. Read/written via readChangeMetadata/writeChangeMetadata in src/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. validateChangeExists re-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 (workspace openers.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_... in src/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 (or DO_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 a surface: 'cli' tag — keyed to a random UUID generated on first run and stored in telemetry config. $ip is explicitly set to null.
  • What is never sent: command arguments, file paths, file contents, repo names, or your IP.
  • Transport: posthog-node to https://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 safeTelemetryFetch wrapper swallows all network errors and returns a synthetic 204, 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, or CI=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

  1. Add an entry to AI_TOOLS in src/core/config.ts (name, value ID, skillsDir, optional detectionPaths).
  2. Create src/core/command-generation/adapters/<tool>.ts exporting a ToolCommandAdapter that encodes the tool's file format and slash-command convention.
  3. Register it in adapters/index.ts and the CommandAdapterRegistry.
  4. 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 skillsDir but 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/ and bin/ alongside dist/, any change to schema files or the CLI shim must be reflected in the files allow-list in package.json or 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_COLOR in scripts and CI to keep logs clean.

14. Code Quality & Conventions

  • TypeScript strictness with ESM and .js import specifiers on source files; import.meta/createRequire for 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.yaml shape (artifacts, requires, apply.tracks) is powerful but currently best learned by reading spec-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