trantor 0.17.22 → 0.17.24
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/baton.mjs +38 -0
- package/bin/cli.mjs +2 -0
- package/bin/write-handoff.mjs +17 -5
- package/hooks/lib/handoff.mjs +39 -1
- package/hooks/sessionstart.mjs +11 -1
- package/package.json +1 -1
- package/skills/handoff/SKILL.md +13 -9
|
@@ -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.24"
|
|
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.24",
|
|
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.24",
|
|
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/baton.mjs
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// `trantor handoff` — one-command manual baton (auto-summary variant). Discovers the current session's
|
|
3
|
+
// transcript, writes a whole-session handoff (auto-summary + verbatim in-flight tail), opens a fresh
|
|
4
|
+
// self-announcing session, and closes THIS window once it takes over. Run from inside the session you
|
|
5
|
+
// want to hand off. (The richer MODEL-authored handoff is the /trantor:handoff skill.)
|
|
6
|
+
import { readdirSync, statSync } from "node:fs";
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
import { homedir } from "node:os";
|
|
9
|
+
import { resolveProject } from "../lib/project.mjs";
|
|
10
|
+
import { writeHandoff, spawnBaton } from "../hooks/lib/handoff.mjs";
|
|
11
|
+
|
|
12
|
+
const cwd = process.cwd();
|
|
13
|
+
const project = resolveProject(cwd);
|
|
14
|
+
|
|
15
|
+
// The active session's transcript = newest *.jsonl directly in this project's Claude dir
|
|
16
|
+
// (~/.claude/projects/<cwd-with-slashes-as-dashes>/), excluding the subagents/ subtree.
|
|
17
|
+
function findTranscript() {
|
|
18
|
+
const dashed = cwd.replace(/\//g, "-");
|
|
19
|
+
const base = join(homedir(), ".claude", "projects");
|
|
20
|
+
let best = "", bestM = 0;
|
|
21
|
+
let dirs = []; try { dirs = readdirSync(base).filter(d => d === dashed || d.endsWith(dashed)); } catch {}
|
|
22
|
+
for (const d of dirs) {
|
|
23
|
+
let ents = []; try { ents = readdirSync(join(base, d)); } catch {}
|
|
24
|
+
for (const f of ents) {
|
|
25
|
+
if (!f.endsWith(".jsonl")) continue;
|
|
26
|
+
try { const m = statSync(join(base, d, f)).mtimeMs; if (m > bestM) { best = join(base, d, f); bestM = m; } } catch {}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return best;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const transcript = findTranscript();
|
|
33
|
+
const { file } = writeHandoff({ projectDir: cwd, sessionId: "", transcript, trigger: "manual-cli" });
|
|
34
|
+
console.log(`📋 handoff saved for ${project}: ${file}`);
|
|
35
|
+
const { spawned, armed, windowId } = spawnBaton({ projectDir: cwd, handoffFile: file });
|
|
36
|
+
console.log(spawned
|
|
37
|
+
? `🔄 baton: a fresh session is opening (it'll recap the handoff)${armed ? ` — this window (${windowId}) closes once it takes over` : " — couldn't detect this window; close it yourself once the new one is up"}`
|
|
38
|
+
: `handoff saved, but couldn't spawn a fresh session (non-macOS or spawn disabled) — open a new session here to take over`);
|
package/bin/cli.mjs
CHANGED
|
@@ -27,6 +27,7 @@ switch (cmd) {
|
|
|
27
27
|
case "watch": run("bin/relay-watch.mjs"); break;
|
|
28
28
|
case "catchup": run("bin/catchup.mjs"); break;
|
|
29
29
|
case "backfill": run("bin/git-backfill.mjs"); break;
|
|
30
|
+
case "handoff": run("bin/baton.mjs"); break;
|
|
30
31
|
case "ui": {
|
|
31
32
|
let url = "http://127.0.0.1:4477";
|
|
32
33
|
try { url = JSON.parse(readFileSync(join(process.env.HOME || "", ".agent-bus", "config.json"), "utf8")).url || url; } catch {}
|
|
@@ -48,6 +49,7 @@ switch (cmd) {
|
|
|
48
49
|
trantor ui open the live dashboard (board + flow views)
|
|
49
50
|
trantor catchup "where are we?" — the continuous board + git, with a synthesized brief
|
|
50
51
|
trantor backfill card past GIT work onto the board (solo commits that were never carded) — [--since "14 days ago"] [--dry-run]
|
|
52
|
+
trantor handoff finish this session NOW: write a handoff, open a fresh session that takes over, and close this one (manual baton)
|
|
51
53
|
trantor advise ask the Advisor directly (JSON on stdin; --demo to see it)
|
|
52
54
|
trantor hub run the hub in the foreground (setup installs it as a service instead)
|
|
53
55
|
trantor watch live bus feed in the terminal
|
package/bin/write-handoff.mjs
CHANGED
|
@@ -1,17 +1,29 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
2
|
+
// Save a model-authored handoff (piped on stdin) for this project; the next session auto-loads it.
|
|
3
|
+
// With --baton: ALSO open a fresh self-announcing session and close THIS window once it takes over
|
|
4
|
+
// (the one-command manual baton behind /trantor:handoff). Without it: just write the file (legacy).
|
|
5
|
+
import { writeFileSync, existsSync, mkdirSync } from "node:fs";
|
|
3
6
|
import { join, basename } from "node:path";
|
|
4
7
|
import { homedir, hostname } from "node:os";
|
|
5
8
|
import { execSync } from "node:child_process";
|
|
9
|
+
import { spawnBaton } from "../hooks/lib/handoff.mjs";
|
|
10
|
+
|
|
11
|
+
const baton = process.argv.includes("--baton");
|
|
6
12
|
const project = process.env.CLAUDE_PROJECT_DIR || process.cwd();
|
|
7
13
|
const name = basename(project);
|
|
8
14
|
let summary = ""; process.stdin.setEncoding("utf8");
|
|
9
15
|
for await (const c of process.stdin) summary += c;
|
|
10
16
|
const dir = join(homedir(), ".agent-bus", "handoffs");
|
|
11
17
|
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
12
|
-
const stamp = (() => { try { return execSync("date +%s",{encoding:"utf8"}).trim(); } catch { return String(process.pid); } })();
|
|
13
|
-
let git=""; try { git = execSync("git -C "+JSON.stringify(project)+" status --short 2>/dev/null | head -30",{encoding:"utf8"}).trim(); } catch {}
|
|
14
|
-
const rec = { id
|
|
18
|
+
const stamp = (() => { try { return execSync("date +%s", { encoding: "utf8" }).trim(); } catch { return String(process.pid); } })();
|
|
19
|
+
let git = ""; try { git = execSync("git -C " + JSON.stringify(project) + " status --short 2>/dev/null | head -30", { encoding: "utf8" }).trim(); } catch {}
|
|
20
|
+
const rec = { id: `${name}-${stamp}`, project, projectName: name, machine: hostname(), trigger: baton ? "manual-baton" : "manual-skill", stamp: Number(stamp) || 0, summary: summary.trim() || "(empty)", gitStatus: git, consumed: false };
|
|
15
21
|
const file = join(dir, `${rec.id}.json`);
|
|
16
|
-
writeFileSync(file, JSON.stringify(rec,null,2));
|
|
22
|
+
writeFileSync(file, JSON.stringify(rec, null, 2));
|
|
17
23
|
console.log(`handoff saved: ${file}`);
|
|
24
|
+
|
|
25
|
+
if (baton) {
|
|
26
|
+
const { spawned, armed, windowId } = spawnBaton({ projectDir: project, handoffFile: file });
|
|
27
|
+
if (spawned) console.log(`baton: fresh session opening (self-recapping)${armed ? ` — this window (${windowId}) closes once it takes over` : " — original window left open (couldn't detect it)"}`);
|
|
28
|
+
else console.log(`baton: could not spawn a fresh session (non-macOS or spawn disabled) — handoff saved, open a new session manually`);
|
|
29
|
+
}
|
package/hooks/lib/handoff.mjs
CHANGED
|
@@ -16,7 +16,7 @@ import { homedir, hostname } from "node:os";
|
|
|
16
16
|
import { execSync, spawn } from "node:child_process";
|
|
17
17
|
import { fileURLToPath } from "node:url";
|
|
18
18
|
|
|
19
|
-
export const HANDOFF_DIR = join(homedir(), ".agent-bus", "handoffs");
|
|
19
|
+
export const HANDOFF_DIR = join(process.env.RELAY_DATA_DIR || join(homedir(), ".agent-bus"), "handoffs");
|
|
20
20
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
21
21
|
|
|
22
22
|
export function readConfig() {
|
|
@@ -274,3 +274,41 @@ export function maybeSpawn(projectDir, conf = readConfig()) {
|
|
|
274
274
|
return true;
|
|
275
275
|
} catch (e) { process.stderr.write(`[trantor] maybeSpawn error: ${e?.message}\n`); return false; }
|
|
276
276
|
}
|
|
277
|
+
|
|
278
|
+
// The self-announcing fresh session command (single-quoted so it survives osascript→shell un-escaped).
|
|
279
|
+
export const RECAP_CMD = "claude 'Recap the handoff you just took over — what was the previous session doing, and where do we continue? Then wait for me.'";
|
|
280
|
+
|
|
281
|
+
// Spawn a fresh self-announcing session WITHOUT the dialog (manual handoff — the user already decided).
|
|
282
|
+
export function spawnFresh(projectDir) {
|
|
283
|
+
try {
|
|
284
|
+
if (process.platform !== "darwin" || process.env.TRANTOR_NO_HANDOFF_SPAWN === "1") return false;
|
|
285
|
+
const script = join(HERE, "..", "..", "bin", "open-session.sh");
|
|
286
|
+
if (!existsSync(script)) return false;
|
|
287
|
+
const child = spawn("/bin/bash", [script, projectDir, RECAP_CMD], { detached: true, stdio: "ignore" });
|
|
288
|
+
child.unref();
|
|
289
|
+
return true;
|
|
290
|
+
} catch { return false; }
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Terminal.app's front window (id + tty) — the fallback when there's no controlling tty (a manual
|
|
294
|
+
// handoff runs through the headless Bash tool). The session you're looking at when you invoke it.
|
|
295
|
+
export function frontTerminalWindow() {
|
|
296
|
+
if (process.platform !== "darwin") return { id: "", tty: "" };
|
|
297
|
+
try {
|
|
298
|
+
const out = execSync(`osascript -e ${JSON.stringify(`tell application "Terminal" to return (id of front window as string) & "|" & (tty of selected tab of front window)`)}`,
|
|
299
|
+
{ encoding: "utf8", timeout: 3000 }).trim();
|
|
300
|
+
const [id, tty] = out.split("|"); return { id: id || "", tty: tty || "" };
|
|
301
|
+
} catch { return { id: "", tty: "" }; }
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// MANUAL one-command baton: spawn the fresh session (no dialog) + arm the close of THIS window once the
|
|
305
|
+
// fresh one consumes the handoff. Window detection: controlling tty first (if invoked with one), else
|
|
306
|
+
// Terminal's front window. Returns { spawned, armed }.
|
|
307
|
+
export function spawnBaton({ projectDir, handoffFile, conf = readConfig() }) {
|
|
308
|
+
const spawned = spawnFresh(projectDir);
|
|
309
|
+
if (!spawned) return { spawned: false, armed: false };
|
|
310
|
+
let tty = controllingTty(), windowId = tty ? terminalWindowForTty(tty) : "";
|
|
311
|
+
if (!windowId) { const f = frontTerminalWindow(); windowId = f.id; tty = f.tty; }
|
|
312
|
+
const armed = windowId ? armBatonClose(handoffFile, windowId, tty, conf) : false;
|
|
313
|
+
return { spawned, armed, windowId };
|
|
314
|
+
}
|
package/hooks/sessionstart.mjs
CHANGED
|
@@ -21,7 +21,17 @@ function loadPendingHandoff(projectName, { claim = true } = {}) {
|
|
|
21
21
|
try {
|
|
22
22
|
const dir = join(homedir(), ".agent-bus", "handoffs");
|
|
23
23
|
if (!existsSync(dir)) return null;
|
|
24
|
-
|
|
24
|
+
// Match ONLY this project's handoffs: "<projectName>-<numeric stamp>.json".
|
|
25
|
+
// NOT a loose startsWith() — that also caught leaked test fixtures like
|
|
26
|
+
// "trantor-handoff-61385-….json" for project "trantor". And sort by the numeric
|
|
27
|
+
// stamp (newest first), NOT lexicographically — string sort ranks a letter prefix
|
|
28
|
+
// ("…-handoff-…") above a digit one, so it could pick a stale/wrong handoff.
|
|
29
|
+
const re = new RegExp("^" + projectName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") + "-(\\d+)\\.json$");
|
|
30
|
+
const files = readdirSync(dir)
|
|
31
|
+
.map(f => { const m = re.exec(f); return m ? { f, stamp: Number(m[1]) } : null; })
|
|
32
|
+
.filter(Boolean)
|
|
33
|
+
.sort((a, b) => b.stamp - a.stamp)
|
|
34
|
+
.map(x => x.f);
|
|
25
35
|
for (const f of files) {
|
|
26
36
|
const p = join(dir, f);
|
|
27
37
|
const rec = JSON.parse(readFileSync(p, "utf8"));
|
package/package.json
CHANGED
package/skills/handoff/SKILL.md
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: handoff
|
|
3
3
|
description: |
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
Finish the CURRENT session in one move: write a rich model-authored handoff, open a fresh
|
|
5
|
+
full-window session that takes over (it auto-recaps the handoff), and close this one once the
|
|
6
|
+
fresh session has it — a clean baton pass, one session at a time. Use when you want to wrap up
|
|
7
|
+
and continue fresh on demand (not just at the compaction threshold). Trigger: /trantor:handoff
|
|
7
8
|
user-invocable: true
|
|
8
9
|
---
|
|
9
10
|
|
|
@@ -23,14 +24,17 @@ re-deriving context, and save it so the next session in this project auto-loads
|
|
|
23
24
|
- **KEY FILES & LOCATIONS** — exact paths, commands, URLs, IDs the successor needs
|
|
24
25
|
- **GOTCHAS** — anything that will bite if forgotten
|
|
25
26
|
|
|
26
|
-
2. Save it
|
|
27
|
+
2. Save it AND pass the baton in one shot — pipe the markdown to the helper with `--baton`:
|
|
27
28
|
```bash
|
|
28
|
-
cat << 'HANDOFF' | node "$
|
|
29
|
+
cat << 'HANDOFF' | node "${CLAUDE_PLUGIN_ROOT}/bin/write-handoff.mjs" --baton
|
|
29
30
|
<your handoff markdown>
|
|
30
31
|
HANDOFF
|
|
31
32
|
```
|
|
32
|
-
|
|
33
|
+
`--baton` writes the handoff, opens a FRESH session that takes over (it auto-recaps the handoff
|
|
34
|
+
on open), and closes THIS Terminal window once the fresh session has consumed it — a true baton
|
|
35
|
+
pass, one session at a time. (Omit `--baton` to only write the handoff without spawning/closing.)
|
|
36
|
+
It's safe: the original window is closed ONLY after the fresh session confirms it took over, and
|
|
37
|
+
never if the fresh session fails to start.
|
|
33
38
|
|
|
34
|
-
3. Tell the user:
|
|
35
|
-
|
|
36
|
-
(The PreCompact hook also writes one automatically at the compaction threshold.)
|
|
39
|
+
3. Tell the user briefly: "Handoff written — a fresh session is opening and will recap it; this
|
|
40
|
+
window closes once it takes over." Then stop (the baton will close this session shortly).
|