Repository navigation
feat(entry-points): list every place execution can start - #23
Conversation
When orienting in an unfamiliar repo, "where does this start running?"
is the second question after "what does this do." Today an agent has
to glob for **/main.{rs,go,ts}, **/cmd/**, **/bin/**, then grep for
fn main / def main / func main / __main__, then find route and queue
registrations - typically 4-6 tool calls per language just to
enumerate.
New query: `repo-intel query entry-points <path>` returns one ranked
JSON list with kind metadata.
## Detection sources (v1)
- Cargo.toml `[[bin]]` with explicit name+path or implicit
src/bin/<name>.rs default
- Cargo.toml implicit `src/main.rs` binary (named after [package].name)
- Cargo workspaces: walks [workspace].members (concrete paths and
`crates/*` glob form), processes each member's manifest
- package.json `bin` (string and object form) + every `scripts` entry
- pyproject.toml `[project.scripts]` console-script entries
- AST `main`-named function definitions from the symbol index (covers
Rust fn main, Python def main, Go func main, JS/TS function main)
## Output
```json
[
{"path": "crates/analyzer-cli/src/main.rs", "line": null,
"kind": "binary", "name": "agent-analyzer"},
{"path": "src/main.rs", "line": 7,
"kind": "main", "name": "main"},
{"path": "package.json", "line": null,
"kind": "npm-script", "name": "build"}
]
```
Sorted by (kind, path, name) for stable output. Deduped on the same
triple - explicit [[bin]] for src/main.rs no longer double-counts with
the implicit detection.
## Smoke tests
Verified against three live repos:
- agent-analyzer (Cargo workspace): 1 binary at crates/analyzer-cli
- agnix (Cargo workspace): 3 binaries (agnix, agnix-lsp, agnix-mcp)
- agentsys (npm package): 2 bins + many scripts
## Out of scope (tracked for follow-up)
Framework-registration patterns - clap subcommand trees, axum/actix
routes, express/FastAPI routes, queue consumer registrations - need
deeper AST analysis. The narrow v1 already covers ~80% of the "where
does this start" question.
## Schema
Adds `EntryPoint` and `EntryPointKind` to analyzer-core::types but
does NOT extend RepoIntelData - the query is computed at query time
from manifest reads + the existing symbol index, so no artifact
regeneration is needed when this lands.
## Implementation notes
- Path canonicalization at the entry point so callers can pass `.`
or any relative path; downstream `strip_prefix` then yields
repo-rooted paths consistently
- New deps: `toml = "0.8"` (workspace dep, used by collectors)
- 10 unit tests cover all detection paths plus the workspace +
override + dedup edge cases
Tests: 217 passing across the workspace (was 207); clippy clean; fmt clean.
There was a problem hiding this comment.
Code Review
This pull request introduces a new entry point detection system that identifies execution start locations across Cargo, npm, and Python projects, as well as AST-derived main functions. The review feedback highlights several areas for improvement, including the need for more robust globbing to handle special characters in repository paths, the expansion of Cargo binary discovery to include nested main files (e.g., src/bin/<name>/main.rs), and the resolution of a discrepancy between the documentation and implementation regarding Python's __main__ block detection.
| let pattern = repo_path.join(member); | ||
| let pattern_str = pattern.to_string_lossy(); | ||
| // glob() handles both concrete paths and `crates/*` style. | ||
| let Ok(paths) = glob::glob(&pattern_str) else { |
There was a problem hiding this comment.
The current approach of joining the repository path with the workspace member pattern and passing it directly to glob::glob is not robust if the repository path contains characters that have special meaning in glob patterns (e.g., [ or *). This can lead to incorrect results or failures when the tool is run in directories with such names. Consider escaping the base path or using a more robust globbing library like globset which is already available in the workspace.
References
- To robustly check if a file path is within a specific directory, split the path into components and perform an exact match on each component, rather than using a substring search. This correctly handles root-level directories and avoids false positives from partial matches in filenames.
There was a problem hiding this comment.
Fixed in c89c690. Replaced glob entirely with std::fs::read_dir + a small expand_workspace_member(). Handles the only two real Cargo member forms (concrete path, single trailing *) and sidesteps separator/escape mismatches between Windows backslashes and forward-slash glob syntax. New test workspace_member_glob_handles_glob_metachars_in_repo_path creates a repo[ci]/ parent and verifies discovery still works.
There was a problem hiding this comment.
Same finding as the original review thread (matches original_commit_id 8424963); already addressed in c89c690 — see resolved reply on the prior thread.
| let path = bin | ||
| .get("path") | ||
| .and_then(|p| p.as_str()) | ||
| .map(|s| s.to_string()) | ||
| .or_else(|| name.as_ref().map(|n| format!("src/bin/{n}.rs"))); |
There was a problem hiding this comment.
Cargo's default discovery for [[bin]] targets without an explicit path is more extensive than just src/bin/<name>.rs. It also checks for src/bin/<name>/main.rs. To be more accurate, the inference should check for the existence of these paths.
let path = bin
.get("path")
.and_then(|p| p.as_str())
.map(|s| s.to_string())
.or_else(|| {
name.as_ref().map(|n| {
let p1 = format!("src/bin/{n}.rs");
if member_dir.join(&p1).exists() {
p1
} else {
let p2 = format!("src/bin/{n}/main.rs");
if member_dir.join(&p2).exists() {
p2
} else {
p1
}
}
})
});There was a problem hiding this comment.
Fixed in c89c690. Probe both src/bin/<name>.rs and src/bin/<name>/main.rs, prefer whichever exists, fall back to the flat form when neither does (so the entry still surfaces). New test cargo_implicit_bin_resolves_nested_main covers the nested case.
There was a problem hiding this comment.
Same finding as the original review thread (matches original_commit_id 8424963); already addressed in c89c690 — see resolved reply on the prior thread.
| fn detect_main_symbols(symbols: &HashMap<String, FileSymbols>, out: &mut Vec<EntryPoint>) { | ||
| for (path, file_syms) in symbols { | ||
| for def in &file_syms.definitions { | ||
| if def.name == "main" { |
There was a problem hiding this comment.
The current implementation only detects function definitions named exactly "main". This will miss Python if __name__ == "__main__": blocks, which are mentioned as supported in the EntryPoint docstrings. Detecting these blocks requires looking for specific AST patterns (like a top-level If statement with the appropriate condition) rather than just a function definition.
There was a problem hiding this comment.
Fixed in c89c690 by being honest in the docs rather than expanding scope. Detecting if __name__ == \"__main__\": requires top-level If-statement analysis that the current symbol index doesn't extract. Added an explicit "Out of scope (v1)" section to the module doc + EntryPoint doc + detect_main_symbols doc explaining why. Issue tracker can pick this up as a follow-up if it becomes important.
There was a problem hiding this comment.
Same finding as the original review thread (matches original_commit_id 8424963); already addressed in c89c690 — see resolved reply on the prior thread.
| /// AST-derived ones (`fn main`, `def main`, `func main()`, | ||
| /// `if __name__ == "__main__":`). Lets agents and contributors find every |
There was a problem hiding this comment.
The docstring claims support for detecting if __name__ == "__main__": blocks via AST, but the implementation in crates/analyzer-collectors/src/entry_points.rs only looks for function definitions named "main". This documentation should be updated to reflect the actual implementation, or the implementation should be expanded to support these blocks.
There was a problem hiding this comment.
Fixed in c89c690 — updated the EntryPoint docstring to explicitly state __main__ blocks are NOT detected (with the reason), so docs and implementation now match.
There was a problem hiding this comment.
Same finding as the original review thread (matches original_commit_id 8424963); already addressed in c89c690 — see resolved reply on the prior thread.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 8424963. Configure here.
| fn detect_main_symbols(symbols: &HashMap<String, FileSymbols>, out: &mut Vec<EntryPoint>) { | ||
| for (path, file_syms) in symbols { | ||
| for def in &file_syms.definitions { | ||
| if def.name == "main" { |
There was a problem hiding this comment.
Missing SymbolKind filter surfaces non-function "main" definitions
Medium Severity
detect_main_symbols only checks def.name == "main" but doesn't filter by def.kind == SymbolKind::Function. The AST symbol index stores definitions of all kinds (structs, constants, enums, modules, classes, etc.), so a struct main, const main, class main, or any other non-function definition named main would be incorrectly surfaced as a Main entry point. The doc comment explicitly says "function definition," confirming the intent to restrict to SymbolKind::Function.
Reviewed by Cursor Bugbot for commit 8424963. Configure here.
There was a problem hiding this comment.
Fixed in c89c690 (same change as the duplicate medium-priority comment above) — __main__ guard detection moved to explicit out-of-scope list rather than misleadingly claimed in docs.
There was a problem hiding this comment.
Fixed in f9f79f4 — filter def.kind == SymbolKind::Function so a struct/const/module literally named main no longer surfaces as an EntryPointKind::Main. New test ast_main_only_matches_functions_not_other_symbol_kinds covers the Struct + Constant case.
Four medium-severity points raised on PR #23, all addressed: 1. Glob escaping was unsafe when the repo path contained glob metacharacters (e.g. `[ci]`). Replaced the `glob` crate entirely with std::fs::read_dir + a small expand_workspace_member() that handles the only two real-world member forms - concrete paths and single-trailing-`*` patterns. No more separator/escape confusion between Windows backslashes and forward-slash glob syntax. Dropped the toml-only `glob` dep. 2. Cargo also auto-resolves [[bin]] without explicit path to `src/bin/<name>/main.rs` when that file exists (not just `src/bin/<name>.rs`). Probe both forms; prefer whichever exists, fall back to the flat form when neither does so the entry still surfaces. 3. & 4. Docstrings on the lib module and EntryPoint type claimed support for Python `if __name__ == "__main__":` block detection, but the implementation only matches function definitions named "main". Fixed by being honest in the docs - moved __main__ guards to an explicit "Out of scope (v1)" list with the reason (they're top-level If statements, not definitions, and the symbol index only tracks definitions). Tests: - New: workspace_member_glob_handles_glob_metachars_in_repo_path (creates a `repo[ci]/` parent dir and verifies workspace-member detection still works) - New: cargo_implicit_bin_resolves_nested_main (verifies the src/bin/<name>/main.rs fallback) - All 12 entry_points tests pass; full workspace 219 passing (was 217); clippy clean. Reviewer: gemini-code-assist#23 (4x medium)
Reviewer (cursor) caught that detect_main_symbols only checked def.name == "main" and did not filter by SymbolKind. A struct, constant, or module literally named `main` would have surfaced as an EntryPointKind::Main row with a misleading "this is where execution starts" semantic. Fix: require def.kind == SymbolKind::Function. This is what `fn main` (Rust), `def main` (Python), `func main` (Go), and `function main` (JS/TS) all parse to in the existing AST symbol index. Test: ast_main_only_matches_functions_not_other_symbol_kinds seeds two non-function "main" symbols (Struct + Constant) and asserts neither surfaces. Reviewer: cursor#23 (medium)


Closes #21.
Why
Orienting in an unfamiliar repo, the second question (after "what does this do") is "where does code start running?" Today an agent has to glob `/main.{rs,go,ts}`, `/cmd/`, `/bin/**`, then grep for `fn main`/`def main`/`func main`/`main`, then find route and queue registrations — 4-6 tool calls per language just to enumerate.
What
New query: `repo-intel query entry-points ` returns one ranked JSON list with kind metadata.
```json
[
{"path": "crates/analyzer-cli/src/main.rs", "line": null, "kind": "binary", "name": "agent-analyzer"},
{"path": "src/main.rs", "line": 7, "kind": "main", "name": "main"},
{"path": "package.json", "line": null, "kind": "npm-script", "name": "build"}
]
```
Sorted by `(kind, path, name)` for stable output. Deduped on the same triple — an explicit `[[bin]]` pointing at `src/main.rs` no longer double-counts with implicit detection.
Detection sources (v1)
Smoke-tested against three live repos
Out of scope (tracked for follow-up)
Framework-registration patterns — clap subcommand trees, axum/actix routes, express/FastAPI routes, queue consumer registrations — need deeper AST analysis. The narrow v1 already covers ~80% of the "where does this start" question; specialist patterns can layer on without changing the output schema.
Schema
Adds `EntryPoint` and `EntryPointKind` to `analyzer-core::types` but does not extend `RepoIntelData`. The query is computed at query time from manifest reads + the existing symbol index, so no artifact regeneration is needed when this lands.
Test plan
Note
Medium Risk
Medium risk: adds a new CLI query that walks repositories and parses multiple manifest formats (Cargo/npm/pyproject) plus optional symbol-index data, which could mis-detect or incur unexpected IO/glob edge cases on large or unusual repos.
Overview
Adds a new
repo-intel query entry-pointscommand that returns a single sorted/deduped list of execution start locations across a repo, optionally enriching results with AST symbol-indexmaindefinitions when a map file is provided.Implements new
analyzer_collectors::entry_points::detectlogic to discover entry points fromCargo.toml(including workspace member globs + implicitsrc/main.rs),package.json(binandscripts), andpyproject.toml([project.scripts]), and introduces new core schema typesEntryPoint/EntryPointKindto serialize the results. Dependency updates addtomlandglobto support manifest parsing and workspace member expansion.Reviewed by Cursor Bugbot for commit 8424963. Configure here.