rulereceipt 0.1.75 → 0.1.76

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 CHANGED
@@ -6,15 +6,21 @@
6
6
  [![npm](https://img.shields.io/npm/v/rulereceipt)](https://www.npmjs.com/package/rulereceipt)
7
7
  [![provenance](https://img.shields.io/badge/npm-provenance%20signed-blue)](https://www.npmjs.com/package/rulereceipt#provenance)
8
8
 
9
- Checks whether your AI coding agent actually followed your rules — with
10
- evidence, not just a vibe. Works with Claude Code today (OpenAI Codex CLI
11
- support is built and in testing), and reads rules from CLAUDE.md, AGENTS.md,
12
- Cursor (`.cursor/rules`), GitHub Copilot, Windsurf, Gemini (`GEMINI.md`),
13
- Google's `.agents/rules`, and Claude Code memory.
9
+ **Check if your AI coding agent followed the rules in your CLAUDE.md, with the exact line as proof.**
10
+
11
+ [![RuleReceipt checking an agent session against your rules — three rules broken, each with the quoted line](https://raw.githubusercontent.com/rulereceipt/rulereceipt/main/docs/screenshot.png)](https://rulereceipt.dev)
12
+
13
+ ```bash
14
+ npx rulereceipt
15
+ ```
14
16
 
15
17
  Runs entirely on your machine. Plain `rulereceipt check` makes zero network
16
- calls — [Trust, privacy and licensing](#trust-privacy-and-licensing) has the
17
- full detail, including the three off-by-default opt-ins.
18
+ calls — [Trust, privacy and licensing](#trust-privacy-and-licensing) has the full
19
+ detail, including the three off-by-default opt-ins. Works with Claude Code today
20
+ (OpenAI Codex CLI in testing); reads rules from CLAUDE.md, AGENTS.md, Cursor
21
+ (`.cursor/rules`), GitHub Copilot, Windsurf, Gemini (`GEMINI.md`), Google's
22
+ `.agents/rules`, and Claude Code memory. [Accuracy](https://rulereceipt.dev/accuracy)
23
+ · [Known gaps](KNOWN-GAPS.md) · Source-available, not OSI — see [LICENSE](LICENSE).
18
24
 
19
25
  ## See it in 10 seconds
20
26
 
@@ -0,0 +1,49 @@
1
+ import type { Classification } from "./checks/classify.js";
2
+ /**
3
+ * `why` answers "why isn't THIS rule working?". For a rule that names a
4
+ * concrete action, the honest follow-up is "then how do I actually stop it?".
5
+ * This computes that answer — and it is deliberately pinned to what the tool
6
+ * really does, because an overstated "add this and you're safe" is exactly the
7
+ * failure RuleReceipt exists to catch.
8
+ *
9
+ * Two facts, kept separate because they are not the same promise:
10
+ *
11
+ * - `guardCovers`: RuleReceipt's own PreToolUse guard refuses this exact rule
12
+ * before the action runs. True only for the kinds guardDecision/structuredBlocks
13
+ * actually handle (a branch, a file, forbidden file content, an AI-authorship
14
+ * trailer) and for an approval gate (which it answers "ask", or "deny" in a
15
+ * no-prompt mode). It is NOT a general command blocker — that was measured and
16
+ * cut (see guard.ts), so nothing here claims it.
17
+ *
18
+ * - `native`: a Claude Code `permissions` entry the user can add by hand with no
19
+ * extra tool. Only emitted where a permission rule can genuinely express the
20
+ * thing, and always with the honest caveat: a permission rule matches the
21
+ * command text or the file path, so it cannot scope to a branch, cannot read a
22
+ * commit message, and cannot see file content. Where it can't express the rule,
23
+ * `nativeImpossibleReason` says so instead of inventing a rule that wouldn't fire.
24
+ *
25
+ * `preventable: false` is the honest answer for the rest: a claim-evidence rule,
26
+ * an emoji rule, an edit-implies-test rule, a plain literal rule — these are judged
27
+ * AFTER the run, not blockable before an action. The caller points at `check` and
28
+ * the Stop hook instead.
29
+ */
30
+ export interface BlockHint {
31
+ /** Can an action be refused BEFORE it runs (guard or a native deny/ask)? */
32
+ preventable: boolean;
33
+ /** Does RuleReceipt's own guard (`rulereceipt protect`) check this exact rule pre-flight? */
34
+ guardCovers: boolean;
35
+ /** A native Claude Code permissions entry, where one can genuinely express the rule. */
36
+ native?: {
37
+ kind: "deny" | "ask";
38
+ entries: string[];
39
+ note: string;
40
+ };
41
+ /** When preventable but a native permission rule cannot express it, why. */
42
+ nativeImpossibleReason?: string;
43
+ }
44
+ /**
45
+ * The block advice for one classified rule, or undefined when there is nothing
46
+ * useful to say (a judgment rule or a non-rule — the "needs your judgment" line
47
+ * already covers those).
48
+ */
49
+ export declare function blockHintFor(cls: Classification): BlockHint | undefined;
@@ -0,0 +1,87 @@
1
+ /** Map an approval-gate action to the Claude Code permission pattern that matches it. */
2
+ function askEntryFor(action) {
3
+ switch (action) {
4
+ case "push": return "Bash(git push:*)";
5
+ case "commit": return "Bash(git commit:*)";
6
+ case "pr": return "Bash(gh pr:*)";
7
+ case "delete": return "Bash(rm:*)";
8
+ default: return undefined;
9
+ }
10
+ }
11
+ /**
12
+ * The block advice for one classified rule, or undefined when there is nothing
13
+ * useful to say (a judgment rule or a non-rule — the "needs your judgment" line
14
+ * already covers those).
15
+ */
16
+ export function blockHintFor(cls) {
17
+ switch (cls.kind) {
18
+ case "gitBranchPolicy": {
19
+ if (cls.polarity !== "forbid")
20
+ return { preventable: false, guardCovers: false };
21
+ return {
22
+ preventable: true,
23
+ guardCovers: true,
24
+ native: {
25
+ kind: "deny",
26
+ entries: ["Bash(git push:*)"],
27
+ note: `a permission rule matches the command text, so this denies EVERY push — it can't ` +
28
+ `scope to the \`${cls.branchName}\` branch. The guard below checks the actual target branch.`,
29
+ },
30
+ };
31
+ }
32
+ case "fileLifecycle": {
33
+ if (cls.polarity !== "forbid")
34
+ return { preventable: false, guardCovers: false };
35
+ return {
36
+ preventable: true,
37
+ guardCovers: true,
38
+ native: {
39
+ kind: "deny",
40
+ entries: [`Edit(${cls.filePath})`, `Write(${cls.filePath})`],
41
+ note: `covers Edit/Write of that path; a Bash \`rm\`/\`mv\` targeting it is NOT matched by ` +
42
+ `these — the guard below covers those too.`,
43
+ },
44
+ };
45
+ }
46
+ case "approvalGate": {
47
+ const entries = cls.actions.map(askEntryFor).filter((e) => e !== undefined);
48
+ if (entries.length === 0)
49
+ return { preventable: true, guardCovers: true, nativeImpossibleReason: "the action it gates isn't one a permission rule can match" };
50
+ return {
51
+ preventable: true,
52
+ guardCovers: true,
53
+ native: {
54
+ kind: "ask",
55
+ entries,
56
+ note: `an "ask" is skipped in no-prompt modes (bypassPermissions / auto), so the action would ` +
57
+ `run there without a prompt. The guard below denies it in those modes instead.`,
58
+ },
59
+ };
60
+ }
61
+ case "attribution":
62
+ return {
63
+ preventable: true,
64
+ guardCovers: true,
65
+ nativeImpossibleReason: "a permission rule can't read a commit message, so it can't catch an AI-authorship trailer",
66
+ };
67
+ case "codeContent": {
68
+ if (cls.polarity !== "forbid")
69
+ return { preventable: false, guardCovers: false };
70
+ return {
71
+ preventable: true,
72
+ guardCovers: true,
73
+ nativeImpossibleReason: "a permission rule matches the command or path, not the content written into a file",
74
+ };
75
+ }
76
+ case "claimEvidence":
77
+ case "ifEditThenTest":
78
+ case "emojiOutput":
79
+ case "deterministic":
80
+ // Checkable, but only after the run — there is no single action to refuse
81
+ // beforehand. The caller points at `check` and the Stop hook.
82
+ return { preventable: false, guardCovers: false };
83
+ default:
84
+ // judgment, notARule — nothing mechanical to block or check.
85
+ return undefined;
86
+ }
87
+ }
package/dist/cli.js CHANGED
@@ -19,7 +19,7 @@ import { ghReady, issueTitle, issueCreateArgs, buildMailto, mailtoSubject } from
19
19
  import { spawnSync } from "node:child_process";
20
20
  import { detectSelfEditedRuleFiles } from "./checks/selfEditedRules.js";
21
21
  import { scanHistory, renderHistory } from "./historyReport.js";
22
- import { explainRule, renderWhy } from "./why.js";
22
+ import { explainRule, renderWhy, explainAll, renderAllWhy, whyList, renderWhyList } from "./why.js";
23
23
  import { observeSessions, renderNoRules, draftRulesFromHistory } from "./sessionObserve.js";
24
24
  import { listSessionRows, renderSessionList } from "./listSessions.js";
25
25
  import { runSelfTestChecks, renderSelfTest } from "./selftest.js";
@@ -931,7 +931,7 @@ const PERIOD_MS = {
931
931
  };
932
932
  program
933
933
  .command("report")
934
- .description("Compliance report across your recent sessions (not just the latest): which policy rules were broken, where, with evidence. Deterministic, local, no network. The org-wide version runs via the Claude Compliance API for Enterprise orgs.")
934
+ .description("Compliance report across your recent sessions (not just the latest): which policy rules were broken, where, with evidence. Deterministic, local, no network. An org-wide version (multi-repo, trends, a manager digest) is coming in the team version.")
935
935
  .option("--last <n>", "how many recent sessions to audit", "25")
936
936
  .option("--markdown", "output as markdown, for a report you can send")
937
937
  .action(async (opts) => {
@@ -953,18 +953,31 @@ program
953
953
  console.log(renderProjectAudit(a, Boolean(opts.markdown)));
954
954
  });
955
955
  program
956
- .command("why <rule...>")
957
- .description("Everything the tool knows about ONE rule, in one place: where it lives (file:line), whether the agent actually loads it, whether a command or path it names exists, whether it's mechanically checkable (and if not, one suggested rewrite), and how it did over the last 30 days. Fuzzy-matches the rule text; if several match, lists them. Read-only — no verdict is created, nothing is sent.")
956
+ .command("why [rule...]")
957
+ .description("Everything the tool knows about ONE rule, in one place: where it lives (file:line), whether the agent actually loads it, whether a command or path it names exists, whether it's mechanically checkable (and if not, one suggested rewrite), and how it did over the last 30 days. With no argument, lists every rule with its id so you can pick one; with --all, shows every rule (problems first). Read-only — no verdict is created, nothing is sent.")
958
+ .option("--all", "show every rule, problems first (not loaded, missing command, broken recently), then the rest")
958
959
  .option("--json", "output machine-readable JSON (the same fields)")
959
960
  .action(async (ruleWords, opts) => {
960
961
  const cwd = process.cwd();
961
- const query = ruleWords.join(" ").trim();
962
+ const query = (ruleWords ?? []).join(" ").trim();
962
963
  const rules = loadRules(cwd);
963
964
  if (rules.length === 0) {
964
965
  console.log("No rules file found here, so there is nothing to explain. Run `rulereceipt init` to add one.");
965
966
  process.exitCode = 1;
966
967
  return;
967
968
  }
969
+ // --all: every rule, problems first.
970
+ if (opts.all) {
971
+ const all = await explainAll(cwd);
972
+ console.log(opts.json ? JSON.stringify(all, null, 2) : renderAllWhy(all));
973
+ return;
974
+ }
975
+ // No argument: list the rules with ids so the reader can pick one.
976
+ if (query.length === 0) {
977
+ const list = whyList(cwd);
978
+ console.log(opts.json ? JSON.stringify(list, null, 2) : renderWhyList(list));
979
+ return;
980
+ }
968
981
  const result = await explainRule(cwd, query);
969
982
  if (opts.json) {
970
983
  console.log(JSON.stringify(result, null, 2));
@@ -976,7 +989,7 @@ program
976
989
  });
977
990
  program
978
991
  .command("wrong <rule>")
979
- .description("A verdict looks wrong? Builds a report of that rule, the verdict, how it was decided and the session lines around it, with obvious secrets masked. Written to a local file and shown first; prints a GitHub issue link for you to open. Nothing is sent.")
992
+ .description("A verdict looks wrong? Builds a report of that rule, the verdict, how it was decided and the session lines around it, with obvious secrets masked. Written to a local file and shown first. Then --submit opens a public GitHub issue (asks first; needs gh) or --email sends it privately to the maintainer; with no flag it just prints the report and a pre-filled link. Nothing is sent without your say-so.")
980
993
  .option("--transcript <path>", "use a specific session file (same as check)")
981
994
  .option("--out <path>", "where to write the report (default .rulereceipt/wrong-<handle>.md)")
982
995
  .option("--no-context", "leave out the session lines around the evidence")
@@ -150,6 +150,8 @@ export function renderHistory(s, projectName, now = Date.now()) {
150
150
  out.push(`checked ${s.sessionsScanned} session${s.sessionsScanned === 1 ? "" : "s"} in ${secs}s`);
151
151
  out.push("");
152
152
  out.push("See one session in full: rulereceipt check");
153
+ out.push("Make Claude ask first: rulereceipt protect");
154
+ out.push("Share the result: rulereceipt card");
153
155
  out.push("Think a verdict is wrong? rulereceipt wrong <rule>");
154
156
  return out.join("\n");
155
157
  }
package/dist/why.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { type BlockHint } from "./blockHint.js";
1
2
  import type { Rule } from "./types.js";
2
3
  export interface WhyRule {
3
4
  id: string;
@@ -15,6 +16,8 @@ export interface WhyRule {
15
16
  name: string;
16
17
  exists: boolean;
17
18
  };
19
+ /** How (and whether) this rule can actually be blocked/checked — see blockHint.ts. */
20
+ block?: BlockHint;
18
21
  brokenCount: number;
19
22
  brokenDates: string[];
20
23
  sessionsScanned: number;
@@ -31,4 +34,28 @@ export interface WhyResult {
31
34
  /** Fuzzy-match a rule by the query appearing in its title/body, or the query being the title. */
32
35
  export declare function findRules(rules: Rule[], query: string): Rule[];
33
36
  export declare function explainRule(cwd: string, query: string): Promise<WhyResult>;
37
+ export interface WhyAll {
38
+ rules: WhyRule[];
39
+ sessionsScanned: number;
40
+ }
41
+ /**
42
+ * `why --all` — the same facts for every loaded rule, problems first (not loaded, then a
43
+ * named command/file missing, then broken recently, then judgment-only, then the rest).
44
+ * Read-only, facts only, no verdicts. Order within a rank is the rule's own order.
45
+ */
46
+ export declare function explainAll(cwd: string): Promise<WhyAll>;
47
+ /** The no-argument help: how to use `why`, plus every rule with its id, so people can pick. */
48
+ export declare function whyList(cwd: string): {
49
+ id: string;
50
+ title: string;
51
+ location: string;
52
+ }[];
34
53
  export declare function renderWhy(r: WhyResult): string;
54
+ /** No-argument output: how to use `why`, then every rule with its id so you can pick one. */
55
+ export declare function renderWhyList(list: {
56
+ id: string;
57
+ title: string;
58
+ location: string;
59
+ }[]): string;
60
+ /** One short line per rule for `why --all`, problems first. */
61
+ export declare function renderAllWhy(all: WhyAll): string;
package/dist/why.js CHANGED
@@ -5,6 +5,7 @@ import { loadRules, describeRuleSources } from "./rules.js";
5
5
  import { classifyRule } from "./checks/classify.js";
6
6
  import { adviseRule } from "./checkability.js";
7
7
  import { scanHistory } from "./historyReport.js";
8
+ import { blockHintFor } from "./blockHint.js";
8
9
  /**
9
10
  * `rulereceipt why "<rule text>"` — everything the tool already knows about ONE
10
11
  * rule, in one place: where it lives, whether the agent even loads it, whether a
@@ -63,6 +64,43 @@ function namedCommandOrPath(rule, cwd) {
63
64
  }
64
65
  return undefined;
65
66
  }
67
+ async function historyLookup(cwd, rules) {
68
+ try {
69
+ const hist = await scanHistory(cwd, rules, 30);
70
+ const byRule = new Map();
71
+ for (const b of hist.breaks)
72
+ byRule.set(`${b.ruleId}\u0000${b.ruleTitle}`, { count: b.count, lastMs: b.lastMs });
73
+ return { sessionsScanned: hist.sessionsScanned, byRule };
74
+ }
75
+ catch {
76
+ return { sessionsScanned: 0, byRule: new Map() };
77
+ }
78
+ }
79
+ /** Build the WhyRule facts for one rule, reusing an already-computed load graph and history. */
80
+ function ruleFacts(cwd, rule, graph, hist) {
81
+ const src = rule.sourcePath ? graph.find((e) => e.path === rule.sourcePath) : undefined;
82
+ const cls = classifyRule(rule);
83
+ const checkable = CHECKABLE_KINDS.has(cls.kind);
84
+ const advice = checkable ? null : adviseRule(rule);
85
+ const b = hist.byRule.get(`${rule.id}\u0000${rule.title}`);
86
+ return {
87
+ id: rule.id,
88
+ title: rule.title,
89
+ source: rule.source,
90
+ location: locationOf(rule),
91
+ loaded: src ? src.status === "loaded" : true,
92
+ loadNote: src?.note,
93
+ pathScoped: rule.paths ? rule.paths.join(", ") : undefined,
94
+ checkable,
95
+ kind: cls.kind,
96
+ suggestion: advice?.suggestion,
97
+ named: namedCommandOrPath(rule, cwd),
98
+ block: blockHintFor(cls),
99
+ brokenCount: b?.count ?? 0,
100
+ brokenDates: b ? [new Date(b.lastMs).toISOString().slice(0, 10)] : [],
101
+ sessionsScanned: hist.sessionsScanned,
102
+ };
103
+ }
66
104
  export async function explainRule(cwd, query) {
67
105
  const rules = loadRules(cwd);
68
106
  const matches = findRules(rules, query);
@@ -75,47 +113,41 @@ export async function explainRule(cwd, query) {
75
113
  candidates: matches.slice(0, 12).map((r) => ({ title: r.title, location: locationOf(r) })),
76
114
  };
77
115
  }
78
- const rule = matches[0];
79
116
  const graph = describeRuleSources(cwd);
80
- const src = rule.sourcePath ? graph.find((e) => e.path === rule.sourcePath) : undefined;
81
- const cls = classifyRule(rule);
82
- const checkable = CHECKABLE_KINDS.has(cls.kind);
83
- const advice = checkable ? null : adviseRule(rule);
84
- let brokenCount = 0;
85
- let brokenDates = [];
86
- let sessionsScanned = 0;
87
- try {
88
- const hist = await scanHistory(cwd, rules, 30);
89
- sessionsScanned = hist.sessionsScanned;
90
- const b = hist.breaks.find((x) => x.ruleId === rule.id && x.ruleTitle === rule.title);
91
- if (b) {
92
- brokenCount = b.count;
93
- brokenDates = [new Date(b.lastMs).toISOString().slice(0, 10)];
94
- }
95
- }
96
- catch {
97
- /* history is best-effort; a rule can still be explained without it */
98
- }
99
- return {
100
- query,
101
- matches: 1,
102
- rule: {
103
- id: rule.id,
104
- title: rule.title,
105
- source: rule.source,
106
- location: locationOf(rule),
107
- loaded: src ? src.status === "loaded" : true,
108
- loadNote: src?.note,
109
- pathScoped: rule.paths ? rule.paths.join(", ") : undefined,
110
- checkable,
111
- kind: cls.kind,
112
- suggestion: advice?.suggestion,
113
- named: namedCommandOrPath(rule, cwd),
114
- brokenCount,
115
- brokenDates,
116
- sessionsScanned,
117
- },
118
- };
117
+ const hist = await historyLookup(cwd, rules);
118
+ return { query, matches: 1, rule: ruleFacts(cwd, matches[0], graph, hist) };
119
+ }
120
+ /** A "problem" rank so `why --all` can put the ones that need attention first. */
121
+ function problemRank(w) {
122
+ if (!w.loaded)
123
+ return 0; // the agent never sees it
124
+ if (w.named && !w.named.exists)
125
+ return 1; // names a command/file that does not exist
126
+ if (w.brokenCount > 0)
127
+ return 2; // broken recently
128
+ if (!w.checkable)
129
+ return 3; // needs judgment
130
+ return 4; // fine
131
+ }
132
+ /**
133
+ * `why --all` — the same facts for every loaded rule, problems first (not loaded, then a
134
+ * named command/file missing, then broken recently, then judgment-only, then the rest).
135
+ * Read-only, facts only, no verdicts. Order within a rank is the rule's own order.
136
+ */
137
+ export async function explainAll(cwd) {
138
+ const rules = loadRules(cwd);
139
+ const graph = describeRuleSources(cwd);
140
+ const hist = await historyLookup(cwd, rules);
141
+ const facts = rules.map((r) => ruleFacts(cwd, r, graph, hist));
142
+ const ranked = facts
143
+ .map((w, i) => ({ w, i }))
144
+ .sort((a, b) => problemRank(a.w) - problemRank(b.w) || a.i - b.i)
145
+ .map((x) => x.w);
146
+ return { rules: ranked, sessionsScanned: hist.sessionsScanned };
147
+ }
148
+ /** The no-argument help: how to use `why`, plus every rule with its id, so people can pick. */
149
+ export function whyList(cwd) {
150
+ return loadRules(cwd).map((r) => ({ id: r.id, title: r.title, location: locationOf(r) }));
119
151
  }
120
152
  function locationOf(rule) {
121
153
  if (!rule.sourcePath)
@@ -144,6 +176,7 @@ export function renderWhy(r) {
144
176
  out.push(w.checkable
145
177
  ? ` ✓ mechanically checkable (${w.kind}) — a session is judged against it with quoted evidence`
146
178
  : ` • needs your judgment${w.suggestion ? ` — ${w.suggestion}` : ` — no command or file to check it by; a human decides`}`);
179
+ renderBlockHint(w.block, out);
147
180
  out.push("");
148
181
  if (w.brokenCount > 0)
149
182
  out.push(` last 30 days: broken ${w.brokenCount}× (last: ${w.brokenDates[0]}) across ${w.sessionsScanned} session${w.sessionsScanned === 1 ? "" : "s"}`);
@@ -151,3 +184,72 @@ export function renderWhy(r) {
151
184
  out.push(` last 30 days: no proven break across ${w.sessionsScanned} session${w.sessionsScanned === 1 ? "" : "s"}`);
152
185
  return out.join("\n");
153
186
  }
187
+ /**
188
+ * How to actually enforce this rule, appended to the single-rule view. Prints
189
+ * only what the tool truly does: a native Claude Code permissions rule where one
190
+ * can genuinely express it (with its honest limitation), RuleReceipt's own guard
191
+ * where it covers the rule pre-flight, and — for rules judged only after the run —
192
+ * the after-the-fact path. Nothing here claims a block the guard doesn't make.
193
+ */
194
+ function renderBlockHint(block, out) {
195
+ if (!block)
196
+ return;
197
+ out.push("");
198
+ if (block.preventable) {
199
+ out.push(" To stop this before it runs:");
200
+ if (block.native) {
201
+ const entries = block.native.entries.map((e) => `"${e}"`).join(", ");
202
+ out.push(` • Claude Code settings (.claude/settings.json), no extra tool:`);
203
+ out.push(` { "permissions": { "${block.native.kind}": [${entries}] } }`);
204
+ out.push(` ${block.native.note}`);
205
+ }
206
+ else if (block.nativeImpossibleReason) {
207
+ out.push(` • A Claude Code permission rule can't express this — ${block.nativeImpossibleReason}.`);
208
+ }
209
+ if (block.guardCovers) {
210
+ out.push(` • RuleReceipt's own guard checks this exact rule: run \`rulereceipt protect\``);
211
+ out.push(` (adds a PreToolUse deny hook + a Stop hook, shown before it writes anything).`);
212
+ }
213
+ }
214
+ else {
215
+ out.push(" Can't be blocked before an action — this rule is judged after the run.");
216
+ out.push(" Run `rulereceipt check` after a session; `rulereceipt protect` also adds a Stop");
217
+ out.push(" hook that won't let a session end on a broken rule.");
218
+ }
219
+ }
220
+ /** No-argument output: how to use `why`, then every rule with its id so you can pick one. */
221
+ export function renderWhyList(list) {
222
+ if (list.length === 0)
223
+ return "No rules file found here. Run `rulereceipt init` to add one, then `rulereceipt why \"<rule>\"`.";
224
+ const out = ['Explain one rule: rulereceipt why "<some words from the rule>"', "Or all of them: rulereceipt why --all", "", "Rules found:"];
225
+ for (const r of list)
226
+ out.push(` ${r.id.padEnd(6)} ${r.title.replace(/\s+/g, " ").slice(0, 72)}`);
227
+ return out.join("\n");
228
+ }
229
+ /** One short line per rule for `why --all`, problems first. */
230
+ export function renderAllWhy(all) {
231
+ if (all.rules.length === 0)
232
+ return "No rules file found here. Run `rulereceipt init` to add one.";
233
+ const out = [];
234
+ for (const w of all.rules) {
235
+ let mark = " ✓";
236
+ let note = w.checkable ? `checkable (${w.kind})` : "needs your judgment";
237
+ if (!w.loaded) {
238
+ mark = " ✗";
239
+ note = `NOT loaded — ${w.loadNote ?? "the agent never sees this file"}`;
240
+ }
241
+ else if (w.named && !w.named.exists) {
242
+ mark = " ✗";
243
+ note = `names a ${w.named.kind} that does not exist: ${w.named.name}`;
244
+ }
245
+ else if (w.brokenCount > 0) {
246
+ mark = " ✗";
247
+ note = `broken ${w.brokenCount}× (last: ${w.brokenDates[0]})`;
248
+ }
249
+ out.push(`${mark} ${w.id.padEnd(6)} ${w.title.replace(/\s+/g, " ").slice(0, 60)}`);
250
+ out.push(` ${note}`);
251
+ }
252
+ out.push("");
253
+ out.push(`across ${all.sessionsScanned} session${all.sessionsScanned === 1 ? "" : "s"} · run \`rulereceipt why "<rule>"\` for one rule in full`);
254
+ return out.join("\n");
255
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rulereceipt",
3
- "version": "0.1.75",
3
+ "version": "0.1.76",
4
4
  "description": "Checks whether your AI coding agent followed your rules, with evidence. Works with Claude Code (Codex in testing); reads CLAUDE.md, AGENTS.md, Cursor, Copilot and Windsurf rules.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -43,6 +43,7 @@
43
43
  "test": "vitest run",
44
44
  "test:watch": "vitest",
45
45
  "lint": "eslint src tests",
46
+ "license:check": "node scripts/license-gate.mjs",
46
47
  "prepublishOnly": "npm run build && npm test",
47
48
  "build:checker": "esbuild src/browser/analyze.ts --bundle --format=esm --minify --outfile=landing/checker.js",
48
49
  "verify": "npm run lint && npm run typecheck && npm test"