chant operator
Synopsis
Section titled “Synopsis”chant operator [flags]chant operator status [flags]chant operator log [flags]chant approve <op> <gate> [flags]Description
Section titled “Description”chant operator runs scheduled ticks for every discovered ConvergeOp locally, on git alone. Coordination is a git-ref lease, and tick state is a ledger fact on the chant/lifecycle orphan branch. See the Operator guide for the full design (pipeline vs converge ops, the lease, crash recovery, gate-as-fact).
chant approve is the sibling command that records a gate’s out-of-band resolution, the durable counterpart to a tick’s gated outcome.
Subcommands
Section titled “Subcommands”operator
Section titled “operator”Tick every discovered ConvergeOp, on --interval, until Ctrl-C.
chant operatorchant operator --env stagingchant operator --interval 30s --lease-ttl 2mchant operator --once| Flag | Default | Meaning |
|---|---|---|
--env <name> | all | Scope discovery to ConvergeOps declaring this environment (labels.Env) |
--interval <duration> | 60s | How often the daemon runs a round. Accepts the same duration syntax as an activity profile timeout (30s, 5m, 1h30m) |
--lease-ttl <duration> | 5m | How long an acquired lease is valid before another operator can reclaim it |
--steward [<name>] | off | Run a declared steward’s scheduled Ops instead of the ConvergeOps. See below |
--once | off | Run a single round and exit, instead of looping. Exits non-zero if any op’s tick failed this round. What a cron job, CI step, systemd timer, or Kubernetes CronJob should pass — the daemon form (--once omitted) is for a machine that stays up |
Each round prints one line per discovered ConvergeOp:
operator: staging-converge@staging ticked=1 ok=trueoperator: prod-converge@prod skipped=1(lease-held:mac.local:4821:9f2c1a3b)operator: nightly-converge@prod skipped=1(not-due:0 3 * * *)A ConvergeOp that declares its own schedule cron is ticked on that cron rather than on every round: the daemon still wakes on --interval, but an op whose cron has not fired since the operator last looked at it is skipped as not-due. --interval is the fallback for an op with no cron of its own, and the resolution at which any cron can be honoured. A round that runs every five minutes cannot see a per-minute cron.
operator --steward [<name>]
Section titled “operator --steward [<name>]”Run a declared steward’s local form instead of every ConvergeOp (#2731). A steward is the one writer for an environment: on Fountain it is the fountain lexicon’s Steward Agent and Teammate, and on a box with no Fountain behind it, it is this command. The declaration is found in the project’s *.op.ts files, from declareSteward in @intentius/chant/op or the fountain Steward’s declaration.
chant operator --steward # the project's only stewardchant operator --steward box-steward --env minimalchant operator --steward box-steward --onceThe command refuses to start when the steward’s form in --env (default local) is fountain, since the Fountain teammate is the steward there, and when another live process holds the steward’s lease, refs/chant/lease/_stewards/<name>. Each round renews that lease and then runs the steward’s scheduled Ops, each on its own cron and under its own operator lease, as the ConvergeOp rounds above do. An Op with no schedule is left for someone to run by hand. Each run is recorded on the run ledger. A round that finds the steward’s lease held by another process ticks nothing and prints operator: steward <name> skipped=1(steward-lease-held:<holder>). --interval, --lease-ttl and --once work as they do without --steward. The steward’s lease and the leases of the Ops it ticked are released on Ctrl-C or SIGTERM.
An Op that declares a work lease runs under it in a steward round too, claimed as <steward>/<op>@<holder> (#2748). An Op that changes the checkout and declares no work lease is refused when the steward is declared, and a scheduled Op whose lease leaves the item to the run is refused the same way, since a scheduled run can’t name one.
A steward’s Op that stops on an open decision point ends its run as waiting, with the question on the run’s ledger record (#2749). The steward does not wait for it and never answers it. A person answers the question through hud or chant workspace points answer. Until then each round prints operator: <op>@<env> waiting=1(point:<id>:<state>) and leaves the Op alone, unless its cron fires, which asks again. The first round after the answer runs the Op again whatever its cron says. Its line names the question as resumed="<id>". This also resumes an Op with no schedule, when its waiting run was the steward’s.
A steward’s run that stops at a gate is resumed the same way (#2779). The gate can be one of the Op’s gate steps, or the gate of a command a step runs, such as chant workspace upgrade’s workspace-upgrade / <scope> (see gatedExit in Ops). The line for a round that finds the gate unapproved is operator: <op>@<env> gated=1(gate:<gate op>/<gate>). Once someone runs chant approve, the next round runs the Op again and its line says approved="<gate op>/<gate>". chant operator status lists a command’s gate that an Op’s run stopped at under “pending gates”, naming the Op.
A local steward is one turn at a time (#2750). Around every run of one of its Ops, a round’s own scheduled tick or a chant run <op> typed by hand, it holds a second lease, refs/chant/lease/_turns/<name>, for exactly as long as that one run takes. A round that finds the turn already held (by a hand run in progress) stops rather than ticking its remaining ops this round, and prints operator: <op>@<env> skipped=1(turn-held:<holder>, steward "<name>" is already mid-turn); the next round tries again. See chant run <op> for what a hand run does with the same lease.
An Op the steward lists under beside runs beside its turns instead (#2861). A round never runs it inline. It starts chant run <op> as a process of its own and goes on with the round, so a build of half an hour holds no turn and converge keeps its cron. The process carries CHANT_STEWARD=<name> and --holder <name>/<op>@<holder>, so its run record names the steward and its work lease is listed under the steward. The run holds the Op’s own lease, refs/chant/lease/<op>, renewed every third of its time to live until it ends. That makes it one run at a time. A round that finds the lease held prints operator: <op>@<env> skipped=1(beside-running:<holder>) and starts nothing, whether the holder is a run the operator started or a hand run. The table lists when a round starts one.
| When | Its line |
|---|---|
the Op’s schedule.cron fires | started=1(beside:cron) |
| the steward’s run of it waits on a question a person has since answered | started=1(beside:resumed) resumed="<id>" |
| its run stopped at a gate someone has since approved | started=1(beside:approved) approved="<gate op>/<gate>" |
its ready step names work the operator has not started a run for | started=1(beside:ready) keys="<keys>" |
The ready step is one activity step, such as shell("node ops/ready.mjs", { json: true }), that the round runs while no run of the Op is in flight. It answers with a list of work keys, one key as a string, true, or no work (false, null, "" or []); for a shell step with json: true the answer is its parsed stdout. The operator keeps the keys it started a run for while it runs, so work that is still ready when the run ends is not started again (skipped=1(not-ready:already-started:<keys>)); true names no key and starts a run whenever none is in flight. A step that fails or answers another shape prints failed=1(ready) error="..." and starts nothing. The round after a run ends prints ended=1(beside) exit=<code>, with chant run’s exit code: 0 for ok, 3 for gated or waiting, 1 for failed. When the operator stops it sends each run it started SIGINT and waits up to ten seconds for it to release its leases. --once waits for the runs its round started before it exits.
chant workspace status --json reports the steward with its lease and the last run of each Op. It also lists the work lease each Op’s turn holds and the questions the steward waits on. The Steward covers the declaration and how the steward shares a checkout with a coding agent.
operator status
Section titled “operator status”Print the last tick, outcome counts, and pending gates for every discovered ConvergeOp, read from the ledger alone, so it works with no daemon running.
chant operator statuschant operator status --env stagingchant operator status --json| Flag | Meaning |
|---|---|
--env <name> | Scope to ConvergeOps declaring this environment |
--json | Emit the same data as a JSON array instead of the human-readable form |
operator log
Section titled “operator log”The history behind status. Every tick record for the discovered ConvergeOps and every gate resolution against them, merged into one timeline ordered by timestamp, oldest first, read from the chant/lifecycle orphan branch. No daemon needs to be running.
chant operator logchant operator log --env staging --op staging-convergechant operator log --since 2026-08-25T00:00:00Z --limit 50 --json| Flag | Meaning |
|---|---|
--env <name> | Scope to ConvergeOps declaring this environment |
--op <name> | Restrict to one ConvergeOp by name. Omitted, every discovered ConvergeOp’s ticks are merged |
--since <iso> | Keep entries at or after this ISO-8601 instant. Inclusive |
--limit <n> | Keep only the newest n entries. They still print oldest-first |
--json | Emit { entries, malformed } as one document on stdout. malformed counts ledger lines that could not be parsed, per ledger, so a shorter timeline is never silent |
Each tick line carries the tick’s own id in brackets, so an outcome can be traced back to the tick that produced it. See the Operator guide for the output format.
approve <op> <gate>
Section titled “approve <op> <gate>”Record a gate’s out-of-band resolution, the durable counterpart to a converge tick’s gated outcome (see the Operator guide’s Gate-as-fact section).
The approval copies the plan digest off the gate’s standing pending fact, and the checkpoint digest too when the gate resumes (resume: true, #3947). The next chant run then starts at the gate rather than the first phase (Resuming at the approved gate). --sign covers the checkpoint digest as well.
An approval of a gate that a newer gate lists as an alias (#3949) is recorded under the old name while the newer gate has no standing pending fact, and the command says which gate the next run counts it toward. Once the newer gate records a pending fact, chant approve refuses and names that gate’s own chant approve line (Renaming a gate).
A release gate, the gate chant components promote --release passes, is refused when its release was cut from a commit that has since been revoked (#4033). The command reads the local ci/revoked/<sha> tags and refuses with release-revoked whatever --plan names, and with release-unreadable when the release file can’t be read. Expire the pending fact with --expire and promote a release cut from a green commit instead. See revoked releases.
chant approve fountain-apply rollout-gatechant approve fountain-apply rollout-gate --actor alex --url "https://github.com/org/repo/pull/123"chant approve fountain-apply rollout-gate --note "paged on-call first; see the incident channel"chant approve fountain-apply rollout-gate --actor github:alice --relayed-by github:hud-follower --sign| Flag | Default | Meaning |
|---|---|---|
--actor <name> | $GITHUB_ACTOR / $GITLAB_USER_LOGIN / $USER | Who resolved the gate, recorded on the resolution fact. A surface that signs people in (hud, studio) passes the person’s forge identity, such as github:alice, or a principal the signers file lists. Under a workspace’s identity.attribution: "identified" any other name is refused (Identity) |
--sign [<key file>] | — | Seal the approval with an ssh key, or with git’s user.signingkey when no file is given. The seal attests --actor when the signers file at base lists the key for them. A gate the workspace’s identity.gates names counts only a sealed approval, from its class when it names one, and without --sign chant approve refuses it (#3163) |
--relayed-by <principal> | — | Who carried the approval to chant for --actor, such as a follower program relaying a person’s approval through hud. Recorded as relayedBy on the resolution line, covered by --sign (the seal signs it as a seventh line, relayed-by <principal>), and listed with the approval in chant workspace status --json. The approver is still --actor, and the seal is still theirs. Under identity.attribution: "identified" the relay must be a checkable principal too (#3402) |
--url <url> | the surrounding CI job’s PR/MR, when there is one | The address this resolution happened at, typically the PR or MR that carried the change. Recorded typed on the resolution fact so a reader is not sniffing free text for a link. Must be an absolute http or https URL |
--note <text> | — | Free-text context recorded alongside the resolution. The link belongs in --url; this is for everything else |
--env <env> | the newest pending fact’s environment | For a component gate: which environment’s pending fact to approve. The resolution records that environment and its plan digest (#2574). Ignored for an Op gate |
--rule <id> | — | On chant approve policy-override <subject>: a rule that denied the plan, once per rule. They must be exactly the rules the subject’s newest denial recorded (Overriding a denied plan, #3760) |
--reason <text> | — | On chant approve policy-override <subject>: why the denied plan goes out anyway, recorded as the override’s note. Required. --note is accepted in its place |
--resume | — | After the approval is pushed, start the CI job that waits at the gate again: a GitHub re-run, a GitLab job retry, or a Forgejo workflow dispatch, with CHANT_FORGE_TOKEN (Resume the waiting job, #3683). Put it after the op and gate |
Recording a resolution does not resume a run that already stopped. Nothing is parked waiting for it. The run read the ledger, found nothing standing, and exited 3 after writing the pending fact. Run the Op again and it reads the resolution you just wrote and walks through the gate, or let the merged PR the note points at carry the change itself. When the run was a CI job, --resume (or a scheduled chant run resume) starts that job again for you.
See also
Section titled “See also”- Operator guide — the full design
chant run— the local executorchant operatorticks through- Converging Lifecycle — defining a
ConvergeOp