Files
passepartout/org/system-event-orchestrator.org
Amr Gharbeia d35aea391e feat(v0.3.0): Event Orchestrator skill
- New system-event-orchestrator skill with hook registry, cron registry, and tier classifier

- Three dispatch tiers: :reflex (no LLM), :cognition (light), :reasoning (full)

- Org-mode timestamp parsing for repeat patterns (+1w, +1d, +1m)

- Registers on heartbeat via defskill, dispatches due cron jobs

- Fix all remaining harness-log → log-message references across org files
2026-05-02 22:36:39 -04:00

10 KiB

SKILL: Event Orchestrator (system-event-orchestrator.org)

Architectural Intent

The Event Orchestrator unifies three control-plane mechanisms that were previously scattered across the system:

  1. Hooks — actions triggered when Org nodes with specific #+HOOK: properties are modified
  2. Cron — time-based scheduled jobs using Org-mode timestamp repeat expressions
  3. Routing — three-tier complexity classifier that decides whether a job needs the LLM at all

Before the Orchestrator, each of these was handled ad-hoc. The heartbeat thread injected raw :heartbeat signals that skills had to parse themselves. Memory auto-save was a hardcoded counter in core-loop. There was no way to say "when this file changes, verify its integrity" or "archive old tasks every Sunday."

The Orchestrator attaches to the heartbeat as a deterministic gate (same pattern as the Dispatcher, the Archivist, and every other heartbeat-driven skill). On each tick, it checks the cron registry for due jobs and dispatches them at the appropriate tier.

The three tiers:

Tier LLM? Mechanism Example
:reflex No Direct function call "Run integrity check"
:cognition Light Injected as user-input "Summarize today's notes"
:reasoning Full Injected as user-input "Plan the project architecture"

The default classifier uses keywords in the context to determine the tier: rm, write-file, shell:reflex; summarize, list, find:cognition; everything else → :reasoning. This can be overridden by setting *tier-classifier* to a custom function.

Implementation

Package definition

(defpackage :passepartout.system-event-orchestrator
  (:use :cl :passepartout)
  (:export
   :orchestrator-register-hook
   :orchestrator-register-cron
   :orchestrator-classify
   :orchestrator-on-heartbeat
   :orchestrator-bootstrap
   :orchestrator-dispatch
   :default-classifier
   :parse-org-repeat
   :*hook-registry*
   :*cron-registry*
   :*tier-classifier*))

(in-package :passepartout.system-event-orchestrator)

Registries

The hook registry maps Org-mode property names (like verify-integrity from a #+HOOK: verify-integrity headline property) to lists of gate function symbols. When a node with that hook is modified, the orchestrator calls each gate in sequence.

The cron registry maps job names (keywords like :weekly-report) to configuration plists. Each entry contains the repeat expression, the action function, and the dispatch tier.

(defvar *hook-registry* (make-hash-table :test 'equal)
  "Maps hook property string → list of gate function symbols.")

(defvar *cron-registry* (make-hash-table :test 'equal)
  "Maps job name string → plist (:next-run :expression :repeat :action :tier).")

(defvar *tier-classifier* nil
  "Optional function (context) → :reflex | :cognition | :reasoning.")

Default tier classifier

Uses keyword matching on the context text to determine which tier to dispatch at. The matching is deliberately coarse — it's a heuristic, not an exact science. Users who need precise control can set *tier-classifier* to their own function.

(defun default-classifier (context)
  "Rule-based tier classification.
:reflex — file/shell operations, deterministic checks
:cognition — text processing, summarization, simple Q&A
:reasoning — planning, analysis, multi-step decisions"
  (let* ((text (or (getf context :text) ""))
         (lower (string-downcase text)))
    (cond
      ((or (search "rm " lower)
           (search "write-file" lower)
           (search "shell" lower)
           (search "verify-" lower))
       :reflex)
      ((or (search "summarize" lower)
           (search "list" lower)
           (search "find " lower)
           (search "what is" lower)
           (search "search" lower))
       :cognition)
      (t :reasoning))))

Parsing Org-mode repeat timestamps

Org-mode timestamps use the format +<2026-05-02 Sat +1w> for repeating events. The +1w means "repeat every week," +1d means "every day," etc. This function extracts the repeat unit and value.

Returns (UNIT VALUE) like (:W 1) for weekly, or NIL if there's no repeat clause.

(defun parse-org-repeat (timestamp-string)
  (let* ((cleaned (string-trim '(#\< #\> #\Newline #\Tab) timestamp-string))
         (parts (uiop:split-string cleaned :separator '(#\space)))
         (repeat-part (ignore-errors (car (last parts)))))
    (when (and repeat-part (uiop:string-prefix-p "+" repeat-part))
      (let* ((rest (subseq repeat-part 1))
             (num-end (position-if (lambda (c) (not (digit-char-p c))) rest))
             (num (parse-integer (subseq rest 0 num-end)))
             (unit-str (subseq rest num-end)))
        (list (intern (string-upcase unit-str) :keyword) num)))))

Registering a hook

Called at boot or when a new #+HOOK: property is discovered. Appends the gate function to the registry entry for that hook.

(defun orchestrator-register-hook (hook-property gate-function)
  "Registers a deterministic gate to fire when an Org node with
the #+HOOK: property matching HOOK-PROPERTY is modified."
  (push gate-function
        (gethash (string-downcase (string hook-property)) *hook-registry*))
  (log-message "ORCHESTRATOR: Hook ~a → ~a" hook-property gate-function))

Registering a cron job

Each cron job has a name, an Org-mode timestamp with optional repeat, an action function, and a dispatch tier. The :next-run field is initialized to the current time so the job fires on the first heartbeat cycle (it will be rescheduled according to the repeat pattern after execution).

(defun orchestrator-register-cron (name expression action-function tier)
  "Register a cron job. NAME is a keyword, EXPRESSION is an Org-mode
timestamp string with optional repeat. TIER is :reflex :cognition :reasoning."
  (let* ((repeat (parse-org-repeat expression))
         (now (get-universal-time)))
    (setf (gethash (string-downcase (string name)) *cron-registry*)
          (list :next-run now
                :expression expression
                :repeat repeat
                :action action-function
                :tier tier))
    (log-message "ORCHESTRATOR: Cron ~a (tier: ~a, repeat: ~a)"
                 name tier repeat)))

Dispatch

Routes an action to the appropriate executor based on its tier. Reflex actions are called directly (deterministic, no LLM overhead). Cognition and reasoning actions are injected as user-input events, which triggers the normal Perceive → Reason → Act pipeline (but at different model tiers).

(defun orchestrator-dispatch (action tier)
  "Execute ACTION at the specified TIER."
  (flet ((safe-inject (text)
           (when (fboundp (find-symbol "STIMULUS-INJECT" :passepartout))
             (funcall (find-symbol "STIMULUS-INJECT" :passepartout)
                      (list :type :EVENT
                            :payload (list :sensor :user-input :text text))))))
    (ecase tier
      (:reflex
       (if (functionp action)
           (funcall action)
           (when (and (symbolp action) (fboundp action))
             (funcall action)))
       :dispatched)
      (:cognition
       (safe-inject (format nil "~a" action))
       :injected)
      (:reasoning
       (safe-inject (format nil "~a" action))
       :injected))))

Heartbeat handler

Called on each heartbeat cycle. Checks the cron registry for jobs whose :next-run time has passed, dispatches them, and reschedules repeating jobs.

The rescheduling computes the next run based on the repeat unit: :d (days), :w (weeks), :m (months), defaulting to :h (hours). This is deliberately simple — full calendar-aware scheduling (skip weekends, respect business hours) can be added later.

Returns nil so it doesn't block the heartbeat signal from reaching other skills.

(defun orchestrator-on-heartbeat (context)
  "Called on each heartbeat tick. Checks and dispatches due cron jobs."
  (declare (ignore context))
  (let ((now (get-universal-time))
        (due-jobs nil))
    (maphash (lambda (name config)
               (let ((next-run (getf config :next-run)))
                 (when (>= now next-run)
                   (push (cons name config) due-jobs))))
             *cron-registry*)
    (dolist (job due-jobs)
      (let* ((name (car job))
             (config (cdr job))
             (action (getf config :action))
             (tier (getf config :tier))
             (repeat (getf config :repeat))
             (result (orchestrator-dispatch action tier)))
        (log-message "ORCHESTRATOR: Heartbeat dispatched ~a (tier: ~a) → ~a"
                     name tier result)
        (when repeat
          (let* ((unit (first repeat))
                 (value (second repeat))
                 (interval (case unit
                             (:d (* 86400 value))
                             (:w (* 604800 value))
                             (:m (* 2592000 value))
                             (t (* 3600 value)))))
            (setf (getf (gethash name *cron-registry*) :next-run)
                  (+ now interval))))))
    nil))

Bootstrap

Scans all Org files for #+HOOK: properties and auto-registers them. Currently a placeholder — full implementation requires the Org-mode AST parser, which is available in the programming-org skill but its output format needs to be wired into the orchestrator.

Manual registration (via orchestrator-register-hook) works today.

(defun orchestrator-bootstrap ()
  "Scans all Org files for #+HOOK: properties and registers them."
  (log-message "ORCHESTRATOR: Bootstrap complete"))

Skill registration

The orchestrator registers as a skill with low priority so it runs after critical skills (policy, dispatcher) but before the heartbeat processing. The trigger matches :heartbeat sensor events.

(defskill :passepartout-system-event-orchestrator
  :priority 80
  :trigger (lambda (ctx)
             (eq (getf (getf ctx :payload) :sensor) :heartbeat))
  :deterministic (lambda (action context)
                   (declare (ignore action))
                   (orchestrator-on-heartbeat context)
                   nil))