feat: REPL development tool + naming drift fixes + HITL gateways
Some checks failed
Deploy (Gitea) / deploy (push) Failing after 3s

REPL tool:
- ~/.opencode/bin/repl — connects to running daemon, evaluates Lisp forms,
  returns results. Usage: repl '(+ 1 2)' or via stdin.
- Server-side handler in programming-repl skill registers for :repl-eval
  sensor, bypasses LLM pipeline, writes result back through reply-stream.
- Core provides pre-reason-handler registry (register-pre-reason-handler)
  for skills to register custom sensors without modifying core code.

HITL gateway integration:
- hitl-handle-message: TUI, Telegram, and Signal gateways intercept
  approval/deny commands before they reach the LLM.
- hitl-create/hitl-approve/hitl-deny: in-memory HITL store with correlation
  tokens for gateway-agnostic approval.
- loop-gate-perceive detects HITL commands and blocks LLM processing.

Naming drift fixes (the complete batch):
- register-actuator vs actuator-register — fixed to register-actuator
- process-signal vs loop-process — alias added
- perceive-gate/reason-gate/act-gate vs loop-gate-* — aliases added
- initialize-actuators vs actuator-initialize — fixed to actuator-initialize
- initialize-all-skills vs skill-initialize-all — fixed to skill-initialize-all
- inject-stimulus alias added for backward compatibility
- All original gateway-manager inject-stimulus → stimulus-inject + HITL check
This commit is contained in:
2026-05-03 13:46:32 -04:00
parent a77580c449
commit e0a47575e9
16 changed files with 209 additions and 49 deletions

View File

@@ -3,7 +3,7 @@
(defvar *actuator-registry* (make-hash-table :test 'equalp)
"Global registry mapping target keywords to their physical actuator functions.")
(defun actuator-register (name fn)
(defun register-actuator (name fn)
"Registers an actuator function. Actuators receive: (ACTION CONTEXT)."
(let ((key (if (keywordp name) name (intern (string-upcase (string name)) :keyword))))
(setf (gethash key *actuator-registry*) fn)))

View File

@@ -56,7 +56,7 @@
#:context-get-skill-telemetry
#:telemetry-track
#:context-assemble-global-awareness
#:loop-process
#:process-signal
#:loop-process
#:perceive-gate
#:probabilistic-gate
@@ -64,8 +64,9 @@
#:act-gate
#:reason-gate
#:dispatch-gate
#:register-pre-reason-handler
#:inject-stimulus
#:initialize-actuators
#:actuator-initialize
#:dispatch-action
#:register-actuator
#:load-skill-from-org

View File

@@ -96,6 +96,10 @@
(t (format nil "TOOL [~a] RESULT: ~s" tool-name result))))
(format nil "TOOL [~a] RESULT: ~a" tool-name result)))
;; Alias: act-gate → loop-gate-act
(defun act-gate (signal)
(loop-gate-act signal))
(defun loop-gate-act (signal)
"Final stage of the metabolic pipeline: Actuation.
For approval-required actions, creates a Flight Plan instead of executing."
@@ -106,21 +110,20 @@ For approval-required actions, creates a Flight Plan instead of executing."
(source (getf meta :source))
(feedback nil))
;; HITL: if the approved action requires human approval,
;; create a Flight Plan and notify the user via their client.
;; create a Flight Plan (Emacs) and HITL entry (all gateways).
(when (and approved
(eq (getf approved :level) :approval-required))
(let* ((payload (getf approved :payload))
(blocked-action (getf payload :action)))
(log-message "ACT: Action requires approval — creating Flight Plan")
(blocked-action (getf payload :action))
(hitl (hitl-create blocked-action)))
(log-message "ACT: Action requires approval — creating Flight Plan + HITL (~a)" (getf hitl :token))
(dispatcher-flight-plan-create blocked-action)
(setf (getf signal :status) :suspended)
;; Dispatch HITL notification to the user's client via the source actuator
(action-dispatch (list :target source
:payload (list :text
"HITL: Action requires your approval. Check Flight Plan and set TODO to APPROVED."))
:payload (list :text (getf hitl :message)))
signal)
(setf approved nil) ;; Don't execute the original action
(setf feedback nil))) ;; Don't loop back — wait for human
(setf approved nil)
(setf feedback nil)))
(when approved
(let* ((original-type (getf approved :type))
(verified (cognitive-verify approved signal)))

View File

@@ -8,6 +8,19 @@
(defvar *loop-focus-id* nil
"The Org ID of the node the user is currently interacting with.")
(defvar *pre-reason-handlers* (make-hash-table :test 'eq)
"Pre-reason handler registry: sensor keyword → handler function.")
(defun register-pre-reason-handler (sensor fn)
"Registers FN to handle signals with SENSOR in the perceive gate.
FN receives (signal) and returns T if consumed, nil to continue."
(setf (gethash sensor *pre-reason-handlers*) fn))
;; Alias for backward compatibility
(defun inject-stimulus (raw-message &key stream (depth 0))
"Alias for stimulus-inject."
(stimulus-inject raw-message :stream stream :depth depth))
(defun stimulus-inject (raw-message &key stream (depth 0))
"Inject a raw message into the signal processing pipeline."
(let* ((payload (getf raw-message :payload))
@@ -40,12 +53,28 @@
(skip-event ()
(log-message "SYSTEM RECOVERY: Stimulus dropped."))))))
;; Alias: perceive-gate → loop-gate-perceive
(defun perceive-gate (signal)
(loop-gate-perceive signal))
(defun loop-gate-perceive (signal)
"Stage 1 of the metabolic pipeline: Normalize sensory input."
(let* ((payload (getf signal :payload))
(type (getf signal :type))
(meta (getf signal :meta))
(sensor (getf payload :sensor)))
(type (getf signal :type))
(meta (getf signal :meta))
(sensor (getf payload :sensor)))
;; HITL: intercept approval/denial commands before LLM processing
(when (and (eq sensor :user-input)
(stringp (getf payload :text)))
(let ((text (getf payload :text)))
(when (ignore-errors (hitl-handle-message text (getf meta :source)))
(log-message "GATE [Perceive]: HITL command processed — ~a" text)
(return-from loop-gate-perceive signal))))
;; Pre-reason handlers: dispatch custom sensors to registered skill handlers
(let ((handler (gethash sensor *pre-reason-handlers*)))
(when handler
(when (funcall handler signal)
(return-from loop-gate-perceive signal))))
(log-message "GATE [Perceive]: ~a (~a) [Source: ~s]"
type (or sensor "no-sensor") (getf meta :source))
@@ -69,7 +98,7 @@
(:approval-required
(when (getf payload :approved)
(log-message "GATE [Perceive]: Approved Flight Plan re-injected")
(setf (getf signal :approved) t)
(setf (getf signal :approved) t)
(setf (getf signal :approved-action) (getf payload :action))))
;; Default sensor: pass through without requiring user-input processing
(otherwise

View File

@@ -121,6 +121,10 @@ modified action (for approval-required or pass)."
:action approval-action))
current-action)))
;; Alias: reason-gate → loop-gate-reason
(defun reason-gate (signal)
(loop-gate-reason signal))
(defun loop-gate-reason (signal)
(let* ((type (proto-get signal :type))
(payload (proto-get signal :payload))

View File

@@ -9,6 +9,10 @@
(defvar *heartbeat-thread* nil
"Handle to the heartbeat thread.")
;; Alias: process-signal → loop-process (backward compatibility)
(defun process-signal (signal)
(loop-process signal))
(defun loop-process (signal)
"The entry point to the Metabolic Pipeline: Perceive -> Reason -> Act."
(let ((current-signal signal))
@@ -111,8 +115,8 @@
(cl-dotenv:load-env env-file)))
(load-memory-from-disk)
(initialize-actuators)
(initialize-all-skills)
(actuator-initialize)
(skill-initialize-all)
;; Run proactive doctor before starting services
(diagnostics-startup-run)

View File

@@ -25,12 +25,13 @@
(chat-id (cdr (assoc :id chat)))
(text (cdr (assoc :text message))))
(setf (getf (gethash "telegram" *gateway-configs*) :last-update-id) update-id)
(when (and text chat-id)
(log-message "TELEGRAM: Received message from ~a" chat-id)
(inject-stimulus
(list :type :EVENT
:meta (list :source :telegram :chat-id (format nil "~a" chat-id))
:payload (list :sensor :user-input :text text)))))))
(when (and text chat-id)
(log-message "TELEGRAM: Received message from ~a" chat-id)
(unless (ignore-errors (hitl-handle-message text :telegram))
(stimulus-inject
(list :type :EVENT
:meta (list :source :telegram :chat-id (format nil "~a" chat-id))
:payload (list :sensor :user-input :text text)))))))
(error (c) (log-message "TELEGRAM POLL ERROR: ~a" c))))))
(defun telegram-send (action context)
@@ -69,12 +70,13 @@
(source (cdr (assoc :source envelope)))
(data-message (cdr (assoc :data-message envelope)))
(text (cdr (assoc :message data-message))))
(when (and source text)
(log-message "SIGNAL: Received message from ~a" source)
(inject-stimulus
(list :type :EVENT
:meta (list :source :signal :chat-id source)
:payload (list :sensor :user-input :text text))))))))
(when (and source text)
(log-message "SIGNAL: Received message from ~a" source)
(unless (ignore-errors (hitl-handle-message text :signal))
(stimulus-inject
(list :type :EVENT
:meta (list :source :signal :chat-id source)
:payload (list :sensor :user-input :text text))))))))
(error (c) (log-message "SIGNAL POLL ERROR: ~a" c))))))
(defun signal-send (action context)

View File

@@ -101,6 +101,30 @@ REPL Skill Commands:
- Show this message
"))
(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)
(defun repl-mandate (context)
"Returns REPL-first engineering mandate when context involves code editing."
(let ((raw (or (proto-get (proto-get context :payload) :text) "")))

View File

@@ -44,7 +44,7 @@ The global registry mapping target keywords (~:cli~, ~:telegram~, ~:signal~, etc
(defvar *actuator-registry* (make-hash-table :test 'equalp)
"Global registry mapping target keywords to their physical actuator functions.")
(defun actuator-register (name fn)
(defun register-actuator (name fn)
"Registers an actuator function. Actuators receive: (ACTION CONTEXT)."
(let ((key (if (keywordp name) name (intern (string-upcase (string name)) :keyword))))
(setf (gethash key *actuator-registry*) fn)))

View File

@@ -81,7 +81,7 @@ The package definition. All public symbols are exported here.
#:context-get-skill-telemetry
#:telemetry-track
#:context-assemble-global-awareness
#:loop-process
#:process-signal
#:loop-process
#:perceive-gate
#:probabilistic-gate
@@ -89,8 +89,9 @@ The package definition. All public symbols are exported here.
#:act-gate
#:reason-gate
#:dispatch-gate
#:register-pre-reason-handler
#:inject-stimulus
#:initialize-actuators
#:actuator-initialize
#:dispatch-action
#:register-actuator
#:load-skill-from-org

View File

@@ -195,6 +195,10 @@ After dispatch, the gate captures any feedback produced by the actuation (tool o
;; REPL-VERIFIED: 2026-05-03T13:00:00
#+begin_src lisp
;; Alias: act-gate → loop-gate-act
(defun act-gate (signal)
(loop-gate-act signal))
(defun loop-gate-act (signal)
"Final stage of the metabolic pipeline: Actuation.
For approval-required actions, creates a Flight Plan instead of executing."

View File

@@ -61,6 +61,30 @@ A global interrupt flag that can be set by any signal. When set, the metabolic l
(defvar *loop-focus-id* nil
"The Org ID of the node the user is currently interacting with.")
#+end_src
** Pre-Reason Handler Registry
Skills register handlers for custom sensors here. When a signal arrives
with a registered sensor, the handler is called in the perceive gate,
before the signal reaches the LLM. The handler receives the full signal
and returns T if the signal was consumed (don't continue to reason)
or nil if processing should proceed normally.
;; REPL-VERIFIED: 2026-05-03T13:00:00
#+begin_src lisp
(defvar *pre-reason-handlers* (make-hash-table :test 'eq)
"Pre-reason handler registry: sensor keyword → handler function.")
(defun register-pre-reason-handler (sensor fn)
"Registers FN to handle signals with SENSOR in the perceive gate.
FN receives (signal) and returns T if consumed, nil to continue."
(setf (gethash sensor *pre-reason-handlers*) fn))
;; Alias for backward compatibility
(defun inject-stimulus (raw-message &key stream (depth 0))
"Alias for stimulus-inject."
(stimulus-inject raw-message :stream stream :depth depth))
#+end_src
#+end_src
** Stimulus Injection (stimulus-inject)
@@ -117,12 +141,28 @@ All signals get tagged with their processing stage (`:status :perceived`) and th
;; REPL-VERIFIED: 2026-05-03T13:00:00
#+begin_src lisp
;; Alias: perceive-gate → loop-gate-perceive
(defun perceive-gate (signal)
(loop-gate-perceive signal))
(defun loop-gate-perceive (signal)
"Stage 1 of the metabolic pipeline: Normalize sensory input."
(let* ((payload (getf signal :payload))
(type (getf signal :type))
(meta (getf signal :meta))
(sensor (getf payload :sensor)))
(type (getf signal :type))
(meta (getf signal :meta))
(sensor (getf payload :sensor)))
;; HITL: intercept approval/denial commands before LLM processing
(when (and (eq sensor :user-input)
(stringp (getf payload :text)))
(let ((text (getf payload :text)))
(when (ignore-errors (hitl-handle-message text (getf meta :source)))
(log-message "GATE [Perceive]: HITL command processed — ~a" text)
(return-from loop-gate-perceive signal))))
;; Pre-reason handlers: dispatch custom sensors to registered skill handlers
(let ((handler (gethash sensor *pre-reason-handlers*)))
(when handler
(when (funcall handler signal)
(return-from loop-gate-perceive signal))))
(log-message "GATE [Perceive]: ~a (~a) [Source: ~s]"
type (or sensor "no-sensor") (getf meta :source))

View File

@@ -281,6 +281,10 @@ The retry limit prevents infinite loops. If the LLM cannot produce a passable pr
;; REPL-VERIFIED: 2026-05-03T13:00:00
#+begin_src lisp
;; Alias: reason-gate → loop-gate-reason
(defun reason-gate (signal)
(loop-gate-reason signal))
(defun loop-gate-reason (signal)
(let* ((type (proto-get signal :type))
(payload (proto-get signal :payload))

View File

@@ -80,6 +80,10 @@ The function handles four failure modes:
;; REPL-VERIFIED: 2026-05-03T13:00:00
#+begin_src lisp
;; Alias: process-signal → loop-process (backward compatibility)
(defun process-signal (signal)
(loop-process signal))
(defun loop-process (signal)
"The entry point to the Metabolic Pipeline: Perceive -> Reason -> Act."
(let ((current-signal signal))
@@ -253,8 +257,8 @@ Boot sequence:
(cl-dotenv:load-env env-file)))
(load-memory-from-disk)
(initialize-actuators)
(initialize-all-skills)
(actuator-initialize)
(skill-initialize-all)
;; Run proactive doctor before starting services
(diagnostics-startup-run)

View File

@@ -60,12 +60,13 @@ Registration of available gateway implementations: each platform registers its p
(chat-id (cdr (assoc :id chat)))
(text (cdr (assoc :text message))))
(setf (getf (gethash "telegram" *gateway-configs*) :last-update-id) update-id)
(when (and text chat-id)
(log-message "TELEGRAM: Received message from ~a" chat-id)
(inject-stimulus
(list :type :EVENT
:meta (list :source :telegram :chat-id (format nil "~a" chat-id))
:payload (list :sensor :user-input :text text)))))))
(when (and text chat-id)
(log-message "TELEGRAM: Received message from ~a" chat-id)
(unless (ignore-errors (hitl-handle-message text :telegram))
(stimulus-inject
(list :type :EVENT
:meta (list :source :telegram :chat-id (format nil "~a" chat-id))
:payload (list :sensor :user-input :text text)))))))
(error (c) (log-message "TELEGRAM POLL ERROR: ~a" c))))))
#+end_src
@@ -117,12 +118,13 @@ Registration of available gateway implementations: each platform registers its p
(source (cdr (assoc :source envelope)))
(data-message (cdr (assoc :data-message envelope)))
(text (cdr (assoc :message data-message))))
(when (and source text)
(log-message "SIGNAL: Received message from ~a" source)
(inject-stimulus
(list :type :EVENT
:meta (list :source :signal :chat-id source)
:payload (list :sensor :user-input :text text))))))))
(when (and source text)
(log-message "SIGNAL: Received message from ~a" source)
(unless (ignore-errors (hitl-handle-message text :signal))
(stimulus-inject
(list :type :EVENT
:meta (list :source :signal :chat-id source)
:payload (list :sensor :user-input :text text))))))))
(error (c) (log-message "SIGNAL POLL ERROR: ~a" c))))))
#+end_src

View File

@@ -195,6 +195,44 @@ REPL Skill Commands:
(is (not (null error)))))
#+end_src
** 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
#+begin_src lisp
(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)
#+end_src
* Phase E: Lifecycle
The REPL skill loads at priority 200 (after diagnostics at 100, before utils-lisp at 400).