Skip to content

feat: CLI/MCP parity, map command, and concurrency fixes#147

Open
zacwolfe wants to merge 9 commits into
Anandb71:mainfrom
zacwolfe:cli-enhancements
Open

feat: CLI/MCP parity, map command, and concurrency fixes#147
zacwolfe wants to merge 9 commits into
Anandb71:mainfrom
zacwolfe:cli-enhancements

Conversation

@zacwolfe

@zacwolfe zacwolfe commented Jun 7, 2026

Copy link
Copy Markdown

Description

This PR brings the CLI to parity with the MCP tool surface, adds a token-budgeted map command, includes several concurrency and indexing fixes, and adds a new arbor hook command that wires arbor into a coding-agent harness (Claude Code) in one shot. The CLI can now perform most operations previously available only to AI agents through the MCP bridge — graph queries, caller/callee lookups, symbol inspection, and path traversal — directly from the terminal. It also fixes a parser bug where Java and C# static method calls were mapped to the wrong target (or dropped).

Type of Change

  • New feature (non-breaking change that adds functionality)
  • Bug fix (non-breaking change that fixes an issue)
  • Performance improvement
  • Code refactoring

Changes Made

  • map command (commands.rs, main.rs): Produces a ranked, token-budgeted project skeleton using PageRank and entry-point detection. Supports --tokens N, --focus, --focus-changed, --json, and --verbose. Centrality scores are persisted to the cache so repeat invocations skip recomputation.
  • CLI/MCP parity: Adds callers, callees, entry-points, file-graph, inspect, and path commands matching the existing MCP tools. All support --json.
  • Multi-term query: query "a|b" . performs OR search with test-file filtering.
  • MCP get_map tool (arbor-mcp/src/lib.rs): Exposes the map output to agents as well.
  • arbor hook claude (arbor-cli/src/hook/): New arbor hook <harness> [path] [--global] command installs arbor agent directives + hooks into a coding-agent harness in one shot. Claude is the lone impl; a Harness trait + dispatch leave room for opencode/codex/etc.
    • CLAUDE.md: upserts a marker-delimited arbor guidance block (root CLAUDE.md preferred, then .claude/, else creates root). Re-run replaces the block in place — never duplicates.
    • .claude/settings.json: adds the 3 Bash hooks (auto-init, block recursive grep/find, daily map inject) plus arbor command permissions.allow. Dedups by exact match, preserves existing user content.
    • --global targets ~/.claude/. Hooks, permissions, and guidance match the documented reference install byte-for-byte.
  • Static method call mapping fix (arbor-core/src/languages/java.rs, csharp.rs): Qualified static calls like MathUtils.add() were captured by the Java parser as the bare method name add, dropping the class qualifier. The builder then resolved that bare name by suffix-matching, so the edge was either dropped (ambiguous name) or linked to the wrong class (a same-named method in a sibling file). C# dropped static calls entirely. Both parsers now keep the receiver (MathUtils.add), which exact-matches the stored FQN in the builder and links to the correct class. this./super./base. calls still strip to the bare name for same-class resolution, and instance calls (obj.method) keep obj.method and fail safe (no false edge). This matches the behavior Go/C++/Rust/Python already had.
  • Concurrency fixes: Single-writer persist lock, atomic cache writes (.tmp plus rename), cache staleness check, and sled lock avoidance when a bridge may hold the exclusive lock.
  • Incremental code map updates (arbor-watcher/src/indexer.rs): Re-parses only changed files.
  • auto_index gate: Indexing of un-indexed projects is now gated behind the auto_index config option (default off) to avoid unexpected indexing.
  • Docs: Updated the agent integration section in CLAUDE.md and added docs/assets/CLAUDE-example.md.

Known Limitations

  • Dart call edges: While auditing static-call handling, I found the Dart parser's collect_calls looks for a call_expression node that the tree-sitter-dart grammar never emits (it uses member_access + selector), so Dart currently captures zero call edges. This is a pre-existing, separate bug and is left for a follow-up rewrite — not addressed here.

Testing

  • Ran cargo test --all
  • Ran cargo clippy --all
  • Ran flutter test (not applicable — no visualizer changes)
  • Tested manually with a real codebase

Added integration coverage in graph_commands_integration.rs (531 lines) for the new query commands. Added regression tests for the static-call fix: parser-level (Java keeps the qualifier; this./super. strip to bare name) and a builder end-to-end test proving a qualified static call resolves to the correct class instead of a same-named sibling.

Checklist

  • My code follows the project's style guidelines
  • I have added tests for my changes
  • I have updated the documentation where necessary
  • All new and existing tests pass
  • I have added appropriate comments where the code isn't self-explanatory

Note: CHANGELOG.md has not yet been updated for these user-facing changes; [Unreleased] entries can be added if desired. The branch can be squashed before merge if preferred.

@zacwolfe zacwolfe marked this pull request as ready for review June 7, 2026 00:50
Zac Wolfe added 3 commits June 7, 2026 18:32
- load_or_index_graph now compares source file mtimes against graph.bin
  before trusting the cache; stale cache triggers re-index automatically
- when a bridge is running (detected via .arbor/cache/db), skip the
  staleness check — the bridge's persister owns freshness
- graph persister acquires an exclusive advisory lock (.arbor/persist.lock)
  so only one bridge per project writes graph.bin, preventing double-writes
  when multiple Claude sessions target the same project
- add fs2 dep for cross-platform advisory file locks
- CLAUDE.md: sqz guidance block (auto-installed by sqz init)
Arbor no longer writes an .arbor/ cache into a project that was never
indexed unless the user opts in. Running a read command (map, query,
callers, etc.) against an un-indexed project now fails fast with guidance
instead of silently mutating the repo — important when an agent reads code
in a separate project.

- split ensure_arbor_initialized into init_arbor_dir (unconditional, used
  by explicit init/index/setup) and a gated wrapper for implicit commands
- gate load_or_index_graph too, since read commands call it directly
  without ensure_arbor_initialized
- auto_index resolution: ARBOR_AUTO_INDEX env, then ~/.arbor/config.json
  "auto_index", else false. Lives globally since an un-indexed project has
  no project config to read
- already-indexed projects need no flag (.arbor/ presence is the opt-in)
- diff integration tests run on fresh temp repos: set ARBOR_AUTO_INDEX=1
new `arbor hook <harness> [path] [--global]` command installs arbor
agent directives + hooks into a coding-agent harness. claude is the
lone impl; Harness trait + dispatch leave room for opencode/codex/etc.

claude harness:
- CLAUDE.md: upserts a marker-delimited arbor guidance block (root
  CLAUDE.md preferred, then .claude/, else creates root). re-run
  replaces block in place, never duplicates.
- .claude/settings.json: adds the 3 Bash hooks (auto-init, block
  rg/recursive-grep/find, daily map inject) + arbor command
  permissions.allow. dedups by exact match, preserves user content.
- --global targets ~/.claude/.

hooks, permissions, and guidance match the reference install
byte-for-byte.
@zacwolfe zacwolfe force-pushed the cli-enhancements branch from 927977b to dfdc874 Compare June 8, 2026 01:33
@arbor-cloud

arbor-cloud Bot commented Jun 8, 2026

Copy link
Copy Markdown

🔴 Arbor PR Walk

Path heat HIGH █░░░░░░░░░ 10%
Branch cli-enhancementsmain
Impact 20 files · 352 symbols · 121 reachable callers
Entry Points 8 production endpoints reached
Languages Rust

Changed Files

File Symbols Direct Transitive Risk
🔴 crates/arbor-cli/src/commands.rs 88 59 7 HIGH
🔴 …ates/arbor-cli/tests/graph_commands_integration.rs 32 35 HIGH
🔴 crates/arbor-watcher/src/indexer.rs 13 13 17 HIGH
🔴 crates/arbor-mcp/src/lib.rs 32 8 HIGH
🟡 crates/arbor-cli/src/hook/claude.rs 16 4 MEDIUM
🟡 crates/arbor-cli/tests/diff_command_integration.rs 8 4 MEDIUM
🟡 crates/arbor-server/src/sync_server.rs 37 3 MEDIUM
🟡 crates/arbor-core/src/languages/csharp.rs 20 3 MEDIUM
🟢 crates/arbor-cli/src/hook/mod.rs 6 1 LOW
crates/arbor-cli/src/main.rs 6 NONE
crates/arbor-graph/src/builder.rs 17 NONE
crates/arbor-graph/src/graph.rs 52 NONE
crates/arbor-core/src/languages/java.rs 23 NONE
crates/arbor-watcher/src/lib.rs 2 NONE

🎯 Production Entry Points Reached

This change propagates to these entry points (HTTP handlers, jobs, CLI commands):

  • analyze
  • apply
  • audit
  • bridge
  • call_tool
  • callees
  • callers
  • check

✅ Before You Merge

  • Manually verify the affected entry points: analyze, apply, audit (+5).
  • Consider splitting this PR — 121 reachable callers were found. Smaller PRs are safer to review and roll back.
  • Request a senior engineer review before merging.

🔍 Sensitive Path Check — REVIEW REQUIRED

4 sensitive surfaces · 0 entry points reachable · Confidence: 80%

Category File Symbols
File I/O crates/arbor-graph/src/graph.rs remove_file, test_graph_remove_file_cleanup
Input Validation …arbor-cli/tests/graph_commands_integration.rs Command Injection (CWE-78), line 6
Input Validation …s/arbor-cli/tests/diff_command_integration.rs Command Injection (CWE-78), line 41
Input Validation crates/arbor-cli/src/commands.rs Command Injection (CWE-78), line 849
Sensitive call paths
  • change reaches sink: callers_json_outputsetup_rust_projectrun_arbor (2 hops) — full graph path to Command Injection in crates/arbor-cli/tests/graph_commands_integration.rs line 6
  • change reaches sink: diff_uses_env_commit_range_when_providedrun_git_stdout (1 hop) — full graph path to Command Injection in crates/arbor-cli/tests/diff_command_integration.rs line 41
  • change reaches sink: diff_uses_env_commit_range_when_providedrun_git_stdout (1 hop) — full graph path to Command Injection in crates/arbor-cli/tests/diff_command_integration.rs line 193
  • change reaches sink: indexindex_changed_onlyload_or_index_graphauto_index_enabled (3 hops) — full graph path to Command Injection in crates/arbor-cli/src/commands.rs line 849
  • change reaches sink: indexindex_changed_onlyload_or_index_graphauto_index_enabled (3 hops) — full graph path to Command Injection in crates/arbor-cli/src/commands.rs line 875
  • change reaches sink: indexindex_changed_onlyload_or_index_graphauto_index_enabled (3 hops) — full graph path to Command Injection in crates/arbor-cli/src/commands.rs line 880
  • change reaches sink: indexindex_changed_onlyload_or_index_graphauto_index_enabled (3 hops) — full graph path to Command Injection in crates/arbor-cli/src/commands.rs line 1474
  • change reaches sink: indexindex_changed_onlyload_or_index_graphauto_index_enabled (3 hops) — full graph path to Command Injection in crates/arbor-cli/src/commands.rs line 1495
  • 🟡 Fix Command Injection in graph_commands_integration.rs (line 6) [35 internal callers — inspect full call path before merge; path: callers_json_output -> setup_rust_project -> run_arbor] — Command::new() with non-literal command name — validate all inputs.
  • 🟡 Fix Command Injection in diff_command_integration.rs (line 41) [4 internal callers — inspect full call path before merge; path: diff_uses_env_commit_range_when_provided -> run_git_stdout] — Command::new() with non-literal command name — validate all inputs.
  • 🟡 Fix Command Injection in commands.rs (line 849) [27 internal callers — inspect full call path before merge; path: index -> index_changed_only -> load_or_index_graph -> auto_index_enabled] — Command::new() with non-literal command name — validate all inputs.
  • 🟡 Verify input validation still rejects malformed and malicious input
📊 Analysis confidence: High · 1388 nodes · 5522ms
  • Graph has 1388 nodes and 680 edges — well-connected codebase
  • 352 symbols changed, 121 upstream nodes analyzed

Arbor · View full report → · 5522ms · 1388 nodes · Senior engineer in your repo · Was this useful? 👍 👎

Java captured only the bare method name for `Foo.bar()`, dropping the
class qualifier. Static calls then collided with same-named methods —
edge dropped, or worse, linked to the wrong class. C# dropped static
calls entirely.

Both now keep the receiver (`Foo.bar`), which exact-matches the stored
FQN in the builder. this./super./base. still strip to bare name for
same-class resolution; instance calls keep `obj.method` and fail safe.

Matches the behavior Go/C++/Rust/Python already had.

Copilot AI 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.

Pull request overview

This PR expands Arbor’s CLI to reach feature parity with the existing MCP tool surface, adds a token-budgeted map view for fast project orientation, introduces a new arbor hook claude installer for Claude Code harness integration, and includes several indexing/cache/concurrency improvements plus parser fixes for qualified static calls.

Changes:

  • Added new CLI graph query commands (callers, callees, entry-points, file-graph, inspect, path) and enhanced query (multi-term OR + --exclude-test), plus a new token-budgeted map command with centrality persistence.
  • Added MCP get_map tool and updated server behavior to support orientation workflows.
  • Improved correctness and runtime behavior: Java/C# static call parsing fix, incremental indexing updates, cache staleness detection, atomic-ish cache writes, and a single-writer graph persister while a bridge runs.

Reviewed changes

Copilot reviewed 18 out of 20 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
docs/assets/CLAUDE-example.md Example Claude Code instructions showing Arbor-first navigation workflow and map usage.
crates/arbor-watcher/src/lib.rs Re-exports sources_newer_than for cache staleness checks.
crates/arbor-watcher/src/indexer.rs Adds sources_newer_than helper and unit tests for stale-cache detection.
crates/arbor-server/src/sync_server.rs Recomputes centrality after incremental graph patches/removals to keep map rankings aligned.
crates/arbor-mcp/src/lib.rs Adds get_map tool and tool descriptions; adds “still indexing” guard before serving tools.
crates/arbor-gui/Cargo.toml Updates egui/eframe dependencies to 0.31.
crates/arbor-graph/src/graph.rs Adds rebuild_search_index() to restore non-serialized search index after deserialization.
crates/arbor-graph/src/builder.rs Adds regression test ensuring qualified static calls resolve to the correct target class.
crates/arbor-core/src/languages/java.rs Fixes Java call collection to retain qualifiers for static/type-qualified invocations; adds tests.
crates/arbor-core/src/languages/csharp.rs Fixes C# call collection to retain qualifiers for static/type-qualified invocations.
crates/arbor-cli/tests/graph_commands_integration.rs Adds broad integration coverage for new CLI graph commands and map.
crates/arbor-cli/tests/diff_command_integration.rs Opts tests into auto-indexing via ARBOR_AUTO_INDEX=1.
crates/arbor-cli/src/main.rs Adds new CLI subcommands and extends query with --exclude-test; wires dispatch.
crates/arbor-cli/src/hook/mod.rs Introduces arbor hook <harness> scaffold (trait + dispatch).
crates/arbor-cli/src/hook/claude.rs Implements Claude harness installer (CLAUDE.md upsert + settings.json hooks + permissions).
crates/arbor-cli/src/commands.rs Major: auto-index gate, staleness checks, cache read/write changes, new commands, map implementation, background indexing/persister.
crates/arbor-cli/Cargo.toml Adds fs2 dependency for advisory file locking.
CLAUDE.md Updates docs for new MCP tool tiering and expanded CLI command reference + agent integration details.
Cargo.lock Lockfile updates from dependency bumps (notably egui/eframe ecosystem).
.gitignore Ignores .updates/ directory.

Comment on lines +182 to +186
let tmp_path = graph_path.with_extension("json.tmp");
let file = std::fs::File::create(&tmp_path)?;
let writer = std::io::BufWriter::new(file);
serde_json::to_writer_pretty(writer, graph)?;
fs::rename(&tmp_path, &graph_path)?;
Comment on lines +196 to 200
let tmp_path = graph_path.with_extension("bin.tmp");
let bytes = bincode::serialize(graph)?;
fs::write(graph_path, bytes)?;
fs::write(&tmp_path, bytes)?;
fs::rename(&tmp_path, &graph_path)?;
Ok(())
Comment on lines +390 to +411
fn parse_numstat_files(output: &str) -> Vec<String> {
output
.lines()
.filter_map(|line| {
let parts: Vec<&str> = line.split('\t').collect();
if parts.len() < 3 {
return None;
}
let path = if parts[2].contains(" => ") || !parts[2].is_empty() {
parts[2]
} else {
parts.get(2).copied().unwrap_or("")
};
let normalized = normalize_slashes(path.trim());
if normalized.is_empty() {
None
} else {
Some(normalized)
}
})
.collect()
}
Comment on lines +3646 to +3649
let glob_boost = match focus_glob {
Some(pattern) if node.file.contains(pattern.trim_matches('*')) => 0.3,
_ => 0.0,
};
Comment on lines +335 to +341
// If the graph is empty, the background index hasn't finished yet
if self.graph.read().await.node_count() == 0 {
return Ok(Self::err_envelope(
name,
"Arbor is still indexing the project. Please retry in a few seconds.",
));
}
Comment on lines +699 to +702
// Recompute centrality so the code map stays in sync with the
// patched graph — new nodes start at 0.0 otherwise and the map drifts.
let scores = compute_centrality(&g, 20, 0.85);
g.set_centrality(scores.into_map());
@Anandb71

Anandb71 commented Jun 8, 2026

Copy link
Copy Markdown
Owner

I will review this manually in some time.

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.

4 participants