Files
zernie__vigiles/docs/experimental.md
T
zernie d6b1ad06aa fix!: exclude reaches every pass, and the guard stops missing re-spelled commands (#194)
`.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.
2026-09-03 15:56:41 +05:00

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 @experimental TSDoc 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 plain result() that only works with experimental_agent(). The root's name also survives destructuring — const { result } = experimental_agent still 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.

  1. Someone outside this repository uses it, and we know what for. Our own in-repo test does not count: same author, same assumptions.
  2. Nothing about its shape has moved for a full release cycle. No signature, no return type, no option name.
  3. Every gap in the table above is closed, or written down as a permanent limitation with its reason.
  4. 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.