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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "trantor",
3
- "version": "0.18.82",
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.agent === seat.agent).at(-1)?.handle || "" : "";
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.agent === seat.agent).at(-1)?.handle || "" : "";
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(agent)} ${shellQuote(ctx.dir)}`;
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) {
@@ -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.agent === seat.agent).at(-1)?.handle || "";
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
 
@@ -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, agent, handle) {
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.agent === agent && row.handle === handle)) return;
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.agent === agent) && (!handle || row.handle === handle)
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.agent);
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.agent)));
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.agent} (${row.kind})`);
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.agent}|${row.handle}`));
143
- const kept = rows.filter(row => !removed.has(`${row.project}|${row.kind}|${row.agent}|${row.handle}`) || (orchProjects.has(row.project) && ["herdrws", "cmuxws", "orch"].includes(row.kind)));
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
- console.log(`— crew torn down (${scope})`);
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, agent: stage.label });
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, agent, cursor) {
122
- writeRows(ctx, readRows(ctx).map(row => row.project === ctx.project && row.agent === stage.label ? { ...row, agent } : row));
123
- writeFileSync(runnerRecord(ctx, ctx.project, agent), JSON.stringify({ label: stage.label, dir: ctx.dir }));
124
- writeFileSync(`${stage.path}.release`, JSON.stringify({ agent, cursor }));
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 = options.agent === options.from ? pending : readPending(ctx, options.agent);
143
- const from = `${options.from}:${ctx.project}`, to = `${options.agent}:${ctx.project}`;
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.agent, { ...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
+ 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
- if (options.agent !== options.from) savePending(ctx, options.from, { ...pending, wake: [], bcast: [], ask: 0, askTo: "" });
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
  }