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.
- package/dist/adapter-conformance.d.ts +1 -1
- package/dist/adapter-conformance.js +106 -25
- package/dist/adapter-registry.d.ts +61 -14
- package/dist/adapter-registry.js +78 -10
- package/dist/adapter.d.ts +23 -2
- package/dist/adapter.js +13 -1
- package/dist/adapters/claude-code/adapter.d.ts +32 -2
- package/dist/adapters/claude-code/adapter.js +44 -23
- package/dist/adapters/claude-code/agent-runtime.js +3 -1
- package/dist/adapters/claude-code/dialect.js +87 -21
- package/dist/adapters/claude-code/effect-region.js +3 -1
- package/dist/adapters/claude-code/hook-protocol.js +16 -0
- package/dist/adapters/claude-code/instruction-chain.d.ts +25 -0
- package/dist/adapters/claude-code/instruction-chain.js +626 -0
- package/dist/adapters/claude-code/layout.d.ts +2 -2
- package/dist/adapters/claude-code/layout.js +42 -8
- package/dist/adapters/claude-code/model-access.d.ts +41 -0
- package/dist/adapters/claude-code/model-access.js +46 -0
- package/dist/adapters/claude-code/skill-reachability.d.ts +125 -0
- package/dist/adapters/claude-code/skill-reachability.js +111 -0
- package/dist/adapters/claude-code/skill-runtime.js +3 -1
- package/dist/adapters/codex/adapter.d.ts +39 -2
- package/dist/adapters/codex/adapter.js +29 -29
- package/dist/adapters/codex/dialect.js +11 -6
- package/dist/adapters/codex/eval.d.ts +10 -0
- package/dist/adapters/codex/eval.js +48 -1
- package/dist/adapters/codex/hook-protocol.d.ts +2 -1
- package/dist/adapters/codex/hook-protocol.js +10 -0
- package/dist/adapters/codex/instruction-chain.d.ts +40 -0
- package/dist/adapters/codex/instruction-chain.js +105 -0
- package/dist/adapters/codex/layout.d.ts +1 -1
- package/dist/adapters/codex/layout.js +41 -14
- package/dist/adapters/opencode/adapter.d.ts +33 -2
- package/dist/adapters/opencode/adapter.js +36 -36
- package/dist/adapters/opencode/dialect.js +2 -2
- package/dist/adapters/opencode/instruction-chain.d.ts +37 -0
- package/dist/adapters/opencode/instruction-chain.js +70 -0
- package/dist/adapters/opencode/layout.d.ts +19 -0
- package/dist/adapters/opencode/layout.js +34 -15
- package/dist/adoptability.d.ts +31 -1
- package/dist/adoptability.js +57 -0
- package/dist/cli-main.js +185 -102
- package/dist/core/adapter.d.ts +213 -61
- package/dist/core/compile.d.ts +2 -2
- package/dist/core/compile.js +57 -46
- package/dist/core/compose.d.ts +5 -3
- package/dist/core/compose.js +5 -3
- package/dist/core/config-schema.d.ts +14 -2
- package/dist/core/config-schema.js +20 -7
- package/dist/core/dialect.d.ts +54 -12
- package/dist/core/dialect.js +56 -0
- package/dist/core/eval-driver.d.ts +194 -0
- package/dist/core/eval-driver.js +3 -0
- package/dist/core/frontmatter-read.d.ts +10 -0
- package/dist/core/frontmatter-read.js +30 -3
- package/dist/core/guards.js +3 -1
- package/dist/core/hook-program.d.ts +27 -2
- package/dist/core/hook-program.js +29 -24
- package/dist/core/hook-protocol.d.ts +54 -0
- package/dist/core/install-reader.d.ts +18 -0
- package/dist/core/install-reader.js +88 -0
- package/dist/core/instruction-chain.d.ts +444 -0
- package/dist/core/instruction-chain.js +292 -0
- package/dist/core/instruction-weight.d.ts +96 -14
- package/dist/core/instruction-weight.js +65 -30
- package/dist/core/layout.d.ts +220 -33
- package/dist/core/layout.js +115 -1
- package/dist/core/lethal-trifecta.d.ts +12 -7
- package/dist/core/lethal-trifecta.js +13 -13
- package/dist/core/live-driver.d.ts +137 -0
- package/dist/core/live-driver.js +14 -0
- package/dist/core/markdown.d.ts +23 -0
- package/dist/core/markdown.js +77 -28
- package/dist/core/orphans.js +9 -7
- package/dist/core/settings-codec.d.ts +17 -0
- package/dist/core/settings-codec.js +56 -0
- package/dist/core/surface-discovery.d.ts +2 -2
- package/dist/core/surface-discovery.js +24 -12
- package/dist/core/surface-scopes.d.ts +26 -6
- package/dist/core/surface-scopes.js +52 -11
- package/dist/core/validate.js +16 -3
- package/dist/coverage-artifact.d.ts +3 -2
- package/dist/coverage-artifact.js +6 -5
- package/dist/eval-cache.d.ts +6 -1
- package/dist/eval-cache.js +11 -1
- package/dist/eval.d.ts +16 -108
- package/dist/eval.js +36 -2
- package/dist/harness-test.d.ts +3 -63
- package/dist/hook-install.d.ts +12 -1
- package/dist/hook-install.js +12 -1
- package/dist/hook-runtime.js +4 -2
- package/dist/hook-state-store.js +3 -1
- package/dist/local-files-tracked.d.ts +17 -0
- package/dist/local-files-tracked.js +70 -0
- package/dist/local-files.d.ts +62 -0
- package/dist/local-files.js +183 -0
- package/dist/observe.d.ts +3 -2
- package/dist/observe.js +7 -6
- package/dist/plugin-loader.d.ts +1 -1
- package/dist/plugin-loader.js +43 -36
- package/dist/scan-behavioral.d.ts +34 -25
- package/dist/scan-behavioral.js +122 -58
- package/dist/scan-core.js +37 -18
- package/dist/scan-files.d.ts +1 -1
- package/dist/scan-files.js +53 -33
- package/dist/scan-trigger-suggest.d.ts +0 -21
- package/dist/scan-trigger-suggest.js +0 -23
- package/dist/scan.d.ts +4 -4
- package/dist/scan.js +120 -84
- package/dist/skill-harness.d.ts +21 -5
- package/dist/skill-harness.js +29 -11
- package/dist/surface-discovery-fs.d.ts +2 -0
- package/dist/surface-discovery-fs.js +108 -6
- package/dist/test-coverage-files.js +24 -17
- package/dist/test-coverage.d.ts +9 -3
- package/dist/test-coverage.js +32 -22
- package/dist/verify-plugin-guards.js +1 -1
- package/package.json +1 -1
- package/dist/skill-reachability.d.ts +0 -68
- package/dist/skill-reachability.js +0 -205
- /package/dist/{dialect-drift.d.ts → adapters/claude-code/dialect-drift.d.ts} +0 -0
- /package/dist/{dialect-drift.js → adapters/claude-code/dialect-drift.js} +0 -0
|
@@ -0,0 +1,626 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.localSiblingOf = localSiblingOf;
|
|
4
|
+
exports.claudeCodeInstructionChain = claudeCodeInstructionChain;
|
|
5
|
+
const Minimatch = () => require("minimatch").Minimatch;
|
|
6
|
+
const instruction_chain_js_1 = require("../../core/instruction-chain.js");
|
|
7
|
+
const frontmatter_read_js_1 = require("../../core/frontmatter-read.js");
|
|
8
|
+
const markdown_js_1 = require("../../core/markdown.js");
|
|
9
|
+
/**
|
|
10
|
+
* The settings key that removes files from the chain, and the frontmatter key
|
|
11
|
+
* that makes a rule load ON DEMAND instead of at launch. Both are Claude Code's
|
|
12
|
+
* own words; they live here and nowhere in the core, which is the boundary the
|
|
13
|
+
* `no-harness-names` lint polices.
|
|
14
|
+
*/
|
|
15
|
+
const EXCLUDES_KEY = "claudeMdExcludes";
|
|
16
|
+
const PATH_SCOPE_KEY = "paths";
|
|
17
|
+
/**
|
|
18
|
+
* The cross-tool instruction filename Claude Code now reads natively — "Claude
|
|
19
|
+
* Code can read `AGENTS.md` as your project instructions".
|
|
20
|
+
*
|
|
21
|
+
* 🔴 IT IS A LITERAL HERE, AND DELIBERATELY NOT A FIELD ON `PluginLayout`.
|
|
22
|
+
* `layout.instructionFile` answers "which file does this harness WRITE and
|
|
23
|
+
* own"; the answer is still `CLAUDE.md` — that is what `vigiles init` compiles
|
|
24
|
+
* into and what `detect` scores on. `AGENTS.md` is a file this harness READS
|
|
25
|
+
* and another harness owns, which is a different question, and putting it in
|
|
26
|
+
* the layout would make `layoutClaims` say Claude Code claims `AGENTS.md` —
|
|
27
|
+
* i.e. two registered adapters claiming the same path, which is the collision
|
|
28
|
+
* `claims` exists to prevent. So it lives beside {@link EXCLUDES_KEY} and
|
|
29
|
+
* {@link PATH_SCOPE_KEY}: Claude Code's own words, in Claude Code's adapter,
|
|
30
|
+
* and nowhere in the core.
|
|
31
|
+
*/
|
|
32
|
+
const AGENTS_FILE = "AGENTS.md";
|
|
33
|
+
/** The directory the vendor names in its "Not read" list. */
|
|
34
|
+
const AGENTS_DIR = ".agents";
|
|
35
|
+
/**
|
|
36
|
+
* The two cross-tool per-machine siblings, DERIVED through the one
|
|
37
|
+
* `siblingNamed` spelling rather than written out: Claude Code names its
|
|
38
|
+
* per-machine file with `local`, Codex with `override`, and both spellings of
|
|
39
|
+
* the `AGENTS.md` family are gitignored by convention.
|
|
40
|
+
*
|
|
41
|
+
* 🔴 USED FOR TWO DIFFERENT THINGS, and that is the point of it being a set
|
|
42
|
+
* rather than two comparisons inside {@link isNeverRead}. It answers "does this
|
|
43
|
+
* harness ever go looking for this name" (no), and it answers "is this file one
|
|
44
|
+
* machine's" — which is what {@link takeImportsOf} needs, because an imported
|
|
45
|
+
* file inherits the IMPORTER's scope, and a committed `AGENTS.md` importing
|
|
46
|
+
* `@AGENTS.local.md` would otherwise put a gitignored file into a published
|
|
47
|
+
* `committedTotal`. That is the very defect {@link InstructionScope} exists to
|
|
48
|
+
* prevent: the browser twin reads a GitHub tree and can never see the file.
|
|
49
|
+
*
|
|
50
|
+
* ⚠️ THE SECOND USE IS A CONVENTION, NOT A VENDOR RULE, and it is the SAME
|
|
51
|
+
* convention this module already applies to `CLAUDE.local.md` by name — the
|
|
52
|
+
* `.local.` / `.override.` infix means "one machine's", for both vendors. So
|
|
53
|
+
* this is the cross-tool family catching up with the rule its twin already
|
|
54
|
+
* follows, not a new one. What it costs if the convention is wrong in some
|
|
55
|
+
* repository — a committed `AGENTS.local.md` — is that the file leaves
|
|
56
|
+
* `committedTotal`; it stays in `effectiveTotal` and on its own breakdown row,
|
|
57
|
+
* so it is moved rather than dropped. Identical to the cost already accepted
|
|
58
|
+
* for `CLAUDE.local.md`.
|
|
59
|
+
*/
|
|
60
|
+
const CROSS_TOOL_LOCAL_LEAVES = new Set([
|
|
61
|
+
(0, instruction_chain_js_1.siblingNamed)(AGENTS_FILE, "local"),
|
|
62
|
+
(0, instruction_chain_js_1.siblingNamed)(AGENTS_FILE, "override"),
|
|
63
|
+
]);
|
|
64
|
+
/** Is this path one machine's file by NAME — `X.local.md` / `X.override.md`? */
|
|
65
|
+
function isPerMachineName(path) {
|
|
66
|
+
return CROSS_TOOL_LOCAL_LEAVES.has(path.slice(path.lastIndexOf("/") + 1));
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Cross-tool instruction files Claude Code NEVER reads, at any depth. Vendor:
|
|
70
|
+
*
|
|
71
|
+
* > **Not read**: `AGENTS.local.md`, `AGENTS.override.md`, or anything under a
|
|
72
|
+
* > `.agents/` directory.
|
|
73
|
+
*
|
|
74
|
+
* 🔴 A NAMED PREDICATE BECAUSE THE DEFAULT IS WRONG FOR THIS FAMILY, and it is
|
|
75
|
+
* wrong QUIETLY. {@link takeSubdirectories} recognises an instruction file by
|
|
76
|
+
* its LEAF NAME, so `.agents/AGENTS.md` — which the domain's bound really does
|
|
77
|
+
* enumerate, it is dot-directory markdown — would come out classified
|
|
78
|
+
* "on-demand". The number is the same either way (an on-demand file weighs
|
|
79
|
+
* nothing), so no total would move and nothing would go red; only the printed
|
|
80
|
+
* REASON would be a confident wrong answer about a file the harness will never
|
|
81
|
+
* open. That is the class this whole module exists to remove.
|
|
82
|
+
*
|
|
83
|
+
* The two sibling names come from {@link CROSS_TOOL_LOCAL_LEAVES}, which
|
|
84
|
+
* derives them through the one `siblingNamed` spelling rather than writing them
|
|
85
|
+
* out, for the reason {@link localSiblingOf} is derived: a second literal is a
|
|
86
|
+
* second thing to keep in step.
|
|
87
|
+
*
|
|
88
|
+
* ⚠️ WHAT THIS DOES NOT DO, stated rather than discovered later: a file it
|
|
89
|
+
* refuses is named by NOTHING — it appears in neither `loaded` nor `unloaded`.
|
|
90
|
+
* `NotLoadedReason` has no member for "this harness never reads this name", and
|
|
91
|
+
* inventing one for a single vendor sentence would put a branch into every
|
|
92
|
+
* consumer of the union for a file that weighs nothing. Silence here is the
|
|
93
|
+
* same answer the bound already gives every non-instruction file.
|
|
94
|
+
*
|
|
95
|
+
* ⏳ AND AN EXPLICIT `@` TOKEN NAMING ONE OF THESE FILES IS AN OPEN QUESTION,
|
|
96
|
+
* raised by the corpus rather than imagined: of the three real imports in the
|
|
97
|
+
* 214-file `AGENTS.md` sample (`core/instruction-chain.ts#resolveImports`), ONE
|
|
98
|
+
* is `@AGENTS.local.md` — a name this predicate refuses, written on purpose by
|
|
99
|
+
* the file beside it. The vendor's two sentences are peers in one bulleted
|
|
100
|
+
* list and neither qualifies the other:
|
|
101
|
+
*
|
|
102
|
+
* > **Inside each `AGENTS.md`**: `@path` imports are expanded …
|
|
103
|
+
* > **Not read**: `AGENTS.local.md`, `AGENTS.override.md`, or anything under a
|
|
104
|
+
* > `.agents/` directory
|
|
105
|
+
*
|
|
106
|
+
* Read as DISCOVERY, the second says where the loader goes looking and the
|
|
107
|
+
* token still loads the file. Read FLATLY, the name is never opened at all and
|
|
108
|
+
* the token resolves to nothing. {@link takeImportsOf} does not consult this
|
|
109
|
+
* predicate, so today the token IS honoured — but that is the behaviour that
|
|
110
|
+
* was already there, not a verdict reached here, and it is left alone rather
|
|
111
|
+
* than changed on a coin-flip. Settled by the same instrument as the exclusion
|
|
112
|
+
* question at {@link supersederOf}: one session, the documented `AGENTS.md
|
|
113
|
+
* loaded` line, or an `InstructionsLoaded` hook transcript.
|
|
114
|
+
*
|
|
115
|
+
* 🔴 WHAT IS DECIDED, BECAUSE IT IS RIGHT UNDER BOTH READINGS, is the SCOPE
|
|
116
|
+
* such a file gets if it is counted at all — see {@link isPerMachineName} and
|
|
117
|
+
* {@link takeImportsOf}. Under the flat reading the file is never taken and
|
|
118
|
+
* that code is simply unreachable; under the discovery reading it is taken, and
|
|
119
|
+
* scoring a `.local.` file as `repo` would put a gitignored file into a
|
|
120
|
+
* published `committedTotal`. Neither answer to the open question makes `repo`
|
|
121
|
+
* correct, which is what makes this one safe to take now.
|
|
122
|
+
*/
|
|
123
|
+
function isNeverRead(path) {
|
|
124
|
+
const leaf = path.slice(path.lastIndexOf("/") + 1);
|
|
125
|
+
return path.startsWith(`${AGENTS_DIR}/`) || CROSS_TOOL_LOCAL_LEAVES.has(leaf);
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* `@path/to/file.md` — how Claude Code names another file from inside an
|
|
129
|
+
* instruction file.
|
|
130
|
+
*
|
|
131
|
+
* 🔴 THIS IS A MODEL OF A LOADER, NOT A PARSE OF A FORMAT, and a reader who
|
|
132
|
+
* misses that will "fix" it in the wrong direction. `@path` is not in CommonMark
|
|
133
|
+
* or in any other specification, so there is no authority to parse against:
|
|
134
|
+
* markdown-it hands back `@AGENTS.md` as ordinary TEXT, correctly, because to
|
|
135
|
+
* markdown it is ordinary text. What we assume, stated so it can be argued with:
|
|
136
|
+
* a leading `@` followed by a path, written in PROSE (never inside a code fence
|
|
137
|
+
* or a code span), naming a markdown file. Anything beyond that — how the real
|
|
138
|
+
* loader treats a path relative to the importing file, a `~` reference, a
|
|
139
|
+
* recursive import — is inferred from the vendor documentation and from a
|
|
140
|
+
* 198-file corpus of public `CLAUDE.md`s, and a divergence from the real loader
|
|
141
|
+
* is discoverable only by OBSERVING it, never by reading a grammar.
|
|
142
|
+
*
|
|
143
|
+
* The narrowness is measured rather than cautious. A loose `@` pattern over that
|
|
144
|
+
* corpus matches Python decorators, Blade templates, npm scopes and CSS at-rules
|
|
145
|
+
* — all of them inside fenced code blocks, which {@link proseLines} removes
|
|
146
|
+
* before this pattern ever runs. Requiring a `.md` tail on top keeps
|
|
147
|
+
* `email me @ foo` and a prose `@dataclass` out. A token this refuses is simply
|
|
148
|
+
* not reported, which under-reports by that file's size; a token it wrongly
|
|
149
|
+
* accepted would make vigiles open a path the repo did not mean to name, and
|
|
150
|
+
* only one of those two is a safety question.
|
|
151
|
+
*/
|
|
152
|
+
const IMPORT_TOKEN = /(?:^|\s)(@[A-Za-z0-9_.][^\s]*\.md)\b/g;
|
|
153
|
+
/** A prose line that is NOTHING BUT one import token — the redirect shape. */
|
|
154
|
+
const ONLY_IMPORT = /^@[A-Za-z0-9_.][^\s]*\.md$/;
|
|
155
|
+
/**
|
|
156
|
+
* The author's own prose, with the frontmatter, the code and the comments gone.
|
|
157
|
+
*
|
|
158
|
+
* Both halves come from the modules that own them — `frontmatterBody` from the
|
|
159
|
+
* frontmatter reader, {@link proseLines} from the ONE markdown-structure helper
|
|
160
|
+
* — rather than from a line-splitting loop here. The helper's own header records
|
|
161
|
+
* why: the hand-rolled fence toggle it replaced had been copy-pasted into five
|
|
162
|
+
* detectors and is wrong on nested and unbalanced fences, which is exactly the
|
|
163
|
+
* `@dataclass`-inside-a-code-block case this has to get right.
|
|
164
|
+
*
|
|
165
|
+
* Indentation, CRLF, trailing whitespace, trailing blank lines and a UTF-8 BOM
|
|
166
|
+
* are all handled THERE, measured; `instruction-chain.test.ts` names each of
|
|
167
|
+
* those spellings as its own case anyway, because they are the ratchet that
|
|
168
|
+
* proves this path still goes through the parser after the next edit.
|
|
169
|
+
*/
|
|
170
|
+
function contentLines(text) {
|
|
171
|
+
return (0, markdown_js_1.proseLines)((0, frontmatter_read_js_1.frontmatterBody)(text));
|
|
172
|
+
}
|
|
173
|
+
/** Every `@import` token in one file, deduplicated, in first-seen order. */
|
|
174
|
+
function importTokens(text) {
|
|
175
|
+
const out = [];
|
|
176
|
+
for (const line of contentLines(text)) {
|
|
177
|
+
for (const m of line.matchAll(IMPORT_TOKEN)) {
|
|
178
|
+
const token = m[1];
|
|
179
|
+
if (token !== undefined && !out.includes(token))
|
|
180
|
+
out.push(token);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
return out;
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Is this file NOTHING BUT imports — a redirect rather than instructions?
|
|
187
|
+
*
|
|
188
|
+
* Binary, never a threshold: every prose line is a bare import token, and there
|
|
189
|
+
* is at least one. "Mostly imports" would be a number nobody can defend, and an
|
|
190
|
+
* empty file is empty rather than a redirect.
|
|
191
|
+
*/
|
|
192
|
+
function isPureRedirect(text) {
|
|
193
|
+
const lines = contentLines(text);
|
|
194
|
+
return lines.length > 0 && lines.every((line) => ONLY_IMPORT.test(line));
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* The `claudeMdExcludes` patterns this chain can APPLY, compiled.
|
|
198
|
+
*
|
|
199
|
+
* ⚠️ THE VENDOR MATCHES THESE AGAINST ABSOLUTE PATHS and the chain is handed
|
|
200
|
+
* repo-relative keys, so a pattern anchored at the filesystem root cannot be
|
|
201
|
+
* applied here at all. Rather than guess a root, only the `**\/`-prefixed form
|
|
202
|
+
* is applied — that leading segment matches zero or more directories, so such a
|
|
203
|
+
* pattern means the same thing against an absolute path and against a
|
|
204
|
+
* repo-relative one, which is exactly the subset that needs no root. Every other
|
|
205
|
+
* pattern is left UNAPPLIED, which counts a file that would not have loaded.
|
|
206
|
+
* That over-reports, and over-reporting is the safe direction: an under-report
|
|
207
|
+
* reads as "you are fine", which is the failure this feature exists to prevent.
|
|
208
|
+
*
|
|
209
|
+
* Matching is `minimatch`, the parser the rest of this repo's exclusion policy
|
|
210
|
+
* already uses (`src/exclude.ts`) — a glob is a LANGUAGE, and hand-rolling a
|
|
211
|
+
* second interpreter for it is precisely the defect being removed here.
|
|
212
|
+
*/
|
|
213
|
+
function compileExcludes(settings) {
|
|
214
|
+
if (settings.length === 0)
|
|
215
|
+
return [];
|
|
216
|
+
const ctor = Minimatch();
|
|
217
|
+
return settings
|
|
218
|
+
.filter((p) => p.startsWith("**/"))
|
|
219
|
+
.map((p) => new ctor(p, { dot: true }));
|
|
220
|
+
}
|
|
221
|
+
/** `claudeMdExcludes` from one settings file's text, or `[]` if it says nothing. */
|
|
222
|
+
function excludesIn(text, parse) {
|
|
223
|
+
if (text === undefined)
|
|
224
|
+
return [];
|
|
225
|
+
let value;
|
|
226
|
+
try {
|
|
227
|
+
value = parse(text);
|
|
228
|
+
}
|
|
229
|
+
catch {
|
|
230
|
+
// A settings file mid-merge or mid-edit must not decide which instructions
|
|
231
|
+
// load. Reporting "nothing is excluded" over-reports, the safe direction.
|
|
232
|
+
return [];
|
|
233
|
+
}
|
|
234
|
+
const raw = value[EXCLUDES_KEY];
|
|
235
|
+
return Array.isArray(raw)
|
|
236
|
+
? raw.filter((p) => typeof p === "string")
|
|
237
|
+
: [];
|
|
238
|
+
}
|
|
239
|
+
/** Does this rule file declare a `paths:` scope — i.e. load only on demand? */
|
|
240
|
+
function isPathScoped(text) {
|
|
241
|
+
const { data } = (0, frontmatter_read_js_1.readFrontmatter)(text);
|
|
242
|
+
return data !== null && Object.hasOwn(data, PATH_SCOPE_KEY);
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* `CLAUDE.md` → `CLAUDE.local.md`: the per-machine sibling, DERIVED so the two
|
|
246
|
+
* names cannot drift apart the way a second constant would.
|
|
247
|
+
*/
|
|
248
|
+
function localSiblingOf(instructionFile) {
|
|
249
|
+
return (0, instruction_chain_js_1.siblingNamed)(instructionFile, "local");
|
|
250
|
+
}
|
|
251
|
+
/** Into `loaded`, or into `unloaded` with the settings reason; absent → nothing. */
|
|
252
|
+
function take(b, path, entry) {
|
|
253
|
+
if (b.files[path] === undefined)
|
|
254
|
+
return;
|
|
255
|
+
const excluder = b.excluderOf(path);
|
|
256
|
+
if (excluder !== undefined) {
|
|
257
|
+
b.unloaded.push({
|
|
258
|
+
path,
|
|
259
|
+
...entry,
|
|
260
|
+
reason: {
|
|
261
|
+
kind: "excluded-by-settings",
|
|
262
|
+
key: EXCLUDES_KEY,
|
|
263
|
+
by: excluder.path,
|
|
264
|
+
byScope: excluder.scope,
|
|
265
|
+
},
|
|
266
|
+
});
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
b.loaded.push({ path, ...entry });
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* Every file under the rules dir: loaded, unless it declares a `paths:` scope.
|
|
273
|
+
*
|
|
274
|
+
* 🔴 THE CLAUDE CODE OVER-REPORT, FIXED HERE. A `paths:`-scoped rule is included
|
|
275
|
+
* when Claude reads a matching file, not at launch, so counting it as
|
|
276
|
+
* always-loaded inflated the one number this whole feature reports. Sorted by
|
|
277
|
+
* path for determinism; the vendor states that the files are concatenated, not
|
|
278
|
+
* in which order the project-scope ones arrive.
|
|
279
|
+
*/
|
|
280
|
+
function takeRules(b, ruleRe) {
|
|
281
|
+
for (const path of Object.keys(b.files).sort()) {
|
|
282
|
+
if (!ruleRe.test(path))
|
|
283
|
+
continue;
|
|
284
|
+
const text = b.files[path];
|
|
285
|
+
if (text === undefined)
|
|
286
|
+
continue;
|
|
287
|
+
if (isPathScoped(text)) {
|
|
288
|
+
b.unloaded.push({
|
|
289
|
+
path,
|
|
290
|
+
role: "rule",
|
|
291
|
+
scope: "repo",
|
|
292
|
+
reason: { kind: "on-demand", when: "path-scoped" },
|
|
293
|
+
});
|
|
294
|
+
continue;
|
|
295
|
+
}
|
|
296
|
+
take(b, path, { role: "rule", scope: "repo" });
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
/**
|
|
300
|
+
* A subdirectory's own instruction file, IF the caller handed one over.
|
|
301
|
+
*
|
|
302
|
+
* The domain's bound never enumerates one ({@link INSTRUCTION_SHAPES}), so this
|
|
303
|
+
* is reached only by a caller holding a wider map — and then the honest answer
|
|
304
|
+
* is "included when Claude reads files in those subdirectories", not "always".
|
|
305
|
+
*
|
|
306
|
+
* 🔴 `AGENTS.md` GETS THE SAME TREATMENT AS A `paths:`-SCOPED RULE, and the
|
|
307
|
+
* vendor puts the two in the same list for the same reason — neither is read at
|
|
308
|
+
* launch: "As Claude works in subdirectories: a subdirectory's `AGENTS.md`,
|
|
309
|
+
* when Claude opens a file there with the Read tool". Counting one would inflate
|
|
310
|
+
* the single number this feature exists to report, which is the over-report
|
|
311
|
+
* `alwaysLoaded` shipped.
|
|
312
|
+
*
|
|
313
|
+
* 🔴 THE VENDOR'S EXTRA CONDITION IS NOT MODELLED, AND THE REASON IS A
|
|
314
|
+
* MEASUREMENT RATHER THAN A PREFERENCE. "…when Claude opens a file there with
|
|
315
|
+
* the Read tool AND THAT SUBDIRECTORY HAS NONE OF THE THREE `CLAUDE.md` FILES
|
|
316
|
+
* OF ITS OWN": with one of them beside it, a nested `AGENTS.md` is never read
|
|
317
|
+
* at all rather than read on demand. Both answers weigh zero, so the only thing
|
|
318
|
+
* at stake is the printed REASON — which is worth getting right, since "same
|
|
319
|
+
* number, confident wrong reason" is the class {@link isNeverRead} exists for.
|
|
320
|
+
*
|
|
321
|
+
* ⚠️ IT CANNOT BE ANSWERED FROM THE MAP THIS METHOD IS HANDED. Measured over
|
|
322
|
+
* both engines, which are the only two callers: `boundedInstructionFiles`
|
|
323
|
+
* (`surface-discovery-fs.ts`) and the browser twin (`scan-files.ts:700`) each
|
|
324
|
+
* build the map as `instructionCandidatePaths(...)` UNION the paths the imports
|
|
325
|
+
* pass resolved — nothing else. `instructionCandidatePaths` keeps
|
|
326
|
+
* `^[^/]+\\.md$` and `^\\.[^/]+\\/[^/]+\\.md$`, so NO `<dir>/…` path is ever a
|
|
327
|
+
* candidate. A subdirectory `AGENTS.md` therefore reaches this loop only
|
|
328
|
+
* because something IMPORTED it, and its sibling `<dir>/CLAUDE.md` is in the
|
|
329
|
+
* map only if something imported that too. Absence of the sibling carries no
|
|
330
|
+
* information at all, so a supersede verdict built on it would be a guess
|
|
331
|
+
* wearing a vendor quote.
|
|
332
|
+
*
|
|
333
|
+
* 🔴 AND WIDENING THE BOUND TO GET IT WOULD BE THE WRONG TRADE. The bound is
|
|
334
|
+
* what stops registering an adapter from widening the read in someone else's
|
|
335
|
+
* repository — the construction this whole port rests on. Enumerating every
|
|
336
|
+
* directory's `CLAUDE.md` family to decide a printed reason on a file that
|
|
337
|
+
* weighs nothing spends the load-bearing thing on a cosmetic one. So the
|
|
338
|
+
* condition is declared here, unmodelled, and the file keeps the honest
|
|
339
|
+
* "on-demand": it says what this chain knows, not what the loader would do.
|
|
340
|
+
*
|
|
341
|
+
* ⏳ WHAT WOULD CLOSE IT: a caller that legitimately holds a per-directory map —
|
|
342
|
+
* a future `vigiles audit <subpackage>` run, where that directory IS the
|
|
343
|
+
* working directory and its files are in the bound by construction. Then the
|
|
344
|
+
* question is the ROOT question, already answered by {@link supersederOf}, and
|
|
345
|
+
* no widening is needed.
|
|
346
|
+
*
|
|
347
|
+
* ⚠️ AND THE CONDITION IS MOOT FOR AN IMPORTED FILE, WHICH IS WHY THIS PASS NOW
|
|
348
|
+
* RUNS LAST. A `<dir>/CLAUDE.md` that something `@`-imports is taken by the
|
|
349
|
+
* imports pass before this loop sees it, so the only paths reaching here are
|
|
350
|
+
* the ones nothing imported — the ones the condition was always about. The
|
|
351
|
+
* ordering, and the bug that came from the other order, are stated at the call.
|
|
352
|
+
*
|
|
353
|
+
* `reserved` holds the paths this chain owns at the root level. Without it, a
|
|
354
|
+
* `.claude/AGENTS.md` that the supersede pass has not yet classified would be
|
|
355
|
+
* read as a SUBDIRECTORY file, because it has a slash and the right basename —
|
|
356
|
+
* and the dot-directory is not a subdirectory of the project in the vendor's
|
|
357
|
+
* sense. It is empty-set-safe: every root candidate is already in `named` on
|
|
358
|
+
* the paths that reach this today.
|
|
359
|
+
*/
|
|
360
|
+
function takeSubdirectories(b, leafNames, reserved) {
|
|
361
|
+
const named = new Set([...b.loaded, ...b.unloaded].map((e) => e.path));
|
|
362
|
+
for (const path of Object.keys(b.files).sort()) {
|
|
363
|
+
if (named.has(path) || reserved.has(path) || !path.includes("/"))
|
|
364
|
+
continue;
|
|
365
|
+
if (isNeverRead(path))
|
|
366
|
+
continue;
|
|
367
|
+
if (!leafNames.has(path.slice(path.lastIndexOf("/") + 1)))
|
|
368
|
+
continue;
|
|
369
|
+
b.unloaded.push({
|
|
370
|
+
path,
|
|
371
|
+
role: "root",
|
|
372
|
+
scope: "repo",
|
|
373
|
+
reason: { kind: "on-demand", when: "subdirectory" },
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
/**
|
|
378
|
+
* Which file, if any, stops this repository's `AGENTS.md` being read — vendor:
|
|
379
|
+
* "a `CLAUDE.md`, `.claude/CLAUDE.md`, or `CLAUDE.local.md` in your working
|
|
380
|
+
* directory or any directory above it".
|
|
381
|
+
*
|
|
382
|
+
* 🔴 AN EXCLUDED `CLAUDE.md` DOES NOT SUPERSEDE, AND THAT IS A MEASUREMENT.
|
|
383
|
+
* Two vendor rules meet here and the vendor composes neither: the supersede
|
|
384
|
+
* rule is phrased about files you HAVE ("look for a `CLAUDE.md`… if you find
|
|
385
|
+
* one"), while `claudeMdExcludes` takes a file out of the chain without taking
|
|
386
|
+
* it off the disk. The page answers neither way, so this was run rather than
|
|
387
|
+
* argued — Claude Code **2.1.278**, fixtures and runner under
|
|
388
|
+
* `test/fixtures/instruction-chain-vendor/`, each file holding a codeword the
|
|
389
|
+
* model is then asked to recite with the file-reading tools denied:
|
|
390
|
+
*
|
|
391
|
+
* c0 both files, no settings → ALPHA only. Supersede happens at all.
|
|
392
|
+
* c1 `claudeMdExcludes` hides the CLAUDE.md → **BETA, and no ALPHA**. The AGENTS.md LOADS.
|
|
393
|
+
* c3 root hidden, `.claude/CLAUDE.md` kept → GAMMA only. A SURVIVING candidate still supersedes.
|
|
394
|
+
*
|
|
395
|
+
* c0 is the control: without it, c1 is a reading of an instrument nobody
|
|
396
|
+
* checked. c3 is what fixes the SHAPE of the fix — the answer is not "ignore
|
|
397
|
+
* exclusions", it is "the first candidate that is present AND not excluded",
|
|
398
|
+
* and the survivor is what gets named in `by`. Hence the predicate below
|
|
399
|
+
* rather than a boolean bypass.
|
|
400
|
+
*
|
|
401
|
+
* 🔴 AND THE OBVIOUS INSTRUMENT IS THE WRONG ONE, which is worth a line here
|
|
402
|
+
* because it nearly reversed this conclusion. An `InstructionsLoaded` hook
|
|
403
|
+
* looks like the exact tool for the job and is BLIND TO `AGENTS.md`: case c2 —
|
|
404
|
+
* a repository holding only an `AGENTS.md` — recites BETA (so the file
|
|
405
|
+
* plainly loaded) while the hook logs NOTHING. Read on the hook alone, c1's
|
|
406
|
+
* silence says "nothing loaded" and this whole function stays as it was. The
|
|
407
|
+
* silence is a fact about the hook. Both instruments are in the runner, with
|
|
408
|
+
* c2 as the case that tells them apart.
|
|
409
|
+
*
|
|
410
|
+
* ⚠️ WHAT THE MEASUREMENT DOES NOT COVER: it was taken on ONE build, and the
|
|
411
|
+
* vendor has already reversed a neighbouring fact once (`AGENTS.md` auto-load,
|
|
412
|
+
* v2.1.277). So the build number is part of the claim, not decoration. Re-run
|
|
413
|
+
* the fixtures before trusting this on a much later version.
|
|
414
|
+
*
|
|
415
|
+
* 🔴 A COMMITTED SUPERSEDER WINS OVER THE PER-MACHINE ONE, and the order of
|
|
416
|
+
* this array is the whole of that rule. Both can be present; picking the local
|
|
417
|
+
* file then would put `AGENTS.md` into `committedTotal`, claiming a teammate
|
|
418
|
+
* loads it — but that teammate has the `CLAUDE.md`, so they do not. The
|
|
419
|
+
* per-machine file is therefore last, and only answers when nothing committed
|
|
420
|
+
* did.
|
|
421
|
+
*
|
|
422
|
+
* ⚠️ THIS ASSUMES THE DEFAULT MODE, AND THE REPOSITORY CANNOT CONFIRM IT. The
|
|
423
|
+
* vendor's `claude-md-or-agents-md` (default) is what supersedes at all;
|
|
424
|
+
* `claude-md-and-agents-md` loads both families and supersedes nothing, and
|
|
425
|
+
* `managed-only` loads neither. The setting is read only from user-level, a
|
|
426
|
+
* `--settings` file or managed settings — "Claude Code ignores it in project
|
|
427
|
+
* and local settings files" — so a repo scan cannot see it, and the same
|
|
428
|
+
* commit therefore loads a different instruction set for two different people
|
|
429
|
+
* with no file in the tree saying which. Nothing in this function can close
|
|
430
|
+
* that; it is the module header's limit 1, restated at the line that assumes.
|
|
431
|
+
*
|
|
432
|
+
* ⚠️ AND THE THREE NON-COUNTING FILES ARE ABSENT BY CONSTRUCTION, not by an
|
|
433
|
+
* omission: `~/.claude/CLAUDE.md` and a managed `CLAUDE.md` are outside a
|
|
434
|
+
* repository audit entirely (reading `~` for a grade is wrong on its face), and
|
|
435
|
+
* `.claude/rules/` files "keep loading alongside `AGENTS.md`" — they are taken
|
|
436
|
+
* by {@link takeRules} in both modes and are not consulted here. Stated because
|
|
437
|
+
* a reader who adds the rules dir to this array would turn `AGENTS.md` off in
|
|
438
|
+
* every repository that has one.
|
|
439
|
+
*/
|
|
440
|
+
function supersederOf(files, input, isExcluded) {
|
|
441
|
+
const candidates = [
|
|
442
|
+
{ path: input.instructionFile, scope: "repo" },
|
|
443
|
+
{
|
|
444
|
+
path: `${input.userSurfaceRoot}/${input.instructionFile}`,
|
|
445
|
+
scope: "repo",
|
|
446
|
+
},
|
|
447
|
+
{ path: localSiblingOf(input.instructionFile), scope: "local" },
|
|
448
|
+
];
|
|
449
|
+
// PRESENT AND NOT EXCLUDED — c3 is why both halves are here. A bypass that
|
|
450
|
+
// just skipped the check when anything was excluded would name the wrong
|
|
451
|
+
// file in `by`, or none at all, in a repo that excludes one candidate and
|
|
452
|
+
// keeps another.
|
|
453
|
+
return candidates.find((c) => files[c.path] !== undefined && !isExcluded(c.path));
|
|
454
|
+
}
|
|
455
|
+
/** One loaded file's `@import` tokens: reported, and TAKEN when already present. */
|
|
456
|
+
function takeImportsOf(b, entry) {
|
|
457
|
+
const text = b.files[entry.path];
|
|
458
|
+
if (text === undefined)
|
|
459
|
+
return;
|
|
460
|
+
const known = new Set([...b.loaded, ...b.unloaded].map((e) => e.path));
|
|
461
|
+
for (const token of importTokens(text)) {
|
|
462
|
+
// The token as WRITTEN carries the `@`; the path is what it resolves to —
|
|
463
|
+
// against the IMPORTING FILE's directory, measured, see `resolveImportPath`.
|
|
464
|
+
const written = token.slice(1);
|
|
465
|
+
if (!(0, instruction_chain_js_1.isRepoRootedImport)(written))
|
|
466
|
+
continue;
|
|
467
|
+
const path = (0, instruction_chain_js_1.resolveImportPath)(entry.path, written);
|
|
468
|
+
if (!b.imports.some((n) => n.path === path && n.from === entry.path)) {
|
|
469
|
+
b.imports.push({ path, token, from: entry.path });
|
|
470
|
+
}
|
|
471
|
+
if (b.files[path] === undefined || known.has(path))
|
|
472
|
+
continue;
|
|
473
|
+
// The importer's scope is INHERITED: a committed file pulled in only by
|
|
474
|
+
// `CLAUDE.local.md` does not load for a teammate, so it must not be in the
|
|
475
|
+
// committed total either. And `via` travels with it, so the report can say
|
|
476
|
+
// WHY a file nobody expected is in the count.
|
|
477
|
+
//
|
|
478
|
+
// 🔴 EXCEPT WHEN THE IMPORTED FILE IS ONE MACHINE'S BY NAME, which is not a
|
|
479
|
+
// hypothetical: `@AGENTS.local.md` is one of the three real imports in the
|
|
480
|
+
// measured 214-file corpus. Inheriting `repo` there would put a gitignored
|
|
481
|
+
// file into a published `committedTotal` — the browser twin reads a GitHub
|
|
482
|
+
// tree and can never see it, so the CLI and the browser would disagree
|
|
483
|
+
// about the same commit. That is the defect `InstructionScope` exists to
|
|
484
|
+
// prevent, and a token in a committed file does not make the target
|
|
485
|
+
// committed. Inheritance still applies in the other direction: a `local`
|
|
486
|
+
// importer keeps `local`, because `"local"` is the narrower answer.
|
|
487
|
+
//
|
|
488
|
+
// ⏳ THIS LINE DOES NOT DECIDE WHETHER THE TOKEN SHOULD LOAD AT ALL — that
|
|
489
|
+
// is the open question at `isNeverRead`, and this predicate is right under
|
|
490
|
+
// either of its answers. See the note there before "simplifying" this.
|
|
491
|
+
take(b, path, {
|
|
492
|
+
role: "import",
|
|
493
|
+
scope: isPerMachineName(path) ? "local" : entry.scope,
|
|
494
|
+
via: { from: entry.path, token },
|
|
495
|
+
});
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
function claudeCodeInstructionChain(files, input) {
|
|
499
|
+
// PER SOURCE, not flattened. Flattening lost which file a pattern came from,
|
|
500
|
+
// and that is the whole fact `byScope` needs — see the measurement on
|
|
501
|
+
// `excluded-by-settings` in core.
|
|
502
|
+
const excluders = input.settingsSources.map((source) => ({
|
|
503
|
+
source,
|
|
504
|
+
matchers: compileExcludes(excludesIn(files[source.path], input.parseSettings)),
|
|
505
|
+
}));
|
|
506
|
+
const b = {
|
|
507
|
+
files,
|
|
508
|
+
loaded: [],
|
|
509
|
+
unloaded: [],
|
|
510
|
+
imports: [],
|
|
511
|
+
excluderOf: (path) => excluders.find((e) => e.matchers.some((m) => m.match(path)))?.source,
|
|
512
|
+
};
|
|
513
|
+
// ORDER. The one ordering fact the vendor states is that the local file is
|
|
514
|
+
// LAST — "the last thing Claude reads at that level" — so it is taken after
|
|
515
|
+
// the rules rather than beside the root file it is named for.
|
|
516
|
+
take(b, input.instructionFile, { role: "root", scope: "repo" });
|
|
517
|
+
take(b, `${input.userSurfaceRoot}/${input.instructionFile}`, {
|
|
518
|
+
role: "root",
|
|
519
|
+
scope: "repo",
|
|
520
|
+
});
|
|
521
|
+
// THE CROSS-FAMILY SWITCH. "At session start: every `AGENTS.md` and
|
|
522
|
+
// `.claude/AGENTS.md` in your working directory" — but only "when you have no
|
|
523
|
+
// CLAUDE.md in your working directory or above it". Both spellings, in the
|
|
524
|
+
// same root-then-dot-directory order as the two takes above; the vendor states
|
|
525
|
+
// no order BETWEEN them, and it cannot matter to a sum.
|
|
526
|
+
const superseder = supersederOf(files, input, (p) => b.excluderOf(p) !== undefined);
|
|
527
|
+
const crossToolPaths = [
|
|
528
|
+
AGENTS_FILE,
|
|
529
|
+
`${input.userSurfaceRoot}/${AGENTS_FILE}`,
|
|
530
|
+
];
|
|
531
|
+
if (superseder === undefined) {
|
|
532
|
+
for (const path of crossToolPaths) {
|
|
533
|
+
take(b, path, { role: "root", scope: "repo" });
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
takeRules(b, input.ruleRe);
|
|
537
|
+
take(b, localSiblingOf(input.instructionFile), {
|
|
538
|
+
role: "root-local",
|
|
539
|
+
scope: "local",
|
|
540
|
+
});
|
|
541
|
+
// THE IMPORTS PASS, ONE LEVEL. It reads a SNAPSHOT of what is loaded so far,
|
|
542
|
+
// so a file pulled in by an import is not itself scanned for imports — see
|
|
543
|
+
// `resolveImports` in the core for the corpus measurement behind that, and for
|
|
544
|
+
// what it costs. Every path reported literally occurs in the file that names
|
|
545
|
+
// it, which is the property that keeps this from being a widening the ADAPTER
|
|
546
|
+
// chose rather than one the repo owner wrote.
|
|
547
|
+
//
|
|
548
|
+
// 🔴 IT RUNS BEFORE THE SUPERSEDE VERDICT, AND THAT ORDER IS A VENDOR ROW
|
|
549
|
+
// RATHER THAN A CONVENIENCE. The third row of the vendor's own table reads:
|
|
550
|
+
// "A `CLAUDE.md` that already imports `AGENTS.md`" → "Your `CLAUDE.md`, with
|
|
551
|
+
// `AGENTS.md` included through the import". So the idiom four of the six real
|
|
552
|
+
// imports in the measured corpus use — a `CLAUDE.md` holding `@AGENTS.md` —
|
|
553
|
+
// still LOADS that file, and marking it superseded first would have deleted
|
|
554
|
+
// the whole redirect finding. Both passes see `known`, so whichever gets there
|
|
555
|
+
// first owns the entry; this one is meant to.
|
|
556
|
+
//
|
|
557
|
+
// It also settles the two vendor sentences about what happens INSIDE an
|
|
558
|
+
// `AGENTS.md` at no extra cost, because both passes are role-blind: "`@path`
|
|
559
|
+
// imports are expanded" is this loop over any loaded entry, and
|
|
560
|
+
// "`claudeMdExcludes` patterns apply" is `take` above. Asserted rather than
|
|
561
|
+
// assumed — `instruction-chain.test.ts` runs each against an `AGENTS.md` that
|
|
562
|
+
// got in as a ROOT file, since a role-keyed version of either would pass every
|
|
563
|
+
// `CLAUDE.md` case and fail exactly those two.
|
|
564
|
+
for (const entry of [...b.loaded])
|
|
565
|
+
takeImportsOf(b, entry);
|
|
566
|
+
// THE SUBDIRECTORY PASS, AFTER THE IMPORTS AND NOT BEFORE THEM. Order is the
|
|
567
|
+
// whole of a fixed bug: this pass claims ANY slash path whose leaf is an
|
|
568
|
+
// instruction name, and `takeImportsOf` skips a path already `known`. Running
|
|
569
|
+
// it first therefore swallowed `@pkg/CLAUDE.md` and `@pkg/AGENTS.md` — a
|
|
570
|
+
// file the root instruction file literally imports, which the loader expands
|
|
571
|
+
// at session start (measured on 2.1.278, case `q2-import`: an `InstructionsLoaded` entry reading
|
|
572
|
+
// `pkg/CLAUDE.md | load_reason: include | parent_file_path: CLAUDE.md`) — and
|
|
573
|
+
// filed it `on-demand/subdirectory`, dropping its bytes from BOTH totals. Only
|
|
574
|
+
// the leaf name decided it, so `@pkg/style.md` was counted and `@pkg/CLAUDE.md`
|
|
575
|
+
// was not: the same import, reported two ways.
|
|
576
|
+
//
|
|
577
|
+
// 🔴 THE FIX IS THE ORDER, NOT A CONDITION, and that is why it is cheap. A
|
|
578
|
+
// sibling test — "is there a `pkg/CLAUDE.md` next to this `pkg/AGENTS.md`" —
|
|
579
|
+
// is the shape this pass CANNOT answer (see its own header: no `<dir>/…` path
|
|
580
|
+
// is ever a candidate, so absence carries no information). Letting the imports
|
|
581
|
+
// pass go first makes the question moot BY CONSTRUCTION: an imported file is
|
|
582
|
+
// taken because a token names it, and what this pass then sees is exactly the
|
|
583
|
+
// set nothing imported. The bound is untouched — no new path is read.
|
|
584
|
+
takeSubdirectories(b, new Set([input.instructionFile, AGENTS_FILE]), new Set(crossToolPaths));
|
|
585
|
+
// THE VERDICT, LAST: anything cross-tool that the passes above did not claim
|
|
586
|
+
// is present, unread, and the reason is a file rather than a setting. `scope`
|
|
587
|
+
// stays `"repo"` — this IS a committed file — and `byScope` carries whether
|
|
588
|
+
// the thing that silenced it is committed too, which is what decides whether a
|
|
589
|
+
// teammate loads it. See `weighInstructions`.
|
|
590
|
+
if (superseder !== undefined) {
|
|
591
|
+
const named = new Set([...b.loaded, ...b.unloaded].map((e) => e.path));
|
|
592
|
+
for (const path of crossToolPaths) {
|
|
593
|
+
if (b.files[path] === undefined || named.has(path))
|
|
594
|
+
continue;
|
|
595
|
+
b.unloaded.push({
|
|
596
|
+
path,
|
|
597
|
+
role: "root",
|
|
598
|
+
scope: "repo",
|
|
599
|
+
reason: {
|
|
600
|
+
kind: "superseded",
|
|
601
|
+
by: superseder.path,
|
|
602
|
+
byScope: superseder.scope,
|
|
603
|
+
},
|
|
604
|
+
});
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
return {
|
|
608
|
+
loaded: b.loaded,
|
|
609
|
+
unloaded: b.unloaded,
|
|
610
|
+
imports: b.imports,
|
|
611
|
+
patterns: [],
|
|
612
|
+
// A loaded file that is NOTHING BUT imports is a redirect, not instructions
|
|
613
|
+
// — reported as a shape so the report can say so instead of printing the
|
|
614
|
+
// reassuring size of a fourteen-byte pointer.
|
|
615
|
+
redirects: b.loaded.flatMap((entry) => {
|
|
616
|
+
const text = files[entry.path];
|
|
617
|
+
if (text === undefined || !isPureRedirect(text))
|
|
618
|
+
return [];
|
|
619
|
+
const to = b.imports
|
|
620
|
+
.filter((i) => i.from === entry.path)
|
|
621
|
+
.map((i) => i.path);
|
|
622
|
+
return to.length === 0 ? [] : [{ path: entry.path, to }];
|
|
623
|
+
}),
|
|
624
|
+
};
|
|
625
|
+
}
|
|
626
|
+
//# sourceMappingURL=instruction-chain.js.map
|
|
@@ -3,11 +3,11 @@
|
|
|
3
3
|
* port's reference implementation). `loadPlugin` defaults to it; a Codex adapter
|
|
4
4
|
* defines a sibling `codexLayout` and passes it to the same loader.
|
|
5
5
|
* 🔴 THE PATHS BELOW ARE DOCUMENTED IN `docs/configuration.md`. Change any of
|
|
6
|
-
* them — `instructionFile`, `
|
|
6
|
+
* them — `instructionFile`, `surfaces`, `userSurfaceRoot`, `rulesDir` — and
|
|
7
7
|
* that page is wrong until you edit it too. The page marks this symbol with
|
|
8
8
|
* `vigiles:symbol`, so RENAMING it turns `vigiles lint` red and forces the
|
|
9
9
|
* edit; changing a VALUE in place does not, and nothing today catches that.
|
|
10
10
|
*/
|
|
11
|
-
import type
|
|
11
|
+
import { type PluginLayout } from "../../core/layout.js";
|
|
12
12
|
export declare const claudeCodeLayout: PluginLayout;
|
|
13
13
|
//# sourceMappingURL=layout.d.ts.map
|