vigiles 17.0.2 → 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
- return `field "${field}" should be ${type}`;
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;
@@ -303,12 +303,45 @@ function tested(r) {
303
303
  "your own test setup detected — vigiles-native skill coverage is optional",
304
304
  ]
305
305
  : findings;
306
+ // 🔴 COVERED BY PLACEMENT ALONE — the execution tier, finally said out loud.
307
+ //
308
+ // `colocated` evidence means a test file NAMED after the surface sits BESIDE it. That is
309
+ // a claim about the filesystem, and the provenance line already admits it ("this says the
310
+ // file EXISTS, not that it ran"). Nobody reads a provenance line. When `executed` is zero
311
+ // across the whole corpus, EVERY surface counted here rests on a filename, and the number
312
+ // above reads as health it has not earned.
313
+ //
314
+ // Measured in a real consumer 2026-08-18: 26 colocated, 0 executed — while its CI invoked
315
+ // exactly those harnesses in a job that never installed the `claude` CLI, so each one
316
+ // called `skip()` and the step reported success. Coverage looked fine the entire time.
317
+ //
318
+ // Deliberately NOT "your CI does not run these": that needs parsing CI config, and the
319
+ // grep-shaped version accuses a healthy repo, because real workflows invoke harnesses by
320
+ // GLOB and name no file. This says only what the run records say, so it cannot be wrong
321
+ // about a repo it has not looked at.
322
+ //
323
+ // ⚠️ KNOWN LIMIT, stated rather than hidden: the threshold is CORPUS-WIDE, so ONE recorded
324
+ // run anywhere silences it for every surface. That is what the data supports — the evidence
325
+ // this receives is an aggregate tally, not a per-surface verdict — and the same consumer
326
+ // demonstrates the cost: it read 0 executed / 26 colocated in the morning and 16 / 26 by
327
+ // evening, after which twenty-six surfaces resting on a filename would no longer be named.
328
+ // Sharpening this means carrying evidence per surface, which is a change to the producer,
329
+ // not to this sentence. Until then it catches the state that actually shipped (a corpus
330
+ // where the tier is entirely absent) and stays quiet the moment the tier exists at all.
331
+ const ev = r.coverageEvidence;
332
+ const placementOnly = ev && ev.executed === 0 && ev.colocated > 0
333
+ ? [
334
+ ...contextualized,
335
+ `${String(ev.colocated)} surface(s) counted as covered by PLACEMENT only — ` +
336
+ `no run on record ever exercised one`,
337
+ ]
338
+ : contextualized;
306
339
  return {
307
340
  key: "Tested",
308
341
  score,
309
342
  weight: 1,
310
343
  advisory: true,
311
- findings: contextualized,
344
+ findings: placementOnly,
312
345
  };
313
346
  }
314
347
  /**
package/dist/cli.d.ts CHANGED
@@ -1,12 +1,13 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * vigiles CLI — compile typed specs to instruction files.
3
+ * vigiles CLI — verify your agent harness is real, and prove it works.
4
4
  *
5
- * Commands:
6
- * vigiles init — scaffold a spec from scratch
7
- * vigiles compile compile .spec.ts .md with linter verification
8
- * vigiles lint — verify hashes, report coverage, detect duplicates
9
- * vigiles generate types — emit .d.ts with types from project state
5
+ * The verbs and their one-liners live in ONE place, `COMMAND_HELP` + `HELP_GROUPS`
6
+ * near the bottom of this file, and `--help` prints from that table. A second list
7
+ * here would be a copy that rots this docblock WAS that copy: it named four
8
+ * commands and omitted `audit`, `test`, `eval` and `eject`, four of the eight, and
9
+ * `self-command-refs.test.ts` did not catch it because it guards against refs to
10
+ * REMOVED commands, not against a list that merely stops growing.
10
11
  */
11
12
  export {};
12
13
  //# sourceMappingURL=cli.d.ts.map
package/dist/cli.js CHANGED
@@ -1,13 +1,14 @@
1
1
  #!/usr/bin/env node
2
2
  "use strict";
3
3
  /**
4
- * vigiles CLI — compile typed specs to instruction files.
4
+ * vigiles CLI — verify your agent harness is real, and prove it works.
5
5
  *
6
- * Commands:
7
- * vigiles init — scaffold a spec from scratch
8
- * vigiles compile compile .spec.ts .md with linter verification
9
- * vigiles lint — verify hashes, report coverage, detect duplicates
10
- * vigiles generate types — emit .d.ts with types from project state
6
+ * The verbs and their one-liners live in ONE place, `COMMAND_HELP` + `HELP_GROUPS`
7
+ * near the bottom of this file, and `--help` prints from that table. A second list
8
+ * here would be a copy that rots this docblock WAS that copy: it named four
9
+ * commands and omitted `audit`, `test`, `eval` and `eject`, four of the eight, and
10
+ * `self-command-refs.test.ts` did not catch it because it guards against refs to
11
+ * REMOVED commands, not against a list that merely stops growing.
11
12
  */
12
13
  Object.defineProperty(exports, "__esModule", { value: true });
13
14
  const node_fs_1 = require("node:fs");
@@ -4291,6 +4292,32 @@ async function handleRunScripts(kind, args, restArgs) {
4291
4292
  process.exit(1);
4292
4293
  }
4293
4294
  if (files.length === 0) {
4295
+ // 🔴 ASKING FOR SOMETHING AND GETTING NOTHING IS A FAILURE; FINDING NOTHING IS NOT.
4296
+ // The two cases were collapsed into one silent exit 0, and the collapse cost a real
4297
+ // repository three days of green CI verifying zero files: a named step ran
4298
+ // `vigiles test .claude/pipeline/skills.harness.mjs` after that file had been split
4299
+ // into one-per-skill, printed "No **/*.harness.* files found" and passed, right next
4300
+ // to a step that was red for the same root cause.
4301
+ //
4302
+ // They are different states. A POSITIONAL argument is a claim that something is there —
4303
+ // when nothing matches it, the path is stale, the glob is wrong, or the run never
4304
+ // reached its target, and every one of those is a defect. Bare discovery finding
4305
+ // nothing is just an empty repository, which is a legitimate place to stand and must
4306
+ // stay quiet.
4307
+ //
4308
+ // This is the default the field settled on: Jest and Vitest FAIL on no tests found and
4309
+ // make you opt in with `--passWithNoTests`; pytest exits 5. `--min=0` remains the
4310
+ // explicit opt-out here, so no new flag is introduced by this change.
4311
+ // `minFlag`, not `minRequired`: 0 is both the DEFAULT and the explicit opt-out, so the
4312
+ // VALUE cannot tell them apart — only the flag's presence can. (Caught by a control:
4313
+ // the first version read `minRequired === 0` and made `--min=0` do nothing.)
4314
+ if (restArgs.length > 0 && minFlag === undefined) {
4315
+ console.error(`✗ vigiles ${kind}: ${String(restArgs.length)} target(s) given and NOTHING matched — ` +
4316
+ `${restArgs.join(", ")}\n` +
4317
+ ` Nothing ran. A stale path, a wrong glob, or a moved file all look like this.\n` +
4318
+ ` If an empty match is expected here, say so with --min=0.`);
4319
+ process.exit(1);
4320
+ }
4294
4321
  console.log(`No ${defaultGlob} files found.`);
4295
4322
  return;
4296
4323
  }
@@ -4380,35 +4407,42 @@ function capabilitiesOfReport(report, dialect) {
4380
4407
  }
4381
4408
  const COMMAND_HELP = {
4382
4409
  init: {
4383
- usage: " vigiles init [flags] Setup project (--ci-only for the CI gate only; --lint, --test, --harness=, --strict, --report-only, --no-gha, --force)",
4410
+ usage: " vigiles init [flags] Set up this repo specs, plugin, and CI.",
4384
4411
  },
4385
- compile: { usage: " vigiles compile [files...] Compile .spec.ts → .md" },
4412
+ compile: { usage: " vigiles compile [files...] Compile .spec.ts → .md" },
4386
4413
  eject: {
4387
- usage: " vigiles eject [file] Un-manage a compiled file plain hand-owned markdown (--keep-spec)",
4414
+ usage: " vigiles eject [file] Hand a compiled file back as plain markdown.",
4388
4415
  },
4389
4416
  lint: {
4390
- usage: " vigiles lint [files...] Verify references, find gaps in instruction files",
4417
+ usage: " vigiles lint [files...] Gate it in CI. The same checks — but a finding fails the build.",
4391
4418
  },
4392
4419
  audit: {
4393
- usage: " vigiles audit [dir...] Lighthouse for your harness a LOCAL report: rings + what's broken + fixes (a deterministic read; 2+ dirs → leaderboard)",
4420
+ // "Reports everything, fails nothing" states always-exit-0 as the FEATURE it is. The line
4421
+ // it replaces had to end with "NOT a CI step — use `vigiles lint` in CI", and a help text
4422
+ // that must say what a command ISN'T is a description that failed. Deleting that sentence
4423
+ // was the checkable success criterion for this rewrite.
4424
+ usage: " vigiles audit [dir...] Grade it on your machine. Reports everything, fails nothing.",
4394
4425
  detail: [
4395
- " writes vigiles-report.html + .json (auto-gitignored; --out=<dir> · --no-html/--no-json · --no-open · --json for machine output). NOT a CI step — use `vigiles lint` in CI.",
4396
- " the executing checks (run your hooks · live MCP · do skills fire?) run only interactively — `audit` asks once (remembered); automation uses the vigiles testing API",
4397
- " --serve opens a LIVE local report whose buttons create specs in one click (own repo only; loopback + token-guarded) · --no-serve to skip the prompt",
4426
+ " 2+ dirs → a leaderboard. Writes vigiles-report.html + .json (auto-gitignored).",
4427
+ " The executing checks (run your hooks · live MCP · do skills fire?) run only",
4428
+ " interactively audit asks once and remembers; automation uses the testing API.",
4398
4429
  ],
4399
4430
  },
4400
4431
  test: {
4401
- usage: " vigiles test [files...] Run *.harness.mjs deterministic harness tests",
4432
+ // "Free, no API key" is the CONSEQUENCE; "deterministic" was the mechanism, and a reader
4433
+ // deciding whether to put this in CI needs the cost, not the implementation.
4434
+ usage: " vigiles test [files...] Against a scripted stand-in model. Free, no API key — every commit.",
4402
4435
  },
4403
4436
  eval: {
4404
- usage: " vigiles eval [files...] Run *.eval.mjs real-model harness evals (--trials=N, --min=N, --no-skip)",
4437
+ usage: " vigiles eval [files...] Against a real model. Spends your subscription on demand.",
4405
4438
  detail: [
4406
- " --update records each named eval's result to a committed lock (run locally on your subscription)",
4407
- " --check verifies committed eval results against current inputs WITHOUT a model — the CI staleness gate",
4439
+ " --update records each named eval's result to a committed lock (run it locally).",
4440
+ " --check verifies those committed results against current inputs with NO model —",
4441
+ " the CI-safe half.",
4408
4442
  ],
4409
4443
  },
4410
4444
  generate: {
4411
- usage: " vigiles generate <kind> Emit a dev-toolchain artifact: types (.d.ts) · schema (JSON Schema) · harness (harness.gen.ts)",
4445
+ usage: " vigiles generate <kind> Emit a dev-toolchain artifact: types · schema · harness",
4412
4446
  detail: [
4413
4447
  " vigiles generate <kind> --check Verify the generated file is up to date",
4414
4448
  ],
@@ -4418,19 +4452,39 @@ const COMMAND_HELP = {
4418
4452
  },
4419
4453
  };
4420
4454
  /** Display order of the human-facing verbs in the banner's "Commands:" block. */
4421
- const HELP_ORDER = [
4422
- "init",
4423
- "compile",
4424
- "eject",
4425
- "lint",
4426
- "audit",
4427
- "test",
4428
- "eval",
4455
+ /**
4456
+ * The top-level help, as GROUPS. One table, so the printer cannot drift from the
4457
+ * grouping and a new verb cannot quietly land outside both.
4458
+ */
4459
+ const HELP_GROUPS = [
4460
+ {
4461
+ heading: "Set up and manage your specs:",
4462
+ verbs: ["init", "compile", "eject"],
4463
+ },
4464
+ {
4465
+ heading: "Check your harness (reads your files — nothing is executed):",
4466
+ verbs: ["audit", "lint"],
4467
+ },
4468
+ {
4469
+ heading: "Run your harness (drives it and watches what happens):",
4470
+ verbs: ["test", "eval"],
4471
+ },
4429
4472
  ];
4430
- function printHelpEntry(v) {
4473
+ /**
4474
+ * One command's line. `detail` is the per-flag prose and appears ONLY in
4475
+ * `vigiles <verb> --help`, never in the top-level list.
4476
+ *
4477
+ * That split is the second half of this rewrite. `audit`'s entry used to carry four
4478
+ * wrapped lines naming nine flags inline, and that single entry was most of the felt
4479
+ * crowding in a CLI whose verb count (8) is the smallest of every comparable tool
4480
+ * measured — vitest ships 8 verbs and 164 flags, cargo 48 verbs, git 166. None of them
4481
+ * thinned their surface by removing verbs; they tiered the help. This does the same.
4482
+ */
4483
+ function printHelpEntry(v, opts = {}) {
4431
4484
  console.log(COMMAND_HELP[v].usage);
4432
- for (const line of COMMAND_HELP[v].detail ?? [])
4433
- console.log(line);
4485
+ if (opts.detail)
4486
+ for (const line of COMMAND_HELP[v].detail ?? [])
4487
+ console.log(line);
4434
4488
  }
4435
4489
  /**
4436
4490
  * The loud "there is nothing here to audit" block. Deliberately says WHAT was
@@ -4470,27 +4524,38 @@ function formatNothingToAudit(root, harness, market) {
4470
4524
  }
4471
4525
  /** `vigiles <verb> --help` — that verb's entry plus its complete flag list. */
4472
4526
  function printCommandHelp(command) {
4473
- printHelpEntry(command);
4527
+ printHelpEntry(command, { detail: true });
4474
4528
  const flags = (0, cli_flag_check_js_1.knownFlagsFor)(command);
4475
4529
  console.log("");
4476
4530
  console.log(`Flags: ${[...flags].sort().join(" ")}`);
4477
4531
  console.log("(`vigiles --help` lists every command.)");
4478
4532
  }
4533
+ /**
4534
+ * The top-level help, grouped. The grouping is load-bearing, not cosmetic: four verbs
4535
+ * (`audit`, `lint`, `test`, `eval`) all read as "check my stuff", and a flat list left the
4536
+ * reader to work out the difference from four independent sentences. The headings state the
4537
+ * shared trait, which frees each verb's own line to state only what makes it different, so
4538
+ * the four form a 2x2 that survives one pass:
4539
+ *
4540
+ * no consequence has a consequence
4541
+ * read the files audit (fails nothing) lint (fails the build)
4542
+ * run the harness test (free) eval (spends money)
4543
+ */
4479
4544
  function printUsage(command) {
4480
- console.log("vigiles — compile typed specs to instruction files");
4481
- console.log("");
4482
- console.log("Commands:");
4483
- for (const v of HELP_ORDER)
4484
- printHelpEntry(v);
4485
- console.log("");
4486
- console.log("Examples:");
4487
- console.log(" vigiles init Auto-detect project, create specs, wire CI");
4488
- console.log(" vigiles compile Compile all .spec.ts files");
4489
- console.log(" vigiles lint Verify references, hashes, coverage + suggestions");
4545
+ console.log("vigiles — verify your agent harness is real, and prove it works");
4490
4546
  console.log("");
4547
+ for (const g of HELP_GROUPS) {
4548
+ console.log(g.heading);
4549
+ for (const v of g.verbs)
4550
+ printHelpEntry(v);
4551
+ console.log("");
4552
+ }
4491
4553
  console.log("Plumbing:");
4492
4554
  printHelpEntry("generate");
4493
- console.log(" vigiles --version Print the version number");
4555
+ console.log(" vigiles --version Print the version number");
4556
+ console.log("");
4557
+ console.log("Flags live in `vigiles <command> --help`.");
4558
+ console.log("New here? Start with `vigiles audit .`");
4494
4559
  if (command && command !== "--help") {
4495
4560
  console.log(`\nUnknown command: "${command}"`);
4496
4561
  process.exit(1);
@@ -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
- .map(([k, t]) => `"${k}": ${t}`)
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
  }
@@ -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
- /** The field types a result contract can declare (kept tiny + dependency-free). */
699
- export type OutputFieldType = "string" | "number" | "boolean" | "string[]";
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 only enum this surface emits. */
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 calls = toolCalls.filter((c) => isEmitCall(c.name, name));
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) {
@@ -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": "17.0.2",
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",
@@ -326,6 +326,20 @@ We do **not** show "% of your subscription" — Anthropic doesn't expose a plan'
326
326
  quota, so any percentage would be invented. Tokens + API-equivalent `$` + the
327
327
  billed-to line is the honest, complete picture. Keep the user's cost visible, always.
328
328
 
329
+ ## CI — don't hand-write the steps
330
+
331
+ These tiers belong in CI, and there is a published Action for it. Run `vigiles init`: it
332
+ writes `.github/workflows/vigiles.yml`, wiring the Action (`zernie/vigiles@v1`) for the jobs
333
+ that can use it plus a plain `npx vigiles test` job for this tier — that one needs
334
+ repo-local `node_modules`, which the Action does not install, so it stays hand-rolled on
335
+ purpose.
336
+
337
+ If the repo already has a workflow, the Action's inputs are documented in
338
+ [docs/github-action.md](../../docs/github-action.md). Read them there rather than guessing:
339
+ the input list is defined in `action.yml`, and a copy of it here would be a second source of
340
+ truth that goes stale without anything noticing — which is exactly what happened to this
341
+ file's own sibling docs and to a consumer's CI comment, both measured on 2026-08-18.
342
+
329
343
  ## Step 5 — Lock the eval so CI stays honest (you do this automatically)
330
344
 
331
345
  Real-model evals run on the user's subscription — locally, never in CI. So **as