Files
passepartout/lisp/system-self-improve.lisp
Amr Gharbeia ee81fa2755
Some checks failed
Deploy (Gitea) / deploy (push) Has been cancelled
fix: system-self-improve — complete self-edit with tangle+reload, fix self-repair
Reworked the system-self-improve skill end-to-end:

1. self-improve-edit:
   - Inline text replacement (no longer depends on org-modify which was
     in an unexported skill package and broken)
   - After editing a .org file, automatically tangles to .lisp, compiles,
     and loads the result into the running daemon
   - Memory snapshot before edit for rollback safety

2. self-improve-balance-parens:
   - New utility: detects unbalanced parens via the Lisp reader, counts
     open/close parens using loop+char= (avoiding #\( #\) which
     confuse text-based paren counting)
   - Returns balanced code or nil if already balanced

3. self-improve-repair-syntax:
   - New driver: locates a skill's .org source file, extracts all lisp
     blocks, runs each through balance-parens, writes fixes back,
     then tangles+compiles+loads

4. self-improve-fix:
   - Diagnosis phase (unchanged): pattern-matches error logs for Reader
     Error, Undefined symbol, or PACKAGE errors
   - Repair phase (new): dispatches syntax errors to
     self-improve-repair-syntax; other error types return diagnosis
     with :repaired nil

5. Infrastructure:
   - org-tangle-file: reads #+PROPERTY: header-args:lisp :tangle from
     any .org file, extracts blocks, writes .lisp, compiles, loads
   - org-extract-lisp-blocks: extracts all #+begin_src lisp blocks
     from an Org content string
2026-05-03 14:49:13 -04:00

199 lines
9.1 KiB
Common Lisp

(defun org-tangle-file (filepath)
"Tangles an Org file's lisp blocks to its :tangle target, compiles, and loads."
(let ((content (uiop:read-file-string filepath))
(tangle-path nil)
(lisp-lines nil)
(in-block nil))
(dolist (line (uiop:split-string content :separator '(#\Newline)))
(let ((trimmed (string-trim '(#\Space #\Tab) line)))
(cond
((and (null tangle-path)
(search "#+PROPERTY:" trimmed)
(search ":tangle" trimmed))
(let* ((parts (uiop:split-string trimmed :separator '(#\Space)))
(target (car (last parts)))
(org-dir (make-pathname :directory (pathname-directory filepath))))
(when (and target (not (string-equal target "no")))
(setf tangle-path
(if (char= (aref target 0) #\/)
(uiop:parse-unix-namestring target)
(uiop:parse-unix-namestring
(format nil "~a/~a" (namestring org-dir) target)))))))
((search "#+begin_src lisp" trimmed)
(setf in-block t))
((search "#+end_src" trimmed)
(setf in-block nil)
(let ((before (search "#+end_src" line)))
(when (and before (> before 0))
(push (subseq line 0 before) lisp-lines))))
(in-block
(push line lisp-lines)))))
(when (and tangle-path lisp-lines)
(setf lisp-lines (nreverse lisp-lines))
(ensure-directories-exist tangle-path)
(with-open-file (f tangle-path :direction :output :if-exists :supersede)
(format f "~{~a~%~}" lisp-lines))
(let ((compiled (compile-file tangle-path)))
(when compiled
(load compiled)
(list :tangled (namestring tangle-path) :compiled t))))))
(defun org-extract-lisp-blocks (content)
"Extracts all #+begin_src lisp blocks from Org CONTENT as a list of strings."
(let ((blocks nil)
(in-block nil)
(current nil))
(dolist (line (uiop:split-string content :separator '(#\Newline)))
(let ((trimmed (string-trim '(#\Space #\Tab) line)))
(cond
((search "#+begin_src lisp" trimmed)
(setf in-block t current nil))
((search "#+end_src" trimmed)
(when in-block
(let ((before (search "#+end_src" line)))
(when (and before (> before 0))
(push (subseq line 0 before) current)))
(push (format nil "~{~a~%~}" (nreverse current)) blocks)
(setf in-block nil current nil)))
(in-block
(push line current)))))
(nreverse blocks)))
(defun self-improve-edit (filepath old-text new-text)
"Surgical text replacement with tangle+reload for Org source files."
(when (or (null filepath) (null old-text) (null new-text))
(return-from self-improve-edit
(list :status :error :reason "Missing arguments")))
(when (not (uiop:file-exists-p filepath))
(return-from self-improve-edit
(list :status :error :reason (format nil "File not found: ~a" filepath))))
(log-message "SELF-IMPROVE: Editing ~a (~d chars)" filepath (length old-text))
(ignore-errors
(when (fboundp 'snapshot-memory)
(snapshot-memory)))
(let* ((content (uiop:read-file-string filepath))
(pos (search old-text content)))
(if pos
(let* ((new-content (concatenate 'string
(subseq content 0 pos)
new-text
(subseq content (+ pos (length old-text)))))
(ext (pathname-type filepath)))
(with-open-file (f filepath :direction :output :if-exists :supersede)
(write-sequence new-content f))
(let ((re-read (uiop:read-file-string filepath)))
(if (search new-text re-read :test 'string=)
(let ((tangle-result
(when (string-equal ext "org")
(ignore-errors (org-tangle-file filepath)))))
(list :status :success
:summary (format nil "Replaced ~d chars in ~a"
(length old-text) filepath)
:tangle tangle-result))
(list :status :error :reason "Verification failed"))))
(list :status :error :reason
(format nil "Text not found in ~a" filepath)))))
(defun self-improve-balance-parens (code)
"Returns balanced code or nil if already balanced."
(handler-case
(progn
(let ((*read-eval* nil))
(with-input-from-string (s code)
(loop for form = (read s nil :eof) until (eq form :eof)))
(values))
nil)
(error ()
(let* ((opens (loop for ch across code count (char= ch #\()))
(closes (loop for ch across code count (char= ch #\))))
(missing (- opens closes)))
(when (plusp missing)
(concatenate 'string code
(make-string missing :initial-element #\))))))))
(defun self-improve-repair-syntax (skill-name)
"Find and fix unbalanced parens in a skill's Org source file."
(let* ((data-dir (uiop:ensure-directory-pathname
(or (uiop:getenv "PASSEPARTOUT_DATA_DIR")
(merge-pathnames ".local/share/passepartout/"
(user-homedir-pathname)))))
(org-path (merge-pathnames (format nil "org/~a.org" skill-name) data-dir)))
(unless (uiop:file-exists-p org-path)
(return-from self-improve-repair-syntax
(list :status :error :reason (format nil "Source not found: ~a" skill-name)
:repaired nil)))
(let* ((content (uiop:read-file-string org-path))
(blocks (org-extract-lisp-blocks content))
(fixed 0) (result content))
(dolist (block blocks)
(let ((balanced (self-improve-balance-parens block)))
(when (and balanced (not (string= block balanced)))
(let ((pos (search block result)))
(when pos
(setf result (concatenate 'string
(subseq result 0 pos)
balanced
(subseq result (+ pos (length block))))
fixed (1+ fixed)))))))
(if (> fixed 0)
(progn
(with-open-file (f org-path :direction :output :if-exists :supersede)
(write-sequence result f))
(let ((tangle-result (org-tangle-file org-path)))
(list :status :success
:action (format nil "Fixed ~d block(s) in ~a" fixed skill-name)
:repaired t :tangle tangle-result)))
(list :status :error
:reason (format nil "No unbalanced blocks in ~a" skill-name)
:repaired nil)))))
(defun self-improve-fix (skill-name error-log)
"Diagnoses and attempts to repair a failing skill."
(when (or (null skill-name) (null error-log))
(return-from self-improve-fix
(list :status :error :reason "Missing arguments: skill-name and error-log required")))
(log-message "SELF-IMPROVE: Diagnosing ~a..." skill-name)
(let* ((log-str (if (stringp error-log) error-log (format nil "~a" error-log)))
(diagnosis nil)
(extracted-type nil))
(cond
((search "Reader Error" log-str :test 'char-equal)
(setf extracted-type :syntax-error
diagnosis (list :type :syntax-error
:detail "Reader Error (likely unbalanced parentheses)"
:log log-str)))
((search "Undefined" log-str :test 'char-equal)
(setf extracted-type :undefined-symbol
diagnosis (list :type :undefined-symbol
:detail "Undefined symbol or missing dependency"
:log log-str)))
((search "PACKAGE" log-str :test 'char-equal)
(setf extracted-type :package-error
diagnosis (list :type :package-error
:detail "Package resolution error"
:log log-str)))
(t
(setf extracted-type :unknown
diagnosis (list :type :unknown
:detail (format nil "Unrecognized error: ~a"
(subseq log-str 0 (min 200 (length log-str))))
:log log-str))))
(log-message "SELF-IMPROVE: Diagnosed ~a as ~a" skill-name extracted-type)
(let ((repair-result
(when (eql extracted-type :syntax-error)
(self-improve-repair-syntax skill-name))))
(if (and repair-result (getf repair-result :repaired))
(progn
(log-message "SELF-IMPROVE: Successfully repaired ~a" skill-name)
repair-result)
(list :status :error
:reason (format nil "Diagnosis for ~a: ~a" skill-name
(getf diagnosis :detail))
:diagnosis diagnosis
:repaired nil)))))
(defskill :passepartout-system-self-improve
:priority 100
:trigger (lambda (ctx) (member (getf ctx :type) '(:LOG :EVENT)))
:deterministic (lambda (action ctx) (declare (ignore action ctx)) nil))