ai-gantry
gantry (n.) — the rigid frame that holds and positions tools. The frame does nothing by itself; the tools and the memory do the work. That frame is this binary: an AI harness around one model so an agent can plan on a long horizon.
The product is the crane. A Distroless Go process you run. One
persona. One OpenAI-compat model — Gemini, ChatGPT, Grok, or Ollama.
Chat dials out. Memory you can sqlite3. Aims that still exist on
Thursday. /new distills; it does not lobotomize.
static binary + persona + any OpenAI-compat LLM → outbound chat
Nothing listens. There is no dashboard in the thing you talk to. Health is an exit code, not a port. Chat, memory, cron, and web search work with zero extra tools — MCP is a grant you add later.
The engineering budget went into the loop: parallel tool batches, repairs so a small local model can finish a turn, context that does not rot, personality that survives a reset. Console, metrics, and fleet live one layer up in gantree — the shipping yard — and never sit in a chat turn. The mouth we run is gantry-pendant (phone PWA) and gantry-cab, a native Android app on that same backend. Android Auto is one surface of Cab, not the whole app. Goals, todos, settings, and voice live there. Telegram stays an option.
If you need a team workspace on day one, this is the wrong repo — and that's fine.
The family
Four public repos. This one is the product. The others exist so you can operate it and talk to it without anyone sitting in a token path.
Telegram, Discord, and Slack are vendor mouths: the crane dials them
directly. Pendant and cab go through the mailbox we own. MCP binaries
are optional children on PATH — not a fifth product.
How the wires and the three doors line up: docs/ecosystem.md.
flowchart LR
subgraph mouths["Mouths we own"]
PWA["pendant PWA"]
CAB["gantry-cab"]
end
TG["Telegram / Discord / Slack"]
DO["gantry-pendant mailbox"]
subgraph crane["ai-gantry"]
K["gantry"]
MCP["optional MCP"]
end
Y["gantree"]
LLM["OpenAI-compat LLM"]
PWA -->|"wss in"| DO
CAB -->|"wss in"| DO
K -->|"outbound wss"| DO
K -->|"outbound"| TG
Y -.->|"files + docker"| K
K --> LLM
K --> MCP
Hello (Docker)
You need Docker, a chat mouth, and a model.
- A Gemini API key or any OpenAI-compatible endpoint (Ollama, xAI, …).
- A Telegram bot token from @BotFather and your
numeric user id (e.g. @userinfobot).
Discord and Slack work too — same compose file, different env.
Pendant is
CHANNEL=pendantafter the Worker exists.
docker pull shotah/ai-gantry:latest
docker run --rm shotah/ai-gantry:latest version
git clone https://github.com/shotah/ai-gantry.git && cd ai-gantry
make example-docker
cd examples/docker
# set in .env:
# GEMINI_API_KEY=... # or LLM_BASE_URL + LLM_API_KEY + LLM_MODEL
# TELEGRAM_BOT_TOKEN=...
# TELEGRAM_ALLOWED_USERS=... # numeric id, allowlist only
make up
make logs
Message the bot, then send /status. Walkthrough:
docs/deploy-docker.md.
Images: shotah/ai-gantry:latest (current main), :edge (main), :0.x.y (pin).
Same tags on ghcr.io/shotah/ai-gantry.
Drop in a different model
The agent does not care which vendor you picked. Set these three and restart:
# examples/docker/.env — Gemini spelled out, for instance
LLM_BASE_URL=https://generativelanguage.googleapis.com/v1beta/openai
LLM_API_KEY=...
LLM_MODEL=gemini-3.5-flash
Other ways to run
Cookbook: examples/README.md. A full life-stack (persona + MCP + compose) lives in a consumer repo, not this harness.
Chat is the console
Ops live in the same chat you already opened. No second UI in this
binary, no inbound port. Type /help anytime. Graphs, a board of
agents, MCP grants without SSH — that is
gantree. It never sits in a
chat turn.
Pendant is the mouth
(CHANNEL=pendant): goals, todos, settings, and voice on a phone we
own. Cab is the native Android app on that same backend, Android
Auto included. Telegram, Discord,
and Slack stay vendor options (one CHANNEL per process). Headless
OAuth: docs/auth.md.
Two files, not a catalog
Long-horizon means the person is still there tomorrow. Most agents feel
like someone after a long chat, then /new wipes them.
MCP tools are not listed in PERSONA.md. They come from the live catalog
(/tools, this turn's schemas, [mcp prefixes]). Keep PERSONA.md short
(examples over rule dumps) or the middle of it gets ignored. Progress logs
and dated to-dos are memory / cron, not persona.
The shipped seed is built around goals: you name the aim, the agent nudges toward it where today's tools disagree with it, does the legwork (the flight, the event, the reminder) the same turn, and treats an empty day as a hole to fill — not "nothing today."
How to write one: docs/persona.md.
The rules behind the seed: docs/persona_doc_goals.md.
SELF.md drift: docs/troubleshooting.md.
Tested two ways
Seven scenarios from the goals doc, N runs each, every run must pass; a rule that holds two times in three is a rule the persona is not carrying. Setup: docs/eval_setup.md.
Read next
The harness is a small static Go binary. Tools are optional MCP processes.
We spent the budget on the loop so a small local model can finish a
tool turn instead of ERROR — that's the production story, not a requirement
to start. Long-horizon planning is the reason the loop, memory, cron, and
SELF.md exist.
License
MIT — see LICENSE.