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.Filesystem tools exposed as bare symbols in the Python sandbox.
Two layers:
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
(ls path) ; -> nested dict tree (name/path/type/size/children)
(ls path opts) ; opts keys: depth / is_hidden / is_respect_gitignore
(rg query) ; -> content hits; query = a term or list of terms (OR),
; smart-case substring. Opts: paths/include/context/is_files_only
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
(ls path) ; -> nested dict tree (name/path/type/size/children)
(ls path opts) ; opts keys: depth / is_hidden / is_respect_gitignore
(rg query) ; -> content hits; query = a term or list of terms (OR),
; smart-case substring. Opts: paths/include/context/is_files_only
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.Structural outline: 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([{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 outline: 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([{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.Fuzzy line-based matching toolkit used by patch exact-replace.
This namespace USED to also carry the Codex apply_patch envelope
parser, but envelope mode was retired — we consolidated to a single
per-intent mutation surface (patch exact-replace + write /
move / delete). The fuzzy matcher stays because it is what
lets multi-line :search blocks tolerate whitespace and typographic
drift before they fall through to :no-match.
Public surface (all pure): seek-sequence lines pattern start eof? -> start-index or nil seek-sequence-with-pass lines pattern start eof? -> {:start :pass :indent-delta?} or nil split-content-lines string -> vec of lines (no trailing \n element) char-offset-at-line content line-idx -> char offset apply-indent-delta delta lines -> re-indented lines
It also owns the reusable HASHLINE layer (content-addressed editing):
line-hash / lines->anchors text -> 6-hex anchor / {ln hash}
render-hashline-block / -range-block tuples -> <hash>| text gutter
indices-matching-hash / resolve-anchor-edit self-locating range replace
Fuzzy line-based matching toolkit used by `patch` exact-replace.
This namespace USED to also carry the Codex `apply_patch` envelope
parser, but envelope mode was retired — we consolidated to a single
per-intent mutation surface (`patch` exact-replace + `write` /
`move` / `delete`). The fuzzy matcher stays because it is what
lets multi-line `:search` blocks tolerate whitespace and typographic
drift before they fall through to `:no-match`.
Public surface (all pure):
seek-sequence lines pattern start eof? -> start-index or nil
seek-sequence-with-pass lines pattern start eof?
-> {:start :pass :indent-delta?} or nil
split-content-lines string -> vec of lines (no trailing \n element)
char-offset-at-line content line-idx -> char offset
apply-indent-delta delta lines -> re-indented lines
It also owns the reusable HASHLINE layer (content-addressed editing):
line-hash / lines->anchors text -> 6-hex anchor / {ln hash}
render-hashline-block / -range-block tuples -> `<hash>| text` gutter
indices-matching-hash / resolve-anchor-edit self-locating range replaceThin 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.
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.
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.
vis-foundation — the agent's environment-awareness layer.
Owns the environment facts: cwd, user, platform, shell, plus:
Model-facing VCS/workspace truth lives in :session/workspace CTX.
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 total bytes, primary language),
* monorepo / multi-package shape detection (polylith, workspace,
submodules) by counting per-ecosystem manifests.
Model-facing VCS/workspace truth lives in `:session/workspace` CTX.
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!)`.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.
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.
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.
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.
Build compact foundation environment data for ctx. No prompt labels.
Build compact foundation environment data for `ctx`. No prompt labels.
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.
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 lean, string-keyed result the model reads
directly: {"cmd", "args", "stdout", "duration_ms"} plus "exit"
(when the process finished), "stderr" (when non-empty), and
"timed_out" (when it blew the timeout). 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 lean, string-keyed result the model reads
directly: `{"cmd", "args", "stdout", "duration_ms"}` plus `"exit"`
(when the process finished), `"stderr"` (when non-empty), and
`"timed_out"` (when it blew the timeout). 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.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-md [session-id]) -> Markdown rendered from that dataEverything 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-md [session-id])` -> Markdown 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.
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_start 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_start` creates a language-owned session resource and `repl_stop` stops one by id. Live REPLs also surface in the ctx `resources` block.
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.
Pure-Java pseudo-terminal for shell_bg — 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:
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).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 `shell_bg` — 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}Passthrough bridge on top of the FFM pseudo-terminal (internal.foundation.pty).
The problem it solves: a shell_bg child runs INSIDE the vis process (the FFM
PTY master fd + reader thread live in vis's heap). That's great for the agent
(shell_send / shell_logs) 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 ext 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 `shell_bg` child runs INSIDE the vis process (the FFM PTY master fd + reader thread live in vis's heap). That's great for the agent (`shell_send` / `shell_logs`) 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 ext 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.
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: a short always-on prompt fragment says the docs exist and when to reach for them; 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: a short always-on prompt fragment says the docs exist and when to reach for them; page content is only paid for when actually fetched.
Declarative session-level slash commands shared by every channel.
These are channel-agnostic: the engine dispatches them for the TUI, the
web, and Telegram alike 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 the TUI, the web, and Telegram alike 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.
shell/ compatibility extension — a DROPPABLE classpath plug-in (drop the
jar, drop the feature), gated behind the user-owned :shell/enabled toggle
(OFF by default; every call short-circuits into a refusal envelope until the
user flips it in settings).
Three model-facing bindings under alias shell (the flat Python sandbox
renders alias/name as <alias>_<name>, so the calls are
shell_run / shell_bg / shell_logs — same shape as git_status,
clj_eval, search_web):
SYNC shell_run(cmd) / shell_run(cmd, opts) — bash -lc in the
workspace root, waits up to a timeout, and returns a LEAN payload with
string keys cmd/stdout/duration_ms + conditional keys (exit when finished;
timed_out/timeout_secs on timeout; stderr when non-empty; truncation
flags when true; cwd when narrowed) — results ride every later prompt
as frozen <results> pins, so dead fields never ship. Output is bounded
at read time to a head+tail budget per stream — only the MIDDLE of a huge
stream is dropped, never its start or end (memory can't balloon on a
chatty-then-killed command). A non-zero exit is DATA the
model reads, not a tool error.
BACKGROUND shell_bg(id, cmd) — spawns the process, pumps its merged
output into a bounded ring buffer, and registers it as a session
RESOURCE in internal.resources: it shows up in the footer count, the
F4 dialog, and the resources ctx block, and the ONE stop path
is resource_stop(id) (model) / the footer dialog (user) — both land
on resources/stop!, which runs our :stop-fn (process-tree kill +
buffer drop). An exited process is NOT auto-pruned (its :alive-fn
reports true while the buffer entry exists) so shell_logs(id) can
still read its output + exit code until the resource is stopped.
shell_logs(id) / shell_logs(id, n) — tail of a background shell's
captured output as [seq, line] tuples plus status/exit/uptime.
The :shell/enabled toggle is registered HERE, extension-owned under the
extension's own namespace.
`shell/` compatibility extension — a DROPPABLE classpath plug-in (drop the jar, drop the feature), gated behind the user-owned `:shell/enabled` toggle (OFF by default; every call short-circuits into a refusal envelope until the user flips it in settings). Three model-facing bindings under alias `shell` (the flat Python sandbox renders `alias/name` as `<alias>_<name>`, so the calls are `shell_run` / `shell_bg` / `shell_logs` — same shape as `git_status`, `clj_eval`, `search_web`): 1. SYNC `shell_run(cmd)` / `shell_run(cmd, opts)` — `bash -lc` in the workspace root, waits up to a timeout, and returns a LEAN payload with string keys cmd/stdout/duration_ms + conditional keys (exit when finished; timed_out/timeout_secs on timeout; stderr when non-empty; truncation flags when true; cwd when narrowed) — results ride every later prompt as frozen <results> pins, so dead fields never ship. Output is bounded at read time to a head+tail budget per stream — only the MIDDLE of a huge stream is dropped, never its start or end (memory can't balloon on a chatty-then-killed command). A non-zero exit is DATA the model reads, not a tool error. 2. BACKGROUND `shell_bg(id, cmd)` — spawns the process, pumps its merged output into a bounded ring buffer, and registers it as a session RESOURCE in `internal.resources`: it shows up in the footer count, the F4 dialog, and the `resources` ctx block, and the ONE stop path is `resource_stop(id)` (model) / the footer dialog (user) — both land on `resources/stop!`, which runs our `:stop-fn` (process-tree kill + buffer drop). An exited process is NOT auto-pruned (its `:alive-fn` reports true while the buffer entry exists) so `shell_logs(id)` can still read its output + exit code until the resource is stopped. 3. `shell_logs(id)` / `shell_logs(id, n)` — tail of a background shell's captured output as `[seq, line]` tuples plus status/exit/uptime. The `:shell/enabled` toggle is registered HERE, extension-owned under the extension's own namespace.
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).
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.
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 conftest.py
file discovery, no CLI, and no import-time assertion rewrite. Instead it
implements the subset that matters in an inline sandbox where the model
writes tests + pytest.main() in ONE block:
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),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), @pytest.mark.parametrize
/ skip / skipif / xfail (+ arbitrary marks), pytest.param,
builtin fixtures request / monkeypatch / capsys / capfd /
tmp_path, and a pytest.main() runner that prints progress + failure
reports + a summary line 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 `conftest.py`
file discovery, no CLI, and no import-time assertion rewrite. 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), `@pytest.mark.parametrize`
/ `skip` / `skipif` / `xfail` (+ arbitrary marks), `pytest.param`,
builtin fixtures `request` / `monkeypatch` / `capsys` / `capfd` /
`tmp_path`, and a `pytest.main()` runner that prints progress + failure
reports + a summary line 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 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).
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.
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-md) for Markdown; 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 :answer :iterations [{:id :position :status :duration-ms :provider :model :thinking :error :tokens :cost-usd :answer-position :returned-empty-blocks? :vars [{:name :code :value :version}] :blocks [{:position :code :comment :result :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-md)` for Markdown; 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 :answer
:iterations
[{:id :position :status :duration-ms
:provider :model :thinking :error
:tokens :cost-usd
:answer-position :returned-empty-blocks?
:vars
[{:name :code :value :version}]
:blocks
[{:position :code :comment :result :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.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.
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
Filesystem permissions (/fs, /root) — session-scoped, every channel:
/root [path] show / CHANGE the session's filesystem root /fs list the session's filesystem permissions /fs root <path> same as /root <path> /fs add <path> also let the session operate under <path> /fs remove <path> drop an added directory /fs create <path> mkdir + add it
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 Filesystem permissions (`/fs`, `/root`) — session-scoped, every channel: /root [path] show / CHANGE the session's filesystem root /fs list the session's filesystem permissions /fs root <path> same as /root <path> /fs add <path> also let the session operate under <path> /fs remove <path> drop an added directory /fs create <path> mkdir + add it 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.
cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |