From c0d3f066e8758653beb0afacc5f25ae0307d92a8 Mon Sep 17 00:00:00 2001 From: Amr Gharbeia Date: Wed, 29 Apr 2026 12:58:09 -0400 Subject: [PATCH] Proactive doctor, setup wizard, and TUI fixes BREAKING CHANGES / KNOWN ISSUES: - 8 skills have syntax errors causing loader warnings: org-skill-bouncer, org-skill-config-manager, org-skill-credentials-vault, org-skill-engineering-standards, org-skill-gardener, org-skill-homoiconic-memory, org-skill-peripheral-vision, org-skill-policy - These skills fail to load but don't block system operation - TUI works despite these errors FEATURES ADDED: 1. Proactive Doctor System - Doctor runs automatically on daemon startup - Health check runs before accepting connections - Adds /health endpoint for health status queries - *system-health* variable tracks: :healthy, :degraded, :unhealthy, :unknown 2. Error Handling (Option B - Debugger Hook) - TUI and CLI now run doctor diagnostics on errors - Shows "Run opencortex doctor" message on crash - Suggests repair commands after failures 3. Interactive Setup Wizard (org-skill-config-manager) - Full wizard implemented in config-manager skill: * LLM provider configuration (OpenAI, Anthropic, OpenRouter, Groq, Gemini, Ollama) * Gateway linking (Slack, Discord) * Memory settings (auto-save interval, history retention) * Network settings (timeout, proxy) - Saves to ~/.config/opencortex/.env (KEY=VALUE format) - CLI integration: opencortex setup, setup --add-provider, setup --link 4. CLI Enhancements - doctor --watch: Background health monitoring (60s interval) - doctor --fix: Interactive repair (falls back to full setup if core files missing) - setup command runs wizard or delegates to setup_system 5. TUI Fixes - Inlined message formatting to avoid dependency issues - Added error handling in handle-return - Cleaner error messages 6. Thin Harness Compliance - Removed doctor from harness (now in org-skill-diagnostics skill) - XDG directories: only .lisp in harness, .org kept in skills for loader --- harness/communication.org | 11 + harness/loop.org | 52 ++++- harness/tui-client.org | 19 +- opencortex.sh | 248 ++++++++++++++++++++-- skills/org-skill-bouncer.org | 34 +-- skills/org-skill-config-manager.org | 274 +++++++++++++++++++++++-- skills/org-skill-credentials-vault.org | 12 +- skills/org-skill-diagnostics.org | 257 +++++++++++++++++++---- 8 files changed, 805 insertions(+), 102 deletions(-) diff --git a/harness/communication.org b/harness/communication.org index 8742248..2990e8a 100644 --- a/harness/communication.org +++ b/harness/communication.org @@ -83,6 +83,17 @@ The ~communication.lisp~ module defines the low-level transport and framing logi (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)))) diff --git a/harness/loop.org b/harness/loop.org index 3348bd3..2c234ad 100644 --- a/harness/loop.org +++ b/harness/loop.org @@ -96,6 +96,44 @@ The Metabolic Loop is the fundamental rhythm of OpenCortex: the continuous proce (defvar *shutdown-save-enabled* t) #+end_src +** Health Status +#+begin_src lisp +(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.") +#+end_src + +** Proactive Doctor +#+begin_src lisp +(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 "==================================================~%~%")) +#+end_src + ** Main Entry Point (main) #+begin_src lisp (defun main () @@ -108,16 +146,20 @@ The Metabolic Loop is the fundamental rhythm of OpenCortex: the continuous proce (load-memory-from-disk) (initialize-actuators) (initialize-all-skills) + + ;; Run proactive doctor before starting services + (run-startup-health-check) + (start-heartbeat) (start-daemon) #+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))) (let ((sleep-interval (or (ignore-errors (parse-integer (uiop:getenv "DAEMON_SLEEP_INTERVAL"))) 3600))) (loop diff --git a/harness/tui-client.org b/harness/tui-client.org index 27cd8d6..ae528b7 100644 --- a/harness/tui-client.org +++ b/harness/tui-client.org @@ -38,7 +38,7 @@ The OpenCortex TUI Client is a standalone Common Lisp application built on **Cro #+begin_src lisp (in-package :cl-user) (defpackage :opencortex.tui - (:use :cl :croatoan) + (:use :cl :croatoan :usocket) (:export :main)) (in-package :opencortex.tui) #+end_src @@ -108,13 +108,18 @@ The OpenCortex TUI Client is a standalone Common Lisp application built on **Cro (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) - (declare (ignore c)) + (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)) diff --git a/opencortex.sh b/opencortex.sh index 35798d6..d77df69 100755 --- a/opencortex.sh +++ b/opencortex.sh @@ -32,6 +32,32 @@ if [ -f "$OC_CONFIG_DIR/.env" ]; then source "$OC_CONFIG_DIR/.env" fi +# --- Dependency Checker --- +check_dependencies() { + local missing=() + for dep in sbcl emacs git curl socat nc; do + if ! command_exists "$dep"; then + missing+=("$dep") + fi + done + + if [ ${#missing[@]} -gt 0 ]; then + echo -e "${YELLOW}--- Missing dependencies: ${missing[*]} ---${NC}" + if command_exists apt-get; then + echo "Attempting to install missing dependencies..." + if sudo apt-get update && sudo apt-get install -y sbcl emacs-nox rlwrap netcat-openbsd curl git socat libssl-dev libncurses-dev libffi-dev zlib1g-dev libsqlite3-dev 2>/dev/null; then + echo -e "${GREEN}✓ Dependencies installed successfully${NC}" + else + echo -e "${RED}✗ Could not install dependencies. Please run with sudo or install manually:${NC}" + echo " sudo apt-get install sbcl emacs-nox rlwrap netcat-openbsd curl git socat" + fi + else + echo -e "${RED}✗ Cannot auto-install: apt-get not available${NC}" + echo "Please install manually: sbcl emacs git curl socat netcat-openbsd" + fi + fi +} + # --- 2. SETUP --- setup_system() { NON_INTERACTIVE=false @@ -60,25 +86,32 @@ setup_system() { 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" export INSTALL_DIR="$OC_DATA_DIR" # Critical: Tangle manifest first to establish system structure (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 \"$OC_DATA_DIR/harness/manifest.org\")" >/dev/null 2>&1) || true + (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 # Tangle harness files into harness/ - for f in harness/*.org; do - if [ "$f" != "harness/manifest.org" ]; then - echo "Tangling $f..." - (cd "$OC_DATA_DIR/harness" && emacs -Q --batch --eval "(require 'org)" --eval "(setq org-confirm-babel-evaluate nil)" --eval "(org-babel-tangle-file \"$OC_DATA_DIR/$f\")" >/dev/null 2>&1) || true + for f in "$SCRIPT_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 fi done # Tangle skill files into skills/ - for f in skills/*.org; do - echo "Tangling $f..." - (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/$f\")" >/dev/null 2>&1) || true + 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 done # Special handling for tests that need to go into tests/ @@ -88,6 +121,11 @@ setup_system() { # 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 + cd "$SCRIPT_DIR" # Create the bin shim echo -e "${YELLOW}--- Creating Bin Shim in $OC_BIN_DIR/opencortex ---${NC}" ln -sf "$SCRIPT_DIR/opencortex.sh" "$OC_BIN_DIR/opencortex" @@ -107,6 +145,66 @@ setup_system() { --eval '(funcall (find-symbol "RUN-SETUP-WIZARD" :opencortex))' } +# --- Doctor Repair (Lightweight Fix) --- +doctor_repair() { + echo -e "${BLUE}=== OpenCortex: Repair Mode ===${NC}" + + # 1. Fix system dependencies + echo -e "${YELLOW}--- Fixing System Dependencies ---${NC}" + check_dependencies + + # 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" + + # 3. Re-tangle harness files that may be broken + echo -e "${YELLOW}--- Re-tangling Harness Files ---${NC}" + for f in "$SCRIPT_DIR/harness"/*.org; do + if [ -f "$f" ]; then + fname=$(basename "$f" .org) + echo " Checking harness/$fname..." + # Try to load each harness file - if it fails, re-tangle + if ! sbcl --non-interactive \ + --eval "(load \"$OC_DATA_DIR/harness/${fname}.lisp\")" \ + --eval "(format t \"OK~%\")" 2>/dev/null | grep -q "OK"; then + echo " Re-tangling $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 \"$f\")" >/dev/null 2>&1) || true + fi + fi + done + + # 4. Re-tangle skill files that may be broken + echo -e "${YELLOW}--- Re-tangling Skill Files ---${NC}" + for f in "$SCRIPT_DIR/skills"/*.org; do + 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/" + if ! sbcl --non-interactive \ + --eval "(load \"$OC_DATA_DIR/skills/${fname}.lisp\")" \ + --eval "(format t \"OK~%\")" 2>/dev/null | grep -q "OK"; then + echo " Re-tangling $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 \"$OC_DATA_DIR/skills/${fname}.org\")" >/dev/null 2>&1) || true + fi + rm -f "$OC_DATA_DIR/skills/${fname}.org" + fi + done + + # 5. Cleanup .org files + rm -f "$OC_DATA_DIR/harness"/*.org "$OC_DATA_DIR/skills"/*.org 2>/dev/null || true + + echo -e "${GREEN}--- Repair Complete ---${NC}" + echo "Run 'opencortex doctor' to verify the system." +} + # --- 3. COMMAND ROUTER --- COMMAND=$1 [ -z "$COMMAND" ] && COMMAND="cli" @@ -116,23 +214,82 @@ case "$COMMAND" in link) 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\")" ;; doctor) - 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 "DOCTOR-MAIN" :opencortex))' + check_dependencies + if [ "$1" = "--watch" ]; then + echo "Starting background health monitor (60s interval)..." + echo "Press Ctrl+C to stop." + echo "" + while true; do + echo "--- $(date '+%Y-%m-%d %H:%M:%S') ---" + 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 "DOCTOR-RUN-ALL" :opencortex))' \ + --eval '(uiop:quit 0)' 2>&1 | grep -E "(HEALTH|OK|FAIL|WARN|SYSTEM|===)" || true + sleep 60 + done + elif [ "$1" = "--fix" ]; then + # Check if major harness files exist - if not, run full setup + if [ ! -f "$OC_DATA_DIR/harness/package.lisp" ] || [ ! -f "$OC_DATA_DIR/harness/skills.lisp" ]; then + echo "Core files missing. Running full setup..." + setup_system "$@" + else + echo "Repairing system..." + doctor_repair + fi + else + 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 "DOCTOR-MAIN" :opencortex))' + fi ;; setup) - setup_system "$@" + check_dependencies + if [ "$1" = "--add-provider" ]; then + echo "Adding LLM provider..." + 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 "SETUP-ADD-PROVIDER" :opencortex))' + elif [ "$1" = "--link" ]; then + PLATFORM=$2 + TOKEN=$3 + if [ -z "$PLATFORM" ] || [ -z "$TOKEN" ]; then + echo "Usage: opencortex setup --link " + echo " platforms: slack, discord" + exit 1 + fi + echo "Linking $PLATFORM gateway..." + $0 link "$PLATFORM" "$TOKEN" + elif [ "$1" = "--non-interactive" ]; then + setup_system "$@" + else + # Run interactive setup wizard + echo "Starting interactive setup wizard..." + 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 "RUN-SETUP-WIZARD" :opencortex))' + fi ;; boot|--boot) + 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*)" \ @@ -140,16 +297,71 @@ case "$COMMAND" in --eval '(opencortex:main)' ;; + daemon) + check_dependencies + echo "Starting OpenCortex daemon in background..." + nohup 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 :croatoan))" \ + --eval '(opencortex:main)' \ + > "$OC_STATE_DIR/daemon.log" 2>&1 & + echo "Daemon started. Waiting for port 9105..." + for i in {1..20}; do + if ss -tln | grep -q 9105; then + echo "✓ Daemon ready on port 9105" + exit 0 + fi + sleep 1 + done + echo "✗ Daemon failed to start. Check $OC_STATE_DIR/daemon.log" + exit 1 + ;; + tui) - exec sbcl \ + check_dependencies + if ! ss -tln | grep -q 9105; then + echo "Daemon not running. Starting daemon first..." + $0 daemon + fi + if 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/tui)' \ - --eval '(opencortex.tui:main)' + --eval '(opencortex.tui:main)'; then + true + else + EXIT_CODE=$? + echo "" + echo "TUI exited with error. Running diagnostics..." + $0 doctor + echo "" + echo "Run 'opencortex doctor --fix' to repair, or 'opencortex setup' to reconfigure." + exit $EXIT_CODE + fi + ;; + + cli|boot) + check_dependencies + if 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 :croatoan))" \ + --eval '(opencortex:main)'; then + true + else + EXIT_CODE=$? + echo "" + echo "CLI exited with error. Running diagnostics..." + $0 doctor + echo "" + echo "Run 'opencortex doctor --fix' to repair, or 'opencortex setup' to reconfigure." + exit $EXIT_CODE + fi ;; *) - echo "Available commands: setup, link, doctor, boot, tui" + echo "Available commands: setup, link, doctor, boot, tui, cli, daemon" exit 1 ;; esac diff --git a/skills/org-skill-bouncer.org b/skills/org-skill-bouncer.org index bc3b87b..785ee08 100644 --- a/skills/org-skill-bouncer.org +++ b/skills/org-skill-bouncer.org @@ -16,8 +16,8 @@ The *Bouncer Skill* is the physical security layer of OpenCortex. It enforces op ** Security Configuration #+begin_src lisp (defvar *bouncer-network-whitelist* - '("api.telegram.org" "matrix.org" "googleapis.com" "openai.com" "anthropic.com - "Domains that the Bouncer considers safe for outbound connections. + '("api.telegram.org" "matrix.org" "googleapis.com" "openai.com" "anthropic.com") + "Domains that the Bouncer considers safe for outbound connections.") #+end_src ** Secret Scanning (bouncer-scan-secrets) @@ -56,9 +56,9 @@ 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) - (when (and (eq target :tool) (equal (proto-get payload :tool) "shell) - (proto-get (proto-get payload :args) :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))))) (approved (proto-get action :approved))) (cond @@ -71,15 +71,15 @@ The *Bouncer Skill* is the physical security layer of OpenCortex. It enforces op :payload (list :level :error :text (format nil "Action blocked: Potential exposure of '~a'" secret-name))))) - ((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. +((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)) - (and (eq target :tool) (member (proto-get payload :tool) '("shell" "repair-file :test #'string=)) - (and (eq target :emacs) (eq (proto-get payload :action) :eval))) +((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)))) (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))) @@ -90,7 +90,7 @@ The *Bouncer Skill* is the physical security layer of OpenCortex. It enforces op #+begin_src lisp (defun bouncer-process-approvals () "Scans for APPROVED flight plans and re-injects them." - (let ((approved-nodes (list-objects-with-attribute :TODO "APPROVED) + (let ((approved-nodes (list-objects-with-attribute :TODO "APPROVED")) (found-any nil)) (dolist (node approved-nodes) (let* ((attrs (org-object-attributes node)) @@ -102,7 +102,7 @@ The *Bouncer Skill* is the physical security layer of OpenCortex. It enforces op (when action (setf (getf action :approved) t) (inject-stimulus action) - (setf (getf (org-object-attributes node) :TODO) "DONE + (setf (getf (org-object-attributes node) :TODO) "DONE") (setq found-any t)))))) found-any)) #+end_src @@ -115,9 +115,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) diff --git a/skills/org-skill-config-manager.org b/skills/org-skill-config-manager.org index b223c3a..92a42b6 100644 --- a/skills/org-skill-config-manager.org +++ b/skills/org-skill-config-manager.org @@ -4,7 +4,7 @@ #+PROPERTY: header-args:lisp :tangle 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. +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 @@ -13,7 +13,7 @@ The *Config Manager* skill provides the OpenCortex Agent with the capability to (in-package :opencortex) #+end_src -** Configuration Logic +** Configuration Paths #+begin_src lisp (defun get-oc-config-dir () "Returns the absolute path to the opencortex config directory." @@ -22,19 +22,267 @@ The *Config Manager* skill provides the OpenCortex Agent with the capability to (uiop:ensure-directory-pathname xdg) (uiop:ensure-directory-pathname (merge-pathnames ".config/opencortex/" (user-homedir-pathname)))))) -(defun save-providers () - "Stubs for saving provider configuration." - (harness-log "CONFIG: Providers saved.")) +(defun get-config-file () + "Returns the path to the .env config file." + (merge-pathnames ".env" (get-oc-config-dir))) -(defun configure-provider (id) - "Stubs for configuring a provider." - (harness-log "CONFIG: Configured provider ~a" id)) +(defun ensure-config-dir () + "Ensures the config directory exists." + (let ((dir (get-oc-config-dir))) + (unless (uiop:directory-exists-p dir) + (uiop:ensure-directory-pathname dir)) + dir)) +#+end_src +** Config File Operations +#+begin_src lisp +(defun read-config-file () + "Reads the .env config file and returns an alist of KEY=VALUE pairs." + (let ((config-file (get-config-file))) + (when (uiop:file-exists-p config-file) + (let ((lines (uiop:read-file-lines config-file)) + (result nil)) + (dolist (line lines) + (when (and line (> (length line) 0) + (not (uiop:string-prefix-p "#" line))) + (let ((eq-pos (position #\= line))) + (when eq-pos + (let ((key (string-trim " " (subseq line 0 eq-pos))) + (value (string-trim " " (subseq line (1+ eq-pos))))) + (push (cons key value) result)))))) + (nreverse result))))) + +(defun write-config-file (config-alist) + "Writes the config alist to the .env file." + (ensure-config-dir) + (let ((config-file (get-config-file))) + (with-open-file (stream config-file :direction :output :if-exists :supersede :if-does-not-exist :create) + (format stream "# OpenCortex Configuration~%") + (format stream "# Generated by opencortex setup~%~%") + (dolist (pair config-alist) + (format stream "~a=~a~%" (car pair) (cdr pair)))))) + +(defun get-config-value (key) + "Gets a config value by key." + (let ((config (read-config-file))) + (cdr (assoc key config :test #'string=)))) + +(defun set-config-value (key value) + "Sets a config value and saves to file." + (let ((config (read-config-file)) + (pair (cons key value))) + (let ((existing (assoc key config :test #'string=))) + (if existing + (setf (cdr existing) value) + (push pair config))) + (write-config-file config)))) +#+end_src + +** Input Utilities +#+begin_src lisp +(defun prompt (prompt-text) + "Simple prompt that returns user input as a string." + (format t "~a" prompt-text) + (finish-output) + (read-line)) + +(defun prompt-yes-no (prompt-text) + "Prompts yes/no question. Returns T for yes, nil for no." + (let ((response (prompt (format nil "~a [Y/n]: " prompt-text)))) + (or (string= response "") + (string-equal response "Y") + (string-equal response "y") + (string-equal response "yes")))) + +(defun prompt-choice (prompt-text options) + "Prompts user to choose from a list of options. Returns the chosen option or nil." + (format t "~a~%" prompt-text) + (let ((i 1)) + (dolist (opt options) + (format t " ~a) ~a~%" i opt) + (incf i))) + (let ((response (prompt "Choice"))) + (let ((num (ignore-errors (parse-integer response)))) + (when (and num (<= 1 num) (>= (length options) num)) + (nth (1- num) options))))) +#+end_src + +** LLM Provider Setup +#+begin_src lisp +(defvar *available-providers* + '(("OpenAI" . "OPENAI_API_KEY") + ("Anthropic" . "ANTHROPIC_API_KEY") + ("OpenRouter" . "OPENROUTER_API_KEY") + ("Groq" . "GROQ_API_KEY") + ("Gemini" . "GEMINI_API_KEY") + ("Ollama (local)" . "OLLAMA_URL"))) + +(defun setup-llm-providers () + "Interactive wizard for configuring LLM providers." + (format t "~%~%") + (format t "==================================================~%") + (format t " LLM Provider Configuration~%") + (format t "==================================================~%~%") + + (let ((current-providers (loop for (name . key) in *available-providers* + when (get-config-value key) + collect name))) + (when current-providers + (format t "Current providers: ~{~a~^, ~}~%~%" current-providers)) + + (format t "Available providers:~%") + (dolist (p *available-providers*) + (format t " - ~a~%" (car p))) + (format t "~%") + + (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)))) + (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)))) + (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 "~%")) + +(defun setup-add-provider () + "Entry point for adding a single provider (called from CLI)." + (setup-llm-providers)) +#+end_src + +** Gateway Setup +#+begin_src lisp +(defun setup-gateways () + "Interactive wizard for configuring external gateways." + (format t "~%~%") + (format t "==================================================~%") + (format t " Gateway Configuration~%") + (format t "==================================================~%~%") + + (format t "Available gateways:~%") + (format t " - Slack (https://api.slack.com/)~%") + (format t " - Discord (https://discord.com/developers/)~%") + (format t "~%") + + (when (prompt-yes-no "Configure a gateway?") + (let ((chosen (prompt-choice "Select platform:" '("Slack" "Discord")))) + (when chosen + (let ((token (prompt (format nil "Enter ~a bot token: " chosen)))) + (if (string= chosen "Slack") + (set-config-value "SLACK_TOKEN" token) + (set-config-value "DISCORD_TOKEN" token)) + (format t "✓ ~a gateway configured~%" chosen)))))) + + (format t "~%")) +#+end_src + +** Skill Management +#+begin_src lisp +(defun setup-skills () + "Interactive wizard for enabling/disabling skills." + (format t "~%~%") + (format t "==================================================~%") + (format t " Skill Management~%") + (format t "==================================================~%~%") + + (format t "Note: Skill management is not yet implemented.~%") + (format t "Skills are automatically loaded from ~a~%" (or (uiop:getenv "SKILLS_DIR") "default location")) + (format t "~%")) +#+end_src + +** Memory Settings +#+begin_src lisp +(defun setup-memory () + "Interactive wizard for memory settings." + (format t "~%~%") + (format t "==================================================~%") + (format t " Memory Settings~%") + (format t "==================================================~%~%") + + (let ((auto-save (prompt "Auto-save interval in seconds [300]:"))) + (when (and auto-save (> (length auto-save) 0)) + (set-config-value "MEMORY_AUTO_SAVE_INTERVAL" auto-save))) + + (let ((history (prompt "History retention in lines [1000]:"))) + (when (and history (> (length history) 0)) + (set-config-value "MEMORY_HISTORY_RETENTION" history))) + + (format t "✓ Memory settings saved~%") + (format t "~%")) +#+end_src + +** Network Settings +#+begin_src lisp +(defun setup-network () + "Interactive wizard for network settings." + (format t "~%~%") + (format t "==================================================~%") + (format t " Network Settings~%") + (format t "==================================================~%~%") + + (let ((timeout (prompt "Request timeout in seconds [30]:"))) + (when (and timeout (> (length timeout) 0)) + (set-config-value "REQUEST_TIMEOUT" timeout))) + + (let ((proxy (prompt "Proxy URL (leave empty for none) []:"))) + (when (and proxy (> (length proxy) 0)) + (set-config-value "HTTP_PROXY" proxy))) + + (format t "✓ Network settings saved~%") + (format t "~%")) +#+end_src + +** Main Setup Wizard +#+begin_src lisp (defun run-setup-wizard () - "Interactive setup wizard for OpenCortex." - (format t "--- OpenCortex Setup Wizard ---~%") - (save-providers) - (doctor-main)) + "Main entry point for the interactive setup wizard." + (format t "~%~%") + (format t "╔═══════════════════════════════════════════════════╗~%") + (format t "║ OpenCortex Setup Wizard ║~%") + (format t "╚═══════════════════════════════════════════════════╝~%") + (format t "~%") + (format t "This wizard will help you configure:~%") + (format t " 1. LLM Providers (OpenAI, Anthropic, etc.)~%") + (format t " 2. Gateway Links (Slack, Discord)~%") + (format t " 3. Memory Settings~%") + (format t " 4. Network Settings~%") + (format t "~%") + + (ensure-config-dir) + + ;; Step 1: LLM Providers + (when (prompt-yes-no "Configure LLM providers?") + (setup-llm-providers)) + + ;; Step 2: Gateways + (when (prompt-yes-no "Configure gateways?") + (setup-gateways)) + + ;; Step 3: Memory + (when (prompt-yes-no "Configure memory settings?") + (setup-memory)) + + ;; Step 4: Network + (when (prompt-yes-no "Configure network settings?") + (setup-network)) + + ;; Summary + (format t "==================================================~%") + (format t " Setup Complete!~%") + (format t "==================================================~%") + (format t "~%") + (format t "Configuration saved to: ~a~%" (get-config-file)) + (format t "~%") + (format t "To verify your setup, run: opencortex doctor~%") + (format t "~%")) #+end_src ** Skill Registration @@ -42,4 +290,4 @@ The *Config Manager* skill provides the OpenCortex Agent with the capability to (defskill :skill-config-manager :priority 100 :trigger (lambda (ctx) (declare (ignore ctx)) nil)) -#+end_src +#+end_src \ No newline at end of file diff --git a/skills/org-skill-credentials-vault.org b/skills/org-skill-credentials-vault.org index 6e013b9..1ca0104 100644 --- a/skills/org-skill-credentials-vault.org +++ b/skills/org-skill-credentials-vault.org @@ -16,7 +16,7 @@ The *Credentials Vault* provides secure in-memory storage for sensitive API keys ** Vault Storage #+begin_src lisp (defvar *vault-memory* (make-hash-table :test 'equal) - "In-memory cache of sensitive credentials. + "In-memory cache of sensitive credentials.") #+end_src ** Secret Management @@ -28,11 +28,11 @@ The *Credentials Vault* provides secure in-memory storage for sensitive API keys (if val val (let ((env-var (case provider - (:gemini "GEMINI_API_KEY - (:openai "OPENAI_API_KEY - (:anthropic "ANTHROPIC_API_KEY - (:openrouter "OPENROUTER_API_KEY - (otherwise nil)))) + (:gemini "GEMINI_API_KEY") + (:openai "OPENAI_API_KEY") + (:anthropic "ANTHROPIC_API_KEY") + (:openrouter "OPENROUTER_API_KEY") + (otherwise nil)))) (when env-var (uiop:getenv env-var)))))) (defun vault-set-secret (provider secret &key (type :api-key)) diff --git a/skills/org-skill-diagnostics.org b/skills/org-skill-diagnostics.org index e32a6cb..6682931 100644 --- a/skills/org-skill-diagnostics.org +++ b/skills/org-skill-diagnostics.org @@ -4,64 +4,249 @@ #+PROPERTY: header-args:lisp :tangle org-skill-diagnostics.lisp * Overview -The *Diagnostics Skill* (Doctor) provides system-wide health checks and dependency verification. +The *Diagnostics Skill* (Doctor) provides system-wide health checks and dependency verification. It validates external dependencies, XDG environment, and LLM provider connectivity. -* Implementation +* Phase A: Demand (Thinking) +** Why a Doctor? +The Doctor transforms opaque startup failures into actionable engineering reports. It ensures the Brain never attempts to boot in a compromised state. + +** Detection Invariant +Binary detection must use shell probing (`which`) to account for varying `$PATH` inheritance between interactive and headless sessions. + +* Phase B: Protocol (Success Criteria) +- Dependency check passes when all required binaries are found +- Environment check passes when XDG directories exist and are accessible +- LLM check passes when at least one provider is configured or Ollama is running locally + +* Phase C: Implementation (Build) ** Package Context #+begin_src lisp (in-package :opencortex) #+end_src -** Dependency Check (doctor-check-dependencies) +** Global Configuration +#+begin_src lisp +(defvar *doctor-required-binaries* '("sbcl" "emacs" "git" "socat" "nc") + "List of external binaries required for full system operation.") + +(defvar *doctor-package-map* + '(("sbcl" . "sbcl") + ("emacs" . "emacs") + ("git" . "git") + ("socat" . "socat") + ("nc" . "netcat-openbsd") + ("curl" . "curl") + ("rlwrap" . "rlwrap")) + "Map binary names to apt package names.") + +(defvar *doctor-missing-deps* nil + "List of missing dependencies populated by doctor-check-dependencies.") + +(defvar *doctor-auto-install* t + "When T, doctor will attempt to install missing dependencies automatically.") +#+end_src + +** Dependency Verification #+begin_src lisp (defun doctor-check-dependencies () - "Verifies that all required external binaries are available." - (let ((deps '("sbcl" "emacs" "git" "curl" "nc")) - (all-ok t)) - (format t "DOCTOR: Checking System Dependencies...~%") - (dolist (dep deps) - (if (uiop:run-program (list "which" dep) :ignore-error-status t) - (format t " [OK] Found ~a~%" dep) - (progn - (format t " [FAIL] Missing ~a~%" dep) - (setf all-ok nil)))) + "Verifies that required external binaries are available in the PATH via shell probe." + (setf *doctor-missing-deps* nil) + (let ((all-ok t)) + (format t "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)) + (format t " [OK] Found ~a~%" dep) + (progn + (format t " [FAIL] Missing binary: ~a~%" dep) + (push dep *doctor-missing-deps*) + (setf all-ok nil))))) + (when (and all-ok (null *doctor-missing-deps*)) + (format t "DOCTOR: All dependencies satisfied.~%")) all-ok)) #+end_src -** XDG Check (doctor-check-xdg) +** Auto-Install Dependencies #+begin_src lisp -(defun doctor-check-xdg () - "Verifies XDG environment variables and directory structure." - (format t "DOCTOR: Checking XDG environment...~%") - (let ((vars '("OC_CONFIG_DIR" "OC_DATA_DIR" "OC_STATE_DIR" "MEMEX_DIR"))) - (dolist (var vars) - (let ((val (uiop:getenv var))) - (if val - (format t " [OK] ~a: ~a~%" var val) - (format t " [WARN] ~a is not set.~%" var))))) - t) +(defun doctor-install-dependencies () + "Attempts to install missing system dependencies via apt." + (when (null *doctor-missing-deps*) + (format t "DOCTOR: No missing dependencies to install.~%") + (return-from doctor-install-dependencies t)) + + (format t "DOCTOR: Attempting to install ~a missing dependencies...~%" (length *doctor-missing-deps*)) + + (let ((packages (remove-duplicates + (mapcar (lambda (dep) + (or (cdr (assoc dep *doctor-package-map* :test #'string=)) + dep)) + *doctor-missing-deps*) + :test #'string=))) + (format t "DOCTOR: Packages to install: ~a~%" packages) + + (let ((cmd (format nil "apt-get install -y ~{~a~^ ~}" packages))) + (format t "DOCTOR: Running: ~a~%" cmd) + (handler-case + (let ((output (uiop:run-program cmd + :output :string + :error-output :string + :external-format :utf-8))) + (if (zerop (uiop:run-program (format nil "which ~a" (car *doctor-missing-deps*)) + :ignore-error-status t)) + (progn + (format t "DOCTOR: Dependencies installed successfully.~%") + (setf *doctor-missing-deps* nil) + t) + (progn + (format t "DOCTOR: Installation failed. Output: ~a~%" output) + nil))) + (error (c) + (format t "DOCTOR: Installation error: ~a~%" c) + nil))))) #+end_src -** Main Diagnostic (doctor-main) +** XDG Environment Validation +#+begin_src lisp +(defun doctor-check-env () + "Validates XDG directories and environment configuration." + (format t "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) + (format t " [OK] ~a: ~a~%" name path) + (progn + (format t " [FAIL] ~a directory missing: ~a~%" name path) + (when critical (setf all-ok nil)))) + (progn + (format t " [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)) +#+end_src + +** LLM Connectivity +The doctor checks all supported LLM providers and detects local Ollama instances. + +#+begin_src lisp +(defun doctor-check-llm () + "Tests connectivity to LLM providers. Returns T if at least one provider is configured." + (format t "DOCTOR: Checking LLM connectivity...~%") + (let ((providers '((:openrouter . "OPENROUTER_API_KEY") + (:anthropic . "ANTHROPIC_API_KEY") + (:openai . "OPENAI_API_KEY") + (:groq . "GROQ_API_KEY") + (:gemini . "GEMINI_API_KEY") + (:ollama . "OLLAMA_URL"))) + (configured nil)) + (dolist (p providers) + (let ((env-val (uiop:getenv (cdr p)))) + (cond + ((and env-val (> (length env-val) 0)) + (format t " [OK] ~a configured~%" (car p)) + (setf configured t)) + ((eq (car p) :ollama) + (let ((ollama-check (ignore-errors + (uiop:run-program '("curl" "-s" "http://localhost:11434/api/tags") + :output :string :ignore-error-status t)))) + (when (and ollama-check (search "\"models\"" ollama-check)) + (format t " [OK] Ollama local model server detected~%") + (setf configured t))))))) + (if configured + (progn + (format t " [OK] LLM provider(s) available~%") + t) + (progn + (format t " [WARN] No LLM provider configured.~%") + (format t " Run 'opencortex setup' to configure a provider.~%") + t)))) +#+end_src + +** Orchestration +#+begin_src lisp +(defun doctor-run-all (&key (auto-install t)) + "Executes the full diagnostic suite and returns T if system is healthy." + (format t "==================================================~%") + (format t " OPENCORTEX DOCTOR: Commencing Health Check~%") + (format t "==================================================~%") + (let ((dep-ok (doctor-check-dependencies))) + (when (and (not dep-ok) auto-install *doctor-auto-install*) + (format t "DOCTOR: Attempting automatic installation...~%") + (setf dep-ok (doctor-install-dependencies)) + (when dep-ok + (setf dep-ok (doctor-check-dependencies)))) + (let ((env-ok (doctor-check-env)) + (llm-ok (doctor-check-llm))) + (format t "==================================================~%") + (if (and dep-ok env-ok) + (progn + (format t " ✓ SYSTEM HEALTHY: Ready for ignition.~%") + t) ;; Explicitly return T + (progn + (format t "==================================================~%") + (format t " ISSUES FOUND:~%") + (when (not dep-ok) + (format t " - Missing system dependencies~%")) + (when (not llm-ok) + (format t " - No LLM provider configured~%")) + (format t "~%") + (format t " RECOMMENDED ACTIONS:~%") + (format t " 1. Run 'opencortex setup' to configure everything~%") + (format t " 2. Or run 'opencortex doctor --fix' for auto-repair~%") + (format t "==================================================~%") + nil))))) ;; Return nil when issues found +#+end_src + +** CLI Entry Point #+begin_src lisp (defun doctor-main () - "Runs all diagnostic checks." - (format t "==================================================~%") - (format t " OpenCortex System Diagnostic~%") - (format t "==================================================~%") - (let ((d-ok (doctor-check-dependencies)) - (x-ok (doctor-check-xdg))) - (format t "==================================================~%") - (if (and d-ok x-ok) - (format t " ✓ SYSTEM HEALTHY: Ready for ignition.~%") - (format t " ✗ SYSTEM UNHEALTHY: Issues detected.~%")))) + "Entry point for the 'doctor' CLI command." + (if (doctor-run-all) + (uiop:quit 0) + (uiop:quit 1))) #+end_src +* Phase D: Verification (Testing) + +** Dependency Test +#+begin_src lisp :tangle no +(test test-doctor-dependency-check + "Verify that missing binaries are correctly identified as failures." + (let ((opencortex::*doctor-required-binaries* '("non-existent-binary-123"))) + (is (null (opencortex:doctor-check-dependencies))))) +#+end_src + +** Environment Test +#+begin_src lisp :tangle no +(test test-doctor-env-check + "Verify that an invalid MEMEX_DIR triggers a critical failure." + (let ((old-m (uiop:getenv "MEMEX_DIR"))) + (unwind-protect + (progn + (setf (uiop:getenv "MEMEX_DIR") "/non/existent/path/999") + (is (null (opencortex:doctor-check-env)))) + (setf (uiop:getenv "MEMEX_DIR") (or old-m ""))))) +#+end_src + +* Phase E: Lifecycle +The doctor skill should be loaded early (priority 100) to validate system health before other skills initialize. + ** Skill Registration #+begin_src lisp (defskill :skill-diagnostics :priority 100 :trigger (lambda (ctx) (eq (getf (getf ctx :payload) :sensor) :heartbeat)) :deterministic (lambda (action ctx) (declare (ignore action ctx)) nil)) -#+end_src +#+end_src \ No newline at end of file