Agent Reference
The reference for a coding agent working in a repository that uses Spec Engine. It has three parts: the model and the working loop, the standard every requirement is written against, and the exact contract of every spec command.
What Spec Engine is
Section titled “What Spec Engine is”Spec Engine helps developers, product teams, and agents write better specs and keep them current, so the software changes accurately.
It works with four things:
| Noun | What it is | Where it lives | Authored or derived |
|---|---|---|---|
| Domain | A named subject area of requirements with an UPPERCASE key (BILLING), an owner, and a charter that says what belongs in it |
spec-engine/<KEY>/SPEC.json, one file per domain |
Authored |
| Requirement | One durable rule of the product’s behavior, addressed by a permanent id KEY-NNN (BILLING-009), with a statement, a why, and a status |
An entry in its domain’s SPEC.json |
Authored |
| Tag | A comment @spec KEY-NNN that binds a line of code or a test to a requirement |
Source and test files, in comments | Authored in code |
| Index | A SQLite database of every domain, requirement, and tag, which the read and check commands query | .spec-engine/index.sqlite at the platform root |
Derived |
- The
SPEC.jsonfiles are the only authored truth about what the product promises. Write them through thespeccommands. An id is never reused or renumbered: a changed promise is superseded by a new id, and an ended one is deprecated. - Tags in code are the only truth about where a requirement is implemented and where it is verified. A tag in implementation code implements; a tag in a test file verifies. The kind comes from the file path, never from the tag text.
- The index is derived and disposable. Every command rebuilds it from the
SPEC.jsonfiles and the tags, and deleting.spec-engine/is always safe. - Requirements are not tickets. One ticket usually becomes several requirements. A ticket id is provenance only: it goes in
--issue, never in a requirement id or a tag.
The loop
Section titled “The loop”spec query "renewal charge" . --json # 1. route: requirements on the topicspec resolve src/billing/renew.ts . --json # or the ones tagged in a file# 2. implement the change# 3. tag the function that produces the behavior and the test that proves itspec check . --ci --json # 4. exit 1: fix before the PRspec guard . --json # 5. exit 1: the change loses a requirementWhen spec guard blocks, relay its 🛑 spec-guard: block verbatim to the user. Then either spec supersede the requirement with a successor or get the user’s explicit decision to spec deprecate it. Never delete a requirement to make the gate pass.
What to tag
Section titled “What to tag”export function renew() { /* … */ } // @spec BILLING-009it("charges current price", () => {}) // @spec BILLING-009 unit- A tag binds the one function that produces the behavior and the one test that proves it.
- Helpers, glue code, and configuration carry no tag.
- If you cannot name the behavior a line produces, do not tag it.
- A test tag may end with a level:
unit,integration, ore2e.
spec check . --ci --base "$(git merge-base HEAD origin/main)"Without --base, check diffs against HEAD, which in a CI checkout already holds the change, so the removal and loss checks find nothing to report.
Requirement authoring standard
Section titled “Requirement authoring standard”The rules every new requirement is written against. The statement shapes and the statement rules are also checked at write time and by spec check.
-
A requirement is one rule of the product’s behavior, stated from the user’s point of view. The statement rules below are the mechanical half of this rule:
spec req,amend,supersede,move, andacceptrefuse a statement that breaks one,spec checkreports an Active statement that breaks one asSTATEMENT_RULES, andspec lintlists every finding. The judgment half stays with the author. -
One behavior per requirement. A ticket usually splits into several requirements. Never mint one requirement per ticket.
-
Write the statement in a fixed shape. Pick the sentence shape that matches the situation. The word “shall” separates who acts from the observable result:
Situation Shape Example Always true The <system>shall<result>The spec CLI shall write JSON output to stdout only. Triggered by an event When <trigger>, the<system>shall<result>When an unknown requirement id is passed, spec resolve shall print []and exit 0.Only in a certain state While <state>, the<system>shall<result>While the index file is missing, read commands shall rebuild it before answering. Handling something going wrong If <failure>, then the<system>shall<result>If the results file cannot be parsed, then spec check shall report the file and exit 2. Only when a feature is present Where <feature>, the<system>shall<result>Where a member config lists ignore directories, the scanner shall skip them. A statement that fits no row has not yet decided what triggers the behavior or what observably happens.
A statement may end with a trailing
so <observable consequence>clause (“…shall report NO_SPEC_CONFIG at warning severity, so a lone warning never fails the gate”) when the consequence is itself observable. The reason the behavior exists belongs in the why, never in the statement.One promise per statement. A result that chains two behaviors with
and, or that sets two severities, is two requirements: a single test must be able to prove the whole statement. PreferWhen,If … then, and the bareshallform. UseWhileandWhereonly when a state or a feature is what gates the behavior. -
It must make sense on its own. A reader who sees only this one statement, in search results or a report, understands it. A statement that says “the normalization” or “that prefix” without naming the subject fails.
-
Describe what it does, not how. Say what a user or calling agent observes. If renaming an internal function would make the statement wrong, it is written at the wrong level.
-
No expiration dates. No “currently”, no “the new behavior”, no references to things that were removed.
-
The why states what breaks. Answer “what goes wrong if this stops being true”. Never restate the requirement in other words.
-
Three link types, three jobs. Tests prove it. Code implements it. Tickets are only history: never an id, never proof.
-
File it where the promise lives, and search first. Pick the domain whose charter covers the behavior, and run
spec querybefore minting. If something close exists, amend it instead.
Requirement id format
Section titled “Requirement id format”- Ids are
KEY-NNN: the domain key, a hyphen, and a three-digit zero-padded number (BILLING-010). - Ids are permanent. A requirement is superseded or deprecated, never renumbered and never deleted.
The one-promise lint
Section titled “The one-promise lint”A domain envelope may declare grammar: "ears" with grammarSeverity: "warning" | "error" (default warning). Set both with spec domain grammar <KEY>, never by hand. In such a domain a statement must fit one of the five shapes, and a statement that fits a shape still fails when:
| Rule | Fails | Passes |
|---|---|---|
| Semicolon clause | ; anywhere after the first shall |
When a; b holds, X shall … |
| Rationale | because, so that, in order to anywhere |
a trailing , so <observable consequence> |
spec checkreports a nonconforming Active or Draft statement asSTATEMENT_GRAMMARat the declared severity, one row per statement, the detail naming the first failed shape or rule.spec req,amend,supersede, andmovejudge new text at write time: they warn and write under warning, and refuse (exit 2) under error.- A second
shallis theone-promisestatement rule’s, reported on every domain asSTATEMENT_RULES, never asSTATEMENT_GRAMMAR. - The lint is a pattern, not a reading: a statement that passes can still chain two behaviors, and the reviewer still owns that call.
- The TERM domain is exempt.
The statement rules
Section titled “The statement rules”Every domain is judged by these rules, whatever its grammar. spec req, amend, supersede, move (with new text), and accept refuse a statement that breaks a rule graded error (the default for every rule but no-trailing-commentary, which defaults to warning): exit 2, one <rule>: <detail> — <reason> line per finding, rule 0 last. A rule graded warning writes the statement and prints the same finding lines on stderr. There is no override flag; the statementRules block of check.json grades a rule, tunes it, or turns it off. spec check reports an Active statement that breaks one as STATEMENT_RULES, one row per rule per statement. The TERM domain is never judged.
| Rule | Reports | Printed reason |
|---|---|---|
one-sentence |
more than one sentence, no closing period, or a line break | a statement is one testable sentence |
length |
over maxLength characters (280 by default) |
a sentence a reader holds at once |
no-file-path |
a repository path or file name | which file holds what is layout, not behaviour |
no-test-narration |
how a test is written | how a test is written is not what the product does |
no-toolchain-layout |
CI or tooling layout | how CI and tooling are arranged is not a product behaviour |
no-history |
how the requirement changed | a statement says what holds now, not how it got there |
no-jargon |
a word from the jargon list | a reader without the codebase cannot parse project jargon |
no-tool-statement |
which tool is in use | naming the tool in use is a decision record, not a requirement |
one-promise |
a second shall after the first, , and <verb> chained onto the result, or , and exit <N> |
a second promise is its own requirement |
no-mechanism |
via, by <verb>ing, through the, using, or by means of in the result |
how the product does it is design, not behaviour |
no-contrast |
instead of, rather than, never … but, or not <X>, <Y> and , not <X> in the result |
a behaviour is stated by what it does, not by what it is not |
no-document-content |
README or the document states anywhere, or a document (the table, a section, the agent reference, AGENTS.md, TAXONOMY.md) as the subject |
what a document contains is documentation, not behaviour |
where-takes-a-feature |
a Where clause holding a --flag or an invocation (runs, is given, is passed) |
a flag or an invocation is a trigger and takes When |
no-trailing-commentary |
a trailing , so … clause over eight words or holding a second verb phrase |
a trailing so-clause names one short consequence, not a second point |
Authoring requirements (brief → mint)
Section titled “Authoring requirements (brief → mint)”The procedure for turning a brief or ticket into requirements. Each draft is written to the requirement authoring standard above.
- Split the brief. One requirement per behavior, never one per ticket.
- Place each.
spec domain list --jsonprints every charter. File the requirement where the promise lives. - Dedup.
spec query "<phrase>" . --jsonbefore minting. Overlap meansspec amendor relate the existing requirement. - Draft to the standard. Every draft quotes the phrase of the brief it derives from.
- Mint.
spec req <domain> --text "<statement>" --why "<what breaks>" [--issue <ticket>]. - Provenance, not identity. The ticket goes in
--issue, never in an id or a tag. - Verify.
spec index . && spec check . --ci(exit 0) andspec guard .before the PR.
Step 3 also runs at write time. spec req --text, spec supersede, spec move, and spec amend --text compare the written statement with the Active requirements spec query ranks highest for its words, print each one whose word overlap reaches the duplicate threshold as possible duplicate of <ID> (<score>): <statement> on stderr, and write anyway. The score is the Jaccard overlap of the two statements’ word sets after lowercasing, dropping punctuation, English stopwords, and the EARS keywords, and folding plural and tense suffixes. The default threshold is 0.5. A hit is a prompt to fold or relate the two, never a refusal. Two statements that mean the same thing in different words score low; the check catches shared wording only.
Command reference
Section titled “Command reference”Invocation
Section titled “Invocation”spec <command> [...] # after: bun add -g @spec-engine/spec-enginebunx @spec-engine/spec-engine <command> [...] # without a global install (requires Bun)- Almost every command takes an optional trailing
platformDirpositional (default: cwd). The platform dir is the directory that containsspec-engine/. - Every command is non-interactive when stdin is not a TTY.
--jsongoes to stdout only. Guidance, warnings, and diagnostics chrome go to stderr. Parse stdout, surface stderr.
Platforms, members, and pins
Section titled “Platforms, members, and pins”- A platform is the directory holding
spec-engine/. When declared, its platform fileplatform-map.jsonis{ name, members, ignore }. - A member is a repository named in that file, carrying a
{ platform, member }marker. A monorepo’s coverage columns are its workspace packages, named by path (packages/engine). A lone repository with no platform file is one column or its packages. A monorepo’s files outside every package are one more column, named by the member or, for a lone monorepo, the directory. spec-engine.member.jsonadds one fact: the member’s pin,spec-engine@N.spec init <name>declares and pins in one step.- A requirement domain’s version is derived from its supersede history. The platform version is the max domain version. Drift compares a member’s pin against each referenced requirement’s
changed_at_version. A strayspec-engine.platform.jsonis ignored with a warning. - A member’s
dependsOn(the platform’s own package names in its manifest) ordersspec propagation: providers before consumers.
Editor support
Section titled “Editor support”The docs site publishes a JSON Schema for each file the engine reads, generated from the schemas the engine parses with:
| File | Schema URL |
|---|---|
spec-engine/<KEY>/SPEC.json |
https://docs.spec-engine.dev/schema/v1/spec.json |
spec-engine/check.json |
https://docs.spec-engine.dev/schema/v1/check.json |
spec-engine.member.json |
https://docs.spec-engine.dev/schema/v1/member.json |
- A file names its schema with a top-level
"$schema"key, which editors read for completion and validation. Every other unknown top-level key still fails the schema. spec domain newandspec initwrite the key first.spec migrateadds it to a file that lacks it, andspec checkreports such a file asMIGRATION_PENDING.- A rewrite keeps an existing
$schemakey in place with its value. - The
v1path only changes additively. A breaking shape is published under a new path.
- The tag is
@spec KEY-NNN, optionally followed by a level tokenunit|integration|e2eon a test tag. Never write “implements” or “verifies” in the tag. - The scanner reads
.ts,.tsx,.js,.jsx,.mjs,.sh,.lua,.luau,.go,.rs,.tf,.toml,.sql,.css,.yml,.yaml, and.astrofiles, any file namedDockerfileorDockerfile.<suffix>, and.mddocuments, whose tags document. - The tag regex ignores the comment leader, so
--,//,#,<!-- -->, and/* … */all work. - Dot-prefixed directories are skipped except
.github/.fixtures/directories are ignored everywhere. - A tag in implementation code implements; a tag in a test path verifies. A test path contains
.test.,.spec.,__tests__/,/tests/,/e2e/, or.e2e., or ends in_test.go,_test.rs,_spec.lua, or_test.lua, or lives underspec/(busted). - Rust tests inline under
#[cfg(test)]sit in the implementation file and count as implements; put a verifying tag intests/or a*_test.rsfile.
Exit code contract
Section titled “Exit code contract”| Code | Meaning |
|---|---|
| 0 | Success. Empty results are success ([] on stdout, guidance on stderr). |
| 1 | Data-level failure: check found an error-severity diagnostic; guard found a loss; gate failed; index crashed mid-build. |
| 2 | Usage/environment error: bad args, not a Spec Engine platform (no spec-engine/), path-containment violation, invalid --limit/--port, FTS5 syntax error. |
Branch on exit codes, not on output text.
Commands
Section titled “Commands”| Command | Synopsis | --json |
Exits |
|---|---|---|---|
spec index [platformDir] |
Build or refresh the derived index | yes (IndexResult) | 0 / 1 / 2 |
spec check [platformDir] [--ci] |
Integrity + coverage + drift diagnostics | yes (array) | 0 / 1 / 2 |
spec guard [platformDir] [--against <ref>] |
Block a change about to lose a requirement, its last implementation, or its last test | yes (array) | 0 / 1 / 2 |
spec map [platformDir] |
Requirement × repo coverage matrix | yes (array) | 0 / 2 |
spec get <KEY-NNN> [platformDir] |
One requirement’s full record and its tag sites; unknown id is [] |
yes (object, or []) |
0 / 2 |
spec list [platformDir] [--domain KEY] [--status S] |
Every requirement record in (key, seq) order, history included | yes (array) | 0 / 2 |
spec query <text> [platformDir] |
Full-text retrieval over requirements | yes (array) | 0 / 2 |
spec relations [platformDir] |
Mermaid graph of relates links |
yes (array) | 0 / 2 |
spec provenance [issueId] [platformDir] |
Per-requirement provenance matrix | yes (array) | 0 / 2 |
spec resolve <files…> [platformDir] |
Requirements tagged in the given files | yes (array) | 0 / 2 |
spec propagation <KEY-NNN> [platformDir] |
Per-repo migrated/drifted state for a superseded requirement | yes (array) | 0 / 2 |
spec gate <repo> <KEY-NNN> [platformDir] |
Approval gate: pass iff Active and the pin covers it | yes (object) | 0 / 1 / 2 |
spec init [repo] |
Declare a member through platform-map and write its spec-engine.member.json pin |
yes (object) | 0 / 2 |
spec domain new <KEY> / spec domain list / spec domain grammar <KEY> |
Scaffold / list spec-engine/<KEY>/SPEC.json / set its statement grammar and severity |
list: yes (array); grammar: yes (object) |
0 / 2 |
spec migrate [platformDir] |
Strip the retired livesIn and verifiedIn fields from every SPEC.json and add a missing $schema key to every SPEC.json, check.json, and member config (idempotent) |
yes (object) | 0 / 2 |
spec req <domain-prefix> [platformDir] |
Piped: next unused id. --text: author non-interactively |
yes (object) | 0 / 2 |
spec term … |
Author, list, revise, and confirm glossary terms | yes | 0 / 2 |
spec glossary [platformDir] |
Generate GLOSSARY.md from the TERM store; --migrate, --check |
yes (object) | 0 / 1 / 2 |
spec deprecate <KEY-NNN> --reason "…" [platformDir] |
End an Active/Draft requirement with a recorded reason | yes (object) | 0 / 2 |
spec supersede <KEY-NNN> [platformDir] |
Flip to superseded, mint the successor, retag every site to it | yes (object) | 0 / 2 |
spec move <KEY-NNN> <NEW-DOMAIN> [platformDir] |
Cross-domain supersede | yes (object) | 0 / 2 |
spec amend <KEY-NNN> [platformDir] |
Revise an unshipped entry in place | yes (object) | 0 / 2 |
spec accept <KEY-NNN> [platformDir] |
Promote a Draft requirement to Active | yes (object) | 0 / 2 |
spec lint [KEY | KEY-NNN] [platformDir] [--all] |
Every statement-rule finding for the platform, a domain, or a requirement | yes (array) | 0 / 1 / 2 |
spec serve [platformDir] [--port N] [--host IP] |
Local webapp + /api/* over the index |
n/a (HTTP) | 0 / 1 / 2 |
spec docs [--port N] |
Serve the bundled docs site offline | n/a (HTTP) | 0 / 1 / 2 |
spec mcp [platformDir] |
MCP server over stdio | n/a (JSON-RPC) | 0 / 2 |
Common flags on the index-touching commands (index, check, map, get, list, query, relations, resolve, propagation, provenance, gate, serve):
| Flag | Effect |
|---|---|
--out <path> |
Index DB path override. Must resolve inside platformDir (exit 2 otherwise). |
--no-prompt |
Suppress the member-onboarding prompt. |
--json |
Machine output (all except serve). |
--fresh |
Read commands only: delete the DB plus WAL/SHM and reindex before answering. |
init, domain list, req register --json only. domain new registers none.
Index freshness. Read commands (map, get, list, query, relations, resolve, propagation, provenance) build the index when it is missing and otherwise trust it. A warm index older than a changed SPEC.json produces a stderr warning. Tag-only code edits are not detected. gate always rebuilds cold, and so does check --ci. After editing specs or tags, pass --fresh.
spec index
Section titled “spec index”Build or refresh the derived index.
--json: { build_id, repos, domains, requirements, tags, diagnostics }.
Exit: 0 built / 1 an invalid SPEC.json refused the build, or the build crashed / 2 not a platform or bad args.
build_idis deterministic for identical inputs (cold-rebuild equivalence).- A
SPEC.jsonthat is not JSON or fails the shared schema refuses the whole build, and the index on disk is left as it was. Every file is parsed first, so one run reports every invalid file. Under--jsonstdout holds only theINVALID_DOMAIN_FILErows in thespec check --jsonrow shape; text mode prints only those rows on stderr. Every other index-building command prints only the same rows and exits 1 (check,gate,guard,serve, and the read commands); a lifecycle command that reindexes for its retag (supersede,amend,deprecate,move,term confirm) exits 2 after its write. - Also writes
.spec-engine/doctor.md, a list of doc lines that mention a requirement id without an explicit<!-- @spec ID -->binding.
spec check
Section titled “spec check”Integrity, coverage, drift, and governance diagnostics.
| Flag | Effect |
|---|---|
--ci |
Delete the DB first. Correctness never trusts a warm index. |
--results <junit.xml> |
The trusted-red gate: a verifying tag counts only when its test passed. Path resolves inside platformDir. |
--base <ref> |
Diff the requirement set against a git ref (deletion and status-flip governance). |
--approved-by <list> |
Approver set for the status-flip check. |
--require-owner-approval |
Escalate UNAPPROVED_STATUS_FLIP from warning to error. |
--unsourced-change |
Also warn on superseded requirements with no supersedes-via issue. |
--json rows: { code, severity, repo, source_file, line, req_id, detail }, nullable except code, severity, detail, sorted deterministically.
Exit: 0 clean or warnings only / 1 any error-severity row / 2 usage.
Codes, in DiagnosticCode order:
| Code | Severity | Condition |
|---|---|---|
DUP_ID |
error | The same requirement id appears twice |
BROKEN_SUPERSEDE |
error | A supersededBy names a missing id, or a supersedes names a missing id or the entry itself |
BAD_STATUS |
error | An entry status outside active / draft / superseded / deprecated |
DANGLING_TAG |
error | A tag references no requirement |
SUPERSEDED_REFERENCED |
error | Code still tags a superseded requirement (retag to the successor) |
DEPRECATED_REFERENCED |
error | Code still tags a deprecated requirement |
STATEMENT_GRAMMAR |
per domain, default warning | A statement fails its domain’s declared grammar |
STATEMENT_RULES |
error; per rule from statementRules.severity |
An Active statement breaks a statement rule: one row per rule per statement, the detail <rule>: <detail> — <reason> |
POSSIBLE_DUPLICATE |
warning | Two Active non-TERM statements reach the duplicate threshold: one row per pair, req_id the higher id, the detail possible duplicate of <ID> (<score>) |
DRAFT_REFERENCED |
error on a verifying tag, warning on an implementing or documenting tag | A tag references a Draft requirement |
GLOSSARY_DRIFT |
warning | The committed glossary differs from the one generated from the term store |
ORPHAN_REQ |
error | An Active requirement has no implementing tag |
UNVERIFIED_REQ |
warning | An Active requirement is implemented but has no verifying tag |
DRIFT |
error | A member is pinned behind the version at which a requirement it references changed |
NO_SPEC_CONFIG |
warning | A declared member is present on disk with no spec-engine.member.json |
BROKEN_RELATES |
warning | A relates entry names a nonexistent requirement |
RELATES_SUPERSEDED |
warning | An Active or Draft requirement’s relates entry names a superseded requirement |
CYCLIC_SUPERSEDE |
error | A circular supersession chain |
SELF_RELATES |
warning | A relates entry names its own requirement (dropped, but surfaced) |
UNKNOWN_ROLE |
warning | An issues entry uses a role outside created / supersedes-via / amends-via (surfaced and dropped, never stored) |
UNSOURCED_CHANGE |
warning, only with --unsourced-change |
A superseded requirement carries no supersedes-via issue |
INVALID_DOMAIN_FILE |
error | A SPEC.json is not JSON or fails the shared schema; one row per failing field |
MIGRATION_PENDING |
warning | A SPEC.json still carries the retired livesIn or verifiedIn fields, or a SPEC.json, check.json, or spec-engine.member.json has no $schema key; the detail names which and the schema URL (https://docs.spec-engine.dev/schema/v1/<spec|check|member>.json); run spec migrate |
UNPROVEN_REQ |
error, only with --results |
An Active requirement has verifying tags but no passing correlated test |
PROOFS_UNCONFIRMED |
warning, only without --results |
The proof gate fell back to presence-only mode |
REQUIREMENT_REMOVED |
error | An Active requirement present at the base ref is gone with no same-change supersession |
IMPL_LOST |
error, when a base ref resolves | The change removes the last implementing tag of a surviving Active requirement |
VERIFY_LOST |
warning, when a base ref resolves | The change removes the last verifying tag of a surviving Active requirement |
SPEC_FILE_DELETED |
error, when a base ref resolves | A SPEC.json present at the base ref is gone |
UNAPPROVED_STATUS_FLIP |
warning; error with --require-owner-approval |
A flip to superseded or deprecated whose CODEOWNERS domain owner is absent from --approved-by |
PARTIAL_PROPAGATION |
error, needs --base and --results |
A changed requirement where some bound tests pass and some fail |
MALFORMED_FILE |
platform-map’s severity: error, or warning for a manifest | A platform file, a package or workspace manifest, or platform-map’s per-user file failed to parse or validate |
MEMBER_MISSING |
warning | A declared member is not on this machine; it is not scanned |
MARKER_MISSING |
warning | A declared member has no marker |
MARKER_MISMATCH |
error | A member’s marker names a different platform |
UNLISTED_REPO |
warning (platform-map info, promoted) | A repository in the platform folder is not a member; its tags are not scanned |
PLATFORM_NOT_LOCATED |
warning | A marker names a platform this machine cannot find |
UNDECLARED_PLATFORM |
warning (platform-map info, promoted) | A folder of repositories with no platform file; nothing in it is a member |
SCAN_TRUNCATED |
warning | A workspace walk hit its depth or entry cap |
UNDEFINED_TERM |
error | A cites entry resolves to no glossary term |
ORPHAN_TERM |
warning | An Active term that nothing cites |
TERM_DRIFT |
warning | A citation’s pin is behind the term’s current version |
SUPERSEDED_TERM_REFERENCED |
error | A citation names a superseded term |
The four TERM codes are the last four rows. Only error rows drive exit 1.
The eight codes before them are platform-map’s, surfaced with the platform-map code: every platform-map error and warning keeps its severity; UNLISTED_REPO and UNDECLARED_PLATFORM are promoted from info to warning because they mean a repository’s tags are not scanned; UNMATCHED_PATTERN and AMBIGUOUS_ECOSYSTEM are not surfaced. repo is the diagnostic’s subject when it names a member; source_file is the subject. NO_SPEC_CONFIG stays Spec Engine’s own code: a declared member present on disk with no spec-engine.member.json.
- Deletion detection runs by default: with no
--base, the working tree is diffed againstHEADwhenever git resolves. An Active requirement absent from the tree without a same-change supersession isREQUIREMENT_REMOVED. History (superseded and deprecated entries) may never be deleted. Outside git the check stays quiet. - Without
--results,checkemitsPROOFS_UNCONFIRMEDand skips proof enforcement. The results file is never hashed intobuild_id. Generate it with the test runner’s JUnit reporter, for examplebun test --reporter=junit --reporter-outfile=.spec-engine/results.xml. - Term citations:
UNDEFINED_TERM,SUPERSEDED_TERM_REFERENCED, andTERM_DRIFTclear withspec term confirm. - Losses: whenever a base ref resolves (
--base, elseHEAD),checkreports theIMPL_LOST,VERIFY_LOST, andSPEC_FILE_DELETEDrowsspec guardwould report against that ref. Guard’sREQUIREMENT_REMOVEDrows are dropped; the removal check already reports that id. - Under
--ciwith no--base, a working tree whose tracked files equalHEADmakes every loss check pass vacuously, socheckwarns on stderr with the line to run instead:spec check . --ci --base "$(git merge-base HEAD origin/<default>)". The exit code and stdout are unchanged. - Coverage counts only Active requirements:
ORPHAN_REQandUNVERIFIED_REQfire for Active entries only.
check.json
Section titled “check.json”spec-engine/check.json regrades diagnostics. Absent means no rules.
{ "severity": [ { "code": "UNVERIFIED_REQ", "severity": "error" }, { "code": "UNVERIFIED_REQ", "severity": "warning", "match": ["LEGACY", "BILLING-004"] }] }codeis any diagnostic code;severityiserror,warning, oroff.matchlists domain keys and requirement ids. A scoped rule applies only to rows whosereq_idis listed or carries a listed key.- The most specific rule wins: an id match, then a domain match, then an unscoped rule. Among equals, the later rule wins.
offdrops the row from every output.- Grading runs inside the operation, so the CLI,
spec_check, and every other surface agree, and exit 1 follows the graded severities. - A file that fails the schema (unknown code or key, a bad severity, a malformed match entry) exits 2 with
spec-engine/check.json: <path>: <reason>.
statementRules configures the statement rules on every surface: the write-time refusal, STATEMENT_RULES, and spec lint.
{ "severity": [], "statementRules": { "maxLength": 280, "jargon": ["seam", "rung"], "exemptNames": ["SPEC.json", "spec-engine/"], "severity": { "no-jargon": "off", "length": "error", "no-trailing-commentary": "error" }} }maxLengthdefaults to 280.jargonreplaces the default list (seam,seams,rung,rungs,residue,matrix entry,stage runner,the stage,self-member,context walk,canonical row,layer,corpus).exemptNamesare product namesno-file-pathnever reports. A name ending in/exempts every path under it.severitymaps a rule toerror,warning, oroff. A rule with no entry iserror, exceptno-trailing-commentary, which iswarning.errorrefuses the write and grades theSTATEMENT_RULESrow error;warningwrites, prints the finding on stderr, and grades the row warning;offturns the rule off everywhere. Theseverityarray regrades by code after it.- The rules:
one-sentence,length,no-file-path,no-test-narration,no-toolchain-layout,no-history,no-jargon,no-tool-statement,one-promise,no-mechanism,no-contrast,no-document-content,where-takes-a-feature,no-trailing-commentary. Unknown keys exit 2.
duplicates tunes duplicate detection on every surface: the write-time warning and POSSIBLE_DUPLICATE.
{ "severity": [], "duplicates": { "threshold": 0.6, "off": false, "ignore": ["REQ-069/REQ-060"] } }thresholdis a score between 0 and 1; a pair at or above it is a possible duplicate. Absent means 0.5.off: trueturns detection off everywhere. Aseverityrule ofoffforPOSSIBLE_DUPLICATEdrops only the check rows.ignorelists pairs reviewed as distinct, each two requirement ids joined by/in either order. A listed pair gets noPOSSIBLE_DUPLICATErow and no write-time warning.
spec guard
Section titled “spec guard”The pre-commit loss gate. Diffs the requirement derivation at --against <ref> (default HEAD) against the working tree, scoped to the files in git diff. Rebuilds the worktree index cold every run.
--json rows: { kind, req_id, file, line, detail }, sorted by (req_id, kind, file, line). kind is one of REQUIREMENT_REMOVED, IMPL_LOST, VERIFY_LOST, SPEC_FILE_DELETED (req_id null).
Exit: 0 clean / 1 any loss / 2 usage.
- A loss is suppressed only by ending the requirement in the same change: supersede it (either direction) or
spec deprecateit. There is no override comment. - A non-git tree, a repo with no
HEAD, or an unknown ref printsNOT_A_GIT_REPOto stderr and exits 0. - A ref that begins with
-or holds a character outsideA-Z a-z 0-9 . _ / -is refused before git runs:guardtreats it as an unknown ref, andcheck --baseexits 2. - Text mode prints a
🛑 spec-guard:block per requirement, written in the second person. Relay it verbatim to the user before proceeding.
spec map
Section titled “spec map”--json rows keyed (req_id, repo): { req_id, domain_key, req_status, req_spec_version, req_changed_at_version, repo, repo_pin, implemented: 0|1, verified: 0|1, test_levels }. Text mode renders src, test, src+test, —.
Exit: 0 / 2.
spec get
Section titled “spec get”One requirement’s full record from the derived index, with the code sites its tags name. Tags are the only record of where a requirement is implemented and verified.
--json: the Requirement row { id, key, seq, status, superseded_by, text, why, source_file, line, spec_version, changed_at_version, superseded_at_version } plus implemented_in: [{ repo, file, line, kind }] (implementing and documenting tags) and verified_in: [{ repo, file, line, level }] (verifying tags), each sorted by (repo, file, line). The same object is served by GET /api/requirements/:id and spec_get, and shown on the webapp /requirements/:id page. Text mode is a one-row table (ID STATUS KEY SEQ CHANGED_AT TEXT) followed by an implemented in: and a verified in: list of file:line.
Exit: 0 / 2. An unknown id prints [] on stdout, guidance on stderr, and exits 0. A malformed id exits 2.
spec list
Section titled “spec list”Every requirement record in (key, seq) order. Unlike query, superseded and deprecated rows are included and nothing is ranked.
| Flag | Effect |
|---|---|
--domain KEY |
Only that domain (case-insensitive key). |
--status S |
Only that status: active, draft, superseded, deprecated. Any other word exits 2. |
--json: an array of Requirement rows, the same rows GET /api/requirements serves. Text mode is the ID STATUS KEY SEQ CHANGED_AT TEXT table.
Exit: 0 / 2. An empty result is [] under --json, guidance on stderr in text mode, exit 0.
spec query
Section titled “spec query”FTS5 MATCH syntax. --limit N (default 10, max 1000).
Exit: 0 / 2 (bare AND or OR is an FTS5 grammar error).
- Wrap multi-word phrases in double quotes inside the shell-quoted arg.
- Results rank ascending (best first). Superseded requirements are excluded.
- Glossary term definitions ride the same index. A matching term appears in a separate Terms group in text mode, discriminated by
keyunder--json.
spec relations
Section titled “spec relations”Text mode: mermaid graph LR, one node per requirement in a relates field, one undirected deduped edge per pair. --json rows: { from_id, to_id, source_file, line }, sorted by (from, to).
Exit: 0 / 2. Empty graph is [] and exit 0.
spec provenance
Section titled “spec provenance”Per requirement: the creating issue, the revising issues (supersedes-via, amends-via), the backing tests, and the source_file:line pointer.
| Flag | Effect |
|---|---|
[issueId] positional |
Display-only reverse filter. Never a join key. |
--resolve-issues |
Overlay tracker title/status/URL. Needs SPEC_TRACKER_TOKEN; degrades to bare ids without it. |
--json rows: { req_id, role, issue_id, source_file, line, req_status, implemented, verified, test_levels }, sorted on the full composite key.
Exit: 0 / 2. Empty matrix is [] and exit 0.
spec resolve
Section titled “spec resolve”Accepts multiple positionals, comma-split inside one positional, and absolute paths under the platform dir.
| Flag | Effect |
|---|---|
--req KEY-NNN |
Reverse query: every tag site for that requirement. Takes no file positionals. |
--json rows (--req): { req_id, repo, file, line, kind, level } sorted by (repo, file, line). kind is implements, verifies, or documents.
Exit: 0 / 2. Empty result is [] and exit 0. Unknown id under --req is [].
Path rules. tags.file is platform-relative: <member>/<path> for a file inside a declared member or a workspace package, and the bare <path> for a file a lone repository owns itself (its directory-named or root column). The key never carries the checkout’s folder name.
- Multi-repo platform: pass
<repo>/<path>, e.g.spec resolve api/src/renew.ts . - Single repo: pass the path from the platform root, e.g.
spec resolve src/orders.ts .orspec resolve .github/workflows/ci.yml . - Absolute paths are normalized against
platformDir. A path outside it exits 2.
spec propagation
Section titled “spec propagation”One row per member repo: { repo, state, via_req_id, drifted }. state is one of MIGRATED_VERIFIED, MIGRATED_UNVERIFIED, ON_PREDECESSOR, ON_OTHER_DOMAIN_REQ, NO_DOMAIN_REFERENCE.
Rows are in dependency order: a member comes after every member it depends on, read from platform-map’s dependsOn (the platform’s own package names in a member’s manifest), and members at the same depth come by name. The same order is served by /api/propagation/:id, spec_propagation, and the webapp’s /propagation/:id page. A platform with no in-platform dependencies lists members by name.
Exit: 0 / 2.
spec gate
Section titled “spec gate”--json: { pass, reason, repo, req_id, detail, status, changed_at_version, pinned_spec_version }. reason is one of PASS, NOT_FOUND, DRAFT, SUPERSEDED, DEPRECATED, VERSION_PIN, decided in that order. Pin equality passes.
Exit: 0 pass / 1 fail / 2 unknown repo name or bad args.
spec init
Section titled “spec init”Declares a member through platform-map and writes spec-engine.member.json: { "$schema": "https://docs.spec-engine.dev/schema/v1/member.json", "specs": "spec-engine@N" }, plus ignore when set. Init writes no check.json.
| Flag | Effect |
|---|---|
--specs spec-engine@N |
Override the pin. |
--force |
Rewrite an existing config, preserving ignore and $schema. Any other key refuses, as does a malformed ignore. |
--platform <dir> |
The platform root, for a declared member checked out outside the platform folder. Its location is recorded in platform-map’s per-user file first. |
--json: { action: "wrote", path, pin, source, declared, linked, agent_surface }, { action: "already-configured", path, pin, extra_fields, declared, linked, agent_surface }, or, on a platform directory, { action: "surface-only", agent_surface }. declared lists the platform-map files written (the platform file, the marker), empty when the member was already declared; linked is the per-user file written under --platform, else null; agent_surface lists the agent-surface files written or changed, [] on a re-run.
Exit: 0 wrote or already configured / 2. Branch on action, not the exit code.
- Where the target sits decides what is written:
- a repository directly under a platform folder (a directory holding
spec-engine/) that the platform file does not list is declared: aplatform-map.jsonplatform file entry at the root (the file is created when absent) and a{ platform, member }marker in the repository, then the pin. platform-map declares only a directory with a.gitentry or apackage.json; a plain folder is refused with exit 2; - a declared member only gets the pin (a missing marker is written back);
- a workspace package of a monorepo gets a nested pin and no declaration; a repository inside a lone monorepo that is not a workspace package is refused;
- a directory under no platform gets the
spec-engine@1fallback pin; - a platform directory (holding
spec-engine/) gets the agent surface and no pin.
- a repository directly under a platform folder (a directory holding
- Every action then installs the agent surface into the repository (the member; the monorepo root for a workspace package), bundled in the npm package under
dist/agent/. Hints for anything left alone go to stderr.AGENTS.md: the agent reference between aspec-engine:agent-reference:beginand aspec-engine:agent-reference:endHTML comment marker, appended or created, replaced in place when the markers exist; text outside them is never touched. ACLAUDE.mdthat does not mentionAGENTS.mdgets a hint only..claude/skills/req-author/: written when absent; a differing copy is replaced only under--force..mcp.json: aspecserver entry (spec mcp <platform>) merged in; other keys and an existingspecentry are kept.pre-commit:spec guard <platform>, written to.githooks/or else.git/hooks/when no hook exists there; an existing hook gets a hint.- With no platform located, the MCP entry and the hook are skipped.
- The platform root is platform-map’s
locate(): the nearestplatform-map.jsonor.gitboundary, a marker resolved through the parent directory or the per-user file; the parent directory when it holdsspec-engine/. A.gitboundary between the target and an outerspec-engine/hides that platform. - Default pin:
--specs, else the derived platform version, elsespec-engine@1with a printed note. - A resolved path with a
spec-enginesegment is refused, except on the engine’s own checkout (root package named@spec-engine/spec-engine), where only the path below the platform root is judged. ignore: ["dir", …]excludes repo-relative directory prefixes from that repo’s scans:vendorhidesvendor/x.tsbut notsrc/vendor/x.ts. A monorepo root column excludes its workspace package paths the same way. A monorepo’s columns are its workspace manifest’s packages.
spec domain
Section titled “spec domain”spec domain new <KEY> scaffolds spec-engine/<KEY>/SPEC.json with "$schema": "https://docs.spec-engine.dev/schema/v1/spec.json" as its first key. spec domain list prints one key per line; --json prints a sorted array of { key, scope } (scope is the charter sentence or null), read from the filesystem.
spec domain grammar <KEY> [platformDir] [--grammar ears|freeform] [--severity warning|error] sets the envelope’s grammar and/or grammarSeverity through the validating write path; every other field is written back as read. --json prints { key, grammar, grammar_severity, file } (an unset field is null). An unknown domain, neither flag, or a value outside the enums exits 2. CLI only: no MCP tool or HTTP route.
spec domain grammar CHCK --severity errorExit: 0 / 2.
spec migrate
Section titled “spec migrate”Rewrites every SPEC.json that still carries the retired livesIn or verifiedIn fields through the write seam, which drops them. Until a file is migrated, the fields are read and ignored, and the next write of a file drops them.
It also adds a $schema key as the first key of every SPEC.json, spec-engine/check.json, and member spec-engine.member.json that lacks one: https://docs.spec-engine.dev/schema/v1/spec.json, …/check.json, and …/member.json. A config is checked against its schema first and only gains the inserted line. A file needing neither change is not touched, so a second run writes nothing.
--json: { files }, the platform-relative paths rewritten: SPEC.json files in key order, then check.json, then member configs.
Exit: 0 / 2.
spec req
Section titled “spec req”| Mode | Behavior |
|---|---|
stdin not a TTY, no --text |
Print the bare next unused id (e.g. BILLING-010), exit 0. No prompts, no writes. |
--json, no --text |
Print { domain, next_id }. Same zero-write contract, even on a TTY. |
--text "<statement>" |
Append an Active entry with zero prompts. --why, --relates, --issue <ticket> fill the other fields and error without --text. --relates repeats and comma-separates. |
--draft |
Author the entry as Draft (with --text, or on a TTY). Off a TTY without --text it exits 2. |
| TTY, no flags | Interactive authoring. |
--json with --text: { id, file, possible_duplicates }, plus clauses: { pattern, condition, system, response } when the statement parses as EARS. possible_duplicates is [{ id, score }], empty when none.
Exit: 0 / 2 (ambiguous prefix, unknown domain, grammar refusal, statement-rule refusal).
- The domain prefix is case-insensitive:
bilresolves toBILLING. - The resolved domain’s charter is echoed to stderr on every authoring path.
- Unresolvable
@<path>refs warn on stderr and never block. - Each possible duplicate prints
possible duplicate of <ID> (<score>): <statement>on stderr, and the entry is written anyway (see the authoring section).
spec term
Section titled “spec term”A term is a requirement row in the reserved TERM domain: definition in statement, headword in term, synonyms in aliases.
| Form | Effect |
|---|---|
spec term "<name>" --def "<definition>" [--aliases a,b] [--section s] |
Append a TERM-NNN. --json: { id, file }. |
spec term with no --def |
Pure id query, mirrors spec req: bare next TERM id, or { domain, next_id }. |
spec term list |
id name status per line, or [{ id, term, status }]. Reads the filesystem. |
spec term revise <TERM-NNN> --def "…" |
Rewrite the definition in place and bump the TERM envelope’s specVersion. --no-bump opts out. --json: { id, file, spec_version }. |
spec term confirm <KEY-NNN> <TERM-NNN> |
Re-pin a citation to the term’s current version (clears TERM_DRIFT); re-point to the successor when the term is superseded (clears SUPERSEDED_TERM_REFERENCED). --json: { req_id, term_id, pinned, file }. |
spec supersede TERM-NNN and spec amend TERM-NNN --term/--aliases work on term ids as on requirements.
Exit: 0 / 2.
spec glossary
Section titled “spec glossary”| Form | Effect |
|---|---|
spec glossary |
Overwrite GLOSSARY.md from the TERM store. Byte-stable: fixed header, terms in id order, ## {section} once per change, one - **{term}** — {statement} bullet each. |
--migrate |
One-time parse of an existing GLOSSARY.md into TERM-001..N. Skips when the TERM domain already has entries. |
--check |
Regenerate into a buffer and diff against the committed file. Exit 1 on drift. |
--json: { generated }, { migrated }, or { ok }.
Exit: 0 / 1 (--check drift) / 2.
spec deprecate
Section titled “spec deprecate”Marks an Active/Draft requirement deprecated, records --reason on the entry, and removes every @spec tag of the id from the code. A line that holds only the tag (and its comment leader) is deleted; on a line with other text, only the tag and its level token go. Text mode prints the removed sites.
--json: { id, file, reason, removed: [{ req_id, repo, file, line, kind, level }] }, the sites as they stood before the removal.
Tag sites come from the freshly built index, so fixtures/ and dot-prefixed directories are never touched. Each rewritten line keeps its comment leader, level token, and line ending, and no other line changes. Running the command again finds no sites.
Exit: 0 / 2.
spec supersede
Section titled “spec supersede”Requires an Active target. Flips it to superseded by NEW, mints the successor in the domain file that holds the target, reindexes fresh, and rewrites every @spec OLD tag in the code to @spec NEW, keeping the level token and the rest of the line. Text mode prints the rewritten sites. Tag sites come from the freshly built index, so fixtures/ and dot-prefixed directories are never touched. Each rewritten line keeps its comment leader, level token, and line ending, and no other line changes. Running the command again finds no sites.
The successor copies the predecessor’s why, relates, cites, aliases, term, and section unless a flag overrides the field. Its issues hold only the created row from --issue.
| Flag | Effect |
|---|---|
--text |
The successor statement. Required when not a TTY. |
--why, --relates |
Successor fields. Default to copies of the predecessor’s. --relates repeats or comma-separates and replaces the whole list; an empty value clears it. |
--issue <ticket> |
Recorded as supersedes-via on the predecessor and created on the successor. |
--no-bump |
TERM domain only: keep the authored specVersion. No-op elsewhere. |
--json: { old_id, new_id, file, spec_version, retagged: [{ req_id, repo, file, line, kind, level }], possible_duplicates: [{ id, score }] }, the rewritten sites (req_id is the old id). On a requirement domain spec_version is the derived domain version after the supersession, and the same number is stamped as the predecessor’s supersededAtVersion.
Exit: 0 / 2. All guards run before the first byte is written.
spec move
Section titled “spec move”Cross-domain supersede. Mints the successor as the next id in <NEW-DOMAIN> (which must exist), copies statement, why, relates, cites, aliases, term, and section (--text and --why override), flips the source to superseded with a cross-domain supersededBy, and rewrites every tag of the source to the successor as spec supersede does. The successor carries no issues.
--json: { old_id, new_id, from_file, to_file, source_spec_version, target_spec_version, retagged: [...], possible_duplicates: [...] }.
Exit: 0 / 2 (non-Active source, or the target is the source’s own domain).
- Both envelopes validate before either is written.
- The source version advances; the target version is unchanged.
spec amend
Section titled “spec amend”Revise an Active or Draft entry in place. Same id, no version change.
| Flag | Effect |
|---|---|
--text, --why, --relates |
Field to change. At least one flag is required. Untouched fields stay byte-identical. --relates repeats or comma-separates and replaces the whole list (the --aliases idiom); an empty value clears it. |
--issue <ticket> |
Append amends-via provenance. |
--term, --aliases |
TERM ids only. |
--json: { id, file, fields_changed, possible_duplicates }. possible_duplicates is empty unless --text changed the statement.
A released requirement is an Active entry present in its domain file at the newest v* tag merged into HEAD. Its promise does not change in place: a --text, --why, --term, or --aliases amend refuses with exit 2, naming the tag. Two paths stay open: spec supersede changes the promise, and an amend of only --relates or --issue writes in place. With no v* tag merged into HEAD, or outside git, every entry is unreleased. A Draft entry always amends.
Exit: 0 / 2 (superseded or deprecated target; a released entry’s promise fields).
spec accept
Section titled “spec accept”Promotes a Draft requirement to Active. Every other field stays byte-identical and the domain version does not move. Reword a draft with spec amend first; end one with spec deprecate.
--json: { id, file }.
Exit: 0 / 2 (the target is not Draft: nothing is written).
- A Draft never raises
ORPHAN_REQorUNVERIFIED_REQand is not counted in the coverage report. A code tag on one isDRAFT_REFERENCED(warning).
spec lint
Section titled “spec lint”Every statement-rule finding. Reads SPEC.json directly, not the index.
| Form | Scope |
|---|---|
spec lint [platformDir] |
the platform |
spec lint <KEY> [platformDir] |
one domain |
spec lint <KEY-NNN> [platformDir] |
one requirement |
--all |
Draft requirements as well as Active ones |
Text mode prints <id> <rule>: <detail> — <reason> per finding on stdout, then rule 0 on stderr. --json prints a sorted array of { req_id, rule, detail, reason, source_file, line }. The TERM domain is never judged. A lone positional that holds spec-engine/ is the platform dir. CLI only: no MCP tool or HTTP route.
Exit: 0 clean / 1 any finding / 2 usage, unknown domain or id, or a bad check.json.
spec serve
Section titled “spec serve”Binds one address: 127.0.0.1, or --host <ip> (a literal loopback, RFC 1918, or 100.64/10 address only; anything else exits 2). --port 0 (default) picks a free port. --probe boots on loopback, fetches /, and exits 0 or 1. The startup line, /api/platform (name), and every page’s title and sidebar name the platform (platform file name, else the directory name), never a path; spec mcp logs the same name on stderr.
A non-loopback --host is LAN mode: stdout prints http://<ip>:<port>/?token=…; every request needs that token (the query parameter is exchanged for an HttpOnly; SameSite=Strict cookie, then a 302 drops it), else 401. A Host other than loopback or the bound address is 403. Read-only unless --allow-writes: non-GET requests, /provenance, and /api/provenance?resolve=1 answer 403. --token / SPEC_SERVE_TOKEN pin the token (≥ 16 of A-Z a-z 0-9 _ -); otherwise it is 256 random bits.
When the reindex after a write refuses on an invalid SPEC.json elsewhere on the platform, the route answers 422 { error: "invalid_domain_file", diagnostics: [...] }, the rows in the check --json shape (the CLI exits 2 with the same rows).
The create, amend, supersede, and move routes add possible_duplicates: [{ id, score }] to their success body, as the matching CLI --json does, and the editor page lists each one under the saved id.
Always-on routes: /, /requirements[/:id], /propagation/:id, /api/coverage, /api/report, /api/repos, /api/platform, /api/requirements[/:id], /api/propagation/:id, /api/resolve?files=….
Flag-gated, 404 until enabled. SPEC_FLAGS is comma-separated; unknown keys are ignored.
SPEC_FLAGS key |
Routes |
|---|---|
query |
/query, /api/query?q=&limit= |
relations |
/relations, /api/relations[?format=mermaid] |
provenance |
/provenance, /api/provenance[?resolve=1], /api/provenance/by-issue?issue= |
editor |
/editor, POST and PUT /api/requirements, POST /api/requirements/:id/supersede, POST /api/requirements/:id/move, POST /api/requirements/:id/deprecate, POST /api/requirements/:id/accept |
glossary, logs |
Placeholder pages |
Exit: 0 / 1 / 2.
spec docs
Section titled “spec docs”Serves the prebuilt docs site on 127.0.0.1, fully offline.
- Takes no platform dir. Registers none of
--json,--out,--no-prompt. - The npm artifact ships the site at
dist/docs/. A source checkout servespackages/site/dist; runbun run build:sitefirst, else exit 2. --probemirrorsserve --probe.
Exit: 0 / 1 / 2.
spec mcp
Section titled “spec mcp”MCP server over stdio. Register it as { "command": "spec", "args": ["mcp", "<platformDir>"] }.
| Tool | Returns |
|---|---|
spec_query |
FTS hits, the spec query --json shape |
spec_resolve |
files → requirements |
spec_req_tags |
requirement → tag sites |
spec_coverage_report |
per-domain rollup { domain, active, implemented, verified, orphans, unverified } |
spec_check |
diagnostic rows |
spec_propagation |
per-repo migration state |
spec_next_id |
{ domain, next_id }, nothing written |
spec_get |
one Requirement row with implemented_in and verified_in, or [] for an unknown id; the spec get --json shape |
spec_list |
Requirement rows in (key, seq) order, domain and status filters; the spec list --json shape |
spec_supersede |
{ old_id, new_id, file, spec_version, retagged, possible_duplicates }, the spec supersede --json shape; optional why, relates, issue arguments |
spec_deprecate |
{ id, file, reason, removed }, the spec deprecate --json shape |
spec_accept |
{ id, file }, the spec accept --json shape |
spec_req |
{ id, file, clauses?, possible_duplicates }, the spec req --text --json shape; domain (key or prefix) and text required, optional why, relates, issue, draft |
spec_amend |
{ id, file, fields_changed, possible_duplicates }, the spec amend --json shape; id required, optional text, why, relates, issue |
spec_move |
{ old_id, new_id, from_file, to_file, source_spec_version, target_spec_version, retagged, possible_duplicates }, the spec move --json shape; id and newDomain required, optional text, why |
Every tool call reindexes fresh. stdout is the protocol channel; chrome goes to stderr. One prompt, author_requirements (brief required, domain optional), returns a static authoring playbook with the brief and the domain’s charter substituted. The engine runs no model.
Exit: 0 / 2.
Gotchas
Section titled “Gotchas”fixtures/is globally ignored by the tag scanner. Tags under anyfixtures/folder are test data, not coverage claims.- The index owns nothing. Never edit
.spec-engine/index.sqlite. Edit theSPEC.jsonsource and re-runspec index. storage_unavailable/SQLITE_IOERR*means your environment. The index runs in WAL mode and needs real file locks. A sandboxed process getsSQLITE_IOERR_VNODE; re-run unsandboxed or grant write and lock access to<platformDir>/.spec-engine/.SQLITE_BUSYmeans anotherspecprocess holds the index.- Empty is not an error. Read commands exit 0 with
[]and guidance on stderr. - Determinism is contractual. JSON outputs are sorted and byte-stable.
- A directory passed as the last positional is
platformDironly if it containsspec-engine/. Otherwise it is treated as a file, with a warning.