#+PROPERTY: header-args:lisp :tangle (concat (getenv "INSTALL_DIR") "/skills/org-skill-self-edit.lisp" (expand-file-name "")) :PROPERTIES: :ID: self-edit-001 :END: #+TITLE: SKILL: Self-Edit Agent #+STARTUP: content #+FILETAGS: :self-repair:autonomy:editing: * Overview The *Self-Edit Agent* enables the agent to modify its own code and files with safety guarantees. It handles: 1. Syntax errors - auto-balance parens, then LLM fix 2. File modifications - surgical edits with memory rollback on failure 3. Skill hot-reload - swap compiled skills without breaking the system * Phase D: Build (Implementation) ** Package Context #+begin_src lisp (in-package :opencortex) #+end_src ** Deterministic Paren Repair Fast paren balancing for syntax errors. #+begin_src lisp (defun self-edit-count-char (char string) "Counts occurrences of CHAR in STRING." (loop for c across string count (char= c char))) (defun self-edit-balance-parens (code) "Balances parentheses in CODE." (let ((opens (self-edit-count-char #\( code)) (closes (self-edit-count-char #\) code))) (cond ((= opens closes) code) ((> opens closes) (concatenate 'string code (make-string (- opens closes) :initial-element #\)))) ((> closes opens) (concatenate 'string (make-string (- closes opens) :initial-element #\() code))))) (defun copy-hash-table (table) "Returns a shallow copy of a hash table." (let ((new-table (make-hash-table :test (hash-table-test table) :size (hash-table-count table)))) (maphash (lambda (k v) (setf (gethash k new-table) v)) table) new-table)) #+end_src ** Parse Target Location Extract file and line info from error context. #+begin_src lisp (defun self-edit-parse-location (context) "Extracts file and line from error context payload." (let* ((payload (getf context :payload)) (message (getf payload :message "")) (file (or (getf payload :file) (when (search "file" message) (car (cl-ppcre:all-matches-as-strings "[a-zA-Z0-9_/-]+\\.lisp" message))))) (line (or (getf payload :line) (let ((match (cl-ppcre:scan-to-strings "line.?(\\d+)" message))) (when match (parse-integer (aref match 0))))))) (list :file file :line line))) #+end_src ** Apply Surgical Edit Apply a find/replace to a file with rollback on failure. #+begin_src lisp (defun self-edit-apply (target-file old-code new-code) "Applies surgical edit to TARGET-FILE: replace OLD-CODE with NEW-CODE. Returns list with :status and :message keys." (unless (uiop:file-exists-p target-file) (return-from self-edit-apply (list :status :error :message (format nil "File not found: ~a" target-file)))) (snapshot-memory) (harness-log "SELF-EDIT: Attempting surgical fix on ~a..." target-file) (let ((original-content (uiop:read-file-string target-file))) (handler-case (if (search old-code original-content) (let ((new-content (cl-ppcre:regex-replace-all (cl-ppcre:quote-meta-chars old-code) original-content new-code))) (with-open-file (out target-file :direction :output :if-exists :supersede) (write-string new-content out)) (harness-log "SELF-EDIT: Edit applied successfully.") (list :status :success :message "Edit applied.")) (progn (harness-log "SELF-EDIT: Pattern not found in file.") (list :status :error :message "Pattern not found in file."))) (error (c) (harness-log "SELF-EDIT: Edit failed: ~a" c) (rollback-memory 0) (list :status :error :message (format nil "Edit failed: ~a" c)))))) #+end_src ** Cognitive Tool: Edit File #+begin_src lisp (def-cognitive-tool :self-edit "Applies a surgical code modification to a file with automatic rollback on failure." ((:file :type :string :description "Path to the target file") (:old :type :string :description "The code block to find") (:new :type :string :description "The code block to replace with")) :body (lambda (args) (let* ((file (getf args :file)) (old (getf args :old)) (new (getf args :new))) (self-edit-apply file old new)))) #+end_src ** Skill Definition Hooks into syntax-error events for self-repair. #+begin_src lisp (defskill :skill-self-edit :priority 95 :trigger (lambda (ctx) (let ((sensor (getf (getf ctx :payload) :sensor))) (member sensor '(:syntax-error :repair-request :self-edit)))) :probabilistic (lambda (ctx) (let ((sensor (getf (getf ctx :payload) :sensor))) (cond ((eq sensor :syntax-error) "You are the Self-Edit Agent. A syntax error occurred. Provide a fixed version of the code as a lisp form.") ((eq sensor :repair-request) "You are the Self-Edit Agent. Apply the surgical fix to the file.") (t nil)))) :deterministic (lambda (action ctx) (let* ((payload (getf ctx :payload)) (sensor (getf payload :sensor))) (cond ((eq sensor :syntax-error) (let ((code (getf payload :code))) (harness-log "SELF-EDIT: Fast paren balancing...") (let ((balanced (self-edit-balance-parens code))) (handler-case (progn (read-from-string balanced) (harness-log "SELF-EDIT: Fast fix SUCCESS.") (list :status :success :repaired balanced)) (error () (harness-log "SELF-EDIT: Fast fix failed, need neural repair.") (list :status :error :reason "needs-llm")))))) ((eq sensor :repair-request) (let ((file (getf payload :file)) (old (getf payload :old)) (new (getf payload :new))) (self-edit-apply file old new))) (t nil))))) #+end_src ** Tool: Quick Paren Fix #+begin_src lisp (def-cognitive-tool :balance-parens "Balances parentheses in a code string." ((:code :type :string :description "The code to balance")) :body (lambda (args) (let* ((code (getf args :code)) (balanced (self-edit-balance-parens code))) (handler-case (progn (read-from-string balanced) (list :status :success :repaired balanced)) (error (c) (list :status :error :message (format nil "Could not repair: ~a" c))))))) #+end_src ** Skill Hot-Reload Swap compiled skill files without breaking active sockets. #+begin_src lisp (defvar *self-edit-skills-backup* nil "Backup of skill registry before hot-reload.") (defun self-edit-hot-reload-skill (skill-name gen-path) "Reloads a skill from its compiled .lisp source. Steps: 1. Backup current *skills-registry* 2. Compile the new skill file 3. Merge new skill into registry 4. Verify the skill loads without error 5. If error, rollback to backup Returns (values :success t) or (values :error message)." (unless *skills-registry* (return-from self-edit-hot-reload-skill (values :error "Skills engine not initialized"))) (unless (uiop:file-exists-p gen-path) (return-from self-edit-hot-reload-skill (values :error (format nil "Skill file not found: ~a" gen-path)))) ;; Step 1: Backup registry (setf *self-edit-skills-backup* (copy-hash-table *skills-registry*)) (handler-case (progn ;; Step 2: Compile new skill (let ((compiled (compile-file gen-path))) (unless compiled (error "Compilation returned nil"))) ;; Step 3: Load the compiled skill (load gen-path) ;; Step 4: Verify skill is in registry (let ((skill (gethash (string skill-name) *skills-registry*))) (if skill (progn (harness-log "SELF-EDIT: Hot-reloaded skill ~a from ~a" skill-name gen-path) (values :success t)) (error "Skill not registered after reload")))) (error (e) ;; Step 5: Rollback (when *self-edit-skills-backup* (clrhash *skills-registry*) (maphash (lambda (k v) (setf (gethash k *skills-registry*) v)) *self-edit-skills-backup*)) (harness-log "SELF-EDIT: Hot-reload FAILED for ~a: ~a" skill-name e) (values :error (format nil "Hot-reload failed: ~a" e))))) #+end_src ** Cognitive Tool: Reload Skill #+begin_src lisp (def-cognitive-tool :reload-skill "Hot-reloads a skill from its compiled source file without restarting the system." ((:skill-name :type :string :description "Name of the skill to reload (e.g. :skill-engineering-standards)") (:gen-path :type :string :description "Absolute path to the compiled .lisp file")) :body (lambda (args) (let ((name (getf args :skill-name)) (path (getf args :gen-path))) (multiple-value-bind (status message) (self-edit-hot-reload-skill name path) (list :status status :message message))))) #+end_src * Phase E: Verification #+begin_src lisp :tangle (concat (getenv "INSTALL_DIR") "/skills/self-edit-tests.lisp" (expand-file-name "")) (defpackage :opencortex-self-edit-tests (:use :cl :fiveam :opencortex) (:export #:self-edit-suite)) (in-package :opencortex-self-edit-tests) (def-suite self-edit-suite :description "Tests for Self-Edit skill.") (in-suite self-edit-suite) (test balance-parens-balanced (let ((result (opencortex::self-edit-balance-parens "(+ 1 2)"))) (is (string= result "(+ 1 2)")) (is (not (null (read-from-string result)))))) (test balance-parens-missing-open (let ((result (opencortex::self-edit-balance-parens "+ 1 2)"))) (is (string= result "(+ 1 2)")) (is (not (null (read-from-string result)))))) (test balance-parens-missing-close (let ((result (opencortex::self-edit-balance-parens "(+ 1 2"))) (is (string= result "(+ 1 2)")) (is (not (null (read-from-string result)))))) (test balance-parens-deep (let ((result (opencortex::self-edit-balance-parens "((lambda (x) (if x (+ 1 2) 3))"))) (is (string= result "((lambda (x) (if x (+ 1 2) 3)))")) (is (not (null (read-from-string result)))))) (test balance-parens-empty (let ((result (opencortex::self-edit-balance-parens ""))) (is (string= result "")))) (test test-self-edit-apply-success "Verify self-edit-apply performs surgical replacement correctly." (let ((test-file "/tmp/self-edit-test.lisp")) (unwind-protect (progn (with-open-file (out test-file :direction :output :if-exists :supersede) (write-string "(defun hello () (format t \"world~%\"))" out)) (let ((result (opencortex::self-edit-apply test-file "world" "universe"))) (is (eq (getf result :status) :success)) (let ((content (uiop:read-file-string test-file))) (is (search "universe" content)) (is (not (search "world" content)))))) (uiop:delete-file-if-exists test-file)))) (test test-self-edit-apply-not-found "Verify self-edit-apply returns error when pattern not found." (let ((test-file "/tmp/self-edit-test2.lisp")) (unwind-protect (progn (with-open-file (out test-file :direction :output :if-exists :supersede) (write-string "(defun hello () t)" out)) (let ((result (opencortex::self-edit-apply test-file "nonexistent-pattern" "new"))) (is (eq (getf result :status) :error)) (is (search "not found" (getf result :message))))) (uiop:delete-file-if-exists test-file)))) (test test-self-edit-apply-file-not-found "Verify self-edit-apply returns error when file does not exist." (let ((result (opencortex::self-edit-apply "/nonexistent/path/file.lisp" "old" "new"))) (is (eq (getf result :status) :error)) (is (search "not found" (getf result :message))))) (test test-self-edit-parse-location-from-payload "Verify self-edit-parse-location extracts file/line from payload." (let ((context '(:payload (:file "/tmp/test.lisp" :line 42 :message "error")))) (let ((result (opencortex::self-edit-parse-location context))) (is (equal "/tmp/test.lisp" (getf result :file))) (is (eq 42 (getf result :line)))))) (test test-self-edit-parse-location-from-message "Verify self-edit-parse-location extracts file/line from error message." (let ((context '(:payload (:message "Error in /home/user/project/foo.lisp at line 99")))) (let ((result (opencortex::self-edit-parse-location context))) (is (listp result)) (is (getf result :line)) (is (eq 99 (getf result :line)))))) #+end_src * See Also - [[file:org-skill-lisp-utils.org][Lisp Utils]] - Validation and repair - [[file:org-skill-self-fix.org][Self-Fix]] - File modification with rollback