From 3637b2a8bc287e5f83a0e9d89ab5e73978145e66 Mon Sep 17 00:00:00 2001 From: Dmitry Teryaev Date: Sun, 24 May 2026 18:52:24 +0300 Subject: [PATCH 1/5] feat(skills): add 14 navigation and workflow skills + static validator Implement AGENT-SKILLS-AND-COMMANDS-PROPOSE (PR-S-2 through PR-S-4): - Tier 1: 10 deterministic MCP-chain navigation skills (nl, controllers, routes, clients, callers, callees, handlers, who-hits-route, implements, injects) with graph-accurate edge sets and resolve-first resolution - Tier 2: 4 bounded workflow skills (explain-feature, impact-of, trace-request-flow, mini-map) with stop conditions, recursion limits, and output shapes - skills/README.md with 3-layer architecture diagram and skill index - tests/test_agent_skills_static.py (127 assertions): frontmatter, MCP tool/kind/direction/edge-type validation against mcp_v2 allowlists, Tier 2 structure checks, directory integrity, AGENT-GUIDE consistency - docs/AGENT-GUIDE.md: slash-aliases replaced with skills/ pointer Co-Authored-By: Claude Opus 4.7 --- docs/AGENT-GUIDE.md | 23 +-- skills/README.md | 66 +++++++ skills/callees/SKILL.md | 40 +++++ skills/callers/SKILL.md | 33 ++++ skills/clients/SKILL.md | 29 +++ skills/controllers/SKILL.md | 28 +++ skills/explain-feature/SKILL.md | 51 ++++++ skills/handlers/SKILL.md | 30 ++++ skills/impact-of/SKILL.md | 57 ++++++ skills/implements/SKILL.md | 26 +++ skills/injects/SKILL.md | 26 +++ skills/mini-map/SKILL.md | 102 +++++++++++ skills/nl/SKILL.md | 33 ++++ skills/routes/SKILL.md | 28 +++ skills/trace-request-flow/SKILL.md | 59 ++++++ skills/who-hits-route/SKILL.md | 33 ++++ tests/test_agent_skills_static.py | 277 +++++++++++++++++++++++++++++ 17 files changed, 924 insertions(+), 17 deletions(-) create mode 100644 skills/README.md create mode 100644 skills/callees/SKILL.md create mode 100644 skills/callers/SKILL.md create mode 100644 skills/clients/SKILL.md create mode 100644 skills/controllers/SKILL.md create mode 100644 skills/explain-feature/SKILL.md create mode 100644 skills/handlers/SKILL.md create mode 100644 skills/impact-of/SKILL.md create mode 100644 skills/implements/SKILL.md create mode 100644 skills/injects/SKILL.md create mode 100644 skills/mini-map/SKILL.md create mode 100644 skills/nl/SKILL.md create mode 100644 skills/routes/SKILL.md create mode 100644 skills/trace-request-flow/SKILL.md create mode 100644 skills/who-hits-route/SKILL.md create mode 100644 tests/test_agent_skills_static.py diff --git a/docs/AGENT-GUIDE.md b/docs/AGENT-GUIDE.md index 54b9e90..88a646c 100644 --- a/docs/AGENT-GUIDE.md +++ b/docs/AGENT-GUIDE.md @@ -269,26 +269,15 @@ Returns **edges** with `attrs` (`confidence`, `strategy`, `match`, … on cross- After two failed attempts on the same intent, stop and report tool name, args, and response snippet. -### Slash-style aliases - -- `/nl ` → `search({"query":"","limit":8})` then `describe` on best `symbol_id`. -- `/controllers ` → `find({"kind":"symbol","filter":{"microservice":"","role":"CONTROLLER"}})`. -- `/routes ` → `find({"kind":"route","filter":{"microservice":""}})`. -- `/clients ` → `find({"kind":"client","filter":{"microservice":""},"limit":100})`. -- `/producers ` → `find({"kind":"producer","filter":{"microservice":""},"limit":100})`. -- `/callers ` → `neighbors({"ids":"","direction":"in","edge_types":["CALLS"]})`. -- `/callees ` → `neighbors({"ids":"","direction":"out","edge_types":["CALLS"]})`. -- `/handlers ` → `neighbors({"ids":"","direction":"in","edge_types":["EXPOSES"]})`. -- `/who-hits-route ` → `neighbors({"ids":"","direction":"in","edge_types":["HTTP_CALLS","ASYNC_CALLS","EXPOSES"]})`. -- `/implements ` → `neighbors({"ids":"","direction":"in","edge_types":["IMPLEMENTS"]})`. -- `/injects ` → `neighbors({"ids":"","direction":"in","edge_types":["INJECTS"]})`. +### Navigation skills (`/` commands) + +Navigation intents (`/callees`, `/callers`, `/routes`, etc.) are shipped as SKILL.md files in [`skills/`](../skills/) at the project root. Each skill defines a deterministic MCP chain (Tier 1) or bounded workflow (Tier 2). See [`skills/README.md`](../skills/README.md) for the full index. + +When the user types a `/` intent matching a shipped skill, execute the chain from the corresponding `skills//SKILL.md`. For intents not covered by a skill, use the decision tree and raw MCP tools above. ### Canonical workflow: "explain feature X" -1. `search` with a short query; pick 1–3 hits with strong `symbol_id` / role fit. -2. `describe` on the chosen id; read `edge_summary`. -3. Walk with `neighbors` using **small** `edge_types` sets (e.g. `CALLS` out, or `EXPOSES` / cross-service edges for boundaries). -4. Stop when you can answer; do not prefetch unrelated subgraphs. +Use the `/explain-feature` skill (Tier 2). The chain is: `search` → pick 1–3 hits → `describe` each → bounded `neighbors` walk until the question is answered. diff --git a/skills/README.md b/skills/README.md new file mode 100644 index 0000000..4dd7e8e --- /dev/null +++ b/skills/README.md @@ -0,0 +1,66 @@ +# skills/ — Layer 3 navigation and workflow skills + +High-level intents over the 5-tool MCP (`search` / `find` / `describe` / `neighbors` / `resolve`). Skills are agent-side prompt scaffolding — they are NOT a second MCP API and NOT CLI subcommands. + +## Three-layer architecture + +``` +┌──────────────────────────────────────────────────────────────┐ +│ Layer 3 — High-level intents (what the user actually thinks) │ +│ /trace-request-flow, /callees, /controllers, /routes, │ +│ /impact-of, /mini-map │ +│ ───────────────────────────────────────────────────────── │ +│ Implementation: SKILL.md in skills/ at project root. │ +│ Tier 1 = deterministic chains; Tier 2 = bounded workflows │ +│ + /mini-map heuristics. │ +├──────────────────────────────────────────────────────────────┤ +│ Layer 2 — Composable primitives (the MCP API) │ +│ search, find, describe, neighbors, resolve │ +├──────────────────────────────────────────────────────────────┤ +│ Layer 1 — Storage primitives │ +│ Kuzu Cypher + LanceDB tables │ +└──────────────────────────────────────────────────────────────┘ +``` + +## Skill index + +### Tier 1 — Navigation (deterministic MCP chains) + +| Skill | Purpose | +| ----- | ------- | +| [`/nl`](nl/SKILL.md) | Natural-language search into the graph | +| [`/controllers`](controllers/SKILL.md) | List controller classes | +| [`/routes`](routes/SKILL.md) | List HTTP and messaging routes | +| [`/clients`](clients/SKILL.md) | List outbound HTTP clients | +| [`/callers`](callers/SKILL.md) | Who calls this method (in-process CALLS) | +| [`/callees`](callees/SKILL.md) | What this method calls (in-process CALLS) | +| [`/handlers`](handlers/SKILL.md) | Method that handles a route | +| [`/who-hits-route`](who-hits-route/SKILL.md) | All inbound paths to a route | +| [`/implements`](implements/SKILL.md) | Concrete classes implementing an interface | +| [`/injects`](injects/SKILL.md) | Where a type is injected | + +### Tier 2 — Workflow (bounded multi-step) + +| Skill | Purpose | +| ----- | ------- | +| [`/explain-feature`](explain-feature/SKILL.md) | Understand how a feature works end-to-end | +| [`/impact-of`](impact-of/SKILL.md) | What breaks if a symbol changes | +| [`/trace-request-flow`](trace-request-flow/SKILL.md) | Follow a request from entry to persistence | +| [`/mini-map`](mini-map/SKILL.md) | Noise-filtered call map for a method | + +## Layout + +``` +skills/ + / + SKILL.md ← frontmatter (name + description) + markdown body + README.md ← this file +``` + +## Relationship to developer skills + +Developer workflow skills (propose, pr-review, etc.) live in `.agents/skills/` — they are for contributors working **on** java-codebase-rag. Skills in this directory are for **consumers** using java-codebase-rag to explore their own codebases. + +## Versioning + +Skills are versioned lockstep with the MCP. When `NodeFilter` keys, `edge_filter` axes, `edge_types`, or `kind` values change, skills are updated in the same PR. diff --git a/skills/callees/SKILL.md b/skills/callees/SKILL.md new file mode 100644 index 0000000..6403eae --- /dev/null +++ b/skills/callees/SKILL.md @@ -0,0 +1,40 @@ +--- +name: callees +description: Show what a method symbol calls (in-process CALLS). Use when the user asks "what does X call", "callees of X", or "what does X invoke". Argument is a sym: id, or an identifier resolved via resolve. +--- + +# /callees — Show callees of a method symbol + +## Argument contract + +Single positional argument: a method **symbol** id (`sym:...` preferred) OR an identifier-shaped string (FQN fragment, method signature) → `resolve(identifier=..., hint_kind="symbol")`. + +This skill is for **method symbols**. For inbound traffic to an HTTP route, use `/who-hits-route`. For outbound Feign/HTTP from a method, see optional step 3 below. + +## Steps + +1. **Resolve.** If the argument starts with `sym:`, use it. Otherwise: + `resolve(identifier=, hint_kind="symbol")` → on `one`, use `node.id`; on `many`, list `candidates` and stop; on `none`, try `search(query=, limit=5)` and stop if still empty. +2. **In-process callees:** + `neighbors({ids: , direction: "out", edge_types: ["CALLS"]})`. + Render grouped by edge type; show callee `fqn` + `microservice`. +3. **Optional — outbound HTTP (only when user asks about cross-service calls):** + `neighbors({ids: , direction: "out", edge_types: ["DECLARES_CLIENT"]})` + → for each client id: + `neighbors({ids: , direction: "out", edge_types: ["HTTP_CALLS"]})`. + (Async: `DECLARES_PRODUCER` → `ASYNC_CALLS` on producer ids.) + +## Worked example + +User: /callees ChatController#joinOperator(JoinOperatorRequest) +You: → resolve(identifier="ChatController#joinOperator", hint_kind="symbol") + → sym:com.bank.chat.core.api.ChatController#joinOperator(JoinOperatorRequest) + → neighbors({ids: "sym:...", direction: "out", edge_types: ["CALLS"]}) + → returns CALLS edges to in-process service methods + → (optional step 3) neighbors(out, ["DECLARES_CLIENT"]) on sym id, then HTTP_CALLS from client ids + +## Out of scope + +- Recursive callees beyond depth 1 (use `/trace-request-flow` or `/mini-map`). +- Noisy CALLS subgraphs on service methods (prefer `/mini-map`; fall back here if the map is too thin). +- Filtering by microservice (compose with `/controllers` if needed). diff --git a/skills/callers/SKILL.md b/skills/callers/SKILL.md new file mode 100644 index 0000000..11f89f9 --- /dev/null +++ b/skills/callers/SKILL.md @@ -0,0 +1,33 @@ +--- +name: callers +description: Show who calls a method symbol (in-process CALLS). Use when the user asks "who calls X", "callers of X", or "what invokes X". Argument is a sym: id or identifier resolved via resolve. +--- + +# /callers — Show callers of a method symbol + +## Argument contract + +Single positional argument: a method **symbol** id (`sym:...` preferred) OR an identifier-shaped string (FQN fragment, method signature) → `resolve(identifier=..., hint_kind="symbol")`. + +This skill is for **method symbols**. For inbound traffic to an HTTP route, use `/who-hits-route`. + +## Steps + +1. **Resolve.** If the argument starts with `sym:`, use it. Otherwise: + `resolve(identifier=, hint_kind="symbol")` → on `one`, use `node.id`; on `many`, list `candidates` and stop; on `none`, try `search(query=, limit=5)` and stop if still empty. +2. **In-process callers:** + `neighbors({ids: , direction: "in", edge_types: ["CALLS"]})`. + Render grouped by caller `fqn` + `microservice`. + +## Worked example + +User: /callers ChatController#joinOperator(JoinOperatorRequest) +You: → resolve(identifier="ChatController#joinOperator", hint_kind="symbol") + → sym:com.bank.chat.core.api.ChatController#joinOperator(JoinOperatorRequest) + → neighbors({ids: "sym:...", direction: "in", edge_types: ["CALLS"]}) + → returns CALLS edges from in-process callers + +## Out of scope + +- Callers of routes (use `/who-hits-route`). +- Recursive callers beyond depth 1 (use `/impact-of`). diff --git a/skills/clients/SKILL.md b/skills/clients/SKILL.md new file mode 100644 index 0000000..a5c872e --- /dev/null +++ b/skills/clients/SKILL.md @@ -0,0 +1,29 @@ +--- +name: clients +description: List outbound HTTP clients, optionally filtered by microservice. Use when the user asks "list clients", "show outbound HTTP calls", or "what Feign clients are in X". +--- + +# /clients — List outbound HTTP clients + +## Argument contract + +Optional positional argument: microservice name. Omit to list all clients. + +## Steps + +1. **Find clients.** + - With microservice: `find(kind="client", filter={microservice: }, limit=100)`. + - Without microservice: `find(kind="client", filter={}, limit=100)`. +2. **Render.** Show each result's `fqn`, `microservice`, `client_kind`, `target_service`, and `id`. +3. **Narrow if needed.** When results are broad, add `client_kind` or `target_service` to the filter. + +## Worked example + +User: /clients chat-core +You: → find(kind="client", filter={microservice: "chat-core"}, limit=100) + → returns outbound HTTP client nodes in chat-core + → e.g. client:ChatServiceClient (feign_method), target_service=chat-service + +User: /clients +You: → find(kind="client", filter={}, limit=100) + → returns all outbound HTTP client nodes diff --git a/skills/controllers/SKILL.md b/skills/controllers/SKILL.md new file mode 100644 index 0000000..5b8090f --- /dev/null +++ b/skills/controllers/SKILL.md @@ -0,0 +1,28 @@ +--- +name: controllers +description: List controller classes, optionally filtered by microservice. Use when the user asks "list controllers", "show me controllers in X", or "what controllers are there". +--- + +# /controllers — List controllers + +## Argument contract + +Optional positional argument: microservice name. Omit to list all controllers. + +## Steps + +1. **Find controllers.** + - With microservice: `find(kind="symbol", filter={role: "CONTROLLER", microservice: })`. + - Without microservice: `find(kind="symbol", filter={role: "CONTROLLER"})`. +2. **Render.** Show each result's `fqn`, `microservice`, and `id`. + +## Worked example + +User: /controllers chat-core +You: → find(kind="symbol", filter={role: "CONTROLLER", microservice: "chat-core"}) + → returns controller symbols in chat-core microservice + → e.g. sym:com.bank.chat.core.api.ChatController + +User: /controllers +You: → find(kind="symbol", filter={role: "CONTROLLER"}) + → returns all controller symbols across all microservices diff --git a/skills/explain-feature/SKILL.md b/skills/explain-feature/SKILL.md new file mode 100644 index 0000000..937a103 --- /dev/null +++ b/skills/explain-feature/SKILL.md @@ -0,0 +1,51 @@ +--- +name: explain-feature +description: Understand how a feature works end-to-end by tracing from entry points through call chains. Use when the user asks "how does X work", "explain feature X", or "walk me through X". Argument is a free-form feature description. +--- + +# /explain-feature — Understand a feature end-to-end + +## Argument contract + +Single positional argument: free-form text describing the feature or concept to explain. + +## Steps + +1. **Locate entry points.** + `search(query=, limit=8)` → pick top 1–3 hits with strong `symbol_id` fit (role, `symbol_kind` alignment). +2. **Inspect each hit.** + `describe(id=)` → read `edge_summary` to understand the node's connectivity. +3. **Walk with bounded neighbors.** + For each inspected node, use `neighbors` with **small** `edge_types` sets per step: + - Methods: `neighbors(out, ["CALLS"])` for in-process flow. + - Boundaries: `EXPOSES` for route handlers; `DECLARES_CLIENT` → `HTTP_CALLS` for outbound HTTP; `DECLARES_PRODUCER` → `ASYNC_CALLS` for async. + - Type wiring: `IMPLEMENTS`, `INJECTS` when relevant. +4. **Render.** Synthesize findings into a narrative: entry points → key methods → data flow → cross-service boundaries. + +## Stop conditions + +- Maximum 3 hops from any entry point. +- Stop when you can answer the user's question. +- Do not prefetch unrelated subgraphs. + +## Recursion limit + +- Depth ≤ 3 from each entry point. +- Maximum 10 `neighbors` calls total. + +## Worked example + +User: /explain-feature operator assignment +You: → search(query="operator assignment", limit=8) + → hit: sym:com.bank.chat.assign.service.OperatorAssignmentService + → describe(id="sym:...") → edge_summary shows CALLS, INJECTS + → neighbors(out, ["CALLS"]) → shows delegation to repository and other services + → neighbors(in, ["IMPLEMENTS"]) → shows concrete implementations + → synthesize: "OperatorAssignmentService is an interface with two implementations. + The controller calls it via DI. It delegates to OperatorRepository for persistence..." + +## Out of scope + +- Exact impact analysis (use `/impact-of`). +- Full request flow tracing (use `/trace-request-flow`). +- Noise-filtered call maps (use `/mini-map`). diff --git a/skills/handlers/SKILL.md b/skills/handlers/SKILL.md new file mode 100644 index 0000000..652db14 --- /dev/null +++ b/skills/handlers/SKILL.md @@ -0,0 +1,30 @@ +--- +name: handlers +description: Show the method that handles an HTTP or messaging route. Use when the user asks "what handles X route", "handler for POST /foo", or "which method handles this endpoint". Argument is a route: id or route identifier. +--- + +# /handlers — Show handler method for a route + +## Argument contract + +Single positional argument: a **route** id (`route:...` preferred) OR an identifier-shaped string (path, METHOD /path) → `resolve(identifier=..., hint_kind="route")`. + +## Steps + +1. **Resolve.** If the argument starts with `route:` or `r:`, use it. Otherwise: + `resolve(identifier=, hint_kind="route")` → on `one`, use `node.id`; on `many`, list `candidates` and stop; on `none`, try `find(kind="route", filter={path_prefix: })` and stop if still empty. +2. **Handler method:** + `neighbors({ids: , direction: "in", edge_types: ["EXPOSES"]})`. + Render the handler method `fqn` + `microservice`. + +## Worked example + +User: /handlers route:POST /chat/join +You: → neighbors({ids: "route:POST /chat/join", direction: "in", edge_types: ["EXPOSES"]}) + → returns the handler method Symbol that exposes this route + +User: /handlers POST /chat/join +You: → resolve(identifier="POST /chat/join", hint_kind="route") + → route:POST /chat/join + → neighbors({ids: "route:...", direction: "in", edge_types: ["EXPOSES"]}) + → returns handler method diff --git a/skills/impact-of/SKILL.md b/skills/impact-of/SKILL.md new file mode 100644 index 0000000..3ad5937 --- /dev/null +++ b/skills/impact-of/SKILL.md @@ -0,0 +1,57 @@ +--- +name: impact-of +description: Analyze what breaks if a symbol changes. Use when the user asks "what breaks if I change X", "impact of changing X", or "who depends on X". Argument is a sym: id or identifier. +--- + +# /impact-of — What breaks if this changes + +## Argument contract + +Single positional argument: a Symbol id (`sym:...` preferred) OR an identifier-shaped string (FQN, simple name) → `resolve(identifier=..., hint_kind="symbol")`. + +## Steps + +1. **Resolve.** If the argument starts with `sym:`, use it. Otherwise: + `resolve(identifier=, hint_kind="symbol")` → on `one`, use `node.id`; on `many`, list `candidates` and stop; on `none`, try `search(query=, limit=5)` and stop if still empty. +2. **Inspect.** + `describe(id=)` → read `edge_summary` and `role` to understand the node's position. +3. **Recursive inbound walk** (depth ≤ 2): + `neighbors({ids: , direction: "in", edge_types: ["CALLS", "INJECTS", "IMPLEMENTS", "EXTENDS"]})`. + For each inbound neighbor that is a method symbol: + `neighbors({ids: , direction: "in", edge_types: ["CALLS", "INJECTS", "IMPLEMENTS", "EXTENDS"]})`. +4. **Route/client impact** (when applicable): + - If the symbol is a method with routes: `neighbors(out, ["EXPOSES"])` → then `neighbors(in, ["HTTP_CALLS", "ASYNC_CALLS"])` on route ids for callers outside the codebase. + - If the symbol declares clients: `neighbors(out, ["DECLARES_CLIENT"])` → `neighbors(out, ["HTTP_CALLS"])` for affected downstream services. +5. **Render impact list.** Deduplicate results. Group by: + - Direct callers/injectors (depth 1) + - Transitive dependents (depth 2) + - Route-level impact (external callers) + +## Stop conditions + +- Depth limit reached (≤ 2 hops). +- No more inbound edges to walk. +- Cycle detected (node already in impact set). + +## Recursion limit + +- Depth ≤ 2 from the target symbol. +- Maximum 8 `neighbors` calls total. + +## Worked example + +User: /impact-of ChatRepository +You: → resolve(identifier="ChatRepository", hint_kind="symbol") + → sym:com.bank.chat.core.repository.ChatRepository + → describe(id="sym:...") → edge_summary shows CALLS in, INJECTS in + → neighbors(in, ["CALLS", "INJECTS", "IMPLEMENTS", "EXTENDS"]) + → callers: ChatService#save, ChatService#findById + → injectors: ChatService (constructor injection) + → neighbors(in, ["CALLS", "INJECTS"]) on ChatService + → callers of ChatService: ChatController methods + → impact: ChatRepository → ChatService → ChatController (depth 2) + +## Out of scope + +- Exact line-level change impact (use `git diff` + source reading). +- Noise-filtered call maps (use `/mini-map`). diff --git a/skills/implements/SKILL.md b/skills/implements/SKILL.md new file mode 100644 index 0000000..7b8337d --- /dev/null +++ b/skills/implements/SKILL.md @@ -0,0 +1,26 @@ +--- +name: implements +description: Show concrete classes that implement an interface. Use when the user asks "what implements X", "implementations of X", or "concrete types for interface X". Argument is a type sym: id or identifier. +--- + +# /implements — Concrete implementors of an interface + +## Argument contract + +Single positional argument: a **type** Symbol id (`sym:...` preferred) OR an identifier-shaped string (FQN, simple name) → `resolve(identifier=..., hint_kind="symbol")`. + +## Steps + +1. **Resolve.** If the argument starts with `sym:`, use it. Otherwise: + `resolve(identifier=, hint_kind="symbol")` → on `one`, use `node.id`; on `many`, list `candidates` and stop; on `none`, try `search(query=, limit=5)` and stop if still empty. +2. **Implementors:** + `neighbors({ids: , direction: "in", edge_types: ["IMPLEMENTS"]})`. + Render each implementor's `fqn` + `microservice`. + +## Worked example + +User: /implements OperatorAssignmentService +You: → resolve(identifier="OperatorAssignmentService", hint_kind="symbol") + → sym:com.bank.chat.assign.service.OperatorAssignmentService (interface) + → neighbors({ids: "sym:...", direction: "in", edge_types: ["IMPLEMENTS"]}) + → returns concrete classes implementing the interface diff --git a/skills/injects/SKILL.md b/skills/injects/SKILL.md new file mode 100644 index 0000000..4c62f8b --- /dev/null +++ b/skills/injects/SKILL.md @@ -0,0 +1,26 @@ +--- +name: injects +description: Show where a type is injected via dependency injection. Use when the user asks "where is X injected", "who injects X", or "what depends on X via DI". Argument is a type sym: id or identifier. +--- + +# /injects — Where a type is injected + +## Argument contract + +Single positional argument: a **type** Symbol id (`sym:...` preferred) OR an identifier-shaped string (FQN, simple name) → `resolve(identifier=..., hint_kind="symbol")`. + +## Steps + +1. **Resolve.** If the argument starts with `sym:`, use it. Otherwise: + `resolve(identifier=, hint_kind="symbol")` → on `one`, use `node.id`; on `many`, list `candidates` and stop; on `none`, try `search(query=, limit=5)` and stop if still empty. +2. **Injection sites:** + `neighbors({ids: , direction: "in", edge_types: ["INJECTS"]})`. + Render each injection site's `fqn` + `microservice` + edge `attrs.mechanism` + `attrs.field_or_param`. + +## Worked example + +User: /injects OperatorAssignmentService +You: → resolve(identifier="OperatorAssignmentService", hint_kind="symbol") + → sym:com.bank.chat.assign.service.OperatorAssignmentService + → neighbors({ids: "sym:...", direction: "in", edge_types: ["INJECTS"]}) + → returns types/methods that inject OperatorAssignmentService diff --git a/skills/mini-map/SKILL.md b/skills/mini-map/SKILL.md new file mode 100644 index 0000000..df6aadf --- /dev/null +++ b/skills/mini-map/SKILL.md @@ -0,0 +1,102 @@ +--- +name: mini-map +description: Noise-filtered call map for a method. Shows delegation, persistence, and publish seams without entity accessor or JDK noise. Use when /callees returns too many rows or the user asks "map what X does", "simplify the call graph for X", or "what does X actually do". Argument is a sym: id or identifier, with optional depth. +--- + +# /mini-map — Noise-filtered call map for a method + +## Argument contract + +- Required: seed id — `sym:` id or identifier-shaped string → `resolve(identifier=..., hint_kind="symbol")`. +- Optional: `depth` (default 2, max 4) — recursion depth on DELEGATES and PUBLISHES targets. +- Optional: `microservice` — scope filter. + +## Steps + +### Step 1 — Resolve + +If the argument starts with `sym:`, use it. Otherwise: +`resolve(identifier=, hint_kind="symbol")` → on `one`, use `node.id`; on `many`, list `candidates` and stop; on `none`, try `search(query=, limit=5)` and stop if still empty. + +### Step 2 — Fetch ordered CALLS + +`neighbors({ids: , direction: "out", edge_types: ["CALLS"]})`. + +Rows are source-ordered (`call_site_line`, `call_site_byte`). After ontology 15, true receiver-failure sites are **not** on `CALLS` — they are `UnresolvedCallSite` nodes. `attrs.resolved=false` on remaining `CALLS` rows means known-receiver-external (JDK/Spring) callees, not receiver failure. + +### Step 3 — Optional MCP pre-filter + +When the raw CALLS set is large (e.g. > 30 rows), prefer MCP-side filtering over hand-rolled rules: + +- **Skeleton pass** (delegation hops): `neighbors(out, ["CALLS"], edge_filter={callee_declaring_role: "SERVICE"})`. +- **Trim JDK/low-signal**: `neighbors(out, ["CALLS"], edge_filter={min_confidence: 0.5})` and/or `edge_filter={exclude_callee_declaring_roles: ["OTHER"]}` (blunt — also drops known-external rows; document in output). +- **Collapse identical callees**: `neighbors(out, ["CALLS"], dedup_calls=True)`. +- **Full transcript with unresolved sites**: `neighbors(out, ["CALLS"], include_unresolved=True)` **only when not using `edge_filter`** on the same call (mutual exclusivity). + +### Step 4 — Skill heuristics + +What `callee_declaring_role` cannot do (accessor noise + semantic labels): + +1. **Skip entity accessors.** Callee simple name matches `get*` / `set*` / `is*` / `` on types matching `*Entity`, `*Request`, `*Response`, `*Event`, `*DTO`, or parent `role=DTO`. +2. **Skip JDK/library** when step 3 did not run: callee `fqn` prefix `java.`, `javax.`, `org.slf4j.`, `lombok.`. +3. **Classify remainder** (use `attrs.callee_declaring_role` when present, else callee parent `role` from `describe`): + - `REPOSITORY` / `MAPPER` → `PERSISTS` (save*/delete*) or `READS` (find*/get*). + - `SERVICE` or listener/scheduled capabilities → `DELEGATES`. + - `CLIENT` or publisher component → `PUBLISHES`. + - Else → `CALLS`. +4. **Deduplicate for display.** Same callee FQN → one line with `(×N)`. + +### Step 5 — Recurse + +On `DELEGATES` and `PUBLISHES` targets, repeat steps 2–4 up to `depth` (default 2, max 4). + +### Step 6 — Render output + +``` +() + DELEGATES → …Service#method + PERSISTS → …Repository#save (×2) + READS → …Repository#findById + [filtered ~N edges: ~A accessors, ~B JDK/OTHER, ~C deduped] +``` + +The `[filtered ...]` line is transparency. Offer raw `/callees` or `neighbors` with a documented `edge_filter` if the map looks too thin (< 3 signal lines). + +## Stop conditions + +- Depth limit reached. +- No `DELEGATES` or `PUBLISHES` targets to recurse on. +- Cycle detected (callee already in map). + +## Recursion limit + +- Default depth 2, max 4. +- When running without subagent: default depth 1, cap total raw edges examined per hop. + +## Subagent preference + +This skill is designed for subagent invocation. The subagent runs the MCP + heuristic pipeline per hop in its own context and returns a compact map. The main agent drills in with file reads. + +**Graceful degradation:** on hosts without subagents (or tight context budgets), run in the main agent with depth default 1. If the map has fewer than 3 signal lines after filtering, fall back to raw `/callees` (optionally with `edge_filter`) and note that stereotype roles may be `OTHER`. + +## Worked example + +User: /mini-map ClientMessageProcessor#process +You: → resolve(identifier="ClientMessageProcessor#process", hint_kind="symbol") + → sym:com.bank.chat.core.processor.ClientMessageProcessor#process(ProcessingContext, InternalEvent) + → neighbors(out, ["CALLS"]) → ~49 rows (post-ontology-15 re-index) + → optional edge_filter={callee_declaring_role: "SERVICE"} → skeleton pass + → skill classify → ~8–12 signal rows + → Output: + ClientMessageProcessor#process(ProcessingContext, InternalEvent) + DELEGATES → …SplitResolverService#… + DELEGATES → …DistributionTriggerPublisher#… + PERSISTS → …Repository#save (×2) + READS → …Repository#find… + [filtered ~37 edges: ~22 accessors, ~10 JDK/OTHER, ~5 deduped] + +## Out of scope + +- Cross-service tracing (use `/trace-request-flow`). +- Impact analysis (use `/impact-of`). +- Replacing MCP `edge_filter` — this skill **composes** MCP filters; heuristics cover accessor noise and semantic labels only. diff --git a/skills/nl/SKILL.md b/skills/nl/SKILL.md new file mode 100644 index 0000000..a0e5354 --- /dev/null +++ b/skills/nl/SKILL.md @@ -0,0 +1,33 @@ +--- +name: nl +description: Natural-language search into the graph. Use when the user asks a fuzzy question like "find authentication code", "where is X handled", or "show me Y". Argument is free-form text. +--- + +# /nl — Natural-language to graph navigation + +## Argument contract + +Single positional argument: free-form text describing what to find. + +## Steps + +1. **Search.** + `search(query=, limit=8)` — review results for strong `symbol_id` fit (role, `symbol_kind`, `microservice` alignment). +2. **Inspect top hit.** + When a result has a `symbol_id`, call `describe(id=)` to get the full record and `edge_summary`. +3. **Stop or walk.** + If the describe answers the question, stop. Otherwise use `neighbors` with relevant `edge_types` from the `edge_summary`. + +## Worked example + +User: /nl operator assignment +You: → search(query="operator assignment", limit=8) + → top hit: sym:com.bank.chat.assign.service.OperatorAssignmentService + → describe(id="sym:com.bank.chat.assign.service.OperatorAssignmentService") + → returns full record with edge_summary showing CALLS, INJECTS edges + → agent can now walk with neighbors if needed + +## Out of scope + +- Structured listing by role or kind (use `/controllers`, `/routes`, etc.). +- Identifier-shaped input where `resolve` would be more precise. diff --git a/skills/routes/SKILL.md b/skills/routes/SKILL.md new file mode 100644 index 0000000..efd0cc1 --- /dev/null +++ b/skills/routes/SKILL.md @@ -0,0 +1,28 @@ +--- +name: routes +description: List HTTP and messaging routes, optionally filtered by microservice. Use when the user asks "list routes", "show me endpoints", or "what routes are in X". +--- + +# /routes — List HTTP and messaging routes + +## Argument contract + +Optional positional argument: microservice name. Omit to list all routes. + +## Steps + +1. **Find routes.** + - With microservice: `find(kind="route", filter={microservice: })`. + - Without microservice: `find(kind="route", filter={})`. +2. **Render.** Show each result's `fqn` (HTTP method + path), `microservice`, `framework`, and `id`. + +## Worked example + +User: /routes chat-assign +You: → find(kind="route", filter={microservice: "chat-assign"}) + → returns routes in chat-assign microservice + → e.g. route:POST /chat/assign, route:GET /chat/status + +User: /routes +You: → find(kind="route", filter={}) + → returns all routes across all microservices diff --git a/skills/trace-request-flow/SKILL.md b/skills/trace-request-flow/SKILL.md new file mode 100644 index 0000000..45bf424 --- /dev/null +++ b/skills/trace-request-flow/SKILL.md @@ -0,0 +1,59 @@ +--- +name: trace-request-flow +description: Follow a request from HTTP entry point through the call chain to persistence or async boundaries. Use when the user asks "trace POST /foo", "follow the request for X", or "what happens when X is called". Argument is a route id, METHOD /path string, or route identifier. +--- + +# /trace-request-flow — Follow a request end-to-end + +## Argument contract + +Single positional argument: a route identifier — `route:` id, `METHOD /path` string, or path fragment → `resolve(identifier=..., hint_kind="route")` or `find(kind="route", filter={path_prefix: ...})`. + +## Steps + +1. **Resolve route.** + - If argument starts with `route:` or `r:`, use it directly. + - Otherwise: `resolve(identifier=, hint_kind="route")` → on `one`, use `node.id`; on `many`, list `candidates` and stop; on `none`, try `find(kind="route", filter={path_prefix: })` and stop if still empty. +2. **Handler method.** + `neighbors({ids: , direction: "in", edge_types: ["EXPOSES"]})` → handler method `sym:` id. +3. **Walk call chain** (depth ≤ 4 on methods): + `neighbors({ids: , direction: "out", edge_types: ["CALLS"]})`. + For each callee method: + - If it is a SERVICE/COMPONENT method likely to delegate further, recurse one more hop. + - If it is a REPOSITORY/MAPPER, classify as persistence and stop on that branch. +4. **Cross-service boundaries:** + At methods with outbound clients: `neighbors(out, ["DECLARES_CLIENT"])` on method id + → `neighbors(out, ["HTTP_CALLS"])` on each client id. + At methods with async producers: `neighbors(out, ["DECLARES_PRODUCER"])` on method id + → `neighbors(out, ["ASYNC_CALLS"])` on each producer id. +5. **Render ordered sequence.** Show the flow as: + `Route → Handler → Service → ... → Repository / Client / Producer` + with edge annotations at boundaries. + +## Stop conditions + +- Depth limit reached (≤ 4 from handler). +- No more `CALLS` edges to follow. +- All branches terminated at REPOSITORY, MAPPER, CLIENT, or PRODUCER endpoints. +- Cycle detected (method already in trace). + +## Recursion limit + +- Depth ≤ 4 from handler method. +- Maximum 10 `neighbors` calls total. + +## Worked example + +User: /trace-request-flow POST /chat/join +You: → resolve(identifier="POST /chat/join", hint_kind="route") + → route:POST /chat/join + → neighbors(in, ["EXPOSES"]) → sym:ChatController#joinOperator + → neighbors(out, ["CALLS"]) → sym:ChatService#join + → neighbors(out, ["CALLS"]) on ChatService#join → Repository#save + → (optional) neighbors(out, ["DECLARES_CLIENT"]) → client ids + → Render: POST /chat/join → ChatController#joinOperator → ChatService#join → Repository#save + +## Out of scope + +- Full noise-filtered call map (use `/mini-map` for single-method deep dives). +- Impact analysis beyond the forward path (use `/impact-of`). diff --git a/skills/who-hits-route/SKILL.md b/skills/who-hits-route/SKILL.md new file mode 100644 index 0000000..09cb1e6 --- /dev/null +++ b/skills/who-hits-route/SKILL.md @@ -0,0 +1,33 @@ +--- +name: who-hits-route +description: Show all inbound paths to an HTTP or messaging route (HTTP_CALLS, ASYNC_CALLS, and EXPOSES). Use when the user asks "who calls this endpoint", "what hits this route", or "all callers of this route". Argument is a route: id or route identifier. +--- + +# /who-hits-route — All inbound paths to a route + +## Argument contract + +Single positional argument: a **route** id (`route:...` preferred) OR an identifier-shaped string (path, METHOD /path) → `resolve(identifier=..., hint_kind="route")`. + +## Steps + +1. **Resolve.** If the argument starts with `route:` or `r:`, use it. Otherwise: + `resolve(identifier=, hint_kind="route")` → on `one`, use `node.id`; on `many`, list `candidates` and stop; on `none`, try `find(kind="route", filter={path_prefix: })` and stop if still empty. +2. **All inbound:** + `neighbors({ids: , direction: "in", edge_types: ["HTTP_CALLS", "ASYNC_CALLS", "EXPOSES"]})`. + Render grouped by edge type: + - `EXPOSES` → handler method Symbol + - `HTTP_CALLS` → Client nodes (with `attrs.match`, `attrs.confidence`) + - `ASYNC_CALLS` → Producer nodes (with `attrs.match`, `attrs.confidence`) + +## Worked example + +User: /who-hits-route POST /chat/join +You: → resolve(identifier="POST /chat/join", hint_kind="route") + → route:POST /chat/join + → neighbors({ids: "route:...", direction: "in", edge_types: ["HTTP_CALLS", "ASYNC_CALLS", "EXPOSES"]}) + → returns EXPOSES from handler method + HTTP_CALLS from clients + ASYNC_CALLS from producers + +## Out of scope + +- In-process callers of a method (use `/callers`). diff --git a/tests/test_agent_skills_static.py b/tests/test_agent_skills_static.py new file mode 100644 index 0000000..689a8e5 --- /dev/null +++ b/tests/test_agent_skills_static.py @@ -0,0 +1,277 @@ +"""Static validation for skills/ directory SKILL.md files. + +Imports allowlists from production code (mcp_v2, java_ontology) — not +hand-maintained lists. Validates: + - frontmatter (name + description present) + - MCP tool names referenced in skill bodies + - find kind values + - direction values + - edge_types values + - Tier 2 body structure (stop conditions, recursion limit) +""" + +from __future__ import annotations + +import re +from pathlib import Path +from typing import get_args + +import pytest + +from java_ontology import NodeKind +from mcp_v2 import ComposedEdgeType, EdgeType + +# --------------------------------------------------------------------------- +# Allowlists sourced from production code +# --------------------------------------------------------------------------- + +_VALID_TOOLS: frozenset[str] = frozenset(["search", "find", "describe", "neighbors", "resolve"]) + +_VALID_KINDS: frozenset[str] = frozenset(k.lower() for k in get_args(NodeKind)) + +_VALID_DIRECTIONS: frozenset[str] = frozenset(["in", "out"]) + +_ALL_EDGE_TYPES: frozenset[str] = frozenset(get_args(EdgeType)) | frozenset(get_args(ComposedEdgeType)) + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + +SKILLS_DIR = Path(__file__).resolve().parent.parent / "skills" + +TIER1_NAMES = [ + "nl", "controllers", "routes", "clients", + "callers", "callees", "handlers", "who-hits-route", + "implements", "injects", +] + +TIER2_NAMES = [ + "explain-feature", "impact-of", "trace-request-flow", "mini-map", +] + +ALL_SKILL_NAMES = TIER1_NAMES + TIER2_NAMES + + +def _parse_frontmatter(text: str) -> dict[str, str]: + """Parse simple YAML frontmatter (key: value pairs only).""" + m = re.match(r"^---\n(.*?)\n---", text, re.DOTALL) + if not m: + return {} + result: dict[str, str] = {} + for line in m.group(1).splitlines(): + if ":" in line: + key, _, value = line.partition(":") + result[key.strip()] = value.strip() + return result + + +def _extract_tool_refs(body: str) -> set[str]: + """Extract tool names referenced in MCP call patterns.""" + # Match patterns like `search(...)`, `find(kind=...)`, `describe(id=...)`, + # `neighbors({ids:`, `resolve(identifier=`, also backtick-wrapped names. + refs: set[str] = set() + for m in re.finditer(r"`(search|find|describe|neighbors|resolve)\b", body): + refs.add(m.group(1)) + # Also catch patterns like search(query=...) find(kind=...) without backticks + for m in re.finditer(r"\b(search|find|describe|neighbors|resolve)\s*[\(\{]", body): + refs.add(m.group(1)) + return refs + + +def _extract_kind_refs(body: str) -> set[str]: + """Extract find kind values from skill body.""" + refs: set[str] = set() + for m in re.finditer(r'kind\s*=\s*["\']?(\w+)["\']?', body): + val = m.group(1).lower() + if val in _VALID_KINDS: + refs.add(val) + return refs + + +def _extract_direction_refs(body: str) -> set[str]: + """Extract direction values from skill body.""" + refs: set[str] = set() + for m in re.finditer(r'direction\s*:\s*["\']?(in|out)["\']?', body): + refs.add(m.group(1)) + return refs + + +def _extract_edge_type_refs(body: str) -> set[str]: + """Extract edge_types values referenced in skill body.""" + refs: set[str] = set() + # Match edge_types lists: ["CALLS"] or ["HTTP_CALLS","ASYNC_CALLS","EXPOSES"] + for m in re.finditer(r'edge_types\s*:\s*\[([^\]]+)\]', body): + inner = m.group(1) + for val in re.findall(r'"(\w[\w.]*)"', inner): + if val in _ALL_EDGE_TYPES: + refs.add(val) + # Also match quoted edge names in backticked patterns + for m in re.finditer(r'\["(\w[\w.]*)"', body): + val = m.group(1) + if val in _ALL_EDGE_TYPES: + refs.add(val) + return refs + + +def _read_skill(name: str) -> tuple[dict[str, str], str]: + """Read a skill's SKILL.md and return (frontmatter, body).""" + path = SKILLS_DIR / name / "SKILL.md" + text = path.read_text(encoding="utf-8") + fm = _parse_frontmatter(text) + # Body is everything after the closing --- + body = re.sub(r"^---\n.*?\n---\n*", "", text, count=1, flags=re.DOTALL) + return fm, body + + +# --------------------------------------------------------------------------- +# Parametrized test ids +# --------------------------------------------------------------------------- + +@pytest.fixture(params=ALL_SKILL_NAMES, ids=lambda n: f"skill:{n}") +def skill_name(request): + return request.param + + +# --------------------------------------------------------------------------- +# Tests +# --------------------------------------------------------------------------- + + +class TestSkillFrontmatter: + """Every SKILL.md must have valid frontmatter.""" + + @pytest.mark.parametrize("name", ALL_SKILL_NAMES) + def test_frontmatter_has_name_and_description(self, name: str): + fm, _ = _read_skill(name) + assert "name" in fm, f"skills/{name}/SKILL.md missing frontmatter 'name'" + assert fm["name"] == name, f"skills/{name}/SKILL.md: name={fm['name']!r}, expected {name!r}" + assert "description" in fm, f"skills/{name}/SKILL.md missing frontmatter 'description'" + assert len(fm["description"]) >= 20, ( + f"skills/{name}/SKILL.md description too short ({len(fm['description'])} chars)" + ) + + @pytest.mark.parametrize("name", ALL_SKILL_NAMES) + def test_skill_file_exists(self, name: str): + path = SKILLS_DIR / name / "SKILL.md" + assert path.is_file(), f"Missing skills/{name}/SKILL.md" + + +class TestMCPToolReferences: + """Tool names in skill bodies must be valid MCP navigation tools.""" + + @pytest.mark.parametrize("name", ALL_SKILL_NAMES) + def test_tool_refs_are_valid(self, name: str): + _, body = _read_skill(name) + refs = _extract_tool_refs(body) + invalid = refs - _VALID_TOOLS + assert not invalid, f"skills/{name}/SKILL.md references invalid tools: {invalid}" + + @pytest.mark.parametrize("name", ALL_SKILL_NAMES) + def test_skill_references_at_least_one_tool(self, name: str): + _, body = _read_skill(name) + refs = _extract_tool_refs(body) + assert refs, f"skills/{name}/SKILL.md references no MCP tools" + + +class TestKindAndEdgeReferences: + """Kind, direction, and edge_type values must match production allowlists.""" + + @pytest.mark.parametrize("name", ALL_SKILL_NAMES) + def test_kind_refs_are_valid(self, name: str): + _, body = _read_skill(name) + refs = _extract_kind_refs(body) + invalid = refs - _VALID_KINDS + assert not invalid, f"skills/{name}/SKILL.md references invalid find kinds: {invalid}" + + @pytest.mark.parametrize("name", ALL_SKILL_NAMES) + def test_direction_refs_are_valid(self, name: str): + _, body = _read_skill(name) + refs = _extract_direction_refs(body) + invalid = refs - _VALID_DIRECTIONS + assert not invalid, f"skills/{name}/SKILL.md references invalid directions: {invalid}" + + @pytest.mark.parametrize("name", ALL_SKILL_NAMES) + def test_edge_type_refs_are_valid(self, name: str): + _, body = _read_skill(name) + refs = _extract_edge_type_refs(body) + invalid = refs - _ALL_EDGE_TYPES + assert not invalid, f"skills/{name}/SKILL.md references invalid edge_types: {invalid}" + + +class TestTier2BodyStructure: + """Tier 2 skills must have stop conditions and recursion limits.""" + + @pytest.mark.parametrize("name", TIER2_NAMES) + def test_has_stop_conditions(self, name: str): + _, body = _read_skill(name) + assert "## Stop conditions" in body, f"skills/{name}/SKILL.md missing '## Stop conditions'" + + @pytest.mark.parametrize("name", TIER2_NAMES) + def test_has_recursion_limit(self, name: str): + _, body = _read_skill(name) + assert "## Recursion limit" in body, f"skills/{name}/SKILL.md missing '## Recursion limit'" + + def test_mini_map_has_classification_rules(self): + _, body = _read_skill("mini-map") + assert "### Step 4 — Skill heuristics" in body or "Classification" in body, ( + "skills/mini-map/SKILL.md missing classification rules" + ) + + def test_mini_map_has_output_shape(self): + _, body = _read_skill("mini-map") + assert "PERSISTS" in body and "DELEGATES" in body, ( + "skills/mini-map/SKILL.md missing output shape (PERSISTS/DELEGATES labels)" + ) + + +class TestWorkedExamples: + """Every skill must have a worked example section.""" + + @pytest.mark.parametrize("name", ALL_SKILL_NAMES) + def test_has_worked_example(self, name: str): + _, body = _read_skill(name) + assert "## Worked example" in body, f"skills/{name}/SKILL.md missing '## Worked example'" + + +class TestDirectoryIntegrity: + """skills/ directory must contain exactly the expected skills.""" + + def test_no_extra_skill_dirs(self): + actual = {p.name for p in SKILLS_DIR.iterdir() if p.is_dir() and (p / "SKILL.md").exists()} + expected = set(ALL_SKILL_NAMES) + extra = actual - expected + assert not extra, f"Unexpected skill directories: {extra}" + + def test_no_missing_skill_dirs(self): + actual = {p.name for p in SKILLS_DIR.iterdir() if p.is_dir() and (p / "SKILL.md").exists()} + expected = set(ALL_SKILL_NAMES) + missing = expected - actual + assert not missing, f"Missing skill directories: {missing}" + + def test_readme_exists(self): + assert (SKILLS_DIR / "README.md").is_file(), "skills/README.md missing" + + +class TestAgentGuideConsistency: + """AGENT-GUIDE.md slash-aliases must point at skills/, not embed chains.""" + + def test_guide_references_skills_directory(self): + guide = Path(__file__).resolve().parent.parent / "docs" / "AGENT-GUIDE.md" + text = guide.read_text(encoding="utf-8") + assert "skills/" in text, "docs/AGENT-GUIDE.md must reference skills/ directory" + assert "skills/README.md" in text or "skills/" in text, ( + "docs/AGENT-GUIDE.md must point to skills/ for navigation commands" + ) + + def test_guide_does_not_embed_full_slash_alias_bullets(self): + """The old slash-style aliases section embedded full MCP chains. + After the rewrite, it must reference skills/ instead.""" + guide = Path(__file__).resolve().parent.parent / "docs" / "AGENT-GUIDE.md" + text = guide.read_text(encoding="utf-8") + # The old format had lines like: /nl → search({"query":...}) + # After rewrite, these should be gone (replaced by skills/ pointers) + old_pattern = re.compile(r'^\- `/(nl|controllers|routes|clients|callers|callees|handlers|who-hits-route|implements|injects)\s+.*→\s*`(search|find|describe|neighbors)', re.MULTILINE) + assert not old_pattern.search(text), ( + "docs/AGENT-GUIDE.md still contains old embedded slash-alias MCP chains — " + "should reference skills/ instead" + ) From d5120ea0976b647f1950207a44790b63845bb6c9 Mon Sep 17 00:00:00 2001 From: Dmitry Teryaev Date: Sun, 24 May 2026 18:54:26 +0300 Subject: [PATCH 2/5] =?UTF-8?q?chore:=20update=20README=20=E2=80=94=20ship?= =?UTF-8?q?=20skills=20references=20+=20layer-3=20diagram?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.7 --- README.md | 26 +++++++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 8762f1d..4ae46aa 100644 --- a/README.md +++ b/README.md @@ -97,8 +97,8 @@ See [`mcp.json.example`](./mcp.json.example) for the same shape in `.mcp.json` ( ### Driving the MCP from an agent -- **[`docs/AGENT-GUIDE.md`](./docs/AGENT-GUIDE.md)** — standalone MCP operating manual (copy-paste into `QWEN.md` / `CLAUDE.md` / `AGENTS.md`): five tools, `NodeFilter`, edge taxonomy, required `neighbors` arguments, ontology glossary, recovery playbook, slash-style aliases. -- **[`skills/`](./skills/)** — user-facing navigation and workflow skills for java-codebase-rag consumers. Skills are `SKILL.md` files; agents discover them via slash-names (`/callees`, `/routes`, etc.). See [`propose/active/AGENT-SKILLS-AND-COMMANDS-PROPOSE.md`](./propose/active/AGENT-SKILLS-AND-COMMANDS-PROPOSE.md) for the full Tier 1 + Tier 2 skill set. +- **[`docs/AGENT-GUIDE.md`](./docs/AGENT-GUIDE.md)** — standalone MCP operating manual (copy-paste into `QWEN.md` / `CLAUDE.md` / `AGENTS.md`): five tools, `NodeFilter`, edge taxonomy, required `neighbors` arguments, ontology glossary, recovery playbook, navigation skill pointers. +- **[`skills/`](./skills/)** — 14 shipped navigation and workflow skills (`SKILL.md` files) for java-codebase-rag consumers. Tier 1 = deterministic MCP chains (`/callers`, `/callees`, `/routes`, `/controllers`, `/clients`, `/handlers`, `/who-hits-route`, `/implements`, `/injects`, `/nl`). Tier 2 = bounded workflows (`/explain-feature`, `/impact-of`, `/trace-request-flow`, `/mini-map`). See [`skills/README.md`](./skills/README.md) for the full index. - **[`docs/MANUAL-VERIFICATION-CHECKLIST.md`](./docs/MANUAL-VERIFICATION-CHECKLIST.md)** — 7-phase agent-driven verification you run after indexing your real project. --- @@ -115,6 +115,26 @@ See [`mcp.json.example`](./mcp.json.example) for the same shape in `.mcp.json` ( Full schemas, `NodeFilter` / `EdgeFilter` semantics, and the hints contract live in [`docs/AGENT-GUIDE.md`](./docs/AGENT-GUIDE.md). Edge types and traversal directions are listed in [`docs/EDGE-NAVIGATION.md`](./docs/EDGE-NAVIGATION.md). +### Three-layer architecture + +``` +┌──────────────────────────────────────────────────────────────┐ +│ Layer 3 — High-level intents (what the user actually thinks) │ +│ /trace-request-flow, /callees, /controllers, /routes, │ +│ /impact-of, /mini-map │ +│ ───────────────────────────────────────────────────────── │ +│ Implementation: SKILL.md in skills/ at project root. │ +│ Tier 1 = deterministic chains; Tier 2 = bounded workflows │ +│ + /mini-map heuristics. │ +├──────────────────────────────────────────────────────────────┤ +│ Layer 2 — Composable primitives (the MCP API) │ +│ search, find, describe, neighbors, resolve │ +├──────────────────────────────────────────────────────────────┤ +│ Layer 1 — Storage primitives │ +│ Kuzu Cypher + LanceDB tables │ +└──────────────────────────────────────────────────────────────┘ +``` + --- ## Configuration @@ -156,7 +176,7 @@ Run `java-codebase-rag --help` to list grouped subcommands. Operator playbook wi | [`docs/CONFIGURATION.md`](./docs/CONFIGURATION.md) | Environment variables, project YAML, graph ontology, brownfield overrides, ignore patterns. | | [`docs/JAVA-CODEBASE-RAG-CLI.md`](./docs/JAVA-CODEBASE-RAG-CLI.md) | CLI operator playbook: workflows, exit codes, env alignment. | | [`docs/EDGE-NAVIGATION.md`](./docs/EDGE-NAVIGATION.md) | MCP-traversable edges, directions, dot-key composition. | -| [`skills/`](./skills/) | User-facing skills for java-codebase-rag consumers. Navigation and workflow skills (Tier 1 + Tier 2) planned — see [`propose/active/AGENT-SKILLS-AND-COMMANDS-PROPOSE.md`](./propose/active/AGENT-SKILLS-AND-COMMANDS-PROPOSE.md). | +| [`skills/`](./skills/) | 14 shipped navigation and workflow skills for java-codebase-rag consumers. Tier 1 = deterministic MCP chains; Tier 2 = bounded workflows. See [`skills/README.md`](./skills/README.md). | | [`docs/MANUAL-VERIFICATION-CHECKLIST.md`](./docs/MANUAL-VERIFICATION-CHECKLIST.md) | 7-phase agent-driven verification after indexing your project. | | [`docs/CODEBASE_REQUIREMENTS.md`](./docs/CODEBASE_REQUIREMENTS.md) | Assumptions about your Java repo + per-file edit map for non-conforming codebases. | | [`automation/cursor_propose_only/README.md`](./automation/cursor_propose_only/README.md) | Optional proposal orchestration workflow (single-command autopilot, planning bundles, automated execution/review loops). | From e77ba948ace12da09d64b923d2f9f4ee7f706110 Mon Sep 17 00:00:00 2001 From: Dmitry Teryaev Date: Sun, 24 May 2026 21:44:57 +0300 Subject: [PATCH 3/5] fix(skills): add /producers, deduplicate diagram, harden test Review feedback for #225: - Add missing /producers Tier 1 skill (was in old AGENT-GUIDE slash aliases) - Deduplicate three-layer diagram: canonical copy in skills/README.md, README.md references it - Document edge_filter validation gap in test module docstring - Build AGENT-GUIDE consistency regex from ALL_SKILL_NAMES instead of hardcoded skill name list Co-Authored-By: Claude Opus 4.7 --- README.md | 22 +++------------------- skills/README.md | 1 + skills/producers/SKILL.md | 28 ++++++++++++++++++++++++++++ tests/test_agent_skills_static.py | 15 +++++++++++++-- 4 files changed, 45 insertions(+), 21 deletions(-) create mode 100644 skills/producers/SKILL.md diff --git a/README.md b/README.md index 4ae46aa..27dfd31 100644 --- a/README.md +++ b/README.md @@ -98,7 +98,7 @@ See [`mcp.json.example`](./mcp.json.example) for the same shape in `.mcp.json` ( ### Driving the MCP from an agent - **[`docs/AGENT-GUIDE.md`](./docs/AGENT-GUIDE.md)** — standalone MCP operating manual (copy-paste into `QWEN.md` / `CLAUDE.md` / `AGENTS.md`): five tools, `NodeFilter`, edge taxonomy, required `neighbors` arguments, ontology glossary, recovery playbook, navigation skill pointers. -- **[`skills/`](./skills/)** — 14 shipped navigation and workflow skills (`SKILL.md` files) for java-codebase-rag consumers. Tier 1 = deterministic MCP chains (`/callers`, `/callees`, `/routes`, `/controllers`, `/clients`, `/handlers`, `/who-hits-route`, `/implements`, `/injects`, `/nl`). Tier 2 = bounded workflows (`/explain-feature`, `/impact-of`, `/trace-request-flow`, `/mini-map`). See [`skills/README.md`](./skills/README.md) for the full index. +- **[`skills/`](./skills/)** — 15 shipped navigation and workflow skills (`SKILL.md` files) for java-codebase-rag consumers. Tier 1 = deterministic MCP chains (`/callers`, `/callees`, `/routes`, `/controllers`, `/clients`, `/producers`, `/handlers`, `/who-hits-route`, `/implements`, `/injects`, `/nl`). Tier 2 = bounded workflows (`/explain-feature`, `/impact-of`, `/trace-request-flow`, `/mini-map`). See [`skills/README.md`](./skills/README.md) for the full index. - **[`docs/MANUAL-VERIFICATION-CHECKLIST.md`](./docs/MANUAL-VERIFICATION-CHECKLIST.md)** — 7-phase agent-driven verification you run after indexing your real project. --- @@ -117,23 +117,7 @@ Full schemas, `NodeFilter` / `EdgeFilter` semantics, and the hints contract live ### Three-layer architecture -``` -┌──────────────────────────────────────────────────────────────┐ -│ Layer 3 — High-level intents (what the user actually thinks) │ -│ /trace-request-flow, /callees, /controllers, /routes, │ -│ /impact-of, /mini-map │ -│ ───────────────────────────────────────────────────────── │ -│ Implementation: SKILL.md in skills/ at project root. │ -│ Tier 1 = deterministic chains; Tier 2 = bounded workflows │ -│ + /mini-map heuristics. │ -├──────────────────────────────────────────────────────────────┤ -│ Layer 2 — Composable primitives (the MCP API) │ -│ search, find, describe, neighbors, resolve │ -├──────────────────────────────────────────────────────────────┤ -│ Layer 1 — Storage primitives │ -│ Kuzu Cypher + LanceDB tables │ -└──────────────────────────────────────────────────────────────┘ -``` +Layer 1 (storage) → Layer 2 (5 MCP tools) → Layer 3 (skills). Navigation skills in [`skills/`](./skills/) wrap the MCP tools into deterministic chains (Tier 1) and bounded workflows (Tier 2). See the [architecture diagram in `skills/README.md`](./skills/README.md#three-layer-architecture). --- @@ -176,7 +160,7 @@ Run `java-codebase-rag --help` to list grouped subcommands. Operator playbook wi | [`docs/CONFIGURATION.md`](./docs/CONFIGURATION.md) | Environment variables, project YAML, graph ontology, brownfield overrides, ignore patterns. | | [`docs/JAVA-CODEBASE-RAG-CLI.md`](./docs/JAVA-CODEBASE-RAG-CLI.md) | CLI operator playbook: workflows, exit codes, env alignment. | | [`docs/EDGE-NAVIGATION.md`](./docs/EDGE-NAVIGATION.md) | MCP-traversable edges, directions, dot-key composition. | -| [`skills/`](./skills/) | 14 shipped navigation and workflow skills for java-codebase-rag consumers. Tier 1 = deterministic MCP chains; Tier 2 = bounded workflows. See [`skills/README.md`](./skills/README.md). | +| [`skills/`](./skills/) | 15 shipped navigation and workflow skills for java-codebase-rag consumers. Tier 1 = deterministic MCP chains; Tier 2 = bounded workflows. See [`skills/README.md`](./skills/README.md). | | [`docs/MANUAL-VERIFICATION-CHECKLIST.md`](./docs/MANUAL-VERIFICATION-CHECKLIST.md) | 7-phase agent-driven verification after indexing your project. | | [`docs/CODEBASE_REQUIREMENTS.md`](./docs/CODEBASE_REQUIREMENTS.md) | Assumptions about your Java repo + per-file edit map for non-conforming codebases. | | [`automation/cursor_propose_only/README.md`](./automation/cursor_propose_only/README.md) | Optional proposal orchestration workflow (single-command autopilot, planning bundles, automated execution/review loops). | diff --git a/skills/README.md b/skills/README.md index 4dd7e8e..fb1022e 100644 --- a/skills/README.md +++ b/skills/README.md @@ -32,6 +32,7 @@ High-level intents over the 5-tool MCP (`search` / `find` / `describe` / `neighb | [`/controllers`](controllers/SKILL.md) | List controller classes | | [`/routes`](routes/SKILL.md) | List HTTP and messaging routes | | [`/clients`](clients/SKILL.md) | List outbound HTTP clients | +| [`/producers`](producers/SKILL.md) | List outbound async producers | | [`/callers`](callers/SKILL.md) | Who calls this method (in-process CALLS) | | [`/callees`](callees/SKILL.md) | What this method calls (in-process CALLS) | | [`/handlers`](handlers/SKILL.md) | Method that handles a route | diff --git a/skills/producers/SKILL.md b/skills/producers/SKILL.md new file mode 100644 index 0000000..cf601b1 --- /dev/null +++ b/skills/producers/SKILL.md @@ -0,0 +1,28 @@ +--- +name: producers +description: List outbound async producers, optionally filtered by microservice. Use when the user asks "list producers", "show outbound async calls", or "what Kafka producers are in X". +--- + +# /producers — List outbound async producers + +## Argument contract + +Optional positional argument: microservice name. Omit to list all producers. + +## Steps + +1. **Find producers.** + - With microservice: `find(kind="producer", filter={microservice: }, limit=100)`. + - Without microservice: `find(kind="producer", filter={}, limit=100)`. +2. **Render.** Show each result's `fqn`, `microservice`, `producer_kind`, `topic_prefix`, and `id`. + +## Worked example + +User: /producers chat-core +You: → find(kind="producer", filter={microservice: "chat-core"}, limit=100) + → returns outbound async producer nodes in chat-core + → e.g. producer:ChatEventPublisher (kafka_send), topic_prefix=chat-events + +User: /producers +You: → find(kind="producer", filter={}, limit=100) + → returns all outbound async producer nodes diff --git a/tests/test_agent_skills_static.py b/tests/test_agent_skills_static.py index 689a8e5..bc8fbee 100644 --- a/tests/test_agent_skills_static.py +++ b/tests/test_agent_skills_static.py @@ -8,6 +8,13 @@ - direction values - edge_types values - Tier 2 body structure (stop conditions, recursion limit) + +Known gap (intentional — see AGENT-SKILLS-AND-COMMANDS-PROPOSE §11): + - edge_filter parameters (callee_declaring_role, min_confidence, + exclude_callee_declaring_roles, dedup_calls, include_unresolved) + referenced in /mini-map are NOT validated against mcp_v2 parameter + definitions. The static validator does not parse edge_filter dicts. + On re-index, manually verify /mini-map against the MCP surface. """ from __future__ import annotations @@ -40,7 +47,7 @@ SKILLS_DIR = Path(__file__).resolve().parent.parent / "skills" TIER1_NAMES = [ - "nl", "controllers", "routes", "clients", + "nl", "controllers", "routes", "clients", "producers", "callers", "callees", "handlers", "who-hits-route", "implements", "injects", ] @@ -270,7 +277,11 @@ def test_guide_does_not_embed_full_slash_alias_bullets(self): text = guide.read_text(encoding="utf-8") # The old format had lines like: /nl → search({"query":...}) # After rewrite, these should be gone (replaced by skills/ pointers) - old_pattern = re.compile(r'^\- `/(nl|controllers|routes|clients|callers|callees|handlers|who-hits-route|implements|injects)\s+.*→\s*`(search|find|describe|neighbors)', re.MULTILINE) + skill_names_pattern = "|".join(re.escape(n) for n in ALL_SKILL_NAMES) + old_pattern = re.compile( + rf"^- `/(?:{skill_names_pattern})\s+.*→\s*`(?:search|find|describe|neighbors)", + re.MULTILINE, + ) assert not old_pattern.search(text), ( "docs/AGENT-GUIDE.md still contains old embedded slash-alias MCP chains — " "should reference skills/ instead" From 7a209322fa59581b741104b8478bdf6e24f29262 Mon Sep 17 00:00:00 2001 From: Dmitry Teryaev Date: Sun, 24 May 2026 21:53:24 +0300 Subject: [PATCH 4/5] fix(guide): restore self-contained copy-paste block, clarify options in README MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit AGENT-GUIDE.md is a standalone artifact designed to be copy-pasted into external projects. Restoring inline slash-style aliases inside the BEGIN/END block (not skills/ references which won't resolve externally). README.md now presents two options with clear priority: 1. Copy-paste AGENT-GUIDE (recommended for most) — self-contained 2. Use skills/ — for hosts with skill discovery (Claude Code, Qwen, Cursor) Test updated: verifies copy-paste block has inline aliases and no skills/ references; uses correct BEGIN/END marker strings. Co-Authored-By: Claude Opus 4.7 --- README.md | 12 ++++++--- docs/AGENT-GUIDE.md | 23 ++++++++++++----- tests/test_agent_skills_static.py | 43 ++++++++++++++++++------------- 3 files changed, 50 insertions(+), 28 deletions(-) diff --git a/README.md b/README.md index 27dfd31..13beb35 100644 --- a/README.md +++ b/README.md @@ -97,9 +97,13 @@ See [`mcp.json.example`](./mcp.json.example) for the same shape in `.mcp.json` ( ### Driving the MCP from an agent -- **[`docs/AGENT-GUIDE.md`](./docs/AGENT-GUIDE.md)** — standalone MCP operating manual (copy-paste into `QWEN.md` / `CLAUDE.md` / `AGENTS.md`): five tools, `NodeFilter`, edge taxonomy, required `neighbors` arguments, ontology glossary, recovery playbook, navigation skill pointers. -- **[`skills/`](./skills/)** — 15 shipped navigation and workflow skills (`SKILL.md` files) for java-codebase-rag consumers. Tier 1 = deterministic MCP chains (`/callers`, `/callees`, `/routes`, `/controllers`, `/clients`, `/producers`, `/handlers`, `/who-hits-route`, `/implements`, `/injects`, `/nl`). Tier 2 = bounded workflows (`/explain-feature`, `/impact-of`, `/trace-request-flow`, `/mini-map`). See [`skills/README.md`](./skills/README.md) for the full index. -- **[`docs/MANUAL-VERIFICATION-CHECKLIST.md`](./docs/MANUAL-VERIFICATION-CHECKLIST.md)** — 7-phase agent-driven verification you run after indexing your real project. +Pick **one** of two options (not both — they cover the same navigation intents): + +1. **[`docs/AGENT-GUIDE.md`](./docs/AGENT-GUIDE.md)** (recommended for most) — standalone MCP operating manual. Copy-paste the `BEGIN`/`END` block into your project's `QWEN.md`, `CLAUDE.md`, or `AGENTS.md`. Contains: five-tool reference, `NodeFilter` / edge taxonomy, ontology glossary, recovery playbook, and inline slash-style aliases (`/callers`, `/callees`, `/routes`, etc.) as prompt templates. Self-contained — no external file dependencies. + +2. **[`skills/`](./skills/)** (for hosts with skill discovery) — 15 shipped `SKILL.md` files. If your MCP host supports skill discovery (Claude Code, Qwen Code, Cursor), the same navigation intents are available as discoverable `/` commands. Tier 1 = deterministic MCP chains (`/callers`, `/callees`, `/routes`, `/controllers`, `/clients`, `/producers`, `/handlers`, `/who-hits-route`, `/implements`, `/injects`, `/nl`). Tier 2 = bounded workflows (`/explain-feature`, `/impact-of`, `/trace-request-flow`, `/mini-map`). See [`skills/README.md`](./skills/README.md) for the full index. + +Also: **[`docs/MANUAL-VERIFICATION-CHECKLIST.md`](./docs/MANUAL-VERIFICATION-CHECKLIST.md)** — 7-phase agent-driven verification you run after indexing your real project. --- @@ -160,7 +164,7 @@ Run `java-codebase-rag --help` to list grouped subcommands. Operator playbook wi | [`docs/CONFIGURATION.md`](./docs/CONFIGURATION.md) | Environment variables, project YAML, graph ontology, brownfield overrides, ignore patterns. | | [`docs/JAVA-CODEBASE-RAG-CLI.md`](./docs/JAVA-CODEBASE-RAG-CLI.md) | CLI operator playbook: workflows, exit codes, env alignment. | | [`docs/EDGE-NAVIGATION.md`](./docs/EDGE-NAVIGATION.md) | MCP-traversable edges, directions, dot-key composition. | -| [`skills/`](./skills/) | 15 shipped navigation and workflow skills for java-codebase-rag consumers. Tier 1 = deterministic MCP chains; Tier 2 = bounded workflows. See [`skills/README.md`](./skills/README.md). | +| [`skills/`](./skills/) | 15 navigation and workflow skills for hosts with skill discovery (alternative to copy-pasting AGENT-GUIDE). See [`skills/README.md`](./skills/README.md). | | [`docs/MANUAL-VERIFICATION-CHECKLIST.md`](./docs/MANUAL-VERIFICATION-CHECKLIST.md) | 7-phase agent-driven verification after indexing your project. | | [`docs/CODEBASE_REQUIREMENTS.md`](./docs/CODEBASE_REQUIREMENTS.md) | Assumptions about your Java repo + per-file edit map for non-conforming codebases. | | [`automation/cursor_propose_only/README.md`](./automation/cursor_propose_only/README.md) | Optional proposal orchestration workflow (single-command autopilot, planning bundles, automated execution/review loops). | diff --git a/docs/AGENT-GUIDE.md b/docs/AGENT-GUIDE.md index 88a646c..54b9e90 100644 --- a/docs/AGENT-GUIDE.md +++ b/docs/AGENT-GUIDE.md @@ -269,15 +269,26 @@ Returns **edges** with `attrs` (`confidence`, `strategy`, `match`, … on cross- After two failed attempts on the same intent, stop and report tool name, args, and response snippet. -### Navigation skills (`/` commands) - -Navigation intents (`/callees`, `/callers`, `/routes`, etc.) are shipped as SKILL.md files in [`skills/`](../skills/) at the project root. Each skill defines a deterministic MCP chain (Tier 1) or bounded workflow (Tier 2). See [`skills/README.md`](../skills/README.md) for the full index. - -When the user types a `/` intent matching a shipped skill, execute the chain from the corresponding `skills//SKILL.md`. For intents not covered by a skill, use the decision tree and raw MCP tools above. +### Slash-style aliases + +- `/nl ` → `search({"query":"","limit":8})` then `describe` on best `symbol_id`. +- `/controllers ` → `find({"kind":"symbol","filter":{"microservice":"","role":"CONTROLLER"}})`. +- `/routes ` → `find({"kind":"route","filter":{"microservice":""}})`. +- `/clients ` → `find({"kind":"client","filter":{"microservice":""},"limit":100})`. +- `/producers ` → `find({"kind":"producer","filter":{"microservice":""},"limit":100})`. +- `/callers ` → `neighbors({"ids":"","direction":"in","edge_types":["CALLS"]})`. +- `/callees ` → `neighbors({"ids":"","direction":"out","edge_types":["CALLS"]})`. +- `/handlers ` → `neighbors({"ids":"","direction":"in","edge_types":["EXPOSES"]})`. +- `/who-hits-route ` → `neighbors({"ids":"","direction":"in","edge_types":["HTTP_CALLS","ASYNC_CALLS","EXPOSES"]})`. +- `/implements ` → `neighbors({"ids":"","direction":"in","edge_types":["IMPLEMENTS"]})`. +- `/injects ` → `neighbors({"ids":"","direction":"in","edge_types":["INJECTS"]})`. ### Canonical workflow: "explain feature X" -Use the `/explain-feature` skill (Tier 2). The chain is: `search` → pick 1–3 hits → `describe` each → bounded `neighbors` walk until the question is answered. +1. `search` with a short query; pick 1–3 hits with strong `symbol_id` / role fit. +2. `describe` on the chosen id; read `edge_summary`. +3. Walk with `neighbors` using **small** `edge_types` sets (e.g. `CALLS` out, or `EXPOSES` / cross-service edges for boundaries). +4. Stop when you can answer; do not prefetch unrelated subgraphs. diff --git a/tests/test_agent_skills_static.py b/tests/test_agent_skills_static.py index bc8fbee..730ee9d 100644 --- a/tests/test_agent_skills_static.py +++ b/tests/test_agent_skills_static.py @@ -260,29 +260,36 @@ def test_readme_exists(self): class TestAgentGuideConsistency: - """AGENT-GUIDE.md slash-aliases must point at skills/, not embed chains.""" + """AGENT-GUIDE.md copy-paste block must be self-contained.""" - def test_guide_references_skills_directory(self): + def test_guide_has_inline_slash_aliases(self): + """The copy-paste block must include inline slash-alias bullets + (it's standalone — no external file references work in a consumer project).""" guide = Path(__file__).resolve().parent.parent / "docs" / "AGENT-GUIDE.md" text = guide.read_text(encoding="utf-8") - assert "skills/" in text, "docs/AGENT-GUIDE.md must reference skills/ directory" - assert "skills/README.md" in text or "skills/" in text, ( - "docs/AGENT-GUIDE.md must point to skills/ for navigation commands" + # Extract the copy-paste block (marker on its own line) + begin = text.find("") + end = text.find("") + assert begin != -1 and end != -1, "AGENT-GUIDE.md missing BEGIN/END markers" + block = text[begin:end] + assert "### Slash-style aliases" in block, ( + "AGENT-GUIDE.md copy-paste block missing '### Slash-style aliases'" ) + # Verify key aliases are present inline + for alias in ["/nl", "/callers", "/callees", "/routes", "/controllers"]: + assert alias in block, f"AGENT-GUIDE.md copy-paste block missing {alias} alias" - def test_guide_does_not_embed_full_slash_alias_bullets(self): - """The old slash-style aliases section embedded full MCP chains. - After the rewrite, it must reference skills/ instead.""" + def test_guide_copy_block_does_not_reference_skills_dir(self): + """The copy-paste block must not reference skills/ — it won't exist + in the consumer's project.""" guide = Path(__file__).resolve().parent.parent / "docs" / "AGENT-GUIDE.md" text = guide.read_text(encoding="utf-8") - # The old format had lines like: /nl → search({"query":...}) - # After rewrite, these should be gone (replaced by skills/ pointers) - skill_names_pattern = "|".join(re.escape(n) for n in ALL_SKILL_NAMES) - old_pattern = re.compile( - rf"^- `/(?:{skill_names_pattern})\s+.*→\s*`(?:search|find|describe|neighbors)", - re.MULTILINE, - ) - assert not old_pattern.search(text), ( - "docs/AGENT-GUIDE.md still contains old embedded slash-alias MCP chains — " - "should reference skills/ instead" + begin = text.find("") + end = text.find("") + assert begin != -1 and end != -1, "AGENT-GUIDE.md missing BEGIN/END markers" + block = text[begin:end] + assert "skills/" not in block, ( + "AGENT-GUIDE.md copy-paste block references skills/ — " + "this path won't resolve in a consumer project. " + "Keep skills/ references outside the copy-paste block." ) From 0d431c6d9de8fbc02b8ac976763919c2c5136c32 Mon Sep 17 00:00:00 2001 From: Dmitry Teryaev Date: Sun, 24 May 2026 21:54:43 +0300 Subject: [PATCH 5/5] fix(guide): replace slash aliases with navigation patterns table Slash-command aliases like /nl, /callers etc mislead agents into thinking these are real commands. Replace with a neutral navigation patterns table that documents the tool chains without implying invocable slash commands. Test updated: verifies copy-paste block has navigation patterns, no skills/ references, and no slash-command alias bullets. Co-Authored-By: Claude Opus 4.7 --- docs/AGENT-GUIDE.md | 31 +++++++++++++++----------- tests/test_agent_skills_static.py | 37 +++++++++++++++++++++++++------ 2 files changed, 48 insertions(+), 20 deletions(-) diff --git a/docs/AGENT-GUIDE.md b/docs/AGENT-GUIDE.md index 54b9e90..7fb41c7 100644 --- a/docs/AGENT-GUIDE.md +++ b/docs/AGENT-GUIDE.md @@ -269,19 +269,24 @@ Returns **edges** with `attrs` (`confidence`, `strategy`, `match`, … on cross- After two failed attempts on the same intent, stop and report tool name, args, and response snippet. -### Slash-style aliases - -- `/nl ` → `search({"query":"","limit":8})` then `describe` on best `symbol_id`. -- `/controllers ` → `find({"kind":"symbol","filter":{"microservice":"","role":"CONTROLLER"}})`. -- `/routes ` → `find({"kind":"route","filter":{"microservice":""}})`. -- `/clients ` → `find({"kind":"client","filter":{"microservice":""},"limit":100})`. -- `/producers ` → `find({"kind":"producer","filter":{"microservice":""},"limit":100})`. -- `/callers ` → `neighbors({"ids":"","direction":"in","edge_types":["CALLS"]})`. -- `/callees ` → `neighbors({"ids":"","direction":"out","edge_types":["CALLS"]})`. -- `/handlers ` → `neighbors({"ids":"","direction":"in","edge_types":["EXPOSES"]})`. -- `/who-hits-route ` → `neighbors({"ids":"","direction":"in","edge_types":["HTTP_CALLS","ASYNC_CALLS","EXPOSES"]})`. -- `/implements ` → `neighbors({"ids":"","direction":"in","edge_types":["IMPLEMENTS"]})`. -- `/injects ` → `neighbors({"ids":"","direction":"in","edge_types":["INJECTS"]})`. +### Common navigation patterns + +These patterns combine the five tools above. Use the decision tree to pick the right starting tool. + +| Intent | Tool chain | +| ------ | ---------- | +| Natural-language "find X" | `search(query=…, limit=8)` → `describe(top_hit.symbol_id)` | +| List controllers in service S | `find(kind="symbol", filter={microservice:"S", role:"CONTROLLER"})` | +| List routes in service S | `find(kind="route", filter={microservice:"S"})` | +| List clients in service S | `find(kind="client", filter={microservice:"S"}, limit=100)` | +| List producers in service S | `find(kind="producer", filter={microservice:"S"}, limit=100)` | +| Who calls method M | `resolve` → `neighbors(ids, "in", ["CALLS"])` | +| What does M call | `resolve` → `neighbors(ids, "out", ["CALLS"])` | +| Handler for route R | `neighbors(route_id, "in", ["EXPOSES"])` | +| All inbound to route R | `neighbors(route_id, "in", ["HTTP_CALLS","ASYNC_CALLS","EXPOSES"])` | +| Implementors of interface T | `neighbors(type_id, "in", ["IMPLEMENTS"])` | +| Where is T injected | `neighbors(type_id, "in", ["INJECTS"])` | +| Impact of changing X | `resolve` → `describe` → bounded `neighbors(in, ["CALLS","INJECTS","IMPLEMENTS","EXTENDS"])` depth ≤2 | ### Canonical workflow: "explain feature X" diff --git a/tests/test_agent_skills_static.py b/tests/test_agent_skills_static.py index 730ee9d..898e742 100644 --- a/tests/test_agent_skills_static.py +++ b/tests/test_agent_skills_static.py @@ -262,8 +262,8 @@ def test_readme_exists(self): class TestAgentGuideConsistency: """AGENT-GUIDE.md copy-paste block must be self-contained.""" - def test_guide_has_inline_slash_aliases(self): - """The copy-paste block must include inline slash-alias bullets + def test_guide_has_navigation_patterns_table(self): + """The copy-paste block must include a navigation patterns section (it's standalone — no external file references work in a consumer project).""" guide = Path(__file__).resolve().parent.parent / "docs" / "AGENT-GUIDE.md" text = guide.read_text(encoding="utf-8") @@ -272,12 +272,12 @@ def test_guide_has_inline_slash_aliases(self): end = text.find("") assert begin != -1 and end != -1, "AGENT-GUIDE.md missing BEGIN/END markers" block = text[begin:end] - assert "### Slash-style aliases" in block, ( - "AGENT-GUIDE.md copy-paste block missing '### Slash-style aliases'" + assert "### Common navigation patterns" in block, ( + "AGENT-GUIDE.md copy-paste block missing '### Common navigation patterns'" ) - # Verify key aliases are present inline - for alias in ["/nl", "/callers", "/callees", "/routes", "/controllers"]: - assert alias in block, f"AGENT-GUIDE.md copy-paste block missing {alias} alias" + # Verify key patterns are present + for pattern in ["CALLS", "EXPOSES", "IMPLEMENTS", "INJECTS"]: + assert pattern in block, f"AGENT-GUIDE.md copy-paste block missing {pattern} pattern" def test_guide_copy_block_does_not_reference_skills_dir(self): """The copy-paste block must not reference skills/ — it won't exist @@ -293,3 +293,26 @@ def test_guide_copy_block_does_not_reference_skills_dir(self): "this path won't resolve in a consumer project. " "Keep skills/ references outside the copy-paste block." ) + + def test_guide_copy_block_has_no_slash_command_aliases(self): + """The copy-paste block must not contain slash-command alias bullets + like `/nl ` → ... — these imply commands that don't exist + and will mislead the agent. Incidental mentions (e.g. cross-references + in prose) are fine.""" + guide = Path(__file__).resolve().parent.parent / "docs" / "AGENT-GUIDE.md" + text = guide.read_text(encoding="utf-8") + begin = text.find("") + end = text.find("") + block = text[begin:end] + # Match alias definition lines: - `/skillname ...` → tool(...) + skill_names_pattern = "|".join(re.escape(n) for n in ALL_SKILL_NAMES) + alias_pattern = re.compile( + rf"^- `/(?:{skill_names_pattern})\s", + re.MULTILINE, + ) + matches = alias_pattern.findall(block) + assert not matches, ( + f"AGENT-GUIDE.md copy-paste block contains slash-command alias bullets: " + f"{alias_pattern.findall(block)}. " + "These are not real commands and will mislead the agent." + )