Recover from empty verified read calls

ober

43455391416ea1b1a0bb01fb99366e3158317235

diff --git a/src/jcode/core/verified-run.ss b/src/jcode/core/verified-run.ss
index 1afa809..3512201 100644
--- a/src/jcode/core/verified-run.ss
+++ b/src/jcode/core/verified-run.ss
@@ -1606,6 +1606,9 @@
 
 (def default-read-start-limit 120)
 
+(def (missing-read-path-message)
+  "read requires a path. Do not retry empty read(). Use list(path=\".\") to inspect the current directory, or read(path=\"Makefile\"), read(path=\"README.md\"), read(path=\"AGENTS.md\"), or read(path=\"test/run-tests.sh\") for a specific file that exists. If you already understand the repo, use edit/write with the first complete draft, then verify().")
+
 (def (slice-content content args)
   (let* ((line-no (arg-int args "line" 0))
          (start-line (arg-int args "start" 0))
@@ -1634,7 +1637,8 @@
 
 (def (do-read-current args cwd)
   (let ((path (arg-path args #f)))
-    (if (not path) (error 'read "missing path arg")
+    (if (not path)
+      (raise-recoverable-tool-error (missing-read-path-message) 'read)
       (let ((p (abs-path cwd path)))
         (cond
           ((rejected-draft-read-message cwd path args) => (lambda (msg) msg))
diff --git a/test/run.ss b/test/run.ss
index b7a8df5..d7c94db 100644
--- a/test/run.ss
+++ b/test/run.ss
@@ -2351,6 +2351,44 @@
            (str-contains? s "Do not retry run/bash/shell")))))
 
 (let* ([vr-dir "/tmp"]
+       [target "jcode-verified-empty-read-recovery.txt"]
+       [target-path (string-append vr-dir "/" target)]
+       [tool-results '()]
+       [wf (coding-workflow (string-append "grep -q ok " target) vr-dir)]
+       [resp (scripted-responder
+               (list
+                 (list (make-wtool-call "read" '() #f))
+                 (list (make-wtool-call "read" '() #f))
+                 (list (make-wtool-call "read" '() #f))
+                 (list (make-wtool-call "edit" (list (cons "path" target)
+                                                      (cons "content" "ok\n")) #f))
+                 (list (make-wtool-call "verify" '() #f))
+                 (list (make-wtool-call "done" '(("summary" . "empty-read-recovered")) #f))))])
+  (safe-delete-test-file! target-path)
+  (let ([result (run-workflow wf "recover from empty read calls" resp
+                  (list (cons 'max-iterations 10)
+                        (cons 'max-tool-errors 0)
+                        (cons 'on-message
+                          (lambda (m)
+                            (when (equal? (message-role m) "tool")
+                              (set! tool-results
+                                (cons (message-content m) tool-results)))))))])
+    (check! "verified-run: repeated empty read recovers with zero hard errors"
+            result "empty-read-recovered")
+    (check-pred! "verified-run: empty read gives recoverable concrete path guidance"
+      (reverse tool-results)
+      (lambda (xs)
+        (let loop ([ys xs])
+          (cond
+            [(null? ys) #f]
+            [(and (str-contains? (car ys) "[ToolRecoverableError]")
+                  (str-contains? (car ys) "read requires a path")
+                  (str-contains? (car ys) "list(path=\".\")"))
+             #t]
+            [else (loop (cdr ys))])))))
+  (safe-delete-test-file! target-path))
+
+(let* ([vr-dir "/tmp"]
        [fixture "jcode-verified-run-budget-fixture.txt"]
        [fixture-path (string-append vr-dir "/" fixture)]
        [target "jcode-verified-run-budget-out.txt"]