(defvar *context-stack* nil "Stack of context plists. Each plist has :project, :base-path, :scope. Top of stack (car) is the current context.") (defvar *context-max-depth* 10 "Maximum context stack depth. Prevents runaway pushes.") (defun current-context () "Returns the current context plist, or nil if no context is set." (car *context-stack*)) (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)) (defun current-project () "Returns the current project name, or nil." (getf (current-context) :project)) (defun current-base-path () "Returns the current base path for file resolution, or nil." (getf (current-context) :base-path)) (defun context-stack-depth () "Returns the current depth of the context stack." (length *context-stack*)) (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*) (log-message "CONTEXT: Pushed ~a (depth ~d)" project (context-stack-depth)) context)) (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*))) (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))) (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)) (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))) (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))) (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)) (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)) (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)) (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))) (defun unfocus () "Pop the top context and return to the previous one." (pop-context)) (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)) (when (boundp '*scope-resolver*) (setf *scope-resolver* #'current-scope))