Wanderland

Wanderland Dev — AI Primer

A frame-of-mind primer for AI agents (Claude) working in the wanderland-* repos. Read this at the start of every session. Built from the current state of the code as of 2026-05-13; flags doc-vs-code drift inline.

Mantra: Before writing anything, look for what already exists. The most expensive mistake an AI makes in this codebase is inventing a thing that's already there with a different name. The kind: shell adapter exists. The :http adapter exists. The containerized_deployment archetype exists. The iterate_artifacts boundary exists. Grep before you invent.

TL;DR — the mental model

Wanderland is a Ruby framework where the unit of work is a boundary: a class with a declared identity, scopes, input/output shape, and a single #call(input). Boundaries talk to the outside world through adapters (HTTP, shell, secret, AWS) that wrap external dependencies behind a uniform facade with mode-switching (live / replay / mock / dry-run). A site's config.yml declares which adapters to mount and which boundaries to wire into routes (HTTP path + verb, also a CLI subcommand). For pipeline-shaped work, archetypes (like containerized_deployment) declare slot orderings that user config fills in. Packs are gems that add adapter kinds and boundaries to the core engine.

Every dispatched boundary produces a signed, chained Crossing (caller identity → boundary identity, type-addressed). The append-only Context log of all crossings for a request is what later slots read — boundaries communicate through the context, not by returning values to each other.

Method — data models first

The order of work here is fixed. It is the cheapest path and the one that keeps a change honest. When a task gets into the weeds, return to this order.

The data model is the spec. Between the shapes, the scenarios that pin them, the conversation, and the code comments, the implementation is determined. Write the fixture, watch the scenario miss, build the boundary, watch it pass. Start from the data a call must produce, not from the boundary.

Workspace map

Repo What lives here
wanderland-core engine, dispatch, registries, built-in adapter kinds (http, shell, secret), boot boundaries, YAML resolvers, scenario runner, introspection routes
wanderland-aws-pack aws adapter kind, ECS-shape boundaries (register_task, poke_service, wait_service_stable, ecs_service_deploy)
wanderland-pipelines-pack pipeline-shape boundaries (start_run, complete_run, mvn, compile_jar, publish_jar, docker_build, docker_image, publish_image, compose_deploy, shell_provisioner, publish_stack, iterators artifacts / component); archetypes (containerized_deployment, infrastructure_deployment)
vivarta-jenkins, per-project images (cool-tools, cfn-tools, …) site-specific boundaries, their own config.yml, consume the packs above as gems

Pack file layout (verified on-disk):

<pack>/
  <pack>.gemspec
  lib/
    <pack>.rb                                # entry: requires deps + each boundary; register_kind here
    <pack_snake>/
      adapters/<kind>.rb
      boundaries/<name>.rb
      archetypes/<name>.yml

Pack entry pattern: require "wanderland-core", require_relative each adapter/boundary (or Dir.glob), BootMountAdapters.register_kind("name") { … } for new kinds, Wanderland::Archetypes.register(name, path) for archetypes. Boundary DSL self-registers on file load.

Boundaries

The DSL:

boundary :name,
  identity:     IDENTITY,            # Wanderland::Identity — id becomes from_addr on every crossing
  requirements: [:scope, ...],        # scopes the *caller* must hold; enforce_denials checks at dispatch
  capabilities: [:cap, ...],          # what this boundary claims to provide
  description:  "one line",
  input_shape:  { ... },              # rich field-rule vocabulary, see below
  output_shape: { ... },              # same vocabulary, applied to input.output writes
  adapters:     [:http, :shell],      # contract: input.adapter(:name) halts hard if name not declared
  when_shape:   { ... },              # optional guard for the chain walker
  serves:       "application/json",   # optional MIME for the format injection
  env:          [...] | { ... },      # ENV keys this boundary consumes — see "Env declaration"
  chain:        [...]                 # optional — make this boundary a wrapper, see "Chain wrappers"

Inside call(input):

input_shape / output_shape — full field-rule vocabulary (per wanderland-core-boundary):

Each field maps to one of these forms:

Form Meaning
"field" => true Declared, optional. No constraints. Shape-validates as present-or-absent.
"field" => false Declared but disabled — explicit signal "not used here." Rarely needed.
"field" => { matches: /regex/ } Shorthand for required + regex constraint. Most boundary writers use this for the 80% case.
"field" => { required: true } Required-flag set; documentation-only enforcement at validate-time. find_or_fail is the actual halt mechanism — declaring required: does not by itself raise.
"field" => { required: true, description: "...", constraints: ..., metadata: { ... } } Full structured form.

The rich field rule:

Nested fields: when a value is itself a hash with sub-fields, declare the nesting:

input_shape: {
  "args" => {
    "stack_name" => {
      required:    true,
      description: "CFN stack name; <system>-<environment>-<workload>",
      constraints: { matches: "^[a-z][a-z0-9-]{2,127}$" }
    },
    "params_file" => {
      required:    true,
      description: "Path to the params YAML relative to the workspace root"
    },
    "lint_threshold" => {
      required:    false,
      description: "Severity floor that halts the verify chain",
      constraints: { includes: ["info", "warning", "error"] },
      metadata:    { default_visible_to_operator: "warning" }
    }
  }
}

Anti-pattern: declaring every field as bare true. That technically registers the shape but tells the operator at /inspect/boundary/:name nothing, and the scenario writer nothing about the contract. Write the rich form.

output_shape uses the same vocabulary, against the keys the boundary writes to input.output. Used by the engine to scrub adapter mask-sets, to verify scenarios, and to document the boundary's product shape at the inspector.

Composition is the wrapper pattern. No dispatch(...) helper in the boundary mixin (today). Wrapper boundaries call Wanderland::Boundary.execute(:sub_name, sub_input) directly, get a Crossing back, halt-passthrough on :signals:stop:*, and context.append(crossing) so later slots can read sub-boundary results. The cfn_verify tutorial walks the worked example. (See "Chain wrappers" below for the in-progress declarative replacement.)

Chain wrappers (in progress, design settled, code in flight):

class CfnVerify
  include Wanderland::Boundary
  boundary :cfn_verify,
    identity:     IDENTITY,
    requirements: [:execute, :read],
    capabilities: [:verify],
    adapters:     [:cloudformation, :cfn_lint],
    chain: [
      :render_params,
      :cfn_validate_template,
      :cfn_lint_template,
      { boundary: :cfn_change_set_preview,
        args: { parameters_resolved: project("context.parameters") } }
    ]
end

The wrapper IS the unit of audit. One signed crossing emitted to the route context, signed by the wrapper's identity. Sub-boundaries run internally, produce signals, but their crossings are not appended to the outer context — implementation detail. Halts from any sub-boundary become the wrapper's halt verbatim (same payload, same type_addr). Pure-chain wrappers don't need #call; conditional ones implement #call and use dispatch(:sub_name, **args) from the mixin.

File layout: lib/<service>/boundaries/<name>.rb. boot_load_boundaries requires every .rb under boundary_path: at boot. Boundaries are registered globally by symbol — two with the same name silently overwrite. Grep boundary :<name> across the workspace before adding one.

Env declaration: a boundary that needs ENV vars declares them on registration so the value flows through the same audit/snapshot path the rest of the framework uses:

boundary :read_node,
  ...
  env: ["OCULUS_BASE_URL", "OCULUS_USER", "OCULUS_PASS"]
  # or rich form: { OCULUS_BASE_URL: { default: "https://i.loss.dev", description: "..." } }

The env_snapshot boot boundary auto-extends the allowlist with these keys. Anti-pattern: reading ENV["FOO"] directly inside call — bypasses the snapshot, the allowlist, the defaults, and the trace.

Adapters

Kinds today — exhaustive. Don't invent.

Kind Provider Wraps Call surface Required spec
http core Net::HTTP .get(url, headers:), .post(url, body:, headers:), .request(method, url, body:, headers:){"status", "body", "headers"} none (mode defaults to live)
shell core Open3.capture3 against ONE fixed binary .run(*args, cwd:, env:){exit_code:, stdout:, stderr:, duration_s:} (symbol keys) command: (e.g. mvn, docker, git)
secret core dotenv-style stores .fetch(key), .key?, .keys, .resolve(template) provider: (e.g. dotenv); modes live/mock only
aws aws-pack Aws::<Service>::Client (method_missing forwards SDK) the SDK's call surface (.describe_instances(...), returns SDK response struct) service: + region:

Config.yml shape (kind defaults to adapter name; mode defaults to live):

adapters:
  http:                                    # name `:http`, kind defaults to "http"
    mode: !Env { name: WANDERLAND_HTTP_MODE, default: live }
    cassette_dir: spec/fixtures/cassettes
  mvn:
    kind: shell
    command: mvn
    baseline_env: { JAVA_HOME: /opt/java }
  api:
    kind: http                             # name `:api`, kind `http` — one process, two HTTP mounts
    mode: live
  ec2:
    kind: aws
    service: ec2
    region: us-east-1

The four modes:

Mode env override: mode: !Env { name: WANDERLAND_HTTP_MODE, default: live }. Resolves at config-load (static for runtime lifetime).

Cassettes: HTTP path <cassette_dir>/http/<sha1>.yml; SHA1 over canonical (method, url, body, headers). Shell adds (cwd, env-subset, sequence); AWS adds (operation, params, sequence). Record via --wanderland-record-to <dir> (CLI) or X-Wanderland-Record-To: <dir> header. Only fires in :live mode; other modes pass through with_recording.

!Fixture <path> lifts a cassette as a {request:, response:} envelope (usable in scenario mocks:). !Merge [base, overlay] deep-merges. !Patch [base, {dot.path: value}] writes single paths into a base.

Wrap order: BoundaryInput#adapter wraps in order — recording innermost (with_recording), then dry-run outermost (with_dry_run). Both no-op unless adapter's @mode == :live. Wrapped result is memoized per BoundaryInput, so sequence counters stay shared across multiple input.adapter(:name) calls.

#client escape hatch: returns the bare gem handle (Net::HTTP for the last live HTTP request; Aws::*::Client always for AWS; Struct(command:, baseline_env:) for shell — not the full Open3 handle).

HTTP basic auth: no native helper. Encode user:pass in Base64 and pass headers: { "Authorization" => "Basic <base64>" }. Ruby 3.4 needs require "base64".

Registering a new kind (only when truly no existing kind fits — first grep register_kind across all three packs):

Wanderland::Boundaries::BootMountAdapters.register_kind("mykind") do |spec, config_dir|
  build_adapter_instance_from(spec, config_dir)
end

Core kinds register at the bottom of boot_mount_adapters.rb. Packs register from their entry file. Unknown kind at boot raises with the known list; per-adapter failures land in Diagnostics and boot continues.

Configuration & routes

config.yml top-level keys:

Route shape:

routes:
  /read-node/:slug:
    method: get
    name: read-node                        # CLI subcommand name; /inspect/route/:name key
    boundary: read_node                    # single boundary; or `chain: [name, {boundary:, args:, when:}, ...]`
    args:
      base_url: !Env { name: OCULUS_BASE_URL, default: https://i.loss.dev }

Boot chain — defined in lib/wanderland/runtime.yml, executes against a @boot_context. One bad slot lands in Diagnostics; the rest continue.

After boot: runtime.engine.compiled_routes is the command table; named_routes indexes by name:; runtime.context_seeds carries every boot crossing tagged context_seed.

Dispatch flow: adapter parses protocol → resolves route from compiled_routesDispatch.invoke(runtime, route, params:, headers:, path:, adapter:) builds the envelope (Envelope.build(:request) — seven-key validated hash plus optional identity), allocates a fresh Context (:trace:req-<uuid>), re-seeds from runtime.context_seeds, runs the adapter-announce boundary (adapter_http / adapter_cli), then route[:dispatcher].execute(input, ctx) walks each slot: effective when: (slot → boundary when_shape:BASE_DEFAULT_WHEN) → Boundary.execute(name, input)context.append(crossing). Terminal crossing returns to the adapter, which renders via Response.from(crossing).

Flow control: type-address namespaces — :types:* normal, :signals:stop:* blocks (BASE_DEFAULT_WHEN skips downstream until count returns to zero via paired :anti:<type> crossings), :signals:pass:* observational. No _halt short-circuit — the chain walker visits every slot; gating is by when: count, not by exception. Exception handlers are later slots whose when: matches :signals:stop: count > 0; when: { always: true } is finally-style.

Context (input["context"]): append-only event stack of every crossing this request. Dual projection — context["key"] is the value (last writer wins across result hashes); context.<boundary> returns the most recent matching crossing. context["env"] short-circuits to runtime.env_snapshot (boot-captured, frozen). Compose filters: context.signed.for_boundary(:auth).by_identity("x").since(5)["scopes"].

Three invocation flavors:

Always-mounted CORE routes (Engine::CORE_ROUTES; site can't remove, only override handlers):

All also surface as CLI subcommands.

Diagnostic recipes:

Archetypes, templates, injections

An archetype is a YAML file shipped by a pack declaring a named pipeline shape: ordered slots, each a {name, boundary, args}. User picks one with top-level or per-route archetype: <name>. Boundary fields can be !UserConfig pipeline.stages.<stage>.type-style thunks that resolve against the user's config at boot.

Iterator slotsiterate_artifacts (registered as artifacts) and iterate_components (as component) — walk a bucket (pipeline.artifacts.<bucket> or pipeline.components), dispatch each entry's type: actor with entry["args"] + {_name: <key>}, and append each sub-dispatch crossing to input["context"] so later slots can read sub-results.

Halt propagation: crossings with type_addr starting :signals:stop: are halts. Iterators halt_passthrough — extract status/error from sub-crossing's result, re-emit as own halt.

Archetypes today (in pipelines-pack):

Pipeline data shape:

pipeline:
  metadata:
    name: <string>
    tags: { system:, environment:, sub_environment:, version: }
  artifacts:
    builds:                                # iterator bucket; {} = noop
      <name>:
        type: mvn                          # actor; compile_jar → publish_jar
        args: { source_path:, repository_url:, repository_id: }
    images:
      <name>:
        type: docker_image                 # actor; docker_build → publish_image
        args: { context_path:, image_name:, image_tag:, docker_build_args: }
  stages:
    build:  { type: artifacts }
    image:  { type: artifacts }
    deploy: { type: component, args: { output_root: ... } }
  components:                              # iterator bucket for deploy stage
    <name>:
      type: ecs_service_deploy | compose_deploy
      args: { task_definition:, service:, wait_timeout_s: }

Each entry's _name (its hash key) is auto-spliced into the per-iteration args.

YAML resolver family (tag → registered phase):

Tag Phase Argument shapes Returns
!Env <NAME> / !Env { name:, default: } :early string raises if unset; hash falls back ENV[NAME] or default
!Environment <path> :early dotted path environments.<active>.<path> (then environments.default.<path>); <active> from top-level wanderland_environment:
!Reference <dotted.path> :early path into the loading doc deep-dup of subtree, recursively resolved
!UserConfig <path> both path into UserConfig.data nil on miss (silent — typos vanish)
!Fixture <path> [at] :late file + optional dig YAML fixture; ERB pre-interpolates <%= … %>
!Merge [layer, layer, …] :late array of hashes HashUtils.deep_merge left-to-right; arrays replace
!Patch [base, overrides] :late base + {dot.path: value} writes base with dotted-path writes, auto-vivifying hashes
!Oculus / !Task :late path remote API fetch via HttpProvider

!Env!Environment. !Env reads ENV[NAME]. !Environment digs the active block of the loading config (and requires wanderland_environment: at the top level, often !Env { name: WANDERLAND_ENVIRONMENT, default: local }). Different concepts.

ERB escape <%= ... %> runs before YAML parses — two places: (1) Config.load over the whole config.yml with EnvBinding resolving names to ENV[NAME.upcase]; (2) !Fixture over fixture file bodies. Use ERB for pre-YAML text substitution (interpolating a name into a YAML key); use !Env for value substitution (the common case — also gets tracked in env_snapshot).

**There is no ${VAR}` substitution inside wanderland.** That's a JCasC / Kustomize / shell layer one level out. Vivarta-jenkins's JCasC config uses `${AGENT_IMAGE_REF} because Jenkins reads it; wanderland's analogue inside vivarta.yml is !Env { name: AGENT_IMAGE_REF } or <%= AGENT_IMAGE_REF %>.

Injections: the user-declared chain (from archetype slots) is folded through InjectionRegistry#apply. Each injection is {boundary, position}: :first / :last / :interleave / {interleave: <shape>} / {before: <name>} / {after: <name>}. Application is a left fold — late :last wins the tail; interleave only sees what came before in the registry. Framework defaults: enforce_denials (interleave), then verify_route / trace_emit / format / seal (last).

Runtime services (adapters, identity, context) don't ride via injections — they're built into the input envelope by Dispatch.build_input (framework keys) and the chain walker (context, args per-slot). input.adapter(:name) looks up on runtime.adapters gated by the boundary's declared adapters: [...].

Scenarios & testing

Red→green is the discipline for wanderland-core changes. Failing scenarios precede the code change. Adding a new feature: write the spec, watch it fail for the right reason, implement, watch it pass. Changing existing behavior: update the spec that pinned the old behavior, watch it fail, implement the new behavior, watch it pass. Skipping this step is not optional — "or it doesn't count."

The wanderland-core repo holds two test patterns side by side. Use them both.

1. RSpec unit/feature specs at wanderland-core/spec/wanderland/<topic>_spec.rb. The spec_helper boots a fresh Runtime per example and resets Boundary.registered between specs. Inline test boundaries register on the runtime overlay:

# spec/wanderland/boundary_input_spec.rb
require "spec_helper"

RSpec.describe Wanderland::BoundaryInput do
  describe "#find_or_fail" do
    it "throws a halt Signal when the path is blank" do
      input = described_class.new({ "params" => {} }, {})
      signal = catch(:wanderland_signal) { input.find_or_fail("params.name") }
      expect(signal).to be_a(Wanderland::Signal)
      expect(signal.type_addr).to eq(Wanderland::Types::HALT)
      expect(signal.payload).to eq("status" => 400, "error" => "name is required")
    end
  end
end

Use these for: DSL surface, Registration shape, signal throw/catch, adapter wrapping order, Slot.from_spec, anything where a single method is the unit.

2. Scenario YAMLs at wanderland-core/spec/scenarios/<topic>/<NN_name>.yml, run by a matching wanderland-core/spec/wanderland/<topic>_scenarios_spec.rb. The runner calls scenario.verify against the boot-fresh runtime. Each YAML has operation, input, expected, optional mocks, optional blanks (Game-mode puzzles), optional level metadata.

# spec/scenarios/wrapper_chain/01_two_steps.yml
name: "wrapper chain runs sub-boundaries in declaration order"
description: |
  A wrapper boundary declares chain: [:step_one, :step_two].
  Both register on the runtime overlay; step_one emits a token,
  step_two reads it. The wrapper emits one crossing signed with
  its own identity; the sub-boundaries' crossings are not
  appended to the request context.

operation: cfn_verify_test

input:
  params: {}

expected:
  result:
    step_two_saw: "from-step-one"
  context_size: 1
  boundaries: [cfn_verify_test]

Use these for: end-to-end behavior, chain composition, halt propagation across multiple slots, request-shaped flows where the full envelope matters.

Scenario YAML shape — full vocabulary (per wanderland-core/spec/scenarios/):

ShapeMatcher in expected: — full reserved-key set:

Collision rule: if a real response field is literally named count / first / last / keys / empty, the matcher swallows it. Escape via blanks: dot-paths or restructure the response.

Running the suite:

# all wanderland-core specs (excludes :live by default)
cd wanderland-core && bundle exec rspec

# one spec file
bundle exec rspec spec/wanderland/boundary_input_spec.rb

# one scenario topic
bundle exec rspec spec/wanderland/wrapper_chain_scenarios_spec.rb

# include :live specs (spawn external processes; slow)
WANDERLAND_LIVE=1 bundle exec rspec

Running site scenarios (against a project's config.yml):

# all scenarios declared in config.yml's scenarios: block
bundle exec wanderland --type test config.yml

# filter
bundle exec wanderland --type test config.yml --route deploy
bundle exec wanderland --type test config.yml --scenario happy_path

Exit codes: 0 all pass, 1 fail/no-match, 2 bad argv.

Workflow checklist for a wanderland-core change:

Tutorials' scenarios (in a project's config.yml, like vivarta-jenkins's scenarios: block) follow the same shape — --type test is the runner. Same red→green discipline applies when adding a boundary to a tutorial: write the scenario first, watch it miss, then build the boundary.

Style guide

For reader-facing nodes, see [wanderland-dev-style-guide]. Rules that apply to code-style writing too:

For code:

Kubernetes deploy (wanderland-k8s-pack / CoreWeave)

The deploy slot of containerized_deployment targets Kubernetes as well as ECS. wanderland-k8s-pack ships the kustomize/kubectl boundaries; a site mounts a :kubectl shell adapter and names them in the deploy slot instead of the ECS/CFN ones.

Boundary Role
kustomize_set_image Pin an overlay's kustomization.yaml images: entry to a built ref. Pure YAML transform.
kubectl_apply Render + kubectl apply an overlay, optional rollout-restart + wait. load_restrictor: renders with kubectl kustomize then apply -f -. context: targets a named cluster.
k8s_provisioner Deploy-slot wrapper: kustomize_set_image then kubectl_apply as one group. The kubectl analogue of cfn_provisioner.
k8s_live_image Read a running Deployment's image (promotion / deploy-only); prod collect_steps names it, image stage goes noop.
k8s_job Launch primitive: kubectl apply -f - a Job manifest, fire-and-forget or wait: for completion. The kubectl analogue of ECS run-task — launch vs deploy.

Mixed substrate: a deploy slot can interleave a cfn_provisioner component (a Route53 A record to the cluster's nginx-ingress IP) with a k8s_provisioner component (the workload) — AWS DNS + Kubernetes workload in one iterator, DNS first so cert-manager's HTTP-01 challenge resolves.

The fe/be/task-launcher pattern (UI submits work → backend launches a k8s Job → an observability receiver collects it) is documented end to end, with the echo + foxfire reference suite and a laptop-runnable drone, in [canonical-fe-be-task-launcher].

Sharp edges & doc drift

The reference nodes are partly "2b shape" — frozen at an earlier point. Where code disagrees with docs, trust the code. Specific drifts found:

Tutorial-ahead-of-code: render_params is referenced in [vivarta-jenkins-tutorial-cfn-verify] as belonging in wanderland-pipelines-pack but does not exist on disk in either pack. Either tutorial is forward-looking or the boundary is planned. Don't assume it exists.

HTTP dry_run_request key-style bug: returns symbol-keyed hashes while live/replay/mock return string-keyed. Code bug, not just docs.

Fixtures, config mocks & the double-ERB (field notes)

Seeding mock/seed data so a boundary's adapter call returns a canned response — and the traps that each cost a debugging cycle.

Config-level adapter mocks — the direct way. Don't wrap a seed-reading branch inside a boundary. Put the mock on the adapter in config; input.adapter(:name).get(url) returns it:

adapters:
  http:
    kind: http
    mode: !Env { name: WANDERLAND_HTTP_MODE, default: mock }   # mock locally, live deployed
    mocks:
      "GET http://foxfire.mock/envelope/img.recontrast_run":    # key = "<METHOD> <URL>", exact
        status: 200
        body: !Fixture fixtures/envelopes/img.recontrast_run    # the file becomes the body

!Fixture path rules:

resolvers:
  Fixture:
    fixture_dirs: ["."]   # relative to this config; add more roots as needed

The double-ERB trap. wanderland ERB-renders config.yml (at Config.load) AND every !Fixture file whose text contains <%. So a string meant for a later runtime ERB (e.g. Wanderland::Template.render at dispatch) is evaluated too early — it fails on the undefined var (or renders blank). Escape the tags so the load pass emits a literal for the runtime pass:

run_opts:
  - --name
  - "<%%= run_id %>"     # load-ERB emits "<%= run_id %>"; the boundary evaluates it later

<%%=<%= is the standard ERB literal escape. Symptom without it: undefined local variable or method 'run_id' for FixtureBinding at boot.

Running a route from the CLI (+ the --wanderland-* flags)

Every route is also a CLI subcommand — no server to boot. The tightest iterate loop for a chain:

bundle exec wanderland --type cli config.yml <route> key=value key=value
#   e.g.  … preview job_type=img.recontrast_run latency_class=deferred

Path captures become declared options; any leftover key=value argv folds into params. The terminal crossing renders to stdout (via the matched formatter); exit 0 on ok, non-zero on halt. Edit a fixture/config, re-run, read the diff.

Global flags (canonical option name in brackets — behaviour verified against a live route):

Combine them: --wanderland-debug --wanderland-trace to watch the chain and see the signed terminal crossing; --wanderland-render alone to debug resolution before you ever dispatch.

serves: registers a FORMATTER, not a content-type hint (gotcha)

serves: "application/json" on a boundary does not mean "this route returns JSON." It registers the boundary in the formatter registry as the handler for that MIME, so its #call runs on every response of that content type, across all routes. Put it on a normal data endpoint and that boundary intercepts /health and everything else — its #call runs without the args it expects and halts (e.g. a 422), which crashloops the pod via the liveness probe.

Quick lookup

If you need to… Use
Make an HTTP call from a boundary kind: http adapter; input.adapter(:http).get(url, headers:). Basic auth: Authorization: Basic <base64> header.
Run a shell command kind: shell adapter with command: <binary>; input.adapter(:foo).run(*args, cwd:, env:). One adapter mount per binary.
Read a secret kind: secret adapter with provider: dotenv; or address :secrets:<provider>:<key> via .resolve(template).
Call AWS kind: aws adapter with service: and region:. SDK calls go through method_missing.
Iterate over a config bucket and dispatch per entry Iterator boundary pattern (iterate_artifacts / iterate_components). Don't reinvent.
Wrap several sub-boundaries into one user-visible boundary Wrapper pattern: Wanderland::Boundary.execute(:sub, sub_input), context.append(crossing), halt-passthrough on :signals:stop:*.
Read a process env var in config !Env { name: ..., default: ... } (tracked in env_snapshot).
Read an environment-specific value from the loading config !Environment <path> (requires top-level wanderland_environment:).
Splice runtime user data into a config slot !UserConfig <path> (silent on miss).
Replay recorded HTTP/shell/AWS calls in tests Set adapter mode: replay and point cassette_dir: at the cassettes.
Stub one shape per scenario Scenario mocks: block — inline envelope, !Fixture, or !Merge/!Patch overlays.
Add a new adapter kind First grep register_kind everywhere. If truly new: BootMountAdapters.register_kind("name") { ... } in your pack's entry file.
Add a new boundary lib/<service>/boundaries/<name>.rb, include Wanderland::Boundary, boundary :name, .... Grep boundary :<name> first — registry is global.
Add a pipeline stage Fill a slot in the archetype, or write a sub-boundary that an iterator dispatches. Don't write per-stage scaffolding.
Find what's currently registered curl /inspect/boundaries, /inspect/route/:name, /inspect/diagnostics.
Test a boundary --type test config.yml --scenario <case> or rspec via Wanderland::Scenario.rspec_describe!(runtime).
Record a cassette Run with --wanderland-record-to <dir> in live mode; the next live call writes a cassette and subsequent runs replay.

Tips & tricks

Reusable lessons from live pipeline and deploy work. When a failure teaches something the next service will hit too, record it here as well as in any local agent memory — the primer is the shared copy every session reads; a lesson that lives only in one agent's memory store is invisible to everyone else.

Pointers

Tutorials (current shape, useful as code examples):

Reference (flag drift per §Sharp edges):

Source code (authoritative when docs and code disagree):