fixes

ober

448ba99c43506d75ebd0f352a771cf231a2b6b47

diff --git a/AGENTS.md b/AGENTS.md
index aba05ec..0f5b749 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -1,3 +1,34 @@
+## STOP: Editing `.ss`/`.sls` Files — Mandatory Rules
+
+These rules exist because local-model sessions have lost **hours** fighting
+parenthesis imbalances that these rules would have prevented in seconds.
+They override every habit from other editors and languages.
+
+1. **NEVER use `edit`, `write`, `sed`, `python`, `perl`, or `awk` to modify
+   `*.ss` or `*.sls` files.** Use the jerboa-mcp tools instead:
+   - Add a top-level form → `jerboa_balanced_insert` (anchor = one unique
+     complete form, e.g. the `def` above the insertion point).
+   - Replace exact text → `jerboa_balanced_replace`. It is **dry-run by
+     default** — pass `dry_run: false` to actually write.
+   - Create a whole new file → `jerboa_write_file` (use `verify: true` to
+     reject unbalanced content before it lands).
+2. **After EVERY `.ss` change, run `jerboa_check_balance` before building.**
+   The balanced tools check automatically; if you bypassed them, check now.
+3. **If a file ever becomes unbalanced, STOP.** Do NOT count parens by hand,
+   do NOT write paren-counting scripts, do NOT poke one character at a time.
+   Recovery is exactly one of:
+   - `git checkout -- <file>` and redo the edit with `jerboa_balanced_insert`
+     (preferred — one command, seconds), or
+   - `jerboa_repair_balance` (dry-run shows the plan; `apply: true` writes).
+4. **Keep closer-runs short.** Never hand-write code that ends in more than
+   ~4 consecutive closers (`))))]` runs). Flatten deep nesting with helper
+   `def`s, `let*`, or cond `=>` clauses so no edit ever depends on counting
+   a long `)` run.
+5. **`(def ...)` after an expression in a body is invalid.** Internal defines
+   must come first in a body, or use `let`/`let*`. The build error
+   "invalid context for definition" means you violated this — or a missing
+   paren above glued two top-level forms together (check balance first).
+
 ## The Jerboa Language — Quick Reference
 
 Jerboa is a Scheme dialect built on Chez Scheme. It is Gerbil-inspired but its own language. **All user-facing code is `.ss` files. Never write `.sls` files for the user** — those are internal implementation files.
@@ -429,6 +460,9 @@ Jerboa is a niche Scheme dialect with limited training data. **Never guess — a
 | FFI work | `jerboa_ffi_scaffold` / `jerboa_ffi_type_check` / `jerboa_ffi_null_safety` |
 | Port Gerbil code | `jerboa_migration_check` + `jerboa_translate_scheme` |
 | Detect paren imbalance | `jerboa_check_balance` — use BEFORE `make build` after deep edits |
+| Edit a `.ss` file | `jerboa_balanced_insert` / `jerboa_balanced_replace` — NEVER raw `edit`/`sed`/`python` (see top of file) |
+| Create a `.ss` file | `jerboa_write_file` — whole-file atomic write, `verify: true` rejects unbalanced content |
+| File already unbalanced | `jerboa_repair_balance` — dry-run repair plan; or `git checkout -- <file>` and redo with `balanced_insert` |
 | Full project audit | `jerboa_project_health_check` — balance, exports, cycles, duplicates |
 | Security audit | `jerboa_security_audit` + `jerboa_import_policy_check` |
 | Static build audit | `jerboa_static_symbol_audit` + `jerboa_boot_library_audit` |
@@ -448,7 +482,7 @@ Jerboa is a niche Scheme dialect with limited training data. **Never guess — a
 
 ### Code Generation & Refactoring
 
-`jerboa_rename_symbol`, `jerboa_balanced_replace`, `jerboa_wrap_form`, `jerboa_splice_form`, `jerboa_scaffold_test`, `jerboa_generate_module`, `jerboa_translate_scheme`, `jerboa_project_template`, `jerboa_httpd_handler_scaffold`, `jerboa_db_pattern_scaffold`, `jerboa_actor_ensemble_scaffold`
+`jerboa_rename_symbol`, `jerboa_balanced_replace`, `jerboa_balanced_insert`, `jerboa_write_file`, `jerboa_repair_balance`, `jerboa_wrap_form`, `jerboa_splice_form`, `jerboa_scaffold_test`, `jerboa_generate_module`, `jerboa_translate_scheme`, `jerboa_project_template`, `jerboa_httpd_handler_scaffold`, `jerboa_db_pattern_scaffold`, `jerboa_actor_ensemble_scaffold`
 
 ### Feature Suggestions
 
diff --git a/data/anti-patterns.sexp b/data/anti-patterns.sexp
index af8f81b..11908ff 100644
--- a/data/anti-patterns.sexp
+++ b/data/anti-patterns.sexp
@@ -744,21 +744,22 @@
      "jerboa_verify"))
  (("advice"
     .
-    "Run `jerboa_check_balance`, read the enclosing top-level form, and replace the whole broken span.")
+    "When a file is already unbalanced there are exactly two moves: (1) `git checkout -- <file>` to restore the last balanced state, then redo the change with jerboa_balanced_insert; or (2) jerboa_repair_balance to get a minimal repair plan (dry-run first, apply=true to write). Then re-run jerboa_check_balance before building.")
    ("avoid"
      .
-     "Do not repair unbalanced code with random one-character paren edits.")
+     "Do not repair unbalanced code with random one-character paren edits, and do not keep retrying jerboa_balanced_replace when it says \"Original file is not balanced\" — that tool cannot fix an already-broken file by design.")
    ("id" . "random-paren-pokes")
    ("kinds" "debug-error" "script" "module")
    ("pattern"
      .
-     "unexpected close|unexpected end|unmatched|invalid syntax")
-   ("severity" . "medium")
+     "unexpected close|unexpected end|unmatched|invalid syntax|not balanced")
+   ("severity" . "high")
    ("tags" "syntax" "paren" "balance" "repair" "replace-range")
    ("title" . "Random paren edits on unbalanced code")
    ("tools"
      "jerboa_check_balance"
-     "jerboa_read_forms"
+     "jerboa_repair_balance"
+     "jerboa_balanced_insert"
      "jerboa_failure_advisor"))
  (("advice"
     .
@@ -5139,4 +5140,44 @@
    ("title"
      .
      "lstat-then-act-by-pathname TOCTOU on filesystem operations")
-   ("tools" "jerboa_security_scan")))
+   ("tools" "jerboa_security_scan"))
+ (("advice"
+    .
+    "Route every .ss change through the jerboa-mcp editing tools: jerboa_balanced_insert to add top-level forms (anchor = one unique complete form), jerboa_balanced_replace for exact text (dry-run by default; pass dry_run=false to write), jerboa_write_file for new files (verify=true rejects unbalanced content). Run jerboa_check_balance before any build.")
+   ("avoid"
+     .
+     "Do not use the generic edit/write tools, sed, python, perl, or awk to modify *.ss or *.sls files. Hand-assembled old_string/new_string patches with long closer runs (\"))))])))\") are how single dropped parens turn into multi-hour repair sessions.")
+   ("id" . "plain-edit-on-scheme-source")
+   ("kinds" "module" "script" "refactor" "debug-error")
+   ("pattern" . "edit|write|sed|python|awk|patch|old_string")
+   ("severity" . "high")
+   ("tags" "editing" "paren" "balance" "ss" "mcp" "workflow")
+   ("title"
+     .
+     "Editing .ss files with raw edit/write/sed/python instead of balanced MCP tools")
+   ("tools"
+     "jerboa_balanced_insert"
+     "jerboa_balanced_replace"
+     "jerboa_write_file"
+     "jerboa_check_balance"))
+ (("advice"
+    .
+    "Let the tools count: jerboa_check_balance reports exact mismatch positions with recovery guidance. If the file is already unbalanced, run jerboa_repair_balance for a repair plan (dry-run first, apply=true to write), or restore with `git checkout -- <file>` and redo the edit with jerboa_balanced_insert. To avoid the situation, keep closer runs short: flatten deep nesting with helper defs, let*, or cond => clauses.")
+   ("avoid"
+     .
+     "Do not count closers by eye, trace paren depth character-by-character, or write python/sed paren-counting scripts. Ad-hoc counters mishandle strings, char literals, and mixed [ ] ( ) pairs, and repeatedly produce wrong answers that extend the repair loop.")
+   ("id" . "manual-paren-counting")
+   ("kinds" "debug-error" "module" "script")
+   ("pattern"
+     .
+     "unexpected close|unexpected end|unbalanced|unclosed|paren")
+   ("severity" . "high")
+   ("tags" "paren" "balance" "counting" "repair" "workflow"
+     "ss")
+   ("title"
+     .
+     "Hand-counting parens or writing ad-hoc paren-counter scripts")
+   ("tools"
+     "jerboa_check_balance"
+     "jerboa_repair_balance"
+     "jerboa_balanced_insert")))
diff --git a/data/changelog.sexp b/data/changelog.sexp
index df77eda..f6ce5d7 100644
--- a/data/changelog.sexp
+++ b/data/changelog.sexp
@@ -2,7 +2,19 @@
    .
    "Machine-readable changelog of Jerboa API drift. Consumers (LLM tooling, lints, jerboa_verify) use this to invalidate stale recommendations and to suggest migrations when a symbol is renamed or relocated.")
   ("entries"
-    (("added" ("raft-log-from" "raft-log-last-index"))
+    (("added" ("jerboa_write_file" "jerboa_repair_balance"))
+      ("date" . "2026-07-24")
+      ("modules_added")
+      ("moved")
+      ("notes"
+        .
+        "Parens-battle prevention wave. New jerboa_write_file writes a whole file atomically (create, or replace only with overwrite=true; verify=true rejects unbalanced/invalid content before writing; non-Scheme files skip verification). New jerboa_repair_balance is the escape hatch for an already-unbalanced .ss file: it computes a minimal repair plan (drop unexpected closers, append missing closers at EOF) as a dry run, and apply=true writes atomically. jerboa_check_balance and jerboa_balanced_replace failure messages now route to recovery (git checkout + balanced_insert, or repair_balance) instead of leaving agents to hand-count parens. AGENTS.md gained a top-of-file mandatory editing rule block: never raw edit/write/sed/python on *.ss/*.sls, always use the balanced MCP tools, keep closer runs short, and recover via git-restore or repair_balance rather than manual paren counting. Repo opencode.json denies the edit tool on *.ss/*.sls, and .opencode/plugin/guard-scheme-edits.ts blocks bash write-vectors (sed -i, perl -i, redirection, cp/mv/tee, python write-calls) against in-repo Scheme files with a redirect message to the balanced tools. Knowledge base: new anti-patterns plain-edit-on-scheme-source and manual-paren-counting, upgraded random-paren-pokes to high severity, new cookbook recipe recover-from-unbalanced-ss-file, new error fixes for 'Original file is not balanced' and 'invalid context for definition'.")
+      ("removed")
+      ("renamed")
+      ("tier_changes")
+      ("tools_added" ("jerboa_write_file" "jerboa_repair_balance"))
+      ("version" . "v0.2.5"))
+     (("added" ("raft-log-from" "raft-log-last-index"))
       ("date" . "2026-07-22")
       ("modules_added")
       ("moved")
diff --git a/data/cookbooks.sexp b/data/cookbooks.sexp
index 71886f7..d7bdb91 100644
--- a/data/cookbooks.sexp
+++ b/data/cookbooks.sexp
@@ -7126,4 +7126,15 @@
      "std-os-temp")
    ("title"
      .
-     "Secure temp files via (std os temp) — never hand-roll /tmp names")))
+     "Secure temp files via (std os temp) — never hand-roll /tmp names"))
+ (("code"
+    .
+    ";; Situation: a .ss file is unbalanced after an edit (build fails with\n;; \"unexpected close parenthesis\" or jerboa_check_balance reports errors).\n;; Do NOT hand-count parens and do NOT poke one character at a time.\n\n;; Move 1 (preferred): restore the last balanced state and redo the edit\n;; with the balanced tools so the imbalance never lands again.\n;;   $ git checkout -- path/to/file.ss\n;; Then re-apply the change:\n{\"tool\":\"jerboa_balanced_insert\",\"args\":{\"file_path\":\"path/to/file.ss\",\"anchor\":\"(def (existing-fn x)\\n  (+ x 1))\",\"form\":\"(def (new-fn y)\\n  (* y 2))\",\"position\":\"after\"}}\n\n;; Move 2: let the repair tool compute the minimal fix. Dry-run first to\n;; inspect the plan (which closers to drop / append), then apply.\n{\"tool\":\"jerboa_repair_balance\",\"args\":{\"file_path\":\"path/to/file.ss\"}}\n{\"tool\":\"jerboa_repair_balance\",\"args\":{\"file_path\":\"path/to/file.ss\",\"apply\":true}}\n\n;; Always finish with:\n{\"tool\":\"jerboa_check_balance\",\"args\":{\"file_path\":\"path/to/file.ss\"}}\n;; then the normal build (jerboa_make / make binary).") ("id" . "recover-from-unbalanced-ss-file") ("imports")
+   ("notes"
+     .
+     "git checkout + redo with balanced_insert is almost always faster and safer than any repair, because a paren-balanced file can still be semantically wrong (forms glued into the wrong parent). repair_balance is the escape hatch when there is no clean git state. Prevention: never use raw edit/sed/python on .ss files; keep closer runs <= 4 by flattening deep nesting with helper defs, let*, or cond => clauses.")
+   ("tags" "paren" "balance" "repair" "git" "balanced_insert"
+     "repair_balance" "workflow")
+   ("title"
+     .
+     "Recover when a .ss file becomes unbalanced (dropped/extra paren)")))
diff --git a/data/error-fixes.sexp b/data/error-fixes.sexp
index 173f552..30b4bf9 100644
--- a/data/error-fixes.sexp
+++ b/data/error-fixes.sexp
@@ -2028,15 +2028,12 @@
      "In Typed Jerboa, both if branches must have the SAME numeric type. Positive integer literals always infer as Nat and negative ones as Int, so (if test -1 1) mixes Int and Nat. Make every branch Int by computing the value from an Int operand — e.g. 1 as (/ n n), 0 as (- n n), or (+ -1 2) — or redesign with Nat-only values.")
    ("id" . "typed-branch-type-mismatch-nat-int")
    ("pattern" . "branch-type-mismatch") ("type" . "typecheck"))
- (("code_example"
+ (("explanation"
     .
-    ";; Bad: one extra close paren\n(def (clear-row! state row)\n  (dotimes (col board-width)\n    (debug-board-set! state col row #f))))\n\n;; Good\n(def (clear-row! state row)\n  (dotimes (col board-width)\n    (debug-board-set! state col row #f)))")
-   ("explanation"
-     .
-     "Chez/Jerboa reader errors like `Exception in read: unexpected close parenthesis at char ...` mean the reader encountered a `)` that closes no open delimiter. In generated-code ladder work this can be a one-character copy error in a long otherwise-valid implementation shape. A separate prompt snippet showing the exact fragile form can prevent recurrence.")
+    "The Chez/Jerboa reader found a `)` that closes no open delimiter — usually a one-character copy error from a raw edit/sed/python change on a long closer run. Hand-repair of the same class of error repeatedly costs hours; tool-routed recovery costs seconds.")
    ("fix"
      .
-     "Run `jerboa_check_balance` on the generated file and inspect the reported line. For AI-copied code blocks, compare the local form with the verified reference shape and prefer replacing the whole malformed top-level form or file instead of making repeated tiny edits. In `jcode verified`, continue with `read`, `edit`, and `verify`; do not switch to unavailable shell/run tools.")
+     "Run jerboa_check_balance on the file to get exact mismatch positions. Then exactly one recovery move: (1) `git checkout -- <file>` and redo the change with jerboa_balanced_insert (preferred), or (2) jerboa_repair_balance (dry-run shows the drop/append plan, apply=true writes). Do NOT count parens by hand, write paren-counting scripts, or make repeated tiny edits. For AI-copied code blocks, replace the whole malformed top-level form instead.")
    ("id" . "unexpected-close-paren-copy-error")
    ("pattern" . "unexpected close parenthesis")
    ("type" . "syntax"))
@@ -2928,4 +2925,22 @@
    ("pattern"
      .
      "Exception in read-json: expected low surrogate after high surrogate")
-   ("type" . "runtime")))
+   ("type" . "runtime"))
+ (("explanation"
+    .
+    "balanced_replace validates the original file before editing, so an earlier bad edit (usually a raw edit/sed change that dropped a paren) blocks all further balanced edits until the file is repaired or restored.")
+   ("fix"
+     .
+     "jerboa_balanced_replace refuses because the file is ALREADY unbalanced; it cannot fix broken files. Two moves: (1) `git checkout -- <file>` then redo the edit with jerboa_balanced_insert; or (2) jerboa_repair_balance (dry-run shows the plan, apply=true writes). Do not poke parens one char at a time or use sed/python.")
+   ("id" . "original-file-not-balanced")
+   ("pattern" . "Original file is not balanced")
+   ("type" . "mcp-tool"))
+ (("explanation"
+    .
+    "Chez only permits internal defines at the start of a body. A dropped closer in the preceding form makes the following top-level def look nested, producing this exact error far from the real mistake.")
+   ("fix"
+     .
+     "Internal (def ...) appeared where only expressions are allowed. Most often a missing closing paren ABOVE glued the next top-level form into the previous form's body. Run jerboa_check_balance first; if unbalanced, restore with `git checkout -- <file>` and redo with jerboa_balanced_insert, or use jerboa_repair_balance. If balanced, move the def to the start of the body or rewrite as let/let*.")
+   ("id" . "invalid-context-for-definition-glued-forms")
+   ("pattern" . "invalid context for definition")
+   ("type" . "compile")))
diff --git a/mcp/server.ss b/mcp/server.ss
index ad477a7..e1b9458 100644
--- a/mcp/server.ss
+++ b/mcp/server.ss
@@ -4088,7 +4088,126 @@
         (let ([errors (balance-scan (cdr src))])
           (if (null? errors)
               (text-result "Balance OK.")
-              (text-result (string-append "Unbalanced delimiters:\n\n" (string-join errors "\n")) #t))))))
+              (text-result
+               (string-append
+                "Unbalanced delimiters:\n\n" (string-join errors "\n")
+                "\n\nRecovery (do NOT poke parens one char at a time, do NOT use sed/python):\n"
+                "1. If you just edited this file: `git checkout -- <file>` and redo the edit with jerboa_balanced_insert.\n"
+                "2. Otherwise run jerboa_repair_balance for a repair plan (dry-run first, apply=true to write).")
+               #t))))))
+(def (count-top-level-forms source)
+  (let loop ([lines (string-split source #\newline)] [n 0])
+    (if (null? lines)
+        n
+        (loop (cdr lines)
+              (if (and (> (string-length (car lines)) 0)
+                       (char=? (string-ref (car lines) 0) #\())
+                  (+ n 1)
+                  n)))))
+
+(def (repair-balance-plan source)
+  (def out (open-output-string))
+  (def stack '())
+  (def deletions '())
+  (def line 1)
+  (def col 1)
+  (def len (string-length source))
+  (let loop ([i 0] [in-string #f] [escaped #f] [comment #f])
+    (when (< i len)
+      (let ([ch (string-ref source i)])
+        (cond
+          [(char=? ch #\newline)
+           (set! line (+ line 1))
+           (set! col 1)
+           (write-char ch out)
+           (loop (+ i 1) in-string #f #f)]
+          [comment
+           (set! col (+ col 1))
+           (write-char ch out)
+           (loop (+ i 1) in-string #f comment)]
+          [in-string
+           (set! col (+ col 1))
+           (write-char ch out)
+           (cond
+             [escaped (loop (+ i 1) #t #f #f)]
+             [(char=? ch #\\) (loop (+ i 1) #t #t #f)]
+             [(char=? ch #\") (loop (+ i 1) #f #f #f)]
+             [else (loop (+ i 1) #t #f #f)])]
+          [(char=? ch #\;)
+           (set! col (+ col 1))
+           (write-char ch out)
+           (loop (+ i 1) #f #f #t)]
+          [(char=? ch #\")
+           (set! col (+ col 1))
+           (write-char ch out)
+           (loop (+ i 1) #t #f #f)]
+          [(and (char=? ch #\#)
+                (< (+ i 1) len)
+                (char=? (string-ref source (+ i 1)) #\\))
+           (let ([j (char-literal-end source i)])
+             (display (substring source i j) out)
+             (set! col (+ col (- j i)))
+             (loop j #f #f #f))]
+          [(or (char=? ch #\() (char=? ch #\[) (char=? ch #\{))
+           (set! stack (cons (list ch line col) stack))
+           (set! col (+ col 1))
+           (write-char ch out)
+           (loop (+ i 1) #f #f #f)]
+          [(or (char=? ch #\)) (char=? ch #\]) (char=? ch #\}))
+           (if (or (null? stack) (not (char=? (caar stack) (matching-opener ch))))
+               (set! deletions
+                     (cons (string-append (string ch) " at "
+                                          (number->string line) ":" (number->string col))
+                           deletions))
+               (begin
+                 (set! stack (cdr stack))
+                 (write-char ch out)))
+           (set! col (+ col 1))
+           (loop (+ i 1) #f #f #f)]
+          [else
+           (set! col (+ col 1))
+           (write-char ch out)
+           (loop (+ i 1) #f #f #f)]))))
+  (let ([insertions (map (lambda (entry) (matching-closer (car entry))) stack)])
+    (for-each (lambda (c) (write-char c out)) insertions)
+    (list (get-output-string out) (reverse deletions) insertions)))
+
+(def (tool-repair-balance args)
+  (def file (hash-get* args "file_path" #f))
+  (def apply? (hash-get* args "apply" #f))
+  (def source (and file (guard (e [else #f]) (read-file-string file))))
+  (cond
+    [(not file) (text-result "file_path is required." #t)]
+    [(not source) (text-result (string-append "Cannot read " file) #t)]
+    [(null? (balance-scan source))
+     (text-result (string-append "Balance OK: " file " is already balanced. No repair needed."))]
+    [else
+     (let* ([plan (repair-balance-plan source)]
+            [repaired (car plan)]
+            [deletions (cadr plan)]
+            [insertions (caddr plan)]
+            [remaining (balance-scan repaired)]
+            [summary
+             (string-append
+              "Repair plan for " file ":\n"
+              "- drop " (number->string (length deletions)) " unexpected closer(s)"
+              (if (null? deletions) "" (string-append ": " (string-join deletions ", "))) "\n"
+              "- append " (number->string (length insertions)) " missing closer(s) at EOF: "
+              (list->string insertions) "\n"
+              "- top-level forms: " (number->string (count-top-level-forms source))
+              " -> " (number->string (count-top-level-forms repaired)) "\n"
+              (if (null? remaining)
+                  "- post-repair balance: OK\n"
+                  (string-append "- post-repair balance: STILL BROKEN:\n" (string-join remaining "\n") "\n"))
+              "\nIf this plan looks wrong, restore with `git checkout -- " file "` and redo the edit with jerboa_balanced_insert.\n")])
+       (cond
+         [(and apply? (null? remaining))
+          (write-file-string/atomic file repaired)
+          (text-result (string-append summary "\nApplied: repaired file written atomically."))]
+         [apply?
+          (text-result (string-append summary "\nNOT applied: repair did not achieve balance; restore from git.") #t)]
+         [else
+          (text-result (string-append summary "\nDry run only: pass apply=true to write the repaired file."))]))]))
 
 (def (line-depths source)
   (def depth 0)
@@ -4480,7 +4599,12 @@
              [after-errors (and updated (balance-scan updated))])
         (cond
           [(not source) (text-result (string-append "Cannot read " file) #t)]
-          [(not (null? before-errors)) (text-result "Original file is not balanced." #t)]
+          [(not (null? before-errors))
+           (text-result
+            (string-append
+             "Original file is not balanced. Stop: do NOT repair with one-character paren edits, sed, or python.\n"
+             "Run jerboa_repair_balance for a repair plan, or `git checkout -- " file "` and redo the edit with jerboa_balanced_insert.")
+            #t)]
           [(not (null? after-errors)) (text-result (string-append "Replacement would break balance:\n\n" (string-join after-errors "\n")) #t)]
           [(not (string-contains source old)) (text-result "old_string not found." #t)]
           [else
@@ -6651,6 +6775,9 @@
     "jerboa_verify"
     "jerboa_verify_changes"
     "jerboa_check_balance"
+    "jerboa_write_file"
+    "jerboa_repair_balance"
+    "jerboa_balanced_insert"
     "jerboa_security_scan"
     "jerboa_run_tests"
     "jerboa_make"
@@ -6681,7 +6808,10 @@
     [(string=? name "jerboa_script_scaffold_verify") "generate and smoke-test tiny CLI scripts"]
     [(string=? name "jerboa_verify") "syntax/compile check changed .ss code"]
     [(string=? name "jerboa_verify_changes") "compile + security + optional tests summary"]
-    [(string=? name "jerboa_check_balance") "delimiter mismatch check after edits"]
+    [(string=? name "jerboa_check_balance") "delimiter mismatch check after edits; on failure use repair/git-restore, never hand-counting"]
+    [(string=? name "jerboa_write_file") "idiot-proof whole-file write for new .ss files"]
+    [(string=? name "jerboa_repair_balance") "escape hatch when a .ss file is already unbalanced"]
+    [(string=? name "jerboa_balanced_insert") "add top-level forms to .ss files instead of Edit/sed/python"]
     [(string=? name "jerboa_security_scan") "file/process/network/FFI/security changes"]
     [(string=? name "jerboa_run_tests") "focused Jerboa tests"]
     [(string=? name "jerboa_make") "repo Makefile targets"]
@@ -6840,6 +6970,62 @@
     [else
      (write-file-string file code)
      "written"]))
+(def (mkdir-p dir)
+  (unless (or (string=? dir ".") (string=? dir "/") (string=? dir ""))
+    (let ([parent (path-dirname-local dir)])
+      (mkdir-p parent)
+      (unless (file-directory? dir)
+        (mkdir dir)))))
+
+(def (scheme-source-path? path)
+  (or (string-suffix? ".ss" path)
+      (string-suffix? ".sls" path)
+      (string-suffix? ".scm" path)))
+
+(def (tool-write-file-verify args content project)
+  (let ([balance (tool-check-balance (jhash "code" content))]
+        [compile (tool-verify (jhash "code" content
+                                     "project_path" project
+                                     "extra_libdirs" (hash-get* args "extra_libdirs" '())
+                                     "jerboa_home" (hash-get* args "jerboa_home" #f)
+                                     "skip_prescan" #t))])
+    (if (or (hash-ref balance "isError") (hash-ref compile "isError"))
+        (list balance compile)
+        #f)))
+
+(def (tool-write-file-run args full-path content overwrite? create-dirs?)
+  (def parent (path-dirname-local full-path))
+  (when (and create-dirs? (> (string-length parent) 0) (not (file-directory? parent)))
+    (mkdir-p parent))
+  (write-file-string/atomic full-path content)
+  (text-result
+   (string-append "Wrote " full-path "\n"
+                  "Bytes: " (number->string (string-length content))
+                  (if overwrite? " (overwrite)" " (new file)"))))
+
+(def (tool-write-file args)
+  (def file (hash-get* args "file_path" #f))
+  (def content (hash-get* args "content" #f))
+  (def overwrite? (hash-get* args "overwrite" #f))
+  (def create-dirs? (hash-get* args "create_dirs" #f))
+  (def verify? (hash-get* args "verify" #f))
+  (def project (and file (tool-project-path args file)))
+  (def full-path (if (and project file) (project-file-path project file) file))
+  (cond
+    [(not file) (text-result "file_path is required." #t)]
+    [(not content) (text-result "content is required." #t)]
+    [(and (file-exists? full-path) (not overwrite?))
+     (text-result (string-append "Write blocked: " full-path " already exists. Pass overwrite=true to replace it.") #t)]
+    [(and verify? (scheme-source-path? full-path)
+          (tool-write-file-verify args content project))
+     => (lambda (failed)
+          (text-result
+           (string-append "Write blocked: verification failed for " full-path ".\n\n"
+                          "Balance check:\n" (result-text (car failed)) "\n\n"
+                          "Compile check:\n" (result-text (cadr failed)))
+           #t))]
+    [else
+     (tool-write-file-run args full-path content overwrite? create-dirs?)]))
 
 (def (tool-script-scaffold-verify args)
   (let* ([task (hash-get* args "task" (hash-get* args "description" ""))]
@@ -9078,7 +9264,7 @@
                          '("old_name" "new_name"))
                  tool-rename-symbol #f '())
   (register-tool "jerboa_balanced_replace" "Balanced Replace"
-                 "Replace text only if delimiter balance remains valid."
+                 "Replace text only if delimiter balance remains valid. Prefer this over raw Edit/sed/python for .ss files. If it reports the ORIGINAL file is unbalanced, use jerboa_repair_balance or git-checkout first — never manual paren edits."
                  (schema (list (list "file_path" (property "string" "File path"))
                                (list "old_string" (property "string" "Old string"))
                                (list "new_string" (property "string" "New string"))
@@ -9086,7 +9272,7 @@
                          '("file_path" "old_string" "new_string"))
                  tool-balanced-replace #t '())
   (register-tool "jerboa_balanced_insert" "Balanced Insert"
-                 "Insert a complete Scheme form before or after a unique balanced anchor form."
+                 "Insert a complete Scheme form before or after a unique balanced anchor form. THE tool for adding top-level definitions to .ss files — use it INSTEAD of Edit/sed/python so a dropped paren can never land."
                  (schema (list (list "file_path" (property "string" "File path"))
                                (list "anchor" (property "string" "Unique complete form to match"))
                                (list "form" (property "string" "Complete form to insert"))
@@ -9682,6 +9868,24 @@
                                (list "jerboa_home" (property "string" "Jerboa home")))
                          '("file_path"))
                  tool-bisect-crash #f '())
+  (register-tool "jerboa_write_file" "Write File"
+                 "Write a whole file atomically (create, or replace only with overwrite=true). Idiot-proof: no anchors, no patches, no old_string matching. Use THIS to create new .ss files; verify=true rejects unbalanced/invalid content before writing."
+                 (schema (list (list "file_path" (property "string" "Path to write"))
+                               (list "content" (property "string" "Full content to write"))
+                               (list "overwrite" (property "boolean" "Allow replacing existing file"))
+                               (list "create_dirs" (property "boolean" "Create missing parent directories"))
+                               (list "verify" (property "boolean" "Run balance/compile checks before writing"))
+                               (list "project_path" (property "string" "Project path for relative paths and libdirs"))
+                               (list "extra_libdirs" (property "array" "Additional Scheme libdirs"))
+                               (list "jerboa_home" (property "string" "Jerboa home")))
+                         '("file_path" "content"))
+                 tool-write-file #t '("write_file"))
+  (register-tool "jerboa_repair_balance" "Repair Balance"
+                 "Escape hatch when a .ss file is ALREADY unbalanced: show a minimal repair plan (drop unexpected closers, append missing closers). Dry-run by default; apply=true writes. Never hand-count parens: use this, or `git checkout` the file and redo the edit with jerboa_balanced_insert."
+                 (schema (list (list "file_path" (property "string" "File path"))
+                               (list "apply" (property "boolean" "Write the repaired file (default dry-run)")))
+                         '("file_path"))
+                 tool-repair-balance #t '("repair_balance"))
   (register-tool "jerboa" "Jerboa Tool Dispatcher"
                  (string-append
                   "Compact dispatcher for the full Jerboa MCP catalog. The schema intentionally "