trantor 0.18.11 → 0.18.12
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/advise.mjs +8 -3
- package/bin/baton.mjs +7 -0
- package/bin/crew-runner.mjs +16 -1
- package/bin/models.mjs +6 -1
- package/hooks/lib/handoff.mjs +21 -0
- package/hub.mjs +32 -0
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "trantor",
|
|
3
|
-
"version": "0.18.
|
|
3
|
+
"version": "0.18.12",
|
|
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/advise.mjs
CHANGED
|
@@ -90,14 +90,19 @@ export function loadWorld() {
|
|
|
90
90
|
|
|
91
91
|
const tierOf = (profile, prov) => profile?.providers?.[prov]?.tier || "api";
|
|
92
92
|
|
|
93
|
+
const FLASH_TIER = /(flash|turbo|lite|mini|highspeed|small)/i;
|
|
94
|
+
|
|
93
95
|
// pick the cheapest Scrooge model that clears the difficulty floor for a task kind
|
|
94
96
|
export function scroogeModelFor(registry, caps, kind = "code", difficulty = "easy") {
|
|
95
97
|
const floor = { easy: 0, medium: 35, hard: 55 }[difficulty] ?? 0;
|
|
96
98
|
const cands = Object.entries(registry.models || {})
|
|
97
99
|
.filter(([, m]) => (m.good_for || []).includes(kind))
|
|
98
|
-
.filter(([id]) => (caps[id]?.coding ?? caps[id]?.intelligence ?? 40) >= floor)
|
|
99
|
-
|
|
100
|
-
|
|
100
|
+
.filter(([id]) => (caps[id]?.coding ?? caps[id]?.intelligence ?? 40) >= floor);
|
|
101
|
+
const hard = difficulty === "hard";
|
|
102
|
+
const strong = hard ? cands.filter(([id]) => !FLASH_TIER.test(id)) : cands;
|
|
103
|
+
const pool = strong.length > 0 ? strong : cands;
|
|
104
|
+
pool.sort((a, b) => (a[1].cost_in + a[1].cost_out) - (b[1].cost_in + b[1].cost_out));
|
|
105
|
+
return pool[0] ? { model: pool[0][0], cost_in: pool[0][1].cost_in, cost_out: pool[0][1].cost_out } : null;
|
|
101
106
|
}
|
|
102
107
|
|
|
103
108
|
// crude per-package token forecast (input+output through the executor)
|
package/bin/baton.mjs
CHANGED
|
@@ -35,6 +35,13 @@ const transcript = findTranscript();
|
|
|
35
35
|
const sessionId = transcript ? basename(transcript, ".jsonl") : "";
|
|
36
36
|
const { file } = writeHandoff({ projectDir: cwd, sessionId, transcript, trigger: "manual-cli", force: true }); // manual = intentional, bypass the storm guard
|
|
37
37
|
console.log(`📋 handoff saved for ${project}: ${file}`);
|
|
38
|
+
// --write-only: the in-app flow (#5509). The app ends the pane's session itself and reopens it
|
|
39
|
+
// through `trantor open`, which claims this handoff — a Terminal window here would be exactly the
|
|
40
|
+
// wrong surface, so the flag writes, announces, and stops.
|
|
41
|
+
if (process.argv.includes("--write-only")) {
|
|
42
|
+
console.log(`🔄 write-only: no window spawned — the pane takeover (trantor open) claims it next.`);
|
|
43
|
+
process.exit(0);
|
|
44
|
+
}
|
|
38
45
|
const { spawned, armed, windowId } = spawnBaton({ projectDir: cwd, handoffFile: file });
|
|
39
46
|
console.log(spawned
|
|
40
47
|
? `🔄 baton: a fresh session is opening (it'll recap the handoff)${armed ? ` — this window (${windowId}) closes once it takes over` : " — couldn't detect this window; close it yourself once the new one is up"}`
|
package/bin/crew-runner.mjs
CHANGED
|
@@ -44,7 +44,22 @@ function ensureSeatWorktree(sourceDir) {
|
|
|
44
44
|
const branch = `seat/${AGENT}`;
|
|
45
45
|
if (existsSync(seatDir)) {
|
|
46
46
|
const ok = gitOut(["-C", seatDir, "rev-parse", "--is-inside-work-tree"], seatDir) === "true";
|
|
47
|
-
if (ok)
|
|
47
|
+
if (ok) {
|
|
48
|
+
// #5403: a worktree created once builds against THAT day's main forever — every wave since
|
|
49
|
+
// has needed a hand fast-forward. Refresh only when it is CLEAN: a dirty tree is a seat's
|
|
50
|
+
// unintegrated work and a diverged branch is a decision, and refreshing must never eat
|
|
51
|
+
// either. Failure to refresh is loud but non-fatal: stale beats broken.
|
|
52
|
+
const dirty = gitOut(["-C", seatDir, "status", "--porcelain"], seatDir);
|
|
53
|
+
if (dirty === "") {
|
|
54
|
+
const head = gitOut(["-C", root, "rev-parse", "HEAD"], root);
|
|
55
|
+
const ff = head && spawnSync("git", ["-C", seatDir, "merge", "--ff-only", head], { stdio: "ignore", timeout: 15000 });
|
|
56
|
+
if (ff && ff.status === 0) console.log(`\x1b[2m[runner]\x1b[0m ${branch} worktree refreshed to ${head.slice(0, 7)}`);
|
|
57
|
+
else console.log(`\x1b[33m[runner]\x1b[0m ${branch} worktree diverged from main HEAD — left as-is (integrate or reset it)`);
|
|
58
|
+
} else {
|
|
59
|
+
console.log(`\x1b[33m[runner]\x1b[0m ${branch} worktree has uncommitted work — not refreshed`);
|
|
60
|
+
}
|
|
61
|
+
return seatDir;
|
|
62
|
+
}
|
|
48
63
|
console.log(`\x1b[33m[runner]\x1b[0m worktree path exists but is not a git worktree: ${seatDir} — using ${sourceDir}`);
|
|
49
64
|
return sourceDir;
|
|
50
65
|
}
|
package/bin/models.mjs
CHANGED
|
@@ -25,9 +25,14 @@ const liveModels = (providerOc) => {
|
|
|
25
25
|
catch { return []; }
|
|
26
26
|
};
|
|
27
27
|
|
|
28
|
+
const FLASH_TIER = /(flash|turbo|lite|mini|highspeed|small)/i;
|
|
29
|
+
|
|
28
30
|
function routePick(candList, diff) {
|
|
29
31
|
try {
|
|
30
|
-
const
|
|
32
|
+
const hard = diff === "hard";
|
|
33
|
+
const strong = hard ? candList.filter(id => !FLASH_TIER.test(id)) : candList;
|
|
34
|
+
const pool = strong.length > 0 ? strong : candList;
|
|
35
|
+
const out = execSync(`python3 ${SCROOGE} route --candidates ${JSON.stringify(pool.join(" "))} -t code -d ${diff} --json 2>/dev/null`, { encoding: "utf8" });
|
|
31
36
|
return JSON.parse(out).qualified || "?";
|
|
32
37
|
} catch { return "?"; }
|
|
33
38
|
}
|
package/hooks/lib/handoff.mjs
CHANGED
|
@@ -423,6 +423,10 @@ export function maybeSpawn(projectDir, conf = readConfig()) {
|
|
|
423
423
|
if (process.platform !== "darwin") return false;
|
|
424
424
|
if (process.env.TRANTOR_NO_HANDOFF_SPAWN === "1") return false;
|
|
425
425
|
if (conf.autoHandoffPrompt === false) return false;
|
|
426
|
+
if (hasOrchPane(basename(projectDir))) {
|
|
427
|
+
process.stderr.write(`[trantor] orch pane hosts ${basename(projectDir)} — no Terminal window; the pane claims the handoff on its next open\n`);
|
|
428
|
+
return false;
|
|
429
|
+
}
|
|
426
430
|
const script = join(HERE, "..", "..", "bin", "handoff-prompt.sh");
|
|
427
431
|
if (!existsSync(script)) { process.stderr.write(`[trantor] handoff-prompt.sh missing\n`); return false; }
|
|
428
432
|
const timeout = String(conf.handoffPromptTimeout || 25);
|
|
@@ -446,9 +450,26 @@ export function spawnSuppressed() {
|
|
|
446
450
|
return process.env.TRANTOR_NO_HANDOFF_SPAWN === "1" || process.env.TRANTOR_NO_BATON_SPAWN === "1";
|
|
447
451
|
}
|
|
448
452
|
|
|
453
|
+
// Does this project have a hosted orchestrator pane? When it does, the PANE is the successor
|
|
454
|
+
// surface: `trantor open` claims the handoff there, and spawning a Terminal window would put the
|
|
455
|
+
// fresh session on exactly the surface the operator is trying to leave (#5509 W1). The tracked
|
|
456
|
+
// row is the signal — rows are recorded by open and dropped by teardown/prune, and a stale row
|
|
457
|
+
// costs only a skipped window, never a lost handoff (the handoff waits, held for the pane).
|
|
458
|
+
export function hasOrchPane(projectName) {
|
|
459
|
+
try {
|
|
460
|
+
const state = join(process.env.AGENT_BUS_DIR || process.env.RELAY_DATA_DIR || join(homedir(), ".agent-bus"), "crew-windows.txt");
|
|
461
|
+
if (!existsSync(state)) return false;
|
|
462
|
+
return readFileSync(state, "utf8").split("\n").some(l => {
|
|
463
|
+
const f = l.split("\t");
|
|
464
|
+
return f[0] === projectName && f[1] === "orch";
|
|
465
|
+
});
|
|
466
|
+
} catch { return false; }
|
|
467
|
+
}
|
|
468
|
+
|
|
449
469
|
export function spawnFresh(projectDir) {
|
|
450
470
|
try {
|
|
451
471
|
if (process.platform !== "darwin" || spawnSuppressed()) return false;
|
|
472
|
+
if (hasOrchPane(basename(projectDir))) return false; // the pane is the successor surface (#5509)
|
|
452
473
|
const script = join(HERE, "..", "..", "bin", "open-session.sh");
|
|
453
474
|
if (!existsSync(script)) return false;
|
|
454
475
|
const child = spawn("/bin/bash", [script, projectDir, RECAP_CMD], { detached: true, stdio: "ignore" });
|
package/hub.mjs
CHANGED
|
@@ -752,6 +752,31 @@ function contractsFor(session, { project = "", windowMs = CONTRACT_WINDOW_MS, ov
|
|
|
752
752
|
if (c.ageMs < CONTRACT_ABANDON_MS) continue;
|
|
753
753
|
if (c.ts < (newestAnswered.get(c.to) || 0)) c.disposition = "superseded";
|
|
754
754
|
}
|
|
755
|
+
// ---- superseded by a later DIRECT reply: the morning case (#11047/#11048) --------------------
|
|
756
|
+
// A row can be unanswerable by a seat that is perfectly healthy: its later replies all carry `re`
|
|
757
|
+
// for NEWER contracts (a re-dispatch, or an "ack by reference" threaded to the newer id), so
|
|
758
|
+
// neither byRe nor the loose fallback ever claims the old row — the two matchers above only ever
|
|
759
|
+
// see replies aimed at the NEWER work. The row then sits WAITING forever: it can never be
|
|
760
|
+
// answered, it can never be abandoned (the seat is alive), and the newestAnswered rule above
|
|
761
|
+
// misses it whenever the newer work was dispatched under a peer identity the old row never shares.
|
|
762
|
+
//
|
|
763
|
+
// The signal both matchers ignored is the DIRECT reply itself: if the assignee has sent this
|
|
764
|
+
// session ANY message after the row was dispatched, the assignee is alive, reachable, and has
|
|
765
|
+
// demonstrably moved on to later work — an older row that has then sat unanswered past the
|
|
766
|
+
// abandon window is dead weight, not in flight. Age still gates it, so honest out-of-order
|
|
767
|
+
// completion (a seat that answered a newer job while still working an older one) is never
|
|
768
|
+
// punished — the older row stays open until the window that any genuine in-flight job would have
|
|
769
|
+
// reported within has passed.
|
|
770
|
+
const latestDirectReply = new Map();
|
|
771
|
+
for (const r of replies) {
|
|
772
|
+
if (r.ts > (latestDirectReply.get(r.from) || 0)) latestDirectReply.set(r.from, r.ts);
|
|
773
|
+
}
|
|
774
|
+
for (const c of out) {
|
|
775
|
+
if (c.answered || c.disposition === "abandoned") continue;
|
|
776
|
+
if (c.ageMs < CONTRACT_ABANDON_MS) continue;
|
|
777
|
+
const latest = latestDirectReply.get(c.to);
|
|
778
|
+
if (latest != null && c.ts < latest) c.disposition = "superseded";
|
|
779
|
+
}
|
|
755
780
|
return out;
|
|
756
781
|
}
|
|
757
782
|
|
|
@@ -1821,6 +1846,13 @@ const server = http.createServer(async (req, res) => {
|
|
|
1821
1846
|
(t.history ||= []).push({ from: t.status, to: b.status, by: b.by || "", ts: now() });
|
|
1822
1847
|
if (t.history.length > 40) t.history.splice(0, 10);
|
|
1823
1848
|
t.status = b.status;
|
|
1849
|
+
// WHO is actually working this card — the SIGNED mover, not the assignee. A card filed by
|
|
1850
|
+
// the orchestrator and built by a seat wore the orchestrator's face on every board
|
|
1851
|
+
// (2026-08-28, operator caught it: "they all say claude"). The assignee stays intent;
|
|
1852
|
+
// workedBy is evidence, stamped only on real work moves and never from a self-asserted by.
|
|
1853
|
+
if (["doing","testing","done"].includes(b.status) && auth?.identity?.name) {
|
|
1854
|
+
t.workedBy = String(auth.identity.name).slice(0, 120);
|
|
1855
|
+
}
|
|
1824
1856
|
}
|
|
1825
1857
|
if (b.difficulty && ["easy","medium","hard"].includes(b.difficulty)) t.difficulty = b.difficulty;
|
|
1826
1858
|
if (b.model !== undefined) t.model = String(b.model).slice(0, 60);
|