trantor 0.18.37 → 0.18.39
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/crew-runner.mjs +27 -6
- package/bin/crew.sh +20 -3
- package/bin/turn-watchdog.mjs +72 -16
- package/hooks/sessionstart.mjs +32 -2
- package/package.json +2 -2
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "trantor",
|
|
3
|
-
"version": "0.18.
|
|
3
|
+
"version": "0.18.39",
|
|
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/crew-runner.mjs
CHANGED
|
@@ -539,6 +539,15 @@ let inFollowUp = false;
|
|
|
539
539
|
let sessionCard = 0;
|
|
540
540
|
|
|
541
541
|
let sid = "";
|
|
542
|
+
// #6206: the watchdog is DETACHED, so a runner that dies without ending it leaves an orphan
|
|
543
|
+
// sleeping toward a false alarm against whatever runner comes next (22 found on 2026-09-03).
|
|
544
|
+
// Every exit path therefore kills it, and the stamp carries this runner's instance id so any
|
|
545
|
+
// survivor that outlives the kill still refuses to speak for a runner it never belonged to.
|
|
546
|
+
const RUNNER_ID = `${process.pid}.${Date.now()}`;
|
|
547
|
+
let WD_CHILD = null;
|
|
548
|
+
function killWatchdog() { if (WD_CHILD) { try { WD_CHILD.kill("SIGTERM"); } catch {} WD_CHILD = null; } }
|
|
549
|
+
process.on("exit", killWatchdog);
|
|
550
|
+
|
|
542
551
|
async function runTurn(prompt, isFirst, trigger = "kickoff") {
|
|
543
552
|
TURN++; banner(trigger);
|
|
544
553
|
const t0 = Date.now();
|
|
@@ -593,17 +602,25 @@ async function runTurn(prompt, isFirst, trigger = "kickoff") {
|
|
|
593
602
|
const inner = cli.sid ? `${cmd} | tee /dev/stderr` : `${cmd} | ${SCRUB} --tee ${ERRF}`;
|
|
594
603
|
// #5684: runTurn is spawnSync, so the runner cannot watch its own turn — a DETACHED watchdog
|
|
595
604
|
// does. Armed by a stamp file, disarmed when the turn ends (stamp removed below); a turn past
|
|
596
|
-
// the window with no
|
|
597
|
-
|
|
605
|
+
// the window with no activity (transcript, worktree, or stderr — #6206: stdout silence alone
|
|
606
|
+
// is never a stall) earns ONE direct stall report to the foreman, never a kill.
|
|
607
|
+
// #6206: the window's floor is 10 minutes and is never derived from TRANTOR_TURN_MAX_MS —
|
|
608
|
+
// a 1-minute alarm is a false alarm by construction. The env override exists for drills.
|
|
609
|
+
const WD_MS = Number(process.env.TRANTOR_TURN_WATCHDOG_MS) || 10 * 60 * 1000;
|
|
598
610
|
const STAMPF = join(homedir(), ".agent-bus", `turnstamp-${AGENT}-${PROJ}.json`);
|
|
611
|
+
// #6206: where the CLI appends its session transcript (claude's project dir; other CLIs may
|
|
612
|
+
// not have one — the watchdog treats a missing dir as a quiet channel). Also exported to the
|
|
613
|
+
// CLI's env so a drill's fake CLI can write transcript lines the watchdog will see.
|
|
614
|
+
const TRANSCRIPT_DIR = join(homedir(), ".claude", "projects", TURN_DIR.replace(/[^a-zA-Z0-9]/g, "-"));
|
|
599
615
|
// Written by the shell's own time box (below) and read back here — the only honest signal that
|
|
600
616
|
// the turn was CUT rather than that the CLI failed on its own. Cleared before every turn.
|
|
601
617
|
const CUTF = join(homedir(), ".agent-bus", `turncut-${AGENT}-${PROJ}`);
|
|
602
618
|
try { unlinkSync(CUTF); } catch {}
|
|
603
619
|
try {
|
|
604
|
-
writeFileSync(STAMPF, JSON.stringify({ turn: TURN, startedAt: Date.now() }));
|
|
605
|
-
const wd = spawn(process.execPath, [join(import.meta.dirname, "turn-watchdog.mjs"), STAMPF, ERRF, String(WD_MS), SESSION, PROJ, HUB],
|
|
620
|
+
writeFileSync(STAMPF, JSON.stringify({ turn: TURN, startedAt: Date.now(), runner: RUNNER_ID }));
|
|
621
|
+
const wd = spawn(process.execPath, [join(import.meta.dirname, "turn-watchdog.mjs"), STAMPF, ERRF, String(WD_MS), SESSION, PROJ, HUB, TRANSCRIPT_DIR, TURN_DIR],
|
|
606
622
|
{ detached: true, stdio: "ignore" });
|
|
623
|
+
WD_CHILD = wd;
|
|
607
624
|
wd.unref();
|
|
608
625
|
} catch {}
|
|
609
626
|
// Preserve the CLI's exit before waiting for the stderr process substitution. Without the
|
|
@@ -663,7 +680,10 @@ exit $turn_exit`;
|
|
|
663
680
|
// Observed on the duty seat: handoff records at 17:24 and 18:59 on 2026-08-24, and two stray
|
|
664
681
|
// `claude` processes in ~/.agent-bus/trantor-duty started at 17:24:57 and 18:59:50, still
|
|
665
682
|
// sitting there days later. To the operator that reads as "why are there two duty agents".
|
|
666
|
-
TRANTOR_NO_HANDOFF_SPAWN: "1", TRANTOR_NO_BATON_SPAWN: "1"
|
|
683
|
+
TRANTOR_NO_HANDOFF_SPAWN: "1", TRANTOR_NO_BATON_SPAWN: "1",
|
|
684
|
+
// #6206: the seat's transcript dir — a real CLI ignores it, a drill's fake CLI writes
|
|
685
|
+
// its transcript lines there so the watchdog sees the liveness a real claude shows.
|
|
686
|
+
TRANTOR_TRANSCRIPT_DIR: TRANSCRIPT_DIR },
|
|
667
687
|
maxBuffer: 16 * 1024 * 1024,
|
|
668
688
|
};
|
|
669
689
|
// A BACKSTOP only, deliberately later than the shell's own box: if bash itself wedges, node
|
|
@@ -671,7 +691,8 @@ exit $turn_exit`;
|
|
|
671
691
|
// is the point: the shell kills while the tree is still walkable, node cannot.
|
|
672
692
|
if (TURN_MAX_MS) { spawnOpts.timeout = TURN_MAX_MS + 30000; spawnOpts.killSignal = "SIGKILL"; }
|
|
673
693
|
const r = spawnSync("/bin/bash", ["-c", shell], spawnOpts);
|
|
674
|
-
|
|
694
|
+
killWatchdog(); // #6206: turn over — the watchdog dies NOW, it does not sleep on
|
|
695
|
+
try { unlinkSync(STAMPF); } catch {} // disarm any survivor: the stamp is gone
|
|
675
696
|
// The shell's box leaves the marker; the backstop leaves an ETIMEDOUT. Either way the turn was
|
|
676
697
|
// cut, not merely failed.
|
|
677
698
|
const boxed = existsSync(CUTF);
|
package/bin/crew.sh
CHANGED
|
@@ -200,12 +200,22 @@ _herdr_ws_create() { # $1=cwd $2=label → "workspace_id<TAB>root_pane_id"
|
|
|
200
200
|
let d="";process.stdin.on("data",c=>d+=c).on("end",()=>{try{const r=(JSON.parse(d.slice(d.search(/[\[{]/))).result)||{};
|
|
201
201
|
process.stdout.write((((r.workspace||{}).workspace_id)||"")+"\t"+(((r.root_pane||{}).pane_id)||""))}catch(e){}})'
|
|
202
202
|
}
|
|
203
|
-
_herdr_split() { # $1=pane id ("" = UI-focused pane) $2=right|down → new pane id
|
|
204
|
-
local a=(pane split); [ -n "$1" ] && a+=("$1"); a+=(--direction "$2" --no-focus)
|
|
203
|
+
_herdr_split() { # $1=pane id ("" = UI-focused pane) $2=right|down $3=cwd (optional) → new pane id
|
|
204
|
+
local a=(pane split); [ -n "$1" ] && a+=("$1"); a+=(--direction "$2" --no-focus); [ -n "$3" ] && a+=(--cwd "$3")
|
|
205
205
|
_herdr "${a[@]}" 2>/dev/null | node -e '
|
|
206
206
|
let d="";process.stdin.on("data",c=>d+=c).on("end",()=>{try{const r=(JSON.parse(d.slice(d.search(/[\[{]/))).result)||{};
|
|
207
207
|
process.stdout.write(((r.pane||{}).pane_id)||"")}catch(e){}})'
|
|
208
208
|
}
|
|
209
|
+
# Any live pane INSIDE a workspace (the one whose cwd is $2 first). The orchestrator pane must be
|
|
210
|
+
# hosted off a pane of ITS OWN workspace: splitting the UI-focused pane put crebral-com's
|
|
211
|
+
# orchestrator in the trantor window, in a trantor shell, twice on 2026-09-03 (the operator was
|
|
212
|
+
# looking at trantor when Wake ran) — twin bus identity, wrong checkout, crossed wires.
|
|
213
|
+
_herdr_ws_pane() { # $1=workspace id $2=preferred cwd → pane id ("" if none)
|
|
214
|
+
_herdr pane list 2>/dev/null | WS="$1" CWD="$2" node -e '
|
|
215
|
+
let d="";process.stdin.on("data",c=>d+=c).on("end",()=>{try{const o=JSON.parse(d.slice(d.search(/[\[{]/)));
|
|
216
|
+
const a=(Array.isArray(o)?o:(o.panes||((o.result||{}).panes)||[])).filter(p=>(p.workspace_id||p.workspace||"")===process.env.WS);
|
|
217
|
+
const m=a.find(p=>(p.cwd||"")===process.env.CWD)||a[0];process.stdout.write(m?(m.pane_id||m.id||""):"")}catch(e){}})'
|
|
218
|
+
}
|
|
209
219
|
_herdr_ws_live() { # workspace list → "ids<TABnewline>names" (names \x01-wrapped+joined, like cmux)
|
|
210
220
|
_herdr workspace list 2>/dev/null | node -e '
|
|
211
221
|
let d="";process.stdin.on("data",c=>d+=c).on("end",()=>{try{const o=JSON.parse(d.slice(d.search(/[\[{]/)));
|
|
@@ -731,12 +741,19 @@ open_orchestrator() {
|
|
|
731
741
|
# Host the pane: a JUST-created workspace's root pane IS the orchestrator pane (claude rides it);
|
|
732
742
|
# an existing workspace gets a split — the crew's seats tile off it on the next `up`, never
|
|
733
743
|
# replacing it (spawn_herdr's REUSE mode splits seats off their own old pane / the previous one).
|
|
744
|
+
# An EXISTING workspace hosts the split off one of ITS panes, in the project's checkout — never off
|
|
745
|
+
# the UI-focused pane, which is whatever the operator happens to be looking at (2026-09-03).
|
|
734
746
|
if [ "$DRY" = "1" ]; then
|
|
735
747
|
orch="%DRYORCH"
|
|
748
|
+
[ "$fresh" = "1" ] || echo "[dry] herdr: pane split %DRYHOST($wsid) --direction right --cwd $DIR" >&2
|
|
736
749
|
echo "[dry] herdr: ${fresh:+root pane + }pane rename $orch 'orchestrator · $PROJ' + run '$(_orch_cmd "$DIR" "$sid")'" >&2
|
|
737
750
|
else
|
|
738
751
|
if [ "$fresh" = "1" ]; then orch="${pair##*$'\t'}"
|
|
739
|
-
else
|
|
752
|
+
else
|
|
753
|
+
local host; host="$(_herdr_ws_pane "$wsid" "$DIR")"
|
|
754
|
+
[ -n "$host" ] || { echo "trantor open: workspace $wsid has no live pane to host the orchestrator" >&2; exit 1; }
|
|
755
|
+
orch="$(_herdr_split "$host" right "$DIR")"
|
|
756
|
+
fi
|
|
740
757
|
[ -n "$orch" ] || { echo "trantor open: could not create the orchestrator pane" >&2; exit 1; }
|
|
741
758
|
_herdr pane rename "$orch" "orchestrator · $PROJ" >/dev/null 2>&1
|
|
742
759
|
_herdr pane run "$orch" "$(_orch_cmd "$DIR" "$sid")" >/dev/null 2>&1
|
package/bin/turn-watchdog.mjs
CHANGED
|
@@ -1,36 +1,92 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// Turn watchdog (#5684). runTurn is spawnSync — the runner cannot watch its own
|
|
3
|
-
// DETACHED helper does: armed at turn start, disarmed by turn end
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
// killed — reporting is the whole job. The operator's 2026-08-31 complaint is the incident:
|
|
7
|
-
// seats sat visibly dead in their panes while every signal channel stayed quiet.
|
|
2
|
+
// Turn watchdog (#5684, reworked #6206). runTurn is spawnSync — the runner cannot watch its own
|
|
3
|
+
// turn — so this DETACHED helper does: armed at turn start, disarmed by turn end. A turn past
|
|
4
|
+
// the window with NO new activity earns ONE direct stall report to the foreman (episode, never a
|
|
5
|
+
// timer storm), and the turn is never killed — reporting is the whole job.
|
|
8
6
|
//
|
|
9
|
-
//
|
|
10
|
-
|
|
7
|
+
// #6206: stdout silence is NOT a stall — `claude -p` prints nothing until the turn ends by
|
|
8
|
+
// design, so a seat editing five files was reported STALLED while its transcript advanced.
|
|
9
|
+
// Liveness is new activity in the seat's transcript (the CLI's session file), its worktree, or
|
|
10
|
+
// stderr growth; silence on ALL of them for a whole window is the only thing reported. And a
|
|
11
|
+
// watchdog never speaks for a runner it does not belong to: the stamp carries the runner's
|
|
12
|
+
// instance id, so a survivor of a replaced runner exits on mismatch or runner death instead of
|
|
13
|
+
// re-matching the NEW runner's turn number (the 09:47 false alarm was exactly that orphan).
|
|
14
|
+
//
|
|
15
|
+
// node bin/turn-watchdog.mjs <stampFile> <errFile> <windowMs> <session> <project> <hubUrl> <transcriptDir> <workDir>
|
|
16
|
+
import { readFileSync, statSync, readdirSync } from "node:fs";
|
|
17
|
+
import { join } from "node:path";
|
|
11
18
|
import { hostId } from "../lib/project.mjs";
|
|
12
19
|
import { signedPost } from "../hooks/lib/api.mjs";
|
|
13
20
|
|
|
14
|
-
const [stampFile, errFile, windowMsRaw, session, project, hub] = process.argv.slice(2);
|
|
15
|
-
|
|
21
|
+
const [stampFile, errFile, windowMsRaw, session, project, hub, transcriptDir = "", workDir = ""] = process.argv.slice(2);
|
|
22
|
+
// SAFETY: the 10-minute floor lives in crew-runner.mjs (the default when TRANTOR_TURN_WATCHDOG_MS
|
|
23
|
+
// is unset); this fallback only covers a missing argument. Drills pass tiny windows on purpose.
|
|
24
|
+
const windowMs = Number(windowMsRaw) || 10 * 60 * 1000;
|
|
16
25
|
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
17
26
|
|
|
18
27
|
const readStamp = () => { try { return JSON.parse(readFileSync(stampFile, "utf8")); } catch { return null; } };
|
|
19
28
|
const errSize = () => { try { return statSync(errFile).size; } catch { return 0; } };
|
|
20
29
|
|
|
30
|
+
// Newest mtime under a directory, .git/node_modules skipped, entry-capped so a big tree cannot
|
|
31
|
+
// wedge a detached helper. A missing directory scores 0 (a codex seat has no claude transcript
|
|
32
|
+
// dir) — liveness needs only ONE channel to move, absence of some channels is fine.
|
|
33
|
+
const SCAN_CAP = 20000;
|
|
34
|
+
function newestMtime(dir) {
|
|
35
|
+
let best = 0, seen = 0;
|
|
36
|
+
const walk = (d) => {
|
|
37
|
+
if (seen > SCAN_CAP) return;
|
|
38
|
+
let rows = [];
|
|
39
|
+
try { rows = readdirSync(d, { withFileTypes: true }); } catch { return; }
|
|
40
|
+
for (const e of rows) {
|
|
41
|
+
if (++seen > SCAN_CAP || e.name === ".git" || e.name === "node_modules") continue;
|
|
42
|
+
const p = join(d, e.name);
|
|
43
|
+
if (e.isDirectory()) walk(p);
|
|
44
|
+
else { try { best = Math.max(best, statSync(p).mtimeMs); } catch {} }
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
walk(dir);
|
|
48
|
+
return best;
|
|
49
|
+
}
|
|
50
|
+
|
|
21
51
|
const armed = readStamp();
|
|
22
52
|
if (!armed) process.exit(0);
|
|
23
|
-
|
|
53
|
+
// The runner instance that armed us (pid + boot ts, so a recycled pid cannot impersonate it).
|
|
54
|
+
// A stamp without a runner id (only possible mid-upgrade) skips the liveness check; the id
|
|
55
|
+
// match in the loop still guards it.
|
|
56
|
+
const runnerPid = Number(String(armed.runner || "").split(".")[0]) || 0;
|
|
57
|
+
const runnerAlive = () => {
|
|
58
|
+
if (!runnerPid) return true;
|
|
59
|
+
try { process.kill(runnerPid, 0); return true; } catch { return false; }
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
// One observation of every liveness channel: stderr growth, transcript mtime, worktree mtime.
|
|
63
|
+
const ago = (t) => (t > 0 ? `${Math.max(1, Math.round((Date.now() - t) / 60000))}m ago` : "never");
|
|
64
|
+
const describeLast = (b) => {
|
|
65
|
+
const parts = [];
|
|
66
|
+
if (b.wk) parts.push(`worktree ${ago(b.wk)}`);
|
|
67
|
+
if (b.tr) parts.push(`transcript ${ago(b.tr)}`);
|
|
68
|
+
return parts.length ? parts.join(", ") : "nothing";
|
|
69
|
+
};
|
|
70
|
+
let baseErr = errSize();
|
|
71
|
+
const armedAt = armed.startedAt || Date.now();
|
|
72
|
+
const SLACK = 2000; // timestamp granularity + scheduler drift under load
|
|
24
73
|
|
|
25
74
|
for (;;) {
|
|
26
75
|
await sleep(windowMs);
|
|
27
76
|
const s = readStamp();
|
|
28
|
-
if (!s || s.turn !== armed.turn) process.exit(0);
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
77
|
+
if (!s || s.turn !== armed.turn || (armed.runner && s.runner !== armed.runner)) process.exit(0); // turn ended, or a NEWER runner owns the stamp now
|
|
78
|
+
if (!runnerAlive()) process.exit(0); // our runner is gone — never speak for it
|
|
79
|
+
// Activity that counts: anything changed DURING the turn (after arm) and within the window —
|
|
80
|
+
// the checklist semantics verbatim: "no new activity for the window". Pre-turn files never
|
|
81
|
+
// count (they predate arm), and absolute freshness cannot drift the way a probe-to-probe
|
|
82
|
+
// delta does when the seat writes coarsely or the machine loads (the +1130ms false alarm).
|
|
83
|
+
const freshCut = Math.max(armedAt, Date.now() - windowMs - SLACK);
|
|
84
|
+
const tr = transcriptDir ? newestMtime(transcriptDir) : 0;
|
|
85
|
+
const wk = workDir ? newestMtime(workDir) : 0;
|
|
86
|
+
if (errSize() > baseErr + 200 || tr > freshCut || wk > freshCut) continue; // producing work: alive, re-arm
|
|
87
|
+
const mins = Math.round((Date.now() - armedAt) / 60000);
|
|
32
88
|
const orch = `${hostId()}:${project}`;
|
|
33
|
-
const text = `⏱ ${session} turn STALLED — running ${mins}m with no
|
|
89
|
+
const text = `⏱ ${session} turn STALLED — running ${mins}m with no activity (turn ${s.turn}; last seen: ${describeLast({ tr, wk })}, stderr ${baseErr > 0 ? `${baseErr}B` : "silent"}). Not killed; check its pane, or \`trantor swap\`.`;
|
|
34
90
|
// Direct = wake. The foreman first; if this seat IS the foreman's own runner, say it to all.
|
|
35
91
|
const to = orch === session ? "all" : orch;
|
|
36
92
|
try { await signedPost(`${hub}/send`, { from: session, to, text, project }, { session }); } catch {}
|
package/hooks/sessionstart.mjs
CHANGED
|
@@ -15,8 +15,9 @@ import { resolveProject, hostId, resolveHubInfo, knownProjects, nonSeatReason, h
|
|
|
15
15
|
import { formatSubagentManifest } from "../lib/subagent-manifest.mjs";
|
|
16
16
|
import { updateAvailable, maybeNotifyDesktop, readConfig } from "./lib/update-check.mjs";
|
|
17
17
|
import { maybeCheckBalances } from "./lib/balance-check.mjs";
|
|
18
|
-
import { getJSON, signedGet, signedPost } from "./lib/api.mjs";
|
|
18
|
+
import { getJSON, signedGet, signedPost, loadIdentity } from "./lib/api.mjs";
|
|
19
19
|
import { ledgerPaths, ensureStart, anchorCursor, writeCursor } from "./lib/inbox-ledger.mjs";
|
|
20
|
+
import { ensureEnrolled } from "../lib/enroll.mjs";
|
|
20
21
|
|
|
21
22
|
// Load the most recent UNCONSUMED handoff for this project (written by precompact.mjs
|
|
22
23
|
// / the heartbeat early-warning). `claim` marks it consumed so exactly one session
|
|
@@ -221,6 +222,33 @@ try {
|
|
|
221
222
|
// instead of silently routing to the default.
|
|
222
223
|
const { url, via: hubVia } = resolveHubInfo(project);
|
|
223
224
|
|
|
225
|
+
// Self-enrol on an enforce hub BEFORE the first read/write, the way bin/crew-runner.mjs does for
|
|
226
|
+
// crew seats (#6270). A session started via `trantor open` (or any Claude session not spawned by
|
|
227
|
+
// the runner) never went through that path: hooks/lib/api.mjs's own ensureEnrolled POSTs /enroll
|
|
228
|
+
// with no invite token, which a non-loopback enforce hub refuses — and refuses SILENTLY, because
|
|
229
|
+
// that helper is fail-open by design. The identity then sits registered-looking (whoami works,
|
|
230
|
+
// it has a keypair) but unknown to the hub, and every read 401s for as long as nobody notices
|
|
231
|
+
// (crebral-com sat like this for 20 minutes). lib/enroll.mjs's ensureEnrolled is the real fix: it
|
|
232
|
+
// mints a one-shot invite with the OPERATOR's owner key and spends it as this identity, exactly
|
|
233
|
+
// like a crew seat enrols itself. Never silent on failure — the reason lands in both the user
|
|
234
|
+
// banner and the model's context, not just a stderr line nobody reads.
|
|
235
|
+
const orchIdentity = loadIdentity(session);
|
|
236
|
+
const enrolment = orchIdentity
|
|
237
|
+
? await ensureEnrolled(url, orchIdentity, project).catch((e) => ({ ok: false, reason: "error:" + String(e?.message || e).slice(0, 40) }))
|
|
238
|
+
: { ok: false, reason: "no-identity" };
|
|
239
|
+
if (!enrolment.ok && enrolment.reason !== "hub-unreachable") {
|
|
240
|
+
const O = "\x1b[1;38;5;208m", R = "\x1b[0m";
|
|
241
|
+
const line = `🟠 ${O}Not enrolled on ${url}${R} (${enrolment.reason}) — reads/writes may 401. `
|
|
242
|
+
+ `Fix: set an owner key (RELAY_OWNER_IDENTITY or ~/.agent-bus/config.json ownerIdentity) that can mint invites on this hub.`;
|
|
243
|
+
userBanner = userBanner ? `${userBanner}\n${line}` : line;
|
|
244
|
+
additionalContext += `<trantor-not-enrolled hub="${sanitize(url)}" reason="${sanitize(enrolment.reason)}">\n`;
|
|
245
|
+
additionalContext += `⚠️ **This session's identity (\`${sanitize(session)}\`) is NOT enrolled on \`${sanitize(url)}\`** (reason: ${sanitize(enrolment.reason)}). `;
|
|
246
|
+
additionalContext += `On an enforce hub this means board reads/writes will 401 until enrolment succeeds. This is not "the hub is down" — the hub is answering, it just does not recognize this identity yet. `;
|
|
247
|
+
additionalContext += `Tell the user: the operator's owner key needs to be able to mint invites on this hub (RELAY_OWNER_IDENTITY env or \`ownerIdentity\` in ~/.agent-bus/config.json), then restart this session.\n`;
|
|
248
|
+
additionalContext += `</trantor-not-enrolled>\n`;
|
|
249
|
+
process.stderr.write(`[trantor] WARNING: ${session} not enrolled on ${url} (${enrolment.reason})\n`);
|
|
250
|
+
}
|
|
251
|
+
|
|
224
252
|
// register self + post an initial presence status (no LLM turn — instant for others to read).
|
|
225
253
|
// kind "orch" when `trantor open` badged THIS session as the project's orchestrator pane
|
|
226
254
|
// (#6075): the peer row's kind is the hub's own record of what a session is — the overseer's
|
|
@@ -229,7 +257,9 @@ try {
|
|
|
229
257
|
// carries no name, so it stamps nothing. Absent kind is preserved by /register, so the MCP's
|
|
230
258
|
// kindless heartbeats never erase this.
|
|
231
259
|
const orchBadge = process.env.TRANTOR_ORCH || "";
|
|
232
|
-
|
|
260
|
+
const registerBody = { session, project, status: `active in ${project}` };
|
|
261
|
+
if (orchBadge === project) registerBody.kind = "orch";
|
|
262
|
+
await jpost(`${url}/register`, registerBody, session).catch(() => {});
|
|
233
263
|
|
|
234
264
|
// fetch roster of OTHER online sessions
|
|
235
265
|
let peers = [], hubAnswered = false;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "trantor",
|
|
3
|
-
"version": "0.18.
|
|
3
|
+
"version": "0.18.39",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"bin": {
|
|
6
6
|
"trantor": "bin/cli.mjs"
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
"zod": "^4.4.3"
|
|
12
12
|
},
|
|
13
13
|
"scripts": {
|
|
14
|
-
"test": "node bin/slop-gate.mjs --surface desktop/src && node test.mjs && node test-scenarios.mjs && node test-failure.mjs && node test-redelivery.mjs && node test-crew-completion.mjs && node test-contracts.mjs && node test-autonomy.mjs && node test-integrate.mjs && node test-handoff.mjs && node test-handoff-summarizer.mjs && node test-baton-turn-boundary.mjs && node test-agents.mjs && node test-update.mjs && node test-handoff-guard.mjs && node test-baton-latest.mjs && node test-balances.mjs && node test-usage-live.mjs && node test-subagent-cost.mjs && node test-inbox.mjs && node test-cursor-rewind.mjs && node test-inflight.mjs && node test-notify.mjs && node test-focus.mjs && node test-cardlog.mjs && node test-checklist.mjs && node test-relay-note.mjs && node test-reaper.mjs && node test-events.mjs && node test-proposals.mjs && node test-scrub.mjs && node test-store-delta.mjs && node test-persist-safety.mjs && node test-message-re.mjs && node test-claims.mjs && node test-adopt.mjs && node test-summarize.mjs && node test-identity-core.mjs && node test-identity.mjs && node test-identity-instances.mjs && node test-duty.mjs && node test-duty-dark.mjs && node test-duty-seat.mjs && node test-discovery.mjs && node test-doctor.mjs && node test-crew-env.mjs && node test-desktop-transport.mjs && node test-crew-worktree.mjs && node test-crew-redact.mjs && node test-crew-classify.mjs && bash test-crew-herdr.sh && npm --prefix desktop run test --silent && node test-splitbrain.mjs && node test-overseer.mjs && node test-overseer-lib.mjs && node test-overseer-warn.mjs && node test-provider-keys.mjs && node test-provider-cli.mjs && node test-crew-model-defaults.mjs && node test-inbox-delivery.mjs && node test-identity-drift.mjs && node test-inbox-staleness.mjs && node test-seats.mjs && node test-seat-identity.mjs && node test-hub-routing.mjs && node test-hook-routing.mjs && node test-relay-wait.mjs && node test-dsh-seat.mjs && node test-kimi-bridge.mjs && node test-kimi-events.mjs && python3 engine/test-routing.py && node test-new.mjs && node test-prd-review.mjs && node test-reconcile.mjs && node test-resources.mjs && node test-patrol.mjs && node test-bridge.mjs && bash test-crew.sh"
|
|
14
|
+
"test": "node bin/slop-gate.mjs --surface desktop/src && node test.mjs && node test-scenarios.mjs && node test-failure.mjs && node test-redelivery.mjs && node test-crew-completion.mjs && node test-contracts.mjs && node test-autonomy.mjs && node test-integrate.mjs && node test-handoff.mjs && node test-handoff-summarizer.mjs && node test-baton-turn-boundary.mjs && node test-agents.mjs && node test-update.mjs && node test-handoff-guard.mjs && node test-enroll.mjs && node test-baton-latest.mjs && node test-balances.mjs && node test-usage-live.mjs && node test-subagent-cost.mjs && node test-inbox.mjs && node test-cursor-rewind.mjs && node test-inflight.mjs && node test-notify.mjs && node test-focus.mjs && node test-cardlog.mjs && node test-checklist.mjs && node test-relay-note.mjs && node test-reaper.mjs && node test-events.mjs && node test-proposals.mjs && node test-scrub.mjs && node test-store-delta.mjs && node test-persist-safety.mjs && node test-message-re.mjs && node test-claims.mjs && node test-adopt.mjs && node test-summarize.mjs && node test-identity-core.mjs && node test-identity.mjs && node test-identity-instances.mjs && node test-duty.mjs && node test-duty-dark.mjs && node test-duty-seat.mjs && node test-discovery.mjs && node test-doctor.mjs && node test-crew-env.mjs && node test-desktop-transport.mjs && node test-crew-worktree.mjs && node test-crew-redact.mjs && node test-crew-classify.mjs && bash test-crew-herdr.sh && npm --prefix desktop run test --silent && node test-splitbrain.mjs && node test-overseer.mjs && node test-overseer-lib.mjs && node test-overseer-warn.mjs && node test-provider-keys.mjs && node test-provider-cli.mjs && node test-crew-model-defaults.mjs && node test-inbox-delivery.mjs && node test-identity-drift.mjs && node test-inbox-staleness.mjs && node test-seats.mjs && node test-seat-identity.mjs && node test-hub-routing.mjs && node test-hook-routing.mjs && node test-relay-wait.mjs && node test-dsh-seat.mjs && node test-kimi-bridge.mjs && node test-kimi-events.mjs && python3 engine/test-routing.py && node test-new.mjs && node test-prd-review.mjs && node test-reconcile.mjs && node test-resources.mjs && node test-patrol.mjs && node test-bridge.mjs && bash test-crew.sh"
|
|
15
15
|
},
|
|
16
16
|
"description": "The hub-world for AI agent crews — orchestrate Claude Code, Codex, GLM, Kimi, DeepSeek & any OpenRouter model as live crews with a plan-aware Advisor, a Kanban/flow command center, a testing gate, and an economics brain (Scrooge).",
|
|
17
17
|
"files": [
|