trantor 0.16.0 → 0.17.1

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.
@@ -1,19 +1,38 @@
1
1
  {
2
- "name": "agent-bus",
3
- "owner": { "name": "Sasha Bogojevic", "email": "hello@hivedigitalllc.com" },
2
+ "name": "trantor",
3
+ "owner": {
4
+ "name": "Sasha Bogojevic",
5
+ "email": "hello@hivedigitalllc.com"
6
+ },
4
7
  "metadata": {
5
- "description": "Live message bus, presence, project Kanban + context-handoff for independent AI coding agents (Claude, Codex, Gemini, …)",
6
- "version": "0.14.0"
8
+ "description": "Trantor — the hub-world for AI agent crews: live message bus, presence, project Kanban/flow board + context-handoff for independent AI coding agents (Claude, Codex, Gemini, …)",
9
+ "version": "0.17.1"
7
10
  },
8
11
  "plugins": [
9
12
  {
10
- "name": "agent-bus",
13
+ "name": "trantor",
11
14
  "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
+ "description": "The hub-world for AI agent crews. Say \"fire up the crew\" and Claude becomes the architect: a plan-aware Advisor routes the work (solo / cheap inline calls / live crew of Codex, Gemini, Kimi & DeepSeek in their own terminal windows), a Kanban/flow command center with a testing gate tracks it, and an economics brain (Scrooge) keeps the receipts. 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.",
16
+ "version": "0.17.1",
17
+ "author": {
18
+ "name": "Sasha Bogojevic"
19
+ },
15
20
  "category": "development",
16
- "keywords": ["multi-agent", "coordination", "mcp", "hooks", "kanban", "context-handoff", "message-bus", "claude-code", "codex", "gemini"]
21
+ "keywords": [
22
+ "multi-agent",
23
+ "agent-crew",
24
+ "orchestration",
25
+ "coordination",
26
+ "mcp",
27
+ "hooks",
28
+ "kanban",
29
+ "context-handoff",
30
+ "message-bus",
31
+ "claude-code",
32
+ "codex",
33
+ "gemini",
34
+ "llm-routing"
35
+ ]
17
36
  }
18
37
  ]
19
38
  }
@@ -1,11 +1,13 @@
1
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)",
2
+ "name": "trantor",
3
+ "version": "0.17.1",
4
+ "description": "Trantor — the hub-world for AI agent crews: live message bus, presence, project Kanban/flow board + crew orchestration for independent AI coding agents (Claude, Codex, Gemini, Kimi, DeepSeek)",
5
5
  "mcpServers": {
6
6
  "relay": {
7
7
  "command": "node",
8
- "args": ["${CLAUDE_PLUGIN_ROOT}/mcp.mjs"]
8
+ "args": [
9
+ "${CLAUDE_PLUGIN_ROOT}/mcp.mjs"
10
+ ]
9
11
  }
10
12
  },
11
13
  "skills": "./skills/"
package/README.md CHANGED
@@ -35,7 +35,7 @@ Then give Claude Code (the orchestrator) the plugin:
35
35
 
36
36
  ```bash
37
37
  claude plugin marketplace add sashabogi/trantor
38
- claude plugin install agent-bus
38
+ claude plugin install trantor
39
39
  ```
40
40
 
41
41
  That's it. (Prefer source? `git clone https://github.com/sashabogi/trantor && cd trantor &&
@@ -53,7 +53,7 @@ core
53
53
  ✓ hub up at http://127.0.0.1:4477
54
54
  claude (the orchestrator)
55
55
  ✗ plugin not installed
56
- → claude plugin marketplace add sashabogi/trantor && claude plugin install agent-bus
56
+ → claude plugin marketplace add sashabogi/trantor && claude plugin install trantor
57
57
  crew CLIs (install any subset — seats follow the work)
58
58
  ✓ codex: wired to the bus
59
59
  ✗ codex: NOT authenticated — it will join the bus but fail on its first turn
@@ -63,12 +63,34 @@ the brain
63
63
  ✗ quota profile not set → trantor profile set claude=max codex=plus deepseek=api
64
64
  ```
65
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."**
66
+ Fix the `→` lines (each CLI's own sign-in happens once, in that CLI) and re-run `trantor doctor`
67
+ until it's clean.
68
68
 
69
69
  Provider API keys (e.g. `DEEPSEEK_API_KEY`) live in one file: **`~/.agent-bus/.env`** — the
70
70
  crew runners source it automatically.
71
71
 
72
+ ## Your first build
73
+
74
+ Open Claude Code in the project you want built and say it in plain words:
75
+
76
+ > **fire up the crew** — build me a 2-player asteroids game with power-ups
77
+
78
+ Any phrasing works ("build it with the crew", "build this with trantor"), or invoke the
79
+ skill directly: **`/trantor:crew`**. Claude becomes the architect: it cuts the work into
80
+ difficulty-tagged packages, asks the Advisor, and shows you the routing table with a
81
+ real-money estimate **before spending anything**. You say go — terminal windows open, the
82
+ board fills, and you watch it live:
83
+
84
+ ```bash
85
+ trantor ui
86
+ ```
87
+
88
+ No crew CLIs installed yet? It still works — the Advisor routes the work `solo` or to cheap
89
+ inline `scrooge` calls instead of seats. Seats follow the work *and* what's actually installed.
90
+
91
+ Running low on context mid-build? Say **`/trantor:handoff`** — a fresh session in the same
92
+ project takes over with a full window (and a PreCompact hook does this automatically).
93
+
72
94
  ## What happens when you fire up a crew
73
95
 
74
96
  1. **The Advisor moment.** Your Claude cuts the work into difficulty-tagged packages, calls
@@ -183,8 +205,10 @@ Identity: `RELAY_SESSION` → `RELAY_AGENT:<project-folder>` → `<hostname>:<pr
183
205
  always-on/remote hub (private tailnet, or public with auth) is on the roadmap — never expose
184
206
  the hub publicly without auth.
185
207
 
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.*
208
+ *Heritage note: Trantor grew out of **agent-bus**. As of v0.17 the plugin and skills are
209
+ named `trantor` (formerly `agent-bus` if you installed before v0.17:
210
+ `claude plugin uninstall agent-bus && claude plugin marketplace update && claude plugin install trantor`).
211
+ The `relay_*` tool names and the `~/.agent-bus` state dir remain until a later release.*
188
212
 
189
213
  ## Honest limits
190
214
 
package/bin/cli.mjs CHANGED
@@ -48,6 +48,6 @@ switch (cmd) {
48
48
  trantor watch live bus feed in the terminal
49
49
 
50
50
  Claude Code plugin (the orchestrator side):
51
- claude plugin marketplace add sashabogi/trantor && claude plugin install agent-bus
51
+ claude plugin marketplace add sashabogi/trantor && claude plugin install trantor
52
52
  Docs: https://github.com/sashabogi/trantor`);
53
53
  }
package/bin/connect.mjs CHANGED
@@ -1,12 +1,12 @@
1
1
  #!/usr/bin/env node
2
- // agent-bus connect — wire every AI coding CLI on this machine to the bus, in one shot.
2
+ // trantor connect — wire every AI coding CLI on this machine to the bus, in one shot.
3
3
  //
4
4
  // node bin/connect.mjs # detect installed CLIs, patch each one's MCP config (idempotent)
5
5
  // node bin/connect.mjs --dry-run # show what would change, touch nothing
6
6
  //
7
7
  // Each CLI keeps its own MCP config file/format; this writes the one "relay" entry into each
8
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.
9
+ // the plugin (claude plugin install trantor), so it's only verified here, not patched.
10
10
  import { readFileSync, writeFileSync, existsSync, mkdirSync, copyFileSync } from "node:fs";
11
11
  import { join, dirname } from "node:path";
12
12
  import { homedir } from "node:os";
@@ -39,10 +39,10 @@ const relayEnv = (agent) => ({ RELAY_URL: URL_, RELAY_AGENT: agent });
39
39
 
40
40
  // ---- Claude Code: plugin handles it; verify only ----
41
41
  if (has("claude")) {
42
- let st = "plugin not detected — run: claude plugin marketplace add sashabogi/trantor && claude plugin install agent-bus";
42
+ let st = "plugin not detected — run: claude plugin marketplace add sashabogi/trantor && claude plugin install trantor";
43
43
  try {
44
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 ✓";
45
+ if (Object.keys(s.enabledPlugins || {}).some(k => k.startsWith("trantor@") || k.startsWith("agent-bus@"))) st = "plugin installed ✓";
46
46
  } catch {}
47
47
  report("claude", st);
48
48
  }
@@ -53,7 +53,7 @@ if (has("codex")) {
53
53
  const cur = existsSync(p) ? readFileSync(p, "utf8") : "";
54
54
  if (cur.includes("[mcp_servers.relay]")) report("codex", "already wired");
55
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`;
56
+ const block = `\n# trantor — 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
57
  if (!DRY) { if (existsSync(p)) backup(p); else mkdirSync(dirname(p), { recursive: true }); writeFileSync(p, cur + block); }
58
58
  report("codex", cur ? "wired" : "wired (new config)", p);
59
59
  }
@@ -88,7 +88,7 @@ if (has("opencode")) {
88
88
  }
89
89
 
90
90
  const found = out.length;
91
- console.log(`agent-bus connect${DRY ? " (dry run)" : ""} — hub: ${URL_}`);
91
+ console.log(`trantor connect${DRY ? " (dry run)" : ""} — hub: ${URL_}`);
92
92
  for (const r of out) console.log(` ${r.cli.padEnd(9)} ${r.status}${r.detail ? ` (${r.detail})` : ""}`);
93
93
  if (!found) console.log(" no supported CLIs found on PATH (claude, codex, gemini, kimi, opencode)");
94
94
  console.log(DRY ? "\nRun without --dry-run to apply." : "\nDone. New sessions of each CLI auto-join the bus.");
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- // agent-bus crew runner — keeps a crew agent alive forever without burning tokens.
2
+ // trantor crew runner — keeps a crew agent alive forever without burning tokens.
3
3
  //
4
4
  // node crew-runner.mjs <agent> [project-dir]
5
5
  //
@@ -34,7 +34,7 @@ try { mkdirSync(LOGDIR, { recursive: true }); } catch {}
34
34
  let TURN = 0;
35
35
  const telemetry = (rec) => { try { appendFileSync(join(LOGDIR, `${AGENT}-${PROJ}.jsonl`), JSON.stringify(rec) + "\n"); } catch {} };
36
36
  const banner = (trigger) => {
37
- console.log(`\x1b[2J\x1b[H\x1b[48;5;236m\x1b[38;5;43m ◤ ${AGENT.toUpperCase()} ◢ agent-bus crew · ${PROJ} · turn ${TURN} · ${trigger}${MODEL ? ` · ${MODEL}` : ""} \x1b[0m\n`);
37
+ console.log(`\x1b[2J\x1b[H\x1b[48;5;236m\x1b[38;5;43m ◤ ${AGENT.toUpperCase()} ◢ trantor crew · ${PROJ} · turn ${TURN} · ${trigger}${MODEL ? ` · ${MODEL}` : ""} \x1b[0m\n`);
38
38
  };
39
39
 
40
40
  async function api(path, body) {
@@ -67,7 +67,7 @@ const CLI = {
67
67
  const cli = CLI[AGENT];
68
68
  if (!cli) { console.error(`unknown agent '${AGENT}' (known: ${Object.keys(CLI).join(", ")})`); process.exit(1); }
69
69
 
70
- const RULES = `Rules: you are ${SESSION} on the agent-bus crew. Work your assigned file(s), report on the bus (relay_send, <280 chars), move your Kanban card as you go (doing -> testing -> done; run the tests in 'testing', use 'failed' + a report if they break). When your work for THIS message is finished, END YOUR TURN — do NOT park, do NOT loop relay_wait; the runner waits for you and will wake you with the next message.`;
70
+ const RULES = `Rules: you are ${SESSION} on the trantor crew. Work your assigned file(s), report on the bus (relay_send, <280 chars), move your Kanban card as you go (doing -> testing -> done; run the tests in 'testing', use 'failed' + a report if they break). When your work for THIS message is finished, END YOUR TURN — do NOT park, do NOT loop relay_wait; the runner waits for you and will wake you with the next message.`;
71
71
 
72
72
  let sid = "";
73
73
  function runTurn(prompt, isFirst, trigger = "kickoff") {
package/bin/crew.sh CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/bin/bash
2
- # agent-bus crew launcher v2 — visible terminal windows that CANNOT silently die or silently fail.
2
+ # trantor crew launcher v2 — visible terminal windows that CANNOT silently die or silently fail.
3
3
  #
4
4
  # bin/crew.sh up codex gemini kimi deepseek # one window per agent, in the CURRENT project dir
5
5
  # bin/crew.sh down # kill crew processes + close windows (no dialogs)
@@ -91,7 +91,7 @@ spawn_grid() { # $@ = agents — (re)computes the grid for THIS batch and spawn
91
91
  osascript \
92
92
  -e 'tell application "Terminal"' \
93
93
  -e " set w to do script \"cd $DIR && clear && CREW_MODEL=$MODEL node $BUS_DIR/bin/crew-runner.mjs $AGENT $DIR\"" \
94
- -e " set custom title of w to \"$(echo "$AGENT" | tr '[:lower:]' '[:upper:]') — agent-bus crew\"" \
94
+ -e " set custom title of w to \"$(echo "$AGENT" | tr '[:lower:]' '[:upper:]') — trantor crew\"" \
95
95
  -e " set theWin to first window whose tabs contains w" \
96
96
  -e " set bounds of theWin to {$X1, $Y1, $(( X1 + CW )), $(( Y1 + CH ))}" \
97
97
  -e " return id of theWin" \
package/bin/doctor.mjs CHANGED
@@ -38,7 +38,7 @@ else {
38
38
  const st = read(join(H, ".claude", "settings.json")) || {};
39
39
  Object.keys(st.enabledPlugins || {}).some(k => k.startsWith("agent-bus@") || k.startsWith("trantor@"))
40
40
  ? ok("plugin installed")
41
- : warn("plugin not installed", "claude plugin marketplace add sashabogi/trantor && claude plugin install agent-bus");
41
+ : warn("plugin not installed", "claude plugin marketplace add sashabogi/trantor && claude plugin install trantor");
42
42
  }
43
43
 
44
44
  // crew CLIs: installed / wired / authenticated
@@ -1,5 +1,5 @@
1
1
  #!/bin/bash
2
- # agent-bus handoff prompt (macOS) — shown when a session hits its context limit.
2
+ # trantor handoff prompt (macOS) — shown when a session hits its context limit.
3
3
  # Asks the user, with a timeout, whether to open a FRESH same-agent session that takes
4
4
  # over via the handoff. Default (incl. timeout, or no UI) = open fresh. "Keep compacting" = skip.
5
5
  #
@@ -11,10 +11,10 @@ AGENT_CMD="${AGENT_CMD:-claude}"
11
11
  HERE="$(cd "$(dirname "$0")" && pwd)"
12
12
  NAME="$(basename "$DIR")"
13
13
 
14
- MSG="agent-bus — this session's context window is full ($NAME). Open a FRESH session to take over with a full window? It loads a handoff of this session. (The current session keeps compacting either way.)"
14
+ MSG="trantor — this session's context window is full ($NAME). Open a FRESH session to take over with a full window? It loads a handoff of this session. (The current session keeps compacting either way.)"
15
15
 
16
16
  # Best-effort timed dialog. On timeout, error, or no UI session -> empty -> we spawn (the default).
17
- CHOICE="$(osascript -e "button returned of (display dialog \"${MSG//\"/\\\"}\" buttons {\"Keep compacting\", \"Open fresh session\"} default button \"Open fresh session\" giving up after $TIMEOUT with title \"agent-bus\")" 2>/dev/null)"
17
+ CHOICE="$(osascript -e "button returned of (display dialog \"${MSG//\"/\\\"}\" buttons {\"Keep compacting\", \"Open fresh session\"} default button \"Open fresh session\" giving up after $TIMEOUT with title \"trantor\")" 2>/dev/null)"
18
18
 
19
19
  if [ "$CHOICE" != "Keep compacting" ]; then
20
20
  "$HERE/open-session.sh" "$DIR" "$AGENT_CMD"
package/bin/profile.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- // agent-bus quota profile — declare what plan each provider runs on, once.
2
+ // trantor quota profile — declare what plan each provider runs on, once.
3
3
  // The Advisor uses this to pick execution modes (plans can't be detected reliably).
4
4
  //
5
5
  // node bin/profile.mjs # show current profile
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
- // agent-bus statusline — prints a tiny live indicator for the agent's status bar:
3
- // 🟢 agent-bus · 3 live
2
+ // trantor statusline — prints a tiny live indicator for the agent's status bar:
3
+ // 🟢 trantor · 3 live
4
4
  // Claude Code: add to settings.json ->
5
- // "statusLine": { "type": "command", "command": "node /path/to/agent-bus/bin/statusline.mjs" }
5
+ // "statusLine": { "type": "command", "command": "node /path/to/trantor/bin/statusline.mjs" }
6
6
  // Reads session info as JSON on stdin (Claude Code convention); fast + fail-silent.
7
7
  import { readFileSync, existsSync } from "node:fs";
8
8
  import { join, basename } from "node:path";
@@ -22,9 +22,9 @@ async function main() {
22
22
  const r = await fetch(`${relayUrl()}/peers`, { signal: AbortSignal.timeout(800) });
23
23
  const { peers } = await r.json();
24
24
  const live = peers.filter(p => p.online && p.session !== me).length;
25
- process.stdout.write(`\x1b[38;5;43m● agent-bus\x1b[0m \x1b[2m· ${live} other${live === 1 ? "" : "s"} live\x1b[0m`);
25
+ process.stdout.write(`\x1b[38;5;43m● trantor\x1b[0m \x1b[2m· ${live} other${live === 1 ? "" : "s"} live\x1b[0m`);
26
26
  } catch {
27
- process.stdout.write(`\x1b[2m○ agent-bus offline\x1b[0m`); // hub unreachable
27
+ process.stdout.write(`\x1b[2m○ trantor offline\x1b[0m`); // hub unreachable
28
28
  }
29
29
  }
30
30
  main();
@@ -1,9 +1,9 @@
1
1
  {
2
- "//": "agent-bus for Gemini CLI — merge into ~/.gemini/settings.json (or a project .gemini/settings.json).",
2
+ "//": "trantor for Gemini CLI — merge into ~/.gemini/settings.json (or a project .gemini/settings.json).",
3
3
  "mcpServers": {
4
4
  "relay": {
5
5
  "command": "node",
6
- "args": ["REPLACE/WITH/ABSOLUTE/PATH/agent-bus/mcp.mjs"],
6
+ "args": ["REPLACE/WITH/ABSOLUTE/PATH/trantor/mcp.mjs"],
7
7
  "env": { "RELAY_URL": "http://127.0.0.1:4477", "RELAY_SESSION": "gemini:myproject" }
8
8
  }
9
9
  }
package/deploy/setup.sh CHANGED
@@ -28,4 +28,5 @@ node "$REPO/bin/connect.mjs"
28
28
  echo
29
29
  node "$REPO/bin/doctor.mjs" || true
30
30
  echo
31
- echo "Next: claude plugin marketplace add sashabogi/trantor && claude plugin install agent-bus"
31
+ echo "Next: claude plugin marketplace add sashabogi/trantor && claude plugin install trantor"
32
+ echo "Then: open Claude Code in any project and say \"fire up the crew\""
package/hooks/hooks.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "description": "agent-bus — auto-register each session + inject live roster (SessionStart); write a handoff before compaction (PreCompact)",
2
+ "description": "trantor — auto-register each session + inject live roster (SessionStart); write a handoff before compaction (PreCompact)",
3
3
  "hooks": {
4
4
  "SessionStart": [
5
5
  { "matcher": "", "hooks": [ { "type": "command", "command": "node ${CLAUDE_PLUGIN_ROOT}/hooks/sessionstart.mjs" } ] }
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- // agent-bus PreCompact hook — fires right before Claude Code compacts a full
2
+ // trantor PreCompact hook — fires right before Claude Code compacts a full
3
3
  // context window. Instead of (just) compacting, it writes a rich HANDOFF so you can
4
4
  // open a FRESH session that takes over with a new full window. The SessionStart hook
5
5
  // detects the pending handoff and loads it.
@@ -50,7 +50,7 @@ function summarize(convo) {
50
50
  return execSync(`scrooge -t summarize -d medium --system ${JSON.stringify(sys)}`, {
51
51
  input: convo, encoding: "utf8", timeout: 45000, maxBuffer: 4 * 1024 * 1024,
52
52
  }).trim();
53
- } catch (e) { process.stderr.write(`[agent-bus] scrooge summarize failed: ${e?.message}\n`); }
53
+ } catch (e) { process.stderr.write(`[trantor] scrooge summarize failed: ${e?.message}\n`); }
54
54
  }
55
55
  // fallback: raw recent tail
56
56
  return `*(no summarizer available — raw recent transcript tail)*\n\n${convo.slice(-6000)}`;
@@ -85,7 +85,7 @@ try {
85
85
  };
86
86
  const file = join(HANDOFF_DIR, `${record.id}.json`);
87
87
  writeFileSync(file, JSON.stringify(record, null, 2));
88
- process.stderr.write(`[agent-bus] handoff written: ${file} (trigger=${trigger})\n`);
88
+ process.stderr.write(`[trantor] handoff written: ${file} (trigger=${trigger})\n`);
89
89
 
90
90
  // best-effort: ping the relay hub so other sessions/machines know a handoff is ready
91
91
  try {
@@ -107,12 +107,12 @@ try {
107
107
  if (existsSync(script)) {
108
108
  const child = spawn("/bin/bash", [script, projectDir, String(conf.handoffPromptTimeout || 25)], { detached: true, stdio: "ignore" });
109
109
  child.unref();
110
- process.stderr.write(`[agent-bus] handoff prompt launched (opt-in)\n`);
110
+ process.stderr.write(`[trantor] handoff prompt launched (opt-in)\n`);
111
111
  }
112
112
  }
113
113
  } catch {}
114
114
  } catch (err) {
115
- process.stderr.write(`[agent-bus] precompact error: ${err?.message || err}\n`);
115
+ process.stderr.write(`[trantor] precompact error: ${err?.message || err}\n`);
116
116
  }
117
117
  process.stdout.write("{}");
118
118
  process.exit(0);
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- // agent-bus SessionStart hook — every session auto-registers with the hub and
2
+ // trantor SessionStart hook — every session auto-registers with the hub and
3
3
  // gets a roster of OTHER live sessions injected into context, so independent
4
4
  // sessions discover each other automatically (locally or across machines).
5
5
  //
@@ -61,6 +61,14 @@ let additionalContext = "";
61
61
  try {
62
62
  await readStdin();
63
63
  const projectDir = process.env.CLAUDE_PROJECT_DIR || process.cwd();
64
+ // Sessions started in the home directory itself aren't project work — registering
65
+ // them spawns a phantom "<username>" project board on the dashboard. Set
66
+ // RELAY_SESSION (or RELAY_PROJECT) to deliberately put a home-dir session on the bus.
67
+ if (!process.env.RELAY_SESSION && !process.env.RELAY_PROJECT && projectDir === homedir()) {
68
+ process.stderr.write("[trantor] session in the home directory — not registering on the bus (set RELAY_SESSION to opt in)\n");
69
+ process.stdout.write("{}");
70
+ process.exit(0);
71
+ }
64
72
  const session = process.env.RELAY_SESSION || `${hostname()}:${basename(projectDir)}`;
65
73
  const url = relayUrl();
66
74
 
@@ -72,30 +80,30 @@ try {
72
80
  try { peers = (await jget(`${url}/peers`)).peers || []; } catch {}
73
81
  const others = peers.filter(p => p.online && p.session !== session);
74
82
 
75
- process.stderr.write(`[agent-bus] registered as ${session} -> ${url} (${others.length} other live session(s))\n`);
83
+ process.stderr.write(`[trantor] registered as ${session} -> ${url} (${others.length} other live session(s))\n`);
76
84
 
77
85
  if (others.length > 0) {
78
- additionalContext += `<agent-bus session="${session}" hub="${url}">\n`;
79
- additionalContext += `You are connected to agent-bus (the cross-agent session bus) as "${session}". Other LIVE agent sessions are running right now:\n`;
86
+ additionalContext += `<trantor session="${session}" hub="${url}">\n`;
87
+ additionalContext += `You are connected to Trantor (the cross-agent session bus) as "${session}". Other LIVE agent sessions are running right now:\n`;
80
88
  for (const p of others) additionalContext += `- ${sanitize(p.session)}\n`;
81
89
  additionalContext += `Use the relay MCP tools (relay_peers, relay_send, relay_inbox, relay_wait) to coordinate with them — hand off work, check for overlap before editing shared files, or ask another session for help. If a sibling session is touching the same project, coordinate before making conflicting changes.\n`;
82
- additionalContext += `</agent-bus>\n`;
90
+ additionalContext += `</trantor>\n`;
83
91
  }
84
92
 
85
93
  // Pending handoff? A prior session hit the context limit and left a handoff for this
86
94
  // project — take over with this fresh full window instead of starting cold.
87
95
  const handoff = loadPendingHandoff(basename(projectDir));
88
96
  if (handoff) {
89
- process.stderr.write(`[agent-bus] loaded pending handoff ${handoff.id}\n`);
90
- additionalContext += `<agent-bus-handoff id="${sanitize(handoff.id)}" from="${sanitize(handoff.machine)}" trigger="${sanitize(handoff.trigger)}">\n`;
97
+ process.stderr.write(`[trantor] loaded pending handoff ${handoff.id}\n`);
98
+ additionalContext += `<trantor-handoff id="${sanitize(handoff.id)}" from="${sanitize(handoff.machine)}" trigger="${sanitize(handoff.trigger)}">\n`;
91
99
  additionalContext += `🔄 **You are taking over from a prior session that hit its context limit.** This is a fresh full window. Resume the work below — the prior session's summary, git state, and a pointer to its full transcript (searchable; Foundation/Gaia has it ingested) follow. Continue from "OPEN THREADS & NEXT STEPS"; do not restart from scratch.\n\n`;
92
100
  additionalContext += `## Handoff summary\n${sanitize(handoff.summary)}\n`;
93
101
  if (handoff.gitStatus) additionalContext += `\n## Git working-tree at handoff\n\`\`\`\n${sanitize(handoff.gitStatus)}\n\`\`\`\n`;
94
102
  if (handoff.transcript_path) additionalContext += `\n_Full prior transcript: ${sanitize(handoff.transcript_path)}_\n`;
95
- additionalContext += `</agent-bus-handoff>\n`;
103
+ additionalContext += `</trantor-handoff>\n`;
96
104
  }
97
105
  } catch (err) {
98
- process.stderr.write(`[agent-bus] sessionstart error: ${err?.message || err}\n`);
106
+ process.stderr.write(`[trantor] sessionstart error: ${err?.message || err}\n`);
99
107
  }
100
108
 
101
109
  // Hook protocol: emit additionalContext via stdout JSON. Self-validate so we never
package/hub.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- // agent-bus hub — message bus + presence/status board + SSE push, so independent
2
+ // trantor hub — message bus + presence/status board + SSE push, so independent
3
3
  // Claude Code sessions can coordinate (near-instant for watchers, cheap for idle peers).
4
4
  // Binds to LOOPBACK (127.0.0.1) by default — local-first and safe (no auth yet). To let other
5
5
  // machines reach it (e.g. over a Tailscale tailnet), set RELAY_HOST=0.0.0.0 — but only on a
@@ -38,6 +38,7 @@ try { UI = readFileSync(new URL("./ui.html", import.meta.url), "utf8"); } catch
38
38
  // open SSE streams: [{ session, res }]
39
39
  const streams = [];
40
40
  const now = () => Date.now();
41
+ const fmtAge = ms => { const m = Math.floor(ms / 60000); return m > 48 * 60 ? `${Math.floor(m / 1440)}d ago` : m > 90 ? `${Math.floor(m / 60)}h ago` : `${m}m ago`; };
41
42
  function body(req) { return new Promise(r => { let d = ""; req.on("data", c => (d += c)); req.on("end", () => { try { r(d ? JSON.parse(d) : {}); } catch { r({}); } }); }); }
42
43
  function json(res, code, obj) { res.writeHead(code, { "content-type": "application/json", "access-control-allow-origin": "*" }); res.end(JSON.stringify(obj)); }
43
44
  function touch(session, status, project) {
@@ -107,16 +108,30 @@ const server = http.createServer(async (req, res) => {
107
108
  state.projectMeta[k] = m; dirty = true;
108
109
  return json(res, 200, { ok: true, project: k, brief: m.brief || "" });
109
110
  }
111
+ if (req.method === "POST" && P === "/project/delete") { // forget a project: its cards, peers, brief, and lane
112
+ const b = await body(req); const k = String(b.project || "").slice(0, 80);
113
+ if (!k) return json(res, 400, { error: "project required" });
114
+ const nt = state.tasks.length, np = Object.keys(state.peers).length, nm = state.messages.length;
115
+ state.tasks = state.tasks.filter(t => t.project !== k);
116
+ for (const [s, v] of Object.entries(state.peers)) if (v.project === k) delete state.peers[s];
117
+ delete state.projectMeta[k];
118
+ state.messages = state.messages.filter(m2 => (m2.project || "") !== k);
119
+ dirty = true; // the project reappears cleanly if an agent ever registers it again
120
+ return json(res, 200, { ok: true, project: k, removed: { tasks: nt - state.tasks.length, peers: np - Object.keys(state.peers).length, messages: nm - state.messages.length } });
121
+ }
110
122
  if (req.method === "GET" && P === "/projects") { // project-grouped view
111
123
  const cutoff = now() - ONLINE_MS; const byProj = {};
112
124
  const proj = p => p || "(unassigned)";
113
- const mk = k => (byProj[k] ||= { project: k, brief: (state.projectMeta[k]?.brief) || "", agents: [], tasks: { todo:0,doing:0,testing:0,failed:0,done:0,blocked:0 }, doingTitles: [] });
125
+ const mk = k => (byProj[k] ||= { project: k, brief: (state.projectMeta[k]?.brief) || "", agents: [], tasks: { todo:0,doing:0,testing:0,failed:0,done:0,blocked:0 }, doingTitles: [], lastActivity: 0 });
114
126
  for (const [s, v] of Object.entries(state.peers)) {
115
- const k = proj(v.project); mk(k).agents.push({ session: s, online: v.lastSeen > cutoff, status: v.status || "" });
127
+ const k = proj(v.project); const e = mk(k); e.agents.push({ session: s, online: v.lastSeen > cutoff, status: v.status || "" });
128
+ if ((v.lastSeen || 0) > e.lastActivity) e.lastActivity = v.lastSeen;
116
129
  }
117
- for (const t of state.tasks) { const e = mk(proj(t.project)); e.tasks[t.status] = (e.tasks[t.status]||0)+1; if (t.status === "doing") e.doingTitles.push(t.title); }
130
+ for (const t of state.tasks) { const e = mk(proj(t.project)); e.tasks[t.status] = (e.tasks[t.status]||0)+1; if (t.status === "doing") e.doingTitles.push(t.title); if ((t.updated || 0) > e.lastActivity) e.lastActivity = t.updated; }
118
131
  // derive a one-line phase ("where it is in the process") from the board
119
132
  for (const e of Object.values(byProj)) {
133
+ const mu = state.projectMeta[e.project]?.updated || 0; if (mu > e.lastActivity) e.lastActivity = mu;
134
+ e.idle = !e.agents.some(a => a.online);
120
135
  const { todo, doing, testing=0, failed=0, done, blocked } = e.tasks; const total = todo+doing+testing+failed+done+blocked;
121
136
  e.phase = total === 0 ? "no cards yet"
122
137
  : failed > 0 ? `${failed} FAILED — fixing`
@@ -126,6 +141,8 @@ const server = http.createServer(async (req, res) => {
126
141
  : done === total ? "shipped — all cards done"
127
142
  : todo > 0 ? `planned: ${todo} card${todo>1?"s":""} queued`
128
143
  : "in progress";
144
+ // dead board: no live agents -> the phase above is stale, say so honestly
145
+ if (e.idle) e.phase = `idle · last activity ${e.lastActivity ? fmtAge(now() - e.lastActivity) : "unknown"}`;
129
146
  }
130
147
  return json(res, 200, { projects: Object.values(byProj) });
131
148
  }
@@ -209,10 +226,10 @@ const server = http.createServer(async (req, res) => {
209
226
  return json(res, 200, { messages: state.messages.slice(-n) });
210
227
  }
211
228
  if (req.method === "GET" && (P === "/" || P === "/ui")) {
212
- res.writeHead(200, { "content-type": "text/html; charset=utf-8" }); return res.end(UI || "<h1>agent-bus</h1><p>dashboard unavailable</p>");
229
+ res.writeHead(200, { "content-type": "text/html; charset=utf-8" }); return res.end(UI || "<h1>trantor</h1><p>dashboard unavailable</p>");
213
230
  }
214
231
  if (P === "/health") return json(res, 200, { ok: true, peers: Object.keys(state.peers).length, messages: state.messages.length, streams: streams.length });
215
232
  json(res, 404, { error: "not found" });
216
233
  } catch (e) { json(res, 500, { error: String(e?.message || e) }); }
217
234
  });
218
- server.listen(PORT, HOST, () => console.error(`[agent-bus] hub on http://${HOST}:${PORT} (data: ${DATA})`));
235
+ server.listen(PORT, HOST, () => console.error(`[trantor] hub on http://${HOST}:${PORT} (data: ${DATA})`));
package/mcp.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- // agent-bus MCP server — gives ANY MCP-capable agent (Claude Code, Codex, Gemini, …)
2
+ // trantor MCP server — gives ANY MCP-capable agent (Claude Code, Codex, Gemini, …)
3
3
  // tools to talk to OTHER live agent sessions through the relay hub. Loaded per-session
4
4
  // via the agent's MCP config. Identity + hub URL come from env (RELAY_SESSION, RELAY_URL).
5
5
  // Loading this server AUTO-REGISTERS the session — so presence works on every agent.
@@ -37,7 +37,7 @@ async function api(method, path, payload) {
37
37
  }
38
38
  const fmt = (m) => `#${m.id} [${m.from} -> ${m.to}] ${new Date(m.ts).toLocaleTimeString()}: ${m.text}`;
39
39
 
40
- const server = new McpServer({ name: "agent-bus", version: "0.1.0" });
40
+ const server = new McpServer({ name: "trantor", version: "0.1.0" });
41
41
 
42
42
  server.tool("relay_whoami", "Show this session's relay identity, project, and the hub URL.", {}, async () => {
43
43
  await api("POST", "/register", { session: SESSION, project: PROJECT }).catch(() => {});
@@ -153,4 +153,4 @@ server.tool("relay_wait", "Block up to `timeout` seconds waiting for the next me
153
153
 
154
154
  await api("POST", "/register", { session: SESSION, project: PROJECT, status: `active in ${PROJECT}` }).catch(() => {});
155
155
  await server.connect(new StdioServerTransport());
156
- process.stderr.write(`[agent-bus-mcp] connected as ${SESSION} -> ${URL_BASE}\n`);
156
+ process.stderr.write(`[trantor-mcp] connected as ${SESSION} -> ${URL_BASE}\n`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "trantor",
3
- "version": "0.16.0",
3
+ "version": "0.17.1",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "trantor": "bin/cli.mjs"
@@ -12,7 +12,7 @@
12
12
  "scripts": {
13
13
  "test": "node test.mjs && node test-scenarios.mjs"
14
14
  },
15
- "description": "The hub-world for AI agent crews \u2014 orchestrate Claude Code, Codex, Gemini, Kimi & DeepSeek as live crews with a plan-aware Advisor, a Kanban/flow command center, a testing gate, and an economics brain (Scrooge).",
15
+ "description": "The hub-world for AI agent crews orchestrate Claude Code, Codex, Gemini, Kimi & DeepSeek as live crews with a plan-aware Advisor, a Kanban/flow command center, a testing gate, and an economics brain (Scrooge).",
16
16
  "files": [
17
17
  "hub.mjs",
18
18
  "mcp.mjs",
@@ -52,4 +52,4 @@
52
52
  "engines": {
53
53
  "node": ">=18"
54
54
  }
55
- }
55
+ }
@@ -1,9 +1,9 @@
1
1
  ---
2
2
  name: crew
3
- description: Orchestrate a multi-agent build over agent-bus — get an Advisor recommendation (solo/scrooge/crew/hybrid based on the user's plans and the work), fire up helper AI CLIs (Codex, Gemini, Kimi, DeepSeek) with pinned models in visible terminal windows, assign difficulty-tagged work over the bus, track it on the Kanban dashboard with a testing gate, delegate grunt to Scrooge, supervise actively, integrate, ship. Use when the user wants several AI agents building something together, says "fire up the crew/agents", or asks to coordinate other coding CLIs on a task.
3
+ description: Orchestrate a multi-agent build with Trantor — get an Advisor recommendation (solo/scrooge/crew/hybrid based on the user's plans and the work), fire up helper AI CLIs (Codex, Gemini, Kimi, DeepSeek) with pinned models in visible terminal windows, assign difficulty-tagged work over the bus, track it on the Kanban dashboard with a testing gate, delegate grunt to Scrooge, supervise actively, integrate, ship. Use when the user wants several AI agents building something together, says "fire up the crew/agents", "build it with trantor / with the crew", or asks to coordinate other coding CLIs on a task.
4
4
  ---
5
5
 
6
- # agent-bus crew — the unified playbook (brain × body)
6
+ # Trantor crew — the unified playbook (brain × body)
7
7
 
8
8
  You are the ARCHITECT. Two execution fabrics serve you:
9
9
  - **Scrooge calls** (`relay_scrooge`) — cheap stateless one-shots; the result returns to you.
@@ -25,7 +25,7 @@ inside a question dialog), paste verbatim: `routing_table_md`, the `why` bullets
25
25
  and the real-money total + quota pools. ONLY THEN ask go / adjust / hold. When creating the
26
26
  board, use `card_args` exactly — each entry is a ready `relay_task_add` call (title,
27
27
  difficulty, assignee with your project substituted, model). Cards without their model set
28
- are a defect. If the profile is unset, say so and suggest `node bin/profile.mjs set …`.
28
+ are a defect. If the profile is unset, say so and suggest `trantor profile set …`.
29
29
 
30
30
  ## Phase 0 — plan (if the user wants a plan first)
31
31
  PRD.md + TDD.md. The TDD MUST define one file-set per agent (no merge conflicts) and an
@@ -36,11 +36,12 @@ explicit EVENT/INTERFACE CONTRACT — cross-agent bugs come from contract drift.
36
36
  2. One card per package: `relay_task_add(title, assignee, difficulty, model)` — set `model`
37
37
  to the advisor-routed model (or the CLI's default name); difficulty + model show as badges
38
38
  on the card. Assignees: `codex:<project>` etc. Keep one for yourself.
39
- 3. Open the dashboard: `open -na "Google Chrome" --args --new-window <hub-url>`
39
+ 3. Open the dashboard: `trantor ui` (or `open -na "Google Chrome" --args --new-window <hub-url>`)
40
40
 
41
41
  ## Phase 2 — fire up the crew (with the Advisor's models)
42
- `bash <plugin-root>/bin/crew.sh up codex:gpt-5.5 gemini kimi deepseek:deepseek-v4-pro`
42
+ `trantor up codex:gpt-5.5 gemini kimi deepseek:deepseek-v4-pro`
43
43
  — `agent:model` pins a model (omit to use that CLI's default; use what relay_advise routed).
44
+ (If `trantor` isn't on PATH, the same launcher is `bash <plugin-root>/bin/crew.sh up …`.)
44
45
  The launcher auto-wires configs, spawns serialized runner windows, then **VERIFIES each agent
45
46
  on the bus with one retry**. READ ITS OUTPUT: it ends "crew verified" or "✗✗ CREW INCOMPLETE"
46
47
  naming no-shows. **Never assign work to an unverified agent.** The bus is the truth.
@@ -58,7 +59,7 @@ Loop until the board is done — you are a foreman, not a mailbox:
58
59
  (assignee lastSeen fresh? runner heartbeats keep live agents fresh in seconds — stale =
59
60
  dead), spot-check files on disk.
60
61
  3. ACT within one cycle: failed card → read the report, send a fix contract, card back to
61
- doing · dead agent → `crew.sh up <agent>` (re-verifies) + resend contract · silent-but-alive
62
+ doing · dead agent → `trantor up <agent>` (re-verifies) + resend contract · silent-but-alive
62
63
  → direct-message nudge naming the card.
63
64
  4. Grunt sub-tasks that appear mid-build (a regex, a config block, a doc paragraph) →
64
65
  `relay_scrooge`, don't burn a crew seat or your own window.
@@ -71,7 +72,7 @@ Card flow is `todo → doing → testing → done`; `testing` runs the project's
71
72
  Enforce the gate — bounce anything that skipped it (bounces are visible: "↩ bounced" on the
72
73
  card, history in its tooltip). When all report done: integrate, fix contract mismatches
73
74
  YOURSELF, move your card through testing → done, broadcast "🚀 <thing> is live", and when the
74
- user is finished: `bash .../crew.sh down`.
75
+ user is finished: `trantor down`.
75
76
 
76
77
  ## Rules
77
78
  - Coordinate ONLY over the bus; messages <280 chars; the dashboard lanes are the user's view.
@@ -1,9 +1,9 @@
1
1
  ---
2
- name: relay-handoff
2
+ name: handoff
3
3
  description: |
4
4
  Write a rich handoff for the CURRENT session so a fresh Claude Code session can take over
5
5
  with a full new context window (instead of compacting). Use proactively when context is
6
- getting full, or before ending, to pass the baton cleanly. Trigger: /agent-bus:relay-handoff
6
+ getting full, or before ending, to pass the baton cleanly. Trigger: /trantor:handoff
7
7
  user-invocable: true
8
8
  ---
9
9
 
package/ui.html CHANGED
@@ -1,6 +1,6 @@
1
1
  <!doctype html>
2
2
  <html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
3
- <title>agent-bus</title>
3
+ <title>trantor</title>
4
4
  <style>
5
5
  :root{--bg:#0a0e16;--panel:#111726;--card:#161d2e;--card2:#1b2335;--line:#1f2839;--tx:#e6edf6;--mut:#8a97a8;--dim:#5b6675;
6
6
  --grn:#2dd4bf;--grn2:#14b8a6;--amb:#f59e0b;--blu:#4a90d9;--red:#ef6a6a;--pur:#9b6fd4}
@@ -56,6 +56,20 @@ main{flex:1;display:grid;grid-template-columns:1fr 330px;min-height:0}
56
56
  .tcard.done{opacity:.65}.tcard.done span{text-decoration:line-through}
57
57
  .empty{color:var(--dim);text-align:center;padding:30px}
58
58
  .empty.big{padding:60px 20px;font-size:15px}
59
+ /* idle projects — collapsed thin rows below the live boards */
60
+ .idle-head{font-size:10.5px;text-transform:uppercase;letter-spacing:.09em;color:var(--dim);font-weight:700;margin:18px 2px 8px}
61
+ .idle-row{display:flex;align-items:center;gap:7px;background:var(--panel);border:1px solid var(--line);border-radius:10px;padding:7px 14px;margin-bottom:7px;font-size:12.5px;color:var(--mut);cursor:pointer}
62
+ .idle-row:hover{background:var(--card);border-color:#2c3a52}
63
+ .idle-row .nm{color:var(--tx);font-weight:600}
64
+ .mv{display:inline-flex;gap:3px;margin-left:6px}
65
+ .mv b{cursor:pointer;color:var(--dim);font-size:10px;line-height:1;padding:3px 5px;border:1px solid var(--line);border-radius:6px;background:var(--card);font-weight:400}
66
+ .mv b:hover{color:var(--tx);border-color:#2c3a52}
67
+ .pdel{cursor:pointer;color:var(--dim);font-size:10.5px;padding:2px 7px;border:1px solid var(--line);border-radius:6px;background:var(--card);margin-left:5px;white-space:nowrap}
68
+ .pdel:hover{color:var(--red);border-color:#5a2c2c}
69
+ .pdel.arm{color:#fff;background:var(--red);border-color:var(--red)}
70
+ .sortmode{font-size:10.5px;color:var(--dim);margin:0 0 10px;cursor:pointer;user-select:none}
71
+ .sortmode:hover{color:var(--mut)}
72
+ .proj.idle .proj-h{opacity:.55;cursor:pointer}
59
73
  /* flow (DAG) view */
60
74
  .vtog{display:flex;gap:2px;margin-left:10px}
61
75
  .vbtn{font-size:10px;font-weight:700;letter-spacing:.05em;padding:2px 9px;border-radius:10px;border:1px solid var(--line);background:var(--card);color:var(--dim);cursor:pointer}
@@ -120,7 +134,7 @@ aside h2{font-size:10.5px;text-transform:uppercase;letter-spacing:.09em;color:va
120
134
  <body>
121
135
  <header>
122
136
  <span class="dot" id="livedot"></span>
123
- <span class="logo">agent<b>·</b>bus</span>
137
+ <span class="logo">tran<b>t</b>or</span>
124
138
  <span class="pill" id="hub">—</span>
125
139
  <span class="spacer"></span>
126
140
  <span class="pill" id="econ" title="Scrooge ledger, last 24h" style="display:none"></span>
@@ -160,7 +174,7 @@ const ICON={
160
174
  const BRAND=[[/claude|anthropic/,'anthropic'],[/codex|openai|gpt|chatgpt/,'openai'],[/gemini|bard/,'gemini'],[/mistral|mixtral/,'mistral'],[/deepseek/,'deepseek'],[/kimi|moonshot|\bk2\b/,'moonshot'],[/qwen/,'qwen'],[/grok|xai/,'grok'],[/cursor/,'cursor']];
161
175
  function brandOf(s){const x=String(s).toLowerCase();for(const[re,b]of BRAND)if(re.test(x))return b;return null;}
162
176
  function iconFor(s,size){const b=brandOf(s);if(!b||!ICON[b])return `<span style="display:inline-flex;width:${size}px;height:${size}px;align-items:center;justify-content:center;border-radius:50%;background:#2a3346;color:#aeb9c9;font-size:${Math.round(size*.55)}px;font-weight:700">${esc(String(s).slice(0,1).toUpperCase())}</span>`;const i=ICON[b];return `<svg viewBox="0 0 24 24" width="${size}" height="${size}" fill="currentColor" style="color:${i.c}">${i.p.map(d=>`<path d="${d}"/>`).join('')}</svg>`;}
163
- const phaseClass=ph=>/FAILED|blocked/.test(ph)?'blocked':/building|verifying|progress/.test(ph)?'building':/shipped|done/.test(ph)?'shipped':'planned';
177
+ const phaseClass=ph=>/^idle ·/.test(ph)?'planned':/FAILED|blocked/.test(ph)?'blocked':/building|verifying|progress/.test(ph)?'building':/shipped|done/.test(ph)?'shipped':'planned';
164
178
 
165
179
  let POOLS={};
166
180
  async function econ(){
@@ -178,6 +192,16 @@ econ();setInterval(econ,15000);
178
192
  function poolOf(session){const b=brandOf(session);const k=b==='anthropic'?'claude':b==='openai'?'codex':b==='moonshot'?'kimi':b;return POOLS[k]||'';}
179
193
  const VIEWS = JSON.parse(localStorage.getItem("abViews") || "{}");
180
194
  function setView(proj, v){ VIEWS[proj] = v; localStorage.setItem("abViews", JSON.stringify(VIEWS)); render(); }
195
+ function toggleIdle(name){ if(!name)return; const s=new Set(JSON.parse(localStorage.getItem("abIdleOpen")||"[]")); s.has(name)?s.delete(name):s.add(name); localStorage.setItem("abIdleOpen",JSON.stringify([...s])); render(); }
196
+ let armedDel=null,armedTs=0; // pending ✕ confirmation (project name + when it was armed)
197
+ // ▲▼: snapshot the currently displayed order, swap, persist — manual order then wins until ↺ reset
198
+ function moveProj(name,dir){
199
+ const names=[...document.querySelectorAll('#boards [data-projname]')].map(d=>d.dataset.projname);
200
+ const i=names.indexOf(name),j=i+dir;
201
+ if(i<0||j<0||j>=names.length)return;
202
+ [names[i],names[j]]=[names[j],names[i]];
203
+ localStorage.setItem("abOrder",JSON.stringify(names)); render();
204
+ }
181
205
  function flowLayout(cards, proj){
182
206
  const byId = Object.fromEntries(cards.map(t => [t.id, t]));
183
207
  let anyDeps = cards.some(t => (t.deps || []).length);
@@ -343,12 +367,21 @@ async function render(){
343
367
  const sel=$('#to'),cur=sel.value;
344
368
  sel.innerHTML='<option value="all">all (broadcast)</option>'+allS.map(s=>`<option value="${esc(s)}">${esc(s)}</option>`).join('');
345
369
  sel.value=cur;
346
- // sort: projects with online agents first, then by name; unassigned last
347
- projects.sort((a,b)=>{const oa=a.agents.some(x=>x.online),ob=b.agents.some(x=>x.online);if(oa!==ob)return ob-oa;if(a.project==='(unassigned)')return 1;if(b.project==='(unassigned)')return -1;return a.project.localeCompare(b.project);});
370
+ // sort: manual order (abOrder, set via the ▲▼ controls) wins; otherwise online
371
+ // first, then most-recent activity working boards float to the top on their own
372
+ const ORDER=JSON.parse(localStorage.getItem("abOrder")||"[]");
373
+ projects.sort((a,b)=>{
374
+ const ia=ORDER.indexOf(a.project),ib=ORDER.indexOf(b.project);
375
+ if(ia>=0||ib>=0)return (ia<0?1e9:ia)-(ib<0?1e9:ib);
376
+ const oa=a.agents.some(x=>x.online),ob=b.agents.some(x=>x.online);if(oa!==ob)return ob-oa;
377
+ if(a.project==='(unassigned)')return 1;if(b.project==='(unassigned)')return -1;
378
+ return (b.lastActivity||0)-(a.lastActivity||0)||a.project.localeCompare(b.project);
379
+ });
348
380
  const el=$('#boards');
349
381
  if(dragging)return; // never rebuild mid-gesture
350
382
  if(!projects.length){el.innerHTML='<div class="empty big">no projects yet — agents register a project on connect</div>';return;}
351
- el.innerHTML=projects.map(p=>{
383
+ const idleOpen=new Set(JSON.parse(localStorage.getItem("abIdleOpen")||"[]"));
384
+ const projBlock=p=>{
352
385
  const pt=tasks.filter(t=>t.project===p.project);
353
386
  const done=pt.filter(t=>t.status==='done').length;
354
387
  const pct=pt.length?Math.round(done/pt.length*100):0;
@@ -371,18 +404,46 @@ async function render(){
371
404
  const pmsgs=msgs.filter(m=>projOf(m)===p.project);
372
405
  const view = VIEWS[p.project] || "board";
373
406
  const vtog = `<div class="vtog"><button class="vbtn ${view==="board"?"on":""}" data-proj="${esc(p.project)}" data-view="board">BOARD</button><button class="vbtn ${view==="flow"?"on":""}" data-proj="${esc(p.project)}" data-view="flow">FLOW</button></div>`;
374
- return `<div class="proj">`+
375
- `<div class="proj-h"><span class="pname">📁 <b>${esc(p.project)}</b></span>${vtog}<div class="agents">${agents||'<span class="dim">no agents</span>'}</div><span class="spacer"></span><span class="prog">${done}/${pt.length} done · ${pct}%</span></div>`+
407
+ const ctl=`<span class="mv"><b class="mvup" data-proj="${esc(p.project)}" title="move up">▲</b><b class="mvdn" data-proj="${esc(p.project)}" title="move down">▼</b></span>${p.idle===true?`<span class="pdel" data-proj="${esc(p.project)}" title="forget this project (cards, peers, brief) — it returns if an agent registers it again">✕</span>`:''}`;
408
+ return `<div class="proj${p.idle===true?' idle':''}" data-projname="${esc(p.project)}"${p.idle===true?` data-idleproj="${esc(p.project)}"`:''}>`+
409
+ `<div class="proj-h"><span class="pname">📁 <b>${esc(p.project)}</b></span>${vtog}<div class="agents">${agents||'<span class="dim">no agents</span>'}</div><span class="spacer"></span><span class="prog">${done}/${pt.length} done · ${pct}%</span>${ctl}</div>`+
376
410
  `<div class="proj-brief">${brief}${ph?`<span class="phase ${phaseClass(ph)}">${esc(ph)}</span>`:''}</div>`+
377
411
  `<div class="pbar"><i style="width:${pct}%"></i></div>`+
378
412
  (view === "flow" ? flowHTML(pt, p.project) : `<div class="kanban">${cols}</div>`)+
379
413
  chatLane(pmsgs)+
380
414
  `</div>`;
381
- }).join('');
415
+ };
416
+ // idle projects (zero online agents) collapse to thin rows below the live boards
417
+ const idleRow=p=>{
418
+ const pt=tasks.filter(t=>t.project===p.project);const done=pt.filter(t=>t.status==='done').length;
419
+ return `<div class="idle-row" data-projname="${esc(p.project)}" data-idleproj="${esc(p.project)}" title="click to expand">💤 <span class="nm">${esc(p.project)}</span><span class="dim">· ${esc(p.phase||'')} · ${done}/${pt.length} cards</span><span class="spacer"></span><span class="mv"><b class="mvup" data-proj="${esc(p.project)}" title="move up">▲</b><b class="mvdn" data-proj="${esc(p.project)}" title="move down">▼</b></span><span class="pdel" data-proj="${esc(p.project)}" title="forget this project (cards, peers, brief) — it returns if an agent registers it again">✕</span></div>`;
420
+ };
421
+ const live=projects.filter(p=>p.idle!==true),idlers=projects.filter(p=>p.idle===true);
422
+ el.innerHTML=(ORDER.length?`<div class="sortmode" id="sortreset" title="you've ordered projects manually — click to go back to automatic (recency) ordering">sort: manual · ↺ back to auto</div>`:'')
423
+ +live.map(projBlock).join('')
424
+ +(idlers.length?`<div class="idle-head">idle projects · ${idlers.length}</div>`+idlers.map(p=>idleOpen.has(p.project)?projBlock(p):idleRow(p)).join(''):'');
382
425
  // keep each project's chat scrolled to the latest line
383
426
  el.querySelectorAll('.chatlog').forEach(c=>{if(c.scrollHeight-c.clientHeight<60||c.dataset.stuck!=='0')c.scrollTop=c.scrollHeight;c.onscroll=()=>{c.dataset.stuck=(c.scrollHeight-c.scrollTop-c.clientHeight<40)?'1':'0';};});
384
427
  // click a card -> advance status todo->doing->done
385
428
  el.querySelectorAll('.vbtn').forEach(b=>b.onclick=()=>setView(b.dataset.proj,b.dataset.view));
429
+ // collapsed idle row -> expand; expanded idle project's header -> collapse again
430
+ el.querySelectorAll('.idle-row').forEach(r=>r.onclick=e=>{if(e.target.closest('.mv,.pdel'))return;toggleIdle(r.dataset.idleproj);});
431
+ el.querySelectorAll('.proj.idle .proj-h').forEach(h=>h.onclick=e=>{if(e.target.closest('.vbtn,.agent,.mv,.pdel'))return;toggleIdle(h.closest('.proj').dataset.idleproj);});
432
+ // ▲▼ reorder (persists as a manual order); ✕ forget — armed two-step, no popup
433
+ el.querySelectorAll('.mvup').forEach(b=>b.onclick=e=>{e.stopPropagation();moveProj(b.dataset.proj,-1);});
434
+ el.querySelectorAll('.mvdn').forEach(b=>b.onclick=e=>{e.stopPropagation();moveProj(b.dataset.proj,1);});
435
+ el.querySelectorAll('.pdel').forEach(b=>b.onclick=async e=>{
436
+ e.stopPropagation();
437
+ const p=b.dataset.proj;
438
+ if(!(armedDel===p&&Date.now()-armedTs<4000)){armedDel=p;armedTs=Date.now();b.classList.add('arm');b.textContent='✕ sure?';return;}
439
+ armedDel=null;
440
+ await fetch('/project/delete',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({project:p})});
441
+ render();
442
+ });
443
+ // the 2.5s re-render rebuilds the DOM — re-apply a still-fresh armed state so the
444
+ // "✕ sure?" confirmation survives until it expires instead of silently resetting
445
+ if(armedDel&&Date.now()-armedTs<4000){const ab=el.querySelector(`.pdel[data-proj="${CSS.escape(armedDel)}"]`);if(ab){ab.classList.add('arm');ab.textContent='✕ sure?';}}
446
+ const sr=$('#sortreset');if(sr)sr.onclick=()=>{localStorage.removeItem('abOrder');render();};
386
447
  wireFlow(el);
387
448
  el.querySelectorAll('.tcard, .fnode').forEach(c=>c.onclick=async()=>{
388
449
  const id=+c.dataset.id,t=tasks.find(x=>x.id===id);if(!t)return;