`.vigilesrc.json#exclude` was honoured by 3 of 11 discovery walks, and the walks that did honour it disagreed on the dialect: glob's string `ignore` treated bare `bench` and `bench/` as matching nothing, while the minimatch helper accepted them. One `ExcludeSet` is now parsed where `loadConfig()` runs and taken as a REQUIRED parameter by every in-scope walk in cli.ts — the shape that let `findSpecs` go a year without it was optional-with-default, which compiles silently. Closes #192. The disaster battery now generates its own inputs: `experimental_alternateSpellings` (renamed from `equivalentDisasters`) re-spells hook test cases so the shell executes them identically — nine families, 7 seeds to 136 spellings. Five of those came from a normalizer fix: `g\it push --force` runs git but was not matched by `runs("git push")`. That fix then had to be scoped, because CI caught the regression it caused: escapes are resolved for the HEAD and FLAG-shaped operands (what the command IS) and never for a path operand (where it WRITES) — the same bytes are a resolved escape to `sh` and a real Windows UNC path on Windows, and fail-closed keeps the path. Both directions pinned by mutation. Also: `spec-refs` and `duplicate-rules` had a meta and a doc but no row in the canonical rules matrix, and nothing compared either against it. A test now asserts every rule has a matrix ROW (matched on the table row, not a prose mention). BREAKING CHANGE: `findSpecs` changed signature; `orphans` gained a `repoExclude` option; `equivalentDisasters` was renamed to `experimental_alternateSpellings` and now takes a required argument. Any repo with an `exclude` key sees it applied to every pass, so an excluded spec is no longer compiled and an excluded harness/eval script is no longer discovered.
5.0 KiB
What experimental_ means
If a name you import from vigiles starts with experimental_, we ship it but do
not promise to keep it. It can change shape, be renamed, or disappear in any
release, including a patch release. Renaming it is not a breaking change and
does not bump the major version.
Everything so named has a stable alternative. The feature's own docs page says what it is.
Contents
The rule
If we export it and it is not stable, its name starts with experimental_.
Why the name, and not a note in the docs or the changelog: the name is in every call site, every diff, every review and every autocomplete for as long as the thing is unstable. An import line scrolls out of view. A changelog is read once. A doc note is read never.
Two checks keep the rule honest:
- The tag and the name cannot disagree. Every experimental export is declared
once with the
@experimentalTSDoc tag. An ESLint rule (local/experimental-name) fails the build if a tagged export lacks the prefix. - One experimental root per feature. The prefix says whether this name is
stable. It cannot say whether the names it depends on are. So a feature's whole
vocabulary hangs off one prefixed root:
experimental_agent.result(), never a plainresult()that only works withexperimental_agent(). The root's name also survives destructuring —const { result } = experimental_agentstill names the root at the binding site — which an import path does not.
What is experimental today
| Symbol | Since | Why it is not stable yet |
|---|---|---|
experimental_defineHook and its five siblings (experimental_defineFileGate, experimental_definePromptGate, experimental_defineStopGate, experimental_defineInject, experimental_defineReact) |
2026-08-20 | The named-state API (record / state) has one author and no outside user. There is no supported way to seed that state in a test other than runHook. vigiles compile appends a duplicate wiring block when re-run. Details: compiled hooks. |
experimental_alternateSpellings |
2026-09-02 | Days old. Its only user is this repo's own test. The set of rewrite rules and the id format of the generated cases are the parts most likely to change. Details: the disaster battery. |
When the prefix comes off
All four of these must hold. They are about other people on purpose: everything else we could judge ourselves, and we would be generous.
- Someone outside this repository uses it, and we know what for. Our own in-repo test does not count: same author, same assumptions.
- Nothing about its shape has moved for a full release cycle. No signature, no return type, no option name.
- Every gap in the table above is closed, or written down as a permanent limitation with its reason.
- Testing it needs no private knowledge. If the only way to test it is to hard-code a path the runtime computes internally, it is not finished: a user cannot test their own use of it.
Dropping the prefix is a rename, so it ships as a feat! with a major bump.
How we learned this
skill() shipped under a stable name while docs/skills.md opened with
"skill() is experimental". Nothing caught the contradiction, because the rule
was a habit rather than a check.
We also tried a vigiles/experimental import path. It marked the import line,
which is out of view by the time anyone reads the call. It was retired.
See also
- Compiled hooks — the largest experimental surface, and the guide that explains what each entry point does.
- Stability — the stability promise for everything that is not prefixed.