Files
zernie__vigiles/examples/experimental-emit
zernie 7c97e4ed7d refactor(spec)!: one experimental root per feature (#171)
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>
2026-08-23 18:08:17 +05:00
..

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 required fields — measured separately, and the reason experimental_parseEmitted re-validates on the receiving side.