vigiles 18.0.0 → 18.1.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.
|
@@ -39,8 +39,17 @@ function fieldMatches(value, type) {
|
|
|
39
39
|
return typeof value === "boolean";
|
|
40
40
|
case "string[]":
|
|
41
41
|
return Array.isArray(value) && value.every((v) => typeof v === "string");
|
|
42
|
+
default:
|
|
43
|
+
// An enum, declared as a readonly tuple of the permitted literals.
|
|
44
|
+
return typeof value === "string" && type.includes(value);
|
|
42
45
|
}
|
|
43
46
|
}
|
|
47
|
+
/** How a field type reads in a message to a human: `string`, or `"CUT" | "MERGE"`. */
|
|
48
|
+
function typeName(type) {
|
|
49
|
+
return typeof type === "string"
|
|
50
|
+
? type
|
|
51
|
+
: type.map((v) => JSON.stringify(v)).join(" | ");
|
|
52
|
+
}
|
|
44
53
|
/**
|
|
45
54
|
* Validate a parsed object against a contract track; null when it conforms.
|
|
46
55
|
*
|
|
@@ -54,7 +63,9 @@ function shapeError(obj, shape) {
|
|
|
54
63
|
if (!(field in obj))
|
|
55
64
|
return `missing field "${field}"`;
|
|
56
65
|
if (!fieldMatches(obj[field], type)) {
|
|
57
|
-
|
|
66
|
+
// `typeName`, not `type`: an enum interpolated raw renders as `CUT,MERGE,KEEP`,
|
|
67
|
+
// which reads like a value rather than a choice among values.
|
|
68
|
+
return `field "${field}" should be ${typeName(type)}`;
|
|
58
69
|
}
|
|
59
70
|
}
|
|
60
71
|
return null;
|
package/dist/core/compile.js
CHANGED
|
@@ -899,7 +899,9 @@ function renderAgentSections(sections, basePath) {
|
|
|
899
899
|
/** Render a result-contract track shape as a compact `{ "f": type, … }` line. */
|
|
900
900
|
function renderShape(shape) {
|
|
901
901
|
const fields = Object.entries(shape)
|
|
902
|
-
|
|
902
|
+
// An enum renders as the choice itself — `"verdict": "CUT" | "MERGE" | "KEEP"` — so
|
|
903
|
+
// the fenced rail shows a worker the same permitted values the tool schema shows.
|
|
904
|
+
.map(([k, t]) => `"${k}": ${typeof t === "string" ? t : t.map((v) => JSON.stringify(v)).join(" | ")}`)
|
|
903
905
|
.join(", ");
|
|
904
906
|
return fields ? `{ ${fields} }` : "{}";
|
|
905
907
|
}
|
package/dist/core/spec.d.ts
CHANGED
|
@@ -695,8 +695,22 @@ export type OkOf<T> = T extends TypedOutcome<infer Ok, Shape> ? Ok : Shape;
|
|
|
695
695
|
* erased `Shape`, and the value is still a plain `AgentSpec` — backwards-compatible.
|
|
696
696
|
*/
|
|
697
697
|
export declare function agent<const P extends AuthoredPurity | undefined = undefined, V extends ToolVocabulary = OpenToolVocabulary, Ok extends Shape = Shape, Err extends Shape = Shape>(spec: AgentSpecInput<P, V, Ok, Err>): TypedAgentSpec<Ok, Err>;
|
|
698
|
-
/**
|
|
699
|
-
|
|
698
|
+
/**
|
|
699
|
+
* The field types a result contract can declare (kept tiny + dependency-free).
|
|
700
|
+
*
|
|
701
|
+
* The literal-array member is an ENUM: `["CUT", "MERGE", "KEEP"] as const` declares a
|
|
702
|
+
* field whose value must be one of those strings. It is the ONLY extension to this union,
|
|
703
|
+
* and it was added because a measured failure had no other cure: across 14 real payloads
|
|
704
|
+
* a `verdict: "string"` field held 3 mutually incomparable invented categories over 3
|
|
705
|
+
* runs, and vocabulary compliance was 3/19 — 16%. `string` cannot express "one of these",
|
|
706
|
+
* so nothing downstream could notice.
|
|
707
|
+
*
|
|
708
|
+
* Nothing RELATIONAL follows it — no `object[]`, no tuples, no per-element enums.
|
|
709
|
+
* Declaring one throws rather than rendering an unsatisfiable schema, because the body of
|
|
710
|
+
* a result is prose by decision: on those same 14 payloads, 23 scalar values carried
|
|
711
|
+
* every assertion anyone made while 80,981 characters of prose carried none.
|
|
712
|
+
*/
|
|
713
|
+
export type OutputFieldType = "string" | "number" | "boolean" | "string[]" | readonly [string, ...string[]];
|
|
700
714
|
/** A field SHAPE — a record of field-name → field-type, kept in the TYPE so a
|
|
701
715
|
* typed pipeline can cross-reference one agent's `ok` against the next agent's
|
|
702
716
|
* `needs`. The erased runtime form is `Record<string, OutputFieldType>`. */
|
|
@@ -68,8 +68,11 @@ export type EmitFieldSchema = {
|
|
|
68
68
|
readonly items: {
|
|
69
69
|
readonly type: "string";
|
|
70
70
|
};
|
|
71
|
+
} | {
|
|
72
|
+
readonly type: "string";
|
|
73
|
+
readonly enum: readonly string[];
|
|
71
74
|
};
|
|
72
|
-
/** The `track` discriminator's schema — the
|
|
75
|
+
/** The `track` discriminator's schema — the enum this module owns, not the author's. */
|
|
73
76
|
export interface EmitTrackSchema {
|
|
74
77
|
readonly type: "string";
|
|
75
78
|
readonly enum: readonly ["ok", "err"];
|
|
@@ -62,6 +62,11 @@ const agent_result_js_1 = require("./adapters/claude-code/agent-result.js");
|
|
|
62
62
|
/** The default tool name, when `options.name` is not given. */
|
|
63
63
|
const DEFAULT_EMIT_TOOL = "emit_result";
|
|
64
64
|
function fieldSchema(type) {
|
|
65
|
+
// An enum reaches the model as a JSON-Schema `enum`, which is the whole point: the
|
|
66
|
+
// permitted values travel WITH the tool definition instead of living in prose the
|
|
67
|
+
// model may or may not have read.
|
|
68
|
+
if (typeof type !== "string")
|
|
69
|
+
return { type: "string", enum: [...type] };
|
|
65
70
|
switch (type) {
|
|
66
71
|
case "string":
|
|
67
72
|
return { type: "string" };
|
|
@@ -189,8 +194,41 @@ function isEmitCall(observed, name) {
|
|
|
189
194
|
*/
|
|
190
195
|
function experimental_parseEmitted(toolCalls, contract, options = {}) {
|
|
191
196
|
const name = options.name ?? DEFAULT_EMIT_TOOL;
|
|
192
|
-
const
|
|
197
|
+
const all = toolCalls.filter((c) => isEmitCall(c.name, name));
|
|
198
|
+
// 🔴 A CALL THAT ERRORED IS NOT AN EMISSION, AND USED TO PARSE AS ONE.
|
|
199
|
+
//
|
|
200
|
+
// Measured 2026-08-19: a permission-denied call carrying a perfectly valid payload
|
|
201
|
+
// returned `{"kind":"ok", …}`, because this function filtered by NAME and never looked
|
|
202
|
+
// at `ToolCall.isError` — a field that has been on the type all along. The call never
|
|
203
|
+
// reached the server; the reader reported success. That is the exact shape of defect
|
|
204
|
+
// this channel exists to remove from the fenced rail, reproduced inside the channel.
|
|
205
|
+
//
|
|
206
|
+
// Denial is not hypothetical: it is what a wrong `allowedTools` spelling produces, and
|
|
207
|
+
// MCP tool names mangle per host (`mcp__plugin_<plugin>_<server>__emit_result` on Claude
|
|
208
|
+
// Code, two segments on Codex), so mis-spelling it is the likely case, not the exotic one.
|
|
209
|
+
//
|
|
210
|
+
// The errored calls get their OWN branch rather than being dropped or counted, because
|
|
211
|
+
// all three collapses lie in a different direction:
|
|
212
|
+
// - counting them → this defect, success for a call nobody received;
|
|
213
|
+
// - dropping them silently → "no tool call in the run", which sends the reader to the
|
|
214
|
+
// skill's instructions when the fault is in permissions;
|
|
215
|
+
// - lumping them with "called twice" → a model that retries a denied call produces a
|
|
216
|
+
// true signal under a false name. (Observed: two denials in one run.)
|
|
217
|
+
// A successful call ALONGSIDE a denied one is one successful emission; the denial is
|
|
218
|
+
// mentioned, not fatal, because the contract was in fact satisfied.
|
|
219
|
+
// `isError` is a required boolean on ToolCall, so truthiness is exact here.
|
|
220
|
+
const errored = all.filter((c) => c.isError);
|
|
221
|
+
const calls = all.filter((c) => !c.isError);
|
|
193
222
|
if (calls.length === 0) {
|
|
223
|
+
if (errored.length > 0) {
|
|
224
|
+
return {
|
|
225
|
+
kind: "malformed",
|
|
226
|
+
reason: `the \`${name}\` call itself errored or was denied ` +
|
|
227
|
+
`(${String(errored.length)} attempt${errored.length === 1 ? "" : "s"}); nothing reached the ` +
|
|
228
|
+
`server, so nothing was emitted. Check the tool's permissions and the exact spelling ` +
|
|
229
|
+
`in \`allowedTools\` — MCP names are host-mangled.`,
|
|
230
|
+
};
|
|
231
|
+
}
|
|
194
232
|
return { kind: "malformed", reason: `no \`${name}\` tool call in the run` };
|
|
195
233
|
}
|
|
196
234
|
if (calls.length > 1) {
|
package/dist/scaffold-test.js
CHANGED
|
@@ -118,6 +118,10 @@ assertTriggerRate(report, { min: 0.8, maxFalsePositive: 0.3 });
|
|
|
118
118
|
}
|
|
119
119
|
/** A JSON value placeholder for an `OutputFieldType`, for the `vigiles:ok` block. */
|
|
120
120
|
function placeholderFor(type) {
|
|
121
|
+
// An enum's placeholder must be a MEMBER, or the scaffolded test fails the moment it
|
|
122
|
+
// is run — a generated test that cannot pass teaches the author to distrust the tool.
|
|
123
|
+
if (Array.isArray(type) && type.length > 0)
|
|
124
|
+
return type[0];
|
|
121
125
|
switch (type) {
|
|
122
126
|
case "number":
|
|
123
127
|
return 1;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vigiles",
|
|
3
|
-
"version": "18.
|
|
3
|
+
"version": "18.1.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",
|