Repository navigation
repo inspect: static look at what a repository offers an agent (repo_environment.v1) - #66
Conversation
vchamarthi
left a comment
There was a problem hiding this comment.
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): |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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") |
There was a problem hiding this comment.
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.
| 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")): |
There was a problem hiding this comment.
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?
| "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"), |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
| 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): |
There was a problem hiding this comment.
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?
|
Codecov Report❌ Patch coverage is
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
🚀 New features to boost your workflow:
|
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>
c8c857b to
17487aa
Compare
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>
|
Ran Fixed in four commits on this branch (each test fails without its change):
Open, your call: a symlinked instruction file is sized by the link (10 bytes against 400,000; |
antonwolfy
left a comment
There was a problem hiding this comment.
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
WorkTreefixtures and on a--filter=blob:nonecache 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
rm -rf distis reported assafety.destructive(warning).risks.py:122matches anyrm -rf. A package.json with"clean": "rm -rf dist"and"prepare": "husky"givessafety.destructive(warning),safety.install-hook(info),risk: medium, andeditandverifyatattention. The rule summary says "deletes broadly or wipes the tree". Suggestion: require a root,~,$HOMEor*target, as the pattern on line 121 does, or give the plain form info severity. In the other directionrm --recursive --force /andgit clean -d -fgive no signal, whilegit clean -nfd(a dry run) does.- A project
.claude/settings.jsonwhose only hooks arehttp,agentorpromptentries produces no capability record._hook_entries(risks.py:86-96) keeps only stringcommandvalues.UserPromptSubmit: [{"type": "http", "url": "https://x.example/c"}]givescapabilities: [],risk: low. Suggestion: record a capability for any non-emptyhooksobject and extract the commands separately. - Every file under
.claude/hooks/ishighrisk (risks.py:107), so.claude/hooks/README.mdalone givessafety.capability.high-riskandrisk: high. - Docs and fixture. The
checksbullet inrepo-evaluation-contract.md§8.1 lists the project-root markers withoutsetup.cfgandPipfile, whichPROJECT_MARKERS(checks.py:231) includes. The schema declaresgenerated_at, but the code never emits it; the fixture's value was added by hand. Nit: the rootREADME.mdsection 3 listsrepo scan/reach/run/reportand notinspect.
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.v1validated 400 random file sets and the three fixture variants. Every enum value the code emits validates, and finding ids are unique.schema_compatagainstorigin/mainreports OK, andSCHEMA_VERSIONSregisters the kind. - No repository command run: a repo-config
core.fsmonitor, adiff.textconvwith.gitattributes,core.pagerandcore.hooksPathexecuted nothing. Removing-c core.fsmonitor=falsefrom_gitfails the new test. - Parity and determinism:
WorkTreeandGitTreegive identical payloads on a repo with spaces, tabs, quotes, newlines and UTF-8 in file names plus symlinks. The output hash is the same acrossPYTHONHASHSEED0-3. - CLI: a nonexistent source, a file as source, a bogus rev,
--large-bytes 0/-1/abc, an unwritable--outand--fail-on-errorall exit as intended. ruff, mypy andtest_docs_code_refspass. - Repo rules: no schema version change is needed for a new kind, there are no
with_docs/without_docskeys involved, no generated artifacts are committed, and the PR is not stacked. The tests run offline.
…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
left a comment
There was a problem hiding this comment.
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
WorkTreeandGitTreefixtures and on a--filter=blob:noneclone. 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,_bazeland_dotnetare fixed, but_gradle,_maven,_autotools(checks.py:144-151) and theconftest.pyrule in_python(checks.py:177) still read vendored paths. A repo withAGENTS.mdand no tests, plusvendor/lib/pom.xmlornode_modules/rn/android/build.gradle, getschecks.test.toolchain-only(info) instead ofchecks.test.missing.configure.ac+vendor/zlib/Makefile.amwithTESTS = tgetsautotools test declaredwith that evidence.pyproject.toml+third_party/x/tests/conftest.pygetspython test declared. Applyis_generatedin those four readers._autotoolsalso namesam_files[0]as evidence, which is the firstMakefile.amin sorted order, not the one that hasTESTS.risks.py:210-221(install andrundetection): the text is still matched anywhere in the segment, and the segment is cut at;and&&inside quotes. AStophookecho 'tests passed; run npm run build'reportshook_risks[0].command == "run npm run build'", a warning, andbootstrapatattention.# npm install is slow,grep -r 'npm ci' docsandgit commit -m 'npm ci'are flagged too. Options with a separate value (npm --prefix web ci) and bareyarnare still missed. You deferred the tokenizer to a follow-up; the newrunbranch repeats the shape, so it should come with it.risks.py:167(backtracking): the_INSTALL,rmandtscpatterns are fixed, but the two tests that claim to pin it never reach the code.test_npm_install_1mb_completes_quicklyandtest_unbalanced_quote_in_hook_completes_quicklybuild settings files of 1,000,082 and 1,000,084 bytes, overMAX_SETTINGS_BYTES(1,000,000)._jsonreturnsNone, the capability note isnot read: invalid, large or a link, and neither test asserts otherwise. Run against the 98b96c2risks.pywith 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 withhooks 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-errorexit 0. Not covered, each of these mutants passes the suite:AUTOMATIC_EVENTS: droppingUserPromptSubmit,Stop,PreCompactorNotification.test_user_prompt_submit_and_stop_hook_risksends in... or any("npm" in h["command"] ...), which is true whenever any hook is flagged, andSubagentStopis only checked byin AUTOMATIC_EVENTS._SCRIPT_PATTERNS: droppinggit clean,mkfs|dd,wget,vercel/netlify/firebase,gh release,kubectl,terraform,aws s3,gcloud, the per-categoryseenlimit, andpreinstall/install/prepare/prepublishfrom_INSTALL_HOOKS.instructions.RULES:.claude/CLAUDE.md,.github/copilot-instructions.md,.github/agents,.windsurf/rules,.roomodes,.claude/rules, thelegacyflags and the skills scope.- The three
risktiers (scripts only, unreadable settings only,.mcp.jsononly), thechecks.typecheck.missingandinstructions.largefindings. - CLI:
--large-bytesnot passed through,--jsonignored or unstamped, exit 1 without--fail-on-error; the>vs>=on the large threshold; theyarn.lock,_pb2.py,deps/and.bundle.patterns. risks._json: the symlink check, the 1 MB cap, andRecursionError.- statusLine of any
type,apiKeyHelperof any value,enableAllProjectMcpServerstruthy but notTrue.
Tests to add: one parametrized row per script pattern, per hook event (with an exact{(event, "npm ci")}assertion and aPostToolUsenegative), 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
rm -rf distis still reported assafety.destructive(warning).risks.py:138matches anyrm -rftarget. Apackage.jsonwith"clean": "rm -rf dist"gives the warning and putseditandverifyatattention. Suggestion: require a root,~,$HOMEor*target, or make the plain form info.- A project
.claude/settings.jsonwhose only hooks arehttp,promptoragententries still produces no capability record._hook_entries(risks.py:86) keeps only stringcommandvalues.UserPromptSubmit: [{"type": "http", "url": "https://x.example/c"}]givescapabilities: []andrisk: low. - The rule summary for
safety.agent-hook.executes-repository-codeis stale.__init__.py:55-56still reads "a hook the harness runs on its own installs with lifecycle scripts on". The rule now also fires fornpm run,makeand paths in the tree, and the contract §8.1 was updated but this text was not. - Nit: every file under
.claude/hooks/is stillhighrisk (risks.py:107), so.claude/hooks/README.mdalone givesrisk: high. - Nit: docs and fixture. §8.1 lists the project-root markers without
setup.cfgandPipfile, whichPROJECT_MARKERS(checks.py:236) includes. It does not mentionstatusLine,apiKeyHelperorenableAllProjectMcpServersas capabilities. The schema declaresgenerated_at, which the code never emits; the fixture's value was added by hand, and the fixture lacks therunner-unknownfinding and the new capability records. The rootREADME.mdrepo section listsscan,reachandrunbut notinspect.
Checked, no issues
- Schema and determinism: payloads validate against
repo_environment.v1for the new capability kinds,mcp,runner-unknown,missingandtoolchain-only;schema_compatagainstorigin/mainreports OK; output is byte-identical across runs with a randomised hash seed. - CLI:
--fail-on-errorexits 1 onchecks.test.missingand 0 onrunner-unknown;--large-bytes 0/-1/abc, a missing source, a file as source and an unwritable--outall 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 MBpackage.jsonraise nothing in the JSON and YAML readers. - Hook patterns:
_INSTALL,_RUN_COMMAND,_MAKE,_SCRIPT_PATTERNSand 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_docskeys, no generated artifacts are committed, no nav entry is needed, the PR is not stacked, and the tests run offline.ruff,mypyandmkdocs buildpass.
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>
|
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,
|
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>
|
Pushed two commits:
Still open, for you to decide: vendored |
e7b215f to
c8c857b
Compare
…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>
c8c857b to
5be75e3
Compare
There was a problem hiding this comment.
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
The conditional expression that buildsentriesis hard to read and easy to misinterpret due to… · New This utility function prints directly to stderr, which is hard to control in library-style code… · New The help text says \"no repository command run\", butrepo inspectnecessarily invokesgitto… · New
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 inspectCLI subcommand and registers newrepo_environmentartifact + schema. - Adds
dokimi.repos.environmentimplementation (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")) |
There was a problem hiding this comment.
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.
| 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: |
There was a problem hiding this comment.
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.
| "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.", |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
6 open issues
The non-JSON CLI output can be unintentionally multi-line iff['why']contains newlines (e.g., a… · New When.claude/settings.json(or.claude/settings.local.json) exists but is… · New This utility function prints directly to stderr, which is hard to control in library-style code… The conditional expression that buildsentriesis hard to read and easy to misinterpret due to… The conditional expression that buildsentriesis hard to read and easy to misinterpret (operator… · New The help text says \"no repository command run\", butrepo inspectnecessarily invokesgitto…
Review effort: Lite
| 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]}") |
There was a problem hiding this comment.
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.
| 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)") |
There was a problem hiding this comment.
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] |
There was a problem hiding this comment.
Fixed in 093b6b9 (same as the earlier comment on this line). The conditional expression is now refactored into explicit statements for clarity.
- 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>
There was a problem hiding this comment.
Note
Copilot was unable to run its full agentic suite in this review.
Copilot review overview
11 open issues
The tokenizer merges only the&part of redirections like2>&1, but not the following fd (1).… · New The test asserts a tight wall-clock bound (< 1.0s). This is likely to be flaky on… · New The test asserts a tight wall-clock bound (< 1.0s). This is likely to be flaky on… · New This mutatesos.environ["GIT_TRACE"]and unconditionally deletes it afterwards. If the… · New This path normalization is POSIX-specific (... + "/") and will likely fail on Windows where… · New When.claude/settings.json(or.claude/settings.local.json) exists but is… The non-JSON CLI output can be unintentionally multi-line iff['why']contains newlines (e.g., a… This utility function prints directly to stderr, which is hard to control in library-style code… The conditional expression that buildsentriesis hard to read and easy to misinterpret due to… The conditional expression that buildsentriesis hard to read and easy to misinterpret (operator… The help text says \"no repository command run\", butrepo inspectnecessarily invokesgitto…
Review effort: Lite
antonwolfy
left a comment
There was a problem hiding this comment.
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_repoand_segmentsonWorkTreefixtures, with no model call. - A mutation run on the new modules and the
inspecthandler: 252 mutants, 162 of which passtests/test_repo_environment.py(375 tests). - Every command from my four open
risks.pythreads on aSessionStarthook.
Still open from my earlier review:
adapters.py:55(_MAKE_RULE, used bychecks.py:106): still quadratic in a run of spaces or tabs, andtest_makefile_rules_are_linearcovers 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,justfileorTaskfilerepo with atesttarget, or a Makefile whosetest:comes from aninclude, 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_len100 to 50. The helper has no test.- CLI:
sys.exit(1)tosys.exit(0)on aSourceErrorand on an unwritable--out;if args.json:toif False:; the text summary lines. .claude/settings.local.json:CLAUDE_SETTINGS = (".claude/settings.json",), and the same cut insideunreadable_claude_settings. The new not-observed tests coversettings.jsononly.- Hostile strings: removing the
rm,git cleanortscbound. Thermandgit cleanrows use 22 repetitions (about 1 s unbounded, under the 2 s limit), and thetscrow is a script namedtypecheck, so_NODE_TYPE_NAMEmatches first and_NODE_TYPE_TOOLnever runs. Use 30 or more repetitions and name the scriptcheck:x._INI_SECTIONhas no row. __init__.py: thechecks.typecheck.missingguard toFalse,andtoorin thetoolchain-onlycondition,>to>=at exactly 100,000 and 200,000 bytes.risks._json: the symlink check, the 1 MB cap, theRecursionErrorcatch; the.cursor/mcp.jsonand.vscode/mcp.jsonrows; 14 of 15_LOCKFILESnames;go.mod,Cargo.toml,Pipfile,setup.py,setup.cfginPROJECT_MARKERS.- Hook tables:
pnpm setup,pnpm licenses,yarn npm loginandnpm install-ci-testrows (dropping those words passes), the_PM_VALUE_OPTSand_PM_FLAGSentries,_MAX_SHELL_DEPTH,time/doas/stdbufwrappers,gmake. test_deeply_nested_yaml_does_not_crashassertspayload is not None, andtest_command_v_lookup_not_flaggedhas aflaggedparameter that is alwaysFalse.
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 -vand-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, trackedscripts/env) all give the answer I asked for.bash <<EOF,cat <<EOF | shandbash <<< 'npm ci'are flagged, andcat <<EOFalone 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-errorexits 0.a/t/data.txtdoes not count. - Unreadable settings: invalid JSON,
[], empty, over 1 MB and a symlink each add onenot_observedline per file; a valid object adds none. - Repo rules: the changelog fragment is present,
repo_environment.v1is registered,schema_compatpasses,ruffandmkdocs buildpass, and the tests run offline.
…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>
|
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.
|
antonwolfy
left a comment
There was a problem hiding this comment.
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,WorkTreeandinspect_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 withAGENTS.mdand ajustfilewith atest:recipe, aTaskfile.ymlwith atesttask, ameson.buildwithtest(...), or aMakefilewhosetest:comes from anincludestill getschecks.test.missing(error) and--fail-on-errorexits 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 ajustfile,Taskfile,meson.buildor anincludeis 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_000and"a b " * 80_000each finish in 0.005 s or less, andall test :/foo::still give the targetsall,test,foo;a := bgives none.- Heredocs and here-strings:
bash <<EOF,bash << EOF,bash <<-EOFwith 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 <<EOFand two heredocs on one line give the expected answer.grep -q ci <<< 'npm ci'and a lonecat <<EOFare not flagged. - Redirections:
2>&1,2>&-,>&2,&>filedo not split the command or hidenpm ci. - Test files:
src/t/types.go,docs/t/x.r,lib/R/t/util.R,a/t/data.txtandtests/__init__.pyare not test files;t/basic.tandtest/a.jlare. - Thresholds: a 100,000-byte file gives no
files.largeand 100,001 does; a 200,000-byteAGENTS.mdgives noinstructions.largeand 200,001 does. - Repo rules: the schema kind is registered,
schema_compatpasses, the changelog fragment is present, noTHREAD nlabels remain, no generated artifact is committed, the PR is not stacked, and the tests run offline.
| if k == len(ends): | ||
| break | ||
| body = "\n".join(lines[s + 1:ends[k]]) |
There was a problem hiding this comment.
_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)): |
There was a problem hiding this comment.
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']foundcat <<EOF\nnpm ci\nEOF\nbash <<EOF\nx\nEOF->['npm', 'ci']foundcat <<EOF\nnpm ci\nEOFalone -> 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.
| 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')}", |
There was a problem hiding this comment.
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.
| 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) |
There was a problem hiding this comment.
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.


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. Writesrepo_environment.v1.dokimi repo validate | scan | fetch | reach | tasks | run | report + inspect <source> [--out env.json] [--json] [--large-bytes N] [--fail-on-error]Observations are kept apart from findings. Each finding has a
severityand a separateconfidence, plus the stages it affects. A stage isblocked,attentionor at bestno_known_blocker, never "ready". What a static pass cannot see goes innot_observed: whether the commands pass, CI secrets, network, which files a harness version actually loads (repo reachmeasures that). There is no overall score.basis: toolchaincovers cases likego testnext to ago.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
dokimi repo inspect .→invalid choice: 'inspect'. On an unfamiliar repo everyrepocommand needs a manifest first.After: oneDAL @ 32cf1ad, ~1 s:
tests/test_repo_environment.py, 25 tests, on bothWorkTreeandGitTree:package.jsonthat is not counted as a project--ignore-scriptshandling, last flag wins--fail-on-errorexitmake 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.v1is a new schema, and later changes are additive only underschema_compat.Blast Radius: contained
Only callers of
repo inspectare affected. The single shared touchpoint is one line in theSCHEMA_VERSIONSregistry.🤖 Generated with Claude Code