@encore-os/eos-spec
v0.33.2
Published
Portable, config-driven spec-pipeline engine + quality harness. One canonical engine consumed as a library (with a client-safe /pure subpath) by every Encore-OS repo.
Readme
@encore-os/eos-spec
A portable, config-driven spec-pipeline engine + quality harness. It ships two
faces from one package: an agent-friendly CLI (eos-spec) and a library (with a
browser-safe /pure subpath). The package carries no corpus data — it drives any
repo's specs/ tree as described by that repo's specs/config.yaml.
Specs move through a pipeline (stub → clarified → validated → … →
tasks_generated); the engine derives each spec's stage from signals in its
markdown, recommends the next action (blocker-aware), and gates completion claims
against on-disk evidence.
Quickstart — with a coding agent (recommended)
npm install -D @encore-os/eos-spec # or run everything via npx
npx @encore-os/eos-spec setup # prints the onboarding plan
npx @encore-os/eos-spec setup --yes # scaffold layout + AI_GUIDE.md + install spec-pipeline-skills (Claude Code)Then prompt your agent — e.g. "Read AI_GUIDE.md and use eos-spec to create and
drive a spec for ." The generated AI_GUIDE.md teaches the agent the
loop: profile --json first, then next / run, always --json, switching on
machine error codes.
Quickstart — manual
npx @encore-os/eos-spec init --scaffold # specs/config.yaml + templates + per-core dirs
npx @encore-os/eos-spec create APP-01 "First feature" # `app` is the scaffolded default core
npx @encore-os/eos-spec validate --core app
npx @encore-os/eos-spec next --spec APP-01 # the one recommended next actioneos-spec --help lists a worked example for every command.
Machine contract
Every command accepts --json and emits one stable envelope:
{ "ok": true, "data": { "…": "…" } }
{ "ok": false, "data": null, "error": { "code": "SPEC_NOT_FOUND", "message": "…" } }Error codes are a frozen, append-only enum (SPEC_NOT_FOUND, UNKNOWN_CORE,
INVALID_ARGUMENT, SCOPE_REQUIRED, VALIDATION_FAILED, DUPLICATE_SPEC_ID,
SUBPROCESS_ERROR, IO_ERROR, ENGINE_VERSION_MISMATCH, INTERNAL, GATE_HALT) —
switch on error.code, never regex the text. GATE_HALT is run stopping at a
compliance/research governance gate it never clears: a human clears it, then re-runs.
Skills that wrap this CLI pin their supported engine range with the global
--require-engine ">=0.23 <1" flag; a mismatch fails fast with ENGINE_VERSION_MISMATCH.
Entry points
@encore-os/eos-spec— fs-bound main entry: the CLI program plus the corpus API (listSpecs,findSpec,loadConfig, …).@encore-os/eos-spec/pure— fs-free re-exports only; safe to bundle in a browser app. Enforced by a purity test on transitive value imports.@encore-os/eos-spec/embeddings— narrow fs-bound subpath for a corpus-local embeddings build + recall, without dragging in the CLI graph.
Precedent & semantic search
The precedent command searches active + archived specs, registry, and deferred
dashboard for prior art:
precedent --spec <id>— Lookup precedent for one spec (identifier overlap). Returns specs sharing backtick-quoted table identifiers, ranked by document frequency (distinctive shared tables rank higher).precedent --topic <text>— Lookup precedent for a topic keyword (ranked search). Ranks results semantically when vectors + embedder are available (via live query embedding with Ollama); lexical fallback when either is absent. The output carries arankModefield ('semantic'or'lexical') to signal which method ranked the hits. When ranking semantically, archived specs are not in the embedding corpus; status metadata may show'unknown'for semantic hits.
Both modes support --core <core> to scope search to one core. The --all flag
lifts the default cap (15 top matches).
Create gates: precedent block mode
Repos can declare a create_gates block in specs/config.yaml to opt into
block mode for the create command's Tier-2 precedent auto-check:
create_gates:
precedent: block # 'warn' (default) | 'block'
precedent_block_min_score: 12precedent: 'warn'(default) — Surfaces prior art but never blocks. Shows top lexical hits when a spec ID or title has a near-duplicate precedent.precedent: 'block'— Haltscreate(non-zero exit, no scaffold) when the top lexical hit's score clearsprecedent_block_min_score. The--forceflag overrides (logged). Fail-open on config/search errors.precedent_block_min_score— Lexical score floor (searchSpecs'scorefield); default is 12. A bare title match never reaches 12 alone — the floor requires genuine two-signal corroboration (title AND body). Passcreate <id> --forceto force-create despite a high precedent hit.
Exactly one create-precedent gate event is logged per invocation, regardless of whether the check warns or blocks.
Gate triggers: the triggers: block
The classifiers that put a spec in scope for a gate or cap (regulated, sensitive,
UI-bearing) and the compliance switch are declared in specs/config.yaml. Every key
is optional; an absent block or key keeps the engine default shown here:
triggers:
regulated_cores: [cl, pm, hr, rh, gr] # each must be a core declared in `cores:`; [] = none
sensitive_tags: [sensitive, phi, pii, billing, credentialing]
sensitive_fields: [] # frontmatter keys; `phi: true` opts a spec in
sensitive_keywords: true # the HIGH/LOW content heuristic
ui_paths: ['\/(components|pages|wizards)\/', '\.tsx$'] # over each affected_files entry
ui_headings: ['UI\s*(?:\/\s*)?UX\b'] # an h1–h3 heading TITLE (see "UI sources" below)
ui_id_patterns: ['-UX-'] # over the resolved spec id, case-insensitive
ui_ux_dir: true # a spec under specs/<core>/ux/ is a UI source
ui_inference: warn # off | warn ('cap' is reserved and rejected)
compliance: true # D12: only a literal `false` turns compliance gating off- Validation is strict and fail-closed. An unknown key, a key present with no value,
a non-list, a blank entry, an undeclared core, a quoted
"false", or a pattern outside the engine's linear-time regex subset fails every command withVALIDATION_FAILED(exit 2) and lists each issue;doctorreports them as findings instead. An engine that validates the block rejects a key it does not know, so a config that uses a key from a newer engine fails loudly there; engines released before the block existed ignore it entirely (compliance and regulated gating stay on). Pair a config that relies on a key with--require-engine. - Patterns use a safe subset: no lookarounds, backreferences, counted or lazy
repetition, nested repetition, or adjacent quantifiers that can match the same
character (
\s*\/?\s*,a*a*); at most 200 characters and 16 quantifiers. - The keyword scan reads the whole file, frontmatter included, so a
tags: [phi]line keeps a spec keyword-sensitive even whenphiis not insensitive_tags. A tag-free sensitivity definition also setssensitive_keywords: false. compliance: falseturns compliance gating off in one place: the compliance-review stage stops applying,isComplianceGatedisfalseinnext/statusJSON,runnever walks strict, the backfill preempt counts a spec review, and close-out never caps on a missing compliance sign-off. The classifiers themselves do not change.- UI sources. A spec is UI-bearing (the UX cap and the UI-evidence tail apply) when
any of these holds: it declares
ui_bearing: true; anaffected_filesentry matchesui_paths; a heading matchesui_headings, plain or after a section number (## UI/UX,## 5. UI/UX,### 8A) UI / UX); its id matchesui_id_patterns; or it lives underspecs/<core>/ux/whileui_ux_diris on. Only one source is inferred: a body that names a UI path (a non-test.tsxfile, or asrc/or@/path throughcomponents/,pages/orwizards/). It never caps. On a spec that is not UI-bearing and notui-gate-waived, it raises a warn-onlyui-inferred-undeclarednote: declareui_bearing: true, oreos-spec signoff --type ui-gate-waived --spec <ID>. The note appears innext/status(data.triggers,data.triggerNotes,data.triggerNoteCounts),audit(rows[].advisoryand an advisories block) anddoctor(data.advisories). It never changes an action,okor an exit code. - Turning UI sources off.
ui_inference: offskips only the body-path scan, which silences the notes; every other source still makes a spec UI-bearing. Those are switched by their own keys:ui_id_patterns: [],ui_ux_dir: false, or a narrowerui_headingsorui_pathslist (aui_headingsentry matches both the plain and the numbered heading). For a single spec,eos-spec signoff --type ui-gate-waived --spec <ID>lifts the UX cap and the UI-evidence tail; the spec stays UI-bearing. - The per-spec markers (
<!-- sensitive -->,<!-- not-sensitive -->,ui_bearing: true,<!-- ui-gate-waived: -->) are always honored. profile --jsonprojects the resolved block astriggers. These commands read it from the--configfile:next,status,run,proceed,reconcile,funnel,signoff,close-out,backfill,scorecard(with--rtm),readiness-rollup,mission-status,list,audit,complete,doctorandprofile. The rest (ready,validate,walkthrough,precedent,create, …) read it fromspecs/config.yaml, as their core list does. When one of them walks the spec corpus, an invalid block there fails it withVALIDATION_FAILED(exit 2) even when--confignames a valid file;createthen scaffolds nothing.
UI-evidence legs (per layout)
A UI-bearing spec owes UI evidence before it completes. The default legs are docshots, a
browser auto-heal sign-off (heal-verified) and a filmed walkthrough (video-walkthrough,
or walkthrough-waived); ui-gate-waived escapes all of them.
- The legs are layout data, not config:
PipelineSchema.uiEvidencedeclares the legs a layout owes, in the ordernextasks for them, with each leg's actor, reason and invocation. A layout that omits it owesDEFAULT_UI_EVIDENCE_LEGS: the three above, with encoreos-shaped invocations (tests/e2e/<core>/*-docs-shots.spec.ts,npm run docs:shots:check). Both built-in layouts (encore1,builder) omit it. next --specroutes a spec whose tasks are done to its first owed leg (capture-docshots,browser-autoheal,record-walkthrough). A leg the layout does not declare is never emitted. A create-wizard's submit→DB E2E (submit-e2e) is not UI evidence: it keeps its own check andsubmit-e2e-waivedescape, right after the docshots leg (before the first leg when a layout declares no docshots).complete --enforcerefuses completion while a leg the repo's layout declares is owed. If the layout cannot be resolved, it requires all three.- Library:
uiEvidenceMissing(spec, legs?)anduiEvidenceGate(spec, legs?)take the leg ids (default: all three, so one-argument calls are unchanged);ResolvedPipeline.uiEvidenceLegsis a layout's set andrenderUiEvidenceInvocationfills an invocation's${id}and${idLower}.
Not in the engine yet. These are owner-gated follow-ups:
- K1, encoreos adoption. An engine bump that adds the specs the numbered-heading,
-UX-id and ux-dir sources now cap to its UX deferral list, replaces its copied UI heuristic withclassifyUiBearing, derives its regulated-core lists from the engine (profile --jsonorresolveTriggers), and picks its compliance definition (its own intriggers:, or the engine default). - K2, builder adoption.
triggers: { compliance: false }(optionallyui_inference: off), and Spec Studio passing the triggers, the spec flags and the layout'sisComplianceGated, and picking its layout frompipeline.layout. - C7, builder's own UI-evidence legs. Until
BUILDER_PIPELINE_SCHEMA.uiEvidencedeclares them, a builder repo is asked for the default (encoreos-shaped) legs. - Phase 3, full inference capping per repo. Body-path inference stays warn-only
(
ui_inference: capis reserved and rejected) until its gates hold and the owner approves that repo's flip.
Scorecard credit: the verifies: block
scorecard and readiness-rollup give a @verifies backing file credit only when it
declares at least one test that would run. They read the file statically: a generated
skeleton, a generator placeholder, or a file whose every test(/it( is a fixme, a
.skip/.todo, an unconditional self-skip, or sits inside describe.skip( /
test.describe.skip( / test.describe.fixme( earns none. Other commands credit every
backer.
A suite whose only tests a helper declares for it reads as a stub unless the repo names that helper:
verifies:
registrars: [registerOrgScopedRlsSmoke] # default: [] (no registrars)- A call to a listed name that passes an inline function as any argument
(
registerOrgScopedRlsSmoke(table, () => ctx)) counts as a would-run test, under the same skip rules asit/test. Without a function argument (registerX(table, ctx)) it does not count. - Each entry must be a bare JavaScript identifier. Other entries are dropped, as is a
value that is not a list. The callee must also be called bare:
helpers.registerX(andregisterX<T>(are not matched. - Library callers pass the list themselves:
scanVerifies(root, roots, { excludeStubBackers: true, registrars: verifiesSettings(config).registrars }).
Companion skills
The encore-plugins marketplace ships
spec-pipeline-skills — nine Claude Code skills that drive this engine
(setup --yes installs them):
claude plugin marketplace add Encore-OS/encore-plugins
claude plugin install spec-pipeline-skills@encore-plugins --scope userVersioning & release
SemVer. Releases are cut by release-please and published to the public npm registry on tag; the CHANGELOG is generated — behavior changes ride Conventional Commit PR titles. Node ≥ 22, ESM only.
