trantor 0.18.68 → 0.18.70

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.68",
3
+ "version": "0.18.70",
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/baton.mjs CHANGED
@@ -3,12 +3,12 @@
3
3
  // a whole-session handoff (auto-summary + verbatim in-flight tail), opens a fresh self-announcing
4
4
  // session, and closes THIS window once it takes over. Run from inside the session you want to hand
5
5
  // off. (The richer MODEL-authored handoff is the /trantor:handoff skill.)
6
- import { readdirSync, statSync, fstatSync } from "node:fs";
6
+ import { readdirSync, statSync } from "node:fs";
7
7
  import { join, basename, dirname } from "node:path";
8
8
  import { homedir } from "node:os";
9
9
  import { spawn } from "node:child_process";
10
10
  import { fileURLToPath } from "node:url";
11
- import { writeHandoff, spawnBaton, resolveHandoffSurface, armBaton, contextUsage, controllingTty, turnInFlight, armMaxMs, sessionProcessState } from "../hooks/lib/handoff.mjs";
11
+ import { writeHandoff, spawnBaton, resolveHandoffSurface, armBaton, contextUsage, controllingTty, turnInFlight, armMaxMs, sessionProcessState, stdinCarriesMarkdown } from "../hooks/lib/handoff.mjs";
12
12
 
13
13
  // #6074: the skill path (write-handoff.mjs) and this CLI path must share ONE resolution of which
14
14
  // project this is and where the session lives. Both call resolveHandoffSurface; the name comes
@@ -22,18 +22,11 @@ const project = resolved.project;
22
22
  // cwd that cd'd somewhere else: a handoff record must carry the transcript it was written from.
23
23
  const sessionDir = resolved.projectDir;
24
24
 
25
- // Model-authored handoffs ride THIS binary too (`trantor handoff` — always the global install's
26
- // CURRENT code, never the plugin-cache copy a session booted with, the stale-0.18.20 bug). A piped
27
- // stdin (a heredoc, a cat, `<<HANDOFF`) means "here is the handoff markdown" — exactly the skill's
28
- // contract — so forward to write-handoff.mjs in the SAME package (same version, same resolution)
29
- // and exit with its status. --latest forwards too. A true pipe is a FIFO; a TTY and /dev/null are
30
- // character devices, so a plain `trantor handoff` typed at a prompt (or run by a hook with stdin
31
- // at /dev/null) keeps the auto-summary behavior. Detection must NOT be `!process.stdin.isTTY` —
32
- // that misreads /dev/null as a handoff and errors where auto-summary used to work.
33
- function stdinIsPipe() {
34
- try { return fstatSync(0).isFIFO(); } catch { return false; }
35
- }
36
- if (stdinIsPipe() || process.argv.includes("--latest")) {
25
+ // Model-authored handoffs ride THIS binary too — always the global install's CURRENT code, never
26
+ // the plugin-cache copy a session booted with (the stale-0.18.20 bug). Markdown on stdin means
27
+ // "here is the handoff", so forward to write-handoff.mjs in the SAME package; --latest forwards too.
28
+ // What counts as markdown, and why /dev/null must not: stdinCarriesMarkdown in hooks/lib (#8459).
29
+ if (stdinCarriesMarkdown() || process.argv.includes("--latest")) {
37
30
  const helper = join(dirname(fileURLToPath(import.meta.url)), "write-handoff.mjs");
38
31
  const child = spawn(process.execPath, [helper, ...process.argv.slice(2)], { stdio: ["inherit", "inherit", "inherit"] });
39
32
  child.on("exit", (c) => process.exit(c ?? 1));
@@ -64,12 +57,10 @@ function autoBaton() {
64
57
  // The transcript's filename IS the writing session's id — record it, or an orchestrator-thread
65
58
  // handoff carries no writer and the baton-hold + map-follow logic in sessionstart.mjs can't fire.
66
59
  const sessionId = transcript ? basename(transcript, ".jsonl") : "";
67
- // #6528: WHO pulled the trigger. A TTY stdin means a human typed `trantor handoff` at a prompt;
68
- // the app chain (lib.rs handoff_now) and hooks spawn this binary with piped stdio. The operator's
69
- // own typed command keeps the storm-guard bypass (force:true — "manual = intentional"); every
70
- // invoked path goes through the boundary gate and the hub's storm guard like any auto handoff.
71
- // --reason rides through from the app (`--reason clicked|countdown|unattended`) so the RECORD
72
- // finally names the real trigger instead of laundering every banner fire into "manual-cli".
60
+ // #6528: WHO pulled the trigger. A TTY stdin is a human typing it; the app chain (handoff_now)
61
+ // and hooks spawn this with piped stdio. Only the typed command keeps the storm-guard bypass
62
+ // (manual = intentional); every invoked path goes through the boundary gate like an auto handoff.
63
+ // --reason rides through from the app so the record names the real trigger, not "manual-cli".
73
64
  const reasonArg = (() => {
74
65
  const i = process.argv.indexOf("--reason");
75
66
  const v = i >= 0 ? String(process.argv[i + 1] || "").trim() : "";
@@ -2,23 +2,18 @@
2
2
  // Save a model-authored handoff (piped on stdin) for this project; the next session auto-loads it.
3
3
  // With --baton: ALSO open a fresh self-announcing session and close THIS window once it takes over
4
4
  // (the one-command manual baton behind /trantor:handoff). Without it: just write the file (legacy).
5
- import { writeFileSync, readFileSync, existsSync, mkdirSync, readdirSync } from "node:fs";
6
- import { join, basename } from "node:path";
7
- import { homedir, hostname } from "node:os";
8
- import { execSync } from "node:child_process";
9
- import { spawnBaton, handoffMode, resolveHandoffSurface, attachState, resolveSeat, resolveHandoffCard } from "../hooks/lib/handoff.mjs";
5
+ import { readFileSync, existsSync, mkdirSync, readdirSync } from "node:fs";
6
+ import { join } from "node:path";
7
+ import { writeHandoff, spawnBaton, resolveHandoffSurface } from "../hooks/lib/handoff.mjs";
10
8
  import { handoffDir } from "../lib/project.mjs";
11
9
 
12
10
  const baton = process.argv.includes("--baton");
13
- // --latest: pass the baton on a handoff ALREADY on disk. Without it the only route was "compose a
14
- // fresh one on stdin", so a session that had just written a 5KB handoff had to write it again to
15
- // hand it over — 2m28s of regenerated prose on a live scribe session, 2026-08-24.
11
+ // --latest: pass the baton on a handoff ALREADY on disk — composing it again cost 2m28s of
12
+ // regenerated prose on a live scribe session (drill: test/handoff/test-baton-latest.mjs).
16
13
  const latest = process.argv.includes("--latest");
17
14
  // #6074: ONE resolver for which project this is and where the session lives — shared with
18
- // bin/baton.mjs (the `trantor handoff` CLI path) so the two cannot diverge. The name comes from
19
- // the session's registration (TRANTOR_ORCH / RELAY_PROJECT / orch-sessions.txt) before the cwd;
20
- // the witnessed crebral-scribe/ios handoff recorded project "ios" from a subfolder cwd and never
21
- // found its pane.
15
+ // bin/baton.mjs so the two cannot diverge. The name comes from the registration
16
+ // (TRANTOR_ORCH / RELAY_PROJECT / orch-sessions.txt) before the cwd; a subfolder cwd never renames the project.
22
17
  const resolved = resolveHandoffSurface({ sessionId: process.env.CLAUDE_SESSION_ID || "" });
23
18
  const project = resolved.projectDir;
24
19
  const name = resolved.project;
@@ -37,8 +32,6 @@ if (!latest) {
37
32
  // directly, so an AGENT_BUS_DIR install wrote where nothing would look.
38
33
  const dir = handoffDir();
39
34
  if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
40
- const stamp = (() => { try { return execSync("date +%s", { encoding: "utf8" }).trim(); } catch { return String(process.pid); } })();
41
- let git = ""; try { git = execSync("git -C " + JSON.stringify(project) + " status --short 2>/dev/null | head -30", { encoding: "utf8" }).trim(); } catch {}
42
35
  // --latest short-circuits everything below: find this project's newest UNCONSUMED handoff and hand
43
36
  // that over, untouched.
44
37
  if (latest) {
@@ -59,18 +52,19 @@ if (latest) {
59
52
  process.exit(0);
60
53
  }
61
54
 
62
- // The manual skill path opens the §5 ledger with WRITTEN like every other writer (#5642), and
63
- // carries the same interface the hooks-side records have: transcript_path ("" — the summary IS
64
- // the model's own words), mode (attended|unattended, #5648).
65
- const rec = { id: `${name}-${stamp}`, project, projectName: name, machine: hostname(), trigger: baton ? "manual-baton" : "manual-skill", stamp: Number(stamp) || 0, summary: summary.trim() || "(empty)", transcript_path: "", mode: handoffMode(name), gitStatus: git, consumed: false, states: [{ state: "written", ts: Number(stamp) || 0, by: baton ? "manual-baton" : "manual-skill" }] };
66
- // The structured working state rides beside the prose (TDD §4.5), dark behind
67
- // TRANTOR_STATE_HANDOFF. The manual path gets it for the same reason the hook path does: this
68
- // record is what the successor loads, and `summary` here IS the model's own handoff — the richest
69
- // STATE block there is.
70
- const seat = resolveSeat(name);
71
- attachState(rec, { project: name, seat, card: resolveHandoffCard({ projectName: name, seat }), worktree: project });
72
- const file = join(dir, `${rec.id}.json`);
73
- writeFileSync(file, JSON.stringify(rec, null, 2));
55
+ // ONE WRITER (#8263): every guarantee CONTRACT-hooks states for "a handoff record" — the read-first
56
+ // floor, verifyGates, the sub-agent manifest, supersedeOlderHandoffs, uncapped persistence — lives
57
+ // in writeHandoff; the literal record assembled here reached none of them. Only trigger,
58
+ // transcript_path "" and force (the command IS the intent) differ.
59
+ const { file } = writeHandoff({
60
+ projectDir: project,
61
+ projectName: name,
62
+ sessionId: "",
63
+ transcript: "",
64
+ trigger: baton ? "manual-baton" : "manual-skill",
65
+ summary: summary.trim(),
66
+ force: true,
67
+ });
74
68
  console.log(`handoff saved: ${file}`);
75
69
 
76
70
  if (baton) {
@@ -649,11 +649,12 @@ export function writeHandoff({ projectDir, sessionId, transcript, trigger, summa
649
649
  else if (r.json && r.json.allow === false) return { skipped: true, reason: r.json.reason || "storm-guard", sinceSec: r.json.sinceSec };
650
650
  }
651
651
  if (!existsSync(HANDOFF_DIR)) mkdirSync(HANDOFF_DIR, { recursive: true });
652
- // #5648: an automatic digest must never recompose+supersede a FRESH model-authored handoff.
653
- // If one exists (<15min, unconsumed), point the baton at THAT and write nothing — the caller's
654
- // spawn path proceeds on the authored handoff exactly as if it had just written it.
652
+ // #5648: an automatic digest must never recompose+supersede a FRESH model-authored handoff —
653
+ // if one exists (<15min, unconsumed), point the baton at THAT and write nothing, so the caller's
654
+ // spawn path proceeds on the authored handoff exactly as if it had just written it. #8263: the
655
+ // defer keys on the TRIGGER — manual-skill/manual-baton IS the model's newer words and must land.
655
656
  const fresh = freshAuthoredHandoff(projectName);
656
- if (fresh) return { deferred: true, file: join(HANDOFF_DIR, `${fresh.id}.json`), record: fresh };
657
+ if (fresh && !MANUAL_TRIGGERS.includes(trigger)) return { deferred: true, file: join(HANDOFF_DIR, `${fresh.id}.json`), record: fresh };
657
658
  const stamp = nowSec() || Date.now();
658
659
  let gitStatus = "";
659
660
  try { gitStatus = execSync("git -C " + JSON.stringify(projectDir) + " status --short 2>/dev/null | head -30", { encoding: "utf8" }).trim(); } catch {}
@@ -887,6 +888,14 @@ export function pathsReadIn(transcriptPath, paths) {
887
888
  return { read, missed: want.filter(p => !read.includes(p)) };
888
889
  }
889
890
 
891
+ /** Markdown on stdin, or a bare `trantor handoff`? A heredoc is a TEMP FILE, not a FIFO, and an
892
+ * isFIFO()-only check discarded the model's handoff while printing "handoff armed" (#8459).
893
+ * Character devices stay out: that is the TTY and the /dev/null a hook runs with.
894
+ */
895
+ export function stdinCarriesMarkdown(fd = 0) {
896
+ try { const st = fstatSync(fd); return st.isFIFO() || st.isFile(); } catch { return false; }
897
+ }
898
+
890
899
  // The self-announcing fresh session command (single-quoted so it survives osascript→shell un-escaped).
891
900
  // Brevity is the point, but #8162 found it had become permission not to READ: the summary is injected
892
901
  // at SessionStart, so a 3-sentence recap is producible without opening a file. The order is now
package/hooks/pricing.mjs CHANGED
@@ -1,23 +1,31 @@
1
1
  // trantor — Anthropic API pricing, the single source of truth for the NOTIONAL dollar cost of a
2
2
  // Claude Code sub-agent's token usage (it's "notional" because on a Pro/Max plan those tokens are
3
3
  // plan-covered, not billed — we still want the number). $/MTok. Re-confirm before trusting old reports.
4
- export const PRICING_AS_OF = "2026-06";
4
+ export const PRICING_AS_OF = "2026-09";
5
5
 
6
6
  // in/out = input/output; cw5m/cw1h = cache-WRITE at the 5-minute / 1-hour TTL; cr = cache-READ.
7
7
  // (cache write 5m = 1.25× input, 1h = 2× input, read = 0.1× input — encoded directly here.)
8
8
  export const ANTHROPIC_PRICING = {
9
- opus: { in: 5, out: 25, cw5m: 6.25, cw1h: 10, cr: 0.5 },
10
- sonnet: { in: 3, out: 15, cw5m: 3.75, cw1h: 6, cr: 0.3 },
9
+ opus: { in: 5, out: 25, cw5m: 6.25, cw1h: 10, cr: 0.5 }, // Opus 5 / 4.8 / 4.7 / 4.6
10
+ "opus-5-5": { in: 4, out: 20, cw5m: 5, cw1h: 8, cr: 0.2 }, // cheaper than Opus 5, and its cache read is 0.05× input, not 0.1×
11
+ sonnet: { in: 2, out: 10, cw5m: 2.5, cw1h: 4, cr: 0.2 }, // Sonnet 5
12
+ "sonnet-4": { in: 3, out: 15, cw5m: 3.75, cw1h: 6, cr: 0.3 }, // Sonnet 4 / 4.5 / 4.6
11
13
  haiku: { in: 1, out: 5, cw5m: 1.25, cw1h: 2, cr: 0.1 },
12
- "opus-4-1": { in: 15, out: 75, cw5m: 18.75, cw1h: 30, cr: 1.5 }, // legacy numbering — far pricier
14
+ fable: { in: 10, out: 50, cw5m: 12.5, cw1h: 20, cr: 0.25 }, // Fable 5 / 5.1, cache read 0.025× input
15
+ "opus-4-1": { in: 15, out: 75, cw5m: 18.75, cw1h: 30, cr: 1.5 }, // legacy numbering — far pricier
13
16
  };
14
17
 
15
18
  // Map a transcript model id ("claude-opus-4-8", "claude-sonnet-4-6", …) to a price tier, or null.
19
+ // MOST SPECIFIC FIRST: a generic includes("opus") priced the default Opus 5.5 off the Opus 5 tier (#8443).
20
+ // An unrecognised version stays null — costUsd:null has a reporting path, a wrong dollar figure does not.
16
21
  export function tierFor(model) {
17
22
  const m = String(model || "").toLowerCase();
18
23
  if (/opus-4-1\b|opus-4\.1/.test(m)) return "opus-4-1";
19
- if (m.includes("opus")) return "opus";
20
- if (m.includes("sonnet")) return "sonnet";
24
+ if (/opus-5-5\b|opus-5\.5/.test(m)) return "opus-5-5";
25
+ if (/opus-(5|4-8|4-7|4-6|4-5)\b/.test(m)) return "opus";
26
+ if (/sonnet-4(-[0-9]+)?\b/.test(m)) return "sonnet-4";
27
+ if (/sonnet-5\b/.test(m)) return "sonnet";
28
+ if (m.includes("fable") || m.includes("mythos")) return "fable";
21
29
  if (m.includes("haiku")) return "haiku";
22
30
  return null; // unknown → caller emits costUsd:null + a costNote rather than guessing
23
31
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "trantor",
3
- "version": "0.18.68",
3
+ "version": "0.18.70",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "trantor": "bin/cli.mjs"