Your terminal was designed for a human typing one command at a time. Your actual workload is now five Claude Code sessions, a Codex agent grinding a backlog on a schedule, and a review pass you'd like to trust — and the terminal shows you a blinking cursor.
TerMinal is a macOS app that hosts the real agent CLIs — Claude Code, Codex, Cursor Agent, plus OpenRouter and Hermes harnesses for everything else — and wraps them in the management layer an agent workforce needs: a backlog agents own, schedules that fire with the app closed, a review surface for every PR, a cost ledger for every run, and one inbox for the moments that genuinely need a human.
One rule holds the whole thing together: agents never merge. They design,
ticket, branch, implement, open the PR, and stop. The merge to main is yours,
every time.
And it's local-first, all the way down. No server, no account, no telemetry.
Sessions run on your claude/gh/glab auth. Every artifact — activity
feed, run logs, schedules, review results, the Inbox — is a file on your
machine that renders offline.
macOS-first (why). Open source, MIT.
![]() |
![]() |
![]() |
![]() |
- Real CLIs, real PTYs. Each session runs the actual engine binary in its own pseudo-terminal (the same xterm.js + node-pty pattern as VS Code's terminal). Nothing is reimplemented or proxied; resume uses each engine's native resume.
- Workspaces as tabs. Repos across the top, sessions within each, split and 4-up grid layouts across repos, ⌘K palette to jump anywhere. Terminals stay mounted when you switch tabs — a session never drops.
- A cockpit per session. A sidebar of live, per-session telemetry: context
window %, token burn, your plan's 5-hour/weekly usage (a live
/usagemirror), what the agent is doing right now, its todo list, git state, and the latest TDD/code-review verdict. Every number describes one session, never an aggregate. - Five engines, one launcher. Claude, Codex, and Cursor run interactive sessions and agent jobs; OpenRouter and Hermes run agent jobs on any model slug — with a curated menu, per-run cost capture, and your API key sealed in the OS keychain.
- Tickets agents can own. Markdown backlog by default; GitHub Issues or Linear as drop-in providers; team boards embed read-only. Every ticket has an owner agent, acceptance criteria, and links to the runs and PRs it produced.
- Agents are the unit of work — a roster of classic (prompt/script) and
persistent (memory-backed) agents with model policy, deterministic checks,
optional LLM judges, and per-repo overrides in
.agents/. Code-changing runs get an isolated git worktree; nothing touches your checkout. - One ledger for runs. In-process, scheduled, background, and terminal-launched runs normalize into a single view: live log, lineage back to the ticket and PR, evaluation results, dollar cost, rerun/cancel.
- Schedules that outlive the app. Local schedules are real launchd jobs (a headless runner ships in the bundle); schedules can also target always-on Linux hosts over SSH — systemd timers or k8s CronJobs — so the factory keeps running when your laptop doesn't. A failed run files itself to the inbox.
- PR/MR review built in.
gh/glabauto-detected per repo, full diff viewer (unified/split, per-file viewed-state), review findings and suggestions, forge CI status, and a merge button that is deliberately the only one in the app. - Loops for goal convergence (experimental — enable in Settings → Experimental). Pair a driver session that writes a gradable contract and adversarially grades, with a worker session implementing in a worktree — or run single-session mode where fresh evaluators grade a long-lived generator. The generator never grades itself.
- Observability you can act on. Cross-repo throughput, cycle time
(ticket-filed → PR-merged with stage splits and a funnel), success rates, an
AI-spend explorer down to per-request payloads, and a
/factoryorchestrator that works the backlog until it's dry.
- One inbox for everything human. Approvals, credentials, decisions, hard blockers — agents file items to a global Inbox from any repo, each pings Telegram, and the app badges the count until you resolve it.
- AFK control from your phone. The Telegram bridge is two-way:
/feature <idea>drafts a ticket and offers a start-work button;/runs,/tail,/inbox,/canceland friends steer the factory while you're away. - Merges stay yours. Agents stop at "PR open" by design, and the tooling itself enforces it — a prompt can't talk an agent past the gate.
Requires bun and at least one engine CLI on your PATH
(claude or codex).
git clone https://github.com/trevormil/TerMinal.git
cd TerMinal
git submodule update --init # optional: vendored references (vendor/)
bun install # also rebuilds node-pty against Electron's ABI
bun run devFirst launch is a two-step setup: step one probes your machine (which of
claude/codex/cursor/gh/glab are installed and authenticated), confirms
your projects folder, and lets you pick a default agent engine; step two offers
the optional connections — Telegram, the MCP server, the activity hook, an
OpenRouter key. Everything has a working default and both steps are skippable.
A one-time orientation screen then maps the tabs, and each repo gets its own
orientation on first open.
From the session picker: resume an existing session, start a new one in any
folder, scaffold a new project from the template, or launch a paired
loop. gh and glab are optional — they light up the forge features that
use them.
Platform: macOS (Apple Silicon). Packaging, launchd scheduling, and the editor/browser handoffs assume macOS. Dev works on Linux minus local schedules (use remote hosts). Rationale and a port checklist: ADR-0003.
Download the latest .dmg →
(Apple Silicon). Every release ships the DMG plus a SHA256SUMS.txt.
Releases are signed with a Developer ID and notarized, so a normal double-click opens them — no Gatekeeper right-click dance. Verify the checksum anyway; it is cheap and it is the one check that does not depend on trusting the signature:
shasum -a 256 -c SHA256SUMS.txt # expect: TerMinal-<ver>-arm64.dmg: OK
spctl --assess --type execute -vv /Applications/TerMinal.app # → acceptedIf the checksum does not match, do not open it. Signing is conditional on the
release pipeline having the Apple credentials: a fork (or a tag built without
them) falls back to an unsigned DMG, which still runs but needs right-click →
Open the first time and cannot deliver macOS notifications. See
docs/setup.md.
bun run release # build → sign (if you have a Developer ID) → notarize → install → relaunchbin/release picks its path from what your keychain actually holds: with a
Developer ID it signs and notarizes (which needs an App Store Connect key — it
aborts rather than shipping a signed-but-un-notarized build); with no identity,
or with TERMINAL_UNSIGNED=1, it ad-hoc signs a local build instead. Ad-hoc is
fine for development, but macOS will not deliver notifications from it
(Electron 42+).
Details: docs/runbooks/build-and-release.md.
The loop TerMinal is built to run, end to end:
- File it. A ticket lands in the repo's backlog — typed in the Tickets
tab, filed by an agent, or texted in via Telegram
/feature. It gets an owner agent and acceptance criteria. - An agent picks it up. On demand, on a schedule, or via the
/factoryorchestrator. The run gets its own git worktree and branch; the run record starts streaming into Runs. - TDD-first implementation (the project-template workflow): failing test → code → suite green → push → PR opened with the ticket linked.
- Review to a bar. A code-review agent scores the diff and writes findings
into the repo's
.reviews/; the MRs/PRs tab renders verdict, findings, and the diff side by side. Deterministic checks and optional judges run per the owning agent's contract. - You merge. The one step no agent performs. Post-merge, ticket state reconciles automatically and cycle-time metrics update.
Anything that stalls — a credential, an approval, a failed scheduled run — files itself to the inbox and pings your phone instead of dying silently in a log.
Tabs are repo-aware and curated: the defaults stay focused, the rest enable in Settings → Tabs.
| Surface | What it's for |
|---|---|
| Terminal | The engine CLI plus the per-session cockpit. |
| Tickets | Browse/filter/create; inline status edits write back to markdown. Provider per repo: local, GitHub, Linear, webview. |
| MRs / PRs | Live forge requests with diff, findings, CI state, and the merge button. |
| Agents | The roster: definitions, model policy, contracts, run history, one-click ticket implementation. |
| Runs | Every run with log, lineage, evaluation, and cost. |
| Schedules | launchd/systemd/k8s-backed cadence with run history and a reconcile that surfaces dark jobs. |
| Monitoring | Deterministic infra checks — HTTP, TLS-cert expiry, TCP, DNS, shell — on their own intervals, grouped by status. No inference, and it flags its own daemon going stale so a frozen green can't read as healthy. |
| Observability | The AI-spend and trace explorer — every request, priced. |
| Reports | Scheduled-agent output: reports/<kind>/<sha>.md grouped by agent, with each run's status, metrics, and PR/ticket links parsed out of its frontmatter. |
| Search | One query across the workspace — files, tickets, MRs, docs, runs, activity, snippets, agent artifacts — with click-through to the owning tab. |
| Sessions | The repo's session history: /session-start working notes plus a search over the raw engine transcripts. |
| Docs | GitBook-style reader over docs/, grouped by category (Changelog, Maintainer, Developer, Personal, Other) with rendered markdown. |
| Activity | The chronological event feed — runs, tickets, PR verdicts, deploys, errors — each row click-through to the surface it came from. |
| Inbox | The global, cross-repo HITL queue: decisions, approvals, credentials, failed cron jobs. Badge counts unread and always visible. |
| Agent Config | An editor over ~/.claude and ~/.codex — your global agent config, edited in place instead of in a side terminal. |
| Panels | Pin arbitrary web dashboards (Grafana, a status page, anything) and view them embedded. Hidden until you configure one. |
| CI / Browser / Files / Notes | Forge CI page, embedded webview, a CodeMirror editor with project search, autosaving markdown notes. |
| Inbox (top-right) | The global Inbox — any category of item needing your eyes; badge always visible. |
Not every surface is on by default. Activity, Docs, Notes, Reports, Sessions, Agent Config, and Help ship hidden; Panels appears only once you configure one. All of it is one toggle away in Settings → Tabs.
Everything user-facing is a folder or a JSON file — no plugin API to learn beyond one object shape.
A widget is a folder under src/renderer/src/plugins/<id>/. It renders in
the cockpit by default; the work column's accordion hosts the same spec for the
few widgets listed in COLUMN_PLUGIN_IDS (Tickets, PRs/MRs), and each widget
belongs to exactly one host:
import { Brain } from 'lucide-react'
import { Card, Big, Gauge } from '../../components/ui'
import type { Plugin, TranscriptStats } from '../../lib/types'
const plugin: Plugin<TranscriptStats> = {
id: 'context',
title: 'Context Window',
icon: Brain,
intervalMs: 2000,
defaultEnabled: true,
realtime: true, // also refresh the instant the transcript changes
poll: (gt) => gt.transcript(),
render: (d) =>
d?.ok ? (
<Card icon={Brain} title="Context Window">
<Big value={`${d.contextPct.toFixed(1)}%`} />
<Gauge pct={d.contextPct} />
</Card>
) : null,
}
export default pluginA tab is the same idea:
src/renderer/src/tabs/<id>/index.tsx exporting
{ id, title, icon, order, appliesTo(ctx), Component }. appliesTo gates it
per repo; an optional badge(gt) paints a live count.
A command widget needs no code at all — global
(~/.config/TerMinal/widgets.json) or per-repo
(<repo>/.TerMinal/widgets.json):
[
{
"id": "uncommitted",
"title": "Uncommitted",
"command": "git status --porcelain | wc -l | tr -d ' '",
"intervalMs": 4000,
"mode": "big"
}
]Trust: command widgets run shell commands. Global ones (
~/.config/TerMinal/) are your own files and run freely. Per-repo ones (<repo>/.TerMinal/widgets.json,tabs.json) are inert until you approve them. The first time you open a session in a repo that defines any, the Plugins drawer shows the literal commands and asks you to approve that repo. The approval is keyed on the exact command set, so editing the file — or agit pullthat changes it — asks again.
Agents integrate from the outside too: an MCP server (installable from
Settings or onboarding) gives any Claude Code/Codex session cross-session views
— tickets, runs, Inbox, activity — and the append-only stores under
~/.config/TerMinal/ mean a CI job or shell script can join the activity feed
by appending a line.
TerMinal ships its project template embedded at templates/project-template
— a scaffold carrying the whole workflow: sessions → tickets → branches → PRs
→ review → human merge, with the TDD gate, cadence checks, and the schemas
these tabs read. It versions with the app itself (one repo, one history). What
it puts in your repo is deliberately small — agent contracts, CI and docs.
Skills come from the global tm plugin and workflow state lives in a
per-project sidecar, so neither is copied per repo.
- From the picker: "New project from template" — name it, pick a parent,
Create. Fresh directory,
git init, first commit, session opened, per-repo orientation shown. Details:docs/runbooks/new-project.md. - From the shell:
bin/new-project my-app [parent-dir].
Existing repos adopt the same workflow via the in-app Bootstrap banner (or the per-repo orientation's Setup row) — your files are never clobbered; legacy per-repo skill copies (Claude and Codex) are moved to a backup, since the workflow now ships globally as the tm plugin. Tickets and reviews already committed in a repo keep working and stay visible; Settings → Updates offers the one-time move out of the checkout when you want it.
The gear icon covers the rest, saved to ~/.config/TerMinal/settings.json:
projects/worktrees paths, engine paths + default engine, forge preference,
Telegram, ticket providers, remote SSH hosts, appearance, tab visibility. The
full walkthrough — GitHub vs GitLab, global skills, Telegram, the
activity-feed contract — is docs/setup.md.
| env var | default | what it does |
|---|---|---|
GT_CLAUDE_BIN |
claude |
Claude binary to launch (Settings → Engines also sets this) |
GT_CONTEXT_LIMIT |
auto | context-window cap; auto = 200k, bumps to 1M past 200k tokens |
Electron in three layers: main owns PTYs, filesystem, and CLI calls;
preload exposes one typed gt bridge; the renderer is React 19 +
Tailwind v4. Cockpit data comes from the session's own transcript on disk; plan
usage mirrors the /usage endpoint with the OAuth token Claude Code already
stores in your keychain; review state reads the project sidecar's artifacts.
Scheduling routes per schedule to launchd (local), systemd, or k8s (remote
hosts). The full map, including the loop engine and the multi-session model:
docs/architecture.md.
src/main/ Electron main: PTY spawn, IPC, all fs/CLI readers
src/preload/ the typed `gt` bridge (contextBridge)
src/renderer/src/
App.tsx multi-session shell (workspace tab bar)
SessionView.tsx one session: terminal + work column + cockpit + tabs
plugins/<id>/ one folder = one widget (auto-discovered; hosted by the
cockpit, or by the work column's accordion)
tabs/<id>/ one folder = one full-screen tab (auto-discovered)
bin/ headless runners: terminal-cron, terminal-cli, MCP server, or-agent tier
templates/ project-template (embedded workflow scaffold)
Static HTML in landing/, deployed to GitHub Pages from main
(.github/workflows/pages.yml). Update landing/index.html, push, done.
It's just code — fork it, drop a plugin or tab folder in, send a PR.
bun run test runs the suite; bunx tsc --noEmit is the type gate; CI runs
both. Merges to main are human-only here too.
MIT © Trevor Miller





