A Smalltalk-inspired message-passing system for Bash.
Trashtalk implements message passing, inheritance, traits, aspect-oriented programming and persistent instances - with bash.
I'm not a big fan of bash. I think POSIX is the computing environment we deserve, not the one we need. Bash's ubiquity is its strongest selling point, so strong in fact that bash scripting remains the more-or-less correct choice for a lot of situations, especially in my line of work. This really gets my goat.
I've seen others twist bash/sh into strange loops to give themselves superpowers - both in-person and from afar: a few small tricks, conventions or utilities can become a force-multiplier for software authorship. Personal software authorship. Trashtalk started as a minimal message-passing implementation in bash, intended as an experiment in the direction of enabling expressive personal tool-making in the ugly substrate of shell-scripting.
It lingered in my dotfiles repo for years.
Then LLMs came. I said "Hey Claude, what do you think about this gewgaw over here?" Claude said "You're absolutely right!" and we were off - it morphed into a DSL transpiled into bash, then I added a compiler written in golang to provide native compilation for a subset of the DSL, then I started trying to add a TUI-based Smalltalk-style IDE on top of it. Things continued to get weirder and weirder, each day I travelled half the distance between here and v1.0, and eventually it dawned on me that I'd gone too far, so I dialed back Trashtalk and jettisoned the non-bash bits. More precisely, I spun them off into their own projects. Anyhow, here we are.
So far I've only really used Trashtalk to work on Trashtalk. I'll let you know when that changes. Until then, some things I'm thinking about doing include:
- Exploring the idea of an acme-like editor as a substitute for the whiz-bang TUI I tried so desperately to make work
- Building multi-process CLI tools using the Actor/Stream/EventBus classes (backed by Honker — pub/sub, work queues, and durable streams in SQLite)
If you have any ideas that aren't terribly rude, I'd love to hear them!
Trashtalk uses a DSL compiler that transforms Smalltalk-inspired source files (.trash) into namespaced Bash functions. This way, we implement message passing without polluting the global namespace. Or, well, we pollute it in a principled fashion.
┌─────────────────┐ ┌──────────────┐ ┌─────────────────┐
│ Source (.trash)│────▶│ Compiler │────▶│ Compiled (bash) │
│ │ │ │ │ │
│ Counter subclass│ │ jq-compiler │ │ __Counter__ │
│ method: inc │ │ │ │ increment() │
└─────────────────┘ └──────────────┘ └─────────────────┘
│
▼
┌─────────────────┐
│ Dispatcher │
│ │
│ @ Counter inc │
│ ▼ │
│ __Counter__ │
│ increment() │
└─────────────────┘
- DSL Compiler (
lib/jq-compiler/) - Bash tokenizer and jq parser/code generator that transforms.trashsource files into executable Bash - Dispatcher (
lib/trash.bash) - Routes@message sends to the appropriate namespaced function - Source Files (
trash/*.trash) - Human-readable class definitions - Compiled Files (
trash/.compiled/) - Generated Bash code loaded by the runtime
- Bash 4.4+ — macOS ships bash 3.2 at
/bin/bash; install a modern one withbrew install bash, put it first on PATH (export PATH="$(brew --prefix)/bin:$PATH"), then start it withexec bash. Build scripts also resolvebashfrom PATH. - Required tools:
jo,jq,sqlite3,uuidgen; builds also needmakeandshasum- Conversation log projection also uses Perl with its core
JSON::PP,Digest::SHA, andTime::HiResmodules for file reads and hashes. - macOS:
brew install jo jq sqlite(uuidgenis built in) - Debian/Ubuntu:
sudo apt install bash jo jq sqlite3 uuid-runtime make perl libdigest-sha-perl
- Conversation log projection also uses Perl with its core
Clone or copy this repository to ~/.trashtalk:
git clone https://github.com/chazu/trashtalk.git ~/.trashtalkCompile the bundled classes (required before first use — the runtime dispatches
to generated bash in trash/.compiled/):
cd ~/.trashtalk && makeAdd the following to your Bash startup file (~/.bashrc; source it from
~/.bash_profile if you use login shells). Trashtalk must run in Bash:
source ~/.trashtalk/lib/trash.bashStart a fresh Bash session or source the file above, then verify:
@ Trash doctor
@ Trash infoThe Bash floor comes from NUL-delimited mapfile -d in the Tool process
boundary. macOS system Bash 3.2 and Zsh are not supported runtimes. See the
documentation index for current APIs, designs, and historical notes.
If anything misbehaves, run the built-in diagnostics first:
@ Trash doctor # or: make doctorIt checks bash version (needs 4.4+), required tools (jo/jq/sqlite3/uuidgen),
whether the sqlite3 in use can load the optional honker extension, and whether
classes have been compiled — and prints a clear OK/WARN/FAIL line for each.
It checks Jcode, the default session harness. Install it from jcode.sh
if needed, then use @ Jcode login for OpenAI subscription authentication.
When TRASHTALK_GUSGUS_PROFILE=maki is selected, doctor installs Maki if missing
and verifies its executable. Maki login is @ Maki loginToProvider: 'openai'.
Common fixes:
declare: -A: invalid option/ nothing works on macOS — you're on the system bash 3.2.brew install bashand use it (exec "$(brew --prefix)/bin/bash").unknown command "load"/ honker errors — yoursqlite3lacks extension support (Apple's does).brew install sqlite, thenexport TRASH_SQLITE3="$(brew --prefix sqlite)/bin/sqlite3".Unknown class '...'— runmaketo compile the classes.
@ Trash help lists all system commands. Tab completion for @ loads
automatically in interactive shells.
# Send a message to an object
@ Trash info
# Create a counter instance
counter=$(@ Counter new)
@ $counter setValue 5
@ $counter increment 3
@ $counter show
# Create an array
arr=$(@ Array new)
@ $arr push hello
@ $arr push world
@ $arr show
# System introspection
@ Trash listObjects
@ Trash methodsFor Counter
@ Trash helpClasses are defined in .trash files using a Smalltalk-inspired syntax:
# Counter - A simple counter class
Counter subclass: Object
include: Debuggable
instanceVars: value:0 step:1
method: increment [
| newValue |
newValue := $(( $(_ivar value) + $(_ivar step) ))
_ivar_set value "$newValue"
echo "$newValue"
]
method: setValue: val [
_ivar_set value "$val"
]
method: show [
echo "Counter value: $(_ivar value)"
]| Element | Syntax | Description |
|---|---|---|
| Class declaration | ClassName subclass: SuperClass |
Declare a class with inheritance |
| Trait declaration | TraitName trait |
Declare a trait (mixin) |
| Include trait | include: TraitName |
Mix in a trait |
| Instance variables | instanceVars: name:default |
Declare instance vars with defaults |
| Dependencies | requires: 'path/to/file.bash' |
Source external dependencies |
| Method | method: name [body] |
Define an instance method |
| Method with args | method: foo: x bar: y [body] |
Keyword-style arguments |
| Class method | classMethod: name [body] |
Define a class method |
| Raw method | rawMethod: name [body] |
Pass-through (no transformation) |
| Test method | testMethod: name [body] |
Define an inline test (see Testing) |
| Local variables | | var1 var2 | |
Declare local variables |
| Assignment | var := value |
Assign to variable |
| Self reference | @ self methodName |
Message to self |
The compiler transforms DSL constructs to Bash:
# DSL syntax:
method: example: arg [
| result |
result := $(some_command)
@ self debug: "Got result: $result"
@ OtherClass doSomething: "$result" with: "$arg"
]
# Compiles to:
__MyClass__example() {
local arg="$1"
local result
result=$(some_command)
@ "$_RECEIVER" debug "Got result: $result"
@ OtherClass doSomething_with "$result" "$arg"
}Use rawMethod: for code that shouldn't be transformed (heredocs, traps, complex bash):
rawMethod: createConfig: name [
cat > "$CONFIG_DIR/$name" << 'EOF'
# Configuration file
setting=value
EOF
echo "Created config: $name"
]Traits provide reusable behavior without inheritance:
Debuggable trait
method: debug: message [
[[ "${TRASH_DEBUG:-1}" == "0" ]] && return 0
local timestamp
timestamp=$(date '+%Y-%m-%d %H:%M:%S')
echo "[$timestamp] DEBUG ($_RECEIVER): $message" >&2
]
method: inspect [
echo "Object: $_RECEIVER"
echo "Class: $_SUPERCLASS"
]Trashtalk supports before/after advice for cross-cutting concerns like logging, validation, or notifications:
Account subclass: Object
instanceVars: balance:0
method: withdraw: amount [
balance := balance - amount
]
method: deposit: amount [
balance := balance + amount
]
# Run before withdraw: executes
before: withdraw: do: [
@ self log: "Attempting withdrawal"
]
# Run after deposit: completes
after: deposit: do: [
@ self notifyBalanceChanged
]Advice hooks execute automatically - before:do: runs prior to the method, after:do: runs after it returns.
Trashtalk supports defining tests directly in class files using testMethod:.
When inmacs is on PATH, @ Trash edit: ClassName opens the source in the
Innards inline editor with Trashtalk syntax highlighting and two-space
indentation. Saving compiles to a temporary artifact, checks the generated Bash,
installs and reloads the class, then runs its inline tests. Compiler and test
failures reopen the editor as annotations at the relevant source line; they are
never inserted into the .trash source.
If Innards is unavailable, the edit command falls back to $VISUAL, then
$EDITOR, then vi. The fallback still uses the same compile, validation,
reload, and test pipeline after the file changes. @ Trash doctor reports
Innards availability as an optional capability.
With inpick on PATH, Trashtalk derives browser records directly from the
canonical jq compiler AST and previews the selected source without maintaining
a second index. fzf is used as a fallback when Innards is unavailable.
@ Trash browse # choose any symbol and open its source
@ Trash browseClass: Counter # browse one class and open a selection
@ Trash pickMethod: Counter # return a structured method selection
@ Trash browseImplementorsOf: 'at:put:'
@ Trash browseSendersOf: 'at:put:'
@ Trash browseInstancesOf: Counter # table of persisted instances, then inspect on Enter
@ Trash inspectInstancesOf: Counter # compatibility spelling for the same workflow
@ Trash selectInstanceOf: Counter # return a structured picker selection to scriptsClass, trait, instance-variable, class-variable, instance-method, class-method, and test-method records carry exact source positions. Namespaced classes and complete multi-keyword selectors remain intact. Enter in an instance table opens its navigable object inspector with declared values and nested containers, rather than printing the selected record JSON. Script-level picker methods return JSON; commands that open source feed the chosen path and line into the same transactional edit/compile/test loop described above.
With ininspect on PATH, any persisted object can open as a navigable inline
tree. Containers expand in place and e on a scalar edits it as a JSON value:
counter=$(@ Counter create)
@ "$counter" inspectInteractiveInnards only returns an edit proposal. Trashtalk checks that the object and its
selected value have not changed, rejects unknown or command-bearing fields,
and then applies the typed value through Runtime. Runtime metadata is not
offered as editable state. Plain @ "$counter" inspect remains the textual
fallback and never requires Innards.
Counter subclass: Object
instanceVars: value:0 step:1
method: increment [
value := value + step.
^ value
]
method: setStep: s [
step := s
]
testMethod: testIncrement [
pragma: primitive
local c result
c=$(@ Counter new)
result=$(@ "$c" increment)
_assert_eq "$result" "1" "increment returns 1"
@ "$c" destroy
]
testMethod: testCustomStep [
pragma: primitive
local c
c=$(@ Counter new)
@ "$c" setStep: 5
_assert_eq "$(@ "$c" increment)" "5" "custom step works"
@ "$c" destroy
]Tests use TAP (Test Anything Protocol) assertions:
| Function | Description |
|---|---|
_assert_eq "$actual" "$expected" "desc" |
Assert values are equal |
_assert_neq "$actual" "$unexpected" "desc" |
Assert values are not equal |
_assert_true "$value" "desc" |
Assert value is non-empty |
_assert_false "$value" "desc" |
Assert value is empty |
_assert_contains "$haystack" "$needle" "desc" |
Assert string contains substring |
_assert_ok "command" "desc" |
Assert command succeeds (exit 0) |
# Run tests for a class
@ Trash runTestsFor: Counter
# Check if a class has tests
@ Trash hasTestsFor: Counter
# Tests run automatically during edit flow
@ Trash edit: CounterOutput follows TAP format:
# Running tests for Counter
ok 1 - increment returns 1
ok 2 - custom step works
1..2
# All 2 tests passed
Tools::Roam provides checkout-local indexing and code-graph queries;
Tools::AstGrep searches syntax patterns; Tools::Cass searches existing agent
sessions. Tools::Chad wraps the local Chad harness for explicit headless tasks
and plan mode. All use exact argument vectors and preserve process diagnostics.
See code and session Tool adapters for setup,
examples, result contracts, and qualification limits.
Use @ AgentSession browse and choose Attach to conversation, or send
focus / attach to an existing session. The Innards inagent applet shows
backlog and live harness output, offers an inbox-backed composer, and detaches
without stopping work. Message actions also offer Attach to sender session
when the sender can be resolved. See session view controls and setup.
@@ talks to Gusgus, a persistent assistant with one current conversation
across directories. Every argument is message text; @@ has no flags. It sends your message and returns immediately; Gusgus works in a
managed Jcode session by default and answers into your inbox, in the same thread as
your question. Replying to that message continues the same conversation.
false
__='the command produced this output'
@@ 'why did that fail?' # prints the message id and returns
inbox=$(@ Trash userInbox)
@ $inbox list # Gusgus's reply appears here
@ $inbox show: $msg
@ $msg reply: 'and how do I fix it?' # resumes the same conversation
@ $inbox thread: $msg # the whole exchange, oldest first
@ Gusgus focusCurrent # Option-U toggles this view
@ Gusgus fresh: "$PWD" # explicitly replace an idle conversation
@ Gusgus helpOption-U opens the current conversation and detaches when pressed inside it. The composer sends directly to the agent session with C-c C-c, including steering at the next safe point while Jcode is working. These inputs create no inbox mail. See the session view for controls and key setup.
inbox=$(@ Trash userInbox) returns your persisted Inbox instance, using
TRASHTALK_USER with $USER as the fallback. Other inboxes are instances of
the same class, retrieved with @ Inbox named: 'gusgus'.
@ "$inbox" browse opens that inbox in Innards: inpick lists the messages
with a rendered preview of each. Displaying a preview marks that message read
and clears its unread dot. Opening a thread marks its messages read too.
Ctrl-D archives the highlighted message
and refreshes the inbox; archived messages remain available in their threads.
Enter opens actions for reply, viewing the thread in inpage, archive,
or back. Reply composes
in inmacs and sends the saved text into the thread, which resumes Gusgus
when the message came from a session. Without Innards the same loop falls
back to fzf and $EDITOR.
@ "$inbox" count returns its total non-archived messages, including read
messages. @ "$inbox" unreadCount counts only unread messages.
@ Inbox count counts stored inbox instances. The Inbox class does not
implicitly select the current user's inbox.
Each @@ becomes an AgentDelivery on Gusgus's current AgentSession.
AgentWorker notifies the configured harness with inbox message references;
the agent reads their contents from Inbox and uses AgentRun result:forDelivery:
and settle: to respond and acknowledge work. Every run gets a private launcher
for the common trash-send API. Busy sessions queue messages for the next prompt.
Jcode is the default and uses a resident daemon, resuming the same native
conversation across runs. Jcode and Maki run with your normal OS permissions.
@@ and inbox replies request foreground ticks. For queued work to continue
without another command, run bin/trash-worker or install and start its user
service with bin/trash-worker-service install and bin/trash-worker-service start.
@ AgentSession browse opens session activity, messages, run logs, and explicit
pause/resume/retry actions in Innards. See agent operations
for recovery behavior, service controls, and validation. Gusgus uses OpenAI
OAuth and medium reasoning effort. Configure with
TRASHTALK_JCODE_MODEL (default gpt-5.6-terra),
TRASHTALK_USER (your inbox name, default $USER), and
TRASHTALK_GUSGUS_PROFILE (jcode by default; maki for Maki; codex or the legacy
assistant-low-power for Codex; shell for a script in
TRASHTALK_SHELL_DRIVER). Codex uses TRASHTALK_CODEX_MODEL
(default gpt-5.6-terra). Maki uses TRASHTALK_MAKI_MODEL
(default openai/gpt-5.6-terra). Jcode uses existing OpenAI subscription login.
@ Jcode login starts interactive authentication. @ "$run" stop pauses its
session and stops that exact run; agents use AgentRun stop: with agent.stop
role authority. Profiles are captured when a session opens;
changing the default does not migrate existing conversations. See the
Jcode driver design,
Maki driver design, and
docs/headless-agent-sessions-design.md.
Assignment adds durable work owned by an identity, with explicit session
selection, progress, inbox questions, and atomic completion. Its first slice is
manual: published work is held from harness dispatch. Follow the
Assignment walkthrough to try it from Bash.
Wrap a command with bin/trash-command --cwd "$PWD" --label 'unit tests' -- make test
and the worker turns repeated failures into one durable alert in your inbox,
with a count for your prompt, acknowledge/snooze/resolve/suppress controls, and
optional delegation to Gusgus or another agent you already run. See the
workstation guide.
@ Agent ask:workingDirectory:status:lastResult: sends one explicit, read-only request through the selected
external agent harness with no memory. Axe is the default; the official Codex
CLI is also supported. Pass the question, working directory, command status, and optional prior result
explicitly. Agent present: displays the answer.
run=$(@ Agent ask: 'why did that fail?' workingDirectory: "$PWD" status: '1' lastResult: '')
@ Agent present: "$(@ Agent answerFromRun: "$run")"
# Inspect the selected backend's exact context without making an LLM call.
@ Agent dryRun: 'what context would you receive?' workingDirectory: "$PWD" status: '0' lastResult: ''Choose the backend in ~/.trashrc:
# Default: Axe with the checked-in project-local agent profile.
TRASHTALK_AGENT_BACKEND=axe
# Official Codex CLI using a ChatGPT subscription login.
TRASHTALK_AGENT_BACKEND=codexFor Codex, run codex login and select the ChatGPT login, then verify it with
codex login status. The adapter refuses API-key authentication and removes
CODEX_API_KEY and OPENAI_API_KEY from the child process so selecting this
backend cannot silently fall back to per-token API billing. It invokes
codex exec ephemerally, ignores user tool configuration, and fixes the
sandbox to read-only. See OpenAI's documentation for
authentication and
non-interactive Codex.
Trashtalk never installs Axe or initializes credentials implicitly. Install it
explicitly with go install github.com/jrswab/axe@latest, configure the
provider required by axe/agents/trashtalk-readonly.toml, and use @ Trash doctor to check availability. The checked-in agent enables only Axe's
list_directory and read_file tools—no file mutation, shell commands, or
subagents.
@@ preserves Axe's status distinctions: 1 runtime, 2 configuration, 3
provider/network, and 4 budget exhaustion. Missing Axe returns 127.
Codex failures preserve their original process status; a non-ChatGPT login is
reported as configuration exit 2, and a missing Codex CLI as 127.
Source mutation is a separate operation from @@. A specialized read-only Axe
agent can propose a one-file .trash unified diff, but cannot apply it:
run=$(@ Agent propose: 'make value return 2' for: Counter)
proposal=$(printf '%s' "$run" | jq -r .result.content)
@ Agent reviewAndApplyProposal: "$proposal"Proposal schema v1 is closed and deliberately narrow:
{
"schema_version": 1,
"kind": "trashtalk_source_patch",
"files": [{
"class_name": "Counter",
"path": "trash/Counter.trash",
"base_sha256": "<64 lowercase hex characters>",
"diff": "--- a/trash/Counter.trash\n+++ b/trash/Counter.trash\n..."
}]
}indiff only displays the diff and records zero-based accepted/rejected hunk
indices. After an acceptance, Trashtalk validates that complete decision,
rechecks the source hash, applies only accepted hunks to a temporary copy,
compiles it with the canonical jq compiler, validates generated Bash, runs the
candidate's tests, checks the hash again, and then installs source and artifact
with rollback backups. Rejection, cancellation, stale hashes, invalid paths or
headers, and failed gates leave the working source and compiled artifact
unchanged. Command fields and multi-file proposals are rejected; no
agent-authored command is executed.
Compile a single class:
make single CLASS=MyClassCompile all classes:
make compileOr use the compiler directly:
lib/jq-compiler/driver.bash compile trash/MyClass.trash > trash/.compiled/MyClassTrashtalk includes a built-in profiling system to help identify performance bottlenecks and optimize method dispatch.
Runtime diagnostics default to warnings/errors on stderr. Use
TRASHTALK_LOG_LEVEL=debug for method-resolution diagnostics or trace to
include message arguments. Interactive slow operations show delayed progress
on /dev/tty; TRASHTALK_PROGRESS=0 disables it. See
performance and terminal output for JSON-value
construction, browser caching, and the isolated bin/trash-bench harness.
Set TRASH_PROFILE=1 to enable profiling output:
# Profile to stderr
TRASH_PROFILE=1 @ Counter new
# Profile to a file
TRASH_PROFILE=1 TRASH_PROFILE_FILE=profile.log @ MyApp runProfiling logs entry and exit points with timing:
[1767909948.119] → Counter.new [bash]
[1767909948.295] ← Counter.new [bash] 176ms
→marks method entry←marks method exit with elapsed time- Routes identify Bash dispatch (
bash) or caller-shell methods (bash:direct).
| Variable | Description |
|---|---|
TRASH_PROFILE=1 |
Enable profiling output |
TRASH_PROFILE_FILE=path |
Write to file instead of stderr |
TRASH_PROFILE_DEPTH=N |
Only log calls up to depth N |
TRASH_PROFILE_MIN_MS=N |
Only log calls taking >= N milliseconds |
Use bin/trash-profile-analyze to generate reports from profile logs:
# Generate profile data
TRASH_PROFILE=1 @ MyApp run 2>profile.log
# Analyze the profile
bin/trash-profile-analyze profile.logThe analyzer reports dispatch routes, slowest individual calls, the most-called methods, and classes by call count. Method times include nested calls, so their sum is not wall-clock duration. The report's timestamp span covers the first through last completed call. Use these measurements to choose a representative workflow to benchmark; the report does not infer subprocess counts or recommend another runtime.
| Class | Description |
|---|---|
Object |
Root class with new, findAll, count methods |
Trash |
System introspection and management |
Store |
SQLite-backed instance persistence |
Array |
Dynamic array with push, pop, map, filter |
Counter |
Simple counter with increment/decrement |
File |
File system operations (read, write, temp files) |
Future |
Async computation with result retrieval |
Process |
External OS process management (subprocess-like) |
ReplServer |
Socket-based REPL server for Emacs integration |
Honker |
Pub/sub, work queues, streams, locks, rate limiting (requires honker extension) |
EventBus |
Observer pattern via ephemeral pub/sub |
Actor |
Mailbox-style actors with background dispatch and at-least-once delivery |
Stream |
Cross-process durable streams with consumer offset tracking |
Scheduler |
Cron-based periodic tasks with leader election |
Inbox |
Durable named inboxes for messages between agents, humans, and processes |
Message |
A persisted message: sender, recipient, kind, status, thread |
Gusgus |
The persistent assistant behind @@: one session per workspace |
AgentSession |
Durable agent conversation bound to an identity, archetype, role, and workspace |
AgentIdentity, AgentArchetype, AgentRole |
Who an agent is, what it is for, and what it may do |
AgentRun, AgentDelivery |
One harness process, and the durable input batch it was offered |
AgentWorker |
Foreground dispatch and reconciliation: claim, launch, settle |
JcodeDriver, MakiDriver, CodexDriver, ShellDriver |
Common session drivers for resident Jcode, Maki SDK, Codex, and test scripts |
| Trait | Description |
|---|---|
Persistable |
Save/reload, deletion, and Store queries |
Debuggable |
Debug logging, inspection, ancestry tracing |
Assignment::Authority, Assignment::Reporting, Assignment::Presentation |
Assignment authorization, progress/questions, and views |
Observable |
Event emission, subscription, and atomic save+emit for any class |
# Basic syntax
@ <Receiver> <selector> [args...]
# Examples
@ Trash info # No arguments
@ Counter new # Returns instance ID
@ "$counter" incrementBy: 5 # Instance method with arg
@ Store getField: "$id" field: name # Public keyword messagenew immediately saves an initial SQLite record. Subsequent mutations update
the session cache and need an explicit save to become durable:
counter=$(@ Counter new)
@ "$counter" value: 42
@ "$counter" save
@ Counter findAll # List stored Counter instances
@ Counter find: 'value > 10' # Query durable state
@ Counter countPersistable supplies save, reload, deletion, and query methods. See
object persistence for cache freshness, deletion, and
Store transactions.
Trashtalk optionally integrates with Honker, a SQLite loadable extension that adds pub/sub, work queues, durable streams, distributed locks, rate limiting, and cron scheduling — all backed by the same SQLite database used for instance persistence. No extra processes or external brokers needed.
The easiest way is the bundled installer, which clones honker, builds it with cargo, and drops the artifact into ~/.trashtalk/lib/vendor/honker/:
bin/install-honker # build from main + install
bin/install-honker --ref v0.2 # pin to a tag/branch/commitRequires cargo (Rust toolchain) and a sqlite3 built with loadable-extension support. On macOS, the system sqlite3 does not allow .load; install Homebrew's and put it ahead on PATH:
brew install sqlite
export PATH="$(brew --prefix sqlite)/bin:$PATH"Or install manually:
# Option 1: Project-local
cp libhonker_ext.dylib ~/.trashtalk/lib/vendor/honker/ # macOS
cp libhonker_ext.so ~/.trashtalk/lib/vendor/honker/ # Linux
# Option 2: System-wide
cp libhonker_ext.dylib /usr/local/lib/ # macOS
cp libhonker_ext.so /usr/local/lib/ # Linux
# Option 3: Explicit path
export HONKER_EXT=/path/to/libhonker_extTrashtalk auto-detects honker at startup. Everything works without it — honker-dependent classes degrade gracefully, and tests skip automatically.
# Create a named event bus
bus=$(@ EventBus named: 'orders')
# Subscribe with a handler block
handler=$(@ Block params: '["payload"]' code: 'echo "Got: $payload"' captured: '{}')
@ $bus on: 'created' do: $handler
# Emit events
@ $bus emit: 'created' payload: '{"id":42,"total":99.50}'
@ $bus emit: 'shipped' # no payload
# Clean up
@ $bus shutdownOrder subclass: Object
include: Persistable
include: Observable
instanceVars: status:'pending' total:0order=$(@ Order new)
@ $order on: 'completed' do: $handler # subscribe to this instance
@ $order emit: 'completed' # fire event
@ $order saveAndEmit: 'saved' # atomic persist + event in one transactionEach actor has a named queue. Messages are processed sequentially by a background dispatcher. Honker provides at-least-once delivery with retries and dead-lettering.
actor=$(@ Actor named: 'order-processor')
@ $actor start # start background dispatcher
@ $actor send: 'processOrder' with: '42'
@ $actor send: 'cleanup'
@ $actor pendingCount # check mailbox depth
@ $actor stopTwo separate trashtalk programs sharing the same database can communicate through streams. Messages survive crashes, and consumers track their position with offsets.
# Terminal 1 (producer)
producer=$(@ Stream named: 'metrics')
@ $producer publish: '{"cpu":42,"host":"web1"}'
# Terminal 2 (consumer)
consumer=$(@ Stream named: 'metrics' consumer: 'dashboard')
msgs=$(@ $consumer read)
@ $consumer ack: 5 # advance offset
@ $consumer consumeDo: $handler # continuous background consumption@ Scheduler every: '*/5 * * * *' call: 'cleanup_fn' named: 'cleanup'
@ Scheduler start # start tick loop
@ Scheduler stopMultiple processes can run the scheduler — honker's leader election ensures each task fires exactly once.
@ Honker lock: 'deploy' # acquire distributed lock
@ Honker unlock: 'deploy'
allowed=$(@ Honker rateLimit: 'api-call' limit: 100 window: 60)An Inbox is a durable, named mailbox that agents, humans, and deterministic
processes all share the same way. Messages are ordinary Persistable objects,
so they survive process exit and can be listed, read, replied to, and archived
from any trashtalk process using the same Store. Inboxes are created on first
use; names may contain letters, digits, _ . : -.
# An agent (or cron job) reports to a human
@ Inbox send: 'all 71 tests pass' to: 'chazu' from: 'maki:abc123' subject: 'done' kind: 'result'
@ Inbox alert: 'disk 95%' to: 'chazu' from: 'cron'
# An agent asks a question and waits for the answer to land in its own inbox
q=$(@ Inbox ask: 'ok to force-push?' to: 'chazu' from: 'maki:abc123')
# The human reads and replies from the REPL
inbox=$(@ Inbox named: 'chazu')
@ $inbox list # unread messages, one line each
@ $inbox show: $q # full message; marks it read
@ $q reply: 'yes' # lands in maki:abc123's inbox, same thread
# The agent finds the answer
@ $(@ Inbox named: 'maki:abc123') unread
@ $(@ Inbox named: 'maki:abc123') thread: $q # question + reply, oldest firstQueries return instance ids, one per line: unread, unreadCount,
questions (unread, kind question), messages / messages: n (recent,
non-archived, newest first), thread: id. Bulk actions: readAll, and per
message markRead / archive. Kinds are free-form; note, question,
alert, and result are the conventions the helpers use.
Delivery transports live outside the core. With the honker extension, an
inbox can run a Block for every future delivery; the block receives the
message as JSON. Put whatever reaches you there: mosquitto_pub, tmux send-keys, a desktop notifier, or a message send into another Trashtalk
object.
handler=$(@ Block params: '["payload"]' \
code: 'mosquitto_pub -t "inbox/$(jq -r .to <<<"$payload")" -m "$(jq -r .body <<<"$payload")"' \
captured: '{}')
@ $inbox onMessage: $handler # background listener; returns its pid
@ $inbox stopListeningWithout honker, send/read/reply work unchanged; onMessage: warns and
returns an empty pid. @ Inbox help and @ Message help list every message.
Run bin/trash for the REPL. It uses Bash Readline for editing, Up/Down history,
and Tab completion of classes, live object variables, and methods; no rlwrap
filter is required. History defaults to ~/.trash_history; override it with
TRASHTALK_HISTORY_FILE. bin/trash --help shows examples, and piped message
input works without terminal setup.
make skips unchanged compiled classes and rebuilds parent/trait dependencies
before their dependents. make single CLASS=Counter uses the same build cache.
Run make verify to build and check both runtime and compiler suites in isolated
parallel test checkouts. See performance for controls and
JSON values for typed reads, bulk field binding, and
collection traversal primitives.
Vendored in lib/vendor/:
sqlite-json.bash- SQLite-based JSON document store and key-value persistencehonker.bash- Bash wrapper for the Honker SQLite extension (pub/sub, queues, streams)tuplespace/- Event coordination (legacy; can be upgraded to honker viatuplespace-honker.bashshim)bsfl.sh- Bash utility functionsfun.sh- Functional programming utilities
External tools (install separately):
jo- JSON output from shelljq- JSON processorsqlite3- Database engineuuidgen- UUID generation (usually pre-installed)perlwithJSON::PP,Digest::SHA, andTime::HiRes- conversation log file adapterlibhonker_ext- Honker SQLite extension (optional — enables EventBus, Actor, Stream, Scheduler)
Trashtalk includes a major mode for Emacs with syntax highlighting, indentation, and REPL integration for interactive development.
Add to your init.el:
(add-to-list 'load-path "~/.trashtalk/emacs")
(require 'trashtalk-mode)Or with use-package:
(use-package trashtalk-mode
:load-path "~/.trashtalk/emacs"
:mode "\\.trash\\'")~/.trashtalk/
├── emacs/
│ └── trashtalk-mode.el # Emacs major mode with REPL support
├── lib/
│ ├── trash.bash # Main runtime & dispatcher
│ ├── jq-compiler/ # jq-based DSL compiler
│ │ ├── driver.bash # CLI entry point
│ │ ├── tokenizer.bash # Source → JSON tokens
│ │ ├── parser.jq # Tokens → AST
│ │ └── codegen.jq # AST → Bash code
│ └── vendor/ # Vendored dependencies
│ ├── sqlite-json.bash # SQLite JSON document store
│ ├── honker.bash # Honker extension wrapper
│ └── tuplespace/ # Legacy event coordination
├── trash/
│ ├── *.trash # DSL source files
│ ├── .compiled/ # Compiled output
│ │ └── traits/ # Compiled traits
│ └── traits/ # Trait source files
│ ├── Debuggable.trash
│ ├── Observable.trash # Event emission mixin
│ └── ...
└── tests/ # Test scripts
Supposedly v1.0.0
Chaz Straney
