From 9867ec4787abb16b4c1a2dbd79fe2f9c0eea2fa2 Mon Sep 17 00:00:00 2001 From: Arnaud Date: Mon, 1 Jun 2026 15:16:41 +0400 Subject: [PATCH 01/13] Add try and finally to make sure teardown is called --- libplum/plum.nim | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/libplum/plum.nim b/libplum/plum.nim index cdcadeb..c70fd33 100644 --- a/libplum/plum.nim +++ b/libplum/plum.nim @@ -79,9 +79,11 @@ type template foreignThreadGc(body: untyped) = when declared(setupForeignThreadGc): setupForeignThreadGc() - body - when declared(tearDownForeignThreadGc): - tearDownForeignThreadGc() + try: + body + finally: + when declared(tearDownForeignThreadGc): + tearDownForeignThreadGc() var activeMappingsLock: Lock var activeMappings {.guard: activeMappingsLock.}: Table[cint, MappingHandle] From 4532517ca5b29172e6d624f5fc62bc6a9c6f3a4a Mon Sep 17 00:00:00 2001 From: Arnaud Date: Mon, 1 Jun 2026 15:20:23 +0400 Subject: [PATCH 02/13] Add safety guard when destroying the mapping --- libplum/plum.nim | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/libplum/plum.nim b/libplum/plum.nim index c70fd33..9201756 100644 --- a/libplum/plum.nim +++ b/libplum/plum.nim @@ -266,6 +266,10 @@ proc createMapping*( proc destroyMapping*(id: cint) {.raises: [].} = ## Must be called exactly once after a successful createMapping. + ## Safe to call again or on an unknown id + withSafeLock: + if id notin activeMappings: + return discard plum_destroy_mapping(id) proc hasMapping*(id: cint): bool {.raises: [].} = From f66c6114f9bdb938fada37bfafe0703cd9f79d22 Mon Sep 17 00:00:00 2001 From: Arnaud Date: Mon, 1 Jun 2026 15:27:47 +0400 Subject: [PATCH 03/13] Remove duplicated pragma --- libplum/libplum.nim | 11 ----------- libplum/plum.nim | 4 ++++ 2 files changed, 4 insertions(+), 11 deletions(-) diff --git a/libplum/libplum.nim b/libplum/libplum.nim index 8de5c8b..4ea5f8e 100644 --- a/libplum/libplum.nim +++ b/libplum/libplum.nim @@ -15,17 +15,6 @@ const libraryPath = libplumPath & "/libplum.a" {.passc: "-I" & includePath & " -DPLUM_STATIC".} {.passl: libraryPath.} -# libplum declares some parameters as `const T*` in C (read-only pointer). -# Nim has no equivalent, so the generated C code drops the `const`, causing -# a type mismatch warning in GCC 15+. This pragma suppresses that warning -# only in this translation unit and is valid for both C and C++. -{. - emit: """ -#ifdef __GNUC__ -#pragma GCC diagnostic ignored "-Wno-incompatible-pointer-types" -#endif -""" -.} when defined(windows): {.passl: "-lws2_32 -liphlpapi -lbcrypt".} diff --git a/libplum/plum.nim b/libplum/plum.nim index 9201756..3ce65e4 100644 --- a/libplum/plum.nim +++ b/libplum/plum.nim @@ -12,6 +12,10 @@ import chronos/threadsync import results import ./libplum +# libplum declares some parameters as `const T*` in C (read-only pointer). +# Nim has no equivalent, so the generated C code drops the `const`, causing +# a type mismatch warning in GCC 15+. This pragma suppresses that warning +# only in this translation unit and is valid for both C and C++. {. emit: """ #ifdef __GNUC__ From c9a976fe6ebc6da0a9f40970774c41cb6155324c Mon Sep 17 00:00:00 2001 From: Arnaud Date: Mon, 1 Jun 2026 15:30:49 +0400 Subject: [PATCH 04/13] Add documentation about onStateChange usage --- api.md | 9 ++++++++- libplum/plum.nim | 3 +++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/api.md b/api.md index 11d08fb..1fc7e59 100644 --- a/api.md +++ b/api.md @@ -98,13 +98,20 @@ Returns a `MappingResult` containing the mapping `id` (needed for `destroyMappin Returns an error if no NAT device is found, the mapping fails, or the timeout expires. +> **Warning:** `onStateChange` runs on libplum's internal C thread, not the +> chronos event loop. Do not call chronos APIs or touch non-thread-safe state +> from it; restrict it to thread-safe operations (e.g. `Atomic`, a channel). + ### destroyMapping ```nim proc destroyMapping*(id: cint) ``` -Removes a mapping. Must be called exactly once after a successful `createMapping`. +Removes a mapping. Must be called exactly once after a successful `createMapping`, +otherwise the mapping's internal handle is leaked for the lifetime of the process. +Calling it again, or with an unknown `id`, is a safe no-op. `cleanup` also releases +any mappings still active. ### hasMapping diff --git a/libplum/plum.nim b/libplum/plum.nim index 3ce65e4..8253e8a 100644 --- a/libplum/plum.nim +++ b/libplum/plum.nim @@ -59,6 +59,9 @@ type mapping*: PlumMapping PlumStateCallback* = proc(state: PlumState, mapping: PlumMapping) {.callback.} + ## Invoked on mapping state changes after the initial result. Runs on + ## libplum's internal C thread, not the chronos loop: only touch + ## thread-safe state from it (e.g. Atomic), never chronos APIs. MappingHandle = ref object signal: ThreadSignalPtr From 7f84c28d9127b1303ad8e1fcd4eadbce8e1523c7 Mon Sep 17 00:00:00 2001 From: Arnaud Date: Mon, 1 Jun 2026 15:43:28 +0400 Subject: [PATCH 05/13] Check if the mapping is being destroyed in hasMapping --- libplum/plum.nim | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/libplum/plum.nim b/libplum/plum.nim index 8253e8a..5defa9c 100644 --- a/libplum/plum.nim +++ b/libplum/plum.nim @@ -280,9 +280,12 @@ proc destroyMapping*(id: cint) {.raises: [].} = discard plum_destroy_mapping(id) proc hasMapping*(id: cint): bool {.raises: [].} = - ## Returns true if the mapping exists and has not been destroyed yet. - withSafeLock: - result = id in activeMappings + ## Returns true if the mapping exists and is not being destroyed. + var st: plum_state_t + if plum_query_mapping(id, addr st, nil) == PLUM_ERR_SUCCESS: + PlumState(st.int) notin {Destroying, Destroyed} + else: + false proc getLocalAddress*(): Result[string, string] {.raises: [].} = var buf = newString(PLUM_MAX_ADDRESS_LEN) From baa8d7a819742f89f4e289e868a85dad8e28d49b Mon Sep 17 00:00:00 2001 From: Arnaud Date: Mon, 1 Jun 2026 17:22:10 +0400 Subject: [PATCH 06/13] Add more tests --- libplum/plum.nim | 6 ++++++ tests/test_plum.nim | 38 +++++++++++++++++++++++++++++++++++--- vendor/libplum | 2 +- 3 files changed, 42 insertions(+), 4 deletions(-) diff --git a/libplum/plum.nim b/libplum/plum.nim index 5defa9c..71e154f 100644 --- a/libplum/plum.nim +++ b/libplum/plum.nim @@ -287,6 +287,12 @@ proc hasMapping*(id: cint): bool {.raises: [].} = else: false +proc activeMappingCount*(): int {.raises: [].} = + ## Number of mappings the wrapper still tracks. Drops to 0 once every + ## mapping has fired DESTROYED. Mainly useful to detect handle leaks. + withSafeLock: + result = activeMappings.len + proc getLocalAddress*(): Result[string, string] {.raises: [].} = var buf = newString(PLUM_MAX_ADDRESS_LEN) let res = plum_get_local_address(buf.cstring, buf.len.csize_t) diff --git a/tests/test_plum.nim b/tests/test_plum.nim index e8dfc12..7e7d2ae 100644 --- a/tests/test_plum.nim +++ b/tests/test_plum.nim @@ -16,6 +16,8 @@ import chronos import libplum/plum import libplum/libplum +const miniupnp_protocol {.strdefine.} = "" + suite "plum": test "init and cleanup": let r = init() @@ -39,7 +41,24 @@ suite "plum": test "hasMapping returns false for unknown id": check not hasMapping(999) -const miniupnp_protocol {.strdefine.} = "" + # Only valid where no NAT device answers; the integration container runs + # miniupnpd, which would make the mapping succeed before the timeout. + when miniupnp_protocol == "": + test "createMapping times out without a NAT device": + require init().isOk() + defer: + discard cleanup() + + let r = waitFor createMapping(TCP, 8101, timeout = milliseconds(50)) + check r.isErr() + + test "destroyMapping is a no-op on an unknown id": + require init().isOk() + defer: + discard cleanup() + + destroyMapping(999.cint) + check not hasMapping(999) # The flag is passed by the Docker / Podman container. when miniupnp_protocol != "": @@ -96,8 +115,6 @@ when miniupnp_protocol != "": let r = waitFor createMapping(TCP, 8101, timeout = mappingTimeout) require r.isOk() let res = r.value - defer: - destroyMapping(res.id) checkpoint miniupnp_protocol & " TCP: " & res.mapping.externalHost & ":" & $res.mapping.externalPort @@ -112,6 +129,11 @@ when miniupnp_protocol != "": else: check res.mapping.mappingProtocol == PCP + destroyMapping(res.id) + check not hasMapping(res.id) + # second destroy on a real id must be a safe no-op + destroyMapping(res.id) + test "createMapping UDP and destroying": require init(discoverTimeout = discoverMs, logLevel = logLevel).isOk() defer: @@ -165,3 +187,13 @@ when miniupnp_protocol != "": startMiniupnpd(miniupnp_protocol) check waitRenewal() + + test "cleanup releases active mappings": + require init(discoverTimeout = discoverMs, logLevel = logLevel).isOk() + + let r = waitFor createMapping(TCP, 8401, timeout = mappingTimeout) + require r.isOk() + + # no destroyMapping on purpose: cleanup must release everything + check cleanup().isOk() + check activeMappingCount() == 0 diff --git a/vendor/libplum b/vendor/libplum index b74757f..b5d5f7d 160000 --- a/vendor/libplum +++ b/vendor/libplum @@ -1 +1 @@ -Subproject commit b74757f88c2a98ea905f5b81ad040232bc019c35 +Subproject commit b5d5f7d31b319b21136ec861d59d95f02eaf207a From d65e1098e29863442189ff2b9b7dcf4a76cde641 Mon Sep 17 00:00:00 2001 From: Arnaud Date: Mon, 1 Jun 2026 18:22:53 +0400 Subject: [PATCH 07/13] Fix handle close --- libplum/plum.nim | 54 ++++++++++++++++-------------------------------- 1 file changed, 18 insertions(+), 36 deletions(-) diff --git a/libplum/plum.nim b/libplum/plum.nim index 71e154f..ae6fbc0 100644 --- a/libplum/plum.nim +++ b/libplum/plum.nim @@ -77,8 +77,8 @@ type resolvedInternalPort: uint16 resolvedExternalPort: uint16 resolvedExternalHost: array[PLUM_MAX_HOST_LEN, char] - # Use abandoned pattern for memory freeing - abandoned: Atomic[bool] + # Refcount-like + signalReleases: Atomic[int] onStateChange: PlumStateCallback # libplum calls mappingCallback from its own C thread. Under refc, any thread @@ -97,6 +97,10 @@ var activeMappings {.guard: activeMappingsLock.}: Table[cint, MappingHandle] initLock(activeMappingsLock) +proc releaseSignal(handle: MappingHandle) {.raises: [].} = + if handle.signalReleases.fetchAdd(1) == 1: + discard handle.signal.close() + # We can be confident that the pattern is GC Safe using # a lock. template withSafeLock(body: untyped) = @@ -118,12 +122,7 @@ proc mappingCallback( if plumState == Destroyed: withSafeLock: activeMappings.del(id) - # The handle can be abandoned after a timeout during a - # mapping creation. - # In that case, destroy is called internally and the - # signal pointer can be closed. - if handle.abandoned.load(): - discard handle.signal.close() + handle.releaseSignal() # Release the pin set in createMapping: the C library is done with the # raw pointer and will never call this callback again for this mapping. GC_unref(handle) @@ -230,40 +229,23 @@ proc createMapping*( raise finally: if not completed: - # Timeout or cancellation: we cannot close the signal here because - # the C callback may fire later and call fireSync on it. - # Mark the handle as abandoned so the DESTROYED callback closes it. - # Access via activeMappings rather than the local `handle` ref, - # in order to make sure we have the valid reference. - withSafeLock: - let h = activeMappings.getOrDefault(id) - if not h.isNil: - h.abandoned.store(true) + # Timeout or cancellation: a late callback may still fireSync, so the + # mapping is torn down and the close is decided by releaseSignal. discard plum_destroy_mapping(id) - else: - # Signal fired normally, safe to close. - discard signal.close() + handle.releaseSignal() # Reached only when completed = true (CancelledError skips this). if not completed: return err("plum: mapping " & $id & " timed out") - # Read result via activeMappings rather than the local `handle` ref in - # order to make sure we have the valid reference. - var resolvedState: PlumState - var resolvedMapping: PlumMapping - - withSafeLock: - let h = activeMappings.getOrDefault(id) - if not h.isNil: - resolvedState = h.resolvedState - resolvedMapping = PlumMapping( - protocol: h.resolvedProtocol, - mappingProtocol: h.resolvedMappingProtocol, - internalPort: h.resolvedInternalPort, - externalPort: h.resolvedExternalPort, - externalHost: $cast[cstring](unsafeAddr h.resolvedExternalHost), - ) + let resolvedState = handle.resolvedState + let resolvedMapping = PlumMapping( + protocol: handle.resolvedProtocol, + mappingProtocol: handle.resolvedMappingProtocol, + internalPort: handle.resolvedInternalPort, + externalPort: handle.resolvedExternalPort, + externalHost: $cast[cstring](unsafeAddr handle.resolvedExternalHost), + ) if resolvedState == Success: return ok(MappingResult(id: id, mapping: resolvedMapping)) From c2b5693e42d72c6c771007d5f830a9c1056ed984 Mon Sep 17 00:00:00 2001 From: Arnaud Date: Mon, 1 Jun 2026 18:27:52 +0400 Subject: [PATCH 08/13] Update comment --- .claude/SKILL.md | 217 +++++++++++++++++++++++++++++++++++++++++++++++ libplum/plum.nim | 3 +- 2 files changed, 218 insertions(+), 2 deletions(-) create mode 100644 .claude/SKILL.md diff --git a/.claude/SKILL.md b/.claude/SKILL.md new file mode 100644 index 0000000..f8d4133 --- /dev/null +++ b/.claude/SKILL.md @@ -0,0 +1,217 @@ +--- +name: nim-mcp-tools +description: 'Use for Nim symbol navigation, diagnostics, and type resolution. MANDATORY: Use specialized MCP tools (nimFindSymbols, nimFindReferences, nimListSymbols, nimCheckFile, nimCheckProject, nimFindTypeDefinition) first; fall back to grep only on error or user confirmation.' +--- + +# Nim MCP Tools + +## Core Mandate + +AI agents MUST prefer specialized MCP tools over general-purpose instruments (grep, shell commands, `read_file` with manual parsing) for all Nim symbol-related and Nim diagnostics tasks. This is critical for: + +1. **Token Efficiency**: MCP tools return structured, relevant data, avoiding large file reads or noisy grep outputs. +2. **Precision**: These tools understand Nim semantics (scopes, imports, overloads) which string-based search cannot. + +## Activation Rule + +If the user asks to find, rename, remove, audit, update, standardize naming, or fix issues/project of a Nim symbol or its usages, this skill MUST be used first and the workflow must start with the Nim MCP symbol tools. + +If the user asks to check a single Nim file for errors, warnings, hints, diagnostics, issues, problems, or compiler feedback, this skill MUST be used first and the workflow must start with `nimCheckFile`. + +If the user asks to check a Nim project for errors, warnings, hints, diagnostics, issues, problems, compiler feedback, or to "fix" it, this skill MUST be used first and the workflow must start with `nimCheckProject`. + +If the user asks to determine where a symbol comes from, what type it is, what its fields are (including private ones), or resolve a symbol's type definition, this skill MUST be used first and the workflow must start with `nimFindTypeDefinition`. + +This applies to requests phrased as: + +- "find all usages/references of `Foo`" +- "remove all definitions of and references to `Foo`" +- "rename `Foo` everywhere" +- "standardize naming for `Foo`" +- "fix casing for all variables" +- "where is `Foo` defined?" +- "list the symbols in this Nim file/module" +- "check this file/module for errors" +- "show diagnostics for `foo.nim`" +- "check this project/workspace/repository/package for errors" +- "fix issues in this project" +- "fix the repo" +- "find Nim diagnostics in the current codebase" +- "show warnings and hints for this repo" +- "scan the current module tree for Nim issues" +- "what is the type of this symbol?" +- "what module/package does this type come from?" +- "what fields does this type have?" +- "is this a type alias or a concrete type?" +- "where is this type defined?" + +Treat user wording such as **project**, **workspace**, **repository**, **repo**, **package**, **codebase**, **checkout**, and **module tree** as referring to the current Nim project context when they are asking for project-wide diagnostics. + +Treat user wording such as **file**, **module** (when a concrete Nim file is identified), **source file**, and explicit `*.nim` paths as referring to single-file diagnostics when they are asking for diagnostics for one file. + +Do **not** pair `nimFindSymbols`, `nimCheckFile`, or `nimCheckProject` with `grep`, `ripgrep`, or shell search "just to double-check". If the task is about a Nim symbol or Nim diagnostics, MCP tools own the search unless they have already failed. + +## User Terminology vs MCP `kind` + +Users may ask for symbol categories using looser or non-strict terminology. AI agents MUST map that wording to the exact Nim MCP `kind` values before filtering results from `nimListSymbols(...)` or `nimFindSymbols(...)`. + +The MCP server returns Nim-oriented kind names derived from nimsuggest symbol kinds with the leading `sk` removed, such as `Const`, `EnumField`, `Field`, `Iterator`, `Converter`, `Let`, `Macro`, `Method`, `Proc`, `Template`, `Type`, `Var`, and `Func`. + +Use these terminology mappings when interpreting user requests: + +- **function** / **functions**: usually match `Func` and `Proc` +- **pure function** / **pure functions**: match `Func` +- **callable** / **routine**: may include `Func`, `Proc`, `Method`, `Iterator`, `Converter`, `Macro`, and `Template` +- **class** / **classes**: match `Type` +- **variable** / **variables**: match `Var` and `Let` +- **property** / **properties**: match `Field` +- **enum member** / **enum members**: match `EnumField` +- **constant** / **constants**: match `Const` + +Interpret user wording semantically, not literally. For example: + +- "list all classes in this module" -> filter for `kind == Type` +- "list all variables" -> filter for `kind in {Var, Let}` +- "list all functions" -> filter for `kind in {Func, Proc}` unless the surrounding context clearly asks for all callables +- "list all pure functions" -> filter for `kind == Func` + +## When to Use + +- **Finding References**: To rename a symbol, update a signature, or find usages. +- **Symbol Discovery**: To find where a symbol is defined by name. +- **Type Resolution**: To determine where a local symbol comes from (which module), what its type is, and what fields it has (including private ones). Uses `nimFindTypeDefinition(path, line, column)` on the symbol's cursor position. +- **Naming Standardization**: To fix casing or follow style guides (e.g., standardizing to `camelCase`) across the project. +- **Fixing Project Issues**: To iteratively find and resolve all diagnostics in the project. +- **File Analysis**: To get an overview of all symbols in a file. +- **File Diagnostics**: To check one specific Nim file for errors, warnings, and hints. +- **Project Diagnostics**: To check the current Nim project/workspace/repository/package for errors, warnings, and hints. +- **Debugging Type Mismatches**: When a diagnostic reveals a type mismatch, use `nimFindTypeDefinition` on both sides to understand what types are expected vs provided. +- **Code Generation / Refactoring**: Before generating code that interacts with a type, find its definition to understand its structure and field layout. + +## Workflows + +### 1. Find All References or Usages of a Symbol Name + +Do NOT grep for the name. + +1. Call `nimFindSymbols(query: "SymbolName")` to get exact `path`, `line`, and `column`. +2. For each relevant result, call `nimFindReferences(path, line, column)`. +3. Aggregate the results. + +Use this workflow for "usages", "references", "call sites", and cleanup requests such as removing all definitions of and references to a symbol. + +### 2. List All Symbols in a File + +Do NOT read the whole file to find definitions. + +1. Call `nimListSymbols(path: "path/to/file.nim")`. +2. If the user asked for a symbol category, filter by the MCP `kind` values that correspond to the user's terminology. +3. Use the returned list to navigate or analyze the file structure. + +### 3. Find Definitions + +1. Call `nimFindSymbols(query: "query")`. + +### 4. Resolve Type / Determine Origin of a Symbol + +Use this when asked where a symbol comes from, what its type is, or to inspect its definition (fields, kind, etc.). + +1. Call `nimFindTypeDefinition(path, line, column)` with the cursor positioned on the symbol of interest. +2. The result contains the definition `path`, `line`, `column`, `name`, `type`, and `kind`. +3. If the user needs to see the full definition (e.g., object fields), read the source at the returned path/line. +4. Use this for requests like "what type is this variable?", "what module does this type come from?", "what fields does this object have?". + +### 5. Debug Type Mismatch (Check + Type Definition) + +Use this when fixing a diagnostic that involves a type mismatch. + +1. Call `nimCheckFile(path)` to get the diagnostic with the exact error location. +2. For the reported location, call `nimFindTypeDefinition(path, line, column)` on the involved symbols to understand what types are expected vs provided. +3. Resolve the mismatch with the correct type or conversion. + +### 6. Explore Object Structure (List Symbols + Type Definition) + +Use this when asked to describe or document a type's shape. + +1. Call `nimFindSymbols(query: "TypeName")` to locate the type definition. +2. Call `nimFindTypeDefinition(path, line, column)` on the type name to confirm the definition location. +3. Read the source at the definition location to enumerate all fields (including private ones) and their types. +4. Optionally, call `nimFindTypeDefinition` on each field type to recursively resolve nested types. + +### 7. Check a Single Nim File for Diagnostics + +Do NOT approximate file diagnostics by reading the file manually or by running a project-wide diagnostic request when the user asked about one file. + +1. Call `nimCheckFile(path: "path/to/file.nim")`. +2. Treat the result as the answer unless the user explicitly asked for broader validation such as project-wide diagnostics, tests, or builds. +3. Use the returned diagnostics to report errors, warnings, and hints for that file. +4. If the user asked for only a subset (for example, only errors or only warnings), filter the returned diagnostics by severity before presenting them. + +Use this workflow for requests phrased as checking a specific file, source file, module, or explicit `*.nim` path for Nim issues. + +### 8. Check the Current Nim Project for Diagnostics + +Do NOT approximate project diagnostics by grepping build logs or manually scanning files. + +1. Call `nimCheckProject()`. +2. Use the returned diagnostics to report errors, warnings, and hints for the current Nim project context. +3. If the user asked for only a subset (for example, only errors or only warnings), filter the returned diagnostics by severity before presenting them. + +Use this workflow for requests phrased as checking the current project, workspace, repository, repo, package, codebase, checkout, or module tree for Nim issues. + +### 9. Standardize Naming + +Use this when asked to fix casing, follow a style guide, or rename symbols to the standard `camelCase` (as per standard Nim convention). + +1. Iterate through the project files one by one. +2. For each file, call `nimListSymbols(path: "path/to/file.nim")` to retrieve the symbols that need to be standardized. +3. For each symbol found, call `nimFindReferences(path, line, column)` to identify all of its usages and call sites across the project. +4. Standardize the definition and all identified reference sites to `camelCase` to ensure consistency and adherence to Nim's standard style. +5. After a file has been processed, call `nimCheckFile(path: "path/to/file.nim")` to verify that the changes are correct and no errors were introduced. + +### 10. Fix Project Issues + +Use this when asked to "fix issues", "fix project", or "fix the repo". + +1. Call `nimCheckProject()` to find all diagnostics in the project. +2. Analyze the diagnostics and resolve the identified issues. +3. Repeat steps 1 and 2 until `nimCheckProject()` returns no more issues. +4. **Limit**: If issues remain after 3 iterations, you MUST stop and prompt the user, asking if they would like you to continue. +5. **Post-Fix Step**: Once all issues are resolved (or the user stops the process), ask the user if they would like you to check for naming consistency. +6. **Consistency Check**: If the user agrees, use `nimFindSymbols` and `nimFindReferences` to find all variations of symbol usage in the project. +7. **Standardization Prompt**: Prompt the user if they would like to standardize naming based on the findings. + +## Fallback Policy + +Specialized Nim MCP tools are the primary instrument. General-purpose tools (grep, ripgrep, shell commands) are secondary and their use is governed by the following rules: + +1. **On Error**: If an MCP tool fails due to a technical error (e.g., tool crash, timeout, connection issue), you MUST fall back to `grep_search` or other general-purpose tools to fulfill the request. + - State clearly that the MCP tool failed. + - Use the narrowest fallback necessary. +2. **On Empty Results**: If an MCP tool returns no results (empty list), do NOT automatically fall back to grep. Instead, you MUST prompt the user, asking if they would like you to attempt a plain text search using general-purpose tools. +3. **Availability**: If MCP tools are unavailable in the environment, use general-purpose tools but inform the user. + +When falling back: + +- Do not present the fallback results as equivalent in precision to MCP tool results. + +## Critical Constraints + +- **NO GREP BY DEFAULT**: Never use `grep`, `ripgrep`, or `grep_search` to find Nim symbols or references unless the MCP tools have explicitly errored out. +- **PROMPT ON EMPTY**: If MCP tools return nothing, you MUST ask the user before falling back to grep. +- **NO MANUAL PARSING**: Do not read large Nim files just to extract symbol locations; use `nimListSymbols` instead. +- **NO DIY FILE CHECKS**: Do not substitute manual inspection, ad-hoc compiler invocations, or `nimCheckProject` when `nimCheckFile` can return structured diagnostics for the specific file the user asked about. +- **NO DIY PROJECT CHECKS**: Do not substitute shelling out to ad-hoc Nim commands, parsing compiler output, or manually inspecting many files when `nimCheckProject` can return structured project diagnostics. +- **TOKEN CONSERVATION**: Minimize turns and context by using the most precise tool available. + +## Anti-Patterns + +- Calling `nimFindSymbols("Foo")` and then running `rg "Foo"` anyway. +- Using `rg` for "find all usages" when `nimFindReferences` is available. +- Reading multiple Nim files to manually enumerate definitions that `nimListSymbols` can return directly. +- Reading a Nim file to manually search for a type definition when `nimFindTypeDefinition` can resolve it precisely from the symbol's cursor position. +- Grepping for `type Foo* =` to find where a type is defined instead of using `nimFindTypeDefinition` or `nimFindSymbols`. +- Manually scanning imports and cross-referencing to determine which module a symbol comes from, when `nimFindTypeDefinition` can give the exact module path. +- Running `nimCheckProject()` when the user asked to check one specific Nim file and `nimCheckFile` is available. +- Manually reading or building a single Nim file first when `nimCheckFile` can return structured diagnostics for it. +- Running a manual project build or grep-based log scan first when the user asked for project/workspace/repository/package diagnostics and `nimCheckProject` is available. diff --git a/libplum/plum.nim b/libplum/plum.nim index ae6fbc0..bf446d3 100644 --- a/libplum/plum.nim +++ b/libplum/plum.nim @@ -206,7 +206,6 @@ proc createMapping*( user_ptr: cast[pointer](handle), ) - # Avoid issue with refc. # Pin the handle to prevent GC: the C library holds a raw pointer to # it (user_ptr) and might use it until DESTROYED fires. GC_ref(handle) @@ -234,7 +233,7 @@ proc createMapping*( discard plum_destroy_mapping(id) handle.releaseSignal() - # Reached only when completed = true (CancelledError skips this). + # Timeout path: the signal never fired within the deadline. if not completed: return err("plum: mapping " & $id & " timed out") From f3d74da7bc7ea19955004f50ee709c854fcba39e Mon Sep 17 00:00:00 2001 From: Arnaud Date: Mon, 1 Jun 2026 18:30:04 +0400 Subject: [PATCH 09/13] Add module-level raises pragma --- libplum/libplum.nim | 4 ++++ libplum/plum.nim | 20 ++++++++++++-------- 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/libplum/libplum.nim b/libplum/libplum.nim index 4ea5f8e..9c3457e 100644 --- a/libplum/libplum.nim +++ b/libplum/libplum.nim @@ -97,6 +97,8 @@ type # Import plum functions +{.push raises: [].} + proc plum_init*( config: ptr plum_config_t ): cint {.importc: "plum_init", header: "plum.h".} @@ -118,3 +120,5 @@ proc plum_destroy_mapping*( proc plum_get_local_address*( buffer: cstring, size: csize_t ): cint {.importc: "plum_get_local_address", header: "plum.h".} + +{.pop.} diff --git a/libplum/plum.nim b/libplum/plum.nim index bf446d3..72bf88f 100644 --- a/libplum/plum.nim +++ b/libplum/plum.nim @@ -28,6 +28,8 @@ export results {.pragma: callback, cdecl, raises: [], gcsafe.} +{.push raises: [].} + type PlumProtocol* = enum TCP = PLUM_IP_PROTOCOL_TCP.int @@ -97,7 +99,7 @@ var activeMappings {.guard: activeMappingsLock.}: Table[cint, MappingHandle] initLock(activeMappingsLock) -proc releaseSignal(handle: MappingHandle) {.raises: [].} = +proc releaseSignal(handle: MappingHandle) = if handle.signalReleases.fetchAdd(1) == 1: discard handle.signal.close() @@ -110,7 +112,7 @@ template withSafeLock(body: untyped) = proc mappingCallback( id: cint, state: plum_state_t, raw: ptr plum_mapping_t -) {.cdecl, raises: [].} = +) {.cdecl.} = ## Called from libplum's internal C thread on SUCCESS, FAILURE, and DESTROYED. foreignThreadGc: @@ -160,7 +162,7 @@ proc init*( discoverTimeout: int = 0, mappingTimeout: int = 0, recheckPeriod: int = 0, -): Result[void, string] {.raises: [].} = +): Result[void, string] = ## init MUST be called to setup internal plum thread (plum_init). var config = plum_config_t( @@ -178,7 +180,7 @@ proc init*( else: err("plum_init failed: " & $res) -proc cleanup*(): Result[void, string] {.raises: [].} = +proc cleanup*(): Result[void, string] = ## cleanup MUST be called to stop the thread and clean the setup. let res = plum_cleanup() @@ -252,7 +254,7 @@ proc createMapping*( discard plum_destroy_mapping(id) return err("plum: mapping " & $id & " failed") -proc destroyMapping*(id: cint) {.raises: [].} = +proc destroyMapping*(id: cint) = ## Must be called exactly once after a successful createMapping. ## Safe to call again or on an unknown id withSafeLock: @@ -260,7 +262,7 @@ proc destroyMapping*(id: cint) {.raises: [].} = return discard plum_destroy_mapping(id) -proc hasMapping*(id: cint): bool {.raises: [].} = +proc hasMapping*(id: cint): bool = ## Returns true if the mapping exists and is not being destroyed. var st: plum_state_t if plum_query_mapping(id, addr st, nil) == PLUM_ERR_SUCCESS: @@ -268,13 +270,13 @@ proc hasMapping*(id: cint): bool {.raises: [].} = else: false -proc activeMappingCount*(): int {.raises: [].} = +proc activeMappingCount*(): int = ## Number of mappings the wrapper still tracks. Drops to 0 once every ## mapping has fired DESTROYED. Mainly useful to detect handle leaks. withSafeLock: result = activeMappings.len -proc getLocalAddress*(): Result[string, string] {.raises: [].} = +proc getLocalAddress*(): Result[string, string] = var buf = newString(PLUM_MAX_ADDRESS_LEN) let res = plum_get_local_address(buf.cstring, buf.len.csize_t) if res >= 0: @@ -282,3 +284,5 @@ proc getLocalAddress*(): Result[string, string] {.raises: [].} = ok(buf) else: err("plum_get_local_address failed: " & $res) + +{.pop.} From e50f341ad8481da276e31e1d0bd9efeb4ea11d74 Mon Sep 17 00:00:00 2001 From: Arnaud Date: Mon, 1 Jun 2026 18:33:20 +0400 Subject: [PATCH 10/13] Use Nim types in init signature --- libplum/plum.nim | 19 ++++++++++++++----- tests/test_plum.nim | 10 +++++----- 2 files changed, 19 insertions(+), 10 deletions(-) diff --git a/libplum/plum.nim b/libplum/plum.nim index 72bf88f..8caf987 100644 --- a/libplum/plum.nim +++ b/libplum/plum.nim @@ -35,6 +35,15 @@ type TCP = PLUM_IP_PROTOCOL_TCP.int UDP = PLUM_IP_PROTOCOL_UDP.int + PlumLogLevel* {.pure.} = enum + Verbose = PLUM_LOG_LEVEL_VERBOSE.int + Debug = PLUM_LOG_LEVEL_DEBUG.int + Info = PLUM_LOG_LEVEL_INFO.int + Warn = PLUM_LOG_LEVEL_WARN.int + Error = PLUM_LOG_LEVEL_ERROR.int + Fatal = PLUM_LOG_LEVEL_FATAL.int + None = PLUM_LOG_LEVEL_NONE.int + PlumState* = enum Destroyed = PLUM_STATE_DESTROYED.int Pending = PLUM_STATE_PENDING.int @@ -158,15 +167,15 @@ proc mappingCallback( handle.onStateChange(plumState, mapping) proc init*( - logLevel: plum_log_level_t = PLUM_LOG_LEVEL_NONE, - discoverTimeout: int = 0, - mappingTimeout: int = 0, - recheckPeriod: int = 0, + logLevel: PlumLogLevel = PlumLogLevel.None, + discoverTimeout: int32 = 0, + mappingTimeout: int32 = 0, + recheckPeriod: int32 = 0, ): Result[void, string] = ## init MUST be called to setup internal plum thread (plum_init). var config = plum_config_t( - log_level: logLevel, + log_level: plum_log_level_t(logLevel.int), log_callback: nil, dummytls_domain: nil, discover_timeout: discoverTimeout.cint, diff --git a/tests/test_plum.nim b/tests/test_plum.nim index 7e7d2ae..ae3850b 100644 --- a/tests/test_plum.nim +++ b/tests/test_plum.nim @@ -73,7 +73,7 @@ when miniupnp_protocol != "": let mappingTimeout = seconds(40) let logLevel = - if getEnv("LIBPLUM_VERBOSE") == "1": PLUM_LOG_LEVEL_VERBOSE else: PLUM_LOG_LEVEL_NONE + if getEnv("LIBPLUM_VERBOSE") == "1": PlumLogLevel.Verbose else: PlumLogLevel.None var gRenewed: Atomic[bool] @@ -108,7 +108,7 @@ when miniupnp_protocol != "": suite "plum - " & miniupnp_protocol & " using miniupnp": test "createMapping TCP and destroyMapping": - require init(discoverTimeout = discoverMs, logLevel = logLevel).isOk() + require init(discoverTimeout = discoverMs.int32, logLevel = logLevel).isOk() defer: discard cleanup() @@ -135,7 +135,7 @@ when miniupnp_protocol != "": destroyMapping(res.id) test "createMapping UDP and destroying": - require init(discoverTimeout = discoverMs, logLevel = logLevel).isOk() + require init(discoverTimeout = discoverMs.int32, logLevel = logLevel).isOk() defer: discard cleanup() @@ -159,7 +159,7 @@ when miniupnp_protocol != "": test "mapping is renewed after miniupnpd restart": require init( - discoverTimeout = discoverMs, logLevel = logLevel, recheckPeriod = recheckMs + discoverTimeout = discoverMs.int32, logLevel = logLevel, recheckPeriod = recheckMs.int32 ) .isOk() defer: @@ -189,7 +189,7 @@ when miniupnp_protocol != "": check waitRenewal() test "cleanup releases active mappings": - require init(discoverTimeout = discoverMs, logLevel = logLevel).isOk() + require init(discoverTimeout = discoverMs.int32, logLevel = logLevel).isOk() let r = waitFor createMapping(TCP, 8401, timeout = mappingTimeout) require r.isOk() From 83d1866877484548536342cd421f22b39ed2ee19 Mon Sep 17 00:00:00 2001 From: Arnaud Date: Mon, 1 Jun 2026 18:38:47 +0400 Subject: [PATCH 11/13] Format --- libplum/plum.nim | 4 +--- tests/test_plum.nim | 4 +++- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/libplum/plum.nim b/libplum/plum.nim index 8caf987..3399724 100644 --- a/libplum/plum.nim +++ b/libplum/plum.nim @@ -119,9 +119,7 @@ template withSafeLock(body: untyped) = withLock activeMappingsLock: body -proc mappingCallback( - id: cint, state: plum_state_t, raw: ptr plum_mapping_t -) {.cdecl.} = +proc mappingCallback(id: cint, state: plum_state_t, raw: ptr plum_mapping_t) {.cdecl.} = ## Called from libplum's internal C thread on SUCCESS, FAILURE, and DESTROYED. foreignThreadGc: diff --git a/tests/test_plum.nim b/tests/test_plum.nim index ae3850b..02baabf 100644 --- a/tests/test_plum.nim +++ b/tests/test_plum.nim @@ -159,7 +159,9 @@ when miniupnp_protocol != "": test "mapping is renewed after miniupnpd restart": require init( - discoverTimeout = discoverMs.int32, logLevel = logLevel, recheckPeriod = recheckMs.int32 + discoverTimeout = discoverMs.int32, + logLevel = logLevel, + recheckPeriod = recheckMs.int32, ) .isOk() defer: From 801445add4a4095632fef462b52bbdc31021631b Mon Sep 17 00:00:00 2001 From: Arnaud Date: Mon, 1 Jun 2026 18:55:06 +0400 Subject: [PATCH 12/13] Test cleanup during a pending mapping --- tests/test_plum.nim | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/tests/test_plum.nim b/tests/test_plum.nim index 02baabf..430ee89 100644 --- a/tests/test_plum.nim +++ b/tests/test_plum.nim @@ -52,6 +52,19 @@ suite "plum": let r = waitFor createMapping(TCP, 8101, timeout = milliseconds(50)) check r.isErr() + test "cleanup while a createMapping is pending completes cleanly": + require init().isOk() + defer: + discard cleanup() + + # No NAT device answers, so the mapping stays PENDING until we cleanup. + let fut = createMapping(TCP, 8501, timeout = milliseconds(200)) + waitFor sleepAsync(50.milliseconds) + discard cleanup() + let r = waitFor fut + check r.isErr() + check activeMappingCount() == 0 + test "destroyMapping is a no-op on an unknown id": require init().isOk() defer: From 3d8ff9bb2e301fe3df9475391ebfe07d8a5bb30d Mon Sep 17 00:00:00 2001 From: Arnaud Date: Mon, 1 Jun 2026 18:56:54 +0400 Subject: [PATCH 13/13] Delete SKILL.md --- .claude/SKILL.md | 217 ----------------------------------------------- 1 file changed, 217 deletions(-) delete mode 100644 .claude/SKILL.md diff --git a/.claude/SKILL.md b/.claude/SKILL.md deleted file mode 100644 index f8d4133..0000000 --- a/.claude/SKILL.md +++ /dev/null @@ -1,217 +0,0 @@ ---- -name: nim-mcp-tools -description: 'Use for Nim symbol navigation, diagnostics, and type resolution. MANDATORY: Use specialized MCP tools (nimFindSymbols, nimFindReferences, nimListSymbols, nimCheckFile, nimCheckProject, nimFindTypeDefinition) first; fall back to grep only on error or user confirmation.' ---- - -# Nim MCP Tools - -## Core Mandate - -AI agents MUST prefer specialized MCP tools over general-purpose instruments (grep, shell commands, `read_file` with manual parsing) for all Nim symbol-related and Nim diagnostics tasks. This is critical for: - -1. **Token Efficiency**: MCP tools return structured, relevant data, avoiding large file reads or noisy grep outputs. -2. **Precision**: These tools understand Nim semantics (scopes, imports, overloads) which string-based search cannot. - -## Activation Rule - -If the user asks to find, rename, remove, audit, update, standardize naming, or fix issues/project of a Nim symbol or its usages, this skill MUST be used first and the workflow must start with the Nim MCP symbol tools. - -If the user asks to check a single Nim file for errors, warnings, hints, diagnostics, issues, problems, or compiler feedback, this skill MUST be used first and the workflow must start with `nimCheckFile`. - -If the user asks to check a Nim project for errors, warnings, hints, diagnostics, issues, problems, compiler feedback, or to "fix" it, this skill MUST be used first and the workflow must start with `nimCheckProject`. - -If the user asks to determine where a symbol comes from, what type it is, what its fields are (including private ones), or resolve a symbol's type definition, this skill MUST be used first and the workflow must start with `nimFindTypeDefinition`. - -This applies to requests phrased as: - -- "find all usages/references of `Foo`" -- "remove all definitions of and references to `Foo`" -- "rename `Foo` everywhere" -- "standardize naming for `Foo`" -- "fix casing for all variables" -- "where is `Foo` defined?" -- "list the symbols in this Nim file/module" -- "check this file/module for errors" -- "show diagnostics for `foo.nim`" -- "check this project/workspace/repository/package for errors" -- "fix issues in this project" -- "fix the repo" -- "find Nim diagnostics in the current codebase" -- "show warnings and hints for this repo" -- "scan the current module tree for Nim issues" -- "what is the type of this symbol?" -- "what module/package does this type come from?" -- "what fields does this type have?" -- "is this a type alias or a concrete type?" -- "where is this type defined?" - -Treat user wording such as **project**, **workspace**, **repository**, **repo**, **package**, **codebase**, **checkout**, and **module tree** as referring to the current Nim project context when they are asking for project-wide diagnostics. - -Treat user wording such as **file**, **module** (when a concrete Nim file is identified), **source file**, and explicit `*.nim` paths as referring to single-file diagnostics when they are asking for diagnostics for one file. - -Do **not** pair `nimFindSymbols`, `nimCheckFile`, or `nimCheckProject` with `grep`, `ripgrep`, or shell search "just to double-check". If the task is about a Nim symbol or Nim diagnostics, MCP tools own the search unless they have already failed. - -## User Terminology vs MCP `kind` - -Users may ask for symbol categories using looser or non-strict terminology. AI agents MUST map that wording to the exact Nim MCP `kind` values before filtering results from `nimListSymbols(...)` or `nimFindSymbols(...)`. - -The MCP server returns Nim-oriented kind names derived from nimsuggest symbol kinds with the leading `sk` removed, such as `Const`, `EnumField`, `Field`, `Iterator`, `Converter`, `Let`, `Macro`, `Method`, `Proc`, `Template`, `Type`, `Var`, and `Func`. - -Use these terminology mappings when interpreting user requests: - -- **function** / **functions**: usually match `Func` and `Proc` -- **pure function** / **pure functions**: match `Func` -- **callable** / **routine**: may include `Func`, `Proc`, `Method`, `Iterator`, `Converter`, `Macro`, and `Template` -- **class** / **classes**: match `Type` -- **variable** / **variables**: match `Var` and `Let` -- **property** / **properties**: match `Field` -- **enum member** / **enum members**: match `EnumField` -- **constant** / **constants**: match `Const` - -Interpret user wording semantically, not literally. For example: - -- "list all classes in this module" -> filter for `kind == Type` -- "list all variables" -> filter for `kind in {Var, Let}` -- "list all functions" -> filter for `kind in {Func, Proc}` unless the surrounding context clearly asks for all callables -- "list all pure functions" -> filter for `kind == Func` - -## When to Use - -- **Finding References**: To rename a symbol, update a signature, or find usages. -- **Symbol Discovery**: To find where a symbol is defined by name. -- **Type Resolution**: To determine where a local symbol comes from (which module), what its type is, and what fields it has (including private ones). Uses `nimFindTypeDefinition(path, line, column)` on the symbol's cursor position. -- **Naming Standardization**: To fix casing or follow style guides (e.g., standardizing to `camelCase`) across the project. -- **Fixing Project Issues**: To iteratively find and resolve all diagnostics in the project. -- **File Analysis**: To get an overview of all symbols in a file. -- **File Diagnostics**: To check one specific Nim file for errors, warnings, and hints. -- **Project Diagnostics**: To check the current Nim project/workspace/repository/package for errors, warnings, and hints. -- **Debugging Type Mismatches**: When a diagnostic reveals a type mismatch, use `nimFindTypeDefinition` on both sides to understand what types are expected vs provided. -- **Code Generation / Refactoring**: Before generating code that interacts with a type, find its definition to understand its structure and field layout. - -## Workflows - -### 1. Find All References or Usages of a Symbol Name - -Do NOT grep for the name. - -1. Call `nimFindSymbols(query: "SymbolName")` to get exact `path`, `line`, and `column`. -2. For each relevant result, call `nimFindReferences(path, line, column)`. -3. Aggregate the results. - -Use this workflow for "usages", "references", "call sites", and cleanup requests such as removing all definitions of and references to a symbol. - -### 2. List All Symbols in a File - -Do NOT read the whole file to find definitions. - -1. Call `nimListSymbols(path: "path/to/file.nim")`. -2. If the user asked for a symbol category, filter by the MCP `kind` values that correspond to the user's terminology. -3. Use the returned list to navigate or analyze the file structure. - -### 3. Find Definitions - -1. Call `nimFindSymbols(query: "query")`. - -### 4. Resolve Type / Determine Origin of a Symbol - -Use this when asked where a symbol comes from, what its type is, or to inspect its definition (fields, kind, etc.). - -1. Call `nimFindTypeDefinition(path, line, column)` with the cursor positioned on the symbol of interest. -2. The result contains the definition `path`, `line`, `column`, `name`, `type`, and `kind`. -3. If the user needs to see the full definition (e.g., object fields), read the source at the returned path/line. -4. Use this for requests like "what type is this variable?", "what module does this type come from?", "what fields does this object have?". - -### 5. Debug Type Mismatch (Check + Type Definition) - -Use this when fixing a diagnostic that involves a type mismatch. - -1. Call `nimCheckFile(path)` to get the diagnostic with the exact error location. -2. For the reported location, call `nimFindTypeDefinition(path, line, column)` on the involved symbols to understand what types are expected vs provided. -3. Resolve the mismatch with the correct type or conversion. - -### 6. Explore Object Structure (List Symbols + Type Definition) - -Use this when asked to describe or document a type's shape. - -1. Call `nimFindSymbols(query: "TypeName")` to locate the type definition. -2. Call `nimFindTypeDefinition(path, line, column)` on the type name to confirm the definition location. -3. Read the source at the definition location to enumerate all fields (including private ones) and their types. -4. Optionally, call `nimFindTypeDefinition` on each field type to recursively resolve nested types. - -### 7. Check a Single Nim File for Diagnostics - -Do NOT approximate file diagnostics by reading the file manually or by running a project-wide diagnostic request when the user asked about one file. - -1. Call `nimCheckFile(path: "path/to/file.nim")`. -2. Treat the result as the answer unless the user explicitly asked for broader validation such as project-wide diagnostics, tests, or builds. -3. Use the returned diagnostics to report errors, warnings, and hints for that file. -4. If the user asked for only a subset (for example, only errors or only warnings), filter the returned diagnostics by severity before presenting them. - -Use this workflow for requests phrased as checking a specific file, source file, module, or explicit `*.nim` path for Nim issues. - -### 8. Check the Current Nim Project for Diagnostics - -Do NOT approximate project diagnostics by grepping build logs or manually scanning files. - -1. Call `nimCheckProject()`. -2. Use the returned diagnostics to report errors, warnings, and hints for the current Nim project context. -3. If the user asked for only a subset (for example, only errors or only warnings), filter the returned diagnostics by severity before presenting them. - -Use this workflow for requests phrased as checking the current project, workspace, repository, repo, package, codebase, checkout, or module tree for Nim issues. - -### 9. Standardize Naming - -Use this when asked to fix casing, follow a style guide, or rename symbols to the standard `camelCase` (as per standard Nim convention). - -1. Iterate through the project files one by one. -2. For each file, call `nimListSymbols(path: "path/to/file.nim")` to retrieve the symbols that need to be standardized. -3. For each symbol found, call `nimFindReferences(path, line, column)` to identify all of its usages and call sites across the project. -4. Standardize the definition and all identified reference sites to `camelCase` to ensure consistency and adherence to Nim's standard style. -5. After a file has been processed, call `nimCheckFile(path: "path/to/file.nim")` to verify that the changes are correct and no errors were introduced. - -### 10. Fix Project Issues - -Use this when asked to "fix issues", "fix project", or "fix the repo". - -1. Call `nimCheckProject()` to find all diagnostics in the project. -2. Analyze the diagnostics and resolve the identified issues. -3. Repeat steps 1 and 2 until `nimCheckProject()` returns no more issues. -4. **Limit**: If issues remain after 3 iterations, you MUST stop and prompt the user, asking if they would like you to continue. -5. **Post-Fix Step**: Once all issues are resolved (or the user stops the process), ask the user if they would like you to check for naming consistency. -6. **Consistency Check**: If the user agrees, use `nimFindSymbols` and `nimFindReferences` to find all variations of symbol usage in the project. -7. **Standardization Prompt**: Prompt the user if they would like to standardize naming based on the findings. - -## Fallback Policy - -Specialized Nim MCP tools are the primary instrument. General-purpose tools (grep, ripgrep, shell commands) are secondary and their use is governed by the following rules: - -1. **On Error**: If an MCP tool fails due to a technical error (e.g., tool crash, timeout, connection issue), you MUST fall back to `grep_search` or other general-purpose tools to fulfill the request. - - State clearly that the MCP tool failed. - - Use the narrowest fallback necessary. -2. **On Empty Results**: If an MCP tool returns no results (empty list), do NOT automatically fall back to grep. Instead, you MUST prompt the user, asking if they would like you to attempt a plain text search using general-purpose tools. -3. **Availability**: If MCP tools are unavailable in the environment, use general-purpose tools but inform the user. - -When falling back: - -- Do not present the fallback results as equivalent in precision to MCP tool results. - -## Critical Constraints - -- **NO GREP BY DEFAULT**: Never use `grep`, `ripgrep`, or `grep_search` to find Nim symbols or references unless the MCP tools have explicitly errored out. -- **PROMPT ON EMPTY**: If MCP tools return nothing, you MUST ask the user before falling back to grep. -- **NO MANUAL PARSING**: Do not read large Nim files just to extract symbol locations; use `nimListSymbols` instead. -- **NO DIY FILE CHECKS**: Do not substitute manual inspection, ad-hoc compiler invocations, or `nimCheckProject` when `nimCheckFile` can return structured diagnostics for the specific file the user asked about. -- **NO DIY PROJECT CHECKS**: Do not substitute shelling out to ad-hoc Nim commands, parsing compiler output, or manually inspecting many files when `nimCheckProject` can return structured project diagnostics. -- **TOKEN CONSERVATION**: Minimize turns and context by using the most precise tool available. - -## Anti-Patterns - -- Calling `nimFindSymbols("Foo")` and then running `rg "Foo"` anyway. -- Using `rg` for "find all usages" when `nimFindReferences` is available. -- Reading multiple Nim files to manually enumerate definitions that `nimListSymbols` can return directly. -- Reading a Nim file to manually search for a type definition when `nimFindTypeDefinition` can resolve it precisely from the symbol's cursor position. -- Grepping for `type Foo* =` to find where a type is defined instead of using `nimFindTypeDefinition` or `nimFindSymbols`. -- Manually scanning imports and cross-referencing to determine which module a symbol comes from, when `nimFindTypeDefinition` can give the exact module path. -- Running `nimCheckProject()` when the user asked to check one specific Nim file and `nimCheckFile` is available. -- Manually reading or building a single Nim file first when `nimCheckFile` can return structured diagnostics for it. -- Running a manual project build or grep-based log scan first when the user asked for project/workspace/repository/package diagnostics and `nimCheckProject` is available.