trantor 0.18.102 → 0.18.104
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/.claude-plugin/plugin.json +1 -1
- package/bin/crew/core.mjs +4 -1
- package/bin/crew-runner.mjs +9 -4
- package/lib/seat-compaction.mjs +39 -0
- package/mcp.mjs +46 -46
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "trantor",
|
|
3
|
-
"version": "0.18.
|
|
3
|
+
"version": "0.18.104",
|
|
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/crew/core.mjs
CHANGED
|
@@ -4,6 +4,9 @@ import { spawnSync } from "node:child_process";
|
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
import { STATE_FLAGS } from "../../lib/state/flags.mjs";
|
|
6
6
|
|
|
7
|
+
// the card limits the runner reads (#11559 budget, #11855 context cap), tunable at `trantor up`
|
|
8
|
+
export const RUNNER_LIMIT_FLAGS = ["TRANTOR_CARD_TOKEN_BUDGET", "TRANTOR_CARD_CONTEXT_CAP", "TRANTOR_CARD_CAP_MIN_STEPS", "TRANTOR_CARD_CAP_MAX_HANDOFFS"];
|
|
9
|
+
|
|
7
10
|
export const ROOT = dirname(dirname(dirname(fileURLToPath(import.meta.url))));
|
|
8
11
|
|
|
9
12
|
export function commandExists(name, env = process.env) {
|
|
@@ -149,7 +152,7 @@ export function gridColumns(size) {
|
|
|
149
152
|
// in lib/state/flags.mjs beside the resolver: the launcher env stays the forwarder (the runner boots
|
|
150
153
|
// before any file is sourced), and #7159 lets the runner fall back to ~/.agent-bus/.env itself, so a
|
|
151
154
|
// flag documented there finally arms the seat that reads it.
|
|
152
|
-
const FORWARDED_ENV = STATE_FLAGS;
|
|
155
|
+
const FORWARDED_ENV = [...STATE_FLAGS, ...RUNNER_LIMIT_FLAGS];
|
|
153
156
|
|
|
154
157
|
export function runnerCommand(ctx, agent, model = "", effort = null, persona = agent) {
|
|
155
158
|
const forwarded = FORWARDED_ENV
|
package/bin/crew-runner.mjs
CHANGED
|
@@ -30,7 +30,7 @@ import {
|
|
|
30
30
|
import { snapshotBeforeCut } from "./cut-snapshot.mjs";
|
|
31
31
|
import { ocTurnUsage, dshTurnUsage, usageTotal, processedInput, codexStepInput, kimiStepInput, claudeStepInput } from "../lib/turn-usage.mjs";
|
|
32
32
|
import { capConfig, capDecision, capHandoffPrompt, capPointerLine, capExhaustedNote } from "../lib/context-cap.mjs";
|
|
33
|
-
import { ocCompactionContent, ocCompactionLogLine } from "../lib/seat-compaction.mjs";
|
|
33
|
+
import { ocCompactionContent, ocCompactionLogLine, readOpencodeUserConfig, seatMcpOverlay, seatConfigContent } from "../lib/seat-compaction.mjs";
|
|
34
34
|
import { seatSignalPrefix } from "../lib/seat-signal-guard.mjs";
|
|
35
35
|
import { listContainers, newContainers, recordContainers, statePathFor as dockerStatePathFor, sweep as dockerSweep } from "../lib/docker-janitor.mjs";
|
|
36
36
|
import {
|
|
@@ -250,7 +250,9 @@ const CLI = {
|
|
|
250
250
|
// kimi-code (successor to kimi-cli) has no --print (-p alone is non-interactive), REJECTS
|
|
251
251
|
// --yolo in prompt mode (prompt mode auto-approves tools), and emits session_-prefixed ids.
|
|
252
252
|
kimi: { first: `kimi{M} -p "$(cat {P})" < /dev/null`,
|
|
253
|
-
|
|
253
|
+
// -S resumes; kimi-code 0.34 silently ignores -r (its own exit hint still prints -r) and
|
|
254
|
+
// opens a new session, so every kimi turn ran cold with no history
|
|
255
|
+
next: `kimi{M} -S {SID} -p "$(cat {P})" < /dev/null`, mflag: " --model ", sid: /To resume this session:\s*kimi (?:-r|-S|--session) (\S+)/ },
|
|
254
256
|
// #6154: `run -c` resumes the globally last session on this machine, any project's, and its stored
|
|
255
257
|
// directory becomes the path root. So every spawn pins --dir and a resume pins -s (ocSid below).
|
|
256
258
|
deepseek: { first: `opencode run --dir {DIR}{M} "$(cat {P})"`,
|
|
@@ -784,6 +786,8 @@ function capStepProcessed(sinceMs) {
|
|
|
784
786
|
// core/src/session/compaction.ts) — so reserved is sized from the seat model's catalog window.
|
|
785
787
|
const OC_COMPACTION_WINDOW = contextWindow(MODEL);
|
|
786
788
|
const OC_COMPACTION = ocCompactionContent(process.env, OC_COMPACTION_WINDOW);
|
|
789
|
+
// #11854: plus every non-relay MCP server of the user's opencode config switched off for the seat.
|
|
790
|
+
const OC_SEAT_CONFIG = seatConfigContent(OC_COMPACTION, seatMcpOverlay(readOpencodeUserConfig()));
|
|
787
791
|
|
|
788
792
|
let sid = releaseState?.sid || "";
|
|
789
793
|
// #6206: the watchdog is DETACHED, so a runner that dies without ending it leaves an orphan
|
|
@@ -985,7 +989,7 @@ exit $turn_exit`;
|
|
|
985
989
|
// #11559: opencode only compacts when the context is FULL, which on big-limit models is far
|
|
986
990
|
// past the point a seat card should reach. The merged-content injection arms compaction per
|
|
987
991
|
// seat (schema: opencode.ai/config.json) without touching the operator's global config.
|
|
988
|
-
if (cli.pinned &&
|
|
992
|
+
if (cli.pinned && OC_SEAT_CONFIG) spawnEnv.OPENCODE_CONFIG_CONTENT = OC_SEAT_CONFIG;
|
|
989
993
|
spawnOpts.env = spawnEnv;
|
|
990
994
|
spawnOpts.maxBuffer = 16 * 1024 * 1024;
|
|
991
995
|
// The timeout signals the shell to snapshot before sweeping, just like the normal box.
|
|
@@ -1049,7 +1053,8 @@ exit $turn_exit`;
|
|
|
1049
1053
|
if (env.turn) log(`state step: ${describeTurn(env.turn)} · ${costLine(env.cost)}`);
|
|
1050
1054
|
else log(`\x1b[33mstate step: no TurnResult — ${env.error}\x1b[0m`);
|
|
1051
1055
|
}
|
|
1052
|
-
|
|
1056
|
+
// kimi prints its resume hint on stderr, so a stdout-only match never captured its sid
|
|
1057
|
+
if (cli.sid) { const all = [...`${r.stdout || ""}\n${ownOut}`.matchAll(new RegExp(cli.sid.source, "g"))]; if (all.length) sid = all.at(-1)[1]; }
|
|
1053
1058
|
// #6154: the opencode family prints no sid on stdout — the id comes from opencode's own DB,
|
|
1054
1059
|
// keyed by the worktree the session was created in. Fail-open: nothing found leaves sid empty,
|
|
1055
1060
|
// and the next turn starts fresh rather than resuming whatever other project ran last.
|
package/lib/seat-compaction.mjs
CHANGED
|
@@ -3,6 +3,10 @@
|
|
|
3
3
|
// additionalProperties:false; calculateCeiling in packages/core/src/session/compaction.ts). So the
|
|
4
4
|
// seat target "compact near 100K" is reached by sizing reserved from the model's window.
|
|
5
5
|
|
|
6
|
+
import { readFileSync } from "node:fs";
|
|
7
|
+
import { homedir } from "node:os";
|
|
8
|
+
import { join } from "node:path";
|
|
9
|
+
|
|
6
10
|
export const OC_COMPACTION_ENV = "TRANTOR_OC_COMPACTION";
|
|
7
11
|
// The context level a seat card should stay near: on a 1M-window model reserved becomes 900K
|
|
8
12
|
// so auto-compact fires at ~100K, not the ~936K a fixed 64K buffer leaves.
|
|
@@ -49,6 +53,41 @@ export function ocCompactionContent(env = process.env, window = undefined) {
|
|
|
49
53
|
return JSON.stringify({ compaction: parsed });
|
|
50
54
|
}
|
|
51
55
|
|
|
56
|
+
export const SEAT_MCP_ENV = "TRANTOR_SEAT_MCP";
|
|
57
|
+
|
|
58
|
+
// The user's opencode config object, or null: $OPENCODE_CONFIG else ~/.config/opencode/opencode.json
|
|
59
|
+
// (then .jsonc with whole-line // comments stripped). Fails open on anything unreadable.
|
|
60
|
+
export function readOpencodeUserConfig(env = process.env, home = homedir()) {
|
|
61
|
+
const dir = join(home, ".config", "opencode");
|
|
62
|
+
const files = env.OPENCODE_CONFIG ? [env.OPENCODE_CONFIG] : [join(dir, "opencode.json"), join(dir, "opencode.jsonc")];
|
|
63
|
+
for (const f of files) {
|
|
64
|
+
try {
|
|
65
|
+
const raw = readFileSync(f, "utf8");
|
|
66
|
+
return JSON.parse(f.endsWith(".jsonc") ? raw.replace(/^\s*\/\/.*$/gm, "") : raw);
|
|
67
|
+
} catch {}
|
|
68
|
+
}
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// #11854: a seat only needs the relay server; every other MCP server in the user's config pays its
|
|
73
|
+
// tool definitions on every model step. Returns { mcp: { name: { enabled: false } } } or null.
|
|
74
|
+
export function seatMcpOverlay(userConfig, env = process.env) {
|
|
75
|
+
if (String(env[SEAT_MCP_ENV] || "").trim() === "all") return null;
|
|
76
|
+
const mcp = userConfig?.mcp;
|
|
77
|
+
// oxlint-disable-next-line anti-slop/no-runtime-typeof
|
|
78
|
+
if (!mcp || typeof mcp !== "object" || Array.isArray(mcp)) return null;
|
|
79
|
+
const off = Object.keys(mcp).filter((n) => n !== "relay");
|
|
80
|
+
return off.length ? { mcp: Object.fromEntries(off.map((n) => [n, { enabled: false }])) } : null;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// The OPENCODE_CONFIG_CONTENT for an opencode seat: compaction JSON plus the MCP overlay, "" if neither.
|
|
84
|
+
export function seatConfigContent(compactionJson, mcpOverlay) {
|
|
85
|
+
if (!mcpOverlay) return compactionJson;
|
|
86
|
+
let base = {};
|
|
87
|
+
try { base = compactionJson ? JSON.parse(compactionJson) : {}; } catch {}
|
|
88
|
+
return JSON.stringify({ ...base, ...mcpOverlay });
|
|
89
|
+
}
|
|
90
|
+
|
|
52
91
|
// The one per-turn runner line: what the injected block will actually do for this seat.
|
|
53
92
|
export function ocCompactionLogLine(window = undefined) {
|
|
54
93
|
const reserved = reservedForContext(window);
|
package/mcp.mjs
CHANGED
|
@@ -109,7 +109,7 @@ server.tool("relay_whoami", "Show this session's relay identity, project, hub UR
|
|
|
109
109
|
return { content: [{ type: "text", text }] };
|
|
110
110
|
});
|
|
111
111
|
|
|
112
|
-
server.tool("relay_contracts", "What you dispatched and are still owed
|
|
112
|
+
server.tool("relay_contracts", "What you dispatched and are still owed: each direct message you sent, its age, whether the assignee is alive, and a disposition. WAITING (alive, working), STALLED (offline or overdue: poke, swap or reassign), ABANDONED (gone for good: reassign), SUPERSEDED (they answered a newer contract: no action), or answered with the outcome. Call before concluding a crew is idle, stuck or done; bus silence is not evidence.", {},
|
|
113
113
|
async () => {
|
|
114
114
|
let r;
|
|
115
115
|
try { r = await api("GET", `/contracts?session=${encodeURIComponent(SESSION)}&project=${encodeURIComponent(PROJECT)}`); }
|
|
@@ -159,23 +159,23 @@ server.tool("relay_contracts", "What you dispatched and are still owed. Lists ev
|
|
|
159
159
|
return { content: [{ type: "text", text }] };
|
|
160
160
|
});
|
|
161
161
|
|
|
162
|
-
server.tool("relay_task_add", "Add a Kanban card
|
|
163
|
-
{ title: z.string()
|
|
162
|
+
server.tool("relay_task_add", "Add a Kanban card (defaults: this project, assigned to you, status todo). `project` targets another board. Attach a `note` when the title alone lacks context; it lands on the card's log (last 40 kept).",
|
|
163
|
+
{ title: z.string(), status: z.enum(["todo","doing","testing","failed","done","blocked"]).optional(), assignee: z.string().optional(), difficulty: z.enum(["easy","medium","hard"]).optional(), model: z.string().optional(), deps: z.array(z.number()).optional(), phase: z.string().optional(), note: z.string().max(2000).optional(), project: z.string().optional(), checklist: z.array(z.string().max(200)).max(20).optional().describe("acceptance items; tick with relay_task_check only when truly met"), drill: z.string().max(300).optional().describe("the DRILL: what a person does on the built artifact and must see, not a test command. The hub refuses done without a drill line (this field, a checklist item or a note starting 'Drill:')") },
|
|
164
164
|
async ({ title, status, assignee, difficulty, model, deps, phase, note, project, checklist, drill }) => {
|
|
165
165
|
const proj = project || PROJECT;
|
|
166
166
|
const { task } = await api("POST", "/task", { project: proj, title, status: status || "todo", assignee: assignee || SESSION, difficulty, model, deps, phase, note, checklist, drill, by: SESSION });
|
|
167
167
|
return { content: [{ type: "text", text: `card #${task.id} added to ${proj}: "${title}" [${task.status}]${phase?` · phase ${phase}`:""}${task.checklist?.length?` · ${task.checklist.length} acceptance item(s)`:""}${task.drill ? " · drill set" : " · NO DRILL — the hub will refuse done until one is set"}` }] };
|
|
168
168
|
});
|
|
169
169
|
|
|
170
|
-
server.tool("relay_task_check", "Tick (or untick)
|
|
171
|
-
{ id: z.number().describe("card id"), index: z.number().int().min(0)
|
|
170
|
+
server.tool("relay_task_check", "Tick (or untick) one checklist item on a card. Tick only when genuinely met (tests run, behavior observed), never to move the progress bar. Items are 0-indexed in relay_task_add order.",
|
|
171
|
+
{ id: z.number().describe("card id"), index: z.number().int().min(0), done: z.boolean().optional() },
|
|
172
172
|
async ({ id, index, done }) => {
|
|
173
173
|
const { task } = await api("POST", "/task/checklist-toggle", { id, index, done: done !== false, by: SESSION });
|
|
174
174
|
const n = task.checklist.filter(c => c.done).length;
|
|
175
175
|
return { content: [{ type: "text", text: `card #${id} checklist: [${done !== false ? "x" : " "}] "${task.checklist[index].text}" — ${n}/${task.checklist.length} done` }] };
|
|
176
176
|
});
|
|
177
177
|
|
|
178
|
-
server.tool("relay_phase_goal", "Set
|
|
178
|
+
server.tool("relay_phase_goal", "Set a phase's goal, shown as its header in the Flow view. Set it when you plan a phase. Phase keys match relay_task_add's `phase`.",
|
|
179
179
|
{ phase: z.string().describe("the phase key (e.g. 'P5', 'Auth', 'Launch')"), goal: z.string().describe("1-2 sentences: what this phase delivers + done-criteria"), project: z.string().optional().describe("board (default: this session's project)") },
|
|
180
180
|
async ({ phase, goal, project }) => {
|
|
181
181
|
const proj = project || PROJECT;
|
|
@@ -183,8 +183,8 @@ server.tool("relay_phase_goal", "Set what a PHASE is for — its goal — shown
|
|
|
183
183
|
return { content: [{ type: "text", text: `phase "${phase}" goal set for ${proj}` }] };
|
|
184
184
|
});
|
|
185
185
|
|
|
186
|
-
server.tool("relay_task_move", "Move a
|
|
187
|
-
{ id: z.number(), status: z.enum(["todo","doing","testing","failed","done","blocked"]), note: z.string().max(2000).optional().describe("card-log entry
|
|
186
|
+
server.tool("relay_task_move", "Move a card: todo -> doing -> testing -> done. Never go straight to done: move to 'testing' when finished, run the tests/typecheck, then 'done' only if green, else 'failed' (relay_send what broke). blocked = waiting on something external. A move to testing or done MUST carry a `note`: what changed plus the evidence (command, pass counts).",
|
|
187
|
+
{ id: z.number(), status: z.enum(["todo","doing","testing","failed","done","blocked"]), note: z.string().max(2000).optional().describe("card-log entry; REQUIRED on testing/done"), drill: z.string().max(300).optional().describe("set the drill line in this call; done is refused without one") },
|
|
188
188
|
async ({ id, status, note, drill }) => {
|
|
189
189
|
// #7750: seat-side hollow-move check. The hub cannot see this worktree, so the evidence is
|
|
190
190
|
// gathered HERE at report time; a hollow move still lands (never blocks a no-code card), the
|
|
@@ -224,7 +224,7 @@ server.tool("relay_task_move", "Move a Kanban card as you progress: todo -> doin
|
|
|
224
224
|
return { content: [{ type: "text", text: `card #${id} -> ${status}` }] };
|
|
225
225
|
});
|
|
226
226
|
|
|
227
|
-
server.tool("relay_project_brief", "Set a one-paragraph brief
|
|
227
|
+
server.tool("relay_project_brief", "Set a one-paragraph project brief shown on the dashboard: what it is, why it matters, the goal. Defaults to this project; set it once when you start work.",
|
|
228
228
|
{ brief: z.string().describe("1-3 sentences: what this project is + why + the goal"), project: z.string().optional().describe("board to brief (default: this session's project)") },
|
|
229
229
|
async ({ brief, project }) => {
|
|
230
230
|
const proj = project || PROJECT;
|
|
@@ -232,10 +232,10 @@ server.tool("relay_project_brief", "Set a one-paragraph brief for a project show
|
|
|
232
232
|
return { content: [{ type: "text", text: `brief set for ${proj}` }] };
|
|
233
233
|
});
|
|
234
234
|
|
|
235
|
-
server.tool("relay_advise", "
|
|
236
|
-
{ task: z.string()
|
|
237
|
-
packages: z.array(z.object({ title: z.string(), difficulty: z.enum(["easy","medium","hard"]).optional(), kind: z.string().optional() }))
|
|
238
|
-
horizon: z.enum(["short","medium","long"]).optional()
|
|
235
|
+
server.tool("relay_advise", "Ask how to execute a body of work before spending tokens. Returns mode (solo|scrooge|crew|hybrid), per-package executor+model routing and a cost estimate. Call at project kickoff and present the summary to the user before starting anything.",
|
|
236
|
+
{ task: z.string(),
|
|
237
|
+
packages: z.array(z.object({ title: z.string(), difficulty: z.enum(["easy","medium","hard"]).optional(), kind: z.string().optional() })),
|
|
238
|
+
horizon: z.enum(["short","medium","long"]).optional() },
|
|
239
239
|
async ({ task, packages, horizon }) => {
|
|
240
240
|
// #7762: the advisor reads THIS project's seat record (derived, fail-open) so a seat whose
|
|
241
241
|
// recent cards produced nothing is benched at that difficulty instead of re-picked as cheap.
|
|
@@ -245,9 +245,9 @@ server.tool("relay_advise", "THE ADVISOR — ask the brain how to execute a body
|
|
|
245
245
|
return { content: [{ type: "text", text: JSON.stringify(out, null, 2) }] };
|
|
246
246
|
});
|
|
247
247
|
|
|
248
|
-
server.tool("relay_scrooge", "Delegate a
|
|
249
|
-
{ prompt: z.string().describe("
|
|
250
|
-
task: z.string().optional().describe("
|
|
248
|
+
server.tool("relay_scrooge", "Delegate a small, self-contained piece of grunt work (draft, summarize, extract, classify, boilerplate) to a cheap external model via Scrooge. Returns the output plus the ledger receipt. Stateless one-shots only; use the crew for anything stateful or large.",
|
|
249
|
+
{ prompt: z.string().describe("complete self-contained task; the model sees only this"),
|
|
250
|
+
task: z.string().optional().describe("code (default) | summarize | extract | draft | verify | reason | cheap"),
|
|
251
251
|
difficulty: z.enum(["easy","medium","hard"]).optional() },
|
|
252
252
|
async ({ prompt, task, difficulty }) => {
|
|
253
253
|
const r = spawnSync("scrooge", ["-t", task || "code", "-d", difficulty || "easy"], { input: prompt, encoding: "utf8", timeout: 120000, maxBuffer: 8 * 1024 * 1024 });
|
|
@@ -257,21 +257,21 @@ server.tool("relay_scrooge", "Delegate a SMALL, SELF-CONTAINED piece of grunt wo
|
|
|
257
257
|
});
|
|
258
258
|
|
|
259
259
|
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.",
|
|
260
|
-
{ text: z.string().describe("one-line imperative guardrail
|
|
260
|
+
{ text: z.string().describe("one-line imperative guardrail"), scope: z.string().optional().describe("'global' (default) or an agent brand") },
|
|
261
261
|
async ({ text, scope }) => {
|
|
262
262
|
const r = await api("POST", "/lesson", { text, scope: scope || "global", by: SESSION });
|
|
263
263
|
return { content: [{ type: "text", text: r.dedup ? "lesson already recorded" : `lesson recorded (${r.count} total)` }] };
|
|
264
264
|
});
|
|
265
265
|
|
|
266
|
-
server.tool("relay_verify_gate", "Record a
|
|
267
|
-
{ action: z.enum(["add", "resolve", "list"])
|
|
268
|
-
claim: z.string().optional().describe("
|
|
269
|
-
why: z.string().optional()
|
|
270
|
-
howToVerify: z.string().optional()
|
|
266
|
+
server.tool("relay_verify_gate", "Record a verification gate: a claim that must be independently verified before the work ships. It travels with handoffs and is shown prominently to whoever takes over. 'add' when you wrote code whose correctness you have not proven (formulas, security, data shape); 'resolve' once verified or waived with the user; 'list' open gates.",
|
|
267
|
+
{ action: z.enum(["add", "resolve", "list"]),
|
|
268
|
+
claim: z.string().optional().describe("claim to verify (required for add)"),
|
|
269
|
+
why: z.string().optional(),
|
|
270
|
+
howToVerify: z.string().optional(),
|
|
271
271
|
id: z.number().optional().describe("gate id to resolve"),
|
|
272
|
-
status: z.string().optional()
|
|
273
|
-
note: z.string().optional()
|
|
274
|
-
project: z.string().optional().describe("
|
|
272
|
+
status: z.string().optional(),
|
|
273
|
+
note: z.string().optional(),
|
|
274
|
+
project: z.string().optional().describe("project (default: this one)") },
|
|
275
275
|
async ({ action, claim, why, howToVerify, id, status, note, project }) => {
|
|
276
276
|
const proj = project || PROJECT;
|
|
277
277
|
if (action === "list") {
|
|
@@ -289,12 +289,12 @@ server.tool("relay_verify_gate", "Record a VERIFICATION GATE — a claim that MU
|
|
|
289
289
|
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` }] };
|
|
290
290
|
});
|
|
291
291
|
|
|
292
|
-
server.tool("relay_propose", "
|
|
293
|
-
{ scope: z.string()
|
|
294
|
-
condition: z.string()
|
|
295
|
-
exclusions: z.string()
|
|
296
|
-
key: z.string().optional()
|
|
297
|
-
project: z.string().optional().describe("project
|
|
292
|
+
server.tool("relay_propose", "Propose a standing permission or scope change to the human operator, the only approver. It must state its bound (scope, condition, exclusions) or it is rejected. Pending max 3 per session (withdraw one to file another); the decision arrives as a bus message. A near-duplicate of a denied proposal is refused: refine the bound or move on. File it, continue your mission, never nag.",
|
|
293
|
+
{ scope: z.string(),
|
|
294
|
+
condition: z.string(),
|
|
295
|
+
exclusions: z.string(),
|
|
296
|
+
key: z.string().optional(),
|
|
297
|
+
project: z.string().optional().describe("project (default: this one)") },
|
|
298
298
|
async ({ scope, condition, exclusions, key, project }) => {
|
|
299
299
|
// signedPost directly (not api()) — a refusal's BODY is the teaching moment (denial note,
|
|
300
300
|
// queue guidance) and api() throws it away, leaving only "hub 409".
|
|
@@ -310,7 +310,7 @@ server.tool("relay_propose", "PROPOSE a standing permission or scope change to t
|
|
|
310
310
|
: `proposal #${pr.id} filed and PENDING operator review. Continue your mission — you'll get a bus message when it's decided. Do NOT act as if it were approved.` }] };
|
|
311
311
|
});
|
|
312
312
|
|
|
313
|
-
server.tool("relay_proposals", "List
|
|
313
|
+
server.tool("relay_proposals", "List this session's permission proposals and statuses (pending / approved / denied / revoked / withdrawn) with decision notes. An approved proposal is a standing grant: act within its bound without re-asking. Pending is not approved.", {},
|
|
314
314
|
async () => {
|
|
315
315
|
const { proposals } = await api("GET", `/proposals?session=${encodeURIComponent(SESSION)}`);
|
|
316
316
|
if (!proposals?.length) return { content: [{ type: "text", text: "no proposals filed by this session" }] };
|
|
@@ -375,10 +375,10 @@ function cardView(tasks, id, proj) {
|
|
|
375
375
|
return out.join("\n");
|
|
376
376
|
}
|
|
377
377
|
|
|
378
|
-
server.tool("relay_board", "Show a project's Kanban board (
|
|
379
|
-
{ project: z.string().optional()
|
|
380
|
-
card: z.number().optional().describe("read ONE card
|
|
381
|
-
mine: z.boolean().optional().describe("
|
|
378
|
+
server.tool("relay_board", "Show a project's Kanban board (cards, status, assignee; ·N = note count). Defaults to this project.",
|
|
379
|
+
{ project: z.string().optional(),
|
|
380
|
+
card: z.number().optional().describe("read ONE card (with its notes and related done cards) instead of the board; call this when starting a card"),
|
|
381
|
+
mine: z.boolean().optional().describe("cards assigned to the calling session (doing/testing/todo), newest first; the first call of a turn whose wake names no card id") },
|
|
382
382
|
async ({ project, card, mine }) => {
|
|
383
383
|
const proj = project || PROJECT;
|
|
384
384
|
// #6983: ask for the SLIM projection, and for that one card full when opening one. The board
|
|
@@ -419,10 +419,10 @@ server.tool("relay_peers", "Find who you can talk to: the live agent sessions on
|
|
|
419
419
|
return { content: [{ type: "text", text: lines.join("\n") || "no peers yet" }] };
|
|
420
420
|
});
|
|
421
421
|
|
|
422
|
-
server.tool("relay_send", "Send a live message to another agent session (or 'all'
|
|
423
|
-
{ to: z.string()
|
|
424
|
-
wake: z.boolean().optional().describe("false = context, not a contract:
|
|
425
|
-
re: z.number().optional().describe("
|
|
422
|
+
server.tool("relay_send", "Send a live message to another agent session (or 'all'). Message the agent yourself; never ask the human to relay. Ids: relay_peers. Cross-project sends are a breach unless the operator linked the projects (`trantor policy link <a> <b> --reason \"<why>\"`); the hub answers 403 for an unlinked project.",
|
|
423
|
+
{ to: z.string(), text: z.string(),
|
|
424
|
+
wake: z.boolean().optional().describe("false = context, not a contract: batches into the target's next turn. Unset or true, a direct message ALWAYS earns a turn whatever its text says, so set false on every ack, FYI and queue note."),
|
|
425
|
+
re: z.number().optional().describe("message id you are answering; set it on every reply. Without it the hub matches your reply to the peer's OLDEST outstanding contract, closing the wrong one.") },
|
|
426
426
|
async ({ to, text, wake, re }) => {
|
|
427
427
|
// The event log is append-only — a secret in it is unrecoverable, so refuse BEFORE
|
|
428
428
|
// anything reaches the hub. Returns the offending kinds so the caller can fix it.
|
|
@@ -446,9 +446,9 @@ server.tool("relay_send", "Send a live message to another agent session (or 'all
|
|
|
446
446
|
return { content: [{ type: "text", text: `sent #${id} to ${to}${wake === false ? " (batched — no turn)" : ""}` }] };
|
|
447
447
|
});
|
|
448
448
|
|
|
449
|
-
server.tool("relay_ask", "Ask a
|
|
450
|
-
{ card: z.number()
|
|
451
|
-
question: z.string().describe("the
|
|
449
|
+
server.tool("relay_ask", "Ask a blocking question on your card without forfeiting the contract. Use it when the contract omits a fact you cannot proceed without; never invent the value. Sends a kind:ask to the card's assigner, blocks the card with your question as its note, and the answer resumes this session with the original contract re-attached. End your turn right after calling it.",
|
|
450
|
+
{ card: z.number(),
|
|
451
|
+
question: z.string().describe("the one missing fact, stated precisely") },
|
|
452
452
|
async ({ card, question }) => {
|
|
453
453
|
// Append-only log rule, same as relay_send: a secret is refused BEFORE anything reaches the hub.
|
|
454
454
|
const scrub = assertNoSecrets(question);
|
|
@@ -474,7 +474,7 @@ server.tool("relay_ask", "Ask a BLOCKING question on your card without forfeitin
|
|
|
474
474
|
});
|
|
475
475
|
|
|
476
476
|
server.tool("relay_duty_failure", "Duty-seat only: record that a required cross-session socket nudge was skipped or that a relay send returned 403. The hub appends the failure to the target project's active focus card and exposes it in trantor doctor.",
|
|
477
|
-
{ recipient: z.string()
|
|
477
|
+
{ recipient: z.string(), project: z.string().optional(), kind: z.enum(["relay-403", "skipped-nudge"]), detail: z.string().max(500).optional() },
|
|
478
478
|
async ({ recipient, project, kind, detail }) => {
|
|
479
479
|
const { failure } = await api("POST", "/duty/failure", { recipient, project, kind, detail });
|
|
480
480
|
return { content: [{ type: "text", text: `recorded: ${failure.text}${failure.focusCard ? ` (focus card #${failure.focusCard})` : " (no active focus card)"}` }] };
|
|
@@ -506,7 +506,7 @@ server.tool("relay_handoff", "Write a rich handoff for THIS session so a fresh s
|
|
|
506
506
|
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.` }] };
|
|
507
507
|
});
|
|
508
508
|
|
|
509
|
-
server.tool("relay_inbox", "Read
|
|
509
|
+
server.tool("relay_inbox", "Read new messages for this session since the last read (non-blocking). Bounded page, newest first, repeats collapsed with ×N. When older messages exist it ends with a before-cursor: pass it back as relay_inbox({before}).",
|
|
510
510
|
{ before: z.number().optional().describe("page BACK: return only messages with id < before (the cursor from the 'older not shown' footer). A peek — does not advance the new-message cursor.") },
|
|
511
511
|
async ({ before }) => {
|
|
512
512
|
if (before) {
|
|
@@ -522,7 +522,7 @@ server.tool("relay_inbox", "Read NEW messages addressed to this session since th
|
|
|
522
522
|
return { content: [{ type: "text", text: page.text || "(no new messages)" }] };
|
|
523
523
|
});
|
|
524
524
|
|
|
525
|
-
server.tool("relay_wait", "
|
|
525
|
+
server.tool("relay_wait", "Long-poll up to `timeout` seconds for the next message to this session; returns the instant one arrives. When idle, park by calling it repeatedly. MCP clients cap long calls (OpenCode ~60s, Codex ~120s, Claude Code backgrounds at 120s), so use timeout 50 and loop.",
|
|
526
526
|
{ timeout: z.number().optional().describe("seconds to wait, default 25. Effective wait is capped at 110s so the call always returns inline (a longer wait would be auto-backgrounded by Claude Code's 120s MCP floor or dropped by other clients). Use 50 and call repeatedly to park while idle.") },
|
|
527
527
|
async ({ timeout }) => {
|
|
528
528
|
// Cap below the 120s MCP auto-background floor (CC 2.1.212+) so relay_wait always
|