Files
passepartout/org/symbolic-scope.org
Amr Gharbeia da160b71e3
Some checks failed
Deploy (Gitea) / deploy (push) Failing after 2s
passepartout: v0.5.0 File Reorganization
Extract non-core fragments using self-repair criterion:
- core-context -> symbolic-awareness (224 lines, fboundp guards in think())
- heartbeat generation -> symbolic-events (renamed events-start-heartbeat)

Rename 23 files for clarity and new naming scheme:
- 6 core: core-package, core-transport, core-pipeline,
          core-perceive, core-reason, core-act
- 13 system: symbolic-*, neuro-*, embedding-*, channel-shell
- 4 gateway: channel-cli, channel-tui-*, channel-tui-state

Utility relocations:
- markdown-strip -> programming-markdown
- plist-keywords-normalize -> programming-lisp
- cognitive-tool-prompt -> programming-tools
- VAULT-MEMORY -> security-vault
- Merge *backend-registry* into *probabilistic-backends*

Split gateway-messaging into channel-telegram/channel-signal/
channel-discord/channel-slack (4 independent skills)

Delete dead system-model.lisp (16-line wrapper)

Document self-repair criterion in DESIGN_DECISIONS

Version bump: 0.4.3 -> 0.5.0
2026-05-07 18:20:48 -04:00

11 KiB

SKILL: Context Manager (org-skill-context-manager.org)

Overview

The Context Manager provides stack-based project focusing. When the agent "focuses" on a project, file paths resolve relative to it and memory queries auto-filter by scope. This enables the agent to work within a bounded context without being distracted by unrelated memory.

The core provides the mechanism (memory-object-scope, context-query with scope parameter). This skill provides the policy — what to focus on, what scope means for each project, and how the stack is managed.

Implementation

Context Stack

;; REPL-VERIFIED: 2026-05-03T13:00:00

(in-package :passepartout)

(defvar *context-stack* nil
  "Stack of context plists. Each plist has :project, :base-path, :scope.
Top of stack (car) is the current context.")

context-max-depth

;; REPL-VERIFIED: 2026-05-03T13:00:00

(defvar *context-max-depth* 10
  "Maximum context stack depth. Prevents runaway pushes.")

#+end_src

Context Accessors

;; REPL-VERIFIED: 2026-05-03T13:00:00

(defun current-context ()
  "Returns the current context plist, or nil if no context is set."
  (car *context-stack*))

current-scope

;; REPL-VERIFIED: 2026-05-03T13:00:00

(defun current-scope ()
  "Returns the current scope keyword (:memex/:session/:project).
Returns :memex when no context is set (defaults to global scope)."
  (or (getf (current-context) :scope) :memex))

current-project

;; REPL-VERIFIED: 2026-05-03T13:00:00

(defun current-project ()
  "Returns the current project name, or nil."
  (getf (current-context) :project))

current-base-path

;; REPL-VERIFIED: 2026-05-03T13:00:00

(defun current-base-path ()
  "Returns the current base path for file resolution, or nil."
  (getf (current-context) :base-path))

context-stack-depth

;; REPL-VERIFIED: 2026-05-03T13:00:00

(defun context-stack-depth ()
  "Returns the current depth of the context stack."
  (length *context-stack*))

#+end_src

Stack Operations

;; REPL-VERIFIED: 2026-05-03T13:00:00

(defun push-context (&key project base-path (scope :project))
  "Pushes a new context onto the stack. When focused on a project:
- File paths resolve relative to BASE-PATH
- Memory queries filter by SCOPE
- :memex scope objects remain visible (always global)
Returns the new context plist."
  (when (>= (context-stack-depth) *context-max-depth*)
    (log-message "CONTEXT: Stack depth limit reached (~d), refusing push" *context-max-depth*)
    (return-from push-context (current-context)))
  (let* ((context (list :project project
                        :base-path base-path
                        :scope scope)))
    (push context *context-stack*)
    (context-save)
    (log-message "CONTEXT: Pushed ~a (depth ~d)" project (context-stack-depth))
    context))

pop-context

;; REPL-VERIFIED: 2026-05-03T13:00:00

(defun pop-context ()
  "Pops the current context, restoring the previous one.
Returns the restored context or nil if stack becomes empty."
  (if *context-stack*
      (let ((popped (pop *context-stack*)))
        (context-save)
        (log-message "CONTEXT: Popped ~a (depth ~d)"
                     (getf popped :project) (context-stack-depth))
        (current-context))
      (progn
        (log-message "CONTEXT: Cannot pop — stack is empty")
        nil)))

with-context

;; REPL-VERIFIED: 2026-05-03T13:00:00

(defmacro with-context ((&key project base-path (scope :project)) &body body)
  "Executes BODY within a scoped context, then restores the previous context.
Example:
  (with-context (:project \"passepartout\" :base-path \"/home/user/memex/projects/passepartout\")
    (context-scoped-query :tag \"bug\"))"
  `(let ((*context-stack* (cons (list :project ,project
                                      :base-path ,base-path
                                      :scope ,scope)
                                *context-stack*)))
     ,@body))

#+end_src

Path Resolution

Resolves file paths relative to the current project's base path.

;; REPL-VERIFIED: 2026-05-03T13:00:00

(defun resolve-path (path)
  "Resolves a file path relative to the current context.
If PATH is absolute, returns it unchanged.
If PATH is relative and a base-path is set, merges them.
Otherwise returns PATH unchanged."
  (let ((base (current-base-path)))
    (if (and base path (not (uiop:absolute-pathname-p path)))
        (namestring (merge-pathnames path (uiop:ensure-directory-pathname base)))
        path)))

Memory Scope Filtering

Provides scope-aware query access. When a context is active (scope ≠ :memex), queries only return objects whose scope is :memex (global) or matches the current scope.

;; REPL-VERIFIED: 2026-05-03T13:00:00

(defun context-scoped-query (&key tag todo-state type)
  "Like context-query but filtered to the current context's scope.
:memex-scoped objects are always visible regardless of current scope."
  (context-query :tag tag :todo-state todo-state :type type :scope (current-scope)))

project-objects

;; REPL-VERIFIED: 2026-05-03T13:00:00

(defun project-objects ()
  "Returns all objects scoped to the current project.
Includes :memex-scoped objects (global knowledge) plus :project-scoped
objects matching the current project."
  (context-scoped-query))

#+end_src

Project Focus Convenience

;; REPL-VERIFIED: 2026-05-03T13:00:00

(defun focus-project (name base-path)
  "Shortcut: focus on a project by name and base path.
Calls push-context with :scope :project."
  (push-context :project name :base-path base-path :scope :project))

focus-session

;; REPL-VERIFIED: 2026-05-03T13:00:00

(defun focus-session ()
  "Shortcut: enter a session context (ephemeral scope).
Objects created in this scope are visible only during the session."
  (push-context :project "session" :scope :session))

focus-memex

;; REPL-VERIFIED: 2026-05-03T13:00:00

(defun focus-memex ()
  "Shortcut: return to global memex scope. Equivalent to pop-context
until stack is empty or :memex context is reached."
  (loop while (and *context-stack*
                   (not (eq (getf (current-context) :scope) :memex)))
        do (pop-context)))

unfocus

;; REPL-VERIFIED: 2026-05-03T13:00:00

(defun unfocus ()
  "Pop the top context and return to the previous one."
  (pop-context))

#+end_src

Skill Registration

Persistence

;; REPL-VERIFIED: 2026-05-05T12:00:00

(defvar *context-persistence-file* nil
  "Path to the context stack persistence file.")

(defun context-persist-file ()
  "Returns the full path to the context persistence file."
  (or *context-persistence-file*
      (setf *context-persistence-file*
            (merge-pathnames ".cache/passepartout/context.lisp"
                             (user-homedir-pathname)))))

(defun context-save ()
  "Writes *context-stack* to the persistence file."
  (handler-case
      (let ((path (context-persist-file)))
        (ensure-directories-exist (make-pathname :directory (pathname-directory path)))
        (with-open-file (s path :direction :output :if-exists :supersede
                                 :if-does-not-exist :create)
          (prin1 *context-stack* s))
        (log-message "CONTEXT: Saved stack (depth ~d) to ~a"
                     (length *context-stack*) path))
    (error (c)
      (log-message "CONTEXT: Failed to save: ~a" c))))

(defun context-load ()
  "Restores *context-stack* from the persistence file."
  (handler-case
      (let ((path (context-persist-file)))
        (when (probe-file path)
          (with-open-file (s path :direction :input)
            (let ((*read-eval* nil)
                  (data (read s nil nil)))
              (when (listp data)
                (setf *context-stack* data)
                (log-message "CONTEXT: Restored stack (depth ~d) from ~a"
                             (length *context-stack*) path))
              t))))
    (error (c)
      (log-message "CONTEXT: Failed to load: ~a" c)
      nil)))

Skill Registration

(defskill :passepartout-system-context-manager
  :priority 90
  :trigger (lambda (ctx) (declare (ignore ctx)) nil)
  :deterministic (lambda (action ctx)
                   (declare (ignore action))
                   (ignore-errors
                     (when (> (context-stack-depth) 0)
                       nil))
                   nil))

Auto-Init: Wire Scope Resolver

Registers current-scope into the core *scope-resolver* hook so the perceive gate tags ingested objects with the active context scope. Also restores any previously saved context stack.

(when (boundp '*scope-resolver*)
  (setf *scope-resolver* #'current-scope))

;; Restore persisted context on load
(context-load)

Contract

  1. (push-context &key project base-path scope): pushes a context plist onto *context-stack* and persists to disk.
  2. (pop-context): pops the top context, persists, returns restored context.
  3. (context-save): serializes *context-stack* to the persistence file.
  4. (context-load): restores *context-stack* from persistence file on boot.

Test Suite

(eval-when (:compile-toplevel :load-toplevel :execute)
  (ql:quickload :fiveam :silent t))

(defpackage :passepartout-context-tests
  (:use :cl :passepartout)
  (:export #:context-suite))

(in-package :passepartout-context-tests)

(fiveam:def-suite context-suite :description "Context manager verification")
(fiveam:in-suite context-suite)

(fiveam:test test-push-pop-context
  "Contract 1-2: push-context and pop-context maintain stack order."
  (let* ((pkg (find-package "PASSEPARTOUT.SKILLS.SYSTEM-CONTEXT-MANAGER"))
         (stack-var (and pkg (find-symbol "*CONTEXT-STACK*" pkg)))
         (pf-var (and pkg (find-symbol "*CONTEXT-PERSISTENCE-FILE*" pkg))))
    (when stack-var
      (setf (symbol-value stack-var) nil)
      (push-context :project "testapp" :base-path "/tmp" :scope :project)
      (fiveam:is (= 1 (length (symbol-value stack-var))))
      (fiveam:is (string= "testapp" (getf (car (symbol-value stack-var)) :project)))
      (pop-context)
      (fiveam:is (null (symbol-value stack-var))))))

(fiveam:test test-context-save-load
  "Contract 3-4: context-save and context-load round-trip."
  (let* ((pkg (find-package "PASSEPARTOUT.SKILLS.SYSTEM-CONTEXT-MANAGER"))
         (stack-var (and pkg (find-symbol "*CONTEXT-STACK*" pkg)))
         (pf-var (and pkg (find-symbol "*CONTEXT-PERSISTENCE-FILE*" pkg))))
    (when (and stack-var pf-var)
      (let* ((tmpfile (merge-pathnames "test-context.lisp" (uiop:temporary-directory))))
        (setf (symbol-value pf-var) tmpfile)
        (setf (symbol-value stack-var) (list '(:project "test" :base-path "/tmp" :scope :project)))
        (context-save)
        (fiveam:is (probe-file tmpfile))
        (setf (symbol-value stack-var) nil)
        (context-load)
        (fiveam:is (= 1 (length (symbol-value stack-var))))
        (fiveam:is (string= "test" (getf (car (symbol-value stack-var)) :project)))
        (ignore-errors (delete-file tmpfile))))))