A live control plane on chant. Every substrate in one graph, coloured by drift. Then act through delegated, gated Ops.
📖 Documentation starts with an agent prompt that gets you to a running graph without reading anything else first.
Where Argo CD shows one cluster's tree, behold shows the mixed-substrate estate: cloud drift on AWS, supply-chain drift on GitHub Actions, artifact presence on Helm. Each substrate's own kind of truth, in one picture.
chant source ──build/lint──▶ graph IR ──behold──▶ live graph + drift + (delegated) actions
(deterministic) (server + browser)
No chant project yet? The bundled demo is the five-minute path: an S3 bucket and policy against a local emulator. Needs Docker, and nothing else.
npx @intentius/behold demo # copies the example to ./behold-demo, installs, serves
# → http://localhost:4600. Blue = declared; click Deploy, watch it turn greenThe copied project is yours: edit its source and watch the graph change live.
There's a whole catalog, and behold demo --list names the rest (behold demo k8s stands the same loop up on a throwaway k3d cluster: runtime Pods, field
ownership; behold demo argo-estate needs nothing at all, being a three-project
Argo CD estate, declared only, so it runs where Docker doesn't; behold demo choudoufu-estate is four tag-owned OpenTofu estates served composed, where a
card's colour is ownership (bound, unowned, pending) rather than "nothing
changed": a resource whose attributes drifted out of band still carries its
markers and is still green, so attribute drift is a separate read you ask for
with ⌘K → "Re-check live with plan"; behold demo carve is the Terraform peel
walkthrough, below). Two more need no Docker either: behold demo terraform-estate draws a multi-root Terraform estate boxed by root, and shows
what behold refuses to infer, since one root reads a key the other declares and
no edge is drawn between them; behold demo flux-estate is a GitOps estate on
k3d, a Flux control plane plus two app projects composed. behold demo fountain
clones a real self-hosted app onto a throwaway cluster. Every loaded demo lands
in the panel's recents, so switching between them is the Scope tab.
Working in a checkout rather than an npm install? There is a second catalog,
workbench.json, which is this checkout's and is not shipped. It is
deliberately absent from package.json's files, so an npm install has
demos.json and nothing else. It holds the eleven internal estates behold is
developed against, named by relative path from this repo's root, so an entry
whose sibling checkout you don't have says so in --list instead of failing
halfway: chant's own two examples and water park's access/ Terraform roots
served in place, ../fountain-ops on its own k3d cluster, the live-mv
workbench's four estates, three estate-gen cohorts, and the generated
terralith at scale 1 and 4, greenfield and adopted from a stock terraform apply by a live-import you run by hand. just example terralith-4 serves
one (just example name="<entry>"); each that boots a substrate brings up its
own scratch emulator and leaves a scripts/down.sh in the target that removes
it. just e2e-workbench loads the whole catalog and asserts each graph.
The catalog is in the panel too (#268): the Scope tab's switcher lists every bundled demo under your recents, one click from any served project. Demos whose prerequisites are missing stay visible and disabled, saying what to install; one that would clone from the network says so on the button.
Already have a chant project?
cd my-chant-project
npx @intentius/behold doctor # will this project serve well? (read-only)
npx @intentius/behold preview # → http://localhost:4600, this project's graph
npx @intentius/behold serve . --env prod --poll 30 # live drift overlaybehold doctor is the first thing to run on a project behold hasn't seen: one
line each for the project's kind, its own chant install and version, declared
lexicons, which of them can be aimed at an emulator, the envs the picker will
infer, the kube context chant binds versus your ambient one, substrate
readiness, committed Ops, and a line per non-chant member kind it found. Each is
pass, warn or fail with a one-line fix. Nothing is started and nothing changes.
The exit code is non-zero only when something would actually stop behold serving
the project well, so CI can gate on it. --json for scripts and agents.
Driving it from an agent or script? GET /api is the front door. It lists the
graph and action routes with a one-line description each, and
GET /api/doctor?reads=1 answers what the last reads cost.
AGENTS.md (shipped in the package) is the read/act contract.
behold preview is the quick way to look at a chant project's graph in a
browser at one port. No env, no emulator, no config: it opens the project you
point it at.
npx @intentius/behold preview # the current directory
npx @intentius/behold preview ../my-project # somewhere elseWith no path it opens the current directory, so running it from inside your
project is the shortest thing that works. From a checkout the same commands run
through npm run dev -- preview.
To colour the graph by what is actually deployed, name an environment with
serve . --env prod, below.
preview/export also take an --emulator flag, a v0.1.0-era turnkey path
for one specific demo project. behold demo and serve --local supersede it for
every purpose; it is kept for compatibility and is not documented further.
behold export freezes whatever estate you're looking at into a self-contained
static folder that any static host can serve. Read-only, and fully
interactive. Pan/zoom, the zoom dial (components, logical, composites,
resources, attributes, plus the ops track and the apply order where the project
has them), radial layout, the inspect pane, and the env/tier pickers all work
client-side; there's no live observe or deploy.
npm run dev -- export --out ./behold-export # defaults to cwd, like preview
# or someone else's project, with its live overlay:
# npm run dev -- export <project> --env <name> --out ./behold-export
npx serve ./behold-export # → open it, no backend runningLike preview, export defaults to the current directory and stays plain (no
env, no emulator) unless you ask. --env <name> turns on that project's live
overlay for the snapshot.
It captures every read endpoint for the whole lens matrix (each env/tier × zoom × radial) in-process, through the exact same handlers the live server runs, so a snapshot is byte-identical to live. The live app can't run on a Worker, since it needs Docker and a chant subprocess; the pre-baked export can.
The bundle is deploy-ready for Cloudflare, since behold export writes an
assets-only wrangler.jsonc (no server code, pure static):
cd ./behold-export && npx wrangler deploy # → https://<name>.<account>.workers.devSet the Worker name with --name, or edit wrangler.jsonc. Auth via
wrangler login or CLOUDFLARE_API_TOKEN + CLOUDFLARE_ACCOUNT_ID (same as any
Workers deploy). GitHub Pages, S3, nginx and Cloudflare Pages
(wrangler pages deploy .) all work too.
The bundled example-writes is one S3 bucket. serve --local boots that
project's own local emulator (Floci, via Docker, generically through
chant emulator up, chant #920), points behold's live overlay at it, and gives
you a ▶ Deploy (floci-apply) button on the panel's Deploy tab that
deploys to it, with no AWS account and no creds. It works for any
emulator-backed lexicon rather than one demo project.
npm install
npm run demo # installs example-writes' deps, then serves it with --local
# → http://localhost:4600(or by hand: npm install --prefix example-writes && npm run dev -- serve example-writes --local --env prod)
- The graph shows the bucket and its TLS policy in blue (declared, not yet deployed).
- Click ▶ Deploy (floci-apply) on the panel's Deploy tab (or ⌘K → "Deploy: Sync"). The now-line streams Build → Apply → Verify; the bucket is created in the emulator via the CloudFormation API.
- The nodes flip green (managed): behold's overlay observed the live emulator.
No Docker running? behold still serves the source graph and tells you to start it, rather than dying on you.
Real AWS. The same project's ▶ Deploy button starts its ApplyOp against a
real account: npm run dev -- serve example-writes --env prod (needs AWS
credentials). What the Deploy tab offers depends on what the project committed: a
committed ApplyOp gets the ▶ Deploy () button (plus Approve when
gated); a project with only components gets ▶ Deploy…, which opens the dial's
component picker; Adopt appears per foreign node (ReconcileOp); every other
Op runs from ⌘K (Run: <name>). Full walkthrough:
example-writes/README.md.
behold demo k8s (or npm run demo:k8s from a checkout) is the k8s analogue
of the Floci demo above: it brings up a
local, single-node k3d cluster (Docker only, no cloud
account), then serves the bundled example-k8s (an nginx Deployment and
Service) with --local. Same mechanism, same shape: declared-not-deployed
(blue) → click Run on k3d-apply → managed (green), server-side applied
with chant's own field manager. Ctrl-C tears the cluster back down.
Beyond the AWS demo's single flip, this one also demonstrates Kubernetes'
two additional tiers (epic #84): zoom into the Deployment to see its Pods
as runtime children (owned by the cluster, never declared, never drift);
induce an out-of-band kubectl scale/kubectl label and refresh to see
managed-fields drift (chant's field manager vs. a competing one); and
switch away from the bound kubectl context to see an unobserved refusal
(an honest "did not look," never a false "all gone"). Full walkthrough,
including the exact commands and what each state looks like over the API:
example-k8s/README.md.
behold never mutates anything itself.
- The vizes only read (
chant graph, snapshots, Temporal history). - Actions don't mutate directly. Sync starts your
ApplyOp; Adopt starts yourReconcileOp(opens a PR a human merges). behold triggers Ops you committed, running on your executor. It holds no apply creds. - Two write gestures, both human-confirmed: Apply (gate signal) and Open PR (merge). Authority stays in your source and your worker, never in behold.
The live path (chant graph --live --overlay, chant lifecycle plan) shells
kubectl/aws/az/the Temporal client and holds cloud creds. That needs a real
process, so behold is a Node service you run where your creds live. Read-only means it only needs read
roles (describe/list), so it's least-privilege to run.
behold is drivable by an agent, and leans on chant's MCP rather than reinventing it:
- Reads.
lifecycle-diff,lifecycle-snapshot, plus behold's own read API (the overlay graph as JSON, blast radius, frame diffs). - Delegated actions. The writes are chant MCP Op tools:
op-runstarts anApplyOp/ReconcileOp,op-signalapproves a gate,op-status/op-reportwatch it. So an agent "syncing prod" isop-run prod-applythenop-signal prod-apply approve-apply: gated, durable, no creds in behold.
behold's value over raw MCP is the live spatial + temporal view and the coupling between them; the underlying capabilities are chant's, exposed the same way to a human and an agent. See AGENTS.md.
npm install
npm run dev -- serve ./path/to/chant-project # source graph (tsx)
npm run dev -- serve ./path/to/chant-project --env prod # + live drift overlay
# or, built:
npm run build && ./bin/behold.js serve ./path/to/chant-project --port 4600Then open http://localhost:4600. With --env, the SPA shows the live overlay;
without it, the source graph.
Live updates. The server watches the served project's source and pushes a
refresh over SSE (/api/events) when a .ts file changes, so you edit your
infra and the graph updates with no reload. Add --poll <secs> (with --env) to also re-query live
drift on an interval and push updates when a node's status changes:
behold serve ./infra --env prod --poll 30 # wait 30s between completed drift sweepsRead budget. HTTP observations, background polling, and frame captures share
one Chant subprocess budget: two reads at once by default (one on a one-CPU
host). BEHOLD_ESTATE_CONCURRENCY overrides that process-wide limit. Up to 64
distinct reads can wait; excess reads fail explicitly rather than growing an
unbounded queue. Identical in-flight reads share work only when the project,
resolved Chant, source stamp, argv, and effective environment match. Source
watcher invalidation also separates new requests from work begun before an edit.
What is cached, and what drops it. A member's source IR is cached under its
source stamp, resolved chant and graph options (src/member-ir.ts), because
~800ms of every chant invocation is module load that has nothing to do with the
member's content. A member's completed live overlay document is cached too
(src/overlay-ir.ts), so that a re-render such as collapsing a box is a
re-render rather than a second pass over the account. That second cache is the
stated exception to "a live read is never cached", and its header names
everything that drops an entry: POST /api/refresh ("Re-check live"),
GET /api/overlay?plan=1 ("Re-check live with plan"), the capture that ends an
Op run, and the member's source moving. What it cannot notice is a change made to
the account by something that is not behold, which is what the re-check rows are
for.
A running read has a 180-second deadline, configurable with
BEHOLD_READ_TIMEOUT_MS. A disconnected GET releases its subscription; when no
callers remain, the read is canceled. On Unix, cancellation stops the whole
npm/tsx/Node process group, escalating from TERM to KILL after one second. On
Windows only the direct child is terminated. Delegated writes retain their
existing lifecycle and are never deduplicated as reads.
The browser runs one refresh at a time and collapses intervening notifications into one follow-up. It no longer starts additional reads at 3/8/15-second offsets. Polling uses the same estate namespace bindings as the HTTP overlay and reuses the primary member's observation for lanes capture. Slow sweeps extend the poll period; they do not overlap the next sweep. These bounds control duplicate work; they do not eliminate Chant's underlying live discovery cost.
What a read cost. Every scheduled chant read is filed with its running time,
its outcome and which side of the source/live split it sits on.
GET /api/doctor serves the same report behold doctor prints with that ledger
on it; GET /api/doctor?reads=1 is the cheap half: counters, the per-member
samples, inFlight (running and queued), and unmeasured, the count of served
members the ledger is blind to because they spawn their own binary rather than
chant. The loading scrim shows the slowest recent member, since an estate read
finishes when its last member does.
The picture can arrive in pieces. GET /api/overlay?progressive=1 answers at
once with the estate composed from each member's source, every card marked
pending, then streams one SSE frame per member as its own live read lands.
Opt-in and never the default: the blocking /api/overlay is what agents read and
what behold export captures, and it is unchanged.
behold shells the project's own chant (resolved from the project's
node_modules first), so the project decides the chant version. Pin it to
@intentius/chant ^0.18.1 or later for the live overlay; graph --live observed
nothing before that fix.
The view is in the URL. behold reads it at boot and writes it back as the view changes, so a reload keeps it and a link opens where the sender was:
http://localhost:4600/?member=delivery&zoom=components&env=prod&node=delivery/appService
member, zoom, env, tier, lens (the colour: drift, cost or
headroom), node and radial (1 or 0). An empty env= is the source
graph. A value the estate can't honour (a member it doesn't have, a zoom it
doesn't offer) is named on the now line. A static export takes the same
parameters against what it captured.
A host pane, such as arugula's workspace block, frames behold with
?embed=1&host=<the host's origin>, and optionally theme=light, dark or
any theme name the picker lists:
- the panel and the inspect pane start folded, and the Deploy tab and the theme picker are gone
- picking a card posts
{type: "behold:select", member, node}to the parent athost; picking a member's box posts the same withnode: null - the parent moves the view with
{type: "behold:view", ...}, carrying any of the URL's fields (member: nullis the whole estate). Messages from any other origin are ignored - gates are the host's to approve: behold draws them and says so
gates=<env> sets the env the gate strip reads, apart from the graph's, so a
host can open behold on the source graph and still show the gates of the env
it watches.
behold binds 127.0.0.1 and answers only loopback names, since it runs writes
with the credentials of whoever started it. A page from another site can't
write to it. A host that frames behold through a proxy of its own passes the
proxy's name with --allow-host <name> (or BEHOLD_ALLOWED_HOSTS); --host
(or BEHOLD_HOST) binds another address, which then needs --allow-host for
the names it is reached by.
In a declared workspace, the gates waiting on a person come from
chant workspace status <env> --json, keyed member/op/gate the way arugula's
workspace block keys them, so the two show the same set. Standalone, a gate's
button says who chant will record (the user running behold) before the click.
Every approve route refuses in preview mode.
A host that already watches the workspace tells behold it changed with
POST /api/refresh?notify=1, instead of running behold with --poll beside
it. behold drops its cached reads and every open page re-pulls.
chant carve advise ranks a Terraform estate by peelability: how cleanly
each resource could be carved into native chant source. behold carve draws
that ranking: one card per resource, three panels (carve now / boundary work /
leave in Terraform) on the same attrs._status drift palette every other view
uses, and the score arithmetic behind each rank in the inspect pane.
chant carve advise --from ./terraform --report carve.json
behold carve carve.json # → http://localhost:4600
curl localhost:4600/api/carve # the raw report, for agentsbehold parses no HCL and needs no Terraform tooling: the report is the
contract. A file that isn't a peelability report is refused with a structured
{error, code: "carve-report", remedy}, in the terminal and from the routes,
never a blank graph. See the carve lens and
the Terraform estate page for the
other half of that lane: Terraform is a member
kind now (MEMBER_KINDS = ["chant", "choudoufu", "terraform"]), so an estate of
.tf files draws beside a chant project, and carve state is read from chant's
own <address>.carve.json manifests.
npx @intentius/behold demo carve # no Docker, no cloud, no terraform binaryCopies a half-migrated estate (a small chant project beside a Terraform one,
both describing the same AWS account), installs the chant it will shell, runs
chant carve advise over the copy, and opens the banded graph with a six-step
stepper on the panel's Carve tab:
- Advise. The bands, with what each one means.
- Pick. Click a green card. The inspect pane shows the score arithmetic; the step names the boundary the cut crosses.
- Emit. Runs
chant carve emit --state --select <addr>intoapp/carveout/in the copy, then shows the emitted chant source and thechant lintresult. - Bridge. Runs
chant carve bridge(never--apply-rewrites) and renders the proposed data source, the rewired survivors and the patch. - Handoff. The runbook's commands with copy buttons, and not a button:
terraform state rmandterraform applychange who owns a live resource, so they stay yours to run. The panel says so. - Done. The card is marked chant-owned at the observe position;
terraform importreverses all of it.
The Emit step reports chant lint, not chant build: build fails on the
emitted bucket (scored 84 by the advisor) on one rule: WAW042, a TLS-deny
bucket policy the source Terraform never declared. The panel links the
reason. example-carve/README.md has the estate's full story, the band table,
and the offline/--live split.
behold writes only into app/carveout/ inside the demo copy it made, and
nothing else. Your Terraform is never edited; see AGENTS.md, "Invariant".
An optional .behold.json in the served project's root is behold's own
config, kept separate from chant.config.ts so behold's concerns (like the
tier picker) don't leak into chant's. Three keys: tiers below, executor
(which forge deploys an environment), and members, spelled [{ "dir": "x", "kind": "chant" | "choudoufu" | "terraform" }], how an estate root names what it
composes, fail-closed on a kind behold has no reader for. members is
deprecated: a chant workspace declaration (chant.workspace.json) lists an
estate's members for every reader, and beside one, .behold.json's list is
ignored. behold doctor --fix writes the declaration from it (see
Serve a chant workspace). First, the
project's deploy-tier axis, a dimension orthogonal to environment (chant
has no native tier concept; it is entirely a project convention, e.g. Loom's
components branching on an env-conditioned namingParams.tier):
{
"tiers": {
"envVar": "LOOM_TIER",
"values": ["light", "production", "production-ha"]
}
}envVaris the env var name the project's source branches on; behold sets it for the chant shell-out whenever a tier is picked (?tier=→ this var, never a chant CLI flag).valuesis the tier picker's options.
No .behold.json (or no tiers key) → no tier axis: the picker doesn't
render and the graph loads with no tier selected, which is the default for any
project that doesn't opt in. There's no other tier config surface (not
chant.config.ts, not an env var behold guesses the name of).
The same file can designate which forge deploys an environment (#165):
{
"executor": {
"prod": { "forge": "github", "workflow": "deploy-prod.yml" }
}
}It names the committed workflow, not just the forge, because two
environments' generated pipelines carry identical job ids and a picker cannot
tell them apart. For a designated environment the Deploy button dispatches that
workflow through your own gh login and follows the run on the dial; a local
apply, a committed ApplyOp for that environment, and auto-sync are all refused
(409 executor-forge), and rollback is withheld. Any approval the workflow's
GitHub environment requires is granted on GitHub by your identity there. The
dial links the run's page and offers nothing else, because behold holds no
identity that could clear a forge gate. A designation behold cannot honour (a
typo'd forge, a missing workflow, one without workflow_dispatch) disables
Deploy for that environment with the reason, and never falls back to running
it on your machine. The workflow is read from the .github/workflows of the
repository the project belongs to, so a project that is one directory of a
monorepo works as-is.
A dispatched run's id is kept under ~/.behold/ci-runs/, the operator's own
state outside the project, so a behold restarted mid-deploy re-adopts the run
and keeps following it. A follow whose stream dies is reported as lost, never
as a verdict; just e2e-ci-github proves the whole contract against GitHub.
dagre places your nodes; you can move them. Drag a card, resize a containment
box, and the offsets are remembered per project and lens: in localStorage
first, and (when the served project is writable) in a .behold/layout.json
sidecar beside it, so a layout is shareable, reviewable in a diff, and honoured
by behold export:
{ "version": 1, "lenses": { "components": { "src/api#Component": { "dx": 40, "dy": -25 } } } }This is the only file behold writes inside a served project. It stores
deltas rather than absolute positions, so the graph stays chant's and your
layout sits on top of it. A delta whose node has left the estate is dropped.
POST /api/layout refuses politely in preview mode, during a static-export
capture, on a read-only directory, and above its size caps. ↺ layout in the
graph clears the current lens on both tiers.
Gitignore it. .behold.json (above) is config and belongs in the repo;
.behold/ is per-user state, one person's arrangement of the picture, so add
it to the served project's .gitignore unless you actually want to share and
review a layout:
.behold/A shape rather than a manifest. src/ is ~85 modules with one concern each, and
AGENTS.md is the map that stays current.
src/
cli.ts the verbs: serve, preview, demo, doctor, export, carve
server.ts the Hono API + static SPA, every route read and delegated
the read path
chant.ts shells the PROJECT's own chant; every read is scheduled
read-scheduler.ts one process-wide budget, in-flight sharing, cancellation
read-stats.ts what each read cost (/api/doctor)
member-ir.ts per-member source cache; overlay-ir.ts, the live one
estate.ts N members composed into one graph (pinhole's composeStacks)
member-kind.ts what a member can be: chant, choudoufu, terraform
the lenses (pure IR -> IR, no subprocess)
logical*.ts one topology projection per substrate
ops-lens.ts declared Ops as a phase track; run-playhead.ts paints a run
carve-lens.ts a peelability report; stack-order.ts, chant's apply order
terraform-lens.ts collapse-lens.ts, edgeless.ts
render.ts pinhole painter, IR -> SVG, one pack per lexicon
web/ the SPA, unbundled ES modules (app.js + ~20 siblings)
docs/ the published site (Astro/Starlight)
demos.json the shipped demo catalog; workbench.json, this checkout's
example-*/ nine bundled projects the demos and e2e serve
e2e/ twelve acceptance runs, one per substrate lane
behold reuses pinhole's SVG painter as a
library: a mature renderer with themes, icons, and _status drift colouring
that already speaks the overlay vocabulary managed/foreign/pending. The server lays the IR out
and paints it with layoutIr + renderSvg (src/render.ts); the SPA inlines the
SVG and wires click-inspect by data-node-id against the IR. pinhole's layout is
dagre, which is pure JS with no native dependency.
Where an official mark exists, a node paints it instead of a generic glyph. A
Deployment gets the Kubernetes wheel-and-helm heptagon, a Kustomization the
Flux mark, a Helm::Release the Helm wheel. The corpus is vendored under
web/icons/ (30 kubernetes/community SVGs, 3 cncf/artwork marks for
Flux/Argo/Helm, licensing in THIRD_PARTY.md) and mapped kind by kind in
src/icon-packs.ts; a kind with no official icon falls through to pinhole's
keyword heuristic rather than a wrong picture.
dagre's layout is a good first draft, not a final one: drag a card to move
it, grab a containment box's corner to resize it, and both survive a reload.
What persists is a delta ({dx,dy} for a card, {dw,dh} for a box) rather
than an absolute position, keyed by behold.layout.<project>.<lens>
(web/layout-store.js), so the graph stays chant's and the arrangement on
top of it is yours. ↺ layout sits beside ⤢ fit and shows up only once
something on the current lens is hand-placed.
Every JSON value the UI shows goes through one renderer (web/json-view.js):
2-space pretty printed, objects and arrays collapsible (the first tier open,
anything deeper or wider than a dozen entries folded), long strings truncated
with an expander, and a copy on every node that yields that subtree's raw
JSON. Enter or Space toggles the focused node. It paints with --fg and
--muted and nothing else, so all 552 palettes keep it readable. That covers
the inspect pane's declared attributes, observed live state, drift pairs and
field ownership, the op log's JSON lines, and the payload behind an /api
error card.
Type splits by purpose: a system mono stack carries node ids, ARNs and
statuses, and sans carries labels only. Colour comes from 552 Ghostty terminal palettes run
through an OKLCH-derived token pipeline (web/theme.js, the palettes in
web/themes.js), so a theme switch
re-derives the whole chrome rather than only the graph. A node whose drift
status just changed pulses once in the colour it became, off under
prefers-reduced-motion.
just lists everything. The core loop:
just install # behold's own deps
just check # tsc + unit tests + build (the fast gate)
just example-install # install the example project's chant + aws lexicon (once)
just serve # serve example/ read-only → http://localhost:4600 (source graph)
just serve example prod # same server, live drift overlay (needs AWS creds)One server, one SPA: passing an env turns on the live overlay (/api/overlay),
omitting it shows the source graph (/api/graph). serve runs via tsx (no build
step); for the built binary, just build then ./bin/behold.js serve <project>.
Which chant runs. behold does not bundle chant. It shells the chant binary
resolved from the served project's node_modules, falling back to its own dep.
So local testing means installing chant into a project, not into behold. The bundled
example/ does exactly that; point serve at any real chant project the same way.
just e2ee2e/run.sh installs the example's chant (the chant install under test), builds
behold, serves the example, and asserts the read-only API against a live server. It
auto-detects AWS credentials:
- no creds → asserts
/api/graph(the source mixed-substrate graph, offline). - AWS creds → asserts
/api/overlay, the source-anchored live overlay: it queries CloudFormation and checks every node carries a drift status. Allpendingwhen nothing is deployed is a valid pass, since the point is the live path.
It's hermetic apart from the chant install and (optionally) the cloud read; the
server is torn down on exit. BEHOLD_E2E_PORT overrides the port.
npm test # vitest, 106 files, including in-process route tests against
# the real Hono app. No cloud, no cluster.behold is a viewer over a compiler, so some of what it cannot do is not its to fix. The open asks:
- chant #822, diff two historical snapshots, which is what a timeline of more than the frames behold captures itself would need.
- chant #2481,
chantcosts ~800ms per invocation before it does any work, and behold shells it once per member per read. - pinhole #79/#80/#81, morph-over-time and first-class drift rendering.
behold's own work is on the issue tracker.