A production-minded end-to-end coding agent in TypeScript, built on the Bun runtime. It pairs a real model loop (OpenRouter tool-calling) with an isolated execution sandbox, hook-based policy enforcement, hard budget ceilings, OTLP telemetry, and GitHub PR publishing — all driven from a live terminal UI.
The agent runs in two modes:
- Host mode — tools execute in a local repository (fast local iteration).
- Sandbox mode — the entire session runs inside a disposable E2B VM; the finished work is pushed to a feature branch and opened as a pull request.
- Real agent loop — OpenRouter chat-completions with tool calling, per-turn usage accounting, and automatic history compaction at 150k tokens.
- Rigorous tool surface —
read_file,edit_file,ripgrep,tree_sitter_symbols,run_shell,git, and full-stateTodoWriteplan management. Tool definitions live in one registry consumed by the dispatcher, the model, and MCP. - Sandboxed by default — every tool call executes inside a fresh E2B VM, never on the harness host. Results are published as a pull request from an
agent/<id>branch. - Policy as code — lifecycle hooks (
PreToolUse,PostToolUse,SessionStart,SessionEnd,Stop, ...) receive JSON events on stdin and can block calls. Two built-in guards are non-disableable:- destructive
rm -rftargets outside the worktree; git pushthat could target the default branch (main/master) — PR branches only.
- destructive
- Hard budget ceilings — 4k completion tokens/turn, 200k session tokens, $5/session, 50 turns. No open-ended runs.
- Defense-in-depth path safety — lexical checks plus realpath and symlink-aware confinement; every tool call passes through
PreToolUse/PostToolUse. - OTLP telemetry — Langfuse-ready
gen_ai.*spans exported over OpenTelemetry. - MCP StreamableHTTP server — the same tool surface exposed for other agents and IDEs.
- Terminal UI — Ink/React: live plan pane, tool-call stream, budget meter, final summary, and PR link.
| Area | Choice |
|---|---|
| Runtime | Bun (TypeScript, ESM) |
| Model loop | OpenRouter chat/completions with function calling |
| UI | Ink + React |
| Sandbox | E2B (isolated Linux VMs) |
| Validation | Zod, plus hand-rolled plan-state validation |
| Telemetry | OpenTelemetry OTLP/HTTP → Langfuse |
| Interop | MCP StreamableHTTP server |
| Tests | bun test (34 tests across 10 suites), tsc --noEmit |
src/index.tsx (CLI entry · Ink terminal UI)
│
├── EventBus ───────────── plan / tool / usage / status / pr events
├── HookRunner ─────────── lifecycle hooks + built-in command guards
└── runSession ─────────── orchestration (session.ts)
│
├── host mode → createToolSurface() → local repo
├── sandbox → E2B Sandbox → createSandboxSurface() → publishSuccess() → PR
│
└── createDispatcher(specs, hooks, events) (dispatcher.ts)
└── runAgentLoop() (model.ts · OpenRouter)
└── registry.ts → read_file · edit_file · ripgrep ·
tree_sitter_symbols · run_shell ·
git · TodoWrite
Flow for every model tool call:
- The model loop requests a tool call.
- The dispatcher emits
PreToolUse; built-in guards (destructiverm -rf, push-to-main) plus any external hook may block it. - The tool runs against the host surface or the sandbox surface.
- The dispatcher emits
PostToolUse, records usage, and streams the call to the UI.
Prerequisites: Bun, an OpenRouter API key, and (for sandbox mode) an E2B API key and a GitHub App installation token.
bun installexport OPENROUTER_API_KEY=sk-... AGENT_MODEL=openai/gpt-5
bun run src/index.tsx run ./path/to/repo "Describe the task"Pass a cloneable Git URL instead of a local path:
export OPENROUTER_API_KEY=sk-...
export E2B_API_KEY=...
export AGENT_GITHUB_INSTALLATION_TOKEN=...
bun run src/index.tsx run https://github.com/acme/widgets.git "Describe the task"Press Ctrl-C to cancel. The terminal UI streams the plan, every tool call,
usage, the final summary, and the PR URL on success.
| Tool | Description |
|---|---|
TodoWrite |
Replaces the complete plan state (full-state only, no patches) |
read_file |
Reads a file inside the repository as UTF-8 |
edit_file |
Replaces the first exact occurrence of oldText with newText |
ripgrep |
Line-numbered repository search (falls back to grep -rn) |
tree_sitter_symbols |
Top-level symbol listing via tree-sitter |
run_shell |
Runs a shell command with a timeout (default 30s, max 120s) |
git |
status / diff / commit / push (push refused on main) |
All outputs are truncated to 4,000 characters. In sandbox mode every tool above
executes inside the VM via src/sandbox.ts.
- Path confinement —
createToolSurfaceresolves paths lexically against the repo root, then walks up to the nearest existing ancestor and compares realpaths, so a symlink inside the repo cannot reach outside. Absolute and..traversal paths are rejected. (Host mode only; sandbox mode is fully isolated.) - Sandbox by default — sandbox mode requires a cloneable Git URL; local host paths are rejected. The worktree is created with
git worktree addon anagent/<id>branch inside the VM. - Non-disableable guards (in
src/hooks.ts, applied inPreToolUse):destructiveCommandGuard— blocksrm -rftargets outside the worktree (~and..included).pushToMainGuard— blocksgit pushtargetingmain/master,--all/--mirror, and ambiguous bare/remote-only pushes.
- External policy hooks — a
PreToolUsehandler can block any call by printing{"block":true,"reason":"..."}. Hook failures are non-fatal. - Budget ceilings — tokens, dollars, and turns are hard limits enforced by the loop; exhaustion fails the session.
Hooks fire as executable commands that receive a JSON event line on stdin. Set them with environment variables:
export AGENT_HOOK_SESSION_END='./scripts/session-end.sh'
export AGENT_HOOK_PRE_TOOL_USE='./scripts/policy.sh'
bun run src/index.tsx run ./repo "Task"Supported events: SessionStart, UserPromptSubmit, PreToolUse, PostToolUse, PreCompact, Notification, SessionEnd, Stop.
PreToolUse handlers may return {"block":true,"reason":"..."} on stdout to reject a call; the built-in guards run in addition to any external handler. Stop writes a session trace to <repo>/.agent/traces/<session>.json (host mode) or .agent-traces/<session>.json (sandbox mode).
TodoWrite manages the task plan as full-state JSON at .agent/state.json (host mode) or .agent/state.json inside the sandbox worktree. Each turn must replace the entire state; incremental patches are rejected. Todos carry pending / in_progress / done status and a free-form notes array. Persistence is atomic (temp file + rename) and validated before write.
The loop (src/model.ts) enforces:
- 4,000 completion tokens per turn
- 200,000 total session tokens
- 150,000-token compaction trigger (older turns are summarized by the model once)
- $5.00 per session
- 50 turns
Exceeding any ceiling fails the session. Model usage is exported as OTLP gen_ai.* spans when Langfuse credentials are present (LANGFUSE_PUBLIC_KEY, LANGFUSE_SECRET_KEY, LANGFUSE_OTLP_ENDPOINT or LANGFUSE_BASE_URL).
On success in sandbox mode, publishSuccess (src/success.ts) commits and pushes the agent/<id> branch, then opens a pull request using a GitHub App installation token (AGENT_GITHUB_INSTALLATION_TOKEN; repository contents + pull-request write permissions). The PR body contains the final plan and a bounded trace bundle.
The same file/shell tool surface is exposed as a StreamableHTTP MCP server:
bun run mcp -- ./repoListens at http://127.0.0.1:3001/mcp (override with MCP_PORT).
src/
index.tsx CLI entry point + Ink terminal UI bootstrap
app.tsx terminal UI (plan, tool stream, budget meter)
session.ts session orchestration: surface selection, dispatch, publishing
model.ts agent loop, budgets, history compaction (OpenRouter)
registry.ts tool definitions shared by dispatcher, model, and MCP
tools.ts host tool surface + path-confinement + output bounding
sandbox.ts E2B sandbox: VM lifecycle, worktree, sandbox tool surface
hooks.ts HookRunner, lifecycle events, command guards
dispatcher.ts tool routing, PreToolUse/PostToolUse, plan store
plan.ts TodoWrite tool, plan validation + atomic persistence
events.ts EventBus (buffer + replay for the UI)
success.ts PR publishing via GitHub App installation token
telemetry.ts OpenTelemetry OTLP export + gen_ai spans
mcp-server.ts StreamableHTTP MCP server for the tool surface
*.test.ts unit + integration tests (bun test)
bun run check # tsc --noEmit
bun test # 34 tests across 10 suitesCoverage includes path-escape attacks (host + sandbox), symlink escape, the destructive-command and push-to-main guards, end-to-end env-configured hooks, plan validation/persistence, dispatcher routing, and sandbox surface behavior.
MIT © 2026 itsmiladlotfi