Skip to content

feat(kanban): add worker-lane registry for the dispatcher - #2

Open
nikitaBarkov wants to merge 1 commit into
mainfrom
nikita.barkov/workers
Open

feat(kanban): add worker-lane registry for the dispatcher#2
nikitaBarkov wants to merge 1 commit into
mainfrom
nikita.barkov/workers

Conversation

@nikitaBarkov

@nikitaBarkov nikitaBarkov commented Jun 26, 2026

Copy link
Copy Markdown

Add a worker-lane registry so an out-of-tree integration can register a custom spawn_fn for a non-Hermes assignee — letting the kanban dispatcher launch that runtime directly instead of the default hermes -p <profile> worker.

What does this PR do?

Problem. The dispatcher's worker spawn is hardcoded to hermes -p <assignee> chat (_default_spawn). There was no supported way for an out-of-tree integration (an external CLI runner) to be spawned for a task — the worker-lanes doc explicitly calls this "not yet a paved path." An assignee that isn't a Hermes profile just sits in ready as skipped_nonspawnable.

Solution. A process-local worker-lane registry. A plugin registers a lane at load time via ctx.register_worker_lane(WorkerLane(name, spawn_fn, ...)); the dispatcher then consults the registry when it resolves a ready task's assignee:

  • spawn resolution (_resolve_spawn_fn): an explicit spawn_fn (tests / callers) wins, else a registered lane's spawn_fn, else the default Hermes-profile spawn — so existing profile lanes are unchanged.
  • a registered lane assignee counts as spawnable (_assignee_is_spawnable / has_spawnable_ready / has_spawnable_review), so a lane task is dispatched rather than skipped or stranded.
  • an optional per-lane max_concurrency caps in-flight workers for that lane (falls back to the global per-profile cap).
  • lanes get the same worker contract as Hermes workers via kanban_worker_env (the HERMES_KANBAN_* env vars), so an external runner sees an identical task/workspace/run environment.

The registry is process-local, so registration must happen in the process that runs the dispatcher (typically the gateway that owns kanban dispatch). The lane contract a spawned worker must satisfy (exactly one terminal kanban action, heartbeat, etc.) is documented in website/docs/user-guide/features/kanban-worker-lanes.md.

Related Issue

N/A — no tracking issue in this fork. Context: the worker-lanes doc references upstream NousResearch issue #19931 (external CLI worker lane) and the closed-unmerged Codex-specific PR #19924; this PR paves the generic mechanism those describe.

A more recent upstream attempt at the same feature is #29777 — its author OK'd a separate implementation (comment). That PR has grown large and drifted from main, so this is a focused, independent take on the same idea.

Type of Change

  • 🐛 Bug fix (non-breaking change that fixes an issue)
  • ✨ New feature (non-breaking change that adds functionality)
  • 🔒 Security fix
  • 📝 Documentation update
  • ✅ Tests (adding or improving test coverage)
  • ♻️ Refactor (no behavior change)
  • 🎯 New skill (bundled or hub)

Changes Made

  • hermes_cli/worker_lanes.py (new): process-local WorkerLane registry — register_worker_lane / get_worker_lane / is_worker_lane_assignee / list_worker_lanes / clear_worker_lanes, lane-name normalization, and kanban_worker_env (the shared HERMES_KANBAN_* worker contract). WorkerLane(name, spawn_fn, kind="", max_concurrency=None) with validation.
  • hermes_cli/plugins.py: PluginContext.register_worker_lane(lane, *, replace=False) so a plugin registers a lane at load time.
  • hermes_cli/kanban_db.py: dispatcher integration — _get_worker_lane, _resolve_spawn_fn (explicit → lane → default), a registered lane assignee treated as spawnable in _assignee_is_spawnable / has_spawnable_ready / has_spawnable_review, and per-lane max_concurrency honored in the dispatch loop's effective concurrency cap. Default profile-spawn path unchanged.
  • tests/hermes_cli/test_worker_lanes.py (new): registry (register / normalize / duplicate-reject / validation), spawn resolution precedence, kanban_worker_env contract, spawnable-counting, dispatch routing, and per-lane concurrency cap (dry-run and real-run).

How to Test

pytest tests/hermes_cli/test_worker_lanes.py -q

→ 14 passed.

Behavior check: from a plugin's register(ctx), call ctx.register_worker_lane(WorkerLane(name="my-runner", spawn_fn=my_spawn)); create a task with assignee="my-runner"; run the dispatcher. It resolves my_spawn (not hermes -p), passes the HERMES_KANBAN_* env, and — with max_concurrency set — caps that lane's in-flight workers. An unregistered lane assignee stays skipped_nonspawnable (not silently dropped).

Checklist

Code

  • I've read the Contributing Guide
  • My commit messages follow Conventional Commits (feat(kanban):)
  • I searched for existing PRs to avoid duplicates — overlaps the earlier upstream attempt #29777; its author OK'd a separate implementation (comment), and that PR has grown large and fallen behind main
  • My PR contains only changes related to this feature
  • Ran the new suite (pytest tests/hermes_cli/test_worker_lanes.py -q) — 14 passed; please run the full pytest tests/ -q before merge
  • I've added tests for my changes
  • I've tested on my platform: macOS (Darwin 24.6)

Documentation & Housekeeping

  • The lane contract is documented in website/docs/user-guide/features/kanban-worker-lanes.md (existing) — no doc change needed in this PR
  • No new config keys
  • No architecture/workflow doc change required (AGENTS.md unchanged) — or N/A
  • Considered cross-platform impact — pure Python, no OS-specific code
  • Tool descriptions/schemas — N/A (no tool changes)

@github-actions

github-actions Bot commented Jun 26, 2026

Copy link
Copy Markdown

🔎 Lint report: nikita.barkov/workers vs origin/main

ruff

Total: 0 on HEAD, 0 on base (➖ 0)

🆕 New issues: none

✅ Fixed issues: none

Unchanged: 0 pre-existing issues carried over.

ty (type checker)

Total: 11488 on HEAD, 11488 on base (➖ 0)

🆕 New issues (2):

Rule Count
unresolved-import 1
invalid-argument-type 1
First entries
tests/hermes_cli/test_worker_lanes.py:16: [unresolved-import] unresolved-import: Cannot resolve imported module `pytest`
tests/hermes_cli/test_worker_lanes.py:89: [invalid-argument-type] invalid-argument-type: Argument is incorrect: Expected `(...) -> int | None`, found `Literal["not-callable"]`

✅ Fixed issues (1):

Rule Count
invalid-assignment 1
First entries
hermes_cli/kanban_db.py:7321: [invalid-assignment] invalid-assignment: Object of type `None` is not assignable to `def profile_exists(name: str) -> bool`

Unchanged: 6036 pre-existing issues carried over.

Diagnostics are surfaced as warnings — this check never fails the build.

Let an out-of-tree integration register a custom spawn_fn for a non-Hermes
assignee, so the kanban dispatcher can launch that runtime (e.g. a Codex /
Claude Code CLI runner) directly instead of the default `hermes -p <profile>`
worker. Paves the "external CLI worker lane" path the worker-lanes doc
describes.

- hermes_cli/worker_lanes.py: process-local WorkerLane registry
  (register / get / list / clear, name normalization, kanban_worker_env) with
  an optional per-lane max_concurrency.
- hermes_cli/plugins.py: ctx.register_worker_lane(...) so a plugin can register
  a lane at load time.
- hermes_cli/kanban_db.py: the dispatcher resolves the spawn_fn from the
  registry (_resolve_spawn_fn: an explicit arg wins, then a registered lane,
  then the default Hermes-profile spawn); a registered lane assignee counts as
  spawnable and honors the lane's max_concurrency cap.
- tests/hermes_cli/test_worker_lanes.py: registry, dispatcher-resolution, and
  concurrency-cap coverage.
@nikitaBarkov
nikitaBarkov force-pushed the nikita.barkov/workers branch from 303f344 to 3461018 Compare July 3, 2026 08:08
@nikitaBarkov nikitaBarkov changed the title Add lane workers registry feat(kanban): add worker-lane registry for the dispatcher Jul 3, 2026
nikitaBarkov pushed a commit that referenced this pull request Aug 3, 2026
… a broken chat

A completely unconfigured install previously booted into a working-looking
chat (banner showed model 'unknown'), accepted a message, spun ~30s, then
failed with 'Set OPENROUTER_API_KEY' — a provider the user never chose —
and never offered setup.

- HermesCLI.run() now probes provider readiness at startup (TTY only) and
  offers the shared provider picker (hermes model flow, which fronts Quick
  Setup / Nous Portal OAuth) when nothing is configured. Decline is
  respected; picker state re-syncs into the live CLI so the next turn works
  without a restart.
- New silent probe _runtime_credentials_ready(): no printing, no state
  mutation; handles keyless local endpoints and callable bearer providers.
- The empty-api-key error is provider-aware: names the actual resolved
  provider and points at 'hermes model' / 'hermes setup' instead of
  hardcoding OPENROUTER_API_KEY.
- Banner: unconfigured installs render 'no model configured — run /model'
  in red instead of the silent 'unknown' model slug.

Consumer-onboarding audit finding #2 (sev 5), Aug 2026.
nikitaBarkov pushed a commit that referenced this pull request Aug 3, 2026
A wedged adapter transport (network hang, dead websocket) previously
blocked _check_session_stalls forever: sibling candidates in the same
pass were never evaluated and the watcher stopped ticking. Wrap the
send in asyncio.wait_for (15s); on timeout log a WARNING and do NOT
latch, so the next tick retries. Regression uses a never-resolving fake
adapter and proves the pass completes, a healthy sibling candidate is
still notified in the same pass, and the watcher ticks again
(sabotage-verified against the unbounded send).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant