rulereceipt 0.1.47 → 0.1.49
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/README.md +22 -0
- package/dist/cli.js +9 -5
- package/dist/init.d.ts +2 -0
- package/dist/init.js +10 -0
- package/dist/parsers/transcriptParser.d.ts +22 -0
- package/dist/parsers/transcriptParser.js +32 -2
- package/dist/projectConfig.d.ts +32 -7
- package/dist/projectConfig.js +32 -7
- package/dist/rules.js +19 -9
- package/dist/shadowedAgents.d.ts +34 -0
- package/dist/shadowedAgents.js +40 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -393,6 +393,28 @@ report to the path you name, and `rules --include/--exclude` records a
|
|
|
393
393
|
correction in `.rulereceipt/overrides.json`. Plain `rulereceipt check` writes
|
|
394
394
|
nothing and makes no network calls.
|
|
395
395
|
|
|
396
|
+
**Severity, per rule.** A committed, team-shared `.rulereceipt/config.json`
|
|
397
|
+
sets how hard each rule bites in CI, by its stable handle (from `rulereceipt
|
|
398
|
+
rules --list`):
|
|
399
|
+
|
|
400
|
+
```json
|
|
401
|
+
{
|
|
402
|
+
"rules": {
|
|
403
|
+
"a1b2c3": "off", // hidden from the report, never gates
|
|
404
|
+
"d4e5f6": "warn", // shown, but does not fail the build
|
|
405
|
+
"97h8i9": "error" // shown, FAILS the build — the default for a checkable rule
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
No config means today's behaviour: every checkable FAIL is an `error`. This is
|
|
411
|
+
the one place severity lives — a team marks the must-not-break rules `error`
|
|
412
|
+
and the nice-to-haves `warn`, so CI gates on what matters instead of going red
|
|
413
|
+
on day one. (Refusing a command *before* it runs is separate, and stays with
|
|
414
|
+
the guard's `rules --forbid` clause-mark — a config that could block on any
|
|
415
|
+
rule would refuse far too much.) The older `{"warn": ["a1b2c3"]}` list still
|
|
416
|
+
works and means the same as `"warn"` above.
|
|
417
|
+
|
|
396
418
|
**You can verify the package came from this source.** Every release from
|
|
397
419
|
0.1.19 on is built and published by GitHub Actions and signed with
|
|
398
420
|
[npm provenance](https://docs.npmjs.com/generating-provenance-statements),
|
package/dist/cli.js
CHANGED
|
@@ -9,6 +9,7 @@ import { parseClaudeMd } from "./parsers/readClaudeMd.js";
|
|
|
9
9
|
import { readLatestTranscript, readTranscriptFromFile, findLatestSessionFile } from "./parsers/transcriptParser.js";
|
|
10
10
|
import { loadRules } from "./rules.js";
|
|
11
11
|
import { adviseRules } from "./checkability.js";
|
|
12
|
+
import { shadowedAgentsMd } from "./shadowedAgents.js";
|
|
12
13
|
import { classifyRules } from "./checks/classify.js";
|
|
13
14
|
import { loadOverrides, saveOverride, clearOverride, staleOverrides, ruleFingerprint, OVERRIDES_PATH } from "./overrides.js";
|
|
14
15
|
import { runDeterministicChecks } from "./checks/deterministicChecks.js";
|
|
@@ -32,7 +33,7 @@ import { maybeShowWhatsNew } from "./whatsNew.js";
|
|
|
32
33
|
import { verifyReceipt, parseReceipt } from "./receipt.js";
|
|
33
34
|
import { buildBadge } from "./badge.js";
|
|
34
35
|
import { buildInitGuidance } from "./init.js";
|
|
35
|
-
import { loadProjectConfig, handleMap, blockingFailures, warningFailures, PROJECT_CONFIG_PATH } from "./projectConfig.js";
|
|
36
|
+
import { loadProjectConfig, handleMap, blockingFailures, warningFailures, visibleResults, PROJECT_CONFIG_PATH } from "./projectConfig.js";
|
|
36
37
|
import { maybeCheckUpdates, isUpdateCheckEnabled } from "./updateCheck.js";
|
|
37
38
|
import { generateDigest } from "./digest.js";
|
|
38
39
|
import { enableSchedule, disableSchedule, scheduleStatus } from "./schedule.js";
|
|
@@ -263,12 +264,14 @@ async function runCheck(opts) {
|
|
|
263
264
|
// sends only a random install ID, never rule text or transcript content,
|
|
264
265
|
// regardless of --llm.
|
|
265
266
|
const judgmentResults = llm ? await runJudgmentChecks(judgment, events) : judgment.map(({ rule }) => needsLlmResult(rule));
|
|
266
|
-
const
|
|
267
|
-
// Severity
|
|
268
|
-
//
|
|
269
|
-
//
|
|
267
|
+
const rawResults = [...deterministicResults, ...judgmentResults];
|
|
268
|
+
// Severity ladder from .rulereceipt/config.json (per rule handle): `off`
|
|
269
|
+
// rules are hidden entirely, `warn` rules are shown but do not fail the
|
|
270
|
+
// build, everything else is `error` (the default). handleFor maps a result
|
|
271
|
+
// back to its stable handle so the mark survives edits that renumber ids.
|
|
270
272
|
const projectConfig = loadProjectConfig(cwd);
|
|
271
273
|
const handleFor = handleMap(rules);
|
|
274
|
+
const results = visibleResults(rawResults, projectConfig, handleFor);
|
|
272
275
|
const blockingFails = blockingFailures(results, projectConfig, handleFor);
|
|
273
276
|
const warnedFails = warningFailures(results, projectConfig, handleFor);
|
|
274
277
|
const meta = { sessionFilePath, ruleCount: results.length };
|
|
@@ -808,6 +811,7 @@ program
|
|
|
808
811
|
hasAgentsMd: existsSync(join(cwd, "AGENTS.md")),
|
|
809
812
|
hookInstalled: hookIsInstalled(cwd),
|
|
810
813
|
hasApiKey: Boolean(process.env.ANTHROPIC_API_KEY),
|
|
814
|
+
shadowedAgents: shadowedAgentsMd(cwd).map((s) => s.agents),
|
|
811
815
|
}));
|
|
812
816
|
});
|
|
813
817
|
program
|
package/dist/init.d.ts
CHANGED
|
@@ -9,6 +9,8 @@ export interface InitState {
|
|
|
9
9
|
hasAgentsMd: boolean;
|
|
10
10
|
hookInstalled: boolean;
|
|
11
11
|
hasApiKey: boolean;
|
|
12
|
+
/** AGENTS.md files a CLAUDE.md shadows, so Claude Code never loads them. */
|
|
13
|
+
shadowedAgents?: string[];
|
|
12
14
|
}
|
|
13
15
|
/** The PreToolUse guard hook, as it goes into .claude/settings.json. */
|
|
14
16
|
export declare const GUARD_HOOK_SNIPPET = "{\n \"hooks\": {\n \"PreToolUse\": [\n { \"hooks\": [ { \"type\": \"command\", \"command\": \"rulereceipt guard\" } ] }\n ]\n }\n}";
|
package/dist/init.js
CHANGED
|
@@ -41,6 +41,16 @@ export function buildInitGuidance(state) {
|
|
|
41
41
|
" judgment graded. Without it those report UNCLEAR — deterministic checks run regardless,\n" +
|
|
42
42
|
" and nothing is ever sent without the --llm flag.");
|
|
43
43
|
}
|
|
44
|
+
const shadowed = state.shadowedAgents ?? [];
|
|
45
|
+
if (shadowed.length > 0) {
|
|
46
|
+
out.push("Warning — rules Claude Code never reads:");
|
|
47
|
+
for (const path of shadowed) {
|
|
48
|
+
out.push(` ${path} sits next to a CLAUDE.md, so Claude Code ignores it.`);
|
|
49
|
+
}
|
|
50
|
+
out.push(" Since 2026-09, AGENTS.md is only read when there is NO CLAUDE.md at that");
|
|
51
|
+
out.push(" level. Move these rules into the CLAUDE.md, or they govern nothing.");
|
|
52
|
+
out.push("");
|
|
53
|
+
}
|
|
44
54
|
if (steps.length === 0) {
|
|
45
55
|
out.push("You're set up. Run: rulereceipt check");
|
|
46
56
|
}
|
|
@@ -14,4 +14,26 @@ export declare function parseLine(line: string): TranscriptEvent[];
|
|
|
14
14
|
* not a failure.
|
|
15
15
|
*/
|
|
16
16
|
export declare function readTranscriptFromFile(filePath: string): TranscriptEvent[];
|
|
17
|
+
/**
|
|
18
|
+
* Subagent transcripts for a session.
|
|
19
|
+
*
|
|
20
|
+
* Claude Code writes each subagent (a Task/background agent, up to 20 at once
|
|
21
|
+
* and 3 deep as of mid-2026) to its own JSONL under a directory named after
|
|
22
|
+
* the PARENT session id — verified against real files 2026-09-23:
|
|
23
|
+
* projects/<enc>/<sessionId>/subagents/agent-*.jsonl
|
|
24
|
+
* and each subagent line's own `sessionId` equals that parent id. So for a
|
|
25
|
+
* picked session file `<sessionId>.jsonl`, the subagents sit in a sibling
|
|
26
|
+
* directory named by its basename.
|
|
27
|
+
*
|
|
28
|
+
* These were invisible before: the reader took only the newest top-level
|
|
29
|
+
* file, so a rule broken by a subagent — the exact shape of the risk as
|
|
30
|
+
* Claude Code pushes toward fleets of unattended agents — was never checked.
|
|
31
|
+
*
|
|
32
|
+
* The events are appended to the main stream. Scan checks (git/file/
|
|
33
|
+
* attribution/emoji) simply gain more to inspect; claim-vs-evidence pairs on
|
|
34
|
+
* globally-unique tool ids so it cannot cross-match; the approval gate can at
|
|
35
|
+
* worst treat a main-session "ask" as covering a subagent action, which is a
|
|
36
|
+
* false negative — the safe direction for a tool that must not over-accuse.
|
|
37
|
+
*/
|
|
38
|
+
export declare function findSubagentFiles(sessionFile: string): string[];
|
|
17
39
|
export declare function readLatestTranscript(cwd: string): TranscriptEvent[];
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { readFileSync, readdirSync, statSync } from "node:fs";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
|
-
import { join } from "node:path";
|
|
3
|
+
import { join, dirname, basename } from "node:path";
|
|
4
4
|
/**
|
|
5
5
|
* Claude Code stores each session as a JSONL file at:
|
|
6
6
|
* ~/.claude/projects/<cwd with every "/" replaced by "-">/<sessionId>.jsonl
|
|
@@ -171,9 +171,39 @@ export function readTranscriptFromFile(filePath) {
|
|
|
171
171
|
}
|
|
172
172
|
return events;
|
|
173
173
|
}
|
|
174
|
+
/**
|
|
175
|
+
* Subagent transcripts for a session.
|
|
176
|
+
*
|
|
177
|
+
* Claude Code writes each subagent (a Task/background agent, up to 20 at once
|
|
178
|
+
* and 3 deep as of mid-2026) to its own JSONL under a directory named after
|
|
179
|
+
* the PARENT session id — verified against real files 2026-09-23:
|
|
180
|
+
* projects/<enc>/<sessionId>/subagents/agent-*.jsonl
|
|
181
|
+
* and each subagent line's own `sessionId` equals that parent id. So for a
|
|
182
|
+
* picked session file `<sessionId>.jsonl`, the subagents sit in a sibling
|
|
183
|
+
* directory named by its basename.
|
|
184
|
+
*
|
|
185
|
+
* These were invisible before: the reader took only the newest top-level
|
|
186
|
+
* file, so a rule broken by a subagent — the exact shape of the risk as
|
|
187
|
+
* Claude Code pushes toward fleets of unattended agents — was never checked.
|
|
188
|
+
*
|
|
189
|
+
* The events are appended to the main stream. Scan checks (git/file/
|
|
190
|
+
* attribution/emoji) simply gain more to inspect; claim-vs-evidence pairs on
|
|
191
|
+
* globally-unique tool ids so it cannot cross-match; the approval gate can at
|
|
192
|
+
* worst treat a main-session "ask" as covering a subagent action, which is a
|
|
193
|
+
* false negative — the safe direction for a tool that must not over-accuse.
|
|
194
|
+
*/
|
|
195
|
+
export function findSubagentFiles(sessionFile) {
|
|
196
|
+
const sessionId = basename(sessionFile).replace(/\.jsonl$/, "");
|
|
197
|
+
const subagentDir = join(dirname(sessionFile), sessionId, "subagents");
|
|
198
|
+
return listSessionFiles(subagentDir);
|
|
199
|
+
}
|
|
174
200
|
export function readLatestTranscript(cwd) {
|
|
175
201
|
const filePath = findLatestSessionFile(cwd);
|
|
176
202
|
if (!filePath)
|
|
177
203
|
return [];
|
|
178
|
-
|
|
204
|
+
const events = readTranscriptFromFile(filePath);
|
|
205
|
+
for (const sub of findSubagentFiles(filePath)) {
|
|
206
|
+
events.push(...readTranscriptFromFile(sub));
|
|
207
|
+
}
|
|
208
|
+
return events;
|
|
179
209
|
}
|
package/dist/projectConfig.d.ts
CHANGED
|
@@ -2,24 +2,49 @@ import type { CheckResult, Rule } from "./types.js";
|
|
|
2
2
|
/**
|
|
3
3
|
* A committed, team-shared config at .rulereceipt/config.json.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* the
|
|
8
|
-
*
|
|
9
|
-
*
|
|
5
|
+
* ONE place for "how hard should this rule bite", a ladder per rule handle:
|
|
6
|
+
*
|
|
7
|
+
* off hidden from the report, never gates CI
|
|
8
|
+
* warn shown, does not fail the build
|
|
9
|
+
* error shown, FAILS the build (the default for a checkable rule)
|
|
10
|
+
*
|
|
11
|
+
* This is the honest version of "severity": a team marks must-not-break rules
|
|
12
|
+
* as errors (the default) and nice-to-haves as warnings, and silences the
|
|
13
|
+
* irrelevant ones, so CI gates on what actually matters instead of going red
|
|
14
|
+
* on day one. It replaces three scattered mechanisms — the old `warn` list,
|
|
15
|
+
* `rules --exclude` (now `off`), and the plain default — with one field.
|
|
16
|
+
*
|
|
17
|
+
* Pre-run BLOCKING is deliberately NOT a mode here: refusing a command before
|
|
18
|
+
* it runs still goes through the guard's clause-mark (`rules --forbid`),
|
|
19
|
+
* because a config that could block on any rule would refuse the 62.8% of
|
|
20
|
+
* commands the measured guard already showed it must not. This file governs
|
|
21
|
+
* the report and the CI gate; the guard governs refusal.
|
|
10
22
|
*
|
|
11
23
|
* Handles, not rule ids: an id is positional and renumbers when the file is
|
|
12
24
|
* edited above it; a handle is a content hash, so it survives edits. Get one
|
|
13
25
|
* from `rulereceipt rules --list`.
|
|
26
|
+
*
|
|
27
|
+
* Backward compatible: the old top-level `warn: [handle, ...]` list still
|
|
28
|
+
* works and means the same as `rules: { <handle>: "warn" }`.
|
|
14
29
|
*/
|
|
30
|
+
export type RuleMode = "off" | "warn" | "error";
|
|
15
31
|
export interface ProjectConfig {
|
|
16
32
|
warn: string[];
|
|
33
|
+
rules: Record<string, RuleMode>;
|
|
17
34
|
}
|
|
18
35
|
export declare const PROJECT_CONFIG_PATH: string;
|
|
19
36
|
export declare function loadProjectConfig(cwd: string): ProjectConfig;
|
|
37
|
+
/**
|
|
38
|
+
* The mode for one result. `rules` wins over the legacy `warn` list; anything
|
|
39
|
+
* unlisted is `error`, so the default is unchanged and no config means today's
|
|
40
|
+
* behaviour exactly.
|
|
41
|
+
*/
|
|
42
|
+
export declare function modeForResult(result: CheckResult, config: ProjectConfig, handleFor: (r: CheckResult) => string): RuleMode;
|
|
43
|
+
/** Results the report should show — everything except rules set to `off`. */
|
|
44
|
+
export declare function visibleResults(results: CheckResult[], config: ProjectConfig, handleFor: (r: CheckResult) => string): CheckResult[];
|
|
20
45
|
/** A lookup from a result back to its stable rule handle, built from the loaded rules. */
|
|
21
46
|
export declare function handleMap(rules: Rule[]): (r: CheckResult) => string;
|
|
22
|
-
/** FAILs
|
|
47
|
+
/** FAILs at `error` mode — these fail the build. (`off` never reaches here.) */
|
|
23
48
|
export declare function blockingFailures(results: CheckResult[], config: ProjectConfig, handleFor: (r: CheckResult) => string): CheckResult[];
|
|
24
|
-
/** FAILs
|
|
49
|
+
/** FAILs at `warn` mode — shown, but they do not fail the build. */
|
|
25
50
|
export declare function warningFailures(results: CheckResult[], config: ProjectConfig, handleFor: (r: CheckResult) => string): CheckResult[];
|
package/dist/projectConfig.js
CHANGED
|
@@ -1,19 +1,44 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { ruleFingerprint } from "./overrides.js";
|
|
4
|
+
const VALID_MODES = ["off", "warn", "error"];
|
|
4
5
|
export const PROJECT_CONFIG_PATH = join(".rulereceipt", "config.json");
|
|
5
6
|
export function loadProjectConfig(cwd) {
|
|
6
7
|
try {
|
|
7
8
|
const parsed = JSON.parse(readFileSync(join(cwd, PROJECT_CONFIG_PATH), "utf-8"));
|
|
8
|
-
const warn = parsed?.warn;
|
|
9
|
-
|
|
9
|
+
const warn = Array.isArray(parsed?.warn) ? parsed.warn.filter((x) => typeof x === "string") : [];
|
|
10
|
+
const rules = {};
|
|
11
|
+
if (parsed?.rules && typeof parsed.rules === "object" && !Array.isArray(parsed.rules)) {
|
|
12
|
+
for (const [handle, mode] of Object.entries(parsed.rules)) {
|
|
13
|
+
if (typeof mode === "string" && VALID_MODES.includes(mode))
|
|
14
|
+
rules[handle] = mode;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
return { warn, rules };
|
|
10
18
|
}
|
|
11
19
|
catch {
|
|
12
20
|
// Missing or malformed config means no severities configured, never an
|
|
13
21
|
// error — same fail-open discipline as the rest of the tool.
|
|
14
|
-
return { warn: [] };
|
|
22
|
+
return { warn: [], rules: {} };
|
|
15
23
|
}
|
|
16
24
|
}
|
|
25
|
+
/**
|
|
26
|
+
* The mode for one result. `rules` wins over the legacy `warn` list; anything
|
|
27
|
+
* unlisted is `error`, so the default is unchanged and no config means today's
|
|
28
|
+
* behaviour exactly.
|
|
29
|
+
*/
|
|
30
|
+
export function modeForResult(result, config, handleFor) {
|
|
31
|
+
const handle = handleFor(result);
|
|
32
|
+
if (config.rules[handle])
|
|
33
|
+
return config.rules[handle];
|
|
34
|
+
if (config.warn.includes(handle))
|
|
35
|
+
return "warn";
|
|
36
|
+
return "error";
|
|
37
|
+
}
|
|
38
|
+
/** Results the report should show — everything except rules set to `off`. */
|
|
39
|
+
export function visibleResults(results, config, handleFor) {
|
|
40
|
+
return results.filter((r) => modeForResult(r, config, handleFor) !== "off");
|
|
41
|
+
}
|
|
17
42
|
/** A lookup from a result back to its stable rule handle, built from the loaded rules. */
|
|
18
43
|
export function handleMap(rules) {
|
|
19
44
|
const m = new Map();
|
|
@@ -21,11 +46,11 @@ export function handleMap(rules) {
|
|
|
21
46
|
m.set(`${rule.source}:${rule.id}`, ruleFingerprint(rule));
|
|
22
47
|
return (r) => m.get(`${r.ruleSource}:${r.ruleId}`) ?? "";
|
|
23
48
|
}
|
|
24
|
-
/** FAILs
|
|
49
|
+
/** FAILs at `error` mode — these fail the build. (`off` never reaches here.) */
|
|
25
50
|
export function blockingFailures(results, config, handleFor) {
|
|
26
|
-
return results.filter((r) => r.status === "FAIL" &&
|
|
51
|
+
return results.filter((r) => r.status === "FAIL" && modeForResult(r, config, handleFor) === "error");
|
|
27
52
|
}
|
|
28
|
-
/** FAILs
|
|
53
|
+
/** FAILs at `warn` mode — shown, but they do not fail the build. */
|
|
29
54
|
export function warningFailures(results, config, handleFor) {
|
|
30
|
-
return results.filter((r) => r.status === "FAIL" && config
|
|
55
|
+
return results.filter((r) => r.status === "FAIL" && modeForResult(r, config, handleFor) === "warn");
|
|
31
56
|
}
|
package/dist/rules.js
CHANGED
|
@@ -13,8 +13,6 @@ import { findClaudeHomeDirNames } from "./parsers/transcriptParser.js";
|
|
|
13
13
|
* the tool never opened is the most misleading result this can produce,
|
|
14
14
|
* worse than no report, because it looks like evidence.
|
|
15
15
|
*/
|
|
16
|
-
const RULE_FILE_NAMES = ["CLAUDE.md", "AGENTS.md", "CLAUDE.local.md", "AGENTS.local.md"];
|
|
17
|
-
const RULE_SUBDIR_FILES = [join(".claude", "CLAUDE.md"), join(".claude", "AGENTS.md")];
|
|
18
16
|
const RULE_DIRS = [join(".claude", "rules")];
|
|
19
17
|
/**
|
|
20
18
|
* Lists the markdown files in a rules directory, if it exists.
|
|
@@ -43,18 +41,30 @@ function markdownFilesIn(dir) {
|
|
|
43
41
|
/** Every rules file at one directory level, in documented load order. */
|
|
44
42
|
function ruleFilesAtLevel(dir) {
|
|
45
43
|
const found = [];
|
|
46
|
-
|
|
44
|
+
const push = (rel) => {
|
|
47
45
|
const p = join(dir, rel);
|
|
48
46
|
if (existsSync(p))
|
|
49
47
|
found.push(p);
|
|
50
|
-
}
|
|
48
|
+
};
|
|
49
|
+
const has = (rel) => existsSync(join(dir, rel));
|
|
50
|
+
// CLAUDE.md shadows AGENTS.md at the same level: as of 2026-09-19 Claude
|
|
51
|
+
// Code loads AGENTS.md ONLY when that level has no CLAUDE.md, and silently
|
|
52
|
+
// ignores it otherwise. Reading a shadowed AGENTS.md here would check the
|
|
53
|
+
// session against rules Claude never loaded — a false accusation. `init`
|
|
54
|
+
// separately WARNS about the shadowed file (see shadowedAgents.ts) so the
|
|
55
|
+
// rules are not lost silently. Mirrored for the `.claude/` subdir pair.
|
|
56
|
+
push(join(".claude", "CLAUDE.md"));
|
|
57
|
+
if (!has(join(".claude", "CLAUDE.md")))
|
|
58
|
+
push(join(".claude", "AGENTS.md"));
|
|
51
59
|
for (const rel of RULE_DIRS)
|
|
52
60
|
found.push(...markdownFilesIn(join(dir, rel)));
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
61
|
+
push("CLAUDE.md");
|
|
62
|
+
if (!has("CLAUDE.md"))
|
|
63
|
+
push("AGENTS.md");
|
|
64
|
+
// .local variants: their precedence relative to the base files is not
|
|
65
|
+
// documented, so both are kept rather than guessing at a shadow rule.
|
|
66
|
+
push("CLAUDE.local.md");
|
|
67
|
+
push("AGENTS.local.md");
|
|
58
68
|
return found;
|
|
59
69
|
}
|
|
60
70
|
/**
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* An AGENTS.md that Claude Code never loads because a CLAUDE.md sits beside it.
|
|
3
|
+
*
|
|
4
|
+
* As of 2026-09-19, Claude Code reads AGENTS.md at a directory level ONLY when
|
|
5
|
+
* that level has no CLAUDE.md; if both exist, the AGENTS.md is silently
|
|
6
|
+
* ignored (InfoWorld / Enterprise DNA, 2026-09). So a rule a user carefully
|
|
7
|
+
* wrote into AGENTS.md next to a CLAUDE.md governs nothing — Claude never saw
|
|
8
|
+
* it.
|
|
9
|
+
*
|
|
10
|
+
* This matters to RuleReceipt in TWO ways:
|
|
11
|
+
* 1. A warning the user needs: "these rules are dead, move them into
|
|
12
|
+
* CLAUDE.md." That is what this surfaces.
|
|
13
|
+
* 2. A false-accusation risk in the tool itself: loadRules currently reads
|
|
14
|
+
* BOTH files, so it could report the session for breaking a shadowed
|
|
15
|
+
* AGENTS.md rule Claude never loaded. That deeper loading fix is tracked
|
|
16
|
+
* separately; this detector is the first, safe, additive step.
|
|
17
|
+
*
|
|
18
|
+
* Detection mirrors Claude Code's own precedence per directory level: a
|
|
19
|
+
* CLAUDE.md shadows an AGENTS.md at the same level, and the same for the
|
|
20
|
+
* `.claude/` subdirectory pair.
|
|
21
|
+
*/
|
|
22
|
+
export interface ShadowedAgents {
|
|
23
|
+
/** The AGENTS.md that is being ignored. */
|
|
24
|
+
agents: string;
|
|
25
|
+
/** The CLAUDE.md at the same level that shadows it. */
|
|
26
|
+
shadowedBy: string;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Walks from cwd up to the repository root (inclusive), the same span
|
|
30
|
+
* loadRules reads project rules over, and returns every AGENTS.md shadowed by
|
|
31
|
+
* a CLAUDE.md. Global (home-dir) files are out of scope: that is a different
|
|
32
|
+
* precedence and a different fix.
|
|
33
|
+
*/
|
|
34
|
+
export declare function shadowedAgentsMd(cwd: string): ShadowedAgents[];
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { join, dirname, parse } from "node:path";
|
|
4
|
+
/** Directory-level pairs where a CLAUDE.md shadows an AGENTS.md. */
|
|
5
|
+
const SHADOW_PAIRS = [
|
|
6
|
+
{ claude: "CLAUDE.md", agents: "AGENTS.md" },
|
|
7
|
+
{ claude: join(".claude", "CLAUDE.md"), agents: join(".claude", "AGENTS.md") },
|
|
8
|
+
];
|
|
9
|
+
/**
|
|
10
|
+
* Walks from cwd up to the repository root (inclusive), the same span
|
|
11
|
+
* loadRules reads project rules over, and returns every AGENTS.md shadowed by
|
|
12
|
+
* a CLAUDE.md. Global (home-dir) files are out of scope: that is a different
|
|
13
|
+
* precedence and a different fix.
|
|
14
|
+
*/
|
|
15
|
+
export function shadowedAgentsMd(cwd) {
|
|
16
|
+
const found = [];
|
|
17
|
+
const { root } = parse(cwd);
|
|
18
|
+
const home = homedir();
|
|
19
|
+
let dir = cwd;
|
|
20
|
+
for (;;) {
|
|
21
|
+
if (dir === home && dir !== cwd)
|
|
22
|
+
break;
|
|
23
|
+
for (const { claude, agents } of SHADOW_PAIRS) {
|
|
24
|
+
const claudePath = join(dir, claude);
|
|
25
|
+
const agentsPath = join(dir, agents);
|
|
26
|
+
if (existsSync(claudePath) && existsSync(agentsPath)) {
|
|
27
|
+
found.push({ agents: agentsPath, shadowedBy: claudePath });
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
if (existsSync(join(dir, ".git")))
|
|
31
|
+
break;
|
|
32
|
+
if (dir === root)
|
|
33
|
+
break;
|
|
34
|
+
const parent = dirname(dir);
|
|
35
|
+
if (parent === dir)
|
|
36
|
+
break;
|
|
37
|
+
dir = parent;
|
|
38
|
+
}
|
|
39
|
+
return found;
|
|
40
|
+
}
|