trantor 0.18.41 → 0.18.42

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.
Files changed (48) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/bin/advise.mjs +10 -3
  3. package/bin/agent-settings.mjs +22 -0
  4. package/bin/autonomy.mjs +2 -2
  5. package/bin/cli.mjs +9 -5
  6. package/bin/crew/cmux.mjs +214 -0
  7. package/bin/crew/core.mjs +142 -0
  8. package/bin/crew/herdr.mjs +179 -0
  9. package/bin/crew/models.mjs +62 -0
  10. package/bin/crew/open.mjs +168 -0
  11. package/bin/crew/state.mjs +153 -0
  12. package/bin/crew/tmux.mjs +75 -0
  13. package/bin/crew/verify.mjs +19 -0
  14. package/bin/crew/worktrees.mjs +50 -0
  15. package/bin/crew-runner.mjs +3 -3
  16. package/bin/crew.mjs +116 -0
  17. package/bin/duty.mjs +2 -2
  18. package/bin/herdr-agent.mjs +1 -1
  19. package/bin/policy.mjs +1 -1
  20. package/bin/provider.mjs +13 -0
  21. package/bin/takeover.mjs +1 -1
  22. package/deploy/restart-hub.sh +18 -1
  23. package/hooks/githooks/pre-push +29 -0
  24. package/hooks/lib/resources.mjs +6 -6
  25. package/hooks/sessionstart.mjs +5 -0
  26. package/hub/auth.mjs +360 -0
  27. package/hub/duty.mjs +86 -0
  28. package/hub/events.mjs +170 -0
  29. package/hub/migrations.mjs +0 -0
  30. package/hub/overseer.mjs +206 -0
  31. package/hub/phases.mjs +80 -0
  32. package/hub/reaper.mjs +307 -0
  33. package/hub/routes/admin.mjs +416 -0
  34. package/hub/routes/cards.mjs +695 -0
  35. package/hub/routes/insights.mjs +205 -0
  36. package/hub/routes/messages.mjs +163 -0
  37. package/hub/store.mjs +245 -0
  38. package/hub.mjs +79 -2976
  39. package/lib/agent-preferences.mjs +100 -0
  40. package/lib/autonomy.mjs +1 -1
  41. package/lib/project.mjs +1 -1
  42. package/lib/providers.mjs +2 -2
  43. package/package.json +2 -1
  44. package/skills/crew/SKILL.md +7 -3
  45. package/skills/handoff/SKILL.md +6 -0
  46. package/skills/prd-review/SKILL.md +5 -1
  47. package/skills/research/SKILL.md +4 -0
  48. package/bin/crew.sh +0 -1294
@@ -0,0 +1,62 @@
1
+ import { existsSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { call } from "./core.mjs";
4
+ import { reapSeat } from "./state.mjs";
5
+
6
+ function routeModel(ctx, provider, candidates, task, difficulty) {
7
+ const bundled = join(ctx.root, "engine/bin/scrooge");
8
+ const script = existsSync(bundled) ? bundled : "scrooge";
9
+ const args = candidates
10
+ ? [script, "route", "--candidates", candidates, "-t", task, "-d", difficulty, "--json"]
11
+ : [script, "route", "--provider", provider, "-t", task, "-d", difficulty, "--json"];
12
+ const result = call("python3", args, { env: ctx.env });
13
+ if (!result.ok || !result.stdout) return "";
14
+ try { return JSON.parse(result.stdout).qualified || ""; }
15
+ catch { return ""; }
16
+ }
17
+
18
+ export function resolveModel(ctx, agent, provider, task, difficulty) {
19
+ const result = call("opencode", ["models", provider], { env: ctx.env });
20
+ const candidates = result.ok ? result.stdout.split(/\s+/).filter(Boolean).join(" ") : "";
21
+ let model = routeModel(ctx, provider, candidates, task, difficulty);
22
+ if (!model && candidates) {
23
+ model = `${provider}/${candidates.split(" ")[0]}`;
24
+ console.error(`[crew] router unavailable for ${agent}:${provider} — using the provider's own catalog head (${model})`);
25
+ }
26
+ if (!model) throw new Error(`[crew] live model selection failed for ${agent}:${provider} — no router and no catalog; refusing opencode global default`);
27
+ if (model.split("/")[0] !== provider) throw new Error(`[crew] router selected ${model} outside ${provider} — refusing cross-provider fallback`);
28
+ return model;
29
+ }
30
+
31
+ export function resolveSpec(ctx, spec, task, difficulty, skipped) {
32
+ const [agent, ...rest] = spec.split(":");
33
+ reapSeat(ctx, agent);
34
+ let field = rest.join(":");
35
+ if (!field) {
36
+ if (agent === "glm") field = "zai-coding-plan";
37
+ else if (!["codex", "kimi", "claude", "gemini", "dsh", "opencode"].includes(agent)) field = agent;
38
+ }
39
+ if (!field) return { agent, model: "" };
40
+ if (field.includes("/")) return { agent, model: field };
41
+ if (["claude", "codex", "kimi", "gemini"].includes(agent)) {
42
+ console.log(` → ${agent}: model ${field} (native pin)`);
43
+ return { agent, model: field };
44
+ }
45
+ try {
46
+ const model = resolveModel(ctx, agent, field, task, difficulty);
47
+ console.log(` → ${agent}: live model ${model} (${field} · ${task}/${difficulty})`);
48
+ return { agent, model };
49
+ } catch (error) {
50
+ console.error(error.message);
51
+ console.error(`[crew] ✗ skipping seat '${agent}' — model resolution failed for ${field} (${task}/${difficulty}); remaining seats still launch`);
52
+ skipped.push(`${agent}: model resolution failed for ${field} (${task}/${difficulty})`);
53
+ return null;
54
+ }
55
+ }
56
+
57
+ export function reportSkipped(skipped) {
58
+ if (!skipped.length) return 0;
59
+ console.log(`\n✗✗ ${skipped.length} seat(s) skipped (model resolution failed) — the rest of the crew still launched:`);
60
+ for (const line of skipped) console.log(` - ${line}`);
61
+ return 1;
62
+ }
@@ -0,0 +1,168 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { existsSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { call } from "./core.mjs";
5
+ import { createWorkspace, herdrCall, paneHasAgent, reportAgent, splitPane, workspaceList, workspacePane } from "./herdr.mjs";
6
+ import { dropState, readRows, recordState } from "./state.mjs";
7
+ import { resolveOrchestratorDir } from "./worktrees.mjs";
8
+
9
+ const STRIP = [
10
+ "CLAUDECODE", "CLAUDE_CODE_ENTRYPOINT", "CLAUDE_CODE_SESSION_ID", "CLAUDE_CODE_CHILD_SESSION",
11
+ "CLAUDE_CODE_BRIDGE_SESSION_ID", "CLAUDE_CODE_MESSAGING_SOCKET", "CLAUDE_CODE_MESSAGING_TOKEN",
12
+ "CLAUDE_CODE_EXECPATH", "CLAUDE_PID",
13
+ ];
14
+
15
+ function usage() {
16
+ console.log("usage: trantor open [<project>]\n host THIS project's orchestrator session as a herdr pane in workspace trantor:<project>\n (creating the workspace if needed) and print its TARGET on stdout. A second open REATTACHES:\n it prints the existing target and exits 0 without spawning a second orchestrator.");
17
+ }
18
+
19
+ function parseArgs(args) {
20
+ let project = "";
21
+ for (const arg of args) {
22
+ if (arg === "--help" || arg === "-h") return { help: true };
23
+ if (arg.startsWith("--")) throw new Error(`trantor open: unknown flag '${arg}'`);
24
+ project = arg;
25
+ }
26
+ return { project };
27
+ }
28
+
29
+ function sessionFile(ctx) {
30
+ return join(ctx.home, ".agent-bus", "orch-sessions.txt");
31
+ }
32
+
33
+ function sessionId(ctx) {
34
+ const file = sessionFile(ctx);
35
+ if (existsSync(file)) {
36
+ const row = readFileSync(file, "utf8").split("\n").map(line => line.split("\t")).find(([project, id]) => project === ctx.project && id);
37
+ if (row) return row[1];
38
+ }
39
+ const id = randomUUID();
40
+ if (!ctx.dry) writeFileSync(file, `${existsSync(file) ? readFileSync(file, "utf8") : ""}${ctx.project}\t${id}\n`);
41
+ return id;
42
+ }
43
+
44
+ export function hasPendingHandoff(ctx) {
45
+ const dir = join(ctx.env.AGENT_BUS_DIR || ctx.env.RELAY_DATA_DIR || join(ctx.home, ".agent-bus"), "handoffs");
46
+ if (!existsSync(dir)) return false;
47
+ const prefix = `${ctx.project}-`;
48
+ for (const file of readdirSync(dir).filter(name => name.startsWith(prefix) && name.endsWith(".json")).sort().reverse()) {
49
+ try { if (!JSON.parse(readFileSync(join(dir, file), "utf8")).consumed) return true; }
50
+ catch {}
51
+ }
52
+ return false;
53
+ }
54
+
55
+ export function takeoverSessionId(ctx, id) {
56
+ return hasPendingHandoff(ctx) ? randomUUID() : id;
57
+ }
58
+
59
+ function harnessFlag(ctx) {
60
+ const result = call(process.execPath, [join(ctx.root, "bin/autonomy.mjs"), "get", "harness", "--project", ctx.project], { env: ctx.env });
61
+ return result.stdout === "bypass" ? " --dangerously-skip-permissions" : "";
62
+ }
63
+
64
+ function transcript(ctx, id) {
65
+ const slug = ctx.dir.replace(/[/.]/g, "-");
66
+ return join(ctx.home, ".claude", "projects", slug, `${id}.jsonl`);
67
+ }
68
+
69
+ function orchestratorCommand(ctx, id) {
70
+ const env = `env ${STRIP.map(name => `-u ${name}`).join(" ")} TRANTOR_ORCH=${ctx.project}`;
71
+ const action = existsSync(transcript(ctx, id)) ? `--resume ${id}` : `--session-id ${id}`;
72
+ return `${env} claude${harnessFlag(ctx)} ${action}`;
73
+ }
74
+
75
+ function tracked(ctx, live) {
76
+ let workspace = "";
77
+ let pane = "";
78
+ for (const row of readRows(ctx).filter(row => row.project === ctx.project)) {
79
+ if (row.kind === "herdrws") {
80
+ if (!ctx.dry && live.proven && !live.ids.has(row.handle)) dropState(ctx, ctx.project, "herdrws", "", row.handle);
81
+ else workspace = row.handle;
82
+ }
83
+ if (row.kind === "orch") pane = row.handle;
84
+ }
85
+ return { workspace, pane };
86
+ }
87
+
88
+ function reattach(ctx, workspace, pane, id) {
89
+ if (!pane) return false;
90
+ if (ctx.dry) { console.log(`herdr:${workspace || "%DRYWS"}/${pane}`); return true; }
91
+ const renamed = herdrCall(ctx, ["pane", "rename", pane, `orchestrator · ${ctx.project}`]);
92
+ if (!renamed.ok) { dropState(ctx, ctx.project, "orch"); return false; }
93
+ if (!paneHasAgent(ctx, pane)) {
94
+ herdrCall(ctx, ["pane", "run", pane, orchestratorCommand(ctx, id)]);
95
+ reportAgent(ctx, pane, "claude");
96
+ console.error(`— orchestrator pane was empty: resumed session ${id} in herdr:${workspace || "?"}/${pane} —`);
97
+ } else console.error(`— orchestrator already hosted: reattached to herdr:${workspace || "?"}/${pane} —`);
98
+ console.log(`herdr:${workspace || "?"}/${pane}`);
99
+ return true;
100
+ }
101
+
102
+ function chooseWorkspace(ctx, live, existing) {
103
+ if (existing) return { workspace: existing, pane: "", fresh: false };
104
+ if (ctx.dry) {
105
+ console.error(`[dry] herdr: workspace create (cwd ${ctx.dir}) --label 'trantor:${ctx.project}'`);
106
+ return { workspace: "%DRYWS", pane: "", fresh: false };
107
+ }
108
+ const found = live.items.find(item => [item.label, item.name, item.custom_title].includes(`trantor:${ctx.project}`));
109
+ if (found) {
110
+ const workspace = found.workspace_id || found.id || "";
111
+ console.error(`— adopting existing crew workspace for ${ctx.project} (${workspace}) —`);
112
+ return { workspace, pane: "", fresh: false };
113
+ }
114
+ const created = createWorkspace(ctx, ctx.dir, `trantor:${ctx.project}`);
115
+ if (!created.workspace) throw new Error("trantor open: herdr workspace create failed");
116
+ return { workspace: created.workspace, pane: created.pane, fresh: true };
117
+ }
118
+
119
+ function hostPane(ctx, chosen, id) {
120
+ let pane = chosen.pane;
121
+ if (ctx.dry) {
122
+ pane = "%DRYORCH";
123
+ if (!chosen.fresh) console.error(`[dry] herdr: pane split %DRYHOST(${chosen.workspace}) --direction right --cwd ${ctx.dir}`);
124
+ console.error(`[dry] herdr: pane rename ${pane} 'orchestrator · ${ctx.project}' + run '${orchestratorCommand(ctx, id)}'`);
125
+ return pane;
126
+ }
127
+ if (!chosen.fresh) {
128
+ const host = workspacePane(ctx, chosen.workspace, ctx.dir);
129
+ if (!host) throw new Error(`trantor open: workspace ${chosen.workspace} has no live pane to host the orchestrator`);
130
+ pane = splitPane(ctx, host, "right", ctx.dir);
131
+ }
132
+ if (!pane) throw new Error("trantor open: could not create the orchestrator pane");
133
+ herdrCall(ctx, ["pane", "rename", pane, `orchestrator · ${ctx.project}`]);
134
+ herdrCall(ctx, ["pane", "run", pane, orchestratorCommand(ctx, id)]);
135
+ reportAgent(ctx, pane, "claude");
136
+ return pane;
137
+ }
138
+
139
+ export function openOrchestrator(ctx, args) {
140
+ let parsed;
141
+ try { parsed = parseArgs(args); }
142
+ catch (error) { console.error(error.message); usage(); return 1; }
143
+ if (parsed.help) { usage(); return 0; }
144
+ try {
145
+ Object.assign(ctx, resolveOrchestratorDir(ctx, parsed.project));
146
+ if (!ctx.have.herdr) throw new Error("trantor open needs herdr (the pane host) — install: curl -fsSL https://herdr.dev/install.sh | sh");
147
+ let id = sessionId(ctx);
148
+ if (hasPendingHandoff(ctx)) {
149
+ const fresh = takeoverSessionId(ctx, id);
150
+ console.error(`— recorded session ${id} handed off: starting fresh as ${fresh} to claim it —`);
151
+ id = fresh;
152
+ }
153
+ const live = ctx.dry ? { proven: false, ids: new Set(), names: new Set(), items: [] } : workspaceList(ctx);
154
+ const prior = tracked(ctx, live);
155
+ if (reattach(ctx, prior.workspace, prior.pane, id)) return 0;
156
+ const chosen = chooseWorkspace(ctx, live, prior.workspace);
157
+ recordState(ctx, ctx.project, "herdrws", "__ws__", chosen.workspace);
158
+ const pane = hostPane(ctx, chosen, id);
159
+ dropState(ctx, ctx.project, "orch");
160
+ recordState(ctx, ctx.project, "orch", "__orch__", pane);
161
+ console.log(`herdr:${chosen.workspace}/${pane}`);
162
+ console.error(`— orchestrator pane hosted: herdr:${chosen.workspace}/${pane} (claude in ${ctx.dir}). 'trantor down' spares it. —`);
163
+ return 0;
164
+ } catch (error) {
165
+ console.error(error.message);
166
+ return 1;
167
+ }
168
+ }
@@ -0,0 +1,153 @@
1
+ import { existsSync, readFileSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
2
+ import { call, listPids, run } from "./core.mjs";
3
+
4
+ export function parseRow(line) {
5
+ const fields = line.split("\t");
6
+ if (fields.length >= 4) return { project: fields[0], kind: fields[1], agent: fields[2], handle: fields[3] };
7
+ if (fields.length === 2) return { project: "", kind: "win", agent: fields[0], handle: fields[1] };
8
+ return { project: "", kind: "win", agent: fields[0] || "", handle: fields[1] || "" };
9
+ }
10
+
11
+ export function readRows(ctx) {
12
+ if (!existsSync(ctx.statePath)) return [];
13
+ return readFileSync(ctx.statePath, "utf8").split("\n").filter(Boolean).map(parseRow);
14
+ }
15
+
16
+ export function writeRows(ctx, rows) {
17
+ if (ctx.dry) return;
18
+ if (!rows.length) {
19
+ try { unlinkSync(ctx.statePath); } catch {}
20
+ return;
21
+ }
22
+ const tmp = `${ctx.statePath}.tmp`;
23
+ writeFileSync(tmp, `${rows.map(row => [row.project, row.kind, row.agent, row.handle].join("\t")).join("\n")}\n`);
24
+ renameSync(tmp, ctx.statePath);
25
+ }
26
+
27
+ export function recordState(ctx, project, kind, agent, handle) {
28
+ if (ctx.dry) return;
29
+ const rows = readRows(ctx);
30
+ rows.push({ project, kind, agent, handle });
31
+ writeRows(ctx, rows);
32
+ }
33
+
34
+ export function dropState(ctx, project, kind, agent = "", handle = "") {
35
+ const rows = readRows(ctx).filter(row => !(
36
+ row.project === project && row.kind === kind &&
37
+ (!agent || row.agent === agent) && (!handle || row.handle === handle)
38
+ ));
39
+ writeRows(ctx, rows);
40
+ }
41
+
42
+ function killSeatProcesses(ctx, project, agent) {
43
+ if (ctx.env.CREW_NO_PROC_KILL === "1" || !project || !agent) return;
44
+ const patterns = [`seats/${project}-${agent}\\.sh`, `crew-runner\\.mjs ${agent} .*/${project}$`];
45
+ for (const pattern of patterns) {
46
+ for (const pid of listPids(pattern)) run(ctx, "kill", ["-9", pid], { rendered: `kill -9 ${pid} 2>/dev/null` });
47
+ }
48
+ }
49
+
50
+ function killTerminal(ctx, handle) {
51
+ if (!handle) return;
52
+ const tty = call("osascript", ["-e", `tell application \"Terminal\" to get tty of (first window whose id is ${handle})`]).stdout;
53
+ if (tty) {
54
+ const processes = call("ps", ["-t", tty.replace("/dev/", ""), "-o", "pid="]).stdout.split(/\s+/).filter(Boolean);
55
+ for (const pid of processes) run(ctx, "kill", ["-9", pid], { rendered: `kill -9 ${pid} 2>/dev/null` });
56
+ }
57
+ run(ctx, "osascript", ["-e", `tell application \"Terminal\" to close (first window whose id is ${handle})`]);
58
+ }
59
+
60
+ function usage(project) {
61
+ console.log(`usage: trantor down [<agent>...] [--all] [--yes]\n trantor down tear down THIS project's crew (${project})\n trantor down codex glm tear down only those seats in this project\n trantor down --all --yes tear down EVERY project's crew on this machine (--yes required)`);
62
+ }
63
+
64
+ function parseDownArgs(ctx, args) {
65
+ const parsed = { all: false, yes: false, agents: [] };
66
+ for (const arg of args) {
67
+ if (arg === "--all") parsed.all = true;
68
+ else if (arg === "--yes" || arg === "-y") parsed.yes = true;
69
+ else if (arg === "--help" || arg === "-h") { usage(ctx.project); return { help: true }; }
70
+ else if (arg.startsWith("--")) {
71
+ console.log(`trantor down: unknown flag '${arg}'`);
72
+ usage(ctx.project);
73
+ return { error: true };
74
+ } else parsed.agents.push(arg);
75
+ }
76
+ return parsed;
77
+ }
78
+
79
+ function closeRow(ctx, row, selected, orchProjects, adapters, tmuxClosed) {
80
+ const perSeat = selected.length > 0;
81
+ const hasOrch = orchProjects.has(row.project);
82
+ if (row.kind === "cmuxws" && !perSeat && !hasOrch) adapters.cmux.closeWorkspace(row.handle);
83
+ if (row.kind === "cmux" && (perSeat || hasOrch)) adapters.cmux.closePane(row.handle);
84
+ if (row.kind === "herdrws" && !perSeat && !hasOrch) adapters.herdr.closeWorkspace(row.handle);
85
+ if (row.kind === "herdr" && (perSeat || hasOrch)) adapters.herdr.closePane(row.handle);
86
+ if (row.kind === "tmux") closeTmux(ctx, row, perSeat, tmuxClosed);
87
+ if ((row.kind === "win" || row.kind === "attach") && !(perSeat && row.kind === "attach")) killTerminal(ctx, row.handle);
88
+ if (!["cmuxws", "attach", "herdrws", "orch"].includes(row.kind)) killSeatProcesses(ctx, row.project, row.agent);
89
+ }
90
+
91
+ function closeTmux(ctx, row, perSeat, seen) {
92
+ if (perSeat) {
93
+ run(ctx, "tmux", ["kill-pane", "-t", row.handle], { rendered: `tmux kill-pane -t '${row.handle}' 2>/dev/null` });
94
+ return;
95
+ }
96
+ const session = `trantor:${row.project}`;
97
+ if (seen.has(session)) return;
98
+ seen.add(session);
99
+ run(ctx, "tmux", ["kill-session", "-t", session], { rendered: `tmux kill-session -t '${session}' 2>/dev/null` });
100
+ }
101
+
102
+ export function down(ctx, args, adapters) {
103
+ const options = parseDownArgs(ctx, args);
104
+ if (options.help) return 0;
105
+ if (options.error) return 1;
106
+ const rows = readRows(ctx);
107
+ if (!rows.length) { console.log("no tracked crew windows"); return 0; }
108
+ const scoped = rows.filter(row => (options.all || row.project === ctx.project) && (!options.agents.length || options.agents.includes(row.agent)));
109
+ const scope = options.all ? "all projects" : `project \"${ctx.project}\"`;
110
+ if (!scoped.length) { console.log(`nothing to tear down for ${scope}`); return 0; }
111
+ console.log(`— tearing down (${scope}):`);
112
+ for (const row of scoped) if (!["attach", "cmuxws", "herdrws", "orch"].includes(row.kind)) console.log(` • ${row.project || "<legacy>"} · ${row.agent} (${row.kind})`);
113
+ if (options.all && !options.yes) {
114
+ console.log("— this is EVERY project's crew. Re-run with --yes to confirm: trantor down --all --yes");
115
+ return 0;
116
+ }
117
+ const orchProjects = new Set(rows.filter(row => row.kind === "orch").map(row => row.project));
118
+ const tmuxClosed = new Set();
119
+ for (const row of scoped) closeRow(ctx, row, options.agents, orchProjects, adapters, tmuxClosed);
120
+ const removed = new Set(scoped.map(row => `${row.project}|${row.kind}|${row.agent}|${row.handle}`));
121
+ const kept = rows.filter(row => !removed.has(`${row.project}|${row.kind}|${row.agent}|${row.handle}`) || (orchProjects.has(row.project) && ["herdrws", "cmuxws", "orch"].includes(row.kind)));
122
+ writeRows(ctx, kept);
123
+ console.log(`— crew torn down (${scope})`);
124
+ return 0;
125
+ }
126
+
127
+ function terminalAlive(row) {
128
+ return Boolean(call("osascript", ["-e", `tell application \"Terminal\" to get id of (first window whose id is ${row.handle})`]).stdout);
129
+ }
130
+
131
+ export function prune(ctx, adapters) {
132
+ const rows = readRows(ctx);
133
+ if (!rows.length || ctx.dry) return;
134
+ const cmuxLive = adapters.cmux.liveWorkspaces();
135
+ const herdrLive = adapters.herdr.liveWorkspaces();
136
+ const kept = rows.filter(row => {
137
+ if (["win", "attach"].includes(row.kind)) return terminalAlive(row);
138
+ if (row.kind === "tmux") return call("tmux", ["has-session", "-t", `trantor:${row.project}`]).ok;
139
+ if (row.kind === "cmuxws") return !cmuxLive.proven || cmuxLive.ids.has(row.handle);
140
+ if (row.kind === "cmux") return !cmuxLive.proven || cmuxLive.names.has(`trantor:${row.project}`);
141
+ if (row.kind === "herdrws") return !herdrLive.proven || herdrLive.ids.has(row.handle);
142
+ if (["herdr", "orch"].includes(row.kind)) return !herdrLive.proven || herdrLive.names.has(`trantor:${row.project}`);
143
+ return true;
144
+ });
145
+ writeRows(ctx, kept);
146
+ }
147
+
148
+ export function reapSeat(ctx, agent) {
149
+ const escapedDir = ctx.dir.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
150
+ for (const pid of listPids(`crew-runner\\.mjs ${agent} ${escapedDir}$`)) {
151
+ run(ctx, "kill", ["-9", pid], { rendered: `kill -9 ${pid} 2>/dev/null` });
152
+ }
153
+ }
@@ -0,0 +1,75 @@
1
+ import { appleScript, appleScriptString, call, runnerCommand, run, shellQuote } from "./core.mjs";
2
+ import { recordState } from "./state.mjs";
3
+
4
+ export function spawnTmux(ctx, specs, resolve) {
5
+ const session = `trantor:${ctx.project}`;
6
+ let first = !call("tmux", ["has-session", "-t", session], { env: ctx.env }).ok;
7
+ for (const spec of specs) {
8
+ const seat = resolve(spec);
9
+ if (!seat) continue;
10
+ const command = runnerCommand(ctx, seat.agent, seat.model);
11
+ let pane = ctx.dry ? "%DRY" : "";
12
+ if (first) {
13
+ run(ctx, "tmux", ["new-session", "-d", "-s", session, "-n", "crew", "-x", "260", "-y", "60"], { rendered: `tmux new-session -d -s '${session}' -n crew -x 260 -y 60` });
14
+ run(ctx, "tmux", ["send-keys", "-t", session, command, "Enter"], { rendered: `tmux send-keys -t '${session}' ${shellQuote(command)} Enter` });
15
+ first = false;
16
+ } else {
17
+ run(ctx, "tmux", ["split-window", "-t", session, command], { rendered: `tmux split-window -t '${session}' ${shellQuote(command)}` });
18
+ run(ctx, "tmux", ["select-layout", "-t", session, "tiled"], { rendered: `tmux select-layout -t '${session}' tiled >/dev/null 2>&1` });
19
+ }
20
+ if (!ctx.dry) pane = call("tmux", ["display-message", "-p", "-t", session, "#{pane_id}"], { env: ctx.env }).stdout;
21
+ run(ctx, "tmux", ["select-pane", "-t", pane || session, "-T", seat.agent.toUpperCase()], { rendered: `tmux select-pane -t '${pane || session}' -T ${seat.agent.toUpperCase()}` });
22
+ recordState(ctx, ctx.project, "tmux", seat.agent, pane || "%?");
23
+ console.log(` → ${seat.agent} pane in ${session}`);
24
+ }
25
+ run(ctx, "tmux", ["set-option", "-t", session, "pane-border-status", "top"], { rendered: `tmux set-option -t '${session}' pane-border-status top >/dev/null 2>&1` });
26
+ run(ctx, "tmux", ["set-option", "-t", session, "pane-border-format", " #{pane_title} "], { rendered: `tmux set-option -t '${session}' pane-border-format ' #{pane_title} ' >/dev/null 2>&1` });
27
+ if (ctx.dry) {
28
+ console.log(`[dry] would attach a Terminal window to ${session}`);
29
+ recordState(ctx, ctx.project, "attach", "__win__", "%DRYWIN");
30
+ return;
31
+ }
32
+ const attached = appleScript(`tell application "Terminal"\nset w to do script "tmux attach -t ${appleScriptString(session)}"\nset custom title of w to "${appleScriptString(`${ctx.project} — trantor crew`)}"\nset theWin to first window whose tabs contains w\nreturn id of theWin\nend tell\n`);
33
+ if (attached.stdout) recordState(ctx, ctx.project, "attach", "__win__", attached.stdout);
34
+ console.log(`— crew grouped in tmux session ${session} (one window: ${ctx.project} — trantor crew). Detach with Ctrl-b d; it keeps running. —`);
35
+ }
36
+
37
+ export function spawnTerminal(ctx, specs, resolve) {
38
+ console.log("— no cmux/tmux → per-agent Terminal windows. For ONE grouped, named window per crew (and");
39
+ console.log(" bulletproof scoped teardown), install cmux, or run: brew install tmux —");
40
+ const rect = terminalRect(ctx);
41
+ const columns = specs.length <= 2 ? 1 : 2;
42
+ const rows = Math.ceil(specs.length / columns);
43
+ const width = Math.floor(rect.width / columns);
44
+ const height = Math.floor(rect.height / rows);
45
+ for (let index = 0; index < specs.length; index += 1) {
46
+ const seat = resolve(specs[index]);
47
+ if (!seat) continue;
48
+ const x = rect.x + (index % columns) * width;
49
+ const y = rect.y + Math.floor(index / columns) * height;
50
+ if (ctx.dry) {
51
+ console.log(`[dry] Terminal window for ${seat.agent} at ${x},${y}`);
52
+ recordState(ctx, ctx.project, "win", seat.agent, `%DRYWIN${index}`);
53
+ continue;
54
+ }
55
+ const command = `clear && ${runnerCommand(ctx, seat.agent, seat.model)}`;
56
+ const title = `${ctx.project} · ${seat.agent.toUpperCase()}`;
57
+ const result = appleScript(`tell application "Terminal"\nset w to do script "${appleScriptString(command)}"\nset custom title of w to "${appleScriptString(title)}"\nset theWin to first window whose tabs contains w\nset bounds of theWin to {${x}, ${y}, ${x + width}, ${y + height}}\nreturn id of theWin\nend tell\n`);
58
+ if (result.stdout) {
59
+ recordState(ctx, ctx.project, "win", seat.agent, result.stdout);
60
+ console.log(` → ${seat.agent} window spawned (${ctx.project} · ${seat.agent})`);
61
+ } else console.log(` ✗ ${seat.agent} osascript spawn ERROR`);
62
+ call("sleep", ["1.2"]);
63
+ }
64
+ }
65
+
66
+ function terminalRect(ctx) {
67
+ if (ctx.env.CREW_RECT) {
68
+ const [x, y, width, height] = ctx.env.CREW_RECT.split(",").map(Number);
69
+ return { x, y, width, height };
70
+ }
71
+ const result = call("osascript", ["-e", 'tell application "Finder" to get bounds of window of desktop']);
72
+ const [left = 0, top = 25, right = 1440, bottom = 900] = result.stdout.split(/,\s*/).map(Number);
73
+ const screenWidth = right - left;
74
+ return { x: left + Math.floor(screenWidth * 0.42), y: top || 25, width: Math.floor(screenWidth * 0.58), height: bottom - (top || 25) };
75
+ }
@@ -0,0 +1,19 @@
1
+ import { call } from "./core.mjs";
2
+
3
+ export function epochMs() {
4
+ return Date.now();
5
+ }
6
+
7
+ export function verifyCrew(ctx, agents, since) {
8
+ const result = call(process.execPath, [
9
+ `${ctx.root}/bin/crew-verify.mjs`, ctx.project, ...agents,
10
+ "--since", String(since), "--timeout", "30",
11
+ ], { env: ctx.env });
12
+ if (result.stdout) console.log(result.stdout);
13
+ return result.stdout;
14
+ }
15
+
16
+ export function failedAgents(output) {
17
+ const line = String(output).split("\n").find(value => value.startsWith("FAILED:"));
18
+ return line ? line.slice("FAILED:".length).split(",").map(value => value.trim()).filter(Boolean) : [];
19
+ }
@@ -0,0 +1,50 @@
1
+ import { basename, join } from "node:path";
2
+ import { existsSync } from "node:fs";
3
+ import { call } from "./core.mjs";
4
+
5
+ function gitRoot(dir) {
6
+ const result = call("git", ["-C", dir, "rev-parse", "--show-toplevel"]);
7
+ return result.ok ? result.stdout : "";
8
+ }
9
+
10
+ export function resolveOrchestratorDir(ctx, projectArg) {
11
+ const target = projectArg || ctx.project;
12
+ const badge = ctx.env.TRANTOR_ORCH || ctx.env.TRANTOR_SEAT || "";
13
+ const root = gitRoot(ctx.dir);
14
+ const here = root ? basename(root) : "";
15
+ if (badge && badge !== target) {
16
+ throw new Error(`trantor open: refused — this shell is badged for '${badge}', not '${target}'; open it from the target project's shell`);
17
+ }
18
+ if (here && here !== target) {
19
+ throw new Error(`trantor open: refused — cwd belongs to project '${here}', not '${target}'; cd to the target checkout first`);
20
+ }
21
+ 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)) {
25
+ console.error(`— opening ${target} in its checkout: ${targetDir} —`);
26
+ return { dir: targetDir, project: target };
27
+ }
28
+ throw new Error(`trantor open: '${target}' has no checkout at ${targetDir} — cd into the project first`);
29
+ }
30
+
31
+ function linkedByTest(env, badge, project) {
32
+ if (!("CREW_TEST_POLICY_LINKS" in env)) return null;
33
+ const links = String(env.CREW_TEST_POLICY_LINKS).split(",");
34
+ return links.includes(`${badge}:${project}`) || links.includes(`${project}:${badge}`);
35
+ }
36
+
37
+ export function guardCrossProjectUp(ctx) {
38
+ const badge = ctx.env.TRANTOR_ORCH || ctx.env.TRANTOR_SEAT || "";
39
+ if (!badge || badge === ctx.project) return true;
40
+ let linked = linkedByTest(ctx.env, badge, ctx.project);
41
+ if (linked === null) linked = call(process.execPath, [join(ctx.root, "bin/policy.mjs"), "check", badge, ctx.project], { env: ctx.env }).ok;
42
+ if (!linked) {
43
+ console.error(`trantor: refused — this shell is badged for '${badge}', not '${ctx.project}'.`);
44
+ console.error(" Cross-project action is a breach unless the operator linked the projects.");
45
+ console.error(` Run: trantor policy link ${badge} ${ctx.project} --reason \"<why>\"`);
46
+ return false;
47
+ }
48
+ console.error(`trantor: '${badge}' ↔ '${ctx.project}' is policy-linked — proceeding.`);
49
+ return true;
50
+ }
@@ -31,7 +31,7 @@ import {
31
31
  const AGENT = process.argv[2];
32
32
  const DIR = process.argv[3] || process.cwd();
33
33
  // Crew agents MUST share the orchestrator's project key (one repo = one lane).
34
- // RELAY_PROJECT is inherited from crew.sh (the host's resolved key); else fall
34
+ // RELAY_PROJECT is inherited from crew.mjs (the host's resolved key); else fall
35
35
  // back to the git-repo-root basename — never a loose dir basename that could
36
36
  // fork the host's "builtbetter.ai" into a separate "builtbetter" lane.
37
37
  const PROJ = process.env.RELAY_PROJECT || resolveProject(DIR);
@@ -669,9 +669,9 @@ exit $turn_exit`;
669
669
  cwd: TURN_DIR, encoding: "utf8", stdio: cli.sid ? ["ignore", "pipe", "inherit"] : ["ignore", "inherit", "inherit"],
670
670
  env: { ...process.env, RELAY_URL: HUB, RELAY_AGENT: AGENT, RELAY_SESSION: SESSION, RELAY_PROJECT: PROJ,
671
671
  // #6228: badges this seat's env as belonging to PROJ, distinctly from a one-off RELAY_PROJECT
672
- // override (bin/crew.sh's own tests, and any deliberate `RELAY_PROJECT=x trantor up` from a
672
+ // override (bin/crew.mjs's own tests, and any deliberate `RELAY_PROJECT=x trantor up` from a
673
673
  // plain shell, set RELAY_PROJECT alone and must keep working — only THIS marker means "the
674
- // env I'm running in already has a project home"). crew.sh's `up` guard refuses to bring up a
674
+ // env I'm running in already has a project home"). crew.mjs's `up` guard refuses to bring up a
675
675
  // DIFFERENT project's crew from a shell carrying this badge, same as it refuses TRANTOR_ORCH.
676
676
  TRANTOR_SEAT: PROJ,
677
677
  // A RUNNER-MANAGED SEAT MUST NEVER HAND ITSELF A BATON.