Some checks failed
Deploy-Agent-V15-Stdin / JOB-V15-STDIN (push) Failing after 3s
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
5.7 KiB
5.7 KiB
OpenCortex TUI Client (Standalone)
Overview
The OpenCortex TUI Client is a standalone Common Lisp application built on Croatoan.
Test Suite
(defpackage :opencortex-tui-tests
(:use :cl :opencortex)
(:export #:tui-suite))
(in-package :opencortex-tui-tests)
(eval-when (:compile-toplevel :load-toplevel :execute)
(ql:quickload :fiveam :silent t))
(fiveam:def-suite tui-suite :description "Verification of the TUI parsing and styling logic")
(fiveam:in-suite tui-suite)
(fiveam:test test-tui-connection-drop
"Tier 2 Chaos: Verify that handle-return degrades gracefully when the daemon connection is lost."
(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
(fiveam:is (member "ERROR: Connection to daemon lost." opencortex.tui::*incoming-msgs* :test #'string=))))
Implementation
Package Context
(in-package :cl-user)
(defpackage :opencortex.tui
(:use :cl :croatoan :usocket)
(:export :main))
(in-package :opencortex.tui)
Global State
(defvar *daemon-host* "127.0.0.1")
(defvar *daemon-port* 9105)
(defvar *socket* nil)
(defvar *stream* nil)
(defvar *chat-history* nil)
(defvar *scroll-index* 0)
(defvar *input-buffer* (make-array 0 :element-type 'char :fill-pointer 0 :adjustable t))
(defvar *is-running* t)
(defvar *queue-lock* (bt:make-lock))
(defvar *incoming-msgs* nil)
Utilities
(defun enqueue-msg (msg)
"Thread-safe addition to incoming message queue."
(bt:with-lock-held (*queue-lock*)
(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 *incoming-msgs*))
(setf *incoming-msgs* nil)
msgs)))
(defun get-line-style (text)
(cond
((uiop:string-prefix-p "*" text) '(:bold :yellow))
((uiop:string-prefix-p "⬆" text) '(:cyan))
((uiop:string-prefix-p "🤔" text) '(:italic))
((uiop:string-prefix-p "ERROR" text) '(:bold :red))
(t nil)))
Rendering
(defun render-chat (win)
(clear win)
(let* ((h (height win))
(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 (add-string win (format nil "│ ~a" msg) :y i :x 1 :attributes (get-line-style msg)))
(refresh win)))
Input Handling
(defun handle-backspace ()
(when (> (fill-pointer *input-buffer*) 0)
(decf (fill-pointer *input-buffer*))))
(defun handle-return (stream)
(let ((cmd (coerce *input-buffer* 'string)))
(setf (fill-pointer *input-buffer*) 0)
(when (> (length cmd) 0)
(enqueue-msg (format nil "⬆ ~a" cmd))
(handler-case
(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)
(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))))
Main Entry Point
(defun main ()
(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*))
(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)))))
(setf *is-running* nil)
(when *socket* (ignore-errors (usocket:socket-close *socket*)))))