Liking cljdoc? Tell your friends :D

com.blockether.vis.core

vis - broad host facade.

This is the ONLY namespace extensions, channel adapters, embedded callers, and tests should import. It deliberately re-exports host, registry, runtime, persistence, prompt, diagnostic, and sandbox helpers from com.blockether.vis.internal.*. The internal tree is not stable; the names exposed here are the host contract.

Canonical runtime language: Session -> Turn -> Iteration -> Block.

A Turn is one user request plus assistant answer inside a Session. New code and documentation should use turn/user-request language.

Primary surfaces:

  • Session / turn runtime: create!, send!, turn!, by-id, by-channel, env-for, close!, delete!, set-title!.
  • Environment runtime: create-environment, dispose-environment!, get-router, rebuild-router!, resolve-effective-model.
  • Extension contract: extension, symbol, value, render-prompt, register-extension!, registered-extensions, discovery, reload.
  • Registries: command, channel, provider, and backend registration helpers for host-owned and embedded use.
  • Persistence facade: db-* functions and connection helpers. The implementation namespace / extension slot are spelled persistance; human-facing language is Persistence.
  • Prompt / Python-sandbox / formatting / cancellation / notifications / doctor helpers shared by channels and extensions.

Not every export is equally high-level. send!, extension maps, registry builders, read-side persistence helpers, and Markdown export are the preferred integration surface. Low-level sandbox, parse-repair, dispatcher, and write-side db helpers are exported because this is a host facade, but ordinary extensions should avoid depending on them unless they are implementing host-level behavior.

Binary entry: -main (invoked by clojure -M:vis, AOT'd to a Java entry class via :gen-class for the GraalVM native-image build).

vis - broad host facade.

This is the ONLY namespace extensions, channel adapters, embedded
callers, and tests should import. It deliberately re-exports host,
registry, runtime, persistence, prompt, diagnostic, and sandbox
helpers from `com.blockether.vis.internal.*`. The internal tree is
not stable; the names exposed here are the host contract.

Canonical runtime language:
  Session -> Turn -> Iteration -> Block.

A Turn is one user request plus assistant answer inside a Session.
New code and documentation should use turn/user-request language.

Primary surfaces:
  - Session / turn runtime: create!, send!, turn!, by-id,
    by-channel, env-for, close!, delete!, set-title!.
  - Environment runtime: create-environment, dispose-environment!,
    get-router, rebuild-router!, resolve-effective-model.
  - Extension contract: extension, symbol, value, render-prompt,
    register-extension!, registered-extensions, discovery, reload.
  - Registries: command, channel, provider, and backend registration
    helpers for host-owned and embedded use.
  - Persistence facade: db-* functions and connection helpers. The
    implementation namespace / extension slot are spelled
    `persistance`; human-facing language is Persistence.
  - Prompt / Python-sandbox / formatting / cancellation /
    notifications / doctor helpers shared by channels and extensions.

Not every export is equally high-level. `send!`, extension maps,
registry builders, read-side persistence helpers, and Markdown export
are the preferred integration surface. Low-level sandbox, parse-repair,
dispatcher, and write-side db helpers are exported because this is a
host facade, but ordinary extensions should avoid depending on them
unless they are implementing host-level behavior.

Binary entry: -main (invoked by `clojure -M:vis`, AOT'd to a Java
entry class via `:gen-class` for the GraalVM native-image build).
raw docstring

com.blockether.vis.internal.agents

Project-guidance discovery — internal, no extension required.

STACKED context files, pi-style: guidance is collected from THREE layers and all of them ride into the PROJECT-INSTRUCTIONS system block, outermost first:

  1. user-global ~/.vis/AGENTS.md (or ~/.vis/CLAUDE.md)
  2. ancestors AGENTS.md / CLAUDE.md in every ancestor directory of the workspace root (outermost first)
  3. workspace AGENTS.md / CLAUDE.md at the workspace root
  4. added roots AGENTS.md / CLAUDE.md at each ADDED filesystem root's own directory (folders granted beyond the primary workspace — no ancestor walk)

Per DIRECTORY precedence is strict: AGENTS.md wins; CLAUDE.md is only consulted when AGENTS.md is absent in that directory. Across directories nothing is dropped — nearer files are rendered LATER so they positionally override outer rules on conflict.

Size policy: NO truncation. Every file goes into the system prompt verbatim. Provider prompt caching amortizes the cost across every iter in the session; trimming would risk dropping the very rule the user is testing. The cwd + per-file (path, mtime, length) marker cache below ensures files are re-read at most once per change.

Failure modes (file unreadable, permissions, I/O error) land in the read-warning vec, NOT in the rendered prompt. The model isn't bound by rules it can't see, but the host knows something is broken.

This namespace replaces the foundation-core/environment/agents.clj that used to live in the extension. Project-guidance discovery is core functionality (drives the system prompt + slim ctx digest); the extension layer no longer owns it.

Project-guidance discovery — internal, no extension required.

STACKED context files, pi-style: guidance is collected from THREE
layers and all of them ride into the PROJECT-INSTRUCTIONS system
block, outermost first:

  1. user-global   `~/.vis/AGENTS.md` (or `~/.vis/CLAUDE.md`)
  2. ancestors     `AGENTS.md` / `CLAUDE.md` in every ancestor
                   directory of the workspace root (outermost first)
  3. workspace     `AGENTS.md` / `CLAUDE.md` at the workspace root
4. added roots   `AGENTS.md` / `CLAUDE.md` at each ADDED filesystem
                 root's own directory (folders granted beyond the
                 primary workspace — no ancestor walk)

Per DIRECTORY precedence is strict: AGENTS.md wins; CLAUDE.md is
only consulted when AGENTS.md is absent in that directory. Across
directories nothing is dropped — nearer files are rendered LATER so
they positionally override outer rules on conflict.

Size policy: NO truncation. Every file goes into the system prompt
verbatim. Provider prompt caching amortizes the cost across every
iter in the session; trimming would risk dropping the very rule the
user is testing. The cwd + per-file (path, mtime, length) marker
cache below ensures files are re-read at most once per change.

Failure modes (file unreadable, permissions, I/O error) land in the
read-warning vec, NOT in the rendered prompt. The model isn't bound
by rules it can't see, but the host knows something is broken.

This namespace replaces the foundation-core/environment/agents.clj
that used to live in the extension. Project-guidance discovery is
core functionality (drives the system prompt + slim ctx digest); the
extension layer no longer owns it.
raw docstring

com.blockether.vis.internal.attachment-storage

Attachment storage-offload rail: a registry of storage BACKENDS plus the pure OFFLOAD DECISION that routes one attachment's payload either INLINE (bytes in the session_attachment.bytes BLOB) or EXTERNAL (bytes handed to a backend, which returns a storage_uri -- scheme://... -- kept in the row instead).

Zero SQL, zero schema change: the V4 session_attachment table already carries a nullable storage_uri with an exactly-one(bytes, storage_uri) CHECK. This namespace only decides WHICH of the two a given attachment takes, PUTs/GETs the external bytes through the scheme-dispatched backend, and hydrates a read-back envelope's :base64 from its :storage-uri on demand.

The decision is a PURE predicate hot? AND size (see default-offload?): an image replays to a vision model every turn its iteration stays live, so it is HOT -- kept inline even when large; a non-image artifact (PDF/CSV/wav/ download) is fetched at most once by a human, so it is COLD -- a good offload candidate past a size floor. A backend may override the predicate wholesale via :storage/offload?.

Precedence, engine-owned so the loop never learns a storage dialect:

  1. active backend's :storage/offload? (the backend knows its own cost)
  2. else default-offload? (engine default policy)
  3. no active backend -> always inline (zero regression)
Attachment storage-offload rail: a registry of storage BACKENDS plus the
pure OFFLOAD DECISION that routes one attachment's payload either INLINE
(bytes in the `session_attachment.bytes` BLOB) or EXTERNAL (bytes handed to
a backend, which returns a `storage_uri` -- `scheme://...` -- kept in the row
instead).

Zero SQL, zero schema change: the V4 `session_attachment` table already
carries a nullable `storage_uri` with an exactly-one(bytes, storage_uri)
CHECK. This namespace only decides WHICH of the two a given attachment takes,
PUTs/GETs the external bytes through the scheme-dispatched backend, and
hydrates a read-back envelope's `:base64` from its `:storage-uri` on demand.

The decision is a PURE predicate `hot? AND size` (see `default-offload?`):
an image replays to a vision model every turn its iteration stays live, so it
is HOT -- kept inline even when large; a non-image artifact (PDF/CSV/wav/
download) is fetched at most once by a human, so it is COLD -- a good offload
candidate past a size floor. A backend may override the predicate wholesale
via `:storage/offload?`.

Precedence, engine-owned so the loop never learns a storage dialect:
  1. active backend's `:storage/offload?`  (the backend knows its own cost)
  2. else `default-offload?`               (engine default policy)
  3. no active backend                     -> always inline (zero regression)
raw docstring

com.blockether.vis.internal.attachments

User-message image attachments.

Dropping a file onto the terminal pastes its PATH into the input (the terminal's drop behavior — same mechanism pi relies on). At turn start the engine scans the user message for path-shaped tokens that resolve to real image files, reads them, and attaches them to the initial user message as multimodal content blocks. Channel-neutral: every channel gets the same behavior because the scan runs in the engine, not the channel.

Only files the model can genuinely consume are attached: the MIME type is sniffed from magic bytes (pi-parity: jpeg / non-animated png / gif / webp / bmp), never trusted from the extension alone. Files over max-image-bytes are skipped with a reason the prompt assembler surfaces to the model (providers downscale server-side; vis does no client-side resizing — AWT/ImageIO is unavailable under GraalVM native-image on macOS).

User-message image attachments.

Dropping a file onto the terminal pastes its PATH into the input (the
terminal's drop behavior — same mechanism pi relies on). At turn start
the engine scans the user message for path-shaped tokens that resolve
to real image files, reads them, and attaches them to the initial user
message as multimodal content blocks. Channel-neutral: every channel
gets the same behavior because the scan runs in the engine,
not the channel.

Only files the model can genuinely consume are attached: the MIME type
is sniffed from magic bytes (pi-parity: jpeg / non-animated png / gif /
webp / bmp), never trusted from the extension alone. Files over
`max-image-bytes` are skipped with a reason the prompt assembler
surfaces to the model (providers downscale server-side; vis does no
client-side resizing — AWT/ImageIO is unavailable under GraalVM
native-image on macOS).
raw docstring

com.blockether.vis.internal.cancellation

Cancellation token - leaf module.

The cancellation token is a tiny two-atom record that lets a UI thread (TUI, REPL caller) cooperatively abort an in-flight turn! AND interrupt the worker future hosting the blocking provider call. The cooperative side is checked at every iteration boundary; the future side hard-cancels any HTTP call that has already started.

Public API:

(cancellation-token) - fresh token (cancellation-atom token) - cooperative flag atom (pass to turn!) (cancellation-set-future! token fut) - register the worker future (cancel! token) - set flag + interrupt registered future (cancelled? token) - true once cancel! has been called (cancellation? throwable) - true if exception was caused by cancel!

This namespace has zero side effects at load time and depends only on Java interop - channels and the runtime can require it directly without pulling in the rest of the SDK.

Cancellation token - leaf module.

The cancellation token is a tiny two-atom record that lets a UI
thread (TUI, REPL caller) cooperatively abort an
in-flight `turn!` AND interrupt the worker future hosting the
blocking provider call. The cooperative side is checked at every
iteration boundary; the future side hard-cancels any HTTP call
that has already started.

Public API:

  `(cancellation-token)`       - fresh token
  `(cancellation-atom token)`  - cooperative flag atom (pass to `turn!`)
  `(cancellation-set-future! token fut)` - register the worker future
  `(cancel! token)`            - set flag + interrupt registered future
  `(cancelled? token)`         - true once `cancel!` has been called
  `(cancellation? throwable)`  - true if exception was caused by `cancel!`

This namespace has zero side effects at load time and depends only
on Java interop - channels and the runtime can require it
directly without pulling in the rest of the SDK.
raw docstring

com.blockether.vis.internal.channel-events

Process-local channel event bus.

Extensions use this to talk to mounted channels without depending on their implementation namespaces. Channels subscribe while running and translate events into their local state/events. No listener failure may take down the publisher or sibling listeners.

Process-local channel event bus.

Extensions use this to talk to mounted channels without depending on their
implementation namespaces. Channels subscribe while running and translate
events into their local state/events. No listener failure may take down the
publisher or sibling listeners.
raw docstring

com.blockether.vis.internal.commandline

CLI command parsing, lookup, help rendering, dispatch.

The command spec, builder, and global registry now live in com.blockether.vis.internal.registry alongside the channel and provider registries. This namespace provides the operations OVER those command maps:

Lookup find-leaf walk the tree consuming tokens until a match find-named same, but ignoring the root command's name

Argument parsing parse-args parse residual tokens against :cmd/args validate-args nil on success, error string on missing required

Help rendering render-command detailed help for one command render-tree top-level overview shown on vis with no args color-enabled? dynamic toggle (TTY auto-detect) pad-right width-padding helper pad-left width-padding helper

Dispatch dispatch! resolve, parse, validate, invoke :cmd/run-fn

The registry surface (command, register-cmd!, deregister-cmd!, registered-commands, registered-under, resolve-subcommands) is reachable through com.blockether.vis.internal.registry.

CLI command parsing, lookup, help rendering, dispatch.

The command spec, builder, and global registry now live in
`com.blockether.vis.internal.registry` alongside the channel and provider
registries. This namespace provides the operations OVER those
command maps:

  Lookup
    find-leaf       walk the tree consuming tokens until a match
    find-named      same, but ignoring the root command's name

  Argument parsing
    parse-args      parse residual tokens against `:cmd/args`
    validate-args   nil on success, error string on missing required

  Help rendering
    render-command  detailed help for one command
    render-tree     top-level overview shown on `vis` with no args
    *color-enabled?*  dynamic toggle (TTY auto-detect)
    pad-right       width-padding helper
    pad-left        width-padding helper

  Dispatch
    dispatch!       resolve, parse, validate, invoke `:cmd/run-fn`

The registry surface (`command`, `register-cmd!`, `deregister-cmd!`,
`registered-commands`, `registered-under`, `resolve-subcommands`)
is reachable through `com.blockether.vis.internal.registry`.
raw docstring

com.blockether.vis.internal.config

Configuration: paths, JVM lifecycle, provider presets, svar-native coercion, config file I/O, and the active-provider state every channel reads through.

Two halves:

  • On-disk config under ~/.vis/: state.yml (machine-written), vis.mdb/, vis.log. init! / init-cli! / shutdown! redirect stdout/stderr into the log file and bring up Telemere's file handler.
  • Live process state: the active-config atom holds the currently-selected provider config; current-config, active-provider, active-model, provider-ids, has-provider? are the read API. reload-config! re-reads from disk.

The ->svar-provider helper resolves :api-key lazily by calling the registered provider's :provider/get-token-fn, so the token-refresh policy stays inside each provider implementation instead of leaking up here.

Configuration: paths, JVM lifecycle, provider presets, svar-native
coercion, config file I/O, and the active-provider state every
channel reads through.

Two halves:

  - On-disk config under `~/.vis/`: `state.yml` (machine-written), `vis.mdb/`, `vis.log`.
    `init!` / `init-cli!` / `shutdown!` redirect stdout/stderr into
    the log file and bring up Telemere's file handler.
  - Live process state: the `active-config` atom holds the
    currently-selected provider config; `current-config`,
    `active-provider`, `active-model`, `provider-ids`,
    `has-provider?` are the read API. `reload-config!` re-reads
    from disk.

The `->svar-provider` helper resolves `:api-key` lazily by calling
the registered provider's `:provider/get-token-fn`, so the
token-refresh policy stays inside each provider implementation
instead of leaking up here.
raw docstring

com.blockether.vis.internal.config-spec

The executable contract for the YAML representation of Vis configuration.

YAMLStar returns maps with string keys. This namespace validates that exact representation: snake_case string keys ONLY (kebab-case is rejected), no recursive keywordization, and no acceptance of keyword-keyed lookalikes. Maps are closed unless their keys are deliberately user-defined (environment variables, headers, toggle ids, MCP server names, pricing/model tables, and provider request bodies).

Security consumers derive their internal policy maps through the adapters at the end of this namespace, so validation and enforcement share one contract.

The executable contract for the YAML representation of Vis configuration.

YAMLStar returns maps with string keys. This namespace validates that exact
representation: snake_case string keys ONLY (kebab-case is rejected), no
recursive keywordization, and no acceptance of keyword-keyed lookalikes.
Maps are closed unless their keys are
deliberately user-defined (environment variables, headers, toggle ids, MCP
server names, pricing/model tables, and provider request bodies).

Security consumers derive their internal policy maps through the adapters at
the end of this namespace, so validation and enforcement share one contract.
raw docstring

com.blockether.vis.internal.content

Canonical role-labelled message and typed content-block contract.

This is the only persisted and transported answer shape. Maps use JSON-ready snake_case string keys so in-process and remote consumers observe identical values. Markdown exists only as the payload of a prose block.

Canonical role-labelled message and typed content-block contract.

This is the only persisted and transported answer shape. Maps use JSON-ready
snake_case string keys so in-process and remote consumers observe identical
values. Markdown exists only as the payload of a prose block.
raw docstring

com.blockether.vis.internal.ctx-engine

Pure helpers for the model-facing context snapshot.

The mutable session state lives outside this namespace; these functions only advance the turn/iteration cursor, project form envelopes for persisted result messages, and compute utilization metadata.

Pure helpers for the model-facing context snapshot.

The mutable session state lives outside this namespace; these functions only
advance the turn/iteration cursor, project form envelopes for persisted
result messages, and compute utilization metadata.
raw docstring

com.blockether.vis.internal.ctx-loop

Loop integration layer for context management.

The loop keeps a per-session :ctx-atom for stable model-facing context and a separate :turn-state-atom for live turn/iteration/form counters. This namespace stamps the cursor, enriches context with env/resources/routing, and renders the standing context block.

Loop integration layer for context management.

The loop keeps a per-session `:ctx-atom` for stable model-facing context
and a separate `:turn-state-atom` for live turn/iteration/form counters.
This namespace stamps the cursor, enriches context with env/resources/routing,
and renders the standing context block.
raw docstring

com.blockether.vis.internal.ctx-renderer

Pure renderer for the standing agent-facing session snapshot.

render-ctx-static projects the session view and serializes it as a Python literal with pure JVM code. The live sandbox session dict is built from the same boundary projection, and render-ctx-delta emits executable updates.

Pure renderer for the standing agent-facing `session` snapshot.

`render-ctx-static` projects the session view and serializes it as a Python
literal with pure JVM code. The live sandbox `session` dict is built from the
same boundary projection, and `render-ctx-delta` emits executable updates.
raw docstring

com.blockether.vis.internal.docs

Embedded documentation subsystem.

Any artifact on the classpath may ship docs by placing markdown under resources/vis-docs/ with a resources/vis-docs/vis-docs.edn manifest:

{:site {:title "Vis" :tagline "…" :repo "https://…"} ; optional, site-level :pages [{:file "index.md" :title "Introduction" :section nil :order 0} {:file "x.md" :title "X" :section "Runtime" :order 30}]}

Every vis-docs/vis-docs.edn on the classpath is discovered (the same per-artifact auto-discovery native-image config uses), so an extension adds a page by dropping a markdown file + a manifest entry — no central registry.

One renderer, two outputs:

  • build-site! writes a static, themed HTML bundle (for GitHub Pages).
  • handle serves the same pages live (HTMX nav), mountable on the gateway via its :gateway.slot/http-routes slot.

Markdown → HTML uses commonmark-java (already a dependency); the theme is an enterprise-grade docs layout (sticky header, sidebar, on-this-page rail) in the VIS palette (cobalt on white, Hanken Grotesk + JetBrains Mono).

Embedded documentation subsystem.

Any artifact on the classpath may ship docs by placing markdown under
`resources/vis-docs/` with a `resources/vis-docs/vis-docs.edn` manifest:

  {:site  {:title "Vis" :tagline "…" :repo "https://…"}   ; optional, site-level
   :pages [{:file "index.md" :title "Introduction" :section nil :order 0}
           {:file "x.md"     :title "X"            :section "Runtime" :order 30}]}

Every `vis-docs/vis-docs.edn` on the classpath is discovered (the same
per-artifact auto-discovery native-image config uses), so an extension adds a
page by dropping a markdown file + a manifest entry — no central registry.

One renderer, two outputs:
  * `build-site!` writes a static, themed HTML bundle (for GitHub Pages).
  * `handle` serves the same pages live (HTMX nav), mountable on the gateway
    via its `:gateway.slot/http-routes` slot.

Markdown → HTML uses commonmark-java (already a dependency); the theme is an
enterprise-grade docs layout (sticky header, sidebar, on-this-page rail) in
the VIS palette (cobalt on white, Hanken Grotesk + JetBrains Mono).
raw docstring

com.blockether.vis.internal.doctor

Doctor protocol: aggregates :ext/doctor-fn from every registered extension into a single cross-cutting diagnostic surface. vis doctor invokes run-checks then format-output + exit-code.

Plan §1 Q19 + §10:

  • One fn per extension. The fn returns a seq of message maps; the extension self-stamps :check-id on each message when it wants the formatter's per-section prefix.
  • Output ordering: extensions in registration order; messages in fn-return order. Levels NOT re-sorted within a section - cause-and-effect narrative preserved.
  • Activation contract: the fn runs for EVERY registered extension regardless of :ext/activation-fn. Doctor fns must defensively handle missing env keys.
  • Exit codes: 0 if only :info or empty; 1 if any :warn (no :error); 2 if any :error.
  • TTY-detected ANSI colors. UTF-8 icons by default.
Doctor protocol: aggregates `:ext/doctor-fn` from every
registered extension into a single cross-cutting diagnostic
surface. `vis doctor` invokes [[run-checks]] then
[[format-output]] + [[exit-code]].

Plan §1 Q19 + §10:
  - One fn per extension. The fn returns a seq of message maps;
    the extension self-stamps `:check-id` on each message when it
    wants the formatter's per-section prefix.
  - Output ordering: extensions in registration order; messages
    in fn-return order. Levels NOT re-sorted within a section -
    cause-and-effect narrative preserved.
  - Activation contract: the fn runs for EVERY registered
    extension regardless of `:ext/activation-fn`. Doctor fns must
    defensively handle missing env keys.
  - Exit codes: 0 if only :info or empty; 1 if any :warn (no
    :error); 2 if any :error.
  - TTY-detected ANSI colors. UTF-8 icons by default.
raw docstring

com.blockether.vis.internal.egress-proxy

Gateway-hosted loopback EGRESS PROXY — the one door a jailed shell child may use to reach the network. Paired with the OS jail's net-off-except-loopback wall (process-jail), it turns vis.yml :network (allowed/denied domains + verb/path :rules) into REAL enforcement for shell children — curl, wget, a script, subprocess — not a cooperative env-var hint.

How the wall + door fit together:

  • The jail denies ALL sockets except TCP to 127.0.0.1:<this-port>, so a raw socket() / /dev/tcp in a script has nowhere to go — the kernel, not politeness, forces every byte through here.
  • http_proxy/https_proxy/ALL_PROXY point the child's HTTP clients at this port, so ordinary tools proxy voluntarily; the wall covers the rest.

What it enforces (no MITM — no new deps, no CA):

  • Plain HTTP (absolute-form proxy request): FULL host + method + path, because the request line is cleartext. GET-not-POST works here.
  • HTTPS (CONNECT host:443): HOST allow/deny always; FULL method + path too when a MITM capability is supplied and the policy asks for it (:mitm?) — the proxy terminates the child's TLS with an ephemeral per-host leaf cert (see internal.tls-mitm), reads the real verb/path, then re-encrypts to the real upstream (whose real cert it still validates). Without MITM it is a raw byte tunnel (verb opaque) — the documented CONNECT-only ceiling.

The policy is a plain VALUE (per session), read fresh per connection via policy-fn so /reload + config edits take effect with no restart. One request per upstream connection (we force Connection: close), so HTTP keep-alive can't smuggle a second, unfiltered verb onto an already-approved socket.

Gateway-hosted loopback EGRESS PROXY — the one door a jailed shell child may use
to reach the network. Paired with the OS jail's *net-off-except-loopback* wall
(`process-jail`), it turns vis.yml `:network` (allowed/denied domains + verb/path
`:rules`) into REAL enforcement for shell children — `curl`, `wget`, a script,
`subprocess` — not a cooperative env-var hint.

How the wall + door fit together:
  - The jail denies ALL sockets except TCP to `127.0.0.1:<this-port>`, so a raw
    `socket()` / `/dev/tcp` in a script has nowhere to go — the kernel, not
    politeness, forces every byte through here.
  - `http_proxy`/`https_proxy`/`ALL_PROXY` point the child's HTTP clients at this
    port, so ordinary tools proxy voluntarily; the wall covers the rest.

What it enforces (no MITM — no new deps, no CA):
  - Plain HTTP  (absolute-form proxy request): FULL host + method + path, because
    the request line is cleartext. GET-not-POST works here.
  - HTTPS (`CONNECT host:443`): HOST allow/deny always; FULL method + path too when
    a MITM capability is supplied and the policy asks for it (`:mitm?`) — the proxy
    terminates the child's TLS with an ephemeral per-host leaf cert (see
    `internal.tls-mitm`), reads the real verb/path, then re-encrypts to the real
    upstream (whose real cert it still validates). Without MITM it is a raw byte
    tunnel (verb opaque) — the documented CONNECT-only ceiling.

The policy is a plain VALUE (per session), read fresh per connection via `policy-fn`
so `/reload` + config edits take effect with no restart. One request per upstream
connection (we force `Connection: close`), so HTTP keep-alive can't smuggle a second,
unfiltered verb onto an already-approved socket.
raw docstring

com.blockether.vis.internal.env-digest

Slim "session_env" digest. Internal, not extension-owned. STRING-KEYED — crosses the Python boundary as session["env"], so keys AND enum values (os/shell/kind/primary_language) are strings, never keywords.

Produces a bounded map the model reads each iter:

{"host" {"os" "shell" "clock"} ; cwd lives in session["workspace"]["root"] "project" {"kind" "primary_language"} "extensions" {"active_count" "aliases"}}

Each slice is small (~50 bytes), so the section costs <200 bytes/turn. Extensions deep-merge their own slices via :ext/ctx-fn returning {"session_env" {their-key {…}}}; the merge happens in ctx-loop/render-block! so internal owns the base section, extensions layer on top.

Heavy environment scans (full byte-counted language tables, polylith brick listings, multi-repo git status) live in the foundation-core focused project-shape helpers for explicit deep-dives. The digest never calls into extensions — host facts come from System/getProperty, project shape from a single directory peek. AGENTS.md / CLAUDE.md contents ride in their own system block (internal.prompt), not here.

Slim `"session_env"` digest. Internal, not extension-owned. STRING-KEYED —
crosses the Python boundary as `session["env"]`, so keys AND enum values
(os/shell/kind/primary_language) are strings, never keywords.

Produces a bounded map the model reads each iter:

  {"host"       {"os" "shell" "clock"}   ; cwd lives in session["workspace"]["root"]
   "project"    {"kind" "primary_language"}
   "extensions" {"active_count" "aliases"}}

Each slice is small (~50 bytes), so the section costs <200 bytes/turn.
Extensions deep-merge their own slices via `:ext/ctx-fn` returning
`{"session_env" {their-key {…}}}`; the merge happens in
`ctx-loop/render-block!` so internal owns the base section, extensions
layer on top.

Heavy environment scans (full byte-counted language tables, polylith
brick listings, multi-repo git status) live in the foundation-core
focused project-shape helpers for explicit deep-dives. The digest never calls
into extensions — host facts come from `System/getProperty`,
project shape from a single directory peek. AGENTS.md / CLAUDE.md
contents ride in their own system block (`internal.prompt`), not here.
raw docstring

com.blockether.vis.internal.env-python

Embedded-GraalPy sandbox machinery — the agent's action substrate. The agent writes Python; this ns embeds a GraalPy org.graalvm.polyglot.Context, marshals values across the Clojure↔Python boundary, wires the Clojure tool fns into the Python globals as ProxyExecutables (so cat("x") in Python runs the Clojure cat), and runs the model's code block as ONE whole-block coroutine.

Public surface used by the loop:

create-python-context / set-python-binding! / bind-and-bump! / bind-and-bump-with-doc! / count-top-level-forms / validate-non-empty-block! / validate-no-banned-defs! / restore-sandbox! / SYSTEM_VAR_NAMES / system-var-sym? / lru-atom / current-turn-position / fresh-lru-atom / run-python-block / map-polyglot-error / bind-ctx! / ctx->python-str

The :python-context slot holds the GraalPy Context; the Python top scope is context.getBindings("python"). GraalPy ships in the default deps (runs on Oracle GraalVM 25 → Truffle gets the Graal JIT).

Embedded-GraalPy sandbox machinery — the agent's action substrate. The agent
writes **Python**; this ns embeds a GraalPy `org.graalvm.polyglot.Context`,
marshals values across the Clojure↔Python boundary, wires the Clojure tool
fns into the Python globals as `ProxyExecutable`s (so `cat("x")` in Python
runs the Clojure `cat`), and runs the model's code block as ONE whole-block
coroutine.

Public surface used by the loop:

  create-python-context / set-python-binding! / bind-and-bump! /
  bind-and-bump-with-doc! / count-top-level-forms / validate-non-empty-block! /
  validate-no-banned-defs! / restore-sandbox! / SYSTEM_VAR_NAMES /
  system-var-sym? / *lru-atom* / *current-turn-position* / fresh-lru-atom /
  run-python-block / map-polyglot-error / bind-ctx! / ctx->python-str

The `:python-context` slot holds the GraalPy `Context`; the Python top scope is
`context.getBindings("python")`. GraalPy ships in the default deps (runs on
Oracle GraalVM 25 → Truffle gets the Graal JIT).
raw docstring

com.blockether.vis.internal.error

Error formatting - leaf module.

Pure functions for turning exceptions, anomaly maps, and ad-hoc error values into human-readable strings. Lives in its own namespace so any layer (SDK facade, channels, extensions, the iteration loop) can format an error without dragging in the rest of the SDK.

Public API: error-message - raw text from a Throwable / map / string format-error - prefix "ERROR: " idempotently final-answer-code-error-message - the "Final-answer code error: ..." prefix used by the iteration loop when an (done ...) form's own code throws.

Error formatting - leaf module.

Pure functions for turning exceptions, anomaly maps, and ad-hoc
error values into human-readable strings. Lives in its own
namespace so any layer (SDK facade, channels, extensions, the
iteration loop) can format an error without dragging in the rest
of the SDK.

Public API:
  `error-message`                 - raw text from a Throwable / map / string
  `format-error`                  - prefix "ERROR: " idempotently
  `final-answer-code-error-message` - the "Final-answer code error: ..." prefix
                                     used by the iteration loop when an
                                     `(done ...)` form's own code throws.
raw docstring

com.blockether.vis.internal.extension

Extension subsystem: spec, builders, hook execution, the global registry, parse-error rescue, and manifest namespace catalog.

An extension is the SINGLE entry point for everything a third-party bundle contributes to vis. Whatever surfaces it populates - Python sandbox symbols, CLI commands, channels, providers, persistence backends - it does so by listing them in the matching :ext/<surface> slot, and register-extension! dispatches each slot to its concrete sub-registry. The same data feeds:

  • the active-extensions list every iteration consults
  • the system-prompt block rendered from :ext.engine/symbols
  • the per-iteration :ext/hooks checks
  • the parse-error rescue chain
  • the manifest id/namespace catalog used for extension metadata

Channel and provider registries live in internal.registry (the sub-registry that lights up when this module dispatches their contributions). Backend dispatch lives in internal.persistance. Classpath manifest scanning lives in internal.manifest. This module is the only consumer of all four.

Extension subsystem: spec, builders, hook execution, the global
registry, parse-error rescue, and manifest namespace catalog.

An extension is the SINGLE entry point for everything a third-party
bundle contributes to vis. Whatever surfaces it populates - Python
sandbox symbols, CLI commands, channels, providers, persistence
backends - it does so by listing them in the matching `:ext/<surface>`
slot, and `register-extension!` dispatches each slot to its concrete
sub-registry. The same data feeds:

  - the active-extensions list every iteration consults
  - the system-prompt block rendered from `:ext.engine/symbols`
  - the per-iteration `:ext/hooks` checks
  - the parse-error rescue chain
  - the manifest id/namespace catalog used for extension metadata

Channel and provider registries live in `internal.registry` (the
sub-registry that lights up when this module dispatches their
contributions). Backend dispatch lives in `internal.persistance`.
Classpath manifest scanning lives in `internal.manifest`. This
module is the only consumer of all four.
raw docstring

com.blockether.vis.internal.extension-aggregate

Extension-owned durable sidecar API.

Public ext-* helpers are for code running inside an extension callback. They never accept :extension-id from callers; the currently executing extension identity is supplied by com.blockether.vis.internal.extension.

The db-* persistence facade remains the privileged/admin surface for inspecting rows across extensions.

Extension-owned durable sidecar API.

Public ext-* helpers are for code running inside an extension callback.
They never accept :extension-id from callers; the currently executing
extension identity is supplied by com.blockether.vis.internal.extension.

The db-* persistence facade remains the privileged/admin surface for
inspecting rows across extensions.
raw docstring

com.blockether.vis.internal.external-opener

Shell out to the host OS opener so Vis can hand a URL or local file path off to the user's preferred external browser/viewer.

Responsibilities, in order:

  1. Classify the candidate target into a whitelisted scheme keyword: :http, :https, :file, :rel, or :rejected.

  2. Resolve the target to a host-friendly form. Relative paths are anchored at the current working directory and re-checked for .. traversal. Returns nil when the path escapes.

  3. Build the OS-appropriate command vector (open / xdg-open / cmd /c start) for ProcessBuilder.

  4. Spawn it with stdio redirected to /dev/null so a chatty opener cannot corrupt terminal output.

Pure-ish: every step except open! itself is a function of its args plus os.name and the current working directory. open! shells out and never throws; errors land in the returned result map.

Shell out to the host OS opener so Vis can hand a URL or local file
path off to the user's preferred external browser/viewer.

Responsibilities, in order:

  1. Classify the candidate target into a whitelisted scheme keyword:
     `:http`, `:https`, `:file`, `:rel`, or `:rejected`.

  2. Resolve the target to a host-friendly form. Relative paths are
     anchored at the current working directory and re-checked for
     `..` traversal. Returns nil when the path escapes.

  3. Build the OS-appropriate command vector
     (`open` / `xdg-open` / `cmd /c start`) for `ProcessBuilder`.

  4. Spawn it with stdio redirected to /dev/null so a chatty opener
     cannot corrupt terminal output.

Pure-ish: every step except `open!` itself is a function of its
args plus `os.name` and the current working directory. `open!`
shells out and never throws; errors land in the returned result map.
raw docstring

com.blockether.vis.internal.fff-index

THE canonical way vis talks to fff.

Every fff instance in this process is born here and lives in ONE pool keyed by [canonical-root respect-ignore-files? ignore-overlay]. Nothing else may call fff/create — a second, unpooled instance would duplicate a whole tree's native path+content index, spin its own watcher threads, and go stale on our own writes.

Contract for callers:

(fff-index/with-index [idx (fff-index/lease root respect-ignore-files? overlay)] (fff/search idx …) (fff/grep idx …))

  • the index is watcher-live (:watch? true) and resynced before the body runs when this process wrote anything since it was last synced,
  • the body must NOT close idx; the pool owns it (LRU + idle TTL),
  • every filesystem mutation this process performs must call note-fs-write! so the next search reads its own writes.
THE canonical way vis talks to fff.

Every fff instance in this process is born here and lives in ONE pool keyed
by `[canonical-root respect-ignore-files? ignore-overlay]`. Nothing else may call
`fff/create` — a second, unpooled instance would duplicate a whole tree's
native path+content index, spin its own watcher threads, and go stale on our
own writes.

Contract for callers:

  (fff-index/with-index [idx (fff-index/lease root respect-ignore-files? overlay)]
    (fff/search idx …) (fff/grep idx …))

- the index is watcher-live (`:watch? true`) and resynced before the body
  runs when this process wrote anything since it was last synced,
- the body must NOT close `idx`; the pool owns it (LRU + idle TTL),
- every filesystem mutation this process performs must call `note-fs-write!`
  so the next search reads its own writes.
raw docstring

com.blockether.vis.internal.file-picker

Backend for file-picking UIs (the @ mention picker, TUI + web).

Everything here rides the ONE canonical pooled fff index (internal.fff-index) that the grep / find_files tools use: fff owns the tree walk, the gitignore policy, the git-status metadata and the frecency-ranked fuzzy match. This namespace only leases that index and turns fff rows into display rows.

There is deliberately NO Clojure-side directory walk, git-status subprocess, ignore matcher or scoring heuristic left in here — reintroducing one means the picker and the search tools would rank and see different files.

Backend for file-picking UIs (the `@` mention picker, TUI + web).

Everything here rides the ONE canonical pooled fff index
(`internal.fff-index`) that the `grep` / `find_files` tools use: fff owns the
tree walk, the gitignore policy, the git-status metadata and the
frecency-ranked fuzzy match. This namespace only leases that index and turns
fff rows into display rows.

There is deliberately NO Clojure-side directory walk, git-status subprocess,
ignore matcher or scoring heuristic left in here — reintroducing one means the
picker and the search tools would rank and see different files.
raw docstring

com.blockether.vis.internal.form

The canonical per-form DISPLAY contract — ONE source of truth for the fields a channel reads to render an executed form, live (via the gateway) and restored (via the DB).

Why this exists: the SAME field set used to be hand-listed in ~7 independent allowlists — the loop chunk, the persisted block, the gateway block.output wire payload, the gateway→chunk client projection, the live progress form, the DB-restored form, and the restored display form. A new field was invisible until every one was edited, and whichever layer forgot it silently dropped the field (the gateway dropping :tool-color-role so the live badge vanished was exactly that). Now every layer projects the WHOLE set through ->display (outbound) / <-wire (inbound, tolerant of the wire's snake_case + stringified keyword values), so a new display field is a ONE-line change to display-keys and form-roundtrip-test fails if a boundary stops carrying it.

Transformed fields (:stdout/:error bounded, :silent/:duration_ms renamed) stay as explicit gateway overrides — they are not carried verbatim, so they are NOT in this set.

The canonical per-form DISPLAY contract — ONE source of truth for the fields a
channel reads to render an executed form, live (via the gateway) and restored
(via the DB).

Why this exists: the SAME field set used to be hand-listed in ~7 independent
allowlists — the loop chunk, the persisted block, the gateway `block.output`
wire payload, the gateway→chunk client projection, the live progress form, the
DB-restored form, and the restored display form. A new field was invisible
until every one was edited, and whichever layer forgot it silently dropped the
field (the gateway dropping `:tool-color-role` so the live badge vanished was
exactly that). Now every layer projects the WHOLE set through `->display`
(outbound) / `<-wire` (inbound, tolerant of the wire's snake_case + stringified
keyword values), so a new display field is a ONE-line change to `display-keys`
and `form-roundtrip-test` fails if a boundary stops carrying it.

Transformed fields (`:stdout`/`:error` bounded, `:silent`/`:duration_ms`
renamed) stay as explicit gateway overrides — they are not carried verbatim, so
they are NOT in this set.
raw docstring

com.blockether.vis.internal.format

Format helpers - leaf module.

Small, dependency-light formatters used by the SDK facade, the TUI footer and the CLI status output. Each one is a pure transform over basic Clojure / Java values.

format-date - java.util.Date to dd-MM-yyyy HH:mm (local TZ) format-clojure - pass-through (code is shown as written, not reformatted) format-duration - millisecond duration to 2.3s, 1m 15s, etc. format-tokens - :input/:output token counts to 'tok 11461→35' format-cost - dollar cost to '~$0.006954' format-iterations- iteration count to '1 iter' / '3 iters' format-meta-line - canonical ' / '-joined turn-summary line (used identically by CLI / TUI)

No external pretty-printer: data renders through clojure.pprint (built-in) and source code is shown verbatim. The namespace is free of state - safe to require from any layer.

Format helpers - leaf module.

Small, dependency-light formatters used by the SDK facade, the
TUI footer and the CLI status output. Each one
is a pure transform over basic Clojure / Java values.

  `format-date`      - `java.util.Date` to `dd-MM-yyyy HH:mm` (local TZ)
  `format-clojure`   - pass-through (code is shown as written, not reformatted)
  `format-duration`  - millisecond duration to `2.3s`, `1m 15s`, etc.
  `format-tokens`    - `:input`/`:output` token counts to 'tok 11461→35'
  `format-cost`      - dollar cost to '~$0.006954'
  `format-iterations`- iteration count to '1 iter' / '3 iters'
  `format-meta-line` - canonical ' / '-joined turn-summary line
                       (used identically by CLI / TUI)

No external pretty-printer: data renders through `clojure.pprint`
(built-in) and source code is shown verbatim. The namespace is free of
state - safe to require from any layer.
raw docstring

com.blockether.vis.internal.foundation.doctor

Foundation's contribution to the host vis doctor aggregator. ONE fn (doctor-fn) returns the full message stream from two logical sections, each stamping its own :check-id so the formatter groups them under the same banner the original four-checks-vec shape produced (plan §1 Q18 / §10):

::agents-md AGENTS.md presence / source / size; one :info line when found, one :warn line when neither AGENTS.md nor CLAUDE.md exists (rules silently absent is worth flagging even though it isn't an error per se).

These section fns are pure data -> message-seq; they don't mutate anything and don't depend on the runtime environment beyond what's needed to read the existing scanners. Activation contract per plan: every registered extension's :ext/doctor-fn runs regardless of :ext/activation-fn, so the section fns must NOT assume :db-info or other env keys are present.

Foundation's contribution to the host `vis doctor` aggregator. ONE fn
(`doctor-fn`) returns the full message stream from two logical
sections, each stamping its own `:check-id` so the formatter
groups them under the same banner the original four-checks-vec
shape produced (plan §1 Q18 / §10):

  ::agents-md         AGENTS.md presence / source / size; one
                       :info line when found, one :warn line
                       when neither AGENTS.md nor CLAUDE.md exists
                       (rules silently absent is worth flagging
                       even though it isn't an error per se).

These section fns are pure data -> message-seq; they don't mutate
anything and don't depend on the runtime environment beyond
what's needed to read the existing scanners. Activation
contract per plan: every registered extension's `:ext/doctor-fn`
runs regardless of `:ext/activation-fn`, so the section fns must
NOT assume `:db-info` or other env keys are present.
raw docstring

com.blockether.vis.internal.foundation.editing.core

Filesystem tools exposed as bare symbols in the Python sandbox.

Two layers:

  1. Structured helpers for read / tree / search:

    (cat path) ; -> {:path :anchors {<N:hash> text…} :next-offset N? :truncated? B} (cat path n) ; first n lines from line 1 (cat path offset n) ; n lines starting at line offset (1-based) (cat path :tail) ; last 400 lines (tail) (cat path :tail n) ; last n lines (cat dir) ; a DIRECTORY path -> shallow listing {:path :entries [{name path type size}] :depth} (cat dir opts) ; opts keys: depth (recurse) / is_hidden / is_respect_gitignore (grep query) ; -> content hits (anchored) + ranked file-NAME matches; ; query = a term or list of terms (OR), smart-case ; substring. Opts: paths/include/limit/is_hidden

  2. Cwd-safe wrappers over the babashka.fs file API. patch is the canonical text edit surface:

    (cat path) (patch [edit-map]) ; keys: path / from_anchor / replace (create-dirs path) (copy src dest) (move src dest) (delete path) (delete-if-exists path) (exists? path)

Hard guard: every path must stay inside the session's working directory (fs/cwd); .. traversal is rejected before any I/O.

Filesystem tools exposed as bare symbols in the Python sandbox.

Two layers:

1. Structured helpers for read / tree / search:

     (cat path)            ; -> {:path :anchors {<N:hash> text…} :next-offset N? :truncated? B}
     (cat path n)          ; first n lines from line 1
     (cat path offset n)   ; n lines starting at line `offset` (1-based)
     (cat path :tail)      ; last 400 lines (tail)
     (cat path :tail n)    ; last n lines
     (cat dir)             ; a DIRECTORY path -> shallow listing {:path :entries [{name path type size}] :depth}
     (cat dir opts)        ; opts keys: depth (recurse) / is_hidden / is_respect_gitignore
     (grep query)         ; -> content hits (anchored) + ranked file-NAME matches;
                           ; query = a term or list of terms (OR), smart-case
                           ; substring. Opts: paths/include/limit/is_hidden

2. Cwd-safe wrappers over the babashka.fs file API. `patch` is
   the canonical text edit surface:

     (cat path)
     (patch [edit-map])    ; keys: path / from_anchor / replace
     (create-dirs path)
     (copy src dest)
     (move src dest)
     (delete path)
     (delete-if-exists path)
     (exists? path)

Hard guard: every path must stay inside the session's working
directory (`fs/cwd`); `..` traversal is rejected before any I/O.
raw docstring

com.blockether.vis.internal.foundation.editing.index

Structural INDEX: a high-level, line-ranged skeleton of a source file produced via tree-sitter (com.blockether/tree-sitter-language-pack, which sources Clojure from our own grammar fork).

Every item carries FULL anchors — the same <lineno>:<hash> anchors cat emits and patch consumes — for its first and last line, so you can replace a whole definition straight from the index with one patch([{path P, from_anchor start, to_anchor end, replace …}]), no intermediate cat. Each line is:

<kind> <name> <signature> @<start-anchor>..<end-anchor>

e.g.

class Greeter @3:a1b..7:c2d function hello @6:1b2..7:c2d function main @9:3c4..10:5d6

Read it BEFORE cat: a cheap map of a file so you jump straight to the right range (and its anchors) instead of reading the whole file.

Requiring this namespace also requires the native resolver, which selects the right per-platform FFI library at runtime.

Structural INDEX: a high-level, line-ranged skeleton of a source file
produced via tree-sitter (com.blockether/tree-sitter-language-pack, which
sources Clojure from our own grammar fork).

Every item carries FULL anchors — the same `<lineno>:<hash>` anchors `cat`
emits and `patch` consumes — for its first and last line, so you can replace
a whole definition straight from the index with one
`patch([{path P, from_anchor start, to_anchor end, replace …}])`, no intermediate
`cat`. Each line is:

  <kind> <name>  <signature>  @<start-anchor>..<end-anchor>

e.g.

  class Greeter  @3:a1b..7:c2d
    function hello  @6:1b2..7:c2d
  function main  @9:3c4..10:5d6

Read it BEFORE `cat`: a cheap map of a file so you jump straight to the right
range (and its anchors) instead of reading the whole file.

Requiring this namespace also requires the native resolver, which selects the
right per-platform FFI library at runtime.
raw docstring

com.blockether.vis.internal.foundation.editing.patch

Pure hashline primitives for content-addressed reading and editing.

Public surface: split-content-lines / char-offset-at-line line-hash / lines->anchors text -> lineno:hash / {ln anchor} render-hashline-block / -range-block tuples -> <hash>| text gutter indices-matching-hash / resolve-anchor-edit-span self-locating range replace

Pure hashline primitives for content-addressed reading and editing.

Public surface:
  split-content-lines / char-offset-at-line
  line-hash / lines->anchors              text -> lineno:hash / {ln anchor}
  render-hashline-block / -range-block   tuples -> `<hash>| text` gutter
  indices-matching-hash / resolve-anchor-edit-span  self-locating range replace
raw docstring

com.blockether.vis.internal.foundation.editing.structural

Thin Clojure adapter over the pack's Java structural-edit engine (dev.kreuzberg.treesitterlanguagepack.StructuralApi). All the work — locate the definition by name from the tree-sitter outline, splice its line span, and re-parse to refuse syntax-breaking edits — lives in Java so it is language-neutral, reusable from any JVM consumer, and native-image clean. This namespace only maps vis op keywords onto the Java API.

Thin Clojure adapter over the pack's Java structural-edit engine
(`dev.kreuzberg.treesitterlanguagepack.StructuralApi`). All the work —
locate the definition by name from the tree-sitter outline, splice its line
span, and re-parse to refuse syntax-breaking edits — lives in Java so it is
language-neutral, reusable from any JVM consumer, and native-image clean.
This namespace only maps vis op keywords onto the Java API.
raw docstring

com.blockether.vis.internal.foundation.editing.zipper

Language-neutral STRUCTURAL ZIPPER over the tree-sitter pack (306+ langs) — the unified cursor the name-based structural ops were missing.

A node's location is a STATELESS PATH: a vector of NAMED-child indices from the root (e.g. [2 0] = first named child of the third named child of the file). Stateless means it round-trips cleanly through async tool calls — no live native cursor to keep between calls. Relative moves (down/up/next/prev) are pure path arithmetic on top, so the model navigates like a rewrite-clj zipper but over EVERY language tree-sitter understands.

Edits splice the target node's UTF-8 byte range and RE-PARSE, refusing a result that introduces a syntax error the original didn't have — the same safety contract as structural. Pairs with the name-based ops: locate a def by name, then walk into it by path.

All native handles (Parser/Tree/Node) are opened and closed inside each call; only plain Clojure data escapes.

Language-neutral STRUCTURAL ZIPPER over the tree-sitter pack (306+ langs) —
the unified cursor the name-based `structural` ops were missing.

A node's location is a STATELESS PATH: a vector of NAMED-child indices from
the root (e.g. `[2 0]` = first named child of the third named child of the
file). Stateless means it round-trips cleanly through async tool calls — no
live native cursor to keep between calls. Relative moves (down/up/next/prev)
are pure path arithmetic on top, so the model navigates like a rewrite-clj
zipper but over EVERY language tree-sitter understands.

Edits splice the target node's UTF-8 byte range and RE-PARSE, refusing a
result that introduces a syntax error the original didn't have — the same
safety contract as `structural`. Pairs with the name-based ops: locate a def
by name, then walk into it by path.

All native handles (Parser/Tree/Node) are opened and closed inside each call;
only plain Clojure data escapes.
raw docstring

com.blockether.vis.internal.foundation.environment.agents

Thin re-export shim — project-guidance discovery moved to com.blockether.vis.internal.agents. AGENTS.md / CLAUDE.md handling is core functionality (drives system prompt + ctx digest), so it now lives outside the foundation extension.

This shim keeps the legacy main-agent-instructions tool + any downstream (:project ctx) :guidance consumers compiling while they migrate to the internal namespace.

Thin re-export shim — project-guidance discovery moved to
`com.blockether.vis.internal.agents`. AGENTS.md / CLAUDE.md
handling is core functionality (drives system prompt + ctx
digest), so it now lives outside the foundation extension.

This shim keeps the legacy `main-agent-instructions` tool +
any downstream `(:project ctx) :guidance` consumers compiling
while they migrate to the internal namespace.
raw docstring

com.blockether.vis.internal.foundation.environment.core

vis-foundation — the agent's environment-awareness layer.

Owns the environment facts: cwd, user, platform, shell, plus:

  • git repository facts via the git binary (root, branch, dirty status, submodules, worktree),
  • a bounded language scan over the working tree (top languages by file count, primary language),
  • monorepo / multi-package shape detection (polylith, workspace, submodules) by counting per-ecosystem manifests.

Model-facing VCS/workspace truth lives in session["workspace"]. Remaining helpers cover coarse project shape (languages, monorepo, repositories) and cache invalidation (refresh!).

Runtime facts are computed lazily on first access and cached per working-directory. The cache is invalidated automatically when cwd changes between calls and explicitly by (refresh!).

vis-foundation — the agent's environment-awareness layer.

Owns the environment facts: cwd, user, platform, shell, plus:

  * git repository facts via the git binary (root, branch, dirty status,
    submodules, worktree),
  * a bounded language scan over the working tree (top languages
    by file count, primary language),
  * monorepo / multi-package shape detection (polylith, workspace,
    submodules) by counting per-ecosystem manifests.

Model-facing VCS/workspace truth lives in `session["workspace"]`.
Remaining helpers cover coarse project shape (`languages`,
`monorepo`, `repositories`) and cache invalidation (`refresh!`).

Runtime facts are computed lazily on first access and cached per
working-directory. The cache is invalidated automatically when
`cwd` changes between calls and explicitly by `(refresh!)`.
raw docstring

com.blockether.vis.internal.foundation.environment.git

Git introspection for the environment block, backed by the native git binary (via internal.git).

Returns a snapshot map for the repository that contains start (typically the JVM working directory). nil when start is not inside any git repository. Never throws — every git call is guarded; on any failure we degrade gracefully to nil or a reduced-shape map.

The expensive call is git status (working-tree walk). When it fails or is suppressed, the snapshot drops the dirty-status fields instead of stalling the system-prompt build.

Git introspection for the environment block, backed by the native `git`
binary (via `internal.git`).

Returns a snapshot map for the repository that contains `start`
(typically the JVM working directory). nil when `start` is not inside any
git repository. Never throws — every git call is guarded; on any failure we
degrade gracefully to nil or a reduced-shape map.

The expensive call is `git status` (working-tree walk). When it fails or is
suppressed, the snapshot drops the dirty-status fields instead of stalling
the system-prompt build.
raw docstring

com.blockether.vis.internal.foundation.environment.host

Host-side facts read from JDK system properties and process environment variables.

No I/O, no shell-out, no third-party deps. Cheap to compute, safe to call from any thread, never throws.

Host-side facts read from JDK system properties and process
environment variables.

No I/O, no shell-out, no third-party deps. Cheap to compute, safe
to call from any thread, never throws.
raw docstring

com.blockether.vis.internal.foundation.environment.languages

Bounded language scan over a directory tree.

Walks the tree with Files/walkFileTree, skipping common non-source subdirectories (.git, node_modules, target, ...) via FileVisitResult/SKIP_SUBTREE. Counts files and bytes per language using a small extension-to-language map.

The walk has TWO hard guards: a max-file count and a wall-time deadline. Either one stops the walk via TERMINATE. Callers get a possibly-partial result; on a small repo the result is exact.

No third-party deps. Reflection-clean.

Bounded language scan over a directory tree.

Walks the tree with `Files/walkFileTree`, skipping common
non-source subdirectories (`.git`, `node_modules`, `target`, ...)
via `FileVisitResult/SKIP_SUBTREE`. Counts files and bytes per
language using a small extension-to-language map.

The walk has TWO hard guards: a max-file count and a wall-time
deadline. Either one stops the walk via `TERMINATE`. Callers get
a possibly-partial result; on a small repo the result is exact.

No third-party deps. Reflection-clean.
raw docstring

com.blockether.vis.internal.foundation.environment.monorepo

Monorepo / multi-package detection.

Walks the tree once (bounded), counting per-language manifest files at any depth below the root. >=2 manifests of the same kind in distinct subdirectories signals a multi-package workspace; we report the kind, the count, and a best-guess shape label (e.g. "polylith", "workspace", "submodules").

Reflection-clean. Honors the same skip-directory list as the language scanner.

Monorepo / multi-package detection.

Walks the tree once (bounded), counting per-language manifest
files at any depth below the root. >=2 manifests of the same
kind in distinct subdirectories signals a multi-package
workspace; we report the kind, the count, and a best-guess
shape label (e.g. "polylith", "workspace", "submodules").

Reflection-clean. Honors the same skip-directory list as the
language scanner.
raw docstring

com.blockether.vis.internal.foundation.environment.render

Build compact foundation environment data for ctx. No prompt labels.

Build compact foundation environment data for `ctx`. No prompt labels.
raw docstring

com.blockether.vis.internal.foundation.environment.repositories

Bounded discovery of multiple Git repositories below the current project root. This catches multirepo workspaces where the user's cwd is a parent directory or a primary repo that vendors sibling/nested repos outside .gitmodules.

Returns compact per-repo Git summaries for the system prompt. Full status walks are bounded per repo by git/snapshot; the repository scan itself is bounded by max files, max repos, and a wall-clock deadline. Never throws.

Bounded discovery of multiple Git repositories below the current
project root. This catches multirepo workspaces where the user's cwd
is a parent directory or a primary repo that vendors sibling/nested
repos outside `.gitmodules`.

Returns compact per-repo Git summaries for the system prompt. Full
status walks are bounded per repo by `git/snapshot`; the repository
scan itself is bounded by max files, max repos, and a wall-clock
deadline. Never throws.
raw docstring

com.blockether.vis.internal.foundation.git-tool

The single git tool — a thin, honest proxy to the host git binary.

ONE built-in Python function, git, runs git <args…> in the active workspace root and returns a TOTAL, string-keyed result the model reads directly: {"cmd", "args", "stdout", "stderr", "exit", "duration_ms", "timed_out", "timeout_secs"} — every key ALWAYS present (stderr "" when empty, exit None only when the run timed out), so no field ever KeyErrors. A non-zero exit is DATA, not a tool failure — the model reads it like it would in a terminal.

This REPLACES the old JGit-backed git_* surface (foundation-git): no embedded git implementation, no SSH/BouncyCastle stack — the only git is the one already on the user's PATH, so behaviour matches their shell exactly. Read-only workspace facts (branch/dirty/ahead-behind for the footer, env block, file picker) still flow through com.blockether.vis.internal.git; this namespace is purely the model- facing command tool.

Built-in (bare git in the sandbox, next to cat/rg), gated to activate only when the workspace sits inside a repository.

The single `git` tool — a thin, honest proxy to the host `git` binary.

ONE built-in Python function, `git`, runs `git <args…>` in the active
workspace root and returns a TOTAL, string-keyed result the model reads
directly: `{"cmd", "args", "stdout", "stderr", "exit", "duration_ms",
"timed_out", "timeout_secs"}` — every key ALWAYS present (`stderr` "" when
empty, `exit` None only when the run timed out), so no field ever KeyErrors.
A non-zero exit is DATA, not a tool failure — the model reads it like it
would in a terminal.

This REPLACES the old JGit-backed `git_*` surface (foundation-git): no
embedded git implementation, no SSH/BouncyCastle stack — the only git is
the one already on the user's PATH, so behaviour matches their shell
exactly. Read-only workspace facts (branch/dirty/ahead-behind for the
footer, env block, file picker) still flow through
`com.blockether.vis.internal.git`; this namespace is purely the model-
facing command tool.

Built-in (bare `git` in the sandbox, next to `cat`/`rg`), gated to
activate only when the workspace sits inside a repository.
raw docstring

com.blockether.vis.internal.foundation.harness.core

harness compatibility layer — a BUILT-IN foundation module (ships in the main jar, always present, gated by toggles) that exposes the AGENTS and SKILLS vis' own project dir and other AI coding HARNESSES (Claude Code, pi, opencode, the agents standard, …) leave on disk to the vis model. The sibling of the shell layer's POSIX compat. Vis reads its OWN project-local skills from .vis/skills (highest precedence).

Two bare verbs (bound like cat/rg via :ext.engine/builtin? true):

  • SKILLS are PROGRESSIVE: the prompt lists every skill name — description (cheap — always present), and skill(name) loads the FULL SKILL.md + its bundled resource PATHS on demand so the model spends tokens only on the one it uses.

  • AGENTS are an ALIAS to sub_loop: agent(name, prompt) runs the named agent as a CHILD loop whose system prompt IS that agent's markdown body, on its declared model.

Both bare verbs are unconditionally available — skills/agents/commands have no user toggle; the layer is always active.

`harness` compatibility layer — a BUILT-IN foundation module (ships in the
main jar, always present, gated by toggles) that exposes the AGENTS and
SKILLS vis' own project
dir and other AI coding HARNESSES (Claude Code, pi, opencode, the agents
standard, …) leave on disk to the vis model. The sibling of the shell
layer's POSIX compat. Vis reads its OWN project-local skills from
`.vis/skills` (highest precedence).

Two bare verbs (bound like cat/rg via `:ext.engine/builtin? true`):

- SKILLS are PROGRESSIVE: the prompt lists every skill `name — description`
  (cheap — always present), and
  `skill(name)` loads the FULL `SKILL.md` + its bundled resource PATHS on
  demand so the model spends tokens only on the one it uses.

- AGENTS are an ALIAS to `sub_loop`: `agent(name, prompt)` runs the named
  agent as a CHILD loop whose system prompt IS that agent's markdown body,
  on its declared model.

Both bare verbs are unconditionally available — skills/agents/commands have
no user toggle; the layer is always active.
raw docstring

com.blockether.vis.internal.foundation.harness.discovery

Cross-HARNESS discovery of agents + skills — the sibling of the shell layer's POSIX compat, for the agent/skill definitions vis' OWN project dir and OTHER AI coding harnesses (Claude Code, pi, opencode, the agents standard, …) leave on disk.

An AGENT is a markdown file with YAML-ish --- frontmatter (name, description, model, tools) + a body that IS a system prompt. A SKILL is a SKILL.md (same frontmatter, name+description) in its own directory, alongside bundled resource files.

Discovery is PURE except for the directory scan: parse-frontmatter, parse-agent, parse-skill-meta, and dedup-by-name take strings and are unit-tested without the filesystem; the discover-* fns walk the known source roots. Precedence is source ORDER, first-name-wins (vis project-local > other harnesses' project > user > plugin; Vis and Claude before pi/agents/opencode).

Cross-HARNESS discovery of agents + skills — the sibling of the shell
layer's POSIX compat, for the agent/skill definitions vis' OWN project dir
and OTHER AI coding harnesses (Claude Code, pi, opencode, the agents
standard, …) leave on disk.

An AGENT is a markdown file with YAML-ish `---` frontmatter
(`name`, `description`, `model`, `tools`) + a body that IS a system
prompt. A SKILL is a `SKILL.md` (same frontmatter, name+description) in
its own directory, alongside bundled resource files.

Discovery is PURE except for the directory scan: `parse-frontmatter`,
`parse-agent`, `parse-skill-meta`, and `dedup-by-name` take strings and
are unit-tested without the filesystem; the `discover-*` fns walk the
known source roots. Precedence is source ORDER, first-name-wins
(vis project-local > other harnesses' project > user > plugin; Vis and
Claude before pi/agents/opencode).
raw docstring

com.blockether.vis.internal.foundation.introspection

Programmatic introspection of the agent's own state from inside :code. The public state surface is deliberately small:

  • (session-state [session-id]) -> data map, including raw LLM diagnostics
  • (session-report-html [session-id]) -> HTML report rendered from that data

Everything else in this namespace is implementation detail. The agent gets the data once, manipulates it via plain Clojure (get-in, filter, map, etc.), and renders the same data to Markdown only when presentation is needed.

Every function is a pure read off the same DB tables the projection layer reads from (or a classpath read for the doc accessors). Failures return nil/[], never throw, so a misbehaving introspection call cannot break iteration execution.

Opt-in: not auto-loaded by default. Add this jar to the classpath to enable.

Programmatic introspection of the agent's own state from inside
`:code`. The public state surface is deliberately small:

- `(session-state [session-id])` -> data map, including raw LLM diagnostics
- `(session-report-html [session-id])` -> HTML report rendered from that data

Everything else in this namespace is implementation detail. The agent
gets the data once, manipulates it via plain Clojure (`get-in`,
`filter`, `map`, etc.), and renders the same data to Markdown only
when presentation is needed.

Every function is a pure read off the same DB tables the projection
layer reads from (or a classpath read for the doc accessors).
Failures return nil/[], never throw, so a misbehaving introspection
call cannot break iteration execution.

Opt-in: not auto-loaded by default. Add this jar to the classpath
to enable.
raw docstring

com.blockether.vis.internal.foundation.language-surface

Language-neutral FORMAT / TEST / REPL_EVAL / START_REPL dispatch.

Language extensions register handlers under :ext/language-tools; this foundation surface exposes stable bare tool names and dispatches to the active handler for the requested/current language. REPL lifecycle is resource backed: repl creates a language-owned session resource and repl_stop stops one by id. Live REPLs also surface in the ctx resources block.

Language-neutral FORMAT / TEST / REPL_EVAL / START_REPL dispatch.

Language extensions register handlers under `:ext/language-tools`; this
foundation surface exposes stable bare tool names and dispatches to the
active handler for the requested/current language. REPL lifecycle is resource
backed: `repl` creates a language-owned session resource and `repl_stop`
stops one by id. Live REPLs also surface in the ctx `resources` block.
raw docstring

com.blockether.vis.internal.foundation.mcp.client

Minimal Model Context Protocol (MCP) client. Speaks JSON-RPC 2.0 over two transports:

:stdio — spawn the server process and frame newline-delimited JSON-RPC on its stdin/stdout (the dominant local-server pattern). A daemon thread drains stderr into the vis log so a chatty server never deadlocks on a full pipe.

:http — Streamable HTTP: POST each JSON-RPC message to one endpoint; the reply is either application/json (one response) or text/event-stream (SSE) — both handled. The Mcp-Session-Id handed back by initialize rides on every later request, a DELETE frees it on shutdown, and an optional GET listen loop reacts to server-pushed notifications/tools/list_changed.

OAuth 2.1 (spec 2025-06-18) is supported for HTTP transports via oauth.clj: pass :bearer-fn (a 0/1-arg fn yielding the current Bearer token, called with the just-rejected token on 401). A 401 triggers a single-flight refresh and one automatic retry.

A conn is a plain map of closures + state; the extension treats it opaquely. Lifecycle: connect (which performs the initialize handshake) → list-tools / call-toolclose.

Minimal Model Context Protocol (MCP) client. Speaks JSON-RPC 2.0 over two
transports:

  :stdio  — spawn the server process and frame newline-delimited JSON-RPC on
            its stdin/stdout (the dominant local-server pattern). A daemon
            thread drains stderr into the vis log so a chatty server never
            deadlocks on a full pipe.

  :http   — Streamable HTTP: POST each JSON-RPC message to one endpoint; the
            reply is either `application/json` (one response) or
            `text/event-stream` (SSE) — both handled. The `Mcp-Session-Id`
            handed back by `initialize` rides on every later request, a
            `DELETE` frees it on shutdown, and an optional GET listen loop
            reacts to server-pushed `notifications/tools/list_changed`.

OAuth 2.1 (spec `2025-06-18`) is supported for HTTP transports via
`oauth.clj`: pass `:bearer-fn` (a 0/1-arg fn yielding the current Bearer
token, called with the just-rejected token on 401). A 401 triggers a
single-flight refresh and one automatic retry.

A `conn` is a plain map of closures + state; the extension treats it
opaquely. Lifecycle: `connect` (which performs the `initialize` handshake) →
`list-tools` / `call-tool` → `close`.
raw docstring

com.blockether.vis.internal.foundation.mcp.core

Built-in Model Context Protocol (MCP) surface. The gateway daemon owns ONE shared pool of MCP connections ({server {:conn spec}}); every session sees the same live tools. Always on: MCP is core infrastructure, not a droppable plug-in and not gated by any toggle. The pool is empty (and costs nothing) until at least one server is declared in config.

Servers are declared natively in ~/.vis/state.yml:

{:mcp {:servers {"filesystem" {:transport :stdio :command "npx" :args ["-y" "@modelcontextprotocol/server-filesystem" "/path"]} "remote" {:transport :http :url "https://..." :headers {"Authorization" "Bearer ${MY_TOKEN}"} :timeout_ms 60000 :listen true :auth {:client_id "..." :scope "..."}} "stale" {:enabled false :url "..."}}}}

Every string in :headers / :env / :args / :url / :command / :cwd supports ${ENV_VAR} interpolation from the host environment. :enabled false skips the server without deleting the entry. HTTP servers with no static bearer transparently negotiate OAuth 2.1 on first 401 (RFC 9728 discovery + RFC 7591 dynamic client registration + PKCE loopback).

Five model-facing verbs under alias mcp (flat sandbox renders alias_name): mcp__servers() — configured servers + status + tool counts mcp__tools(server) — a server's tools (auto-connects) mcp__call(server, tool, args) — call a tool (auto-connects) mcp__connect(server) / mcp__disconnect(server) — manage the connection

Live connections + tool counts also ride in ctx under env.mcp.

Built-in Model Context Protocol (MCP) surface. The gateway daemon owns ONE
shared pool of MCP connections (`{server {:conn spec}}`); every session sees
the same live tools. Always on: MCP is core infrastructure, not a droppable
plug-in and not gated by any toggle. The pool is empty (and costs nothing)
until at least one server is declared in config.

Servers are declared natively in `~/.vis/state.yml`:

  {:mcp {:servers {"filesystem" {:transport :stdio :command "npx"
                                 :args ["-y" "@modelcontextprotocol/server-filesystem" "/path"]}
                   "remote"     {:transport :http :url "https://..."
                                 :headers {"Authorization" "Bearer ${MY_TOKEN}"}
                                 :timeout_ms 60000
                                 :listen true
                                 :auth {:client_id "..." :scope "..."}}
                   "stale"      {:enabled false :url "..."}}}}

Every string in `:headers` / `:env` / `:args` / `:url` / `:command` / `:cwd`
supports `${ENV_VAR}` interpolation from the host environment. `:enabled
false` skips the server without deleting the entry. HTTP servers with no
static bearer transparently negotiate OAuth 2.1 on first 401 (RFC 9728
discovery + RFC 7591 dynamic client registration + PKCE loopback).

Five model-facing verbs under alias `mcp` (flat sandbox renders `alias_name`):
  mcp__servers()                — configured servers + status + tool counts
  mcp__tools(server)            — a server's tools (auto-connects)
  mcp__call(server, tool, args) — call a tool (auto-connects)
  mcp__connect(server) / mcp__disconnect(server) — manage the connection

Live connections + tool counts also ride in ctx under `env.mcp`.
raw docstring

com.blockether.vis.internal.foundation.mcp.http

One JDK java.net.http.HttpClient shared by every MCP subsystem (request/response, OAuth discovery + token exchange, SSE listen loop).

Rationale — perf + threading:

  • The JDK client owns a small internal selector pool AND an executor used to run response-body pipelines and sendAsync completions. Two client instances = two selector pools = wasted daemon threads for zero win. So we share ONE instance across client.clj + oauth.clj.
  • The executor is a newVirtualThreadPerTaskExecutor — on Java 25 virtual threads no longer pin on synchronized (JEP 491), so blocking-.send calls made from a virtual thread never park a carrier. Bounded footprint, no cached thread-pool growth under bursty MCP load.
  • defonce + delay = constructed lazily at runtime, never at namespace-load; safe for GraalVM native-image (the client owns non-heap state that can't land in the build image).
One JDK `java.net.http.HttpClient` shared by every MCP subsystem
(request/response, OAuth discovery + token exchange, SSE listen loop).

Rationale — perf + threading:
  * The JDK client owns a small internal selector pool AND an executor used
    to run response-body pipelines and `sendAsync` completions. Two client
    instances = two selector pools = wasted daemon threads for zero win.
    So we share ONE instance across `client.clj` + `oauth.clj`.
  * The executor is a `newVirtualThreadPerTaskExecutor` — on Java 25 virtual
    threads no longer pin on `synchronized` (JEP 491), so blocking-`.send`
    calls made from a virtual thread never park a carrier. Bounded footprint,
    no cached thread-pool growth under bursty MCP load.
  * `defonce` + `delay` = constructed lazily at runtime, never at
    namespace-load; safe for GraalVM native-image (the client owns non-heap
    state that can't land in the build image).
raw docstring

com.blockether.vis.internal.foundation.mcp.oauth

OAuth 2.1 client for Model Context Protocol servers (spec 2025-06-18).

Flow — on HTTP 401 from an MCP server, we:

  1. read WWW-Authenticate: Bearer resource_metadata="..." (RFC 9728), fall back to ${origin}/.well-known/oauth-protected-resource;
  2. GET the resource-metadata JSON → pick an authorization_servers[0];
  3. GET its .well-known/oauth-authorization-server (RFC 8414) or .well-known/openid-configuration for endpoints + capabilities;
  4. dynamic-client-register (RFC 7591) if the AS supports it, or use the caller-supplied client_id;
  5. run PKCE S256 authorization-code with a loopback redirect (http://127.0.0.1:<ephemeral>/mcp-callback) — spawn a one-shot com.sun.net.httpserver.HttpServer, print the URL, best-effort open it in the user's browser, wait for ?code=;
  6. exchange code → access + refresh tokens; persist to ~/.vis/mcp-tokens/<server>.edn;
  7. on later expiry / 401, refresh the token single-flight through com.blockether.vis.internal.oauth/make-file-refresher.

The returned bearer-fn is a 0/1-arg function: 0-arg yields the current bearer token (running the auth flow on first use); 1-arg (with the token the server just rejected) forces a refresh.

OAuth 2.1 client for Model Context Protocol servers (spec `2025-06-18`).

Flow — on HTTP 401 from an MCP server, we:
  1. read `WWW-Authenticate: Bearer resource_metadata="..."` (RFC 9728),
     fall back to `${origin}/.well-known/oauth-protected-resource`;
  2. GET the resource-metadata JSON → pick an `authorization_servers[0]`;
  3. GET its `.well-known/oauth-authorization-server` (RFC 8414) or
     `.well-known/openid-configuration` for endpoints + capabilities;
  4. dynamic-client-register (RFC 7591) if the AS supports it, or use
     the caller-supplied `client_id`;
  5. run PKCE S256 authorization-code with a loopback redirect
     (`http://127.0.0.1:<ephemeral>/mcp-callback`) — spawn a one-shot
     `com.sun.net.httpserver.HttpServer`, print the URL, best-effort open
     it in the user's browser, wait for `?code=`;
  6. exchange code → access + refresh tokens; persist to
     `~/.vis/mcp-tokens/<server>.edn`;
  7. on later expiry / 401, refresh the token single-flight through
     `com.blockether.vis.internal.oauth/make-file-refresher`.

The returned `bearer-fn` is a 0/1-arg function: 0-arg yields the current
bearer token (running the auth flow on first use); 1-arg (with the token the
server just rejected) forces a refresh.
raw docstring

com.blockether.vis.internal.foundation.mpl-capture

SINK for artifacts the sandbox PRODUCES — matplotlib figures (plt.show() / plt.savefig()), anything a tool hands to vis_attach, AND anything written into the per-context OUTBOX directory ($VIS_OUTBOX) — so the engine OWNS the bytes (a session_iteration_attachment row) captured AT THE SOURCE, with NO re-parsing of the model-facing stdout fence.

The old flow rendered each figure to a $TMPDIR/vis-mpl temp file, printed a vis-image fence carrying just that PATH, then at persist time re-parsed the fence out of stdout and re-read the (possibly already-gone) file. We control the whole boundary, so that round-trip is gone: a producer renders/reads the bytes HOST-side (matplotlib's __vis_mpl_render_file__ Java2D backend; vis_attach's sandbox-confined open; the outbox filesystem tap in sandbox-fs) and, right where it already holds the bytes, calls record-attachment! (or record-file!). run-python-block binds *attachment-sink* to a fresh collector around each block's eval and drains it into the block outcome's :attachments; the loop stamps each with the block's tool-call-id and hands them to db-store-iteration!'s :attachments. The stdout fence now serves ONLY the inline TUI/web display + ASCII fallback — never persistence.

Deliberately dependency-free (no AWT, no vis.core): safe to require from BOTH a render shim and the hot engine loop without dragging Java2D or a require cycle.

SINK for artifacts the sandbox PRODUCES — matplotlib figures (`plt.show()` /
`plt.savefig()`), anything a tool hands to `vis_attach`, AND anything written
into the per-context OUTBOX directory (`$VIS_OUTBOX`) — so the engine OWNS the
bytes (a `session_iteration_attachment` row) captured AT THE SOURCE, with NO
re-parsing of the model-facing stdout fence.

The old flow rendered each figure to a `$TMPDIR/vis-mpl` temp file, printed a
`vis-image` fence carrying just that PATH, then at persist time re-parsed the
fence out of stdout and re-read the (possibly already-gone) file. We control the
whole boundary, so that round-trip is gone: a producer renders/reads the bytes
HOST-side (matplotlib's `__vis_mpl_render_file__` Java2D backend; `vis_attach`'s
sandbox-confined `open`; the outbox filesystem tap in `sandbox-fs`) and, right
where it already holds the bytes, calls `record-attachment!` (or `record-file!`).
`run-python-block` binds `*attachment-sink*` to a fresh collector around each
block's eval and drains it into the block outcome's `:attachments`; the loop
stamps each with the block's tool-call-id and hands them to `db-store-iteration!`'s
`:attachments`. The stdout fence now serves ONLY the inline TUI/web display +
ASCII fallback — never persistence.

Deliberately dependency-free (no AWT, no `vis.core`): safe to require from BOTH
a render shim and the hot engine loop without dragging Java2D or a require
cycle.
raw docstring

com.blockether.vis.internal.foundation.pty

Pure-Java pseudo-terminal for background shell children — NO JNA, NO extracted native helper, NO external tmux. Everything is a java.lang.foreign (Panama FFM) downcall into the platform libc, so it survives GraalVM native-image the same way the rest of vis's FFM surface (fff / rift / ruff / tree-sitter) does.

Why FFM and not pty4j:

  • pty4j drags in JNA and ships its own compiled libpty that it extracts to a temp dir at runtime — two native-image headaches (reflection metadata for JNA + a resource-extraction dance for the .dylib/.so).
  • The obvious pure-FFM shortcut — call libc forkpty then execve in the child — does NOT work from a JVM: invoking an FFM downcall MethodHandle after fork() is not async-signal-safe and SIGBUSes the child. (Verified.)
  • posix_spawn sidesteps that entirely: libc does the fork+exec ATOMICALLY in native code, so vis only ever issues ONE parent-side downcall and never runs any JVM code in the child. Paired with openpty (master/slave fds) + a dup2 of the slave onto the child's 0/1/2, the child gets a real TTY: isatty() is true, $TERM is honoured, stdin is writable.

Public surface — spawn! returns a PLAIN MAP (not a java.lang.Process; a runtime proxy/gen-class would break native-image), shaped for the internal.foundation.shell background pump:

{:pid <long> OS pid (a genuine child — ProcessHandle/of works, unlike a pty4j process) :in <java.io.InputStream> master-fd reader (a real piped stream) :send (fn [^bytes b]) write bytes to the master (the stdin channel) :wait (fn [] <int>) block until exit, reap, return the exit code :alive? (fn [] <bool>) :destroy (fn [force?]) SIGTERM (false) / SIGKILL (true) the child}

Pure-Java pseudo-terminal for background `shell` children — NO JNA, NO extracted native helper,
NO external `tmux`. Everything is a `java.lang.foreign` (Panama FFM) downcall
into the platform libc, so it survives GraalVM native-image the same way the
rest of vis's FFM surface (fff / rift / ruff / tree-sitter) does.

Why FFM and not pty4j:
- pty4j drags in JNA *and* ships its own compiled `libpty` that it extracts to
  a temp dir at runtime — two native-image headaches (reflection metadata for
  JNA + a resource-extraction dance for the .dylib/.so).
- The obvious pure-FFM shortcut — call libc `forkpty` then `execve` in the
  child — does NOT work from a JVM: invoking an FFM downcall MethodHandle after
  `fork()` is not async-signal-safe and SIGBUSes the child. (Verified.)
- `posix_spawn` sidesteps that entirely: libc does the fork+exec ATOMICALLY in
  native code, so vis only ever issues ONE parent-side downcall and never runs
  any JVM code in the child. Paired with `openpty` (master/slave fds) + a
  `dup2` of the slave onto the child's 0/1/2, the child gets a real TTY:
  `isatty()` is true, `$TERM` is honoured, stdin is writable.

Public surface — `spawn!` returns a PLAIN MAP (not a `java.lang.Process`; a
runtime `proxy`/`gen-class` would break native-image), shaped for the
`internal.foundation.shell` background pump:

  {:pid      <long>              OS pid (a genuine child — `ProcessHandle/of`
                                 works, unlike a pty4j process)
   :in       <java.io.InputStream>   master-fd reader (a real piped stream)
   :send     (fn [^bytes b])     write bytes to the master (the stdin channel)
   :wait     (fn [] <int>)       block until exit, reap, return the exit code
   :alive?   (fn [] <bool>)
   :destroy  (fn [force?])       SIGTERM (false) / SIGKILL (true) the child}
raw docstring

com.blockether.vis.internal.foundation.pty-bridge

Passthrough bridge on top of the FFM pseudo-terminal (internal.foundation.pty).

The problem it solves: a background shell child runs INSIDE the vis process (the FFM PTY master fd + reader thread live in vis's heap). That's great for the agent (the shell send / logs ops) but a HUMAN can't jump into the live terminal to finish a step the agent can't — click through a browser OAuth, answer a prompt only a person can. tmux gets that for free because its server is a separate daemon you can tmux attach to; the FFM child is not.

This namespace restores that capability WITHOUT tmux: each background PTY optionally exposes a per-shell UNIX-DOMAIN SOCKET. vis is the server (it holds the master fd); vis extension shell attach <id> is a thin client the human runs in their OWN Terminal.app. On connect the server (a) tees live master output to the socket and (b) forwards the socket's bytes to the master (stdin) — a genuine bidirectional passthrough. Multiple humans can attach at once; detaching just drops the socket and leaves the child running (exactly like tmux detach).

Everything here is stdlib: java.nio.channels AF_UNIX sockets (JDK 16+, already in vis's native-image reachability metadata) on the server side, and stty for raw mode on the client side (the human's interactive shell always has it). No JNA, no new dep, native-image clean.

Passthrough bridge on top of the FFM pseudo-terminal (internal.foundation.pty).

The problem it solves: a background `shell` child runs INSIDE the vis process (the FFM
PTY master fd + reader thread live in vis's heap). That's great for the agent
(the shell send / logs ops) but a HUMAN can't jump into the live terminal to
finish a step the agent can't — click through a browser OAuth, answer a prompt
only a person can. tmux gets that for free because its server is a separate
daemon you can `tmux attach` to; the FFM child is not.

This namespace restores that capability WITHOUT tmux: each background PTY
optionally exposes a per-shell UNIX-DOMAIN SOCKET. vis is the server (it holds
the master fd); `vis extension shell attach <id>` is a thin client the human runs in
their OWN Terminal.app. On connect the server (a) tees live master output to the
socket and (b) forwards the socket's bytes to the master (stdin) — a genuine
bidirectional passthrough. Multiple humans can attach at once; detaching just
drops the socket and leaves the child running (exactly like `tmux detach`).

Everything here is stdlib: `java.nio.channels` AF_UNIX sockets (JDK 16+,
already in vis's native-image reachability metadata) on the server side, and
`stty` for raw mode on the client side (the human's interactive shell always
has it). No JNA, no new dep, native-image clean.
raw docstring

com.blockether.vis.internal.foundation.self-docs

Vis self-documentation lookup — the vis_docs sandbox tool.

Vis ships its documentation as embedded markdown pages under vis-docs/ on the classpath (the same corpus the website and the gateway /docs site render, discovered via each artifact's vis-docs/vis-docs.edn manifest — so extensions' doc pages are lookup-able too). This namespace exposes that corpus to the MODEL through one observation tool so vis can answer questions about ITSELF — features, configuration, how to write an extension — from its real docs instead of guessing.

Progressive disclosure: CORE routes vis questions here; page content is only paid for when actually fetched.

Vis self-documentation lookup — the `vis_docs` sandbox tool.

Vis ships its documentation as embedded markdown pages under
`vis-docs/` on the classpath (the same corpus the website and the
gateway `/docs` site render, discovered via each artifact's
`vis-docs/vis-docs.edn` manifest — so extensions' doc pages are
lookup-able too). This namespace exposes that corpus to the MODEL
through one observation tool so vis can answer questions about
ITSELF — features, configuration, how to write an extension — from
its real docs instead of guessing.

Progressive disclosure: CORE routes vis questions here; page content is
only paid for when actually fetched.
raw docstring

com.blockether.vis.internal.foundation.session-slashes

Declarative session-level slash commands shared by every channel.

These are channel-agnostic: the engine dispatches them for every channel through the same slash/dispatch path, and each handler mutates state via the gateway so the change fans out everywhere.

/rename <new title> set this session's title

/rename routes through titling/set-title-with-broadcast! — the single title mutation point.

Declarative session-level slash commands shared by every channel.

These are channel-agnostic: the engine dispatches them for every channel
through the same `slash/dispatch` path, and each
handler mutates state via the gateway so the change fans out everywhere.

  /rename <new title>   set this session's title

`/rename` routes through `titling/set-title-with-broadcast!` — the single
title mutation point.
raw docstring

com.blockether.vis.internal.foundation.shell

shell/ compatibility extension — a DROPPABLE classpath plug-in (drop the jar, drop the feature). Bound only when the user-owned shell toggle is ON (default ON; flip it OFF in Settings or in vis.yml via toggles: {shell: false} to drop the tools). The OS process jail is the containment layer while active.

ONE model-facing binding — shell — bound BARE in the flat Python sandbox next to git / cat / grep. There is no shell_run / shell_bg / shell_logs / shell_send quartet any more: four names for ONE subsystem meant four call shapes and four result shapes for what is a single process lifecycle. One tool, one op grammar, one TOTAL result map:

  1. RUN (default) await shell(cmd) / await shell(cmd, opts)bash -lc in the workspace root, waits up to a timeout. Output is bounded at READ time to a head+tail budget per stream, so only the MIDDLE of a huge stream is dropped, never its start or end (a chatty-then-killed command cannot balloon the heap). A non-zero exit is DATA the model reads, not an error.

  2. BACKGROUND await shell(cmd, {"op": "background", "id": "dev"}) — an id makes it background (the op may stay implicit): spawned under a REAL pty, its merged output pumped into a bounded ring buffer, registered as a session RESOURCE in internal.resources (footer count, F4 dialog, resources ctx block). Prefer this for long builds, test suites, servers, watchers, and interactive commands; reserve run for short bounded work.

  3. LOGS / SEND / STOP await shell({"op": "logs", "id": "dev"}) — tail the ring buffer, type into the pty, or kill the tree and drop the resource. Shell stop and await resource_stop(id) land on the same resources/stop!. An EXITED process is not auto-pruned, so its output and exit code stay readable until it is stopped.

Commands have ONE spelling in Python: the first positional argument for run/background. Resource IDs also have ONE spelling: id inside the options map for background/logs/send/stop. Native JSON still carries a cmd property; the symbol's :call shape converts that transport field to the Python positional before dispatch.

Every op answers with the SAME key set (shell-result-base): keys a stage does not fill are nil / false / 0 instead of absent, so model Python indexes any field without a KeyError.

The shell toggle is registered HERE, extension-owned under the vis namespace.

`shell/` compatibility extension — a DROPPABLE classpath plug-in (drop the
jar, drop the feature). Bound only when the user-owned `shell` toggle is ON
(default ON; flip it OFF in Settings or in `vis.yml` via `toggles: {shell: false}`
to drop the tools). The OS process jail is the containment layer while active.

ONE model-facing binding — `shell` — bound BARE in the flat Python sandbox
next to `git` / `cat` / `grep`. There is no `shell_run` / `shell_bg` /
`shell_logs` / `shell_send` quartet any more: four names for ONE subsystem
meant four call shapes and four result shapes for what is a single process
lifecycle. One tool, one `op` grammar, one TOTAL result map:

1. RUN (default) `await shell(cmd)` / `await shell(cmd, opts)` — `bash -lc` in the
   workspace root, waits up to a timeout. Output is bounded at READ time to a
   head+tail budget per stream, so only the MIDDLE of a huge stream is
   dropped, never its start or end (a chatty-then-killed command cannot
   balloon the heap). A non-zero exit is DATA the model reads, not an error.

2. BACKGROUND `await shell(cmd, {"op": "background", "id": "dev"})` — an `id`
   makes it background (the op may stay implicit): spawned under a REAL pty,
   its merged output pumped into a bounded ring buffer, registered as a session
   RESOURCE in `internal.resources` (footer count, F4 dialog, `resources` ctx
   block). Prefer this for long builds, test suites, servers, watchers, and
   interactive commands; reserve run for short bounded work.

3. LOGS / SEND / STOP `await shell({"op": "logs", "id": "dev"})` — tail the ring
   buffer, type into the pty, or kill the tree and drop the resource. Shell stop
   and `await resource_stop(id)` land on the same `resources/stop!`. An EXITED process
   is not auto-pruned, so its output and exit code stay readable until it is
   stopped.

Commands have ONE spelling in Python: the first positional argument for
run/background. Resource IDs also have ONE spelling: `id` inside the options map
for background/logs/send/stop. Native JSON still carries a `cmd` property; the symbol's
`:call` shape converts that transport field to the Python positional before
dispatch.

Every op answers with the SAME key set (`shell-result-base`): keys a stage
does not fill are nil / false / 0 instead of absent, so model Python indexes
any field without a KeyError.

The `shell` toggle is registered HERE, extension-owned under the vis namespace.
raw docstring

com.blockether.vis.internal.foundation.shim-attach

Built-in sandbox SHIM: vis_attach / vis_attach_bytes — the GENERIC producer twin of the matplotlib capture. A tool running in python_execution writes any artifact (a PNG it rendered, a CSV/JSON/PDF/wav it built, whatever) and hands it to vis_attach(path) (or vis_attach_bytes(data, name)); the engine then OWNS the bytes as a durable session_iteration_attachment row, exactly like a matplotlib figure — surviving a web/TUI restart and (for image media-types) replayable to a vision model cross-turn.

No parsing, no round-trip through the model-facing stdout: we control the whole boundary. The Python side reads the file through the sandbox's OWN confined open (so filesystem-root confinement is enforced for free — a path outside the roots raises the normal sandbox error), sniffs the media-type (magic bytes then extension then utf-8 probe), base64-encodes, and calls the tiny host bridge __vis_record_attachment__, which appends the attachment map to the per-block *image-sink* (mpl-capture/record-attachment!). run-python-block drains that sink into the block outcome's :attachments; the loop stamps each with the producing block's tool-call-id and hands them to db-store-iteration!'s :attachments.

Registered unconditionally as a foundation shim (like shim-yaml / shim-matplotlib): its :ext/sandbox-shims entry autoloads vis_attach into every sandbox (main + every sub_loop fork).

Built-in sandbox SHIM: `vis_attach` / `vis_attach_bytes` — the GENERIC
producer twin of the matplotlib capture. A tool running in `python_execution`
writes any artifact (a PNG it rendered, a CSV/JSON/PDF/wav it built, whatever)
and hands it to `vis_attach(path)` (or `vis_attach_bytes(data, name)`); the
engine then OWNS the bytes as a durable `session_iteration_attachment` row,
exactly like a matplotlib figure — surviving a web/TUI restart and (for image
media-types) replayable to a vision model cross-turn.

No parsing, no round-trip through the model-facing stdout: we control the whole
boundary. The Python side reads the file through the sandbox's OWN confined
`open` (so filesystem-root confinement is enforced for free — a path outside
the roots raises the normal sandbox error), sniffs the media-type (magic bytes
then extension then utf-8 probe), base64-encodes, and calls the tiny host
bridge `__vis_record_attachment__`, which appends the attachment map to the
per-block `*image-sink*` (`mpl-capture/record-attachment!`). `run-python-block`
drains that sink into the block outcome's `:attachments`; the loop stamps each with
the producing block's tool-call-id and hands them to `db-store-iteration!`'s
`:attachments`.

Registered unconditionally as a foundation shim (like shim-yaml /
shim-matplotlib): its `:ext/sandbox-shims` entry autoloads `vis_attach` into
every sandbox (main + every `sub_loop` fork).
raw docstring

com.blockether.vis.internal.foundation.shim-bs4

Built-in sandbox SHIM: a bs4 (BeautifulSoup)-compatible module for the model's Python sandbox, implemented in PURE Python on the stdlib html.parser — NO host/JVM bridge, NOT a line of Clojure or babashka. bs4 is a third-party wheel that does not ship in GraalPy, so agents that reach for from bs4 import BeautifulSoup (the natural partner to the requests shim: fetch then parse) would otherwise hit ModuleNotFoundError; this extension contributes a :ext/sandbox-shims entry that env-python/build-agent-context installs into every sandbox Context (main + every sub_loop fork).

It builds a Tag / NavigableString tree via html.parser, with find/find_all (name/attrs/class_/id/string/recursive/limit), CSS .select / .select_one (type / #id / .class / [attr]/[attr=v]/~=/^=/$=/ *=, descendant + > child combinators, comma groups), get_text, .string/.strings/.stripped_strings, sibling/parent navigation, dynamic soup.tagname access, and HTML serialization. A deliberate subset of full bs4 (no lxml, no advanced CSS pseudo-classes).

Like shim-requests there are NO :shim/bindings: the shim is a self-contained Python preamble with zero host callables. It publishes a bs4 module (+ bs4.element) into sys.modules (so from bs4 import BeautifulSoup works) and staples BeautifulSoup onto builtins.

Built-in sandbox SHIM: a `bs4` (BeautifulSoup)-compatible module for the
model's Python sandbox, implemented in PURE Python on the stdlib
`html.parser` — NO host/JVM bridge, NOT a line of Clojure or babashka. bs4 is
a third-party wheel that does not ship in GraalPy, so agents that reach for
`from bs4 import BeautifulSoup` (the natural partner to the `requests` shim:
fetch then parse) would otherwise hit ModuleNotFoundError; this extension
contributes a `:ext/sandbox-shims` entry that `env-python/build-agent-context`
installs into every sandbox Context (main + every `sub_loop` fork).

It builds a `Tag` / `NavigableString` tree via `html.parser`, with
`find`/`find_all` (name/attrs/class_/id/string/recursive/limit), CSS `.select`
/ `.select_one` (type / `#id` / `.class` / `[attr]`/`[attr=v]`/`~=`/`^=`/`$=`/
`*=`, descendant + `>` child combinators, comma groups), `get_text`,
`.string`/`.strings`/`.stripped_strings`, sibling/parent navigation, dynamic
`soup.tagname` access, and HTML serialization. A deliberate subset of full
bs4 (no lxml, no advanced CSS pseudo-classes).

Like `shim-requests` there are NO `:shim/bindings`: the shim is a
self-contained Python preamble with zero host callables. It publishes a `bs4`
module (+ `bs4.element`) into `sys.modules` (so `from bs4 import BeautifulSoup`
works) and staples `BeautifulSoup` onto builtins.
raw docstring

com.blockether.vis.internal.foundation.shim-fonttools

Built-in sandbox SHIM: a fontTools/brotli pair for the model's Python sandbox, scoped to WOFF2 -> TTF conversion. The fonttools and brotli PyPI packages are not in GraalPy and cannot be pip-installed, so an agent that reaches for from fontTools.ttLib.woff2 import decompress (or import brotli) would otherwise hit ModuleNotFoundError. This extension contributes a :ext/sandbox-shims entry that env-python installs into every sandbox Context (main + every sub_loop fork).

Why this exists: WOFF2 web fonts are Brotli-compressed sfnt data, so PIL (ImageFont.truetype) cannot load a .woff2 directly - it needs a real .ttf/.otf. Converting WOFF2 -> TTF is exactly what fontTools' woff2 module does, and it needs a Brotli decoder. Both are absent, so brand text rendering with PIL dead-ends. This shim closes that gap fully in pure Python.

Backing: the vendored brotlidecpy decoder (Sidney Markowitz, MIT) provides Brotli decompression and its 122784-byte dictionary; an inlined WOFF2 reader rebuilds the sfnt, undoing the glyf/loca transforms (composite glyphs copied verbatim, simple glyphs re-encoded). Verified against fontTools 4.63 on Inter, Roboto (hinted) and NotoSans - ~10800 glyphs, identical outlines; the reconstructed TTF renders pixel-identical to the vendor TTF under the PIL shim on GraalPy.

Supported: fontTools.ttLib.woff2.decompress(input, output) and brotli.decompress(bytes). NOT supported: brotli.compress (raises NotImplementedError), WOFF1, and the wider fontTools.ttLib.TTFont API - this is a correctness-focused WOFF2->TTF subset, not full fontTools.

Like shim-pil there are NO :shim/bindings: a self-contained Python preamble with zero host callables. Publishes fontTools, fontTools.ttLib, fontTools.ttLib.woff2 and brotli (plus brotlidecpy) into sys.modules and staples fontTools/brotli onto builtins. No pip, no native wheel, no host binary.

Built-in sandbox SHIM: a `fontTools`/`brotli` pair for the model's Python
sandbox, scoped to WOFF2 -> TTF conversion. The `fonttools` and `brotli`
PyPI packages are not in GraalPy and cannot be pip-installed, so an agent
that reaches for `from fontTools.ttLib.woff2 import decompress` (or
`import brotli`) would otherwise hit ModuleNotFoundError. This extension
contributes a `:ext/sandbox-shims` entry that env-python installs into every
sandbox Context (main + every `sub_loop` fork).

Why this exists: WOFF2 web fonts are Brotli-compressed sfnt data, so PIL
(`ImageFont.truetype`) cannot load a `.woff2` directly - it needs a real
`.ttf`/`.otf`. Converting WOFF2 -> TTF is exactly what fontTools' woff2 module
does, and it needs a Brotli decoder. Both are absent, so brand text rendering
with PIL dead-ends. This shim closes that gap fully in pure Python.

Backing: the vendored `brotlidecpy` decoder (Sidney Markowitz, MIT) provides
Brotli decompression and its 122784-byte dictionary; an inlined WOFF2 reader
rebuilds the sfnt, undoing the glyf/loca transforms (composite glyphs copied
verbatim, simple glyphs re-encoded). Verified against fontTools 4.63 on
Inter, Roboto (hinted) and NotoSans - ~10800 glyphs, identical outlines; the
reconstructed TTF renders pixel-identical to the vendor TTF under the PIL
shim on GraalPy.

Supported: `fontTools.ttLib.woff2.decompress(input, output)` and
`brotli.decompress(bytes)`. NOT supported: `brotli.compress` (raises
NotImplementedError), WOFF1, and the wider `fontTools.ttLib.TTFont` API -
this is a correctness-focused WOFF2->TTF subset, not full fontTools.

Like `shim-pil` there are NO `:shim/bindings`: a self-contained Python
preamble with zero host callables. Publishes `fontTools`, `fontTools.ttLib`,
`fontTools.ttLib.woff2` and `brotli` (plus `brotlidecpy`) into `sys.modules`
and staples `fontTools`/`brotli` onto builtins. No pip, no native wheel, no
host binary.
raw docstring

com.blockether.vis.internal.foundation.shim-httpx

Built-in sandbox SHIM: an httpx-compatible module for the model's Python sandbox, implemented as a thin synchronous wrapper over the already-installed requests shim (which itself rides the sandbox socket via stdlib urllib and honours the network guard). No pip, no native wheel, no host bridge.

The preamble publishes an httpx module into sys.modules (so import httpx and httpx.get(...) work) and staples it onto builtins. It exposes the sync surface agents actually reach for: module-level get/post/put/patch/delete/ head/options/request, a Client (with base_url, default headers/params, context-manager support), an httpx-style Response (.status_code, .text, .content, .json(), .headers, .url, .is_success/.is_error/.is_redirect, .raise_for_status()), Headers, URL, Timeout, and the httpx exception tree (HTTPError, RequestError, HTTPStatusError, TimeoutException, ConnectError). Async is supported too: an AsyncClient whose request/get/ post/put/patch/delete/head/options are awaitable coroutines (with aclose and async with support) over the same sync core.

Built-in sandbox SHIM: an `httpx`-compatible module for the model's Python
sandbox, implemented as a thin synchronous wrapper over the already-installed
`requests` shim (which itself rides the sandbox socket via stdlib urllib and
honours the network guard). No pip, no native wheel, no host bridge.

The preamble publishes an `httpx` module into `sys.modules` (so `import httpx`
and `httpx.get(...)` work) and staples it onto builtins. It exposes the sync
surface agents actually reach for: module-level `get/post/put/patch/delete/
head/options/request`, a `Client` (with `base_url`, default headers/params,
context-manager support), an httpx-style `Response` (`.status_code`, `.text`,
`.content`, `.json()`, `.headers`, `.url`, `.is_success/.is_error/.is_redirect`,
`.raise_for_status()`), `Headers`, `URL`, `Timeout`, and the `httpx` exception
tree (`HTTPError`, `RequestError`, `HTTPStatusError`, `TimeoutException`,
`ConnectError`). Async is supported too: an `AsyncClient` whose `request/get/
post/put/patch/delete/head/options` are awaitable coroutines (with `aclose` and
`async with` support) over the same sync core.
raw docstring

com.blockether.vis.internal.foundation.shim-matplotlib

Built-in sandbox SHIM: a minimal matplotlib.pyplot-compatible module for the model's Python sandbox, backed by a pure-JVM Java2D renderer. The agent sandbox ships no CPython matplotlib wheel (it needs numpy's native core + freetype, all blocked by the deny-by-default Context); this extension instead contributes a :ext/sandbox-shims entry whose Python preamble accumulates the familiar pyplot artists (plot/scatter/bar/hist/fill_between/step/ pie/axhline/axvline/title/xlabel/… plus the OO subplots/Axes API) and whose savefig DELEGATES the whole figure spec across the boundary to the host callable __vis_mpl_render__, which draws it with java.awt/ ImageIO and returns a base64 PNG. The Python side base64-decodes and writes it (to a path, confined to the sandbox roots, or to any file-like buffer).

It is a SUBSET, not real matplotlib: line/scatter/bar/hist/fill/step/pie with title, axis labels, grid, legend, dashed line styles, markers, log scales and text annotations — enough for the model to visualize data. Rendering runs entirely on the JVM (no pip, no native wheels), and any render failure surfaces to Python as a catchable exception (never crashes the sandbox).

Together with shim-yaml this demonstrates the sandbox-shim mechanism: an extension turns a host / JVM capability into a real importable Python module while env-python stays completely generic about which shims exist.

Built-in sandbox SHIM: a minimal `matplotlib.pyplot`-compatible module for the
model's Python sandbox, backed by a pure-JVM Java2D renderer. The agent
sandbox ships no CPython matplotlib wheel (it needs numpy's native core +
freetype, all blocked by the deny-by-default Context); this extension instead
contributes a `:ext/sandbox-shims` entry whose Python preamble accumulates the
familiar pyplot artists (`plot`/`scatter`/`bar`/`hist`/`fill_between`/`step`/
`pie`/`axhline`/`axvline`/`title`/`xlabel`/… plus the OO `subplots`/`Axes`
API) and whose `savefig` DELEGATES the whole figure spec across the boundary
to the host callable `__vis_mpl_render__`, which draws it with `java.awt`/
`ImageIO` and returns a base64 PNG. The Python side base64-decodes and writes
it (to a path, confined to the sandbox roots, or to any file-like buffer).

It is a SUBSET, not real matplotlib: line/scatter/bar/hist/fill/step/pie with
title, axis labels, grid, legend, dashed line styles, markers, log scales and
text annotations — enough for the model to visualize data. Rendering runs
entirely on the JVM (no pip, no native wheels), and any render failure
surfaces to Python as a catchable exception (never crashes the sandbox).

Together with `shim-yaml` this demonstrates the sandbox-shim mechanism: an
extension turns a host / JVM capability into a real importable Python module
while `env-python` stays completely generic about which shims exist.
raw docstring

com.blockether.vis.internal.foundation.shim-nippy

Built-in sandbox SHIM exposing Vis's Nippy persistence codec to Python.

nippy_decode(bytes) decodes trusted Vis-owned Nippy BLOBs (for example session_turn_iteration.forms, session_turn_state.ctx, and session_turn_state.error) into native Python data. Nippy-stream Vectorz vectors decode as Python lists. nippy_encode(value) performs the inverse for Python plain data. The same functions are available as nippy.decode / nippy.encode after import nippy.

The Python module and Vectorz codec registration are lazy: neither runs during sandbox context initialization; Vectorz installs on the first codec call. Decoded Clojure values cross the normal sandbox boundary: map keys become canonical snake_case strings, keyword/symbol values become strings, dates become epoch milliseconds, and unsupported leaves stringify. This is for inspection and plain-data round trips, not exact Clojure type preservation. Java Serializable fallback is disabled in both directions.

Built-in sandbox SHIM exposing Vis's Nippy persistence codec to Python.

`nippy_decode(bytes)` decodes trusted Vis-owned Nippy BLOBs (for example
`session_turn_iteration.forms`, `session_turn_state.ctx`, and
`session_turn_state.error`) into native Python data. Nippy-stream Vectorz
vectors decode as Python lists. `nippy_encode(value)` performs the inverse for
Python plain data. The same functions are available as `nippy.decode` /
`nippy.encode` after `import nippy`.

The Python module and Vectorz codec registration are lazy: neither runs during
sandbox context initialization; Vectorz installs on the first codec call.
Decoded Clojure values cross the normal sandbox boundary: map keys become
canonical snake_case strings, keyword/symbol values become strings, dates
become epoch milliseconds, and unsupported leaves stringify. This is for
inspection and plain-data round trips, not exact Clojure type preservation.
Java Serializable fallback is disabled in both directions.
raw docstring

com.blockether.vis.internal.foundation.shim-numpy

Built-in sandbox SHIM: a numpy-compatible module for the model's Python sandbox, implemented in PURE Python (stdlib math + random) — NO host/JVM bridge, NOT a line of Clojure or babashka. numpy is a native C wheel that does not ship in GraalPy, so agents that reach for import numpy would otherwise hit ModuleNotFoundError; this extension contributes a :ext/sandbox-shims entry that env-python/build-agent-context installs into every sandbox Context (main + every sub_loop fork).

The shim is a correctness-focused SUBSET, not a C-speed numpy: an ndarray backed by a flat Python list + shape tuple, with broadcasting, reductions, ufuncs, fancy/boolean/slice indexing, dot/matmul, a linalg submodule (norm/det/inv/solve/matrix_power/matrix_rank via pure-Python Gaussian elimination) and a random submodule (stdlib random). Big arrays are slow; the goal is that agent glue code (np.array, arithmetic, mean/sum, small linear algebra) just works.

Like shim-requests there are NO :shim/bindings: the shim is a self-contained Python preamble with zero host callables. It publishes a numpy module into sys.modules (so import numpy works) and staples it onto builtins (so numpy.array(...) works with NO import, like json/os).

Built-in sandbox SHIM: a `numpy`-compatible module for the model's Python
sandbox, implemented in PURE Python (stdlib math + random) — NO host/JVM
bridge, NOT a line of Clojure or babashka. numpy is a native C wheel that does
not ship in GraalPy, so agents that reach for `import numpy` would otherwise
hit ModuleNotFoundError; this extension contributes a `:ext/sandbox-shims`
entry that `env-python/build-agent-context` installs into every sandbox
Context (main + every `sub_loop` fork).

The shim is a correctness-focused SUBSET, not a C-speed numpy: an `ndarray`
backed by a flat Python list + shape tuple, with broadcasting, reductions,
ufuncs, fancy/boolean/slice indexing, `dot`/`matmul`, a `linalg` submodule
(norm/det/inv/solve/matrix_power/matrix_rank via pure-Python Gaussian
elimination) and a `random` submodule (stdlib random). Big arrays are slow;
the goal is that agent glue code (`np.array`, arithmetic, `mean`/`sum`, small
linear algebra) just works.

Like `shim-requests` there are NO `:shim/bindings`: the shim is a
self-contained Python preamble with zero host callables. It publishes a
`numpy` module into `sys.modules` (so `import numpy` works) and staples it
onto builtins (so `numpy.array(...)` works with NO import, like json/os).
raw docstring

com.blockether.vis.internal.foundation.shim-pandas

Built-in sandbox SHIM: a pandas-compatible module for the model's Python sandbox, implemented in PURE Python (stdlib csv/json/math) — NO host/JVM bridge, NOT a line of Clojure or babashka. pandas is a native/heavy wheel that does not ship in GraalPy, so agents that reach for import pandas would otherwise hit ModuleNotFoundError; this extension contributes a :ext/sandbox-shims entry that env-python installs into every sandbox Context (main + every sub_loop fork).

The shim is a correctness-focused SUBSET, not C-speed pandas: a Series is a labelled 1-D column, a DataFrame is an ordered dict of columns. It covers construction (dict / records / list-of-lists / read_csv / read_json), []/loc/iloc selection, boolean masking, column arithmetic, groupby (sum/mean/min/max/count/size/agg), merge (inner/left/right/outer), concat, sort_values, describe, fillna/dropna, apply, a .str accessor, to_dict/to_csv/to_json and a pandas-style __repr__. It interoperates with the numpy shim (.values) when present. Big frames are slow; the goal is that agent glue code just works.

Like shim-numpy there are NO :shim/bindings: the shim is a self-contained Python preamble with zero host callables. It publishes a pandas module into sys.modules (so import pandas works) and staples it onto builtins (so pandas.DataFrame(...) works with NO import, like json/os).

Built-in sandbox SHIM: a `pandas`-compatible module for the model's Python
sandbox, implemented in PURE Python (stdlib csv/json/math) — NO host/JVM
bridge, NOT a line of Clojure or babashka. pandas is a native/heavy wheel that
does not ship in GraalPy, so agents that reach for `import pandas` would
otherwise hit ModuleNotFoundError; this extension contributes a
`:ext/sandbox-shims` entry that env-python installs into every sandbox Context
(main + every `sub_loop` fork).

The shim is a correctness-focused SUBSET, not C-speed pandas: a `Series` is a
labelled 1-D column, a `DataFrame` is an ordered dict of columns. It covers
construction (dict / records / list-of-lists / read_csv / read_json),
`[]`/`loc`/`iloc` selection, boolean masking, column arithmetic, `groupby`
(sum/mean/min/max/count/size/agg), `merge` (inner/left/right/outer),
`concat`, `sort_values`, `describe`, `fillna`/`dropna`, `apply`, a `.str`
accessor, `to_dict`/`to_csv`/`to_json` and a pandas-style `__repr__`. It
interoperates with the numpy shim (`.values`) when present. Big frames are
slow; the goal is that agent glue code just works.

Like `shim-numpy` there are NO `:shim/bindings`: the shim is a self-contained
Python preamble with zero host callables. It publishes a `pandas` module into
`sys.modules` (so `import pandas` works) and staples it onto builtins (so
`pandas.DataFrame(...)` works with NO import, like json/os).
raw docstring

com.blockether.vis.internal.foundation.shim-paramiko

Built-in sandbox SHIM: a paramiko-compatible SSH2 module backed by the pure-Java mwiede JSch fork (com.github.mwiede/jsch) so import paramiko works without the native CPython cryptography/cffi wheels. SSH sessions and SFTP channels live HOST-side (JSch Session/ChannelSftp in integer-keyed registries); the Python classes are thin handle wrappers, exchanging command/path strings and base64 file bytes across the boundary.

Built-in sandbox SHIM: a `paramiko`-compatible SSH2 module backed by the
pure-Java mwiede JSch fork (`com.github.mwiede/jsch`) so `import paramiko`
works without the native CPython cryptography/cffi wheels. SSH sessions and
SFTP channels live HOST-side (JSch `Session`/`ChannelSftp` in integer-keyed
registries); the Python classes are thin handle wrappers, exchanging
command/path strings and base64 file bytes across the boundary.
raw docstring

com.blockether.vis.internal.foundation.shim-pil

Built-in sandbox SHIM: a Pillow (PIL)-compatible PIL package for the model's Python sandbox, backed by the JVM's Java2D / ImageIO image stack. No CPython Pillow wheel ships in the sandbox; this extension contributes a :ext/sandbox-shims entry that env-python/build-agent-context installs into every sandbox Context (main + every sub_loop fork): the host bridge callables are wired onto the globals, then the Python preamble publishes a PIL package (with Image, ImageDraw, ImageFilter, ImageOps, ImageColor, ImageEnhance, ImageChops, ImageFont, ImageMath submodules) into sys.modules (so from PIL import Image works) and staples them onto builtins.

Images live HOST-side as BufferedImages in a per-JVM registry keyed by an integer handle; the Python Image object is a thin handle wrapper. All pixel ops, drawing, filtering, geometry and codec work happen on the JVM; only small metadata vectors and base64 blobs cross the strings-only boundary. Mirrors the shim-matplotlib Java2D approach and reuses mpl-capture/record-attachment! so Image.show() surfaces the image inline as a session attachment.

Built-in sandbox SHIM: a Pillow (PIL)-compatible `PIL` package for the model's
Python sandbox, backed by the JVM's Java2D / ImageIO image stack. No CPython
Pillow wheel ships in the sandbox; this extension contributes a
`:ext/sandbox-shims` entry that `env-python/build-agent-context` installs into
every sandbox Context (main + every `sub_loop` fork): the host bridge callables
are wired onto the globals, then the Python preamble publishes a `PIL` package
(with `Image`, `ImageDraw`, `ImageFilter`, `ImageOps`, `ImageColor`,
`ImageEnhance`, `ImageChops`, `ImageFont`, `ImageMath` submodules) into
`sys.modules` (so `from PIL import Image` works) and staples them onto builtins.

Images live HOST-side as `BufferedImage`s in a per-JVM registry keyed by an
integer handle; the Python `Image` object is a thin handle wrapper. All pixel
ops, drawing, filtering, geometry and codec work happen on the JVM; only small
metadata vectors and base64 blobs cross the strings-only boundary. Mirrors the
`shim-matplotlib` Java2D approach and reuses `mpl-capture/record-attachment!`
so `Image.show()` surfaces the image inline as a session attachment.
raw docstring

com.blockether.vis.internal.foundation.shim-pptx

Built-in sandbox SHIM: a pptx (python-pptx) compatible module backed by Apache POI XSLF (org.apache.poi/poi-ooxml) so from pptx import Presentation writes real .pptx files without the CPython package. Presentations/slides/ shapes live HOST-side in an integer registry; the Python classes are thin handle wrappers exchanging EMU geometry + base64 image/file bytes across the boundary.

Built-in sandbox SHIM: a `pptx` (python-pptx) compatible module backed by
Apache POI XSLF (`org.apache.poi/poi-ooxml`) so `from pptx import Presentation`
writes real .pptx files without the CPython package. Presentations/slides/
shapes live HOST-side in an integer registry; the Python classes are thin
handle wrappers exchanging EMU geometry + base64 image/file bytes across the
boundary.
raw docstring

com.blockether.vis.internal.foundation.shim-pytest

Built-in sandbox SHIM: a pytest-compatible module for the model's Python sandbox, implemented PURELY in Python on the stdlib (ast, inspect, linecache, traceback, warnings, tempfile) — NO host/JVM bridge, NOT a line of Clojure or babashka. pytest is a third-party wheel that does not ship in GraalPy, so agents writing full Python extensions and wanting to TEST them inline would otherwise hit ModuleNotFoundError; this extension contributes a :ext/sandbox-shims entry that env-python/build-agent-context installs into every sandbox Context (main + every sub_loop fork).

It is NOT real pytest: there is no pluggy/plugin system, no ini/plugin CLI (only -k / -x / --maxfail / -v), and no import-time assertion rewrite. It DOES do conftest.py fixture discovery (walked from the test file's dir up to the fs root, outer→inner) in disk mode. Instead it implements the subset that matters in an inline sandbox where the model writes tests + pytest.main() in ONE block:

  • collection of test_* functions and Test* classes (scoped to the CURRENT block via __vis_src__, so leftovers from earlier blocks in the shared globals are NOT swept in),
  • RUNTIME assert introspection (assert 2 == 3 reconstructed with operand values) done by registering __vis_src__ into linecache and walking the failing frame's AST — the same UX as pytest's rewrite, via a different mechanism,
  • pytest.raises / warns / approx / fail / skip / xfail / importorskip, @pytest.fixture (function/module/session scope, yield-teardown, autouse, recursive injection, parametrized fixtures via params/ids with request.param, request.getfixturevalue chains), @pytest.mark.parametrize (incl. indirect=) / skip / skipif / xfail / usefixtures (+ arbitrary marks), pytest.param, builtin fixtures request / monkeypatch / capsys / capfd / tmp_path / tmp_path_factory / tmpdir / tmpdir_factory / caplog / recwarn / pytester / testdir, conftest.py fixture discovery, and a pytest.main() runner (with -k keyword selection, -x / --maxfail fail-fast, and -v) that prints progress + failure reports
    • a summary line (incl. deselected counts) and returns an exit code.

Unlike shim-yaml/shim-matplotlib there are NO :shim/bindings: the shim is a self-contained Python preamble with zero host callables. It publishes a pytest module into sys.modules (so import pytest works) and staples it onto builtins (so pytest.raises(...) works with NO import, like json/os/requests).

Built-in sandbox SHIM: a `pytest`-compatible module for the model's Python
sandbox, implemented PURELY in Python on the stdlib (`ast`, `inspect`,
`linecache`, `traceback`, `warnings`, `tempfile`) — NO host/JVM bridge, NOT a
line of Clojure or babashka. `pytest` is a third-party wheel that does not
ship in GraalPy, so agents writing full Python extensions and wanting to
TEST them inline would otherwise hit ModuleNotFoundError; this extension
contributes a `:ext/sandbox-shims` entry that
`env-python/build-agent-context` installs into every sandbox Context (main +
every `sub_loop` fork).

It is NOT real pytest: there is no pluggy/plugin system, no ini/plugin CLI
(only `-k` / `-x` / `--maxfail` / `-v`), and no import-time assertion
rewrite. It DOES do `conftest.py` fixture discovery
(walked from the test file's dir up to the fs root, outer→inner) in disk
mode. Instead it
implements the subset that matters in an inline sandbox where the model
writes tests + `pytest.main()` in ONE block:

  - collection of `test_*` functions and `Test*` classes (scoped to the
    CURRENT block via `__vis_src__`, so leftovers from earlier blocks in the
    shared globals are NOT swept in),
  - RUNTIME assert introspection (`assert 2 == 3` reconstructed with operand
    values) done by registering `__vis_src__` into `linecache` and walking
    the failing frame's AST — the same UX as pytest's rewrite, via a
    different mechanism,
  - `pytest.raises` / `warns` / `approx` / `fail` / `skip` / `xfail` /
    `importorskip`, `@pytest.fixture` (function/module/session scope,
    yield-teardown, autouse, recursive injection, parametrized fixtures via
    `params`/`ids` with `request.param`, `request.getfixturevalue` chains),
    `@pytest.mark.parametrize` (incl. `indirect=`) / `skip` / `skipif` / `xfail`
    / `usefixtures` (+ arbitrary marks), `pytest.param`,
    builtin fixtures `request` / `monkeypatch` / `capsys` / `capfd` /
    `tmp_path` / `tmp_path_factory` / `tmpdir` / `tmpdir_factory` /
    `caplog` / `recwarn` / `pytester` / `testdir`, `conftest.py` fixture discovery,
    and a `pytest.main()` runner (with `-k` keyword selection, `-x` /
    `--maxfail` fail-fast, and `-v`) that prints progress + failure reports
    + a summary line (incl. deselected counts) and returns an exit code.

Unlike `shim-yaml`/`shim-matplotlib` there are NO `:shim/bindings`: the shim
is a self-contained Python preamble with zero host callables. It publishes a
`pytest` module into `sys.modules` (so `import pytest` works) and staples it
onto builtins (so `pytest.raises(...)` works with NO import, like
json/os/requests).
raw docstring

com.blockether.vis.internal.foundation.shim-requests

Built-in sandbox SHIM: a requests-compatible module for the model's Python sandbox, backed PURELY by the stdlib urllib.request — NO host/JVM bridge, NOT a line of Clojure or babashka. requests is a third-party wheel that does not ship in GraalPy, so agents that reach for import requests out of habit would otherwise hit ModuleNotFoundError; this extension contributes a :ext/sandbox-shims entry that env-python/build-agent-context installs into every sandbox Context (main + every sub_loop fork).

Because every call travels through the sandbox's OWN socket (urllib -> http.client -> socket), it automatically honours the network toggle (allowHostSocketAccess) AND the allow/deny + anti-SSRF network-guard-python — a JVM babashka.http-client bridge would open an egress path OUTSIDE the sandbox and disarm all of that, which is exactly why this stays 100% Python.

Unlike shim-yaml/shim-matplotlib there are NO :shim/bindings: the shim is a self-contained Python preamble with zero host callables. It publishes a requests module into sys.modules (so import requests works) and staples it onto builtins (so requests.get(...) works with NO import, like json/os).

Built-in sandbox SHIM: a `requests`-compatible module for the model's Python
sandbox, backed PURELY by the stdlib `urllib.request` — NO host/JVM bridge,
NOT a line of Clojure or babashka. `requests` is a third-party wheel that does
not ship in GraalPy, so agents that reach for `import requests` out of habit
would otherwise hit ModuleNotFoundError; this extension contributes a
`:ext/sandbox-shims` entry that `env-python/build-agent-context` installs into
every sandbox Context (main + every `sub_loop` fork).

Because every call travels through the sandbox's OWN socket (urllib ->
http.client -> socket), it automatically honours the network toggle
(`allowHostSocketAccess`) AND the allow/deny + anti-SSRF `network-guard-python`
— a JVM `babashka.http-client` bridge would open an egress path OUTSIDE the
sandbox and disarm all of that, which is exactly why this stays 100% Python.

Unlike `shim-yaml`/`shim-matplotlib` there are NO `:shim/bindings`: the shim is
a self-contained Python preamble with zero host callables. It publishes a
`requests` module into `sys.modules` (so `import requests` works) and staples
it onto builtins (so `requests.get(...)` works with NO import, like json/os).
raw docstring

com.blockether.vis.internal.foundation.shim-sqlite3

Built-in sandbox SHIM: a DB-API 2.0 sqlite3 module for the model's Python sandbox, backed by the JVM's xerial sqlite-jdbc driver (already on the classpath via the persistence extension, so no new dependency and native-image reachability is already configured). CPython's _sqlite3 native extension is absent in GraalPy, so import sqlite3 otherwise fails with ModuleNotFoundError.

Connections live HOST-side as java.sql.Connections in a per-JVM registry keyed by an integer handle; the Python Connection/Cursor are thin handle wrappers. SQL + params cross the strings-only boundary; result rows come back as vectors (BLOBs base64-tagged). :memory: databases are fully supported; a file path is opened host-side via jdbc:sqlite:<path>. Autocommit is on, so commit() is a no-op flush and data persists without it (the forgiving DB-API path).

Built-in sandbox SHIM: a DB-API 2.0 `sqlite3` module for the model's Python
sandbox, backed by the JVM's xerial `sqlite-jdbc` driver (already on the
classpath via the persistence extension, so no new dependency and native-image
reachability is already configured). CPython's `_sqlite3` native extension is
absent in GraalPy, so `import sqlite3` otherwise fails with ModuleNotFoundError.

Connections live HOST-side as `java.sql.Connection`s in a per-JVM registry keyed
by an integer handle; the Python `Connection`/`Cursor` are thin handle wrappers.
SQL + params cross the strings-only boundary; result rows come back as vectors
(BLOBs base64-tagged). `:memory:` databases are fully supported; a file path is
opened host-side via `jdbc:sqlite:<path>`. Autocommit is on, so `commit()` is a
no-op flush and data persists without it (the forgiving DB-API path).
raw docstring

com.blockether.vis.internal.foundation.shim-tabulate

Built-in sandbox SHIM: a tabulate-compatible module for the model's Python sandbox, implemented in PURE Python (stdlib only) — NO host/JVM bridge. The tabulate PyPI package is not in GraalPy, so agents that reach for from tabulate import tabulate would otherwise hit ModuleNotFoundError; this extension contributes a :ext/sandbox-shims entry that env-python installs into every sandbox Context (main + every sub_loop fork).

The shim covers the tablefmts agents reach for most: plain, simple, github, pipe, orgtbl, presto, grid, fancy_grid, rst, tsv and html, with numeric / string alignment, floatfmt, showindex, and headers='keys'/'firstrow'. It accepts list-of-lists, list-of-dicts, dict-of-lists, and duck-types the pandas shim's DataFrame. A correctness-focused SUBSET, not full tabulate.

Like shim-numpy there are NO :shim/bindings: a self-contained Python preamble with zero host callables. Publishes a tabulate module into sys.modules (so import tabulate works) and staples the tabulate fn onto builtins.

Built-in sandbox SHIM: a `tabulate`-compatible module for the model's Python
sandbox, implemented in PURE Python (stdlib only) — NO host/JVM bridge. The
`tabulate` PyPI package is not in GraalPy, so agents that reach for
`from tabulate import tabulate` would otherwise hit ModuleNotFoundError; this
extension contributes a `:ext/sandbox-shims` entry that env-python installs
into every sandbox Context (main + every `sub_loop` fork).

The shim covers the tablefmts agents reach for most: plain, simple, github,
pipe, orgtbl, presto, grid, fancy_grid, rst, tsv and html, with numeric /
string alignment, `floatfmt`, `showindex`, and `headers='keys'/'firstrow'`.
It accepts list-of-lists, list-of-dicts, dict-of-lists, and duck-types the
pandas shim's DataFrame. A correctness-focused SUBSET, not full tabulate.

Like `shim-numpy` there are NO `:shim/bindings`: a self-contained Python
preamble with zero host callables. Publishes a `tabulate` module into
`sys.modules` (so `import tabulate` works) and staples the `tabulate` fn onto
builtins.
raw docstring

com.blockether.vis.internal.foundation.shim-toml

Built-in sandbox SHIM: a toml-compatible module for the model's Python sandbox — NO host/JVM bridge. The toml PyPI package is not in GraalPy, so agents that reach for import toml would otherwise hit ModuleNotFoundError; this extension contributes a :ext/sandbox-shims entry that env-python installs into every sandbox Context (main + every sub_loop fork).

Reading (toml.loads/toml.load) delegates to the stdlib tomllib (present in GraalPy's 3.11 stdlib) for a spec-correct parse; writing (toml.dumps/toml.dump) is a pure-Python serializer covering scalars, arrays, inline tables, nested [table] sections and [[array.of.tables]]. A correctness-focused SUBSET of the toml package API.

Like shim-numpy there are NO :shim/bindings: a self-contained Python preamble with zero host callables. Publishes a toml module into sys.modules (so import toml works) and staples it onto builtins.

Built-in sandbox SHIM: a `toml`-compatible module for the model's Python
sandbox — NO host/JVM bridge. The `toml` PyPI package is not in GraalPy, so
agents that reach for `import toml` would otherwise hit ModuleNotFoundError;
this extension contributes a `:ext/sandbox-shims` entry that env-python
installs into every sandbox Context (main + every `sub_loop` fork).

Reading (`toml.loads`/`toml.load`) delegates to the stdlib `tomllib` (present
in GraalPy's 3.11 stdlib) for a spec-correct parse; writing
(`toml.dumps`/`toml.dump`) is a pure-Python serializer covering scalars,
arrays, inline tables, nested `[table]` sections and `[[array.of.tables]]`.
A correctness-focused SUBSET of the `toml` package API.

Like `shim-numpy` there are NO `:shim/bindings`: a self-contained Python
preamble with zero host callables. Publishes a `toml` module into
`sys.modules` (so `import toml` works) and staples it onto builtins.
raw docstring

com.blockether.vis.internal.foundation.shim-tzdata

Built-in sandbox SHIM: zoneinfo, pytz and dateutil for the model's Python sandbox, backed by the JVM's java.time IANA time-zone database.

GraalPy ships no writable filesystem, so the real zoneinfo / pytz / tzdata crash at import (_tzpath calls getcwd, which the denied FS refuses with an UN-catchable Java SecurityException that aborts the whole eval). This extension contributes a :ext/sandbox-shims entry that env-python/build-agent-context installs into every sandbox Context (main + every sub_loop fork): host callables resolve zone offsets / DST / names via java.time.ZoneId (604+ zones, no data files), then a Python preamble publishes zoneinfo, pytz, tzdata and the dateutil package (dateutil.tz, dateutil.parser, dateutil.relativedelta) into sys.modules and staples them onto builtins. All tz math happens on the JVM; only small metadata vectors cross the strings-only boundary. Kills the whole class of tz-aware-datetime failures.

Built-in sandbox SHIM: `zoneinfo`, `pytz` and `dateutil` for the model's Python
sandbox, backed by the JVM's `java.time` IANA time-zone database.

GraalPy ships no writable filesystem, so the real `zoneinfo` / `pytz` /
`tzdata` crash at import (`_tzpath` calls `getcwd`, which the denied FS refuses
with an UN-catchable Java `SecurityException` that aborts the whole eval). This
extension contributes a `:ext/sandbox-shims` entry that
`env-python/build-agent-context` installs into every sandbox Context (main +
every `sub_loop` fork): host callables resolve zone offsets / DST / names via
`java.time.ZoneId` (604+ zones, no data files), then a Python preamble publishes
`zoneinfo`, `pytz`, `tzdata` and the `dateutil` package (`dateutil.tz`,
`dateutil.parser`, `dateutil.relativedelta`) into `sys.modules` and staples them
onto builtins. All tz math happens on the JVM; only small metadata vectors cross
the strings-only boundary. Kills the whole class of tz-aware-`datetime` failures.
raw docstring

com.blockether.vis.internal.foundation.shim-urllib3

Built-in sandbox SHIM: a urllib3-compatible module for the model's Python sandbox, implemented as a thin wrapper over the already-installed requests shim (which rides the sandbox socket via stdlib urllib and honours the network guard). No pip, no native wheel, no host bridge.

The preamble publishes a urllib3 module (plus urllib3.exceptions) into sys.modules and staples it onto builtins. It exposes the surface agents reach for: PoolManager / HTTPConnectionPool / HTTPSConnectionPool with .request(method, url, fields=, headers=, body=, json=), a top-level urllib3.request(...) (urllib3 2.x), an HTTPResponse (.status, .data, .headers, .json(), .read(), .getheader()), HTTPHeaderDict, disable_warnings(), and the urllib3.exceptions tree (HTTPError, MaxRetryError, NewConnectionError, ReadTimeoutError, ProtocolError, InsecureRequestWarning). Real connection pooling / retries are no-ops.

Built-in sandbox SHIM: a `urllib3`-compatible module for the model's Python
sandbox, implemented as a thin wrapper over the already-installed `requests`
shim (which rides the sandbox socket via stdlib urllib and honours the network
guard). No pip, no native wheel, no host bridge.

The preamble publishes a `urllib3` module (plus `urllib3.exceptions`) into
`sys.modules` and staples it onto builtins. It exposes the surface agents
reach for: `PoolManager` / `HTTPConnectionPool` / `HTTPSConnectionPool` with
`.request(method, url, fields=, headers=, body=, json=)`, a top-level
`urllib3.request(...)` (urllib3 2.x), an `HTTPResponse` (`.status`, `.data`,
`.headers`, `.json()`, `.read()`, `.getheader()`), `HTTPHeaderDict`,
`disable_warnings()`, and the `urllib3.exceptions` tree (`HTTPError`,
`MaxRetryError`, `NewConnectionError`, `ReadTimeoutError`, `ProtocolError`,
`InsecureRequestWarning`). Real connection pooling / retries are no-ops.
raw docstring

com.blockether.vis.internal.foundation.shim-xlsxwriter

Built-in sandbox SHIM: an xlsxwriter-compatible module backed by Apache POI (org.apache.poi/poi-ooxml) so import xlsxwriter writes real .xlsx files without the CPython package. Workbooks/formats live HOST-side in an integer registry; the Python classes are thin handle wrappers; the finished file crosses the boundary as base64 bytes on close().

Built-in sandbox SHIM: an `xlsxwriter`-compatible module backed by Apache POI
(`org.apache.poi/poi-ooxml`) so `import xlsxwriter` writes real .xlsx files
without the CPython package. Workbooks/formats live HOST-side in an integer
registry; the Python classes are thin handle wrappers; the finished file
crosses the boundary as base64 bytes on `close()`.
raw docstring

com.blockether.vis.internal.foundation.shim-yaml

Built-in sandbox SHIM: a PyYAML-compatible yaml module for the model's Python sandbox, backed by the pure-Clojure YAMLStar loader (org.yamlstar/yamlstar). No CPython PyYAML wheel ships in the sandbox; this extension contributes a :ext/sandbox-shims entry that env-python/build-agent-context installs into every sandbox Context (main + every sub_loop fork): the host bridge callables are wired onto the globals, then the Python preamble publishes a yaml module into sys.modules (so import yaml works) and staples it onto builtins (so yaml.safe_load(...) works with NO import).

This is the reference example of the sandbox-shim mechanism: a host / JVM capability surfaced to sandbox Python as a real importable module, with the engine staying completely generic about which shims exist.

Built-in sandbox SHIM: a PyYAML-compatible `yaml` module for the model's
Python sandbox, backed by the pure-Clojure YAMLStar loader
(`org.yamlstar/yamlstar`). No CPython PyYAML wheel ships in the sandbox; this
extension contributes a `:ext/sandbox-shims` entry that
`env-python/build-agent-context` installs into every sandbox Context (main +
every `sub_loop` fork): the host bridge callables are wired onto the globals,
then the Python preamble publishes a `yaml` module into `sys.modules` (so
`import yaml` works) and staples it onto builtins (so `yaml.safe_load(...)`
works with NO import).

This is the reference example of the sandbox-shim mechanism: a host / JVM
capability surfaced to sandbox Python as a real importable module, with the
engine staying completely generic about which shims exist.
raw docstring

com.blockether.vis.internal.foundation.surface-contract

clojure.spec CONTRACT for the language-surface tool RESULTS (format_code, lint_code, run_tests).

Every language pack that registers a :format-fn / :lint-fn / :test-fn under :ext/language-tools returns a result map that MUST conform to these specs, so the shape is UNIFORM across packs (clojure, and a future python / js) and can never silently drift. Both results share the directory-nested by-dir grouping ({<dir> {<basename> <payload>}}) that writes each long directory prefix ONCE.

The result maps cross the strings-only Python boundary, so their keys are STRINGS ("op", "findings", "by-dir", ...). clojure.spec's s/keys only speaks keyword keys, so the map specs here are plain predicates over the string keys, composed from s/map-of / s/coll-of for the nested pieces.

check validates a result and returns it UNCHANGED, throwing ex-info with s/explain-data when it violates the contract — the schema check the packs run on every format/lint result before handing it back through the surface. capability->spec is the single source of truth mapping a capability keyword to its result spec.

clojure.spec CONTRACT for the language-surface tool RESULTS (`format_code`,
`lint_code`, `run_tests`).

Every language pack that registers a `:format-fn` / `:lint-fn` / `:test-fn`
under
`:ext/language-tools` returns a result map that MUST conform to these specs,
so the shape is UNIFORM across packs (clojure, and a future python / js) and
can never silently drift. Both results share the directory-nested `by-dir`
grouping (`{<dir> {<basename> <payload>}}`) that writes each long directory
prefix ONCE.

The result maps cross the strings-only Python boundary, so their keys are
STRINGS ("op", "findings", "by-dir", ...). clojure.spec's `s/keys` only
speaks keyword keys, so the map specs here are plain predicates over the
string keys, composed from `s/map-of` / `s/coll-of` for the nested pieces.

`check` validates a result and returns it UNCHANGED, throwing ex-info with
`s/explain-data` when it violates the contract — the schema check the packs
run on every format/lint result before handing it back through the surface.
`capability->spec` is the single source of truth mapping a capability keyword
to its result spec.
raw docstring

com.blockether.vis.internal.foundation.transcript

Full session transcript - DATA first, presentation second.

transcript returns one canonical Clojure map with every turn, every iteration, every executed block plus the LLM-side context (system prompt, message envelope, reasoning trace, top-level provider error, per-iteration vars, answer-form pointer, returned-empty-blocks flag) and the per-block forensic detail (code, comment, result, error, duration, timeout?, repaired?). Pure data. The agent can pattern-match on it; the CLI renders Markdown on top; a future TUI screen, JSON exporter, or analytics extension consumes the same shape.

Lives in foundation because it's an introspection surface, not host plumbing. The sandbox-visible public surface is (session-state) for data and (session-report-html) for an HTML report; this namespace owns the transcript portion behind that deeper interface.

Public Clojure surface:

(transcript db-info session-id) -> transcript data map (transcript->md data) -> Markdown string (transcript-md db-info session-id) -> DB lookup + Markdown string

Canonical data shape:

{:session {:id :title :channel :model :provider :created-at} :totals {:turns N :iterations N :tokens {:input :output :reasoning :cached} :cost-usd D} :dialog [{:role :turn-id :content}] :calls [{:kind :ref :parent-ref :turn-id :iteration-id :op :tool :var :code :status :duration-ms :command :target :result :result-summary :info}] :timeline [{:kind :ref :turn-id :iteration-id :content :code :status :duration-ms :result-summary}] :turns [{:id :user-request :status :prior-outcome :provider :model :iteration-count :failure-count :tokens :cost-usd :content :iterations [{:id :position :status :duration-ms :provider :model :thinking :error :tokens :cost-usd :answer-position :returned-empty-blocks? :vars [{:name :code :value :version}] :attachments [{:id :source :tool-call-id :position :kind :media-type :filename :size :stored}] :blocks [{:position :code :comment :result :stdout :error :duration-ms :timeout? :repaired?}]}]}]}

The Markdown renderer renders thinking, iteration-level errors, vars, per-block forensic previews, and final answer text. Large fields are bounded so reports stay safe to open.

Full session transcript - DATA first, presentation second.

`transcript` returns one canonical Clojure map with every turn,
every iteration, every executed block plus the LLM-side context
(system prompt, message envelope, reasoning trace, top-level
provider error, per-iteration vars, answer-form pointer,
returned-empty-blocks flag) and the per-block forensic detail
(code, comment, result, error, duration, timeout?, repaired?).
Pure data. The agent can pattern-match on it; the CLI
renders Markdown on top; a future TUI screen, JSON exporter, or
analytics extension consumes the same shape.

Lives in foundation because it's an introspection surface, not host
plumbing. The sandbox-visible public surface is `(session-state)`
for data and `(session-report-html)` for an HTML report; this namespace
owns the transcript portion behind that deeper interface.

Public Clojure surface:

  `(transcript      db-info session-id)`  -> transcript data map
  `(transcript->md  data)`             -> Markdown string
  `(transcript-md   db-info session-id)`  -> DB lookup + Markdown string

Canonical data shape:

  {:session {:id :title :channel :model :provider :created-at}
   :totals       {:turns N :iterations N
                  :tokens {:input :output :reasoning :cached}
                  :cost-usd D}
   :dialog      [{:role :turn-id :content}]
   :calls       [{:kind :ref :parent-ref :turn-id :iteration-id :op :tool
                  :var :code :status :duration-ms :command :target
                  :result :result-summary :info}]
   :timeline    [{:kind :ref :turn-id :iteration-id :content :code
                  :status :duration-ms :result-summary}]
   :turns
    [{:id :user-request :status :prior-outcome :provider :model
      :iteration-count :failure-count
      :tokens :cost-usd :content
      :iterations
       [{:id :position :status :duration-ms
         :provider :model :thinking :error
         :tokens :cost-usd
         :answer-position :returned-empty-blocks?
         :vars
         [{:name :code :value :version}]
         :attachments
         [{:id :source :tool-call-id :position :kind
           :media-type :filename :size :stored}]
         :blocks
         [{:position :code :comment :result :stdout :error
           :duration-ms :timeout? :repaired?}]}]}]}

The Markdown renderer renders thinking, iteration-level errors,
vars, per-block forensic previews, and final answer text. Large
fields are bounded so reports stay safe to open.
raw docstring

com.blockether.vis.internal.foundation.workspace-ctx

Pre-turn "session_workspace" CTX block (STRING-KEYED — crosses the Python boundary as session["workspace"]).

Sessions may work directly in trunk or inside an isolated backend workspace. That distinction is reported on "sandbox", NOT as a VCS. "vcs_kind" reports the underlying repository VCS ("git" when the root is inside a git repo, else "none") so it matches the git/ extension surface, which activates on the same predicate. The model reads the workspace block to know the active root and what it has changed since the fork. The block is stamped once per turn at engine start; ctx_renderer serialises it verbatim.

Pre-turn `"session_workspace"` CTX block (STRING-KEYED — crosses the
Python boundary as `session["workspace"]`).

Sessions may work directly in trunk or inside an isolated backend
workspace. That distinction is reported on `"sandbox"`, NOT
as a VCS. `"vcs_kind"` reports the underlying repository VCS (`"git"`
when the root is inside a git repo, else `"none"`) so it matches the
`git/` extension surface, which activates on the same predicate. The
model reads the workspace block to know the active root and what it has
changed since the fork. The block is stamped once per turn at engine
start; ctx_renderer serialises it verbatim.
raw docstring

com.blockether.vis.internal.foundation.workspace-slashes

Declarative /draft … slash tree.

Drafts are OPT-IN. By default a session works directly in the user's real cwd (trunk). /draft new <label> clones cwd into an isolated draft (an isolated workspace named <label>) and enters it; /draft apply lands the draft's changes into cwd and leaves the draft; /draft abandon discards it and leaves. The header shows <label> (DRAFT) while you're in one.

/draft show whether you're on trunk or in a draft /draft new <label> clone cwd into a draft named <label>, enter it /draft apply land the draft's changes into cwd, leave the draft /draft abandon [why] discard the draft, leave it /draft-blank <label> like /draft new, but the draft starts EMPTY — no files from the current HEAD are carried in

Filesystem (/cd) — session-scoped, every channel. What the jail ALLOWS is derived from config (jail.filesystem in vis.yml, global or project); /cd moves the session's PRIMARY LIVE root within that grant:

/cd [path] show / CHANGE the session's filesystem root

Vis owns no git lifecycle — apply copies the changed files into the user's real cwd, uncommitted. Handlers are PURE w.r.t. the channel.

Declarative `/draft …` slash tree.

Drafts are OPT-IN. By default a session works directly in the user's
real cwd (trunk). `/draft new <label>` clones cwd into an isolated
draft (an isolated workspace named `<label>`) and enters it; `/draft apply`
lands the draft's changes into cwd and leaves the draft; `/draft
abandon` discards it and leaves. The header shows `<label> (DRAFT)`
while you're in one.

  /draft                show whether you're on trunk or in a draft
  /draft new <label>    clone cwd into a draft named <label>, enter it
  /draft apply          land the draft's changes into cwd, leave the draft
  /draft abandon [why]  discard the draft, leave it
  /draft-blank <label>  like /draft new, but the draft starts EMPTY —
                        no files from the current HEAD are carried in

Filesystem (`/cd`) — session-scoped, every channel. What the jail ALLOWS is
derived from config (`jail.filesystem` in vis.yml, global or project); `/cd`
moves the session's PRIMARY LIVE root within that grant:

  /cd [path]            show / CHANGE the session's filesystem root

Vis owns no git lifecycle — `apply` copies the changed files into the
user's real cwd, uncommitted. Handlers are PURE w.r.t. the channel.
raw docstring

com.blockether.vis.internal.gateway-sandbox

Gateway-lifecycle SANDBOX CAPABILITY: ONE shared loopback egress proxy and ONE ephemeral MITM CA for the WHOLE daemon, keyed PER SESSION.

Why shared, not per-session (turn 32): the gateway is multi-tenant — many clients/sessions hit one daemon. A per-session proxy+CA thrashes listeners and mints a fresh CA per session; worse, every child would carry a different trust root. Instead there is ONE listener and ONE CA (the "same certs" property — every child trusts the same root), plus a REGISTRY mapping a per-session TOKEN → that session's live policy fn.

How a connection is attributed to a session: the jailed child's proxy env carries its unguessable token in the proxy URL userinfo (http://<token>@127.0.0.1:<port>); curl/git/requests/… send it back as Proxy-Authorization: Basic base64(<token>:); the proxy hands the token to resolve-policy, which looks up the registry.

FAIL-CLOSED: a request whose token is missing or not registered to a LIVE session is DENIED (a :deny-all? sentinel policy) — the shared door never serves a policy it cannot attribute. The token is a random UUID, so one session cannot reach another's (broader) policy by guessing.

Lazy: the proxy listener and the CA keygen happen only on first ensure-proxy! / ensure-ca! — a gateway that never jails a shell child opens neither.

Gateway-lifecycle SANDBOX CAPABILITY: ONE shared loopback egress proxy and ONE
ephemeral MITM CA for the WHOLE daemon, keyed PER SESSION.

Why shared, not per-session (turn 32): the gateway is multi-tenant — many
clients/sessions hit one daemon. A per-session proxy+CA thrashes listeners and
mints a fresh CA per session; worse, every child would carry a different trust
root. Instead there is ONE listener and ONE CA (the "same certs" property —
every child trusts the same root), plus a REGISTRY mapping a per-session TOKEN →
that session's live policy fn.

How a connection is attributed to a session: the jailed child's proxy env carries
its unguessable token in the proxy URL userinfo (`http://<token>@127.0.0.1:<port>`);
curl/git/requests/… send it back as `Proxy-Authorization: Basic base64(<token>:)`;
the proxy hands the token to `resolve-policy`, which looks up the registry.

FAIL-CLOSED: a request whose token is missing or not registered to a LIVE session
is DENIED (a `:deny-all?` sentinel policy) — the shared door never serves a policy
it cannot attribute. The token is a random UUID, so one session cannot reach
another's (broader) policy by guessing.

Lazy: the proxy listener and the CA keygen happen only on first `ensure-proxy!` /
`ensure-ca!` — a gateway that never jails a shell child opens neither.
raw docstring

com.blockether.vis.internal.gateway.bus

Cross-process gateway event bus.

The gateway's live event log + SSE fan-out (gateway.state) is a PROCESS-LOCAL in-memory registry: append-event! only reaches subscribers inside the SAME JVM. That is why a turn streaming in the TUI process is invisible to another process watching the SAME conversation — each process has its own registry, and the only thing they share is the persisted DB (which lands whole turns, not the live token stream). So two watchers never stream together.

This bus closes that gap with the simplest transport that needs no schema change and no always-on daemon: a shared append-only journal under ~/.vis/gateway/events/<sid>.ndjson. Every LOCALLY-produced gateway event is publish!ed there (one JSON line, tagged with this process's producer id). A background tailer in each process follows those files and re-delivers FOREIGN events (producer != self) into the local registry via a delivery fn wired by gateway.state — so every process's subscribers see the same stream, live.

Ordering/seq: exactly ONE turn runs per session at a time, so at any moment a single producer owns the stream and its monotonic :seq is authoritative for every watcher. The producer truncates the journal at each turn.started, bounding a file to one turn's worth of deltas; consumers detect the truncation (offset past EOF) and rewind.

Degrades safely: any IO failure is swallowed and the process falls back to today's in-process-only behavior.

Cross-process gateway event bus.

The gateway's live event log + SSE fan-out (`gateway.state`) is a
PROCESS-LOCAL in-memory registry: `append-event!` only reaches
subscribers inside the SAME JVM. That is why a turn streaming in the
TUI process is invisible to another process watching the SAME
conversation — each process has its own registry, and the only thing
they share is the persisted DB (which lands whole turns, not the live
token stream). So two watchers never stream together.

This bus closes that gap with the simplest transport that needs no
schema change and no always-on daemon: a shared append-only journal
under `~/.vis/gateway/events/<sid>.ndjson`. Every LOCALLY-produced
gateway event is `publish!`ed there (one JSON line, tagged with this
process's `producer` id). A background tailer in each process follows
those files and re-delivers FOREIGN events (producer != self) into the
local registry via a delivery fn wired by `gateway.state` — so every
process's subscribers see the same stream, live.

Ordering/seq: exactly ONE turn runs per session at a time, so at any
moment a single producer owns the stream and its monotonic `:seq` is
authoritative for every watcher. The producer truncates the journal at
each `turn.started`, bounding a file to one turn's worth of deltas;
consumers detect the truncation (offset past EOF) and rewind.

Degrades safely: any IO failure is swallowed and the process falls
back to today's in-process-only behavior.
raw docstring

com.blockether.vis.internal.gateway.client

HTTP/SSE client for the long-lived gateway daemon.

Interactive channels call this facade instead of gateway.state directly. It discover-or-starts the one daemon for the current DB, then speaks the same HTTP/SSE API every other client uses. This is the thin-client half of the gateway-daemon plan: token refresh, turn execution, and live streaming happen in ONE process.

HTTP/SSE client for the long-lived gateway daemon.

Interactive channels call this facade instead of `gateway.state` directly. It
discover-or-starts the one daemon for the current DB, then speaks the same
HTTP/SSE API every other client uses. This is the thin-client half of the
gateway-daemon plan: token refresh, turn execution, and live streaming happen
in ONE process.
raw docstring

com.blockether.vis.internal.gateway.discovery

Gateway daemon discovery + registry (build order step 1).

One long-lived gateway per DB owns execution; every TUI/web/whatever is a thin client of it. This namespace answers the boot-time question: "is a gateway already running for my DB — attach; else spawn one, DETACHED (nobody's child), then hand back where to connect."

Registry: one EDN file per DB at ~/.vis/gateway/registry/<sha256(db)>.edn holding {:pid :port :host :secret :db :created-at}. Freshness = the recorded :pid is still alive AND a caller-supplied probe confirms the port+secret are really OUR daemon (guards OS pid reuse — see D4/Q3 in TODO-gateway-daemon).

Design decisions (locked, see TODO-gateway-daemon.md):

  • Q2 registry key = the DB path (two dirs sharing --db share one daemon).
  • Q3 race = port-bind winner is the daemon; the loser attaches. The daemon SELF-REGISTERS on startup (via register-self! from serve-main!), so a spawner never needs the child pid — it just polls for a fresh registry.
  • Q5 :memory never registers/spawns (headless one-shot stays in-process).

Effects (spawn, probe, pid-liveness) are injectable so the orchestration in discover-or-start! is unit-testable without a real process.

Gateway daemon discovery + registry (build order step 1).

One long-lived gateway per DB owns execution; every TUI/web/whatever is a
thin client of it. This namespace answers the boot-time question: "is a
gateway already running for my DB — attach; else spawn one, DETACHED (nobody's
child), then hand back where to connect."

Registry: one EDN file per DB at `~/.vis/gateway/registry/<sha256(db)>.edn`
holding `{:pid :port :host :secret :db :created-at}`. Freshness = the recorded
`:pid` is still alive AND a caller-supplied `probe` confirms the port+secret
are really OUR daemon (guards OS pid reuse — see D4/Q3 in TODO-gateway-daemon).

Design decisions (locked, see TODO-gateway-daemon.md):
- Q2 registry key = the DB path (two dirs sharing `--db` share one daemon).
- Q3 race = port-bind winner is the daemon; the loser attaches. The daemon
  SELF-REGISTERS on startup (via [[register-self!]] from `serve-main!`), so a
  spawner never needs the child pid — it just polls for a fresh registry.
- Q5 `:memory` never registers/spawns (headless one-shot stays in-process).

Effects (`spawn`, `probe`, pid-liveness) are injectable so the orchestration
in [[discover-or-start!]] is unit-testable without a real process.
raw docstring

com.blockether.vis.internal.gateway.fcm

Android push (Firebase Cloud Messaging HTTP v1).

Apple's APNs lives in gateway.push; this is its Android twin and the two are dispatched on the registered device's :platform. Credentials are a Google service-account JSON — from the macOS keychain (service vis-fcm, account service_account), from the environment, or from a file under ~/.vis/fcm/. Key material is never returned, logged or sent over the wire.

Android push (Firebase Cloud Messaging HTTP v1).

Apple's APNs lives in `gateway.push`; this is its Android twin and the two
are dispatched on the registered device's `:platform`. Credentials are a
Google service-account JSON — from the macOS keychain (service `vis-fcm`,
account `service_account`), from the environment, or from a file under
`~/.vis/fcm/`. Key material is never returned, logged or sent over the wire.
raw docstring

com.blockether.vis.internal.gateway.pairing

Gateway pairing helpers for remote clients.

The QR payload is deliberately tiny and URL-shaped so native apps can scan it without an HTTP round trip:

vis://gateway?url=http%3A%2F%2F100.64.0.10%3A7890&token=...

Tailscale fits naturally: if a 100.64.0.0/10 interface is present we prefer it over LAN addresses, otherwise we fall back to site-local IPv4 addresses.

Gateway pairing helpers for remote clients.

The QR payload is deliberately tiny and URL-shaped so native apps can scan it
without an HTTP round trip:

  vis://gateway?url=http%3A%2F%2F100.64.0.10%3A7890&token=...

Tailscale fits naturally: if a 100.64.0.0/10 interface is present we prefer it
over LAN addresses, otherwise we fall back to site-local IPv4 addresses.
raw docstring

com.blockether.vis.internal.gateway.protocol

THE single source of truth for gateway <-> client version compatibility.

Vis ships three INDEPENDENTLY updatable halves that share one HTTP/SSE wire: the gateway daemon, the TUI/CLI client, and the Vis Companion app. Any of them can lag. A daemon started by yesterday's binary keeps running across brew upgrade; a phone holds a cached web build for weeks; a Tailscale peer runs last month's release. Without an explicit contract a breaking wire change surfaces as a mystery 404, a missing field, or an event type nobody renders.

So every peer publishes two numbers next to its human release version:

protocol the wire protocol IT speaks min-* the OLDEST counterpart it still speaks to

Compatibility is then a pure comparison (verdict) — never feature sniffing, never guessing from a release string. Both halves advertise, both halves judge, so whichever side is newer can explain the mismatch even when the other side is too old to know the concept exists.

Bump protocol-version on any BREAKING wire change (a removed field, a renamed event type, a changed status shape). Raise min-client-protocol or min-gateway-protocol only when the old shape genuinely stops working — additive changes keep the number and are negotiated through /v1/capabilities features instead.

Wire shape (snake_case strings, per the gateway wire contract):

{"protocol": 1, "min_client": 1, "min_gateway": 1, "version": "0.1.5"}

THE single source of truth for gateway <-> client version compatibility.

Vis ships three INDEPENDENTLY updatable halves that share one HTTP/SSE
wire: the gateway daemon, the TUI/CLI client, and the Vis Companion app.
Any of them can lag. A daemon started by yesterday's binary keeps running
across `brew upgrade`; a phone holds a cached web build for weeks; a
Tailscale peer runs last month's release. Without an explicit contract a
breaking wire change surfaces as a mystery 404, a missing field, or an
event type nobody renders.

So every peer publishes two numbers next to its human release version:

  `protocol`     the wire protocol IT speaks
  `min-*`        the OLDEST counterpart it still speaks to

Compatibility is then a pure comparison ([[verdict]]) — never feature
sniffing, never guessing from a release string. Both halves advertise, both
halves judge, so whichever side is newer can explain the mismatch even when
the other side is too old to know the concept exists.

Bump [[protocol-version]] on any BREAKING wire change (a removed field, a
renamed event type, a changed status shape). Raise [[min-client-protocol]]
or [[min-gateway-protocol]] only when the old shape genuinely stops
working — additive changes keep the number and are negotiated through
`/v1/capabilities` features instead.

Wire shape (snake_case strings, per the gateway wire contract):

  {"protocol": 1, "min_client": 1, "min_gateway": 1, "version": "0.1.5"}
raw docstring

com.blockether.vis.internal.gateway.push

Native push notifications (Apple Push Notification service).

ONE job: when a turn finishes on this gateway, wake the phone that asked to be woken. Everything here is server-side; the app only ever hands us a device token.

Three moving parts:

  1. Credentials. A token-based APNs auth key (.p8, ES256) plus its key id, the Apple team id and the app's bundle id (the APNs topic). Resolved from VIS_APNS_KEY_PATH / VIS_APNS_KEY_ID / VIS_APNS_TEAM_ID / VIS_APNS_TOPIC, else auto-discovered from ~/.vis/apns/AuthKey_<kid>.p8 (the key id is read off the filename) with the team/topic still from env or ~/.vis/apns/apns.edn. No credentials = push silently OFF; the gateway keeps working exactly as before.

  2. A device registry at ~/.vis/devices.edn — device token -> platform, APNs environment, client label/version, timestamps. Registration is idempotent on the token. Tokens are SECRETS: nothing here logs more than a masked prefix.

  3. A sender — a signed ES256 JWT (cached, refreshed well inside Apple's one-hour window) over HTTP/2 to api.push.apple.com. A device that registered with the wrong environment is retried once against the other host, and an APNs BadDeviceToken/Unregistered verdict evicts the device so a stale token cannot accumulate.

The wire surface lives in gateway.server (/v1/devices); this namespace knows nothing about Ring.

Native push notifications (Apple Push Notification service).

ONE job: when a turn finishes on this gateway, wake the phone that asked
to be woken. Everything here is server-side; the app only ever hands us a
device token.

Three moving parts:

1. **Credentials.** A token-based APNs auth key (`.p8`, ES256) plus its key
   id, the Apple team id and the app's bundle id (the APNs *topic*).
   Resolved from `VIS_APNS_KEY_PATH` / `VIS_APNS_KEY_ID` / `VIS_APNS_TEAM_ID`
   / `VIS_APNS_TOPIC`, else auto-discovered from `~/.vis/apns/AuthKey_<kid>.p8`
   (the key id is read off the filename) with the team/topic still from env
   or `~/.vis/apns/apns.edn`. No credentials = push silently OFF; the gateway
   keeps working exactly as before.

2. **A device registry** at `~/.vis/devices.edn` — device token -> platform,
   APNs environment, client label/version, timestamps. Registration is
   idempotent on the token. Tokens are SECRETS: nothing here logs more than
   a masked prefix.

3. **A sender** — a signed ES256 JWT (cached, refreshed well inside Apple's
   one-hour window) over HTTP/2 to `api.push.apple.com`. A device that
   registered with the wrong environment is retried once against the other
   host, and an APNs `BadDeviceToken`/`Unregistered` verdict evicts the
   device so a stale token cannot accumulate.

The wire surface lives in `gateway.server` (`/v1/devices`); this namespace
knows nothing about Ring.
raw docstring

com.blockether.vis.internal.gateway.server

Gateway HTTP/SSE server.

Clojure-native stack: reitit-ring routes -> Ring middleware -> the Ring Jetty adapter on JDK virtual threads (:virtual-threads? true). SSE is a Ring StreamableResponseBody whose virtual thread is the connection's SINGLE socket writer: replay rides first, then it drains a bounded per-connection event queue that state/fan-out! enqueues onto, emitting a heartbeat comment on idle to keep the pipe warm and detect dead clients.

This is internal plumbing, not a channel: it registers no channel descriptor and owns no renderer - it ships canonical IR and the client renders (§4.1). Any host process (the vis gateway start daemon, a TUI run, an embedded caller) can start it alongside whatever else it is doing via start!.

Gateway HTTP/SSE server.

Clojure-native stack: reitit-ring routes -> Ring middleware -> the
Ring Jetty adapter on JDK virtual threads (`:virtual-threads? true`).
SSE is a Ring `StreamableResponseBody` whose virtual thread is the
connection's SINGLE socket writer: replay rides first, then it drains a
bounded per-connection event queue that `state/fan-out!` enqueues onto,
emitting a heartbeat comment on idle to keep the pipe warm and detect
dead clients.

This is internal plumbing, not a channel: it registers no channel
descriptor and owns no renderer - it ships canonical IR and the
client renders (§4.1). Any host process (the `vis gateway start` daemon, a
TUI run, an embedded caller) can start it alongside whatever else it
is doing via `start!`.
raw docstring

com.blockether.vis.internal.gateway.state

Gateway session manager.

One process-global registry over the live session fleet: per-session ordered event log (monotonic :seq, ring-buffered), SSE subscriber fan-out, async turn submission with idempotency keys, cancellation, and turn/cost metrics.

The engine is reached ONLY through the same internal surfaces the TUI channel uses: loop/create!-send!-close! for the lifecycle, :hooks {:on-chunk ...} phased chunks for the live stream, ctx-loop/session-snapshot for the context. No engine state lives here - this namespace owns wire bookkeeping (events, turn records, subscribers), nothing else.

Gateway session manager.

One process-global registry over the live session fleet: per-session
ordered event log (monotonic `:seq`, ring-buffered), SSE subscriber
fan-out, async turn submission with idempotency keys, cancellation,
and turn/cost metrics.

The engine is reached ONLY through the same internal surfaces the
TUI channel uses: `loop/create!`-`send!`-`close!` for the
lifecycle, `:hooks {:on-chunk ...}` phased chunks for the live
stream, `ctx-loop/session-snapshot` for the context. No engine state
lives here - this namespace owns wire bookkeeping (events, turn
records, subscribers), nothing else.
raw docstring

com.blockether.vis.internal.gateway.wire

Wire encoding for the HTTP gateway.

One dumb, deterministic boundary: engine EDN -> JSON. Keyword/symbol keys become snake_case strings (namespace dropped), keyword VALUES keep their full ns/name (a badge role like :tool-color/search must survive the hop — dropping the namespace made the remote TUI see :search while the hop), non-JSON leaves fall back to str. The walker makes zero semantic or rendering decisions. Canonical message content is already a string-keyed vector of typed block maps before it reaches this boundary.

canonical is the SAME shape on the Clojure side: by definition what parse-jsonjson-str yields — snake_case STRING keys — serve it from a facade and in-process readers see exactly what a remote client sees.

Wire encoding for the HTTP gateway.

One dumb, deterministic boundary: engine EDN -> JSON. Keyword/symbol
keys become snake_case strings (namespace dropped), keyword VALUES keep
their full `ns/name` (a badge role like `:tool-color/search` must survive
the hop — dropping the namespace made the remote TUI see `:search` while
the hop), non-JSON leaves fall back to `str`. The walker makes zero semantic
or rendering decisions. Canonical message content is already a string-keyed
vector of typed block maps before it reaches this boundary.

`canonical` is the SAME shape on the Clojure side: by definition what
`parse-json` ∘ `json-str` yields — snake_case STRING keys — serve it from
a facade and in-process readers see exactly what a remote client sees.
raw docstring

com.blockether.vis.internal.git

Shared workspace inspection, backed by the native git binary.

UI surfaces and extensions depend on this namespace instead of embedding their own git calls. It reports small, renderable facts about the current repository (branch, dirty buckets, ahead/behind, porcelain entries) by shelling out to git status --porcelain=v2 and git rev-parse, parsed into stable Clojure maps. No JGit — the only git implementation is the one already on the user's PATH, so behaviour matches their shell exactly.

Workspace lifecycle mutations stay in com.blockether.vis.internal.workspace.

Shared workspace inspection, backed by the native `git` binary.

UI surfaces and extensions depend on this namespace instead of embedding
their own git calls. It reports small, renderable facts about the current
repository (branch, dirty buckets, ahead/behind, porcelain entries) by
shelling out to `git status --porcelain=v2` and `git rev-parse`, parsed
into stable Clojure maps. No JGit — the only git implementation is the one
already on the user's PATH, so behaviour matches their shell exactly.

Workspace lifecycle mutations stay in
`com.blockether.vis.internal.workspace`.
raw docstring

com.blockether.vis.internal.gitignore

Minimal, pure-Clojure .gitignore matcher — the JGit-free replacement for IgnoreNode. Parses ONE .gitignore file (the one at a walk root) into ordered rules and evaluates a /-separated relative path against them with git's semantics: last matching rule wins, ! negates, a trailing / restricts to directories, a leading/embedded / anchors to the root, and a slash-free pattern matches at any depth.

Minimal, pure-Clojure `.gitignore` matcher — the JGit-free replacement for
`IgnoreNode`. Parses ONE `.gitignore` file (the one at a walk root) into
ordered rules and evaluates a `/`-separated relative path against them with
git's semantics: last matching rule wins, `!` negates, a trailing `/`
restricts to directories, a leading/embedded `/` anchors to the root, and a
slash-free pattern matches at any depth.
raw docstring

com.blockether.vis.internal.header

Channel-agnostic header layout & content spec.

Every channel — the terminal TUI and future surfaces — renders the same conceptual header band:

[LEFT 30%]   [CENTER 40% workspace switcher]   [RIGHT 30%]

The decisions a channel cannot make on its own (slot ratios, workspace switcher sizing/visibility caps, default labels, copy id length, glyphs) live here as plain Clojure data. A channel imports this namespace, reads the values, and projects them onto its medium — TextGraphics cells for Lanterna, and so on.

No graphics. No I/O. No channel-specific deps. Pure data + tiny pure helpers, written as .cljc so a future ClojureScript client can require it directly.

Channel-agnostic header layout & content spec.

Every channel — the terminal TUI and future surfaces —
renders the same conceptual header band:

    [LEFT 30%]   [CENTER 40% workspace switcher]   [RIGHT 30%]

The decisions a channel cannot make on its own (slot ratios, workspace
switcher sizing/visibility caps, default labels, copy id length, glyphs)
live here as plain Clojure data. A channel imports this namespace,
reads the values, and projects them onto its medium — TextGraphics
cells for Lanterna, and so on.

No graphics. No I/O. No channel-specific deps. Pure data + tiny
pure helpers, written as `.cljc` so a future ClojureScript client can
require it directly.
raw docstring

com.blockether.vis.internal.import

In-house import-vars — re-export public vars from other namespaces under the current namespace, carrying their :doc/:arglists so the facade reads like the originals and doc/editor help work on a re-export. A tiny, dependency-free stand-in for potemkin's import-vars (the project keeps no separate lib for it).

Value semantics are identical to a plain (def alias src): the alias captures the source var's value at load time — it re-exports a fn/const, it does NOT track later redefinitions, and (like a def-alias always did) it is not a binding-rebindable handle for a dynamic source var.

In-house `import-vars` — re-export public vars from other namespaces under the
current namespace, carrying their :doc/:arglists so the facade reads like the
originals and `doc`/editor help work on a re-export. A tiny, dependency-free
stand-in for potemkin's import-vars (the project keeps no separate lib for it).

Value semantics are identical to a plain `(def alias src)`: the alias captures
the source var's value at load time — it re-exports a fn/const, it does NOT
track later redefinitions, and (like a def-alias always did) it is not a
`binding`-rebindable handle for a dynamic source var.
raw docstring

com.blockether.vis.internal.iteration

Canonical iteration-entry shape — the single source of truth shared by the LIVE progress tracker (internal/progress) and the RESUME projection (channel-tui/chat).

Background: the live-vs-resume split was the root cause of every TUI regression. The live tracker accumulated chunks into one map shape; the resume path rebuilt a different map shape from persisted rows. This ns pins ONE shape both paths populate.

Vocabulary

form envelope — one block = one form record (the engine :forms BLOB), carrying :code, :result, :error, and :stdout (what the block PRINTED — the single display surface; op cards / render-fns are gone).

Canonical iteration-entry

{:position n ;; 0-based display position of the iteration :scope "tN/iM" ;; BLOCK-level scope, never /fK :thinking string-or-nil ;; reasoning text for this iteration :code "<block source>" :forms [<form> ...] ;; form envelopes (engine :forms BLOB) :status :ok|:error|:running|:cancelled|:timeout :duration-ms long :error error-map-or-nil}

Canonical iteration-entry shape — the single source of truth shared by
the LIVE progress tracker (`internal/progress`) and the RESUME projection
(`channel-tui/chat`).

Background: the live-vs-resume split was the root cause of every TUI
regression. The live tracker accumulated chunks into one map shape; the
resume path rebuilt a *different* map shape from persisted rows. This ns
pins ONE shape both paths populate.

## Vocabulary

  form envelope — one block = one form record (the engine `:forms`
                  BLOB), carrying `:code`, `:result`, `:error`, and
                  `:stdout` (what the block PRINTED — the single display
                  surface; op cards / render-fns are gone).

## Canonical iteration-entry

  {:position    n              ;; 0-based display position of the iteration
   :scope       "tN/iM"        ;; BLOCK-level scope, never /fK
   :thinking    string-or-nil  ;; reasoning text for this iteration
   :code        "<block source>"
   :forms       [<form> ...]   ;; form envelopes (engine :forms BLOB)
   :status      :ok|:error|:running|:cancelled|:timeout
   :duration-ms long
   :error       error-map-or-nil}
raw docstring

com.blockether.vis.internal.jfr

Opt-in Java Flight Recorder profiling — ONE recording per PROCESS.

Turned on per-process by the VIS_JFR env var (set by bin/vis --jfr). The var is INHERITED by the detached gateway daemon that a client spawns, so both the client (TUI/web) process AND the gateway daemon each start their OWN recording into a role+pid-tagged file under ~/.vis/logs/:

vis-client-<pid>-<ts>.jfr ← the TUI / web / one-shot process vis-gateway-<pid>-<ts>.jfr ← the long-lived gateway daemon

That gives you two SEPARATE readings to compare when a client seems to be waiting on the gateway (jfr print --events jdk.ExecutionSample <file>).

Works on the JVM always; on the compiled native binary only when it was built with --enable-monitoring=jfr (see the app's native-image.properties). Never throws and never blocks startup — if JFR is unavailable it just no-ops.

Opt-in Java Flight Recorder profiling — ONE recording per PROCESS.

Turned on per-process by the `VIS_JFR` env var (set by `bin/vis --jfr`). The
var is INHERITED by the detached gateway daemon that a client spawns, so both
the client (TUI/web) process AND the gateway daemon each start their OWN
recording into a role+pid-tagged file under `~/.vis/logs/`:

  vis-client-<pid>-<ts>.jfr     ← the TUI / web / one-shot process
  vis-gateway-<pid>-<ts>.jfr    ← the long-lived gateway daemon

That gives you two SEPARATE readings to compare when a client seems to be
waiting on the gateway (`jfr print --events jdk.ExecutionSample <file>`).

Works on the JVM always; on the compiled native binary only when it was built
with `--enable-monitoring=jfr` (see the app's native-image.properties). Never
throws and never blocks startup — if JFR is unavailable it just no-ops.
raw docstring

com.blockether.vis.internal.limits-format

Channel-neutral {:dynamic {:limits [...]}} row formatters.

Hoisted from the TUI extension (channel_tui/limits_fmt.clj) so every channel — TUI footer, TUI provider cards — renders the SAME compact account-quota summary from a provider's normalized limits report. The TUI namespace now aliases these vars; other channels consume them through vis.core.

The interesting account-level rows (:zai-coding-plan-5h, :codex-7d, :premium_interactions, ...) live under [:dynamic :limits]; static :rpm/:tpm are svar catalog defaults, identical for every provider, useful only as fallback.

Channel-neutral `{:dynamic {:limits [...]}}` row formatters.

Hoisted from the TUI extension (`channel_tui/limits_fmt.clj`) so
every channel — TUI footer, TUI provider cards — renders the SAME
compact account-quota summary from a provider's normalized limits
report. The TUI namespace now aliases these vars; other channels
consume them through `vis.core`.

The interesting account-level rows (`:zai-coding-plan-5h`,
`:codex-7d`, `:premium_interactions`, ...) live under
`[:dynamic :limits]`; static `:rpm`/`:tpm` are svar catalog
defaults, identical for every provider, useful only as fallback.
raw docstring

com.blockether.vis.internal.main

vis CLI binary - :db Telemere handler, one-shot agent helper, built-in CLI commands, and the -main dispatcher entry point.

Everything in this file is binary-only. The library surface (iteration loop, turn engine, environment lifecycle, session cache) lives in com.blockether.vis.internal.loop; this namespace requires that one and wires it into the command tree the vis binary exposes.

Public entry point:

(-main & args) - invoked by the :vis alias / bin/vis. Configures logging, runs the unified extension discovery scan, redirects stderr to ~/.vis/vis.log for any TTY-owning channel, then dispatches to the resolved command's :cmd/run-fn.

Built-in commands registered here: vis providers - provider inspection, auth, and limits vis sessions - list persisted sessions vis extension list - list registered extensions vis tui - interactive terminal UI (alias for channels tui) vis channels <name> - auto-mounted via the channel registry

vis doctor is host-owned. Extensions plug diagnostics into it with :ext/doctor-fn; extension-owned CLI commands stay under vis extension.

vis CLI binary - :db Telemere handler, one-shot agent helper,
built-in CLI commands, and the `-main` dispatcher entry point.

Everything in this file is binary-only. The library surface
(iteration loop, turn engine, environment lifecycle, session
cache) lives in `com.blockether.vis.internal.loop`; this namespace requires
that one and wires it into the command tree the `vis` binary
exposes.

Public entry point:

  (-main & args)   - invoked by the `:vis` alias / `bin/vis`.
                     Configures logging, runs the unified extension
                     discovery scan, redirects stderr to ~/.vis/vis.log
                     for any TTY-owning channel, then dispatches to
                     the resolved command's `:cmd/run-fn`.

Built-in commands registered here:
  vis providers          - provider inspection, auth, and limits
  vis sessions      - list persisted sessions
  vis extension list     - list registered extensions
  vis tui                - interactive terminal UI (alias for `channels tui`)
  vis channels <name>    - auto-mounted via the channel registry

`vis doctor` is host-owned. Extensions plug diagnostics into it
with `:ext/doctor-fn`; extension-owned CLI commands stay under
`vis extension`.
raw docstring

com.blockether.vis.internal.manifest

Classpath auto-discovery of vis extension manifests.

ONE resource per jar: META-INF/vis-extension/vis.edn. Each file is an EDN map keyed by extension id symbol:

{git {:nses [com.acme.ext.git.core com.acme.channel.web.bot ...]}}

scan-extensions! is the primitive: it walks every URL, parses each map, requires every namespace listed under :nses exactly once across all URLs (so registrar side effects in those namespaces fire), and returns the merged manifest map {<id> {:nses [...]}}. Idempotent and memoized.

rediscover! is a test/REPL helper that drops the cache and re-scans.

Classpath auto-discovery of vis extension manifests.

ONE resource per jar: `META-INF/vis-extension/vis.edn`. Each file
is an EDN map keyed by extension id symbol:

  {git
   {:nses [com.acme.ext.git.core
           com.acme.channel.web.bot
           ...]}}

`scan-extensions!` is the primitive: it walks every URL, parses
each map, `require`s every namespace listed under `:nses` exactly
once across all URLs (so registrar side effects in those namespaces
fire), and returns the merged manifest map `{<id> {:nses [...]}}`.
Idempotent and memoized.

`rediscover!` is a test/REPL helper that drops the cache and
re-scans.
raw docstring

com.blockether.vis.internal.nativeimage

GraalVM native-image build-time Feature: initialize Clojure namespaces the right way so build-time class initialization doesn't blow up.

The problem: graal-build-time registers every Clojure-generated package for build-time class initialization. native-image then runs each <clinit> RAW on a parallel analysis worker thread, with no Clojure thread-binding frame. Any namespace whose body has a top-level (set! *warn-on-reflection* true) (most libraries — babashka.fs, next.jdbc, rewrite-clj, honeysql, nippy, …) throws

java.lang.IllegalStateException: Can't change/establish root binding of:
*warn-on-reflection* with set

because set! on a dynamic var requires a thread binding. Core namespaces (clojure.string, clojure.spec.alpha, …) survive only because clojure.core's bootstrap loads them through require, which DOES push that binding. Libraries reached directly by the analysis get no such courtesy.

The fix: in beforeAnalysis — which runs in the image-builder JVM, before the analysis can raw-init anything — require every app + extension namespace with the compiler vars bound. require initializes each class through Clojure's loader (binding active), so its set! succeeds; by the time the analysis marks the class build-time-initialized it is already initialized and is not re-run.

Wired via --features=com.blockether.vis.internal.nativeimage in main's resources/META-INF/native-image/com.blockether/vis/native-image.properties, alongside graal-build-time's feature. Build-time only — never loaded at runtime.

GraalVM native-image build-time Feature: initialize Clojure namespaces the
*right* way so build-time class initialization doesn't blow up.

The problem: `graal-build-time` registers every Clojure-generated package for
build-time class initialization. native-image then runs each `<clinit>` RAW on
a parallel analysis worker thread, with no Clojure thread-binding frame. Any
namespace whose body has a top-level `(set! *warn-on-reflection* true)` (most
libraries — babashka.fs, next.jdbc, rewrite-clj, honeysql, nippy, …) throws

    java.lang.IllegalStateException: Can't change/establish root binding of:
    *warn-on-reflection* with set

because `set!` on a dynamic var requires a thread binding. Core namespaces
(clojure.string, clojure.spec.alpha, …) survive only because clojure.core's
bootstrap loads them through `require`, which DOES push that binding. Libraries
reached directly by the analysis get no such courtesy.

The fix: in `beforeAnalysis` — which runs in the image-builder JVM, before the
analysis can raw-init anything — `require` every app + extension namespace with
the compiler vars bound. `require` initializes each class through Clojure's
loader (binding active), so its `set!` succeeds; by the time the analysis marks
the class build-time-initialized it is already initialized and is not re-run.

Wired via `--features=com.blockether.vis.internal.nativeimage` in main's
`resources/META-INF/native-image/com.blockether/vis/native-image.properties`,
alongside graal-build-time's feature. Build-time only — never loaded at runtime.
raw docstring

com.blockether.vis.internal.notifications

Cross-channel ephemeral notifications.

A single in-memory pub-sub the host runtime, every extension, and every channel can use to surface a transient signal - "copied to clipboard", "tests passed", "provider switched" - without embedding it in the answer body or polluting Telemere logs.

Surface (re-exported on com.blockether.vis.core):

(notify! text) (notify! text :level :info|:success|:warn|:error :ttl-ms <long>|nil) (notifications) ;; vec of currently-active entries (dismiss! id) ;; force-clear by id (clear-expired!) ;; prune; called on every read (watch! key (fn [vec] ...)) ;; cross-channel reactivity (unwatch! key)

Entry shape: {:id <uuid> :text <string> :level :info | :success | :warn | :error :created-at <inst> :until <epoch-ms> | nil ;; nil = sticky / manual dismiss

Levels are advisory metadata for channels: TUI uses them for colour, the CLI could prefix [notice] / [warn]. The host stores them but never interprets them.

Why a flat module instead of a generic event bus: notifications are a single, narrow concern. A 50-line atom + watcher map serves it without introducing a generic pub-sub abstraction nobody asked for. If we ever grow more event types, this becomes one consumer of a richer system.

Cross-channel ephemeral notifications.

A single in-memory pub-sub the host runtime, every extension, and
every channel can use to surface a transient signal - "copied to
clipboard", "tests passed", "provider switched" - without
embedding it in the answer body or polluting Telemere logs.

Surface (re-exported on `com.blockether.vis.core`):

  (notify! text)
  (notify! text :level :info|:success|:warn|:error
                :ttl-ms <long>|nil)
  (notifications)              ;; vec of currently-active entries
  (dismiss! id)                ;; force-clear by id
  (clear-expired!)             ;; prune; called on every read
  (watch! key (fn [vec] ...))    ;; cross-channel reactivity
  (unwatch! key)

Entry shape:
  {:id          <uuid>
   :text        <string>
   :level       :info | :success | :warn | :error
   :created-at  <inst>
   :until       <epoch-ms> | nil   ;; nil = sticky / manual dismiss

Levels are advisory metadata for channels: TUI uses them for
colour, the CLI could prefix `[notice]` /
`[warn]`. The host stores them but never interprets them.

Why a flat module instead of a generic event bus: notifications
are a single, narrow concern. A 50-line atom + watcher map serves
it without introducing a generic pub-sub abstraction nobody asked
for. If we ever grow more event types, this becomes one consumer
of a richer system.
raw docstring

com.blockether.vis.internal.oauth

Unified OAuth token-refresh facade, shared by every provider.

WHY THIS EXISTS — Providers whose token endpoint ROTATES the refresh_token on every exchange (Anthropic, OpenAI Codex) must never run two refresh exchanges at once: the second reuses an already-rotated refresh token and the server answers HTTP 400 invalid_grant. Under a 401 "storm" — the turn loop's per-iteration retry PLUS a usage/limits poll, all sharing one credential — that race killed whole turns (~10 refreshes/min: one lost the rotation race and the turn died). Providers that mint a short-lived token from a STABLE credential (GitHub Copilot) don't 400, but still benefit: concurrent 401s otherwise stampede the exchange endpoint with redundant calls.

THE MODEL — refresh is serialized PER CREDENTIAL STORE, never globally. Each make-file-refresher / refresher call mints its OWN lock, so a refresh to Anthropic and a refresh to Codex run fully in parallel; only two refreshes to the SAME store (e.g. two sessions both hitting Anthropic) serialize — which is the whole point. Once the lock is held, a caller REUSES a result another thread just produced (creds persisted within default-reuse-window-ms, or an already-valid cache) so a burst of N concurrent 401s collapses into ONE exchange.

USE — file-backed rotating stores (Anthropic, Codex): make-file-refresher. Cache-backed / bespoke stores (Copilot): refresher with custom reuse/refresh fns. Both return a 0-arg fn yielding the provider-token map, owning their own lock; drop them straight into :provider/get-token-fn / :provider/refresh-token-fn.

Unified OAuth token-refresh facade, shared by every provider.

WHY THIS EXISTS — Providers whose token endpoint ROTATES the
refresh_token on every exchange (Anthropic, OpenAI Codex) must never run
two refresh exchanges at once: the second reuses an already-rotated
refresh token and the server answers HTTP 400 `invalid_grant`. Under a
401 "storm" — the turn loop's per-iteration retry PLUS a usage/limits
poll, all sharing one credential — that race killed whole turns (~10 refreshes/min: one lost the
rotation race and the turn died). Providers that mint a short-lived token from a STABLE
credential (GitHub Copilot) don't 400, but still benefit: concurrent
401s otherwise stampede the exchange endpoint with redundant calls.

THE MODEL — refresh is serialized PER CREDENTIAL STORE, never globally.
Each `make-file-refresher` / `refresher` call mints its OWN lock, so a
refresh to Anthropic and a refresh to Codex run fully in parallel; only
two refreshes to the SAME store (e.g. two sessions both hitting
Anthropic) serialize — which is the whole point. Once the lock is held,
a caller REUSES a result another thread just produced (creds persisted
within `default-reuse-window-ms`, or an already-valid cache) so a burst
of N concurrent 401s collapses into ONE exchange.

USE — file-backed rotating stores (Anthropic, Codex): `make-file-refresher`.
Cache-backed / bespoke stores (Copilot): `refresher` with custom
reuse/refresh fns. Both return a 0-arg fn yielding the provider-token
map, owning their own lock; drop them straight into
`:provider/get-token-fn` / `:provider/refresh-token-fn`.
raw docstring

com.blockether.vis.internal.parse-diagnose

Cheap heuristics that turn opaque parse / eval errors into precise diagnostic hints. Engine wires the catalogue into the per-form trailer so the model sees actionable repair instructions instead of a stack trace.

Catalogue today:

diagnose-quote-balance Odd number of unescaped double quotes in the source. Pinpoints the 1-based line where the running count first becomes odd. parinferish does indent-mode paren balancing only; string-quote imbalance needs its own walker.

diagnose-bracket-balance Unbalanced (), [], {}. Walks a bracket stack skipping string/char literals (incl. triple-quoted, with escapes) and # comments; reports the FIRST wrong-type / extra / unclosed bracket + 1-based line/col. repair-bracket-balance offers a single-candidate auto-fix, but ONLY when one edit rebalances the WHOLE form. Python's mixed (), [], {} are NOT indentation-determined, so parinfer (the Clojure paren repairer) cannot be reused here.

unresolved-symbol-hint The Python eval raised a NameError for an undefined name X. Suggests the closest name(s) in the user's sandbox bindings (Levenshtein-style score), so the model sees 'did you mean ...?' instead of 'X is undefined'.

Cheap heuristics that turn opaque parse / eval errors into precise
diagnostic hints. Engine wires the catalogue into the per-form trailer so
the model sees actionable repair instructions instead of a stack trace.

Catalogue today:

  diagnose-quote-balance
    Odd number of unescaped double quotes in the source. Pinpoints the
    1-based line where the running count first becomes odd. parinferish
    does indent-mode paren balancing only; string-quote imbalance needs
    its own walker.

  diagnose-bracket-balance
    Unbalanced (), [], {}. Walks a bracket stack skipping string/char
    literals (incl. triple-quoted, with escapes) and `#` comments; reports
    the FIRST wrong-type / extra / unclosed bracket + 1-based line/col.
    `repair-bracket-balance` offers a single-candidate auto-fix, but ONLY
    when one edit rebalances the WHOLE form. Python's mixed (), [], {} are
    NOT indentation-determined, so parinfer (the Clojure paren repairer)
    cannot be reused here.

  unresolved-symbol-hint
    The Python eval raised a NameError for an undefined name X. Suggests
    the closest name(s) in the user's sandbox bindings (Levenshtein-style score),
    so the model sees 'did you mean ...?' instead of 'X is undefined'.
raw docstring

com.blockether.vis.internal.paths

Cross-platform path helpers. A LEAF namespace (no project deps) so any layer — core, extensions, tests — can normalize without a require cycle.

Cross-platform path helpers. A LEAF namespace (no project deps) so any
layer — core, extensions, tests — can normalize without a require cycle.
raw docstring

com.blockether.vis.internal.persistance

Persistence facade: backend registry, connection lifecycle, and every delegated store-*/db-* fn.

Extensions register backend adapters through :ext/persistance. The facade dispatches each delegated call by resolving the matching var on the chosen backend namespace ((ns-resolve ns-sym 'db-store-iteration!) etc.) and applying it to the original args. This keeps the facade dialect-agnostic - every migration runner / driver-specific oddity stays inside the backend adapter.

Frontends still call db-error->user-message here, but the actual translation is offered by backend adapters. Same for store-staleness checks used by the process-wide shared connection.

Persistence facade: backend registry, connection lifecycle, and every
delegated `store-*`/`db-*` fn.

Extensions register backend adapters through `:ext/persistance`. The
facade dispatches each delegated call by resolving the matching var on
the chosen backend namespace (`(ns-resolve ns-sym 'db-store-iteration!)`
etc.) and applying it to the original args. This keeps the facade
dialect-agnostic - every migration runner / driver-specific oddity stays
inside the backend adapter.

Frontends still call `db-error->user-message` here, but the actual
translation is offered by backend adapters. Same for store-staleness
checks used by the process-wide shared connection.
raw docstring

com.blockether.vis.internal.process-jail

OS-level process CONTAINMENT — the 'jail' — that wraps the shell executors' argv so an allowed child is physically confined to the session workspace roots and, when network is off, cannot open a socket. This is real containment, a real containment boundary — not a cooperative name/argv check, which can be walked around since argv[0] is bash and the real binary hides inside the -lc string; the jail constrains what the child can DO once it runs, regardless of what a script inside it tries (curl, python -c, /dev/tcp — all hit the same wall).

POLICY, NOT GUARDS. The jail is driven by a declarative policy compiled from vis.yml + the LIVE session roots, not by hand-written guard functions. The policy is a plain VALUE passed per spawn (never a process-global singleton, so many concurrent sessions in one gateway never stomp each other). Its shape:

{:roots-fn (fn [] [root-strings]) ; live session RW roots, re-read/spawn :net-enabled? <bool> ; whole shell-child network on/off :allow-read-write [<path> …] ; concise full read+write grant :allow-write [<path> …] ; legacy writable paths (also readable) :deny-write [<path> …] ; protect within writable (deny wins) :allow-read [<path> …] ; additional read-only paths :deny-read [<path> …] ; protect a read region (deny wins) :inbound-ports [<int> …]} ; extra local ports a child may ACCEPT on ; (bind is local-only; accept is port-gated)

The filesystem model mirrors Anthropic's sandbox-runtime:

  • WRITE is allow-only: denied everywhere except the session roots + tmp + :allow-read-write + :allow-write; :deny-write wins.
  • READ is default-deny here (workspace-focused, stronger than srt's read-everywhere default): system code/config + RW paths + :allow-read are readable; :deny-read wins.

wrap-argv compiles the policy, per spawn, into the OS enforcement primitive:

  • macOS : a Seatbelt profile handed to the system sandbox-exec -p — ships with the OS, ZERO install. IMPLEMENTED + verified.
  • Linux : bubblewrap (bwrap) mount + network namespaces. IMPLEMENTED (fs confinement + net-off wall); filtered egress via the proxy still needs seccomp, so a proxy-restricted net is denied ENTIRELY (safe) rather than left open. supported? is true only when bwrap is installed; otherwise unenforceable-reason explains the gap so a requested jail fails LOUD instead of silently passing the child.
  • other : unsupported — callers keep the cooperative gate as the floor.

Two locks learned from the kernel, baked in here:

  1. sandbox-exec matches RESOLVED real paths, so every root is realpath'd before templating (/tmp -> /private/tmp, else the rule never matches).
  2. a default-deny profile MUST (import "system.sb") or dyld/sysctl startup reads are denied and every binary aborts before main.
OS-level process CONTAINMENT — the 'jail' — that wraps the shell executors'
argv so an allowed child is physically confined to the session workspace roots
and, when network is off, cannot open a socket. This is real containment, a
real containment boundary — not a cooperative name/argv check, which can be
walked around since argv[0] is `bash` and the real binary hides inside the
`-lc` string;
the jail constrains what the child can DO once it runs, regardless of what a
script inside it tries (curl, python -c, /dev/tcp — all hit the same wall).

POLICY, NOT GUARDS. The jail is driven by a declarative *policy* compiled from
vis.yml + the LIVE session roots, not by hand-written guard functions. The
policy is a plain VALUE passed per spawn (never a process-global singleton, so
many concurrent sessions in one gateway never stomp each other). Its shape:

  {:roots-fn     (fn [] [root-strings])  ; live session RW roots, re-read/spawn
   :net-enabled? <bool>                  ; whole shell-child network on/off
   :allow-read-write [<path> …]           ; concise full read+write grant
   :allow-write      [<path> …]           ; legacy writable paths (also readable)
   :deny-write       [<path> …]           ; protect within writable (deny wins)
   :allow-read       [<path> …]           ; additional read-only paths
   :deny-read        [<path> …]           ; protect a read region (deny wins)
   :inbound-ports    [<int> …]}           ; extra local ports a child may ACCEPT on
                                          ; (bind is local-only; accept is port-gated)

The filesystem model mirrors Anthropic's sandbox-runtime:
  - WRITE is allow-only: denied everywhere except the session roots + tmp +
    `:allow-read-write` + `:allow-write`; `:deny-write` wins.
  - READ is default-deny here (workspace-focused, stronger than srt's
    read-everywhere default): system code/config + RW paths + `:allow-read`
    are readable; `:deny-read` wins.

`wrap-argv` compiles the policy, per spawn, into the OS enforcement primitive:

  - macOS  : a Seatbelt profile handed to the system `sandbox-exec -p` — ships
             with the OS, ZERO install. IMPLEMENTED + verified.
  - Linux  : bubblewrap (`bwrap`) mount + network namespaces. IMPLEMENTED (fs
             confinement + net-off wall); filtered egress via the proxy still
             needs seccomp, so a proxy-restricted net is denied ENTIRELY (safe)
             rather than left open. `supported?` is true only when `bwrap` is
             installed; otherwise `unenforceable-reason` explains the gap so a
             requested jail fails LOUD instead of silently passing the child.
  - other  : unsupported — callers keep the cooperative gate as the floor.

Two locks learned from the kernel, baked in here:
  1. sandbox-exec matches RESOLVED real paths, so every root is realpath'd
     before templating (`/tmp` -> `/private/tmp`, else the rule never matches).
  2. a default-deny profile MUST `(import "system.sb")` or dyld/sysctl startup
     reads are denied and every binary aborts before `main`.
raw docstring

com.blockether.vis.internal.progress

Streaming progress tracker - leaf module.

Channels (TUI, CLI agent) consume the iteration loop's PHASED chunks via this tracker. Every chunk carries a :phase keyword that tells the tracker what to do with it; the tracker accumulates the chunks into a per-iteration timeline that the channel re-renders incrementally.

Phases (every chunk has exactly one):

:reasoning LLM is streaming reasoning text. Updates the iteration entry's :thinking field.

:tool-preview LLM is streaming a native call. Carries tool identity and cumulative code separately from reasoning/content; the first real form replaces this ephemeral slot.

:form-start One block is about to evaluate. Carries :position and :code. The tracker writes the code immediately so channels can show the currently-running block before the result lands.

:form-result One block finished evaluating. Carries :position, :code, :result/:error, and :envelope timestamps. The tracker writes the completed form record into :forms at the chunk's display index. Chunks tagged :silent? (or returning :vis/silent) keep their :silent? flag so channels can toggle visibility.

:iteration-final Iteration is complete. Carries :final (nil when the turn isn't done yet) and :done? (true when this iteration produced the turn-terminal answer). The block chunk has already streamed; this is the trim "iteration done" marker.

:iteration-error Iteration aborted before forms could run (e.g. LLM call failed). Carries :thinking and :error.

:provider-retry-reset Provider stream failed before code eval and Vis is retrying the provider call. Clears stale live reasoning/content for this attempt and keeps a retry recap in :provider-fallbacks.

Public API:

(make-progress-tracker) - fresh tracker, no callback (make-progress-tracker {:on-update}) - invokes (on-update timeline chunk) on every chunk

Returns {:on-chunk fn :get-timeline fn}. Pass the :on-chunk fn under :hooks {:on-chunk ...} of sessions/send!. Each timeline entry has the shape:

{:iteration N :thinking str-or-nil :forms [{:code str :comment str-or-nil :render-segments [{:kind ...} ...] ;; source classification :result-render str-or-IR-or-nil ;; pre-rendered tool result :result-kind :tool|:value|:error :result-detail map-or-nil ;; tool metadata :error map-or-nil :duration-ms int :success? bool :silent? bool :started-at-ms int-or-nil} ...] :provider-fallbacks [map ...] ;; routed provider fallback notices :activity nil-or-keyword ;; live coarse phase (:provider-call/:response-parse) :elided-form-idxs #{int ...} ;; original loop indices hidden from :forms :error nil-or-iteration-error :final nil-or-{:answer :iteration-count :status} :done? bool}

The pre-existing :events interleaving log was removed: it lived only in memory (never persisted), and resumed bubbles re-render from this single flat layout. One layout path is enough.

Streaming progress tracker - leaf module.

Channels (TUI, CLI agent) consume the iteration loop's
PHASED chunks via this tracker. Every chunk carries a `:phase`
keyword that tells the tracker what to do with it; the tracker
accumulates the chunks into a per-iteration timeline that the
channel re-renders incrementally.

Phases (every chunk has exactly one):

  :reasoning        LLM is streaming reasoning text. Updates the
                    iteration entry's `:thinking` field.

  :tool-preview     LLM is streaming a native call. Carries tool identity
                    and cumulative code separately from reasoning/content;
                    the first real form replaces this ephemeral slot.

  :form-start       One block is about to evaluate. Carries
                    `:position` and `:code`. The tracker writes the
                    code immediately so channels can show the
                    currently-running block before the result lands.

  :form-result      One block finished evaluating. Carries
                    `:position`, `:code`, `:result`/`:error`,
                    and `:envelope` timestamps. The tracker writes
                    the completed form record into `:forms` at the
                    chunk's display index. Chunks tagged `:silent?`
                    (or returning `:vis/silent`) keep their `:silent?`
                    flag so channels can toggle visibility.

  :iteration-final  Iteration is complete. Carries `:final` (nil
                    when the turn isn't done yet) and `:done?`
                    (true when this iteration produced the
                    turn-terminal answer). The block chunk has
                    already streamed; this is the trim
                    "iteration done" marker.

  :iteration-error  Iteration aborted before forms could run
                    (e.g. LLM call failed). Carries `:thinking`
                    and `:error`.

  :provider-retry-reset
                    Provider stream failed before code eval and Vis is
                    retrying the provider call. Clears stale live
                    reasoning/content for this attempt and keeps a retry
                    recap in `:provider-fallbacks`.

Public API:

  `(make-progress-tracker)`              - fresh tracker, no callback
  `(make-progress-tracker {:on-update})` - invokes `(on-update timeline chunk)`
                                           on every chunk

Returns `{:on-chunk fn :get-timeline fn}`. Pass the `:on-chunk` fn
under `:hooks {:on-chunk ...}` of `sessions/send!`. Each timeline
entry has the shape:

  {:iteration N
   :thinking  str-or-nil
   :forms     [{:code            str
                :comment         str-or-nil
                :render-segments [{:kind ...} ...] ;; source classification
                :result-render   str-or-IR-or-nil  ;; pre-rendered tool result
                :result-kind     :tool|:value|:error
                :result-detail   map-or-nil        ;; tool metadata
                :error           map-or-nil
                :duration-ms     int
                :success?        bool
                :silent?         bool
                :started-at-ms   int-or-nil} ...]
   :provider-fallbacks [map ...]   ;; routed provider fallback notices
   :activity           nil-or-keyword ;; live coarse phase (:provider-call/:response-parse)
   :elided-form-idxs   #{int ...}  ;; original loop indices hidden from :forms
   :error              nil-or-iteration-error
   :final              nil-or-{:answer :iteration-count :status}
   :done?              bool}

The pre-existing `:events` interleaving log was removed: it lived
only in memory (never persisted), and resumed bubbles re-render
from this single flat layout. One layout path is enough.
raw docstring

com.blockether.vis.internal.prompt

Prompt assembly.

Provider messages are explicit blocks in send order: core system rules, project instructions (AGENTS.md / CLAUDE.md when present), extension fragments, current user message. Per-iteration user-role context is the engine snapshot rendered as a Python dict (session) by the loop.

Prompt assembly.

Provider messages are explicit blocks in send order: core system rules,
project instructions (AGENTS.md / CLAUDE.md when present), extension
fragments, current user message. Per-iteration user-role context is the
engine snapshot rendered as a Python dict (`session`) by the loop.
raw docstring

com.blockether.vis.internal.prompt-templates

File-based prompt templates — pi-style slash-expandable markdown prompts.

A template is a *.md file whose body becomes the user message when the user types /<name> [args…]. Discovery, project wins over global:

  1. <workspace>/.vis/prompts/*.md (project)
  2. ~/.vis/prompts/*.md (user-global)

Frontmatter is the same minimal --- fenced key: value block the harness discovery reads: name (defaults to the filename stem) and description. The body is the template.

Argument handling matches the common harness convention: when the body contains $ARGUMENTS every occurrence is substituted with the raw argument string (empty when none given); otherwise non-blank args are appended after the body on their own paragraph.

Extensions can contribute DYNAMIC templates through register-provider! — e.g. the harness extension exposes every discovered skill as /<name>. File templates win on a name collision; among providers, registration order wins.

Dispatch: the engine consults expand ONLY for slash texts no registered extension slash claimed (slash/dispatch returned :reason :unknown), so real slash commands always win — same precedence pi uses.

File-based prompt templates — pi-style slash-expandable markdown prompts.

A template is a `*.md` file whose body becomes the user message when
the user types `/<name> [args…]`. Discovery, project wins over global:

  1. `<workspace>/.vis/prompts/*.md`   (project)
  2. `~/.vis/prompts/*.md`             (user-global)

Frontmatter is the same minimal `---` fenced `key: value` block the
harness discovery reads: `name` (defaults to the filename stem) and
`description`. The body is the template.

Argument handling matches the common harness convention: when the
body contains `$ARGUMENTS` every occurrence is substituted with the
raw argument string (empty when none given); otherwise non-blank
args are appended after the body on their own paragraph.

Extensions can contribute DYNAMIC templates through
`register-provider!` — e.g. the harness extension exposes every
discovered skill as `/<name>`. File templates win on a name
collision; among providers, registration order wins.

Dispatch: the engine consults `expand` ONLY for slash texts no
registered extension slash claimed (`slash/dispatch` returned
`:reason :unknown`), so real slash commands always win — same
precedence pi uses.
raw docstring

com.blockether.vis.internal.provider-auth

Headless, resumable OAuth for registered providers — the daemon-side engine behind POST /v1/providers/:id/auth/{start,complete,poll}.

WHY THIS EXISTS. :provider/auth-fn is interactive: it prints prompts and blocks on read-line or a Lanterna dialog. That works only when the client IS the daemon's own terminal, which is exactly why the TUI reaches PAST the gateway and calls provider-anthropic/login! in-process. A phone, a browser tab, or any remote client cannot do that.

This namespace splits the same flows into steps that survive a request boundary, keyed by a short-lived server-side flow id:

start -> mints a flow, returns what the USER must see (URL, user code) complete -> PKCE: exchange the pasted redirect URL (Anthropic, Codex) poll -> device: read the background await's verdict (GitHub Copilot)

SECURITY BOUNDARY. The provider's :flow value (PKCE verifier, CSRF state, device code) stays in this atom and is NEVER emitted. Credentials are written by the provider extension into the daemon's own auth file, exactly as the interactive path does — no token, verifier, or refresh token is ever serialized to a client. public-view is an allowlist, not a redaction pass.

Headless, resumable OAuth for registered providers — the daemon-side engine
behind `POST /v1/providers/:id/auth/{start,complete,poll}`.

WHY THIS EXISTS. `:provider/auth-fn` is interactive: it prints prompts and
blocks on `read-line` or a Lanterna dialog. That works only when the client
IS the daemon's own terminal, which is exactly why the TUI reaches PAST the
gateway and calls `provider-anthropic/login!` in-process. A phone, a browser
tab, or any remote client cannot do that.

This namespace splits the same flows into steps that survive a request
boundary, keyed by a short-lived server-side flow id:

  start    -> mints a flow, returns what the USER must see (URL, user code)
  complete -> PKCE: exchange the pasted redirect URL           (Anthropic, Codex)
  poll     -> device: read the background await's verdict      (GitHub Copilot)

SECURITY BOUNDARY. The provider's `:flow` value (PKCE verifier, CSRF state,
device code) stays in this atom and is NEVER emitted. Credentials are
written by the provider extension into the daemon's own auth file, exactly
as the interactive path does — no token, verifier, or refresh token is ever
serialized to a client. `public-view` is an allowlist, not a redaction pass.
raw docstring

com.blockether.vis.internal.provider-error

Single source of truth for provider-error presentation.

Typed provider-error content and per-iteration trace rows derive their wording and facts from this namespace, so a failure reads identically everywhere.

err is the error map carried on a trace entry / ex-info: {:message .. :data {:status .. :body .. :request-id ..} ..}. Every helper tolerates the bare ex-info shape too (via ex-message).

Single source of truth for provider-error presentation.

Typed provider-error content and per-iteration trace rows derive their wording
and facts from this namespace, so a failure reads identically everywhere.

`err` is the error map carried on a trace entry / ex-info:
`{:message .. :data {:status .. :body .. :request-id ..} ..}`. Every
helper tolerates the bare ex-info shape too (via `ex-message`).
raw docstring

com.blockether.vis.internal.provider-limits

Normalized provider limits surface.

Providers may optionally expose :provider/limits-fn in the global registry. The function returns provider-specific limit/quota data; this namespace wraps it in one validated envelope and augments it with static provider metadata from svar's catalog (currently RPM / TPM).

Goals:

  • one host-level shape for all providers,
  • explicit support for providers that only know static limits,
  • spec validation of every returned report,
  • graceful error envelopes instead of exploding the caller when a provider-specific implementation is absent or malformed.
Normalized provider limits surface.

Providers may optionally expose `:provider/limits-fn` in the global
registry. The function returns provider-specific limit/quota data;
this namespace wraps it in one validated envelope and augments it
with static provider metadata from svar's catalog (currently RPM /
TPM).

Goals:
- one host-level shape for all providers,
- explicit support for providers that only know static limits,
- spec validation of every returned report,
- graceful error envelopes instead of exploding the caller when a
  provider-specific implementation is absent or malformed.
raw docstring

com.blockether.vis.internal.providers

Channel-neutral provider management service.

Everything a channel needs to render and mutate the provider fleet — status probing, account limits, live model catalogs, presets, and config persistence — WITHOUT any UI. Hoisted from the TUI extension (channel_tui/provider.clj) so any future surface manages the SAME fleet through the SAME primitives; the channels keep only their interaction layer (lanterna dialogs, ...).

Auth is classified, not implemented, here: auth-kind tells a channel whether a provider wants an API key, an interactive OAuth flow (owned by the provider extension + channel), or nothing (local). The registry's :provider/*-fn contract stays the single integration point for provider extensions, so a provider extension automatically works in every channel.

Channel-neutral provider management service.

Everything a channel needs to render and mutate the provider fleet
— status probing, account limits, live model catalogs, presets, and
config persistence — WITHOUT any UI. Hoisted from the TUI extension
(`channel_tui/provider.clj`) so any future surface manages the SAME
fleet through the SAME primitives; the channels keep only their
interaction layer (lanterna dialogs, ...).

Auth is classified, not implemented, here: `auth-kind` tells a
channel whether a provider wants an API key, an interactive OAuth
flow (owned by the provider extension + channel), or nothing
(local). The registry's `:provider/*-fn` contract stays the single
integration point for provider extensions, so a provider extension
automatically works in every channel.
raw docstring

com.blockether.vis.internal.pyfmt

Beautify model-emitted Python with ruff (com.blockether/ruff — in-process via the JDK FFM API, black-compatible) before it is shown. Used by the gateway's code renderer so the trace shows tidy, consistently-wrapped Python instead of the model's raw one-liners.

CACHED: ruff output is deterministic for a given input, and the same code block is rendered many times (pinned trace + live SSE re-emits + reconnect replay), so an LRU memo means each distinct block formats exactly once.

SAFE: ruff/format-or returns the source verbatim if ruff is unavailable (e.g. the native lib isn't bundled in a particular build) or the code doesn't parse — the original is never lost.

Beautify model-emitted Python with ruff (com.blockether/ruff — in-process via
the JDK FFM API, black-compatible) before it is shown. Used by the gateway's
code renderer so the trace shows tidy, consistently-wrapped Python instead of
the model's raw one-liners.

CACHED: ruff output is deterministic for a given input, and the same code
block is rendered many times (pinned trace + live SSE re-emits + reconnect
replay), so an LRU memo means each distinct block formats exactly once.

SAFE: `ruff/format-or` returns the source verbatim if ruff is unavailable
(e.g. the native lib isn't bundled in a particular build) or the code doesn't
parse — the original is never lost.
raw docstring

com.blockether.vis.internal.python-extensions

Project-local Python extensions — trusted-context plug-ins.

Vis extensions are normally Clojure libraries baked into the binary at build time. This namespace adds a second, fully dynamic authoring path: drop a *.py file into

~/.vis/extensions/ (global — every project) <project>/.vis/extensions/ (project-local — this project only)

and it loads at startup (and on /reload) in BOTH the JVM and the GraalVM native-image build — Python redefinition is pure Truffle dynamism, no runtime class definition involved.

Each file is evaluated in its own TRUSTED GraalPy context. This is NOT the model's sandbox: the model's context is untrusted, per-session and deny-by-default; extension contexts are user-trusted (same trust level as a Clojure extension on the classpath), process-wide, and get real filesystem / network / environment access. The two share nothing — the model can call an extension TOOL (through the host wrapper, envelope- checked like any tool) but can never evaluate code in the extension's context. Host capabilities are reachable ONLY through the bound vis API (no arbitrary Java interop: allowAllAccess stays false and no host classes are exposed).

Every context is built on env-python/shared-engine — the ONE process-wide Engine that makes context creation safe even while a sandbox eval is running (see the deadlock notes there). Calls into an extension (tool, activation, prompt, slash, op hook) are serialized with locking on its context, the same proven pattern as the printer context.

The file's top-level vis.extension(...) call registers through the ordinary register-extension! — from the registry's perspective a Python extension is indistinguishable from a Clojure one (activation, prompt assembly, slash dispatch, vis extension list all just work). A file that fails to load becomes a load-failure warning (surfaced via vis doctor), never a crash.

Project-local Python extensions — trusted-context plug-ins.

Vis extensions are normally Clojure libraries baked into the binary at
build time. This namespace adds a second, fully dynamic authoring path:
drop a `*.py` file into

  ~/.vis/extensions/           (global — every project)
  <project>/.vis/extensions/   (project-local — this project only)

and it loads at startup (and on `/reload`) in BOTH the JVM and the
GraalVM native-image build — Python redefinition is pure Truffle
dynamism, no runtime class definition involved.

Each file is evaluated in its own TRUSTED GraalPy context. This is NOT
the model's sandbox: the model's context is untrusted, per-session and
deny-by-default; extension contexts are user-trusted (same trust level
as a Clojure extension on the classpath), process-wide, and get real
filesystem / network / environment access. The two share nothing — the
model can call an extension TOOL (through the host wrapper, envelope-
checked like any tool) but can never evaluate code in the extension's
context. Host capabilities are reachable ONLY through the bound `vis`
API (no arbitrary Java interop: `allowAllAccess` stays false and no
host classes are exposed).

Every context is built on `env-python/shared-engine` — the ONE
process-wide Engine that makes context creation safe even while a
sandbox eval is running (see the deadlock notes there). Calls into an
extension (tool, activation, prompt, slash, op hook) are serialized
with `locking` on its context, the same proven pattern as the printer
context.

The file's top-level `vis.extension(...)` call registers through the
ordinary `register-extension!` — from the registry's perspective a
Python extension is indistinguishable from a Clojure one (activation,
prompt assembly, slash dispatch, `vis extension list` all just work).
A file that fails to load becomes a load-failure warning (surfaced via
`vis doctor`), never a crash.
raw docstring

com.blockether.vis.internal.python-test-runner

Runs an extension author's Python tests (test_*.py / *_test.py) through the built-in pytest-compat shim, each in its own TRUSTED GraalPy context (same trust level as the extension it covers). Tests import the extension's own package through the SAME sys.path sugar the loader gives extension.py, so an author ships real Python tests next to the code and runs them with the project's own tooling. Pure Python end to end — the shim is stdlib-only, no host bridge, no pip.

Split out of python-extensions (which owns loading/registration) so the runner is a single, testable responsibility. It depends on that namespace's trusted-context builder; the reverse wiring (/test slash + vis extension test CLI) is resolved lazily there to avoid a require cycle.

The source of truth for the outcome is the shim's PER-TEST record list (nodeid, outcome, message). Counts and pass/fail are DERIVED from those records on the host side — never a separate tally that could drift, and never scraped from stdout.

Runs an extension author's Python tests (`test_*.py` / `*_test.py`) through
the built-in `pytest`-compat shim, each in its own TRUSTED GraalPy context
(same trust level as the extension it covers). Tests import the extension's
own package through the SAME `sys.path` sugar the loader gives `extension.py`,
so an author ships real Python tests next to the code and runs them with the
project's own tooling. Pure Python end to end — the shim is stdlib-only, no
host bridge, no pip.

Split out of `python-extensions` (which owns loading/registration) so the
runner is a single, testable responsibility. It depends on that namespace's
trusted-context builder; the reverse wiring (`/test` slash + `vis extension test`
CLI) is resolved lazily there to avoid a require cycle.

The source of truth for the outcome is the shim's PER-TEST record list
(nodeid, outcome, message). Counts and pass/fail are DERIVED from those
records on the host side — never a separate tally that could drift, and
never scraped from stdout.
raw docstring

com.blockether.vis.internal.registry

Three global registries in one place: channels, providers, commands.

Each is a small, self-contained piece - keyword id, a spec for the descriptor map, a process-level atom holding the registered entries, and a handful of fns for register / deregister / lookup. Putting them together means one file owns every "this is the canonical shape of an extension contribution" decision and one file owns every place an extension's contribution lands at runtime.

Channel registry (:channel/id keyword): channel build + validate a descriptor register-channel! register, idempotent on :channel/id deregister-channel! remove by id registered-channels all entries, vec channel-by-id lookup by id by-cmd lookup by :channel/cmd

Provider registry (:provider/id keyword): provider build + validate a descriptor register-provider! register, idempotent on :provider/id deregister-provider! remove by id registered-providers all entries, vec provider-by-id lookup by id

Command registry ([:cmd/parent :cmd/name] tuple key): command build + validate a descriptor resolve-subcommands static vec or dynamic 0-arg fn -> vec register-cmd! register, idempotent on [parent name] deregister-cmd! remove by [parent name] registered-commands all entries, vec (registration order) registered-under filter by parent path

Channel mounting: channel-subcommands compose vis channels subcommand vec from the channel registry + any commands registered with :cmd/parent ["channels"]. Loading this ns also registers the vis channels parent itself.

Specs for keyword fields (:channel/id, :provider/id, :cmd/name, ..., plus the descriptor specs ::channel, ::provider, ::command, ::arg) live here too - the spec IS the registry's contract.

Parsing / help rendering / dispatch utilities live in com.blockether.vis.internal.commandline. Classpath manifest scanning lives in com.blockether.vis.internal.manifest; the extension layer that wraps it (and re-exports discover-extensions!) lives in com.blockether.vis.internal.extension.

Three global registries in one place: channels, providers, commands.

Each is a small, self-contained piece - keyword id, a spec for the
descriptor map, a process-level atom holding the registered entries,
and a handful of fns for register / deregister / lookup. Putting
them together means one file owns every "this is the canonical
shape of an extension contribution" decision and one file owns
every place an extension's contribution lands at runtime.

Channel registry (`:channel/id` keyword):
  channel                  build + validate a descriptor
  register-channel!        register, idempotent on :channel/id
  deregister-channel!      remove by id
  registered-channels      all entries, vec
  channel-by-id            lookup by id
  by-cmd                   lookup by :channel/cmd

Provider registry (`:provider/id` keyword):
  provider                 build + validate a descriptor
  register-provider!       register, idempotent on :provider/id
  deregister-provider!     remove by id
  registered-providers     all entries, vec
  provider-by-id           lookup by id

Command registry (`[:cmd/parent :cmd/name]` tuple key):
  command                  build + validate a descriptor
  resolve-subcommands      static vec or dynamic 0-arg fn -> vec
  register-cmd!            register, idempotent on [parent name]
  deregister-cmd!          remove by [parent name]
  registered-commands      all entries, vec (registration order)
  registered-under         filter by parent path

Channel mounting:
  channel-subcommands      compose `vis channels` subcommand vec
                           from the channel registry + any commands
                           registered with `:cmd/parent ["channels"]`.
                           Loading this ns also registers the
                           `vis channels` parent itself.

Specs for keyword fields (`:channel/id`, `:provider/id`, `:cmd/name`,
..., plus the descriptor specs `::channel`, `::provider`, `::command`,
`::arg`) live here too - the spec IS the registry's contract.

Parsing / help rendering / dispatch utilities live in
`com.blockether.vis.internal.commandline`. Classpath manifest
scanning lives in `com.blockether.vis.internal.manifest`; the
extension layer that wraps it (and re-exports `discover-extensions!`)
lives in `com.blockether.vis.internal.extension`.
raw docstring

com.blockether.vis.internal.render

Transient Markdown parsing and renderer projections.

Canonical answers are role-labelled, string-keyed content blocks from com.blockether.vis.internal.content. Parsed Markdown trees are created only inside renderers and are never transported or persisted.

markdown->ast parses prose for renderer-local layout. render, extract-code, extract-text, and session->markdown are disposable projections; none of their intermediate trees are canonical message data.

Transient Markdown parsing and renderer projections.

Canonical answers are role-labelled, string-keyed content blocks from
`com.blockether.vis.internal.content`. Parsed Markdown trees are created only
inside renderers and are never transported or persisted.

`markdown->ast` parses prose for renderer-local layout. `render`,
`extract-code`, `extract-text`, and `session->markdown` are disposable
projections; none of their intermediate trees are canonical message data.
raw docstring

com.blockether.vis.internal.resources

Canonical registry of VIS-MANAGED STATEFUL RESOURCES — the one interface every long-lived thing vis spawns (an nREPL, a daemon, a background shell, a file watch, a capture) registers under, so a single definition drives THREE spouts:

  1. the agent — :session/resources in ctx + resource_stop/resource_restart
  2. the footer/TUI — a live count + a dialog that stops/restarts by id
  3. the engine — teardown on shutdown

SESSION-SCOPED. The registry is partitioned by session-id: a resource one session registers is INVISIBLE to every other session, and ids only need to be unique WITHIN a session (the (session, id) pair is the global key). Every public verb takes the owning session as its first arg; the per-session sandbox tools + ctx are closed over that id, so the agent in session A can neither see nor stop session B's resources.

B-DISPATCH model. A resource is split into:

  • DATA (serializable, STRING-KEYED): id, kind, label, status, detail, pid, owner, session, can_stop, can_restart, created_at. This is what the footer renders and what persists to ~/.vis/resources.edn so a resource survives a vis restart (display + pid re-attach).
  • LIFECYCLE THUNKS (live, in-memory only): :stop-fn :restart-fn :alive-fn :logs-fn :health-fn. Never serialized. Across a restart the OWNER re-registers them (e.g. the Clojure pack re-attaches its nREPLs by pid on init) — exactly the pattern repl-manager already uses, lifted here so EVERY kind gets it. :health-fn answers "alive, but is it WORKING?" (:up :starting :failed :down …) and is probed — parallel, hard timeout — on every list-resources, flipping the stored status to reality.

:kind is OPEN-ENDED — a bare keyword, no closed enum. The registry never switches on it; only owners do.

Model ctx stays PURE DATA and groups the flat registry through model-view: REPLs live at session["resources"]["repls"][language][dir]; dir is their model-facing identity. It advertises can_stop/can_restart but never carries a callable. Killing goes through stop!/restart! (by session + id) — the single path the agent tool AND the footer both call. id IS the binding.

Canonical registry of VIS-MANAGED STATEFUL RESOURCES — the one interface every
long-lived thing vis spawns (an nREPL, a daemon, a background shell, a file
watch, a capture) registers under, so a single definition drives THREE spouts:

  1. the agent     — `:session/resources` in ctx + `resource_stop`/`resource_restart`
  2. the footer/TUI — a live count + a dialog that stops/restarts by id
  3. the engine     — teardown on shutdown

SESSION-SCOPED. The registry is partitioned by `session-id`: a resource one
session registers is INVISIBLE to every other session, and `id`s only need to
be unique WITHIN a session (the `(session, id)` pair is the global key). Every
public verb takes the owning `session` as its first arg; the per-session
sandbox tools + ctx are closed over that id, so the agent in session A can
neither see nor stop session B's resources.

B-DISPATCH model. A resource is split into:

  - DATA (serializable, STRING-KEYED): id, kind, label, status, detail,
    pid, owner, session, can_stop, can_restart, created_at. This is
    what the footer renders and what persists to `~/.vis/resources.edn` so a
    resource survives a vis restart (display + pid re-attach).
  - LIFECYCLE THUNKS (live, in-memory only): `:stop-fn :restart-fn :alive-fn
    :logs-fn :health-fn`. Never serialized. Across a restart the OWNER
    re-registers them (e.g. the Clojure pack re-attaches its nREPLs by pid on
    init) — exactly the pattern `repl-manager` already uses, lifted here so
    EVERY kind gets it. `:health-fn` answers "alive, but is it WORKING?"
    (:up :starting :failed :down …) and is probed — parallel, hard timeout —
    on every `list-resources`, flipping the stored `status` to reality.

`:kind` is OPEN-ENDED — a bare keyword, no closed enum. The registry never
switches on it; only owners do.

Model ctx stays PURE DATA and groups the flat registry through `model-view`:
REPLs live at `session["resources"]["repls"][language][dir]`; `dir` is
their model-facing identity. It advertises `can_stop`/`can_restart` but never
carries a callable. Killing goes through `stop!`/`restart!` (by session + id) — the
single path the agent tool AND the footer both call. `id` IS the binding.
raw docstring

com.blockether.vis.internal.runtime-settings

Per-eval / per-call runtime knobs for the loop: Python-sandbox eval timeouts (with clamping and a shell-timeout-aware widener), the svar/ask-code! stream watchdog defaults, and the dynamic vars the loop binds per call.

A LEAF — depends on nothing else in the engine, so the loop and its tests read these settings from one place instead of carrying them in the loop namespace.

Per-eval / per-call runtime knobs for the loop: Python-sandbox eval timeouts
(with clamping and a shell-timeout-aware widener), the `svar/ask-code!` stream
watchdog defaults, and the dynamic vars the loop binds per call.

A LEAF — depends on nothing else in the engine, so the loop and its tests read
these settings from one place instead of carrying them in the loop namespace.
raw docstring

com.blockether.vis.internal.sandbox-fs

A GraalPy FileSystem that gives the Python sandbox REAL filesystem access CONFINED to the session's filesystem roots.

Security model — every path-accessing operation canonicalizes its arguments and refuses anything that does not resolve UNDER a current filesystem root:

  • .. traversal is defeated by normalize.
  • symlink escapes are defeated by resolving the path through the REAL path of its nearest existing ancestor (so a symlink inside a root that points outside is rejected, and a symlink whose target is inside is allowed).
  • the root set is read LIVE via roots-fn on every check, so /fs add|/fs remove takes effect immediately.

GraalPy's own stdlib / internal resources live OUTSIDE the roots, so the confined FS is wrapped with allowLanguageHomeAccess + allowInternalResourceAccess (read-only access to the language home and bundled resources) before it reaches the Context.

OUTBOX tap — an optional engine-managed capture directory ($VIS_OUTBOX, distinct from the user /fs roots): the sandbox may WRITE there and every file it closes is handed to on-close so the engine can persist it as a session_iteration_attachment (the implicit twin of vis_attach). Reads, and writes anywhere else, are untouched.

Empty/zero roots ⇒ DENY everything (fail closed).

A GraalPy `FileSystem` that gives the Python sandbox REAL filesystem access
CONFINED to the session's filesystem roots.

Security model — every path-accessing operation canonicalizes its arguments
and refuses anything that does not resolve UNDER a current filesystem root:

  - `..` traversal is defeated by `normalize`.
  - symlink escapes are defeated by resolving the path through the REAL path
    of its nearest existing ancestor (so a symlink inside a root that points
    outside is rejected, and a symlink whose target is inside is allowed).
  - the root set is read LIVE via `roots-fn` on every check, so
    `/fs add|/fs remove` takes effect immediately.

GraalPy's own stdlib / internal resources live OUTSIDE the roots, so the
confined FS is wrapped with `allowLanguageHomeAccess` +
`allowInternalResourceAccess` (read-only access to the language home and
bundled resources) before it reaches the Context.

OUTBOX tap — an optional engine-managed capture directory (`$VIS_OUTBOX`,
distinct from the user `/fs` roots): the sandbox may WRITE there and every
file it closes is handed to `on-close` so the engine can persist it as a
`session_iteration_attachment` (the implicit twin of `vis_attach`). Reads,
and writes anywhere else, are untouched.

Empty/zero roots ⇒ DENY everything (fail closed).
raw docstring

com.blockether.vis.internal.security-policy

Canonical immutable security-policy snapshots and their model-facing view.

A snapshot is created once for a root environment, inherited unchanged by child environments, and replaced only by an explicit environment rebuild. Enforcement and context both derive from this value.

Canonical immutable security-policy snapshots and their model-facing view.

A snapshot is created once for a root environment, inherited unchanged by
child environments, and replaced only by an explicit environment rebuild.
Enforcement and context both derive from this value.
raw docstring

com.blockether.vis.internal.session-model

Persistent, channel-NEUTRAL per-session model preference.

ONE source of truth — session_soul.llm_pref_provider + llm_pref_model in the DB — for every channel (web gateway + TUI), so a session routes through the same PROVIDER + MODEL wherever it's opened and the choice survives restarts. Provider + model (not just a model name) mirrors how a turn records its route and disambiguates a model name shared by >1 provider.

The engine reads it at turn start (prepare-turn-context in loop.clj) as the default route when the caller passes none; router-for-model hoists the chosen model (the provider follows, since it's the one carrying that model).

DEBOUNCED WRITE-BACK: set-model! updates an in-memory value IMMEDIATELY (footer + engine see it at once) and coalesces the DB write, so cycling the model (TUI Ctrl+T) many times in a row produces a SINGLE write. Reads prefer the pending in-memory value, falling back to the DB.

Values are {:provider <id-string-or-nil> :model <name>} or nil. Keyed by the session-soul id (the gateway's sid and the engine env's :session-id).

Persistent, channel-NEUTRAL per-session model preference.

ONE source of truth — `session_soul.llm_pref_provider` + `llm_pref_model`
in the DB — for every channel (web gateway + TUI), so a session routes
through the same PROVIDER + MODEL wherever it's opened and the choice
survives restarts. Provider + model (not just a model name) mirrors how a
turn records its route and disambiguates a model name shared by >1 provider.

The engine reads it at turn start (`prepare-turn-context` in loop.clj) as
the default route when the caller passes none; `router-for-model` hoists the
chosen model (the provider follows, since it's the one carrying that model).

DEBOUNCED WRITE-BACK: `set-model!` updates an in-memory value IMMEDIATELY
(footer + engine see it at once) and coalesces the DB write, so cycling the
model (TUI Ctrl+T) many times in a row produces a SINGLE write. Reads prefer
the pending in-memory value, falling back to the DB.

Values are `{:provider <id-string-or-nil> :model <name>}` or nil. Keyed by
the session-soul id (the gateway's `sid` and the engine env's `:session-id`).
raw docstring

com.blockether.vis.internal.slash

Channel-agnostic slash dispatch.

Slashes are DECLARATIVE: every extension carries :ext/slash-commands on its manifest; the engine derives the active slash set by walking (active-extensions environment) at lookup time. NO global atom, NO register-slash! imperative call.

Public surface (re-exported through core.clj):

(active-slashes env) -> vec of slash specs (slash-by-path env path) -> slash spec or nil (slash-children env parent) -> vec of slash specs whose :slash/parent = parent (parse text) -> {:path :args :raw} | nil (raw tokenisation only; does NOT consult any registry) (dispatch env ctx text) -> envelope (see below)

The dispatch envelope is the contract every channel renders against:

{:handled? true :result <slash result map> :path path} {:handled? true :error msg :reason :unknown :tokens tokens} {:handled? true :error msg :reason :requires-failed :missing #{} :path} {:handled? true :error msg :reason :unavailable :path} {:handled? true :error msg :reason :no-run-fn :path} {:handled? true :error msg :reason :run-failed :ex t :path} {:handled? false} -- text was not a slash; channel forwards to LLM.

A slash text is any non-blank string starting with / followed by at least one word. Plain prose without the leading / is ALWAYS {:handled? false}.

Slash run-fns may return an EXTENDED :slash/* envelope that carries a rendered result card back to the channel:

{:slash/status :ok | :error | :nothing-to-commit | :ff-failed :slash/title short headline (string, plain) :slash/body IR (vector starting with :ir ...) OR Markdown string :slash/actions [{:label :slash}] ;; optional follow-ups :slash/data arbitrary payload (workspace-id, sha, ...)}

Channel-agnostic slash dispatch.

Slashes are DECLARATIVE: every extension carries `:ext/slash-commands`
on its manifest; the engine derives the active slash set by walking
`(active-extensions environment)` at lookup time. NO global atom, NO
`register-slash!` imperative call.

Public surface (re-exported through `core.clj`):

  (active-slashes  env)              -> vec of slash specs
  (slash-by-path   env path)         -> slash spec or nil
  (slash-children  env parent)       -> vec of slash specs whose
                                        `:slash/parent` = parent
  (parse           text)             -> {:path :args :raw} | nil
                                        (raw tokenisation only;
                                         does NOT consult any registry)
  (dispatch        env ctx text)     -> envelope (see below)

The dispatch envelope is the contract every channel renders against:

  {:handled? true  :result <slash result map> :path path}
  {:handled? true  :error msg :reason :unknown :tokens tokens}
  {:handled? true  :error msg :reason :requires-failed :missing #{} :path}
  {:handled? true  :error msg :reason :unavailable :path}
  {:handled? true  :error msg :reason :no-run-fn :path}
  {:handled? true  :error msg :reason :run-failed :ex t :path}
  {:handled? false}  -- text was not a slash; channel forwards to LLM.

A slash text is any non-blank string starting with `/` followed by
at least one word. Plain prose without the leading `/` is ALWAYS
{:handled? false}.

Slash run-fns may return an EXTENDED `:slash/*` envelope that carries
a rendered result card back to the channel:

  {:slash/status :ok | :error | :nothing-to-commit | :ff-failed
   :slash/title  short headline (string, plain)
   :slash/body   IR (vector starting with :ir ...) OR Markdown string
   :slash/actions [{:label :slash}]   ;; optional follow-ups
   :slash/data    arbitrary payload (workspace-id, sha, ...)}
raw docstring

com.blockether.vis.internal.strutil

Shared tiny string helpers. A dependency-free leaf so any namespace can use it without risking a cycle.

Shared tiny string helpers. A dependency-free leaf so any namespace can use it
without risking a cycle.
raw docstring

com.blockether.vis.internal.test-contract

Language-neutral test-runner CONTRACT shared across vis language packs.

ONE vocabulary for selecting and reporting tests, modeled on lazytest's CLI (NoahTheDuke/lazytest): single test, many namespaces, ignore by name or by metadata tag. A future python / js language pack returns the SAME shaped result map and accepts the SAME selector keys, so the agent learns the words once and they carry across languages.

The selector vocabulary and the result shape are DEFINED with clojure.spec (::selectors, ::result). selector-keys / result-keys are DERIVED from those specs (via s/form) so the spec is the single source of truth - a key never drifts out of sync with its documentation.

SELECTOR keys (all optional; the Python dict the tool receives): :ns string OR vector of namespace strings - which namespace(s) to run. One string = a single ns; a vector = many. (lazytest -n) :only vector of test-name strings - run ONLY these tests/vars within the selected namespace(s). (lazytest -v / source :focus) :include vector of metadata-tag strings - run only tests carrying one of these tags, e.g. "integration". (lazytest -i) :exclude vector of metadata-tag strings - skip tests carrying one of these tags, e.g. "slow". (lazytest -e)

PRECEDENCE (copied verbatim from lazytest):

  • :exclude OVERRIDES :include (a test tagged both is skipped).
  • source-level :skip OVERRIDES :focus.

RESULT keys (the uniform map every pack returns): :language "clojure" | "python" | ... :mode "repl" | "cli" - which execution path ran :framework "clojure.test" | "lazytest" | ... (repl path) :tool "clj" | "lein" | "bb" | ... (cli path) :ns the namespace(s) run :total test count actually run :pass passing count :fail failing + erroring count :selected count chosen by the selectors (before skips) :skipped count filtered out by :exclude / source :skip :failures [{:ns :test :message :file :line} ...] :errors the erroring-test subset of :failures :output captured run log (framework report + error/exception traces)

Language-neutral test-runner CONTRACT shared across vis language packs.

ONE vocabulary for selecting and reporting tests, modeled on lazytest's CLI
(NoahTheDuke/lazytest): single test, many namespaces, ignore by name or by
metadata tag. A future python / js language pack returns the SAME shaped
result map and accepts the SAME selector keys, so the agent learns the
words once and they carry across languages.

The selector vocabulary and the result shape are DEFINED with clojure.spec
(`::selectors`, `::result`). `selector-keys` / `result-keys` are DERIVED from
those specs (via `s/form`) so the spec is the single source of truth - a key
never drifts out of sync with its documentation.

SELECTOR keys (all optional; the Python dict the tool receives):
  :ns       string OR vector of namespace strings - which namespace(s) to
            run. One string = a single ns; a vector = many. (lazytest -n)
  :only     vector of test-name strings - run ONLY these tests/vars within
            the selected namespace(s). (lazytest -v / source :focus)
  :include  vector of metadata-tag strings - run only tests carrying one of
            these tags, e.g. "integration". (lazytest -i)
  :exclude  vector of metadata-tag strings - skip tests carrying one of
            these tags, e.g. "slow". (lazytest -e)

PRECEDENCE (copied verbatim from lazytest):
  - :exclude OVERRIDES :include (a test tagged both is skipped).
  - source-level :skip OVERRIDES :focus.

RESULT keys (the uniform map every pack returns):
  :language  "clojure" | "python" | ...
  :mode      "repl" | "cli"        - which execution path ran
  :framework "clojure.test" | "lazytest" | ... (repl path)
  :tool      "clj" | "lein" | "bb" | ... (cli path)
  :ns        the namespace(s) run
  :total     test count actually run
  :pass      passing count
  :fail      failing + erroring count
  :selected  count chosen by the selectors (before skips)
  :skipped   count filtered out by :exclude / source :skip
  :failures  [{:ns :test :message :file :line} ...]
  :errors    the erroring-test subset of :failures
  :output    captured run log (framework report + error/exception traces)
raw docstring

com.blockether.vis.internal.titling

Session-title subsystem, lifted out of the loop namespace: the three listener registries (per-session value, global, and the pending/spinner channel), the single set-title-with-broadcast! mutation point, and the async auto-title side-channel (an off-surface ask! that names a session on its first real turn). A LEAF — depends only on persistance + svar + runtime-settings, never back on the loop.

Session-title subsystem, lifted out of the loop namespace: the three listener
registries (per-session value, global, and the pending/spinner channel), the
single `set-title-with-broadcast!` mutation point, and the async auto-title
side-channel (an off-surface `ask!` that names a session on its first
real turn). A LEAF — depends only on persistance + svar + runtime-settings,
never back on the loop.
raw docstring

com.blockether.vis.internal.tls-mitm

Ephemeral CA + per-host leaf minting for the egress proxy's TLS-terminating (MITM) tier — the piece that gives a jailed shell child GET-not-POST fidelity over HTTPS, matching what the interpreter method-guard already does pre-TLS.

Why it exists: a plain CONNECT proxy only sees CONNECT host:443 — the method and path live inside the TLS the proxy never opens, so HTTPS verb/path is opaque. To read them the proxy must TERMINATE the child's TLS: present the child a leaf cert for the requested host, decrypt, inspect method+path, then re-encrypt to the real upstream (whose real cert the proxy still validates).

Trust model:

  • The CA is EPHEMERAL and per-session — born in this JVM, never written to the host trust store. Its cert PEM is written to a temp file whose path is injected into the jailed child's trust env (CURL_CA_BUNDLE/SSL_CERT_FILE /REQUESTS_CA_BUNDLE/NODE_EXTRA_CA_CERTS/GIT_SSL_CAINFO). Only children inside the jail ever see or trust it.
  • Upstream (proxy -> real server) uses the SYSTEM trust store by default, so the real server's real certificate is still validated end to end.

No JCA provider is registered globally: bcpkix's Jca* builders use the default platform signer (SHA256withRSA via SunRsaSign), which keeps this native-image friendly and side-effect free.

Ephemeral CA + per-host leaf minting for the egress proxy's TLS-terminating
(MITM) tier — the piece that gives a jailed shell child GET-not-POST fidelity
over HTTPS, matching what the interpreter method-guard already does pre-TLS.

Why it exists: a plain CONNECT proxy only sees `CONNECT host:443` — the method
and path live inside the TLS the proxy never opens, so HTTPS verb/path is
opaque. To read them the proxy must TERMINATE the child's TLS: present the
child a leaf cert for the requested host, decrypt, inspect method+path, then
re-encrypt to the real upstream (whose real cert the proxy still validates).

Trust model:
  - The CA is EPHEMERAL and per-session — born in this JVM, never written to
    the host trust store. Its cert PEM is written to a temp file whose path is
    injected into the jailed child's trust env (`CURL_CA_BUNDLE`/`SSL_CERT_FILE`
    /`REQUESTS_CA_BUNDLE`/`NODE_EXTRA_CA_CERTS`/`GIT_SSL_CAINFO`). Only children
    inside the jail ever see or trust it.
  - Upstream (proxy -> real server) uses the SYSTEM trust store by default, so
    the real server's real certificate is still validated end to end.

No JCA provider is registered globally: bcpkix's Jca* builders use the default
platform signer (SHA256withRSA via SunRsaSign), which keeps this native-image
friendly and side-effect free.
raw docstring

com.blockether.vis.internal.toggles

Process-wide feature-toggle registry.

Replaces the two parallel toggle plumbings we had drifting apart:

  • hard-coded TUI booleans (:show-thinking, :show-iterations, :show-silent) wired into state/default-settings,
  • per-render if (some-flag …) … else … checks scattered through internal/render.clj and channel-tui's render layer.

A toggle has stable metadata (id, label, description, default, owner) and a current ON/OFF value. Anyone — internal modules, extensions, channels — registers their toggles into the same registry; any caller flips a toggle through the same set!. The TUI settings dialog walks the registry to render the list, so adding a new toggle from an extension shows up in the user's UI without any TUI patch.

Persistence is opt-in: register-toggle! accepts :persist? true and on set! the wrapper writes {:toggles {id value}} into the machine store ~/.vis/state.yml via vis.config/save-config!. Hand-authored vis.yml / config.yml may ALSO declare a toggles: block. Ids are plain snake_case strings there (reasoning_level: deep), identical to the registered id. coerce-config-value maps YAML strings onto each toggle's type, so a config-declared toggle behaves exactly like a UI flip. Hydration happens at process start (call hydrate-from-config! once after config/load-config-raw).

Contract:

  • Toggle ids are non-blank snake_case strings (reasoning_level, shell, ...) — no keywords, namespaces, slashes, or kebab-case. YAML config uses the same string verbatim (reasoning_level: deep).
  • enabled? is cheap (single atom deref + string lookup), called per-paint per-row by the render layer; do not turn it into a function-call indirection.
  • Defaults are immutable once registered. Re-registering the same id is allowed (idempotent boot path) and merges over prior metadata; the live VALUE in state is left alone so a user override survives a reload.
Process-wide feature-toggle registry.

Replaces the two parallel toggle plumbings we had drifting apart:
  - hard-coded TUI booleans (`:show-thinking`, `:show-iterations`,
    `:show-silent`) wired into `state/default-settings`,
  - per-render `if (some-flag …) … else …` checks scattered through
    `internal/render.clj` and channel-tui's render layer.

A toggle has stable metadata (`id`, label, description, default,
owner) and a current ON/OFF value. Anyone — internal modules,
extensions, channels — registers their toggles into the same
registry; any caller flips a toggle through the same `set!`. The
TUI settings dialog walks the registry to render the list, so
adding a new toggle from an extension shows up in the user's UI
without any TUI patch.

Persistence is opt-in: `register-toggle!` accepts `:persist? true`
and on `set!` the wrapper writes `{:toggles {id value}}` into the
machine store `~/.vis/state.yml` via `vis.config/save-config!`.
Hand-authored `vis.yml` / `config.yml` may ALSO declare a `toggles:`
block. Ids are plain snake_case strings there (`reasoning_level: deep`),
identical to the registered id. `coerce-config-value` maps YAML strings
onto each toggle's type, so a config-declared toggle behaves exactly like
a UI flip. Hydration happens at process start (call
`hydrate-from-config!` once after `config/load-config-raw`).

Contract:
  - Toggle ids are non-blank snake_case strings (`reasoning_level`,
    `shell`, ...) — no keywords, namespaces, slashes, or kebab-case.
    YAML config uses the same string verbatim (`reasoning_level: deep`).
  - `enabled?` is cheap (single atom deref + string lookup), called
    per-paint per-row by the render layer; do not turn it into a
    function-call indirection.
  - Defaults are immutable once registered. Re-registering the
    same id is allowed (idempotent boot path) and merges over
    prior metadata; the live VALUE in `state` is left alone so a
    user override survives a reload.
raw docstring

com.blockether.vis.internal.workspace

Backend-neutral workspaces, DB-pinned to session_state 1:1.

The user's real cwd is trunk — Vis never mutates it, and Vis no longer requires it to be a git repo. A session works in trunk by default; /draft new opts into an isolated workspace supplied by a registered backend. Backends declare concrete capabilities such as isolated fork, rollback, merge-back, retained revisions, and parallel safety. Core never assumes which implementation provides them.

'What changed since the fork' is computed git-free: clonefile preserves source mtimes, so files the agent touches in the clone get a fresh mtime greater than the fork timestamp we capture at clone time. apply! lands exactly those files back into cwd, uncommitted, and leaves the user to commit with their own tools — Vis owns no git/branch/commit/merge lifecycle whatsoever.

Vis never mutates JVM user.dir. Channels rebind workspace-root per turn from the active workspace; tools resolve paths via (workspace/cwd). There is NO process-cwd fallback in production - the env carries :workspace/root from create-environment onward.

Backend-neutral workspaces, DB-pinned to session_state 1:1.

The user's real cwd is *trunk* — Vis never mutates it, and Vis no
longer requires it to be a git repo. A session works in trunk by
default; `/draft new` opts into an isolated workspace supplied by a
registered backend. Backends declare concrete capabilities such as
isolated fork, rollback, merge-back, retained revisions, and parallel
safety. Core never assumes which implementation provides them.

'What changed since the fork' is computed git-free: `clonefile`
preserves source mtimes, so files the agent touches in the clone get
a fresh mtime greater than the fork timestamp we capture at clone
time. `apply!` lands exactly those files back into cwd, uncommitted,
and leaves the user to commit with their own tools — Vis owns no
git/branch/commit/merge lifecycle whatsoever.

Vis never mutates JVM user.dir. Channels rebind *workspace-root* per
turn from the active workspace; tools resolve paths via
(workspace/cwd). There is NO process-cwd fallback in production -
the env carries `:workspace/root` from `create-environment` onward.
raw docstring

cljdoc builds & hosts documentation for Clojure/Script libraries

Keyboard shortcuts
Ctrl+kJump to recent docs
Move to previous article
Move to next article
Ctrl+/Jump to the search field
× close