trantor 0.18.82 → 0.18.86
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/bin/cli.mjs +8 -0
- package/bin/crew/cmux.mjs +11 -11
- package/bin/crew/core.mjs +3 -2
- package/bin/crew/herdr.mjs +8 -8
- package/bin/crew/state.mjs +30 -15
- package/bin/crew/swap.mjs +12 -12
- package/bin/crew/tmux.mjs +11 -11
- package/bin/crew-runner.mjs +176 -63
- package/bin/crew.mjs +22 -10
- package/bin/cut-snapshot.mjs +37 -0
- package/bin/gate.mjs +30 -0
- package/bin/keys.mjs +53 -0
- package/bin/turn-watchdog.mjs +14 -9
- package/bin/wake-nudge.mjs +21 -2
- package/hooks/file-claim.mjs +19 -7
- package/hooks/file-hold.mjs +49 -0
- package/hooks/heartbeat.mjs +2 -1
- package/hooks/hooks.json +10 -1
- package/hooks/lib/git-checkout.mjs +10 -0
- package/hooks/lib/overseer-level-cache.mjs +25 -0
- package/hooks/overseer-warn.mjs +1 -1
- package/hub/auth.mjs +7 -52
- package/hub/overseer.mjs +100 -34
- package/hub/routes/admin.mjs +58 -12
- package/hub/store.mjs +3 -2
- package/hub.mjs +1 -1
- package/lib/duty-nudges.mjs +12 -0
- package/lib/identity.mjs +181 -8
- package/lib/nudge-ledger.mjs +95 -0
- package/lib/overseer.mjs +82 -24
- package/lib/persona.mjs +43 -0
- package/lib/remote-overlap.mjs +193 -0
- package/lib/seat-why.mjs +6 -1
- package/lib/session-liveness.mjs +83 -0
- package/lib/state/gate.mjs +1 -1
- package/lib/store-pg.mjs +2 -0
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "trantor",
|
|
3
|
-
"version": "0.18.
|
|
3
|
+
"version": "0.18.86",
|
|
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/bin/cli.mjs
CHANGED
|
@@ -24,6 +24,7 @@ switch (cmd) {
|
|
|
24
24
|
case "profile": run("bin/profile.mjs"); break;
|
|
25
25
|
case "provider": case "providers": run("bin/provider.mjs"); break;
|
|
26
26
|
case "secrets": run("bin/secrets.mjs"); break;
|
|
27
|
+
case "keys": run("bin/keys.mjs"); break;
|
|
27
28
|
case "models": run("bin/models.mjs"); break;
|
|
28
29
|
case "advise": run("bin/advise.mjs"); break;
|
|
29
30
|
case "verify": run("bin/crew-verify.mjs"); break;
|
|
@@ -70,6 +71,10 @@ switch (cmd) {
|
|
|
70
71
|
}
|
|
71
72
|
break;
|
|
72
73
|
}
|
|
74
|
+
// Local throwaway: the hub's default is RELAY_AUTH=enforce (#11368), but a foreground
|
|
75
|
+
// `trantor hub` on loopback is a dev convenience — keep it usable (warn) unless the
|
|
76
|
+
// operator asked for a mode explicitly.
|
|
77
|
+
if (!process.env.RELAY_AUTH) process.env.RELAY_AUTH = "warn";
|
|
73
78
|
run("hub.mjs"); break;
|
|
74
79
|
}
|
|
75
80
|
case "project": {
|
|
@@ -103,6 +108,7 @@ switch (cmd) {
|
|
|
103
108
|
case "watch": run("bin/relay-watch.mjs"); break;
|
|
104
109
|
case "catchup": run("bin/catchup.mjs"); break;
|
|
105
110
|
case "agents": run("bin/agents.mjs"); break;
|
|
111
|
+
case "gate": run("bin/gate.mjs"); break;
|
|
106
112
|
case "gates": run("bin/gates.mjs"); break;
|
|
107
113
|
case "backfill": run("bin/git-backfill.mjs"); break;
|
|
108
114
|
case "sweep": run("bin/sweep.mjs"); break;
|
|
@@ -234,6 +240,7 @@ switch (cmd) {
|
|
|
234
240
|
trantor profile declare your plans: trantor profile set claude=max codex=plus deepseek=api
|
|
235
241
|
trantor provider bring ANY model (BYOM): list · status [--json] · verify <name> --key … · add <name> --key … · remove <name>
|
|
236
242
|
trantor secrets provider keys in the OS keychain: list · set <NAME> (stdin) · remove <NAME> · migrate [--dry-run]
|
|
243
|
+
trantor keys identity keys in the OS keychain: status · migrate [--dry-run] — ~/.agent-bus/keys keeps public material only
|
|
237
244
|
trantor models browse live models behind each seat + the router's pick per difficulty
|
|
238
245
|
trantor up … spawn a crew here: trantor up codex kimi deepseek:deepseek glm:zai-coding-plan
|
|
239
246
|
trantor open host THIS session as the project's orchestrator pane (trantor down spares it)
|
|
@@ -250,6 +257,7 @@ switch (cmd) {
|
|
|
250
257
|
trantor app the DESKTOP app: status | install | update — pulls the latest DMG from GitHub Releases
|
|
251
258
|
trantor catchup "where are we?" — the continuous board + git, with a synthesized brief
|
|
252
259
|
trantor agents what this session's sub-agents did (task · returned? · files written · survived on disk) — [<sessionId>] [--json]
|
|
260
|
+
trantor gate go|nogo <id> decide a conflicting file edit
|
|
253
261
|
trantor gates verification gates: "must verify before shipping" claims that survive handoffs — [--all] [--json]
|
|
254
262
|
trantor backfill card past GIT work onto the board (solo commits that were never carded) — [--since "14 days ago"] [--dry-run]
|
|
255
263
|
trantor init-hooks install a git post-commit hook so EVERY commit auto-cards on the board (reliable solo-work backstop) — [--uninstall]
|
package/bin/crew/cmux.mjs
CHANGED
|
@@ -110,13 +110,13 @@ export function spawnCmux(ctx, specs, resolve, prune) {
|
|
|
110
110
|
for (let index = 0; index < specs.length; index += 1) {
|
|
111
111
|
const seat = resolve(specs[index]);
|
|
112
112
|
if (!seat) continue;
|
|
113
|
-
const launcher = seatLauncher(ctx, seat.agent, runnerCommand(ctx, seat.agent, seat.model, seat.effort));
|
|
114
|
-
const old = reuse ? readRows(ctx).filter(row => row.project === ctx.project && row.kind === "cmux" && row.
|
|
113
|
+
const launcher = seatLauncher(ctx, (seat.persona || seat.agent), runnerCommand(ctx, seat.agent, seat.model, seat.effort, seat.persona));
|
|
114
|
+
const old = reuse ? readRows(ctx).filter(row => row.project === ctx.project && row.kind === "cmux" && row.persona === (seat.persona || seat.agent)).at(-1)?.handle || "" : "";
|
|
115
115
|
let pane = "";
|
|
116
116
|
if (reuse) {
|
|
117
|
-
console.log(ctx.dry ? `[dry] cmux: reuse workspace ${workspace} — new-split for ${seat.agent}${old ? ` (replacing ${old})` : ""}` : ` → reusing workspace ${workspace}`);
|
|
117
|
+
console.log(ctx.dry ? `[dry] cmux: reuse workspace ${workspace} — new-split for ${(seat.persona || seat.agent)}${old ? ` (replacing ${old})` : ""}` : ` → reusing workspace ${workspace}`);
|
|
118
118
|
pane = ctx.dry ? `%DRYT${index}` : split(ctx, workspace, "right", old || panes.at(-1) || "", launcher);
|
|
119
|
-
if (old) { closePane(ctx, old); dropState(ctx, ctx.project, "cmux", seat.agent); }
|
|
119
|
+
if (old) { closePane(ctx, old); dropState(ctx, ctx.project, "cmux", (seat.persona || seat.agent)); }
|
|
120
120
|
} else if (index === 0) {
|
|
121
121
|
if (ctx.dry) {
|
|
122
122
|
console.log(`[dry] cmux: new-workspace (cwd ${ctx.dir}) --command 'bash ${launcher}' → rename 'trantor:${ctx.project}'`);
|
|
@@ -133,8 +133,8 @@ export function spawnCmux(ctx, specs, resolve, prune) {
|
|
|
133
133
|
} else pane = split(ctx, workspace, direction, target, launcher);
|
|
134
134
|
}
|
|
135
135
|
panes.push(pane);
|
|
136
|
-
recordState(ctx, ctx.project, "cmux", seat.agent, pane);
|
|
137
|
-
console.log(` → ${seat.agent} seat in cmux workspace (${ctx.project})`);
|
|
136
|
+
recordState(ctx, ctx.project, "cmux", (seat.persona || seat.agent), pane, seat);
|
|
137
|
+
console.log(` → ${(seat.persona || seat.agent)} seat in cmux workspace (${ctx.project})`);
|
|
138
138
|
}
|
|
139
139
|
console.log(`— crew grouped in cmux: ONE workspace tab for ${ctx.project}, seats tiled + sidebar status. Teardown (this project only): trantor down —`);
|
|
140
140
|
}
|
|
@@ -150,8 +150,8 @@ function spawnAppleScript(ctx, specs, resolve) {
|
|
|
150
150
|
for (let index = 0; index < specs.length; index += 1) {
|
|
151
151
|
const seat = resolve(specs[index]);
|
|
152
152
|
if (!seat) continue;
|
|
153
|
-
const launcher = seatLauncher(ctx, seat.agent, runnerCommand(ctx, seat.agent, seat.model, seat.effort));
|
|
154
|
-
const old = reuse ? readRows(ctx).filter(row => row.project === ctx.project && row.kind === "cmux" && row.
|
|
153
|
+
const launcher = seatLauncher(ctx, (seat.persona || seat.agent), runnerCommand(ctx, seat.agent, seat.model, seat.effort, seat.persona));
|
|
154
|
+
const old = reuse ? readRows(ctx).filter(row => row.project === ctx.project && row.kind === "cmux" && row.persona === (seat.persona || seat.agent)).at(-1)?.handle || "" : "";
|
|
155
155
|
let pane;
|
|
156
156
|
if (!tab && index === 0) {
|
|
157
157
|
if (ctx.dry) {
|
|
@@ -167,11 +167,11 @@ function spawnAppleScript(ctx, specs, resolve) {
|
|
|
167
167
|
console.log(`[dry] cmux(AppleScript): split ${direction} from ${target || "<focused>"} + run 'bash ${launcher}'`);
|
|
168
168
|
pane = `%DRYT${index}`;
|
|
169
169
|
} else pane = splitAppleScriptPane(ctx, tab, target, direction, launcher);
|
|
170
|
-
if (old && pane && pane !== "ERR") { closePane(ctx, old); dropState(ctx, ctx.project, "cmux", seat.agent); }
|
|
170
|
+
if (old && pane && pane !== "ERR") { closePane(ctx, old); dropState(ctx, ctx.project, "cmux", (seat.persona || seat.agent)); }
|
|
171
171
|
}
|
|
172
172
|
panes.push(pane);
|
|
173
|
-
recordState(ctx, ctx.project, "cmux", seat.agent, pane);
|
|
174
|
-
console.log(` → ${seat.agent} seat in cmux workspace (${ctx.project})`);
|
|
173
|
+
recordState(ctx, ctx.project, "cmux", (seat.persona || seat.agent), pane, seat);
|
|
174
|
+
console.log(` → ${(seat.persona || seat.agent)} seat in cmux workspace (${ctx.project})`);
|
|
175
175
|
}
|
|
176
176
|
console.log(`— crew grouped in cmux (AppleScript): ONE workspace tab for ${ctx.project}, seats tiled. Teardown: trantor down —`);
|
|
177
177
|
}
|
package/bin/crew/core.mjs
CHANGED
|
@@ -139,7 +139,7 @@ export function gridColumns(size) {
|
|
|
139
139
|
// flag documented there finally arms the seat that reads it.
|
|
140
140
|
const FORWARDED_ENV = STATE_FLAGS;
|
|
141
141
|
|
|
142
|
-
export function runnerCommand(ctx, agent, model = "", effort = null) {
|
|
142
|
+
export function runnerCommand(ctx, agent, model = "", effort = null, persona = agent) {
|
|
143
143
|
const forwarded = FORWARDED_ENV
|
|
144
144
|
.filter((name) => process.env[name])
|
|
145
145
|
.map((name) => `${name}=${shellQuote(process.env[name])} `)
|
|
@@ -147,8 +147,9 @@ export function runnerCommand(ctx, agent, model = "", effort = null) {
|
|
|
147
147
|
// #7777: the launcher resolved the catalog's per-difficulty effort for this seat; the runner
|
|
148
148
|
// reads CREW_EFFORT, applies what the CLI accepts and logs the one-line summary.
|
|
149
149
|
const swapEnv = ctx.swap ? ` CREW_SWAP_STAGE=${shellQuote(ctx.swap.path)} CREW_SWAP_LABEL=${shellQuote(ctx.swap.label)} CREW_SWAP_AGENT=${shellQuote(ctx.swap.agent)} CREW_SWAP_DRIVER=${shellQuote(ctx.swap.driver || "")}` : "";
|
|
150
|
+
const personaEnv = ` CREW_AGENT=${shellQuote(agent)} CREW_PERSONA=${shellQuote(ctx.swap?.persona || persona)}`;
|
|
150
151
|
const effortEnv = effort ? ` CREW_EFFORT=${shellQuote(JSON.stringify(effort))}` : "";
|
|
151
|
-
return `cd ${shellQuote(ctx.dir)} && ${forwarded}CREW_MODEL=${shellQuote(model)}${effortEnv}${swapEnv} RELAY_PROJECT=${shellQuote(ctx.project)} RELAY_URL=${shellQuote(ctx.hub)} node ${shellQuote(join(ROOT, "bin/crew-runner.mjs"))} ${shellQuote(
|
|
152
|
+
return `cd ${shellQuote(ctx.dir)} && ${forwarded}CREW_MODEL=${shellQuote(model)}${effortEnv}${swapEnv}${personaEnv} RELAY_PROJECT=${shellQuote(ctx.project)} RELAY_URL=${shellQuote(ctx.hub)} node ${shellQuote(join(ROOT, "bin/crew-runner.mjs"))} ${shellQuote(persona)} ${shellQuote(ctx.dir)}`;
|
|
152
153
|
}
|
|
153
154
|
|
|
154
155
|
export function listPids(pattern) {
|
package/bin/crew/herdr.mjs
CHANGED
|
@@ -159,18 +159,18 @@ function prepareWorkspace(ctx, prune) {
|
|
|
159
159
|
function replacementPane(ctx, workspace, spec, hostPane, resolve) {
|
|
160
160
|
const seat = resolve(spec);
|
|
161
161
|
if (!seat) return null;
|
|
162
|
-
const old = readRows(ctx).filter(row => row.project === ctx.project && row.kind === "herdr" && row.
|
|
162
|
+
const old = readRows(ctx).filter(row => row.project === ctx.project && row.kind === "herdr" && row.persona === (seat.persona || seat.agent)).at(-1)?.handle || "";
|
|
163
163
|
let pane;
|
|
164
164
|
if (ctx.dry) {
|
|
165
|
-
console.log(`[dry] herdr: reuse workspace ${workspace} — pane split for ${seat.agent}${old ? ` (replacing ${old})` : ""}`);
|
|
165
|
+
console.log(`[dry] herdr: reuse workspace ${workspace} — pane split for ${(seat.persona || seat.agent)}${old ? ` (replacing ${old})` : ""}`);
|
|
166
166
|
pane = `%DRYT${spec.index}`;
|
|
167
167
|
} else {
|
|
168
168
|
pane = splitPane(ctx, hostPane, "right", ctx.dir);
|
|
169
|
-
runSeat(ctx, pane, seat.agent, runnerCommand(ctx, seat.agent, seat.model, seat.effort));
|
|
169
|
+
runSeat(ctx, pane, (seat.persona || seat.agent), runnerCommand(ctx, seat.agent, seat.model, seat.effort, seat.persona));
|
|
170
170
|
}
|
|
171
171
|
if (old) {
|
|
172
172
|
closePane(ctx, old);
|
|
173
|
-
dropState(ctx, ctx.project, "herdr", seat.agent);
|
|
173
|
+
dropState(ctx, ctx.project, "herdr", (seat.persona || seat.agent));
|
|
174
174
|
}
|
|
175
175
|
return { ...seat, pane };
|
|
176
176
|
}
|
|
@@ -194,8 +194,8 @@ export function spawnHerdr(ctx, specs, resolve, prune) {
|
|
|
194
194
|
workspace = seat.workspace || workspace;
|
|
195
195
|
panes.push(seat.pane);
|
|
196
196
|
reportAgent(ctx, seat.pane, seat.agent);
|
|
197
|
-
recordState(ctx, ctx.project, "herdr", seat.agent, seat.pane);
|
|
198
|
-
console.log(` → ${seat.agent} seat in herdr workspace (${ctx.project})`);
|
|
197
|
+
recordState(ctx, ctx.project, "herdr", (seat.persona || seat.agent), seat.pane, seat);
|
|
198
|
+
console.log(` → ${(seat.persona || seat.agent)} seat in herdr workspace (${ctx.project})`);
|
|
199
199
|
}
|
|
200
200
|
console.log(`— crew grouped in herdr: ONE workspace for ${ctx.project}, seats as named panes in its server. Teardown (this project only): trantor down —`);
|
|
201
201
|
}
|
|
@@ -203,7 +203,7 @@ export function spawnHerdr(ctx, specs, resolve, prune) {
|
|
|
203
203
|
function freshPane(ctx, workspace, spec, panes, columns, resolve) {
|
|
204
204
|
const seat = resolve(spec.value);
|
|
205
205
|
if (!seat) return null;
|
|
206
|
-
const command = runnerCommand(ctx, seat.agent, seat.model, seat.effort);
|
|
206
|
+
const command = runnerCommand(ctx, seat.agent, seat.model, seat.effort, seat.persona);
|
|
207
207
|
let pane = "";
|
|
208
208
|
if (spec.index === 0) {
|
|
209
209
|
if (ctx.dry) {
|
|
@@ -221,7 +221,7 @@ function freshPane(ctx, workspace, spec, panes, columns, resolve) {
|
|
|
221
221
|
pane = `%DRYT${spec.index}`;
|
|
222
222
|
} else pane = splitPane(ctx, target, direction, ctx.dir);
|
|
223
223
|
}
|
|
224
|
-
runSeat(ctx, pane, seat.agent, command);
|
|
224
|
+
runSeat(ctx, pane, (seat.persona || seat.agent), command);
|
|
225
225
|
return { ...seat, pane, workspace };
|
|
226
226
|
}
|
|
227
227
|
|
package/bin/crew/state.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { existsSync, readFileSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
|
|
1
|
+
import { appendFileSync, existsSync, mkdirSync, readFileSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
2
3
|
import { seatProcesses, signalProcesses } from "./processes.mjs";
|
|
3
4
|
import { call, listPids, run } from "./core.mjs";
|
|
4
5
|
import { hostId } from "../../lib/project.mjs";
|
|
@@ -6,9 +7,9 @@ import { statePathFor as dockerStatePathFor, sweep as dockerSweep } from "../../
|
|
|
6
7
|
|
|
7
8
|
export function parseRow(line) {
|
|
8
9
|
const fields = line.split("\t");
|
|
9
|
-
if (fields.length >= 4) return { project: fields[0], kind: fields[1], agent: fields[2], handle: fields[3] };
|
|
10
|
-
if (fields.length === 2) return { project: "", kind: "win", agent: fields[0], handle: fields[1] };
|
|
11
|
-
return { project: "", kind: "win", agent: fields[0] || "", handle: fields[1] || "" };
|
|
10
|
+
if (fields.length >= 4) return { project: fields[0], kind: fields[1], persona: fields[2], agent: fields[4] || fields[2], model: fields[5] || "", handle: fields[3] };
|
|
11
|
+
if (fields.length === 2) return { project: "", kind: "win", persona: fields[0], model: "", agent: fields[0], handle: fields[1] };
|
|
12
|
+
return { project: "", kind: "win", persona: fields[0], model: "", agent: fields[0] || "", handle: fields[1] || "" };
|
|
12
13
|
}
|
|
13
14
|
|
|
14
15
|
export function readRows(ctx) {
|
|
@@ -23,24 +24,24 @@ export function writeRows(ctx, rows) {
|
|
|
23
24
|
return;
|
|
24
25
|
}
|
|
25
26
|
const tmp = `${ctx.statePath}.tmp`;
|
|
26
|
-
writeFileSync(tmp, `${rows.map(row => [row.project, row.kind, row.agent, row.handle].join("\t")).join("\n")}\n`);
|
|
27
|
+
writeFileSync(tmp, `${rows.map(row => [row.project, row.kind, row.persona || row.agent, row.handle, ...((row.persona && row.persona !== row.agent) || row.model ? [row.agent, row.model || ""] : [])].join("\t")).join("\n")}\n`);
|
|
27
28
|
renameSync(tmp, ctx.statePath);
|
|
28
29
|
}
|
|
29
30
|
|
|
30
|
-
export function recordState(ctx, project, kind,
|
|
31
|
+
export function recordState(ctx, project, kind, persona, handle, metadata = {}) {
|
|
31
32
|
if (ctx.dry) return;
|
|
32
33
|
const rows = readRows(ctx);
|
|
33
34
|
// #7285: an exact duplicate carries no new fact, and a doubled herdrws row is how `up` came to
|
|
34
35
|
// see the live workspace as its own stale copy. One row per fact — refuse the push.
|
|
35
|
-
if (rows.some(row => row.project === project && row.kind === kind && row.
|
|
36
|
-
rows.push({ project, kind, agent, handle });
|
|
36
|
+
if (rows.some(row => row.project === project && row.kind === kind && row.persona === persona && row.handle === handle)) return;
|
|
37
|
+
rows.push({ project, kind, persona, agent: metadata.agent || persona, model: metadata.model || "", handle });
|
|
37
38
|
writeRows(ctx, rows);
|
|
38
39
|
}
|
|
39
40
|
|
|
40
41
|
export function dropState(ctx, project, kind, agent = "", handle = "") {
|
|
41
42
|
const rows = readRows(ctx).filter(row => !(
|
|
42
43
|
row.project === project && row.kind === kind &&
|
|
43
|
-
(!agent || row.
|
|
44
|
+
(!agent || row.persona === agent) && (!handle || row.handle === handle)
|
|
44
45
|
));
|
|
45
46
|
writeRows(ctx, rows);
|
|
46
47
|
}
|
|
@@ -107,7 +108,7 @@ async function closeRow(ctx, row, selected, orchProjects, adapters, tmuxClosed)
|
|
|
107
108
|
if (row.kind === "herdr" && (perSeat || hasOrch)) adapters.herdr.closePane(row.handle);
|
|
108
109
|
if (row.kind === "tmux") closeTmux(ctx, row, perSeat, tmuxClosed);
|
|
109
110
|
if ((row.kind === "win" || row.kind === "attach") && !(perSeat && row.kind === "attach")) killTerminal(ctx, row.handle);
|
|
110
|
-
if (!["cmuxws", "attach", "herdrws", "orch"].includes(row.kind)) await killSeatProcesses(ctx, row.project, row.
|
|
111
|
+
if (!["cmuxws", "attach", "herdrws", "orch"].includes(row.kind)) await killSeatProcesses(ctx, row.project, row.persona);
|
|
111
112
|
}
|
|
112
113
|
|
|
113
114
|
function closeTmux(ctx, row, perSeat, seen) {
|
|
@@ -127,11 +128,11 @@ export async function down(ctx, args, adapters) {
|
|
|
127
128
|
if (options.error) return 1;
|
|
128
129
|
const rows = readRows(ctx);
|
|
129
130
|
if (!rows.length) { console.log("no tracked crew windows"); return 0; }
|
|
130
|
-
const scoped = rows.filter(row => (options.all || row.project === ctx.project) && (!options.agents.length || options.agents.includes(row.
|
|
131
|
+
const scoped = rows.filter(row => (options.all || row.project === ctx.project) && (!options.agents.length || options.agents.includes(row.persona)));
|
|
131
132
|
const scope = options.all ? "all projects" : `project \"${ctx.project}\"`;
|
|
132
133
|
if (!scoped.length) { console.log(`nothing to tear down for ${scope}`); return 0; }
|
|
133
134
|
console.log(`— tearing down (${scope}):`);
|
|
134
|
-
for (const row of scoped) if (!["attach", "cmuxws", "herdrws", "orch"].includes(row.kind)) console.log(` • ${row.project || "<legacy>"} · ${row.
|
|
135
|
+
for (const row of scoped) if (!["attach", "cmuxws", "herdrws", "orch"].includes(row.kind)) console.log(` • ${row.project || "<legacy>"} · ${row.persona} (${row.kind})`);
|
|
135
136
|
if (options.all && !options.yes) {
|
|
136
137
|
console.log("— this is EVERY project's crew. Re-run with --yes to confirm: trantor down --all --yes");
|
|
137
138
|
return 0;
|
|
@@ -139,10 +140,24 @@ export async function down(ctx, args, adapters) {
|
|
|
139
140
|
const orchProjects = new Set(rows.filter(row => row.kind === "orch").map(row => row.project));
|
|
140
141
|
const tmuxClosed = new Set();
|
|
141
142
|
for (const row of scoped) await closeRow(ctx, row, options.agents, orchProjects, adapters, tmuxClosed);
|
|
142
|
-
const removed = new Set(scoped.map(row => `${row.project}|${row.kind}|${row.
|
|
143
|
-
const kept = rows.filter(row => !removed.has(`${row.project}|${row.kind}|${row.
|
|
143
|
+
const removed = new Set(scoped.map(row => `${row.project}|${row.kind}|${row.persona}|${row.handle}`));
|
|
144
|
+
const kept = rows.filter(row => !removed.has(`${row.project}|${row.kind}|${row.persona}|${row.handle}`) || (orchProjects.has(row.project) && ["herdrws", "cmuxws", "orch"].includes(row.kind)));
|
|
144
145
|
writeRows(ctx, kept);
|
|
145
|
-
|
|
146
|
+
const seatRows = scoped.filter(row => !["attach", "cmuxws", "herdrws", "orch"].includes(row.kind));
|
|
147
|
+
if (!ctx.dry) {
|
|
148
|
+
const logDir = join(dirname(ctx.statePath), "logs");
|
|
149
|
+
mkdirSync(logDir, { recursive: true });
|
|
150
|
+
for (const row of new Map(seatRows.map(row => [`${row.project}/${row.persona}`, row])).values()) {
|
|
151
|
+
appendFileSync(join(logDir, `${row.persona}-${row.project}.jsonl`),
|
|
152
|
+
`${JSON.stringify({ ts: Date.now(), agent: row.persona, project: row.project, stopped: true })}\n`);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
if (options.agents.length) {
|
|
156
|
+
const stopped = [...new Set(seatRows.map(row => row.persona))];
|
|
157
|
+
const remaining = [...new Set(kept.filter(row => row.project === ctx.project &&
|
|
158
|
+
!["attach", "cmuxws", "herdrws", "orch"].includes(row.kind)).map(row => row.persona))];
|
|
159
|
+
console.log(`— ${stopped.join(", ") || options.agents.join(", ")} stopped${remaining.length ? ` (${remaining.join(", ")} still running)` : ""} (${scope})`);
|
|
160
|
+
} else console.log(`— crew torn down (${scope})`);
|
|
146
161
|
return 0;
|
|
147
162
|
}
|
|
148
163
|
|
package/bin/crew/swap.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { updatePersona } from "../../lib/persona.mjs";
|
|
1
2
|
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
2
3
|
import { join } from "node:path";
|
|
3
4
|
import { randomUUID } from "node:crypto";
|
|
@@ -89,12 +90,11 @@ export function swapRuntime(ctx, adapters, spawn) {
|
|
|
89
90
|
let replacementPid;
|
|
90
91
|
return {
|
|
91
92
|
validate: options => {
|
|
92
|
-
if (options.agent !== options.from && readRows(ctx).some(row => row.project === ctx.project && row.agent === options.agent)) throw new Error(`replacement seat ${options.agent} already exists; old seat unchanged`);
|
|
93
93
|
return validateReplacement(ctx, options);
|
|
94
94
|
},
|
|
95
95
|
async start(seat, stage) {
|
|
96
96
|
const staged = { ...ctx, swap: { ...stage, agent: seat.agent, driver: seat.driver } };
|
|
97
|
-
spawn(staged, { ...seat,
|
|
97
|
+
spawn(staged, { ...seat, persona: stage.label });
|
|
98
98
|
},
|
|
99
99
|
async ready(stage) {
|
|
100
100
|
const deadline = Date.now() + 30000;
|
|
@@ -118,10 +118,11 @@ export function swapRuntime(ctx, adapters, spawn) {
|
|
|
118
118
|
signalProcesses(pids, "SIGKILL");
|
|
119
119
|
},
|
|
120
120
|
async cancel(stage) { await down(ctx, [stage.label], adapters); },
|
|
121
|
-
release(stage,
|
|
122
|
-
writeRows(ctx, readRows(ctx).map(row => row.project === ctx.project && row.
|
|
123
|
-
writeFileSync(runnerRecord(ctx, ctx.project,
|
|
124
|
-
|
|
121
|
+
release(stage, persona, cursor) {
|
|
122
|
+
writeRows(ctx, readRows(ctx).map(row => row.project === ctx.project && row.persona === stage.label ? { ...row, persona } : row));
|
|
123
|
+
writeFileSync(runnerRecord(ctx, ctx.project, persona), JSON.stringify({ label: stage.label, dir: ctx.dir }));
|
|
124
|
+
updatePersona({ dir: join(ctx.home, ".agent-bus"), project: ctx.project, persona, agent: stage.agent, model: stage.model });
|
|
125
|
+
writeFileSync(`${stage.path}.release`, JSON.stringify({ agent: stage.agent, persona, cursor }));
|
|
125
126
|
},
|
|
126
127
|
};
|
|
127
128
|
}
|
|
@@ -133,23 +134,22 @@ export async function performSwap(ctx, options, runtime) {
|
|
|
133
134
|
const seat = await runtime.validate(options);
|
|
134
135
|
const label = `${options.agent}-swap-${randomUUID().slice(0, 8)}`;
|
|
135
136
|
mkdirSync(ctx.seatDir, { recursive: true });
|
|
136
|
-
stage = { label, path: join(ctx.seatDir, label) };
|
|
137
|
+
stage = { label, path: join(ctx.seatDir, label), persona: options.from, agent: seat.agent, model: seat.model };
|
|
137
138
|
await runtime.start(seat, stage);
|
|
138
139
|
const readySession = await runtime.ready(stage);
|
|
139
140
|
frozen = true;
|
|
140
141
|
await runtime.freeze(options.from);
|
|
141
142
|
const pending = readPending(ctx, options.from);
|
|
142
|
-
const target =
|
|
143
|
-
const from = `${options.from}:${ctx.project}`, to =
|
|
143
|
+
const target = pending;
|
|
144
|
+
const from = `${options.from}:${ctx.project}`, to = from;
|
|
144
145
|
const handoff = await runtime.transfer({ from, to, project: ctx.project, readySession, _op: `swap-${stage.label}`, heldAsk: pending.ask || 0, pendingIds: pending.wake.map(m => m.id) });
|
|
145
146
|
transferred = true;
|
|
146
147
|
result.moved = [...new Set([...pending.wake.map(m => m.id), ...handoff.moved])];
|
|
147
148
|
const wake = new Map([...target.wake, ...pending.wake, ...handoff.messages].map(m => [m.id, { ...m, to }]));
|
|
148
|
-
savePending(ctx, options.
|
|
149
|
+
savePending(ctx, options.from, { ...target, wake: [...wake.values()], bcast: [...new Map([...target.bcast, ...pending.bcast].map(m => [m.id, m])).values()], ask: handoff.messages.some(m => m.re === pending.ask) ? 0 : pending.ask || target.ask, askTo: pending.askTo || target.askTo });
|
|
149
150
|
await runtime.stop(options.from);
|
|
150
151
|
frozen = false;
|
|
151
|
-
|
|
152
|
-
await runtime.release(stage, options.agent, handoff.cursor);
|
|
152
|
+
await runtime.release(stage, options.from, handoff.cursor);
|
|
153
153
|
return { ...result, ok: true };
|
|
154
154
|
} catch (error) {
|
|
155
155
|
if (!transferred) {
|
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, seat.effort);
|
|
10
|
+
const command = runnerCommand(ctx, seat.agent, seat.model, seat.effort, seat.persona);
|
|
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` });
|
|
@@ -18,9 +18,9 @@ export function spawnTmux(ctx, specs, resolve) {
|
|
|
18
18
|
run(ctx, "tmux", ["select-layout", "-t", session, "tiled"], { rendered: `tmux select-layout -t '${session}' tiled >/dev/null 2>&1` });
|
|
19
19
|
}
|
|
20
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}`);
|
|
21
|
+
run(ctx, "tmux", ["select-pane", "-t", pane || session, "-T", (seat.persona || seat.agent).toUpperCase()], { rendered: `tmux select-pane -t '${pane || session}' -T ${(seat.persona || seat.agent).toUpperCase()}` });
|
|
22
|
+
recordState(ctx, ctx.project, "tmux", (seat.persona || seat.agent), pane || "%?", seat);
|
|
23
|
+
console.log(` → ${(seat.persona || seat.agent)} pane in ${session}`);
|
|
24
24
|
}
|
|
25
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
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` });
|
|
@@ -48,17 +48,17 @@ export function spawnTerminal(ctx, specs, resolve) {
|
|
|
48
48
|
const x = rect.x + (index % columns) * width;
|
|
49
49
|
const y = rect.y + Math.floor(index / columns) * height;
|
|
50
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}
|
|
51
|
+
console.log(`[dry] Terminal window for ${(seat.persona || seat.agent)} at ${x},${y}`);
|
|
52
|
+
recordState(ctx, ctx.project, "win", (seat.persona || seat.agent), `%DRYWIN${index}`, seat);
|
|
53
53
|
continue;
|
|
54
54
|
}
|
|
55
|
-
const command = `clear && ${runnerCommand(ctx, seat.agent, seat.model, seat.effort)}`;
|
|
56
|
-
const title = `${ctx.project} · ${seat.agent.toUpperCase()}`;
|
|
55
|
+
const command = `clear && ${runnerCommand(ctx, seat.agent, seat.model, seat.effort, seat.persona)}`;
|
|
56
|
+
const title = `${ctx.project} · ${(seat.persona || 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) {
|
|
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`);
|
|
59
|
+
recordState(ctx, ctx.project, "win", (seat.persona || seat.agent), result.stdout, seat);
|
|
60
|
+
console.log(` → ${(seat.persona || seat.agent)} window spawned (${ctx.project} · ${(seat.persona || seat.agent)})`);
|
|
61
|
+
} else console.log(` ✗ ${(seat.persona || seat.agent)} osascript spawn ERROR`);
|
|
62
62
|
call("sleep", ["1.2"]);
|
|
63
63
|
}
|
|
64
64
|
}
|