trantor 0.17.50 → 0.17.52
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 +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/bin/cli.mjs +1 -0
- package/bin/crew-runner.mjs +7 -3
- package/bin/reconcile.mjs +124 -0
- package/hooks/sessionstart.mjs +6 -0
- package/package.json +2 -2
|
@@ -6,14 +6,14 @@
|
|
|
6
6
|
},
|
|
7
7
|
"metadata": {
|
|
8
8
|
"description": "Trantor — the hub-world for AI agent crews: live message bus, presence, project Kanban/flow board + context-handoff for independent AI coding agents (Claude, Codex, Gemini, …)",
|
|
9
|
-
"version": "0.17.
|
|
9
|
+
"version": "0.17.52"
|
|
10
10
|
},
|
|
11
11
|
"plugins": [
|
|
12
12
|
{
|
|
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.52",
|
|
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.52",
|
|
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
|
@@ -32,6 +32,7 @@ switch (cmd) {
|
|
|
32
32
|
case "gates": run("bin/gates.mjs"); break;
|
|
33
33
|
case "backfill": run("bin/git-backfill.mjs"); break;
|
|
34
34
|
case "sweep": run("bin/sweep.mjs"); break;
|
|
35
|
+
case "reconcile": run("bin/reconcile.mjs"); break;
|
|
35
36
|
case "init-hooks": run("bin/init-hooks.mjs"); break;
|
|
36
37
|
case "balances": case "balance": case "credits": run("bin/balances.mjs"); break;
|
|
37
38
|
case "recost": run("bin/recost.mjs"); break;
|
package/bin/crew-runner.mjs
CHANGED
|
@@ -79,8 +79,10 @@ const CLI = {
|
|
|
79
79
|
next: `codex exec resume --last{M} --skip-git-repo-check --dangerously-bypass-approvals-and-sandbox "$(cat {P})" < /dev/null`, mflag: " -m " },
|
|
80
80
|
gemini: { first: `gemini --yolo{M} -p "$(cat {P})"`,
|
|
81
81
|
next: `gemini --yolo{M} -r latest -p "$(cat {P})"`, mflag: " -m " },
|
|
82
|
-
kimi
|
|
83
|
-
|
|
82
|
+
// kimi-code (successor to kimi-cli) has no --print (-p alone is non-interactive), REJECTS
|
|
83
|
+
// --yolo in prompt mode (prompt mode auto-approves tools), and emits session_-prefixed ids.
|
|
84
|
+
kimi: { first: `kimi{M} -p "$(cat {P})" < /dev/null`,
|
|
85
|
+
next: `kimi{M} -r {SID} -p "$(cat {P})" < /dev/null`, mflag: " --model ", sid: /To resume this session: kimi -r (\S+)/ },
|
|
84
86
|
deepseek: { first: `opencode run{M} "$(cat {P})"`,
|
|
85
87
|
next: `opencode run -c{M} "$(cat {P})"`, mflag: " -m ", env: join(homedir(), ".token-scrooge", ".env") },
|
|
86
88
|
opencode: { first: `opencode run{M} "$(cat {P})"`,
|
|
@@ -164,8 +166,10 @@ function runTurn(prompt, isFirst, trigger = "kickoff") {
|
|
|
164
166
|
// inherit stdio so the window shows the agent working live; also capture for sid-parsing.
|
|
165
167
|
// Tee stderr to ERRF (still shown live in the window) so a failed turn can be classified.
|
|
166
168
|
try { appendFileSync(ERRF, "", { flag: "w" }); } catch {}
|
|
169
|
+
// pipefail: without it the sid-capture `| tee` makes a FAILED turn exit 0 (tee's status),
|
|
170
|
+
// so the failure reporter never fires and a dead seat heartbeats green on the bus.
|
|
167
171
|
const inner = cli.sid ? `${cmd} | tee /dev/stderr` : cmd;
|
|
168
|
-
const r = spawnSync("/bin/bash", ["-c", `{ ${inner} ; } 2> >(tee -a ${ERRF} >&2)`], {
|
|
172
|
+
const r = spawnSync("/bin/bash", ["-c", `set -o pipefail; { ${inner} ; } 2> >(tee -a ${ERRF} >&2)`], {
|
|
169
173
|
cwd: DIR, encoding: "utf8", stdio: cli.sid ? ["ignore", "pipe", "inherit"] : "inherit",
|
|
170
174
|
env: { ...process.env, RELAY_URL: HUB, RELAY_AGENT: AGENT, RELAY_PROJECT: PROJ },
|
|
171
175
|
maxBuffer: 16 * 1024 * 1024,
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// trantor reconcile — INTELLIGENT board cleanup. The mechanical reaper only knows "owner offline → stale".
|
|
3
|
+
// reconcile reads the STUCK cards + recent git commits + project memory and, for each, asks a CHEAP model
|
|
4
|
+
// (Scrooge) whether it is DONE (already implemented/merged → close it so no future session re-does the work),
|
|
5
|
+
// truly STALE (abandoned), or still ACTIVE (leave it). The judgment is grunt classification → routed to a
|
|
6
|
+
// cheap model, never frontier tokens. Preview-first; changes nothing until --yes.
|
|
7
|
+
// trantor reconcile # preview verdicts for this project's stuck cards
|
|
8
|
+
// trantor reconcile --yes # apply: DONE→done, abandoned→stale, ACTIVE left alone
|
|
9
|
+
// trantor reconcile --older 6h # only cards untouched this long are candidates (default 2h)
|
|
10
|
+
// trantor reconcile --difficulty hard # spend a stronger model on the judgment (default medium)
|
|
11
|
+
import { readFileSync, existsSync } from "node:fs";
|
|
12
|
+
import { join } from "node:path";
|
|
13
|
+
import { homedir } from "node:os";
|
|
14
|
+
import { execSync, spawnSync } from "node:child_process";
|
|
15
|
+
import { resolveProject } from "../lib/project.mjs";
|
|
16
|
+
|
|
17
|
+
function relayUrl() {
|
|
18
|
+
if (process.env.RELAY_URL) return process.env.RELAY_URL;
|
|
19
|
+
try { const c = join(homedir(), ".agent-bus", "config.json"); if (existsSync(c)) { const u = JSON.parse(readFileSync(c, "utf8")).url; if (u) return u; } } catch {}
|
|
20
|
+
return "http://127.0.0.1:4477";
|
|
21
|
+
}
|
|
22
|
+
function parseDur(s, def) {
|
|
23
|
+
if (!s) return def;
|
|
24
|
+
const m = String(s).match(/^(\d+(?:\.\d+)?)\s*(ms|s|m|h|d)?$/i);
|
|
25
|
+
if (!m) return def;
|
|
26
|
+
return Math.round(Number(m[1]) * ({ ms: 1, s: 1000, m: 60000, h: 3600000, d: 86400000 }[(m[2] || "h").toLowerCase()]));
|
|
27
|
+
}
|
|
28
|
+
const fmtAge = ms => { const m = Math.round(ms / 60000); return m >= 1440 ? `${Math.floor(m / 1440)}d` : m >= 60 ? `${Math.floor(m / 60)}h` : `${m}m`; };
|
|
29
|
+
const scroogeBin = () => process.env.SCROOGE_BIN
|
|
30
|
+
|| (() => { try { return execSync("command -v scrooge", { encoding: "utf8" }).trim(); } catch {} })()
|
|
31
|
+
|| (existsSync(new URL("../engine/bin/scrooge", import.meta.url)) ? new URL("../engine/bin/scrooge", import.meta.url).pathname : "");
|
|
32
|
+
|
|
33
|
+
const argv = process.argv.slice(2);
|
|
34
|
+
const has = (...f) => f.some(x => argv.includes(x));
|
|
35
|
+
const val = (...f) => { for (const x of f) { const i = argv.indexOf(x); if (i >= 0) return argv[i + 1]; } return undefined; };
|
|
36
|
+
const olderMs = parseDur(val("--older", "-o"), 2 * 3600 * 1000);
|
|
37
|
+
const doIt = has("--yes", "-y");
|
|
38
|
+
const difficulty = ["easy", "medium", "hard"].includes(val("--difficulty", "-d")) ? val("--difficulty", "-d") : "medium";
|
|
39
|
+
const dir = process.cwd();
|
|
40
|
+
const project = resolveProject(dir);
|
|
41
|
+
const url = relayUrl();
|
|
42
|
+
|
|
43
|
+
async function tasks() {
|
|
44
|
+
const r = await fetch(`${url}/tasks?project=${encodeURIComponent(project)}`, { signal: AbortSignal.timeout(6000) });
|
|
45
|
+
const j = await r.json();
|
|
46
|
+
return Array.isArray(j) ? j : (j.tasks || j.cards || []);
|
|
47
|
+
}
|
|
48
|
+
async function move(id, status) {
|
|
49
|
+
await fetch(`${url}/task/update`, { method: "POST", headers: { "content-type": "application/json" },
|
|
50
|
+
body: JSON.stringify({ id, status, by: "reconcile" }), signal: AbortSignal.timeout(4000) }).catch(() => {});
|
|
51
|
+
}
|
|
52
|
+
// the memory record for THIS project (Claude Code stores it per encoded-cwd); optional context.
|
|
53
|
+
function memoryExcerpt() {
|
|
54
|
+
try {
|
|
55
|
+
const p = join(homedir(), ".claude", "projects", dir.replaceAll("/", "-"), "memory", "MEMORY.md");
|
|
56
|
+
if (existsSync(p)) return readFileSync(p, "utf8").slice(0, 6000);
|
|
57
|
+
} catch {}
|
|
58
|
+
return "";
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
console.log(`\n🧠 trantor reconcile — "${project}" · stuck > ${fmtAge(olderMs)} · judge=scrooge/${difficulty}\n${"─".repeat(60)}`);
|
|
62
|
+
|
|
63
|
+
// candidates: real work cards (not ephemeral sub-agent infra, not the session focus card) that are stuck.
|
|
64
|
+
const cut = Date.now() - olderMs;
|
|
65
|
+
const all = await tasks().catch(e => { console.error(`could not reach hub at ${url}: ${e.message}`); process.exit(1); });
|
|
66
|
+
const cand = all.filter(t =>
|
|
67
|
+
["todo", "doing", "testing", "stale"].includes(t.status) &&
|
|
68
|
+
!["cc-subagent", "cc-bg-agent", "session"].includes(t.source) &&
|
|
69
|
+
(t.updated || t.ts || 0) < cut);
|
|
70
|
+
|
|
71
|
+
if (!cand.length) { console.log(" nothing stuck to reconcile — the board is current.\n"); process.exit(0); }
|
|
72
|
+
|
|
73
|
+
const bin = scroogeBin();
|
|
74
|
+
if (!bin) { console.error(" scrooge not found (install it or set SCROOGE_BIN) — can't judge. Leaving the board untouched.\n"); process.exit(1); }
|
|
75
|
+
|
|
76
|
+
let gitlog = "";
|
|
77
|
+
try { gitlog = execSync(`git -C ${JSON.stringify(dir)} log --oneline -80 2>/dev/null`, { encoding: "utf8", timeout: 3000 }).trim(); } catch {}
|
|
78
|
+
const mem = memoryExcerpt();
|
|
79
|
+
|
|
80
|
+
const cardLines = cand.map(t => `#${t.id} [${t.status}] ${String(t.title).replace(/\s+/g, " ").slice(0, 110)} (updated ${fmtAge(Date.now() - (t.updated || t.ts || 0))} ago)`).join("\n");
|
|
81
|
+
const prompt = `You reconcile a Kanban board against reality. For EACH card, decide if the work is:
|
|
82
|
+
- "done": clearly completed/merged — a recent commit or the memory shows it shipped;
|
|
83
|
+
- "stale": genuinely abandoned — queued/in-progress but no evidence of completion AND clearly old/superseded;
|
|
84
|
+
- "active": still legitimately pending or in progress — LEAVE IT.
|
|
85
|
+
Be CONSERVATIVE: mark "done" ONLY with real evidence (name the matching commit sha), and when unsure use "active".
|
|
86
|
+
Return ONLY a JSON array, no prose: [{"id":<number>,"verdict":"done"|"stale"|"active","reason":"<=90 chars","commit":"<sha or empty>"}]
|
|
87
|
+
|
|
88
|
+
RECENT COMMITS (newest first):
|
|
89
|
+
${gitlog || "(none)"}
|
|
90
|
+
|
|
91
|
+
${mem ? `PROJECT MEMORY (durable record):\n${mem}\n` : ""}
|
|
92
|
+
CARDS TO JUDGE:
|
|
93
|
+
${cardLines}`;
|
|
94
|
+
|
|
95
|
+
process.stdout.write(" judging with a cheap model… ");
|
|
96
|
+
const res = spawnSync(bin, ["-t", "reason", "-d", difficulty, "--json"], { input: prompt, encoding: "utf8", timeout: 90000 });
|
|
97
|
+
if (res.error || (res.status !== 0 && !res.stdout)) { console.error(`\n scrooge unavailable/failed (${res.error ? res.error.code : (res.stderr || "").slice(-200)}) — leaving the board untouched.\n`); process.exit(1); }
|
|
98
|
+
let verdicts = [];
|
|
99
|
+
try {
|
|
100
|
+
const out = res.stdout || "";
|
|
101
|
+
const s = out.indexOf("["), e = out.lastIndexOf("]");
|
|
102
|
+
verdicts = JSON.parse(out.slice(s, e + 1));
|
|
103
|
+
} catch (e) { console.error(`\n couldn't parse the judge's output — leaving the board untouched.\n raw: ${(res.stdout || "").slice(0, 300)}\n`); process.exit(1); }
|
|
104
|
+
console.log("done.\n");
|
|
105
|
+
|
|
106
|
+
const byId = new Map(cand.map(t => [t.id, t]));
|
|
107
|
+
const done = [], stale = [], active = [];
|
|
108
|
+
for (const v of verdicts) {
|
|
109
|
+
const t = byId.get(Number(v.id)); if (!t) continue;
|
|
110
|
+
(v.verdict === "done" ? done : v.verdict === "stale" ? stale : active).push({ ...v, t });
|
|
111
|
+
}
|
|
112
|
+
const show = (label, arr) => { if (!arr.length) return; console.log(` ${label} (${arr.length}):`); for (const x of arr) console.log(` #${x.t.id} [${x.t.status}] ${String(x.t.title).slice(0, 62)}\n → ${x.verdict}${x.commit ? ` (${x.commit})` : ""}: ${x.reason || ""}`); };
|
|
113
|
+
show("✓ DONE — already shipped, will close", done);
|
|
114
|
+
show("🗑 STALE — abandoned, will move to Stale", stale);
|
|
115
|
+
show("• ACTIVE — still relevant, leaving alone", active);
|
|
116
|
+
|
|
117
|
+
if (!done.length && !stale.length) { console.log("\n nothing to change — all stuck cards judged still-active.\n"); process.exit(0); }
|
|
118
|
+
if (!doIt) {
|
|
119
|
+
console.log(`\n ${done.length} card(s) → done, ${stale.length} → stale. Re-run to apply:\n trantor reconcile${val("--older", "-o") ? ` --older ${val("--older", "-o")}` : ""} --yes\n`);
|
|
120
|
+
process.exit(0);
|
|
121
|
+
}
|
|
122
|
+
for (const x of done) await move(x.t.id, "done");
|
|
123
|
+
for (const x of stale) await move(x.t.id, "stale");
|
|
124
|
+
console.log(`\n ✓ reconciled: ${done.length} closed as done, ${stale.length} moved to stale. ${active.length} left active.\n`);
|
package/hooks/sessionstart.mjs
CHANGED
|
@@ -179,6 +179,12 @@ try {
|
|
|
179
179
|
if (cu.blocked?.length) additionalContext += `\n_Blocked:_\n ${sanitize(line(cu.blocked))}\n`;
|
|
180
180
|
if (cu.todo?.length) additionalContext += `\n_Queued (todo):_\n ${sanitize(line(cu.todo))}\n`;
|
|
181
181
|
if (cu.recentDone?.length) additionalContext += `\n_Recently done:_\n ${sanitize(line(cu.recentDone))}\n`;
|
|
182
|
+
// Intelligent-cleanup nudge: if the board has a pile of in-flight/stale cards, some are probably
|
|
183
|
+
// already shipped (a stuck card ≠ unfinished work). Point the session at `trantor reconcile`, which
|
|
184
|
+
// judges each stuck card against git + memory (cheap model) and closes what's done — so no session
|
|
185
|
+
// burns tokens re-doing finished work — and stales what's truly abandoned.
|
|
186
|
+
const stuck = (c.doing || 0) + (c.testing || 0) + (c.stale || 0);
|
|
187
|
+
if (stuck >= 4 || (c.stale || 0) > 0) additionalContext += `\n🧠 **${stuck} card(s) look stuck** (doing/testing/stale). Some may already be shipped. Run \`trantor reconcile\` — it checks git + memory (cheap model), closes anything already done so you don't re-do it, and stales what's abandoned. Preview first; \`--yes\` applies.\n`;
|
|
182
188
|
}
|
|
183
189
|
if (gitlog) additionalContext += `\n**Recent commits:**\n\`\`\`\n${sanitize(gitlog)}\n\`\`\`\n`;
|
|
184
190
|
additionalContext += `\nFor a synthesized "where are we" narrative on demand, run \`trantor catchup\`.\n`;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "trantor",
|
|
3
|
-
"version": "0.17.
|
|
3
|
+
"version": "0.17.52",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"bin": {
|
|
6
6
|
"trantor": "bin/cli.mjs"
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"zod": "^4.4.3"
|
|
11
11
|
},
|
|
12
12
|
"scripts": {
|
|
13
|
-
"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 && python3 engine/test-routing.py && bash test-crew.sh"
|
|
13
|
+
"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 && python3 engine/test-routing.py && node test-reconcile.mjs && bash test-crew.sh"
|
|
14
14
|
},
|
|
15
15
|
"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).",
|
|
16
16
|
"files": [
|