# behold > A live control plane on chant (https://intentius.io/chant/). behold draws an estate as one graph coloured by what the live estate holds. It reads the estates of the other INTENTIUS tools as well as chant's. Every view is a read. Each write starts an Op the project committed or a forge workflow, on the credentials behold was started with. An agent driving behold starts with https://intentius.io/behold/using/agents/. Every pane is plain JSON over HTTP, and `GET /api` lists the routes. An MCP client adds `npx @intentius/behold mcp `, a stdio server with four tools: `capabilities`, read first, returns chant's capability catalog verbatim (what each `cap:` term means, its parameters and its rule); `ask` returns chant's search document for a query, verbatim, with the link to the view that draws it; `show` moves the person's open tab to a view, its matches lit; and `shot` returns a PNG of one with its link. No tool deploys, approves, runs an Op, rolls back or writes. A person new to it starts with https://intentius.io/behold/start/run-it/. A read-only live demo of behold on real terragucci pipelines runs at https://demo.intentius.io/kiosk. Each tool has its own page there, such as https://demo.intentius.io/terraform. Each page under /for/ is a starting point for one kind of estate or one role. A page with a prompt carries it under "Optional: hand this page to your coding agent", and llms.txt lists it under the page. Every prompt ends with the same line, which keeps each write with the person. --- # Driving it from an agent Source: https://intentius.io/behold/using/agents/ ## Optional: hand this page to your coding agent ```text Clone https://github.com/INTENTIUS/behold and get me looking at a live infrastructure graph in my browser. I want the bundled demo. It needs Docker and no cloud account. Install what it needs and start it, then tell me the URL. Walk me through what I am looking at: what the colours mean, and how I make a resource change colour while I watch. When Docker is not running, say so. Show me the source graph on its own instead. For anything past the demo, read https://intentius.io/behold/llms.txt and https://intentius.io/behold/using/agents/. Never deploy with the Deploy button, `POST /api/apply` or `POST /api/ci/dispatch`. Gate approvals stay mine as well (`/api/ops/:name/signal/:gate`, `/api/workspace/gates/approve`, `/api/operator/approve/:op/:gate`). Op runs (`/api/ops/:name/run`) and rollbacks (`/api/rollback`) are mine too. Leave `--auto-sync` off, and keep a `behold preview` or `--public` serve read-only. ``` The SPA has no private channel: everything it shows rides plain JSON over HTTP, so an agent or a script can drive the same server headless. Start one and curl it: ```sh npx @intentius/behold serve # or preview / demo curl -s http://localhost:4600/api | jq ``` `GET /api` is the front door. It lists the routes with a one-line description each, the server's version, and a link to the full contract. ## The read loop 1. **observe.** `GET /api/graph` returns `{ ir, svg, meta }`. Every node in `ir.nodes` carries `id`, `kind`, `lexicon`, `attrs`, and `sourceLoc`. Drift status, when present, is `attrs._status`: `good` = managed, `warn` = foreign, `accent` = pending, `neutral` = unobserved, `runtime` = runtime child. 2. **focus.** Narrow with query params: `?detail=0..3`, `?components=1`, `?logical=1`, `?lens=blast:&down=1`, `?lens=lexicon:aws`, plus the `env` / `stack` / `tier` / `target` axes the UI's Scope tab exposes. 3. **go deeper.** With an `?env=`: `/api/overlay` is the live entity overlay, `/api/diff` slices per-node observed state / drift / field ownership, `/api/reconcile` summarizes the pending change set. `/api/substrates` reports substrate readiness, and `/api/events` (SSE) pushes `changed` / `op` / `apply` / `pr` so you re-pull only when something moved. 4. **inspect.** A node's `sourceLoc.file` is the typed source that declared it. To change the estate, edit there: chant is the source of truth, not behold. Provenance rides on the node too: `attrs._release` (the recorded deploy: run id, origin, commit, digest, actor, approver), `attrs._carve` (the Terraform address a carved entity came from), and on a Helm chart `attrs._renderDrift.identity` (whether the running release was deployed from this project's render). A directory that is not a chant project gets a structured `{code: "no-project", error, remedy}` from `/api/graph`. Every other precondition failure (`lint`, `not-installed`, `tier`, `eval`) takes the same shape, so an agent branches on `code` instead of parsing prose. ## Over MCP: `behold mcp` `behold mcp` is a stdio MCP server for a coding agent that wants to show the person what it found. chant answers and behold draws: `capabilities` returns chant's capability catalog, `ask` returns chant's answer to an estate question, and `show` and `shot` draw it. Read `capabilities` first. It is chant's capability catalog (`chant search --capabilities --json`, chant#4375), and it says which `cap:` questions this estate can be asked: each capability's name, what it means, its parameters, the search term that asks it (`cap:internet-reachable(22)`, `->[cap=network]`), the questions it answers, what it does not evaluate, its rule, and whether each lexicon supplies it. Then `ask` with that term. ```sh claude mcp add behold -- npx @intentius/behold mcp ``` | Tool | What it does | Arguments | |---|---|---| | `capabilities` | Returns chant's capability catalog, the read contract's `capabilities` document, verbatim (`chant search --capabilities --json` on one project or a Terraform folder, `chant workspace search --capabilities --json` on a declared workspace), through `GET /api/capabilities`. Needs chant 0.124.46 or newer. | none | | `ask` | Returns chant's search document for the query, verbatim (`chant workspace search` on a declared workspace, `chant search --json` on one project or a Terraform folder), with `link` to the view that draws it and `drawn: false` added. Never moves the tab. | `query`, `env`, `snapshot`, `plan`, `countBy`, `show` | | `show` | Moves the person's open behold tab to a view through `POST /api/view`. The tab says an agent moved it and keeps the view it left one Back away. With no tab open it answers the link to hand the person. | `query`, `env`, `snapshot`, `plan`, `countBy`, `member`, `zoom`, `focus`, `node` | | `shot` | Returns a PNG of a view as MCP image content, then a text block with the link, through `GET /api/shot`. | the same, plus `width` and `height` | `query` is chant's search grammar, unchanged. `env` reads that environment live (`chant search --live --env `), `snapshot` with `env` replays a recorded snapshot (`latest`, or a ref chant lists), and `--explain` is always on, for the near-misses behold draws. No tool takes `--check-live`, `--check-snapshot`, `--ambient`, `--src` or `--fail-on-drift`, and `at` is refused: in chant's read contract it is a git revision. The tab and the PNG light the query's matches, draw the edges on their paths and dim the rest; a `cap:` match's path is the hops in its path's `because`, one entry per capability it relied on, and those are drawn too; with no matches the whole graph is dimmed and the near-misses outlined. Without a query, `shot` frames the view's own `node` or `focus`. `countBy` (`kind`, `module` or `attr:`) is chant's `--count-by`: `ask` returns chant's groups and total, and the tab and the PNG light each group's cards and label the group with its count; the query may then be empty, to count the whole estate. `plan: true` answers at the plan depth (chant reads a saved `terraform plan` of each root), so `attr:planAction=replace` with `show: "planActions,planReplacePaths,planActionReason"` says what a change replaces and why, and the cards carry the plan facts. A bare Terraform folder needs no `env` for it; a chant project does. `plan` with `snapshot` is refused, naming both. The server is a client of a running serve's HTTP API and holds no query or view logic of its own. It finds the serve through `--url` or `--port` when given, then through a per-user registry under the OS temp directory (`/behold/serves/.json`, holding `{url, pid, projectDir, version, startedAt}`), which every `behold serve` writes at start and removes at exit, then at `http://127.0.0.1:4600`. Nothing is written in the project. Before a tool uses a serve it reads `GET /api/project`. If that serve serves another project, `show` refuses and names both. With no serve running, `show` refuses with the command that starts one, and `capabilities`, `ask` and `shot` start behold in their own process for the call. Every argument is listed and any other is refused, a credential-named one by name. A refusal comes back with `isError` and the route's `{error, code, remedy}`. The server will not start with a tool whose name says it would deploy, apply, approve, run an Op, roll back or write, and a call to such a tool is refused. `show` changes what the tab draws and nothing else; `shot` writes no file. ## The act loop, delegated and never direct behold performs no cloud write of its own. The mutating endpoints trigger Ops the project committed: `POST /api/apply?env=&component=`, `POST /api/ops/:name/run`, `POST /api/ops/:name/signal/:gate`, `POST /api/rollback?to=`. All are guarded (one write at a time), gated where the Op declares a gate, and locked out entirely in preview and static modes. An environment its member's `chant.config.ts` designates to a forge (an `executor` on the environment entry, read through chant) answers those with `409 executor-forge`; its one deploy is `POST /api/ci/dispatch?env=`, through the operator's `gh`, followed on the dial, with `GET /api/ci/run` the persisted record and `POST /api/ci/readopt` the re-follow after a restart. For build, lint, lifecycle diff and op status, prefer chant's own MCP tools over shelling. ## The contract The canonical version of this guide ships inside the npm package and lives at [AGENTS.md](https://github.com/INTENTIUS/behold/blob/main/AGENTS.md). Hand it to your agent verbatim. --- # behold Source: https://intentius.io/behold/ ## Where it sits ``` chant source ──build/lint──▶ graph IR ──behold──▶ live graph + drift + (delegated) actions (deterministic) (server + browser) ``` behold reads what [chant](https://github.com/INTENTIUS/chant) compiled and what the cloud reports, and shows you the difference. As a viewer over a compiler, it can say plainly which parts it could not read. ## What the colours say Green means managed and amber means foreign. Blue is declared but not yet observed. The colours come from reading the live estate, and behold hosts no state file. A resource behold could not read shows as unobserved, which is a separate claim from absent. [Reading the graph](/behold/using/reading-the-graph/) has every colour and zoom tier. ## Where writes run Sync starts the `ApplyOp` you committed, and Adopt starts your `ReconcileOp`, which opens a pull request a person merges. Deploy runs chant with the credentials behold was started with. A terragucci or SQL Yodeler repo leaves every write to its pipeline. [What behold is](/behold/start/what-it-is/#read-only-core-delegated-writes) covers the rest. ## Where to read next --- # What behold is Source: https://intentius.io/behold/start/what-it-is/ behold is a **live control plane** over a [chant](https://github.com/INTENTIUS/chant) estate. chant compiles typed TypeScript into spec-native infrastructure output; behold reads what chant declared and what the cloud reports, and shows you the difference. ``` chant source ──build/lint──▶ graph IR ──behold──▶ live graph + drift + (delegated) actions (deterministic) (server + browser) ``` ## The mixed estate is the point Where Argo CD shows one cluster's tree, behold shows the estate that spans substrates: an EKS cluster declared in CloudFormation and the Kubernetes Service running on it. Each substrate has its own kind of truth, and behold keeps them distinct rather than flattening: | substrate | what "deployed" means there | |---|---| | AWS | CloudFormation lists the resource, and the last stack operation succeeded | | Kubernetes | the object exists, its owned Pods are running, and chant's field manager still owns the fields it wrote | | Azure / GCP | the resource answered a read; there is no deploy object to ask | That last row is why component status reads from a resource rollup rather than a stack: on a substrate with no deployment object, the stack cannot be the source of truth, because there isn't one. ## Read-only core, delegated writes **behold writes nothing to a cloud or to your source itself.** Every write it offers is a command it starts for you, run by a tool you already trust. - Every view is a read: `chant graph`, snapshots, live describes. Viewing needs only read roles. - **Sync** starts the `ApplyOp` your project committed. **Adopt** starts your `ReconcileOp`, which opens a PR a human merges. - **Deploy** on a project with no committed `ApplyOp` runs `chant run --components` on chant's local executor, as a child of behold. It applies with the credentials in behold's environment, so Deploy from behold needs write roles there. - An environment `.behold.json` designates to a forge is never applied from your machine; Deploy dispatches the forge's workflow through your `gh`. - A terragucci repo is applied by its pipeline only: behold refuses every deploy, Op run, approval and dispatch there, and offers none of them. - A SQL Yodeler repo (one with a `yodel.config.ts`) is guarded the same way. Its refusal says `code: "sql-yodeler"` and names `npx yodel approve --plan ` (with `--sign` under a sealed gate) and `yodel apply ` in the project's pipeline. Authority stays in your committed source and your executor. If a request would have behold write to a cloud or to source directly, that is a bug. An agent driving behold can look at everything. What it can change is what the commands above can change with the credentials behold was started with, so start it with read-only credentials when nothing should be applied from it. ## What it is not - Not a deployer: behold starts your Ops or chant's local executor, and they do the work. - Not a state store. Truth lives in the live system, and ownership is read from a marker on the resource. - Not a new abstraction over your cloud. The graph is your declarations and your resources, at their real names. --- # Run it in five minutes Source: https://intentius.io/behold/start/run-it/ To watch behold before you install anything, open [the live demo](https://demo.intentius.io/kiosk). It follows real terragucci pipelines in a loop and needs no install. Both demos below need **Docker** and nothing else. No cloud account, no credentials, no cost. Each ends with a resource changing colour while you watch, which is the thing worth seeing. If you would rather not follow steps, hand [the agent prompt on the front page](/behold/) to a coding agent and let it do this. ## The AWS demo: your first apply One S3 bucket, deployed to a local [Floci](https://github.com/lex00/floci) emulator through the CloudFormation API. 1. Start it: ```sh git clone https://github.com/INTENTIUS/behold && cd behold npm install npm run demo ``` That installs the bundled `example-writes` project's dependencies, boots its emulator, and serves it at **http://localhost:4600**. No checkout? `npx @intentius/behold demo` runs the same demo from npm, copying the example project to `./behold-demo` (yours to edit). 2. Look at the graph. The bucket and its TLS policy are **blue**: declared in source, not yet deployed. 3. Click **▶ Deploy (floci-apply)** on the floating control panel's **Deploy** tab. The now-line streams Build → Apply → Verify as the bucket is created in the emulator. 4. The nodes flip **green**. behold observed the live emulator and found them. That is the whole loop: declared → applied → observed. Nothing was mocked, and behold did not do the applying. It triggered an Op the project committed. ## The Kubernetes demo, and three things only k8s can show ```sh npm run demo:k8s ``` Brings up a single-node [k3d](https://k3d.io) cluster, serves an nginx Deployment + Service, and tears the cluster back down on Ctrl-C. Same blue → **▶ Deploy** → green loop, applied server-side with chant's own field manager. Then three things that exist on no other substrate: **Runtime children.** Zoom into the Deployment and its **Pods** appear, owned by the cluster through `ownerReferences` and never declared. "Deployment exists" tells an operator almost nothing; a crashlooping Pod tells them everything. **Field-level ownership.** Run `kubectl scale` or `kubectl label` out of band, refresh, and see which fields chant's field manager owns versus the one that just wrote. Foreign-owned reads differently from drifted, because a field the HPA legitimately owns is not a problem. **An honest refusal.** Switch away from the bound kubectl context and behold reports **unobserved**, which means "I did not look" rather than "all gone". behold still serves the source graph and tells you what to start. It never dies on you, and it never paints a resource as absent because it could not read it. ## Then what - Point it at [your own project](/behold/start/your-project/). - Learn [what the colours and zoom tiers mean](/behold/using/reading-the-graph/). - [Freeze a snapshot](/behold/using/export/) into a static site you can share. --- # Open your own project Source: https://intentius.io/behold/start/your-project/ behold opens any [chant](https://github.com/INTENTIUS/chant) project. There is nothing to add to your project to get the source graph: no config file, no annotations, no plugin. ## The source graph, with nothing set up ```sh npx behold preview # the current directory npx behold preview ../my-project # somewhere else ``` That serves your project's declared graph at **http://localhost:4600**. It shells your project's own `chant` binary, so what you see is what your project builds. `preview` with no path opens the current directory, so running it from inside your project is the shortest thing that works. ## If it doesn't look right: `behold doctor` ```sh npx behold doctor # the current directory npx behold doctor ../my-project --json ``` A read-only diagnosis of everything behold needs to serve the project well. One line each for the project's kind, its own `chant` install and version, declared lexicons, the environments the picker will infer, the kube context chant binds versus your ambient one, substrate readiness, and committed Ops. Each is pass, warn or fail with a one-line fix; the exit code is non-zero only when something failed, so CI can gate on it. It starts no server and changes nothing. The distinction it draws is the useful one: a **fail** is something that stops behold serving the project (no `chant` in the project's `node_modules` is the common first-touch one, since behold shells your project's chant). A **warn** costs you the live half rather than the graph: no environment declared, no cluster binding, an emulator that isn't up. ## Adding the live overlay To colour the graph by what is actually deployed, name an environment: ```sh npx behold serve . --env prod ``` That needs whatever credentials reading your estate needs; behold uses your ambient environment and holds nothing of its own. Nodes become green (managed), amber (foreign), or blue (declared, not yet observed). If a read fails, the affected nodes report **unobserved** rather than being painted absent. A resource behold could not look at is never the same claim as a resource that is not there. ## Watching for drift ```sh npx behold serve . --env prod --poll 30 ``` Re-queries every 30 seconds and pushes updates to the browser. Add `--auto-sync` to trigger a committed Op when drift appears: `apply` heals toward source, `pull-request` opens a PR adopting live into source. It is off by default and never approves a gate. ## Optional: a tier picker If your project's source branches on a deployment tier, declare it as a [build-time parameter](https://intentius.io/chant/concepts/build-time-parameters/) with an `enum` in `chant.config.ts`, and behold renders a picker for it: ```typescript export default { buildParams: { tier: { type: "string", enum: ["minimal", "prod", "prod-ha"], default: "minimal", env: "KMV_TIER" }, }, } satisfies ChantConfig; ``` Your source reads it as `params.tier` from `@intentius/chant/params`. When a tier is picked, behold passes it to chant as `--param tier=`, and also sets the parameter's `env` variable when it declares one. behold takes the parameter named `tier`, or else the first one with a string `enum`. A project with no such parameter gets no picker. Before #470 the tier axis was a `tiers` block in `.behold.json`. behold no longer reads it, and `behold doctor` warns where one is left. ## Optional: a traffic level for live versus declared behold can show what your estate costs as the account stands beside what the file says it should, with the difference. Both figures are predictions at a traffic level, and behold never picks one for you. Name it in behold's per-user settings for the project, a file outside the project at `/behold/settings/.json` (`~/Library/Caches/behold/settings/` on macOS, `~/.cache/behold/settings/` on Linux, `%LOCALAPPDATA%\behold\settings\` on Windows; `BEHOLD_CACHE_DIR` moves it), where `` is the same as the hand layout's: ```json { "behaviour": { "traffic": "100 rps, p50" } } ``` A project served before #470 that named it in `.behold.json` has that block copied into the settings on the first read. behold never writes or deletes `.behold.json`. The string goes to chant verbatim as `chant graph --traffic`. `?traffic=` on `/api/graph` and `/api/overlay` overrides it for one request. With neither, behold asks for no prediction, which reads as "not looked" and is not a refusal. With a level, `/api/overlay` asks chant twice for each member: once about the account (`--live`) and once about the file. The payload's `meta.behaviour` carries `live`, `declared` and `delta`, per member and for the estate, and a card the account no longer holds keeps its declared figure under `attrs._behaviourDeclared`. The live figure moves with drift. The declared one moves only when a file under the member changes, so behold reads it once per edit. The figures are an engine's, reached by chant through `CHANT_BEHAVIOUR_ENGINE`. behold calls no engine and holds no key. A choudoufu or Terraform member reads through `@intentius/chant-lexicon-terraform`, which behold installs with itself (#909), since that lexicon is what predicts a `.tf` root. ## Your hand layout Drag a card or resize a containment box and behold remembers the offset, in your browser and in a per-user cache outside the project (`~/Library/Caches/behold/layouts/` on macOS, `~/.cache/behold/layouts/` on Linux, `%LOCALAPPDATA%\behold\layouts\` on Windows; `BEHOLD_CACHE_DIR` moves it). behold writes nothing inside your project, so there is nothing to gitignore. The layout stores offsets rather than positions, so the graph underneath stays chant's, and `behold export` bakes it into the exported SVGs. A `.behold/layout.json` left by an older behold is read once into the cache and never changed or deleted. Your project's chant needs to resolve build parameters in `chant graph`, which landed in chant 0.38.0. A caret on a `0.x` pins the minor, so `^0.37.x` cannot reach it. Bump to `^0.38.0`. ## What behold needs from your project Nothing, for the source graph. For the rest: - **Live overlay.** An environment your lexicons can read, and credentials for it. - **Action buttons.** Ops you committed. **Sync** appears if the project has an `ApplyOp`, **Adopt** per foreign node if it has a `ReconcileOp`, **Run** for anything else. A project with no Ops shows no buttons, by design. --- # Reading the graph Source: https://intentius.io/behold/using/reading-the-graph/ ## The colours | colour | means | reads as | |---|---|---| | **green** | managed | chant declared it and the estate confirms it | | **amber** | foreign | it is there, chant did not put it there | | **blue** | pending | declared, not yet observed as deployed | | **grey** | neutral | nothing deployed for this component yet | A node behold could not read is **unobserved**, reported as a hole and never painted as absent. "I did not look" and "it is gone" are different claims, and conflating them is how a viewer talks someone into deleting something. ## Health and rotation Drift says whether the account matches the source. Health says whether a declared thing is serving, and behold draws it apart from the colours above. With an environment picked and chant 0.124.33 or newer, behold reads `chant lifecycle health ` for each chant project (each member of a workspace) and marks every card that has a health read: a Route 53 health check, an ECS service, a Container App and a Cloud Run service. | mark under the card | means | |---|---| | green **healthy** | the provider says it is serving | | red **unhealthy**, with a solid red ring | the provider says it is not | | grey **health unknown** | the read worked and the answer decides neither way, such as a service scaled to zero or a rollout in progress | | **health not read: \**, with a dashed grey ring | chant could not read it (`read-failed`, `no-credentials`, `no-binding`, `unsupported-kind`) | | grey **not deployed** | the provider says it is not there | A health check chant could not read is a hole, the same as an unobserved card, and is never drawn as unhealthy. The card's fill stays the drift colour, so a health check can be managed and unhealthy at once. Rotation is derived from the graph. A Route 53 record whose `HealthCheckId` names a declared health check is out of rotation while that check is unhealthy: the record and its edges are dimmed and its line reads `out of rotation: azureCheck unhealthy`. A healthy check puts the record in rotation. When the check is unknown, not read or not deployed, the record's line says its rotation is unknown and nothing is dimmed, since behold does not guess. Routing beyond that, such as failover pairs or alias targets, is not modelled. The Model tab counts each state under the drift legend and lists the records out of rotation. Clicking a card shows the provider's detail line, when it was checked and when chant read it, and for a health check, the records it guards. The health is read once per environment and kept for 30 seconds; "Re-check live" reads it again. Agents read the same answer from `GET /api/health?env=`. ## The zoom dial From widest to narrowest: 1. **Components.** The deploy units and the waves they go out in. 2. **Logical.** An architecture diagram in the shape the substrate actually has. AWS nests `region → VPC → subnet`; Azure nests under the resource group; GCP is `project → location`, with no network containment, because the emulated GCP surface has no VPC to draw. 3. **Composites.** The higher-level constructs your source declares. 4. **Resources.** Individual resources at their real provider names. 5. **Attributes.** The properties on one resource. On Kubernetes there is one tier further down, below anything you declared: **Runtime.** The Pods, ReplicaSets and EndpointSlices the cluster created and maintains through `ownerReferences`. They are runtime children rather than drift: nothing declared them and nothing should. This tier is why a healthy-looking Deployment in front of a dead Service is visible rather than green. The cluster maintains an owner chain and per-field ownership records that no other substrate has. behold uses them where they exist and does not pretend they exist elsewhere. An AWS stack at `CREATE_COMPLETE` genuinely is the end of what is knowable there. ## Field ownership, on Kubernetes The API server records, per field, which manager last wrote it. chant compiled the manifest, so it knows which fields it authored. Put together, the inspect pane can say *these fields are chant's, `spec.replicas` is the HPA's, and someone edited the image by hand*. Foreign-owned is shown as a distinct state from drifted. A field the HPA legitimately owns is not a problem, and colouring it like drift would recreate the noise that per-field ownership exists to remove. ## Cross-substrate edges An estate that spans providers keeps its edges: a managed cluster declared in your cloud lexicon and the workload running on it render connected, in the live view and not only in the source graph. ## Pipeline boxes A project that declares CI (the github, forgejo or gitlab lexicon, chant 0.124.20 or later) draws each workflow or pipeline as a box holding the workflow and its jobs. The box takes the workflow's `name:` when it has one (`CI Pipeline`), and the entity's key otherwise. A reusable-workflow call runs between two boxes. Pipeline boxes appear on a project's resources view, alone or in an estate. In an estate (`behold serve a b`, or a declared workspace) each member's box holds its pipeline boxes, one level down, titled the same way without the member name the box around them already shows (`release`, not `api/release`). A pipeline goes in the member box that holds most of its cards. When the member boxes are packed into rows, or a wide member's cards are laid out again, each pipeline box is drawn again around where its cards ended up, so it stays inside its member box and clear of the others. The composites level has no pipeline boxes, because chant leaves pipeline membership off at detail 1 ([chant#3921](https://github.com/INTENTIUS/chant/issues/3921)). When a graph carries deploy waves as well, the wave boxes are drawn instead. ## CI edges A GitHub Actions, Forgejo or GitLab project draws its edges by the kind chant gives them (chant 0.124.20 or newer). A `needs` edge is a heavier solid line, `calls` and `triggers` have their own colours, and `includes` and `extends` are dotted. An edge that leaves its own workflow or pipeline, such as a call to a reusable workflow or a `workflow_run` trigger, is dashed. The chip on the edge says the kind, with the attribute when that adds something (`needs (stage)`, `calls (uses)`), as chant's Mermaid output does, and hovering the edge says what the kind means. The Model tab lists the kinds the graph holds, with a count for each. Two nodes can be joined by more than one kind, such as a GitLab job that both `needs` and `extends` another. Each kind is its own edge, drawn as its own line a little apart from the other, with its own chip, tooltip and highlight. ## What was deployed, from where Three cards carry provenance beside their status, so "is this what we meant to deploy" has an answer on the card rather than in a CI log. - **A component** that the release ledger recorded shows ` via `, the commit and the id space of the run that deployed it. The inspect pane's **release** section carries the run id, commit, digest, actor and approver. The run is a link only when the record itself carries an address (chant ≥ 0.54); a bare id from an older chant is stated as unlinked rather than guessed at. - **A Helm chart** whose observed release records its render identity (chant ≥ 0.54) gets a **deployed from** row in the render-diff section: the release was deployed from this project's render byte for byte, from the same inputs, or from something else. A mismatch repaints the card the drift colour, since a release running someone else's render is bad news whatever its status. A release no ledger record names reads as unrecorded rather than as a mismatch. - **A chant entity carved out of Terraform** carries `carved:
` and a `carved → chant` mark on the entity graph and the overlay: the carve manifest names the file it emitted, `chant graph` names the file that declared the entity, and the join is a byte comparison. The inspect pane's **carved from** section gives the Terraform address, stage and estate. ## Ops that touch the estate On the ops lens, a step whose contract declares which of its arguments identify an estate entity (`httpCheck` and its `url`, chant ≥ 0.54) links to the card it touches: the card is drawn beside the track in an `estate` box with a dashed edge from the step, and the pane lists the value on both ends. The join is exact: a node id, or a declared attribute value equal to the step's. A value nothing carries stays on the step as unresolved rather than guessed. --- # Acting on it Source: https://intentius.io/behold/using/acting/ Every action below starts something **you** committed, running on **your** executor. behold holds no credentials and performs no cloud write. ## The actions, and where they come from | action | where | starts | direction | |---|---|---|---| | **▶ Deploy (\)** | the control panel's Deploy tab, and ⌘K as "Deploy: Sync" | your `ApplyOp` | code → cloud | | **▶ Deploy…** | Deploy tab (no `ApplyOp`, but an env) | `chant run --components`, via the dial's picker | code → cloud | | **Approve \** | Deploy tab beside Deploy, and ⌘K | the gate signal on your `ApplyOp` | — | | **Adopt** | per foreign node, in the inspect panel | your `ReconcileOp` | cloud → code, as a PR | | **Run: \** | ⌘K palette | any other committed Op | whatever you wrote | | **Rollback…** | ⌘K palette | a rollback PR to a picked commit | code → code | The floating control panel's **Deploy** tab carries a single **▶ Deploy** button whether the project routes it through a committed `ApplyOp` or a raw component apply; the tooltip says which. Every action is also in the **⌘K palette**. A project with no Ops and no environment shows no deploy affordance, by design. ## Gates A destructive apply pauses for approval. behold surfaces the gate and the **Approve** signal; the workflow is durable, so the pause survives a restart of anything, including behold. **Auto-sync never approves a gate.** That is the invariant that makes an automatic loop acceptable at all. [Approving a gate](/behold/using/approving/) says who chant records when you click Approve, and how behold signs a gate that needs a signed approval. ## Deploying through a forge An environment can be designated to deploy through GitHub Actions rather than from the machine behold runs on. In the project's `.behold.json`: ```json { "executor": { "prod": { "forge": "github", "workflow": "deploy-prod.yml" } } } ``` The designation names the committed workflow file rather than only the forge, because two environments' generated pipelines carry identical job ids and nothing short of the file tells prod's from staging's. For a designated environment: - **Deploy** dispatches that workflow through your own `gh` login (`gh workflow run`, on the current branch) and follows the run on the dial, job by job, to GitHub's own verdict. The run's page is linked from the dial. - A local apply, a committed ApplyOp for that environment, and auto-sync are refused with `executor-forge`, and rollback is withheld. Nothing about that environment ever runs here. - **Approval lives on the forge.** If the workflow's GitHub environment has a protection rule, the approval is granted there, by your GitHub identity. behold holds none that could, on purpose, so the only affordance it offers is the link. - A designation behold cannot honour (a forge it has no trigger for, a workflow that is missing or lacks `workflow_dispatch`) disables Deploy for that environment and says why. It never falls back to a guess, and never to your laptop. Without a designation, the palette's dispatch picks the committed workflow named for the environment (`chant-components-`, as chant 0.54 names a generated pipeline). On older projects it matches by job ids, refuses a tie rather than letting directory order choose, and never picks a workflow named for another environment. A dispatched run's id is persisted under `~/.behold/ci-runs/`, the operator's own state. Restart behold mid-deploy and it re-adopts the run at boot; `POST /api/ci/readopt` does the same on request. A follow whose polls keep failing, or that outlives its deadline, is reported as **lost** rather than as a verdict. The run itself may still be live at its page, which the lost line names. ## Auto-sync Off by default. With `--poll` and `--auto-sync`, a detected drift triggers a committed Op: ```sh npx behold serve . --env prod --poll 30 --auto-sync apply ``` - `apply` heals the cloud toward source, via your `ApplyOp`. - `pull-request` adopts live into source, via your `ReconcileOp`, as a PR someone merges. It routes **per substrate**: drift in the Kubernetes half triggers the Op that owns Kubernetes rather than whichever Op sorts first. Where several Ops match a substrate, or none does, it declines and says so on the now-line. A self-heal loop that picks the wrong half of an estate to rewrite is worse than one that stops. While a rollback PR is open, `pull-request` mode suspends. Otherwise the loop would re-adopt exactly what the rollback removed, and the two would fight. All delegated writes share one guard. If something is already running, a second trigger is refused and says which. It is not queued: the substrate waits until it drifts again. ## Rollback Rollback opens a PR restoring source to a prior revision. Merging it moves source; the estate follows on the next apply. behold never force-reverts a cloud. --- # Export a snapshot Source: https://intentius.io/behold/using/export/ `behold export` captures whatever you are looking at into a **static folder** that any host can serve. Read-only, and fully interactive. ```sh npx behold export --out ./behold-export npx serve ./behold-export ``` Pan and zoom, the zoom dial, the inspect pane and the env/tier pickers all work client-side. There is no backend and no live observe: it is a snapshot, and it says so. ## What it captures Every read endpoint for the whole lens matrix (each env/tier × zoom × radial), rendered in-process by **the same handlers the live server runs**. A snapshot is byte-identical to what live would have shown at that moment. ## With a live overlay ```sh npx behold export ../my-project --env prod --out ./behold-export ``` Freezes the drift colours as they were when you exported. Useful for an incident write-up, or for showing someone an estate they have no credentials for. Each captured environment also carries its [health and rotation](/behold/using/reading-the-graph/#health-and-rotation), read once with `chant lifecycle health` when the export runs: the health marks on the cards and the Route 53 records out of rotation are drawn as they were then. The read uses the export's own credentials and endpoints (`AWS_ENDPOINT_URL` and the like, or the environment's declared endpoint), the same as the drift overlay. A chant older than 0.124.33 captures the refusal, and the bundle draws no health marks. ## The view it opens on A bundle opens on the source graph (`env: (source)`) at the zoom behold would pick for the estate. `--open-env` and `--open-zoom` choose something else: ```sh npx behold export ../my-workspace --open-env frozen --open-zoom composites --out ./behold-export ``` `--open-env` takes one of the environments the export captures, which are the ones the estate declares. `--open-zoom` takes `components`, `logical`, `composites`, `resources` or `attributes`, and `ops` when the estate has emitted Ops. The export refuses any other value before it captures anything, and says which values it would take. The choice is written to the bundle's `manifest.json` as `opening`. A link with its own `?env=` or `?zoom=` still opens where the link points. ## A line at the foot of the page `--note` puts one line of text under the graph, such as how to run the estate yourself. A span in backticks is drawn as code, and nothing else is markup: ```sh npx behold export . --note 'Run this yourself: `npx @intentius/behold demo three-cloud`' --out ./behold-export ``` The note is at most 200 characters on one line, and the export refuses anything else before it captures. It is written to `manifest.json` as `note`. [The three-cloud demo](/behold/using/three-cloud/) ends its page this way. ## A declared workspace Exporting a directory that holds a `chant.workspace.json` names the bundle after the workspace, not after its first member. The generated `wrangler.jsonc` names the Worker `behold-` (`behold-three-cloud` for a workspace declared as `three-cloud`), and `manifest.json` carries `workspace` with the declared name and `projectDir` with the workspace root's directory name. `--name` still overrides the Worker name. A single project is named after its directory, as before. ## What it leaves out A bundle carries the picture and nothing about the machine that made it: no list of your recent projects, no user name, and every absolute path reduced to its directory's name. A Terraform card carries its root's whole source file. `--no-source` leaves that text out of every snapshot: ```sh npx behold export . --no-source --out ./behold-export ``` ## A terragucci estate `--terragucci ` captures the marks [`serve --terragucci`](/behold/using/terragucci/) shows, with the same dates, and the audit record's timeline as one more snapshot (a refusal or an absent record is captured as the lane shows it, and does not fail the export). `--publish s3:////views/` also uploads the bundle there, signed with AWS credentials from the environment; it writes only under a `views/` directory, one folder per commit (`views///`, the checkout's `HEAD` or `--commit `), with `history.json`, `latest.json` and an `index.html` that opens the newest beside them; `--keep ` (default 30) is how many commits stay. [Publish a view](/behold/using/terragucci/#publish-a-view-beside-the-estate-page) says how terragucci's estate job puts it beside `estate.html`. ## Hosting it The bundle is deploy-ready for Cloudflare, since `export` writes an assets-only `wrangler.jsonc`: ```sh cd ./behold-export && npx wrangler deploy ``` GitHub Pages, S3 and nginx work too. The live app needs Docker and a chant subprocess and cannot run on an edge worker; the pre-baked export can. --- # Serve a chant workspace Source: https://intentius.io/behold/using/workspace/ ## Optional: hand this page to your coding agent ```text Read https://intentius.io/behold/using/workspace/. Serve this directory with `npx @intentius/behold serve .` and tell me the URL. Name each member that draws as one card, and the reason code on it. If it does not serve, show me what `npx @intentius/behold doctor .` reports. Never deploy with the Deploy button, `POST /api/apply` or `POST /api/ci/dispatch`. Gate approvals stay mine as well (`/api/ops/:name/signal/:gate`, `/api/workspace/gates/approve`, `/api/operator/approve/:op/:gate`). Op runs (`/api/ops/:name/run`) and rollbacks (`/api/rollback`) are mine too. Leave `--auto-sync` off, and keep a `behold preview` or `--public` serve read-only. ``` Since chant 0.81.0, a directory can hold many members and list them in a `chant.workspace.json` at its root. behold serves such a workspace from that list: ```sh npx @intentius/behold serve ./estate # the directory holding chant.workspace.json ``` behold reads the declaration through chant rather than parsing it itself. Every reader goes through the same contract, which keeps behold and `chant workspace` in agreement about the member list. ## What behold asks chant | Question | Command | |---|---| | Which members are there, and can each be read? | `chant workspace ls --json` | | What does a member hold? | `chant workspace graph --member `, with `--live`, `--overlay` and `--traffic` for the live picture | Both documents carry a contract version, and behold reads version 1. A document written to any other version is refused with what to upgrade, because a new contract version can change what a field means. The chant that answers is the one the workspace root resolves: its own install, or behold's when the root has none, which is how a Terraform-only estate with no chant of its own stays viewable. ## What is drawn | Member | Drawn as | |---|---| | `chant` and `terraform` | A box of the member's resources, read through `chant workspace graph` | | `choudoufu` | A box of the estate's instances. The edges between them and each block's place in the source come from `chant workspace graph`; the instances, their rungs and the references to other estates come from `choudoufu live-check`, since chant's graph has one node per block and no rung or reference | | A member chant cannot read | A box with one card that gives chant's reason code and message, such as `dir-missing` or `unknown-kind` | | `other`, or a nested `workspace` | Nothing. It is listed when behold starts and in `behold doctor` | Node ids are `/`, with the member's declared name. That is the id chant composes too. The hand layout is kept per user, outside the project, and keyed by the workspace root. Several directories on the command line (`behold serve a b c`) are still the loose view behold composes itself, with no declaration involved. ## Policy denials A run whose plan the policy denies records a gate of op `policy-override` on `chant/lifecycle`. `chant workspace status` lists it with the other gates, but an override needs the rules that denied the plan and a person's reason, so behold draws it as an override card with no button. The card names the subject (a root, or SQL Yodeler's migrations Op), the denied rules and the plan digest, read from `_gates/policy-override.jsonl`, and the command to run at your shell: | Repo | Command on the card | |---|---| | terragucci (a `terragucci.yml` in the member or above it) | `npx terragucci override --rule --reason "..."` | | SQL Yodeler (a `yodel.config.ts`) | `npx yodel override --rule --reason "..."` | | anything else | `chant approve policy-override --rule --reason "..." --plan ` | An override that counts reads as overridden until a run applies it. Once a run records it in `_gates/policy-override/applied.jsonl` the card reads as spent, with the time it was applied. ## Converting a `.behold.json` member list The `members` key in `.behold.json` is deprecated. When a `chant.workspace.json` sits beside it, behold ignores the key and prints a warning at startup. An estate root that has only the `.behold.json` list converts with one command: ```sh npx @intentius/behold doctor ./estate --fix ``` This writes `chant.workspace.json` in the root and never over an existing one. It then runs `chant workspace check` on what it wrote. - Each member keeps the name behold already composed it under, so a chant member's node ids don't change. - A Terraform member holding several roots becomes one member per root, because a chant `terraform` member is a single root. - Any `terraform` or `choudoufu` member makes the declaration pin `@intentius/chant-lexicon-terraform`, which supplies both kinds. - The saved layout of the first member is copied to the root's entry in the per-user layout cache, and its node ids are rewritten to the declared names. One file in the estate root, `chant.workspace.json`, and the root's layout in the per-user cache outside it. The first member's layout and `.behold.json` stay as they were. Every other doctor run writes nothing. --- # Look at a Terraform estate Source: https://intentius.io/behold/using/terraform/ ## Optional: hand this page to your coding agent ```text Read https://intentius.io/behold/using/terraform/. Serve this repository's Terraform with `npx @intentius/behold serve ` and tell me the URL. List the roots it found and each directory it skipped, and why. Never deploy with the Deploy button, `POST /api/apply` or `POST /api/ci/dispatch`. Gate approvals stay mine as well (`/api/ops/:name/signal/:gate`, `/api/workspace/gates/approve`, `/api/operator/approve/:op/:gate`). Op runs (`/api/ops/:name/run`) and rollbacks (`/api/rollback`) are mine too. Leave `--auto-sync` off, and keep a `behold preview` or `--public` serve read-only. ``` In a folder of `.tf` files, one command draws it: ```sh npx @intentius/behold serve . ``` Nothing else is installed or run first. behold draws the folder the way it draws a chant project: roots as boxes, resources as cards, and the references the configuration actually states as edges. No cloud account, no Docker, no `terraform` binary. To see the bundled estate instead of your own: ```sh npx @intentius/behold demo terraform-estate ``` ## behold parses no HCL This is the posture rather than an implementation detail. `@intentius/chant-lexicon-terraform` reads the HCL an estate already has and emits one entity per block, so **a Terraform estate is a chant project whose only lexicon is a reader**, and `chant graph` serves it like any other. behold ships no HCL parser and never has. The lexicon and `@cdktn/hcl2json`, the ~1.8 MB wasm HCL parser chant loads when it reads a root, are dependencies of behold, so `npx` installs them with it. They add about 8 MB to behold's install (#909). behold still checks for both before it serves a Terraform folder. chant itself warns, exits 0 and prints an empty graph when the parser is missing, so an install that skipped or pruned them (`npm install --omit`, a trimmed container image) is refused instead, with where behold looked and one install line: ```sh npm install @intentius/chant-lexicon-terraform@^0.124.52 @cdktn/hcl2json@^0.24.0 ``` The lexicon's minor has to be behold's chant's (0.124): each lexicon release asks for its own chant minor as a peer, so an older one puts a second chant in the tree and the lexicon where behold's reader does not look. chant resolves a lexicon from its config file's location, so behold generates the reader config in a scratch directory of its own and points chant at that. The estate is read exactly as it sits, and behold writes nothing inside it: a card you move is remembered in a per-user cache outside the estate. ## Roots are discovered, and the skips are reported A root is a directory with a `.tf` declaring a line-start `terraform {` or `provider "` block beside a `resource`, `data` or `module` block. A regex probe at depth, never a parse. Two exclusions stand beside it, and both are named in the graph's note with their reason: - a directory under a `modules/` segment is a called module, and the roots that call it draw its blocks already; - a directory with nothing to draw, such as a lone backend fragment, is reported rather than drawn as an empty box. The probe alone is not enough, and the bundled estate is built to show why: the shared module's `versions.tf` is byte-for-byte a root's. Nothing in the file says which is which. Water park's real estate is what refuted the first version of this rule. ## What is a card Measured on a real estate: 247 nodes for 43 resources, four fifths of it not infrastructure. | zoom | blocks drawn | why | |---|---|---| | default (detail 0-2) | `resource`, `data`, `module` | the estate: what is declared, what it reads, what it composes | | attributes (detail 3) | + `output`, `variable` | its interface: real, and a second question | | never | `terraform`, `provider`, `locals` | settings, not estate | Nothing is dropped silently. The status line says what is not drawn and where to see it. ## The edges behold refuses to invent Edges arrive from chant or not at all. A `module` call expands into its own contents, with the reference edges the lexicon resolves from the HCL. Those are real: one block names another. A **cross-root read** is different, and behold draws nothing for it. In the bundled estate `platform` declares a KMS key and `envs/prod` reads it back: ```hcl data "aws_kms_key" "estate" { key_id = "alias/${var.name}-estate" } ``` Both ends carry the same unresolved `${var.name}` interpolation. Matching them would be a coincidence of variable naming rather than a relationship either root states, so the data source card says what it reads and no edge is drawn. That was measured on a real estate before it was refused. ## There is no live half `behold serve ` reads source. There is no `--env`, no drift colour, and no overlay: behold strips `live`, `overlay` and `env` for a Terraform member, because a reader lexicon would be asking chant to observe entities it never applied. What the cloud actually holds is a different question with two other answers here: - [a choudoufu estate](/behold/using/choudoufu/) reads ownership from markers on the live resources, and a green card there means *this is ours*; - a committed Op observes whatever its own lexicon observes. Painting a Terraform estate green would be claiming an observation behold never made. ## The plan, when you ask for it One read is about the cloud and still belongs here, because chant makes it from the root's own state and providers: the plan. Press ⌘K and pick **Read the plan on the Terraform roots** (or `GET /api/graph?plan=1`), and behold asks its own chant to read each stock root at the plan depth chant's terraform lexicon adds in 0.124.46 (chant#4358): ```sh CHANT_TERRAFORM_OBSERVE=plan chant graph . --live --overlay --env behold-plan --format ir --detail 3 ``` That runs `terraform plan -input=false -lock=false -out=/plan.tfplan` and `terraform show -json` over the file, in each root, with the plan file under the OS temp directory and removed afterwards. `-lock=false` means the read never takes the backend's state lock. Nothing is written in the root, and `init` is never run: a root that has not been initialised is refused with the line to run. Each card then leads with what the plan says about it: | row | what it says | | --- | --- | | `plan` | what the next apply does: `replace`, `update`, `delete`, or `no changes` | | `forced by` | the attribute that forces a replace, with its value before and after | | `from` | where that value comes from in the configuration, such as `var.generation` | | `read` | which read answered: `plan`, or the fallback below | When a root cannot be planned, for missing provider credentials or any other provider error, chant falls back to the state read, and the card says `plan: not read` and `read: state, no provider credentials`. A card is never shown as "no changes" unless the plan itself said so. A root whose backend cannot be reached is declared only. Values the plan marks sensitive, and any copy of one, arrive as `(sensitive)`. Only the plan's facts are added to a card; the plan document never leaves chant. `GET /api/terraform/plan` gives the same facts per card as JSON for an agent: the action, the replace reason and paths, where each forcing value comes from, the values unknown until apply, the changes and the drift. A chant or lexicon older than 0.124.46 is refused with the version to upgrade to, and `behold doctor` lists the floor. A choudoufu member keeps its own plan read (see [choudoufu](/behold/using/choudoufu/)): chant's plan depth covers stock roots, and a live root keeps its `live-plan -json` read (chant#4371). ## Not the carve lens Both involve Terraform and they answer different questions. **This** asks what is in your Terraform: draw it, box it by root, tell the roots from the modules. **[The carve lens](/behold/using/carve/)** asks what it would cost to leave: `chant carve advise` ranks each resource by peelability, and `behold carve` draws that ranking with the score arithmetic behind each card. An estate being drawn here is not being migrated, and carries no score. --- # Look at a terragucci estate Source: https://intentius.io/behold/using/terragucci/ ## Optional: hand this page to your coding agent ```text Read https://intentius.io/behold/using/terragucci/. Ask me where the reports bucket is. Serve this repository with `npx @intentius/behold serve . --terragucci ` and tell me the URL. List the roots that carry a mark and the date on each. Never deploy with the Deploy button, `POST /api/apply` or `POST /api/ci/dispatch`. Gate approvals stay mine as well (`/api/ops/:name/signal/:gate`, `/api/workspace/gates/approve`, `/api/operator/approve/:op/:gate`). Op runs (`/api/ops/:name/run`) and rollbacks (`/api/rollback`) are mine too. Leave `--auto-sync` off, and keep a `behold preview` or `--public` serve read-only. ``` [terragucci](https://intentius.io/terragucci/) plans, applies and checks drift for a Terraform or OpenTofu repo in your CI, and keeps every run's report in a bucket. behold reads those reports and draws the repo's roots as one graph, each card marked with what the newest run found about it. [The live demo](https://demo.intentius.io/kiosk) shows behold on real terragucci pipelines. It runs in a loop and is read-only. ```sh npx @intentius/behold serve . --terragucci s3://acme-terragucci-reports npx @intentius/behold serve . --terragucci ./reports # an `aws s3 sync` of the prefix npx @intentius/behold serve . --terragucci https://reports.example.com ``` Nothing new runs in your CI and nothing is hosted. behold runs on your laptop, reads the checkout for the graph and the bucket for the verdicts, and reads no cloud. Serve the repo's root: its `chant.workspace.json` declares no members (terragucci writes one for its gates), so behold reads the Terraform roots in the directory. ## What it reads The keys from terragucci's [reports bucket](https://intentius.io/terragucci/reference/reports-bucket/), each with one GET and never with a list: | Key | What behold takes from it | |---|---| | `index.json` | every run, newest first, and the directory of each | | `/report.json` | for each root, the newest `tf-plan` and `tf-drift` run that planned it, and the newest `tf-apply` wave that held it | | `estate.json` | the counts and the waiting waves, as terragucci's estate page shows them | | `audit.jsonl` | the audit record, for the [timeline](#the-timeline-from-the-audit-record), when the job writes one | | `/runs//run.json` | the newest apply's progress per resource, for [apply progress](#apply-progress-from-the-run-view) | With an `s3://` source on a laptop, behold reads each key with your own `aws s3 cp -`, so it uses your shell's profile and region. In a job, where the credentials are in the environment, it signs its GETs itself. A read-only identity needs `GetObject` on `/*index.json`, `/*/report.json` and `/estate.json`, the same policy terragucci's estate job has. `audit.jsonl` and the run views are optional reads, and they need `GetObject` on `/audit.jsonl` and `/*/runs/*/run.json` as well. When S3 answers 403 for one of them, behold says the record is not readable with these credentials, names the key and `s3:GetObject` (S3 also answers 403 for a missing object when the identity lacks `s3:ListBucket`), and draws the rest of the page as it would for an absent file. A 403 on `index.json`, a report or `estate.json` still fails the read, with the same words. When `@intentius/terragucci` is installed beside behold or in the repo, every document is checked against the JSON Schemas it ships. Without it, behold checks the schema id and the fields it reads, and refuses anything else with `code: "terragucci-report"` and what to do. behold looks for the package on every read, so installing or upgrading it while `serve` runs takes effect on the next read (within the 30 second cache, or at once with `?fresh=1`). ## What a mark means A mark is what one run found, when that run finished, and where its report is: - **⚠ drift**: the newest drift check found the resource changed outside Terraform, deleted outside Terraform, or existing outside the state. - **~ plan**: the newest plan that holds the root would change the resource. The pull request is named. - **⏸ wave**: a `tf-apply` wave holding the resource waits for an approval and would change it once approved. Each card's corner says how old its newest mark is. Clicking the card lists every mark in the inspect pane, worded the way terragucci words it ("drift found 06:04 UTC, 4h ago: deleted outside Terraform"), with links to the run's report, job, pull request and trace. The **Terragucci** tab lists every root: its newest drift check, plan and apply wave, or "no report" when no run holds it, which is not the same as "no changes". A mark is never a fill. The fill is behold's live-status channel, and nothing here is live: a report is what a run saw when it ran. A card no report flags is left as it is. Read it as "the reports say nothing about this card", not as "this card is fine". Report pages open through behold, which reads them from the same source, so a link works for a private bucket too. They are served sandboxed. ## The timeline, from the audit record When terragucci's estate job runs `terragucci audit`, the top of the reports prefix holds `audit.jsonl`: one line per approval, approval request and revocation, policy override, apply, refused wave, state migration, lock release, state export and ephemeral copy, with who, when and the evidence ([The audit trail](https://intentius.io/terragucci/reference/audit-trail/)). behold reads it with one more GET and draws it as a **Timeline** lane, newest first, under the roots in the Terragucci tab. Clicking a card shows the entries of its root in the inspect pane (the record names roots, never a single resource). Each entry keeps its time and its run, and links the run's report the same way a mark does, plus its CI job or the ledger commit on `chant/lifecycle`. An approval names no root of its own: behold gives it the roots of the request, apply or refusal of the same wave and plan digest. A source without `audit.jsonl` (the job does not run `terragucci audit`, or `--terragucci` names one project's directory rather than the prefix) shows the sentence "has no audit.jsonl", which is not the same as "nothing happened". Every line is checked against `dist/audit.schema.json` when `@intentius/terragucci` 0.4.7 or newer is installed, and structurally otherwise; one bad line refuses the record with `code: "terragucci-report"` and its line number. The read-only identity needs `GetObject` on `/audit.jsonl` too. For an agent: `GET /api/terragucci/timeline?root=&limit=` (200 by default, 1000 at most). ### The ledger's own history Under the audit record, the Timeline shows a second list labelled "ledger": every request, approval, policy override and revocation in the gate ledgers on `chant/lifecycle`, read from the branch's git history with chant's gate-audit reader. Each entry names the ledger line it came from and the commit that added it, with that commit's author and date. A revocation is a line a commit removed, attributed to whoever made that commit. When the reports have no `audit.jsonl`, this list is the history. It holds no applies: those are in terragucci's reports, not the ledger. The branch is read from the same cache as the gates and locks below, and falls back to your checkout's `origin/chant/lifecycle` the same way. The route works for any product that keeps its gates on `chant/lifecycle`: `GET /api/lifecycle/history?op=&gate=&limit=`. ## Waiting waves A wave waiting for an approval appears as a gate card in the Terragucci tab: the roots it holds, what its kept report says it destroys or replaces, how long it has waited, and the line to run: ```sh npx terragucci approve wave-2 --plan jcs1-sha256:2e0f… ``` There is no approve button. An approval is a git record on `chant/lifecycle`, bound to the wave's plan digest and, under `approval: sealed`, signed with your key, so it is made at your shell. Copy the line from the gate card, digest included: `--plan` makes terragucci approve only the plan you looked at, and refuse if the wave now holds a different one. On a terragucci repo behold refuses every write (Deploy, Op runs, approvals, pipeline dispatch, rollback, and any write route added later) with `409` and `code: "terragucci"`. The refusal's remedy is the same `npx terragucci approve wave- --plan ` line. The only writes it still answers stay inside behold: saving the hand layout, switching the served project, refreshing caches, and the carve walkthrough's scratch steps. The pipeline is the only way to apply. ## Gates and locks, from chant/lifecycle The reports say what a wave asked for when its run finished. The gates and locks themselves live on the repo's `chant/lifecycle` branch, and the Terragucci tab reads them too: - each wave's gate from `_gates/tf-apply.jsonl` and `_gates/tf-apply/applied.jsonl`: waiting (with the approve line), approved and by whom, applied, failed, a request that expired, or refused because an approval stands for other plans. Once terragucci's waves run on chant's op waves, a wave can also be refused by policy (a run was denied and no override counts; the card gives the override line for each run), held on another gate such as a state migration (the card names the hook that holds it and gives that gate's approve line), or applied with its outcome not recorded. A gate that asks a decision point gives `chant workspace points answer ` instead of an approve line. behold decides each wave with chant's own gate decision, the one terragucci applies under: an approval a wave already applied under is spent, and under `approval: sealed` an approval counts only when its seal verifies against the signers file on the default branch. A gate that `identity.gates` in chant.workspace.json names counts only a signed approval under any mode, and when it names a class, only one from a person the role grants on the default branch put in that class. A class that a pinned package declares is read from that package as installed in your checkout, so run `npm install` there first; until it is installed at the pinned version, the card says the pin could not be read and no approval counts for that gate. Each wave is decided under the approval mode its request recorded, and the card says which. A request written by an older chant records none: that wave takes the mode read on the default branch, and the card and the lane's legend say so. An approval that does not count is shown with the reason. A card also names who relayed an approval from chat, the pull request review it came from, and the roots the wave planned; - each locked root from `_locks/tf-apply.json`: the pull request holding it, who took it, when, and whether a plan, an apply or `/terragucci lock` did. Every gate and lock names the commit of `chant/lifecycle` it was read from and when that commit was made, and the tab says when behold last read the branch. behold fetches the branch from your checkout's `origin` with your own git and credentials, into a cache of its own (`/behold-lifecycle-*`). It never fetches into your checkout and never moves one of its refs. When the fetch fails (offline, or no access), the tab shows your checkout's `origin/chant/lifecycle` as of your last `git fetch`, and says so. A repo with no `chant/lifecycle` yet shows that no gate has asked and no root is locked. While the page is open, behold checks every 30 seconds whether the reports' `index.json` changed (a conditional request: `If-None-Match` for a bucket or an address, the file's time for a directory) and fetches `chant/lifecycle` again, and the page updates its marks and gate cards without a reload (every 10 seconds under `--public`, below). Nothing is polled when no page is open. Change the interval, or turn it off with `0`: ```sh npx @intentius/behold serve . --terragucci s3://acme-terragucci-reports --terragucci-poll 120 ``` With an `s3://` source on a laptop, the check is `aws s3api head-object --if-none-match`, so the identity needs `GetObject` on `/index.json`, which it already has. behold approves, locks and unlocks nothing: unlock is a pull request comment, approve is the line on the gate card. ## Apply progress, from the run view A choudoufu repo's `tf-apply` wave job writes its progress per resource into the run view while it applies: each resource its plans change, done, in flight, waiting, or not applied once its root's apply failed, as of the job's last read of the estates' records. behold reads the run view of the newest commit `index.json` names an apply of (`//runs//run.json`, one more GET) and tags each card of the wave at its top corner, for example `▶ wave 2 in flight · 2m`. The age is that of the records read, not of the page. The tag is never a fill, and the Terragucci tab lists each wave's counts with the same read time. While a wave is applying, or a resource is in flight or waiting, the 30 second poll also asks whether that `run.json` changed (`If-None-Match`, or the file's time for a directory) and updates the tags without a reload. Once every resource is done or not applied it stops asking. A source with no run view (no `reports.bucket`, or a Terraform repo, whose waves write no progress) says so, which is not the same as "nothing is applying". The read-only identity needs `GetObject` on `/*/runs/*/run.json`. For an agent: `GET /api/terragucci/progress`. ## The counts are terragucci's When the source holds `estate.json`, the drifted, failed and waiting counts behold shows are the estate page's own, and the tab links `estate.html`. behold never counts them again, so the two never disagree. behold owns the resource graph: which resources, in which roots, drifted or will change. ## Publish a view beside the estate page For a team that wants a link rather than a command, the job that already runs `terragucci estate` can publish a static view to the reports bucket. terragucci keeps `/views/` for a viewer's files and never writes there. One more step in that job, after `terragucci estate`: ```sh npx -y @intentius/behold export . --terragucci s3:/// --no-source \ --publish s3:////views/behold ``` The Terraform packages behold reads HCL with (chant's terraform lexicon and `@cdktn/hcl2json`) are dependencies of behold since #909, so the one `npx` brings them. behold is built and tested on chant 0.124, and the lexicon range it declares starts at its own chant floor: the lexicon's peer on `@intentius/chant` holds its minor, and a lexicon from another minor would install a second chant that behold's reader never resolves. behold's chant reads a root whose `chant.workspace.json` asks for `minReader` 0.108.0 or anything up to 0.124. The step reads the reports and writes the view with the job's own credentials from the environment (static keys, or a role assumed with the job's OIDC token), so the job needs no aws CLI. Add `PutObject` and `GetObject` on `/views/behold/*` (and `DeleteObject` there, for the sweep below) to the job's policy, beside the reads `terragucci estate` has and `GetObject` on `/*/report.json`. `--publish` writes only under a `views/` directory, so a typo never puts the view's `index.html` over the prefix's own. The view is a static folder: the same marks with the same dates, the gate cards and their lines, and terragucci's counts. Its report links are relative to `/views/behold//`, so they open the bucket's own report pages through the same presigned link or front door as `estate.html`. It carries no credential, no user name and no path of the machine that made it, and `--no-source` leaves each root's HCL out. The upload is ordered so a reader never opens a half-written view: the snapshots and the SPA's files first, then `manifest.json`, then `index.html`. Each snapshot's name carries a hash of its contents and is uploaded with `Cache-Control: public, max-age=31536000, immutable`; `manifest.json`, `index.html` and the SPA's other files get `Cache-Control: no-cache`, so a browser or CDN asks the bucket again and finds the new manifest. Before uploading, the step reads the previous `manifest.json` (one GET), and after the new `index.html` it deletes the snapshots that manifest named and this export did not write, only under `/views/behold/`. That needs `DeleteObject` on `/views/behold/*`; without it the step prints a warning naming `s3:DeleteObject`, leaves the old snapshots, and still succeeds. Each run keeps its own view, filed under the commit it was exported from: ``` /views/behold// the view, as above (index.html, manifest.json, snapshots/, ...) /views/behold/history.json newest first: [{commit, at, project, generated}] /views/behold/latest.json {commit} /views/behold/index.html opens the commit latest.json names ``` The commit is the checkout's `HEAD`, or `--commit `. The ordering above applies inside `/`; after that commit's `index.html` the step rewrites `history.json` (one GET of the old one, no list call), then `latest.json`, then the root `index.html`, all `no-cache`. The root page is a few lines that read `latest.json` and open `/index.html` with the link's query and fragment, so a link to `/views/behold/index.html` keeps opening the newest view. Inside a view, a picker beside the "static snapshot" pill reads `../history.json` and opens another commit's view, each labeled with its commit and when it was exported. `at` is the commit's own date and `generated` the export's. `--keep ` (30 by default) is how many commits `history.json` holds. A commit past it leaves `history.json`, and its files are deleted as that commit's own `manifest.json` names them (`index.html` first, `manifest.json` last), with the same `DeleteObject` permission and the same warning as the sweep above. Nothing outside `/views/behold/` is touched. A view published before this layout, its files directly under `views/behold/`, is left where it is; only its `index.html` becomes the page that opens the newest commit. Make it a step of its own that may fail: a view that cannot be built leaves the estate page as it was, and `behold export` fails rather than publishing a picture with no marks. ## Several projects in one bucket A bucket prefix can hold many repos' runs. behold picks the project named by the checkout's git remote (`github.com/acme/infra`). If the index holds several and none is this checkout, it asks for one: ```sh npx @intentius/behold serve . --terragucci s3://acme-reports --terragucci-project github.com/acme/infra ``` ## A control repo A control repo lists its projects under `projects:` in `terragucci.yml` and holds no Terraform root, so there is no card to mark. Serve it with the top of the reports prefix, where `terragucci estate` writes `estate.json`: ```sh npx @intentius/behold serve . --terragucci s3://acme-reports ``` behold draws one box per project with its roots grouped by wave, a solid arrow for each read inside a project and a dashed one for each read between projects. It reads `estate.json` once, then each project's newest run view, `/runs//run.json`, once. The commit comes from `estate.json`: the project's `run_view` when it names one, otherwise its newest apply. A read between projects is a root's `terraform_remote_state` read of a state that a root of another project holds, matched the same way terragucci's estate page matches it. Each box shows terragucci's counts for the project, dated by `estate.json`'s `generated`, and the run view it was drawn from, dated by when a wave last wrote it. Under the picture, each project lists its waiting waves with the approve line to copy. The approve line comes from the run view, so it carries the plan digest; run it in the project's own checkout, because behold approves nothing. Each project also links its index, the estate page and its run view. When the project's repo is checked out next to the control repo (a sibling directory whose git remote names the project), the list shows the `behold serve --terragucci ` line that opens that repo. A project with no run view (no apply yet, or none in the bucket) stays on the page as a box that says "no run view". A run view that does not match terragucci's `run.schema.json` is refused on its own project, and the other projects still draw. A project listed in `terragucci.yml` that `estate.json` does not list is shown as "not in estate.json". The read needs `GetObject` on `/estate.json` and `/*/runs/*/run.json`, and no list call. ## A public view `behold serve --public` serves a read-only view to anyone who reaches it, for a repo whose pipeline is the only thing that applies it, such as one a loop drives through a change again and again: ```sh npx @intentius/behold serve . --public --allow-host demo.example.com \ --terragucci s3://acme-terragucci-reports ``` Under `--public`: - behold builds no op runner, so no route can start an Op, an apply or any other write. - Every method but GET, HEAD and OPTIONS is refused with 405, on every path, before any route sees it. - Every read that runs a command or calls out with behold's own credentials, and isn't part of the view, is refused with 403: - `/api/ops//status` and `/api/operator/*`, the op tools' reads - `/api/overlay`, the live overlay, its `plan=1` attribute-drift read included - `/api/graph` with `env`, `target`, `consistent`, `plan` or `live`; the source graph without them still answers - `/api/reconcile`, `/api/diff`, `/api/diff/` and `/api/components/compare`, which read or plan the live estate - `/api/resources`, `/api/ci` and `/api/ci/run` - `/api/doctor` and `/api/substrates`, which probe the host's tools - `/api/history`, which runs `git log` - `/api/workspace/gates` and `/api/workspace/why` - `/api/choudoufu/split` and `/choudoufu/split` (and their older names, `/api/choudoufu/moves` and `/choudoufu/morph`) - `/api/sql-yodeler/lifecycle` - What still answers: `/api/terragucci` and everything under it (the timeline, progress, estate, lifecycle and its events), the gate history (`/api/lifecycle/history`) and releases (`/api/lifecycle/releases`), which read the same `chant/lifecycle` branch; the source graph; the project summary, which offers no environment; the event stream; the lanes; and the page itself. - The page shows no control that writes or asks for a refused read, and no environment picker. A waiting wave's approve line is still shown, as the line someone else runs. - The `--terragucci` poll defaults to 10 seconds. `--terragucci-poll` still sets it. - One project directory only, and not with `--env`, `--poll`, `--local` or `--auto-sync`. behold still binds 127.0.0.1. Put a proxy in front of it that forwards the public name in `Host`, and pass that name with `--allow-host`; any other name is refused with 421. Give the process read-only credentials: `GetObject` on the reports bucket, and a read-only key for the checkout's `origin`, which `chant/lifecycle` is fetched from. ### The kiosk `/kiosk` is a page that follows terragucci's looping demo, one change across the `network`, `app`, `edge` and `other` roots, run through the real pipeline again and again (`just demo loop` in terragucci's repo). Open `https://demo.example.com/kiosk` on the host above; it works under `--public` and without it, and needs `--terragucci`. The demo's driver writes `demo.json` beside `index.json` at every step, so the kiosk reads it from the top of the `--terragucci` source (for the demo, `s3://demo-reports/demo`), through `GET /api/terragucci/demo`. That read needs `GetObject` on `/demo.json` and nothing else, and one read is shared by every open kiosk for 2 seconds. A `demo.json` that is missing, refused or not in the demo's format is said on the page, which waits for the next one. When the estate has choudoufu roots, or the reports hold estate rosters (`estates.json`, each entry naming its roster), the kiosk also draws a carve as it is written. It reads each roster and each estate migration's progress (`/migrations//progress.jsonl`, appended while the migration runs) through `GET /api/terragucci/carve`, one GET per key, shared for 1.5 seconds while a migration runs. Zoomed out, every resource is a dot in its estate's box: an adoption's stamp leaves a ring, a move flies the dot to its new estate, and a refused move bounces back and stays lit red. While a migration runs, Watch frames the source estate, then the estate receiving moves, then the whole set, under a caption with a "live" badge, and Tips mark each estate's first landing, the first refusal and the end. With neither choudoufu roots nor rosters the route answers `live: false` and the kiosk is as described here. The kiosk is one screen: a status bar with the narration; under it, the pipeline's runs the way a CI page shows them (a sidebar of recent runs with the live one pinned on top, the selected run's jobs in their `needs` order, each job's steps and the step that matters opened on its log, following it while it runs); and behold's own view of the change beside them (the same page as `/`, framed with `?embed=1&bare=1`, so nothing in it opens or writes), and a line of facts under it. In the bar: - The scenario, numbered as terragucci numbers it, with the title the driver wrote. A scenario id behold doesn't know shows the title alone. - Under the title, the scenario's card, always open so a screen nobody touches reads it: three rows, "What changes", "Watch for" and "Shows", from demo.json's optional `about` (`changes`, `watch` and `shows`, each a non-empty string). terragucci keeps the text beside each scenario, in `example-demo/scenarios//about.json`. Without `about`, or with null (the base), there is no card. - Where the cycle is: pull request, planned, merged, the waves, applied, reset. A failed step is marked; a stalled cycle marks none. - The driver's words for the step (`label`), as written. Where a value applies without a review that showed it, the driver says so, and so does the kiosk: each read of an output known only after its upstream applies gets its own sentence. - The driver's next action, with a countdown to `next.at` when the driver acts on its own clock, or "when the pipeline gets there" when it waits on a run. - At a waiting wave, the wave and its roots, and what the driver runs when the countdown ends: `chant approve tf-apply wave- --actor --sign `, then a `/terragucci apply` comment on the pull request. The driver plays two people (terragucci#1052): an author who opens, merges and comments, and a reviewer with no hand in the change who approves with its own signing key, since only such an approval counts. The actor and key are placeholders, never a path. The wave's digest from the run view is shown beside it, so a viewer sees which plan is approved. There is no copy button: nobody watching needs to run it. - Until the reports hold the step's run, that it isn't written yet. In the framed view, which is behold's resources view of the repo, and draws the same in the full app: - Each root is a box. Its border and the badge on it say its wave and that wave's state: planned (blue), applying (teal, pulsing), waiting for an approval (yellow), applied (green), failed or refused (red), not started or not in the change (outlined). A root its wave plans no change in reads "no changes" (outlined) whatever its wave does, and the kiosk leaves it out of the roots it names for an approval. The resource cards wear one neutral ground while no drift was read, so colour on this view means a wave's state. Under a box, its facts: the plan a waiting wave approves, its changes, that it plans again once an earlier wave applies. A wave that failed leaves the waves before it green: what applied stays applied. A composed run that failed part way shows each root's own part. - Each read between roots is an arrow from the reader's data block to the root it reads, dashed while it reads planned values and yellow when it reads a value unknown until its upstream applies, with what it reads written beside it. - A caption says which run all of it comes from: the pull request's plan before the merge, the run view from the merge on. - The kiosk keeps the view on the cycle its narration describes with a `behold:view` message (`run`): the pull request's plan before the merge, the cycle's commit (demo.json's `commit`) from the merge through `applied`, and nothing in flight at `reset`, where the caption says the states went back to the base. The loop's own next runs (the reset's push back to the base) never take the view while the narration is before `reset`. - The kiosk brings the roots each step is about forward with a `behold:view` message (`focus`): the pull request's roots while it is planned, then the wave that waits, applies or failed, then everything once applied. - Pointing at a card opens a popover next to it. The popover stays inside the frame and leaves the card in view. The resources view names the resource and its root and gives each attribute the plan changes its own line as `before -> after`. Six lines show at most, and a count stands for the rest. A value known only after apply shows in the unknown colour with a note that no review shows it. Once a wave has applied the root, the popover also lists the values the state holds now. The view by wave lists the root's resources and marks whether each one changes, stays as it is or is known after apply. Its first line counts the changes. A click keeps the popover open until a click elsewhere or Escape, and for 90 seconds at most. Nothing opens without a pointer, so a screen nobody touches looks the same. - Each root has its own colour. It shows only as a dot by the root's name and never as the colour of a text or a fill, since those mean a state. The dots sit on the root's box or card and by each wave's job. The narration and the facts carry them too, as do terragucci's own log lines. The colours follow the sorted list of roots in `terragucci.components.json`, so each cycle keeps them. Pointing at any root's name lights that root in every pane, and the same goes for a wave's job and a box. Under it: the pull request's totals, its blast radius, the roots it does not touch and its cost. The runs are the demo repo's real pipeline runs, which the loop driver copies from its forge into the reports bucket as they go: `/runs/index.json` (the newest runs, at most `keep`, each with its jobs) and `/runs/.json` (one run, each job's steps and each step's log tail), read through `GET /api/terragucci/runs` and `GET /api/terragucci/runs/`, shared between kiosks for 2 seconds like `demo.json`. A job held at a terragucci gate is `waiting` and is drawn as held at the gate, never as a failure. Step names are shown as the driver wrote them. A viewer can open an older run in the sidebar; the kiosk goes back to the live run 90 seconds after the last pick. `--terragucci-runs ` reads the runs from somewhere else than the reports, such as a directory of fixtures (behold's own are in `src/__fixtures__/terragucci-demo/`). The kiosk asks for `demo.json` every 3 seconds, and for the reports and the run view when the step moves or the event stream says they did. Everything it asks is a GET, and it shows no control. #### One checkout per repo The demo runs each tool on its own forge repo. `behold serve --repos ` serves every git checkout directly under ``, one per request: a request with `?repo=` (the checkout's directory name, or `/` as `demo.json` writes it) reads that checkout, and a request without one reads ``. The kiosk asks for the repo its scenario runs on, and a tool's page for its tool's repo, so each draws its own repo's graph, marks and popover. A name the server doesn't serve is refused with 404; only the names it listed at startup ever reach the filesystem. A Terragrunt repo is drawn the same way: each unit (a directory with a `terragrunt.hcl`) is a root box holding the resources of the module its `terraform { source }` names, when that is a directory of `.tf` in the repo, and each `dependency` block is a card with an arrow to the unit it reads. behold reads those two blocks and evaluates nothing else. A unit whose module it can't read is one card saying so. #### chant's fan-out demo, from its own bucket chant runs its fan-out demo itself, from `examples/fan-out-estate/demo` in chant's repo, with no terragucci in it. Its driver writes two records under one prefix: `step.json` at every step (chant's `demo-step/v1`) and `cycles/latest.json` at each reset (`demo-cycle/v1`). Point behold at that prefix: ```sh behold serve --repos --chant-demo s3://chant-demo/fan-out-estate ``` `/chant-fanout` then reads the demo from those records, through `GET /api/chant/demo` (the step) and `GET /api/chant/demo/cycle` (the last cycle, which the page replays between cycles). Without `--terragucci`, `/kiosk` is the same page. The step keeps the field names the kiosk reads from `demo.json`, `chapter` included. Each step names chant's report keys (`reports`: `url`, `plan` and `apply`, each `/`). behold reads the stage's `pr-plan.json` or `pr-apply.json` (`pr-report/v1`) and its `.events.jsonl` (`run-event/v1`) at those keys, for the graph's waves and the gate. The runs pane shows one run per stage, built from its run events: the plan, the gate and each wave as steps, each with the events' lines. Nothing is read from forge runs, and the pipeline's `--report-url` is not looked up in the workflow file. `--chant-report-url`, when given, overrides the step's `url`. The step's `cycle_reports`, every stage of the cycle so far, name the first apply's key, so the fix's run shows what that apply landed even when behold started mid-cycle. Each record is checked against chant's schema before it is read. A record of another contract version (`contract`, or the `v` in its `$schema`) is refused, and the page says which version behold reads. So is a `reports.url` that is not `s3://` or `https://`. A `step` behold doesn't know is not refused: chant may add steps within a version, so the page shows it by its name and lights no stop. Without `--chant-demo`, every page reads terragucci's loop as above. #### Runs from chant's run index `chant ci run --report-url ` (chant 0.124.31 and newer) keeps a run index at `/runs/index.json` (`ci-run-index/v1`), the document `chant ci runs --report-url --json` prints. Give behold the same URL and the kiosk's runs pane reads its runs there instead of the forge's: ```sh behold serve --terragucci s3://reports --ci-report-url s3://reports/chant ``` `BEHOLD_CI_REPORT_URL` does the same. The URL is s3://, https://, file:// or a directory. The pane lists the index's runs, newest first, each with its member, why it failed and, when the run's record was signed, who signed it. An apply that exited 3 (the plan moved, or a gate waits) is drawn held, not failed, whether its report says why or only the entry's `exitCode` does (chant#4165). Opening a run reads its `pr-.events.jsonl` under the entry's key as the log, the way the fan-out demo's runs are built. A job run without `--events-json` has no events file, and its log says so. A run whose forge names it links to it. `GET /api/chant/ci/runs` and `GET /api/chant/ci/runs/` answer them; a run is read only by an id the index lists, and only under its own key, so `--public` serves both. An index of another contract, or one written by a chant older than 0.124.31, is refused with the reason in the pane. A workspace can declare the URL instead, in `chant.workspace.json` (chant 0.124.32 and newer, chant#4160): ```json { "ci": { "reportUrl": "s3://reports/chant" } } ``` When `behold serve` is pointed at that workspace without `--ci-report-url`, it reads the run index at the declared URL. The flag still overrides the declaration, and both routes' answers carry `urlFrom`, `flag` or `declaration`, to say which one named it. `behold doctor` lists the floor with the rest. With neither, or on an older chant, the pane reads the forge's runs, as before. On a served workspace, the panel's **Local** tab runs one of these plan jobs on this machine and draws it in the same run view beside CI's run: see [Running a CI job locally](/behold/using/local-run/). ## Try it on terragucci's example behold ships a reports bucket for terragucci's own example estate, written by terragucci's code (`example-terragucci-reports/` in behold's repo, whose README lists the runs). From a terragucci checkout: ```sh npx @intentius/behold serve example --terragucci /example-terragucci-reports ``` It shows a drift on staging orders' jobs queue, two pull requests' plans, a failed drift check on prod payments, and wave 2 waiting with a destroy. Its `audit.jsonl` gives the timeline wave 1's apply and wave 2's request. --- # Look at a SQL Yodeler project Source: https://intentius.io/behold/using/sql-yodeler/ ## Optional: hand this page to your coding agent ```text Read https://intentius.io/behold/using/sql-yodeler/. Serve this SQL Yodeler project with `npx @intentius/behold serve .` and tell me the URL. List the environments the Releases tab shows. For each gate that waits, show me the `yodel approve` line to run myself. Never deploy with the Deploy button, `POST /api/apply` or `POST /api/ci/dispatch`. Gate approvals stay mine as well (`/api/ops/:name/signal/:gate`, `/api/workspace/gates/approve`, `/api/operator/approve/:op/:gate`). Op runs (`/api/ops/:name/run`) and rollbacks (`/api/rollback`) are mine too. Leave `--auto-sync` off, and keep a `behold preview` or `--public` serve read-only. ``` [SQL Yodeler](https://intentius.io/sql-yodeler/) plans and applies database migrations through chant, one environment at a time. behold draws the project's declared schema and reads what `yodel` and chant already wrote about each environment. It never writes to the repository, its `chant/lifecycle` branch or a database. ## The schema `behold serve .` in a SQL Yodeler project draws what `src/` declares: Postgres schemas, tables, indexes and the rest, or ClickHouse databases and tables, each card named by its kind ("Postgres table") and its qualified name, with the column and foreign key count. A table that `REFERENCES` another has an edge to it. The migrations ops in `ops/` draw as `Chant::Op` cards. ## The pipeline The Pipeline tab reads the gates SQL Yodeler and chant write on `chant/lifecycle`: the apply pipeline's waves (`_gates/yodel-apply.jsonl`, one gate `yodel-apply-wave-` per environment) and each environment's migrations op (`_gates/migrate-.jsonl`, gate `approve-migrate-`). Environments are listed in the order `yodel-waves.json` applies them. Each gate says whether it waits, was approved, applied, or was refused, the plan digest it is bound to, who approved it and how (signed, through a pull request's review), and which commit of `chant/lifecycle` it was read from and how old that is. A waiting gate shows the line to approve it: ```sh npx yodel approve prod --plan ``` Copy it and run it yourself; behold has no approve button for SQL Yodeler. When the waves in `yodel-waves.json` at the base commit name different approval modes (say `pr-review` for staging and `sealed` for prod), each wave is decided under its own mode. A wave's request records the mode it asked under, and its card shows it; a request written by an older chant records none, and then the card says the mode was read at base. A wave that names none takes chant's rule. A wave held on another gate names the hook that holds it. Under `pr-review`, a waiting wave also lists the pull request heads whose recorded plans (`_wave-plans/yodel-apply/.json`) name its digest: a writer's approving review of that head, once merged, approves it when the wave runs again. behold does not read the forge, so it cannot say whether that review exists. A project that is a member of a chant workspace keeps all of this under `_members//` on `chant/lifecycle`. behold asks chant which member owns the served directory and reads the gates, the head plans and the releases there; the legend names the member. Under the tab, the gate ledgers' history lists every request, approval and apply in the order they were committed. The history table that records which migrations ran lives in the database, which behold does not connect to; the report files below and the release ledger cover that ground. ## Releases After every apply SQL Yodeler appends a release record to `/releases.jsonl` on the repository's `chant/lifecycle` branch, keyed by the digest of the schema the environment now holds. When the served directory has a `yodel.config.ts` or a `yodel-waves.json`, behold adds a Releases tab: one column per environment, one row per component or tenant (`/`). Each cell is that environment's newest release, with its digest, when it was recorded, who ran it and who approved it, and the `chant/lifecycle` commit it was read from. Columns follow the wave order in `yodel-waves.json`. A cell whose digest the environment before it does not hold is marked "differs from ``", and its hover gives both digests and says whether that environment ever recorded this one. It answers the same question as `chant components status prod --compare-to staging`. behold fetches `chant/lifecycle` from the checkout's origin into a cache of its own and reads it with chant's release-ledger reader. If the fetch fails it reads the checkout's `origin/chant/lifecycle` instead and says so. It never writes to the repository, the branch or a database. `behold export` carries the strip as a snapshot of the moment it ran. ## Report and drift ```sh yodel report --json > out/report.json yodel drift prod --json > out/drift-prod.json behold serve . --yodel-report out/ ``` `--yodel-report` takes the report file, or the directory holding it. Any drift JSON in the same directory is read too. behold never runs `yodel` and never connects to the database: it reads the files you or your pipeline wrote. - The SQL Yodeler tab lists each environment's applied migrations, the ones not applied with their state, and the gaps the report found, dated by the report's `generatedAt`. - A table or other object a drift file names as changed or missing gets a corner tag on its card, such as `drift prod · 4h`. Drift JSON has no time of its own, so the age is when its file was written. Hover the card or open it for the properties that differ. - Objects no card is are listed in the tab, not dropped. - The report's audit entries are listed under their environment, oldest first, each with its own time and where SQL Yodeler read it: a gate ledger on `chant/lifecycle`, or a row of the environment's history table. `behold export . --yodel-report out/` carries the report and drift as a snapshot of the moment it ran, and fails if the files cannot be read. A SQL Yodeler repo's Pipeline lane is captured the same way. Every file is checked against SQL Yodeler's own JSON Schemas, from `@intentius/sql-yodeler` when it is installed and from a copy pinned in behold otherwise. A file that does not match is refused by name with `code: "yodel-report"`, and nothing from that read is drawn. --- # Look at a choudoufu estate Source: https://intentius.io/behold/using/choudoufu/ ## Optional: hand this page to your coding agent ```text Read https://intentius.io/behold/using/choudoufu/. Start `npx @intentius/behold demo choudoufu-estate`, or serve this estate with `npx @intentius/behold serve . --env `. Tell me the URL and list the cards that read unowned. Copy out each move preview for me to run. Never deploy with the Deploy button, `POST /api/apply` or `POST /api/ci/dispatch`. Gate approvals stay mine as well (`/api/ops/:name/signal/:gate`, `/api/workspace/gates/approve`, `/api/operator/approve/:op/:gate`). Op runs (`/api/ops/:name/run`) and rollbacks (`/api/rollback`) are mine too. Leave `--auto-sync` off, and keep a `behold preview` or `--public` serve read-only. ``` [choudoufu](https://github.com/INTENTIUS/choudoufu) is an OpenTofu fork whose ownership lives in two AWS tags rather than in a state file. behold draws such an estate as a member kind of its own, beside chant projects and Terraform roots. ```sh npx @intentius/behold demo choudoufu-estate # four estates on a scratch emulator npx @intentius/behold serve ./my-estate --env live ``` ## A green card means "this is ours" This is the one thing worth getting right before reading anything else. The overlay is composed from `live-ls -json` and `live-plan -json`, and those two documents answer one question: is this resource bound, unowned, omitted, or invisible to the listing. **Neither of them ever compares an attribute value.** So a resource whose tag or path was changed out of band still carries its markers, is still bound, and is still green. Read a green card as *this is ours*, never as *nothing changed*. | colour | choudoufu's word | means | |---|---|---| | green | bound | a declared instance matched a live object | | amber | unowned | a live object at a declared identity that is not ours | | blue | pending | the plan would create it | | grey | not observed | the listing could not see it, or neither document mentions it | Those are choudoufu's words, not chant's, and the legend says so. A `bound` resource is not "managed": a resource bound by derived identity carries no marker yet, and claiming it is managed would claim a marker that is not there. ## Attribute drift is a separate question Since the ownership read never compares a value, "would a plan change anything?" is a second read you ask for: ⌘K → **Re-check live with plan**. It costs differently. The ownership read is answered out of the tagging index and is flat in the estate's size; a plan refreshes every resource, one provider read per card, which is 301 of them on the estate behold is sized against. So it never runs on an ordinary overlay. A bound card that drifted stays bound and stays counted as bound. It gains a dashed edge and a `~ n attributes` corner, because the ownership verdict is still the card's colour. The legend prints a drift count only after a plan was actually read. Before that it says so in words and prints no number, because "0 drifted" would be a claim nobody made. ## Unowned, and adoptable Two different situations, and the difference is where the identity comes from. **Unowned** is a live object sitting at an identity the configuration *declares*. The claim needs no evidence, because the identity is the match. The card leads with the two marker tags that adopt it. **Adoptable** is a live object the estate-wide sweep found for a declaration that names *no identity at all*: a VPC, say, whose `vpc-…` id EC2 assigns at create time and which nothing in the configuration reconstructs. choudoufu matches it by content instead, comparing the identity-bearing arguments for that resource type. An adoptable card leads with choudoufu's own adopt command, and carries `matchedOn`: the arguments the sweep compared. behold composes no command of its own, and shows the evidence because a person about to stamp two tags onto somebody's resource is owed it. Content matching covers a closed list of resource types. A type outside it can never produce an adoptable row, which is a property of the type rather than of your estate. ## The sweep is off unless you ask The sweep is bounded by the **account** rather than by the estate, and behold polls, so it cannot be on by default. Turn it on for a serve: ```json { "spawnEnv": { "TOFU_LIVE_COLLECT_UNCLAIMED": "1" } } ``` Without it, `adoptable` is empty on every run. Which is why an empty result says which empty it is: - *the sweep listed every `aws_vpc` and matched none of them to this declaration*, so the question was asked and answered. - *this run did not sweep the account, so nothing was looked for*, and the line names the variable that asks it. Reporting the second as the first would be an absence of evidence dressed as evidence of absence. ## A card's tag: marker or derived Each bound card wears a small tag on its top-right edge. The tag is filled when the resource wears this estate's marker as `live-ls` lists it. An inline policy has no tags argument and so no marker of its own, and its tag is outlined. The tag's tooltip and the inspect pane say how the plan found the resource: `marker` when the estate-wide tag sweep bound it, `derived` when the configuration names it. choudoufu binds a named resource, such as an IAM role or a log group, by its name whether or not a marker is on it, so a stamped log group reads `derived` with a filled tag. ## The terralith walkthrough `npx behold demo terralith` takes one stock Terraform root to four estates on a scratch floci. It starts with 23 resources in one state file on S3. The adopt phase runs terragucci's `backends` migration. There `choudoufu live-import` stamps the marker on each resource it verifies and leaves the state file as it was. Split runs the `moves` migration, which adds three team roots while `choudoufu live-mv` moves each team's resources into the team's estate. `split.json` keeps the record, and the team boxes appear as the cards glide into them. The plan phase changes all four estates in one step and plans it without applying anything. The terralith outputs a shared setting, and each team root plans against that planned value with `-estate-outputs-from`. The page outlines each team's cone, badges a root that planned in full with choudoufu's reason, draws an edge for the planned output to each root that reads it, and brackets the team boxes whose identical changes `live-summary` folds into one group. You run each phase's line in the demo copy, and the page at `/terralith` reads the documents it leaves in `snapshots/`. ## Splits are previewed, never run `GET /api/choudoufu/split` reads a `split.json` plan (or the live-mv workbench's `carve.json`, which reads the same) from inside a served member and previews each move with `live-mv -json -dry-run`, handing the lines back with copy buttons. `/choudoufu/split` draws the plan as a morph: every member a box, a new box for each estate the plan names that is not served yet, and the moved cards gliding into place. The names from before, `/api/choudoufu/moves` and `/choudoufu/morph`, keep working. **There is no `/api/choudoufu/mv`**, and a test asserts it stays absent. A move is one tag write against somebody's account. behold previews it and reads the receipt afterwards; the write is yours to run, which is the line the carve walkthrough draws around `terraform state rm`. --- # Look at a Terraform carve-out Source: https://intentius.io/behold/using/carve/ ## Optional: hand this page to your coding agent ```text Read https://intentius.io/behold/using/carve/. Run `chant carve advise --from --report carve.json`. Then serve the report with `npx behold carve carve.json` and tell me the URL. List the resources in the carve now band, and hand me the runbook commands to run myself. Never deploy with the Deploy button, `POST /api/apply` or `POST /api/ci/dispatch`. Gate approvals stay mine as well (`/api/ops/:name/signal/:gate`, `/api/workspace/gates/approve`, `/api/operator/approve/:op/:gate`). Op runs (`/api/ops/:name/run`) and rollbacks (`/api/rollback`) are mine too. Leave `--auto-sync` off, and keep a `behold preview` or `--public` serve read-only. ``` `chant carve advise` reads a Terraform estate and ranks each resource by **peelability**: how cleanly it could be carved into native chant source. `behold carve` draws that ranking. ```sh chant carve advise --from ./terraform --report carve.json npx behold carve carve.json ``` Read-only twice over. chant's advisor emits no chant source, patches no Terraform and makes no cloud calls; behold only draws what the report says. behold parses no HCL and needs no Terraform tooling installed: the report is the whole contract between the two. ## What you see One card per ranked resource or module, in three panels: - **carve now** (green), score 80-100. A clean leaf with a 1:1 native mapping and little or no boundary work. - **boundary work** (amber), score 50-79. Carvable, but survivors depend on it, so carving means patching the surviving Terraform. - **leave in Terraform** (grey), score 0-49. Unmappable, or too load-bearing to be worth it. The bands ride the same `attrs._status` channel the live drift overlay paints, so the colours mean what they always mean here: green is the good outcome, amber is work, grey is "not a claim". ## The arithmetic behind a rank Click a card. The inspect pane shows the score spelled out (`100 - 12x3 inbound = 64`) alongside the counts it came from: inbound edges, which cost a Terraform `data`-source patch each, outbound edges, which defer to a deploy-time input, and the native tier the resource maps to. Terraform sub-resources that inline into a parent, such as `aws_s3_bucket_versioning` into its bucket, are not ranked separately. They carve with the parent. ## The walkthrough: `behold demo carve` `npx behold demo carve` copies a bundled half-migrated estate (a small chant project beside a Terraform one, both describing the same AWS account), installs the chant it will shell, runs the advisor over the copy, and opens the graph with a six-step stepper on the panel's **Carve** tab. No Docker, no cloud account, no `terraform` binary. The demo's graph is an **estate frame**: the banded ranking sits inside a `legacy-tf` box, and the copy's chant project draws as an `app` box beside it, holding the pieces carved last month. The migration reads as half-done from the first frame. A plain `behold carve report.json` has no chant project to show, so it draws the banded ranking alone. 1. **Advise.** The bands, with what each one means and how many landed in it. 2. **Pick.** Click a green card. The inspect pane shows the score arithmetic; the step names the boundary the cut crosses (and says plainly when the report carries counts rather than edge lists). 3. **Emit.** Runs `chant carve emit --state --select
` into `app/carveout/` in the copy, then shows the emitted chant source, chant's own boundary report for the carve, and the `chant lint` result. 4. **Bridge.** Runs `chant carve bridge`, never with `--apply-rewrites`. The proposed data source, the rewired survivors and a git-applyable patch land beside the runbook; the demo's own `.tf` files are not edited. 5. **Handoff.** The runbook's commands, each with a copy button, and deliberately **not** a run button. `terraform state rm` releases Terraform's claim on a live resource and `terraform apply` writes to your cloud, so behold echoes them and leaves them yours to run. 6. **Done.** The card is marked chant-owned at the observe position, and `terraform import` reverses all of it. The Emit step reports **`chant lint`, not `chant build`**. chant#1637: `carve emit` folds the bucket's versioning and public-access-block sub-resources into the carve set but does not yet carry them as native props, so `build` fails two AWS policy rules on source the advisor scored 88. The step links the reason rather than letting a lint pass read as a build pass. behold writes only into the copy it just made, and only into `app/carveout/` inside it. Both steps refuse outside a demo copy; a plain `behold carve report.json` shows the same six steps with the runs greyed out and a reason. ## The strangler fig: what has actually graduated `chant carve emit` (chant ≥ 0.52.2) writes a state manifest beside the source it emits: `
.carve.json` in the `--output` directory. `carve bridge` adds its record to the same file, and `carve apply` adds the ownership marker. behold reads those manifests and never writes one, which is what makes the picture survive a restart: the progression lives on disk in chant's file. Three stages, and only the last of them is a graduation: - **emitted.** Typed chant source exists; the surviving Terraform still reads the resource directly. The card stays in its band, repainted in the in-flight blue with the words `emitted — not bridged`. - **bridged.** The survivor patch is proposed. Still banded, still Terraform's: `bridged — apply is yours`. - **applied.** `carve apply` recorded the ownership marker. The card leaves the Terraform bands and draws inside the chant project box, keeping its Terraform address, with `carved → chant` on it. The chant box's panel carries the count (`carved so far — 1 of 12 carved`) and the panel's **project** section lists each carved address with its stage. A resource that graduated in a previous session opens in the chant box; a partly-carved one opens back in its band, saying how far it got. Point `behold carve` at a report in a directory that also holds manifests and you get the same reading with no demo at all: the graduated pile becomes its own band above the ranking, since there is no chant member box to move a card into. An ordinary `behold serve ` whose directory carries a carveout surfaces the state in the same panel section, off `/api/project`. ### `carve apply` is not a button `carve apply` graduates ownership. behold renders what the manifest records about it and echoes the command, with no `--select`, because the manifest in the output directory already names the target: ```sh chant carve apply --from legacy-tf --output app/carveout --env prod --stack assets --write-source ``` There is no `/api/carve/apply` endpoint and no apply button anywhere in the UI. That is the same line the Handoff step draws around `terraform state rm`: behold triggers delegated work, it does not decide when an estate changes hands. ### The `--live` tier `npx behold demo carve --live` (docker + terraform on PATH) boots a scratch Floci — its own container name and port, refused if taken, deleted on exit — arms the estate's provider override into the demo copy, and really applies the starred resources into it. The advisor then reads a tfstate terraform wrote, and the Handoff step gains a read-only **terraform plan** button: run it after pasting `terraform state rm` and the carved resource is simply absent from the plan — `0 to destroy`, nothing blinked. The Emit step gains the observe beat: **watch chant read it live** runs `chant lifecycle diff --live` in the carveout against the scratch Floci, and the carved resource reads observed — `EXTERNAL`, ownership `foreign` — while Terraform still owns it (chant#1647's identity read path, chant ≥ 0.44.12). ## For agents ```sh curl localhost:4600/api/carve # the raw report, verbatim curl localhost:4600/api/graph # the same thing as {ir, svg, meta} # demo only (behold demo carve): the walkthrough's two safe steps curl -X POST localhost:4600/api/carve/emit -H 'content-type: application/json' -d '{"select":"aws_s3_bucket.assets"}' curl -X POST localhost:4600/api/carve/bridge -H 'content-type: application/json' -d '{"select":"aws_s3_bucket.assets"}' ``` `select` must name a resource the served report ranks; anything else is a 400. Outside a demo copy both POSTs answer 403 `{code: "read-only"}`, and on an ordinary project serve they aren't registered at all. `GET /api/project`'s `carve.demo.runnable` is how a client knows which it is. A file that isn't a peelability report gets a structured `{error, code: "carve-report", remedy}` — in the terminal before the server starts, and from the routes if it goes bad later. It never renders as a blank graph. ## The carved entity, on the chant side Since chant 0.54 a carve manifest records its emitted files relative to its own directory, and `chant carve status --from --json` is chant's own read over a project's manifests — behold uses it (the local walk stands in below that chant) and joins each manifest's emitted file to the chant entity that file declares. On the entity graph and the overlay that entity carries `carved:
` and the `carved → chant` mark; in the carve lens a graduated card takes on the kind, lexicon and declared attributes of the entity it became, while keeping its Terraform address as its id so the morph still glides. ## What is not here yet Boundary edges draw from the lists a chant ≥ 0.52 report carries (chant#1636); a report from an older chant has counts only, and the graph says so on its status line. The cross-environment question — is prod running the render staging tested — now has digests on both sides but no two-environment surface to ask it on. --- # Approving a gate Source: https://intentius.io/behold/using/approving/ A gate's card in behold has an approve button. behold does not write the approval itself: it runs `chant approve` in the member's directory, and chant records it on `chant/lifecycle`. chant never signs a person in, so behold tells chant who is approving with `--actor`. The button says who that is before you click it: | Button | What it means | |---|---| | **Approve as github:alice** | chant records `github:alice` | | **Approve as github:alice, signed** | the gate needs a signed approval, and behold signs it with alice's key | | no button, and the line to run | the gate needs a signed approval and behold has no key for you, or behold can't tell who you are | Hover the button to see the command behold runs and where the name came from. Every approval behold makes also carries `--note "approved in behold"`, so the ledger line says where it was made. ## Who you approve as behold learns who you are in one of three ways. The first that applies wins. | You run behold | Start it with | chant records | |---|---|---| | behind a proxy that signs people in through the forge or OIDC | `--identity-header X-Forwarded-User` | the person the proxy names, such as `github:alice` | | for yourself | `--as github:alice` | `github:alice` | | with neither | nothing | the user running behold, from `GITHUB_ACTOR`, `GITLAB_USER_LOGIN` or `USER` | ```sh npx @intentius/behold serve ./estate --as github:alice ``` `--as` takes a forge identity: `github:`, `gitlab:`, or `@:` for Forgejo, Gitea or a self-hosted GitHub or GitLab, such as `forgejo@codeberg.org:alice`. It also takes any principal your workspace's `.chant/allowed_signers` lists. `BEHOLD_APPROVER` sets the same thing, and so does the `approver` block in behold's per-user settings for the project (`/behold/settings/.json`, outside the project, beside the hand layout; `BEHOLD_CACHE_DIR` moves it): ```json { "approver": { "as": "github:alice", "signingKey": "~/.ssh/id_ed25519" } } ``` A project served before #470 that had this block in `.behold.json` has it copied into the settings on the first read, with relative key paths made absolute against the project. behold never writes or deletes `.behold.json`. A workspace whose `chant.workspace.json` sets `identity.attribution` to `identified` refuses a plain user name, so on such a workspace start behold with `--as` or behind a signing-in proxy. Without either, chant refuses the approval and the card shows chant's reason. ### Behind a proxy When a team opens one behold, put a proxy in front of it that signs people in (oauth2-proxy, or your forge's own), and tell behold which header names the person: ```sh npx @intentius/behold serve ./estate --identity-header X-Forwarded-User --identity-forge github ``` A bare login in the header, such as `alice`, is read as an account on `--identity-forge`: `github` unless you set it, `gitlab`, or `@`. A header that already holds a forge identity is taken as it is. A request without the header gets no button. behold trusts the header because the proxy sets it. Anyone who reaches behold's port directly could send it too, so `--identity-header` needs behold bound to `127.0.0.1` (the default) with the proxy on the same machine, and behold refuses to start with it on any other `--host`. Without `--identity-header`, behold refuses an approval from a request that carries `X-Forwarded-User` or a header like it, rather than recording someone other than the person it names. ## Signed gates A gate the workspace names in `identity.gates` counts only an approval signed with an ssh key that `.chant/allowed_signers` lists for the approver. behold signs such a gate when it holds your key: ```sh npx @intentius/behold serve ./estate --as github:alice --signing-key ~/.ssh/id_ed25519 ``` behold then runs `chant approve --plan --actor github:alice --note "approved in behold" --sign ~/.ssh/id_ed25519`, and chant checks the seal against the signers at base before it writes anything. `BEHOLD_SIGNING_KEY` sets the same key. Behind a proxy, list a key per person in the per-user settings: ```json { "approver": { "header": "x-forwarded-user", "keys": { "github:alice": "/srv/keys/alice", "github:bob": "/srv/keys/bob" } } } ``` The key file stays on the machine behold runs on. The page is told only whether behold holds a key for you. A key with a passphrase needs the ssh agent to hold it, since behold can't type the passphrase. Without a key, the card shows the `chant approve ... --sign` line to run at your own shell. The converge gate and the Op run gate on the Deploy tab record the same person with `--actor`, but behold does not sign them. ## What behold never approves A policy denial (a gate of op `policy-override`) needs the denied rules and your reason, so its card shows the command and no button. A framed behold leaves approving to the page that framed it, which knows who is looking. behold never approves a gate on its own, and auto-sync never approves one. --- # A change set on the graph Source: https://intentius.io/behold/using/change-set/ A gate that binds a change set (a pull request's gate, or a release gate planned for an environment) has the plan it approves stored on `chant/lifecycle`. behold draws that plan on the graph, so you can see what approving the gate lets chant create, change or delete before you click **Approve**. ## Opening it In the [Trunk tab](/behold/using/trunk/), a waiting gate's card says `change set 9cf74a6fee02` when chant stored one. Click it. The panel opens a **Change set** tab and the graph draws the marks. A release gate's card also says `release 1a23268fa8b1`, the release its change set was planned for. The tab's first lines name what you are looking at: - `change set 9cf74a6fee02 · gate web/release/promote in prod` - `commit a14610e · run run-9 · release 1a23268fa8b1 planned for prod` The commit is the one the run reached the gate at. Hover for the full digests and the `chant approve` line. ## The colours Each card the change set touches gets an outline and a tag above it naming what happens to it (`create`, `update`, `replace`, `2 create, delete`). The colours are the graph's own: | Outline | Legend row | What the change set does to the card | |---|---|---| | green (managed) | create | creates it | | yellow (foreign) | update or replace | updates it in place, or replaces it with a new object | | red (degraded) | delete or forget | deletes it, or forgets it (chant stops managing it and leaves the object standing) | A card with entries of several kinds takes the colour that destroys most: a delete over an update, an update over a create. The legend counts entries, not cards, and adds a row for entries that change nothing (`no change (not drawn)`). The fill of a card is untouched. It is still the live status, so a preview never reads as what is running. ## A card's entries Click a marked card. The inspect pane lists each entry on it under `change set 9cf74a6fee02 (preview, not applied)`: the action and address, the resource type and disruption, and the fields where the plan names them: - `FunctionName: before "cart-v1", after "cart-v2" (forces replacement)` - `Tags: before [...], after: known after apply` - `Environment: sensitive, not shown` ## What is not on the graph Some entries have no card. The Change set tab lists them: - Under `not on the graph`, chant mapped the entry to no graph node, for example a delete of a block the planning commit no longer declares. - Under `not in this view`, the entry's node exists, but the current view does not draw it (another lens, or a member not on screen). When a member failed to plan, or planned with addresses it could not read, the tab says so in red: the picture leaves out what that member would change. ## Turning it off **Turn off the overlay** removes the marks and keeps the change set in the tab; **Turn on the overlay** draws them again. **Read again** reads the gate and its change set from chant once more. Once the gate is approved, it no longer waits, and reading again says so. ## What it reads | chant command | What the overlay takes from it | |---|---| | `chant workspace status --json` | the waiting gate, the commit and run that reached it, and the digest of the change set it binds | | `chant change-set show --member --json` | the stored change set: each entry's address, action, fields and graph node | Both are read contract 1 documents, new in chant 0.124.28. On an older chant the tab says `Change set needs chant 0.124.28 or newer` with the project's version, and the rest of behold works. `behold doctor` lists the read with its floor. A component gate in a plain `chant run --components` binds a lifecycle plan, and chant stores no change set for it. Its card has no change set link. --- # Collision marks Source: https://intentius.io/behold/using/collisions/ Two people or two agents can work on the same resource under two different work items, and nothing in a work lease compares them. chant does: since chant 0.124.29, `chant workspace status --json` reports an `overlap` between the in-flight work items. behold draws it. It reports and nothing more. It locks no item and refuses no change; the people on the two items sort it out. ## What counts A work item is in flight unless it implements a decision and every decision it implements is confirmed. Its commits are the ones that carry a `Chant-Record: :` trailer, read from every local and remote-tracking branch. A node is marked when a waiting gate's change set has an entry on it, and a commit of each of two items changed the file that declares the node. ## The Collisions tab When behold serves a declared workspace, the panel has a **Collisions** tab. It reads from chant when you open it: - the nodes touched by two or more work items, with the items; - under **no graph node**, the files two items both changed, and any shared entry chant mapped to no node; - each in-flight work item with its state, record, commits and the items it collides with. ## On the graph Each marked card gets a dashed ring and a tag under it, `collision: W-001, W-002`. Hover the tag for the items and the entries behind it. Click the card and the inspect pane lists the colliding work. **Show only colliding work** hides every card that is not a marked node, and every edge with an unmarked end, and lists only the items that collide. **Show all work** brings them back. **Turn off the marks** removes the rings and tags. ## When there is nothing to show On a chant older than 0.124.29 the tab says which chant adds the read, and the graph draws no marks. If the workspace declares no work record kind, or chant could not read the work records or the history, the tab says so. --- # Feature flags on the graph Source: https://intentius.io/behold/using/flags/ A feature flag in a chant workspace is a record that names a member, the build parameter of that member it sets, an owner, the work item it hides and a value per environment ([chant's guide](https://intentius.io/chant/guide/feature-flags/)). Turning one on is a reviewed commit that changes the record, and the release cut from that commit carries the new value through the environment's gate. behold shows where each flag stands and never changes one. ## The Flags tab When behold serves a declared workspace, the panel has a **Flags** tab. It reads the flags from chant when you open it. Each flag shows: - its member and id, `web/new-checkout`, with the record's path on hover; - `param newCheckout · owner alice · work W-002 (in-progress)`; - its value in each environment, in promotion order: `staging on`, `prod off`. An environment the record leaves out reads `unset`, and builds with the parameter's default. A flag the stale-flag lint reports (`chant workspace check`, WSP118) says `due for removal (WSP118)` after its name. Hover for the work item's decisions: every one is confirmed, so the record and the parameter can go. ## On the graph Pick an environment under `on the graph in`. Every card of a flagged member gets a tag under it with the flag's value there: `new-checkout: on` in green, `new-checkout: off` in grey, a value that is not true or false in blue. A member with several flags reads `2 flags: 1 on`. A stale flag adds `stale` to the tag, in red. The tag's tooltip lists each flag. chant does not say which resources read a build parameter: the graph carries no parameter reads. So a flag marks every card of its member, and the tab says so. **Turn off the marks** removes the tags and keeps the tab. Click a card to see its member's flags in the inspect pane. ## Turning a flag on There is no toggle. A change to a flag is previewed as a change set: - When a release that moves the flag's parameter waits at an environment's gate, the flag says so, `newCheckout false to true waits at gate web/release/promote in staging`, with a `change set 05c8b35bc9a3` link. Click it to draw that change set on the graph, as the [Trunk tab's link](/behold/using/change-set/) does. A release's entry is the component, `web`, which is no node of the graph, so the Change set tab lists it under `not on the graph`. - Where the flag is off and nothing waits, the tab shows the lines that turn it on: a `chant workspace records amend` of the record's values, and the `git commit` of the record. Once that commit is green, a release cut from it and promoted to the environment waits at the environment's gate, and its change set opens from here. ## What it reads | chant command | What the tab takes from it | |---|---| | `chant workspace status --json` | each member's `featureFlags`, the promotion order, and in each environment the gates waiting there with the change set they bind | | `chant change-set show --member --json` | for a waiting gate of a flagged member, the entries whose `param:` attribute moves the flag's parameter | `featureFlags` is new in chant 0.124.29. On an older chant the tab says `Feature flags needs chant 0.124.29 or newer` with the project's version, the graph draws no flag marks, and the rest of behold works. `behold doctor` lists the read with its floor. --- # Running a CI job locally Source: https://intentius.io/behold/using/local-run/ When behold serves a chant workspace, the panel has a **Local** tab. It runs a pull request's plan job on this machine the way CI runs it, and shows the run next to CI's run of the same job at the same commit. behold starts: ```sh chant ci run plan --local --events-json --env --base --commit --pr --forge ``` chant runs the job in a fresh worktree of the commit, with the job's clean environment, and writes nothing beyond this machine: no push, no commit status, nothing under a report URL and nothing in the run index. Its record stays on your local `chant/lifecycle` with `runOrigin: {forge: "local"}`. A member's job runs in the member's directory, as CI runs it. ## Starting one Fill in the commit and its pull request's number (chant's plan job reads it as `PR_NUMBER`), or, when serve has [`--ci-report-url`](/behold/using/terragucci/#runs-from-chants-run-index), press **Run this job locally** on one of CI's plan jobs from chant's run index. | Field | Default | |---|---| | env | the env behold serves (`--env`) | | base | where the commit leaves the trunk (`git merge-base` with `origin/HEAD`, else `main` or `master`), or the commit's parent when it is on the trunk | | member | the workspace | | forge | the one workflow directory the checkout has (`.github`, `.gitlab-ci.yml`, `.forgejo`) | One command runs at a time, as for an Op run: a second start while one runs is refused. ## What it shows The run is drawn the way the kiosk's runs pane draws one: a job named after chant's Op (`pr-plan`), a step per phase of the run, each with chant's event lines and the step output chant tagged with it, and a last step with what chant printed outside a phase. The lines arrive as chant writes them. The verdict is the stream's own `run-settle`; a stream that ends without one reads `lost`. The heading says the base and env it ran with, and says so if chant's events do not name the run local. Beside it is CI's run of the same job at the same commit (and member), read from chant's run index with its log, when serve has `--ci-report-url`. Without it the CI side says there is no index to read. behold keeps the last 20 runs it started, in memory. chant keeps the record. ## Plan only `chant ci run apply --local` still changes the environment's real infrastructure and pushes its release records and gate facts. The job's clean environment keeps `HOME`, so your own cloud credentials would apply even when behold serves with `--local` against an emulator. behold never offers it, and `POST /api/chant/ci/local` answers an apply `422` with code `local-apply`. ## Where it is not offered The tab is not drawn, and the route refuses, in a public view (`--public`), an embed, a static export, preview mode, and a terragucci or SQL Yodeler repository. On a chant older than 0.124.32 the tab says `Local CI run needs chant 0.124.32 or newer` with the project's version, and starts nothing. ## Routes | Route | What it does | |---|---| | `GET /api/chant/ci/local` | what is offered, and the runs behold started | | `POST /api/chant/ci/local` | start one: `{job: "plan", commit, pr, env?, base?, member?, forge?}` | | `GET /api/chant/ci/local/` | one run, and CI's run of the same job at the same commit | ## Limits - Steps chant served from its step cache are not marked yet: that waits on chant's step cache (chant#4015). - The job runs with the default image's set-up lines, as in CI, so a machine without OpenTofu downloads it. --- # The three-cloud demo Source: https://intentius.io/behold/using/three-cloud/ One small website runs in three clouds: ECS on Fargate behind a load balancer in AWS, a Container App in Azure, and a Cloud Run service in GCP. Route 53 sits in front with one latency record per cloud, and each record is guarded by a health check on that cloud's endpoint. All of it runs on the [floci](https://github.com/lex00/floci) emulators in Docker, so no cloud account is involved. The demo answers one question: turn Azure off, and where does traffic go? ```sh npx @intentius/behold demo three-cloud ``` That copies `example-three-cloud` to `./behold-demos/three-cloud`, boots floci, floci-az and floci-gcp, and serves the workspace with the `local` environment picked. It needs Docker and pulls three emulator images the first time. ## What you are looking at The estate is a chant workspace of four members, drawn as four boxes: | member | what it declares | |---|---| | `dns` | the hosted zone, three health checks, three latency CNAMEs, and an A record naming the GCP load balancer | | `aws` | a VPC (`VpcDefault`: public and private subnets in two zones, internet and NAT gateways) and the `FargateAlb` composite: the service, its task, the load balancer | | `azure` | a Log Analytics workspace, a VNet with a subnet for the Container Apps environment, the environment, a user-assigned identity, and the app with external ingress | | `gcp` | a custom VPC and a subnet for Cloud Run's egress, and the Cloud Run service (public, running as its own service account) behind a global external Application Load Balancer: address, forwarding rule, HTTP proxy, URL map, backend service, serverless NEG | Each cloud publishes its endpoint with `output()` (`awsEndpoint`, `azureEndpoint`, `gcpEndpoint`), and `dns` declares a parameter of the same name. That shared name is the edge behold draws from each record's endpoint to the cloud that answers it. Config Connector YAML has no outputs, so `chant build` warns that the gcp member's output is not written; the edge is still drawn, and the Op below reads the load balancer's IP from the emulator and hands it to `dns`. ## Turning Azure off The `front-door` Op, in `dns/ops/front-door.op.ts`, rebuilds the whole picture: 1. Apply gcp, azure, aws and dns to the emulators, then wait until the Azure health check reads healthy. 2. Deactivate the Azure app's active revision, so its endpoint answers 503. 3. Wait until `chant lifecycle health` reads the Azure check unhealthy. 4. Export the page. Run it from the Deploy tab, or from the copy: ```sh cd behold-demos/three-cloud/dns && npx chant run front-door # or, the same steps without chant's runner, from the workspace root npm run front-door ``` Afterwards the Azure check is unhealthy, the Azure record is dimmed and reads `out of rotation: azureCheck unhealthy`, and the AWS and GCP records stay in rotation. Route 53 answers with what is left. The marks are the ones [Health and rotation](/behold/using/reading-the-graph/#health-and-rotation) describes. The emulators' checkers run inside floci's container, where a floci-az or floci-gcp hostname resolves to nothing. The Op therefore gives each deployed health check the emulator container's address and port, and the checkers send the endpoint as the `Host` header, as Route 53 does for a check with an `IPAddress`. The source declares the checks as they would be on AWS, without either field. floci runs each load balancer listener on the listener's own port in its container, so the AWS check is given port 80 rather than floci's API port. ## The exported page The Op's last step is a [static export](/behold/using/export/) into `export/`: ```sh behold export . --open-env local --open-zoom logical \ --note "Azure was switched off: its app answers 503, Route 53's check on it fails, and AWS and GCP take the traffic. Run this yourself: \`npx @intentius/behold demo three-cloud\`" --out export ``` It opens on the `local` environment at the logical zoom, which draws each record, the service it fronts and whether it is in rotation, and ends with the line that brings a reader back here. To rebuild the page from nothing, boot the emulators and run the Op: ```sh npm run emulators && npm run front-door ``` ## Cleaning up Stopping `behold demo` stops the three emulators. floci-gcp leaves the Cloud Run container it started behind; `docker ps -a --filter name=floci-gcp-cloudrun` lists it. --- # The trunk lane Source: https://intentius.io/behold/using/trunk/ When behold serves a chant workspace, the panel has a **Trunk** tab. It answers "what is in flight, and where is it?" without a pull request list or a forge page. behold reads it from chant when you open the tab, and again when you press **Read again**. It keeps nothing of its own. ## What it shows At the top, the trunk: the first-parent commits of the branch your workspace's `ci.green` names, newest first, within its window. Each commit says: | Part | What it means | |---|---| | `a14610e green` | every required phase passed, and `chant ci tick` tagged it `ci/green/` | | `9c4250f revoked` | it was green and a required phase failed later; the line names the phase | | `0c7c85a undecided` | no ci tag yet | | `runs in staging`, `waits in prod` | where the commit is now: an environment runs a release cut from it, or a gate there waits on it | | `work task:12` | the work item its `Chant-Record` or `Chant-Lease` trailer names | | `no work item` | no trailer names one | | `names a work item that does not exist` | a trailer names one that isn't there; hover for which | | `test` | a link to each run behind the state | Then one lane per environment, in the order your members' `chant.config.ts` lists their `environments` (a member's promotion order). Each lane says what the environment runs: - `runs v1, cut from 6828a7f · web`: the release labelled `v1`, cut from that commit, which the latest deploy record of `web` names. Hover for who cut it and who approved the deploy. - `runs web 4b5e57f6eb2f from 6828a7f`: a deploy record that names no release. - `no deploy record`: chant has none for the environment, so behold does not guess what runs there. A project with no components can cut releases that no deploy record names, and they show this way. Below that, each gate waiting in the environment, as a card: the commit it waits on and that commit's state, the approvals it has and needs, the run that reached it, and the change set it binds. Hover the card's facts for the `chant approve` line and `chant change-set show `. The change set is a link: click it to draw the change set on the graph (see [A change set on the graph](/behold/using/change-set/)). ## Approving from the lane A waiting gate's card has the same button as the gate strip, through the same route, so it says who chant records before you click: **Approve as github:alice**. See [Approving a gate](/behold/using/approving/) for where that name comes from and how a signed gate is signed. After the approval the lane is read again, and the gate leaves it once chant counts the approval. chant records the channel an approval came through, and it has no channel for behold. An approval made from the lane shows `channel: "cli"` in `chant workspace status`, with the note `approved in behold` on the ledger line. The principal is the person, as above. ## What it reads | chant command | What the lane takes from it | |---|---| | `chant ci list --json` | the commits, their state, phases, runs and work items | | `chant workspace status --json`, once per environment | each member's promotion order, the gates waiting there with their commit, run and change set, and the deploy records | | `chant release list --json`, in each member that declares environments | each release, the commit it was cut from, and the environments that run it | These are read contract 1 documents, and they are new in chant 0.124.28. On an older chant the tab says `Trunk commits needs chant 0.124.28 or newer` with the project's version, and the rest of behold works. `behold doctor` lists the three reads with the floor each needs. ## Limits - `chant ci list` reads the checkout's own tags and never asks the forge. A commit that failed without ever going green has no tag, so it reads `undecided`. Fetch tags (`git fetch --tags`) to see what CI decided. - A workspace with no `ci.green` declaration has no trunk to list: the tab says `trunk not read` with chant's reason, and still draws the environments. - A gate that records no environment, such as an Op's gate, is not a step of the promotion. It stays in the gate strip and is not in the lane. - A workspace whose members declare no environments has no promotion order. The lane then shows the one environment behold serves (`--env`), and says so. --- # behold on a chant workspace Source: https://intentius.io/behold/for/workspace/ ## Optional: hand this page to your coding agent ```text Read https://intentius.io/behold/for/workspace/. Serve this directory with `npx @intentius/behold serve .` and tell me the URL. Name each member that draws as one card, and the reason code on it. If it does not serve, show me what `npx @intentius/behold doctor .` reports. Never deploy with the Deploy button, `POST /api/apply` or `POST /api/ci/dispatch`. Gate approvals stay mine as well (`/api/ops/:name/signal/:gate`, `/api/workspace/gates/approve`, `/api/operator/approve/:op/:gate`). Op runs (`/api/ops/:name/run`) and rollbacks (`/api/rollback`) are mine too. Leave `--auto-sync` off, and keep a `behold preview` or `--public` serve read-only. ``` Serve a directory whose chant.workspace.json lists its members and see every member in one graph. One chant project on its own works the same way. ## Works - `behold serve ./estate` reads the member list through `chant workspace ls --json` - A box per member: resources for chant and Terraform members, instances for choudoufu - A member chant cannot read draws as one card that names the reason code - `behold doctor ./estate --fix` turns a `.behold.json` member list into a `chant.workspace.json` ## Needs - A chant install the workspace root resolves, or behold's own - Read credentials for the environment that `--env` names ## First step To try it first, `npx @intentius/behold demo argo-estate` serves a declared workspace without Docker. ```sh npx @intentius/behold serve ./estate ``` Next: [Serve a chant workspace](https://intentius.io/behold/using/workspace/). ## Proof - [example-argo-estate](https://github.com/INTENTIUS/behold/tree/main/example-argo-estate) - [example-flux-estate](https://github.com/INTENTIUS/behold/tree/main/example-flux-estate) - [e2e/workbench-e2e.sh](https://github.com/INTENTIUS/behold/tree/main/e2e/workbench-e2e.sh) ## Then read ### Tasks - [Serve a chant workspace](https://intentius.io/behold/using/workspace/) - [Open your own project](https://intentius.io/behold/start/your-project/) ### Background - [Reading the graph](https://intentius.io/behold/using/reading-the-graph/) - [What behold is](https://intentius.io/behold/start/what-it-is/) ### Reference - [behold doctor](https://intentius.io/behold/start/your-project/#if-it-doesnt-look-right-behold-doctor) - [Converting a member list](https://intentius.io/behold/using/workspace/#converting-a-beholdjson-member-list) --- # behold on a Terraform estate Source: https://intentius.io/behold/for/terraform/ ## Optional: hand this page to your coding agent ```text Read https://intentius.io/behold/for/terraform/. Serve this repository's Terraform with `npx @intentius/behold serve ` and tell me the URL. List the roots it found and each directory it skipped, and why. Never deploy with the Deploy button, `POST /api/apply` or `POST /api/ci/dispatch`. Gate approvals stay mine as well (`/api/ops/:name/signal/:gate`, `/api/workspace/gates/approve`, `/api/operator/approve/:op/:gate`). Op runs (`/api/ops/:name/run`) and rollbacks (`/api/rollback`) are mine too. Leave `--auto-sync` off, and keep a `behold preview` or `--public` serve read-only. ``` Draw a directory of Terraform roots straight from the checkout. Each root is a box of resource cards, and the references the configuration states are its edges. ## Works - A probe finds the roots and names each skipped directory - Module calls open into their contents, joined by the references chant's Terraform lexicon resolves - Outputs and variables join the cards at the attributes zoom - A read across two roots stays on its card as text ## Needs - Only the checkout: behold reads the `.tf` files without a cloud account or a `terraform` binary ## First step ```sh npx @intentius/behold serve ./infra ``` Next: [Look at a Terraform estate](https://intentius.io/behold/using/terraform/). ## Proof - [example-terraform-estate](https://github.com/INTENTIUS/behold/tree/main/example-terraform-estate) ## Then read ### Tasks - [Look at a Terraform estate](https://intentius.io/behold/using/terraform/) - [Look at a Terraform carve-out](https://intentius.io/behold/using/carve/) ### Background - [Roots and skips](https://intentius.io/behold/using/terraform/#roots-are-discovered-and-the-skips-are-reported) - [What is a card](https://intentius.io/behold/using/terraform/#what-is-a-card) ### Reference - [Export a snapshot](https://intentius.io/behold/using/export/) - [Driving it from an agent](https://intentius.io/behold/using/agents/) - [The live Terraform demo](https://demo.intentius.io/terraform) --- # behold on a terragucci estate Source: https://intentius.io/behold/for/terragucci/ ## Optional: hand this page to your coding agent ```text Read https://intentius.io/behold/for/terragucci/. Ask me where the reports bucket is. Serve this repository with `npx @intentius/behold serve . --terragucci ` and tell me the URL. List the roots that carry a mark and the date on each. Never deploy with the Deploy button, `POST /api/apply` or `POST /api/ci/dispatch`. Gate approvals stay mine as well (`/api/ops/:name/signal/:gate`, `/api/workspace/gates/approve`, `/api/operator/approve/:op/:gate`). Op runs (`/api/ops/:name/run`) and rollbacks (`/api/rollback`) are mine too. Leave `--auto-sync` off, and keep a `behold preview` or `--public` serve read-only. ``` Serve a repo that terragucci runs and see every root in one graph. Each card carries what the newest run found about it. ## Works - A dated mark on each card the newest drift check or plan names - Each mark links to its run's report - A timeline from the audit record - The waves that wait for an approval - Gates and locks read from `chant/lifecycle` - Apply progress per resource from the run view - A view to publish beside terragucci's estate page ## Needs - Read access to the reports bucket through your `aws` profile, or a local sync of it - The repo's checkout for the graph ## First step ```sh npx @intentius/behold serve . --terragucci s3://acme-terragucci-reports ``` Next: [Look at a terragucci estate](https://intentius.io/behold/using/terragucci/). ## Proof - [example-terragucci-reports](https://github.com/INTENTIUS/behold/tree/main/example-terragucci-reports) - [e2e/terragucci-e2e.sh](https://github.com/INTENTIUS/behold/tree/main/e2e/terragucci-e2e.sh) ## Then read ### Tasks - [Watch the live demo](https://demo.intentius.io/kiosk) - [Look at a terragucci estate](https://intentius.io/behold/using/terragucci/) - [Publish a view](https://intentius.io/behold/using/terragucci/#publish-a-view-beside-the-estate-page) - [Export a terragucci estate](https://intentius.io/behold/using/export/#a-terragucci-estate) ### Background - [What a mark means](https://intentius.io/behold/using/terragucci/#what-a-mark-means) - [Waiting waves](https://intentius.io/behold/using/terragucci/#waiting-waves) ### Reference - [The reports bucket](https://intentius.io/terragucci/reference/reports-bucket/) - [terragucci docs](https://intentius.io/terragucci/) --- # behold on a SQL Yodeler project Source: https://intentius.io/behold/for/sql-yodeler/ ## Optional: hand this page to your coding agent ```text Read https://intentius.io/behold/for/sql-yodeler/. Serve this SQL Yodeler project with `npx @intentius/behold serve .` and tell me the URL. List the environments the Releases tab shows. For each gate that waits, show me the `yodel approve` line to run myself. Never deploy with the Deploy button, `POST /api/apply` or `POST /api/ci/dispatch`. Gate approvals stay mine as well (`/api/ops/:name/signal/:gate`, `/api/workspace/gates/approve`, `/api/operator/approve/:op/:gate`). Op runs (`/api/ops/:name/run`) and rollbacks (`/api/rollback`) are mine too. Leave `--auto-sync` off, and keep a `behold preview` or `--public` serve read-only. ``` Draw the schema a SQL Yodeler project declares. What each environment holds comes from the files yodel wrote and the `chant/lifecycle` branch. ## Works - Tables and indexes as cards named by kind, joined by each `REFERENCES` - The Pipeline tab: each wave's gate and the plan digest it is bound to - Who approved each gate, and how - The Releases tab: each environment's newest release per component - Drift files as dated corner tags on the cards they name ## Needs - The project's checkout, with `chant/lifecycle` on its origin - Report and drift JSON from `yodel` for the SQL Yodeler tab ## First step ```sh yodel report --json > out/report.json yodel drift prod --json > out/drift-prod.json npx @intentius/behold serve . --yodel-report out/ ``` Next: [Look at a SQL Yodeler project](https://intentius.io/behold/using/sql-yodeler/). ## Proof - [e2e/fixtures/sql-yodeler-fk](https://github.com/INTENTIUS/behold/tree/main/e2e/fixtures/sql-yodeler-fk) ## Then read ### Tasks - [Look at a SQL Yodeler project](https://intentius.io/behold/using/sql-yodeler/) - [Report and drift](https://intentius.io/behold/using/sql-yodeler/#report-and-drift) ### Background - [The pipeline](https://intentius.io/behold/using/sql-yodeler/#the-pipeline) - [Releases](https://intentius.io/behold/using/sql-yodeler/#releases) ### Reference - [Policy denials](https://intentius.io/behold/using/workspace/#policy-denials) - [SQL Yodeler docs](https://intentius.io/sql-yodeler/) --- # behold on a choudoufu estate Source: https://intentius.io/behold/for/choudoufu/ ## Optional: hand this page to your coding agent ```text Read https://intentius.io/behold/for/choudoufu/. Start `npx @intentius/behold demo choudoufu-estate`, or serve this estate with `npx @intentius/behold serve . --env `. Tell me the URL and list the cards that read unowned. Copy out each move preview for me to run. Never deploy with the Deploy button, `POST /api/apply` or `POST /api/ci/dispatch`. Gate approvals stay mine as well (`/api/ops/:name/signal/:gate`, `/api/workspace/gates/approve`, `/api/operator/approve/:op/:gate`). Op runs (`/api/ops/:name/run`) and rollbacks (`/api/rollback`) are mine too. Leave `--auto-sync` off, and keep a `behold preview` or `--public` serve read-only. ``` choudoufu manages estates on AWS, GCP, Azure and Kubernetes and keeps ownership in marker tags on the live resources. behold reads those tags, so a green card means the resource is ours. ## Works - Each instance in choudoufu's own words for ownership - Attribute drift on request, through Re-check live with plan in the command palette - Adoptable cards that show what the sweep matched on next to choudoufu's adopt command - Moves previewed with `live-mv -json -dry-run` and ready to copy ## Needs - Credentials that read the estate's account, for `--env` - `TOFU_LIVE_COLLECT_UNCLAIMED=1` in `spawnEnv` to sweep the whole account ## First step The demo needs Docker and choudoufu installed. ```sh npx @intentius/behold demo choudoufu-estate ``` Next: [Look at a choudoufu estate](https://intentius.io/behold/using/choudoufu/). ## Proof - [example-choudoufu-estate](https://github.com/INTENTIUS/behold/tree/main/example-choudoufu-estate) - [e2e/choudoufu-estate-floci-e2e.sh](https://github.com/INTENTIUS/behold/tree/main/e2e/choudoufu-estate-floci-e2e.sh) ## Then read ### Tasks - [Look at a choudoufu estate](https://intentius.io/behold/using/choudoufu/) - [Serve a chant workspace](https://intentius.io/behold/using/workspace/) ### Background - [A green card means ours](https://intentius.io/behold/using/choudoufu/#a-green-card-means-this-is-ours) - [Unowned, and adoptable](https://intentius.io/behold/using/choudoufu/#unowned-and-adoptable) ### Reference - [Splits are previewed](https://intentius.io/behold/using/choudoufu/#splits-are-previewed-never-run) - [choudoufu on GitHub](https://github.com/INTENTIUS/choudoufu) - [choudoufu in the live demo](https://demo.intentius.io/choudoufu) --- # behold on a Terraform carve-out Source: https://intentius.io/behold/for/carve/ ## Optional: hand this page to your coding agent ```text Read https://intentius.io/behold/for/carve/. Run `chant carve advise --from --report carve.json`. Then serve the report with `npx behold carve carve.json` and tell me the URL. List the resources in the carve now band, and hand me the runbook commands to run myself. Never deploy with the Deploy button, `POST /api/apply` or `POST /api/ci/dispatch`. Gate approvals stay mine as well (`/api/ops/:name/signal/:gate`, `/api/workspace/gates/approve`, `/api/operator/approve/:op/:gate`). Op runs (`/api/ops/:name/run`) and rollbacks (`/api/rollback`) are mine too. Leave `--auto-sync` off, and keep a `behold preview` or `--public` serve read-only. ``` Move resources from Terraform to chant one at a time. behold draws chant's peelability report and ranks each resource by what carving it costs. ## Works - Cards banded by verdict, with carve now at the top - The score arithmetic behind each rank in the inspect pane - `behold demo carve` walks a half-migrated estate step by step - Each carved resource's stage, read from chant's carve manifests ## Needs - A peelability report from `chant carve advise` - Docker and `terraform` on PATH for the demo's `--live` tier ## First step ```sh chant carve advise --from ./terraform --report carve.json npx behold carve carve.json ``` Next: [Look at a Terraform carve-out](https://intentius.io/behold/using/carve/). ## Proof - [example-carve](https://github.com/INTENTIUS/behold/tree/main/example-carve) ## Then read ### Tasks - [Look at a Terraform carve-out](https://intentius.io/behold/using/carve/) - [The walkthrough](https://intentius.io/behold/using/carve/#the-walkthrough-behold-demo-carve) ### Background - [The arithmetic behind a rank](https://intentius.io/behold/using/carve/#the-arithmetic-behind-a-rank) - [What has graduated](https://intentius.io/behold/using/carve/#the-strangler-fig-what-has-actually-graduated) ### Reference - [Look at a Terraform estate](https://intentius.io/behold/using/terraform/) - [For agents](https://intentius.io/behold/using/carve/#for-agents) --- # behold with no project yet Source: https://intentius.io/behold/for/demo/ ## Optional: hand this page to your coding agent ```text Read https://intentius.io/behold/for/demo/. Start `npx @intentius/behold demo` and tell me the URL. Walk me through Deploy on the control panel so I watch the bucket turn green. When Docker is not running, say so. Never deploy with the Deploy button, `POST /api/apply` or `POST /api/ci/dispatch`. Gate approvals stay mine as well (`/api/ops/:name/signal/:gate`, `/api/workspace/gates/approve`, `/api/operator/approve/:op/:gate`). Op runs (`/api/ops/:name/run`) and rollbacks (`/api/rollback`) are mine too. Leave `--auto-sync` off, and keep a `behold preview` or `--public` serve read-only. ``` The bundled demo deploys an S3 bucket to a local emulator. Its cards turn green once behold observes it. To watch behold first, open the live demo, which needs no install. ## Works - An S3 bucket and its TLS policy on a local Floci emulator - Deploy on the control panel streams each phase as it runs - `behold demo k8s` runs an nginx Deployment on a throwaway k3d cluster - `behold demo --list` names the rest of the catalog ## Needs - Docker ## First step ```sh npx @intentius/behold demo ``` Next: [Run it in five minutes](https://intentius.io/behold/start/run-it/). ## Proof - [example-writes](https://github.com/INTENTIUS/behold/tree/main/example-writes) - [example-k8s](https://github.com/INTENTIUS/behold/tree/main/example-k8s) ## Then read ### Tasks - [Watch the live demo](https://demo.intentius.io/kiosk) - [Run it in five minutes](https://intentius.io/behold/start/run-it/) - [Open your own project](https://intentius.io/behold/start/your-project/) ### Background - [What behold is](https://intentius.io/behold/start/what-it-is/) - [Reading the graph](https://intentius.io/behold/using/reading-the-graph/) ### Reference - [The Kubernetes demo](https://intentius.io/behold/start/run-it/#the-kubernetes-demo-and-three-things-only-k8s-can-show) - [Export a snapshot](https://intentius.io/behold/using/export/) --- # Working with a coding agent Source: https://intentius.io/behold/for/agents/ ## Optional: hand this page to your coding agent ```text Read https://intentius.io/behold/for/agents/. Serve this project with `npx @intentius/behold preview .`. Fetch `GET /api` and `GET /api/graph`, then tell me what the graph holds. Never deploy with the Deploy button, `POST /api/apply` or `POST /api/ci/dispatch`. Gate approvals stay mine as well (`/api/ops/:name/signal/:gate`, `/api/workspace/gates/approve`, `/api/operator/approve/:op/:gate`). Op runs (`/api/ops/:name/run`) and rollbacks (`/api/rollback`) are mine too. Leave `--auto-sync` off, and keep a `behold preview` or `--public` serve read-only. ``` Hand behold to a coding agent. Every pane is plain JSON over HTTP, and each write stays with you. ## Works - `GET /api` lists every route and what it does - `/api/graph` returns the graph as `{ ir, svg, meta }` with drift status on each node - A failed precondition answers `{code, error, remedy}`, so an agent branches on `code` - `behold preview` and `serve --public` refuse every write ## Needs - An agent that can run commands - Read-only credentials in the shell that starts behold, when nothing should be applied ## First step Hand the agent this prompt: ```text Clone https://github.com/INTENTIUS/behold and get me looking at a live infrastructure graph in my browser. I want the bundled demo. It needs Docker and no cloud account. Install what it needs and start it, then tell me the URL. Walk me through what I am looking at: what the colours mean, and how I make a resource change colour while I watch. When Docker is not running, say so. Show me the source graph on its own instead. For anything past the demo, read https://intentius.io/behold/llms.txt and https://intentius.io/behold/using/agents/. Never deploy with the Deploy button, `POST /api/apply` or `POST /api/ci/dispatch`. Gate approvals stay mine as well (`/api/ops/:name/signal/:gate`, `/api/workspace/gates/approve`, `/api/operator/approve/:op/:gate`). Op runs (`/api/ops/:name/run`) and rollbacks (`/api/rollback`) are mine too. Leave `--auto-sync` off, and keep a `behold preview` or `--public` serve read-only. ``` Next: [Driving it from an agent](https://intentius.io/behold/using/agents/). ## Proof - [AGENTS.md](https://github.com/INTENTIUS/behold/tree/main/AGENTS.md) ## Then read ### Tasks - [Driving it from an agent](https://intentius.io/behold/using/agents/) - [Run it in five minutes](https://intentius.io/behold/start/run-it/) ### Background - [The read loop](https://intentius.io/behold/using/agents/#the-read-loop) - [Read-only core, delegated writes](https://intentius.io/behold/start/what-it-is/#read-only-core-delegated-writes) ### Reference - [The contract](https://intentius.io/behold/using/agents/#the-contract) - [AGENTS.md](https://github.com/INTENTIUS/behold/blob/main/AGENTS.md) --- # Running behold for a platform team Source: https://intentius.io/behold/for/platform/ ## Optional: hand this page to your coding agent ```text Read https://intentius.io/behold/for/platform/. Serve this estate with `npx @intentius/behold serve . --env --poll 30` and tell me the URL. List the nodes it reports as unobserved. Name the Ops it offers so I can decide on them. Never deploy with the Deploy button, `POST /api/apply` or `POST /api/ci/dispatch`. Gate approvals stay mine as well (`/api/ops/:name/signal/:gate`, `/api/workspace/gates/approve`, `/api/operator/approve/:op/:gate`). Op runs (`/api/ops/:name/run`) and rollbacks (`/api/rollback`) are mine too. Leave `--auto-sync` off, and keep a `behold preview` or `--public` serve read-only. ``` Give a platform team one graph per estate across substrates. Deploys go through the Ops and forges the team already runs. ## Works - Each substrate in the graph keeps its own meaning of deployed - An environment designated to a forge deploys through that forge's workflow and your `gh` - Auto-sync routes drift to the Op that owns its substrate - `behold export` writes a static snapshot any host serves ## Needs - Read roles for every substrate the estate spans - Write roles only where Deploy runs from behold ## First step ```sh npx @intentius/behold serve . --env prod --poll 30 ``` Next: [Acting on it](https://intentius.io/behold/using/acting/). ## Proof - [example-flux-estate](https://github.com/INTENTIUS/behold/tree/main/example-flux-estate) - [example-argo-estate](https://github.com/INTENTIUS/behold/tree/main/example-argo-estate) - [e2e/ci-executor-github-e2e.sh](https://github.com/INTENTIUS/behold/tree/main/e2e/ci-executor-github-e2e.sh) ## Then read ### Tasks - [Acting on it](https://intentius.io/behold/using/acting/) - [Deploying through a forge](https://intentius.io/behold/using/acting/#deploying-through-a-forge) - [Export a snapshot](https://intentius.io/behold/using/export/) ### Background - [The mixed estate](https://intentius.io/behold/start/what-it-is/#the-mixed-estate-is-the-point) - [Auto-sync](https://intentius.io/behold/using/acting/#auto-sync) ### Reference - [Reading the graph](https://intentius.io/behold/using/reading-the-graph/) - [Serve a chant workspace](https://intentius.io/behold/using/workspace/) --- # Security review Source: https://intentius.io/behold/for/security/ ## Optional: hand this page to your coding agent ```text Read https://intentius.io/behold/for/security/. Run `npx @intentius/behold doctor . --json` in this project and summarise it. List the Ops this project commits so I can review which ones behold would offer. Never deploy with the Deploy button, `POST /api/apply` or `POST /api/ci/dispatch`. Gate approvals stay mine as well (`/api/ops/:name/signal/:gate`, `/api/workspace/gates/approve`, `/api/operator/approve/:op/:gate`). Op runs (`/api/ops/:name/run`) and rollbacks (`/api/rollback`) are mine too. Leave `--auto-sync` off, and keep a `behold preview` or `--public` serve read-only. ``` Review what behold reads and where each of its writes runs. ## Works - Every view is a read and needs read roles only - Each write starts a committed Op or chant's local executor, on the credentials behold was started with - A terragucci or SQL Yodeler repo refuses every write behold could start - One delegated write runs at a time, and auto-sync leaves every gate for a person - An export cuts each absolute path down to its directory name ## Needs - The credentials of the shell that starts behold, which set what Deploy can change ## First step ```sh npx @intentius/behold doctor . --json ``` Next: [Read-only core, delegated writes](https://intentius.io/behold/start/what-it-is/#read-only-core-delegated-writes). ## Proof - [AGENTS.md](https://github.com/INTENTIUS/behold/tree/main/AGENTS.md) - [e2e/ci-executor-github-e2e.sh](https://github.com/INTENTIUS/behold/tree/main/e2e/ci-executor-github-e2e.sh) ## Then read ### Tasks - [Acting on it](https://intentius.io/behold/using/acting/) - [Export a snapshot](https://intentius.io/behold/using/export/#what-it-leaves-out) ### Background - [Read-only core, delegated writes](https://intentius.io/behold/start/what-it-is/#read-only-core-delegated-writes) - [Gates](https://intentius.io/behold/using/acting/#gates) ### Reference - [The act loop](https://intentius.io/behold/using/agents/#the-act-loop-delegated-and-never-direct) - [behold doctor](https://intentius.io/behold/start/your-project/#if-it-doesnt-look-right-behold-doctor) --- # Evaluating behold Source: https://intentius.io/behold/for/evaluate/ ## Optional: hand this page to your coding agent ```text Read https://intentius.io/behold/for/evaluate/. Start `npx @intentius/behold demo` and tell me the URL. Then run `npx @intentius/behold demo --list` and tell me which demos run on this machine. Never deploy with the Deploy button, `POST /api/apply` or `POST /api/ci/dispatch`. Gate approvals stay mine as well (`/api/ops/:name/signal/:gate`, `/api/workspace/gates/approve`, `/api/operator/approve/:op/:gate`). Op runs (`/api/ops/:name/run`) and rollbacks (`/api/rollback`) are mine too. Leave `--auto-sync` off, and keep a `behold preview` or `--public` serve read-only. ``` Try the demo on a local emulator first. Then draw your own project's source graph with nothing set up. ## Works - The demo turns a bucket green on a local emulator without a cloud account - `behold preview` draws any chant project's declared graph with no config - More demos for Kubernetes and Terraform estates, named by `behold demo --list` - `behold doctor` names what each project still needs ## Needs - Docker for the demo - A chant project for `behold preview` ## First step ```sh npx @intentius/behold demo ``` Next: [Run it in five minutes](https://intentius.io/behold/start/run-it/). ## Proof - [example-writes](https://github.com/INTENTIUS/behold/tree/main/example-writes) - [example-terraform-estate](https://github.com/INTENTIUS/behold/tree/main/example-terraform-estate) - [example-argo-estate](https://github.com/INTENTIUS/behold/tree/main/example-argo-estate) ## Then read ### Tasks - [Run it in five minutes](https://intentius.io/behold/start/run-it/) - [Open your own project](https://intentius.io/behold/start/your-project/) ### Background - [What behold is](https://intentius.io/behold/start/what-it-is/) - [Reading the graph](https://intentius.io/behold/using/reading-the-graph/) ### Reference - [Driving it from an agent](https://intentius.io/behold/using/agents/) - [behold on GitHub](https://github.com/INTENTIUS/behold)