trantor 0.17.63 → 0.17.65
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 +4 -1
- package/bin/crew-runner.mjs +21 -5
- package/bin/crew.sh +2 -1
- package/bin/duty.mjs +3 -1
- package/hooks/heartbeat.mjs +16 -0
- package/hub.mjs +32 -1
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "trantor",
|
|
3
|
-
"version": "0.17.
|
|
3
|
+
"version": "0.17.65",
|
|
4
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": {
|
package/bin/app.mjs
CHANGED
|
@@ -40,7 +40,10 @@ function installedVersion() {
|
|
|
40
40
|
// Newest release carrying a Trantor DMG for this arch (falls back to any Trantor DMG — old
|
|
41
41
|
// releases may predate multi-arch naming). GITHUB_TOKEN is honored but not required (public repo).
|
|
42
42
|
async function latestAppRelease() {
|
|
43
|
-
|
|
43
|
+
// cache-control: GitHub serves unauthenticated API responses through a shared ~60s cache — a
|
|
44
|
+
// release published seconds ago comes back MISSING and `app update` re-installs the previous
|
|
45
|
+
// version (observed live on the 0.2.0 release). no-cache punches through it.
|
|
46
|
+
const headers = { accept: "application/vnd.github+json", "user-agent": "trantor-app", "cache-control": "no-cache" };
|
|
44
47
|
if (process.env.GITHUB_TOKEN) headers.authorization = `Bearer ${process.env.GITHUB_TOKEN}`;
|
|
45
48
|
const r = await fetch(`https://api.github.com/repos/${REPO}/releases?per_page=30`, { headers, signal: AbortSignal.timeout(15000) });
|
|
46
49
|
if (!r.ok) throw new Error(`GitHub API ${r.status} — ${(await r.text()).slice(0, 200)}`);
|
package/bin/crew-runner.mjs
CHANGED
|
@@ -83,14 +83,23 @@ async function api(path, body) {
|
|
|
83
83
|
const CMUX_BIN = process.env.CMUX_BIN
|
|
84
84
|
|| (existsSync("/Applications/cmux.app/Contents/Resources/bin/cmux") ? "/Applications/cmux.app/Contents/Resources/bin/cmux" : "cmux");
|
|
85
85
|
const inCmux = () => !!process.env.CMUX_SURFACE_ID;
|
|
86
|
-
|
|
86
|
+
// Brand colors — the SAME hexes the desktop app's Avatar.tsx uses, so a seat is the same color in
|
|
87
|
+
// the cmux sidebar and the Trantor app. cmux status icons are a fixed named set (no images), so an
|
|
88
|
+
// actual LLM logo in the pill is not possible — brand COLOR + the agent's name in the label is the
|
|
89
|
+
// closest cmux allows.
|
|
90
|
+
const BRAND_HEX = { claude: "#D97757", codex: "#e8e8ee", openai: "#e8e8ee", deepseek: "#5786FE",
|
|
91
|
+
kimi: "#8b8bf5", moonshot: "#8b8bf5", glm: "#5ea0f5", zai: "#5ea0f5", gemini: "#8E75B2", openrouter: "#94A3B8" };
|
|
92
|
+
function cmuxStatus(value, color, icon = "robot", opts = {}) {
|
|
87
93
|
if (!inCmux()) return;
|
|
88
94
|
// Label with the REAL seat identity, not a literal. This was hardcoded to "trantor", so every seat
|
|
89
95
|
// in every project reported under one name — four different agents (and their duplicates) rendered
|
|
90
96
|
// identically in the sidebar, which is why a runner leak looked like mystery sessions instead of
|
|
91
97
|
// obvious duplicates. Note this is the DISPLAY path; two previous fixes to the crossed-label
|
|
92
98
|
// symptom both landed on the *bus* identity and never touched this line.
|
|
93
|
-
|
|
99
|
+
// Pill = "<agent> · <state>" in the agent's BRAND color (alerts keep their alarm color — a red
|
|
100
|
+
// error must read as red at a glance); errors sort first via --priority.
|
|
101
|
+
const col = opts.alert ? color : (BRAND_HEX[AGENT.toLowerCase()] || color);
|
|
102
|
+
try { spawnSync(CMUX_BIN, ["set-status", SESSION, `${AGENT} · ${value}`, "--color", col, "--icon", icon, "--priority", String(opts.priority ?? 0)], { stdio: "ignore", timeout: 1500, env: { ...process.env, CMUX_QUIET: "1" } }); } catch {}
|
|
94
103
|
}
|
|
95
104
|
function cmuxLog(message, level = "info") {
|
|
96
105
|
if (!inCmux()) return;
|
|
@@ -170,7 +179,7 @@ async function reportFailure(exit, trigger) {
|
|
|
170
179
|
? `🛑 ${SESSION} DOWN — ${consecFails} consecutive failures (${reason}, exit ${exit})${hint}`
|
|
171
180
|
: `⚠️ ${SESSION} turn FAILED (${trigger}, exit ${exit} · ${reason})${hint}`;
|
|
172
181
|
await api("/send", { from: SESSION, to: "all", text, project: PROJ }).catch(() => {});
|
|
173
|
-
cmuxStatus(down ? "down" : "error", "#ef6a6a", "alert"); cmuxLog(`turn failed: ${reason} (exit ${exit})`, "error");
|
|
182
|
+
cmuxStatus(down ? "down" : "error", "#ef6a6a", "alert", { alert: true, priority: 90 }); cmuxLog(`turn failed: ${reason} (exit ${exit})`, "error");
|
|
174
183
|
log(`\x1b[31mreported failure to bus: ${reason} (exit ${exit})\x1b[0m`);
|
|
175
184
|
}
|
|
176
185
|
|
|
@@ -195,7 +204,7 @@ function runTurn(prompt, isFirst, trigger = "kickoff") {
|
|
|
195
204
|
const envs = [join(homedir(), ".agent-bus", ".env"), cli.env].filter(f => f && existsSync(f));
|
|
196
205
|
for (const f of envs.reverse()) cmd = `set -a; source ${f}; set +a; ${cmd}`; // ~/.agent-bus/.env wins
|
|
197
206
|
log(`turn starting (${isFirst ? "fresh session" : "resume"})${MODEL ? ` · model=${MODEL}` : ""}`);
|
|
198
|
-
cmuxStatus("building", "#4a90d9", "hammer");
|
|
207
|
+
cmuxStatus("building", "#4a90d9", "hammer", { priority: 50 });
|
|
199
208
|
// inherit stdio so the window shows the agent working live; also capture for sid-parsing.
|
|
200
209
|
// Tee stderr to ERRF (still shown live in the window) so a failed turn can be classified.
|
|
201
210
|
try { appendFileSync(ERRF, "", { flag: "w" }); } catch {}
|
|
@@ -256,7 +265,14 @@ async function loadLessons() {
|
|
|
256
265
|
try {
|
|
257
266
|
const r = await api(`/poll?session=${encodeURIComponent(SESSION)}&since=${cursor}&wait=280`);
|
|
258
267
|
msgs = r.messages || []; cursor = r.cursor ?? cursor;
|
|
259
|
-
} catch (e) {
|
|
268
|
+
} catch (e) {
|
|
269
|
+
// Deadline-abort on the LONG-POLL is not an outage — it means the hold expired with no hub
|
|
270
|
+
// response (stalled event loop, napped machine, dead socket). Reconnect immediately and say
|
|
271
|
+
// so calmly; reserve the scary "hub unreachable" + 5s backoff for real connection failures.
|
|
272
|
+
const expired = e && (e.name === "TimeoutError" || /abort/i.test(String(e.message)));
|
|
273
|
+
log(expired ? `long-poll hold expired with no hub response — reconnecting` : `hub unreachable (${e.message}) — retrying in 5s`);
|
|
274
|
+
await new Promise(s => setTimeout(s, expired ? 250 : 5000)); continue;
|
|
275
|
+
}
|
|
260
276
|
if (!msgs.length) continue; // heartbeat tick, nothing for us
|
|
261
277
|
const direct = msgs.filter(m => m.to === SESSION);
|
|
262
278
|
const mentions = msgs.filter(m => m.to === "all" && (m.text.includes(`@${AGENT}`) || m.text.toLowerCase().includes(`${AGENT}:`)));
|
package/bin/crew.sh
CHANGED
|
@@ -554,7 +554,8 @@ spawn_cmux() { # $@ = specs
|
|
|
554
554
|
echo " → $AGENT seat in cmux workspace ($PROJ)"
|
|
555
555
|
i=$(( i + 1 ))
|
|
556
556
|
done
|
|
557
|
-
|
|
557
|
+
# (no workspace-level "crew up" pill — the per-seat pills the runners push carry all the signal;
|
|
558
|
+
# a fifth static pill just forced the sidebar into "Show more".)
|
|
558
559
|
echo "— crew grouped in cmux: ONE workspace tab for $PROJ, seats tiled + sidebar status. Teardown (this project only): trantor down —"
|
|
559
560
|
}
|
|
560
561
|
|
package/bin/duty.mjs
CHANGED
|
@@ -66,7 +66,9 @@ async function ensureFleetIdentity(hub) {
|
|
|
66
66
|
function alivePid() {
|
|
67
67
|
try {
|
|
68
68
|
const pid = Number(readFileSync(PIDF, "utf8"));
|
|
69
|
-
|
|
69
|
+
// process.kill(pid, 0) returns TRUE on success (it throws when the pid is gone) — the original
|
|
70
|
+
// `=== undefined` comparison made alivePid always 0, so `duty status` reported NOT running forever.
|
|
71
|
+
if (pid) { process.kill(pid, 0); return pid; }
|
|
70
72
|
} catch {}
|
|
71
73
|
return 0;
|
|
72
74
|
}
|
package/hooks/heartbeat.mjs
CHANGED
|
@@ -147,6 +147,22 @@ async function main(stdinRaw) {
|
|
|
147
147
|
}
|
|
148
148
|
} catch {}
|
|
149
149
|
|
|
150
|
+
// Overseer narration, same ambient pattern (10-min machine-wide stamp): the narrate worker was
|
|
151
|
+
// built "to run ambiently from the heartbeat" but was never actually wired in — warns sat
|
|
152
|
+
// mechanical forever unless someone ran it by hand. The worker exits in one cheap signed GET per
|
|
153
|
+
// hub when nothing is unnarrated; Scrooge is only invoked when there IS a warn to explain.
|
|
154
|
+
try {
|
|
155
|
+
const narStamp = join(homedir(), ".agent-bus", "narrate.stamp");
|
|
156
|
+
const last = existsSync(narStamp) ? Number(readFileSync(narStamp, "utf8")) || 0 : 0;
|
|
157
|
+
if (Date.now() - last > 10 * 60 * 1000) {
|
|
158
|
+
writeFileSync(narStamp, String(Date.now()));
|
|
159
|
+
const worker = spawn(process.execPath, [join(HERE, "..", "bin", "overseer-narrate.mjs"), "--quiet"], {
|
|
160
|
+
detached: true, stdio: "ignore",
|
|
161
|
+
});
|
|
162
|
+
worker.unref();
|
|
163
|
+
}
|
|
164
|
+
} catch {}
|
|
165
|
+
|
|
150
166
|
// Same cadence as the presence ping: check context pressure and hand off early
|
|
151
167
|
// if we've crossed the warn threshold of a known window.
|
|
152
168
|
await maybeEarlyWarn(stdinRaw, session);
|
package/hub.mjs
CHANGED
|
@@ -214,6 +214,10 @@ import("./lib/overseer.mjs").then(m => { _overseer = m; }).catch(() => {});
|
|
|
214
214
|
const OVERSEER_TICK_MS = Number(process.env.RELAY_OVERSEER_TICK_MS || 30 * 1000);
|
|
215
215
|
const OVERSEER_DEDUP_MS = Number(process.env.RELAY_OVERSEER_DEDUP_MS || 10 * 60 * 1000);
|
|
216
216
|
const overseerWarned = new Map(); // dedup key -> ts
|
|
217
|
+
// Heartbeat for the WATCHER itself: /overseer/status must distinguish "fleet is clear" from "the
|
|
218
|
+
// overseer stopped ticking" — a monitor that cannot prove it is alive reads as clear when dead.
|
|
219
|
+
let overseerLastTick = 0;
|
|
220
|
+
let overseerLastCollisions = [];
|
|
217
221
|
function overseerPolicy() {
|
|
218
222
|
const p = state.orgPolicy && typeof state.orgPolicy === "object" ? state.orgPolicy : {};
|
|
219
223
|
return {
|
|
@@ -236,6 +240,7 @@ function overseerTick() {
|
|
|
236
240
|
if (!_overseer?.detectCollisions) return;
|
|
237
241
|
let collisions = [];
|
|
238
242
|
try { collisions = _overseer.detectCollisions(overseerInputs()) || []; } catch { return; }
|
|
243
|
+
overseerLastTick = now(); overseerLastCollisions = collisions;
|
|
239
244
|
const cut = now() - OVERSEER_DEDUP_MS;
|
|
240
245
|
for (const [k, ts] of overseerWarned) if (ts < cut) overseerWarned.delete(k);
|
|
241
246
|
const pol = overseerPolicy();
|
|
@@ -495,7 +500,7 @@ function cmpSemver(a, b) {
|
|
|
495
500
|
const AUTH_HEADERS = ["x-trantor-pubkey", "x-trantor-sig", "x-trantor-ts", "x-trantor-nonce"];
|
|
496
501
|
const PUBLIC_ENDPOINTS = new Set(["/", "/ui", "/health", "/enroll"]);
|
|
497
502
|
const OWNER_ENDPOINTS = new Set(["/project/delete", "/sweep", "/reconcile", "/invite", "/import", "/policy"]);
|
|
498
|
-
const READ_ENDPOINTS = new Set(["/peers", "/tasks", "/events", "/inbox", "/peer", "/card", "/stream", "/history", "/projects", "/catchup", "/phases", "/recent", "/handoffs", "/verify-gates", "/claims", "/overseer/context"]);
|
|
503
|
+
const READ_ENDPOINTS = new Set(["/peers", "/tasks", "/events", "/inbox", "/peer", "/card", "/stream", "/history", "/projects", "/catchup", "/phases", "/recent", "/handoffs", "/verify-gates", "/claims", "/overseer/context", "/overseer/status"]);
|
|
499
504
|
const roleRank = { read: 1, write: 2, owner: 3 };
|
|
500
505
|
const hasAuthHeaders = (req) => AUTH_HEADERS.some(h => !!req.headers[h]);
|
|
501
506
|
const authPath = (u) => `${u.pathname}${u.search || ""}`;
|
|
@@ -1013,6 +1018,32 @@ const server = http.createServer(async (req, res) => {
|
|
|
1013
1018
|
}
|
|
1014
1019
|
// What a session arriving on <project> needs to know: its autonomy level, who else is live,
|
|
1015
1020
|
// which files are in flight, which projects are declared codependent, current collisions.
|
|
1021
|
+
if (req.method === "GET" && P === "/overseer/status") {
|
|
1022
|
+
// The Overseer view's backbone: is the watcher ALIVE, and what is it watching right now.
|
|
1023
|
+
// `warnings` is the LIVE detection result from the last tick (pre-dedup), not the event log —
|
|
1024
|
+
// the log answers "what did it do", this answers "what does it see".
|
|
1025
|
+
const pol = overseerPolicy();
|
|
1026
|
+
const cutoff = now() - ONLINE_MS;
|
|
1027
|
+
const livePeers = Object.entries(state.peers).filter(([, v]) => v.lastSeen > cutoff);
|
|
1028
|
+
pruneClaims();
|
|
1029
|
+
return json(res, 200, {
|
|
1030
|
+
engine: !!_overseer?.detectCollisions,
|
|
1031
|
+
lastTickTs: overseerLastTick,
|
|
1032
|
+
tickMs: OVERSEER_TICK_MS,
|
|
1033
|
+
dedupMs: OVERSEER_DEDUP_MS,
|
|
1034
|
+
dutySession: DUTY_SESSION || "",
|
|
1035
|
+
watching: {
|
|
1036
|
+
sessions: livePeers.length,
|
|
1037
|
+
projects: new Set(livePeers.map(([, v]) => v.project).filter(Boolean)).size,
|
|
1038
|
+
claims: fileClaims.size,
|
|
1039
|
+
links: pol.links.length,
|
|
1040
|
+
},
|
|
1041
|
+
autonomy: pol.autonomy,
|
|
1042
|
+
links: pol.links,
|
|
1043
|
+
warnings: overseerLastCollisions,
|
|
1044
|
+
warnedRecent: overseerWarned.size,
|
|
1045
|
+
});
|
|
1046
|
+
}
|
|
1016
1047
|
if (req.method === "GET" && P === "/overseer/context") {
|
|
1017
1048
|
const proj = canon(String(q.project || "").slice(0, 80));
|
|
1018
1049
|
if (!proj) return json(res, 400, { error: "project required" });
|