8.6 KiB
SKILL: REPL (org-skill-repl.org)
- Overview
- Phase A: Demand (Thinking)
- Phase B: Protocol (Spec)
- Phase C: Implementation
- Phase D: Verification
- Phase E: Lifecycle
Overview
The REPL Skill provides persistent Lisp evaluation, inspection, and debugging capabilities. This enables the agent to verify behavior at runtime rather than just at the text level.
Phase A: Demand (Thinking)
Why a REPL?
The lisp-eval function provides one-shot evaluation but:
- No state persistence between calls
- No variable inspection
- No debugging capabilities
The REPL skill fills this gap by:
- Maintaining evaluation state across turns
- Supporting variable inspection
- Providing debugging commands
- Optionally connecting to external Swank servers
Success Criteria
- Code evaluation returns result + stdout/stderr separately
- Variables can be inspected
- Can load code into image
- Optional: connect to external SLIME/Swank session
Phase B: Protocol (Spec)
- `repl-eval` returns: (values result output error)
- `repl-inspect` returns: structured description
- `repl-list-vars` returns: list of bound symbols
- `repl-load-file` returns: t on success, error on failure
Phase C: Implementation
Global State
;; REPL-VERIFIED: 2026-05-03T13:00:00
(in-package :passepartout)
(defvar *repl-package* :passepartout
"Default package for REPL evaluations.")
repl-history
;; REPL-VERIFIED: 2026-05-03T13:00:00
(defvar *repl-history* nil
"History of evaluated forms for session continuity.")
repl-variables
;; REPL-VERIFIED: 2026-05-03T13:00:00
(defvar *repl-variables* (make-hash-table :test #'eq)
"Cache of bound variables for inspection.")
#+end_src
Core Evaluation
;; REPL-VERIFIED: 2026-05-03T13:00:00
(defun repl-eval (code-string &key (package *repl-package*))
"Evaluate Lisp code and return (values result output error).
- result: the return value as string
- output: captured stdout
- error: error message or nil on success"
(let ((out (make-string-output-stream))
(err (make-string-output-stream))
(pkg (or (find-package package) (find-package :passepartout))))
(handler-case
(let* ((*standard-output* out)
(*error-output* err)
(*package* pkg)
(*read-eval* nil)
(result nil))
(with-input-from-string (s code-string)
(loop for form = (read s nil :eof) until (eq form :eof)
do (setf result (eval form))))
(push code-string *repl-history*)
(values
(format nil "~a" result)
(get-output-stream-string out)
nil))
(error (c)
(values
nil
(get-output-stream-string out)
(format nil "~a" c))))))
Variable Inspection
;; REPL-VERIFIED: 2026-05-03T13:00:00
(defun repl-inspect (symbol-name &key (package *repl-package*))
"Inspect a variable's value and structure."
(let* ((pkg (or (find-package package) (find-package :passepartout)))
(sym (find-symbol (string-upcase symbol-name) pkg)))
(cond
((null sym)
(format nil "Symbol ~a not found in package ~a" symbol-name package))
((boundp sym)
(let ((val (symbol-value sym)))
(format nil "~a = ~a~%Type: ~a~%~%"
sym val (type-of val))))
((fboundp sym)
(format nil "~a is a function~%Args: ~a~%"
sym (documentation sym 'function)))
(t
(format nil "~a is unbound" symbol-name)))))
List Bound Variables
;; REPL-VERIFIED: 2026-05-03T13:00:00
(defun repl-list-vars (&key (package *repl-package*))
"List all bound variables in the package."
(let* ((pkg (or (find-package package) (find-package :passepartout)))
(vars nil))
(do-symbols (sym pkg)
(when (boundp sym)
(push (format nil "~a" sym) vars)))
(sort vars #'string<)))
Load File into Image
;; REPL-VERIFIED: 2026-05-03T13:00:00
(defun repl-load-file (filepath)
"Load a Lisp file into the current image."
(handler-case
(progn
(load filepath)
(format nil "Loaded ~a" filepath))
(error (c)
(format nil "Error loading ~a: ~a" filepath c))))
Package Switching
;; REPL-VERIFIED: 2026-05-03T13:00:00
(defun repl-set-package (package-name)
"Set the default package for REPL evaluations."
(let ((pkg (find-package (string-upcase package-name))))
(if pkg
(setf *repl-package* pkg)
(format nil "Package ~a not found" package-name))))
Help/Info
;; REPL-VERIFIED: 2026-05-03T13:00:00
(defun repl-help ()
"Return available REPL commands."
(format nil "~%
REPL Skill Commands:
-------------------
(repl-eval \"code\" :package :passepartout)
- Evaluate Lisp code, returns (values result output error)
(repl-inspect \"symbol\" :package :passepartout)
- Inspect a variable or function
(repl-list-vars :package :passepartout)
- List all bound variables
(repl-load-file \"/path/to/file.lisp\")
- Load a file into the image
(repl-set-package :package-name)
- Switch default package
(repl-help)
- Show this message
"))
Phase D: Verification
Basic Evaluation Test
(test test-repl-eval-simple
"Test basic arithmetic evaluation."
(multiple-value-bind (result output error)
(passepartout:repl-eval "(+ 1 2)")
(is (string= result "3"))
(is (null error))))
Error Handling Test
(test test-repl-eval-error
"Test that errors are caught and returned."
(multiple-value-bind (result output error)
(passepartout:repl-eval "(+ 1 \"string\")")
(is (null result))
(is (not (null error)))))
REPL-EVAL Pre-Reason Handler
Registers a handler for :repl-eval sensor signals. When the daemon
receives a framed message with :sensor :repl-eval, this handler
evaluates the Lisp code directly and writes the result back through
the reply-stream, bypassing the LLM pipeline entirely.
Since this handler is registered via register-pre-reason-handler,
the perceive gate calls it before any LLM reasoning occurs. The
handler returns T (consumed), so the signal never reaches Reason.
;; REPL-VERIFIED: 2026-05-03T13:00:00
(defun repl-handle (signal)
"Pre-reason handler for :repl-eval sensor. Evaluates code and
writes the result back through the reply-stream."
(let* ((payload (getf signal :payload))
(code (getf payload :code))
(stream (getf (getf signal :meta) :reply-stream))
(result (multiple-value-bind (val out err)
(repl-eval code)
(if err
(list :status :error :message err)
(list :status :success :value (or val ""))))))
(when stream
(handler-case
(progn
(write-sequence (frame-message result) stream)
(finish-output stream))
(error (c)
(log-message "REPL-EVAL: Failed to write response: ~a" c))))
;; Return T to signal the message was consumed
t))
;; Register the handler at load time
(register-pre-reason-handler :repl-eval #'repl-handle)
Phase E: Lifecycle
The REPL skill loads at priority 200 (after diagnostics at 100, before utils-lisp at 400).
System Prompt Augment (repl-mandate)
;; REPL-VERIFIED: 2026-05-03T13:00:00
(defun repl-mandate (context)
"Returns REPL-first engineering mandate when context involves code editing."
(let ((raw (or (proto-get (proto-get context :payload) :text) "")))
(when (or (search "org-skill-" raw :test #'char-equal)
(and (search ".org" raw :test #'char-equal)
(or (search "defun" raw :test #'char-equal)
(search "tangle" raw :test #'char-equal)
(search "write-file" raw :test #'char-equal)
(search "lisp" raw :test #'char-equal)))
(search "defun " raw :test #'char-equal)
(search "repl-eval" raw :test #'char-equal)
(search "validate" raw :test #'char-equal))
(format nil "~%REPL-FIRST MANDATE:~%Before writing any defun to an Org file, prototype it in the REPL first. Set :repl-verified t on the write action. On rejection, fix the error and retry.~%"))))
Skill Registration
(defskill :passepartout-programming-repl
:priority 200
:trigger (lambda (ctx) (declare (ignore ctx)) nil)
:deterministic (lambda (action ctx) (declare (ignore action ctx)) nil)
:system-prompt-augment #'repl-mandate)