rulereceipt 0.1.47 → 0.1.48

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/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";
@@ -808,6 +809,7 @@ program
808
809
  hasAgentsMd: existsSync(join(cwd, "AGENTS.md")),
809
810
  hookInstalled: hookIsInstalled(cwd),
810
811
  hasApiKey: Boolean(process.env.ANTHROPIC_API_KEY),
812
+ shadowedAgents: shadowedAgentsMd(cwd).map((s) => s.agents),
811
813
  }));
812
814
  });
813
815
  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
- return readTranscriptFromFile(filePath);
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/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
- for (const rel of RULE_SUBDIR_FILES) {
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
- for (const name of RULE_FILE_NAMES) {
54
- const p = join(dir, name);
55
- if (existsSync(p))
56
- found.push(p);
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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rulereceipt",
3
- "version": "0.1.47",
3
+ "version": "0.1.48",
4
4
  "description": "Checks whether a Claude Code session actually followed your CLAUDE.md / AGENTS.md rules, with evidence.",
5
5
  "repository": {
6
6
  "type": "git",