Skip to content

Run ops on a fountain steward

llms.txt lists every page for an agent
Optional: hand this page to your coding agentThe steps work by hand too.
Show the whole prompt
Read https://intentius.io/chant/tutorials/fountain-steward/.
In a new directory, run `npx chant init --lexicon fountain --template steward` and `npm install`, then `npx chant build src --lexicon fountain -o dist/fountain.yaml` and `npx chant lint src`.
Show me the six documents the build wrote and the lint result, and stop there.
Do not run `steward-apply`, post anything with `--on fountain`, or create a fountain token.
Never deploy to a real cloud account or shared environment (with `chant run`, `chant components` or a platform's own CLI), approve (a pull request review, `chant approve` or `chant run approve`), override a policy denial (`chant approve policy-override`), or merge; never cut or publish a release (`chant release cut --yes` or `chant release publish`), push or delete a `ci/*` tag (`chant ci tick` pushes them), or change the value of an existing flag; never touch `.chant/allowed_signers` or `chant/lifecycle`.

This tutorial declares a steward: one writer for an environment, running on fountain. A steward is an agent that speaks chant’s own ACP server, with one long-lived conversation that chant run --on fountain opens on the first run and resumes by its channel_id after that. Every op you run against that environment becomes a turn in that conversation, so the conversation is the environment’s operational history, and its sandbox keeps its disk across park and wake.

Steps 1 and 2 run offline. Steps 3 onward need a fountain instance and a token.

The runnable project is examples/fountain-steward.

Terminal window
npm install --save-dev @intentius/chant @intentius/chant-lexicon-fountain

For the live half, a fountain endpoint and a token in FOUNTAIN_TOKEN. Mint one with POST /api/auth/token or from the account UI. A local mix phx.server fountain works the same way.

Terminal window
npx chant init --lexicon fountain --template steward
npm install

Four files come out. src/fountain.ts declares the toolchain Environment, the Vault, and the Steward that binds them. src/prod-watch.op.ts and src/prod-converge.op.ts are the two ops, each carrying its own cadence. chant.config.ts declares fountain.profiles and the repoUrl build parameter.

The steward itself is five lines:

export const { agent } = Steward({
name: "prod-steward",
environment: toolchain,
vault: prodCreds,
ops: [prodWatch, prodConverge],
});

Each op is listed once, with or without a schedule. Listing it is how a run of it knows which conversation it belongs on. Managoat has no scheduler, so a cadence is data on the Op that a clock outside Managoat reads (step 7).

Terminal window
npx chant build src --lexicon fountain -o dist/fountain.yaml

Three documents come out in dependency order: Environment, Vault and Agent. The agent carries the steward’s defaults:

kind: Agent
metadata:
name: prod-steward
spec:
runtime: acp
runtime_command: chant acp --steward prod-steward
environment: prod-toolchain
permission_policy:
default: auto_allow
allowed_vault_ids:
- prod-creds

runtime_command: chant acp --steward prod-steward is what makes a prompt in this conversation a chant command line, and one of this steward’s turns. The agent sets no sandbox_mode, because every Managoat conversation gets a sandbox of its own. The steward is one computer because every run goes to one conversation, whose sandbox keeps its checkout and tool cache. auto_allow is there because nobody is at the keyboard to answer a permission card; approvals belong to chant’s gates, which step 5 covers.

Lint it before going further:

Terminal window
npx chant lint src

FTN023 checks that an acp agent carries its command, and FTN025 that nothing uses a kind or value Managoat removed.

Terminal window
export FOUNTAIN_TOKEN=...
npx chant run steward-apply # or call fountainApply directly

fountainApply sends Environment, Vault and Agent through Managoat’s bulk POST /api/apply in one request. Running it a second time writes nothing.

Terminal window
npx chant run prod-watch --on fountain

The line chant run prod-watch is posted to the steward’s conversation, opened with channel_id chant:steward:prod-steward on the first run and resumed by it after that, and the CLI tails the event stream until the turn settles. What comes back is the same run record a local chant run prod-watch would have written.

A quiet turn loses its connection after 60 seconds, which is fountain closing an idle stream rather than the turn ending; the reader reconnects with Last-Event-ID and picks up what it missed.

If the steward is already running something, the post is refused with the conversation’s address rather than queued. That is the single-writer rule, and it is the reason one conversation is the right place for an op to run: two runs on one checkout would interleave. A cron fire that lands mid-turn gets the same refusal, which is why a steward’s Ops schedule with overlap: "skip".

Terminal window
npx chant run status prod-watch --on fountain
npx chant run log prod-watch --on fountain
npx chant run list --on fountain

status reads the latest turn that ran this op, log every turn, and list one row per declared Op, grouped by steward so twenty ops on one steward cost one round trip.

Open the conversation in Managoat and you see the same history as prose: one command line per turn, with each step of a run rendered as a tool call. The whole plan appears before the first step finishes, because an Op’s declaration already says what the steps are.

The example adds a third op, steward-apply, with an approval gate before the apply.

Terminal window
npx chant run steward-apply --on fountain

The turn builds, reaches the gate, and ends with the approve line in its reply. Nothing is blocked, because the gate is a fact on chant’s ledger branch rather than a wait held open inside the sandbox. No machine is pinned by a decision nobody has made yet.

Record the resolution, then wake the conversation:

Terminal window
npx chant approve steward-apply approve-steward-apply --approver you
npx chant run approve steward-apply approve-steward-apply --on fountain

The second command posts chant run steward-apply back into the steward’s conversation, carrying --approver and --url so the conversation names who resolved the gate, and --on local so the sandbox runs it rather than posting it again. The sandbox re-runs the op, reads the resolution off the ledger, and walks through the gate.

Managoat has no scheduler. prod-watch and prod-converge carry their crons, and something outside Managoat fires them by running the same command you ran in step 4:

  • a CI schedule, such as a GitHub Actions schedule: trigger whose job runs npx chant run prod-watch --on fountain;
  • a Kubernetes CronJob running that command line.

chant operator --steward runs a steward’s Ops on their crons as well, but only in an environment whose form is local. Where the form is fountain it refuses to start, because the steward’s conversation is the one writer there.

Two stores, each authoritative for one thing.

FactStore
A run: its turn, steps, output, and how it endedfountain’s database, as the conversation
A gate’s resolution, and anything that outlives a runchant’s ledger branch in git
The declared shape of the estatethe repo

Nothing is written to both.

Coming from Teammate, Schedule and Webhook

Section titled “Coming from Teammate, Schedule and Webhook”

Earlier releases of the lexicon built the steward on fountain’s team: the Steward composite also returned a Teammate seat, one Schedule per cadence and an optional Webhook, and its Agent ran with sandbox_mode: persistent. Managoat removed the team and its schedules, webhooks and persistent sandboxes, and their routes answer 404 or 422. managoat/managoat#2793 tracks bringing chant along.

Teammate, Schedule and Webhook are still exported, deprecated, so a project that declares one builds far enough for FTN025 to fail it with the replacement. The serializer leaves them out of the manifest, and chant import skips them in an old manifest.

BeforeNow
A Teammate seat, and its fountain:team threadOne conversation per steward, opened and resumed by channel_id chant:steward:<name>. chant run --on fountain does it for you
A Schedule per Op cadenceThe cron stays on the Op. A CI schedule or a CronJob runs chant run <op> --on fountain on it, or chant operator --steward where the form is local
A WebhookNothing. Read a run’s outcome from chant run status or chant run log, or from the conversation’s event stream
sandbox_mode: "persistent" on an AgentDrop it. The steward’s conversation keeps its sandbox’s disk across park and wake. FTN025 reports the old value
sandbox_provider: "daytona" or "runner""sprites" or "e2b", or leave it unset
fountain.profiles.<name>.teamfountain.profiles.<name>.steward. team still works, and warns
const { agent, teammate, schedules, webhook } = Steward(...)const { agent, declaration } = Steward(...)
FTN020, FTN021, FTN022Removed with the kinds they checked

chant