vigiles 15.2.0 → 15.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -17,7 +17,7 @@
17
17
  * third `malformed` track for a worker that didn't honor its contract (no block,
18
18
  * bad JSON, or a shape that doesn't match the declared schema).
19
19
  */
20
- import type { OutputContract } from "../../core/spec.js";
20
+ import type { OutputContract, OutputFieldType } from "../../core/spec.js";
21
21
  /** The outcome of parsing a worker's result block. */
22
22
  export type ParsedAgentResult<S = Record<string, unknown>, E = Record<string, unknown>> = {
23
23
  readonly kind: "ok";
@@ -29,6 +29,15 @@ export type ParsedAgentResult<S = Record<string, unknown>, E = Record<string, un
29
29
  readonly kind: "malformed";
30
30
  readonly reason: string;
31
31
  };
32
+ /**
33
+ * Validate a parsed object against a contract track; null when it conforms.
34
+ *
35
+ * Exported because the EXPERIMENTAL emit channel (`src/experimental-emit.ts`)
36
+ * validates the SAME `OutputContract` on a different delivery. One contract, two
37
+ * deliveries, one validator — a second copy would drift, and the two rails
38
+ * disagreeing about what satisfies a contract is the worst outcome available.
39
+ */
40
+ export declare function shapeError(obj: Record<string, unknown>, shape: Readonly<Record<string, OutputFieldType>>): string | null;
32
41
  /**
33
42
  * Parse the last `vigiles:ok` / `vigiles:err` block from a worker's output.
34
43
  *
@@ -19,10 +19,15 @@
19
19
  * bad JSON, or a shape that doesn't match the declared schema).
20
20
  */
21
21
  Object.defineProperty(exports, "__esModule", { value: true });
22
+ exports.shapeError = shapeError;
22
23
  exports.parseAgentResult = parseAgentResult;
23
24
  // Capture every vigiles:ok / vigiles:err fenced block; the LAST one is the
24
25
  // worker's final answer (earlier ones may be illustrative in its reasoning).
25
- const BLOCK_RE = /```vigiles:(ok|err)[ \t]*\r?\n([\s\S]*?)```/g;
26
+ // The CLOSING fence must own its line: a ``` inside a JSON string value sits
27
+ // mid-line, so it no longer terminates the block (measured 2026-08-13 — a
28
+ // contract field carrying a code snippet made every such answer `malformed`
29
+ // even though the model's JSON was valid).
30
+ const BLOCK_RE = /^[ \t]*```vigiles:(ok|err)[ \t]*\r?\n([\s\S]*?)\r?\n^[ \t]*```[ \t]*$/gm;
26
31
  /** Does a runtime value match a declared field type? */
27
32
  function fieldMatches(value, type) {
28
33
  switch (type) {
@@ -36,7 +41,14 @@ function fieldMatches(value, type) {
36
41
  return Array.isArray(value) && value.every((v) => typeof v === "string");
37
42
  }
38
43
  }
39
- /** Validate a parsed object against a contract track; null when it conforms. */
44
+ /**
45
+ * Validate a parsed object against a contract track; null when it conforms.
46
+ *
47
+ * Exported because the EXPERIMENTAL emit channel (`src/experimental-emit.ts`)
48
+ * validates the SAME `OutputContract` on a different delivery. One contract, two
49
+ * deliveries, one validator — a second copy would drift, and the two rails
50
+ * disagreeing about what satisfies a contract is the worst outcome available.
51
+ */
40
52
  function shapeError(obj, shape) {
41
53
  for (const [field, type] of Object.entries(shape)) {
42
54
  if (!(field in obj))
package/dist/eval.js CHANGED
@@ -981,7 +981,23 @@ async function executeTrial(spec, arm, trialIndex, runner, cfg) {
981
981
  }
982
982
  // A signal in the captured streams that the model call was rate-limited /
983
983
  // overloaded — worth a backoff + retry rather than counting as a real sample.
984
- const RATE_LIMIT_RE = /rate.?limit|\b429\b|overloaded|too many requests/i;
984
+ //
985
+ // 🔴 The separator is `[ -]?`, NOT `.?`, and that is load-bearing. Claude Code's
986
+ // stream-json emits an INFORMATIONAL `{"type":"rate_limit_event","rate_limit_info":
987
+ // {"status":"allowed",…}}` line on EVERY run (captured verbatim in
988
+ // examples/experimental-emit/records/rate-limit-event.json). `rate.?limit` matched
989
+ // `rate_limit_event`, because `.` matches `_` — so every trial looked rate-limited,
990
+ // every trial was retried `retries + 1` = 4 times, and only the LAST attempt's cost
991
+ // reached `maxCostUsd`. Measured 2026-08-13: 2 trials → 8 model runs, a budget cap
992
+ // of $0.60 crossed at roughly $3, and the run reported one trial. `[ -]?` cannot
993
+ // match `_`, so the telemetry line is no longer a match; a REAL limit still is,
994
+ // because the API reports it as `rate_limit_error` / 429 / "overloaded".
995
+ //
996
+ // Known boundary, stated rather than hidden: a `rate_limit_event` whose `status`
997
+ // is a rejection is no longer a match either. It never was one on its merits — the
998
+ // old pattern fired on the event's NAME regardless of status, so status-aware
999
+ // detection has never existed here.
1000
+ const RATE_LIMIT_RE = /rate[ -]?limit(?:ed)?\b|rate_limit_error|\b429\b|overloaded|too many requests/i;
985
1001
  /** Whether a run's captured output looks like a rate-limit / overload. Pure. */
986
1002
  function isRateLimited(out) {
987
1003
  return RATE_LIMIT_RE.test(`${out.stderr ?? ""}\n${out.stdout}`);
@@ -0,0 +1,163 @@
1
+ /**
2
+ * ⚠️ EXPERIMENTAL — the EMIT delivery for a typed result: the skill CALLS a tool
3
+ * carrying its outcome instead of ENDING its turn with a fenced block.
4
+ *
5
+ * ## Why this exists
6
+ *
7
+ * A typed `output` (an `OutputContract`) is valid only on a forked skill: compile
8
+ * hard-errors `output-without-fork` on the other 31, because an inline skill is
9
+ * spliced into the conversation, has no call→return boundary, and therefore has
10
+ * no return value to type (`research/spec-syntax-and-railway-scope.md`).
11
+ *
12
+ * A TOOL CALL needs no return boundary. The skill does not return the structure —
13
+ * it EMITS it, mid-conversation, and the call lands in `Trace.toolCalls`. So the
14
+ * objection that grounds the exclusion does not apply to this delivery. Same
15
+ * `OutputContract`; a different way of getting it out.
16
+ *
17
+ * ## What is UNPROVEN (why the `experimental_` prefix is on every runtime export)
18
+ *
19
+ * 1. **N=8, one skill, one model.** Measured 2026-08-13 against `paper-status`
20
+ * (unforked; `allowed-tools: Bash, Read, Grep, Glob`) on sonnet: 8 runs, 8
21
+ * emits, all on the `ok` track, all parsing against the contract, none
22
+ * repeated. That answers "does it land at all" and nothing about the rate —
23
+ * 8 of 8 bounds the true failure rate at about 31%, which is not evidence of
24
+ * reliability. Raw arguments + the free re-scorer:
25
+ * `examples/experimental-emit/`.
26
+ * 2. **Nobody depends on it.** Neither `compileSkill` nor `compileAgent` emits
27
+ * this instruction; the caller pastes `.instruction` into a skill body and
28
+ * serves `.tool` from their own MCP server by hand. There is no compile-time
29
+ * path, so no skill in any corpus is typed by it yet.
30
+ * 3. **The transport is not part of the contract.** MCP is how the tool reached
31
+ * the model in the measurement. Whether a plugin can hand the model a tool
32
+ * WITHOUT a separate server process is untested, and the answer changes what
33
+ * this surface should look like.
34
+ * 4. **The runtime does not enforce `required`.** Measured: Claude Code accepted a
35
+ * call omitting three declared-required fields (raw proof in `mine`,
36
+ * `vigiles/repro/output-contract-2026-08-13/mcp-arm/schema-probe-emitted.jsonl`).
37
+ * `inputSchema` is DESCRIPTION for the model, not a runtime gate — which is
38
+ * exactly why `experimental_parseEmitted` re-validates on the receiving side
39
+ * and why "the API validates it for you" must not be claimed.
40
+ *
41
+ * ## What would have to be true to drop the prefix
42
+ *
43
+ * - A rate, not an existence proof: ≥30 trials across ≥2 unforked skills and ≥2
44
+ * models, with the emit-landing rate reported and its failure modes named.
45
+ * - One consumer inside vigiles that compiles the instruction from the spec, so
46
+ * the tool name and the shape cannot drift apart by hand.
47
+ * - A measured answer to (3) — plugin-served tool vs external MCP server — since
48
+ * an in-process tool would remove the standing-up cost this surface assumes.
49
+ *
50
+ * Until then: not covered by the stability guarantee, may change or be removed
51
+ * without a major bump. See `docs/../STABILITY.md` and `src/experimental.ts`.
52
+ *
53
+ * @experimental
54
+ * @module
55
+ */
56
+ import type { OutputContract } from "./core/spec.js";
57
+ import type { ToolCall } from "./core/harness-driver.js";
58
+ import { type ParsedAgentResult } from "./adapters/claude-code/agent-result.js";
59
+ /** A JSON-Schema fragment for one declared field. */
60
+ export type EmitFieldSchema = {
61
+ readonly type: "string";
62
+ } | {
63
+ readonly type: "number";
64
+ } | {
65
+ readonly type: "boolean";
66
+ } | {
67
+ readonly type: "array";
68
+ readonly items: {
69
+ readonly type: "string";
70
+ };
71
+ };
72
+ /** The `track` discriminator's schema — the only enum this surface emits. */
73
+ export interface EmitTrackSchema {
74
+ readonly type: "string";
75
+ readonly enum: readonly ["ok", "err"];
76
+ }
77
+ /** Anything that can sit under `properties`: a field, the discriminator, a track. */
78
+ export type EmitPropertySchema = EmitFieldSchema | EmitTrackSchema | EmitObjectSchema;
79
+ /** A JSON-Schema object node — one track's payload, or the whole argument. */
80
+ export interface EmitObjectSchema {
81
+ readonly type: "object";
82
+ readonly properties: Readonly<Record<string, EmitPropertySchema>>;
83
+ readonly required: readonly string[];
84
+ readonly additionalProperties: false;
85
+ }
86
+ /** An MCP tool definition, in the shape a `tools/list` response carries. */
87
+ export interface EmitToolDefinition {
88
+ readonly name: string;
89
+ readonly description: string;
90
+ readonly inputSchema: EmitObjectSchema;
91
+ }
92
+ /** What `experimental_emitTool` hands back: the tool, and the prose that asks for it. */
93
+ export interface ExperimentalEmitTool {
94
+ /** Serve this from your MCP server's `tools/list`. */
95
+ readonly tool: EmitToolDefinition;
96
+ /**
97
+ * Markdown fragment for the skill body. The SAME contract rendered for the
98
+ * model — kept next to the schema so the two cannot drift when hand-wired.
99
+ */
100
+ readonly instruction: string;
101
+ }
102
+ /**
103
+ * ⚠️ EXPERIMENTAL. Derive an emit TOOL from an `OutputContract` — the same
104
+ * contract the fork rail renders as a `vigiles:ok` / `vigiles:err` fenced block.
105
+ *
106
+ * const emit = experimental_emitTool(contract);
107
+ * // emit.tool → serve from your MCP server
108
+ * // emit.instruction → paste into the (unforked) skill's body
109
+ *
110
+ * The two tracks are NESTED (`{ track, ok? , err? }`), not flattened into one bag
111
+ * of fields. That is deliberate: a flat union cannot say which fields are required
112
+ * on which track, so "success fields mixed with error fields" would be a
113
+ * well-formed call. Nested, it is not expressible.
114
+ *
115
+ * 🔴 `required` in the returned schema is DESCRIPTION, not enforcement — measured,
116
+ * see the module header (4). Validate what arrives with
117
+ * `experimental_parseEmitted`.
118
+ *
119
+ * @experimental
120
+ */
121
+ export declare function experimental_emitTool(contract: OutputContract, options?: {
122
+ readonly name?: string;
123
+ }): ExperimentalEmitTool;
124
+ /**
125
+ * ⚠️ EXPERIMENTAL. Read the emitted result out of a run's tool calls, validated
126
+ * against the contract. Pure — returns the same `ParsedAgentResult` vocabulary the
127
+ * fenced rail's `parseAgentResult` returns, so an eval `measure` can use it as a
128
+ * metric and the assertion below can wrap it, without one dual-purpose function.
129
+ *
130
+ * Accepts `Trace["toolCalls"]`, `SubagentTrace["toolCalls"]` or an eval
131
+ * `ctx.toolCalls`. Names are matched bare (`emit_result`) or MCP-prefixed
132
+ * (`mcp__<server>__emit_result`).
133
+ *
134
+ * Differs from the fenced rail in ONE deliberate way: **more than one call is
135
+ * `malformed`, not last-one-wins.** The fenced parser takes the LAST block because
136
+ * an earlier block may be illustrative reasoning; a tool call is an action, never
137
+ * illustrative, so "exactly once" is checkable here and is not checkable there.
138
+ *
139
+ * @experimental
140
+ */
141
+ export declare function experimental_parseEmitted(toolCalls: readonly ToolCall[], contract: OutputContract, options?: {
142
+ readonly name?: string;
143
+ }): ParsedAgentResult;
144
+ /**
145
+ * ⚠️ EXPERIMENTAL. Assert the run emitted a SUCCESS result, and return its value —
146
+ * the emit-channel counterpart of `assertAgentOk`, for a skill that has no return
147
+ * value to assert on.
148
+ *
149
+ * Throws on a missing emit, a repeated emit, an error track, or a payload that
150
+ * does not match the contract. The failure message names every tool the run DID
151
+ * call, because "the skill never emitted" and "the skill emitted the wrong shape"
152
+ * are different bugs and the tool list separates them at a glance.
153
+ *
154
+ * The error track is reachable through `experimental_parseEmitted`; a matching
155
+ * `…EmittedErr` is deliberately NOT shipped while the surface is this young —
156
+ * three exports is the whole prototype.
157
+ *
158
+ * @experimental
159
+ */
160
+ export declare function experimental_assertEmittedOk(toolCalls: readonly ToolCall[], contract: OutputContract, options?: {
161
+ readonly name?: string;
162
+ }): Record<string, unknown>;
163
+ //# sourceMappingURL=experimental-emit.d.ts.map
@@ -0,0 +1,260 @@
1
+ "use strict";
2
+ /**
3
+ * ⚠️ EXPERIMENTAL — the EMIT delivery for a typed result: the skill CALLS a tool
4
+ * carrying its outcome instead of ENDING its turn with a fenced block.
5
+ *
6
+ * ## Why this exists
7
+ *
8
+ * A typed `output` (an `OutputContract`) is valid only on a forked skill: compile
9
+ * hard-errors `output-without-fork` on the other 31, because an inline skill is
10
+ * spliced into the conversation, has no call→return boundary, and therefore has
11
+ * no return value to type (`research/spec-syntax-and-railway-scope.md`).
12
+ *
13
+ * A TOOL CALL needs no return boundary. The skill does not return the structure —
14
+ * it EMITS it, mid-conversation, and the call lands in `Trace.toolCalls`. So the
15
+ * objection that grounds the exclusion does not apply to this delivery. Same
16
+ * `OutputContract`; a different way of getting it out.
17
+ *
18
+ * ## What is UNPROVEN (why the `experimental_` prefix is on every runtime export)
19
+ *
20
+ * 1. **N=8, one skill, one model.** Measured 2026-08-13 against `paper-status`
21
+ * (unforked; `allowed-tools: Bash, Read, Grep, Glob`) on sonnet: 8 runs, 8
22
+ * emits, all on the `ok` track, all parsing against the contract, none
23
+ * repeated. That answers "does it land at all" and nothing about the rate —
24
+ * 8 of 8 bounds the true failure rate at about 31%, which is not evidence of
25
+ * reliability. Raw arguments + the free re-scorer:
26
+ * `examples/experimental-emit/`.
27
+ * 2. **Nobody depends on it.** Neither `compileSkill` nor `compileAgent` emits
28
+ * this instruction; the caller pastes `.instruction` into a skill body and
29
+ * serves `.tool` from their own MCP server by hand. There is no compile-time
30
+ * path, so no skill in any corpus is typed by it yet.
31
+ * 3. **The transport is not part of the contract.** MCP is how the tool reached
32
+ * the model in the measurement. Whether a plugin can hand the model a tool
33
+ * WITHOUT a separate server process is untested, and the answer changes what
34
+ * this surface should look like.
35
+ * 4. **The runtime does not enforce `required`.** Measured: Claude Code accepted a
36
+ * call omitting three declared-required fields (raw proof in `mine`,
37
+ * `vigiles/repro/output-contract-2026-08-13/mcp-arm/schema-probe-emitted.jsonl`).
38
+ * `inputSchema` is DESCRIPTION for the model, not a runtime gate — which is
39
+ * exactly why `experimental_parseEmitted` re-validates on the receiving side
40
+ * and why "the API validates it for you" must not be claimed.
41
+ *
42
+ * ## What would have to be true to drop the prefix
43
+ *
44
+ * - A rate, not an existence proof: ≥30 trials across ≥2 unforked skills and ≥2
45
+ * models, with the emit-landing rate reported and its failure modes named.
46
+ * - One consumer inside vigiles that compiles the instruction from the spec, so
47
+ * the tool name and the shape cannot drift apart by hand.
48
+ * - A measured answer to (3) — plugin-served tool vs external MCP server — since
49
+ * an in-process tool would remove the standing-up cost this surface assumes.
50
+ *
51
+ * Until then: not covered by the stability guarantee, may change or be removed
52
+ * without a major bump. See `docs/../STABILITY.md` and `src/experimental.ts`.
53
+ *
54
+ * @experimental
55
+ * @module
56
+ */
57
+ Object.defineProperty(exports, "__esModule", { value: true });
58
+ exports.experimental_emitTool = experimental_emitTool;
59
+ exports.experimental_parseEmitted = experimental_parseEmitted;
60
+ exports.experimental_assertEmittedOk = experimental_assertEmittedOk;
61
+ const agent_result_js_1 = require("./adapters/claude-code/agent-result.js");
62
+ /** The default tool name, when `options.name` is not given. */
63
+ const DEFAULT_EMIT_TOOL = "emit_result";
64
+ function fieldSchema(type) {
65
+ switch (type) {
66
+ case "string":
67
+ return { type: "string" };
68
+ case "number":
69
+ return { type: "number" };
70
+ case "boolean":
71
+ return { type: "boolean" };
72
+ case "string[]":
73
+ return { type: "array", items: { type: "string" } };
74
+ default:
75
+ // 🔴 Unreachable from TypeScript, reachable from JavaScript — and the one
76
+ // shipped example of this API (examples/experimental-emit/run-emit.mjs) is
77
+ // .mjs, so this is the path a real author takes.
78
+ //
79
+ // Without this arm the switch fell through to `undefined`, and every later
80
+ // step read that as a field: `"actions" in properties` is TRUE for an
81
+ // undefined value, so nothing noticed; `JSON.stringify` then DROPPED the
82
+ // key while `required` kept it and `additionalProperties: false` forbade
83
+ // it. The served schema demanded a property it also banned — unsatisfiable,
84
+ // silent, and contradicted by `.instruction`, which still asked the model
85
+ // to send it. Throwing here makes the contradiction impossible to construct
86
+ // instead of merely unlikely.
87
+ throw new TypeError(`experimental_emitTool: unsupported field type ${JSON.stringify(type)}. ` +
88
+ `Supported: "string", "number", "boolean", "string[]". ` +
89
+ `Nested objects and enums are outside this surface's ceiling — flatten the field, ` +
90
+ `or use the fenced fork rail if the shape cannot be flattened.`);
91
+ }
92
+ }
93
+ function trackSchema(shape) {
94
+ const properties = {};
95
+ for (const [field, type] of Object.entries(shape)) {
96
+ properties[field] = fieldSchema(type);
97
+ }
98
+ return {
99
+ type: "object",
100
+ properties,
101
+ required: Object.keys(shape),
102
+ additionalProperties: false,
103
+ };
104
+ }
105
+ /** Render a declared shape the way the fenced contract renders it, for the prose half. */
106
+ function renderShape(shape) {
107
+ const fields = Object.entries(shape)
108
+ .map(([k, t]) => `"${k}": ${t}`)
109
+ .join(", ");
110
+ return fields ? `{ ${fields} }` : "{}";
111
+ }
112
+ /**
113
+ * ⚠️ EXPERIMENTAL. Derive an emit TOOL from an `OutputContract` — the same
114
+ * contract the fork rail renders as a `vigiles:ok` / `vigiles:err` fenced block.
115
+ *
116
+ * const emit = experimental_emitTool(contract);
117
+ * // emit.tool → serve from your MCP server
118
+ * // emit.instruction → paste into the (unforked) skill's body
119
+ *
120
+ * The two tracks are NESTED (`{ track, ok? , err? }`), not flattened into one bag
121
+ * of fields. That is deliberate: a flat union cannot say which fields are required
122
+ * on which track, so "success fields mixed with error fields" would be a
123
+ * well-formed call. Nested, it is not expressible.
124
+ *
125
+ * 🔴 `required` in the returned schema is DESCRIPTION, not enforcement — measured,
126
+ * see the module header (4). Validate what arrives with
127
+ * `experimental_parseEmitted`.
128
+ *
129
+ * @experimental
130
+ */
131
+ function experimental_emitTool(contract, options = {}) {
132
+ const name = options.name ?? DEFAULT_EMIT_TOOL;
133
+ const tool = {
134
+ name,
135
+ description: "Emit this task's structured result. Call this exactly once. Set " +
136
+ '`track` to "ok" and fill `ok` on success, or "err" and fill `err` on ' +
137
+ "failure. Do not call it twice and do not fill both tracks.",
138
+ inputSchema: {
139
+ type: "object",
140
+ properties: {
141
+ track: { type: "string", enum: ["ok", "err"] },
142
+ ok: trackSchema(contract.ok),
143
+ err: trackSchema(contract.err),
144
+ },
145
+ required: ["track"],
146
+ additionalProperties: false,
147
+ },
148
+ };
149
+ const instruction = [
150
+ "## Output contract",
151
+ "",
152
+ `Emit your result by calling the \`${name}\` tool exactly once, at the point`,
153
+ "you have the answer. Do not print it as a code block; the call IS the result.",
154
+ "",
155
+ `On success: \`track: "ok"\`, with \`ok\` =`,
156
+ "",
157
+ "```json",
158
+ renderShape(contract.ok),
159
+ "```",
160
+ "",
161
+ `On failure: \`track: "err"\`, with \`err\` =`,
162
+ "",
163
+ "```json",
164
+ renderShape(contract.err),
165
+ "```",
166
+ ].join("\n");
167
+ return { tool, instruction };
168
+ }
169
+ /** Does this observed tool name refer to `name` (bare, or MCP-prefixed)? */
170
+ function isEmitCall(observed, name) {
171
+ return observed === name || observed.endsWith(`__${name}`);
172
+ }
173
+ /**
174
+ * ⚠️ EXPERIMENTAL. Read the emitted result out of a run's tool calls, validated
175
+ * against the contract. Pure — returns the same `ParsedAgentResult` vocabulary the
176
+ * fenced rail's `parseAgentResult` returns, so an eval `measure` can use it as a
177
+ * metric and the assertion below can wrap it, without one dual-purpose function.
178
+ *
179
+ * Accepts `Trace["toolCalls"]`, `SubagentTrace["toolCalls"]` or an eval
180
+ * `ctx.toolCalls`. Names are matched bare (`emit_result`) or MCP-prefixed
181
+ * (`mcp__<server>__emit_result`).
182
+ *
183
+ * Differs from the fenced rail in ONE deliberate way: **more than one call is
184
+ * `malformed`, not last-one-wins.** The fenced parser takes the LAST block because
185
+ * an earlier block may be illustrative reasoning; a tool call is an action, never
186
+ * illustrative, so "exactly once" is checkable here and is not checkable there.
187
+ *
188
+ * @experimental
189
+ */
190
+ function experimental_parseEmitted(toolCalls, contract, options = {}) {
191
+ const name = options.name ?? DEFAULT_EMIT_TOOL;
192
+ const calls = toolCalls.filter((c) => isEmitCall(c.name, name));
193
+ if (calls.length === 0) {
194
+ return { kind: "malformed", reason: `no \`${name}\` tool call in the run` };
195
+ }
196
+ if (calls.length > 1) {
197
+ return {
198
+ kind: "malformed",
199
+ reason: `\`${name}\` was called ${String(calls.length)} times; the contract is exactly once`,
200
+ };
201
+ }
202
+ const input = calls[0].input;
203
+ if (typeof input !== "object" || input === null || Array.isArray(input)) {
204
+ return {
205
+ kind: "malformed",
206
+ reason: `\`${name}\` call carried no object argument`,
207
+ };
208
+ }
209
+ const args = input;
210
+ const track = args.track;
211
+ if (track !== "ok" && track !== "err") {
212
+ return {
213
+ kind: "malformed",
214
+ reason: `\`${name}\` call has no \`track\` of "ok" or "err"`,
215
+ };
216
+ }
217
+ const payload = args[track];
218
+ if (typeof payload !== "object" ||
219
+ payload === null ||
220
+ Array.isArray(payload)) {
221
+ return {
222
+ kind: "malformed",
223
+ reason: `\`${name}\` call declared track "${track}" but carried no \`${track}\` object`,
224
+ };
225
+ }
226
+ const obj = payload;
227
+ const bad = (0, agent_result_js_1.shapeError)(obj, track === "ok" ? contract.ok : contract.err);
228
+ if (bad)
229
+ return { kind: "malformed", reason: `${track} payload: ${bad}` };
230
+ return track === "ok"
231
+ ? { kind: "ok", value: obj }
232
+ : { kind: "err", error: obj };
233
+ }
234
+ /**
235
+ * ⚠️ EXPERIMENTAL. Assert the run emitted a SUCCESS result, and return its value —
236
+ * the emit-channel counterpart of `assertAgentOk`, for a skill that has no return
237
+ * value to assert on.
238
+ *
239
+ * Throws on a missing emit, a repeated emit, an error track, or a payload that
240
+ * does not match the contract. The failure message names every tool the run DID
241
+ * call, because "the skill never emitted" and "the skill emitted the wrong shape"
242
+ * are different bugs and the tool list separates them at a glance.
243
+ *
244
+ * The error track is reachable through `experimental_parseEmitted`; a matching
245
+ * `…EmittedErr` is deliberately NOT shipped while the surface is this young —
246
+ * three exports is the whole prototype.
247
+ *
248
+ * @experimental
249
+ */
250
+ function experimental_assertEmittedOk(toolCalls, contract, options = {}) {
251
+ const r = experimental_parseEmitted(toolCalls, contract, options);
252
+ if (r.kind === "ok")
253
+ return r.value;
254
+ const why = r.kind === "err"
255
+ ? `it emitted an error result: ${JSON.stringify(r.error)}`
256
+ : r.reason;
257
+ const observed = toolCalls.map((c) => c.name).join(", ") || "none";
258
+ throw new Error(`expected an emitted success result, but ${why} (tools called: ${observed})`);
259
+ }
260
+ //# sourceMappingURL=experimental-emit.js.map
@@ -10,8 +10,14 @@
10
10
  * NOT covered by the stability guarantee (STABILITY.md): the shape may change or
11
11
  * be removed WITHOUT a major-version bump. Do not depend on it in production.
12
12
  *
13
- * Current contents — the R3 disposable-service tier (real side-effect testing;
14
- * see docs/measuring-skills.md § Experimental and src/services.ts).
13
+ * Current contents:
14
+ * - the R3 disposable-service tier (real side-effect testing; see
15
+ * docs/measuring-skills.md § Experimental and src/services.ts);
16
+ * - the EMIT delivery for a typed result (`src/experimental-emit.ts`) — a skill
17
+ * that CALLS a tool with its outcome instead of ending its turn with a fenced
18
+ * block, which is how an UNFORKED skill can carry an `OutputContract` at all.
19
+ * Read that module's header before using it: it lists, by number, what is
20
+ * unproven and what would have to be true to drop the prefix.
15
21
  *
16
22
  * ⚠️ SAFETY: R3 runs a model-driven skill FOR REAL. The disposable container is
17
23
  * the ONLY isolation vigiles provides — it does not confine the skill's filesystem
@@ -24,4 +30,5 @@
24
30
  */
25
31
  export { experimental_startServices, experimental_withServices, type ServiceSpec, type ServiceReady, type ServiceReset, type ServiceHandle, type ServiceSession, type ContainerRuntime, } from "./services.js";
26
32
  export { experimental_dockerRuntime, makeDockerRuntime, type DockerExec, type NetProbe, } from "./services-docker.js";
33
+ export { experimental_emitTool, experimental_parseEmitted, experimental_assertEmittedOk, type EmitFieldSchema, type EmitObjectSchema, type EmitPropertySchema, type EmitTrackSchema, type EmitToolDefinition, type ExperimentalEmitTool, } from "./experimental-emit.js";
27
34
  //# sourceMappingURL=experimental.d.ts.map
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.makeDockerRuntime = exports.experimental_dockerRuntime = exports.experimental_withServices = exports.experimental_startServices = void 0;
3
+ exports.experimental_assertEmittedOk = exports.experimental_parseEmitted = exports.experimental_emitTool = exports.makeDockerRuntime = exports.experimental_dockerRuntime = exports.experimental_withServices = exports.experimental_startServices = void 0;
4
4
  /**
5
5
  * `vigiles/experimental` — ⚠️ EXPERIMENTAL, UNSTABLE public surface.
6
6
  *
@@ -13,8 +13,14 @@ exports.makeDockerRuntime = exports.experimental_dockerRuntime = exports.experim
13
13
  * NOT covered by the stability guarantee (STABILITY.md): the shape may change or
14
14
  * be removed WITHOUT a major-version bump. Do not depend on it in production.
15
15
  *
16
- * Current contents — the R3 disposable-service tier (real side-effect testing;
17
- * see docs/measuring-skills.md § Experimental and src/services.ts).
16
+ * Current contents:
17
+ * - the R3 disposable-service tier (real side-effect testing; see
18
+ * docs/measuring-skills.md § Experimental and src/services.ts);
19
+ * - the EMIT delivery for a typed result (`src/experimental-emit.ts`) — a skill
20
+ * that CALLS a tool with its outcome instead of ending its turn with a fenced
21
+ * block, which is how an UNFORKED skill can carry an `OutputContract` at all.
22
+ * Read that module's header before using it: it lists, by number, what is
23
+ * unproven and what would have to be true to drop the prefix.
18
24
  *
19
25
  * ⚠️ SAFETY: R3 runs a model-driven skill FOR REAL. The disposable container is
20
26
  * the ONLY isolation vigiles provides — it does not confine the skill's filesystem
@@ -31,4 +37,8 @@ Object.defineProperty(exports, "experimental_withServices", { enumerable: true,
31
37
  var services_docker_js_1 = require("./services-docker.js");
32
38
  Object.defineProperty(exports, "experimental_dockerRuntime", { enumerable: true, get: function () { return services_docker_js_1.experimental_dockerRuntime; } });
33
39
  Object.defineProperty(exports, "makeDockerRuntime", { enumerable: true, get: function () { return services_docker_js_1.makeDockerRuntime; } });
40
+ var experimental_emit_js_1 = require("./experimental-emit.js");
41
+ Object.defineProperty(exports, "experimental_emitTool", { enumerable: true, get: function () { return experimental_emit_js_1.experimental_emitTool; } });
42
+ Object.defineProperty(exports, "experimental_parseEmitted", { enumerable: true, get: function () { return experimental_emit_js_1.experimental_parseEmitted; } });
43
+ Object.defineProperty(exports, "experimental_assertEmittedOk", { enumerable: true, get: function () { return experimental_emit_js_1.experimental_assertEmittedOk; } });
34
44
  //# sourceMappingURL=experimental.js.map
@@ -0,0 +1,96 @@
1
+ /** One edit: the file, the exact substring to find, and what replaces it. */
2
+ export type MutationEdit = readonly [
3
+ file: string,
4
+ find: string,
5
+ replace: string
6
+ ];
7
+ /** One planted defect and the assertion that must catch it. */
8
+ export interface MutationCase {
9
+ /** Short id, printed in the report. */
10
+ readonly name: string;
11
+ /** What the mutation takes away, in words — the column a reader scans. */
12
+ readonly disables: string;
13
+ /**
14
+ * The edits that plant it. A LIST, because some defects are only expressible as more than one:
15
+ * removing a guard AND the vocabulary check that would otherwise throw for an unrelated reason.
16
+ * Every edit must match its `find` EXACTLY ONCE, or the case is reported `not-applied`.
17
+ */
18
+ readonly edits: readonly MutationEdit[];
19
+ /** The test file that must go red. Its absence is refused, loudly, before anything is touched. */
20
+ readonly test: string;
21
+ /** A substring the test's complaint must contain, so a kill by a NEIGHBOUR is not counted. */
22
+ readonly expect: string;
23
+ }
24
+ /**
25
+ * - `killed` — the test went red AND printed `expect`. The only outcome that counts as proof.
26
+ * - `wrong-assertion` — red, but not with this case's message: two defects share one assertion and
27
+ * neither is really watched.
28
+ * - `survived` — the test stayed green. The assertion is vacuous, or absent.
29
+ * - `unjudgeable` — the test was ALREADY red before the run, and `expect` never printed, so "red"
30
+ * carries no information about this mutation. Not a pass and not a failure of the assertion.
31
+ * - `not-applied` — the edit did not land: `find` matched zero or several times, or the
32
+ * replacement equalled the original.
33
+ */
34
+ export type MutationVerdict = "killed" | "wrong-assertion" | "survived" | "unjudgeable" | "not-applied";
35
+ /** What one case did. */
36
+ export interface MutationOutcome {
37
+ readonly name: string;
38
+ readonly disables: string;
39
+ readonly verdict: MutationVerdict;
40
+ /** Human-readable specifics — which file, how many matches, whether a retry was needed. */
41
+ readonly detail: string;
42
+ }
43
+ /** What a whole run did. */
44
+ export interface MutationReport {
45
+ readonly outcomes: readonly MutationOutcome[];
46
+ /** Cases with verdict `killed`. */
47
+ readonly killed: number;
48
+ /** Test files that were red BEFORE any mutation, so they can testify about nothing. */
49
+ readonly alreadyRed: readonly string[];
50
+ /**
51
+ * Whether every test that was green before the run is green again after it. `false` means the
52
+ * restore failed and the working tree is not what it was — the one outcome worth interrupting for.
53
+ */
54
+ readonly restored: boolean;
55
+ }
56
+ export interface RunMutationsOptions {
57
+ /** Working directory for the test runs; every path in `edits` and `test` is absolute. */
58
+ readonly cwd: string;
59
+ readonly cases: readonly MutationCase[];
60
+ /**
61
+ * Extra environment for each test run, merged over `process.env`.
62
+ * Use it to hand the test the same variables its normal runner would.
63
+ */
64
+ readonly env?: NodeJS.ProcessEnv;
65
+ }
66
+ /**
67
+ * Plant each case's defect, run the test that owns it, restore, and report what happened.
68
+ *
69
+ * ```ts
70
+ * const report = runMutations({
71
+ * cwd: repoRoot,
72
+ * cases: [{
73
+ * name: "year",
74
+ * disables: "the year comparison",
75
+ * edits: [[checker, "rec.year !== ourYear", "false"]],
76
+ * test: harness,
77
+ * expect: "a wrong year was not reported",
78
+ * }],
79
+ * });
80
+ * console.log(formatMutationReport(report));
81
+ * process.exit(report.killed === report.outcomes.length && report.restored ? 0 : 1);
82
+ * ```
83
+ *
84
+ * 🔴 THIS REWRITES THE FILES NAMED IN `edits` AND RESTORES THEM. The restore runs in a `finally`
85
+ * AND on SIGINT/SIGTERM, because a run killed midway would otherwise leave a neutered checker on
86
+ * disk — and the next run would then measure an already-broken repo and call it healthy. Commit
87
+ * before running. (Do not reach for `git stash` to clear the tree: the stash is repository-global,
88
+ * so a second agent working in another worktree of the same repo will pop your entries.)
89
+ *
90
+ * @throws if `cases` is empty, or if any named test file does not exist — both BEFORE any file is
91
+ * touched, so the message arrives with a clean working tree.
92
+ */
93
+ export declare function runMutations(o: RunMutationsOptions): MutationReport;
94
+ /** Render a report the way the CLI-style runners in this package render theirs. */
95
+ export declare function formatMutationReport(report: MutationReport): string;
96
+ //# sourceMappingURL=mutations.d.ts.map
@@ -0,0 +1,237 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.runMutations = runMutations;
4
+ exports.formatMutationReport = formatMutationReport;
5
+ /**
6
+ * `runMutations` — prove a test can FAIL, by breaking the thing it watches.
7
+ *
8
+ * A green test says the checker passed. It does not say the test would notice if the check were
9
+ * deleted: an assertion can be vacuous, a fixture can be wrong in the same direction as the code,
10
+ * and both look exactly like a pass. The only way to tell is to plant a defect and require the
11
+ * test to go red — with the message that NAMES that defect, not merely with something red.
12
+ *
13
+ * ## Why this is in vigiles rather than in a repo's own scripts
14
+ *
15
+ * It arrived as ten hand-written copies of one driver in a dogfooding repo (1799 lines), and the
16
+ * copies had DRIFTED. Counted against the committed versions on 2026-08-14:
17
+ *
18
+ * - the NO-OP guard (a replacement equal to the original leaves a green test proving nothing)
19
+ * was in 4 of 10;
20
+ * - the RETRY (a non-kill re-run once before it is believed) was in 1 of 10;
21
+ * - the strict rule — killed by its OWN named assertion, not merely by something going red —
22
+ * was in 1 of 10.
23
+ *
24
+ * Every one of those was written AFTER it caught something, in whichever copy happened to catch
25
+ * it, and never travelled to the other nine. Two copies also named a test file that a refactor had
26
+ * deleted; the runner they used exits 0 on a path matching nothing, so those cases reported
27
+ * SURVIVED on every run for three days. That is the argument for one engine: not less code, but
28
+ * one place where each of those becomes impossible again.
29
+ *
30
+ * ## What this is NOT
31
+ *
32
+ * Not Stryker/mutmut/PIT. Those GENERATE mutants from operators (flip a `<`, drop a `return`) over
33
+ * production code and score a suite by kill ratio. Here the mutations are HAND-AUTHORED and each
34
+ * one names the assertion that must catch it, because the subject is usually not general-purpose
35
+ * code — it is a checker, a hook, an instruction file — where "flip an operator" produces mostly
36
+ * unreachable nonsense and a kill ratio measures nothing. The generated-operator approach is the
37
+ * better tool when it applies; reach for it there.
38
+ *
39
+ * ## The contract, in one sentence
40
+ *
41
+ * Plant one defect, run the test that owns it, and require the test to fail with the message that
42
+ * names it — anything else is reported as a finding, never as a pass.
43
+ *
44
+ * @module
45
+ */
46
+ const node_fs_1 = require("node:fs");
47
+ const node_child_process_1 = require("node:child_process");
48
+ const node_path_1 = require("node:path");
49
+ const run_scripts_js_1 = require("./adapters/claude-code/run-scripts.js");
50
+ function runTest(file, o) {
51
+ const argv = (0, run_scripts_js_1.interpreterArgs)(file, (0, run_scripts_js_1.detectNodeCaps)(o.cwd));
52
+ const r = (0, node_child_process_1.spawnSync)("node", argv, {
53
+ cwd: o.cwd,
54
+ encoding: "utf8",
55
+ env: { ...process.env, ...o.env },
56
+ });
57
+ return { failed: r.status !== 0, out: (r.stdout ?? "") + (r.stderr ?? "") };
58
+ }
59
+ /**
60
+ * Plant each case's defect, run the test that owns it, restore, and report what happened.
61
+ *
62
+ * ```ts
63
+ * const report = runMutations({
64
+ * cwd: repoRoot,
65
+ * cases: [{
66
+ * name: "year",
67
+ * disables: "the year comparison",
68
+ * edits: [[checker, "rec.year !== ourYear", "false"]],
69
+ * test: harness,
70
+ * expect: "a wrong year was not reported",
71
+ * }],
72
+ * });
73
+ * console.log(formatMutationReport(report));
74
+ * process.exit(report.killed === report.outcomes.length && report.restored ? 0 : 1);
75
+ * ```
76
+ *
77
+ * 🔴 THIS REWRITES THE FILES NAMED IN `edits` AND RESTORES THEM. The restore runs in a `finally`
78
+ * AND on SIGINT/SIGTERM, because a run killed midway would otherwise leave a neutered checker on
79
+ * disk — and the next run would then measure an already-broken repo and call it healthy. Commit
80
+ * before running. (Do not reach for `git stash` to clear the tree: the stash is repository-global,
81
+ * so a second agent working in another worktree of the same repo will pop your entries.)
82
+ *
83
+ * @throws if `cases` is empty, or if any named test file does not exist — both BEFORE any file is
84
+ * touched, so the message arrives with a clean working tree.
85
+ */
86
+ function runMutations(o) {
87
+ if (o.cases.length === 0) {
88
+ throw new Error("runMutations: no cases. A mutation run with nothing to run reports success, which is the exact claim this API exists to make impossible.");
89
+ }
90
+ const tests = [...new Set(o.cases.map((c) => c.test))];
91
+ // A test path that resolves to nothing is the defect that hid for three days in the corpus this
92
+ // came from: the runner exits 0 on "no files matched", so every case naming it reported SURVIVED
93
+ // and sent the reader hunting for an assertion that was never reached.
94
+ const missing = tests.filter((t) => !(0, node_fs_1.existsSync)(t));
95
+ if (missing.length > 0) {
96
+ throw new Error(`runMutations: test file(s) do not exist, so no case naming them could ever be judged:\n ${missing.join("\n ")}`);
97
+ }
98
+ // Baselined BEFORE anything is touched. Without this, a test that is red on purpose (an open
99
+ // finding it is meant to report) makes every run announce "the restore failed" about a working
100
+ // restore, and makes every case against it look killed.
101
+ const alreadyRed = tests.filter((t) => runTest(t, o).failed);
102
+ const redBefore = new Set(alreadyRed);
103
+ const targets = [...new Set(o.cases.flatMap((c) => c.edits.map(([f]) => f)))];
104
+ const originals = new Map(targets.map((f) => [f, (0, node_fs_1.readFileSync)(f, "utf8")]));
105
+ const restore = () => {
106
+ for (const [f, text] of originals)
107
+ (0, node_fs_1.writeFileSync)(f, text);
108
+ };
109
+ const onSignal = (sig) => {
110
+ restore();
111
+ process.exit(sig === "SIGINT" ? 130 : 143);
112
+ };
113
+ process.on("SIGINT", onSignal);
114
+ process.on("SIGTERM", onSignal);
115
+ const outcomes = [];
116
+ try {
117
+ for (const c of o.cases) {
118
+ const applied = apply(c);
119
+ if (applied) {
120
+ outcomes.push({
121
+ name: c.name,
122
+ disables: c.disables,
123
+ verdict: "not-applied",
124
+ detail: applied,
125
+ });
126
+ restore();
127
+ continue;
128
+ }
129
+ // A non-kill is retried ONCE before it is believed. Observed on a real corpus: a row killed
130
+ // as named came back as a neighbour's assertion in a later full run and reproduced as a clean
131
+ // kill when replayed alone. Reporting a flake as a survivor sends the next reader to rewrite
132
+ // a working assertion, which is worse than one extra run.
133
+ let judged = judge(c, o, redBefore);
134
+ if (judged.verdict !== "killed") {
135
+ const second = judge(c, o, redBefore);
136
+ if (second.verdict === "killed")
137
+ judged = {
138
+ verdict: "killed",
139
+ detail: "killed on retry (the first run was a flake)",
140
+ };
141
+ else
142
+ judged = second;
143
+ }
144
+ outcomes.push({ name: c.name, disables: c.disables, ...judged });
145
+ restore();
146
+ }
147
+ }
148
+ finally {
149
+ restore();
150
+ process.off("SIGINT", onSignal);
151
+ process.off("SIGTERM", onSignal);
152
+ }
153
+ // Only a test that was GREEN before can testify about the restore.
154
+ const restored = tests
155
+ .filter((t) => !redBefore.has(t))
156
+ .every((t) => !runTest(t, o).failed);
157
+ return {
158
+ outcomes,
159
+ killed: outcomes.filter((r) => r.verdict === "killed").length,
160
+ alreadyRed,
161
+ restored,
162
+ };
163
+ }
164
+ /** Plants one case's edits. Returns a reason string when it could not be planted, else null. */
165
+ function apply(c) {
166
+ for (const [file, find, replace] of c.edits) {
167
+ const src = (0, node_fs_1.readFileSync)(file, "utf8");
168
+ const n = src.split(find).length - 1;
169
+ if (n !== 1) {
170
+ return n === 0
171
+ ? `"find" matched nothing in ${(0, node_path_1.basename)(file)} — the source moved under the case`
172
+ : `"find" matched ${String(n)} times in ${(0, node_path_1.basename)(file)}; an edit must be unambiguous`;
173
+ }
174
+ const next = src.replace(find, replace);
175
+ // The mutation-that-does-not-mutate, caught against the BYTES rather than the intent: a
176
+ // replacement equal to the original leaves a green test that reads exactly like a kill.
177
+ if (next === src)
178
+ return `the replacement equals the original in ${(0, node_path_1.basename)(file)}`;
179
+ (0, node_fs_1.writeFileSync)(file, next);
180
+ }
181
+ return null;
182
+ }
183
+ function judge(c, o, redBefore) {
184
+ const { failed, out } = runTest(c.test, o);
185
+ if (!failed)
186
+ return {
187
+ verdict: "survived",
188
+ detail: `${(0, node_path_1.basename)(c.test)} stayed green with the defect planted`,
189
+ };
190
+ if (out.includes(c.expect))
191
+ return { verdict: "killed", detail: `named by "${c.expect}"` };
192
+ // When the test was ALREADY red, "red" carries no information — but the MESSAGE still does,
193
+ // because a runner that aborts at the first failure never reaches a later assertion. Absent it,
194
+ // the two causes are indistinguishable, and calling it a wrong assertion would be a guess.
195
+ return redBefore.has(c.test)
196
+ ? {
197
+ verdict: "unjudgeable",
198
+ detail: `${(0, node_path_1.basename)(c.test)} was red before the run and "${c.expect}" never printed`,
199
+ }
200
+ : {
201
+ verdict: "wrong-assertion",
202
+ detail: `red, but "${c.expect}" never printed — a neighbour's assertion caught it`,
203
+ };
204
+ }
205
+ /** Render a report the way the CLI-style runners in this package render theirs. */
206
+ function formatMutationReport(report) {
207
+ const lines = [];
208
+ if (report.alreadyRed.length > 0) {
209
+ lines.push(`ℹ️ already red before any mutation: ${report.alreadyRed.map((t) => (0, node_path_1.basename)(t)).join(", ")} — excluded from the restore check; a case naming one is reported unjudgeable.`, "");
210
+ }
211
+ const width = Math.max(...report.outcomes.map((r) => r.name.length));
212
+ const mark = {
213
+ killed: "✓ killed",
214
+ "wrong-assertion": "🔴 wrong assertion",
215
+ survived: "🔴 SURVIVED",
216
+ unjudgeable: "🔴 unjudgeable",
217
+ "not-applied": "🔴 not applied",
218
+ };
219
+ for (const r of report.outcomes) {
220
+ lines.push(`${r.name.padEnd(width)} ${mark[r.verdict].padEnd(20)} ${r.disables}`);
221
+ if (r.verdict !== "killed")
222
+ lines.push(`${" ".repeat(width + 2)} ${r.detail}`);
223
+ }
224
+ lines.push("");
225
+ lines.push(report.restored
226
+ ? "restored: every test that was green before the run is green again"
227
+ : "🔴 RESTORE FAILED — a test that was green before the run is red now. The working tree is not what it was.");
228
+ const bad = report.outcomes.length - report.killed;
229
+ lines.push(bad === 0
230
+ ? `✓ all ${String(report.outcomes.length)} mutations killed, each at its own assertion`
231
+ : `🔴 ${String(bad)} of ${String(report.outcomes.length)} not killed as named: ${report.outcomes
232
+ .filter((r) => r.verdict !== "killed")
233
+ .map((r) => r.name)
234
+ .join(", ")}`);
235
+ return lines.join("\n");
236
+ }
237
+ //# sourceMappingURL=mutations.js.map
package/dist/testing.d.ts CHANGED
@@ -11,6 +11,7 @@
11
11
  * `research/adapter-api-design.md`.
12
12
  */
13
13
  export { recordCheck } from "./check-count.js";
14
+ export { runMutations, formatMutationReport, type MutationCase, type MutationEdit, type MutationOutcome, type MutationReport, type MutationVerdict, type RunMutationsOptions, } from "./mutations.js";
14
15
  export { runScript } from "./run-script.js";
15
16
  export type { RunScriptOptions, ScriptRunResult } from "./run-script.js";
16
17
  export { runHook, propertyHook, fileToolEvents } from "./run-hook.js";
package/dist/testing.js CHANGED
@@ -30,7 +30,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
30
30
  for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
31
31
  };
32
32
  Object.defineProperty(exports, "__esModule", { value: true });
33
- exports.formatContainment = exports.compareContainment = exports.skillContract = exports.mustNotInclude = exports.mustInclude = exports.commandsIn = exports.runHarness = exports.runHarnessTest = exports.judge = exports.hookFired = exports.loadHook = exports.stubSkillBody = exports.parseClaudeRun = exports.claudeEvalDriver = exports.formatTriggerRateReport = exports.formatEvalReport = exports.formatCheckReport = exports.checkReportToJUnit = exports.checkPromptDiversity = exports.assertPromptDiversity = exports.assertRates = exports.measureTriggerRate = exports.measureArms = exports.measure = exports.runEval = exports.fileToolEvents = exports.propertyHook = exports.runHook = exports.runScript = exports.recordCheck = void 0;
33
+ exports.formatContainment = exports.compareContainment = exports.skillContract = exports.mustNotInclude = exports.mustInclude = exports.commandsIn = exports.runHarness = exports.runHarnessTest = exports.judge = exports.hookFired = exports.loadHook = exports.stubSkillBody = exports.parseClaudeRun = exports.claudeEvalDriver = exports.formatTriggerRateReport = exports.formatEvalReport = exports.formatCheckReport = exports.checkReportToJUnit = exports.checkPromptDiversity = exports.assertPromptDiversity = exports.assertRates = exports.measureTriggerRate = exports.measureArms = exports.measure = exports.runEval = exports.fileToolEvents = exports.propertyHook = exports.runHook = exports.runScript = exports.formatMutationReport = exports.runMutations = exports.recordCheck = void 0;
34
34
  // --- reporting: how much did this script actually do? ---
35
35
  // `vigiles test` can otherwise see only an exit code, so a file that runs NOTHING
36
36
  // prints the same `✓` as one that ran and passed (measured 2026-08-08 on a file
@@ -39,6 +39,14 @@ exports.formatContainment = exports.compareContainment = exports.skillContract =
39
39
  // vitest's `expect` — so those are visible to the runner too. See check-count.ts.
40
40
  var check_count_js_1 = require("./check-count.js");
41
41
  Object.defineProperty(exports, "recordCheck", { enumerable: true, get: function () { return check_count_js_1.recordCheck; } });
42
+ // --- the tier above the tiers: is a passing test PROVING anything? ---
43
+ // Every tier below reports that a check passed. None can tell a watched assertion
44
+ // from a vacuous one — both print `✓`. `runMutations` plants a defect, runs the
45
+ // test that owns it, and requires the test to fail with the message that NAMES
46
+ // it, so "green" stops being the strongest claim a suite can make about itself.
47
+ var mutations_js_1 = require("./mutations.js");
48
+ Object.defineProperty(exports, "runMutations", { enumerable: true, get: function () { return mutations_js_1.runMutations; } });
49
+ Object.defineProperty(exports, "formatMutationReport", { enumerable: true, get: function () { return mutations_js_1.formatMutationReport; } });
42
50
  // --- unit tier: runScript (the primitive) + runHook (it, plus a decision) ---
43
51
  // `runScript` runs any program and reports what it DID (exit, both streams,
44
52
  // writes, egress). `runHook` is that plus the hook protocol: event to stdin,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vigiles",
3
- "version": "15.2.0",
3
+ "version": "15.3.0",
4
4
  "description": "Audit, test and measure the harness your AI agent runs on — grade your CLAUDE.md / AGENTS.md, skills, subagents and hooks, run them against a scripted model, and measure whether they actually fire.",
5
5
  "keywords": [
6
6
  "claude-code",