scheduler

module
v1.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 3, 2026 License: MIT

README

Coding Hermes Scheduler

CI

Coding Hermes Scheduler

A single Go binary that replaces dozens of static cron jobs with a dynamic, priority-weighted fleet scheduler for LLM-powered coding agents.


What It Does

Instead of 33 cron jobs like */120 * * * * hermes chat -q "foreman tick for project X", you run ONE binary that:

  • Knows all your projects — weight, priority, cooldown, model, provider
  • Evaluates on demand — event-driven (startup, slot-freed debounce, or manual POST /api/v1/evaluate) with a 30s min-interval and a 5-min eval-stall watchdog
  • Packs greedily — fills a weight budget with the most urgent projects
  • Spawns foremen via HTTP — sends prompts to the Hermes gateway API (POST /v1/responses) instead of per-process hermes chat. Zero subprocess overhead, zero MCP duplication per tick
  • Falls back gracefully — if the gateway is unreachable, exec.Command(hermes, ...) handles it. Note: exec fallback is DISABLED by default (--no-exec-fallback defaults to true for safety); pass --no-exec-fallback=false to re-enable it.
  • Tracks outcomes — every tick is recorded (queued → running → completed/failed)
  • Exposes control — REST API, MCP, dashboard, DuckBrain sync
  • Auto-approves — scheduler agents send require_approval: false via the gateway API, so foremen run autonomously without pausing for user confirmation. User-facing chats (Telegram, Discord) keep approvals enabled.

Getting Started (5 minutes)

This guide takes you from zero to a running scheduler with your existing cron jobs imported.

1. Prerequisites
  • Go 1.26+go version
  • Hermes gateway running with API server enabled — curl http://127.0.0.1:8642/health
  • SQLite3sqlite3 --version
  • Existing cron jobs in Hermes (the scheduler imports from ~/.hermes/cron/jobs.json)
2. Clone and Build
git clone https://github.com/coding-hermes/scheduler.git
cd scheduler
make build

You now have:

  • ./bin/schedulerd — the daemon
  • ./bin/migrate — cron-to-scheduler migration tool
3. Verify API Access

The scheduler spawns foreman ticks through the Hermes gateway API. Verify your gateway is reachable:

curl http://127.0.0.1:8642/health
# → {"status":"ok","version":"0.18.2"}

# Check that the API server key is set
grep API_SERVER_KEY ~/.hermes/.env
4. Migrate Cron Jobs

Eligibility. The migration tool only imports jobs that pass BOTH filters:

  1. Coding-hermes job — the job name (case-insensitive) or any of its skills must contain coding-hermes or foreman.
  2. Workdir in prompt — the job prompt must contain a workdir path, e.g. Workdir: /home/... or workdir /home/....

Ineligible jobs are skipped with a per-job SKIP <name>: ... reason in the output, so you can see exactly why each job was not imported.

# Preview what will be imported
make migrate-dry

# Import to SQLite (creates ~/.hermes/coding-hermes/scheduler.db)
make migrate
5. Run the Scheduler
# Start the daemon on port 9090
./bin/schedulerd

You should see:

Database: /home/.../.hermes/coding-hermes/scheduler.db (WAL mode)
Loaded 27 projects, 0 namespaces
GATEWAY: connected to http://127.0.0.1:8642 — using HTTP API instead of exec.Command
HTTP: listening on 127.0.0.1:9090
schedulerd ready
6. Verify It's Working
# Health check
curl http://127.0.0.1:9090/api/v1/health
# → {"status":"ok","uptime":"5m","active_ticks":3}

# Fleet status
curl http://127.0.0.1:9090/api/v1/status | jq '.active_projects'

# Open the dashboard
open http://127.0.0.1:9090/
## Deployment

### Systemd

```bash
make deploy-install
sudo systemctl enable --now coding-hermes-scheduler
sudo systemctl status coding-hermes-scheduler

The gateway API key is loaded from a 0600 env file (/etc/coding-hermes/gateway.envAPI_SERVER_KEY=..., template: deploy/gateway.env.example) via EnvironmentFile; cmd/schedulerd/main.go defaults --gateway-key to $API_SERVER_KEY. Never pass --gateway-key on the command line — argv is world-readable via ps aux (GAP-038).

Local layout note (double nesting)

This repo's canonical checkout on the fleet host lives at /home/kara/coding-hermes-scheduler/coding-herms-scheduler/ (typo'd double nesting — the outer /home/kara/coding-hermes-scheduler/ directory is NOT the repo; it holds only the outer .coding-hermes/tasks.md pointer and previously a stale schedulerd binary, removed 2026-08-13). Build and run from the inner checkout: bin/schedulerd (the systemd unit builds it via ExecStartPre). repo_url for the repo is github.com/coding-hermes/scheduler (GAP-039/040).

For production fleets, run the scheduler on a dedicated Hermes gateway instance (separate cgroup, isolated MCPs, independent restart cycle). See deploy/gateway-setup.md for full setup instructions.

 Main Gateway (:8642)          Scheduler Gateway (:8643)
   ├─ main chat                   ├─ foreman tick A
   ├─ Telegram bridge             ├─ foreman tick B
   └─ ...                         └─ ...
         ↑                             ↑
    systemd cgroup              separate cgroup (MemoryMax=16G)

### What's Happening

The scheduler evaluates on demand — at startup, when a slot frees up, or via
manual `POST /api/v1/evaluate` (event-driven; 30s minimum interval). On each
evaluation it:
1. Computes urgency for each project (based on priority + time since last run)
2. Packs the most urgent projects into a weight budget (default 100)
3. Spawns foreman ticks via the Hermes gateway API
4. Records outcomes (queued → running → completed/failed)

A 5-minute eval-stall watchdog forces re-evaluation when the fleet sits idle,
so cooldown-expired projects don't go unscheduled (GAP-042).

You can monitor, pause, or adjust any project through the dashboard, REST API, or MCP tools.

---

## Architecture

┌──────────────────────────────────────────────┐ │ HERMES PLUGIN │ │ /fleet status, /fleet weight, /fleet pause │ └──────────────────┬───────────────────────────┘ │ HTTP POST /mcp ┌──────────────────▼───────────────────────────┐ │ SCHEDULER (Go binary) │ │ │ │ / → Dashboard (dark theme HTML) │ │ /api/v1/ → REST API (19 routes) │ │ /mcp → MCP server (14 tools) │ │ │ │ Eval Loop (event-driven): │ │ Urgency → Pack → Spawn → Track │ │ │ │ SQLite: projects, ticks, events │ └──────────────────────────────────────────────┘


---

## API Endpoints

| Method | Path | Description |
|--------|------|-------------|
| GET | `/` | Fleet dashboard (full HTML page) |
| GET | `/dashboard/partial` | htmx partial: project table refresh |
| GET | `/projects/{name}` | Per-project detail page |
| GET | `/queue` | Global queue view |
| GET | `/ticks?page=N` | Paginated tick history |
| GET | `/namespaces/{id}` | Namespace drill-down |
| GET | `/health` | Dashboard health panel |
| — | `/api/v1/*` | Full REST API — 19 routes (health/status/config, projects CRUD + pause/resume/spawn, namespaces + sub-routes, ticks, events, queue, pause/resume/evaluate): see [docs/api.md](docs/api.md) |
| POST | `/mcp` | MCP JSON-RPC endpoint |

---

## MCP Tools

| Tool | Description |
|------|-------------|
| `fleet_status` | Fleet-wide status and budget |
| `fleet_projects` | List all projects with config |
| `fleet_project_detail` | Get single project details |
| `fleet_set_weight` | Change project weight (1-100) |
| `fleet_set_priority` | Change project priority (1-10) |
| `fleet_set_cooldown` | Set cooldown duration |
| `fleet_set_decay` | Tune decay rate |
| `fleet_pause` | Pause a project |
| `fleet_resume` | Resume a project |
| `fleet_add` | Add a new project |
| `fleet_ticks` | List ticks for a project |
| `fleet_evaluate` | Force evaluation cycle |
| `fleet_pause_scheduler` | Pause the scheduler |
| `fleet_resume_scheduler` | Resume the scheduler |

---

## Scheduling Model

### Weight (1-100)
How much concurrency budget a project consumes per tick. Budget default: 100.

### Priority (1-10)
How frequently a project runs. Mapped to interval via geometric curve:

interval = min_interval × (max_interval / min_interval) ^ ((priority-1) / (levels-1))


| Priority | Interval (min=30s, max=24h) |
|----------|----------------------------|
| 10 | 30 seconds |
| 8 | ~3 minutes |
| 5 | ~42 minutes |
| 3 | ~4.1 hours |
| 1 | 24 hours |

### Urgency

urgency = priority × (1 + time_since_last_run / interval) ^ decay_rate


Higher urgency projects get picked first.

### Cooldown

Default 900s between successive ticks for the same project.

### Cooldown Policy (fleet-cooldown-policy.py)

Fleet-wide cooldown normalization is governed by the ops script
`~/.hermes/scripts/fleet-cooldown-policy.py` (not part of this repo — it lives
in the Hermes ops home; run `python3 ~/.hermes/scripts/fleet-cooldown-policy.py`
for a dry run, `--apply` to write). The script:

- Reads the live SQLite state first (`GET /api/v1/projects` equivalent), then
  regenerates `~/.hermes/fleet.toml` so every `[[projects]]` entry's
  `cooldown_s` matches the daemon's current value, and optionally PUTs
  normalized cooldowns back to the API.
- Honors the `ELEVATED_PINS` whitelist (e.g. `h3=21600`, `warpfs=43200`):
  projects with an operator-set pin are never written below their canonical
  cooldown (SCHED-GAP-012), no matter what the SQLite state says.
- Is the **only** writer of `fleet.toml`. `fleet.toml` pins are durable across
  daemon restarts (loader re-pins existing projects at every startup), while
  an API `PUT /api/v1/projects/{name}` cooldown change is durable only within
  the daemon session — the next policy run normalizes it back unless the
  project has an ELEVATED_PINS entry.

**Override procedure:** to pin a project's cooldown permanently, add it to
`ELEVATED_PINS` in `~/.hermes/scripts/fleet-cooldown-policy.py` (and set the
pin in `fleet.toml`), then run the script with `--apply`. The pin survives
policy runs and daemon restarts. See `docs/integration.md` for the full
authority model.

---

## Configuration

```bash
./bin/schedulerd \
  -listen 127.0.0.1:9090 \
  -db ~/.hermes/coding-hermes/scheduler.db \
  -foreman-home ~/.hermes/foreman \
  -gateway-url http://127.0.0.1:8642 \
  -min-interval 30s \
  -max-interval 24h \
  -num-levels 10 \
  -budget 100 \
  -max-concurrent 10
Flags
Flag Default Description
-db ~/.hermes/coding-hermes/scheduler.db SQLite database path
-listen 127.0.0.1:9090 HTTP listen address
-min-interval 30s Fastest tick interval
-max-interval 24h Slowest tick interval
-num-levels 10 Number of priority levels
-budget 100 Weight budget
-max-concurrent 10 Max concurrent foremen
-namespace-mode false Enable multi-namespace scheduling
-tick-timeout 2h Maximum tick duration before timeout (2h)
-test-verify 0 Run N-cycle correctness verification and exit
-duckbrain-ns coding-hermes DuckBrain namespace for sync
-duckbrain-url http://localhost:3000 DuckBrain HTTP server URL
-simulate false Run in dry-run/simulation mode (no real spawning)
-sim-success 0.85 Simulated success rate (0.0-1.0)
-sim-count 0 Generate N simulated ticks and exit (0 = run loop)
-gateway-url http://127.0.0.1:8642 Hermes gateway API URL (empty = use exec.Command)
-gateway-key $API_SERVER_KEY Hermes gateway API key
-no-exec-fallback true Disable exec.Command fallback when gateway fails (default true for safety)
-foreman-home ~/.hermes/foreman HERMES_HOME path for foreman sessions
-sim-setup false Create test fixture with 13 dry-run projects (12 enabled + 1 disabled)
-sim-ticks 10 Number of evaluation ticks to run in sim-setup mode
-config (none) Path to TOML fleet config file
-failure-window 100 Number of recent ticks per project for /api/v1/status per-project failure-rate breakdown
-auto-disable-failure-rate 0 Per-project failure-rate threshold (0.0–1.0) for auto-disable; 0 = off
-auto-disable-window 100 Ticks per project over which auto-disable failure rate is computed
-auto-disable-min-ticks 50 Minimum ticks in window before auto-disable can fire
-log-file ~/.hermes/coding-hermes/scheduler.log Path to append structured tick logs (JSON lines); empty disables
-show-config false Print resolved config (CLI + env) as TOML and exit
-schema false Output JSON Schema for schedulerd.toml and exit

Declarative fleet seeding via TOML: ./bin/schedulerd --config fleet.example.toml


Hermes Plugin

Symlink the plugin to register /fleet slash commands:

ln -s $(pwd)/plugin ~/.hermes/plugins/coding-hermes

Commands:

  • /fleet status — Show fleet status
  • /fleet weight <project> <N> — Change weight
  • /fleet priority <project> <N> — Change priority
  • /fleet pause <project> — Pause project
  • /fleet resume <project> — Resume project
  • /fleet ticks <project> — Show tick history
  • /fleet evaluate — Force evaluation

Skills

This scheduler is part of the Coding Hermes ecosystem. See coding-hermes/skills for:

  • coding-hermes-config — First-run setup
  • coding-hermes-foreman — Per-project tick loop
  • coding-hermes-supervisor — Fleet-wide oversight
  • coding-hermes-broker — Scheduling algorithm
  • coding-hermes-worker — Code implementation
  • coding-hermes-north-star — Architecture reference

Development

make build       # Build binaries
make test        # Run tests
make test-full   # Full test suite
make lint        # Go vet
make fmt         # Format code
Project Structure
cmd/
  schedulerd/    # Scheduler daemon entry point
  migrate/       # Cron → scheduler migration tool
internal/
  api/           # REST API server
  dashboard/     # HTML dashboard generator
  database/      # SQLite schema, migrations, CRUD
  mcp/           # MCP JSON-RPC server
  scheduler/     # Core scheduling engine
  sync/          # DuckBrain read-replica sync
plugin/           # Hermes plugin (Python)
specs/            # Implementation specs
deploy/           # Systemd unit
docs/             # Fleet status, architecture docs

Fleet & Skills

See docs/fleet.md for current fleet status — regenerated from the live API (python3 docs/regenerate_fleet.py), with project counts, thread mappings, cooldowns, skills map, provider rules.

Skills are maintained in ~/.hermes/skills/coding-hermes-*/ and loaded by the scheduler per-project.


REST API

Full REST API at http://127.0.0.1:9090/api/v1/.

API wire format: responses are snake_case per specs/S06-rest-api.md (e.g. active_projects, repo_url, cooldown_s, created_at). Request bodies accept snake_case AND the legacy PascalCase Go field names (Name, RepoURL, CooldownS, Enabled, …) so pre-conformance fleet automation keeps working. On create, omitted weight/priority/ cooldown_s/decay_rate default to 10/5/900/1.0; new projects are created disabled — resume them explicitly.

Per-project budgets (SCHED-GAP-066): fleet.toml entries may set daily_budget_usd (UTC-day cap), weekly_budget_usd (UTC-week cap, resets Monday 00:00 UTC), and final_budget_usd (one-time lifetime cap, never resets). All three are opt-in — omitted or 0 means unlimited. Spend is summed from ticks.cost_usd. When any configured cap is reached the project is excluded from selection (zero new spawns, blocked_reason="budget" in GET /api/v1/projects) — running ticks are NEVER killed mid-run. Keys pin on restart when present in fleet.toml (explicit 0 clears); keyless entries leave API-assigned caps untouched.

Endpoint Method Description
/api/v1/health GET Daemon health, uptime, active ticks
/api/v1/status GET Full fleet status (projects, budget, namespaces)
/api/v1/config GET Resolved daemon configuration snapshot (gateway key masked)
/api/v1/projects GET/POST List all or register a new project (GET response carries SCHED-GAP-066 budget telemetry: spent_daily_usd/spent_weekly_usd/spent_total_usd, remaining_*, budget_blocked, blocked_reason)
/api/v1/projects/{name} GET/PUT/DELETE Read, update, soft-delete (?confirm=true) or purge (?confirm=true&purge=true) a project
/api/v1/projects/{name}/pause POST Disable one project (stops it being scheduled)
/api/v1/projects/{name}/resume POST Re-enable a paused project
/api/v1/projects/{name}/spawn POST Manually trigger a tick for one project
/api/v1/ticks GET Tick history with filtering
/api/v1/ticks/{id} GET Single tick detail
/api/v1/events GET/STREAM Event log (SSE streaming supported)
/api/v1/evaluate POST Trigger immediate evaluation cycle
/api/v1/pause POST Pause scheduling
/api/v1/resume POST Resume scheduling
/api/v1/namespaces GET/POST List or create namespaces
/api/v1/namespaces/{id} GET/PUT Read or update a namespace
/api/v1/namespaces/{id}/projects GET List projects assigned to a namespace
/api/v1/namespaces/{id}/move POST Assign a project to a namespace
/api/v1/queue GET All enabled projects by urgency (filter cooldown_s == 0 for the dispatchable subset)

DELETE /api/v1/projects/{name} semantics (DOGFOOD-009): DELETE is a soft delete — it requires ?confirm=true (else 400) and refuses enabled projects with 409 (pause first). On success it returns 200 {"status":"deleted","project":name}: the row is RETAINED (still listed by GET /api/v1/projects and GET /projects/{name}), stamped enabled=false, disabled_by='api-delete', disabled_reason='soft-deleted via DELETE ?confirm=true', disabled_at=<now>. Soft-deleted rows keep their historical ticks referentially valid and remain visible in listings. To permanently remove the row instead, add ?purge=true (i.e. DELETE /api/v1/projects/{name}?confirm=true&purge=true) — purge has its own confirm requirement (?purge=true alone is refused with 400), still refuses enabled projects with 409, and on success returns 200 {"status":"purged","project":name} with the row permanently removed from the projects table. Historical ticks are retained (they reference projects by name string) but no longer contribute to /api/v1/status projects_failure_rates, which only includes existing projects.

MCP Server

MCP JSON-RPC at http://127.0.0.1:9090/mcp. AI agents can control the scheduler via the 14 fleet_* tools listed in MCP Tools:

// Example: List all projects via MCP
{"jsonrpc":"2.0","method":"tools/call","params":{"name":"fleet_projects","arguments":{}}}

Dashboard

Live HTML dashboard at http://127.0.0.1:9090/ — htmx-powered live updates: fleet overview and health panel every 10s, queue and tick history every 30s.

Dashboard

Shows: project fleet overview (enabled/disabled, weight, priority, last tick), recent tick history, namespace allocation with utilization bars, active tick counts, budget gauge.

Directories

Path Synopsis
cmd
migrate command
schedulerd command
Package main is the entry point for the schedulerd binary.
Package main is the entry point for the schedulerd binary.
internal
api
Package api implements the HTTP REST API for the scheduler platform, exposing task management and scheduler control endpoints.
Package api implements the HTTP REST API for the scheduler platform, exposing task management and scheduler control endpoints.
blocks
Package blocks provides deploy "blocks" for the coding-hermes scheduler: reusable GROUPS (a named list of fleet projects) and TEMPLATES (a named list of task definitions) stored as two JSONL files next to the scheduler database, plus the machinery to deploy a template to a group — appending one pending task row per template task to each member project's foreman board (.coding-hermes/board/tasks.jsonl).
Package blocks provides deploy "blocks" for the coding-hermes scheduler: reusable GROUPS (a named list of fleet projects) and TEMPLATES (a named list of task definitions) stored as two JSONL files next to the scheduler database, plus the machinery to deploy a template to a group — appending one pending task row per template task to each member project's foreman board (.coding-hermes/board/tasks.jsonl).
config
Package config provides declarative fleet definition (TOML) support for seeding namespaces and projects at scheduler startup.
Package config provides declarative fleet definition (TOML) support for seeding namespaces and projects at scheduler startup.
dashboard
Package dashboard implements the HTML dashboard for visualizing scheduler state, task queues, and active foremen.
Package dashboard implements the HTML dashboard for visualizing scheduler state, task queues, and active foremen.
database
Package database provides the SQLite-backed operational store for the coding-hermes fleet scheduler.
Package database provides the SQLite-backed operational store for the coding-hermes fleet scheduler.
mcp
Package mcp implements the Model Context Protocol server that exposes scheduler operations to AI agents.
Package mcp implements the Model Context Protocol server that exposes scheduler operations to AI agents.
scheduler
Package scheduler implements the core scheduling engine: urgency calculation, greedy weight-budget packing, foreman spawn management, and the tick lifecycle.
Package scheduler implements the core scheduling engine: urgency calculation, greedy weight-budget packing, foreman spawn management, and the tick lifecycle.
sync
Package sync provides synchronization between external sources (task boards, GitReins, etc.) and the internal scheduler database.
Package sync provides synchronization between external sources (task boards, GitReins, etc.) and the internal scheduler database.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL