vigiles 29.1.0 → 30.0.1

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 (122) hide show
  1. package/dist/adapter-conformance.d.ts +1 -1
  2. package/dist/adapter-conformance.js +106 -25
  3. package/dist/adapter-registry.d.ts +61 -14
  4. package/dist/adapter-registry.js +78 -10
  5. package/dist/adapter.d.ts +23 -2
  6. package/dist/adapter.js +13 -1
  7. package/dist/adapters/claude-code/adapter.d.ts +32 -2
  8. package/dist/adapters/claude-code/adapter.js +44 -23
  9. package/dist/adapters/claude-code/agent-runtime.js +3 -1
  10. package/dist/adapters/claude-code/dialect.js +87 -21
  11. package/dist/adapters/claude-code/effect-region.js +3 -1
  12. package/dist/adapters/claude-code/hook-protocol.js +16 -0
  13. package/dist/adapters/claude-code/instruction-chain.d.ts +25 -0
  14. package/dist/adapters/claude-code/instruction-chain.js +626 -0
  15. package/dist/adapters/claude-code/layout.d.ts +2 -2
  16. package/dist/adapters/claude-code/layout.js +42 -8
  17. package/dist/adapters/claude-code/model-access.d.ts +41 -0
  18. package/dist/adapters/claude-code/model-access.js +46 -0
  19. package/dist/adapters/claude-code/skill-reachability.d.ts +125 -0
  20. package/dist/adapters/claude-code/skill-reachability.js +111 -0
  21. package/dist/adapters/claude-code/skill-runtime.js +3 -1
  22. package/dist/adapters/codex/adapter.d.ts +39 -2
  23. package/dist/adapters/codex/adapter.js +29 -29
  24. package/dist/adapters/codex/dialect.js +11 -6
  25. package/dist/adapters/codex/eval.d.ts +10 -0
  26. package/dist/adapters/codex/eval.js +48 -1
  27. package/dist/adapters/codex/hook-protocol.d.ts +2 -1
  28. package/dist/adapters/codex/hook-protocol.js +10 -0
  29. package/dist/adapters/codex/instruction-chain.d.ts +40 -0
  30. package/dist/adapters/codex/instruction-chain.js +105 -0
  31. package/dist/adapters/codex/layout.d.ts +1 -1
  32. package/dist/adapters/codex/layout.js +41 -14
  33. package/dist/adapters/opencode/adapter.d.ts +33 -2
  34. package/dist/adapters/opencode/adapter.js +36 -36
  35. package/dist/adapters/opencode/dialect.js +2 -2
  36. package/dist/adapters/opencode/instruction-chain.d.ts +37 -0
  37. package/dist/adapters/opencode/instruction-chain.js +70 -0
  38. package/dist/adapters/opencode/layout.d.ts +19 -0
  39. package/dist/adapters/opencode/layout.js +34 -15
  40. package/dist/adoptability.d.ts +31 -1
  41. package/dist/adoptability.js +57 -0
  42. package/dist/cli-main.js +185 -102
  43. package/dist/core/adapter.d.ts +213 -61
  44. package/dist/core/compile.d.ts +2 -2
  45. package/dist/core/compile.js +57 -46
  46. package/dist/core/compose.d.ts +5 -3
  47. package/dist/core/compose.js +5 -3
  48. package/dist/core/config-schema.d.ts +14 -2
  49. package/dist/core/config-schema.js +20 -7
  50. package/dist/core/dialect.d.ts +54 -12
  51. package/dist/core/dialect.js +56 -0
  52. package/dist/core/eval-driver.d.ts +194 -0
  53. package/dist/core/eval-driver.js +3 -0
  54. package/dist/core/frontmatter-read.d.ts +10 -0
  55. package/dist/core/frontmatter-read.js +30 -3
  56. package/dist/core/guards.js +3 -1
  57. package/dist/core/hook-program.d.ts +27 -2
  58. package/dist/core/hook-program.js +29 -24
  59. package/dist/core/hook-protocol.d.ts +54 -0
  60. package/dist/core/install-reader.d.ts +18 -0
  61. package/dist/core/install-reader.js +88 -0
  62. package/dist/core/instruction-chain.d.ts +444 -0
  63. package/dist/core/instruction-chain.js +292 -0
  64. package/dist/core/instruction-weight.d.ts +96 -14
  65. package/dist/core/instruction-weight.js +65 -30
  66. package/dist/core/layout.d.ts +220 -33
  67. package/dist/core/layout.js +115 -1
  68. package/dist/core/lethal-trifecta.d.ts +12 -7
  69. package/dist/core/lethal-trifecta.js +13 -13
  70. package/dist/core/live-driver.d.ts +137 -0
  71. package/dist/core/live-driver.js +14 -0
  72. package/dist/core/markdown.d.ts +23 -0
  73. package/dist/core/markdown.js +77 -28
  74. package/dist/core/orphans.js +9 -7
  75. package/dist/core/settings-codec.d.ts +17 -0
  76. package/dist/core/settings-codec.js +56 -0
  77. package/dist/core/surface-discovery.d.ts +2 -2
  78. package/dist/core/surface-discovery.js +24 -12
  79. package/dist/core/surface-scopes.d.ts +26 -6
  80. package/dist/core/surface-scopes.js +52 -11
  81. package/dist/core/validate.js +16 -3
  82. package/dist/coverage-artifact.d.ts +3 -2
  83. package/dist/coverage-artifact.js +6 -5
  84. package/dist/eval-cache.d.ts +6 -1
  85. package/dist/eval-cache.js +11 -1
  86. package/dist/eval.d.ts +16 -108
  87. package/dist/eval.js +36 -2
  88. package/dist/harness-test.d.ts +3 -63
  89. package/dist/hook-install.d.ts +12 -1
  90. package/dist/hook-install.js +12 -1
  91. package/dist/hook-runtime.js +4 -2
  92. package/dist/hook-state-store.js +3 -1
  93. package/dist/local-files-tracked.d.ts +17 -0
  94. package/dist/local-files-tracked.js +70 -0
  95. package/dist/local-files.d.ts +62 -0
  96. package/dist/local-files.js +183 -0
  97. package/dist/observe.d.ts +3 -2
  98. package/dist/observe.js +7 -6
  99. package/dist/plugin-loader.d.ts +1 -1
  100. package/dist/plugin-loader.js +43 -36
  101. package/dist/scan-behavioral.d.ts +34 -25
  102. package/dist/scan-behavioral.js +122 -58
  103. package/dist/scan-core.js +37 -18
  104. package/dist/scan-files.d.ts +1 -1
  105. package/dist/scan-files.js +53 -33
  106. package/dist/scan-trigger-suggest.d.ts +0 -21
  107. package/dist/scan-trigger-suggest.js +0 -23
  108. package/dist/scan.d.ts +4 -4
  109. package/dist/scan.js +120 -84
  110. package/dist/skill-harness.d.ts +21 -5
  111. package/dist/skill-harness.js +29 -11
  112. package/dist/surface-discovery-fs.d.ts +2 -0
  113. package/dist/surface-discovery-fs.js +108 -6
  114. package/dist/test-coverage-files.js +24 -17
  115. package/dist/test-coverage.d.ts +9 -3
  116. package/dist/test-coverage.js +32 -22
  117. package/dist/verify-plugin-guards.js +1 -1
  118. package/package.json +1 -1
  119. package/dist/skill-reachability.d.ts +0 -68
  120. package/dist/skill-reachability.js +0 -205
  121. /package/dist/{dialect-drift.d.ts → adapters/claude-code/dialect-drift.d.ts} +0 -0
  122. /package/dist/{dialect-drift.js → adapters/claude-code/dialect-drift.js} +0 -0
@@ -0,0 +1,88 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.declaresVigilesDependency = declaresVigilesDependency;
4
+ exports.buildInstallReader = buildInstallReader;
5
+ /**
6
+ * The bounded reader an adapter's `advisories` is handed — built HERE, by the
7
+ * domain, never by the adapter.
8
+ *
9
+ * 🔴 WHY A READER AND NOT A ROOT. It is the same rule `detect(exists)` follows:
10
+ * an adapter given a directory and `node:fs` could enumerate anything under it,
11
+ * so registering an adapter would change what vigiles reads in someone's
12
+ * repository. {@link InstallReader.repo} answers only for paths that adapter
13
+ * `claims`, and spells a refusal exactly like an absence — so an adapter cannot
14
+ * even probe for the existence of a file outside its own surface.
15
+ *
16
+ * Two facts travel as VALUES rather than reads, because the shipped checks need
17
+ * them from files NO adapter claims: the repo's `package.json`, and vigiles's
18
+ * own package inside `node_modules`. Reading those is the domain's business, so
19
+ * the domain reads them once and passes the answer instead of widening the port.
20
+ */
21
+ const node_fs_1 = require("node:fs");
22
+ const node_os_1 = require("node:os");
23
+ const node_path_1 = require("node:path");
24
+ /** Read a file, or null when it is missing or unreadable. Never throws. */
25
+ function readOrNull(absolute) {
26
+ try {
27
+ return (0, node_fs_1.existsSync)(absolute) ? (0, node_fs_1.readFileSync)(absolute, "utf-8") : null;
28
+ }
29
+ catch {
30
+ return null;
31
+ }
32
+ }
33
+ /** Directory names directly under `dir`, or [] when it is not readable. */
34
+ function dirNames(dir) {
35
+ try {
36
+ return (0, node_fs_1.readdirSync)(dir, { withFileTypes: true })
37
+ .filter((e) => e.isDirectory())
38
+ .map((e) => e.name);
39
+ }
40
+ catch {
41
+ return [];
42
+ }
43
+ }
44
+ /**
45
+ * Is `vigiles` a declared dependency of this `package.json` text? Pure. Any of
46
+ * the four dependency fields counts — the question is "did this repo take
47
+ * vigiles on", not how. The vigiles repo itself is not a consumer: it IS the
48
+ * package, so it answers false.
49
+ */
50
+ function declaresVigilesDependency(pkgJson) {
51
+ let pkg;
52
+ try {
53
+ pkg = JSON.parse(pkgJson);
54
+ }
55
+ catch {
56
+ return false;
57
+ }
58
+ if (pkg.name === "vigiles")
59
+ return false;
60
+ return [
61
+ "dependencies",
62
+ "devDependencies",
63
+ "peerDependencies",
64
+ "optionalDependencies",
65
+ ].some((f) => {
66
+ const deps = pkg[f];
67
+ return typeof deps === "object" && deps !== null && "vigiles" in deps;
68
+ });
69
+ }
70
+ /**
71
+ * Build the reader for one adapter over one repo root.
72
+ *
73
+ * `home` is injectable so the machine read is testable without a real `$HOME`;
74
+ * it defaults to the user's own. Everything it feeds is advisory-only.
75
+ */
76
+ function buildInstallReader(adapter, root, opts = {}) {
77
+ const pkgJson = readOrNull((0, node_path_1.join)(root, "package.json"));
78
+ const home = opts.home ?? (0, node_os_1.homedir)();
79
+ return {
80
+ repo: (repoRelative) => adapter.claims(repoRelative)
81
+ ? readOrNull((0, node_path_1.resolve)(root, repoRelative))
82
+ : null,
83
+ home: (homeRelative) => readOrNull((0, node_path_1.join)(home, homeRelative)),
84
+ repoDependsOnVigiles: pkgJson !== null && declaresVigilesDependency(pkgJson),
85
+ vendoredSkillNames: dirNames((0, node_path_1.join)(root, "node_modules", "vigiles", "skills")),
86
+ };
87
+ }
88
+ //# sourceMappingURL=install-reader.js.map
@@ -0,0 +1,444 @@
1
+ /**
2
+ * The INSTRUCTION CHAIN — which of a repo's instruction-shaped files a harness
3
+ * actually LOADS at a repo-root session, in what order, and for every one it
4
+ * does not load, WHY.
5
+ *
6
+ * 🔴 WHY THIS IS A PORT METHOD AND NOT A LIST OF GLOBS. It replaced
7
+ * `HarnessDialect.instructionBudget.alwaysLoaded` — an array of glob strings an
8
+ * adapter wrote and the CORE interpreted, with its own `matchesGlob` plus a
9
+ * private repo walk in `scan.ts` (`readAlwaysLoaded`: `glob.endsWith("/**")` →
10
+ * recurse, `glob.startsWith("**\/")` → walk every non-dot directory). Three
11
+ * things were wrong with that at once, and they share one cause:
12
+ *
13
+ * 1. **An adapter drove the domain's walk.** `codexDialect` shipped
14
+ * `"**\/AGENTS.md"`, so registering that adapter made vigiles read every
15
+ * directory of somebody else's repository. The whole discovery refactor
16
+ * (`./surface-discovery.ts`) exists to make that impossible for SURFACES;
17
+ * instructions had a side door. The discovery lint did not catch it
18
+ * because that lint polices EXCLUSION — which paths are skipped — not who
19
+ * chose the walk.
20
+ * 2. **The printed weight was wrong on BOTH harnesses, in opposite
21
+ * directions.** Codex does not load every `AGENTS.md` in a repo: from the
22
+ * project root it walks DOWN to the cwd taking at most one file per
23
+ * directory, so a sibling package's file pays into a budget no session
24
+ * ever pays. Claude Code's `paths:`-scoped rules and its nested
25
+ * `CLAUDE.md` files load ON DEMAND, not at launch, so counting them
26
+ * inflates the one number the whole feature exists to report.
27
+ * 3. **An uncommitted file was scored.** `CLAUDE.local.md` was in the glob
28
+ * list, so a published grade depended on a gitignored file — irreproducible
29
+ * between two teammates on the same commit, and invisible to the browser
30
+ * twin, which reads a GitHub tree and can never see it.
31
+ *
32
+ * None of the three is expressible as a better glob, because none of them is a
33
+ * question about WHERE a file is. Whether a rule loads depends on its own
34
+ * frontmatter; whether a committed file loads depends on a SIBLING file
35
+ * (`AGENTS.override.md` takes the directory's one slot); and which files are
36
+ * candidates at all depends on the REPOSITORY'S OWN SETTINGS — Codex's
37
+ * `project_doc_fallback_filenames` and Claude Code's `claudeMdExcludes` are two
38
+ * independent vendors arriving at the same shape. Content, siblings, settings:
39
+ * a path lookup can see none of them, which is what makes this a method.
40
+ *
41
+ * ── WHAT THE METHOD MAY AND MAY NOT DO ──────────────────────────────────────
42
+ * It receives the map the DOMAIN enumerated ({@link instructionCandidatePaths})
43
+ * and returns a classification of those keys. It never names a root, never
44
+ * reaches a `node:` module, and is pure in its argument. The properties are
45
+ * asserted for every implementation in `src/adapter-properties.test.ts`:
46
+ *
47
+ * (i) `loaded ∪ unloaded ⊆ keys(F)` — an adapter cannot widen the read
48
+ * (ii) every `imports[i].path` literally occurs in `F[imports[i].from]`
49
+ * (iii) every `patterns[i].pattern` likewise
50
+ * (iv) pure and monotone: `chain(F)` is stable, and adding files that are
51
+ * not instruction-shaped changes nothing
52
+ *
53
+ * (ii) is the one worth reading twice. An import IS a widening of the read —
54
+ * `@docs/style.md` points outside the dot-directory bound — and it is allowed
55
+ * anyway, because the bound exists so that REGISTERING AN ADAPTER cannot widen
56
+ * what vigiles reads in someone else's repository, and an `@import` token is a
57
+ * concrete path written by the REPOSITORY OWNER in their own instruction file.
58
+ * The adapter only FINDS it; it does not choose it, which is exactly what (ii)
59
+ * makes checkable. Leaving imports out would under-report the weight, and an
60
+ * under-report reads as "you are fine" — the failure mode this whole feature
61
+ * exists to prevent.
62
+ */
63
+ import type { PluginLayout } from "./layout.js";
64
+ /** What a file IS to the harness that loads it. */
65
+ export type InstructionRole =
66
+ /** The committed team file at a directory's root (`CLAUDE.md`, `AGENTS.md`). */
67
+ "root"
68
+ /**
69
+ * One machine's file beside it (`CLAUDE.local.md`). NOT Codex's
70
+ * `AGENTS.override.md`: the vendor documents that as the directory's
71
+ * first-priority instruction file, and only its global copy as temporary.
72
+ */
73
+ | "root-local"
74
+ /** A file under {@link PluginLayout.rulesDir}. */
75
+ | "rule"
76
+ /** A repo-configured alternate name (Codex `project_doc_fallback_filenames`). */
77
+ | "fallback"
78
+ /** Reached through an `@path` token in a loaded file, not by location. */
79
+ | "import";
80
+ /**
81
+ * Team instruction (committed) or one machine's?
82
+ *
83
+ * 🔴 `"local"` IS READ AND LINTED, NEVER SCORED, and that is a decision with a
84
+ * measurement behind it rather than a preference. The browser twin reads a
85
+ * GitHub tree, so it can never see a gitignored file; any design that scored
86
+ * one would put the CLI and the browser permanently out of agreement about the
87
+ * same commit, and would make a published grade irreproducible between two
88
+ * teammates. So the weight carries TWO numbers — `committedTotal` (what a
89
+ * teammate or CI sees) and `effectiveTotal` (what this working copy actually
90
+ * loads) — and only the first is judged against the budget.
91
+ */
92
+ export type InstructionScope = "repo" | "local";
93
+ /** One file the harness loads, and what it is to it. */
94
+ export interface LoadedInstruction {
95
+ /** Repo-relative POSIX path — always a key of the map that was handed in. */
96
+ readonly path: string;
97
+ readonly role: InstructionRole;
98
+ readonly scope: InstructionScope;
99
+ /**
100
+ * Set only on `role: "import"` — WHO named this file, and with what text.
101
+ *
102
+ * 🔴 IT IS A FIELD AND NOT A PRINT-SITE LOOKUP BECAUSE THE READER CANNOT
103
+ * DECOMPOSE THE NUMBER WITHOUT IT. `AGENTS.md` appearing in a CLAUDE CODE
104
+ * weight looks like a bug — whether it got there by LOCATION or because
105
+ * somebody wrote an import is a difference the size cannot show — and the only
106
+ * thing that makes it legible, and actionable, is the line the user wrote:
107
+ * `via @AGENTS.md in CLAUDE.md`. A total a reader cannot take apart is the
108
+ * failure mode this whole report exists to prevent.
109
+ */
110
+ readonly via?: {
111
+ readonly from: string;
112
+ readonly token: string;
113
+ };
114
+ }
115
+ /**
116
+ * Why a file the domain enumerated is NOT in the loaded chain.
117
+ *
118
+ * A tagged union so an unloaded entry WITHOUT a reason is unrepresentable.
119
+ * That is what issue #262's `shadows` field was reaching for and could not be:
120
+ * "shadowing" is not a semantic the core can own, because what differs between
121
+ * harnesses is the COMBINATION RULE, not the fact of hiding — Claude Code
122
+ * APPENDS `CLAUDE.local.md` after `CLAUDE.md` (both are in context), Codex takes
123
+ * `AGENTS.override.md` INSTEAD OF `AGENTS.md`. The ordered `loaded` list already
124
+ * expresses the first; `{kind:"replaced"}` expresses the second.
125
+ */
126
+ export type NotLoadedReason =
127
+ /** Another file took this directory's one slot — Codex reads at most one. */
128
+ {
129
+ readonly kind: "replaced";
130
+ readonly by: string;
131
+ }
132
+ /** Loaded only when the agent reads a matching file — never at launch. */
133
+ | {
134
+ readonly kind: "on-demand";
135
+ readonly when: "path-scoped" | "subdirectory";
136
+ }
137
+ /**
138
+ * A setting removed it — Claude Code's `claudeMdExcludes`.
139
+ *
140
+ * 🔴 `byScope` FOR THE SAME REASON IT EXISTS ON `superseded` BELOW, and it was
141
+ * missing here while being right there. The patterns from
142
+ * `.claude/settings.json` and from its gitignored `.local` sibling were
143
+ * flattened into one list, so an exclusion nobody else has removed the file
144
+ * from `committedTotal` too. Measured 2026-09-22 on a repo with a 500-char
145
+ * `.claude/rules/policy.md`:
146
+ *
147
+ * no excludes committed=800 effective=800
148
+ * excluded in settings.json committed=300 effective=300
149
+ * excluded in settings.LOCAL.json committed=300 effective=300 <- wrong
150
+ *
151
+ * The third row is a gitignored file lowering the PUBLISHED score, which is
152
+ * the one thing the two-number contract exists to prevent, and it put the CLI
153
+ * permanently out of agreement with the browser engine that reads a GitHub
154
+ * tree and can never see that file.
155
+ */
156
+ | {
157
+ readonly kind: "excluded-by-settings";
158
+ readonly key: string;
159
+ /** The settings file whose pattern matched. */
160
+ readonly by: string;
161
+ readonly byScope: InstructionScope;
162
+ }
163
+ /**
164
+ * A file of a DIFFERENT instruction family is present, and its presence turns
165
+ * this whole family off — Claude Code reading `AGENTS.md` only when no
166
+ * `CLAUDE.md` counts.
167
+ *
168
+ * 🔴 NOT THE SAME THING AS `replaced`, AND CONFLATING THEM WOULD LOSE THE ONE
169
+ * FACT THAT MATTERS. `replaced` is Codex taking at most ONE file per
170
+ * directory out of a same-named family, so the loser is a near-copy of the
171
+ * winner in the same place. `superseded` is a cross-family switch: the
172
+ * superseding file may sit in a different directory (`.claude/CLAUDE.md`),
173
+ * carries entirely different content, and — the part `replaced` has no room
174
+ * for — MAY NOT BE COMMITTED.
175
+ *
176
+ * 🔴 WHICH IS WHY `byScope` IS A FIELD AND NOT A LOOKUP AT THE PRINT SITE.
177
+ * When the superseding file is `"local"`, a gitignored file has changed the
178
+ * MEMBERSHIP of the load, not just its size: a teammate on the same commit
179
+ * loads this file and this working copy does not. `weighInstructions` reads
180
+ * exactly this field to keep `committedTotal` right in that case — see
181
+ * `WeighedFile.supersededLocallyBy`. A consumer that only knew `by` would
182
+ * have to re-derive the scope from the path, which is the "second list"
183
+ * defect this redesign removes.
184
+ */
185
+ | {
186
+ readonly kind: "superseded";
187
+ /** The file whose presence did it. */
188
+ readonly by: string;
189
+ /** Whether that file is committed, or one machine's. */
190
+ readonly byScope: InstructionScope;
191
+ };
192
+ export interface UnloadedInstruction extends LoadedInstruction {
193
+ readonly reason: NotLoadedReason;
194
+ }
195
+ /** A path a loaded file NAMES, the text that names it, and the file it is in. */
196
+ export interface NamedImport {
197
+ /** The repo-relative path the token resolves to. */
198
+ readonly path: string;
199
+ /**
200
+ * The token EXACTLY as the author wrote it (`@AGENTS.md`). Reported because
201
+ * it is the thing the user can act on — they typed that line — and because it
202
+ * is what the property test checks: a token must literally occur in `from`.
203
+ */
204
+ readonly token: string;
205
+ /** The loaded file that names it. */
206
+ readonly from: string;
207
+ }
208
+ /** A glob or URL a loaded file names, and the file that names it. */
209
+ export interface PatternFrom {
210
+ readonly pattern: string;
211
+ readonly from: string;
212
+ }
213
+ export interface InstructionChain {
214
+ /** Loaded at a repo-root session, in the order the harness concatenates them. */
215
+ readonly loaded: readonly LoadedInstruction[];
216
+ /** Instruction-shaped keys this harness does NOT load, each with why. */
217
+ readonly unloaded: readonly UnloadedInstruction[];
218
+ /**
219
+ * Concrete repo-relative paths the loaded files name as imports. The adapter
220
+ * only reports them; the domain reads them in a second, bounded pass, and an
221
+ * import that was named but not read is printed rather than dropped.
222
+ */
223
+ readonly imports: readonly NamedImport[];
224
+ /**
225
+ * Patterns or URLs the harness would expand at launch and the domain will NOT
226
+ * walk (OpenCode's `instructions: ["packages/*\/AGENTS.md"]`). Reported so the
227
+ * weight can say "plus N pattern(s) not weighed" instead of a number that is
228
+ * silently wrong. Never read.
229
+ */
230
+ readonly patterns: readonly PatternFrom[];
231
+ /**
232
+ * A loaded file whose ENTIRE content is import tokens — a REDIRECT, not
233
+ * instructions, and a finding rather than a footnote.
234
+ *
235
+ * 🔴 WHY THIS IS A NAMED SHAPE. Four of the six real imports in the measured
236
+ * corpus are `@AGENTS.md`, written when Claude Code did not yet read
237
+ * `AGENTS.md` natively (anthropics/claude-code#34235; it does since v2.1.277 —
238
+ * see `adapters/claude-code/dialect.ts`), and the idiom that follows is a
239
+ * `CLAUDE.md` holding that one line and nothing else. THE SHAPE DID NOT GO
240
+ * AWAY WITH THE VENDOR CHANGE: those files are still in those repositories,
241
+ * the `CLAUDE.md` beside them SUPPRESSES the `AGENTS.md`, and the vendor's own
242
+ * table still gives the case a row. Reported as a size, such a repo has
243
+ * a ~14-byte instruction file — a confident wrong answer about a repository
244
+ * that really loads tens of kilobytes. The report says "this file is a
245
+ * redirect, here is what it points at" instead of printing a reassuring
246
+ * number.
247
+ *
248
+ * BINARY, NEVER A THRESHOLD: after the frontmatter, the HTML comments and the
249
+ * blank lines are removed, EVERY remaining line is an import token. "Mostly
250
+ * imports" would be a number nobody can defend.
251
+ */
252
+ readonly redirects: readonly {
253
+ readonly path: string;
254
+ readonly to: readonly string[];
255
+ }[];
256
+ }
257
+ /** An empty chain — the answer for a harness with no instruction surface. */
258
+ export declare const EMPTY_CHAIN: InstructionChain;
259
+ /**
260
+ * The SHAPE of an instruction candidate — the domain's bound, stated the same
261
+ * way `SURFACE_SHAPES` states the surface one, and for the same reason: if an
262
+ * adapter could add a root we would be back to "registering an adapter widens
263
+ * the read in everyone's repository".
264
+ *
265
+ * These are CROSS-VENDOR shapes, not one harness's paths. `rules` is the name
266
+ * Claude Code (`.claude/rules`), Cursor (`.cursor/rules`) and Windsurf
267
+ * (`.windsurf/rules`) all use; the dot-directory is the variable, the shape name
268
+ * is not. Nothing here spells a harness's own directory.
269
+ *
270
+ * ⚠️ WHAT THIS CANNOT SEE, stated rather than assumed: a nested
271
+ * `packages/x/AGENTS.md` (neither vendor loads it at a root session — the chain
272
+ * classifies one as on-demand if it is handed one, and never goes looking);
273
+ * `~/.claude/CLAUDE.md`, `~/.codex/AGENTS.md` and every other home-directory
274
+ * file (reading `~` for a grade is wrong on its face); and a repo-configured
275
+ * fallback instruction name that is not markdown, because the root entry below
276
+ * is bounded to `.md` rather than to "every file in the repo root". That last
277
+ * one under-reports, which is the wrong direction, and it is the price of not
278
+ * reading a lockfile to grade a harness.
279
+ */
280
+ export declare const INSTRUCTION_SHAPES: ReadonlyArray<{
281
+ /** For messages and for the test that names each shape. */
282
+ readonly what: string;
283
+ /** Anchored, over repo-relative POSIX paths. */
284
+ readonly re: RegExp;
285
+ }>;
286
+ /**
287
+ * The files that carry SETTINGS for one layout — the parse target and the
288
+ * per-machine override beside it.
289
+ *
290
+ * 🔴 A SETTINGS SOURCE IS NOT AN INSTRUCTION, and keeping the two roles apart
291
+ * is the whole point of this function having its own name. These files are
292
+ * handed to `instructionChain` and are never weighed, never appear in `loaded`
293
+ * and never get a `role`: they are not read TO the model, they decide WHICH
294
+ * files are. Claude Code's `claudeMdExcludes` and Codex's
295
+ * `project_doc_fallback_filenames` both live in one, which is why they are in
296
+ * the bound at all.
297
+ *
298
+ * The `.local` sibling is DERIVED from `settingsPath` rather than listed,
299
+ * because a second list is the defect this whole redesign removes. It is
300
+ * advisory for the same reason `scope: "local"` is: it is gitignored by
301
+ * convention, so the browser twin can never see it, and anything that DEPENDED
302
+ * on it would make the two engines disagree. It can only narrow what loads.
303
+ */
304
+ /**
305
+ * `AGENTS.md` + `override` → `AGENTS.override.md`; `settings.json` + `local` →
306
+ * `settings.local.json`. The ONE place the "sibling file" spelling lives.
307
+ *
308
+ * Both vendors name a per-machine file by inserting a word before the
309
+ * extension, and they choose DIFFERENT words — Claude Code `local`, Codex
310
+ * `override` — so the word is the argument and the spelling is not. A file with
311
+ * no extension gets the word appended, which is the only reading that does not
312
+ * invent a dot.
313
+ */
314
+ export declare function siblingNamed(path: string, infix: string): string;
315
+ /** A settings file the chain parses, and whether a teammate has it too. */
316
+ export interface SettingsSource {
317
+ readonly path: string;
318
+ readonly scope: InstructionScope;
319
+ }
320
+ /**
321
+ * The settings files in precedence order, each carrying its SCOPE.
322
+ *
323
+ * The scope is the whole point: a pattern read out of a gitignored sibling may
324
+ * not change what a teammate on this commit is scored for. Callers that only
325
+ * need the paths use {@link settingsSourcePaths}, which is this list flattened.
326
+ */
327
+ export declare function settingsSources(layout: PluginLayout): readonly SettingsSource[];
328
+ export declare function settingsSourcePaths(layout: PluginLayout): readonly string[];
329
+ /** Does this repo-relative path match one of the {@link INSTRUCTION_SHAPES}? */
330
+ export declare function isInstructionShaped(path: string): boolean;
331
+ /**
332
+ * The bounded candidate set: every path a harness may be ASKED about.
333
+ *
334
+ * Pure and storage-blind on purpose, exactly as `discoverSurfaces` is — the
335
+ * disk walk (`src/surface-discovery-fs.ts`) and the browser file-map twin
336
+ * (`src/scan-files.ts`) each enumerate from their own storage and call THIS for
337
+ * the decision, so the pair cannot disagree about what is a candidate.
338
+ */
339
+ export declare function instructionCandidatePaths(paths: readonly string[], layout: PluginLayout): readonly string[];
340
+ /**
341
+ * Is `token` a path this repo could hold, and safe to resolve against its root?
342
+ *
343
+ * Refuses an absolute path, a `~` home reference, and anything with a `..`
344
+ * segment — an instruction file that points outside the repository is not a
345
+ * fact about the repository, and following it would let a file decide what
346
+ * vigiles opens on the machine running it.
347
+ */
348
+ export declare function isRepoRootedImport(token: string): boolean;
349
+ /**
350
+ * Where an `@import` token inside `from` actually points.
351
+ *
352
+ * 🔴 RELATIVE TO THE IMPORTING FILE, NOT TO THE REPOSITORY ROOT, and that is a
353
+ * MEASUREMENT rather than a reading of the docs — Claude Code 2.1.278, fixture
354
+ * `q3-relative` in `test/fixtures/instruction-chain-vendor/`. The case is built
355
+ * so the answer cannot be "it found nothing": `.claude/CLAUDE.md` holds
356
+ * `@notes.md` and BOTH candidates exist, each with its own codeword.
357
+ *
358
+ * recited: SAIGA-8181 (`.claude/notes.md`) not TAPIR-6262 (`notes.md`)
359
+ * hook: .claude/notes.md load_reason: include parent: .claude/CLAUDE.md
360
+ *
361
+ * Resolving against the root instead reports that file unread and charges the
362
+ * weight of a DIFFERENT file that happens to share its name — wrong in both
363
+ * directions at once, and silent.
364
+ *
365
+ * ⚠️ A TOKEN WITH `..` STAYS REFUSED even though this resolution would make
366
+ * some of them land inside the repository (`@../notes.md` from `.claude/`).
367
+ * {@link isRepoRootedImport} rejects them before this is called, and lifting
368
+ * that is a separate decision needing its own fixture: the refusal is what
369
+ * stops an instruction file deciding what vigiles opens on the machine running
370
+ * it, and "it happens to stay inside" is a property of one path, not a rule.
371
+ */
372
+ export declare function resolveImportPath(from: string, token: string): string;
373
+ /**
374
+ * Read the `@import` paths the loaded files NAME — ONE LEVEL, no recursion.
375
+ * Returns a NEW map: the candidates handed in, plus whatever they named.
376
+ *
377
+ * 🔴 THIS READS OUTSIDE THE DOT-DIRECTORY BOUND, DELIBERATELY, AND THE REASON IS
378
+ * WHO CHOSE THE PATH. The bound exists so that REGISTERING AN ADAPTER cannot
379
+ * widen what vigiles reads in someone else's repository. An `@import` token is a
380
+ * concrete path written by the REPOSITORY OWNER in their own instruction file;
381
+ * the adapter only finds it, and `adapter-properties.test.ts` asserts exactly
382
+ * that — every reported import literally occurs in the file that reports it.
383
+ *
384
+ * 🔴 ONE LEVEL IS A MEASUREMENT, NOT A SHORTCUT — DO NOT "IMPROVE" IT INTO A
385
+ * RECURSIVE PASS. Across a corpus of 198 real `CLAUDE.md` files scraped from
386
+ * public repositories (July sample; the grep finds `@name.md` shapes only),
387
+ * exactly SIX files carried an import at all — 3%:
388
+ *
389
+ * 4 @AGENTS.md
390
+ * 1 @docs/architecture.md
391
+ * 1 @.maister/docs/INDEX.md
392
+ *
393
+ * Every one is a single concrete path at depth 1. Nothing in that corpus needs
394
+ * recursion, a depth budget or an exclude pass, and a recursive walk driven by
395
+ * strings found in files is the exact defect zernie/vigiles#262 is about.
396
+ *
397
+ * 🔴 AND THE SAME IS NOW MEASURED FOR `AGENTS.md`, WHICH USED TO BE THE HOLE IN
398
+ * THIS BOUND. The corpus above is `CLAUDE.md` BY CONSTRUCTION, so it said
399
+ * nothing about the family Claude Code reads natively since v2.1.277 — and a
400
+ * one-level bound justified by a corpus that could not contain the file is not
401
+ * justified, it is extrapolated. Measured over the same sample, by the same
402
+ * method, carrying the same two caveats (July sample of public repositories;
403
+ * the grep finds `@name.md` shapes only): 214 real `AGENTS.md` files, THREE
404
+ * carry an import at all — 1.4%:
405
+ *
406
+ * 1 @tasks/BASED.md
407
+ * 1 @ai-rules/rule-loading.md
408
+ * 1 @AGENTS.local.md
409
+ *
410
+ * Every one is a single concrete path at depth 1 — the same SHAPE and the same
411
+ * RARITY as the six on the `CLAUDE.md` side (3%). So one level is measured on
412
+ * both families rather than assumed to carry over from one.
413
+ *
414
+ * ⏳ THE THIRD OF THOSE THREE IS NOT AN ORDINARY IMPORT, and it is an OPEN
415
+ * QUESTION rather than a decided one: `AGENTS.local.md` is a name Claude Code
416
+ * lists under "Not read", so an explicit `@` token names a file the loader may
417
+ * never open. Both readings and the observation that settles them are at
418
+ * `isNeverRead` in `adapters/claude-code/instruction-chain.ts` — one harness's
419
+ * list belongs in one harness's adapter, not in the domain.
420
+ *
421
+ * ⚠️ AND THE VENDOR PUTS A NUMBER ON THE THING THIS BOUND APPROXIMATES, which
422
+ * the measurement above does not repeal: "Imported files can recursively import
423
+ * other files, with a maximum depth of FOUR HOPS" (same page, read 2026-09-21).
424
+ * So one level is a bound on what this reads, chosen because neither corpus has
425
+ * a second hop — not a claim that a second hop cannot exist. A repository that
426
+ * uses them is under-reported by the nested size, and the honest form of that
427
+ * is the sentence below rather than a depth counter nothing exercises.
428
+ *
429
+ * 🔴 AND THE SIX ARE WHERE THE NUMBER IS MOST WRONG WITHOUT THIS PASS. Four of
430
+ * them are `@AGENTS.md` — the workaround for Claude Code not yet reading
431
+ * `AGENTS.md` natively (anthropics/claude-code#34235; reversed in v2.1.277, see
432
+ * `adapters/claude-code/dialect.ts`). Skipping imports would still miss that
433
+ * file's whole size in exactly those repositories, because the vendor's rule is
434
+ * that a `CLAUDE.md` SUPPRESSES `AGENTS.md` — so the import is the only way in,
435
+ * and dropping it is an under-report, which reads as "you are fine".
436
+ *
437
+ * ⚠️ WHAT ONE LEVEL COSTS, stated rather than implied: a transitive import (an
438
+ * imported file that imports again) is a real Claude Code feature, and its
439
+ * nested size is NOT counted. NEITHER corpus — 198 `CLAUDE.md`, 214
440
+ * `AGENTS.md`, 412 files, nine imports between them — holds one; if a real case
441
+ * shows up, those measurements are the thing to redo, not this loop.
442
+ */
443
+ export declare function resolveImports(layout: PluginLayout, files: Readonly<Record<string, string>>, read: (path: string) => string | undefined): Record<string, string>;
444
+ //# sourceMappingURL=instruction-chain.d.ts.map