trantor 0.18.8 → 0.18.10

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,6 +1,6 @@
1
1
  {
2
2
  "name": "trantor",
3
- "version": "0.18.8",
3
+ "version": "0.18.10",
4
4
  "description": "Trantor \u2014 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": {
package/bin/app.mjs CHANGED
@@ -77,9 +77,21 @@ await pipeline(Readable.fromWeb(dl.body), createWriteStream(dmg));
77
77
 
78
78
  let mount = "";
79
79
  try {
80
- // -nobrowse keeps the volume out of Finder; mount point is the last tab-field of the last line.
81
- const out = sh("hdiutil", ["attach", "-nobrowse", "-readonly", dmg]);
82
- mount = (out.trim().split("\n").pop() || "").split("\t").pop().trim();
80
+ // diskutil first: on macOS 26 the deprecated hdiutil shim IGNORES -nobrowse, so the mounted
81
+ // volume popped a Finder window mid-update and read as an install prompt (2026-08-27). Parse
82
+ // the mount point as everything after the last " at " volume names can contain spaces.
83
+ try {
84
+ // real output (verified 2026-08-27): tab-separated, same shape as hdiutil —
85
+ // "/dev/disk12s1\tApple_HFS \t/Volumes/Trantor" — last tab field is the mount.
86
+ const out = sh("diskutil", ["image", "attach", "--mountOptions", "nobrowse", "--readOnly", dmg]);
87
+ const line = out.trim().split("\n").filter(l => l.includes("/Volumes/")).pop() || "";
88
+ mount = line.split("\t").pop().trim();
89
+ if (!mount.startsWith("/Volumes/")) throw new Error("no mount point in diskutil output");
90
+ } catch {
91
+ // older macOS: the original hdiutil path, tab-field parse (robust to spaces)
92
+ const out = sh("hdiutil", ["attach", "-nobrowse", "-readonly", dmg]);
93
+ mount = (out.trim().split("\n").pop() || "").split("\t").pop().trim();
94
+ }
83
95
  const src = join(mount, "Trantor.app");
84
96
  if (!mount.startsWith("/Volumes/") || !existsSync(src)) throw new Error(`unexpected DMG layout (mount: ${mount || "none"})`);
85
97
  if (existsSync(APP)) { console.log(`replacing ${APP} (was ${have || "unknown"})`); rmSync(APP, { recursive: true, force: true }); }
@@ -91,6 +103,9 @@ try {
91
103
  } catch (e) {
92
104
  console.error(`install failed: ${e.message}`); process.exitCode = 1;
93
105
  } finally {
94
- if (mount) try { sh("hdiutil", ["detach", mount, "-quiet"]); } catch {}
106
+ if (mount) {
107
+ try { sh("diskutil", ["eject", mount]); }
108
+ catch { try { sh("hdiutil", ["detach", mount, "-quiet"]); } catch {} }
109
+ }
95
110
  try { rmSync(dmg, { force: true }); } catch {}
96
111
  }
package/bin/crew.sh CHANGED
@@ -46,11 +46,21 @@ HAVE_TMUX=0; command -v tmux >/dev/null 2>&1 && HAVE_TMUX=1
46
46
  # Events) — NOT the control socket — so it needs NO socket password (the socket denies external processes
47
47
  # by default; AppleScript bypasses that, exactly like we already script Terminal.app).
48
48
  HAVE_CMUX=0; [ -d "/Applications/cmux.app" ] && HAVE_CMUX=1
49
+ # PRESENCE vs PREFERENCE: HAVE_* answer "which mux do NEW crews get" and are stomped by CREW_MUX and
50
+ # the herdr auto-preference below. *_PRESENT answer "is this mux on the machine at all" and are what
51
+ # PRUNE keys on — a row is validated by ITS OWN kind's liveness, never by which mux new crews prefer.
52
+ # Conflating them let a dead cmux row survive prune the moment herdr became the preferred default.
53
+ CMUX_PRESENT=$HAVE_CMUX
54
+ HERDR_PRESENT=0; command -v herdr >/dev/null 2>&1 && HERDR_PRESENT=1
49
55
  # herdr (https://herdr.dev — a server-held terminal runtime: panes live in its background server, so a
50
- # crew survives the launcher exiting) is OPT-IN ONLY, never auto-detected a machine with herdr
51
- # installed keeps its default cmux→tmux→terminal dispatch untouched. CREW_MUX=herdr is the explicit
52
- # switch; opting in without the binary is a HARD ERROR, because a silent fallback would defeat the ask.
56
+ # crew survives the launcher exiting) is the PREFERRED mux when its binary is installed: it is the
57
+ # only backend the desktop app's Workspace pane can render, so an auto-detected herdr means every
58
+ # `trantor up` on every project shows up live in the app with no flag. (It was opt-in for exactly one
59
+ # wave; the first thing the operator noticed was that other projects' crews stayed invisible.)
60
+ # CREW_MUX=cmux|tmux|terminal still forces the old dispatch; CREW_MUX=herdr without the binary stays
61
+ # a HARD ERROR, because an explicit ask must never silently fall back.
53
62
  HAVE_HERDR=0
63
+ [ -z "${CREW_MUX:-}" ] && command -v herdr >/dev/null 2>&1 && { HAVE_HERDR=1; HAVE_CMUX=0; HAVE_TMUX=0; }
54
64
  # explicit override (user preference or tests): CREW_MUX=cmux|tmux|terminal|herdr forces the grouping UI.
55
65
  case "${CREW_MUX:-}" in
56
66
  cmux) HAVE_CMUX=1; HAVE_TMUX=0 ;;
@@ -100,7 +110,7 @@ _cmux() { CMUX_QUIET=1 "$CMUX_BIN" "$@"; } # quiet CLI wrapper (suppr
100
110
  _CMUX_OK="" # cached: does the control socket accept us (allowAll)?
101
111
  _cmux_ok() {
102
112
  [ -n "$_CMUX_OK" ] && { [ "$_CMUX_OK" = "1" ] && return 0 || return 1; }
103
- if [ "$HAVE_CMUX" = "1" ] && _cmux ping >/dev/null 2>&1; then _CMUX_OK=1; return 0; fi
113
+ if [ "$CMUX_PRESENT" = "1" ] && _cmux ping >/dev/null 2>&1; then _CMUX_OK=1; return 0; fi
104
114
  _CMUX_OK=0; return 1
105
115
  }
106
116
  # resolve a freshly-created workspace REF (workspace:N) → its stable UUID (survives index shifts).
@@ -409,10 +419,11 @@ prune_dead_state() {
409
419
  CLIVE="$(printf '%s' "$pair" | sed -n 1p)"
410
420
  CLIVE_NAMES="$(printf '%s' "$pair" | sed -n 2p)"
411
421
  fi
412
- # herdr liveness is queried ONLY under the explicit opt-in (CREW_MUX=herdr): a default run never
413
- # invokes herdr at all. Same keep-when-unprovable doctrine as cmux — no answer ⇒ rows are KEPT.
422
+ # herdr liveness is queried whenever the BINARY is present (herdr rows deserve validation no
423
+ # matter which mux new crews prefer). Same keep-when-unprovable doctrine as cmux — no answer,
424
+ # or no binary at all ⇒ rows are KEPT.
414
425
  local HLIVE="" HLIVE_NAMES=""
415
- if [ "$HAVE_HERDR" = "1" ]; then
426
+ if [ "$HERDR_PRESENT" = "1" ]; then
416
427
  local hpair
417
428
  hpair="$(_herdr_ws_live)"
418
429
  HLIVE="$(printf '%s' "$hpair" | sed -n 1p)"
package/hub.mjs CHANGED
@@ -343,32 +343,46 @@ function overseerTick() {
343
343
  overseerLastTick = t;
344
344
  const pol = overseerPolicy();
345
345
  const seen = new Set();
346
+ // Hand each party the others' session ids at the moment coordination is warranted. Telling two
347
+ // sessions to "coordinate over the bus" is useless if neither knows the other's id, and the
348
+ // warning alone went only to the duty seat and the log — so coordination needed a human to carry
349
+ // the ids across. Shared by the episode-start branch (all parties) and the standing branch
350
+ // (newcomers only): existing members never re-hear it, so a standing condition must not re-wake
351
+ // every party every tick.
352
+ const intro = (c, me, others) => {
353
+ const rest = others.filter(p => p !== me);
354
+ if (rest.length === 0) return;
355
+ hubSend(me,
356
+ `🤝 OVERSEER ${c.kind}: you and ${rest.join(", ")} are working on overlapping ground${c.files?.length ? ` (${c.files.slice(0, 3).join(", ")})` : ""}. ${c.detail || ""} Coordinate directly — relay_send to ${rest[0]} — and split the work between you. No human needs to relay this.`,
357
+ c.project);
358
+ };
346
359
  for (const c of collisions) {
347
- const key = `${c.project} ${c.kind} ${(c.sessions || []).join(",")} ${(c.files || []).join(",")}`;
360
+ // Episode identity is the CONDITION (project+kind+files), never the session list (#5350):
361
+ // membership is volatile — a third seat bouncing in and out of a standing collision minted a
362
+ // fresh key, so a fresh episode, so a fresh warn (+ duty wake + party intros) per permutation.
363
+ // Sessions are participants, not identity; current membership still rides every warn payload.
364
+ const key = `${c.project} ${c.kind} ${(c.files || []).join(",")}`;
348
365
  c.key = key;
349
366
  seen.add(key);
367
+ const parties = [...new Set(c.sessions || [])].filter(s => s && s !== DUTY_SESSION);
350
368
  const standing = overseerActive.get(key);
351
- if (standing) { standing.lastTick = t; c.since = standing.since; continue; } // holds — stay quiet
352
- overseerActive.set(key, { since: t, lastTick: t });
369
+ if (standing) {
370
+ // The episode HOLDS — no new warn. But a NEWCOMER to a standing collision still needs the
371
+ // intro: it was not present when the episode started, so it never learned the others' ids.
372
+ // Diff the current membership against the set the episode has already introduced, hand the
373
+ // intro only to newly arrived sessions, and remember them so they are not re-introduced.
374
+ standing.lastTick = t;
375
+ c.since = standing.since;
376
+ for (const me of parties) if (!standing.sessions.has(me)) intro(c, me, parties);
377
+ for (const me of parties) standing.sessions.add(me);
378
+ continue;
379
+ }
380
+ overseerActive.set(key, { since: t, lastTick: t, sessions: new Set(parties) });
353
381
  c.since = t;
354
382
  appendEvent("overseer.warn", c.project, "overseer",
355
383
  { kind: c.kind, sessions: c.sessions || [], files: c.files || [], detail: c.detail || "", narrated: false });
356
384
  if (DUTY_SESSION) hubSend(DUTY_SESSION, `⚠️ OVERSEER ${c.kind} [${c.project}]: ${c.detail || ""} — if the parties are not already coordinating, message them.`, c.project);
357
- // INTRODUCE the parties to each other. Telling two sessions to "coordinate over the bus" is
358
- // useless if neither knows the other's session id, and until now the warning went only to the
359
- // duty seat and the log — so coordination needed a human to carry the ids across. Hand each
360
- // party the others' ids at the moment coordination is warranted. This sits inside the
361
- // episode-start branch, so it fires ONCE per episode, not once per tick: a standing condition
362
- // must not re-wake two sessions every 30 seconds.
363
- const parties = [...new Set(c.sessions || [])].filter(s => s && s !== DUTY_SESSION);
364
- if (parties.length > 1) {
365
- for (const me of parties) {
366
- const others = parties.filter(p => p !== me);
367
- hubSend(me,
368
- `🤝 OVERSEER ${c.kind}: you and ${others.join(", ")} are working on overlapping ground${c.files?.length ? ` (${c.files.slice(0, 3).join(", ")})` : ""}. ${c.detail || ""} Coordinate directly — relay_send to ${others[0]} — and split the work between you. No human needs to relay this.`,
369
- c.project);
370
- }
371
- }
385
+ if (parties.length > 1) for (const me of parties) intro(c, me, parties);
372
386
  const level = _overseer.levelFor ? _overseer.levelFor(c.project, pol.autonomy) : 1;
373
387
  if (level >= 3 && c.kind === "file-conflict") {
374
388
  const g = { id: ++state.verifyGateSeq, project: c.project, status: "open", ts: now(),
package/lib/project.mjs CHANGED
@@ -21,6 +21,20 @@ export function gitRoot(dir) {
21
21
  export function resolveProject(cwd = process.cwd()) {
22
22
  if (process.env.RELAY_PROJECT) return process.env.RELAY_PROJECT.slice(0, 80);
23
23
  const root = gitRoot(cwd);
24
+ // A LINKED WORKTREE must resolve to its MAIN repo's name, not its own directory name. Seat
25
+ // worktrees live at ~/.agent-bus/worktrees/<project>/<agent>, so the old basename rule named the
26
+ // project after the AGENT — codex's seat registered as codex:codex the first time a worktree crew
27
+ // came up (2026-08-27), because codex spawns its MCP with a sanitized env, so the RELAY_PROJECT
28
+ // guard above never arrives there. git-common-dir points at the main repo's .git from any
29
+ // worktree; in the main repo it equals its own .git, so this is a no-op for normal checkouts.
30
+ if (root) {
31
+ try {
32
+ const common = execSync("git rev-parse --path-format=absolute --git-common-dir", {
33
+ cwd: root, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout: 4000,
34
+ }).trim();
35
+ if (common.endsWith("/.git")) return basename(dirname(common)).slice(0, 80);
36
+ } catch {}
37
+ }
24
38
  return basename(root || cwd).slice(0, 80);
25
39
  }
26
40
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "trantor",
3
- "version": "0.18.8",
3
+ "version": "0.18.10",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "trantor": "bin/cli.mjs"