Skip to content

wommy/wom.womr-kanban-governor

v0.1.0MIT

Structural priority governor for the Hermes kanban working lane.

womr-kanban-governor

Keeps unranked working-lane kanban cards ranked, in-process, on the hermes dispatcher's own tick.

The problem it replaces

Ready-dispatch orders by priority DESC, created_at ASC. specify and decompose both mint working-lane cards without a priority, so cards arrive at 0, the ORDER BY degenerates to oldest-first FIFO, and the drain hands out whatever is oldest.

The predecessor was an external script on a systemd timer that shelled out one subprocess per card (~3.3s each, including a first attempt at a rail verb that no longer exists). At ~2450 unranked cards that is ~2.2 hours of subprocess churn against a 2-minute timer and a 30-minute unit timeout — so laps stopped completing at all, and the board went ungoverned while intake continued.

This does the same work with direct DB writes inside a hook hermes already fires. No subprocess, no timer, no external scheduler that can fall behind.

Seam

on_kanban_dispatch_tick fires once per dispatcher tick, strictly after _dispatch_tick_lock is released (kanban_db.py:9858/9886:349), so a slow subscriber cannot extend the single-writer critical section. The hook is observer-only: the return value is ignored and this plugin never raises into the dispatcher.

Guards

GuardWhy it is load-bearing
Only priority == 0 rows are scoredan operator's hand-set number is never a candidate
Computed scores clamped < HAND_SET_FLOOR (90)structure can never outvote human judgement
UPDATE ... AND priority = 0 CAS on writea hand-set landing mid-tick still wins
max_writes bound (default 200)the shared tick can never be spent without limit
Interval throttle (default 120s)scoring cost is decoupled from tick frequency
connect_closing, never connectlong-lived gateway would otherwise leak FDs (#33159)
PRAGMA busy_timeout pinned (8s)hook is synchronous on the tick path; an unbounded wait stalls dispatch itself
sqlite3.OperationalError yields the ticka busy board is normal, not an error — never contend with the live writer
All exceptions swallowed + loggeda governor bug must not take down dispatch
dry_run ticks return immediatelynever writes during a dry-run dispatch

Scoring

Structural only — no judgement about what a card is about:

  • leverage = open transitive descendants (what finishing this card releases)
  • free = no OPEN parent (nothing upstream blocks it)

Free cards score 60 + min(leverage, 29); gated cards 30 + min(leverage, 19); recognised test fixtures sink to 1. Bands do not overlap, so a free card always outranks a gated one. Leverage traversal is iterative with a seen-set: a cycle in the link graph cannot hang the tick.

Activation (operator-gated)

Installed disabled. Nothing loads until it is explicitly enabled:

hermes plugins list                          # shows: disabled
hermes plugins enable womr-kanban-governor

Takes effect for new dispatcher processes; the gateway must be restarted to pick it up (a separate, explicitly gated action).

Rollback

hermes plugins disable womr-kanban-governor

Emergency kill switch without touching config — the plugin returns immediately on every tick:

WOMR_KANBAN_GOVERNOR_DISABLE=1

Tuning

EnvDefaultMeaning
WOMR_KANBAN_GOVERNOR_DISABLEunsetany non-empty, non-0/false value disables
WOMR_KANBAN_GOVERNOR_MAX_WRITES200cards ranked per run
WOMR_KANBAN_GOVERNOR_INTERVAL_SECONDS120minimum seconds between runs
WOMR_KANBAN_GOVERNOR_LOCK_BUDGET_MS8000fail-fast SQLite busy budget

Test

python3 test/test_governor.py

12 tests, pure-python, no hermes runtime and no DB — the scoring contract is provable without a board. Validate the wiring against the real loader with:

hermes plugins doctor .