trantor 0.18.97 → 0.18.99

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.97",
3
+ "version": "0.18.99",
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;
package/bin/crew/core.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { accessSync, constants, existsSync, mkdirSync, readFileSync } from "node:fs";
1
+ import { accessSync, constants, existsSync, mkdirSync, readFileSync, realpathSync } from "node:fs";
2
2
  import { basename, dirname, join } from "node:path";
3
3
  import { spawnSync } from "node:child_process";
4
4
  import { fileURLToPath } from "node:url";
@@ -85,8 +85,20 @@ function selectMux(env, have) {
85
85
  return "terminal";
86
86
  }
87
87
 
88
+ // PWD keeps a symlinked spelling of the cwd, so prefer it, but only while it still names the cwd.
89
+ // A parent that spawns with a different cwd leaves its own PWD behind: the app relaunched from a
90
+ // trantor shell ran every `trantor open <other>` as trantor and the handoff refused.
91
+ export function workingDir(env = process.env, cwd = process.cwd()) {
92
+ if (!env.PWD) return cwd;
93
+ try {
94
+ return realpathSync(env.PWD) === realpathSync(cwd) ? env.PWD : cwd;
95
+ } catch {
96
+ return cwd;
97
+ }
98
+ }
99
+
88
100
  export function createContext(command, env = process.env) {
89
- const dir = env.PWD || process.cwd();
101
+ const dir = workingDir(env);
90
102
  const home = env.HOME || "";
91
103
  const project = env.RELAY_PROJECT || projectFromGit(dir);
92
104
  const have = {
@@ -0,0 +1,113 @@
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 = 1;
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
+ return { agent: SEAT.test(instance) ? instance.toLowerCase().replace(/-\d+$/, "") : instance, instance };
15
+ }
16
+
17
+ function uniqueIdentity(names) {
18
+ const choices = names.filter(Boolean);
19
+ if (!choices.length) return null;
20
+ return choices.every(row => row.instance === choices[0].instance) ? choices[0] : null;
21
+ }
22
+
23
+ export function resolveCommit(commit, { receipts, cards, branches }) {
24
+ const harvested = receipts.filter(row => shaMatches(row.main, commit.sha));
25
+ const fromHarvest = uniqueIdentity(harvested.map(row => {
26
+ const branch = identity(String(row.branch || "").replace(/^seat\//, ""), true);
27
+ const holders = uniqueIdentity((branches.get(row.seat) || []).map(name => identity(name.replace(/^seat\//, ""), true)));
28
+ return branch || holders || identity(cards.get(Number(row.card)));
29
+ }));
30
+ if (fromHarvest) return { ...fromHarvest, rule: "harvest" };
31
+ const ids = [...commit.subject.matchAll(/#(\d+)\b/g)].map(match => Number(match[1]));
32
+ const fromCard = uniqueIdentity(ids.map(id => identity(cards.get(id))));
33
+ if (fromCard) return { ...fromCard, rule: "card" };
34
+ const author = identity(commit.author, true);
35
+ if (author) return { ...author, rule: "author" };
36
+ if (/^Co-Authored-By:\s*Claude(?:\s|<).*<[^>]+>\s*$/im.test(commit.body)) {
37
+ return { agent: "orchestrator", instance: "orchestrator", rule: "claude" };
38
+ }
39
+ return { agent: "unknown", instance: "unknown", rule: "unknown" };
40
+ }
41
+
42
+ export function parseHistory(raw) {
43
+ const tokens = raw.split("\0");
44
+ const commits = [];
45
+ let current = null;
46
+ for (let i = 0; i < tokens.length; i++) {
47
+ if (tokens[i] === "" && tokens[i + 1] === MARKER) {
48
+ i++;
49
+ current = { sha: tokens[++i], author: tokens[++i], lastAt: tokens[++i], subject: tokens[++i], body: tokens[++i], paths: [] };
50
+ commits.push(current);
51
+ // Git puts one formatting newline before the first NUL-delimited path.
52
+ if (tokens[i + 1]?.startsWith("\n")) tokens[i + 1] = tokens[i + 1].slice(1);
53
+ } else if (current && tokens[i]) current.paths.push(tokens[i]);
54
+ }
55
+ return commits;
56
+ }
57
+
58
+ export function attributeHistory(commits, evidence) {
59
+ const files = new Map();
60
+ const counts = Object.fromEntries(RULES.map(rule => [rule, 0]));
61
+ const totals = new Map();
62
+ for (const commit of commits) {
63
+ const who = resolveCommit(commit, evidence);
64
+ counts[who.rule]++;
65
+ totals.set(who.agent, (totals.get(who.agent) || 0) + 1);
66
+ for (const path of new Set(commit.paths)) {
67
+ if (!files.has(path)) files.set(path, new Map());
68
+ const agents = files.get(path);
69
+ const row = agents.get(who.agent) || { agent: who.agent, commits: 0, lastAt: commit.lastAt, instances: new Set() };
70
+ row.commits++;
71
+ if (Date.parse(commit.lastAt) > Date.parse(row.lastAt)) row.lastAt = commit.lastAt;
72
+ row.instances.add(who.instance);
73
+ agents.set(who.agent, row);
74
+ }
75
+ }
76
+ const order = (a, b) => b.commits - a.commits || a.agent.localeCompare(b.agent);
77
+ return {
78
+ files: [...files].sort(([a], [b]) => a.localeCompare(b)).map(([path, rows]) => {
79
+ const agents = [...rows.values()].map(row => ({ ...row, instances: [...row.instances].sort() })).sort(order);
80
+ return { path, agents, dominant: agents[0].agent };
81
+ }),
82
+ agents: [...totals].map(([agent, commits]) => ({ agent, commits })).sort(order),
83
+ coverage: { total: commits.length, rules: RULES.map(rule => ({ rule, commits: counts[rule], percent: commits.length ? counts[rule] * 100 / commits.length : 0 })) },
84
+ };
85
+ }
86
+
87
+ function git(root, args) {
88
+ return execFileSync("git", args, { cwd: root, encoding: "utf8", maxBuffer: 128 * 1024 * 1024, timeout: 120000 });
89
+ }
90
+
91
+ export async function projectAttribution({ root, project, loadCards, receipts = readReceipts(project), cacheDir = join(root, ".trantor", "cache"), ref = "HEAD" }) {
92
+ const head = git(root, ["rev-parse", "--verify", `${ref}^{commit}`]).trim();
93
+ const cachePath = join(cacheDir, "attribution.json");
94
+ let cached;
95
+ try { cached = JSON.parse(readFileSync(cachePath, "utf8")); }
96
+ catch (error) { if (error.code !== "ENOENT" && !(error instanceof SyntaxError)) throw error; }
97
+ if (cached?.version === VERSION && cached.head === head && cached.project === project) return cached;
98
+ 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` ]));
99
+ const cards = await loadCards();
100
+ const branches = new Map();
101
+ for (const receipt of receipts) {
102
+ if (receipt.branch || !commits.some(commit => shaMatches(receipt.main, commit.sha))) continue;
103
+ try {
104
+ branches.set(receipt.seat, git(root, ["branch", "--list", "seat/*", "--contains", receipt.seat, "--format=%(refname:short)"]).trim().split("\n").filter(Boolean));
105
+ } catch { branches.set(receipt.seat, []); }
106
+ }
107
+ const result = { version: VERSION, project, head, ...attributeHistory(commits, { receipts, cards, branches }) };
108
+ mkdirSync(cacheDir, { recursive: true });
109
+ const temp = `${cachePath}.${process.pid}.tmp`;
110
+ writeFileSync(temp, JSON.stringify(result));
111
+ renameSync(temp, cachePath);
112
+ return result;
113
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "trantor",
3
- "version": "0.18.97",
3
+ "version": "0.18.99",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "trantor": "bin/cli.mjs"