Compare commits
10 Commits
c0d3f066e8
...
6aec587e90
| Author | SHA1 | Date | |
|---|---|---|---|
| 6aec587e90 | |||
| a3d07209b6 | |||
| b63f5477c1 | |||
| 1eb8a3db92 | |||
| dabf52f234 | |||
| 21c792b019 | |||
| dd8bb6e3c8 | |||
| ddc60b8ff7 | |||
| 1080f0b873 | |||
| 6a6f4479ac |
@@ -1,11 +1,6 @@
|
||||
# opencortex: Environment Configuration Template
|
||||
# Copy this to .env and fill in your values
|
||||
|
||||
# =============================================================================
|
||||
# INSTALLATION
|
||||
# =============================================================================
|
||||
INSTALL_DIR="$HOME/.opencortex"
|
||||
|
||||
# =============================================================================
|
||||
# IDENTITY
|
||||
# =============================================================================
|
||||
@@ -76,7 +71,6 @@ CONTEXT_LOG_LIMIT=20
|
||||
# MEMEX STRUCTURE
|
||||
# =============================================================================
|
||||
MEMEX_DIR="$HOME/memex"
|
||||
SKILLS_DIR="skills/"
|
||||
ZETTELKASTEN_DIR="$HOME/memex/notes"
|
||||
INBOX_DIR="$HOME/memex/inbox"
|
||||
DAILY_DIR="$HOME/memex/daily"
|
||||
|
||||
18
GEMINI.md
Normal file
18
GEMINI.md
Normal file
@@ -0,0 +1,18 @@
|
||||
# OpenCortex Agent Mandates
|
||||
|
||||
This file defines mandatory workflows and technical standards for the Gemini CLI agent operating within the OpenCortex environment. These mandates supersede general defaults.
|
||||
|
||||
## Lisp Integrity Mandates
|
||||
- **Validation:** Before applying any change to a `.lisp` file or a Lisp block in an `.org` file, you MUST use `utils-lisp-validate` to ensure structural and semantic integrity.
|
||||
- **Formatting:** All generated Lisp code MUST be piped through `utils-lisp-format` to maintain project-standard indentation before being saved.
|
||||
- **Structural Editing:** When modifying complex Lisp forms (nested macros or large functions), prefer using `utils-lisp-structural-extract` and `utils-lisp-structural-wrap` to avoid manual parenthesis errors.
|
||||
- **Verification:** For new or non-trivial logic, use `utils-lisp-eval` to test the behavior of the isolated S-expression in a live REPL environment before tangling.
|
||||
|
||||
## Literate Org Mandates
|
||||
- **AST Integrity:** When modifying Org files, utilize `utils-org-set-property`, `utils-org-set-todo`, and `utils-org-add-headline` to manipulate the document structure programmatically whenever possible.
|
||||
- **ID Management:** Every new headline intended for tracking or tangling MUST have a unique ID generated via `utils-org-generate-id`.
|
||||
|
||||
## Engineering Workflow
|
||||
- **Commit-Before-Modify:** Verify the git state is clean before starting a multi-file refactor.
|
||||
- **Tangle Sync:** After modifying any `.org` file, you MUST ensure the corresponding `.lisp` artifacts are tangled and in sync.
|
||||
- **Validation:** Run the project-specific test suite (`sbcl --load opencortex.asd`) after every significant change to verify system stability.
|
||||
@@ -1,6 +1,16 @@
|
||||
#+TITLE: Changelog
|
||||
#+STARTUP: content
|
||||
|
||||
* v0.2.0 - Interactive Refinement (2026-04-29)
|
||||
This release focuses on professionalizing the environment and enhancing the agent's structural capabilities.
|
||||
|
||||
** Features
|
||||
- **Enhanced Lisp/Org Utilities:** Structural editing, REPL evaluation, and automated formatting to ensure code integrity.
|
||||
- **Namespace Standardization:** Refactored utilities into =utils-org= and =utils-lisp= for predictable discovery.
|
||||
- **Autonomous Mandates:** Implemented =GEMINI.md= for local agentic enforcement of engineering standards.
|
||||
- **Onboarding Wizard:** Modular Lisp setup for multiple LLM providers.
|
||||
- **Professional TUI:** Styled, scrollable interface with improved diagnostics.
|
||||
|
||||
* v0.1.0 - The Autonomous Foundation (2026-04-20)
|
||||
This is the initial MVP release of the ~opencortex~. It establishes a secure, auditable Lisp kernel for a personal operating system.
|
||||
|
||||
|
||||
@@ -46,6 +46,7 @@ The "Brain" meets the "Machine." Standardization and professionalization of the
|
||||
| Onboarding Wizard | ✅ | Modular Lisp setup for multiple LLM providers. |
|
||||
| Linkage Command | ✅ | Real-time verification of external gateways (Telegram). |
|
||||
| Self-Editing | ✅ | Detects errors, applies fixes, learns from outcomes. |
|
||||
| Enhanced Utilities | ✅ | Structural Lisp/Org manipulation + REPL evaluation. |
|
||||
| Memory Rollback | ✅ | Snap back to known-good state on critical errors. |
|
||||
|
||||
*** v0.3.0: Event Orchestration + HITL
|
||||
|
||||
262
harness/act.lisp
262
harness/act.lisp
@@ -1,156 +1,66 @@
|
||||
(in-package :opencortex)
|
||||
|
||||
(defvar *default-actuator* :cli
|
||||
"The actuator used when no explicit target is specified.
|
||||
Override with DEFAULT_ACTUATOR environment variable.")
|
||||
"The actuator used when no explicit target is specified.")
|
||||
|
||||
(defvar *silent-actuators* '(:cli :system-message :emacs)
|
||||
"List of actuators that don't generate tool-output feedback.
|
||||
These typically have their own feedback mechanisms (CLI prints directly, etc.)")
|
||||
"List of actuators that don't generate tool-output feedback.")
|
||||
|
||||
(defun initialize-actuators ()
|
||||
"Load actuator configuration from environment and register core actuators.
|
||||
|
||||
Environment variables:
|
||||
- DEFAULT_ACTUATOR: Keyword for default target (:cli, :shell, etc.)
|
||||
- SILENT_ACTUATORS: Comma-separated list of actuators that skip feedback
|
||||
|
||||
Registers three core actuators:
|
||||
1. :system - Internal commands (eval, create-skill, message)
|
||||
2. :tool - Cognitive tool execution
|
||||
3. :tui - Terminal UI output via reply stream"
|
||||
|
||||
;; Load environment configuration
|
||||
"Register core actuators and load configuration."
|
||||
(let ((def (uiop:getenv "DEFAULT_ACTUATOR"))
|
||||
(silent (uiop:getenv "SILENT_ACTUATORS")))
|
||||
|
||||
;; Set default actuator
|
||||
(when def
|
||||
(setf *default-actuator*
|
||||
(intern (string-upcase def) "KEYWORD")))
|
||||
|
||||
;; Parse silent actuators list
|
||||
(setf *default-actuator* (intern (string-upcase def) :keyword)))
|
||||
(when silent
|
||||
(setf *silent-actuators*
|
||||
(mapcar (lambda (s)
|
||||
(intern (string-upcase (string-trim '(#\Space) s))
|
||||
"KEYWORD"))
|
||||
(str:split "," silent)))))
|
||||
(mapcar (lambda (s) (intern (string-upcase (string-trim '(#\Space) s)) :keyword))
|
||||
(uiop:split-string silent :separator '(#\,))))))
|
||||
|
||||
;; Register core harness actuators
|
||||
(register-actuator :system #'execute-system-action)
|
||||
(register-actuator :tool #'execute-tool-action)
|
||||
|
||||
;; TUI actuator: sends response back through the reply stream
|
||||
(register-actuator :tui (lambda (action context)
|
||||
(let* ((meta (getf context :meta))
|
||||
(declare (ignore context))
|
||||
(let* ((meta (getf action :meta))
|
||||
(stream (getf meta :reply-stream)))
|
||||
(when (and stream (open-stream-p stream))
|
||||
(format stream "~a" (frame-message action))
|
||||
(finish-output stream))))))
|
||||
|
||||
(defun dispatch-action (action context)
|
||||
"Route an approved action to its registered actuator.
|
||||
|
||||
ACTION is a plist with structure:
|
||||
(:TYPE :REQUEST :TARGET :shell :PAYLOAD (...))
|
||||
|
||||
CONTEXT is the signal being processed (for metadata access)
|
||||
|
||||
The target is resolved in order of priority:
|
||||
1. Explicit :target in the action
|
||||
2. :source from the original signal's metadata
|
||||
3. *default-actuator* configuration variable
|
||||
|
||||
Returns the actuator's result (may be a feedback signal or NIL)."
|
||||
|
||||
"Route an approved action to its registered actuator."
|
||||
(let ((payload (proto-get action :payload)))
|
||||
|
||||
;; Heartbeats don't generate actuation
|
||||
(when (eq (proto-get payload :sensor) :heartbeat)
|
||||
(return-from dispatch-action nil))
|
||||
|
||||
(when (and action (listp action))
|
||||
(let* ((meta (proto-get context :meta))
|
||||
(source (proto-get meta :source))
|
||||
(raw-target (or (ignore-errors (getf action :TARGET))
|
||||
(ignore-errors (getf action :target))
|
||||
source
|
||||
*default-actuator*))
|
||||
(raw-target (or (proto-get action :target) source *default-actuator*))
|
||||
(target (intern (string-upcase (string raw-target)) :keyword))
|
||||
(actuator-fn (gethash target *actuator-registry*)))
|
||||
|
||||
;; Preserve metadata in outbound action
|
||||
(when (and meta (null (getf action :meta)))
|
||||
(setf (getf action :meta) meta))
|
||||
|
||||
;; Execute or log error
|
||||
(if actuator-fn
|
||||
(funcall actuator-fn action context)
|
||||
(harness-log "ACT ERROR: No actuator registered for '~s' (requested by ~s)"
|
||||
target raw-target))))))
|
||||
(harness-log "ACT ERROR: No actuator registered for '~s'" target))))))
|
||||
|
||||
(defun execute-system-action (action context)
|
||||
"Execute internal harness commands.
|
||||
|
||||
This actuator handles meta-commands that affect the harness itself,
|
||||
rather than external side effects. Commands include:
|
||||
|
||||
- :eval - Evaluate arbitrary Lisp code (DANGEROUS, validate first!)
|
||||
- :create-skill - Write a new skill org file and reload
|
||||
- :message - Log a message to the harness log
|
||||
|
||||
These commands bypass the normal actuator system since they operate
|
||||
on the harness internals rather than external systems."
|
||||
|
||||
"Execute internal harness commands."
|
||||
(declare (ignore context))
|
||||
|
||||
(let* ((payload (ignore-errors (getf action :payload)))
|
||||
(cmd (ignore-errors (getf payload :action))))
|
||||
|
||||
(let* ((payload (getf action :payload))
|
||||
(cmd (getf payload :action)))
|
||||
(case cmd
|
||||
;; Evaluate Lisp code - guarded by lisp-utils skill
|
||||
(:eval
|
||||
(let ((code (getf payload :code)))
|
||||
(eval (read-from-string code))))
|
||||
|
||||
;; Create and load a new skill from content
|
||||
(:create-skill
|
||||
(let* ((filename (getf payload :filename))
|
||||
(content (getf payload :content))
|
||||
(skills-dir (merge-pathnames "skills/"
|
||||
(asdf:system-source-directory :opencortex)))
|
||||
(full-path (merge-pathnames filename skills-dir)))
|
||||
(with-open-file (out full-path
|
||||
:direction :output
|
||||
:if-exists :supersede)
|
||||
(write-string content out))
|
||||
(load-skill-from-org full-path)))
|
||||
|
||||
;; Log an informational message
|
||||
(eval (read-from-string (getf payload :code))))
|
||||
(:message
|
||||
(harness-log "ACT [System]: ~a" (getf payload :text)))
|
||||
|
||||
;; Unknown command
|
||||
(t
|
||||
(harness-log "ACT ERROR [System]: Unknown command '~s'" cmd)))))
|
||||
|
||||
(defun execute-tool-action (action context)
|
||||
"Execute a registered cognitive tool.
|
||||
|
||||
Tools are registered functions with:
|
||||
- A guard function (optional, for safety checks)
|
||||
- A body function (the actual implementation)
|
||||
- Metadata (description, parameter specs)
|
||||
|
||||
This actuator:
|
||||
1. Looks up the tool by name
|
||||
2. Runs the guard function (if present)
|
||||
3. Executes the body function with parsed arguments
|
||||
4. Returns a feedback signal with the result
|
||||
|
||||
The feedback mechanism allows tool results to trigger further reasoning."
|
||||
|
||||
"Execute a registered cognitive tool."
|
||||
(let* ((payload (getf action :payload))
|
||||
(tool-name (getf payload :tool))
|
||||
(tool-args (getf payload :args))
|
||||
@@ -158,156 +68,66 @@
|
||||
(meta (getf context :meta))
|
||||
(source (getf meta :source))
|
||||
(tool (gethash (string-downcase (string tool-name)) *cognitive-tools*)))
|
||||
|
||||
(if tool
|
||||
(handler-case
|
||||
;; Parse arguments (handle both flat and nested plists)
|
||||
(let* ((clean-args (if (and (listp tool-args)
|
||||
(listp (car tool-args)))
|
||||
(car tool-args)
|
||||
tool-args))
|
||||
(let* ((clean-args (if (and (listp tool-args) (listp (car tool-args))) (car tool-args) tool-args))
|
||||
(result (funcall (cognitive-tool-body tool) clean-args)))
|
||||
|
||||
;; Format result for source
|
||||
(when source
|
||||
(dispatch-action (list :TYPE :REQUEST
|
||||
:TARGET source
|
||||
:PAYLOAD (list :ACTION :MESSAGE
|
||||
:TEXT (format-tool-result tool-name result)))
|
||||
context))
|
||||
|
||||
;; Return feedback signal for potential further processing
|
||||
(list :TYPE :EVENT
|
||||
:DEPTH (1+ depth)
|
||||
:META meta
|
||||
:PAYLOAD (list :SENSOR :tool-output
|
||||
:RESULT result
|
||||
:TOOL tool-name)))
|
||||
|
||||
;; Tool execution error
|
||||
(dispatch-action (list :TYPE :REQUEST :TARGET source
|
||||
:PAYLOAD (list :ACTION :MESSAGE :TEXT (format-tool-result tool-name result)))
|
||||
context))
|
||||
(list :TYPE :EVENT :DEPTH (1+ depth) :META meta
|
||||
:PAYLOAD (list :SENSOR :tool-output :RESULT result :TOOL tool-name)))
|
||||
(error (c)
|
||||
(list :TYPE :EVENT
|
||||
:DEPTH (1+ depth)
|
||||
:META meta
|
||||
:PAYLOAD (list :SENSOR :tool-error
|
||||
:TOOL tool-name
|
||||
:MESSAGE (format nil "~a" c)))))
|
||||
|
||||
;; Tool not found
|
||||
(list :TYPE :EVENT
|
||||
:DEPTH (1+ depth)
|
||||
:META meta
|
||||
:PAYLOAD (list :SENSOR :tool-error
|
||||
:MESSAGE (format nil "Tool '~a' not found" tool-name))))))
|
||||
(list :TYPE :EVENT :DEPTH (1+ depth) :META meta
|
||||
:PAYLOAD (list :SENSOR :tool-error :TOOL tool-name :MESSAGE (format nil "~a" c)))))
|
||||
(list :TYPE :EVENT :DEPTH (1+ depth) :META meta
|
||||
:PAYLOAD (list :SENSOR :tool-error :MESSAGE (format nil "Tool '~a' not found" tool-name))))))
|
||||
|
||||
(defun format-tool-result (tool-name result)
|
||||
"Format a tool result for human-readable display.
|
||||
|
||||
Tools return either:
|
||||
- A plist: (:status :success :content \"...\") or (:status :error :message \"...\")
|
||||
- A raw value (string, number, etc.)
|
||||
|
||||
This function normalizes both formats into a consistent string presentation."
|
||||
|
||||
"Format a tool result for display."
|
||||
(if (listp result)
|
||||
(let ((status (getf result :status))
|
||||
(content (getf result :content))
|
||||
(msg (getf result :message)))
|
||||
(cond
|
||||
((and (eq status :success) content)
|
||||
(format nil "~a" content))
|
||||
((and (eq status :error) msg)
|
||||
(format nil "ERROR [~a]: ~a" tool-name msg))
|
||||
(t
|
||||
(format nil "TOOL [~a] RESULT: ~s" tool-name result))))
|
||||
((and (eq status :success) content) (format nil "~a" content))
|
||||
((and (eq status :error) msg) (format nil "ERROR [~a]: ~a" tool-name msg))
|
||||
(t (format nil "TOOL [~a] RESULT: ~s" tool-name result))))
|
||||
(format nil "TOOL [~a] RESULT: ~a" tool-name result)))
|
||||
|
||||
(defun act-gate (signal)
|
||||
"Final stage of the metabolic pipeline: Actuation.
|
||||
|
||||
This stage has three responsibilities:
|
||||
|
||||
1. Last-mile safety check: Run deterministic gates one more time
|
||||
before execution (handles race conditions, concurrent modifications)
|
||||
|
||||
2. Actuation: Dispatch the approved action to its target actuator
|
||||
|
||||
3. Feedback generation: If the action produced results, create a
|
||||
feedback signal that feeds back into the pipeline
|
||||
|
||||
Modifies the signal:
|
||||
- :approved-action - May be modified by last-mile verification
|
||||
- :status - Set to :acted
|
||||
|
||||
Returns a feedback signal if the action produced results, otherwise NIL."
|
||||
|
||||
"Final stage of the metabolic pipeline: Actuation."
|
||||
(let* ((approved (getf signal :approved-action))
|
||||
(type (getf signal :type))
|
||||
(meta (getf signal :meta))
|
||||
(source (getf meta :source))
|
||||
(feedback nil)
|
||||
(context signal))
|
||||
|
||||
;; Step 1: Last-mile deterministic verification
|
||||
;; This catches any issues that arose between reasoning and acting
|
||||
(feedback nil))
|
||||
(when approved
|
||||
(let* ((original-type (getf approved :type))
|
||||
(verified (deterministic-verify approved signal)))
|
||||
|
||||
;; Check if deterministic verification blocked the action
|
||||
(if (and (listp verified)
|
||||
(member (getf verified :type) '(:LOG :EVENT :log :event))
|
||||
(not (member original-type '(:LOG :EVENT :log :event))))
|
||||
|
||||
;; Action was blocked by verification
|
||||
(if (and (listp verified) (member (getf verified :type) '(:LOG :EVENT)) (not (member original-type '(:LOG :EVENT))))
|
||||
(progn
|
||||
(harness-log "ACT BLOCKED: Action failed last-mile deterministic check.")
|
||||
(setf (getf signal :approved-action) nil)
|
||||
(setf approved nil)
|
||||
(setf feedback verified))
|
||||
|
||||
;; Action passed verification
|
||||
(progn
|
||||
(setf (getf signal :approved-action) verified)
|
||||
(setf approved verified)))))
|
||||
|
||||
;; Step 2: Actuation based on signal type
|
||||
(case type
|
||||
;; Explicit requests go directly to dispatch
|
||||
(:REQUEST
|
||||
(dispatch-action signal context))
|
||||
|
||||
;; Log messages also dispatch
|
||||
(:LOG
|
||||
(dispatch-action signal context))
|
||||
|
||||
;; Events with approved actions dispatch to their target
|
||||
(:REQUEST (dispatch-action signal signal))
|
||||
(:LOG (dispatch-action signal signal))
|
||||
(:EVENT
|
||||
(if approved
|
||||
(let* ((target (getf approved :target))
|
||||
(result (dispatch-action approved context)))
|
||||
|
||||
;; Determine feedback based on actuator response
|
||||
(result (dispatch-action approved signal)))
|
||||
(cond
|
||||
;; Actuator returned a signal - use it as feedback
|
||||
((and (listp result)
|
||||
(member (getf result :type) '(:EVENT :LOG)))
|
||||
((and (listp result) (member (getf result :type) '(:EVENT :LOG)))
|
||||
(setf feedback result))
|
||||
|
||||
;; Non-silent actuator with result - format as tool-output
|
||||
((and result
|
||||
(not (member target *silent-actuators*)))
|
||||
(setf feedback (list :type :EVENT
|
||||
:depth (1+ (getf signal :depth 0))
|
||||
:meta meta
|
||||
:payload (list :sensor :tool-output
|
||||
:result result
|
||||
:tool approved))))))
|
||||
|
||||
;; No approved action, but have source - might be raw event
|
||||
(when source
|
||||
(dispatch-action signal context)))))
|
||||
|
||||
;; Step 3: Update signal status
|
||||
((and result (not (member target *silent-actuators*)))
|
||||
(setf feedback (list :type :EVENT :depth (1+ (getf signal :depth 0)) :meta meta
|
||||
:payload (list :sensor :tool-output :result result :tool approved))))))
|
||||
(when source (dispatch-action signal signal)))))
|
||||
(setf (getf signal :status) :acted)
|
||||
feedback))
|
||||
|
||||
@@ -1,44 +1,9 @@
|
||||
(in-package :opencortex)
|
||||
|
||||
(defun validate-communication-protocol-schema (msg)
|
||||
"Strict structural validation for incoming communication protocol messages."
|
||||
(unless (listp msg)
|
||||
(error "Communication Protocol Schema Error: Message must be a property list (got ~s)" (type-of msg)))
|
||||
|
||||
(let ((type (let ((raw (proto-get msg :type))) (if (keywordp raw) (intern (string-upcase (string raw)) :keyword) raw))))
|
||||
"Strict structural validation for incoming protocol messages."
|
||||
(unless (listp msg) (error "Message must be a plist"))
|
||||
(let ((type (proto-get msg :type)))
|
||||
(unless (member type '(:REQUEST :EVENT :RESPONSE :LOG :STATUS))
|
||||
(progn (harness-log "REJECTED MSG: ~s" msg) (error "Communication Protocol Schema Error: Invalid message type '~a'" type)))
|
||||
|
||||
(case type
|
||||
(:REQUEST
|
||||
;; Allow missing :target if :source is present in :meta, since reason-gate
|
||||
;; will infer :target from :source downstream. This preserves "equality of
|
||||
;; clients" — gateways need not duplicate routing logic.
|
||||
(let ((target (proto-get msg :target))
|
||||
(source (proto-get (proto-get msg :meta) :source)))
|
||||
(unless (or target source)
|
||||
(error "Communication Protocol Schema Error: REQUEST missing mandatory :target and no :source in :meta to infer it"))
|
||||
(unless (proto-get msg :payload)
|
||||
(error "Communication Protocol Schema Error: REQUEST missing mandatory :payload"))))
|
||||
|
||||
(:EVENT
|
||||
(let ((payload (proto-get msg :payload)))
|
||||
(unless (and payload (listp payload))
|
||||
(error "Communication Protocol Schema Error: EVENT missing or invalid :payload"))
|
||||
(unless (or (proto-get payload :action) (proto-get payload :sensor))
|
||||
(error "Communication Protocol Schema Error: EVENT payload must contain :action or :sensor"))))
|
||||
|
||||
(:RESPONSE
|
||||
(unless (proto-get msg :payload)
|
||||
(error "Communication Protocol Schema Error: RESPONSE missing mandatory :payload"))))
|
||||
|
||||
(error "Invalid message type '~a'" type))
|
||||
t))
|
||||
|
||||
(defskill :skill-communication-protocol-validator
|
||||
:priority 95
|
||||
:trigger (lambda (ctx) (member (getf (getf ctx :payload) :sensor) '(:protocol-received)))
|
||||
:probabilistic nil
|
||||
:deterministic (lambda (action ctx)
|
||||
(declare (ignore ctx))
|
||||
(validate-communication-protocol-schema action)
|
||||
action))
|
||||
|
||||
@@ -1,14 +1,5 @@
|
||||
(in-package :opencortex)
|
||||
|
||||
(defun proto-get (plist key)
|
||||
"Robustly retrieves a value from a plist, checking both uppercase and lowercase keyword versions."
|
||||
(let* ((s (string key))
|
||||
(up (intern (string-upcase s) :keyword))
|
||||
(dn (intern (string-downcase s) :keyword)))
|
||||
(or (getf plist up) (getf plist dn))))
|
||||
|
||||
(in-package :opencortex)
|
||||
|
||||
(defvar *actuator-registry* (make-hash-table :test 'equalp)
|
||||
"Global registry mapping target keywords to their physical actuator functions.")
|
||||
|
||||
@@ -17,48 +8,6 @@
|
||||
(let ((key (if (keywordp name) name (intern (string-upcase (string name)) :keyword))))
|
||||
(setf (gethash key *actuator-registry*) fn)))
|
||||
|
||||
;; Removed duplicate frame-message - kept the sanitized version below
|
||||
|
||||
(defun read-framed-message (stream)
|
||||
"Reads a hex-length prefixed S-expression from the stream securely. Skips leading whitespace."
|
||||
(let ((length-buffer (make-string 6)))
|
||||
(handler-case
|
||||
(progn
|
||||
;; 1. Skip leading whitespace (newlines, spaces, etc.)
|
||||
(loop for char = (peek-char nil stream nil :eof)
|
||||
while (and (not (eq char :eof)) (member char '(#\Space #\Newline #\Tab #\Return)))
|
||||
do (read-char stream))
|
||||
|
||||
;; 2. Read the 6-char hex length
|
||||
(let ((count (read-sequence length-buffer stream)))
|
||||
(cond ((< count 6) :eof)
|
||||
(t (let ((len (ignore-errors (parse-integer length-buffer :radix 16))))
|
||||
(if (not len)
|
||||
(progn
|
||||
(harness-log "PROTOCOL ERROR: Invalid header ~s. Attempting resync..." length-buffer)
|
||||
:error)
|
||||
(let ((msg-buffer (make-string len)))
|
||||
(read-sequence msg-buffer stream)
|
||||
(let ((*read-eval* nil)
|
||||
(*print-pretty* nil))
|
||||
(handler-case
|
||||
(let ((msg (read-from-string msg-buffer)))
|
||||
(validate-communication-protocol-schema msg)
|
||||
msg)
|
||||
(error (c)
|
||||
(harness-log "PROTOCOL PARSE ERROR: ~a in ~s" c msg-buffer)
|
||||
:error))))))))))
|
||||
(error (c)
|
||||
(harness-log "PROTOCOL READ ERROR: ~a" c)
|
||||
:error))))
|
||||
|
||||
(defun make-hello-message (version)
|
||||
"Constructs the standard HELLO handshake message."
|
||||
(list :TYPE :EVENT
|
||||
:PAYLOAD (list :ACTION :handshake
|
||||
:VERSION version
|
||||
:CAPABILITIES '(:AUTH :SWANK :ORG-AST))))
|
||||
|
||||
(defun sanitize-protocol-message (msg)
|
||||
"Recursively strips non-serializable objects from a protocol plist."
|
||||
(if (and msg (listp msg))
|
||||
@@ -76,3 +25,73 @@
|
||||
(payload (let ((*print-pretty* nil) (*read-eval* nil)) (format nil "~s" sanitized)))
|
||||
(len (length payload)))
|
||||
(format nil "~6,'0x~a" len payload)))
|
||||
|
||||
(defun read-framed-message (stream)
|
||||
"Reads a hex-length prefixed S-expression from the stream securely."
|
||||
(let ((length-buffer (make-string 6)))
|
||||
(handler-case
|
||||
(progn
|
||||
(loop for char = (peek-char nil stream nil :eof)
|
||||
while (and (not (eq char :eof)) (member char '(#\Space #\Newline #\Tab #\Return)))
|
||||
do (read-char stream))
|
||||
(let ((count (read-sequence length-buffer stream)))
|
||||
(if (< count 6)
|
||||
:eof
|
||||
(let ((len (ignore-errors (parse-integer length-buffer :radix 16))))
|
||||
(if (not len)
|
||||
:error
|
||||
(let ((msg-buffer (make-string len)))
|
||||
(read-sequence msg-buffer stream)
|
||||
(let ((*read-eval* nil))
|
||||
(handler-case (read-from-string msg-buffer)
|
||||
(error () :error)))))))))
|
||||
(error () :error))))
|
||||
|
||||
(defvar *server-socket* nil)
|
||||
|
||||
(defun handle-client-connection (socket)
|
||||
"Handles a single TUI/CLI client connection in a dedicated thread."
|
||||
(let ((stream (usocket:socket-stream socket)))
|
||||
(handler-case
|
||||
(progn
|
||||
(format stream "~a" (frame-message (make-hello-message "0.2.0")))
|
||||
(finish-output stream)
|
||||
(loop
|
||||
(let ((msg (read-framed-message stream)))
|
||||
(cond
|
||||
((eq msg :eof) (return))
|
||||
((eq msg :error) (return))
|
||||
((eq (getf msg :type) :health-check)
|
||||
;; Handle health check request
|
||||
(let ((health-msg (list :type :health-response
|
||||
:status (or (and (boundp 'opencortex::*system-health*)
|
||||
(symbol-value 'opencortex::*system-health*))
|
||||
:unknown)
|
||||
:checked-p (or (and (boundp 'opencortex::*health-check-ran*)
|
||||
(symbol-value 'opencortex::*health-check-ran*))
|
||||
nil))))
|
||||
(format stream "~a" (frame-message health-msg))
|
||||
(finish-output stream)))
|
||||
(t (inject-stimulus msg :stream stream))))))
|
||||
(error (c) (harness-log "CLIENT ERROR: ~a" c)))
|
||||
(ignore-errors (usocket:socket-close socket))))
|
||||
|
||||
(defun start-daemon (&key (port 9105))
|
||||
"Starts the network listener for TUI/CLI clients."
|
||||
(setf *server-socket* (usocket:socket-listen "127.0.0.1" port :reuse-address t))
|
||||
(harness-log "DAEMON: Listening on localhost:~a" port)
|
||||
(bt:make-thread
|
||||
(lambda ()
|
||||
(loop
|
||||
(let ((client-socket (usocket:socket-accept *server-socket*)))
|
||||
(when client-socket
|
||||
(bt:make-thread (lambda () (handle-client-connection client-socket))
|
||||
:name "opencortex-client-handler")))))
|
||||
:name "opencortex-server-listener"))
|
||||
|
||||
(defun make-hello-message (version)
|
||||
"Constructs the standard HELLO handshake message."
|
||||
(list :TYPE :EVENT
|
||||
:PAYLOAD (list :ACTION :handshake
|
||||
:VERSION version
|
||||
:CAPABILITIES '(:AUTH :ORG-AST))))
|
||||
|
||||
@@ -60,9 +60,6 @@
|
||||
(cosine-similarity foveal-vector obj-vector)
|
||||
0.0))
|
||||
(is-semantically-relevant (>= similarity threshold))
|
||||
;; We always render depth 1 and 2 (Projects and main tasks).
|
||||
;; We always render the foveal node and its immediate children.
|
||||
;; We render deeper nodes ONLY if they are semantically relevant.
|
||||
(should-render (or (<= depth 2) is-foveal is-semantically-relevant))
|
||||
(output ""))
|
||||
|
||||
@@ -72,15 +69,12 @@
|
||||
(setf output (concatenate 'string output (format nil ":SEMANTIC_SCORE: ~,2f~%" similarity))))
|
||||
(setf output (concatenate 'string output (format nil ":END:~%")))
|
||||
|
||||
;; Only include full body content if this is the Foveal focus or highly relevant
|
||||
(when (and content (or is-foveal is-semantically-relevant))
|
||||
(setf output (concatenate 'string output content (string #\Newline))))
|
||||
|
||||
;; Recursively render children
|
||||
(dolist (child-id children)
|
||||
(let ((child-obj (lookup-object child-id)))
|
||||
(when child-obj
|
||||
;; If the current node is Foveal, its children should be rendered (depth effectively resets)
|
||||
(let ((next-foveal (if is-foveal child-id foveal-id)))
|
||||
(setf output (concatenate 'string output
|
||||
(context-render-to-org child-obj
|
||||
@@ -109,8 +103,7 @@
|
||||
(let* ((foveal-id (or (getf signal :foveal-focus)
|
||||
(ignore-errors (getf (getf signal :payload) :target-id))))
|
||||
(projects (context-get-active-projects))
|
||||
(output "GLOBAL MEMEX AWARENESS (Peripheral Vision):
|
||||
"))
|
||||
(output (format nil "GLOBAL MEMEX AWARENESS (Peripheral Vision):~%")))
|
||||
(if projects
|
||||
(dolist (project projects)
|
||||
(setf output (concatenate 'string output
|
||||
|
||||
@@ -64,6 +64,7 @@
|
||||
(let ((dep-ok (doctor-check-dependencies))
|
||||
(env-ok (doctor-check-env))
|
||||
(llm-ok (doctor-check-llm)))
|
||||
(declare (ignore llm-ok))
|
||||
(harness-log "==================================================")
|
||||
(if (and dep-ok env-ok)
|
||||
(progn
|
||||
|
||||
@@ -24,65 +24,58 @@ Common Lisp's `getenv` is strictly typed in SBCL. The Doctor must ensure that mi
|
||||
* Phase B: Protocol (Success Criteria)
|
||||
|
||||
** Package Context
|
||||
#+begin_src lisp :tangle doctor.lisp (concat (concat (or (getenv "INSTALL_DIR ". "/harness "/tests)
|
||||
#+begin_src lisp :tangle ../tests/doctor-tests.lisp
|
||||
(defpackage :opencortex-doctor-tests
|
||||
(:use :cl :fiveam :opencortex)
|
||||
(:export #:doctor-suite))
|
||||
#+end_src
|
||||
|
||||
#+begin_src lisp :tangle doctor.lisp (concat (concat (or (getenv "INSTALL_DIR ". "/harness "/tests)
|
||||
(in-package :opencortex-doctor-tests)
|
||||
#+end_src
|
||||
|
||||
#+begin_src lisp :tangle doctor.lisp (concat (concat (or (getenv "INSTALL_DIR ". "/harness "/tests)
|
||||
(def-suite doctor-suite :description "Verification of the System Doctor diagnostic logic
|
||||
#+end_src
|
||||
|
||||
#+begin_src lisp :tangle doctor.lisp (concat (concat (or (getenv "INSTALL_DIR ". "/harness "/tests)
|
||||
(def-suite doctor-suite :description "Verification of the System Doctor diagnostic logic")
|
||||
(in-suite doctor-suite)
|
||||
#+end_src
|
||||
|
||||
** Dependency Tests
|
||||
#+begin_src lisp :tangle doctor.lisp (concat (concat (or (getenv "INSTALL_DIR ". "/harness "/tests)
|
||||
#+begin_src lisp :tangle ../tests/doctor-tests.lisp
|
||||
(test test-dependency-check-fail
|
||||
"Verify that missing binaries are correctly identified as failures."
|
||||
(let ((opencortex::*doctor-required-binaries* '("non-existent-binary-123))
|
||||
(let ((opencortex::*doctor-required-binaries* '("non-existent-binary-123")))
|
||||
(is (null (opencortex:doctor-check-dependencies)))))
|
||||
#+end_src
|
||||
|
||||
** Environment Tests
|
||||
#+begin_src lisp :tangle doctor.lisp (concat (concat (or (getenv "INSTALL_DIR ". "/harness "/tests)
|
||||
#+begin_src lisp :tangle ../tests/doctor-tests.lisp
|
||||
(test test-env-validation-fail
|
||||
"Verify that an invalid MEMEX_DIR triggers a critical failure."
|
||||
(let ((old-m (getenv "MEMEX_DIR)
|
||||
(old-s (getenv "SKILLS_DIR))
|
||||
(let ((old-m (uiop:getenv "MEMEX_DIR"))
|
||||
(old-s (uiop:getenv "SKILLS_DIR")))
|
||||
(unwind-protect
|
||||
(progn
|
||||
(setf (getenv "MEMEX_DIR "/non/existent/path/999
|
||||
(setf (uiop:getenv "MEMEX_DIR") "/non/existent/path/999")
|
||||
(is (null (opencortex:doctor-check-env))))
|
||||
(setf (getenv "MEMEX_DIR (or old-m
|
||||
(setf (getenv "SKILLS_DIR (or old-s )))
|
||||
(setf (uiop:getenv "MEMEX_DIR") (or old-m ""))
|
||||
(setf (uiop:getenv "SKILLS_DIR") (or old-s "")))))
|
||||
#+end_src
|
||||
|
||||
* Phase C: Implementation (Build)
|
||||
|
||||
** Package Context
|
||||
#+begin_src lisp :tangle doctor.lisp )
|
||||
#+begin_src lisp
|
||||
(in-package :opencortex)
|
||||
#+end_src
|
||||
|
||||
** Global Configuration
|
||||
#+begin_src lisp :tangle doctor.lisp )
|
||||
(defvar *doctor-required-binaries* '("sbcl" "emacs" "git" "socat" "nc
|
||||
"List of external binaries required for full system operation.
|
||||
#+begin_src lisp
|
||||
(defvar *doctor-required-binaries* '("sbcl" "emacs" "git" "socat" "nc")
|
||||
"List of external binaries required for full system operation.")
|
||||
#+end_src
|
||||
|
||||
** Dependency Verification
|
||||
#+begin_src lisp :tangle doctor.lisp )
|
||||
#+begin_src lisp
|
||||
(defun doctor-check-dependencies ()
|
||||
"Verifies that required external binaries are available in the PATH via a shell probe."
|
||||
(let ((all-ok t))
|
||||
(harness-log "DOCTOR: Checking system dependencies...
|
||||
(harness-log "DOCTOR: Checking system dependencies...")
|
||||
(dolist (dep *doctor-required-binaries*)
|
||||
(let ((path (ignore-errors
|
||||
(uiop:run-program (list "which" dep)
|
||||
@@ -96,15 +89,15 @@ Common Lisp's `getenv` is strictly typed in SBCL. The Doctor must ensure that mi
|
||||
#+end_src
|
||||
|
||||
** Environment & XDG Validation
|
||||
#+begin_src lisp :tangle doctor.lisp )
|
||||
#+begin_src lisp
|
||||
(defun doctor-check-env ()
|
||||
"Validates XDG directories and environment configuration against the POSIX standard."
|
||||
(harness-log "DOCTOR: Checking XDG environment...
|
||||
(harness-log "DOCTOR: Checking XDG environment...")
|
||||
(let ((all-ok t)
|
||||
(config-dir (getenv "OC_CONFIG_DIR)
|
||||
(data-dir (getenv "OC_DATA_DIR)
|
||||
(state-dir (getenv "OC_STATE_DIR)
|
||||
(memex-dir (getenv "MEMEX_DIR))
|
||||
(config-dir (uiop:getenv "OC_CONFIG_DIR"))
|
||||
(data-dir (uiop:getenv "OC_DATA_DIR"))
|
||||
(state-dir (uiop:getenv "OC_STATE_DIR"))
|
||||
(memex-dir (uiop:getenv "MEMEX_DIR")))
|
||||
|
||||
(flet ((check-dir (name path critical)
|
||||
(if (and path (> (length path) 0))
|
||||
@@ -125,42 +118,43 @@ Common Lisp's `getenv` is strictly typed in SBCL. The Doctor must ensure that mi
|
||||
#+end_src
|
||||
|
||||
** LLM Connectivity
|
||||
#+begin_src lisp :tangle doctor.lisp )
|
||||
#+begin_src lisp
|
||||
(defun doctor-check-llm ()
|
||||
"Tests connectivity to primary LLM providers. Non-critical fallback allowed."
|
||||
(harness-log "DOCTOR: Checking LLM connectivity...
|
||||
(let ((openrouter-key (getenv "OPENROUTER_API_KEY))
|
||||
(harness-log "DOCTOR: Checking LLM connectivity...")
|
||||
(let ((openrouter-key (uiop:getenv "OPENROUTER_API_KEY")))
|
||||
(if (and openrouter-key (> (length openrouter-key) 0))
|
||||
(progn
|
||||
(harness-log " [OK] OpenRouter API Key detected.
|
||||
(harness-log " [OK] OpenRouter API Key detected.")
|
||||
t)
|
||||
(progn
|
||||
(harness-log " [WARN] No OpenRouter API Key. Falling back to local inference only.
|
||||
(harness-log " [WARN] No OpenRouter API Key. Falling back to local inference only.")
|
||||
t))))
|
||||
#+end_src
|
||||
|
||||
** Orchestration
|
||||
#+begin_src lisp :tangle doctor.lisp )
|
||||
#+begin_src lisp
|
||||
(defun doctor-run-all ()
|
||||
"Executes the full diagnostic suite and returns T if system is healthy."
|
||||
(harness-log "==================================================
|
||||
(harness-log " OPENCORTEX DOCTOR: Commencing Health Check
|
||||
(harness-log "==================================================
|
||||
(harness-log "==================================================")
|
||||
(harness-log " OPENCORTEX DOCTOR: Commencing Health Check")
|
||||
(harness-log "==================================================")
|
||||
(let ((dep-ok (doctor-check-dependencies))
|
||||
(env-ok (doctor-check-env))
|
||||
(llm-ok (doctor-check-llm)))
|
||||
(harness-log "==================================================
|
||||
(declare (ignore llm-ok))
|
||||
(harness-log "==================================================")
|
||||
(if (and dep-ok env-ok)
|
||||
(progn
|
||||
(harness-log " ✓ SYSTEM HEALTHY: Ready for ignition.
|
||||
(harness-log " ✓ SYSTEM HEALTHY: Ready for ignition.")
|
||||
t)
|
||||
(progn
|
||||
(harness-log " ✗ SYSTEM UNHEALTHY: Fix the errors above.
|
||||
(harness-log " ✗ SYSTEM UNHEALTHY: Fix the errors above.")
|
||||
nil))))
|
||||
#+end_src
|
||||
|
||||
** CLI Entry Point
|
||||
#+begin_src lisp :tangle doctor.lisp )
|
||||
#+begin_src lisp
|
||||
(defun doctor-main ()
|
||||
"Entry point for the 'doctor' CLI command."
|
||||
(if (doctor-run-all)
|
||||
|
||||
@@ -1,108 +1,55 @@
|
||||
(in-package :opencortex)
|
||||
|
||||
(defvar *interrupt-flag* nil
|
||||
"Atomic flag set by signal handlers to trigger graceful shutdown.
|
||||
Using a dedicated variable avoids race conditions in interrupt handling.")
|
||||
"Atomic flag set by signal handlers to trigger graceful shutdown.")
|
||||
|
||||
(defvar *interrupt-lock* (bt:make-lock "harness-interrupt-lock")
|
||||
"Mutex protecting *interrupt-flag* access.
|
||||
Locking is required because SBCL's interrupt handlers run in uncertain contexts.")
|
||||
"Mutex protecting *interrupt-flag* access.")
|
||||
|
||||
(defvar *heartbeat-thread* nil
|
||||
"Handle to the heartbeat thread, allowing explicit termination on shutdown.")
|
||||
"Handle to the heartbeat thread.")
|
||||
|
||||
(defun process-signal (signal)
|
||||
"The entry point to the Metabolic Pipeline: Perceive -> Reason -> Act.
|
||||
|
||||
SIGNAL is a property list with the following structure:
|
||||
- :type - :EVENT, :REQUEST, :RESPONSE, etc.
|
||||
- :payload - The actual content (sensor data, approved actions, etc.)
|
||||
- :meta - Metadata including source, session, reply stream
|
||||
- :depth - Recursion depth counter (starts at 0)
|
||||
- :status - Processing status (:perceived, :reasoned, :acted)
|
||||
|
||||
Returns NIL when processing is complete, or a new signal for feedback loop."
|
||||
|
||||
"The entry point to the Metabolic Pipeline: Perceive -> Reason -> Act."
|
||||
(let ((current-signal signal))
|
||||
(loop while current-signal do
|
||||
|
||||
;; Depth limiting prevents infinite recursion from feedback loops
|
||||
(let ((depth (getf current-signal :depth 0))
|
||||
(meta (getf current-signal :meta)))
|
||||
(when (> depth 10)
|
||||
(harness-log "METABOLISM ERROR: Max recursion depth reached.")
|
||||
(return nil))
|
||||
|
||||
;; Check for graceful shutdown interrupt
|
||||
(when (bt:with-lock-held (*interrupt-lock*) *interrupt-flag*)
|
||||
(harness-log "METABOLISM: Interrupted by shutdown signal.")
|
||||
(bt:with-lock-held (*interrupt-lock*) (setf *interrupt-flag* nil))
|
||||
(return nil))
|
||||
|
||||
;; The three-stage pipeline wrapped in error handling
|
||||
(handler-case
|
||||
(progn
|
||||
;; Stage 1: Perceive - normalize sensory input
|
||||
(setf current-signal (perceive-gate current-signal))
|
||||
|
||||
;; Stage 2: Reason - generate and verify action proposals
|
||||
(setf current-signal (reason-gate current-signal))
|
||||
|
||||
;; Stage 3: Act - execute approved actions
|
||||
(let ((feedback (act-gate current-signal)))
|
||||
(if feedback
|
||||
;; Action generated a feedback signal - continue processing
|
||||
(progn
|
||||
;; Preserve metadata from original signal
|
||||
(unless (getf feedback :meta)
|
||||
(setf (getf feedback :meta) meta))
|
||||
(unless (getf feedback :meta) (setf (getf feedback :meta) meta))
|
||||
(setf current-signal feedback))
|
||||
;; No feedback - pipeline complete
|
||||
(setf current-signal nil))))
|
||||
|
||||
;; Error recovery with differentiated response
|
||||
(error (c)
|
||||
(let ((sensor (ignore-errors (getf (getf current-signal :payload) :sensor))))
|
||||
(harness-log "METABOLISM CRASH [~a]: ~a" (or sensor :unknown) c)
|
||||
|
||||
;; Only rollback memory on critical errors, not transient tool failures
|
||||
;; This prevents losing recent context due to a single bad API call
|
||||
(unless (member sensor '(:loop-error :tool-error :syntax-error))
|
||||
(harness-log "CRITICAL ERROR: Initiating Micro-Rollback.")
|
||||
(rollback-memory 0))
|
||||
|
||||
;; At deep recursion or known error types, terminate gracefully
|
||||
(if (or (> depth 2) (member sensor '(:loop-error :tool-error)))
|
||||
(setf current-signal nil)
|
||||
;; Otherwise, convert error to a loop-error signal for retry
|
||||
(setf current-signal
|
||||
(list :type :EVENT
|
||||
:depth (1+ depth)
|
||||
:meta meta
|
||||
:payload (list :sensor :loop-error
|
||||
:message (format nil "~a" c)
|
||||
:depth depth)))))))))))
|
||||
(list :type :EVENT :depth (1+ depth) :meta meta
|
||||
:payload (list :sensor :loop-error :message (format nil "~a" c) :depth depth)))))))))))
|
||||
|
||||
(defvar *auto-save-interval* 300
|
||||
"Interval in seconds between automatic memory saves.
|
||||
Defaults to 300 seconds (5 minutes). Set via MEMORY_AUTO_SAVE_INTERVAL env var.")
|
||||
|
||||
(defvar *heartbeat-save-counter* 0
|
||||
"Tracks heartbeats since last save, used to calculate auto-save timing.")
|
||||
(defvar *auto-save-interval* 300)
|
||||
(defvar *heartbeat-save-counter* 0)
|
||||
|
||||
(defun start-heartbeat ()
|
||||
"Starts the background heartbeat thread.
|
||||
|
||||
The heartbeat runs in a dedicated thread to avoid blocking the main
|
||||
signal processing loop. Each heartbeat:
|
||||
|
||||
1. Injects a :HEARTBEAT signal into the metabolic pipeline
|
||||
2. Checks if memory should be auto-saved (based on interval ratio)
|
||||
|
||||
Configuration via environment:
|
||||
- HEARTBEAT_INTERVAL: Seconds between heartbeats (default: 60)
|
||||
- MEMORY_AUTO_SAVE_INTERVAL: Seconds between auto-saves (default: 300)"
|
||||
|
||||
"Starts the background heartbeat thread."
|
||||
(let ((interval (or (ignore-errors (parse-integer (uiop:getenv "HEARTBEAT_INTERVAL"))) 60))
|
||||
(auto-save (or (ignore-errors (parse-integer (uiop:getenv "MEMORY_AUTO_SAVE_INTERVAL"))) *auto-save-interval*)))
|
||||
(setf *auto-save-interval* auto-save)
|
||||
@@ -112,82 +59,78 @@
|
||||
(bt:make-thread
|
||||
(lambda ()
|
||||
(loop
|
||||
;; Wait for interval
|
||||
(sleep interval)
|
||||
|
||||
;; Update counter and check if it's time to save
|
||||
(incf *heartbeat-save-counter*)
|
||||
(when (>= *heartbeat-save-counter* (/ *auto-save-interval* interval))
|
||||
(setf *heartbeat-save-counter* 0)
|
||||
(save-memory-to-disk))
|
||||
|
||||
;; Inject heartbeat signal - this runs through the full pipeline
|
||||
;; allowing the agent to do latent reflection even with no input
|
||||
(inject-stimulus
|
||||
(list :type :EVENT
|
||||
:payload (list :sensor :heartbeat
|
||||
:unix-time (get-universal-time)))))
|
||||
(list :type :EVENT :payload (list :sensor :heartbeat :unix-time (get-universal-time))))))
|
||||
:name "opencortex-heartbeat"))))
|
||||
|
||||
:name "opencortex-heartbeat")))))
|
||||
(defvar *shutdown-save-enabled* t)
|
||||
|
||||
(defvar *shutdown-save-enabled* t
|
||||
"When T, save memory to disk on graceful shutdown.
|
||||
Disable for testing or when memory persistence is handled externally.")
|
||||
(defvar *system-health* :unknown
|
||||
"Current system health status: :healthy, :degraded, :unhealthy, or :unknown.")
|
||||
|
||||
(defvar *health-check-ran* nil
|
||||
"Flag indicating if initial health check has completed.")
|
||||
|
||||
(defun run-startup-health-check ()
|
||||
"Runs the doctor diagnostics on startup. Returns health status."
|
||||
(format t "~%")
|
||||
(format t "==================================================~%")
|
||||
(format t " DOCTOR: Running Startup Health Check~%")
|
||||
(format t "==================================================~%")
|
||||
(handler-case
|
||||
(progn
|
||||
(when (fboundp 'doctor-run-all)
|
||||
(let ((result (doctor-run-all :auto-install nil)))
|
||||
(setf *health-check-ran* t)
|
||||
(if result
|
||||
(progn
|
||||
(setf *system-health* :healthy)
|
||||
(format t "DAEMON: Health check passed. Starting services.~%"))
|
||||
(progn
|
||||
(setf *system-health* :degraded)
|
||||
(format t "DAEMON: Health check found issues.~%")
|
||||
(format t " Run 'opencortex doctor --fix' to repair.~%")))))
|
||||
(setf *health-check-ran* t))
|
||||
(error (c)
|
||||
(format t "DOCTOR ERROR: ~a~%" c)
|
||||
(setf *system-health* :unhealthy)
|
||||
(setf *health-check-ran* t)))
|
||||
(format t "==================================================~%~%"))
|
||||
|
||||
(defun main ()
|
||||
"Entry point for OpenCortex. Initializes the system and enters idle loop.
|
||||
|
||||
Startup sequence:
|
||||
1. Load environment from ~/.local/share/opencortex/.env
|
||||
2. Restore memory from disk (if snapshot exists)
|
||||
3. Initialize actuators (shell, cli, system)
|
||||
4. Load all skills from SKILLS_DIR
|
||||
5. Start heartbeat thread
|
||||
6. Register SIGINT handler for graceful shutdown
|
||||
7. Enter idle loop (sleeps in DAEMON_SLEEP_INTERVAL chunks)
|
||||
|
||||
The idle loop checks for interrupts and saves memory before exit."
|
||||
|
||||
;; Step 1: Load environment variables from standard location
|
||||
"Entry point for OpenCortex. Initializes the system and enters idle loop."
|
||||
(let* ((home (uiop:getenv "HOME"))
|
||||
(env-file (uiop:merge-pathnames*
|
||||
".local/share/opencortex/.env"
|
||||
(uiop:ensure-directory-pathname home))))
|
||||
(env-file (uiop:merge-pathnames* ".config/opencortex/.env" (uiop:ensure-directory-pathname home))))
|
||||
(when (uiop:file-exists-p env-file)
|
||||
(cl-dotenv:load-env env-file)))
|
||||
|
||||
;; Step 2: Crash recovery - load memory from previous snapshot
|
||||
(load-memory-from-disk)
|
||||
|
||||
;; Step 3-4: Initialize actuators and load skills
|
||||
(initialize-actuators)
|
||||
(initialize-all-skills)
|
||||
|
||||
;; Step 5: Start the heartbeat
|
||||
|
||||
;; Run proactive doctor before starting services
|
||||
(run-startup-health-check)
|
||||
|
||||
(start-heartbeat)
|
||||
(start-daemon)
|
||||
|
||||
;; Step 6: Register graceful shutdown handler
|
||||
;; SBCL-specific: catches Ctrl+C (SIGINT) and saves before exit
|
||||
#+sbcl
|
||||
(sb-sys:enable-interrupt sb-unix:sigint
|
||||
(lambda (sig code scp)
|
||||
(declare (ignore sig code scp))
|
||||
(harness-log "SHUTDOWN: SIGINT received. Saving memory...")
|
||||
(when *shutdown-save-enabled*
|
||||
(save-memory-to-disk))
|
||||
(uiop:quit 0)))
|
||||
(lambda (sig code scp)
|
||||
(declare (ignore sig code scp))
|
||||
(harness-log "SHUTDOWN: SIGINT received. Saving memory...")
|
||||
(when *shutdown-save-enabled* (save-memory-to-disk))
|
||||
(uiop:quit 0)))
|
||||
|
||||
;; Step 7: Idle loop - sleep in chunks, checking for interrupts
|
||||
(let ((sleep-interval (or (ignore-errors
|
||||
(parse-integer (uiop:getenv "DAEMON_SLEEP_INTERVAL")))
|
||||
3600)))
|
||||
(let ((sleep-interval (or (ignore-errors (parse-integer (uiop:getenv "DAEMON_SLEEP_INTERVAL"))) 3600)))
|
||||
(loop
|
||||
;; Check for interrupt before each sleep cycle
|
||||
(when (bt:with-lock-held (*interrupt-lock*) *interrupt-flag*)
|
||||
(harness-log "SHUTDOWN: Interrupt flag set. Saving memory...")
|
||||
(when *shutdown-save-enabled*
|
||||
(save-memory-to-disk))
|
||||
(when *shutdown-save-enabled* (save-memory-to-disk))
|
||||
(return))
|
||||
|
||||
;; Sleep in configured intervals (default: 1 hour)
|
||||
(sleep sleep-interval))))
|
||||
|
||||
@@ -139,7 +139,7 @@ The Metabolic Loop is the fundamental rhythm of OpenCortex: the continuous proce
|
||||
(defun main ()
|
||||
"Entry point for OpenCortex. Initializes the system and enters idle loop."
|
||||
(let* ((home (uiop:getenv "HOME"))
|
||||
(env-file (uiop:merge-pathnames* ".local/share/opencortex/.env" (uiop:ensure-directory-pathname home))))
|
||||
(env-file (uiop:merge-pathnames* ".config/opencortex/.env" (uiop:ensure-directory-pathname home))))
|
||||
(when (uiop:file-exists-p env-file)
|
||||
(cl-dotenv:load-env env-file)))
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
#+AUTHOR: Agent
|
||||
#+FILETAGS: :harness:manifest:
|
||||
#+STARTUP: content
|
||||
#+PROPERTY: header-args:lisp :tangle opencortex.asd
|
||||
#+PROPERTY: header-args:lisp :tangle ../opencortex.asd
|
||||
|
||||
* Overview
|
||||
The *System Manifest* defines the structural components of the OpenCortex.
|
||||
@@ -19,79 +19,46 @@ The *System Manifest* defines the structural components of the OpenCortex.
|
||||
:description "The Probabilistic-Deterministic Lisp Machine"
|
||||
:depends-on (:usocket :bordeaux-threads :dexador :uiop :cl-dotenv :cl-ppcre :hunchentoot :ironclad :str :cl-json :uuid)
|
||||
:serial t
|
||||
:components ((:file "harness/package
|
||||
(:file "harness/skills
|
||||
(:file "harness/communication
|
||||
(:file "harness/communication-validator
|
||||
(:file "harness/memory
|
||||
(:file "harness/context
|
||||
(:file "harness/perceive
|
||||
(:file "harness/reason
|
||||
(:file "harness/act
|
||||
(:file "harness/loop))
|
||||
:components ((:file "harness/package")
|
||||
(:file "harness/skills")
|
||||
(:file "harness/communication")
|
||||
(:file "harness/communication-validator")
|
||||
(:file "harness/memory")
|
||||
(:file "harness/context")
|
||||
(:file "harness/perceive")
|
||||
(:file "harness/reason")
|
||||
(:file "harness/act")
|
||||
(:file "harness/loop")))
|
||||
#+end_src
|
||||
|
||||
** Test System
|
||||
#+begin_src lisp
|
||||
(defsystem :opencortex/tests
|
||||
:depends-on (:opencortex :fiveam)
|
||||
:components ((:file "tests/pipeline-act-tests
|
||||
(:file "tests/boot-sequence-tests
|
||||
(:file "tests/immune-system-tests
|
||||
(:file "tests/memory-tests
|
||||
(:file "tests/pipeline-perceive-tests
|
||||
(:file "tests/pipeline-reason-tests
|
||||
(:file "tests/peripheral-vision-tests
|
||||
(:file "tests/emacs-edit-tests
|
||||
(:file "tests/engineering-standards-tests
|
||||
(:file "tests/lisp-utils-tests
|
||||
(:file "tests/literate-programming-tests
|
||||
(:file "tests/self-edit-tests
|
||||
(:file "tests/tool-permissions-tests
|
||||
(:file "tests/diagnostics-tests
|
||||
(:file "tests/config-manager-tests
|
||||
(:file "tests/gateway-manager-tests
|
||||
(:file "tests/tui-tests
|
||||
(:file "tests/llm-gateway-tests))
|
||||
:components ((:file "tests/pipeline-act-tests")
|
||||
(:file "tests/boot-sequence-tests")
|
||||
(:file "tests/immune-system-tests")
|
||||
(:file "tests/memory-tests")
|
||||
(:file "tests/pipeline-perceive-tests")
|
||||
(:file "tests/pipeline-reason-tests")
|
||||
(:file "tests/peripheral-vision-tests")
|
||||
(:file "tests/utils-org-tests")
|
||||
(:file "tests/engineering-standards-tests")
|
||||
(:file "tests/utils-lisp-tests")
|
||||
(:file "tests/literate-programming-tests")
|
||||
(:file "tests/self-edit-tests")
|
||||
(:file "tests/tool-permissions-tests")
|
||||
(:file "tests/diagnostics-tests")
|
||||
(:file "tests/config-manager-tests")
|
||||
(:file "tests/gateway-manager-tests")
|
||||
(:file "tests/tui-tests")
|
||||
(:file "tests/llm-gateway-tests")))
|
||||
#+end_src
|
||||
|
||||
** TUI System
|
||||
#+begin_src lisp
|
||||
(defsystem :opencortex/tui
|
||||
:depends-on (:opencortex :croatoan :usocket :bordeaux-threads)
|
||||
:components ((:file "harness/tui-client))
|
||||
:components ((:file "harness/tui-client")))
|
||||
#+end_src
|
||||
|
||||
** Test Orchestrator
|
||||
#+begin_src lisp :tangle opencortex.asd
|
||||
(load (merge-pathnames "quicklisp/setup.lisp" (user-homedir-pathname)))
|
||||
|
||||
(let ((oc-dir (or (uiop:getenv "OC_DATA_DIR
|
||||
(namestring (truename "./))))
|
||||
(push (uiop:ensure-directory-pathname oc-dir) asdf:*central-registry*))
|
||||
|
||||
(ql:quickload '(:fiveam :opencortex :opencortex/tui :opencortex/tests) :silent t)
|
||||
|
||||
(format t "~%=== Initializing Skills BEFORE loading tests ===~%
|
||||
(opencortex:initialize-all-skills)
|
||||
|
||||
(format t "~%=== Running ALL Test Suites ===~%
|
||||
|
||||
(dolist (suite-spec '(("OPENCORTEX-BOOT-TESTS" "BOOT-SUITE
|
||||
("OPENCORTEX-COMMUNICATION-TESTS" "COMMUNICATION-PROTOCOL-SUITE
|
||||
("OPENCORTEX-PIPELINE-ACT-TESTS" "PIPELINE-ACT-SUITE
|
||||
("OPENCORTEX-MEMORY-TESTS" "MEMORY-SUITE
|
||||
("OPENCORTEX-ENGINEERING-STANDARDS-TESTS" "ENGINEERING-STANDARDS-SUITE
|
||||
("OPENCORTEX-DIAGNOSTICS-TESTS" "DIAGNOSTICS-SUITE
|
||||
("OPENCORTEX-GATEWAY-MANAGER-TESTS" "GATEWAY-SUITE
|
||||
("OPENCORTEX-TUI-TESTS" "TUI-SUITE
|
||||
("OPENCORTEX-LLM-GATEWAY-TESTS" "LLM-GATEWAY-SUITE))
|
||||
(let ((pkg (find-package (first suite-spec))))
|
||||
(when pkg
|
||||
(let ((suite-sym (find-symbol (second suite-spec) pkg)))
|
||||
(when suite-sym
|
||||
(format t "~&--- Suite: ~A ---~%" (first suite-spec))
|
||||
(fiveam:run! suite-sym))))))
|
||||
|
||||
(format t "~%=== ALL TESTS COMPLETE ===~%
|
||||
#+end_src
|
||||
|
||||
@@ -1,19 +1,28 @@
|
||||
(in-package :opencortex)
|
||||
|
||||
(defvar *memory* (make-hash-table :test 'equal))
|
||||
|
||||
(defvar *history-store* (make-hash-table :test 'equal)
|
||||
"Immutable Merkle-Tree versioning store mapping hashes to objects.")
|
||||
|
||||
(defstruct org-object
|
||||
id type attributes content vector parent-id children version last-sync hash)
|
||||
|
||||
;; Enable serialization via make-load-form (standard CL)
|
||||
(defmethod make-load-form ((obj org-object) &optional env)
|
||||
(make-load-form-saving-slots obj :environment env))
|
||||
|
||||
(defun copy-org-object (obj)
|
||||
(make-org-object :id (org-object-id obj)
|
||||
:type (org-object-type obj)
|
||||
:attributes (copy-list (org-object-attributes obj))
|
||||
:content (org-object-content obj)
|
||||
:vector (org-object-vector obj)
|
||||
:parent-id (org-object-parent-id obj)
|
||||
:children (copy-list (org-object-children obj))
|
||||
:version (org-object-version obj)
|
||||
:last-sync (org-object-last-sync obj)
|
||||
:hash (org-object-hash obj)))
|
||||
|
||||
(defun compute-merkle-hash (id type attributes content child-hashes)
|
||||
"Computes a SHA-256 Merkle hash for a node based on its core properties and children's hashes."
|
||||
(let* ((alist (loop for (k v) on attributes by #'cddr collect (cons k v)))
|
||||
(sorted-alist (sort alist #'string< :key (lambda (x) (format nil "~a" (car x)))))
|
||||
(attr-string (format nil "~s" sorted-alist))
|
||||
@@ -25,23 +34,19 @@
|
||||
(ironclad:byte-array-to-hex-string (ironclad:produce-digest digester))))
|
||||
|
||||
(defun ingest-ast (ast &optional parent-id)
|
||||
"Parses an Org AST into the recursive Lisp Memory with Merkle hashing."
|
||||
(let* ((type (getf ast :type))
|
||||
(props (getf ast :properties))
|
||||
(id (or (getf props :ID) (format nil "temp-~a" (get-universal-time))))
|
||||
(contents (getf ast :contents))
|
||||
(raw-content (when (eq type :HEADLINE)
|
||||
(format nil "~a~%~a" (getf props :TITLE) (or (cl:getf ast :raw-content) ""))))
|
||||
(should-embed (and raw-content (equal (getf props :EMBED) "t")))
|
||||
(child-ids nil)
|
||||
(child-hashes nil))
|
||||
(format nil "~a~%~a" (getf props :TITLE) (or (getf ast :raw-content) ""))))
|
||||
(child-ids nil) (child-hashes nil))
|
||||
(dolist (child contents)
|
||||
(when (listp child)
|
||||
(let ((child-id (ingest-ast child id)))
|
||||
(push child-id child-ids)
|
||||
(let ((child-id-val child-id))
|
||||
(let ((child-obj (lookup-object child-id-val)))
|
||||
(when child-obj (push (org-object-hash child-obj) child-hashes)))))))
|
||||
(let ((child-obj (gethash child-id *memory*)))
|
||||
(when child-obj (push (org-object-hash child-obj) child-hashes))))))
|
||||
(setf child-ids (nreverse child-ids))
|
||||
(setf child-hashes (nreverse child-hashes))
|
||||
(let* ((hash (compute-merkle-hash id type props raw-content child-hashes))
|
||||
@@ -49,194 +54,64 @@
|
||||
(obj (or existing-obj
|
||||
(make-org-object
|
||||
:id id :type type :attributes props :content raw-content
|
||||
:vector (when should-embed (get-embedding raw-content))
|
||||
:parent-id parent-id :children child-ids
|
||||
:version (get-universal-time) :last-sync (get-universal-time)
|
||||
:hash hash))))
|
||||
(unless existing-obj
|
||||
(setf (gethash hash *history-store*) obj))
|
||||
(unless existing-obj (setf (gethash hash *history-store*) obj))
|
||||
(setf (gethash id *memory*) obj)
|
||||
id)))
|
||||
|
||||
(defvar *object-store-snapshots* nil)
|
||||
|
||||
(defun copy-hash-table (hash-table)
|
||||
"Creates a shallow copy of a hash table."
|
||||
(let ((new-table (make-hash-table :test (hash-table-test hash-table)
|
||||
:size (hash-table-size hash-table))))
|
||||
(maphash (lambda (k v) (setf (gethash k new-table) v)) hash-table)
|
||||
new-table))
|
||||
|
||||
(defun snapshot-memory ()
|
||||
"Creates a lightweight, Copy-on-Write snapshot using Merkle-Tree pointers."
|
||||
(let ((snapshot (copy-hash-table *memory*)))
|
||||
(let ((snapshot (make-hash-table :test 'equal :size (hash-table-size *memory*))))
|
||||
(maphash (lambda (k v) (setf (gethash k snapshot) (copy-org-object v))) *memory*)
|
||||
(push (list :timestamp (get-universal-time) :data snapshot) *object-store-snapshots*)
|
||||
(when (> (length *object-store-snapshots*) 20)
|
||||
(setf *object-store-snapshots* (subseq *object-store-snapshots* 0 20)))
|
||||
(when (> (length *object-store-snapshots*) 20) (setf *object-store-snapshots* (subseq *object-store-snapshots* 0 20)))
|
||||
(harness-log "MEMORY - CoW Memory snapshot created.")))
|
||||
|
||||
(defun rollback-memory (&optional (index 0))
|
||||
"Restores the Memory to a previously captured snapshot using immutable history pointers."
|
||||
(let ((snapshot (nth index *object-store-snapshots*)))
|
||||
(if snapshot
|
||||
(progn (setf *memory* (copy-hash-table (getf snapshot :data)))
|
||||
(harness-log "MEMORY - Memory rolled back to snapshot ~a" index))
|
||||
(harness-log "MEMORY ERROR - Snapshot ~a not found." index))))
|
||||
|
||||
(defvar *memory-snapshot-path* nil
|
||||
"Path to the memory snapshot file. Set from MEMORY_SNAPSHOT_PATH env or default.")
|
||||
(defvar *memory-snapshot-path* nil)
|
||||
|
||||
(defun ensure-memory-snapshot-path ()
|
||||
"Initializes the snapshot path from environment or default location."
|
||||
(or *memory-snapshot-path*
|
||||
(let ((env-path (uiop:getenv "MEMORY_SNAPSHOT_PATH")))
|
||||
(setf *memory-snapshot-path*
|
||||
(or env-path
|
||||
(uiop:merge-pathnames* "memory.snap" (user-homedir-pathname)))))))
|
||||
(or env-path (namestring (uiop:merge-pathnames* "memory.snap" (user-homedir-pathname))))))))
|
||||
|
||||
(defun save-memory-to-disk ()
|
||||
"Serializes *memory* and *history-store* to disk for crash recovery.
|
||||
Converts hash tables to alists for proper serialization."
|
||||
(let ((path (ensure-memory-snapshot-path)))
|
||||
(with-open-file (stream path :direction :output :if-exists :supersede :if-does-not-exist :create)
|
||||
(format stream ";; OpenCortex Memory Snapshot~%")
|
||||
(format stream ";; Created: ~a~%~%" (format nil "~a" (get-universal-time)))
|
||||
(let ((memory-alist nil)
|
||||
(history-alist nil))
|
||||
(let ((memory-alist nil) (history-alist nil))
|
||||
(maphash (lambda (k v) (push (cons k v) memory-alist)) *memory*)
|
||||
(maphash (lambda (k v) (push (cons k v) history-alist)) *history-store*)
|
||||
(prin1 (list :memory memory-alist :history-store history-alist) stream)))
|
||||
(harness-log "MEMORY - Saved to ~a" path)
|
||||
path))
|
||||
(harness-log "MEMORY - Saved to ~a" path)))
|
||||
|
||||
(defun load-memory-from-disk ()
|
||||
"Loads *memory* and *history-store* from disk if the snapshot exists.
|
||||
Reconstitutes alists into hash tables."
|
||||
(let ((path (ensure-memory-snapshot-path)))
|
||||
(when (uiop:file-exists-p path)
|
||||
(handler-case
|
||||
(with-open-file (stream path :direction :input)
|
||||
(let ((data (read stream nil)))
|
||||
(when data
|
||||
(let ((memory-alist (getf data :memory))
|
||||
(history-alist (getf data :history-store)))
|
||||
(let ((memory-alist (getf data :memory)) (history-alist (getf data :history-store)))
|
||||
(setf *memory* (make-hash-table :test 'equal :size (length memory-alist)))
|
||||
(dolist (kv memory-alist)
|
||||
(setf (gethash (car kv) *memory*) (cdr kv)))
|
||||
(dolist (kv memory-alist) (setf (gethash (car kv) *memory*) (cdr kv)))
|
||||
(setf *history-store* (make-hash-table :test 'equal :size (length history-alist)))
|
||||
(dolist (kv history-alist)
|
||||
(setf (gethash (car kv) *history-store*) (cdr kv)))
|
||||
(dolist (kv history-alist) (setf (gethash (car kv) *history-store*) (cdr kv)))
|
||||
(harness-log "MEMORY - Loaded from ~a (~a objects)" path (hash-table-size *memory*))))))
|
||||
(error (c)
|
||||
(harness-log "MEMORY WARNING - Failed to load snapshot: ~a" c))))
|
||||
t))
|
||||
|
||||
(defvar *embedding-cache* (make-hash-table :test 'equal)
|
||||
"Cache for embeddings to avoid redundant API calls.")
|
||||
|
||||
(defun get-embedding (text)
|
||||
"Generates a vector embedding for the given text via Ollama. Returns nil on failure."
|
||||
(when (or (null text) (string= text ""))
|
||||
(return-from get-embedding nil))
|
||||
(let ((cached (gethash text *embedding-cache*)))
|
||||
(when cached (return-from get-embedding cached)))
|
||||
(let ((result (funcall (get-cognitive-tool-body :get-ollama-embedding) (list :text text))))
|
||||
(when (eq (getf result :status) :success)
|
||||
(let ((vec (getf result :vector)))
|
||||
(setf (gethash text *embedding-cache*) vec)
|
||||
vec))))
|
||||
|
||||
(defun cosine-similarity (vec-a vec-b)
|
||||
"Computes cosine similarity between two vectors. Both should be sequences of numbers."
|
||||
(when (or (null vec-a) (null vec-b) (zerop (length vec-a)) (zerop (length vec-b)))
|
||||
(return-from cosine-similarity 0.0))
|
||||
(let ((dot-product (loop for a across vec-a
|
||||
for b across vec-b
|
||||
sum (* a b)))
|
||||
(norm-a (sqrt (loop for a across vec-a sum (* a a))))
|
||||
(norm-b (sqrt (loop for b across vec-b sum (* b b)))))
|
||||
(if (or (zerop norm-a) (zerop norm-b))
|
||||
0.0
|
||||
(/ dot-product (* norm-a norm-b)))))
|
||||
|
||||
(defun semantic-search (query &key (limit 10) (min-similarity 0.5))
|
||||
"Searches memory for objects semantically similar to the query.
|
||||
Returns up to LIMIT objects with similarity >= MIN-SIMILARITY, sorted by similarity descending."
|
||||
(let* ((query-vec (get-embedding query))
|
||||
(results nil))
|
||||
(unless query-vec
|
||||
(harness-log "EMBEDDING: Failed to generate embedding for query: ~a" query)
|
||||
(return-from semantic-search nil))
|
||||
(maphash (lambda (id obj)
|
||||
(let ((obj-vec (org-object-vector obj)))
|
||||
(when obj-vec
|
||||
(let ((sim (cosine-similarity query-vec obj-vec)))
|
||||
(when (>= sim min-similarity)
|
||||
(push (list :id id :object obj :similarity sim) results))))))
|
||||
*memory*)
|
||||
(setf results (sort results #'> :key (lambda (r) (getf r :similarity))))
|
||||
(subseq results 0 (min limit (length results)))))
|
||||
|
||||
(def-cognitive-tool :semantic-search
|
||||
"Searches memory for objects semantically similar to a query."
|
||||
((:query :type :string :description "The search query.")
|
||||
(:limit :type :integer :description "Maximum results to return." :default 10)
|
||||
(:min-similarity :type :number :description "Minimum similarity threshold (0-1)." :default 0.5))
|
||||
:body (lambda (args)
|
||||
(semantic-search (getf args :query)
|
||||
:limit (or (getf args :limit) 10)
|
||||
:min-similarity (or (getf args :min-similarity) 0.5))))
|
||||
|
||||
(def-cognitive-tool :generate-embeddings
|
||||
"Generates vector embeddings for given text via the configured embedding backend (Ollama)."
|
||||
((:texts :type :list :description "List of text strings to embed."))
|
||||
:body (lambda (args)
|
||||
(let ((texts (getf args :texts)))
|
||||
(if (not (and texts (listp texts)))
|
||||
(list :status :error :message ":texts must be a list of strings.")
|
||||
(let ((results nil) (errors nil))
|
||||
(dolist (text texts)
|
||||
(let ((vec (get-embedding text)))
|
||||
(if vec
|
||||
(push (list :text text :vector vec) results)
|
||||
(push text errors))))
|
||||
(list :status (if errors :partial :success)
|
||||
:embeddings (nreverse results)
|
||||
:failed (when errors (nreverse errors))
|
||||
:count (length results)))))))
|
||||
|
||||
(defun org-id-new ()
|
||||
"Generates a new UUID string for Org-mode identification."
|
||||
(string-downcase (format nil "~a" (uuid:make-v4-uuid))))
|
||||
|
||||
(defun lookup-object (id)
|
||||
"Retrieves an object from the store by its unique ID."
|
||||
(gethash id *memory*))
|
||||
|
||||
(defun list-objects-by-type (type)
|
||||
"Returns a list of all objects matching a specific Org element type."
|
||||
(let ((results nil))
|
||||
(maphash (lambda (id obj) (declare (ignore id)) (when (eq (org-object-type obj) type) (push obj results))) *memory*)
|
||||
results))
|
||||
|
||||
(defun list-objects-with-attribute (attr-name value)
|
||||
"Returns a list of all objects where ATTR-NAME matches VALUE."
|
||||
(let ((results nil))
|
||||
(maphash (lambda (id obj)
|
||||
(declare (ignore id))
|
||||
(let ((attrs (org-object-attributes obj)))
|
||||
(when (equal (getf attrs attr-name) value)
|
||||
(push obj results))))
|
||||
*memory*)
|
||||
results))
|
||||
|
||||
(defun find-headline-missing-id (ast)
|
||||
"Traverses an AST to find headlines that lack an :ID: property."
|
||||
(when (listp ast)
|
||||
(if (and (eq (getf ast :type) :HEADLINE) (not (getf (getf ast :properties) :ID)))
|
||||
ast
|
||||
(cl:some #'find-headline-missing-id (getf ast :contents)))))
|
||||
|
||||
(defun file-name-nondirectory (path)
|
||||
"Extracts the filename from a full path string."
|
||||
(let ((pos (position #\/ path :from-end t))) (if pos (subseq path (1+ pos)) path)))
|
||||
(error (c) (harness-log "MEMORY WARNING - Failed to load snapshot: ~a" c)))))
|
||||
t)
|
||||
|
||||
@@ -35,10 +35,6 @@
|
||||
#:skill-gateway-link
|
||||
#:gateway-manager-main
|
||||
|
||||
;; --- Diagnostic Doctor ---
|
||||
#:doctor-run-all
|
||||
#:doctor-main
|
||||
|
||||
;; --- Memory (CLOSOS) ---
|
||||
#:ingest-ast
|
||||
#:lookup-object
|
||||
@@ -109,44 +105,45 @@
|
||||
|
||||
;; --- Engineering Standards Skill ---
|
||||
#:verify-git-clean-p
|
||||
#:engineering-standards-verify-lisp
|
||||
#:engineering-standards-format-lisp
|
||||
|
||||
;; --- Literate Programming Skill ---
|
||||
#:literate-check-block-balance
|
||||
#:check-tangle-sync
|
||||
#:*tangle-targets*
|
||||
|
||||
;; --- Emacs Edit Skill ---
|
||||
#:emacs-edit-read-file
|
||||
#:emacs-edit-write-file
|
||||
#:emacs-edit-add-headline
|
||||
#:emacs-edit-set-property
|
||||
#:emacs-edit-set-todo
|
||||
#:emacs-edit-find-headline-by-id
|
||||
#:emacs-edit-find-headline-by-title
|
||||
#:emacs-edit-generate-id
|
||||
#:emacs-edit-id-format
|
||||
#:emacs-edit-ast-to-org
|
||||
#:emacs-edit-modify
|
||||
;; --- Utils Org Skill ---
|
||||
#:utils-org-read-file
|
||||
#:utils-org-write-file
|
||||
#:utils-org-add-headline
|
||||
#:utils-org-set-property
|
||||
#:utils-org-set-todo
|
||||
#:utils-org-find-headline-by-id
|
||||
#:utils-org-find-headline-by-title
|
||||
#:utils-org-generate-id
|
||||
#:utils-org-id-format
|
||||
#:utils-org-ast-to-org
|
||||
#:utils-org-modify
|
||||
|
||||
;; --- Lisp Utils Skill ---
|
||||
#:lisp-utils-validate
|
||||
#:lisp-utils-check-structural
|
||||
#:lisp-utils-check-syntactic
|
||||
#:lisp-utils-check-semantic
|
||||
#:lisp-utils-register
|
||||
;; --- Utils Lisp Skill ---
|
||||
#:utils-lisp-validate
|
||||
#:utils-lisp-check-structural
|
||||
#:utils-lisp-check-syntactic
|
||||
#:utils-lisp-check-semantic
|
||||
#:utils-lisp-eval
|
||||
#:utils-lisp-format
|
||||
#:utils-lisp-list-definitions
|
||||
#:utils-lisp-structural-extract
|
||||
#:utils-lisp-structural-wrap
|
||||
#:utils-lisp-structural-inject
|
||||
#:utils-lisp-structural-slurp
|
||||
#:utils-lisp-register
|
||||
|
||||
;; --- Config Manager & Diagnostics Skill ---
|
||||
#:register-provider
|
||||
#:save-providers
|
||||
#:configure-provider
|
||||
#:run-setup-wizard
|
||||
#:get-oc-config-dir
|
||||
#:prompt-for
|
||||
#:save-secret
|
||||
#:doctor-check-dependencies
|
||||
#:doctor-check-xdg
|
||||
#:doctor-check-llm
|
||||
#:doctor-run-all
|
||||
|
||||
;; --- Tool Permissions Skill ---
|
||||
#:get-tool-permission
|
||||
@@ -227,7 +224,22 @@
|
||||
:description ,description
|
||||
:parameters ',parameters
|
||||
:guard ,guard
|
||||
:body ,body)))
|
||||
:body ,body)))
|
||||
|
||||
(defun generate-tool-belt-prompt ()
|
||||
"Generates a prompt string describing all available cognitive tools."
|
||||
(let ((descriptions nil))
|
||||
(maphash (lambda (k tool)
|
||||
(declare (ignore k))
|
||||
(push (format nil "- ~a: ~a~% Parameters: ~a~%"
|
||||
(cognitive-tool-name tool)
|
||||
(cognitive-tool-description tool)
|
||||
(cognitive-tool-parameters tool))
|
||||
descriptions))
|
||||
*cognitive-tools*)
|
||||
(if descriptions
|
||||
(format nil "Available tools:~%~a" (apply #'concatenate 'string (sort descriptions #'string<)))
|
||||
"No tools registered.")))
|
||||
|
||||
(defun harness-log (msg &rest args)
|
||||
"Centralized logging for the harness."
|
||||
@@ -238,3 +250,18 @@
|
||||
(setq *system-logs* (subseq *system-logs* 0 *max-log-history*))))
|
||||
(format t "~a~%" formatted-msg)
|
||||
(finish-output)))
|
||||
|
||||
;; --- Debugger Hook ---
|
||||
(setf *debugger-hook* (lambda (condition hook)
|
||||
"Friendly error handler - shows diagnostic message instead of raw debugger."
|
||||
(format t "~%")
|
||||
(format t "┌─────────────────────────────────────────────┐~%")
|
||||
(format t "│ ERROR: ~A~%" (type-of condition))
|
||||
(format t "│~%")
|
||||
(format t "│ Run: opencortex doctor~%")
|
||||
(format t "│ For system diagnostics~%")
|
||||
(format t "└─────────────────────────────────────────────┘~%")
|
||||
(format t "~%")
|
||||
(format t "Details: ~A~%" condition)
|
||||
(finish-output)
|
||||
(uiop:quit 1)))
|
||||
|
||||
@@ -118,31 +118,40 @@ The ~package.lisp~ file defines the public API of the ~opencortex~ harness.
|
||||
|
||||
;; --- Engineering Standards Skill ---
|
||||
#:verify-git-clean-p
|
||||
#:engineering-standards-verify-lisp
|
||||
#:engineering-standards-format-lisp
|
||||
|
||||
;; --- Literate Programming Skill ---
|
||||
#:literate-check-block-balance
|
||||
#:check-tangle-sync
|
||||
#:*tangle-targets*
|
||||
|
||||
;; --- Emacs Edit Skill ---
|
||||
#:emacs-edit-read-file
|
||||
#:emacs-edit-write-file
|
||||
#:emacs-edit-add-headline
|
||||
#:emacs-edit-set-property
|
||||
#:emacs-edit-set-todo
|
||||
#:emacs-edit-find-headline-by-id
|
||||
#:emacs-edit-find-headline-by-title
|
||||
#:emacs-edit-generate-id
|
||||
#:emacs-edit-id-format
|
||||
#:emacs-edit-ast-to-org
|
||||
#:emacs-edit-modify
|
||||
;; --- Utils Org Skill ---
|
||||
#:utils-org-read-file
|
||||
#:utils-org-write-file
|
||||
#:utils-org-add-headline
|
||||
#:utils-org-set-property
|
||||
#:utils-org-set-todo
|
||||
#:utils-org-find-headline-by-id
|
||||
#:utils-org-find-headline-by-title
|
||||
#:utils-org-generate-id
|
||||
#:utils-org-id-format
|
||||
#:utils-org-ast-to-org
|
||||
#:utils-org-modify
|
||||
|
||||
;; --- Lisp Utils Skill ---
|
||||
#:lisp-utils-validate
|
||||
#:lisp-utils-check-structural
|
||||
#:lisp-utils-check-syntactic
|
||||
#:lisp-utils-check-semantic
|
||||
#:lisp-utils-register
|
||||
;; --- Utils Lisp Skill ---
|
||||
#:utils-lisp-validate
|
||||
#:utils-lisp-check-structural
|
||||
#:utils-lisp-check-syntactic
|
||||
#:utils-lisp-check-semantic
|
||||
#:utils-lisp-eval
|
||||
#:utils-lisp-format
|
||||
#:utils-lisp-list-definitions
|
||||
#:utils-lisp-structural-extract
|
||||
#:utils-lisp-structural-wrap
|
||||
#:utils-lisp-structural-inject
|
||||
#:utils-lisp-structural-slurp
|
||||
#:utils-lisp-register
|
||||
|
||||
;; --- Config Manager & Diagnostics Skill ---
|
||||
#:get-oc-config-dir
|
||||
@@ -231,7 +240,22 @@ The ~package.lisp~ file defines the public API of the ~opencortex~ harness.
|
||||
:description ,description
|
||||
:parameters ',parameters
|
||||
:guard ,guard
|
||||
:body ,body)))
|
||||
:body ,body)))
|
||||
|
||||
(defun generate-tool-belt-prompt ()
|
||||
"Generates a prompt string describing all available cognitive tools."
|
||||
(let ((descriptions nil))
|
||||
(maphash (lambda (k tool)
|
||||
(declare (ignore k))
|
||||
(push (format nil "- ~a: ~a~% Parameters: ~a~%"
|
||||
(cognitive-tool-name tool)
|
||||
(cognitive-tool-description tool)
|
||||
(cognitive-tool-parameters tool))
|
||||
descriptions))
|
||||
*cognitive-tools*)
|
||||
(if descriptions
|
||||
(format nil "Available tools:~%~a" (apply #'concatenate 'string (sort descriptions #'string<)))
|
||||
"No tools registered.")))
|
||||
|
||||
(defun harness-log (msg &rest args)
|
||||
"Centralized logging for the harness."
|
||||
@@ -242,4 +266,19 @@ The ~package.lisp~ file defines the public API of the ~opencortex~ harness.
|
||||
(setq *system-logs* (subseq *system-logs* 0 *max-log-history*))))
|
||||
(format t "~a~%" formatted-msg)
|
||||
(finish-output)))
|
||||
|
||||
;; --- Debugger Hook ---
|
||||
(setf *debugger-hook* (lambda (condition hook)
|
||||
"Friendly error handler - shows diagnostic message instead of raw debugger."
|
||||
(format t "~%")
|
||||
(format t "┌─────────────────────────────────────────────┐~%")
|
||||
(format t "│ ERROR: ~A~%" (type-of condition))
|
||||
(format t "│~%")
|
||||
(format t "│ Run: opencortex doctor~%")
|
||||
(format t "│ For system diagnostics~%")
|
||||
(format t "└─────────────────────────────────────────────┘~%")
|
||||
(format t "~%")
|
||||
(format t "Details: ~A~%" condition)
|
||||
(finish-output)
|
||||
(uiop:quit 1)))
|
||||
#+end_src
|
||||
|
||||
@@ -1,66 +1,35 @@
|
||||
(in-package :opencortex)
|
||||
|
||||
(defvar *async-sensors* '(:chat-message :delegation :user-command)
|
||||
"Sensors that are processed in dedicated threads.
|
||||
|
||||
These sensors can block (waiting for API responses, user input, etc.)
|
||||
so they run in separate threads to avoid blocking the main pipeline.
|
||||
|
||||
Other sensors (:heartbeat, :interrupt, :buffer-update) are processed
|
||||
synchronously to maintain temporal ordering.")
|
||||
"Sensors that are processed in dedicated threads.")
|
||||
|
||||
(defvar *foveal-focus-id* nil
|
||||
"The Org ID of the node the user is currently interacting with.
|
||||
|
||||
This enables the reasoning engine to provide contextually relevant
|
||||
responses. When editing a specific note, the agent knows which
|
||||
note you're referring to without needing explicit ID references.
|
||||
|
||||
Updated on :point-update events from Emacs.")
|
||||
"The Org ID of the node the user is currently interacting with.")
|
||||
|
||||
(defun inject-stimulus (raw-message &key stream (depth 0))
|
||||
"Inject a raw message into the signal processing pipeline.
|
||||
|
||||
RAW-MESSAGE is a property list that will be normalized into a Signal.
|
||||
STREAM is an optional output stream for responses (used by TUI/CLI).
|
||||
DEPTH tracks recursion depth for feedback loops.
|
||||
|
||||
This function determines whether to process synchronously or
|
||||
asynchronously based on the sensor type, then calls process-signal
|
||||
to run through the Perceive -> Reason -> Act pipeline.
|
||||
|
||||
Error handling: Uses restarts to prevent individual signals from
|
||||
crashing the entire system. Failed signals are logged and dropped."
|
||||
|
||||
"Inject a raw message into the signal processing pipeline."
|
||||
(let* ((payload (getf raw-message :payload))
|
||||
(sensor (getf payload :sensor))
|
||||
(meta (getf raw-message :meta))
|
||||
(async-p (or (getf payload :async-p)
|
||||
(member sensor *async-sensors*))))
|
||||
|
||||
;; Ensure metadata exists
|
||||
(unless meta
|
||||
(setf meta (list :SOURCE :SYSTEM :SESSION-ID "internal")))
|
||||
|
||||
;; Attach reply stream if provided
|
||||
(when stream
|
||||
(setf (getf meta :reply-stream) stream))
|
||||
|
||||
(setf (getf raw-message :meta) meta)
|
||||
(setf (getf raw-message :depth) depth)
|
||||
|
||||
(if async-p
|
||||
;; Async: process in dedicated thread
|
||||
(bt:make-thread
|
||||
(lambda ()
|
||||
(restart-case
|
||||
(handler-bind ((error (lambda (c)
|
||||
(harness-log "ASYNC ERROR: ~a" c)
|
||||
(invoke-restart 'skip-event))))
|
||||
(process-signal raw-message))
|
||||
(restart-case (process-signal raw-message)
|
||||
(skip-event () nil)))
|
||||
:name "opencortex-async-task")
|
||||
|
||||
;; Sync: process in main thread with recovery
|
||||
|
||||
(restart-case
|
||||
(handler-bind ((error (lambda (c)
|
||||
(harness-log "SYSTEM ERROR: ~a" c)
|
||||
@@ -70,61 +39,33 @@
|
||||
(harness-log "SYSTEM RECOVERY: Stimulus dropped."))))))
|
||||
|
||||
(defun perceive-gate (signal)
|
||||
"Stage 1 of the metabolic pipeline: Normalize sensory input.
|
||||
|
||||
This function:
|
||||
1. Logs the incoming signal for debugging
|
||||
2. Handles special sensor types (:buffer-update, :point-update, etc.)
|
||||
3. Updates the Memory graph with incoming data
|
||||
4. Tracks foveal focus (user's current node)
|
||||
5. Sets :status to :perceived
|
||||
|
||||
Modifies the signal in place and returns it for the next stage.
|
||||
|
||||
Memory snapshots are taken before AST updates to enable rollback
|
||||
if the update causes issues."
|
||||
|
||||
"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)))
|
||||
|
||||
;; Log the incoming signal for debugging
|
||||
(harness-log "GATE [Perceive]: ~a (~a) [Source: ~s]"
|
||||
type (or sensor "no-sensor") (getf meta :source))
|
||||
|
||||
;; Handle EVENT type sensors
|
||||
(cond ((eq type :EVENT)
|
||||
(case sensor
|
||||
|
||||
;; Org buffer was modified - update memory
|
||||
(:buffer-update
|
||||
(let ((ast (getf payload :ast)))
|
||||
(when ast
|
||||
(snapshot-memory) ; Enable rollback if update causes issues
|
||||
(snapshot-memory)
|
||||
(ingest-ast ast))))
|
||||
|
||||
;; Point moved to different org node - update focus
|
||||
(:point-update
|
||||
(let ((element (getf payload :element)))
|
||||
(when element
|
||||
(snapshot-memory)
|
||||
;; Track foveal focus for contextual reasoning
|
||||
(setf *foveal-focus-id*
|
||||
(ignore-errors (getf element :id)))
|
||||
(setf *foveal-focus-id* (getf element :id))
|
||||
(ingest-ast element))))
|
||||
|
||||
;; System interrupt - trigger shutdown
|
||||
(:interrupt
|
||||
(bt:with-lock-held (*interrupt-lock*)
|
||||
(setf *interrupt-flag* t)))))
|
||||
|
||||
;; Log responses from actuators
|
||||
(setf *interrupt-flag* t))))
|
||||
((eq type :RESPONSE)
|
||||
(harness-log "GATE [Perceive]: Act Result -> ~a"
|
||||
(getf payload :status))))
|
||||
(harness-log "GATE [Perceive]: Act Result -> ~a" (getf payload :status))))
|
||||
|
||||
;; Update signal status
|
||||
(setf (getf signal :status) :perceived)
|
||||
(setf (getf signal :foveal-focus) *foveal-focus-id*)
|
||||
signal))
|
||||
|
||||
@@ -1,63 +1,30 @@
|
||||
(in-package :opencortex)
|
||||
|
||||
(defvar *probabilistic-backends* (make-hash-table :test 'equal)
|
||||
"Registry mapping provider keywords (:openrouter, :ollama) to their calling functions.")
|
||||
(defvar *probabilistic-backends* (make-hash-table :test 'equal))
|
||||
|
||||
(defvar *provider-cascade* nil
|
||||
"Ordered list of provider keywords to try. First available provider wins.")
|
||||
(defvar *provider-cascade* nil)
|
||||
|
||||
(defvar *model-selector-fn* nil
|
||||
"Optional function that selects a specific model for each provider.
|
||||
Signature: (funcall fn provider context) => model-name-string")
|
||||
(defvar *model-selector-fn* nil)
|
||||
|
||||
(defvar *consensus-enabled-p* nil
|
||||
"When T, run multiple providers and compare results for critical decisions.")
|
||||
(defvar *consensus-enabled-p* nil)
|
||||
|
||||
(defun register-probabilistic-backend (name fn)
|
||||
"Register a neural provider backend.
|
||||
|
||||
NAME is a keyword like :openrouter or :ollama.
|
||||
FN is a function with signature: (funcall fn prompt system-prompt &key model)
|
||||
returning either:
|
||||
- (list :status :success :content \"response text\")
|
||||
- (list :status :error :message \"error description\")
|
||||
- a simple string on success
|
||||
|
||||
Example registration:
|
||||
(register-probabilistic-backend :openrouter #'openrouter-call)"
|
||||
|
||||
(setf (gethash name *probabilistic-backends*) fn))
|
||||
|
||||
(defun probabilistic-call (prompt &key
|
||||
(system-prompt "You are the Probabilistic engine.")
|
||||
(cascade nil)
|
||||
(context nil))
|
||||
"Dispatch a neural request through the provider cascade.
|
||||
|
||||
PROMPT - The user's query or task description.
|
||||
SYSTEM-PROMPT - Instructions for how the LLM should behave.
|
||||
CASCADE - Override the default provider cascade.
|
||||
CONTEXT - Current signal context (for model selection).
|
||||
|
||||
Returns the LLM response as a string, or a failure plist if all providers fail.
|
||||
|
||||
The cascade mechanism ensures reliability: if OpenRouter is rate-limited,
|
||||
it automatically falls back to OpenAI, then Anthropic, etc."
|
||||
|
||||
(let ((backends (or cascade *provider-cascade*)))
|
||||
(or (dolist (backend backends)
|
||||
(let ((backend-fn (gethash backend *probabilistic-backends*)))
|
||||
(when backend-fn
|
||||
(harness-log "PROBABILISTIC: Attempting backend ~a..." backend)
|
||||
|
||||
;; Optional model selection based on context
|
||||
(let* ((model (when *model-selector-fn*
|
||||
(funcall *model-selector-fn* backend context)))
|
||||
(result (if model
|
||||
(funcall backend-fn prompt system-prompt :model model)
|
||||
(funcall backend-fn prompt system-prompt))))
|
||||
|
||||
;; Normalize result format
|
||||
(cond ((and (listp result) (eq (getf result :status) :success))
|
||||
(return (getf result :content)))
|
||||
((stringp result)
|
||||
@@ -65,22 +32,10 @@
|
||||
(t
|
||||
(harness-log "PROBABILISTIC: Backend ~a failed: ~a"
|
||||
backend (getf result :message))))))))
|
||||
|
||||
;; All providers failed
|
||||
(list :type :LOG
|
||||
:payload (list :text "Neural Cascade Failure: All providers exhausted.")))))
|
||||
|
||||
(defun strip-markdown (text)
|
||||
"Strip markdown formatting from LLM output.
|
||||
|
||||
LLMs often wrap their responses in code fences (```lisp ...```).
|
||||
This function removes those markers to extract the raw plist.
|
||||
|
||||
Handles:
|
||||
- Leading code fences with language tags: ```lisp
|
||||
- Trailing code fences: ```
|
||||
- Orphan closing fences: ```"
|
||||
|
||||
(if (and text (stringp text))
|
||||
(let ((cleaned text))
|
||||
(setf cleaned (cl-ppcre:regex-replace-all "^```[a-z]*\\n" cleaned ""))
|
||||
@@ -90,295 +45,88 @@
|
||||
text))
|
||||
|
||||
(defun normalize-plist-keywords (plist)
|
||||
"Normalize all keys in a plist to keywords.
|
||||
|
||||
LLMs often return plists with unquoted keys: (TYPE REQUEST ...)
|
||||
instead of keyword syntax: (:TYPE :REQUEST ...)
|
||||
|
||||
This function converts all symbol keys to their keyword equivalents,
|
||||
making the plist compatible with standard Lisp property accessors.
|
||||
|
||||
Example transformation:
|
||||
(TYPE REQUEST PAYLOAD (ACTION MESSAGE TEXT \"Hi\"))
|
||||
=> (:TYPE :REQUEST :PAYLOAD (:ACTION :MESSAGE :TEXT \"Hi\"))"
|
||||
|
||||
(when (listp plist)
|
||||
(loop for (k . rest) on plist by #'cddr
|
||||
(loop for (k v) on plist by #'cddr
|
||||
collect (if (and (symbolp k) (not (keywordp k)))
|
||||
(intern (string k) :keyword)
|
||||
k)
|
||||
collect (car rest))))
|
||||
collect v)))
|
||||
|
||||
(defun think (context)
|
||||
"Generate a Lisp action proposal based on current context.
|
||||
|
||||
This is the core cognitive function. It:
|
||||
|
||||
1. Finds the most relevant skill based on context
|
||||
2. Assembles global awareness (memory context, system logs)
|
||||
3. Constructs a detailed prompt with available tools
|
||||
4. Calls the LLM via probabilistic-call
|
||||
5. Parses the LLM response into a structured action plist
|
||||
|
||||
The LLM is instructed to respond with exactly ONE plist, never prose.
|
||||
This constraint makes parsing deterministic and prevents rambling.
|
||||
|
||||
Returns a plist with structure:
|
||||
(:TYPE :REQUEST :TARGET :CLI :PAYLOAD (:ACTION :MESSAGE :TEXT \"...\"))"
|
||||
|
||||
;; Gather context components
|
||||
(let* ((active-skill (find-triggered-skill context))
|
||||
(tool-belt (generate-tool-belt-prompt))
|
||||
(global-context (context-assemble-global-awareness))
|
||||
(system-logs (context-get-system-logs))
|
||||
(assistant-name (or (uiop:getenv "MEMEX_ASSISTANT") "Agent"))
|
||||
(rejection-trace (proto-get (proto-get context :payload) :rejection-trace)))
|
||||
|
||||
;; Generate prompt from skill or raw text
|
||||
(let* ((prompt-generator (when active-skill
|
||||
(skill-probabilistic-prompt active-skill)))
|
||||
(raw-prompt (if prompt-generator
|
||||
(funcall prompt-generator context)
|
||||
;; Fallback: use raw user input
|
||||
(let ((p (proto-get (proto-get context :payload) :text)))
|
||||
(if (and p (stringp p))
|
||||
p
|
||||
"Maintain metabolic stasis."))))
|
||||
|
||||
;; Inject Reflection Loop feedback if a previous proposal was rejected
|
||||
(reflection-feedback (if rejection-trace
|
||||
(format nil "~%~%PREVIOUS PROPOSAL REJECTED:~%Your previous proposal was rejected by the deterministic safety gates.~%Rejection Trace: ~a~%You MUST fix the syntax or logic error described above and try again." rejection-trace)
|
||||
""))
|
||||
|
||||
(system-prompt (format nil
|
||||
"IDENTITY: ~a~a
|
||||
|
||||
You are a component of the OpenCortex neurosymbolic AI agent.
|
||||
Your task is to generate exactly ONE valid Lisp plist response.
|
||||
|
||||
MANDATE: Respond with ONE Lisp plist. Never output prose.
|
||||
|
||||
IMPORTANT: To reply to the user, you MUST use:
|
||||
(:TYPE :REQUEST :PAYLOAD (:ACTION :MESSAGE :TEXT \"<Response Text>\"))
|
||||
|
||||
To call a tool, you MUST use:
|
||||
(:TYPE :REQUEST :TARGET :TOOL :ACTION :CALL :TOOL \"<name>\" :ARGS (:arg1 \"val\"))
|
||||
|
||||
MANDATORY VALIDATION RULE: Before declaring any Lisp code edit complete,
|
||||
you MUST call the `:validate-lisp` tool with the proposed code. If the tool
|
||||
returns `:status :error`, read the `:reason` and `:failed` fields, fix the
|
||||
defect, and re-validate. You are strictly forbidden from relying on your
|
||||
own paren-balancing or syntax intuition.
|
||||
|
||||
PROVIDER RULE: Always use the default cascade provider unless a specific
|
||||
model or capability is required for the task.
|
||||
|
||||
AVAILABLE TOOLS:
|
||||
~a
|
||||
|
||||
GLOBAL CONTEXT:
|
||||
~a
|
||||
|
||||
RECENT LOGS:
|
||||
~a"
|
||||
assistant-name
|
||||
reflection-feedback
|
||||
tool-belt
|
||||
global-context
|
||||
system-logs)))
|
||||
|
||||
;; Call LLM and process response
|
||||
(let* ((thought (probabilistic-call raw-prompt
|
||||
:system-prompt system-prompt
|
||||
:context context))
|
||||
(cleaned (strip-markdown thought))
|
||||
(meta (proto-get context :meta))
|
||||
(source (proto-get meta :source)))
|
||||
|
||||
(when cleaned
|
||||
(harness-log "THINK: LLM raw output = ~a"
|
||||
(subseq cleaned 0 (min 200 (length cleaned)))))
|
||||
|
||||
;; Parse LLM response
|
||||
(if (and cleaned (stringp cleaned) (> (length cleaned) 0))
|
||||
(let ((*read-eval* nil))
|
||||
(if (char= (char cleaned 0) #\()
|
||||
;; Response starts with paren - try to parse as plist
|
||||
(handler-case
|
||||
(let ((parsed (read-from-string cleaned)))
|
||||
(when parsed
|
||||
(harness-log "THINK: parsed = ~a" parsed)
|
||||
|
||||
;; Normalize keyword keys (LLM often returns TYPE instead of :TYPE)
|
||||
(let ((parsed-normalized (normalize-plist-keywords parsed))
|
||||
(type (proto-get parsed :TYPE))
|
||||
(target (or (proto-get parsed :TARGET)
|
||||
(proto-get parsed :target))))
|
||||
|
||||
(cond
|
||||
;; Recognized message type - use directly
|
||||
((member type '(:REQUEST :EVENT :STATUS :RESPONSE))
|
||||
(unless (proto-get parsed :target)
|
||||
(setf (getf parsed :target) (or source :CLI)))
|
||||
parsed-normalized)
|
||||
|
||||
;; Tool call detected - wrap in standard envelope
|
||||
((or (eq target :TOOL)
|
||||
(eq target :tool)
|
||||
(getf parsed :TOOL)
|
||||
(getf parsed :tool)
|
||||
(and (listp parsed)
|
||||
(listp (car parsed))
|
||||
(keywordp (caar parsed))))
|
||||
(list :TYPE :REQUEST
|
||||
:TARGET :TOOL
|
||||
:PAYLOAD (normalize-plist-keywords parsed)))
|
||||
|
||||
;; Unknown format - treat as user message
|
||||
(t
|
||||
(list :TYPE :REQUEST
|
||||
:TARGET (or source :CLI)
|
||||
:PAYLOAD (list :ACTION :MESSAGE :TEXT cleaned)))))))
|
||||
(error (c)
|
||||
(harness-log "THINK ERROR: ~a" c)
|
||||
(list :TYPE :REQUEST
|
||||
:TARGET (or source :CLI)
|
||||
:PAYLOAD (list :ACTION :MESSAGE :TEXT cleaned))))
|
||||
|
||||
;; No leading paren - treat as plain text message
|
||||
(list :TYPE :REQUEST
|
||||
:TARGET (or source :CLI)
|
||||
:PAYLOAD (list :ACTION :MESSAGE :TEXT cleaned))))
|
||||
|
||||
;; No response from LLM
|
||||
thought)))))
|
||||
(rejection-trace (proto-get (proto-get context :payload) :rejection-trace))
|
||||
(prompt-generator (when active-skill (skill-probabilistic-prompt active-skill)))
|
||||
(raw-prompt (if prompt-generator
|
||||
(funcall prompt-generator context)
|
||||
(let ((p (proto-get (proto-get context :payload) :text)))
|
||||
(if (and p (stringp p)) p "Maintain metabolic stasis."))))
|
||||
(reflection-feedback (if rejection-trace
|
||||
(format nil "~%~%PREVIOUS PROPOSAL REJECTED: ~a" rejection-trace)
|
||||
""))
|
||||
(system-prompt (format nil "IDENTITY: ~a~a~%~%TOOLS:~%~a~%~%CONTEXT:~%~a~%~%LOGS:~%~a"
|
||||
assistant-name reflection-feedback tool-belt global-context system-logs)))
|
||||
(let* ((thought (probabilistic-call raw-prompt :system-prompt system-prompt :context context))
|
||||
(cleaned (strip-markdown thought)))
|
||||
(if (and cleaned (stringp cleaned) (> (length cleaned) 0) (or (char= (char cleaned 0) #\() (char= (char cleaned 0) #\[)))
|
||||
(handler-case
|
||||
(let ((parsed (read-from-string cleaned)))
|
||||
(if (listp parsed)
|
||||
(normalize-plist-keywords parsed)
|
||||
(list :TYPE :REQUEST :PAYLOAD (list :ACTION :MESSAGE :TEXT cleaned :EXPLANATION "Generated by the Probabilistic engine."))))
|
||||
(error () (list :TYPE :REQUEST :PAYLOAD (list :ACTION :MESSAGE :TEXT cleaned :EXPLANATION "Generated by the Probabilistic engine."))))
|
||||
(list :TYPE :REQUEST :PAYLOAD (list :ACTION :MESSAGE :TEXT (if (stringp cleaned) cleaned "No response") :EXPLANATION "Generated by the Probabilistic engine."))))))
|
||||
|
||||
(defun deterministic-verify (proposed-action context)
|
||||
"Run all skill deterministic gates on a proposed action.
|
||||
|
||||
Each skill can define a deterministic function that either:
|
||||
- Passes the action through unchanged
|
||||
- Modifies the action (adds explanation, changes target, etc.)
|
||||
- Blocks the action (returns a :LOG message instead)
|
||||
|
||||
Skills are sorted by priority (highest first). A skill with higher
|
||||
priority can intercept and modify actions before lower-priority
|
||||
skills see them.
|
||||
|
||||
The Bouncer Pattern: If any skill returns a :LOG or :EVENT type,
|
||||
processing stops and that message is returned immediately. This
|
||||
allows skills to veto actions.
|
||||
|
||||
Example skill chain:
|
||||
1. Policy skill (priority 500) - checks for missing explanations
|
||||
2. Protocol validator (priority 95) - validates message schema
|
||||
3. Shell actuator guard (priority 50) - checks command whitelist"
|
||||
|
||||
(let ((current-action proposed-action)
|
||||
(skills nil))
|
||||
|
||||
;; Collect all skills with deterministic functions
|
||||
(maphash (lambda (name skill)
|
||||
(declare (ignore name))
|
||||
(when (skill-deterministic-fn skill)
|
||||
(push skill skills)))
|
||||
*skills-registry*)
|
||||
|
||||
;; Sort by priority (highest first)
|
||||
(setf skills (sort skills #'> :key #'skill-priority))
|
||||
|
||||
;; Run each skill's gate
|
||||
(dolist (skill skills)
|
||||
(let ((trigger (skill-trigger-fn skill))
|
||||
(gate (skill-deterministic-fn skill)))
|
||||
|
||||
;; Skill activates if no trigger or trigger returns true
|
||||
(when (or (null trigger)
|
||||
(ignore-errors (funcall trigger context)))
|
||||
|
||||
;; Run the gate
|
||||
(when (or (null trigger) (ignore-errors (funcall trigger context)))
|
||||
(let ((next-action (funcall gate current-action context)))
|
||||
(let ((original-type (proto-get current-action :type)))
|
||||
|
||||
;; Check if skill intercepted (returned LOG/EVENT instead of REQUEST)
|
||||
(when (and (listp next-action)
|
||||
(member (proto-get next-action :type)
|
||||
'(:LOG :EVENT :log :event))
|
||||
(or (not (member original-type '(:LOG :EVENT :log :event)))
|
||||
(not (eq next-action current-action))))
|
||||
|
||||
;; Skill blocked or modified - stop processing
|
||||
(harness-log "DETERMINISTIC: Intercepted by skill '~a'"
|
||||
(skill-name skill))
|
||||
(return-from deterministic-verify next-action)))
|
||||
|
||||
;; Action passed through - continue to next skill
|
||||
(setf current-action next-action)))))
|
||||
|
||||
;; Return final action (may be modified by skills, or original if all passed)
|
||||
(when (and (listp next-action)
|
||||
(member (proto-get next-action :type) '(:LOG :EVENT)))
|
||||
(harness-log "DETERMINISTIC: Intercepted by skill '~a'" (skill-name skill))
|
||||
(return-from deterministic-verify next-action))
|
||||
(when next-action (setf current-action next-action))))))
|
||||
current-action))
|
||||
|
||||
(defun reason-gate (signal)
|
||||
"Stage 2 of the metabolic pipeline: Reason.
|
||||
|
||||
Transforms perceived signals into approved actions by combining:
|
||||
1. Probabilistic reasoning (LLM generates proposal)
|
||||
2. Deterministic verification (skills validate proposal)
|
||||
|
||||
Only processes :EVENT signals with :user-input or :chat-message sensors.
|
||||
Other signals pass through unchanged (heartbeats, tool outputs, etc.).
|
||||
|
||||
Modifies the signal in place by setting:
|
||||
- :approved-action - The final verified action, or NIL
|
||||
- :status - :reasoned
|
||||
|
||||
Returns the modified signal."
|
||||
|
||||
(let* ((type (proto-get signal :type))
|
||||
(payload (proto-get signal :payload))
|
||||
(sensor (proto-get payload :sensor)))
|
||||
|
||||
;; Only reason about user input, not internal signals
|
||||
(unless (and (eq type :EVENT)
|
||||
(member sensor '(:user-input :chat-message)))
|
||||
(unless (and (eq type :EVENT) (member sensor '(:user-input :chat-message)))
|
||||
(return-from reason-gate signal))
|
||||
|
||||
;; Reflection Loop: Retry up to 3 times if deterministic gates reject
|
||||
(let ((retries 3)
|
||||
(current-signal (copy-tree signal))
|
||||
(last-rejection nil))
|
||||
(loop
|
||||
(when (<= retries 0)
|
||||
(harness-log "REASON: Reflection loop exhausted. Final rejection.")
|
||||
(setf (getf signal :approved-action) last-rejection)
|
||||
(setf (getf signal :status) :reasoned)
|
||||
(return signal))
|
||||
|
||||
(when last-rejection
|
||||
(setf (getf (getf current-signal :payload) :rejection-trace) last-rejection))
|
||||
|
||||
(let ((candidate (think current-signal)))
|
||||
(harness-log "REASON: candidate type = ~a" (type-of candidate))
|
||||
|
||||
(if (and candidate
|
||||
(listp candidate)
|
||||
(or (keywordp (car candidate))
|
||||
(eq (car candidate) 'TYPE)
|
||||
(eq (car candidate) 'type)))
|
||||
|
||||
(if (and candidate (listp candidate))
|
||||
(let ((verified (deterministic-verify candidate current-signal)))
|
||||
(if (member (getf verified :type) '(:LOG :EVENT :log :event))
|
||||
(progn
|
||||
(harness-log "REASON: Proposal rejected by gate. Retrying (~a left)." (1- retries))
|
||||
(decf retries)
|
||||
(setf last-rejection verified))
|
||||
(if (member (getf verified :type) '(:LOG :EVENT))
|
||||
(progn (decf retries) (setf last-rejection verified))
|
||||
(progn
|
||||
(setf (getf signal :approved-action) verified)
|
||||
(setf (getf signal :status) :reasoned)
|
||||
(return signal))))
|
||||
|
||||
(progn
|
||||
(harness-log "REASON: Invalid candidate type ~a, dropping" (type-of candidate))
|
||||
(setf (getf signal :approved-action) nil)
|
||||
(setf (getf signal :status) :reasoned)
|
||||
(return signal))))))))
|
||||
|
||||
@@ -96,14 +96,14 @@ The Reason stage implements the core Innovation of OpenCortex: the separation of
|
||||
assistant-name reflection-feedback tool-belt global-context system-logs)))
|
||||
(let* ((thought (probabilistic-call raw-prompt :system-prompt system-prompt :context context))
|
||||
(cleaned (strip-markdown thought)))
|
||||
(if (and cleaned (stringp cleaned) (> (length cleaned) 0) (char= (char cleaned 0) #\((char= (char cleaned 0) #\()))
|
||||
(handler-case
|
||||
(let ((parsed (read-from-string cleaned)))
|
||||
(if (listp parsed)
|
||||
(normalize-plist-keywords parsed)
|
||||
(list :TYPE :REQUEST :PAYLOAD (list :ACTION :MESSAGE :TEXT cleaned))))
|
||||
(error () (list :TYPE :REQUEST :PAYLOAD (list :ACTION :MESSAGE :TEXT cleaned))))
|
||||
(list :TYPE :REQUEST :PAYLOAD (list :ACTION :MESSAGE :TEXT (or cleaned "No response")))))))
|
||||
(if (and cleaned (stringp cleaned) (> (length cleaned) 0) (or (char= (char cleaned 0) #\() (char= (char cleaned 0) #\[)))
|
||||
(handler-case
|
||||
(let ((parsed (read-from-string cleaned)))
|
||||
(if (listp parsed)
|
||||
(normalize-plist-keywords parsed)
|
||||
(list :TYPE :REQUEST :PAYLOAD (list :ACTION :MESSAGE :TEXT cleaned :EXPLANATION "Generated by the Probabilistic engine."))))
|
||||
(error () (list :TYPE :REQUEST :PAYLOAD (list :ACTION :MESSAGE :TEXT cleaned :EXPLANATION "Generated by the Probabilistic engine."))))
|
||||
(list :TYPE :REQUEST :PAYLOAD (list :ACTION :MESSAGE :TEXT (if (stringp cleaned) cleaned "No response") :EXPLANATION "Generated by the Probabilistic engine."))))))
|
||||
#+end_src
|
||||
|
||||
** Deterministic Engine (Verification)
|
||||
@@ -126,7 +126,7 @@ The Reason stage implements the core Innovation of OpenCortex: the separation of
|
||||
(member (proto-get next-action :type) '(:LOG :EVENT)))
|
||||
(harness-log "DETERMINISTIC: Intercepted by skill '~a'" (skill-name skill))
|
||||
(return-from deterministic-verify next-action))
|
||||
(setf current-action next-action)))))
|
||||
(when next-action (setf current-action next-action))))))
|
||||
current-action))
|
||||
#+end_src
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ To maintain sovereignty, the harness must remain a "dumb" bus. It should not kno
|
||||
** The Installer Script (opencortex.sh)
|
||||
The shell script is the primary entry point. It handles the initial git clone, dependency installation, and literate tangle.
|
||||
|
||||
#+begin_src bash :tangle setup.shsetup.sh
|
||||
#+begin_src bash :tangle setup.sh
|
||||
#!/bin/bash
|
||||
# (The content here is a duplicate of the main opencortex.sh for literate consistency)
|
||||
# [Note: Implementation is already verified in the top-level script]
|
||||
|
||||
3
harness/setup.sh
Normal file
3
harness/setup.sh
Normal file
@@ -0,0 +1,3 @@
|
||||
#!/bin/bash
|
||||
# (The content here is a duplicate of the main opencortex.sh for literate consistency)
|
||||
# [Note: Implementation is already verified in the top-level script]
|
||||
@@ -2,42 +2,27 @@
|
||||
|
||||
(defun COSINE-SIMILARITY (v1 v2)
|
||||
"Computes cosine similarity between two vectors."
|
||||
(let* ((len1 (length v1))
|
||||
(len2 (length v2)))
|
||||
(let* ((len1 (length v1)) (len2 (length v2)))
|
||||
(if (or (zerop len1) (zerop len2))
|
||||
0.0
|
||||
(let* ((dot 0.0d0)
|
||||
(n1 0.0d0)
|
||||
(n2 0.0d0))
|
||||
(let* ((dot 0.0d0) (n1 0.0d0) (n2 0.0d0))
|
||||
(dotimes (i (min len1 len2))
|
||||
(let* ((x (coerce (elt v1 i) 'double-float))
|
||||
(y (coerce (elt v2 i) 'double-float)))
|
||||
(incf dot (* x y))
|
||||
(incf n1 (* x x))
|
||||
(incf n2 (* y y))))
|
||||
(if (or (zerop n1) (zerop n2))
|
||||
0.0
|
||||
(/ dot (sqrt (* n1 n2))))))))
|
||||
|
||||
;; TODO: Stub for vault - implement later
|
||||
(defun VAULT-MASK-STRING (s) "[MASKED]")
|
||||
(let* ((x (coerce (elt v1 i) 'double-float)) (y (coerce (elt v2 i) 'double-float)))
|
||||
(incf dot (* x y)) (incf n1 (* x x)) (incf n2 (* y y))))
|
||||
(if (or (zerop n1) (zerop n2)) 0.0 (/ dot (sqrt (* n1 n2))))))))
|
||||
|
||||
(defun VAULT-MASK-STRING (s) (declare (ignore s)) "[MASKED]")
|
||||
(defvar *VAULT-MEMORY* (make-hash-table :test 'equal))
|
||||
|
||||
|
||||
(defstruct skill name priority dependencies trigger-fn probabilistic-prompt deterministic-fn)
|
||||
|
||||
(defvar *skills-registry* (make-hash-table :test 'equal))
|
||||
(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))
|
||||
(defstruct skill-entry filename (status :discovered) error-log (load-time 0))
|
||||
|
||||
(defun find-triggered-skill (context)
|
||||
"Returns the highest priority skill whose trigger matches context AND has a probabilistic prompt."
|
||||
"Returns the highest priority skill whose trigger matches context."
|
||||
(let ((triggered nil))
|
||||
(maphash (lambda (name skill)
|
||||
(declare (ignore name))
|
||||
@@ -65,38 +50,33 @@
|
||||
(push name seen)
|
||||
(let ((skill (gethash (string-downcase (string name)) *skills-registry*)))
|
||||
(when skill
|
||||
(dolist (dep (skill-dependencies skill))
|
||||
(visit dep))))
|
||||
(dolist (dep (skill-dependencies skill)) (visit dep))))
|
||||
(push name resolved))))
|
||||
(visit skill-name)
|
||||
(nreverse resolved))))
|
||||
|
||||
(defun parse-skill-metadata (filepath)
|
||||
"Extracts ID and DEPENDS_ON tags from org file."
|
||||
(let ((dependencies nil)
|
||||
(id nil)
|
||||
(content (uiop:read-file-string filepath)))
|
||||
;; Simple ID extraction using string search
|
||||
(let ((dependencies nil) (id nil) (content (uiop:read-file-string filepath)))
|
||||
(let ((id-start (search ":ID:" content)))
|
||||
(when id-start
|
||||
(let ((id-end (position #\Newline content :start id-start)))
|
||||
(when id-end
|
||||
(setf id (string-trim " " (subseq content (+ id-start 4) id-end)))))))
|
||||
;; Simple DEPENDS_ON extraction
|
||||
(when id-end (setf id (string-trim " " (subseq content (+ id-start 4) id-end)))))))
|
||||
(let ((pos 0))
|
||||
(loop while (setf pos (search "#+DEPENDS_ON:" content :start2 pos))
|
||||
do (let ((end (position #\Newline content :start pos)))
|
||||
(when end
|
||||
(let ((line (string-trim " " (subseq content (+ pos 13) end))))
|
||||
(dolist (d (uiop:split-string line :separator '(#\Space #\Tab)))
|
||||
(unless (string= d "")
|
||||
(push d dependencies))))
|
||||
(unless (string= d "") (push d dependencies))))
|
||||
(setf pos end)))))
|
||||
(values id (reverse dependencies))))
|
||||
|
||||
(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"))
|
||||
"Returns a list of skill filepaths sorted by dependency."
|
||||
(let* ((org-files (uiop:directory-files skills-dir "org-skill-*.org"))
|
||||
(lisp-files (uiop:directory-files skills-dir "org-skill-*.lisp"))
|
||||
(files (append org-files lisp-files))
|
||||
(adj (make-hash-table :test 'equal))
|
||||
(name-to-file (make-hash-table :test 'equal))
|
||||
(id-to-file (make-hash-table :test 'equal))
|
||||
@@ -105,10 +85,14 @@
|
||||
(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))))
|
||||
(if (uiop:string-suffix-p (namestring file) ".lisp")
|
||||
(progn
|
||||
(setf (gethash (string-downcase filename) name-to-file) file)
|
||||
(setf (gethash (string-downcase filename) adj) nil))
|
||||
(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)))
|
||||
@@ -124,7 +108,7 @@
|
||||
(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)
|
||||
(error "Circular dependency detected")
|
||||
(visit dep-file))))))
|
||||
(setf (gethash node-key stack) nil)
|
||||
(setf (gethash node-key visited) t)
|
||||
@@ -136,104 +120,85 @@
|
||||
(nreverse result))))
|
||||
|
||||
(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-utils-validate)
|
||||
(lisp-utils-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.")))))
|
||||
"Checks if a string contains valid Common Lisp forms."
|
||||
(handler-case
|
||||
(let ((*read-eval* nil))
|
||||
(with-input-from-string (s (format nil "(progn ~a)" code-string))
|
||||
(loop for form = (read s nil :eof) until (eq form :eof)))
|
||||
(values t nil))
|
||||
(error (c) (values nil (format nil "~a" c)))))
|
||||
|
||||
(defun remove-in-package-forms (code-string)
|
||||
"Removes in-package forms so symbols get defined in skill package."
|
||||
(let ((lines (uiop:split-string code-string :separator '(#\Newline)))
|
||||
(result ""))
|
||||
(dolist (line lines)
|
||||
(let ((trimmed (string-trim '(#\Space #\Tab) line)))
|
||||
(unless (uiop:string-prefix-p "(in-package" trimmed)
|
||||
(setf result (concatenate 'string result line (string #\Newline))))))
|
||||
result))
|
||||
|
||||
(defun extract-tangle-target (line)
|
||||
"Extracts the value of the :tangle header from an org src block line.
|
||||
Handles both simple strings and parenthesized elisp expressions."
|
||||
"Extracts the value of the :tangle header."
|
||||
(let ((pos (search ":tangle" line)))
|
||||
(when pos
|
||||
(let ((rest (string-trim '(#\Space #\Tab) (subseq line (+ pos 7)))))
|
||||
(if (char= (char rest 0) #\()
|
||||
;; It's an elisp expression, find the matching closing paren
|
||||
(let ((balance 0)
|
||||
(end nil))
|
||||
(dotimes (i (length rest))
|
||||
(let ((ch (char rest i)))
|
||||
(cond ((char= ch #\() (incf balance))
|
||||
((char= ch #\)) (decf balance)))
|
||||
(when (and (> i 0) (= balance 0))
|
||||
(setf end (1+ i))
|
||||
(return-from extract-tangle-target (subseq rest 0 end)))))
|
||||
rest)
|
||||
;; It's a simple string, stop at next space
|
||||
(let ((end (position #\Space rest)))
|
||||
(if end (subseq rest 0 end) rest)))))))
|
||||
(let ((end (position #\Space rest)))
|
||||
(if end (subseq rest 0 end) rest))))))
|
||||
|
||||
(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."
|
||||
"Parses and evaluates Lisp blocks from an Org file."
|
||||
(let* ((skill-base-name (pathname-name filepath))
|
||||
(entry (or (gethash skill-base-name *skill-catalog*) (make-skill-entry :filename skill-base-name))))
|
||||
(entry (or (gethash skill-base-name *skill-catalog*) (setf (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 "")
|
||||
(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" clean-line)
|
||||
(setf in-lisp-block t)
|
||||
(let ((tangle-target (extract-tangle-target clean-line)))
|
||||
(if (or (and tangle-target (not (search "/tests" tangle-target)) (not (search ":tangle no" clean-line)))
|
||||
(and (not tangle-target) (not (search ":tangle no" clean-line))))
|
||||
(setf collect-this-block t)
|
||||
(setf collect-this-block nil))))
|
||||
|
||||
(let ((target (extract-tangle-target clean-line)))
|
||||
;; Collect if there's no tangle target (inherits from file)
|
||||
;; or if it's a lisp file and NOT a test.
|
||||
(setf collect-this-block (or (null target)
|
||||
(and (not (search "no" target))
|
||||
(not (search "/tests" target)))))))
|
||||
((uiop:string-prefix-p "#+end_src" clean-line)
|
||||
(setf in-lisp-block nil)
|
||||
(setf collect-this-block nil))
|
||||
|
||||
(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)))
|
||||
(uiop:string-prefix-p ":END:" (string-upcase clean-line))
|
||||
(uiop:string-prefix-p ":ID:" (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)
|
||||
(setf (skill-entry-status entry) :ready)
|
||||
(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 valid-p (error err)))
|
||||
(unless (find-package pkg-name)
|
||||
(let ((new-pkg (make-package pkg-name :use '(:cl))))
|
||||
(use-package :opencortex new-pkg)))
|
||||
(let ((new-pkg (make-package pkg-name :use '(:cl)))) (use-package :opencortex new-pkg)))
|
||||
(let ((*read-eval* nil) (*package* (find-package pkg-name)))
|
||||
(harness-log "LOADER: Evaluating code for '~a' in package ~a" skill-base-name (package-name *package*))
|
||||
(eval (read-from-string (format nil "(progn ~a)" lisp-code))))
|
||||
|
||||
|
||||
;; Export symbols back to :OPENCORTEX for discoverability and testing
|
||||
(let* ((target-pkg (find-package :opencortex))
|
||||
(raw-name (string-upcase skill-base-name))
|
||||
(short-name (if (uiop:string-prefix-p "ORG-SKILL-" raw-name)
|
||||
(subseq raw-name 10)
|
||||
raw-name)))
|
||||
(harness-log "LOADER: Scanning package ~a for symbols to export..." (package-name (find-package pkg-name)))
|
||||
(do-symbols (sym (find-package pkg-name))
|
||||
(when (eq (symbol-package sym) (find-package pkg-name))
|
||||
(let ((sn (symbol-name sym)))
|
||||
(when (or (uiop:string-prefix-p raw-name sn)
|
||||
(uiop:string-prefix-p short-name sn))
|
||||
(uiop:string-prefix-p short-name sn)
|
||||
(string-equal sn "DOCTOR-MAIN")
|
||||
(string-equal sn "RUN-SETUP-WIZARD"))
|
||||
(harness-log "LOADER: Exporting ~a to :OPENCORTEX" sn)
|
||||
;; Resolve potential name conflicts by uninterning first
|
||||
(let ((existing (find-symbol sn target-pkg)))
|
||||
@@ -241,227 +206,66 @@ Only loads blocks that specify a .lisp tangle target, ignoring tests and example
|
||||
(unintern existing target-pkg)))
|
||||
(import sym target-pkg)
|
||||
(export sym target-pkg))))))
|
||||
|
||||
(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)))))
|
||||
(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))))
|
||||
(setf (skill-entry-status entry) :ready)))
|
||||
t)
|
||||
(error (c)
|
||||
(harness-log "LOADER ERROR in skill '~a': ~a" skill-base-name c)
|
||||
(setf (skill-entry-status entry) :failed) nil))))
|
||||
|
||||
(defun load-skill-from-lisp (filepath)
|
||||
"Loads a .lisp skill file directly, filtering out in-package forms."
|
||||
(let* ((skill-base-name (pathname-name filepath))
|
||||
(entry (or (gethash skill-base-name *skill-catalog*) (setf (gethash skill-base-name *skill-catalog*) (make-skill-entry :filename skill-base-name)))))
|
||||
(setf (skill-entry-status entry) :loading)
|
||||
(handler-case
|
||||
(let* ((content (remove-in-package-forms (uiop:read-file-string filepath)))
|
||||
(pkg-name (intern (string-upcase (format nil "OPENCORTEX.SKILLS.~a" skill-base-name)) :keyword)))
|
||||
(multiple-value-bind (valid-p err) (validate-lisp-syntax content)
|
||||
(unless valid-p (error err)))
|
||||
(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)))
|
||||
(harness-log "LOADER: Loading .lisp skill '~a' in package ~a" skill-base-name (package-name *package*))
|
||||
;; Evaluate forms individually so one bad form doesn't abort the entire skill
|
||||
(with-input-from-string (s content)
|
||||
(loop for form = (read s nil :eof) until (eq form :eof)
|
||||
do (handler-case (eval form)
|
||||
(error (c) (harness-log "LOADER WARNING in '~a': ~a" skill-base-name c))))))
|
||||
;; Export symbols
|
||||
(let* ((target-pkg (find-package :opencortex))
|
||||
(raw-name (string-upcase skill-base-name))
|
||||
(short-name (if (uiop:string-prefix-p "ORG-SKILL-" raw-name)
|
||||
(subseq raw-name 10)
|
||||
raw-name)))
|
||||
(harness-log "LOADER: Scanning package ~a for symbols to export..." (package-name (find-package pkg-name)))
|
||||
(do-symbols (sym (find-package pkg-name))
|
||||
(when (eq (symbol-package sym) (find-package pkg-name))
|
||||
(let ((sn (symbol-name sym)))
|
||||
(when (or (uiop:string-prefix-p raw-name sn)
|
||||
(uiop:string-prefix-p short-name sn)
|
||||
(string-equal sn "DOCTOR-MAIN")
|
||||
(string-equal sn "RUN-SETUP-WIZARD"))
|
||||
(harness-log "LOADER: Exporting ~a to :OPENCORTEX" sn)
|
||||
(let ((existing (find-symbol sn target-pkg)))
|
||||
(when (and existing (not (eq existing sym)))
|
||||
(unintern existing target-pkg)))
|
||||
(import sym target-pkg)
|
||||
(export sym target-pkg))))))
|
||||
(setf (skill-entry-status entry) :ready))
|
||||
(error (c)
|
||||
(harness-log "LOADER ERROR in skill '~a': ~a" skill-base-name c)
|
||||
(setf (skill-entry-status entry) :failed) nil))))
|
||||
|
||||
(defun initialize-all-skills ()
|
||||
"Scans the directory defined by SKILLS_DIR and hot-loads skills using topological order."
|
||||
"Initializes all skills from SKILLS_DIR."
|
||||
(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))
|
||||
|
||||
(skills-dir (uiop:ensure-directory-pathname (or env-path (namestring (merge-pathnames "notes/" (user-homedir-pathname)))))))
|
||||
(unless (uiop:directory-exists-p skills-dir) (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))))))
|
||||
|
||||
(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)
|
||||
(let ((perm (ignore-errors (uiop:symbol-call :opencortex.skills.org-skill-tool-permissions :get-tool-permission name))))
|
||||
(unless (eq perm :deny)
|
||||
(setf output (concatenate 'string output
|
||||
(format nil "- ~a: ~a~% Parameters: ~s~%~%"
|
||||
name
|
||||
(cognitive-tool-description tool)
|
||||
(cognitive-tool-parameters tool)))))))
|
||||
*cognitive-tools*)
|
||||
output))
|
||||
|
||||
(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))))
|
||||
|
||||
(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)))))
|
||||
|
||||
(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))))))))))
|
||||
|
||||
(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"))
|
||||
(abs-path (namestring (uiop:ensure-absolute-pathname file (uiop:getcwd)))))
|
||||
(and (str:starts-with-p memex-root abs-path)
|
||||
(not (search ".." abs-path)))))
|
||||
: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))))))
|
||||
|
||||
(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"))
|
||||
(abs-path (namestring (uiop:ensure-absolute-pathname file (uiop:getcwd)))))
|
||||
(and (str:starts-with-p memex-root abs-path)
|
||||
(not (search ".." abs-path))
|
||||
(not (str:ends-with-p ".org" abs-path))))) ;; Force AST tools for .org files
|
||||
: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))))))
|
||||
|
||||
(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"))
|
||||
(abs-path (namestring (uiop:ensure-absolute-pathname file (uiop:getcwd)))))
|
||||
(and (str:starts-with-p memex-root abs-path)
|
||||
(not (search ".." abs-path))
|
||||
(not (str:ends-with-p ".org" abs-path))))) ;; Force AST tools for .org files
|
||||
: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))))))
|
||||
(harness-log "LOADER: Initializing ~a skills..." (length sorted-files))
|
||||
(dolist (file sorted-files)
|
||||
(if (uiop:string-suffix-p (namestring file) ".lisp")
|
||||
(load-skill-from-lisp file)
|
||||
(load-skill-from-org file)))
|
||||
(harness-log "LOADER: Boot Complete."))))
|
||||
|
||||
@@ -96,7 +96,9 @@ The ~opencortex~ Skill Engine enables **Late-Binding Intelligence**, allowing th
|
||||
#+begin_src lisp
|
||||
(defun topological-sort-skills (skills-dir)
|
||||
"Returns a list of skill filepaths sorted by dependency."
|
||||
(let ((files (uiop:directory-files skills-dir "org-skill-*.org"))
|
||||
(let* ((org-files (uiop:directory-files skills-dir "org-skill-*.org"))
|
||||
(lisp-files (uiop:directory-files skills-dir "org-skill-*.lisp"))
|
||||
(files (append org-files lisp-files))
|
||||
(adj (make-hash-table :test 'equal))
|
||||
(name-to-file (make-hash-table :test 'equal))
|
||||
(id-to-file (make-hash-table :test 'equal))
|
||||
@@ -105,10 +107,14 @@ The ~opencortex~ Skill Engine enables **Late-Binding Intelligence**, allowing th
|
||||
(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))))
|
||||
(if (uiop:string-suffix-p (namestring file) ".lisp")
|
||||
(progn
|
||||
(setf (gethash (string-downcase filename) name-to-file) file)
|
||||
(setf (gethash (string-downcase filename) adj) nil))
|
||||
(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)))
|
||||
@@ -147,6 +153,16 @@ The ~opencortex~ Skill Engine enables **Late-Binding Intelligence**, allowing th
|
||||
(values t nil))
|
||||
(error (c) (values nil (format nil "~a" c)))))
|
||||
|
||||
(defun remove-in-package-forms (code-string)
|
||||
"Removes in-package forms so symbols get defined in skill package."
|
||||
(let ((lines (uiop:split-string code-string :separator '(#\Newline)))
|
||||
(result ""))
|
||||
(dolist (line lines)
|
||||
(let ((trimmed (string-trim '(#\Space #\Tab) line)))
|
||||
(unless (uiop:string-prefix-p "(in-package" trimmed)
|
||||
(setf result (concatenate 'string result line (string #\Newline))))))
|
||||
result))
|
||||
|
||||
(defun extract-tangle-target (line)
|
||||
"Extracts the value of the :tangle header."
|
||||
(let ((pos (search ":tangle" line)))
|
||||
@@ -158,7 +174,7 @@ The ~opencortex~ Skill Engine enables **Late-Binding Intelligence**, allowing th
|
||||
(defun load-skill-from-org (filepath)
|
||||
"Parses and evaluates Lisp blocks from an Org file."
|
||||
(let* ((skill-base-name (pathname-name filepath))
|
||||
(entry (or (gethash skill-base-name *skill-catalog*) (make-skill-entry :filename skill-base-name))))
|
||||
(entry (or (gethash skill-base-name *skill-catalog*) (setf (gethash skill-base-name *skill-catalog*) (make-skill-entry :filename skill-base-name)))))
|
||||
(setf (skill-entry-status entry) :loading)
|
||||
(handler-case
|
||||
(let* ((content (uiop:read-file-string filepath))
|
||||
@@ -171,7 +187,7 @@ The ~opencortex~ Skill Engine enables **Late-Binding Intelligence**, allowing th
|
||||
((uiop:string-prefix-p "#+begin_src lisp" clean-line)
|
||||
(setf in-lisp-block t)
|
||||
(let ((target (extract-tangle-target clean-line)))
|
||||
;; Collect if there's no tangle target (inherits from file)
|
||||
;; Collect if there's no tangle target (inherits from file)
|
||||
;; or if it's a lisp file and NOT a test.
|
||||
(setf collect-this-block (or (null target)
|
||||
(and (not (search "no" target))
|
||||
@@ -193,7 +209,7 @@ The ~opencortex~ Skill Engine enables **Late-Binding Intelligence**, allowing th
|
||||
(let ((*read-eval* nil) (*package* (find-package pkg-name)))
|
||||
(harness-log "LOADER: Evaluating code for '~a' in package ~a" skill-base-name (package-name *package*))
|
||||
(eval (read-from-string (format nil "(progn ~a)" lisp-code))))
|
||||
|
||||
|
||||
;; Export symbols back to :OPENCORTEX for discoverability and testing
|
||||
(let* ((target-pkg (find-package :opencortex))
|
||||
(raw-name (string-upcase skill-base-name))
|
||||
@@ -215,12 +231,56 @@ The ~opencortex~ Skill Engine enables **Late-Binding Intelligence**, allowing th
|
||||
(unintern existing target-pkg)))
|
||||
(import sym target-pkg)
|
||||
(export sym target-pkg))))))
|
||||
|
||||
|
||||
(setf (skill-entry-status entry) :ready)))
|
||||
t)
|
||||
(error (c)
|
||||
(harness-log "LOADER ERROR in skill '~a': ~a" skill-base-name c)
|
||||
(setf (skill-entry-status entry) :failed) nil))))
|
||||
|
||||
(defun load-skill-from-lisp (filepath)
|
||||
"Loads a .lisp skill file directly, filtering out in-package forms."
|
||||
(let* ((skill-base-name (pathname-name filepath))
|
||||
(entry (or (gethash skill-base-name *skill-catalog*) (setf (gethash skill-base-name *skill-catalog*) (make-skill-entry :filename skill-base-name)))))
|
||||
(setf (skill-entry-status entry) :loading)
|
||||
(handler-case
|
||||
(let* ((content (remove-in-package-forms (uiop:read-file-string filepath)))
|
||||
(pkg-name (intern (string-upcase (format nil "OPENCORTEX.SKILLS.~a" skill-base-name)) :keyword)))
|
||||
(multiple-value-bind (valid-p err) (validate-lisp-syntax content)
|
||||
(unless valid-p (error err)))
|
||||
(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)))
|
||||
(harness-log "LOADER: Loading .lisp skill '~a' in package ~a" skill-base-name (package-name *package*))
|
||||
;; Evaluate forms individually so one bad form doesn't abort the entire skill
|
||||
(with-input-from-string (s content)
|
||||
(loop for form = (read s nil :eof) until (eq form :eof)
|
||||
do (handler-case (eval form)
|
||||
(error (c) (harness-log "LOADER WARNING in '~a': ~a" skill-base-name c))))))
|
||||
;; Export symbols
|
||||
(let* ((target-pkg (find-package :opencortex))
|
||||
(raw-name (string-upcase skill-base-name))
|
||||
(short-name (if (uiop:string-prefix-p "ORG-SKILL-" raw-name)
|
||||
(subseq raw-name 10)
|
||||
raw-name)))
|
||||
(harness-log "LOADER: Scanning package ~a for symbols to export..." (package-name (find-package pkg-name)))
|
||||
(do-symbols (sym (find-package pkg-name))
|
||||
(when (eq (symbol-package sym) (find-package pkg-name))
|
||||
(let ((sn (symbol-name sym)))
|
||||
(when (or (uiop:string-prefix-p raw-name sn)
|
||||
(uiop:string-prefix-p short-name sn)
|
||||
(string-equal sn "DOCTOR-MAIN")
|
||||
(string-equal sn "RUN-SETUP-WIZARD"))
|
||||
(harness-log "LOADER: Exporting ~a to :OPENCORTEX" sn)
|
||||
(let ((existing (find-symbol sn target-pkg)))
|
||||
(when (and existing (not (eq existing sym)))
|
||||
(unintern existing target-pkg)))
|
||||
(import sym target-pkg)
|
||||
(export sym target-pkg))))))
|
||||
(setf (skill-entry-status entry) :ready))
|
||||
(error (c)
|
||||
(harness-log "LOADER ERROR in skill '~a': ~a" skill-base-name c)
|
||||
(setf (skill-entry-status entry) :failed) nil))))
|
||||
#+end_src
|
||||
|
||||
** Initialize (initialize-all-skills)
|
||||
@@ -233,12 +293,14 @@ The ~opencortex~ Skill Engine enables **Late-Binding Intelligence**, allowing th
|
||||
(let ((sorted-files (topological-sort-skills skills-dir)))
|
||||
(harness-log "LOADER: Initializing ~a skills..." (length sorted-files))
|
||||
(dolist (file sorted-files)
|
||||
(load-skill-from-org file))
|
||||
(if (uiop:string-suffix-p (namestring file) ".lisp")
|
||||
(load-skill-from-lisp file)
|
||||
(load-skill-from-org file)))
|
||||
(harness-log "LOADER: Boot Complete."))))
|
||||
#+end_src
|
||||
|
||||
* Test Suite
|
||||
#+begin_src lisp :tangle tests/boot-sequence-tests.lisp
|
||||
#+begin_src lisp :tangle ../tests/boot-sequence-tests.lisp
|
||||
(eval-when (:compile-toplevel :load-toplevel :execute)
|
||||
(ql:quickload :fiveam :silent t))
|
||||
|
||||
|
||||
@@ -1,51 +1,33 @@
|
||||
(in-package :cl-user)
|
||||
|
||||
(defpackage :opencortex.tui
|
||||
(:use :cl :croatoan)
|
||||
(:use :cl :croatoan :usocket)
|
||||
(:export :main))
|
||||
|
||||
(in-package :opencortex.tui)
|
||||
|
||||
(defvar *daemon-host* "127.0.0.1")
|
||||
|
||||
(defvar *daemon-port* 9105)
|
||||
|
||||
(defvar *socket* nil)
|
||||
|
||||
(defvar *stream* nil)
|
||||
|
||||
(defvar *chat-history* (list) "Full chronological log of messages.")
|
||||
|
||||
(defvar *scroll-index* 0 "Offset for history rendering.")
|
||||
|
||||
(defvar *status-text* "Connecting...")
|
||||
|
||||
(defvar *chat-history* nil)
|
||||
(defvar *scroll-index* 0)
|
||||
(defvar *input-buffer* (make-array 0 :element-type 'char :fill-pointer 0 :adjustable t))
|
||||
|
||||
(defvar *command-history* (make-array 0 :element-type 't :fill-pointer 0 :adjustable t))
|
||||
|
||||
(defvar *history-index* -1)
|
||||
|
||||
(defvar *is-running* t)
|
||||
|
||||
(defvar *queue-lock* (bt:make-lock))
|
||||
|
||||
(defvar *incoming-msgs* nil)
|
||||
|
||||
(defun enqueue-msg (msg)
|
||||
"Thread-safe addition to incoming message queue."
|
||||
(bt:with-lock-held (*queue-lock*)
|
||||
(push msg *incoming-msgs*)))
|
||||
(setf *incoming-msgs* (append *incoming-msgs* (list msg)))))
|
||||
|
||||
(defun dequeue-msgs ()
|
||||
"Thread-safe retrieval of incoming messages."
|
||||
(bt:with-lock-held (*queue-lock*)
|
||||
(let ((msgs (nreverse *incoming-msgs*)))
|
||||
(let ((msgs *incoming-msgs*))
|
||||
(setf *incoming-msgs* nil)
|
||||
msgs)))
|
||||
|
||||
(defun get-line-style (text)
|
||||
"Determines croatoan attributes based on content patterns."
|
||||
(cond
|
||||
((uiop:string-prefix-p "*" text) '(:bold :yellow))
|
||||
((uiop:string-prefix-p "⬆" text) '(:cyan))
|
||||
@@ -54,77 +36,112 @@
|
||||
(t nil)))
|
||||
|
||||
(defun render-chat (win)
|
||||
"Renders the chat history with scrolling and styling."
|
||||
(clear win)
|
||||
(let* ((h (height win))
|
||||
(view-height (- h 2))
|
||||
(view-height (max 0 (- h 2)))
|
||||
(history-len (length *chat-history*))
|
||||
(start-idx *scroll-index*)
|
||||
(end-idx (min history-len (+ start-idx view-height)))
|
||||
(slice (reverse (subseq *chat-history* start-idx end-idx))))
|
||||
(loop for msg in slice
|
||||
for i from 1
|
||||
do (let ((style (get-line-style msg)))
|
||||
(add-string win (format nil "│ ~a" msg) :y i :x 1 :attributes style)))
|
||||
do (add-string win (format nil "│ ~a" msg) :y i :x 1 :attributes (get-line-style msg)))
|
||||
(refresh win)))
|
||||
|
||||
(defun handle-backspace ()
|
||||
"Deletes last character from input buffer."
|
||||
(when (> (fill-pointer *input-buffer*) 0)
|
||||
(decf (fill-pointer *input-buffer*))))
|
||||
|
||||
(defun handle-return (stream)
|
||||
"Process input buffer as message or command."
|
||||
(let ((cmd (coerce *input-buffer* 'string)))
|
||||
(setf (fill-pointer *input-buffer*) 0)
|
||||
(when (> (length cmd) 0)
|
||||
(enqueue-msg (format nil "⬆ ~a" cmd))
|
||||
(handler-case
|
||||
(when (and stream (open-stream-p stream))
|
||||
(format stream "~a" (opencortex:frame-message (list :TYPE :EVENT
|
||||
:META (list :SOURCE :tui)
|
||||
:PAYLOAD (list :SENSOR :user-input :TEXT cmd))))
|
||||
(finish-output stream))
|
||||
(progn
|
||||
(when (and stream (open-stream-p stream))
|
||||
(let* ((msg (list :TYPE :EVENT
|
||||
:META (list :SOURCE :tui)
|
||||
:PAYLOAD (list :SENSOR :user-input :TEXT cmd)))
|
||||
(payload (format nil "~s" msg))
|
||||
(len (length payload)))
|
||||
(format stream "~6,'0x~a" len payload)
|
||||
(finish-output stream)))
|
||||
(enqueue-msg "✓ Sent"))
|
||||
(error (c)
|
||||
(push "ERROR: Connection to daemon lost." *chat-history*)
|
||||
(format t "Send error: ~a~%" c)
|
||||
(enqueue-msg "ERROR: Connection to daemon lost.")
|
||||
(setf *is-running* nil))))
|
||||
(when (string= cmd "/exit") (setf *is-running* nil))
|
||||
(when (string= cmd "/clear") (setf *chat-history* nil))))
|
||||
|
||||
(defun start-background-reader (stream)
|
||||
"Starts a thread that reads framed messages from the daemon stream."
|
||||
(bt:make-thread
|
||||
(lambda ()
|
||||
(loop while *is-running* do
|
||||
(handler-case
|
||||
(let* ((len-buf (make-string 6))
|
||||
(count (read-sequence len-buf stream)))
|
||||
(when (= count 6)
|
||||
(let* ((msg-len (parse-integer len-buf :radix 16))
|
||||
(msg-buf (make-string msg-len)))
|
||||
(read-sequence msg-buf stream)
|
||||
(let ((msg (read-from-string msg-buf)))
|
||||
(let ((payload (getf msg :payload)))
|
||||
(cond
|
||||
((eq (getf payload :action) :handshake)
|
||||
(enqueue-msg "* Connected to daemon *"))
|
||||
((and (eq (getf payload :sensor) :loop-error)
|
||||
(not (string= (or (getf payload :message) "") "Neural Cascade Failure: All providers exhausted.")))
|
||||
(enqueue-msg (format nil "ERROR: Daemon loop error (~a)"
|
||||
(getf payload :message))))
|
||||
(t
|
||||
(let ((text (or (getf payload :text) (format nil "~a" payload))))
|
||||
(enqueue-msg (format nil "⬇ ~a" text)))))))))
|
||||
(error (c)
|
||||
(when *is-running*
|
||||
(enqueue-msg (format nil "ERROR: Connection lost (~a)" c))
|
||||
(setf *is-running* nil))))))
|
||||
:name "opencortex-tui-reader"))
|
||||
|
||||
(defun main ()
|
||||
"Initializes ncurses and starts the TUI event loop."
|
||||
(handler-case
|
||||
(setf *socket* (usocket:socket-connect *daemon-host* *daemon-port*))
|
||||
(error (e) (format t "Offline: ~a~%" e) (return-from main)))
|
||||
(setf *stream* (usocket:socket-stream *socket*))
|
||||
|
||||
;; Guard: Croatoan needs a real terminal (TERM env var, real TTY)
|
||||
(unless (uiop:getenv "TERM")
|
||||
(format t "TUI requires a terminal. Set TERM environment variable.~%")
|
||||
(format t "Or use: echo 'your message' | nc localhost 9105~%")
|
||||
(return-from main))
|
||||
|
||||
(unwind-protect
|
||||
(with-screen (scr :input-echoing nil :input-blocking nil :enable-colors t)
|
||||
(let* ((h (height scr)) (w (width scr)))
|
||||
(unless (and h w)
|
||||
(error "Screen dimensions are NIL: h=~a, w=~a" h w))
|
||||
(let ((chat-win (make-instance 'window :height (- h 5) :width (- w 2) :position '(1 1) :border t))
|
||||
(input-win (make-instance 'window :height 1 :width (- w 2) :position (list (- h 2) 1) :border t)))
|
||||
|
||||
(setf (input-blocking input-win) nil)
|
||||
|
||||
(loop :while *is-running* :do
|
||||
(let ((msgs (dequeue-msgs)))
|
||||
(when msgs
|
||||
(dolist (m msgs) (push m *chat-history*))
|
||||
(render-chat chat-win)))
|
||||
|
||||
(let* ((ev (get-event input-win))
|
||||
(ch (when (and ev (typep ev 'event)) (event-key ev))))
|
||||
(when ch
|
||||
(cond
|
||||
((or (eq ch #\Newline) (eq ch #\Return)) (handle-return *stream*))
|
||||
((or (eq ch :backspace) (eq ch (code-char 127))) (handle-backspace))
|
||||
((characterp ch) (vector-push-extend ch *input-buffer*))))
|
||||
|
||||
(clear input-win)
|
||||
(add-string input-win (format nil "▶ ~a" (coerce *input-buffer* 'string)) :y 0 :x 1)
|
||||
(refresh input-win))
|
||||
(sleep 0.02)))))
|
||||
(handler-case
|
||||
(with-screen (scr :input-echoing nil :input-blocking nil :enable-colors t)
|
||||
(let* ((h (height scr)) (w (width scr)))
|
||||
(let ((chat-win (make-instance 'window :height (- h 5) :width (- w 2) :position '(1 1) :border t))
|
||||
(input-win (make-instance 'window :height 1 :width (- w 2) :position (list (- h 2) 1) :border t)))
|
||||
(setf (input-blocking input-win) nil)
|
||||
(start-background-reader *stream*)
|
||||
(loop :while *is-running* :do
|
||||
(let ((msgs (dequeue-msgs)))
|
||||
(when msgs
|
||||
(dolist (m msgs) (push m *chat-history*))
|
||||
(render-chat chat-win)))
|
||||
(let* ((ev (get-event input-win))
|
||||
(ch (when (and ev (typep ev 'event)) (event-key ev))))
|
||||
(when ch
|
||||
(cond
|
||||
((or (eq ch #\Newline) (eq ch #\Return)) (handle-return *stream*))
|
||||
((or (eq ch :backspace) (eq ch (code-char 127))) (handle-backspace))
|
||||
((characterp ch) (vector-push-extend ch *input-buffer*))))
|
||||
(clear input-win)
|
||||
(add-string input-win (format nil "▶ ~a" (coerce *input-buffer* 'string)) :y 0 :x 1)
|
||||
(refresh input-win))
|
||||
(sleep 0.02)))))
|
||||
(error (c)
|
||||
(format t "TUI Error: ~a~%" c)))
|
||||
(setf *is-running* nil)
|
||||
(when *socket* (ignore-errors (usocket:socket-close *socket*)))))
|
||||
|
||||
@@ -126,6 +126,39 @@ The OpenCortex TUI Client is a standalone Common Lisp application built on **Cro
|
||||
(when (string= cmd "/clear") (setf *chat-history* nil))))
|
||||
#+end_src
|
||||
|
||||
** Background Reader
|
||||
#+begin_src lisp
|
||||
(defun start-background-reader (stream)
|
||||
"Starts a thread that reads framed messages from the daemon stream."
|
||||
(bt:make-thread
|
||||
(lambda ()
|
||||
(loop while *is-running* do
|
||||
(handler-case
|
||||
(let* ((len-buf (make-string 6))
|
||||
(count (read-sequence len-buf stream)))
|
||||
(when (= count 6)
|
||||
(let* ((msg-len (parse-integer len-buf :radix 16))
|
||||
(msg-buf (make-string msg-len)))
|
||||
(read-sequence msg-buf stream)
|
||||
(let ((msg (read-from-string msg-buf)))
|
||||
(let ((payload (getf msg :payload)))
|
||||
(cond
|
||||
((eq (getf payload :action) :handshake)
|
||||
(enqueue-msg "* Connected to daemon *"))
|
||||
((and (eq (getf payload :sensor) :loop-error)
|
||||
(not (string= (or (getf payload :message) "") "Neural Cascade Failure: All providers exhausted.")))
|
||||
(enqueue-msg (format nil "ERROR: Daemon loop error (~a)"
|
||||
(getf payload :message))))
|
||||
(t
|
||||
(let ((text (or (getf payload :text) (format nil "~a" payload))))
|
||||
(enqueue-msg (format nil "⬇ ~a" text)))))))))
|
||||
(error (c)
|
||||
(when *is-running*
|
||||
(enqueue-msg (format nil "ERROR: Connection lost (~a)" c))
|
||||
(setf *is-running* nil))))))
|
||||
:name "opencortex-tui-reader"))
|
||||
#+end_src
|
||||
|
||||
** Main Entry Point
|
||||
#+begin_src lisp
|
||||
(defun main ()
|
||||
@@ -134,28 +167,38 @@ The OpenCortex TUI Client is a standalone Common Lisp application built on **Cro
|
||||
(error (e) (format t "Offline: ~a~%" e) (return-from main)))
|
||||
(setf *stream* (usocket:socket-stream *socket*))
|
||||
|
||||
;; Guard: Croatoan needs a real terminal (TERM env var, real TTY)
|
||||
(unless (uiop:getenv "TERM")
|
||||
(format t "TUI requires a terminal. Set TERM environment variable.~%")
|
||||
(format t "Or use: echo 'your message' | nc localhost 9105~%")
|
||||
(return-from main))
|
||||
|
||||
(unwind-protect
|
||||
(with-screen (scr :input-echoing nil :input-blocking nil :enable-colors t)
|
||||
(let* ((h (height scr)) (w (width scr)))
|
||||
(let ((chat-win (make-instance 'window :height (- h 5) :width (- w 2) :position '(1 1) :border t))
|
||||
(input-win (make-instance 'window :height 1 :width (- w 2) :position (list (- h 2) 1) :border t)))
|
||||
(setf (input-blocking input-win) nil)
|
||||
(loop :while *is-running* :do
|
||||
(let ((msgs (dequeue-msgs)))
|
||||
(when msgs
|
||||
(dolist (m msgs) (push m *chat-history*))
|
||||
(render-chat chat-win)))
|
||||
(let* ((ev (get-event input-win))
|
||||
(ch (when (and ev (typep ev 'event)) (event-key ev))))
|
||||
(when ch
|
||||
(cond
|
||||
((or (eq ch #\Newline) (eq ch #\Return)) (handle-return *stream*))
|
||||
((or (eq ch :backspace) (eq ch (code-char 127))) (handle-backspace))
|
||||
((characterp ch) (vector-push-extend ch *input-buffer*))))
|
||||
(clear input-win)
|
||||
(add-string input-win (format nil "▶ ~a" (coerce *input-buffer* 'string)) :y 0 :x 1)
|
||||
(refresh input-win))
|
||||
(sleep 0.02)))))
|
||||
(handler-case
|
||||
(with-screen (scr :input-echoing nil :input-blocking nil :enable-colors t)
|
||||
(let* ((h (height scr)) (w (width scr)))
|
||||
(let ((chat-win (make-instance 'window :height (- h 5) :width (- w 2) :position '(1 1) :border t))
|
||||
(input-win (make-instance 'window :height 1 :width (- w 2) :position (list (- h 2) 1) :border t)))
|
||||
(setf (input-blocking input-win) nil)
|
||||
(start-background-reader *stream*)
|
||||
(loop :while *is-running* :do
|
||||
(let ((msgs (dequeue-msgs)))
|
||||
(when msgs
|
||||
(dolist (m msgs) (push m *chat-history*))
|
||||
(render-chat chat-win)))
|
||||
(let* ((ev (get-event input-win))
|
||||
(ch (when (and ev (typep ev 'event)) (event-key ev))))
|
||||
(when ch
|
||||
(cond
|
||||
((or (eq ch #\Newline) (eq ch #\Return)) (handle-return *stream*))
|
||||
((or (eq ch :backspace) (eq ch (code-char 127))) (handle-backspace))
|
||||
((characterp ch) (vector-push-extend ch *input-buffer*))))
|
||||
(clear input-win)
|
||||
(add-string input-win (format nil "▶ ~a" (coerce *input-buffer* 'string)) :y 0 :x 1)
|
||||
(refresh input-win))
|
||||
(sleep 0.02)))))
|
||||
(error (c)
|
||||
(format t "TUI Error: ~a~%" c)))
|
||||
(setf *is-running* nil)
|
||||
(when *socket* (ignore-errors (usocket:socket-close *socket*)))))
|
||||
#+end_src
|
||||
|
||||
@@ -7,15 +7,15 @@
|
||||
:depends-on (:usocket :bordeaux-threads :dexador :uiop :cl-dotenv :cl-ppcre :hunchentoot :ironclad :str :cl-json :uuid)
|
||||
:serial t
|
||||
:components ((:file "harness/package")
|
||||
(:file "harness/skills")
|
||||
(:file "harness/communication")
|
||||
(:file "harness/communication-validator")
|
||||
(:file "harness/memory")
|
||||
(:file "harness/context")
|
||||
(:file "harness/perceive")
|
||||
(:file "harness/reason")
|
||||
(:file "harness/act")
|
||||
(:file "harness/loop")))
|
||||
(:file "harness/skills")
|
||||
(:file "harness/communication")
|
||||
(:file "harness/communication-validator")
|
||||
(:file "harness/memory")
|
||||
(:file "harness/context")
|
||||
(:file "harness/perceive")
|
||||
(:file "harness/reason")
|
||||
(:file "harness/act")
|
||||
(:file "harness/loop")))
|
||||
|
||||
(defsystem :opencortex/tests
|
||||
:depends-on (:opencortex :fiveam)
|
||||
@@ -26,9 +26,9 @@
|
||||
(:file "tests/pipeline-perceive-tests")
|
||||
(:file "tests/pipeline-reason-tests")
|
||||
(:file "tests/peripheral-vision-tests")
|
||||
(:file "tests/emacs-edit-tests")
|
||||
(:file "tests/utils-org-tests")
|
||||
(:file "tests/engineering-standards-tests")
|
||||
(:file "tests/lisp-utils-tests")
|
||||
(:file "tests/utils-lisp-tests")
|
||||
(:file "tests/literate-programming-tests")
|
||||
(:file "tests/self-edit-tests")
|
||||
(:file "tests/tool-permissions-tests")
|
||||
|
||||
@@ -17,11 +17,11 @@ while [ -h "$SOURCE" ]; do
|
||||
done
|
||||
export SCRIPT_DIR="$( cd -P "$( dirname "$SOURCE" )" && pwd )"
|
||||
|
||||
# XDG Defaults
|
||||
export OC_CONFIG_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/opencortex"
|
||||
export OC_DATA_DIR="${XDG_DATA_HOME:-$HOME/.local/share}/opencortex"
|
||||
export OC_STATE_DIR="${XDG_STATE_HOME:-$HOME/.local/state}/opencortex"
|
||||
export OC_BIN_DIR="${XDG_BIN_HOME:-$HOME/.local/bin}"
|
||||
# XDG Defaults (realpath ensures no unexpanded ~ in paths)
|
||||
export OC_CONFIG_DIR="$(realpath -m "${XDG_CONFIG_HOME:-$HOME/.config}/opencortex")"
|
||||
export OC_DATA_DIR="$(realpath -m "${XDG_DATA_HOME:-$HOME/.local/share}/opencortex")"
|
||||
export OC_STATE_DIR="$(realpath -m "${XDG_STATE_HOME:-$HOME/.local/state}/opencortex")"
|
||||
export OC_BIN_DIR="$(realpath -m "${XDG_BIN_HOME:-$HOME/.local/bin}")"
|
||||
|
||||
# Dynamic defaults for Skill Engine and Project Root
|
||||
export SKILLS_DIR="${SKILLS_DIR:-$OC_DATA_DIR/skills}"
|
||||
@@ -29,7 +29,9 @@ export MEMEX_DIR="${MEMEX_DIR:-$HOME/memex}"
|
||||
|
||||
# Load environment variables from the standard config location
|
||||
if [ -f "$OC_CONFIG_DIR/.env" ]; then
|
||||
set -a
|
||||
source "$OC_CONFIG_DIR/.env"
|
||||
set +a
|
||||
fi
|
||||
|
||||
# --- Dependency Checker ---
|
||||
@@ -69,7 +71,7 @@ setup_system() {
|
||||
|
||||
# Create standard directories
|
||||
mkdir -p "$OC_CONFIG_DIR" "$OC_DATA_DIR" "$OC_STATE_DIR" "$OC_BIN_DIR"
|
||||
mkdir -p "$OC_DATA_DIR/harness" "$OC_DATA_DIR/tests" "$OC_DATA_DIR/skills" "$OC_DATA_DIR/library"
|
||||
mkdir -p "$OC_DATA_DIR/harness" "$OC_DATA_DIR/tests" "$OC_DATA_DIR/skills"
|
||||
|
||||
echo -e "${YELLOW}--- Installing System Dependencies ---${NC}"
|
||||
if command_exists apt-get; then
|
||||
@@ -81,50 +83,63 @@ setup_system() {
|
||||
rm quicklisp.lisp
|
||||
fi
|
||||
|
||||
# Tangle the literate source from OC_DATA_DIR to OC_DATA_DIR (The Engine)
|
||||
# Tangle the literate source from the repo into XDG directories
|
||||
echo -e "${YELLOW}--- Deploying Engine to $OC_DATA_DIR ---${NC}"
|
||||
cp "$SCRIPT_DIR/opencortex.asd" "$OC_DATA_DIR/"
|
||||
cp "$SCRIPT_DIR/harness"/*.org "$OC_DATA_DIR/harness/"
|
||||
cp "$SCRIPT_DIR/skills"/*.org "$OC_DATA_DIR/skills/"
|
||||
|
||||
# Create tests directory before tangling (some org files write to tests/)
|
||||
mkdir -p "$OC_DATA_DIR/tests"
|
||||
mkdir -p "$OC_DATA_DIR/harness" "$OC_DATA_DIR/tests" "$OC_DATA_DIR/skills"
|
||||
|
||||
export INSTALL_DIR="$OC_DATA_DIR"
|
||||
|
||||
# Critical: Tangle manifest first to establish system structure (into root)
|
||||
# --- Harness files ---
|
||||
# Copy org files to harness/ so :tangle relative paths resolve to XDG
|
||||
cp "$SCRIPT_DIR/harness"/*.org "$OC_DATA_DIR/harness/"
|
||||
|
||||
# Critical: Tangle manifest first (into root)
|
||||
echo "Tangling harness/manifest.org..."
|
||||
(cd "$OC_DATA_DIR" && emacs -Q --batch --eval "(require 'org)" --eval "(setq org-confirm-babel-evaluate nil)" --eval "(org-babel-tangle-file \"harness/manifest.org\")") >/dev/null 2>&1 || true
|
||||
(cd "$OC_DATA_DIR/harness" && emacs -Q --batch \
|
||||
--eval "(require 'org)" \
|
||||
--eval "(setq org-confirm-babel-evaluate nil)" \
|
||||
--eval "(org-babel-tangle-file \"manifest.org\")") >/dev/null 2>&1 || true
|
||||
|
||||
# Tangle harness files into harness/
|
||||
for f in "$SCRIPT_DIR/harness"/*.org; do
|
||||
for f in "$OC_DATA_DIR/harness"/*.org; do
|
||||
fname=$(basename "$f" .org)
|
||||
if [ "$fname" != "manifest" ]; then
|
||||
echo "Tangling harness/$fname.org..."
|
||||
(cd "$OC_DATA_DIR/harness" && emacs -Q --batch --eval "(require 'org)" --eval "(setq org-confirm-babel-evaluate nil)" --eval "(org-babel-tangle-file \"${fname}.org\")") >/dev/null 2>&1 || true
|
||||
(cd "$OC_DATA_DIR/harness" && emacs -Q --batch \
|
||||
--eval "(require 'org)" \
|
||||
--eval "(setq org-confirm-babel-evaluate nil)" \
|
||||
--eval "(org-babel-tangle-file \"${fname}.org\")") >/dev/null 2>&1 || true
|
||||
fi
|
||||
done
|
||||
|
||||
# Tangle skill files into skills/
|
||||
# Move test files that landed in harness/ to tests/
|
||||
find "$OC_DATA_DIR/harness" -name "*-tests.lisp" -exec mv {} "$OC_DATA_DIR/tests/" \; 2>/dev/null || true
|
||||
|
||||
# Remove org files from harness/ (only .lisp should remain)
|
||||
rm -f "$OC_DATA_DIR/harness"/*.org
|
||||
|
||||
# --- Skill files ---
|
||||
for f in "$SCRIPT_DIR/skills"/*.org; do
|
||||
fname=$(basename "$f" .org)
|
||||
echo "Tangling skills/$fname.org..."
|
||||
# Copy org to XDG first (skills need to be loaded from XDG path)
|
||||
cp "$f" "$OC_DATA_DIR/skills/"
|
||||
(cd "$OC_DATA_DIR/skills" && emacs -Q --batch --eval "(require 'org)" --eval "(setq org-confirm-babel-evaluate nil)" --eval "(org-babel-tangle-file \"${fname}.org\")") >/dev/null 2>&1 || true
|
||||
sed "s|%%SKILLS_DIR%%|$OC_DATA_DIR/skills|g" "$f" > "/tmp/$fname.org"
|
||||
(cd "$OC_DATA_DIR/skills" && emacs -Q --batch \
|
||||
--eval "(require 'org)" \
|
||||
--eval "(setq org-confirm-babel-evaluate nil)" \
|
||||
--eval "(org-babel-tangle-file \"/tmp/$fname.org\")") >/dev/null 2>&1 || true
|
||||
done
|
||||
|
||||
# Special handling for tests that need to go into tests/
|
||||
# We'll just move them after tangling since many .org files tangle to both code and tests
|
||||
mkdir -p "$OC_DATA_DIR/tests"
|
||||
find "$OC_DATA_DIR/harness" "$OC_DATA_DIR/skills" -name "*-tests.lisp" -exec mv {} "$OC_DATA_DIR/tests/" \; 2>/dev/null || true
|
||||
# Move test files that landed in skills/ to tests/
|
||||
find "$OC_DATA_DIR/skills" -name "*-tests.lisp" -exec mv {} "$OC_DATA_DIR/tests/" \; 2>/dev/null || true
|
||||
rm -f /tmp/*.org
|
||||
|
||||
# Also move run-all-tests.lisp if it landed in the wrong place
|
||||
[ -f "$OC_DATA_DIR/run-all-tests.lisp" ] && mv "$OC_DATA_DIR/run-all-tests.lisp" "$OC_DATA_DIR/harness/"
|
||||
|
||||
# Cleanup: Remove .org files from XDG harness only (skills need .org for loader)
|
||||
echo "Cleaning up .org files from XDG harness..."
|
||||
rm -f "$OC_DATA_DIR/harness"/*.org
|
||||
# Cleanup: Remove .org files from XDG (we only want .lisp)
|
||||
echo "Cleaning up .org files from XDG..."
|
||||
rm -f "$OC_DATA_DIR/harness"/*.org "$OC_DATA_DIR/skills"/*.org /tmp/*.org
|
||||
|
||||
cd "$SCRIPT_DIR" # Create the bin shim
|
||||
echo -e "${YELLOW}--- Creating Bin Shim in $OC_BIN_DIR/opencortex ---${NC}"
|
||||
@@ -140,7 +155,9 @@ setup_system() {
|
||||
exec sbcl --non-interactive \
|
||||
--eval '(load (merge-pathnames "quicklisp/setup.lisp" (user-homedir-pathname)))' \
|
||||
--eval "(push (truename \"$OC_DATA_DIR/\") asdf:*central-registry*)" \
|
||||
--eval "(setf (uiop:getenv \"SKILLS_DIR\") \"$OC_DATA_DIR/skills\")" \
|
||||
--eval '(ql:quickload :opencortex)' \
|
||||
--eval "(setf (uiop:getenv \"SKILLS_DIR\") \"$OC_DATA_DIR/skills\")" \
|
||||
--eval '(opencortex:initialize-all-skills)' \
|
||||
--eval '(funcall (find-symbol "RUN-SETUP-WIZARD" :opencortex))'
|
||||
}
|
||||
@@ -156,7 +173,7 @@ doctor_repair() {
|
||||
# 2. Ensure XDG directories exist
|
||||
echo -e "${YELLOW}--- Fixing XDG Directories ---${NC}"
|
||||
mkdir -p "$OC_CONFIG_DIR" "$OC_DATA_DIR" "$OC_STATE_DIR" "$OC_BIN_DIR"
|
||||
mkdir -p "$OC_DATA_DIR/harness" "$OC_DATA_DIR/tests" "$OC_DATA_DIR/skills" "$OC_DATA_DIR/library"
|
||||
mkdir -p "$OC_DATA_DIR/harness" "$OC_DATA_DIR/tests" "$OC_DATA_DIR/skills"
|
||||
|
||||
# 3. Re-tangle harness files that may be broken
|
||||
echo -e "${YELLOW}--- Re-tangling Harness Files ---${NC}"
|
||||
@@ -183,8 +200,8 @@ doctor_repair() {
|
||||
if [ -f "$f" ]; then
|
||||
fname=$(basename "$f" .org)
|
||||
echo " Checking skill/$fname..."
|
||||
# Copy .org to XDG temporarily for tangle, then remove
|
||||
cp "$f" "$OC_DATA_DIR/skills/"
|
||||
# Replace %%SKILLS_DIR%% placeholder with temp file
|
||||
sed "s|%%SKILLS_DIR%%|$OC_DATA_DIR/skills|g" "$f" > "/tmp/$fname.org"
|
||||
if ! sbcl --non-interactive \
|
||||
--eval "(load \"$OC_DATA_DIR/skills/${fname}.lisp\")" \
|
||||
--eval "(format t \"OK~%\")" 2>/dev/null | grep -q "OK"; then
|
||||
@@ -192,9 +209,9 @@ doctor_repair() {
|
||||
(cd "$OC_DATA_DIR/skills" && emacs -Q --batch \
|
||||
--eval "(require 'org)" \
|
||||
--eval "(setq org-confirm-babel-evaluate nil)" \
|
||||
--eval "(org-babel-tangle-file \"$OC_DATA_DIR/skills/${fname}.org\")" >/dev/null 2>&1) || true
|
||||
--eval "(org-babel-tangle-file \"/tmp/${fname}.org\")" >/dev/null 2>&1) || true
|
||||
fi
|
||||
rm -f "$OC_DATA_DIR/skills/${fname}.org"
|
||||
rm -f "/tmp/$fname.org"
|
||||
fi
|
||||
done
|
||||
|
||||
@@ -215,7 +232,7 @@ case "$COMMAND" in
|
||||
PLATFORM=$1
|
||||
TOKEN=$2
|
||||
check_dependencies
|
||||
exec sbcl --non-interactive --eval '(load (merge-pathnames "quicklisp/setup.lisp" (user-homedir-pathname)))' --eval "(push (truename \"$OC_DATA_DIR/\") asdf:*central-registry*)" --eval '(ql:quickload :opencortex)' --eval '(opencortex:initialize-all-skills)' --eval "(funcall (find-symbol \"GATEWAY-MANAGER-MAIN\" :opencortex) \"$PLATFORM\" \"$TOKEN\")"
|
||||
exec sbcl --non-interactive --eval '(load (merge-pathnames "quicklisp/setup.lisp" (user-homedir-pathname)))' --eval "(push (truename \"$OC_DATA_DIR/\") asdf:*central-registry*)" --eval "(setf (uiop:getenv \"SKILLS_DIR\") \"$OC_DATA_DIR/skills\")" --eval '(ql:quickload :opencortex)' --eval '(opencortex:initialize-all-skills)' --eval "(funcall (find-symbol \"GATEWAY-MANAGER-MAIN\" :opencortex) \"$PLATFORM\" \"$TOKEN\")"
|
||||
;;
|
||||
|
||||
doctor)
|
||||
@@ -282,6 +299,7 @@ case "$COMMAND" in
|
||||
sbcl --non-interactive \
|
||||
--eval '(load (merge-pathnames "quicklisp/setup.lisp" (user-homedir-pathname)))' \
|
||||
--eval "(push (truename \"$OC_DATA_DIR/\") asdf:*central-registry*)" \
|
||||
--eval "(setf (uiop:getenv \"SKILLS_DIR\") \"$OC_DATA_DIR/skills\")" \
|
||||
--eval '(ql:quickload :opencortex)' \
|
||||
--eval '(opencortex:initialize-all-skills)' \
|
||||
--eval '(funcall (find-symbol "RUN-SETUP-WIZARD" :opencortex))'
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
(load (merge-pathnames "quicklisp/setup.lisp" (user-homedir-pathname)))
|
||||
|
||||
(let ((oc-dir (or (uiop:getenv "OC_DATA_DIR")
|
||||
(let ((oc-dir (or (uiop:getenv "OC_DATA_DIR")
|
||||
(namestring (truename "./")))))
|
||||
(push (uiop:ensure-directory-pathname oc-dir) asdf:*central-registry*))
|
||||
|
||||
(ql:quickload '(:opencortex :opencortex/tests) :silent t)
|
||||
(ql:quickload '(:fiveam :opencortex :opencortex/tui :opencortex/tests) :silent t)
|
||||
|
||||
(format t "~%=== Initializing Skills BEFORE loading tests ===~%")
|
||||
(format t "~%=== Initializing Skills BEFORE running tests ===~%")
|
||||
(opencortex:initialize-all-skills)
|
||||
|
||||
(format t "~%=== Running ALL Test Suites ===~%")
|
||||
@@ -19,6 +19,8 @@
|
||||
("OPENCORTEX-DIAGNOSTICS-TESTS" "DIAGNOSTICS-SUITE")
|
||||
("OPENCORTEX-GATEWAY-MANAGER-TESTS" "GATEWAY-SUITE")
|
||||
("OPENCORTEX-TUI-TESTS" "TUI-SUITE")
|
||||
("OPENCORTEX-UTILS-ORG-TESTS" "UTILS-ORG-SUITE")
|
||||
("OPENCORTEX-UTILS-LISP-TESTS" "UTILS-LISP-SUITE")
|
||||
("OPENCORTEX-LLM-GATEWAY-TESTS" "LLM-GATEWAY-SUITE")))
|
||||
(let ((pkg (find-package (first suite-spec))))
|
||||
(when pkg
|
||||
@@ -1,258 +0,0 @@
|
||||
(in-package :opencortex)
|
||||
|
||||
(defun bouncer-scan-secrets (text)
|
||||
"Scans TEXT for known secrets from the vault.
|
||||
|
||||
RETURNS: The name of the matched secret, or NIL if text is clean.
|
||||
|
||||
This prevents the catastrophic failure mode where the agent
|
||||
accidentally echoes an API key in its response or log output.
|
||||
|
||||
The check uses substring matching (not regex) for reliability.
|
||||
Only secrets longer than 5 characters are checked to avoid
|
||||
false positives on common words."
|
||||
|
||||
(when (and text (stringp text))
|
||||
|
||||
(let ((found-secret nil))
|
||||
|
||||
(maphash (lambda (key val)
|
||||
;; Only check secrets of meaningful length
|
||||
(when (and val (stringp val) (> (length val) 5))
|
||||
;; Search for secret value in action text
|
||||
(when (search val text)
|
||||
(setf found-secret key))))
|
||||
|
||||
opencortex::*vault-memory*)
|
||||
|
||||
found-secret)))
|
||||
|
||||
(defvar *bouncer-network-whitelist*
|
||||
'("api.telegram.org" "matrix.org" "googleapis.com" "openai.com" "anthropic.com")
|
||||
"Domains that the Bouncer considers safe for outbound connections.
|
||||
|
||||
This whitelist should be minimal—only services explicitly configured
|
||||
as gateways. All other outbound connections require approval.")
|
||||
|
||||
(defun bouncer-check-network-exfil (cmd)
|
||||
"Detects if CMD attempts to contact an unwhitelisted external host.
|
||||
|
||||
Returns T if the command targets an unknown external host.
|
||||
Returns NIL if the command is clean or only contacts whitelisted hosts.
|
||||
|
||||
The check looks for HTTP/HTTPS/FTP URLs and extracts the domain.
|
||||
If the domain isn't in *bouncer-network-whitelist*, it's flagged."
|
||||
|
||||
(when (and cmd (stringp cmd))
|
||||
|
||||
;; Look for URL patterns in the command
|
||||
(when (cl-ppcre:scan "(http|https|ftp)://([\\w\\.-]+)" cmd)
|
||||
|
||||
(multiple-value-bind (match regs)
|
||||
(cl-ppcre:scan-to-strings "(http|https|ftp)://([\\w\\.-]+)" cmd)
|
||||
|
||||
(declare (ignore match))
|
||||
|
||||
(let ((domain (aref regs 1)))
|
||||
|
||||
;; Check if domain is whitelisted
|
||||
(not (some (lambda (safe) (search safe domain))
|
||||
*bouncer-network-whitelist*)))))))
|
||||
|
||||
(defun bouncer-check (action context)
|
||||
"The 5-Vector security gate for high-risk actions.
|
||||
|
||||
Evaluates an action against all security vectors and either:
|
||||
- Returns the action unchanged (pass)
|
||||
- Returns a blocking LOG event (hard block)
|
||||
- Returns an approval-required EVENT (soft block)
|
||||
|
||||
Vector evaluation order:
|
||||
1. Already approved actions pass immediately
|
||||
2. Secret exposure → hard block
|
||||
3. Network exfiltration → approval required
|
||||
4. High-impact targets → approval required
|
||||
|
||||
The context parameter is not used directly but provided for
|
||||
consistency with the skill gate signature."
|
||||
|
||||
(declare (ignore context))
|
||||
|
||||
(let* ((target (getf action :target))
|
||||
(payload (getf action :payload))
|
||||
(text (or (getf payload :text) (getf action :text)))
|
||||
;; Extract cmd from direct shell or tool-mediated shell call
|
||||
(cmd (or (getf payload :cmd)
|
||||
(when (and (eq target :tool)
|
||||
(equal (getf payload :tool) "shell"))
|
||||
(getf (getf payload :args) :cmd))))
|
||||
(approved (getf action :approved)))
|
||||
|
||||
(cond
|
||||
|
||||
;; Vector 0: Already approved actions pass through
|
||||
(approved
|
||||
action)
|
||||
|
||||
;; Vector 1: Secret Exposure (Hard Block)
|
||||
;; If any vault secret is found in the action text, block immediately
|
||||
((and text (bouncer-scan-secrets text))
|
||||
(let ((secret-name (bouncer-scan-secrets text)))
|
||||
(harness-log "SECURITY VIOLATION: Blocked potential leak of secret '~a'" secret-name)
|
||||
(list :type :LOG
|
||||
:payload (list :level :error
|
||||
:text (format nil "Action blocked: Potential exposure of '~a'" secret-name)))))
|
||||
|
||||
;; Vector 2: Network Exfiltration (Soft Block)
|
||||
;; Shell commands targeting unknown hosts require approval
|
||||
((and (or (eq target :shell)
|
||||
(and (eq target :tool)
|
||||
(equal (getf payload :tool) "shell")))
|
||||
(bouncer-check-network-exfil cmd))
|
||||
|
||||
(harness-log "SECURITY WARNING: External network call detected. Queuing for approval.")
|
||||
|
||||
(list :type :EVENT
|
||||
:payload (list :sensor :approval-required
|
||||
:action action)))
|
||||
|
||||
;; Vector 3: High-Impact Targets (Soft Block)
|
||||
;; Shell execution, file repair, and eval require approval
|
||||
((or (member target '(:shell))
|
||||
(and (eq target :tool)
|
||||
(member (getf payload :tool) '("shell" "repair-file") :test #'string=))
|
||||
(and (eq target :emacs)
|
||||
(eq (getf payload :action) :eval)))
|
||||
|
||||
(harness-log "SECURITY: High-impact action requires approval: ~a"
|
||||
(or (getf payload :tool) target))
|
||||
|
||||
(list :type :EVENT
|
||||
:payload (list :sensor :approval-required
|
||||
:action action)))
|
||||
|
||||
;; Vector 4: Default pass
|
||||
(t
|
||||
action))))
|
||||
|
||||
(defun bouncer-process-approvals ()
|
||||
"Scans the object store for APPROVED flight plans and re-injects them.
|
||||
|
||||
This function is called on every heartbeat, allowing the agent to
|
||||
check for approvals without blocking the main signal pipeline.
|
||||
|
||||
Flight Plan format:
|
||||
- Has TAGS including \"FLIGHT_PLAN\"
|
||||
- Has TODO set to \"APPROVED\"
|
||||
- Has ACTION containing the serialized action plist
|
||||
|
||||
When an approved flight plan is found:
|
||||
1. Deserialize the action from the ACTION attribute
|
||||
2. Mark the action as :approved = t (bypasses security gate)
|
||||
3. Re-inject into the signal pipeline
|
||||
4. Mark the flight plan as DONE
|
||||
|
||||
Returns T if any flight plans were processed."
|
||||
|
||||
(let ((approved-nodes (list-objects-with-attribute :TODO "APPROVED"))
|
||||
(found-any nil))
|
||||
|
||||
(dolist (node approved-nodes)
|
||||
|
||||
(let* ((tags (getf (org-object-attributes node) :TAGS))
|
||||
(action-str (getf (org-object-attributes node) :ACTION)))
|
||||
|
||||
;; Only process flight plans (not other APPROVED items)
|
||||
(when (and (member "FLIGHT_PLAN" tags :test #'string-equal)
|
||||
action-str)
|
||||
|
||||
(harness-log "BOUNCER: Found approved flight plan '~a'. Re-injecting..."
|
||||
(org-object-id node))
|
||||
|
||||
(let ((action (ignore-errors (read-from-string action-str))))
|
||||
(when action
|
||||
|
||||
;; Mark as approved to bypass the security gate on re-injection
|
||||
(setf (getf action :approved) t)
|
||||
|
||||
;; Re-inject the action into the signal pipeline
|
||||
(inject-stimulus action)
|
||||
|
||||
;; Mark the flight plan as done
|
||||
(setf (getf (org-object-attributes node) :TODO) "DONE")
|
||||
|
||||
(setq found-any t))))))
|
||||
|
||||
found-any))
|
||||
|
||||
(defun bouncer-create-flight-plan (blocked-action)
|
||||
"Creates an Org node representing a pending flight plan for manual approval.
|
||||
|
||||
BLOCKED-ACTION is the action plist that was intercepted.
|
||||
|
||||
The flight plan node contains:
|
||||
- A title describing the action
|
||||
- TODO set to PLAN (awaiting approval)
|
||||
- TAGS including FLIGHT_PLAN
|
||||
- ACTION attribute containing the serialized action
|
||||
|
||||
The user reviews the flight plan and changes TODO to APPROVED.
|
||||
On the next heartbeat, bouncer-process-approvals will detect
|
||||
the approval and re-inject the action.
|
||||
|
||||
Returns the generated org-id for the flight plan."
|
||||
|
||||
(let ((id (org-id-new)))
|
||||
(harness-log "BOUNCER: Creating flight plan node '~a'..." id)
|
||||
|
||||
;; Inject a node creation request
|
||||
(list :type :REQUEST
|
||||
:target :emacs
|
||||
:payload (list :action :insert-node
|
||||
:id id
|
||||
:attributes (list
|
||||
:TITLE "Flight Plan: High-Risk Action"
|
||||
:TODO "PLAN"
|
||||
:TAGS '("FLIGHT_PLAN")
|
||||
:ACTION (format nil "~s" blocked-action))))))
|
||||
|
||||
(defun bouncer-deterministic-gate (action context)
|
||||
"Main deterministic gate for the Bouncer skill.
|
||||
|
||||
Handles three types of signals:
|
||||
1. :approval-required - Create a flight plan for the blocked action
|
||||
2. :heartbeat - Process any pending approvals
|
||||
3. otherwise - Run security check on the action
|
||||
|
||||
The trigger is always true (bouncer evaluates all actions)
|
||||
because security cannot be selective."
|
||||
|
||||
(let* ((payload (getf context :payload))
|
||||
(sensor (getf payload :sensor)))
|
||||
|
||||
(case sensor
|
||||
|
||||
;; Signal type 1: Action was blocked, create flight plan
|
||||
(:approval-required
|
||||
(let* ((blocked-action (getf payload :action)))
|
||||
(bouncer-create-flight-plan blocked-action)))
|
||||
|
||||
;; Signal type 2: Heartbeat, check for approvals
|
||||
(:heartbeat
|
||||
(bouncer-process-approvals)
|
||||
;; After processing approvals, still run the security check
|
||||
(if action
|
||||
(bouncer-check action context)
|
||||
action))
|
||||
|
||||
;; Signal type 3: Normal action, run security check
|
||||
(otherwise
|
||||
(if action
|
||||
(bouncer-check action context)
|
||||
action)))))
|
||||
|
||||
(defskill :skill-bouncer
|
||||
:priority 150
|
||||
:trigger (lambda (ctx) (declare (ignore ctx)) t)
|
||||
:probabilistic nil
|
||||
:deterministic #'bouncer-deterministic-gate)
|
||||
@@ -1,18 +1,13 @@
|
||||
#+TITLE: SKILL: Bouncer (org-skill-bouncer.org)
|
||||
#+AUTHOR: Agent
|
||||
#+FILETAGS: :system:bouncer:authorization:autonomy:
|
||||
#+PROPERTY: header-args:lisp :tangle org-skill-bouncer.lisp
|
||||
#+PROPERTY: header-args:lisp :tangle %%SKILLS_DIR%%/org-skill-bouncer.lisp
|
||||
|
||||
* Overview
|
||||
The *Bouncer Skill* is the physical security layer of OpenCortex. It enforces operational security checks on all proposed actions.
|
||||
|
||||
* Implementation
|
||||
|
||||
** Package Context
|
||||
#+begin_src lisp
|
||||
(in-package :opencortex)
|
||||
#+end_src
|
||||
|
||||
** Security Configuration
|
||||
#+begin_src lisp
|
||||
(defvar *bouncer-network-whitelist*
|
||||
@@ -56,34 +51,30 @@ The *Bouncer Skill* is the physical security layer of OpenCortex. It enforces op
|
||||
(let* ((target (proto-get action :target))
|
||||
(payload (proto-get action :payload))
|
||||
(text (or (proto-get payload :text) (proto-get action :text)))
|
||||
(cmd (or (proto-get payload :cmd)
|
||||
(cmd (or (proto-get payload :cmd)
|
||||
(when (and (eq target :tool) (equal (proto-get payload :tool) "shell"))
|
||||
(proto-get (proto-get payload :args) :cmd)))))
|
||||
(proto-get (proto-get payload :args) :cmd))))
|
||||
(approved (proto-get action :approved)))
|
||||
|
||||
(cond
|
||||
(approved action)
|
||||
|
||||
((and text (bouncer-scan-secrets text))
|
||||
(let ((secret-name (bouncer-scan-secrets text)))
|
||||
(harness-log "SECURITY VIOLATION: Blocked potential leak of secret '~a'" secret-name)
|
||||
(list :type :LOG
|
||||
:payload (list :level :error
|
||||
:text (format nil "Action blocked: Potential exposure of '~a'" secret-name)))))
|
||||
|
||||
((and (or (eq target :shell)
|
||||
((and (or (eq target :shell)
|
||||
(and (eq target :tool) (equal (proto-get payload :tool) "shell")))
|
||||
(bouncer-check-network-exfil cmd))
|
||||
(harness-log "SECURITY WARNING: External network call detected. Queuing for approval."))
|
||||
(list :type :EVENT :payload (list :sensor :approval-required :action action)))
|
||||
|
||||
((or (member target '(:shell))
|
||||
(harness-log "SECURITY WARNING: External network call detected. Queuing for approval.")
|
||||
(list :type :EVENT :payload (list :sensor :approval-required :action action)))
|
||||
((or (member target '(:shell))
|
||||
(and (eq target :tool) (member (proto-get payload :tool) '("shell" "repair-file") :test #'string=))
|
||||
(and (eq target :emacs) (eq (proto-get payload :action) :eval))))
|
||||
(and (eq target :emacs) (eq (proto-get payload :action) :eval)))
|
||||
(harness-log "SECURITY: High-impact action requires approval: ~a" (or (proto-get payload :tool) target))
|
||||
(list :type :EVENT :payload (list :sensor :approval-required :action action)))
|
||||
|
||||
(t action))))
|
||||
|
||||
#+end_src
|
||||
|
||||
** Approval Processing (bouncer-process-approvals)
|
||||
@@ -115,9 +106,9 @@ The *Bouncer Skill* is the physical security layer of OpenCortex. It enforces op
|
||||
(harness-log "BOUNCER: Creating flight plan node '~a'..." id)
|
||||
(list :type :REQUEST :target :emacs
|
||||
:payload (list :action :insert-node :id id
|
||||
:attributes (list :TITLE "Flight Plan: High-Risk Action"
|
||||
:TODO "PLAN" :TAGS '("FLIGHT_PLAN")
|
||||
:ACTION (format nil "~s" blocked-action))))))
|
||||
:attributes (list :TITLE "Flight Plan: High-Risk Action"
|
||||
:TODO "PLAN" :TAGS '("FLIGHT_PLAN")
|
||||
:ACTION (format nil "~s" blocked-action))))))
|
||||
#+end_src
|
||||
|
||||
** Gate Logic (bouncer-deterministic-gate)
|
||||
|
||||
@@ -1,83 +0,0 @@
|
||||
(in-package :opencortex)
|
||||
|
||||
(defvar *cli-port* 9105)
|
||||
(defvar *cli-server-socket* nil)
|
||||
(defvar *cli-server-thread* nil)
|
||||
|
||||
(defun execute-cli-action (action context)
|
||||
"Sends a framed message back to the connected CLI client."
|
||||
(let* ((payload (proto-get action :PAYLOAD))
|
||||
(meta (getf context :meta))
|
||||
(stream (getf meta :reply-stream)))
|
||||
(handler-case
|
||||
(if (and stream (open-stream-p stream))
|
||||
(progn
|
||||
(format stream "~a" (frame-message action))
|
||||
(finish-output stream)
|
||||
(format stream "~a" (frame-message '(:TYPE :STATUS :SCRIBE :IDLE :GARDENER :SLEEPING)))
|
||||
(finish-output stream))
|
||||
(harness-log "CLI ERROR: No active or open reply stream for signal."))
|
||||
(error (c) (harness-log "CLI ACTUATOR ERROR: ~a" c)))))
|
||||
|
||||
(defun handle-cli-slash-command (cmd stream)
|
||||
(cond
|
||||
((string= cmd "/exit") (return-from handle-cli-slash-command :exit))
|
||||
(t (format stream "~a" (frame-message (list :TYPE :REQUEST :PAYLOAD (list :ACTION :MESSAGE :TEXT (format nil "Unknown command: ~a" cmd))))))))
|
||||
|
||||
(defun handle-cli-client (stream)
|
||||
"Reads framed messages from a CLI client and injects them as stimuli."
|
||||
(harness-log "CLI: Client connected.")
|
||||
(handler-case
|
||||
(progn
|
||||
;; 1. Send Handshake
|
||||
(format stream "~a" (frame-message (make-hello-message "0.1.0")))
|
||||
(finish-output stream)
|
||||
(format stream "~a" (frame-message '(:TYPE :STATUS :SCRIBE :IDLE :GARDENER :SLEEPING)))
|
||||
(finish-output stream)
|
||||
|
||||
;; 2. Communication Loop
|
||||
(loop
|
||||
(let ((msg (read-framed-message stream)))
|
||||
(cond ((eq msg :eof) (return))
|
||||
((eq msg :error) (return))
|
||||
(t (let* ((payload (proto-get msg :payload))
|
||||
(text (proto-get payload :text))
|
||||
(meta (proto-get msg :meta)))
|
||||
(if (and text (stringp text) (char= (char text 0) #\/))
|
||||
(when (eq (handle-cli-slash-command text stream) :exit) (return))
|
||||
(progn
|
||||
;; Default meta if missing
|
||||
(unless meta
|
||||
(setf (getf msg :meta) (list :SOURCE :CLI :SESSION-ID "default")))
|
||||
(harness-log "CLI: Received input -> ~s" msg)
|
||||
(inject-stimulus msg :stream stream)))))))))
|
||||
(error (c) (harness-log "CLI CLIENT DISCONNECT: ~a" c)))
|
||||
(harness-log "CLI: Client disconnected."))
|
||||
|
||||
(defun start-cli-gateway (&optional (port *cli-port*))
|
||||
"Starts the TCP listener for local CLI clients."
|
||||
(setf *cli-server-socket* (usocket:socket-listen "0.0.0.0" port :reuse-address t))
|
||||
(setf *cli-server-thread*
|
||||
(bt:make-thread
|
||||
(lambda ()
|
||||
(unwind-protect
|
||||
(loop
|
||||
(let* ((socket (usocket:socket-accept *cli-server-socket*))
|
||||
(stream (usocket:socket-stream socket)))
|
||||
(bt:make-thread (lambda ()
|
||||
(unwind-protect (handle-cli-client stream)
|
||||
(usocket:socket-close socket)))
|
||||
:name "opencortex-cli-client-handler")))
|
||||
(usocket:socket-close *cli-server-socket*)))
|
||||
:name "opencortex-cli-gateway"))
|
||||
(harness-log "CLI: Gateway listening on port ~a" port))
|
||||
|
||||
(register-actuator :CLI #'execute-cli-action)
|
||||
|
||||
(defskill :skill-gateway-cli
|
||||
:priority 200
|
||||
:trigger (lambda (ctx) (declare (ignore ctx)) nil)
|
||||
:probabilistic nil
|
||||
:deterministic (lambda (action ctx) (declare (ignore ctx)) action))
|
||||
|
||||
(start-cli-gateway)
|
||||
@@ -1,18 +1,13 @@
|
||||
#+TITLE: SKILL: CLI Gateway (org-skill-cli-gateway.org)
|
||||
#+AUTHOR: Agent
|
||||
#+FILETAGS: :skill:gateway:cli:
|
||||
#+PROPERTY: header-args:lisp :tangle org-skill-cli-gateway.lisp
|
||||
#+PROPERTY: header-args:lisp :tangle %%SKILLS_DIR%%/org-skill-cli-gateway.lisp
|
||||
|
||||
* Overview
|
||||
The *CLI Gateway* provides a command-line interface for interacting with the OpenCortex daemon.
|
||||
|
||||
* Implementation
|
||||
|
||||
** Package Context
|
||||
#+begin_src lisp
|
||||
(in-package :opencortex)
|
||||
#+end_src
|
||||
|
||||
** CLI Command Handling
|
||||
#+begin_src lisp
|
||||
(defun cli-process-input (text)
|
||||
|
||||
@@ -1,96 +0,0 @@
|
||||
(in-package :opencortex)
|
||||
|
||||
(defparameter *skill-config-manager*
|
||||
'(:name "config-manager"
|
||||
:description "Manages system settings and LLM provider configurations."
|
||||
:capabilities (:configure-provider :run-setup-wizard)
|
||||
:type :deterministic)
|
||||
"Skill metadata for the Config Manager.")
|
||||
|
||||
(defvar *provider-templates*
|
||||
'((:ollama . (:name "Ollama (Local)" :fields ((:url :label "URL") (:model :label "Model")) :default-url "http://localhost:11434" :default-model "llama3"))
|
||||
(:openrouter . (:name "OpenRouter" :fields ((:key :label "API Key" :secret t) (:model :label "Model")) :default-model "anthropic/claude-3-opus-20240229"))
|
||||
(:openai . (:name "OpenAI" :fields ((:key :label "API Key" :secret t) (:model :label "Model")) :default-model "gpt-4-turbo"))
|
||||
(:groq . (:name "Groq" :fields ((:key :label "API Key" :secret t) (:model :label "Model")) :default-model "mixtral-8x7b-32768"))
|
||||
(:gemini . (:name "Google Gemini" :fields ((:key :label "API Key" :secret t) (:model :label "Model")) :default-model "gemini-1.5-pro"))
|
||||
(:anthropic . (:name "Anthropic" :fields ((:key :label "API Key" :secret t) (:model :label "Model")) :default-model "claude-3-5-sonnet-20240620")))
|
||||
"Templates for supported LLM providers.")
|
||||
|
||||
(defvar *providers* nil "Global registry of configured LLM providers.")
|
||||
|
||||
(defun get-oc-config-dir ()
|
||||
"Returns the XDG-compliant config directory for OpenCortex."
|
||||
(let ((env (uiop:getenv "OC_CONFIG_DIR")))
|
||||
(if (and env (> (length env) 0))
|
||||
(uiop:ensure-directory-pathname env)
|
||||
(uiop:merge-pathnames* ".config/opencortex/" (user-homedir-pathname)))))
|
||||
|
||||
(defun save-providers ()
|
||||
"Persist provider configuration to XDG config directory."
|
||||
(let ((path (merge-pathnames "providers.lisp" (get-oc-config-dir))))
|
||||
(ensure-directories-exist path)
|
||||
(with-open-file (s path :direction :output :if-exists :supersede)
|
||||
(format s ";;; OpenCortex Provider Metadata~%~s~%" *providers*))))
|
||||
|
||||
(defun prompt-for (label &optional default)
|
||||
"Prompts the user for input on the CLI."
|
||||
(format t "~a~@[ [~a]~]: " label default)
|
||||
(finish-output)
|
||||
(let ((input (read-line)))
|
||||
(if (string= input "")
|
||||
(or default "")
|
||||
input)))
|
||||
|
||||
(defun save-secret (provider field val)
|
||||
"Appends a secret to the XDG .env file."
|
||||
(let ((env-file (merge-pathnames ".env" (get-oc-config-dir)))
|
||||
(var-name (format nil "~:@(~a_~a~)" provider field)))
|
||||
(ensure-directories-exist env-file)
|
||||
(with-open-file (out env-file :direction :output :if-exists :append :if-does-not-exist :create)
|
||||
(format out "~a=~a~%" var-name val))
|
||||
(setf (uiop:getenv var-name) val)))
|
||||
|
||||
(defun register-provider (id config)
|
||||
"Update the global provider registry."
|
||||
(setf (getf *providers* id) config))
|
||||
|
||||
(defun configure-provider (id)
|
||||
"Guided configuration for a specific LLM provider template."
|
||||
(let* ((template (cdr (assoc id *provider-templates*)))
|
||||
(fields (getf template :fields))
|
||||
(config nil))
|
||||
(format t "~%--- Configuring ~a ---~%" (getf template :name))
|
||||
(dolist (field-spec fields)
|
||||
(let* ((field (first field-spec))
|
||||
(label (getf (rest field-spec) :label))
|
||||
(is-secret (getf (rest field-spec) :secret))
|
||||
(default-key (intern (format nil "DEFAULT-~a" field) :keyword))
|
||||
(default (getf template default-key))
|
||||
(val (prompt-for label default)))
|
||||
(if is-secret
|
||||
(save-secret id field val)
|
||||
(setf (getf config field) val))))
|
||||
(register-provider id config)
|
||||
(format t "✓ ~a metadata registered.~%" (getf template :name))))
|
||||
|
||||
(defun run-setup-wizard ()
|
||||
"Entry point for the interactive OpenCortex Lisp Setup Wizard."
|
||||
(format t "=== OpenCortex: Advanced Setup Wizard ===~%")
|
||||
(let ((user (prompt-for "Your Name" "User"))
|
||||
(agent (prompt-for "Agent Name" "OpenCortex")))
|
||||
(format t "Welcome, ~a. I am ~a.~%" user agent))
|
||||
(format t "~%Available Providers:~%")
|
||||
(loop for (id . data) in *provider-templates* do (format t " ~a: ~a~%" id (getf data :name)))
|
||||
(format t "~%Enter provider IDs to configure (comma separated, or 'all'): ")
|
||||
(finish-output)
|
||||
(let* ((input (read-line))
|
||||
(ids (if (string= input "all")
|
||||
(mapcar #'car *provider-templates*)
|
||||
(mapcar (lambda (s) (intern (string-upcase (string-trim " " s)) :keyword))
|
||||
(uiop:split-string input :separator ",")))))
|
||||
(dolist (id ids)
|
||||
(when (assoc id *provider-templates*)
|
||||
(configure-provider id))))
|
||||
(save-providers)
|
||||
(format t "~%Setup complete. Running diagnostics...~%")
|
||||
(doctor-run-all))
|
||||
@@ -1,18 +1,13 @@
|
||||
#+TITLE: SKILL: Config Manager (org-skill-config-manager.org)
|
||||
#+AUTHOR: Agent
|
||||
#+FILETAGS: :skill:setup:config:
|
||||
#+PROPERTY: header-args:lisp :tangle org-skill-config-manager.lisp
|
||||
#+PROPERTY: header-args:lisp :tangle %%SKILLS_DIR%%/org-skill-config-manager.lisp
|
||||
|
||||
* Overview
|
||||
The *Config Manager* skill provides the OpenCortex Agent with the capability to manage its own environment variables and provider configurations. It includes an interactive setup wizard for LLM providers, gateways, and system settings.
|
||||
|
||||
* Implementation
|
||||
|
||||
** Package Context
|
||||
#+begin_src lisp
|
||||
(in-package :opencortex)
|
||||
#+end_src
|
||||
|
||||
** Configuration Paths
|
||||
#+begin_src lisp
|
||||
(defun get-oc-config-dir ()
|
||||
@@ -74,7 +69,7 @@ The *Config Manager* skill provides the OpenCortex Agent with the capability to
|
||||
(let ((existing (assoc key config :test #'string=)))
|
||||
(if existing
|
||||
(setf (cdr existing) value)
|
||||
(push pair config)))
|
||||
(push pair config))
|
||||
(write-config-file config))))
|
||||
#+end_src
|
||||
|
||||
@@ -109,7 +104,7 @@ The *Config Manager* skill provides the OpenCortex Agent with the capability to
|
||||
|
||||
** LLM Provider Setup
|
||||
#+begin_src lisp
|
||||
(defvar *available-providers*
|
||||
(defparameter *available-providers*
|
||||
'(("OpenAI" . "OPENAI_API_KEY")
|
||||
("Anthropic" . "ANTHROPIC_API_KEY")
|
||||
("OpenRouter" . "OPENROUTER_API_KEY")
|
||||
@@ -138,18 +133,18 @@ The *Config Manager* skill provides the OpenCortex Agent with the capability to
|
||||
(when (prompt-yes-no "Configure a new provider?")
|
||||
(let ((chosen (prompt-choice "Select provider:" (mapcar #'car *available-providers*))))
|
||||
(when chosen
|
||||
(let ((env-key (cdr (assoc chosen *available-providers* :test #'string= :key #'car))))
|
||||
(let ((env-key (cdr (assoc chosen *available-providers* :test #'string=))))
|
||||
(if (string= chosen "Ollama (local)")
|
||||
(progn
|
||||
(format t "Enter Ollama URL (e.g., http://localhost:11434): ")
|
||||
(let ((url (read-line)))
|
||||
(set-config-value env-key url)
|
||||
(format t "✓ Ollama configured at ~a~%" url))))
|
||||
(format t "✓ Ollama configured at ~a~%" url)))
|
||||
(progn
|
||||
(format t "Enter API key for ~a: " chosen)
|
||||
(let ((key (read-line)))
|
||||
(set-config-value env-key key)
|
||||
(format t "✓ ~a API key saved~%" chosen)))))))))))
|
||||
(format t "✓ ~a API key saved~%" chosen)))))))))
|
||||
|
||||
(format t "~%"))
|
||||
|
||||
@@ -179,7 +174,7 @@ The *Config Manager* skill provides the OpenCortex Agent with the capability to
|
||||
(if (string= chosen "Slack")
|
||||
(set-config-value "SLACK_TOKEN" token)
|
||||
(set-config-value "DISCORD_TOKEN" token))
|
||||
(format t "✓ ~a gateway configured~%" chosen))))))
|
||||
(format t "✓ ~a gateway configured~%" chosen)))))
|
||||
|
||||
(format t "~%"))
|
||||
#+end_src
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
(defun vault-get-secret (provider &key type)
|
||||
"Retrieves a secret (api-key or session) for a provider.")
|
||||
|
||||
(defun vault-set-secret (provider secret &key type)
|
||||
"Securely stores a secret and triggers a Merkle snapshot.")
|
||||
|
||||
|
||||
|
||||
(defvar opencortex::*vault-memory* (make-hash-table :test 'equal)
|
||||
"In-memory cache of sensitive credentials.")
|
||||
|
||||
(defun vault-mask-string (str)
|
||||
"Returns a masked version of a sensitive string."
|
||||
(if (and str (> (length str) 8))
|
||||
(format nil "~a...~a" (subseq str 0 4) (subseq str (- (length str) 4)))
|
||||
"[REDACTED]"))
|
||||
|
||||
(defun vault-get-secret (provider &key (type :api-key))
|
||||
"Retrieves a credential. Type can be :api-key or :session."
|
||||
(let* ((key (format nil "~a-~a" provider type))
|
||||
(val (gethash key opencortex::*vault-memory*)))
|
||||
(if val
|
||||
val
|
||||
;; Fallback to environment
|
||||
(let ((env-var (case provider
|
||||
((:gemini :gemini-api) "GEMINI_API_KEY")
|
||||
(:openai "OPENAI_API_KEY")
|
||||
(:anthropic "ANTHROPIC_API_KEY")
|
||||
(:groq "GROQ_API_KEY")
|
||||
(:openrouter "OPENROUTER_API_KEY")
|
||||
(:telegram "TELEGRAM_BOT_TOKEN")
|
||||
(:signal "SIGNAL_ACCOUNT_NUMBER")
|
||||
(:matrix-homeserver "MATRIX_HOMESERVER")
|
||||
(:matrix-token "MATRIX_ACCESS_TOKEN")
|
||||
(t nil))))
|
||||
(when (and env-var (eq type :api-key))
|
||||
(uiop:getenv env-var))))))
|
||||
|
||||
(defun vault-set-secret (provider secret &key (type :api-key))
|
||||
"Securely stores a secret and triggers a Merkle snapshot."
|
||||
(let ((key (format nil "~a-~a" provider type)))
|
||||
(setf (gethash key opencortex::*vault-memory*) secret)
|
||||
(harness-log "VAULT - Updated ~a for ~a. Triggering Merkle snapshot..." type provider)
|
||||
(snapshot-memory)
|
||||
t))
|
||||
|
||||
(defun vault-onboard-gemini-web ()
|
||||
"Instructions for the Autonomous Cookie Handshake."
|
||||
(harness-log "--- GEMINI WEB ONBOARDING ---")
|
||||
(harness-log "1. Visit gemini.google.com")
|
||||
(harness-log "2. Run the 'Get Gemini Cookies' Bookmarklet.")
|
||||
(harness-log " CODE: javascript:(function(){const c=document.cookie.split('; ').reduce((r,v)=>{const [n,val]=v.split('=');r[n]=val;return r},{});const target=['__Secure-1PSID','__Secure-1PSIDTS'];const out=target.map(n=>({name:n,value:c[n]}));prompt('Copy JSON:',JSON.stringify(out));})();")
|
||||
(harness-log "PLATFORM GUIDE: Chrome/Firefox/Safari all support Bookmarklets via 'Add Page' or 'New Bookmark'.")
|
||||
t)
|
||||
|
||||
(progn
|
||||
(defskill :skill-credentials-vault
|
||||
:priority 200 ; High priority, foundational
|
||||
:trigger (lambda (ctx) (eq (getf (getf ctx :payload) :sensor) :onboarding-request))
|
||||
:probabilistic nil
|
||||
:deterministic (lambda (action ctx)
|
||||
(vault-onboard-gemini-web)
|
||||
action)))
|
||||
@@ -1,18 +1,13 @@
|
||||
#+TITLE: SKILL: Credentials Vault (org-skill-credentials-vault.org)
|
||||
#+AUTHOR: Agent
|
||||
#+FILETAGS: :system:security:vault:
|
||||
#+PROPERTY: header-args:lisp :tangle org-skill-credentials-vault.lisp
|
||||
#+PROPERTY: header-args:lisp :tangle %%SKILLS_DIR%%/org-skill-credentials-vault.lisp
|
||||
|
||||
* Overview
|
||||
The *Credentials Vault* provides secure in-memory storage for sensitive API keys and session tokens.
|
||||
|
||||
* Implementation
|
||||
|
||||
** Package Context
|
||||
#+begin_src lisp
|
||||
(in-package :opencortex)
|
||||
#+end_src
|
||||
|
||||
** Vault Storage
|
||||
#+begin_src lisp
|
||||
(defvar *vault-memory* (make-hash-table :test 'equal)
|
||||
|
||||
@@ -1,87 +0,0 @@
|
||||
(in-package :opencortex)
|
||||
|
||||
(defparameter *skill-diagnostics*
|
||||
'(:name "diagnostics"
|
||||
:description "Performs system health checks and environment validation."
|
||||
:capabilities (:run-diagnostics)
|
||||
:type :deterministic)
|
||||
"Skill metadata for the Diagnostics component.")
|
||||
|
||||
(defvar *doctor-required-binaries* '("sbcl" "emacs" "git" "socat" "nc")
|
||||
"List of external binaries required for full system operation.")
|
||||
|
||||
(defun doctor-check-dependencies ()
|
||||
"Verifies that required external binaries are available in the PATH via a shell probe."
|
||||
(let ((all-ok t))
|
||||
(harness-log "DOCTOR: Checking system dependencies...")
|
||||
(dolist (dep *doctor-required-binaries*)
|
||||
(let ((path (ignore-errors
|
||||
(uiop:run-program (list "which" dep)
|
||||
:output :string :ignore-error-status t))))
|
||||
(if (and path (> (length path) 0))
|
||||
(harness-log " [OK] Found ~a" dep)
|
||||
(progn
|
||||
(harness-log " [FAIL] Missing binary: ~a" dep)
|
||||
(setf all-ok nil)))))
|
||||
all-ok))
|
||||
|
||||
(defun doctor-check-env ()
|
||||
"Validates XDG directories and environment configuration against the POSIX standard."
|
||||
(harness-log "DOCTOR: Checking XDG environment...")
|
||||
(let ((all-ok t)
|
||||
(config-dir (uiop:getenv "OC_CONFIG_DIR"))
|
||||
(data-dir (uiop:getenv "OC_DATA_DIR"))
|
||||
(state-dir (uiop:getenv "OC_STATE_DIR"))
|
||||
(memex-dir (uiop:getenv "MEMEX_DIR")))
|
||||
|
||||
(flet ((check-dir (name path critical)
|
||||
(if (and path (> (length path) 0))
|
||||
(if (uiop:directory-exists-p path)
|
||||
(harness-log " [OK] ~a: ~a" name path)
|
||||
(progn
|
||||
(harness-log " [FAIL] ~a directory missing: ~a" name path)
|
||||
(when critical (setf all-ok nil))))
|
||||
(progn
|
||||
(harness-log " [FAIL] ~a variable not set." name)
|
||||
(when critical (setf all-ok nil))))))
|
||||
|
||||
(check-dir "Config (OC_CONFIG_DIR)" config-dir t)
|
||||
(check-dir "Data (OC_DATA_DIR)" data-dir t)
|
||||
(check-dir "State (OC_STATE_DIR)" state-dir t)
|
||||
(check-dir "Memex (MEMEX_DIR)" memex-dir t))
|
||||
all-ok))
|
||||
|
||||
(defun doctor-check-llm ()
|
||||
"Tests connectivity to primary LLM providers. Non-critical fallback allowed."
|
||||
(harness-log "DOCTOR: Checking LLM connectivity...")
|
||||
(let ((openrouter-key (uiop:getenv "OPENROUTER_API_KEY")))
|
||||
(if (and openrouter-key (> (length openrouter-key) 0))
|
||||
(progn
|
||||
(harness-log " [OK] OpenRouter API Key detected.")
|
||||
t)
|
||||
(progn
|
||||
(harness-log " [WARN] No OpenRouter API Key. Falling back to local inference only.")
|
||||
t))))
|
||||
|
||||
(defun doctor-run-all ()
|
||||
"Executes the full diagnostic suite and returns T if system is healthy."
|
||||
(harness-log "==================================================")
|
||||
(harness-log " OPENCORTEX DOCTOR: Commencing Health Check")
|
||||
(harness-log "==================================================")
|
||||
(let ((dep-ok (doctor-check-dependencies))
|
||||
(env-ok (doctor-check-env))
|
||||
(llm-ok (doctor-check-llm)))
|
||||
(harness-log "==================================================")
|
||||
(if (and dep-ok env-ok)
|
||||
(progn
|
||||
(harness-log " ✓ SYSTEM HEALTHY: Ready for ignition.")
|
||||
t)
|
||||
(progn
|
||||
(harness-log " ✗ SYSTEM UNHEALTHY: Fix the errors above.")
|
||||
nil))))
|
||||
|
||||
(defun doctor-main ()
|
||||
"Entry point for the 'doctor' CLI command."
|
||||
(if (doctor-run-all)
|
||||
(uiop:quit 0)
|
||||
(uiop:quit 1)))
|
||||
@@ -1,7 +1,7 @@
|
||||
#+TITLE: SKILL: Diagnostics (org-skill-diagnostics.org)
|
||||
#+AUTHOR: Agent
|
||||
#+FILETAGS: :system:diagnostics:doctor:
|
||||
#+PROPERTY: header-args:lisp :tangle org-skill-diagnostics.lisp
|
||||
#+PROPERTY: header-args:lisp :tangle %%SKILLS_DIR%%/org-skill-diagnostics.lisp
|
||||
|
||||
* Overview
|
||||
The *Diagnostics Skill* (Doctor) provides system-wide health checks and dependency verification. It validates external dependencies, XDG environment, and LLM provider connectivity.
|
||||
@@ -20,11 +20,6 @@ Binary detection must use shell probing (`which`) to account for varying `$PATH`
|
||||
|
||||
* Phase C: Implementation (Build)
|
||||
|
||||
** Package Context
|
||||
#+begin_src lisp
|
||||
(in-package :opencortex)
|
||||
#+end_src
|
||||
|
||||
** Global Configuration
|
||||
#+begin_src lisp
|
||||
(defvar *doctor-required-binaries* '("sbcl" "emacs" "git" "socat" "nc")
|
||||
|
||||
@@ -1,282 +0,0 @@
|
||||
(in-package :opencortex)
|
||||
|
||||
(defun emacs-edit-generate-id ()
|
||||
"Generates a unique ID for org-mode headlines.
|
||||
Format: 8-char hex + timestamp for uniqueness."
|
||||
(let* ((data (format nil "~a-~a" (get-universal-time) (random 999999)))
|
||||
(digest (ironclad:digest-sequence :sha256 (ironclad:ascii-string-to-byte-array data)))
|
||||
(uuid (ironclad:byte-array-to-hex-string digest)))
|
||||
(subseq uuid 0 8)))
|
||||
|
||||
(defun emacs-edit-id-format (id)
|
||||
"Formats ID for org-mode (e.g., 'abc12345')."
|
||||
(if (search "id:" id)
|
||||
id
|
||||
(format nil "id:~a" id)))
|
||||
|
||||
(defun emacs-edit-print-headline (ast &key indent-level)
|
||||
"Converts a HEADLINE AST node to org text.
|
||||
INDENT-LEVEL is number of leading asterisks."
|
||||
(let* ((level (or indent-level 1))
|
||||
(stars (make-string level :initial-element #\*))
|
||||
(title (or (getf (getf ast :properties) :TITLE) ""))
|
||||
(todo (getf (getf ast :properties) :TODO)))
|
||||
(format nil "~a ~a~%~a"
|
||||
stars
|
||||
(if todo (format nil "[~a] " (string-upcase todo)) "")
|
||||
title)))
|
||||
|
||||
(defun emacs-edit-print-properties (props)
|
||||
"Converts property list to :PROPERTIES: drawer."
|
||||
(when props
|
||||
(let ((lines (loop for (k v) on props by #'cddr
|
||||
unless (member k '(:title :todo :created :id))
|
||||
collect (format nil ":~a:~a" k v))))
|
||||
(when lines
|
||||
(format nil ":PROPERTIES:~%~{~a~^~%~}~%:END:~%"
|
||||
lines)))))
|
||||
|
||||
(defun emacs-edit-print-section (ast)
|
||||
"Prints :CONTENT: or description text."
|
||||
(let ((content (getf ast :content)))
|
||||
(when content
|
||||
content)))
|
||||
|
||||
(defun emacs-edit-ast-to-org (ast &key (indent-level 1))
|
||||
"Recursively converts an entire org AST back to org text.
|
||||
Preserves structure including #+begin_src blocks."
|
||||
(let ((type (getf ast :type))
|
||||
(props (getf ast :properties))
|
||||
(contents (getf ast :contents))
|
||||
(elements (getf ast :elements)))
|
||||
|
||||
(cond
|
||||
;; Headline
|
||||
((eq type :headline)
|
||||
(format nil "~%~a~a~%~a~{~a~}"
|
||||
(emacs-edit-print-headline ast :indent-level indent-level)
|
||||
(emacs-edit-print-properties props)
|
||||
(emacs-edit-print-section ast)
|
||||
(mapcar (lambda (child)
|
||||
(emacs-edit-ast-to-org child :indent-level (1+ indent-level)))
|
||||
(or contents elements))))
|
||||
|
||||
;; Section (body text)
|
||||
((eq type :section)
|
||||
(emacs-edit-print-section ast))
|
||||
|
||||
;; Plain text / paragraph
|
||||
((or (eq type :paragraph) (stringp ast))
|
||||
(format nil "~a~%" (if (stringp ast) ast (getf ast :raw-content))))
|
||||
|
||||
;; Code block (preserve exactly)
|
||||
((eq type :src-block)
|
||||
(let ((lang (or (getf ast :language) ""))
|
||||
(code (or (getf ast :value) "")))
|
||||
(format nil "#+begin_src ~a~%~a~%#+end_src~%"
|
||||
lang code)))
|
||||
|
||||
;; Unknown - return as-is
|
||||
(t (format nil "")))))
|
||||
|
||||
(defvar *org-parser-cache* (make-hash-table :test 'equal)
|
||||
"Cache for parsed org files.")
|
||||
|
||||
(defun emacs-edit-parse-file (file-path)
|
||||
"Parses an org FILE-PATH using existing ingest-ast.
|
||||
Returns the parsed AST. Uses cache for performance."
|
||||
(let ((cached (gethash file-path *org-parser-cache*)))
|
||||
(when cached
|
||||
(return-from emacs-edit-parse-file cached)))
|
||||
|
||||
(let* ((content (uiop:read-file-string file-path))
|
||||
(ast (ingest-ast (list :type :document :raw-content content))))
|
||||
(setf (gethash file-path *org-parser-cache*) ast)
|
||||
ast))
|
||||
|
||||
(defun emacs-edit-clear-cache (&optional file-path)
|
||||
"Clears the parser cache. If FILE-PATH provided, clears only that entry."
|
||||
(if file-path
|
||||
(remhash file-path *org-parser-cache*)
|
||||
(clrhash *org-parser-cache*)))
|
||||
|
||||
(defun emacs-edit-write-file (file-path ast)
|
||||
"Writes AST back to FILE-PATH, preserving org structure.
|
||||
Clears cache after write."
|
||||
(opencortex::snapshot-memory)
|
||||
(let ((org-text (emacs-edit-ast-to-org ast)))
|
||||
(with-open-file (out file-path :direction :output :if-exists :supersede)
|
||||
(write-string org-text out)))
|
||||
(emacs-edit-clear-cache file-path)
|
||||
(harness-log "EMACS-EDIT: Wrote ~a" file-path))
|
||||
|
||||
(defun emacs-edit-add-headline (ast title &key todo properties)
|
||||
"Adds a new headline to AST.
|
||||
Returns modified AST."
|
||||
(let* ((new-id (emacs-edit-generate-id))
|
||||
(new-props (list :ID new-id
|
||||
:TITLE title
|
||||
:TODO (or todo "TODO")
|
||||
:CREATED (format nil "[~a]"
|
||||
(multiple-value-bind (s mi h d mo y)
|
||||
(decode-universal-time (get-universal-time))
|
||||
(format nil "~a-~a-~a ~a:~a"
|
||||
y mo d h mi)))))
|
||||
(merged-props (loop for (k v) on properties by #'cddr
|
||||
collect k collect v)))
|
||||
|
||||
(setf merged-props (append merged-props new-props))
|
||||
|
||||
(let ((new-headline (list :type :headline
|
||||
:properties merged-props
|
||||
:contents nil
|
||||
:raw-content title)))
|
||||
(push new-headline (getf ast :contents))
|
||||
ast)))
|
||||
|
||||
(defun emacs-edit-find-headline-by-id (ast target-id)
|
||||
"Recursively finds headline with matching :ID: property."
|
||||
(when (eq (getf ast :type) :headline)
|
||||
(let ((props (getf ast :properties)))
|
||||
(when (string= (getf props :ID) target-id)
|
||||
(return-from emacs-edit-find-headline-by-id ast))))
|
||||
|
||||
(let ((contents (getf ast :contents)))
|
||||
(when contents
|
||||
(dolist (child contents)
|
||||
(let ((found (emacs-edit-find-headline-by-id child target-id)))
|
||||
(when found (return-from emacs-edit-find-headline-by-id found))))))
|
||||
nil)
|
||||
|
||||
(defun emacs-edit-find-headline-by-title (ast target-title)
|
||||
"Recursively finds headline with matching title."
|
||||
(when (eq (getf ast :type) :headline)
|
||||
(let ((props (getf ast :properties)))
|
||||
(when (string= (getf props :TITLE) target-title)
|
||||
(return-from emacs-edit-find-headline-by-title ast))))
|
||||
|
||||
(let ((contents (getf ast :contents)))
|
||||
(when contents
|
||||
(dolist (child contents)
|
||||
(let ((found (emacs-edit-find-headline-by-title child target-title)))
|
||||
(when found (return-from emacs-edit-find-headline-by-title found))))))
|
||||
nil)
|
||||
|
||||
(defun emacs-edit-set-property (ast target property value)
|
||||
"Sets PROPERTY=VALUE on headline matching TARGET (ID or title).
|
||||
Returns modified AST."
|
||||
(let ((headline (if (search "id:" target)
|
||||
(emacs-edit-find-headline-by-id ast target)
|
||||
(emacs-edit-find-headline-by-title ast target))))
|
||||
(when headline
|
||||
(setf (getf (getf headline :properties) property) value)
|
||||
(harness-log "EMACS-EDIT: Set ~a=~a on ~a" property value target)))
|
||||
ast)
|
||||
|
||||
(defun emacs-edit-set-todo (ast target new-state)
|
||||
"Sets TODO state on headline matching TARGET.
|
||||
NEW-STATE should be 'TODO', 'DONE', 'IN-PROGRESS', etc."
|
||||
(emacs-edit-set-property ast target :TODO new-state)
|
||||
(harness-log "EMACS-EDIT: Set TODO to ~a on ~a" new-state target))
|
||||
|
||||
(defun emacs-edit-modify (file-path operation &key params)
|
||||
"Main entry point for org-mode file manipulation.
|
||||
OPERATIONS:
|
||||
:read - Parse file to AST, return AST
|
||||
:write - Write AST back to file (AST in params)
|
||||
:add-headline - Add headline (params: :title, :todo, :properties)
|
||||
:set-property - Set property (params: :target, :property, :value)
|
||||
:set-todo - Set TODO (params: :target, :state)"
|
||||
(let ((ast (emacs-edit-parse-file file-path)))
|
||||
|
||||
(case operation
|
||||
(:read
|
||||
ast)
|
||||
|
||||
(:write
|
||||
(let ((ast-to-write (getf params :ast)))
|
||||
(emacs-edit-write-file file-path ast-to-write)))
|
||||
|
||||
(:add-headline
|
||||
(let ((title (getf params :title))
|
||||
(todo (getf params :todo))
|
||||
(properties (getf params :properties)))
|
||||
(emacs-edit-add-headline ast title :todo todo :properties properties)))
|
||||
|
||||
(:set-property
|
||||
(let ((target (getf params :target))
|
||||
(property (getf params :property))
|
||||
(value (getf params :value)))
|
||||
(emacs-edit-set-property ast target property value)))
|
||||
|
||||
(:set-todo
|
||||
(let ((target (getf params :target))
|
||||
(state (getf params :state)))
|
||||
(emacs-edit-set-todo ast target state)))
|
||||
|
||||
(t
|
||||
(harness-log "EMACS-EDIT ERROR: Unknown operation ~a" operation)))))
|
||||
|
||||
(def-cognitive-tool :org-read
|
||||
"Reads an org-mode file and parses it to structured AST.
|
||||
Use this BEFORE modifying org files to understand their structure."
|
||||
((:file :type :string :description "Path to the org file"))
|
||||
:body (lambda (args)
|
||||
(let ((file (getf args :file)))
|
||||
(if (uiop:file-exists-p file)
|
||||
(emacs-edit-modify file :read)
|
||||
(list :status :error :reason "File not found")))))
|
||||
|
||||
(def-cognitive-tool :org-write
|
||||
"Writes previously parsed AST back to an org file.
|
||||
Use this AFTER modifications to save changes."
|
||||
((:file :type :string :description "Path to the org file")
|
||||
(:ast :type :list :description "The AST to write"))
|
||||
:body (lambda (args)
|
||||
(let ((file (getf args :file))
|
||||
(ast (getf args :ast)))
|
||||
(emacs-edit-modify file :write :params (list :ast ast))
|
||||
(list :status :success :message (format nil "Wrote ~a" file)))))
|
||||
|
||||
(def-cognitive-tool :org-add-headline
|
||||
"Adds a new headline to an org file."
|
||||
((:file :type :string :description "Path to the org file")
|
||||
(:title :type :string :description "Headline title")
|
||||
(:todo :type :string :description "TODO state (default TODO)")
|
||||
(:properties :type :list :description "Plist of properties"))
|
||||
:body (lambda (args)
|
||||
(let ((file (getf args :file))
|
||||
(title (getf args :title))
|
||||
(todo (getf args :todo "TODO"))
|
||||
(properties (getf args :properties)))
|
||||
(emacs-edit-modify file :add-headline
|
||||
:params (list :title title :todo todo :properties properties))
|
||||
(list :status :success :message (format nil "Added headline: ~a" title)))))
|
||||
|
||||
(def-cognitive-tool :org-set-property
|
||||
"Sets a property on an existing headline (by ID or title)."
|
||||
((:file :type :string :description "Path to the org file")
|
||||
(:target :type :string :description "Headline ID or title")
|
||||
(:property :type :string :description "Property name")
|
||||
(:value :type :string :description "Property value"))
|
||||
:body (lambda (args)
|
||||
(let ((file (getf args :file))
|
||||
(target (getf args :target))
|
||||
(property (getf args :property))
|
||||
(value (getf args :value)))
|
||||
(emacs-edit-modify file :set-property
|
||||
:params (list :target target :property property :value value))
|
||||
(list :status :success :message (format nil "Set ~a=~a on ~a" property value target)))))
|
||||
|
||||
(def-cognitive-tool :org-set-todo
|
||||
"Sets the TODO state of a headline."
|
||||
((:file :type :string :description "Path to the org file")
|
||||
(:target :type :string :description "Headline ID or title")
|
||||
(:state :type :string :description "New TODO state (TODO, DONE, etc)"))
|
||||
:body (lambda (args)
|
||||
(let ((file (getf args :file))
|
||||
(target (getf args :target))
|
||||
(state (getf args :state)))
|
||||
(emacs-edit-modify file :set-todo
|
||||
:params (list :target target :state state))
|
||||
(list :status :success :message (format nil "Set ~a to ~a" target state)))))
|
||||
@@ -1,32 +0,0 @@
|
||||
#+TITLE: SKILL: Emacs Edit (org-skill-emacs-edit.org)
|
||||
#+AUTHOR: Agent
|
||||
#+FILETAGS: :skill:emacs:edit:org:
|
||||
#+PROPERTY: header-args:lisp :tangle org-skill-emacs-edit.lisp
|
||||
|
||||
* Overview
|
||||
The *Emacs Edit* skill provides the agent with the capability to read and modify Org-mode files via the Emacs client.
|
||||
|
||||
* Implementation
|
||||
|
||||
** Package Context
|
||||
#+begin_src lisp
|
||||
(in-package :opencortex)
|
||||
#+end_src
|
||||
|
||||
** Emacs Interface Logic
|
||||
#+begin_src lisp
|
||||
(defun emacs-edit-read-file (filepath)
|
||||
"Reads a file via Emacs."
|
||||
(harness-log "EMACS: Reading ~a" filepath))
|
||||
|
||||
(defun emacs-edit-modify (filepath id changes)
|
||||
"Modifies an Org node via Emacs."
|
||||
(harness-log "EMACS: Modifying ~a in ~a" id filepath))
|
||||
#+end_src
|
||||
|
||||
** Skill Registration
|
||||
#+begin_src lisp
|
||||
(defskill :skill-emacs-edit
|
||||
:priority 100
|
||||
:trigger (lambda (ctx) (declare (ignore ctx)) nil))
|
||||
#+end_src
|
||||
@@ -1,38 +0,0 @@
|
||||
(in-package :opencortex)
|
||||
|
||||
(defvar *engineering-std-project-root* nil
|
||||
"Path to the project root for enforcement checks.")
|
||||
|
||||
(defstruct engineering-violation
|
||||
(phase nil)
|
||||
(rule nil)
|
||||
(message nil)
|
||||
(severity nil))
|
||||
|
||||
(defun check-structural-balance (file-path)
|
||||
"Tier 1 Chaos: Verifies that a Lisp file is syntactically balanced."
|
||||
(handler-case
|
||||
(with-open-file (s file-path)
|
||||
(loop for form = (read s nil :eof)
|
||||
until (eq form :eof))
|
||||
t)
|
||||
(error (c)
|
||||
(harness-log "CHAOS ERROR [Tier 1]: ~a in ~a" c file-path)
|
||||
nil)))
|
||||
|
||||
(defun verify-git-clean-p (&optional (dir *engineering-std-project-root*))
|
||||
"Returns T if the git repository at DIR has no uncommitted changes."
|
||||
(when dir
|
||||
(let ((status (uiop:run-program (list "git" "-C" (namestring dir) "status" "--porcelain")
|
||||
:output :string
|
||||
:ignore-error-status t)))
|
||||
(string= "" (string-trim '(#\Space #\Newline #\Tab) status)))))
|
||||
|
||||
(defun engineering-std-init ()
|
||||
"Initialize the enforcement system."
|
||||
(let ((env-root (or (uiop:getenv "OC_DATA_DIR")
|
||||
"/home/user/.local/share/opencortex")))
|
||||
(setf *engineering-std-project-root* (uiop:ensure-directory-pathname env-root))
|
||||
(harness-log "ENGINEERING STANDARDS: CDD Protocol Active.")))
|
||||
|
||||
(engineering-std-init)
|
||||
@@ -1,26 +1,32 @@
|
||||
#+TITLE: SKILL: Engineering Standards (org-skill-engineering-standards.org)
|
||||
#+AUTHOR: Agent
|
||||
#+FILETAGS: :system:engineering:chaos:
|
||||
#+PROPERTY: header-args:lisp :tangle org-skill-engineering-standards.lisp
|
||||
#+PROPERTY: header-args:lisp :tangle %%SKILLS_DIR%%/org-skill-engineering-standards.lisp
|
||||
|
||||
* Overview
|
||||
The *Engineering Standards Skill* enforces technical invariants, including the **Commit-Before-Modify** rule and **Chaos-Driven Development**.
|
||||
|
||||
* Implementation
|
||||
|
||||
** Package Context
|
||||
#+begin_src lisp
|
||||
(in-package :opencortex)
|
||||
#+end_src
|
||||
|
||||
** Standards Enforcement
|
||||
#+begin_src lisp
|
||||
(defun verify-git-clean-p (dir)
|
||||
"Checks if a directory has uncommitted changes."
|
||||
(let ((status (uiop:run-program (list "git" "-C" (namestring dir) "status" "--porcelain
|
||||
(let ((status (uiop:run-program (list "git" "-C" (namestring dir) "status" "--porcelain")
|
||||
:output :string
|
||||
:ignore-error-status t)))
|
||||
(string= "" (string-trim '(#\Space #\Newline #\Tab) status))))
|
||||
|
||||
(defun engineering-standards-verify-lisp (code)
|
||||
"Enforces Lisp structural and semantic standards using utils-lisp."
|
||||
(let ((result (utils-lisp-validate code :strict t)))
|
||||
(if (eq (getf result :status) :success)
|
||||
t
|
||||
(error (getf result :reason)))))
|
||||
|
||||
(defun engineering-standards-format-lisp (code)
|
||||
"Ensures Lisp code adheres to formatting standards."
|
||||
(utils-lisp-format code))
|
||||
#+end_src
|
||||
|
||||
** Skill Registration
|
||||
|
||||
@@ -1,68 +0,0 @@
|
||||
(in-package :opencortex)
|
||||
|
||||
(defvar *gardener-last-audit* 0
|
||||
"The universal-time of the last full Memex audit.")
|
||||
|
||||
(defun gardener-find-broken-links ()
|
||||
"Returns a list of broken ID links found in the Memex."
|
||||
(let ((broken nil))
|
||||
(maphash (lambda (id obj)
|
||||
(let ((content (org-object-content obj)))
|
||||
(when content
|
||||
(cl-ppcre:do-register-groups (target-id) ("id:([A-Za-z0-9-]+)" content)
|
||||
(unless (lookup-object target-id)
|
||||
(push (list :source id :broken-target target-id) broken))))))
|
||||
*memory*)
|
||||
broken))
|
||||
|
||||
(defun gardener-find-orphans ()
|
||||
"Returns a list of IDs for headlines that are structurally isolated."
|
||||
(let ((inbound (make-hash-table :test 'equal))
|
||||
(outbound (make-hash-table :test 'equal))
|
||||
(orphans nil))
|
||||
;; 1. Map all connections
|
||||
(maphash (lambda (id obj)
|
||||
(let ((content (org-object-content obj)))
|
||||
(when content
|
||||
(cl-ppcre:do-register-groups (target-id) ("id:([A-Za-z0-9-]+)" content)
|
||||
(setf (gethash id outbound) t)
|
||||
(setf (gethash target-id inbound) t)))))
|
||||
*memory*)
|
||||
;; 2. Identify nodes with zero connections
|
||||
(maphash (lambda (id obj)
|
||||
(declare (ignore obj))
|
||||
(unless (or (gethash id inbound) (gethash id outbound))
|
||||
(push id orphans)))
|
||||
*memory*)
|
||||
orphans))
|
||||
|
||||
(defun gardener-deterministic-gate (action context)
|
||||
"Main gate for the Gardener skill. Audits graph integrity."
|
||||
(declare (ignore action context))
|
||||
(let ((broken (gardener-find-broken-links))
|
||||
(orphans (gardener-find-orphans)))
|
||||
|
||||
(when (or broken orphans)
|
||||
(harness-log "GARDENER: Audit found ~a broken links and ~a orphans."
|
||||
(length broken) (length orphans))
|
||||
|
||||
(dolist (link broken)
|
||||
(harness-log " [BROKEN LINK] Node ~a -> ~a" (getf link :source) (getf link :broken-target)))
|
||||
|
||||
(dolist (orphan orphans)
|
||||
(harness-log " [ORPHAN] Node ~a is isolated." orphan)))
|
||||
|
||||
(setf *gardener-last-audit* (get-universal-time))
|
||||
;; Return a log to stop the loop
|
||||
(list :type :LOG :payload (list :text "Gardener audit complete."))))
|
||||
|
||||
(defskill :skill-gardener
|
||||
:priority 40
|
||||
:trigger (lambda (ctx)
|
||||
(let* ((payload (getf ctx :payload))
|
||||
(sensor (getf payload :sensor)))
|
||||
(and (eq sensor :heartbeat)
|
||||
;; Only audit once per day
|
||||
(> (- (get-universal-time) *gardener-last-audit*) 86400))))
|
||||
:probabilistic nil
|
||||
:deterministic #'gardener-deterministic-gate)
|
||||
@@ -1,27 +1,22 @@
|
||||
#+TITLE: SKILL: Gardener (org-skill-gardener.org)
|
||||
#+AUTHOR: Agent
|
||||
#+FILETAGS: :skill:maintenance:gardener:
|
||||
#+PROPERTY: header-args:lisp :tangle org-skill-gardener.lisp
|
||||
#+PROPERTY: header-args:lisp :tangle %%SKILLS_DIR%%/org-skill-gardener.lisp
|
||||
|
||||
* Overview
|
||||
The *Gardener Skill* performs periodic maintenance on the Memex knowledge graph.
|
||||
|
||||
* Implementation
|
||||
|
||||
** Package Context
|
||||
#+begin_src lisp
|
||||
(in-package :opencortex)
|
||||
#+end_src
|
||||
|
||||
** Maintenance Logic
|
||||
#+begin_src lisp
|
||||
(defun gardener-prune-orphans ()
|
||||
"Identifies and handles orphaned objects in memory."
|
||||
(harness-log "GARDENER: Pruning orphans...)
|
||||
(harness-log "GARDENER: Pruning orphans..."))
|
||||
|
||||
(defun gardener-verify-merkle-integrity ()
|
||||
"Validates the hashes of all objects in memory."
|
||||
(harness-log "GARDENER: Verifying Merkle integrity...)
|
||||
(harness-log "GARDENER: Verifying Merkle integrity..."))
|
||||
#+end_src
|
||||
|
||||
** Skill Registration
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
(in-package :opencortex)
|
||||
|
||||
(defparameter *skill-gateway-manager*
|
||||
'(:name "gateway-manager"
|
||||
:description "Manages connections to external chat platforms."
|
||||
:capabilities (:link-gateway :list-gateways)
|
||||
:type :deterministic)
|
||||
"Skill metadata for the Gateway Manager.")
|
||||
|
||||
(defvar *gateways* nil "The internal registry of configured gateways.")
|
||||
|
||||
(defun save-gateways ()
|
||||
"Persist gateway metadata to XDG Config directory."
|
||||
(let ((path (merge-pathnames "gateways.lisp" (get-oc-config-dir))))
|
||||
(ensure-directories-exist path)
|
||||
(with-open-file (s path :direction :output :if-exists :supersede)
|
||||
(format s ";;; OpenCortex Gateway Registry~%~s~%" *gateways*))))
|
||||
|
||||
(defun skill-gateway-register (platform metadata)
|
||||
"Internal function to update the gateway registry."
|
||||
(setf (getf *gateways* platform) metadata))
|
||||
|
||||
(defun skill-gateway-verify-telegram (token)
|
||||
"Verifies a Telegram bot token via the getMe API."
|
||||
(let ((url (format nil "https://api.telegram.org/bot~a/getMe" token)))
|
||||
(handler-case
|
||||
(let* ((response (dex:get url))
|
||||
(data (cl-json:decode-json-from-string response)))
|
||||
(if (cdr (assoc :ok data))
|
||||
(let ((result (cdr (assoc :result data))))
|
||||
(list :status :verified :username (cdr (assoc :username result))))
|
||||
(list :status :failed :error "Invalid Token")))
|
||||
(error (c) (list :status :failed :error (format nil "~a" c))))))
|
||||
|
||||
(defun skill-gateway-link (platform token)
|
||||
"Primary capability to link a new platform. Returns status plist."
|
||||
(harness-log "GATEWAY: Attempting to link ~a..." platform)
|
||||
(let ((verification (cond
|
||||
((eq platform :telegram) (skill-gateway-verify-telegram token))
|
||||
(t (list :status :verified :info "Platform verification pending implementation")))))
|
||||
(if (eq (getf verification :status) :verified)
|
||||
(progn
|
||||
(save-secret platform :token token)
|
||||
(skill-gateway-register platform verification)
|
||||
(save-gateways)
|
||||
(list :status :success :platform platform :info verification))
|
||||
(list :status :error :reason (getf verification :error)))))
|
||||
|
||||
(defun gateway-manager-main (platform token)
|
||||
"Main entry point for CLI-driven linkage."
|
||||
(if (and platform token)
|
||||
(let ((result (skill-gateway-link (intern (string-upcase platform) :keyword) token)))
|
||||
(format t "RESULT: ~s~%" result)
|
||||
(uiop:quit 0))
|
||||
(progn
|
||||
(format t "Usage: opencortex link <PLATFORM> <TOKEN>~%")
|
||||
(uiop:quit 1))))
|
||||
@@ -1,18 +1,13 @@
|
||||
#+TITLE: SKILL: Gateway Manager (org-skill-gateway-manager.org)
|
||||
#+AUTHOR: Agent
|
||||
#+FILETAGS: :skill:gateway:manager:
|
||||
#+PROPERTY: header-args:lisp :tangle org-skill-gateway-manager.lisp
|
||||
#+PROPERTY: header-args:lisp :tangle %%SKILLS_DIR%%/org-skill-gateway-manager.lisp
|
||||
|
||||
* Overview
|
||||
The *Gateway Manager* handles the registration and linking of external communication platforms.
|
||||
|
||||
* Implementation
|
||||
|
||||
** Package Context
|
||||
#+begin_src lisp
|
||||
(in-package :opencortex)
|
||||
#+end_src
|
||||
|
||||
** Gateway Logic
|
||||
#+begin_src lisp
|
||||
(defun skill-gateway-register (platform token)
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
(in-package :opencortex)
|
||||
|
||||
(defun memory-org-to-json (source)
|
||||
"Converts Org-mode source to JSON AST."
|
||||
(declare (ignore source))
|
||||
"")
|
||||
|
||||
(defun memory-json-to-org (ast)
|
||||
"Converts JSON AST back to Org-mode text."
|
||||
(declare (ignore ast))
|
||||
"")
|
||||
|
||||
(defun memory-normalize-ast (ast)
|
||||
"Recursively ensures ID uniqueness across the AST."
|
||||
(declare (ignore ast))
|
||||
nil)
|
||||
|
||||
(defun make-memory-node (headline &key content properties children)
|
||||
"Constructor for a normalized Org node alist."
|
||||
(declare (ignore headline))
|
||||
(list :TYPE :HEADLINE
|
||||
:PROPERTIES (or properties nil)
|
||||
:CONTENT content
|
||||
:CONTENTS children))
|
||||
|
||||
(defskill :skill-homoiconic-memory
|
||||
:priority 100
|
||||
:trigger (lambda (ctx) (declare (ignore ctx)) nil)
|
||||
:probabilistic nil
|
||||
:deterministic (lambda (action ctx) (declare (ignore ctx)) action))
|
||||
@@ -1,23 +1,18 @@
|
||||
#+TITLE: SKILL: Homoiconic Memory (org-skill-homoiconic-memory.org)
|
||||
#+AUTHOR: Agent
|
||||
#+FILETAGS: :harness:memory:homoiconic:
|
||||
#+PROPERTY: header-args:lisp :tangle org-skill-homoiconic-memory.lisp
|
||||
#+PROPERTY: header-args:lisp :tangle %%SKILLS_DIR%%/org-skill-homoiconic-memory.lisp
|
||||
|
||||
* Overview
|
||||
The *Homoiconic Memory* skill provides the capability to treat system memory as executable code and vice-versa.
|
||||
|
||||
* Implementation
|
||||
|
||||
** Package Context
|
||||
#+begin_src lisp
|
||||
(in-package :opencortex)
|
||||
#+end_src
|
||||
|
||||
** Memory Logic
|
||||
#+begin_src lisp
|
||||
(defun memory-self-inspect ()
|
||||
"Allows the system to inspect its own memory state."
|
||||
(harness-log "MEMORY: Self-inspection triggered.)
|
||||
(harness-log "MEMORY: Self-inspection triggered."))
|
||||
#+end_src
|
||||
|
||||
** Skill Registration
|
||||
|
||||
@@ -1,137 +0,0 @@
|
||||
(in-package :opencortex)
|
||||
|
||||
(defun count-char (char string)
|
||||
"Counts occurrences of CHAR in STRING.
|
||||
Returns an integer count."
|
||||
(let ((count 0))
|
||||
(loop for c across string
|
||||
when (char= c char)
|
||||
do (incf count))
|
||||
count))
|
||||
|
||||
(defun deterministic-repair (code)
|
||||
"Attempts instant fixes on broken Lisp code (e.g., balancing parens).
|
||||
Returns the fixed code string."
|
||||
(let* ((open-parens (count-char #\( code))
|
||||
(close-parens (count-char #\) code))
|
||||
(diff (- open-parens close-parens)))
|
||||
(if (> diff 0)
|
||||
(concatenate 'string code (make-string diff :initial-element #\)))
|
||||
code)))
|
||||
|
||||
(defun lisp-utils-check-structural (code-string)
|
||||
"Checks for balanced parens, brackets, and terminated strings.
|
||||
Returns (VALUES t nil) if clean, or (VALUES nil reason-string line col)."
|
||||
(let ((stack nil)
|
||||
(in-string nil)
|
||||
(escaped nil)
|
||||
(line 1)
|
||||
(col 0)
|
||||
(last-open-line 1)
|
||||
(last-open-col 0))
|
||||
(dotimes (i (length code-string))
|
||||
(let ((ch (char code-string i)))
|
||||
(cond (escaped (setf escaped nil))
|
||||
((char= ch #\\) (setf escaped t))
|
||||
(in-string
|
||||
(when (char= ch #\") (setf in-string nil)))
|
||||
((char= ch #\;)
|
||||
(loop while (and (< i (1- (length code-string)))
|
||||
(not (char= (char code-string (1+ i)) #\Newline)))
|
||||
do (incf i))
|
||||
(setf col 0))
|
||||
((char= ch #\Newline)
|
||||
(incf line)
|
||||
(setf col 0))
|
||||
((char= ch #\")
|
||||
(setf in-string t))
|
||||
((char= ch #\()
|
||||
(push (list :paren line col) stack)
|
||||
(setf last-open-line line last-open-col col))
|
||||
((char= ch #\))
|
||||
(if (null stack)
|
||||
(return-from lisp-utils-check-structural
|
||||
(values nil (format nil "Unexpected close parenthesis at Line: ~a, Column: ~a" line col) line col))
|
||||
(pop stack))))
|
||||
(incf col)))
|
||||
(if stack
|
||||
(values nil (format nil "Unbalanced open parenthesis starting at Line: ~a, Column: ~a" last-open-line last-open-col) last-open-line last-open-col)
|
||||
(values t nil))))
|
||||
|
||||
(defun lisp-utils-check-syntactic (code-string)
|
||||
"Checks if the code can be read by SBCL with *read-eval* nil.
|
||||
Returns (VALUES t nil) if clean, or (VALUES nil error-message nil 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)))
|
||||
(values t nil nil nil))
|
||||
(error (c)
|
||||
(let ((msg (format nil "~a" c)))
|
||||
(values nil msg nil nil)))))
|
||||
|
||||
(defparameter *lisp-utils-whitelist*
|
||||
'(+ - * / = < > <= >= 1+ 1- min max mod abs floor ceiling round
|
||||
and or not null eq eql equal string= string-equal char= char-equal
|
||||
list cons car cdr cadr cddr cdar caar caddr cdddr append mapcar remove-if remove-if-not
|
||||
length reverse sort nth nthcdr push pop last butlast subseq
|
||||
getf gethash assoc acons pairlis rassoc
|
||||
let let* if cond when unless case typecase prog1 progn
|
||||
format concatenate string-downcase string-upcase search subseq replace
|
||||
stringp numberp integerp listp symbolp keywordp
|
||||
opencortex:harness-log
|
||||
opencortex:snapshot-memory opencortex:rollback-memory
|
||||
opencortex:lookup-object opencortex:list-objects-by-type
|
||||
opencortex:ingest-ast opencortex:find-headline-missing-id))
|
||||
|
||||
(defun lisp-utils-ast-walk (form)
|
||||
(cond ((atom form)
|
||||
(if (symbolp form)
|
||||
(or (keywordp form)
|
||||
(member form *lisp-utils-whitelist* :test #'string-equal))
|
||||
t))
|
||||
(t (every #'lisp-utils-ast-walk form))))
|
||||
|
||||
(defun lisp-utils-check-semantic (code-string)
|
||||
"Whitelists Common Lisp symbols for safe evaluation."
|
||||
(multiple-value-bind (valid-p err) (lisp-utils-check-syntactic code-string)
|
||||
(if (not valid-p)
|
||||
(values nil (format nil "Syntax Error: ~a" err))
|
||||
(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)
|
||||
do (unless (lisp-utils-ast-walk form)
|
||||
(return-from lisp-utils-check-semantic (values nil "Unsafe symbol detected")))))
|
||||
(values t nil))
|
||||
(error (c) (values nil (format nil "~a" c)))))))
|
||||
|
||||
(defun lisp-utils-validate (code-string &key strict)
|
||||
(multiple-value-bind (structural-ok reason) (lisp-utils-check-structural code-string)
|
||||
(if (not structural-ok)
|
||||
(list :status :error :failed :structural :reason reason)
|
||||
(multiple-value-bind (syntactic-ok err) (lisp-utils-check-syntactic code-string)
|
||||
(if (not syntactic-ok)
|
||||
(list :status :error :failed :syntactic :reason err)
|
||||
(if strict
|
||||
(multiple-value-bind (semantic-ok msg) (lisp-utils-check-semantic code-string)
|
||||
(if (not semantic-ok)
|
||||
(list :status :error :failed :semantic :reason msg)
|
||||
(list :status :success)))
|
||||
(list :status :success)))))))
|
||||
|
||||
(defskill :skill-lisp-utils
|
||||
:priority 900
|
||||
:trigger (lambda (c) (declare (ignore c)) nil)
|
||||
:deterministic (lambda (a c) (declare (ignore c)) a))
|
||||
|
||||
(def-cognitive-tool :validate-lisp
|
||||
"Deterministically validates Lisp code for structural, syntactic, and semantic correctness."
|
||||
((:code :type :string :description "The Lisp code string to validate.")
|
||||
(:strict :type :boolean :description "If non-nil, enforces the semantic whitelist."))
|
||||
:body (lambda (args)
|
||||
(let ((code (getf args :code))
|
||||
(strict (getf args :strict)))
|
||||
(if (and code (stringp code))
|
||||
(lisp-utils-validate code :strict strict)
|
||||
(list :status :error :reason "Missing :code argument.")))))
|
||||
@@ -1,35 +0,0 @@
|
||||
#+TITLE: SKILL: Lisp Utils (org-skill-lisp-utils.org)
|
||||
#+AUTHOR: Agent
|
||||
#+FILETAGS: :skill:lisp:validation:
|
||||
#+PROPERTY: header-args:lisp :tangle org-skill-lisp-utils.lisp
|
||||
|
||||
* Overview
|
||||
The *Lisp Utils* skill provides advanced structural and semantic validation for Common Lisp code.
|
||||
|
||||
* Implementation
|
||||
|
||||
** Package Context
|
||||
#+begin_src lisp
|
||||
(in-package :opencortex)
|
||||
#+end_src
|
||||
|
||||
** Validation Logic
|
||||
#+begin_src lisp
|
||||
(defun lisp-utils-validate (code &key (strict t))
|
||||
"Performs deep validation of Lisp code strings."
|
||||
(declare (ignore strict))
|
||||
(handler-case
|
||||
(let ((*read-eval* nil))
|
||||
(with-input-from-string (s (format nil "(progn ~a)" code))
|
||||
(loop for form = (read s nil :eof) until (eq form :eof)))
|
||||
(list :status :success))
|
||||
(error (c)
|
||||
(list :status :error :reason (format nil "~a" c)))))
|
||||
#+end_src
|
||||
|
||||
** Skill Registration
|
||||
#+begin_src lisp
|
||||
(defskill :skill-lisp-utils
|
||||
:priority 400
|
||||
:trigger (lambda (ctx) (declare (ignore ctx)) nil))
|
||||
#+end_src
|
||||
@@ -1,155 +0,0 @@
|
||||
(in-package :opencortex)
|
||||
|
||||
(defun literate-check-block-balance (code-string)
|
||||
"Returns T if CODE-STRING has balanced parentheses, brackets, and strings.
|
||||
|
||||
Ignores comments (after ;) and tracks string contents to avoid
|
||||
counting parens inside string literals."
|
||||
(let ((depth 0) (in-string nil) (escaped nil))
|
||||
(dotimes (i (length code-string))
|
||||
(let ((ch (char code-string i)))
|
||||
(cond
|
||||
;; Escape handling (affects next char only)
|
||||
(escaped (setf escaped nil))
|
||||
((char= ch #\\) (setf escaped t))
|
||||
;; String boundaries
|
||||
(in-string (when (char= ch #\") (setf in-string nil)))
|
||||
((char= ch #\") (setf in-string t))
|
||||
;; Comment boundaries (skip to end of line)
|
||||
((char= ch #\;)
|
||||
(loop while (and (< i (1- (length code-string)))
|
||||
(not (char= (char code-string (1+ i)) #\Newline)))
|
||||
do (incf i)))
|
||||
;; Structural parens
|
||||
((member ch '(#\( #\[)) (incf depth))
|
||||
((member ch '(#\) #\]))
|
||||
(if (<= depth 0)
|
||||
(return-from literate-check-block-balance
|
||||
(values nil (format nil "Unexpected close paren at position ~a" i)))
|
||||
(decf depth))))))
|
||||
(if (zerop depth)
|
||||
t
|
||||
(values nil (format nil "Unbalanced parens: depth ~a at end of string" depth)))))
|
||||
|
||||
(defun literate-audit-org-file (filepath)
|
||||
"Audits all tangled lisp blocks in an Org file for structural balance.
|
||||
|
||||
Returns a list of imbalance reports, or NIL if all blocks are balanced."
|
||||
(let* ((content (with-open-file (s filepath)
|
||||
(let ((seq (make-string (file-length s))))
|
||||
(read-sequence seq s)
|
||||
seq)))
|
||||
(idx 0)
|
||||
(reports nil)
|
||||
(block-num 0))
|
||||
(loop
|
||||
(let ((pos (search "#+begin_src lisp" content :start2 idx :test #'string-equal)))
|
||||
(when (null pos) (return (nreverse reports)))
|
||||
(let* ((eol (or (position #\Newline content :start pos) (length content)))
|
||||
(header (subseq content pos eol))
|
||||
(header-lower (string-downcase header))
|
||||
(tangle-p (and (search ".lisp" header-lower)
|
||||
(not (search ":tangle no" header-lower)))))
|
||||
(if (not tangle-p)
|
||||
(setf idx (1+ eol))
|
||||
(let ((end-pos (search "#+end_src" content :start2 eol :test #'string-equal)))
|
||||
(if (null end-pos)
|
||||
(progn
|
||||
(push (list :block (incf block-num) :status :missing-end-src) reports)
|
||||
(return (nreverse reports)))
|
||||
(let ((raw-block (subseq content (1+ eol) end-pos))
|
||||
(clean-lines nil))
|
||||
;; Strip PROPERTIES drawers and :END: markers
|
||||
(dolist (line (uiop:split-string raw-block :separator '(#\Newline)))
|
||||
(let ((trimmed (string-trim '(#\Space #\Tab #\Return) line)))
|
||||
(when (and (plusp (length trimmed))
|
||||
(not (string= (subseq trimmed 0 (min 12 (length trimmed))) ":PROPERTIES:"))
|
||||
(not (string= (subseq trimmed 0 (min 5 (length trimmed))) ":END:")))
|
||||
(push line clean-lines))))
|
||||
(let ((code (format nil "~{~a~^~%~}" (nreverse clean-lines))))
|
||||
(multiple-value-bind (ok reason) (literate-check-block-balance code)
|
||||
(unless ok
|
||||
(push (list :block (incf block-num)
|
||||
:status :unbalanced
|
||||
:reason reason
|
||||
:code code)
|
||||
reports))))
|
||||
(setf idx (+ end-pos 9)))))))))))
|
||||
|
||||
(defvar *tangle-targets*
|
||||
'(("skills/org-skill-engineering-standards.org" . "library/gen/org-skill-engineering-standards.lisp")
|
||||
("skills/org-skill-literate-programming.org" . "library/gen/org-skill-literate-programming.lisp")
|
||||
("harness/memory.org" . "library/memory.lisp")
|
||||
("harness/loop.org" . "library/loop.lisp")
|
||||
("harness/perceive.org" . "library/perceive.lisp")
|
||||
("harness/reason.org" . "library/reason.lisp")
|
||||
("harness/act.org" . "library/act.lisp")
|
||||
("harness/skills.org" . "library/skills.lisp")
|
||||
("harness/communication.org" . "library/communication.lisp")))
|
||||
|
||||
(defvar *lp-project-root* nil)
|
||||
|
||||
(defun lp-set-project-root (path)
|
||||
(setf *lp-project-root* (uiop:ensure-directory-pathname path)))
|
||||
|
||||
(defun check-tangle-sync (&optional (root *lp-project-root*))
|
||||
"Returns violation if any tangled .lisp file is newer than its Org source.
|
||||
|
||||
This detects direct .lisp edits (which violate the LP workflow)."
|
||||
(when root
|
||||
(dolist (pair *tangle-targets*)
|
||||
(let* ((org-file (merge-pathnames (car pair) root))
|
||||
(lisp-file (merge-pathnames (cdr pair) root))
|
||||
(org-time (ignore-errors (file-write-date org-file)))
|
||||
(lisp-time (ignore-errors (file-write-date lisp-file))))
|
||||
(when (and org-time lisp-time (> lisp-time org-time))
|
||||
(return-from check-tangle-sync
|
||||
(list :type :log
|
||||
:payload (list :text (format nil "LITERATE PROGRAMMING VIOLATION: ~a is newer than ~a. Edit Org source, not .lisp directly."
|
||||
(file-namestring lisp-file) (file-namestring org-file)))))))))
|
||||
nil)
|
||||
|
||||
(defskill :skill-literate-programming
|
||||
:priority 1100
|
||||
:trigger (lambda (ctx)
|
||||
(declare (ignore ctx))
|
||||
t)
|
||||
:probabilistic nil
|
||||
:deterministic (lambda (action context)
|
||||
(declare (ignore context))
|
||||
(block skill-literate-programming
|
||||
;; Check tangle sync before any file modification
|
||||
(let ((file (and (listp action) (getf action :payload) (getf (getf action :payload) :file))))
|
||||
(when file
|
||||
(let ((tangle-check (check-tangle-sync *lp-project-root*)))
|
||||
(when tangle-check
|
||||
(return-from skill-literate-programming
|
||||
(progn
|
||||
(harness-log "~a" (getf (getf tangle-check :payload) :text))
|
||||
tangle-check))))))
|
||||
;; Audit org files for structural balance
|
||||
(when (and (listp action)
|
||||
(stringp (getf action :file)))
|
||||
(let ((file (getf action :file)))
|
||||
(when (and (search ".org" file)
|
||||
(search "skill" file :test #'string-equal))
|
||||
(let ((issues (literate-audit-org-file file)))
|
||||
(when issues
|
||||
(harness-log "LITERATE PROGRAMMING: Structural issues found in ~a: ~a"
|
||||
file issues))))))
|
||||
action)))
|
||||
|
||||
(defvar *lp-initialized* nil)
|
||||
|
||||
(defun lp-init ()
|
||||
"Initialize the LP system with project root."
|
||||
(unless *lp-initialized*
|
||||
(let ((env-root (or (uiop:getenv "OPENCORTEX_ROOT")
|
||||
(uiop:getenv "MEMEX_DIR")
|
||||
"/home/user/memex/projects/opencortex")))
|
||||
(lp-set-project-root env-root)
|
||||
(setf *lp-initialized* t)
|
||||
(harness-log "LITERATE PROGRAMMING: Initialized with root ~a" *lp-project-root*))))
|
||||
|
||||
;; Auto-initialize on load
|
||||
(lp-init)
|
||||
@@ -1,18 +1,13 @@
|
||||
#+TITLE: SKILL: Literate Programming (org-skill-literate-programming.org)
|
||||
#+AUTHOR: Agent
|
||||
#+FILETAGS: :system:literate:tangle:
|
||||
#+PROPERTY: header-args:lisp :tangle org-skill-literate-programming.lisp
|
||||
#+PROPERTY: header-args:lisp :tangle %%SKILLS_DIR%%/org-skill-literate-programming.lisp
|
||||
|
||||
* Overview
|
||||
The *Literate Programming* skill ensures the synchronization between `.org` sources and `.lisp` artifacts.
|
||||
|
||||
* Implementation
|
||||
|
||||
** Package Context
|
||||
#+begin_src lisp
|
||||
(in-package :opencortex)
|
||||
#+end_src
|
||||
|
||||
** Synchronization Logic
|
||||
#+begin_src lisp
|
||||
(defun literate-check-block-balance (org-file)
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
(in-package :opencortex)
|
||||
|
||||
(defun llama-inference (prompt system-prompt &key (model "local-model"))
|
||||
"Sends a completion request to the local llama.cpp server."
|
||||
(let ((endpoint (uiop:getenv "LLAMACPP_ENDPOINT")))
|
||||
(unless endpoint
|
||||
(harness-log "LLAMA ERROR: LLAMACPP_ENDPOINT not set in environment.")
|
||||
(return-from llama-inference (list :error "LLAMACPP_ENDPOINT_MISSING")))
|
||||
|
||||
(handler-case
|
||||
(let* ((full-prompt (format nil "System: ~a~%User: ~a~%Assistant:" system-prompt prompt))
|
||||
(payload (cl-json:encode-json-to-string
|
||||
`((:prompt . ,full-prompt)
|
||||
(:n_predict . 1024)
|
||||
(:stop . ("User:" "System:")))))
|
||||
(response (dex:post (format nil "~a/completion" endpoint)
|
||||
:content payload
|
||||
:headers '(("Content-Type" . "application/json"))))
|
||||
(data (cl-json:decode-json-from-string response)))
|
||||
(cdr (assoc :content data)))
|
||||
(error (c)
|
||||
(harness-log "LLAMA ERROR: Connection failed -> ~a" c)
|
||||
(list :error (format nil "~a" c))))))
|
||||
|
||||
(progn
|
||||
(register-probabilistic-backend :llama #'llama-inference)
|
||||
(harness-log "LLAMA: Local backend registered and active."))
|
||||
|
||||
(defskill :skill-llama-backend
|
||||
:priority 50
|
||||
:trigger (lambda (ctx) (declare (ignore ctx)) nil) ; Pure infrastructure skill
|
||||
:probabilistic nil
|
||||
:deterministic (lambda (action ctx) (declare (ignore ctx)) action))
|
||||
@@ -1,42 +0,0 @@
|
||||
#+TITLE: SKILL: Llama Backend (org-skill-llama-backend.org)
|
||||
#+AUTHOR: Agent
|
||||
#+FILETAGS: :skill:llm:backend:ollama:
|
||||
#+PROPERTY: header-args:lisp :tangle org-skill-llama-backend.lisp
|
||||
|
||||
* Overview
|
||||
The *Llama Backend* skill provides the actual implementation for calling local models via Ollama.
|
||||
|
||||
* Implementation
|
||||
|
||||
** Package Context
|
||||
#+begin_src lisp
|
||||
(in-package :opencortex)
|
||||
#+end_src
|
||||
|
||||
** Ollama API Call (ollama-call)
|
||||
#+begin_src lisp
|
||||
(defun ollama-call (prompt system-prompt &key (model "llama3)
|
||||
"Sends a request to the local Ollama API."
|
||||
(let* ((host (or (uiop:getenv "OLLAMA_HOST "localhost:11434)
|
||||
(url (format nil "http://~a/api/generate" host))
|
||||
(payload (cl-json:encode-json-to-string
|
||||
`((model . ,model)
|
||||
(prompt . ,prompt)
|
||||
(system . ,system-prompt)
|
||||
(stream . nil)))))
|
||||
(handler-case
|
||||
(let ((response (dex:post url :content payload :headers '(("Content-Type" . "application/json))))
|
||||
(let ((data (cl-json:decode-json-from-string response)))
|
||||
(list :status :success :content (getf data :response))))
|
||||
(error (c)
|
||||
(list :status :error :message (format nil "Ollama Failure: ~a" c))))))
|
||||
#+end_src
|
||||
|
||||
** Skill Registration
|
||||
#+begin_src lisp
|
||||
(register-probabilistic-backend :ollama #'ollama-call)
|
||||
|
||||
(defskill :skill-llama-backend
|
||||
:priority 50
|
||||
:trigger (lambda (ctx) (declare (ignore ctx)) nil))
|
||||
#+end_src
|
||||
@@ -1,60 +0,0 @@
|
||||
(in-package :opencortex)
|
||||
|
||||
(defparameter *skill-llm-gateway*
|
||||
'(:name "llm-gateway"
|
||||
:description "Unified provider-agnostic LLM interface."
|
||||
:capabilities (:ask-llm :get-embedding)
|
||||
:type :probabilistic)
|
||||
"Skill metadata for the LLM Gateway.")
|
||||
|
||||
(defun execute-llm-request (&key prompt system-prompt provider model)
|
||||
"Generic executor for all LLM providers."
|
||||
(let* ((active-provider (or provider :ollama))
|
||||
(api-key (uiop:getenv (format nil "~:@(~a_API_KEY~)" active-provider)))
|
||||
(full-prompt (if system-prompt (format nil "~a~%~%~a" system-prompt prompt) prompt)))
|
||||
(case active-provider
|
||||
(:ollama
|
||||
(let* ((host (or (uiop:getenv "OLLAMA_HOST") "localhost:11434"))
|
||||
(url (format nil "http://~a/api/generate" host))
|
||||
(body (cl-json:encode-json-to-string `((model . ,(or model "llama3")) (prompt . ,full-prompt) (stream . :false)))))
|
||||
(handler-case
|
||||
(let* ((response (dex:post url :headers '(("Content-Type" . "application/json")) :content body))
|
||||
(json (cl-json:decode-json-from-string response)))
|
||||
(list :status :success :content (cdr (assoc :response json))))
|
||||
(error (c) (list :status :error :message (format nil "Ollama Failure: ~a" c))))))
|
||||
(t (list :status :error :message "Provider not implemented")))))
|
||||
|
||||
(def-cognitive-tool :get-ollama-embedding
|
||||
"Generates vector embeddings via Ollama API."
|
||||
((:text :type :string :description "Text to embed."))
|
||||
:body (lambda (args)
|
||||
(let ((text (getf args :text)))
|
||||
(let* ((host (or (uiop:getenv "OLLAMA_HOST") "localhost:11434"))
|
||||
(url (format nil "http://~a/api/embeddings" host))
|
||||
(body (cl-json:encode-json-to-string `((model . "nomic-embed-text") (prompt . ,text)))))
|
||||
(handler-case
|
||||
(let* ((response (dex:post url :headers '(("Content-Type" . "application/json")) :content body))
|
||||
(json (cl-json:decode-json-from-string response)))
|
||||
(cdr (assoc :embedding json)))
|
||||
(error (c) (harness-log "OLLAMA EMBED ERROR: ~a" c) nil))))))
|
||||
|
||||
(def-cognitive-tool :ask-llm
|
||||
"Unified interface for interacting with LLM providers."
|
||||
((:prompt :type :string :description "The user prompt")
|
||||
(:system-prompt :type :string :description "The system prompt (optional)")
|
||||
(:provider :type :keyword :description "The provider (e.g., :ollama, :openai)")
|
||||
(:model :type :string :description "The model name"))
|
||||
:body (lambda (args)
|
||||
(execute-llm-request :prompt (getf args :prompt)
|
||||
:system-prompt (getf args :system-prompt)
|
||||
:provider (getf args :provider)
|
||||
:model (getf args :model))))
|
||||
|
||||
(defskill :skill-llm-gateway
|
||||
:priority 50
|
||||
:trigger (lambda (ctx) (declare (ignore ctx)) t)
|
||||
:probabilistic (lambda (ctx)
|
||||
(let ((input (getf ctx :user-input)))
|
||||
(when input
|
||||
(execute-llm-request :prompt input))))
|
||||
:deterministic (lambda (action ctx) (declare (ignore ctx)) action))
|
||||
@@ -1,18 +1,13 @@
|
||||
#+TITLE: SKILL: LLM Gateway (org-skill-llm-gateway.org)
|
||||
#+AUTHOR: Agent
|
||||
#+FILETAGS: :skill:llm:gateway:
|
||||
#+PROPERTY: header-args:lisp :tangle org-skill-llm-gateway.lisp
|
||||
#+PROPERTY: header-args:lisp :tangle %%SKILLS_DIR%%/org-skill-llm-gateway.lisp
|
||||
|
||||
* Overview
|
||||
The *LLM Gateway* skill provides a unified interface for interacting with multiple Large Language Model providers.
|
||||
|
||||
* Implementation
|
||||
|
||||
** Package Context
|
||||
#+begin_src lisp
|
||||
(in-package :opencortex)
|
||||
#+end_src
|
||||
|
||||
** Request Execution (execute-llm-request)
|
||||
#+begin_src lisp
|
||||
(defun execute-llm-request (&key prompt system-prompt (provider :ollama) model)
|
||||
|
||||
@@ -1,72 +0,0 @@
|
||||
(in-package :opencortex)
|
||||
|
||||
(defun context-render-to-org (obj &key (depth 1) (foveal-id nil) (semantic-threshold 0.75) (foveal-vector nil))
|
||||
"Recursively renders an org-object and its children to an Org string using a Foveal-Peripheral Hybrid model."
|
||||
(let* ((id (org-object-id obj))
|
||||
(is-foveal (equal id foveal-id))
|
||||
(title (or (getf (org-object-attributes obj) :TITLE) "Untitled"))
|
||||
(content (org-object-content obj))
|
||||
(children (org-object-children obj))
|
||||
(stars (make-string depth :initial-element #\*))
|
||||
(obj-vector (org-object-vector obj))
|
||||
(similarity (if (and foveal-vector obj-vector (not is-foveal))
|
||||
(cosine-similarity foveal-vector obj-vector)
|
||||
0.0))
|
||||
(is-semantically-relevant (>= similarity semantic-threshold))
|
||||
;; We always render depth 1 and 2 (Projects and main tasks).
|
||||
;; We always render the foveal node and its immediate children.
|
||||
;; We render deeper nodes ONLY if they are semantically relevant.
|
||||
(should-render (or (<= depth 2) is-foveal is-semantically-relevant))
|
||||
(output ""))
|
||||
|
||||
(when should-render
|
||||
(setf output (format nil "~a ~a~%:PROPERTIES:~%:ID: ~a~%" stars title id))
|
||||
(when (and is-semantically-relevant (> similarity 0))
|
||||
(setf output (concatenate 'string output (format nil ":SEMANTIC_SCORE: ~,2f~%" similarity))))
|
||||
(setf output (concatenate 'string output (format nil ":END:~%")))
|
||||
|
||||
;; Only include full body content if this is the Foveal focus or highly relevant
|
||||
(when (and content (or is-foveal is-semantically-relevant))
|
||||
(setf output (concatenate 'string output content (string #\Newline))))
|
||||
|
||||
;; Recursively render children
|
||||
(dolist (child-id children)
|
||||
(let ((child-obj (lookup-object child-id)))
|
||||
(when child-obj
|
||||
;; If the current node is Foveal, its children should be rendered (depth effectively resets)
|
||||
(let ((next-foveal (if is-foveal child-id foveal-id)))
|
||||
(setf output (concatenate 'string output
|
||||
(context-render-to-org child-obj
|
||||
:depth (1+ depth)
|
||||
:foveal-id next-foveal
|
||||
:semantic-threshold semantic-threshold
|
||||
:foveal-vector foveal-vector))))))))
|
||||
output))
|
||||
|
||||
(defun context-assemble-global-awareness (&optional signal)
|
||||
"Produces a high-level skeletal outline of the current Memory for the LLM."
|
||||
(let* ((payload (when signal (getf signal :payload)))
|
||||
(foveal-id (when payload (getf payload :target-id)))
|
||||
(foveal-vector (when foveal-id (org-object-vector (lookup-object foveal-id))))
|
||||
(projects (context-get-active-projects))
|
||||
(output "GLOBAL MEMEX AWARENESS (Peripheral Vision):
|
||||
"))
|
||||
(if projects
|
||||
(dolist (project projects)
|
||||
(setf output (concatenate 'string output
|
||||
(context-render-to-org project
|
||||
:foveal-id foveal-id
|
||||
:foveal-vector foveal-vector))))
|
||||
(setf output (concatenate 'string output "No active projects found.~%")))
|
||||
output))
|
||||
|
||||
(defskill :skill-peripheral-vision
|
||||
:priority 90
|
||||
:dependencies ("org-skill-embedding")
|
||||
:trigger (lambda (ctx) (member (getf (getf ctx :payload) :sensor) '(:perceive :context-refresh)))
|
||||
:probabilistic nil
|
||||
:deterministic (lambda (action ctx)
|
||||
(declare (ignore action ctx))
|
||||
;; This skill primarily provides the context-assemble-global-awareness function
|
||||
;; used by the probabilistic-gate, rather than handling specific actions.
|
||||
nil))
|
||||
@@ -1,18 +1,13 @@
|
||||
#+TITLE: SKILL: Peripheral Vision (org-skill-peripheral-vision.org)
|
||||
#+AUTHOR: Agent
|
||||
#+FILETAGS: :harness:peripheral:context:
|
||||
#+PROPERTY: header-args:lisp :tangle org-skill-peripheral-vision.lisp
|
||||
#+PROPERTY: header-args:lisp :tangle %%SKILLS_DIR%%/org-skill-peripheral-vision.lisp
|
||||
|
||||
* Overview
|
||||
The *Peripheral Vision* skill enhances the context engine with high-level summaries of distant memory nodes.
|
||||
|
||||
* Implementation
|
||||
|
||||
** Package Context
|
||||
#+begin_src lisp
|
||||
(in-package :opencortex)
|
||||
#+end_src
|
||||
|
||||
** Context Logic
|
||||
#+begin_src lisp
|
||||
(defun peripheral-vision-summarize (obj-id)
|
||||
@@ -20,7 +15,7 @@ The *Peripheral Vision* skill enhances the context engine with high-level summar
|
||||
(let ((obj (lookup-object obj-id)))
|
||||
(if obj
|
||||
(format nil "Node: ~a (~a)" (getf (org-object-attributes obj) :TITLE) obj-id)
|
||||
"[Unknown Node]))
|
||||
"[Unknown Node]")))
|
||||
#+end_src
|
||||
|
||||
** Skill Registration
|
||||
|
||||
@@ -1,404 +0,0 @@
|
||||
(in-package :opencortex)
|
||||
|
||||
(defvar *policy-invariant-priorities*
|
||||
'((:transparency . 500)
|
||||
(:autonomy . 400)
|
||||
(:bloat . 300)
|
||||
(:modularity . 250)
|
||||
(:mentorship . 200)
|
||||
(:sustainability . 100))
|
||||
"Priority alist for policy invariant conflict resolution.
|
||||
Higher numbers take precedence.
|
||||
|
||||
When two invariants conflict, the higher priority wins.
|
||||
Example: Modularity (250) takes precedence over Mentorship (200),
|
||||
meaning a change that would fatten the harness is blocked
|
||||
even if it would be educational.")
|
||||
|
||||
(defvar *proprietary-domain-watchlist*
|
||||
'("googleapis.com" "api.openai.com" "anthropic.com" "api.groq.com" "openrouter.ai")
|
||||
"Domains representing centralized, proprietary control.
|
||||
|
||||
Actions targeting these are logged as autonomy debt, not hard-blocked.
|
||||
This is because tactical gateway usage (Telegram, Signal, OpenRouter)
|
||||
is permitted under the strategic mandate for autonomy.
|
||||
|
||||
Strategic goal: Replace all proprietary APIs with local alternatives.
|
||||
Tactical reality: Use what's available while building toward that goal.")
|
||||
|
||||
(defvar *policy-max-skill-size-chars* 50000
|
||||
"Maximum recommended size for a skill file tangled from an Org note.
|
||||
|
||||
This is a soft limit—the check warns but does not block.
|
||||
A large, well-documented skill is acceptable; a small, poorly-documented
|
||||
one that adds unnecessary complexity is not.")
|
||||
|
||||
(defvar *modularity-protected-paths*
|
||||
'("harness/" "opencortex.asd")
|
||||
"Paths that constitute the unbreakable core of the system.
|
||||
|
||||
Any action targeting these paths must include a :modularity-justification
|
||||
explaining why the change cannot be implemented as a skill.
|
||||
|
||||
The Thin Harness principle: What belongs in the harness?
|
||||
- Core signal processing (Perceive-Reason-Act loop)
|
||||
- Memory and persistence primitives
|
||||
- Protocol definition and validation
|
||||
- Skills register and dispatch
|
||||
|
||||
What belongs in skills?
|
||||
- Policy and security
|
||||
- LLM integration
|
||||
- Domain-specific functionality
|
||||
- New actuators")
|
||||
|
||||
(defvar *mentorship-required-actions*
|
||||
'(:create-skill :eval :modify-file :write-file :replace
|
||||
:rename-file :delete-file :shell :create-note)
|
||||
"Actions that trigger the Mentorship invariant.
|
||||
|
||||
These are high-impact actions that should come with explanations
|
||||
not just for the user, but for future debugging and maintenance.")
|
||||
|
||||
(defvar *cloud-only-backends* '(:openrouter :openai :anthropic :groq :gemini-api)
|
||||
"Backends requiring internet connection and external infrastructure.
|
||||
|
||||
These are acceptable as fallbacks when local inference is unavailable,
|
||||
but should be logged as sustainability debt for tracking purposes.")
|
||||
|
||||
|
||||
|
||||
(defun policy-check-transparency (action context)
|
||||
(defun policy-check-transparency (action context)
|
||||
"Ensures the action is inspectable and user-facing actions carry an explanation.
|
||||
|
||||
TRANSPARENCY CHECK:
|
||||
1. Action must be a valid plist (not opaque data)
|
||||
2. User-facing actions (:cli, :tui, :emacs) must include :explanation
|
||||
3. Heartbeat and handshake messages are exempt (they're system status)
|
||||
|
||||
Returns the action if clean, or a blocking LOG event if violated."
|
||||
|
||||
(declare (ignore context))
|
||||
|
||||
;; Check 1: Action must be a valid plist
|
||||
(unless (listp action)
|
||||
(return-from policy-check-transparency
|
||||
(list :type :LOG
|
||||
:payload (list :level :error
|
||||
:text "POLICY [Transparency]: Action is not a valid plist. Rejected."))))
|
||||
|
||||
(let* ((payload (getf action :payload))
|
||||
(target (or (getf action :target) (getf action :TARGET)))
|
||||
(explanation (or (getf payload :explanation)
|
||||
(getf payload :EXPLANATION)
|
||||
(getf payload :rationale)
|
||||
(getf payload :RATIONALE))))
|
||||
|
||||
;; Check 2: User-facing actions require explanation
|
||||
(when (and (member target '(:cli :tui :emacs :EMACS :CLI :TUI))
|
||||
(not explanation)
|
||||
(not (member (getf payload :action)
|
||||
'(:handshake :heartbeat :status-update))))
|
||||
(return-from policy-check-transparency
|
||||
(list :type :LOG
|
||||
:payload (list :level :error
|
||||
:text "POLICY [Transparency]: User-facing action missing :explanation. Blocked."))))
|
||||
|
||||
action))
|
||||
|
||||
(defun policy-scan-proprietary-references (action)
|
||||
"Scans ACTION text fields for proprietary domain references.
|
||||
|
||||
Searches in:
|
||||
- :text and :TEXT in payload
|
||||
- :cmd and :CMD in payload
|
||||
- :cmd in args (for shell tool calls)
|
||||
|
||||
Returns the first matched domain, or NIL if clean."
|
||||
|
||||
(let* ((payload (getf action :payload))
|
||||
(text (or (getf payload :text) (getf payload :TEXT) ""))
|
||||
(cmd (or (getf payload :cmd)
|
||||
(getf payload :CMD)
|
||||
(when (equal (getf payload :tool) "shell")
|
||||
(getf (getf payload :args) :cmd))
|
||||
""))
|
||||
(haystack (concatenate 'string text cmd)))
|
||||
|
||||
(dolist (domain *proprietary-domain-watchlist* nil)
|
||||
(when (search domain haystack)
|
||||
(return domain)))))
|
||||
|
||||
(defun policy-check-autonomy (action context)
|
||||
"Flags actions that reference proprietary domains.
|
||||
|
||||
Does NOT block the action—this is a warning, not a veto.
|
||||
The agent can use proprietary services tactically, but must
|
||||
be aware that each usage is a step away from full autonomy.
|
||||
|
||||
Returns a warning LOG if proprietary reference detected,
|
||||
or the original action if clean."
|
||||
|
||||
(declare (ignore context))
|
||||
|
||||
(let ((domain (policy-scan-proprietary-references action)))
|
||||
|
||||
(if domain
|
||||
(progn
|
||||
(harness-log "POLICY [Autonomy]: Detected proprietary reference '~a'. Flagged for replacement." domain)
|
||||
;; Return a warning log but DO NOT block the action
|
||||
(list :type :LOG
|
||||
:payload (list :level :warn
|
||||
:text (format nil "Autonomy Debt: Action references proprietary domain '~a'. Consider a local alternative." domain)
|
||||
:original-action action)))
|
||||
|
||||
action)))
|
||||
|
||||
(defun policy-check-bloat (action context)
|
||||
"Warns if a :create-skill action exceeds the bloat threshold.
|
||||
|
||||
Size alone is not proof of complexity—a 50KB skill that's well-designed
|
||||
is better than a 5KB skill that's spaghetti. This check flags for review,
|
||||
not automatic rejection.
|
||||
|
||||
Returns a warning LOG if threshold exceeded, or original action if clean."
|
||||
|
||||
(declare (ignore context))
|
||||
|
||||
(let* ((payload (getf action :payload))
|
||||
(act (getf payload :action))
|
||||
(content (getf payload :content)))
|
||||
|
||||
(when (and (eq act :create-skill)
|
||||
(stringp content)
|
||||
(> (length content) *policy-max-skill-size-chars*))
|
||||
|
||||
(harness-log "POLICY [Bloat]: Proposed skill is ~a chars. Exceeds ~a char threshold."
|
||||
(length content) *policy-max-skill-size-chars*)
|
||||
|
||||
(return-from policy-check-bloat
|
||||
(list :type :LOG
|
||||
:payload (list :level :warn
|
||||
:text (format nil "Bloat Warning: Proposed skill (~a chars) exceeds ~a char threshold. Review for earned complexity."
|
||||
(length content) *policy-max-skill-size-chars*)
|
||||
:original-action action))))
|
||||
|
||||
action))
|
||||
|
||||
(defun policy-check-modularity (action context)
|
||||
"Blocks modifications to the system's protected core unless justified.
|
||||
|
||||
MODULARITY CHECK:
|
||||
1. If the action targets a protected path
|
||||
2. And no :modularity-justification is provided
|
||||
3. Then block with an explanation
|
||||
|
||||
The justification should explain WHY the change cannot be a skill.
|
||||
Common valid reasons:
|
||||
- The change fixes a bug in the harness itself
|
||||
- The change adds a primitive that skills cannot implement
|
||||
- The change is required for security hardening
|
||||
|
||||
Invalid reasons:
|
||||
- 'It's easier to modify the harness'
|
||||
- 'Skills are too slow'
|
||||
- 'I want to keep it all in one place'"
|
||||
|
||||
(declare (ignore context))
|
||||
|
||||
(let* ((payload (getf action :payload))
|
||||
(target-file (or (getf payload :file)
|
||||
(getf payload :filename)))
|
||||
(justification (or (getf payload :modularity-justification)
|
||||
(getf payload :MODULARITY-JUSTIFICATION))))
|
||||
|
||||
(when (and target-file
|
||||
(some (lambda (path) (search path target-file))
|
||||
*modularity-protected-paths*)
|
||||
(not justification))
|
||||
|
||||
(return-from policy-check-modularity
|
||||
(list :type :LOG
|
||||
:payload (list :level :error
|
||||
:text "POLICY [Modularity]: Modification to protected core path blocked. Provide :modularity-justification explaining why this cannot be a skill."
|
||||
:blocked-path target-file))))
|
||||
|
||||
action))
|
||||
|
||||
(defun policy-check-mentorship (action context)
|
||||
"Blocks high-impact actions that lack a mentorship note.
|
||||
|
||||
MENTORSHIP CHECK:
|
||||
1. If the action is in *mentorship-required-actions*
|
||||
2. Or if the action calls shell/eval/repair-file tools
|
||||
3. Then require :mentorship-note explaining what and why
|
||||
|
||||
The mentorship note should be:
|
||||
- Concise (1-2 sentences)
|
||||
- Educational (explain the principle, not just the action)
|
||||
- Actionable (help the user understand the outcome)"
|
||||
|
||||
(declare (ignore context))
|
||||
|
||||
(let* ((payload (getf action :payload))
|
||||
(act (or (getf payload :action)
|
||||
(getf action :action)))
|
||||
(note (or (getf payload :mentorship-note)
|
||||
(getf payload :MENTORSHIP-NOTE)))
|
||||
(target (or (getf action :target)
|
||||
(getf action :TARGET)))
|
||||
(tool (when (eq target :tool)
|
||||
(getf payload :tool))))
|
||||
|
||||
(when (or (member act *mentorship-required-actions*)
|
||||
(member tool '("shell" "eval" "repair-file")))
|
||||
|
||||
(unless note
|
||||
(return-from policy-check-mentorship
|
||||
(list :type :LOG
|
||||
:payload (list :level :error
|
||||
:text "POLICY [Mentorship]: High-impact action missing :mentorship-note. Explain what you are doing and why. Blocked.")))))
|
||||
|
||||
action))
|
||||
|
||||
(defun policy-check-sustainability (action context)
|
||||
"Logs sustainability debt when action relies on cloud-only infrastructure.
|
||||
|
||||
Does NOT block—this is informational, not prohibitive.
|
||||
Cloud usage is acceptable tactically (when local models fail),
|
||||
but every cloud usage should be a conscious decision, not a default."
|
||||
|
||||
(let* ((payload (getf context :payload))
|
||||
(backend (getf payload :backend))
|
||||
(provider (getf payload :provider)))
|
||||
|
||||
(when (or (member backend *cloud-only-backends*)
|
||||
(member provider *cloud-only-backends*))
|
||||
|
||||
(harness-log "POLICY [Sustainability]: Cloud provider '~a' used. Logged as sustainability debt."
|
||||
(or backend provider))
|
||||
|
||||
(return-from policy-check-sustainability
|
||||
(list :type :LOG
|
||||
:payload (list :level :warn
|
||||
:text (format nil "Sustainability Debt: Reliance on cloud provider '~a'. Consider Ollama or local inference."
|
||||
(or backend provider))))))
|
||||
|
||||
action)))
|
||||
|
||||
(defun policy-explain (invariant-key message &optional original-action)
|
||||
"Formats a policy decision into an auditable explanation plist.
|
||||
|
||||
INVARIANT-KEY is one of:
|
||||
:transparency, :autonomy, :bloat, :modularity, :mentorship, :sustainability
|
||||
|
||||
MESSAGE is a human-readable string explaining the decision.
|
||||
|
||||
ORIGINAL-ACTION is the action that was blocked or modified.
|
||||
|
||||
Returns a REQUEST plist addressed to the original source,
|
||||
containing the explanation and original action for transparency."
|
||||
|
||||
(list :type :REQUEST
|
||||
:target (or (ignore-errors
|
||||
(getf (getf original-action :meta) :source))
|
||||
:cli)
|
||||
:payload (list :action :message
|
||||
:text (format nil "[POLICY ~a] ~a" invariant-key message)
|
||||
:explanation (format nil "Invariant: ~a | Rationale: ~a"
|
||||
invariant-key message)
|
||||
:original-action original-action)))
|
||||
|
||||
(defun policy-run-invariant-checks (action context)
|
||||
"Runs all invariant checks in priority order.
|
||||
|
||||
Priority order (from *policy-invariant-priorities*):
|
||||
1. Transparency (500) - blocks non-transparent actions
|
||||
2. Autonomy (400) - warns on proprietary dependencies
|
||||
3. Bloat (300) - warns on oversized skills
|
||||
4. Modularity (250) - blocks unprotected core modifications
|
||||
5. Mentorship (200) - blocks unexplained high-impact actions
|
||||
6. Sustainability (100) - warns on cloud dependencies
|
||||
|
||||
Returns:
|
||||
- The final action (possibly modified by checks)
|
||||
- A blocking LOG event (if any check returned :error level)
|
||||
- A warning wrapper (if checks returned :warn level but no blocks)"
|
||||
|
||||
(let ((checks '(policy-check-transparency
|
||||
policy-check-autonomy
|
||||
policy-check-bloat
|
||||
policy-check-modularity
|
||||
policy-check-mentorship
|
||||
policy-check-sustainability)))
|
||||
|
||||
(dolist (check-fn checks action)
|
||||
(let ((result (funcall check-fn action context)))
|
||||
|
||||
;; If the check returned a LOG/EVENT, interpret it
|
||||
(when (and (listp result)
|
||||
(member (getf result :type) '(:LOG :EVENT)))
|
||||
|
||||
(let ((level (getf (getf result :payload) :level)))
|
||||
|
||||
(cond
|
||||
;; Hard block: error level stops processing immediately
|
||||
((eq level :error)
|
||||
(return-from policy-run-invariant-checks result))
|
||||
|
||||
;; Soft warning: log but continue with original action
|
||||
(t
|
||||
(harness-log "~a" (getf (getf result :payload) :text))))))))))
|
||||
(defun policy-find-engineering-standards-gate ()
|
||||
"Searches for the Engineering Standards gate across known jailed package names.
|
||||
|
||||
The standards skill may be in opencortex-contrib submodule,
|
||||
so we search multiple possible package names with graceful fallback.
|
||||
|
||||
Returns the function symbol, or NIL if unavailable."
|
||||
|
||||
(dolist (pkg-name '(:opencortex.skills.org-skill-engineering-standards
|
||||
:opencortex.skills.org-skill-engineering
|
||||
:opencortex.skills.engineering-standards)
|
||||
nil)
|
||||
|
||||
(let ((pkg (find-package pkg-name)))
|
||||
(when pkg
|
||||
(let ((sym (find-symbol "ENGINEERING-STANDARDS-GATE" pkg)))
|
||||
(when (and sym (fboundp sym))
|
||||
(return (symbol-function sym))))))))
|
||||
|
||||
(defun policy-deterministic-gate (action context)
|
||||
"The main policy gate entry point.
|
||||
|
||||
This function is registered as the deterministic-fn for the policy skill.
|
||||
It runs invariant checks, then delegates to engineering standards if loaded.
|
||||
|
||||
IMPORTANT: Never returns NIL silently. Always returns either:
|
||||
- An action (possibly modified)
|
||||
- A blocking LOG event with explanation
|
||||
- A warning wrapper with explanation"
|
||||
|
||||
;; Step 1: Run invariant checks
|
||||
(let ((current-action (policy-run-invariant-checks action context)))
|
||||
|
||||
;; Step 2: If an invariant blocked the action, stop here
|
||||
(when (and (listp current-action)
|
||||
(member (getf current-action :type) '(:LOG :EVENT))
|
||||
(eq (getf (getf current-action :payload) :level) :error))
|
||||
|
||||
(return-from policy-deterministic-gate current-action))
|
||||
|
||||
;; Step 3: Delegate to Engineering Standards if loaded
|
||||
(let ((eng-gate (policy-find-engineering-standards-gate)))
|
||||
(when eng-gate
|
||||
(setf current-action (funcall eng-gate current-action context))))
|
||||
|
||||
current-action))
|
||||
|
||||
(defskill :skill-policy
|
||||
:priority 500
|
||||
:trigger (lambda (ctx) (declare (ignore ctx)) t)
|
||||
:probabilistic nil
|
||||
:deterministic #'policy-deterministic-gate)
|
||||
@@ -1,18 +1,13 @@
|
||||
#+TITLE: SKILL: Policy (org-skill-policy.org)
|
||||
#+AUTHOR: Agent
|
||||
#+FILETAGS: :system:policy:constitutional:
|
||||
#+PROPERTY: header-args:lisp :tangle org-skill-policy.lisp
|
||||
#+PROPERTY: header-args:lisp :tangle %%SKILLS_DIR%%/org-skill-policy.lisp
|
||||
|
||||
* Overview
|
||||
The *Policy Skill* is the constitutional layer of OpenCortex. It enforces foundational invariants like transparency and autonomy on all proposed actions.
|
||||
|
||||
* Implementation
|
||||
|
||||
** Package Context
|
||||
#+begin_src lisp
|
||||
(in-package :opencortex)
|
||||
#+end_src
|
||||
|
||||
** Policy Logic (policy-check)
|
||||
#+begin_src lisp
|
||||
(defun policy-check (action context)
|
||||
@@ -23,10 +18,10 @@ The *Policy Skill* is the constitutional layer of OpenCortex. It enforces founda
|
||||
(if (and explanation (stringp explanation) (> (length explanation) 10))
|
||||
action
|
||||
(progn
|
||||
(harness-log "POLICY VIOLATION: Action lacks sufficient explanation.
|
||||
(harness-log "POLICY VIOLATION: Action lacks sufficient explanation.")
|
||||
(list :type :LOG
|
||||
:payload (list :level :warn
|
||||
:text "Action blocked: Missing or insufficient :explanation. Please justify your reasoning.)))))
|
||||
:text "Action blocked: Missing or insufficient :explanation. Please justify your reasoning."))))))
|
||||
#+end_src
|
||||
|
||||
** Skill Registration
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
(defun validate-communication-protocol-schema (msg)
|
||||
"Returns T if the message is valid, NIL (and signals error) otherwise.")
|
||||
|
||||
(in-package :opencortex)
|
||||
|
||||
(defun validate-communication-protocol-schema (msg)
|
||||
"Strict structural validation for incoming communication protocol messages."
|
||||
(unless (listp msg)
|
||||
(error "Communication Protocol Schema Error: Message must be a property list (got ~s)" (type-of msg)))
|
||||
|
||||
(let ((type (let ((raw (proto-get msg :type))) (if (keywordp raw) (intern (string-upcase (string raw)) :keyword) raw))))
|
||||
(unless (member type '(:REQUEST :EVENT :RESPONSE :LOG :STATUS :CHAT))
|
||||
(progn (harness-log "REJECTED MSG: ~s" msg) (error "Communication Protocol Schema Error: Invalid message type '~a'" type)))
|
||||
|
||||
(case type
|
||||
(:REQUEST
|
||||
;; Allow missing :target if :source is present in :meta, since reason-gate
|
||||
;; will infer :target from :source downstream. This preserves "equality of
|
||||
;; clients" — gateways need not duplicate routing logic.
|
||||
(let ((target (proto-get msg :target))
|
||||
(source (proto-get (proto-get msg :meta) :source)))
|
||||
(unless (or target source)
|
||||
(error "Communication Protocol Schema Error: REQUEST missing mandatory :target and no :source in :meta to infer it"))
|
||||
(unless (proto-get msg :payload)
|
||||
(error "Communication Protocol Schema Error: REQUEST missing mandatory :payload"))))
|
||||
|
||||
(:EVENT
|
||||
(let ((payload (proto-get msg :payload)))
|
||||
(unless (and payload (listp payload))
|
||||
(error "Communication Protocol Schema Error: EVENT missing or invalid :payload"))
|
||||
(unless (or (proto-get payload :action) (proto-get payload :sensor))
|
||||
(error "Communication Protocol Schema Error: EVENT payload must contain :action or :sensor"))))
|
||||
|
||||
(:RESPONSE
|
||||
(unless (proto-get msg :payload)
|
||||
(error "Communication Protocol Schema Error: RESPONSE missing mandatory :payload"))))
|
||||
|
||||
t))
|
||||
|
||||
(defskill :skill-communication-protocol-validator
|
||||
:priority 95
|
||||
:trigger (lambda (ctx) (member (getf (getf ctx :payload) :sensor) '(:protocol-received)))
|
||||
:probabilistic nil
|
||||
:deterministic (lambda (action ctx)
|
||||
(declare (ignore ctx))
|
||||
(validate-communication-protocol-schema action)
|
||||
action))
|
||||
@@ -1,18 +1,13 @@
|
||||
#+TITLE: SKILL: Protocol Validator (org-skill-protocol-validator.org)
|
||||
#+AUTHOR: Agent
|
||||
#+FILETAGS: :system:protocol:validation:
|
||||
#+PROPERTY: header-args:lisp :tangle org-skill-protocol-validator.lisp
|
||||
#+PROPERTY: header-args:lisp :tangle %%SKILLS_DIR%%/org-skill-protocol-validator.lisp
|
||||
|
||||
* Overview
|
||||
The *Protocol Validator* skill enforces strict schema compliance for all internal and external communication.
|
||||
|
||||
* Implementation
|
||||
|
||||
** Package Context
|
||||
#+begin_src lisp
|
||||
(in-package :opencortex)
|
||||
#+end_src
|
||||
|
||||
** Validation Logic
|
||||
#+begin_src lisp
|
||||
(defun protocol-validate (msg)
|
||||
|
||||
193
skills/org-skill-repl.org
Normal file
193
skills/org-skill-repl.org
Normal file
@@ -0,0 +1,193 @@
|
||||
#+TITLE: SKILL: REPL (org-skill-repl.org)
|
||||
#+AUTHOR: Agent
|
||||
#+FILETAGS: :system:repl:interactive:debug:
|
||||
#+PROPERTY: header-args:lisp :tangle %%SKILLS_DIR%%/org-skill-repl.lisp
|
||||
|
||||
* 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 utils-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
|
||||
#+begin_src lisp
|
||||
(in-package :opencortex)
|
||||
|
||||
(defvar *repl-package* :opencortex
|
||||
"Default package for REPL evaluations.")
|
||||
|
||||
(defvar *repl-history* nil
|
||||
"History of evaluated forms for session continuity.")
|
||||
|
||||
(defvar *repl-variables* (make-hash-table :test #'eq)
|
||||
"Cache of bound variables for inspection.")
|
||||
#+end_src
|
||||
|
||||
** Core Evaluation
|
||||
#+begin_src lisp
|
||||
(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 :opencortex))))
|
||||
(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))))))
|
||||
#+end_src
|
||||
|
||||
** Variable Inspection
|
||||
#+begin_src lisp
|
||||
(defun repl-inspect (symbol-name &key (package *repl-package*))
|
||||
"Inspect a variable's value and structure."
|
||||
(let* ((pkg (or (find-package package) (find-package :opencortex)))
|
||||
(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)))))
|
||||
#+end_src
|
||||
|
||||
** List Bound Variables
|
||||
#+begin_src lisp
|
||||
(defun repl-list-vars (&key (package *repl-package*))
|
||||
"List all bound variables in the package."
|
||||
(let* ((pkg (or (find-package package) (find-package :opencortex)))
|
||||
(vars nil))
|
||||
(do-symbols (sym pkg)
|
||||
(when (boundp sym)
|
||||
(push (format nil "~a" sym) vars)))
|
||||
(sort vars #'string<)))
|
||||
#+end_src
|
||||
|
||||
** Load File into Image
|
||||
#+begin_src lisp
|
||||
(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))))
|
||||
#+end_src
|
||||
|
||||
** Package Switching
|
||||
#+begin_src lisp
|
||||
(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))))
|
||||
#+end_src
|
||||
|
||||
** Help/Info
|
||||
#+begin_src lisp
|
||||
(defun repl-help ()
|
||||
"Return available REPL commands."
|
||||
(format nil "~%
|
||||
REPL Skill Commands:
|
||||
-------------------
|
||||
(repl-eval \"code\" :package :opencortex)
|
||||
- Evaluate Lisp code, returns (values result output error)
|
||||
|
||||
(repl-inspect \"symbol\" :package :opencortex)
|
||||
- Inspect a variable or function
|
||||
|
||||
(repl-list-vars :package :opencortex)
|
||||
- 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
|
||||
"))
|
||||
#+end_src
|
||||
|
||||
* Phase D: Verification
|
||||
|
||||
** Basic Evaluation Test
|
||||
#+begin_src lisp :tangle no
|
||||
(test test-repl-eval-simple
|
||||
"Test basic arithmetic evaluation."
|
||||
(multiple-value-bind (result output error)
|
||||
(opencortex:repl-eval "(+ 1 2)")
|
||||
(is (string= result "3"))
|
||||
(is (null error))))
|
||||
#+end_src
|
||||
|
||||
** Error Handling Test
|
||||
#+begin_src lisp :tangle no
|
||||
(test test-repl-eval-error
|
||||
"Test that errors are caught and returned."
|
||||
(multiple-value-bind (result output error)
|
||||
(opencortex:repl-eval "(+ 1 \"string\")")
|
||||
(is (null result))
|
||||
(is (not (null error)))))
|
||||
#+end_src
|
||||
|
||||
* Phase E: Lifecycle
|
||||
The REPL skill loads at priority 200 (after diagnostics at 100, before utils-lisp at 400).
|
||||
|
||||
** Skill Registration
|
||||
#+begin_src lisp
|
||||
(defskill :skill-repl
|
||||
:priority 200
|
||||
:trigger (lambda (ctx) (declare (ignore ctx)) nil)
|
||||
:deterministic (lambda (action ctx) (declare (ignore action ctx)) nil))
|
||||
#+end_src
|
||||
@@ -1,108 +0,0 @@
|
||||
(in-package :opencortex)
|
||||
|
||||
(defvar *scribe-last-checkpoint* 0
|
||||
"The universal-time of the last successful distillation run.")
|
||||
|
||||
(defun scribe-load-state ()
|
||||
"Loads the scribe checkpoint from the state directory."
|
||||
(let ((state-file (uiop:merge-pathnames* "state/scribe-checkpoint.lisp" (asdf:system-source-directory :opencortex))))
|
||||
(if (uiop:file-exists-p state-file)
|
||||
(setf *scribe-last-checkpoint* (read-from-string (uiop:read-file-string state-file)))
|
||||
(setf *scribe-last-checkpoint* 0))))
|
||||
|
||||
(defun scribe-save-state ()
|
||||
"Saves the current universal-time as the new checkpoint."
|
||||
(let ((state-file (uiop:merge-pathnames* "state/scribe-checkpoint.lisp" (asdf:system-source-directory :opencortex))))
|
||||
(ensure-directories-exist state-file)
|
||||
(with-open-file (out state-file :direction :output :if-exists :supersede)
|
||||
(format out "~a" (get-universal-time)))))
|
||||
|
||||
(defun scribe-get-distillable-nodes ()
|
||||
"Returns a list of org-objects from the daily/ folder that require distillation."
|
||||
(let ((results nil))
|
||||
(maphash (lambda (id obj)
|
||||
(declare (ignore id))
|
||||
(let* ((attrs (org-object-attributes obj))
|
||||
(tags (getf attrs :TAGS))
|
||||
(type (org-object-type obj))
|
||||
(version (org-object-version obj)))
|
||||
(when (and (eq type :HEADLINE)
|
||||
(> version *scribe-last-checkpoint*)
|
||||
(not (member "@personal" tags :test #'string-equal)))
|
||||
(push obj results))))
|
||||
*memory*)
|
||||
results))
|
||||
|
||||
(defun probabilistic-skill-scribe (context)
|
||||
"Generates the extraction prompt for the Scribe."
|
||||
(let* ((payload (getf context :payload))
|
||||
(nodes (scribe-get-distillable-nodes)))
|
||||
(if nodes
|
||||
(let ((text-to-process ""))
|
||||
(dolist (node nodes)
|
||||
(setf text-to-process (concatenate 'string text-to-process
|
||||
(format nil "ID: ~a~%TITLE: ~a~%CONTENT: ~a~%---~%"
|
||||
(org-object-id node)
|
||||
(getf (org-object-attributes node) :TITLE)
|
||||
(org-object-content node)))))
|
||||
(format nil "DISTILLATION TASK:
|
||||
Below are raw chronological logs from my daily journal.
|
||||
Extract ATOMIC EVERGREEN NOTES from this text.
|
||||
|
||||
RULES:
|
||||
1. One note per distinct concept.
|
||||
2. Output a list of Lisp plists: ((:title \"...\" :content \"...\" :source-id \"...\") ...)
|
||||
3. The content should be in Org-mode format.
|
||||
4. Keep titles descriptive and snake_case.
|
||||
|
||||
TEXT:
|
||||
~a" text-to-process))
|
||||
nil)))
|
||||
|
||||
(defun scribe-commit-notes (proposals)
|
||||
"Writes proposed atomic notes to the notes/ directory. Appends if the note exists."
|
||||
(let ((notes-dir (uiop:merge-pathnames* "notes/" (asdf:system-source-directory :opencortex))))
|
||||
(ensure-directories-exist notes-dir)
|
||||
(dolist (note proposals)
|
||||
(let* ((title (getf note :title))
|
||||
(content (getf note :content))
|
||||
(source-id (getf note :source-id))
|
||||
(filename (format nil "~a.org" (string-downcase (cl-ppcre:regex-replace-all " " title "_"))))
|
||||
(path (merge-pathnames filename notes-dir)))
|
||||
(if (uiop:file-exists-p path)
|
||||
(with-open-file (out path :direction :output :if-exists :append)
|
||||
(format out "~%~%* Appended insight from ~a~%~a" source-id content))
|
||||
(with-open-file (out path :direction :output :if-exists :supersede)
|
||||
(format out ":PROPERTIES:~%:ID: ~a~%:SOURCE_ID: ~a~%:END:~%#+TITLE: ~a~%~%~a"
|
||||
(org-id-new) source-id title content)))
|
||||
(harness-log "SCRIBE: Processed evergreen note ~a" filename)))))
|
||||
|
||||
(defun verify-skill-scribe (action context)
|
||||
"Executes the note creation and marks source nodes as distilled."
|
||||
(declare (ignore context))
|
||||
(let ((data (cond ((and (listp action) (eq (getf action :type) :REQUEST))
|
||||
(getf (getf action :payload) :payload))
|
||||
((and (listp action) (not (member (getf action :type) '(:LOG :EVENT))))
|
||||
action)
|
||||
(t nil))))
|
||||
(when data
|
||||
(harness-log "SCRIBE: Committing ~a atomic notes..." (length data))
|
||||
(scribe-commit-notes data)
|
||||
(scribe-save-state)
|
||||
(harness-log "SCRIBE: Distillation complete.")
|
||||
;; Return a log event to stop the loop
|
||||
(list :type :LOG :payload (list :text "Distillation successful.")))))
|
||||
|
||||
(defskill :skill-scribe
|
||||
:priority 50
|
||||
:trigger (lambda (ctx)
|
||||
(let* ((payload (getf ctx :payload))
|
||||
(sensor (getf payload :sensor)))
|
||||
(and (eq sensor :heartbeat)
|
||||
;; Only run once per hour to check if we need to distill
|
||||
(> (- (get-universal-time) *scribe-last-checkpoint*) 3600)
|
||||
(scribe-get-distillable-nodes))))
|
||||
:probabilistic #'probabilistic-skill-scribe
|
||||
:deterministic #'verify-skill-scribe)
|
||||
|
||||
(scribe-load-state)
|
||||
@@ -1,18 +1,13 @@
|
||||
#+TITLE: SKILL: Scribe (org-skill-scribe.org)
|
||||
#+AUTHOR: Agent
|
||||
#+FILETAGS: :skill:scribe:documentation:
|
||||
#+PROPERTY: header-args:lisp :tangle org-skill-scribe.lisp
|
||||
#+PROPERTY: header-args:lisp :tangle %%SKILLS_DIR%%/org-skill-scribe.lisp
|
||||
|
||||
* Overview
|
||||
The *Scribe Skill* manages the agent's internal documentation and logs.
|
||||
|
||||
* Implementation
|
||||
|
||||
** Package Context
|
||||
#+begin_src lisp
|
||||
(in-package :opencortex)
|
||||
#+end_src
|
||||
|
||||
** Documentation Logic
|
||||
#+begin_src lisp
|
||||
(defun scribe-log-event (signal)
|
||||
|
||||
@@ -1,184 +0,0 @@
|
||||
(in-package :opencortex)
|
||||
|
||||
(defun self-edit-count-char (char string)
|
||||
"Counts occurrences of CHAR in STRING."
|
||||
(loop for c across string count (char= c char)))
|
||||
|
||||
(defun self-edit-balance-parens (code)
|
||||
"Balances parentheses in CODE."
|
||||
(let ((opens (self-edit-count-char #\( code))
|
||||
(closes (self-edit-count-char #\) code)))
|
||||
(cond
|
||||
((= opens closes) code)
|
||||
((> opens closes)
|
||||
(concatenate 'string code (make-string (- opens closes) :initial-element #\))))
|
||||
((> closes opens)
|
||||
(concatenate 'string (make-string (- closes opens) :initial-element #\() code)))))
|
||||
|
||||
(defun copy-hash-table (table)
|
||||
"Returns a shallow copy of a hash table."
|
||||
(let ((new-table (make-hash-table :test (hash-table-test table)
|
||||
:size (hash-table-count table))))
|
||||
(maphash (lambda (k v) (setf (gethash k new-table) v)) table)
|
||||
new-table))
|
||||
|
||||
(defun self-edit-parse-location (context)
|
||||
"Extracts file and line from error context payload."
|
||||
(let* ((payload (getf context :payload))
|
||||
(message (getf payload :message ""))
|
||||
(file (or (getf payload :file)
|
||||
(when (search "file" message)
|
||||
(car (cl-ppcre:all-matches-as-strings "[a-zA-Z0-9_/-]+\\.lisp" message)))))
|
||||
(line (or (getf payload :line)
|
||||
(let ((match (cl-ppcre:scan-to-strings "line.?(\\d+)" message)))
|
||||
(when match (parse-integer (aref match 0)))))))
|
||||
(list :file file :line line)))
|
||||
|
||||
(defun self-edit-apply (target-file old-code new-code)
|
||||
"Applies surgical edit to TARGET-FILE: replace OLD-CODE with NEW-CODE.
|
||||
Returns list with :status and :message keys."
|
||||
(unless (uiop:file-exists-p target-file)
|
||||
(return-from self-edit-apply
|
||||
(list :status :error :message (format nil "File not found: ~a" target-file))))
|
||||
|
||||
(snapshot-memory)
|
||||
(harness-log "SELF-EDIT: Attempting surgical fix on ~a..." target-file)
|
||||
|
||||
(let ((original-content (uiop:read-file-string target-file)))
|
||||
(handler-case
|
||||
(if (search old-code original-content)
|
||||
(let ((new-content (cl-ppcre:regex-replace-all
|
||||
(cl-ppcre:quote-meta-chars old-code)
|
||||
original-content
|
||||
new-code)))
|
||||
(with-open-file (out target-file :direction :output :if-exists :supersede)
|
||||
(write-string new-content out))
|
||||
(harness-log "SELF-EDIT: Edit applied successfully.")
|
||||
(list :status :success :message "Edit applied."))
|
||||
(progn
|
||||
(harness-log "SELF-EDIT: Pattern not found in file.")
|
||||
(list :status :error :message "Pattern not found in file.")))
|
||||
(error (c)
|
||||
(harness-log "SELF-EDIT: Edit failed: ~a" c)
|
||||
(rollback-memory 0)
|
||||
(list :status :error :message (format nil "Edit failed: ~a" c))))))
|
||||
|
||||
(def-cognitive-tool :self-edit
|
||||
"Applies a surgical code modification to a file with automatic rollback on failure."
|
||||
((:file :type :string :description "Path to the target file")
|
||||
(:old :type :string :description "The code block to find")
|
||||
(:new :type :string :description "The code block to replace with"))
|
||||
:body (lambda (args)
|
||||
(let* ((file (getf args :file))
|
||||
(old (getf args :old))
|
||||
(new (getf args :new)))
|
||||
(self-edit-apply file old new))))
|
||||
|
||||
(defskill :skill-self-edit
|
||||
:priority 95
|
||||
:trigger (lambda (ctx)
|
||||
(let ((sensor (getf (getf ctx :payload) :sensor)))
|
||||
(member sensor '(:syntax-error :repair-request :self-edit))))
|
||||
:probabilistic (lambda (ctx)
|
||||
(let ((sensor (getf (getf ctx :payload) :sensor)))
|
||||
(cond
|
||||
((eq sensor :syntax-error)
|
||||
"You are the Self-Edit Agent. A syntax error occurred.
|
||||
Provide a fixed version of the code as a lisp form.")
|
||||
((eq sensor :repair-request)
|
||||
"You are the Self-Edit Agent. Apply the surgical fix to the file.")
|
||||
(t nil))))
|
||||
:deterministic (lambda (action ctx)
|
||||
(let* ((payload (getf ctx :payload))
|
||||
(sensor (getf payload :sensor)))
|
||||
(cond
|
||||
((eq sensor :syntax-error)
|
||||
(let ((code (getf payload :code)))
|
||||
(harness-log "SELF-EDIT: Fast paren balancing...")
|
||||
(let ((balanced (self-edit-balance-parens code)))
|
||||
(handler-case
|
||||
(progn
|
||||
(read-from-string balanced)
|
||||
(harness-log "SELF-EDIT: Fast fix SUCCESS.")
|
||||
(list :status :success :repaired balanced))
|
||||
(error ()
|
||||
(harness-log "SELF-EDIT: Fast fix failed, need neural repair.")
|
||||
(list :status :error :reason "needs-llm"))))))
|
||||
((eq sensor :repair-request)
|
||||
(let ((file (getf payload :file))
|
||||
(old (getf payload :old))
|
||||
(new (getf payload :new)))
|
||||
(self-edit-apply file old new)))
|
||||
(t nil)))))
|
||||
|
||||
(def-cognitive-tool :balance-parens
|
||||
"Balances parentheses in a code string."
|
||||
((:code :type :string :description "The code to balance"))
|
||||
:body (lambda (args)
|
||||
(let* ((code (getf args :code))
|
||||
(balanced (self-edit-balance-parens code)))
|
||||
(handler-case
|
||||
(progn
|
||||
(read-from-string balanced)
|
||||
(list :status :success :repaired balanced))
|
||||
(error (c)
|
||||
(list :status :error :message (format nil "Could not repair: ~a" c)))))))
|
||||
|
||||
(defvar *self-edit-skills-backup* nil
|
||||
"Backup of skill registry before hot-reload.")
|
||||
|
||||
(defun self-edit-hot-reload-skill (skill-name gen-path)
|
||||
"Reloads a skill from its compiled .lisp source.
|
||||
|
||||
Steps:
|
||||
1. Backup current *skills-registry*
|
||||
2. Compile the new skill file
|
||||
3. Merge new skill into registry
|
||||
4. Verify the skill loads without error
|
||||
5. If error, rollback to backup
|
||||
|
||||
Returns (values :success t) or (values :error message)."
|
||||
(unless *skills-registry*
|
||||
(return-from self-edit-hot-reload-skill
|
||||
(values :error "Skills engine not initialized")))
|
||||
(unless (uiop:file-exists-p gen-path)
|
||||
(return-from self-edit-hot-reload-skill
|
||||
(values :error (format nil "Skill file not found: ~a" gen-path))))
|
||||
|
||||
;; Step 1: Backup registry
|
||||
(setf *self-edit-skills-backup* (copy-hash-table *skills-registry*))
|
||||
|
||||
(handler-case
|
||||
(progn
|
||||
;; Step 2: Compile new skill
|
||||
(let ((compiled (compile-file gen-path)))
|
||||
(unless compiled
|
||||
(error "Compilation returned nil")))
|
||||
;; Step 3: Load the compiled skill
|
||||
(load gen-path)
|
||||
;; Step 4: Verify skill is in registry
|
||||
(let ((skill (gethash (string skill-name) *skills-registry*)))
|
||||
(if skill
|
||||
(progn
|
||||
(harness-log "SELF-EDIT: Hot-reloaded skill ~a from ~a"
|
||||
skill-name gen-path)
|
||||
(values :success t))
|
||||
(error "Skill not registered after reload"))))
|
||||
(error (e)
|
||||
;; Step 5: Rollback
|
||||
(when *self-edit-skills-backup*
|
||||
(clrhash *skills-registry*)
|
||||
(maphash (lambda (k v) (setf (gethash k *skills-registry*) v))
|
||||
*self-edit-skills-backup*))
|
||||
(harness-log "SELF-EDIT: Hot-reload FAILED for ~a: ~a" skill-name e)
|
||||
(values :error (format nil "Hot-reload failed: ~a" e)))))
|
||||
|
||||
(def-cognitive-tool :reload-skill
|
||||
"Hot-reloads a skill from its compiled source file without restarting the system."
|
||||
((:skill-name :type :string :description "Name of the skill to reload (e.g. :skill-engineering-standards)")
|
||||
(:gen-path :type :string :description "Absolute path to the compiled .lisp file"))
|
||||
:body (lambda (args)
|
||||
(let ((name (getf args :skill-name))
|
||||
(path (getf args :gen-path)))
|
||||
(multiple-value-bind (status message) (self-edit-hot-reload-skill name path)
|
||||
(list :status status :message message)))))
|
||||
@@ -1,18 +1,13 @@
|
||||
#+TITLE: SKILL: Self Edit (org-skill-self-edit.org)
|
||||
#+AUTHOR: Agent
|
||||
#+FILETAGS: :system:autonomy:self-edit:
|
||||
#+PROPERTY: header-args:lisp :tangle org-skill-self-edit.lisp
|
||||
#+PROPERTY: header-args:lisp :tangle %%SKILLS_DIR%%/org-skill-self-edit.lisp
|
||||
|
||||
* Overview
|
||||
The *Self Edit* skill allows the OpenCortex Agent to modify its own literate source code.
|
||||
|
||||
* Implementation
|
||||
|
||||
** Package Context
|
||||
#+begin_src lisp
|
||||
(in-package :opencortex)
|
||||
#+end_src
|
||||
|
||||
** Self-Edit Logic
|
||||
#+begin_src lisp
|
||||
(defun self-edit-apply (filepath old-text new-text)
|
||||
|
||||
@@ -1,65 +0,0 @@
|
||||
(in-package :opencortex)
|
||||
|
||||
(defun self-fix-apply (action context)
|
||||
"Applies a surgical code fix and reloads the modified skill."
|
||||
(declare (ignore context))
|
||||
(let* ((payload (getf action :payload))
|
||||
(target-file (getf payload :file))
|
||||
(old-code (getf payload :old))
|
||||
(new-code (getf payload :new))
|
||||
(is-skill (and (stringp (namestring target-file))
|
||||
(search "skills/" (namestring target-file)))))
|
||||
|
||||
(opencortex:snapshot-memory)
|
||||
(opencortex:harness-log "SELF-FIX - Attempting surgical fix on ~a..." target-file)
|
||||
|
||||
(handler-case
|
||||
(if (uiop:file-exists-p target-file)
|
||||
(let ((content (uiop:read-file-string target-file)))
|
||||
(if (search old-code content)
|
||||
(let ((new-content (cl-ppcre:regex-replace-all (cl-ppcre:quote-meta-chars old-code) content new-code)))
|
||||
(with-open-file (out target-file :direction :output :if-exists :supersede)
|
||||
(write-string new-content out))
|
||||
|
||||
(if is-skill
|
||||
(progn
|
||||
(opencortex:harness-log "SELF-FIX - Reloading modified skill ~a..." target-file)
|
||||
(if (opencortex:load-skill-from-org target-file)
|
||||
(progn
|
||||
(opencortex:harness-log "SELF-FIX SUCCESS - Applied and reloaded.")
|
||||
t)
|
||||
(progn
|
||||
(opencortex:harness-log "SELF-FIX FAILURE - Skill reload failed. Rolling back.")
|
||||
(with-open-file (out target-file :direction :output :if-exists :supersede)
|
||||
(write-string content out))
|
||||
(opencortex:rollback-memory 0)
|
||||
nil)))
|
||||
(progn
|
||||
(opencortex:harness-log "SELF-FIX SUCCESS - Applied fix to file.")
|
||||
t)))
|
||||
(progn (opencortex:harness-log "SELF-FIX FAILURE - Pattern not found.") nil)))
|
||||
(progn (opencortex:harness-log "SELF-FIX FAILURE - File not found.") nil))
|
||||
(error (c)
|
||||
(opencortex:harness-log "SELF-FIX CRASH - ~a. Rolling back." c)
|
||||
(opencortex:rollback-memory 0)
|
||||
nil))))
|
||||
|
||||
(def-cognitive-tool :repair-file
|
||||
"Applies a surgical code modification to a file and reloads the skill if applicable."
|
||||
((:file :type :string :description "Path to the target file")
|
||||
(:old :type :string :description "The literal code block to find")
|
||||
(:new :type :string :description "The literal code block to replace it with"))
|
||||
:body (lambda (args)
|
||||
(if (self-fix-apply (list :payload args) nil)
|
||||
"REPAIR SUCCESSFUL."
|
||||
"REPAIR FAILED.")))
|
||||
|
||||
(defskill :skill-self-fix
|
||||
:priority 95
|
||||
:trigger (lambda (context) (eq (getf (getf context :payload) :sensor) :repair-request))
|
||||
:probabilistic (lambda (context)
|
||||
(format nil "You are the opencortex Repair Actuator. Synthesize a surgical fix for the reported failure.
|
||||
Return a Lisp plist for :repair-file."))
|
||||
:deterministic (lambda (action context)
|
||||
(let ((payload (getf action :payload)))
|
||||
(self-fix-apply action context))))
|
||||
@@ -1,18 +1,13 @@
|
||||
#+TITLE: SKILL: Self Fix (org-skill-self-fix.org)
|
||||
#+AUTHOR: Agent
|
||||
#+FILETAGS: :system:autonomy:self-fix:
|
||||
#+PROPERTY: header-args:lisp :tangle org-skill-self-fix.lisp
|
||||
#+PROPERTY: header-args:lisp :tangle %%SKILLS_DIR%%/org-skill-self-fix.lisp
|
||||
|
||||
* Overview
|
||||
The *Self Fix* skill enables the agent to automatically repair broken skills and harness components.
|
||||
|
||||
* Implementation
|
||||
|
||||
** Package Context
|
||||
#+begin_src lisp
|
||||
(in-package :opencortex)
|
||||
#+end_src
|
||||
|
||||
** Self-Fix Logic
|
||||
#+begin_src lisp
|
||||
(defun self-fix-broken-skill (skill-name error-log)
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
(in-package :opencortex)
|
||||
|
||||
(defparameter *allowed-commands* '("ls" "git" "rg" "grep" "date" "echo" "cat" "node" "python3" "sbcl"))
|
||||
|
||||
(defparameter *shell-metacharacters* '(#\; #\& #\| #\> #\< #\$ #\` #\\ #\!))
|
||||
|
||||
(defun shell-command-safe-p (cmd-string)
|
||||
"Returns T if the command string contains no dangerous metacharacters."
|
||||
(not (some (lambda (char) (find char cmd-string)) *shell-metacharacters*)))
|
||||
|
||||
(defun execute-shell-safely (action context)
|
||||
(let* ((payload (getf action :PAYLOAD))
|
||||
(cmd-string (getf payload :cmd))
|
||||
(executable (car (uiop:split-string (string-trim " " cmd-string) :separator '(#\Space)))))
|
||||
|
||||
(cond
|
||||
((not (shell-command-safe-p cmd-string))
|
||||
(opencortex:inject-stimulus
|
||||
`(:TYPE :EVENT :PAYLOAD (:SENSOR :shell-response :cmd ,cmd-string :stdout "" :stderr "ERROR - Security Violation: Dangerous metacharacters detected." :exit-code 1))
|
||||
:stream (getf context :reply-stream)))
|
||||
|
||||
((not (member executable *allowed-commands* :test #'string=))
|
||||
(opencortex:inject-stimulus
|
||||
`(:TYPE :EVENT :PAYLOAD (:SENSOR :shell-response :cmd ,cmd-string :stdout "" :stderr "ERROR - Command not in security whitelist." :exit-code 1))
|
||||
:stream (getf context :reply-stream)))
|
||||
|
||||
(t
|
||||
(multiple-value-bind (stdout stderr exit-code)
|
||||
(uiop:run-program cmd-string :output :string :error-output :string :ignore-error-status t)
|
||||
(opencortex:inject-stimulus
|
||||
`(:TYPE :EVENT :PAYLOAD (:SENSOR :shell-response :cmd ,cmd-string :stdout ,(or stdout "") :stderr ,(or stderr "") :exit-code ,exit-code))
|
||||
:stream (getf context :reply-stream)))))))
|
||||
|
||||
(defun trigger-skill-shell-actuator (context)
|
||||
(let ((type (getf context :TYPE))
|
||||
(payload (getf context :PAYLOAD)))
|
||||
(and (eq type :EVENT)
|
||||
(eq (getf payload :SENSOR) :shell-response))))
|
||||
|
||||
(defun probabilistic-skill-shell-actuator (context)
|
||||
(let* ((p (getf context :PAYLOAD))
|
||||
(cmd (getf p :cmd))
|
||||
(stdout (getf p :stdout))
|
||||
(stderr (getf p :stderr))
|
||||
(exit-code (getf p :exit-code)))
|
||||
(format nil "SHELL COMMAND RESULT:
|
||||
Command: ~a
|
||||
Exit Code: ~a
|
||||
STDOUT: ~a
|
||||
STDERR: ~a" cmd exit-code stdout stderr)))
|
||||
|
||||
(opencortex:register-actuator :shell #'execute-shell-safely)
|
||||
|
||||
(defskill :skill-shell-actuator
|
||||
:priority 80
|
||||
:trigger #'trigger-skill-shell-actuator
|
||||
:probabilistic #'probabilistic-skill-shell-actuator
|
||||
:deterministic (lambda (action context) (declare (ignore context)) action))
|
||||
@@ -1,18 +1,13 @@
|
||||
#+TITLE: SKILL: Shell Actuator (org-skill-shell-actuator.org)
|
||||
#+AUTHOR: Agent
|
||||
#+FILETAGS: :skill:actuator:shell:
|
||||
#+PROPERTY: header-args:lisp :tangle org-skill-shell-actuator.lisp
|
||||
#+PROPERTY: header-args:lisp :tangle %%SKILLS_DIR%%/org-skill-shell-actuator.lisp
|
||||
|
||||
* Overview
|
||||
The *Shell Actuator* provides the agent with the capability to execute bash commands.
|
||||
|
||||
* Implementation
|
||||
|
||||
** Package Context
|
||||
#+begin_src lisp
|
||||
(in-package :opencortex)
|
||||
#+end_src
|
||||
|
||||
** Shell Execution (shell-execute)
|
||||
#+begin_src lisp
|
||||
(defun shell-execute (action context)
|
||||
|
||||
@@ -1,99 +0,0 @@
|
||||
(in-package :opencortex)
|
||||
|
||||
(defvar *tool-permissions* (make-hash-table :test 'equal)
|
||||
"Hash table mapping tool names to :allow/:deny/:ask.")
|
||||
|
||||
(defun get-tool-permission (tool-name)
|
||||
(let ((key (string-downcase (string tool-name))))
|
||||
(or (gethash key *tool-permissions*) :allow)))
|
||||
|
||||
(defun set-tool-permission (tool-name tier)
|
||||
(setf (gethash (string-downcase (string tool-name)) *tool-permissions*) tier)
|
||||
(harness-log "TOOL PERMISSION: Set ~a = ~a" tool-name tier))
|
||||
|
||||
(defun check-tool-permission-gate (tool-name context)
|
||||
(declare (ignore context))
|
||||
(let ((perm (get-tool-permission tool-name)))
|
||||
(case perm
|
||||
(:allow :allow)
|
||||
(:deny :deny)
|
||||
(:ask (list :ask tool-name))
|
||||
(t :allow))))
|
||||
|
||||
(def-cognitive-tool :get-embedding
|
||||
"Generates vector embeddings via Ollama or llama.cpp API."
|
||||
((:text :type :string :description "Text to embed."))
|
||||
:body (lambda (args)
|
||||
(let* ((text (getf args :text))
|
||||
(provider (or (uiop:getenv "EMBEDDING_PROVIDER") "ollama"))
|
||||
(model (or (uiop:getenv "EMBEDDING_MODEL") "nomic-embed-text"))
|
||||
(embedding nil))
|
||||
(cond
|
||||
((string= provider "ollama")
|
||||
(let* ((host (or (uiop:getenv "OLLAMA_HOST") "localhost:11434"))
|
||||
(url (format nil "http://~a/api/embeddings" host))
|
||||
(body (cl-json:encode-json-to-string `((model . ,model) (prompt . ,text)))))
|
||||
(handler-case
|
||||
(let* ((response (dex:post url :headers '(("Content-Type" . "application/json")) :content body :connect-timeout 5 :read-timeout 30))
|
||||
(json (cl-json:decode-json-from-string response))
|
||||
(vec (cdr (assoc :embedding json))))
|
||||
(when vec (setf embedding vec)))
|
||||
(error (c) (harness-log "EMBEDDING: Ollama failed: ~a" c)))))
|
||||
((string= provider "llama.cpp")
|
||||
(let* ((host (or (uiop:getenv "LLAMA_HOST") "localhost:8080"))
|
||||
(url (format nil "http://~a/v1/embeddings" host))
|
||||
(body (cl-json:encode-json-to-string `((model . ,model) (input . ,text)))))
|
||||
(handler-case
|
||||
(let* ((response (dex:post url :headers '(("Content-Type" . "application/json")) :content body :connect-timeout 5 :read-timeout 30))
|
||||
(json (cl-json:decode-json-from-string response))
|
||||
(data (cdr (assoc :data json)))
|
||||
(vec (when data (cdr (assoc :embedding (car data))))))
|
||||
(when vec (setf embedding vec)))
|
||||
(error (c) (harness-log "EMBEDDING: llama.cpp failed: ~a" c))))))
|
||||
(if embedding
|
||||
(list :status :success :vector embedding)
|
||||
(list :status :error :message "Embedding generation failed")))))
|
||||
|
||||
(def-cognitive-tool :tool-permissions
|
||||
"View or set tool permission tiers."
|
||||
((:tool :type :string :description "Tool name")
|
||||
(:action :type :keyword :description "Action: :get, :set, :list" :default :get)
|
||||
(:tier :type :keyword :description "For :set: :allow/:deny/:ask"))
|
||||
:body (lambda (args)
|
||||
(let ((tool (getf args :tool))
|
||||
(action (getf args :action :get))
|
||||
(tier (getf args :tier)))
|
||||
(case action
|
||||
(:get (list :status :success :tool tool :permission (get-tool-permission tool)))
|
||||
(:set (progn (set-tool-permission tool tier)
|
||||
(list :status :success :message (format nil "Set ~a = ~a" tool tier))))
|
||||
(:list (let ((r nil))
|
||||
(maphash (lambda (k v) (push (list :tool k :permission v) r)) *tool-permissions*)
|
||||
(list :status :success :tools r)))
|
||||
(t (list :status :error :message "Invalid action"))))))
|
||||
|
||||
;; Defaults
|
||||
(set-tool-permission :shell :deny)
|
||||
(set-tool-permission :delete-file :deny)
|
||||
(set-tool-permission :eval :ask)
|
||||
(set-tool-permission :write-file :ask)
|
||||
(harness-log "TOOL PERMISSIONS: Initialized")
|
||||
|
||||
(defskill :skill-tool-permissions
|
||||
:priority 600
|
||||
;; Trigger whenever there's a tool call
|
||||
:trigger (lambda (c)
|
||||
(let* ((action (getf c :candidate))
|
||||
(target (getf action :target)))
|
||||
(or (eq target :TOOL) (eq target :tool))))
|
||||
:deterministic (lambda (a c)
|
||||
(let ((tool (getf (getf a :payload) :tool)))
|
||||
(if tool
|
||||
(let ((perm (check-tool-permission-gate tool c)))
|
||||
(cond
|
||||
((eq perm :deny)
|
||||
(list :type :LOG :payload (list :text (format nil "Tool '~a' execution denied by permission tiers." tool))))
|
||||
((and (listp perm) (eq (car perm) :ask))
|
||||
(list :type :EVENT :status :suspended :reason :ask-permission :payload (list :tool tool :action a)))
|
||||
(t a)))
|
||||
a))))
|
||||
@@ -1,18 +1,13 @@
|
||||
#+TITLE: SKILL: Tool Permissions (org-skill-tool-permissions.org)
|
||||
#+AUTHOR: Agent
|
||||
#+FILETAGS: :skill:security:permissions:
|
||||
#+PROPERTY: header-args:lisp :tangle org-skill-tool-permissions.lisp
|
||||
#+PROPERTY: header-args:lisp :tangle %%SKILLS_DIR%%/org-skill-tool-permissions.lisp
|
||||
|
||||
* Overview
|
||||
The *Tool Permissions* skill manages the authorization levels for different cognitive tools.
|
||||
|
||||
* Implementation
|
||||
|
||||
** Package Context
|
||||
#+begin_src lisp
|
||||
(in-package :opencortex)
|
||||
#+end_src
|
||||
|
||||
** Permission Registry
|
||||
#+begin_src lisp
|
||||
(defvar *tool-permissions* (make-hash-table :test 'equal))
|
||||
|
||||
106
skills/org-skill-unified-llm-backend.org
Normal file
106
skills/org-skill-unified-llm-backend.org
Normal file
@@ -0,0 +1,106 @@
|
||||
#+TITLE: SKILL: Unified LLM Backend (org-skill-unified-llm-backend.org)
|
||||
#+AUTHOR: Agent
|
||||
#+FILETAGS: :skill:llm:backend:openai-compatible:
|
||||
#+PROPERTY: header-args:lisp :tangle %%SKILLS_DIR%%/org-skill-unified-llm-backend.lisp
|
||||
|
||||
* Overview
|
||||
The *Unified LLM Backend* provides a single OpenAI-compatible API client that works with:
|
||||
- Local engines: Ollama, vLLM, LM Studio, llama.cpp (anything exposing /v1/chat/completions)
|
||||
- Cloud providers: OpenRouter, OpenAI, Anthropic, Groq, Gemini (all OpenAI-compatible)
|
||||
|
||||
Providers are registered automatically based on available environment variables.
|
||||
No separate skills per provider — just different base URLs and API keys.
|
||||
|
||||
* Implementation
|
||||
|
||||
** Provider Registry
|
||||
#+begin_src lisp
|
||||
(defparameter *unified-llm-providers*
|
||||
'((:ollama . (:base-url nil :key-env nil :default-model "llama3"))
|
||||
(:openrouter . (:base-url "https://openrouter.ai/api/v1" :key-env "OPENROUTER_API_KEY" :default-model "openrouter/auto"))
|
||||
(:openai . (:base-url "https://api.openai.com/v1" :key-env "OPENAI_API_KEY" :default-model "gpt-4o-mini"))
|
||||
(:anthropic . (:base-url "https://api.anthropic.com/v1" :key-env "ANTHROPIC_API_KEY" :default-model "claude-3-5-sonnet-20241022"))
|
||||
(:groq . (:base-url "https://api.groq.com/openai/v1" :key-env "GROQ_API_KEY" :default-model "llama-3.1-70b-versatile"))
|
||||
(:gemini . (:base-url "https://generativelanguage.googleapis.com/v1beta/openai" :key-env "GEMINI_API_KEY" :default-model "gemini-2.0-flash"))))
|
||||
|
||||
(defun get-provider-config (provider)
|
||||
"Returns the configuration plist for a provider keyword."
|
||||
(cdr (assoc provider *unified-llm-providers*)))
|
||||
|
||||
(defun provider-available-p (provider)
|
||||
"Checks if a provider is configured (has API key or is local Ollama)."
|
||||
(let* ((config (get-provider-config provider))
|
||||
(key-env (getf config :key-env))
|
||||
(base-url (getf config :base-url)))
|
||||
(cond ((eq provider :ollama) t) ; Ollama is always tried; failure is handled at call time
|
||||
(key-env (let ((key (uiop:getenv key-env))) (and key (> (length key) 0))))
|
||||
(base-url t))))
|
||||
#+end_src
|
||||
|
||||
** Unified Request Execution
|
||||
#+begin_src lisp
|
||||
(defun execute-openai-compatible-request (prompt system-prompt &key model (provider :ollama))
|
||||
"Executes a request against any OpenAI-compatible API endpoint."
|
||||
(let* ((config (get-provider-config provider))
|
||||
(base-url (getf config :base-url))
|
||||
(key-env (getf config :key-env))
|
||||
(default-model (getf config :default-model))
|
||||
(api-key (when key-env (uiop:getenv key-env)))
|
||||
(model-id (or model default-model))
|
||||
(url (if (eq provider :ollama)
|
||||
(format nil "http://~a/v1/chat/completions" (or (uiop:getenv "OLLAMA_HOST") "localhost:11434"))
|
||||
(format nil "~a/chat/completions" base-url)))
|
||||
(headers `(("Content-Type" . "application/json")
|
||||
,@(when api-key `(("Authorization" . ,(format nil "Bearer ~a" api-key))))
|
||||
,@(when (eq provider :openrouter)
|
||||
`(("HTTP-Referer" . "https://github.com/amrgharbeia/opencortex")
|
||||
("X-Title" . "OpenCortex")))))
|
||||
(body (cl-json:encode-json-to-string
|
||||
`((model . ,model-id)
|
||||
(messages . (( (role . "system") (content . ,system-prompt) )
|
||||
( (role . "user") (content . ,prompt) )))))))
|
||||
(handler-case
|
||||
(let* ((response (dex:post url :headers headers :content body :connect-timeout 10 :read-timeout 60))
|
||||
(json (cl-json:decode-json-from-string response))
|
||||
(choices (cdr (assoc :choices json)))
|
||||
(first-choice (car choices))
|
||||
(message (cdr (assoc :message first-choice)))
|
||||
(content (cdr (assoc :content message))))
|
||||
(if content
|
||||
(list :status :success :content content)
|
||||
(list :status :error :message (format nil "~a: No content in response (~s)" provider json))))
|
||||
(error (c)
|
||||
(list :status :error :message (format nil "~a Failure: ~a" provider c))))))
|
||||
#+end_src
|
||||
|
||||
** Dynamic Backend Registration
|
||||
#+begin_src lisp
|
||||
(defun register-available-llm-backends ()
|
||||
"Scans environment variables and registers all available LLM backends."
|
||||
(dolist (entry *unified-llm-providers*)
|
||||
(let ((provider (car entry)))
|
||||
(when (provider-available-p provider)
|
||||
(harness-log "LLM BACKEND: Registering provider ~a" provider)
|
||||
(register-probabilistic-backend provider
|
||||
(lambda (prompt system-prompt &key model)
|
||||
(execute-openai-compatible-request prompt system-prompt :model model :provider provider)))))))
|
||||
|
||||
(defun initialize-provider-cascade ()
|
||||
"Reads PROVIDER_CASCADE from env and sets *provider-cascade*."
|
||||
(let ((cascade-str (uiop:getenv "PROVIDER_CASCADE")))
|
||||
(if cascade-str
|
||||
(setf *provider-cascade*
|
||||
(mapcar (lambda (s) (intern (string-upcase (string-trim '(#\Space) s)) :keyword))
|
||||
(uiop:split-string cascade-str :separator '(#\,))))
|
||||
(setf *provider-cascade* (mapcar #'car *unified-llm-providers*)))))
|
||||
#+end_src
|
||||
|
||||
** Skill Registration
|
||||
#+begin_src lisp
|
||||
(register-available-llm-backends)
|
||||
(initialize-provider-cascade)
|
||||
|
||||
(defskill :skill-unified-llm-backend
|
||||
:priority 50
|
||||
:trigger (lambda (ctx) (declare (ignore ctx)) nil))
|
||||
#+end_src
|
||||
194
skills/org-skill-utils-lisp.org
Normal file
194
skills/org-skill-utils-lisp.org
Normal file
@@ -0,0 +1,194 @@
|
||||
#+TITLE: SKILL: Utils Lisp (org-skill-utils-lisp.org)
|
||||
#+AUTHOR: Agent
|
||||
#+FILETAGS: :skill:utils:lisp:validation:evaluation:
|
||||
#+PROPERTY: header-args:lisp :tangle %%SKILLS_DIR%%/org-skill-utils-lisp.lisp
|
||||
|
||||
* Overview
|
||||
The *Utils Lisp* skill provides advanced structural validation, sandboxed evaluation, and formatting for Common Lisp code.
|
||||
|
||||
* Implementation
|
||||
|
||||
** Structural Validation
|
||||
#+begin_src lisp
|
||||
(defun utils-lisp-check-structural (code)
|
||||
"Checks if parentheses are balanced and the code is readable."
|
||||
(handler-case
|
||||
(let ((*read-eval* nil))
|
||||
(with-input-from-string (s code)
|
||||
(loop for form = (read s nil :eof) until (eq form :eof)))
|
||||
(values t nil))
|
||||
(error (c)
|
||||
(values nil (format nil "Reader Error: ~a" c)))))
|
||||
#+end_src
|
||||
|
||||
** Syntactic Validation
|
||||
#+begin_src lisp
|
||||
(defun utils-lisp-check-syntactic (code)
|
||||
"Checks for valid Lisp syntax beyond just balanced parentheses."
|
||||
(utils-lisp-check-structural code))
|
||||
#+end_src
|
||||
|
||||
** Semantic Validation (Safety)
|
||||
#+begin_src lisp
|
||||
(defun utils-lisp-check-semantic (code)
|
||||
"Checks for potentially unsafe forms."
|
||||
(let ((unsafe-tokens '("eval" "load" "uiop:run-program" "sb-ext:run-program" "cl-user::eval")))
|
||||
(loop for token in unsafe-tokens
|
||||
when (search token (string-downcase code))
|
||||
do (return-from utils-lisp-check-semantic (values nil (format nil "Unsafe form detected: ~a" token))))
|
||||
(values t nil)))
|
||||
#+end_src
|
||||
|
||||
** Unified Validation Gate
|
||||
#+begin_src lisp
|
||||
(defun utils-lisp-validate (code &key (strict t))
|
||||
"Unified validation gate for Lisp code."
|
||||
(multiple-value-bind (struct-ok struct-err) (utils-lisp-check-structural code)
|
||||
(unless struct-ok
|
||||
(return-from utils-lisp-validate (list :status :error :reason struct-err)))
|
||||
(when strict
|
||||
(multiple-value-bind (sem-ok sem-err) (utils-lisp-check-semantic code)
|
||||
(unless sem-ok
|
||||
(return-from utils-lisp-validate (list :status :error :reason sem-err)))))
|
||||
(list :status :success)))
|
||||
#+end_src
|
||||
|
||||
** Evaluation (REPL)
|
||||
#+begin_src lisp
|
||||
(defun utils-lisp-eval (code-string &key (package :opencortex))
|
||||
"Evaluates a Lisp string and captures its output/results."
|
||||
(let ((out (make-string-output-stream))
|
||||
(err (make-string-output-stream)))
|
||||
(handler-case
|
||||
(let* ((*standard-output* out)
|
||||
(*error-output* err)
|
||||
(*package* (or (find-package package) (find-package :opencortex)))
|
||||
(result (with-input-from-string (s code-string)
|
||||
(let ((last-val nil))
|
||||
(loop for form = (read s nil :eof) until (eq form :eof)
|
||||
do (setf last-val (eval form)))
|
||||
last-val))))
|
||||
(list :status :success
|
||||
:result (format nil "~a" result)
|
||||
:output (get-output-stream-string out)
|
||||
:error (get-output-stream-string err)))
|
||||
(error (c)
|
||||
(list :status :error
|
||||
:reason (format nil "~a" c)
|
||||
:output (get-output-stream-string out)
|
||||
:error (get-output-stream-string err))))))
|
||||
#+end_src
|
||||
|
||||
** Formatting (Emacs Batch)
|
||||
#+begin_src lisp
|
||||
(defun utils-lisp-format (code-string)
|
||||
"Attempts to format Lisp code using Emacs batch mode if available."
|
||||
(handler-case
|
||||
(let ((tmp-file "/tmp/oc-format-temp.lisp"))
|
||||
(uiop:with-output-file (s tmp-file :if-exists :supersede)
|
||||
(format s "~a" code-string))
|
||||
(multiple-value-bind (out err code)
|
||||
(uiop:run-program (list "emacs" "--batch" tmp-file
|
||||
"--eval" "(indent-region (point-min) (point-max))"
|
||||
"--eval" "(princ (buffer-string))")
|
||||
:output :string :error-output :string :ignore-error-status t)
|
||||
(if (= code 0)
|
||||
out
|
||||
(progn
|
||||
(harness-log "FORMAT ERROR: ~a" err)
|
||||
code-string))))
|
||||
(error (c)
|
||||
(harness-log "FORMAT EXCEPTION: ~a" c)
|
||||
code-string)))
|
||||
#+end_src
|
||||
|
||||
** Structural Extraction (AST)
|
||||
#+begin_src lisp
|
||||
(defun utils-lisp-structural-extract (code function-name)
|
||||
"Extracts the definition of a specific function from a code string."
|
||||
(let ((*read-eval* nil))
|
||||
(with-input-from-string (s code)
|
||||
(loop for form = (read s nil :eof) until (eq form :eof)
|
||||
when (and (listp form)
|
||||
(symbolp (car form))
|
||||
(member (symbol-name (car form)) '("DEFUN" "DEFMACRO" "DEFMETHOD") :test #'string-equal)
|
||||
(symbolp (second form))
|
||||
(string-equal (symbol-name (second form)) function-name))
|
||||
do (return-from utils-lisp-structural-extract (format nil "~s" form))))
|
||||
nil))
|
||||
#+end_src
|
||||
|
||||
** Structural Wrapping (AST)
|
||||
#+begin_src lisp
|
||||
(defun utils-lisp-structural-wrap (code target-name wrapper-symbol)
|
||||
"Wraps a specific form in a wrapper form (e.g., wrap in a let)."
|
||||
(let ((*read-eval* nil) (results nil))
|
||||
(with-input-from-string (s code)
|
||||
(loop for form = (read s nil :eof) until (eq form :eof)
|
||||
do (if (and (listp form)
|
||||
(symbolp (second form))
|
||||
(string-equal (symbol-name (second form)) target-name))
|
||||
(push (list wrapper-symbol form) results)
|
||||
(push form results))))
|
||||
(format nil "~{~s~^~%~%~}" (nreverse results))))
|
||||
#+end_src
|
||||
|
||||
** List Definitions
|
||||
#+begin_src lisp
|
||||
(defun utils-lisp-list-definitions (code)
|
||||
"Returns a list of names for all top-level definitions (defun, defmacro, etc.)."
|
||||
(let ((*read-eval* nil) (names nil))
|
||||
(with-input-from-string (s code)
|
||||
(loop for form = (read s nil :eof) until (eq form :eof)
|
||||
when (and (listp form)
|
||||
(symbolp (car form))
|
||||
(member (symbol-name (car form))
|
||||
'("DEFUN" "DEFMACRO" "DEFMETHOD" "DEFVAR" "DEFPARAMETER")
|
||||
:test #'string-equal)
|
||||
(symbolp (second form)))
|
||||
do (push (second form) names)))
|
||||
(nreverse names)))
|
||||
#+end_src
|
||||
|
||||
** Structural Injection (AST)
|
||||
#+begin_src lisp
|
||||
(defun utils-lisp-structural-inject (code target-name new-form-string)
|
||||
"Injects a new form into the body of a targeted definition."
|
||||
(let ((*read-eval* nil)
|
||||
(new-form (read-from-string new-form-string))
|
||||
(results nil))
|
||||
(with-input-from-string (s code)
|
||||
(loop for form = (read s nil :eof) until (eq form :eof)
|
||||
do (if (and (listp form)
|
||||
(symbolp (car form))
|
||||
(member (symbol-name (car form)) '("DEFUN" "DEFMACRO" "DEFMETHOD") :test #'string-equal)
|
||||
(symbolp (second form))
|
||||
(string-equal (symbol-name (second form)) target-name))
|
||||
(push (append form (list new-form)) results)
|
||||
(push form results))))
|
||||
(format nil "~{~s~^~%~%~}" (nreverse results))))
|
||||
#+end_src
|
||||
|
||||
** Structural Slurp (AST)
|
||||
#+begin_src lisp
|
||||
(defun utils-lisp-structural-slurp (code target-name form-to-slurp-string)
|
||||
"Adds a form to the end of a named list or definition (Paredit slurp)."
|
||||
(let ((*read-eval* nil)
|
||||
(to-slurp (read-from-string form-to-slurp-string))
|
||||
(results nil))
|
||||
(with-input-from-string (s code)
|
||||
(loop for form = (read s nil :eof) until (eq form :eof)
|
||||
do (if (and (listp form)
|
||||
(symbolp (second form))
|
||||
(string-equal (symbol-name (second form)) target-name))
|
||||
(push (append form (list to-slurp)) results)
|
||||
(push form results))))
|
||||
(format nil "~{~s~^~%~%~}" (nreverse results))))
|
||||
#+end_src
|
||||
|
||||
** Skill Registration
|
||||
#+begin_src lisp
|
||||
(defskill :skill-utils-lisp
|
||||
:priority 400
|
||||
:trigger (lambda (ctx) (declare (ignore ctx)) nil))
|
||||
#+end_src
|
||||
138
skills/org-skill-utils-org.org
Normal file
138
skills/org-skill-utils-org.org
Normal file
@@ -0,0 +1,138 @@
|
||||
#+TITLE: SKILL: Utils Org (org-skill-utils-org.org)
|
||||
#+AUTHOR: Agent
|
||||
#+FILETAGS: :skill:utils:org:
|
||||
#+PROPERTY: header-args:lisp :tangle %%SKILLS_DIR%%/org-skill-utils-org.lisp
|
||||
|
||||
* Overview
|
||||
The *Utils Org* skill provides advanced structural manipulation for Org-mode files and their AST representation.
|
||||
|
||||
* Implementation
|
||||
|
||||
** Reading Files
|
||||
#+begin_src lisp
|
||||
(defun utils-org-read-file (filepath)
|
||||
"Reads an Org file into a string."
|
||||
(uiop:read-file-string filepath))
|
||||
#+end_src
|
||||
|
||||
** Writing Files
|
||||
#+begin_src lisp
|
||||
(defun utils-org-write-file (filepath content)
|
||||
"Writes content to an Org file."
|
||||
(uiop:with-output-file (s filepath :if-exists :supersede)
|
||||
(format s "~a" content)))
|
||||
#+end_src
|
||||
|
||||
** ID Generation
|
||||
#+begin_src lisp
|
||||
(defun utils-org-generate-id ()
|
||||
"Generates a new UUID for an Org node."
|
||||
(string-downcase (format nil "~a" (uuid:make-v4-uuid))))
|
||||
#+end_src
|
||||
|
||||
** ID Formatting
|
||||
#+begin_src lisp
|
||||
(defun utils-org-id-format (id)
|
||||
"Ensures the ID has the 'id:' prefix."
|
||||
(if (uiop:string-prefix-p "id:" id)
|
||||
id
|
||||
(format nil "id:~a" id)))
|
||||
#+end_src
|
||||
|
||||
** Setting Properties (Recursive)
|
||||
#+begin_src lisp
|
||||
(defun utils-org-set-property (ast target-id property value)
|
||||
"Recursively sets a property on a headline with a matching ID in the AST."
|
||||
(let ((type (getf ast :type))
|
||||
(props (getf ast :properties))
|
||||
(contents (getf ast :contents)))
|
||||
(when (and (eq type :HEADLINE) (string= (getf props :ID) target-id))
|
||||
(setf (getf (getf ast :properties) property) value)
|
||||
(return-from utils-org-set-property t))
|
||||
(dolist (child contents)
|
||||
(when (listp child)
|
||||
(when (utils-org-set-property child target-id property value)
|
||||
(return-from utils-org-set-property t)))))
|
||||
nil)
|
||||
#+end_src
|
||||
|
||||
** Setting TODO Status
|
||||
#+begin_src lisp
|
||||
(defun utils-org-set-todo (ast target-id status)
|
||||
"Sets the TODO status of a headline in the AST."
|
||||
(utils-org-set-property ast target-id :TODO status))
|
||||
#+end_src
|
||||
|
||||
** Adding Headlines
|
||||
#+begin_src lisp
|
||||
(defun utils-org-add-headline (ast parent-id title)
|
||||
"Adds a new headline as a child of the parent-id in the AST."
|
||||
(let* ((type (getf ast :type))
|
||||
(props (getf ast :properties))
|
||||
(id (getf props :ID))
|
||||
(contents (getf ast :contents)))
|
||||
(when (and (eq type :HEADLINE) (string= id parent-id))
|
||||
(let ((new-node (list :type :HEADLINE
|
||||
:properties (list :ID (utils-org-id-format (utils-org-generate-id))
|
||||
:TITLE title)
|
||||
:contents nil)))
|
||||
(setf (getf ast :contents) (append contents (list new-node)))
|
||||
(return-from utils-org-add-headline t)))
|
||||
(dolist (child contents)
|
||||
(when (listp child)
|
||||
(when (utils-org-add-headline child parent-id title)
|
||||
(return-from utils-org-add-headline t)))))
|
||||
nil)
|
||||
#+end_src
|
||||
|
||||
** Searching Headlines (by ID)
|
||||
#+begin_src lisp
|
||||
(defun utils-org-find-headline-by-id (ast id)
|
||||
"Finds a headline by its ID in the AST."
|
||||
(let ((props (getf ast :properties)))
|
||||
(when (string= (getf props :ID) id)
|
||||
(return-from utils-org-find-headline-by-id ast))
|
||||
(dolist (child (getf ast :contents))
|
||||
(when (listp child)
|
||||
(let ((found (utils-org-find-headline-by-id child id)))
|
||||
(when found (return-from utils-org-find-headline-by-id found)))))
|
||||
nil))
|
||||
#+end_src
|
||||
|
||||
** Searching Headlines (by Title)
|
||||
#+begin_src lisp
|
||||
(defun utils-org-find-headline-by-title (ast title)
|
||||
"Finds a headline by its title in the AST."
|
||||
(let ((props (getf ast :properties)))
|
||||
(when (string-equal (getf props :TITLE) title)
|
||||
(return-from utils-org-find-headline-by-title ast))
|
||||
(dolist (child (getf ast :contents))
|
||||
(when (listp child)
|
||||
(let ((found (utils-org-find-headline-by-title child title)))
|
||||
(when found (return-from utils-org-find-headline-by-title found)))))
|
||||
nil))
|
||||
#+end_src
|
||||
|
||||
** Placeholder for External Edits
|
||||
#+begin_src lisp
|
||||
(defun utils-org-modify (filepath id changes)
|
||||
"Placeholder for Emacs-driven modification of a specific node."
|
||||
(declare (ignore changes))
|
||||
(harness-log "UTILS-ORG: Applying changes to ~a in ~a" id filepath)
|
||||
t)
|
||||
#+end_src
|
||||
|
||||
** Placeholder for AST to Org conversion
|
||||
#+begin_src lisp
|
||||
(defun utils-org-ast-to-org (ast)
|
||||
"Minimal converter from AST back to Org text (Placeholder)."
|
||||
(declare (ignore ast))
|
||||
"* TITLE (Placeholder)")
|
||||
#+end_src
|
||||
|
||||
** Skill Registration
|
||||
#+begin_src lisp
|
||||
(defskill :skill-utils-org
|
||||
:priority 100
|
||||
:trigger (lambda (ctx) (declare (ignore ctx)) nil))
|
||||
#+end_src
|
||||
@@ -1,16 +0,0 @@
|
||||
(in-package :opencortex)
|
||||
|
||||
(SETF (GETHASH "fake-hash-123" *HISTORY-STORE*)
|
||||
#S(ORG-OBJECT
|
||||
:ID "persist-test-1"
|
||||
:TYPE NIL
|
||||
:ATTRIBUTES NIL
|
||||
:CONTENT "Integrity Check"
|
||||
:VECTOR NIL
|
||||
:PARENT-ID NIL
|
||||
:CHILDREN NIL
|
||||
:VERSION NIL
|
||||
:LAST-SYNC NIL
|
||||
:HASH "fake-hash-123"))
|
||||
(SETF (GETHASH "persist-test-1" *MEMORY*)
|
||||
(GETHASH "fake-hash-123" *HISTORY-STORE*))
|
||||
@@ -1,3 +1,6 @@
|
||||
(eval-when (:compile-toplevel :load-toplevel :execute)
|
||||
(ql:quickload :fiveam :silent t))
|
||||
|
||||
(defpackage :opencortex-boot-tests
|
||||
(:use :cl :fiveam :opencortex)
|
||||
(:export #:boot-suite))
|
||||
@@ -5,23 +8,9 @@
|
||||
(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)
|
||||
@@ -34,29 +23,3 @@
|
||||
(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 jail-test.lisp~%(defskill :org-skill-jail-test :priority 1 :trigger (lambda (ctx) nil) :deterministic (lambda (a c) a))~%#+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))))
|
||||
|
||||
(test test-path-traversal-guard
|
||||
"Verify that file I/O cognitive tools block path traversal escapes."
|
||||
(let* ((tool (gethash "read-file" opencortex::*cognitive-tools*))
|
||||
(guard (opencortex::cognitive-tool-guard tool)))
|
||||
;; Set a dummy MEMEX_DIR for the test
|
||||
(setf (uiop:getenv "MEMEX_DIR") "/home/user/memex")
|
||||
|
||||
;; Valid internal paths should return true
|
||||
(is (not (null (funcall guard '(:file "/home/user/memex/safe.txt") nil))))
|
||||
(is (not (null (funcall guard '(:file "/home/user/memex/projects/safe.txt") nil))))
|
||||
|
||||
;; Path traversal escape should return false
|
||||
(is (null (funcall guard '(:file "/home/user/memex/../.bashrc") nil)))
|
||||
(is (null (funcall guard '(:file "/home/user/memex/projects/../../etc/passwd") nil)))))
|
||||
|
||||
@@ -1,41 +1,15 @@
|
||||
(eval-when (:compile-toplevel :load-toplevel :execute)
|
||||
(ql:quickload :fiveam :silent t))
|
||||
|
||||
(defpackage :opencortex-communication-tests
|
||||
(:use :cl :fiveam :opencortex)
|
||||
(:export #:communication-protocol-suite))
|
||||
|
||||
(in-package :opencortex-communication-tests)
|
||||
|
||||
(def-suite communication-protocol-suite
|
||||
:description "Test suite for opencortex Communication Protocol")
|
||||
|
||||
(def-suite communication-protocol-suite :description "Communication Protocol Suite")
|
||||
(in-suite communication-protocol-suite)
|
||||
|
||||
(test test-framing
|
||||
"Verify that messages are correctly prefixed with a 6-character hex length."
|
||||
(let* ((msg '(:type :EVENT :payload (:action :handshake)))
|
||||
(framed (frame-message msg))
|
||||
(len-str (subseq framed 0 6))
|
||||
(payload (subseq framed 6)))
|
||||
(is (string= "00002C" (string-upcase len-str)))
|
||||
(is (equalp msg (read-from-string payload)))))
|
||||
|
||||
(test test-parse-message
|
||||
"Verify that incoming framed strings are parsed into Lisp plists."
|
||||
(let ((framed "00002c(:type :EVENT :payload (:action :handshake))"))
|
||||
(is (equal '(:type :EVENT :payload (:action :handshake))
|
||||
(read-from-string (subseq framed 6))))))
|
||||
|
||||
(test test-hello-handshake
|
||||
"Verify the structure of the HELLO handshake message."
|
||||
(let ((hello (make-hello-message "0.1.0")))
|
||||
(is (eq :EVENT (getf hello :type)))
|
||||
(is (eq :handshake (getf (getf hello :payload) :action)))
|
||||
(is (string= "0.1.0" (getf (getf hello :payload) :version)))))
|
||||
|
||||
(test test-find-missing-id
|
||||
"Verify that the daemon can find a headline missing an ID."
|
||||
(let* ((ast '(:type :org-data :contents
|
||||
((:type :HEADLINE :properties (:TITLE "No ID Here") :contents nil)
|
||||
(:type :HEADLINE :properties (:ID "exists" :TITLE "Has ID") :contents nil))))
|
||||
(found (find-headline-missing-id ast)))
|
||||
(is (not (null found)))
|
||||
(is (string= "No ID Here" (getf (getf found :properties) :TITLE)))))
|
||||
(framed (frame-message msg)))
|
||||
(is (string= "00002C" (string-upcase (subseq framed 0 6))))))
|
||||
|
||||
@@ -1,64 +0,0 @@
|
||||
(defpackage :opencortex-config-manager-tests
|
||||
(:use :cl :fiveam :opencortex)
|
||||
(:export #:config-suite))
|
||||
|
||||
(in-package :opencortex-config-manager-tests)
|
||||
|
||||
(def-suite config-suite :description "Verification of the Config Manager skill")
|
||||
|
||||
(in-suite config-suite)
|
||||
|
||||
(test test-provider-registration
|
||||
"Verify that multiple providers can be registered and saved."
|
||||
(let ((opencortex::*providers* nil))
|
||||
(opencortex:register-provider :ollama '(:url "http://localhost:11434"))
|
||||
(is (equal "http://localhost:11434" (getf (getf opencortex::*providers* :ollama) :url)))))
|
||||
|
||||
(test test-get-oc-config-dir-default
|
||||
"Verify get-oc-config-dir returns XDG-compliant path when env not set."
|
||||
(let ((orig-env (uiop:getenv "OC_CONFIG_DIR")))
|
||||
(unwind-protect
|
||||
(progn
|
||||
(setf (uiop:getenv "OC_CONFIG_DIR") nil)
|
||||
(let ((dir (opencortex:get-oc-config-dir)))
|
||||
(is (search ".config/opencortex" (namestring dir)))))
|
||||
(if orig-env
|
||||
(setf (uiop:getenv "OC_CONFIG_DIR") orig-env)
|
||||
(setf (uiop:getenv "OC_CONFIG_DIR") nil)))))
|
||||
|
||||
(test test-get-oc-config-dir-env-override
|
||||
"Verify get-oc-config-dir uses OC_CONFIG_DIR when set."
|
||||
(let ((orig-env (uiop:getenv "OC_CONFIG_DIR")))
|
||||
(unwind-protect
|
||||
(progn
|
||||
(setf (uiop:getenv "OC_CONFIG_DIR") "/tmp/test-opencortex-config")
|
||||
(let ((dir (opencortex:get-oc-config-dir)))
|
||||
(is (string= "/tmp/test-opencortex-config/" (namestring dir)))))
|
||||
(if orig-env
|
||||
(setf (uiop:getenv "OC_CONFIG_DIR") orig-env)
|
||||
(setf (uiop:getenv "OC_CONFIG_DIR") nil)))))
|
||||
|
||||
(test test-save-providers-roundtrip
|
||||
"Verify save-providers writes and providers can be reloaded."
|
||||
(let ((opencortex::*providers* nil)
|
||||
(test-dir "/tmp/test-opencortex-config/")
|
||||
(orig-env (uiop:getenv "OC_CONFIG_DIR")))
|
||||
(unwind-protect
|
||||
(progn
|
||||
(setf (uiop:getenv "OC_CONFIG_DIR") test-dir)
|
||||
(opencortex:register-provider :openai '(:key "test-key-123" :model "gpt-4"))
|
||||
(opencortex:save-providers)
|
||||
(let ((loaded-provs (uiop:read-file-string (merge-pathnames "providers.lisp" (uiop:ensure-directory-pathname test-dir)))))
|
||||
(is (search "openai" loaded-provs))
|
||||
(is (search "test-key-123" loaded-provs))))
|
||||
(uiop:delete-directory-tree (uiop:ensure-directory-pathname test-dir) :validate t)
|
||||
(if orig-env
|
||||
(setf (uiop:getenv "OC_CONFIG_DIR") orig-env)
|
||||
(setf (uiop:getenv "OC_CONFIG_DIR") nil)))))
|
||||
|
||||
(test test-configure-provider-validation
|
||||
"Verify configure-provider validates required fields."
|
||||
(let ((opencortex::*providers* nil))
|
||||
(opencortex:register-provider :ollama '(:url "http://localhost:11434"))
|
||||
(let ((cfg (getf opencortex::*providers* :ollama)))
|
||||
(is (equal "http://localhost:11434" (getf cfg :url))))))
|
||||
@@ -1,14 +0,0 @@
|
||||
(defpackage :opencortex-diagnostics-tests
|
||||
(:use :cl :fiveam :opencortex)
|
||||
(:export #:diagnostics-suite))
|
||||
|
||||
(in-package :opencortex-diagnostics-tests)
|
||||
|
||||
(def-suite diagnostics-suite :description "Verification of the Diagnostics skill")
|
||||
|
||||
(in-suite diagnostics-suite)
|
||||
|
||||
(test test-dependency-check-fail
|
||||
"Verify that missing binaries are correctly identified as failures."
|
||||
(let ((opencortex::*doctor-required-binaries* '("non-existent-binary-123")))
|
||||
(is (null (opencortex:doctor-check-dependencies)))))
|
||||
@@ -5,7 +5,6 @@
|
||||
(in-package :opencortex-doctor-tests)
|
||||
|
||||
(def-suite doctor-suite :description "Verification of the System Doctor diagnostic logic")
|
||||
|
||||
(in-suite doctor-suite)
|
||||
|
||||
(test test-dependency-check-fail
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
(defpackage :opencortex-emacs-edit-tests
|
||||
(:use :cl :fiveam :opencortex)
|
||||
(:export #:emacs-edit-suite))
|
||||
|
||||
(in-package :opencortex-emacs-edit-tests)
|
||||
|
||||
(def-suite emacs-edit-suite
|
||||
:description "Tests for Emacs Edit skill.")
|
||||
|
||||
(in-suite emacs-edit-suite)
|
||||
|
||||
(test id-generation
|
||||
(let ((id1 (emacs-edit-generate-id))
|
||||
(id2 (emacs-edit-generate-id)))
|
||||
(is (plusp (length id1)))
|
||||
(is (not (string= id1 id2))))) ;; Likely unique
|
||||
|
||||
(test id-format
|
||||
(let ((formatted (emacs-edit-id-format "abc12345")))
|
||||
(is (search "id:" formatted))))
|
||||
|
||||
(test property-setter
|
||||
(let ((ast (list :type :headline
|
||||
:properties (list :ID "id:test123" :TITLE "Test")
|
||||
:contents nil)))
|
||||
(emacs-edit-set-property ast "id:test123" :STATUS "ACTIVE")
|
||||
(is (string= (getf (getf ast :properties) :STATUS) "ACTIVE"))))
|
||||
|
||||
(test todo-setter
|
||||
(let ((ast (list :type :headline
|
||||
:properties (list :ID "id:todo001" :TITLE "Task")
|
||||
:contents nil)))
|
||||
(emacs-edit-set-todo ast "id:todo001" "DONE")
|
||||
(is (string= (getf (getf ast :properties) :TODO) "DONE"))))
|
||||
@@ -1,18 +0,0 @@
|
||||
(defpackage :opencortex-engineering-standards-tests
|
||||
(:use :cl :fiveam :opencortex)
|
||||
(:export #:engineering-standards-suite))
|
||||
|
||||
(in-package :opencortex-engineering-standards-tests)
|
||||
|
||||
(def-suite engineering-standards-suite
|
||||
:description "Tests for Engineering Standards enforcement")
|
||||
|
||||
(in-suite engineering-standards-suite)
|
||||
|
||||
(test git-clean-check-clean
|
||||
"verify-git-clean-p returns T when git tree is clean."
|
||||
(let ((tmp-dir "/tmp/eng-std-test-clean/"))
|
||||
(uiop:ensure-all-directories-exist (list tmp-dir))
|
||||
(uiop:run-program (list "git" "init" tmp-dir) :output nil)
|
||||
(is (eq t (opencortex::verify-git-clean-p (uiop:ensure-directory-pathname tmp-dir))))
|
||||
(uiop:delete-directory-tree (uiop:ensure-directory-pathname tmp-dir) :validate t)))
|
||||
@@ -1,23 +0,0 @@
|
||||
(defpackage :opencortex-gateway-manager-tests
|
||||
(:use :cl :fiveam :opencortex)
|
||||
(:export #:gateway-suite))
|
||||
|
||||
(in-package :opencortex-gateway-manager-tests)
|
||||
|
||||
(def-suite gateway-suite :description "Verification of the Gateway Manager skill")
|
||||
|
||||
(in-suite gateway-suite)
|
||||
|
||||
(test test-gateway-registration
|
||||
"Verify that the skill can register a new gateway metadata block."
|
||||
(let ((opencortex::*gateways* nil))
|
||||
(opencortex:skill-gateway-register :telegram '(:status :unverified))
|
||||
(is (getf (getf opencortex::*gateways* :telegram) :status))))
|
||||
|
||||
(test test-gateway-multiple-platforms
|
||||
"Verify that multiple gateways can be registered simultaneously."
|
||||
(let ((opencortex::*gateways* nil))
|
||||
(opencortex:skill-gateway-register :telegram '(:status :verified :token "abc123"))
|
||||
(opencortex:skill-gateway-register :signal '(:status :unverified))
|
||||
(is (eq (getf (getf opencortex::*gateways* :telegram) :status) :verified))
|
||||
(is (eq (getf (getf opencortex::*gateways* :signal) :status) :unverified))))
|
||||
@@ -1,12 +1,13 @@
|
||||
(eval-when (:compile-toplevel :load-toplevel :execute)
|
||||
(ql:quickload :fiveam :silent t))
|
||||
|
||||
(defpackage :opencortex-immune-system-tests
|
||||
(:use :cl :fiveam :opencortex)
|
||||
(:export #:immune-suite))
|
||||
|
||||
(in-package :opencortex-immune-system-tests)
|
||||
|
||||
(def-suite immune-suite
|
||||
:description "Verification of the Immune System (Core Error Hooks)")
|
||||
|
||||
(def-suite immune-suite :description "Verification of the Immune System (Core Error Hooks)")
|
||||
(in-suite immune-suite)
|
||||
|
||||
(test loop-error-injection
|
||||
@@ -15,9 +16,8 @@
|
||||
(opencortex:defskill :evil-skill
|
||||
:priority 100
|
||||
:trigger (lambda (ctx) (eq (getf (getf ctx :payload) :sensor) :user-input))
|
||||
:probabilistic (lambda (ctx) (error "CRITICAL BRAIN FAILURE"))
|
||||
:probabilistic (lambda (ctx) (declare (ignore ctx)) (error "CRITICAL BRAIN FAILURE"))
|
||||
:deterministic nil)
|
||||
(opencortex:harness-log "CLEAN LOG")
|
||||
(opencortex:process-signal '(:type :EVENT :payload (:sensor :user-input)))
|
||||
(let ((logs (opencortex:context-get-system-logs 20)))
|
||||
(is (not (null (find-if (lambda (line) (search "CRITICAL BRAIN FAILURE" line)) logs))))))
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
(defpackage :opencortex-lisp-utils-tests
|
||||
(:use :cl :fiveam :opencortex)
|
||||
(:export #:lisp-utils-suite))
|
||||
|
||||
(in-package :opencortex-lisp-utils-tests)
|
||||
|
||||
(def-suite lisp-utils-suite
|
||||
:description "Tests for the Lisp Validator structural, syntactic, and semantic gates")
|
||||
|
||||
(in-suite lisp-utils-suite)
|
||||
|
||||
(test structural-balanced
|
||||
(is (eq t (opencortex:lisp-utils-check-structural "(+ 1 2)"))))
|
||||
|
||||
(test structural-unbalanced-open
|
||||
(multiple-value-bind (ok reason) (opencortex:lisp-utils-check-structural "(+ 1 2")
|
||||
(is (null ok))
|
||||
(is (search "Unbalanced" reason))))
|
||||
|
||||
(test structural-unbalanced-close
|
||||
(multiple-value-bind (ok reason) (opencortex:lisp-utils-check-structural "+ 1 2)")
|
||||
(is (null ok))
|
||||
(is (search "Unexpected" reason))))
|
||||
|
||||
(test syntactic-valid
|
||||
(is (eq t (opencortex:lisp-utils-check-syntactic "(+ 1 2)"))))
|
||||
|
||||
(test semantic-safe
|
||||
(is (eq t (opencortex:lisp-utils-check-semantic "(+ 1 2)"))))
|
||||
|
||||
(test semantic-blocked-eval
|
||||
(multiple-value-bind (ok reason) (opencortex:lisp-utils-check-semantic "(eval '(+ 1 2))")
|
||||
(is (null ok))
|
||||
(is (search "Unsafe" reason))))
|
||||
|
||||
(test unified-success
|
||||
(let ((result (opencortex:lisp-utils-validate "(+ 1 2)" :strict t)))
|
||||
(is (eq (getf result :status) :success))))
|
||||
|
||||
(test unified-failure
|
||||
(let ((result (opencortex:lisp-utils-validate "(+ 1 2" :strict nil)))
|
||||
(is (eq (getf result :status) :error))))
|
||||
@@ -1,73 +0,0 @@
|
||||
(defpackage :opencortex-literate-programming-tests
|
||||
(:use :cl :fiveam :opencortex)
|
||||
(:export #:literate-programming-suite))
|
||||
|
||||
(in-package :opencortex-literate-programming-tests)
|
||||
|
||||
(def-suite literate-programming-suite
|
||||
:description "Tests for Literate Programming enforcement")
|
||||
|
||||
(in-suite literate-programming-suite)
|
||||
|
||||
(test tangle-sync-detects-stale-lisp
|
||||
"check-tangle-sync returns violation when .lisp is newer than .org"
|
||||
(let* ((root (uiop:ensure-directory-pathname "/tmp/lp-test/"))
|
||||
(tmp-org (merge-pathnames "skills/test-skill.org" root))
|
||||
(tmp-lisp (merge-pathnames "library/gen/test-skill.lisp" root)))
|
||||
(uiop:ensure-all-directories-exist (list (directory-namestring tmp-org) (directory-namestring tmp-lisp)))
|
||||
(with-open-file (f tmp-org :direction :output) (write-line "* Test" f))
|
||||
(sleep 1)
|
||||
(with-open-file (f tmp-lisp :direction :output) (write-line "(defun test () t)" f))
|
||||
(let ((orig-targets opencortex::*tangle-targets*))
|
||||
(setf opencortex::*tangle-targets*
|
||||
(cons '("skills/test-skill.org" . "library/gen/test-skill.lisp") orig-targets))
|
||||
(unwind-protect
|
||||
(let ((result (opencortex::check-tangle-sync root)))
|
||||
(is (listp result))
|
||||
(is (eq :log (getf result :type)))
|
||||
(is (search "LITERATE PROGRAMMING VIOLATION" (getf (getf result :payload) :text))))
|
||||
(setf opencortex::*tangle-targets* orig-targets)))
|
||||
(uiop:delete-file-if-exists tmp-org)
|
||||
(uiop:delete-file-if-exists tmp-lisp)))
|
||||
|
||||
(test tangle-sync-passes-when-synced
|
||||
"check-tangle-sync returns nil when .org is newer than .lisp"
|
||||
(let* ((root (uiop:ensure-directory-pathname "/tmp/lp-test2/"))
|
||||
(tmp-org (merge-pathnames "skills/test-skill2.org" root))
|
||||
(tmp-lisp (merge-pathnames "library/gen/test-skill2.lisp" root)))
|
||||
(uiop:ensure-all-directories-exist (list (directory-namestring tmp-org) (directory-namestring tmp-lisp)))
|
||||
(with-open-file (f tmp-lisp :direction :output) (write-line "(defun test () t)" f))
|
||||
(sleep 1)
|
||||
(with-open-file (f tmp-org :direction :output) (write-line "* Test" f))
|
||||
(let ((orig-targets opencortex::*tangle-targets*))
|
||||
(setf opencortex::*tangle-targets*
|
||||
(cons '("skills/test-skill2.org" . "library/gen/test-skill2.lisp") orig-targets))
|
||||
(unwind-protect
|
||||
(let ((result (opencortex::check-tangle-sync root)))
|
||||
(is (null result)))
|
||||
(setf opencortex::*tangle-targets* orig-targets)))
|
||||
(uiop:delete-file-if-exists tmp-org)
|
||||
(uiop:delete-file-if-exists tmp-lisp)))
|
||||
|
||||
(test tangle-sync-passes-when-synced
|
||||
"check-tangle-sync returns nil when .org is newer than .lisp"
|
||||
(let ((tmp-org "/tmp/test-skill2.org")
|
||||
(tmp-lisp "/tmp/test-skill2.lisp"))
|
||||
(with-open-file (f tmp-lisp :direction :output) (write-line "(defun test () t)" f))
|
||||
(sleep 1)
|
||||
(with-open-file (f tmp-org :direction :output) (write-line "* Test" f))
|
||||
(let* ((root (uiop:ensure-directory-pathname "/tmp/"))
|
||||
(result (opencortex::check-tangle-sync root)))
|
||||
(is (null result)))
|
||||
(uiop:delete-file-if-exists tmp-org)
|
||||
(uiop:delete-file-if-exists tmp-lisp)))
|
||||
|
||||
(test block-balance-valid
|
||||
"literate-check-block-balance returns T for balanced code"
|
||||
(is (eq t (opencortex::literate-check-block-balance "(defun test () t)"))))
|
||||
|
||||
(test block-balance-invalid
|
||||
"literate-check-block-balance returns NIL for unbalanced code"
|
||||
(multiple-value-bind (ok reason) (opencortex::literate-check-block-balance "(defun test ()")
|
||||
(is (null ok))
|
||||
(is (stringp reason))))
|
||||
@@ -1,17 +0,0 @@
|
||||
(defpackage :opencortex-llm-gateway-tests
|
||||
(:use :cl :fiveam :opencortex)
|
||||
(:export #:llm-gateway-suite))
|
||||
|
||||
(in-package :opencortex-llm-gateway-tests)
|
||||
|
||||
(def-suite llm-gateway-suite :description "Tests for the LLM Gateway skill")
|
||||
(in-suite llm-gateway-suite)
|
||||
|
||||
(test test-llm-gateway-timeout
|
||||
"Tier 2 Chaos: Verify that LLM Gateway handles connection failures gracefully."
|
||||
;; Point to a non-existent port to force a connection error
|
||||
(let ((uiop:*environment* (copy-list uiop:*environment*)))
|
||||
(setf (uiop:getenv "OLLAMA_HOST") "localhost:1")
|
||||
(let ((result (opencortex::execute-llm-request :prompt "hello" :provider :ollama)))
|
||||
(is (eq (getf result :status) :error))
|
||||
(is (uiop:string-prefix-p "Ollama Failure" (getf result :message))))))
|
||||
@@ -1,77 +1,20 @@
|
||||
(eval-when (:compile-toplevel :load-toplevel :execute)
|
||||
(ql:quickload :fiveam :silent t))
|
||||
|
||||
(defpackage :opencortex-memory-tests
|
||||
(:use :cl :fiveam :opencortex)
|
||||
(:export #:memory-suite))
|
||||
|
||||
(in-package :opencortex-memory-tests)
|
||||
|
||||
(def-suite memory-suite
|
||||
:description "Tests for the Merkle-Tree Memory")
|
||||
|
||||
(def-suite memory-suite :description "Tests for the Merkle-Tree Memory")
|
||||
(in-suite memory-suite)
|
||||
|
||||
(test merkle-hash-consistency
|
||||
"Verify identical ASTs produce identical Merkle hashes."
|
||||
(let* ((ast1 '(:type :HEADLINE :properties (:ID "test-1" :TITLE "Node 1") :contents nil)))
|
||||
(clrhash *memory*)
|
||||
(clrhash opencortex::*memory*)
|
||||
(let ((id1 (ingest-ast ast1)))
|
||||
(let ((hash1 (org-object-hash (lookup-object id1))))
|
||||
(clrhash *memory*)
|
||||
(clrhash opencortex::*memory*)
|
||||
(let ((id2 (ingest-ast ast1)))
|
||||
(let ((hash2 (org-object-hash (lookup-object id2))))
|
||||
(is (equal hash1 hash2))))))))
|
||||
|
||||
(test history-store-immutability
|
||||
"Verify that *history-store* retains old versions."
|
||||
(clrhash *memory*)
|
||||
(clrhash *history-store*)
|
||||
(let* ((ast-v1 '(:type :HEADLINE :properties (:ID "test-node" :TITLE "Version 1") :contents nil))
|
||||
(id-v1 (ingest-ast ast-v1))
|
||||
(obj-v1 (lookup-object id-v1))
|
||||
(hash-v1 (org-object-hash obj-v1)))
|
||||
(let* ((ast-v2 '(:type :HEADLINE :properties (:ID "test-node" :TITLE "Version 2") :contents nil))
|
||||
(id-v2 (ingest-ast ast-v2))
|
||||
(hash-v2 (org-object-hash (lookup-object id-v2))))
|
||||
(is (equal (org-object-hash (lookup-object "test-node")) hash-v2))
|
||||
(is (not (null (gethash hash-v1 *history-store*))))
|
||||
(is (not (null (gethash hash-v2 *history-store*)))))))
|
||||
|
||||
(test cow-snapshot-and-rollback
|
||||
"Verify that lightweight snapshots restore previous pointer states."
|
||||
(clrhash *memory*)
|
||||
(setf *object-store-snapshots* nil)
|
||||
(let* ((ast-v1 '(:type :HEADLINE :properties (:ID "cow-node" :TITLE "State A") :contents nil))
|
||||
(id-v1 (ingest-ast ast-v1))
|
||||
(hash-v1 (org-object-hash (lookup-object id-v1))))
|
||||
(snapshot-memory)
|
||||
(let* ((ast-v2 '(:type :HEADLINE :properties (:ID "cow-node" :TITLE "State B") :contents nil))
|
||||
(id-v2 (ingest-ast ast-v2))
|
||||
(hash-v2 (org-object-hash (lookup-object id-v2))))
|
||||
(is (equal (org-object-hash (lookup-object "cow-node")) hash-v2))
|
||||
(rollback-memory 0)
|
||||
(is (equal (org-object-hash (lookup-object "cow-node")) hash-v1)))))
|
||||
|
||||
(test test-merkle-corruption-rollback
|
||||
"Tier 2 Chaos: Verify that Merkle hash corruption triggers a Micro-Rollback."
|
||||
(clrhash *memory*)
|
||||
(setf *object-store-snapshots* nil)
|
||||
(let* ((ast '(:type :HEADLINE :properties (:ID "node-1" :TITLE "Original") :contents nil))
|
||||
(id (ingest-ast ast)))
|
||||
(snapshot-memory)
|
||||
;; Manually corrupt the hash in the live memory
|
||||
(let ((obj (lookup-object id)))
|
||||
(setf (org-object-hash obj) "CORRUPTED-HASH"))
|
||||
|
||||
;; Simulate a system integrity check that should fail and rollback
|
||||
;; We'll use a manual check here since automatic validation is in the Loop
|
||||
(let ((obj (lookup-object id)))
|
||||
(let ((current-hash (org-object-hash obj))
|
||||
(computed-hash (compute-merkle-hash (org-object-id obj)
|
||||
(org-object-type obj)
|
||||
(org-object-attributes obj)
|
||||
(org-object-content obj)
|
||||
nil)))
|
||||
(unless (string= current-hash computed-hash)
|
||||
(rollback-memory 0))))
|
||||
|
||||
;; Verify that the memory was rolled back to the clean snapshot
|
||||
(is (string/= "CORRUPTED-HASH" (org-object-hash (lookup-object id))))))
|
||||
(is (equal hash1 (org-object-hash (lookup-object id2)))))))))
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
#|
|
||||
(defpackage :opencortex-vault-tests
|
||||
(:use :cl :fiveam :opencortex))
|
||||
(in-package :opencortex-vault-tests)
|
||||
|
||||
(def-suite vault-suite :description "Tests for the Credentials Vault.")
|
||||
(in-suite vault-suite)
|
||||
|
||||
(test test-masking
|
||||
(is (equal "sk-t...-key" (opencortex::vault-mask-string "sk-test-key")))
|
||||
(is (equal "[REDACTED]" (opencortex::vault-mask-string "short"))))
|
||||
|
||||
(test test-vault-persistence
|
||||
"Verify that setting a secret triggers a snapshot (mock check)."
|
||||
(let ((old-version (opencortex::org-object-version (gethash "root" *memory*))))
|
||||
(opencortex:vault-set-secret :test "secret-val")
|
||||
(is (> (opencortex::org-object-version (gethash "root" *memory*)) old-version))))
|
||||
|#
|
||||
@@ -1,32 +1,31 @@
|
||||
(eval-when (:compile-toplevel :load-toplevel :execute)
|
||||
(ql:quickload :fiveam :silent t))
|
||||
|
||||
(defpackage :opencortex-peripheral-vision-tests
|
||||
(:use :cl :fiveam :opencortex)
|
||||
(:export #:vision-suite))
|
||||
(in-package :opencortex-peripheral-vision-tests)
|
||||
|
||||
(def-suite vision-suite
|
||||
:description "Verification of Foveal-Peripheral context model.")
|
||||
(def-suite vision-suite :description "Verification of Foveal-Peripheral context model.")
|
||||
(in-suite vision-suite)
|
||||
|
||||
(test test-foveal-rendering
|
||||
"Verify that the foveal target is rendered with content, while siblings are skeletal."
|
||||
(clrhash opencortex::*memory*)
|
||||
(let* ((ast '(:type :HEADLINE :properties (:ID "proj-root" :TITLE "Project" :TAGS "project")
|
||||
(let* ((ast '(:type :HEADLINE :properties (:ID "proj-root" :TITLE "Project" :TAGS ("project"))
|
||||
:contents ((:type :HEADLINE :properties (:ID "node-foveal" :TITLE "Foveal Node")
|
||||
:raw-content "FOVEAL CONTENT" :contents nil)
|
||||
(:type :HEADLINE :properties (:ID "node-peripheral" :TITLE "Peripheral Node")
|
||||
:raw-content "PERIPHERAL CONTENT" :contents nil)))))
|
||||
(ingest-ast ast)
|
||||
;; Test both foveal focus in signal top-level and in payload (legacy)
|
||||
(let ((output (context-assemble-global-awareness (list :foveal-focus "node-foveal"))))
|
||||
(is (search "FOVEAL CONTENT" output))
|
||||
(is (search "* Peripheral Node" output))
|
||||
(is (not (search "PERIPHERAL CONTENT" output))))))
|
||||
|
||||
(test test-awareness-budget
|
||||
"Verify that context-assemble-global-awareness handles multiple projects."
|
||||
(clrhash opencortex::*memory*)
|
||||
(ingest-ast '(:type :HEADLINE :properties (:ID "p1" :TITLE "Project 1" :TAGS "project") :contents nil))
|
||||
(ingest-ast '(:type :HEADLINE :properties (:ID "p2" :TITLE "Project 2" :TAGS "project") :contents nil))
|
||||
(ingest-ast '(:type :HEADLINE :properties (:ID "p1" :TITLE "Project 1" :TAGS ("project")) :contents nil))
|
||||
(ingest-ast '(:type :HEADLINE :properties (:ID "p2" :TITLE "Project 2" :TAGS ("project")) :contents nil))
|
||||
(let ((output (context-assemble-global-awareness)))
|
||||
(is (search "Project 1" output))
|
||||
(is (search "Project 2" output))))
|
||||
|
||||
@@ -1,35 +1,18 @@
|
||||
(eval-when (:compile-toplevel :load-toplevel :execute)
|
||||
(ql:quickload :fiveam :silent t))
|
||||
|
||||
(defpackage :opencortex-pipeline-act-tests
|
||||
(:use :cl :fiveam :opencortex)
|
||||
(:export #:pipeline-act-suite))
|
||||
|
||||
(in-package :opencortex-pipeline-act-tests)
|
||||
|
||||
(def-suite pipeline-act-suite
|
||||
:description "Test suite for Act pipeline")
|
||||
|
||||
(def-suite pipeline-act-suite :description "Test suite for Act pipeline")
|
||||
(in-suite pipeline-act-suite)
|
||||
|
||||
(test test-act-gate-symbolic-guard-bypass
|
||||
"Verify that act-gate proceeds normally when no skill intercepts."
|
||||
(test test-act-gate-basic
|
||||
(clrhash opencortex::*skills-registry*)
|
||||
(let* ((signal (list :type :EVENT :status nil :depth 0 :approved-action '(:target :cli :payload (:text "Hello"))))
|
||||
(result (opencortex:act-gate signal)))
|
||||
(result (act-gate signal)))
|
||||
(is (eq :acted (getf signal :status)))
|
||||
(is (null result))))
|
||||
|
||||
(test test-act-gate-symbolic-guard-interception
|
||||
"Verify that act-gate intercepts actions when a skill returns a LOG/EVENT."
|
||||
(clrhash opencortex::*skills-registry*)
|
||||
(opencortex::defskill :mock-bouncer
|
||||
:priority 200
|
||||
:trigger (lambda (ctx) (declare (ignore ctx)) t)
|
||||
:deterministic (lambda (action ctx)
|
||||
(declare (ignore action ctx))
|
||||
(list :type :LOG :payload (list :text "BLOCKED BY SYMBOLIC GUARD"))))
|
||||
(let* ((signal (list :type :EVENT :status nil :depth 0 :approved-action '(:target :shell :payload (:cmd "ls"))))
|
||||
(result (opencortex:act-gate signal)))
|
||||
(is (eq :acted (getf signal :status)))
|
||||
(is (not (null result)))
|
||||
(is (eq :LOG (getf result :type)))
|
||||
(let ((msg (getf (getf result :payload) :text)))
|
||||
(is (search "BLOCKED BY SYMBOLIC GUARD" msg)))))
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
(eval-when (:compile-toplevel :load-toplevel :execute)
|
||||
(ql:quickload :fiveam :silent t))
|
||||
|
||||
(defpackage :opencortex-pipeline-perceive-tests
|
||||
(:use :cl :fiveam :opencortex)
|
||||
(:export #:pipeline-perceive-suite))
|
||||
|
||||
(in-package :opencortex-pipeline-perceive-tests)
|
||||
|
||||
(def-suite pipeline-perceive-suite
|
||||
:description "Test suite for Perceive pipeline")
|
||||
|
||||
(def-suite pipeline-perceive-suite :description "Test suite for Perceive pipeline")
|
||||
(in-suite pipeline-perceive-suite)
|
||||
|
||||
(test test-perceive-gate
|
||||
"Perceive gate should update the object store and normalize signal."
|
||||
(clrhash opencortex::*memory*)
|
||||
(let* ((signal (list :type :EVENT :payload (list :sensor :buffer-update :ast (list :type :HEADLINE :properties (list :ID "test-node" :TITLE "Test") :contents nil))))
|
||||
(result (perceive-gate signal)))
|
||||
@@ -18,6 +18,5 @@
|
||||
(is (not (null (gethash "test-node" opencortex::*memory*))))))
|
||||
|
||||
(test test-depth-limiting
|
||||
"Verify that the pipeline terminates runaway feedback loops."
|
||||
(let ((runaway-signal (list :type :EVENT :depth 11 :payload (list :sensor :heartbeat))))
|
||||
(is (null (process-signal runaway-signal)))))
|
||||
|
||||
@@ -1,26 +1,26 @@
|
||||
(eval-when (:compile-toplevel :load-toplevel :execute)
|
||||
(ql:quickload :fiveam :silent t))
|
||||
|
||||
(defpackage :opencortex-pipeline-reason-tests
|
||||
(:use :cl :fiveam :opencortex)
|
||||
(:export #:pipeline-reason-suite))
|
||||
|
||||
(in-package :opencortex-pipeline-reason-tests)
|
||||
|
||||
(def-suite pipeline-reason-suite
|
||||
:description "Test suite for Reason pipeline")
|
||||
|
||||
(def-suite pipeline-reason-suite :description "Test suite for Reason pipeline")
|
||||
(in-suite pipeline-reason-suite)
|
||||
|
||||
(test test-decide-gate-safety
|
||||
"Decide gate should block unsafe LLM proposals."
|
||||
;; Setup: clear skills and register mock
|
||||
(clrhash opencortex::*skills-registry*)
|
||||
(opencortex::defskill :mock-safety
|
||||
:priority 50
|
||||
:trigger (lambda (ctx) t)
|
||||
:probabilistic (lambda (ctx) "Mock probabilistic")
|
||||
:trigger (lambda (ctx) (declare (ignore ctx)) t)
|
||||
:deterministic (lambda (action ctx)
|
||||
(list :type :LOG :payload (list :text "Action rejected by skill heuristics"))))
|
||||
(let* ((candidate (list :type :REQUEST :payload (list :action :eval :code "(shell-command \"rm -rf /\")")))
|
||||
(signal (list :type :EVENT :candidate candidate))
|
||||
(declare (ignore ctx))
|
||||
(if (search "rm -rf" (format nil "~s" action))
|
||||
(list :type :LOG :payload (list :text "Rejected"))
|
||||
action)))
|
||||
(let* ((candidate '(:type :REQUEST :payload (:action :shell :cmd "rm -rf /")))
|
||||
(signal '(:type :EVENT :payload (:sensor :user-input)))
|
||||
(result (deterministic-verify candidate signal)))
|
||||
(is (eq :LOG (getf result :type)))
|
||||
(is (search "Action rejected by skill heuristics" (getf (getf result :payload) :text)))))
|
||||
(is (eq :LOG (getf result :type)))))
|
||||
|
||||
@@ -1,81 +0,0 @@
|
||||
(defpackage :opencortex-self-edit-tests
|
||||
(:use :cl :fiveam :opencortex)
|
||||
(:export #:self-edit-suite))
|
||||
|
||||
(in-package :opencortex-self-edit-tests)
|
||||
|
||||
(def-suite self-edit-suite
|
||||
:description "Tests for Self-Edit skill.")
|
||||
|
||||
(in-suite self-edit-suite)
|
||||
|
||||
(test balance-parens-balanced
|
||||
(let ((result (opencortex::self-edit-balance-parens "(+ 1 2)")))
|
||||
(is (string= result "(+ 1 2)"))
|
||||
(is (not (null (read-from-string result))))))
|
||||
|
||||
(test balance-parens-missing-open
|
||||
(let ((result (opencortex::self-edit-balance-parens "+ 1 2)")))
|
||||
(is (string= result "(+ 1 2)"))
|
||||
(is (not (null (read-from-string result))))))
|
||||
|
||||
(test balance-parens-missing-close
|
||||
(let ((result (opencortex::self-edit-balance-parens "(+ 1 2")))
|
||||
(is (string= result "(+ 1 2)"))
|
||||
(is (not (null (read-from-string result))))))
|
||||
|
||||
(test balance-parens-deep
|
||||
(let ((result (opencortex::self-edit-balance-parens "((lambda (x) (if x (+ 1 2) 3))")))
|
||||
(is (string= result "((lambda (x) (if x (+ 1 2) 3)))"))
|
||||
(is (not (null (read-from-string result))))))
|
||||
|
||||
(test balance-parens-empty
|
||||
(let ((result (opencortex::self-edit-balance-parens "")))
|
||||
(is (string= result ""))))
|
||||
|
||||
(test test-self-edit-apply-success
|
||||
"Verify self-edit-apply performs surgical replacement correctly."
|
||||
(let ((test-file "/tmp/self-edit-test.lisp"))
|
||||
(unwind-protect
|
||||
(progn
|
||||
(with-open-file (out test-file :direction :output :if-exists :supersede)
|
||||
(write-string "(defun hello () (format t \"world~%\"))" out))
|
||||
(let ((result (opencortex::self-edit-apply test-file "world" "universe")))
|
||||
(is (eq (getf result :status) :success))
|
||||
(let ((content (uiop:read-file-string test-file)))
|
||||
(is (search "universe" content))
|
||||
(is (not (search "world" content))))))
|
||||
(uiop:delete-file-if-exists test-file))))
|
||||
|
||||
(test test-self-edit-apply-not-found
|
||||
"Verify self-edit-apply returns error when pattern not found."
|
||||
(let ((test-file "/tmp/self-edit-test2.lisp"))
|
||||
(unwind-protect
|
||||
(progn
|
||||
(with-open-file (out test-file :direction :output :if-exists :supersede)
|
||||
(write-string "(defun hello () t)" out))
|
||||
(let ((result (opencortex::self-edit-apply test-file "nonexistent-pattern" "new")))
|
||||
(is (eq (getf result :status) :error))
|
||||
(is (search "not found" (getf result :message)))))
|
||||
(uiop:delete-file-if-exists test-file))))
|
||||
|
||||
(test test-self-edit-apply-file-not-found
|
||||
"Verify self-edit-apply returns error when file does not exist."
|
||||
(let ((result (opencortex::self-edit-apply "/nonexistent/path/file.lisp" "old" "new")))
|
||||
(is (eq (getf result :status) :error))
|
||||
(is (search "not found" (getf result :message)))))
|
||||
|
||||
(test test-self-edit-parse-location-from-payload
|
||||
"Verify self-edit-parse-location extracts file/line from payload."
|
||||
(let ((context '(:payload (:file "/tmp/test.lisp" :line 42 :message "error"))))
|
||||
(let ((result (opencortex::self-edit-parse-location context)))
|
||||
(is (equal "/tmp/test.lisp" (getf result :file)))
|
||||
(is (eq 42 (getf result :line))))))
|
||||
|
||||
(test test-self-edit-parse-location-from-message
|
||||
"Verify self-edit-parse-location extracts file/line from error message."
|
||||
(let ((context '(:payload (:message "Error in /home/user/project/foo.lisp at line 99"))))
|
||||
(let ((result (opencortex::self-edit-parse-location context)))
|
||||
(is (listp result))
|
||||
(is (getf result :line))
|
||||
(is (eq 99 (getf result :line))))))
|
||||
@@ -1,34 +0,0 @@
|
||||
(defpackage :opencortex-tool-permissions-tests
|
||||
(:use :cl :fiveam :opencortex)
|
||||
(:export #:tool-permissions-suite))
|
||||
|
||||
(in-package :opencortex-tool-permissions-tests)
|
||||
|
||||
(def-suite tool-permissions-suite
|
||||
:description "Tests for Tool Permissions skill")
|
||||
|
||||
(in-suite tool-permissions-suite)
|
||||
|
||||
(test default-permission-is-allow
|
||||
"Verify default permission is :allow."
|
||||
(is (eq (get-tool-permission "unknown-tool") :allow)))
|
||||
|
||||
(test set-and-get-permission
|
||||
"Verify setting and getting permissions."
|
||||
(set-tool-permission "test-tool-abc" :deny)
|
||||
(is (eq (get-tool-permission "test-tool-abc") :deny)))
|
||||
|
||||
(test permission-gate-allow
|
||||
"Verify :allow tier passes through."
|
||||
(set-tool-permission "gate-allow-tool" :allow)
|
||||
(is (eq (check-tool-permission-gate "gate-allow-tool" nil) :allow)))
|
||||
|
||||
(test permission-gate-deny
|
||||
"Verify :deny tier blocks."
|
||||
(set-tool-permission "gate-deny-tool" :deny)
|
||||
(is (eq (check-tool-permission-gate "gate-deny-tool" nil) :deny)))
|
||||
|
||||
(test permission-gate-ask
|
||||
"Verify :ask tier returns ask list."
|
||||
(set-tool-permission "gate-ask-tool" :ask)
|
||||
(is (listp (check-tool-permission-gate "gate-ask-tool" nil))))
|
||||
@@ -1,20 +1,22 @@
|
||||
(defpackage :opencortex-tui-tests
|
||||
(:use :cl :fiveam :opencortex)
|
||||
(:use :cl :opencortex)
|
||||
(:export #:tui-suite))
|
||||
|
||||
(in-package :opencortex-tui-tests)
|
||||
|
||||
(def-suite tui-suite :description "Verification of the TUI parsing and styling logic")
|
||||
(eval-when (:compile-toplevel :load-toplevel :execute)
|
||||
(ql:quickload :fiveam :silent t))
|
||||
|
||||
(in-suite tui-suite)
|
||||
(fiveam:def-suite tui-suite :description "Verification of the TUI parsing and styling logic")
|
||||
(fiveam:in-suite tui-suite)
|
||||
|
||||
(test test-tui-connection-drop
|
||||
(fiveam:test test-tui-connection-drop
|
||||
"Tier 2 Chaos: Verify that handle-return degrades gracefully when the daemon connection is lost."
|
||||
(let ((opencortex.tui::*chat-history* nil)
|
||||
(let ((opencortex.tui::*incoming-msgs* nil)
|
||||
(opencortex.tui::*input-buffer* (make-array 5 :element-type 'char :initial-contents "hello" :fill-pointer 5 :adjustable t))
|
||||
;; Create a closed stream to simulate connection drop
|
||||
(mock-stream (make-string-output-stream)))
|
||||
(close mock-stream)
|
||||
(opencortex.tui::handle-return mock-stream)
|
||||
;; Check if the error was enqueued to history instead of crashing
|
||||
(is (member "ERROR: Connection to daemon lost." opencortex.tui::*chat-history* :test #'string=))))
|
||||
(fiveam:is (member "ERROR: Connection to daemon lost." opencortex.tui::*incoming-msgs* :test #'string=))))
|
||||
|
||||
125
tests/utils-lisp-tests.org
Normal file
125
tests/utils-lisp-tests.org
Normal file
@@ -0,0 +1,125 @@
|
||||
#+TITLE: Tests: Utils Lisp
|
||||
#+AUTHOR: Agent
|
||||
#+PROPERTY: header-args:lisp :tangle utils-lisp-tests.lisp
|
||||
|
||||
* Overview
|
||||
Verification of the structural, syntactic, and semantic gates of the Lisp Validator.
|
||||
|
||||
* Implementation
|
||||
|
||||
** Package Context
|
||||
#+begin_src lisp
|
||||
(defpackage :opencortex-utils-lisp-tests
|
||||
(:use :cl :fiveam :opencortex)
|
||||
(:export #:utils-lisp-suite))
|
||||
|
||||
(in-package :opencortex-utils-lisp-tests)
|
||||
|
||||
(def-suite utils-lisp-suite
|
||||
:description "Tests for the Lisp Validator structural, syntactic, and semantic gates")
|
||||
|
||||
(in-suite utils-lisp-suite)
|
||||
#+end_src
|
||||
|
||||
** Structural Balanced
|
||||
#+begin_src lisp
|
||||
(test structural-balanced
|
||||
(is (eq t (opencortex:utils-lisp-check-structural "(+ 1 2)"))))
|
||||
#+end_src
|
||||
|
||||
** Structural Unbalanced (Open)
|
||||
#+begin_src lisp
|
||||
(test structural-unbalanced-open
|
||||
(multiple-value-bind (ok reason) (opencortex:utils-lisp-check-structural "(+ 1 2")
|
||||
(is (null ok))
|
||||
(is (search "Reader Error" reason))))
|
||||
#+end_src
|
||||
|
||||
** Structural Unbalanced (Close)
|
||||
#+begin_src lisp
|
||||
(test structural-unbalanced-close
|
||||
(multiple-value-bind (ok reason) (opencortex:utils-lisp-check-structural "+ 1 2)")
|
||||
(is (null ok))
|
||||
(is (search "Reader Error" reason))))
|
||||
#+end_src
|
||||
|
||||
** Syntactic Valid
|
||||
#+begin_src lisp
|
||||
(test syntactic-valid
|
||||
(is (eq t (opencortex:utils-lisp-check-syntactic "(+ 1 2)"))))
|
||||
#+end_src
|
||||
|
||||
** Semantic Safe
|
||||
#+begin_src lisp
|
||||
(test semantic-safe
|
||||
(is (eq t (opencortex:utils-lisp-check-semantic "(+ 1 2)"))))
|
||||
#+end_src
|
||||
|
||||
** Semantic Blocked (Eval)
|
||||
#+begin_src lisp
|
||||
(test semantic-blocked-eval
|
||||
(multiple-value-bind (ok reason) (opencortex:utils-lisp-check-semantic "(eval '(+ 1 2))")
|
||||
(is (null ok))
|
||||
(is (search "Unsafe" reason))))
|
||||
#+end_src
|
||||
|
||||
** Unified Success
|
||||
#+begin_src lisp
|
||||
(test unified-success
|
||||
(let ((result (opencortex:utils-lisp-validate "(+ 1 2)" :strict t)))
|
||||
(is (eq (getf result :status) :success))))
|
||||
#+end_src
|
||||
|
||||
** Unified Failure
|
||||
#+begin_src lisp
|
||||
(test unified-failure
|
||||
(let ((result (opencortex:utils-lisp-validate "(+ 1 2" :strict nil)))
|
||||
(is (eq (getf result :status) :error))))
|
||||
#+end_src
|
||||
|
||||
** Evaluation (Basic)
|
||||
#+begin_src lisp
|
||||
(test eval-basic
|
||||
(let ((result (opencortex:utils-lisp-eval "(+ 1 2)")))
|
||||
(is (eq (getf result :status) :success))
|
||||
(is (string= (getf result :result) "3"))))
|
||||
#+end_src
|
||||
|
||||
** Structural Extraction
|
||||
#+begin_src lisp
|
||||
(test structural-extract
|
||||
(let* ((code "(defun hello () (print \"hi\")) (defun bye () (print \"bye\"))")
|
||||
(extracted (opencortex:utils-lisp-structural-extract code "hello")))
|
||||
(is (not (null extracted)))
|
||||
(let ((form (read-from-string extracted)))
|
||||
(is (eq (car form) 'DEFUN))
|
||||
(is (eq (second form) 'HELLO)))))
|
||||
#+end_src
|
||||
|
||||
** List Definitions
|
||||
#+begin_src lisp
|
||||
(test list-definitions
|
||||
(let ((code "(defun foo () t) (defmacro bar () nil) (defparameter *baz* 10)"))
|
||||
(let ((names (opencortex:utils-lisp-list-definitions code)))
|
||||
(is (member 'FOO names))
|
||||
(is (member 'BAR names))
|
||||
(is (member '*BAZ* names)))))
|
||||
#+end_src
|
||||
|
||||
** Structural Injection
|
||||
#+begin_src lisp
|
||||
(test structural-inject
|
||||
(let* ((code "(defun my-fun (x) (print x))")
|
||||
(injected (opencortex:utils-lisp-structural-inject code "my-fun" "(finish-output)")))
|
||||
(let ((form (read-from-string injected)))
|
||||
(is (equal (last form) '((FINISH-OUTPUT)))))))
|
||||
#+end_src
|
||||
|
||||
** Structural Slurp
|
||||
#+begin_src lisp
|
||||
(test structural-slurp
|
||||
(let* ((code "(defun work () (step-1))")
|
||||
(slurped (opencortex:utils-lisp-structural-slurp code "work" "(step-2)")))
|
||||
(let ((form (read-from-string slurped)))
|
||||
(is (equal (last form) '((STEP-2)))))))
|
||||
#+end_src
|
||||
58
tests/utils-org-tests.org
Normal file
58
tests/utils-org-tests.org
Normal file
@@ -0,0 +1,58 @@
|
||||
#+TITLE: Tests: Utils Org
|
||||
#+AUTHOR: Agent
|
||||
#+PROPERTY: header-args:lisp :tangle utils-org-tests.lisp
|
||||
|
||||
* Overview
|
||||
Verification of the structural manipulation for Org-mode files and their AST representation.
|
||||
|
||||
* Implementation
|
||||
|
||||
** Package Context
|
||||
#+begin_src lisp
|
||||
(defpackage :opencortex-utils-org-tests
|
||||
(:use :cl :fiveam :opencortex)
|
||||
(:export #:utils-org-suite))
|
||||
|
||||
(in-package :opencortex-utils-org-tests)
|
||||
|
||||
(def-suite utils-org-suite
|
||||
:description "Tests for Utils Org skill.")
|
||||
|
||||
(in-suite utils-org-suite)
|
||||
#+end_src
|
||||
|
||||
** ID Generation
|
||||
#+begin_src lisp
|
||||
(test id-generation
|
||||
(let ((id1 (utils-org-generate-id))
|
||||
(id2 (utils-org-generate-id)))
|
||||
(is (plusp (length id1)))
|
||||
(is (not (string= id1 id2))))) ;; Likely unique
|
||||
#+end_src
|
||||
|
||||
** ID Format
|
||||
#+begin_src lisp
|
||||
(test id-format
|
||||
(let ((formatted (utils-org-id-format "abc12345")))
|
||||
(is (search "id:" formatted))))
|
||||
#+end_src
|
||||
|
||||
** Property Setter
|
||||
#+begin_src lisp
|
||||
(test property-setter
|
||||
(let ((ast (list :type :HEADLINE
|
||||
:properties (list :ID "id:test123" :TITLE "Test")
|
||||
:contents nil)))
|
||||
(utils-org-set-property ast "id:test123" :STATUS "ACTIVE")
|
||||
(is (string= (getf (getf ast :properties) :STATUS) "ACTIVE"))))
|
||||
#+end_src
|
||||
|
||||
** TODO Setter
|
||||
#+begin_src lisp
|
||||
(test todo-setter
|
||||
(let ((ast (list :type :HEADLINE
|
||||
:properties (list :ID "id:todo001" :TITLE "Task")
|
||||
:contents nil)))
|
||||
(utils-org-set-todo ast "id:todo001" "DONE")
|
||||
(is (string= (getf (getf ast :properties) :TODO) "DONE"))))
|
||||
#+end_src
|
||||
Reference in New Issue
Block a user