Skip to content

About

SpecScore CLI — lint, query, and scaffold SpecScore specifications.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Repository files navigation

specscore-cli

CI Coverage

CLI for SpecScore — lint, query, and scaffold SpecScore specifications.

Our approach to development

We build with our own tooling:

  • SpecScore — specify requirements as SpecScore.md artifacts
  • SpecStudio — author & manage specs across their lifecycle
  • inGitDB — store structured data in Git where applicable
  • DALgo — data access layer for Go
  • cover100.dev — drive toward 100% test coverage
  • DataTug — query & explore data

Used in DataTug development

DataTug uses SpecScore contracts to shape product requirements, acceptance criteria, and plans; this CLI provides the linting and inspection tools for that specification workflow. See the tools behind DataTug.

Install

macOS — Homebrew cask (recommended)

brew install --cask specscore/tap/specscore

The Darwin artifact is signed with a Developer ID, notarized by Apple, and the release fails closed (require_notarized_macos: true) if that ever regresses — see spec/features/cli/release-distribution/ for the verification evidence. Upgrade with brew upgrade --cask specscore. Do not bypass Gatekeeper or remove quarantine attributes.

macOS — source build (secondary)

go install github.com/specscore/specscore-cli/cmd/specscore@latest

This compiles the latest published source release locally. Automated or agent evidence MUST instead pin an exact released tag or merged commit SHA, then verify the resulting build identity; never use @main.

Linux — curl

curl -fsSL https://specscore.md/install/get-cli | sh

Windows — PowerShell

powershell -c "irm https://specscore.md/install/get-cli.ps1 | iex"

Windows — Scoop / WinGet

scoop bucket add specscore https://github.com/specscore/scoop-bucket
scoop install specscore
# or
winget install SpecScore.CLI

See installation docs for options (version pinning, custom install dir).

Usage

specscore spec lint              # lint the current spec tree
specscore feature list           # list features
specscore feature show <slug>    # inspect a feature
specscore task list              # show the task board
specscore rule list --applies-to <path>   # which normative rules bind this path
specscore rehearse run           # execute markdown acceptance scenarios
specscore version                # full build identity
specscore --version              # bare semver

Full command reference: see spec/features/cli/.

Rules — the transferable half of agent memory

A Rule is one normative sentence — MUST or NEVER, in plain words — with the scope it binds, the reason it exists, the sources that produced it, and the control that enforces it. Rules exist because durable operating knowledge otherwise accumulates in per-agent memory files, which do not transfer: a new session, a new machine, or a new runtime starts blind.

Two forms, one entity. An inline rule is one row in spec/rules/README.md and nothing else — most rules are one sentence, and a directory each would be ceremony that discourages recording them at all. A detailed rule keeps the identical row, linked to spec/rules/<slug>/README.md, which carries the reason, worked compliant and violating examples, agent instructions, exceptions, and supersession. The index row is the source of truth for every field it carries; a detail document repeats those fields for readability and lint (R-011) requires them to agree, with --fix rewriting the document from the row and never the reverse.

specscore rule new never-mock-backends \
  --statement "Never ship a mocked extension backend; ship real dalgo-backed routes or ship nothing." \
  --scope fleet --source lesson:never-mock-extension-backends     # inline: one row

specscore rule new gofmt-first --scope 'path:**/*.go' --detailed \
  --statement "Always run gofmt on changed Go files before any build, lint or test command." \
  --enforcement Enforced --control "wb pre-commit hook profile go-standard"

specscore rule expand never-mock-backends                # give an inline rule a document
specscore rule list --applies-to internal/cli/rule.go    # which rules bind this file?
specscore rule show never-mock-backends --format json    # row + document + resolved links
specscore rule update never-mock-backends --enforcement Enforced --control "wb pre-push hook"
specscore rule delete old-rule --supersede-with new-rule
specscore rule lint                                      # just the R-001..R-011 family

Only the slug is required: a bare specscore rule new <slug> records a lint-clean Draft row with a TODO statement, so writing a rule down under time pressure is one command. Every verb takes --format text|yaml|json and is non-interactive. rule list reads only the index — never a detail document — so a tool can afford to run it at the start of every agent stream and print the rules that apply.

Fields. The row carries Statement (the normative sentence), Scope (fleet | product:<name> | repo:<owner/repo> | path:<glob>, repeatable), Status (Draft | Active | Superseded), Enforcement (Stated | Enforced | Automated), Control (required for the latter two — an enforced rule with no control is a stated rule wearing a stronger label), and Sources (lesson:<slug> | decision:<id> | idea:<slug> | a URL). A detail document adds Why, Exceptions, Supersedes / Superseded By, an ## Instructions section, and ## Examples with both ### Compliant and ### Violation.

Lessons promote into Rules. A Lesson explains a process gap; a rule is the transferable sentence that comes out of it:

specscore rule promote --from-lesson kinder-fake-hides-bug no-hand-rolled-fakes

This records the rule with its statement pre-filled from the Lesson's **Control:** (detailed by default — the Lesson already carries the reason; pass --inline to opt out), writes **Promotes To:** rule:no-hand-rolled-fakes back into the Lesson, and records lesson:kinder-fake-hides-bug in the rule's Sources. Lint rule R-008 then checks that pair in both directions, so neither half can be edited away unnoticed. One consequence is deliberate: a Lesson carries exactly one promotion pointer, so it can be the source of at most one rule (a rule may cite several Lessons). A Lesson that merely informed a rule without producing it belongs in that rule's **Why:**, not its Sources.

Rules pair with skills. A skill tells an agent how to do a job; a rule tells it what it must never do while doing that job. A detail document's Instructions may name skill:<name>, and a skill's ## Rules section may name rule:<slug>; R-010 checks that pair in both directions too, so a skill cannot silently outlive the rule constraining it. Skills are read from ai/skills/ by default; set rules.skills_path in specscore.yaml to move them.

The R-001..R-011 rules run inside specscore spec lint as well; --fix repairs the index table shape, adds a row for an orphan document, corrects a stale link cell, and rewrites a drifted document header from its row — and never touches the Why, Instructions or Examples an author wrote. Full contract: spec/features/cli/rule/.

Note the singular: specscore rule is this artifact kind. specscore rules (plural) is the unrelated read-only catalog of lint rules.

SpecScore Studio — multi-repo fact indexing

specscore studio federates per-repo artifacts (spec trees, CodeGrapher codegraph/ snapshots, go.mod/package.json manifests, ops registries) across a whole ecosystem into one queryable fact store. Describe the ecosystem in a studio.yaml workspace file:

name: demo
repos:              # directory paths or globs, absolute or workspace-relative
  - ../repo-a
  - ../org/*        # glob entries expand to existing directories
  - path: ../ops    # mapping form: extra registry files for this repo
    registries: [data/domains.json]

Then index and query:

specscore studio index                          # rebuild the store from ./studio.yaml
specscore studio index --workspace path/to/studio.yaml --strict   # any warning exits 3
specscore studio facts --predicate has-status   # table of matching facts
specscore studio facts --subject 'repo-a#*' --format json         # full fact shape
specscore studio facts --predicate imports --count                # row count only

studio index rebuilds <workspace-dir>/.specscore-studio/facts.db from scratch on every run (override with --db) and prints a summary with per-repo and per-adapter fact counts plus every warning. Broken repos, adapters, or files are skipped at the smallest granularity and reported as warnings; the exit code stays 0 unless --strict is set.

Every run also exports the facts as INGR recordsets — the same encoding as the committed codegraph/ snapshots — one directory per repo slug under <workspace-dir>/.specscore-studio/ingr/<repo-slug>/facts.ingr (override the root with --ingr-dir, skip with --no-ingr). Each recordset starts with a fixed header naming the nine fact fields (subject, predicate, object, evidence_class, evidence_pointer, adapter_id, adapter_version, observed_at, ecosystem), followed by one JSON value per line (nine lines per record) and a # <n> records trailer; the per-repo record count always equals that repo's fact count in the index summary. Full contract: spec/features/cli/studio/index/.

Rehearse — executable acceptance scenarios

specscore rehearse run <paths...> executes markdown acceptance scenarios — files carrying **Verifies:** AC identity in body metadata plus fenced executable step blocks — and reports per-scenario, per-AC pass/fail:

specscore rehearse run                                # inside a SpecScore repo: all spec/features/**/_tests/
specscore rehearse run spec/features/cli/studio/index/_tests   # a directory (recursive *.md, excluding README.md)
specscore rehearse run scenario.md --format json      # a single file, machine-readable report

Discovery accepts files, directories, and globs; explicit paths work in any directory — no specscore.yaml required (standalone mode). A scenario's step blocks run in order in one scenario-scoped temp working dir; the first failing step fails the scenario and the remaining steps are skipped-after-failure.

Step-block kinds (v0.3) — one scenario can mix all five:

# Rehearse: checkout applies a discount

**Status:** pending
**Verifies:** shop/checkout#ac:discount-applied

```bash
sqlite3 app.db < seed.sql                # runs via bash -euo pipefail
echo "uid=42" >> "$REHEARSE_CAPTURES"    # capture into the context bag
```

```hurl
POST http://127.0.0.1:8080/checkout
{"user": {{uid}}}
HTTP 200
[Captures]
order_id: jsonpath "$.id"
```

```sql dsn=sqlite:app.db
SELECT username FROM orders WHERE id = {{order_id}};
-- assert-rows: 1
-- capture: name = username
```

```tugql db=.specscore-studio/facts.db
from:
  name: facts
-- assert-rows: 128
```

```graphql url=http://127.0.0.1:8080/graphql
query { order(id: {{order_id}}) { ok } }
-- variables: {}
-- assert-jsonpath: $.data.order.ok == true
```

hurl and graphql blocks delegate to the hurl binary (hurl --test) — the runner ships no HTTP client of its own. When hurl is missing from PATH, scenarios containing hurl-derived blocks are reported skipped (with a warning naming the binary) rather than failed, and skips never affect the exit code. sql runs against a DSN (v0.3 driver: sqlite:<path>); tugql runs a dalgo TugQL query document against a SQLite store — which makes a Studio fact store (facts.db) directly assertable by scenarios.

Directives are trailing -- name: value comment lines inside declarative blocks: -- assert-rows: <N> and -- assert-row-json: {...} (sql/tugql), -- capture: <name> = <column> (sql/tugql), -- variables: {...} and -- assert-jsonpath: <path> == <json-value> (graphql), -- capture-jsonpath: <name> = <path> (graphql).

Context bag. Each scenario owns an ordered map of string variables shared across its steps. Consumption is per block class: bash/sql/tugql bodies and info-string params have {{name}} placeholders textually interpolated before execution (an unknown variable fails the step); hurl-derived blocks (hurl, graphql) get the bag as --variable name=value flags instead — Hurl owns the {{name}} syntax natively, so multi-request hurl blocks stay verbatim-valid. Captures into the bag: bash appends name=value lines to $REHEARSE_CAPTURES; hurl uses native [Captures]; sql/tugql/graphql use the capture directives above.

Reporting & exit codes. The human report is one line per scenario (status, file, Verifies: AC ids, duration) plus totals; --format json emits [{file, status, verifies[], duration_ms, bag{}, steps[{kind, status, detail}]}]. Exit 0 when no scenario failed, 1 when any failed, 2 on usage/config errors — including when discovery matches zero scenario files.

The runner is self-hosting: the committed acceptance corpus under spec/features/**/_tests/ (including the Rehearse feature's own scenarios) runs green through specscore rehearse run, and CI executes it on every change (the Rehearse corpus job in go-ci.yml). Full contract: spec/features/cli/rehearse/run/.

Updating

Bring an existing install to the latest release:

specscore self-update            # or: specscore update
specscore self-update --check    # report whether a newer release is available, and the next step; change nothing
specscore self-update --yes      # skip the confirmation prompt (non-interactive / CI)
specscore self-update --dry-run  # show what would happen (target version, download URL) without changing anything

self-update detects how specscore was installed. Package-managed installs (Homebrew, Scoop, WinGet) are never overwritten directly by specscore — instead, self-update runs that manager's own upgrade command for you (confirming first, unless --yes is given) and verifies the result; it never downloads or writes the managed binary itself. Manual installs (release-archive download, go install) are replaced in place after the downloaded asset's checksums.txt entry is verified. On macOS, a Homebrew cask install runs brew upgrade --cask specscore; the source-build channel above upgrades in place like any other manual install.

The detection, download, verification, and manager-invocation logic all live in the shared github.com/strongo/cli-helpers/selfupdate module — specscore only supplies its own release identity (binary name, repository, managers) and exit-code contract. See spec/features/cli/self-update/ for what's specscore's own versus inherited from the library.

Install a specific release instead of the latest:

specscore self-update --version v0.6.0                    # leading "v" optional
specscore self-update --version 0.4.0 --allow-downgrade   # an older target requires --allow-downgrade

--check exit codes: 0 up to date, 10 update available, other non-zero on error — convenient for CI staleness gates. Full contract: spec/features/cli/self-update/.

Installing and upgrading sibling fleet CLIs

specscore install lists and installs the other fleet CLIs (wb, ingitdb, synchestra, chatwright, codegrapher) relevant to specscore, the same way self-update installs specscore itself:

specscore install                # list fleet CLIs relevant to specscore, with live status
specscore install wb ingitdb     # install named ones, after one confirmation

specscore upgrade is the fleet-wide counterpart to self-update: it reports and upgrades every installed catalog CLI, including specscore itself.

specscore upgrade                # report every installed catalog CLI plus specscore; changes nothing
specscore upgrade --all          # upgrade every installed catalog CLI plus specscore
specscore upgrade --all --check  # report upgrade availability only; changes nothing

specscore self-update is exactly specscore upgrade specscore: both are built from the same release identity and reach the same library call, so they never disagree. Full contract: spec/features/cli/install/.

Configure your AI agents

Teach the AI coding agents working in this repo about its SpecScore conventions in one command. specscore agent setup writes each agent's instruction file (and, where the agent supports a skills directory, copies the SpecScore skill bundles into it).

Pass agents as a comma-separated list (space-separated and --all also work):

specscore agent setup claude,codex,copilot,cursor,antigravity.google,pi.dev,opencode,deepseek

Supported agents: claude, codex, copilot, cursor, antigravity.google, pi.dev, opencode. The command is idempotent (existing files are skipped unless --force) and reports every path it adds, modifies, or skips. Full contract: spec/features/cli/agent/setup/.

AI skills

If you drive specscore from inside Claude Code (or any agent host that loads Claude Code plugins), install the ai-plugin-specscore plugin. It ships agent skills that wrap each CLI resource group — they teach the agent when to call which command, which flags to pass, and how to interpret exit codes, all grounded in the feature specs in this repo.

/plugin marketplace add specscore/ai-marketplace
/plugin install specscore@specscore

Then bootstrap the CLI itself with /specscore:install, or install manually with the one-liner above.

Code exploration & spec ↔ code linkage

We use Codegrapher for efficient code exploration and bidirectional linkage between specifications and source code. You can browse this repository's code graph online — the directory tree and quick file search are served from the committed snapshot in codegrapher/. Codegrapher indexes the codebase into a queryable knowledge graph of symbols and their relationships, letting AI agents navigate and trace code quickly instead of grepping — and connect SpecScore specs to the code that implements them, and back.

Test coverage

specscore-cli maintains 100% statement coverage across all packages. This is enforced automatically — the CI pipeline and the local pre-push hook both reject any change that drops below 100%.

All contributions are required to maintain 100% coverage. If your change adds or modifies code, include tests that cover every new branch.

Releasing

Important

Do not bump the version manually. There is no version string to edit in source — the version is derived from the git tag at build time, and releases are continuous. The Release workflow runs on every push to main and delegates to the shared strongo/cicd release workflow (pinned to an exact tag in release.yml), which computes the next version from the conventional commits since the last tag, then tags, builds, signs, and publishes the GitHub release in one run.

  • Merge to main — the bump is inferred from the commit types since the last tag: feat: → minor, fix: (or any other releasing type) → patch. chore:, ci:, docs: and refactor: commits cut no tag and publish nothing. specscore is pre-1.0, so a feat!: / BREAKING CHANGE: commit is capped to a minor bump (0.x → 0.(x+1)); it never produces v1.0.0.
  • Push a vX.Y.Z tag — releases exactly that version and skips the bump. This is also how v1.0.0 gets cut, deliberately.

On either path, a red Go CI run for that commit blocks the release, and re-running it green clears the gate. The gate only sees runs that exist, and go-ci.yml has no tag trigger, so only tag commits that are already on main, where the merge produced one. The workflow can also be started by hand on main (Actions → Release → Run workflow, or gh workflow run release.yml), but it takes no inputs: that re-runs the merge-to-main path against the current head of main, so it only serves to retry a failed release run — it cannot pick a bump level or a version. After a release, run specscore self-update to pull the new binary locally.

License

Apache License 2.0 — see LICENSE.

Related

About

SpecScore CLI — lint, query, and scaffold SpecScore specifications.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages