shprout is a 33-line bash loop that shows an LLM its own source,
runs one fenced bash action, returns the output, and repeats.
The source file is both the harness and most of the prompt. The model also sees an explicit runtime description, so the same agent can understand whether it is in real bash or the browser's constrained just-bash environment.
The center of the agent is deliberately ordinary shell:
p="<you>$(<"$0")</you> ... <task>$task</task> ... $history"
body=$(jq -Rs "{model:\"$MODEL\",messages:[{role:\"user\",content:.}]}" <<<"$p")
rsp=$(curl ... -d "$body" ... | jq -er '.choices[0].message.content|strings') \
|| { echo 'shprout: model request failed' >&2; exit 1; }
cmd=$(awk '/^```/{if(f)exit;f=1;next} f' <<<"$rsp")
out=$(eval "$cmd" </dev/null 2>&1)
history+=$'\n--- you ---\n'$rsp$'\n--- bash ---\n'$outp is the complete world the model sees. jq -Rs reads that world as one raw
string and constructs the request body structurally, so the shell never has to
escape prompt text as JSON by hand. The response's first fenced block is the
action; its output becomes the next observation. Everything else in the repo is
an execution surface, experiment, or evaluator around this loop.
OPENAI_API_KEY=sk_... \
MODEL=gpt-4o \
OPENAI_BASE_URL=https://api.openai.com/v1 \
./shprout "your task"Pollinations can provide the defaults and authentication:
./shprout-polli "your task"
./shprout-polli --sandbox "your task"Local runs need bash, jq, and curl. They execute model output with eval; use the sandbox launcher or another isolated environment for untrusted tasks.
npm install
npm run devOpen the browser Bash terminal. The default
runs the repository’s shprout unchanged: it reads its own source into the
prompt and runs the model’s fenced Bash actions. The source is visible and
editable below the terminal. Generating it from a compressed prompt is optional.
GNU Bash and Linux programs execute inside the browser through CheerpX
WebAssembly, with an xterm.js terminal and a persistent browser filesystem.
There is no shell process on the serving machine. The lightweight just-bash
option runs the same script with a smaller supported command set.
Pollinations consent uses the existing browser auth flow without a client ID.
A small curl transport forwards only model requests to the page; credentials
stay outside the Linux guest. The current VM does not support /dev/fd process
substitution or unrestricted networking. See runtime notes
for verified behavior, hosting headers, and runtime dependencies.
npm install
npm run devOpen the experiment index, then enter the
shell workshop. The workshop runs the
same shprout file with just-bash.
It does not use WebContainer or require cross-origin isolation.
The Pollinations key remains in session storage in the page host and is injected at the restricted network boundary. The OAuth callback is checked against a one-time state value. The shell receives a dummy key and cannot print the real credential.
The workshop keeps the shell loop visible without replacing it with browser-specific agent code:
Previewrenders the first HTML artifact in an opaque, network-disabled iframe.Activityshows each model thought and fenced bash action at the API boundary.Workspaceedits both generated files andSOUL,GOAL,RECENT, and opt-inHEARTBEATstate through one file browser.Transcriptpreserves the exact stdout, stderr, and exit status for debugging.Sourceshows the exact shell harness mounted into the virtual filesystem.
A deterministic ?demo=1 run drives the real
shell loop through three scripted model turns, builds an interactive artifact,
and verifies it without authentication. The classic, capped-history, and
self-modifying harness experiments remain available through the gate and arena;
they are not separate browser applications.
There is also a local-only Common Lisp experiment inspired by An Agent in 100 Lines of Lisp:
./shprout-polli --lisp --sandbox "compute something, then reuse the function"It requires SBCL, curl, and jq. The model emits one fenced Lisp form per
turn; eval runs it in the agent's live image, so definitions persist for the
rest of the run. This is the property under test, not recursion by itself.
Unlike the browser harness, the evaluated form can inspect process state and
credentials. The --sandbox example uses the bundled macOS Seatbelt wrapper,
which limits writes but is not a disposable isolation boundary; do not expose
credentials or readable files you are unwilling to give the generated code.
The separate DOM agent runs generated
JavaScript against assigned page subtrees. It can delegate non-overlapping DOM
regions to parallel child branches and recurse further within a region. Model
access and the Pollinations key stay in the parent page; actions run in an
opaque-origin iframe with network access disabled. A deterministic
?demo=1 run exercises the real
sandbox and recursive fan-out without authentication.
Create any of these files under .shprout/:
SOUL.md: stable principles, tone, and boundaries.GOAL.md: the persistent objective and current definition of done.RECENT.md: a short cross-run handoff, not a transcript.HEARTBEAT.md: a tiny periodic checklist, included only withSHPROUT_HEARTBEAT=1.
State is read again before every model turn, so the agent can update it with ordinary bash. The run log stays separate and contains the full current trajectory.
mkdir -p .shprout
printf '%s\n' 'Prefer small, testable changes.' > .shprout/SOUL.md
printf '%s\n' 'Make the test suite pass.' > .shprout/GOAL.md
./shprout-polli "continue toward the goal"See Optimization loops for the evaluation and self-improvement design, and Approach archive for the useful ideas retained from earlier branches.
The deterministic gate runs an agent against scripted model replies inside just-bash:
npm run gate -- shprout
npm run gate -- approaches/classic --classic
npm run gate -- approaches/syscap --split-capped
npm run gate -- approaches/self-mod --self-modIt verifies task injection, action execution, cumulative history, termination, and each profile-specific contract. The original compression-search result and its limitations are recorded in research/compression.
The arena runs any two profiles and models concurrently in isolated virtual filesystems:
OPENAI_API_KEY=sk_... OPENAI_BASE_URL=https://gen.pollinations.ai/v1 \
MODEL_A=claude-large MODEL_B=openai-fast \
npm run arena -- "your task" shprout approaches/syscapIt emits structured JSON containing each source size, elapsed time, exit code, stdout, and stderr. A scorer or judge can consume that artifact without being coupled to the agent runtime.