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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "trantor",
3
- "version": "0.18.61",
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`) live in one file: **`~/.agent-bus/.env`** the
154
- crew runners source it automatically, and it wins over anything Scrooge has.
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
- const devRoot = process.env.TRANTOR_DEV_ROOT || join(homedir(), "development");
27
- const dir = join(devRoot, project);
28
- if (!existsSync(dir)) {
29
- console.error(`no local checkout for ${project} (looked in ${devRoot})`);
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 tdir = join(homedir(), ".claude", "projects", slug);
36
- if (!existsSync(tdir)) {
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 candidates = readdirSync(tdir)
44
- .filter(f => f.endsWith(".jsonl"))
45
- .map(f => {
46
- const p = join(tdir, f);
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: f.replace(/\.jsonl$/, ""), mtime: st.mtimeMs, size: st.size };
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. Given work packages, decide HOW to execute:
3
- // solo | scrooge | crew | hybrid from task shape × plan economics × context horizon.
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
- // node bin/advise.mjs --demo # canned example
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 that must exist (`cli`) · the `trantor up` LAUNCH spec · the bus
27
- // SESSION label (its identity on the board) · the profile PROVIDER key (tier/cost) · for
28
- // opencode-driven seats, the opencode PROVIDER id (`providerOc`) used to enumerate models + auth.
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 has configured — from opencode.json `provider` keys AND
51
- // from profile providers declared via `trantor provider add` that aren't already built-in. Each
52
- // becomes an opencode-driven seat under its OWN bus label (distinct session, no collisions). THIS
53
- // is what lets a brought provider (Inception, a Japanese model, any opencode vendor) light up a
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
- // The npm package deliberately does NOT ship desktop/ (a 6MB DMG has no business in node_modules);
5
- // the app travels as a GitHub Release asset instead. This command is the whole distribution story
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, so app
15
- // releases interleave freely with code (npm) releases.
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("usage: trantor app [status|install|update]"); process.exit(1);
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 (2026-08-27). Parse
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 2026-08-27): tab-separated, same shape as hdiutil —
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 { resolveProject, resolveHubInfo } from "../lib/project.mjs";
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 PROJECT_AT_CONNECT = resolveProject(process.cwd());
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)");
@@ -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
- const here = root ? basename(root) : "";
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
- const devRoot = ctx.env.TRANTOR_DEV_ROOT || join(ctx.home, "development");
23
- const targetDir = join(devRoot, target);
24
- if (existsSync(targetDir)) {
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 at ${targetDir} — cd into the project first`);
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) {