trantor 0.17.59 → 0.17.60
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/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/bin/cli.mjs +1 -0
- package/bin/crew-runner.mjs +3 -1
- package/bin/duty.mjs +111 -0
- package/hub.mjs +36 -0
- package/package.json +2 -2
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
"name": "trantor",
|
|
14
14
|
"source": "./",
|
|
15
15
|
"description": "The hub-world for AI agent crews. Say \"fire up the crew\" and Claude becomes the architect: a plan-aware Advisor routes the work (solo / cheap inline calls / live crew of Codex, GLM, Kimi & DeepSeek in their own terminal windows), a Kanban/flow command center with a testing gate tracks it, and an economics brain (Scrooge) keeps the receipts. Includes the relay MCP, a SessionStart auto-discovery hook, and a PreCompact context-handoff so a fresh session can take over a full window instead of compacting.",
|
|
16
|
-
"version": "0.17.
|
|
16
|
+
"version": "0.17.60",
|
|
17
17
|
"author": {
|
|
18
18
|
"name": "Sasha Bogojevic"
|
|
19
19
|
},
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "trantor",
|
|
3
|
-
"version": "0.17.
|
|
3
|
+
"version": "0.17.60",
|
|
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/cli.mjs
CHANGED
|
@@ -70,6 +70,7 @@ switch (cmd) {
|
|
|
70
70
|
case "summarize": run("bin/summarize.mjs"); break;
|
|
71
71
|
case "policy": run("bin/policy.mjs"); break;
|
|
72
72
|
case "inbox": run("bin/inbox.mjs"); break;
|
|
73
|
+
case "duty": run("bin/duty.mjs"); break;
|
|
73
74
|
case "identity": {
|
|
74
75
|
const { load, publicView, generate, keyPath } = await import(join(ROOT, "lib/identity.mjs"));
|
|
75
76
|
const sub = args[0], name = args[1] || "human";
|
package/bin/crew-runner.mjs
CHANGED
|
@@ -136,7 +136,9 @@ const cli = CLI[AGENT] || (NATIVE.has(AGENT) ? null : CLI.opencode);
|
|
|
136
136
|
if (!cli) { console.error(`unknown agent '${AGENT}' (native: ${[...NATIVE].join(", ")}; any other name = an opencode provider seat)`); process.exit(1); }
|
|
137
137
|
if (!CLI[AGENT]) log(`'${AGENT}' is not a built-in seat — running it as an opencode provider (BYOM)`);
|
|
138
138
|
|
|
139
|
-
|
|
139
|
+
// RUNNER_RULES / RUNNER_KICKOFF env overrides: the runner is also the substrate for non-crew
|
|
140
|
+
// always-on seats (the fleet DUTY agent, bin/duty.mjs) whose doctrine is not "work your card".
|
|
141
|
+
const RULES = process.env.RUNNER_RULES || `Rules: you are ${SESSION} on the trantor crew. Work your assigned file(s), report on the bus (relay_send, <280 chars), move your Kanban card as you go (doing -> testing -> done; run the tests in 'testing', use 'failed' + a report if they break). When your work for THIS message is finished, END YOUR TURN — do NOT park, do NOT loop relay_wait; the runner waits for you and will wake you with the next message.`;
|
|
140
142
|
|
|
141
143
|
// ---- failure visibility ----------------------------------------------------
|
|
142
144
|
// A turn's CLI can fail (credits exhausted, auth, crash) and the runner would just
|
package/bin/duty.mjs
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// trantor duty — the always-on fleet DUTY AGENT: one seat that watches the whole hub and triages
|
|
3
|
+
// so the human never has to be a switchboard.
|
|
4
|
+
//
|
|
5
|
+
// trantor duty up [--hub <url>] [--agent claude] start (idempotent — reaps a prior seat)
|
|
6
|
+
// trantor duty down stop
|
|
7
|
+
// trantor duty status pid + last turns + presence
|
|
8
|
+
//
|
|
9
|
+
// Division of labor (the overseer doctrine, extended): DETECTION stays mechanical and hub-side —
|
|
10
|
+
// RELAY_DUTY_SESSION makes the hub DM this seat when a direct message sits undelivered past
|
|
11
|
+
// RELAY_DUTY_UNDELIVERED_MS or the overseer emits a warning. The SEAT only triages: relay, wake,
|
|
12
|
+
// annotate, and only involves the human when a real decision is needed. It runs under the same
|
|
13
|
+
// crew-runner that keeps crew seats alive (long-poll wake, turn telemetry, failure reporting) —
|
|
14
|
+
// just with a triage doctrine instead of "work your card" (RUNNER_RULES / CREW_KICKOFF).
|
|
15
|
+
import { spawn, execSync } from "node:child_process";
|
|
16
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync, openSync, rmSync } from "node:fs";
|
|
17
|
+
import { join, dirname } from "node:path";
|
|
18
|
+
import { homedir } from "node:os";
|
|
19
|
+
import { fileURLToPath } from "node:url";
|
|
20
|
+
import { loadOrCreate } from "../lib/identity.mjs";
|
|
21
|
+
import { sfetchJson } from "../lib/signed-fetch.mjs";
|
|
22
|
+
|
|
23
|
+
const ROOT = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
24
|
+
const BUS = process.env.AGENT_BUS_DIR || join(homedir(), ".agent-bus");
|
|
25
|
+
const DIR = join(BUS, "fleet"); // the seat's cwd — project name "fleet"
|
|
26
|
+
const PIDF = join(BUS, "duty.pid");
|
|
27
|
+
const LOGF = join(BUS, "duty.log");
|
|
28
|
+
|
|
29
|
+
const argv = process.argv.slice(2);
|
|
30
|
+
const cmd = argv[0] || "status";
|
|
31
|
+
const val = (k, d) => { const i = argv.indexOf(`--${k}`); return i >= 0 ? (argv[i + 1] || d) : d; };
|
|
32
|
+
|
|
33
|
+
let config = {}; try { config = JSON.parse(readFileSync(join(BUS, "config.json"), "utf8")); } catch {}
|
|
34
|
+
// The fleet hub: where the projects live. Default = the most common per-project hub in config,
|
|
35
|
+
// falling back to the global url — a fleet watcher belongs where the fleet is.
|
|
36
|
+
function fleetHub() {
|
|
37
|
+
const counts = new Map();
|
|
38
|
+
for (const u of Object.values(config.hubs || {})) counts.set(u, (counts.get(u) || 0) + 1);
|
|
39
|
+
const top = [...counts.entries()].sort((a, b) => b[1] - a[1])[0];
|
|
40
|
+
return val("hub", top?.[0] || config.url || "http://127.0.0.1:4477");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const AGENT = val("agent", "claude");
|
|
44
|
+
const SESSION = `${AGENT}:fleet`;
|
|
45
|
+
|
|
46
|
+
const RULES = `Rules: you are ${SESSION}, the trantor fleet DUTY AGENT — the always-on triage seat. You NEVER write code and NEVER edit project files. On every wake: (1) read the message(s) that woke you; (2) triage with your relay tools — relay_peers for who is live/down, relay_board with the project param for any board, relay_inbox for your own backlog; runner logs live at ~/.agent-bus/logs/<agent>-<project>.jsonl if a seat looks dead; (3) ACT: an UNDELIVERED escalation means the recipient is idle, deaf (wrong hub / stale hooks — a known failure mode), or gone — relay the content to a live session that can act, wake a crew seat with a direct message, or post the information into the project lane so the human's app notifies them; an OVERSEER warning means two parties may collide — message them to coordinate; a seat reported down/errored — check its log tail and either resend its contract or report exactly what is needed. (4) Report each action in ONE bus message (<280 chars) to the lane it concerns. If only a human can decide, say exactly that, in that lane, once. Then END YOUR TURN — the runner wakes you for the next event.`;
|
|
47
|
+
|
|
48
|
+
const KICKOFF = `You are ${SESSION}, the fleet duty agent, freshly started. Do a short patrol: relay_peers (note anything down/errored), then relay_inbox. Handle what is actionable per the Rules, post one line to the bus saying the duty seat is on watch, and end your turn.\n\n${RULES}`;
|
|
49
|
+
|
|
50
|
+
async function ensureFleetIdentity(hub) {
|
|
51
|
+
const id = loadOrCreate(SESSION, "agent");
|
|
52
|
+
const probe = await sfetchJson(`${hub}/peers`, { method: "GET", identity: id, signal: AbortSignal.timeout(5000) }).catch(() => null);
|
|
53
|
+
if (probe && probe.status !== 401) return true; // enrolled (or hub not enforcing)
|
|
54
|
+
// Not enrolled: mint an owner invite with FLEET-WIDE write (write ⊇ read) and enroll with it.
|
|
55
|
+
const owner = loadOrCreate(config.ownerIdentity || "admin", "human");
|
|
56
|
+
const inv = await sfetchJson(`${hub}/invite`, { identity: owner, payload: { scopes: [{ project: "*", role: "write" }], ttlSec: 3600 }, signal: AbortSignal.timeout(5000) });
|
|
57
|
+
const invJson = await inv.json().catch(() => ({}));
|
|
58
|
+
if (!inv.ok || !invJson.token) { console.error(`could not mint fleet invite on ${hub}: ${invJson.error || inv.status} — is ${config.ownerIdentity || "admin"} the owner there?`); return false; }
|
|
59
|
+
const enr = await sfetchJson(`${hub}/enroll`, { identity: id, payload: { token: invJson.token, pubkey: id.pubkey, name: SESSION, kind: "agent" }, signal: AbortSignal.timeout(5000) });
|
|
60
|
+
const enrJson = await enr.json().catch(() => ({}));
|
|
61
|
+
if (!enr.ok) { console.error(`fleet enroll failed: ${enrJson.error || enr.status}`); return false; }
|
|
62
|
+
console.log(`— enrolled ${SESSION} on ${hub} with fleet-wide write —`);
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function alivePid() {
|
|
67
|
+
try {
|
|
68
|
+
const pid = Number(readFileSync(PIDF, "utf8"));
|
|
69
|
+
if (pid && process.kill(pid, 0) === undefined) return pid;
|
|
70
|
+
} catch {}
|
|
71
|
+
return 0;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (cmd === "up") {
|
|
75
|
+
const hub = fleetHub();
|
|
76
|
+
mkdirSync(DIR, { recursive: true });
|
|
77
|
+
const prior = alivePid();
|
|
78
|
+
if (prior) { console.log(`— reaping prior duty seat (pid ${prior}) —`); try { process.kill(prior); } catch {} }
|
|
79
|
+
try { execSync(`pkill -f "crew-runner.mjs ${AGENT} ${DIR}"`, { stdio: "ignore" }); } catch {}
|
|
80
|
+
if (!(await ensureFleetIdentity(hub))) process.exit(1);
|
|
81
|
+
const out = openSync(LOGF, "a");
|
|
82
|
+
const child = spawn(process.execPath, [join(ROOT, "bin", "crew-runner.mjs"), AGENT, DIR], {
|
|
83
|
+
detached: true, stdio: ["ignore", out, out],
|
|
84
|
+
env: { ...process.env, RELAY_URL: hub, RUNNER_RULES: RULES, CREW_KICKOFF: KICKOFF },
|
|
85
|
+
});
|
|
86
|
+
child.unref();
|
|
87
|
+
writeFileSync(PIDF, String(child.pid));
|
|
88
|
+
console.log(`— duty agent up: ${SESSION} (pid ${child.pid}) watching ${hub} — log: ${LOGF}`);
|
|
89
|
+
console.log(` hub feeds it: undelivered DMs (>${Math.round(Number(process.env.RELAY_DUTY_UNDELIVERED_MS || 600000) / 60000)}m) + overseer warnings — set RELAY_DUTY_SESSION=${SESSION} on the hub service.`);
|
|
90
|
+
process.exit(0);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (cmd === "down") {
|
|
94
|
+
const pid = alivePid();
|
|
95
|
+
if (pid) { try { process.kill(pid); } catch {} console.log(`— duty seat stopped (pid ${pid}) —`); }
|
|
96
|
+
else console.log("no duty seat running");
|
|
97
|
+
try { execSync(`pkill -f "crew-runner.mjs ${AGENT} ${DIR}"`, { stdio: "ignore" }); } catch {}
|
|
98
|
+
try { rmSync(PIDF, { force: true }); } catch {}
|
|
99
|
+
process.exit(0);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// status
|
|
103
|
+
{
|
|
104
|
+
const pid = alivePid();
|
|
105
|
+
console.log(pid ? `duty seat RUNNING (pid ${pid}) as ${SESSION}` : "duty seat NOT running");
|
|
106
|
+
try {
|
|
107
|
+
const lines = readFileSync(join(BUS, "logs", `${AGENT}-fleet.jsonl`), "utf8").trim().split("\n").slice(-3);
|
|
108
|
+
console.log("last turns:"); for (const l of lines) console.log(` ${l}`);
|
|
109
|
+
} catch { console.log("(no turns logged yet)"); }
|
|
110
|
+
process.exit(0);
|
|
111
|
+
}
|
package/hub.mjs
CHANGED
|
@@ -245,6 +245,7 @@ function overseerTick() {
|
|
|
245
245
|
overseerWarned.set(key, now());
|
|
246
246
|
appendEvent("overseer.warn", c.project, "overseer",
|
|
247
247
|
{ kind: c.kind, sessions: c.sessions || [], files: c.files || [], detail: c.detail || "", narrated: false });
|
|
248
|
+
if (DUTY_SESSION) hubSend(DUTY_SESSION, `⚠️ OVERSEER ${c.kind} [${c.project}]: ${c.detail || ""} — if the parties are not already coordinating, message them.`, c.project);
|
|
248
249
|
const level = _overseer.levelFor ? _overseer.levelFor(c.project, pol.autonomy) : 1;
|
|
249
250
|
if (level >= 3 && c.kind === "file-conflict") {
|
|
250
251
|
const g = { id: ++state.verifyGateSeq, project: c.project, status: "open", ts: now(),
|
|
@@ -257,6 +258,41 @@ function overseerTick() {
|
|
|
257
258
|
}
|
|
258
259
|
setInterval(overseerTick, OVERSEER_TICK_MS).unref?.();
|
|
259
260
|
|
|
261
|
+
// --- the DUTY AGENT feed (deterministic escalation; the seat itself is bin/duty.mjs) -----------
|
|
262
|
+
// RELAY_DUTY_SESSION names the always-on triage seat (e.g. "claude:fleet"). The hub DMs it when:
|
|
263
|
+
// 1. a DIRECT message has sat undelivered past RELAY_DUTY_UNDELIVERED_MS — the sender believes
|
|
264
|
+
// it was heard and nobody is listening (the crebral-health 4-day failure mode);
|
|
265
|
+
// 2. the overseer emits a warning (wired inside overseerTick below).
|
|
266
|
+
// Escalations are hub-authored ("hub:duty") — they never impersonate a session — and dedup per
|
|
267
|
+
// message id so a standing outage escalates once, not every tick.
|
|
268
|
+
const DUTY_SESSION = String(process.env.RELAY_DUTY_SESSION || "");
|
|
269
|
+
const DUTY_UNDELIVERED_MS = Number(process.env.RELAY_DUTY_UNDELIVERED_MS || 10 * 60 * 1000);
|
|
270
|
+
const dutyEscalated = new Set();
|
|
271
|
+
function hubSend(to, text, project) {
|
|
272
|
+
const msg = { id: ++state.seq, ts: now(), from: "hub:duty", to, text: String(text).slice(0, 2000), project: String(project || "").slice(0, 80) };
|
|
273
|
+
state.messages.push(msg); if (state.messages.length > 5000) state.messages.splice(0, 1000);
|
|
274
|
+
dirty = true; pushToStreams(msg);
|
|
275
|
+
appendEvent("message", msg.project, msg.from, { msgId: msg.id, toSession: msg.to, text: msg.text.slice(0, 2000), refs: [] });
|
|
276
|
+
return msg;
|
|
277
|
+
}
|
|
278
|
+
function dutyTick() {
|
|
279
|
+
if (!DUTY_SESSION) return;
|
|
280
|
+
const cutoff = now() - DUTY_UNDELIVERED_MS;
|
|
281
|
+
const floor = now() - 24 * 3600 * 1000; // never escalate ancient history
|
|
282
|
+
for (const m of state.messages) {
|
|
283
|
+
if (m.ts > cutoff || m.ts < floor) continue;
|
|
284
|
+
if (!m.to || m.to === "all" || m.to === DUTY_SESSION || m.from === "hub:duty") continue;
|
|
285
|
+
if (dutyEscalated.has(m.id)) continue;
|
|
286
|
+
if ((state.peers[m.to]?.deliveredUpTo || 0) >= m.id) continue;
|
|
287
|
+
dutyEscalated.add(m.id);
|
|
288
|
+
hubSend(DUTY_SESSION,
|
|
289
|
+
`⚠️ UNDELIVERED for ${Math.round((now() - m.ts) / 60000)}m: #${m.id} ${m.from} -> ${m.to} — "${String(m.text).slice(0, 280)}" — the recipient has not been handed this. Triage: is the recipient's session idle, deaf (wrong hub / old hooks), or gone? Relay, wake, or note it on their board.`,
|
|
290
|
+
m.project || "");
|
|
291
|
+
}
|
|
292
|
+
if (dutyEscalated.size > 5000) { let n = dutyEscalated.size - 4000; for (const k of dutyEscalated) { dutyEscalated.delete(k); if (--n <= 0) break; } }
|
|
293
|
+
}
|
|
294
|
+
setInterval(dutyTick, OVERSEER_TICK_MS).unref?.();
|
|
295
|
+
|
|
260
296
|
if (durableStore?.subscribeChanges) {
|
|
261
297
|
durableStore.subscribeChanges((p) => {
|
|
262
298
|
if (p && p.src === HUB_SRC) return;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "trantor",
|
|
3
|
-
"version": "0.17.
|
|
3
|
+
"version": "0.17.60",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"bin": {
|
|
6
6
|
"trantor": "bin/cli.mjs"
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
"pg": "^8.22.0"
|
|
12
12
|
},
|
|
13
13
|
"scripts": {
|
|
14
|
-
"test": "node test.mjs && node test-scenarios.mjs && node test-failure.mjs && node test-handoff.mjs && node test-agents.mjs && node test-update.mjs && node test-handoff-guard.mjs && node test-balances.mjs && node test-subagent-cost.mjs && node test-inbox.mjs && node test-inflight.mjs && node test-notify.mjs && node test-focus.mjs && node test-reaper.mjs && node test-events.mjs && node test-scrub.mjs && node test-store-delta.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-inbox-delivery.mjs && node test-hub-routing.mjs && python3 engine/test-routing.py && node test-reconcile.mjs && bash test-crew.sh"
|
|
14
|
+
"test": "node test.mjs && node test-scenarios.mjs && node test-failure.mjs && node test-handoff.mjs && node test-agents.mjs && node test-update.mjs && node test-handoff-guard.mjs && node test-balances.mjs && node test-subagent-cost.mjs && node test-inbox.mjs && node test-inflight.mjs && node test-notify.mjs && node test-focus.mjs && node test-reaper.mjs && node test-events.mjs && node test-scrub.mjs && node test-store-delta.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-inbox-delivery.mjs && node test-hub-routing.mjs && python3 engine/test-routing.py && node test-reconcile.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": [
|