diff --git a/dokimi/changelog.d/20261023_150000_antonwolfy_nested-cursor-rules.md b/dokimi/changelog.d/20261023_150000_antonwolfy_nested-cursor-rules.md
new file mode 100644
index 00000000..d316eaf4
--- /dev/null
+++ b/dokimi/changelog.d/20261023_150000_antonwolfy_nested-cursor-rules.md
@@ -0,0 +1,8 @@
+### Fixed
+
+- **`repo inspect` lists a `.cursor/rules/*.mdc` below the root** — a rule
+ file such as `web/.cursor/rules/style.mdc` was not listed in
+ `instructions`. It is now a record with `directory` `web`, and with scope
+ and activation `unknown`: Cursor's rules docs cover only the root
+ `.cursor/rules` (and nested `AGENTS.md`), so whether Cursor reads it is not
+ known from the docs. Its note says so.
diff --git a/dokimi/docs/repo-evaluation-contract.md b/dokimi/docs/repo-evaluation-contract.md
index 00aec556..a5304e97 100644
--- a/dokimi/docs/repo-evaluation-contract.md
+++ b/dokimi/docs/repo-evaluation-contract.md
@@ -877,11 +877,12 @@ not measured effects.
The report keeps observations apart from findings:
- `instructions`: one record per instruction file and per harness rule it
- matches (a root `AGENTS.md`, a nested `CLAUDE.md`, `.cursor/rules/*.mdc`,
- `.github/instructions/*.instructions.md`, …), with its `scope`
- (`root`, `path-specific`, `mode-specific`, `local-private`, `legacy`,
- `capability`, `unknown`), `activation`, the `directory` it applies to and
- its size (for a link, the size of the file it names). This is what each
+ matches (a root `AGENTS.md`, a nested `CLAUDE.md`, `.cursor/rules/*.mdc` at
+ the root or in a subdirectory, `.github/instructions/*.instructions.md`, …),
+ with its `scope` (`root`, `path-specific`, `mode-specific`, `local-private`,
+ `legacy`, `capability`, `unknown`), `activation`, the `directory` it applies to
+ (for a record whose scope is `unknown`, the one it would apply to) and its size
+ (for a link, the size of the file it names). This is what each
harness documents; which files a given harness version loads is measured by
the reach probe (§7).
- `checks`: ways to build, test, lint and type-check, per project root (the
diff --git a/dokimi/dokimi/repos/environment/instructions.py b/dokimi/dokimi/repos/environment/instructions.py
index d645a7a4..ed1c56d3 100644
--- a/dokimi/dokimi/repos/environment/instructions.py
+++ b/dokimi/dokimi/repos/environment/instructions.py
@@ -74,6 +74,16 @@ def _parent(p: str) -> str:
return posixpath.dirname(p) or "."
+def _nested_cursor_rule(p: str) -> bool:
+ """A ``
/.cursor/rules/*.mdc`` below the root; one under the root ``.cursor/rules/`` is the root rule's."""
+ return not p.startswith(".cursor/rules/") and re.match(r"^.+/\.cursor/rules/.+\.mdc$", p) is not None
+
+
+def _cursor_dir(p: str) -> str:
+ """The directory a nested ``/.cursor/rules/*.mdc`` would apply to: ````, the closest one."""
+ return p.rsplit("/.cursor/rules/", 1)[0]
+
+
def _roo_mode(p: str) -> str | None:
if p.startswith(".roo/rules-"):
return p.split("/")[1].removeprefix("rules-")
@@ -104,6 +114,11 @@ def _roo_mode(p: str) -> str | None:
directory=_parent),
Rule(_under(".cursor/rules/", r"\.mdc$"), ("cursor",), "unknown", "unknown",
notes=("activation depends on the rule's frontmatter",)),
+ # Cursor's rules docs (cursor.com/docs/context/rules, read 2026-10-23) place rules in the root
+ # .cursor/rules only, and nested AGENTS.md: one below the root is listed, its scope not known.
+ Rule(_nested_cursor_rule, ("cursor",), "unknown", "unknown", directory=_cursor_dir,
+ notes=("below the root: Cursor documents only the root .cursor/rules, so whether it reads this "
+ "one is not known",)),
Rule(_named(".cursorrules"), ("cursor", "cline"), "legacy", "always", legacy=True),
Rule(_named("GEMINI.md"), ("gemini", "github-copilot"), "root", "always"),
Rule(_nested("GEMINI.md"), ("gemini",), "path-specific", "path-scoped", directory=_parent),
diff --git a/dokimi/dokimi/runner/manifest.py b/dokimi/dokimi/runner/manifest.py
index 3fb1207c..4a6cf9e6 100644
--- a/dokimi/dokimi/runner/manifest.py
+++ b/dokimi/dokimi/runner/manifest.py
@@ -19,7 +19,8 @@
from dokimi import __version__, process
-# Direct dependencies whose versions materially affect results.
+# Dependencies whose versions materially affect results. ``ragas`` comes with the ``ragas``
+# extra and ``anthropic`` with that extra's ``langchain-anthropic``; one not installed is None.
TRACKED_DEPS = (
"litellm", "openai", "anthropic", "ragas", "datasets",
"jsonschema", "numpy", "PyYAML", "httpx",
diff --git a/dokimi/scripts/ci_local.py b/dokimi/scripts/ci_local.py
index 5e12ac02..c0ea3b83 100755
--- a/dokimi/scripts/ci_local.py
+++ b/dokimi/scripts/ci_local.py
@@ -20,9 +20,9 @@
windows-smoke, conda-forge-deps) run in a throwaway venv on a copy of the
tracked tree instead, so the dev venv stays as CI's.
- Every step runs even when an earlier one fails, so one failure does not hide
- the next (CI stops a job's checks at its first failing step, except
- `static`, whose steps after the install all run under
- `if: ${{ !cancelled() }}`).
+ the next (CI stops a job's checks at its first failing step, except for
+ steps under `if: ${{ !cancelled() }}`: every step of `static` after the
+ install, and the deptry step of `typecheck`).
- A matrix runs one leg: the dev venv's Python, standing in for the coverage
leg too (`matrix.coverage` is true), so the changed-lines gate runs here. A
notice says so when that is not the Python CI's single-version jobs use.
diff --git a/dokimi/scripts/growth_baseline.json b/dokimi/scripts/growth_baseline.json
index 0c237b9f..8d585f33 100644
--- a/dokimi/scripts/growth_baseline.json
+++ b/dokimi/scripts/growth_baseline.json
@@ -11,7 +11,7 @@
"tests/test_judge_reliability.py": {"lines": 1695, "rationale": "grew on main in #73 (judge-validity --context stored), 1505 -> 1695, before the ratchet landed; split it in its own PR"},
"tests/test_judge_validity.py": {"lines": 2235, "rationale": "grew on main in #73 (judge-validity --context stored), 1423 -> 2235, before the ratchet landed; split it in its own PR"},
"tests/test_pipeline_question_set.py": {"lines": 1223},
- "tests/test_repo_environment.py": {"lines": 1373, "rationale": "grew on main in #147 before the ratchet landed; +2 for the instructions assert of two crash tests (#103 review); split it in its own PR"},
+ "tests/test_repo_environment.py": {"lines": 1337, "rationale": "grew on main in #147 before the ratchet landed; +2 for the instructions assert of two crash tests (#103 review); split it in its own PR"},
"tests/test_subjects.py": {"lines": 1064},
"tests/test_task_formats.py": {"lines": 1003}
},
diff --git a/dokimi/tests/test_repo_environment.py b/dokimi/tests/test_repo_environment.py
index 649080cd..3838f1ec 100644
--- a/dokimi/tests/test_repo_environment.py
+++ b/dokimi/tests/test_repo_environment.py
@@ -877,42 +877,6 @@ def test_autotools_names_the_makefile_that_declares_tests(tmp_path):
"tests/Makefile.am"]
-INSTRUCTION_TABLE = [
- (".claude/CLAUDE.md", ('claude-code',), "root", "always", False),
- (".claude/rules/a.md", ('claude-code',), "unknown", "unknown", False),
- (".claude/skills/x/SKILL.md", ('claude-code',), "capability", "on-demand", False),
- (".clinerules", ('cline',), "legacy", "always", True),
- (".clinerules/a.md", ('cline',), "root", "unknown", False),
- (".cursor/rules/a.mdc", ('cursor',), "unknown", "unknown", False),
- (".cursorrules", ('cursor', 'cline'), "legacy", "always", True),
- (".github/agents/a.agent.md", ('github-copilot',), "capability", "manual", False),
- (".github/copilot-instructions.md", ('github-copilot',), "root", "always", False),
- (".github/instructions/a.instructions.md", ('github-copilot',), "path-specific", "path-scoped", False),
- (".roo/rules-code/a.md", ('roo-code',), "mode-specific", "mode-scoped", False),
- (".roo/rules/a.md", ('roo-code',), "root", "always", False),
- (".roomodes", ('roo-code',), "mode-specific", "mode-scoped", False),
- (".roorules", ('roo-code',), "legacy", "always", True),
- (".roorules-code", ('roo-code',), "mode-specific", "mode-scoped", True),
- (".windsurf/rules/a.md", ('windsurf',), "unknown", "unknown", False),
- (".windsurfrules", ('windsurf', 'cline'), "legacy", "always", True),
- ("AGENTS.md", ('codex', 'github-copilot', 'cursor', 'windsurf', 'cline', 'generic'), "root", "always", False),
- ("AGENTS.override.md", ('codex',), "root", "always", False),
- ("CLAUDE.local.md", ('claude-code',), "local-private", "always", False),
- ("CLAUDE.md", ('claude-code', 'github-copilot'), "root", "always", False),
- ("GEMINI.md", ('gemini', 'github-copilot'), "root", "always", False),
- ("sub/AGENTS.md", ('codex', 'github-copilot', 'cursor', 'windsurf', 'cline', 'generic'), "path-specific", "path-scoped", False),
- ("sub/AGENTS.override.md", ('codex',), "path-specific", "path-scoped", False),
- ("sub/CLAUDE.md", ('claude-code',), "path-specific", "path-scoped", False),
- ("sub/GEMINI.md", ('gemini',), "path-specific", "path-scoped", False),
-]
-
-
-def test_instruction_rule_table():
- got = [(i.path, tuple(i.harnesses), i.scope, i.activation, i.legacy)
- for i in detect_instructions([row[0] for row in INSTRUCTION_TABLE])]
- assert got == INSTRUCTION_TABLE
-
-
@pytest.mark.parametrize("command, flagged", [
# wrappers with options that take a value, and a wrapper named by path
("sudo -u nobody npm ci", True), ("nice -n 10 npm ci", True), ("timeout -s KILL 30 npm ci", True),
diff --git a/dokimi/tests/test_repo_instructions.py b/dokimi/tests/test_repo_instructions.py
new file mode 100644
index 00000000..b4a0eb4e
--- /dev/null
+++ b/dokimi/tests/test_repo_instructions.py
@@ -0,0 +1,58 @@
+# Copyright (C) 2026 Intel Corporation
+# SPDX-License-Identifier: Apache-2.0
+
+"""The instruction-file rules of ``repo inspect``: which harness reads each file, and where it applies."""
+
+from __future__ import annotations
+
+from dokimi.repos.environment.instructions import detect_instructions
+
+INSTRUCTION_TABLE = [
+ (".claude/CLAUDE.md", ('claude-code',), "root", "always", False),
+ (".claude/rules/a.md", ('claude-code',), "unknown", "unknown", False),
+ (".claude/skills/x/SKILL.md", ('claude-code',), "capability", "on-demand", False),
+ (".clinerules", ('cline',), "legacy", "always", True),
+ (".clinerules/a.md", ('cline',), "root", "unknown", False),
+ (".cursor/rules/a.mdc", ('cursor',), "unknown", "unknown", False),
+ (".cursorrules", ('cursor', 'cline'), "legacy", "always", True),
+ (".github/agents/a.agent.md", ('github-copilot',), "capability", "manual", False),
+ (".github/copilot-instructions.md", ('github-copilot',), "root", "always", False),
+ (".github/instructions/a.instructions.md", ('github-copilot',), "path-specific", "path-scoped", False),
+ (".roo/rules-code/a.md", ('roo-code',), "mode-specific", "mode-scoped", False),
+ (".roo/rules/a.md", ('roo-code',), "root", "always", False),
+ (".roomodes", ('roo-code',), "mode-specific", "mode-scoped", False),
+ (".roorules", ('roo-code',), "legacy", "always", True),
+ (".roorules-code", ('roo-code',), "mode-specific", "mode-scoped", True),
+ (".windsurf/rules/a.md", ('windsurf',), "unknown", "unknown", False),
+ (".windsurfrules", ('windsurf', 'cline'), "legacy", "always", True),
+ ("AGENTS.md", ('codex', 'github-copilot', 'cursor', 'windsurf', 'cline', 'generic'), "root", "always", False),
+ ("AGENTS.override.md", ('codex',), "root", "always", False),
+ ("CLAUDE.local.md", ('claude-code',), "local-private", "always", False),
+ ("CLAUDE.md", ('claude-code', 'github-copilot'), "root", "always", False),
+ ("GEMINI.md", ('gemini', 'github-copilot'), "root", "always", False),
+ ("sub/.cursor/rules/a.mdc", ('cursor',), "unknown", "unknown", False),
+ ("sub/AGENTS.md", ('codex', 'github-copilot', 'cursor', 'windsurf', 'cline', 'generic'), "path-specific", "path-scoped", False),
+ ("sub/AGENTS.override.md", ('codex',), "path-specific", "path-scoped", False),
+ ("sub/CLAUDE.md", ('claude-code',), "path-specific", "path-scoped", False),
+ ("sub/GEMINI.md", ('gemini',), "path-specific", "path-scoped", False),
+]
+
+
+def test_instruction_rule_table():
+ got = [(i.path, tuple(i.harnesses), i.scope, i.activation, i.legacy)
+ for i in detect_instructions([row[0] for row in INSTRUCTION_TABLE])]
+ assert got == INSTRUCTION_TABLE
+
+
+def test_a_nested_cursor_rule_is_listed_with_its_directory_and_no_known_scope():
+ (rule,) = detect_instructions(["pkg/web/.cursor/rules/style.mdc", "pkg/web/.cursor/notes.md"])
+ assert (rule.path, rule.scope, rule.directory) == ("pkg/web/.cursor/rules/style.mdc", "unknown", "pkg/web")
+ assert any("Cursor documents only the root .cursor/rules" in note for note in rule.notes)
+
+
+def test_a_cursor_rules_path_gets_one_record_and_the_closest_directory():
+ paths = [".cursor/rules/sub/.cursor/rules/b.mdc", "a/.cursor/rules/b/.cursor/rules/c.mdc"]
+ got = sorted((i.path, i.directory) for i in detect_instructions(paths))
+ # under the root .cursor/rules: the root rule's only; nested twice: the inner .cursor's directory
+ assert got == [(".cursor/rules/sub/.cursor/rules/b.mdc", None),
+ ("a/.cursor/rules/b/.cursor/rules/c.mdc", "a/.cursor/rules/b")]