aiur-cli 0.0.3 → 0.0.4
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/README.md +1 -0
- package/bin/aiur.js +3 -2
- package/libexec/aiur-engine.sh +812 -29
- package/package.json +6 -6
- package/scripts/postinstall.mjs +5 -4
package/README.md
CHANGED
|
@@ -58,6 +58,7 @@ to the issues you want worked and run `aiur`.
|
|
|
58
58
|
| `aiur` | Start the workflow in the foreground (local-only bind) |
|
|
59
59
|
| `aiur --bg` | Start a headless BEAM in one detached tmux lifetime session |
|
|
60
60
|
| `aiur status` | Show active agents and their state |
|
|
61
|
+
| `aiur alerts [--needs-attention]` | Show structured alert feed JSON lines |
|
|
61
62
|
| `aiur pause <id…>` / `resume <id…>` | Pause or resume agents by issue ID |
|
|
62
63
|
| `aiur stop` | Stop the running session |
|
|
63
64
|
|
package/bin/aiur.js
CHANGED
|
@@ -162,8 +162,9 @@ function ensureTmux() {
|
|
|
162
162
|
}
|
|
163
163
|
}
|
|
164
164
|
|
|
165
|
-
// True only when the pinned opencode is on PATH; a wrong
|
|
166
|
-
//
|
|
165
|
+
// True only when the pinned opencode is on PATH; a wrong (e.g. newer,
|
|
166
|
+
// unverified) version must be replaced with the pin, so presence alone
|
|
167
|
+
// isn't enough.
|
|
167
168
|
function opencodeMatchesPin() {
|
|
168
169
|
const result = safeSpawn("opencode", ["--version"], { encoding: "utf8" });
|
|
169
170
|
return !result.error && result.status === 0 && (result.stdout || "").trim() === opencodeVersion();
|
package/libexec/aiur-engine.sh
CHANGED
|
@@ -44,23 +44,67 @@ engine_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
|
44
44
|
|
|
45
45
|
# --- distribution identity (per-instance: keyed by the aiur project root) -----
|
|
46
46
|
|
|
47
|
-
# The aiur project root
|
|
48
|
-
#
|
|
47
|
+
# The aiur project root used to key this instance. AIUR_REPO_ROOT (set by the dev
|
|
48
|
+
# shim) wins. Otherwise walk up from $PWD to the first dir holding a REPO-LOCAL
|
|
49
|
+
# config — but the walk STOPS at $HOME: the global config at ~/.aiur/config (legacy
|
|
50
|
+
# ~/.aiurconfig) is not a repo root, and treating it as one would collapse every
|
|
51
|
+
# project under $HOME onto one key (#443). When no repo-local config is found, the
|
|
52
|
+
# BEAM serves this run via the global config (mirroring its discovery order in
|
|
53
|
+
# src/lib/aiur/workflow.ex) — or via none — and in both cases the project being
|
|
54
|
+
# served is the cwd, so we key by realpath($PWD). That gives each global-config
|
|
55
|
+
# project a distinct identity instead of an empty key (legacy aiur-$USER@…) or $HOME.
|
|
56
|
+
#
|
|
57
|
+
# Caveat: a global-config run's key is cwd-derived (there is no repo root to
|
|
58
|
+
# converge on), so control commands (status/pause/stop) must be run from the SAME
|
|
59
|
+
# directory the run was launched from. A repo with a repo-local .aiur/config keeps
|
|
60
|
+
# the walk-up, so its control commands still resolve from any subdir.
|
|
49
61
|
aiur_project_root() {
|
|
50
62
|
if [ -n "${AIUR_REPO_ROOT:-}" ]; then printf '%s' "$AIUR_REPO_ROOT"; return; fi
|
|
51
|
-
|
|
52
|
-
|
|
63
|
+
|
|
64
|
+
# Canonicalize $PWD and $HOME so the home-boundary test below holds even when
|
|
65
|
+
# either is reached through a symlink.
|
|
66
|
+
local pwd_real home_real
|
|
67
|
+
pwd_real="$(pwd -P 2>/dev/null || printf '%s' "$PWD")"
|
|
68
|
+
# ${HOME:-} (not bare $HOME) so an unset HOME under `set -u` can't abort the
|
|
69
|
+
# script here; an empty home_real simply disables the boundary (walk to /).
|
|
70
|
+
home_real="$(cd "${HOME:-}" 2>/dev/null && pwd -P || printf '%s' "${HOME:-}")"
|
|
71
|
+
|
|
72
|
+
local d="$pwd_real"
|
|
73
|
+
while [ -n "$d" ] && [ "$d" != "/" ] && [ "$d" != "$home_real" ]; do
|
|
53
74
|
if [ -f "$d/.aiur/config" ] || [ -f "$d/.aiurconfig" ]; then
|
|
54
75
|
printf '%s' "$d"
|
|
55
76
|
return
|
|
56
77
|
fi
|
|
57
78
|
d="$(dirname "$d")"
|
|
58
79
|
done
|
|
80
|
+
|
|
81
|
+
printf '%s' "$pwd_real"
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
aiur_project_root_source() {
|
|
85
|
+
if [ -n "${AIUR_REPO_ROOT:-}" ]; then printf 'env'; return; fi
|
|
86
|
+
|
|
87
|
+
local pwd_real home_real
|
|
88
|
+
pwd_real="$(pwd -P 2>/dev/null || printf '%s' "$PWD")"
|
|
89
|
+
home_real="$(cd "${HOME:-}" 2>/dev/null && pwd -P || printf '%s' "${HOME:-}")"
|
|
90
|
+
|
|
91
|
+
local d="$pwd_real"
|
|
92
|
+
while [ -n "$d" ] && [ "$d" != "/" ] && [ "$d" != "$home_real" ]; do
|
|
93
|
+
if [ -f "$d/.aiur/config" ] || [ -f "$d/.aiurconfig" ]; then
|
|
94
|
+
printf 'repo'
|
|
95
|
+
return
|
|
96
|
+
fi
|
|
97
|
+
d="$(dirname "$d")"
|
|
98
|
+
done
|
|
99
|
+
|
|
100
|
+
printf 'cwd'
|
|
59
101
|
}
|
|
60
102
|
|
|
61
103
|
# Short, stable, node-name-legal (lowercase hex) key for the project root, so two
|
|
62
104
|
# aiur instances for the same user get distinct node/session/socket names and can't
|
|
63
|
-
# reap each other.
|
|
105
|
+
# reap each other. Any real cwd now resolves a key (global-config runs key by
|
|
106
|
+
# realpath($PWD), #443); only a degenerate unreadable cwd yields empty, falling back
|
|
107
|
+
# to the legacy un-keyed name.
|
|
64
108
|
aiur_instance_key() {
|
|
65
109
|
local root
|
|
66
110
|
root="$(aiur_project_root)"
|
|
@@ -74,6 +118,8 @@ aiur_instance_key() {
|
|
|
74
118
|
|
|
75
119
|
aiur_resolve_identity() {
|
|
76
120
|
local config_home="${XDG_CONFIG_HOME:-$HOME/.config}"
|
|
121
|
+
AIUR_PROJECT_ROOT="$(aiur_project_root)"
|
|
122
|
+
AIUR_PROJECT_ROOT_SOURCE="$(aiur_project_root_source)"
|
|
77
123
|
|
|
78
124
|
: "${AIUR_BG_STATE_DIR:=$config_home/aiur}"
|
|
79
125
|
: "${AIUR_COOKIE_FILE:=$AIUR_BG_STATE_DIR/cookie}"
|
|
@@ -87,7 +133,8 @@ aiur_resolve_identity() {
|
|
|
87
133
|
: "${AIUR_RELEASE_NODE:=aiur-${USER}${AIUR_INSTANCE_KEY:+-$AIUR_INSTANCE_KEY}@127.0.0.1}"
|
|
88
134
|
|
|
89
135
|
export AIUR_BG_STATE_DIR AIUR_COOKIE_FILE AIUR_SESSION_PREFIX \
|
|
90
|
-
AIUR_PROFILES_FILE AIUR_RELEASE_NODE AIUR_INSTANCE_KEY
|
|
136
|
+
AIUR_PROFILES_FILE AIUR_RELEASE_NODE AIUR_INSTANCE_KEY \
|
|
137
|
+
AIUR_PROJECT_ROOT AIUR_PROJECT_ROOT_SOURCE
|
|
91
138
|
}
|
|
92
139
|
|
|
93
140
|
aiur_print_identity() {
|
|
@@ -98,6 +145,8 @@ aiur_print_identity() {
|
|
|
98
145
|
printf 'AIUR_PROFILES_FILE=%s\n' "$AIUR_PROFILES_FILE"
|
|
99
146
|
printf 'AIUR_RELEASE_NODE=%s\n' "$AIUR_RELEASE_NODE"
|
|
100
147
|
printf 'AIUR_INSTANCE_KEY=%s\n' "$AIUR_INSTANCE_KEY"
|
|
148
|
+
printf 'AIUR_PROJECT_ROOT=%s\n' "$AIUR_PROJECT_ROOT"
|
|
149
|
+
printf 'AIUR_PROJECT_ROOT_SOURCE=%s\n' "$AIUR_PROJECT_ROOT_SOURCE"
|
|
101
150
|
printf 'AIUR_COOKIE_FILE=%s\n' "$AIUR_COOKIE_FILE"
|
|
102
151
|
}
|
|
103
152
|
|
|
@@ -224,9 +273,12 @@ Usage: aiur [--interactive] [--max-agents <n>] [--logs-root <path>] [--port <por
|
|
|
224
273
|
aiur stop stop the running session
|
|
225
274
|
aiur status show agent status
|
|
226
275
|
aiur agents show each agent's state + current activity
|
|
276
|
+
aiur alerts [--needs-attention] show structured alert feed
|
|
277
|
+
aiur watch [--full|--changes] [--interval <secs>] server-side status board
|
|
227
278
|
aiur set max-agents <n> change the concurrent-agent cap at runtime
|
|
228
279
|
aiur pause <ids|--all> | resume <ids|--all>
|
|
229
280
|
aiur message <id> <text> send operator text to a running agent
|
|
281
|
+
aiur cleanup-stale [--dry-run] list/reap stale manual-smoke leftovers
|
|
230
282
|
aiur --version
|
|
231
283
|
EOF
|
|
232
284
|
}
|
|
@@ -355,6 +407,8 @@ run_session() {
|
|
|
355
407
|
conf="$(resolve_tmux_conf)"
|
|
356
408
|
[ -f "$conf" ] || die "tmux conf not found at $conf"
|
|
357
409
|
|
|
410
|
+
preflight_stale_manual_smoke
|
|
411
|
+
|
|
358
412
|
mkdir -p "$AIUR_BG_STATE_DIR"
|
|
359
413
|
printf '%s\n' "$session" >"$AIUR_BG_STATE_DIR/state"
|
|
360
414
|
export AIUR_TMUX_SESSION="$session"
|
|
@@ -372,6 +426,11 @@ run_session() {
|
|
|
372
426
|
export AIUR_AGENT_TMPFILE="${session_root}/aiur-${$}-agents"
|
|
373
427
|
: >"$AIUR_AGENT_TMPFILE"
|
|
374
428
|
|
|
429
|
+
# Workspace-root handoff for the shell cwd-sweep backstop. The BEAM writes the
|
|
430
|
+
# resolved Aiur.Config.workspace_root() here after config loads.
|
|
431
|
+
export AIUR_WORKSPACE_ROOT_FILE="${session_root}/aiur-${$}-workspace-root"
|
|
432
|
+
: >"$AIUR_WORKSPACE_ROOT_FILE"
|
|
433
|
+
|
|
375
434
|
# Background runs persist under a known run log dir so (a) the BEAM-death
|
|
376
435
|
# watchdog, which outlives the BEAM, can drop a crash record next to aiur.log,
|
|
377
436
|
# and (b) the post-start boot capture is not lost to /tmp when the launcher
|
|
@@ -406,8 +465,8 @@ run_session() {
|
|
|
406
465
|
for v in AIUR_RELEASE_DIR AIUR_ARGV_FILE RELEASE_DISTRIBUTION RELEASE_NODE \
|
|
407
466
|
RELEASE_COOKIE ERL_AFLAGS ERL_EPMD_ADDRESS AIUR_NODE AIUR_ERLANG_COOKIE \
|
|
408
467
|
AIUR_TMUX_SESSION AIUR_TMUX_SOCKET AIUR_TMUX_CONF AIUR_BIN \
|
|
409
|
-
AIUR_SESSION_TMPFILE AIUR_AGENT_TMPFILE
|
|
410
|
-
AIUR_OPENCODE_BRIDGE_PORT AIUR_DEBUG; do
|
|
468
|
+
AIUR_SESSION_TMPFILE AIUR_AGENT_TMPFILE AIUR_WORKSPACE_ROOT_FILE \
|
|
469
|
+
ELIXIR_ERL_OPTIONS AIUR_LOGS_ROOT AIUR_OPENCODE_BRIDGE_PORT AIUR_DEBUG; do
|
|
411
470
|
if [ -n "${!v:-}" ]; then printf 'export %s=%q\n' "$v" "${!v}"; fi
|
|
412
471
|
done
|
|
413
472
|
printf 'capture=%q\n' "$startup_capture"
|
|
@@ -424,7 +483,8 @@ run_session() {
|
|
|
424
483
|
_session_socket="$socket" _session_name="$session" _session_conf="$conf" \
|
|
425
484
|
_session_tmpfile="$AIUR_SESSION_TMPFILE" _session_capture="$startup_capture" \
|
|
426
485
|
_session_argv="$argv_file" _session_release="$release_dir" _session_tmux="$tmux_bin" \
|
|
427
|
-
_session_node="$AIUR_RELEASE_NODE" _session_pidfile="$AIUR_AGENT_TMPFILE"
|
|
486
|
+
_session_node="$AIUR_RELEASE_NODE" _session_pidfile="$AIUR_AGENT_TMPFILE" \
|
|
487
|
+
_session_workspace_root_file="$AIUR_WORKSPACE_ROOT_FILE"
|
|
428
488
|
install_foreground_traps
|
|
429
489
|
fi
|
|
430
490
|
|
|
@@ -433,10 +493,11 @@ run_session() {
|
|
|
433
493
|
# session whose BEAM/control plane is gone should be reclaimed before retry.
|
|
434
494
|
if [ "$mode" = "background" ] && "$tmux_bin" -L "$socket" -f "$conf" has-session -t "$session" 2>/dev/null; then
|
|
435
495
|
if [ "$(probe_control_liveness)" = "up" ]; then
|
|
496
|
+
write_aiur_instance_record "$session" "$socket"
|
|
436
497
|
echo "aiur is already running in the background (tmux session ${session})." >&2
|
|
437
498
|
echo "Use: aiur status # inspect agents" >&2
|
|
438
499
|
echo "Use: aiur stop # stop it before starting a fresh session" >&2
|
|
439
|
-
rm -f "$startup_capture" "$argv_file" "$launcher" "$AIUR_SESSION_TMPFILE" "$AIUR_AGENT_TMPFILE" 2>/dev/null || true
|
|
500
|
+
rm -f "$startup_capture" "$argv_file" "$launcher" "$AIUR_SESSION_TMPFILE" "$AIUR_AGENT_TMPFILE" "$AIUR_WORKSPACE_ROOT_FILE" 2>/dev/null || true
|
|
440
501
|
return 0
|
|
441
502
|
fi
|
|
442
503
|
|
|
@@ -474,7 +535,7 @@ run_session() {
|
|
|
474
535
|
-x "${COLUMNS:-200}" -y "${LINES:-50}" "$inner_cmd"; then
|
|
475
536
|
echo "❌ aiur failed to start; captured output:" >&2
|
|
476
537
|
tail -n 30 "$startup_capture" 2>/dev/null | sed 's/^/ /' >&2 || true
|
|
477
|
-
rm -f "$startup_capture" "$argv_file" "$launcher" "$AIUR_SESSION_TMPFILE" "$AIUR_AGENT_TMPFILE" 2>/dev/null || true
|
|
538
|
+
rm -f "$startup_capture" "$argv_file" "$launcher" "$AIUR_SESSION_TMPFILE" "$AIUR_AGENT_TMPFILE" "$AIUR_WORKSPACE_ROOT_FILE" 2>/dev/null || true
|
|
478
539
|
exit 1
|
|
479
540
|
fi
|
|
480
541
|
|
|
@@ -483,11 +544,11 @@ run_session() {
|
|
|
483
544
|
# chat panes it opens. Best-effort — a missing title just shows the default.
|
|
484
545
|
"$tmux_bin" -L "$socket" -f "$conf" select-pane -t "$session" -T "AIUR Agents" 2>/dev/null || true
|
|
485
546
|
|
|
486
|
-
# Grace window: surface a boot crash instead of attaching to nothing.
|
|
487
|
-
#
|
|
488
|
-
#
|
|
489
|
-
|
|
490
|
-
|
|
547
|
+
# Grace window: surface a boot crash instead of attaching to nothing. Prove
|
|
548
|
+
# the control plane can answer before declaring startup usable: background
|
|
549
|
+
# control commands depend on that RPC path, and foreground automation must not
|
|
550
|
+
# treat a pre-TUI application crash as a successful `tmux attach`.
|
|
551
|
+
local require_control=1
|
|
491
552
|
if ! wait_for_session_startup "$tmux_bin" "$socket" "$conf" "$session" "$startup_capture" "$require_control"; then
|
|
492
553
|
if [ "$mode" = "background" ]; then
|
|
493
554
|
"$tmux_bin" -L "$socket" -f "$conf" kill-session -t "$session" 2>/dev/null || true
|
|
@@ -495,10 +556,16 @@ run_session() {
|
|
|
495
556
|
kill_beams_matching "-name ${AIUR_RELEASE_NODE}"
|
|
496
557
|
fi
|
|
497
558
|
[ "$mode" = "foreground" ] && return 1
|
|
498
|
-
rm -f "$startup_capture" "$argv_file" "$launcher" "$AIUR_SESSION_TMPFILE" "$AIUR_AGENT_TMPFILE" 2>/dev/null || true
|
|
559
|
+
rm -f "$startup_capture" "$argv_file" "$launcher" "$AIUR_SESSION_TMPFILE" "$AIUR_AGENT_TMPFILE" "$AIUR_WORKSPACE_ROOT_FILE" 2>/dev/null || true
|
|
499
560
|
exit 1
|
|
500
561
|
fi
|
|
501
562
|
|
|
563
|
+
write_aiur_instance_record "$session" "$socket"
|
|
564
|
+
|
|
565
|
+
if [ "$mode" = "foreground" ]; then
|
|
566
|
+
echo "aiur foreground tmux socket ${socket}, session ${session}" >&2
|
|
567
|
+
fi
|
|
568
|
+
|
|
502
569
|
if [ "$mode" = "background" ]; then
|
|
503
570
|
# Fresh run: drop any stale crash/stop state from a prior dead instance so
|
|
504
571
|
# `status` doesn't report a phantom orphan and the watchdog starts clean.
|
|
@@ -508,9 +575,9 @@ run_session() {
|
|
|
508
575
|
background_watchdog_pid="$(start_beam_death_watchdog \
|
|
509
576
|
"-name ${AIUR_RELEASE_NODE}" "$socket" "$AIUR_AGENT_TMPFILE" 1 1 \
|
|
510
577
|
"$AIUR_RELEASE_NODE" "${AIUR_LOGS_ROOT:-}" \
|
|
511
|
-
"$(aiur_stop_sentinel_path)" "$(aiur_crash_marker_path)")"
|
|
578
|
+
"$(aiur_stop_sentinel_path)" "$(aiur_crash_marker_path)" "$AIUR_WORKSPACE_ROOT_FILE")"
|
|
512
579
|
disown "$background_watchdog_pid" 2>/dev/null || true
|
|
513
|
-
echo "aiur started in the background (tmux session ${session}). Attach with: aiur" >&2
|
|
580
|
+
echo "aiur started in the background (tmux socket ${socket}, session ${session}). Attach with: aiur" >&2
|
|
514
581
|
# Keep $startup_capture (boot.out.log) for the run's lifetime; only the
|
|
515
582
|
# transient argv file is no longer needed.
|
|
516
583
|
rm -f "$argv_file" 2>/dev/null || true
|
|
@@ -524,11 +591,19 @@ run_session() {
|
|
|
524
591
|
# the session, and unblocks the attach. It polls the node name rather than a
|
|
525
592
|
# captured pid, so another Aiur instance from the same release cannot hold it open.
|
|
526
593
|
_session_watchdog_pid="$(start_beam_death_watchdog \
|
|
527
|
-
"-name ${AIUR_RELEASE_NODE}" "$socket" "$AIUR_AGENT_TMPFILE" 1
|
|
594
|
+
"-name ${AIUR_RELEASE_NODE}" "$socket" "$AIUR_AGENT_TMPFILE" 1 0 \
|
|
595
|
+
"" "" "" "" "$AIUR_WORKSPACE_ROOT_FILE")"
|
|
528
596
|
|
|
529
597
|
# Foreground: attach the UI. Do not exec — that would drop the teardown trap.
|
|
530
|
-
|
|
531
|
-
|
|
598
|
+
# Avoid process substitution here: some sandboxed non-TTY launchers reject
|
|
599
|
+
# opening /dev/fd/* during the real manual-test wrapper path.
|
|
600
|
+
local attach_stderr attach_code
|
|
601
|
+
attach_stderr="$(mktemp "${TMPDIR:-/tmp}/aiur-attach-stderr.XXXXXX")"
|
|
602
|
+
"$tmux_bin" -L "$socket" -f "$conf" attach -t "$session" 2>"$attach_stderr"
|
|
603
|
+
attach_code=$?
|
|
604
|
+
grep -v -F "[server exited]" "$attach_stderr" >&2 || true
|
|
605
|
+
rm -f "$attach_stderr" 2>/dev/null || true
|
|
606
|
+
return "$attach_code"
|
|
532
607
|
}
|
|
533
608
|
|
|
534
609
|
resolve_tmux_conf() {
|
|
@@ -563,6 +638,237 @@ kill_beams_matching() {
|
|
|
563
638
|
for pid in $pids; do kill -KILL "$pid" 2>/dev/null || true; done
|
|
564
639
|
}
|
|
565
640
|
|
|
641
|
+
pid_owner() {
|
|
642
|
+
ps -p "$1" -o user= 2>/dev/null | awk '{print $1}' || true
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
pid_command() {
|
|
646
|
+
ps -p "$1" -o command= 2>/dev/null || true
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
pid_ppid() {
|
|
650
|
+
ps -p "$1" -o ppid= 2>/dev/null | awk '{print $1}' || true
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
pid_cwd() {
|
|
654
|
+
local pid="$1" cwd
|
|
655
|
+
if [ -e "/proc/$pid/cwd" ]; then
|
|
656
|
+
readlink "/proc/$pid/cwd" 2>/dev/null || true
|
|
657
|
+
return 0
|
|
658
|
+
fi
|
|
659
|
+
if command -v lsof >/dev/null 2>&1; then
|
|
660
|
+
cwd="$(lsof -a -p "$pid" -d cwd -Fn 2>/dev/null | sed -n 's/^n//p' | head -n 1 || true)"
|
|
661
|
+
[ -n "$cwd" ] && printf '%s\n' "$cwd"
|
|
662
|
+
fi
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
kill_pid_with_escalation() {
|
|
666
|
+
local pid="$1" waited=0
|
|
667
|
+
[ -n "$pid" ] || return 0
|
|
668
|
+
kill -TERM "$pid" 2>/dev/null || true
|
|
669
|
+
while [ "$waited" -lt 20 ]; do
|
|
670
|
+
kill -0 "$pid" 2>/dev/null || return 0
|
|
671
|
+
sleep 0.1
|
|
672
|
+
waited=$((waited + 1))
|
|
673
|
+
done
|
|
674
|
+
kill -KILL "$pid" 2>/dev/null || true
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
aiur_node_from_command() {
|
|
678
|
+
local cmd="$1" node
|
|
679
|
+
node="$(printf '%s\n' "$cmd" | sed -n "s/.*--name[[:space:]]\\(aiur-${USER}[-A-Za-z0-9_]*@127\\.0\\.0\\.1\\).*/\\1/p" | head -n 1)"
|
|
680
|
+
[ -n "$node" ] && printf '%s\n' "$node"
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
aiur_release_root_from_command() {
|
|
684
|
+
local cmd="$1"
|
|
685
|
+
if [[ "$cmd" =~ --boot-var[[:space:]]+RELEASE_LIB[[:space:]]+([^[:space:]]+)/lib ]]; then
|
|
686
|
+
printf '%s\n' "${BASH_REMATCH[1]}"
|
|
687
|
+
return 0
|
|
688
|
+
fi
|
|
689
|
+
if [[ "$cmd" =~ ([^[:space:]]*/src/_build/dev/rel/aiur)/releases/ ]]; then
|
|
690
|
+
printf '%s\n' "${BASH_REMATCH[1]}"
|
|
691
|
+
return 0
|
|
692
|
+
fi
|
|
693
|
+
if [[ "$cmd" =~ ([^[:space:]]*/release)/releases/ ]]; then
|
|
694
|
+
printf '%s\n' "${BASH_REMATCH[1]}"
|
|
695
|
+
return 0
|
|
696
|
+
fi
|
|
697
|
+
return 1
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
workspace_root_from_aiur_release_root() {
|
|
701
|
+
local release_root="${1%/}"
|
|
702
|
+
case "$release_root" in
|
|
703
|
+
*/src/_build/dev/rel/aiur)
|
|
704
|
+
printf '%s\n' "${release_root%/src/_build/dev/rel/aiur}"
|
|
705
|
+
;;
|
|
706
|
+
*)
|
|
707
|
+
printf '%s\n' "$release_root"
|
|
708
|
+
;;
|
|
709
|
+
esac
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
aiur_node_tmux_session_alive() {
|
|
713
|
+
local node="$1" short tmux_bin
|
|
714
|
+
short="${node%@*}"
|
|
715
|
+
tmux_bin="$(command -v tmux || true)"
|
|
716
|
+
[ -n "$tmux_bin" ] || return 1
|
|
717
|
+
"$tmux_bin" -L "$short" has-session -t "${short}-default" 2>/dev/null
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
stale_manual_smoke_beam_inventory() {
|
|
721
|
+
local pid cmd node release_root workspace_root owner
|
|
722
|
+
aiur_resolve_identity
|
|
723
|
+
|
|
724
|
+
for pid in $(pgrep -f -- "beam\\.smp.*--name aiur-${USER}.*@127\\.0\\.0\\.1" 2>/dev/null || true); do
|
|
725
|
+
[ -n "$pid" ] || continue
|
|
726
|
+
owner="$(pid_owner "$pid")"
|
|
727
|
+
[ "$owner" = "$USER" ] || continue
|
|
728
|
+
|
|
729
|
+
cmd="$(pid_command "$pid")"
|
|
730
|
+
node="$(aiur_node_from_command "$cmd")"
|
|
731
|
+
[ -n "$node" ] || continue
|
|
732
|
+
|
|
733
|
+
release_root="$(aiur_release_root_from_command "$cmd" || true)"
|
|
734
|
+
[ -n "$release_root" ] || continue
|
|
735
|
+
workspace_root="$(workspace_root_from_aiur_release_root "$release_root")"
|
|
736
|
+
|
|
737
|
+
# Scope this broad cleanup to stale issue/manual-smoke workspaces. The
|
|
738
|
+
# current instance cleanup path handles the active daemon's own node.
|
|
739
|
+
case "$workspace_root" in
|
|
740
|
+
*/aiur-workspaces/*) ;;
|
|
741
|
+
*) continue ;;
|
|
742
|
+
esac
|
|
743
|
+
|
|
744
|
+
# A matching live aiur tmux session means this node may still be in use.
|
|
745
|
+
aiur_node_tmux_session_alive "$node" && continue
|
|
746
|
+
|
|
747
|
+
printf '%s\t%s\t%s\t%s\n' "$pid" "$node" "$workspace_root" "$release_root"
|
|
748
|
+
done
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
manual_smoke_wrapper_tmux_sockets() {
|
|
752
|
+
local tmux_bin sockdir path name panes current pane_path start manual_context all_sleep
|
|
753
|
+
tmux_bin="$(command -v tmux || true)"
|
|
754
|
+
[ -n "$tmux_bin" ] || return 0
|
|
755
|
+
sockdir="${TMUX_TMPDIR:-/tmp}/tmux-$(id -u)"
|
|
756
|
+
[ -d "$sockdir" ] || return 0
|
|
757
|
+
|
|
758
|
+
for path in "$sockdir"/*; do
|
|
759
|
+
[ -S "$path" ] || continue
|
|
760
|
+
name="${path##*/}"
|
|
761
|
+
case "$name" in
|
|
762
|
+
codex-driver-* | codex-aiur* | codex-[0-9]* | claude-driver*) ;;
|
|
763
|
+
*) continue ;;
|
|
764
|
+
esac
|
|
765
|
+
panes="$("$tmux_bin" -L "$name" list-panes -a -F '#{pane_current_command} #{pane_current_path} #{pane_start_command}' 2>/dev/null || true)"
|
|
766
|
+
[ -n "$panes" ] || continue
|
|
767
|
+
manual_context=0
|
|
768
|
+
all_sleep=1
|
|
769
|
+
while IFS=$'\t' read -r current pane_path start; do
|
|
770
|
+
case "$start" in
|
|
771
|
+
*aiurdev\ --test* | *scripts/aiurdev\ --test*) manual_context=1 ;;
|
|
772
|
+
esac
|
|
773
|
+
[ "$current" = "sleep" ] || all_sleep=0
|
|
774
|
+
done <<<"$panes"
|
|
775
|
+
[ "$manual_context" -eq 1 ] && [ "$all_sleep" -eq 1 ] && printf '%s\n' "$name"
|
|
776
|
+
done
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
orphaned_opencode_attach_inventory() {
|
|
780
|
+
local pid owner ppid cmd cwd
|
|
781
|
+
for pid in $(pgrep -f -- "opencode attach http://127\\.0\\.0\\.1:" 2>/dev/null || true); do
|
|
782
|
+
owner="$(pid_owner "$pid")"
|
|
783
|
+
[ "$owner" = "$USER" ] || continue
|
|
784
|
+
ppid="$(pid_ppid "$pid")"
|
|
785
|
+
[ "$ppid" = "1" ] || continue
|
|
786
|
+
cwd="$(pid_cwd "$pid")"
|
|
787
|
+
case "$cwd" in
|
|
788
|
+
*/aiur-workspaces/*) ;;
|
|
789
|
+
*) continue ;;
|
|
790
|
+
esac
|
|
791
|
+
cmd="$(pid_command "$pid")"
|
|
792
|
+
case "$cmd" in
|
|
793
|
+
*"opencode attach http://127.0.0.1:"*) printf '%s\t%s\t%s\n' "$pid" "$cwd" "$cmd" ;;
|
|
794
|
+
esac
|
|
795
|
+
done
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
report_stale_manual_smoke() {
|
|
799
|
+
local found=0 pid node workspace_root release_root socket cwd cmd
|
|
800
|
+
|
|
801
|
+
while IFS=$'\t' read -r pid node workspace_root release_root; do
|
|
802
|
+
[ -n "$pid" ] || continue
|
|
803
|
+
if [ "$found" -eq 0 ]; then
|
|
804
|
+
echo "aiur: stale manual-smoke leftovers detected:" >&2
|
|
805
|
+
found=1
|
|
806
|
+
fi
|
|
807
|
+
echo " BEAM pid=${pid} node=${node} workspace=${workspace_root} release=${release_root}" >&2
|
|
808
|
+
done < <(stale_manual_smoke_beam_inventory)
|
|
809
|
+
|
|
810
|
+
while IFS= read -r socket; do
|
|
811
|
+
[ -n "$socket" ] || continue
|
|
812
|
+
if [ "$found" -eq 0 ]; then
|
|
813
|
+
echo "aiur: stale manual-smoke leftovers detected:" >&2
|
|
814
|
+
found=1
|
|
815
|
+
fi
|
|
816
|
+
echo " wrapper tmux socket=${socket}" >&2
|
|
817
|
+
done < <(manual_smoke_wrapper_tmux_sockets)
|
|
818
|
+
|
|
819
|
+
while IFS=$'\t' read -r pid cwd cmd; do
|
|
820
|
+
[ -n "$pid" ] || continue
|
|
821
|
+
if [ "$found" -eq 0 ]; then
|
|
822
|
+
echo "aiur: stale manual-smoke leftovers detected:" >&2
|
|
823
|
+
found=1
|
|
824
|
+
fi
|
|
825
|
+
echo " orphan opencode attach pid=${pid} workspace=${cwd}" >&2
|
|
826
|
+
done < <(orphaned_opencode_attach_inventory)
|
|
827
|
+
|
|
828
|
+
if [ "$found" -eq 1 ]; then
|
|
829
|
+
echo "aiur: run 'aiur cleanup-stale' to TERM/KILL only these same-user Aiur leftovers." >&2
|
|
830
|
+
fi
|
|
831
|
+
|
|
832
|
+
return "$found"
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
preflight_stale_manual_smoke() {
|
|
836
|
+
report_stale_manual_smoke >/dev/null || true
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
reap_stale_manual_smoke() {
|
|
840
|
+
local include_wrappers="${1:-0}"
|
|
841
|
+
local pid node workspace_root release_root socket cwd cmd tmux_bin reaped=0
|
|
842
|
+
|
|
843
|
+
while IFS=$'\t' read -r pid node workspace_root release_root; do
|
|
844
|
+
[ -n "$pid" ] || continue
|
|
845
|
+
echo "aiur: reaping stale BEAM pid=${pid} node=${node} workspace=${workspace_root}" >&2
|
|
846
|
+
kill_pid_with_escalation "$pid"
|
|
847
|
+
reaped=$((reaped + 1))
|
|
848
|
+
done < <(stale_manual_smoke_beam_inventory)
|
|
849
|
+
|
|
850
|
+
tmux_bin="$(command -v tmux || true)"
|
|
851
|
+
if [ "$include_wrappers" = "1" ] && [ -n "$tmux_bin" ]; then
|
|
852
|
+
while IFS= read -r socket; do
|
|
853
|
+
[ -n "$socket" ] || continue
|
|
854
|
+
echo "aiur: reaping stale wrapper tmux socket=${socket}" >&2
|
|
855
|
+
"$tmux_bin" -L "$socket" kill-server 2>/dev/null || true
|
|
856
|
+
reaped=$((reaped + 1))
|
|
857
|
+
done < <(manual_smoke_wrapper_tmux_sockets)
|
|
858
|
+
fi
|
|
859
|
+
|
|
860
|
+
while IFS=$'\t' read -r pid cwd cmd; do
|
|
861
|
+
[ -n "$pid" ] || continue
|
|
862
|
+
echo "aiur: reaping orphan opencode attach pid=${pid}" >&2
|
|
863
|
+
kill_pid_with_escalation "$pid"
|
|
864
|
+
reaped=$((reaped + 1))
|
|
865
|
+
done < <(orphaned_opencode_attach_inventory)
|
|
866
|
+
|
|
867
|
+
if [ "$reaped" -eq 0 ]; then
|
|
868
|
+
echo "aiur: no stale manual-smoke leftovers found" >&2
|
|
869
|
+
fi
|
|
870
|
+
}
|
|
871
|
+
|
|
566
872
|
# Echo $1 and every descendant pid, depth-first. Mac-safe (`pgrep -P`, no
|
|
567
873
|
# /proc), mirroring Aiur.RemoteControl.collect_descendants so the launcher reaps
|
|
568
874
|
# the same tree the BEAM-side reaper would. The recorded agent pid is a bash
|
|
@@ -643,6 +949,34 @@ aiur_state_slug() {
|
|
|
643
949
|
printf '%s' "${AIUR_RELEASE_NODE:-aiur}" | tr -c 'A-Za-z0-9._-' '_'
|
|
644
950
|
}
|
|
645
951
|
|
|
952
|
+
aiur_instances_dir() {
|
|
953
|
+
printf '%s/instances' "${AIUR_BG_STATE_DIR:?}"
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
aiur_instance_record_path() {
|
|
957
|
+
printf '%s/%s.instance' "$(aiur_instances_dir)" "$(aiur_state_slug)"
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
write_aiur_instance_record() {
|
|
961
|
+
local session="$1" socket="$2" record_dir record tmp root
|
|
962
|
+
record_dir="$(aiur_instances_dir)"
|
|
963
|
+
record="$(aiur_instance_record_path)"
|
|
964
|
+
root="$(canonical_workspace_root "${AIUR_PROJECT_ROOT:-}")"
|
|
965
|
+
mkdir -p "$record_dir" 2>/dev/null || return 0
|
|
966
|
+
tmp="$(mktemp "$record_dir/.${AIUR_INSTANCE_KEY:-aiur}.XXXXXX" 2>/dev/null)" || return 0
|
|
967
|
+
{
|
|
968
|
+
printf 'AIUR_RECORD_NODE=%q\n' "$AIUR_RELEASE_NODE"
|
|
969
|
+
printf 'AIUR_RECORD_INSTANCE_KEY=%q\n' "$AIUR_INSTANCE_KEY"
|
|
970
|
+
printf 'AIUR_RECORD_SESSION=%q\n' "$session"
|
|
971
|
+
printf 'AIUR_RECORD_SOCKET=%q\n' "$socket"
|
|
972
|
+
printf 'AIUR_RECORD_PROJECT_ROOT=%q\n' "$root"
|
|
973
|
+
printf 'AIUR_RECORD_PROJECT_ROOT_SOURCE=%q\n' "${AIUR_PROJECT_ROOT_SOURCE:-}"
|
|
974
|
+
printf 'AIUR_RECORD_WRITTEN_AT=%q\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || true)"
|
|
975
|
+
} >"$tmp" || { rm -f "$tmp" 2>/dev/null || true; return 0; }
|
|
976
|
+
chmod 0600 "$tmp" 2>/dev/null || true
|
|
977
|
+
mv "$tmp" "$record" 2>/dev/null || rm -f "$tmp" 2>/dev/null || true
|
|
978
|
+
}
|
|
979
|
+
|
|
646
980
|
# Marker `status` reads to tell "daemon crashed, agents may be orphaned" apart
|
|
647
981
|
# from "nothing was ever running". Written by the watchdog on an unexpected exit,
|
|
648
982
|
# cleared on a clean stop and on the next successful background start.
|
|
@@ -690,6 +1024,123 @@ record_beam_crash() {
|
|
|
690
1024
|
fi
|
|
691
1025
|
}
|
|
692
1026
|
|
|
1027
|
+
canonical_workspace_root() {
|
|
1028
|
+
local root="$1"
|
|
1029
|
+
if [ -d "$root" ]; then
|
|
1030
|
+
(cd "$root" 2>/dev/null && pwd -P) || printf '%s\n' "$root"
|
|
1031
|
+
else
|
|
1032
|
+
printf '%s\n' "$root"
|
|
1033
|
+
fi
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
workspace_root_is_shallow() {
|
|
1037
|
+
local root="${1%/}" trimmed
|
|
1038
|
+
trimmed="${root#/}"
|
|
1039
|
+
[ -n "$trimmed" ] || return 0
|
|
1040
|
+
case "$trimmed" in
|
|
1041
|
+
*/*) return 1 ;;
|
|
1042
|
+
*) return 0 ;;
|
|
1043
|
+
esac
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
workspace_cwd_pids() {
|
|
1047
|
+
local root="${1%/}" proc_dir="${2:-/proc}" entry pid cwd
|
|
1048
|
+
[ -d "$proc_dir" ] || return 0
|
|
1049
|
+
|
|
1050
|
+
for entry in "$proc_dir"/[0-9]*; do
|
|
1051
|
+
[ -d "$entry" ] || continue
|
|
1052
|
+
pid="${entry##*/}"
|
|
1053
|
+
cwd="$(readlink "$entry/cwd" 2>/dev/null || true)"
|
|
1054
|
+
[ -n "$cwd" ] || continue
|
|
1055
|
+
cwd="${cwd%/}"
|
|
1056
|
+
if [ "$cwd" != "$root" ] && [[ "$cwd/" == "$root/"* ]]; then
|
|
1057
|
+
printf '%s\n' "$pid"
|
|
1058
|
+
fi
|
|
1059
|
+
done
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
shell_protected_pid_list() {
|
|
1063
|
+
local p
|
|
1064
|
+
printf '%s\n' "$$"
|
|
1065
|
+
[ "${BASHPID:-$$}" = "$$" ] || printf '%s\n' "$BASHPID"
|
|
1066
|
+
[ -n "${PPID:-}" ] && printf '%s\n' "$PPID"
|
|
1067
|
+
for p in $(agent_pid_tree "$$" 2>/dev/null || true); do
|
|
1068
|
+
printf '%s\n' "$p"
|
|
1069
|
+
done
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
reap_workspace_cwd_agents() {
|
|
1073
|
+
local root="${1:-}"
|
|
1074
|
+
[ -n "$root" ] || return 0
|
|
1075
|
+
[ -d /proc ] || return 0
|
|
1076
|
+
|
|
1077
|
+
root="$(canonical_workspace_root "$root")"
|
|
1078
|
+
if workspace_root_is_shallow "$root"; then
|
|
1079
|
+
echo "⚠️ refusing shallow workspace cwd sweep root: $root" >&2
|
|
1080
|
+
return 0
|
|
1081
|
+
fi
|
|
1082
|
+
|
|
1083
|
+
local max_sweeps="${AIUR_WORKSPACE_REAP_SWEEPS:-6}"
|
|
1084
|
+
case "$max_sweeps" in '' | *[!0-9]*) max_sweeps=6 ;; esac
|
|
1085
|
+
[ "$max_sweeps" -gt 0 ] || return 0
|
|
1086
|
+
|
|
1087
|
+
local protected pids=() filtered=() pid p sweep waited any
|
|
1088
|
+
protected=" $(shell_protected_pid_list | tr '\n' ' ') "
|
|
1089
|
+
|
|
1090
|
+
sweep=0
|
|
1091
|
+
while [ "$sweep" -lt "$max_sweeps" ]; do
|
|
1092
|
+
mapfile -t pids < <(workspace_cwd_pids "$root" /proc)
|
|
1093
|
+
filtered=()
|
|
1094
|
+
for pid in "${pids[@]}"; do
|
|
1095
|
+
case "$protected" in *" $pid "*) continue ;; esac
|
|
1096
|
+
kill -0 "$pid" 2>/dev/null && filtered+=("$pid")
|
|
1097
|
+
done
|
|
1098
|
+
|
|
1099
|
+
[ "${#filtered[@]}" -gt 0 ] || return 0
|
|
1100
|
+
|
|
1101
|
+
for p in "${filtered[@]}"; do kill -TERM "$p" 2>/dev/null || true; done
|
|
1102
|
+
waited=0
|
|
1103
|
+
while [ "$waited" -lt 10 ]; do
|
|
1104
|
+
any=0
|
|
1105
|
+
for p in "${filtered[@]}"; do kill -0 "$p" 2>/dev/null && { any=1; break; }; done
|
|
1106
|
+
[ "$any" -eq 0 ] && break
|
|
1107
|
+
sleep 0.1
|
|
1108
|
+
waited=$((waited + 1))
|
|
1109
|
+
done
|
|
1110
|
+
for p in "${filtered[@]}"; do kill -KILL "$p" 2>/dev/null || true; done
|
|
1111
|
+
|
|
1112
|
+
sweep=$((sweep + 1))
|
|
1113
|
+
sleep 0.1
|
|
1114
|
+
done
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
reap_workspace_cwd_from_file() {
|
|
1118
|
+
local root_file="${1:-}" root
|
|
1119
|
+
[ -n "$root_file" ] && [ -s "$root_file" ] || return 0
|
|
1120
|
+
IFS= read -r root <"$root_file" || root=""
|
|
1121
|
+
reap_workspace_cwd_agents "$root"
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
current_workspace_root() {
|
|
1125
|
+
resolve_release
|
|
1126
|
+
prepare_distribution >/dev/null 2>&1 || return 1
|
|
1127
|
+
|
|
1128
|
+
local output status line
|
|
1129
|
+
set +e
|
|
1130
|
+
output="$("$release_bin" rpc "IO.puts(Aiur.Config.workspace_root())" 2>/dev/null)"
|
|
1131
|
+
status=$?
|
|
1132
|
+
set -e
|
|
1133
|
+
[ "$status" -eq 0 ] || return 1
|
|
1134
|
+
|
|
1135
|
+
while IFS= read -r line; do
|
|
1136
|
+
case "$line" in
|
|
1137
|
+
"" | :ok) continue ;;
|
|
1138
|
+
*) printf '%s\n' "$line"; return 0 ;;
|
|
1139
|
+
esac
|
|
1140
|
+
done <<<"$output"
|
|
1141
|
+
return 1
|
|
1142
|
+
}
|
|
1143
|
+
|
|
693
1144
|
# Background watchdog that survives the BEAM. Polls for the release BEAM by
|
|
694
1145
|
# command pattern and, once it has SEEN the BEAM and then the BEAM disappears
|
|
695
1146
|
# (orderly halt OR :emfile-style crash), reaps every agent. Polling the pattern
|
|
@@ -702,12 +1153,13 @@ record_beam_crash() {
|
|
|
702
1153
|
#
|
|
703
1154
|
# $1 beam_pattern $2 socket $3 pidfile $4 interval_s $5 initial_seen
|
|
704
1155
|
# $6 node $7 run_log_dir $8 stop_sentinel $9 crash_marker
|
|
1156
|
+
# $10 workspace_root_file
|
|
705
1157
|
# Args 6-9 arm crash recording (background mode). Omit them (foreground) and the
|
|
706
1158
|
# watchdog only reaps — a foreground BEAM death is already visible at the UI.
|
|
707
1159
|
# Prints the watchdog's own pid so the caller can kill it on a clean teardown.
|
|
708
1160
|
start_beam_death_watchdog() {
|
|
709
1161
|
local beam_pattern="$1" socket="$2" pidfile="$3" interval="${4:-1}" initial_seen="${5:-0}"
|
|
710
|
-
local node="${6:-}" run_log_dir="${7:-}" stop_sentinel="${8:-}" crash_marker="${9:-}"
|
|
1162
|
+
local node="${6:-}" run_log_dir="${7:-}" stop_sentinel="${8:-}" crash_marker="${9:-}" workspace_root_file="${10:-}"
|
|
711
1163
|
# Redirect the subshell's stdout so a command-substitution caller
|
|
712
1164
|
# (`pid=$(start_beam_death_watchdog ...)`) returns immediately instead of
|
|
713
1165
|
# blocking on the still-open pipe until the watchdog finishes.
|
|
@@ -730,6 +1182,7 @@ start_beam_death_watchdog() {
|
|
|
730
1182
|
record_beam_crash "$node" "$run_log_dir" "$crash_marker"
|
|
731
1183
|
fi
|
|
732
1184
|
reap_aiur_agents "$socket" "$pidfile"
|
|
1185
|
+
reap_workspace_cwd_from_file "$workspace_root_file"
|
|
733
1186
|
) >/dev/null 2>&1 &
|
|
734
1187
|
printf '%s\n' "$!"
|
|
735
1188
|
}
|
|
@@ -789,7 +1242,7 @@ wait_for_session_startup() {
|
|
|
789
1242
|
# Foreground teardown: kill the session + BEAM and reap opencode sessions on exit.
|
|
790
1243
|
_session_socket="" _session_name="" _session_conf="" _session_tmpfile=""
|
|
791
1244
|
_session_capture="" _session_argv="" _session_release="" _session_tmux="" _session_node=""
|
|
792
|
-
_session_pidfile="" _session_watchdog_pid=""
|
|
1245
|
+
_session_pidfile="" _session_watchdog_pid="" _session_workspace_root_file=""
|
|
793
1246
|
_cleanup_ran=0
|
|
794
1247
|
session_cleanup() {
|
|
795
1248
|
[ "$_cleanup_ran" = 1 ] && return 0
|
|
@@ -819,6 +1272,11 @@ session_cleanup() {
|
|
|
819
1272
|
# release dir, so release-path pgrep would terminate sibling workflows.
|
|
820
1273
|
[ -n "$_session_node" ] && kill_beams_matching "-name ${_session_node}"
|
|
821
1274
|
|
|
1275
|
+
# Final cwd-sweep backstop after the BEAM is gone: catches workspace-rooted
|
|
1276
|
+
# agents or test children that registered too late, reparented during the
|
|
1277
|
+
# BEAM-side sweep, or survived a bounded in-BEAM cleanup.
|
|
1278
|
+
reap_workspace_cwd_from_file "$_session_workspace_root_file"
|
|
1279
|
+
|
|
822
1280
|
# Reap the epmd our BEAM spawned for distribution so it doesn't linger after
|
|
823
1281
|
# exit. The node is dead by now, so `epmd -kill` succeeds; it safely refuses
|
|
824
1282
|
# while any *other* Erlang node is alive, so a daemon shared with an unrelated
|
|
@@ -846,7 +1304,8 @@ session_cleanup() {
|
|
|
846
1304
|
# tempfiles are spared (fresh + live pid) before the explicit rm below clears them.
|
|
847
1305
|
sweep_stale_tmp_artifacts || true
|
|
848
1306
|
|
|
849
|
-
rm -f "$_session_tmpfile" "$_session_capture" "$_session_argv" "$_session_pidfile" 2>/dev/null || true
|
|
1307
|
+
rm -f "$_session_tmpfile" "$_session_capture" "$_session_argv" "$_session_pidfile" "$_session_workspace_root_file" 2>/dev/null || true
|
|
1308
|
+
rm -f "$(aiur_instance_record_path)" 2>/dev/null || true
|
|
850
1309
|
return $code
|
|
851
1310
|
}
|
|
852
1311
|
install_foreground_traps() {
|
|
@@ -891,21 +1350,207 @@ probe_node_liveness() {
|
|
|
891
1350
|
printf 'unknown'
|
|
892
1351
|
}
|
|
893
1352
|
|
|
1353
|
+
probe_named_node_liveness() {
|
|
1354
|
+
local node="$1" old_node="${RELEASE_NODE:-}" state
|
|
1355
|
+
RELEASE_NODE="$node"
|
|
1356
|
+
state="$(probe_node_liveness)"
|
|
1357
|
+
RELEASE_NODE="$old_node"
|
|
1358
|
+
printf '%s' "$state"
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
load_aiur_instance_record() {
|
|
1362
|
+
local file="$1"
|
|
1363
|
+
AIUR_RECORD_NODE=""
|
|
1364
|
+
AIUR_RECORD_INSTANCE_KEY=""
|
|
1365
|
+
AIUR_RECORD_SESSION=""
|
|
1366
|
+
AIUR_RECORD_SOCKET=""
|
|
1367
|
+
AIUR_RECORD_PROJECT_ROOT=""
|
|
1368
|
+
AIUR_RECORD_PROJECT_ROOT_SOURCE=""
|
|
1369
|
+
[ -r "$file" ] || return 1
|
|
1370
|
+
# Records are written by this engine as KEY=%q lines under the user's state dir.
|
|
1371
|
+
# shellcheck disable=SC1090
|
|
1372
|
+
source "$file" 2>/dev/null || return 1
|
|
1373
|
+
[ -n "$AIUR_RECORD_NODE" ] && [ -n "$AIUR_RECORD_SESSION" ] && \
|
|
1374
|
+
[ -n "$AIUR_RECORD_SOCKET" ] && [ -n "$AIUR_RECORD_PROJECT_ROOT" ]
|
|
1375
|
+
}
|
|
1376
|
+
|
|
1377
|
+
path_is_within_root() {
|
|
1378
|
+
local root child
|
|
1379
|
+
root="$(canonical_workspace_root "$1")"
|
|
1380
|
+
child="$(canonical_workspace_root "$2")"
|
|
1381
|
+
[ "$child" = "$root" ] || [[ "$child/" == "$root/"* ]]
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1384
|
+
AIUR_CONTROL_ADOPTED_RECORD=0
|
|
1385
|
+
AIUR_CONTROL_CURRENT_NODE_STATE=""
|
|
1386
|
+
AIUR_CONTROL_HINT_ROOTS=""
|
|
1387
|
+
AIUR_CONTROL_CALLER_ROOT=""
|
|
1388
|
+
AIUR_CONTROL_CALLER_NODE=""
|
|
1389
|
+
AIUR_CONTROL_CALLER_ROOT_SOURCE=""
|
|
1390
|
+
|
|
1391
|
+
resolve_control_identity_from_records() {
|
|
1392
|
+
AIUR_CONTROL_ADOPTED_RECORD=0
|
|
1393
|
+
AIUR_CONTROL_CURRENT_NODE_STATE="$(probe_node_liveness)"
|
|
1394
|
+
AIUR_CONTROL_HINT_ROOTS=""
|
|
1395
|
+
AIUR_CONTROL_CALLER_ROOT="$(canonical_workspace_root "${AIUR_PROJECT_ROOT:-}")"
|
|
1396
|
+
AIUR_CONTROL_CALLER_NODE="$AIUR_RELEASE_NODE"
|
|
1397
|
+
AIUR_CONTROL_CALLER_ROOT_SOURCE="${AIUR_PROJECT_ROOT_SOURCE:-}"
|
|
1398
|
+
|
|
1399
|
+
[ "$AIUR_CONTROL_CURRENT_NODE_STATE" = "down" ] || return 0
|
|
1400
|
+
[ "${AIUR_PROJECT_ROOT_SOURCE:-}" = "cwd" ] || return 0
|
|
1401
|
+
|
|
1402
|
+
local dir file state root match_count=0
|
|
1403
|
+
local match_node="" match_key="" match_session="" match_socket="" match_root="" match_source=""
|
|
1404
|
+
dir="$(aiur_instances_dir)"
|
|
1405
|
+
[ -d "$dir" ] || return 0
|
|
1406
|
+
|
|
1407
|
+
for file in "$dir"/*.instance; do
|
|
1408
|
+
[ -e "$file" ] || continue
|
|
1409
|
+
load_aiur_instance_record "$file" || continue
|
|
1410
|
+
state="$(probe_named_node_liveness "$AIUR_RECORD_NODE")"
|
|
1411
|
+
[ "$state" = "up" ] || continue
|
|
1412
|
+
|
|
1413
|
+
root="$(canonical_workspace_root "$AIUR_RECORD_PROJECT_ROOT")"
|
|
1414
|
+
case "$AIUR_CONTROL_HINT_ROOTS" in
|
|
1415
|
+
*"
|
|
1416
|
+
$root
|
|
1417
|
+
"*) ;;
|
|
1418
|
+
*) AIUR_CONTROL_HINT_ROOTS="${AIUR_CONTROL_HINT_ROOTS}${root}
|
|
1419
|
+
" ;;
|
|
1420
|
+
esac
|
|
1421
|
+
|
|
1422
|
+
path_is_within_root "$root" "$AIUR_CONTROL_CALLER_ROOT" || continue
|
|
1423
|
+
match_count=$((match_count + 1))
|
|
1424
|
+
match_node="$AIUR_RECORD_NODE"
|
|
1425
|
+
match_key="$AIUR_RECORD_INSTANCE_KEY"
|
|
1426
|
+
match_session="$AIUR_RECORD_SESSION"
|
|
1427
|
+
match_socket="$AIUR_RECORD_SOCKET"
|
|
1428
|
+
match_root="$root"
|
|
1429
|
+
match_source="$AIUR_RECORD_PROJECT_ROOT_SOURCE"
|
|
1430
|
+
done
|
|
1431
|
+
|
|
1432
|
+
[ "$match_count" -eq 1 ] || return 0
|
|
1433
|
+
|
|
1434
|
+
AIUR_RELEASE_NODE="$match_node"
|
|
1435
|
+
AIUR_INSTANCE_KEY="$match_key"
|
|
1436
|
+
AIUR_PROJECT_ROOT="$match_root"
|
|
1437
|
+
AIUR_PROJECT_ROOT_SOURCE="$match_source"
|
|
1438
|
+
AIUR_ADOPTED_TMUX_SESSION="$match_session"
|
|
1439
|
+
AIUR_ADOPTED_TMUX_SOCKET="$match_socket"
|
|
1440
|
+
export AIUR_RELEASE_NODE AIUR_INSTANCE_KEY AIUR_PROJECT_ROOT AIUR_PROJECT_ROOT_SOURCE \
|
|
1441
|
+
AIUR_ADOPTED_TMUX_SESSION AIUR_ADOPTED_TMUX_SOCKET
|
|
1442
|
+
RELEASE_NODE="$AIUR_RELEASE_NODE"
|
|
1443
|
+
export RELEASE_NODE
|
|
1444
|
+
AIUR_CONTROL_ADOPTED_RECORD=1
|
|
1445
|
+
}
|
|
1446
|
+
|
|
1447
|
+
print_global_config_control_hint() {
|
|
1448
|
+
[ "${AIUR_CONTROL_CALLER_ROOT_SOURCE:-}" = "cwd" ] || return 0
|
|
1449
|
+
[ "${AIUR_CONTROL_CURRENT_NODE_STATE:-}" = "down" ] || return 0
|
|
1450
|
+
|
|
1451
|
+
echo "aiur: global-config control identity is keyed by cwd ${AIUR_CONTROL_CALLER_ROOT:-${AIUR_PROJECT_ROOT:-unknown}}" >&2
|
|
1452
|
+
echo "aiur: run control commands from the launch directory, or from a subdirectory of that launch directory" >&2
|
|
1453
|
+
if [ -n "${AIUR_CONTROL_HINT_ROOTS:-}" ]; then
|
|
1454
|
+
echo "aiur: live launch directory candidate(s):" >&2
|
|
1455
|
+
printf '%s' "$AIUR_CONTROL_HINT_ROOTS" | sed '/^$/d; s/^/ /' >&2
|
|
1456
|
+
fi
|
|
1457
|
+
}
|
|
1458
|
+
|
|
1459
|
+
control_rpc_timeout_seconds() {
|
|
1460
|
+
local seconds="${AIUR_CONTROL_RPC_TIMEOUT_SECONDS:-10}"
|
|
1461
|
+
case "$seconds" in
|
|
1462
|
+
'' | *[!0-9]* | 0) seconds=10 ;;
|
|
1463
|
+
esac
|
|
1464
|
+
printf '%s' "$seconds"
|
|
1465
|
+
}
|
|
1466
|
+
|
|
1467
|
+
kill_control_rpc_process() {
|
|
1468
|
+
local pid="$1" grouped="$2" p tree=()
|
|
1469
|
+
[ -n "$pid" ] || return 0
|
|
1470
|
+
|
|
1471
|
+
if [ "$grouped" = "1" ]; then
|
|
1472
|
+
kill -TERM "-$pid" 2>/dev/null || kill -TERM "$pid" 2>/dev/null || true
|
|
1473
|
+
sleep 0.2
|
|
1474
|
+
kill -KILL "-$pid" 2>/dev/null || kill -KILL "$pid" 2>/dev/null || true
|
|
1475
|
+
return 0
|
|
1476
|
+
fi
|
|
1477
|
+
|
|
1478
|
+
while IFS= read -r p; do tree+=("$p"); done < <(agent_pid_tree "$pid")
|
|
1479
|
+
for p in "${tree[@]}"; do kill -TERM "$p" 2>/dev/null || true; done
|
|
1480
|
+
sleep 0.2
|
|
1481
|
+
for p in "${tree[@]}"; do kill -KILL "$p" 2>/dev/null || true; done
|
|
1482
|
+
}
|
|
1483
|
+
|
|
1484
|
+
run_release_rpc_with_timeout() {
|
|
1485
|
+
local expression="$1" timeout output_file timeout_file pid watchdog_pid status grouped=0
|
|
1486
|
+
timeout="$(control_rpc_timeout_seconds)"
|
|
1487
|
+
output_file="$(mktemp "${TMPDIR:-/tmp}/aiur-control-rpc-output.XXXXXX")"
|
|
1488
|
+
timeout_file="$(mktemp "${TMPDIR:-/tmp}/aiur-control-rpc-timeout.XXXXXX")"
|
|
1489
|
+
rm -f "$timeout_file" 2>/dev/null || true
|
|
1490
|
+
|
|
1491
|
+
if command -v setsid >/dev/null 2>&1; then
|
|
1492
|
+
setsid "$release_bin" rpc "$expression" >"$output_file" 2>&1 &
|
|
1493
|
+
grouped=1
|
|
1494
|
+
else
|
|
1495
|
+
"$release_bin" rpc "$expression" >"$output_file" 2>&1 &
|
|
1496
|
+
fi
|
|
1497
|
+
pid=$!
|
|
1498
|
+
|
|
1499
|
+
(
|
|
1500
|
+
sleep "$timeout"
|
|
1501
|
+
if kill -0 "$pid" 2>/dev/null; then
|
|
1502
|
+
: >"$timeout_file"
|
|
1503
|
+
kill_control_rpc_process "$pid" "$grouped"
|
|
1504
|
+
fi
|
|
1505
|
+
) &
|
|
1506
|
+
watchdog_pid=$!
|
|
1507
|
+
|
|
1508
|
+
if wait "$pid"; then
|
|
1509
|
+
status=0
|
|
1510
|
+
else
|
|
1511
|
+
status=$?
|
|
1512
|
+
fi
|
|
1513
|
+
kill "$watchdog_pid" 2>/dev/null || true
|
|
1514
|
+
wait "$watchdog_pid" 2>/dev/null || true
|
|
1515
|
+
|
|
1516
|
+
AIUR_CONTROL_RPC_OUTPUT="$(cat "$output_file" 2>/dev/null || true)"
|
|
1517
|
+
if [ -f "$timeout_file" ]; then
|
|
1518
|
+
AIUR_CONTROL_RPC_TIMED_OUT=1
|
|
1519
|
+
status=124
|
|
1520
|
+
else
|
|
1521
|
+
AIUR_CONTROL_RPC_TIMED_OUT=0
|
|
1522
|
+
fi
|
|
1523
|
+
|
|
1524
|
+
rm -f "$output_file" "$timeout_file" 2>/dev/null || true
|
|
1525
|
+
return "$status"
|
|
1526
|
+
}
|
|
1527
|
+
|
|
894
1528
|
# RPC an expression into the running node. The control CLI prints a trailing
|
|
895
1529
|
# `__AIUR_CONTROL_EXIT__:<code>` marker we translate into the process exit code.
|
|
896
1530
|
run_control_rpc() {
|
|
897
1531
|
local expression="$1"
|
|
898
1532
|
resolve_release
|
|
899
1533
|
prepare_distribution || die "distribution setup failed; cannot contact aiur"
|
|
1534
|
+
resolve_control_identity_from_records
|
|
1535
|
+
if [ "${AIUR_CONTROL_ADOPTED_RECORD:-0}" -eq 1 ]; then
|
|
1536
|
+
prepare_distribution || die "distribution setup failed; cannot contact aiur"
|
|
1537
|
+
fi
|
|
900
1538
|
|
|
901
1539
|
local marker="__AIUR_CONTROL_EXIT__:"
|
|
902
1540
|
local output status exit_code=0 saw_marker=0 line
|
|
903
1541
|
|
|
904
1542
|
set +e
|
|
905
|
-
|
|
1543
|
+
run_release_rpc_with_timeout "$expression"
|
|
906
1544
|
status=$?
|
|
1545
|
+
output="$AIUR_CONTROL_RPC_OUTPUT"
|
|
907
1546
|
set -e
|
|
908
1547
|
|
|
1548
|
+
if [ "${AIUR_CONTROL_RPC_TIMED_OUT:-0}" -eq 1 ]; then
|
|
1549
|
+
[ -n "$output" ] && printf '%s\n' "$output" >&2
|
|
1550
|
+
echo "aiur: control rpc to ${RELEASE_NODE} timed out after $(control_rpc_timeout_seconds)s; helper process was terminated" >&2
|
|
1551
|
+
return 124
|
|
1552
|
+
fi
|
|
1553
|
+
|
|
909
1554
|
if [ "$status" -ne 0 ]; then
|
|
910
1555
|
# A non-zero exit here is the rpc transport failing — application-level
|
|
911
1556
|
# outcomes ride the marker path below. `bin/aiur rpc` (Elixir --rpc-eval)
|
|
@@ -925,6 +1570,7 @@ run_control_rpc() {
|
|
|
925
1570
|
echo "aiur: run 'aiur stop' to reap any orphaned agents, then start aiur again" >&2
|
|
926
1571
|
else
|
|
927
1572
|
echo "aiur: no running aiur node at ${RELEASE_NODE}; start aiur and try again" >&2
|
|
1573
|
+
print_global_config_control_hint
|
|
928
1574
|
fi
|
|
929
1575
|
;;
|
|
930
1576
|
up)
|
|
@@ -950,7 +1596,11 @@ run_control_rpc() {
|
|
|
950
1596
|
esac
|
|
951
1597
|
done <<<"$output"
|
|
952
1598
|
|
|
953
|
-
[ "$saw_marker" -
|
|
1599
|
+
if [ "$saw_marker" -ne 1 ]; then
|
|
1600
|
+
echo "aiur: control rpc to ${RELEASE_NODE} returned no exit marker; command output may be incomplete" >&2
|
|
1601
|
+
return 1
|
|
1602
|
+
fi
|
|
1603
|
+
|
|
954
1604
|
return "$exit_code"
|
|
955
1605
|
}
|
|
956
1606
|
|
|
@@ -1046,6 +1696,99 @@ cmd_agents() {
|
|
|
1046
1696
|
run_control_rpc "Aiur.AgentControlCLI.agents()"
|
|
1047
1697
|
}
|
|
1048
1698
|
|
|
1699
|
+
# `aiur alerts` — newline-delimited structured alert feed from persisted
|
|
1700
|
+
# per-agent logs. `--needs-attention` filters to operator-actionable alerts.
|
|
1701
|
+
cmd_alerts() {
|
|
1702
|
+
local needs_attention=0 arg
|
|
1703
|
+
for arg in "$@"; do
|
|
1704
|
+
case "$arg" in
|
|
1705
|
+
--needs-attention) needs_attention=1 ;;
|
|
1706
|
+
*)
|
|
1707
|
+
echo "aiur: alerts only accepts --needs-attention" >&2
|
|
1708
|
+
exit 64
|
|
1709
|
+
;;
|
|
1710
|
+
esac
|
|
1711
|
+
done
|
|
1712
|
+
|
|
1713
|
+
if [ "$needs_attention" -eq 1 ]; then
|
|
1714
|
+
run_control_rpc "Aiur.AgentControlCLI.alerts(needs_attention: true)"
|
|
1715
|
+
else
|
|
1716
|
+
run_control_rpc "Aiur.AgentControlCLI.alerts()"
|
|
1717
|
+
fi
|
|
1718
|
+
}
|
|
1719
|
+
|
|
1720
|
+
# `aiur watch` — the server-side status board. Compiles one row per active
|
|
1721
|
+
# agent (state · complexity · activity-age · doing) plus an actionable section
|
|
1722
|
+
# from aiur's own state, with no GitHub round-trip. `--changes` (default) prints
|
|
1723
|
+
# only state-level deltas since the last call; `--full` prints every row;
|
|
1724
|
+
# `--interval N` re-renders every N seconds as a foreground watcher (the Elixir
|
|
1725
|
+
# call stays one-shot — the loop lives here).
|
|
1726
|
+
cmd_watch() {
|
|
1727
|
+
local mode="changes" interval="" arg
|
|
1728
|
+
|
|
1729
|
+
while [ "$#" -gt 0 ]; do
|
|
1730
|
+
arg="$1"
|
|
1731
|
+
case "$arg" in
|
|
1732
|
+
--full) mode="full" ;;
|
|
1733
|
+
--changes) mode="changes" ;;
|
|
1734
|
+
--once) : ;;
|
|
1735
|
+
--interval)
|
|
1736
|
+
shift
|
|
1737
|
+
interval="${1:-}"
|
|
1738
|
+
watch_validate_interval "$interval"
|
|
1739
|
+
;;
|
|
1740
|
+
--interval=*)
|
|
1741
|
+
interval="${arg#--interval=}"
|
|
1742
|
+
watch_validate_interval "$interval"
|
|
1743
|
+
;;
|
|
1744
|
+
*)
|
|
1745
|
+
echo "aiur: watch accepts --full, --changes, --once, --interval <secs>" >&2
|
|
1746
|
+
exit 64
|
|
1747
|
+
;;
|
|
1748
|
+
esac
|
|
1749
|
+
shift
|
|
1750
|
+
done
|
|
1751
|
+
|
|
1752
|
+
local expression="Aiur.AgentControlCLI.watch(mode: :${mode})"
|
|
1753
|
+
|
|
1754
|
+
if [ -n "$interval" ]; then
|
|
1755
|
+
while true; do
|
|
1756
|
+
run_control_rpc "$expression" || true
|
|
1757
|
+
sleep "$interval"
|
|
1758
|
+
done
|
|
1759
|
+
else
|
|
1760
|
+
run_control_rpc "$expression"
|
|
1761
|
+
fi
|
|
1762
|
+
}
|
|
1763
|
+
|
|
1764
|
+
watch_validate_interval() {
|
|
1765
|
+
if ! [[ "$1" =~ ^[0-9]+$ ]] || [ "$1" -le 0 ]; then
|
|
1766
|
+
echo "aiur: watch --interval expects a positive integer (seconds)" >&2
|
|
1767
|
+
exit 64
|
|
1768
|
+
fi
|
|
1769
|
+
}
|
|
1770
|
+
|
|
1771
|
+
cmd_cleanup_stale() {
|
|
1772
|
+
local dry_run=0 arg
|
|
1773
|
+
for arg in "$@"; do
|
|
1774
|
+
case "$arg" in
|
|
1775
|
+
--dry-run) dry_run=1 ;;
|
|
1776
|
+
*)
|
|
1777
|
+
echo "aiur: cleanup-stale only accepts --dry-run" >&2
|
|
1778
|
+
exit 64
|
|
1779
|
+
;;
|
|
1780
|
+
esac
|
|
1781
|
+
done
|
|
1782
|
+
|
|
1783
|
+
aiur_resolve_identity
|
|
1784
|
+
if [ "$dry_run" -eq 1 ]; then
|
|
1785
|
+
report_stale_manual_smoke || true
|
|
1786
|
+
else
|
|
1787
|
+
report_stale_manual_smoke || true
|
|
1788
|
+
reap_stale_manual_smoke 1
|
|
1789
|
+
fi
|
|
1790
|
+
}
|
|
1791
|
+
|
|
1049
1792
|
# `aiur set <key> <value>` — runtime config overrides without editing
|
|
1050
1793
|
# `.aiur/config`. Currently: `aiur set max-agents N`.
|
|
1051
1794
|
cmd_set() {
|
|
@@ -1196,11 +1939,33 @@ sweep_stale_tmp_artifacts() {
|
|
|
1196
1939
|
cmd_stop() {
|
|
1197
1940
|
resolve_release
|
|
1198
1941
|
aiur_resolve_identity
|
|
1942
|
+
RELEASE_NODE="$AIUR_RELEASE_NODE"
|
|
1943
|
+
ERL_EPMD_ADDRESS="${ERL_EPMD_ADDRESS:-127.0.0.1}"
|
|
1944
|
+
export RELEASE_NODE ERL_EPMD_ADDRESS
|
|
1945
|
+
resolve_control_identity_from_records
|
|
1946
|
+
|
|
1947
|
+
local workspace_root
|
|
1948
|
+
workspace_root="$(current_workspace_root 2>/dev/null || true)"
|
|
1199
1949
|
|
|
1200
1950
|
local tmux_bin
|
|
1201
1951
|
tmux_bin="$(command -v tmux || true)"
|
|
1202
|
-
local session="${AIUR_SESSION_PREFIX}-${USER:-user}${AIUR_INSTANCE_KEY:+-$AIUR_INSTANCE_KEY}-default"
|
|
1203
|
-
local socket="${AIUR_SESSION_PREFIX}-${USER:-user}${AIUR_INSTANCE_KEY:+-$AIUR_INSTANCE_KEY}"
|
|
1952
|
+
local session="${AIUR_ADOPTED_TMUX_SESSION:-${AIUR_SESSION_PREFIX}-${USER:-user}${AIUR_INSTANCE_KEY:+-$AIUR_INSTANCE_KEY}-default}"
|
|
1953
|
+
local socket="${AIUR_ADOPTED_TMUX_SOCKET:-${AIUR_SESSION_PREFIX}-${USER:-user}${AIUR_INSTANCE_KEY:+-$AIUR_INSTANCE_KEY}}"
|
|
1954
|
+
|
|
1955
|
+
local has_session=0
|
|
1956
|
+
if [ -n "$tmux_bin" ] && "$tmux_bin" -L "$socket" has-session -t "$session" 2>/dev/null; then
|
|
1957
|
+
has_session=1
|
|
1958
|
+
fi
|
|
1959
|
+
|
|
1960
|
+
if [ "${AIUR_CONTROL_ADOPTED_RECORD:-0}" -ne 1 ] && \
|
|
1961
|
+
[ "${AIUR_CONTROL_CURRENT_NODE_STATE:-}" = "down" ] && \
|
|
1962
|
+
[ "${AIUR_PROJECT_ROOT_SOURCE:-}" = "cwd" ] && \
|
|
1963
|
+
[ "$has_session" -eq 0 ] && \
|
|
1964
|
+
[ ! -f "$(aiur_crash_marker_path)" ]; then
|
|
1965
|
+
echo "aiur: no running aiur node at ${AIUR_RELEASE_NODE}; nothing stopped" >&2
|
|
1966
|
+
print_global_config_control_hint
|
|
1967
|
+
return 1
|
|
1968
|
+
fi
|
|
1204
1969
|
|
|
1205
1970
|
# Tell the background BEAM-death watchdog this exit is intentional before we
|
|
1206
1971
|
# kill the BEAM, so it consumes the sentinel instead of recording a crash. The
|
|
@@ -1219,6 +1984,10 @@ cmd_stop() {
|
|
|
1219
1984
|
# it. Do not sweep by release dir: sibling instances share dev releases.
|
|
1220
1985
|
kill_beams_matching "-name ${AIUR_RELEASE_NODE}"
|
|
1221
1986
|
|
|
1987
|
+
# Final cwd-sweep backstop after the BEAM is dead. `cmd_stop` is a fresh shell,
|
|
1988
|
+
# so it captures the root from the live control node before the kill above.
|
|
1989
|
+
reap_workspace_cwd_agents "$workspace_root"
|
|
1990
|
+
|
|
1222
1991
|
# The BEAM (alive until the TERM above) reaped its own headless agents through
|
|
1223
1992
|
# ProcessReaper on Application.stop. kill-server is the guarantee the earlier
|
|
1224
1993
|
# kill-session can't give for mid-turn agents: every pane agent across all
|
|
@@ -1238,8 +2007,10 @@ cmd_stop() {
|
|
|
1238
2007
|
rm -f "$agentfile" 2>/dev/null || true
|
|
1239
2008
|
done
|
|
1240
2009
|
|
|
2010
|
+
reap_stale_manual_smoke 0
|
|
1241
2011
|
sweep_dead_tmux_sockets
|
|
1242
2012
|
sweep_stale_tmp_artifacts
|
|
2013
|
+
rm -f "$(aiur_instance_record_path)" 2>/dev/null || true
|
|
1243
2014
|
}
|
|
1244
2015
|
|
|
1245
2016
|
# --- dispatch ----------------------------------------------------------------
|
|
@@ -1275,6 +2046,14 @@ aiur_engine_main() {
|
|
|
1275
2046
|
shift
|
|
1276
2047
|
cmd_agents "$@"
|
|
1277
2048
|
;;
|
|
2049
|
+
alerts)
|
|
2050
|
+
shift
|
|
2051
|
+
cmd_alerts "$@"
|
|
2052
|
+
;;
|
|
2053
|
+
watch)
|
|
2054
|
+
shift
|
|
2055
|
+
cmd_watch "$@"
|
|
2056
|
+
;;
|
|
1278
2057
|
set)
|
|
1279
2058
|
shift
|
|
1280
2059
|
cmd_set "$@"
|
|
@@ -1287,6 +2066,10 @@ aiur_engine_main() {
|
|
|
1287
2066
|
shift
|
|
1288
2067
|
cmd_message "$@"
|
|
1289
2068
|
;;
|
|
2069
|
+
cleanup-stale)
|
|
2070
|
+
shift
|
|
2071
|
+
cmd_cleanup_stale "$@"
|
|
2072
|
+
;;
|
|
1290
2073
|
stop)
|
|
1291
2074
|
cmd_stop
|
|
1292
2075
|
;;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "aiur-cli",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.4",
|
|
4
4
|
"description": "AI Unit Runtime for Executors.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"homepage": "https://github.com/its-everdred/aiur",
|
|
@@ -24,11 +24,11 @@
|
|
|
24
24
|
"engines": {
|
|
25
25
|
"node": ">=18"
|
|
26
26
|
},
|
|
27
|
-
"opencodeVersion": "1.
|
|
27
|
+
"opencodeVersion": "1.17.10",
|
|
28
28
|
"optionalDependencies": {
|
|
29
|
-
"aiur-cli-linux-x64": "0.0.
|
|
30
|
-
"aiur-cli-linux-arm64": "0.0.
|
|
31
|
-
"aiur-cli-darwin-x64": "0.0.
|
|
32
|
-
"aiur-cli-darwin-arm64": "0.0.
|
|
29
|
+
"aiur-cli-linux-x64": "0.0.4",
|
|
30
|
+
"aiur-cli-linux-arm64": "0.0.4",
|
|
31
|
+
"aiur-cli-darwin-x64": "0.0.4",
|
|
32
|
+
"aiur-cli-darwin-arm64": "0.0.4"
|
|
33
33
|
}
|
|
34
34
|
}
|
package/scripts/postinstall.mjs
CHANGED
|
@@ -23,14 +23,15 @@ import path from "node:path";
|
|
|
23
23
|
|
|
24
24
|
// Pinned opencode version, read from package.json `opencodeVersion` (single
|
|
25
25
|
// source of truth — keep in sync with mise.toml). aiur's opencode pane
|
|
26
|
-
// integration is validated against this version;
|
|
27
|
-
//
|
|
26
|
+
// integration is validated end-to-end against this exact version; floating to
|
|
27
|
+
// `latest` would adopt an unverified opencode that could regress custom-provider
|
|
28
|
+
// model resolution or the chat panes, so the spec must carry a fixed version.
|
|
28
29
|
const pkg = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
|
|
29
30
|
export const OPENCODE_PACKAGE = `opencode-ai@${pkg.opencodeVersion}`;
|
|
30
31
|
|
|
31
32
|
// True only when the *pinned* opencode version is on PATH. A wrong (e.g.
|
|
32
|
-
// newer) version must be reinstalled
|
|
33
|
-
//
|
|
33
|
+
// newer, unverified) version must be reinstalled to the pin — so a mere
|
|
34
|
+
// presence check isn't enough.
|
|
34
35
|
export function opencodeMatchesPin() {
|
|
35
36
|
const result = spawnSync("opencode", ["--version"], { encoding: "utf8" });
|
|
36
37
|
return !result.error && result.status === 0 && result.stdout.trim() === pkg.opencodeVersion;
|