trantor 0.18.60 → 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 +56 -35
- package/bin/app.mjs +43 -16
- package/bin/cli.mjs +33 -0
- package/bin/connect.mjs +36 -11
- package/bin/crew/cmux.mjs +2 -2
- package/bin/crew/core.mjs +5 -2
- package/bin/crew/herdr.mjs +2 -2
- package/bin/crew/models.mjs +11 -4
- package/bin/crew/tmux.mjs +2 -2
- package/bin/crew/worktrees.mjs +7 -5
- package/bin/crew-runner.mjs +84 -25
- package/bin/doctor.mjs +73 -47
- package/bin/new.mjs +3 -1
- package/bin/provider.mjs +18 -18
- package/bin/seat-record.mjs +41 -0
- package/bin/secrets.mjs +83 -0
- package/bin/takeover.mjs +4 -4
- package/configs/model-catalog.json +119 -0
- package/hooks/ask-sidecar.mjs +64 -12
- package/hooks/hooks.json +18 -0
- 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/model-catalog.mjs +123 -0
- package/lib/project.mjs +95 -11
- package/lib/provider-keys.mjs +5 -3
- package/lib/providers.mjs +3 -2
- package/lib/seat-record.mjs +150 -0
- package/lib/secrets.mjs +232 -0
- package/lib/turn-policy.mjs +14 -0
- package/mcp.mjs +37 -9
- 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,38 +1,25 @@
|
|
|
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";
|
|
16
9
|
import { execSync } from "node:child_process";
|
|
17
10
|
import { pathToFileURL } from "node:url";
|
|
18
|
-
import { busDir, readConfig } from "../lib/project.mjs";
|
|
11
|
+
import { busDir, readConfig, resolveProject } from "../lib/project.mjs";
|
|
12
|
+
import { loadCatalog, lookup as catalogLookup, effortParams, UNCATALOGUED_STATUS } from "../lib/model-catalog.mjs";
|
|
13
|
+
import { benchedAt, loadSeatRecord } from "../lib/seat-record.mjs";
|
|
14
|
+
import { openStore } from "../lib/secrets.mjs";
|
|
19
15
|
|
|
20
16
|
const H = homedir();
|
|
21
17
|
const read = (p, fb) => { try { return JSON.parse(readFileSync(p, "utf8")); } catch { return fb; } };
|
|
22
18
|
|
|
23
19
|
// ---- crew roster: BUILT-IN seats + ANY opencode provider the user has brought (BYOM) ----
|
|
24
|
-
// Each seat: the CLI binary
|
|
25
|
-
//
|
|
26
|
-
//
|
|
27
|
-
//
|
|
28
|
-
// GEMINI is deliberately absent: Google retired the free CLI seat (2026-06-18) → `gemini --yolo`
|
|
29
|
-
// crashes exit 1. Its replacement is GLM via opencode.
|
|
30
|
-
//
|
|
31
|
-
// The opencode-driven seats are the BYOM substrate: opencode is a UNIVERSAL adapter, so any
|
|
32
|
-
// provider the user configures in opencode (or declares in their profile) becomes a crew seat
|
|
33
|
-
// with ZERO code change here — `buildRoster()` discovers them at runtime. The built-ins below are
|
|
34
|
-
// just the curated defaults + the two opencode seats with non-obvious mappings (glm: profile key
|
|
35
|
-
// `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.
|
|
36
23
|
export const BUILTIN_ROSTER = {
|
|
37
24
|
codex: { cli: "codex", launch: "codex", session: "codex", provider: "codex" },
|
|
38
25
|
kimi: { cli: "kimi", launch: "kimi", session: "kimi", provider: "kimi" },
|
|
@@ -45,11 +32,10 @@ export const BUILTIN_ROSTER = {
|
|
|
45
32
|
const BUILTIN_OC = new Set(Object.values(BUILTIN_ROSTER).filter(s => s.providerOc).map(s => s.providerOc));
|
|
46
33
|
const NEVER_DISCOVER = new Set(["claude", "codex", "kimi", "gemini", "zai", "opencode"]);
|
|
47
34
|
|
|
48
|
-
// Discover opencode providers the user
|
|
49
|
-
//
|
|
50
|
-
//
|
|
51
|
-
//
|
|
52
|
-
// 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.
|
|
53
39
|
export function discoverSeats(profile, ocConfig) {
|
|
54
40
|
const out = {};
|
|
55
41
|
const provKeys = new Set([...Object.keys(ocConfig?.provider || {}), ...Object.keys(profile?.providers || {})]);
|
|
@@ -81,7 +67,7 @@ export function loadWorld() {
|
|
|
81
67
|
const opencodeKey = (prov) => !!ocConfig?.provider?.[prov]?.options?.apiKey;
|
|
82
68
|
// a key the user already has for Scrooge counts too — the opencode runner sources these .env
|
|
83
69
|
// files, so e.g. OPENROUTER_API_KEY in ~/.token-scrooge/.env lights up the seat with no extra setup.
|
|
84
|
-
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")]
|
|
85
71
|
.some(f => { try { return readFileSync(f, "utf8").includes(k); } catch { return false; } });
|
|
86
72
|
// a seat is available only if its CLI exists AND (for opencode-driven seats) the provider is
|
|
87
73
|
// actually set up — a present binary with a dead/missing seat must NOT be recommended.
|
|
@@ -119,7 +105,7 @@ const FORECAST = { easy: 0.3e6, medium: 1.5e6, hard: 6e6 }; // tokens
|
|
|
119
105
|
const CREW_PREF = { hard: ["codex", "glm", "kimi", "deepseek", "openrouter"], medium: ["kimi", "glm", "codex", "deepseek", "openrouter"], easy: ["deepseek", "kimi", "glm", "codex", "openrouter"] };
|
|
120
106
|
|
|
121
107
|
export function advise(input, world = loadWorld()) {
|
|
122
|
-
const { profile, registry, caps, agents, scrooge, roster = BUILTIN_ROSTER } = world;
|
|
108
|
+
const { profile, registry, caps, agents, scrooge, roster = BUILTIN_ROSTER, record } = world;
|
|
123
109
|
// brought (discovered) opencode providers extend the preference list — appended LAST in every
|
|
124
110
|
// tier (unknown strength a priori, like openrouter), so they fill once the curated seats are
|
|
125
111
|
// taken, and are the only option for a user who brought nothing but a custom provider.
|
|
@@ -148,17 +134,39 @@ export function advise(input, world = loadWorld()) {
|
|
|
148
134
|
|
|
149
135
|
// ---- routing per package ----
|
|
150
136
|
const used = {};
|
|
137
|
+
// #7762: seats benched by the record ride along so the recommendation says WHO was benched,
|
|
138
|
+
// at which difficulty, and what their producing nothing already cost (tokens the crew burned).
|
|
139
|
+
const seatFeedback = [];
|
|
151
140
|
const routing = pkgs.map(p => {
|
|
152
141
|
if ((mode === "hybrid" || mode === "scrooge") && p.difficulty === "easy" && scrooge) {
|
|
153
142
|
const m = scroogeModelFor(registry, caps, p.kind, p.difficulty);
|
|
154
143
|
const tok = FORECAST.easy;
|
|
155
144
|
const cost = m ? +(tok * 0.6 * m.cost_in / 1e6 + tok * 0.4 * m.cost_out / 1e6).toFixed(3) : null;
|
|
156
|
-
|
|
145
|
+
// #7777: scores pick WHICH model; the catalog says HOW to call it — attach the per-difficulty
|
|
146
|
+
// request parameters so the executor does not have to look them up again.
|
|
147
|
+
const catEntry = catalogLookup(m?.model);
|
|
148
|
+
const catParams = catEntry.found ? effortParams(m.model, p.difficulty, "openai-chat") : null;
|
|
149
|
+
const effort = catEntry.found
|
|
150
|
+
? { found: true, difficulty: p.difficulty, api: "openai-chat", params: catParams || {} }
|
|
151
|
+
: { found: false, difficulty: p.difficulty, params: null, status: UNCATALOGUED_STATUS };
|
|
152
|
+
return { ...p, executor: "scrooge", model: m?.model, pool: "api", est_cost_usd: cost, effort,
|
|
157
153
|
reason: `easy + stateless → cheapest capable model (${m?.model}); not worth a crew seat` };
|
|
158
154
|
}
|
|
159
155
|
if (p.owner === "self") return { ...p, executor: "orchestrator", pool: tierOf(profile, "claude"), reason: "architect-owned (foundation/integration doctrine) — the orchestrator keeps the shared contract in its own hands" };
|
|
160
156
|
if (mode === "solo") return { ...p, executor: "orchestrator", pool: tierOf(profile, "claude"), reason: "small enough to do inline" };
|
|
161
|
-
const
|
|
157
|
+
const prefAll = [...CREW_PREF[p.difficulty], ...broughtPref].filter(a => agents.includes(a));
|
|
158
|
+
// #7762 feedback loop: a seat whose last 3 cards AT THIS DIFFICULTY in THIS project were
|
|
159
|
+
// empty/bounced is benched at that difficulty — the redone work counts against it, so it
|
|
160
|
+
// stops being "the cheap option". A bench never empties the pool (better a struck seat
|
|
161
|
+
// than no seat), and the record is per project — never global.
|
|
162
|
+
const struck = prefAll.map(a => [a, benchedAt(record, a, p.difficulty)]).filter(([, b]) => b);
|
|
163
|
+
const eligible = prefAll.filter(a => !benchedAt(record, a, p.difficulty));
|
|
164
|
+
const pref = eligible.length ? eligible : prefAll;
|
|
165
|
+
for (const [s, b] of struck) {
|
|
166
|
+
const wasted = b.wastedTokens ? ` ≈${(b.wastedTokens / 1e6).toFixed(1)}M tok burned` : "";
|
|
167
|
+
seatFeedback.push({ seat: s, difficulty: p.difficulty, streak: b.streak, cardIds: b.cardIds, wastedTokens: b.wastedTokens,
|
|
168
|
+
note: `${s} benched at ${p.difficulty}: last ${b.streak.length} cards ${b.streak.join("/")} (#${b.cardIds.join(" #")})${wasted}` });
|
|
169
|
+
}
|
|
162
170
|
const agent = pref.sort((a, b) => (used[a] || 0) - (used[b] || 0))[0] || agents[0] || "deepseek";
|
|
163
171
|
used[agent] = (used[agent] || 0) + 1;
|
|
164
172
|
const pool = tierOf(profile, roster[agent]?.provider || agent);
|
|
@@ -176,6 +184,7 @@ export function advise(input, world = loadWorld()) {
|
|
|
176
184
|
// has scored it (AA scores + price proxy + per-difficulty cost weighting → hard escalates to a
|
|
177
185
|
// strong model, easy stays cheap). If it hasn't been run, routing falls back to cost-only.
|
|
178
186
|
if (agent === "openrouter" && p.difficulty === "hard") why_r += ` — OpenRouter ranks capability×cost; run \`scrooge-capabilities\` to keep the catalog scored (or pin openrouter:openrouter/<vendor>/<model>)`;
|
|
187
|
+
if (struck.length && !struck.some(([s]) => s === agent)) why_r += ` — ${struck.map(([s]) => s).join("/")} benched at ${p.difficulty} here (last 3 cards empty/bounced; seat_feedback)`;
|
|
179
188
|
return { ...p, executor: agent, pool, est_cost_usd: est, reason: why_r };
|
|
180
189
|
});
|
|
181
190
|
// crew-size rationale: seats are EMERGENT from the work, and we say so
|
|
@@ -188,11 +197,14 @@ export function advise(input, world = loadWorld()) {
|
|
|
188
197
|
|
|
189
198
|
const apiCost = +(routing.reduce((s, r) => s + (r.est_cost_usd || 0), 0)).toFixed(2);
|
|
190
199
|
const pools = [...new Set(routing.map(r => `${r.executor}:${r.pool}`))];
|
|
200
|
+
// One bench line per seat+difficulty, however many packages tripped it.
|
|
201
|
+
const feedback = [...new Map(seatFeedback.map(f => [`${f.seat}@${f.difficulty}`, f])).values()];
|
|
191
202
|
const summary =
|
|
192
203
|
`Recommendation: ${mode.toUpperCase()}. ${why.join("; ")}. ` +
|
|
193
204
|
(mode === "crew" || mode === "hybrid"
|
|
194
205
|
? `Routing: ${routing.map(r => `${r.title}→${r.executor}${r.model ? `(${r.model})` : ""}`).join(", ")}. ` +
|
|
195
|
-
`Estimated real-money cost ≈ $${apiCost} (everything on a subscription pool is $0 marginal — quota pooling across ${pools.length} pools).`
|
|
206
|
+
`Estimated real-money cost ≈ $${apiCost} (everything on a subscription pool is $0 marginal — quota pooling across ${pools.length} pools).` +
|
|
207
|
+
(feedback.length ? ` Seat record (this project): ${feedback.map(f => f.note).join("; ")}.` : "")
|
|
196
208
|
: "");
|
|
197
209
|
const table = ["| package | diff | executor (model) | pool | est $ | reason |", "|---|---|---|---|---|---|",
|
|
198
210
|
...routing.map(r => `| ${r.title} | ${r.difficulty} | ${r.executor}${r.model ? ` (${r.model})` : ""} | ${r.pool} | ${r.est_cost_usd ?? "—"} | ${r.reason} |`)].join("\n");
|
|
@@ -221,7 +233,12 @@ export function advise(input, world = loadWorld()) {
|
|
|
221
233
|
? routing.map((x, j) => j + 1).filter(j => j !== i + 1)
|
|
222
234
|
: (r.executor !== "orchestrator" ? foundationIdx.filter(f => f !== i + 1) : []) };
|
|
223
235
|
});
|
|
224
|
-
|
|
236
|
+
// #7777: crew-bound packages get their catalog effort attached at SPAWN, when the live model is
|
|
237
|
+
// known (bin/crew/models.mjs resolveSpec → CREW_EFFORT); the advisor only records that it is deferred.
|
|
238
|
+
const catalogMeta = (() => { const c = loadCatalog(); return { version: c.version, models: Object.keys(c.models).length, source: "configs/model-catalog.json" }; })();
|
|
239
|
+
const recommendation = { mode, why, crew, routing, routing_table_md: table, card_args: cards, est_api_cost_usd: apiCost, quota_pools: pools, summary, orchestrator_tier: orchTier, agents_available: agents, catalog: catalogMeta };
|
|
240
|
+
if (feedback.length) recommendation.seat_feedback = feedback;
|
|
241
|
+
return recommendation;
|
|
225
242
|
}
|
|
226
243
|
|
|
227
244
|
// ---- CLI ----
|
|
@@ -238,6 +255,10 @@ if (import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
|
238
255
|
const stdin = readFileSync(0, "utf8").trim();
|
|
239
256
|
input = stdin ? JSON.parse(stdin) : { packages: [] };
|
|
240
257
|
}
|
|
241
|
-
const
|
|
258
|
+
const world = loadWorld();
|
|
259
|
+
// #7762: fold this project's seat record into the routing (fail-open — no hub, no record,
|
|
260
|
+
// and the advice is exactly what it was before the feedback loop existed).
|
|
261
|
+
try { world.record = await loadSeatRecord({ project: resolveProject(process.cwd()) }); } catch {}
|
|
262
|
+
const out = advise(input, world);
|
|
242
263
|
console.log(JSON.stringify(out, null, 2));
|
|
243
264
|
}
|
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;
|
|
@@ -99,6 +128,7 @@ switch (cmd) {
|
|
|
99
128
|
case "state": run("bin/state.mjs"); break;
|
|
100
129
|
case "seats": case "seat": run("bin/seats.mjs"); break;
|
|
101
130
|
case "seat-why": case "why": run("bin/seat-why.mjs"); break;
|
|
131
|
+
case "seat-record": run("bin/seat-record.mjs"); break;
|
|
102
132
|
case "orchestrate": run("bin/orchestrate.mjs"); break;
|
|
103
133
|
case "app": run("bin/app.mjs"); break;
|
|
104
134
|
case "patrol": run("bin/patrol.mjs"); break;
|
|
@@ -201,6 +231,7 @@ switch (cmd) {
|
|
|
201
231
|
trantor connect (re)wire every installed AI CLI to the bus
|
|
202
232
|
trantor profile declare your plans: trantor profile set claude=max codex=plus deepseek=api
|
|
203
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]
|
|
204
235
|
trantor models browse live models behind each seat + the router's pick per difficulty
|
|
205
236
|
trantor up … spawn a crew here: trantor up codex kimi deepseek:deepseek glm:zai-coding-plan
|
|
206
237
|
trantor open host THIS session as the project's orchestrator pane (trantor down spares it)
|
|
@@ -226,11 +257,13 @@ switch (cmd) {
|
|
|
226
257
|
trantor recost recompute sub-agent notional cost from on-disk transcripts + reseed the board (repair after upgrade) — [--dry-run]
|
|
227
258
|
trantor handoff finish this session NOW: write a handoff, open a fresh session that takes over, and close this one (manual baton)
|
|
228
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
|
|
229
261
|
trantor hub run the hub in the foreground (setup installs it as a service instead)
|
|
230
262
|
…or manage per-project hub pins: hub list · hub set <project> <url> · hub unset <project>
|
|
231
263
|
seats: which project lives in which directory — seats · seats add · seats up · seats login install
|
|
232
264
|
trantor state a seat's working memory: show <seat> <card> [--json] · validate · reset --force · gc [--apply]
|
|
233
265
|
trantor seat-why WHY a seat is down (err file, logs, pids): seat-why <agent> [--json] — quota, auth, crash, or just no pane
|
|
266
|
+
trantor seat-record the per-project seat record the advisor benches from (✓/∅/↩ per difficulty) — [--project p] [--reset <seat>] [--json]
|
|
234
267
|
trantor watch live bus feed in the terminal
|
|
235
268
|
trantor inbox THIS session's unread bus messages, signed (works under enforce) — [--all] [--consume] [--json]
|
|
236
269
|
trantor policy the autonomy ladder: show | set <project> <1-4> | link <a> <b> --reason "<why>"
|
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
|
|
@@ -110,15 +111,28 @@ if (has("gemini")) {
|
|
|
110
111
|
}), p);
|
|
111
112
|
}
|
|
112
113
|
|
|
113
|
-
// ---- Kimi CLI ---- (same refresh: the stale entry that caused #7893 was {RELAY_AGENT: kimi} only)
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
114
|
+
// ---- Kimi CLI + kimi-code ---- (same refresh: the stale entry that caused #7893 was {RELAY_AGENT: kimi} only)
|
|
115
|
+
// kimi-code is a separate install reading ~/.kimi-code/mcp.json — wiring only the old file left the
|
|
116
|
+
// running kimi seat on a hard-coded stale hub (#7938). Same shape and stamp; the dir is detected by
|
|
117
|
+
// config.toml presence — existsSync only, the config itself is never read.
|
|
118
|
+
const kimiRelay = (cli, p) => report(cli, patchJson(p, d => {
|
|
119
|
+
d.mcpServers ||= {};
|
|
120
|
+
d.mcpServers.relay ||= { command: "node", args: [MCP], env: {} };
|
|
121
|
+
d.mcpServers.relay.env = { ...d.mcpServers.relay.env, ...relayEnv("kimi") };
|
|
122
|
+
if (HAS_GRAFT) d.mcpServers.graft ||= { command: GRAFT, args: ["mcp"] };
|
|
123
|
+
}), p);
|
|
124
|
+
const kimiPaths = [join(homedir(), ".kimi", "mcp.json"), join(homedir(), ".kimi-code", "mcp.json")];
|
|
125
|
+
const kimiWritten = new Set();
|
|
126
|
+
if (has("kimi")) { kimiRelay("kimi", kimiPaths[0]); kimiWritten.add(kimiPaths[0]); }
|
|
127
|
+
if (existsSync(join(homedir(), ".kimi-code", "config.toml"))) { kimiRelay("kimi-code", kimiPaths[1]); kimiWritten.add(kimiPaths[1]); }
|
|
128
|
+
// A kimi-family config this run did NOT write still names a hub of its own; if it disagrees with
|
|
129
|
+
// the pin, say so — one CLI of the pair would keep registering on a different bus (#7938).
|
|
130
|
+
for (const p of kimiPaths) {
|
|
131
|
+
if (kimiWritten.has(p) || !existsSync(p)) continue;
|
|
132
|
+
try {
|
|
133
|
+
const u = JSON.parse(readFileSync(p, "utf8"))?.mcpServers?.relay?.env?.RELAY_URL;
|
|
134
|
+
if (u && u !== URL_) report("kimi", `WARN: ${p} still points at ${u} — re-run connect or refresh it by hand`);
|
|
135
|
+
} catch {}
|
|
122
136
|
}
|
|
123
137
|
|
|
124
138
|
// ---- OpenCode ----
|
|
@@ -247,6 +261,17 @@ ${HAS_GRAFT ? ` - id: trantor-graft
|
|
|
247
261
|
}
|
|
248
262
|
|
|
249
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
|
+
}
|
|
250
275
|
console.log(`trantor connect${DRY ? " (dry run)" : ""} — project: ${PROJECT_AT_CONNECT}, hub: ${URL_}`);
|
|
251
276
|
for (const r of out) console.log(` ${r.cli.padEnd(9)} ${r.status}${r.detail ? ` (${r.detail})` : ""}`);
|
|
252
277
|
if (!found) console.log(" no supported CLIs found on PATH (claude, codex, gemini, kimi, opencode, dsh)");
|
package/bin/crew/cmux.mjs
CHANGED
|
@@ -109,7 +109,7 @@ export function spawnCmux(ctx, specs, resolve, prune) {
|
|
|
109
109
|
for (let index = 0; index < specs.length; index += 1) {
|
|
110
110
|
const seat = resolve(specs[index]);
|
|
111
111
|
if (!seat) continue;
|
|
112
|
-
const launcher = seatLauncher(ctx, seat.agent, runnerCommand(ctx, seat.agent, seat.model));
|
|
112
|
+
const launcher = seatLauncher(ctx, seat.agent, runnerCommand(ctx, seat.agent, seat.model, seat.effort));
|
|
113
113
|
const old = reuse ? readRows(ctx).filter(row => row.project === ctx.project && row.kind === "cmux" && row.agent === seat.agent).at(-1)?.handle || "" : "";
|
|
114
114
|
let pane = "";
|
|
115
115
|
if (reuse) {
|
|
@@ -149,7 +149,7 @@ function spawnAppleScript(ctx, specs, resolve) {
|
|
|
149
149
|
for (let index = 0; index < specs.length; index += 1) {
|
|
150
150
|
const seat = resolve(specs[index]);
|
|
151
151
|
if (!seat) continue;
|
|
152
|
-
const launcher = seatLauncher(ctx, seat.agent, runnerCommand(ctx, seat.agent, seat.model));
|
|
152
|
+
const launcher = seatLauncher(ctx, seat.agent, runnerCommand(ctx, seat.agent, seat.model, seat.effort));
|
|
153
153
|
const old = reuse ? readRows(ctx).filter(row => row.project === ctx.project && row.kind === "cmux" && row.agent === seat.agent).at(-1)?.handle || "" : "";
|
|
154
154
|
let pane;
|
|
155
155
|
if (!tab && index === 0) {
|
package/bin/crew/core.mjs
CHANGED
|
@@ -141,12 +141,15 @@ export function gridColumns(size) {
|
|
|
141
141
|
// file that was never written and a runner still on the transcript path.
|
|
142
142
|
const FORWARDED_ENV = ["TRANTOR_STATE", "TRANTOR_STATE_ASSEMBLE", "TRANTOR_STATE_HANDOFF", "TRANTOR_STATE_GATE"];
|
|
143
143
|
|
|
144
|
-
export function runnerCommand(ctx, agent, model = "") {
|
|
144
|
+
export function runnerCommand(ctx, agent, model = "", effort = null) {
|
|
145
145
|
const forwarded = FORWARDED_ENV
|
|
146
146
|
.filter((name) => process.env[name])
|
|
147
147
|
.map((name) => `${name}=${shellQuote(process.env[name])} `)
|
|
148
148
|
.join("");
|
|
149
|
-
|
|
149
|
+
// #7777: the launcher resolved the catalog's per-difficulty effort for this seat; the runner
|
|
150
|
+
// reads CREW_EFFORT, applies what the CLI accepts and logs the one-line summary.
|
|
151
|
+
const effortEnv = effort ? ` CREW_EFFORT=${shellQuote(JSON.stringify(effort))}` : "";
|
|
152
|
+
return `cd ${shellQuote(ctx.dir)} && ${forwarded}CREW_MODEL=${shellQuote(model)}${effortEnv} RELAY_PROJECT=${shellQuote(ctx.project)} RELAY_URL=${shellQuote(ctx.hub)} node ${shellQuote(join(ROOT, "bin/crew-runner.mjs"))} ${shellQuote(agent)} ${shellQuote(ctx.dir)}`;
|
|
150
153
|
}
|
|
151
154
|
|
|
152
155
|
export function listPids(pattern) {
|
package/bin/crew/herdr.mjs
CHANGED
|
@@ -165,7 +165,7 @@ function replacementPane(ctx, workspace, spec, hostPane, resolve) {
|
|
|
165
165
|
pane = `%DRYT${spec.index}`;
|
|
166
166
|
} else {
|
|
167
167
|
pane = splitPane(ctx, hostPane, "right", ctx.dir);
|
|
168
|
-
runSeat(ctx, pane, seat.agent, runnerCommand(ctx, seat.agent, seat.model));
|
|
168
|
+
runSeat(ctx, pane, seat.agent, runnerCommand(ctx, seat.agent, seat.model, seat.effort));
|
|
169
169
|
}
|
|
170
170
|
if (old) {
|
|
171
171
|
closePane(ctx, old);
|
|
@@ -202,7 +202,7 @@ export function spawnHerdr(ctx, specs, resolve, prune) {
|
|
|
202
202
|
function freshPane(ctx, workspace, spec, panes, columns, resolve) {
|
|
203
203
|
const seat = resolve(spec.value);
|
|
204
204
|
if (!seat) return null;
|
|
205
|
-
const command = runnerCommand(ctx, seat.agent, seat.model);
|
|
205
|
+
const command = runnerCommand(ctx, seat.agent, seat.model, seat.effort);
|
|
206
206
|
let pane = "";
|
|
207
207
|
if (spec.index === 0) {
|
|
208
208
|
if (ctx.dry) {
|