vigiles 4.0.1 → 4.0.2

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 (54) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +1 -1
  3. package/dist/adapter-conformance.js +1 -1
  4. package/dist/check.d.ts +132 -0
  5. package/dist/check.js +318 -0
  6. package/dist/cli.js +130 -52
  7. package/dist/core/compile.d.ts +1 -1
  8. package/dist/core/compile.js +1 -1
  9. package/dist/core/compose.d.ts +1 -1
  10. package/dist/core/compose.js +1 -1
  11. package/dist/core/generate-schema.d.ts +1 -1
  12. package/dist/core/generate-schema.js +4 -4
  13. package/dist/core/linters.js +2 -2
  14. package/dist/core/orphans.js +57 -14
  15. package/dist/core/proofs.js +1 -1
  16. package/dist/core/refs.d.ts +1 -1
  17. package/dist/core/refs.js +2 -2
  18. package/dist/core/sidecar.d.ts +1 -1
  19. package/dist/core/sidecar.js +1 -1
  20. package/dist/core/spec.d.ts +1 -1
  21. package/dist/core/spec.js +1 -1
  22. package/dist/core/types.d.ts +1 -1
  23. package/dist/core/validate.js +2 -2
  24. package/dist/e2e.d.ts +10 -13
  25. package/dist/e2e.js +10 -17
  26. package/dist/eval.d.ts +217 -2
  27. package/dist/eval.js +428 -18
  28. package/dist/harness-assert.d.ts +3 -0
  29. package/dist/harness-assert.js +16 -0
  30. package/dist/harness-test.d.ts +46 -0
  31. package/dist/harness-test.js +102 -0
  32. package/dist/integration.d.ts +8 -0
  33. package/dist/integration.js +10 -0
  34. package/dist/jest.d.ts +3 -1
  35. package/dist/jest.js +3 -2
  36. package/dist/run-hook.d.ts +22 -0
  37. package/dist/run-hook.js +28 -0
  38. package/dist/scan.d.ts +1 -1
  39. package/dist/scan.js +1 -1
  40. package/dist/setup-plan.d.ts +5 -1
  41. package/dist/setup-plan.js +11 -1
  42. package/dist/test-coverage.js +8 -1
  43. package/dist/testing.d.ts +2 -0
  44. package/dist/testing.js +7 -0
  45. package/dist/unit.d.ts +4 -2
  46. package/dist/unit.js +7 -1
  47. package/dist/vitest.d.mts +3 -1
  48. package/hooks/refs-nudge.sh +1 -1
  49. package/package.json +3 -2
  50. package/skills/edit-spec/SKILL.md +21 -10
  51. package/skills/linter-docs/SKILL.md +23 -0
  52. package/skills/migrate-to-spec/SKILL.md +1 -1
  53. package/skills/strengthen/SKILL.md +1 -2
  54. package/skills/generate-rule/SKILL.md +0 -64
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.claudeCodeDriver = exports.sandboxAvailable = exports.specTrusted = exports.decideSandbox = exports.resolveHarness = exports.loadPlugin = exports.scriptModel = void 0;
4
4
  exports.parseToolCalls = parseToolCalls;
5
+ exports.parseSubagents = parseSubagents;
5
6
  exports.parseResultEvent = parseResultEvent;
6
7
  exports.parseOutput = parseOutput;
7
8
  exports.parseHooks = parseHooks;
@@ -9,6 +10,7 @@ exports.buildClaudeArgs = buildClaudeArgs;
9
10
  exports.parseClaudeRun = parseClaudeRun;
10
11
  exports.claudeAvailable = claudeAvailable;
11
12
  exports.runHarnessTest = runHarnessTest;
13
+ exports.runHarness = runHarness;
12
14
  /**
13
15
  * vigiles — deterministic Claude Code harness testing.
14
16
  *
@@ -114,6 +116,82 @@ function parseToolCalls(streamJson) {
114
116
  isError: results.get(u.id)?.isError ?? false,
115
117
  }));
116
118
  }
119
+ /**
120
+ * Recover sub-agent runs as nested traces. A subagent-dispatch tool call (the
121
+ * `Agent` tool on the live CLI — older docs say `Task` — carrying an
122
+ * `input.subagent_type`) spawns a subagent whose own events the CLI tags with a
123
+ * top-level `parent_tool_use_id` = the dispatch tool-use id. We group those
124
+ * tagged tool calls under their dispatch, keyed by `subagent_type`. **Schema
125
+ * verified against real claude output** (`parent_tool_use_id` sibling of
126
+ * `message`, `subagent_type` in the dispatch input; tool named `Agent`) — the
127
+ * same `message.content` line shape `parseToolCalls` consumes, and we match the
128
+ * input field NOT the tool name so a future rename can't break it. Pure; empty
129
+ * for a harness that doesn't emit `parent_tool_use_id` (e.g. Codex).
130
+ */
131
+ function parseSubagents(streamJson) {
132
+ const tasks = new Map(); // dispatch id → subagent name
133
+ const byParent = new Map();
134
+ const groupFor = (parent) => {
135
+ let g = byParent.get(parent);
136
+ if (!g) {
137
+ g = { uses: [], results: new Map() };
138
+ byParent.set(parent, g);
139
+ }
140
+ return g;
141
+ };
142
+ for (const line of streamJson.split("\n")) {
143
+ if (!line.trim())
144
+ continue;
145
+ let evt;
146
+ try {
147
+ evt = JSON.parse(line);
148
+ }
149
+ catch {
150
+ continue;
151
+ }
152
+ const content = evt.message?.content;
153
+ if (!Array.isArray(content))
154
+ continue;
155
+ const parent = typeof evt.parent_tool_use_id === "string"
156
+ ? evt.parent_tool_use_id
157
+ : undefined;
158
+ for (const b of content) {
159
+ if (b.type === "tool_use" && typeof b.name === "string") {
160
+ const id = typeof b.id === "string" ? b.id : "";
161
+ if (!parent) {
162
+ // A subagent dispatch is any top-level tool_use whose input carries a
163
+ // `subagent_type` — the dispatch tool is named "Agent" on the live CLI
164
+ // (older docs say "Task"), so match the input field, NOT the tool name,
165
+ // to survive the rename. Confirmed against real claude output.
166
+ const sub = b.input?.subagent_type;
167
+ if (typeof sub === "string")
168
+ tasks.set(id, sub);
169
+ }
170
+ if (parent)
171
+ groupFor(parent).uses.push({ id, name: b.name, input: b.input });
172
+ }
173
+ else if (b.type === "tool_result" && parent) {
174
+ const id = typeof b.tool_use_id === "string" ? b.tool_use_id : "";
175
+ groupFor(parent).results.set(id, {
176
+ text: contentText(b.content),
177
+ isError: b.is_error === true,
178
+ });
179
+ }
180
+ }
181
+ }
182
+ const out = [];
183
+ for (const [taskId, name] of tasks) {
184
+ const g = byParent.get(taskId);
185
+ const toolCalls = (g?.uses ?? []).map((u) => ({
186
+ name: u.name,
187
+ input: u.input,
188
+ resultText: g?.results.get(u.id)?.text ?? "",
189
+ isError: g?.results.get(u.id)?.isError ?? false,
190
+ }));
191
+ out.push({ name, toolCalls });
192
+ }
193
+ return out;
194
+ }
117
195
  /**
118
196
  * The terminal `result` event — present in BOTH `--output-format` shapes (a
119
197
  * `{type:"result", …}` line in stream-json, the single object in `json`), or
@@ -297,6 +375,7 @@ function makeResult(cwd, out, parsed, turns, modelRequests) {
297
375
  hooks: parsed.hooks,
298
376
  output: parsed.output,
299
377
  modelRequests,
378
+ subagents: parseSubagents(out.stdout),
300
379
  file: (p) => {
301
380
  const f = (0, node_path_1.resolve)(cwd, p);
302
381
  return (0, node_fs_1.existsSync)(f) ? (0, node_fs_1.readFileSync)(f, "utf-8") : null;
@@ -380,6 +459,29 @@ async function runHarnessTest(spec, opts = {}) {
380
459
  await mock.close();
381
460
  }
382
461
  }
462
+ /**
463
+ * `runHarness` — the harness-scope entry of the revamped API (Phase 2 of
464
+ * `research/testing-api-design.md`). The harness has two execution scopes, `hook`
465
+ * (`runHook`) and `harness` (the whole assembled agent); today's `integration` /
466
+ * `e2e` / `eval` are all the **harness** scope under realness flags. This entry is
467
+ * the **deterministic** harness run (`model: "mock"`, the default) — the
468
+ * workhorse you gate every commit, with no key. A **real-model** harness run is
469
+ * non-deterministic by definition, so you don't *assert* a single one — you
470
+ * `measure()` it across trials (the eval scope). `egress` is a capability of this
471
+ * scope (the e2e tier), not a separate tier.
472
+ *
473
+ * Behaviour is identical to `runHarnessTest` (which it wraps); the new name +
474
+ * `model` flag make the scope/realness explicit and steer real-model runs to the
475
+ * right tool.
476
+ */
477
+ async function runHarness(spec, opts = {}) {
478
+ if (opts.model === "real") {
479
+ throw new Error("runHarness runs the harness DETERMINISTICALLY (model: 'mock'). A real-model " +
480
+ "harness run is non-deterministic, so a single one can't be asserted — " +
481
+ "measure it across trials with `measure()` / `runEval` (the eval scope) instead.");
482
+ }
483
+ return runHarnessTest(spec, opts);
484
+ }
383
485
  /** Pull the pillar-2 driver off an adapter, asserting it supports testing. */
384
486
  function requireDriver(adapter) {
385
487
  (0, adapter_conformance_js_1.assertHarnessTestable)(adapter);
@@ -8,9 +8,17 @@
8
8
  * (the plugin loader). Capability contract: needs the **`claude` binary and
9
9
  * bubblewrap**, but **no API key and no network**. A `*.integration.test.ts`
10
10
  * imports from here.
11
+ *
12
+ * Real **egress** is a CAPABILITY of this scope (the former `e2e` tier), not a
13
+ * separate tier: `egressRoutes()` probes whether allowlisted egress can route,
14
+ * and `runHook` takes `egress: { allow }` for allowlisted real outbound — gated
15
+ * by a routable sandbox + real network (a test self-skips via `egressRoutes()`).
16
+ * `vigiles/e2e` remains as a thin back-compat alias. See
17
+ * `research/testing-api-design.md` Part 4.
11
18
  */
12
19
  export * from "./unit.js";
13
20
  export * from "./harness-test.js";
14
21
  export * from "./mock-model.js";
15
22
  export type { LoadedPlugin } from "./plugin-loader.js";
23
+ export { egressRoutes } from "./run-hook.js";
16
24
  //# sourceMappingURL=integration.d.ts.map
@@ -14,6 +14,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
14
  for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
15
  };
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
+ exports.egressRoutes = void 0;
17
18
  /**
18
19
  * `vigiles/integration` — the **deterministic, assembled-machine** tier.
19
20
  *
@@ -24,8 +25,17 @@ Object.defineProperty(exports, "__esModule", { value: true });
24
25
  * (the plugin loader). Capability contract: needs the **`claude` binary and
25
26
  * bubblewrap**, but **no API key and no network**. A `*.integration.test.ts`
26
27
  * imports from here.
28
+ *
29
+ * Real **egress** is a CAPABILITY of this scope (the former `e2e` tier), not a
30
+ * separate tier: `egressRoutes()` probes whether allowlisted egress can route,
31
+ * and `runHook` takes `egress: { allow }` for allowlisted real outbound — gated
32
+ * by a routable sandbox + real network (a test self-skips via `egressRoutes()`).
33
+ * `vigiles/e2e` remains as a thin back-compat alias. See
34
+ * `research/testing-api-design.md` Part 4.
27
35
  */
28
36
  __exportStar(require("./unit.js"), exports);
29
37
  __exportStar(require("./harness-test.js"), exports);
30
38
  __exportStar(require("./mock-model.js"), exports);
39
+ var run_hook_js_1 = require("./run-hook.js");
40
+ Object.defineProperty(exports, "egressRoutes", { enumerable: true, get: function () { return run_hook_js_1.egressRoutes; } });
31
41
  //# sourceMappingURL=integration.js.map
package/dist/jest.d.ts CHANGED
@@ -1,9 +1,11 @@
1
+ import type { Check } from "./check.js";
1
2
  declare module "@jest/expect" {
2
3
  interface Matchers<R> {
3
4
  toHaveCreated(path: string): R;
4
5
  toBlock(): R;
5
6
  toBeatBaseline(baseline: string, arm: string, metric: string, by?: number): R;
7
+ toPass(check: Check<any>): R;
8
+ toPassAll(checks: readonly Check<any>[]): R;
6
9
  }
7
10
  }
8
- export {};
9
11
  //# sourceMappingURL=jest.d.ts.map
package/dist/jest.js CHANGED
@@ -1,7 +1,8 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- /* eslint-disable max-params --
4
- The matcher signatures mirror the runtime vigilesMatchers (positional args). */
3
+ /* eslint-disable max-params, @typescript-eslint/no-explicit-any --
4
+ The matcher signatures mirror the runtime vigilesMatchers (positional args);
5
+ `Check<any>` matches the runtime generic for the type-only augmentation. */
5
6
  /**
6
7
  * vigiles — jest integration (opt-in).
7
8
  *
@@ -1,6 +1,28 @@
1
1
  import type { HookProtocol } from "./core/hook-protocol.js";
2
2
  import { type SandboxMode, type EgressAttempt } from "./sandbox.js";
3
3
  export type { EgressAttempt };
4
+ /** Result of {@link propertyHook}: the first shrunk counterexample, if any. */
5
+ export interface HookPropertyResult<E> {
6
+ readonly passed: boolean;
7
+ readonly iterations: number;
8
+ /** The (shrunk) event that broke an invariant — present iff `!passed`. */
9
+ readonly counterexample?: E;
10
+ /** Which invariant failed — present iff `!passed`. */
11
+ readonly failedInvariant?: string;
12
+ }
13
+ /**
14
+ * Property-test a hook's decision over generated events. Throws nothing — returns
15
+ * a result you assert on (`assert.ok(r.passed)`), so the counterexample is
16
+ * inspectable. `mutate(event, rng)` produces a variation from the running event;
17
+ * each named invariant is checked against `decide(event)`.
18
+ */
19
+ export declare function propertyHook<E, D>(opts: {
20
+ readonly seed: E;
21
+ readonly mutate: (event: E, rng: number) => E;
22
+ readonly decide: (event: E) => D;
23
+ readonly invariants: Record<string, (decision: D, event: E) => boolean>;
24
+ readonly iterations?: number;
25
+ }): HookPropertyResult<E>;
4
26
  /** A hook event payload (the JSON Claude Code writes to the hook's stdin). */
5
27
  export interface HookInput {
6
28
  /** e.g. "PreToolUse", "PostToolUse", "Stop", "SessionStart", "PreCompact". */
package/dist/run-hook.js CHANGED
@@ -1,5 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.propertyHook = propertyHook;
3
4
  exports.parseHookOutput = parseHookOutput;
4
5
  exports.decideHook = decideHook;
5
6
  exports.runHookWith = runHookWith;
@@ -41,6 +42,33 @@ const node_path_1 = require("node:path");
41
42
  const hook_protocol_js_1 = require("./adapters/claude-code/hook-protocol.js");
42
43
  const egress_js_1 = require("./egress.js");
43
44
  const sandbox_js_1 = require("./sandbox.js");
45
+ const proofs_js_1 = require("./core/proofs.js");
46
+ /**
47
+ * Property-test a hook's decision over generated events. Throws nothing — returns
48
+ * a result you assert on (`assert.ok(r.passed)`), so the counterexample is
49
+ * inspectable. `mutate(event, rng)` produces a variation from the running event;
50
+ * each named invariant is checked against `decide(event)`.
51
+ */
52
+ function propertyHook(opts) {
53
+ const wrapped = {};
54
+ for (const [name, inv] of Object.entries(opts.invariants)) {
55
+ wrapped[name] = (e) => inv(opts.decide(e), e);
56
+ }
57
+ const r = (0, proofs_js_1.propertyTest)(opts.seed, opts.mutate, wrapped, {
58
+ iterations: opts.iterations ?? 100,
59
+ sequenceLength: 1, // each event is independent — no mutation sequence
60
+ seed: 1,
61
+ });
62
+ if (r.passed)
63
+ return { passed: true, iterations: r.iterations };
64
+ const last = r.failingSequence?.[r.failingSequence.length - 1];
65
+ return {
66
+ passed: false,
67
+ iterations: r.iterations,
68
+ counterexample: r.shrunk ?? last,
69
+ failedInvariant: r.failedInvariant,
70
+ };
71
+ }
44
72
  /** Parse stdout as a hook JSON decision (pure, testable without a process). */
45
73
  function parseHookOutput(stdout) {
46
74
  const s = stdout.trim();
package/dist/scan.d.ts CHANGED
@@ -3,7 +3,7 @@
3
3
  * and what's broken, with **no model and no API key**.
4
4
  *
5
5
  * This is the deterministic substrate under the plugin/skill leaderboard
6
- * (research/divergent-bets.md #9) and the harness-aware audit
6
+ * (research/divergent-bets.md #9) and the harness-aware scan
7
7
  * (research/agent-supply-chain-security.md #1): it re-aims the machinery that
8
8
  * already exists — `loadPlugin` (surfaces + dangling-ref/MCP/empty-machine
9
9
  * warnings), `parseAgentTools` (the declared tool contract), and
package/dist/scan.js CHANGED
@@ -4,7 +4,7 @@
4
4
  * and what's broken, with **no model and no API key**.
5
5
  *
6
6
  * This is the deterministic substrate under the plugin/skill leaderboard
7
- * (research/divergent-bets.md #9) and the harness-aware audit
7
+ * (research/divergent-bets.md #9) and the harness-aware scan
8
8
  * (research/agent-supply-chain-security.md #1): it re-aims the machinery that
9
9
  * already exists — `loadPlugin` (surfaces + dangling-ref/MCP/empty-machine
10
10
  * warnings), `parseAgentTools` (the declared tool contract), and
@@ -10,7 +10,7 @@
10
10
  */
11
11
  /** What `vigiles init` will set up. */
12
12
  export interface SetupPlan {
13
- /** Lint pillar — verify instruction-file references (specs, types, compile, lint/audit, hooks). */
13
+ /** Lint pillar — verify instruction-file references (specs, types, compile, lint, hooks). */
14
14
  lint: boolean;
15
15
  /** Test pillar — test the harness (scaffold a starter harness test + CI job). */
16
16
  test: boolean;
@@ -20,12 +20,16 @@ export interface SetupPlan {
20
20
  plugin: boolean;
21
21
  /** Strict rule severities in `.vigilesrc.json`. */
22
22
  strict: boolean;
23
+ /** Rewrite an existing STALE CI workflow in place (instead of only warning). */
24
+ force: boolean;
23
25
  }
24
26
  /** The explicit choices a user pinned via flags (undefined = "not specified"). */
25
27
  export interface ParsedSetupArgs {
26
28
  target?: string;
27
29
  strict: boolean;
28
30
  yes: boolean;
31
+ /** `--force` — rewrite a stale CI workflow in place. */
32
+ force: boolean;
29
33
  /** Lint pillar — `--lint` → true, `--no-lint` → false, absent → undefined. */
30
34
  lint?: boolean;
31
35
  /** Test pillar — `--test` → true, `--no-test` → false, absent → undefined. */
@@ -33,6 +33,7 @@ function parseSetupArgs(args) {
33
33
  target: flagValue(args, "--target="),
34
34
  strict: args.includes("--strict"),
35
35
  yes: args.includes("--yes") || args.includes("-y"),
36
+ force: args.includes("--force"),
36
37
  lint: boolFlag(args, "lint"),
37
38
  test: boolFlag(args, "test"),
38
39
  harness: flagValue(args, "--harness="),
@@ -42,7 +43,14 @@ function parseSetupArgs(args) {
42
43
  }
43
44
  /** The non-interactive defaults: both pillars, CI, and the plugin. */
44
45
  function defaultPlan(strict = false) {
45
- return { lint: true, test: true, gha: true, plugin: true, strict };
46
+ return {
47
+ lint: true,
48
+ test: true,
49
+ gha: true,
50
+ plugin: true,
51
+ strict,
52
+ force: false,
53
+ };
46
54
  }
47
55
  /**
48
56
  * Whether to drop into interactive prompts: a human at a TTY who passed neither
@@ -151,6 +159,8 @@ function resolvePlan(parsed, answers) {
151
159
  plan.gha = false;
152
160
  if (parsed.plugin === false)
153
161
  plan.plugin = false;
162
+ if (parsed.force)
163
+ plan.force = true;
154
164
  if (parsed.target)
155
165
  plan.test = false;
156
166
  if (answers)
@@ -151,7 +151,14 @@ function discoverHooks(basePath) {
151
151
  }));
152
152
  }
153
153
  function discoverTests(basePath, globs, ignore) {
154
- const found = (0, glob_1.globSync)([...globs], { cwd: basePath, ignore });
154
+ // `dot: true` so a colocated test under a DOT directory is found — most
155
+ // loose skills live in `.claude/skills/<name>/`, so the eval the warning
156
+ // suggests (`.claude/skills/<name>/<name>.eval.mjs`) is itself dot-pathed.
157
+ // Without this, a globstar (`**/*.eval.mjs`) silently skips it while the
158
+ // skill (matched by the explicit-dot `.claude/skills/*/SKILL.md` pattern) is
159
+ // still discovered — so the surface looks untested even after the user adds
160
+ // exactly the suggested file. DEFAULT_IGNORE still drops .git/node_modules/etc.
161
+ const found = (0, glob_1.globSync)([...globs], { cwd: basePath, ignore, dot: true });
155
162
  return found.map((path) => ({ path, content: read((0, node_path_1.join)(basePath, path)) }));
156
163
  }
157
164
  /** Colocated: a test inside a skill dir, or a name-prefixed sibling of an agent/hook. */
package/dist/testing.d.ts CHANGED
@@ -14,4 +14,6 @@ export * from "./run-hook.js";
14
14
  export * from "./harness-test.js";
15
15
  export * from "./eval.js";
16
16
  export * from "./harness-assert.js";
17
+ export * from "./check.js";
18
+ export { hookFired } from "./check.js";
17
19
  //# sourceMappingURL=testing.d.ts.map
package/dist/testing.js CHANGED
@@ -14,6 +14,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
14
  for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
15
  };
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
+ exports.hookFired = void 0;
17
18
  /**
18
19
  * `vigiles/testing` — Pillar 2 entry point: the **harness-testing** API. Re-exports
19
20
  * the three tiers — `runHook` (unit), `runHarnessTest` (deterministic), `runEval`
@@ -30,4 +31,10 @@ __exportStar(require("./run-hook.js"), exports);
30
31
  __exportStar(require("./harness-test.js"), exports);
31
32
  __exportStar(require("./eval.js"), exports);
32
33
  __exportStar(require("./harness-assert.js"), exports);
34
+ // The declarative check vocabulary is now first-class at the front door. Its
35
+ // `hookFired` (a `Check<Trace>`) supersedes the legacy boolean predicate of the
36
+ // same name — the explicit re-export below wins over the two `export *`s.
37
+ __exportStar(require("./check.js"), exports);
38
+ var check_js_1 = require("./check.js");
39
+ Object.defineProperty(exports, "hookFired", { enumerable: true, get: function () { return check_js_1.hookFired; } });
33
40
  //# sourceMappingURL=testing.js.map
package/dist/unit.d.ts CHANGED
@@ -12,6 +12,8 @@
12
12
  * `vigiles/e2e`.
13
13
  */
14
14
  export * from "./harness-assert.js";
15
- export { runHook, parseHookOutput, decideHook } from "./run-hook.js";
16
- export type { HookInput, HookOutput, HookRunResult, RunHookOptions, } from "./run-hook.js";
15
+ export { runHook, parseHookOutput, decideHook, propertyHook, } from "./run-hook.js";
16
+ export type { HookInput, HookOutput, HookRunResult, RunHookOptions, HookPropertyResult, } from "./run-hook.js";
17
+ export * from "./check.js";
18
+ export { hookFired } from "./check.js";
17
19
  //# sourceMappingURL=unit.d.ts.map
package/dist/unit.js CHANGED
@@ -14,7 +14,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
14
  for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
15
  };
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
- exports.decideHook = exports.parseHookOutput = exports.runHook = void 0;
17
+ exports.hookFired = exports.propertyHook = exports.decideHook = exports.parseHookOutput = exports.runHook = void 0;
18
18
  /**
19
19
  * `vigiles/unit` — the **no-capability** harness-testing surface.
20
20
  *
@@ -33,4 +33,10 @@ var run_hook_js_1 = require("./run-hook.js");
33
33
  Object.defineProperty(exports, "runHook", { enumerable: true, get: function () { return run_hook_js_1.runHook; } });
34
34
  Object.defineProperty(exports, "parseHookOutput", { enumerable: true, get: function () { return run_hook_js_1.parseHookOutput; } });
35
35
  Object.defineProperty(exports, "decideHook", { enumerable: true, get: function () { return run_hook_js_1.decideHook; } });
36
+ Object.defineProperty(exports, "propertyHook", { enumerable: true, get: function () { return run_hook_js_1.propertyHook; } });
37
+ // The check vocabulary is part of the base surface (pure, no capability). Its
38
+ // `hookFired` check supersedes the legacy boolean predicate of the same name.
39
+ __exportStar(require("./check.js"), exports);
40
+ var check_js_1 = require("./check.js");
41
+ Object.defineProperty(exports, "hookFired", { enumerable: true, get: function () { return check_js_1.hookFired; } });
36
42
  //# sourceMappingURL=unit.js.map
package/dist/vitest.d.mts CHANGED
@@ -1,9 +1,11 @@
1
+ import type { Check } from "./check.js";
1
2
  declare module "@vitest/expect" {
2
3
  interface Matchers<T = any> {
3
4
  toHaveCreated(path: string): T;
4
5
  toBlock(): T;
5
6
  toBeatBaseline(baseline: string, arm: string, metric: string, by?: number): T;
7
+ toPass(check: Check<any>): T;
8
+ toPassAll(checks: readonly Check<any>[]): T;
6
9
  }
7
10
  }
8
- export {};
9
11
  //# sourceMappingURL=vitest.d.mts.map
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env bash
2
2
  # PostToolUse hook — nudge the agent to express references in instruction files
3
- # (CLAUDE.md / AGENTS.md / SKILL.md) as vigiles marks, so `vigiles audit` can
3
+ # (CLAUDE.md / AGENTS.md / SKILL.md) as vigiles marks, so `vigiles lint` can
4
4
  # actually verify them. Non-blocking by default; set the `unmarked-refs` rule to
5
5
  # "error" in .vigilesrc.json to turn the nudge into a hard block, or to false to
6
6
  # disable it. Runs as its OWN PostToolUse entry so its stdout stays clean JSON.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vigiles",
3
- "version": "4.0.1",
3
+ "version": "4.0.2",
4
4
  "description": "Compile .spec.ts files to instruction files (CLAUDE.md, AGENTS.md) with linter cross-referencing",
5
5
  "bin": {
6
6
  "vigiles": "dist/cli.js"
@@ -32,7 +32,8 @@
32
32
  "types": "./dist/vitest.d.mts",
33
33
  "default": "./dist/vitest.mjs"
34
34
  },
35
- "./jest": "./dist/jest.js"
35
+ "./jest": "./dist/jest.js",
36
+ "./check": "./dist/check.js"
36
37
  },
37
38
  "files": [
38
39
  "dist/**/*.js",
@@ -1,7 +1,6 @@
1
1
  ---
2
2
  name: edit-spec
3
- description: Edit a vigiles spec file to update instruction files (CLAUDE.md, AGENTS.md)
4
- disable-model-invocation: true
3
+ description: Edit a vigiles .spec.ts to change a compiled instruction file (CLAUDE.md / AGENTS.md) — add, modify, or remove a rule, section, command, or key file. Use whenever you need to change a CLAUDE.md/AGENTS.md that carries a vigiles hash (edit the spec, never the artifact), including adding a new enforce()/check()/guidance() rule.
5
4
  argument-hint: <what to change — e.g., "add a rule about error handling" or "update the testing section">
6
5
  ---
7
6
 
@@ -27,7 +26,8 @@ Look for spec files in the repo root:
27
26
  - `AGENTS.md.spec.ts` — source for AGENTS.md
28
27
  - Any `*.spec.ts` matching instruction files
29
28
 
30
- If no spec exists, suggest: `npx vigiles setup`
29
+ If no spec exists: if there's a hand-written `CLAUDE.md`, suggest the
30
+ `migrate-to-spec` skill; otherwise suggest `npx vigiles init` to scaffold one.
31
31
 
32
32
  ### Step 2: Read and Understand the Spec
33
33
 
@@ -83,11 +83,22 @@ export default claude({
83
83
 
84
84
  Based on what the user asked for:
85
85
 
86
- **Adding a rule:**
87
-
88
- - Determine the type: `enforce()` if a linter rule exists, `check()` for filesystem conventions, `guidance()` for prose-only
89
- - For `enforce()`: find the actual linter rule name (e.g., `eslint/no-console`, `@typescript-eslint/no-explicit-any`, `ruff/T201`)
90
- - Add to the `rules` object with a descriptive key
86
+ **Adding a rule** (this absorbs the old `generate-rule` skill):
87
+
88
+ - **Classify the rule** from the request:
89
+ - `enforce()` a linter rule can back it. Check the project's linter configs
90
+ (ESLint, Ruff, Clippy, Pylint, RuboCop, Stylelint) for a matching rule; also
91
+ consider an architectural tool (ast-grep, Dependency Cruiser, Steiger). If
92
+ uncertain whether a rule exists, **ask the user** rather than guessing.
93
+ - `check()` — a filesystem structural convention ("every X needs a Y"). Only
94
+ for file-pairing; never for code content.
95
+ - `guidance()` — can't be mechanically enforced (subjective conventions,
96
+ process rules, migration context).
97
+ - For `enforce()`: use the real linter rule name (e.g. `eslint/no-console`,
98
+ `@typescript-eslint/no-explicit-any`, `ruff/T201`).
99
+ - Add to the `rules` object with a kebab-case key derived from the intent,
100
+ preserving alphabetical order if the existing rules are alphabetical. Import
101
+ any new builders needed (e.g. `check` and `every` for the first `check()`).
91
102
 
92
103
  **Updating a section:**
93
104
 
@@ -118,10 +129,10 @@ This regenerates the compiled instruction file(s). Review the output for any err
118
129
  ### Step 5: Verify
119
130
 
120
131
  ```bash
121
- npx vigiles check
132
+ npx vigiles lint
122
133
  ```
123
134
 
124
- If the PostToolUse hook is installed (via `npx skills add zernie/vigiles`), compilation happens automatically after you save the spec.
135
+ If the vigiles plugin is installed (`/plugin marketplace add zernie/vigiles` then `/plugin install vigiles@vigiles`, or `npx vigiles init`), the PostToolUse hook recompiles automatically after you save the spec.
125
136
 
126
137
  ## Important
127
138
 
@@ -0,0 +1,23 @@
1
+ ---
2
+ name: linter-docs
3
+ description: Deep linter reference for authoring or debugging a vigiles enforce() rule — plugin tables, AST selectors, type-aware rules, auto-fix, and edge cases for ESLint, Ruff, Pylint, RuboCop, and Stylelint. Use when you need the exact rule name or config for a specific linter, not for running a linter.
4
+ disable-model-invocation: true
5
+ ---
6
+
7
+ Reference material for the linters vigiles cross-references. Open the file for
8
+ the linter you're working with when you need exact rule names, AST selectors,
9
+ or config details to author an `enforce()` rule (or diagnose why one is reported
10
+ missing or disabled).
11
+
12
+ | Linter | Reference |
13
+ | --------- | -------------------------------------------------------------------------------------- |
14
+ | ESLint | [`eslint.md`](eslint.md) — plugin table, AST selectors, type-aware rules, auto-fix |
15
+ | Ruff | [`ruff.md`](ruff.md) — 800+ reimplemented rules, selection, auto-fix, pyproject config |
16
+ | Pylint | [`pylint.md`](pylint.md) — plugin table, astroid AST, type inference, custom checkers |
17
+ | RuboCop | [`rubocop.md`](rubocop.md) — gem table, node pattern DSL, auto-correct, custom cops |
18
+ | Stylelint | [`stylelint.md`](stylelint.md) — plugin table, PostCSS AST, custom rules, SCSS |
19
+ | Clippy | [`clippy.md`](clippy.md) — Rust lint groups and configuration |
20
+
21
+ These are read by the `strengthen` and `edit-spec` skills when matching a
22
+ guidance rule to a real linter rule. This skill is user-invoked (a reference,
23
+ not an action), so it never fires on its own — open the relevant file directly.
@@ -7,7 +7,7 @@ argument-hint: <path to CLAUDE.md, defaults to CLAUDE.md>
7
7
 
8
8
  Convert an existing hand-written CLAUDE.md (or AGENTS.md) into a typed `CLAUDE.md.spec.ts` file. This is the incremental adoption path — you keep your existing instruction file as the starting point and get type safety going forward.
9
9
 
10
- > **Don't need full TypeScript?** A typed spec is the deepest commitment level. If the user only wants verified rules without a build step, point them at markdown mode first: inline `<!-- vigiles:enforce ... -->` comments (Level 0) or a `vigiles:` YAML frontmatter block with `vigiles generate-schema` for editor autocomplete (Level 1). Both are verified by `vigiles audit` with the same engine as a spec. See `docs/markdown-mode.md`. Migrate to a spec only when they want compiler-grade guarantees.
10
+ > **Don't need full TypeScript?** A typed spec is the deepest commitment level. If the user only wants verified rules without a build step, point them at markdown mode first: inline `<!-- vigiles:enforce ... -->` comments (Level 0) or a `vigiles:` YAML frontmatter block with `vigiles generate-schema` for editor autocomplete (Level 1). Both are verified by `vigiles lint` with the same engine as a spec. See `docs/markdown-mode.md`. Migrate to a spec only when they want compiler-grade guarantees.
11
11
 
12
12
  ## Instructions
13
13
 
@@ -1,7 +1,6 @@
1
1
  ---
2
2
  name: strengthen
3
- description: Upgrade guidance() rules to enforce() by finding existing linter rules that match
4
- disable-model-invocation: true
3
+ description: Upgrade a vigiles spec's guidance() rules to enforce() scan the guidance rules in a CLAUDE.md/AGENTS.md spec and find existing linter rules (ESLint, Ruff, Clippy, Pylint, RuboCop, Stylelint) that back them. Use when asked to strengthen, harden, or make vigiles rules enforceable; NOT for general linting or fixing lint errors.
5
4
  ---
6
5
 
7
6
  Scan spec files for `guidance()` rules and suggest `enforce()` replacements backed by real linter rules.
@@ -1,64 +0,0 @@
1
- <!-- vigiles:sha256:a9df2c1748576753 compiled from skills/generate-rule/SKILL.md.spec.ts -->
2
-
3
- ---
4
-
5
- name: generate-rule
6
- description: Add a new enforce(), check(), or guidance() rule to an existing spec file
7
- disable-model-invocation: true
8
- argument-hint: <rule>
9
-
10
- ---
11
-
12
- ## Arguments
13
-
14
- - `$1` **rule** — natural-language description of what the rule should enforce, e.g. "no console.log in production code"
15
-
16
- ## Steps
17
-
18
- ### Step 1
19
-
20
- **Find the spec file.** Look for `CLAUDE.md.spec.ts` in the repo root. If it doesn't exist:
21
-
22
- - If there's a hand-written `CLAUDE.md`, suggest running the `migrate-to-spec` skill first.
23
- - If there's no `CLAUDE.md` either, suggest `npx vigiles init` to scaffold one.
24
-
25
- ### Step 2
26
-
27
- **Classify the rule** from the description ($1):
28
-
29
- - **enforce()** — if a linter rule can back it. Check the project's linter configs (ESLint, Ruff, Clippy, Pylint, RuboCop, Stylelint) for a matching rule; also consider an architectural tool (ast-grep, Dependency Cruiser, Steiger). If uncertain whether a rule exists, **ask the user** rather than guessing.
30
- - **check()** — if it's a filesystem structural convention ("every X needs a Y"). Only for file-pairing; never for code content.
31
- - **guidance()** — if it can't be mechanically enforced (subjective conventions, process rules, migration context).
32
-
33
- ### Step 3
34
-
35
- **Generate the rule.** Create an entry with a kebab-case ID derived from the description. Examples:
36
-
37
- "no-console": enforce("eslint/no-console", "Use structured logger for observability."),
38
-
39
- "controller-tests": check(
40
- every("src/**/*.controller.ts").has("{name}.controller.test.ts"),
41
- "Every controller must have a co-located test file.",
42
- ),
43
-
44
- "research-before-implementing": guidance(
45
- "Google unfamiliar APIs before implementing.",
46
- ),
47
-
48
- ### Step 4
49
-
50
- **Add it to the spec.** Read the existing spec file and add the new rule to the `rules` object, preserving alphabetical ordering if the existing rules are alphabetical. Import any new builders needed (e.g. `check` and `every` for the first `check()` rule).
51
-
52
- ### Step 5
53
-
54
- **Compile and verify.** Build and recompile; if compilation fails (e.g. the linter rule doesn't exist), report the error and suggest alternatives. Show the user the updated spec and the compiled `CLAUDE.md` diff.
55
-
56
- **Gate** — run the project's build command (retry up to 2×); do not proceed until it passes.
57
-
58
- <!-- vigiles:gate role:build retry:2 -->
59
-
60
- ## Result
61
-
62
- This skill is complete when the project's build command passes.
63
-
64
- <!-- vigiles:result role:build -->