trantor 0.18.9 → 0.18.11
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 +5 -1
- package/bin/adopt.mjs +89 -132
- package/bin/autonomy.mjs +83 -0
- package/bin/baton.mjs +5 -2
- package/bin/cli.mjs +12 -0
- package/bin/crew-runner.mjs +64 -8
- package/bin/crew.sh +258 -18
- package/bin/herdr-agent.mjs +96 -0
- package/bin/integrate.mjs +86 -0
- package/bin/seat-why.mjs +27 -0
- package/hooks/lib/handoff.mjs +3 -1
- package/hooks/sessionstart.mjs +39 -8
- package/hub.mjs +63 -4
- package/lib/autonomy.mjs +101 -0
- package/lib/integrate.mjs +94 -0
- package/lib/project.mjs +48 -1
- package/lib/seat-why.mjs +123 -0
- package/mcp.mjs +17 -7
- package/package.json +2 -2
package/bin/crew.sh
CHANGED
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
# session's teardown can't nuke out from under ANOTHER session's crew.
|
|
4
4
|
#
|
|
5
5
|
# bin/crew.sh up codex glm kimi deepseek # bring up THIS project's crew (tmux panes, or Terminal windows)
|
|
6
|
+
# bin/crew.sh open [<project>] # host the operator's claude as the `orchestrator · <project>`
|
|
7
|
+
# # pane in herdr (reattaches; never stacks a second one)
|
|
6
8
|
# bin/crew.sh down # tear down ONLY THIS PROJECT's crew (scoped, prints what it kills)
|
|
7
9
|
# bin/crew.sh down codex # tear down ONE seat
|
|
8
10
|
# bin/crew.sh down --all --yes # tear down EVERY project's crew (global; --yes required)
|
|
@@ -46,11 +48,21 @@ HAVE_TMUX=0; command -v tmux >/dev/null 2>&1 && HAVE_TMUX=1
|
|
|
46
48
|
# Events) — NOT the control socket — so it needs NO socket password (the socket denies external processes
|
|
47
49
|
# by default; AppleScript bypasses that, exactly like we already script Terminal.app).
|
|
48
50
|
HAVE_CMUX=0; [ -d "/Applications/cmux.app" ] && HAVE_CMUX=1
|
|
51
|
+
# PRESENCE vs PREFERENCE: HAVE_* answer "which mux do NEW crews get" and are stomped by CREW_MUX and
|
|
52
|
+
# the herdr auto-preference below. *_PRESENT answer "is this mux on the machine at all" and are what
|
|
53
|
+
# PRUNE keys on — a row is validated by ITS OWN kind's liveness, never by which mux new crews prefer.
|
|
54
|
+
# Conflating them let a dead cmux row survive prune the moment herdr became the preferred default.
|
|
55
|
+
CMUX_PRESENT=$HAVE_CMUX
|
|
56
|
+
HERDR_PRESENT=0; command -v herdr >/dev/null 2>&1 && HERDR_PRESENT=1
|
|
49
57
|
# herdr (https://herdr.dev — a server-held terminal runtime: panes live in its background server, so a
|
|
50
|
-
# crew survives the launcher exiting) is
|
|
51
|
-
#
|
|
52
|
-
#
|
|
58
|
+
# crew survives the launcher exiting) is the PREFERRED mux when its binary is installed: it is the
|
|
59
|
+
# only backend the desktop app's Workspace pane can render, so an auto-detected herdr means every
|
|
60
|
+
# `trantor up` on every project shows up live in the app with no flag. (It was opt-in for exactly one
|
|
61
|
+
# wave; the first thing the operator noticed was that other projects' crews stayed invisible.)
|
|
62
|
+
# CREW_MUX=cmux|tmux|terminal still forces the old dispatch; CREW_MUX=herdr without the binary stays
|
|
63
|
+
# a HARD ERROR, because an explicit ask must never silently fall back.
|
|
53
64
|
HAVE_HERDR=0
|
|
65
|
+
[ -z "${CREW_MUX:-}" ] && command -v herdr >/dev/null 2>&1 && { HAVE_HERDR=1; HAVE_CMUX=0; HAVE_TMUX=0; }
|
|
54
66
|
# explicit override (user preference or tests): CREW_MUX=cmux|tmux|terminal|herdr forces the grouping UI.
|
|
55
67
|
case "${CREW_MUX:-}" in
|
|
56
68
|
cmux) HAVE_CMUX=1; HAVE_TMUX=0 ;;
|
|
@@ -100,7 +112,7 @@ _cmux() { CMUX_QUIET=1 "$CMUX_BIN" "$@"; } # quiet CLI wrapper (suppr
|
|
|
100
112
|
_CMUX_OK="" # cached: does the control socket accept us (allowAll)?
|
|
101
113
|
_cmux_ok() {
|
|
102
114
|
[ -n "$_CMUX_OK" ] && { [ "$_CMUX_OK" = "1" ] && return 0 || return 1; }
|
|
103
|
-
if [ "$
|
|
115
|
+
if [ "$CMUX_PRESENT" = "1" ] && _cmux ping >/dev/null 2>&1; then _CMUX_OK=1; return 0; fi
|
|
104
116
|
_CMUX_OK=0; return 1
|
|
105
117
|
}
|
|
106
118
|
# resolve a freshly-created workspace REF (workspace:N) → its stable UUID (survives index shifts).
|
|
@@ -170,6 +182,103 @@ console.log("\u0001"+a.map(x=>x.label||x.name||x.custom_title||"").filter(Boolea
|
|
|
170
182
|
# Teardown works regardless of CREW_MUX: you must never need to remember the flag to tear a crew down.
|
|
171
183
|
_herdr_close_ws() { [ "$DRY" = "1" ] && { echo "[dry] herdr workspace close $1"; return 0; }; _herdr workspace close "$1" >/dev/null 2>&1; }
|
|
172
184
|
_herdr_close_pane() { [ "$DRY" = "1" ] && { echo "[dry] herdr pane close $1"; return 0; }; _herdr pane close "$1" >/dev/null 2>&1; }
|
|
185
|
+
# Tell herdr a pane hosts an agent. WITHOUT this the pane is just a shell to herdr, `herdr agent
|
|
186
|
+
# attach` answers agent_not_found, and the app's terminal shows that error instead of the seat —
|
|
187
|
+
# observed 2026-08-27 on every seat while the orchestrator (claude, which herdr detects on its own)
|
|
188
|
+
# streamed fine. NOTE the argument order: the pane id must come FIRST, before the flags.
|
|
189
|
+
# ── orchestrator continuity ────────────────────────────────────────────────────────────────────
|
|
190
|
+
# herdr keeps the PANE alive across an app quit, and launchd keeps its server alive across a
|
|
191
|
+
# reboot. Neither preserves the CONVERSATION: a pty that dies comes back empty, because the
|
|
192
|
+
# transcript was never a property of the terminal. That part is ours.
|
|
193
|
+
#
|
|
194
|
+
# So the project gets ONE claude session id, chosen by us and remembered. First open starts claude
|
|
195
|
+
# under it; every later open resumes it. Discovering the id afterwards would be guesswork, and
|
|
196
|
+
# `--continue` would grab whatever ran last in this directory, which may be a different window.
|
|
197
|
+
_orch_sid() { # $1=project → the project's session uuid, minting one on first use
|
|
198
|
+
local f="${STATE%/*}/orch-sessions.txt" p sid
|
|
199
|
+
if [ -f "$f" ]; then
|
|
200
|
+
while IFS="$(printf '\t')" read -r p sid; do
|
|
201
|
+
[ "$p" = "$1" ] && [ -n "$sid" ] && { printf '%s' "$sid"; return 0; }
|
|
202
|
+
done < "$f"
|
|
203
|
+
fi
|
|
204
|
+
sid="$(uuidgen 2>/dev/null | tr 'A-Z' 'a-z')"
|
|
205
|
+
[ -n "$sid" ] || return 1
|
|
206
|
+
[ "$DRY" = "1" ] || { mkdir -p "${f%/*}"; printf '%s\t%s\n' "$1" "$sid" >> "$f"; }
|
|
207
|
+
printf '%s' "$sid"
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
# claude writes each conversation to ~/.claude/projects/<slug>/<session-id>.jsonl, where the slug
|
|
211
|
+
# is the working directory with every / and . turned into -.
|
|
212
|
+
_orch_transcript() { printf '%s' "$HOME/.claude/projects/$(printf '%s' "$1" | tr '/.' '--')/$2.jsonl"; }
|
|
213
|
+
|
|
214
|
+
# A herdr server started from inside a Claude Code session hands its whole environment to every
|
|
215
|
+
# pane it later spawns, and those markers are session-scoped, not machine-scoped. The damage is
|
|
216
|
+
# quiet and total: claude sees CLAUDE_CODE_CHILD_SESSION, decides it is a sub-session and TURNS
|
|
217
|
+
# TRANSCRIPT SAVING OFF — so --resume would have nothing to resume and the persistence above would
|
|
218
|
+
# be theatre. CLAUDE_CODE_MESSAGING_SOCKET/TOKEN are worse: they point the new session at the
|
|
219
|
+
# ORIGINATING session's socket, which is how a fresh pane ends up claiming another session's baton.
|
|
220
|
+
#
|
|
221
|
+
# Feature flags the operator actually set (AGENT_TEAMS, FORK_SUBAGENT, EFFORT) are deliberately
|
|
222
|
+
# left alone. Only identity is stripped.
|
|
223
|
+
ORCH_STRIP="CLAUDECODE CLAUDE_CODE_ENTRYPOINT CLAUDE_CODE_SESSION_ID CLAUDE_CODE_CHILD_SESSION CLAUDE_CODE_BRIDGE_SESSION_ID CLAUDE_CODE_MESSAGING_SOCKET CLAUDE_CODE_MESSAGING_TOKEN CLAUDE_CODE_EXECPATH CLAUDE_PID"
|
|
224
|
+
# TRANTOR_ORCH marks the pane's claude as THE orchestrator pane for $PROJ: the sessionstart hook
|
|
225
|
+
# uses it to claim a held orchestrator baton immediately and to record its own session id in
|
|
226
|
+
# orch-sessions.txt (project-matched on the hook side, so a child claude elsewhere can't inherit it).
|
|
227
|
+
_orch_env() { local v out="env"; for v in $ORCH_STRIP; do out="$out -u $v"; done; printf '%s TRANTOR_ORCH=%s' "$out" "$PROJ"; }
|
|
228
|
+
|
|
229
|
+
# The HARNESS dial (trantor autonomy). "bypass" is what the operator means by letting the agent
|
|
230
|
+
# just work; "prompt" is the default because a fresh install must never silently skip a permission
|
|
231
|
+
# the operator has not chosen to skip. Read per open, so changing it in the app takes effect the
|
|
232
|
+
# next time the session is started rather than needing a reinstall.
|
|
233
|
+
_orch_flags() {
|
|
234
|
+
local mode
|
|
235
|
+
mode="$(node "$(dirname "$0")/autonomy.mjs" get harness --project "$PROJ" 2>/dev/null)"
|
|
236
|
+
[ "$mode" = "bypass" ] && printf ' --dangerously-skip-permissions'
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
# Resume when the conversation already exists on disk; otherwise start it under the id we picked so
|
|
240
|
+
# the NEXT open can resume it.
|
|
241
|
+
_orch_cmd() { # $1=dir $2=sid
|
|
242
|
+
if [ -f "$(_orch_transcript "$1" "$2")" ]; then printf '%s claude%s --resume %s' "$(_orch_env)" "$(_orch_flags)" "$2"
|
|
243
|
+
else printf '%s claude%s --session-id %s' "$(_orch_env)" "$(_orch_flags)" "$2"; fi
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
# A recorded thread that HANDED OFF has ended: resuming it replays a dead conversation while the
|
|
247
|
+
# handoff waits for a successor (the 2026-08-27 seam — "Trantor resumes the wrong thread"). When the
|
|
248
|
+
# newest unconsumed handoff for the project was written BY the recorded session, open must start a
|
|
249
|
+
# FRESH id instead; the sessionstart hook then claims the baton and records the fresh id in
|
|
250
|
+
# orch-sessions.txt (single writer for the map: the hook + adopt — this function writes nothing).
|
|
251
|
+
_orch_takeover_sid() { # $1=project $2=recorded-sid → prints the sid open should use
|
|
252
|
+
local fresh
|
|
253
|
+
if node -e '
|
|
254
|
+
const fs=require("fs"),path=require("path"),os=require("os");
|
|
255
|
+
const dir=path.join(process.env.AGENT_BUS_DIR||process.env.RELAY_DATA_DIR||path.join(os.homedir(),".agent-bus"),"handoffs");
|
|
256
|
+
const [proj,sid]=process.argv.slice(1);
|
|
257
|
+
try{
|
|
258
|
+
const re=new RegExp("^"+proj.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")+"-(\\d+)\\.json$");
|
|
259
|
+
const files=fs.readdirSync(dir).map(f=>{const m=re.exec(f);return m?{f,s:Number(m[1])}:null}).filter(Boolean).sort((a,b)=>b.s-a.s);
|
|
260
|
+
for(const {f} of files){
|
|
261
|
+
const r=JSON.parse(fs.readFileSync(path.join(dir,f),"utf8"));
|
|
262
|
+
if(r.consumed) continue;
|
|
263
|
+
process.exit(r.session_id&&r.session_id===sid?0:1); // newest UNCONSUMED decides
|
|
264
|
+
}
|
|
265
|
+
}catch(e){}
|
|
266
|
+
process.exit(1);' "$1" "$2" 2>/dev/null; then
|
|
267
|
+
fresh="$(uuidgen 2>/dev/null | tr 'A-Z' 'a-z')"
|
|
268
|
+
[ -n "$fresh" ] && { echo "— recorded session $2 handed off: starting fresh as $fresh to claim it —" >&2; printf '%s' "$fresh"; return 0; }
|
|
269
|
+
fi
|
|
270
|
+
printf '%s' "$2"
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
# Does herdr see a LIVE agent in this pane? A pane answering `rename` only proves the pane exists;
|
|
274
|
+
# the process inside it can be long gone, and reattaching to a dead shell shows the operator an
|
|
275
|
+
# empty terminal that never fills.
|
|
276
|
+
_herdr_pane_has_agent() { _herdr agent list 2>/dev/null | grep -q "\"pane_id\":\"$1\""; }
|
|
277
|
+
|
|
278
|
+
_herdr_report_agent() { # $1=pane $2=agent-label
|
|
279
|
+
[ "$DRY" = "1" ] && { echo "[dry] herdr pane report-agent $1 --source crew --agent $2 --state working"; return 0; }
|
|
280
|
+
_herdr pane report-agent "$1" --source crew --agent "$2" --state working >/dev/null 2>&1
|
|
281
|
+
}
|
|
173
282
|
|
|
174
283
|
# ── herdr spawn: ONE workspace `trantor:$PROJ`, one named pane per seat ─────────────────────────────
|
|
175
284
|
# Topology = workspace create (its root pane runs seat 1) + pane split for the rest, same ceil(√N) grid
|
|
@@ -267,6 +376,9 @@ spawn_herdr() { # $@ = specs
|
|
|
267
376
|
[ -n "$surf" ] && { _herdr pane rename "$surf" "$AGENT · $PROJ" >/dev/null 2>&1; _herdr pane run "$surf" "$cmd" >/dev/null 2>&1; }
|
|
268
377
|
fi
|
|
269
378
|
fi
|
|
379
|
+
# Every path, dry included: herdr must know this pane hosts an agent or `agent attach` — which
|
|
380
|
+
# is how the app streams it — answers agent_not_found and the seat renders as an error.
|
|
381
|
+
[ -n "$surf" ] && _herdr_report_agent "$surf" "$AGENT"
|
|
270
382
|
surfs+=("$surf")
|
|
271
383
|
record_state "$PROJ" "herdr" "$AGENT" "$surf"
|
|
272
384
|
echo " → $AGENT seat in herdr workspace ($PROJ)"
|
|
@@ -328,7 +440,7 @@ down() {
|
|
|
328
440
|
|
|
329
441
|
# collect the rows in scope + build a human kill-list. `scoped` entries are joined with '|' (NOT a tab):
|
|
330
442
|
# tab is IFS whitespace, so re-splitting a tab-joined string would collapse an empty leading PROJECT.
|
|
331
|
-
local scoped=() line killlist="" seentmux=""
|
|
443
|
+
local scoped=() line killlist="" seentmux="" orch_scopes=""
|
|
332
444
|
while IFS= read -r line; do
|
|
333
445
|
[ -n "$line" ] || continue
|
|
334
446
|
_parse_row "$line" # → RP RK RA RH
|
|
@@ -336,14 +448,19 @@ down() {
|
|
|
336
448
|
# SKIPPED by a scoped down — we can't prove they're ours, and killing another project's crew is the exact
|
|
337
449
|
# bug we're fixing. `trantor down --all` reaches them; otherwise they self-heal via prune_dead_state.
|
|
338
450
|
if [ "$WANT_ALL" != "1" ] && [ "$RP" != "$PROJ" ]; then continue; fi
|
|
451
|
+
# a project hosting the orchestrator pane degrades to PER-SEAT teardown: its workspace must survive
|
|
452
|
+
# (the orch pane lives inside it — closing the workspace closes the terminal the operator is typing
|
|
453
|
+
# in, from inside that terminal), so only the seat panes are closed.
|
|
454
|
+
[ "$RK" = "orch" ] && case ";$orch_scopes;" in *";$RP;"*) : ;; *) orch_scopes="$orch_scopes;$RP;" ;; esac
|
|
339
455
|
if [ "${#AGENTS[@]}" -gt 0 ]; then # agent filter (if specific seats named)
|
|
340
456
|
local match=0 want; for want in "${AGENTS[@]}"; do [ "$RA" = "$want" ] && match=1; done
|
|
341
457
|
[ "$match" = "1" ] || continue
|
|
342
458
|
fi
|
|
343
459
|
scoped+=("$RP|$RK|$RA|$RH")
|
|
344
|
-
case "$RK" in attach|cmuxws|herdrws) continue ;; esac
|
|
460
|
+
case "$RK" in attach|cmuxws|herdrws|orch) continue ;; esac # infra rows (attach/cmux/herdr workspace/orch) — not seats; `orch` is SPARED entirely
|
|
345
461
|
killlist="$killlist • ${RP:-<legacy>} · $RA ($RK)"$'\n'
|
|
346
462
|
done < "$STATE"
|
|
463
|
+
_has_orch() { case "$orch_scopes" in *";$1;"*) return 0 ;; esac; return 1; }
|
|
347
464
|
|
|
348
465
|
if [ "${#scoped[@]}" -eq 0 ]; then echo "nothing to tear down for $SCOPE_DESC"; return 0; fi
|
|
349
466
|
echo "— tearing down ($SCOPE_DESC):"; printf '%s' "$killlist"
|
|
@@ -357,10 +474,10 @@ down() {
|
|
|
357
474
|
for s in "${scoped[@]}"; do
|
|
358
475
|
IFS='|' read -r P2 K2 A2 H2 <<< "$s"
|
|
359
476
|
case "$K2" in
|
|
360
|
-
cmuxws) [ "${#AGENTS[@]}" -gt 0 ] || _cmux_close_tab "$H2" ;; # whole workspace (
|
|
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 (
|
|
363
|
-
herdr) [ "${#AGENTS[@]}" -gt 0 ] && _herdr_close_pane "$H2" ;;# a single seat's pane
|
|
477
|
+
cmuxws) { [ "${#AGENTS[@]}" -gt 0 ] || _has_orch "$P2"; } || _cmux_close_tab "$H2" ;; # whole workspace (unless seats are named or an orch pane lives in it)
|
|
478
|
+
cmux) { [ "${#AGENTS[@]}" -gt 0 ] || _has_orch "$P2"; } && _cmux_close_term "$H2" ;; # a single seat's pane
|
|
479
|
+
herdrws) { [ "${#AGENTS[@]}" -gt 0 ] || _has_orch "$P2"; } || _herdr_close_ws "$H2" ;; # whole workspace (unless seats are named or an orch pane lives in it)
|
|
480
|
+
herdr) { [ "${#AGENTS[@]}" -gt 0 ] || _has_orch "$P2"; } && _herdr_close_pane "$H2" ;;# a single seat's pane
|
|
364
481
|
tmux)
|
|
365
482
|
if [ "${#AGENTS[@]}" -gt 0 ]; then run "tmux kill-pane -t '$H2' 2>/dev/null" # per-seat
|
|
366
483
|
else
|
|
@@ -372,7 +489,8 @@ down() {
|
|
|
372
489
|
case "$K2" in cmuxws|attach|herdrws) : ;; *) _kill_seat_procs "$P2" "$A2" ;; esac
|
|
373
490
|
done
|
|
374
491
|
|
|
375
|
-
# rewrite STATE minus the rows we tore down (leaves OTHER projects' rows intact)
|
|
492
|
+
# rewrite STATE minus the rows we tore down (leaves OTHER projects' rows intact). An orch-hosting
|
|
493
|
+
# project's workspace rows survive too — the workspace is still needed by the spared orch pane.
|
|
376
494
|
if [ "$DRY" != "1" ]; then
|
|
377
495
|
local tmp="$STATE.tmp"; : > "$tmp"
|
|
378
496
|
while IFS= read -r line; do
|
|
@@ -380,6 +498,10 @@ down() {
|
|
|
380
498
|
_parse_row "$line"
|
|
381
499
|
local drop=0
|
|
382
500
|
for s in "${scoped[@]}"; do [ "$s" = "$RP|$RK|$RA|$RH" ] && drop=1; done
|
|
501
|
+
# `orch` itself is spared alongside the workspace that hosts it: down never closes the
|
|
502
|
+
# orchestrator pane, so dropping its row would orphan a live pane and let the next `open`
|
|
503
|
+
# stack a second orchestrator on top of it.
|
|
504
|
+
[ "$drop" = "1" ] && _has_orch "$RP" && case "$RK" in herdrws|cmuxws|orch) drop=0 ;; esac
|
|
383
505
|
[ "$drop" = "1" ] || printf '%s\t%s\t%s\t%s\n' "$RP" "$RK" "$RA" "$RH" >> "$tmp"
|
|
384
506
|
done < "$STATE"
|
|
385
507
|
mv "$tmp" "$STATE"
|
|
@@ -388,7 +510,7 @@ down() {
|
|
|
388
510
|
echo "— crew torn down ($SCOPE_DESC)"
|
|
389
511
|
}
|
|
390
512
|
[ "$CMD" = "down" ] && { down "$@"; exit $?; }
|
|
391
|
-
case "$CMD" in up|swap|prune) ;; *) echo "usage: crew.sh up <agent...> | crew.sh swap <old> <new[:provider[/model]]> | crew.sh down [<agent>...] [--all --yes] | crew.sh prune"; exit 1 ;; esac
|
|
513
|
+
case "$CMD" in up|open|swap|prune) ;; *) echo "usage: crew.sh up <agent...> | crew.sh open [<project>] | crew.sh swap <old> <new[:provider[/model]]> | crew.sh down [<agent>...] [--all --yes] | crew.sh prune"; exit 1 ;; esac
|
|
392
514
|
|
|
393
515
|
# self-heal: drop STATE rows whose Terminal window is already gone (dead crews from past sessions), so the
|
|
394
516
|
# file doesn't accumulate ghosts across ups. tmux rows are validated by their session existing; cmux rows
|
|
@@ -409,10 +531,11 @@ prune_dead_state() {
|
|
|
409
531
|
CLIVE="$(printf '%s' "$pair" | sed -n 1p)"
|
|
410
532
|
CLIVE_NAMES="$(printf '%s' "$pair" | sed -n 2p)"
|
|
411
533
|
fi
|
|
412
|
-
# herdr liveness is queried
|
|
413
|
-
#
|
|
534
|
+
# herdr liveness is queried whenever the BINARY is present (herdr rows deserve validation no
|
|
535
|
+
# matter which mux new crews prefer). Same keep-when-unprovable doctrine as cmux — no answer,
|
|
536
|
+
# or no binary at all ⇒ rows are KEPT.
|
|
414
537
|
local HLIVE="" HLIVE_NAMES=""
|
|
415
|
-
if [ "$
|
|
538
|
+
if [ "$HERDR_PRESENT" = "1" ]; then
|
|
416
539
|
local hpair
|
|
417
540
|
hpair="$(_herdr_ws_live)"
|
|
418
541
|
HLIVE="$(printf '%s' "$hpair" | sed -n 1p)"
|
|
@@ -435,9 +558,9 @@ prune_dead_state() {
|
|
|
435
558
|
if [ -n "$CLIVE" ]; then case "$CLIVE_NAMES" in *$'\x01'"trantor:$RP"$'\x01'*) : ;; *) alive=0 ;; esac; fi
|
|
436
559
|
elif [ "$RK" = "herdrws" ]; then
|
|
437
560
|
if [ -n "$HLIVE" ]; then case " $HLIVE " in *" $RH "*) : ;; *) alive=0 ;; esac; fi
|
|
438
|
-
elif [ "$RK" = "herdr" ]; then
|
|
439
|
-
# seat
|
|
440
|
-
# WORKSPACE granularity, never per pane (the cmux 0.17.61 lesson generalized)
|
|
561
|
+
elif [ "$RK" = "herdr" ] || [ "$RK" = "orch" ]; then
|
|
562
|
+
# seat + orchestrator rows live exactly as long as their project still has a live crew workspace
|
|
563
|
+
# — validated at WORKSPACE granularity, never per pane (the cmux 0.17.61 lesson generalized)
|
|
441
564
|
if [ -n "$HLIVE" ]; then case "$HLIVE_NAMES" in *$'\x01'"trantor:$RP"$'\x01'*) : ;; *) alive=0 ;; esac; fi
|
|
442
565
|
fi
|
|
443
566
|
[ "$alive" = "1" ] && printf '%s\t%s\t%s\t%s\n' "$RP" "$RK" "$RA" "$RH" >> "$tmp"
|
|
@@ -447,6 +570,123 @@ prune_dead_state() {
|
|
|
447
570
|
# `crew.sh prune` — run the self-heal on demand (ops: clean ghost rows without spawning anything).
|
|
448
571
|
[ "$CMD" = "prune" ] && { prune_dead_state; echo "— pruned dead crew rows ($STATE) —"; exit 0; }
|
|
449
572
|
|
|
573
|
+
# ── open: host the OPERATOR's session as the `orchestrator · <project>` pane (card #5396) ────────────
|
|
574
|
+
# The session a developer actually works in used to be an unowned Terminal window — invisible to the
|
|
575
|
+
# app, and killed by any whole-project `down` that closed the workspace under it. `trantor open`
|
|
576
|
+
# hosts it as a herdr pane in the SAME `trantor:<project>` workspace the crew uses (herdr keeps the
|
|
577
|
+
# pane alive server-side; Terminal attaches to the SAME session), and prints the pane TARGET on
|
|
578
|
+
# stdout as ONE line: herdr:<workspace_id>/<pane_id> (human chatter goes to stderr, so the
|
|
579
|
+
# target is capturable). REATTACH, NEVER STACK: a second open finds the tracked `orch` row alive and
|
|
580
|
+
# reprints the target — two orchestrator claude processes on one project would fight over one bus
|
|
581
|
+
# identity (the duplicated-runner lesson, crew.sh edition). The liveness probe is an idempotent
|
|
582
|
+
# rename to the pane's own title: no per-pane list API is trusted (the 0.17.61 workspace-granularity
|
|
583
|
+
# lesson), and a closed pane can never pass it. `down` SPARES `orch` rows for the same reason.
|
|
584
|
+
usage_open() {
|
|
585
|
+
cat <<EOF
|
|
586
|
+
usage: trantor open [<project>]
|
|
587
|
+
host THIS project's orchestrator session as a herdr pane in workspace trantor:<project>
|
|
588
|
+
(creating the workspace if needed) and print its TARGET on stdout. A second open REATTACHES:
|
|
589
|
+
it prints the existing target and exits 0 without spawning a second orchestrator.
|
|
590
|
+
EOF
|
|
591
|
+
}
|
|
592
|
+
open_orchestrator() {
|
|
593
|
+
local a
|
|
594
|
+
for a in "$@"; do case "$a" in
|
|
595
|
+
--help|-h) usage_open; return 0 ;;
|
|
596
|
+
--*) echo "trantor open: unknown flag '$a'"; usage_open; return 1 ;;
|
|
597
|
+
*) PROJ="$a" ;;
|
|
598
|
+
esac; done
|
|
599
|
+
command -v herdr >/dev/null 2>&1 || { echo "trantor open needs herdr (the pane host) — install: curl -fsSL https://herdr.dev/install.sh | sh"; exit 1; }
|
|
600
|
+
local wsid="" orch="" live_ids="" live_names="" pair="" line fresh=0
|
|
601
|
+
local sid; sid="$(_orch_sid "$PROJ")" || { echo "trantor open: could not mint a session id (uuidgen missing?)" >&2; exit 1; }
|
|
602
|
+
sid="$(_orch_takeover_sid "$PROJ" "$sid")"
|
|
603
|
+
if [ "$DRY" != "1" ]; then
|
|
604
|
+
pair="$(_herdr_ws_live)"
|
|
605
|
+
live_ids="$(printf '%s' "$pair" | sed -n 1p)"
|
|
606
|
+
live_names="$(printf '%s' "$pair" | sed -n 2p)"
|
|
607
|
+
fi
|
|
608
|
+
if [ -f "$STATE" ]; then
|
|
609
|
+
while IFS= read -r line; do
|
|
610
|
+
[ -n "$line" ] || continue
|
|
611
|
+
_parse_row "$line"
|
|
612
|
+
[ "$RP" = "$PROJ" ] || continue
|
|
613
|
+
case "$RK" in
|
|
614
|
+
herdrws)
|
|
615
|
+
# a tracked workspace is only usable while LIVE (label match); a provably dead row is dropped.
|
|
616
|
+
# No live answer ⇒ kept (the keep-when-unprovable doctrine shared with prune).
|
|
617
|
+
if [ "$DRY" != "1" ] && [ -n "$live_ids" ]; then
|
|
618
|
+
case " $live_ids " in *" $RH "*) wsid="$RH" ;; *) _state_drop "$PROJ" "herdrws" "" "$RH" ;; esac
|
|
619
|
+
else wsid="$RH"; fi ;;
|
|
620
|
+
orch) orch="$RH" ;;
|
|
621
|
+
esac
|
|
622
|
+
done < "$STATE"
|
|
623
|
+
fi
|
|
624
|
+
# REATTACH, never stack: a tracked orch pane that still answers is reused as-is.
|
|
625
|
+
if [ -n "$orch" ]; then
|
|
626
|
+
if [ "$DRY" = "1" ]; then
|
|
627
|
+
echo "herdr:${wsid:-%DRYWS}/$orch"
|
|
628
|
+
return 0
|
|
629
|
+
fi
|
|
630
|
+
if _herdr pane rename "$orch" "orchestrator · $PROJ" >/dev/null 2>&1; then
|
|
631
|
+
if _herdr_pane_has_agent "$orch"; then
|
|
632
|
+
echo "herdr:${wsid:-?}/$orch"
|
|
633
|
+
echo "— orchestrator already hosted: reattached to herdr:${wsid:-?}/$orch —" >&2
|
|
634
|
+
return 0
|
|
635
|
+
fi
|
|
636
|
+
# The pane outlived its claude (a crash, or a reboot that took the process but not the row).
|
|
637
|
+
# Restart the CONVERSATION in the same pane rather than handing back an empty terminal.
|
|
638
|
+
_herdr pane run "$orch" "$(_orch_cmd "$DIR" "$sid")" >/dev/null 2>&1
|
|
639
|
+
_herdr_report_agent "$orch" "claude"
|
|
640
|
+
echo "herdr:${wsid:-?}/$orch"
|
|
641
|
+
echo "— orchestrator pane was empty: resumed session $sid in herdr:${wsid:-?}/$orch —" >&2
|
|
642
|
+
return 0
|
|
643
|
+
fi
|
|
644
|
+
_state_drop "$PROJ" "orch" "" ""; orch="" # stale row → healed by the create path below
|
|
645
|
+
fi
|
|
646
|
+
# No usable workspace yet: adopt an untracked LIVE crew workspace before creating a second one
|
|
647
|
+
# (spawn_herdr's adopt-else-create doctrine — open must not stack trantor:<project> workspaces).
|
|
648
|
+
if [ -z "$wsid" ]; then
|
|
649
|
+
if [ "$DRY" = "1" ]; then
|
|
650
|
+
wsid="%DRYWS"
|
|
651
|
+
echo "[dry] herdr: workspace create (cwd $DIR) --label 'trantor:$PROJ'" >&2
|
|
652
|
+
else
|
|
653
|
+
local found=""
|
|
654
|
+
case "$live_names" in
|
|
655
|
+
*$'\x01'"trantor:$PROJ"$'\x01'*)
|
|
656
|
+
found="$(_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)[0]||"")}catch(e){}})')"
|
|
657
|
+
[ -n "$found" ] && echo "— adopting existing crew workspace for $PROJ ($found) —" >&2 ;;
|
|
658
|
+
esac
|
|
659
|
+
if [ -n "$found" ]; then wsid="$found"
|
|
660
|
+
else
|
|
661
|
+
pair="$(_herdr_ws_create "$DIR" "trantor:$PROJ")"
|
|
662
|
+
wsid="${pair%%$'\t'*}"; fresh=1
|
|
663
|
+
fi
|
|
664
|
+
[ -n "$wsid" ] || { echo "trantor open: herdr workspace create failed" >&2; exit 1; }
|
|
665
|
+
fi
|
|
666
|
+
record_state "$PROJ" "herdrws" "__ws__" "$wsid"
|
|
667
|
+
fi
|
|
668
|
+
# Host the pane: a JUST-created workspace's root pane IS the orchestrator pane (claude rides it);
|
|
669
|
+
# an existing workspace gets a split — the crew's seats tile off it on the next `up`, never
|
|
670
|
+
# replacing it (spawn_herdr's REUSE mode splits seats off their own old pane / the previous one).
|
|
671
|
+
if [ "$DRY" = "1" ]; then
|
|
672
|
+
orch="%DRYORCH"
|
|
673
|
+
echo "[dry] herdr: ${fresh:+root pane + }pane rename $orch 'orchestrator · $PROJ' + run '$(_orch_cmd "$DIR" "$sid")'" >&2
|
|
674
|
+
else
|
|
675
|
+
if [ "$fresh" = "1" ]; then orch="${pair##*$'\t'}"
|
|
676
|
+
else orch="$(_herdr_split "" right)"; fi
|
|
677
|
+
[ -n "$orch" ] || { echo "trantor open: could not create the orchestrator pane" >&2; exit 1; }
|
|
678
|
+
_herdr pane rename "$orch" "orchestrator · $PROJ" >/dev/null 2>&1
|
|
679
|
+
_herdr pane run "$orch" "$(_orch_cmd "$DIR" "$sid")" >/dev/null 2>&1
|
|
680
|
+
_herdr_report_agent "$orch" "claude"
|
|
681
|
+
fi
|
|
682
|
+
_state_drop "$PROJ" "orch" "" "" # replace-never-stack: exactly one orch row per project
|
|
683
|
+
record_state "$PROJ" "orch" "__orch__" "$orch"
|
|
684
|
+
echo "herdr:$wsid/$orch"
|
|
685
|
+
echo "— orchestrator pane hosted: herdr:$wsid/$orch (claude in $DIR). 'trantor down' spares it. —" >&2
|
|
686
|
+
return 0
|
|
687
|
+
}
|
|
688
|
+
[ "$CMD" = "open" ] && { open_orchestrator "$@"; exit $?; }
|
|
689
|
+
|
|
450
690
|
# --task/--difficulty drive LAZY live-model selection for provider-only specs (agent:provider).
|
|
451
691
|
TASK="code"; DIFF="medium"; _ARGS=()
|
|
452
692
|
while [ $# -gt 0 ]; do
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Keep herdr's server alive across a reboot.
|
|
3
|
+
//
|
|
4
|
+
// herdr already holds every pane in a background server, so quitting the app or closing a Terminal
|
|
5
|
+
// window never costs you a session. A REBOOT does: the server dies with everything else, and
|
|
6
|
+
// nothing brings it back, so the next `trantor open` finds no workspace and starts over. That was
|
|
7
|
+
// the missing half of "it survives restarts" (card #5401).
|
|
8
|
+
//
|
|
9
|
+
// This is only the SERVER's lifetime. The conversation inside a pane is a separate problem, solved
|
|
10
|
+
// in crew.sh: the project keeps one claude session id and `open` resumes it.
|
|
11
|
+
import { existsSync, writeFileSync, unlinkSync, mkdirSync } from "node:fs";
|
|
12
|
+
import { execFileSync } from "node:child_process";
|
|
13
|
+
import { homedir } from "node:os";
|
|
14
|
+
import { join, dirname } from "node:path";
|
|
15
|
+
|
|
16
|
+
const LABEL = "com.trantor.herdr";
|
|
17
|
+
const PLIST = join(homedir(), "Library", "LaunchAgents", `${LABEL}.plist`);
|
|
18
|
+
const LOG = "/tmp/trantor-herdr-server.log";
|
|
19
|
+
const D = "\x1b[2m", R = "\x1b[0m";
|
|
20
|
+
|
|
21
|
+
function herdrBin() {
|
|
22
|
+
// launchd runs with a minimal PATH, so the absolute path has to be baked in at install time.
|
|
23
|
+
for (const p of [join(homedir(), ".local/bin/herdr"), "/opt/homebrew/bin/herdr", "/usr/local/bin/herdr"]) {
|
|
24
|
+
if (existsSync(p)) return p;
|
|
25
|
+
}
|
|
26
|
+
try {
|
|
27
|
+
const found = execFileSync("/usr/bin/which", ["herdr"], { encoding: "utf8" }).trim();
|
|
28
|
+
if (found) return found;
|
|
29
|
+
} catch { /* not on PATH either */ }
|
|
30
|
+
return "";
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const plistBody = (bin) => `<?xml version="1.0" encoding="UTF-8"?>
|
|
34
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
35
|
+
<plist version="1.0">
|
|
36
|
+
<dict>
|
|
37
|
+
<key>Label</key><string>${LABEL}</string>
|
|
38
|
+
<key>ProgramArguments</key>
|
|
39
|
+
<array>
|
|
40
|
+
<string>/bin/bash</string>
|
|
41
|
+
<string>-lc</string>
|
|
42
|
+
<!-- Idempotent on purpose. A second herdr server against a live one prints "already running"
|
|
43
|
+
and exits 1, so a plain KeepAlive would respawn it every 10s forever. Bail out cleanly
|
|
44
|
+
instead, and only exec when nothing is holding the socket. -->
|
|
45
|
+
<string>${bin} status 2>/dev/null | grep -q 'status: running' && exit 0; exec ${bin} server</string>
|
|
46
|
+
</array>
|
|
47
|
+
<key>RunAtLoad</key><true/>
|
|
48
|
+
<!-- Restart a server that DIED, never one that stopped cleanly (herdr server stop) or one that
|
|
49
|
+
bailed because another was already up. -->
|
|
50
|
+
<key>KeepAlive</key><dict><key>SuccessfulExit</key><false/></dict>
|
|
51
|
+
<key>StandardOutPath</key><string>${LOG}</string>
|
|
52
|
+
<key>StandardErrorPath</key><string>${LOG}</string>
|
|
53
|
+
</dict>
|
|
54
|
+
</plist>
|
|
55
|
+
`;
|
|
56
|
+
|
|
57
|
+
const cmd = process.argv[2] || "status";
|
|
58
|
+
const uid = process.getuid();
|
|
59
|
+
|
|
60
|
+
if (cmd === "install") {
|
|
61
|
+
const bin = herdrBin();
|
|
62
|
+
if (!bin) {
|
|
63
|
+
console.error("herdr is not installed — get it first: curl -fsSL https://herdr.dev/install.sh | sh");
|
|
64
|
+
process.exit(1);
|
|
65
|
+
}
|
|
66
|
+
mkdirSync(dirname(PLIST), { recursive: true });
|
|
67
|
+
writeFileSync(PLIST, plistBody(bin));
|
|
68
|
+
// bootout first so a re-install replaces rather than stacks
|
|
69
|
+
try { execFileSync("/bin/launchctl", ["bootout", `gui/${uid}/${LABEL}`], { stdio: "ignore" }); } catch { /* not loaded */ }
|
|
70
|
+
try {
|
|
71
|
+
execFileSync("/bin/launchctl", ["bootstrap", `gui/${uid}`, PLIST], { stdio: "ignore" });
|
|
72
|
+
} catch (e) {
|
|
73
|
+
console.error(`wrote ${PLIST} but launchctl bootstrap failed: ${e?.message || e}`);
|
|
74
|
+
process.exit(1);
|
|
75
|
+
}
|
|
76
|
+
console.log(`herdr's server now starts at login (${bin} server)`);
|
|
77
|
+
console.log(`${D}plist: ${PLIST} log: ${LOG}${R}`);
|
|
78
|
+
} else if (cmd === "remove") {
|
|
79
|
+
try { execFileSync("/bin/launchctl", ["bootout", `gui/${uid}/${LABEL}`], { stdio: "ignore" }); } catch { /* not loaded */ }
|
|
80
|
+
if (existsSync(PLIST)) { unlinkSync(PLIST); console.log("removed the herdr login agent"); }
|
|
81
|
+
else console.log("no herdr login agent installed");
|
|
82
|
+
} else if (cmd === "status") {
|
|
83
|
+
const installed = existsSync(PLIST);
|
|
84
|
+
let loaded = false;
|
|
85
|
+
try {
|
|
86
|
+
loaded = execFileSync("/bin/launchctl", ["list"], { encoding: "utf8" }).split("\n").some(l => l.includes(LABEL));
|
|
87
|
+
} catch { /* launchctl unavailable */ }
|
|
88
|
+
let running = false;
|
|
89
|
+
try { running = /status:\s*running/.test(execFileSync(herdrBin() || "herdr", ["status"], { encoding: "utf8" })); } catch { /* server down */ }
|
|
90
|
+
console.log(`login agent: ${installed ? "installed" : "not installed"}${installed ? ` (${loaded ? "loaded" : "NOT loaded"})` : ""}`);
|
|
91
|
+
console.log(`herdr server: ${running ? "running" : "not running"}`);
|
|
92
|
+
if (!installed) console.log(`${D}install it so panes survive a reboot: trantor herdr install${R}`);
|
|
93
|
+
} else {
|
|
94
|
+
console.log("usage: trantor herdr install | remove | status");
|
|
95
|
+
process.exit(1);
|
|
96
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// `trantor integrate` — collect the crew's work, prove it, ship it.
|
|
3
|
+
//
|
|
4
|
+
// The steps are the ones the orchestrator already performs by hand. The dials decide how far it is
|
|
5
|
+
// allowed to go on its own, and every stop says which dial stopped it, so "why didn't it push"
|
|
6
|
+
// always has an answer.
|
|
7
|
+
import { resolveProject } from "../lib/project.mjs";
|
|
8
|
+
import { resolveAutonomy } from "../lib/autonomy.mjs";
|
|
9
|
+
import { seatWorktrees, commitSeatWork, seatAhead, mergeSeat, verify, git } from "../lib/integrate.mjs";
|
|
10
|
+
|
|
11
|
+
const D = "\x1b[2m", B = "\x1b[1m", G = "\x1b[32m", Y = "\x1b[33m", RED = "\x1b[31m", R = "\x1b[0m";
|
|
12
|
+
const args = process.argv.slice(2);
|
|
13
|
+
const dry = args.includes("--dry-run");
|
|
14
|
+
const repo = process.cwd();
|
|
15
|
+
const project = resolveProject(repo);
|
|
16
|
+
const a = resolveAutonomy(project);
|
|
17
|
+
|
|
18
|
+
console.log(`${B}integrate${R} · ${project} ${D}commit=${a.commit ? "on" : "off"} push=${a.push ? "on" : "off"} deploy=${a.deploy ? "on" : "off"}${R}`);
|
|
19
|
+
|
|
20
|
+
const base = git(repo, ["rev-parse", "--abbrev-ref", "HEAD"]);
|
|
21
|
+
const seats = seatWorktrees(project);
|
|
22
|
+
if (!seats.length) {
|
|
23
|
+
console.log(`${D}no seat worktrees for ${project} — nothing to integrate${R}`);
|
|
24
|
+
process.exit(0);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// 1. Each seat's uncommitted work becomes a commit AUTHORED TO THAT SEAT, so git blame stays a
|
|
28
|
+
// truthful answer to "who wrote this" once integration stops being manual.
|
|
29
|
+
let blocked = false;
|
|
30
|
+
for (const w of seats) {
|
|
31
|
+
const dirty = git(w.dir, ["status", "--porcelain"]);
|
|
32
|
+
if (!dirty) { console.log(` ${D}${w.agent}: clean${R}`); continue; }
|
|
33
|
+
if (!a.commit) {
|
|
34
|
+
console.log(` ${Y}${w.agent}: has uncommitted work, and commit is off${R} ${D}(trantor autonomy set commit on)${R}`);
|
|
35
|
+
blocked = true;
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
if (dry) { console.log(` ${D}[dry] would commit ${dirty.split("\n").length} file(s) as ${w.agent}${R}`); continue; }
|
|
39
|
+
const r = commitSeatWork(w);
|
|
40
|
+
console.log(` ${G}${w.agent}: committed ${r.sha}${R}`);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// 2. Merge what each seat is ahead by. A conflict stops the pass: two seats on the same lines is
|
|
44
|
+
// the collision the overseer exists to surface, not something to resolve silently.
|
|
45
|
+
const merged = [];
|
|
46
|
+
for (const w of seats) {
|
|
47
|
+
const { branch, ahead } = seatAhead(w, base);
|
|
48
|
+
if (!ahead) continue;
|
|
49
|
+
if (dry) { console.log(` ${D}[dry] would merge ${branch} (${ahead} commit(s))${R}`); merged.push(branch); continue; }
|
|
50
|
+
const m = mergeSeat(repo, branch);
|
|
51
|
+
if (m.conflict) {
|
|
52
|
+
console.log(` ${RED}${branch}: CONFLICT — left untouched${R}\n${D}${m.detail}${R}`);
|
|
53
|
+
blocked = true;
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
if (m.merged) { console.log(` ${G}merged ${branch} (${ahead} commit(s))${R}`); merged.push(branch); }
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (blocked) {
|
|
60
|
+
console.log(`\n${Y}stopping here — resolve the above before anything ships${R}`);
|
|
61
|
+
process.exit(1);
|
|
62
|
+
}
|
|
63
|
+
if (!merged.length && !dry) {
|
|
64
|
+
console.log(`${D}nothing new to integrate${R}`);
|
|
65
|
+
process.exit(0);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// 3. Proof. This gate ignores the dial on purpose: unverified work does not leave the machine,
|
|
69
|
+
// whatever the operator turned on.
|
|
70
|
+
if (dry) { console.log(`${D}[dry] would verify, then push=${a.push}${R}`); process.exit(0); }
|
|
71
|
+
console.log(`\n${D}verifying…${R}`);
|
|
72
|
+
const v = verify(repo);
|
|
73
|
+
if (!v.ok) {
|
|
74
|
+
console.log(`${RED}verify FAILED (${v.cmd}) — not pushing${R}\n${D}${v.tail}${R}`);
|
|
75
|
+
process.exit(1);
|
|
76
|
+
}
|
|
77
|
+
console.log(`${G}verified (${v.cmd})${R}`);
|
|
78
|
+
|
|
79
|
+
// 4. Ship.
|
|
80
|
+
if (!a.push) {
|
|
81
|
+
console.log(`${D}push is off — integrated and verified locally (trantor autonomy set push on)${R}`);
|
|
82
|
+
process.exit(0);
|
|
83
|
+
}
|
|
84
|
+
git(repo, ["push"]);
|
|
85
|
+
console.log(`${G}pushed ${base}${R}`);
|
|
86
|
+
if (a.deploy) console.log(`${D}deploy is on, but this project has no deploy step wired here yet${R}`);
|
package/bin/seat-why.mjs
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// trantor seat-why <agent> [--json] — explain why a crew seat is (not) working, straight from
|
|
3
|
+
// local ~/.agent-bus evidence. No hub needed; works even when the whole fleet is down.
|
|
4
|
+
import { seatWhy } from "../lib/seat-why.mjs";
|
|
5
|
+
import { resolveProject } from "../lib/project.mjs";
|
|
6
|
+
|
|
7
|
+
const args = process.argv.slice(2);
|
|
8
|
+
const agent = args.find(a => !a.startsWith("--"));
|
|
9
|
+
const asJson = args.includes("--json");
|
|
10
|
+
const projIdx = args.indexOf("--project");
|
|
11
|
+
const project = projIdx !== -1 && args[projIdx + 1]
|
|
12
|
+
? args[projIdx + 1]
|
|
13
|
+
: process.env.RELAY_PROJECT || resolveProject(process.cwd());
|
|
14
|
+
|
|
15
|
+
if (!agent) {
|
|
16
|
+
console.error("usage: trantor seat-why <agent> [--json] [--project <name>]");
|
|
17
|
+
process.exit(2);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const out = await seatWhy(project, agent);
|
|
21
|
+
if (asJson) {
|
|
22
|
+
console.log(JSON.stringify({ agent, project, ...out }, null, 2));
|
|
23
|
+
} else {
|
|
24
|
+
console.log(`seat ${agent}:${project} -> ${out.state}`);
|
|
25
|
+
console.log(`why: ${out.why}`);
|
|
26
|
+
console.log(`advice: ${out.advice}`);
|
|
27
|
+
}
|
package/hooks/lib/handoff.mjs
CHANGED
|
@@ -433,7 +433,9 @@ export function maybeSpawn(projectDir, conf = readConfig()) {
|
|
|
433
433
|
}
|
|
434
434
|
|
|
435
435
|
// The self-announcing fresh session command (single-quoted so it survives osascript→shell un-escaped).
|
|
436
|
-
|
|
436
|
+
// The recap must be READABLE: on 2026-08-27 a takeover session answered with three consecutive
|
|
437
|
+
// 4-5k-character status dumps and the operator abandoned the app. Brevity is part of the prompt.
|
|
438
|
+
export const RECAP_CMD = "claude 'Recap the handoff you just took over — task, state, next step — in at most 3 sentences. Then wait for me. Keep all replies short by default: no status tables, no headers, no walls of text unless I explicitly ask for detail.'";
|
|
437
439
|
|
|
438
440
|
// Spawn a fresh self-announcing session WITHOUT the dialog (manual handoff — the user already decided).
|
|
439
441
|
// ONE suppression check for every path that can open a terminal window. There were two names for
|