`experimental_verifyPluginGuards` returned a discriminated union and nothing to print it with, so the one use case it exists for — "point the battery at YOUR hooks" — was a six-line fold over that union at every call site. That is a worse code block than the two-liner it replaces, and `cohesive-feature-delivery` row 9 (output parity) could not be claimed. `experimental_formatPluginGuardReport(report)` renders it as terminal text, and its contract is that the union's honesty survives becoming a string: measured prints `blocks n/7` and one row per battery event not-applicable prints its REASON under a ⊘ heading, and no count unresolved likewise notes printed FIRST and in full A rendered `0/7` beside a hook the battery never reached would reintroduce, one layer above the type system, exactly the false confidence the union was built to make unrepresentable. So it is asserted directly and in both directions: for a sweep that measured nothing the output must match no `n/m` at all, and must carry the sentence saying nothing was measured. Mutating the formatter to print `blocks 0/7` for a not-applicable hook fails that assertion and three others. MANY HOOKS stay readable by grouping the unmeasured half BY REASON — a repo with thirty hooks usually has two or three — and naming at most three hooks per reason before counting the rest. That is the only lossy step, and it is confined to the half whose payload is the reason rather than the hook; the measured half is never collapsed, since those are the hooks you came for. The per-event rows come from a shared `guardrailRow`, so this renderer and `formatGuardrailReport` cannot drift into two vocabularies for the same three outcomes; `formatGuardrailReport`'s output is byte-identical. The prefix is on the name because the report SHAPE is documented as likely to move, and a stable formatter pinned to an unstable shape is a lie. It comes off with the same change that takes it off the sweep. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
7.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. |
experimental_verifyPluginGuards |
2026-09-07 | Days old, with no user outside this repository. The REPORT SHAPE is what is most likely to move — whether not-applicable stays one status or splits by cause, and whether the per-hook counts stay arrays of event ids. The prefix comes off when that shape survives sweeping several real third-party repos unchanged. Details: sweep every hook a repo declares. |
experimental_formatPluginGuardReport |
2026-09-07 | It renders PluginGuardReport, so it inherits that shape's instability exactly — a stable name here would promise more than its only input can. The prefix comes off with the same change that takes it off the sweep. Details: sweep every hook a repo declares. |
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.