Files
passepartout/org/programming-repl.org
Amr Gharbeia 639bc348d9
Some checks failed
Deploy (Gitea) / deploy (push) Failing after 2s
passepartout: v0.4.1 Design Cleanup
- Remove system-prompt-augment mechanism, introduce *standing-mandates*
- Fix false token-overhead claims in DESIGN_DECISIONS + ROADMAP
- Update security vector count 9-10 across all docs and dispatcher docstring
- Rewrite README with agent section, soften aspirational claims
- Register 10 cognitive tools in programming-tools.org with test suite
- Enforce NO-HARDCODED-CONSTANTS in .env.example
- ROADMAP: mark v0.3.x patches DONE, add LOGBOOKs, mark releases
- AGENTS.md: rewrite compact (180 to 50 lines), move refs to CONTRIBUTING
- Normalize org tangle directives to file-level PROPERTY inheritance
2026-05-07 16:44:59 -04:00

10 KiB

SKILL: REPL (org-skill-repl.org)

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: Contract

  1. (repl-eval code-string &key package): evaluates Lisp code in a sandboxed environment (*read-eval* nil). Returns (values result output error) as three strings. Adds to *repl-history*.
  2. (repl-inspect symbol-name &key package): returns a formatted string describing the symbol's value, type, or function documentation.
  3. (repl-list-vars &key package): returns a list of bound variable names in the given package.

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).

Standing Mandate (repl-mandate)

The REPL-first mandate is registered as a standing mandate — it runs on every think() cycle, inspecting the user input for code-related keywords. When it matches, the mandate text is injected into the IDENTITY section of the system prompt.

;; 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))
(eval-when (:load-toplevel :execute)
  (push #'repl-mandate *standing-mandates*))

Test Suite

(eval-when (:compile-toplevel :load-toplevel :execute)
  (ql:quickload :fiveam :silent t))

(defpackage :passepartout-programming-repl-tests
  (:use :cl :fiveam :passepartout)
  (:export #:repl-suite))

(in-package :passepartout-programming-repl-tests)

(def-suite repl-suite :description "Verification of the REPL skill")
(in-suite repl-suite)

(test test-repl-eval-success
  "Contract 1: repl-eval returns result and no error for valid code."
  (multiple-value-bind (result output error) (repl-eval "(+ 1 2)")
    (is (equal "3" result))
    (is (null error))))

(test test-repl-eval-error
  "Contract 1: repl-eval returns error message for invalid code."
  (multiple-value-bind (result output error) (repl-eval "(+ 1 ")
    (is (null result))
    (is (stringp error))))

(test test-repl-inspect-found
  "Contract 2: repl-inspect returns description for a bound symbol."
  (let ((desc (repl-inspect "+" :package :cl)))
    (is (search "+" desc))))

(test test-repl-list-vars
  "Contract 3: repl-list-vars returns a list of symbol name strings."
  (let ((vars (repl-list-vars :package :keyword)))
    (is (listp vars))
    (is (member "PASSEPARTOUT" vars :test #'string-equal))))