Improve local verified edit flow

ober

8bd3a828fdea726547f9599c936c1f6ab2dcb808

diff --git a/main-binary.ss b/main-binary.ss
index 16eb44f..b3f16ce 100644
--- a/main-binary.ss
+++ b/main-binary.ss
@@ -6,8 +6,10 @@
         (jcode core config)
         (jcode core prompts)
         (jcode core agent)
+        (jcode tool web)
         (jcode ui cli)
         (jerbsearch engine-worker)
+        (only (std net uri) uri-parse)
         (only (std os exec-id)
               exec-id-resolve exec-id-realpath)
         (only (std os env) unsetenv))
diff --git a/src/jcode/core/verified-run.ss b/src/jcode/core/verified-run.ss
index 6a198d8..5750f4f 100644
--- a/src/jcode/core/verified-run.ss
+++ b/src/jcode/core/verified-run.ss
@@ -41,7 +41,7 @@
 (def default-verified-history-keep-batches 3)
 (def default-local-verified-history-token-limit 24000)
 (def default-local-verified-history-keep-batches 2)
-(def default-local-verified-max-completion-tokens 8192)
+(def default-local-verified-max-completion-tokens 4096)
 (def default-local-verified-repair-max-completion-tokens 4096)
 
 (def (opt-get o key) (let ((p (assoc key o))) (and p (cdr p))))
@@ -181,16 +181,24 @@
 
 (def (verified-staged-repair-tool-spec? spec)
   (member (tool-spec-name spec)
-          '("line_edit" "replace_def" "replace_range")))
+          '("line_edit" "replace_def" "replace_range" "verify")))
 
 (def (verified-rejected-draft-inspection-tool-spec? spec)
   (member (tool-spec-name spec) '("read" "balance")))
 
+(def (rejected-draft-staged-repair-mode?)
+  (and (current-rejected-ss-draft)
+       (or (> (current-rejected-draft-inspections)
+              rejected-draft-inspection-limit)
+           (>= (current-rejected-draft-path-reject-count)
+               local-expert-after-rejected-drafts))))
+
 (def (local-focused-repair-mode?)
   (and (current-verified-local-model?)
        (or (current-after-failed-verify?)
            (and (current-rejected-ss-draft)
                 (> (current-rejected-draft-inspections) 0))
+           (rejected-draft-staged-repair-mode?)
            (> (current-successful-edit-count) 0))))
 
 (def (verified-provider-tool-spec spec)
@@ -289,9 +297,7 @@
           ;; bounded staged edits; another read or full rewrite repeats the
           ;; expensive failure cycle.
           ((and (current-verified-local-model?)
-                (current-rejected-ss-draft)
-                (> (current-rejected-draft-inspections)
-                   rejected-draft-inspection-limit)
+                (rejected-draft-staged-repair-mode?)
                 (not (verified-staged-repair-tool-spec? spec)))
            #f)
           ((not (verified-mcp-tool-spec? spec)) #t)
@@ -311,9 +317,49 @@
               ((and (= (current-successful-edit-count) 0)
                     (>= (current-pre-edit-mcp-count) pre-edit-mcp-limit))
                #f)
-              (else #t))))))
+          (else #t))))))
       specs)))
 
+(def (tool-specs-include? specs name)
+  (let loop ((xs specs))
+    (cond
+      ((null? xs) #f)
+      ((string=? (tool-spec-name (car xs)) name) #t)
+      (else (loop (cdr xs))))))
+
+(def (local-force-first-edit-tool-choice specs)
+  (and (current-verified-local-model?)
+       (= (current-successful-edit-count) 0)
+       (not (current-edited-since-verify?))
+       (not (current-after-failed-verify?))
+       (not (current-rejected-ss-draft))
+       (not (current-pending-ss-create-repair))
+       (>= (current-pre-edit-inspection-count) 2)
+       (tool-specs-include? specs "edit")
+       (openai-function-tool-choice "edit")))
+
+(def (local-forced-first-edit-specs specs forced-choice)
+  (if forced-choice
+    (filter (lambda (spec)
+              (member (tool-spec-name spec) '("edit" "write")))
+            specs)
+    specs))
+
+(def (local-forced-first-edit-response response forced-choice)
+  (if (and forced-choice
+           (pair? response)
+           (let loop ((xs response))
+             (cond
+               ((null? xs) #f)
+               ((not (and (wtool-call? (car xs))
+                          (member (wtool-call-tool (car xs))
+                                  '("edit" "write"))))
+                #t)
+               (else (loop (cdr xs))))))
+    (make-text-response
+      "Forced first-edit turn rejected: the model called a hidden inspection or non-edit tool. Do not call read, list, verify, or any other tool on this turn. Call exactly edit(path=<target file>, content=<complete minimal acceptance-complete file body>) or write(path=<target file>, content=<complete minimal acceptance-complete file body>) now.")
+    response))
+
 (def (provider-responder provider)
   (let ((backend (if (procedure? provider)
                    provider
@@ -353,7 +399,14 @@
                 ((make-provider-backend
                    (get-expert-provider) chat-direct-via-stream)
                  messages specs #f))
-              (backend messages specs #f))))))))
+	              (let ((forced-choice (local-force-first-edit-tool-choice specs)))
+	                (parameterize
+	                  ((current-tool-choice-override forced-choice))
+	                  (local-forced-first-edit-response
+	                    (backend messages
+	                             (local-forced-first-edit-specs specs forced-choice)
+	                             #f)
+	                    forced-choice))))))))))
 
 ;; ── verify oracle ──────────────────────────────────────────────────────
 (def (tail-lines s n)
@@ -1476,6 +1529,16 @@
          (not (string=? (string-downcase v) "false"))
          (not (string=? (string-downcase v) "no")))))
 
+(def (env-positive-int name)
+  (let ((v (getenv name)))
+    (and v
+         (let ((n (string->number (string-trim v))))
+           (and (integer? n) (> n 0) n)))))
+
+(def (local-verified-first-draft-cap)
+  (or (env-positive-int "JCODE_LOCAL_VERIFIED_MAX_COMPLETION_TOKENS")
+      default-local-verified-max-completion-tokens))
+
 (def (schema-function schema)
   (and (hash-table? schema) (hash-get schema "function")))
 
@@ -1679,6 +1742,9 @@
 (def current-successful-edit-count
   (make-parameter 0))
 
+(def current-last-edit-path
+  (make-parameter #f))
+
 (def current-pre-edit-mcp-count
   (make-parameter 0))
 
@@ -1734,6 +1800,7 @@
   (current-requirements-review-pending? #f)
   (current-inspections-after-edit 0)
   (current-successful-edit-count 0)
+  (current-last-edit-path #f)
   (current-pre-edit-mcp-count 0)
   (current-pre-edit-run-alias-count 0)
   (current-pre-edit-inspection-count 0)
@@ -2000,6 +2067,7 @@
   (reset-failed-verify-inspections!)
   (current-pre-edit-run-alias-count 0)
   (current-successful-edit-count (+ (current-successful-edit-count) 1))
+  (current-last-edit-path path)
   (current-edited-since-verify? #t)
   (current-inspections-after-edit 0))
 
@@ -2567,13 +2635,26 @@
             (map string-trim (string-split raw #\:)))
     '()))
 
+(def (env-disabled? name)
+  (let ((v (getenv name)))
+    (and v
+         (let ((s (string-downcase (string-trim v))))
+           (or (string=? s "0")
+               (string=? s "false")
+               (string=? s "no")
+               (string=? s "off"))))))
+
+(def (auto-jerboa-home-lib-enabled?)
+  (not (env-disabled? "JCODE_AUTO_JERBOA_HOME_LIB")))
+
 (def (verified-read-roots cwd)
   (let* ((configured
            (map (lambda (root) (abs-path cwd (expand-home-path root)))
                 (split-read-roots (getenv "JCODE_READ_ROOTS"))))
          (jerboa-home (getenv "JERBOA_HOME"))
          (jerboa-lib
-           (and jerboa-home
+           (and (auto-jerboa-home-lib-enabled?)
+                jerboa-home
                 (not (string=? jerboa-home ""))
                 (string-append
                   (abs-path cwd (expand-home-path jerboa-home)) "/lib"))))
@@ -2686,6 +2767,17 @@
 (def (missing-edit-args-message)
   "edit/write requires concrete arguments. Do not retry empty edit() or write(). To create or replace a file, call edit(path=\"life.ss\", content=<complete file contents>). To patch existing code, call edit(path=\"life.ss\", old_str=<exact current text>, new_str=<replacement>) or use line_edit/replace_def/replace_range. Then call verify().")
 
+(def (infer-local-edit-path path content old-str line-no)
+  (or path
+      (and (current-verified-local-model?)
+           (not old-str)
+           (= line-no 0)
+           (string? content)
+           (not (string=? (string-trim content) ""))
+           (or (current-rejected-draft-path)
+               (current-last-edit-path)
+               (scope-primary-file (current-write-scope))))))
+
 (def (missing-local-mutation-args-message tool call-shape)
   (string-append
     (symbol->string tool)
@@ -4205,7 +4297,7 @@
     (cons "(return)" "Jerboa has no return form; use cond/if/named let to return values")
     (cons "(break " "Jerboa has no break form; use named let/conditionals")
     (cons "(continue " "Jerboa has no continue form; use named let/conditionals")
-    (cons "list-set!" "list-set! is not available in Jerboa; use vectors or rebuild lists")
+    (cons "list-set!" "list-set! is not available in Jerboa. For mutable grids, make the board and rows vectors and use vector-set! on the row vector; if the data is a list, rebuild the list/row and replace the parent value instead.")
     (cons "random-make-state" "use (random n); do not use random-make-state")
     (cons "'." "do not use '. as a datum; use a normal symbol such as 'empty or a number sentinel")
     (cons "(quote .)" "do not use (quote .); use a normal symbol such as 'empty or a number sentinel")
@@ -4729,13 +4821,15 @@
 
 (def (do-edit args cwd)
   (let* ((content (arg-content args))
-         (path    (arg-path args #f))
+         (path0   (arg-path args #f))
          (old-str (or (arg-ref args "old_str" #f)
                       (arg-ref args "old_string" #f)))
          (new-str (or (arg-ref args "new_str" #f)
                       (arg-ref args "new_string" #f)
                       (and old-str content)))
          (line-no (arg-int args "line" 0))
+         (path    (infer-local-edit-path path0 content old-str line-no))
+         (inferred-path? (and (not path0) path))
          (required-full-rewrite?
            (required-repair-full-rewrite? cwd path content old-str line-no)))
     (cond
@@ -4884,6 +4978,7 @@
            ((and (file-exists? p)
                  (source-ss-path? path)
                  (local-focused-repair-mode?)
+                 (not inferred-path?)
                  (not required-full-rewrite?))
             (raise-recoverable-tool-error
               (string-append
@@ -5512,7 +5607,7 @@
 	            (number->string local-pre-edit-inspection-limit)
 	            " substantive read/shell/MCP inspections. Do not inspect external Jerboa sources unless one exact API fact blocks the implementation. Write the first complete draft within these budgets. After a failed verify, use at most "
 	            (number->string local-inspection-after-failed-verify-limit)
-	            " focused inspections before editing the diagnosed span. Local replace_range edits are transactional: a syntactically broken result is rejected and leaves the on-disk file unchanged.\n")
+	            " focused inspections before editing the diagnosed span. Code written in reasoning, analysis, markdown fences, or prose does not change files and will be discarded; put the implementation directly in an edit/write/line_edit/replace_def/replace_range tool call. For the first implementation draft, use one edit/write tool call with the complete file content instead of drafting the file in reasoning. For larger app/game/TUI tasks, write the smallest acceptance-complete vertical slice first: satisfy the verifier's required modes and source hooks, verify it, then add only behavior the verifier proves is missing. Local replace_range edits are transactional: a syntactically broken result is rejected and leaves the on-disk file unchanged.\n")
 	          "")
 	        (if run-aliases?
 	          (string-append
@@ -5645,6 +5740,7 @@
                    (current-requirements-review-pending? #f)
                    (current-inspections-after-edit 0)
                    (current-successful-edit-count 0)
+                   (current-last-edit-path #f)
                    (current-pre-edit-mcp-count 0)
                    (current-pre-edit-run-alias-count 0)
                    (current-pre-edit-inspection-count 0)
@@ -5652,7 +5748,7 @@
                    (current-verified-local-model? local-model?)
                    (current-max-tokens-cap
                      (and local-model?
-                          default-local-verified-max-completion-tokens))
+                          (local-verified-first-draft-cap)))
                    (current-verify-failure-count 0)
                    (current-force-expert-next? #f)
                    (current-serving-forced-expert? #f)
diff --git a/src/jcode/core/workflow-runner.ss b/src/jcode/core/workflow-runner.ss
index e6df257..ccfa060 100644
--- a/src/jcode/core/workflow-runner.ss
+++ b/src/jcode/core/workflow-runner.ss
@@ -398,7 +398,7 @@
       "If code must change, call edit/write/line_edit/replace_def/replace_range with complete concrete code, never placeholders.\n"
       (if (and (string? raw)
                (string-prefix? "<think>" (string-trim raw)))
-        "Your prior turn exhausted its output budget in reasoning. Do not restart or repeat that analysis; use the task and tool results already present and issue the pending edit now.\n"
+        "Your prior turn exhausted its output budget in reasoning. Code in reasoning was discarded and did not change any file. Do not restart or repeat that analysis; issue exactly one edit/write/line_edit/replace_def/replace_range tool call now with the concrete code in the tool arguments.\n"
         "")
       (if (> retry-count 1)
         "This is a repeated prose-only response. Stop free-form text and issue the tool call now.\n"
diff --git a/src/jcode/provider/provider.ss b/src/jcode/provider/provider.ss
index 2addff7..2a5d3b9 100644
--- a/src/jcode/provider/provider.ss
+++ b/src/jcode/provider/provider.ss
@@ -13,6 +13,8 @@
         provider-base-url
         provider-local?
         current-max-tokens-cap
+        current-tool-choice-override
+        openai-function-tool-choice
         current-stream-abort?
         model-rejects-tools?
         extract-text-tool-calls
@@ -990,6 +992,19 @@
 ;; cloud-origin workflows leave it unbound and are unaffected.
 (def current-max-tokens-cap (make-parameter #f))
 
+;; Verified workflows can temporarily require a specific OpenAI-compatible
+;; function call after the model has inspected enough context. Leave unbound
+;; for ordinary chat and providers that should choose tools normally.
+(def current-tool-choice-override (make-parameter #f))
+
+(def (openai-function-tool-choice name)
+  (let ((fn (make-hash-table))
+        (choice (make-hash-table)))
+    (hash-put! fn "name" name)
+    (hash-put! choice "type" "function")
+    (hash-put! choice "function" fn)
+    choice))
+
 (def (openai-max-tokens provider)
   (let* ((configured
            (or (positive-int-value (getenv "JCODE_MAX_TOKENS"))
@@ -1551,7 +1566,8 @@
     (when (and tools (not (null? tools))
                (not (model-rejects-tools? (provider-model provider))))
       (hash-put! body "tools" tools)
-      (hash-put! body "tool_choice" "auto"))
+      (hash-put! body "tool_choice"
+                 (or (current-tool-choice-override) "auto")))
     body))
 
 ;; Some models reject the `tools` parameter entirely (e.g. DeepSeek's
@@ -2269,7 +2285,8 @@
     (when (and tools (not (null? tools))
                (not (model-rejects-tools? (provider-model provider))))
       (hash-put! body "tools" tools)
-      (hash-put! body "tool_choice" "auto"))
+      (hash-put! body "tool_choice"
+                 (or (current-tool-choice-override) "auto")))
     body))
 
 (def (openai-stream-chat provider messages tools token-cb)
diff --git a/test/run.ss b/test/run.ss
index 9b1aed2..7cb27f2 100644
--- a/test/run.ss
+++ b/test/run.ss
@@ -3263,10 +3263,11 @@
 	    (check-pred! "verified-run: repeated local rejection focuses staged tools"
 	      staged-tool-names
 	      (lambda (names)
-	        (and (= (length names) 3)
+	        (and (= (length names) 4)
 	             (member "line_edit" names)
 	             (member "replace_def" names)
-	             (member "replace_range" names))))
+	             (member "replace_range" names)
+	             (member "verify" names))))
 	    (check! "verified-run: local staged line edit promotes repaired file"
 	            (slurp target-path)
 	            (string-append "(import (jerboa prelude))\n" good-line "\n")))
@@ -3434,8 +3435,8 @@
 	    (check! "verified-run: local edit is automatically verified"
 	            result "VERIFIED: exit 0\n"))
 	  (let ([observed (reverse caps)])
-	    (check! "verified-run: local first-draft completion cap is 8k"
-	            (car observed) 8192)
+	    (check! "verified-run: local first-draft completion cap is 4k"
+	            (car observed) 4096)
 	    (check! "verified-run: local post-verifier repair cap is 4k"
 	            (cadr observed) 4096))
 	  (check! "verified-run: automatic verify avoids a provider round"
@@ -3443,6 +3444,45 @@
 	  (safe-delete-test-file! target-path))
 
 	(let* ([vr-dir "/tmp"]
+	       [target "jcode-local-env-first-cap.txt"]
+	       [target-path (string-append vr-dir "/" target)]
+	       [old-cap (getenv "JCODE_LOCAL_VERIFIED_MAX_COMPLETION_TOKENS")]
+	       [calls 0]
+	       [caps '()]
+	       [responder
+	         (lambda (_messages _tools _step)
+	           (set! calls (+ calls 1))
+	           (set! caps (cons (current-max-tokens-cap) caps))
+	           (case calls
+	             [(1) (list (make-wtool-call "verify" '() #f))]
+	             [(2) (list (make-wtool-call "edit"
+	                         (list (cons "path" target)
+	                               (cons "content" "repaired\n")) #f))]
+	             [else (error 'test "automatic local verify should avoid another provider call")]))])
+	  (safe-delete-test-file! target-path)
+	  (dynamic-wind
+	    (lambda () (putenv "JCODE_LOCAL_VERIFIED_MAX_COMPLETION_TOKENS" "8192"))
+	    (lambda ()
+	      (let ([result
+	              (verified-run responder "repair with a larger local first cap"
+	                (list
+	                  (cons 'cwd vr-dir)
+	                  (cons 'verify-command (string-append "grep -q repaired " target))
+	                  (cons 'write-scope (parse-write-scope target))
+	                  (cons 'local-model? #t)
+	                  (cons 'max-iterations 8)))])
+	        (check! "verified-run: env local first cap still verifies"
+	                result "VERIFIED: exit 0\n"))
+	      (let ([observed (reverse caps)])
+	        (check! "verified-run: env local first cap is honored"
+	                (car observed) 8192)
+	        (check! "verified-run: env local repair cap remains 4k"
+	                (cadr observed) 4096)))
+	    (lambda ()
+	      (putenv "JCODE_LOCAL_VERIFIED_MAX_COMPLETION_TOKENS" (or old-cap ""))
+	      (safe-delete-test-file! target-path))))
+
+	(let* ([vr-dir "/tmp"]
 	       [target "jcode-local-env-initial-verify.txt"]
 	       [target-path (string-append vr-dir "/" target)]
 	       [old-initial (getenv "JCODE_VERIFIED_INITIAL_VERIFY")]
@@ -3660,8 +3700,8 @@
 	    (check! "verified-run: inspected rejected draft repair verifies"
 	            result "VERIFIED: exit 0\n"))
 	  (let ([observed (reverse caps)])
-	    (check! "verified-run: uninspected rejected draft keeps 8k cap"
-	            (cadr observed) 8192)
+	    (check! "verified-run: uninspected rejected draft keeps 4k cap"
+	            (cadr observed) 4096)
 	    (check! "verified-run: inspected rejected draft repair uses 4k cap"
 	            (caddr observed) 4096))
 	  (check-pred! "verified-run: inspected rejected draft hides broad write"
@@ -3671,6 +3711,47 @@
 	  (safe-delete-test-file! target-path))
 
 	(let* ([vr-dir "/tmp"]
+	       [target "jcode-local-repeated-rejected-draft-schema.ss"]
+	       [target-path (string-append vr-dir "/" target)]
+	       [bad-a "(import (jerboa prelude))\n(def (main) (displayln \"bad\")))\n"]
+	       [bad-b "(import (jerboa prelude))\n(def (main) (displayln \"still bad\")))\n"]
+	       [tool-names-after-repeat '()]
+	       [calls 0]
+	       [responder
+	         (lambda (_messages tools _step)
+	           (set! calls (+ calls 1))
+	           (when (= calls 3)
+	             (set! tool-names-after-repeat (map tool-spec-name tools)))
+	           (case calls
+	             [(1) (list (make-wtool-call "write"
+	                         (list (cons "path" target)
+	                               (cons "content" bad-a)) #f))]
+	             [(2) (list (make-wtool-call "write"
+	                         (list (cons "path" target)
+	                               (cons "content" bad-b)) #f))]
+	             [(3) (list (make-wtool-call "line_edit"
+	                         (list (cons "path" target)
+	                               (cons "line" 2)
+	                               (cons "content" "(def (main) (displayln \"fixed\"))")) #f))]
+	             [else (error 'test "repeated rejected draft should auto-verify")]))])
+	  (safe-delete-test-file! target-path)
+	  (let ([result
+	          (verified-run responder "repair repeated rejected draft without another full write"
+	            (list
+	              (cons 'cwd vr-dir)
+	              (cons 'verify-command (string-append "grep -q fixed " target))
+	              (cons 'write-scope (parse-write-scope target))
+	              (cons 'local-model? #t)
+	              (cons 'max-iterations 8)))])
+	    (check! "verified-run: repeated rejected draft schema repair verifies"
+	            result "VERIFIED: exit 0\n"))
+	  (check-pred! "verified-run: repeated rejected draft hides write schema"
+	    tool-names-after-repeat (lambda (names) (not (member "write" names))))
+	  (check-pred! "verified-run: repeated rejected draft keeps line edit schema"
+	    tool-names-after-repeat (lambda (names) (member "line_edit" names)))
+	  (safe-delete-test-file! target-path))
+
+	(let* ([vr-dir "/tmp"]
 	       [target "jcode-local-rejected-draft-read-after-failure.ss"]
 	       [target-path (string-append vr-dir "/" target)]
 	       [initial "(import (jerboa prelude))\n(def value \"broken\")\n"]
@@ -4288,7 +4369,8 @@
        [lib (string-append home "/lib")]
        [external-file (string-append lib "/runtime-api.ss")]
        [old-home (getenv "JERBOA_HOME")]
-       [old-read-roots (getenv "JCODE_READ_ROOTS")])
+       [old-read-roots (getenv "JCODE_READ_ROOTS")]
+       [old-auto-lib (getenv "JCODE_AUTO_JERBOA_HOME_LIB")])
   (ensure-test-directory! home)
   (ensure-test-directory! lib)
   (write-test-output-file external-file
@@ -4311,7 +4393,25 @@
                  (str-contains? s lib))))))
     (lambda ()
       (putenv "JERBOA_HOME" (or old-home ""))
-      (putenv "JCODE_READ_ROOTS" (or old-read-roots "")))))
+      (putenv "JCODE_READ_ROOTS" (or old-read-roots ""))))
+  (dynamic-wind
+    (lambda ()
+      (putenv "JERBOA_HOME" home)
+      (putenv "JCODE_READ_ROOTS" "")
+      (putenv "JCODE_AUTO_JERBOA_HOME_LIB" "0"))
+    (lambda ()
+      (let* ([wf (coding-workflow "true" "/tmp/jcode-verified-home-workspace")]
+             [read-tool (tool-def-callable (workflow-get-tool-def wf "read"))])
+        (check-pred! "verified-run: auto Jerboa lib root can be disabled"
+          (read-tool (list (cons "path" external-file)))
+          (lambda (s) (str-contains? s "read refused outside")))
+        (check-pred! "verified-run: disabled auto Jerboa lib is not advertised"
+          (workflow-system-prompt-template wf)
+          (lambda (s) (not (str-contains? s "JERBOA_HOME/lib"))))))
+    (lambda ()
+      (putenv "JERBOA_HOME" (or old-home ""))
+      (putenv "JCODE_READ_ROOTS" (or old-read-roots ""))
+      (putenv "JCODE_AUTO_JERBOA_HOME_LIB" (or old-auto-lib "")))))
 
 (let* ([wf (coding-workflow "true" "/tmp")]
        [names (workflow-tool-names wf)])
@@ -7827,6 +7927,107 @@
 	  (safe-delete-test-file! target-path)))
 
 	(let* ([vr-dir  "/tmp"]
+	       [target "jcode-verified-infer-rejected-draft-path.ss"]
+	       [target-path (string-append vr-dir "/" target)]
+	       [bad "(import (jerboa prelude))\n(def (main)\n  (return 1))\n"]
+	       [good "(import (jerboa prelude))\n(displayln \"fixed\")\n"]
+	       [tool-results '()]
+	       [slurp (lambda (p) (call-with-input-file p (lambda (i) (get-string-all i))))]
+	       [calls 0]
+	       [responder
+	         (lambda (_messages _tools _step)
+	           (set! calls (+ calls 1))
+	           (case calls
+	             [(1)
+	              (list (make-wtool-call "write"
+	                      (list (cons "path" target)
+	                            (cons "content" bad)) #f))]
+	             [(2)
+	              (list (make-wtool-call "write"
+	                      (list (cons "content" good)) #f))]
+	             [else
+	              (error 'test "path inference should auto-verify after repaired write")]))])
+	  (safe-delete-test-file! target-path)
+	  (let ([result
+	          (verified-run responder "infer the rejected draft path for a local rewrite"
+	            (list
+	              (cons 'cwd vr-dir)
+	              (cons 'verify-command (string-append "grep -q fixed " target))
+	              (cons 'write-scope (parse-write-scope target))
+	              (cons 'local-model? #t)
+	              (cons 'max-iterations 8)
+	              (cons 'max-tool-errors 4)
+	              (cons 'on-message
+	                (lambda (m)
+	                  (when (equal? (message-role m) "tool")
+	                    (set! tool-results
+	                      (cons (message-content m) tool-results)))))))])
+	    (check! "verified-run: local rewrite infers rejected draft path"
+	            result "VERIFIED: exit 0\n")
+	    (check! "verified-run: inferred rejected path wrote repaired file"
+	            (slurp target-path) good)
+	    (check-pred! "verified-run: missing path repair followed rejected draft"
+	      (reverse tool-results)
+	      (lambda (xs)
+	        (let loop ([ys xs])
+	          (cond
+	            [(null? ys) #f]
+	            [(and (str-contains? (car ys) "syntax guard rejected")
+	                  (str-contains? (car ys) target)) #t]
+	            [else (loop (cdr ys))])))))
+	  (safe-delete-test-file! target-path))
+
+	(let* ([vr-dir  "/tmp"]
+	       [target "jcode-verified-infer-last-edit-path.ss"]
+	       [target-path (string-append vr-dir "/" target)]
+	       [draft "(import (jerboa prelude))\n(displayln \"draft\")\n"]
+	       [good "(import (jerboa prelude))\n(displayln \"fixed\")\n"]
+	       [calls 0]
+	       [tool-results '()]
+	       [responder
+	         (lambda (_messages _tools _step)
+	           (set! calls (+ calls 1))
+	           (case calls
+	             [(1)
+	              (list (make-wtool-call "write"
+	                      (list (cons "path" target)
+	                            (cons "content" draft)) #f))]
+	             [(2)
+	              (list (make-wtool-call "write"
+	                      (list (cons "content" good)) #f))]
+	             [else
+	              (list (make-wtool-call "verify" '() #f))]))]
+	       [slurp (lambda (p) (call-with-input-file p (lambda (i) (get-string-all i))))])
+	  (safe-delete-test-file! target-path)
+	  (let ([result
+	          (verified-run responder "infer the last edit path for a local rewrite"
+	            (list
+	              (cons 'cwd vr-dir)
+	              (cons 'verify-command (string-append "grep -q fixed " target))
+	              (cons 'write-scope (parse-write-scope target))
+	              (cons 'local-model? #t)
+	              (cons 'max-iterations 8)
+	              (cons 'max-tool-errors 4)
+	              (cons 'on-message
+	                (lambda (m)
+	                  (when (equal? (message-role m) "tool")
+	                    (set! tool-results
+	                      (cons (message-content m) tool-results)))))))])
+	    (check! "verified-run: local rewrite infers last edited path"
+	            result "VERIFIED: exit 0\n")
+	    (check! "verified-run: inferred last edited path wrote repaired file"
+	            (slurp target-path) good)
+	    (check-pred! "verified-run: pathless rewrite uses last edited path"
+	      (reverse tool-results)
+	      (lambda (xs)
+	        (let loop ([ys xs])
+	          (cond
+	            [(null? ys) #f]
+	            [(str-contains? (car ys) "wrote jcode-verified-infer-last-edit-path.ss") #t]
+	            [else (loop (cdr ys))])))))
+	  (safe-delete-test-file! target-path))
+
+	(let* ([vr-dir  "/tmp"]
 	       [target "jcode-verified-rejected-draft-limit.ss"]
 	       [target-path (string-append vr-dir "/" target)]
 	       [bad "(import (jerboa prelude))\n(define (main)\n  (displayln \"draft\")))\n"]
@@ -9493,12 +9694,24 @@
     (lambda ()
       (serve-one-captured-json! srv captured 200 chat-body)
       (let* ([p (make-provider "mlx" "" "unit-test-model" base-url)]
-             [msg (parameterize ([*config* cfg])
+             [msg (parameterize ([*config* cfg]
+                                 [current-tool-choice-override
+                                  (openai-function-tool-choice "lookup")])
                     (provider-chat p (list (make-user-message "hi")) (list tool)))]
-             [req (vector-ref captured 0)])
+             [req (vector-ref captured 0)]
+             [req-json (and req (string->json-object req))]
+             [tool-choice (and req-json
+                               (hashtable-ref req-json "tool_choice" #f))]
+             [tool-choice-fn
+               (and (hashtable? tool-choice)
+                    (hashtable-ref tool-choice "function" #f))])
         (check! "mlx local tool chat reply" (message-content msg) "ok")
         (check! "mlx local tool chat includes tools"
                 (and req (str-contains? req "\"tools\"")) #t)
+        (check! "mlx local tool chat honors tool_choice override"
+                (and (hashtable? tool-choice-fn)
+                     (hashtable-ref tool-choice-fn "name" #f))
+                "lookup")
         (check! "mlx local tool chat omits logprobs"
                 (and req (str-contains? req "\"logprobs\"")) #f)
         (check! "mlx local tool chat omits top_logprobs"
@@ -10221,6 +10434,12 @@
              (slurp-file "main-binary.ss")
              (lambda (src) (str-find src "\"--no-expert\"" 0)))
 
+(check-pred! "binary roots web URI dependency"
+             (slurp-file "main-binary.ss")
+             (lambda (src)
+               (and (str-find src "(jcode tool web)" 0)
+                    (str-find src "(std net uri)" 0))))
+
 (check-pred! "built-in CLI help advertises verified iteration budget"
              (slurp-file "src/jcode/ui/cli.ss")
              (lambda (src)