Bayt gives you Bazel-quality incremental invalidation on top of the build tools you already use — gradle, pnpm, go, cargo, make, whatever. One CUE declaration per target generates every file your existing tools expect: Taskfile.yml, per-target Dockerfiles, compose.yaml, skaffold.yaml, docker-bake.hcl, .vscode/tasks.json, plus a canonical per-target JSON manifest.
You don't migrate away from your build tool. You just stop hand-maintaining seven files that all describe the same target in slightly different ways.
Bayt is an opinionated build-config generator: you describe targets in CUE, bayt emits the toolchain-specific files. It pairs naturally with sayt (sayt's generate verb invokes bayt as one of its rulemap steps) but bayt is standalone — you can run bayt directly from any project containing a bayt.cue. If you're happy with .vscode/tasks.json you don't need bayt. If you're tired of your build graph disagreeing with your compose graph which disagrees with your CI graph, bayt is the DSL that makes one source of truth for all of them.
- One target, every format.
srcs,deps,outs,cmd— declared once in CUE, emitted into Taskfile, Dockerfile, compose, skaffold, bake, and vscode. No drift, no copy-paste. - Merkle-chain fingerprinting. Every target hashes its own manifest + srcs + each direct dep's stamp file. A change anywhere in the DAG cascades exactly once per layer. Same correctness guarantee Bazel gives you, with no sandbox, no Starlark, no rule ecosystem to learn.
- Works with what you have. Your gradle/pnpm/go commands keep running them. Bayt doesn't replace
./gradleworpnpm install; it just makes sure they run exactly when they need to. - Shared stack definitions. Concept libraries (
gradle,pnpm,mise) capture per-toolchain primitives; thesaytumbrella maps them onto the canonical 10-verb shape. A new gradle service is five lines of CUE:_proj: sayt.gradle & { dir: "..." }. - Content-addressable caching, two layers.
cache.nuwraps every Taskfile cmd with content-addressed restore-and-run. Stack-side, gradle/cargo/go/etc. get their own native build cache pointed at the same$BAYT_CACHE_DIRdirectory — gradle's per-task cache for example is ~15× finer than bayt's per-target cache. The two layers compose: bayt skips the whole cmd when the target hasn't changed; the tool skips most of its work when only some inputs changed. Backends for the bayt layer: local-FS (default, XDG-compliant),BAYT_CACHE_URLfor buchgr/bazel-remote,BAYT_CACHE_REGISTRYfor ORAS OCI. Per-targetbayt.cache.fullskips cmd entirely on exact hit (the gradle daemon cold-start escape hatch). - Gradual adoption. Start with one target. Add more when the hand-maintained files get painful. No all-in moment.
Pin the release in your project's mise.toml:
[tools]
"github:bonisoft3/bayt" = "0.1.0"bayt is then on PATH and the cue/nu tree lives next to it. Run from any directory containing a bayt.cue:
bayt --recursiveSayt pairing (optional). If the project uses sayt, add bayt to the generate rulemap so sayt generate re-emits bayt's outputs alongside the rest of the pipeline:
say:
generate:
rulemap:
bayt: nullStart with one target. Here's a gradle service:
// services/my-service/bayt.cue
package my_service
import (
bayt "bonisoft.org/plugins/bayt/core:bayt"
sayt "bonisoft.org/plugins/bayt/stacks/sayt"
)
_proj: sayt.gradle & {
dir: "services/my-service"
targets: {
"release": skaffold: image: "gcr.io/proj/my-service"
}
}
project: _proj
depManifestsIn: {[string]: _}
_render: (bayt.#render & {project: _proj, depManifests: depManifestsIn})From the project dir:
nu ../../plugins/bayt/runtime/generate-bayt.nuThis emits Taskfile.yml, .bayt/Taskfile.<verb>.yaml, .bayt/<verb>.Dockerfile, compose.yaml, .bayt/compose.<verb>.yaml, skaffold.yaml, .bayt/skaffold.<verb>.yaml, .bayt/bake.<verb>.hcl, .vscode/tasks.json, and the per-target JSON manifests under .bayt/targets/. All are committed — lint enforces "don't hand-edit."
Now task build:build runs ./gradlew assemble only when sources changed. task test:test runs only when build or test sources changed. Touching any upstream file cascades through the chain automatically. If you run the same target twice with no changes, nothing happens.
Adding a second service takes five more lines of CUE. Cross-project dependencies are first-class — services/tracker depending on libraries/xproto gets xproto's build stamp folded into tracker's fingerprint, so tracker rebuilds when xproto's srcs change.
Every #target is described by a small, fixed set of fields. Declare them once, emit everywhere.
| Field | Meaning | Matches Bazel |
|---|---|---|
srcs.globs |
Files whose content change invalidates this target. | srcs |
srcs.exclude |
Glob patterns pruned from srcs walks (node_modules/**, etc.). |
glob(exclude=) |
outs.globs |
Files this target exposes to consumers. Missing outs force re-run. | outs |
outs.exclude |
Glob patterns pruned from outs. | — |
deps |
Other targets to build first. Strings (same-project: :target, cross-project: project:target). |
deps |
visibility |
"internal" (default) or "public". Public targets are consumable cross-project. |
visibility |
cmd |
The action to run. Shorthand do: "cmd" or the full rulemap. |
cmd / exec |
env |
Environment variables passed to cmd. | env (via --action_env) |
activate |
Toolchain prefix (usually mise x --). Defaults from #project. |
toolchains |
dockerfile.from |
FROM source for this target's Dockerfile stage. Either a fresh image (from: name: ..., typically via an image preset like bayt.nubox) or a chain to another target (from: ref: ":<target>" for same-project, "<project>:<target>" for cross-project). Cross-project from: ref: automatically wires federation (compose include + additional_contexts + visibility check) — no separate deps: entry needed for the from-ref alone. Default: scratch (when no preset). |
— |
cache.full |
When true, on EXACT cache hit restore outs and skip cmd entirely. Default false (restore + run cmd, letting its own incremental engine no-op on warm outputs). Use bayt.cache.full capability to set. |
— |
cache.similar |
When true, on EXACT-match miss look for the closest cached entry (weighted intersection over inputs + user/branch/day) and restore as warm starting state. Default false. Use bayt.cache.similar capability to set. |
— |
srcs and outs are structured {globs, exclude}. The shorthand for the common case (no exclude) is one line:
srcs: globs: ["src/**/*.kt", "build.gradle.kts"]
outs: globs: ["build/libs/**/*.jar"]A minimal target:
"build": bayt.build & {
srcs: globs: ["src/**/*.go", "go.mod", "go.sum"]
outs: globs: ["bin/app"]
do: "go build -o bin/app"
}For the 20% of targets that need OS variants, dockerfile mounts, or compose decoration, the full cmd: "builtin": { do, windows, dockerfile, compose } rulemap is available alongside.
What flows from a producer to its consumers is declared by the producer, never by framework heuristic:
outs.globs/exclude— the producer's public interface. Cross-project consumers (deps: ["foo:build"]) get exactly these files via per-globCOPY --from=<producer>in the consumer's Dockerfile. If the producer wants.task/stamps/<target>.hashto flow (so the consumer's task chain short-circuits the cross-project dep), they include it in outs. If not, they exclude it. No framework--exclude=.taskmagic.visibility—"internal"(default) means same-project consumers only."public"means cross-project consumers candeps:orfrom:reference this target. Generation fails at CUE-evaluation time if a cross-project dep targets an internal target.
Each emitted Dockerfile stage's FROM is the producer's choice. Bazel-style refs: :target (same project) or project:target (cross):
// Leaf: FROM an image. Use a base preset (sets stage + preamble too).
"setup": dockerfile: bayt.nubox
// Chain: FROM another target in the same project. Inherits the upstream
// stage's filesystem (toolchain installs in /root/.local/, .task/ stamps,
// project tree). Stack defaults already do this for build/test/integrate.
"build": dockerfile: from: ref: ":setup"
// Cross-project chain — common pattern for stacks that want to inherit
// a shared base (workspace-root setup, JVM toolchain stage, etc.).
"setup": dockerfile: from: ref: "workspaceroot:setup"The chain form means the build stage is the setup stage extended — no mise install re-run inside build, and task bayt:build's ::bayt:setup dep correctly short-circuits on the inherited stamp.
Cross-project from-refs federate automatically. A from: ref: "X:Y" is enough — bayt wires the compose include for X, the additional_contexts entry for the FROM alias, and the visibility check on Y in one go. You only add deps: ["X:Y", ...] when you also need the dep's outs COPY'd in (the explicit-data path), separate from the FROM-chain inheritance.
A stack captures what a language toolchain needs. Bayt ships four:
stacks/gradle— kotlin/java/gradle concept fragments:assemble,test,integrationTest,jibBuildTar,check,run. Default srcs scoped tosrc/main/forassemble(so test edits don't invalidate build);bayt.cache.fullonassembleandintegrationTest(gradle's daemon cold-start is too costly to pay on every cache hit). Emits.bayt/init.gradle.ktsper project pointing gradle's local build cache at$BAYT_CACHE_DIR/gradle— gradle's per-task cache and bayt's per-target cache share the same on-disk store and complement each other (per-task hits when only some inputs changed, per-target full skips when nothing changed).stacks/pnpm— pnpm/node/vite/vitest concept fragments:install,build,test,dev,testInt,testE2E,lint. Test srcs split betweensrcsTest(*.test.ts(x)) andsrcsIntegrate(*.spec.ts(x)) matching the repo's vitest convention. pnpm store cache mount.stacks/mise— toolchain installer.install(provisions the project's.mise.toml),exec(setsactivate: "mise x --"so cmds resolve through mise's shim layer),doctor. Used as a building block by other stacks.stacks/sayt— umbrella that maps the 10 sayt verbs (setup/build/test/launch/integrate/release/verify/generate/lint/doctor) onto stack fragments.sayt.gradle,sayt.pnpm,sayt.pnpmWorkspaceare the standard mappings projects compose against.
Using the umbrella collapses a typical service to a handful of lines:
_tracker: sayt.gradle & {
dir: "services/tracker"
targets: {
// Cross-project deps: producer must mark visibility "public".
"build": deps: [
":setup", "workspaceroot:setup",
"libraries_xproto:build",
"plugins_jvm:build",
]
"release": skaffold: image: "gcr.io/proj/tracker"
}
}A consumed library declares visibility: "public" on the verbs it exposes:
_xproto: sayt.gradle & {
dir: "libraries/xproto"
targets: "build": visibility: "public" // tracker can deps: ["libraries_xproto:build"]
}bayt.healthcheck.* are composable fragments that wire a target's
Dockerfile HEALTHCHECK + compose healthcheck override + any required
tool COPY (microcheck's httpcheck / portcheck static binaries) in
one declaration. Five templates ship today:
| Fragment | Probe | Tool source |
|---|---|---|
bayt.healthcheck.http |
HTTP GET 2xx | httpcheck from microcheck (auto COPY) |
bayt.healthcheck.tcp |
TCP listener up | portcheck from microcheck (auto COPY) |
bayt.healthcheck.postgres |
pg_isready |
bundled in postgres image |
bayt.healthcheck.redis |
redis-cli ping PONG |
bundled in redis image |
bayt.healthcheck.ollama |
model listed via ollama list |
bundled in ollama image |
Defaults follow "probe aggressively, fail leniently" (1s interval,
30 retries, 200ms start_interval, 30s start_period). Override per-target
inline only when needed — postgres/ollama typically bump start_period
for slow cold-starts.
"release-proxy": sayt.release & bayt.healthcheck.http & {
healthcheck: url: "http://127.0.0.1:8081/health"
dockerfile: from: name: "caddy:..."
}
"release-cdc": sayt.release & bayt.healthcheck.http & {
healthcheck: {
url: "http://127.0.0.1:8080/healthz"
start_period: "120s" // conduit's slot-init dance
}
...
}compose.healthcheck carries the compose-spec extension start_interval
(probe rapidly during the start_period window) which Dockerfile
HEALTHCHECK doesn't model.
Edit a source in libraries/xproto
│
▼
xproto.build.hash changes (own srcs fingerprint flips)
│
▼ (consumer fingerprints xproto's stamp)
services/tracker/build.hash changes
│
▼
services/tracker/test.hash changes (test fingerprints build's stamp)
Each task's stamp = hash(platform-key + srcs + each direct-dep stamp file). Because go-task runs deps before evaluating the parent's status:, each dep's stamp on disk reflects its latest state by the time the parent reads it. Invalidation propagates one hop at a time through the real file system — no recursive walk, no dependency-graph library, just stamp files on disk acting as content-addressed identities for each subtree.
That's the same key recipe the remote cache (bazel-remote / ORAS) uses, so a L0 miss can become a L1/L2 fetch instead of a rebuild whenever someone else has already built the same content.
Bazel is a great system — a lot of bayt's design is explicitly borrowed from it (srcs, deps, outs as attribute names; Merkle hashing for correctness; content-addressable remote caching; composable rules/stacks). The comparison below is about fit, not quality.
Where Bazel is more effective:
- In-process action sandboxing. Bazel sandboxes every action so it can only see the inputs you declared. That gives reproducibility guarantees bayt's default mode doesn't match — your
./gradlewinvocation has access to$HOME, the network, and whatever else gradle decides to poke. Bayt has a different answer (docker-based, see below) but at the per-action level Bazel's sandbox is tighter. - Action-level granularity. Bazel splits a compile into per-source actions that can be cached, replayed, and distributed individually. Bayt's unit is the task (one gradle invocation, one pnpm build). For a monorepo with thousands of Go packages where you want to rebuild three of them, Bazel's model wins — bayt re-runs the whole gradle subproject on any invalidation inside it. For teams whose bottleneck is cross-package incrementality, that's a real gap.
- Mature rule ecosystem.
rules_go,rules_nodejs,rules_cc,rules_python,rules_kotlin,rules_proto— Bazel has battle-tested rules for virtually every language, often maintained by the language vendors. Bayt has two stacks (gradle, pnpm) and expects new stacks to be authored per monorepo. - Query and analysis.
bazel query,bazel cquery,bazel aqueryare unmatched for introspecting the build graph. Bayt's graph lives in.bayt/targets/*.json— readable, but no query CLI yet.
Where bayt has its own answer:
-
Docker-based hermeticity. Bayt's hermeticity story runs through Docker, not a per-action sandbox. A target with
bayt.incrementalruns inside a Dockerfile stage the emitter generates — the environment is defined by the base image + declared srcs + cache mounts, which is arguably more hermetic than Bazel's sandbox because you control the entire OS layer, not just the filesystem inputs.launchandintegrateverbs extend this: your app runs in docker with testcontainers or compose-managed dependencies, so "hermetic run" is a first-class concept alongside "hermetic build." This is the path google3 takes for many services internally, and it's what Docker/BuildKit was designed for. -
Remote execution via BuildKit. For docker-centric flows, depot.dev and similar services already provide remote BuildKit execution — your Dockerfile builds run on managed infrastructure, outputs come back cached. For bayt targets that use
bayt.incremental, this gives you Bazel's remote-execution value (run the action elsewhere, ship artifacts back) without standing up a Bazel RE cluster. Testcontainers, Kubernetes jobs, or Cloud Run can play the same role for short-lived execution of integration tests. -
Two-layer cache, composed.
cache.nuprovides per-target content-addressed cache (full skip on exact hit viabayt.cache.full; warm-start on miss viabayt.cache.similar). Stack-side, each language stack configures its own native build cache at the same$BAYT_CACHE_DIRdirectory — gradle's per-task cache via init.gradle.kts, planned go GOCACHE, cargo sccache, etc. The two layers compose: bayt skips the whole cmd when nothing changed; the tool skips most of its own work when only some inputs changed. Same Merkle hash key as the local L0 stamp. Backends for the bayt layer: local-FS (default, XDG-compliant),BAYT_CACHE_URLfor buchgr/bazel-remote (which itself can chain to S3/GCS/Azure or proxy to depot.dev / BuildBuddy),BAYT_CACHE_REGISTRYfor ORAS OCI. Stable BuildKit cache mount (id=bayt-cache) means hits work inside Dockerfile RUNs across stage rebuilds. -
Onboarding cost. A team can adopt bayt on a single service in an afternoon. The common mode is "add a
bayt.cuenext to an existingbuild.gradle.kts, run the generator, check the Taskfile in." A Bazel migration is typically measured in quarters — most are successful, but the up-front commitment is real, and partial migrations are painful because coexistence with native tools isn't Bazel's strength. Bayt is designed for incremental adoption; one target at a time is a normal path. -
Integration with existing tools. Your IDE already understands
./gradlew, your CI already runsdocker compose, your ops team already deploys via skaffold. Bayt emits files those tools natively consume. No Bazel build wrapper, noibazel, no "why doesn't VSCode see my imports." For teams whose day-to-day already runs on those tools, bayt is essentially transparent. -
Tools keep their internal caching. gradle's incremental compile still works inside bayt's task boundary. pnpm's store cache still works. You benefit from both the tool's internal caching AND bayt's task-level cache. Bazel replaces the tool's own caching with Bazel's, which is great when the replacement is solid and painful when there's a mismatch (gradle's worker daemon behavior, for instance, is notoriously hard to preserve under Bazel).
-
CUE vs. Starlark. CUE's unification catches a lot at evaluation time. CUE stacks compose by structural unification; Starlark rules compose by function call. Both are valid; CUE's flavor is the one bayt chose.
-
Heterogeneous stacks. A monorepo with one gradle service, two pnpm apps, and a Go tool is bayt's happy path — three stack definitions, each scoped to its language. Bazel can handle this but the ruleset upkeep and cross-rule interop effort is non-trivial.
-
Operations cost. Bayt's remote cache is a single
bazel-remotecontainer or an OCI registry. Bazel's remote execution needs a worker pool, a scheduler, a disk farm, a rollout story — great when a company has a build infrastructure squad to own it. For teams that don't, bayt + depot.dev (or similar) reaches a similar outcome without the operational surface.
Where the comparison lands:
Bayt is in many ways a Bazel subset implemented under different constraints. It keeps Bazel's correctness guarantees (Merkle-tree invalidation, content-addressable cache keys) and borrows Bazel's core vocabulary (srcs/deps/outs). It trades Bazel's per-action sandboxing and rule ecosystem for easier interop with native tools and a dramatically lower onboarding cost. For monorepos with mixed stacks, moderate size, and a team that would rather extend their existing tooling than migrate to a new build system, bayt is the better fit. For monorepos with thousands of same-language packages, heavy cross-package incrementality needs, or companies with a dedicated build-infra team already invested in Bazel, Bazel remains the right answer.
Worth noting: the two aren't mutually exclusive. .bayt/targets/<n>.json is a machine-readable description of every target's action; a team that grows into needing Bazel can feed that into a rule-gen layer rather than starting from scratch. Bayt is useful scaffolding whether you stop there or eventually move beyond.
All bayt-generated files use the <tool>.<verb>.<ext> convention under
.bayt/. The user-authored tool roots (Taskfile.yml, compose.yaml,
skaffold.yaml) sit alongside as single root files that include
their per-target sibling.
| Path | Purpose |
|---|---|
.bayt/bayt.<n>.json |
canonical per-target manifest (srcs, outs, deps, cmds, …) |
Taskfile.yml |
root go-task (version + includes) |
.bayt/Taskfile.<n>.yaml |
per-target go-task include |
.bayt/Dockerfile.<n> |
per-target Dockerfile body |
compose.yaml |
root compose (include of bayt-generated services) |
.bayt/compose.<n>.yaml |
per-target compose service |
skaffold.yaml |
root skaffold (requires: of bayt-generated configs) |
.bayt/skaffold.<n>.yaml |
per-target skaffold config |
.bayt/bake.<n>.hcl |
per-target bake HCL |
.bayt/vscode.<n>.json |
per-target vscode task entries (build/test only). User merges into .vscode/tasks.json; sayt lint warns on drift. vscode's tasks.json has no native include, so bayt doesn't overwrite it directly. |
The .bayt/ directory is generated but committed. A single sayt generate (or nu plugins/bayt/runtime/generate-bayt.nu) rebuilds the whole tree atomically.
- One declaration, every format. Cross-cutting concerns live once on
#target; each emitter projects into its own output format. - Canonical manifest as the source of truth.
#manifestGenproduces format-neutral JSON; every other emitter consumes it. So do downstream tools likefingerprint.nuandcache.nu. - Pure CUE for schemas; impure nushell for I/O.
generate-bayt.nuis the only layer that touches the filesystem.fingerprint.nuhashes files.cache.nutalks to HTTP caches. CUE stays deterministic and sandboxable. - No path math in CUE. Repo-relative
../computation lives in nushell, which has a proper path library. CUE carries structured data ({name, projectDir}), nushell joins it. - Fragments via unification, not inheritance. Verbs (
setup,build, …) and base presets (nubox,busybox, …) are plain structs, not closed#-prefixed definitions — CUE's closed conjunction rejects cross-def fields. See the closedness note inbayt/bayt.cue. - Version intent vs. version lock. Base image tags go in
bayt.cue; digests live inbases.lock.cue.pin-bases.nurefreshes the lock. - Pin every layer ingredient. Image presets and consumer preambles pin OS-package versions inline (
zypper -n install findutils=4.10.0-160000.2.2 which=2.23-160000.2.2). The base image is digest-pinned, so an unpinned package install is the one remaining drift surface — pinning closes it. Reproducibility holds across registry-side base updates. - Never swallow errors. fingerprint.nu and cache.nu fail fast on missing inputs, malformed manifests, git-hash-object errors. A misconfigured target surfaces immediately instead of poisoning the cache with silent defaults.
Bayt ships as a Claude Code plugin with skills that teach Claude how to write and edit bayt.cue, add new stacks, and debug the generated output.
| Skill | What Claude learns |
|---|---|
| bayt-target | How to write a bayt.cue — the seven #target fields, cmd rulemap, dep references, skaffold/bake blocks. Auto-invoked when editing bayt.cue. |
| bayt-stack | How to author a new language stack — workspace prefix, verb defaults, cache mounts, what belongs in the stack vs. the consumer. |
| bayt-debug | How to diagnose fingerprint mismatches, missing-src errors, cross-project stamp resolution. |
The bayt-dev-loop agent can drive the generate → build → verify cycle for a new service end-to-end.
plugins/bayt/
├── README.md ← this file
├── DESIGN.md ← full design doc (rationale, cross-cutting concerns)
├── bayt/ ← CUE package `bayt`: schema + emitters
│ ├── bayt.cue (#target, #project, #cmd, #dockerfile, #compose,
│ │ #skaffold, #bake, #vscode, #taskfile, #mount)
│ ├── capabilities.cue (bayt.incremental, bayt.cache, …)
│ ├── images.cue (nubox / busybox / staging / wolfi / dind /
│ │ dockerCli presets — set dockerfile.from)
│ ├── images.lock.cue (digest pin per image — package.json-style)
│ ├── emitter.cue (#render — composes the per-format generators)
│ ├── gen_bayt.cue (manifest emitter — the canonical .bayt/bayt.<n>.json)
│ ├── gen_taskfile.cue (Taskfile + per-target Taskfile.<n>.yaml)
│ ├── gen_compose.cue (Dockerfile.<n> + compose.<n>.yaml)
│ ├── gen_skaffold.cue
│ ├── gen_vscode.cue
│ ├── gen_bake.cue
│ ├── mapaslist.cue (#MapAsList helper for compose-friendly defaults)
│ ├── listutils.cue
│ └── *_check.cue (vet-as-test stress patterns)
├── stacks/ ← language preset libraries
│ ├── gradle/gradle.cue (gradle concept fragments: assemble, test,
│ │ integrationTest, jibBuildTar, check, run)
│ ├── pnpm/pnpm.cue (pnpm concept fragments + pnpmWorkspace)
│ ├── mise/mise.cue (install / exec / doctor — used by other stacks)
│ └── sayt/sayt.cue (umbrella — maps 10 sayt verbs onto stack
│ fragments; sayt.gradle, sayt.pnpm, …)
├── runtime/ ← impure nushell bits invoked by generated files
│ ├── generate-bayt.nu (reads `render` output, writes files atomically;
│ │ runs cache.nu gc at end of generation)
│ ├── fingerprint.nu (content hash + Merkle chain, git-aware,
│ │ platform-key includes arch + libc flavor)
│ ├── cache.nu (3-backend cache wrap: local-FS / bazel-remote /
│ │ ORAS; `cache.nu run` is the per-cmd wrap;
│ │ `cache.nu gc` evicts oldest mtimes to budget)
│ └── cache_test.nu (12-test suite: miss / hit / hit+full /
│ disabled / failed-cmd / gc-evicts /
│ gc-noop / manifest-bypass / warm-with-
│ similar / no-similar-no-warm / debug-log /
│ similarity-picks-closest)
└── tests/
├── test-bayt.nu (positive + negative suite runner)
└── _negative/ (intentional-cycle test; separate CUE package)
bayt's own dev workflow is wired through standard sayt verbs (so the sayt:sayt-dev-loop TDD skill can drive it):
cd plugins/bayt
just sayt build # full CUE positive + negative suite
just sayt test # bayt suite + cache.nu nu test suite
just sayt integrate # same as test today (docker variant deferred)Direct invocation also works (skip the sayt wrapper):
nu tests/test-bayt.nu # 4 CUE suites: core schema + sayt
# mappings + stacks consumers + negative
# cycle (intentional A→B→A must fail
# `cue eval`). Strict `cue eval`, not
# lenient `cue vet`.
nu runtime/cache_test.nu # 12 nu tests: miss / hit / hit-with-full
# / disabled / failed-cmd / gc-evicts /
# gc-noop / manifest-bypass / warm with
# --similar / no warm without --similar
# / debug-log records decisions /
# similarity picks closest candidate.- Bayt is written in CUE + nushell. Every piece of file I/O lives in
runtime/*.nu; everything else is pure CUE. - Prefer to add new capabilities as plain structs unifiable into
#target, not as closed#-prefixed definitions. - Run the test suite before opening a PR —
nu tests/test-bayt.nucovers both positive and negative paths. - Keep the core schema small. Stacks are where toolchain-specific knowledge lives.
- No path math in CUE. Use nushell's
pathprimitives fromfingerprint.nuor equivalent. - Never swallow errors —
try/catchwith a fallback is a smell. Let misconfigurations fail fast.
MIT. See the sayt repo for the license file.