trantor 0.18.98 → 0.18.100

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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "trantor",
3
- "version": "0.18.98",
3
+ "version": "0.18.100",
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": {
@@ -0,0 +1,21 @@
1
+ #!/usr/bin/env node
2
+ import { resolve } from "node:path";
3
+ import { parseArgs } from "node:util";
4
+ import { projectAttribution } from "../lib/attribution.mjs";
5
+ import { resolveProject } from "../lib/project.mjs";
6
+ import { signedGet } from "../hooks/lib/api.mjs";
7
+
8
+ try {
9
+ const { values } = parseArgs({ options: { json: { type: "boolean" }, root: { type: "string" }, project: { type: "string" }, ref: { type: "string" }, "cache-dir": { type: "string" } } });
10
+ const root = resolve(values.root || process.cwd());
11
+ const project = values.project || resolveProject(root);
12
+ const result = await projectAttribution({ root, project, ref: values.ref, cacheDir: values["cache-dir"], loadCards: async () => {
13
+ const response = await signedGet(`/tasks?project=${encodeURIComponent(project)}&fields=slim`, { project, timeoutMs: 15000 });
14
+ if (!response.ok || !Array.isArray(response.json?.tasks)) throw new Error("Attribution unavailable: could not read card assignees from the hub");
15
+ return new Map(response.json.tasks.map(card => [card.id, card.assignee]));
16
+ } });
17
+ console.log(JSON.stringify(result));
18
+ } catch (error) {
19
+ console.error(error.message);
20
+ process.exitCode = 1;
21
+ }
package/bin/cli.mjs CHANGED
@@ -36,6 +36,7 @@ switch (cmd) {
36
36
  case "agent-settings": run("bin/agent-settings.mjs"); break;
37
37
  case "adopt": spawn(process.execPath, [join(ROOT, "bin/adopt.mjs"), ...args], { stdio: "inherit", cwd: process.cwd() }).on("exit", c => process.exit(c ?? 0)); break;
38
38
  case "integrate": spawn(process.execPath, [join(ROOT, "bin/integrate.mjs"), ...args], { stdio: "inherit", cwd: process.cwd() }).on("exit", c => process.exit(c ?? 0)); break;
39
+ case "attribution": run("bin/attribution.mjs"); break;
39
40
  case "harvest": run("bin/harvest.mjs"); break;
40
41
  case "sync": run("bin/sync.mjs"); break;
41
42
  case "down": runCrew(); break;
@@ -0,0 +1,114 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import { readFileSync, writeFileSync, mkdirSync, renameSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { readReceipts, shaMatches } from "./harvest.mjs";
5
+
6
+ const SEAT = /^(glm|codex|kimi|openrouter|claude|gemini|deepseek|opencode|qwen|ollama)(-\d+)?$/i;
7
+ const RULES = ["harvest", "card", "author", "claude", "unknown"];
8
+ const MARKER = "TRANTOR-COMMIT";
9
+ const VERSION = 2;
10
+
11
+ export function identity(name, seatOnly = false) {
12
+ const instance = String(name || "").trim().split(":")[0];
13
+ if (!instance || (seatOnly && !SEAT.test(instance))) return null;
14
+ // a non-seat assignee is an orchestrator session (<host>:<project>), same agent as the Claude-trailer rule
15
+ return { agent: SEAT.test(instance) ? instance.toLowerCase().replace(/-\d+$/, "") : "orchestrator", instance };
16
+ }
17
+
18
+ function uniqueIdentity(names) {
19
+ const choices = names.filter(Boolean);
20
+ if (!choices.length) return null;
21
+ return choices.every(row => row.instance === choices[0].instance) ? choices[0] : null;
22
+ }
23
+
24
+ export function resolveCommit(commit, { receipts, cards, branches }) {
25
+ const harvested = receipts.filter(row => shaMatches(row.main, commit.sha));
26
+ const fromHarvest = uniqueIdentity(harvested.map(row => {
27
+ const branch = identity(String(row.branch || "").replace(/^seat\//, ""), true);
28
+ const holders = uniqueIdentity((branches.get(row.seat) || []).map(name => identity(name.replace(/^seat\//, ""), true)));
29
+ return branch || holders || identity(cards.get(Number(row.card)));
30
+ }));
31
+ if (fromHarvest) return { ...fromHarvest, rule: "harvest" };
32
+ const ids = [...commit.subject.matchAll(/#(\d+)\b/g)].map(match => Number(match[1]));
33
+ const fromCard = uniqueIdentity(ids.map(id => identity(cards.get(id))));
34
+ if (fromCard) return { ...fromCard, rule: "card" };
35
+ const author = identity(commit.author, true);
36
+ if (author) return { ...author, rule: "author" };
37
+ if (/^Co-Authored-By:\s*Claude(?:\s|<).*<[^>]+>\s*$/im.test(commit.body)) {
38
+ return { agent: "orchestrator", instance: "orchestrator", rule: "claude" };
39
+ }
40
+ return { agent: "unknown", instance: "unknown", rule: "unknown" };
41
+ }
42
+
43
+ export function parseHistory(raw) {
44
+ const tokens = raw.split("\0");
45
+ const commits = [];
46
+ let current = null;
47
+ for (let i = 0; i < tokens.length; i++) {
48
+ if (tokens[i] === "" && tokens[i + 1] === MARKER) {
49
+ i++;
50
+ current = { sha: tokens[++i], author: tokens[++i], lastAt: tokens[++i], subject: tokens[++i], body: tokens[++i], paths: [] };
51
+ commits.push(current);
52
+ // Git puts one formatting newline before the first NUL-delimited path.
53
+ if (tokens[i + 1]?.startsWith("\n")) tokens[i + 1] = tokens[i + 1].slice(1);
54
+ } else if (current && tokens[i]) current.paths.push(tokens[i]);
55
+ }
56
+ return commits;
57
+ }
58
+
59
+ export function attributeHistory(commits, evidence) {
60
+ const files = new Map();
61
+ const counts = Object.fromEntries(RULES.map(rule => [rule, 0]));
62
+ const totals = new Map();
63
+ for (const commit of commits) {
64
+ const who = resolveCommit(commit, evidence);
65
+ counts[who.rule]++;
66
+ totals.set(who.agent, (totals.get(who.agent) || 0) + 1);
67
+ for (const path of new Set(commit.paths)) {
68
+ if (!files.has(path)) files.set(path, new Map());
69
+ const agents = files.get(path);
70
+ const row = agents.get(who.agent) || { agent: who.agent, commits: 0, lastAt: commit.lastAt, instances: new Set() };
71
+ row.commits++;
72
+ if (Date.parse(commit.lastAt) > Date.parse(row.lastAt)) row.lastAt = commit.lastAt;
73
+ row.instances.add(who.instance);
74
+ agents.set(who.agent, row);
75
+ }
76
+ }
77
+ const order = (a, b) => b.commits - a.commits || a.agent.localeCompare(b.agent);
78
+ return {
79
+ files: [...files].sort(([a], [b]) => a.localeCompare(b)).map(([path, rows]) => {
80
+ const agents = [...rows.values()].map(row => ({ ...row, instances: [...row.instances].sort() })).sort(order);
81
+ return { path, agents, dominant: agents[0].agent };
82
+ }),
83
+ agents: [...totals].map(([agent, commits]) => ({ agent, commits })).sort(order),
84
+ coverage: { total: commits.length, rules: RULES.map(rule => ({ rule, commits: counts[rule], percent: commits.length ? counts[rule] * 100 / commits.length : 0 })) },
85
+ };
86
+ }
87
+
88
+ function git(root, args) {
89
+ return execFileSync("git", args, { cwd: root, encoding: "utf8", maxBuffer: 128 * 1024 * 1024, timeout: 120000 });
90
+ }
91
+
92
+ export async function projectAttribution({ root, project, loadCards, receipts = readReceipts(project), cacheDir = join(root, ".trantor", "cache"), ref = "HEAD" }) {
93
+ const head = git(root, ["rev-parse", "--verify", `${ref}^{commit}`]).trim();
94
+ const cachePath = join(cacheDir, "attribution.json");
95
+ let cached;
96
+ try { cached = JSON.parse(readFileSync(cachePath, "utf8")); }
97
+ catch (error) { if (error.code !== "ENOENT" && !(error instanceof SyntaxError)) throw error; }
98
+ if (cached?.version === VERSION && cached.head === head && cached.project === project) return cached;
99
+ const commits = parseHistory(git(root, ["log", head, "--name-only", "-z", "--diff-merges=first-parent", `--format=%x00${MARKER}%x00%H%x00%an%x00%cI%x00%s%x00%b` ]));
100
+ const cards = await loadCards();
101
+ const branches = new Map();
102
+ for (const receipt of receipts) {
103
+ if (receipt.branch || !commits.some(commit => shaMatches(receipt.main, commit.sha))) continue;
104
+ try {
105
+ branches.set(receipt.seat, git(root, ["branch", "--list", "seat/*", "--contains", receipt.seat, "--format=%(refname:short)"]).trim().split("\n").filter(Boolean));
106
+ } catch { branches.set(receipt.seat, []); }
107
+ }
108
+ const result = { version: VERSION, project, head, ...attributeHistory(commits, { receipts, cards, branches }) };
109
+ mkdirSync(cacheDir, { recursive: true });
110
+ const temp = `${cachePath}.${process.pid}.tmp`;
111
+ writeFileSync(temp, JSON.stringify(result));
112
+ renameSync(temp, cachePath);
113
+ return result;
114
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "trantor",
3
- "version": "0.18.98",
3
+ "version": "0.18.100",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "trantor": "bin/cli.mjs"