trantor 0.18.59 → 0.18.61
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 +8 -0
- package/bin/advise.mjs +44 -7
- package/bin/cli.mjs +2 -0
- package/bin/connect.mjs +81 -61
- 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/preflight.mjs +70 -0
- package/bin/crew/tmux.mjs +2 -2
- package/bin/crew-payload.mjs +19 -10
- package/bin/crew-runner.mjs +128 -110
- package/bin/crew.mjs +8 -5
- package/bin/seat-record.mjs +41 -0
- package/bin/turn-watchdog.mjs +38 -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 +12 -1
- package/lib/classify-failure.mjs +19 -0
- package/lib/model-catalog.mjs +123 -0
- package/lib/project.mjs +34 -9
- package/lib/seat-record.mjs +150 -0
- package/lib/seat-worktree.mjs +203 -0
- package/mcp.mjs +25 -12
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "trantor",
|
|
3
|
-
"version": "0.18.
|
|
3
|
+
"version": "0.18.61",
|
|
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
|
@@ -200,6 +200,14 @@ project takes over with a full window (and a PreCompact hook does this automatic
|
|
|
200
200
|
**best live model** for the work at spawn (capability × cost), enumerated from the provider
|
|
201
201
|
itself — never a guessed endpoint. **Serialized and then verified on the bus** — the launcher
|
|
202
202
|
ends with "crew verified" or names the no-shows loudly. The orchestrator never gets a green lie.
|
|
203
|
+
**Seats build in their own git worktrees** (`~/.agent-bus/worktrees/<project>/<seat>`), and a
|
|
204
|
+
fresh checkout lacks gitignored files and cannot resolve relative sibling packages. Declare what a
|
|
205
|
+
worktree needs in `.trantor/worktree.json`:
|
|
206
|
+
`{"link": ["../sibling-repo"], "provision": [{"path": "ios/Config.swift", "mode": "link|stub|operator"}], "preflight": "npm test --silent"}`.
|
|
207
|
+
`trantor up` links the siblings beside the worktree, links or stubs the declared files (a real
|
|
208
|
+
credential is never copied; `operator` entries are named for you), runs the preflight once in the
|
|
209
|
+
first fresh worktree (5-minute cap) and prints `preflight ok` or the last 20 lines — broadcast on
|
|
210
|
+
the bus too, so the orchestrator sees it before writing contracts. No declaration, no preflight.
|
|
203
211
|
3. **Work flows over the bus.** Contracts arrive as messages; each agent owns its own files;
|
|
204
212
|
coordination happens in <280-char messages you can read on the dashboard. Crew members
|
|
205
213
|
live under a **runner**: the CLI works one turn and exits, the runner long-polls the bus
|
package/bin/advise.mjs
CHANGED
|
@@ -15,7 +15,9 @@ import { join } from "node:path";
|
|
|
15
15
|
import { homedir } from "node:os";
|
|
16
16
|
import { execSync } from "node:child_process";
|
|
17
17
|
import { pathToFileURL } from "node:url";
|
|
18
|
-
import { busDir, readConfig } from "../lib/project.mjs";
|
|
18
|
+
import { busDir, readConfig, resolveProject } from "../lib/project.mjs";
|
|
19
|
+
import { loadCatalog, lookup as catalogLookup, effortParams, UNCATALOGUED_STATUS } from "../lib/model-catalog.mjs";
|
|
20
|
+
import { benchedAt, loadSeatRecord } from "../lib/seat-record.mjs";
|
|
19
21
|
|
|
20
22
|
const H = homedir();
|
|
21
23
|
const read = (p, fb) => { try { return JSON.parse(readFileSync(p, "utf8")); } catch { return fb; } };
|
|
@@ -119,7 +121,7 @@ const FORECAST = { easy: 0.3e6, medium: 1.5e6, hard: 6e6 }; // tokens
|
|
|
119
121
|
const CREW_PREF = { hard: ["codex", "glm", "kimi", "deepseek", "openrouter"], medium: ["kimi", "glm", "codex", "deepseek", "openrouter"], easy: ["deepseek", "kimi", "glm", "codex", "openrouter"] };
|
|
120
122
|
|
|
121
123
|
export function advise(input, world = loadWorld()) {
|
|
122
|
-
const { profile, registry, caps, agents, scrooge, roster = BUILTIN_ROSTER } = world;
|
|
124
|
+
const { profile, registry, caps, agents, scrooge, roster = BUILTIN_ROSTER, record } = world;
|
|
123
125
|
// brought (discovered) opencode providers extend the preference list — appended LAST in every
|
|
124
126
|
// tier (unknown strength a priori, like openrouter), so they fill once the curated seats are
|
|
125
127
|
// taken, and are the only option for a user who brought nothing but a custom provider.
|
|
@@ -148,17 +150,39 @@ export function advise(input, world = loadWorld()) {
|
|
|
148
150
|
|
|
149
151
|
// ---- routing per package ----
|
|
150
152
|
const used = {};
|
|
153
|
+
// #7762: seats benched by the record ride along so the recommendation says WHO was benched,
|
|
154
|
+
// at which difficulty, and what their producing nothing already cost (tokens the crew burned).
|
|
155
|
+
const seatFeedback = [];
|
|
151
156
|
const routing = pkgs.map(p => {
|
|
152
157
|
if ((mode === "hybrid" || mode === "scrooge") && p.difficulty === "easy" && scrooge) {
|
|
153
158
|
const m = scroogeModelFor(registry, caps, p.kind, p.difficulty);
|
|
154
159
|
const tok = FORECAST.easy;
|
|
155
160
|
const cost = m ? +(tok * 0.6 * m.cost_in / 1e6 + tok * 0.4 * m.cost_out / 1e6).toFixed(3) : null;
|
|
156
|
-
|
|
161
|
+
// #7777: scores pick WHICH model; the catalog says HOW to call it — attach the per-difficulty
|
|
162
|
+
// request parameters so the executor does not have to look them up again.
|
|
163
|
+
const catEntry = catalogLookup(m?.model);
|
|
164
|
+
const catParams = catEntry.found ? effortParams(m.model, p.difficulty, "openai-chat") : null;
|
|
165
|
+
const effort = catEntry.found
|
|
166
|
+
? { found: true, difficulty: p.difficulty, api: "openai-chat", params: catParams || {} }
|
|
167
|
+
: { found: false, difficulty: p.difficulty, params: null, status: UNCATALOGUED_STATUS };
|
|
168
|
+
return { ...p, executor: "scrooge", model: m?.model, pool: "api", est_cost_usd: cost, effort,
|
|
157
169
|
reason: `easy + stateless → cheapest capable model (${m?.model}); not worth a crew seat` };
|
|
158
170
|
}
|
|
159
171
|
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
172
|
if (mode === "solo") return { ...p, executor: "orchestrator", pool: tierOf(profile, "claude"), reason: "small enough to do inline" };
|
|
161
|
-
const
|
|
173
|
+
const prefAll = [...CREW_PREF[p.difficulty], ...broughtPref].filter(a => agents.includes(a));
|
|
174
|
+
// #7762 feedback loop: a seat whose last 3 cards AT THIS DIFFICULTY in THIS project were
|
|
175
|
+
// empty/bounced is benched at that difficulty — the redone work counts against it, so it
|
|
176
|
+
// stops being "the cheap option". A bench never empties the pool (better a struck seat
|
|
177
|
+
// than no seat), and the record is per project — never global.
|
|
178
|
+
const struck = prefAll.map(a => [a, benchedAt(record, a, p.difficulty)]).filter(([, b]) => b);
|
|
179
|
+
const eligible = prefAll.filter(a => !benchedAt(record, a, p.difficulty));
|
|
180
|
+
const pref = eligible.length ? eligible : prefAll;
|
|
181
|
+
for (const [s, b] of struck) {
|
|
182
|
+
const wasted = b.wastedTokens ? ` ≈${(b.wastedTokens / 1e6).toFixed(1)}M tok burned` : "";
|
|
183
|
+
seatFeedback.push({ seat: s, difficulty: p.difficulty, streak: b.streak, cardIds: b.cardIds, wastedTokens: b.wastedTokens,
|
|
184
|
+
note: `${s} benched at ${p.difficulty}: last ${b.streak.length} cards ${b.streak.join("/")} (#${b.cardIds.join(" #")})${wasted}` });
|
|
185
|
+
}
|
|
162
186
|
const agent = pref.sort((a, b) => (used[a] || 0) - (used[b] || 0))[0] || agents[0] || "deepseek";
|
|
163
187
|
used[agent] = (used[agent] || 0) + 1;
|
|
164
188
|
const pool = tierOf(profile, roster[agent]?.provider || agent);
|
|
@@ -176,6 +200,7 @@ export function advise(input, world = loadWorld()) {
|
|
|
176
200
|
// has scored it (AA scores + price proxy + per-difficulty cost weighting → hard escalates to a
|
|
177
201
|
// strong model, easy stays cheap). If it hasn't been run, routing falls back to cost-only.
|
|
178
202
|
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>)`;
|
|
203
|
+
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
204
|
return { ...p, executor: agent, pool, est_cost_usd: est, reason: why_r };
|
|
180
205
|
});
|
|
181
206
|
// crew-size rationale: seats are EMERGENT from the work, and we say so
|
|
@@ -188,11 +213,14 @@ export function advise(input, world = loadWorld()) {
|
|
|
188
213
|
|
|
189
214
|
const apiCost = +(routing.reduce((s, r) => s + (r.est_cost_usd || 0), 0)).toFixed(2);
|
|
190
215
|
const pools = [...new Set(routing.map(r => `${r.executor}:${r.pool}`))];
|
|
216
|
+
// One bench line per seat+difficulty, however many packages tripped it.
|
|
217
|
+
const feedback = [...new Map(seatFeedback.map(f => [`${f.seat}@${f.difficulty}`, f])).values()];
|
|
191
218
|
const summary =
|
|
192
219
|
`Recommendation: ${mode.toUpperCase()}. ${why.join("; ")}. ` +
|
|
193
220
|
(mode === "crew" || mode === "hybrid"
|
|
194
221
|
? `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).`
|
|
222
|
+
`Estimated real-money cost ≈ $${apiCost} (everything on a subscription pool is $0 marginal — quota pooling across ${pools.length} pools).` +
|
|
223
|
+
(feedback.length ? ` Seat record (this project): ${feedback.map(f => f.note).join("; ")}.` : "")
|
|
196
224
|
: "");
|
|
197
225
|
const table = ["| package | diff | executor (model) | pool | est $ | reason |", "|---|---|---|---|---|---|",
|
|
198
226
|
...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 +249,12 @@ export function advise(input, world = loadWorld()) {
|
|
|
221
249
|
? routing.map((x, j) => j + 1).filter(j => j !== i + 1)
|
|
222
250
|
: (r.executor !== "orchestrator" ? foundationIdx.filter(f => f !== i + 1) : []) };
|
|
223
251
|
});
|
|
224
|
-
|
|
252
|
+
// #7777: crew-bound packages get their catalog effort attached at SPAWN, when the live model is
|
|
253
|
+
// known (bin/crew/models.mjs resolveSpec → CREW_EFFORT); the advisor only records that it is deferred.
|
|
254
|
+
const catalogMeta = (() => { const c = loadCatalog(); return { version: c.version, models: Object.keys(c.models).length, source: "configs/model-catalog.json" }; })();
|
|
255
|
+
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 };
|
|
256
|
+
if (feedback.length) recommendation.seat_feedback = feedback;
|
|
257
|
+
return recommendation;
|
|
225
258
|
}
|
|
226
259
|
|
|
227
260
|
// ---- CLI ----
|
|
@@ -238,6 +271,10 @@ if (import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
|
238
271
|
const stdin = readFileSync(0, "utf8").trim();
|
|
239
272
|
input = stdin ? JSON.parse(stdin) : { packages: [] };
|
|
240
273
|
}
|
|
241
|
-
const
|
|
274
|
+
const world = loadWorld();
|
|
275
|
+
// #7762: fold this project's seat record into the routing (fail-open — no hub, no record,
|
|
276
|
+
// and the advice is exactly what it was before the feedback loop existed).
|
|
277
|
+
try { world.record = await loadSeatRecord({ project: resolveProject(process.cwd()) }); } catch {}
|
|
278
|
+
const out = advise(input, world);
|
|
242
279
|
console.log(JSON.stringify(out, null, 2));
|
|
243
280
|
}
|
package/bin/cli.mjs
CHANGED
|
@@ -99,6 +99,7 @@ switch (cmd) {
|
|
|
99
99
|
case "state": run("bin/state.mjs"); break;
|
|
100
100
|
case "seats": case "seat": run("bin/seats.mjs"); break;
|
|
101
101
|
case "seat-why": case "why": run("bin/seat-why.mjs"); break;
|
|
102
|
+
case "seat-record": run("bin/seat-record.mjs"); break;
|
|
102
103
|
case "orchestrate": run("bin/orchestrate.mjs"); break;
|
|
103
104
|
case "app": run("bin/app.mjs"); break;
|
|
104
105
|
case "patrol": run("bin/patrol.mjs"); break;
|
|
@@ -231,6 +232,7 @@ switch (cmd) {
|
|
|
231
232
|
seats: which project lives in which directory — seats · seats add · seats up · seats login install
|
|
232
233
|
trantor state a seat's working memory: show <seat> <card> [--json] · validate · reset --force · gc [--apply]
|
|
233
234
|
trantor seat-why WHY a seat is down (err file, logs, pids): seat-why <agent> [--json] — quota, auth, crash, or just no pane
|
|
235
|
+
trantor seat-record the per-project seat record the advisor benches from (✓/∅/↩ per difficulty) — [--project p] [--reset <seat>] [--json]
|
|
234
236
|
trantor watch live bus feed in the terminal
|
|
235
237
|
trantor inbox THIS session's unread bus messages, signed (works under enforce) — [--all] [--consume] [--json]
|
|
236
238
|
trantor policy the autonomy ladder: show | set <project> <1-4> | link <a> <b> --reason "<why>"
|
package/bin/connect.mjs
CHANGED
|
@@ -1,27 +1,25 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// trantor connect — wire every AI coding CLI on this machine to the bus, in one shot
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
// node bin/connect.mjs --dry-run # show what would change, touch nothing
|
|
6
|
-
//
|
|
7
|
-
// Each CLI keeps its own MCP config file/format; this writes the one "relay" entry into each
|
|
8
|
-
// (with a timestamped .bak backup the first time it changes a file). Claude Code is handled by
|
|
9
|
-
// the plugin (claude plugin install trantor), so it's only verified here, not patched.
|
|
2
|
+
// trantor connect — wire every AI coding CLI on this machine to the bus, in one shot (idempotent;
|
|
3
|
+
// --dry-run touches nothing). Writes the one "relay" MCP entry into each CLI's own config format,
|
|
4
|
+
// with a timestamped .bak backup on first change. Claude Code rides the plugin: verified, not patched.
|
|
10
5
|
import { readFileSync, writeFileSync, existsSync, mkdirSync, copyFileSync } from "node:fs";
|
|
11
6
|
import { join, dirname } from "node:path";
|
|
12
7
|
import { homedir } from "node:os";
|
|
13
8
|
import { execSync } from "node:child_process";
|
|
14
9
|
import { fileURLToPath } from "node:url";
|
|
10
|
+
import { resolveProject, resolveHubInfo } from "../lib/project.mjs";
|
|
15
11
|
|
|
16
12
|
const DRY = process.argv.includes("--dry-run");
|
|
17
13
|
const MCP = join(dirname(dirname(fileURLToPath(import.meta.url))), "mcp.mjs");
|
|
18
|
-
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
//
|
|
14
|
+
// Connect-time truth stamped into every relay entry env (#7893): the project this checkout resolves
|
|
15
|
+
// to and the hub THAT project resolves to. Some CLIs spawn MCP with a scrubbed env where even `git`
|
|
16
|
+
// is missing, so the stamp is the belt; the worktree path rule in lib/project.mjs stays primary.
|
|
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());
|
|
19
|
+
const URL_ = resolveHubInfo(PROJECT_AT_CONNECT).url;
|
|
20
|
+
// Graft (github.com/NanoNets/context-graph-engine): local Tree-sitter dependency graph over MCP,
|
|
21
|
+
// wired next to `relay` so a seat locates code in one call; the graph refreshes itself per query
|
|
22
|
+
// and a project with no graft/ index simply returns empty tools. `graft build` seeds an index.
|
|
25
23
|
const GRAFT = (() => { try { return execSync("command -v graft", { encoding: "utf8", shell: "/bin/sh" }).trim(); } catch { return "graft"; } })();
|
|
26
24
|
const HAS_GRAFT = GRAFT !== "graft" || (() => { try { execSync("command -v graft", { stdio: "ignore", shell: "/bin/sh" }); return true; } catch { return false; } })();
|
|
27
25
|
const has = (cmd) => { try { execSync(`command -v ${cmd}`, { stdio: "ignore", shell: "/bin/sh" }); return true; } catch { return false; } };
|
|
@@ -43,11 +41,11 @@ function patchJson(path, mutate) {
|
|
|
43
41
|
return exists ? "wired" : "wired (new config)";
|
|
44
42
|
}
|
|
45
43
|
|
|
46
|
-
//
|
|
47
|
-
//
|
|
48
|
-
//
|
|
49
|
-
//
|
|
50
|
-
const relayEnv = (agent) => ({ RELAY_AGENT: agent });
|
|
44
|
+
// Two keys, refreshed by every connect run: agent identity and the hub. The PROJECT is never
|
|
45
|
+
// stamped: these configs are global to the CLI, and a seat of that CLI in another project would
|
|
46
|
+
// inherit the wrong board (RELAY_PROJECT outranks the worktree rule in resolveProject, #7893).
|
|
47
|
+
// A user-added env key survives the refresh merge.
|
|
48
|
+
const relayEnv = (agent) => ({ RELAY_AGENT: agent, RELAY_URL: URL_ });
|
|
51
49
|
// OpenCode hosts several differently-named seats. Its global MCP environment must not stamp all
|
|
52
50
|
// of them "opencode": ambient runner identity wins, while this fallback names a normal interactive
|
|
53
51
|
// OpenCode session that has no RELAY_AGENT/RELAY_SESSION of its own.
|
|
@@ -63,15 +61,31 @@ if (has("claude")) {
|
|
|
63
61
|
report("claude", st);
|
|
64
62
|
}
|
|
65
63
|
|
|
66
|
-
// ---- Codex (TOML append
|
|
64
|
+
// ---- Codex (TOML — append a missing relay section, refresh its env when it exists) ----
|
|
65
|
+
const tomlRelayEnv = `env = { RELAY_AGENT = "codex", RELAY_URL = "${URL_}" }`;
|
|
67
66
|
if (has("codex")) {
|
|
68
67
|
const p = join(homedir(), ".codex", "config.toml");
|
|
69
|
-
|
|
70
|
-
if (cur.includes("[mcp_servers.relay]"))
|
|
71
|
-
|
|
72
|
-
const block = `\n# trantor — auto-registers each Codex session on the bus + adds relay_* tools\n# (no RELAY_URL on purpose: the per-project hub pin decides the hub)\n[mcp_servers.relay]\ncommand = "node"\nargs = ["${MCP}"]\nenv = { RELAY_AGENT = "codex" }\n`;
|
|
68
|
+
let cur = existsSync(p) ? readFileSync(p, "utf8") : "";
|
|
69
|
+
if (!cur.includes("[mcp_servers.relay]")) {
|
|
70
|
+
const block = `\n# trantor — auto-registers each Codex session on the bus + adds relay_* tools\n# (env is REFRESHED by every \`trantor connect\`: agent + connect-time hub + project)\n[mcp_servers.relay]\ncommand = "node"\nargs = ["${MCP}"]\n${tomlRelayEnv}\n`;
|
|
73
71
|
if (!DRY) { if (existsSync(p)) backup(p); else mkdirSync(dirname(p), { recursive: true }); writeFileSync(p, cur + block); }
|
|
74
72
|
report("codex", cur ? "wired" : "wired (new config)", p);
|
|
73
|
+
} else {
|
|
74
|
+
// Refresh the connect-written single-line `env = { ... }` inside the existing section; a
|
|
75
|
+
// hand-rolled multi-line env table is left untouched (user customization wins) and is named.
|
|
76
|
+
const start = cur.indexOf("[mcp_servers.relay]");
|
|
77
|
+
const next = cur.indexOf("\n[", start + 1);
|
|
78
|
+
const end = next === -1 ? cur.length : next + 1;
|
|
79
|
+
const section = cur.slice(start, end);
|
|
80
|
+
let refreshed = section, note = "already wired";
|
|
81
|
+
if (/^env\s*=\s*\{.*\}\s*$/m.test(section)) refreshed = section.replace(/^env\s*=.*$/m, tomlRelayEnv);
|
|
82
|
+
else if (!/^env\s*=/m.test(section)) refreshed = section.replace(/\n*$/, "\n") + tomlRelayEnv + "\n";
|
|
83
|
+
else note = "relay env kept (custom shape) — refresh it by hand";
|
|
84
|
+
if (refreshed !== section) {
|
|
85
|
+
if (!DRY) { backup(p); writeFileSync(p, cur.slice(0, start) + refreshed + cur.slice(end)); }
|
|
86
|
+
note = "relay env refreshed";
|
|
87
|
+
}
|
|
88
|
+
report("codex", note, p);
|
|
75
89
|
}
|
|
76
90
|
// graft alongside relay
|
|
77
91
|
if (HAS_GRAFT) {
|
|
@@ -85,27 +99,45 @@ if (has("codex")) {
|
|
|
85
99
|
}
|
|
86
100
|
}
|
|
87
101
|
|
|
88
|
-
// ---- Gemini CLI ---- (
|
|
102
|
+
// ---- Gemini CLI ---- (relay entry env is REFRESHED: ||= kept an older connect's stale env forever)
|
|
89
103
|
if (has("gemini")) {
|
|
90
104
|
const p = join(homedir(), ".gemini", "settings.json");
|
|
91
105
|
report("gemini", patchJson(p, d => {
|
|
92
106
|
d.mcpServers ||= {};
|
|
93
|
-
d.mcpServers.relay ||= { command: "node", args: [MCP], env:
|
|
107
|
+
d.mcpServers.relay ||= { command: "node", args: [MCP], env: {} };
|
|
108
|
+
d.mcpServers.relay.env = { ...d.mcpServers.relay.env, ...relayEnv("gemini") };
|
|
94
109
|
if (HAS_GRAFT) d.mcpServers.graft ||= { command: GRAFT, args: ["mcp"] };
|
|
95
110
|
}), p);
|
|
96
111
|
}
|
|
97
112
|
|
|
98
|
-
// ---- Kimi CLI ----
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
113
|
+
// ---- Kimi CLI + kimi-code ---- (same refresh: the stale entry that caused #7893 was {RELAY_AGENT: kimi} only)
|
|
114
|
+
// kimi-code is a separate install reading ~/.kimi-code/mcp.json — wiring only the old file left the
|
|
115
|
+
// running kimi seat on a hard-coded stale hub (#7938). Same shape and stamp; the dir is detected by
|
|
116
|
+
// config.toml presence — existsSync only, the config itself is never read.
|
|
117
|
+
const kimiRelay = (cli, p) => report(cli, patchJson(p, d => {
|
|
118
|
+
d.mcpServers ||= {};
|
|
119
|
+
d.mcpServers.relay ||= { command: "node", args: [MCP], env: {} };
|
|
120
|
+
d.mcpServers.relay.env = { ...d.mcpServers.relay.env, ...relayEnv("kimi") };
|
|
121
|
+
if (HAS_GRAFT) d.mcpServers.graft ||= { command: GRAFT, args: ["mcp"] };
|
|
122
|
+
}), p);
|
|
123
|
+
const kimiPaths = [join(homedir(), ".kimi", "mcp.json"), join(homedir(), ".kimi-code", "mcp.json")];
|
|
124
|
+
const kimiWritten = new Set();
|
|
125
|
+
if (has("kimi")) { kimiRelay("kimi", kimiPaths[0]); kimiWritten.add(kimiPaths[0]); }
|
|
126
|
+
if (existsSync(join(homedir(), ".kimi-code", "config.toml"))) { kimiRelay("kimi-code", kimiPaths[1]); kimiWritten.add(kimiPaths[1]); }
|
|
127
|
+
// A kimi-family config this run did NOT write still names a hub of its own; if it disagrees with
|
|
128
|
+
// the pin, say so — one CLI of the pair would keep registering on a different bus (#7938).
|
|
129
|
+
for (const p of kimiPaths) {
|
|
130
|
+
if (kimiWritten.has(p) || !existsSync(p)) continue;
|
|
131
|
+
try {
|
|
132
|
+
const u = JSON.parse(readFileSync(p, "utf8"))?.mcpServers?.relay?.env?.RELAY_URL;
|
|
133
|
+
if (u && u !== URL_) report("kimi", `WARN: ${p} still points at ${u} — re-run connect or refresh it by hand`);
|
|
134
|
+
} catch {}
|
|
106
135
|
}
|
|
107
136
|
|
|
108
137
|
// ---- OpenCode ----
|
|
138
|
+
// Deliberately NOT stamped with RELAY_URL/RELAY_PROJECT: OpenCode hosts several differently-named
|
|
139
|
+
// seats from one config, and a stamped project/hub would override every hosted seat's runner-provided
|
|
140
|
+
// env (the overlay bug the deletes below fix). Ambient runner env + RELAY_AGENT_FALLBACK stay in charge.
|
|
109
141
|
if (has("opencode")) {
|
|
110
142
|
const p = join(homedir(), ".config", "opencode", "opencode.json");
|
|
111
143
|
report("opencode", patchJson(p, d => {
|
|
@@ -122,16 +154,10 @@ if (has("opencode")) {
|
|
|
122
154
|
}), p);
|
|
123
155
|
}
|
|
124
156
|
|
|
125
|
-
// ---- DeepSeek Harness (dsh)
|
|
126
|
-
//
|
|
127
|
-
//
|
|
128
|
-
//
|
|
129
|
-
// 1. their Claude Code hooks bridge pointed at OUR hooks.json — presence, focus cards, heartbeats,
|
|
130
|
-
// file claims run inside dsh exactly as they do inside CC (verified live 2026-08-19);
|
|
131
|
-
// 2. their MCP client spawning our relay server — relay_* tools with the seat identity forwarded
|
|
132
|
-
// from the ambient RELAY_* env (crew-runner sets those per seat).
|
|
133
|
-
// The bridge's own protocol lib is declared as a dependency explicitly: the rc package forgets it
|
|
134
|
-
// (ERR_MODULE_NOT_FOUND at boot without it — reported upstream).
|
|
157
|
+
// ---- DeepSeek Harness (dsh): composes from PROFILES (~/.dsh/profiles/<name>), no single MCP config.
|
|
158
|
+
// We build a "trantor" profile mounting their Claude Code hooks bridge at OUR hooks.json plus their
|
|
159
|
+
// MCP client running our relay server with ambient RELAY_* identity; the bridge's protocol lib is
|
|
160
|
+
// declared explicitly — the rc package forgets it (ERR_MODULE_NOT_FOUND at boot).
|
|
135
161
|
if (has("dsh")) {
|
|
136
162
|
const ROOT = dirname(MCP);
|
|
137
163
|
const prof = join(homedir(), ".dsh", "profiles", "trantor");
|
|
@@ -183,12 +209,10 @@ ${HAS_GRAFT ? ` - id: trantor-graft
|
|
|
183
209
|
command: ${GRAFT}
|
|
184
210
|
args: ['mcp']
|
|
185
211
|
` : ""}`;
|
|
186
|
-
// "a profile exists" is not "a profile is current": connect grows rows over time
|
|
187
|
-
//
|
|
188
|
-
//
|
|
189
|
-
//
|
|
190
|
-
// one regenerates the patch (backed up). Presence, not diff — user edits to rows that ARE there
|
|
191
|
-
// still win, the same rule as the gemini/kimi/opencode `||=` patches above.
|
|
212
|
+
// "a profile exists" is not "a profile is current": connect grows rows over time, and an existence
|
|
213
|
+
// check short-circuits on a profile written by an older connect forever. The gate is CONTENT-based:
|
|
214
|
+
// every row id this connect writes must already be in the patch, else regenerate (backed up).
|
|
215
|
+
// Presence, not diff — user edits to rows that ARE there still win, like the JSON patches above.
|
|
192
216
|
const expectedIds = [...patch.matchAll(/- id: (\S+)/g)].map(m => m[1]);
|
|
193
217
|
const cur = existsSync(patchPath) ? readFileSync(patchPath, "utf8") : "";
|
|
194
218
|
const missing = expectedIds.filter(id => !cur.split("\n").some(l => l.trim() === `- id: ${id}`));
|
|
@@ -201,11 +225,9 @@ ${HAS_GRAFT ? ` - id: trantor-graft
|
|
|
201
225
|
if (!DRY) {
|
|
202
226
|
mkdirSync(prof, { recursive: true });
|
|
203
227
|
// The seat runs the plugin's hooks MINUS SessionStart: the crew runner already owns
|
|
204
|
-
// registration/announcement, and per-turn roster
|
|
205
|
-
//
|
|
206
|
-
//
|
|
207
|
-
// crash was the duplicated-core install below, refuted by a clean-profile repro before we
|
|
208
|
-
// reported upstream (deepseek-harness discussions #3515/#3516).
|
|
228
|
+
// registration/announcement, and per-turn roster injection is wasted spend in a one-shot
|
|
229
|
+
// session. (A dsh teardown crash was once blamed on SessionStart — FALSE: it was the
|
|
230
|
+
// duplicated-core install below, refuted by clean-profile repro; deepseek-harness #3515/#3516.)
|
|
209
231
|
try {
|
|
210
232
|
const full = JSON.parse(readFileSync(join(ROOT, "hooks", "hooks.json"), "utf8"));
|
|
211
233
|
const subset = Object.fromEntries(Object.entries(full.hooks || {}).filter(([k]) => k !== "SessionStart"));
|
|
@@ -222,10 +244,8 @@ ${HAS_GRAFT ? ` - id: trantor-graft
|
|
|
222
244
|
const rootPath = join(prof, "cordis.yml");
|
|
223
245
|
if (!existsSync(rootPath)) writeFileSync(rootPath, "# dsh profile root — an empty entry list; the tree is composed from bundles + cordis.patch.yml.\n[]\n");
|
|
224
246
|
// pnpm settings mirroring dsh's own profile template. autoInstallPeers:false is LOAD-BEARING:
|
|
225
|
-
// an installer that pulls the bridge's peers drops a SECOND copy of dsh's core
|
|
226
|
-
//
|
|
227
|
-
// dies on ctx.tools[TOOL_RUNTIME_SCHEDULER] being undefined (observed: every turn that used
|
|
228
|
-
// any tool crashed "reading 'prepare'"; tool-free turns worked).
|
|
247
|
+
// an installer that pulls the bridge's peers drops a SECOND copy of dsh's core into the
|
|
248
|
+
// profile, both instances mount, and the first tool call dies on ctx.tools being undefined.
|
|
229
249
|
writeFileSync(join(prof, "pnpm-workspace.yaml"), "packages:\n - .\n\nnodeLinker: hoisted\nautoInstallPeers: false\n");
|
|
230
250
|
// the two bridge packages must be importable from the profile's node_modules — via pnpm
|
|
231
251
|
// (peers OFF, hoisted) like dsh's own template; npm needs --legacy-peer-deps for the same
|
|
@@ -240,7 +260,7 @@ ${HAS_GRAFT ? ` - id: trantor-graft
|
|
|
240
260
|
}
|
|
241
261
|
|
|
242
262
|
const found = out.length;
|
|
243
|
-
console.log(`trantor connect${DRY ? " (dry run)" : ""} — hub: ${URL_}`);
|
|
263
|
+
console.log(`trantor connect${DRY ? " (dry run)" : ""} — project: ${PROJECT_AT_CONNECT}, hub: ${URL_}`);
|
|
244
264
|
for (const r of out) console.log(` ${r.cli.padEnd(9)} ${r.status}${r.detail ? ` (${r.detail})` : ""}`);
|
|
245
265
|
if (!found) console.log(" no supported CLIs found on PATH (claude, codex, gemini, kimi, opencode, dsh)");
|
|
246
266
|
console.log(DRY ? "\nRun without --dry-run to apply." : "\nDone. New sessions of each CLI auto-join the bus.");
|
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) {
|
package/bin/crew/models.mjs
CHANGED
|
@@ -2,6 +2,7 @@ import { existsSync } from "node:fs";
|
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { call } from "./core.mjs";
|
|
4
4
|
import { reapSeat } from "./state.mjs";
|
|
5
|
+
import { resolveEffort } from "../../lib/model-catalog.mjs";
|
|
5
6
|
|
|
6
7
|
function routeModel(ctx, provider, candidates, task, difficulty) {
|
|
7
8
|
const bundled = join(ctx.root, "engine/bin/scrooge");
|
|
@@ -36,16 +37,22 @@ export function resolveSpec(ctx, spec, task, difficulty, skipped) {
|
|
|
36
37
|
if (agent === "glm") field = "zai-coding-plan";
|
|
37
38
|
else if (!["codex", "kimi", "claude", "gemini", "dsh", "opencode"].includes(agent)) field = agent;
|
|
38
39
|
}
|
|
39
|
-
if (!field)
|
|
40
|
-
|
|
40
|
+
if (!field) {
|
|
41
|
+
// #7777: CLI-default seat (kimi/codex/claude/dsh) — the catalog finds the entry through the
|
|
42
|
+
// agent alias so the runner still gets a CREW_EFFORT record (or the uncatalogued line).
|
|
43
|
+
return { agent, model: "", effort: resolveEffort(agent, "", difficulty) };
|
|
44
|
+
}
|
|
45
|
+
if (field.includes("/")) return { agent, model: field, effort: resolveEffort(agent, field, difficulty) };
|
|
41
46
|
if (["claude", "codex", "kimi", "gemini"].includes(agent)) {
|
|
42
47
|
console.log(` → ${agent}: model ${field} (native pin)`);
|
|
43
|
-
return { agent, model: field };
|
|
48
|
+
return { agent, model: field, effort: resolveEffort(agent, field, difficulty) };
|
|
44
49
|
}
|
|
45
50
|
try {
|
|
46
51
|
const model = resolveModel(ctx, agent, field, task, difficulty);
|
|
52
|
+
// #7777: the live model is known — attach the catalog's per-difficulty effort parameters here
|
|
53
|
+
// so the runner applies them where the CLI accepts them (CREW_EFFORT, below).
|
|
47
54
|
console.log(` → ${agent}: live model ${model} (${field} · ${task}/${difficulty})`);
|
|
48
|
-
return { agent, model };
|
|
55
|
+
return { agent, model, effort: resolveEffort(agent, model, difficulty) };
|
|
49
56
|
} catch (error) {
|
|
50
57
|
console.error(error.message);
|
|
51
58
|
console.error(`[crew] ✗ skipping seat '${agent}' — model resolution failed for ${field} (${task}/${difficulty}); remaining seats still launch`);
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// #7760: `trantor up` used to hand out worktrees that could not build — every seat discovered the
|
|
2
|
+
// missing sibling package and the gitignored config at once. Up now provisions the first seat's
|
|
3
|
+
// worktree from .trantor/worktree.json, runs the declared preflight there once, and broadcasts.
|
|
4
|
+
import {
|
|
5
|
+
PREFLIGHT_CAP_MS, applyWorktreeDeclaration, ensureSeatWorktree, preflightLine, provisioningLines,
|
|
6
|
+
readWorktreeDeclaration, runPreflight,
|
|
7
|
+
} from "../../lib/seat-worktree.mjs";
|
|
8
|
+
import { gitRoot, hostId } from "../../lib/project.mjs";
|
|
9
|
+
import { loadOrCreate } from "../../lib/identity.mjs";
|
|
10
|
+
import { ensureEnrolled } from "../../lib/enroll.mjs";
|
|
11
|
+
import { sfetchJson } from "../../lib/signed-fetch.mjs";
|
|
12
|
+
|
|
13
|
+
const BROADCAST_MAX = 1500;
|
|
14
|
+
|
|
15
|
+
export function broadcastText(agent, applied, result) {
|
|
16
|
+
const parts = [];
|
|
17
|
+
if (result) parts.push(result.ok ? `${preflightLine(result)} in the ${agent} worktree` : `preflight failed in the ${agent} worktree (${result.command}): ${result.tail.join(" | ")}`);
|
|
18
|
+
else parts.push(`worktree declaration applied for ${agent} (no preflight declared)`);
|
|
19
|
+
if (applied.operator.length) parts.push(`operator steps: ${applied.operator.join("; ")}`);
|
|
20
|
+
if (applied.problems.length) parts.push(`problems: ${applied.problems.join("; ")}`);
|
|
21
|
+
const text = parts.join(" — ");
|
|
22
|
+
return text.length > BROADCAST_MAX ? `${text.slice(0, BROADCAST_MAX - 1)}…` : text;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async function broadcast(ctx, text, log) {
|
|
26
|
+
const session = ctx.env.RELAY_SESSION || `${hostId()}:${ctx.project}`;
|
|
27
|
+
try {
|
|
28
|
+
const identity = loadOrCreate(session, "agent");
|
|
29
|
+
await ensureEnrolled(ctx.hub, identity, ctx.project);
|
|
30
|
+
const r = await sfetchJson(`${ctx.hub}/send`, {
|
|
31
|
+
identity, payload: { from: session, to: "all", project: ctx.project, kind: "status", text },
|
|
32
|
+
signal: AbortSignal.timeout(5000),
|
|
33
|
+
});
|
|
34
|
+
if (!r.ok) log(`— preflight result NOT recorded on the bus (${ctx.hub}/send answered ${r.status}) —`);
|
|
35
|
+
return r.ok;
|
|
36
|
+
} catch (e) {
|
|
37
|
+
log(`— preflight result NOT recorded on the bus (${String(e?.message || e).slice(0, 80)}) —`);
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Returns { skipped } when the project declares nothing (unchanged behaviour), else what was
|
|
43
|
+
// applied, the preflight result, and whether the bus recorded it.
|
|
44
|
+
export async function preflightFirstSeat(ctx, agent, { capMs = PREFLIGHT_CAP_MS, log = console.log } = {}) {
|
|
45
|
+
const decl = readWorktreeDeclaration(gitRoot(ctx.dir) || ctx.dir);
|
|
46
|
+
if (!decl) return { skipped: "no declaration" };
|
|
47
|
+
const wt = ensureSeatWorktree({ sourceDir: ctx.dir, project: ctx.project, agent, home: ctx.home, env: ctx.env, log });
|
|
48
|
+
if (!wt.root) {
|
|
49
|
+
log(`— preflight skipped: no seat worktree for ${agent} (running from ${wt.dir}) —`);
|
|
50
|
+
return { skipped: "no worktree" };
|
|
51
|
+
}
|
|
52
|
+
const applied = applyWorktreeDeclaration(decl, { root: wt.root, seatDir: wt.dir });
|
|
53
|
+
log(`— ${agent} worktree ${wt.created ? "created" : "reused"} at ${wt.dir}; .trantor/worktree.json applied —`);
|
|
54
|
+
for (const l of provisioningLines(applied, { prefix: " " })) log(l);
|
|
55
|
+
|
|
56
|
+
let result = null;
|
|
57
|
+
if (!decl.preflight) log("— no preflight declared —");
|
|
58
|
+
else if (wt.dirty) log(`— preflight skipped: the ${agent} worktree has uncommitted work (commit or harvest it, then re-run up) —`);
|
|
59
|
+
else {
|
|
60
|
+
log(`— preflight in ${wt.dir}: ${decl.preflight} (cap ${Math.round(capMs / 60000)} min) —`);
|
|
61
|
+
result = runPreflight(decl.preflight, { seatDir: wt.dir, capMs, env: ctx.env });
|
|
62
|
+
log(result.ok ? `\x1b[32m${preflightLine(result)}\x1b[0m` : `\x1b[31m${preflightLine(result)}\x1b[0m`);
|
|
63
|
+
if (!result.ok) log(" ✗ fix the worktree need above (or the declaration) BEFORE writing contracts — the seats will hit the same wall.");
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const worthPosting = result || applied.operator.length || applied.problems.length;
|
|
67
|
+
const text = worthPosting ? broadcastText(agent, applied, result) : "";
|
|
68
|
+
const posted = text ? await broadcast(ctx, text, log) : false;
|
|
69
|
+
return { applied, result, text, posted, seatDir: wt.dir, created: wt.created };
|
|
70
|
+
}
|
package/bin/crew/tmux.mjs
CHANGED
|
@@ -7,7 +7,7 @@ export function spawnTmux(ctx, specs, resolve) {
|
|
|
7
7
|
for (const spec of specs) {
|
|
8
8
|
const seat = resolve(spec);
|
|
9
9
|
if (!seat) continue;
|
|
10
|
-
const command = runnerCommand(ctx, seat.agent, seat.model);
|
|
10
|
+
const command = runnerCommand(ctx, seat.agent, seat.model, seat.effort);
|
|
11
11
|
let pane = ctx.dry ? "%DRY" : "";
|
|
12
12
|
if (first) {
|
|
13
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` });
|
|
@@ -52,7 +52,7 @@ export function spawnTerminal(ctx, specs, resolve) {
|
|
|
52
52
|
recordState(ctx, ctx.project, "win", seat.agent, `%DRYWIN${index}`);
|
|
53
53
|
continue;
|
|
54
54
|
}
|
|
55
|
-
const command = `clear && ${runnerCommand(ctx, seat.agent, seat.model)}`;
|
|
55
|
+
const command = `clear && ${runnerCommand(ctx, seat.agent, seat.model, seat.effort)}`;
|
|
56
56
|
const title = `${ctx.project} · ${seat.agent.toUpperCase()}`;
|
|
57
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
58
|
if (result.stdout) {
|