Expose concrete verified tool schemas

ober

edf95f837cec13920c4850d2454597cebf0b76b0

diff --git a/src/jcode/core/verified-run.ss b/src/jcode/core/verified-run.ss
index 36e4372..8b991d5 100644
--- a/src/jcode/core/verified-run.ss
+++ b/src/jcode/core/verified-run.ss
@@ -3412,9 +3412,148 @@
 	                               "")))))))))
   )
 
-;; Minimal JSON-Schema object; the per-arg contract rides in each tool's
-;; description (nested schema alists don't serialize cleanly, descriptions do).
-(def *obj-schema* '(("type" . "object")))
+(def (schema-object pairs)
+  (let ((ht (make-hash-table)))
+    (for-each
+      (lambda (kv) (hash-put! ht (car kv) (cdr kv)))
+      pairs)
+    ht))
+
+(def *obj-schema* (schema-object (list (cons "type" "object"))))
+(def *empty-obj-schema* (schema-object (list (cons "type" "object"))))
+
+(def (string-schema desc)
+  (schema-object
+    (list (cons "type" "string")
+          (cons "description" desc))))
+
+(def (number-schema desc)
+  (schema-object
+    (list (cons "type" "number")
+          (cons "description" desc))))
+
+(def (script-kind-schema)
+  (schema-object
+    (list (cons "type" "string")
+          (cons "enum" (list "minimal-pass" "cli-two-args" "vector-grid"))
+          (cons "description" "Generic starter kind"))))
+
+(def (path-schema required?)
+  (schema-object
+    (list (cons "type" "object")
+          (cons "properties"
+                (schema-object
+                  (list (cons "path"
+                              (string-schema "Path inside the current repository")))))
+          (cons "required" (if required? (list "path") '())))))
+
+(def *read-schema*
+  (schema-object
+    (list (cons "type" "object")
+          (cons "properties"
+                (schema-object
+                  (list (cons "path" (string-schema "File path to read"))
+                        (cons "line" (number-schema "1-based line number to start at"))
+                        (cons "start" (number-schema "1-based start line"))
+                        (cons "end" (number-schema "1-based end line"))
+                        (cons "offset" (number-schema "Character offset"))
+                        (cons "limit" (number-schema "Maximum characters to return")))))
+          (cons "required" (list "path")))))
+
+(def *line-read-schema*
+  (schema-object
+    (list (cons "type" "object")
+          (cons "properties"
+                (schema-object
+                  (list (cons "path" (string-schema "File path to inspect"))
+                        (cons "file" (string-schema "Alias for path"))
+                        (cons "lines" (number-schema "Number of lines")))))
+          (cons "required" (list "path")))))
+
+(def *script-schema*
+  (schema-object
+    (list (cons "type" "object")
+          (cons "properties"
+                (schema-object
+                  (list (cons "path"
+                              (string-schema "New .ss script path, for example life.ss"))
+                        (cons "kind" (script-kind-schema)))))
+          (cons "required" (list "path")))))
+
+(def *run-alias-schema*
+  (schema-object
+    (list (cons "type" "object")
+          (cons "properties"
+                (schema-object
+                  (list (cons "command"
+                              (string-schema "Simple ls/cat/head/tail/wc/grep/rg inspection command")))))
+          (cons "required" (list "command")))))
+
+(def *edit-schema*
+  (schema-object
+    (list (cons "type" "object")
+          (cons "properties"
+                (schema-object
+                  (list (cons "path" (string-schema "File path to edit, for example life.ss"))
+                        (cons "file" (string-schema "Alias for path"))
+                        (cons "content" (string-schema "Complete new file contents, or replacement text with old_str"))
+                        (cons "contents" (string-schema "Alias for content"))
+                        (cons "old_str" (string-schema "Exact existing text to replace"))
+                        (cons "new_str" (string-schema "Replacement text"))
+                        (cons "line" (number-schema "1-based line number for line replacement")))))
+          (cons "required" (list "path")))))
+
+(def *write-schema*
+  (schema-object
+    (list (cons "type" "object")
+          (cons "properties"
+                (schema-object
+                  (list (cons "path" (string-schema "File path to write, for example life.ss"))
+                        (cons "file" (string-schema "Alias for path"))
+                        (cons "content" (string-schema "Complete file contents"))
+                        (cons "contents" (string-schema "Alias for content")))))
+          (cons "required" (list "path" "content")))))
+
+(def *line-edit-schema*
+  (schema-object
+    (list (cons "type" "object")
+          (cons "properties"
+                (schema-object
+                  (list (cons "path" (string-schema "File path to edit"))
+                        (cons "file" (string-schema "Alias for path"))
+                        (cons "line" (number-schema "1-based line number"))
+                        (cons "content" (string-schema "Replacement line content")))))
+          (cons "required" (list "path" "line" "content")))))
+
+(def *replace-def-schema*
+  (schema-object
+    (list (cons "type" "object")
+          (cons "properties"
+                (schema-object
+                  (list (cons "path" (string-schema "File path to edit"))
+                        (cons "name" (string-schema "Top-level function name"))
+                        (cons "content" (string-schema "Complete replacement definition")))))
+          (cons "required" (list "path" "name" "content")))))
+
+(def *replace-range-schema*
+  (schema-object
+    (list (cons "type" "object")
+          (cons "properties"
+                (schema-object
+                  (list (cons "path" (string-schema "File path to edit"))
+                        (cons "start" (number-schema "1-based inclusive start line"))
+                        (cons "end" (number-schema "1-based inclusive end line"))
+                        (cons "content" (string-schema "Replacement content for the range"))
+                        (cons "new_str" (string-schema "Alias for content")))))
+          (cons "required" (list "path" "start" "end" "content")))))
+
+(def *done-schema*
+  (schema-object
+    (list (cons "type" "object")
+          (cons "properties"
+                (schema-object
+                  (list (cons "summary" (string-schema "Short completion summary")))))
+          (cons "required" (list "summary")))))
 
 (def (scope-instruction scope)
   (if scope
@@ -3602,31 +3741,31 @@
 	          (make-tool-def
             (make-tool-spec "read"
               "Read a file's current contents. args: {\"path\": string, optional \"line\"/\"start\" and \"end\" line numbers, or \"offset\"/\"limit\"}."
-              *obj-schema*)
+              *read-schema*)
             (scoped-tool (lambda (args) (do-read args cwd))) '()))
         (list-def
           (make-tool-def
             (make-tool-spec "list"
               "List a directory under the verified working directory. args: {\"path\": string, optional; default \".\"}. Read-only."
-              *obj-schema*)
+              (path-schema #f))
             (scoped-tool (lambda (args) (do-list args cwd))) '()))
         (ls-def
           (make-tool-def
             (make-tool-spec "ls"
               "Alias for list. args: {\"path\": string, optional; default \".\"}. Read-only."
-              *obj-schema*)
+              (path-schema #f))
             (scoped-tool (lambda (args) (do-list args cwd))) '()))
         (cat-def
           (make-tool-def
             (make-tool-spec "cat"
               "Alias for read. args: {\"path\": string}."
-              *obj-schema*)
+              *read-schema*)
             (scoped-tool (lambda (args) (do-read args cwd))) '()))
         (tail-def
           (make-tool-def
             (make-tool-spec "tail"
               "Return the last lines of a file. args: {\"path\": string, \"lines\": number optional}. \"file\" is accepted as a path alias."
-              *obj-schema*)
+              *line-read-schema*)
             (scoped-tool (lambda (args)
               (let ((path (arg-path args #f)))
                 (if path
@@ -3639,7 +3778,7 @@
           (make-tool-def
             (make-tool-spec "head"
               "Return the first lines of a file. args: {\"path\": string, \"lines\": number optional}. \"file\" is accepted as a path alias."
-              *obj-schema*)
+              *line-read-schema*)
             (scoped-tool (lambda (args)
               (let ((path (arg-path args #f)))
                 (if path
@@ -3652,7 +3791,7 @@
           (make-tool-def
             (make-tool-spec "wc"
               "Return a file line count. args: {\"path\": string}. \"file\" is accepted as a path alias."
-              *obj-schema*)
+              (path-schema #t))
             (scoped-tool (lambda (args)
               (let ((path (arg-path args #f)))
                 (if path
@@ -3665,13 +3804,13 @@
           (make-tool-def
             (make-tool-spec "balance"
               "Check delimiter balance for a file and report unmatched paren/bracket/brace line and column. args: {\"path\": string}. Use after syntax errors such as unexpected close parenthesis."
-              *obj-schema*)
+              (path-schema #t))
             (scoped-tool (lambda (args) (balance-file args cwd))) '()))
         (create-script-def
           (make-tool-def
             (make-tool-spec "create_verified_jerboa_script"
               "Create a small executable Jerboa .ss starter through the verified edit path. args: {\"path\": string ending .ss, \"kind\": \"minimal-pass\" | \"cli-two-args\" | \"vector-grid\"}. Use only when a generic template clearly matches: minimal-pass for trivial scripts, cli-two-args for simple numeric two-argument CLIs, vector-grid for 2-D vector/grid examples. For nontrivial algorithms, you may skip scaffolding and write a small complete first version, then verify and expand deterministically."
-              *obj-schema*)
+              *script-schema*)
             (scoped-tool
               (lambda (args) (create-verified-jerboa-script args cwd)))
             '()))
@@ -3681,7 +3820,7 @@
 	              (if run-aliases?
 	                "Safe alias for simple shell habits: supports ls/cat/head/tail/wc/grep/rg, plain file read, plain directory list, and mkdir -p PATH under write scope. Arbitrary shell is unavailable. Use verify for the configured build/test command."
 	                "Guidance-only disabled shell alias. Do not retry run. Use verify for the configured build/test command and edit/line_edit/replace_def/replace_range for repairs.")
-	              *obj-schema*)
+	              *run-alias-schema*)
 	            (scoped-tool (lambda (args)
 	              (if run-aliases?
 	                (do-run-alias args cwd)
@@ -3693,7 +3832,7 @@
 	              (if run-aliases?
 	                "Safe alias for simple shell habits: supports ls/cat/head/tail/wc/grep/rg, plain file read, plain directory list, and mkdir -p PATH under write scope. Arbitrary shell is unavailable. Use verify for the configured build/test command."
 	                "Guidance-only disabled shell alias. Do not retry bash. Use verify for the configured build/test command and edit/line_edit/replace_def/replace_range for repairs.")
-	              *obj-schema*)
+	              *run-alias-schema*)
 	            (scoped-tool (lambda (args)
 	              (if run-aliases?
 	                (do-run-alias args cwd)
@@ -3705,7 +3844,7 @@
 	              (if run-aliases?
 	                "Safe alias for simple shell habits: supports ls/cat/head/tail/wc/grep/rg, plain file read, plain directory list, and mkdir -p PATH under write scope. Arbitrary shell is unavailable. Use verify for the configured build/test command."
 	                "Guidance-only disabled shell alias. Do not retry shell. Use verify for the configured build/test command and edit/line_edit/replace_def/replace_range for repairs.")
-	              *obj-schema*)
+	              *run-alias-schema*)
 	            (scoped-tool (lambda (args)
 	              (if run-aliases?
 	                (do-run-alias args cwd)
@@ -3715,19 +3854,19 @@
           (make-tool-def
             (make-tool-spec "edit"
               "Edit a file. Full write args: {\"path\": string, \"content\": string}; \"file\", \"filename\", \"file_path\", \"filepath\", \"target\", and \"target_path\" are accepted as path aliases. \"file\" is still accepted as a content alias when a separate path is present. \"contents\", \"new_content\", \"body\", and \"text\" are accepted as content aliases. Exact replacement args: {\"path\": string, \"old_str\": string, \"new_str\": string}; old_string/new_string are accepted aliases; \"content\" may be used instead of \"new_str\" when \"old_str\" is present. Line replacement args: {\"path\": string, \"line\": number, \"content\": string}."
-              *obj-schema*)
+              *edit-schema*)
             (scoped-tool (lambda (args) (do-edit args cwd))) '()))
         (write-def
           (make-tool-def
             (make-tool-spec "write"
               "Alias for edit. Full write args: {\"path\": string, \"content\": string}. Exact replacement accepts old_str/new_str or old_string/new_string. \"file\" is accepted as a path alias and content/file/body/text aliases are accepted like edit."
-              *obj-schema*)
+              *write-schema*)
             (scoped-tool (lambda (args) (do-edit args cwd))) '()))
         (line-edit-def
           (make-tool-def
             (make-tool-spec "line_edit"
               "Replace exactly one line in a file. args: {\"path\": string, \"line\": number, \"content\": string}. \"file\" is accepted as a path alias. Use this for compiler errors or verifier diagnostics with stable line numbers."
-              *obj-schema*)
+              *line-edit-schema*)
             (scoped-tool (lambda (args)
               (let ((path (arg-path args #f))
                     (content (arg-content args))
@@ -3748,14 +3887,14 @@
           (make-tool-def
             (make-tool-spec "replace_def"
               "Replace a whole top-level function definition by name using delimiter matching. args: {\"path\": string, \"name\": string, \"content\": string}. Use this when a verifier error is inside one function and exact old_str replacement is brittle."
-              *obj-schema*)
+              *replace-def-schema*)
             (scoped-tool (lambda (args) (replace-definition args cwd)))
             '()))
         (replace-range-def
           (make-tool-def
             (make-tool-spec "replace_range"
               "Replace an inclusive line range, even if the old code is unbalanced. args: {\"path\": string, \"start\": number, \"end\": number, \"content\": string}. Replacement aliases accepted: new_content, new_str, new_string, replacement, body, text. Path/start/end without content does nothing. Use this after balance/read/sed identifies a broken span and replace_def cannot parse it."
-              *obj-schema*)
+              *replace-range-schema*)
             (scoped-tool (lambda (args) (replace-range args cwd)))
             '()))
         (verify-def
@@ -3764,7 +3903,7 @@
               (string-append "Run the build/tests (" verify-cmd "). No args. On failure it "
                              "raises with the error output — read it, fix the code with edit, "
                              "then call verify again.")
-              *obj-schema*)
+              *empty-obj-schema*)
             (make-verify-callable
 	              (scoped-tool (lambda (args)
 	                (let ((pending
@@ -3784,7 +3923,7 @@
           (make-tool-def
             (make-tool-spec "done"
               "Finish the task. Only call AFTER verify has passed. args: {\"summary\": string}."
-              *obj-schema*)
+              *done-schema*)
             (scoped-tool (lambda (args) (arg-ref args "summary" "done")))
             '())))
     (make-workflow
diff --git a/test/run.ss b/test/run.ss
index 5ce72bb..ad19b7d 100644
--- a/test/run.ss
+++ b/test/run.ss
@@ -4359,7 +4359,23 @@
 	      (lambda (s)
 	        (and (str-contains? s "create_verified_jerboa_script")
 	             (str-contains? s "optional")
-	             (not (str-contains? s "life-blinker"))))))
+	             (not (str-contains? s "life-blinker")))))
+	    (check-pred! "verified-run: edit schema advertises concrete args"
+	      (tool-spec-parameters
+	        (tool-def-spec (workflow-get-tool-def wf "edit")))
+	      (lambda (schema)
+	        (let ([props (hashtable-ref schema "properties" #f)]
+	              [required (hashtable-ref schema "required" '())])
+	          (and (hashtable-ref props "path" #f)
+	               (hashtable-ref props "content" #f)
+	               (member "path" required)))))
+	    (check-pred! "verified-run: write schema requires path and content"
+	      (tool-spec-parameters
+	        (tool-def-spec (workflow-get-tool-def wf "write")))
+	      (lambda (schema)
+	        (let ([required (hashtable-ref schema "required" '())])
+	          (and (member "path" required)
+	               (member "content" required))))))
 	  (safe-delete-test-file! target-path))
 
 	(let* ([vr-dir  "/tmp"]