vigiles 6.0.0 → 8.0.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 (92) hide show
  1. package/README.md +189 -88
  2. package/dist/action-gate.js +1 -1
  3. package/dist/adapters/claude-code/agent-runtime.d.ts +46 -11
  4. package/dist/adapters/claude-code/agent-runtime.js +95 -24
  5. package/dist/adapters/claude-code/effect-region.js +1 -1
  6. package/dist/adapters/claude-code/skill-runtime.d.ts +1 -1
  7. package/dist/adapters/claude-code/skill-runtime.js +1 -1
  8. package/dist/adapters/codex/hook-protocol.js +3 -0
  9. package/dist/adapters/codex/mock-model.js +1 -1
  10. package/dist/cli-commands.d.ts +19 -0
  11. package/dist/cli-commands.js +47 -0
  12. package/dist/cli.d.ts +1 -1
  13. package/dist/cli.js +1054 -201
  14. package/dist/core/adopt.d.ts +65 -0
  15. package/dist/core/adopt.js +199 -0
  16. package/dist/core/bash-effects.d.ts +12 -0
  17. package/dist/core/bash-effects.js +31 -0
  18. package/dist/core/capability-diff.d.ts +46 -0
  19. package/dist/core/capability-diff.js +97 -0
  20. package/dist/core/compose.d.ts +1 -1
  21. package/dist/core/compose.js +1 -1
  22. package/dist/core/evolve.d.ts +4 -0
  23. package/dist/core/evolve.js +4 -0
  24. package/dist/core/frontmatter.d.ts +8 -7
  25. package/dist/core/frontmatter.js +8 -7
  26. package/dist/core/generate-harness.d.ts +1 -1
  27. package/dist/core/generate-harness.js +3 -3
  28. package/dist/core/generate-schema.js +1 -1
  29. package/dist/core/guards.d.ts +126 -0
  30. package/dist/core/guards.js +309 -0
  31. package/dist/core/harness-driver.d.ts +1 -1
  32. package/dist/core/hook-program.d.ts +459 -0
  33. package/dist/core/hook-program.js +468 -0
  34. package/dist/core/hook-protocol.d.ts +7 -0
  35. package/dist/core/hook-providers.d.ts +138 -0
  36. package/dist/core/hook-providers.js +155 -0
  37. package/dist/core/hook-spec.d.ts +74 -0
  38. package/dist/core/hook-spec.js +130 -0
  39. package/dist/core/inline.d.ts +6 -6
  40. package/dist/core/inline.js +7 -7
  41. package/dist/core/integrity.d.ts +31 -0
  42. package/dist/core/integrity.js +45 -0
  43. package/dist/core/mcp-tool.d.ts +12 -0
  44. package/dist/core/mcp-tool.js +20 -0
  45. package/dist/core/mcp.d.ts +13 -0
  46. package/dist/core/mcp.js +67 -0
  47. package/dist/core/orphans.js +1 -1
  48. package/dist/core/spec.d.ts +40 -2
  49. package/dist/core/spec.js +16 -1
  50. package/dist/core/types.d.ts +37 -5
  51. package/dist/core/validate.js +26 -26
  52. package/dist/dialect-drift.d.ts +65 -0
  53. package/dist/dialect-drift.js +216 -0
  54. package/dist/eval.d.ts +40 -5
  55. package/dist/eval.js +59 -5
  56. package/dist/guardrail-check.d.ts +85 -0
  57. package/dist/guardrail-check.js +152 -0
  58. package/dist/harness-assert.d.ts +10 -0
  59. package/dist/harness-assert.js +30 -0
  60. package/dist/hook-install.d.ts +43 -0
  61. package/dist/hook-install.js +91 -0
  62. package/dist/hook.d.ts +52 -0
  63. package/dist/hook.js +98 -0
  64. package/dist/leaderboard.d.ts +6 -0
  65. package/dist/leaderboard.js +43 -1
  66. package/dist/linting.d.ts +9 -5
  67. package/dist/linting.js +17 -5
  68. package/dist/optimize.js +1 -1
  69. package/dist/scaffold-test.js +21 -7
  70. package/dist/scan-behavioral.d.ts +60 -0
  71. package/dist/scan-behavioral.js +239 -1
  72. package/dist/scan-trigger-suggest.d.ts +54 -0
  73. package/dist/scan-trigger-suggest.js +70 -0
  74. package/dist/scan.d.ts +31 -1
  75. package/dist/scan.js +65 -3
  76. package/dist/score-explainer.js +1 -1
  77. package/dist/self-command-refs.d.ts +21 -0
  78. package/dist/self-command-refs.js +125 -0
  79. package/dist/setup-plan.d.ts +59 -1
  80. package/dist/setup-plan.js +103 -5
  81. package/dist/testing.d.ts +5 -3
  82. package/dist/testing.js +37 -23
  83. package/dist/tool-intercept.d.ts +4 -4
  84. package/dist/tool-intercept.js +5 -5
  85. package/dist/unit.d.ts +2 -0
  86. package/dist/unit.js +8 -1
  87. package/hooks/post-edit.sh +1 -1
  88. package/hooks/refs-nudge.sh +1 -1
  89. package/package.json +5 -3
  90. package/skills/adopt-spec/SKILL.md +7 -7
  91. package/skills/linter-docs/eslint.md +1 -1
  92. package/skills/strengthen/SKILL.md +1 -1
@@ -0,0 +1,125 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.findStaleCommandRefs = findStaleCommandRefs;
4
+ /**
5
+ * Self-command-reference verification — the cross-reference moat applied to
6
+ * vigiles's OWN docs. vigiles verifies that a CLAUDE.md's commands resolve; its
7
+ * own docs cite ITS OWN commands, and nothing checked those until a rename left
8
+ * `vigiles compile-hook` / `vigiles run-skill` rotting in the docs. This is the
9
+ * deterministic gate: every `vigiles <cmd>` reference in a doc/comment must
10
+ * resolve to a real command (a VERB or a `hook-runtime <kind>`).
11
+ *
12
+ * HIGH-PRECISION by construction (don't-cry-wolf). A reference is inspected only
13
+ * when it's an unambiguous COMMAND — i.e. it sits inside an inline `` `span` ``,
14
+ * inside a ```shell fence```, or is prefixed `npx vigiles` / `Usage: vigiles` /
15
+ * is a `cli.js` or `${CLI}` invocation (the harness-test convention `node ${CLI}
16
+ * <cmd>`). Prose ("vigiles compiles the spec", "adds vigiles to devDependencies")
17
+ * and non-shell fences (```text agent prompts, ```ts) are never matched. A bare unknown VERB is flagged only when hyphenated (every
18
+ * renamed vigiles command is — `compile-hook`, `run-skill`) or in an explicit
19
+ * invocation; `hook-runtime <kind>` is checked in any command context. Source of
20
+ * truth: {@link VERBS} / {@link HOOK_RUNTIME_KINDS}.
21
+ *
22
+ * KNOWN LIMITATION (measured, deliberately not "fixed"): this catches the
23
+ * `vigiles <cmd>` / `cli.js <cmd>` INVOCATION form. It does NOT flag a BARE
24
+ * command name (`agent-hook` without the `vigiles ` prefix) nor a bare file path
25
+ * (`src/foo.ts`), because both collide with legitimate non-references: bare
26
+ * command names double as CONCEPT names (`the refs-hook nudge` appears as a
27
+ * feature name in ~10 files; `agent-hook` as a rail name + in test labels +
28
+ * `makeTmpDir("agent-hook-cli")`), and bare paths collide with ILLUSTRATIVE
29
+ * examples (the README's `src/auth/login.ts`, `docs/foo.md`) and test fixtures
30
+ * (`session.test.ts` builds fake `src/compile.ts` diffs). A denylist over either
31
+ * would cry wolf on dozens of valid usages — so terminology/path-accuracy stays
32
+ * the JUDGMENT half the `doc-consistency` rule assigns to discipline, not a check.
33
+ */
34
+ const cli_commands_js_1 = require("./cli-commands.js");
35
+ const DEFAULT_KNOWN = {
36
+ verbs: cli_commands_js_1.VERBS,
37
+ kinds: cli_commands_js_1.HOOK_RUNTIME_KINDS,
38
+ };
39
+ const TOKEN = "[a-z][a-z0-9-]*";
40
+ // `[npx ][vigiles|cli.js|${CLI}] <verb> [<kind>]` — capture the optional prefix +
41
+ // literal. `${CLI}` is the harness-test convention (`const CLI = new
42
+ // URL("…/dist/cli.js")` → `node ${CLI} <cmd>`); without it a stale runtime ref in
43
+ // a `.mjs`/`.test.ts` invocation slips through (it did: refs-nudge.harness.mjs kept
44
+ // calling `refs-hook` after the hook-runtime rename). `\b` can't anchor `${CLI}`
45
+ // (it starts with `$`, a non-word char), so each word literal carries its own `\b`.
46
+ const INVOKE = new RegExp(String.raw `(npx |Usage: )?(\bvigiles|\bcli\.js|\$\{CLI\})\s+(${TOKEN})(?:\s+(${TOKEN}))?`, "g");
47
+ // A bare `hook-runtime <kind>` (e.g. `node ${CLI} hook-runtime agent` in a test).
48
+ const KIND = new RegExp(String.raw `hook-runtime\s+(${TOKEN})`, "g");
49
+ const SHELL_FENCE = /^\s*```(?:bash|sh|shell|zsh|console|shell-session)\s*$/;
50
+ const FENCE = /^\s*```/;
51
+ /** Inclusive index ranges of inline `code spans` in a line. */
52
+ function codeSpans(line) {
53
+ const ranges = [];
54
+ for (const m of line.matchAll(/`[^`\n]+`/g))
55
+ ranges.push([m.index, m.index + m[0].length]);
56
+ return ranges;
57
+ }
58
+ const inSpan = (i, spans) => spans.some(([a, b]) => i >= a && i < b);
59
+ function scanLine(line, inShellFence, known) {
60
+ const spans = inShellFence ? null : codeSpans(line);
61
+ const ctx = (i) => inShellFence || inSpan(i, spans ?? []);
62
+ const out = [];
63
+ const seen = new Set();
64
+ const push = (ref, reason) => {
65
+ const key = ref + "\0" + reason;
66
+ if (!seen.has(key)) {
67
+ seen.add(key);
68
+ out.push({ ref, reason });
69
+ }
70
+ };
71
+ for (const m of line.matchAll(INVOKE)) {
72
+ const explicit = inShellFence || Boolean(m[1]) || m[2] === "cli.js" || m[2] === "${CLI}";
73
+ if (!explicit && !ctx(m.index))
74
+ continue;
75
+ const verb = m[3];
76
+ if (verb === "hook-runtime") {
77
+ if (m[4] !== undefined && !known.kinds.includes(m[4]))
78
+ push(`hook-runtime ${m[4]}`, `unknown hook-runtime kind "${m[4]}"`);
79
+ }
80
+ else if (!known.verbs.includes(verb) &&
81
+ (verb.includes("-") || explicit)) {
82
+ push(`${m[2]} ${verb}`, `unknown/removed command "${verb}"`);
83
+ }
84
+ }
85
+ // Bare `hook-runtime <kind>` (no vigiles/cli.js literal) inside a command context.
86
+ for (const m of line.matchAll(KIND)) {
87
+ if (!ctx(m.index))
88
+ continue;
89
+ if (!known.kinds.includes(m[1]))
90
+ push(`hook-runtime ${m[1]}`, `unknown hook-runtime kind "${m[1]}"`);
91
+ }
92
+ return out;
93
+ }
94
+ /**
95
+ * Find stale/unknown vigiles command references across the given files. Pure —
96
+ * the caller supplies file contents (so it works over the repo in a test, or any
97
+ * file set).
98
+ */
99
+ function findStaleCommandRefs(files, known = DEFAULT_KNOWN) {
100
+ const issues = [];
101
+ for (const { path, content } of files) {
102
+ let inAnyFence = false;
103
+ let inShellFence = false;
104
+ const lines = content.split("\n");
105
+ for (let i = 0; i < lines.length; i++) {
106
+ const line = lines[i];
107
+ if (FENCE.test(line)) {
108
+ if (inAnyFence) {
109
+ inAnyFence = inShellFence = false;
110
+ }
111
+ else {
112
+ inAnyFence = true;
113
+ inShellFence = SHELL_FENCE.test(line);
114
+ }
115
+ continue;
116
+ }
117
+ if (line.includes("vigiles:ignore-cmd"))
118
+ continue;
119
+ for (const f of scanLine(line, inShellFence, known))
120
+ issues.push({ file: path, line: i + 1, ref: f.ref, reason: f.reason });
121
+ }
122
+ }
123
+ return issues;
124
+ }
125
+ //# sourceMappingURL=self-command-refs.js.map
@@ -27,6 +27,10 @@ export interface SetupPlan {
27
27
  export interface ParsedSetupArgs {
28
28
  target?: string;
29
29
  strict: boolean;
30
+ /** `--report-only` — write the gating rules at "warn" (nothing fails CI). The
31
+ * orthogonal severity dial; composes with `--strict` (which rules) by setting
32
+ * their severity. */
33
+ reportOnly: boolean;
30
34
  yes: boolean;
31
35
  /** `--force` — rewrite a stale CI workflow in place. */
32
36
  force: boolean;
@@ -51,9 +55,52 @@ export declare function defaultPlan(strict?: boolean): SetupPlan;
51
55
  * changed (so the IO layer skips the write). The IO (read/parse/write + the
52
56
  * malformed-file guard) stays in cli.ts.
53
57
  */
58
+ /**
59
+ * The structural rules `init` gates BY DEFAULT (severity `error`, so a broken
60
+ * surface fails `vigiles lint`). Every one is HIGH-PRECISION / FP-safe — it fires
61
+ * only on a genuine defect (a never-available/typo'd tool, a subagent missing
62
+ * `name`/`description`, a typo'd hook event, a dead hook script, a broken MCP
63
+ * ref, two skills that collide in the selector) — so a well-formed plugin stays
64
+ * green and catching real breakage out of the box never cries wolf.
65
+ *
66
+ * Deliberately EXCLUDES `require-instructions-spec` and the workflow-forcing rules:
67
+ * those make a CLEAN repo fail (you simply haven't written the spec/test yet), so
68
+ * they stay opt-in under `--strict` (progressive adoption — see
69
+ * `STRICT_EXTRA_RULES`).
70
+ *
71
+ * This is the **`structural`** rule group (see research/install-enforcement-dx.md).
72
+ */
73
+ export declare const STRUCTURAL_RULES: readonly ["subagent-tool-contract", "subagent-frontmatter", "hook-events", "hook-script-exists", "mcp-config", "mcp-tool-resolves", "mcp-hook-target-resolves", "disallowed-tools-contract", "description-overlap"];
74
+ /**
75
+ * The **`workflow`** group — the WORKFLOW-FORCING / opinionated tier `--strict`
76
+ * gates, which a clean repo can still fail because you haven't done the work yet:
77
+ * a spec per instruction file (`require-instructions-spec`), a test/eval per
78
+ * surface (`untested-*`). Opt-in by design (the smooth-adoption on-ramp). The
79
+ * Clippy-`pedantic` / TS-`strict` analog — ONE opinionated opt-in.
80
+ *
81
+ * NB `frontmatter-valid` / `skill-frontmatter` live in the `nudge` group, not
82
+ * here: they're acknowledged-noisy recommendations we never gate on (see
83
+ * research/install-enforcement-dx.md).
84
+ */
85
+ export declare const WORKFLOW_RULES: readonly ["require-instructions-spec", "untested-skill", "untested-subagent", "untested-hook"];
86
+ /**
87
+ * The **`nudge`** group — recommendations / acknowledged-noisy checks that NEVER
88
+ * gate (not even under `--strict`): `frontmatter-valid` (js-yaml is stricter than
89
+ * CC's loader), `skill-frontmatter` (skills load without it) and `unmarked-refs`
90
+ * (the undecidable-plaintext nudge) sit at `warn`; `prefer-compiled-hooks` defaults
91
+ * OFF (a recommendation that shouldn't fire unasked — the shell lane stays
92
+ * first-class). `init` does not write these — they keep their own default
93
+ * severities. Named for the group taxonomy (research/install-enforcement-dx.md).
94
+ */
95
+ export declare const NUDGE_RULES: readonly ["frontmatter-valid", "skill-frontmatter", "prefer-compiled-hooks", "unmarked-refs"];
54
96
  export declare function mergeProjectConfig(existing: Record<string, unknown>, opts: {
55
97
  harness: string | string[];
56
98
  strict: boolean;
99
+ reportOnly?: boolean;
100
+ /** Whether the LINT pillar is on (default true). The rule gate is a lint-layer
101
+ * concern, so a test-only setup (`init --test` / `--no-lint`) records the
102
+ * harness but writes NO lint rules. */
103
+ lint?: boolean;
57
104
  }): Record<string, unknown> | null;
58
105
  /**
59
106
  * Whether to drop into interactive prompts: a human at a TTY who passed neither
@@ -62,7 +109,18 @@ export declare function mergeProjectConfig(existing: Record<string, unknown>, op
62
109
  */
63
110
  export declare function shouldPrompt(parsed: ParsedSetupArgs, isTTY: boolean): boolean;
64
111
  /** Interactive answers (only the fields the prompts cover). */
65
- export type SetupAnswers = Partial<Pick<SetupPlan, "lint" | "test" | "gha" | "plugin">>;
112
+ export type SetupAnswers = Partial<Pick<SetupPlan, "lint" | "test" | "gha" | "plugin" | "strict">>;
113
+ /** Ask one question with a default — injected so the interactive Q&A is pure +
114
+ * unit-testable (a fake `ask` scripts answers; no TTY, no readline). */
115
+ export type AskFn = (question: string, def: string) => Promise<string>;
116
+ /**
117
+ * The interactive setup Q&A as PURE logic over an injected `ask` — the prompts,
118
+ * their defaults, and the answer→`SetupAnswers` mapping. The IO shell (readline)
119
+ * lives in `cli.ts`'s `promptSetup`, which just supplies a real `ask`. Keeping
120
+ * this here means the fragile interactive path is unit-tested deterministically
121
+ * (the questions can't silently break) without a terminal.
122
+ */
123
+ export declare function collectSetupAnswers(ask: AskFn): Promise<SetupAnswers>;
66
124
  /**
67
125
  * How to install vigiles's skills/hooks for ONE harness — the deterministic
68
126
  * decision behind the IO in cli.ts, so a CI test asserts WHICH commands an
@@ -10,10 +10,12 @@
10
10
  * on a prompt. See docs/agent-setup.md.
11
11
  */
12
12
  Object.defineProperty(exports, "__esModule", { value: true });
13
+ exports.NUDGE_RULES = exports.WORKFLOW_RULES = exports.STRUCTURAL_RULES = void 0;
13
14
  exports.parseSetupArgs = parseSetupArgs;
14
15
  exports.defaultPlan = defaultPlan;
15
16
  exports.mergeProjectConfig = mergeProjectConfig;
16
17
  exports.shouldPrompt = shouldPrompt;
18
+ exports.collectSetupAnswers = collectSetupAnswers;
17
19
  exports.planPluginInstall = planPluginInstall;
18
20
  exports.resolvePlan = resolvePlan;
19
21
  function flagValue(args, prefix) {
@@ -33,6 +35,7 @@ function parseSetupArgs(args) {
33
35
  return {
34
36
  target: flagValue(args, "--target="),
35
37
  strict: args.includes("--strict"),
38
+ reportOnly: args.includes("--report-only"),
36
39
  yes: args.includes("--yes") || args.includes("-y"),
37
40
  force: args.includes("--force"),
38
41
  lint: boolFlag(args, "lint"),
@@ -60,6 +63,64 @@ function defaultPlan(strict = false) {
60
63
  * changed (so the IO layer skips the write). The IO (read/parse/write + the
61
64
  * malformed-file guard) stays in cli.ts.
62
65
  */
66
+ /**
67
+ * The structural rules `init` gates BY DEFAULT (severity `error`, so a broken
68
+ * surface fails `vigiles lint`). Every one is HIGH-PRECISION / FP-safe — it fires
69
+ * only on a genuine defect (a never-available/typo'd tool, a subagent missing
70
+ * `name`/`description`, a typo'd hook event, a dead hook script, a broken MCP
71
+ * ref, two skills that collide in the selector) — so a well-formed plugin stays
72
+ * green and catching real breakage out of the box never cries wolf.
73
+ *
74
+ * Deliberately EXCLUDES `require-instructions-spec` and the workflow-forcing rules:
75
+ * those make a CLEAN repo fail (you simply haven't written the spec/test yet), so
76
+ * they stay opt-in under `--strict` (progressive adoption — see
77
+ * `STRICT_EXTRA_RULES`).
78
+ *
79
+ * This is the **`structural`** rule group (see research/install-enforcement-dx.md).
80
+ */
81
+ exports.STRUCTURAL_RULES = [
82
+ "subagent-tool-contract",
83
+ "subagent-frontmatter",
84
+ "hook-events",
85
+ "hook-script-exists",
86
+ "mcp-config",
87
+ "mcp-tool-resolves",
88
+ "mcp-hook-target-resolves",
89
+ "disallowed-tools-contract",
90
+ "description-overlap",
91
+ ];
92
+ /**
93
+ * The **`workflow`** group — the WORKFLOW-FORCING / opinionated tier `--strict`
94
+ * gates, which a clean repo can still fail because you haven't done the work yet:
95
+ * a spec per instruction file (`require-instructions-spec`), a test/eval per
96
+ * surface (`untested-*`). Opt-in by design (the smooth-adoption on-ramp). The
97
+ * Clippy-`pedantic` / TS-`strict` analog — ONE opinionated opt-in.
98
+ *
99
+ * NB `frontmatter-valid` / `skill-frontmatter` live in the `nudge` group, not
100
+ * here: they're acknowledged-noisy recommendations we never gate on (see
101
+ * research/install-enforcement-dx.md).
102
+ */
103
+ exports.WORKFLOW_RULES = [
104
+ "require-instructions-spec",
105
+ "untested-skill",
106
+ "untested-subagent",
107
+ "untested-hook",
108
+ ];
109
+ /**
110
+ * The **`nudge`** group — recommendations / acknowledged-noisy checks that NEVER
111
+ * gate (not even under `--strict`): `frontmatter-valid` (js-yaml is stricter than
112
+ * CC's loader), `skill-frontmatter` (skills load without it) and `unmarked-refs`
113
+ * (the undecidable-plaintext nudge) sit at `warn`; `prefer-compiled-hooks` defaults
114
+ * OFF (a recommendation that shouldn't fire unasked — the shell lane stays
115
+ * first-class). `init` does not write these — they keep their own default
116
+ * severities. Named for the group taxonomy (research/install-enforcement-dx.md).
117
+ */
118
+ exports.NUDGE_RULES = [
119
+ "frontmatter-valid",
120
+ "skill-frontmatter",
121
+ "prefer-compiled-hooks",
122
+ "unmarked-refs",
123
+ ];
63
124
  function mergeProjectConfig(existing, opts) {
64
125
  const config = { ...existing };
65
126
  let changed = false;
@@ -67,13 +128,23 @@ function mergeProjectConfig(existing, opts) {
67
128
  config.harness = opts.harness;
68
129
  changed = true;
69
130
  }
70
- if (opts.strict) {
131
+ // The rule gate belongs to the LINT layer — a test-only setup records the
132
+ // harness but writes no rules (honoring the positive-flag contract that
133
+ // `--test` selects only the test pillar).
134
+ if (opts.lint !== false) {
135
+ // Gate the FP-safe `structural` group by default; `--strict` adds the
136
+ // `workflow` group on top. `--report-only` is the orthogonal severity dial —
137
+ // it writes the SAME rule set at "warn" (nothing fails CI; the
138
+ // migration/observe mode). Never clobber a severity the user already set —
139
+ // only fill the undefined ones.
140
+ const severity = opts.reportOnly ? "warn" : "error";
141
+ const gate = opts.strict
142
+ ? [...exports.STRUCTURAL_RULES, ...exports.WORKFLOW_RULES]
143
+ : [...exports.STRUCTURAL_RULES];
71
144
  const rules = { ...config.rules };
72
- // `require-skill-spec` is deprecated (skills can be hand-written), so --strict
73
- // no longer promotes it; it tightens only `require-spec` (instruction files).
74
- for (const r of ["require-spec"]) {
145
+ for (const r of gate) {
75
146
  if (rules[r] === undefined) {
76
- rules[r] = "error";
147
+ rules[r] = severity;
77
148
  changed = true;
78
149
  }
79
150
  }
@@ -93,6 +164,31 @@ function shouldPrompt(parsed, isTTY) {
93
164
  const allPinned = pillarsPinned && parsed.gha !== undefined && parsed.plugin !== undefined;
94
165
  return !allPinned;
95
166
  }
167
+ const isYesAnswer = (s) => /^y(es)?$/i.test(s);
168
+ /**
169
+ * The interactive setup Q&A as PURE logic over an injected `ask` — the prompts,
170
+ * their defaults, and the answer→`SetupAnswers` mapping. The IO shell (readline)
171
+ * lives in `cli.ts`'s `promptSetup`, which just supplies a real `ask`. Keeping
172
+ * this here means the fragile interactive path is unit-tested deterministically
173
+ * (the questions can't silently break) without a terminal.
174
+ */
175
+ async function collectSetupAnswers(ask) {
176
+ const pillars = (await ask("Set up which pillars? [both/lint/test] (both): ", "both")).toLowerCase();
177
+ const gha = isYesAnswer(await ask("Wire CI (GitHub Action)? [Y/n]: ", "y"));
178
+ const plugin = isYesAnswer(await ask("Install the Claude Code plugin (hooks + skills)? [Y/n]: ", "y"));
179
+ // Structural gating (broken tools/hooks/MCP/collisions) is always on. This asks
180
+ // about the WORKFLOW tier — a spec per file + a test per surface — which a clean
181
+ // repo can fail just for not having done the work yet, so it's the recommended
182
+ // default a human opts OUT of (never forced on a silent run).
183
+ const strict = isYesAnswer(await ask("Also enforce specs + a test per surface (recommended)? [Y/n]: ", "y"));
184
+ return {
185
+ lint: pillars !== "test",
186
+ test: pillars !== "lint" && pillars !== "verify",
187
+ gha,
188
+ plugin,
189
+ strict,
190
+ };
191
+ }
96
192
  /**
97
193
  * Apply the pillar flags. A positive flag (`--lint` and/or `--test`) is an
98
194
  * explicit SELECTION — enable exactly the named pillars. Otherwise default to
@@ -118,6 +214,8 @@ function applyAnswers(plan, answers) {
118
214
  plan.gha = answers.gha;
119
215
  if (answers.plugin !== undefined)
120
216
  plan.plugin = answers.plugin;
217
+ if (answers.strict !== undefined)
218
+ plan.strict = answers.strict;
121
219
  }
122
220
  /** Per-harness install plan. `hasClaude` gates the auto-run `claude plugin` CLI
123
221
  * (else the same two steps are printed as `/plugin` slash commands).
package/dist/testing.d.ts CHANGED
@@ -10,13 +10,15 @@
10
10
  * boundary forbids importing `src/adapters/*` from here. See
11
11
  * `research/adapter-api-design.md`.
12
12
  */
13
- export * from "./run-hook.js";
14
- export * from "./eval.js";
13
+ export { runHook, propertyHook } from "./run-hook.js";
14
+ export type { HookRunResult, RunHookOptions, HookInput, HookOutput, HookPropertyResult, } from "./run-hook.js";
15
+ export { runEval, measure, measureArms, measureTriggerRate, assertRates, assertPromptDiversity, checkPromptDiversity, checkReportToJUnit, formatCheckReport, formatEvalReport, formatTriggerRateReport, claudeEvalDriver, parseClaudeRun, stubSkillBody, } from "./eval.js";
16
+ export type { EvalArm, EvalDriver, EvalSpec, EvalReport, EvalUsage, MeasureSpec, ArmsMeasureSpec, ArmReport, ArmUsage, ArmsCheckReport, CheckRate, CheckReport, MetricStat, Metrics, ModelOutputParser, ParsedModelRun, PromptDiversityIssue, PromptTriggerStat, RunContext, RunOut, SelectionTrialResult, TriggerRateReport, TriggerRateSpec, AgentRunArgs, AgentRunner, } from "./eval.js";
15
17
  export * from "./harness-assert.js";
16
18
  export * from "./check.js";
17
19
  export { hookFired } from "./check.js";
18
20
  export { judge } from "./judge.js";
19
21
  export * from "./tool-stub.js";
20
- export { runHarnessTest, runHarness, parseToolCalls, parseSubagents, parseResultEvent, parseOutput, parseHooks, decideSandbox, specTrusted, sandboxAvailable, } from "./harness-test.js";
22
+ export { runHarnessTest, runHarness } from "./harness-test.js";
21
23
  export type { HarnessTestSpec, Trace, SubagentTrace, HarnessTestResult, RunHarnessTestOptions, ModelTurn, ModelRequest, ToolCall, HookFire, HarnessTestDriver, SandboxMode, } from "./harness-test.js";
22
24
  //# sourceMappingURL=testing.d.ts.map
package/dist/testing.js CHANGED
@@ -1,4 +1,20 @@
1
1
  "use strict";
2
+ /**
3
+ * `vigiles/testing` — Pillar 2 entry point: the **harness-testing** API. Re-exports
4
+ * the three tiers — `runHook` (unit), `runHarnessTest` (deterministic), `runEval`
5
+ * (eval) — plus the runner-agnostic predicates/assertions. Kept deliberately
6
+ * separate from `vigiles/claude-code` so this surface can stay harness-agnostic as
7
+ * more harnesses are added. Granular paths (`vigiles/run-hook`, etc.) still work.
8
+ *
9
+ * It re-exports the composition-root runner modules (which do the Claude-Code
10
+ * default-wiring), never an adapter directly — the `agnostic-surface` eslint
11
+ * boundary forbids importing `src/adapters/*` from here. See
12
+ * `research/adapter-api-design.md`.
13
+ */
14
+ // Curated public re-exports (named, not `export *`) so the internal seams —
15
+ // the injectable `*With` runners, low-level `parse*`, pool/aggregate/model-tier
16
+ // helpers — stay out of the public surface, the api reports, and the docs site.
17
+ // (vigiles's own tests import those from the source modules directly.)
2
18
  var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
19
  if (k2 === undefined) k2 = k;
4
20
  var desc = Object.getOwnPropertyDescriptor(m, k);
@@ -14,21 +30,27 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
30
  for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
31
  };
16
32
  Object.defineProperty(exports, "__esModule", { value: true });
17
- exports.sandboxAvailable = exports.specTrusted = exports.decideSandbox = exports.parseHooks = exports.parseOutput = exports.parseResultEvent = exports.parseSubagents = exports.parseToolCalls = exports.runHarness = exports.runHarnessTest = exports.judge = exports.hookFired = void 0;
18
- /**
19
- * `vigiles/testing` — Pillar 2 entry point: the **harness-testing** API. Re-exports
20
- * the three tiers — `runHook` (unit), `runHarnessTest` (deterministic), `runEval`
21
- * (eval) — plus the runner-agnostic predicates/assertions. Kept deliberately
22
- * separate from `vigiles/claude-code` so this surface can stay harness-agnostic as
23
- * more harnesses are added. Granular paths (`vigiles/run-hook`, etc.) still work.
24
- *
25
- * It re-exports the composition-root runner modules (which do the Claude-Code
26
- * default-wiring), never an adapter directly — the `agnostic-surface` eslint
27
- * boundary forbids importing `src/adapters/*` from here. See
28
- * `research/adapter-api-design.md`.
29
- */
30
- __exportStar(require("./run-hook.js"), exports);
31
- __exportStar(require("./eval.js"), exports);
33
+ exports.runHarness = exports.runHarnessTest = exports.judge = exports.hookFired = exports.stubSkillBody = exports.parseClaudeRun = exports.claudeEvalDriver = exports.formatTriggerRateReport = exports.formatEvalReport = exports.formatCheckReport = exports.checkReportToJUnit = exports.checkPromptDiversity = exports.assertPromptDiversity = exports.assertRates = exports.measureTriggerRate = exports.measureArms = exports.measure = exports.runEval = exports.propertyHook = exports.runHook = void 0;
34
+ // --- unit tier: runHook ---
35
+ var run_hook_js_1 = require("./run-hook.js");
36
+ Object.defineProperty(exports, "runHook", { enumerable: true, get: function () { return run_hook_js_1.runHook; } });
37
+ Object.defineProperty(exports, "propertyHook", { enumerable: true, get: function () { return run_hook_js_1.propertyHook; } });
38
+ // --- eval tier: runEval / measure / trigger-rate ---
39
+ var eval_js_1 = require("./eval.js");
40
+ Object.defineProperty(exports, "runEval", { enumerable: true, get: function () { return eval_js_1.runEval; } });
41
+ Object.defineProperty(exports, "measure", { enumerable: true, get: function () { return eval_js_1.measure; } });
42
+ Object.defineProperty(exports, "measureArms", { enumerable: true, get: function () { return eval_js_1.measureArms; } });
43
+ Object.defineProperty(exports, "measureTriggerRate", { enumerable: true, get: function () { return eval_js_1.measureTriggerRate; } });
44
+ Object.defineProperty(exports, "assertRates", { enumerable: true, get: function () { return eval_js_1.assertRates; } });
45
+ Object.defineProperty(exports, "assertPromptDiversity", { enumerable: true, get: function () { return eval_js_1.assertPromptDiversity; } });
46
+ Object.defineProperty(exports, "checkPromptDiversity", { enumerable: true, get: function () { return eval_js_1.checkPromptDiversity; } });
47
+ Object.defineProperty(exports, "checkReportToJUnit", { enumerable: true, get: function () { return eval_js_1.checkReportToJUnit; } });
48
+ Object.defineProperty(exports, "formatCheckReport", { enumerable: true, get: function () { return eval_js_1.formatCheckReport; } });
49
+ Object.defineProperty(exports, "formatEvalReport", { enumerable: true, get: function () { return eval_js_1.formatEvalReport; } });
50
+ Object.defineProperty(exports, "formatTriggerRateReport", { enumerable: true, get: function () { return eval_js_1.formatTriggerRateReport; } });
51
+ Object.defineProperty(exports, "claudeEvalDriver", { enumerable: true, get: function () { return eval_js_1.claudeEvalDriver; } });
52
+ Object.defineProperty(exports, "parseClaudeRun", { enumerable: true, get: function () { return eval_js_1.parseClaudeRun; } });
53
+ Object.defineProperty(exports, "stubSkillBody", { enumerable: true, get: function () { return eval_js_1.stubSkillBody; } });
32
54
  __exportStar(require("./harness-assert.js"), exports);
33
55
  // The declarative check vocabulary is now first-class at the front door. Its
34
56
  // `hookFired` (a `Check<Trace>`) supersedes the legacy boolean predicate of the
@@ -51,12 +73,4 @@ __exportStar(require("./tool-stub.js"), exports);
51
73
  var harness_test_js_1 = require("./harness-test.js");
52
74
  Object.defineProperty(exports, "runHarnessTest", { enumerable: true, get: function () { return harness_test_js_1.runHarnessTest; } });
53
75
  Object.defineProperty(exports, "runHarness", { enumerable: true, get: function () { return harness_test_js_1.runHarness; } });
54
- Object.defineProperty(exports, "parseToolCalls", { enumerable: true, get: function () { return harness_test_js_1.parseToolCalls; } });
55
- Object.defineProperty(exports, "parseSubagents", { enumerable: true, get: function () { return harness_test_js_1.parseSubagents; } });
56
- Object.defineProperty(exports, "parseResultEvent", { enumerable: true, get: function () { return harness_test_js_1.parseResultEvent; } });
57
- Object.defineProperty(exports, "parseOutput", { enumerable: true, get: function () { return harness_test_js_1.parseOutput; } });
58
- Object.defineProperty(exports, "parseHooks", { enumerable: true, get: function () { return harness_test_js_1.parseHooks; } });
59
- Object.defineProperty(exports, "decideSandbox", { enumerable: true, get: function () { return harness_test_js_1.decideSandbox; } });
60
- Object.defineProperty(exports, "specTrusted", { enumerable: true, get: function () { return harness_test_js_1.specTrusted; } });
61
- Object.defineProperty(exports, "sandboxAvailable", { enumerable: true, get: function () { return harness_test_js_1.sandboxAvailable; } });
62
76
  //# sourceMappingURL=testing.js.map
@@ -15,7 +15,7 @@
15
15
  *
16
16
  * - `decideIntercept` — does this call get intercepted, and with what deny reason;
17
17
  * - `buildInterceptSettings` — the PreToolUse hook fragment routing matched tools
18
- * through `vigiles intercept-tool-hook`;
18
+ * through `vigiles hook-runtime intercept-tool`;
19
19
  * - `serializeIntercepts` / `parseIntercepts` — the env round-trip (incl. RegExp
20
20
  * matchers) the hook subprocess reads back.
21
21
  *
@@ -32,7 +32,7 @@
32
32
  * tools, so deny+reason is the closest available — with that ceiling.
33
33
  */
34
34
  import { type ArgMatcher } from "./arg-match.js";
35
- /** Env var the spawned `vigiles intercept-tool-hook` reads its intercept list from. */
35
+ /** Env var the spawned `vigiles hook-runtime intercept-tool` reads its intercept list from. */
36
36
  export declare const INTERCEPT_TOOLS_ENV = "VIGILES_INTERCEPT_TOOLS";
37
37
  /** Declare a tool to intercept: deny its real execution with a block message. */
38
38
  export interface ToolIntercept {
@@ -63,7 +63,7 @@ export type InterceptDecision = {
63
63
  /**
64
64
  * Decide whether a tool call should be intercepted. Returns the first matching
65
65
  * intercept's denial reason (preventing real execution), or `{ intercept: false }`
66
- * to let the call run for real. Pure — the same logic `vigiles intercept-tool-hook`
66
+ * to let the call run for real. Pure — the same logic `vigiles hook-runtime intercept-tool`
67
67
  * runs.
68
68
  */
69
69
  export declare function decideIntercept(toolName: string, input: unknown, intercepts: readonly ToolIntercept[]): InterceptDecision;
@@ -81,7 +81,7 @@ export interface InterceptSettings {
81
81
  }
82
82
  /**
83
83
  * Build the PreToolUse hook fragment that routes every intercepted tool through
84
- * `vigiles intercept-tool-hook`. The `matcher` is a CC tool-name regex over the
84
+ * `vigiles hook-runtime intercept-tool`. The `matcher` is a CC tool-name regex over the
85
85
  * union of intercepted tool names (each escaped), so unrelated tools are never
86
86
  * intercepted. Merge the result into an arm's `settings`; the intercept list
87
87
  * itself travels in the {@link INTERCEPT_TOOLS_ENV} env var (see
@@ -23,7 +23,7 @@ exports.parseIntercepts = parseIntercepts;
23
23
  *
24
24
  * - `decideIntercept` — does this call get intercepted, and with what deny reason;
25
25
  * - `buildInterceptSettings` — the PreToolUse hook fragment routing matched tools
26
- * through `vigiles intercept-tool-hook`;
26
+ * through `vigiles hook-runtime intercept-tool`;
27
27
  * - `serializeIntercepts` / `parseIntercepts` — the env round-trip (incl. RegExp
28
28
  * matchers) the hook subprocess reads back.
29
29
  *
@@ -40,7 +40,7 @@ exports.parseIntercepts = parseIntercepts;
40
40
  * tools, so deny+reason is the closest available — with that ceiling.
41
41
  */
42
42
  const arg_match_js_1 = require("./arg-match.js");
43
- /** Env var the spawned `vigiles intercept-tool-hook` reads its intercept list from. */
43
+ /** Env var the spawned `vigiles hook-runtime intercept-tool` reads its intercept list from. */
44
44
  exports.INTERCEPT_TOOLS_ENV = "VIGILES_INTERCEPT_TOOLS";
45
45
  /** The default denial reason — honest that the call was intercepted (prevented), NOT executed. */
46
46
  exports.DEFAULT_INTERCEPT_REASON = "vigiles intercepted this tool call for testing — it was NOT executed. " +
@@ -48,7 +48,7 @@ exports.DEFAULT_INTERCEPT_REASON = "vigiles intercepted this tool call for testi
48
48
  /**
49
49
  * Decide whether a tool call should be intercepted. Returns the first matching
50
50
  * intercept's denial reason (preventing real execution), or `{ intercept: false }`
51
- * to let the call run for real. Pure — the same logic `vigiles intercept-tool-hook`
51
+ * to let the call run for real. Pure — the same logic `vigiles hook-runtime intercept-tool`
52
52
  * runs.
53
53
  */
54
54
  function decideIntercept(toolName, input, intercepts) {
@@ -90,14 +90,14 @@ function uniqueToolNames(intercepts) {
90
90
  }
91
91
  /**
92
92
  * Build the PreToolUse hook fragment that routes every intercepted tool through
93
- * `vigiles intercept-tool-hook`. The `matcher` is a CC tool-name regex over the
93
+ * `vigiles hook-runtime intercept-tool`. The `matcher` is a CC tool-name regex over the
94
94
  * union of intercepted tool names (each escaped), so unrelated tools are never
95
95
  * intercepted. Merge the result into an arm's `settings`; the intercept list
96
96
  * itself travels in the {@link INTERCEPT_TOOLS_ENV} env var (see
97
97
  * {@link serializeIntercepts}).
98
98
  */
99
99
  function buildInterceptSettings(intercepts, opts = {}) {
100
- const command = opts.command ?? "npx vigiles intercept-tool-hook";
100
+ const command = opts.command ?? "npx vigiles hook-runtime intercept-tool";
101
101
  const matcher = uniqueToolNames(intercepts).map(escapeRegex).join("|");
102
102
  return {
103
103
  hooks: {
package/dist/unit.d.ts CHANGED
@@ -16,4 +16,6 @@ export { runHook, parseHookOutput, decideHook, propertyHook, } from "./run-hook.
16
16
  export type { HookInput, HookOutput, HookRunResult, RunHookOptions, HookPropertyResult, } from "./run-hook.js";
17
17
  export * from "./check.js";
18
18
  export { hookFired } from "./check.js";
19
+ export { DISASTER_CATALOG, verifyGuardrail, unblockedDisasters, assertBlocksDisasters, formatGuardrailReport, } from "./guardrail-check.js";
20
+ export type { DisasterEvent, DisasterCategory, GuardrailResult, VerifyGuardrailOptions, } from "./guardrail-check.js";
19
21
  //# 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.hookFired = exports.propertyHook = exports.decideHook = exports.parseHookOutput = exports.runHook = void 0;
17
+ exports.formatGuardrailReport = exports.assertBlocksDisasters = exports.unblockedDisasters = exports.verifyGuardrail = exports.DISASTER_CATALOG = 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
  *
@@ -39,4 +39,11 @@ Object.defineProperty(exports, "propertyHook", { enumerable: true, get: function
39
39
  __exportStar(require("./check.js"), exports);
40
40
  var check_js_1 = require("./check.js");
41
41
  Object.defineProperty(exports, "hookFired", { enumerable: true, get: function () { return check_js_1.hookFired; } });
42
+ // Guardrail verification — "prove your safety hook actually blocks" (over runHook).
43
+ var guardrail_check_js_1 = require("./guardrail-check.js");
44
+ Object.defineProperty(exports, "DISASTER_CATALOG", { enumerable: true, get: function () { return guardrail_check_js_1.DISASTER_CATALOG; } });
45
+ Object.defineProperty(exports, "verifyGuardrail", { enumerable: true, get: function () { return guardrail_check_js_1.verifyGuardrail; } });
46
+ Object.defineProperty(exports, "unblockedDisasters", { enumerable: true, get: function () { return guardrail_check_js_1.unblockedDisasters; } });
47
+ Object.defineProperty(exports, "assertBlocksDisasters", { enumerable: true, get: function () { return guardrail_check_js_1.assertBlocksDisasters; } });
48
+ Object.defineProperty(exports, "formatGuardrailReport", { enumerable: true, get: function () { return guardrail_check_js_1.formatGuardrailReport; } });
42
49
  //# sourceMappingURL=unit.js.map
@@ -19,7 +19,7 @@ BASENAME=$(basename "$FILE")
19
19
  case "$BASENAME" in
20
20
  eslint.config.*|.eslintrc*|.stylelintrc*|.rubocop.yml|pyproject.toml|Cargo.toml|package.json)
21
21
  if command -v npx &>/dev/null && [ -f "package.json" ]; then
22
- npx vigiles generate-types 2>&1 || true
22
+ npx vigiles generate types 2>&1 || true
23
23
  fi
24
24
  ;;
25
25
  esac
@@ -13,7 +13,7 @@ INPUT=$(cat)
13
13
  command -v npx >/dev/null 2>&1 || exit 0
14
14
  [ -f package.json ] || exit 0
15
15
 
16
- printf '%s' "$INPUT" | npx vigiles refs-hook
16
+ printf '%s' "$INPUT" | npx vigiles hook-runtime refs
17
17
  status=${PIPESTATUS[1]}
18
18
 
19
19
  # Propagate ONLY an explicit block (exit 2). Swallow tool/setup errors so a
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vigiles",
3
- "version": "6.0.0",
3
+ "version": "8.0.0",
4
4
  "description": "Lint & test the harness your AI agent runs on — verify the references in your CLAUDE.md / AGENTS.md and test that your hooks and skills actually work.",
5
5
  "keywords": [
6
6
  "claude-code",
@@ -38,6 +38,7 @@
38
38
  "./linting": "./dist/linting.js",
39
39
  "./testing": "./dist/testing.js",
40
40
  "./unit": "./dist/unit.js",
41
+ "./hook": "./dist/hook.js",
41
42
  "./integration": "./dist/integration.js",
42
43
  "./e2e": "./dist/e2e.js",
43
44
  "./claude-code": "./dist/claude-code.js",
@@ -82,11 +83,10 @@
82
83
  "test:types": "npm run build && tsc --noEmit -p test/types/tsconfig.json",
83
84
  "api:report": "npm run build && node scripts/api-extractor.mjs --local",
84
85
  "api:check": "npm run build && node scripts/api-extractor.mjs",
85
- "docs:api": "npm run api:report && api-documenter markdown -i temp -o api-reference"
86
+ "docs:api": "typedoc"
86
87
  },
87
88
  "devDependencies": {
88
89
  "@eslint/js": "^10.0.1",
89
- "@microsoft/api-documenter": "^7.30.7",
90
90
  "@microsoft/api-extractor": "^7.58.9",
91
91
  "@types/js-yaml": "^4.0.9",
92
92
  "@types/minimatch": "^5.1.2",
@@ -102,6 +102,7 @@
102
102
  "jest": "^30.4.2",
103
103
  "prettier": "^3.8.1",
104
104
  "tsx": "^4.21.0",
105
+ "typedoc": "^0.28.19",
105
106
  "typescript": "^5.9.3",
106
107
  "vitest": "^4.1.8"
107
108
  },
@@ -124,6 +125,7 @@
124
125
  "@ast-grep/napi": "^0.43.0",
125
126
  "@iarna/toml": "^2.2.5",
126
127
  "@jackchuka/mdschema": "^0.12.8",
128
+ "ci-info": "^4.4.0",
127
129
  "cosmiconfig": "^9.0.1",
128
130
  "glob": "^13.0.6",
129
131
  "js-yaml": "^4.1.0",