trantor 0.17.15 → 0.17.16
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 +2 -0
- package/bin/git-backfill.mjs +70 -0
- package/hub.mjs +5 -2
- package/package.json +1 -1
|
@@ -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.16"
|
|
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, Gemini, 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.16",
|
|
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.16",
|
|
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
|
@@ -26,6 +26,7 @@ switch (cmd) {
|
|
|
26
26
|
case "hub": run("hub.mjs"); break;
|
|
27
27
|
case "watch": run("bin/relay-watch.mjs"); break;
|
|
28
28
|
case "catchup": run("bin/catchup.mjs"); break;
|
|
29
|
+
case "backfill": run("bin/git-backfill.mjs"); break;
|
|
29
30
|
case "ui": {
|
|
30
31
|
let url = "http://127.0.0.1:4477";
|
|
31
32
|
try { url = JSON.parse(readFileSync(join(process.env.HOME || "", ".agent-bus", "config.json"), "utf8")).url || url; } catch {}
|
|
@@ -46,6 +47,7 @@ switch (cmd) {
|
|
|
46
47
|
trantor down tear the crew down (kills processes, closes windows, no dialogs)
|
|
47
48
|
trantor ui open the live dashboard (board + flow views)
|
|
48
49
|
trantor catchup "where are we?" — the continuous board + git, with a synthesized brief
|
|
50
|
+
trantor backfill card past GIT work onto the board (solo commits that were never carded) — [--since "14 days ago"] [--dry-run]
|
|
49
51
|
trantor advise ask the Advisor directly (JSON on stdin; --demo to see it)
|
|
50
52
|
trantor hub run the hub in the foreground (setup installs it as a service instead)
|
|
51
53
|
trantor watch live bus feed in the terminal
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// trantor backfill — bridge GIT history → the board. Solo work that was committed but never carded
|
|
3
|
+
// (no crew, no TodoWrite) is invisible on the board; this turns it into done-cards so the project's
|
|
4
|
+
// living record reflects what actually happened. Commits are grouped by feature THEME (conventional-
|
|
5
|
+
// commit scope `feat(x):` → "x", or a "Prefix:" → the prefix), one done-card per theme placed at its
|
|
6
|
+
// latest commit time (so it slots into the FLOW timeline correctly). Idempotent: skips titles already
|
|
7
|
+
// on the board. Usage: trantor backfill [--since "14 days ago"] [--project <p>] [--dry-run]
|
|
8
|
+
import { execSync } from "node:child_process";
|
|
9
|
+
import { readFileSync, existsSync } from "node:fs";
|
|
10
|
+
import { join } from "node:path";
|
|
11
|
+
import { homedir } from "node:os";
|
|
12
|
+
import { resolveProject, hostId } from "../lib/project.mjs";
|
|
13
|
+
|
|
14
|
+
function relayUrl() {
|
|
15
|
+
if (process.env.RELAY_URL) return process.env.RELAY_URL;
|
|
16
|
+
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 {}
|
|
17
|
+
return "http://127.0.0.1:4477";
|
|
18
|
+
}
|
|
19
|
+
const args = process.argv.slice(2);
|
|
20
|
+
const arg = (name, def) => { const i = args.indexOf("--" + name); return i >= 0 ? args[i + 1] : def; };
|
|
21
|
+
const dir = process.cwd();
|
|
22
|
+
const project = arg("project", resolveProject(dir));
|
|
23
|
+
const since = arg("since", "14 days ago");
|
|
24
|
+
const dry = args.includes("--dry-run");
|
|
25
|
+
const url = relayUrl();
|
|
26
|
+
const me = `${hostId()}:${project}`;
|
|
27
|
+
|
|
28
|
+
const themeOf = (s) => {
|
|
29
|
+
let m;
|
|
30
|
+
if ((m = s.match(/^[a-z]+\(([^)]+)\)\s*:/i))) return m[1].trim(); // feat(engine): → engine
|
|
31
|
+
if ((m = s.match(/^([A-Za-z][\w &+/.]*?)\s*:/))) return m[1].trim(); // "Landing: …" → Landing
|
|
32
|
+
if ((m = s.match(/^([A-Za-z][\w.+-]*)/))) return m[1]; // first word
|
|
33
|
+
return "misc";
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
let rows = [];
|
|
37
|
+
try {
|
|
38
|
+
rows = execSync(`git -C ${JSON.stringify(dir)} log --since=${JSON.stringify(since)} --format=%H%x09%ct%x09%s`,
|
|
39
|
+
{ encoding: "utf8", maxBuffer: 16 * 1024 * 1024 }).trim().split("\n").filter(Boolean);
|
|
40
|
+
} catch (e) { console.error(`git log failed: ${e.message}`); process.exit(1); }
|
|
41
|
+
if (!rows.length) { console.log(`no commits since "${since}" in ${dir}`); process.exit(0); }
|
|
42
|
+
|
|
43
|
+
const groups = new Map();
|
|
44
|
+
for (const r of rows) {
|
|
45
|
+
const [hash, ct, ...rest] = r.split("\t");
|
|
46
|
+
const subject = rest.join("\t");
|
|
47
|
+
const key = themeOf(subject).slice(0, 32);
|
|
48
|
+
if (!groups.has(key)) groups.set(key, { commits: [], latest: 0 });
|
|
49
|
+
const g = groups.get(key); const ms = Number(ct) * 1000;
|
|
50
|
+
g.commits.push({ hash, ts: ms, subject }); if (ms > g.latest) g.latest = ms;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
let existing = new Set();
|
|
54
|
+
try { const t = (await (await fetch(`${url}/tasks?project=${encodeURIComponent(project)}`)).json()).tasks || []; existing = new Set(t.map(x => x.title)); } catch {}
|
|
55
|
+
|
|
56
|
+
const ents = [...groups.entries()].sort((a, b) => a[1].latest - b[1].latest);
|
|
57
|
+
let posted = 0, skipped = 0;
|
|
58
|
+
for (const [theme, g] of ents) {
|
|
59
|
+
const latest = g.commits.sort((a, b) => b.ts - a.ts)[0];
|
|
60
|
+
const subj = latest.subject.replace(/^[a-z]+\([^)]*\)\s*:\s*/i, "").replace(/^[A-Za-z][\w &+/.]*?:\s*/, "").slice(0, 70);
|
|
61
|
+
const title = `${theme}: ${subj}${g.commits.length > 1 ? ` (+${g.commits.length - 1} more)` : ""}`.slice(0, 190);
|
|
62
|
+
if (existing.has(title)) { skipped++; continue; }
|
|
63
|
+
if (dry) { console.log(`+ [${new Date(g.latest).toISOString().slice(0, 10)}] ${theme.padEnd(20)} ${g.commits.length}c ${title.slice(0, 64)}`); posted++; continue; }
|
|
64
|
+
try {
|
|
65
|
+
await fetch(`${url}/task`, { method: "POST", headers: { "content-type": "application/json" },
|
|
66
|
+
body: JSON.stringify({ project, title, status: "done", phase: theme, source: "git", ts: g.latest, assignee: me, by: me }) });
|
|
67
|
+
posted++;
|
|
68
|
+
} catch (e) { console.error(`post failed for "${title}": ${e.message}`); }
|
|
69
|
+
}
|
|
70
|
+
console.log(`${dry ? "[dry-run] " : ""}backfill: ${posted} theme-card(s) from ${rows.length} commits / ${groups.size} themes (${skipped} already on board) → ${project}`);
|
package/hub.mjs
CHANGED
|
@@ -257,14 +257,17 @@ const server = http.createServer(async (req, res) => {
|
|
|
257
257
|
if (req.method === "POST" && P === "/task") { // create a card
|
|
258
258
|
const b = await body(req); touch(b.by, undefined, b.project);
|
|
259
259
|
const st0 = ["todo","doing","testing","failed","done","blocked"].includes(b.status) ? b.status : "todo";
|
|
260
|
+
// optional historical ts (backfill from git/import) — accept a past epoch-ms; else now().
|
|
261
|
+
const ts0 = (Number.isFinite(b.ts) && b.ts > 0 && b.ts <= now() + 864e5) ? Math.floor(b.ts) : now();
|
|
260
262
|
const t = { id: ++state.taskSeq, project: canon(String(b.project || "").slice(0,80)), title: String(b.title||"").slice(0,200),
|
|
261
263
|
assignee: b.assignee || "", status: st0,
|
|
262
264
|
phase: String(b.phase || "").slice(0, 40), // explicit phase tag (FLOW v2) — wins over title-prefix inference
|
|
265
|
+
source: String(b.source || "").slice(0, 20), // e.g. "git" (backfill), "todo" — provenance
|
|
263
266
|
difficulty: ["easy","medium","hard"].includes(b.difficulty) ? b.difficulty : "",
|
|
264
267
|
model: String(b.model || "").slice(0, 60),
|
|
265
268
|
deps: Array.isArray(b.deps) ? [...new Set(b.deps.map(Number).filter(n => Number.isInteger(n) && n > 0))].slice(0, 20) : [],
|
|
266
|
-
by: b.by || "", ts:
|
|
267
|
-
history: [{ to: st0, by: b.by || "", ts:
|
|
269
|
+
by: b.by || "", ts: ts0, updated: ts0,
|
|
270
|
+
history: [{ to: st0, by: b.by || "", ts: ts0 }] };
|
|
268
271
|
state.tasks.push(t); if (state.tasks.length > 2000) state.tasks.splice(0, 500);
|
|
269
272
|
appendCardEvent("created", t, b.by, null, st0);
|
|
270
273
|
dirty = true; return json(res, 200, { ok: true, task: t });
|