trantor 0.18.7 → 0.18.8
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-runner.mjs +40 -4
- package/bin/crew.sh +163 -6
- package/package.json +2 -2
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "trantor",
|
|
3
|
-
"version": "0.18.
|
|
3
|
+
"version": "0.18.8",
|
|
4
4
|
"description": "Trantor \u2014 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-runner.mjs
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
// agent arrives it RESUMES the CLI session (native resume = full context kept) with that
|
|
10
10
|
// message as the prompt. The model just works and ends its turn; the runner does the rest.
|
|
11
11
|
import { execSync, spawnSync } from "node:child_process";
|
|
12
|
-
import { readFileSync, writeFileSync, unlinkSync, existsSync, appendFileSync } from "node:fs";
|
|
12
|
+
import { readFileSync, writeFileSync, unlinkSync, existsSync, appendFileSync, mkdirSync } from "node:fs";
|
|
13
13
|
import { join, basename } from "node:path";
|
|
14
14
|
import { homedir } from "node:os";
|
|
15
15
|
import { resolveProject, resolveHub, withEnvFiles } from "../lib/project.mjs";
|
|
@@ -24,6 +24,41 @@ const DIR = process.argv[3] || process.cwd();
|
|
|
24
24
|
// back to the git-repo-root basename — never a loose dir basename that could
|
|
25
25
|
// fork the host's "builtbetter.ai" into a separate "builtbetter" lane.
|
|
26
26
|
const PROJ = process.env.RELAY_PROJECT || resolveProject(DIR);
|
|
27
|
+
|
|
28
|
+
function safePathSegment(s) {
|
|
29
|
+
return String(s).replace(/\.{2,}/g, "_").replace(/[^A-Za-z0-9_.-]/g, "_");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function gitOut(args, cwd = DIR) {
|
|
33
|
+
const r = spawnSync("git", args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], timeout: 8000 });
|
|
34
|
+
return r.status === 0 ? String(r.stdout || "").trim() : "";
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function ensureSeatWorktree(sourceDir) {
|
|
38
|
+
if (process.env.TRANTOR_NO_WORKTREE === "1") return sourceDir;
|
|
39
|
+
const root = gitOut(["-C", sourceDir, "rev-parse", "--show-toplevel"], sourceDir);
|
|
40
|
+
if (!root) return sourceDir;
|
|
41
|
+
|
|
42
|
+
spawnSync("git", ["-C", root, "worktree", "prune"], { stdio: "ignore", timeout: 8000 });
|
|
43
|
+
const seatDir = join(homedir(), ".agent-bus", "worktrees", safePathSegment(PROJ), safePathSegment(AGENT));
|
|
44
|
+
const branch = `seat/${AGENT}`;
|
|
45
|
+
if (existsSync(seatDir)) {
|
|
46
|
+
const ok = gitOut(["-C", seatDir, "rev-parse", "--is-inside-work-tree"], seatDir) === "true";
|
|
47
|
+
if (ok) return seatDir;
|
|
48
|
+
console.log(`\x1b[33m[runner]\x1b[0m worktree path exists but is not a git worktree: ${seatDir} — using ${sourceDir}`);
|
|
49
|
+
return sourceDir;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
try { mkdirSync(join(homedir(), ".agent-bus", "worktrees", safePathSegment(PROJ)), { recursive: true }); } catch {}
|
|
53
|
+
const r = spawnSync("git", ["-C", root, "worktree", "add", "-B", branch, seatDir, "HEAD"], {
|
|
54
|
+
encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], timeout: 30000,
|
|
55
|
+
});
|
|
56
|
+
if (r.status === 0) return seatDir;
|
|
57
|
+
console.log(`\x1b[33m[runner]\x1b[0m could not create ${branch} worktree — using ${sourceDir}`);
|
|
58
|
+
return sourceDir;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const TURN_DIR = ensureSeatWorktree(DIR);
|
|
27
62
|
// RUNNER_SESSION override: an orchestrator seat (bin/orchestrate.mjs) runs the same CLI as a crew
|
|
28
63
|
// seat but must live on the bus under its own name (claude-orch:proj), or it would collide with a
|
|
29
64
|
// plain claude crew seat on the same project.
|
|
@@ -53,7 +88,6 @@ process.on("uncaughtException", (e) => { console.log(`\x1b[31m[runner] UNCAUGHT:
|
|
|
53
88
|
process.on("unhandledRejection", (e) => { console.log(`\x1b[31m[runner] UNHANDLED REJECTION: ${e?.stack || e}\x1b[0m`); });
|
|
54
89
|
const log = (s) => console.log(`\x1b[38;5;43m[runner]\x1b[0m ${s}`);
|
|
55
90
|
const LOGDIR = join(homedir(), ".agent-bus", "logs");
|
|
56
|
-
import { mkdirSync } from "node:fs";
|
|
57
91
|
try { mkdirSync(LOGDIR, { recursive: true }); } catch {}
|
|
58
92
|
let TURN = 0;
|
|
59
93
|
const telemetry = (rec) => { try { appendFileSync(join(LOGDIR, `${AGENT}-${PROJ}.jsonl`), JSON.stringify(rec) + "\n"); } catch {} };
|
|
@@ -278,7 +312,9 @@ async function notifyAssigners(pairs, text) {
|
|
|
278
312
|
seen.add(f);
|
|
279
313
|
// `re` threads this outcome to the exact contract it answers, so the sender's ledger closes the
|
|
280
314
|
// right one instead of guessing from timing.
|
|
281
|
-
|
|
315
|
+
const payload = { from: SESSION, to: f, text: text.slice(0, 280), project: PROJ };
|
|
316
|
+
if (id) payload.re = id;
|
|
317
|
+
await api("/send", payload).catch(() => {});
|
|
282
318
|
}
|
|
283
319
|
if (seen.size) log(`reported outcome to ${[...seen].join(", ")}`);
|
|
284
320
|
}
|
|
@@ -325,7 +361,7 @@ function runTurn(prompt, isFirst, trigger = "kickoff") {
|
|
|
325
361
|
// substitution) so bash waits for tee to flush before we read the file back.
|
|
326
362
|
const inner = cli.sid ? `${cmd} | tee /dev/stderr` : `${cmd} | tee -a ${ERRF}`;
|
|
327
363
|
const r = spawnSync("/bin/bash", ["-c", `set -o pipefail; { ${inner} ; } 2> >(tee -a ${ERRF} >&2)`], {
|
|
328
|
-
cwd:
|
|
364
|
+
cwd: TURN_DIR, encoding: "utf8", stdio: cli.sid ? ["ignore", "pipe", "inherit"] : "inherit",
|
|
329
365
|
env: { ...process.env, RELAY_URL: HUB, RELAY_AGENT: AGENT, RELAY_PROJECT: PROJ,
|
|
330
366
|
// A RUNNER-MANAGED SEAT MUST NEVER HAND ITSELF A BATON.
|
|
331
367
|
//
|
package/bin/crew.sh
CHANGED
|
@@ -46,11 +46,19 @@ HAVE_TMUX=0; command -v tmux >/dev/null 2>&1 && HAVE_TMUX=1
|
|
|
46
46
|
# Events) — NOT the control socket — so it needs NO socket password (the socket denies external processes
|
|
47
47
|
# by default; AppleScript bypasses that, exactly like we already script Terminal.app).
|
|
48
48
|
HAVE_CMUX=0; [ -d "/Applications/cmux.app" ] && HAVE_CMUX=1
|
|
49
|
-
#
|
|
49
|
+
# herdr (https://herdr.dev — a server-held terminal runtime: panes live in its background server, so a
|
|
50
|
+
# crew survives the launcher exiting) is OPT-IN ONLY, never auto-detected — a machine with herdr
|
|
51
|
+
# installed keeps its default cmux→tmux→terminal dispatch untouched. CREW_MUX=herdr is the explicit
|
|
52
|
+
# switch; opting in without the binary is a HARD ERROR, because a silent fallback would defeat the ask.
|
|
53
|
+
HAVE_HERDR=0
|
|
54
|
+
# explicit override (user preference or tests): CREW_MUX=cmux|tmux|terminal|herdr forces the grouping UI.
|
|
50
55
|
case "${CREW_MUX:-}" in
|
|
51
56
|
cmux) HAVE_CMUX=1; HAVE_TMUX=0 ;;
|
|
52
57
|
tmux) HAVE_CMUX=0; HAVE_TMUX=1 ;;
|
|
53
58
|
terminal) HAVE_CMUX=0; HAVE_TMUX=0 ;;
|
|
59
|
+
herdr) HAVE_CMUX=0; HAVE_TMUX=0
|
|
60
|
+
command -v herdr >/dev/null 2>&1 || { echo "CREW_MUX=herdr but herdr is not installed — user-local (no sudo): curl -fsSL https://herdr.dev/install.sh | sh — see https://herdr.dev"; exit 1; }
|
|
61
|
+
HAVE_HERDR=1 ;;
|
|
54
62
|
esac
|
|
55
63
|
SEATDIR="$HOME/.agent-bus/seats"; mkdir -p "$SEATDIR"
|
|
56
64
|
DRY="${CREW_DRY_RUN:-0}"
|
|
@@ -136,6 +144,137 @@ end tell
|
|
|
136
144
|
OSA
|
|
137
145
|
}
|
|
138
146
|
|
|
147
|
+
# ── herdr helpers (active ONLY under the explicit CREW_MUX=herdr opt-in) ────────────────────────────
|
|
148
|
+
# herdr's creation commands print JSON ids — capture them, never predict (herdr.dev/docs). All parsing
|
|
149
|
+
# tolerates a bare array or a .result wrapper, and a failed/empty parse yields "" (the caller records
|
|
150
|
+
# the empty handle and crew-verify flags the dead seat on the bus — the spawn is not the truth).
|
|
151
|
+
_herdr() { herdr "$@"; }
|
|
152
|
+
_herdr_ws_create() { # $1=cwd $2=label → "workspace_id<TAB>root_pane_id"
|
|
153
|
+
_herdr workspace create --cwd "$1" --label "$2" --no-focus 2>/dev/null | node -e '
|
|
154
|
+
let d="";process.stdin.on("data",c=>d+=c).on("end",()=>{try{const r=(JSON.parse(d.slice(d.search(/[\[{]/))).result)||{};
|
|
155
|
+
process.stdout.write((((r.workspace||{}).workspace_id)||"")+"\t"+(((r.root_pane||{}).pane_id)||""))}catch(e){}})'
|
|
156
|
+
}
|
|
157
|
+
_herdr_split() { # $1=pane id ("" = UI-focused pane) $2=right|down → new pane id
|
|
158
|
+
local a=(pane split); [ -n "$1" ] && a+=("$1"); a+=(--direction "$2" --no-focus)
|
|
159
|
+
_herdr "${a[@]}" 2>/dev/null | node -e '
|
|
160
|
+
let d="";process.stdin.on("data",c=>d+=c).on("end",()=>{try{const r=(JSON.parse(d.slice(d.search(/[\[{]/))).result)||{};
|
|
161
|
+
process.stdout.write(((r.pane||{}).pane_id)||"")}catch(e){}})'
|
|
162
|
+
}
|
|
163
|
+
_herdr_ws_live() { # workspace list → "ids<TABnewline>names" (names \x01-wrapped+joined, like cmux)
|
|
164
|
+
_herdr workspace list 2>/dev/null | node -e '
|
|
165
|
+
let d="";process.stdin.on("data",c=>d+=c).on("end",()=>{try{const o=JSON.parse(d.slice(d.search(/[\[{]/)));
|
|
166
|
+
const a=Array.isArray(o)?o:(o.workspaces||((o.result||{}).workspaces)||[]);
|
|
167
|
+
console.log(a.map(x=>x.workspace_id||x.id||"").filter(Boolean).join(" "));
|
|
168
|
+
console.log("\u0001"+a.map(x=>x.label||x.name||x.custom_title||"").filter(Boolean).join("\u0001")+"\u0001")}catch(e){}})'
|
|
169
|
+
}
|
|
170
|
+
# Teardown works regardless of CREW_MUX: you must never need to remember the flag to tear a crew down.
|
|
171
|
+
_herdr_close_ws() { [ "$DRY" = "1" ] && { echo "[dry] herdr workspace close $1"; return 0; }; _herdr workspace close "$1" >/dev/null 2>&1; }
|
|
172
|
+
_herdr_close_pane() { [ "$DRY" = "1" ] && { echo "[dry] herdr pane close $1"; return 0; }; _herdr pane close "$1" >/dev/null 2>&1; }
|
|
173
|
+
|
|
174
|
+
# ── herdr spawn: ONE workspace `trantor:$PROJ`, one named pane per seat ─────────────────────────────
|
|
175
|
+
# Topology = workspace create (its root pane runs seat 1) + pane split for the rest, same ceil(√N) grid
|
|
176
|
+
# doctrine as cmux; pane rename labels each seat; pane run submits the seat command (bracketed-paste
|
|
177
|
+
# safe, sends Enter). Same replace-never-stack doctrine: REUSE the newest tracked workspace for this
|
|
178
|
+
# project, close older stacked ones, adopt one untracked live trantor:$PROJ workspace.
|
|
179
|
+
spawn_herdr() { # $@ = specs
|
|
180
|
+
local REUSE_WS="" line w
|
|
181
|
+
local stale_ws=()
|
|
182
|
+
if [ -f "$STATE" ]; then
|
|
183
|
+
while IFS= read -r line; do
|
|
184
|
+
[ -n "$line" ] || continue
|
|
185
|
+
_parse_row "$line"
|
|
186
|
+
{ [ "$RP" = "$PROJ" ] && [ "$RK" = "herdrws" ]; } || continue
|
|
187
|
+
[ -n "$REUSE_WS" ] && stale_ws+=("$REUSE_WS")
|
|
188
|
+
REUSE_WS="$RH"
|
|
189
|
+
done < "$STATE"
|
|
190
|
+
fi
|
|
191
|
+
if [ "${#stale_ws[@]}" -gt 0 ]; then
|
|
192
|
+
for w in "${stale_ws[@]}"; do
|
|
193
|
+
echo " → closing stale stacked crew workspace for $PROJ ($w)"
|
|
194
|
+
_herdr_close_ws "$w"; _state_drop "$PROJ" "herdrws" "" "$w"
|
|
195
|
+
done
|
|
196
|
+
prune_dead_state # the closed workspaces' seat rows just died with them
|
|
197
|
+
fi
|
|
198
|
+
# Untracked strays: LIVE workspaces labeled trantor:$proj that STATE doesn't know. Adopt one as the
|
|
199
|
+
# reuse target if we have none; close the rest. (Skipped in DRY: read-only drills seed STATE instead.)
|
|
200
|
+
if [ "$DRY" != "1" ]; then
|
|
201
|
+
local named nid
|
|
202
|
+
named="$(_herdr workspace list 2>/dev/null | WSNAME="trantor:$PROJ" node -e 'let d="";process.stdin.on("data",c=>d+=c).on("end",()=>{try{const o=JSON.parse(d.slice(d.search(/[\[{]/)));const a=Array.isArray(o)?o:(o.workspaces||((o.result||{}).workspaces)||[]);console.log(a.filter(x=>x.label===process.env.WSNAME||x.name===process.env.WSNAME||x.custom_title===process.env.WSNAME).map(x=>x.workspace_id||x.id||"").filter(Boolean).join(" "))}catch(e){}})')"
|
|
203
|
+
for nid in $named; do
|
|
204
|
+
[ "$nid" = "$REUSE_WS" ] && continue
|
|
205
|
+
[ -f "$STATE" ] && grep -qF "$nid" "$STATE" && continue # tracked → handled above
|
|
206
|
+
if [ -z "$REUSE_WS" ]; then
|
|
207
|
+
echo " → adopting existing untracked crew workspace for $PROJ ($nid)"
|
|
208
|
+
REUSE_WS="$nid"; record_state "$PROJ" "herdrws" "__ws__" "$nid"
|
|
209
|
+
else
|
|
210
|
+
echo " → closing stray crew workspace for $PROJ ($nid)"
|
|
211
|
+
_herdr_close_ws "$nid"
|
|
212
|
+
fi
|
|
213
|
+
done
|
|
214
|
+
fi
|
|
215
|
+
# Grid tiling (identical math to cmux): row 0 splits RIGHT off the previous column, later rows split
|
|
216
|
+
# DOWN from the pane directly above. In REUSE mode a replaced seat splits off its OWN old pane —
|
|
217
|
+
# keeping its spot — and added seats split right off the previous new pane.
|
|
218
|
+
local N=$# COLS=1; while [ $(( COLS * COLS )) -lt "$N" ]; do COLS=$(( COLS + 1 )); done
|
|
219
|
+
local SPEC wsid="" surf i=0
|
|
220
|
+
local surfs=()
|
|
221
|
+
[ -n "$REUSE_WS" ] && wsid="$REUSE_WS"
|
|
222
|
+
for SPEC in "$@"; do
|
|
223
|
+
resolve_spec "$SPEC"
|
|
224
|
+
local cmd; cmd="$(RUN_CMD)"
|
|
225
|
+
if [ -n "$REUSE_WS" ]; then
|
|
226
|
+
# replace-in-place: split the fresh pane FIRST (targeting the agent's old pane when tracked),
|
|
227
|
+
# then close the old pane — split-first so the workspace never dips to zero panes mid-swap.
|
|
228
|
+
local OLD_SURF=""
|
|
229
|
+
if [ -f "$STATE" ]; then
|
|
230
|
+
while IFS= read -r line; do
|
|
231
|
+
[ -n "$line" ] || continue
|
|
232
|
+
_parse_row "$line"
|
|
233
|
+
[ "$RP" = "$PROJ" ] && [ "$RK" = "herdr" ] && [ "$RA" = "$AGENT" ] && OLD_SURF="$RH"
|
|
234
|
+
done < "$STATE"
|
|
235
|
+
fi
|
|
236
|
+
local t=""
|
|
237
|
+
[ -n "$OLD_SURF" ] && t="$OLD_SURF"
|
|
238
|
+
{ [ -z "$t" ] && [ "$i" -gt 0 ]; } && t="${surfs[$(( i - 1 ))]}"
|
|
239
|
+
if [ "$DRY" = "1" ]; then
|
|
240
|
+
echo "[dry] herdr: reuse workspace $wsid — pane split for $AGENT${OLD_SURF:+ (replacing $OLD_SURF)}"
|
|
241
|
+
surf="%DRYT$i"
|
|
242
|
+
[ -n "$OLD_SURF" ] && _herdr_close_pane "$OLD_SURF"
|
|
243
|
+
else
|
|
244
|
+
surf="$(_herdr_split "$t" right)"
|
|
245
|
+
[ -n "$surf" ] && { _herdr pane rename "$surf" "$AGENT · $PROJ" >/dev/null 2>&1; _herdr pane run "$surf" "$cmd" >/dev/null 2>&1; }
|
|
246
|
+
if [ -n "$OLD_SURF" ]; then _herdr_close_pane "$OLD_SURF"; _state_drop "$PROJ" "herdr" "$AGENT" ""; fi
|
|
247
|
+
fi
|
|
248
|
+
elif [ "$i" = "0" ]; then
|
|
249
|
+
if [ "$DRY" = "1" ]; then
|
|
250
|
+
echo "[dry] herdr: workspace create (cwd $DIR) --label 'trantor:$PROJ' → root pane + run '$cmd'"
|
|
251
|
+
wsid="%DRYWS"; surf="%DRYT0"
|
|
252
|
+
else
|
|
253
|
+
local pair; pair="$(_herdr_ws_create "$DIR" "trantor:$PROJ")"
|
|
254
|
+
wsid="${pair%%$'\t'*}"; surf="${pair##*$'\t'}"
|
|
255
|
+
[ -n "$surf" ] && { _herdr pane rename "$surf" "$AGENT · $PROJ" >/dev/null 2>&1; _herdr pane run "$surf" "$cmd" >/dev/null 2>&1; }
|
|
256
|
+
fi
|
|
257
|
+
record_state "$PROJ" "herdrws" "__ws__" "$wsid"
|
|
258
|
+
else
|
|
259
|
+
local dir target
|
|
260
|
+
if [ $(( i / COLS )) = "0" ]; then dir="right"; target="${surfs[$(( i - 1 ))]}"
|
|
261
|
+
else dir="down"; target="${surfs[$(( i - COLS ))]}"; fi
|
|
262
|
+
if [ "$DRY" = "1" ]; then
|
|
263
|
+
echo "[dry] herdr: pane split ${target:-<focused>} --direction $dir + run '$cmd'"
|
|
264
|
+
surf="%DRYT$i"
|
|
265
|
+
else
|
|
266
|
+
surf="$(_herdr_split "$target" "$dir")"
|
|
267
|
+
[ -n "$surf" ] && { _herdr pane rename "$surf" "$AGENT · $PROJ" >/dev/null 2>&1; _herdr pane run "$surf" "$cmd" >/dev/null 2>&1; }
|
|
268
|
+
fi
|
|
269
|
+
fi
|
|
270
|
+
surfs+=("$surf")
|
|
271
|
+
record_state "$PROJ" "herdr" "$AGENT" "$surf"
|
|
272
|
+
echo " → $AGENT seat in herdr workspace ($PROJ)"
|
|
273
|
+
i=$(( i + 1 ))
|
|
274
|
+
done
|
|
275
|
+
echo "— crew grouped in herdr: ONE workspace for $PROJ, seats as named panes in its server. Teardown (this project only): trantor down —"
|
|
276
|
+
}
|
|
277
|
+
|
|
139
278
|
# surgical STATE row removal: drop every row matching PROJECT+KIND (+AGENT/+HANDLE when given).
|
|
140
279
|
# Empty $3/$4 = wildcard. Callers beware: uses _parse_row, which clobbers the RP/RK/RA/RH globals.
|
|
141
280
|
_state_drop() { # $1=project $2=kind $3=agent(''=any) $4=handle(''=any)
|
|
@@ -202,7 +341,7 @@ down() {
|
|
|
202
341
|
[ "$match" = "1" ] || continue
|
|
203
342
|
fi
|
|
204
343
|
scoped+=("$RP|$RK|$RA|$RH")
|
|
205
|
-
case "$RK" in attach|cmuxws) continue ;; esac # infra rows (attach
|
|
344
|
+
case "$RK" in attach|cmuxws|herdrws) continue ;; esac # infra rows (attach/cmux/herdr workspace) — not seats
|
|
206
345
|
killlist="$killlist • ${RP:-<legacy>} · $RA ($RK)"$'\n'
|
|
207
346
|
done < "$STATE"
|
|
208
347
|
|
|
@@ -220,6 +359,8 @@ down() {
|
|
|
220
359
|
case "$K2" in
|
|
221
360
|
cmuxws) [ "${#AGENTS[@]}" -gt 0 ] || _cmux_close_tab "$H2" ;; # whole workspace (no specific seats)
|
|
222
361
|
cmux) [ "${#AGENTS[@]}" -gt 0 ] && _cmux_close_term "$H2" ;; # a single seat's pane
|
|
362
|
+
herdrws) [ "${#AGENTS[@]}" -gt 0 ] || _herdr_close_ws "$H2" ;; # whole workspace (no specific seats)
|
|
363
|
+
herdr) [ "${#AGENTS[@]}" -gt 0 ] && _herdr_close_pane "$H2" ;;# a single seat's pane
|
|
223
364
|
tmux)
|
|
224
365
|
if [ "${#AGENTS[@]}" -gt 0 ]; then run "tmux kill-pane -t '$H2' 2>/dev/null" # per-seat
|
|
225
366
|
else
|
|
@@ -228,7 +369,7 @@ down() {
|
|
|
228
369
|
fi ;;
|
|
229
370
|
win|attach) { [ "${#AGENTS[@]}" -gt 0 ] && [ "$K2" = "attach" ]; } && continue; _kill_win "$H2" ;;
|
|
230
371
|
esac
|
|
231
|
-
case "$K2" in cmuxws|attach) : ;; *) _kill_seat_procs "$P2" "$A2" ;; esac
|
|
372
|
+
case "$K2" in cmuxws|attach|herdrws) : ;; *) _kill_seat_procs "$P2" "$A2" ;; esac
|
|
232
373
|
done
|
|
233
374
|
|
|
234
375
|
# rewrite STATE minus the rows we tore down (leaves OTHER projects' rows intact)
|
|
@@ -268,6 +409,15 @@ prune_dead_state() {
|
|
|
268
409
|
CLIVE="$(printf '%s' "$pair" | sed -n 1p)"
|
|
269
410
|
CLIVE_NAMES="$(printf '%s' "$pair" | sed -n 2p)"
|
|
270
411
|
fi
|
|
412
|
+
# herdr liveness is queried ONLY under the explicit opt-in (CREW_MUX=herdr): a default run never
|
|
413
|
+
# invokes herdr at all. Same keep-when-unprovable doctrine as cmux — no answer ⇒ rows are KEPT.
|
|
414
|
+
local HLIVE="" HLIVE_NAMES=""
|
|
415
|
+
if [ "$HAVE_HERDR" = "1" ]; then
|
|
416
|
+
local hpair
|
|
417
|
+
hpair="$(_herdr_ws_live)"
|
|
418
|
+
HLIVE="$(printf '%s' "$hpair" | sed -n 1p)"
|
|
419
|
+
HLIVE_NAMES="$(printf '%s' "$hpair" | sed -n 2p)"
|
|
420
|
+
fi
|
|
271
421
|
local tmp="$STATE.tmp" line alive
|
|
272
422
|
: > "$tmp"
|
|
273
423
|
while IFS= read -r line; do
|
|
@@ -283,6 +433,12 @@ prune_dead_state() {
|
|
|
283
433
|
elif [ "$RK" = "cmux" ]; then
|
|
284
434
|
# seat row lives exactly as long as its project still has a live crew workspace
|
|
285
435
|
if [ -n "$CLIVE" ]; then case "$CLIVE_NAMES" in *$'\x01'"trantor:$RP"$'\x01'*) : ;; *) alive=0 ;; esac; fi
|
|
436
|
+
elif [ "$RK" = "herdrws" ]; then
|
|
437
|
+
if [ -n "$HLIVE" ]; then case " $HLIVE " in *" $RH "*) : ;; *) alive=0 ;; esac; fi
|
|
438
|
+
elif [ "$RK" = "herdr" ]; then
|
|
439
|
+
# seat row lives exactly as long as its project still has a live crew workspace — validated at
|
|
440
|
+
# WORKSPACE granularity, never per pane (the cmux 0.17.61 lesson generalized)
|
|
441
|
+
if [ -n "$HLIVE" ]; then case "$HLIVE_NAMES" in *$'\x01'"trantor:$RP"$'\x01'*) : ;; *) alive=0 ;; esac; fi
|
|
286
442
|
fi
|
|
287
443
|
[ "$alive" = "1" ] && printf '%s\t%s\t%s\t%s\n' "$RP" "$RK" "$RA" "$RH" >> "$tmp"
|
|
288
444
|
done < "$STATE"
|
|
@@ -690,8 +846,9 @@ OSA
|
|
|
690
846
|
echo "— crew grouped in cmux (AppleScript): ONE workspace tab for $PROJ, seats tiled. Teardown: trantor down —"
|
|
691
847
|
}
|
|
692
848
|
|
|
693
|
-
spawn_crew() { # dispatch:
|
|
694
|
-
if [ "$
|
|
849
|
+
spawn_crew() { # dispatch: herdr (explicit opt-in) → cmux → tmux → Terminal grid
|
|
850
|
+
if [ "$HAVE_HERDR" = "1" ]; then spawn_herdr "$@"
|
|
851
|
+
elif [ "$HAVE_CMUX" = "1" ]; then spawn_cmux "$@"
|
|
695
852
|
elif [ "$HAVE_TMUX" = "1" ]; then spawn_tmux "$@"
|
|
696
853
|
else
|
|
697
854
|
echo "— no cmux/tmux → per-agent Terminal windows. For ONE grouped, named window per crew (and"
|
|
@@ -729,7 +886,7 @@ fi
|
|
|
729
886
|
|
|
730
887
|
spec_for_agent() { local want="$1"; shift; local s; for s in "$@"; do [ "${s%%:*}" = "$want" ] && { printf '%s' "$s"; return; }; done; printf '%s' "$want"; }
|
|
731
888
|
|
|
732
|
-
CREW_UI="Terminal windows"; [ "$HAVE_TMUX" = "1" ] && CREW_UI="tmux"; [ "$HAVE_CMUX" = "1" ] && CREW_UI="cmux"
|
|
889
|
+
CREW_UI="Terminal windows"; [ "$HAVE_TMUX" = "1" ] && CREW_UI="tmux"; [ "$HAVE_CMUX" = "1" ] && CREW_UI="cmux"; [ "$HAVE_HERDR" = "1" ] && CREW_UI="herdr"
|
|
733
890
|
echo "— bringing up crew for $PROJ ($CREW_UI) —"
|
|
734
891
|
SPAWN_EPOCH=$(epoch_ms)
|
|
735
892
|
spawn_crew "$@"
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "trantor",
|
|
3
|
-
"version": "0.18.
|
|
3
|
+
"version": "0.18.8",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"bin": {
|
|
6
6
|
"trantor": "bin/cli.mjs"
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
"zod": "^4.4.3"
|
|
12
12
|
},
|
|
13
13
|
"scripts": {
|
|
14
|
-
"test": "node bin/slop-gate.mjs --surface desktop/src && node test.mjs && node test-scenarios.mjs && node test-failure.mjs && node test-redelivery.mjs && node test-crew-completion.mjs && node test-contracts.mjs && node test-handoff.mjs && node test-handoff-summarizer.mjs && node test-baton-turn-boundary.mjs && node test-agents.mjs && node test-update.mjs && node test-handoff-guard.mjs && node test-baton-latest.mjs && node test-balances.mjs && node test-subagent-cost.mjs && node test-inbox.mjs && node test-inflight.mjs && node test-notify.mjs && node test-focus.mjs && node test-cardlog.mjs && node test-relay-note.mjs && node test-reaper.mjs && node test-events.mjs && node test-proposals.mjs && node test-scrub.mjs && node test-store-delta.mjs && node test-message-re.mjs && node test-claims.mjs && node test-adopt.mjs && node test-summarize.mjs && node test-identity-core.mjs && node test-identity.mjs && node test-identity-instances.mjs && node test-duty.mjs && node test-duty-seat.mjs && node test-discovery.mjs && node test-doctor.mjs && node test-crew-env.mjs && node test-desktop-transport.mjs && node test-splitbrain.mjs && node test-overseer.mjs && node test-overseer-lib.mjs && node test-overseer-warn.mjs && node test-provider-keys.mjs && node test-inbox-delivery.mjs && node test-identity-drift.mjs && node test-inbox-staleness.mjs && node test-seats.mjs && node test-seat-identity.mjs && node test-hub-routing.mjs && node test-hook-routing.mjs && node test-relay-wait.mjs && node test-dsh-seat.mjs && node test-kimi-bridge.mjs && node test-kimi-events.mjs && python3 engine/test-routing.py && node test-reconcile.mjs && node test-resources.mjs && node test-patrol.mjs && node test-bridge.mjs && bash test-crew.sh"
|
|
14
|
+
"test": "node bin/slop-gate.mjs --surface desktop/src && node test.mjs && node test-scenarios.mjs && node test-failure.mjs && node test-redelivery.mjs && node test-crew-completion.mjs && node test-contracts.mjs && node test-handoff.mjs && node test-handoff-summarizer.mjs && node test-baton-turn-boundary.mjs && node test-agents.mjs && node test-update.mjs && node test-handoff-guard.mjs && node test-baton-latest.mjs && node test-balances.mjs && node test-subagent-cost.mjs && node test-inbox.mjs && node test-inflight.mjs && node test-notify.mjs && node test-focus.mjs && node test-cardlog.mjs && node test-relay-note.mjs && node test-reaper.mjs && node test-events.mjs && node test-proposals.mjs && node test-scrub.mjs && node test-store-delta.mjs && node test-message-re.mjs && node test-claims.mjs && node test-adopt.mjs && node test-summarize.mjs && node test-identity-core.mjs && node test-identity.mjs && node test-identity-instances.mjs && node test-duty.mjs && node test-duty-seat.mjs && node test-discovery.mjs && node test-doctor.mjs && node test-crew-env.mjs && node test-desktop-transport.mjs && node test-crew-worktree.mjs && bash test-crew-herdr.sh && npm --prefix desktop run test --silent && node test-splitbrain.mjs && node test-overseer.mjs && node test-overseer-lib.mjs && node test-overseer-warn.mjs && node test-provider-keys.mjs && node test-inbox-delivery.mjs && node test-identity-drift.mjs && node test-inbox-staleness.mjs && node test-seats.mjs && node test-seat-identity.mjs && node test-hub-routing.mjs && node test-hook-routing.mjs && node test-relay-wait.mjs && node test-dsh-seat.mjs && node test-kimi-bridge.mjs && node test-kimi-events.mjs && python3 engine/test-routing.py && node test-reconcile.mjs && node test-resources.mjs && node test-patrol.mjs && node test-bridge.mjs && bash test-crew.sh"
|
|
15
15
|
},
|
|
16
16
|
"description": "The hub-world for AI agent crews \u2014 orchestrate Claude Code, Codex, GLM, Kimi, DeepSeek & any OpenRouter model as live crews with a plan-aware Advisor, a Kanban/flow command center, a testing gate, and an economics brain (Scrooge).",
|
|
17
17
|
"files": [
|