Command: frontier-agent (compatibility alias: apodex; module: python -m apodex).
Point it at a local repository and give it work in plain language: Stateful ReAct reads, searches, edits files, runs commands, and iterates on failures. Agent Team adds a coordinator, task board, parallel sub-agents, report collection, and synthesis.
Thinking and tool calls stream as they happen. Anything that writes shows a
unified diff and waits for approval. Every action lands in a local JSONL trace,
every file write is snapshotted so /revert can undo the whole session, and
--resume picks a session back up after Ctrl-C.
| Reused | From |
|---|---|
| The ReAct engine (loop, streaming, compaction, guardrails) | frontier_agent.core.runtime.loop.agent_loop.run_agent_loop |
| Observer contract (streaming, approval, path rewriting) | frontier_agent.core.loop_types |
| Robustness observers | frontier_agent.components.observers (TextRepetitionGuard, LeakedToolCallRetryObserver) |
| LLM binding, streaming, retries, and response normalization | frontier_agent.core.runtime.loop.llm_client |
| Workflow prompts and profiles | workflows.stateful_react_agent and workflows.agent_team |
| Editing and web tools | plugins.tools (write_file, file_editor_*, web_search, web_fetch) |
bash, read_file, grep_search, glob_search and delete_file are
reimplemented in local_tools.py: they work against your actual working
directory and skip whatever your .gitignore declares, which the shared
sandbox-backed variants are not built for.
The UI stack is intentionally frozen to two layers:
- Textual owns the interactive full-screen application: layout, input, workers, modal approval and responsive terminal behaviour.
- Rich owns reusable renderables such as Markdown, diffs and panels, and
the line-mode fallback used by
--no-tui, one-shot and piped runs.
Urwid, PyTermTk, PyTermGUI and web dashboard frameworks are alternatives to this stack, not add-ons. They are not dependencies: adding a second widget or event-loop system would duplicate input, rendering and tests without adding a required capability. Revisit this decision only if a confirmed product requirement cannot be implemented with Textual and Rich.
This is the part worth reading before pointing it at a real repository. The strategy is resolved once at startup, and announced if it is not isolating you.
| Platform | Strategy | What it means |
|---|---|---|
| Linux | native (default) | Runs as the current user. Run records stay under <project>/.apodex/runs; runtime caches and temporary state stay under <project>/.apodex/runtime/native. This is not an OS sandbox. |
Linux with --bwrap |
bubblewrap jail | Explicit isolation option: working directory bound read-write at its own absolute path; the system read-only; the rest of $HOME hidden. Requires bubblewrap and usable user namespaces. |
| macOS with Docker | container | The whole CLI re-executes inside the repo's Docker image. Your project is mounted at /project, run-private scratch at /workspace, final workflow artifacts at /outputs, and ~/.apodex is bind-mounted. |
| macOS without Docker | native | Starts directly without requiring Docker. Run records stay under <project>/.apodex/runs; runtime caches and temporary state stay under <project>/.apodex/runtime/native. Commands still have the permissions of your macOS user and remain approval-gated; this is not an OS sandbox. |
Native mode is announced on every run so it is never mistaken for an OS
security boundary. Approval, path authorization, journaling, and /revert
remain active, but native shell commands have the current user's permissions.
APODEX_SANDBOX accepts native, bwrap, host or container to override
the resolution. Note that a Linux host can ship bwrap and still refuse to
mount a fresh /proc — common inside an unprivileged container — so explicit
bubblewrap mode probes with the real arguments instead of assuming the binary
is enough.
Run from the repository root, so frontier_agent, plugins and workflows
import:
uv sync
cp .env.example .env
# Add your own model credentials to .env:
# OPENAI_API_KEY=...
# OPENAI_BASE_URL=https://api.openai.com/v1
# OPENAI_MODEL=gpt-4o
# research mode additionally wants SERPER_API_KEY / JINA_API_KEY
# Interactive TUI, Stateful ReAct, against another repository
frontier-agent --mode react --cwd /path/to/your/repo
# Coordinator with parallel sub-agents
frontier-agent --mode agent_team --cwd /path/to/your/repo
# One-shot: run, print, exit
frontier-agent --cwd /repo -p "explain src/foo.py"
# Auto-approve every tool call (batch / trusted use)
frontier-agent --cwd /repo --yes "add a --verbose flag to the CLI"
# Resume (the session id is printed at startup and by /log)
frontier-agent --resume 20260804-153000-coding-ab12
# List sessions that can be resumed
frontier-agent --resume
# Run the whole CLI inside Docker
frontier-agent --docker --cwd /path/to/your/repo
# Force the workspace-local native runtime (already the Linux default; macOS
# uses it automatically when Docker is unavailable)
frontier-agent --native --cwd /path/to/your/repo
# Explicit Linux bubblewrap isolation
frontier-agent --bwrap --cwd /path/to/your/repo
# Start with read-only PDF/image inputs (repeat --input as needed)
frontier-agent --cwd /path/to/your/repo \
--input ~/Downloads/claim.pdf --input ~/Desktop/photo.jpgIn the full-screen TUI on macOS, Ctrl+V or /paste reads Finder file
selections and image data from the system clipboard. Absolute-path text is
attached through the same session input manager; ordinary text is inserted into
the prompt. Cmd+V remains the terminal's normal text paste shortcut.
This is a local, open-source BYOK tool: there is no account or login command.
Keys stay in your environment or local .env; the TUI never asks for or displays
them. Startup validates the local configuration before opening the TUI, and
/config shows only safe diagnostics such as provider, model, endpoint host and
whether the required key is configured.
The first --docker run builds the image, which takes a few minutes
(LibreOffice and the document readers are large); later runs reuse it.
APODEX_IMAGE overrides the tag.
On Linux, native mode is the default. On macOS, Docker remains preferred when
its daemon is reachable, with automatic fallback to native mode. Native mode
redirects common Python, Node, Rust, Go, and Ruby caches/state into
.apodex/runtime/native; the CLI's already-installed Python environment is reused
read-only from its install location. Heavy scientific, plotting, spreadsheet,
and document packages are optional: the agent installs only a package required
by the current task, and native Python installs land in
.apodex/runtime/native/home/.local/site-packages.
| Mode | Tools | Prompt |
|---|---|---|
| react (default) | stateful web, shell, and file tools | focused single-agent research and file work |
| agent_team | coordinator tools plus bounded sub-agent tools | decomposition, parallel investigation, collection, and synthesis |
/workflow react and /workflow agent_team switch workflows in the TUI.
| Option | Meaning |
|---|---|
task (positional) |
the task; omit it for the interactive REPL |
--mode react|agent_team |
workflow mode (default react) |
--resume [id] |
list saved sessions without an id; restore history, changes, mode and working directory with one |
--model |
model id (defaults to $OPENAI_MODEL / $APODEX_MODEL) |
--cwd |
working directory the agent operates in |
--input PATH |
attach a file or directory as a read-only session input; repeatable |
--max-turns / --max-tokens |
turns per task (50) / output tokens per call (8192) |
-y, --yes |
auto-approve every tool call |
-p, --print |
one-shot: run, print, exit |
--plan |
plan mode: investigate and propose first; edits locked until approved |
--docker |
run the whole CLI in a container (implied on macOS) |
--native |
use the workspace-local native runtime (default on Linux) |
--bwrap |
require the optional bubblewrap filesystem jail on Linux |
--no-sandbox |
run commands directly on this machine |
--theme <name> / --no-color |
application palette (defaults to Catppuccin); supports dark, light, Tokyo Night, Dracula, Nord, Gruvbox, One Dark, and Solarized variants |
--no-tui |
plain line-mode UI instead of the full-screen TUI |
The full-screen UI is selected only when stdin and stdout are both terminals.
Pipes, TERM=dumb, NO_COLOR (including an empty value), --theme mono,
--no-color, --no-tui, and one-shot runs use line mode automatically. SSH
and tmux use the full-screen UI when they expose a normal TTY and terminal type.
This makes line mode the dependable fallback rather than an error path.
In line mode a palette changes FrontierAgent's ANSI output only; the terminal
emulator still owns its own background and full colour scheme. Use the
full-screen TUI to apply the palette to the complete application surface.
One palette per theme (apodex/tui/themes.py) drives both surfaces, so the
line UI and the TUI can't disagree about what a theme looks like, and selecting
a theme recolours everything rather than only the widget chrome.
Each upstream palette is the source of truth. Gruvbox's orange is #fe8019
and Solarized's yellow is #b58900 because that is what makes them those
themes. Semantic colours are used verbatim unless they fall below 3:1, and are
then corrected by the smallest lightness step that clears it — hue and chroma
are never touched. 68 of 84 are untouched.
The three text tiers come from each designer's own ramp (gruvbox fg/fg3/fg4,
Solarized base1/base0/base00, Catppuccin text/subtext0/overlay2),
which is why quiet text keeps the palette's cast — gruvbox warm, Solarized teal
— instead of going grey. Floors match what each role actually is: foreground
≥ 6:1 and muted ≥ 4.5:1 are body text and owe WCAG AA, while subtle and the
semantic colours are short bold labels, glyphs, borders and diff markers, so
they owe 3:1 (WCAG 1.4.3 / 1.4.11). apodex/tests/test_themes.py checks the
floors and asserts the semantic colours still match upstream byte-for-byte —
holding accents to the 4.5:1 body-text floor is what once turned Catppuccin
Latte's amber into a dark brown, and contrast alone cannot catch that.
Two rules follow from that and are worth knowing before editing colours:
- No Rich
dim.dimis a terminal-side blend by an unspecified amount, so it destroys a measured ratio — it was why thinking text collided with the background on darker palettes. Quiet text usesmuted/subtleinstead. - No colour emoji in the UI. A colour emoji paints itself and ignores the
surrounding foreground, so it can't follow a theme. Every glyph is a
monochrome character from the shared
GLYPHSvocabulary and takes the active theme's colour like any other text.
dark and light are our own palettes, not Textual's built-ins — those had no
Rich half, so the transcript fell back to Catppuccin and painted dark-theme
colours onto a light background.
The right-hand workspace has three tabs by default, with Plan selected. A fourth Diff tab appears as soon as the session has file changes:
- Plan — the todo list, or Agent Team's current task board.
- Activity — a selectable tool timeline. Arrow keys move between calls; Space or a mouse click opens the complete call details.
- Files — final reports and workflow deliverables. Arrow keys select a file;
Space or a mouse click previews source code, Markdown, PDF documents, Office files (
.docx,.xlsx,.pptx), Jupyter Notebooks (.ipynb), 3D/macromolecular structures (.pdb,.stl,.obj,.gltf), images (ANSI pixel art), and archives (.zip,.tar). - Diff — every change accumulated since the session first touched each
file, rendered as a colourized unified diff and scrollable with the arrow /
page keys. File tools are journaled by path; a non-read-only
bashcall is journaled by scanning the working directory and the session's outputs directory around the tool phase, so what a shell script wrote shows up too — though/revertleaves those alone, since a scan cannot separate the shell's writes from a concurrent editor's. The tab count and footer show changed files plus total added and removed lines.
When a task completes, the workspace opens Diff when changes exist and Files otherwise. Removing or reverting every change hides Diff again.
Ctrl+Tab / Ctrl+Shift+Tab cycle the tabs, while Ctrl+O jumps directly to
Files (and returns to Plan when pressed again). Each tab receives the full pane
instead of competing vertically for height. Tab labels carry compact counts
(Plan 8/14, Activity 31 ◐1 ✗2, Diff 3) when content extends off-screen. Ctrl+B
hides the sidebar entirely; it also hides itself below 100 columns.
A palette needs colours to exist in. Rich and Textual pick their colour depth
from COLORTERM, then TERM, and never query the terminal. At 8 colours the
themes are not approximated but destroyed: every value snaps to one of eight
ANSI slots your terminal defines, so gruvbox's #ebdbb2 cream becomes pure
white, its muted tan becomes #aaaaaa grey, and its orange and red both become
the same red. That is the "everything is black or grey" failure, and no palette
change can fix it.
apodex prints a warning when it detects fewer than 256 colours. To fix it:
export COLORTERM=truecolorThe containerised path (the default on macOS) forwards your host's TERM and
COLORTERM inward and falls back to a 256-colour floor, because Docker
otherwise sets TERM=xterm inside the container and forwards no COLORTERM at
all — the container's TERM is a Docker default, not a measurement of the
terminal actually painting the pixels. TERM=dumb is still honoured as-is.
/help · /mode <name> · /model <name> · /config (safe local settings) ·
/cwd [<path>] · /new (save and start fresh) · /fork (branch the current
context) · /sessions · /rename <name> · /clear (context and plan) ·
/context (window and cumulative token usage) · /revert (undo every file
change this session) · /log (trace
path) · /auto (toggle auto-approve) · /theme <name> · /exit
Available names: dark, light, catppucin, catppucin-latte,
tokyo-night, tokyo-night-day, dracula, nord, gruvbox,
gruvbox-light, one-dark, one-light, solarized, solarized-light, and
mono. The correctly spelled catppuccin and catppuccin-latte aliases also
work.
/compact creates a fresh context checkpoint: it first condenses every tool
result (keeping useful leading/trailing output and source URLs), then asks the
summary model to produce one concise session summary.
In the full-screen TUI, entering /theme opens a mouse- and keyboard-selectable
theme list. Entering /workflow similarly opens a picker for react and
agent_team; explicit forms such as /workflow react work in both TUI and
line mode.
In the full-screen UI, press F1 for interaction help, Ctrl-P for the command
palette, Ctrl-B to toggle the plan/activity sidebar, and ↑/↓ to revisit
submitted input without losing the current draft. Tab completes an unambiguous
slash command. Approval starts on No; long command and diff previews scroll
with Ctrl-U / Ctrl-D.
- Approval gate. Read-only calls (read/grep/glob/view/web_search/web_fetch,
and read-only bash such as
ls/git status) run straight through. Writing, deleting, installing and dangerous shell need confirmation, which-yskips. A hard-denied command (rm -rf /, writing outside the working directory,xargsfeedingrm) is refused regardless of-y. - Revertible. Every write, edit and delete snapshots the previous content
first, so
/revertrestores the whole session.delete_fileexists as a first-class tool for exactly this reason — prefer it overbash rm. - Local trace. Every LLM and tool action, including refused ones, is written
to
<cwd>/.apodex/runs/<id>/trace.jsonl. - Working-directory boundary. Reads, writes and deletes stay inside the
working directory, and
findprunes the artifact directories your.gitignoredeclares. - Interrupt and resume. Ctrl-C stops the run; state is persisted per turn,
so
--resume <id>continues from the last completed turn.
Pointing --cwd at this repository itself is not the intended use — the file
tools' checkout protection may refuse writes. Use it on another repository.
apodex/
├── cli.py # argparse entry, .env load, sandbox resolution, docker dispatch
├── native.py # workspace-local runtime home, caches, dependencies, inputs
├── sandbox.py # native / bwrap / container / host strategy + execution
├── docker.py # the macOS path: re-exec the CLI inside the repo image
├── config.py # ModelConfig from env (OPENAI_* / APODEX_*)
├── llm.py # terminal LLM construction and provider configuration
├── prompts.py # legacy generic-profile prompts
├── prompts_base.py # shared generic-profile prompt builders
├── profiles/ # terminal workflow selection and compatibility profiles
├── agent_tools.py # tool lists, risk levels, path rewriting, read-only bash test
├── local_tools.py # local bash/read_file/glob/grep/delete_file
├── todo.py # todo_write tool + plan panel state
├── diff_preview.py # unified diff shown before anything is written
├── changes.py # WorkspaceJournal: snapshot / diffstat / revert
├── trace.py # TraceObserver: every action to JSONL
├── observers.py # streaming render, approval, path rewriting, journal
├── render.py # Rich / plain-text rendering
├── session.py # REPL, slash commands, and terminal-session coordination
├── task_runner.py # generic-loop and native-workflow task execution
├── session_state.py # session IDs, checkpoints, and resume listings
├── middleware.py # terminal skill-injection wiring
├── tui/ # full-screen Textual UI
└── tests/ # mock-LLM end-to-end plus journal / trace / persistence units
- No second confirmation for large diffs or deletions beyond the single approval plus diff preview.
- The trace records
is_errorand duration but does not classify failures (timeout vs test failure vs syntax error). - No skills are bundled. The loader is wired, so a profile's
skills:list picks up anyplugins/skills/<id>/SKILL.mdyou add. - The bubblewrap path runs in CI on Linux; the macOS container path has not been exercised on macOS hardware.