rulebookguard 0.11.0 → 0.13.0

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
@@ -43,7 +43,7 @@ Requires Node.js 22 or later.
43
43
  rulebookguard does not need to be a dependency of your project. Nothing in your code imports it, so you can run it with `npx`, pinning the version so that every machine and CI run the same one:
44
44
 
45
45
  ```sh
46
- npx rulebookguard@0.11.0 lint
46
+ npx rulebookguard@0.13.0 lint
47
47
  ```
48
48
 
49
49
  Each release locks its own dependencies for npm and `npx`, so a pinned version keeps running the same code. Other package managers, such as `pnpm dlx`, ignore that lock and resolve the dependency ranges. Your `package.json` and lockfile stay untouched.
@@ -56,10 +56,10 @@ npm install --save-dev rulebookguard
56
56
 
57
57
  A first rule in four steps:
58
58
 
59
- 1. Set up the rulebook: `npx rulebookguard@0.11.0 init clients --ci` writes `rulebookguard.config.yaml`, the `clients` context and a CI workflow (see [Configuration](#configuration) to adjust `testGlobs`).
60
- 2. Scaffold a rule: `npx rulebookguard@0.11.0 new clients` creates `docs/rules/clients/R-CLI-001.md` as `proposed`.
59
+ 1. Set up the rulebook: `npx rulebookguard@0.13.0 init clients --ci` writes `rulebookguard.config.yaml`, the `clients` context and a CI workflow (see [Configuration](#configuration) to adjust `testGlobs`).
60
+ 2. Scaffold a rule: `npx rulebookguard@0.13.0 new clients` creates `docs/rules/clients/R-CLI-001.md` as `proposed`.
61
61
  3. Write the rule, add its `anchors`, set `status: active`, and wrap the test that proves it with `@rule R-CLI-001` and `@endrule R-CLI-001`.
62
- 4. Run `npx rulebookguard@0.11.0 lint`, and add it to CI with the [CI recipe](#ci-recipe).
62
+ 4. Run `npx rulebookguard@0.13.0 lint`, and add it to CI with the [CI recipe](#ci-recipe).
63
63
 
64
64
  ## Configuration
65
65
 
@@ -164,6 +164,29 @@ Scenario: Duplicate document in the same country
164
164
 
165
165
  The Markdown body holds the full rule and its examples (Gherkin scenarios or tables). Rules do not list their tests: the link lives only in the tests, through `@rule` tags.
166
166
 
167
+ ### Evidence
168
+
169
+ `evidence` links a rule to the code and tests where it shows, without touching them. Each entry is a path, optionally followed by what to look for in that file:
170
+
171
+ | entry | resolves to |
172
+ | ------------------------------------------------------------ | ------------------------------------------------- |
173
+ | `src/clients/create.ts validateDocument` | the definition of `validateDocument` |
174
+ | `src/clients/service.ts Clients.create` | `create` inside `Clients` (`::` and `#` work too) |
175
+ | `test/clients/create.test.ts "rejects a duplicate document"` | the test with that name |
176
+ | `src/clients/create.ts` | the file |
177
+
178
+ Lines are found when the rule is read, so the link follows the code as it moves. Built in, a heuristic finds the line where a symbol is defined (function, method, class, constant, assignment or key, in most languages) or the line holding a quoted string. With the optional [`@rulebookguard/symbols`](#exact-ranges-with-rulebookguardsymbols) package installed, files in the languages it knows resolve to the full range of the definition or of the test. `lint` warns with `dead-evidence` when the file is gone or the symbol is not found, so a rename shows up instead of leaving a stale link. Deprecated rules are not checked. An older `path:line` or `path:line-endLine` entry still works, and only its file is checked.
179
+
180
+ #### Exact ranges with `@rulebookguard/symbols`
181
+
182
+ The heuristic gives one line. For the full range of a function, class, method or test, install the optional package, which reads the code with [tree-sitter](https://tree-sitter.github.io/):
183
+
184
+ ```sh
185
+ npm install --save-dev rulebookguard @rulebookguard/symbols
186
+ ```
187
+
188
+ It knows JavaScript, TypeScript (and TSX), Python, Go, Java, C# and Rust; files in other languages keep using the heuristic, and so does any file it fails to read. It is a separate package because its grammars add about 11 MB; they ship inside it, so installing it builds nothing. Without it, nothing fails: lines come from the heuristic.
189
+
167
190
  ### Linking tests with `@rule` … `@endrule`
168
191
 
169
192
  A test links to a rule with a region: `@rule <ID>` opens it and `@endrule <ID>` closes it, in a file matched by `testGlobs`. Both lines belong to the region. Regions may nest (a suite tagged with one rule holding tests tagged with others) as long as they close in order. One line can carry several markers.
@@ -250,6 +273,7 @@ func validateDocument(country, number string) error {
250
273
  | [`export gherkin`](#export-gherkin---out-dir) | writes the rules' scenarios as Cucumber `.feature` files |
251
274
  | [`export cursor\|copilot\|kiro`](#export-cursorcopilotkiro---out-dir) | writes the active rules as rule files of other AI tools |
252
275
  | [`export agents-md`](#export-agents-md---out-dir) | keeps an index of the active rules in `AGENTS.md` |
276
+ | [`export kiro-agents`](#export-kiro-agents---out-dir) | writes the `rule-author` and `rule-miner` agents for Kiro |
253
277
  | [`hook`](#hook) | Claude Code hook that injects the rules of a file |
254
278
  | [`mcp`](#mcp---project-dir) | MCP server for AI agents |
255
279
  | [`site:generate`](#sitegenerate---out-dir---base-path) | builds a static site of the rulebook |
@@ -307,7 +331,7 @@ file: docs/rules/clients/R-CLI-001.md
307
331
  ...
308
332
  ```
309
333
 
310
- `supersedes`, `dependsOn`, `conflictsWith`, `origin` and `evidence` lines appear only when the rule sets them; `--json` always has `supersedes` and `origin` (or `null`), and `dependsOn`, `conflictsWith` and `evidence` (or `[]`). An `implementations:` section lists the [`@implements` regions](#marking-the-code-that-enforces-a-rule-with-implements--endimplements) of the rule as `file:line-endLine`, read from the files its anchors match; `--json` has them as `implementations: [{ file, line, endLine }]` (or `[]`). Exits 1 for an unknown id.
334
+ `supersedes`, `dependsOn`, `conflictsWith`, `origin` and `evidence` lines appear only when the rule sets them; `--json` always has `supersedes` and `origin` (or `null`), and `dependsOn`, `conflictsWith` and `evidence` (or `[]`). Each [evidence](#evidence) entry with a symbol is followed by the lines it resolves to, as `(lines a-b)`; `--json` keeps `evidence` as written and adds `evidenceLinks: [{ entry, file, line, endLine }]`, with `null` lines for an entry that does not resolve (or `[]`). An `implementations:` section lists the [`@implements` regions](#marking-the-code-that-enforces-a-rule-with-implements--endimplements) of the rule as `file:line-endLine`, read from the files its anchors match; `--json` has them as `implementations: [{ file, line, endLine }]` (or `[]`). Exits 1 for an unknown id.
311
335
 
312
336
  ### `search <text> [--context <c>] [--json]`
313
337
 
@@ -329,7 +353,7 @@ $ rulebookguard for src/clients/create.ts
329
353
  R-CLI-001 (clients): A client document number is unique per country. (lines 12-30)
330
354
  ```
331
355
 
332
- When the file holds `@implements` regions of a rule, its line ends with their line ranges; `--json` rows carry them as `regions: [{ line, endLine }]` (or `[]`). Exits 1 when the path is outside the rulebook root.
356
+ When the file holds `@implements` regions of a rule, or lines its [evidence](#evidence) resolves to, its line ends with those line ranges; `--json` rows carry them apart, as `regions: [{ line, endLine }]` and `evidence: [{ entry, line, endLine }]` (each `[]` when there are none). Exits 1 when the path is outside the rulebook root.
333
357
 
334
358
  ### `for --changed <ref>`
335
359
 
@@ -412,6 +436,7 @@ Checks the whole catalog. Each finding is printed as `<severity> <code> <file>[:
412
436
  | error | `dependency-cycle` | `dependsOn` links form a cycle (reported once, at its lowest id) |
413
437
  | error | `active-rule-conflict` | two active rules where one lists the other in `conflictsWith` |
414
438
  | warning | `context-over-budget` | a context holds more rules than `contextBudget` |
439
+ | warning | `dead-evidence` | an `evidence` entry names a file or a symbol that no longer exists |
415
440
 
416
441
  ```sh
417
442
  $ rulebookguard lint
@@ -488,6 +513,10 @@ Run it again after changing rules, for example in CI or before committing: it ov
488
513
 
489
514
  Keeps an index of the active rules in `AGENTS.md`, at the rulebook root or in `--out`. `AGENTS.md` has no path scoping and is always read, so it holds each rule's id, anchors (or `no anchors`), summary and rule file, not the full text. The index lives between `<!-- rulebookguard:start -->` and `<!-- rulebookguard:end -->`: the rest of the file is kept, the block is appended the first time and replaced after that, and it is removed when no rule is active, along with the file if nothing else is left (printing `removed AGENTS.md`). Anything other than one start marker followed by one end marker, or neither, fails with exit code 1 and leaves the file untouched.
490
515
 
516
+ ### `export kiro-agents [--out <dir>]`
517
+
518
+ Writes `rule-author` and `rule-miner` as [Kiro custom agents](https://kiro.dev/docs/custom-agents/) in `.kiro/agents/rulebookguard-<name>.md`, under the rulebook root or directly in `--out`. Each file carries its own MCP server, pinned to the version of the CLI that wrote it, and the rule writing guide, so it works without the [Kiro power](#kiro). Run it again after upgrading. Other files in the directory are left alone, and if `rulebookguard-rule-author.md` or `rulebookguard-rule-miner.md` lacks the generated line, the command fails with exit code 1 before writing anything.
519
+
491
520
  ### `hook`
492
521
 
493
522
  A Claude Code hook for `PreToolUse` and `PostToolUse` events of tools that take a `file_path` (the [Claude Code plugin](#claude-code-plugin) wires it up). It reads the hook event as JSON on stdin, takes `tool_input.file_path` (relative paths resolve against the event's `cwd`), finds `rulebookguard.config.yaml` by walking up from that file, and prints the active rules anchored to it as additional context for Claude, under the same `hookEventName`:
@@ -499,7 +528,7 @@ $ echo '{"hook_event_name":"PostToolUse","tool_name":"Read","tool_input":{"file_
499
528
 
500
529
  Claude Code adds the context next to the tool result, so a `PostToolUse` hook on `Read` delivers the rules together with the file contents, before Claude writes a change (Claude Code requires reading a file before editing it).
501
530
 
502
- Each rule comes with its id, context, summary, rule file, the lines of its `@implements` regions in that file (when there are any) and full text; when the texts add up to more than 10,000 characters only the summaries are listed, with a pointer to `rulebookguard show <id>`. The header follows `language` in the config.
531
+ Each rule comes with its id, context, summary, rule file, the lines of its `@implements` regions and of its evidence in that file (when there are any) and full text; when the texts add up to more than 10,000 characters only the summaries are listed, with a pointer to `rulebookguard show <id>`. The header follows `language` in the config.
503
532
 
504
533
  > [!NOTE]
505
534
  > The hook never blocks a tool and never answers the permission prompt: with no config, a path outside the rulebook root, no active rule, another event, unreadable input or any error, it prints nothing and exits 0.
@@ -528,7 +557,7 @@ The rulebook is found from `--project` (default: the current directory). A comma
528
557
  > npm install --save-dev rulebookguard @rulebookguard/site
529
558
  > ```
530
559
 
531
- Without it, the site commands fail, exit 1, with `The site commands need the @rulebookguard/site package. Install it next to rulebookguard: npm install --save-dev @rulebookguard/site`. One-off, without installing: `npx -p rulebookguard@0.11.0 -p @rulebookguard/site@0.11.0 rulebookguard site:run`.
560
+ Without it, the site commands fail, exit 1, with `The site commands need the @rulebookguard/site package. Install it next to rulebookguard: npm install --save-dev @rulebookguard/site`. One-off, without installing: `npx -p rulebookguard@0.13.0 -p @rulebookguard/site@0.13.0 rulebookguard site:run`.
532
561
 
533
562
  Builds a read-only static site of the rulebook with [Astro Starlight](https://starlight.astro.build) into `--out` (default `site`, relative to the current directory) and prints that path. Open it from disk or publish it as is, for example on GitHub Pages; pass `--base` when it is served from a sub-path (`--base /rules`).
534
563
 
@@ -554,7 +583,7 @@ The site has:
554
583
  - **Home:** the contexts map, with how many rules of each status every context holds.
555
584
  - **A page per context** with its description and rules.
556
585
  - **All rules:** one table filterable by text, context and status.
557
- - **A page per rule:** summary, status, entities, rule file, the supersedes chain both ways, the origin (an `extracted` rule still `proposed` is flagged as not validated by the business yet) and evidence, the rule text, its anchors and the files they match, the tests tagging it, and the git history of the rule file.
586
+ - **A page per rule:** summary, status, entities, rule file, the supersedes chain both ways, the origin (an `extracted` rule still `proposed` is flagged as not validated by the business yet) and evidence, each entry with the `file:line-endLine` it resolves to, the rule text, its anchors and the files they match, the tests tagging it, and the git history of the rule file.
558
587
  - **Health:** the `lint` findings when the site was built.
559
588
  - Full-text search (Pagefind).
560
589
 
@@ -581,7 +610,7 @@ jobs:
581
610
  - uses: actions/setup-node@v6
582
611
  with:
583
612
  node-version: 22
584
- - run: npx --yes rulebookguard@0.11.0 lint --base origin/${{ github.base_ref }}
613
+ - run: npx --yes rulebookguard@0.13.0 lint --base origin/${{ github.base_ref }}
585
614
  ```
586
615
 
587
616
  > [!TIP]
@@ -593,9 +622,9 @@ The plugin in [`plugin/`](plugin) connects the rulebook to Claude Code:
593
622
 
594
623
  - **Hooks:** after every `Read` and before every `Write`, it runs `rulebookguard hook`, so Claude gets the active rules anchored to a file together with its contents, before it writes a change. A `Write` of a new file, which needs no `Read`, gets them right after the file is created.
595
624
  - **MCP server:** the tools of `rulebookguard mcp` (`rules_for`, `rules_show`, `contexts`, `rules_list`, `rules_search`, `lint`), for rules of files Claude has not opened yet and for working on the rulebook itself.
596
- - **Skill** `rulebookguard:rules`: the protocol, that is, follow the rules, and change a rule (then its tests, then the code, marked with `@implements`) only when the user asks for the behavior change.
625
+ - **Skill** `rulebookguard:rules`: the protocol, that is, follow the rules, and change a rule (then its tests, then the code, inside its `@implements` regions when it has any; new regions only on request) only when the user asks for the behavior change.
597
626
  - **Skill** `rulebookguard:rule-writing`: the shared writing guide, so a rule reads the same whoever writes it: one verifiable behavior, a one-line summary in business language, Gherkin examples, and `origin` and `evidence` for rules lifted from code.
598
- - **Agent** `rulebookguard:rule-miner`: lifts the business rules existing code already enforces, one context at a time. It writes them as `proposed` rules with `origin: extracted`, evidence as `path symbol` and questions for the business, through `rulebookguard new`, and marks the code that enforces each rule with `@implements` … `@endimplements`. It checks existing rules first to avoid duplicates, stays within `contextBudget`, wraps tests that already verify a rule in `@rule` … `@endrule`, and finishes only when `lint` has no errors of its own. It never activates a rule or edits existing rules, and adds only marker comments to code: a person reviews the diff and activates what holds. Ask for it by name, for example "use rule-miner on the clients context".
627
+ - **Agent** `rulebookguard:rule-miner`: lifts the business rules existing code already enforces, one context at a time. It writes them as `proposed` rules with `origin: extracted`, evidence as `path symbol` and questions for the business, through `rulebookguard new`. It traces every flow of the context from its entry points down to persistence before writing, and its report starts with the flows it traced. It does not edit production code: it suggests the lines that enforce each rule, and adds `@implements` … `@endimplements` markers only when asked. It checks existing rules first to avoid duplicates, stays within `contextBudget`, wraps tests that already verify a rule in `@rule` … `@endrule`, and finishes only when `lint` has no errors of its own. It never activates a rule or edits existing rules, and touches code only to add marker comments when asked: a person reviews the diff and activates what holds. Ask for it by name, for example "use rule-miner on the clients context".
599
628
  - **Agent** `rulebookguard:rule-author`: designs the rules a feature needs before its code. By default it writes nothing: it reads the rulebook by levels and returns a rule change plan with new rules and their Gherkin, changed rules as diffs, deprecations, conflicts with active rules, test skeletons and open questions, then stops for a person to approve. Called again with the approved plan, it writes the new rules as `proposed` through `rulebookguard new`, applies the approved diffs and finishes when `lint` has no errors of its own. Tests and code come after, as the `rules` skill says. Ask for it by name, for example "use rule-author for this feature: …".
600
629
 
601
630
  Install it from this repository's marketplace:
@@ -608,3 +637,20 @@ claude plugin install rulebookguard@rulebookguard
608
637
  Or, inside Claude Code 2.1.275 or later: `/plugin install rulebookguard --marketplace JuanCJR/rulebookguard`.
609
638
 
610
639
  The plugin runs the project's own rulebookguard (`node_modules/rulebookguard`, in the project directory or a parent) when its version is at least the plugin's; otherwise it runs `npx rulebookguard@<plugin version>`, which needs Node.js and network access the first time. In a checkout of rulebookguard itself it runs the checkout's own built CLI (`dist/cli.mjs`, from `vp pack`), whatever its version. The hooks never block a tool: any failure, including a timeout, lets it go ahead without rules.
640
+
641
+ ## Kiro
642
+
643
+ Kiro gets the same rules through three pieces:
644
+
645
+ | Piece | What it gives Kiro | How to get it |
646
+ | ----------------------------------------------------- | -------------------------------------------------------- | ---------------------------------- |
647
+ | [Steering files](#export-cursorcopilotkiro---out-dir) | the active rules of the files it works on | `rulebookguard export kiro` |
648
+ | Power in [`plugin/`](plugin) | the `rules` and `rule-writing` skills and the MCP server | import the `plugin` folder |
649
+ | [Custom agents](#export-kiro-agents---out-dir) | `rule-author` and `rule-miner` | `rulebookguard export kiro-agents` |
650
+
651
+ To install the power, open the Powers panel in Kiro, choose **Add Custom Power**, then **Import power from a folder** with a clone of this repository's `plugin` folder. Its MCP server runs `npx --yes rulebookguard@0.13.0 mcp` from the directory Kiro starts it in, and needs Node.js and network access the first time.
652
+
653
+ Keep `rulebookguard.config.yaml` at the root of the Kiro workspace: Kiro reads `.kiro/` from there, and the steering patterns are relative to the rulebook root. There is no hook for Kiro, because steering files already deliver the rules of a matching file.
654
+
655
+ > [!NOTE]
656
+ > Kiro limits what these pieces reach. Kiro CLI V1 and V2 do not load `fileMatch` steering files automatically, so `rulebookguard export agents-md` is the way to reach them, and powers are listed for CLI V3. A custom agent does not read steering files unless its `resources` include `file://.kiro/steering/**/*.md`.
@@ -1,4 +1,4 @@
1
- import { S as toRepoPath, y as createGlobMatcher } from "./lint-BPdk6XxH.mjs";
1
+ import { S as createGlobMatcher, T as toRepoPath } from "./lint-BdC2U8K3.mjs";
2
2
  // src/catalog/anchors.ts
3
3
  function rulesForPath(rules, path) {
4
4
  const target = toRepoPath(path);
package/dist/cli.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
- import { B as FormatError, I as CONFIG_FILE_NAME, M as CONTEXT_FILE_NAME, S as toRepoPath, V as formatIssue, _ as loadCatalog, b as expandGlobs, c as gitFallbackWarning, d as writeJson, f as writeLines, g as findRulebookRoot, h as scanTestTags, i as lintCommand, k as isRuleId, l as openCatalog, o as CliError, p as scanImplementations, s as findRule, t as changedFiles, u as shownPath, z as messages } from "./lint-BPdk6XxH.mjs";
3
- import { t as rulesForPath } from "./anchors-jf2bKfyg.mjs";
2
+ import { C as expandGlobs, F as CONTEXT_FILE_NAME, H as messages, M as isRuleId, R as splitFrontmatter, T as toRepoPath, U as FormatError, W as formatIssue, b as loadCatalog, c as gitFallbackWarning, d as writeJson, f as writeLines, g as scanImplementations, h as parseEvidence, i as lintCommand, l as openCatalog, o as CliError, p as createEvidenceResolver, s as findRule, t as changedFiles, u as shownPath, v as scanTestTags, y as findRulebookRoot, z as CONFIG_FILE_NAME } from "./lint-BdC2U8K3.mjs";
3
+ import { t as rulesForPath } from "./anchors-BGiDD0zY.mjs";
4
4
  import { access, mkdir, readFile, readdir, rm, writeFile } from "node:fs/promises";
5
5
  import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
6
6
  import { parseArgs } from "node:util";
@@ -85,7 +85,7 @@ async function handWrittenFiles(dir, format, names) {
85
85
  }
86
86
  async function staleRuleFiles(dir, format, target, current) {
87
87
  const stale = [];
88
- for (const name of await listNames(dir)) if (!current.has(name) && name.startsWith(RULE_FILE_PREFIX) && name.endsWith(target.extension) && isRuleId(name.slice(14, -target.extension.length)) && hasGeneratedLine(await readFile(join(dir, name), "utf8"), format)) stale.push(name);
88
+ for (const name of await listNames(dir)) if (!current.has(name) && name.startsWith("rulebookguard-") && name.endsWith(target.extension) && isRuleId(name.slice(14, -target.extension.length)) && hasGeneratedLine(await readFile(join(dir, name), "utf8"), format)) stale.push(name);
89
89
  return stale.sort();
90
90
  }
91
91
  function hasGeneratedLine(content, format) {
@@ -172,6 +172,78 @@ function expandBraces(glob) {
172
172
  function normalizedBody(rule) {
173
173
  return rule.body.replaceAll("\r\n", "\n").trim();
174
174
  }
175
+ // src/commands/export-kiro-agents.ts
176
+ const KIRO_AGENTS_FORMAT = "kiro-agents";
177
+ const AGENTS_DIR = ".kiro/agents";
178
+ const AGENT_NAMES = ["rule-author", "rule-miner"];
179
+ const WRITING_GUIDE = "skills/rule-writing/SKILL.md";
180
+ const KIRO_TOOLS = [
181
+ "read",
182
+ "write",
183
+ "shell",
184
+ "@rulebookguard"
185
+ ];
186
+ const PRELOADED_GUIDE = "the preloaded `rulebookguard:rule-writing` skill says";
187
+ const INLINE_GUIDE = "the writing guide at the end of these instructions says";
188
+ const LAUNCHER_NEW = "`rulebookguard.mjs new <context>` (on PATH while the plugin is enabled; if the command is not found, `npx rulebookguard new <context>`)";
189
+ async function exportKiroAgents(catalog, io, out) {
190
+ const text = messages(catalog.config.language);
191
+ const dir = out === void 0 ? join(catalog.root, AGENTS_DIR) : resolve(io.cwd, out);
192
+ const fileName = (name) => `${RULE_FILE_PREFIX}${name}.md`;
193
+ const handWritten = await handWrittenFiles(dir, KIRO_AGENTS_FORMAT, AGENT_NAMES.map(fileName));
194
+ if (handWritten.length > 0) throw new CliError(handWritten.map((name) => text.handWrittenExport(toRepoPath(shownPath(io.cwd, join(dir, name))), KIRO_AGENTS_FORMAT)).join("\n"));
195
+ const { version } = JSON.parse(await readPluginFile("plugin.json"));
196
+ const guide = bodyOf(await readPluginFile(WRITING_GUIDE), WRITING_GUIDE);
197
+ const lines = [];
198
+ await mkdir(dir, { recursive: true });
199
+ for (const name of AGENT_NAMES) {
200
+ const file = join(dir, fileName(name));
201
+ await writeFile(file, await kiroAgent(name, guide, version, text));
202
+ lines.push(toRepoPath(shownPath(io.cwd, file)));
203
+ }
204
+ writeLines(io, lines);
205
+ }
206
+ async function kiroAgent(name, guide, version, text) {
207
+ const source = `agents/${name}.md`;
208
+ const { data, body } = splitFrontmatter(await readPluginFile(source), source);
209
+ const { description } = data;
210
+ const instructions = normalized(body).replaceAll(PRELOADED_GUIDE, INLINE_GUIDE).replaceAll(LAUNCHER_NEW, `\`npx --yes rulebookguard@${version} new <context>\``);
211
+ return [
212
+ "---",
213
+ `name: ${name}`,
214
+ `description: ${JSON.stringify(description)}`,
215
+ `tools: ${JSON.stringify(KIRO_TOOLS)}`,
216
+ "mcpServers:",
217
+ " rulebookguard:",
218
+ " command: npx",
219
+ ` args: ${JSON.stringify([
220
+ "--yes",
221
+ `rulebookguard@${version}`,
222
+ "mcp"
223
+ ])}`,
224
+ "---",
225
+ "",
226
+ `<!-- ${text.generatedKiroAgent(KIRO_AGENTS_FORMAT, version)} -->`,
227
+ "",
228
+ instructions,
229
+ "",
230
+ `${guide}\n`
231
+ ].join("\n");
232
+ }
233
+ function bodyOf(source, file) {
234
+ return normalized(splitFrontmatter(source, file).body);
235
+ }
236
+ function normalized(text) {
237
+ return text.replaceAll("\r\n", "\n").trim();
238
+ }
239
+ async function readPluginFile(path) {
240
+ try {
241
+ return await readFile(new URL(`../plugin/${path}`, import.meta.url), "utf8");
242
+ } catch (error) {
243
+ if (error.code !== "ENOENT") throw error;
244
+ return readFile(new URL(`../../plugin/${path}`, import.meta.url), "utf8");
245
+ }
246
+ }
175
247
  // src/commands/export.ts
176
248
  const EXPORT_USAGE = "rulebookguard export <format> [--out <dir>]";
177
249
  const GHERKIN_BLOCK = /^```gherkin[^\n]*\n([\s\S]*?)^```/gm;
@@ -181,8 +253,16 @@ async function exportCommand(catalog, io, format, out) {
181
253
  await exportAgentRules(catalog, io, format, out);
182
254
  return;
183
255
  }
256
+ if (format === "kiro-agents") {
257
+ await exportKiroAgents(catalog, io, out);
258
+ return;
259
+ }
184
260
  if (format !== "gherkin") {
185
- const supported = ["gherkin", ...AGENT_FORMATS].join(", ");
261
+ const supported = [
262
+ "gherkin",
263
+ ...AGENT_FORMATS,
264
+ KIRO_AGENTS_FORMAT
265
+ ].join(", ");
186
266
  throw new CliError(`Unknown export format "${format}"; supported: ${supported}.\nUsage: ${EXPORT_USAGE}`, 2);
187
267
  }
188
268
  const language = catalog.config.language ?? "en";
@@ -278,6 +358,7 @@ async function showCommand(catalog, io, id, json) {
278
358
  const rule = findRule(catalog, id);
279
359
  const files = await expandGlobs(catalog.root, rule.anchors, gitFallbackWarning(io, catalog.config.language));
280
360
  const implementations = (await scanImplementations(catalog.root, files)).regions.get(rule.id) ?? [];
361
+ const links = await resolveEvidence(catalog, rule.evidence ?? []);
281
362
  if (json) {
282
363
  writeJson(io, { rule: {
283
364
  ...toSummary(rule),
@@ -288,13 +369,20 @@ async function showCommand(catalog, io, id, json) {
288
369
  conflictsWith: rule.conflictsWith ?? [],
289
370
  origin: rule.origin ?? null,
290
371
  evidence: rule.evidence ?? [],
372
+ evidenceLinks: links.map(({ entry, file, line, endLine }) => ({
373
+ entry,
374
+ file,
375
+ line,
376
+ endLine
377
+ })),
291
378
  implementations,
292
379
  file: rule.file,
293
380
  body: rule.body
294
381
  } });
295
382
  return;
296
383
  }
297
- const none = messages(catalog.config.language).none;
384
+ const text = messages(catalog.config.language);
385
+ const none = text.none;
298
386
  const header = [
299
387
  `${rule.id}: ${rule.summary}`,
300
388
  `context: ${rule.context}`,
@@ -306,7 +394,7 @@ async function showCommand(catalog, io, id, json) {
306
394
  ...rule.dependsOn?.length ? [`dependsOn: ${rule.dependsOn.join(", ")}`] : [],
307
395
  ...rule.conflictsWith?.length ? [`conflictsWith: ${rule.conflictsWith.join(", ")}`] : [],
308
396
  ...rule.origin === void 0 ? [] : [`origin: ${rule.origin}`],
309
- ...rule.evidence?.length ? ["evidence:", ...indent(rule.evidence, none)] : [],
397
+ ...links.length ? ["evidence:", ...indent(links.map((link) => withLines(link, text.lines)), none)] : [],
310
398
  ...implementations.length ? ["implementations:", ...indent(implementations.map((region) => `${region.file}:${region.line}-${region.endLine}`), none)] : [],
311
399
  `file: ${rule.file}`
312
400
  ];
@@ -316,10 +404,12 @@ async function forCommand(catalog, io, input, json) {
316
404
  const text = messages(catalog.config.language);
317
405
  const path = toRootPath(catalog, resolve(io.cwd, input), input);
318
406
  const regions = await regionsIn(catalog.root, path);
407
+ const evidence = await evidenceIn(catalog, path);
319
408
  const rules = rulesForPath(catalog.rules, path).filter((rule) => rule.status === "active").map((rule) => ({
320
409
  ...toSummary(rule),
321
410
  file: rule.file,
322
- regions: regions(rule.id)
411
+ regions: regions(rule.id),
412
+ evidence: evidence(rule.id)
323
413
  }));
324
414
  if (json) {
325
415
  writeJson(io, {
@@ -332,7 +422,7 @@ async function forCommand(catalog, io, input, json) {
332
422
  writeLines(io, [text.noActiveRulesFor(path)]);
333
423
  return;
334
424
  }
335
- writeLines(io, rules.map((rule) => `${rule.id} (${rule.context}): ${rule.summary}${rule.regions.length === 0 ? "" : ` (${text.lines(formatSpans(rule.regions))})`}`));
425
+ writeLines(io, rules.map((rule) => `${rule.id} (${rule.context}): ${rule.summary}${linesNote(mergeSpans(rule.regions, rule.evidence), text.lines)}`));
336
426
  }
337
427
  async function forChangedCommand(catalog, io, base, json) {
338
428
  const text = messages(catalog.config.language);
@@ -368,6 +458,54 @@ async function regionsIn(root, path) {
368
458
  endLine
369
459
  }));
370
460
  }
461
+ async function resolveEvidence(catalog, entries) {
462
+ if (entries.length === 0) return [];
463
+ const resolveEntry = await createEvidenceResolver(catalog.root);
464
+ return Promise.all(entries.map(resolveEntry));
465
+ }
466
+ /** The resolved evidence lines each rule has in `path`, by rule id. */
467
+ async function evidenceIn(catalog, path) {
468
+ const found = /* @__PURE__ */ new Map();
469
+ const inPath = catalog.rules.map((rule) => ({
470
+ id: rule.id,
471
+ entries: (rule.evidence ?? []).filter((entry) => parseEvidence(entry).file === path)
472
+ })).filter(({ entries }) => entries.length > 0);
473
+ if (inPath.length === 0) return () => [];
474
+ const resolveEntry = await createEvidenceResolver(catalog.root);
475
+ for (const { id, entries } of inPath) {
476
+ const spans = [];
477
+ for (const { entry, line, endLine } of await Promise.all(entries.map(resolveEntry))) if (line !== null && endLine !== null) spans.push({
478
+ entry,
479
+ line,
480
+ endLine
481
+ });
482
+ if (spans.length > 0) found.set(id, spans);
483
+ }
484
+ return (id) => found.get(id) ?? [];
485
+ }
486
+ /** Region and evidence lines together, in order and without repeats. */
487
+ function mergeSpans(...groups) {
488
+ const seen = /* @__PURE__ */ new Set();
489
+ return groups.flat().map(({ line, endLine }) => ({
490
+ line,
491
+ endLine
492
+ })).sort((a, b) => a.line - b.line || a.endLine - b.endLine).filter(({ line, endLine }) => {
493
+ const key = `${line}-${endLine}`;
494
+ if (seen.has(key)) return false;
495
+ seen.add(key);
496
+ return true;
497
+ });
498
+ }
499
+ function linesNote(spans, lines) {
500
+ return spans.length === 0 ? "" : ` (${lines(formatSpans(spans))})`;
501
+ }
502
+ function withLines(link, lines) {
503
+ if (link.line === null || link.endLine === null || parseEvidence(link.entry).lines !== void 0) return link.entry;
504
+ return `${link.entry}${linesNote([{
505
+ line: link.line,
506
+ endLine: link.endLine
507
+ }], lines)}`;
508
+ }
371
509
  function formatSpans(spans) {
372
510
  return spans.map(({ line, endLine }) => `${line}-${endLine}`).join(", ");
373
511
  }
@@ -452,7 +590,9 @@ async function hookContext(event, cwd) {
452
590
  const repoPath = path.split(sep).join("/");
453
591
  const rules = rulesForPath(catalog.rules, repoPath).filter((rule) => rule.status === "active");
454
592
  if (rules.length === 0) return;
455
- return render(repoPath, rules, await regionsIn(root, repoPath).catch(() => () => []), TEXT[catalog.config.language ?? "en"]);
593
+ const regions = await regionsIn(root, repoPath).catch(() => () => []);
594
+ const evidence = await evidenceIn(catalog, repoPath).catch(() => () => []);
595
+ return render(repoPath, rules, (id) => mergeSpans(regions(id), evidence(id)), TEXT[catalog.config.language ?? "en"]);
456
596
  }
457
597
  function parseEvent(input) {
458
598
  let event;
@@ -495,7 +635,7 @@ function render(path, rules, regions, text) {
495
635
  return parts.join("\n\n");
496
636
  }
497
637
  // package.json
498
- var version = "0.11.0";
638
+ var version = "0.12.0";
499
639
  // src/commands/new.ts
500
640
  const MAX_NUMBER = 999;
501
641
  const CONTEXT_NAME = /^[a-z0-9][a-z0-9_-]*$/;
@@ -669,7 +809,7 @@ function createRulesServer(projectDir) {
669
809
  version
670
810
  });
671
811
  server.registerTool("rules_for", {
672
- description: "Active business rules anchored to a file (id, context, summary, rule file). Call it before changing a file; the rules are the source of truth for its behavior.",
812
+ description: "Active business rules anchored to a file (id, context, summary, rule file, and the lines of the file each rule names through @implements regions or evidence). Call it before changing a file; the rules are the source of truth for its behavior.",
673
813
  inputSchema: z.object({ path: z.string().describe("File path, absolute or relative to the project directory") }),
674
814
  annotations: { readOnlyHint: true }
675
815
  }, ({ path }) => cli(projectDir, [
@@ -678,7 +818,7 @@ function createRulesServer(projectDir) {
678
818
  "--json"
679
819
  ]));
680
820
  server.registerTool("rules_show", {
681
- description: "Full text of one business rule: summary, anchors, entities, supersedes and the rule body with its examples.",
821
+ description: "Full text of one business rule: summary, anchors, entities, supersedes, evidence with the lines each entry resolves to, and the rule body with its examples.",
682
822
  inputSchema: z.object({ id: z.string().describe("Rule id, for example R-CLI-001") }),
683
823
  annotations: { readOnlyHint: true }
684
824
  }, ({ id }) => cli(projectDir, [
@@ -791,6 +931,8 @@ Commands:
791
931
  active; --out: output directory
792
932
  export agents-md [--out <dir>] keep an index of the active rules in a marked block of
793
933
  AGENTS.md; --out: its directory (default: rulebook root)
934
+ export kiro-agents [--out <dir>] write the rule-author and rule-miner agents for Kiro in
935
+ .kiro/agents; --out: output directory
794
936
  hook Claude Code hook for PreToolUse and PostToolUse: reads
795
937
  the event on stdin and prints the rules anchored to
796
938
  the file of the tool (Read, Edit, Write...)
@@ -935,7 +1077,7 @@ async function dispatch(argv, io) {
935
1077
  return 0;
936
1078
  }
937
1079
  if (name === "site" || name === "site:generate" || name === "site:run") {
938
- const { siteCommand } = await import("./site-B7g8lbjx.mjs");
1080
+ const { siteCommand } = await import("./site-hSQxz6_e.mjs");
939
1081
  await siteCommand(io, {
940
1082
  out: values.out,
941
1083
  base: values.base,
package/dist/index.d.mts CHANGED
@@ -167,6 +167,8 @@ interface ShowJson {
167
167
  conflictsWith: string[];
168
168
  origin: RuleOrigin | null;
169
169
  evidence: string[];
170
+ /** Each evidence entry with the lines it resolves to, or null lines when it does not. */
171
+ evidenceLinks: EvidenceLinkJson[];
170
172
  implementations: TagRegion[];
171
173
  file: string;
172
174
  body: string;
@@ -177,8 +179,18 @@ interface ForJson {
177
179
  rules: (RuleSummaryJson & {
178
180
  file: string;
179
181
  regions: LineSpanJson[];
182
+ evidence: EvidenceSpanJson[];
180
183
  })[];
181
184
  }
185
+ interface EvidenceLinkJson {
186
+ entry: string;
187
+ file: string;
188
+ line: number | null;
189
+ endLine: number | null;
190
+ }
191
+ interface EvidenceSpanJson extends LineSpanJson {
192
+ entry: string;
193
+ }
182
194
  interface ForChangedJson {
183
195
  base: string;
184
196
  files: string[];
package/dist/index.mjs CHANGED
@@ -1,3 +1,3 @@
1
- import { A as loadRule, B as FormatError, D as findRuleRegions, E as RULE_TAG_PATTERN, F as splitFrontmatter, I as CONFIG_FILE_NAME, L as loadConfig, M as CONTEXT_FILE_NAME, N as loadContext, O as findRuleTags, P as parseContext, R as parseConfig, S as toRepoPath, T as RULE_MARKER_PATTERN, V as formatIssue, _ as loadCatalog, a as pendingImplementation, b as expandGlobs, g as findRulebookRoot, h as scanTestTags, j as parseRule, k as isRuleId, m as scanTestRegions, n as formatFinding, p as scanImplementations, r as lintCatalog, v as relativeToRoot, w as IMPLEMENTS_MARKER_PATTERN, x as listRepoFiles, y as createGlobMatcher } from "./lint-BPdk6XxH.mjs";
2
- import { t as rulesForPath } from "./anchors-jf2bKfyg.mjs";
1
+ import { A as findRuleRegions, B as loadConfig, C as expandGlobs, D as IMPLEMENTS_MARKER_PATTERN, F as CONTEXT_FILE_NAME, I as loadContext, L as parseContext, M as isRuleId, N as loadRule, O as RULE_MARKER_PATTERN, P as parseRule, R as splitFrontmatter, S as createGlobMatcher, T as toRepoPath, U as FormatError, V as parseConfig, W as formatIssue, _ as scanTestRegions, a as pendingImplementation, b as loadCatalog, g as scanImplementations, j as findRuleTags, k as RULE_TAG_PATTERN, n as formatFinding, r as lintCatalog, v as scanTestTags, w as listRepoFiles, x as relativeToRoot, y as findRulebookRoot, z as CONFIG_FILE_NAME } from "./lint-BdC2U8K3.mjs";
2
+ import { t as rulesForPath } from "./anchors-BGiDD0zY.mjs";
3
3
  export { CONFIG_FILE_NAME, CONTEXT_FILE_NAME, FormatError, IMPLEMENTS_MARKER_PATTERN, RULE_MARKER_PATTERN, RULE_TAG_PATTERN, createGlobMatcher, expandGlobs, findRuleRegions, findRuleTags, findRulebookRoot, formatFinding, formatIssue, isRuleId, lintCatalog, listRepoFiles, loadCatalog, loadConfig, loadContext, loadRule, parseConfig, parseContext, parseRule, pendingImplementation, relativeToRoot, rulesForPath, scanImplementations, scanTestRegions, scanTestTags, splitFrontmatter, toRepoPath };