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,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
|