Surface Parity Map

July 16, 2026 · View on GitHub

This document tracks which REPL operations are exposed across each Beamtalk surface (CLI, REPL meta-commands, MCP tools, LSP capabilities). It is the single source of truth for identifying gaps and preventing surface drift.

Parity Contract

Any operation not labelled surface-specific must produce equivalent output across all surfaces where it appears. "Equivalent" means the same structured data (modulo transport encoding); display formatting may vary per surface.

When adding a new capability to any surface, update this table. If the capability maps to an existing REPL op, add it to the corresponding row. If it is surface-specific, add the reason.

REPL surface — meta-commands vs. message-sends

The REPL exposes capability through two surfaces:

  1. Message-sends (Workspace …, Beamtalk …, or any object) — the primary surface. Object-level capability is reached this way, not through meta-commands.
  2. Meta-commands (:cmd) — reserved for client-side concerns (:exit, :help) or transport-out-of-band ops that cannot be expressed as a normal eval (e.g. :interrupt, since you cannot send an eval while one is blocking the session).

Historically meta-commands like :bindings, :sync, :test existed to bootstrap the REPL before the object model was rich enough. Now that Workspace and Beamtalk express most capability, the REPL column in this map cites the message-send (via Workspace classes, via Beamtalk help:) rather than treating absent meta-commands as gaps.

Legend

SymbolMeaning
nameBinding exists (tool/command/handler name shown)
via XReachable as a message-send on X (e.g. via Workspace classes) — counts as parity for the REPL surface
--Not applicable for this surface
surface-specific: reasonIntentionally present only on this surface
MISSINGShould exist but does not yet

Inventory Sources

SurfaceSource of truth
REPL opsdescribe_ops() in beamtalk_repl_ops_dev.erl + beamtalk_repl_ops_perf.erl
CLI subcommandsCommand enum in crates/beamtalk-cli/src/main.rs
REPL meta-commandshandle_repl_command() in crates/beamtalk-cli/src/commands/repl/mod.rs
MCP tools#[tool(...)] in crates/beamtalk-mcp/src/server.rs
LSP capabilitiesServerCapabilities in crates/beamtalk-lsp/src/server.rs
LiveView IDE ops@ops in editors/liveview/lib/bt_attach/facade.ex

LiveView IDE (remote authenticated front, ADR 0091)

The Phoenix LiveView IDE (editors/liveview/, Attach topology) is not a new op vocabulary — its curated facade (BtAttach.Facade) dispatches the same REPL ops (eval, inspect, bindings, load-source, save/flush, pid-stats, the transcript/bindings/per-object subscriptions, …) via the BT-2399 term-returning seam, so results are surface-consistent by construction (live terms; JSON only at the WebSocket edge). It adds, only on this surface, two access controls that do not change op output:

  • Curated facade — the browser cannot supply an arbitrary {m,f,a}; an off-vocabulary op is refused (403) with no dist call (surface-specific).
  • Two-level RBACOwner (execute+read+admin) vs. Observer (read-only); per-op authorization runs before any dist call, keyed to the OIDC identity (surface-specific). See docs/security/threat-model.md.

The IDE's REPL tab (BT-2543) also carries a client-side meta-command layer, the GUI analogue of the CLI's handle_repl_command() (BT-2543 follow-up): a leading-colon submit is recognised in WorkspaceLive (repl_meta_command/1) and routed to the matching IDE surface instead of being sent to eval (which would reject :h as a parse error). :help <Class> — and a Beamtalk help: <Class> message-send — focus the System Browser on that class; :bindings, :changes/:dirty, :flush point at the Bindings pane / Changes tab; :test notes the forthcoming test-runner pane (BT-2557); :sync/:s, :clear, :show-codegen/:sc, and :exit get a :point note explaining the IDE equivalent (or that the op is CLI-only); unknown :cmds get a friendly note. This is surface-specific UX routing — it adds no op vocabulary and changes no op output (the underlying capability is still the same eval / browse_* ops these panes already use).

Core Operations

REPL opCLI subcommandREPL meta-commandMCP toolLSP capabilityNotes
eval--(implicit: any expression)evaluate--Core evaluation; CLI uses REPL client, LSP has no eval. The LiveView IDE (Attach topology, editors/liveview/, BT-2407) is a further consumer: it dispatches the same eval op via the term-returning seam (beamtalk_repl_ops:dispatch/4, BT-2399) over Erlang distribution, so it shares the same structured result — surface-consistent by construction, no JSON-flattened string. The result value (render_term/format_value) is identical across surfaces; presentation differs and is surface-specific — the CLI prints to scrollback, while the LiveView IDE offers two interactive presentations of the same eval op, on the same session and structured result: the Workspace (BT-2542) inserts the result inline in the editor buffer as a collapsible annotation (Smalltalk "Print it"), with a transient → result status echo and Do it/Inspect it variants; the REPL tab (BT-2543) instead appends a › request / → response pair to a classic TUI scrollback with the input pinned at the bottom (terminal idiom — Enter submits, ↑/↓ recall history, each → response keeps an Inspect affordance into the Inspector). Ambient Transcript show: output streams to the Transcript pane on every LiveView presentation, never duplicated into the Workspace/REPL result view.
stdin--(implicit: interactive input)----Provides input to a blocked eval; CLI handles interactively
complete--(implicit: tab completion)completecompletionAutocompletion suggestions. The CLI REPL drives the complete op over the protocol (tab completion); the LiveView IDE (Attach topology, BT-2544) is a further consumer — its CodeMirror editors (Workspace and REPL composer) round-trip the current line up to the caret through the term-returning seam (beamtalk_repl_ops:dispatch/4BtAttach.Workspace.complete/2 → facade :complete, capability :read) and feed the candidates to @codemirror/autocomplete. All consumers share the same receiver-aware, binding-aware ranking engine (BT-783) — no forked completion list. Completion runs no user code (pure reflection/index), so the Observer role completes too. The LSP completion capability is the editor-protocol projection of the same suggestions. FFI-typed fallback (BT-2891): when the receiver-aware engine can't type an expression, beamtalk_repl_ops_dev:resolve_type_via_compiler/1's compiler-based fallback (BT-1068) sends a resolve_completion_type request to the Rust compiler port; handle_resolve_completion_type now consults a process-wide NativeTypeRegistry loaded once from _build/type_cache/ (mirroring how check/LSP (BT-2859), lint (BT-2851/BT-2858), and type-coverage (BT-2867) already source the same registry), so an FFI expression like Erlang lists reverse: x resolves its real return class instead of Dynamic when the project has been built. Loaded once per compiler-port process (no live re-extraction, unlike the LSP's load_type_cache) to stay within the REPL completion latency budget (ADR 0045); a beamtalk build run after the process starts is not picked up until the process restarts. BT-2901 (ADR 0108 Phase 8): the static LSP completion path offers type alias names in type-annotation position, alongside class/protocol names — completion_provider::compute_completions_with_aliases adds a project-wide AliasRegistry lookup (same-file type declarations first, then ProjectIndex::alias_registry's cross-file entries). BT-2918: the live complete op's receiver-aware engine now matches — beamtalk_repl_ops_dev:type_annotation_prefix/1 detects annotation position (immediately after ::, in either the {expression, ReceiverExpr, Prefix} or the no-space-before-:: {Receiver, Prefix} parse shape) and type_annotation_completions/2 offers class_name_completions/1 plus this session's live type alias names (beamtalk_repl_state's per-session alias_table, threaded through a new beamtalk_repl_shell:get_alias_table/1 call mirroring get_bindings/1) — no forked completion list, so CLI REPL, MCP complete, and the LiveView IDE all pick it up automatically.
hover--(implicit: :help shows the same live docs)--textDocument/hover (see note — different engine)Hover documentation (signature + doc-comment tooltips). New for the LiveView IDE (Attach topology, BT-2555): its CodeMirror editors (Workspace, method editor, REPL composer) round-trip the editor line up to the hovered token through the term-returning seam (beamtalk_repl_ops:dispatch/4BtAttach.Workspace.hover/2 → facade :hover, capability :read) and feed the docs to @codemirror/view's hoverTooltip (no new npm dep). The op resolves the hovered token against the live class registry — a class name → its docs, a Receiver selector pair → that method's docs — and formats it via beamtalk_repl_docs, the same live doc engine the CLI REPL :help uses (no forked hover engine). Deliberate divergence from the LSP textDocument/hover: the LSP path (hover_provider.rs, Backend::hover) is purely static — it reads on-disk .bt ASTs (class.doc_comment / method.doc_comment) and so cannot see REPL-defined classes, live-patched (>>) methods, or unflushed edits. The cockpit's hover is live-image-accurate: it shows docs for exactly the code that is running. This mirrors the BT-2544 decision to route completion through the live complete op rather than the static Rust completion_provider. Hover runs no user code (pure reflection), so the Observer role hovers too.
diagnostics--(implicit: a compile error shows in eval output)--(LSP publishes via textDocument/publishDiagnostics — a separate static-push transport, see note)Live parse-only diagnostics (error/warning squiggles as you type). New for the LiveView IDE (Attach topology, BT-2556): its editable CodeMirror editors (Workspace, method editor, REPL composer) round-trip the full buffer through the term-returning seam (beamtalk_repl_ops:dispatch/4BtAttach.Workspace.diagnostics/2 → facade :diagnostics, capability :read) and feed the ranges to @codemirror/lint's linter(...). The op runs the compiler's side-effect-free diagnostics path (beamtalk_compiler:diagnostics/2 → the Rust port's diagnostics command): the buffer is parsed + semantically checked for diagnosis only — it generates no code, installs no module, mutates no image state, appends nothing to the ChangeLog, and runs no user code — so it is safe to fire as the buffer changes and is :read (the Observer sees diagnostics too). A mode param (BT-2569) selects the parse grammar: the Workspace + REPL editors (and the method editor's :def class-definition tab) send "expression" (the default — a top-level script: full-module parse + semantic analysis), while the System Browser method-editor method tabs send "method" (data-lint-mode="method"). A method tab's buffer is a bare method body (e.g. decrement => self.value := self.value - 1); under the default grammar the => body separator is not a valid top-level token, so it reported a false expected expression, found ⇒. Method mode parses it with parse_method and is parse-only — a method analysed outside its class has no field/self/type context, so semantic checks would false-positive and are deferred to Compile (compile_method, which has class context). Debounce is upstream of the round-trip, on the @codemirror/lint delay (~150 ms idle), mirroring how the LSP debounces didChange; a server-side drop debounce would break the request/reply contract. Deliberate divergence from the LSP textDocument/publishDiagnostics: the LSP pushes diagnostics on did_change/did_open over its own transport (static, on-disk ASTs via beamtalk-lsp); the cockpit pulls them through the live workspace seam, so they reflect the running image's class hierarchy rather than a second static analyzer. Diagnostics carry byte-offset spans (start/end) which the client maps to editor positions (renamed from/to). BT-2839 (ADR 0100 Rule 3): in "expression" mode (full semantic analysis, not the parse-only "method" mode), this op now also applies the package's beamtalk.toml [diagnostics] severity-override table — the same table beamtalk build/check and the LSP already apply (BT-2793, BT-2800) — so a category escalated to Error shows as an error here too, not a quiet hint; see the check row above for the shared mechanism.
reload-findings (plus push: reload_check/completed)--(none — see BT-2801 note below)--(seeds on attach, then pushes textDocument/publishDiagnostics, tagged source: "beamtalk (reload)", plus a window/logMessage summary)Reload-induced diagnostics (ADR 0105 Phase 1, BT-2779): on a live reload with a non-no-op signature diff, beamtalk_recheck (BT-2778) re-checks known dependents and beamtalk_repl_loader:maybe_trigger_recheck/4 publishes the outcome as a 'ReloadCheckCompleted' system announcement — a push, not a request/response op by itself, so new outcomes have no REPL-op name; they ride the same beamtalk_repl_subscriptions stream facade as classes/flush (stream reload_check, subscribed automatically by every connected client via subscribe_all/0). Attribution is to the caller's source location, not the reload's. Clearing-by-replacement (ADR 0105 §Mechanism step 4) is the cross-surface contract every consumer must honour: the event's checkedOwners list names every caller class whose reload-induced diagnostics must be replaced wholesale with findings filtered to that owner — including replaced with nothing, which is how a stale finding clears without anyone editing the caller (reload-fixes-reload) or how back-to-back reloads avoid accumulating contradictory findings (supersession). beamtalk_repl_loader also fires this on any install/removal for the installed class's own findings-as-caller (not just the ADR's literal "on Workspace changes revert:" bullet — a revert re-installs through the identical path, so the broader rule subsumes it with no bespoke revert hook) and never persists across a workspace restart (beamtalk_workspace_findings_store is in-memory, supervised alongside beamtalk_workspace_signature_store). shape_change (ADR 0105 Phase 2, BT-2780) is a third classification alongside signature_change/removal: a class-body reload that adds/removes/retypes a state:/field: slot is captured by beamtalk_workspace_shape_store (two-phase, around code:load_binary rather than the single pre-install call the signature store uses — see its moduledoc) and re-checked by beamtalk_recheck:trigger_shape/2 against spawnWith: call sites and the changed slots' compiler-generated accessor selectors, publishing through this exact same put_owner_origin/'ReloadCheckCompleted' path — changedSelector carries a comma-joined list of the affected slot names (beamtalk_repl_loader:shape_change_summary/1) since there is no single changed selector. reload-findings (BT-2801) is the request/response counterpart every push-only surface was missing: a snapshot read of beamtalk_workspace_findings_store:all/0, returned in the exact wire shape (encode_reload_finding/1, shared between the op and the push encoder so the two can never drift) the reload_check push's findings field already uses — closing the surface-parity gap where findings that existed before a caller connected were invisible until the next reload happened to touch that specific caller again. Surface presentations: the CLI REPL prints an asynchronous notice interleaved with the prompt for new outcomes (format_reload_check_notice, crates/beamtalk-cli/src/commands/protocol.rs), matching the ADR's demo (/ for a stale signature-change/removal/shape-change, for a pure clearing event with no dependent re-check) — BT-2801 deliberately did not wire a fresh :repl connection to call reload-findings and print an initial summary: CLAUDE.md requires confirming with the user before changing any REPL display behaviour, and the issue's acceptance criteria called that out explicitly; a background agent implementing this issue chose the conservative option (stay silent until the next push, i.e. no CLI-side change) and flagged the "should a fresh connection show a one-time pre-existing-findings summary?" question for human follow-up rather than guessing at new REPL output; the reload-findings op itself is fully implemented and available for the CLI to call once that UX is decided. The LSP (crates/beamtalk-lsp/src/runtime.rs + server.rs) now calls reload-findings once on ensure_runtime_attached success — seeding reload_diagnostics and republishing for any already-open affected document — before falling back to the live listener for new outcomes; per-outcome it resolves each owner to a document URI via nav-symbols and publishes/merges reload-induced Diagnostics (line-granular — the site's xref-recorded line, not the byte-offset span computed against the live combined source, since there is no on-disk mapping for that span) alongside the file's static diagnostics, so one publishDiagnostics call never clobbers the other; the workspace/cockpit UI (editors/liveview/) exposes its own dist-attached reload_findings read op (initial snapshot, a raw :rpc.call/4 to the same store — distinct from the new curated reload-findings REPL op, which exists for non-dist-attached surfaces like the LSP) + subscribe_reload_check (live push) and renders a "Reload Checks" section in the Changes tab, applying the same clearing-by-replacement rule to its reload_findings assign — its classification field is rendered generically (to_string/1), so shape_change needed no LiveView-side changes. Site-level @expect markers (ADR 0100 Rule 3) are already absent from the underlying diagnostics by the time beamtalk_recheck sees them — no separate suppression needed on any surface.
precheck-method--(via eval: Class precheckCompile: #sel source: body)precheck_methodexecuteCommand: beamtalk.precheckMethodPre-save advisory (ADR 0105 Phase 3, BT-2782): compile a pending method edit and report would-be-stale dependents without installing — the editor/LSP's "check before save" hook, reusing BT-2778's re-check orchestration (beamtalk_recheck:trigger_pending/5) against the pending, not-yet-installed signature rather than the live one. Behaviour>>precheckCompile:source: is the one chokepoint every surface reaches: MCP precheck_method and the LSP beamtalk.precheckMethod command are typed front-ends that build aClass precheckCompile: #sel source: body and submit via evaluate — no dedicated workspace-side op. Non-blocking and read-only: nothing installs, nothing is recorded to the ChangeLog, and the automatic post-reload check (fired by the real compile:source:/save_method save) remains the authority — this is strictly an early warning. Returns a report shaped like the reload_check push's ReloadCheckCompleted payload (findings/checked/totalCandidates/notChecked/capNote/checkedOwners); a pending edit whose signature has not type-relevantly changed reports empty.
recheck-image--:recheck imageWorkspace recheckImagerecheck_imageexecuteCommand: beamtalk.recheckImageWhole-image re-check (ADR 0105 Phase 3, BT-2782): the "complete but unbounded" path ADR 0105's Alternatives section keeps out of the automatic per-reload trigger. Re-checks every live class the workspace has a recorded source for (beamtalk_recheck:trigger_image/0), not just the xref-filtered, capped dependents of the last changed selector. Workspace recheckImage is the one chokepoint every surface reaches: the REPL :recheck image meta-command, MCP recheck_image tool, and LSP beamtalk.recheckImage command are all shortcuts that submit the same message-send via evaluate — no dedicated workspace-side op. Returns a report Dictionary: checked (classes a re-check round-trip completed for), stale (distinct classes with at least one finding), findings (a List of per-class Dictionaries — owner/severity/category/message/start/end). Unlike the reload-triggered check, findings are not persisted to beamtalk_workspace_findings_store or published as diagnostics on any surface — this is a one-off report, not a live-updating stream.
show-codegen--:show-codegen / :scshow_codegen--Show generated Core Erlang
load-source--surface-specific: LiveView IDE Editor pane----Load inline source string (the Phase-1 vanilla-JS browser workspace was removed in BT-2415; the op is now consumed by the Phoenix LiveView IDE)
load-project--:sync / :sload_project--Sync project files from beamtalk.toml
run-entrybeamtalk run … --connect------surface-specific: connected-mode beamtalk run entry dispatch (BT-2691, ADR 0099 §3). Dispatches ClassName selector [args] into the project's live shared workspace over the protocol instead of booting a fresh run-mode node; argv crosses as a structured JSON array (no source-string splice). The entry runs on a session eval worker's synchronous call chain, so it reuses the streaming-output + script_exitexit_code machinery (BT-2688): a connected Program exit: N ends only that session (the node + other sessions stay up) and the CLI process adopts N, a normal return exits 0, an uncaught error exits 1. CLI-only — there is no REPL/MCP/LSP analogue (the REPL surface dispatches entries via plain eval).

Session Operations

REPL opCLI subcommandREPL meta-commandMCP toolLSP capabilityNotes
(via eval: Session current clear)--:clear (via Session current clear)(via evaluate)--Clear session locals. ADR 0081 / BT-2367 added the first-class Session object; BT-2369 (ADR 0081 Phase 6) removed the clear protocol op and the MCP clear toolSession current clear is the object-side accessor, reachable via eval/evaluate on every surface. The :clear CLI meta-command is retained as a shortcut that now evaluates Session current clear.
(via eval: Session current bindings)--:bindings / :b (via Session current bindings keys)(via evaluate)--View session locals. ADR 0081 / BT-2367 added Session current bindings, returning a live BindingsView (Dictionary protocol: at:, at:put:, removeKey:, includesKey:, keys, values, size, do:). BT-2369 (ADR 0081 Phase 6) removed the bindings protocol op and the MCP get_bindings tool — read via eval/evaluate (Session current bindings keys) on every surface. The :bindings / :b CLI meta-command is retained as a shortcut that now evaluates Session current bindings keys (it lists binding names; the old op also showed values). Note the object accessor returns session-locals only — workspace globals are a separate layer reached via Workspace globals (also a BindingsView).
(via eval: Session current kind / info / printString)--(via eval)(via evaluate)(via executeCommand: evaluate)Session origin/debug metadata. Session kind returns the originating client surface (repl/mcp/lsp/liveview/ide/attach, or unknown); Session info returns the full Dictionary (id, kind, and where known peer, node, user, connected_at); Session printString renders Session(<kind>: <id>). Object-side accessors reachable via eval/evaluate on every surface — not surface-specific. The surface is declared in the client field of the transport auth handshake (see docs/repl-protocol.md); the CLI sends repl, MCP mcp, LSP lsp, LiveView liveview. Read-only and not liveness-checked, so Workspace sessions renders each entry with its kind even if a session has died.
sessions--surface-specific: transport handshake----List active REPL sessions, each rendering via Session printString (e.g. Session(repl: <id>))
clone--surface-specific: transport handshake----Create a new session
close--:exit / :quit / :q----Close session; :exit exits the CLI REPL
interrupt--:interrupt / :intinterrupt--Cancel a running evaluation; out-of-band by definition (BT-2090)

Actor Operations

REPL opCLI subcommandREPL meta-commandMCP toolLSP capabilityNotes
actors--via Workspace actorslist_actors--List running actors
processes--via Workspace processessupervision_tree--Snapshot the live supervision tree (ADR 0092): a flat list of node records (pid, registeredName, kind, class, childCount, isSupervisor, parentPid for adjacency), serialised by SupervisionTree asDictionaries so every surface shows equivalent data. The MCP supervision_tree tool and the LiveView IDE processes op both surface the default scope (workspace tree, runtime plumbing filtered — a Read op, scoped like actors, ADR 0091 / BT-2432). The system scope (everything, incl. runtime internals) is privileged: reachable via the MCP tool's scope:"system" arg and the LiveView processes_system op, gated to the Owner (:execute) role — never exposed to an unprivileged caller.
inspect--surface-specific: agent-only typed introspectioninspect--Inspect actor state. Locked: structured-JSON view is for agents; humans use Transcript show: actorRef or send messages directly. LiveView-specific presentation (BT-2634): the Cockpit Inspector routes a supervisor handle ({:beamtalk_supervisor, …}) away from the actor sys:get_state path to a children / supervision-tree view — drillable child rows (class, pid, kind, childCount) backed by beamtalk_process_navigation:child_handles/1, each carrying a live child handle for reference-following (ADR 0095). Live-tracking (field-flash / pid-stats / per-object watch) is disabled for supervisors. This is IDE rendering of the supervision data (already cross-surface via the processes op), not a new wire op.
kill--via anActor stop----Terminate an actor; MCP can evaluate the same send

Module Operations

REPL opCLI subcommandREPL meta-commandMCP toolLSP capabilityNotes
unload--:unload <class>unload--Unload a class from the workspace
define-class--(via eval: Object subclass: … or Object classBuilder … register)(via evaluate)--Define/register a class. Both the grammar form and the programmatic ClassBuilder cascade (ADR 0038 / ADR 0084) flow through the shared eval/evaluate surface, so any surface that can evaluate an expression can create a class — no dedicated op. register returns the canonical class object (BT-2258).
live-edit-method--(via eval: Class >> sel => body / Class class >> sel => body, or Class compile: #sel source: body)save_method (durable; ephemeral counterpart try_method is listed in MCP-Only Tools)--Live-patch an instance- or class-side method on a registered class (ADR 0066 / ADR 0082 Phase 1 / ADR 0084 / Phase 3 BT-2288). The >> patcher form desugars to compile:source: (durable); tryCompile:source: is the ephemeral variant; both take the body as a String value. Every successful in-memory patch emits a ChangeLog entry. MCP save_methodaClass compile: #sel source: body (durable); MCP try_methodaClass tryCompile: #sel source: body (ephemeral). Both are typed front-ends that build the Beamtalk expression and submit via evaluate — no dedicated workspace-side op. The class-side path recompiles the class's recorded source. compile:source: / tryCompile:source:, MCP save_method/try_method, and the LiveView IDE save all share one install chokepoint (beamtalk_repl_eval:compile_methodbeamtalk_repl_loader:install_method → the backend compile_method port op, BT-2553 follow-up): the method body is parsed standalone and merged into the class — no textual Class >> wrap, no header-sniffing — so the stored source round-trips byte-for-byte (leading ////// comments preserved, idempotent across repeated saves). The REPL-typed Class >> sel => body form instead parses as a standalone method definition and installs via reload_method_definitionrecompile_with_method (the class-recompile path), using the source the user typed verbatim. Both paths preserve the patched class's package-qualified module name + on-disk source attribution, so a patched project class stays flushable/revertable. (Class-side Class class >> sel => body is only reachable through the REPL/recompile_with_method path today; the compile:source:/save_method chokepoint installs instance-side methods.) The LiveView IDE method editor reaches this op from two surface-specific UI entry points that both post (class, selector, source) to the same :save: an existing method opened from the System Browser / omnibar / nav popover, and a brand-new method opened via the System Browser's Owner-only "new method" entry (a blank :method tab whose selector the author supplies in-line, since no selector exists yet to name in the breadcrumb). The editor opens with no tab — authoring is always an explicit open, never a default-seeded tab.
new-class--(via eval: Workspace newClass: source at: path)save_classexecuteCommand: beamtalk.saveClass (BT-2289)Create a brand-new class from a source String at a target path (ADR 0082 Phase 1, MCP wiring BT-2288, LSP wiring BT-2289). MCP save_classWorkspace newClass: source at: path. Compiles and installs the class in memory and logs a durable kind: "new-class" ChangeLog entry; Phase 1 does not write the file (that is Phase 2 flush). Raises a loud, specific error (no silent fallback) when the target already exists, lies outside the project tree, the declared class name does not match the path basename, or a class of that name is already loaded. The MCP tool and the LSP beamtalk.saveClass command are both typed front-ends that build the Beamtalk expression and submit via evaluate — no dedicated workspace-side op. LSP arguments: [source, path] (positional) or [{source}, {path}] (object-wrapped). The LiveView IDE exposes this as the System Browser's New File form (ADR 0082 Phase 5, BT-2293), routing source + path through BtAttach.Workspace.new_class/2BtAttach.Facade (:new_class, :execute capability — Owner only) → beamtalk_repl_eval:new_class/2.
flush--:flush / :flush <arg>Workspace flush / Workspace flush: <arg> (arg is a Class, a Symbol kind, or #{#file => "path"})flushexecuteCommand: beamtalk.flush / executeCommand: beamtalk.flush.class / executeCommand: beamtalk.flush.file / executeCommand: beamtalk.flush.kind (BT-2289)Write the pending ChangeLog entries to disk (ADR 0082 Phase 2, MCP wiring BT-2288, LSP wiring BT-2289). MCP flush with no argument ≡ Workspace flush; the optional class / file / kind parameters (mutually exclusive) build Workspace flush: ClassName, Workspace flush: #{ #file => "path" }, or Workspace flush: #'kind'. The LSP exposes the four shapes as four separate command identifiers (beamtalk.flush, beamtalk.flush.class, beamtalk.flush.file, beamtalk.flush.kind) for clean editor-side binding. Splices each durable+flushable entry's body back into its source file via byte-span replacement (no AST reprint), atomically (<file>.tmp + atomic rename), with external-edit conflict detection. Multi-file flushes use a two-phase commit: write every <file>.tmp first, then rename each in sequence; any Phase A failure aborts the whole flush with no rename happening. Returns a FlushResult summary recording what was written (flushed, files, newClasses) and what conflicted (conflicts, with reason ∈ {external_edit, target_exists, span_out_of_range, source_file_unreadable, rename_failed}). Flushed entries stay in the audit log but drop out of the active Workspace changes view. After a flush completes, the runtime broadcasts a flush_completed push frame on the workspace channel; the LSP server consumes it and emits workspace/applyEdit for each touched file that is currently open in the editor, so open buffers refresh against the new on-disk state. The REPL :flush meta-command, the MCP flush tool, and the LSP beamtalk.flush* commands are all CLI/typed-arg-side shortcuts that construct the equivalent Beamtalk expression — no dedicated workspace-side op.
changes--:changesWorkspace changeslist_changes--Return the workspace ChangeLog — the navigable view of pending in-memory changes (ADR 0082, BT-2284, MCP wiring BT-2288). MCP list_changesWorkspace changes. Backed by beamtalk_workspace_changelog:changeLog/0 via FFI. The REPL :changes meta-command and the MCP list_changes tool are both shortcuts for the underlying message send. All pending-state queries (size, notEmpty, dirtyMethods, select:, ...) live on the returned ChangeLog object per Pharo's Smalltalk changes idiom — no dedicated workspace-side op. The default pending view (activeEntries, and thus size/do:/isEmpty/notEmpty and the LiveView Changes pane / MCP list_changes) collapses to the latest active entry per (class, selector) — repeated patches/reverts of one method (a revert is itself a patch, ADR 0082 "Undo") show as one row, matching what Workspace flush applies (shadow_duplicates) and what dirtyMethods groups (BT-2574). Shadowed (superseded) entries stay reachable for audit via allEntries / select:. The view is further filtered to entries whose installed body still differs from disk: a method reverted back to its on-disk body has no net change and drops out entirely ("disappear when clean"), and each remaining entry carries a net on-disk→in-memory unified diff (ChangeEntry diff / isClean) computed once in the runtime so every surface renders identical text. Workspace changes thus reads as "what differs from disk", not "everything touched this session" — the full history stays in allEntries / select: (BT-2575).
dirty--:dirtyWorkspace changes dirtyMethodsdirty_methods--Per-class set of dirty selectors (ADR 0082, BT-2284, MCP wiring BT-2288). MCP dirty_methodsWorkspace changes dirtyMethods. The REPL :dirty meta-command and the MCP dirty_methods tool are both shortcuts that compose from the existing changes primitive — no dedicated workspace-side op. Pair with list_changes for the full ChangeLog summary or with flush to write the durable entries to disk. Returns a Dictionary of Class -> {selectors}.
revert--(via eval: Workspace changes revert: anEntry)(via evaluate)(via executeCommand: evaluate)Undo a single pending in-memory change (ADR 0082 Phase 4, BT-2290; completeness BT-2663/BT-2664/BT-2665). For a method modify it re-installs the recorded prior body (and emits a fresh durable ChangeEntry — the original entry is preserved in the audit log); for an add (a method whose selector did not exist pre-patch) it removes the just-added method; for a new-class entry it removes the just-created class. Both instance-side and class-side method reverts work symmetrically — the entry's kind carries the side. Lives on the ChangeLog object (per Pharo's Smalltalk changes idiom) and is reached through the existing evaluate pathway on every surface; no dedicated workspace-side op. Raises a structured error for a modify whose prior body is genuinely unrecoverable (recorded body gone and on-disk span no longer resolves — never silently deletes a method that existed before) and for entries with no active log row. The LiveView IDE exposes this as a per-entry revert button in the ChangeLog viewer (ADR 0082 Phase 5, BT-2293), routing (class, selector) through BtAttach.Workspace.revert/2BtAttach.Facade (:revert, :execute capability — Owner only) → the clean-returning beamtalk_workspace_interface_primitives:revert_method/2 wrapper over the FFI changeLogRevert/1. New-class rows carry no selector, so the button sends the new-class placeholder selector, which the workspace maps back to the class's new-class entry.
clear (changes)--(via eval: Workspace changes clear)(via evaluate)(via executeCommand: evaluate)Discard every pending ChangeLog entry without writing to disk (ADR 0082 Phase 4, BT-2290). Memory still holds the latest patched method versions until the next workspace restart, when disk wins — matching the ADR's "clear discards the ChangeLog without writing" contract. Idempotent. Distinct from the clear REPL op for session locals (Session Operations row), which clears REPL bindings — Workspace changes clear operates on the ChangeLog.
flush-kinds--(via eval: Workspace changes flushKinds: kinds)(via evaluate)(via executeCommand: evaluate)Flush only the ChangeEntries whose kind or author_kind is in kinds (ADR 0082 Phase 4, BT-2290). Accepts a Set or List of Symbols — entry kinds (#instance, #class, #'new-class') and/or author kinds (#human, #agent). When both dimensions are present, an entry must satisfy both (e.g. #{#agent, #'new-class'} flushes only agent-authored new-class entries). Unknown symbols and empty sets are rejected with a structured error. Returns the same FlushResult shape as Workspace flush. Lives on the ChangeLog object; no dedicated workspace-side op.
autoflush--(via eval: Workspace autoflush / Workspace autoflush: true)(via evaluate)(via executeCommand: evaluate)Workspace setting that, when enabled, causes every successful durable in-memory patch to immediately trigger Workspace flush (ADR 0082 Phase 4, BT-2290). Default false. Setting persists across workspace restarts via metadata.json. Best-effort: a flush failure (external-edit conflict, write error) leaves the entry pending in the log; the BEAM module install is not rolled back because live actors may hold references to the new closures (per the ADR's autoflush failure semantics). Ephemeral patches via tryCompile:source: are never autoflushed. The LiveView cockpit additionally reads this flag through a surface-specific :read facade op (:autoflushBtAttach.Workspace.autoflush/0beamtalk_workspace_meta:get_setting(autoflush, false), BT-2590) to decide whether a per-method/new-class save should refresh its Git panel: with autoflush off the save touches the image only (the on-disk working tree is unchanged), so the redundant git shell-out is skipped. This op only reads the same setting already exposed above on every surface — it adds no new cross-surface operation.

Git panel (cockpit post-flush VCS surface, ADR 0082 Amendment 1 / BT-2586)

The git ops are surface-specific to the LiveView cockpit — the human-facing post-flush VCS surface (disk↔HEAD), the layer below the ChangeLog (memory↔disk) rows above. They are intentionally not exposed on the REPL, MCP, or LSP surfaces: CLI/terminal users use git directly (the .bt files already are the git working tree, ADR 0004), and the runtime-side ChangeLog (changes/revert/flush) remains the agent/MCP-facing layer (ADR 0082 Amendment 1 / BT-2585 surface split). The wrapper is a shell-out to system git parsed into typed Erlang maps (runtime/apps/beamtalk_workspace/src/beamtalk_git.erl), not a libgit2 NIF; read ops parse --porcelain/--format, mutating ops invoke system git so hooks/signing/credentials/config apply. All ops route LiveView → BtAttach.Workspace.git_*BtAttach.Facadebeamtalk_git on the workspace node. Not-a-repo / git-absent degrades to a structured error (the panel never crashes the node). The panel's status+log load runs off-socket via start_async (BT-2590): each git op crosses the node boundary into the workspace's collect/5 loop bounded by GIT_TIMEOUT_MS = 30_000, so a hung git could otherwise leave the LiveView socket unresponsive for ~60s; running both reads in one async task (resolved in handle_async(:git_load, …)) keeps the socket responsive to other events while git runs.

REPL opCLI subcommandREPL meta-commandMCP toolLSP capabilityNotes
--surface-specific: use git directly----surface-specific: LiveView cockpit Git panelgit_status — working-tree status (branch/upstream/ahead/behind + per-file index/worktree state) parsed from git status --porcelain=v2 -b -z. Facade :git_status, :read capability — Observer-visible.
--surface-specific: use git directly----surface-specific: LiveView cockpit Git panelgit_diff — per-file unified diff (working-tree-vs-HEAD + staged-vs-HEAD), passed through verbatim. Facade :git_diff, :read capability — Observer-visible.
--surface-specific: use git directly----surface-specific: LiveView cockpit Git panelgit_log — last N commits (sha/short_sha/subject/author/ts) parsed from git log --format=.... Facade :git_log, :read capability — Observer-visible.
--surface-specific: use git directly----surface-specific: LiveView cockpit Git panelgit_stage / git_unstage — stage (git add) / unstage (git restore --staged) a path. Facade :git_stage / :git_unstage, :execute capability — Owner-gated.
--surface-specific: use git directly----surface-specific: LiveView cockpit Git panelgit_commit — commit the staged index (git commit -m); system git applies hooks/signing/config. Facade :git_commit, :execute capability — Owner-gated.
--surface-specific: use git directly----surface-specific: LiveView cockpit Git panelgit_revert_file — discard a working-tree change (git restore -- <path>), the human counterpart to the agent ChangeLog revert:. Facade :git_revert_file, :execute capability — Owner-gated. BT-2598: a revert is a content-mutating git op, so the cockpit does more than refresh the git panel. (1) Pending-edit guard: if the target path has unflushed in-memory ChangeLog edits, the revert is blocked with a warning (flush or discard first) so live work is never silently lost. (2) Image reload: on a clean revert the affected module is reloaded from disk into the live image (:reloadbeamtalk_repl_eval:reload_file/1) so image == disk (git-first / disk-as-source-of-truth, BT-2585). (3) Window refresh: the reload's ClassLoaded push (the cockpit now subscribes to the classes stream at mount via :subscribe_classes) plus a synchronous re-pull refresh open method-editor / definition / Changes panes + the System Browser to the reverted content, with no manual refresh. The classes subscription also keeps open windows fresh on any source change — another session's flush, an external edit reloaded into the image.
--------surface-specific: LiveView cockpitreload (Facade :reload, :execute) / subscribe_classes (Facade :subscribe_classes, :read) — BT-2598 cockpit-internal plumbing for the revert refresh above. :reload re-installs a .bt file from disk into the live image (beamtalk_repl_eval:reload_file/1); :subscribe_classes registers the LiveView pid on the runtime classes push stream (beamtalk_repl_subscriptions:subscribe(:classes, pid)) so ClassLoaded/ClassRemoved drive open-window refreshes. Not exposed on other surfaces (CLI users reload via git + the REPL Workspace load: / sync).
REPL opCLI subcommandREPL meta-commandMCP toolLSP capabilityNotes
nav-query--(via eval: SystemNavigation default {senders,implementors,references}…)(via evaluate / LiveView IDE)textDocument/references / textDocument/implementation / textDocument/prepareCallHierarchy / callHierarchy/incomingCalls / callHierarchy/outgoingCalls / textDocument/prepareTypeHierarchy / typeHierarchy/supertypes / typeHierarchy/subtypesStructured navigation query channel for runtime-attached LSP / MCP (BT-2239). Returns typed beamtalk_xref site records as JSON (avoiding the inspect-string round-trip of eval-based queries). The op is wired and answered today; LSP nav features call it via Backend::delegate_nav_query when initializationOptions.delegateToRuntime is on and a workspace is running, falling back to the in-process AST walker otherwise. The Beamtalk SystemNavigation default sendersOf: / implementorsOf: / referencesTo: selectors remain the human-facing surface — nav-query is the typed channel for tooling. Per-method LSP children flip individual queries over to the runtime path one at a time and wire their own capabilities: BT-2240 closed the declaration-merge gap in textDocument/references — when context.includeDeclaration = true the LSP overlays method-definition headers (selector cursor: via runtime implementorsOf → AST fallback find_selector_declarations) and class-declaration name spans (class cursor: AST find_class_declarations) onto the runtime senders/references result; when false, declarations are stripped from the cold-file fallback so both paths obey the flag identically. BT-2241 ships textDocument/implementation (selector → implementorsOf: → method-header locations, instance- and class-side both reported). BT-2243 adds textDocument/prepareCallHierarchy + callHierarchy/{incomingCalls,outgoingCalls} (prepare lives under textDocument/ per the LSP spec because it takes a text-document position; incoming routes through nav-query senders; outgoing walks the method body's AST in-process via SystemNavigation messagesSentBy:'s shared all_sends_query). BT-2242 adds textDocument/prepareTypeHierarchy + typeHierarchy/{supertypes,subtypes} via Behaviour superclassChain / allSubclasses. Type hierarchy stays cold-file (the nav-query wire shape is deliberately locked to senders/implementors/references; the ClassHierarchy index already covers stdlib + user code and matches the runtime answer on the indexed corpus). The Cockpit Phase 3 method-editor Senders/Implementors popovers (BT-2495) consume this op over distribution via BtAttach.Workspace.senders_of/1 / implementors_of/1BtAttach.Facade (:read capability — Observer may navigate), listing the call/definition sites for the active method's selector.
nav-query callers_of_native_module--(via eval: SystemNavigation-style xref over (Erlang <module>) sends)(via evaluate / LiveView IDE)surface-specific: LiveView IDE native-module viewer → "Callers" affordanceThe reverse of "go to native source" (BT-2669): given a native (Erlang) module, the Beamtalk class>>selector sites that call into it. A nav-query kind backed by the maintained beamtalk_xref index — the compiler's find_all_sends_in_source tags each erlang_ffi send with its resolved target_module, which beamtalk_xref stores on the send site so beamtalk_xref:callers_of_native_module/1 can answer the reverse query for explicit (Erlang <module>) … FFI sends (previously dropped from the selector index). BT-2732 extends the answer with ADR 0056 self delegate callers: a native: class's delegating methods route into its backing module via generated dispatch (beamtalk_actor:sync_send/3), not erlang_ffi sends, so the xref index alone misses the most common native module in a project — the gen_server that backs an actor class. The nav op now merges beamtalk_repl_ops_browse:delegate_callers_of_native_module/1 (which walks the loaded classes, keeps those whose backing native module matches, and surfaces each class's self delegate instance methods, keyed off the facade's dispatch_<selector> exports) with the FFI callers, de-duplicated by calling method. Takes module = the native module name and returns the same "sites" row shape as senders/implementors (so the IDE reuses the BT-2495 popover renderer; each row — FFI or delegate — opens the calling Beamtalk method). The LiveView IDE consumes it over distribution via BtAttach.Workspace.callers_of_native_module/1BtAttach.Facade (:read capability — Observer may navigate, exactly like senders/implementors). The native-module editor tab (BT-2667) exposes a Callers button that opens the popover; a module with neither FFI callers nor delegating classes shows the quiet empty state. No LSP request consumes this kind today (it is wired and answered for the structured channel + IDE).
(go-to-definition: composes nav-query implementors + browse-class-definition)--(via eval: SystemNavigation default implementorsOf: / class reflection)(via evaluate / LiveView IDE)textDocument/definition (see note — different engine)Ctrl/Cmd-click go-to-definition in the LiveView IDE editors (BT-2666). Holding the platform modifier (Cmd on macOS, Ctrl elsewhere) and clicking a symbol jumps to its definition, an LSP-style affordance. The CodeMirror cm_goto.js extension detects the modifier-click, identifies the clicked token, and fires the goto_definition LiveView event with the bare token + the code line-prefix (Receiver selector, the same shape the hover op consumes). The server resolves against the live image, mirroring the LSP definition_provider.rs resolution order: a known class name → its :def definition tab (open_definition + browser nav, reusing the nav_open_class path); otherwise a selector send → its implementor(s) via the BT-2495 nav-query implementors kind — one implementor opens that method tab directly (the nav_open open path), several open the shared Senders/Implementors popover so the user disambiguates, none flashes a brief "No definition found." no-op. A keyword selector is recovered from the line-prefix (dict at: k put: vat:put:). The affordance is editable-pane only — read-only docs/native viewers are unaffected, matching the AC — and modifier-hover underlines the token under the pointer. No new workspace op: it composes the existing :implementors facade op (:read capability — Observer may navigate) with :browse_class_definition, so it is surface-specific UX routing over already-cross-surface ops. Deliberate divergence from the LSP textDocument/definition: the LSP path (definition_provider.rs) is static — it resolves locals + class/protocol declarations from on-disk ASTs (ProjectIndex) — whereas the cockpit resolves class names + selector implementors against the running image, so it sees REPL-defined classes and live-patched methods. Locals/params (the LSP's first resolution tier) are left to a future client-side resolve; the core class + selector navigation is the deliverable.
nav-query required_methods / conforming_classes--(via eval: aProtocol requiredMethods / aProtocol conformingClasses)(via evaluate / LiveView IDE)surface-specific: LiveView IDE System Browser → protocol definition tabThe protocol equivalent of Senders/Implementors (BT-2639), the System Browser's protocol-definition action row. Two new nav-query kinds backed by beamtalk_protocol_registry: required_methods (a protocol's contract selectors — class-side requirements carry class_side=true, the bare selector recovered from the class prefix) and conforming_classes (the classes that structurally conform, BT-1611). Both take class = the protocol name and return the same "sites" row shape as senders/implementors (so the IDE reuses the popover renderer). The Cockpit consumes them over distribution via BtAttach.Workspace.required_methods_of/1 / conforming_classes_of/1BtAttach.Facade (:read capability — Observer may navigate, exactly like senders/implementors). In the popover a Required methods row is navigable to its Implementors (reusing the BT-2495 nav path); a Conforming classes row opens that class's definition pane. The human-facing surface is aProtocol requiredMethods / aProtocol conformingClasses — the protocol class object's class-side methods (ADR 0068), reachable through the shared eval/evaluate pathway on every surface — nav-query is the typed channel for tooling, mirroring how SystemNavigation's selectors relate to nav-query senders/implementors. The is_protocol boolean on the browse-class-definition row (op 4) lets the IDE detect a protocol by runtime reflection (not a header string-sniff) to gate the action row. The LSP does not consume the protocol kinds today (no protocol-navigation LSP request exists); they are wired and answered for the structured channel.
(via eval: AnnouncementNavigation default {subscribersOf:,announcedClasses,subscriptions} / anAnnouncer subscribersOf:)--(via eval)(via evaluate)(via executeCommand: evaluate)Live subscription-graph introspection for the Announcements bus (ADR 0093 §7, BT-2444) — the third navigation sibling alongside SystemNavigation (static classes, nav-query) and ProcessNavigation (live supervision tree, processes). Two levels, both object-side message-sends reachable through the shared eval/evaluate pathway on every surface — no dedicated op. Object-knows-itself: a live Announcer reads its own rows (subscriptions, subscribersOf:, subscriptionCount). Navigator-discovers-system: AnnouncementNavigation default (the system bus) / of: anAnnouncer answers subscribersOf: / announcedClasses / subscriptions, returning read-only SubscriptionNode value snapshots (announcementClass, announcer, subscriber, handlerKind, once) — surface-consistent by construction, like the other two navigators' human-facing selectors. Each Announcer new is an independent dispatcher with its own subscription namespace (BT-2454), so AnnouncementNavigation of: anAnnouncer reports only that announcer's subscriptions. The publisher-side static dual is SystemNavigation default announcementsSentBy: aClass (BT-2475) — a derived static analysis of announce: call sites (which Announcement subclasses a class emits), with a site-level announcementSitesSentBy: form. Like the subscription queries it is an object-side message-send reachable through the shared eval/evaluate pathway on every surface (no dedicated op), so it is surface-consistent by construction; it is advisory (constructor-call arguments resolve, Dynamic/indirect arguments are skipped).
browse-classes--(via eval: Beamtalk allClasses + per-class introspection)(via evaluate / LiveView IDE)surface-specific: LiveView IDE System Browser class treeSystem Browser data source op 1 (ADR 0096, BT-2488). Returns one ClassRow per class — name, superclass, category, first-line comment, sealed/typed/abstract/internal, source_file, origin (both/static/runtime), source_origin (project/stdlib/dependency, BT-2552; classification split from package by BT-2643), package (package name for all origins, BT-2643), and is_test (true for a loaded TestCase subclass, BT-2557). Term-returning ({value, [ClassRow]}, BT-2399); the LiveView IDE consumes the term over distribution via BtAttach.Workspace.browse_classes/0BtAttach.Facade (:read capability — Observer may browse). The cold-mode static peer is the LSP documentSymbol tree (nav-symbols); the browse op adds the live image + category/origin divergence tagging the editor tree does not carry. The LiveView IDE class tree adds two surface-specific view affordances over these fields (BT-2557): a synthetic "Tests" category bucket grouping every is_test class, and a source-origin filter (All / Project / Deps / Stdlib) over source_origin so a project's own classes aren't buried under the stdlib. The filter narrows the interactive class set across the Hierarchy (matching classes interactive, connecting superclass ancestors kept as dimmed context rows, BT-2649) and Category views; BT-2661 defaults it to Project on first open (falling back to All when the workspace has no project-origin classes), so the tree opens scoped to the project's own code — any in-session pick wins over the default.
browse-protocols--(via eval: SystemNavigation + beamtalk_xref:defined_selectors/2)(via LiveView IDE)surface-specific: LiveView IDE System Browser protocol/selector panesSystem Browser data source op 2 (ADR 0096, BT-2488). Given {class, side} returns selectors grouped by protocol; each selector row carries line, source_status (xref tag verbatim — indexed/synthetic/unindexed_runtime_fun), origin, and (BT-2735) signature/doc for a VS Code-style method-row hover (the IDE renders the signature over the doc's first line, falling back to the bare selector when both null). To bound cost on the browse hot path, signature/doc are resolved only for synthetic rows — the compiler-derived methods whose intent a bare selector conveys least (a subclass's derived spawn inherits Actor's curated doc), via the same hierarchy walk help: uses (beamtalk_repl_docs:method_doc_signature_resolved/3) — so hand-written rows stay null (no unbounded per-method CompiledMethod read per browse). The protocol bucket is decided most-authoritative-first (BT-2506): a declared category (pragma/xref category field, when present) → the synthetic source fact (source_status = synthetic — compiler-generated value-type accessors → accessing; compiler-injected actor class-side constructors new/new:/spawn/spawn:instance creation, BT-2614) → the xref-extension source fact (extension provenance → extensions) → a Pharo-convention selector-name heuristic (is*/has*testing, as*/to*converting, print*printing, keyword sends → operations, …); unmatched selectors fall into as yet unclassified. The two source-fact tiers (synthetic, then extension) outrank the name heuristic. Compiler-injected synthetic methods are surfaced, not hidden (BT-2614): the class-side new/new:/spawn/spawn: an actor's codegen emits as sourceless exported functions are recorded as synthetic xref rows, so the browser's method set matches runtime aClass class allMethods reflection — the canonical "methods of a class" is its source-defined methods plus its locally-injected synthetic entry points. They are read-only: browse-method-source returns source = null for a synthetic row (no editable body, no [source] jump that would 404). BT-2714: the LiveView IDE method list badges a synthetic selector with a derived tag (.derived-tag) so a compiler-derived method reads as visibly distinct from a hand-written one — pure surface-specific presentation of the source_status = synthetic fact this op already carries, no new op data. Backed by the maintained xref index (ADR 0087) for sub-millisecond panes. Term-returning ({value, ProtocolTree}); :read capability.
browse-method-source--(via eval: (aClass >> #sel) source)(via LiveView IDE)surface-specific: LiveView IDE System Browser → method editor tabSystem Browser data source op 3 (ADR 0096, BT-2488). Selecting a method in the browser opens it as an editable tab in the centre method editor seeded with this source (browsing is editing, the Smalltalk idiom — the dedicated read-only pane was retired, BT-2491 follow-up). Returns one method's image-accurate source (null for a sourceless runtime method), its doc (the /// doc-comment) and signature for the read-only documentation block the editor shows alongside the editable body (BT-2558 — null when the method carries neither; pulled from the same __doc__/__signature__ the help: aClass selector: send reads, so the browser and help: agree on the docs for a method), with source_status, origin, and disk_differs (true when the live/patched body differs from disk — an unflushed >> patch, ADR 0082; null when no static source exists). BT-2714: a synthetic row keeps source = null (no editable body — and never surfaces an inherited implementation, e.g. Actor's real spawn body, under the subclass's generated row) but now does resolve doc/signature — via the same hierarchy walk help: uses (beamtalk_repl_docs:method_doc_signature_resolved/3), so a derived spawn/new shows Actor's curated docs and a value accessor shows its generated signature instead of a blank pane. The LiveView IDE opens a synthetic row as a surface-specific read-only tab (the doc block forced open in its normal position + a "compiler-derived, no editable source" note) rather than the editable CodeMirror buffer, so a derived method can't be mistakenly edited into a blank buffer. BT-2734: a Value subclass: class's compiler-generated accessors — slot getters, with<Field>: copy-setters, and the keyword constructor — carry a compiler-derived __doc__/__signature__ baked in at codegen (e.g. withSeq: aValue -> ChangeEntry + "Compiler-derived copy-setter…"), so these synthetics show real docs uniformly across the browser, help:, and MCP docs instead of a bare selector. Term-returning ({value, MethodSource}); :read capability.
browse-class-definition--(via eval: class reflection)(via LiveView IDE)surface-specific: LiveView IDE System Browser class-definition paneSystem Browser data source op 4 (ADR 0096, BT-2488). Returns the class header definition (null for a file-less ClassBuilder class), state slots (name + default, field reflection per ADR 0035 — no user code), full comment (rendered as a formatted documentation block in the class-definition tab, BT-2558 — the same get_doc text Beamtalk help: aClass reads), origin, disk_differs. Term-returning ({value, ClassDefinition}); :read capability. The class-definition op also carries native + backing_module (BT-2578) so a native: class (ADR 0056) is badged with its backing Erlang module. BT-2605: the LiveView IDE method-editor header surfaces the class/method modifiers as colored badges — a Class badge for a class-side method (from the tab's side), plus Sealed/Typed/Abstract from the op's reflected sealed/typed/abstract booleans (added to op 4 to mirror the same reflection op 1's class tree carries — the synthesized definition skeleton has no leading modifier keywords to parse) and Native from the native flag, mirroring the apidoc badge labels (Sealed/Typed/Abstract) for docs↔IDE parity. The badges show on both class-definition tabs and method tabs of the same class. BT-2629: the typed badge reflects the already-emitted is_typed meta flag (codegen emits it into __beamtalk_meta/0; the runtime now threads it through beamtalk_object_class:is_typed/1beamtalk_runtime_api:is_typed/1 → the op's typed key, mirroring sealed/abstract line-for-line — no compiler change). The badge rendering is surface-specific presentation; the new sealed/typed/abstract keys are a small additive extension of op 4's reflected result. BT-2642: the same editor header also carries a package/origin badge for every active tab (method + class-definition), reusing BT-2641's vocabulary (STDLIB / DEP · <pkg>) and adding the bare project package name for project tabs — colored by origin and sourced from the source_origin/package fields op 1 (browse-classes) already carries (no new op data; the class tree still hides project badges, the header shows them). Pure surface-specific presentation. The browse ops are pure reflection — they run no user code (no printOn:/displayString), the ADR 0091 Decision 4 acceptance criterion that makes them Observer-grantable, asserted in beamtalk_repl_ops_browse_tests.
browse-native-source--(via eval: aClass backingModule + read the .erl)(via LiveView IDE)surface-specific: LiveView IDE System Browser → read-only native paneSystem Browser data source op 5 (BT-2578). For a native: class (ADR 0056) whose Beamtalk methods are all self delegate facades, returns its backing gen_server module's Erlang source read-only — the real logic lives in handle_call/handle_info clauses, not the facade. Carries backing_module, source_file (null when the .erl is not shipped — a .beam-only build, surfaced as a "source not available" empty state), source_origin, editable (projecttrue, else read-only; R/W editing of project-owned native/ is a follow-up), content (null = not shipped), and a best-effort handle_call clause line-map (clauses + selected_clause for an optional selector param — loose by design: e.g. Subprocess>>readLine replies from handle_info). Term-returning ({value, NativeSource}); :read capability (pure reflection over the facade's __beamtalk_meta/0 + the backing module's on-disk source). The LiveView IDE consumes it via BtAttach.Workspace.browse_native_source/2BtAttach.Facade (:browse_native_source); the class-definition tab badges native classes and toggles the read-only pane. BT-2659: the class-definition tab also exposes a direct "Open native source →" link that opens the backing module's full .erl as its own read-only :native editor tab (via browse-native-module-source / open_native_module_tab/2) — a one-click jump from Beamtalk code to its native backing source, complementing the inline BT-2578 pane (which shows only the matched clauses). surface-specific LiveView IDE presentation over the unchanged op.
browse-native-modules--(via eval: Package enumeration + per-app native_modules)(via LiveView IDE)surface-specific: LiveView IDE separate "Native" browserSystem Browser data source op 6 (BT-2648). Enumerates a loaded package's hand-written native Erlang modules so a dependency's .erl files are navigable even when no native: class backs them (the reported beamtalk-http case: a dep loaded without instantiating its classes was invisible to the class-keyed browse-classes). Filter rule (ADR 0072 native_modules): walks beamtalk_package:all/0 (every loaded OTP app that is a Beamtalk package); per package takes the generated {native_modules, [...]} app-env key (user/dep packages) or, for stdlib (no such key), the app's OTP modules list filtered to non-bt@ modules — so the auto-generated bt@{pkg}@{class} class facades are excluded by construction (they are already in browse-classes, never duplicated here). Each row carries module, source_file (compile-info source, null when no readable .erl → not openable, like browse-native-source), package, source_origin (project/dependency/stdlib), and openable. Clicking a module opens its .erl read-only via browse-native-source keyed by a module param (the BT-2648 alternative to class; class = null in the result, editable = false — standalone native modules have no Beamtalk editing seam), reusing the BT-2578 native source-view pane and its .beam-only "source not available" empty state. Term-returning ({value, [NativeModuleRow]}); :read capability (pure reflection over app env + the module's on-disk source — no user code, Observer-grantable like the other browse ops). The LiveView IDE consumes it via BtAttach.Workspace.browse_native_modules/0 / browse_native_module_source/1BtAttach.Facade (:browse_native_modules / :browse_native_module_source). **BT-2656: native modules now live in their own left-hand "Native" browser, switched in via a `Classes
browse-type-aliases--(via eval: alias resolution has no bare-selector introspection form yet)(via LiveView IDE / VS Code sidebar)surface-specific: LiveView IDE "Type Aliases" browser / VS Code Workspace Explorer sidebar sectionSystem Browser data source op 7 (ADR 0108 Phase 8, BT-2903). A type Name = ... declaration erases entirely at compile time — no BEAM module, no live process — so unlike browse-classes there is nothing to reflect on; rows come from each loaded package's compiled .app-file {type_aliases, [...]} env key (app_file.rs AliasMetadata/format_type_aliases_entry, populated by build_alias_metadata), read back the same way browse-native-modules reads native_modules. Walks beamtalk_package:all/0; each row (AliasRow: name, expansion — the RHS rendered to Beamtalk display form, doc, source_file, internal) is additionally tagged package/source_origin (project/dependency/stdlib), mirroring ClassRow/NativeModuleRow. Seeding-boundary exclusion (ADR 0108 Implementation): an internal alias is visible only from its own package — every other package's (including stdlib's) internal aliases are dropped during enumeration, before a row is ever built, mirroring how AliasRegistry::add_pre_loaded (BT-2898) never seeds a dependency's internal aliases into a consumer's alias table at compile time. Term-returning ({value, [AliasRow]}, BT-2399); :read capability (pure .app-env reflection — no user code, Observer-grantable like the other browse ops). The LiveView IDE consumes it via BtAttach.Workspace.browse_type_aliases/0BtAttach.Facade (:browse_type_aliases), rendering a third `Classes
save-native-source--(via eval: edit the .erl + Workspace sync / ClassName reload)(via LiveView IDE)surface-specific: LiveView IDE editable native (.erl) tabEdit → compile → reload → write-back for a project-owned native (.erl) module (BT-2670). The IDE makes a project native's :native editor tab (BT-2667) editable for the Owner; deps/stdlib natives stay strictly read-only, as does every non-Owner role. Takes module (the native module) + source (the edited Erlang). The op re-derives project ownership server-side via beamtalk_repl_ops_browse:native_module_editable_target/1 (the module's own compile-info source path + the source_origin classification — never a client path), rejecting deps/stdlib/outside-project targets with a structured #beamtalk_error{}. On a clean compile it reuses the workspace native build (beamtalk_repl_ops_load:compile_native_erl_files/2erlc compile + code:load_binary + mtime stamp, BT-2653 freshness) and then atomically writes the source to disk (temp file + rename); a compile error returns the same structured format_erl_compile_errors/2 maps the load path produces (rendered inline like a Beamtalk compile error) and leaves the on-disk source untouched (fail-safe: it compiles a temp .erl first). Term-returning (`{value, #{ok
nav-symbols--(via eval: Beamtalk allClasses + per-class introspection)(via evaluate / LiveView IDE)textDocument/documentSymbol / workspace/symbolBulk class+method outline channel for runtime-attached LSP / MCP (BT-2244). Sibling of nav-query — kept on a separate op so nav-query's wire shape stays locked to selector-shaped navigation (senders / implementors / references). Returns one row per loaded class with its instance- and class-side method headers, sourced from the live class registry + beamtalk_xref:defined_selectors/2 + beamtalk_xref:method_info/3. LSP document_symbol and symbol call this via Backend::delegate_nav_symbols when initializationOptions.delegateToRuntime is on and a workspace is running, falling back to the in-process AST/glob walker otherwise. The headline win: classes loaded purely at the REPL (no .bt file) and methods installed via Behaviour >> / compile:source: since the last flush appear in workspace/symbol here — the AST walker can't see either. Source-less classes attach to the workspace-root URI with a zero-width range and (no source file) in the symbol detail so editors render them visibly distinct. textDocument/documentSymbol uses scope = "user" (source-backed classes only — a URI is the natural lookup key); workspace/symbol uses scope = "all" (every loaded class, for the headline win). The Cockpit Phase 3 top-bar omni search (BT-2495) consumes this op over distribution via BtAttach.Workspace.symbol_index/1 (scope all) → BtAttach.Facade (:read capability — Observer may search), filtering the class + selector index for the results popover.

Test Operations

REPL opCLI subcommandREPL meta-commandMCP toolLSP capabilityNotes
testtest:test / :ttest--Run BUnit tests (single class or file). The LiveView IDE (Attach topology, BT-2557) is a further consumer: its Tests dock pane runs a selected class through the term-returning seam (beamtalk_repl_ops:dispatch/4BtAttach.Workspace.run_tests/1 → facade :run_tests, capability :execute — tests evaluate code, so Owner-only, the same gate as eval) and renders per-case pass/fail with failure detail. The REPL :test / :t meta-command now routes to this pane instead of pointing at the CLI.
test-all--via Workspace test----Run all loaded tests; MCP test covers the all-tests case via empty params. The LiveView IDE Tests pane's "Run all" button (BT-2557) drives this op (facade :run_tests with no class).
list-tests--(implicit: the Tests pane lists them)----Discover loaded TestCase subclasses + their selectors for the LiveView IDE Tests pane (Attach topology, BT-2557). Pure reflection over the live class registry (beamtalk_test_runner:discover_tests/0) — runs no test code — so it is :read (the Observer may browse the catalogue, but cannot run, which is :execute). Routed through the term seam (beamtalk_repl_ops:dispatch/4BtAttach.Workspace.list_tests/0 → facade :list_tests) and returned as a {value, _} term.
load-tests--(implicit: the Tests pane's "Load tests" button)----Load the project's test/ .bt files into the live image for the LiveView IDE Tests pane (Attach topology, BT-2557). Plain load-project defaults to include_tests=false, so a freshly-opened project loads only src/ and the Tests catalogue (and the System Browser's "Tests" group) start empty — this op closes that gap. Delegates to the shared beamtalk_repl_ops_load:sync_project/2 with include_tests=true (scoped to the workspace cwd). With include_tests=true the path also compiles + code-paths the package's native/test/ helper modules so a .bt test can drive them via (Erlang <helper>) <msg>, and regenerates a fresh beamtalk_classes.hrl before native compilation — mirroring how the beamtalk test CLI builds native modules into _build/dev/native/ebin/ (BT-2653); without this the LiveView test-load diverged from the green CLI run with does not understand and stale spec for undefined function cascades. Compiles + loads user test code (mutating the image), so it is :execute (Owner-only, the same gate as run_tests/eval). Routed through the term seam (beamtalk_repl_ops:dispatch/4BtAttach.Workspace.load_tests/0 → facade :load_tests) and returned as a {value, _} term carrying classes/errors/summary.

Dev / Introspection Operations

REPL opCLI subcommandREPL meta-commandMCP toolLSP capabilityNotes
methods--via aClass methods----List methods for a class; reachable on any Behaviour
list-classes--via Workspace classeslist_classes--List available classes. workspace/symbol was the historical LSP binding (BT-2081); BT-2244 moved the LSP source of truth to the nav-symbols op so the editor sees REPL-loaded and live-edited classes that list-classes does not enumerate as outline children. list_classes MCP still answers via this op.
erlang-help--surface-specific: REPL completion helper----Erlang module documentation; MCP coverage tracked in BT-1903
erlang-complete--surface-specific: REPL completion helper----Erlang module/function completion

Performance / Tracing Operations

REPL opCLI subcommandREPL meta-commandMCP toolLSP capabilityNotes
enable-tracing--surface-specific: agent-only workflow (BT-1606)enable_tracing--Enable actor trace capture
disable-tracing--surface-specific: agent-only workflow (BT-1606)disable_tracing--Disable actor trace capture
get-traces--surface-specific: agent-only workflow (BT-1606)get_traces--Retrieve captured traces
actor-stats--surface-specific: agent-only workflow (BT-1606)actor_stats--Per-actor/method aggregate stats
export-traces--surface-specific: agent-only workflow (BT-1606)export_traces--Export traces to JSON file
pid-stats--surface-specific: LiveView IDE Inspector(via LiveView IDE)--Live process metrics (queue depth, memory, reductions, status, current function) for one actor pid — the read companion to the live-Inspector per-object change stream (ADR 0095 §5, BT-2489). Read-only process_info reflection (the :observer/LiveDashboard primitive set), guarded so a dead pid degrades to status: dead. The Cockpit Inspector pane re-issues it on each {object_changed, …} push (or a refresh timer).

Server Operations

REPL opCLI subcommandREPL meta-commandMCP toolLSP capabilityNotes
describe--surface-specific: transport handshakedescribe--Capability discovery (list supported ops)
health--surface-specific: operator probe----Workspace health probe
shutdownworkspace stopsurface-specific: lifecycle command----Graceful workspace shutdown

CLI-Only Commands (no REPL op equivalent)

These CLI subcommands are build/tooling commands that operate offline (no workspace needed) and have no corresponding REPL op.

CLI subcommandMCP toolLSP capabilityNotes
build----surface-specific: offline compiler, no workspace. The --escript packaging mode (ADR 0099 §4, BT-2689) — beamtalk build --escript --entry "ClassName selector" -o <name> — is CLI-specific: it bundles the project bt@*.beam + the beamtalk_runtime/beamtalk_stdlib/beamtalk_workspace (+ deps) beams + a generated main/1 bootstrap into a single executable escript. The bootstrap reuses ADR 0061's run-mode lifecycle (workspace repl = false, topo-ordered class registration, node_owning/program_name seeded) so the packaged Console/Program/System behaviour matches beamtalk run. On Windows a .cmd launcher is emitted alongside (ADR 0027). BT-2920: beamtalk build now sets CompilerOptions.current_package (and stamps cross-file ClassInfos with the package via ClassHierarchy::stamp_package_on_infos) so check_class_visibility/check_alias_leaked_visibility (E0401/E0402/E0403) fire at build time — previously only the LSP's ProjectIndex-backed path ever set current_package, so a project with zero LSP-detected problems could still build and ship code that violated its own declared internal boundaries. See the lint row below and BT-2921 for the MCP counterpart.
build-stdlib----surface-specific: internal stdlib build step
run----surface-specific: script/service runner. The script-mode entry-argument contract (ADR 0099 §2, BT-2686) is CLI-specific: beamtalk run ClassName selector accepts either a unary selector (run, no args) or a single arity-1 keyword selector (main:) whose post-selector tokens are delivered to ClassName>>main: as one List(String). Bare words need no --; -- shields flag-shaped tokens (-- --verbose). Multi-keyword sends (move:to:) stay rejected (the validator amends ADR 0061). The Console and Program stdlib classes themselves are not CLI-specific — they are runtime stdlib reachable via eval/evaluate on every surface (like System/OS); only the run … <args> invocation form is.
new----surface-specific: project scaffolding
clean----surface-specific: removes build artifacts (offline, no workspace). Removes generated output only — never source. Default removes the project's _build/<profile>/ output (compiled BEAM, generated beamtalk_classes.hrl, native ebin/include) and the type cache; --deps also clears dependency artifacts (_build/deps/); --all removes the whole _build/; --dry-run lists without deleting. All paths resolve through BuildLayout (BT-2672), so src/, native/ source, and beamtalk.toml are never touched.
repl----surface-specific: starts the REPL client
check--textDocument/publishDiagnosticsOffline syntax/type check; LSP provides diagnostics on save. BT-2800 (ADR 0100 Rule 3): both beamtalk build/check and the LSP apply the package's beamtalk.toml [diagnostics] per-category severity-override table through the same shared pipeline (compute_project_diagnosticsapply_diagnostics_table, beamtalk-core's compilation::diagnostics_policy module) — a package that sets dnu = "error" fails the CLI build and shows the same site as an Error in the editor, closing what was previously a surface-parity gap (a beamtalk-lsp → beamtalk-cli dependency was avoided by moving the table parser + apply_diagnostics_table into beamtalk-core, which both crates already depend on). The LSP loads each workspace root's table once at startup (Backend::load_diagnostics_table, alongside the ADR 0075 type-cache load) — editing beamtalk.toml while the server is running requires a restart to pick up the change. BT-2839 extended this to the REPL, closing a third instance of the same gap: the REPL's diagnostics entry point (compute_diagnostics_with_known_vars_and_classes, called from beamtalk-compiler-port's compile_expression/compile/diagnostics request handlers) now applies the same table too, so dnu = "error" fails beamtalk build, shows Error in the LSP, and shows Error at the REPL — never a REPL-only soft Hint. The compiler-port process caches the table once per session, read from beamtalk.toml in its working directory (which the parent BEAM node pins to the project root at REPL startup) — mirroring the LSP's load-once-at-startup semantics; like the LSP, a beamtalk.toml edit requires restarting the REPL session to take effect. BT-2859: the LSP's Backend::load_type_cache now calls beamtalk_core::ffi_type_specs::extract_type_specs directly — the same single source of truth beamtalk build/beamtalk lint and the MCP lint/diagnostic_summary tools (BT-2858) already share — instead of only reading _build/type_cache/ JSON files. On a workspace opened before any beamtalk build has run, it now live-extracts from OTP/dependency .beam files (same tiered cache the other surfaces use, so a subsequent build/lint reads it back rather than re-extracting) instead of leaving NativeTypeRegistry empty for the rest of the session. The extraction logic itself moved from beamtalk-cli's binary crate into beamtalk-core (a new ffi_type_specs module) specifically so the LSP could reach it without a beamtalk-lsp → beamtalk-cli dependency (forbidden — same rationale as the [diagnostics] table move for BT-2800/ADR 0100 Rule 3, above).
fmt--textDocument/formattingFormat source files; LSP provides document formatting
fmt-check----surface-specific: CI formatting check
lintlint--Lint checks; MCP exposes for AI-assisted workflows. BT-2823: both resolve dependency (beamtalk.toml) class metadata into the same ClassHierarchy so Unresolved class diagnostics match for classes defined only in a git/path dependency. Deliberate divergence: the CLI's resolve_dep_class_infos will fetch a missing/stale git dependency over the network (ensure_deps_resolved); the MCP tool stays fully offline and only reads whatever dependency checkout already exists under _build/deps/<name>/ (as left by a prior beamtalk build) — an unfetched dependency's classes are silently skipped rather than triggering a fetch. BT-2851: the CLI's beamtalk lint now populates its Erlang FFI native-type registry via the same extract_type_specs extractor beamtalk build/beamtalk test call (falling back to live .beam extraction when _build/type_cache/ is absent or stale, rather than only reading whatever the cache holds), so @expect type on an FFI argument-type mismatch is never stale on one surface and legitimate on the other. BT-2858: the MCP lint/diagnostic_summary tools' analysis path (run_module_analysis in crates/beamtalk-mcp/src/server.rs) now builds and passes the same registry (build_native_type_registry, which calls beamtalk_cli::native_type_specs::extract_project_type_specs — the extraction logic itself moved from the CLI binary's beam_compiler module into the shared beamtalk_cli library crate so both surfaces call the exact same code, not just an equivalent copy), closing the last MCP/CLI divergence on FFI argument-type checks and return-type inference. BT-2921: run_module_analysis now also accepts current_package and sets it on CompilerOptions, and both MCP call sites (compute_diagnostic_summary's and the lint tool's Pass 1 loops) resolve the package name from the target's beamtalk.toml (resolve_current_package, mirroring beamtalk lint's find_manifest_full resolution) and call stamp_package_on_infos on each file's freshly-extracted ClassInfos before merging — mirroring BT-2920's CLI fix exactly. E0401/E0402/E0403 visibility checks now fire identically across CLI build, CLI lint, MCP lint/diagnostic_summary, and the LSP; previously MCP was the last surface silently reporting zero visibility diagnostics.
test-script----surface-specific: btscript expression tests (CI)
test-docs----surface-specific: doctest runner (CI)
doctor----surface-specific: environment health check
deps add----surface-specific: package management
deps list----surface-specific: package management
deps update----surface-specific: package management
generate native----surface-specific: code generation
generate stubs----surface-specific: code generation
doc----surface-specific: HTML documentation generator
type-coverage----surface-specific: type inference coverage stats. BT-2867: now builds and passes a NativeTypeRegistry the same way beamtalk lint does (find_package_rootBuildLayoutextract_type_specs, falling back to extract_stdlib_type_specs() for a manifest-less tree such as stdlib/src), so calls to well-specced Erlang FFI functions — and expressions downstream of them — are no longer misreported as Dynamic(UntypedFfi). Before the fix, type-coverage's parse+infer-only path never built a registry at all; beamtalk build/beamtalk lint diagnostics already used the correct registry-threaded path and were unaffected. The same gap existed in the LSP's hover_provider.rs/signature_help_provider.rs/completion_provider.rs: each already received a live native_types registry for its own FFI-call-site special case, but never threaded it into the shared enrich_hierarchy_with_inferred_returns/infer_types call that builds the general TypeMap — so hovering the FFI call site itself showed the right type, but hovering anything built from its result still showed Dynamic. Both surfaces now share one registry-consumption path.
workspace list----surface-specific: workspace management
workspace stop----Maps to shutdown REPL op (see Server Operations)
workspace status----surface-specific: workspace management
workspace attach----surface-specific: attaches REPL to existing workspace
workspace transcript----surface-specific: streams Transcript output
workspace logs----surface-specific: workspace log viewer
workspace create----surface-specific: workspace management

MCP-Only Tools (no REPL op equivalent)

These MCP tools provide AI-assistant-specific capabilities that have no direct REPL op.

MCP toolNotes
diagnostic_summarysurface-specific: AI-facing diagnostic overview for a file/package. Shares the same offline, best-effort dependency-class resolution as MCP lint (BT-2823) — see that row's note.
search_examplessurface-specific: offline corpus search for code examples
search_classessurface-specific: offline class discovery by keyword
list_packagessurface-specific: list loaded Beamtalk packages
package_classessurface-specific: list classes in a named package
docsWraps Beamtalk help: ClassName (optionally selector: #sel for instance- or class-side methods) — REPL docs op was hard-removed (BT-2091). BT-2902 (ADR 0108 Phase 8): when ClassName names a type Name = ... alias declared earlier in this session, the eval layer (beamtalk_repl_eval:maybe_help_for_alias/2) answers from the session's alias table before the expression ever reaches Beamtalk help: — aliases erase entirely at compile time, so there is no live class/process for the global beamtalk_class_registry lookup Beamtalk help: normally does to ever see. Renders type Name = <expansion>, the doc comment (if any), and Declared in: REPL. Since MCP tool calls run through the same per-session beamtalk_repl_shell/do_eval pipeline as the CLI REPL, this is automatic cross-surface parity, not a separate implementation — a session-local alias is visible identically from whichever surface opened that session. Cross-package/exported aliases (visible regardless of which session declared them) are BT-2898's seeding mechanism, not yet wired into this lookup.
load_fileWraps Workspace load: "path" — REPL load-file op was hard-removed (BT-2091)
reload_classWraps ClassName reload — REPL reload op was hard-removed (BT-2091)
try_methodEphemeral counterpart of save_method (ADR 0082 Phase 3, BT-2288). Wraps aClass tryCompile: #sel source: body — installs in memory and logs an ephemeral ChangeLog entry that does not flush. Listed here because the live-edit-method REPL op row only carries one MCP binding (save_method, the durable path); humans typing >> at the REPL reach the same install chokepoint without a separate token. Promote a successful spike by calling save_method with the same body.

LSP-Only Capabilities (no REPL op equivalent)

These LSP capabilities are editor-specific and have no direct REPL op.

LSP capabilityNotes
textDocument/hoverClass/method documentation in editor hover tooltips. Wires to Beamtalk help: ClassName (optionally selector: #sel) (BT-2081). The REPL docs op was hard-removed in BT-2091; same capability is now reached via the Beamtalk help: message-send (which walks both instance and class-side method tables) across CLI/REPL/MCP. This LSP path is purely statichover_provider.rs / Backend::hover read the on-disk .bt AST (class.doc_comment / method.doc_comment), so they cannot see REPL-defined classes, live-patched (>>) methods, or unflushed edits. The cockpit (LiveView IDE) instead answers hover from the live image via the hover REPL op (BT-2555, see Core Operations) — beamtalk_repl_docs over the term-seam — so its tooltips are live-image-accurate where the LSP's lag the running session. The divergence is intentional and is the same live-vs-static split BT-2544 made for completion. BT-2901 (ADR 0108 Phase 8): on-disk type Name = ... alias hover is now resolved on this LSP path too — SimpleLanguageService::hover threads a project-wide AliasRegistry (built from every indexed file's type declarations, ProjectIndex::alias_registry) through to hover_provider::compute_hover, so hovering an alias-typed value/annotation renders AliasName (expansion) and hovering the alias's own declaration name shows its RHS as written, both purely statically (same on-disk-AST scope as class/method hover above — no live-image awareness). The REPL/MCP :help/docs path (BT-2902) separately answers a session-local alias declared live at the prompt (see the docs row above) — the two are complementary, not divergent: one is the static on-disk view, the other the live-session view, matching the class/method split this row already documents.
textDocument/signatureHelpsurface-specific: editor parameter hints
textDocument/definitionsurface-specific: editor go-to-definition; parity-tested (BT-2081) against the user-class set surfaced by MCP list_classes — every LSP-resolved location must point inside the loaded project tree. For a native: class's self delegate method (ADR 0056), go-to-implementation redirects into the backing .erl and lands on the matching handle_call clause line — resolving the same clause the System Browser's browse-native-source jump does (BT-2582). The selector→clause-line algorithm is shared as a conformance corpus (runtime/apps/beamtalk_workspace/test/fixtures/handle_call_clause_corpus.json) that both the Rust definition_provider::clause_selector and the runtime beamtalk_repl_ops_browse:clause_selector/1 are pinned to, so the two surfaces cannot diverge on quoted vs bare atoms, multi-keyword selectors, the generic {Selector, …} / catch-all clauses (skipped), or a selector with no handle_call clause (falls back to the file top). BT-2901 (ADR 0108 Phase 8): a reference to a type alias name in annotation position, or the alias's own type Name = ... declaration, now resolves too — find_class_or_protocol_declaration falls back to a new find_alias_declaration (gated on ProjectIndex::alias_registry, since aliases are never registered into ClassHierarchy) when the name isn't a class or protocol.
textDocument/referencessurface-specific: editor find-all-references. BT-2901 (ADR 0108 Phase 8): find-references on a type alias name enumerates its declaration site plus every annotation-position use site (find_class_references now also walks module.type_aliases, both the declaration name and RHS references to other names — so `type B = A
textDocument/rangeFormattingsurface-specific: editor format-selection
textDocument/codeActionsurface-specific: editor quick-fixes and refactorings
textDocument/publishDiagnosticssurface-specific: editor inline error/warning display
textDocument/didOpensurface-specific: editor document lifecycle
textDocument/didChangesurface-specific: editor document lifecycle
textDocument/didClosesurface-specific: editor document lifecycle
textDocument/didSavesurface-specific: editor document lifecycle
workspace/applyEditServer-initiated edit emitted on flush_completed runtime push (ADR 0082 Phase 3, BT-2289). For each flushed file open in the editor, the LSP issues a whole-document TextEdit so the buffer realigns with the new on-disk content. Closed files are skipped — VSCode reads them fresh on next did_open.

Cross-Surface Behaviours

Some behaviours are not discrete ops but display contracts that every surface inherits. They are recorded here because their output must stay consistent wherever a Beamtalk value is rendered as text.

Object string representation (printString / displayString)

ADR 0094 defines a two-protocol model that governs how any value renders as text across surfaces:

  • printString (Debug) is the canonical structural form and the source of truth for object display. The same printString output appears wherever a value is shown as a developer-facing string: the REPL result line, log lines (runtime logging, Transcript show:), LSP hovers / tooling that echo a value, and any nested rendering inside another printString. Default forms: ClassName(field: value, ...) for Value, Actor(ClassName, pid) / Supervisor(ClassName, pid) / DynamicSupervisor(ClassName, pid) for live processes, bare ClassName for plain objects. The old a ClassName article form is gone. This is not surface-specific — REPL, logs, and tooling must show the same text for the same value.
  • displayString (Display) is the string-interpolation hook: every {...} segment in a string literal renders its value via displayString (defaults to printString). Because interpolation is a language feature, this is identical on every surface that evaluates source.
  • The runtime keeps two implementations in lockstep — the compiled stdlib (Value.bt, Object.bt) and the runtime fallback (beamtalk_object_ops, beamtalk_primitive, beamtalk_reflection) — both delegating to one shared structural renderer so output is byte-identical regardless of dispatch path.

inspect (the language method) is repurposed in Phase 3 (BT-2504): anObject inspect now returns an Inspector cursor (Inspector on: self), not a String. The structural Debug string — formerly the inspect result — is produced solely by printString (ADR 0094). Phase 1 (BT-2502) added the navigable Inspector / InspectorField classes and the Inspector on: anObject entry point — a drillable cursor with at:, structural/snapshot fields, refresh, and a depth-1 printString tree; Phase 2 (BT-2503) added collection/foreign kinds and value evaluate:. Phase 3 also adds the cross-surface wire form — Inspector asDictionaries (one Dictionary per InspectorField) and Inspector asDictionary (the cursor envelope: kind/path/childCount/page + fields) — the same typed-record pattern SupervisionTree asDictionaries (ADR 0092) uses; surfaces consume it through the shared evaluate seam (e.g. (Inspector on: anObject) asDictionary).

A transitional lint (inspect_in_string_position) flags inspect used directly in ++/string-interpolation position, since it no longer yields a String. The dedicated "op": "inspect" REPL/MCP wire op stays the agent-only flat JSON (line 86) — a compat shim — until its full asDictionaries migration in Phase 5; the navigable wire form is already reachable via evaluate. Epic: BT-2501 (design BT-2397).

Declaration-echo values (type, subclass:, reload)

A handful of top-level forms are declarations, not value expressions — evaluating them has no natural runtime value to print, so each has its own "what does the REPL show" convention. These are recorded here because, per CLAUDE.md's REPL-output rule, a display convention is deliberate and must agree across every surface that evaluates source (CLI REPL, MCP evaluate; the LSP has no eval affordance and is unaffected).

  • Actor subclass: Counter / Object subclass: Foo echoes the bare class name (=> Counter) — the existing, longest-standing convention. Counter reload / :reload Counter echo the same bare name on a successful hot-swap (=> Counter).
  • type Direction = ... echoes the bare declared alias name (=> Direction), mirroring the class-declaration convention above rather than a protocol-declaration-style confirmation message. This is the current, shipped, e2e-tested behaviour (ADR 0108 Phase 8, BT-2902), pinned by tests/repl-protocol/cases/type_alias_repl.btscript. Not yet formally signed off by the maintainer — BT-2902's own PR description flagged it as an ADR-suggested default adopted in the absence of an available maintainer, per CLAUDE.md's REPL-output rule (any REPL display value needs confirmation before being treated as permanent); it is recorded here as the documented status quo, not as a closed decision. Revisit if the maintainer prefers different wording. Implemented as a plain binary echo (not promoted through binary_to_atom, unlike a class name becoming a BEAM module atom) — an alias never needs to become a BEAM module name, so the class path's unbounded-atom-creation trade-off does not apply here.
  • A session-local alias (one declared directly at the REPL prompt, not loaded from a .bt file) has no source file for :help <Alias>'s Declared in: line to point at; that line reads Declared in: REPL — new display text with no prior precedent, also pinned by the same .btscript test. A file-declared alias's :help shows its real source_file path instead (see the docs row in Core Operations).

Drift Check (CI)

The beamtalk-surface-drift binary (crates/beamtalk-surface-drift/, BT-2082) enforces this contract automatically. It runs on every PR via just check-surface-drift, wired into the check job in .github/workflows/ci.yml and just ci.

The check parses this document plus the canonical inventory sources listed above and fails when:

  • A REPL op is registered in runtime/apps/beamtalk_workspace/src/beamtalk_repl_ops_*.erl but missing from any operations table here. Add a row, with -- / binding name / surface-specific: <reason> per surface.
  • A documented Bound binding cell has no corresponding code artifact (e.g. :cmd listed but no dispatch arm in crates/beamtalk-cli/src/commands/repl/mod.rs::handle_repl_command, or an MCP tool name with no #[tool(...)] async fn of that name in crates/beamtalk-mcp/src/server.rs, or an LSP capability that is not enabled in ServerCapabilities).
  • An MCP tool is implemented but missing from this doc — add it to the matching op row, or to the MCP-Only Tools section if it is intentionally surface-specific.
  • A REPL meta-command is dispatched in the CLI but missing from this doc — add it to the matching op row or the REPL Meta-Command Reference table.
  • An LSP capability is enabled in crates/beamtalk-lsp/src/server.rs but missing from this doc — add it to the LSP-Only Capabilities section (or the matching op row).

When adding a new surface binding, the workflow is: implement on the surface(s), update this document in the same PR, and let CI confirm the inventory matches.

REPL Meta-Command Reference

For completeness, the full list of REPL meta-commands and their corresponding REPL ops:

Meta-commandAliasesREPL op
:exit:quit, :qclose
:help:h, :?-- (client-side sugar for Beamtalk help: X, evaluated server-side; BT-2902 intercepts a bare Beamtalk help: <Alias> naming a session-local type Name = ... declaration before it reaches Beamtalk help: — see the MCP docs row)
:clear---- (evaluates Session current clear; BT-2369 removed the clear op)
:bindings:b-- (evaluates Session current bindings keys; BT-2369 removed the bindings op)
:sync:sload-project
:unload <class>--unload
:test:ttest / test-all
:show-codegen:scshow-codegen
:interrupt:intinterrupt
:changes---- (composes evaluate of Workspace changes; ADR 0082 Phase 3)
:dirty---- (composes evaluate of Workspace changes dirtyMethods; ADR 0082 Phase 3)
:flush / :flush <arg>---- (composes evaluate of Workspace flush / Workspace flush: <arg>; ADR 0082 Phase 3)
:recheck image---- (composes evaluate of Workspace recheckImage; ADR 0105 Phase 3)