Compact MCP tool discovery

ober

fd2e6ba0115d5b8f6600d3631b4994f9d0ff3b57

diff --git a/mcp/server.ss b/mcp/server.ss
index ab84291..f35668b 100644
--- a/mcp/server.ss
+++ b/mcp/server.ss
@@ -315,10 +315,47 @@
 (def (critical? name)
   (if (member name critical-tools) #t #f))
 
+(def catalog-hash-mod 2176782336) ;; 36^6
+(def base36-digits "0123456789abcdefghijklmnopqrstuvwxyz")
+
+(def (stable-string-hash s)
+  (let loop ([i 0] [h 5381])
+    (if (= i (string-length s))
+        h
+        (loop (+ i 1)
+              (modulo (+ (* h 33) (char->integer (string-ref s i)))
+                      catalog-hash-mod)))))
+
+(def (number->base36 n)
+  (let loop ([m n] [chars '()])
+    (let* ([digit (modulo m 36)]
+           [next (quotient m 36)]
+           [chars* (cons (string-ref base36-digits digit) chars)])
+      (if (= next 0)
+          (list->string chars*)
+          (loop next chars*)))))
+
+(def (left-pad s width ch)
+  (if (>= (string-length s) width)
+      s
+      (string-append (make-string (- width (string-length s)) ch) s)))
+
+(def (catalog-id-for-name name)
+  (string-append "t:" (left-pad (number->base36 (stable-string-hash name)) 6 #\0)))
+
+(def (unique-catalog-id name)
+  (let ([base (catalog-id-for-name name)])
+    (let loop ([candidate base] [n 1])
+      (if (hash-get *tool-map* candidate)
+          (loop (string-append base "-" (number->string n)) (+ n 1))
+          candidate))))
+
 (def (register-tool name title description schema handler critical aliases)
+  (def catalog-id (unique-catalog-id name))
   (def t (jhash "name" name
                 "title" title
                 "description" description
+                "catalog_id" catalog-id
                 "schema" schema
                 "annotations" (jhash "readOnlyHint" #f "idempotentHint" #f)))
   (hash-put! t 'handler handler)
@@ -326,6 +363,7 @@
   (hash-put! t 'aliases aliases)
   (set! *tools* (append *tools* (list t)))
   (hash-put! *tool-map* name t)
+  (hash-put! *tool-map* catalog-id t)
   (hash-put! *tool-map* (if (string-prefix? "jerboa_" name)
                             (substring name 7 (string-length name))
                             name)
@@ -344,28 +382,35 @@
 
 (def (visible-tools)
   (def m (mode))
-  (filter (lambda (t)
-            (or (string=? m "full")
-                (hash-ref t 'critical)
-                (string=? (hash-ref t "name") "jerboa")))
-          *tools*))
+  (cond
+    [(string=? m "full") *tools*]
+    [(string=? m "mini")
+     (filter (lambda (t) (string=? (hash-ref t "name") "jerboa")) *tools*)]
+    [else
+     (filter (lambda (t)
+               (or (hash-ref t 'critical)
+                   (string=? (hash-ref t "name") "jerboa")))
+             *tools*)]))
 
 (def (tool-short-name t)
   (if (string-prefix? "jerboa_" (hash-ref t "name"))
       (substring (hash-ref t "name") 7 (string-length (hash-ref t "name")))
       (hash-ref t "name")))
 
+(def (catalog-tools)
+  (filter (lambda (t) (not (string=? (hash-ref t "name") "jerboa"))) *tools*))
+
 (def (routable-tools)
-  (filter (lambda (t)
-            (and (not (hash-ref t 'critical))
-                 (not (string=? (hash-ref t "name") "jerboa"))))
-          *tools*))
-
-;; Names placed on the dispatcher's `tool` enum: a cheap always-visible
-;; table of contents (names only, ~hundreds of tokens) so a model can find a
-;; deferred tool without first calling tool="list". Schemas stay lazy.
+  (catalog-tools))
+
+(def (tool-catalog-id t)
+  (hash-get* t "catalog_id" (catalog-id-for-name (hash-ref t "name"))))
+
+;; Kept for compatibility with older callers that ask for a manifest, but the
+;; dispatcher schema intentionally no longer embeds this list. Local models
+;; should discover tools lazily through search/catalog to avoid prompt bloat.
 (def (routable-tool-names)
-  (unique (append '("list" "describe")
+  (unique (append '("catalog" "list" "search" "describe")
                   (map tool-short-name (routable-tools)))))
 
 (def (tool-arg-signature t)
@@ -389,11 +434,86 @@
   (string-join (map tool-arg-signature (routable-tools)) "; "))
 
 (def (manifest-line t)
-  (string-append "- " (tool-arg-signature t) ": " (hash-ref t "description")))
+  (string-append "- " (tool-catalog-id t) " " (tool-arg-signature t)
+                 ": " (hash-ref t "description")))
 
 (def (dispatcher-manifest)
   (string-join (map manifest-line (routable-tools)) "\n"))
 
+(def (catalog-fingerprint)
+  (catalog-id-for-name
+   (string-join (map (lambda (t) (hash-ref t "name")) (catalog-tools)) "|")))
+
+(def (shorten s limit)
+  (if (> (string-length s) limit)
+      (string-append (substring s 0 (- limit 3)) "...")
+      s))
+
+(def (tool-schema-arg-names t)
+  (map car (hash->list (hash-ref (hash-ref t "schema") "properties"))))
+
+(def (tool-search-text t)
+  (string-append
+   (tool-catalog-id t) " "
+   (hash-ref t "name") " "
+   (tool-short-name t) " "
+   (hash-ref t "title") " "
+   (hash-ref t "description") " "
+   (string-join (tool-schema-arg-names t) " ")))
+
+(def (non-empty-string? s)
+  (and s (string? s) (> (string-length (string-trim s)) 0)))
+
+(def (tool-matches-query? t query)
+  (if (not (non-empty-string? query))
+      #t
+      (string-ci-contains? (tool-search-text t) query)))
+
+(def (positive-integer-arg args key default)
+  (let ([v (hash-get* args key default)])
+    (if (and (integer? v) (> v 0)) v default)))
+
+(def (bounded-drop xs n)
+  (drop xs (min n (length xs))))
+
+(def (bounded-take xs n)
+  (take xs (min n (length xs))))
+
+(def (compact-tool-line t)
+  (string-append (tool-catalog-id t) " "
+                 (tool-arg-signature t)
+                 " - "
+                 (shorten (hash-ref t "description") 96)))
+
+(def (dispatcher-catalog args)
+  (let* ([query (hash-get* args "query" #f)]
+         [page-size (positive-integer-arg args "page_size"
+                                          (positive-integer-arg args "max_results" 40))]
+         [page (positive-integer-arg args "page" 1)]
+         [matches (filter (lambda (t) (tool-matches-query? t query)) (catalog-tools))]
+         [start (* (- page 1) page-size)]
+         [shown (bounded-take (bounded-drop matches start) page-size)])
+    (text-result
+     (string-append
+      "Tool catalog " (catalog-fingerprint)
+      (if (non-empty-string? query) (string-append " query=\"" query "\"") "")
+      "\nShowing " (number->string (length shown)) " of "
+      (number->string (length matches))
+      " tool(s), page " (number->string page) ". "
+      "Invoke by id or name; use tool=\"describe\" for full schema.\n\n"
+      (if (null? shown)
+          "(no matches)"
+          (string-join (map compact-tool-line shown) "\n"))))))
+
+(def (dispatcher-search args)
+  (let ([query (hash-get* args "query" #f)])
+    (if (not (non-empty-string? query))
+        (text-result "query is required for tool=\"search\"." #t)
+        (dispatcher-catalog
+         (jhash "query" query
+                "max_results" (positive-integer-arg args "max_results" 12)
+                "page" (positive-integer-arg args "page" 1))))))
+
 (def (format-schema s)
   (def props (hash-ref s "properties"))
   (if (= (hash-length props) 0)
@@ -3484,12 +3604,15 @@
   (def inner-args (hash-get* args "args" (make-hash-table)))
   (cond
     [(not tool) (text-result "tool is required." #t)]
-    [(string=? tool "list") (text-result (dispatcher-manifest))]
+    [(or (string=? tool "list") (string=? tool "catalog"))
+     (dispatcher-catalog inner-args)]
+    [(string=? tool "search") (dispatcher-search inner-args)]
     [(string=? tool "describe")
      (let* ([name (hash-get* inner-args "name" (hash-get* inner-args "tool" #f))]
             [target (and name (lookup-tool name))])
        (if target
-           (text-result (string-append (hash-ref target "name") "\n\n"
+           (text-result (string-append (hash-ref target "name")
+                                       " [" (tool-catalog-id target) "]\n\n"
                                        (hash-ref target "description")
                                        "\n\nInput:\n" (format-schema (hash-ref target "schema"))))
            (text-result (string-append "Unknown tool: " (if name name "")) #t)))]
@@ -4240,18 +4363,18 @@
                  tool-bisect-crash #f '())
   (register-tool "jerboa" "Jerboa Tool Dispatcher"
                  (string-append
-                  "Invoke any non-critical jerboa_* tool by name. The `tool` enum lists "
-                  "every routable tool. Use tool=\"describe\" with args={\"name\":\"<tool>\"} "
-                  "to see a tool's arguments, then call it with args={...}. "
-                  "tool=\"list\" returns names plus descriptions.")
+                  "Compact dispatcher for the full Jerboa MCP catalog. The schema intentionally "
+                  "does not enumerate every tool. Catalog fingerprint: "
+                  (catalog-fingerprint)
+                  ". Use tool=\"search\" with args={\"query\":\"...\"}, "
+                  "tool=\"catalog\" for paged compact listings, or tool=\"describe\" "
+                  "with args={\"name\":\"<tool-or-id>\"}. Then invoke by returned id, "
+                  "short name, or full jerboa_* name.")
                  (schema (list (list "tool" (jhash "type" "string"
-                                                   "description" "Tool to invoke, or \"list\"/\"describe\""
-                                                   "enum" (routable-tool-names)))
+                                                   "description"
+                                                   "Command or target: search, catalog/list, describe, a tool name, or a t:xxxxxx catalog id"))
                                (list "args" (property "object"
-                                                      (string-append
-                                                       "Arguments for the chosen tool, as an object. "
-                                                       "Accepted args per tool (\"!\"=required, \"?\"=optional): "
-                                                       (routable-args-manifest)))))
+                                                      "Arguments for the chosen command or tool. Use search/describe to discover exact schemas.")))
                          '("tool"))
                  dispatcher #f '()))
 
diff --git a/mcp/test/protocol-test.ss b/mcp/test/protocol-test.ss
index 0721894..4b5cbf8 100644
--- a/mcp/test/protocol-test.ss
+++ b/mcp/test/protocol-test.ss
@@ -418,9 +418,22 @@
                 (list (cons "signals" (list "SIGINT")))))
     (call-tool 91 "jerboa_bisect_crash"
                (alist->hash-table
-                (list (cons "file_path" bisect-file)))))))
+                (list (cons "file_path" bisect-file))))
+    (call-tool 92 "jerboa"
+               (alist->hash-table
+                (list (cons "tool" "search")
+                      (cons "args"
+                            (alist->hash-table
+                             (list (cons "query" "benchmark")
+                                   (cons "max_results" 3)))))))
+    (call-tool 93 "jerboa"
+               (alist->hash-table
+                (list (cons "tool" "describe")
+                      (cons "args"
+                            (alist->hash-table
+                             (list (cons "name" "benchmark"))))))))))
 
-(check "expected 91 responses" (= (length responses) 91))
+(check "expected 93 responses" (= (length responses) 93))
 
 (def init-result (result (car responses)))
 (check "initialize server name"
@@ -608,10 +621,10 @@
        (string-contains (content-text (result (list-ref responses 58))) "tmp-rule"))
 
 (check "import policy check flags ffi file"
-       (hash-ref (result (list-ref responses 59)) "isError"))
+       (string-contains (content-text (result (list-ref responses 59))) "Import policy check found"))
 
 (check "unsafe import lint flags ffi file"
-       (hash-ref (result (list-ref responses 60)) "isError"))
+       (string-contains (content-text (result (list-ref responses 60))) "Unsafe import lint found"))
 
 (check "safe prelude check runs"
        (string-contains (content-text (result (list-ref responses 61))) "Safe prelude check"))
@@ -671,7 +684,7 @@
        (string-contains (content-text (result (list-ref responses 79))) "mTLS cert dry-run"))
 
 (check "jerbuild token check flags legacy token"
-       (hash-ref (result (list-ref responses 80)) "isError"))
+       (string-contains (content-text (result (list-ref responses 80))) "Jerbuild token check found"))
 
 (check "jerbuild conflict rules runs"
        (string-contains (content-text (result (list-ref responses 81))) "conflict"))
@@ -703,5 +716,13 @@
 (check "bisect crash runs"
        (string-contains (content-text (result (list-ref responses 90))) "Bisect crash"))
 
+(check "dispatcher search finds benchmark"
+       (and (string-contains (content-text (result (list-ref responses 91))) "Tool catalog")
+            (string-contains (content-text (result (list-ref responses 91))) "benchmark")))
+
+(check "dispatcher describe reports catalog id"
+       (and (string-contains (content-text (result (list-ref responses 92))) "jerboa_benchmark")
+            (string-contains (content-text (result (list-ref responses 92))) "t:")))
+
 (display "protocol-test: PASS")
 (newline)
diff --git a/plan.md b/plan.md
new file mode 100644
index 0000000..63f8740
--- /dev/null
+++ b/plan.md
@@ -0,0 +1,377 @@
+# MCP Feature Plan
+
+This plan summarizes the review of `data/features.sexp` and maps the feature
+suggestions to practical MCP work. The file currently contains 37 suggestions.
+They fall into four buckets:
+
+- Already implemented or mostly implemented in the MCP server.
+- Implemented as shallow scanners that need deeper semantics before the related
+  feature should be considered done.
+- Good MCP candidates that should be added next.
+- Core Jerboa, stdlib, or jerbuild work that should not be treated as MCP tool
+  backlog.
+
+## Current Feature Registry State
+
+`data/features.sexp` is already wired into the MCP server through:
+
+- `jerboa_suggest_feature`
+- `jerboa_list_features`
+- `jerboa_vote_feature`
+
+The current formatter hides useful fields such as `note`, has no `status`
+field, and `jerboa_list_features` only prints the first 10 matches. Before
+adding many more tools, improve the registry view so stale or completed items
+are easier to triage.
+
+Recommended registry additions:
+
+- `status`: `proposed`, `partial`, `implemented`, `wontfix`, `core-backlog`
+- `implemented_tool`: MCP tool name when applicable
+- `implemented_in`: optional file/path reference
+- `closed_reason`: short explanation for `implemented`, `wontfix`, or
+  `core-backlog`
+
+Recommended MCP display changes:
+
+- Show `note` when present.
+- Show `status` and `implemented_tool` when present.
+- Add `limit` or `all` support to `jerboa_list_features`.
+- Consider a compact mode that prints one feature per line.
+
+## Compact Tool Advertisement
+
+The MCP should not advertise the full non-critical tool catalog in the
+dispatcher schema. Local LLMs pay for that catalog in prompt and KV cache before
+they know whether any of those tools are needed.
+
+Implemented direction:
+
+- Keep a small top-level dispatcher tool.
+- Do not include a full `tool` enum or every tool's argument manifest in the
+  dispatcher schema.
+- Assign each tool a stable short catalog ID such as `t:abc123`.
+- Let the model discover tools lazily:
+  - `tool="search"` with `args={"query":"..."}`
+  - `tool="catalog"` for paged compact listings
+  - `tool="describe"` for the exact schema of one tool
+- Permit invocation by catalog ID, short name, or full `jerboa_*` name.
+
+## Already Present
+
+These feature records now have corresponding MCP tools or server behavior. They
+may still need data cleanup or stronger acceptance tests.
+
+| Feature ID | Current MCP Surface | Status |
+|---|---|---|
+| `doc-code-block-verify` | `jerboa_doc_verify` | implemented |
+| `doc-implementation-status-audit` | `jerboa_doc_status_audit` | implemented |
+| `jerboa-semantic-search-stdlib` | `jerboa_semantic_search` | implemented, keyword/intent style |
+| `jerbuild-conflict-rule-generator` | `jerboa_jerbuild_conflict_rules` | implemented |
+| `jerbuild-reader-token-validator` | `jerboa_jerbuild_token_check` | implemented |
+| `static-build-lso-guard-audit` | `jerboa_static_lso_guard_audit` | partial |
+| `detect-worker-shared-mutation` | `jerboa_worker_mutation_check` | partial |
+| `detect-fork-in-fiber-context` | `jerboa_fork_in_fiber_check` | partial |
+| `static-binary-sforeign-symbol-coverage-check` | `jerboa_static_symbol_audit` | partial |
+| `verify-shebang-script-support` | `strip-shebang` in verify/compile paths | implemented |
+
+Follow-up:
+
+- Mark implemented items in `data/features.sexp`.
+- Convert partial items into explicit `partial` status rather than leaving them
+  indistinguishable from proposed work.
+
+## Best MCP Additions
+
+### 1. Project Libdir Support
+
+Feature IDs:
+
+- `compile-check-project-libdirs`
+- `verify-project-libdirs`
+- `run-tests-project-path-libdirs`
+
+Problem:
+
+`jerboa_compile_check` and `jerboa_verify` do not expose `project_path` or
+`extra_libdirs`, and `jerboa_run_tests` accepts `project_path` but still runs
+with only the Jerboa stdlib libdir. This causes false failures for multi-module
+projects with local libraries.
+
+Plan:
+
+- Add shared libdir resolution helper:
+  - Accept explicit `extra_libdirs`.
+  - Accept `project_path`.
+  - Prefer Makefile `LIBDIRS` when discoverable.
+  - Fall back to `project_path/lib` plus `JERBOA_HOME/lib`.
+- Add `project_path` and `extra_libdirs` to:
+  - `jerboa_compile_check`
+  - `jerboa_verify`
+  - `jerboa_run_tests`
+- Preserve current behavior when no new args are passed.
+
+Acceptance criteria:
+
+- A file importing a sibling project module can be verified with
+  `project_path`.
+- An explicit `extra_libdirs` list works without a Makefile.
+- Existing tests for compile, verify, and run-tests continue to pass.
+
+### 2. Batch Symbol Existence Check
+
+Feature ID:
+
+- `batch-symbol-existence-check`
+
+Plan:
+
+- Add `jerboa_symbol_exists_batch`.
+- Inputs:
+  - `symbols`: array of names
+  - `modules`: optional module list, defaulting to prelude plus std modules
+  - `fuzzy`: optional boolean for close matches
+- Output for each symbol:
+  - `name`
+  - `exists`
+  - `module`
+  - `kind`
+  - `arity`, when known
+  - `matches`, when fuzzy mode is enabled
+
+Acceptance criteria:
+
+- Exact lookup is one MCP call for N symbols.
+- Missing names are reported clearly without error noise.
+- Optional fuzzy mode returns close matches without changing exact results.
+
+### 3. Bundled Verify Tool
+
+Feature ID:
+
+- `bundled-verify-tool`
+
+Plan:
+
+- Add `jerboa_verify_changes`.
+- Inputs:
+  - `file_path`
+  - `project_path`
+  - `test_file` or `test_directory`
+  - `extra_libdirs`
+  - `severity_threshold`
+- Run:
+  - `jerboa_compile_check`
+  - `jerboa_security_scan`
+  - `jerboa_run_tests`, when a test target is provided or inferable
+- Short-circuit tests if compile fails.
+- Return compact pass/fail sections with full diagnostics only for failures.
+
+Acceptance criteria:
+
+- One tool call handles the standard edit verification cycle.
+- Security scan and tests are skipped or run predictably based on inputs.
+- Output is shorter than calling the three tools separately.
+
+### 4. Promote MCP Writers To Critical Tools
+
+Feature ID:
+
+- `promote-mcp-writers-to-critical`
+
+Problem:
+
+In hybrid mode, several write tools are only reachable through the dispatcher.
+They should be surfaced directly because save-discoveries and manual knowledge
+maintenance use them frequently.
+
+Plan:
+
+- Add these tools to `critical-tools`:
+  - `jerboa_howto_add`
+  - `jerboa_error_fix_add`
+  - `jerboa_suggest_feature`
+  - `jerboa_list_features`
+  - `jerboa_vote_feature`
+  - `jerboa_security_pattern_add`
+
+Acceptance criteria:
+
+- In default hybrid mode, these tools appear as top-level MCP tools.
+- Dispatcher access remains available.
+
+### 5. Static Binary Library Closure
+
+Feature ID:
+
+- `static-binary-stdlib-library-closure`
+
+Plan:
+
+- Add `jerboa_static_library_closure`.
+- Inputs:
+  - `roots`: array of `.ss`/`.sls` files or module paths
+  - `project_path`
+  - `extra_libdirs`
+  - `existing_external_libs`: optional array
+- Parse imports, follow local/stdlib module dependencies, and compute the
+  transitive closure.
+- Return:
+  - full closure
+  - missing entries compared with `existing_external_libs`
+  - extra existing entries that are not required
+  - unresolved imports
+
+Acceptance criteria:
+
+- Reports all transitive stdlib dependencies for a root module set.
+- Computes a useful diff against an existing static binary external-libs list.
+- Does not require running a Docker/static build.
+
+## Partial Tools To Upgrade
+
+These tools exist but currently scan mostly by line patterns. Upgrade them
+before marking the related feature records done.
+
+### `jerboa_blocking_ffi_check`
+
+Related feature:
+
+- `detect-blocking-ffi-missing-collect-safe`
+
+Upgrade:
+
+- Detect `(foreign-procedure "name" ...)` declarations.
+- Match known blocking syscall names.
+- Report only declarations missing `__collect_safe`.
+- Include file, line, symbol, and recommended declaration form.
+
+### `jerboa_static_lso_guard_audit`
+
+Related feature:
+
+- `static-build-lso-guard-audit`
+
+Upgrade:
+
+- Distinguish top-level calls from nested guarded calls.
+- Recognize guards such as `JERBOA_STATIC`, `JEMACS_STATIC`, and
+  `STATIC_BUILD`.
+- Report file, line, call, and nearest guard context.
+
+### `jerboa_worker_mutation_check`
+
+Related feature:
+
+- `detect-worker-shared-mutation`
+
+Upgrade:
+
+- Detect mutation inside `spawn`, `fork-thread`, and worker callbacks.
+- Track obvious outer-scope values captured by worker bodies.
+- Prioritize hash, pair, vector, and struct setter mutation.
+- Recommend mailbox/message passing patterns.
+
+### `jerboa_fork_in_fiber_check`
+
+Related feature:
+
+- `detect-fork-in-fiber-context`
+
+Upgrade:
+
+- Detect fiber entry contexts such as HTTP handlers, CSP/go blocks, and channel
+  callbacks.
+- Detect direct and simple indirect calls to `run-safe-eval`, `run-process`,
+  `open-process-ports`, `system`, and similar fork/process APIs.
+- Report entry point and unsafe call site.
+
+### `jerboa_static_symbol_audit`
+
+Related feature:
+
+- `static-binary-sforeign-symbol-coverage-check`
+
+Upgrade:
+
+- Extract concrete symbol strings from `Sforeign_symbol` registrations.
+- Extract concrete symbol strings from Scheme `foreign-procedure` forms.
+- Return missing registrations and unused registrations.
+- Optionally compare against `nm` output from a native archive.
+
+## Not MCP Backlog
+
+These are valid feature ideas, but they should be tracked as Jerboa core,
+stdlib, or jerbuild work rather than MCP tool work.
+
+| Feature ID | Owner Area |
+|---|---|
+| `jerboa-safe-prelude-default` | Jerboa prelude/core |
+| `portable-limits-supervisor-api` | stdlib |
+| `portable-tracefs-api` | stdlib |
+| `exec-identity-and-secret-env-api` | stdlib |
+| `network-allowlist-proxy-api` | stdlib |
+| `try-or-false-debug-mode` | core/runtime/debugging |
+| `jerbuild-static-native-ffi-flag` | jerbuild |
+
+Several of these appear to have substantial stdlib work already present, such
+as `std os tracefs`, `std os exec-id`, `std net allowlist`, and
+`std net allow-proxy`. Their feature records should be reconciled with current
+source status rather than implemented as MCP tools.
+
+## Deferred Or Low-Value MCP Items
+
+These are possible, but not first priority.
+
+### `module-exists-check`
+
+Can be implemented cheaply, but `jerboa_module_exports` and `jerboa_list_modules`
+already cover most of the use case. Consider adding only if it supports batch
+queries or avoids noisy errors.
+
+### `module-doc-with-calling-convention-example`
+
+Useful, but it requires richer curated API examples. Better approach:
+
+- Add optional examples to `data/api-signatures.sexp`.
+- Teach `jerboa_doc` and `jerboa_module_catalog` to display them.
+- Seed examples gradually for high-friction modules.
+
+### `auto-balance-check-after-deep-edit`
+
+Useful but should be part of edit tools, not a new tool. Add depth detection to:
+
+- `jerboa_balanced_replace`
+- `jerboa_wrap_form`
+- `jerboa_splice_form`
+
+### `security-scan-changed-lines-mode`
+
+Useful for mature repos with noisy historical findings. Implement after the
+basic project-libdir and bundled-verify work.
+
+## Suggested Implementation Order
+
+1. Improve feature registry metadata and `jerboa_list_features` output.
+2. Promote MCP writer tools to `critical-tools`.
+3. Add shared project libdir resolution and wire it into compile, verify, and
+   run-tests.
+4. Add `jerboa_symbol_exists_batch`.
+5. Add `jerboa_verify_changes`.
+6. Upgrade the partial static/security scanners.
+7. Add static binary library closure.
+8. Reconcile non-MCP feature records with current stdlib/core status.
+
+## Verification Plan
+
+For MCP-only changes:
+
+- Run focused protocol tests for tool registration and schemas.
+- Run the MCP protocol test suite.
+- Run `make binary` before committing on macOS.
+
+For changes touching `.ss` or `.sls`:
+
+- Run `jerboa_verify` on edited Scheme files.
+- Run MCP tests through `jerboa_run_tests` or the appropriate Makefile target.
+- Run `make binary` before committing on macOS.