vigiles 2.1.1 → 2.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.
package/README.md CHANGED
@@ -40,7 +40,7 @@ Reads fine. Four things are wrong:
40
40
  3. `npm run typecheck` — script removed from package.json
41
41
  4. Service/test pairing — no automated check, just a hope
42
42
 
43
- The agent reads this, trusts it, and writes code based on stale claims nobody verified. vigiles **verifies the references in your instruction files** — that each linter rule exists and is enabled, that every file path and script is real — and meets you at whatever commitment level you want.
43
+ The agent reads this, trusts it, and writes code based on stale claims nobody verified. vigiles **verifies the references in your instruction files** — that each linter rule exists and is enabled, that every file path and script is real, and that referenced **code symbols** (functions, classes, constants) actually exist in the files that define them — and meets you at whatever commitment level you want.
44
44
 
45
45
  Three levels. Each is independently useful; adopt as far up as you like.
46
46
 
@@ -210,19 +210,20 @@ Same monotonicity guarantees as `enforce()` — guards can't be silently removed
210
210
 
211
211
  ## Verified References
212
212
 
213
- `file()`, `cmd()`, and `ref()` catch stale references at compile time:
213
+ `file()`, `cmd()`, `symbol()`, and `ref()` catch stale references at compile time:
214
214
 
215
215
  ```typescript
216
- import { claude, file, cmd, ref, instructions } from "vigiles/spec";
216
+ import { claude, file, cmd, symbol, ref, instructions } from "vigiles/spec";
217
217
 
218
218
  export default claude({
219
219
  sections: {
220
220
  architecture: instructions`
221
221
  Core engine in ${file("src/compile.ts")}.
222
+ Compile specs with ${symbol("src/compile.ts", "compileClaude")}.
222
223
  Run ${cmd("npm test")} to verify.
223
224
  See ${ref("skills/strengthen/SKILL.md")} for the strengthen skill.
224
225
  `,
225
- // If any path is stale → compile error
226
+ // If any path / script / symbol is stale → compile error
226
227
  },
227
228
  // ...
228
229
  });
@@ -230,6 +231,12 @@ export default claude({
230
231
 
231
232
  Skill specs use the same helpers for verified references inside instructions. [Full spec format →](docs/spec-format.md)
232
233
 
234
+ ### Symbol references (cross-language)
235
+
236
+ `symbol("file", "name")` (and the markdown mark `` `vigiles:symbol file#name` ``) verify that the named file actually **defines** the symbol — a function, class, method, or constant — parsed with [ast-grep](https://ast-grep.github.io) across **JS/TS, Python, Ruby, Rust, and CSS**. Rename the function and `audit` fails; no project-wide index, no autoloader guessing — it parses the one named file.
237
+
238
+ In markdown mode the `refs-hook` (PostToolUse) **forces the mark**: it blocks an edit that leaves a code reference bare, telling the agent to write `` `vigiles:symbol path#name` `` or opt out with `<!-- vigiles:ignore -->`. The harness makes the agent mark its references at write time, with full context; `audit` re-verifies them. [Symbol verification →](research/symbol-verification.md)
239
+
233
240
  ## Type-Safe Rule References
234
241
 
235
242
  `vigiles generate-types` scans your linter configs and emits `.vigiles/generated.d.ts`. With this file, `enforce("eslint/no-consolee")` is a red squiggle in your editor — a typo caught at authoring time, not a runtime surprise. Without it, everything falls back to broad types and still works.
@@ -249,7 +256,8 @@ For markdown frontmatter (Level 1), `vigiles generate-schema` gives the same aut
249
256
  ```bash
250
257
  npx vigiles init [--target=X.md] # Scaffold a spec (runs full setup wizard by default)
251
258
  npx vigiles compile [files...] # Compile .spec.ts → .md
252
- npx vigiles audit [files...] # Verify hashes + inline/frontmatter/spec rules + coverage
259
+ npx vigiles audit [files...] # Verify hashes + inline/frontmatter/spec rules + symbols + coverage
260
+ npx vigiles refs <file.md> # Check the symbol references in an instruction file
253
261
  npx vigiles generate-types # Emit .d.ts from project state (for spec mode)
254
262
  npx vigiles generate-types --check # Verify .d.ts is up to date
255
263
  npx vigiles generate-schema # Emit JSON Schema for vigiles: frontmatter (Level 1)
@@ -323,6 +331,69 @@ Install with [Vercel Skills](https://github.com/vercel-labs/skills): `npx skills
323
331
  | `enforce-rules-format` | Validate all rules have enforcement classification |
324
332
  | `audit-feedback-loop` | Score your repo's feedback loop maturity |
325
333
 
334
+ ## Test your Claude Code harness
335
+
336
+ vigiles also ships a library for **testing the harness itself** — your hooks,
337
+ settings, skills, and instruction files. `Agent = Model + Harness`; this tests
338
+ the harness, at two levels.
339
+
340
+ **Evals — does my change actually move agent behaviour?** Define a fixture, a set
341
+ of **arms** (a hook on vs off, with/without a CLAUDE.md rule), a task, and a
342
+ metric; `runEval` drives the real `claude` CLI N trials per arm and aggregates.
343
+
344
+ ```typescript
345
+ import { runEval, formatEvalReport } from "vigiles/eval";
346
+
347
+ const report = await runEval({
348
+ fixture: { "src/billing.ts": "export function chargeCard() {}" },
349
+ arms: {
350
+ vanilla: {},
351
+ gated: { settings: { hooks: { PostToolUse: [refsHook] } } },
352
+ },
353
+ task: "Document chargeCard in SKILL.md, referencing it by name.",
354
+ measure: (ctx) => ({
355
+ marked: ctx.sh("grep -c vigiles:symbol SKILL.md") !== "0",
356
+ }),
357
+ trials: 6,
358
+ });
359
+ console.log(formatEvalReport(report)); // vanilla marked=0.00 gated marked=0.50
360
+ ```
361
+
362
+ **Deterministic tests — does my hook fire correctly?** No API key, no cost.
363
+ `runHarnessTest` runs real `claude` against a **scripted mock model**
364
+ (`vigiles/mock-model`), so your real hooks fire but the agent's turns are fixed.
365
+
366
+ ```typescript
367
+ import { runHarnessTest, scriptModel } from "vigiles/harness-test";
368
+
369
+ const r = await runHarnessTest({
370
+ settings: {
371
+ hooks: {
372
+ Stop: [
373
+ {
374
+ hooks: [
375
+ {
376
+ type: "command",
377
+ command: "test -f DONE || { echo 'not done' >&2; exit 2; }",
378
+ },
379
+ ],
380
+ },
381
+ ],
382
+ },
383
+ },
384
+ model: scriptModel([
385
+ { text: "I'm done" }, // tries to stop → blocked
386
+ { tool: "Bash", input: { command: "touch DONE" } },
387
+ { text: "now done" },
388
+ ]),
389
+ });
390
+ assert(JSON.parse(r.stdout).num_turns > 1); // the Stop hook forced more work
391
+ ```
392
+
393
+ The deterministic tier is reliable for **Stop hooks**; tool-event hooks
394
+ (Edit/Write) are headless-gated, so test those via the eval tier. Our own
395
+ findings from this harness live in [`research/benchmarks-runtime-gates.md`](research/benchmarks-runtime-gates.md).
396
+
326
397
  ## Maturity Levels
327
398
 
328
399
  From [Feedback Loop Is All You Need](https://zernie.com/blog/feedback-loop-is-all-you-need):
@@ -0,0 +1,28 @@
1
+ import { type RuntimeGate } from "./skill-runtime.js";
2
+ export interface ActionGate {
3
+ /** Tool name to gate, e.g. "Write" | "Edit" | "Bash". */
4
+ readonly on: string;
5
+ /** Deterministic check; a `cmd` command may include `{file}`. */
6
+ readonly gate: RuntimeGate;
7
+ /** Optional substring the (JSON-serialized) tool input must contain. */
8
+ readonly when?: string;
9
+ }
10
+ export interface ActionEvent {
11
+ /** The tool that just ran (PostToolUse `tool_name`). */
12
+ readonly tool: string;
13
+ /** The tool input (`tool_input`), e.g. `{ file_path, command }`. */
14
+ readonly input?: Record<string, unknown>;
15
+ }
16
+ export interface ActionDecision {
17
+ readonly allow: boolean;
18
+ readonly message: string;
19
+ }
20
+ /**
21
+ * Evaluate action gates against a tool event. Runs every gate whose `on`
22
+ * matches the tool (and whose `when` substring matches the input); the first
23
+ * failure blocks. Plan-agnostic — order in any runtime workflow is irrelevant.
24
+ */
25
+ export declare function evaluateAction(event: ActionEvent, gates: readonly ActionGate[], cwd: string): ActionDecision;
26
+ /** Load action gates from `.vigiles/action-gates.json`. */
27
+ export declare function loadActionGates(cwd: string): ActionGate[];
28
+ //# sourceMappingURL=action-gate.d.ts.map
@@ -0,0 +1,73 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.evaluateAction = evaluateAction;
4
+ exports.loadActionGates = loadActionGates;
5
+ /**
6
+ * vigiles — Action gates (the dynamic-workflow reframe).
7
+ *
8
+ * A skill gate is bound to a *step* (a fixed position in a plan). When the plan
9
+ * is generated at runtime (dynamic workflows), the step is the wrong unit. An
10
+ * **action gate** binds a deterministic check to a *tool action type* instead —
11
+ * "any time a Write happens to a `.ts` file, eslint must pass on it" — so it
12
+ * fires regardless of where in the runtime plan the action occurs.
13
+ *
14
+ * It is the same deterministic gate primitive (reuses `runGate` + the
15
+ * author-time reference resolution), re-anchored from step → action. Delivered
16
+ * as a PostToolUse hook (`vigiles action-hook`): exit 2 blocks the action and
17
+ * feeds the reason back, exit 0 allows it.
18
+ *
19
+ * Config: `.vigiles/action-gates.json` → `{ "gates": [ { on, gate, when? } ] }`.
20
+ * The gate command may contain `{file}`, substituted with the action's path.
21
+ */
22
+ const node_fs_1 = require("node:fs");
23
+ const node_path_1 = require("node:path");
24
+ const skill_runtime_js_1 = require("./skill-runtime.js");
25
+ /** The file path an action touched, for `{file}` substitution. */
26
+ function fileOf(event) {
27
+ const i = event.input ?? {};
28
+ const v = i.file_path ?? i.path;
29
+ return typeof v === "string" ? v : "";
30
+ }
31
+ /** Substitute `{file}` in a cmd gate with the action's path. */
32
+ function resolveGate(gate, event) {
33
+ if (gate.kind !== "cmd" || !gate.command.includes("{file}"))
34
+ return gate;
35
+ return { ...gate, command: gate.command.replaceAll("{file}", fileOf(event)) };
36
+ }
37
+ /**
38
+ * Evaluate action gates against a tool event. Runs every gate whose `on`
39
+ * matches the tool (and whose `when` substring matches the input); the first
40
+ * failure blocks. Plan-agnostic — order in any runtime workflow is irrelevant.
41
+ */
42
+ function evaluateAction(event, gates, cwd) {
43
+ const inputStr = JSON.stringify(event.input ?? "");
44
+ for (const g of gates) {
45
+ if (g.on !== event.tool)
46
+ continue;
47
+ if (g.when && !inputStr.includes(g.when))
48
+ continue;
49
+ const outcome = (0, skill_runtime_js_1.runGate)(resolveGate(g.gate, event), cwd);
50
+ if (!outcome.ok) {
51
+ const tail = outcome.output ? `\n${outcome.output}` : "";
52
+ return {
53
+ allow: false,
54
+ message: `Action gate failed after ${event.tool}: ${(0, skill_runtime_js_1.gateLabel)(g.gate)} did not pass.${tail}`,
55
+ };
56
+ }
57
+ }
58
+ return { allow: true, message: "" };
59
+ }
60
+ /** Load action gates from `.vigiles/action-gates.json`. */
61
+ function loadActionGates(cwd) {
62
+ const p = (0, node_path_1.resolve)(cwd, ".vigiles/action-gates.json");
63
+ if (!(0, node_fs_1.existsSync)(p))
64
+ return [];
65
+ try {
66
+ const parsed = JSON.parse((0, node_fs_1.readFileSync)(p, "utf-8"));
67
+ return Array.isArray(parsed.gates) ? parsed.gates : [];
68
+ }
69
+ catch {
70
+ return [];
71
+ }
72
+ }
73
+ //# sourceMappingURL=action-gate.js.map