Skip to content

feat(repo): repo budget, what a repository puts into an agent's context - #71

Merged
napetrov merged 38 commits into
intel:mainfrom
napetrov:feat/repo-budget
Oct 20, 2026
Merged

napetrov merged 38 commits into
intel:mainfrom
napetrov:feat/repo-budget

Conversation

@napetrov

@napetrov napetrov commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor

Summary

New command: python cli.py repo budget <source>. It lists what the tree puts into an agent's context when the agent starts at the repository root, and when each part loads. It reads only the tree and calls no model.

when        what                                                 who pays
start       root CLAUDE.md / AGENTS.md + their @imports          every model call
            skill listing: name + description per .claude/skills/*/SKILL.md
lazy        nested guidance, and what a nested CLAUDE.md imports  calls after the agent reaches the subtree
on demand   skill bodies                                         calls after the skill is invoked
off         AGENTS.md when a CLAUDE.md is at the root            never, unless the agent opens the file
            and no CLAUDE.md imports it

It has two warnings, and --fail-on-warning turns them into a gate:

  • Skills whose descriptions are near-duplicates: word overlap of at least 0.5. The agent chooses from the listing alone.
  • AGENTS.md files that a CLAUDE.md turns off.

MCP servers in .mcp.json are listed by name only. Their tools are not counted, because the server reports them at start-up.

dokimi/repos/budget.py      budget(tree, guidance_names) -> entries, totals, duplicate_skills, mcp_servers, warnings
dokimi/commands/repo.py     `repo budget SOURCE [--harness] [--json] [--fail-on-warning]`

The unit is characters, not tokens. One AGENTS.md (4,826 chars) cost 1,510 tokens on Claude Haiku 4.5 and 2,104 on Sonnet 5.5, measured as the first-call difference with and without the file. The exact token cost is a two-run measurement; this is the static inventory that comes before it.

Evidence

  • Before: no command answered "what does this repo load, and when?" repo scan checks claims, repo reach spends a model call per scenario, and neither sizes anything.

  • After, run on real checkouts:

    oneDAL:    start 5,627 chars, lazy 31,965, on demand 0, off 0
    sklearnex: start 6,607 chars, lazy 25,201, on demand 0, off 0
    abicheck:  start 20,278 chars, lazy 166,872, on demand 29,566, off 157,568
    warning: CLAUDE.md at the root turns AGENTS.md loading off and no CLAUDE.md imports these,
             so Claude Code reads none of .github/AGENTS.md, abicheck/AGENTS.md, ... (13 files)
    

    abicheck's nested CLAUDE.md files link their sibling AGENTS.md with a Markdown link, not an @ import. Claude Code therefore loads none of those 13 files (157k chars) by itself. The rule that one CLAUDE.md turns AGENTS.md off for the whole session was measured with repo reach on Claude Code 2.1.286 and 2.1.296.

  • One case found during development: the first version ignored imports from nested CLAUDE.md files and reported their bridged AGENTS.md as off. test_a_nested_claude_md_bridge_loads_its_agents_md_lazily covers it.

  • tests/test_repo_budget.py (10 tests) covers:

    • start vs lazy loading;
    • off;
    • transitive and relative imports, and imports that point outside the tree;
    • skill listing vs body;
    • near-duplicate skills and skills without a description;
    • .mcp.json, including a broken one;
    • the table, JSON output, the gate, and a bad source.
  • make ci-local: 0 failed, 19 passed, 4 skipped. The skipped steps need docker or network.

Merge Danger

Door: two-way. This adds a subcommand and a module; nothing existing calls them.

Blast Radius: none

It writes no persisted format, only stdout. The start total is the characters the tree supplies at start, not an upper bound: it omits what it does not count (see the docs), and a harness may also show less. In a 30-skill fixture, the listing counted 6,109 chars. Haiku 4.5's first call grew by only 514 tokens, while Sonnet 5.5's grew by 2,151. That is consistent with the harness cutting the listing for the smaller model.

Checklist

  • Tests added/updated and pytest passes
  • ruff, mypy, and schema checks pass
  • Docs updated (or N/A) — see the docs-drift checklist in CONTRIBUTING.md
  • No ad-hoc run artifacts committed

🤖 Generated with Claude Code

A static inventory, in characters, of the context the tree gives an agent
that starts at its root: root guidance files and their @ imports and the
skill listing load at start, nested guidance lazily, skill bodies on demand,
and AGENTS.md files a root CLAUDE.md does not import are off. Warns on
near-duplicate skill descriptions and on AGENTS.md files turned off;
--fail-on-warning makes it a gate. No LLM call.

Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
@codecov-commenter

codecov-commenter commented Oct 16, 2026 •

Copy link
Copy Markdown

⚠️ Please install the 'codecov app svg image' to ensure uploads and comments are reliably processed by Codecov.

Codecov Report

❌ Patch coverage is 99.26471% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 91.21%. Comparing base (4a854c1) to head (31ff745).
⚠️ Report is 6 commits behind head on main.

Files with missing lines Patch % Lines
dokimi/dokimi/repos/source.py 84.61% 1 Missing and 1 partial ⚠️
❗ Your organization needs to install the Codecov GitHub app to enable full functionality.
Additional details and impacted files
@@            Coverage Diff             @@
##             main      #71      +/-   ##
==========================================
+ Coverage   90.70%   91.21%   +0.50%     
==========================================
  Files         211      213       +2     
  Lines       25732    26303     +571     
  Branches     4217     4287      +70     
==========================================
+ Hits        23339    23991     +652     
+ Misses       1713     1644      -69     
+ Partials      680      668      -12     
Files with missing lines Coverage Δ
dokimi/dokimi/commands/repo.py 94.38% <100.00%> (+0.73%) ⬆️
dokimi/dokimi/repos/budget.py 100.00% <100.00%> (ø)
dokimi/dokimi/repos/harness_profile.py 94.01% <100.00%> (+0.21%) ⬆️
dokimi/dokimi/repos/source.py 94.13% <84.61%> (-0.40%) ⬇️

... and 30 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

tree.is_file raises SourceError for a link that leaves the repository, and the
import loop did not catch it, so `repo budget` ended in a traceback with no
inventory. Treat such a path as not a file of the tree.

Signed-off-by: Alexander Rybkin <alexander.rybkin@intel.com>
An AGENTS.md linked to CLAUDE.md was listed `off` and raised the "Claude Code reads
none of AGENTS.md" warning, though the same bytes load through CLAUDE.md; an
@import of a link to a loaded file added its size a second time. Resolve links
and keep one entry per file, preferring the CLAUDE.md path.

Signed-off-by: Alexander Rybkin <alexander.rybkin@intel.com>
…ptions

The check keyed skills by name, so two directories declaring the same name
collapsed into one and an identical pair was never reported. Compare every skill;
the output keeps the name, and uses the path only when two names collide.

Signed-off-by: Alexander Rybkin <alexander.rybkin@intel.com>
…solute path

Claude Code ignores an @path inside a fenced block or a code span, and an absolute
path names the host filesystem, not a file of the tree. All three were listed as
`start` imports and added to the total.

Signed-off-by: Alexander Rybkin <alexander.rybkin@intel.com>
It is the documented alternative project memory file, but it was listed `lazy`,
its imports were lazy, and it did not turn a root AGENTS.md off.

Signed-off-by: Alexander Rybkin <alexander.rybkin@intel.com>
@xaleryb

xaleryb commented Oct 16, 2026

Copy link
Copy Markdown
Contributor

Ran repo budget at c0ffc02 on mattpocock/skills, oven-sh/bun and hostile fixtures. Output is deterministic (three runs and a different cwd give the same md5), no model is called, exit codes and docs flags match. Five defects, fixed in the pushed commits (each test red on the previous commit):

  1. be2471c: CLAUDE.md with @notes.md where notes.md is a symlink to a file outside the repo (or a symlinked .mcp.json) raised SourceError from tree.is_file: traceback, no inventory. Now skipped.
  2. aef82db: AGENTS.md symlinked to CLAUDE.md was listed off and warned "reads none of AGENTS.md", so --fail-on-warning exited 1 (mattpocock/skills off 3,957 to 0; bun 59,599 to 10,096, the rest being a real nested AGENTS.md). A linked file or import is now counted once.
  3. 864d67b: two skills sharing a name collapsed in the duplicate check, so an identical pair (overlap 1.0) was never reported.
  4. efd2999: @x.md in a fenced block or a span, and @/abs.md, were counted as start; Claude Code does not follow them.
  5. d1cb5ed: .claude/CLAUDE.md was lazy and did not turn a root AGENTS.md off; it is project memory read at start.

Still open, your call: a chain of 9 imports lists all 9 as start (Claude Code stops at 5 hops); .claude/rules/*.md without paths: are not counted, so start is an undercount; unreadable (non-UTF-8) imports count as 0 chars without a warning; the skill-listing edge rules (no description, disable-model-invocation, when_to_use, caps) are unconfirmed; and the .claude/CLAUDE.md, rules and CLAUDE.local.md classification disagrees with #66 repo inspect on the same files. #71 conflicts with #66 only in repo-evaluation-contract.md (keep both intro paragraphs).

@antonwolfy antonwolfy left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review at d1cb5ed. The command does what the description says on ordinary trees, and the earlier symlink and code-block fixes hold. The gaps are in how aliases and unreadable guidance files are classified, in what --harness actually switches, and in the tests, which leave most of the classification logic unpinned. Two of the inline comments I'd fix before merge: the alias handling at budget.py:119 and the zero-char guidance file at budget.py:133.

What I ran, on a Linux review host at d1cb5ed:

  • make ci-local: 18 passed, 1 failed, 4 skipped. 4,309 tests passed (1 skipped), changed-line coverage 95% (170 lines measured). The one failure is schema_compat: the origin/main it compared against (918f7a2) is ahead of the PR's merge-base (8193ce0) and has tool_preference*.v1.json that this branch lacks. The PR touches no schema; rebase before merge and the check should clear.
  • Reproductions for the code findings, on hostile fixtures with budget() called directly. No model is involved.
  • A mutation run on budget.py and cmd_repo_budget: 149 single-line mutants, 80 of which pass the full test_repo_budget.py (13 of those are equivalent). I re-ran four of the survivors by hand: >= to > on the 0.5 threshold, dropping the \w lookbehind on IMPORT, replacing the .. escape guard, and ignoring CLAUDE.local.md. All four pass 15/15.

Not anchored inline

  1. The docs say AGENTS.md's @ imports are counted; the code follows imports only from CLAUDE* files. The description's start row reads "root CLAUDE.md / AGENTS.md + their @imports", and architecture.md says "Root guidance files and their @ imports". With a root AGENTS.md of See @docs/style.md (19 chars) and a 108-char docs/style.md, the output is start 19 and no row for docs/style.md. Either follow them or reword the docs to "CLAUDE.md files and their imports".
  2. The parent help string for repo was not updated (outside this diff): commands/repo.py:269 still reads "validate repo-eval.yaml, L0 scan, fetch sources, reach probe, task self-check, agent runs and their report". dokimi/README.md:120 now names the size command, so the two disagree. Also docs/architecture.md:272 lists budget after scan while repo --help prints {validate,scan,fetch,reach,budget,...}.
  3. Non-blocking: repo budget --json carries no schema field. repo scan --json is stamped (repo_readiness, repo.py:82); repo validate --json is not, so there is precedent either way. If anyone gates CI on the JSON, a stamp lets them detect a shape change.

Still open from the earlier review, unchanged at this head: the 5-hop import limit, .claude/rules/*.md not counted, and non-UTF-8 imports counting as 0 chars with no warning (the same silent path as the first inline comment below).

Checked, no issues

  • Flags and docs: --harness, --json, --fail-on-warning and --cache-dir match repo budget --help and the contract's usage line. No new doc page, so no mkdocs.yml change. mkdocs build passes, and no generated artifacts are in the diff.
  • Determinism and trees: nine hostile fixtures run through WorkTree and GitTree gave identical entries and warnings. The sort is stable.
  • Loops and bad input: import cycles and self-imports terminate. Malformed, list-valued or missing skill frontmatter does not crash. @/abs.md, emails and trailing punctuation are handled.
  • Base check: test_repo_budget.py against origin/main fails at collection (ModuleNotFoundError: dokimi.repos.budget), as expected.

Comment thread dokimi/dokimi/repos/budget.py Resolved
Comment thread dokimi/dokimi/repos/budget.py Resolved
Comment thread dokimi/dokimi/commands/repo.py Resolved
Comment thread dokimi/dokimi/repos/budget.py Resolved
Comment thread dokimi/dokimi/repos/budget.py Resolved
Comment thread dokimi/dokimi/repos/budget.py Resolved
Comment thread dokimi/tests/test_repo_budget.py Resolved
…, handle unreadable files

- Add 6 new tests to pin import rules: existing files, duplicates at boundary, CLAUDE.local.md, circular imports, non-UTF-8, mcp.json edge cases (Thread 6)
- Update module docstring and help text to document the three warnings: no description, near-duplicates, and AGENTS.md turned off (Thread 5)
- Change `_chars` to return (count, warning_message) tuple; add warning for unreadable guidance files instead of silent 0 (Thread 0)
- Clarify in --harness help that classification rules are hardcoded to Claude Code, not profile-driven (Thread 2)
- Reorder repo command help to list budget after reach (reflect actual order)

Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
… tokenization

- Change _chars to warn on ALL unreadable files (dangling/out-of-tree symlinks, non-UTF-8 decode errors)
- Track readable vs all root CLAUDE files separately; unreadable root CLAUDE.md doesn't turn AGENTS.md off
- Add _is_readable helper to check UTF-8 decodability
- Use \w+ for duplicate description tokenization (fixes non-English word splitting)
- Skip duplicate pairs where either description has <3 tokens (avoids false positives)
- Add 8 new tests: unreadable guidance files, import rules, non-English descriptions, CLI table order
- Fix architecture.md to say "Root CLAUDE.md files" not "Root guidance files"
- Fix repo help command order to match real order (validate, scan, fetch, reach, budget, ...)
- Update help text to name Jaccard index; remove "at the root" from off warning

Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
…icate detection

- Key _unaliased by shallowest path (count /), then by CLAUDE prefix: fixes alias misclassification
- Add start_t/loaded_t sets keyed by _target for proper start/off decision
- Only root CLAUDE.md (not .claude/CLAUDE.md or CLAUDE.local.md) turns AGENTS.md off (Thread 3 cheap fix)
- Add _is_resolvable helper; exclude only unresolvable (dangling/out-of-tree) files from off rule
- Non-UTF-8 CLAUDE.md still counts for off rule (it exists, just can't be read)
- Always report exact duplicates (overlap 1.0); 3-token minimum only for partial overlaps
- Update warning to name "Jaccard" index; fix tests for new behavior
- Add 7 new/fixed tests: alias layouts, exact duplicates, import guards, non-English, CLI formatting

Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
… three alias layouts

- Fixed _is_resolvable to call tree.resolve() first, ensuring dangling/out-of-tree links excluded from off rule
- Added assertion for dangling symlink case: AGENTS.md stays start (test_unreadable_guidance_files_are_warned)
- Fixed ruff F841 in test_cli_table_row_order: removed unused tree assignment
- Added exact 0.5 overlap pair test: "alpha beta delta" vs "alpha beta gamma" (2 shared of 4 = 0.5)
- Completed test_alias_layouts_thread_1_repro with all three symlink layouts (a, b, c)
- Restored email.md in test_import_rules_import_must_exist_and_not_be_escaped to test lookbehind
- Updated replies-71.md with accurate test counts and descriptions

Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>

@antonwolfy antonwolfy left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review at 94f2cb8. Six of my seven earlier threads are fixed: the unreadable-file warnings, the three alias layouts, the .claude/CLAUDE.md and CLAUDE.local.md rule, the tokenizer, the three-warning docs and the --harness help. The alias handling still misclassifies two nearby layouts, and the tests still leave a lot of the new logic unpinned. I'd fix the two inline budget.py bugs and the test gaps before merge.

What I ran, on a Linux review host at 94f2cb8:

  • make ci-local: 19 passed, 0 failed, 4 skipped. 4,319 tests passed (1 skipped), changed-line coverage 97% (191 lines measured, 4 missing: budget.py:193, :210-211, :233). schema_compat now passes, so the earlier failure was the stale base.
  • Reproductions for the code findings on tmp-dir fixtures with symlinks, calling budget() directly. No model is involved.
  • A mutation run on budget.py and cmd_repo_budget: 182 single-line mutants, 73 of which pass the full 25-test file. 9 are equivalent and 10 are ordering-only or spec corners; the other 54 are real gaps (see the open thread below). I re-ran two by hand: both pass 25/25.

Still open from my earlier review:

  • tests/test_repo_budget.py (thread on the "not imported" assertions): the rewrite killed most of the earlier survivors (the \w lookbehind, the .. escape, >= to >, CLAUDE.local.md, the cycle guard) but about 50 real gaps remain. The ones that matter: the exact-duplicate rule and the empty-union guard (is_exact = False passes 25/25); the 0.2-overlap pair that the test comments call 0.33, and the proc-a/proc-b "exact" pair that has 4 tokens, not 3, so the partial rule catches it; the skill body chars and the on demand total (never compared to a number); the .mcp.json of []; fence edge cases (a ~ or single-backtick opener, an unclosed fence, a closer with an info string, a code span across a newline); and the CLI table (header commas, group order, in-group size order, --fail-on-warning exiting 1 on a clean tree, --harness being ignored). Two more are inline below.

Not anchored inline

  1. Non-blocking: the start total is described as an upper bound, but .claude/rules/*.md without paths: are not counted, so it can also be an undercount. This was in my earlier review and is unchanged. The 5-hop import limit is also still not modelled: a chain of 9 imports lists all 9 as start. Say so in the docs, or count them.

Checked, no issues

  • Threads fixed: a non-UTF-8 or dangling root CLAUDE.md now warns, stays start with 0 chars, and a dangling one no longer turns AGENTS.md off. The three layouts from my alias thread now classify correctly: (a) the root AGENTS.md is kept as start, (b) sub/CLAUDE.md counts at start, (c) AGENTS.md imported through link.md is start, not off. .claude/CLAUDE.md and a root CLAUDE.local.md read as start and no longer turn AGENTS.md off.
  • Duplicates: two identical Cyrillic descriptions are reported at 1.00; "用于部署 API" vs "用于测试数据库 API" is no longer a false pair; 2 shared of 4 words is reported and 1 of 3 is not.
  • Flags and docs: --harness, --json, --fail-on-warning and --cache-dir match repo budget --help and the contract's usage line. The parent repo help and the architecture entry-point order now match repo --help. mkdocs build passes, no schema or with_docs key is touched, and no generated artifacts are in the diff.
  • CLI: the gate exits 1 on duplicate, off and unreadable fixtures and 0 without the flag; a missing source and an unknown harness exit 1 with ✗ ... on stderr.

Comment thread dokimi/dokimi/repos/budget.py Resolved
Comment thread dokimi/dokimi/repos/budget.py Resolved
Comment thread dokimi/dokimi/repos/budget.py Resolved
Comment thread dokimi/tests/test_repo_budget.py Resolved
Comment thread dokimi/tests/test_repo_budget.py Resolved
Comment thread dokimi/docs/repo-evaluation-contract.md Resolved
Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
… on unreadable SKILL.md

Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
…d changelog

Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
@napetrov

Copy link
Copy Markdown
Contributor Author

On the review point about .claude/rules and the 5-hop limit: documented in 076662a. architecture.md and the contract now say start is not an upper bound, since .claude/rules/*.md files are not counted and imports are followed past the 5-hop limit. Counting rules files and the hop limit are left for a follow-up.

@antonwolfy antonwolfy left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review at 076662a. Six of my seven open threads are fixed: both alias layouts, the nested-import timing, the unreadable SKILL.md warning, the pytest.skip wrapping, the target-keyed fixtures and the contract wording. The alias rules now hold on the layouts I tried. Two problems remain at the edges, and the test file still leaves a lot of the new logic unpinned. I'd fix the two budget.py comments and the test gaps before merge.

What I ran, on a Linux review host at 076662a:

  • make ci-local: 19 passed, 0 failed, 4 skipped. 4,327 tests passed (1 skipped), changed-line coverage 99% (206 lines measured).
  • Reproductions for the code findings on tmp-dir fixtures with symlinks, through both WorkTree and a GitTree built from a local git init repo, calling budget() directly. No model is involved.
  • A mutation run on budget.py and cmd_repo_budget: 143 single-line mutants, 63 of which pass the full 33-test file (13 are equivalent). I re-ran seven of the survivors by hand; all seven pass 33/33.
  • About 700 random link layouts built from guidance and import slots, compared between WorkTree and GitTree: no crash, no double counting, no guidance file missing from entries. The only differences are the first two inline comments.

Still open from my earlier review:

  • tests/test_repo_budget.py (thread on the "not imported" assertions): the rewrite killed the earlier survivors and the alias, target-key and unreadable-file mutants, but about 50 real gaps remain. The ones that matter:
    • A plain missing import (CLAUDE.md = see @missing.md) is never tested; the only "missing" import is ../etc/c.md, which _normalize drops before any file check. Mutation check: _is_file returning tree.is_file(path) or True passes 33/33.
    • The exact-duplicate rule: is_exact = False passes 33/33, because every duplicate fixture has 3 or more tokens and the partial rule fires. Two skills described "Deploy app" are the case to add (it reports at 1.00). The same holds for the 0.5 threshold lowered to 0.34 and the >= 3 token floor lowered to >= 2.
    • Skill sizes: "body_chars": len(body) replaced by 0 passes 33/33. No test compares the on demand total, or any total containing a skill, to a number.
    • .mcp.json whose top level is not an object ([], null): dropping AttributeError from the except at budget.py:267 passes 33/33.
    • CLI: if args.fail_on_warning and payload["warnings"] mutated to if args.fail_on_warning passes 33/33, so a gate that exits 1 on a clean tree is not caught. The table order inside a group and the lazy / on demand group order are not pinned either.
    • Fence edges: an unclosed fence, a closer longer than the opener, a code span across a newline, and @./x.md (entry path ./x.md without the . drop in _normalize) all pass.

Checked, no issues

  • Threads fixed: AGENTS.md + CLAUDE.md + sub/CLAUDE.md -> ../AGENTS.md gives lazy 100 (not off). sub/CLAUDE.md = @../AGENTS.md gives start 1, lazy 113 with no warning. CLAUDE.local.md -> CLAUDE.md no longer hides the root file from the off rule. A non-UTF-8 SKILL.md warns about the encoding and not the description.
  • Tests: _link wraps only symlink_to; the run shows 0 skipped. The f vs _target swaps at the alias checks are killed.
  • Flags and docs: --harness, --json, --fail-on-warning and --cache-dir match repo budget --help and the contract's usage line. The parent repo help, the README bullet and the architecture entry-point order match repo --help. mkdocs build --strict passes, no new doc page, no schema or with_docs key is touched, no generated artifacts in the diff.
  • CLI: a bad directory, a file as source and an unknown --harness exit 1 with ✗ ... on stderr and empty stdout. --fail-on-warning exits 1 in table and --json mode.
  • Loops and hostile trees: import cycles, link loops, a directory named CLAUDE.md, non-UTF-8 imports and a looping .mcp.json give no crash and the same result in both trees.

Comment thread dokimi/dokimi/repos/budget.py Resolved
Comment thread dokimi/dokimi/repos/budget.py Resolved
Comment thread dokimi/dokimi/repos/budget.py Resolved
Comment thread dokimi/docs/architecture.md Resolved
…as load time, skill warnings

- Imports and .mcp.json use _is_resolvable, so a dangling or out-of-tree link
  is no import on a GitTree either (it was listed with a warning there).
- A CLAUDE* alias group with any root alias loads at start, whichever path
  is displayed.
- An unreadable SKILL.md names its cause and says only the body counts 0.
- Docs, help and changelog: identical descriptions are always reported,
  @path.md-only imports, root .claude/skills only, AGENTS.md start rule,
  MCP servers listed not sized.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RW8mAicb3ibXdzHMV83wDn
Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
…ting

A skill with `disable-model-invocation: true` is not in the listing the
model sees (harnesses/skill_treatment.py refuses such a skill for that
reason), but `repo budget` charged its name and description to `start`,
warned about its missing description and compared it for near-duplicates.
Its body still counts on demand: a user can invoke it.

Signed-off-by: Alexander Rybkin <alexander.rybkin@intel.com>
…a passing gate

Each of these passed the suite with its code removed: `is_exact = False`
(every duplicate fixture had 3+ words, so the partial rule caught it), the
`AttributeError` in `_mcp_servers` (a top-level `[]` or `null`), and
`--fail-on-warning` exiting 1 on a tree with no warning.

Signed-off-by: Alexander Rybkin <alexander.rybkin@intel.com>
@xaleryb

xaleryb commented Oct 17, 2026

Copy link
Copy Markdown
Contributor

Re-ran at 2fab356 on mattpocock/skills, oven-sh/bun and hostile fixtures; our five commits are intact and each test still fails without its fix. Holding: unreadable imports now warn ("not readable (non-UTF-8 or binary)"), dangling and out-of-tree imports give the same result on a work tree and a commit, a .claude/CLAUDE.md alias group loads at start, and SKILL.md warnings name the cause.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Directory-symlink resolution can select incorrect content, and false depth-limit warnings can incorrectly fail the warning gate.

6 open issues
1 previously missed issue

In code that hasn't changed since last review

Medium severity Depth-limit warnings falsely flag already loaded or unresolved imports

dokimi/​dokimi/​repos/​budget.py:127

The depth-limit warning checks import path spellings, not all files already loaded. With a four-hop limit, CLAUDE.md → c1.md → c2.md → c3.md → c4.md → CLAUDE.md warns even though the root guidance is already loaded. A cut import through an alias of a loaded file also warns, as does a nonexistent fifth-hop target. These false positives can fail --fail-on-warning without any omitted context. Skip unresolvable targets and compare resolved targets against all loaded guidance/import entries.

Review effort: Balanced

Comment thread dokimi/dokimi/repos/source.py Resolved
Copilot AI balanced review requested due to automatic review settings October 19, 2026 14:56
… threshold

- Clarify budget.py docstring: root CLAUDE* file (not just CLAUDE.md) imports
- Fix repo-evaluation-contract.md: `start` is an upper bound, clarify logic
- Add deterministic tie-breaker (path) to table row sort key
- Optimize source.py: cache _links() result for single resolve() call
- Relax timing threshold in test (5s instead of 1s) to reduce flakiness

Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Import cycle detection, hop-limit warnings, and fence parsing have verified correctness issues.

1 open issue
2 previously missed issues

In code that hasn't changed since last review

Medium severity Require closing fences to match the opener character

dokimi/​dokimi/​repos/​budget.py:49

The closing-fence pattern accepts mixed backticks and tildes. A line of three backticks followed by tildes incorrectly closes a backtick block, so an @notes.md example before the real closer is counted as an import. The real closer can then be treated as a new opener, hiding subsequent valid imports. Restrict closing runs to the opener's character while allowing longer closers.

Medium severity Use resolved identities for hop-limit warning suppression

dokimi/​dokimi/​repos/​budget.py:127

The hop-limit warning check omits automatically loaded guidance and compares path spelling rather than resolved file identity. CLAUDE.md → c1.md → c2.md → c3.md → c4.md → CLAUDE.md therefore warns even though the root is already loaded; a fifth-hop alias of an already imported file also warns. Both incorrectly fail --fail-on-warning. Suppress these warnings using resolved identities of guidance and imports already classified as start or lazy.

6 resolved since last review

Review effort: Balanced

Comment thread dokimi/dokimi/repos/budget.py Resolved

@antonwolfy antonwolfy left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review at c6f34bf. The five threads from 74c4892 about the hop limit, the loop test, the nested chain test, the help text and the docstring are fixed. The new commit leaves the start caveat contradicting itself across two pages, and the hop-limit warning still fires for imports that were loaded. I'd fix the first before merge.

What I ran, on a Linux review host at c6f34bf:

  • make ci-local: 19 passed, 0 failed, 4 skipped. 5,175 tests passed (1 skipped), changed-line coverage 99% (261 lines measured).
  • Reproductions for the code findings on tmp-dir fixtures, calling budget() directly on a WorkTree. No model is involved.
  • Mutation checks on the code added since 74c4892: dropping the suppression in the warning loop, each half of it, RuntimeError in _imports, import_max_depth in the nested call, cut_lazy, and both escape checks in GitTree.resolve.

Not anchored inline

  1. The hop-limit warning still fires for imports that were loaded, or that load nothing. I agree with @copilot-pull-request-reviewer on the root-file, alias and nonexistent-target cases. New evidence: import_max_depth=4, CLAUDE.md -> c1 -> c2 -> c3 -> c4, and c4.md = @nope.md gives c4.md: import of nope.md is past the harness's 4-hop import limit, not followed, though nope.md does not exist and a within-limit missing import is dropped silently. @d/c1.md with d -> . warns the same way, while @./c1.md does not. A nested sub/c4.md = @CLAUDE.md warns about sub/CLAUDE.md. Each exits 1 under --fail-on-warning with nothing omitted: the filters at budget.py:126 and :235 compare path spellings, not _target() results.
  2. A directory-link cycle never terminates when no import limit is set. I agree with @copilot-pull-request-reviewer on the cycle detection. New evidence: CLAUDE.md = @alias/CLAUDE.md with alias -> ., budget(WorkTree(d), names, None) was still running after 20 s (killed by timeout). With import_max_depth=4 it finishes at once. The shipped claude-code profile sets 4, so the CLI is shielded; a custom --harness profile without guidance.import_max_depth and any caller of budget() with the default are not.

Still open from my earlier review:

  • dokimi/dokimi/repos/source.py:229: the path == ".." or path.startswith("../") check in GitTree.resolve still has no test. Replacing the condition with False passes all 727 repo tests. The first escape check (:226) is now killed by the absolute-link case.

Checked, no issues

  • Hop boundary: a chain of exactly four hops does not warn, a fifth hop does, for root and nested chains. Dropping import_max_depth from the nested call or cut_lazy from the warning loop now fails test_a_nested_claude_md_chain_is_limited_and_warns.
  • Loop test: removing RuntimeError from the except in _imports fails test_an_import_of_a_link_loop_does_not_crash.
  • Warning lists: the budget docstring, repo budget --help, architecture.md and the changelog fragment list the same five warnings. The sort key now ends in the path.
  • Repo rules: import_max_depth is an optional property added to harness_profile.v1, so the schema stays at v1 and schema_compat passes. No new doc page, so no mkdocs.yml change, and no generated artifacts are in the diff.

Comment thread dokimi/docs/repo-evaluation-contract.md Resolved
Comment thread dokimi/dokimi/repos/budget.py Resolved
… docs

An alias that points back up (CLAUDE.md imports alias/CLAUDE.md, alias -> .)
named the same file under ever longer paths, and the traversal dedupes by
path string, so it expanded until the link limit or forever. Dedupe imports
by the file they resolve to, keeping the shortest-hop placement.

Add tests for the alias cycle (work tree and git tree) and for a cut import
that a nested chain loads lazily, which must not warn.

architecture.md and the evaluation contract said different things about
whether start is an upper bound; both now say it is too low by what it does
not count and too high by the skill listing the harness cuts.

Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>

@vchamarthi vchamarthi left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Read budget.py, commands/repo.py, the source.py change and the tests at c030002, and checked the classification rules against the current Claude Code memory and skills docs. CI is pending at this head (15 checks). The two majors below are rules that disagree with the documented harness behavior and change what --fail-on-warning reports.

Comment thread dokimi/dokimi/repos/budget.py Outdated
# Only a root CLAUDE.md (not .claude/CLAUDE.md or CLAUDE.local.md) can turn AGENTS.md off
root_claude = [f for f in claude if _is_root(f)]
# For off rule: only root CLAUDE.md that resolves (not dangling/out-of-tree); unreadable UTF-8 still counts
root_claude_for_off = [f for f in root_claude if f == "CLAUDE.md" and _is_resolvable(tree, f)]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should fix: root_claude_for_off keeps only f == "CLAUDE.md", but Claude Code also lets .claude/CLAUDE.md and CLAUDE.local.md displace AGENTS.md; count all three root CLAUDE* paths here.

Claude Code memory docs, "When Claude Code reads AGENTS.md" (https://code.claude.com/docs/en/memory#agents-md): "Count, so Claude reads them instead of AGENTS.md: a CLAUDE.md, .claude/CLAUDE.md, or CLAUDE.local.md in your working directory or any directory above it". The docs also warn that adding a CLAUDE.local.md "stops Claude from reading AGENTS.md for you".

At c030002, with a tree of .claude/CLAUDE.md + AGENTS.md: root_claude_for_off is empty (budget.py:74), so the AGENTS.md branch is skipped and _is_root("AGENTS.md") makes it start (budget.py:99-100). No off entry, no warning, and --fail-on-warning passes on exactly the layout the docs say silently drops AGENTS.md. test_claude_local_md_import_at_start and test_claude_dir_claude_md_is_project_memory_read_at_start pin the current behavior and would need flipping.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 4a68ac9. root_claude_for_off now counts every root CLAUDE*: CLAUDE.md, .claude/CLAUDE.md and CLAUDE.local.md all turn AGENTS.md off, matching the "When Claude Code reads AGENTS.md" section. test_claude_dir_claude_md_is_project_memory_read_at_start and test_claude_local_md_import_at_start now assert AGENTS.md is off with a warning; both fail on c030002.

Comment thread dokimi/dokimi/repos/budget.py Outdated
root_claude_for_off = [f for f in root_claude if f == "CLAUDE.md" and _is_resolvable(tree, f)]
root_claude_readable = [f for f in root_claude if _is_resolvable(tree, f)]
# A root CLAUDE.md's imports load at start; a nested one's when the agent reaches its subtree.
at_start, cut_start = _imports(tree, root_claude_readable, import_max_depth)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should fix: Follow @ imports from every AGENTS.md that is classified start or lazy (not off), the same way as from CLAUDE* files; Claude Code expands them.

Claude Code memory docs (https://code.claude.com/docs/en/memory#agents-md): "Inside each AGENTS.md: @path imports are expanded". Also, "At session start: every AGENTS.md and .claude/AGENTS.md in your working directory and the directories above it".

At c030002 the import roots are root_claude_readable and the nested claude list (budget.py:77-78), both built from basenames starting with CLAUDE (budget.py:70). A root AGENTS.md containing @docs/guide.md with no CLAUDE.md gives docs/guide.md no entry, and the start caveat in architecture.md and the contract does not list this gap. Related: _is_root (budget.py:157) treats .claude/CLAUDE.md as root but not .claude/AGENTS.md, which the same docs load at start, so that file is reported lazy.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 4a68ac9. AGENTS.md files that are not turned off are now import roots: root ones load their @imports at start, nested ones lazily. .claude/AGENTS.md is treated as a root file. test_an_agents_md_import_loads_what_it_names fails on c030002. The start caveat in architecture.md no longer lists this as a gap.

Comment thread dokimi/dokimi/repos/budget.py Outdated
meta, body, unreadable = {}, "", _unreadable_reason(err)
meta = meta if isinstance(meta, dict) else {}
name = str(meta.get("name") or path.split("/")[-2])
description = str(meta.get("description") or "")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Optional: When description is missing, use the first non-empty body line as the listed text (for listing_chars and _near_duplicates), and reword the warning so it does not say the listing is empty.

Claude Code skills docs, frontmatter table (https://code.claude.com/docs/en/skills): description: "If omitted, uses the first non-empty line of the markdown content."

At c030002 description = str(meta.get("description") or "") (budget.py:268) feeds listing_chars and _near_duplicates, and budget.py:139 warns "no description; the listing gives the agent nothing to choose it by". Two skills with no description and the same first body line are a real near-duplicate in the listing but compare as two empty word sets, which _near_duplicates skips (if not union: continue).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 4a68ac9. A skill with no description now uses the first non-empty body line for the listing and the near-duplicate check, as the skills docs describe. The no-description warning fires only when the body is also empty, and its wording no longer says the listing is empty. test_skill_without_description_is_described_by_its_first_body_line fails on c030002.


from dokimi.repos.source import RepoTree

SKILL_GLOB = re.compile(r"^\.claude/skills/[^/]+/SKILL\.md$")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Optional: Either count <subdir>/.claude/skills/*/SKILL.md as lazy listing (and include them in the skill warnings), or add nested skill directories to the documented list of what the budget does not count.

Claude Code skills docs, "Load skills in monorepos and subdirectories" (https://code.claude.com/docs/en/skills): "Skills in a .claude/skills/ directory below where you started don't load at startup. They load the first time Claude reads or edits a file in that subdirectory". SKILL_GLOB = ^\.claude/skills/[^/]+/SKILL\.md$ (budget.py:39) excludes apps/web/.claude/skills/deploy/SKILL.md, so a monorepo's nested skills get no row, no lazy chars and no duplicate or no-description warning under --fail-on-warning.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 4a68ac9 by documenting the exclusion instead of counting: architecture.md and repo-evaluation-contract.md now list nested /.claude/skills/ skills (loaded on the first read in that subdirectory) among what the start total does not count.

napetrov and others added 3 commits October 19, 2026 22:41
…hout a description

Claude Code reads AGENTS.md only when no CLAUDE.md, .claude/CLAUDE.md or
CLAUDE.local.md is in the working directory or above it, so all three
turn it off. AGENTS.md files Claude reads expand their own @imports, and
.claude/AGENTS.md loads at start. A skill with no description is listed by
its first non-empty body line. Nested .claude/skills skills are documented
as not counted.

Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
A missing or dangling import one hop past the limit was reported as cut and
failed --fail-on-warning, though it loads nothing; within the limit the same
import is dropped silently.

Signed-off-by: Alexander Rybkin <alexander.rybkin@intel.com>
…mports

An AGENTS.md a root CLAUDE* displaces was an import root at hop 0 once a
CLAUDE.md imported it, so the hop limit and the start/lazy split were lost.
Only an AGENTS.md read on its own, or a root CLAUDE* alias, is a root now.

Signed-off-by: Alexander Rybkin <alexander.rybkin@intel.com>
@xaleryb

xaleryb commented Oct 20, 2026

Copy link
Copy Markdown
Contributor

Since 4a68ac9, an AGENTS.md that a CLAUDE.md imports is also followed as its own import root at hop 0, so its imports get a fresh hop budget. Pushed d50018a, with tests that fail on 6cac186:

  • CLAUDE.md = @AGENTS.md, then AGENTS.md -> a1 -> a2 -> a3 -> a4: a4.md was counted at start with no warning. Expected, as at c030002: "a3.md: import of a4.md is past the 4-hop import limit", and a4 not counted.
  • A root CLAUDE.md, sub/CLAUDE.md = @../AGENTS.md, AGENTS.md = @docs/x.md: docs/x.md was start. Expected: lazy.

An AGENTS.md is now its own import root only when no root CLAUDE* displaces it, or when it is a link alias of one. vchamarthi's cases still pass.

@napetrov

Copy link
Copy Markdown
Contributor Author

Fixed in d50018a (pushed by Alexander Rybkin on feat/repo-budget). The AGENTS.md that a CLAUDE.md imports now counts its hops from the CLAUDE.md, so the 4-hop limit and the start/lazy split apply to it. Only an AGENTS.md read on its own, or a root CLAUDE* alias, is an import root.

Checked at d50018a: the two new tests (test_an_agents_md_read_through_an_import_counts_hops_from_the_claude_md and test_an_agents_md_only_a_nested_claude_md_imports_loads_its_imports_lazily) fail on 6cac186 and pass on d50018a. All 70 tests in tests/test_repo_budget.py pass, and ruff check is clean on the changed files.

🤖 Generated with Claude Code

@xaleryb xaleryb left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approved at d50018a. Every earlier item of ours and every reviewer thread is fixed or answered at this head; our last commit closes the one mid item left, with a test red without it. Full suite, ruff and mypy clean on Linux; CI green.

@antonwolfy antonwolfy left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review at d50018a. The AGENTS.md import roots, the root CLAUDE* displacement rule and the first-body-line description all behave as the docs say on the layouts I tried. Three points are left: one false hop-limit warning, a test gap around the new code, and docs that lag it.

What I ran, on a Linux review host at d50018a:

  • make ci-local: 19 passed, 0 failed, 4 skipped. 5,184 tests passed (1 skipped), changed-line coverage 99% (271 lines measured; the one missing line is source.py:230).
  • Reproductions for the code finding on tmp-dir fixtures with a symlink, calling budget() directly on a WorkTree. No model is involved.
  • A mutation run on the code added since c6f34bf (the agents import roots, root_claude_for_off, the reached set, the hop-warning filters, the description fallback): 33 single-line mutants, 13 of which pass test_repo_budget.py. I re-ran three by hand; all three pass 70/70.

Not anchored inline

  1. Nit: the docs lag the new code in three places. The module docstring (budget.py:9-10) says "imports are followed from CLAUDE* files only", but a root AGENTS.md of @a.md gives start guidance AGENTS.md and start import a.md, and architecture.md says so. The changelog fragment (line 3) lists start as root CLAUDE.md files plus a root AGENTS.md "when no root CLAUDE.md displaces it", and repo budget --help (commands/repo.py:368) says "a root CLAUDE.md turns off"; CLAUDE.local.md alone, or .claude/CLAUDE.md alone, also gives off guidance AGENTS.md. The fragment, the help and the docstring (budget.py:24) also say the command warns on "skills with no description", but a skill with no description and a non-empty body is listed by its first line and does not warn; only an empty body does. Suggestion: reword the three places to the architecture.md wording.

Still open from my earlier review:

  • dokimi/dokimi/repos/source.py:229: the path == ".." or path.startswith("../") check in GitTree.resolve still has no test, and the changed-line coverage report lists source.py:230 as the one missing line. With the condition replaced by False, tree.read_text("docs/../../x.md") (with docs -> real) raises docs/../../x.md links to nothing: ../x.md instead of path escapes the repository. Nothing is read outside the tree, so it is a test gap only. The test is pytest.raises(SourceError, match="escapes") on that path.

Checked, no issues

  • Displacement rule: CLAUDE.local.md alone and .claude/CLAUDE.md alone turn a root AGENTS.md off with a warning; .claude/CLAUDE.md -> ../AGENTS.md keeps it at start.
  • Hop counting: CLAUDE.md -> sub/AGENTS.md -> a1..a3 follows three imports and warns on a4; an AGENTS.md that links to CLAUDE.md counts from hop 0 with no double count. A cut import of a missing file no longer warns.
  • Skills: a skill with an empty body and no description warns; one with a body line is listed with that line stripped (First line = 11 chars). disable-model-invocation: true keeps its body row.
  • Repo rules: changelog fragment present, no schema or with_docs key touched beyond the optional import_max_depth, no new doc page, no generated artifacts, tests run offline.

Comment thread dokimi/dokimi/repos/budget.py Resolved
Comment thread dokimi/dokimi/repos/budget.py Resolved
A cut import was warned about even when another chain had already loaded
the same file through a link, because the check compared path strings.
Compare resolved targets instead. Also pin the root and nested AGENTS.md
chains past the limit, the back-import to a root file and a directory-link
loop, which the mutation checks showed no test covered.

Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>

@antonwolfy antonwolfy left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review at 947b915. The two hop-limit threads are fixed, and the new tests kill the mutants they were written for. One bug is left in the same warning: it still fires for a cut import whose target is a guidance file. One nit on docs.

What I ran, on a Linux review host at 947b915:

  • make ci-local: 19 passed, 0 failed, 4 skipped. 5,189 tests passed (1 skipped), changed-line coverage 99% (272 lines measured).
  • Reproductions for the code finding on tmp-dir fixtures, calling budget() directly on a WorkTree. No model is involved.
  • Mutation checks on the code added since d50018a and on the earlier threads: 9 mutants in budget.py (cut_start / cut_lazy, the reached seed and reached.add, and the three variants of the new loaded filter), all killed; 2 in GitTree.resolve, one survives (below).

Not anchored inline

  1. Nit: three doc wordings from my last review are unchanged. The module docstring (budget.py:9-10) says imports are followed "from CLAUDE* files only", but a root AGENTS.md of @a.md is followed (budget.py:83-93) and architecture.md says so. The changelog fragment, the repo budget --help text (commands/repo.py:368) and the docstring (budget.py:17, :25) say a "root CLAUDE.md" turns AGENTS.md off, but .claude/CLAUDE.md or CLAUDE.local.md alone does too. The same three places say the command warns on "skills with no description"; budget.py:152-154 warns only when the body is also empty. Suggestion: use the architecture.md wording in all three. The PR description still calls start an upper bound, while both doc pages say it is not.

Still open from my earlier review:

  • dokimi/dokimi/repos/source.py:229: the path == ".." or path.startswith("../") check in GitTree.resolve still has no test. The first check (:226) is now killed by the new cases.

Checked, no issues

  • Thread fixes: lnk.md -> x.md loaded by sub/CLAUDE.md no longer warns; reverting the filter to target in at_start or target in lazy_all fails the new link test. The root and nested AGENTS.md chain tests, the import-back-to-root test and the directory-link loop test each fail under the matching mutant (cut_start = cut_start, cut_lazy = cut_lazy, reached = set(), dropping reached.add).
  • Hop counting: a cut target that is a displaced AGENTS.md warns, as it should: it is off, so it is omitted. A target reachable by a shorter chain does not warn. A root chain whose c4.md imports @CLAUDE.md does not warn.
  • Repo rules: import_max_depth is an optional harness_profile.v1 property, so the schema stays at v1 and schema_compat passes. The changelog fragment is present, no new doc page, no generated artifacts, and the tests run offline.

Comment thread dokimi/dokimi/repos/budget.py Resolved
…ady counted

A guidance file is its own entry, not an import, so the hop-limit filter
never saw it as loaded. An import of a CLAUDE.md past the depth limit
warned even though that file is counted at start or lazy.

Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>

@antonwolfy antonwolfy left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The hop-limit fix is right: a cut import of a counted guidance file no longer warns, and I found no false positive on the layouts from my last thread. One test gap in the new filter is worth closing before merge, and one thread is still open.

What I ran, on a Linux review host at 00a4af2:

  • make ci-local: 20 passed, 0 failed, 4 skipped. 6,155 tests passed (1 skipped), changed-line coverage 99% (272 lines measured).
  • Reproductions on tmp-dir fixtures with import_max_depth=4 and a chain c1..c4 ending in @<target>, for CLAUDE.md, sub/CLAUDE.md, sub/dir/CLAUDE.md, .claude/CLAUDE.md, CLAUDE.local.md, a root AGENTS.md and sub/AGENTS.md, calling budget() directly on a WorkTree and a GitTree built from a local git init repo. No model is involved. None warns when the target is counted, and a displaced AGENTS.md still does.
  • Mutation checks on the new filter and on GitTree.resolve (below).

Still open from my earlier review:

  • dokimi/dokimi/repos/source.py:229: the path == ".." or path.startswith("../") check in GitTree.resolve still has no test. The first check (:226) is now killed by the absolute-link case.

Checked, no issues

  • Thread fix: loaded now holds the resolved target of every guidance entry that is not off. Dropping the whole addition fails test_a_cut_import_of_a_counted_guidance_file_is_not_warned[sub/CLAUDE.md].
  • GitTree vs WorkTree: 57 queries each for resolve, resolve(dirs=True) and read_text over nested links, .. after a link, links to ., loops and absolute links. Every escape raises SourceError in both trees. repo budget never reaches the two places where they differ (resolve of a path with a .. and no link, resolve of a missing plain path).
  • Repo rules: import_max_depth is an optional harness_profile.v1 property, so the schema stays at v1 and schema_compat passes. No new doc page, no generated artifacts, and the changelog fragment is present.

warnings = unreadable_warnings[:]
# A guidance file is an entry of its own, so a cut import of it is already loaded (not an import).
loaded = {_target(tree, p) for p in at_start | lazy_all} | {
_target(tree, e["path"]) for e in entries if e["kind"] == "guidance" and e["when"] != "off"}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nothing pins the kind == "guidance" and when != "off" halves of the new loaded filter.

Mutation check: both of these pass all 77 tests in test_repo_budget.py.

  • e["when"] != "off" -> removed. A cut import of an off AGENTS.md (root CLAUDE.md -> c1..c4, c4.md = @AGENTS.md, a root AGENTS.md) must still print c4.md: import of AGENTS.md is past the harness's 4-hop import limit, not followed, because the file is omitted. With the condition gone it would be counted as loaded and the warning would vanish. --fail-on-warning would then pass on a tree that drops context.
  • e["kind"] == "guidance" -> removed. A skill entry's path is then also loaded, so a cut @.claude/skills/s/SKILL.md import stops warning. The unmutated code warns c4.md: import of .claude/skills/s/SKILL.md is past the harness's 4-hop import limit, not followed.

The new parametrized test uses only counted CLAUDE.md targets.

Suggestion: add AGENTS.md (displaced by a root CLAUDE.md) to the new test's targets with the warning text asserted, and a cut @.claude/skills/s/SKILL.md case.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 31ff745. Added a cut-import test for an off AGENTS.md (warning text asserted) and one for a cut @.claude/skills/s/SKILL.md; each fails when the matching half of the loaded filter is removed.

Comment thread dokimi/dokimi/repos/budget.py Outdated
A static inventory, in characters, of the context a harness takes from the tree when it
starts at the repository root:

- **start**: guidance files at the root, the files a root ``CLAUDE*`` file ``@import``s (imports are

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: three doc wordings from my last two reviews are unchanged. budget.py:9-10 says imports are followed "from CLAUDE* files only", but a root AGENTS.md of @extra.md gives start import extra.md 100 (:80-90 follow it), and architecture.md says so. The changelog fragment, the repo budget --help text and the docstring (:17, :25) say a "root CLAUDE.md" turns AGENTS.md off, but .claude/CLAUDE.md or CLAUDE.local.md alone gives off guidance AGENTS.md too. The same three places say the command warns on "skills with no description"; a skill with no description and a body line is listed by that line and does not warn, only an empty body does (:154). Suggestion: use the architecture.md wording in all three. The PR description also still calls start an upper bound, while both doc pages say it is not.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 31ff745. The module docstring, repo budget --help and changelog fragment now match architecture.md: AGENTS.md imports are followed, .claude/CLAUDE.md and CLAUDE.local.md also turn AGENTS.md off, and only a skill with no description and no body text warns. The PR description no longer calls start an upper bound.

…gn budget wording (intel#71)

Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
@napetrov
napetrov merged commit 6ae5dc5 into intel:main Oct 20, 2026
39 of 42 checks passed
antonwolfy added a commit that referenced this pull request Oct 23, 2026
…ed (#198)

- #71: `GitTree.resolve` refuses a path that climbs out of the tree after
  a directory link (`docs -> real`, then `docs/../../x.md`). With that
  check disabled, the read failed later as "links to nothing" and the
  suite passed.
- #153: a patch that adds only an MCP server, or only a file an agent can
  use, still gets an `uptake` record. Gating `uptake` on the files alone,
  or on the servers alone, passed the suite.
- #105: the catalog's competitor cards keep their descriptions. Building
  them with `description=""` passed the suite.

Signed-off-by: Anton Volkov <antonwolfy@gmail.com>
napetrov pushed a commit that referenced this pull request Oct 23, 2026
…v1 (#205)

* feat(repo): stamp and schema-check `repo budget` JSON as repo_budget.v1

`repo budget --json` carried no schema field, unlike `repo scan`
(repo_readiness.v1), `repo inspect` (repo_environment.v1) and `repo reach`
(guidance_reach.v1), so nobody gating CI on the JSON could detect a shape
change. This settles that non-blocking review point on #71.

Add schemas/repo_budget.v1.json for the payload `budget()` returns,
register it in artifacts.SCHEMA_VERSIONS, stamp the `--json` output, and
write `--out` through save_artifact as the sibling commands do (it now
creates missing parent directories and validates before writing). Add the
schema fixture, tests, and name the schema in the contract and architecture
docs.

Signed-off-by: Anton Volkov <antonwolfy@gmail.com>

* docs(changelog): repo budget --out validates before writing, as repo scan does

Review of #205 (exai-bot): "refuses a payload that does not match the
schema" read like a handled user error. A payload that fails the schema
is a bug in `budget()`, and `--out` raises on it as `repo scan --out`
and `repo inspect --out` do; the entry now says it validates.

Signed-off-by: Anton Volkov <antonwolfy@gmail.com>

* feat(repo): repo_budget.v1 is closed at every level, not only the top

Review of #205 (napetrov): only the top level had
`additionalProperties: false`, so an entry, `totals` or a
`duplicate_skills` pair could gain a key and still validate, and a CI
gate on the JSON would miss that shape change. `budget()` writes exactly
`{path, kind, when, chars}`, the four `when` totals and `{a, b,
overlap}` (the real-output fixture has no other key), so all three are
closed now. The sibling schemas close only their top level; this one is
new, so it can start strict.

Signed-off-by: Anton Volkov <antonwolfy@gmail.com>

---------

Signed-off-by: Anton Volkov <antonwolfy@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

8 participants