trantor 0.18.58 → 0.18.60
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/README.md +8 -0
- package/bin/connect.mjs +62 -55
- package/bin/crew/preflight.mjs +70 -0
- package/bin/crew-payload.mjs +19 -10
- package/bin/crew-runner.mjs +222 -102
- package/bin/crew.mjs +8 -5
- package/bin/turn-watchdog.mjs +38 -4
- package/hooks/lib/hollow-move.mjs +12 -1
- package/hub/reaper.mjs +23 -56
- package/hub/routes/messages.mjs +24 -25
- package/lib/classify-failure.mjs +19 -0
- package/lib/project.mjs +34 -9
- package/lib/seat-worktree.mjs +203 -0
- package/mcp.mjs +45 -10
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "trantor",
|
|
3
|
-
"version": "0.18.
|
|
3
|
+
"version": "0.18.60",
|
|
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/README.md
CHANGED
|
@@ -200,6 +200,14 @@ project takes over with a full window (and a PreCompact hook does this automatic
|
|
|
200
200
|
**best live model** for the work at spawn (capability × cost), enumerated from the provider
|
|
201
201
|
itself — never a guessed endpoint. **Serialized and then verified on the bus** — the launcher
|
|
202
202
|
ends with "crew verified" or names the no-shows loudly. The orchestrator never gets a green lie.
|
|
203
|
+
**Seats build in their own git worktrees** (`~/.agent-bus/worktrees/<project>/<seat>`), and a
|
|
204
|
+
fresh checkout lacks gitignored files and cannot resolve relative sibling packages. Declare what a
|
|
205
|
+
worktree needs in `.trantor/worktree.json`:
|
|
206
|
+
`{"link": ["../sibling-repo"], "provision": [{"path": "ios/Config.swift", "mode": "link|stub|operator"}], "preflight": "npm test --silent"}`.
|
|
207
|
+
`trantor up` links the siblings beside the worktree, links or stubs the declared files (a real
|
|
208
|
+
credential is never copied; `operator` entries are named for you), runs the preflight once in the
|
|
209
|
+
first fresh worktree (5-minute cap) and prints `preflight ok` or the last 20 lines — broadcast on
|
|
210
|
+
the bus too, so the orchestrator sees it before writing contracts. No declaration, no preflight.
|
|
203
211
|
3. **Work flows over the bus.** Contracts arrive as messages; each agent owns its own files;
|
|
204
212
|
coordination happens in <280-char messages you can read on the dashboard. Crew members
|
|
205
213
|
live under a **runner**: the CLI works one turn and exits, the runner long-polls the bus
|
package/bin/connect.mjs
CHANGED
|
@@ -1,27 +1,25 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// trantor connect — wire every AI coding CLI on this machine to the bus, in one shot
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
// node bin/connect.mjs --dry-run # show what would change, touch nothing
|
|
6
|
-
//
|
|
7
|
-
// Each CLI keeps its own MCP config file/format; this writes the one "relay" entry into each
|
|
8
|
-
// (with a timestamped .bak backup the first time it changes a file). Claude Code is handled by
|
|
9
|
-
// the plugin (claude plugin install trantor), so it's only verified here, not patched.
|
|
2
|
+
// trantor connect — wire every AI coding CLI on this machine to the bus, in one shot (idempotent;
|
|
3
|
+
// --dry-run touches nothing). Writes the one "relay" MCP entry into each CLI's own config format,
|
|
4
|
+
// with a timestamped .bak backup on first change. Claude Code rides the plugin: verified, not patched.
|
|
10
5
|
import { readFileSync, writeFileSync, existsSync, mkdirSync, copyFileSync } from "node:fs";
|
|
11
6
|
import { join, dirname } from "node:path";
|
|
12
7
|
import { homedir } from "node:os";
|
|
13
8
|
import { execSync } from "node:child_process";
|
|
14
9
|
import { fileURLToPath } from "node:url";
|
|
10
|
+
import { resolveProject, resolveHubInfo } from "../lib/project.mjs";
|
|
15
11
|
|
|
16
12
|
const DRY = process.argv.includes("--dry-run");
|
|
17
13
|
const MCP = join(dirname(dirname(fileURLToPath(import.meta.url))), "mcp.mjs");
|
|
18
|
-
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
//
|
|
14
|
+
// Connect-time truth stamped into every relay entry env (#7893): the project this checkout resolves
|
|
15
|
+
// to and the hub THAT project resolves to. Some CLIs spawn MCP with a scrubbed env where even `git`
|
|
16
|
+
// is missing, so the stamp is the belt; the worktree path rule in lib/project.mjs stays primary.
|
|
17
|
+
// Env wins in resolveHubInfo, so these keys are REFRESHED on every connect run — re-run after a pin change.
|
|
18
|
+
const PROJECT_AT_CONNECT = resolveProject(process.cwd());
|
|
19
|
+
const URL_ = resolveHubInfo(PROJECT_AT_CONNECT).url;
|
|
20
|
+
// Graft (github.com/NanoNets/context-graph-engine): local Tree-sitter dependency graph over MCP,
|
|
21
|
+
// wired next to `relay` so a seat locates code in one call; the graph refreshes itself per query
|
|
22
|
+
// and a project with no graft/ index simply returns empty tools. `graft build` seeds an index.
|
|
25
23
|
const GRAFT = (() => { try { return execSync("command -v graft", { encoding: "utf8", shell: "/bin/sh" }).trim(); } catch { return "graft"; } })();
|
|
26
24
|
const HAS_GRAFT = GRAFT !== "graft" || (() => { try { execSync("command -v graft", { stdio: "ignore", shell: "/bin/sh" }); return true; } catch { return false; } })();
|
|
27
25
|
const has = (cmd) => { try { execSync(`command -v ${cmd}`, { stdio: "ignore", shell: "/bin/sh" }); return true; } catch { return false; } };
|
|
@@ -43,11 +41,11 @@ function patchJson(path, mutate) {
|
|
|
43
41
|
return exists ? "wired" : "wired (new config)";
|
|
44
42
|
}
|
|
45
43
|
|
|
46
|
-
//
|
|
47
|
-
//
|
|
48
|
-
//
|
|
49
|
-
//
|
|
50
|
-
const relayEnv = (agent) => ({ RELAY_AGENT: agent });
|
|
44
|
+
// Two keys, refreshed by every connect run: agent identity and the hub. The PROJECT is never
|
|
45
|
+
// stamped: these configs are global to the CLI, and a seat of that CLI in another project would
|
|
46
|
+
// inherit the wrong board (RELAY_PROJECT outranks the worktree rule in resolveProject, #7893).
|
|
47
|
+
// A user-added env key survives the refresh merge.
|
|
48
|
+
const relayEnv = (agent) => ({ RELAY_AGENT: agent, RELAY_URL: URL_ });
|
|
51
49
|
// OpenCode hosts several differently-named seats. Its global MCP environment must not stamp all
|
|
52
50
|
// of them "opencode": ambient runner identity wins, while this fallback names a normal interactive
|
|
53
51
|
// OpenCode session that has no RELAY_AGENT/RELAY_SESSION of its own.
|
|
@@ -63,15 +61,31 @@ if (has("claude")) {
|
|
|
63
61
|
report("claude", st);
|
|
64
62
|
}
|
|
65
63
|
|
|
66
|
-
// ---- Codex (TOML append
|
|
64
|
+
// ---- Codex (TOML — append a missing relay section, refresh its env when it exists) ----
|
|
65
|
+
const tomlRelayEnv = `env = { RELAY_AGENT = "codex", RELAY_URL = "${URL_}" }`;
|
|
67
66
|
if (has("codex")) {
|
|
68
67
|
const p = join(homedir(), ".codex", "config.toml");
|
|
69
|
-
|
|
70
|
-
if (cur.includes("[mcp_servers.relay]"))
|
|
71
|
-
|
|
72
|
-
const block = `\n# trantor — auto-registers each Codex session on the bus + adds relay_* tools\n# (no RELAY_URL on purpose: the per-project hub pin decides the hub)\n[mcp_servers.relay]\ncommand = "node"\nargs = ["${MCP}"]\nenv = { RELAY_AGENT = "codex" }\n`;
|
|
68
|
+
let cur = existsSync(p) ? readFileSync(p, "utf8") : "";
|
|
69
|
+
if (!cur.includes("[mcp_servers.relay]")) {
|
|
70
|
+
const block = `\n# trantor — auto-registers each Codex session on the bus + adds relay_* tools\n# (env is REFRESHED by every \`trantor connect\`: agent + connect-time hub + project)\n[mcp_servers.relay]\ncommand = "node"\nargs = ["${MCP}"]\n${tomlRelayEnv}\n`;
|
|
73
71
|
if (!DRY) { if (existsSync(p)) backup(p); else mkdirSync(dirname(p), { recursive: true }); writeFileSync(p, cur + block); }
|
|
74
72
|
report("codex", cur ? "wired" : "wired (new config)", p);
|
|
73
|
+
} else {
|
|
74
|
+
// Refresh the connect-written single-line `env = { ... }` inside the existing section; a
|
|
75
|
+
// hand-rolled multi-line env table is left untouched (user customization wins) and is named.
|
|
76
|
+
const start = cur.indexOf("[mcp_servers.relay]");
|
|
77
|
+
const next = cur.indexOf("\n[", start + 1);
|
|
78
|
+
const end = next === -1 ? cur.length : next + 1;
|
|
79
|
+
const section = cur.slice(start, end);
|
|
80
|
+
let refreshed = section, note = "already wired";
|
|
81
|
+
if (/^env\s*=\s*\{.*\}\s*$/m.test(section)) refreshed = section.replace(/^env\s*=.*$/m, tomlRelayEnv);
|
|
82
|
+
else if (!/^env\s*=/m.test(section)) refreshed = section.replace(/\n*$/, "\n") + tomlRelayEnv + "\n";
|
|
83
|
+
else note = "relay env kept (custom shape) — refresh it by hand";
|
|
84
|
+
if (refreshed !== section) {
|
|
85
|
+
if (!DRY) { backup(p); writeFileSync(p, cur.slice(0, start) + refreshed + cur.slice(end)); }
|
|
86
|
+
note = "relay env refreshed";
|
|
87
|
+
}
|
|
88
|
+
report("codex", note, p);
|
|
75
89
|
}
|
|
76
90
|
// graft alongside relay
|
|
77
91
|
if (HAS_GRAFT) {
|
|
@@ -85,27 +99,32 @@ if (has("codex")) {
|
|
|
85
99
|
}
|
|
86
100
|
}
|
|
87
101
|
|
|
88
|
-
// ---- Gemini CLI ---- (
|
|
102
|
+
// ---- Gemini CLI ---- (relay entry env is REFRESHED: ||= kept an older connect's stale env forever)
|
|
89
103
|
if (has("gemini")) {
|
|
90
104
|
const p = join(homedir(), ".gemini", "settings.json");
|
|
91
105
|
report("gemini", patchJson(p, d => {
|
|
92
106
|
d.mcpServers ||= {};
|
|
93
|
-
d.mcpServers.relay ||= { command: "node", args: [MCP], env:
|
|
107
|
+
d.mcpServers.relay ||= { command: "node", args: [MCP], env: {} };
|
|
108
|
+
d.mcpServers.relay.env = { ...d.mcpServers.relay.env, ...relayEnv("gemini") };
|
|
94
109
|
if (HAS_GRAFT) d.mcpServers.graft ||= { command: GRAFT, args: ["mcp"] };
|
|
95
110
|
}), p);
|
|
96
111
|
}
|
|
97
112
|
|
|
98
|
-
// ---- Kimi CLI ----
|
|
113
|
+
// ---- Kimi CLI ---- (same refresh: the stale entry that caused #7893 was {RELAY_AGENT: kimi} only)
|
|
99
114
|
if (has("kimi")) {
|
|
100
115
|
const p = join(homedir(), ".kimi", "mcp.json");
|
|
101
116
|
report("kimi", patchJson(p, d => {
|
|
102
117
|
d.mcpServers ||= {};
|
|
103
|
-
d.mcpServers.relay ||= { command: "node", args: [MCP], env:
|
|
118
|
+
d.mcpServers.relay ||= { command: "node", args: [MCP], env: {} };
|
|
119
|
+
d.mcpServers.relay.env = { ...d.mcpServers.relay.env, ...relayEnv("kimi") };
|
|
104
120
|
if (HAS_GRAFT) d.mcpServers.graft ||= { command: GRAFT, args: ["mcp"] };
|
|
105
121
|
}), p);
|
|
106
122
|
}
|
|
107
123
|
|
|
108
124
|
// ---- OpenCode ----
|
|
125
|
+
// Deliberately NOT stamped with RELAY_URL/RELAY_PROJECT: OpenCode hosts several differently-named
|
|
126
|
+
// seats from one config, and a stamped project/hub would override every hosted seat's runner-provided
|
|
127
|
+
// env (the overlay bug the deletes below fix). Ambient runner env + RELAY_AGENT_FALLBACK stay in charge.
|
|
109
128
|
if (has("opencode")) {
|
|
110
129
|
const p = join(homedir(), ".config", "opencode", "opencode.json");
|
|
111
130
|
report("opencode", patchJson(p, d => {
|
|
@@ -122,16 +141,10 @@ if (has("opencode")) {
|
|
|
122
141
|
}), p);
|
|
123
142
|
}
|
|
124
143
|
|
|
125
|
-
// ---- DeepSeek Harness (dsh)
|
|
126
|
-
//
|
|
127
|
-
//
|
|
128
|
-
//
|
|
129
|
-
// 1. their Claude Code hooks bridge pointed at OUR hooks.json — presence, focus cards, heartbeats,
|
|
130
|
-
// file claims run inside dsh exactly as they do inside CC (verified live 2026-08-19);
|
|
131
|
-
// 2. their MCP client spawning our relay server — relay_* tools with the seat identity forwarded
|
|
132
|
-
// from the ambient RELAY_* env (crew-runner sets those per seat).
|
|
133
|
-
// The bridge's own protocol lib is declared as a dependency explicitly: the rc package forgets it
|
|
134
|
-
// (ERR_MODULE_NOT_FOUND at boot without it — reported upstream).
|
|
144
|
+
// ---- DeepSeek Harness (dsh): composes from PROFILES (~/.dsh/profiles/<name>), no single MCP config.
|
|
145
|
+
// We build a "trantor" profile mounting their Claude Code hooks bridge at OUR hooks.json plus their
|
|
146
|
+
// MCP client running our relay server with ambient RELAY_* identity; the bridge's protocol lib is
|
|
147
|
+
// declared explicitly — the rc package forgets it (ERR_MODULE_NOT_FOUND at boot).
|
|
135
148
|
if (has("dsh")) {
|
|
136
149
|
const ROOT = dirname(MCP);
|
|
137
150
|
const prof = join(homedir(), ".dsh", "profiles", "trantor");
|
|
@@ -183,12 +196,10 @@ ${HAS_GRAFT ? ` - id: trantor-graft
|
|
|
183
196
|
command: ${GRAFT}
|
|
184
197
|
args: ['mcp']
|
|
185
198
|
` : ""}`;
|
|
186
|
-
// "a profile exists" is not "a profile is current": connect grows rows over time
|
|
187
|
-
//
|
|
188
|
-
//
|
|
189
|
-
//
|
|
190
|
-
// one regenerates the patch (backed up). Presence, not diff — user edits to rows that ARE there
|
|
191
|
-
// still win, the same rule as the gemini/kimi/opencode `||=` patches above.
|
|
199
|
+
// "a profile exists" is not "a profile is current": connect grows rows over time, and an existence
|
|
200
|
+
// check short-circuits on a profile written by an older connect forever. The gate is CONTENT-based:
|
|
201
|
+
// every row id this connect writes must already be in the patch, else regenerate (backed up).
|
|
202
|
+
// Presence, not diff — user edits to rows that ARE there still win, like the JSON patches above.
|
|
192
203
|
const expectedIds = [...patch.matchAll(/- id: (\S+)/g)].map(m => m[1]);
|
|
193
204
|
const cur = existsSync(patchPath) ? readFileSync(patchPath, "utf8") : "";
|
|
194
205
|
const missing = expectedIds.filter(id => !cur.split("\n").some(l => l.trim() === `- id: ${id}`));
|
|
@@ -201,11 +212,9 @@ ${HAS_GRAFT ? ` - id: trantor-graft
|
|
|
201
212
|
if (!DRY) {
|
|
202
213
|
mkdirSync(prof, { recursive: true });
|
|
203
214
|
// The seat runs the plugin's hooks MINUS SessionStart: the crew runner already owns
|
|
204
|
-
// registration/announcement, and per-turn roster
|
|
205
|
-
//
|
|
206
|
-
//
|
|
207
|
-
// crash was the duplicated-core install below, refuted by a clean-profile repro before we
|
|
208
|
-
// reported upstream (deepseek-harness discussions #3515/#3516).
|
|
215
|
+
// registration/announcement, and per-turn roster injection is wasted spend in a one-shot
|
|
216
|
+
// session. (A dsh teardown crash was once blamed on SessionStart — FALSE: it was the
|
|
217
|
+
// duplicated-core install below, refuted by clean-profile repro; deepseek-harness #3515/#3516.)
|
|
209
218
|
try {
|
|
210
219
|
const full = JSON.parse(readFileSync(join(ROOT, "hooks", "hooks.json"), "utf8"));
|
|
211
220
|
const subset = Object.fromEntries(Object.entries(full.hooks || {}).filter(([k]) => k !== "SessionStart"));
|
|
@@ -222,10 +231,8 @@ ${HAS_GRAFT ? ` - id: trantor-graft
|
|
|
222
231
|
const rootPath = join(prof, "cordis.yml");
|
|
223
232
|
if (!existsSync(rootPath)) writeFileSync(rootPath, "# dsh profile root — an empty entry list; the tree is composed from bundles + cordis.patch.yml.\n[]\n");
|
|
224
233
|
// pnpm settings mirroring dsh's own profile template. autoInstallPeers:false is LOAD-BEARING:
|
|
225
|
-
// an installer that pulls the bridge's peers drops a SECOND copy of dsh's core
|
|
226
|
-
//
|
|
227
|
-
// dies on ctx.tools[TOOL_RUNTIME_SCHEDULER] being undefined (observed: every turn that used
|
|
228
|
-
// any tool crashed "reading 'prepare'"; tool-free turns worked).
|
|
234
|
+
// an installer that pulls the bridge's peers drops a SECOND copy of dsh's core into the
|
|
235
|
+
// profile, both instances mount, and the first tool call dies on ctx.tools being undefined.
|
|
229
236
|
writeFileSync(join(prof, "pnpm-workspace.yaml"), "packages:\n - .\n\nnodeLinker: hoisted\nautoInstallPeers: false\n");
|
|
230
237
|
// the two bridge packages must be importable from the profile's node_modules — via pnpm
|
|
231
238
|
// (peers OFF, hoisted) like dsh's own template; npm needs --legacy-peer-deps for the same
|
|
@@ -240,7 +247,7 @@ ${HAS_GRAFT ? ` - id: trantor-graft
|
|
|
240
247
|
}
|
|
241
248
|
|
|
242
249
|
const found = out.length;
|
|
243
|
-
console.log(`trantor connect${DRY ? " (dry run)" : ""} — hub: ${URL_}`);
|
|
250
|
+
console.log(`trantor connect${DRY ? " (dry run)" : ""} — project: ${PROJECT_AT_CONNECT}, hub: ${URL_}`);
|
|
244
251
|
for (const r of out) console.log(` ${r.cli.padEnd(9)} ${r.status}${r.detail ? ` (${r.detail})` : ""}`);
|
|
245
252
|
if (!found) console.log(" no supported CLIs found on PATH (claude, codex, gemini, kimi, opencode, dsh)");
|
|
246
253
|
console.log(DRY ? "\nRun without --dry-run to apply." : "\nDone. New sessions of each CLI auto-join the bus.");
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// #7760: `trantor up` used to hand out worktrees that could not build — every seat discovered the
|
|
2
|
+
// missing sibling package and the gitignored config at once. Up now provisions the first seat's
|
|
3
|
+
// worktree from .trantor/worktree.json, runs the declared preflight there once, and broadcasts.
|
|
4
|
+
import {
|
|
5
|
+
PREFLIGHT_CAP_MS, applyWorktreeDeclaration, ensureSeatWorktree, preflightLine, provisioningLines,
|
|
6
|
+
readWorktreeDeclaration, runPreflight,
|
|
7
|
+
} from "../../lib/seat-worktree.mjs";
|
|
8
|
+
import { gitRoot, hostId } from "../../lib/project.mjs";
|
|
9
|
+
import { loadOrCreate } from "../../lib/identity.mjs";
|
|
10
|
+
import { ensureEnrolled } from "../../lib/enroll.mjs";
|
|
11
|
+
import { sfetchJson } from "../../lib/signed-fetch.mjs";
|
|
12
|
+
|
|
13
|
+
const BROADCAST_MAX = 1500;
|
|
14
|
+
|
|
15
|
+
export function broadcastText(agent, applied, result) {
|
|
16
|
+
const parts = [];
|
|
17
|
+
if (result) parts.push(result.ok ? `${preflightLine(result)} in the ${agent} worktree` : `preflight failed in the ${agent} worktree (${result.command}): ${result.tail.join(" | ")}`);
|
|
18
|
+
else parts.push(`worktree declaration applied for ${agent} (no preflight declared)`);
|
|
19
|
+
if (applied.operator.length) parts.push(`operator steps: ${applied.operator.join("; ")}`);
|
|
20
|
+
if (applied.problems.length) parts.push(`problems: ${applied.problems.join("; ")}`);
|
|
21
|
+
const text = parts.join(" — ");
|
|
22
|
+
return text.length > BROADCAST_MAX ? `${text.slice(0, BROADCAST_MAX - 1)}…` : text;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async function broadcast(ctx, text, log) {
|
|
26
|
+
const session = ctx.env.RELAY_SESSION || `${hostId()}:${ctx.project}`;
|
|
27
|
+
try {
|
|
28
|
+
const identity = loadOrCreate(session, "agent");
|
|
29
|
+
await ensureEnrolled(ctx.hub, identity, ctx.project);
|
|
30
|
+
const r = await sfetchJson(`${ctx.hub}/send`, {
|
|
31
|
+
identity, payload: { from: session, to: "all", project: ctx.project, kind: "status", text },
|
|
32
|
+
signal: AbortSignal.timeout(5000),
|
|
33
|
+
});
|
|
34
|
+
if (!r.ok) log(`— preflight result NOT recorded on the bus (${ctx.hub}/send answered ${r.status}) —`);
|
|
35
|
+
return r.ok;
|
|
36
|
+
} catch (e) {
|
|
37
|
+
log(`— preflight result NOT recorded on the bus (${String(e?.message || e).slice(0, 80)}) —`);
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Returns { skipped } when the project declares nothing (unchanged behaviour), else what was
|
|
43
|
+
// applied, the preflight result, and whether the bus recorded it.
|
|
44
|
+
export async function preflightFirstSeat(ctx, agent, { capMs = PREFLIGHT_CAP_MS, log = console.log } = {}) {
|
|
45
|
+
const decl = readWorktreeDeclaration(gitRoot(ctx.dir) || ctx.dir);
|
|
46
|
+
if (!decl) return { skipped: "no declaration" };
|
|
47
|
+
const wt = ensureSeatWorktree({ sourceDir: ctx.dir, project: ctx.project, agent, home: ctx.home, env: ctx.env, log });
|
|
48
|
+
if (!wt.root) {
|
|
49
|
+
log(`— preflight skipped: no seat worktree for ${agent} (running from ${wt.dir}) —`);
|
|
50
|
+
return { skipped: "no worktree" };
|
|
51
|
+
}
|
|
52
|
+
const applied = applyWorktreeDeclaration(decl, { root: wt.root, seatDir: wt.dir });
|
|
53
|
+
log(`— ${agent} worktree ${wt.created ? "created" : "reused"} at ${wt.dir}; .trantor/worktree.json applied —`);
|
|
54
|
+
for (const l of provisioningLines(applied, { prefix: " " })) log(l);
|
|
55
|
+
|
|
56
|
+
let result = null;
|
|
57
|
+
if (!decl.preflight) log("— no preflight declared —");
|
|
58
|
+
else if (wt.dirty) log(`— preflight skipped: the ${agent} worktree has uncommitted work (commit or harvest it, then re-run up) —`);
|
|
59
|
+
else {
|
|
60
|
+
log(`— preflight in ${wt.dir}: ${decl.preflight} (cap ${Math.round(capMs / 60000)} min) —`);
|
|
61
|
+
result = runPreflight(decl.preflight, { seatDir: wt.dir, capMs, env: ctx.env });
|
|
62
|
+
log(result.ok ? `\x1b[32m${preflightLine(result)}\x1b[0m` : `\x1b[31m${preflightLine(result)}\x1b[0m`);
|
|
63
|
+
if (!result.ok) log(" ✗ fix the worktree need above (or the declaration) BEFORE writing contracts — the seats will hit the same wall.");
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const worthPosting = result || applied.operator.length || applied.problems.length;
|
|
67
|
+
const text = worthPosting ? broadcastText(agent, applied, result) : "";
|
|
68
|
+
const posted = text ? await broadcast(ctx, text, log) : false;
|
|
69
|
+
return { applied, result, text, posted, seatDir: wt.dir, created: wt.created };
|
|
70
|
+
}
|
package/bin/crew-payload.mjs
CHANGED
|
@@ -1,11 +1,7 @@
|
|
|
1
1
|
// Payload composition for crew-runner turn prompts — pure, unit-testable (card #5683).
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
// last turn file — 90%), plus an unbounded FYI-broadcast backlog that grows across a failure streak
|
|
6
|
-
// and is replayed on every redelivery, on a RESUMED session where it all stacks. So every section
|
|
7
|
-
// here is capped, and the assembled prompt has ONE hard total cap with a visible truncation notice.
|
|
8
|
-
// Below the caps the output is byte-identical to the old string concatenation.
|
|
2
|
+
// A fresh codex seat burned 306k tokens re-feeding the FULL lessons block plus an unbounded
|
|
3
|
+
// broadcast backlog every turn, so every section here is capped and the assembled prompt has ONE
|
|
4
|
+
// hard total cap with a visible truncation notice; below the caps the output is byte-identical.
|
|
9
5
|
|
|
10
6
|
export const PAYLOAD_CAPS = Object.freeze({
|
|
11
7
|
wakeCount: 10, // direct/@mention messages: keep the last ~10
|
|
@@ -80,9 +76,8 @@ export function pickLessons(lessons, trigger = "", caps = PAYLOAD_CAPS) {
|
|
|
80
76
|
|
|
81
77
|
// ---- one composer, one hard total cap ----
|
|
82
78
|
// sections: [{ name, text, trim?, order? }] joined in order. `trim: "drop"` removes the whole
|
|
83
|
-
// section when
|
|
84
|
-
//
|
|
85
|
-
// the runner-authored frame. The payload carries a visible notice naming every trim.
|
|
79
|
+
// section when over `totalChars` (lowest `order` first); `trim: "truncate"` cuts it to the remaining
|
|
80
|
+
// budget; no `trim` = the runner-authored frame, never touched. A visible notice names every trim.
|
|
86
81
|
export function composePrompt(sections, caps = PAYLOAD_CAPS) {
|
|
87
82
|
const secs = sections.map(s => ({ ...s, text: String(s?.text ?? "") }));
|
|
88
83
|
let total = secs.reduce((a, s) => a + s.text.length, 0);
|
|
@@ -113,3 +108,17 @@ export function composePrompt(sections, caps = PAYLOAD_CAPS) {
|
|
|
113
108
|
dropped,
|
|
114
109
|
};
|
|
115
110
|
}
|
|
111
|
+
|
|
112
|
+
// ---- the integration head (#7754): one `base: <sha>` line on every contract. A seat starts its
|
|
113
|
+
// branch at that sha, never at origin/main, which trails the orchestrator's unpushed integration.
|
|
114
|
+
const BASE_LINE_RE = /^\s*base:\s*([0-9a-f]{7,40})\b/im;
|
|
115
|
+
export const baseLine = (sha) => (sha ? `base: ${sha}` : "");
|
|
116
|
+
// The newest wake message naming a base wins: an orchestrator's explicit head outranks local main.
|
|
117
|
+
export function contractBase(wake) {
|
|
118
|
+
const list = Array.isArray(wake) ? wake : [];
|
|
119
|
+
for (let i = list.length - 1; i >= 0; i--) {
|
|
120
|
+
const m = String(list[i]?.text ?? "").match(BASE_LINE_RE);
|
|
121
|
+
if (m) return m[1];
|
|
122
|
+
}
|
|
123
|
+
return "";
|
|
124
|
+
}
|