Some checks failed
Deploy-Agent-V15-Stdin / JOB-V15-STDIN (push) Failing after 3s
- Add test suites inline in harness/communication.org - Add test suites inline in harness/perceive.org, reason.org, act.org - Add test suites inline in harness/skills.org, memory.org, loop.org - Tests now live alongside code, not in separate .lisp files - Each test block has prose explaining its purpose Test results: 84/84 pass (100%)
536 lines
27 KiB
Org Mode
536 lines
27 KiB
Org Mode
#+TITLE: The Skill Engine (skills.lisp)
|
|
#+AUTHOR: Amr
|
|
#+FILETAGS: :harness:skills:
|
|
#+STARTUP: content
|
|
|
|
* The Skill Engine (skills.lisp)
|
|
** Architectural Intent: Late-Binding Intelligence
|
|
|
|
A static, hardcoded architecture is inherently fragile. The ~opencortex~ Skill Engine enables **Late-Binding Intelligence**, allowing the system to discover and integrate new cognitive capabilities (actuators, solvers, sensors) at runtime without a kernel restart.
|
|
|
|
** Global Skill Registry
|
|
|
|
#+begin_src lisp :tangle ../library/skills.lisp
|
|
(in-package :opencortex)
|
|
|
|
(defun COSINE-SIMILARITY (v1 v2)
|
|
"Computes the cosine similarity between two vectors.
|
|
Both arguments should be sequences of numbers. Returns a value between -1.0 and 1.0."
|
|
(let ((len1 (length v1)) (len2 (length v2)))
|
|
(if (or (zerop len1) (zerop len2))
|
|
0.0
|
|
(let ((dot-product 0.0d0)
|
|
(norm1 0.0d0)
|
|
(norm2 0.0d0))
|
|
(let ((len (min len1 len2)))
|
|
(dotimes (i len)
|
|
(let ((x (coerce (elt v1 i) 'double-float)))
|
|
(let ((y (coerce (elt v2 i) 'double-float)))
|
|
(incf dot-product (* x y))
|
|
(incf norm1 (* x x))
|
|
(incf norm2 (* y y))))))
|
|
(if (or (zerop norm1) (zerop norm2))
|
|
0.0
|
|
(/ dot-product (sqrt (* norm1 norm2))))))))
|
|
(defun VAULT-MASK-STRING (s) "[MASKED]") ; Stub
|
|
(defvar *VAULT-MEMORY* (make-hash-table :test 'equal))
|
|
|
|
|
|
(defstruct skill name priority dependencies trigger-fn probabilistic-prompt deterministic-fn)
|
|
|
|
(defvar *skill-catalog* (make-hash-table :test 'equal)
|
|
"A stateful tracking table for all skill files discovered in the environment.")
|
|
|
|
(defstruct skill-entry
|
|
filename
|
|
(status :discovered) ;; :discovered, :loading, :ready, :failed
|
|
error-log
|
|
(load-time 0))
|
|
|
|
(defun find-triggered-skill (context)
|
|
"Returns the highest priority skill whose trigger matches context AND has a probabilistic prompt."
|
|
(let ((triggered nil))
|
|
(maphash (lambda (name skill)
|
|
(declare (ignore name))
|
|
(when (and (skill-probabilistic-prompt skill)
|
|
(ignore-errors (funcall (skill-trigger-fn skill) context)))
|
|
(push skill triggered)))
|
|
*skills-registry*)
|
|
(first (sort triggered #'> :key #'skill-priority))))
|
|
|
|
(defmacro defskill (name &key priority dependencies trigger probabilistic deterministic)
|
|
"Registers a new skill into the global registry."
|
|
`(setf (gethash (string-downcase (string ,name)) *skills-registry*)
|
|
(make-skill :name (string-downcase (string ,name))
|
|
:priority (or ,priority 10)
|
|
:dependencies ',dependencies
|
|
:trigger-fn ,trigger
|
|
:probabilistic-prompt ,probabilistic
|
|
:deterministic-fn ,deterministic)))
|
|
|
|
(defun resolve-skill-dependencies (skill-name)
|
|
"Recursively resolves dependencies for a given skill name."
|
|
(let ((resolved nil) (seen nil))
|
|
(labels ((visit (name)
|
|
(unless (member name seen :test #'equal)
|
|
(push name seen)
|
|
(let ((skill (gethash (string-downcase (string name)) *skills-registry*)))
|
|
(when skill
|
|
(dolist (dep (skill-dependencies skill))
|
|
(visit dep))))
|
|
(push name resolved))))
|
|
(visit skill-name)
|
|
(nreverse resolved))))
|
|
#+end_src
|
|
|
|
** Skill File Analysis (parse-skill-metadata)
|
|
#+begin_src lisp :tangle ../library/skills.lisp
|
|
(defun parse-skill-metadata (filepath)
|
|
"Extracts ID and DEPENDS_ON tags using robust regex scanning."
|
|
(let ((dependencies nil)
|
|
(id nil)
|
|
(content (uiop:read-file-string filepath)))
|
|
;; Extract ID
|
|
(multiple-value-bind (match regs)
|
|
(ppcre:scan-to-strings "(?im:^:ID:\\s*([^\\s\\r\\n]+))" content)
|
|
(when match (setf id (aref regs 0))))
|
|
;; Extract all DEPENDS_ON lines
|
|
(ppcre:do-register-groups (deps-string)
|
|
("(?im:^#\\+DEPENDS_ON:\\s*(.*))" content)
|
|
(let ((deps (ppcre:split "\\s+" (string-trim " " deps-string))))
|
|
(setf dependencies (append dependencies (mapcar (lambda (s) (string-trim "[] " s)) deps)))))
|
|
(values id (remove-if (lambda (s) (= 0 (length s))) dependencies))))
|
|
#+end_src
|
|
|
|
** Dependency Resolution (topological-sort-skills)
|
|
#+begin_src lisp :tangle ../library/skills.lisp
|
|
(defun topological-sort-skills (skills-dir)
|
|
"Returns a list of skill filepaths sorted by dependency (dependencies first)."
|
|
(let ((files (uiop:directory-files skills-dir "org-skill-*.org"))
|
|
(adj (make-hash-table :test 'equal))
|
|
(name-to-file (make-hash-table :test 'equal))
|
|
(id-to-file (make-hash-table :test 'equal))
|
|
(result nil)
|
|
(visited (make-hash-table :test 'equal))
|
|
(stack (make-hash-table :test 'equal)))
|
|
(dolist (file files)
|
|
(let ((filename (pathname-name file)))
|
|
(multiple-value-bind (id deps) (parse-skill-metadata file)
|
|
(setf (gethash (string-downcase filename) name-to-file) file)
|
|
(when id (setf (gethash (string-downcase id) id-to-file) file))
|
|
(setf (gethash (string-downcase filename) adj) deps))))
|
|
(labels ((visit (file)
|
|
(let* ((filename (pathname-name file))
|
|
(node-key (string-downcase filename)))
|
|
(unless (gethash node-key visited)
|
|
(setf (gethash node-key stack) t)
|
|
(dolist (dep (gethash node-key adj))
|
|
(let* ((is-id-p (uiop:string-prefix-p "id:" (string-downcase dep)))
|
|
(dep-key (string-downcase (if is-id-p (subseq dep 3) dep)))
|
|
(dep-file (if is-id-p
|
|
(gethash dep-key id-to-file)
|
|
(or (gethash dep-key id-to-file)
|
|
(gethash dep-key name-to-file)))))
|
|
(when dep-file
|
|
(let ((dep-filename (pathname-name dep-file)))
|
|
(if (gethash (string-downcase dep-filename) stack)
|
|
(error "Circular dependency detected: ~a -> ~a" filename dep-filename)
|
|
(visit dep-file))))))
|
|
(setf (gethash node-key stack) nil)
|
|
(setf (gethash node-key visited) t)
|
|
(push file result)))))
|
|
(let ((filenames (sort (mapcar #'pathname-name files) #'string<)))
|
|
(dolist (name filenames)
|
|
(let ((file (gethash (string-downcase name) name-to-file)))
|
|
(when file (visit file)))))
|
|
(nreverse result))))
|
|
#+end_src
|
|
|
|
** Jailed Loading (load-skill-from-org)
|
|
#+begin_src lisp :tangle ../library/skills.lisp
|
|
(defun validate-lisp-syntax (code-string)
|
|
"Checks if a string contains valid, readable Common Lisp forms.
|
|
Delegates to the Lisp Validator skill when available; falls back to a basic
|
|
reader check during early boot before the validator skill is loaded."
|
|
(let ((result
|
|
(if (fboundp 'lisp-validator-validate)
|
|
(lisp-validator-validate code-string :strict nil)
|
|
(handler-case
|
|
(let ((*read-eval* nil))
|
|
(with-input-from-string (stream (format nil "(progn ~a)" code-string))
|
|
(loop for form = (read stream nil :eof) until (eq form :eof)))
|
|
(list :status :success))
|
|
(error (c)
|
|
(list :status :error :reason (format nil "~a" c)))))))
|
|
(if (eq (getf result :status) :success)
|
|
(values t nil)
|
|
(values nil (or (getf result :reason) "Lisp Validator rejected code.")))))
|
|
|
|
(defun load-skill-from-org (filepath)
|
|
"Parses and evaluates Lisp blocks with :tangle directives from an Org file.
|
|
Only loads blocks that specify a .lisp tangle target, ignoring tests and examples."
|
|
(let* ((skill-base-name (pathname-name filepath))
|
|
(entry (or (gethash skill-base-name *skill-catalog*) (make-skill-entry :filename skill-base-name))))
|
|
(setf (skill-entry-status entry) :loading)
|
|
(setf (gethash skill-base-name *skill-catalog*) entry)
|
|
|
|
(handler-case
|
|
(let* ((content (uiop:read-file-string filepath))
|
|
(lines (uiop:split-string content :separator '(#\Newline)))
|
|
(in-lisp-block nil)
|
|
(collect-this-block nil)
|
|
(lisp-code "")
|
|
(pkg-name (intern (string-upcase (format nil "OPENCORTEX.SKILLS.~a" skill-base-name)) :keyword)))
|
|
|
|
(dolist (line lines)
|
|
(let ((clean-line (string-trim '(#\Space #\Tab #\Return) line)))
|
|
(cond ((uiop:string-prefix-p "#+begin_src lisp" (string-downcase clean-line))
|
|
(setf in-lisp-block t)
|
|
;; Only collect blocks with a :tangle directive pointing to a
|
|
;; runtime .lisp file (exclude tests and :tangle no)
|
|
(let ((tl (string-downcase clean-line)))
|
|
(setf collect-this-block
|
|
(and (search ":tangle" tl)
|
|
(not (search ":tangle no" tl))
|
|
(search ".lisp" tl)
|
|
(not (search "tests/" tl))
|
|
(not (search "test/" tl))))))
|
|
((uiop:string-prefix-p "#+end_src" (string-downcase clean-line))
|
|
(setf in-lisp-block nil)
|
|
(setf collect-this-block nil))
|
|
((and in-lisp-block collect-this-block)
|
|
(unless (or (uiop:string-prefix-p ":PROPERTIES:" (string-upcase clean-line))
|
|
(uiop:string-prefix-p ":END:" (string-upcase clean-line)))
|
|
(setf lisp-code (concatenate 'string lisp-code line (string #\Newline))))))))
|
|
|
|
(if (= (length lisp-code) 0)
|
|
(progn (setf (skill-entry-status entry) :ready) t)
|
|
(progn
|
|
(multiple-value-bind (valid-p err) (validate-lisp-syntax lisp-code)
|
|
(unless valid-p (error "Syntax Error: ~a" err)))
|
|
(harness-log "HARNESS: Jailing skill '~a' in package ~a" skill-base-name pkg-name)
|
|
(unless (find-package pkg-name)
|
|
(let ((new-pkg (make-package pkg-name :use '(:cl))))
|
|
(use-package :opencortex new-pkg)))
|
|
(let ((*read-eval* nil) (*package* (find-package pkg-name)))
|
|
(eval (read-from-string (format nil "(progn ~a)" lisp-code))))
|
|
(setf (skill-entry-status entry) :ready)
|
|
t)))
|
|
(error (c)
|
|
(let ((msg (format nil "~a" c)))
|
|
(harness-log "LOADER ERROR in skill '~a': ~a" skill-base-name msg)
|
|
(setf (skill-entry-status entry) :failed)
|
|
(setf (skill-entry-error-log entry) msg)
|
|
nil)))))
|
|
|
|
(defun load-skill-with-timeout (filepath timeout-seconds)
|
|
"Loads a skill Org file with a hard execution timeout."
|
|
(let* ((finished nil)
|
|
(thread (bt:make-thread (lambda ()
|
|
(if (load-skill-from-org filepath)
|
|
(setf finished t)
|
|
(setf finished :error)))
|
|
:name (format nil "loader-~a" (pathname-name filepath))))
|
|
(start-time (get-internal-real-time))
|
|
(timeout-units (truncate (* timeout-seconds internal-time-units-per-second))))
|
|
(loop
|
|
(when (eq finished t) (return :success))
|
|
(when (eq finished :error) (return :error))
|
|
(unless (bt:thread-alive-p thread) (return :error))
|
|
(when (> (- (get-internal-real-time) start-time) timeout-units)
|
|
(harness-log "HARNESS: Timing out skill ~a..." (pathname-name filepath))
|
|
#+sbcl (sb-thread:terminate-thread thread)
|
|
#-sbcl (bt:destroy-thread thread)
|
|
(return :timeout))
|
|
(sleep 0.05))))
|
|
#+end_src
|
|
|
|
** Initializing All Skills (initialize-all-skills)
|
|
#+begin_src lisp :tangle ../library/skills.lisp
|
|
(defun initialize-all-skills ()
|
|
"Scans the directory defined by SKILLS_DIR and hot-loads skills using topological order."
|
|
(let* ((env-path (uiop:getenv "SKILLS_DIR"))
|
|
(skills-dir-str (or env-path (namestring (merge-pathnames "notes/" (user-homedir-pathname)))))
|
|
(resolved-path (context-resolve-path skills-dir-str))
|
|
(skills-dir (if resolved-path (uiop:ensure-directory-pathname resolved-path) nil)))
|
|
|
|
(unless (and skills-dir (uiop:directory-exists-p skills-dir))
|
|
(harness-log "HARNESS ERROR: Skills directory not found: ~a" skills-dir-str)
|
|
(return-from initialize-all-skills nil))
|
|
|
|
(let ((sorted-files (topological-sort-skills skills-dir)))
|
|
(let* ((mandatory-env (uiop:getenv "MANDATORY_SKILLS"))
|
|
(mandatory-skills (if mandatory-env
|
|
(mapcar (lambda (s) (string-trim '(#\Space #\" #\') s))
|
|
(uiop:split-string mandatory-env :separator '( #\,)))
|
|
'("org-skill-policy" "org-skill-bouncer"))))
|
|
(dolist (req mandatory-skills)
|
|
(unless (member req sorted-files :key #'pathname-name :test #'string-equal)
|
|
(error "BOOT FAILURE: Mandatory skill '~a' not found in skills directory: ~a" req (uiop:native-namestring skills-dir))))
|
|
|
|
(harness-log "==================================================")
|
|
(harness-log " LOADER: Initializing ~a skills..." (length sorted-files))
|
|
|
|
(dolist (file sorted-files)
|
|
(let* ((skill-name (pathname-name file))
|
|
(is-mandatory (member skill-name mandatory-skills :test #'string-equal)))
|
|
(harness-log " LOADER: Loading ~a..." skill-name)
|
|
(let ((status (load-skill-with-timeout file 5)))
|
|
(unless (eq status :success)
|
|
(if is-mandatory
|
|
(error "BOOT FAILURE: Mandatory skill '~a' failed to load (Status: ~a)." skill-name status)
|
|
(harness-log "LOADER WARNING: Skill '~a' failed to load." skill-name))))))
|
|
|
|
(let ((ready 0) (failed 0))
|
|
(maphash (lambda (k v)
|
|
(declare (ignore k))
|
|
(if (eq (skill-entry-status v) :ready) (incf ready) (incf failed)))
|
|
*skill-catalog*)
|
|
(harness-log " LOADER: Boot Complete. [Ready: ~a] [Failed: ~a]" ready failed)
|
|
(harness-log "==================================================")
|
|
(values ready failed))))))
|
|
#+end_src
|
|
|
|
** Toolbelt Prompt Generation (generate-tool-belt-prompt)
|
|
#+begin_src lisp :tangle ../library/skills.lisp
|
|
(defun generate-tool-belt-prompt ()
|
|
"Aggregates all registered cognitive tools into a descriptive prompt."
|
|
(let ((output (format nil "AVAILABLE TOOLS:
|
|
You can call tools by returning a Lisp plist: (:target :tool :action :call :tool <name> :args (...))
|
|
|
|
EXAMPLES:
|
|
(:target :tool :action :call :tool \"eval\" :args (:code \"(+ 1 1)\"))
|
|
(:target :tool :action :call :tool \"grep-search\" :args (:pattern \"autonomousty\"))
|
|
(:target :tool :action :call :tool \"shell\" :args (:cmd \"ls -la\"))
|
|
|
|
---
|
|
" )))
|
|
(maphash (lambda (name tool)
|
|
(setf output (concatenate 'string output
|
|
(format nil "- ~a: ~a~% Parameters: ~s~%~%"
|
|
name
|
|
(cognitive-tool-description tool)
|
|
(cognitive-tool-parameters tool)))))
|
|
*cognitive-tools*)
|
|
output))
|
|
#+end_src
|
|
|
|
** The Default Tool Belt
|
|
*** The Eval Tool (Internal Inspection)
|
|
#+begin_src lisp :tangle ../library/skills.lisp
|
|
(def-cognitive-tool :eval "Evaluates raw Common Lisp code in the harness image. Use this for complex calculations or internal state inspection."
|
|
((:code :type :string :description "The Lisp code to evaluate"))
|
|
:guard (lambda (args context)
|
|
(declare (ignore context))
|
|
(let ((code (getf args :code)))
|
|
(let ((harness-pkg (find-package :opencortex.skills.org-skill-lisp-validator)))
|
|
(if harness-pkg
|
|
(uiop:symbol-call :opencortex.skills.org-skill-lisp-validator :lisp-validator-validate code)
|
|
t))))
|
|
:body (lambda (args)
|
|
(let ((code (getf args :code)))
|
|
(handler-case (let ((result (eval (read-from-string code))))
|
|
(format nil "~s" result))
|
|
(error (c) (format nil "ERROR: ~a" c))))))
|
|
#+end_src
|
|
|
|
*** The Grep Tool (File Discovery)
|
|
#+begin_src lisp :tangle ../library/skills.lisp
|
|
(def-cognitive-tool :grep-search "Searches for a pattern in the project files."
|
|
((:pattern :type :string :description "The regex pattern to search for")
|
|
(:dir :type :string :description "Directory to search in (default is project root)"))
|
|
:body (lambda (args)
|
|
(let ((pattern (getf args :pattern))
|
|
(dir (or (getf args :dir) (uiop:getenv "MEMEX_DIR"))))
|
|
(uiop:run-program (list "grep" "-r" "-n" "--exclude-dir=node_modules" pattern dir)
|
|
:output :string :ignore-error-status t))))
|
|
#+end_src
|
|
|
|
*** The Shell Tool (Machine Actuation)
|
|
#+begin_src lisp :tangle ../library/skills.lisp
|
|
(def-cognitive-tool :shell "Executes a shell command on the local machine. Use this for file operations, system checks, or running tests."
|
|
((:cmd :type :string :description "The full bash command to execute"))
|
|
:guard (lambda (args context)
|
|
(declare (ignore context))
|
|
(let ((cmd (getf args :cmd)))
|
|
(not (or (search "rm -rf /" cmd) (search ":(){ :|:& };:" cmd)))))
|
|
:body (lambda (args)
|
|
(let ((cmd (getf args :cmd)))
|
|
(multiple-value-bind (out err code)
|
|
(uiop:run-program (list "bash" "-c" cmd) :output :string :error-output :string :ignore-error-status t)
|
|
(format nil "EXIT-CODE: ~a~%~%STDOUT:~%~a~%~%STDERR:~%~a" code out err)))))
|
|
#+end_src
|
|
|
|
*** The Reload-Skill Tool (Hot Reload)
|
|
#+begin_src lisp :tangle ../library/skills.lisp
|
|
(def-cognitive-tool :reload-skill "Reloads a skill from its Org-mode source file, recompiling into the live image without restarting the daemon."
|
|
((:skill :type :string :description "The skill name (e.g., \"org-skill-policy\") or full path to the .org file"))
|
|
:guard (lambda (args context)
|
|
(declare (ignore context))
|
|
(let ((skill (getf args :skill)))
|
|
(or (uiop:file-exists-p skill)
|
|
(let ((skills-dir (or (ignore-errors (uiop:getenv "SKILLS_DIR"))
|
|
(namestring (merge-pathnames "notes/" (user-homedir-pathname))))))
|
|
(uiop:file-exists-p (merge-pathnames (format nil "~a.org" skill) skills-dir))))))
|
|
:body (lambda (args)
|
|
(let ((skill (getf args :skill)))
|
|
(snapshot-memory)
|
|
(let ((skills-dir (or (ignore-errors (uiop:getenv "SKILLS_DIR"))
|
|
(namestring (merge-pathnames "notes/" (user-homedir-pathname)))))
|
|
(resolved-path (context-resolve-path skills-dir))
|
|
(skills-dir-actual (if (ignore-errors (uiop:getenv "SKILLS_DIR"))
|
|
(uiop:ensure-directory-pathname (context-resolve-path (uiop:getenv "SKILLS_DIR")))
|
|
(uiop:ensure-directory-pathname (user-homedir-pathname)))))
|
|
(let ((file (if (uiop:file-exists-p skill)
|
|
(uiop:ensure-pathname skill)
|
|
(merge-pathnames (format nil "~a.org" skill) skills-dir-actual))))
|
|
(cond
|
|
((not (uiop:file-exists-p file))
|
|
(format nil "ERROR: Skill file not found: ~a" (uiop:native-namestring file)))
|
|
(t
|
|
(harness-log "SKILL: Hot-reloading ~a..." (pathname-name file))
|
|
(let ((status (load-skill-with-timeout file 10)))
|
|
(if (eq status :success)
|
|
(let ((base-name (pathname-name file)))
|
|
(setf (skill-entry-status (gethash base-name *skill-catalog*)) :ready)
|
|
(format nil "OK: Skill '~a' reloaded successfully." base-name))
|
|
(format nil "ERROR: Reload failed with status ~a" status)))))))))
|
|
#+end_src
|
|
|
|
*** The File Read Tool (V 0.2.0 File I/O)
|
|
#+begin_src lisp :tangle ../library/skills.lisp
|
|
(def-cognitive-tool :read-file "Reads the contents of a file as a string."
|
|
((:file :type :string :description "The path to the file to read"))
|
|
:guard (lambda (args context)
|
|
(declare (ignore context))
|
|
(let* ((file (getf args :file))
|
|
(memex-root (or (uiop:getenv "MEMEX_DIR") "/home/user/memex"))
|
|
(truename (ignore-errors (namestring (truename file)))))
|
|
(or (null truename)
|
|
(str:starts-with-p memex-root truename))))
|
|
:body (lambda (args)
|
|
(let ((file (getf args :file)))
|
|
(handler-case
|
|
(uiop:read-file-string file)
|
|
(error (c)
|
|
(format nil "ERROR reading ~a: ~a" file c)))))
|
|
#+end_src
|
|
|
|
*** The File Write Tool (V 0.2.0 File I/O)
|
|
#+begin_src lisp :tangle ../library/skills.lisp
|
|
(def-cognitive-tool :write-file "Writes content to a file, creating it if it doesn't exist."
|
|
((:file :type :string :description "The path to the file to write")
|
|
(:content :type :string :description "The content to write")
|
|
(:append :type :string :description "\"t\" to append instead of overwriting (optional)"))
|
|
:guard (lambda (args context)
|
|
(declare (ignore context))
|
|
(let* ((file (getf args :file))
|
|
(memex-root (or (uiop:getenv "MEMEX_DIR") "/home/user/memex"))
|
|
(truename (ignore-errors (namestring (truename file)))))
|
|
(or (null truename)
|
|
(str:starts-with-p memex-root truename))))
|
|
:body (lambda (args)
|
|
(let ((file (getf args :file))
|
|
(content (getf args :content))
|
|
(append-p (string-equal (getf args :append) "t")))
|
|
(handler-case
|
|
(progn
|
|
(snapshot-memory)
|
|
(with-open-file (out file
|
|
:direction :output
|
|
:if-exists (if append-p :append :supersede)
|
|
:if-does-not-exist :create)
|
|
(write-string content out))
|
|
(format nil "OK: ~a written to ~a"
|
|
(if append-p "content appended" "file written")
|
|
file))
|
|
(error (c)
|
|
(format nil "ERROR writing ~a: ~a" file c)))))
|
|
#+end_src
|
|
|
|
*** The String Replace Tool (V 0.2.0 File I/O)
|
|
#+begin_src lisp :tangle ../library/skills.lisp
|
|
(def-cognitive-tool :replace-string "Replaces occurrences of old-string with new-string in a file."
|
|
((:file :type :string :description "The path to the file")
|
|
(:old :type :string :description "The substring to find and replace")
|
|
(:new :type :string :description "The replacement string"))
|
|
:guard (lambda (args context)
|
|
(declare (ignore context))
|
|
(let* ((file (getf args :file))
|
|
(memex-root (or (uiop:getenv "MEMEX_DIR") "/home/user/memex"))
|
|
(truename (ignore-errors (namestring (truename file)))))
|
|
(or (null truename)
|
|
(str:starts-with-p memex-root truename))))
|
|
:body (lambda (args)
|
|
(let ((file (getf args :file))
|
|
(old (getf args :old))
|
|
(new (getf args :new)))
|
|
(handler-case
|
|
(progn
|
|
(snapshot-memory)
|
|
(let ((content (uiop:read-file-string file)))
|
|
(if (search old content)
|
|
(let ((new-content (cl-ppcre:regex-replace-all (cl-ppcre:quote-meta-chars old) content new)))
|
|
(with-open-file (out file :direction :output :if-exists :supersede)
|
|
(write-string new-content out))
|
|
(format nil "OK: Replaced first occurrence in ~a" file))
|
|
(format nil "ERROR: Pattern not found in ~a" file))))
|
|
(error (c)
|
|
(format nil "ERROR replacing in ~a: ~a" file c)))))
|
|
#+end_src
|
|
|
|
* Test Suite
|
|
|
|
These tests verify the Skill Engine and loader. Run with:
|
|
~(fiveam:run! 'boot-suite)~
|
|
|
|
#+begin_src lisp :tangle ../tests/boot-sequence-tests.lisp
|
|
(defpackage :opencortex-boot-tests
|
|
(:use :cl :fiveam :opencortex)
|
|
(:export #:boot-suite))
|
|
|
|
(in-package :opencortex-boot-tests)
|
|
|
|
(def-suite boot-suite :description "Verification of the Skill Engine loader")
|
|
|
|
(in-suite boot-suite)
|
|
|
|
(test test-parse-skill-metadata
|
|
"Verify extraction of ID and DEPENDS_ON from Org headers."
|
|
(let ((tmp-file "/tmp/org-skill-test-metadata.org"))
|
|
(with-open-file (out tmp-file :direction :output :if-exists :supersede)
|
|
(format out ":PROPERTIES:~%:ID: test-id~%:END:~%#+DEPENDS_ON: dep1 dep2~%"))
|
|
(unwind-protect
|
|
(multiple-value-bind (id deps) (opencortex::parse-skill-metadata tmp-file)
|
|
(is (equal "test-id" id))
|
|
(is (member "dep1" deps :test #'string=))
|
|
(is (member "dep2" deps :test #'string=)))
|
|
(uiop:delete-file-if-exists tmp-file))))
|
|
|
|
(test test-topological-sort-basic
|
|
"Verify that skills are ordered by dependency."
|
|
(let ((tmp-dir "/tmp/opencortex-boot-test/"))
|
|
(uiop:ensure-all-directories-exist (list tmp-dir))
|
|
(with-open-file (out (merge-pathnames "org-skill-a.org" tmp-dir) :direction :output :if-exists :supersede)
|
|
(format out "#+DEPENDS_ON: skill-b-id~%"))
|
|
(with-open-file (out (merge-pathnames "org-skill-b.org" tmp-dir) :direction :output :if-exists :supersede)
|
|
(format out ":PROPERTIES:~%:ID: skill-b-id~%:END:~%"))
|
|
(unwind-protect
|
|
(let ((sorted (opencortex::topological-sort-skills tmp-dir)))
|
|
(let ((pos-a (position "org-skill-a" sorted :key #'pathname-name :test #'string-equal))
|
|
(pos-b (position "org-skill-b" sorted :key #'pathname-name :test #'string-equal)))
|
|
(is (< pos-b pos-a)))
|
|
(uiop:delete-directory-tree (uiop:ensure-directory-pathname tmp-dir) :validate t))))
|
|
|
|
(test test-skill-jailing
|
|
"Verify that skills are loaded into their own packages."
|
|
(let ((tmp-skill "/tmp/org-skill-jail-test.org"))
|
|
(with-open-file (out tmp-skill :direction :output :if-exists :supersede)
|
|
(format out ":PROPERTIES:~%:ID: jail-test-id~%:END:~%#+TITLE: Jail Test Skill~%#+begin_src lisp :tangle no~(defun jail-test-fn () t)~#+end_src"))
|
|
(unwind-protect
|
|
(progn
|
|
(opencortex::load-skill-from-org tmp-skill)
|
|
(is (not (null (gethash "org-skill-jail-test" opencortex::*skills-registry*)))))
|
|
(uiop:delete-file-if-exists tmp-skill))))
|
|
#+end_src
|