Ten launch blockers, all sharing one cause: a first reader gets a false claim or an embarrassing failure in their first ten minutes. Fixes: ruff enabled-state was never read on a real repo; compile was unreachable without node_modules/vigiles (every Python/Rust/Go repo); a hooks-only plugin graded F/0 while its own report listed inlineHooks: 2; skill-resource findings printed a body-relative line number; the README quoted a caveman number its own FINDINGS.md had invalidated; STABILITY.md claimed v21 against 26.0.1; the Action's floating v1 tag was never created; a react's notice() reached nobody. Features: experimental_hookState, so a stateful compiled hook can be tested by backdating the fact it throttles on; the site now leads with the proof (compiled hooks, the disaster battery, 2/7) and its OG card is rendered rather than hand-drawn, with a generator that fails loud if the page fetches anything off file:. Also: this repository's first compiled hook, which is what exposed the notice-delivery defect; a floor under the harness CI job, which was green on zero files; and a smoke test of the PUBLISHED package, since every gate until now ran dist/.
10 KiB
Stability
vigiles's major version is high and still climbing — read that as "still moving fast," not "battle-hardened."
semantic-releasecuts a new major on every breaking API change, and there have been a lot of them. The number is an artifact of how it ships, not a claim of maturity, so this page names no version: the badge on the README is always the current one. What follows is what I try hardest not to break, and what's still in motion.
The steadiest contract is the CLI — the commands you run, their flags, and their exit codes. Most of the churn is in the library API underneath it.
What's stable — depend on it
- The CLI — the verbs (
init,compile,eject,lint,test,eval,audit,generate), their flags, and their exit codes (0clean /1warn /2error). This is the narrowest, steadiest contract and the surface almost everyone touches — including the GitHub Action, which wraps it. - The authoring + testing library entry points:
vigiles/linting— the compiler + reference verification (compileClaude,compileSkill, …).vigiles/spec— the core builders (enforce,guidance,instructionFile,file,cmd,ref,dir,glob,prose,result,delegate,railway). Skill authoring is not on this list — seeexperimental_skillbelow, and neither is subagent authoring — seeexperimental_agent.vigiles(the package root) — the free harness-test + check vocabulary, plusdefineEval, which declares what a*.eval.mjsfile measures.*.eval.*FILE SHAPE IS A CONTRACT and it changed in a major release — an eval file DESCRIBES its eval, it does not run one; see eval files describe their eval for the migration.vigiles/eval— the model-calling measurement API; every runtime export carries apaid_prefix (paid_runEval,paid_measure,paid_measureArms,paid_measureTriggerRate,paid_judge,paid_judged,paid_claudeEvalDriver). Types are not prefixed.vigiles/claude-code,vigiles/codex— the per-harness surfaces.vigiles/adapter— the adapter-authoring kit.
- Compiled output contracts — the
vigiles:sha256integrity header and the emitted markdown/settings shapes a hook or spec compiles to.
A breaking change to any of the above is signalled with a Conventional-Commit
! and reflected in the version.
What's still evolving — pin if you rely on it
- The library API is 0.x. Symbols outside the entry points above (and the
internal modules under
dist/core/…reached by deep import) may change in a minor release. Import from the published subpaths, not deep paths. vigiles/hook(compiled hooks) is exported and usable but not yet frozen. Its long-standing delivery caveat (#34692 — a subagent's tool calls never reachingPreToolUse) is fixed as of Claude Code 2.1.241, measured on a stock install — headlessclaude -p, so interactive sessions are unmeasured — and pinned by a test that goes red if it regresses. A gate is still a strong default rather than an unbypassable wall, because a model can route around a tool entirely.
Experimental — no stability promise
These are kept and worked on, but a launch user never needs them and they may change or be removed without a major bump.
The one rule
If we export it and it isn't stable, its name starts with experimental_.
That is the whole convention. Two supporting clauses:
@experimentalin the TSDoc is how it's declared, and the ESLint rulelocal/experimental-namefails CI if a tagged, exported declaration lacks the prefix — so the tag and the name cannot drift apart. It checks every exported declaration, internal ones included: an internal reader is still a reader who trusts the name.@internalno longer means "unstable". It answers a different question — is this part of the API at all — and it is only correct on something we do not export. An@internalsymbol that appears inapi-surface/*.api.mdis a contradiction, reported by a separate check (npm run internal:check) because that one needs the export graph rather than one file: the exports map ships it, so it is public whatever the tag says.
Why the name and not just a tag: a tag is invisible where it matters. You see a
name at every call site, in every diff, in every review, in autocomplete. Nobody
using experimental_pipe(...) can say they weren't told. We arrived at this the
hard way — skill() shipped under a stable name while its own documentation
opened with "skill() is experimental", and nothing caught it, because the
convention was a habit rather than a check.
Cost of the promise: renaming one of these is not a breaking change and does not get a major bump. That is what "no stability promise" means.
The corollary: one experimental ROOT per feature
The prefix answers "is this name stable?". It does not answer "is the name my
stable symbol depends on stable?" — and that gap shipped: railway(),
delegate() and result() carried stable names while being meaningless without
experimental_agent(). A stable name resting on an unstable one promises more
than the feature can keep, and local/experimental-name cannot see it (it checks
tag-vs-name on one declaration, not the dependency between two).
So the vocabulary of an experimental feature hangs off ONE marked root:
experimental_agent({ … }) // the builder
experimental_agent.result(ok, err) // its outcome contract
experimental_agent.railway({ steps: […] })// the flat orchestrator
experimental_agent.pipe(a, b) // the typed pipeline
The same chokepoint as experimental_skill.input(). Three things it buys, and
the first is the one the prefix alone could not:
- No stable name can rest on an unstable one — there are no top-level names left to rest.
- Collision-prone words stop being global.
result,needs,start,pipesay nothing as package-level exports; on the root they are unambiguous. - The warning survives destructuring.
const { result } = experimental_agentstill names the root at the binding site — unlike a namespace OBJECT or an import subpath, which mark the import line and are out of view by the time anyone reads the call. That is whyvigiles/experimentalwas retired rather than reused here.
TYPES stay top-level: a type annotation is not a call site, which is the same reason the prefix rule excludes them.
Mechanically it is Object.assign(fn, { … }), not a TypeScript namespace —
@typescript-eslint/no-namespace is an error in this repo, and a namespace
merges only with function declarations (experimental_skill is a const).
What's on the list
experimental_skill()invigiles/spec/vigiles/claude-code— skill authoring. Compiles, and its gates are verified, but no real skill corpus has been converted and a compiledSKILL.mdhas never been exercised as an installed skill. Seedocs/skills.md§Status. ⚠️ Known gap: its helper vocabulary —input()andstep()— is used only by skill specs, yet is exported with no tag and no prefix, so nothing warns you. The check enforces "tagged ⇒ named", not "everything that should be tagged is". Treat both as carryingexperimental_skill's promise.experimental_agent()invigiles/spec— subagent authoring. Moved off the stable list on 2026-08-21, and the reason matters because the evidence points the other way: a compiledagents/code-reviewer.mdwas loaded by real Claude Code, dispatched to and read, 100% of trials (2026-06-20) — stronger end-to-end proof thanexperimental_skillhas. What is unsettled is the shape, which is what this marker promises.agentstays as a@deprecatedalias for one major.- Typed-composition combinators in
vigiles/spec—experimental_pipe/experimental_pipeStep/experimental_needs/experimental_start/experimental_andThen, and their helper types (Supplies,Handoff,KnownAgentName). The types stay unprefixed: the convention covers callables, since a type annotation is not a call site. experimental_effect()/ effect-region (parked).- The whole-harness codegen (
generate harness) and capability lattice. - Internal-only research/spike modules (
guards,hook-spec,evolve) — not exported from any entry point. - Every export named
experimental_, wherever it lives. The prefix is the whole signal — there is no quarantined subpath any more.vigiles/experimentalwas deleted 2026-08-21: it marked the IMPORT LINE, which is out of view by the time anyone reads the call, and it split each draft feature across two doors (7 of the 20 experimental names were behind it; the other 13 already sat beside stable ones invigiles/specandvigiles/hook, so the subpath was not even a reliable place to look). The prefix marks every call site instead, and the ESLint rulelocal/experimental-namefails CI when a declaration tagged@experimentalis not named for it. Today that covers the R3 disposable-service tier (experimental_startServices, … — onvigiles: it starts containers and calls no model, so it is not behind the paidvigiles/evaldoor, though it does have real side effects), the emit channel (experimental_emitTool, … — onvigiles), the compiled-hook entry points (vigiles/hook), and the spec buildersexperimental_skill/experimental_agent(vigiles/spec).
How the surface is enforced
The public surface is tracked by API Extractor: a committed report per
entry point under api-surface/*.api.md, checked in CI (npm run api:check),
so an export can't silently appear or change. See
docs/cli.md for the verbs and
docs/README.md for the full doc index.