diff --git a/AGENTS.md b/AGENTS.md index 2708351..12a8831 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -14,7 +14,7 @@ gitignored). | Directory | Audience | Purpose | |-----------|----------|---------| | **`.agents/skills/`** (`.claude/skills/`, `.cursor/skills/`) | Agents **developing** this repo | propose, plan-prompts, pr-open, pr-review | -| **`skills/`** (project root) | Agents **using** this tool on their own codebase | /callers, /routes, /explain-feature, /impact-of, etc. | +| **`skills/tier-1/`** + **`skills/tier-2/`** (project root) | Agents **using** this tool on their own codebase | /callers, /routes, /explain-feature, /impact-of, etc. | `.agents/` skills are loaded by the agent working *on* java-codebase-rag source code. `skills/` are shipped to consumers — they instruct an agent to call the @@ -55,7 +55,7 @@ when needed. - `docs/CODEBASE_REQUIREMENTS.md` — Java-repo assumptions and per-file map of what to edit when a target tree doesn't match defaults. - `tests/README.md` — testing philosophy. -- **`skills/`** — user-facing skills shipped to java-codebase-rag consumers (navigation, workflow). Developer workflow skills live in **`.agents/skills/`**, not here. +- **`skills/tier-1/`** + **`skills/tier-2/`** — user-facing skills shipped to java-codebase-rag consumers. Tier 1 are single-intent listings (`/callers`, `/routes`, `/clients`, …); Tier 2 are multi-step workflows (`/explain-feature`, `/impact-of`, `/trace-request-flow`, `/mini-map`). Users opt in per tier. Developer workflow skills live in **`.agents/skills/`**, not here. - **`propose/`** — design proposes. **In-flight** proposes live in **`propose/active/`**. **`propose/completed/`** — landed work and rationale. **List or search this tree** for current filenames; do not rely on enumerated diff --git a/README.md b/README.md index 13beb35..4ab1d38 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,24 @@ For the design rationale, the GPS metaphor, and the full ontology, see [`docs/pa --- +## Why this exists + +Generic code-search tools (grep, ctags, vector-only RAG) hit a ceiling on real Java microservice estates: they find files but lose the structure that makes a Spring/JAX-RS system navigable. This project is built around five choices that target that gap. + +- **Hybrid RAG + GraphRAG, not either-or.** Semantic recall (LanceDB chunk vectors) and structural navigation (Kuzu property graph) are composed in one surface. `search` finds candidate nodes by meaning; `neighbors` walks the exact edge you care about (`CALLS`, `IMPLEMENTS`, `INJECTS`, `DECLARES_ROUTE`, …). The agent picks the right primitive per step instead of being forced into pure-vector or pure-symbol search. + +- **A Java-tuned role model.** Symbols are labelled with stereotypes inferred from Spring and JAX-RS conventions — `CONTROLLER`, `SERVICE`, `REPOSITORY`, `CLIENT`, `PRODUCER`, `MAPPER`, `DTO`. Agents can ask "list controllers" or "who injects this repository" directly, instead of grep-ing for `@RestController` and hoping for the best. Roles drive both filtering (`find` with a `NodeFilter`) and ranking. + +- **Ranking specialized for Java codebases.** The composite ranker is aware of role, microservice, and FQN structure — not a generic BM25. A search for `"chat ingress"` surfaces controllers before utility classes; a search scoped to one microservice doesn't drown in matches from the other 19. Defaults are tuned on the bank-chat fixture and exposed in `docs/CONFIGURATION.md` for per-repo overrides. + +- **Cross-service resolution + system-level navigation.** `HTTP_CALLS` and `ASYNC_CALLS` edges connect Clients and Producers in one microservice to Routes and Handlers in another, resolved at index time from URL/topic strings + Spring `@FeignClient` / `RestTemplate` conventions. `/who-hits-route`, `/trace-request-flow`, and `/impact-of` use these to answer questions a single-service tool fundamentally can't — "who calls this REST endpoint from outside this service", "trace this Kafka message end-to-end", "if I change this DTO, which services break". + +- **Brownfield annotations as a first-class override.** Real Java estates have hand-rolled HTTP clients, dynamic topic names, reflection-heavy routing. `@CodebaseHttpRoute`, `@CodebaseAsyncRoute`, `@CodebaseHttpClient`, and `@CodebaseProducer` let you pin the truth in source. They have **exclusive priority** — when a symbol is annotated, framework-convention inference is skipped entirely. You get a correct graph on legacy code without rewriting it. + +The rest of this README is the install, walkthrough, and tool cheat sheet for putting that to work. + +--- + ## Install ```bash diff --git a/skills/README.md b/skills/README.md index 4a4bc63..ebce504 100644 --- a/skills/README.md +++ b/skills/README.md @@ -1,6 +1,20 @@ # 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. +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. + +## Pick the tier you need + +Skills are organized by tier — load only what you use. + +``` +skills/ + tier-1/ ← Navigation. 11 single-purpose skills. + tier-2/ ← Workflow. 4 multi-step skills that compose Tier 1 with bounds. +``` + +- **Just want to list controllers/routes/clients?** Tier 1 is enough — `skills/tier-1/controllers`, `skills/tier-1/routes`, etc. +- **Need to trace a request, explain a feature, or analyze blast radius?** Tier 2 — `skills/tier-2/trace-request-flow`, etc. +- **Don't want skills at all?** Copy the block in `docs/AGENT-GUIDE.md` between `` and `` into your project's `AGENTS.md` / `CLAUDE.md`. Skills and the guide are **alternatives**, not complements — pick one. ## Three-layer architecture @@ -10,9 +24,8 @@ High-level intents over the 5-tool MCP (`search` / `find` / `describe` / `neighb │ /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. │ +│ Implementation: SKILL.md files in skills/tier-1/ and │ +│ skills/tier-2/. │ ├──────────────────────────────────────────────────────────────┤ │ Layer 2 — Composable primitives (the MCP API) │ │ search, find, describe, neighbors, resolve │ @@ -22,58 +35,87 @@ High-level intents over the 5-tool MCP (`search` / `find` / `describe` / `neighb └──────────────────────────────────────────────────────────────┘ ``` -## 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 | -| [`/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 | -| [`/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 | +## Tier 1 — Navigation (deterministic MCP chains) + +11 single-purpose skills. Each one is one MCP call (sometimes preceded by a `resolve`). + +| Skill | Purpose | One-shot tool chain | +| ----- | ------- | ------------------- | +| [`/nl`](tier-1/nl/SKILL.md) | Natural-language search into the graph | `search` → `describe` | +| [`/controllers`](tier-1/controllers/SKILL.md) | List controller classes | `find(kind="symbol", role="CONTROLLER")` | +| [`/routes`](tier-1/routes/SKILL.md) | List HTTP and messaging routes | `find(kind="route")` | +| [`/clients`](tier-1/clients/SKILL.md) | List outbound HTTP clients | `find(kind="client")` | +| [`/producers`](tier-1/producers/SKILL.md) | List outbound async producers | `find(kind="producer")` | +| [`/callers`](tier-1/callers/SKILL.md) | Who calls this method (in-process CALLS) | `resolve` → `neighbors(in, CALLS)` | +| [`/callees`](tier-1/callees/SKILL.md) | What this method calls (in-process CALLS) | `resolve` → `neighbors(out, CALLS)` | +| [`/handlers`](tier-1/handlers/SKILL.md) | Method that handles a route | `resolve` → `neighbors(in, EXPOSES)` | +| [`/who-hits-route`](tier-1/who-hits-route/SKILL.md) | All inbound paths to a route | `resolve` → `neighbors(in, [HTTP_CALLS, ASYNC_CALLS, EXPOSES])` | +| [`/implements`](tier-1/implements/SKILL.md) | Concrete classes implementing an interface | `resolve` → `neighbors(in, IMPLEMENTS)` | +| [`/injects`](tier-1/injects/SKILL.md) | Where a type is injected | `resolve` → `neighbors(in, INJECTS)` | + +## Tier 2 — Workflow (bounded multi-step) + +4 multi-step skills. Each one composes Tier 1 calls with explicit depth, recursion, and stop conditions. + +| Skill | Purpose | Shape | +| ----- | ------- | ----- | +| [`/explain-feature`](tier-2/explain-feature/SKILL.md) | Understand how a feature works end-to-end | `search` → `describe` → bounded `neighbors` walks | +| [`/impact-of`](tier-2/impact-of/SKILL.md) | What breaks if a symbol changes | `resolve` → `describe` → recursive inbound `neighbors` ≤ depth 2 | +| [`/trace-request-flow`](tier-2/trace-request-flow/SKILL.md) | Follow a request from entry to persistence | `resolve(route)` → handler → forward CALLS walk ≤ depth 4 + boundary hops | +| [`/mini-map`](tier-2/mini-map/SKILL.md) | Noise-filtered call map for a hot method | `resolve` → `edge_filter`'d CALLS + skill heuristics ≤ depth 4 | ## Layout ``` skills/ - / - SKILL.md ← frontmatter (name + description) + markdown body - README.md ← this file + README.md ← this file + tier-1/ + nl/SKILL.md + controllers/SKILL.md + routes/SKILL.md + clients/SKILL.md + producers/SKILL.md + callers/SKILL.md + callees/SKILL.md + handlers/SKILL.md + who-hits-route/SKILL.md + implements/SKILL.md + injects/SKILL.md + tier-2/ + explain-feature/SKILL.md + impact-of/SKILL.md + trace-request-flow/SKILL.md + mini-map/SKILL.md ``` -## Skill structure +## SKILL.md structure -Every SKILL.md follows this structure: +Every SKILL.md is self-sufficient — load one skill, get a single working scaffolded prompt: -1. **Frontmatter** — `name` and `description` (used by skill discovery) -2. **MCP required** — declares the MCP server dependency and enforces tool usage -3. **Argument contract** — what the skill accepts -4. **Steps** — imperative MCP call chain (each step names the tool to call) -5. **Worked example** — concrete walk-through -6. **Do not** — guardrails against bypassing MCP tools -7. **Out of scope** — boundaries for when to use a different skill +1. **Frontmatter** (`name` + `description`) — used by Claude Code / Cursor / Qwen Code for auto-discovery. +2. **When to use** — concrete triggers and when to prefer a different skill. +3. **Tools used** — exactly which of `search` / `find` / `describe` / `neighbors` / `resolve` this skill calls. +4. **Reasoning preamble** — the mandatory `Q-class: ` line before each MCP call, with the taxonomy defined inline. +5. **Argument contract** — what the skill takes. +6. **Steps** — exact MCP calls with parameters. +7. **Recovery / stop conditions / recursion limit** (Tier 2: required; Tier 1: short). +8. **Worked example** — end-to-end on the `tests/bank-chat-system` fixture. +9. **Do not / Out of scope** — guardrails and pointers to neighboring skills. +10. **Going deeper** — pointer to `docs/AGENT-GUIDE.md` for the full reference. -## Relationship to developer skills +## Versioning -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. +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. The static validator (`tests/test_agent_skills_static.py`) checks every SKILL.md against the live MCP allowlists. -## Versioning +## Relationship to `docs/AGENT-GUIDE.md` + +Skills and `docs/AGENT-GUIDE.md` are **alternatives**. Pick one: + +- **Skills** — load on demand by name. Lower context cost per query. Best for hosts that natively support skills (Claude Code, Cursor, Qwen Code). +- **AGENT-GUIDE block** — paste once into your project's `AGENTS.md` / `CLAUDE.md`. Always-on. Best for hosts without skill loading, or when you want one persistent guide for everything. + +Do not mix the two — duplicate context confuses tool selection. The static validator (`TestAgentGuideConsistency`) verifies the AGENT-GUIDE copy-paste block does not reference `skills/`. + +## Relationship to developer skills -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. +Developer workflow skills (propose-doc-author, cursor-task-prompt, cursor-pr-review, etc.) live in `.agents/skills/` — they are for contributors working **on** java-codebase-rag. Skills under `skills/` are for **consumers** using java-codebase-rag to explore their own codebases. diff --git a/skills/callees/SKILL.md b/skills/callees/SKILL.md deleted file mode 100644 index 585ef91..0000000 --- a/skills/callees/SKILL.md +++ /dev/null @@ -1,53 +0,0 @@ ---- -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 - -## MCP required - -This skill requires the **java-codebase-rag** MCP server (tools: `search`, `find`, `describe`, `neighbors`, `resolve`). - -**You MUST call these MCP tools to answer.** Do not answer from training data, file browsing, or general knowledge. Each MCP call must be preceded by the reasoning preamble: - -``` -Q-class: -Pick: Why: -``` - -For the full operating manual (NodeFilter keys, edge taxonomy, argument shapes, recovery playbook), read `docs/AGENT-GUIDE.md`. - -## 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 as the id. Otherwise, call `resolve(identifier=, hint_kind="symbol")`. On status `one`, use `node.id`; on `many`, list `candidates` and stop; on `none`, call `search(query=, limit=5)` and stop if still empty. -2. **In-process callees.** Call `neighbors` with `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).** Call `neighbors` with `ids=`, `direction="out"`, `edge_types=["DECLARES_CLIENT"]`. For each client id, call `neighbors` with `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 - -## Do not - -- Do not answer from training data or general Java knowledge. -- Do not read source files directly when MCP tools can provide the answer. -- Do not skip MCP calls and guess at results. -- Do not fabricate symbol ids — always obtain them from `resolve`, `find`, or `search`. - -## 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 deleted file mode 100644 index 4286121..0000000 --- a/skills/callers/SKILL.md +++ /dev/null @@ -1,50 +0,0 @@ ---- -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 - -## MCP required - -This skill requires the **java-codebase-rag** MCP server (tools: `search`, `find`, `describe`, `neighbors`, `resolve`). - -**You MUST call these MCP tools to answer.** Do not answer from training data, file browsing, or general knowledge. Each MCP call must be preceded by the reasoning preamble: - -``` -Q-class: -Pick: Why: -``` - -For the full operating manual (NodeFilter keys, edge taxonomy, argument shapes, recovery playbook), read `docs/AGENT-GUIDE.md`. - -## 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 as the id. Otherwise, call `resolve(identifier=, hint_kind="symbol")`. On status `one`, use `node.id`; on `many`, list `candidates` and stop; on `none`, call `search(query=, limit=5)` and stop if still empty. -2. **In-process callers.** Call `neighbors` with `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 - -## Do not - -- Do not answer from training data or general Java knowledge. -- Do not read source files directly when MCP tools can provide the answer. -- Do not skip MCP calls and guess at results. -- Do not fabricate symbol ids — always obtain them from `resolve`, `find`, or `search`. - -## 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 deleted file mode 100644 index 11de948..0000000 --- a/skills/clients/SKILL.md +++ /dev/null @@ -1,47 +0,0 @@ ---- -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 - -## MCP required - -This skill requires the **java-codebase-rag** MCP server (tools: `search`, `find`, `describe`, `neighbors`, `resolve`). - -**You MUST call these MCP tools to answer.** Do not answer from training data, file browsing, or general knowledge. Each MCP call must be preceded by the reasoning preamble: - -``` -Q-class: -Pick: Why: -``` - -For the full operating manual (NodeFilter keys, edge taxonomy, argument shapes, recovery playbook), read `docs/AGENT-GUIDE.md`. - -## Argument contract - -Optional positional argument: microservice name. Omit to list all clients. - -## Steps - -1. **Find clients.** Call `find(kind="client", filter={microservice: }, limit=100)` when a microservice is given, or `find(kind="client", filter={}, limit=100)` when listing all. -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 - -## Do not - -- Do not answer from training data or general Java knowledge. -- Do not read source files directly when MCP tools can provide the answer. -- Do not skip MCP calls and guess at results. -- Do not fabricate symbol ids — always obtain them from `resolve`, `find`, or `search`. diff --git a/skills/controllers/SKILL.md b/skills/controllers/SKILL.md deleted file mode 100644 index 6345fea..0000000 --- a/skills/controllers/SKILL.md +++ /dev/null @@ -1,46 +0,0 @@ ---- -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 - -## MCP required - -This skill requires the **java-codebase-rag** MCP server (tools: `search`, `find`, `describe`, `neighbors`, `resolve`). - -**You MUST call these MCP tools to answer.** Do not answer from training data, file browsing, or general knowledge. Each MCP call must be preceded by the reasoning preamble: - -``` -Q-class: -Pick: Why: -``` - -For the full operating manual (NodeFilter keys, edge taxonomy, argument shapes, recovery playbook), read `docs/AGENT-GUIDE.md`. - -## Argument contract - -Optional positional argument: microservice name. Omit to list all controllers. - -## Steps - -1. **Find controllers.** Call `find(kind="symbol", filter={role: "CONTROLLER", microservice: })` when a microservice is given, or `find(kind="symbol", filter={role: "CONTROLLER"})` when listing all. -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 - -## Do not - -- Do not answer from training data or general Java knowledge. -- Do not read source files directly when MCP tools can provide the answer. -- Do not skip MCP calls and guess at results. -- Do not fabricate symbol ids — always obtain them from `resolve`, `find`, or `search`. diff --git a/skills/explain-feature/SKILL.md b/skills/explain-feature/SKILL.md deleted file mode 100644 index 1bf6ec2..0000000 --- a/skills/explain-feature/SKILL.md +++ /dev/null @@ -1,68 +0,0 @@ ---- -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 - -## MCP required - -This skill requires the **java-codebase-rag** MCP server (tools: `search`, `find`, `describe`, `neighbors`, `resolve`). - -**You MUST call these MCP tools to answer.** Do not answer from training data, file browsing, or general knowledge. Each MCP call must be preceded by the reasoning preamble: - -``` -Q-class: -Pick: Why: -``` - -For the full operating manual (NodeFilter keys, edge taxonomy, argument shapes, recovery playbook), read `docs/AGENT-GUIDE.md`. - -## Argument contract - -Single positional argument: free-form text describing the feature or concept to explain. - -## Steps - -1. **Locate entry points.** Call `search(query=, limit=8)`. Pick top 1–3 hits with strong `symbol_id` fit (role, `symbol_kind` alignment). -2. **Inspect each hit.** Call `describe(id=)` for each hit. Read `edge_summary` to understand the node's connectivity. -3. **Walk with bounded neighbors.** For each inspected node, call `neighbors` with **small** `edge_types` sets per step: - - Methods: call `neighbors` with `direction="out"`, `edge_types=["CALLS"]` for in-process flow. - - Boundaries: call `neighbors` for `EXPOSES` (route handlers), `DECLARES_CLIENT` → `HTTP_CALLS` (outbound HTTP), `DECLARES_PRODUCER` → `ASYNC_CALLS` (async). - - Type wiring: call `neighbors` for `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..." - -## Do not - -- Do not answer from training data or general Java knowledge. -- Do not read source files directly when MCP tools can provide the answer. -- Do not skip MCP calls and guess at results. -- Do not fabricate symbol ids — always obtain them from `resolve`, `find`, or `search`. - -## 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 deleted file mode 100644 index 06139f8..0000000 --- a/skills/handlers/SKILL.md +++ /dev/null @@ -1,47 +0,0 @@ ---- -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 - -## MCP required - -This skill requires the **java-codebase-rag** MCP server (tools: `search`, `find`, `describe`, `neighbors`, `resolve`). - -**You MUST call these MCP tools to answer.** Do not answer from training data, file browsing, or general knowledge. Each MCP call must be preceded by the reasoning preamble: - -``` -Q-class: -Pick: Why: -``` - -For the full operating manual (NodeFilter keys, edge taxonomy, argument shapes, recovery playbook), read `docs/AGENT-GUIDE.md`. - -## 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 directly. Otherwise, call `resolve(identifier=, hint_kind="route")`. On status `one`, use `node.id`; on `many`, list `candidates` and stop; on `none`, call `find(kind="route", filter={path_prefix: })` and stop if still empty. -2. **Handler method.** Call `neighbors` with `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 - -## Do not - -- Do not answer from training data or general Java knowledge. -- Do not read source files directly when MCP tools can provide the answer. -- Do not skip MCP calls and guess at results. -- Do not fabricate route ids — always obtain them from `resolve` or `find`. diff --git a/skills/impact-of/SKILL.md b/skills/impact-of/SKILL.md deleted file mode 100644 index eba75ff..0000000 --- a/skills/impact-of/SKILL.md +++ /dev/null @@ -1,72 +0,0 @@ ---- -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 - -## MCP required - -This skill requires the **java-codebase-rag** MCP server (tools: `search`, `find`, `describe`, `neighbors`, `resolve`). - -**You MUST call these MCP tools to answer.** Do not answer from training data, file browsing, or general knowledge. Each MCP call must be preceded by the reasoning preamble: - -``` -Q-class: -Pick: Why: -``` - -For the full operating manual (NodeFilter keys, edge taxonomy, argument shapes, recovery playbook), read `docs/AGENT-GUIDE.md`. - -## 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 as the id. Otherwise, call `resolve(identifier=, hint_kind="symbol")`. On status `one`, use `node.id`; on `many`, list `candidates` and stop; on `none`, call `search(query=, limit=5)` and stop if still empty. -2. **Inspect.** Call `describe(id=)`. Read `edge_summary` and `role` to understand the node's position. -3. **Recursive inbound walk** (depth ≤ 2). Call `neighbors` with `ids=`, `direction="in"`, `edge_types=["CALLS", "INJECTS", "IMPLEMENTS", "EXTENDS"]`. For each inbound neighbor that is a method symbol, call `neighbors` again with `ids=`, `direction="in"`, `edge_types=["CALLS", "INJECTS", "IMPLEMENTS", "EXTENDS"]`. -4. **Route/client impact** (when applicable): - - If the symbol is a method with routes: call `neighbors` with `direction="out"`, `edge_types=["EXPOSES"]`, then call `neighbors` with `direction="in"`, `edge_types=["HTTP_CALLS", "ASYNC_CALLS"]` on route ids for callers outside the codebase. - - If the symbol declares clients: call `neighbors` with `direction="out"`, `edge_types=["DECLARES_CLIENT"]`, then call `neighbors` with `direction="out"`, `edge_types=["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) - -## Do not - -- Do not answer from training data or general Java knowledge. -- Do not read source files directly when MCP tools can provide the answer. -- Do not skip MCP calls and guess at results. -- Do not fabricate symbol ids — always obtain them from `resolve`, `find`, or `search`. - -## 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 deleted file mode 100644 index c6cc2fb..0000000 --- a/skills/implements/SKILL.md +++ /dev/null @@ -1,43 +0,0 @@ ---- -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 - -## MCP required - -This skill requires the **java-codebase-rag** MCP server (tools: `search`, `find`, `describe`, `neighbors`, `resolve`). - -**You MUST call these MCP tools to answer.** Do not answer from training data, file browsing, or general knowledge. Each MCP call must be preceded by the reasoning preamble: - -``` -Q-class: -Pick: Why: -``` - -For the full operating manual (NodeFilter keys, edge taxonomy, argument shapes, recovery playbook), read `docs/AGENT-GUIDE.md`. - -## 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 as the id. Otherwise, call `resolve(identifier=, hint_kind="symbol")`. On status `one`, use `node.id`; on `many`, list `candidates` and stop; on `none`, call `search(query=, limit=5)` and stop if still empty. -2. **Implementors.** Call `neighbors` with `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 - -## Do not - -- Do not answer from training data or general Java knowledge. -- Do not read source files directly when MCP tools can provide the answer. -- Do not skip MCP calls and guess at results. -- Do not fabricate symbol ids — always obtain them from `resolve`, `find`, or `search`. diff --git a/skills/injects/SKILL.md b/skills/injects/SKILL.md deleted file mode 100644 index a0ad948..0000000 --- a/skills/injects/SKILL.md +++ /dev/null @@ -1,43 +0,0 @@ ---- -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 - -## MCP required - -This skill requires the **java-codebase-rag** MCP server (tools: `search`, `find`, `describe`, `neighbors`, `resolve`). - -**You MUST call these MCP tools to answer.** Do not answer from training data, file browsing, or general knowledge. Each MCP call must be preceded by the reasoning preamble: - -``` -Q-class: -Pick: Why: -``` - -For the full operating manual (NodeFilter keys, edge taxonomy, argument shapes, recovery playbook), read `docs/AGENT-GUIDE.md`. - -## 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 as the id. Otherwise, call `resolve(identifier=, hint_kind="symbol")`. On status `one`, use `node.id`; on `many`, list `candidates` and stop; on `none`, call `search(query=, limit=5)` and stop if still empty. -2. **Injection sites.** Call `neighbors` with `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 - -## Do not - -- Do not answer from training data or general Java knowledge. -- Do not read source files directly when MCP tools can provide the answer. -- Do not skip MCP calls and guess at results. -- Do not fabricate symbol ids — always obtain them from `resolve`, `find`, or `search`. diff --git a/skills/mini-map/SKILL.md b/skills/mini-map/SKILL.md deleted file mode 100644 index 9c1b873..0000000 --- a/skills/mini-map/SKILL.md +++ /dev/null @@ -1,121 +0,0 @@ ---- -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 - -## MCP required - -This skill requires the **java-codebase-rag** MCP server (tools: `search`, `find`, `describe`, `neighbors`, `resolve`). - -**You MUST call these MCP tools to answer.** Do not answer from training data, file browsing, or general knowledge. Each MCP call must be preceded by the reasoning preamble: - -``` -Q-class: -Pick: Why: -``` - -For the full operating manual (NodeFilter keys, edge taxonomy, argument shapes, recovery playbook), read `docs/AGENT-GUIDE.md`. - -## 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 as the id. Otherwise, call `resolve(identifier=, hint_kind="symbol")`. On status `one`, use `node.id`; on `many`, list `candidates` and stop; on `none`, call `search(query=, limit=5)` and stop if still empty. - -### Step 2 — Fetch ordered CALLS - -Call `neighbors` with `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. Call `neighbors` again with the appropriate `edge_filter`: - -- **Skeleton pass** (delegation hops): call `neighbors` with `direction="out"`, `edge_types=["CALLS"]`, `edge_filter={callee_declaring_role: "SERVICE"}`. -- **Trim JDK/low-signal**: call `neighbors` with `direction="out"`, `edge_types=["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**: call `neighbors` with `direction="out"`, `edge_types=["CALLS"]`, `dedup_calls=True`. -- **Full transcript with unresolved sites**: call `neighbors` with `direction="out"`, `edge_types=["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] - -## Do not - -- Do not answer from training data or general Java knowledge. -- Do not read source files directly when MCP tools can provide the answer. -- Do not skip MCP calls and guess at results. -- Do not fabricate symbol ids — always obtain them from `resolve`, `find`, or `search`. - -## 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 deleted file mode 100644 index 3fdb9c0..0000000 --- a/skills/nl/SKILL.md +++ /dev/null @@ -1,50 +0,0 @@ ---- -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 - -## MCP required - -This skill requires the **java-codebase-rag** MCP server (tools: `search`, `find`, `describe`, `neighbors`, `resolve`). - -**You MUST call these MCP tools to answer.** Do not answer from training data, file browsing, or general knowledge. Each MCP call must be preceded by the reasoning preamble: - -``` -Q-class: -Pick: Why: -``` - -For the full operating manual (NodeFilter keys, edge taxonomy, argument shapes, recovery playbook), read `docs/AGENT-GUIDE.md`. - -## Argument contract - -Single positional argument: free-form text describing what to find. - -## Steps - -1. **Search.** Call `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 call `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 - -## Do not - -- Do not answer from training data or general Java knowledge. -- Do not read source files directly when MCP tools can provide the answer. -- Do not skip MCP calls and guess at results. -- Do not fabricate symbol ids — always obtain them from `resolve`, `find`, or `search`. - -## 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/producers/SKILL.md b/skills/producers/SKILL.md deleted file mode 100644 index a5fecee..0000000 --- a/skills/producers/SKILL.md +++ /dev/null @@ -1,46 +0,0 @@ ---- -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 - -## MCP required - -This skill requires the **java-codebase-rag** MCP server (tools: `search`, `find`, `describe`, `neighbors`, `resolve`). - -**You MUST call these MCP tools to answer.** Do not answer from training data, file browsing, or general knowledge. Each MCP call must be preceded by the reasoning preamble: - -``` -Q-class: -Pick: Why: -``` - -For the full operating manual (NodeFilter keys, edge taxonomy, argument shapes, recovery playbook), read `docs/AGENT-GUIDE.md`. - -## Argument contract - -Optional positional argument: microservice name. Omit to list all producers. - -## Steps - -1. **Find producers.** Call `find(kind="producer", filter={microservice: }, limit=100)` when a microservice is given, or `find(kind="producer", filter={}, limit=100)` when listing all. -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 - -## Do not - -- Do not answer from training data or general Java knowledge. -- Do not read source files directly when MCP tools can provide the answer. -- Do not skip MCP calls and guess at results. -- Do not fabricate symbol ids — always obtain them from `resolve`, `find`, or `search`. diff --git a/skills/routes/SKILL.md b/skills/routes/SKILL.md deleted file mode 100644 index 113e0d6..0000000 --- a/skills/routes/SKILL.md +++ /dev/null @@ -1,46 +0,0 @@ ---- -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 - -## MCP required - -This skill requires the **java-codebase-rag** MCP server (tools: `search`, `find`, `describe`, `neighbors`, `resolve`). - -**You MUST call these MCP tools to answer.** Do not answer from training data, file browsing, or general knowledge. Each MCP call must be preceded by the reasoning preamble: - -``` -Q-class: -Pick: Why: -``` - -For the full operating manual (NodeFilter keys, edge taxonomy, argument shapes, recovery playbook), read `docs/AGENT-GUIDE.md`. - -## Argument contract - -Optional positional argument: microservice name. Omit to list all routes. - -## Steps - -1. **Find routes.** Call `find(kind="route", filter={microservice: })` when a microservice is given, or `find(kind="route", filter={})` when listing all. -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 - -## Do not - -- Do not answer from training data or general Java knowledge. -- Do not read source files directly when MCP tools can provide the answer. -- Do not skip MCP calls and guess at results. -- Do not fabricate symbol ids — always obtain them from `resolve`, `find`, or `search`. diff --git a/skills/tier-1/callees/SKILL.md b/skills/tier-1/callees/SKILL.md new file mode 100644 index 0000000..1d79404 --- /dev/null +++ b/skills/tier-1/callees/SKILL.md @@ -0,0 +1,82 @@ +--- +name: callees +description: Show what a method symbol calls (in-process CALLS). Use when the user asks "what does X call", "callees of X", "what does X invoke", or "what methods does Y use". Argument is a sym: id, or an identifier resolved via `resolve`. For noisy method bodies prefer /mini-map; for outbound HTTP/async see optional step 3 below. +--- + +# /callees — In-process callees of a method symbol + +## When to use + +The user wants the **direct in-process callees** of a method. CALLS edges only — outbound HTTP/async are reached via `DECLARES_CLIENT` / `DECLARES_PRODUCER` then `HTTP_CALLS` / `ASYNC_CALLS` (see optional step 3). + +If the method is a hot SERVICE/COMPONENT with > ~30 raw callees, prefer `/mini-map`. + +## Tools used + +`resolve`, `neighbors`. Optional second `neighbors` chain for HTTP/async. `search` only as a recovery fallback. + +## Reasoning preamble (mandatory) + +``` +Q-class: walk Pick: neighbors Why: out-edges of CALLS on method +``` + +**Q-class taxonomy:** `semantic` (`search`), `structured` (`find`/`resolve`), `inspect` (`describe`), **`walk`** (`neighbors`). + +## Argument contract + +Single positional argument: a method **symbol** id (`sym:...` preferred) OR an identifier-shaped string → `resolve(identifier=..., hint_kind="symbol")`. + +This skill is for **method symbols**. For inbound traffic to a route use `/who-hits-route`. + +## Steps + +1. **Resolve.** If the argument starts with `sym:`, use it directly. Else `resolve(identifier=, hint_kind="symbol")` (`one`/`many`/`none` handling per `/callers`). +2. **In-process callees.** Call `neighbors(ids=, direction="out", edge_types=["CALLS"])`. + Group by callee `fqn` + `microservice`. Mark rows where `attrs.resolved=false` (known-external receivers — JDK/Spring/etc.). +3. **Optional — outbound HTTP/async** (only when the user asks about cross-service calls): + - HTTP: `neighbors(ids=, direction="out", edge_types=["DECLARES_CLIENT"])` → for each Client id, `neighbors(ids=, direction="out", edge_types=["HTTP_CALLS"])`. + - Async: `neighbors(ids=, direction="out", edge_types=["DECLARES_PRODUCER"])` → for each Producer id, `neighbors(ids=, direction="out", edge_types=["ASYNC_CALLS"])`. + +## Noise hint + +After ontology 15, true receiver-failure call sites are **not** on `CALLS` — they are `UnresolvedCallSite` nodes (reachable via `include_unresolved=True` on the same `neighbors` call). `attrs.resolved=false` on a `CALLS` row means the callee is known but external (JDK/Spring/Lombok/etc.). + +For noisy bodies, prefer one of: +- `/mini-map ` — handles accessor/JDK filtering + DELEGATES/PERSISTS/READS labels. +- Pre-filter inline: `neighbors(ids=, direction="out", edge_types=["CALLS"], edge_filter={callee_declaring_role:"SERVICE"})`. + +## Recovery + +- Empty result but `describe.edge_summary` shows `CALLS.out > 0`: re-resolve — wrong overload. +- After two failed attempts on the same intent, stop and report. + +## Worked example + +User: `/callees ChatController#joinOperator(JoinOperatorRequest)` +You: +``` +Q-class: structured Pick: resolve Why: identifier-shaped argument +→ resolve(identifier="ChatController#joinOperator", hint_kind="symbol") + → sym:com.bank.chat.core.api.ChatController#joinOperator(JoinOperatorRequest) +Q-class: walk Pick: neighbors Why: out-edges of CALLS on method +→ neighbors(ids="sym:...", direction="out", edge_types=["CALLS"]) + → 6 in-process callees (service + repository) + → (optional) neighbors(out, ["DECLARES_CLIENT"]) → 1 client → HTTP_CALLS → 1 route +``` + +## Do not + +- Do not pass `HTTP_CALLS` or `ASYNC_CALLS` to `neighbors` on a bare method `sym:` — those edges live on Client/Producer nodes (decision #13 in the locked agent-skills propose). +- Do not fabricate `sym:` ids. +- Do not read source files when MCP can answer. + +## Out of scope + +- Recursive callees beyond depth 1 (use `/trace-request-flow` or `/mini-map`). +- Noise-filtered call maps (use `/mini-map`; fall back here only if the map is too thin). +- Filtering by microservice (compose with `/controllers` + `/callees` per result). + +## Going deeper + +The full edge-filter axes (`callee_declaring_role`, `min_confidence`, `dedup_calls`, `include_unresolved`) and the `DECLARES_CLIENT → HTTP_CALLS` rationale live in `docs/AGENT-GUIDE.md`. This skill is self-sufficient for `/callees`. diff --git a/skills/tier-1/callers/SKILL.md b/skills/tier-1/callers/SKILL.md new file mode 100644 index 0000000..24f5b04 --- /dev/null +++ b/skills/tier-1/callers/SKILL.md @@ -0,0 +1,76 @@ +--- +name: callers +description: Show in-process callers of a method symbol via CALLS edges. Use when the user asks "who calls X", "callers of X", "what invokes X method", or "find usages of method Y". Argument is a sym: id or an identifier resolved via `resolve`. For inbound HTTP/async traffic to a route use /who-hits-route; for recursive impact analysis use /impact-of. +--- + +# /callers — In-process callers of a method symbol + +## When to use + +The user wants the **direct in-process callers** of a method. CALLS edges are method-Symbol → method-Symbol within the indexed graph (no cross-service HTTP/async — those are different edges). + +## Tools used + +`resolve` (when argument isn't already a `sym:` id) + `neighbors`. `search` only as a recovery fallback. + +## Reasoning preamble (mandatory) + +``` +Q-class: walk Pick: neighbors Why: in-edges of CALLS on method +``` + +If you call `resolve` first, that one uses `Q-class: structured` (id-shaped input). + +**Q-class taxonomy:** `semantic` (`search`), `structured` (`find` / `resolve` for id-shaped strings), `inspect` (`describe`), **`walk`** (`neighbors`). + +## Argument contract + +Single positional argument: a method **symbol** id (`sym:...` preferred) OR an identifier-shaped string (FQN fragment, `Class#method`, signature). + +This skill is for **method symbols**. For inbound traffic to an HTTP/async **route**, use `/who-hits-route`. + +## Steps + +1. **Resolve.** If the argument starts with `sym:`, use it directly. + Else call `resolve(identifier=, hint_kind="symbol")`. + - `status="one"` → use `node.id`. + - `status="many"` → list `candidates` and stop for user pick. + - `status="none"` → call `search(query=, limit=5)`; if still empty, stop and report. +2. **Walk inbound CALLS.** Call `neighbors(ids=, direction="in", edge_types=["CALLS"])`. +3. **Render.** Group by caller `fqn` + `microservice`. Show count when one caller has multiple call sites (the edge rows include `call_site_line`). + +## Recovery + +- `neighbors` returns empty but `describe()` shows `CALLS.in > 0`: re-run with the exact `id` from `describe`; the symbol you resolved may be the wrong overload. +- Resolved id is for a **type** Symbol, not a method: use `/implements` or `/injects` instead. +- After two failed attempts on the same intent, stop and report. + +## Worked example + +User: `/callers ChatController#joinOperator(JoinOperatorRequest)` +You: +``` +Q-class: structured Pick: resolve Why: identifier-shaped argument +→ resolve(identifier="ChatController#joinOperator", hint_kind="symbol") + → status=one id=sym:com.bank.chat.core.api.ChatController#joinOperator(JoinOperatorRequest) +Q-class: walk Pick: neighbors Why: in-edges of CALLS on method +→ neighbors(ids="sym:...", direction="in", edge_types=["CALLS"]) + → 3 callers in chat-core, 1 in chat-assign +``` + +## Do not + +- Do not answer from training data or general Java knowledge. +- Do not read source files when MCP can answer. +- Do not fabricate `sym:` ids — always obtain them from `resolve` / `find` / `search`. +- Do not pass `HTTP_CALLS` or `ASYNC_CALLS` to `neighbors` on a bare method `sym:` — those edges live on Client/Producer nodes. + +## Out of scope + +- Callers of a **route** (use `/who-hits-route`). +- Recursive callers beyond depth 1 (use `/impact-of`). +- Outbound callees (use `/callees`). + +## Going deeper + +CALLS edge details (call-site columns, what `attrs.resolved=false` means after ontology 15) and the full recovery playbook live in `docs/AGENT-GUIDE.md`. This skill is self-sufficient for `/callers`. diff --git a/skills/tier-1/clients/SKILL.md b/skills/tier-1/clients/SKILL.md new file mode 100644 index 0000000..6ed659e --- /dev/null +++ b/skills/tier-1/clients/SKILL.md @@ -0,0 +1,76 @@ +--- +name: clients +description: List outbound HTTP clients in the indexed Java codebase, optionally filtered by microservice. Use when the user asks "list clients", "show outbound HTTP calls", "what Feign clients exist in X", or "what HTTP calls does this service make". Returns Client nodes — feign methods, RestTemplate/WebClient call sites, brownfield-annotated outbound calls. Argument is an optional microservice name. +--- + +# /clients — List outbound HTTP clients + +## When to use + +The user wants a **list** of outbound HTTP call sites: `@FeignClient` methods, `RestTemplate`/`WebClient`/`HttpClient` call expressions, or brownfield `@CodebaseHttpClient`. Optionally scoped to one microservice. + +## Tools used + +`find` only. + +## Reasoning preamble (mandatory) + +``` +Q-class: structured Pick: find Why: client kind + optional microservice +``` + +**Q-class taxonomy reminder:** `semantic` (`search`), **`structured`** (`find`), `inspect` (`describe`), `walk` (`neighbors`). + +## Argument contract + +Optional positional argument: microservice name. Omit to list all clients. + +**`microservice` value note:** Not validated; invalid name returns empty. + +## Steps + +1. **Find.** Call: + - With microservice: `find(kind="client", filter={microservice:}, limit=100)` + - Without: `find(kind="client", filter={}, limit=100)` +2. **Render.** For each row show `fqn`, `microservice`, `client_kind` (e.g. `feign_method`, `rest_template`, `web_client`, `http_call`, `codebase_http`), `target_service` (when known), and `id`. +3. **Narrow if needed.** When results are broad, add `client_kind` or `target_service` to the filter. + +## Optional filters + +- `client_kind:"feign_method"` — only declared Feign methods +- `client_kind:"http_call"` — call-site clients (RestTemplate/WebClient/raw HttpClient) +- `target_service:"chat-service"` — only clients pointing at one target + +## Recovery + +- Empty with a microservice argument: re-run without the filter to confirm the microservice name. +- Many results: narrow with `client_kind` or `target_service`. +- After two failed attempts on the same intent, stop and report. + +## Worked example + +User: `/clients chat-core` +You: +``` +Q-class: structured Pick: find Why: client listing for one microservice +→ find(kind="client", filter={microservice:"chat-core"}, limit=100) + → client:com.bank.chat.core.client.ChatServiceClient#assign feign_method target=chat-service + → client:com.bank.chat.core.client.NotifyClient#send feign_method target=notify-service + → ... 7 more +``` + +## Do not + +- Do not infer clients from training data — they are project-specific. +- Do not read source files when `find` can answer. +- Do not call `search` for this — it's a structured listing. + +## Out of scope + +- The downstream **route** a client targets (use `neighbors(client_id, "out", ["HTTP_CALLS"])`, or `/trace-request-flow`). +- Async outbound (use `/producers`). +- Who declares a client (use `neighbors(client_id, "in", ["DECLARES_CLIENT"])`). + +## Going deeper + +The full `Client` schema (every `client_kind` value, when `target_service` is set, brownfield-annotated clients) and the `DECLARES_CLIENT → HTTP_CALLS` pattern are in `docs/AGENT-GUIDE.md`. This skill is self-sufficient for `/clients`. diff --git a/skills/tier-1/controllers/SKILL.md b/skills/tier-1/controllers/SKILL.md new file mode 100644 index 0000000..6d47aa3 --- /dev/null +++ b/skills/tier-1/controllers/SKILL.md @@ -0,0 +1,78 @@ +--- +name: controllers +description: List controller classes in the indexed Java codebase, optionally filtered by microservice. Use when the user asks "list controllers", "show me controllers in X", "what REST entry points exist", or "give me all @RestController classes". Returns Symbol nodes with role=CONTROLLER. Argument is an optional microservice name. +--- + +# /controllers — List controller classes + +## When to use + +The user wants a **list** of controller-stereotype classes (Spring `@RestController`, `@Controller`, JAX-RS resources, or brownfield-annotated equivalents). Optionally scoped to one microservice. + +## Tools used + +`find` only. + +## Reasoning preamble (mandatory) + +Before the MCP call, output one line: + +``` +Q-class: structured Pick: find Why: known role + optional microservice +``` + +**Q-class taxonomy reminder:** `semantic` (NL → `search`), **`structured`** (role/kind/microservice → `find`), `inspect` (→ `describe`), `walk` (→ `neighbors`). + +## Argument contract + +Optional positional argument: microservice name. Omit to list all controllers across all microservices. + +**`microservice` value note:** Not validated by the MCP. An invalid name silently returns an empty list. If results are empty, verify the name with `find(kind="microservice", filter={})` first. + +## Steps + +1. **Find.** Call: + - With microservice: `find(kind="symbol", filter={role:"CONTROLLER", microservice:})` + - Without: `find(kind="symbol", filter={role:"CONTROLLER"})` +2. **Render.** For each row show `fqn`, `microservice`, and `id`. Group by microservice when no filter was given. + +## Recovery + +- Empty result with a microservice argument: re-run without the microservice filter; if non-empty, the microservice name is wrong. Look up the canonical names via `find(kind="microservice", filter={})`. +- Empty result without a microservice argument: likely no `CONTROLLER` role assigned. Try `find(kind="symbol", filter={symbol_kind:"class", fqn_prefix:""})` and inspect with `describe` to see `role`. +- After two failed attempts on the same intent, stop and report. + +## Worked example + +User: `/controllers chat-core` +You: +``` +Q-class: structured Pick: find Why: role+microservice listing +→ find(kind="symbol", filter={role:"CONTROLLER", microservice:"chat-core"}) + → sym:com.bank.chat.core.api.ChatController microservice=chat-core + → sym:com.bank.chat.core.api.OperatorController microservice=chat-core +``` + +User: `/controllers` +You: +``` +Q-class: structured Pick: find Why: role listing, no scope +→ find(kind="symbol", filter={role:"CONTROLLER"}) + → grouped by microservice: chat-core (2), chat-assign (1), ... +``` + +## Do not + +- Do not answer from training data — controllers vary by project. +- Do not read source files when `find` can answer the question. +- Do not call `search` for this — it's a structured listing, not a fuzzy query. + +## Out of scope + +- HTTP route enumeration (use `/routes`). +- The handler method for a specific route (use `/handlers`). +- Listing controllers + their routes together (compose `/controllers` then `/routes` per service). + +## Going deeper + +Full `NodeFilter` reference (all valid keys for `find`, including `role`, `symbol_kind`, `framework`, `fqn_prefix`) and the role taxonomy live in `docs/AGENT-GUIDE.md`. This skill is self-sufficient for `/controllers`. diff --git a/skills/tier-1/handlers/SKILL.md b/skills/tier-1/handlers/SKILL.md new file mode 100644 index 0000000..4845fc8 --- /dev/null +++ b/skills/tier-1/handlers/SKILL.md @@ -0,0 +1,67 @@ +--- +name: handlers +description: Show the method that handles an HTTP or messaging route via the EXPOSES edge. Use when the user asks "what handles X route", "handler for POST /foo", "which method handles this endpoint", or "find the controller method for path Y". Argument is a route: id or a route identifier (path, METHOD /path) resolved via `resolve`. +--- + +# /handlers — Handler method for a route + +## When to use + +The user has a **route** (path, METHOD /path, async topic, or `route:` id) and wants the method `sym:` that handles it. The edge is `Symbol —EXPOSES→ Route`, so the handler is the *in-neighbor* of the route. + +## Tools used + +`resolve` (when argument isn't already `route:`) + `neighbors`. `find` only as recovery fallback. + +## Reasoning preamble (mandatory) + +``` +Q-class: walk Pick: neighbors Why: in-edges of EXPOSES on route +``` + +If `resolve` runs first, that one is `Q-class: structured`. + +## Argument contract + +Single positional argument: a **route** id (`route:...` preferred) OR an identifier-shaped string (path like `/chat/join` or `METHOD /path`). + +## Steps + +1. **Resolve.** If argument starts with `route:` or `r:`, use directly. Else `resolve(identifier=, hint_kind="route")`: + - `status="one"` → use `node.id`. + - `status="many"` → list candidates, stop. + - `status="none"` → `find(kind="route", filter={path_prefix:})`; if still empty, stop and report. +2. **Walk EXPOSES inbound.** Call `neighbors(ids=, direction="in", edge_types=["EXPOSES"])`. +3. **Render.** Show the handler method `fqn` + `microservice` + (parent class via `describe` if useful). + +## Recovery + +- Multiple EXPOSES neighbors (rare): possible duplicate mapping across frameworks. Report all; let the user disambiguate. +- After two failed attempts on the same intent, stop and report. + +## Worked example + +User: `/handlers POST /chat/join` +You: +``` +Q-class: structured Pick: resolve Why: identifier-shaped route arg +→ resolve(identifier="POST /chat/join", hint_kind="route") + → status=one id=route:POST /chat/join +Q-class: walk Pick: neighbors Why: in-edges of EXPOSES on route +→ neighbors(ids="route:POST /chat/join", direction="in", edge_types=["EXPOSES"]) + → sym:com.bank.chat.core.api.ChatController#joinOperator(JoinOperatorRequest) microservice=chat-core +``` + +## Do not + +- Do not fabricate `route:` ids — always obtain them from `resolve` or `find`. +- Do not read source files when MCP can answer. + +## Out of scope + +- All inbound paths to a route — cross-service `HTTP_CALLS` + async `ASYNC_CALLS` + handler `EXPOSES` (use `/who-hits-route`). +- Following the request through the call graph (use `/trace-request-flow`). + +## Going deeper + +`EXPOSES` semantics (always Symbol→Route, one-to-one in well-formed projects) and the full `find(kind="route")` filter set are in `docs/AGENT-GUIDE.md`. This skill is self-sufficient for `/handlers`. diff --git a/skills/tier-1/implements/SKILL.md b/skills/tier-1/implements/SKILL.md new file mode 100644 index 0000000..c5ea50d --- /dev/null +++ b/skills/tier-1/implements/SKILL.md @@ -0,0 +1,69 @@ +--- +name: implements +description: Show concrete classes that implement an interface or abstract type. Use when the user asks "what implements X", "implementations of X", "concrete types for interface X", or "subclasses of X". Argument is a type sym: id or identifier resolved via `resolve`. Uses the IMPLEMENTS edge (also follow EXTENDS for class hierarchy). +--- + +# /implements — Concrete implementors of a type + +## When to use + +The user has an **interface** (or abstract class) Symbol and wants its concrete implementors. The edge is `Concrete —IMPLEMENTS→ Interface`, so implementors are the *in-neighbors*. + +For class-hierarchy parents/children use `EXTENDS` instead — see optional step 3. + +## Tools used + +`resolve` + `neighbors`. `search` only as a recovery fallback. + +## Reasoning preamble (mandatory) + +``` +Q-class: walk Pick: neighbors Why: in-edges of IMPLEMENTS on type +``` + +## Argument contract + +Single positional argument: a **type** Symbol id (`sym:...` preferred) OR an identifier-shaped string (FQN, simple class name). + +## Steps + +1. **Resolve.** If argument starts with `sym:`, use directly. Else `resolve(identifier=, hint_kind="symbol")`: + - `one` → use `node.id`. + - `many` → list candidates, stop. + - `none` → `search(query=, limit=5)`; if still empty, stop and report. +2. **Walk IMPLEMENTS inbound.** Call `neighbors(ids=, direction="in", edge_types=["IMPLEMENTS"])`. Each row is a concrete implementor. +3. **Optional — also follow EXTENDS** (when the user asks for the *full* subtype tree): `neighbors(ids=, direction="in", edge_types=["IMPLEMENTS","EXTENDS"])`. +4. **Render.** Show each implementor's `fqn` + `microservice`. Note if any are themselves interfaces (rare — multi-level interfaces). + +## Recovery + +- Empty result but the type is clearly an interface used in the codebase: confirm the resolved id is the **type** Symbol, not a method on it. `describe()` should show `symbol_kind: "interface"` or `"class"`. +- After two failed attempts on the same intent, stop and report. + +## Worked example + +User: `/implements OperatorAssignmentService` +You: +``` +Q-class: structured Pick: resolve Why: identifier-shaped argument +→ resolve(identifier="OperatorAssignmentService", hint_kind="symbol") + → status=one id=sym:com.bank.chat.assign.service.OperatorAssignmentService (interface) +Q-class: walk Pick: neighbors Why: in-edges of IMPLEMENTS on type +→ neighbors(ids="sym:...", direction="in", edge_types=["IMPLEMENTS"]) + → sym:com.bank.chat.assign.service.RoundRobinOperatorAssignmentService (chat-assign) + → sym:com.bank.chat.assign.service.WeightedOperatorAssignmentService (chat-assign) +``` + +## Do not + +- Do not enumerate implementors from training data — they are project-specific. +- Do not fabricate `sym:` ids. + +## Out of scope + +- Where the type is injected (use `/injects`). +- Per-method overrides — `IMPLEMENTS` is **type→type**; the per-method counterpart is `OVERRIDES` on a method id (composed key `OVERRIDDEN_BY.IMPLEMENTS`/`OVERRIDDEN_BY.EXTENDS` walks both directions). + +## Going deeper + +The `IMPLEMENTS` vs `EXTENDS` vs `OVERRIDES` distinction and the composed `OVERRIDDEN_BY.*` navigation are in `docs/AGENT-GUIDE.md`. This skill is self-sufficient for `/implements`. diff --git a/skills/tier-1/injects/SKILL.md b/skills/tier-1/injects/SKILL.md new file mode 100644 index 0000000..399ab8e --- /dev/null +++ b/skills/tier-1/injects/SKILL.md @@ -0,0 +1,70 @@ +--- +name: injects +description: Show where a type is injected via dependency injection. Use when the user asks "where is X injected", "who injects X", "what depends on X via DI", or "find DI consumers of bean X". Argument is a type sym: id or identifier resolved via `resolve`. Uses the INJECTS edge (covers constructor, field, and setter injection). +--- + +# /injects — Where a type is injected via DI + +## When to use + +The user has a **type** Symbol (interface, abstract class, or concrete bean) and wants the call sites where it's injected via DI — Spring constructor / field / setter injection, Lombok `@RequiredArgsConstructor`, brownfield equivalents. + +The edge is `Consumer —INJECTS→ Type`, so consumers are the *in-neighbors*. + +## Tools used + +`resolve` + `neighbors`. `search` only as a recovery fallback. + +## Reasoning preamble (mandatory) + +``` +Q-class: walk Pick: neighbors Why: in-edges of INJECTS on type +``` + +## Argument contract + +Single positional argument: a **type** Symbol id (`sym:...` preferred) OR an identifier-shaped string (FQN, simple class name). + +## Steps + +1. **Resolve.** If argument starts with `sym:`, use directly. Else `resolve(identifier=, hint_kind="symbol")`: + - `one` → use `node.id`. + - `many` → list candidates, stop. + - `none` → `search(query=, limit=5)`; if still empty, stop and report. +2. **Walk INJECTS inbound.** Call `neighbors(ids=, direction="in", edge_types=["INJECTS"])`. +3. **Render.** For each row show consumer `fqn` + `microservice` + edge `attrs.mechanism` (e.g. `constructor`, `field`, `setter`, `lombok_required_args`) + `attrs.field_or_param` (the field or parameter name). + +## Recovery + +- Empty result but the type is clearly a bean: confirm resolved id is the type Symbol (`describe()` → `symbol_kind:"class"|"interface"`). +- For interface types, also check `/implements ` — if no implementor is annotated as a Spring bean, the type may not be DI-managed at all. +- After two failed attempts on the same intent, stop and report. + +## Worked example + +User: `/injects OperatorAssignmentService` +You: +``` +Q-class: structured Pick: resolve Why: identifier-shaped argument +→ resolve(identifier="OperatorAssignmentService", hint_kind="symbol") + → sym:com.bank.chat.assign.service.OperatorAssignmentService +Q-class: walk Pick: neighbors Why: in-edges of INJECTS on type +→ neighbors(ids="sym:...", direction="in", edge_types=["INJECTS"]) + → sym:com.bank.chat.assign.api.AssignController constructor field_or_param=service + → sym:com.bank.chat.assign.scheduler.HealthCheck field field_or_param=service +``` + +## Do not + +- Do not fabricate `sym:` ids. +- Do not infer DI usage from training data — it's project-specific. + +## Out of scope + +- Concrete implementors of the type (use `/implements`). +- Who **calls methods on** an injected dependency (use `/callers` on the method id, not the type). +- Reverse direction — what does a class inject? Use `neighbors(, "out", ["INJECTS"])` directly. + +## Going deeper + +`INJECTS` `attrs` schema (mechanism, field_or_param, qualifier when present) and DI-framework coverage (Spring, Lombok, brownfield) are in `docs/AGENT-GUIDE.md`. This skill is self-sufficient for `/injects`. diff --git a/skills/tier-1/nl/SKILL.md b/skills/tier-1/nl/SKILL.md new file mode 100644 index 0000000..5b2d276 --- /dev/null +++ b/skills/tier-1/nl/SKILL.md @@ -0,0 +1,84 @@ +--- +name: nl +description: Natural-language search into the java-codebase-rag graph index. Use when the user asks a fuzzy question like "find authentication code", "where is X handled", "show me code that does Y", or any concept search that doesn't start with a sym:/route:/client:/producer: id or a recognizable FQN. Argument is free-form text. Composes search → describe → optional neighbors. +--- + +# /nl — Natural-language search into the graph + +## When to use + +The user's request is **conceptual**, not identifier-shaped. Examples: + +- "find authentication code" +- "where do we handle operator assignment?" +- "show me anything about chat escalation" + +If the user gives a `sym:` / `route:` / `client:` / `producer:` id or a clear FQN, prefer `/callers`, `/handlers`, `/describe` (via `resolve`), etc. + +## Tools used + +`search`, `describe`, `neighbors` (rarely). + +## Reasoning preamble (mandatory) + +Before **each** MCP call, output one line: + +``` +Q-class: +Pick: Why: <≤8 words> +``` + +**Q-class taxonomy:** + +- **semantic** — fuzzy NL → `search` +- **structured** — known role/kind/microservice/FQN-prefix listing → `find` +- **inspect** — get the full record + edge summary of a known id → `describe` +- **walk** — follow edges from a known id → `neighbors` + +For `/nl` the first call is always `semantic` → `search`. + +## Argument contract + +Single positional argument: free-form text describing what to find. + +## Steps + +1. **Search.** Call `search(query=, limit=8)`. Each row has `symbol_id`, `microservice`, `symbol_kind`, `role`. Review for strong fit (role aligns with what the user wants, FQN looks plausible). +2. **Inspect top hit.** When the top result has a `symbol_id`, call `describe(id=)` to get the full record and `edge_summary` (per-label `in`/`out` counts). +3. **Stop or walk.** If `describe` answers the question, stop. Otherwise pick the most relevant edge type from `edge_summary` and call `neighbors(ids=, direction=, edge_types=[])`. Single hop only — for deeper traces hand off to `/explain-feature` or `/trace-request-flow`. + +## Recovery + +- `search` returns empty: try `search(query=, table="all")`, then fall back to reading source files. If a known FQN fragment exists in ``, try `find(kind="symbol", filter={fqn_prefix:})`. +- After two failed attempts on the same intent, stop and report the tool name, args, and response snippet. + +## Worked example + +User: `/nl operator assignment` +You: +``` +Q-class: semantic Pick: search Why: NL query, no id +→ search(query="operator assignment", limit=8) + → top hit: sym:com.bank.chat.assign.service.OperatorAssignmentService (role: SERVICE) +Q-class: inspect Pick: describe Why: get edge_summary +→ describe(id="sym:com.bank.chat.assign.service.OperatorAssignmentService") + → edge_summary: { CALLS: {in: 4, out: 12}, INJECTS: {in: 3, out: 1}, IMPLEMENTS: {in: 2} } +→ stop: caller has enough to ask "/implements OperatorAssignmentService" next +``` + +## Do not + +- Do not answer from training data or general Java knowledge. +- Do not read source files when MCP tools can provide the answer. +- Do not fabricate `symbol_id` values — they always come from `search` / `find` / `resolve`. +- Do not walk deeper than one hop in this skill — hand off to a Tier 2 skill. + +## Out of scope + +- Structured listing by role or kind (use `/controllers`, `/routes`, `/clients`, `/producers`). +- Identifier-shaped input where `resolve` would be more precise. +- Multi-hop traces (use `/explain-feature`, `/trace-request-flow`, `/impact-of`). + +## Going deeper + +The full operating manual (NodeFilter keys, edge taxonomy, recovery playbook, navigation patterns) lives in `docs/AGENT-GUIDE.md`. This skill is self-sufficient for `/nl` — no need to read the guide first. diff --git a/skills/tier-1/producers/SKILL.md b/skills/tier-1/producers/SKILL.md new file mode 100644 index 0000000..3e52b46 --- /dev/null +++ b/skills/tier-1/producers/SKILL.md @@ -0,0 +1,67 @@ +--- +name: producers +description: List outbound async producers in the indexed Java codebase, optionally filtered by microservice. Use when the user asks "list producers", "show outbound async calls", "what Kafka producers are in X", or "list message senders". Returns Producer nodes — KafkaTemplate / StreamBridge call sites and brownfield-annotated producers. Argument is an optional microservice name. +--- + +# /producers — List outbound async producers + +## When to use + +The user wants a **list** of outbound async call sites: `KafkaTemplate.send`, `StreamBridge.send`, or brownfield `@CodebaseProducer`. Optionally scoped to one microservice. Symmetric counterpart to `/clients` for async. + +## Tools used + +`find` only. + +## Reasoning preamble (mandatory) + +``` +Q-class: structured Pick: find Why: producer kind + optional microservice +``` + +**Q-class taxonomy reminder:** `semantic` (`search`), **`structured`** (`find`), `inspect` (`describe`), `walk` (`neighbors`). + +## Argument contract + +Optional positional argument: microservice name. Omit to list all producers. + +**`microservice` value note:** Not validated; invalid name returns empty. + +## Steps + +1. **Find.** Call: + - With microservice: `find(kind="producer", filter={microservice:}, limit=100)` + - Without: `find(kind="producer", filter={}, limit=100)` +2. **Render.** For each row show `fqn`, `microservice`, `producer_kind` (e.g. `kafka_send`, `stream_bridge`, `codebase_producer`), `topic_prefix` (when known), and `id`. + +## Recovery + +- Empty with a microservice argument: re-run without the filter to confirm the microservice name. +- After two failed attempts on the same intent, stop and report. + +## Worked example + +User: `/producers chat-core` +You: +``` +Q-class: structured Pick: find Why: producer listing for one microservice +→ find(kind="producer", filter={microservice:"chat-core"}, limit=100) + → producer:com.bank.chat.core.publisher.ChatEventPublisher kafka_send topic_prefix=chat-events + → producer:com.bank.chat.core.publisher.AuditPublisher kafka_send topic_prefix=audit +``` + +## Do not + +- Do not infer producers from training data — they are project-specific. +- Do not read source files when `find` can answer. +- Do not call `search` for this. + +## Out of scope + +- Outbound HTTP (use `/clients`). +- The downstream async **route** a producer targets (use `neighbors(producer_id, "out", ["ASYNC_CALLS"])`). +- Who declares a producer (use `neighbors(producer_id, "in", ["DECLARES_PRODUCER"])`). + +## Going deeper + +The full `Producer` schema (every `producer_kind` value, `topic_prefix` semantics, brownfield-annotated producers) and the `DECLARES_PRODUCER → ASYNC_CALLS` pattern are in `docs/AGENT-GUIDE.md`. This skill is self-sufficient for `/producers`. diff --git a/skills/tier-1/routes/SKILL.md b/skills/tier-1/routes/SKILL.md new file mode 100644 index 0000000..33e7187 --- /dev/null +++ b/skills/tier-1/routes/SKILL.md @@ -0,0 +1,78 @@ +--- +name: routes +description: List HTTP and messaging routes in the indexed Java codebase, optionally filtered by microservice. Use when the user asks "list routes", "show me endpoints", "list REST APIs", "what HTTP routes are in X", or "list Kafka listeners". Returns Route nodes (both HTTP and async). Argument is an optional microservice name. +--- + +# /routes — List HTTP and messaging routes + +## When to use + +The user wants a **list** of routes: HTTP endpoints (`@GetMapping` etc., or brownfield `@CodebaseHttpRoute`) and async inbound topics (`@KafkaListener`, `@RabbitListener`, or brownfield `@CodebaseAsyncRoute`). Optionally scoped to one microservice. + +## Tools used + +`find` only. + +## Reasoning preamble (mandatory) + +``` +Q-class: structured Pick: find Why: route kind + optional microservice +``` + +**Q-class taxonomy reminder:** `semantic` (NL → `search`), **`structured`** (`find`), `inspect` (`describe`), `walk` (`neighbors`). + +## Argument contract + +Optional positional argument: microservice name. Omit to list all routes. + +**`microservice` value note:** Not validated by the MCP — invalid name returns an empty list. Verify with `find(kind="microservice", filter={})` if results look wrong. + +## Steps + +1. **Find.** Call: + - With microservice: `find(kind="route", filter={microservice:})` + - Without: `find(kind="route", filter={})` +2. **Render.** For each row show `fqn` (HTTP method + path, or async topic), `microservice`, `framework` (e.g. `spring_mvc`, `spring_kafka`, `codebase_http`), and `id`. Group by microservice if no filter. + +## Optional filters to narrow + +- `path_prefix:"/chat"` — HTTP routes under a path +- `topic_prefix:"chat-events"` — async topics under a prefix +- `framework:"spring_mvc"` — only HTTP routes +- `framework:"spring_kafka"` — only Kafka listeners + +These compose with `microservice`. Brownfield-annotated routes have `framework` values starting with `codebase_`. + +## Recovery + +- Empty with a microservice argument: re-run without the filter; if non-empty, the name is wrong. +- Empty without any filter: confirm the index is built (`describe(id="meta:index")` if available, else ask the user to re-run `java-codebase-rag init`). +- After two failed attempts on the same intent, stop and report. + +## Worked example + +User: `/routes chat-assign` +You: +``` +Q-class: structured Pick: find Why: route listing for one microservice +→ find(kind="route", filter={microservice:"chat-assign"}) + → route:POST /chat/assign microservice=chat-assign framework=spring_mvc + → route:GET /chat/status microservice=chat-assign framework=spring_mvc + → route:chat-events.assigned microservice=chat-assign framework=spring_kafka +``` + +## Do not + +- Do not enumerate routes from training-data Spring conventions — the route set is per-project. +- Do not read source files when `find` can answer. +- Do not call `search` for this — it's a structured listing. + +## Out of scope + +- The handler method for a route (use `/handlers`). +- All inbound paths to a route (use `/who-hits-route`). +- Following a route end-to-end (use `/trace-request-flow`). + +## Going deeper + +Full route schema (HTTP vs async, framework values, brownfield-annotated routes) is in `docs/AGENT-GUIDE.md`. This skill is self-sufficient for `/routes`. diff --git a/skills/tier-1/who-hits-route/SKILL.md b/skills/tier-1/who-hits-route/SKILL.md new file mode 100644 index 0000000..f2b9eb8 --- /dev/null +++ b/skills/tier-1/who-hits-route/SKILL.md @@ -0,0 +1,75 @@ +--- +name: who-hits-route +description: Show all inbound paths to a route — cross-service HTTP/async callers plus the local handler. Use when the user asks "who calls this endpoint", "who hits this route", "what services call POST /foo", or "all inbound to route X". Argument is a route: id or route identifier. Combines HTTP_CALLS, ASYNC_CALLS, and EXPOSES in one neighbors call. +--- + +# /who-hits-route — All inbound paths to a route + +## When to use + +The user has a **route** and wants every inbound edge: cross-service HTTP callers (`HTTP_CALLS` from Client nodes), async producers (`ASYNC_CALLS` from Producer nodes), and the local handler method (`EXPOSES` from a Symbol). + +This is the *cross-service* counterpart to `/callers` (which only handles in-process method-to-method). + +## Tools used + +`resolve` (when argument isn't already `route:`) + `neighbors`. `find` only as recovery fallback. + +## Reasoning preamble (mandatory) + +``` +Q-class: walk Pick: neighbors Why: all inbound edges of a route +``` + +## Argument contract + +Single positional argument: a **route** id (`route:...` preferred) OR an identifier-shaped string (`/chat/join`, `POST /chat/join`). + +## Steps + +1. **Resolve.** If argument starts with `route:` or `r:`, use directly. Else `resolve(identifier=, hint_kind="route")`: + - `one` → use `node.id`. + - `many` → list candidates, stop. + - `none` → `find(kind="route", filter={path_prefix:})`; if still empty, stop and report. +2. **Walk all inbound.** Call `neighbors(ids=, direction="in", edge_types=["HTTP_CALLS","ASYNC_CALLS","EXPOSES"])`. +3. **Render grouped by edge type:** + - `EXPOSES` → handler method Symbol (always exactly one in well-formed projects). + - `HTTP_CALLS` → Client nodes. Each row carries `attrs.match` (path/method match strength) and `attrs.confidence`. + - `ASYNC_CALLS` → Producer nodes. Same `attrs.match` / `attrs.confidence` columns. + +## Recovery + +- Empty result on a known route: the route exists but has no callers indexed. Confirm with `describe()` (`edge_summary` will show the same). +- Low `attrs.confidence` rows: those are probabilistic matches (path-template inference). Report but flag. +- After two failed attempts on the same intent, stop and report. + +## Worked example + +User: `/who-hits-route POST /chat/join` +You: +``` +Q-class: structured Pick: resolve Why: identifier-shaped route arg +→ resolve(identifier="POST /chat/join", hint_kind="route") + → status=one id=route:POST /chat/join +Q-class: walk Pick: neighbors Why: all inbound edges of route +→ neighbors(ids="route:POST /chat/join", direction="in", + edge_types=["HTTP_CALLS","ASYNC_CALLS","EXPOSES"]) + → EXPOSES : sym:com.bank.chat.core.api.ChatController#joinOperator (chat-core) + → HTTP_CALLS : client:com.bank.gateway.client.ChatClient#join (gateway) match=exact confidence=1.0 + → ASYNC_CALLS: (none) +``` + +## Do not + +- Do not pass `HTTP_CALLS`/`ASYNC_CALLS` on a bare method `sym:` — those edges originate at Client/Producer nodes, never methods. +- Do not fabricate `route:` ids. + +## Out of scope + +- In-process callers of a method (use `/callers`). +- Following the request forward from the handler (use `/trace-request-flow`). +- The handler alone (use `/handlers`). + +## Going deeper + +`HTTP_CALLS` / `ASYNC_CALLS` schema (always Client→Route / Producer→Route, never one-hop from method) and the `attrs.match` semantics are in `docs/AGENT-GUIDE.md`. This skill is self-sufficient for `/who-hits-route`. diff --git a/skills/tier-2/explain-feature/SKILL.md b/skills/tier-2/explain-feature/SKILL.md new file mode 100644 index 0000000..5227ae0 --- /dev/null +++ b/skills/tier-2/explain-feature/SKILL.md @@ -0,0 +1,97 @@ +--- +name: explain-feature +description: Understand how a feature works end-to-end by locating entry points and tracing call chains with bounded depth. Use when the user asks "how does X work", "explain feature X", "walk me through Y", or "show me the flow of Z". Argument is free-form feature/concept text. Composes search → describe → bounded neighbors walks. +--- + +# /explain-feature — Understand a feature end-to-end + +## When to use + +The user wants a **narrative explanation** of how something works: entry points, the call chain to persistence/async boundaries, and any cross-service hops. The input is fuzzy (a feature name, not an id). + +For a more focused single-method noise-filtered map use `/mini-map`. For a known route end-to-end use `/trace-request-flow`. + +## Tools used + +`search`, `describe`, `neighbors`. No `find` — the input is fuzzy. + +## Reasoning preamble (mandatory) + +Before **each** MCP call: + +``` +Q-class: +Pick: Why: <≤8 words> +``` + +**Q-class taxonomy:** `semantic` (`search`), `structured` (`find`/`resolve`), `inspect` (`describe`), `walk` (`neighbors`). + +This skill typically uses `semantic → inspect → walk → walk`. + +## Argument contract + +Single positional argument: free-form text describing the feature or concept to explain. + +## Steps + +1. **Locate entry points.** Call `search(query=, limit=8)`. Pick top 1–3 hits with strong `symbol_id` fit (role aligns with what the user wants — `CONTROLLER` for HTTP features, `SERVICE` for business logic, etc.). +2. **Inspect each hit.** Call `describe(id=)` for each. Read `edge_summary` (per-label `in`/`out` counts) and `role` to choose which edges to walk next. +3. **Walk with bounded `neighbors`.** Use *small* `edge_types` sets per call: + - **Methods:** `neighbors(ids=, direction="out", edge_types=["CALLS"])` for in-process flow. + - **At controller/handler boundaries:** `neighbors(ids=, direction="in", edge_types=["EXPOSES"])` to find the handler from a route, or vice versa. + - **At outbound HTTP/async:** `neighbors(out, ["DECLARES_CLIENT"])` then `neighbors(out, ["HTTP_CALLS"])`; or `neighbors(out, ["DECLARES_PRODUCER"])` then `neighbors(out, ["ASYNC_CALLS"])`. + - **Type wiring:** `neighbors(in, ["IMPLEMENTS"])` to see who realizes an interface; `neighbors(in, ["INJECTS"])` to see who depends on a type. +4. **Render.** Synthesize a narrative: entry points → key methods → data flow → cross-service boundaries. Cite each claim with the `sym:` / `route:` id you walked. + +## Stop conditions + +- Maximum 3 hops from any entry point. +- Stop as soon as you can answer the user's question. +- Do not pre-fetch unrelated subgraphs. +- After two empty/failed `neighbors` calls on the same node, stop walking that branch. + +## Recursion limit + +- Depth ≤ 3 from each entry point. +- Maximum 10 `neighbors` calls total across the whole skill invocation. +- If the explanation needs more, hand off to `/trace-request-flow` (route-anchored, depth 4) or `/impact-of` (reverse direction). + +## Worked example + +User: `/explain-feature operator assignment` +You: +``` +Q-class: semantic Pick: search Why: NL feature name +→ search(query="operator assignment", limit=8) + → sym:com.bank.chat.assign.service.OperatorAssignmentService (interface, SERVICE) + → sym:com.bank.chat.assign.api.AssignController (CONTROLLER) +Q-class: inspect Pick: describe Why: edge_summary on interface +→ describe(id="sym:...OperatorAssignmentService") + → edge_summary { IMPLEMENTS.in: 2, INJECTS.in: 3, CALLS.in: 4 } +Q-class: walk Pick: neighbors Why: find concrete implementors +→ neighbors(ids="sym:...OperatorAssignmentService", direction="in", edge_types=["IMPLEMENTS"]) + → RoundRobin..., Weighted... (2 strategies) +Q-class: walk Pick: neighbors Why: trace inbound from controller +→ neighbors(ids="sym:...AssignController", direction="out", edge_types=["CALLS"]) + → AssignController → OperatorAssignmentService#assign → OperatorRepository#save +Synthesize: "Operator assignment has two strategies (RoundRobin, Weighted) +behind an interface. Triggered via AssignController. Persists via OperatorRepository..." +``` + +## Do not + +- Do not answer from training data or general Java knowledge. +- Do not read source files when MCP can answer. +- Do not skip MCP calls and guess. +- Do not fabricate ids — always obtain them from `search` / `find` / `resolve`. +- Do not walk all edge types at once — small `edge_types` sets per call. + +## Out of scope + +- Exact impact analysis (use `/impact-of`). +- Route-anchored end-to-end trace (use `/trace-request-flow`). +- Noise-filtered single-method call map (use `/mini-map`). + +## Going deeper + +Edge taxonomy, the locate→inspect→walk workflow, and the recovery playbook are in `docs/AGENT-GUIDE.md`. This skill is self-sufficient for `/explain-feature`. diff --git a/skills/tier-2/impact-of/SKILL.md b/skills/tier-2/impact-of/SKILL.md new file mode 100644 index 0000000..ba634f2 --- /dev/null +++ b/skills/tier-2/impact-of/SKILL.md @@ -0,0 +1,95 @@ +--- +name: impact-of +description: Analyze what breaks if a symbol changes by walking inbound edges recursively with bounded depth. Use when the user asks "what breaks if I change X", "impact of changing X", "who depends on X", or "blast radius of modifying Y". Argument is a sym: id or identifier resolved via `resolve`. Covers CALLS, INJECTS, IMPLEMENTS, EXTENDS plus route/client impact when applicable. +--- + +# /impact-of — What breaks if this changes + +## When to use + +The user wants the **blast radius** of changing a symbol: who calls it, who injects it, who implements it, and (for methods on routes / methods declaring clients) what crosses service boundaries. + +This is the reverse direction of `/trace-request-flow`. For a *forward* request walk use that skill instead. + +## Tools used + +`resolve`, `describe`, `neighbors`. `search` only as a recovery fallback. + +## Reasoning preamble (mandatory) + +``` +Q-class: +Pick: Why: <≤8 words> +``` + +This skill typically uses `structured → inspect → walk → walk ...`. + +## 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 argument starts with `sym:`, use directly. Else `resolve(identifier=, hint_kind="symbol")` (`one`/`many`/`none` handling per Tier 1 skills). +2. **Inspect.** Call `describe(id=)`. Read `edge_summary` and `role`. +3. **Recursive inbound walk (depth ≤ 2).** Call `neighbors(ids=, direction="in", edge_types=["CALLS","INJECTS","IMPLEMENTS","EXTENDS"])`. For each inbound neighbor that is a **method** symbol, repeat the same call on that id (one more hop only). +4. **Route/client impact** (when applicable): + - If the symbol is a method that handles a route: `neighbors(, "out", ["EXPOSES"])` to find the route, then `neighbors(, "in", ["HTTP_CALLS","ASYNC_CALLS"])` for callers outside the codebase. Each row is a Client or Producer in another service. + - If the symbol declares clients: `neighbors(, "out", ["DECLARES_CLIENT"])`, then `neighbors(, "out", ["HTTP_CALLS"])` for affected downstream services. +5. **Render impact list.** Deduplicate. Group as: + - **Direct (depth 1):** callers, injectors, implementors, extenders. + - **Transitive (depth 2):** callers-of-callers, injectors-of-callers. + - **Cross-service:** route / client impact when applicable. + - Cite each entry by `sym:` / `route:` / `client:` id. + +## Stop conditions + +- Depth limit reached (≤ 2 hops on the inbound walk). +- No more inbound edges to follow. +- Cycle detected (node already in impact set). +- After two empty/failed `neighbors` calls in a row, stop walking that branch. + +## Recursion limit + +- Depth ≤ 2 from the target symbol on the inbound walk. +- Maximum 8 `neighbors` calls total. Route/client cross-service hops count toward this budget. + +## Worked example + +User: `/impact-of ChatRepository` +You: +``` +Q-class: structured Pick: resolve Why: identifier-shaped argument +→ resolve(identifier="ChatRepository", hint_kind="symbol") + → sym:com.bank.chat.core.repository.ChatRepository +Q-class: inspect Pick: describe Why: edge_summary +→ describe(id="sym:...") + → role: REPOSITORY edge_summary { CALLS.in: 7, INJECTS.in: 2 } +Q-class: walk Pick: neighbors Why: depth-1 inbound +→ neighbors(ids="sym:...", direction="in", edge_types=["CALLS","INJECTS","IMPLEMENTS","EXTENDS"]) + → callers: ChatService#save, ChatService#findById, ... + → injectors: ChatService (constructor) +Q-class: walk Pick: neighbors Why: depth-2 from ChatService +→ neighbors(ids="sym:...ChatService", direction="in", edge_types=["CALLS","INJECTS","IMPLEMENTS","EXTENDS"]) + → callers: ChatController#joinOperator, ... +Render impact: + Direct (depth 1): ChatService#save, ChatService#findById (CALLS); ChatService (INJECTS) + Transitive (depth 2): ChatController#joinOperator (CALLS via ChatService) +``` + +## Do not + +- Do not answer from training data. +- Do not read source files when MCP can answer. +- Do not fabricate ids. +- Do not walk **outbound** here — that's `/trace-request-flow` / `/callees` / `/explain-feature`. + +## Out of scope + +- Exact line-level change impact (use `git diff` + source reading after this analysis). +- Noise-filtered call maps (use `/mini-map`). +- Forward request flow (use `/trace-request-flow`). + +## Going deeper + +Edge semantics for `CALLS` / `INJECTS` / `IMPLEMENTS` / `EXTENDS`, plus the `HTTP_CALLS` cross-service caller pattern, are in `docs/AGENT-GUIDE.md`. This skill is self-sufficient for `/impact-of`. diff --git a/skills/tier-2/mini-map/SKILL.md b/skills/tier-2/mini-map/SKILL.md new file mode 100644 index 0000000..f9a8ab4 --- /dev/null +++ b/skills/tier-2/mini-map/SKILL.md @@ -0,0 +1,142 @@ +--- +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, the user asks "map what X does", "simplify the call graph for X", "what does X actually do", or any time a hot SERVICE/COMPONENT method needs a clean readout. Argument is a sym: id or identifier, with optional depth (default 2, max 4). +--- + +# /mini-map — Noise-filtered call map for a method + +## When to use + +The user has a **hot method** (typically a SERVICE/COMPONENT) where raw `/callees` returns 30+ rows mixed with entity accessors and JDK calls. `/mini-map` composes MCP `edge_filter` axes with a small set of skill-side heuristics to produce a clean DELEGATES / PERSISTS / READS / PUBLISHES readout. + +For a single one-hop callee listing use `/callees`. For a feature-level walk use `/explain-feature`. For impact analysis use `/impact-of`. + +## Tools used + +`resolve`, `neighbors` (with `edge_filter`). `describe` and `search` only as recovery fallbacks. + +## Reasoning preamble (mandatory) + +``` +Q-class: +Pick: Why: <≤8 words> +``` + +This skill is mostly `structured → walk → walk ...` (one `walk` per hop, possibly multiple per hop for filtered passes). + +## 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 to apply on each recursion (uses `microservice` on `find` lookups; on `neighbors` it's an out-of-band display filter — apply when rendering). + +## Steps + +### Step 1 — Resolve + +If the argument starts with `sym:`, use it as the id. Otherwise call `resolve(identifier=, hint_kind="symbol")`. On `status="one"`, use `node.id`; on `many`, list `candidates` and stop; on `none`, call `search(query=, limit=5)` and stop if still empty. + +### Step 2 — Fetch ordered CALLS + +Call `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(direction="out", edge_types=["CALLS"], edge_filter={callee_declaring_role:"SERVICE"})`. +- **Trim JDK/low-signal:** `neighbors(direction="out", edge_types=["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(direction="out", edge_types=["CALLS"], dedup_calls=True)`. +- **Full transcript with unresolved sites:** `neighbors(direction="out", edge_types=["CALLS"], include_unresolved=True)` — use **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 + PUBLISHES → …Publisher#publish (×1) + [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). +- After two empty filtered `neighbors` calls on the same node, fall back to raw `/callees` for that node. + +## 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 after. + +**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: +``` +Q-class: structured Pick: resolve Why: identifier-shaped argument +→ resolve(identifier="ClientMessageProcessor#process", hint_kind="symbol") + → sym:com.bank.chat.core.processor.ClientMessageProcessor#process(ProcessingContext, InternalEvent) +Q-class: walk Pick: neighbors Why: raw CALLS scan +→ neighbors(direction="out", edge_types=["CALLS"]) + → ~49 rows (post-ontology-15 re-index) +Q-class: walk Pick: neighbors Why: skeleton pass via SERVICE role +→ neighbors(direction="out", edge_types=["CALLS"], edge_filter={callee_declaring_role:"SERVICE"}) + → skeleton: 8 rows +Skill classify → ~8–12 signal rows total. +Output: + ClientMessageProcessor#process(ProcessingContext, InternalEvent) + DELEGATES → SplitResolverService#resolveSplit + DELEGATES → DistributionTriggerPublisher#trigger + PERSISTS → ChatRepository#save (×2) + READS → ChatRepository#findById + [filtered ~37 edges: ~22 accessors, ~10 JDK/OTHER, ~5 deduped] +``` + +## Do not + +- Do not answer from training data or general Java knowledge. +- Do not read source files when MCP can answer. +- Do not skip MCP calls and guess. +- Do not fabricate `sym:` ids — always obtain them from `resolve` / `find` / `search`. +- Do not bypass the MCP `edge_filter` when applicable — these heuristics **compose** with `edge_filter`, they don't replace it. + +## 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. + +## Going deeper + +The full `edge_filter` axis list, the role taxonomy (every `callee_declaring_role` value), and the rationale behind the post-ontology-15 `UnresolvedCallSite` split are in `docs/AGENT-GUIDE.md`. This skill is self-sufficient for `/mini-map`. diff --git a/skills/tier-2/trace-request-flow/SKILL.md b/skills/tier-2/trace-request-flow/SKILL.md new file mode 100644 index 0000000..d52a568 --- /dev/null +++ b/skills/tier-2/trace-request-flow/SKILL.md @@ -0,0 +1,107 @@ +--- +name: trace-request-flow +description: Follow a request from an HTTP/async route entry point through the in-process call chain to persistence or async boundaries, with cross-service hops at clients/producers. Use when the user asks "trace POST /foo", "follow the request for X", "what happens when X is called", or "end-to-end flow of route Y". Argument is a route id, METHOD /path string, or path fragment. +--- + +# /trace-request-flow — Follow a request end-to-end + +## When to use + +The user has a **route** (or path) and wants a forward trace: route → handler method → service calls → repository / outbound client / producer. This is the *forward* counterpart to `/impact-of`. + +For a feature explanation that isn't anchored to a route use `/explain-feature`. For a single noisy method use `/mini-map`. + +## Tools used + +`resolve`, `find` (route fallback), `neighbors`. `describe` optional. `search` only as a deep-fallback. + +## Reasoning preamble (mandatory) + +``` +Q-class: +Pick: Why: <≤8 words> +``` + +Typical sequence: `structured → walk → walk → walk` (with the occasional `inspect`). + +## Argument contract + +Single positional argument: a route identifier — `route:` id, `METHOD /path`, or path fragment → `resolve(identifier=..., hint_kind="route")` then `find(kind="route", filter={path_prefix:...})` on `none`. + +## Steps + +1. **Resolve route.** If argument starts with `route:` or `r:`, use directly. Else `resolve(identifier=, hint_kind="route")`: + - `one` → use `node.id`. + - `many` → list candidates, stop. + - `none` → `find(kind="route", filter={path_prefix:})`; if still empty, stop and report. +2. **Handler method.** `neighbors(ids=, direction="in", edge_types=["EXPOSES"])` → exactly one handler `sym:` in well-formed projects. +3. **Walk call chain (depth ≤ 4 on methods).** `neighbors(ids=, direction="out", edge_types=["CALLS"])`. For each callee: + - **SERVICE / COMPONENT** → likely to delegate further; recurse one more hop. + - **REPOSITORY / MAPPER** → classify as persistence, **stop** that branch. + - **CLIENT-declaring** (parent has `DECLARES_CLIENT.out > 0` on `describe`) → go to step 4 for that method. + - **PRODUCER-declaring** → same, for producers. +4. **Cross-service boundaries.** At methods with outbound clients: `neighbors(, "out", ["DECLARES_CLIENT"])`, then for each Client id `neighbors(, "out", ["HTTP_CALLS"])`. At methods with async producers: `neighbors(, "out", ["DECLARES_PRODUCER"])`, then for each Producer id `neighbors(, "out", ["ASYNC_CALLS"])`. +5. **Render ordered sequence.** + ``` + Route → Handler → Service → ... → Repository / Client → DownstreamRoute / Producer → DownstreamTopic + ``` + with edge type annotations at every boundary. + +## Stop conditions + +- Depth limit reached (≤ 4 hops from the handler). +- No more `CALLS` edges to follow. +- All branches terminated at REPOSITORY / MAPPER / CLIENT / PRODUCER endpoints. +- Cycle detected (method already in trace). +- After two empty `neighbors` calls in a row on the same branch, stop that branch. + +## Recursion limit + +- Depth ≤ 4 from the handler method on the CALLS walk. +- Maximum 10 `neighbors` calls total. Cross-service hops in step 4 count toward this budget. + +## Worked example + +User: `/trace-request-flow POST /chat/join` +You: +``` +Q-class: structured Pick: resolve Why: identifier-shaped route arg +→ resolve(identifier="POST /chat/join", hint_kind="route") + → route:POST /chat/join +Q-class: walk Pick: neighbors Why: route → handler +→ neighbors(ids="route:POST /chat/join", direction="in", edge_types=["EXPOSES"]) + → sym:com.bank.chat.core.api.ChatController#joinOperator(JoinOperatorRequest) +Q-class: walk Pick: neighbors Why: depth-1 CALLS from handler +→ neighbors(ids="sym:...ChatController#joinOperator(...)", direction="out", edge_types=["CALLS"]) + → ChatService#join (SERVICE — recurse) +Q-class: walk Pick: neighbors Why: depth-2 from ChatService#join +→ neighbors(ids="sym:...ChatService#join(...)", direction="out", edge_types=["CALLS"]) + → ChatRepository#save (REPOSITORY — persistence, stop branch) + → ChatEventPublisher#publishJoined (PRODUCER-declaring — step 4 hop) +Q-class: walk Pick: neighbors Why: producer fan-out +→ neighbors(ids="sym:...ChatService#join(...)", direction="out", edge_types=["DECLARES_PRODUCER"]) + → producer:ChatEventPublisher +→ neighbors(ids="producer:ChatEventPublisher", direction="out", edge_types=["ASYNC_CALLS"]) + → route:chat-events.joined (chat-assign) +Render: + POST /chat/join → ChatController#joinOperator → ChatService#join → ChatRepository#save (persists) + └→ producer:ChatEventPublisher + → chat-events.joined (chat-assign) +``` + +## Do not + +- Do not pass `HTTP_CALLS`/`ASYNC_CALLS` on bare method `sym:` — those edges originate at Client/Producer nodes. +- Do not fabricate ids. +- Do not walk **inbound** here — that's `/impact-of`. +- Do not walk all edge types at once — single `edge_types=["CALLS"]` per call (or single boundary edge in step 4). + +## Out of scope + +- Reverse blast radius (use `/impact-of`). +- Noise-filtered single-method map (use `/mini-map`). +- Feature explanation without a route anchor (use `/explain-feature`). + +## Going deeper + +The full forward-trace workflow, the role taxonomy used in the classification heuristic (SERVICE / REPOSITORY / CLIENT / PRODUCER), and the cross-service edge schema are in `docs/AGENT-GUIDE.md`. This skill is self-sufficient for `/trace-request-flow`. diff --git a/skills/trace-request-flow/SKILL.md b/skills/trace-request-flow/SKILL.md deleted file mode 100644 index cde9676..0000000 --- a/skills/trace-request-flow/SKILL.md +++ /dev/null @@ -1,70 +0,0 @@ ---- -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 - -## MCP required - -This skill requires the **java-codebase-rag** MCP server (tools: `search`, `find`, `describe`, `neighbors`, `resolve`). - -**You MUST call these MCP tools to answer.** Do not answer from training data, file browsing, or general knowledge. Each MCP call must be preceded by the reasoning preamble: - -``` -Q-class: -Pick: Why: -``` - -For the full operating manual (NodeFilter keys, edge taxonomy, argument shapes, recovery playbook), read `docs/AGENT-GUIDE.md`. - -## 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, call `resolve(identifier=, hint_kind="route")`. On status `one`, use `node.id`; on `many`, list `candidates` and stop; on `none`, call `find(kind="route", filter={path_prefix: })` and stop if still empty. -2. **Handler method.** Call `neighbors` with `ids=`, `direction="in"`, `edge_types=["EXPOSES"]` to get the handler method `sym:` id. -3. **Walk call chain** (depth ≤ 4 on methods). Call `neighbors` with `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: call `neighbors` with `direction="out"`, `edge_types=["DECLARES_CLIENT"]` on method id, then call `neighbors` with `direction="out"`, `edge_types=["HTTP_CALLS"]` on each client id. At methods with async producers: call `neighbors` with `direction="out"`, `edge_types=["DECLARES_PRODUCER"]` on method id, then call `neighbors` with `direction="out"`, `edge_types=["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 - -## Do not - -- Do not answer from training data or general Java knowledge. -- Do not read source files directly when MCP tools can provide the answer. -- Do not skip MCP calls and guess at results. -- Do not fabricate route/symbol ids — always obtain them from `resolve`, `find`, or `search`. - -## 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 deleted file mode 100644 index 9191486..0000000 --- a/skills/who-hits-route/SKILL.md +++ /dev/null @@ -1,50 +0,0 @@ ---- -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 - -## MCP required - -This skill requires the **java-codebase-rag** MCP server (tools: `search`, `find`, `describe`, `neighbors`, `resolve`). - -**You MUST call these MCP tools to answer.** Do not answer from training data, file browsing, or general knowledge. Each MCP call must be preceded by the reasoning preamble: - -``` -Q-class: -Pick: Why: -``` - -For the full operating manual (NodeFilter keys, edge taxonomy, argument shapes, recovery playbook), read `docs/AGENT-GUIDE.md`. - -## 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 directly. Otherwise, call `resolve(identifier=, hint_kind="route")`. On status `one`, use `node.id`; on `many`, list `candidates` and stop; on `none`, call `find(kind="route", filter={path_prefix: })` and stop if still empty. -2. **All inbound.** Call `neighbors` with `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 - -## Do not - -- Do not answer from training data or general Java knowledge. -- Do not read source files directly when MCP tools can provide the answer. -- Do not skip MCP calls and guess at results. -- Do not fabricate route ids — always obtain them from `resolve` or `find`. - -## 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 index 898e742..da996f6 100644 --- a/tests/test_agent_skills_static.py +++ b/tests/test_agent_skills_static.py @@ -45,6 +45,8 @@ # --------------------------------------------------------------------------- SKILLS_DIR = Path(__file__).resolve().parent.parent / "skills" +TIER1_DIR = SKILLS_DIR / "tier-1" +TIER2_DIR = SKILLS_DIR / "tier-2" TIER1_NAMES = [ "nl", "controllers", "routes", "clients", "producers", @@ -59,6 +61,15 @@ ALL_SKILL_NAMES = TIER1_NAMES + TIER2_NAMES +def _skill_dir(name: str) -> Path: + """Return the tier directory for a skill name.""" + if name in TIER1_NAMES: + return TIER1_DIR / name + if name in TIER2_NAMES: + return TIER2_DIR / name + raise ValueError(f"Unknown skill name: {name}") + + 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) @@ -122,7 +133,7 @@ def _extract_edge_type_refs(body: str) -> set[str]: 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" + path = _skill_dir(name) / "SKILL.md" text = path.read_text(encoding="utf-8") fm = _parse_frontmatter(text) # Body is everything after the closing --- @@ -150,17 +161,18 @@ class TestSkillFrontmatter: @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'" + rel = _skill_dir(name).relative_to(SKILLS_DIR.parent) + assert "name" in fm, f"{rel}/SKILL.md missing frontmatter 'name'" + assert fm["name"] == name, f"{rel}/SKILL.md: name={fm['name']!r}, expected {name!r}" + assert "description" in fm, f"{rel}/SKILL.md missing frontmatter 'description'" assert len(fm["description"]) >= 20, ( - f"skills/{name}/SKILL.md description too short ({len(fm['description'])} chars)" + f"{rel}/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" + path = _skill_dir(name) / "SKILL.md" + assert path.is_file(), f"Missing {path.relative_to(SKILLS_DIR.parent)}" class TestMCPToolReferences: @@ -169,15 +181,17 @@ class TestMCPToolReferences: @pytest.mark.parametrize("name", ALL_SKILL_NAMES) def test_tool_refs_are_valid(self, name: str): _, body = _read_skill(name) + rel = _skill_dir(name).relative_to(SKILLS_DIR.parent) refs = _extract_tool_refs(body) invalid = refs - _VALID_TOOLS - assert not invalid, f"skills/{name}/SKILL.md references invalid tools: {invalid}" + assert not invalid, f"{rel}/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) + rel = _skill_dir(name).relative_to(SKILLS_DIR.parent) refs = _extract_tool_refs(body) - assert refs, f"skills/{name}/SKILL.md references no MCP tools" + assert refs, f"{rel}/SKILL.md references no MCP tools" class TestKindAndEdgeReferences: @@ -186,23 +200,27 @@ class TestKindAndEdgeReferences: @pytest.mark.parametrize("name", ALL_SKILL_NAMES) def test_kind_refs_are_valid(self, name: str): _, body = _read_skill(name) + rel = _skill_dir(name).relative_to(SKILLS_DIR.parent) refs = _extract_kind_refs(body) invalid = refs - _VALID_KINDS - assert not invalid, f"skills/{name}/SKILL.md references invalid find kinds: {invalid}" + assert not invalid, f"{rel}/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) + rel = _skill_dir(name).relative_to(SKILLS_DIR.parent) refs = _extract_direction_refs(body) invalid = refs - _VALID_DIRECTIONS - assert not invalid, f"skills/{name}/SKILL.md references invalid directions: {invalid}" + assert not invalid, f"{rel}/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) + rel = _skill_dir(name).relative_to(SKILLS_DIR.parent) refs = _extract_edge_type_refs(body) invalid = refs - _ALL_EDGE_TYPES - assert not invalid, f"skills/{name}/SKILL.md references invalid edge_types: {invalid}" + assert not invalid, f"{rel}/SKILL.md references invalid edge_types: {invalid}" + class TestTier2BodyStructure: @@ -211,23 +229,25 @@ class TestTier2BodyStructure: @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'" + rel = _skill_dir(name).relative_to(SKILLS_DIR.parent) + assert "## Stop conditions" in body, f"{rel}/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'" + rel = _skill_dir(name).relative_to(SKILLS_DIR.parent) + assert "## Recursion limit" in body, f"{rel}/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" + "skills/tier-2/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)" + "skills/tier-2/mini-map/SKILL.md missing output shape (PERSISTS/DELEGATES labels)" ) @@ -237,23 +257,50 @@ class TestWorkedExamples: @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'" + rel = _skill_dir(name).relative_to(SKILLS_DIR.parent) + assert "## Worked example" in body, f"{rel}/SKILL.md missing '## Worked example'" class TestDirectoryIntegrity: - """skills/ directory must contain exactly the expected skills.""" + """skills/ must split into tier-1/ and tier-2/ with 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) + def test_tier_dirs_exist(self): + assert TIER1_DIR.is_dir(), "skills/tier-1/ missing" + assert TIER2_DIR.is_dir(), "skills/tier-2/ missing" + + def test_tier1_no_extra_dirs(self): + actual = {p.name for p in TIER1_DIR.iterdir() if p.is_dir() and (p / "SKILL.md").exists()} + expected = set(TIER1_NAMES) extra = actual - expected - assert not extra, f"Unexpected skill directories: {extra}" + assert not extra, f"Unexpected skills under skills/tier-1/: {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) + def test_tier1_no_missing_dirs(self): + actual = {p.name for p in TIER1_DIR.iterdir() if p.is_dir() and (p / "SKILL.md").exists()} + expected = set(TIER1_NAMES) missing = expected - actual - assert not missing, f"Missing skill directories: {missing}" + assert not missing, f"Missing skills under skills/tier-1/: {missing}" + + def test_tier2_no_extra_dirs(self): + actual = {p.name for p in TIER2_DIR.iterdir() if p.is_dir() and (p / "SKILL.md").exists()} + expected = set(TIER2_NAMES) + extra = actual - expected + assert not extra, f"Unexpected skills under skills/tier-2/: {extra}" + + def test_tier2_no_missing_dirs(self): + actual = {p.name for p in TIER2_DIR.iterdir() if p.is_dir() and (p / "SKILL.md").exists()} + expected = set(TIER2_NAMES) + missing = expected - actual + assert not missing, f"Missing skills under skills/tier-2/: {missing}" + + def test_no_skills_at_root(self): + """Skills must live under tier-1/ or tier-2/, not at the root of skills/.""" + root_skill_dirs = { + p.name for p in SKILLS_DIR.iterdir() + if p.is_dir() and p.name not in ("tier-1", "tier-2") and (p / "SKILL.md").exists() + } + assert not root_skill_dirs, ( + f"Found skills at skills/ root (must be moved into tier-1/ or tier-2/): {root_skill_dirs}" + ) def test_readme_exists(self): assert (SKILLS_DIR / "README.md").is_file(), "skills/README.md missing"