trantor 0.18.8 → 0.18.9

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.9",
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/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.9",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "trantor": "bin/cli.mjs"