You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
feat(repo): repo budget, what a repository puts into an agent's context - #71
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.
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
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>
❌ 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.
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>
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):
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.
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.
864d67b: two skills sharing a name collapsed in the duplicate check, so an identical pair (overlap 1.0) was never reported.
efd2999: @x.md in a fenced block or a span, and @/abs.md, were counted as start; Claude Code does not follow them.
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 #66repo inspect on the same files. #71 conflicts with #66 only in repo-evaluation-contract.md (keep both intro paragraphs).
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.
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
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".
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,...}.
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.
…, 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>
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.
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
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.
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.
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.
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.
…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>
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.
A skill with disable-model-invocation: true was charged to the start listing and compared for near-duplicates, though skill_treatment.py and coding-harnesses.md treat it as invisible to the model. Pushed b6ec7eb.
Four mutants from the test thread still passed (is_exact = False, body_chars = 0, dropping the .mcp.jsonAttributeError, --fail-on-warning exiting 1 on a clean tree). Pushed ba7e23e with tests; each now fails.
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.
… 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>
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.
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.
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.
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
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.
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.
… 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>
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.
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.
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.
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.
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.
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 c030002description = 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).
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.
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.
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.
…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>
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.
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.
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.
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.
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
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.
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>
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.
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
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.
…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>
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.
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.
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 offAGENTS.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.
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.
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.
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.
…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>
…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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.It has two warnings, and
--fail-on-warningturns them into a gate:MCP servers in
.mcp.jsonare listed by name only. Their tools are not counted, because the server reports them at start-up.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 scanchecks claims,repo reachspends a model call per scenario, and neither sizes anything.After, run on real checkouts:
abicheck's nested
CLAUDE.mdfiles link their siblingAGENTS.mdwith 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 withrepo reachon Claude Code 2.1.286 and 2.1.296.One case found during development: the first version ignored imports from nested
CLAUDE.mdfiles and reported their bridgedAGENTS.mdas off.test_a_nested_claude_md_bridge_loads_its_agents_md_lazilycovers it.tests/test_repo_budget.py(10 tests) covers:.mcp.json, including a broken one;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
starttotal 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
pytestpassesruff,mypy, and schema checks pass🤖 Generated with Claude Code