Skip to content

Command reference ​

Every command accepts the same global flags. Command-specific flags are noted in the table.

Global flags ​

flageffect
--jsonmachine-readable output
--no-colordisable ANSI color
--cwd <path>run as if invoked from <path>; a <path> that is not an existing directory fails with cospec: directory not found: <path> (exit 1) before anything is spawned, on every command that resolves an operating root and on store, config and workset — see Stores
--store <id>operate against a registered OpenSpec store instead of the local repo; every command also resolves the enclosing root from a subdirectory, a store: config pointer, or the global defaultStore even with no --store at all — see Stores
-h, --helpshow help for the command
-V, --versionprint the installed cospec version and exit — in any position before --, ahead of everything else

--cwd and --store need a value: given none, they're refused with cospec <command>: option '--store <id>' argument missing (or '--cwd <path>'), exit 1. An empty --cwd= is refused with … argument must not be empty, and so is an empty --store= on store, workset and config. On every command that selects its root through --store, an empty --store= is refused as OpenSpec refuses it — cospec: Store id must not be empty and its Fix: line, or the invalid_store_id document under --json (see Stores). Either way the exit is 1 and the command never falls back to the local repo.

--store applies only where a command reads its root through it. init, update, completion and feedback never do — upstream refuses --store on the ones it shares too — so they refuse it with cospec <command>: unknown option '--store', exit 1, before any work, in either form and before the command name alike (cospec --store <id> init); to scaffold a store's root, run cospec init <store-path>. Their --help lists no --store. config refuses it as machine-global; on the forwarded store and workset commands, which take no root, it has no effect.

Global flags are recognised before the command name and anywhere after it, up to a -- terminator — except right after an option that takes a value, where the next token is that option's value whatever it looks like, as in OpenSpec: cospec status --change --help looks up a change named --help, and cospec templates --schema --json asks OpenSpec for a schema named --json. --no-color is the exception, because OpenSpec takes it out before the command reads its options: cospec status --change --no-color refuses the missing --change value. After -- every token is an operand of the command, as in OpenSpec: cospec list -- --json is refused as too many arguments, not run as list --json. A -- before the command name works the same way: cospec -- list runs list, cospec -- list --help is refused as too many arguments, and the token after the command is still its subcommand (cospec -- config help path prints the config path help). So is the token after a -- that directly follows a command with subcommands: cospec config -- path runs config path.

As in OpenSpec, the program level and the command never rank against each other; cospec answers in two phases:

  1. -V/--version anywhere before -- prints the version, ahead of everything else — a short cluster that starts with -V (cospec list -Vh) included.
  2. The tokens before the command name are read first. A --cwd/--store with no value is refused; then, at the first -h/--help or unknown option, a help flag anywhere in the argv prints cospec's command list, and otherwise the option is refused with cospec: unknown option '<flag>' (plus Did you mean '<closest-global-flag>'? when an unknown -- option is close to one — never for a short option, as OpenSpec offers none), exit 1 — or, for --store-path, its redirect. Nothing after the command name is looked at: cospec --bogus list lists nothing and cospec --bogus list --store refuses --bogus, as openspec --bogus list refuses too, and cospec --help list --store prints the command list. An unknown command answers as one, unless a help flag follows it (cospec bogus --help prints the command list).
  3. Only then does the command read its own argv, in OpenSpec's per-command order: a missing value anywhere in it (cospec status --help --change and cospec status --bogus --change both refuse the missing --change, and a trailing --store-path answers its redirect where OpenSpec declares it), then --help, then the command's other refusals (the first unknown or not-yet-supported option, then a missing required argument, then too many arguments, then --store-path: cospec list --store-path /x --bogus refuses --bogus, and cospec new feat --store-path /x the missing slug), then an empty --cwd/--store (cospec list --store= --help prints help), then the command runs.

Short options combine as in OpenSpec: a cluster such as -yh splits only when its first letter is a short option the command declares, so cospec archive <slug> -yh is -y -h and prints the help, while cospec list -yh (no -y there) and cospec archive <slug> -hy (-h never starts a split) are refused as one unknown option.

cospec <command> help — a bare help token as the first thing after the command name, global flags aside (cospec archive --no-color help) — is equivalent to cospec <command> --help on every table-parsed command; it never runs the command. On a forwarded command it answers as OpenSpec does: cospec config help and cospec schema help print help, cospec store help and cospec workset help answer with OpenSpec's own refusal of help as an unknown subcommand, spelled cospec, and cospec show help passes help to the binary as the item name.

Commands ​

commandsynopsiskey flagssee
cospec init [path]Scaffold openspec/, the eleven typed schemas, and harness files. Idempotent. Under --json the document carries copilotCloud, the GitHub Copilot cloud-agent decision and what it wrote, removed or left in place.--yes, --force, --harness <list> (upstream spells it --tools; any tool id in the target table, all or none — trimmed and case-insensitive, as upstream reads --tools; an empty list is refused with upstream's message and writes nothing), --gate / --no-gate, --remove-opsx (also strips the OPENSPEC block from the eight root-level config files, CLAUDE.md and AGENTS.md among them, and keeps each file: one left with nothing else is written empty, never deleted), --profile core|custom (overrides the global profile key for this run; see Installed workflows and delivery), --language <language> (writes upstream's three-line directive into a new config.yaml's context:, and refuses, writing nothing, when an existing config lacks it; see Configuration), --copilot-cloud / --no-copilot-cloud (write or skip the GitHub Copilot cloud-agent files; the last one given wins, and it is saved as githubCopilot.cloudAgent; decision order)Installation
cospec update [path]Regenerate managed files (schemas, harness files) for the project at path (default .). It installs the global profile's workflows plus every cospec workflow already installed, so it never removes one; a delivery change removes the dropped surface's cospec files (see Installed workflows and delivery, BREAKING).--check (drift gate, exits nonzero on drift — including a not-yet-migrated .codex/skills layout — changes nothing), --force (also discards hand-edited legacy skill copies)Installation
cospec doctorRead-only health check: wrapped-OpenSpec version, schema/harness drift, a legacy-layout warning per file still under a legacy tool root, dangling slash/skill refs (failing on a residual [[opsx: conditional marker too), config.yaml validity (including rules: keys that are not artifact ids — see Configuration — and a malformed verification.layers, which gates ignore), openspec-global-profile (INFO: the explicit global profile and delivery, and any installed workflow outside the profile), installed-workflows (INFO: with no explicit profile, a harness that holds only some workflows, as after init --profile alone, which nothing remembers; names the ones update would install, and the missing-file and drift checks measure only what the harness holds), opsx-leftover for each root-level config file that still carries an OPENSPEC block, changes stuck on an old schemaVersion, and — on every root — OpenSpec's own doctor report folded in as openspec-* findings (root relationship, references, and for a store root its git/metadata facts), its remedies spelled cospec. Its harness checks (stale-harness, mixed-versions, dangling-ref) read only the files cospec writes: <skills-root>/<skill>/SKILL.md and each harness's command paths, such as .claude/commands/cospec/<id>.md. A shared .agents/skills root is checked for the one row its .cospec-target marker names. A user's own markdown under a harness dir, or a nested worktree's copy under .claude/worktrees/, is never checked; earlier releases checked every .md file under those dirs, so such a file could fail doctor. Its stale-sidecar check (an unreconciled .cospec-new file) and those harness reads share one boundary: they never descend into a nested git worktree checkout and never follow a symlinked .claude, .agents or openspec out of the project. The opsx-leftover check still reads every leftover location cospec init --remove-opsx does, except it never descends into a nested git worktree checkout and never follows a symlinked scan root (a .claude or .agents/skills resolving outside the project) out of the project, and both now also detect a real OpenCode .opencode/commands/opsx-<id>.md leftover — for one of the 12 ids the pinned dist ever generates, carrying its PROJECT_ROOT_GUARD lead sentence and command reference — by its own description-only shape, not just the author: openspec / name: "OPSX: …" markers other tools' files carry. The project config is openspec/config.yaml, else config.yml, as OpenSpec reads it. --json is {version, findings, summary, root, store, references, status}: the last four are OpenSpec's own keys as it reports them (each diagnostic's fix, and on a failed report its message, spelled cospec); with no OpenSpec root, its no-root diagnostic stays in status beside cospec's one initialized ERROR finding. Each line OpenSpec's doctor writes to stderr — its config warnings, such as Invalid 'context' field in config (must be string) — is an openspec-stderr WARNING finding (in --json too), printed once. cospec's own checks run on the operating root: the enclosing root from a subdirectory, and the store an explicit --store <id>, a store: pointer or the global defaultStore selects — so --store <id> checks the store from a bare workspace or from inside another project, exiting as openspec doctor --store <id> does; with no root selected they don't run, and a selection that fails for any other reason is reported by OpenSpec's folded diagnostic alone.—How it relates to OpenSpec, Stores
cospec new <type> <slug>Create a typed change and print its artifact plan. Also accepts cospec new "<type>: <description>", --goal <text> (stored in .openspec.yaml beside schema:/created:), and upstream's own create spelling, cospec new change <name> — without --schema (or with --schema '') OpenSpec itself picks the schema from the root's config.yaml schema: default, else spec-driven, printing its own warning on stderr for every config.yaml field it can't use (the file unparseable or not a mapping, a schema: that isn't a non-empty string, a bad context:, rules:, operations:, references:, store: or githubCopilot:), and its own refusal when that default names a schema it can't find (a whitespace-only schema:, or a cospec type the repo has no schema for); --description/--goal work the same on both spellings. --initiative <x> / --areas <x> (upstream's now-removed options) print upstream's removed-option message on stderr, or its initiative_option_removed / areas_option_removed document under --json, and create nothing. A cospec type the repo has no schema for, named as <type> or --schema, is refused before OpenSpec runs. Under --json every refusal of its own — no openspec/ tree, unknown type, missing schema, a slug it cannot derive, an invalid slug, an existing or archived change, a failed OpenSpec call — is one {change: null, status: [{severity, code: "change_error", message}]} document on stdout, exit 1; on success new … --json carries change, root, type, dir and (typed lane) artifacts — under cospec new <type> <slug> change is the slug string, while under upstream's cospec new change <name> it is upstream's own {id, path, metadataPath, schema} object, and root is the wrapped call's own on both. A failed OpenSpec call is answered with OpenSpec's own reason (a schema it cannot parse or a directory it cannot create, say — its paths and quoted excerpts verbatim, only OpenSpec's own remedy sentences respelled to cospec), as cospec new: <reason> in text or as the document's message; a missing type or slug or an unknown option stays a text parse refusal, as OpenSpec's own parse errors do, answered ahead of every other refusal (a missing openspec/ tree included).--description <text>, --goal <text>Types and artifacts
cospec migrate <slug>Opt-in: stamp a change created under an older schemaVersion to the current one, scaffolding a fully-deferred verification.md where the type requires it. Never runs automatically. Under --json, one document {change, schemaVersion, migrated, verificationScaffolded} on both paths — migrated: false when the change is already current.—Verification
cospec validate [name]Validate one or all changes and specs against cospec's rules. A name is resolved as OpenSpec resolves it: --type forces the kind; a name that is both a change and a living spec is refused (ambiguous_item) and one that is neither gets OpenSpec's nearest matches (unknown_item); a bulk flag beside a name runs the bulk scope and ignores the name. --report findings prints only the items with findings (the exit code is still the full report's); --concurrency bounds the change validations run at once. --json carries OpenSpec's root, items[].durationMs and summary.totals/byType beside cospec's keys, version stays 1, and an item's type stays the change's schema while kind carries OpenSpec's change/spec — see Validation rules. An unreadable artifact — a change file, the living spec.md a delta targets, or a living spec.md itself — is a meta/unreadable-artifact ERROR (a directory no artifact lives in, a dot-directory or one outside specs/, is passed by, as OpenSpec passes it by), a namespace folder a meta/nested-change ERROR, and a relayed OpenSpec message names cospec, never bare openspec. OpenSpec's own validation of an item is asked for by kind (--type change|spec), so a change sharing a living spec's name is still validated as a change; when OpenSpec refuses an item instead of reporting it, its refusal is that item's openspec/validate ERROR, never an empty pass. --strict fails a spec with a warning in valid and summary.totals, as OpenSpec does. --type spec on a spec discovery skips (a dot-directory, a capability behind a linked directory) validates that file as OpenSpec does. An unreadable openspec/changes/archive/ validates as if nothing were archived, with a warning (archive_unreadable in the document's warnings, Warning: on stderr). With no openspec/ directory a name alone is resolved as OpenSpec resolves it and, matching nothing, is unknown_item; any other --json invocation there is OpenSpec's one no_openspec_root document, exit 1. An unreadable openspec/changes/, openspec/specs/ or capability directory is one validate_error document under --json; --archived relays OpenSpec's own failure document (or its message in text) with its exit code. BREAKING: validate <name> --all|--changes|--specs validates the bulk scope, not the one item; an ambiguous name is refused and an unknown one prints OpenSpec's message.--strict (promote warnings to errors), --all, --changes, --specs, --archived, --type <change|spec>, --report <full|findings>, --concurrency <n> (else OPENSPEC_CONCURRENCY, else 6), --fast, --no-interactiveValidation rules
cospec status --change <slug>Per-artifact completion, the blocker gate state, and archive-readiness for one change (archiveReady, archive-ready: in text): true when every artifact the change's schemaVersion requires exists, every task is checked, the blocker gate is clear, and verification.blockedReasons is empty, so the flag is false while verification.md has an unresolved or malformed row, or any other verification/* error cospec archive's validation raises on it (a [~] row without a defer: reason, a group with no rows, a [x] row without evidence, an unknown layer or owner, a missing per-type row), each named in blockedReasons, and true again once each is fixed; it also reads a verification.md the type declares but does not require (build, ci, revert, or a schemaVersion 1 change) for those same verification/* errors, since cospec archive validates it, though a bare [ ] row there does not block; it does not model archive/scenario-preservation or validation errors outside verification.md (delta specs, proposal, design), which cospec archive still checks; --all sweeps every active change instead of one. Every entry names its next step — next under --json, a Next: line in text: the first ready artifact the change requires, else cospec apply <id> once every required one is done, else the first ready optional one. --json also carries every key OpenSpec's own status --json does (changeName, schemaName, planningHome, changeRoot, artifactPaths, isPlanningComplete, isComplete, applyRequires, nextSteps spelled cospec, actionContext, root, and each artifact's outputPath/status/requires), from one delegated call. --schema <name> is OpenSpec's schema override, not a filter: every change is reported as that schema, and an unknown name is refused with OpenSpec's Schema '<name>' not found before the sweep enumerates or the named change is reported. A change whose schema isn't a cospec type (a fork, spec-driven, or a name that resolves nowhere) is answered from OpenSpec's own status document, rendered as OpenSpec renders it in text, with OpenSpec's exit code. A change is looked up as OpenSpec looks it up: a directory under openspec/changes/ (a regular file of that name is no change) whose name OpenSpec accepts — no path separator, no leading dot, not archive — kebab-case or not. A change directory with no .openspec.yaml takes the root's config.yaml schema: (else spec-driven) at schemaVersion 1. A cospec-typed change with no artifacts yet is state: in-progress with artifacts: [], never filled with OpenSpec's artifact objects. A namespace folder is refused (--change) or a failure entry (--all), exit 1. An unreadable openspec/changes/archive/ computes the gate from an empty index with a warning (archive_unreadable under --json). A change OpenSpec refuses is refused: any error in OpenSpec's status for it is the answer — its change_error document under --json, its message in text, a failure entry under --all — and text mode asks OpenSpec too for a change cospec can't read every entry of, whose .openspec.yaml OpenSpec refuses (unreadable, not YAML, naming a schema OpenSpec doesn't list, or failing OpenSpec's metadata schema: a created that isn't YYYY-MM-DD, an empty goal, a non-boolean skip_specs or retire_capabilities, an affected_areas that isn't a list of non-empty strings, an initiative that isn't exactly {store, id} in kebab-case), or whose schema OpenSpec can't load (missing, unreadable, unparsable or invalid). So a cospec-typed change whose schema was removed from openspec/schemas/ is refused with OpenSpec's Unknown schema message in text as under --json, one whose created is malformed with OpenSpec's Invalid metadata message, and an unreadable change directory is refused, as is, under Bun on macOS, an unreadable file in it; elsewhere OpenSpec reads past the file, and an unreadable tasks.md is counted as no tasks with a warning (tasks_unreadable). Any other read failure is a change_error document, an unreadable openspec/changes/ included ({changes: [], root: null, status} under --all). Every OpenSpec message status relays, in text or in status[], is spelled cospec. BREAKING: archiveReady is false (text archive-ready: no) while verification rows are unresolved or malformed or the ledger fails archive's validation, where it read true; root is OpenSpec's {path, source} object, not a path string; a namespace folder makes status exit 1; --json on a schema cospec doesn't type exits 1 when OpenSpec does; a cospec-typed change whose schema OpenSpec can't load, or whose .openspec.yaml OpenSpec refuses, exits 1, in text and --json; a directory without .openspec.yaml is typed by config.yaml.--change <slug>, --all, --schema <name>Apply and archive
cospec listList active changes with type, gate state, task progress, and archive-readiness columns (the same archiveReady as status, for the same change), in OpenSpec's order and membership: most recently modified first, or by name with --sort name (any other value is the default, as in OpenSpec). --json rows also carry OpenSpec's name, completedTasks, totalTasks, lastModified, status and nested, and the document its warnings and root, from one delegated call. A namespace folder's row reads not a change (state not-a-change) with OpenSpec's Warning: after the table. A cospec-typed change's state (in-progress/building) is cospec's own fixed artifact filenames; a change on a schema cospec doesn't type additionally checks that schema's own generates pattern against the change directory, so a custom-named artifact cospec doesn't recognize by filename still reads building, not forced in-progress. A change directory with no .openspec.yaml takes the root's config.yaml schema: (else spec-driven) for this, the same fallback status and OpenSpec's own hasSchemaOutput use, so its row's type and completeness agree with status's; a declared schema that resolves to a real schema directory but fails to read, parse or validate warns (schema_unreadable) rather than silently reporting the row as empty. An unreadable openspec/changes/archive/ lists normally with a warning (archive_unreadable); a read failure OpenSpec refuses is OpenSpec's list_error answer; an unreadable tasks.md OpenSpec lists past counts as no tasks with a warning (tasks_unreadable); an unreadable blocking-changes.md fails only its row (error), exit 1. --specs instead lists living specs by requirement count (--json carries root); a failure OpenSpec reports there is relayed — its document under --json, cospec: <message> and its Fix: line in text — exit 1. BREAKING: archiveReady (the archive-ready marker) is false while verification rows are unresolved or malformed or the ledger fails archive's validation, where it read true; the default order is most recent first — pass --sort name for the old order; outside an OpenSpec root list answers OpenSpec's own no_openspec_root refusal (its message and Fix: line, or its document under --json), exit 1, where it printed No active changes.--blocked (only changes with a non-clear gate), --specs, --sort <recent|name>Apply and archive
cospec instructions [artifact] --change <slug>Print the authoring instructions for one artifact of a change (e.g. proposal, verification, tasks, archive). archive is a read-only relay of the wrapped openspec instructions archive, not an alias for cospec archive (requires openspec >=1.7.0). --schema <name> forwards to the wrapped call; both artifact and --change are optional, as upstream declares them — with either missing, the wrapped binary answers instead of a cospec-side refusal (its Available changes/Valid artifacts message), so --json gets exactly one document on every path. instructions apply --change <slug> is always cospec apply <slug> — the gate, from any directory and for any slug, with apply's own refusals (no openspec/ tree, an unknown change) — never OpenSpec's ungated apply instructions. --schema is refused there, before the gate runs, exit 1 (cospec instructions: '--schema' does not apply to 'apply' … on stderr, or one {status: [{severity, code: "schema_not_applicable", message}]} document under --json): OpenSpec's instructions apply --schema answers from another schema's apply requirements, while the gate enforces the change's own. Every other artifact's answer is built from the wrapped binary's own --json document: only the commands OpenSpec writes into it itself are respelled to cospec — each referenced store's Fetch: recipe and Fix: remedy (references[].fetch, references[].status[].fix, rewritten only where the whole value is one of OpenSpec's own remedies) and, for a change on OpenSpec's built-in spec-driven schema as the package ships it (not a project or user copy), that schema's own lines naming a bare openspec command. Your template, context, rules, spec summaries, store ids and paths are exactly what OpenSpec prints; text mode is OpenSpec's instruction layout rendered from the rewritten document, byte-identical to OpenSpec's wherever nothing was respelled. Every failure — an unknown change, a missing artifact or --change, apply or archive without a change — is OpenSpec's own answer rendered from its --json document: only a message or fix that is wholly one of OpenSpec's remedies names cospec (Create one with: cospec new <type> <name>), and the change names it lists under Available changes are exactly your directory names, whatever they read like.--change <slug>, --schema <name>, --allow-softWorkflow
cospec apply <slug>The gate: check blockers and required artifacts before you implement. On a clear gate, --json carries the project's context, operationGuidance and references under apply (apply.context, apply.operationGuidance, apply.references), and the text output prints them after the instruction, or before it for references; a project that configures none of them sees no new output. A references command field is spelled cospec, and nothing else is respelled.--allow-soft (proceed past a soft block), --skip-specs (one-shot equivalent of a persisted skip_specs: true marker)Apply and archive
cospec archive <slug>Validate, gate on tasks and verification, archive via OpenSpec, verify the move on disk, and fan out blocker sync. The Specs: line names why no spec sync ran (skipped (--skip-specs), none (the <type> schema has no specs artifact), none (no delta specs, so no spec sync)) or reads already in sync for an early-synced change. --json adds warnings/retired arrays (always present, [] when empty), specsSkipReason (flag/schema/no-deltas) when no sync ran, and OpenSpec's archive and root keys; every refusal is one document (see below). When the project configures them, the success --json document also carries top-level context and operationGuidance keys, and the text output prints them after the summary or a text-mode refusal.--skip-specs, --force-incomplete, --no-validate (skip revalidation, cospec's and OpenSpec's; every other gate still runs)Apply and archive
cospec sync-specs <slug>Merge a change's delta specs into the main specs without archiving it: archive's revalidation and scenario-preservation gate, then OpenSpec's own archive -y on a scratch copy under the OS temp directory, and only the main-spec files it changed copied back, byte-for-byte as cospec archive would write them. A symbolic link inside the copy, absolute or relative, points at the scratch copy of its target, so the run never writes through it into your tree; one leading outside the copied paths is refused before anything runs. A spec it cannot read is copied as an unreadable placeholder, so an unrelated one fails nothing. The change stays active, and its later archive is a no-op merge. Prints a Synced: line per file written or deleted and OpenSpec's totals, already in sync, or Nothing to sync: <why> (exit 0, no merge run) for a schema with no specs artifact, or — once archive's revalidation passes, so a delta kept in a file the merge never reads (specs/spec.md, specs/<capability>.md, a note beside spec.md) is refused as archive refuses it — for skip_specs: true or no delta files. --json: {change, type, synced, totals, files: {written, deleted}, warnings, root}; a refusal is archive's failure document with synced: false.—Apply and archive
cospec sync-blockersCheck off blocking-changes entries whose target has shipped, across all active changes.--check (report only, no writes), --change <slug>Blocking changes
cospec store <sub>First-class wrap of the store lifecycle: setup/register/unregister/remove/list (ls)/doctor. setup/register auto-run cospec init --harness none on success. No subcommand, an unknown one, an option where it belongs, or anything after -- (cospec store, store bogus, store --bogus, store -- --bogus) gets OpenSpec's own refusal, exit 1 — under --json its one unknown_store_subcommand document. Every relayed diagnostic's fix, and on failure its message, names the cospec command, text and --json.--no-cospec-init (setup/register only)Stores
cospec contextRead-only cross-repo working-set brief across a repo and its references: stores. The reference block's commands — each Fetch: and Fix: line, and under --json members[].fetch, members[].status[].fix and status[].fix — name cospec, spelled from OpenSpec's own document only where the whole value is one of OpenSpec's reference remedies; store ids, paths and a declared clone remote are printed as OpenSpec prints them.--json, --code-workspace <path>, --forceStores
cospec workset create|list|remove|openPersonal, local working views. No subcommand, an unknown one, an option where it belongs, or anything after -- gets OpenSpec's own refusal, exit 1 — under --json its one unknown_workset_subcommand document; create and an empty list print their next step as cospec workset …. open hands the terminal over to the workset's editor/agent session and never accepts --store; under --json it opens nothing and relays OpenSpec's workset_open_json_unsupported document, exit 1. Before handing over it refuses what OpenSpec would — the argv first, then, with no terminal (no TTY on stdin, CI, OPEN_SPEC_INTERACTIVE=0) or for a workset OpenSpec would refuse (an unreadable worksets file, not saved, no member folder on this machine), it runs the call piped and relays its answer, remedies spelled cospec.—Stores
cospec show <item>Show a single change or spec, text or JSON.--type, --deltas-only, --requirements-only, -r/--requirement, --no-scenarios, --diffRead-only and personal commands, OpenSpec's show
cospec viewSummary dashboard for the operating root. Accepts neither --json nor --store.—Read-only and personal commands, OpenSpec's view
cospec schemasList every resolvable schema — the eleven cospec types plus any project-local (forked) schema — with its artifact chain.—Configuration
cospec schema which|validate|fork|initInspect which schema a change resolves to, validate a schema's own structure, or create a project-local schema (fork <type> [name], init <name>). Refuses a destination name that collides with one of the eleven cospec types.--description <text>, --artifacts <list> (init only)Configuration
cospec templatesList resolved per-artifact template paths for a schema.--schema <name> (default spec-driven)Configuration
cospec config <sub>Machine-global OpenSpec config (~/.config/openspec/config.json): path, list, get <key>, set <key> <value>, unset <key>, reset, edit, profile [preset]. edit, profile with no preset, and reset --all without -y hand the terminal over (inherited stdio, verbatim child exit code) once their argv has been checked (reset --all with no TTY on stdin runs piped instead); the rest are piped. With no subcommand it prints cospec config --help on stderr, exit 1.--scope global (only accepted value), --json (list only — the rest get a cospec-owned envelope), -y/--yes (reset --all)Configuration
cospec completion [bash|zsh|fish|powershell]Print a shell completion script to stdout, generated from cospec's own command table. Shell auto-detected from $SHELL when omitted; a given shell name is case-insensitive, as upstream reads it. Also accepts upstream's cospec completion generate [shell]. Also install and uninstall (below).—Installation
cospec completion install|uninstall [shell]Write cospec's completion script for a shell and wire the shell to load it (install), or remove both (uninstall). Shell auto-detected from $SHELL when omitted. Four shells: bash, zsh, fish and powershell. The rc edit is one # COSPEC:START/# COSPEC:END block that uninstall removes exactly. uninstall asks first; -y skips the prompt, and with no terminal it refuses without -y.--verbose on install; -y, --yes on uninstallInstallation
cospec help [command]Print the program help, or one command's help (a hidden command's included), matching commander's implicit help. An unknown name prints the program help on stderr and exits 1.——
cospec feedback "<message>" [--body <text>]File a bug report at aligned-team/cospec via gh issue create (array argv, no shell); prints a prefilled manual-submission URL and exits 0 if gh is missing or unauthenticated. --upstream relays to openspec feedback instead, filing at OpenSpec's own tracker.--body <text>, --upstream—

cospec check-commit is a hidden commit-msg hook entrypoint (advisory only, never blocks a commit) and isn't part of the everyday command surface. cospec __complete <changes|specs|types|schemas|archived-changes> is a hidden dynamic-completion source the generated shell scripts call at Tab time — a failed lookup is silent (exit 1, nothing on either stream) so it can never corrupt a keystroke; only a parse refusal (no source at all, an unknown option) reaches stderr, which the scripts discard. The source name is matched in any case; schemas lists cospec schemas --json's names in their order (the scripts complete every --schema value and schema which|validate|fork's schema from it), and archived-changes the resolved root's archive entries. cospec experimental [--tool <id>] [--no-interactive] is a hidden, deprecated alias of init kept for upstream compatibility only — it never appears in cospec --help or completions, prints a deprecation note (unless --json), then runs init on . with --tool read as --harness.

Exit codes apply and archive use the same four-code contract

(0/1/2/3) across every gated command. The full table lives on Apply and archive — read it once, not per command. :::

status --all and --change are mutually exclusive Passing both (or

--all plus a positional change name) fails with The --all and --change options are mutually exclusive. — as a plain stderr line, exit 1, without --json; as { "changes": [], "root": null, "error": "..." } on stdout, exit 1, with --json. On success, --all's --json shape is { changes: ChangeEntry[], root: { path, source } }, where each entry is the shape a single cospec status --change <id> --json emits, less its root (or { change, error } plus OpenSpec's changeName/status if that one change's status could not be computed, a namespace folder included); exit is 1 if any entry failed, 0 otherwise. Under --json a lookup that fails — an unknown change, or no --change with several active changes — is one document on stdout in OpenSpec's shape, { "status": [{ "severity": "error", "code": "change_error", "message": "..." }] }, exit 1, and no active changes is { "changes": [], "message": "No active changes.", "root": { "path": "...", "source": "..." } }, exit 0. A root that can't be selected is one document too, with OpenSpec's payload — { "changes": [], "root": null, "status": [...] } for list and status --all, { "specs": [], "root": null, "status": [...] } for list --specs — and, for a failure OpenSpec doesn't diagnose (an unreadable store registry), its per-command code: list_error (list), change_error (status, apply) or validate_error (validate). Every cospec apply early exit under --json — no openspec/ root, an unknown change, a failed OpenSpec call, an unreadable openspec/changes/ — is one change_error document, exit 1; when OpenSpec's instructions apply refuses the change after the gate clears, its own failure document is the answer, spelled cospec (cospec apply: <message> and its Fix: line in text). :::

archive and sync-specs JSON documents A successful

cospec archive <slug> --json keeps every cospec key and adds OpenSpec's own: archive: { change, archivedAs, path, specsUpdated, totals?, warnings? } and root: { path, source, store_id? }. totals and specsUpdated are what OpenSpec reported applying (its Totals: and in-sync lines); totals is absent when no spec sync ran, warnings when OpenSpec reported none. Every refusal is one document on stdout, exit 1: cospec's change, type, archived: false and reason, then archive: null, root (absent when no root resolved) and status: [{ severity, code, message, fix? }], with OpenSpec's code and message wherever OpenSpec refuses the same input — archive_change_not_found, archive_change_name_invalid, archive_change_is_namespace_folder, archive_validation_failed (the revalidation report's keys kept), archive_tasks_incomplete (fix: --force-incomplete, since --yes does not lift cospec's tasks gate), archive_target_exists, archive_spec_update_failed (scenario preservation), archive_path_outside_root or archive_error for an unreadable openspec/changes/archive/ (whichever OpenSpec answers on that runtime: the first on macOS, the second on Linux), and archive_error for a failure after delegation, carrying OpenSpec's own reason. A bare [ ] verification row is the cospec-only archive_verification_incomplete. A root that can't be selected answers { "archive": null, "status": [...] }. sync-specs refuses with the same codes, synced: false in place of archived, and archive_error with OpenSpec's reason when its scratch run is refused. Relayed text in both — warnings, refusals, message and fix — is spelled cospec. :::

Read-only and personal commands ​

store, context, workset, show, view, schemas, schema which/ validate/fork/init, templates, config path/list/get, and list --specs/validate --all/--specs/--archived carry no cospec gate — none of them block a change lifecycle, require an artifact, or touch the verification ledger. Most of them (everything except store, which is a first-class wrap with its own filesystem-verified post-conditions) are disciplined passthroughs: cospec forwards the call to the wrapped OpenSpec binary under the same rigor as every gated command — a version-asserted spawn, a declared set of acceptable exit codes, a stdout deny-list, and stdout/stderr relayed as OpenSpec wrote them, its own remedies spelled cospec (see Unknown options) — and, when you pass --json, guarantees exactly one JSON document on stdout (never a stack trace, even on failure) so a script or agent reading the output can always parse it. The exceptions are where OpenSpec itself answers in text: a failed cospec templates --json (an unknown --schema, say) relays OpenSpec's ✖ Error: … line on stderr and exits 1 with nothing on stdout, exactly as openspec templates --json does, and so does a forwarded command's refused argument (--bogus), which is OpenSpec's own parse refusal. None of this changes what the commands do — show, view, schemas, schema, templates, and config's own key semantics in particular are genuinely OpenSpec's own job, and their full semantics live on OpenSpec's command reference — it only guarantees they fail predictably instead of silently.

config set/unset/reset/edit/profile are the read-only list's exceptions: they mutate the machine-global config file (or, for edit and a preset-less profile, hand the terminal over) — see Configuration for the full call-class split and the precedence notes cospec prints alongside them.

templates and schema which/validate/fork/init are a deliberate superset here: both reject --store on the wrapped binary, so cospec spawns them inside the resolved root's own directory instead of the invocation working directory — reaching a store-backed or ancestor-walked root, and (run from a subdirectory) the project's own schemas, where bare openspec reads only its own working directory and so can't see either. When the root can't be selected and you passed no --store (no root with stores registered, a broken store: pointer or defaultStore, an unreadable store registry or selected store), they run in the invocation working directory and answer as openspec does there — the same output, exit code, stderr and files. On every route they print none of root selection's own lines — no ignored-pointer warning, no Using OpenSpec root: … banner, no Warning: Invalid JSON … line — since openspec never selects a root for these commands. See Stores for the full account.

For anything that's the wrapped binary's own job — the delta format, OpenSpec's glossary, or its own commands — see OpenSpec's command reference.

Unknown options ​

Every command answers a flag or subcommand the pinned OpenSpec binary itself doesn't have the same way OpenSpec does — a refusal, exit 1 — instead of silently dropping it, which used to hand the flag's value to a positional (cospec validate --type change x validated an item literally named change). One command table (core/command-table.ts) drives argv parsing, per-command --help, and the shell completion spec together, so the three can't drift apart — a command that refuses --store neither lists it in its --help nor completes it after its name. Five refusal shapes, all exit 1:

  • Unknown flag: cospec <command>: unknown option '<flag>', followed by Did you mean '<closest-flag>'? — or Did you mean one of '<a>', '<b>'? when several tie — for an unknown -- option close to a long flag the command offers, matched the way OpenSpec matches it: the whole token (--srt=name included) within three edits, over half of it unchanged, never a one-letter name. An unknown short option gets no suggestion, as OpenSpec gives none.
  • Missing value:cospec <command>: option '<flag> <placeholder>' argument missing, for a value-taking flag given with nothing after it.
  • Missing argument: cospec <command>: missing required argument '<name>', then cospec <command>: usage — cospec <command> <positionals>, for a required positional given nothing — text even under --json, as OpenSpec's parse errors are, and ahead of --store-path and of any check on the openspec/ tree: cospec new feat --store-path /x refuses the missing slug, as openspec new change --store-path /x refuses its missing name. cospec new "<type>: <description>" fills both. archive <change> is required here where OpenSpec's is optional (openspec archive prompts for one), so cospec archive --store-path /x refuses the missing change, not the path; instructions [artifact] is optional, as OpenSpec declares it — a missing --change or artifact reaches the wrapped binary instead of a cospec-side refusal; check-commit's message file stays optional, since the hook never blocks a commit.
  • Too many arguments:cospec <command>: too many arguments. Expected N argument(s) but got M., for a positional the command doesn't take — on every table-parsed command, which is every command except the forwarded ones below. Nothing is dropped and run on the rest: cospec new feat add login is refused rather than creating a change named add (spell the slug add-login, or pass cospec new "feat: add login" as one argument), and so are cospec init a b, cospec apply <slug> extra, cospec migrate <slug> extra, cospec status a b and cospec instructions <artifact> extra. status's positional change is cospec's own spelling of --change (OpenSpec's status takes none), so beside --change or --all it's the excess argument OpenSpec refuses: cospec status foo --change bar answers Expected 0 arguments but got 1. instead of dropping foo.
  • Not supported yet: cospec <command>: '<flag>' is not supported yet, for an upstream flag cospec hasn't implemented. Its value is still consumed first, so a pending flag can never leak into a positional either. An upstream positional or subcommand cospec hasn't implemented is refused the same way, naming the slot or subcommand. No subcommand or flag is pending today, so this refusal has no live example.

Three flags are accepted as no-ops, because cospec already behaves as they ask: init --no-animation (cospec has no animation), archive -y/--yes (cospec never prompts), and list --changes (the default).

--store-path is refused on every command — in the --store-path <path> and --store-path=<path> forms, in both the pre-command and post-command position — with the same redirect OpenSpec prints, respelled to cospec, at the point OpenSpec refuses it. On a forwarded command OpenSpec itself decides: whatever it refuses first is relayed (cospec config path --bogus --store-path /x relays unknown option '--bogus'), and a --store-path that is another option's value is just that value (cospec schema init s1 --description --store-path creates the schema, as OpenSpec does). It is never refused after a leading --, where it is an operand (cospec -- config --store-path /x refuses --store-path as an unknown config subcommand):

✖ Error: --store-path is not supported. Register the path with cospec store
register <path>, then select it with --store <id>.
Fix: cospec store register <path>, then rerun with --store <id>.

Where OpenSpec declares the option — list, view, archive, validate, status, instructions, new, context, doctor, show and schemas — the space form takes the next token as its value whatever it looks like, and the command refuses it after its other parse refusals. Under --json there that's one document on stdout instead of stderr text: {"status":[{"severity":"error","code":"store_path_not_supported","message":"…","target":"store.id","fix":"…"}]}. cospec list --store-path --json prints the text redirect (--json is the path, so no document), and cospec show c1 --store-path --help the redirect, not help. Everywhere else — init, update, completion, feedback, config, schema, workset, store, templates and cospec's own commands (apply, migrate, sync-blockers, check-commit) — OpenSpec (or cospec) doesn't declare it, so it's an unknown option that takes no value: an earlier unknown option is refused first, a help flag after it prints the help (cospec init --store-path --help, cospec workset list --store-path -h), and the redirect stays stderr text even under --json. Before the command name it takes no value either, so cospec --store-path --help list prints the help. Register the path with cospec store register <path> and select it with --store <id> — see Stores.

Forwarded commands relay OpenSpec's own answer. show, templates, schemas, schema, store, workset and config (the read-only and personal commands above) don't reject an unknown option themselves — every token their own pre-spawn guards don't consume reaches the wrapped binary unchanged, and its answer is relayed verbatim — but for OpenSpec's own guidance, spelled cospec: on a successful answer only in the fields and whole lines where OpenSpec gives it (a store diagnostic's fix, the next-step line of workset create, an empty workset list and config profile <preset>), on a failed one wherever one of its remedy sentences appears. The flags cospec adds to the call (--json, --no-color, --store <id>) go right after the command, ahead of your own tokens, so they never become the value of an option you left without one: cospec store setup s1 --path is refused with OpenSpec's option '--path <path>' argument missing and writes nothing. OpenSpec's templates and schema subcommands declare no --store, so cospec never passes it to them: a --store <id> before or after the command name selects the root, and cospec runs them inside that root's directory instead — a deliberate superset, see Stores. Every other token reaches OpenSpec as you typed it: cospec templates --bogus --store <id> names --bogus, as openspec does, and cospec schema init s1 --store <id> --description refuses the missing --description value. Where openspec would refuse the --store itself (openspec --store <id> templates, or openspec templates --store <id> --bogus, which names --store), cospec reads the store's templates, or names the next unknown option (--bogus). Where OpenSpec's answer names a bare openspec command as the remedy, cospec relays it naming the cospec command of the same shape — show's Run "cospec status --change <id>" for a change with no proposal.md (text and --json), view's cospec list --changes/--specs, cospec init in the no-root answer from show, context and instructions, and schema init's "cospec schema fork" remedy (text and --json) and last next step, 3. Use with: cospec new <name> <slug> — and drops OpenSpec's noun-form change show/spec show suggestion, which cospec has no command for. Only OpenSpec's own remedy sentences are respelled, each where it appears verbatim: the path, name or list a sentence carries, and any other text — a directory named run openspec init, say — is relayed exactly as OpenSpec printed it. What a successful show prints is your own content, relayed untouched; a successful instructions answer respells only the commands OpenSpec writes into its own document (see the instructions row above), never your content. The pre-spawn guards answer only what OpenSpec would not — the terminal-handover leaves (workset open, config edit, config profile, config reset --all) aside, which refuse what OpenSpec would before handing the terminal over (see Configuration): an option where a subcommand belongs (cospec config --bogus path, cospec schema --bogus) reaches OpenSpec, which refuses it as an unknown option, not an unknown subcommand, and cospec show --bogus or cospec show --type gets OpenSpec's own answer (Unknown item '--bogus'., option '--type <type>' argument missing); only a show with no item at all — an empty "" is none, after -- too, and -r1, -r=1 or -rr is -r with its value, as OpenSpec reads it — gets cospec's item-name error (cospec show: an item name is required, exit 1; under --json one {"status":[{"severity":"error","code":"missing_item",…}]} document on stdout) instead of OpenSpec's "Nothing to show" screen, which names bare openspec commands. openspec show itself accepts an unrecognized flag by design (allowUnknownOption(true)), so a cospec-side rejection there would be the divergence from upstream, not a fix for one; the same forwarding lets a newer in-range OpenSpec's new flag keep working immediately instead of failing until cospec's table catches up.

--json on a command that can't emit it. cospec view renders a text dashboard and cospec completion prints or installs a shell script; both refuse --json with exactly one document on stdout — {"version":1,"command":"<name>","ok":false,"message":"…"} — and exit 1, rather than the older behavior of accepting the flag and silently ignoring it.

Per-surface runtime minimums ​

Most of cospec runs against any accepted >=1.0.0 <2.0.0 build, but a handful of surfaces are pure delegation to a feature the wrapped binary only grew at a later version. Below the stated minimum, cospec exits 1 with a named error rather than faking the feature or silently degrading:

SurfaceRequires openspecBehavior below the minimum
cospec instructions archive>=1.7.0the wrapped binary's own exit-1 error (Artifact 'archive' not found …) is the answer, rendered from its --json document — no special-casing; its text spinner line reads Loading archive inputs... where that binary's reads Generating instructions...
cospec validate --archived>=1.9.0exits 1 with a named error, never an empty passing report