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
11 KiB
SKILL: Context Manager (org-skill-context-manager.org)
- Overview
- Implementation
- Context Stack
- context-max-depth
- Context Accessors
- current-scope
- current-project
- current-base-path
- context-stack-depth
- Stack Operations
- pop-context
- with-context
- Path Resolution
- Memory Scope Filtering
- project-objects
- Project Focus Convenience
- focus-session
- focus-memex
- unfocus
- Skill Registration
- Persistence
- Skill Registration
- Auto-Init: Wire Scope Resolver
- Contract
- Test Suite
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
- (push-context &key project base-path scope): pushes a context plist
onto
*context-stack*and persists to disk. - (pop-context): pops the top context, persists, returns restored context.
- (context-save): serializes
*context-stack*to the persistence file. - (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))))))