fixes
ober
448ba99c43506d75ebd0f352a771cf231a2b6b47
--- 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 --- 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"))) --- 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") --- 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)"))) --- 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"))) --- 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 "