Constrain pre-edit MCP discovery in verified runs

ober

603a75aaa8fcf4adf5c1d9faf103fdddd0310e42

diff --git a/src/jcode/core/verified-run.ss b/src/jcode/core/verified-run.ss
index dad0775..a0a365d 100644
--- a/src/jcode/core/verified-run.ss
+++ b/src/jcode/core/verified-run.ss
@@ -603,6 +603,16 @@
   (string-has-any? (tool-def-name tool-def)
                    *verified-compact-mcp-name-fragments*))
 
+(def (pre-edit-mcp-block-message name)
+  (and (= (current-successful-edit-count) 0)
+       (let ((next (+ (current-pre-edit-mcp-count) 1)))
+         (current-pre-edit-mcp-count next)
+         (and (> next pre-edit-mcp-limit)
+              (string-append
+                "pre-edit MCP discovery limit reached while calling "
+                name
+                ". Stop routing to MCP/API discovery. The next useful tool must be read/list on the current repo if a local file is still unknown, or edit/write with the first complete draft. After a successful edit and verify failure, use targeted MCP only for concrete syntax/API questions.")))))
+
 (def (env-truthy? name)
   (let ((v (getenv name)))
     (and v
@@ -630,6 +640,8 @@
                  (make-tool-spec name desc params)
                  (lambda (args)
                    (or (rejected-draft-hard-recovery-message cwd name)
+                       (let ((msg (pre-edit-mcp-block-message name)))
+                         (and msg (error 'mcp msg)))
                        (tool-execute name (workflow-args->hash args))))
                  '())))))
     (get-tool-schemas)))
@@ -649,7 +661,10 @@
                            "")
                          ".\n")
           ".\n")
-        "MCP tools are optional support for language, API, project-specific, or validator facts; caller guidance may forbid or narrow their use, and that caller guidance wins. After verify() fails, the verifier output is the repair target: make the smallest relevant edit and call verify again before broad MCP/API discovery. Use MCP after a failure only for targeted syntax or API facts that block the edit. Keep code changes through edit/write/line_edit/replace_def/replace_range and final validation through verify.\n"))))
+        "MCP tools are optional support for language, API, project-specific, or validator facts; caller guidance may forbid or narrow their use, and that caller guidance wins. After verify() fails, the verifier output is the repair target: make the smallest relevant edit and call verify again before broad MCP/API discovery. Use MCP after a failure only for targeted syntax or API facts that block the edit. Keep code changes through edit/write/line_edit/replace_def/replace_range and final validation through verify.\n"
+        "Before the first successful edit, MCP discovery has a hard budget of "
+        (number->string pre-edit-mcp-limit)
+        " calls. Use it only to remove concrete uncertainty; then create the first draft with edit/write and verify it.\n"))))
 
 (def *tool-path-suffixes*
   '(".ss" ".scm" ".sls" ".md" ".txt" ".json" ".yaml" ".yml"
@@ -788,6 +803,12 @@
 (def current-inspections-after-edit
   (make-parameter 0))
 
+(def current-successful-edit-count
+  (make-parameter 0))
+
+(def current-pre-edit-mcp-count
+  (make-parameter 0))
+
 (def current-last-verify-detail
   (make-parameter #f))
 
@@ -801,6 +822,8 @@
 
 (def inspection-after-edit-limit 4)
 
+(def pre-edit-mcp-limit 2)
+
 (def rejected-draft-inspection-limit 2)
 
 (def rejected-draft-repeat-threshold 1)
@@ -979,6 +1002,7 @@
   (reset-existing-ss-rewrite-state!)
   (reset-path-only-run-state!)
   (reset-failed-verify-inspections!)
+  (current-successful-edit-count (+ (current-successful-edit-count) 1))
   (current-edited-since-verify? #t)
   (current-inspections-after-edit 0))
 
@@ -3363,6 +3387,8 @@
            " Call verify now."))))))
 
 (def (coding-workflow verify-cmd cwd . opt)
+  (current-successful-edit-count 0)
+  (current-pre-edit-mcp-count 0)
   (let* ((o (if (pair? opt) (car opt) '()))
          (scope (or (opt-get o 'write-scope) (current-write-scope)))
          (run-aliases? (let ((p (assoc 'run-aliases? o)))
@@ -3648,6 +3674,8 @@
                    (current-inspections-after-failed-verify 0)
                    (current-edited-since-verify? #f)
                    (current-inspections-after-edit 0)
+                   (current-successful-edit-count 0)
+                   (current-pre-edit-mcp-count 0)
                    (current-required-range-repair #f))
       (if (> k 1)
         (run-best-of-k wf task (lambda () (provider-responder provider)) k ropt)
diff --git a/test/run.ss b/test/run.ss
index 5ecb841..29aaea1 100644
--- a/test/run.ss
+++ b/test/run.ss
@@ -2385,7 +2385,46 @@
            (str-contains? s "jerboa_test_lookup")
            (str-contains? s "caller guidance wins")
            (str-contains? s "After verify() fails")
-           (str-contains? s "before broad MCP/API discovery")))))
+           (str-contains? s "before broad MCP/API discovery")
+           (str-contains? s "Before the first successful edit")
+           (str-contains? s "MCP discovery has a hard budget")))))
+
+(let* ([vr-dir "/tmp"]
+       [target "jcode-verified-mcp-budget.txt"]
+       [target-path (string-append vr-dir "/" target)]
+       [tool-results '()])
+  (guard (e [#t (void)]) (delete-file target-path))
+  (let* ([wf (coding-workflow (string-append "grep -q first-draft " target-path) vr-dir)]
+         [resp (scripted-responder
+                 (list
+                   (list (make-wtool-call "jerboa_test_lookup" '(("q" . "one")) #f))
+                   (list (make-wtool-call "jerboa_test_lookup" '(("q" . "two")) #f))
+                   (list (make-wtool-call "jerboa_test_lookup" '(("q" . "three")) #f))
+                   (list (make-wtool-call "write" (list (cons "path" target)
+                                                        (cons "content" "first-draft"))
+                                          #f))
+                   (list (make-wtool-call "verify" '() #f))
+                   (list (make-wtool-call "done" '(("summary" . "mcp-budget-ok")) #f))))])
+    (let ([result (run-workflow wf "avoid pre-edit MCP loop" resp
+                    (list (cons 'max-iterations 10)
+                          (cons 'on-message
+                            (lambda (m)
+                              (when (equal? (message-role m) "tool")
+                                (set! tool-results
+                                  (cons (message-content m) tool-results)))))))])
+      (check! "verified-run: pre-edit MCP budget recovers through write"
+              result "mcp-budget-ok")
+      (check-pred! "verified-run: pre-edit MCP budget blocks third lookup"
+        (reverse tool-results)
+        (lambda (xs)
+          (let loop ([ys xs])
+            (cond
+              [(null? ys) #f]
+              [(and (str-contains? (car ys) "pre-edit MCP discovery limit reached")
+                    (str-contains? (car ys) "edit/write with the first complete draft"))
+               #t]
+              [else (loop (cdr ys))]))))))
+  (guard (e [#t (void)]) (delete-file target-path)))
 
 (register-tool! "jerboa_failure_advisor"
                 "Fake MCP failure advisor for verified-run tests."