Jazari
Operating procedures you can call, instead of documents you hope someone reads.
Recipes as data, per-subject runbooks, stable names for rituals that outlive any record, and per-run evidence — so "did last night's run actually complete?" is a query rather than a guess.
Requires Ruby 3.2+, Rails 7.1+, and PostgreSQL.
Guides
The README is the pitch; the public Kuickr guide
is the operating manual. Its source lives in guide/, including
concepts (start here: one word means something
different than you expect), adoption,
anchors, runs and evidence,
MCP, and migrating an existing checklist.
The problem
You have a procedure. Verify a backup by restoring it. Provision a server. Triage an alert before waking anyone.
It is written down. It lives in a document, or a wiki, or a comment. And so:
- Nothing can check it off, so nobody knows how far a run got.
- Nothing knows whether it is current, so it rots silently.
- Nothing records who ran it, when, or what they saw.
- An agent cannot call it, because it has no name — only a location.
The usual fix is a checklist attached to a record. That helps, and then it
runs out: some procedures belong to no record at all, and a checklist you
reset to run again has just destroyed the evidence it ever ran.
Four layers
RECIPE the canon — how this ritual is done. Data, not code.
↓ operator-editable at runtime, digest-versioned
RUNBOOK one subject's override — how THIS record differs
↓ materialised on first edit; reading a default writes nothing
QUEUE a stable name for a ritual that outlives any record
↓ read-only: a ritual has exactly one editable home
RUN one execution — who, when, which ticks, what evidence
Most systems stop at the second. The third makes a procedure callable; the fourth makes it auditable.
Install
gem "jazari"
bin/rails generate jazari:install
bin/rails db:migrate
The generator copies one migration. Jazari never auto-appends migrations to
your schema — a shared operations table appearing in someone's next
db:migrate without them asking is how a gem loses trust in a production fleet.
Thirty seconds
# 1. Seed a recipe. The gem ships NO content — these are your procedures.
Jazari::RecipeRegistry.seed!([
{ id: "backup.verify.v1",
topic: "Prove a backup by restoring it",
description: "## Purpose\n\nA green schedule is not a verified backup.",
run_policy: "once_per_calendar_day",
checklist: [
{ id: "dump", text: "Dump to scratch" },
{ id: "restore", text: "Restore into a throwaway database" },
{ id: "counts", text: "Compare table and row counts" }
] }
])
# 2. Address the ritual by NAME. No record required.
target = Jazari::QueueTarget.new(
queue: "backup-verify", public_reference: { kind: "queue" },
recipe_id: "backup.verify.v1"
)
Jazari.resolve(target: target).progress # => { done: 0, total: 3, percent: 0 }
# 3. Open a run, work it, attach what you saw.
result = Jazari.open_run(target: target, actor_ref: "agent:nightly")
run = result[:run]
Jazari.tick(run: run, expected_revision: run.lock_version,
item_id: "restore", done: true, actor_ref: "agent:nightly")
Jazari.attach_evidence(run: run.reload, expected_revision: run.lock_version,
item_id: "counts", kind: "count", value: "4211 rows",
actor_ref: "agent:nightly")
Jazari.close_run(run: run.reload, expected_revision: run.lock_version,
outcome: "completed")
# 4. The question that started all this:
Jazari.last_run(target: target).outcome # => "completed"
Idempotency belongs to the ritual
Verifying a backup should happen once a day. Triaging an incident may happen five times. So there is no global rule — each recipe declares its own:
run_policy |
Behaviour |
|---|---|
unrestricted (default) |
every open_run starts a run |
once_per_calendar_day |
one run per recipe + subject + UTC day |
Under the daily policy a second call returns the existing run rather than erroring, so a retrying cron converges:
Jazari.open_run(target: target, actor_ref: "cron")
# => { run: #<Run id: 4412>, created: false, idempotent_reuse: true }
Enforced by a partial unique index, not by application logic — a
find-then-insert races. Two details that are easy to get wrong and are handled
here: the index COALESCEs the nullable polymorphic subject (otherwise queue
runs are unconstrained entirely, because NULL != NULL), and the day is UTC
via timestamptz, so one nightly ritual cannot land on two different days
depending on which region's machine called it.
Revision guards
Every mutation carries the revision from the read before it:
resolved = Jazari.resolve(target: target)
Jazari.check_item(target: target, expected_revision: resolved.revision,
item_id: "dump", done: true)
A customised runbook uses its lock_version; a default uses
default:<recipe-digest>. Editing a recipe changes its digest, so anyone
holding a stale default gets revision_conflict instead of silently writing
onto ground that moved. This matters most when several automated writers share
one procedure — last-writer-wins is the same defect class as two people
force-pushing a branch.
Recipes are data, not code
The gem ships no recipe content — not one checklist item, only the mechanism and an empty fallback. Your procedures are rows: seeded once, then operator-owned. Reseeding never overwrites an edit.
That means fixing a ritual is a write, not a deploy — and a fresh install can ship with working procedures instead of an empty text box.
For version-controlled recipes, load YAML or JSON as a seed and report drift without overwriting operator edits:
entries = Jazari::RecipeFiles.load("config/recipes")
Jazari::RecipeRegistry.seed!(entries)
Jazari::RecipeFiles.drift(entries)
Runs are bound to the canon they opened against
A run snapshots its checklist when it opens. Edit the recipe mid-run and the in-flight run still ticks its own steps, and refuses steps that did not exist when it started. Without this, an operator improving a procedure silently breaks every run in progress.
Actor identity is part of the evidence
Jazari stores the opaque identity attached to the run, every tick, and every evidence entry. Pass a stable reference when a human, agent, or job acts:
Jazari.open_run(target: target, actor_ref: "user:42")
Jazari.tick(run: run, expected_revision: run.lock_version,
item_id: "restore", done: true, actor_ref: "user:42")
Jazari.attach_evidence(run: run.reload, expected_revision: run.lock_version,
item_id: "restore", kind: "note", value: "verified",
actor_ref: "user:42")
For trusted system jobs, configure a zero-argument default. Explicit references always win. When a tick or evidence entry omits its actor, it inherits the run's actor; opening a run without an explicit actor requires this configured default.
Jazari.configure { |c| c.actor_ref = -> { "system:nightly-backup" } }
Jazari.open_run(target: target)
MCP
Jazari::Mcp::Handler maps action names onto the domain and knows nothing about
transport, auth, or product naming:
Jazari::Mcp::Handler.new.call(action: "get", target: target)
# => { ok: true, state: "default", topic: "...", progress: {...}, last_run: {...} }
Tool identity stays yours. Your app exposes its own flat action-enum tool
with its own subject vocabulary and permissions; this handler is the shared
implementation underneath. Domain failures cross the wire as codes from a closed
set — target_not_found, invalid_runbook, revision_conflict,
item_not_found, read_only_target, run_closed — never as messages that
could disclose a record or whether a target exists.
Handler.actions_for("read") returns the read-only subset, so a read-scoped
connection never advertises mutations.
You authorize; Jazari never sees an actor object
The domain accepts no raw IDs, arbitrary records, or actor objects. Your app
authorizes first, then constructs exactly one immutable target. It passes only an
opaque actor_ref string for audit history:
Jazari::RecordTarget.new(runbookable: site, public_reference: { kind: "site" },
recipe_id: "site.maintenance.v1")
Jazari::QueueTarget.new(queue: "backup-verify", ...) # read-only
Jazari::AnchorTarget.new(scope_type: "Tree", scope_id: 7, key: "node-x", ...)
AnchorTarget covers subjects that are not ActiveRecord rows — a JSON-tree
node, a file path, a DNS zone. Register the scope at boot; unregistered scopes
fail closed.
Deleting a subject
Jazari cannot hook your models — a subject may live in a different logical
database, so no cross-database foreign key is claimed and no cascade exists.
Call in from your own after_commit:
class Site < ApplicationRecord
after_commit :forget_jazari, on: :destroy
def forget_jazari = Jazari.forget_subject(self)
end
That removes the subject's runbook. Runs are deliberately preserved — a run records something that actually happened, and deleting the subject does not un-happen it.
PostgreSQL only
The guarantees lean on Postgres: jsonb, timestamptz, four CHECK constraints,
and a partial unique index over a COALESCEd polymorphic subject. Supporting a
second adapter would make those conditional, which weakens the design. Other
adapters are additive — open an issue if you need one.
The test suite runs the migration the gem ships, so the schema cannot drift out of coverage.
What this is not
Not an execution framework. Jazari holds the state of a procedure — the canon, the overrides, the runs, the evidence. It does not SSH anywhere, shell out, or run your commands.
That boundary is deliberate. Execution is already well served by whatever you have — CI, a rake task, Ansible, a deploy tool — and those differ per shop. What none of them keep is a durable, checkable, addressable record of which procedure was run, by whom, and what came back. Jazari keeps that, and stays out of the way of however you actually run things.
(The name runbook was already taken on RubyGems, by a DSL for executing
operational procedures. That gem's last release was 2021 and its last commit
2022, so it is not a dependency worth taking — but the name is still occupied,
which is one reason this gem is called jazari.)
License
MIT.