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.
- package/.claude-plugin/plugin.json +1 -1
- package/bin/app.mjs +19 -4
- package/bin/crew.sh +18 -7
- package/hub.mjs +32 -18
- package/lib/project.mjs +14 -0
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "trantor",
|
|
3
|
-
"version": "0.18.
|
|
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
|
-
//
|
|
81
|
-
|
|
82
|
-
mount
|
|
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)
|
|
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
|
|
51
|
-
#
|
|
52
|
-
#
|
|
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 [ "$
|
|
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
|
|
413
|
-
#
|
|
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 [ "$
|
|
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
|
-
|
|
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) {
|
|
352
|
-
|
|
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
|
-
|
|
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
|
|