The prefix answers "is this name stable?" and cannot answer "is the name my stable symbol depends on stable?". That gap shipped: railway(), delegate() and result() carried stable names while being meaningless without the experimental builder, and the lint rule cannot see it — it compares tag and name within one declaration, not the dependency between two. So the subagent vocabulary now hangs off one marked root: result, railway, delegate, pipe, pipeStep, needs, start and andThen become members of the builder. Eight names leave the top level. Two further gains the prefix alone could not reach: collision-prone words stop owning package-level names, and the warning survives destructuring, since the binding site still names the root — unlike a namespace object or an import subpath, which mark the import line. Object.assign rather than a TypeScript namespace: namespaces are an error in this repo's lint config, and one merges only with function declarations while the skill builder is a const. Types stay top-level, for the same reason the prefix rule excludes them. No deprecation window: nothing outside this repository imports these, and a window subtracts nothing. The container-rollback argument for a window does not reach them — they take no part in loading hooks. Found along the way, and worth more than the refactor: two example specs had not compiled since the previous rename, because the examples directory was in no tsconfig at all. Both fixed, and the class is closed — examples now type-check. Gates run, in order: vitest (3281 passed), lint (0 errors), prettier --check, api:check, build, exports:check, internal:check, docs:check, and tsc over the types project now that it includes examples. Claude-Session: https://claude.ai/code/session_017aUJEoaEQ3DxtvoSVWAWqe Co-authored-by: Claude <noreply@anthropic.com>
experimental-emit — can an UNFORKED skill emit a structured result?
The runnable half of the experimental_emitTool emit channel
(src/experimental-emit.ts). Read that module's header first: it lists, by
number, what is unproven and what would have to be true to drop the
experimental_ prefix.
The question
A typed output contract compiles only on a skill with context: "fork" —
compileSkill hard-errors output-without-fork on every other one, because an
inline skill is spliced into the conversation, has no call→return boundary, and
so has no return value to type.
A tool call needs no return boundary. The skill does not return the
structure; it emits it, mid-conversation, and the call lands in
Trace.toolCalls. So the objection that grounds the exclusion does not apply to
this delivery.
What is here
| file | 💵 | what it is |
|---|---|---|
emit-server.mjs |
$0 | ~70-line dependency-free stdio MCP server serving ONE tool. The tool definition is READ from JSON produced by experimental_emitTool — never hand-copied, or the measurement would be testing the copy. |
run-emit.mjs |
💵 paid | The measurement. Takes a real skill directory, swaps exactly one section for the emit instruction, runs it through the real CLI, reads the result back out of ctx.toolCalls. |
records/records-emit.jsonl |
— | One line per trial, appended the moment it is measured. |
records/emitted.jsonl |
— | The same calls as the SERVER saw them — an independent witness to Trace.toolCalls. |
records/emit-tool.json |
— | The tool definition the server actually served. |
Run it
npm run build:core
# $0 — print the patched skill and stop
VIGILES_EMIT_DRY=1 node examples/experimental-emit/run-emit.mjs <skill-dir>
# 💵 real model, real money; the third argument is a hard cost cap
node examples/experimental-emit/run-emit.mjs <skill-dir> 3 0.60
<skill-dir> is any directory holding a SKILL.md without context: fork —
the runner refuses a forked one, since a forked skill can already carry an
output: contract and is not the question. Nothing from the skill is written
back into this repository: the copy is built into a throwaway fixture at run
time, so a private corpus stays private.
The file is deliberately not named *.eval.mjs. That suffix makes a file
discoverable by vigiles eval --all, and a paid run must never be reachable from
a CI sweep.
The one edit made to the skill
Exactly one section is replaced: ## Record the verdict — the block that today
shells out to a ledger script — becomes
experimental_emitTool(contract).instruction. Everything else, frontmatter
included, is byte-identical to the shipped skill. That substitution is the whole
prototype: the same verdict, carried by a tool call instead of a shell command.
What this does NOT measure
- Triggering. The task names the skill, so skill activation is not being
sampled here. Whether the skill would have fired on its own is a separate
question with its own instrument (
measureTriggerRate). - A rate. The published run is N=3 on one skill and one model. It answers "does it land at all". Three of three bounds the true failure rate at roughly 63% and says nothing more.
- Schema enforcement. The runtime does not reject a call that violates its
own declared
requiredfields — measured separately, and the reasonexperimental_parseEmittedre-validates on the receiving side.