Some checks failed
Deploy (Gitea) / deploy (push) Has been cancelled
Adds *scope-resolver* hook in core-loop-perceive that the context-manager skill sets at load time. The perceive gate now passes the active scope to ingest-ast, tagging all ingested objects with the current context scope. Implementation: - core-loop-perceive: *scope-resolver* defvar (default nil → :memex), two ingest-ast calls now pass (if *scope-resolver* (funcall it) :memex) - core-defpackage: export *scope-resolver* and context-query - system-context-manager: auto-init sets *scope-resolver* to #'current-scope This completes the Memory Scope Segmentation feature: all three scopes (:memex, :session, :project) are supported, scope-aware retrieval (context-query :scope / context-scoped-query) was already implemented, and ingested objects now correctly carry the active scope.
122 lines
4.6 KiB
Common Lisp
122 lines
4.6 KiB
Common Lisp
(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))
|