# v2.1.0 Agent-Native Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Expand Arbor's MCP surface from 4 blunt tools to 10 surgical tools and standardize all output to a parseable JSON envelope.

**Architecture:** Add 6 new tools to `arbor-mcp/src/lib.rs` backed by 2 new `ArborGraph` methods. All new tools emit a standard JSON envelope with `ok`, `data`, and `meta` (including `suggested_next_tool`). Existing 4 tools get a `suggested_next_tool` field appended to their responses. No parser or graph schema changes.

**Tech Stack:** Rust, serde_json, arbor-graph (petgraph + heuristics), MCP stdio JSON-RPC

**Worktree:** `.worktrees/v2.1.0-agent-native` on branch `feat/v2.1.0-agent-native`

---

## File Map

| File | Change |
|------|--------|
| `crates/arbor-core/src/languages/dart.rs` | Fix `split_once` clippy warning |
| `crates/arbor-core/src/languages/typescript.rs` | Fix 2× `split_once` + doc indent warnings |
| `crates/arbor-graph/src/heuristics.rs` | Fix collapsible-if warning |
| `crates/arbor-graph/src/graph.rs` | Add `list_entry_points()`, `nodes_in_file_with_edges()` |
| `crates/arbor-mcp/src/lib.rs` | Add 6 new tool handlers + envelope helper + update `list_tools()` |

---

## Task 1: Fix Clippy Warnings (Codebase Audit)

**Files:**
- Modify: `crates/arbor-core/src/languages/dart.rs:498`
- Modify: `crates/arbor-core/src/languages/typescript.rs:458-459,478`
- Modify: `crates/arbor-graph/src/heuristics.rs:155`

- [ ] **Step 1: Fix dart.rs split_once**

In `crates/arbor-core/src/languages/dart.rs`, line 498 — replace:
```rust
if let Some(method) = call_text.splitn(2, '.').nth(1) {
```
with:
```rust
if let Some(method) = call_text.split_once('.').map(|x| x.1) {
```

- [ ] **Step 2: Fix typescript.rs split_once**

In `crates/arbor-core/src/languages/typescript.rs`, line 478 — replace:
```rust
if let Some(method) = call_text.splitn(2, '.').nth(1) {
```
with:
```rust
if let Some(method) = call_text.split_once('.').map(|x| x.1) {
```

- [ ] **Step 3: Fix typescript.rs doc indent**

In `crates/arbor-core/src/languages/typescript.rs`, lines 458-459, the doc comment has excessive leading spaces. Change from (the original has many spaces before the text):
```rust
///                                                     can't resolve without type inference,
///                                                     and would cause false name collisions)
```
to use 4 spaces of indent:
```rust
///     can't resolve without type inference,
///     and would cause false name collisions)
```
(Preserve the surrounding context of the doc comment — only fix the indentation of those two lines.)

- [ ] **Step 4: Fix heuristics.rs collapsible-if**

In `crates/arbor-graph/src/heuristics.rs`, the warning is at line 155 — a nested `if` inside another `if` that can be collapsed with `&&`. Find the block around line 155 that matches:
```rust
if name.contains("job")
    || name.contains("task")
    || name.contains("worker")
    || name.contains("cron")
    || name.ends_with("_run")
    || name == "run" || name == "execute" || name == "process"
{
    if file.contains("job")
        || file.contains("task")
        || file.contains("worker")
        || file.contains("cron")
        || file.contains("celery")
        || file.contains("sidekiq")
        || file.contains("background")
    {
        return true;
    }
}
```
Collapse to:
```rust
if (name.contains("job")
    || name.contains("task")
    || name.contains("worker")
    || name.contains("cron")
    || name.ends_with("_run")
    || name == "run"
    || name == "execute"
    || name == "process")
    && (file.contains("job")
        || file.contains("task")
        || file.contains("worker")
        || file.contains("cron")
        || file.contains("celery")
        || file.contains("sidekiq")
        || file.contains("background"))
{
    return true;
}
```

- [ ] **Step 5: Verify clean**

```bash
cd .worktrees/v2.1.0-agent-native
cargo clippy --workspace --all-targets --all-features 2>&1 | grep -E "^warning|^error"
```
Expected: no warnings, no errors.

- [ ] **Step 6: Run tests**

```bash
cargo test --workspace 2>&1 | grep -E "^test result"
```
Expected: all `ok`, 0 failed.

- [ ] **Step 7: Commit**

```bash
git add crates/arbor-core/src/languages/dart.rs \
        crates/arbor-core/src/languages/typescript.rs \
        crates/arbor-graph/src/heuristics.rs
git commit -m "fix: resolve all clippy warnings (split_once, doc indent, collapsible-if)"
```

---

## Task 2: Add Graph Query Methods for New Tools

**Files:**
- Modify: `crates/arbor-graph/src/graph.rs`

Two new public methods needed by the MCP tools:
1. `list_entry_points()` — iterate all nodes, filter by `HeuristicsMatcher::is_likely_entry_point`
2. `nodes_in_file_with_edges()` — get all nodes in a file plus the edges between them

- [ ] **Step 1: Write failing tests in graph.rs**

At the bottom of `crates/arbor-graph/src/graph.rs`, inside the existing `#[cfg(test)]` block (or add one), add:

```rust
#[cfg(test)]
mod new_query_tests {
    use super::*;
    use arbor_core::{CodeNode, NodeKind};

    fn make_node(name: &str, kind: NodeKind, file: &str) -> CodeNode {
        CodeNode::new(name, &format!("{}::{}", file, name), kind, file)
    }

    #[test]
    fn test_list_entry_points_returns_main() {
        let mut g = ArborGraph::new();
        g.add_node(make_node("main", NodeKind::Function, "src/main.rs"));
        g.add_node(make_node("helper", NodeKind::Function, "src/util.rs"));
        let eps = g.list_entry_points();
        assert!(eps.iter().any(|n| n.name == "main"), "main must be an entry point");
        assert!(!eps.iter().any(|n| n.name == "helper"), "helper must not be an entry point");
    }

    #[test]
    fn test_nodes_in_file_with_edges_returns_edges() {
        use crate::edge::{Edge, EdgeKind};
        let mut g = ArborGraph::new();
        let a = g.add_node(make_node("foo", NodeKind::Function, "src/a.rs"));
        let b = g.add_node(make_node("bar", NodeKind::Function, "src/a.rs"));
        let _c = g.add_node(make_node("baz", NodeKind::Function, "src/b.rs"));
        g.add_edge(a, b, Edge { kind: EdgeKind::Calls, confidence: 1.0, metadata: None });
        let (nodes, edges) = g.nodes_in_file_with_edges("src/a.rs");
        assert_eq!(nodes.len(), 2);
        assert_eq!(edges.len(), 1);
        assert_eq!(edges[0].0, "foo");
        assert_eq!(edges[0].1, "bar");
    }
}
```

- [ ] **Step 2: Run tests to verify they fail**

```bash
cd .worktrees/v2.1.0-agent-native
cargo test -p arbor-graph new_query_tests 2>&1 | tail -10
```
Expected: compile error — `list_entry_points` and `nodes_in_file_with_edges` not found.

- [ ] **Step 3: Add `list_entry_points()` to ArborGraph**

In `crates/arbor-graph/src/graph.rs`, after the existing `get_callees` method, add:

```rust
/// Returns all nodes detected as production entry points.
pub fn list_entry_points(&self) -> Vec<&CodeNode> {
    use crate::heuristics::HeuristicsMatcher;
    self.graph
        .node_weights()
        .filter(|n| HeuristicsMatcher::is_likely_entry_point(n))
        .collect()
}
```

- [ ] **Step 4: Add `nodes_in_file_with_edges()` to ArborGraph**

In `crates/arbor-graph/src/graph.rs`, after `list_entry_points`, add:

```rust
/// Returns all nodes in a file plus edges between those nodes.
///
/// Edges are returned as (caller_name, callee_name, edge_kind_str) triples.
pub fn nodes_in_file_with_edges(&self, file: &str) -> (Vec<&CodeNode>, Vec<(String, String, String)>) {
    let nodes = self.find_by_file(file);
    let node_ids: std::collections::HashSet<NodeId> = nodes
        .iter()
        .filter_map(|n| self.id_index.get(&n.id).copied())
        .collect();

    let mut edges = Vec::new();
    for &from in &node_ids {
        for edge_ref in self.graph.edges(from) {
            let to = edge_ref.target();
            if node_ids.contains(&to) {
                if let (Some(from_node), Some(to_node)) = (
                    self.graph.node_weight(from),
                    self.graph.node_weight(to),
                ) {
                    edges.push((
                        from_node.name.clone(),
                        to_node.name.clone(),
                        format!("{:?}", edge_ref.weight().kind),
                    ));
                }
            }
        }
    }
    (nodes, edges)
}
```

Note: `self.graph.edges(from)` requires `use petgraph::visit::IntoEdgeReferences;` — check the existing imports at the top of graph.rs (it's already imported there).

- [ ] **Step 5: Export new methods via lib.rs (no change needed — methods are on ArborGraph which is already pub)**

Verify `ArborGraph` is already re-exported from `crates/arbor-graph/src/lib.rs`:
```bash
grep "pub use graph::ArborGraph" crates/arbor-graph/src/lib.rs
```
Expected: line found.

- [ ] **Step 6: Run tests**

```bash
cargo test -p arbor-graph new_query_tests 2>&1 | tail -10
```
Expected: 2 passed, 0 failed.

- [ ] **Step 7: Run full test suite**

```bash
cargo test --workspace 2>&1 | grep "^test result"
```
Expected: all ok.

- [ ] **Step 8: Commit**

```bash
git add crates/arbor-graph/src/graph.rs
git commit -m "feat(graph): add list_entry_points() and nodes_in_file_with_edges() query methods"
```

---

## Task 3: Add 6 New MCP Tools + Response Envelope

**Files:**
- Modify: `crates/arbor-mcp/src/lib.rs`

Add a private `ok_response()` helper that wraps data in the standard envelope, then implement 6 new tools. Also retrofit `suggested_next_tool` into existing tool responses.

### Envelope structure

All new tools emit:
```json
{
  "content": [{
    "type": "text",
    "text": "<JSON string of envelope>"
  }]
}
```
Where envelope JSON is:
```json
{
  "ok": true,
  "tool": "<tool_name>",
  "arbor_version": "2.1.0",
  "data": { ... },
  "meta": {
    "node_count": 3,
    "suggested_next_tool": "analyze_impact",
    "suggested_next_args": { "node_id": "checkout" }
  }
}
```

- [ ] **Step 1: Write failing tests for new tools**

Add a test module at the bottom of `crates/arbor-mcp/src/lib.rs`:

```rust
#[cfg(test)]
mod tool_tests {
    use super::*;
    use arbor_graph::ArborGraph;
    use arbor_server::SharedGraph;
    use std::sync::Arc;
    use tokio::sync::RwLock;

    fn empty_server() -> McpServer {
        let graph = Arc::new(RwLock::new(ArborGraph::new()));
        McpServer::new(graph)
    }

    #[tokio::test]
    async fn test_list_entry_points_tool_returns_envelope() {
        let server = empty_server();
        let result = server
            .call_tool(serde_json::json!({ "name": "list_entry_points", "arguments": {} }))
            .await;
        assert!(result.is_ok());
        let val = result.unwrap();
        let text = val["content"][0]["text"].as_str().unwrap();
        let envelope: serde_json::Value = serde_json::from_str(text).unwrap();
        assert_eq!(envelope["ok"], true);
        assert_eq!(envelope["tool"], "list_entry_points");
        assert!(envelope["data"]["entry_points"].is_array());
        assert!(envelope["meta"]["suggested_next_tool"].is_string());
    }

    #[tokio::test]
    async fn test_get_callers_tool_node_not_found() {
        let server = empty_server();
        let result = server
            .call_tool(serde_json::json!({
                "name": "get_callers",
                "arguments": { "symbol": "nonexistent" }
            }))
            .await;
        assert!(result.is_ok());
        let val = result.unwrap();
        let text = val["content"][0]["text"].as_str().unwrap();
        let envelope: serde_json::Value = serde_json::from_str(text).unwrap();
        assert_eq!(envelope["ok"], false);
    }

    #[tokio::test]
    async fn test_get_callees_tool_node_not_found() {
        let server = empty_server();
        let result = server
            .call_tool(serde_json::json!({
                "name": "get_callees",
                "arguments": { "symbol": "nonexistent" }
            }))
            .await;
        assert!(result.is_ok());
        let val = result.unwrap();
        let text = val["content"][0]["text"].as_str().unwrap();
        let envelope: serde_json::Value = serde_json::from_str(text).unwrap();
        assert_eq!(envelope["ok"], false);
    }

    #[tokio::test]
    async fn test_search_symbols_tool_returns_envelope() {
        let server = empty_server();
        let result = server
            .call_tool(serde_json::json!({
                "name": "search_symbols",
                "arguments": { "query": "main" }
            }))
            .await;
        assert!(result.is_ok());
        let val = result.unwrap();
        let text = val["content"][0]["text"].as_str().unwrap();
        let envelope: serde_json::Value = serde_json::from_str(text).unwrap();
        assert_eq!(envelope["ok"], true);
        assert!(envelope["data"]["results"].is_array());
    }

    #[tokio::test]
    async fn test_get_file_graph_tool_node_not_found() {
        let server = empty_server();
        let result = server
            .call_tool(serde_json::json!({
                "name": "get_file_graph",
                "arguments": { "file_path": "src/nonexistent.rs" }
            }))
            .await;
        assert!(result.is_ok());
        let val = result.unwrap();
        let text = val["content"][0]["text"].as_str().unwrap();
        let envelope: serde_json::Value = serde_json::from_str(text).unwrap();
        assert_eq!(envelope["ok"], true);
        assert!(envelope["data"]["nodes"].is_array());
    }

    #[tokio::test]
    async fn test_get_node_detail_not_found() {
        let server = empty_server();
        let result = server
            .call_tool(serde_json::json!({
                "name": "get_node_detail",
                "arguments": { "symbol": "nonexistent" }
            }))
            .await;
        assert!(result.is_ok());
        let val = result.unwrap();
        let text = val["content"][0]["text"].as_str().unwrap();
        let envelope: serde_json::Value = serde_json::from_str(text).unwrap();
        assert_eq!(envelope["ok"], false);
    }

    #[tokio::test]
    async fn test_unknown_tool_returns_error() {
        let server = empty_server();
        let result = server
            .call_tool(serde_json::json!({ "name": "does_not_exist", "arguments": {} }))
            .await;
        assert!(result.is_err());
    }
}
```

- [ ] **Step 2: Run tests to verify they fail**

```bash
cd .worktrees/v2.1.0-agent-native
cargo test -p arbor-mcp tool_tests 2>&1 | tail -15
```
Expected: compile errors — tool names not found.

- [ ] **Step 3: Add `ok_envelope()` helper method to McpServer**

In `crates/arbor-mcp/src/lib.rs`, add this private method to `impl McpServer` (before `list_tools`):

```rust
fn ok_envelope(tool: &str, data: Value, node_count: usize, next_tool: &str, next_args: Value) -> Value {
    json!({
        "content": [{
            "type": "text",
            "text": serde_json::to_string_pretty(&json!({
                "ok": true,
                "tool": tool,
                "arbor_version": env!("CARGO_PKG_VERSION"),
                "data": data,
                "meta": {
                    "node_count": node_count,
                    "suggested_next_tool": next_tool,
                    "suggested_next_args": next_args
                }
            })).unwrap_or_default()
        }]
    })
}

fn err_envelope(tool: &str, message: &str) -> Value {
    json!({
        "content": [{
            "type": "text",
            "text": serde_json::to_string_pretty(&json!({
                "ok": false,
                "tool": tool,
                "arbor_version": env!("CARGO_PKG_VERSION"),
                "error": message
            })).unwrap_or_default()
        }]
    })
}
```

- [ ] **Step 4: Add 6 new tool entries to `list_tools()`**

In `list_tools()`, extend the `"tools"` array with these 6 entries (after the existing 4):

```rust
{
    "name": "list_entry_points",
    "description": "Lists all detected production entry points: HTTP handlers, main functions, webhooks, background jobs, and CLI commands. Use this first to understand the execution surface of a codebase.",
    "inputSchema": {
        "type": "object",
        "properties": {},
        "required": []
    }
},
{
    "name": "get_callers",
    "description": "Returns the direct callers of a symbol (one hop upstream). Use to answer 'what calls this function?'",
    "inputSchema": {
        "type": "object",
        "properties": {
            "symbol": { "type": "string", "description": "Name or ID of the symbol to look up" }
        },
        "required": ["symbol"]
    }
},
{
    "name": "get_callees",
    "description": "Returns the direct callees of a symbol (one hop downstream). Use to answer 'what does this function call?'",
    "inputSchema": {
        "type": "object",
        "properties": {
            "symbol": { "type": "string", "description": "Name or ID of the symbol to look up" }
        },
        "required": ["symbol"]
    }
},
{
    "name": "search_symbols",
    "description": "Fuzzy-searches symbol names across the graph. Use when you know part of a name but not the full ID.",
    "inputSchema": {
        "type": "object",
        "properties": {
            "query": { "type": "string", "description": "Partial or full symbol name to search for" },
            "limit": { "type": "integer", "description": "Maximum results to return (default: 20)", "default": 20 }
        },
        "required": ["query"]
    }
},
{
    "name": "get_file_graph",
    "description": "Returns all symbols and internal call edges within a single file. Use to understand a file's internal structure.",
    "inputSchema": {
        "type": "object",
        "properties": {
            "file_path": { "type": "string", "description": "Relative path to the file (e.g. 'src/auth.rs')" }
        },
        "required": ["file_path"]
    }
},
{
    "name": "get_node_detail",
    "description": "Returns full detail for a single symbol: file, line range, kind, role, centrality rank, and confidence. Use after search_symbols or list_entry_points to inspect a specific node.",
    "inputSchema": {
        "type": "object",
        "properties": {
            "symbol": { "type": "string", "description": "Name or ID of the symbol" }
        },
        "required": ["symbol"]
    }
}
```

- [ ] **Step 5: Implement the 6 new tool handlers in `call_tool()`**

In the `match name { ... }` block in `call_tool()`, add these 6 arms before the `_ =>` catch-all:

```rust
"list_entry_points" => {
    let graph = self.graph.read().await;
    let eps = graph.list_entry_points();
    let entries: Vec<Value> = eps.iter().map(|n| json!({
        "id": n.id,
        "name": n.name,
        "kind": n.kind.to_string(),
        "file": n.file,
        "line": n.line_start
    })).collect();
    let count = entries.len();
    let next_symbol = entries.first()
        .and_then(|e| e["name"].as_str())
        .unwrap_or("")
        .to_string();
    Ok(Self::ok_envelope(
        "list_entry_points",
        json!({ "entry_points": entries }),
        count,
        "analyze_impact",
        json!({ "node_id": next_symbol }),
    ))
}

"get_callers" => {
    let symbol = arguments.get("symbol").and_then(|v| v.as_str()).unwrap_or("");
    let graph = self.graph.read().await;
    let idx = graph.get_index(symbol)
        .or_else(|| graph.find_by_name(symbol).first().and_then(|n| graph.get_index(&n.id)));
    match idx {
        None => Ok(Self::err_envelope("get_callers", &format!("Symbol '{}' not found", symbol))),
        Some(idx) => {
            let callers = graph.get_callers(idx);
            let items: Vec<Value> = callers.iter().map(|n| json!({
                "id": n.id,
                "name": n.name,
                "kind": n.kind.to_string(),
                "file": n.file,
                "line": n.line_start
            })).collect();
            let count = items.len();
            Ok(Self::ok_envelope(
                "get_callers",
                json!({ "symbol": symbol, "callers": items }),
                count,
                if count > 0 { "analyze_impact" } else { "search_symbols" },
                if count > 0 { json!({ "node_id": symbol }) } else { json!({ "query": symbol }) },
            ))
        }
    }
}

"get_callees" => {
    let symbol = arguments.get("symbol").and_then(|v| v.as_str()).unwrap_or("");
    let graph = self.graph.read().await;
    let idx = graph.get_index(symbol)
        .or_else(|| graph.find_by_name(symbol).first().and_then(|n| graph.get_index(&n.id)));
    match idx {
        None => Ok(Self::err_envelope("get_callees", &format!("Symbol '{}' not found", symbol))),
        Some(idx) => {
            let callees = graph.get_callees(idx);
            let items: Vec<Value> = callees.iter().map(|n| json!({
                "id": n.id,
                "name": n.name,
                "kind": n.kind.to_string(),
                "file": n.file,
                "line": n.line_start
            })).collect();
            let count = items.len();
            let first_callee = items.first()
                .and_then(|e| e["name"].as_str())
                .unwrap_or("").to_string();
            Ok(Self::ok_envelope(
                "get_callees",
                json!({ "symbol": symbol, "callees": items }),
                count,
                if count > 0 { "get_node_detail" } else { "list_entry_points" },
                if count > 0 { json!({ "symbol": first_callee }) } else { json!({}) },
            ))
        }
    }
}

"search_symbols" => {
    let query = arguments.get("query").and_then(|v| v.as_str()).unwrap_or("");
    let limit = arguments.get("limit").and_then(|v| v.as_u64()).unwrap_or(20) as usize;
    let graph = self.graph.read().await;
    let results = graph.search(query);
    let items: Vec<Value> = results.iter().take(limit).map(|n| json!({
        "id": n.id,
        "name": n.name,
        "kind": n.kind.to_string(),
        "file": n.file,
        "line": n.line_start
    })).collect();
    let count = items.len();
    let first = items.first().and_then(|e| e["name"].as_str()).unwrap_or("").to_string();
    Ok(Self::ok_envelope(
        "search_symbols",
        json!({ "query": query, "results": items }),
        count,
        "get_node_detail",
        json!({ "symbol": first }),
    ))
}

"get_file_graph" => {
    let file_path = arguments.get("file_path").and_then(|v| v.as_str()).unwrap_or("");
    let graph = self.graph.read().await;
    let (nodes, edges) = graph.nodes_in_file_with_edges(file_path);
    let node_items: Vec<Value> = nodes.iter().map(|n| json!({
        "id": n.id,
        "name": n.name,
        "kind": n.kind.to_string(),
        "line": n.line_start
    })).collect();
    let edge_items: Vec<Value> = edges.iter().map(|(from, to, kind)| json!({
        "from": from,
        "to": to,
        "kind": kind
    })).collect();
    let count = node_items.len();
    let highest = nodes.iter()
        .max_by_key(|n| n.line_end.saturating_sub(n.line_start))
        .map(|n| n.name.clone())
        .unwrap_or_default();
    Ok(Self::ok_envelope(
        "get_file_graph",
        json!({ "file": file_path, "nodes": node_items, "edges": edge_items }),
        count,
        "analyze_impact",
        json!({ "node_id": highest }),
    ))
}

"get_node_detail" => {
    let symbol = arguments.get("symbol").and_then(|v| v.as_str()).unwrap_or("");
    let graph = self.graph.read().await;
    let idx = graph.get_index(symbol)
        .or_else(|| graph.find_by_name(symbol).first().and_then(|n| graph.get_index(&n.id)));
    match idx {
        None => Ok(Self::err_envelope("get_node_detail", &format!("Symbol '{}' not found", symbol))),
        Some(idx) => {
            let node = graph.get(idx).unwrap();
            let centrality = graph.centrality(idx);
            let callers = graph.get_callers(idx);
            let callees = graph.get_callees(idx);
            let is_entry = arbor_graph::HeuristicsMatcher::is_likely_entry_point(node);
            let role = if is_entry { "entry_point" }
                else if callers.is_empty() { "unreachable" }
                else if callees.is_empty() { "utility" }
                else { "internal" };
            let next = if callers.is_empty() { "get_callees" } else { "get_callers" };
            Ok(Self::ok_envelope(
                "get_node_detail",
                json!({
                    "id": node.id,
                    "name": node.name,
                    "kind": node.kind.to_string(),
                    "file": node.file,
                    "line_start": node.line_start,
                    "line_end": node.line_end,
                    "signature": node.signature,
                    "centrality": centrality,
                    "role": role,
                    "caller_count": callers.len(),
                    "callee_count": callees.len(),
                    "is_entry_point": is_entry
                }),
                1,
                next,
                json!({ "symbol": symbol }),
            ))
        }
    }
}
```

- [ ] **Step 6: Run tests**

```bash
cd .worktrees/v2.1.0-agent-native
cargo test -p arbor-mcp tool_tests 2>&1 | tail -20
```
Expected: 7 passed, 0 failed.

- [ ] **Step 7: Run full test suite**

```bash
cargo test --workspace 2>&1 | grep "^test result"
```
Expected: all ok.

- [ ] **Step 8: Run clippy**

```bash
cargo clippy --workspace --all-targets --all-features 2>&1 | grep -E "^warning|^error"
```
Expected: no warnings, no errors.

- [ ] **Step 9: Commit**

```bash
git add crates/arbor-mcp/src/lib.rs
git commit -m "feat(mcp): add 6 new agent tools + ok/err response envelope with suggested_next_tool"
```

---

## Task 4: Update Docs

**Files:**
- Modify: `CLAUDE.md`
- Modify: `docs/ROADMAP.md`
- Modify: `docs/ARCHITECTURE.md` (if exists, else skip)
- Add: `CLAUDE.md` commit (currently untracked)

- [ ] **Step 1: Update CLAUDE.md**

In `CLAUDE.md`, find the `arbor-mcp` crate description and update it to list all 10 MCP tools. Replace the existing `**arbor-mcp**` paragraph with:

```markdown
**`arbor-mcp`** — MCP stdio bridge for AI agents. Ten tools across two tiers:
- **Surgical** (new in v2.1.0): `list_entry_points`, `get_callers`, `get_callees`, `search_symbols`, `get_file_graph`, `get_node_detail`
- **Broad** (existing): `get_logic_path`, `get_knowledge_path`, `find_path`, `analyze_impact`
All new tools emit a standard JSON envelope: `{ok, tool, arbor_version, data, meta: {node_count, suggested_next_tool, suggested_next_args}}`. Run via `arbor bridge`.
```

- [ ] **Step 2: Update ROADMAP.md**

In `docs/ROADMAP.md`, under "Shipped / In-Progress Foundation", add a new bullet:

```markdown
- [x] **v2.1.0 Agent-Native MCP:** 6 new surgical tools (`list_entry_points`, `get_callers`, `get_callees`, `search_symbols`, `get_file_graph`, `get_node_detail`), standard JSON envelope with `suggested_next_tool` routing hints. All tools backward-compatible.
```

Also mark the "Agent Bridge (MCP): Deepen integration" item in section 2 as partially done.

- [ ] **Step 3: Commit docs + CLAUDE.md**

```bash
git add CLAUDE.md docs/ROADMAP.md
git commit -m "docs: update CLAUDE.md and ROADMAP for v2.1.0 agent-native MCP tools"
```

---

## Task 5: Audit and Fix CI Workflows

**Files:**
- Read: `.github/workflows/rust.yml`
- Read: `.github/workflows/release.yml`
- Read: `.github/workflows/pr-bot.yml` (if exists)
- Modify: any workflow file with issues

- [ ] **Step 1: Read and audit rust.yml**

Read `.github/workflows/rust.yml`. Check:
1. OS matrix includes `ubuntu-latest`, `macos-latest`, `windows-latest`
2. Rust toolchain version is not pinned to an old version
3. `cargo clippy` step uses `--all-targets --all-features`
4. `cargo test` step covers full workspace
5. No references to old binary names (`arbor-cli` vs `arbor-graph-cli`)

Fix any issues found.

- [ ] **Step 2: Read and audit release.yml**

Read `.github/workflows/release.yml`. Check:
1. Version numbers reference `2.0.1` or are dynamic (not hardcoded to old versions)
2. Binary name is `arbor-graph-cli` (the actual crate name)
3. Targets include: `x86_64-unknown-linux-gnu`, `aarch64-unknown-linux-gnu`, `x86_64-apple-darwin`, `aarch64-apple-darwin`, `x86_64-pc-windows-msvc`
4. Sled dependency doesn't cause linking issues on Linux ARM (check for `CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER`)

Fix any issues found.

- [ ] **Step 3: Check for pr-bot workflow**

```bash
ls .github/workflows/ | grep -i pr
```
If a pr-bot workflow exists, read it and verify it calls `arbor diff` or `arbor check` (not a mock). Fix if needed.

- [ ] **Step 4: Commit any workflow fixes**

```bash
git add .github/workflows/
git commit -m "ci: fix workflow issues found in v2.1.0 audit"
```
If no changes needed, skip this step.

---

## Final Verification

After all tasks complete:

- [ ] `cargo clippy --workspace --all-targets --all-features` — zero warnings
- [ ] `cargo test --workspace` — all tests pass
- [ ] `git log --oneline -10` — clean commit history on `feat/v2.1.0-agent-native`
- [ ] `git diff main..HEAD --stat` — review all changed files make sense
