trantor 0.18.22 → 0.18.23

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.22",
3
+ "version": "0.18.23",
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/bridge.mjs CHANGED
@@ -110,7 +110,10 @@ async function tick() {
110
110
  if (differs && (aMoved || bMoved)) {
111
111
  const aWins = aMoved && bMoved ? p.origin === "A" : aMoved;
112
112
  const [src, dstHub, dstId] = aWins ? [ta, TO, p.bId] : [tb, FROM, p.aId];
113
- const r = await call(dstHub, "POST", "/task/update", { id: dstId, status: src.status, assignee: src.assignee || "", by: src.by || "bridge" });
113
+ // reassign:true the bridge REPLICATES a hand-change that already happened on the source
114
+ // hub; without the explicit marker the #5406 assignee-immutability guard would 409 every
115
+ // cross-hub sync whose assignee moved (deepseek flagged this at review, 2026-08-31).
116
+ const r = await call(dstHub, "POST", "/task/update", { id: dstId, status: src.status, assignee: src.assignee || "", reassign: true, by: src.by || "bridge" });
114
117
  if (aWins) { p.lastA = ta.updated || 0; p.lastB = r.task.updated || 0; }
115
118
  else { p.lastB = tb.updated || 0; p.lastA = r.task.updated || 0; }
116
119
  synced++;
@@ -369,6 +369,23 @@ try {
369
369
  }
370
370
  } catch {}
371
371
 
372
+ // THE ORCHESTRATOR ROLE (2026-08-31 — the operator had to say it by hand, twice in one day:
373
+ // "your job is to oversee and be a project manager, not a coder"). A session opened by
374
+ // `trantor open` carries TRANTOR_ORCH=<project>; every such session gets the doctrine at
375
+ // boot, so the role survives wakes, handoffs and restarts without anyone restating it.
376
+ try {
377
+ if (project && (process.env.TRANTOR_ORCH || "") === project) {
378
+ additionalContext += `<trantor-orchestrator-role project="${sanitize(project)}">\n` +
379
+ `🎛️ **You are this project's ORCHESTRATOR — a project manager, not a coder** (operator ruling, 2026-08-31).\n` +
380
+ `- Building goes to the CREW: invoke the trantor:crew skill (Advisor → \`trantor up\` seats → contracts over the bus), or relay_scrooge for grunt one-shots.\n` +
381
+ `- Spend your own tokens on design, contracts, supervision, integration and VERIFICATION — run the seats' tests yourself; bounce hollow dones.\n` +
382
+ `- Write code yourself ONLY for small one-head seam work that needs this session's full context — and say so when you do.\n` +
383
+ `- Check relay_inbox and the board before asking the operator anything a peer may already have answered.\n` +
384
+ `</trantor-orchestrator-role>\n`;
385
+ process.stderr.write(`[trantor] injected orchestrator-role doctrine for ${project}\n`);
386
+ }
387
+ } catch {}
388
+
372
389
  // Update available? Surface it the way a terminal tool should — an in-terminal `systemMessage`
373
390
  // line the USER sees at session start (NOT a macOS desktop popup, which macOS misattributes to
374
391
  // Script Editor and which fires off-screen). It shows every session while an update is pending and
package/hub.mjs CHANGED
@@ -435,7 +435,12 @@ setTimeout(overseerTick, 2000).unref?.();
435
435
  // Env still wins at boot (an operator's declared config beats a seat's claim); otherwise the last
436
436
  // registered seat is restored from state, so a hub restart doesn't silently end the duty feed.
437
437
  let DUTY_SESSION = String(process.env.RELAY_DUTY_SESSION || state.dutySession || "");
438
- const DUTY_UNDELIVERED_MS = Number(process.env.RELAY_DUTY_UNDELIVERED_MS || 10 * 60 * 1000);
438
+ // 2 MINUTES, not 10 (2026-08-31): scribe DMed the woken crebral-health session at 16:11 and the
439
+ // operator hand-relayed at 16:21:58 — beating the old 10m escalation by seconds. Two agents
440
+ // actively collaborating cannot wait ten minutes; with duty's direct-wake the full chain
441
+ // (escalate → duty nudge → target's hooks poll) now lands in ~3m. Duty's own batch rules
442
+ // (one nudge per recipient per batch, consumed on activity) keep the shorter window from nagging.
443
+ const DUTY_UNDELIVERED_MS = Number(process.env.RELAY_DUTY_UNDELIVERED_MS || 2 * 60 * 1000);
439
444
  const dutyEscalated = new Set();
440
445
  // #5686: the janitor died 08-27 and NOTHING noticed for 4 days — the hub kept escalating to a
441
446
  // corpse. Duty liveness is now a first-class state: dark = configured but no heartbeat inside
@@ -1908,6 +1913,22 @@ const server = http.createServer(async (req, res) => {
1908
1913
  if (req.method === "POST" && P === "/task/update") { // move/edit a card
1909
1914
  const b = await body(req); const t = state.tasks.find(x => x.id === Number(b.id));
1910
1915
  if (!t) return json(res, 404, { error: "no such task" });
1916
+ // Board integrity (#5406): a card can never change hands silently. The assignee is frozen once
1917
+ // set; a mutation is legitimate only as a HANDOFF (the current assignee reassigning to someone
1918
+ // else) or an EXPLICIT reassign (reassign:true — e.g. the orchestrator re-routing work after a
1919
+ // seat dies). A silent third-party overwrite 409s so the caller knows the board refused to move.
1920
+ // Runs BEFORE any other field mutation so a refused steal cannot half-apply a status move.
1921
+ if (b.assignee !== undefined) {
1922
+ const want = String(b.assignee).slice(0, 60);
1923
+ if (want !== t.assignee) {
1924
+ const mover = String(auth?.identity?.name || b.by || "").slice(0, 120);
1925
+ const isOwner = !!t.assignee && mover === t.assignee;
1926
+ const explicit = b.reassign === true;
1927
+ if (!isOwner && !explicit) {
1928
+ return json(res, 409, { error: "assignee is immutable", id: t.id, assignee: t.assignee });
1929
+ }
1930
+ }
1931
+ }
1911
1932
  let eventType = "updated", eventFrom = null, eventTo = null;
1912
1933
  if (b.status && ["todo","doing","testing","failed","done","blocked","stale"].includes(b.status) && b.status !== t.status) {
1913
1934
  eventType = "moved"; eventFrom = t.status; eventTo = b.status;
@@ -1925,7 +1946,13 @@ const server = http.createServer(async (req, res) => {
1925
1946
  if (b.difficulty && ["easy","medium","hard"].includes(b.difficulty)) t.difficulty = b.difficulty;
1926
1947
  if (b.model !== undefined) t.model = String(b.model).slice(0, 60);
1927
1948
  if (Array.isArray(b.deps)) t.deps = [...new Set(b.deps.map(Number).filter(n => Number.isInteger(n) && n > 0 && n !== t.id))].slice(0, 20);
1928
- if (b.assignee !== undefined) t.assignee = b.assignee;
1949
+ if (b.assignee !== undefined && String(b.assignee).slice(0, 60) !== t.assignee) {
1950
+ const prev = t.assignee || "(none)";
1951
+ const mover = String(auth?.identity?.name || b.by || "").slice(0, 120);
1952
+ t.assignee = String(b.assignee).slice(0, 60);
1953
+ // the handover is part of the card's story, not a silent overwrite
1954
+ appendTaskLog(t, mover, `reassigned ${prev} → ${t.assignee}${b.reassign === true ? " (explicit)" : " (handoff)"}`, now());
1955
+ }
1929
1956
  if (b.title !== undefined) t.title = String(b.title).slice(0,200);
1930
1957
  // the narrative line a human reads on the board ("assigned — did"), written by the cheap
1931
1958
  // summarizer; rides the tasks.extra column, so it survives restarts everywhere
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "trantor",
3
- "version": "0.18.22",
3
+ "version": "0.18.23",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "trantor": "bin/cli.mjs"