trantor 0.17.31 → 0.17.32

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.17.31",
3
+ "version": "0.17.32",
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
@@ -27,6 +27,7 @@ switch (cmd) {
27
27
  case "watch": run("bin/relay-watch.mjs"); break;
28
28
  case "catchup": run("bin/catchup.mjs"); break;
29
29
  case "agents": run("bin/agents.mjs"); break;
30
+ case "gates": run("bin/gates.mjs"); break;
30
31
  case "backfill": run("bin/git-backfill.mjs"); break;
31
32
  case "handoff": run("bin/baton.mjs"); break;
32
33
  case "ui": {
@@ -50,6 +51,7 @@ switch (cmd) {
50
51
  trantor ui open the live dashboard (board + flow views)
51
52
  trantor catchup "where are we?" — the continuous board + git, with a synthesized brief
52
53
  trantor agents what this session's sub-agents did (task · returned? · files written · survived on disk) — [<sessionId>] [--json]
54
+ trantor gates verification gates: "must verify before shipping" claims that survive handoffs — [--all] [--json]
53
55
  trantor backfill card past GIT work onto the board (solo commits that were never carded) — [--since "14 days ago"] [--dry-run]
54
56
  trantor handoff finish this session NOW: write a handoff, open a fresh session that takes over, and close this one (manual baton)
55
57
  trantor advise ask the Advisor directly (JSON on stdin; --demo to see it)
package/bin/gates.mjs ADDED
@@ -0,0 +1,41 @@
1
+ #!/usr/bin/env node
2
+ // trantor gates [--all] [--json] — verification gates for THIS project: structured "must verify
3
+ // before shipping" claims that survive handoffs and surface to whoever takes over. Open by default;
4
+ // --all includes resolved ones. Set/resolve gates from inside a session via the relay_verify_gate tool.
5
+ import { readFileSync } from "node:fs";
6
+ import { join } from "node:path";
7
+ import { homedir } from "node:os";
8
+ import { resolveProject } from "../lib/project.mjs";
9
+
10
+ const args = process.argv.slice(2);
11
+ const all = args.includes("--all");
12
+ const asJson = args.includes("--json");
13
+
14
+ function relayUrl() {
15
+ if (process.env.RELAY_URL) return process.env.RELAY_URL;
16
+ try { const u = JSON.parse(readFileSync(join(homedir(), ".agent-bus", "config.json"), "utf8")).url; if (u) return u; } catch {}
17
+ return "http://127.0.0.1:4477";
18
+ }
19
+
20
+ const project = resolveProject(process.cwd());
21
+ const url = `${relayUrl()}/verify-gates?project=${encodeURIComponent(project)}${all ? "&all=1" : ""}`;
22
+ let gates = [];
23
+ try {
24
+ const r = await fetch(url, { signal: AbortSignal.timeout(2500) });
25
+ gates = (await r.json()).gates || [];
26
+ } catch {
27
+ console.error(`could not reach the hub at ${relayUrl()} — is it running? (trantor setup / trantor hub)`);
28
+ process.exit(1);
29
+ }
30
+
31
+ if (asJson) { process.stdout.write(JSON.stringify(gates, null, 2) + "\n"); process.exit(0); }
32
+ if (!gates.length) { console.log(`${project}: no ${all ? "" : "open "}verification gates`); process.exit(0); }
33
+
34
+ console.log(`${project} — ${gates.length} ${all ? "" : "open "}verification gate(s):`);
35
+ for (const g of gates) {
36
+ const badge = g.status === "open" ? "⚠️ OPEN" : `✓ ${g.status}`;
37
+ console.log(`\n#${g.id} ${badge} ${g.claim}`);
38
+ if (g.why) console.log(` why: ${g.why}`);
39
+ if (g.howToVerify) console.log(` how: ${g.howToVerify}`);
40
+ if (g.status !== "open" && g.resolvedNote) console.log(` resolved: ${g.resolvedNote}`);
41
+ }
@@ -227,6 +227,15 @@ export function writeHandoff({ projectDir, sessionId, transcript, trigger, summa
227
227
  // baked copy is just orientation if the live command isn't available. Best-effort; never throws.
228
228
  let subagents = null;
229
229
  try { subagents = deriveSubagentManifest(transcript, { projectRoot: projectDir }); } catch {}
230
+ // Open verification gates for this project — structured "must verify before shipping" claims that
231
+ // MUST survive the handoff (a narrative line gets skimmed past; this is what the v0.17.31 incident
232
+ // taught — the "verify Gail coefficients" intent vanished into prose). Fetched synchronously from
233
+ // the local hub; best-effort, never blocks the handoff.
234
+ let verifyGates = [];
235
+ try {
236
+ const out = execSync(`curl -s --max-time 2 ${JSON.stringify(relayUrl() + "/verify-gates?project=" + encodeURIComponent(projectName))}`, { encoding: "utf8", timeout: 2500 });
237
+ verifyGates = JSON.parse(out).gates || [];
238
+ } catch {}
230
239
  const record = {
231
240
  id: `${projectName}-${stamp}`,
232
241
  project: projectDir, projectName, machine: hostname(),
@@ -234,7 +243,7 @@ export function writeHandoff({ projectDir, sessionId, transcript, trigger, summa
234
243
  transcript_path: transcript || "", stamp: Number(stamp) || 0,
235
244
  // narrative + a verbatim recent-exchange block so exact in-flight state always survives
236
245
  summary: narrative + (tail ? `\n\n---\n## Verbatim recent exchange (exact in-flight state — continue from here)\n${tail}` : ""),
237
- gitStatus, subagents, consumed: false,
246
+ gitStatus, subagents, verifyGates, consumed: false,
238
247
  };
239
248
  const file = join(HANDOFF_DIR, `${record.id}.json`);
240
249
  writeFileSync(file, JSON.stringify(record, null, 2));
@@ -197,6 +197,19 @@ try {
197
197
  process.stderr.write(`[trantor] ${isCompact ? "showing (not claiming, compact)" : "loaded"} pending handoff ${handoff.id}\n`);
198
198
  additionalContext += `<trantor-handoff id="${sanitize(handoff.id)}" from="${sanitize(handoff.machine)}" trigger="${sanitize(handoff.trigger)}">\n`;
199
199
  additionalContext += `🔄 **You are taking over from a prior session that hit its context limit.** This is a fresh full window. Resume the work below — the prior session's summary, git state, and a pointer to its full transcript (searchable; Foundation/Gaia has it ingested) follow. Continue from "OPEN THREADS & NEXT STEPS"; do not restart from scratch.\n\n`;
200
+ // Verification gates FIRST — these are structured "must verify before shipping" claims the prior
201
+ // session couldn't independently prove. They go above the summary on purpose: a safety-critical
202
+ // check must not be skimmed past (the lesson of the lost "verify Gail coefficients" intent).
203
+ if (Array.isArray(handoff.verifyGates) && handoff.verifyGates.length) {
204
+ additionalContext += `## ⚠️ UNVERIFIED — verify before shipping (${handoff.verifyGates.length})\n`;
205
+ additionalContext += `The prior session flagged these as NOT independently verified. Do NOT commit or ship the related work until each is verified (or explicitly waived WITH the user) — passing the author's own tests is not verification. Resolve via the \`relay_verify_gate\` tool (action "resolve") once checked.\n`;
206
+ for (const g of handoff.verifyGates) {
207
+ additionalContext += `- **#${sanitize(String(g.id))}: ${sanitize(g.claim)}**${g.why ? ` — ${sanitize(g.why)}` : ""}`;
208
+ if (g.howToVerify) additionalContext += `\n how to verify: ${sanitize(g.howToVerify)}`;
209
+ additionalContext += `\n`;
210
+ }
211
+ additionalContext += `\n`;
212
+ }
200
213
  additionalContext += `## Handoff summary\n${sanitize(handoff.summary)}\n`;
201
214
  if (handoff.gitStatus) additionalContext += `\n## Git working-tree at handoff\n\`\`\`\n${sanitize(handoff.gitStatus)}\n\`\`\`\n`;
202
215
  // Sub-agent manifest: LIVE-primary, snapshot-as-fallback. The prior session may have had
package/hub.mjs CHANGED
@@ -50,11 +50,11 @@ function scanTelemetry() {
50
50
 
51
51
  // peers: { session: { lastSeen, status, project } } ; tasks: kanban cards
52
52
  // projectMeta: { project: { brief, by, updated } } — the "what & why" blurb per project
53
- let state = { messages: [], peers: {}, seq: 0, tasks: [], taskSeq: 0, projectMeta: {}, lessons: [], cardEvents: [], cardEventsBackfilled: false, aliases: {}, phaseMeta: {} };
53
+ let state = { messages: [], peers: {}, seq: 0, tasks: [], taskSeq: 0, projectMeta: {}, lessons: [], cardEvents: [], cardEventsBackfilled: false, aliases: {}, phaseMeta: {}, verifyGates: [], verifyGateSeq: 0 };
54
54
  try {
55
55
  if (existsSync(DATA)) {
56
56
  const loaded = JSON.parse(readFileSync(DATA, "utf8"));
57
- state = { messages: loaded.messages || [], peers: {}, seq: loaded.seq || 0, tasks: loaded.tasks || [], taskSeq: loaded.taskSeq || 0, projectMeta: loaded.projectMeta || {}, lessons: loaded.lessons || [], cardEvents: Array.isArray(loaded.cardEvents) ? loaded.cardEvents : [], cardEventsBackfilled: !!loaded.cardEventsBackfilled, aliases: (loaded.aliases && typeof loaded.aliases === "object") ? loaded.aliases : {}, phaseMeta: (loaded.phaseMeta && typeof loaded.phaseMeta === "object") ? loaded.phaseMeta : {} };
57
+ state = { messages: loaded.messages || [], peers: {}, seq: loaded.seq || 0, tasks: loaded.tasks || [], taskSeq: loaded.taskSeq || 0, projectMeta: loaded.projectMeta || {}, lessons: loaded.lessons || [], cardEvents: Array.isArray(loaded.cardEvents) ? loaded.cardEvents : [], cardEventsBackfilled: !!loaded.cardEventsBackfilled, aliases: (loaded.aliases && typeof loaded.aliases === "object") ? loaded.aliases : {}, phaseMeta: (loaded.phaseMeta && typeof loaded.phaseMeta === "object") ? loaded.phaseMeta : {}, verifyGates: Array.isArray(loaded.verifyGates) ? loaded.verifyGates : [], verifyGateSeq: loaded.verifyGateSeq || 0 };
58
58
  for (const [s, v] of Object.entries(loaded.peers || {})) // migrate old numeric form
59
59
  state.peers[s] = typeof v === "number" ? { lastSeen: v, status: "", project: "" } : { lastSeen: v.lastSeen || 0, status: v.status || "", project: v.project || "" };
60
60
  }
@@ -487,6 +487,34 @@ const server = http.createServer(async (req, res) => {
487
487
  if (state.lessons.length > 500) state.lessons.splice(0, 100);
488
488
  dirty = true; return json(res, 200, { ok: true, count: state.lessons.length });
489
489
  }
490
+ // --- verification gates: structured "must verify before shipping" claims that travel with
491
+ // handoffs and surface PROMINENTLY to whoever takes over (so a safety-critical check can't be
492
+ // skimmed past in narrative prose — the "verify Gail coefficients" intent that got lost). ---
493
+ if (req.method === "POST" && P === "/verify-gate") {
494
+ const b = await body(req); touch(b.by, undefined, b.project);
495
+ const project = canon(String(b.project || "").slice(0, 80));
496
+ if (b.resolve) {
497
+ const g = state.verifyGates.find(x => x.id === Number(b.id) && x.project === project);
498
+ if (!g) return json(res, 404, { error: "gate not found" });
499
+ g.status = ["verified", "failed", "waived"].includes(b.status) ? b.status : "verified";
500
+ g.resolvedBy = b.by || ""; g.resolvedNote = String(b.note || "").slice(0, 300); g.resolvedTs = now();
501
+ dirty = true; return json(res, 200, { ok: true, gate: g });
502
+ }
503
+ const claim = String(b.claim || "").trim().slice(0, 300);
504
+ if (!claim) return json(res, 400, { error: "claim required" });
505
+ const dup = state.verifyGates.find(x => x.project === project && x.claim === claim && x.status === "open");
506
+ if (dup) return json(res, 200, { ok: true, gate: dup, dedup: true });
507
+ const g = { id: ++state.verifyGateSeq, project, claim, why: String(b.why || "").slice(0, 300),
508
+ howToVerify: String(b.howToVerify || "").slice(0, 300), status: "open", by: b.by || "", ts: now() };
509
+ state.verifyGates.push(g); if (state.verifyGates.length > 500) state.verifyGates.splice(0, 100);
510
+ dirty = true; return json(res, 200, { ok: true, gate: g });
511
+ }
512
+ if (req.method === "GET" && P === "/verify-gates") {
513
+ const project = canon(String(q.project || ""));
514
+ let gates = state.verifyGates.filter(g => !project || g.project === project);
515
+ if (q.all !== "1") gates = gates.filter(g => g.status === "open");
516
+ return json(res, 200, { gates });
517
+ }
490
518
  if (req.method === "GET" && P === "/economics") { // the brain's books, surfaced: scrooge ledger + quota profile
491
519
  const out = { scrooge: null, lifetime: null, profile: null };
492
520
  try { out.profile = JSON.parse(readFileSync(join(homedir(), ".agent-bus", "profile.json"), "utf8")).providers || {}; } catch {}
package/mcp.mjs CHANGED
@@ -105,6 +105,32 @@ server.tool("relay_lesson", "Record a LESSON learned from a failure so future cr
105
105
  return { content: [{ type: "text", text: r.dedup ? "lesson already recorded" : `lesson recorded (${r.count} total)` }] };
106
106
  });
107
107
 
108
+ server.tool("relay_verify_gate", "Record a VERIFICATION GATE — a claim that MUST be independently verified before the related work ships (e.g. 'Gail breast coefficients match the published BCRAT model'). Unlike a note buried in a handoff narrative, a gate is STRUCTURED: it travels with handoffs and is shown PROMINENTLY to whoever takes over, so a safety-critical 'verify before commit' can't be skimmed past. action 'add' when you produce code whose correctness you have NOT independently proven (especially formulas/coefficients/security/data-shape); 'resolve' once you've verified it (or waived with the user); 'list' to see open gates. Defaults to THIS project.",
109
+ { action: z.enum(["add", "resolve", "list"]).describe("add a gate · resolve one · list open gates"),
110
+ claim: z.string().optional().describe("what must be verified (required for add) — a specific, checkable claim"),
111
+ why: z.string().optional().describe("why it matters / the risk if it ships unverified"),
112
+ howToVerify: z.string().optional().describe("the concrete check that would verify it (source to cross-check, command to run)"),
113
+ id: z.number().optional().describe("gate id to resolve"),
114
+ status: z.string().optional().describe("resolve status: 'verified' (default) | 'failed' | 'waived'"),
115
+ note: z.string().optional().describe("resolution note (what you checked / why waived)"),
116
+ project: z.string().optional().describe("target project (default: this session's project)") },
117
+ async ({ action, claim, why, howToVerify, id, status, note, project }) => {
118
+ const proj = project || PROJECT;
119
+ if (action === "list") {
120
+ const { gates } = await api("GET", `/verify-gates?project=${encodeURIComponent(proj)}`);
121
+ if (!gates || !gates.length) return { content: [{ type: "text", text: `${proj}: no open verification gates` }] };
122
+ return { content: [{ type: "text", text: gates.map(g => `#${g.id} ⚠️ ${g.claim}${g.why ? ` — ${g.why}` : ""}`).join("\n") }] };
123
+ }
124
+ if (action === "resolve") {
125
+ if (!id) return { content: [{ type: "text", text: "id required to resolve a gate" }] };
126
+ const r = await api("POST", "/verify-gate", { resolve: true, id, status: status || "verified", note, project: proj, by: SESSION });
127
+ return { content: [{ type: "text", text: r.error ? `error: ${r.error}` : `gate #${id} resolved (${r.gate?.status || status || "verified"})` }] };
128
+ }
129
+ if (!claim) return { content: [{ type: "text", text: "claim required to add a gate" }] };
130
+ const r = await api("POST", "/verify-gate", { claim, why, howToVerify, project: proj, by: SESSION });
131
+ return { content: [{ type: "text", text: r.dedup ? `gate already open (#${r.gate.id})` : `🔒 verification gate #${r.gate.id} recorded — surfaces on every handoff until you resolve it` }] };
132
+ });
133
+
108
134
  server.tool("relay_board", "Show a project's Kanban board (all cards + their status + assignee). Defaults to THIS project; pass `project` to read a crew board you orchestrate from elsewhere.",
109
135
  { project: z.string().optional().describe("board to show (default: this session's project)") },
110
136
  async ({ project }) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "trantor",
3
- "version": "0.17.31",
3
+ "version": "0.17.32",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "trantor": "bin/cli.mjs"