Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
### Fixed
Comment thread
antonwolfy marked this conversation as resolved.

- **`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.
11 changes: 6 additions & 5 deletions dokimi/docs/repo-evaluation-contract.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
15 changes: 15 additions & 0 deletions dokimi/dokimi/repos/environment/instructions.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,16 @@ def _parent(p: str) -> str:
return posixpath.dirname(p) or "."


def _nested_cursor_rule(p: str) -> bool:
"""A ``<dir>/.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 ``<dir>/.cursor/rules/*.mdc`` would apply to: ``<dir>``, 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-")
Expand Down Expand Up @@ -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),
Expand Down
3 changes: 2 additions & 1 deletion dokimi/dokimi/runner/manifest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
6 changes: 3 additions & 3 deletions dokimi/scripts/ci_local.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion dokimi/scripts/growth_baseline.json
Original file line number Diff line number Diff line change
Expand Up @@ -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}
},
Expand Down
36 changes: 0 additions & 36 deletions dokimi/tests/test_repo_environment.py
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
58 changes: 58 additions & 0 deletions dokimi/tests/test_repo_instructions.py
Original file line number Diff line number Diff line change
@@ -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")]
Loading