From 48520ec5171dc50758737fe6f89e04a3bc2d6e77 Mon Sep 17 00:00:00 2001 From: Amr Gharbeia Date: Fri, 1 May 2026 12:43:25 -0400 Subject: [PATCH] refactor(harness): centralize mandates, fix TUI reader structure, and enhance memory/perceive --- GEMINI.md | 18 -------- harness/loop.lisp | 4 +- harness/memory.lisp | 7 ++- harness/memory.org | 10 ++++- harness/package.lisp | 1 + harness/package.org | 2 + harness/perceive.lisp | 1 + harness/perceive.org | 5 +++ harness/tui-client.lisp | 19 +++++---- harness/tui-client.org | 21 ++++----- opencortex.asd | 1 + scripts/harness-screen.sh | 62 +++++++++++++++++++++++++++ scripts/test-tui.sh | 89 +++++++++++++++++++++++++++++++++++++++ tests/tui-tests.lisp | 2 +- 14 files changed, 198 insertions(+), 44 deletions(-) delete mode 100644 GEMINI.md create mode 100755 scripts/harness-screen.sh create mode 100755 scripts/test-tui.sh diff --git a/GEMINI.md b/GEMINI.md deleted file mode 100644 index a97687b..0000000 --- a/GEMINI.md +++ /dev/null @@ -1,18 +0,0 @@ -# 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. diff --git a/harness/loop.lisp b/harness/loop.lisp index 6ddf3e7..3be7988 100644 --- a/harness/loop.lisp +++ b/harness/loop.lisp @@ -84,8 +84,8 @@ (format t "==================================================~%") (handler-case (progn - (when (fboundp 'doctor-run-all) - (let ((result (doctor-run-all :auto-install nil))) + (when (fboundp 'doctor-run-all) + (let ((result (doctor-run-all))) (setf *health-check-ran* t) (if result (progn diff --git a/harness/memory.lisp b/harness/memory.lisp index 176148c..9ff8364 100644 --- a/harness/memory.lisp +++ b/harness/memory.lisp @@ -4,13 +4,16 @@ (defvar *history-store* (make-hash-table :test 'equal) "Immutable Merkle-Tree versioning store mapping hashes to objects.") +(defun lookup-object (id) + (gethash id *memory*)) + (defstruct org-object id type attributes content vector parent-id children version last-sync hash) (defmethod make-load-form ((obj org-object) &optional env) (make-load-form-saving-slots obj :environment env)) -(defun copy-org-object (obj) +(defun deep-copy-org-object (obj) (make-org-object :id (org-object-id obj) :type (org-object-type obj) :attributes (copy-list (org-object-attributes obj)) @@ -71,7 +74,7 @@ (defun snapshot-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*) + (maphash (lambda (k v) (setf (gethash k snapshot) (deep-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))) (harness-log "MEMORY - CoW Memory snapshot created."))) diff --git a/harness/memory.org b/harness/memory.org index f2cf44d..3327794 100644 --- a/harness/memory.org +++ b/harness/memory.org @@ -21,6 +21,12 @@ The Memory module is the cognitive bedrock of the opencortex. It is not a databa "Immutable Merkle-Tree versioning store mapping hashes to objects.") #+end_src +** Object Lookup +#+begin_src lisp +(defun lookup-object (id) + (gethash id *memory*)) +#+end_src + ** The Data Structure (org-object) #+begin_src lisp (defstruct org-object @@ -29,7 +35,7 @@ The Memory module is the cognitive bedrock of the opencortex. It is not a databa (defmethod make-load-form ((obj org-object) &optional env) (make-load-form-saving-slots obj :environment env)) -(defun copy-org-object (obj) +(defun deep-copy-org-object (obj) (make-org-object :id (org-object-id obj) :type (org-object-type obj) :attributes (copy-list (org-object-attributes obj)) @@ -99,7 +105,7 @@ The Memory module is the cognitive bedrock of the opencortex. It is not a databa (defun snapshot-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*) + (maphash (lambda (k v) (setf (gethash k snapshot) (deep-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))) (harness-log "MEMORY - CoW Memory snapshot created."))) diff --git a/harness/package.lisp b/harness/package.lisp index 7b8f770..cce4ddc 100644 --- a/harness/package.lisp +++ b/harness/package.lisp @@ -254,6 +254,7 @@ ;; --- Debugger Hook --- (setf *debugger-hook* (lambda (condition hook) "Friendly error handler - shows diagnostic message instead of raw debugger." + (declare (ignore hook)) (format t "~%") (format t "┌─────────────────────────────────────────────┐~%") (format t "│ ERROR: ~A~%" (type-of condition)) diff --git a/harness/package.org b/harness/package.org index 0c5790a..24f5bb7 100644 --- a/harness/package.org +++ b/harness/package.org @@ -268,8 +268,10 @@ The ~package.lisp~ file defines the public API of the ~opencortex~ harness. (finish-output))) ;; --- Debugger Hook --- +#+begin_src lisp :tangle package.lisp (setf *debugger-hook* (lambda (condition hook) "Friendly error handler - shows diagnostic message instead of raw debugger." + (declare (ignore hook)) (format t "~%") (format t "┌─────────────────────────────────────────────┐~%") (format t "│ ERROR: ~A~%" (type-of condition)) diff --git a/harness/perceive.lisp b/harness/perceive.lisp index 5037c69..ddf1af4 100644 --- a/harness/perceive.lisp +++ b/harness/perceive.lisp @@ -1,5 +1,6 @@ (in-package :opencortex) +(defvar *interrupt-flag* nil) (defvar *async-sensors* '(:chat-message :delegation :user-command) "Sensors that are processed in dedicated threads.") diff --git a/harness/perceive.org b/harness/perceive.org index daddfac..b3ff848 100644 --- a/harness/perceive.org +++ b/harness/perceive.org @@ -14,6 +14,11 @@ The Perceive stage is the "sensory cortex" of OpenCortex. Its job is to take raw (in-package :opencortex) #+end_src +** Interrupt Handling +#+begin_src lisp +(defvar *interrupt-flag* nil) +#+end_src + ** Sensor Configuration #+begin_src lisp (defvar *async-sensors* '(:chat-message :delegation :user-command) diff --git a/harness/tui-client.lisp b/harness/tui-client.lisp index 0bca04f..d2c9ad3 100644 --- a/harness/tui-client.lisp +++ b/harness/tui-client.lisp @@ -1,6 +1,6 @@ (in-package :cl-user) (defpackage :opencortex.tui - (:use :cl :croatoan :usocket) + (:use :cl :croatoan :usocket :bordeaux-threads) (:export :main)) (in-package :opencortex.tui) @@ -10,7 +10,7 @@ (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 *input-buffer* (make-array 0 :element-type 'character :fill-pointer 0 :adjustable t)) (defvar *is-running* t) (defvar *queue-lock* (bt:make-lock)) (defvar *incoming-msgs* nil) @@ -70,10 +70,10 @@ (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)))) +(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." @@ -98,12 +98,13 @@ (getf payload :message)))) (t (let ((text (or (getf payload :text) (format nil "~a" payload)))) - (enqueue-msg (format nil "⬇ ~a" text))))))))) + (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")) + :name "opencortex-tui-reader"))) +) (defun main () (handler-case @@ -144,4 +145,4 @@ (error (c) (format t "TUI Error: ~a~%" c))) (setf *is-running* nil) - (when *socket* (ignore-errors (usocket:socket-close *socket*))))) + (when *socket* (ignore-errors (usocket:socket-close *socket*)))) diff --git a/harness/tui-client.org b/harness/tui-client.org index 45163e7..d6847c0 100644 --- a/harness/tui-client.org +++ b/harness/tui-client.org @@ -23,7 +23,7 @@ The OpenCortex TUI Client is a standalone Common Lisp application built on **Cro (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)) + (opencortex.tui::*input-buffer* (make-array 5 :element-type 'character :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) @@ -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 :usocket) + (:use :cl :croatoan :usocket :bordeaux-threads) (:export :main)) (in-package :opencortex.tui) #+end_src @@ -51,7 +51,7 @@ The OpenCortex TUI Client is a standalone Common Lisp application built on **Cro (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 *input-buffer* (make-array 0 :element-type 'character :fill-pointer 0 :adjustable t)) (defvar *is-running* t) (defvar *queue-lock* (bt:make-lock)) (defvar *incoming-msgs* nil) @@ -120,10 +120,10 @@ The OpenCortex TUI Client is a standalone Common Lisp application built on **Cro (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)))) +(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)))) #+end_src ** Background Reader @@ -151,12 +151,13 @@ The OpenCortex TUI Client is a standalone Common Lisp application built on **Cro (getf payload :message)))) (t (let ((text (or (getf payload :text) (format nil "~a" payload)))) - (enqueue-msg (format nil "⬇ ~a" text))))))))) + (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")) + :name "opencortex-tui-reader"))) +) #+end_src ** Main Entry Point @@ -200,5 +201,5 @@ The OpenCortex TUI Client is a standalone Common Lisp application built on **Cro (error (c) (format t "TUI Error: ~a~%" c))) (setf *is-running* nil) - (when *socket* (ignore-errors (usocket:socket-close *socket*))))) + (when *socket* (ignore-errors (usocket:socket-close *socket*)))) #+end_src diff --git a/opencortex.asd b/opencortex.asd index 0c91f5a..c284268 100644 --- a/opencortex.asd +++ b/opencortex.asd @@ -15,6 +15,7 @@ (:file "harness/perceive") (:file "harness/reason") (:file "harness/act") + (:file "harness/doctor") (:file "harness/loop"))) (defsystem :opencortex/tests diff --git a/scripts/harness-screen.sh b/scripts/harness-screen.sh new file mode 100755 index 0000000..e27ba27 --- /dev/null +++ b/scripts/harness-screen.sh @@ -0,0 +1,62 @@ +#!/bin/bash +# OpenCortex TUI Harness via GNU Screen +# Provides a persistent PTY for Croatoan/ncurses TUI testing. + +set -euo pipefail + +SESSION="oct-tui" +LOG="$HOME/.local/state/opencortex/tui-screen.log" + +function cleanup() { + screen -S "$SESSION" -X quit 2>/dev/null || true +} + +case "${1:-start}" in + start) + cleanup + mkdir -p "$(dirname "$LOG")" + export TERM=screen-256color + export SKILLS_DIR="$HOME/.local/share/opencortex/skills" + screen -dmS "$SESSION" bash -c ' + sbcl --non-interactive \ + --eval "(load (merge-pathnames \"quicklisp/setup.lisp\" (user-homedir-pathname)))" \ + --eval "(push (truename \"$HOME/.local/share/opencortex/\") asdf:*central-registry*)" \ + --eval "(ql:quickload :opencortex/tui :silent t)" \ + --eval "(opencortex.tui:main)" \ + 2>&1 | tee '"$LOG"' + echo "[TUI exited with code $?]" + sleep 3600 + ' + sleep 2 + echo "TUI started in screen session '$SESSION'" + echo "Logs: $LOG" + ;; + + send) + shift + screen -S "$SESSION" -X stuff "$*" + ;; + + enter) + screen -S "$SESSION" -X stuff "$(printf '\r')" + ;; + + capture) + screen -S "$SESSION" -X hardcopy -h /tmp/oct-tui-capture.txt + cat /tmp/oct-tui-capture.txt + ;; + + log) + tail -f "$LOG" + ;; + + kill) + cleanup + echo "TUI session killed." + ;; + + *) + echo "Usage: $0 {start|send |enter|capture|log|kill}" + exit 1 + ;; +esac diff --git a/scripts/test-tui.sh b/scripts/test-tui.sh new file mode 100755 index 0000000..f3bbde4 --- /dev/null +++ b/scripts/test-tui.sh @@ -0,0 +1,89 @@ +#!/bin/bash +# OpenCortex TUI Automated Test Harness +# Runs the TUI in a tmux pane, sends "hi", captures response. + +set -euo pipefail + +SESSION="opencortex-tui-test" +TUI_LOG="/tmp/opencortex-tui-test.log" +CAPTURE="/tmp/opencortex-tui-capture.txt" +TIMEOUT_SEC=30 + +echo "=== OpenCortex TUI Test Harness ===" +echo "Log: $TUI_LOG" +echo "Capture: $CAPTURE" + +# Clean up any stale session +tmux kill-session -t "$SESSION" 2>/dev/null || true + +# Verify daemon is running +if ! ss -tln | grep -q ':9105'; then + echo "ERROR: Daemon not running on port 9105" + echo "Start it with: cd ~/memex/projects/opencortex && ./opencortex.sh daemon" + exit 1 +fi + +# Create tmux session with TUI +echo "[1/5] Starting TUI in tmux session '$SESSION'..." +tmux new-session -d -s "$SESSION" \ + -e OC_CONFIG_DIR="$HOME/.config/opencortex" \ + -e OC_DATA_DIR="$HOME/.local/share/opencortex" \ + -e SKILLS_DIR="$HOME/.local/share/opencortex/skills" \ + -e TERM="screen-256color" \ + "sbcl --non-interactive \ + --eval '(load (merge-pathnames \"quicklisp/setup.lisp\" (user-homedir-pathname)))' \ + --eval '(push (truename \"$HOME/.local/share/opencortex/\") asdf:*central-registry*)' \ + --eval '(ql:quickload :opencortex/tui)' \ + --eval '(opencortex.tui:main)' 2>&1 | tee $TUI_LOG" + +sleep 3 + +# Capture initial state +tmux capture-pane -t "$SESSION" -p > "$CAPTURE" +echo "[2/5] Initial TUI state captured ($(wc -l < "$CAPTURE") lines)" + +# Send message +echo "[3/5] Sending 'hi' + Enter..." +tmux send-keys -t "$SESSION" "hi" Enter + +# Wait for response +echo "[4/5] Waiting up to ${TIMEOUT_SEC}s for response..." +for i in $(seq 1 $TIMEOUT_SEC); do + tmux capture-pane -t "$SESSION" -p > "$CAPTURE" + # Check if daemon response arrived (contains arrow-down marker or actual response text) + if grep -qE "(⬇|Hi|Hello|Neural Cascade)" "$CAPTURE"; then + echo " ✓ Response detected after ${i}s" + break + fi + sleep 1 +done + +# Final capture +tmux capture-pane -t "$SESSION" -p > "$CAPTURE" +echo "[5/5] Final capture ($(wc -l < "$CAPTURE") lines)" + +# Extract and display results +echo "" +echo "=== SCREEN CAPTURE ===" +cat "$CAPTURE" +echo "" +echo "=== TUI LOG (last 20 lines) ===" +tail -20 "$TUI_LOG" +echo "" + +# Check for errors +if grep -qE "(TUI Error|Connection lost|ERROR:)" "$TUI_LOG"; then + echo "❌ TEST FAILED: Errors detected in TUI log" + tmux kill-session -t "$SESSION" 2>/dev/null || true + exit 1 +fi + +if grep -qE "(⬇|Hi|Hello)" "$CAPTURE"; then + echo "✅ TEST PASSED: Response received from daemon" +else + echo "⚠️ TEST INCOMPLETE: No response marker found (daemon may have timed out)" +fi + +# Cleanup +tmux kill-session -t "$SESSION" 2>/dev/null || true +echo "Done." diff --git a/tests/tui-tests.lisp b/tests/tui-tests.lisp index ecd2d6e..285718a 100644 --- a/tests/tui-tests.lisp +++ b/tests/tui-tests.lisp @@ -13,7 +13,7 @@ (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)) + (opencortex.tui::*input-buffer* (make-array 5 :element-type 'character :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)