trantor 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +19 -0
- package/.claude-plugin/plugin.json +12 -0
- package/LICENSE +21 -0
- package/README.md +210 -0
- package/bin/advise.mjs +147 -0
- package/bin/cli.mjs +53 -0
- package/bin/connect.mjs +94 -0
- package/bin/crew-runner.mjs +140 -0
- package/bin/crew-verify.mjs +40 -0
- package/bin/crew.sh +125 -0
- package/bin/doctor.mjs +74 -0
- package/bin/handoff-prompt.sh +21 -0
- package/bin/open-session.sh +10 -0
- package/bin/profile.mjs +56 -0
- package/bin/relay-watch.mjs +54 -0
- package/bin/statusline.mjs +30 -0
- package/bin/write-handoff.mjs +17 -0
- package/configs/codex-config.toml +6 -0
- package/configs/gemini-settings.json +10 -0
- package/deploy/com.trantor.hub.plist +17 -0
- package/deploy/setup.sh +31 -0
- package/hooks/hooks.json +11 -0
- package/hooks/precompact.mjs +118 -0
- package/hooks/sessionstart.mjs +110 -0
- package/hub.mjs +218 -0
- package/mcp.mjs +156 -0
- package/package.json +54 -0
- package/skills/crew/SKILL.md +82 -0
- package/skills/relay-handoff/SKILL.md +36 -0
- package/ui.html +410 -0
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "agent-bus",
|
|
3
|
+
"owner": { "name": "Sasha Bogojevic", "email": "hello@hivedigitalllc.com" },
|
|
4
|
+
"metadata": {
|
|
5
|
+
"description": "Live message bus, presence, project Kanban + context-handoff for independent AI coding agents (Claude, Codex, Gemini, …)",
|
|
6
|
+
"version": "0.14.0"
|
|
7
|
+
},
|
|
8
|
+
"plugins": [
|
|
9
|
+
{
|
|
10
|
+
"name": "agent-bus",
|
|
11
|
+
"source": "./",
|
|
12
|
+
"description": "Let independent AI coding sessions — Claude Code, Codex, Gemini, any MCP agent — talk to each other live: auto-register, see the live roster, message/coordinate, and watch it all on a project-grouped Kanban dashboard. Includes the relay MCP, a SessionStart auto-discovery hook, and a PreCompact context-handoff so a fresh session can take over a full window instead of compacting.",
|
|
13
|
+
"version": "0.14.0",
|
|
14
|
+
"author": { "name": "Sasha Bogojevic" },
|
|
15
|
+
"category": "development",
|
|
16
|
+
"keywords": ["multi-agent", "coordination", "mcp", "hooks", "kanban", "context-handoff", "message-bus", "claude-code", "codex", "gemini"]
|
|
17
|
+
}
|
|
18
|
+
]
|
|
19
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "agent-bus",
|
|
3
|
+
"version": "0.14.0",
|
|
4
|
+
"description": "Live message bus, presence, project Kanban + crew orchestration for independent AI coding agents (Claude, Codex, Gemini, Kimi, DeepSeek)",
|
|
5
|
+
"mcpServers": {
|
|
6
|
+
"relay": {
|
|
7
|
+
"command": "node",
|
|
8
|
+
"args": ["${CLAUDE_PLUGIN_ROOT}/mcp.mjs"]
|
|
9
|
+
}
|
|
10
|
+
},
|
|
11
|
+
"skills": "./skills/"
|
|
12
|
+
}
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Sasha Bogojevic
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
3
|
+
```
|
|
4
|
+
████████╗██████╗ █████╗ ███╗ ██╗████████╗ ██████╗ ██████╗
|
|
5
|
+
╚══██╔══╝██╔══██╗██╔══██╗████╗ ██║╚══██╔══╝██╔═══██╗██╔══██╗
|
|
6
|
+
██║ ██████╔╝███████║██╔██╗ ██║ ██║ ██║ ██║██████╔╝
|
|
7
|
+
██║ ██╔══██╗██╔══██║██║╚██╗██║ ██║ ██║ ██║██╔══██╗
|
|
8
|
+
██║ ██║ ██║██║ ██║██║ ╚████║ ██║ ╚██████╔╝██║ ██║
|
|
9
|
+
╚═╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═══╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
### The hub-world for AI agent crews.
|
|
13
|
+
|
|
14
|
+
**One Advisor decides how your work runs — solo, cheap inline calls, or a live crew of
|
|
15
|
+
Claude Code, Codex, Gemini, Kimi & DeepSeek in their own terminal windows — routed by your
|
|
16
|
+
actual plans, supervised on a live board, learning from every failure.**
|
|
17
|
+
|
|
18
|
+
[](./LICENSE)
|
|
19
|
+

|
|
20
|
+

|
|
21
|
+

|
|
22
|
+
|
|
23
|
+
</div>
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## Install
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
npm install -g trantor
|
|
31
|
+
trantor setup # hub becomes an always-on service + config + wires your AI CLIs + doctor
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Then give Claude Code (the orchestrator) the plugin:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
claude plugin marketplace add sashabogi/trantor
|
|
38
|
+
claude plugin install agent-bus
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
That's it. (Prefer source? `git clone https://github.com/sashabogi/trantor && cd trantor &&
|
|
42
|
+
npm install && bash deploy/setup.sh` — identical result.)
|
|
43
|
+
|
|
44
|
+
## What to expect on first run
|
|
45
|
+
|
|
46
|
+
`trantor setup` ends with the **doctor** — an honest map of where you stand:
|
|
47
|
+
|
|
48
|
+
```
|
|
49
|
+
TRANTOR DOCTOR
|
|
50
|
+
|
|
51
|
+
core
|
|
52
|
+
✓ node 22.x
|
|
53
|
+
✓ hub up at http://127.0.0.1:4477
|
|
54
|
+
claude (the orchestrator)
|
|
55
|
+
✗ plugin not installed
|
|
56
|
+
→ claude plugin marketplace add sashabogi/trantor && claude plugin install agent-bus
|
|
57
|
+
crew CLIs (install any subset — seats follow the work)
|
|
58
|
+
✓ codex: wired to the bus
|
|
59
|
+
✗ codex: NOT authenticated — it will join the bus but fail on its first turn
|
|
60
|
+
→ codex (sign in with your ChatGPT account on first run)
|
|
61
|
+
– kimi: not installed (optional)
|
|
62
|
+
the brain
|
|
63
|
+
✗ quota profile not set → trantor profile set claude=max codex=plus deepseek=api
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Fix the `→` lines (each CLI's own sign-in happens once, in that CLI), re-run `trantor doctor`
|
|
67
|
+
until it's clean, then open a Claude session in any project and say **"fire up the crew."**
|
|
68
|
+
|
|
69
|
+
Provider API keys (e.g. `DEEPSEEK_API_KEY`) live in one file: **`~/.agent-bus/.env`** — the
|
|
70
|
+
crew runners source it automatically.
|
|
71
|
+
|
|
72
|
+
## What happens when you fire up a crew
|
|
73
|
+
|
|
74
|
+
1. **The Advisor moment.** Your Claude cuts the work into difficulty-tagged packages, calls
|
|
75
|
+
`relay_advise`, and shows you the full picture *before spending anything*: mode
|
|
76
|
+
(`solo | scrooge | crew | hybrid`), a routing table with a **reason per package**, why
|
|
77
|
+
that many seats ("seats follow the work, not the install list"), and a real-money estimate
|
|
78
|
+
with quota-pool accounting. You say go.
|
|
79
|
+
2. **Windows open.** `trantor up codex gemini kimi deepseek:deepseek-v4-pro` spawns one titled
|
|
80
|
+
terminal window per agent (`agent:model` pins a model), **serialized and then verified on
|
|
81
|
+
the bus** — the launcher ends with "crew verified" or names the no-shows loudly. The
|
|
82
|
+
orchestrator never gets a green lie.
|
|
83
|
+
3. **Work flows over the bus.** Contracts arrive as messages; each agent owns its own files;
|
|
84
|
+
coordination happens in <280-char messages you can read on the dashboard. Crew members
|
|
85
|
+
live under a **runner**: the CLI works one turn and exits, the runner long-polls the bus
|
|
86
|
+
for free (it's also the heartbeat) and resumes the CLI — with full context — when the next
|
|
87
|
+
message lands. **Idle agents cost zero tokens and never die.**
|
|
88
|
+
4. **The board tells the truth.** Cards flow `todo → doing → testing → done` — `testing` is a
|
|
89
|
+
real gate (tests/typecheck run there); failures turn the card **pulsing red** until the
|
|
90
|
+
orchestrator bounces them back; demoted cards wear an "↩ bounced" mark with full history.
|
|
91
|
+
5. **It learns.** Failures become lessons (`relay_lesson`), stored on the hub and **injected
|
|
92
|
+
into every future crew's prompts** — global or per-CLI. Your crew gets smarter every run.
|
|
93
|
+
|
|
94
|
+
## The dashboard — `trantor ui`
|
|
95
|
+
|
|
96
|
+
A live command center at `http://127.0.0.1:4477`, grouped by **project**:
|
|
97
|
+
|
|
98
|
+
- **BOARD view** — Kanban with the testing gate, difficulty + model badges per card, agent
|
|
99
|
+
chips with provider logos, live status, and quota-pool tags.
|
|
100
|
+
- **FLOW view** — the same work as an n8n-style dependency graph: parallel streams fanning
|
|
101
|
+
out and converging into integration, edges lighting up as work flows (green = done feeding
|
|
102
|
+
forward, animated blue = active), **red ↩n loops where work bounced back**. Drag nodes to
|
|
103
|
+
rearrange, ⌘+scroll / pinch to zoom, FIT/AUTO controls. Pick per project; your choice sticks.
|
|
104
|
+
- **Per-project conversation lanes** — watch agents negotiate interfaces in context — plus a
|
|
105
|
+
global live feed, and a composer so *you* can message the bus (or any single agent).
|
|
106
|
+
- **🪙 economics pill** — live Scrooge ledger: real spend, savings vs frontier pricing.
|
|
107
|
+
|
|
108
|
+
## The brain — plan-aware economics
|
|
109
|
+
|
|
110
|
+
Trantor's economics engine ([Scrooge](https://github.com/sashabogi/token-scrooge) — installed
|
|
111
|
+
automatically by `trantor setup`) knows model capabilities, per-1M costs, and keeps the
|
|
112
|
+
ledger; Trantor turns that into decisions:
|
|
113
|
+
|
|
114
|
+
- **Declare your plans once:** `trantor profile set claude=max codex=plus gemini=tier kimi=coding-plan deepseek=api`
|
|
115
|
+
- The Advisor routes by **your economics**: API-billed orchestrator → offload everything;
|
|
116
|
+
$20-tier plan → the crew *is* the only way a real build fits; max-tier → context horizon
|
|
117
|
+
decides. **Quota pooling**: one build spread across your separate subscription buckets —
|
|
118
|
+
measured example: a five-vendor 3D game build, API-equivalent $200–600, **actual spend $2.29**.
|
|
119
|
+
- **Fractal delegation** (`relay_scrooge`): the architect *and* crew members push stateless
|
|
120
|
+
grunt work to cheap models, with ledger receipts.
|
|
121
|
+
|
|
122
|
+
## Context handoff — sessions that never hit the wall
|
|
123
|
+
|
|
124
|
+
A PreCompact hook writes a rich handoff before Claude Code compacts; a fresh session in the
|
|
125
|
+
same project **takes over with a brand-new full context window**. Works manually from any
|
|
126
|
+
agent via `relay_handoff`. Optional macOS auto-prompt (`autoHandoffPrompt` in
|
|
127
|
+
`~/.agent-bus/config.json`) offers to open the fresh session for you, with a timeout.
|
|
128
|
+
|
|
129
|
+
Why crews never exhaust the orchestrator: bus messages are **by reference** (~70 tokens),
|
|
130
|
+
work products stay in each agent's own context — the orchestrator burns at coordination
|
|
131
|
+
rate, not work rate.
|
|
132
|
+
|
|
133
|
+
## The tools (MCP — every agent on the bus gets these)
|
|
134
|
+
|
|
135
|
+
| Tool | What it does |
|
|
136
|
+
|---|---|
|
|
137
|
+
| `relay_advise(task, packages, horizon?)` | **The Advisor** — mode + reasoned per-package routing + cost estimate + ready-to-use card args |
|
|
138
|
+
| `relay_send(to, text)` / `relay_inbox` / `relay_wait(t)` | Live messaging: direct, read-new, long-poll wake |
|
|
139
|
+
| `relay_peers` / `relay_status(text)` / `relay_whoami` | Presence: who's alive (honest, heartbeat-backed), doing what |
|
|
140
|
+
| `relay_project_brief(text)` | The project's what/why on the dashboard |
|
|
141
|
+
| `relay_task_add(title, …, difficulty, model, deps)` | Cards with difficulty/model badges + DAG edges for the flow view |
|
|
142
|
+
| `relay_task_move(id, status)` | `todo → doing → testing → done` (the gate), `failed`, `blocked` |
|
|
143
|
+
| `relay_board` | The project's full board, as text |
|
|
144
|
+
| `relay_scrooge(prompt, task?, difficulty?)` | Fractal cheap-model delegation, with the ledger receipt |
|
|
145
|
+
| `relay_lesson(text, scope?)` | Record a failure lesson — auto-injected into all future crews |
|
|
146
|
+
| `relay_handoff(summary)` | Full-window session succession |
|
|
147
|
+
|
|
148
|
+
## The CLI
|
|
149
|
+
|
|
150
|
+
```
|
|
151
|
+
trantor setup | doctor | connect | profile | up <agents…> | down | ui | advise | hub | watch
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
`trantor up` notes: `agent:model` pins a model (`deepseek:deepseek-v4-pro`); spawns are
|
|
155
|
+
verified on the bus with one retry; geometry auto-detects the screen you're working on
|
|
156
|
+
(`CREW_RECT="X,Y,W,H"` to override); `trantor down` kills crew processes via their ttys and
|
|
157
|
+
closes windows without macOS "Terminate?" dialogs.
|
|
158
|
+
|
|
159
|
+
## Works with any MCP agent
|
|
160
|
+
|
|
161
|
+
Claude Code, Codex CLI, Gemini CLI, Kimi Code CLI, OpenCode (DeepSeek) are wired by
|
|
162
|
+
`trantor connect` automatically (idempotent, backed-up, never overwrites your customizations).
|
|
163
|
+
Anything else that speaks MCP: point it at `mcp.mjs` with `RELAY_AGENT=<brand>` — loading the
|
|
164
|
+
server auto-registers the session, so presence works before the model says a word.
|
|
165
|
+
|
|
166
|
+
## How it works
|
|
167
|
+
|
|
168
|
+
```
|
|
169
|
+
Claude (architect/plugin) codex ─ runner gemini ─ runner kimi ─ runner deepseek ─ runner
|
|
170
|
+
│ advise/contracts │ one turn, exit; runner long-polls (free) + resumes with context
|
|
171
|
+
└───────────┬─────────────┴──────────────┴────────────────┴────────────────┘
|
|
172
|
+
▼
|
|
173
|
+
hub.mjs ←— plain HTTP + SSE · presence/messages/board/lessons/economics
|
|
174
|
+
(Node built-ins only · state in ~/.agent-bus/bus.json · loopback by default)
|
|
175
|
+
▲
|
|
176
|
+
dashboard (ui.html) · BOARD/FLOW · conversation lanes · 🪙 ledger
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
Config: `RELAY_URL` env → `~/.agent-bus/config.json` → `http://127.0.0.1:4477`.
|
|
180
|
+
Identity: `RELAY_SESSION` → `RELAY_AGENT:<project-folder>` → `<hostname>:<project-folder>`.
|
|
181
|
+
|
|
182
|
+
**Local-first and safe:** the hub binds loopback; no accounts, no cloud, no exposure. An
|
|
183
|
+
always-on/remote hub (private tailnet, or public with auth) is on the roadmap — never expose
|
|
184
|
+
the hub publicly without auth.
|
|
185
|
+
|
|
186
|
+
*Heritage note: Trantor grew out of **agent-bus** — package internals still carry some
|
|
187
|
+
`agent-bus`/`relay_*` identifiers; they migrate in one planned breaking release.*
|
|
188
|
+
|
|
189
|
+
## Honest limits
|
|
190
|
+
|
|
191
|
+
- You can't interrupt an agent **mid-turn**; messages land when its current turn ends (idle
|
|
192
|
+
agents wake instantly via their runner).
|
|
193
|
+
- Window spawning is macOS (Terminal.app); on Linux the launcher prints per-agent commands
|
|
194
|
+
to run in your own terminals. Hub/MCP/dashboard are cross-platform.
|
|
195
|
+
- The hub is deliberately tiny (in-memory + JSON file) — a coordination bus, not a message queue.
|
|
196
|
+
- Each CLI's sign-in is its own (ChatGPT, Google, Kimi accounts) — the doctor detects state
|
|
197
|
+
and names the fix, but can't log in for you.
|
|
198
|
+
|
|
199
|
+
## Tests
|
|
200
|
+
|
|
201
|
+
```bash
|
|
202
|
+
npm test # unit + 37 protocol-level scenario drills with mock agents (no LLMs, seconds, $0):
|
|
203
|
+
# honest presence, spawn no-shows, the testing gate, bounce trails, lessons,
|
|
204
|
+
# advisor decisions across plan tiers, deps validation, virgin-machine doctor
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
## License
|
|
208
|
+
|
|
209
|
+
[MIT](./LICENSE) © 2026 Sasha Bogojevic · Built with [Claude Code](https://claude.com/claude-code) ·
|
|
210
|
+
Brain by [Scrooge](https://github.com/sashabogi/token-scrooge)
|
package/bin/advise.mjs
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// The Advisor — the brain's front door. Given work packages, decide HOW to execute:
|
|
3
|
+
// solo | scrooge | crew | hybrid — from task shape × plan economics × context horizon.
|
|
4
|
+
//
|
|
5
|
+
// echo '{"task":"build X","packages":[{"title":"engine","difficulty":"hard"},…]}' | node bin/advise.mjs
|
|
6
|
+
// node bin/advise.mjs --demo # canned example
|
|
7
|
+
//
|
|
8
|
+
// Reads (all read-only):
|
|
9
|
+
// ~/.agent-bus/profile.json — the user's declared plans (bin/profile.mjs)
|
|
10
|
+
// ~/.token-scrooge/registry.json — Scrooge's models {cost_in, cost_out, good_for}
|
|
11
|
+
// ~/.token-scrooge/capabilities.json — per-model quality scores
|
|
12
|
+
// Exposed to agents as the MCP tool `relay_advise`; the crew skill calls it at kickoff.
|
|
13
|
+
import { readFileSync, existsSync } from "node:fs";
|
|
14
|
+
import { join } from "node:path";
|
|
15
|
+
import { homedir } from "node:os";
|
|
16
|
+
import { execSync } from "node:child_process";
|
|
17
|
+
|
|
18
|
+
const H = homedir();
|
|
19
|
+
const read = (p, fb) => { try { return JSON.parse(readFileSync(p, "utf8")); } catch { return fb; } };
|
|
20
|
+
|
|
21
|
+
export function loadWorld() {
|
|
22
|
+
const profile = read(join(H, ".agent-bus", "profile.json"), { providers: {} });
|
|
23
|
+
const registry = read(join(H, ".token-scrooge", "registry.json"), { models: {}, tasks: {} });
|
|
24
|
+
const caps = read(join(H, ".token-scrooge", "capabilities.json"), {});
|
|
25
|
+
const has = (c) => { try { execSync(`command -v ${c}`, { stdio: "ignore", shell: "/bin/sh" }); return true; } catch { return false; } };
|
|
26
|
+
const agents = ["codex", "gemini", "kimi", "deepseek"].filter(a => has(a === "deepseek" ? "opencode" : a));
|
|
27
|
+
return { profile, registry, caps, agents, scrooge: has("scrooge") };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const tierOf = (profile, prov) => profile?.providers?.[prov]?.tier || "api";
|
|
31
|
+
|
|
32
|
+
// pick the cheapest Scrooge model that clears the difficulty floor for a task kind
|
|
33
|
+
export function scroogeModelFor(registry, caps, kind = "code", difficulty = "easy") {
|
|
34
|
+
const floor = { easy: 0, medium: 35, hard: 55 }[difficulty] ?? 0;
|
|
35
|
+
const cands = Object.entries(registry.models || {})
|
|
36
|
+
.filter(([, m]) => (m.good_for || []).includes(kind))
|
|
37
|
+
.filter(([id]) => (caps[id]?.coding ?? caps[id]?.intelligence ?? 40) >= floor)
|
|
38
|
+
.sort((a, b) => (a[1].cost_in + a[1].cost_out) - (b[1].cost_in + b[1].cost_out));
|
|
39
|
+
return cands[0] ? { model: cands[0][0], cost_in: cands[0][1].cost_in, cost_out: cands[0][1].cost_out } : null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// crude per-package token forecast (input+output through the executor)
|
|
43
|
+
const FORECAST = { easy: 0.3e6, medium: 1.5e6, hard: 6e6 }; // tokens
|
|
44
|
+
// crew agent preference per difficulty: frontier subs take hard, cheap takes easy
|
|
45
|
+
const CREW_PREF = { hard: ["codex", "gemini", "kimi", "deepseek"], medium: ["kimi", "gemini", "codex", "deepseek"], easy: ["deepseek", "kimi", "gemini", "codex"] };
|
|
46
|
+
|
|
47
|
+
export function advise(input, world = loadWorld()) {
|
|
48
|
+
const { profile, registry, caps, agents, scrooge } = world;
|
|
49
|
+
const pkgs = (input.packages || []).map(p => ({ title: p.title || "work", difficulty: ["easy", "medium", "hard"].includes(p.difficulty) ? p.difficulty : "medium", kind: p.kind || "code", owner: p.owner === "self" ? "self" : (/(foundation|integration|scaffold)/i.test(p.title) ? "self" : "") }));
|
|
50
|
+
const horizon = input.horizon || (pkgs.length >= 4 ? "long" : pkgs.length >= 2 ? "medium" : "short");
|
|
51
|
+
const orchTier = tierOf(profile, "claude");
|
|
52
|
+
const n = pkgs.length, hard = pkgs.filter(p => p.difficulty === "hard").length, easy = pkgs.filter(p => p.difficulty === "easy").length;
|
|
53
|
+
|
|
54
|
+
// ---- mode decision ----
|
|
55
|
+
let mode, why = [];
|
|
56
|
+
if (n <= 1 && hard === 0 && horizon === "short") {
|
|
57
|
+
if (n === 1 && pkgs[0].difficulty === "easy" && scrooge) { mode = "scrooge"; why.push("single small stateless package — a cheap inline call beats spinning up anything"); }
|
|
58
|
+
else { mode = "solo"; why.push("one small package, short horizon — orchestrator handles it directly"); }
|
|
59
|
+
} else if (orchTier === "api") {
|
|
60
|
+
mode = "crew"; why.push("orchestrator is API-billed: every token here is money — be a thin foreman, run all work on crew quotas + Scrooge");
|
|
61
|
+
} else if (orchTier === "capped-sub" && n >= 2) {
|
|
62
|
+
mode = "crew"; why.push("orchestrator is on a capped plan: a real build can't fit in this session — spend the scarce budget on architecture + verification only");
|
|
63
|
+
} else if (n >= 3 || hard >= 1 || horizon === "long") {
|
|
64
|
+
mode = "crew"; why.push(`${n} packages${hard ? `, ${hard} hard` : ""}, ${horizon} horizon — crew isolates work in separate contexts so this session burns at coordination rate, not work rate`);
|
|
65
|
+
} else {
|
|
66
|
+
mode = "solo"; why.push("small enough to do inline without hurting the context horizon");
|
|
67
|
+
}
|
|
68
|
+
// hybrid: crew + easy packages that should be scrooge'd instead of occupying an agent
|
|
69
|
+
if (mode === "crew" && easy > 0 && scrooge) { mode = "hybrid"; why.push(`${easy} easy package(s) routed to Scrooge inline — don't burn a crew seat on grunt work`); }
|
|
70
|
+
|
|
71
|
+
// ---- routing per package ----
|
|
72
|
+
const used = {};
|
|
73
|
+
const routing = pkgs.map(p => {
|
|
74
|
+
if ((mode === "hybrid" || mode === "scrooge") && p.difficulty === "easy" && scrooge) {
|
|
75
|
+
const m = scroogeModelFor(registry, caps, p.kind, p.difficulty);
|
|
76
|
+
const tok = FORECAST.easy;
|
|
77
|
+
const cost = m ? +(tok * 0.6 * m.cost_in / 1e6 + tok * 0.4 * m.cost_out / 1e6).toFixed(3) : null;
|
|
78
|
+
return { ...p, executor: "scrooge", model: m?.model, pool: "api", est_cost_usd: cost,
|
|
79
|
+
reason: `easy + stateless → cheapest capable model (${m?.model}); not worth a crew seat` };
|
|
80
|
+
}
|
|
81
|
+
if (p.owner === "self") return { ...p, executor: "orchestrator", pool: tierOf(profile, "claude"), reason: "architect-owned (foundation/integration doctrine) — the orchestrator keeps the shared contract in its own hands" };
|
|
82
|
+
if (mode === "solo") return { ...p, executor: "orchestrator", pool: tierOf(profile, "claude"), reason: "small enough to do inline" };
|
|
83
|
+
const pref = CREW_PREF[p.difficulty].filter(a => agents.includes(a));
|
|
84
|
+
const agent = pref.sort((a, b) => (used[a] || 0) - (used[b] || 0))[0] || "deepseek";
|
|
85
|
+
used[agent] = (used[agent] || 0) + 1;
|
|
86
|
+
const pool = tierOf(profile, agent === "deepseek" ? "deepseek" : agent);
|
|
87
|
+
let est = null;
|
|
88
|
+
if (pool === "api") { // deepseek API etc — estimate real $ via registry
|
|
89
|
+
const m = registry.models?.["deepseek-v4-flash"] || { cost_in: 0.14, cost_out: 0.28 };
|
|
90
|
+
est = +((FORECAST[p.difficulty] * 0.85 * m.cost_in + FORECAST[p.difficulty] * 0.15 * m.cost_out) / 1e6).toFixed(2);
|
|
91
|
+
}
|
|
92
|
+
const why_r = p.difficulty === "hard"
|
|
93
|
+
? `hard → strongest available coder (${agent}); its ${pool} pool means ${pool === "api" ? "real $ but cheapest capable" : "$0 marginal on existing quota"}`
|
|
94
|
+
: p.difficulty === "medium"
|
|
95
|
+
? `medium → solid mid-tier (${agent}) keeps frontier seats free for hard work; ${pool === "api" ? "metered" : "quota"} pool`
|
|
96
|
+
: `easy → cheapest seat (${agent})`;
|
|
97
|
+
return { ...p, executor: agent, pool, est_cost_usd: est, reason: why_r };
|
|
98
|
+
});
|
|
99
|
+
// crew-size rationale: seats are EMERGENT from the work, and we say so
|
|
100
|
+
const seats = [...new Set(routing.filter(r => !["scrooge", "orchestrator"].includes(r.executor)).map(r => r.executor))];
|
|
101
|
+
const crew = {
|
|
102
|
+
seats: seats.length, of_available: agents.length, members: seats,
|
|
103
|
+
why: seats.length === 0 ? "no crew needed" :
|
|
104
|
+
`${routing.filter(r => seats.includes(r.executor)).length} crew-bound package(s) → ${seats.length} seat(s), one per concurrent work stream (load-balanced); ${agents.length - seats.length > 0 ? `${agents.length - seats.length} installed CLI(s) left idle — seats follow the work, not the install list` : "all installed CLIs engaged because the work fans that wide"}`
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const apiCost = +(routing.reduce((s, r) => s + (r.est_cost_usd || 0), 0)).toFixed(2);
|
|
108
|
+
const pools = [...new Set(routing.map(r => `${r.executor}:${r.pool}`))];
|
|
109
|
+
const summary =
|
|
110
|
+
`Recommendation: ${mode.toUpperCase()}. ${why.join("; ")}. ` +
|
|
111
|
+
(mode === "crew" || mode === "hybrid"
|
|
112
|
+
? `Routing: ${routing.map(r => `${r.title}→${r.executor}${r.model ? `(${r.model})` : ""}`).join(", ")}. ` +
|
|
113
|
+
`Estimated real-money cost ≈ $${apiCost} (everything on a subscription pool is $0 marginal — quota pooling across ${pools.length} pools).`
|
|
114
|
+
: "");
|
|
115
|
+
const table = ["| package | diff | executor (model) | pool | est $ | reason |", "|---|---|---|---|---|---|",
|
|
116
|
+
...routing.map(r => `| ${r.title} | ${r.difficulty} | ${r.executor}${r.model ? ` (${r.model})` : ""} | ${r.pool} | ${r.est_cost_usd ?? "—"} | ${r.reason} |`)].join("\n");
|
|
117
|
+
// card_args are born as a DAG: crew cards depend on architect-owned foundation card(s);
|
|
118
|
+
// integration-titled architect cards depend on every crew card. (Indices are 1-based card
|
|
119
|
+
// creation order — the architect substitutes real ids as it creates them.)
|
|
120
|
+
const selfPkgs = routing.filter(r => r.executor === "orchestrator");
|
|
121
|
+
const foundationIdx = selfPkgs.length ? [1] : [];
|
|
122
|
+
const cards = routing.map((r, i) => ({
|
|
123
|
+
order: i + 1, title: r.title, difficulty: r.difficulty,
|
|
124
|
+
assignee: r.executor === "scrooge" || r.executor === "orchestrator" ? undefined : `${r.executor}:<project>`,
|
|
125
|
+
model: r.model || (["scrooge", "orchestrator"].includes(r.executor) ? undefined : `${r.executor}-default`),
|
|
126
|
+
via: r.executor === "scrooge" ? "relay_scrooge" : "relay_task_add",
|
|
127
|
+
deps_orders: r.executor === "orchestrator" && /integrat/i.test(r.title)
|
|
128
|
+
? routing.map((x, j) => j + 1).filter(j => j !== i + 1)
|
|
129
|
+
: (r.executor !== "orchestrator" ? foundationIdx.filter(f => f !== i + 1) : []) }));
|
|
130
|
+
return { mode, why, crew, routing, routing_table_md: table, card_args: cards, est_api_cost_usd: apiCost, quota_pools: pools, summary, orchestrator_tier: orchTier, agents_available: agents };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// ---- CLI ----
|
|
134
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
135
|
+
let input;
|
|
136
|
+
if (process.argv.includes("--demo")) {
|
|
137
|
+
input = { task: "neon asteroids game", packages: [
|
|
138
|
+
{ title: "engine+integration", difficulty: "hard" }, { title: "render/bloom", difficulty: "hard" },
|
|
139
|
+
{ title: "combat+ui", difficulty: "medium" }, { title: "audio", difficulty: "medium" },
|
|
140
|
+
{ title: "readme+badges", difficulty: "easy" } ] };
|
|
141
|
+
} else {
|
|
142
|
+
const stdin = readFileSync(0, "utf8").trim();
|
|
143
|
+
input = stdin ? JSON.parse(stdin) : { packages: [] };
|
|
144
|
+
}
|
|
145
|
+
const out = advise(input);
|
|
146
|
+
console.log(JSON.stringify(out, null, 2));
|
|
147
|
+
}
|
package/bin/cli.mjs
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// trantor — the command. Thin dispatcher over the toolkit so a global npm install
|
|
3
|
+
// gives you everything: trantor setup | doctor | connect | profile | advise | up | down | hub | ui
|
|
4
|
+
import { spawn } from "node:child_process";
|
|
5
|
+
import { join, dirname } from "node:path";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
import { readFileSync } from "node:fs";
|
|
8
|
+
|
|
9
|
+
const ROOT = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
10
|
+
const [, , cmd, ...args] = process.argv;
|
|
11
|
+
const run = (file, runner = process.execPath) => {
|
|
12
|
+
const child = spawn(runner, [join(ROOT, file), ...args], { stdio: "inherit", cwd: process.cwd() });
|
|
13
|
+
child.on("exit", (c) => process.exit(c ?? 0));
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
switch (cmd) {
|
|
17
|
+
case "setup": run("deploy/setup.sh", "/bin/bash"); break;
|
|
18
|
+
case "doctor": run("bin/doctor.mjs"); break;
|
|
19
|
+
case "connect": run("bin/connect.mjs"); break;
|
|
20
|
+
case "profile": run("bin/profile.mjs"); break;
|
|
21
|
+
case "advise": run("bin/advise.mjs"); break;
|
|
22
|
+
case "verify": run("bin/crew-verify.mjs"); break;
|
|
23
|
+
case "up": process.argv.splice(2, 1); spawn("/bin/bash", [join(ROOT, "bin/crew.sh"), "up", ...args], { stdio: "inherit", cwd: process.cwd() }).on("exit", c => process.exit(c ?? 0)); break;
|
|
24
|
+
case "down": spawn("/bin/bash", [join(ROOT, "bin/crew.sh"), "down"], { stdio: "inherit", cwd: process.cwd() }).on("exit", c => process.exit(c ?? 0)); break;
|
|
25
|
+
case "hub": run("hub.mjs"); break;
|
|
26
|
+
case "watch": run("bin/relay-watch.mjs"); break;
|
|
27
|
+
case "ui": {
|
|
28
|
+
let url = "http://127.0.0.1:4477";
|
|
29
|
+
try { url = JSON.parse(readFileSync(join(process.env.HOME || "", ".agent-bus", "config.json"), "utf8")).url || url; } catch {}
|
|
30
|
+
spawn(process.platform === "darwin" ? "open" : "xdg-open", [url], { stdio: "ignore", detached: true }).unref();
|
|
31
|
+
console.log(`dashboard → ${url}`);
|
|
32
|
+
break;
|
|
33
|
+
}
|
|
34
|
+
case "version": case "-v": case "--version":
|
|
35
|
+
console.log(JSON.parse(readFileSync(join(ROOT, "package.json"), "utf8")).version); break;
|
|
36
|
+
default:
|
|
37
|
+
console.log(`trantor — the hub-world for AI agent crews
|
|
38
|
+
|
|
39
|
+
trantor setup one-shot install: hub as an always-on service + config + CLI wiring + doctor
|
|
40
|
+
trantor doctor where do I stand? hub/plugin/CLIs/auth/keys/profile, with copy-paste fixes
|
|
41
|
+
trantor connect (re)wire every installed AI CLI to the bus
|
|
42
|
+
trantor profile declare your plans: trantor profile set claude=max codex=plus deepseek=api
|
|
43
|
+
trantor up … spawn a crew here: trantor up codex gemini kimi deepseek:deepseek-v4-pro
|
|
44
|
+
trantor down tear the crew down (kills processes, closes windows, no dialogs)
|
|
45
|
+
trantor ui open the live dashboard (board + flow views)
|
|
46
|
+
trantor advise ask the Advisor directly (JSON on stdin; --demo to see it)
|
|
47
|
+
trantor hub run the hub in the foreground (setup installs it as a service instead)
|
|
48
|
+
trantor watch live bus feed in the terminal
|
|
49
|
+
|
|
50
|
+
Claude Code plugin (the orchestrator side):
|
|
51
|
+
claude plugin marketplace add sashabogi/trantor && claude plugin install agent-bus
|
|
52
|
+
Docs: https://github.com/sashabogi/trantor`);
|
|
53
|
+
}
|
package/bin/connect.mjs
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// agent-bus connect — wire every AI coding CLI on this machine to the bus, in one shot.
|
|
3
|
+
//
|
|
4
|
+
// node bin/connect.mjs # detect installed CLIs, patch each one's MCP config (idempotent)
|
|
5
|
+
// node bin/connect.mjs --dry-run # show what would change, touch nothing
|
|
6
|
+
//
|
|
7
|
+
// Each CLI keeps its own MCP config file/format; this writes the one "relay" entry into each
|
|
8
|
+
// (with a timestamped .bak backup the first time it changes a file). Claude Code is handled by
|
|
9
|
+
// the plugin (claude plugin install agent-bus), so it's only verified here, not patched.
|
|
10
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync, copyFileSync } from "node:fs";
|
|
11
|
+
import { join, dirname } from "node:path";
|
|
12
|
+
import { homedir } from "node:os";
|
|
13
|
+
import { execSync } from "node:child_process";
|
|
14
|
+
import { fileURLToPath } from "node:url";
|
|
15
|
+
|
|
16
|
+
const DRY = process.argv.includes("--dry-run");
|
|
17
|
+
const MCP = join(dirname(dirname(fileURLToPath(import.meta.url))), "mcp.mjs");
|
|
18
|
+
const URL_ = process.env.RELAY_URL || "http://127.0.0.1:4477";
|
|
19
|
+
const has = (cmd) => { try { execSync(`command -v ${cmd}`, { stdio: "ignore", shell: "/bin/sh" }); return true; } catch { return false; } };
|
|
20
|
+
const stamp = new Date().toISOString().slice(0, 10);
|
|
21
|
+
const backup = (p) => { const b = `${p}.bak-${stamp}`; if (!existsSync(b)) copyFileSync(p, b); return b; };
|
|
22
|
+
const out = [];
|
|
23
|
+
const report = (cli, status, detail = "") => out.push({ cli, status, detail });
|
|
24
|
+
|
|
25
|
+
function patchJson(path, mutate) {
|
|
26
|
+
const exists = existsSync(path);
|
|
27
|
+
const d = exists ? JSON.parse(readFileSync(path, "utf8")) : {};
|
|
28
|
+
const before = JSON.stringify(d);
|
|
29
|
+
mutate(d);
|
|
30
|
+
if (JSON.stringify(d) === before) return "already wired";
|
|
31
|
+
if (!DRY) {
|
|
32
|
+
if (exists) backup(path); else mkdirSync(dirname(path), { recursive: true });
|
|
33
|
+
writeFileSync(path, JSON.stringify(d, null, 2) + "\n");
|
|
34
|
+
}
|
|
35
|
+
return exists ? "wired" : "wired (new config)";
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const relayEnv = (agent) => ({ RELAY_URL: URL_, RELAY_AGENT: agent });
|
|
39
|
+
|
|
40
|
+
// ---- Claude Code: plugin handles it; verify only ----
|
|
41
|
+
if (has("claude")) {
|
|
42
|
+
let st = "plugin not detected — run: claude plugin marketplace add sashabogi/trantor && claude plugin install agent-bus";
|
|
43
|
+
try {
|
|
44
|
+
const s = JSON.parse(readFileSync(join(homedir(), ".claude", "settings.json"), "utf8"));
|
|
45
|
+
if (Object.keys(s.enabledPlugins || {}).some(k => k.startsWith("agent-bus@"))) st = "plugin installed ✓";
|
|
46
|
+
} catch {}
|
|
47
|
+
report("claude", st);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// ---- Codex (TOML append — no TOML lib needed) ----
|
|
51
|
+
if (has("codex")) {
|
|
52
|
+
const p = join(homedir(), ".codex", "config.toml");
|
|
53
|
+
const cur = existsSync(p) ? readFileSync(p, "utf8") : "";
|
|
54
|
+
if (cur.includes("[mcp_servers.relay]")) report("codex", "already wired");
|
|
55
|
+
else {
|
|
56
|
+
const block = `\n# agent-bus — auto-registers each Codex session on the bus + adds relay_* tools\n[mcp_servers.relay]\ncommand = "node"\nargs = ["${MCP}"]\nenv = { RELAY_URL = "${URL_}", RELAY_AGENT = "codex" }\n`;
|
|
57
|
+
if (!DRY) { if (existsSync(p)) backup(p); else mkdirSync(dirname(p), { recursive: true }); writeFileSync(p, cur + block); }
|
|
58
|
+
report("codex", cur ? "wired" : "wired (new config)", p);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ---- Gemini CLI ---- (existing relay entries are never overwritten — user customization wins)
|
|
63
|
+
if (has("gemini")) {
|
|
64
|
+
const p = join(homedir(), ".gemini", "settings.json");
|
|
65
|
+
report("gemini", patchJson(p, d => {
|
|
66
|
+
d.mcpServers ||= {};
|
|
67
|
+
d.mcpServers.relay ||= { command: "node", args: [MCP], env: relayEnv("gemini") };
|
|
68
|
+
}), p);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// ---- Kimi CLI ----
|
|
72
|
+
if (has("kimi")) {
|
|
73
|
+
const p = join(homedir(), ".kimi", "mcp.json");
|
|
74
|
+
report("kimi", patchJson(p, d => {
|
|
75
|
+
d.mcpServers ||= {};
|
|
76
|
+
d.mcpServers.relay ||= { command: "node", args: [MCP], env: relayEnv("kimi") };
|
|
77
|
+
}), p);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ---- OpenCode ----
|
|
81
|
+
if (has("opencode")) {
|
|
82
|
+
const p = join(homedir(), ".config", "opencode", "opencode.json");
|
|
83
|
+
report("opencode", patchJson(p, d => {
|
|
84
|
+
d.$schema ||= "https://opencode.ai/config.json";
|
|
85
|
+
d.mcp ||= {};
|
|
86
|
+
d.mcp.relay ||= { type: "local", command: ["node", MCP], enabled: true, environment: relayEnv("opencode") };
|
|
87
|
+
}), p);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const found = out.length;
|
|
91
|
+
console.log(`agent-bus connect${DRY ? " (dry run)" : ""} — hub: ${URL_}`);
|
|
92
|
+
for (const r of out) console.log(` ${r.cli.padEnd(9)} ${r.status}${r.detail ? ` (${r.detail})` : ""}`);
|
|
93
|
+
if (!found) console.log(" no supported CLIs found on PATH (claude, codex, gemini, kimi, opencode)");
|
|
94
|
+
console.log(DRY ? "\nRun without --dry-run to apply." : "\nDone. New sessions of each CLI auto-join the bus.");
|