vigiles 33.1.0 → 33.2.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.
Files changed (50) hide show
  1. package/dist/adapter-conformance.js +14 -10
  2. package/dist/adapter.d.ts +2 -0
  3. package/dist/adapters/claude-code/layout.js +3 -0
  4. package/dist/adapters/claude-code/output-style.d.ts +3 -0
  5. package/dist/adapters/claude-code/output-style.js +81 -0
  6. package/dist/adapters/claude-code/replies.d.ts +14 -0
  7. package/dist/adapters/claude-code/replies.js +78 -0
  8. package/dist/audit-report.d.ts +5 -0
  9. package/dist/audit-report.js +29 -20
  10. package/dist/audit-report.template.html +25 -25
  11. package/dist/audit-score.js +2 -2
  12. package/dist/check.d.ts +20 -0
  13. package/dist/check.js +87 -2
  14. package/dist/cli-main.js +25 -13
  15. package/dist/core/config-schema.d.ts +9 -0
  16. package/dist/core/config-schema.js +1 -0
  17. package/dist/core/eval-driver.d.ts +10 -0
  18. package/dist/core/frontmatter-read.d.ts +31 -0
  19. package/dist/core/frontmatter-read.js +4 -2
  20. package/dist/core/harness-driver.d.ts +2 -0
  21. package/dist/core/layout.d.ts +7 -0
  22. package/dist/core/output-style.d.ts +84 -0
  23. package/dist/core/output-style.js +90 -0
  24. package/dist/core/rule-meta.d.ts +1 -1
  25. package/dist/core/rule-meta.js +8 -0
  26. package/dist/core/types.d.ts +2 -0
  27. package/dist/coverage-evidence.d.ts +42 -0
  28. package/dist/coverage-evidence.js +39 -0
  29. package/dist/eval.d.ts +26 -3
  30. package/dist/eval.js +54 -5
  31. package/dist/harness-test.d.ts +30 -1
  32. package/dist/harness-test.js +65 -15
  33. package/dist/optimize.js +1 -5
  34. package/dist/output-style-arms.d.ts +27 -0
  35. package/dist/output-style-arms.js +46 -0
  36. package/dist/scan-files.js +1 -1
  37. package/dist/scan.d.ts +6 -0
  38. package/dist/scan.js +14 -4
  39. package/dist/score-core.d.ts +3 -0
  40. package/dist/score-core.js +5 -1
  41. package/dist/setup-plan.d.ts +1 -1
  42. package/dist/setup-plan.js +1 -0
  43. package/dist/test-coverage-files.d.ts +3 -1
  44. package/dist/test-coverage-files.js +20 -6
  45. package/dist/test-coverage.d.ts +18 -1
  46. package/dist/test-coverage.js +64 -14
  47. package/dist/test.d.ts +3 -1
  48. package/dist/test.js +8 -2
  49. package/dist/vigilesrc.schema.json +134 -0
  50. package/package.json +1 -1
@@ -12,6 +12,9 @@ exports.matchesSurfaceGlob = matchesSurfaceGlob;
12
12
  exports.countEvidence = countEvidence;
13
13
  exports.formatEvidence = formatEvidence;
14
14
  exports.declaredSurfaceName = declaredSurfaceName;
15
+ exports.outputStyleSurface = outputStyleSurface;
16
+ exports.outputStyleCount = outputStyleCount;
17
+ exports.inventoryCounts = inventoryCounts;
15
18
  /**
16
19
  * HOW a surface was decided to be covered — the provenance of every coverage
17
20
  * decision.
@@ -390,4 +393,40 @@ function declaredSurfaceName(content) {
390
393
  const name = (0, frontmatter_read_js_1.frontmatterScalar)((0, frontmatter_read_js_1.readFrontmatter)(content), "name");
391
394
  return name !== undefined && name.trim() ? name.trim() : null;
392
395
  }
396
+ /**
397
+ * One output style as a coverage surface — shared by the disk detector and the
398
+ * browser twin, so the two name and exempt a style the same way.
399
+ *
400
+ * Named by FILE (`status-block.md` → `status-block`), not by the name a
401
+ * harness selects it by: the surface name is what a colocated test is spelled
402
+ * after, and a declared name may hold spaces or colons no file name should.
403
+ */
404
+ function outputStyleSurface(path, content) {
405
+ return {
406
+ kind: "output-style",
407
+ path,
408
+ name: (0, posix_path_js_1.basename)(path).replace(/\.[^.]+$/, ""),
409
+ tokens: [path],
410
+ ignored: content.includes("vigiles:ignore-test"),
411
+ };
412
+ }
413
+ /**
414
+ * How many output styles a scan found, or that the harness has none. Shared by
415
+ * the disk and browser scan engines, so the inventory reads the one discovery
416
+ * pass both already ran. Exempt styles (`vigiles:ignore-test`) are counted: the
417
+ * marker waives the test, not the style, and a style-only repository with an
418
+ * exempt style is not an empty machine.
419
+ */
420
+ function outputStyleCount(layouts, tier, exempt) {
421
+ if (layouts.every((l) => l.outputStyles === undefined))
422
+ return "not-supported";
423
+ return [...tier.covered, ...tier.untested, ...exempt].filter((s) => s.kind === "output-style").length;
424
+ }
425
+ /** The inventory counts both scan engines report the same way. */
426
+ function inventoryCounts(files, isCommand, layouts, coverage) {
427
+ return {
428
+ commands: Object.keys(files).filter(isCommand).length,
429
+ outputStyles: outputStyleCount(layouts, coverage.harness, coverage.exemptSurfaces),
430
+ };
431
+ }
393
432
  //# sourceMappingURL=coverage-evidence.js.map
package/dist/eval.d.ts CHANGED
@@ -119,7 +119,11 @@ export interface EvalSpec<M extends Metrics> {
119
119
  * the cache, and never read from an env var. Omit for the harness default.
120
120
  */
121
121
  readonly effort?: string | number;
122
- /** Tools the agent may use. Default: Read Edit Write Bash. */
122
+ /**
123
+ * Tools pre-approved for the run (`--allowedTools`, a permission allowlist);
124
+ * the default tools stay available either way. `[]` approves none.
125
+ * Default: Read Edit Write Bash.
126
+ */
123
127
  readonly allowedTools?: readonly string[];
124
128
  /** Per-run timeout ms. Default 240000. */
125
129
  readonly timeoutMs?: number;
@@ -309,6 +313,22 @@ export declare function withEffortGuard(runner: AgentRunner): AgentRunner;
309
313
  * inline there could not be asserted at all. Mirrors `buildCodexArgs`.
310
314
  */
311
315
  export declare function buildAgentArgs(a: AgentRunArgs): string[];
316
+ /**
317
+ * Why a run never reached the model, or null when it did. A run that exited
318
+ * non-zero with no `assistant` event and no `result` event asked the model
319
+ * nothing — a usage error in the command line, a missing binary — and scored as
320
+ * a trial it reads as a miss on every check that needs output and a pass on
321
+ * every bound it cannot break. A run that reached the model and then broke is a
322
+ * scored outcome, not this. Rate-limited runs are left to the retry loop.
323
+ */
324
+ export declare function startFailure(out: Readonly<RunOut>): string | null;
325
+ /**
326
+ * Wrap a runner so a run that never reached the model THROWS instead of being
327
+ * scored — the same choice, for the same reason, as {@link withEffortGuard}:
328
+ * the cause is usually deterministic, so every trial would fail it. Not `async`,
329
+ * for the reason that guard gives.
330
+ */
331
+ export declare function withStartGuard(runner: AgentRunner): AgentRunner;
312
332
  /**
313
333
  * The real `claude`-spawning runner (composition root). Exported so other
314
334
  * real-model entries (e.g. the `audit` trigger tier) bind the same runner.
@@ -391,7 +411,7 @@ export interface MeasureSpec {
391
411
  * the cache, and never read from an env var. Omit for the harness default.
392
412
  */
393
413
  readonly effort?: string | number;
394
- /** Tools the agent may use. */
414
+ /** Tools pre-approved for the run (`--allowedTools`); `[]` approves none. */
395
415
  readonly allowedTools?: readonly string[];
396
416
  /** Per-run timeout ms. */
397
417
  readonly timeoutMs?: number;
@@ -744,7 +764,10 @@ export interface TriggerRateSpec {
744
764
  * false-negative recall. Default `"sonnet"`. Lower it deliberately for a cheap run.
745
765
  */
746
766
  readonly minModel?: string;
747
- /** Tools the agent may use. Default: Read Edit Write Bash Skill. */
767
+ /**
768
+ * Tools pre-approved for the run (`--allowedTools`); `[]` approves none.
769
+ * Default: Read Edit Write Bash Skill.
770
+ */
748
771
  readonly allowedTools?: readonly string[];
749
772
  /** Per-run timeout ms. Default 240000. */
750
773
  readonly timeoutMs?: number;
package/dist/eval.js CHANGED
@@ -6,6 +6,8 @@ exports.pinEffortEnv = pinEffortEnv;
6
6
  exports.effortRejection = effortRejection;
7
7
  exports.withEffortGuard = withEffortGuard;
8
8
  exports.buildAgentArgs = buildAgentArgs;
9
+ exports.startFailure = startFailure;
10
+ exports.withStartGuard = withStartGuard;
9
11
  exports.unregisteredSkillFiles = unregisteredSkillFiles;
10
12
  exports.runEval = runEval;
11
13
  exports.measureWith = measureWith;
@@ -215,10 +217,41 @@ function buildAgentArgs(a) {
215
217
  ? ["--plugin-dir", (0, node_path_1.resolve)(a.pluginDir)]
216
218
  : []),
217
219
  ...(a.hasSettings ? ["--settings", "settings.json"] : []),
218
- "--allowedTools",
219
- ...a.tools,
220
+ // A permission allowlist: an empty one approves nothing, so the flag is
221
+ // left out (a bare `--allowedTools` exits before any model turn).
222
+ ...(a.tools.length === 0 ? [] : ["--allowedTools", ...a.tools]),
220
223
  ];
221
224
  }
225
+ /**
226
+ * Why a run never reached the model, or null when it did. A run that exited
227
+ * non-zero with no `assistant` event and no `result` event asked the model
228
+ * nothing — a usage error in the command line, a missing binary — and scored as
229
+ * a trial it reads as a miss on every check that needs output and a pass on
230
+ * every bound it cannot break. A run that reached the model and then broke is a
231
+ * scored outcome, not this. Rate-limited runs are left to the retry loop.
232
+ */
233
+ function startFailure(out) {
234
+ if (out.code === 0 || (0, harness_test_js_1.parseResultEvent)(out.stdout) !== null)
235
+ return null;
236
+ if ((0, harness_test_js_1.reachedModel)(out.stdout) || isRateLimited(out))
237
+ return null;
238
+ const said = (out.stderr ?? "").trim().split("\n").slice(-5).join("\n");
239
+ return `the harness exited ${String(out.code)} before calling the model: ${said || "(no stderr)"}`;
240
+ }
241
+ /**
242
+ * Wrap a runner so a run that never reached the model THROWS instead of being
243
+ * scored — the same choice, for the same reason, as {@link withEffortGuard}:
244
+ * the cause is usually deterministic, so every trial would fail it. Not `async`,
245
+ * for the reason that guard gives.
246
+ */
247
+ function withStartGuard(runner) {
248
+ return (a) => runner(a).then((out) => {
249
+ const failure = startFailure(out);
250
+ if (failure !== null)
251
+ throw new Error(failure);
252
+ return out;
253
+ });
254
+ }
222
255
  /**
223
256
  * The real `claude`-spawning runner (composition root). Exported so other
224
257
  * real-model entries (e.g. the `audit` trigger tier) bind the same runner.
@@ -228,7 +261,7 @@ function buildAgentArgs(a) {
228
261
  * not yet written, is covered by construction. Guarding each call site instead is
229
262
  * the shape that left four of five compilers unprotected in #173.
230
263
  */
231
- exports.spawnAgent = withEffortGuard(spawnAgentRaw);
264
+ exports.spawnAgent = withEffortGuard(withStartGuard(spawnAgentRaw));
232
265
  /* v8 ignore start -- real claude subprocess; exercised by bench/, not the unit gate */
233
266
  /** The unguarded spawn itself; wrapped by {@link spawnAgent}, never bound raw. */
234
267
  function spawnAgentRaw(a) {
@@ -572,6 +605,7 @@ function parseClaudeRun(out) {
572
605
  toolCalls: (0, harness_test_js_1.parseToolCalls)(out.stdout),
573
606
  hooks: (0, harness_test_js_1.parseHooks)(out.stdout),
574
607
  subagents: (0, harness_test_js_1.parseSubagents)(out.stdout),
608
+ replies: (0, harness_test_js_1.parseReplies)(out.stdout),
575
609
  usage: usageFrom(result),
576
610
  };
577
611
  }
@@ -592,6 +626,7 @@ function makeContext(cwd, out, parse = parseClaudeRun) {
592
626
  hooks: p.hooks,
593
627
  output: p.output,
594
628
  subagents: p.subagents,
629
+ ...(p.replies === undefined ? {} : { replies: p.replies }),
595
630
  usage: p.usage,
596
631
  // The eval tier drives the real API (no mock between the agent and the model),
597
632
  // so the requests can't be captured here — modelRequests is harness-tier only.
@@ -755,7 +790,10 @@ async function runWithCache(runArgs, keyParts, runner, cfg) {
755
790
  trialIndex: keyParts.trialIndex,
756
791
  });
757
792
  const hit = (0, eval_cache_js_1.readCache)(cfg.cacheDir, key);
758
- if (hit) {
793
+ // A run that never reached the model is not replayed: the real runner now
794
+ // throws on one, and an entry cached before that guard existed would
795
+ // otherwise be scored as a trial on every later run.
796
+ if (hit && startFailure(hit.out) === null) {
759
797
  (0, eval_cache_js_1.restoreDir)(runArgs.cwd, hit.files);
760
798
  return hit.out;
761
799
  }
@@ -1122,14 +1160,25 @@ async function runWithRetry(runArgs, runner, retries, baseMs) {
1122
1160
  /** Map `worker` over `items` with at most `concurrency` in flight, order preserved. */
1123
1161
  async function runPool(items, concurrency, worker) {
1124
1162
  const results = new Array(items.length);
1163
+ // A worker that throws rejects the whole pool; the others must stop taking
1164
+ // items then, or they keep starting (and paying for) work nobody will read.
1165
+ const stop = new AbortController();
1125
1166
  let next = 0;
1126
1167
  const drain = async () => {
1127
1168
  for (;;) {
1169
+ if (stop.signal.aborted)
1170
+ return;
1128
1171
  const i = next++;
1129
1172
  const item = items[i];
1130
1173
  if (i >= items.length || item === undefined)
1131
1174
  return;
1132
- results[i] = await worker(item);
1175
+ try {
1176
+ results[i] = await worker(item);
1177
+ }
1178
+ catch (e) {
1179
+ stop.abort();
1180
+ throw e;
1181
+ }
1133
1182
  }
1134
1183
  };
1135
1184
  const workers = Math.max(1, Math.min(concurrency, items.length || 1));
@@ -2,10 +2,14 @@ import type { HarnessAdapter } from "./core/adapter.js";
2
2
  import type { HarnessTestDriver, ToolCall, HookFire, ParsedRun, ModelTurn } from "./core/harness-driver.js";
3
3
  import type { Trace, SubagentTrace } from "./core/eval-driver.js";
4
4
  export type { Trace, SubagentTrace } from "./core/eval-driver.js";
5
+ import { parseReplies, reachedModel } from "./adapters/claude-code/replies.js";
6
+ /** Re-exported for the eval tier's parser, which reads the same stream. */
7
+ export { parseReplies, reachedModel };
5
8
  import { type SandboxMode } from "./sandbox.js";
6
9
  export { scriptModel } from "./mock-model.js";
7
10
  export type { ModelTurn, ModelRequest, ToolCall, HookFire, HarnessTestDriver, } from "./core/harness-driver.js";
8
- export { loadPlugin, resolveHarness, } from "./adapters/claude-code/plugin-loader.js";
11
+ import { loadPlugin, resolveHarness } from "./adapters/claude-code/plugin-loader.js";
12
+ export { loadPlugin, resolveHarness };
9
13
  export { decideSandbox, specTrusted, sandboxAvailable, type SandboxMode, } from "./sandbox.js";
10
14
  export interface HarnessTestSpec {
11
15
  /** Fixture files to write in a fresh temp working dir (path → contents). */
@@ -28,6 +32,21 @@ export interface HarnessTestSpec {
28
32
  * `plugin` still layer on top. Resolved to an absolute path.
29
33
  */
30
34
  readonly pluginDir?: string;
35
+ /**
36
+ * Path to an OUTPUT STYLE file to switch on for this run. The style is
37
+ * written where the harness keeps styles, and selected by the name the
38
+ * harness reads from the file — never by a name you type, because a setting
39
+ * that misses the name loads no style and raises no error.
40
+ *
41
+ * The run is then checked: if no model request carried the style,
42
+ * `runHarnessTest` THROWS instead of returning a trace, so a style that never
43
+ * loaded cannot pass a test. Refused up front on a harness without output
44
+ * styles, or when the fixture's files or settings already decide.
45
+ *
46
+ * What it proves is delivery. A scripted model does not read the style, so
47
+ * nothing in this tier shows that a model FOLLOWS it.
48
+ */
49
+ readonly outputStyle?: string;
31
50
  /** The scripted model turns the agent will take. */
32
51
  readonly model: readonly ModelTurn[];
33
52
  /** The user prompt. Default: "go". */
@@ -156,4 +175,14 @@ export declare function runHarnessTest(spec: HarnessTestSpec, opts?: RunHarnessT
156
175
  export declare function runHarness(spec: HarnessTestSpec, opts?: RunHarnessTestOptions & {
157
176
  model?: "mock" | "real";
158
177
  }): Promise<HarnessTestResult>;
178
+ /**
179
+ * The files and settings a run with `outputStyle` writes — the style at its
180
+ * place, selected by the name the harness reads from it. The same fixture
181
+ * `runHarnessTest({ outputStyle })` runs, so an eval arm built from it gets
182
+ * exactly what that run proved reaches the model.
183
+ */
184
+ export declare function outputStyleFixture(outputStyle: string, adapter?: HarnessAdapter): {
185
+ readonly files: Readonly<Record<string, string>>;
186
+ readonly settings: unknown;
187
+ };
159
188
  //# sourceMappingURL=harness-test.d.ts.map
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.claudeCodeDriver = exports.sandboxAvailable = exports.specTrusted = exports.decideSandbox = exports.resolveHarness = exports.loadPlugin = exports.scriptModel = void 0;
3
+ exports.claudeCodeDriver = exports.sandboxAvailable = exports.specTrusted = exports.decideSandbox = exports.resolveHarness = exports.loadPlugin = exports.scriptModel = exports.reachedModel = exports.parseReplies = void 0;
4
4
  exports.parseToolCalls = parseToolCalls;
5
5
  exports.parseSubagents = parseSubagents;
6
6
  exports.parseResultEvent = parseResultEvent;
@@ -11,6 +11,7 @@ exports.parseClaudeRun = parseClaudeRun;
11
11
  exports.claudeAvailable = claudeAvailable;
12
12
  exports.runHarnessTest = runHarnessTest;
13
13
  exports.runHarness = runHarness;
14
+ exports.outputStyleFixture = outputStyleFixture;
14
15
  /**
15
16
  * vigiles — deterministic Claude Code harness testing.
16
17
  *
@@ -49,13 +50,17 @@ const check_count_js_1 = require("./check-count.js");
49
50
  const coverage_probe_js_1 = require("./coverage-probe.js");
50
51
  const runtime_js_1 = require("./adapters/claude-code/runtime.js");
51
52
  const mock_model_js_1 = require("./mock-model.js");
52
- const plugin_loader_js_1 = require("./adapters/claude-code/plugin-loader.js");
53
+ const replies_js_1 = require("./adapters/claude-code/replies.js");
54
+ Object.defineProperty(exports, "parseReplies", { enumerable: true, get: function () { return replies_js_1.parseReplies; } });
55
+ Object.defineProperty(exports, "reachedModel", { enumerable: true, get: function () { return replies_js_1.reachedModel; } });
56
+ const adapter_registry_js_1 = require("./adapter-registry.js");
57
+ const output_style_js_1 = require("./core/output-style.js");
53
58
  const sandbox_js_1 = require("./sandbox.js");
54
59
  var mock_model_js_2 = require("./mock-model.js");
55
60
  Object.defineProperty(exports, "scriptModel", { enumerable: true, get: function () { return mock_model_js_2.scriptModel; } });
56
- var plugin_loader_js_2 = require("./adapters/claude-code/plugin-loader.js");
57
- Object.defineProperty(exports, "loadPlugin", { enumerable: true, get: function () { return plugin_loader_js_2.loadPlugin; } });
58
- Object.defineProperty(exports, "resolveHarness", { enumerable: true, get: function () { return plugin_loader_js_2.resolveHarness; } });
61
+ const plugin_loader_js_1 = require("./adapters/claude-code/plugin-loader.js");
62
+ Object.defineProperty(exports, "loadPlugin", { enumerable: true, get: function () { return plugin_loader_js_1.loadPlugin; } });
63
+ Object.defineProperty(exports, "resolveHarness", { enumerable: true, get: function () { return plugin_loader_js_1.resolveHarness; } });
59
64
  var sandbox_js_2 = require("./sandbox.js");
60
65
  Object.defineProperty(exports, "decideSandbox", { enumerable: true, get: function () { return sandbox_js_2.decideSandbox; } });
61
66
  Object.defineProperty(exports, "specTrusted", { enumerable: true, get: function () { return sandbox_js_2.specTrusted; } });
@@ -285,8 +290,9 @@ function buildClaudeArgs(spec, hasSettings) {
285
290
  ? ["--plugin-dir", (0, node_path_1.resolve)(spec.pluginDir)]
286
291
  : []),
287
292
  ...(hasSettings ? ["--settings", "settings.json"] : []),
288
- "--allowedTools",
289
- ...tools,
293
+ // A permission allowlist: an empty one approves nothing, so the flag is
294
+ // left out (a bare `--allowedTools` exits before any model turn).
295
+ ...(tools.length === 0 ? [] : ["--allowedTools", ...tools]),
290
296
  ];
291
297
  }
292
298
  /** Build the `claude` argv from the driver context — wraps `buildClaudeArgs`. */
@@ -305,6 +311,7 @@ function parseClaudeRun(stdout) {
305
311
  toolCalls: parseToolCalls(stdout),
306
312
  hooks: parseHooks(stdout),
307
313
  output: parseOutput(stdout),
314
+ replies: (0, replies_js_1.parseReplies)(stdout),
308
315
  };
309
316
  }
310
317
  /* v8 ignore start -- spawns the real claude CLI + filesystem; exercised by the
@@ -399,6 +406,7 @@ function makeResult(cwd, out, parsed, turns, modelRequests) {
399
406
  toolCalls: parsed.toolCalls,
400
407
  hooks: parsed.hooks,
401
408
  output: parsed.output,
409
+ ...(parsed.replies === undefined ? {} : { replies: parsed.replies }),
402
410
  modelRequests,
403
411
  subagents: parseSubagents(out.stdout),
404
412
  file: (p) => {
@@ -443,12 +451,8 @@ async function runHarnessTest(spec, opts = {}) {
443
451
  if (decision.action === "sandbox" && !isClaudeCode) {
444
452
  throw new Error(`sandbox not supported for ${driver.runtime.name}: confined execution is Claude Code only. Pass sandbox: false to run ${driver.runtime.name} unconfined (you audited the code, or trust the outer container).`);
445
453
  }
454
+ const { files, settings, check } = fixtureFor(spec, opts.adapter);
446
455
  const cwd = (0, tmp_root_js_1.makeTmpDir)("harness");
447
- const { files, settings } = (0, plugin_loader_js_1.resolveHarness)({
448
- plugin: spec.plugin,
449
- settings: spec.settings,
450
- files: spec.files,
451
- });
452
456
  writeFixture(cwd, files, settings);
453
457
  const tools = spec.allowedTools ?? ["Read", "Edit", "Write", "Bash"];
454
458
  const timeoutMs = spec.timeoutMs ?? 60000;
@@ -481,7 +485,7 @@ async function runHarnessTest(spec, opts = {}) {
481
485
  const unconsumed = (0, mock_model_js_1.scriptUnconsumedWarning)(count, sideChannelCount);
482
486
  if (unconsumed !== undefined)
483
487
  console.error(unconsumed);
484
- return makeResult(cwd, out, parseClaudeRun(out.stdout), count, out.requests);
488
+ return check(makeResult(cwd, out, parseClaudeRun(out.stdout), count, out.requests));
485
489
  }
486
490
  // Direct path: mock runs in this process; the agent reaches it over localhost.
487
491
  // Start the mock first so its URL feeds the driver's mock-wiring args.
@@ -496,9 +500,9 @@ async function runHarnessTest(spec, opts = {}) {
496
500
  const unconsumed = (0, mock_model_js_1.scriptUnconsumedWarning)(mock.count, mock.sideChannelCount ?? 0);
497
501
  if (unconsumed !== undefined)
498
502
  console.error(unconsumed);
499
- return makeResult(cwd, out, driver.parseRun(out.stdout), mock.count, [
503
+ return check(makeResult(cwd, out, driver.parseRun(out.stdout), mock.count, [
500
504
  ...mock.requests,
501
- ]);
505
+ ]));
502
506
  }
503
507
  finally {
504
508
  await mock.close();
@@ -527,6 +531,52 @@ async function runHarness(spec, opts = {}) {
527
531
  }
528
532
  return runHarnessTest(spec, opts);
529
533
  }
534
+ /**
535
+ * The fixture to write, and the check its result must pass. Without
536
+ * `outputStyle` the check returns the result as it is.
537
+ */
538
+ function fixtureFor(spec, adapter) {
539
+ // One layout for both phases: the plugin is loaded the way the selected
540
+ // harness lays it out, the same layout the style is then planned against.
541
+ const { layout } = adapter ?? adapter_registry_js_1.defaultAdapter;
542
+ const resolved = (0, plugin_loader_js_1.resolveHarness)({ plugin: spec.plugin, settings: spec.settings, files: spec.files }, layout);
543
+ if (spec.outputStyle === undefined)
544
+ return { ...resolved, check: (result) => result };
545
+ const rules = layout.outputStyles;
546
+ if (rules === undefined)
547
+ throw new Error("outputStyle: this harness has no output styles");
548
+ const plan = (0, output_style_js_1.planStyleRun)(layout, { path: spec.outputStyle, text: readStyleFile(spec.outputStyle) }, resolved);
549
+ if (plan.kind === "refused")
550
+ throw new Error(`outputStyle: ${plan.reason}`);
551
+ return {
552
+ files: plan.files,
553
+ settings: plan.settings,
554
+ check: (result) => {
555
+ if ((0, output_style_js_1.styleReached)(rules, plan.style, result.modelRequests))
556
+ return result;
557
+ throw new Error(`outputStyle: "${plan.style.name ?? ""}" never reached the model — ` +
558
+ `no request carried it, so the harness did not load it. ` +
559
+ `Work dir kept for inspection: ${result.cwd}`);
560
+ },
561
+ };
562
+ }
563
+ /**
564
+ * The files and settings a run with `outputStyle` writes — the style at its
565
+ * place, selected by the name the harness reads from it. The same fixture
566
+ * `runHarnessTest({ outputStyle })` runs, so an eval arm built from it gets
567
+ * exactly what that run proved reaches the model.
568
+ */
569
+ function outputStyleFixture(outputStyle, adapter) {
570
+ const { files, settings } = fixtureFor({ outputStyle, model: [] }, adapter);
571
+ return { files, settings };
572
+ }
573
+ /** The style file's text, or a clear error naming the path. */
574
+ function readStyleFile(path) {
575
+ const abs = (0, node_path_1.resolve)(path);
576
+ if (!(0, node_fs_1.existsSync)(abs))
577
+ throw new Error(`outputStyle: no such file: ${path}`);
578
+ return (0, node_fs_1.readFileSync)(abs, "utf-8");
579
+ }
530
580
  /** Pull the pillar-2 driver off an adapter, asserting it supports testing. */
531
581
  async function requireDriver(adapter) {
532
582
  (0, adapter_conformance_js_1.assertHarnessTestable)(adapter);
package/dist/optimize.js CHANGED
@@ -36,10 +36,6 @@ const score_explainer_js_1 = require("./score-explainer.js");
36
36
  function actionFor(e) {
37
37
  return e.symptom === "wrong-skill-fires" ? "differentiate" : "fix";
38
38
  }
39
- function isEmptyMachine(r) {
40
- const surfaces = r.skills.length + r.agents.length + r.hooks.length + r.commands;
41
- return surfaces === 0 && !r.mcp;
42
- }
43
39
  /**
44
40
  * Turn a scan report into a prioritized optimization plan: the structural-health
45
41
  * score + a typed recommendation per deterministic finding (`likely` dead-ends
@@ -60,7 +56,7 @@ function optimize(report) {
60
56
  score,
61
57
  grade: (0, score_core_js_1.gradeFor)(score),
62
58
  recommendations,
63
- empty: isEmptyMachine(report),
59
+ empty: (0, score_core_js_1.isEmptyMachine)(report),
64
60
  };
65
61
  }
66
62
  const ACTION_LABEL = {
@@ -0,0 +1,27 @@
1
+ import type { HarnessAdapter } from "./core/adapter.js";
2
+ import type { EvalArm } from "./eval.js";
3
+ /** The style on and the style off, ready for `paid_measureArms({ arms })`. */
4
+ export interface OutputStyleArms {
5
+ /** The style file at its place in the work dir, selected in the settings. */
6
+ readonly with: EvalArm;
7
+ /** Every style switched off, including one the user set for all projects. */
8
+ readonly without: EvalArm;
9
+ }
10
+ /**
11
+ * Build the with/without arms for the style at `path` (a file in the shape the
12
+ * harness loads, e.g. a Claude Code `.md` style), for the harness the paid tier
13
+ * drives. Spends nothing: the one run it makes uses the scripted model. Throws,
14
+ * before any eval, when the harness has no output styles, cannot tell the
15
+ * style's name, or the style does not reach the model.
16
+ *
17
+ * @experimental One real-model run behind it; how arms are built may change
18
+ * once a style can be authored as a compiled spec with its checks attached.
19
+ */
20
+ export declare function experimental_outputStyleArms(path: string): Promise<OutputStyleArms>;
21
+ /**
22
+ * {@link experimental_outputStyleArms} on a given adapter. Not public: the paid
23
+ * tier drives only the default harness, so arms built for another one would be
24
+ * run by a harness they were not checked on.
25
+ */
26
+ export declare function outputStyleArmsWith(path: string, adapter: HarnessAdapter): Promise<OutputStyleArms>;
27
+ //# sourceMappingURL=output-style-arms.d.ts.map
@@ -0,0 +1,46 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.experimental_outputStyleArms = experimental_outputStyleArms;
4
+ exports.outputStyleArmsWith = outputStyleArmsWith;
5
+ /**
6
+ * The two arms of a paid output-style eval — the style on, the style off — built
7
+ * from the style FILE, after one free run has shown the style reaches the model.
8
+ *
9
+ * Three silent failures this closes. A hand-typed arm name that misses the
10
+ * style's name loads no style and raises no error, so the "with" arm is the
11
+ * "without" arm and the eval compares a run with itself. A bare "without" arm
12
+ * still loads a style the user switched on for every project, which is the same
13
+ * comparison from the other side. And the paid tier cannot check delivery at
14
+ * all: it drives the real API, so no request is captured. The free run here
15
+ * uses the scripted model, where requests ARE captured, with the same fixture
16
+ * the paid arm gets — so a style that would not load stops the eval before the
17
+ * first billed trial.
18
+ */
19
+ const adapter_registry_js_1 = require("./adapter-registry.js");
20
+ const harness_test_js_1 = require("./harness-test.js");
21
+ /**
22
+ * Build the with/without arms for the style at `path` (a file in the shape the
23
+ * harness loads, e.g. a Claude Code `.md` style), for the harness the paid tier
24
+ * drives. Spends nothing: the one run it makes uses the scripted model. Throws,
25
+ * before any eval, when the harness has no output styles, cannot tell the
26
+ * style's name, or the style does not reach the model.
27
+ *
28
+ * @experimental One real-model run behind it; how arms are built may change
29
+ * once a style can be authored as a compiled spec with its checks attached.
30
+ */
31
+ async function experimental_outputStyleArms(path) {
32
+ return outputStyleArmsWith(path, adapter_registry_js_1.defaultAdapter);
33
+ }
34
+ /**
35
+ * {@link experimental_outputStyleArms} on a given adapter. Not public: the paid
36
+ * tier drives only the default harness, so arms built for another one would be
37
+ * run by a harness they were not checked on.
38
+ */
39
+ async function outputStyleArmsWith(path, adapter) {
40
+ const preflight = await (0, harness_test_js_1.runHarnessTest)({ outputStyle: path, prompt: "hi", model: [{ text: "ok" }] }, { adapter });
41
+ preflight.cleanup();
42
+ const { files, settings } = (0, harness_test_js_1.outputStyleFixture)(path, adapter);
43
+ const none = adapter.layout.outputStyles?.selectNone ?? {};
44
+ return { with: { files, settings }, without: { settings: none } };
45
+ }
46
+ //# sourceMappingURL=output-style-arms.js.map
@@ -539,7 +539,7 @@ function scanFiles(files, layout = layout_js_1.claudeCodeLayout, dialect = diale
539
539
  hooks,
540
540
  inlineHooks: inline,
541
541
  manualHookCount: manual,
542
- commands: Object.keys(loaded.files).filter(cls.isCommand).length,
542
+ ...(0, coverage_evidence_js_1.inventoryCounts)(loaded.files, cls.isCommand, [lay], coverage),
543
543
  // A declared server set counts even when the loader emitted no warning —
544
544
  // otherwise a plugin whose servers come from the Agent Plugins `mcp.json`
545
545
  // reports "MCP servers: no" while the report lists an MCP finding.
package/dist/scan.d.ts CHANGED
@@ -211,6 +211,12 @@ export interface ScanReport {
211
211
  */
212
212
  readonly manualHookCount: number;
213
213
  readonly commands: number;
214
+ /**
215
+ * Output styles found, exempt ones aside, or `"not-supported"` when the
216
+ * harness has none — the inventory side of `untested-output-style`. Optional
217
+ * so a hand-built report predating the field still type-checks.
218
+ */
219
+ readonly outputStyles?: number | "not-supported";
214
220
  readonly mcp: boolean;
215
221
  /**
216
222
  * Intra-plugin file references (hook scripts, skill bodies) pointing at files
package/dist/scan.js CHANGED
@@ -258,6 +258,7 @@ function scanPlugin(dir, layout, dialect, opts = {}) {
258
258
  const coverage = (0, test_coverage_js_1.findUntestedSurfaces)({
259
259
  basePath: dir,
260
260
  layout: lay,
261
+ harnessLayouts: declared.map((h) => h.layout),
261
262
  // The SAME `.vigilesrc.json#exclude`. Untested-surface discovery is a
262
263
  // second walk over the same trees, so leaving it out would have excluded a
263
264
  // skill from the GRADE while still naming it in "Untested surfaces: 1" — a
@@ -276,7 +277,7 @@ function scanPlugin(dir, layout, dialect, opts = {}) {
276
277
  hooks,
277
278
  inlineHooks: inline,
278
279
  manualHookCount: manual,
279
- commands: Object.keys(loaded.files).filter(cls.isCommand).length,
280
+ ...(0, coverage_evidence_js_1.inventoryCounts)(loaded.files, cls.isCommand, declared.map((h) => h.layout), coverage),
280
281
  // A declared server set counts even when the loader emitted no warning —
281
282
  // otherwise a plugin whose servers come from the Agent Plugins `mcp.json`
282
283
  // reports "MCP servers: no" while the report lists an MCP finding.
@@ -655,6 +656,16 @@ function instructionWeightLines(w) {
655
656
  ...tail,
656
657
  ];
657
658
  }
659
+ /** The commands and output-style lines of a scan's facts; a harness without styles says so. */
660
+ function inventoryFacts(r) {
661
+ const commands = r.commands > 0 ? [`Commands: ${String(r.commands)}`] : [];
662
+ if (r.outputStyles === "not-supported")
663
+ return [...commands, "Output styles: n/a (this harness has none)"];
664
+ const styles = r.outputStyles ?? 0;
665
+ return styles > 0
666
+ ? [...commands, `Output styles: ${String(styles)}`]
667
+ : commands;
668
+ }
658
669
  function formatScanReport(r) {
659
670
  const out = [`Scan: ${r.dir}`, ""];
660
671
  if (r.instructions) {
@@ -715,15 +726,14 @@ function formatScanReport(r) {
715
726
  out.push(...section("Ineffective hook guards (false confidence)", r.hookBlockFindings.map((h) => ` ✗ [${h.event}] ${h.scriptPath ?? "(inline)"}: ${h.message}`)));
716
727
  out.push(...section("Hook matchers that don't fire as written", r.hookMatcherFindings.map((m) => ` ✗ ${m.message}`)));
717
728
  const facts = [];
718
- if (r.commands > 0)
719
- facts.push(`Commands: ${String(r.commands)}`);
729
+ facts.push(...inventoryFacts(r));
720
730
  facts.push(`MCP servers: ${r.mcp ? "yes" : "no"}`);
721
731
  facts.push(`Untested surfaces: ${String(r.untested)}`);
722
732
  // The two tiers, named separately — a surface with a deterministic harness and
723
733
  // no eval is NOT the same position as one with neither. Shown only when the
724
734
  // producer supplied the split (a hand-built report may not have).
725
735
  if (r.untestedHarness !== undefined && r.unevaluated !== undefined) {
726
- facts.push(` no harness: ${String(r.untestedHarness)} · firing never measured: ${String(r.unevaluated)}`);
736
+ facts.push(` no harness: ${String(r.untestedHarness)} · never measured with a real model: ${String(r.unevaluated)}`);
727
737
  }
728
738
  // …and HOW the rest passed. Without this the count is unfalsifiable from the
729
739
  // outside: "28 covered" and "28 names that appear in some file" print the same.
@@ -180,6 +180,9 @@ export declare function reportDeductions(r: ScanReport): Deduction[];
180
180
  * surface of any kind", because it reads as "this plugin is broken" — a
181
181
  * different statement from "this plugin has no skills or subagents".
182
182
  *
183
+ * An output style counts too: a repo can ship only a style, and "nothing to
184
+ * audit" for it would hide the untested-output-style finding.
185
+ *
183
186
  * Hooks count in BOTH forms: `hooks[]` is the script-backed ones, `inlineHooks`
184
187
  * the shell one-liners that carry no script file to path-check. The second is
185
188
  * still a gate that runs.
@@ -332,6 +332,9 @@ function reportDeductions(r) {
332
332
  * surface of any kind", because it reads as "this plugin is broken" — a
333
333
  * different statement from "this plugin has no skills or subagents".
334
334
  *
335
+ * An output style counts too: a repo can ship only a style, and "nothing to
336
+ * audit" for it would hide the untested-output-style finding.
337
+ *
335
338
  * Hooks count in BOTH forms: `hooks[]` is the script-backed ones, `inlineHooks`
336
339
  * the shell one-liners that carry no script file to path-check. The second is
337
340
  * still a gate that runs.
@@ -341,7 +344,8 @@ function isEmptyMachine(r) {
341
344
  r.agents.length +
342
345
  r.hooks.length +
343
346
  r.inlineHooks +
344
- r.commands;
347
+ r.commands +
348
+ (typeof r.outputStyles === "number" ? r.outputStyles : 0);
345
349
  return surfaces === 0 && !r.mcp;
346
350
  }
347
351
  /**
@@ -118,7 +118,7 @@ export declare const STRUCTURAL_RULES: readonly ["subagent-tool-contract", "suba
118
118
  * here: they're acknowledged-noisy recommendations we never gate on (see
119
119
  * research/install-enforcement-dx.md).
120
120
  */
121
- export declare const WORKFLOW_RULES: readonly ["require-instructions-spec", "untested-skill", "untested-subagent", "untested-hook"];
121
+ export declare const WORKFLOW_RULES: readonly ["require-instructions-spec", "untested-skill", "untested-subagent", "untested-hook", "untested-output-style"];
122
122
  /**
123
123
  * The **`nudge`** group — recommendations / acknowledged-noisy checks that NEVER
124
124
  * gate (not even under `--strict`): `frontmatter-valid` (js-yaml is stricter than
@@ -114,6 +114,7 @@ exports.WORKFLOW_RULES = [
114
114
  "untested-skill",
115
115
  "untested-subagent",
116
116
  "untested-hook",
117
+ "untested-output-style",
117
118
  ];
118
119
  /**
119
120
  * The **`nudge`** group — recommendations / acknowledged-noisy checks that NEVER