Skip to content

The Steward

llms.txt lists every page for an agent

A steward is the declared machine an environment is operated from. It is one Agent running chant acp, and one long-lived conversation of that agent. chant run <op> --on fountain opens the conversation the first time and resumes it after that by channel_id, chant:steward:<name>. The conversation’s sandbox keeps its disk across park and wake and is destroyed only when the conversation is terminated, so the checkout and tool cache survive a turn ending.

That conversation is the environment’s operational history. Each turn is one chant command line, so scrolling it is scrolling what was done to the environment, redacted and searchable, with no separate log to keep.

chant needs seven things from a place to run ops. fountain already has a noun for each:

chant needfountain noun
a place to runEnvironment (repo, chant, kubectl) + Vault (creds, or the broker) + the conversation’s sandbox
a recordthe conversation: turns, events, SSE, search, audit
a cadencenone upstream: a CI schedule: or a CronJob running chant run <op> --on fountain
waiting on a humana gate fact on chant’s ledger, resolved by chant approve or a merged PR
waiting on the worldthe next run’s observation
telling someonenone upstream: read the run’s outcome from chant run or the conversation’s event stream
one writer per environmentthe one conversation

The steward composite wires those nouns together in one declaration, so an author writes === the environment once instead of six resources that have to agree with each other.

import { ConvergeOp, WatchOp, gt, report, when } from "@intentius/chant/op";
import { Environment, Repository, Steward, Vault } from "@intentius/chant-lexicon-fountain";
export const toolchain = new Environment({
name: "prod-toolchain",
repositories: [new Repository({ url: "https://github.com/acme/estate", mount_path: "/workspace/estate", ref: "main" })],
setup_script: "npm ci && npm install -g @intentius/chant",
networking_type: "limited",
networking_config: { allowed_hosts: ["github.com", "registry.npmjs.org"] },
metadata: { "managed-by": "chant" },
});
export const prodCreds = new Vault({ name: "prod-creds", metadata: { "managed-by": "chant" } });
export const { op: prodWatch } = WatchOp({ name: "prod-watch", env: "prod", schedule: "*/15 * * * *" });
export const { op: prodConverge } = ConvergeOp({
name: "prod-converge",
env: "prod",
schedule: "0 * * * *",
rules: [
when(gt("updateCount", 0), report("declared and live state disagree"), {
id: "prod-drift",
why: "An update pending against prod means something changed outside this repo.",
}),
],
});
export const { agent, declaration } = Steward({
name: "prod-steward",
environment: toolchain,
vault: prodCreds,
ops: [prodWatch, prodConverge],
});

ops takes the Op declarations the composites hand back, or plain OpConfig objects for an Op assembled by hand. The steward reads the name off each, so chant run <op> --on fountain knows which conversation the run belongs on. The schedule stays on the declaration; Managoat has no scheduler, so fire it from outside:

.github/workflows/prod-watch.yml
on:
schedule: [{ cron: "*/15 * * * *" }]
jobs:
watch:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npx chant run prod-watch --on fountain
env: { FOUNTAIN_TOKEN: "${{ secrets.FOUNTAIN_TOKEN }}" }
DefaultEffect
runtime: acp, runtime_command: "chant acp --steward <name>"the sandbox launches chant’s own ACP server, so a prompt is a chant command line, and every turn is this steward’s (see Decision points)
no sandbox_modeevery conversation gets a sandbox of its own; the steward’s one conversation keeps its disk across park and wake
permission_policy: { default: "auto_allow" }nobody is at the keyboard to answer a permission card; chant’s gates are where a human belongs
no modelan acp agent’s model is whatever the command on the other end of the protocol chooses
no skillsa steward’s competence is the project’s op definitions
allowed_vault_ids scoped to the given vault, [] with noneno conversation may attach a vault the steward was not given
managed-by: chant on the Agentowned-only reconcile, prune and drift filtering see it

Two stores, each authoritative for one kind of thing, and nothing stored twice.

FactStoreRead it with
A run: its turn, its steps, its stdout, when it started and how it endedManagoat’s database, as the conversationchant run status|log|list --on fountain
A gate’s resolution, and anything that has to survive across runschant’s ledger branch in gitchant approve, chant run approve <op> <gate>
The declared shape of the estatethe repochant build, chant lifecycle diff --live

A gate is a fact on the ledger. A run that reaches an unapproved gate ends its turn saying so; the resolution is recorded on chant’s ledger by a person or a merged PR, and the next run re-evaluates it. No sandbox stays open waiting on a decision, so the steward is one computer rather than one per pending decision.

A conversation runs one turn at a time. That is Managoat’s rule, and the steward is built around it: every run of every op goes to the same conversation, so two runs cannot interleave on one checkout.

The composite refuses these at construction, so the failure arrives while the author is looking at the declaration:

  • Two stewards on the same environment and vault. That is two writers on one environment; Managoat would accept both and their turns would interleave.
  • An op whose schedule.overlap is anything but skip (core’s declareSteward refuses it, in both forms).
  • The old webhook option, from an untyped caller. Managoat has no webhooks.
  • An op listed twice, and a form that is local in every environment.

A box on a preset with no Fountain behind it, such as a docker compose box or a Sprites box, still has one steward. It is the same declaration in its local form: chant operator --steward in the box runs the same Ops on the same crons, one at a time.

The declaration says which form runs where. form is one form, or a default with per-environment exceptions:

export const { agent, declaration } = Steward({
name: "box-steward",
environment: boxToolchain,
ops: [converge, dispatch, release, upgrade],
form: { default: "fountain", environments: { minimal: "local", spritzer: "local" } },
});

Core reads the steward from the *.op.ts files it already reads Ops from, so export declaration from one:

ops/steward.op.ts
export { declaration as steward } from "../src/steward";

A box that never runs on Fountain declares its steward without this lexicon, with declareSteward from @intentius/chant/op. It takes the same name, ops and form, with local as the default form.

chant operator --steward box-steward --env minimal then:

StepWhat it does
picks the stewardby name, or the project’s only one when the flag has no name
checks the formrefuses an environment where the form is fountain, because this Agent’s conversation is the steward there and a local operator would be a second writer
takes the steward’s leaserefs/chant/lease/_stewards/box-steward, renewed every round and after every run; a second operator for the same steward is refused while the lease is live
runs the scheduled Opseach on its own cron, each under its own operator lease, exactly as chant operator ticks a ConvergeOp; an Op with no schedule is left for someone to ask for
records each runon the run ledger on chant/lifecycle, the record chant run <op> writes

declareSteward also takes beside: Ops the local form runs beside its turns, each in a chant run <op> process of its own under the Op’s own lease, so a long build holds none of the other Ops up (#2861). An entry is an Op, or { op, ready } with a step that says when there is work:

export const steward = declareSteward({
name: "box-steward",
ops: [converge, awake, release, upgrade],
beside: [{ op: dispatch, ready: shell("node ops/ready.mjs", { json: true }) }],
});

The operator starts such an Op on its cron, when its ready step names new work, or to resume the steward’s run of it once its question is answered or its gate approved, and keeps ticking the other Ops meanwhile. chant run <op> for it takes the Op’s lease and not the steward’s turn. operator --steward has the details. This composite takes no beside, so on Fountain every Op is a turn.

--once runs one round and exits. --interval and --lease-ttl work as they do for chant operator. On Ctrl-C or SIGTERM the steward releases its lease; one that dies without releasing it is reclaimed when the lease expires.

The declaration refuses the same things in both forms: an Op listed twice, a schedule whose overlap isn’t skip, a cron that doesn’t parse. Discovery refuses one Op listed by two stewards, and two stewards with one name.

A box’s steward holds no credential (#2726). The box’s member in the workspace declaration lists the capabilities the box reaches through a broker, such as fountain and inference, and the broker holds the keys. The steward names the capabilities its Ops use and takes no vault:

export const { agent, declaration } = Steward({
name: "box-steward",
environment: boxToolchain,
ops: [converge, dispatch, release, upgrade],
capabilities: ["fountain", "inference"],
});

vault together with capabilities is refused at construction. vault alone still works for a steward that isn’t behind a broker. chant workspace check fails a box member whose files hold a literal secret (box-credential-declared), which covers a steward’s vault declared there. chant workspace status --json lists each capability the steward names with the broker the box block gives it, and declared: false for one the block doesn’t list.

A box also has a coding agent editing the app, in the same checkout as the steward. They share it by writing different things:

WriterWritesNever writes
the stewardrun, converge and gate records on the chant/lifecycle branch, and lease refs under refs/chant/lease/the app’s files, the index, commits on the working branch
the coding agentthe app’s files, the index, commits on the working branchchant’s ledgers and leases, except through a chant command

Chant enforces the steward’s half. Every ledger and lease write goes through git plumbing (hash-object, mktree, commit-tree, update-ref), which reads and writes neither the index nor the working tree. A steward run succeeds while the coding agent holds git’s index lock, and leaves the agent’s uncommitted edits as they were. A core test edits app/ and takes the index lock in the middle of a steward run to check this.

An Op a steward runs doesn’t edit the app. When an Op has to change the checkout, as applying a build or chant workspace upgrade does, it declares changesCheckout: true and a workLease, and chant enforces the rest (#2748). declareSteward, and so this composite, refuses such an Op without a work lease, and the executor refuses to run it. The run claims the work item’s lease before its leased steps, which then work in a git worktree of their own on chant/work/<item>, at .chant/work/<item> in the checkout, which git status leaves out. The coding agent’s working tree, index and uncommitted edits stay as they were, and a core test checks that with the agent’s edit and its index lock in place while a steward applies a build. The branch keeps what the steps committed, for the runner to deliver. Running under a work lease covers the declaration, and the dispatcher’s lease semantics: a second dispatcher is refused, a lost lease stops the run, and the release records the outcome.

An Op the steward runs may ask a decision point, such as the factory’s builder tier (#2749). The activity asks it with askPointInRun from @intentius/chant/op. The steward runs unattended. On Fountain its conversation is its own, and in the local form nobody is at the terminal. So a question it reaches follows the rules below, in both forms.

RuleHow
The model call goes through the brokerThe caller supplies the call as a BrokeredModelAsk, which is handed the capability to reach the model through. In a steward’s turn it is made only through a capability the steward names in capabilities, inference by default. A steward that names none, or holds a vault, makes no model call: the model decider is recorded as not reached, and the question goes to people.
An open question ends the turnAn escalated or proposed question stops the run with status waiting, as a gate stops it with gated. The run ledger names the question as point, and the run names the steward as steward. No later step or onFailure phase runs.
The question stays out of the checkoutThe answer record is written on the chant/lifecycle branch, under _answers/<kind name>/<id>.md, not in the checkout’s working tree, which is the coding agent’s (#2786). It outlives the turn and any leased worktree. points reads it as part of the answer kind and gives its place on the branch as ledger.
People answer it, not the steward’s conversationThe question is a record in the workspace. chant workspace points --open and MCP’s workspace-points list it with askedBy, the steward and the run that asked it. workspace status --json lists it in the steward’s waiting, beside its Ops. hud reads those and asks a person, who answers through hud or chant workspace points answer.
The steward never answerspoints answer is refused with answer-in-steward-turn during the steward’s turn and in any process the turn starts, and the quorum never counts the steward that asked. A steward can’t clear a gate either.
The next run reads the answerThe next run asks the same point with the same inputs, finds the answered record and goes on. The local form runs a waiting Op again on the first round after its question is answered, whatever its cron says, and prints resumed="<id>". An Op beside the turns is started again the same way, in a process of its own. Until then each round prints waiting=1(point:<id>:<state>). On Fountain the Op’s next run reads the answer.

The Agent’s command is chant acp --steward <name>, which marks every turn in the session as the steward’s. In the local form, chant operator --steward marks each run it starts. Both export CHANT_STEWARD to the processes a run starts.

chant workspace status <env> --json lists each member’s stewards: the name, the file that declares it, its form in <env> and its declared forms, the local lease (holder, expiry, and whether it is live), each Op with its schedule, its last run from the run ledger and the work lease its turn holds, and waiting, the open decision points the steward waits on. See workspace status.

Environment, Vault and Agent, in that order. fountainApply sends them through Managoat’s bulk POST /api/apply, which reports each resource as created or updated and nothing else, so an Environment that changed in no way still comes back updated. Nothing is applied for the conversation: chant run <op> --on fountain opens it on the first run.

See Composites for the constructor’s full option list, Runtime for running an op on the steward, and ACP for what the sandbox is actually speaking.

chant