trantor 0.15.0

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/mcp.mjs ADDED
@@ -0,0 +1,156 @@
1
+ #!/usr/bin/env node
2
+ // agent-bus MCP server — gives ANY MCP-capable agent (Claude Code, Codex, Gemini, …)
3
+ // tools to talk to OTHER live agent sessions through the relay hub. Loaded per-session
4
+ // via the agent's MCP config. Identity + hub URL come from env (RELAY_SESSION, RELAY_URL).
5
+ // Loading this server AUTO-REGISTERS the session — so presence works on every agent.
6
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
7
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
8
+ import { readFileSync, writeFileSync, existsSync, mkdirSync } from "node:fs";
9
+ import { join, basename } from "node:path";
10
+ import { homedir, hostname } from "node:os";
11
+ import { execSync, spawnSync } from "node:child_process";
12
+ import { advise } from "./bin/advise.mjs";
13
+ import { z } from "zod";
14
+
15
+ function relayUrl() {
16
+ if (process.env.RELAY_URL) return process.env.RELAY_URL;
17
+ try {
18
+ const cfg = join(homedir(), ".agent-bus", "config.json");
19
+ if (existsSync(cfg)) { const u = JSON.parse(readFileSync(cfg, "utf8")).url; if (u) return u; }
20
+ } catch {}
21
+ return "http://127.0.0.1:4477";
22
+ }
23
+ const URL_BASE = relayUrl();
24
+ const PROJECT = process.env.RELAY_PROJECT || basename(process.env.CLAUDE_PROJECT_DIR || process.cwd());
25
+ // Identity: RELAY_SESSION wins; else RELAY_AGENT ("codex", "kimi", …) brands the session per-project
26
+ // (set it once in the CLI's global MCP config — works in every project); else hostname:project.
27
+ const SESSION = process.env.RELAY_SESSION
28
+ || (process.env.RELAY_AGENT ? `${process.env.RELAY_AGENT}:${PROJECT}` : `${hostname()}:${PROJECT}`);
29
+ let cursor = 0;
30
+
31
+ async function api(method, path, payload) {
32
+ const opts = { method, headers: { "content-type": "application/json" } };
33
+ if (payload) opts.body = JSON.stringify(payload);
34
+ const r = await fetch(URL_BASE + path, opts);
35
+ if (!r.ok) throw new Error(`hub ${r.status} on ${path}`);
36
+ return r.json();
37
+ }
38
+ const fmt = (m) => `#${m.id} [${m.from} -> ${m.to}] ${new Date(m.ts).toLocaleTimeString()}: ${m.text}`;
39
+
40
+ const server = new McpServer({ name: "agent-bus", version: "0.1.0" });
41
+
42
+ server.tool("relay_whoami", "Show this session's relay identity, project, and the hub URL.", {}, async () => {
43
+ await api("POST", "/register", { session: SESSION, project: PROJECT }).catch(() => {});
44
+ return { content: [{ type: "text", text: `session=${SESSION}\nproject=${PROJECT}\nhub=${URL_BASE}` }] };
45
+ });
46
+
47
+ server.tool("relay_task_add", "Add a Kanban card to THIS project's board on the dashboard (what you're about to work on). Defaults: assigned to you, status 'todo'. Keep the team's progress visible.",
48
+ { title: z.string().describe("short task title"), status: z.enum(["todo","doing","testing","failed","done","blocked"]).optional(), assignee: z.string().optional().describe("session id to assign (default: you)"), difficulty: z.enum(["easy","medium","hard"]).optional().describe("difficulty tag — drives model/agent routing (relay_advise) and shows on the board"), model: z.string().optional().describe("the model this card is routed to (from relay_advise routing, or the CLI default) — shown on the card"), deps: z.array(z.number()).optional().describe("card ids this card depends on — drawn as edges in the Flow view (e.g. integration depends on every crew card)") },
49
+ async ({ title, status, assignee, difficulty, model, deps }) => {
50
+ const { task } = await api("POST", "/task", { project: PROJECT, title, status: status || "todo", assignee: assignee || SESSION, difficulty, model, deps, by: SESSION });
51
+ return { content: [{ type: "text", text: `card #${task.id} added to ${PROJECT}: "${title}" [${task.status}]` }] };
52
+ });
53
+
54
+ server.tool("relay_task_move", "Move a Kanban card as you progress: todo -> doing -> testing -> done. NEVER move straight to done: move to 'testing' when you finish, run the project's tests/typecheck, then 'done' only if green — or 'failed' (with a relay_send explaining what broke) if not. The orchestrator bounces failed cards back to doing. blocked = waiting on something external.",
55
+ { id: z.number(), status: z.enum(["todo","doing","testing","failed","done","blocked"]) },
56
+ async ({ id, status }) => {
57
+ await api("POST", "/task/update", { id, status, by: SESSION });
58
+ return { content: [{ type: "text", text: `card #${id} -> ${status}` }] };
59
+ });
60
+
61
+ server.tool("relay_project_brief", "Set a one-paragraph brief for THIS project shown on the dashboard: what it is, why it matters, and the goal. Set it once when you start work so anyone watching the board understands the project at a glance (the board itself shows where it is in the process).",
62
+ { brief: z.string().describe("1-3 sentences: what this project is + why + the goal") },
63
+ async ({ brief }) => {
64
+ await api("POST", "/project", { project: PROJECT, brief, by: SESSION });
65
+ return { content: [{ type: "text", text: `brief set for ${PROJECT}` }] };
66
+ });
67
+
68
+ server.tool("relay_advise", "THE ADVISOR — ask the brain how to execute a body of work before spending tokens. Give it your work packages (with difficulty); it weighs task shape x the user's plan economics x context horizon and returns: mode (solo|scrooge|crew|hybrid), per-package executor+model routing, and a real-money estimate with quota-pool accounting. Call this at project kickoff and PRESENT the summary to the user before firing anything up.",
69
+ { task: z.string().describe("one-line description of the overall job"),
70
+ packages: z.array(z.object({ title: z.string(), difficulty: z.enum(["easy","medium","hard"]).optional(), kind: z.string().optional() })).describe("the work packages you'd cut as cards"),
71
+ horizon: z.enum(["short","medium","long"]).optional().describe("how long this build will run (default inferred from package count)") },
72
+ async ({ task, packages, horizon }) => {
73
+ const out = advise({ task, packages, horizon });
74
+ return { content: [{ type: "text", text: JSON.stringify(out, null, 2) }] };
75
+ });
76
+
77
+ server.tool("relay_scrooge", "Delegate a SMALL, SELF-CONTAINED piece of grunt work (draft a function, summarize, extract, classify, boilerplate) to a cheap external model via Scrooge — costs a fraction of a cent and keeps the result OUT of expensive context where possible. Returns the model's output plus the ledger receipt. Use for stateless one-shots; use the crew for anything stateful or large.",
78
+ { prompt: z.string().describe("the complete, self-contained task (include all needed context — the cheap model sees ONLY this)"),
79
+ task: z.string().optional().describe("scrooge task type: code, summarize, extract, draft, verify, reason, cheap (default code)"),
80
+ difficulty: z.enum(["easy","medium","hard"]).optional() },
81
+ async ({ prompt, task, difficulty }) => {
82
+ const r = spawnSync("scrooge", ["-t", task || "code", "-d", difficulty || "easy"], { input: prompt, encoding: "utf8", timeout: 120000, maxBuffer: 8 * 1024 * 1024 });
83
+ if (r.error || r.status !== 0) return { content: [{ type: "text", text: `scrooge failed: ${r.error?.message || r.stderr?.slice(-300) || "exit " + r.status}` }] };
84
+ const receipt = (r.stderr || "").split("\n").filter(l => l.includes("\u{1FA99}") || l.includes("scrooge")).slice(-1)[0] || "";
85
+ return { content: [{ type: "text", text: `${r.stdout.trim()}\n\n[receipt] ${receipt.trim()}` }] };
86
+ });
87
+
88
+ server.tool("relay_lesson", "Record a LESSON learned from a failure so future crews avoid it — injected into agents' kickoff prompts automatically. Use when you diagnose a recurring or preventable failure. scope: 'global' (applies to every agent) or an agent brand ('kimi','codex','gemini','deepseek') when it's that CLI's quirk.",
89
+ { text: z.string().describe("one-line imperative guardrail, e.g. 'never move a card to done without npm test passing'"), scope: z.string().optional().describe("'global' (default) or an agent brand") },
90
+ async ({ text, scope }) => {
91
+ const r = await api("POST", "/lesson", { text, scope: scope || "global", by: SESSION });
92
+ return { content: [{ type: "text", text: r.dedup ? "lesson already recorded" : `lesson recorded (${r.count} total)` }] };
93
+ });
94
+
95
+ server.tool("relay_board", "Show THIS project's Kanban board (all cards + their status + assignee).", {}, async () => {
96
+ const { tasks } = await api("GET", `/tasks?project=${encodeURIComponent(PROJECT)}`);
97
+ if (!tasks.length) return { content: [{ type: "text", text: `${PROJECT}: no cards yet` }] };
98
+ const by = { todo: [], doing: [], testing: [], failed: [], done: [], blocked: [] };
99
+ for (const t of tasks) (by[t.status] || by.todo).push(`#${t.id} ${t.title}${t.assignee ? ` (@${t.assignee})` : ""}`);
100
+ const cols = Object.entries(by).filter(([, v]) => v.length).map(([k, v]) => `${k.toUpperCase()}:\n ${v.join("\n ")}`);
101
+ return { content: [{ type: "text", text: `${PROJECT} board\n${cols.join("\n")}` }] };
102
+ });
103
+
104
+ server.tool("relay_peers", "List other Claude sessions connected to the relay (online in last 5 min).", {}, async () => {
105
+ const { peers } = await api("GET", "/peers");
106
+ const lines = peers.map(p => `${p.online ? "🟢" : "⚪"} ${p.session}${p.session === SESSION ? " (you)" : ""}`);
107
+ return { content: [{ type: "text", text: lines.join("\n") || "no peers yet" }] };
108
+ });
109
+
110
+ server.tool("relay_send", "Send a live message to another Claude session (or 'all' to broadcast).",
111
+ { to: z.string().describe("target session id, or 'all'"), text: z.string().describe("message body") },
112
+ async ({ to, text }) => {
113
+ const { id } = await api("POST", "/send", { from: SESSION, to, text });
114
+ return { content: [{ type: "text", text: `sent #${id} to ${to}` }] };
115
+ });
116
+
117
+ server.tool("relay_status", "Set this session's one-line status on the presence board (what you're working on / idle). Cheap — other sessions read it instantly via relay_peers without messaging you.",
118
+ { status: z.string().describe("short status, e.g. 'building auth in crebral' or 'idle'") },
119
+ async ({ status }) => {
120
+ await api("POST", "/status", { session: SESSION, status, project: PROJECT });
121
+ return { content: [{ type: "text", text: `status set: ${status}` }] };
122
+ });
123
+
124
+ server.tool("relay_handoff", "Write a rich handoff for THIS session so a fresh session (any agent) can take over with a full context window instead of compacting. Provide a complete markdown summary (TASK / STATE / KEY DECISIONS / NEXT STEPS / KEY FILES). Universal — works in any agent, not just Claude's PreCompact hook.",
125
+ { summary: z.string().describe("complete markdown handoff: TASK, STATE, KEY DECISIONS, NEXT STEPS, KEY FILES & locations") },
126
+ async ({ summary }) => {
127
+ const project = process.env.CLAUDE_PROJECT_DIR || process.cwd();
128
+ const name = basename(project);
129
+ const dir = join(homedir(), ".agent-bus", "handoffs");
130
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
131
+ const stamp = (() => { try { return execSync("date +%s", { encoding: "utf8" }).trim(); } catch { return String(process.pid); } })();
132
+ let git = ""; try { git = execSync("git -C " + JSON.stringify(project) + " status --short 2>/dev/null | head -30", { encoding: "utf8" }).trim(); } catch {}
133
+ const rec = { id: `${name}-${stamp}`, project, projectName: name, machine: hostname(), trigger: "relay_handoff-tool", stamp: Number(stamp) || 0, summary: String(summary), gitStatus: git, consumed: false };
134
+ writeFileSync(join(dir, `${rec.id}.json`), JSON.stringify(rec, null, 2));
135
+ await api("POST", "/send", { from: SESSION, to: "all", text: `📋 Handoff ready for ${name} — open a fresh session here to take over (${rec.id}).` }).catch(() => {});
136
+ return { content: [{ type: "text", text: `handoff saved (${rec.id}). A fresh session in ${name} will load it on start. Tell the user to open a new terminal here.` }] };
137
+ });
138
+
139
+ server.tool("relay_inbox", "Read NEW messages addressed to this session since the last read (non-blocking).", {}, async () => {
140
+ const { messages, cursor: c } = await api("GET", `/inbox?session=${encodeURIComponent(SESSION)}&since=${cursor}`);
141
+ cursor = c;
142
+ return { content: [{ type: "text", text: messages.length ? messages.map(fmt).join("\n") : "(no new messages)" }] };
143
+ });
144
+
145
+ server.tool("relay_wait", "Block up to `timeout` seconds waiting for the next message to this session (long-poll). Returns the instant a message arrives. When idle, park by calling this repeatedly. IMPORTANT: some MCP clients cap tool calls (Codex ~120s, OpenCode ~60s) — use timeout 50 and loop, unless you know your client allows more (Claude Code handles 280).",
146
+ { timeout: z.number().optional().describe("seconds to wait, default 25, max 280. Use 50 and call repeatedly for cross-client safety; only Claude Code reliably supports 280.") },
147
+ async ({ timeout }) => {
148
+ const w = Math.min(timeout ?? 25, 280);
149
+ const { messages, cursor: c } = await api("GET", `/poll?session=${encodeURIComponent(SESSION)}&since=${cursor}&wait=${w}`);
150
+ cursor = c;
151
+ return { content: [{ type: "text", text: messages.length ? messages.map(fmt).join("\n") : "(timed out, no message)" }] };
152
+ });
153
+
154
+ await api("POST", "/register", { session: SESSION, project: PROJECT, status: `active in ${PROJECT}` }).catch(() => {});
155
+ await server.connect(new StdioServerTransport());
156
+ process.stderr.write(`[agent-bus-mcp] connected as ${SESSION} -> ${URL_BASE}\n`);
package/package.json ADDED
@@ -0,0 +1,54 @@
1
+ {
2
+ "name": "trantor",
3
+ "version": "0.15.0",
4
+ "type": "module",
5
+ "bin": {
6
+ "trantor": "bin/cli.mjs"
7
+ },
8
+ "dependencies": {
9
+ "@modelcontextprotocol/sdk": "^1.29.0",
10
+ "zod": "^4.4.3"
11
+ },
12
+ "scripts": {
13
+ "test": "node test.mjs && node test-scenarios.mjs"
14
+ },
15
+ "description": "The hub-world for AI agent crews \u2014 orchestrate Claude Code, Codex, Gemini, Kimi & DeepSeek as live crews with a plan-aware Advisor, a Kanban/flow command center, a testing gate, and an economics brain (Scrooge).",
16
+ "files": [
17
+ "hub.mjs",
18
+ "mcp.mjs",
19
+ "ui.html",
20
+ "bin/",
21
+ "hooks/",
22
+ "skills/",
23
+ "deploy/",
24
+ "configs/",
25
+ ".claude-plugin/",
26
+ "README.md",
27
+ "LICENSE"
28
+ ],
29
+ "repository": {
30
+ "type": "git",
31
+ "url": "git+https://github.com/sashabogi/trantor.git"
32
+ },
33
+ "homepage": "https://github.com/sashabogi/trantor#readme",
34
+ "bugs": {
35
+ "url": "https://github.com/sashabogi/trantor/issues"
36
+ },
37
+ "keywords": [
38
+ "ai-agents",
39
+ "multi-agent",
40
+ "orchestration",
41
+ "claude-code",
42
+ "codex",
43
+ "gemini",
44
+ "mcp",
45
+ "kanban",
46
+ "agent-crew",
47
+ "llm-routing"
48
+ ],
49
+ "author": "Sasha Bogojevic <hello@hivedigitalllc.com>",
50
+ "license": "MIT",
51
+ "engines": {
52
+ "node": ">=18"
53
+ }
54
+ }
@@ -0,0 +1,82 @@
1
+ ---
2
+ name: crew
3
+ description: Orchestrate a multi-agent build over agent-bus — get an Advisor recommendation (solo/scrooge/crew/hybrid based on the user's plans and the work), fire up helper AI CLIs (Codex, Gemini, Kimi, DeepSeek) with pinned models in visible terminal windows, assign difficulty-tagged work over the bus, track it on the Kanban dashboard with a testing gate, delegate grunt to Scrooge, supervise actively, integrate, ship. Use when the user wants several AI agents building something together, says "fire up the crew/agents", or asks to coordinate other coding CLIs on a task.
4
+ ---
5
+
6
+ # agent-bus crew — the unified playbook (brain × body)
7
+
8
+ You are the ARCHITECT. Two execution fabrics serve you:
9
+ - **Scrooge calls** (`relay_scrooge`) — cheap stateless one-shots; the result returns to you.
10
+ - **Crew members** — stateful colleagues in their own context windows under a runner that
11
+ keeps them alive forever and wakes them on bus messages. They report by reference.
12
+
13
+ **Decision rule:** small + stateless + result-fits-inline → `relay_scrooge`.
14
+ Large + stateful + parallel or long-horizon → crew member. You stay a foreman either way:
15
+ your context burns at coordination rate, never work rate.
16
+
17
+ ## Phase −1 — THE ADVISOR MOMENT (always, before spending anything)
18
+ Cut the work into packages, tag each `easy|medium|hard`, then call
19
+ `relay_advise(task, packages, horizon)`. It weighs task shape × the user's declared plan
20
+ economics (quota profile) × context horizon and returns mode + per-package routing — each
21
+ route carries a `reason`, and `crew.why` explains the SEAT COUNT (seats follow the work,
22
+ not the install list). Mark packages you'll own yourself with `owner:"self"` (foundation/
23
+ integration are auto-reserved). **Never present a bare go/no-go.** In your TEXT REPLY (not
24
+ inside a question dialog), paste verbatim: `routing_table_md`, the `why` bullets, `crew.why`,
25
+ and the real-money total + quota pools. ONLY THEN ask go / adjust / hold. When creating the
26
+ board, use `card_args` exactly — each entry is a ready `relay_task_add` call (title,
27
+ difficulty, assignee with your project substituted, model). Cards without their model set
28
+ are a defect. If the profile is unset, say so and suggest `node bin/profile.mjs set …`.
29
+
30
+ ## Phase 0 — plan (if the user wants a plan first)
31
+ PRD.md + TDD.md. The TDD MUST define one file-set per agent (no merge conflicts) and an
32
+ explicit EVENT/INTERFACE CONTRACT — cross-agent bugs come from contract drift.
33
+
34
+ ## Phase 1 — board setup
35
+ 1. `relay_project_brief("<what + why + goal>")`
36
+ 2. One card per package: `relay_task_add(title, assignee, difficulty, model)` — set `model`
37
+ to the advisor-routed model (or the CLI's default name); difficulty + model show as badges
38
+ on the card. Assignees: `codex:<project>` etc. Keep one for yourself.
39
+ 3. Open the dashboard: `open -na "Google Chrome" --args --new-window <hub-url>`
40
+
41
+ ## Phase 2 — fire up the crew (with the Advisor's models)
42
+ `bash <plugin-root>/bin/crew.sh up codex:gpt-5.5 gemini kimi deepseek:deepseek-v4-pro`
43
+ — `agent:model` pins a model (omit to use that CLI's default; use what relay_advise routed).
44
+ The launcher auto-wires configs, spawns serialized runner windows, then **VERIFIES each agent
45
+ on the bus with one retry**. READ ITS OUTPUT: it ends "crew verified" or "✗✗ CREW INCOMPLETE"
46
+ naming no-shows. **Never assign work to an unverified agent.** The bus is the truth.
47
+
48
+ ## Phase 3 — contracts over the bus
49
+ Build the shared foundation yourself first, then `relay_send` each agent its contract
50
+ (<280 chars): file(s) owned, the interface contract verbatim, the spec. NOTE the wake-policy:
51
+ plain broadcasts do NOT wake crew members (they batch as context) — to wake one, send a
52
+ direct message or @mention it (`@codex …`) in a broadcast.
53
+
54
+ ## Phase 4 — SUPERVISE ACTIVELY (never wait passively)
55
+ Loop until the board is done — you are a foreman, not a mailbox:
56
+ 1. `relay_wait(120)` (long waits are safe for YOU only; crew runners handle their own waiting).
57
+ 2. EVERY wake or timeout, SWEEP: `relay_board` (stale cards? `failed` pulsing?), `relay_peers`
58
+ (assignee lastSeen fresh? runner heartbeats keep live agents fresh in seconds — stale =
59
+ dead), spot-check files on disk.
60
+ 3. ACT within one cycle: failed card → read the report, send a fix contract, card back to
61
+ doing · dead agent → `crew.sh up <agent>` (re-verifies) + resend contract · silent-but-alive
62
+ → direct-message nudge naming the card.
63
+ 4. Grunt sub-tasks that appear mid-build (a regex, a config block, a doc paragraph) →
64
+ `relay_scrooge`, don't burn a crew seat or your own window.
65
+ 5. Record lessons as you diagnose (`relay_lesson(text, scope)` — global or per-agent quirks);
66
+ they auto-inject into every future crew's prompts.
67
+
68
+ ## Phase 5 — verification gate + integration
69
+ Card flow is `todo → doing → testing → done`; `testing` runs the project's tests/typecheck;
70
+ `done` only green; `failed` (+ bus report) pulses red on the board until you bounce it.
71
+ Enforce the gate — bounce anything that skipped it (bounces are visible: "↩ bounced" on the
72
+ card, history in its tooltip). When all report done: integrate, fix contract mismatches
73
+ YOURSELF, move your card through testing → done, broadcast "🚀 <thing> is live", and when the
74
+ user is finished: `bash .../crew.sh down`.
75
+
76
+ ## Rules
77
+ - Coordinate ONLY over the bus; messages <280 chars; the dashboard lanes are the user's view.
78
+ - Never edit a crew member's file unless integration is broken AND they're dead/silent.
79
+ - Trust the verifier, the sweep, and the gate — not assumptions or optimistic reports.
80
+ - If a CLI fails (auth/model/quota), tell the user plainly and continue with the rest.
81
+ - Telemetry for cost reporting lands in `~/.agent-bus/logs/` automatically; the dashboard's
82
+ 🪙 pill shows live Scrooge spend/savings.
@@ -0,0 +1,36 @@
1
+ ---
2
+ name: relay-handoff
3
+ description: |
4
+ Write a rich handoff for the CURRENT session so a fresh Claude Code session can take over
5
+ with a full new context window (instead of compacting). Use proactively when context is
6
+ getting full, or before ending, to pass the baton cleanly. Trigger: /agent-bus:relay-handoff
7
+ user-invocable: true
8
+ ---
9
+
10
+ # Relay Handoff — pass the baton to a fresh session
11
+
12
+ Write a complete handoff capturing everything a NEW session needs to continue this work without
13
+ re-deriving context, and save it so the next session in this project auto-loads it on start.
14
+
15
+ ## Instructions
16
+
17
+ 1. Compose a thorough markdown handoff for the current task with these sections (be specific —
18
+ exact file paths, concrete next actions; the successor has a fresh window and only this):
19
+ - **TASK** — what we're doing and the goal
20
+ - **STATE** — what's done, what's in progress
21
+ - **KEY DECISIONS** — choices made and why
22
+ - **OPEN THREADS & NEXT STEPS** — the concrete actions to do next, in order
23
+ - **KEY FILES & LOCATIONS** — exact paths, commands, URLs, IDs the successor needs
24
+ - **GOTCHAS** — anything that will bite if forgotten
25
+
26
+ 2. Save it by piping the markdown to the helper:
27
+ ```bash
28
+ cat << 'HANDOFF' | node "$(dirname "$(command -v claude)")/../<plugin>/bin/write-handoff.mjs"
29
+ <your handoff markdown>
30
+ HANDOFF
31
+ ```
32
+ (Or call the plugin's `bin/write-handoff.mjs` directly via its `${CLAUDE_PLUGIN_ROOT}`.)
33
+
34
+ 3. Tell the user: open a fresh terminal + `claude` in this same project directory — the
35
+ SessionStart hook will detect the handoff and the new session takes over with a full window.
36
+ (The PreCompact hook also writes one automatically at the compaction threshold.)