vigiles 27.3.0 → 29.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 (59) hide show
  1. package/README.md +1 -1
  2. package/dist/adapter-registry.d.ts +39 -0
  3. package/dist/adapter-registry.js +45 -0
  4. package/dist/adapter.d.ts +8 -0
  5. package/dist/adapter.js +10 -1
  6. package/dist/adapters/claude-code/adapter.js +9 -2
  7. package/dist/adapters/claude-code/layout.d.ts +5 -0
  8. package/dist/adapters/claude-code/plugin-loader.d.ts +10 -1
  9. package/dist/adapters/claude-code/plugin-loader.js +10 -1
  10. package/dist/adapters/codex/adapter.js +7 -2
  11. package/dist/adapters/codex/layout.d.ts +51 -5
  12. package/dist/adapters/codex/layout.js +13 -4
  13. package/dist/adapters/opencode/adapter.js +6 -0
  14. package/dist/audit-report.template.html +1 -1
  15. package/dist/audit-score.d.ts +7 -0
  16. package/dist/audit-score.js +49 -2
  17. package/dist/cli-main.js +162 -39
  18. package/dist/core/adapter.d.ts +45 -1
  19. package/dist/core/compile.js +11 -1
  20. package/dist/core/config-schema.d.ts +244 -0
  21. package/dist/core/config-schema.js +452 -0
  22. package/dist/core/hook-program.d.ts +43 -0
  23. package/dist/core/hook-program.js +32 -0
  24. package/dist/core/refs.js +10 -1
  25. package/dist/core/surface-discovery.d.ts +270 -0
  26. package/dist/core/surface-discovery.js +425 -0
  27. package/dist/core/surface-scopes.d.ts +38 -1
  28. package/dist/core/surface-scopes.js +73 -1
  29. package/dist/core/symbols.d.ts +24 -2
  30. package/dist/core/symbols.js +66 -18
  31. package/dist/core/types.d.ts +36 -107
  32. package/dist/core/validate.d.ts +46 -18
  33. package/dist/core/validate.js +98 -172
  34. package/dist/exclude.d.ts +20 -0
  35. package/dist/exclude.js +11 -1
  36. package/dist/harness-test.js +3 -3
  37. package/dist/hook-install.d.ts +53 -0
  38. package/dist/hook-install.js +60 -0
  39. package/dist/hook-runtime.d.ts +2 -2
  40. package/dist/hook-runtime.js +82 -63
  41. package/dist/layout-registry.d.ts +14 -0
  42. package/dist/layout-registry.js +40 -0
  43. package/dist/load-hook.d.ts +1 -1
  44. package/dist/load-hook.js +2 -2
  45. package/dist/plugin-loader.d.ts +49 -1
  46. package/dist/plugin-loader.js +120 -14
  47. package/dist/run-hook.js +17 -1
  48. package/dist/scan-core.d.ts +19 -0
  49. package/dist/scan-core.js +30 -0
  50. package/dist/scan-files.js +15 -5
  51. package/dist/scan.d.ts +63 -0
  52. package/dist/scan.js +68 -12
  53. package/dist/score-core.js +8 -0
  54. package/dist/setup-plan.d.ts +2 -1
  55. package/dist/setup-plan.js +7 -2
  56. package/dist/surface-discovery-fs.d.ts +12 -0
  57. package/dist/surface-discovery-fs.js +108 -0
  58. package/dist/vigilesrc.schema.json +1689 -0
  59. package/package.json +10 -6
@@ -1,8 +1,6 @@
1
1
  "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.installedGrammars = installedGrammars;
6
4
  exports.langForFile = langForFile;
7
5
  exports.definedSymbols = definedSymbols;
8
6
  exports.definedSymbolsInFile = definedSymbolsInFile;
@@ -23,18 +21,60 @@ exports.fileDefinesSymbol = fileDefinesSymbol;
23
21
  * delegated. Ambiguity (a name defined in several files) is reported, not guessed.
24
22
  */
25
23
  const node_fs_1 = require("node:fs");
24
+ const node_module_1 = require("node:module");
26
25
  const node_path_1 = require("node:path");
27
26
  const napi_1 = require("@ast-grep/napi");
28
- const lang_python_1 = __importDefault(require("@ast-grep/lang-python"));
29
- const lang_rust_1 = __importDefault(require("@ast-grep/lang-rust"));
30
- const lang_ruby_1 = __importDefault(require("@ast-grep/lang-ruby"));
31
- let registered = false;
27
+ /**
28
+ * The non-web grammars, as OPTIONAL packages keyed by the id ast-grep registers them under.
29
+ *
30
+ * 🔴 WHY OPTIONAL, AND WHY IT IS NOT A PREFERENCE. These three are the only packages in this
31
+ * dependency tree carrying a `postinstall` (measured 2026-09-20 with `npm query
32
+ * ":attr(scripts, [postinstall])"`). Since pnpm 10 a consumer's install FAILS on an
33
+ * unapproved lifecycle script, so every downstream project installing vigiles with pnpm got
34
+ * `ERR_PNPM_IGNORED_BUILDS` and a non-zero exit — for grammars most of them never use. The web
35
+ * grammars every user does need (TypeScript, TSX, JavaScript, CSS) are built into
36
+ * `@ast-grep/napi` and cost nothing.
37
+ *
38
+ * They load through `createRequire` rather than `await import()` on purpose: the packages are
39
+ * CommonJS (`"main": "index.js"`, no `exports`), so a synchronous require works and NOTHING in
40
+ * this module's public surface has to become async. Measured, not assumed.
41
+ */
42
+ const OPTIONAL_GRAMMARS = {
43
+ python: "@ast-grep/lang-python",
44
+ rust: "@ast-grep/lang-rust",
45
+ ruby: "@ast-grep/lang-ruby",
46
+ };
47
+ // Anchored on THIS module's own file, not on the consumer's project root. Under pnpm a
48
+ // consumer's root does not contain our transitive packages at all — the same addressing
49
+ // mistake that made every hook fail there — and these grammars are OUR optional dependencies,
50
+ // so they resolve from where this file lives. `__filename` rather than `import.meta.url`
51
+ // because this package compiles to CommonJS (`module: Node16`, `main: ./dist/test.js`).
52
+ const require_ = (0, node_module_1.createRequire)(__filename);
53
+ /** Registered grammar ids, populated on first use. `null` until then. */
54
+ let loaded = null;
32
55
  function ensureRegistered() {
33
- if (registered)
34
- return;
35
- // Non-web grammars ship as separate packages registered at runtime.
36
- (0, napi_1.registerDynamicLanguage)({ python: lang_python_1.default, rust: lang_rust_1.default, ruby: lang_ruby_1.default });
37
- registered = true;
56
+ if (loaded)
57
+ return loaded;
58
+ const dynamic = {};
59
+ const present = new Set();
60
+ for (const [id, pkg] of Object.entries(OPTIONAL_GRAMMARS)) {
61
+ try {
62
+ dynamic[id] = require_(pkg);
63
+ present.add(id);
64
+ }
65
+ catch {
66
+ // Absent by design: an optional dependency the consumer did not install. The caller is
67
+ // told WHICH id is missing (see `langForFile`), so "not checked" never reads as "clean".
68
+ }
69
+ }
70
+ if (present.size > 0)
71
+ (0, napi_1.registerDynamicLanguage)(dynamic);
72
+ loaded = present;
73
+ return loaded;
74
+ }
75
+ /** Which optional grammars this process actually has. Exported so a report can say so. */
76
+ function installedGrammars() {
77
+ return ensureRegistered();
38
78
  }
39
79
  const EXT_LANG = {
40
80
  ".ts": napi_1.Lang.TypeScript,
@@ -53,11 +93,18 @@ const EXT_LANG = {
53
93
  ".rb": "ruby",
54
94
  ".rbi": "ruby",
55
95
  };
56
- /** The ast-grep language for a file, or null if unsupported (graceful skip). */
57
96
  function langForFile(file) {
58
- if (file.endsWith(".d.ts"))
59
- return napi_1.Lang.TypeScript;
60
- return EXT_LANG[(0, node_path_1.extname)(file).toLowerCase()] ?? null;
97
+ const key = file.endsWith(".d.ts")
98
+ ? napi_1.Lang.TypeScript
99
+ : EXT_LANG[(0, node_path_1.extname)(file).toLowerCase()];
100
+ if (key === undefined)
101
+ return { kind: "unsupported" };
102
+ // A string key is one of the dynamically registered grammars; the enum members are built in.
103
+ if (typeof key === "string" && key in OPTIONAL_GRAMMARS) {
104
+ if (!ensureRegistered().has(key))
105
+ return { kind: "grammar-missing", id: key, pkg: OPTIONAL_GRAMMARS[key] };
106
+ }
107
+ return { kind: "ready", lang: key };
61
108
  }
62
109
  const ID_KINDS = new Set(["identifier", "constant", "type_identifier"]);
63
110
  const SCOPE_KINDS = new Set([
@@ -97,9 +144,10 @@ function definedSymbols(code, lang) {
97
144
  }
98
145
  /** Defined symbols for a file on disk, or [] if unreadable/unsupported. */
99
146
  function definedSymbolsInFile(file) {
100
- const lang = langForFile(file);
101
- if (!lang)
147
+ const support = langForFile(file);
148
+ if (support.kind !== "ready")
102
149
  return [];
150
+ const lang = support.lang;
103
151
  try {
104
152
  return definedSymbols((0, node_fs_1.readFileSync)(file, "utf-8"), lang);
105
153
  }
@@ -1,4 +1,5 @@
1
1
  import type { HarnessDialect } from "./dialect.js";
2
+ import type { HarnessDeclarationShape, VigilesConfigShape } from "./config-schema.js";
2
3
  /** A parsed rule from a markdown instruction file. */
3
4
  export interface ParsedRule {
4
5
  title: string;
@@ -377,113 +378,41 @@ export interface RulesConfig {
377
378
  export declare function ruleSeverity<T>(rule: RuleWithOptions<T> | undefined): RuleSeverity;
378
379
  /** Extract options from a rule value (returns undefined for simple severity). */
379
380
  export declare function ruleOptions<T>(rule: RuleWithOptions<T> | undefined): T | undefined;
380
- /** Full vigiles configuration. Loaded from .vigilesrc.json. */
381
- export interface VigilesConfig {
382
- ruleMarkers: MarkerType[];
383
- rules: Required<RulesConfig>;
384
- files: string[];
385
- /** Maximum number of rules allowed per spec. */
386
- maxRules?: number;
387
- /** Maximum estimated tokens for compiled output. */
388
- maxTokens?: number;
389
- /** Maximum lines per prose section. */
390
- maxSectionLines?: number;
391
- /** Skip config-enabled checks, only verify rule exists in catalog. */
392
- catalogOnly?: boolean;
393
- /** Custom linter configs (rulesDir). */
394
- linters?: Record<string, {
395
- rulesDir?: string | string[];
396
- }>;
397
- /** Orphan-docs check configuration. Include/exclude globs, tsconfig-style. */
398
- /**
399
- * Which bundles `lint` scores: `"root"` (default) or `"all"`.
400
- *
401
- * A monorepo holding `skills/` plus `plugins/ * /skills/` had its nested skills
402
- * silently uncounted — the counters looked complete while whole surfaces were
403
- * never read (#185). `"all"` scores every discovered bundle in one pass, so a
404
- * CI gate keeps ONE exit code over the whole repo.
405
- *
406
- * Root-only remains the default because descending unconditionally would score
407
- * vendored third-party plugins (a repo may keep a pinned corpus on disk) as if
408
- * they were the project's own. The default no longer hides the skip: `lint`
409
- * names the bundles it did not score.
410
- */
411
- bundles?: "root" | "all";
412
- orphans?: OrphansConfig;
413
- /**
414
- * Paths and globs the repo's own tooling does NOT police — vendored corpora,
415
- * benchmark fixtures, frozen reproductions (tsconfig-style, relative to the
416
- * repo root; a bare directory name such as `"bench"` excludes its subtree, as
417
- * do `"bench/"` and `"bench/**"`). `node_modules`/`dist`/`.git`/`.vigiles` are
418
- * always excluded.
419
- *
420
- * ONE filter, every pass (#192): `compile` does not load an excluded spec,
421
- * `lint` does not discover an excluded instruction file, nested bundle, doc, or
422
- * surface, `audit` does not read an excluded instruction file, and
423
- * `test`/`eval` do not discover an excluded script. It filters DISCOVERY only:
424
- * a path you name on the command line is still processed, and one line says
425
- * which pattern it matched. The rule-level `orphans.exclude` and
426
- * `untested-*` `exclude` NARROW their own rule further and never re-admit a
427
- * path excluded here (union, not override). Parsed once in `src/exclude.ts`.
428
- */
429
- exclude?: readonly string[];
430
- /**
431
- * Top-level dir names shared across skills, e.g. `["scripts", "references"]`.
432
- * OPT-IN: many skill libraries keep ONE top-level `scripts/`/`references/` tree
433
- * rather than a copy beside every `SKILL.md`, so a bundled ref like
434
- * `scripts/promptfoo/x.py` lives at the REPO ROOT. When a `SKILL.md` body ref's
435
- * first path segment is a declared shared dir, `skill-resource-resolves` / audit
436
- * ALSO resolves it against the repo root, not only the skill's own dir. Scoped
437
- * to declared dirs on purpose: a repo that omits this key behaves exactly as
438
- * before (skill-dir-only resolution), and a ref outside a shared dir is never
439
- * masked by a same-named repo-root file. See feedback P1-4.
440
- */
441
- sharedDirs?: readonly string[];
442
- /**
443
- * The harness(es) this repo targets — selects the compile dialect / skill
444
- * frontmatter profile / instruction-file shape, instead of sniffing the cwd.
445
- * A single name (`"codex"`) for the common single-harness repo, or an array
446
- * (`["claude-code", "codex"]`) declaring the supported set. Written by
447
- * `vigiles init`. Omitted → the CLI auto-detects (backwards-compatible).
448
- * Canonical adapter names; `"claude"` is accepted as an alias for
449
- * `"claude-code"`. See research/multi-harness-compile.md.
450
- */
451
- harness?: string | string[];
452
- /**
453
- * `vigiles audit` preferences. `measure` is the sticky remembered answer to the
454
- * "run the executing checks against your harness?" prompt — at a TTY `audit`
455
- * asks once, then records the choice here so it never asks again. `true` runs
456
- * the executing checks (safety battery · live MCP · skill firing) on every
457
- * interactive run, `false` keeps them off (edit this key to change). Written by
458
- * the audit consent prompt, not `init`. Headless runs never execute regardless
459
- * (audit is a local report, not a CI step — there is no execution flag).
460
- */
461
- audit?: {
462
- measure?: boolean;
463
- };
464
- /**
465
- * `vigiles eval` preferences. `apiVersion` is the hand-bumped **behavior epoch**
466
- * folded into the eval LOCK's input hash (`src/eval-lock.ts`): bump it when a
467
- * harness-side change YOU made (a CLAUDE.md edit, a global hook) would shift
468
- * eval outputs but isn't otherwise visible to the lock — so `vigiles eval
469
- * --check` reports the committed eval results STALE and forces a local re-run.
470
- * Default 1. Distinct from the (auto-resolved) `claude` CLI version, which is
471
- * recorded as provenance but deliberately NOT hashed.
472
- */
473
- eval?: {
474
- apiVersion?: number;
475
- };
476
- /**
477
- * Suppress the adoption nudges `audit` prints (the "N surfaces not yet
478
- * spec-managed → create specs" invitation). Set `"dismissed"` to hide them —
479
- * the "remembered decline" half of the non-evil adoption contract, so a team
480
- * that deliberately runs the integrity GATE alone (no specs/plugin) isn't
481
- * nagged on every run. USER-SET only: `audit`/`lint` are pure reads and never
482
- * write this (a read never writes). The deterministic findings + fixes are
483
- * unaffected — this silences only the invitation, never a real finding.
484
- */
485
- nudge?: "dismissed";
486
- }
381
+ /**
382
+ * ONE harness's entry in {@link VigilesConfig.harnesses} — what this repo tells
383
+ * that harness about itself. Today that is only `roots`; the OBJECT (rather than
384
+ * a bare array of roots) is what leaves room for a second per-harness fact
385
+ * without another top-level key, which is how `surfaceRoots` came to exist
386
+ * beside `harness` in the first place.
387
+ *
388
+ * `{}` is meaningful and is the common case: "this repo targets this harness,
389
+ * and it reads it where it normally reads it."
390
+ *
391
+ * `roots` are extra repo-relative dirs THIS harness's surfaces live under, read
392
+ * with THIS harness's own `surfaceDirs` — `[".ai"]` under `"claude-code"` means
393
+ * `.ai/skills`, `.ai/agents`, `.ai/commands`. Every entry must resolve to at
394
+ * least one surface dir that exists on disk under this harness's layout; one
395
+ * that does not is a hard error, because a root declared under a harness that
396
+ * reads nothing there changes nothing and used to say nothing.
397
+ */
398
+ export type HarnessDeclaration = HarnessDeclarationShape;
399
+ /**
400
+ * Full vigiles configuration, loaded from `.vigilesrc.json`.
401
+ *
402
+ * 🔴 DERIVED FROM THE SCHEMA, NOT WRITTEN BESIDE IT. The shape, the defaults and
403
+ * the per-key prose all live in `./config-schema.ts`; this is
404
+ * `z.infer<typeof vigilesConfigSchema>` with the defaults applied, so the type
405
+ * says exactly what a parsed config carries — `rules` non-optional and complete
406
+ * because every rule key has a schema default, optional keys optional because
407
+ * the schema says so. A hand-written twin is the copy that drifts: `harness` and
408
+ * `surfaceRoots` lived in that twin, in `docs/cli.md` and in the loader's
409
+ * coercions, and the three disagreed about what was read (#240).
410
+ *
411
+ * The re-export is TYPE-ONLY on purpose — `types.ts` is imported by node-free
412
+ * core modules, and a value import of the schema module would pull Zod into
413
+ * every one of them.
414
+ */
415
+ export type VigilesConfig = VigilesConfigShape;
487
416
  /** Valid marker types for rule detection. */
488
417
  export type MarkerType = "headings" | "checkboxes";
489
418
  /** Options for parseRules. */
@@ -1,25 +1,53 @@
1
- import type { ParsedRule, ValidationError, ValidationResult, ReadResult, FileResult, ValidatePathsResult, RulesConfig, VigilesConfig, MarkerType, ParseOptions, ValidateOptions, ValidatePathsOptions, ReadOptions } from "./types.js";
2
- export type { ParsedRule, ValidationError, ValidationResult, ReadResult, FileResult, ValidatePathsResult, RulesConfig, VigilesConfig, MarkerType, ParseOptions, ValidateOptions, ValidatePathsOptions, ReadOptions, };
3
- export declare const DEFAULT_RULES: Required<RulesConfig>;
4
- export declare function findInstructionFiles(cwd?: string, configFiles?: string[]): string[];
1
+ import type { ParsedRule, ValidationError, ValidationResult, ReadResult, FileResult, ValidatePathsResult, RulesConfig, VigilesConfig, HarnessDeclaration, MarkerType, ParseOptions, ValidateOptions, ValidatePathsOptions, ReadOptions } from "./types.js";
2
+ export type { ParsedRule, ValidationError, ValidationResult, ReadResult, FileResult, ValidatePathsResult, RulesConfig, VigilesConfig, HarnessDeclaration, MarkerType, ParseOptions, ValidateOptions, ValidatePathsOptions, ReadOptions, };
5
3
  /**
6
- * ESLint users write `"off"` / `0` / `1` / `2` for severity; normalize to
7
- * vigiles's `"warn" | "error" | false` so a rule the user meant to DISABLE
8
- * (`"off"`) or GATE (`2`) actually does — instead of a truthy string / number
9
- * silently rendering as a non-gating warn (issue #112 + numeric severities). The
10
- * array form `[sev, opts]` recurses on the head. An unrecognized value is left
11
- * as-is (it renders as a warn, the pre-existing behavior).
4
+ * The shipped default severity of every rule — DERIVED from the schema, never
5
+ * listed twice.
6
+ *
7
+ * It used to be the literal beside the type, which is exactly the pair that
8
+ * drifts: `rule-meta.test.ts` already cross-checks each rule's documented
9
+ * `defaultSeverity` against this object, and it could only ever catch a doc that
10
+ * disagreed with the literal, never a literal that disagreed with what parsing
11
+ * actually produced. Reading it out of the parser closes that gap: this IS what
12
+ * a `{}` config loads as.
12
13
  */
13
- export declare function normalizeSeverity(v: unknown): unknown;
14
+ export declare const DEFAULT_RULES: Required<RulesConfig>;
15
+ export declare function findInstructionFiles(cwd?: string, configFiles?: string[]): string[];
14
16
  /**
15
- * Coerce a config value that should be a `string[]`: a bare STRING becomes a
16
- * one-element array (the natural first-value mistake), so `exclude` /
17
- * `orphans.include` don't silently iterate a string's CHARACTERS as globs — a
18
- * no-op at best, and garbage "orphan" matches (`.`, `/`, `README.md`) at worst.
19
- * A non-string/array value falls back with a warning (the `ruleMarkers` pattern).
17
+ * Read and VALIDATE `.vigilesrc.json`.
18
+ *
19
+ * 🔴 `searchFrom` IS NOT A CONVENIENCE. cosmiconfig defaults to the process's
20
+ * working directory and walks up — right for a CLI verb, where the user is
21
+ * standing in the project they mean, and wrong for a hook, whose process has no
22
+ * stable cwd. A hook rail that omits it reads a DIFFERENT project's config, or
23
+ * none, and the failure runs the wrong way: a missing file means defaults, so a
24
+ * rule the author switched OFF comes back on, silently, because the file saying
25
+ * "off" was never found. Nothing in the output distinguishes that from a project
26
+ * that never configured the rule.
27
+ *
28
+ * 🔴 `onInvalid` IS THE ONE PLACE THE CALL SITES DIFFER, AND IT IS NOT A SPLIT
29
+ * IN WHAT IS CHECKED. Every reader validates; they disagree only about what a
30
+ * bad config is allowed to do to them. A VERB is a human standing at a prompt
31
+ * having just edited the file — `"throw"`, so the line they typed is refused out
32
+ * loud. A HOOK RAIL is a fresh process inside somebody's editing session, and a
33
+ * hook that dies on a malformed config turns a typo in a JSON file into a failed
34
+ * edit, which is a worse outcome than the nudge not firing — `"warn"`, print the
35
+ * same lines to stderr and carry on with the defaults.
36
+ *
37
+ * The two hook rails are `refsHookCommand` and `evalLockNudgeHookCommand`
38
+ * (`vigiles hook-runtime refs` / `eval-lock-nudge`, both registered as
39
+ * PostToolUse `Edit|Write` in `.claude-plugin/plugin.json`). Every other reader
40
+ * is a verb.
41
+ *
42
+ * ⚠️ THE SCHEMA MODULE IS REQUIRED IN-BODY, and that is hygiene rather than an
43
+ * optimization worth a paragraph: Zod is a ~45 ms cold import, `tsc` emits
44
+ * CommonJS across 230 separate files, so a `require` in a function body genuinely
45
+ * does not execute until the function is called. A rail that never reads a
46
+ * config never pays for the validator.
20
47
  */
21
- export declare function asStringArray(v: unknown, fallback: readonly string[], key: string): readonly string[];
22
- export declare function loadConfig(): VigilesConfig;
48
+ export declare function loadConfig(searchFrom?: string, { onInvalid }?: {
49
+ onInvalid?: "throw" | "warn";
50
+ }): VigilesConfig;
23
51
  export declare function parseRules(content: string, { ruleMarkers }?: ParseOptions): ParsedRule[];
24
52
  export declare function validate(content: string, { ruleMarkers, rules: rulesConfig, filePath, dialect }?: ValidateOptions): ValidationResult;
25
53
  export declare function readInstructionFile(filePath: string, options?: ReadOptions): ReadResult;