gantree

Architecture

Gantree is the operator plane for ai-gantry: a shipping yard, not the chat. The crane is the product — why the harness is worth operating lives in the root readme. What you click on the board: console.md. People (login, profile, settings): operators.md. This page is how the yard is put together — stack, host I/O, and why the console never sits in a chat turn.

Harness-side design note (nested checkout, dev only): repos/ai-gantry/docs/gantree.md.


Operator plane

[ browser ]
     |
     |  localhost | Tailscale | Cloudflare Tunnel | nginx-proxy (console only)
     v
[ gantree  — Vinext on Node, on the Docker host ]
     |
     |  Docker API + files on disk
     v
[ gantry ] [ gantry ] [ gantry ]     Hub image, outbound chat

Agents open zero inbound ports. Only this UI is reachable, and only through a path you chose. Bind 127.0.0.1 by default. Never a public load balancer.

Two install stories, one runtime: Linux + Docker + this process on that host. Home Mini vs cloud VM: install.md.


Why a process on the Docker host — and why Vinext

We need a server that can see Docker from day one. Even the first useful screen is “is Kit up, and what did it just log?” That is docker inspect + docker logs, not a static site.

So we start in a framework. Vinext is the pick: write app/ like Next, run vinext, TypeScript the whole way. The harness stays Go. The dashboard is not Go.

The thing that would be wrong is putting that framework where Docker is not. Vinext’s happy path is Cloudflare Workers. Workers cannot open docker.sock, tail a container, or rewrite mcp.toml on the Mini. v1 is therefore Vinext targeting Node (--platform=node / standalone), running on the Docker host — Mini or a small VM.

Layer Where it lives Why
UI (app/) Vinext / React Board, build crane, Tools, graphs + logs
Host I/O (lib/yard) Node route handlers dockerode, compose, files
Harness shotah/ai-gantry container Chat, memory, MCP children

Docker, compose, and tools-fetch stay in route handlers (or lib/yard), never in a React Server Component. Vinext’s native-addon footgun is real; this is how we don’t step on it.

Later, the same app/ can sit on Workers as a portal that calls this host. That is a second skin, not v1. Tailscale or a Cloudflare Tunnel in front of the Node console is how you reach a cloud VM today.

Avoid: Next-on-Vercel as the host. A SPA plus a mystery API. Harness + console in one Distroless image.

No second language, no gantree CLI until a real gap forces it:

npm start          # vinext start — bind 127.0.0.1
npm run dev        # vinext dev

How gantree sees a gantry

The crane does not grow a /metrics port. Gantree pulls.

What you want Where it comes from
Alive, image, restart Docker inspect / compose
Visual logs (per instance) docker logs stream, structured in the UI
CPU / RAM / net graphs sampled docker stats / cgroup (ring + yard sqlite, 7d cap). Host card rolls containers up by role (agents / dashboard / other).
Turn / token graphs JSON slog turn perf (prompt_est_tokens, gen_est_tokens, iterations, user_id) — same sqlite so a bounce keeps this billing month (local 1st)
Published vs skipped MCP mcp.toml + gantry status JSON (mcp.servers[].reason: no_binary / no_key / no_oauth)
Persona, secrets PERSONA.md, avatar.jpg, .env, data/ on disk
Telegram bot Bot API getMe / setMy* after a token exists. Allowlist is .env. Never getUpdates.

Files remain the source of truth. The UI is an editor of those files, not a second inventory. Secrets never go in git. Console-in-Docker must see those files at the inventory path — uncomment the same-path volume in compose.yml when attaching absolute host dirs (headless.md). Metrics and container env still work without it; persona and mcp.toml do not.

Inventory is gantree.toml (no secrets). One mutation path: the UI calls lib/yard. Never dockerode from a React Server Component.

Meh yard, tight crane. The operator can wait 200ms for a chart. The human on Telegram cannot wait for a serial tool loop. Never add a listen port, a scrape, or a hook that taxes parallel tool calls, Completer rounds, or RSS. If a dashboard want would make ai-gantry slower, the want is wrong.

Recreate / pin keep the crane’s host user (Vinext uid:gid, never image 65532), network_mode, and extra binds. Dropping uid is how session store open failed happens: Distroless cannot write a gantry.db owned by your login.


Repo layout

gantree/                    this repo — shipping yard
├── app/                    Vinext / Next-shaped UI
│   ├── lib/                browser helpers (yardFetch, jpeg, phone frame)
│   └── components/         nested by screen, not by widget type
│       ├── shared/         DoorShell, DashFold, HintField, avatars, EventStrip
│       ├── yard/           YardBoard, BuildCrane, HostCard, SpendBoard
│       ├── crane/          AgentDashboard + folds, Telegram, logs, charts
│       ├── host/           HostDashboard, HostCharts
│       └── operators/      AuthForms, OperatorProfile, YardSettings panes
├── site/                   GitHub Pages source (`npm run pages` → dist/pages)
├── assets/banner.svg       GitHub README banner
├── assets/banner.png       Hub overview (Hub does not render SVG)
├── assets/logo.svg         app icon mark (portal crane)
├── app/icon.svg            tab icon (SVG)
├── app/favicon.ico         tab icon (browsers that still ask for .ico)
├── assets/docs/            console screenshots (shot.mjs) + pitch stills
├── scripts/shot.mjs        headless Chrome recapture
├── scripts/pages.mjs       github.io tree from site/ + docs/*.md
├── scripts/seed.ts         screenshot operators, cranes, observe series
├── lib/yard/               host I/O (not RSC)
│   ├── door/               operators, session, audit events
│   ├── host/               dockerode, identity, stats, files, .env, avatar, telegram, logs
│   ├── crane/              inventory, build, run, doctor
│   ├── tools/              catalog, grant, mcp, auth
│   ├── observe/            stats samples, sqlite memory, spend rollup
│   └── shot/               screenshot yard (`npm run seed` + GANTREE_SHOT)
├── test/                   all tests — mirrors source, never next to it
│   ├── yard/               mirrors lib/yard
│   ├── app/                mirrors app/ (components by screen, lib helpers)
│   └── scripts/            mirrors scripts/
└── repos/                  local nested checkouts (gitignored)
    └── ai-gantry/          harness — own remote, own git
        └── repos/          MCP servers — own remotes

Nested checkouts are for dev. Runtime pins shotah/ai-gantry:latest and speaks the file/env contract. Each nested project keeps its own remote when you push. Do not copy .env or data/ from a private checkout.

lib/yard is the host I/O surface: inventory, build, grant/revoke, doctor, run (start / stop / recreate), logs, stats, auth hop, tools-fetch, the operator door (lib/yard/door, yard gantree.db — operators, sessions, graph samples, audit). Import dockerode from lib/yard, not from app/. Import leaf modules (@/lib/yard/crane/build), not a root barrel.

Tests live under test/. They mirror the source tree and never sit beside production files. test/yard/ covers lib/yard (Node / Docker / files). test/app/ covers the UI and app/lib/ helpers. test/scripts/ covers scripts/. Coverage thresholds apply to lib/yard only.

UI folders match screens (shared / yard / crane / host / operators). Pages stay thin route shells. Do not invent atoms/molecules or a src/ wrap.


Isolation

One human, one bot, one directory, one data/. Gantree does not merge memories or OAuth across gantries. Delete a tryout = delete that directory.

Profiles (slim / life / life-cast) are build-time menus, not a plugin system. Grant is still “listed in mcp.toml.” life-cast is home-only (mDNS / host network). Custom servers: custom-mcp.md.


v1 vs v2 vs v3 vs later

v1: this Node process on the Docker host, or the Hub image shotah/gantree with docker.sock. Board, per-crane dashboard, host page (gantree.toml + yard sqlite), build wizard, MCP toggles, auth hop, start / stop / recreate, image pin. Telegram + Hub shotah/ai-gantry. Bind localhost (LAN publish is a home choice). npm run release tags and publishes the console image (same Hub secrets as the harness).

v2 (delivered): a door on that process (setup + login + session on every API and log SSE), a handful of operators in yard gantree.db (independent of each crane’s gantry.db; admin sees every crane, user / readonly their assigned), board nags for skipped MCP / needs-auth, sqlite so graphs survive a bounce, audit of who mutated what. What that door checks: security.md. Portal (Workers skin) is a parallel / last walk with mobile — not a blocker for calling the door done. Walk: todo.md (v2 looks like).

v3: the yard you live in. Settings beyond people (retain, timezone, default pin, optional $/1M). Richer pull-only graphs (net, disk, turn duration when slog has it). Profile chat ids label spend and can be pushed onto a crane allowlist. Backup is a loop (list / restore / prune); retire a tryout. Compare overlay and a filterable audit. Inventory still toml. Still pull, never a /metrics port on the crane. Mobile / phone layout is someone else’s track. Walk: todo.md (v3 looks like).

Later: systemd yards; a gantree CLI only if npm scripts are genuinely not enough (still TypeScript); Prometheus; billed-provider invoices; SSO.

Not the product: hosted Gantree SaaS, Kubernetes, Cloud Run / Lambda, a shared family brain, pairing the agent through the console, anything that makes the harness slower so this UI looks nicer.

Walk order: todo.md.