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+. Embed it in Rails 7.1+ with PostgreSQL, or run the on-demand standalone CLI with SQLite.
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.
Standalone SQLite, JSON, and MCP stdio covers the
on-demand executable.
The public jazari/v1 JSON contract and its conformance fixtures
are shipped with the gem. Development plans and agent coordination stay out of
this public repository.
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
ruby
gem "jazari"
bash
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.
For a local standalone database:
bash
jazari recipes seed config/recipes --database .jazari/jazari.sqlite3
jazari capabilities --database .jazari/jazari.sqlite3 --json
jazari call --database .jazari/jazari.sqlite3 --request request.json
jazari mcp stdio --database .jazari/jazari.sqlite3 --recipes config/recipes
The process starts for the command or MCP stdio session and exits with its caller. It opens no port, installs no daemon, and stores no credentials. Queue and anchor targets are supported; an embedding host still owns authorization and record-backed targets.
Thirty seconds
```ruby # 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:
ruby
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:
ruby
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:
ruby
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:
ruby
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.
ruby
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:
ruby
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, item_not_in_snapshot, 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:
ruby
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:
ruby
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 and SQLite
Rails hosts keep PostgreSQL’s jsonb, timestamptz, CHECK constraints, and
partial expression index. Standalone mode uses SQLite JSON columns, UTC date
checks, foreign keys, WAL, a busy timeout, and the same partial expression
index. Adapter syntax differs; observable revision, idempotency, snapshot,
attribution, and evidence behavior does not.
The test suite runs the migration the gem ships against both adapters and replays the same JSON fixtures against PostgreSQL and SQLite.
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.