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.
- package/.claude-plugin/plugin.json +1 -1
- package/bin/app.mjs +19 -4
- 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.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
|
-
//
|
|
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/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
|
|