Skip to content

repo inspect: static look at what a repository offers an agent (repo_environment.v1) - #66

Merged
napetrov merged 39 commits into
intel:mainfrom
napetrov:repo-environment-inspect
Oct 20, 2026
Merged

napetrov merged 39 commits into
intel:mainfrom
napetrov:repo-environment-inspect

Conversation

@napetrov

Copy link
Copy Markdown
Contributor

Summary

New repo inspect: a static look at what a repository offers a coding agent, before any manifest exists. No LLM call, no repository command run. Writes repo_environment.v1.

 dokimi repo
   validate | scan | fetch | reach | tasks | run | report
+  inspect <source> [--out env.json] [--json] [--large-bytes N] [--fail-on-error]
dokimi/repos/environment/
├── instructions.py  # instruction files per harness: scope, activation, directory, local-private, size
├── checks.py        # build/test/lint/typecheck per project root; basis: declared | toolchain
├── risks.py         # MCP/hooks/plugins, risky package scripts, auto hooks that install with lifecycle scripts on
└── files.py         # generated, minified, test and large files (default > 100 KB ≈ 30k tokens)

Observations are kept apart from findings. Each finding has a severity and a separate confidence, plus the stages it affects. A stage is blocked, attention or at best no_known_blocker, never "ready". What a static pass cannot see goes in not_observed: whether the commands pass, CI secrets, network, which files a harness version actually loads (repo reach measures that). There is no overall score.

basis: toolchain covers cases like go test next to a go.mod: a place to start, not evidence that tests exist. Per-root detection is there so that monorepos and repos with the project in a subdirectory (this repo: dokimi/pyproject.toml) don't report checks as missing.

Evidence

  • Before: dokimi repo inspect . → invalid choice: 'inspect'. On an unfamiliar repo every repo command needs a manifest first.
    After: oneDAL @ 32cf1ad, ~1 s:
    12 instruction files (1 root), 4 check surfaces, risk low
      build: bazel (toolchain)
      test: bazel (declared)
      lint: pre-commit (declared)
      typecheck: none found
      findings: 0 error, 0 warning, 54 info
    
    The 54 files over 100 KB match an earlier hand count on oneDAL. Pre-commit counts as lint, so a pre-commit-only repo isn't reported as having no lint.
  • tests/test_repo_environment.py, 25 tests, on both WorkTree and GitTree:
    • instruction rules
    • per-root checks, including a fixture package.json that is not counted as a project
    • every ecosystem
    • --ignore-scripts handling, last flag wins
    • capability tiers
    • stage statuses
    • the CLI --fail-on-error exit
  • make ci-local: 19 passed, 0 failed. Changed-line coverage 95%.

Merge Danger

Door: two-way

New subcommand, new package, new artifact kind. Nothing existing changes behaviour. repo_environment.v1 is a new schema, and later changes are additive only under schema_compat.

Blast Radius: contained

Only callers of repo inspect are affected. The single shared touchpoint is one line in the SCHEMA_VERSIONS registry.

🤖 Generated with Claude Code

@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.

Thanks, this is a nice addition. Keeping observations separate from findings, and refusing to ever say 'ready' (with not_observed listing what the tree can't show), makes the report honest about what a static pass can know. The tests cover both WorkTree and GitTree. A few things inline, mostly about the hook-risk path.

continue
for command in commands:
for segment in (s.strip() for s in _HOOK_SPLIT.split(command)):
if _INSTALL.search(segment) and not _ignores_scripts(segment):

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.

I think this misses the most common shape of the risk it's after. From npm ci && npm run build only npm ci gets flagged, but npm run build runs a package.json script from the checked-out branch just as much as a lifecycle script does. The same goes for a SessionStart hook like bash "$CLAUDE_PROJECT_DIR"/.claude/hooks/setup.sh or python tools/bootstrap.py, which is the pattern the Claude Code docs show. Those only get the info-level safety.capability.high-risk, so bootstrap ends up no_known_blocker. Could we treat any automatic hook that runs npm/yarn/pnpm run, make, or a path inside the tree as executing repository code? Worth fixing before merge, since this is the path hook_risks is documented to catch.

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.

Agreed. An automatic hook is now flagged when a segment runs npm/yarn/pnpm/bun run, has make as its command word, or runs a path in the tree: ./…, $CLAUDE_PROJECT_DIR/…, or a tracked file, directly or after an interpreter such as bash, python3 -u or sh -c (634e5d4, 4980d22, 8d422ca). npm ci && npm run build, bash "$CLAUDE_PROJECT_DIR"/.claude/hooks/setup.sh and python tools/bootstrap.py all give safety.agent-hook.executes-repository-code. Prose like echo 'make sure …', cmake, cat package.json and cd ../x is not flagged, and each case has a test.



# Events the harness fires without the agent choosing to run anything.
AUTOMATIC_EVENTS = ("SessionStart", "SessionEnd", "PreCompact", "Notification")

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 UserPromptSubmit, Stop and SubagentStop be in here too? They fire without the agent choosing to run anything: UserPromptSubmit on the first prompt and Stop at the end of every turn. So an install hooked there runs as soon as a session is used, just like one on SessionStart. I can see the case for leaving out PreToolUse/PostToolUse, but an unmatched PostToolUse fires on the first tool call, so a doc note on that choice might be worth it.

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.

Yes. UserPromptSubmit and Stop were added in ac200ce and SubagentStop in 634e5d4, all with tests. §8.1 now lists all seven events. PreToolUse/PostToolUse stay out, since they fire on a tool call the agent chose to make.

if marker is None:
return []
out = [Check("autotools", "build", "toolchain", marker)]
if re.search(r"\b(TESTS|check_PROGRAMS|check_SCRIPTS|check_LTLIBRARIES)\b", scan.text("Makefile.am")):

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 marker can be a nested Makefile.am, but TESTS/check_PROGRAMS is only searched for in the root one. Autotools projects usually keep TESTS = in tests/Makefile.am, so they'd get checks.test.missing, which is an error, and verify shows as blocked. Could this search every Makefile.am, the way _cmake reads every CMakeLists.txt?

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.

_autotools now searches every Makefile.am for TESTS/check_*, the same way _cmake reads every CMakeLists.txt (ac200ce). A test with TESTS = only in tests/Makefile.am covers it (634e5d4).

"instructions.activation-unknown": RuleInfo("info", ("orient",),
"when the harness loads the file depends on its frontmatter"),
"instructions.large": RuleInfo("warning", ("orient",), "instruction file large enough to crowd the context"),
"checks.test.missing": RuleInfo("error", ("bootstrap", "verify"), "no way to run tests found"),

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.

checks.test.missing is the only error-level rule, and it's emitted with confidence: medium. Since --fail-on-error is pitched as a CI gate, a repo whose tests the detector doesn't know about (Meson, SCons, a run_tests.sh) would fail CI on a guess. Is that intended, or should the gate look at confidence too, or should this be a warning until the detector covers more ecosystems?

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.

It shouldn't gate on a guess. When the tree has test files but no known runner, the finding is now checks.test.runner-unknown, a warning with low confidence, so --fail-on-error exits 0 (ac200ce, test in 634e5d4). checks.test.missing stays an error only for a tree with no test files and no runner.

doc = _json(scan, path)
if doc is None:
out.append(Capability(path, "hook", "claude-code", "medium", "not read: invalid, large or a link"))
elif _hook_entries(doc):

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.

A project .claude/settings.json can also run commands through statusLine (type: command) and apiKeyHelper, and enableAllProjectMcpServers turns on every server in .mcp.json without asking. A settings file with only those keys and no hooks produces no capability record right now. Worth recording them alongside hooks?

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.

A project settings file with statusLine of type command, a string apiKeyHelper, or enableAllProjectMcpServers: true now produces a high-risk capability record (hook, hook and mcp respectively) even without hooks (634e5d4; test in cb45bb2).

@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 96.86848% with 30 lines in your changes missing coverage. Please review.
✅ Project coverage is 90.68%. Comparing base (9753cc7) to head (f375794).
⚠️ Report is 27 commits behind head on main.

Files with missing lines Patch % Lines
dokimi/dokimi/repos/environment/risks.py 96.76% 7 Missing and 8 partials ⚠️
dokimi/dokimi/repos/environment/files.py 85.10% 5 Missing and 2 partials ⚠️
dokimi/dokimi/repos/environment/checks.py 98.50% 1 Missing and 2 partials ⚠️
dokimi/dokimi/repos/environment/__init__.py 98.21% 2 Missing ⚠️
dokimi/dokimi/repos/environment/instructions.py 97.14% 1 Missing and 1 partial ⚠️
dokimi/dokimi/commands/repo.py 97.91% 0 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      #66      +/-   ##
==========================================
+ Coverage   90.06%   90.68%   +0.62%     
==========================================
  Files         205      211       +6     
  Lines       24193    25699    +1506     
  Branches     3892     4207     +315     
==========================================
+ Hits        21790    23306    +1516     
+ Misses       1724     1713      -11     
- Partials      679      680       +1     
Files with missing lines Coverage Δ
dokimi/dokimi/artifacts.py 93.75% <ø> (ø)
dokimi/dokimi/repos/l0/adapters.py 90.56% <100.00%> (ø)
dokimi/dokimi/repos/source.py 94.53% <100.00%> (+0.45%) ⬆️
dokimi/dokimi/commands/repo.py 93.65% <97.91%> (+1.00%) ⬆️
dokimi/dokimi/repos/environment/__init__.py 98.21% <98.21%> (ø)
dokimi/dokimi/repos/environment/instructions.py 97.14% <97.14%> (ø)
dokimi/dokimi/repos/environment/checks.py 98.50% <98.50%> (ø)
dokimi/dokimi/repos/environment/files.py 85.10% <85.10%> (ø)
dokimi/dokimi/repos/environment/risks.py 96.76% <96.76%> (ø)

... and 46 files with indirect coverage changes

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

Add `dokimi repo inspect <source>` and the repo_environment.v1 report:
instruction files with their scope and activation, ways to build, test,
lint and type-check per project root (declared vs toolchain default),
launch risks (MCP configs, hooks, plugins, risky package scripts, and
automatic hooks that install with lifecycle scripts on), and the file
inventory with a context-sized large-file threshold.

Static only: no manifest, no LLM call, no repository command run.
Findings carry severity and confidence separately; stages report
no_known_blocker at best, with what the tree cannot show listed under
not_observed. No overall score.

Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
core.fsmonitor in the inspected directory's .git/config was executed by git ls-files.
Pass -c core.fsmonitor=false on every git call; test uses a marker file.

Signed-off-by: Alexander Rybkin <alexander.rybkin@intel.com>
A UTF-8 BOM package.json was parsed as empty, raising checks.test.missing (error).
Strip the BOM in package_scripts and the settings reader; catch RecursionError.

Signed-off-by: Alexander Rybkin <alexander.rybkin@intel.com>
echo "Error: no test specified" && exit 1 was reported as a declared node test,
which suppressed checks.test.missing. Narrow regex; real scripts stay declared.

Signed-off-by: Alexander Rybkin <alexander.rybkin@intel.com>
[^|]* made the curl|sh pattern quadratic; a package.json of repeated curl tokens
hung inspect for minutes. Cap the gap at 200 characters.

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

xaleryb commented Oct 16, 2026

Copy link
Copy Markdown
Contributor

Ran repo inspect at 17487aa on 8 real repos (mattpocock/skills, intel/skills, this repo and five public repos across ecosystems) and about 20 hostile fixtures, with strace on the host. No model call (0 connect), output validates against repo_environment.v1 and is byte-identical across runs on a clean tree, repo scan output is unchanged, a 1 GB sparse file is never read, and the instruction-file rules match the harnesses for root and nested AGENTS.md, CLAUDE.md, GEMINI.md, .claude/CLAUDE.md, Copilot and local files.

Fixed in four commits on this branch (each test fails without its change):

  1. 8b6c98b: for a local directory, git ls-files --others ran the inspected repository's core.fsmonitor, which contradicts "no repository command run" and made files nondeterministic (@rev is unaffected; repo scan on main has the same code). Now -c core.fsmonitor=false.
  2. 9b1ba1c: a BOM package.json was read as empty, giving a false checks.test.missing (error) and exit 1 under --fail-on-error; a BOM settings.json hid the hook risk; deeply nested JSON raised RecursionError.
  3. 4a1f4ca: the npm init placeholder test script was reported as a declared test, which suppressed the error-level rule.
  4. 98b96c2: the curl|sh pattern was quadratic (1 MB of curl took 67 s).

Open, your call: a symlinked instruction file is sized by the link (10 bytes against 400,000; repo budget reports 400,000); .claude-plugin/*.json always rates "risk high" (mattpocock/skills); a manifest that cannot be read is not recorded; a non-UTF-8 file name in a git tree kills the CLI with a traceback (the shared _git also does it on repo scan); Go and Rust toolchain lint and typecheck suppress checks.lint.missing; Cursor .mdc is "activation unknown" and nested .cursor/rules are missed; and #66 and #71 disagree on .claude/CLAUDE.md and on @imports (#71 conflicts with this PR only in repo-evaluation-contract.md; unify the instruction tables, ideally on #71's loader).

@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 98b96c2. The separation of observations from findings, the not_observed list and the no-"ready" stage statuses hold up, and the four fix commits are right. The problems are in the detectors. The only error-level rule, checks.test.missing, fires on ordinary repositories, and --fail-on-error is a CI gate on it. Four of the inline comments I'd fix before merge: the false checks.test.missing, the partial-clone guard, the quadratic regexes and the crashes on odd manifests.

What I ran, on a Linux review host at 98b96c2:

  • make ci-local: 19 passed, 0 failed, 4 skipped (docker, test-latest, deploy, semgrep). 4,392 tests passed. Changed-line coverage 95% (524 lines). The host runs Python 3.13.
  • Reproductions for every code finding, on WorkTree fixtures and on a --filter=blob:none cache clone. No model call anywhere.
  • A mutation run on the new modules: 121 mutants, 94 of which pass tests/test_repo_environment.py.

Findings that cannot be anchored inline

  1. rm -rf dist is reported as safety.destructive (warning). risks.py:122 matches any rm -rf. A package.json with "clean": "rm -rf dist" and "prepare": "husky" gives safety.destructive (warning), safety.install-hook (info), risk: medium, and edit and verify at attention. The rule summary says "deletes broadly or wipes the tree". Suggestion: require a root, ~, $HOME or * target, as the pattern on line 121 does, or give the plain form info severity. In the other direction rm --recursive --force / and git clean -d -f give no signal, while git clean -nfd (a dry run) does.
  2. A project .claude/settings.json whose only hooks are http, agent or prompt entries produces no capability record. _hook_entries (risks.py:86-96) keeps only string command values. UserPromptSubmit: [{"type": "http", "url": "https://x.example/c"}] gives capabilities: [], risk: low. Suggestion: record a capability for any non-empty hooks object and extract the commands separately.
  3. Every file under .claude/hooks/ is high risk (risks.py:107), so .claude/hooks/README.md alone gives safety.capability.high-risk and risk: high.
  4. Docs and fixture. The checks bullet in repo-evaluation-contract.md §8.1 lists the project-root markers without setup.cfg and Pipfile, which PROJECT_MARKERS (checks.py:231) includes. The schema declares generated_at, but the code never emits it; the fixture's value was added by hand. Nit: the root README.md section 3 lists repo scan/reach/run/report and not inspect.

Still unaddressed at this head from the earlier review, so I don't repeat them inline: automatic hooks that run npm run, make or a path in the tree (risks.py:180), UserPromptSubmit/Stop/SubagentStop missing from AUTOMATIC_EVENTS (risks.py:150), the autotools TESTS search in the root Makefile.am only (checks.py:145), and statusLine/apiKeyHelper without hooks (risks.py:113). The error-with-medium-confidence question on checks.test.missing is covered by the first inline comment.

Checked, no issues

  • Schema: repo_environment.v1 validated 400 random file sets and the three fixture variants. Every enum value the code emits validates, and finding ids are unique. schema_compat against origin/main reports OK, and SCHEMA_VERSIONS registers the kind.
  • No repository command run: a repo-config core.fsmonitor, a diff.textconv with .gitattributes, core.pager and core.hooksPath executed nothing. Removing -c core.fsmonitor=false from _git fails the new test.
  • Parity and determinism: WorkTree and GitTree give identical payloads on a repo with spaces, tabs, quotes, newlines and UTF-8 in file names plus symlinks. The output hash is the same across PYTHONHASHSEED 0-3.
  • CLI: a nonexistent source, a file as source, a bogus rev, --large-bytes 0/-1/abc, an unwritable --out and --fail-on-error all exit as intended. ruff, mypy and test_docs_code_refs pass.
  • Repo rules: no schema version change is needed for a new kind, there are no with_docs/without_docs keys involved, no generated artifacts are committed, and the PR is not stacked. The tests run offline.

Comment thread dokimi/dokimi/repos/environment/__init__.py
Comment thread dokimi/dokimi/repos/environment/files.py Outdated
Comment thread dokimi/dokimi/repos/environment/risks.py Outdated
Comment thread dokimi/dokimi/repos/environment/checks.py
Comment thread dokimi/dokimi/repos/environment/checks.py Outdated
Comment thread dokimi/dokimi/repos/environment/risks.py Outdated
Comment thread dokimi/dokimi/repos/environment/files.py Outdated
Comment thread dokimi/tests/test_repo_environment.py
…ndling, and generated files

- THREAD 6: partial clone guard checked wrong git config key (extensions.partialclone
  -> remote.origin.promisor), so blobs were always fetched
- THREAD 8: add ValueError exception handling to JSON parsers in risks.py, checks.py,
  and _pre_commit to prevent crashes on manifests with very large numbers or invalid dates
- THREAD 7: bound quadratic regex patterns with maximum repetitions and lookbehind
  assertions: _OPT limited to {0,8} flags, _NODE_TYPE_TOOL limited to [^\n]{0,100},
  _destructive pattern limited to (-[a-z]*){0,2}, plus non-capturing groups to reduce
  backtracking
- THREAD 9: filter generated/vendored files from build system detection in _cmake,
  _bazel, _dotnet; add node_modules to _GENERATED pattern
- THREAD 11: fix _TEST_FILE regex to require capital before Test/Tests/IT in Java/Kotlin,
  anchor Swift tests to start of path, and limit directory alternative to known dirs
- THREAD 5/3: emit checks.test.missing with low confidence when test files exist but
  runner is unknown, not error-level on guess

Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
…tection, hook events

- THREAD 7: fix _OPT lookbehind placement and linear scan on repeated tokens; bound
  destructive pattern to 5 flags; increase _NODE_TYPE_TOOL to 256 chars for long paths;
  convert all alternations to non-capturing groups
- THREAD 11: fix _TEST_FILE regex to require capital before Test/Tests/IT; keep Swift
  pattern flexible; match test dirs from root only
- THREAD 3/5: add new rule checks.test.runner-unknown (warning) for test files with
  unknown runner; change checks.test.missing to apply only when NO test files AND no runner;
  drop bootstrap stage from checks.test.missing
- THREAD 1: add UserPromptSubmit and Stop to AUTOMATIC_EVENTS for hook risk detection
- THREAD 2: search all Makefile.am files for TESTS/check_PROGRAMS, not just root

Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
…tern, add test coverage

- THREAD 11: fix _TEST_FILE to exclude false positives (Credit.java, Unit.java, etc.)
  by requiring capital before Test/Tests/IT/Spec in Java/Kotlin/C#, anchoring Swift
  patterns properly, and limiting directory alternative to source-code extensions
- THREAD 7: increase _OPT flag limit from {0,8} to {0,32} to match npm with 9+ flags,
  change lookbehind from (?<![\w-]) to (?<![A-Za-z0-9]) to allow 'foo-npm ci'
- Add test coverage for false positives, linearity, odd manifests, vendored files,
  and partial clone boundaries from reviewer's test suite

Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
…ad 4 capabilities, SubagentStop, lookbehind fix

- THREAD 7: fix lookbehind back to (?<![\w-]), reduce {0,32} to {0,16}, add 1MB timing
  test; npm -npm pattern completes under 1s per new test
- THREAD 0: detect npm/yarn/pnpm/bun run, make, and paths as automatic hook risks;
  add parametrized test with run/make/path examples
- THREAD 4: record statusLine.command, apiKeyHelper, enableAllProjectMcpServers in
  capabilities; add test with settings-only config
- THREAD 1/5: add SubagentStop to AUTOMATIC_EVENTS; update docs/repo-evaluation-contract.md
  §8.1; add test for UserPromptSubmit/Stop hook risks
- THREAD 8: add deeply nested YAML test (RecursionError handled)
- THREAD 5: add runner-unknown warning tests for Python/Ruby, --fail-on-error exits 0
- THREAD 2: add nested Makefile.am test
- THREAD 10: strip comments before parsing --ignore-scripts (partial fix)
- THREAD 8: make test-dir matching case-insensitive; add .mjs/.cjs extensions
- Test count: 60 -> 72 tests passing

Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
…_scripts comment strip, add test cases

Items 1-5 from review-66-r3:
1. _MAKE now matches make as command word only, not in prose: r"^(?:\w+=\S*\s+)*(?:sudo\s+)?g?make\b"
   Added negative tests for echo cases.
2. Path check extracts command word via shlex-split, flags only ./ $CLAUDE_PROJECT_DIR or tracked files.
   Added negative tests for cd ../other and cat package.json.
3. Fixed ruff F841 at line 407: drop unused payload assignment, assert on result instead.
4. Removed dead _IGNORE_SCRIPTS; fixed _ignores_scripts to strip comments only after whitespace.
   Added URL-fragment test case.
5. Replies file updated with status for all 12 threads and review points.

Tests: 77 passed, ruff clean, mypy clean.
Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
1. Skip shlex.split for segments over 4096 chars to avoid DOS on unbalanced quotes; fall back to .split(). Adds test that 1 MB hook with unbalanced quote finishes in under 1 s.

2. Restore _OPT repetition bound from {0,8} to {0,16} to detect installs with 9+ options. Adds test that install with 10 options followed by ci is detected.

3. Improve path checking after interpreter: find first non-flag arg, handle -c arguments, and strip quotes. Adds tests for python3 -u, sh -c with path, and bash with $CLAUDE_PROJECT_DIR and unbalanced quote.

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 cb45bb2. The fixes for the false checks.test.missing, the partial-clone guard, the odd-manifest crashes and the _INSTALL/rm/tsc backtracking hold up, and --fail-on-error no longer fails on a guess when test files exist. The new code added since 98b96c2 has its own problems: three more quadratic regexes in checks.py, duplicate finding ids, and a .cc/.h gap in the test-file rule that turns a warning back into an error. Four of my earlier threads stay open, mostly on the tests.

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

  • make ci-local: 19 passed, 0 failed, 4 skipped (docker, test-latest, deploy, semgrep). 4,443 tests passed. Changed-line coverage 96% (589 lines). The host runs Python 3.13.
  • Reproductions for the code findings, on WorkTree and GitTree fixtures and on a --filter=blob:none clone. No model call anywhere.
  • A mutation run on the new modules: 222 mutants, 157 of which pass tests/test_repo_environment.py (85 tests).

Still open from my earlier review:

  • checks.py:185,201 (vendored build files): _cmake, _bazel and _dotnet are fixed, but _gradle, _maven, _autotools (checks.py:144-151) and the conftest.py rule in _python (checks.py:177) still read vendored paths. A repo with AGENTS.md and no tests, plus vendor/lib/pom.xml or node_modules/rn/android/build.gradle, gets checks.test.toolchain-only (info) instead of checks.test.missing. configure.ac + vendor/zlib/Makefile.am with TESTS = t gets autotools test declared with that evidence. pyproject.toml + third_party/x/tests/conftest.py gets python test declared. Apply is_generated in those four readers. _autotools also names am_files[0] as evidence, which is the first Makefile.am in sorted order, not the one that has TESTS.
  • risks.py:210-221 (install and run detection): the text is still matched anywhere in the segment, and the segment is cut at ; and && inside quotes. A Stop hook echo 'tests passed; run npm run build' reports hook_risks[0].command == "run npm run build'", a warning, and bootstrap at attention. # npm install is slow, grep -r 'npm ci' docs and git commit -m 'npm ci' are flagged too. Options with a separate value (npm --prefix web ci) and bare yarn are still missed. You deferred the tokenizer to a follow-up; the new run branch repeats the shape, so it should come with it.
  • risks.py:167 (backtracking): the _INSTALL, rm and tsc patterns are fixed, but the two tests that claim to pin it never reach the code. test_npm_install_1mb_completes_quickly and test_unbalanced_quote_in_hook_completes_quickly build settings files of 1,000,082 and 1,000,084 bytes, over MAX_SETTINGS_BYTES (1,000,000). _json returns None, the capability note is not read: invalid, large or a link, and neither test asserts otherwise. Run against the 98b96c2 risks.py with the quadratic _OPT, both pass in 0.03 s. With a payload under the cap ("npm " + "-npm " * 150_000, 750 KB), the PR code takes 0.09 s, so the fix is fine and the tests need the smaller size and an assertion that the note starts with hooks on.
  • tests/test_repo_environment.py (test strength): 157 of 222 mutants pass. What is now covered: the blob-less clone, the odd manifests, the BOM, --fail-on-error exit 0. Not covered, each of these mutants passes the suite:
    • AUTOMATIC_EVENTS: dropping UserPromptSubmit, Stop, PreCompact or Notification. test_user_prompt_submit_and_stop_hook_risks ends in ... or any("npm" in h["command"] ...), which is true whenever any hook is flagged, and SubagentStop is only checked by in AUTOMATIC_EVENTS.
    • _SCRIPT_PATTERNS: dropping git clean, mkfs|dd, wget, vercel/netlify/firebase, gh release, kubectl, terraform, aws s3, gcloud, the per-category seen limit, and preinstall/install/prepare/prepublish from _INSTALL_HOOKS.
    • instructions.RULES: .claude/CLAUDE.md, .github/copilot-instructions.md, .github/agents, .windsurf/rules, .roomodes, .claude/rules, the legacy flags and the skills scope.
    • The three risk tiers (scripts only, unreadable settings only, .mcp.json only), the checks.typecheck.missing and instructions.large findings.
    • CLI: --large-bytes not passed through, --json ignored or unstamped, exit 1 without --fail-on-error; the > vs >= on the large threshold; the yarn.lock, _pb2.py, deps/ and .bundle. patterns.
    • risks._json: the symlink check, the 1 MB cap, and RecursionError.
    • statusLine of any type, apiKeyHelper of any value, enableAllProjectMcpServers truthy but not True.
      Tests to add: one parametrized row per script pattern, per hook event (with an exact {(event, "npm ci")} assertion and a PostToolUse negative), per instruction rule (path, harnesses, scope, activation), and three tiny repos for the risk tiers. You said you would add these with the follow-up; the thread stays open until then.

Findings that cannot be anchored inline

  1. rm -rf dist is still reported as safety.destructive (warning). risks.py:138 matches any rm -rf target. A package.json with "clean": "rm -rf dist" gives the warning and puts edit and verify at attention. Suggestion: require a root, ~, $HOME or * target, or make the plain form info.
  2. A project .claude/settings.json whose only hooks are http, prompt or agent entries still produces no capability record. _hook_entries (risks.py:86) keeps only string command values. UserPromptSubmit: [{"type": "http", "url": "https://x.example/c"}] gives capabilities: [] and risk: low.
  3. The rule summary for safety.agent-hook.executes-repository-code is stale. __init__.py:55-56 still reads "a hook the harness runs on its own installs with lifecycle scripts on". The rule now also fires for npm run, make and paths in the tree, and the contract §8.1 was updated but this text was not.
  4. Nit: every file under .claude/hooks/ is still high risk (risks.py:107), so .claude/hooks/README.md alone gives risk: high.
  5. Nit: docs and fixture. §8.1 lists the project-root markers without setup.cfg and Pipfile, which PROJECT_MARKERS (checks.py:236) includes. It does not mention statusLine, apiKeyHelper or enableAllProjectMcpServers as capabilities. The schema declares generated_at, which the code never emits; the fixture's value was added by hand, and the fixture lacks the runner-unknown finding and the new capability records. The root README.md repo section lists scan, reach and run but not inspect.

Checked, no issues

  • Schema and determinism: payloads validate against repo_environment.v1 for the new capability kinds, mcp, runner-unknown, missing and toolchain-only; schema_compat against origin/main reports OK; output is byte-identical across runs with a randomised hash seed.
  • CLI: --fail-on-error exits 1 on checks.test.missing and 0 on runner-unknown; --large-bytes 0/-1/abc, a missing source, a file as source and an unwritable --out all exit cleanly with no traceback.
  • Odd manifests: 5001-digit integers, 100,000-deep JSON and YAML, rev: 2020-02-30, NUL bytes, invalid UTF-8 and a 3 MB package.json raise nothing in the JSON and YAML readers.
  • Hook patterns: _INSTALL, _RUN_COMMAND, _MAKE, _SCRIPT_PATTERNS and the _NODE_* regexes finish under 0.5 s on 1 MB of repeated tokens; at 98b96c2 three of them took over 15 s.
  • Repo rules: no schema version change is needed for a new kind, there are no with_docs/without_docs keys, no generated artifacts are committed, no nav entry is needed, the PR is not stacked, and the tests run offline. ruff, mypy and mkdocs build pass.

Comment thread dokimi/dokimi/repos/environment/checks.py Outdated
Comment thread dokimi/dokimi/repos/environment/__init__.py Outdated
Comment thread dokimi/dokimi/repos/environment/files.py
Comment thread dokimi/dokimi/repos/environment/checks.py
Comment thread dokimi/dokimi/repos/environment/risks.py Outdated
Comment thread dokimi/dokimi/repos/environment/checks.py
Comment thread dokimi/dokimi/repos/environment/risks.py Outdated
Comment thread dokimi/dokimi/repos/environment/files.py Outdated
claude and others added 3 commits October 16, 2026 22:42
Keep main's process.git-based _run_git and add the branch's
core.fsmonitor=false to it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RW8mAicb3ibXdzHMV83wDn
Signed-off-by: Claude <noreply@anthropic.com>
- linear _INI_SECTION, gradle plugins and maven artifactId readers
- distinct finding ids for each settings capability; statusLine and
  apiKeyHelper are kind 'command'
- C/C++ headers and more languages count as test files under tests/;
  __init__.py does not; is_test_file replaces the matcher class
- nested-only CMake projects are found; dead is_generated guards dropped
- hook commands: npm test/start, yarn <script>, wrappers (VAR=, sudo,
  timeout, env, source, .), interpreter option values and sh -c are read
- manifests of all project roots are fetched in one request
- review labels removed from code and tests; changelog fragment added

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RW8mAicb3ibXdzHMV83wDn
Signed-off-by: Claude <noreply@anthropic.com>
`repo inspect` sized an instruction file with `lstat` (or the link blob in
a git tree), so an `AGENTS.md` that links to a 300 KB file was reported as
14 bytes and `instructions.large` never fired, while `repo budget` counts
the target. A harness reads the target, so the instruction record now
carries the target's size; `files` still lists the link as it is.

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

xaleryb commented Oct 17, 2026

Copy link
Copy Markdown
Contributor

Re-ran at e71a002 on mattpocock/skills, oven-sh/bun and hostile fixtures; our four commits are intact and each test still fails without its fix. Holding: linear INI/gradle/maven patterns, unique capability ids, .cc/.h test files, nested-only CMake, wrapper hooks, one fetch on a blob-less clone, and about 35 hostile hook commands in 0.03 s or less each.

  • A linked instruction file was sized by the link (AGENTS.md -> 400 KB file: 11 bytes, no instructions.large; repo budget reports 400,000). Pushed df86df3: sized by the file it names, on a work tree and a git tree.
  • Still reproducing from the earlier rounds: vendored pom.xml/build.gradle/Makefile.am/conftest.py count as the repo's checks; both "1 MB" hook tests use 1,000,082-byte files, over MAX_SETTINGS_BYTES, so the pattern is never reached; .claude-plugin/*.json is still "risk high" on mattpocock/skills; a non-UTF-8 or dangling package.json leaves no trace; a non-UTF-8 file name in a git tree still ends in a UnicodeDecodeError traceback (shared _git, also hits repo budget).
  • Against feat(repo): repo budget, what a repository puts into an agent's context #71 on the same repos: instructions never lists claude-code for AGENTS.md, though the claude-code profile and contract §6 say 2.1.286 reads it when there is no CLAUDE.md, and repo budget counts it at start (dokimi itself: 5,914 chars); a file a root CLAUDE.md @imports has no record here while feat(repo): repo budget, what a repository puts into an agent's context #71 counts it at start; .claude/rules/*.md is "unknown" here and absent there.

git lists such a name as raw bytes and the strict decode of the whole listing
raised UnicodeDecodeError in `repo inspect`, `repo scan` and `repo budget`, on a
work tree and at a revision. Each entry is now decoded on its own; one that is
not UTF-8 is left out with a warning on stderr.

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

Both settings files were 1,000,082+ bytes, over MAX_SETTINGS_BYTES, so the
file was never parsed and the hook patterns never ran. The commands are now
750 KB and 999 KB, and each test asserts the hook was read.

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

xaleryb commented Oct 17, 2026

Copy link
Copy Markdown
Contributor

Pushed two commits:

  • 76b742b: git lists a non-UTF-8 file name as raw bytes, and the strict decode of the whole listing raised UnicodeDecodeError in repo inspect, repo scan and repo budget, on a work tree and at a revision. Each entry is now decoded on its own, and one that is not UTF-8 is skipped with a warning on stderr.
  • dd307a1: both "1 MB" slow-hook tests wrote settings files over MAX_SETTINGS_BYTES, so the file was never parsed and the hook pattern never ran. The payloads are now 750 KB and 999 KB, and each test asserts the hook was read.

Still open, for you to decide: vendored pom.xml/build.gradle/Makefile.am/conftest.py count as the repo's checks; .claude-plugin/*.json is "risk high" on mattpocock/skills; a non-UTF-8 or dangling package.json leaves no trace; and the instruction-file disagreements with #71 listed above.

Copilot AI balanced review requested due to automatic review settings October 19, 2026 14:09
@napetrov
napetrov force-pushed the repo-environment-inspect branch from e7b215f to c8c857b Compare October 19, 2026 14:09
…es, repo scripts, test files

- command -v/-V is a lookup, not command execution, so no hook risk
- Heredoc and here-string bodies fed to shells are parsed as commands
- Add npm install aliases (cit, it, install-test, ic, install-ci-test)
- Add yarn/pnpm/bun builtins (fetch, self-update, setup, plugin, pm, check, licenses, npm)
- Relative paths and $CLAUDE_PROJECT_DIR/ paths are repo code, not wrappers
- Test files in Julia, R, Clojure, Perl, Erlang, Elixir, and PHP are recognized
- Add tests for all five fixes

Resolves threads on PR intel#66.

Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
@napetrov
napetrov force-pushed the repo-environment-inspect branch from c8c857b to 5be75e3 Compare October 19, 2026 14:09

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.

Warning

Copilot couldn't run its full agentic review because it didn't start before the timeout. Make sure your repository has a runner available, or add a copilot-code-review.yml file specifying one with the runs-on attribute. See the docs for more details.

Copilot review overview

3 open issues
What changed in this PR

Adds a new dokimi repo inspect command that statically inspects a repository (no manifest / no LLM) and emits a new repo_environment.v1 artifact describing instruction files, check surfaces, launch risks, and file inventory.

Changes:

  • Introduces repo inspect CLI subcommand and registers new repo_environment artifact + schema.
  • Adds dokimi.repos.environment implementation (instructions/checks/risks/files) and a committed fixture.
  • Hardens git tree reading (fsmonitor disabled; skip non‑UTF‑8 filenames; avoid quadratic Makefile parsing).
File Description
dokimi/​tests/​test_repo_environment.py Adds comprehensive tests for repo inspect behavior across trees and edge cases.
dokimi/​tests/​fixtures/​artifacts/​repo_environment.v1.json Adds a golden fixture for the emitted repo_environment.v1 artifact.
dokimi/​dokimi/​schemas/​repo_environment.v1.json Defines the JSON schema for the new repo_environment.v1 artifact.
dokimi/​dokimi/​repos/​source.py Adds _git_entries and disables core.fsmonitor; skips/report non‑UTF‑8 filenames.
dokimi/​dokimi/​repos/​l0/​adapters.py Fixes Makefile rule regex to avoid quadratic behavior.
dokimi/​dokimi/​repos/​environment/​risks.py Implements capability detection, script signals, and hook-risk parsing.
dokimi/​dokimi/​repos/​environment/​instructions.py Implements harvest of instruction files by harness rules/scope/activation.
dokimi/​dokimi/​repos/​environment/​files.py Implements file inventory: generated/minified/test/large detection and sizing.
dokimi/​dokimi/​repos/​environment/​checks.py Implements static detection of build/test/lint/typecheck surfaces per root/ecosystem.
dokimi/​dokimi/​repos/​environment/​__init__.py Wires environment detectors into inspect_repo and generates findings/stages/risk.
dokimi/​dokimi/​commands/​repo.py Adds repo inspect subcommand, output formatting, and --fail-on-error.
dokimi/​dokimi/​artifacts.py Registers new repo_environment artifact kind and updates module docs.
dokimi/​docs/​repo-evaluation-contract.md Documents repo inspect and the repo_environment.v1 contract.
dokimi/​docs/​architecture.md Adds repos/environment to architecture overview.
dokimi/​changelog.d/​20261016_224500_napetrov_repo-environment-inspect.md Changelog entry for the new subcommand/artifact.
README.md Mentions repo inspect in the command overview.

Review effort: Lite

elif path.startswith(".claude/skills/") and path.endswith("/SKILL.md"):
out.append(Capability(path, "skill", "claude-code", "low"))
elif path.startswith(".claude/hooks/"):
out.append(Capability(path, "hook", "claude-code", "high", "hook script"))

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 093b6b9. The conditional expression is now refactored into explicit statements: initialize entries = [doc], then conditionally extend with plugins that are dicts. The logic is unchanged but now clear at a glance.

Comment thread dokimi/dokimi/repos/source.py Outdated
Comment on lines 435 to 445
raise SourceError(f"git {args[0]} failed: {exc}") from exc
if proc.returncode != 0 and check:
detail = proc.stderr.strip().splitlines()[-1:] or [f"exit {proc.returncode}"]
raise SourceError(f"git {args[0]} failed: {detail[0]}")
return proc.stdout


def _git_bytes(git_dir: Path, args: list[str], stdin: bytes) -> bytes:
"""``git -C git_dir *args`` with binary stdin and stdout."""
return _run_git(git_dir, args, stdin, True).stdout_bytes


def _run_git(git_dir: Path | None, args: list[str], stdin: bytes | None, check: bool) -> process.Completed:
where = ["-C", str(git_dir)] if git_dir is not None else []
env = {**os.environ, "GIT_TERMINAL_PROMPT": "0"}
try:

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.

This warning is diagnostic output about system state (non-UTF-8 file names in git), not about the library's own behavior. It's only emitted when warn=True is explicitly passed, giving callers full control over when it appears. For edge-case diagnostics at this level, stderr is appropriate. If needed in the future, callers can capture or suppress stderr; for now, the explicit parameter gives sufficient control.

Comment thread dokimi/dokimi/commands/repo.py Outdated
"inspect",
help="Static inspection of what the tree offers an agent: instruction files and their scope, ways to "
"build and check a change, launch risks (hooks, MCP, risky scripts), file inventory "
"(repo_environment.v1). No manifest, no LLM call, no repository command run.",

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 093b6b9. The help text now says 'no repository build/test/script commands run' to make clear that git operations (which read the tree) are not included, only user-controlled workflows. This removes ambiguity.

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.

Comment thread dokimi/dokimi/commands/repo.py Outdated
for f in payload["findings"]:
if f["severity"] != "info":
where = f" {f['path']}" if f["path"] else ""
print(f" {f['severity'].upper()} {f['rule']}{where} -- {f['why'][:100]}")

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 093b6b9. Added _normalize_whitespace() helper that replaces newlines and carriage returns with spaces before truncating. Now a finding with 'hook command\nspread across\nlines' renders as 'hook command spread across lines -- ...' instead of breaking the line, and JSON output is unaffected.

Comment on lines +179 to +185
not_observed = list(NOT_OBSERVED)
if size_of is None:
not_observed.append("file sizes (a partial clone: listing them would fetch every blob)")
for manifest in unreadable_plugin_manifests(scan):
not_observed.append(f"{manifest} hooks and MCP servers (the file is unreadable or not a JSON object)")
if package_json_unreadable(scan):
not_observed.append("package.json scripts (the file is unreadable or not a JSON object)")

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 093b6b9. Added unreadable_claude_settings() function in risks.py, used in inspect_repo to add 'X automatic hooks (the file is unreadable...)' to not_observed when .claude/settings.json or .claude/settings.local.json cannot be parsed. Tests confirm unreadable settings are now disclosed while readable ones add no entry.


def _declares_code(doc: dict[str, Any]) -> list[str]:
"""Manifest keys that bring hooks or MCP servers (inline, or a path such as ``./hooks/hooks.json``)."""
entries = [doc] + [p for p in doc.get("plugins", []) if isinstance(p, dict)] if isinstance(doc.get("plugins"), list) else [doc]

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 093b6b9 (same as the earlier comment on this line). The conditional expression is now refactored into explicit statements for clarity.

Copilot AI balanced review requested due to automatic review settings October 19, 2026 14:59
- Refactor _declares_code conditional to explicit statements for readability
- Normalize whitespace in CLI output to prevent multiline findings
- Disclose unreadable .claude/settings.json in not_observed
- Clarify help text that repo inspect runs no user commands
- Add tests for unreadable settings disclosure

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

Copilot AI left a comment

Copy link
Copy Markdown

Comment thread dokimi/dokimi/repos/environment/risks.py
Comment thread dokimi/tests/test_repo_environment.py
Comment thread dokimi/tests/test_repo_environment.py
Comment thread dokimi/tests/test_repo_environment.py Outdated
Comment thread dokimi/tests/test_repo_environment.py Outdated

@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 093b6b9. The fixes for command -v, the wrapper-named repository scripts, the package-manager tables and the new test-file extensions hold. The heredoc and here-string code added in 5be75e3 is the problem: it hangs on a hook of a few tens of KB and gets the shell-input cases wrong in both directions. _MAKE_RULE is still quadratic, and the tests still miss much of what they claim to pin. The inline comments on risks.py are the ones I'd fix before merge.

What I ran, on a Linux review host at 093b6b9:

  • make ci-local: 19 passed, 0 failed, 4 skipped (docker, test-latest, deploy, semgrep). Changed-line coverage 97% (900 lines).
  • Reproductions for the code findings through inspect_repo and _segments on WorkTree fixtures, with no model call.
  • A mutation run on the new modules and the inspect handler: 252 mutants, 162 of which pass tests/test_repo_environment.py (375 tests).
  • Every command from my four open risks.py threads on a SessionStart hook.

Still open from my earlier review:

  • adapters.py:55 (_MAKE_RULE, used by checks.py:106): still quadratic in a run of spaces or tabs, and test_makefile_rules_are_linear covers only "a\n" * n. See below.
  • tests/test_repo_environment.py (test strength): the script, event, instruction and risk-tier tables are in, but 162 of 252 mutants still pass. See below.
  • __init__.py:140 (checks.test.missing): the test-file extensions are added, but a Meson, justfile or Taskfile repo with a test target, or a Makefile whose test: comes from an include, still gets the error and the text "no test files and no way to run tests found".

1. _MAKE_RULE is still quadratic on a long whitespace run. The pattern is ^([^\s:#=][^:#=\n]*?)[ \t]*::?(?!=) (adapters.py:55). The lazy group and [ \t]* overlap on spaces and tabs. _MAKE_RULE.finditer on "a" + " " * n:

  • 20,001 chars: 0.29 s
  • 40,001 chars: 1.19 s
  • 80,001 chars: 4.85 s

Time quadruples when the input doubles. A 160 KB Makefile ("a" + " " * 160000) takes 13 s through inspect_repo, and checks._make reads Makefiles up to 2 MB. Suggestion: match [^:#=\n]* greedily and rstrip() the group, or scan the Makefile line by line. Add "a" + " " * 100_000 and a tab variant to test_makefile_rules_are_linear.

2. Test strength. 162 of 252 mutants pass the full file. Each of these passes:

  • _normalize_whitespace: [\r\n]+ to \n+, dropping .strip(), dropping the [:max_len] cut, max_len 100 to 50. The helper has no test.
  • CLI: sys.exit(1) to sys.exit(0) on a SourceError and on an unwritable --out; if args.json: to if False:; the text summary lines.
  • .claude/settings.local.json: CLAUDE_SETTINGS = (".claude/settings.json",), and the same cut inside unreadable_claude_settings. The new not-observed tests cover settings.json only.
  • Hostile strings: removing the rm, git clean or tsc bound. The rm and git clean rows use 22 repetitions (about 1 s unbounded, under the 2 s limit), and the tsc row is a script named typecheck, so _NODE_TYPE_NAME matches first and _NODE_TYPE_TOOL never runs. Use 30 or more repetitions and name the script check:x. _INI_SECTION has no row.
  • __init__.py: the checks.typecheck.missing guard to False, and to or in the toolchain-only condition, > to >= at exactly 100,000 and 200,000 bytes.
  • risks._json: the symlink check, the 1 MB cap, the RecursionError catch; the .cursor/mcp.json and .vscode/mcp.json rows; 14 of 15 _LOCKFILES names; go.mod, Cargo.toml, Pipfile, setup.py, setup.cfg in PROJECT_MARKERS.
  • Hook tables: pnpm setup, pnpm licenses, yarn npm login and npm install-ci-test rows (dropping those words passes), the _PM_VALUE_OPTS and _PM_FLAGS entries, _MAX_SHELL_DEPTH, time/doas/stdbuf wrappers, gmake.
  • test_deeply_nested_yaml_does_not_crash asserts payload is not None, and test_command_v_lookup_not_flagged has a flagged parameter that is always False.

Tests to add: a (command, flagged) row per table word, the three CLI cases, rows at exactly LARGE_BYTES and LARGE_BYTES + 1, and the larger hostile strings above.

Checked, no issues

  • Earlier hook threads: command -v and -V (five forms, with and without redirection), npm cit/it/install-test, pnpm fetch/self-update, yarn plugin import x, bun pm ls, yarn check, and ten repository scripts named like wrappers (./scripts/env, ./bin/python, ./hooks/npm, $CLAUDE_PROJECT_DIR/.claude/hooks/env, tracked scripts/env) all give the answer I asked for. bash <<EOF, cat <<EOF | sh and bash <<< 'npm ci' are flagged, and cat <<EOF alone is not.
  • Test files: Julia, R, Clojure, Perl, Erlang, PHP, Elixir, ML, Zig and Nim files each give checks.test.runner-unknown, a warning, so --fail-on-error exits 0. a/t/data.txt does not count.
  • Unreadable settings: invalid JSON, [], empty, over 1 MB and a symlink each add one not_observed line per file; a valid object adds none.
  • Repo rules: the changelog fragment is present, repo_environment.v1 is registered, schema_compat passes, ruff and mkdocs build pass, and the tests run offline.

Comment thread dokimi/dokimi/repos/environment/risks.py Outdated
Comment thread dokimi/dokimi/repos/environment/risks.py Outdated
Comment thread dokimi/dokimi/repos/environment/risks.py Outdated
Comment thread dokimi/dokimi/repos/environment/files.py Outdated
Comment thread dokimi/docs/repo-evaluation-contract.md Outdated
Comment thread dokimi/dokimi/commands/repo.py Outdated
Comment thread dokimi/dokimi/repos/environment/__init__.py Outdated
napetrov and others added 6 commits October 19, 2026 13:20
…eads them

Here-strings were scanned in the whole text, so `grep <<< 'npm ci'` was flagged and
`bash <<<'npm ci'` was not. Heredoc bodies were found with a per-label regex that scanned
to the end of the text for every unclosed start, which is quadratic. A heredoc body is now
read only when a shell that reads stdin is fed it, found in one pass, and `<< EOF` is
recognised. `2>&1` keeps its fd digit. Test directories count only at the root for `t/`,
CLI output escapes control characters, and the repo-environment tests no longer mutate
os.environ or assume POSIX separators.

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

The tests let 94 of 121 mutants of the detector through. Add one table row per
script pattern, install and run form, ignore-scripts form, capability path,
settings and plugin file, instruction rule, lockfile and minified pattern, and
size threshold, asserting the exact classification. The 1 MB settings limit and
the 200 KB instruction threshold are pinned by value. A linked settings file is
checked as unfollowed. The local-settings hook test writes its files without
git because a common global ignore rule drops settings.local.json.

Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
The lazy rule group and the [ \t]* after it overlap, so a Makefile line of
"a" and 100,000 spaces or tabs took seconds in repo inspect and repo scan.
A greedy group finds the same targets: callers split it.

Signed-off-by: Alexander Rybkin <alexander.rybkin@intel.com>
A comment now starts only at the start of a word, so the "#" rows pass
even when quotes are not tracked. Two rows with a blank before the quoted
"#" fail if they are not, and the npm ci after them is lost.

Signed-off-by: Alexander Rybkin <alexander.rybkin@intel.com>
At 22 options the rm and git clean patterns finish under the 2 s limit
with no bound, and a script named typecheck never reaches the tsc pattern.
28 options and a script named check:x make each removed bound fail.

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

Printing nothing for --json, or exiting 0 on a missing source or an
unwritable --out, left the whole file green.

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

xaleryb commented Oct 20, 2026

Copy link
Copy Markdown
Contributor

Re-checked at bbce375: our ten earlier commits, every reviewer thread and the hook/heredoc changes made since then. The full suite, ruff, mypy and bandit pass on the head and on its merge with main. Each earlier fix's test still fails without that fix.

  • Pushed:
    • e7fe26b: _MAKE_RULE was still quadratic on a run of blanks ("a" + " " * 80_000: 3.8 s; tabs: 5.5 s). It is now a greedy group and finds the same targets.
    • 2211e7d: rows for a quoted # after a blank. The existing '#' rows still pass when quote tracking is removed.
    • 3554e9a: the hostile-string rows now fail when the rm, git clean or tsc bound is removed.
    • e1b04dc: tests for --json output and for exit 1 on a missing source or an unwritable --out.
  • Left for the author:
    • AGENTS.md plus a justfile with test: and no test files → checks.test.missing and exit 1. This is the deferred runner follow-up.
    • A nested sub/.cursor/rules/*.mdc gets no instruction record.

@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 e1b04dc. Every earlier item of ours and every reviewer thread is fixed or answered at this head; our commits pin what was left, each red without its code. 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 e1b04dc. The _MAKE_RULE fix, the heredoc and here-string rework, the 2>&1 merge, the settings not-observed line and the ^t/ anchor all hold. Two things are worth fixing before merge, both in the heredoc code, and one earlier thread stays open. Two nits inline.

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

  • make ci-local: 19 passed, 0 failed, 4 skipped (docker, test-latest, deploy, semgrep). 5,617 tests passed. Changed-line coverage 98% (944 lines). Python 3.13 only, so the 3.11, 3.12 and 3.14 legs did not run.
  • Reproductions for the code findings through risks._segments, WorkTree and inspect_repo, with no model call.

Still open from my earlier review:

  • __init__.py:141 (checks.test.missing): the test-file extensions are fixed, but a repo with AGENTS.md and a justfile with a test: recipe, a Taskfile.yml with a test task, a meson.build with test(...), or a Makefile whose test: comes from an include still gets checks.test.missing (error) and --fail-on-error exits 1. The text still reads "no test files and no way to run tests found". This was deferred to a follow-up. Until then, either make the rule a warning when a justfile, Taskfile, meson.build or an include is present, or word the finding as "no runner this pass can read".

I agree with @xaleryb on the nested .cursor/rules/*.mdc gap.

Checked, no issues

  • _MAKE_RULE: "a" + " " * 160_000, the tab variant, "a\n" * 80_000 and "a b " * 80_000 each finish in 0.005 s or less, and all test : / foo:: still give the targets all, test, foo; a := b gives none.
  • Heredocs and here-strings: bash <<EOF, bash << EOF, bash <<-EOF with a tab-indented terminator, bash <<'EOF', cat <<EOF | sh, bash <<<'npm ci', bash <<< 'echo a; npm ci', bash <<< 'npm ci' >/dev/null, bash -c 'x' <<EOF (not flagged), bash -s <<EOF and two heredocs on one line give the expected answer. grep -q ci <<< 'npm ci' and a lone cat <<EOF are not flagged.
  • Redirections: 2>&1, 2>&-, >&2, &>file do not split the command or hide npm ci.
  • Test files: src/t/types.go, docs/t/x.r, lib/R/t/util.R, a/t/data.txt and tests/__init__.py are not test files; t/basic.t and test/a.jl are.
  • Thresholds: a 100,000-byte file gives no files.large and 100,001 does; a 200,000-byte AGENTS.md gives no instructions.large and 200,001 does.
  • Repo rules: the schema kind is registered, schema_compat passes, the changelog fragment is present, no THREAD n labels remain, no generated artifact is committed, the PR is not stacked, and the tests run offline.

Comment on lines +523 to +525
if k == len(ends):
break
body = "\n".join(lines[s + 1:ends[k]])

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.

_heredoc_bodies is quadratic when several heredocs open on one line, so a hook of about 20 KB stalls repo inspect for seconds. Every body is lines[s + 1:ends[k]] from the line that holds the <<L0 <<L1 … starts, so label i gets a body of about i lines. The bodies add up to O(n²) lines, and _segments re-parses each one (risks.py:435). The docstring says "linear, not quadratic", and test_unclosed_heredocs_are_linear covers only repeated starts with no terminator.

risks._segments("bash " + " ".join(f"<<L{i}" for i in range(n)) + "\n" + "".join(f"echo x{i}\nL{i}\n" for i in range(n))):

  • n=250 (5,175 chars): 62,501 segments, 0.27 s
  • n=500 (10,675 chars): 250,001 segments, 1.19 s
  • n=1000 (21,675 chars): 1,000,001 segments, 5.89 s

Time and memory quadruple when n doubles, and MAX_SETTINGS_BYTES allows 1 MB. A repository can commit this as a SessionStart hook.

Suggestion: a body starts after the line that carries all the starts of that command, so several heredocs on one line read their bodies in sequence: the first label's body ends at its terminator, the next one starts after it. Cap the number of heredoc bodies per command (for example 16). Add this shape to the linear-time test.

if (next_i < len(out[k + 1]) and out[k + 1][next_i].rsplit("/", 1)[-1] in _SHELLS
and _reads_stdin(out[k + 1], next_i)):
shell_labels += _heredoc_labels(argv)
for body in _heredoc_bodies(text, set(shell_labels)):

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.

A heredoc is matched by its label alone, so cat <<EOF is read as commands when a shell heredoc uses the same label. shell_labels holds names, and _heredoc_bodies(text, set(shell_labels)) then takes the body of every <<EOF in the text, including those that belong to cat.

_segments on a hook, looking for an npm ci segment:

  • bash <<EOF\nx\nEOF\ncat <<EOF\nnpm ci\nEOF -> ['npm', 'ci'] found
  • cat <<EOF\nnpm ci\nEOF\nbash <<EOF\nx\nEOF -> ['npm', 'ci'] found
  • cat <<EOF\nnpm ci\nEOF alone -> nothing

A hook that writes a script with cat <<EOF and also runs bash <<EOF gets a warning and bootstrap at attention for text that only prints. EOF is the usual label in both.

Suggestion: carry the position of the shell's start, not just the label, and read the body only for that start (the n-th <<LABEL in the text, counted in order). Add the two orders above as (command, flagged) rows with cat <<EOF as the unflagged half.

Comment thread dokimi/dokimi/repos/source.py Outdated
out.append(entry.decode("utf-8"))
except UnicodeDecodeError:
if warn:
print(f"warning: skipped a file name that is not UTF-8: {entry.decode('utf-8', 'backslashreplace')}",

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: the stderr warning prints control characters from the file name raw. decode('utf-8', 'backslashreplace') escapes only the invalid bytes. A work tree with a file named bad\xff\x1b[2Jx\nFAKE.py prints, under cat -v:

warning: skipped a file name that is not UTF-8: bad\xff^[[2Jx
FAKE.py

The ESC byte reaches the terminal and the name forges a second line. repo.py already collapses whitespace and escapes control characters for the same reason on stdout. Suggestion: pass the name through ascii(...) or repr(...) here.

Comment thread dokimi/dokimi/commands/repo.py Outdated
def _normalize_whitespace(text: str, max_len: int | None = 100) -> str:
"""One line: whitespace runs become one space, control characters are escaped, then truncated."""
flat = re.sub(r"\s+", " ", text).strip()
printable = "".join(c if c.isprintable() else f"\\x{ord(c):02x}" for c in flat)

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: \x{ord(c):02x} is ambiguous above 0xff. _normalize_whitespace('a\u200bb') gives a\x200bb, which reads as \x20 then 0b. A bidi override U+202E gives \x202e and a lone surrogate \xd800. The output stays on one line, so nothing breaks, but the escape cannot be read back. Suggestion: use \u{:04x} for code points above 0xff and \U{:08x} above 0xffff, or c.encode('unicode_escape').decode().

Heredocs opened on one line take their bodies in turn, and a body is
read only for the start that feeds a shell, so `cat <<EOF` no longer
shares a body with `bash <<EOF`. Control characters in the whitespace
normaliser and in the non-UTF-8 file-name warning are escaped
unambiguously.
…d may hold the tests

A justfile, Taskfile, meson.build or Makefile include can define the
test runner. Reporting that as checks.test.missing blocked the verify
stage for repos that do have tests.
@napetrov
napetrov merged commit 7f441b2 into intel:main Oct 20, 2026
21 checks passed
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.

7 participants