trantor 0.18.61 → 0.18.62
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/plugin.json +1 -1
- package/README.md +12 -4
- package/bin/adopt.mjs +28 -13
- package/bin/advise.mjs +12 -28
- package/bin/app.mjs +43 -16
- package/bin/cli.mjs +31 -0
- package/bin/connect.mjs +14 -2
- package/bin/crew/worktrees.mjs +7 -5
- package/bin/crew-runner.mjs +72 -25
- package/bin/doctor.mjs +73 -47
- package/bin/new.mjs +3 -1
- package/bin/provider.mjs +18 -18
- package/bin/secrets.mjs +83 -0
- package/bin/takeover.mjs +4 -4
- package/hooks/lib/hollow-move.mjs +52 -1
- package/hooks/sessionstart.mjs +11 -3
- package/hub/events.mjs +2 -1
- package/hub/routes/admin.mjs +47 -32
- package/hub/routes/cards.mjs +38 -43
- package/lib/balances.mjs +3 -1
- package/lib/classify-failure.mjs +17 -8
- package/lib/launch-env.mjs +23 -0
- package/lib/project.mjs +95 -11
- package/lib/provider-keys.mjs +5 -3
- package/lib/providers.mjs +3 -2
- package/lib/secrets.mjs +232 -0
- package/lib/turn-policy.mjs +14 -0
- package/mcp.mjs +30 -7
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "trantor",
|
|
3
|
-
"version": "0.18.
|
|
3
|
+
"version": "0.18.62",
|
|
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/README.md
CHANGED
|
@@ -150,8 +150,13 @@ the brain
|
|
|
150
150
|
Fix the `→` lines (each CLI's own sign-in happens once, in that CLI) and re-run `trantor doctor`
|
|
151
151
|
until it's clean.
|
|
152
152
|
|
|
153
|
-
Provider API keys (e.g. `DEEPSEEK_API_KEY`)
|
|
154
|
-
|
|
153
|
+
Provider API keys (e.g. `DEEPSEEK_API_KEY`) start in one file, **`~/.agent-bus/.env`**, and on
|
|
154
|
+
macOS they belong in the keychain: `trantor secrets migrate` moves every key there and leaves a
|
|
155
|
+
`# NAME -> keychain` stub in its place, `trantor secrets list` shows which layer holds each key
|
|
156
|
+
(never a value), and `printf '%s' "$KEY" | trantor secrets set NAME` adds one. The crew runner
|
|
157
|
+
reads the store at every turn and hands the keys to the seat in its environment, so nothing is
|
|
158
|
+
copied to disk; a key still in `.env` keeps working as the fallback, and `trantor doctor` names it.
|
|
159
|
+
The store wins over `.env`, which wins over anything Scrooge has.
|
|
155
160
|
|
|
156
161
|
That precedence is the point. Scrooge (the cheap-model router) keeps its own keys in
|
|
157
162
|
`~/.token-scrooge/.env`, and if the crew has no key of its own it falls through to Scrooge's. That
|
|
@@ -304,7 +309,10 @@ STALE with an "aged out" note instead of rotting silently, and todo tiles wear a
|
|
|
304
309
|
day 7. A commit closes the session's focus card and the two link both ways. `trantor doctor`
|
|
305
310
|
cross-checks every hub you know about against the per-project pins and reports any **split-brain**
|
|
306
311
|
(a project live on two hubs) with the exact fix — and `trantor adopt <project>` migrates a project
|
|
307
|
-
between hubs in one verified step, telling stale sessions to restart.
|
|
312
|
+
between hubs in one verified step, telling stale sessions to restart. A project's identity lives
|
|
313
|
+
in its checkout (`.trantor/project.json`, written by `trantor new`, `trantor connect` or
|
|
314
|
+
`trantor project <id>`), so renaming the directory keeps the board, the pin and the sessions; the
|
|
315
|
+
doctor names an orphaned identity and the one command that reclaims it.
|
|
308
316
|
|
|
309
317
|
Crew output is gated mechanically, too: `bin/slop-gate.mjs` runs the vendored
|
|
310
318
|
[anti-slop](https://github.com/dmmulroy/anti-slop) Oxlint rules over an agent's **changed files
|
|
@@ -397,7 +405,7 @@ rate, not work rate.
|
|
|
397
405
|
| `relay_task_add(title, …, difficulty, model, deps, note?, project?)` | Cards with difficulty/model badges + DAG edges; `note` seeds the card's **permanent log**; `project` targets another board when you orchestrate from elsewhere |
|
|
398
406
|
| `relay_task_move(id, status, note?)` | `todo → doing → testing → done` (the gate), `failed`, `blocked` — moves to testing/done should carry a `note`: what you did + the evidence, stored on the card forever |
|
|
399
407
|
| `relay_task_check(id, index, done?)` | Tick one acceptance item on a card's checklist (seeded via `relay_task_add`'s `checklist`) — checked/total is the card's one honest progress denominator |
|
|
400
|
-
| `relay_board` | The project's full board, as text |
|
|
408
|
+
| `relay_board` | The project's full board, as text — or `card:<id>` for one card, or `mine: true` for the calling session's own open cards (doing/testing/todo, newest first) |
|
|
401
409
|
| `relay_scrooge(prompt, task?, difficulty?)` | Fractal cheap-model delegation, with the ledger receipt |
|
|
402
410
|
| `relay_lesson(text, scope?)` | Record a failure lesson — auto-injected into all future crews |
|
|
403
411
|
| `relay_handoff(summary)` | Full-window session succession |
|
package/bin/adopt.mjs
CHANGED
|
@@ -13,9 +13,9 @@
|
|
|
13
13
|
// newest, rather than asserting which one is yours.
|
|
14
14
|
import { readdirSync, statSync, existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
|
|
15
15
|
import { execFileSync } from "node:child_process";
|
|
16
|
-
import { join, dirname } from "node:path";
|
|
16
|
+
import { join, dirname, basename } from "node:path";
|
|
17
17
|
import { homedir } from "node:os";
|
|
18
|
-
import { resolveProject, writeOrchSession } from "../lib/project.mjs";
|
|
18
|
+
import { resolveProject, writeOrchSession, checkoutFor, devRootFor, readOrchSession } from "../lib/project.mjs";
|
|
19
19
|
|
|
20
20
|
const D = "\x1b[2m", B = "\x1b[1m", Y = "\x1b[33m", G = "\x1b[32m", R = "\x1b[0m";
|
|
21
21
|
const args = process.argv.slice(2);
|
|
@@ -23,29 +23,44 @@ const flag = (n) => { const i = args.indexOf(n); return i >= 0 ? args[i + 1] : n
|
|
|
23
23
|
|
|
24
24
|
const project = args.find(a => !a.startsWith("--") && args[args.indexOf(a) - 1] !== "--session")
|
|
25
25
|
|| resolveProject(process.cwd());
|
|
26
|
-
|
|
27
|
-
const dir =
|
|
28
|
-
if (!
|
|
29
|
-
console.error(`no local checkout for ${project} (looked in ${
|
|
26
|
+
// The checkout is found by the project's ID (#6724): a renamed directory still answers.
|
|
27
|
+
const dir = checkoutFor(project);
|
|
28
|
+
if (!dir) {
|
|
29
|
+
console.error(`no local checkout for ${project} (looked in ${devRootFor()})`);
|
|
30
30
|
process.exit(1);
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
/** claude keeps a project's transcripts under a slug of its working directory. */
|
|
34
34
|
const slug = dir.replace(/[/.]/g, "-");
|
|
35
|
-
const
|
|
36
|
-
|
|
35
|
+
const projectsDir = join(homedir(), ".claude", "projects");
|
|
36
|
+
const tdir = join(projectsDir, slug);
|
|
37
|
+
// A session that was running when the directory was renamed keeps writing under the OLD path's
|
|
38
|
+
// slug (#6724: "no transcript in the last hour" while the thread was 9 minutes fresh). The
|
|
39
|
+
// recorded orchestrator sid names that transcript wherever its slug lives, so it stays a candidate.
|
|
40
|
+
const recordedSid = readOrchSession(project);
|
|
41
|
+
const recordedTranscript = (() => {
|
|
42
|
+
if (!recordedSid) return "";
|
|
43
|
+
try {
|
|
44
|
+
for (const d of readdirSync(projectsDir)) {
|
|
45
|
+
const t = join(projectsDir, d, `${recordedSid}.jsonl`);
|
|
46
|
+
if (existsSync(t)) return t;
|
|
47
|
+
}
|
|
48
|
+
} catch {}
|
|
49
|
+
return "";
|
|
50
|
+
})();
|
|
51
|
+
if (!existsSync(tdir) && !recordedTranscript) {
|
|
37
52
|
console.error(`no claude sessions have ever run in ${dir}`);
|
|
38
53
|
process.exit(1);
|
|
39
54
|
}
|
|
40
55
|
|
|
41
56
|
const RECENT_MS = 60 * 60 * 1000;
|
|
42
57
|
const now = Date.now();
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
58
|
+
const transcripts = existsSync(tdir) ? readdirSync(tdir).filter(f => f.endsWith(".jsonl")).map(f => join(tdir, f)) : [];
|
|
59
|
+
if (recordedTranscript && !transcripts.includes(recordedTranscript)) transcripts.push(recordedTranscript);
|
|
60
|
+
const candidates = transcripts
|
|
61
|
+
.map(p => {
|
|
47
62
|
const st = statSync(p);
|
|
48
|
-
return { id:
|
|
63
|
+
return { id: basename(p, ".jsonl"), mtime: st.mtimeMs, size: st.size };
|
|
49
64
|
})
|
|
50
65
|
.filter(c => now - c.mtime < RECENT_MS)
|
|
51
66
|
.sort((a, b) => b.mtime - a.mtime);
|
package/bin/advise.mjs
CHANGED
|
@@ -1,15 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// The Advisor — the brain's front door
|
|
3
|
-
//
|
|
4
|
-
//
|
|
2
|
+
// The Advisor — the brain's front door: given work packages, decide HOW to execute (solo |
|
|
3
|
+
// scrooge | crew | hybrid) from task difficulty × plan economics × context horizon.
|
|
5
4
|
// echo '{"task":"build X","packages":[{"title":"engine","difficulty":"hard"},…]}' | node bin/advise.mjs
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
// Reads (all read-only):
|
|
9
|
-
// ~/.agent-bus/profile.json — the user's declared plans (bin/profile.mjs)
|
|
10
|
-
// ~/.token-scrooge/registry.json — Scrooge's models {cost_in, cost_out, good_for}
|
|
11
|
-
// ~/.token-scrooge/capabilities.json — per-model quality scores
|
|
12
|
-
// Exposed to agents as the MCP tool `relay_advise`; the crew skill calls it at kickoff.
|
|
5
|
+
// Reads profile.json and Scrooge's registry + capabilities (read-only); exposed as `relay_advise`.
|
|
13
6
|
import { readFileSync, existsSync } from "node:fs";
|
|
14
7
|
import { join } from "node:path";
|
|
15
8
|
import { homedir } from "node:os";
|
|
@@ -18,23 +11,15 @@ import { pathToFileURL } from "node:url";
|
|
|
18
11
|
import { busDir, readConfig, resolveProject } from "../lib/project.mjs";
|
|
19
12
|
import { loadCatalog, lookup as catalogLookup, effortParams, UNCATALOGUED_STATUS } from "../lib/model-catalog.mjs";
|
|
20
13
|
import { benchedAt, loadSeatRecord } from "../lib/seat-record.mjs";
|
|
14
|
+
import { openStore } from "../lib/secrets.mjs";
|
|
21
15
|
|
|
22
16
|
const H = homedir();
|
|
23
17
|
const read = (p, fb) => { try { return JSON.parse(readFileSync(p, "utf8")); } catch { return fb; } };
|
|
24
18
|
|
|
25
19
|
// ---- crew roster: BUILT-IN seats + ANY opencode provider the user has brought (BYOM) ----
|
|
26
|
-
// Each seat: the CLI binary
|
|
27
|
-
//
|
|
28
|
-
//
|
|
29
|
-
//
|
|
30
|
-
// GEMINI is deliberately absent: Google retired the free CLI seat (2026-06-18) → `gemini --yolo`
|
|
31
|
-
// crashes exit 1. Its replacement is GLM via opencode.
|
|
32
|
-
//
|
|
33
|
-
// The opencode-driven seats are the BYOM substrate: opencode is a UNIVERSAL adapter, so any
|
|
34
|
-
// provider the user configures in opencode (or declares in their profile) becomes a crew seat
|
|
35
|
-
// with ZERO code change here — `buildRoster()` discovers them at runtime. The built-ins below are
|
|
36
|
-
// just the curated defaults + the two opencode seats with non-obvious mappings (glm: profile key
|
|
37
|
-
// `zai` ↔ opencode provider `zai-coding-plan`).
|
|
20
|
+
// Each seat: the CLI binary (`cli`), the `trantor up` LAUNCH spec, the bus SESSION label, the
|
|
21
|
+
// profile PROVIDER key and, for opencode seats, the opencode provider id (`providerOc`). Gemini is
|
|
22
|
+
// absent (its free CLI seat was retired; GLM via opencode replaced it); buildRoster() finds the rest.
|
|
38
23
|
export const BUILTIN_ROSTER = {
|
|
39
24
|
codex: { cli: "codex", launch: "codex", session: "codex", provider: "codex" },
|
|
40
25
|
kimi: { cli: "kimi", launch: "kimi", session: "kimi", provider: "kimi" },
|
|
@@ -47,11 +32,10 @@ export const BUILTIN_ROSTER = {
|
|
|
47
32
|
const BUILTIN_OC = new Set(Object.values(BUILTIN_ROSTER).filter(s => s.providerOc).map(s => s.providerOc));
|
|
48
33
|
const NEVER_DISCOVER = new Set(["claude", "codex", "kimi", "gemini", "zai", "opencode"]);
|
|
49
34
|
|
|
50
|
-
// Discover opencode providers the user
|
|
51
|
-
//
|
|
52
|
-
//
|
|
53
|
-
//
|
|
54
|
-
// seat with no code edit. T2's capability ingestion then makes it route well by difficulty.
|
|
35
|
+
// Discover opencode providers the user configured (opencode.json `provider` keys and profile
|
|
36
|
+
// providers from `trantor provider add`) that aren't built in. Each becomes an opencode-driven
|
|
37
|
+
// seat under its OWN bus label, so a brought provider lights up a seat with no code edit; T2's
|
|
38
|
+
// capability ingestion then makes it route well by difficulty.
|
|
55
39
|
export function discoverSeats(profile, ocConfig) {
|
|
56
40
|
const out = {};
|
|
57
41
|
const provKeys = new Set([...Object.keys(ocConfig?.provider || {}), ...Object.keys(profile?.providers || {})]);
|
|
@@ -83,7 +67,7 @@ export function loadWorld() {
|
|
|
83
67
|
const opencodeKey = (prov) => !!ocConfig?.provider?.[prov]?.options?.apiKey;
|
|
84
68
|
// a key the user already has for Scrooge counts too — the opencode runner sources these .env
|
|
85
69
|
// files, so e.g. OPENROUTER_API_KEY in ~/.token-scrooge/.env lights up the seat with no extra setup.
|
|
86
|
-
const envHasKey = (k) => !!process.env[k] || [join(H, ".token-scrooge", ".env"), join(H, ".agent-bus", ".env")]
|
|
70
|
+
const envHasKey = (k) => !!process.env[k] || openStore().has(k) || [join(H, ".token-scrooge", ".env"), join(H, ".agent-bus", ".env")]
|
|
87
71
|
.some(f => { try { return readFileSync(f, "utf8").includes(k); } catch { return false; } });
|
|
88
72
|
// a seat is available only if its CLI exists AND (for opencode-driven seats) the provider is
|
|
89
73
|
// actually set up — a present binary with a dead/missing seat must NOT be recommended.
|
package/bin/app.mjs
CHANGED
|
@@ -1,24 +1,18 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// trantor app — install/update the Trantor DESKTOP APP (Tauri) from GitHub Releases.
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
|
|
6
|
-
// for a teammate: `npm i -g trantor && trantor app install` → latest DMG lands in /Applications.
|
|
7
|
-
//
|
|
8
|
-
// trantor app status: installed version vs latest release
|
|
9
|
-
// trantor app install download the latest release DMG and install to /Applications
|
|
10
|
-
// trantor app update same as install (re-pulls whatever is latest)
|
|
11
|
-
//
|
|
2
|
+
// trantor app — install/update the Trantor DESKTOP APP (Tauri) from GitHub Releases. The npm
|
|
3
|
+
// package does not ship desktop/ (a 6MB DMG has no business in node_modules); the app travels as
|
|
4
|
+
// a GitHub Release asset, so `npm i -g trantor && trantor app install` is the whole story.
|
|
5
|
+
|
|
12
6
|
// Release side (maintainer): build the DMG (cd desktop && npm run tauri build), then
|
|
13
7
|
// gh release create app-v<ver> desktop/src-tauri/target/release/bundle/dmg/Trantor_<ver>_aarch64.dmg
|
|
14
|
-
// Any release whose assets include a Trantor_*.dmg is an app release; the newest one wins
|
|
15
|
-
|
|
16
|
-
import { execFileSync } from "node:child_process";
|
|
8
|
+
// Any release whose assets include a Trantor_*.dmg is an app release; the newest one wins.
|
|
9
|
+
import { execFileSync, spawn } from "node:child_process";
|
|
17
10
|
import { createWriteStream, existsSync, rmSync } from "node:fs";
|
|
18
11
|
import { Readable } from "node:stream";
|
|
19
12
|
import { pipeline } from "node:stream/promises";
|
|
20
13
|
import { join } from "node:path";
|
|
21
14
|
import { tmpdir } from "node:os";
|
|
15
|
+
import { cleanLaunchEnv } from "../lib/launch-env.mjs";
|
|
22
16
|
|
|
23
17
|
const REPO = "sashabogi/trantor";
|
|
24
18
|
const APP = "/Applications/Trantor.app";
|
|
@@ -27,11 +21,39 @@ const cmd = process.argv[2] || "status";
|
|
|
27
21
|
|
|
28
22
|
if (process.platform !== "darwin") { console.error("trantor app: the desktop app is macOS-only for now"); process.exit(1); }
|
|
29
23
|
if (!["status", "install", "update"].includes(cmd)) {
|
|
30
|
-
console.error(
|
|
24
|
+
console.error([
|
|
25
|
+
"usage: trantor app [status|install|update]",
|
|
26
|
+
" status installed version vs latest release (default)",
|
|
27
|
+
" install download the latest release DMG and install to /Applications",
|
|
28
|
+
" update same as install, then relaunch the app from a clean env",
|
|
29
|
+
].join("\n"));
|
|
30
|
+
process.exit(1);
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
function sh(file, args) { return execFileSync(file, args, { encoding: "utf8" }); }
|
|
34
34
|
|
|
35
|
+
function appRunning() {
|
|
36
|
+
try { return sh("/usr/bin/pgrep", ["-x", "Trantor"]).trim() !== ""; }
|
|
37
|
+
catch { return false; }
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function pause(ms) { Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms); }
|
|
41
|
+
|
|
42
|
+
// `open` hands the caller's environment to the app, so an update run from a badged crew pane would
|
|
43
|
+
// badge every child the new app spawns and its hand-offs would reattach to the wrong project (#7414).
|
|
44
|
+
function relaunch(wasRunning) {
|
|
45
|
+
if (wasRunning) {
|
|
46
|
+
try { sh("/usr/bin/osascript", ["-e", 'tell application "Trantor" to quit']); } catch {}
|
|
47
|
+
const deadline = Date.now() + 10000;
|
|
48
|
+
while (appRunning() && Date.now() < deadline) pause(200);
|
|
49
|
+
if (appRunning()) { try { sh("/usr/bin/pkill", ["-x", "Trantor"]); } catch {} pause(500); }
|
|
50
|
+
}
|
|
51
|
+
const child = spawn("/usr/bin/open", ["-a", APP], { env: cleanLaunchEnv(), stdio: "ignore", detached: true });
|
|
52
|
+
child.on("error", e => console.error(`relaunch failed: ${e.message}`));
|
|
53
|
+
child.unref();
|
|
54
|
+
console.log(`↻ ${wasRunning ? "quit the old app and " : ""}launched ${APP} from a clean env`);
|
|
55
|
+
}
|
|
56
|
+
|
|
35
57
|
function installedVersion() {
|
|
36
58
|
try { return sh("plutil", ["-extract", "CFBundleShortVersionString", "raw", join(APP, "Contents/Info.plist")]).trim(); }
|
|
37
59
|
catch { return ""; }
|
|
@@ -76,12 +98,14 @@ if (!dl.ok || !dl.body) { console.error(`download failed: HTTP ${dl.status}`); p
|
|
|
76
98
|
await pipeline(Readable.fromWeb(dl.body), createWriteStream(dmg));
|
|
77
99
|
|
|
78
100
|
let mount = "";
|
|
101
|
+
let installed = false;
|
|
102
|
+
const wasRunning = appRunning();
|
|
79
103
|
try {
|
|
80
104
|
// diskutil first: on macOS 26 the deprecated hdiutil shim IGNORES -nobrowse, so the mounted
|
|
81
|
-
// volume popped a Finder window mid-update and read as an install prompt
|
|
105
|
+
// volume popped a Finder window mid-update and read as an install prompt. Parse
|
|
82
106
|
// the mount point as everything after the last " at " — volume names can contain spaces.
|
|
83
107
|
try {
|
|
84
|
-
// real output (verified
|
|
108
|
+
// real output (verified live): tab-separated, same shape as hdiutil —
|
|
85
109
|
// "/dev/disk12s1\tApple_HFS \t/Volumes/Trantor" — last tab field is the mount.
|
|
86
110
|
const out = sh("diskutil", ["image", "attach", "--mountOptions", "nobrowse", "--readOnly", dmg]);
|
|
87
111
|
const line = out.trim().split("\n").filter(l => l.includes("/Volumes/")).pop() || "";
|
|
@@ -100,6 +124,7 @@ try {
|
|
|
100
124
|
// Gatekeeper doesn't refuse the unsigned build on first launch.
|
|
101
125
|
try { sh("xattr", ["-dr", "com.apple.quarantine", APP]); } catch {}
|
|
102
126
|
console.log(`✓ Trantor.app ${installedVersion() || rel.version} installed → ${APP}`);
|
|
127
|
+
installed = true;
|
|
103
128
|
} catch (e) {
|
|
104
129
|
console.error(`install failed: ${e.message}`); process.exitCode = 1;
|
|
105
130
|
} finally {
|
|
@@ -109,3 +134,5 @@ try {
|
|
|
109
134
|
}
|
|
110
135
|
try { rmSync(dmg, { force: true }); } catch {}
|
|
111
136
|
}
|
|
137
|
+
// A replaced app keeps running its deleted binary until relaunched; `update` always relaunches.
|
|
138
|
+
if (installed && (cmd === "update" || wasRunning)) relaunch(wasRunning);
|
package/bin/cli.mjs
CHANGED
|
@@ -23,6 +23,7 @@ switch (cmd) {
|
|
|
23
23
|
case "connect": run("bin/connect.mjs"); break;
|
|
24
24
|
case "profile": run("bin/profile.mjs"); break;
|
|
25
25
|
case "provider": case "providers": run("bin/provider.mjs"); break;
|
|
26
|
+
case "secrets": run("bin/secrets.mjs"); break;
|
|
26
27
|
case "models": run("bin/models.mjs"); break;
|
|
27
28
|
case "advise": run("bin/advise.mjs"); break;
|
|
28
29
|
case "verify": run("bin/crew-verify.mjs"); break;
|
|
@@ -71,6 +72,34 @@ switch (cmd) {
|
|
|
71
72
|
}
|
|
72
73
|
run("hub.mjs"); break;
|
|
73
74
|
}
|
|
75
|
+
case "project": {
|
|
76
|
+
// The checkout's identity (#6724): show where this project's name comes from, or record an id
|
|
77
|
+
// in .trantor/project.json so a directory rename cannot orphan the pin, board and sessions.
|
|
78
|
+
const { resolveProjectInfo, resolveHubInfo, gitRoot, readProjectId, writeProjectId, isProjectId, PROJECT_MARKER } = await import(join(ROOT, "lib/project.mjs"));
|
|
79
|
+
const { basename } = await import("node:path");
|
|
80
|
+
const id = args.find(a => !a.startsWith("--"));
|
|
81
|
+
const root = gitRoot(process.cwd()) || process.cwd();
|
|
82
|
+
const label = basename(root);
|
|
83
|
+
if (!id) {
|
|
84
|
+
const { project, via } = resolveProjectInfo(process.cwd());
|
|
85
|
+
const hub = resolveHubInfo(project);
|
|
86
|
+
const marked = readProjectId(root);
|
|
87
|
+
console.log(`project: ${project} (via ${via})`);
|
|
88
|
+
console.log(`directory: ${label}${marked && marked !== label ? " (a label — the id is recorded in the checkout)" : ""}`);
|
|
89
|
+
console.log(`marker: ${marked ? `${PROJECT_MARKER} → ${marked}` : `none — trantor project ${project} records one, so a rename cannot orphan the identity`}`);
|
|
90
|
+
console.log(`hub: ${hub.url} (via ${hub.via})`);
|
|
91
|
+
break;
|
|
92
|
+
}
|
|
93
|
+
if (!isProjectId(id)) { console.error(`error: "${id}" is not a project id (letters, digits, . _ - ; 80 max)`); process.exit(1); }
|
|
94
|
+
const current = readProjectId(root);
|
|
95
|
+
if (current && current !== id && !args.includes("--force")) {
|
|
96
|
+
console.error(`refused: ${root} is recorded as "${current}" — pass --force to re-claim it as "${id}" (the old name's board, pin and session rows will NOT follow)`);
|
|
97
|
+
process.exit(1);
|
|
98
|
+
}
|
|
99
|
+
const p = writeProjectId(root, id, "trantor project");
|
|
100
|
+
console.log(`${id} recorded in ${p}${current === id ? " (unchanged)" : ""} — commit it so worktrees and clones carry the identity`);
|
|
101
|
+
break;
|
|
102
|
+
}
|
|
74
103
|
case "watch": run("bin/relay-watch.mjs"); break;
|
|
75
104
|
case "catchup": run("bin/catchup.mjs"); break;
|
|
76
105
|
case "agents": run("bin/agents.mjs"); break;
|
|
@@ -202,6 +231,7 @@ switch (cmd) {
|
|
|
202
231
|
trantor connect (re)wire every installed AI CLI to the bus
|
|
203
232
|
trantor profile declare your plans: trantor profile set claude=max codex=plus deepseek=api
|
|
204
233
|
trantor provider bring ANY model (BYOM): list · status [--json] · verify <name> --key … · add <name> --key … · remove <name>
|
|
234
|
+
trantor secrets provider keys in the OS keychain: list · set <NAME> (stdin) · remove <NAME> · migrate [--dry-run]
|
|
205
235
|
trantor models browse live models behind each seat + the router's pick per difficulty
|
|
206
236
|
trantor up … spawn a crew here: trantor up codex kimi deepseek:deepseek glm:zai-coding-plan
|
|
207
237
|
trantor open host THIS session as the project's orchestrator pane (trantor down spares it)
|
|
@@ -227,6 +257,7 @@ switch (cmd) {
|
|
|
227
257
|
trantor recost recompute sub-agent notional cost from on-disk transcripts + reseed the board (repair after upgrade) — [--dry-run]
|
|
228
258
|
trantor handoff finish this session NOW: write a handoff, open a fresh session that takes over, and close this one (manual baton)
|
|
229
259
|
trantor advise ask the Advisor directly (JSON on stdin; --demo to see it)
|
|
260
|
+
trantor project this checkout's identity: project [<id>] — records .trantor/project.json so a directory rename cannot orphan the board, pin and sessions
|
|
230
261
|
trantor hub run the hub in the foreground (setup installs it as a service instead)
|
|
231
262
|
…or manage per-project hub pins: hub list · hub set <project> <url> · hub unset <project>
|
|
232
263
|
seats: which project lives in which directory — seats · seats add · seats up · seats login install
|
package/bin/connect.mjs
CHANGED
|
@@ -7,7 +7,7 @@ import { join, dirname } from "node:path";
|
|
|
7
7
|
import { homedir } from "node:os";
|
|
8
8
|
import { execSync } from "node:child_process";
|
|
9
9
|
import { fileURLToPath } from "node:url";
|
|
10
|
-
import {
|
|
10
|
+
import { resolveProjectInfo, resolveHubInfo, gitRoot, writeProjectId, PROJECT_MARKER } from "../lib/project.mjs";
|
|
11
11
|
|
|
12
12
|
const DRY = process.argv.includes("--dry-run");
|
|
13
13
|
const MCP = join(dirname(dirname(fileURLToPath(import.meta.url))), "mcp.mjs");
|
|
@@ -15,7 +15,8 @@ const MCP = join(dirname(dirname(fileURLToPath(import.meta.url))), "mcp.mjs");
|
|
|
15
15
|
// to and the hub THAT project resolves to. Some CLIs spawn MCP with a scrubbed env where even `git`
|
|
16
16
|
// is missing, so the stamp is the belt; the worktree path rule in lib/project.mjs stays primary.
|
|
17
17
|
// Env wins in resolveHubInfo, so these keys are REFRESHED on every connect run — re-run after a pin change.
|
|
18
|
-
const
|
|
18
|
+
const PROJECT_INFO = resolveProjectInfo(process.cwd());
|
|
19
|
+
const PROJECT_AT_CONNECT = PROJECT_INFO.project;
|
|
19
20
|
const URL_ = resolveHubInfo(PROJECT_AT_CONNECT).url;
|
|
20
21
|
// Graft (github.com/NanoNets/context-graph-engine): local Tree-sitter dependency graph over MCP,
|
|
21
22
|
// wired next to `relay` so a seat locates code in one call; the graph refreshes itself per query
|
|
@@ -260,6 +261,17 @@ ${HAS_GRAFT ? ` - id: trantor-graft
|
|
|
260
261
|
}
|
|
261
262
|
|
|
262
263
|
const found = out.length;
|
|
264
|
+
// The checkout records its id at connect time (#6724) when the name came from the directory, so a
|
|
265
|
+
// later rename carries the pin, board and sessions along. Never from RELAY_PROJECT or a seat
|
|
266
|
+
// worktree path: a badge must not stamp its name into somebody else's repo.
|
|
267
|
+
{
|
|
268
|
+
const root = gitRoot(process.cwd());
|
|
269
|
+
if (PROJECT_INFO.via === "marker") report("project", `id ${PROJECT_AT_CONNECT} already recorded in ${PROJECT_MARKER}`);
|
|
270
|
+
else if (root && PROJECT_INFO.via === "git") {
|
|
271
|
+
if (!DRY) writeProjectId(root, PROJECT_AT_CONNECT, "trantor connect");
|
|
272
|
+
report("project", `id ${PROJECT_AT_CONNECT} recorded in ${PROJECT_MARKER} — commit it so worktrees and clones carry the identity`, root);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
263
275
|
console.log(`trantor connect${DRY ? " (dry run)" : ""} — project: ${PROJECT_AT_CONNECT}, hub: ${URL_}`);
|
|
264
276
|
for (const r of out) console.log(` ${r.cli.padEnd(9)} ${r.status}${r.detail ? ` (${r.detail})` : ""}`);
|
|
265
277
|
if (!found) console.log(" no supported CLIs found on PATH (claude, codex, gemini, kimi, opencode, dsh)");
|
package/bin/crew/worktrees.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { basename, join } from "node:path";
|
|
2
2
|
import { existsSync } from "node:fs";
|
|
3
3
|
import { call } from "./core.mjs";
|
|
4
|
+
import { checkoutFor, readProjectId } from "../../lib/project.mjs";
|
|
4
5
|
|
|
5
6
|
function gitRoot(dir) {
|
|
6
7
|
const result = call("git", ["-C", dir, "rev-parse", "--show-toplevel"]);
|
|
@@ -11,7 +12,8 @@ export function resolveOrchestratorDir(ctx, projectArg) {
|
|
|
11
12
|
const target = projectArg || ctx.project;
|
|
12
13
|
const badge = ctx.env.TRANTOR_ORCH || ctx.env.TRANTOR_SEAT || "";
|
|
13
14
|
const root = gitRoot(ctx.dir);
|
|
14
|
-
|
|
15
|
+
// The checkout's recorded id outranks its directory name (#6724): a renamed dir is the same project.
|
|
16
|
+
const here = root ? (readProjectId(root) || basename(root)) : "";
|
|
15
17
|
if (badge && badge !== target) {
|
|
16
18
|
throw new Error(`trantor open: refused — this shell is badged for '${badge}', not '${target}'; open it from the target project's shell`);
|
|
17
19
|
}
|
|
@@ -19,13 +21,13 @@ export function resolveOrchestratorDir(ctx, projectArg) {
|
|
|
19
21
|
throw new Error(`trantor open: refused — cwd belongs to project '${here}', not '${target}'; cd to the target checkout first`);
|
|
20
22
|
}
|
|
21
23
|
if (!projectArg || target === here) return { dir: ctx.dir, project: target };
|
|
22
|
-
|
|
23
|
-
const targetDir =
|
|
24
|
-
if (
|
|
24
|
+
// By the project's id (#6724): a renamed checkout carries its marker and still answers.
|
|
25
|
+
const targetDir = checkoutFor(target, { ...ctx.env, HOME: ctx.home });
|
|
26
|
+
if (targetDir) {
|
|
25
27
|
console.error(`— opening ${target} in its checkout: ${targetDir} —`);
|
|
26
28
|
return { dir: targetDir, project: target };
|
|
27
29
|
}
|
|
28
|
-
throw new Error(`trantor open: '${target}' has no checkout
|
|
30
|
+
throw new Error(`trantor open: '${target}' has no checkout under ${ctx.env.TRANTOR_DEV_ROOT || join(ctx.home, "development")} — cd into the project first`);
|
|
29
31
|
}
|
|
30
32
|
|
|
31
33
|
function linkedByTest(env, badge, project) {
|