aiur-cli 0.0.5-nightly.df55dc3 → 0.0.5
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 +61 -86
- package/libexec/aiur-engine.sh +237 -42
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -1,61 +1,77 @@
|
|
|
1
1
|
# AIUR
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Oversee a fleet of coding agents that coordinate via events.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
## Install
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
> [!WARNING]
|
|
12
|
-
> **Use at your own risk.** Aiur is an unstable, vibecoded engineering preview for trusted
|
|
13
|
-
> environments only. It **bypasses all agent permission prompts** and has very few efficiency
|
|
14
|
-
> optimizations. Suggested for simple tasks under supervision.
|
|
15
|
-
>
|
|
16
|
-
> Provided "as is", without warranty of any kind. You assume all risk for any cost, token
|
|
17
|
-
> spend, data loss, or damage from running it.
|
|
7
|
+
```bash
|
|
8
|
+
npm install --global aiur-cli
|
|
9
|
+
```
|
|
18
10
|
|
|
19
|
-
##
|
|
11
|
+
## Usage
|
|
20
12
|
|
|
21
13
|
```bash
|
|
22
|
-
npm install -g aiur-cli # Node.js 18+
|
|
23
|
-
|
|
24
14
|
cd your-repo
|
|
25
|
-
aiur init
|
|
26
|
-
aiur
|
|
15
|
+
aiur init # interactive setup wizard
|
|
16
|
+
aiur
|
|
27
17
|
```
|
|
28
18
|
|
|
29
19
|
`aiur init` scaffolds `.aiur/config` and provisions the repo: tracker and
|
|
30
20
|
repository, agent backends and routing, concurrency and turn limits, the GitHub
|
|
31
21
|
token, and the `agent:*` lifecycle labels. Re-running resumes from your saved
|
|
32
|
-
answers.
|
|
22
|
+
answers. When setup finishes, label the issues you want worked `agent:todo` and
|
|
23
|
+
run `aiur`.
|
|
33
24
|
|
|
34
|
-
|
|
35
|
-
`aiur
|
|
25
|
+
To have a coding agent operate the run as its Executor, install the repository's
|
|
26
|
+
[`aiur-intro`, `aiur-run`, and `aiur-monitor` skills](https://aiur.team/docs/skills#executor-skills)
|
|
27
|
+
through the agent's supported skill mechanism, then ask it to "run aiur." The npm
|
|
28
|
+
package installs the CLI, not those skills. [Learn about the Executor role](https://aiur.team/docs/concepts/executor).
|
|
36
29
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
30
|
+
## Features
|
|
31
|
+
|
|
32
|
+
- **Issue Monitoring** — Dispatch agents automatically via GitHub or Linear tickets.
|
|
33
|
+
- **Shared Bus** — Agents auto-pub/sub to dependency events, unblocking work early.
|
|
34
|
+
- **Optimize Effort** — Story points guide model sizes and skills across Claude and Codex.
|
|
35
|
+
- **Take the Wheel** — Drive at any moment via pre-warmed [Opencode](https://opencode.ai) sessions in configurable tmux panes.
|
|
36
|
+
- **Smart Alerts** — Customizable notifications and sounds when agents finish, stall, or need a decision.
|
|
37
|
+
- **Live Dashboard** — Shareable web view of every agent, event, and progress bar.
|
|
38
|
+
- **Build Orders** — Plan a large feature into typed members, lanes, phases, and dependencies, dispatched in dependency-safe parallel batches.
|
|
39
|
+
- **Analytics** — Live-run telemetry: lifecycle time, concurrency, complexity tiers, and cost over a chosen window.
|
|
40
|
+
- **Stream Deck** — Elgato Stream Deck + keys ranked by urgency, with pause/resume, a live log surface, and hold-to-dictate voice messages.
|
|
41
|
+
- **Hyper Optimized** — Monitors API and model token usage, routes on peak pricing and other factors, and supports Cloudflare and more.
|
|
42
42
|
|
|
43
|
-
|
|
43
|
+
The TUI and GUI are both optional: `aiur --bg` runs headless with the GUI, and
|
|
44
|
+
`--no-dashboard` drops the web listener. Every usable GUI requires
|
|
45
|
+
`AIUR_DASHBOARD_USERNAME` and `AIUR_DASHBOARD_PASSWORD` — without them the loopback
|
|
46
|
+
listener binds but refuses every request until both are set. The GUI is writable
|
|
47
|
+
by default, and writable mode requires those same two credentials even on loopback.
|
|
44
48
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
49
|
+
## Interfaces
|
|
50
|
+
|
|
51
|
+
### GUI
|
|
52
|
+
|
|
53
|
+

|
|
54
|
+
|
|
55
|
+
The GUI is the browser interface for supervising a run: the Units fleet table
|
|
56
|
+
and meters, the Commands decision inbox, Build Order graphs, and Analytics.
|
|
57
|
+
|
|
58
|
+
### TUI
|
|
59
|
+
|
|
60
|
+

|
|
61
|
+
|
|
62
|
+
The TUI is a live agent board in your terminal with chat panes opened into
|
|
63
|
+
individual agent sessions. Press `enter` on a row to open its conversation,
|
|
64
|
+
`space` to pause or resume, `←` / `→` to change the concurrency cap, and `?`
|
|
65
|
+
for the full keymap. A later bare `aiur` from the same project attaches to that
|
|
66
|
+
live directory-scoped session; detaching leaves the run healthy.
|
|
51
67
|
|
|
52
68
|
### CLI
|
|
53
69
|
|
|
54
70
|
| Command | What it does |
|
|
55
71
|
| --- | --- |
|
|
56
72
|
| `aiur init` | Interactive setup wizard |
|
|
57
|
-
| `aiur` | Start
|
|
58
|
-
| `aiur --bg` | Start a detached headless run,
|
|
73
|
+
| `aiur` | Start a foreground TUI, or attach to this directory's live session |
|
|
74
|
+
| `aiur --bg` | Start a detached headless run, GUI still served |
|
|
59
75
|
| `aiur status` | Daemon state, active agents, and the current concurrency cap |
|
|
60
76
|
| `aiur alerts --needs-attention` | Unresolved items waiting on you |
|
|
61
77
|
| `aiur pause <id…>` / `aiur resume <id…>` | Pause or resume agents by issue id |
|
|
@@ -64,56 +80,15 @@ When setup finishes, label the issues you want worked `agent:todo` and run
|
|
|
64
80
|
| `aiur --todo <id…> [--only]` | Queue tickets; `--only` dequeues the rest |
|
|
65
81
|
| `aiur stop` / `aiur restart` | Stop the session, or refresh the release and restart |
|
|
66
82
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
### TUI
|
|
70
|
-
|
|
71
|
-
The foreground run opens a tmux board with one row per ticket, showing runtime,
|
|
72
|
-
turn count, backend, pinned model, and a state glyph. Press `enter` on a row to
|
|
73
|
-
open that agent's chat pane beside the board, `space` to pause or resume, `←`
|
|
74
|
-
and `→` to change the concurrency cap, and `?` for the full keymap.
|
|
75
|
-
|
|
76
|
-
### Dashboard
|
|
77
|
-
|
|
78
|
-
Served on foreground and `--bg` runs at the host and port in your config,
|
|
79
|
-
`http://127.0.0.1:4000` by default. The launch output prints the real URL.
|
|
80
|
-
|
|
81
|
-
| Page | What it shows |
|
|
82
|
-
| --- | --- |
|
|
83
|
-
| Units | Fleet table, ticket backlog, provider rate meters |
|
|
84
|
-
| Commands | Durable decision inbox: answer, revise, retry delivery |
|
|
85
|
-
| Build Order | Ticket dependency graph and phases |
|
|
86
|
-
| Analytics | Throughput, complexity tiers, and spend over a chosen window |
|
|
87
|
-
|
|
88
|
-
Writable mode is the default and requires `AIUR_DASHBOARD_USERNAME` and
|
|
89
|
-
`AIUR_DASHBOARD_PASSWORD`, even on loopback. Set
|
|
90
|
-
`observability.dashboard_writable: false` for an unauthenticated read-only
|
|
91
|
-
loopback dashboard. Basic Auth is unencrypted, so put a TLS proxy in front of any
|
|
92
|
-
non-loopback bind.
|
|
93
|
-
|
|
94
|
-
### Stream Deck
|
|
83
|
+
## License
|
|
95
84
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
>
|
|
101
|
-
>
|
|
102
|
-
>
|
|
103
|
-
>
|
|
104
|
-
|
|
105
|
-
## Update notices
|
|
106
|
-
|
|
107
|
-
When a newer `aiur-cli` is published, a one-line notice is printed to stderr,
|
|
108
|
-
checked in the background at most once a day and never blocking your command.
|
|
109
|
-
Set `AIUR_NO_UPDATE_NOTIFIER=1` to silence it; it is skipped under CI.
|
|
110
|
-
|
|
111
|
-
Prerelease builds are published under the `nightly` dist-tag and never reach
|
|
112
|
-
`latest`, so a plain `npm install -g aiur-cli` always gives you a stable release.
|
|
113
|
-
To try one: `npm install -g aiur-cli@nightly`.
|
|
114
|
-
|
|
115
|
-
---
|
|
116
|
-
|
|
117
|
-
_Command macro, delegate micro, maximize APM._
|
|
85
|
+
> [!WARNING]
|
|
86
|
+
> **Use at your own risk.** Aiur is an unstable, vibecoded engineering preview
|
|
87
|
+
> for trusted environments only. It **bypasses all agent permission prompts**
|
|
88
|
+
> and has very few efficiency optimizations. Suggested for simple tasks under
|
|
89
|
+
> supervision.
|
|
90
|
+
>
|
|
91
|
+
> Provided "as is", without warranty of any kind. You assume all risk for any
|
|
92
|
+
> cost, token spend, data loss, or damage from running it.
|
|
118
93
|
|
|
119
|
-
[
|
|
94
|
+
This project is licensed under the [Apache License 2.0](https://github.com/aiur-team/aiur/blob/main/LICENSE).
|
package/libexec/aiur-engine.sh
CHANGED
|
@@ -490,6 +490,8 @@ Usage: aiur [--interactive] [--no-dashboard] [--executor] [--pause] [--max-agent
|
|
|
490
490
|
aiur asks [--open|--all] [--json] inspect current-repository operator requests
|
|
491
491
|
aiur cleanup-stale [--dry-run] list/reap stale manual-smoke leftovers
|
|
492
492
|
aiur --version
|
|
493
|
+
|
|
494
|
+
Bare aiur: start or attach to this directory's interactive session.
|
|
493
495
|
EOF
|
|
494
496
|
}
|
|
495
497
|
|
|
@@ -686,6 +688,65 @@ run_session() {
|
|
|
686
688
|
[ -n "$tmux_bin" ] || die "tmux is required to run aiur; install tmux and retry"
|
|
687
689
|
|
|
688
690
|
aiur_resolve_identity
|
|
691
|
+
prepare_distribution
|
|
692
|
+
|
|
693
|
+
local session="${AIUR_SESSION_PREFIX}-${USER:-user}${AIUR_INSTANCE_KEY:+-$AIUR_INSTANCE_KEY}-default"
|
|
694
|
+
local socket="${AIUR_SESSION_PREFIX}-${USER:-user}${AIUR_INSTANCE_KEY:+-$AIUR_INSTANCE_KEY}"
|
|
695
|
+
local conf launch_lock
|
|
696
|
+
conf="$(resolve_tmux_conf)"
|
|
697
|
+
[ -f "$conf" ] || die "tmux conf not found at $conf"
|
|
698
|
+
launch_lock="$(aiur_launch_lock_path)"
|
|
699
|
+
acquire_aiur_launch_lock "$launch_lock" || return 1
|
|
700
|
+
|
|
701
|
+
# An invocation targeting an existing directory session resolves here before
|
|
702
|
+
# fresh-start credentials, argv files, run logs, watchdogs, or teardown traps.
|
|
703
|
+
# Foreground attaches; background exits with explicit attach guidance.
|
|
704
|
+
if "$tmux_bin" -L "$socket" -f "$conf" has-session -t "$session" 2>/dev/null; then
|
|
705
|
+
if [ "$(probe_control_liveness)" = "up" ]; then
|
|
706
|
+
# Preserve the live launch's record and workspace-root handoff. Only
|
|
707
|
+
# backfill old/missing records, without pointing them at this attach
|
|
708
|
+
# invocation's transient state.
|
|
709
|
+
AIUR_WORKSPACE_ROOT_FILE="" write_aiur_instance_record "$session" "$socket" if-absent
|
|
710
|
+
|
|
711
|
+
if [ "$mode" = "background" ]; then
|
|
712
|
+
echo "aiur is already running in the background (tmux session ${session})." >&2
|
|
713
|
+
echo "Attach with: aiur" >&2
|
|
714
|
+
echo "Use: aiur status # inspect agents" >&2
|
|
715
|
+
echo "Use: aiur stop # stop it before starting a fresh session" >&2
|
|
716
|
+
release_aiur_launch_lock "$launch_lock"
|
|
717
|
+
return 0
|
|
718
|
+
fi
|
|
719
|
+
|
|
720
|
+
echo "aiur is already running; attaching to the running session ${session}." >&2
|
|
721
|
+
release_aiur_launch_lock "$launch_lock"
|
|
722
|
+
attach_tmux_session "$tmux_bin" "$socket" "$conf" "$session"
|
|
723
|
+
return $?
|
|
724
|
+
fi
|
|
725
|
+
|
|
726
|
+
# A tmux holder can exist while a healthy BEAM is still booting or while
|
|
727
|
+
# epmd itself is temporarily unreachable. Never turn one failed control
|
|
728
|
+
# probe into destructive cleanup unless distribution confirms the keyed
|
|
729
|
+
# node is absent.
|
|
730
|
+
case "$(probe_node_liveness)" in
|
|
731
|
+
down)
|
|
732
|
+
if ! confirm_stale_tmux_session "$tmux_bin" "$socket" "$conf" "$session"; then
|
|
733
|
+
echo "aiur found session ${session}, but it became live while checking stale state; leaving it intact." >&2
|
|
734
|
+
echo "Retry: aiur" >&2
|
|
735
|
+
release_aiur_launch_lock "$launch_lock"
|
|
736
|
+
return 1
|
|
737
|
+
fi
|
|
738
|
+
echo "aiur found stale tmux session ${session}; cleaning it up before restart" >&2
|
|
739
|
+
reap_aiur_agents "$socket" ""
|
|
740
|
+
kill_beams_matching "-name ${AIUR_RELEASE_NODE}"
|
|
741
|
+
;;
|
|
742
|
+
up | unknown)
|
|
743
|
+
echo "aiur found session ${session}, but its control plane is not ready; leaving the running session intact." >&2
|
|
744
|
+
echo "Retry: aiur" >&2
|
|
745
|
+
release_aiur_launch_lock "$launch_lock"
|
|
746
|
+
return 1
|
|
747
|
+
;;
|
|
748
|
+
esac
|
|
749
|
+
fi
|
|
689
750
|
|
|
690
751
|
# Pick up GITHUB_TOKEN / dashboard creds the wizard wrote to ./.env so the
|
|
691
752
|
# running tracker can authenticate. Shell exports still take precedence.
|
|
@@ -717,7 +778,6 @@ run_session() {
|
|
|
717
778
|
write_argv "${run_argv[@]}"
|
|
718
779
|
export AIUR_ARGV_FILE="$argv_file"
|
|
719
780
|
|
|
720
|
-
prepare_distribution
|
|
721
781
|
build_release_cmd
|
|
722
782
|
|
|
723
783
|
# Force +fnu when no locale is set so the BEAM does not mangle non-ASCII paths.
|
|
@@ -725,12 +785,6 @@ run_session() {
|
|
|
725
785
|
export ELIXIR_ERL_OPTIONS="${ELIXIR_ERL_OPTIONS:-} +fnu"
|
|
726
786
|
fi
|
|
727
787
|
|
|
728
|
-
local session="${AIUR_SESSION_PREFIX}-${USER:-user}${AIUR_INSTANCE_KEY:+-$AIUR_INSTANCE_KEY}-default"
|
|
729
|
-
local socket="${AIUR_SESSION_PREFIX}-${USER:-user}${AIUR_INSTANCE_KEY:+-$AIUR_INSTANCE_KEY}"
|
|
730
|
-
local conf
|
|
731
|
-
conf="$(resolve_tmux_conf)"
|
|
732
|
-
[ -f "$conf" ] || die "tmux conf not found at $conf"
|
|
733
|
-
|
|
734
788
|
preflight_stale_manual_smoke
|
|
735
789
|
|
|
736
790
|
mkdir -p "$AIUR_BG_STATE_DIR"
|
|
@@ -844,31 +898,11 @@ run_session() {
|
|
|
844
898
|
_session_node="$AIUR_RELEASE_NODE" _session_pidfile="$AIUR_AGENT_TMPFILE" \
|
|
845
899
|
_session_workspace_root_file="$AIUR_WORKSPACE_ROOT_FILE" \
|
|
846
900
|
_session_alert_ledger_path_file="$AIUR_ALERT_LEDGER_PATH_FILE" \
|
|
847
|
-
_session_crash_dump_baseline_file="$crash_dump_baseline_file"
|
|
901
|
+
_session_crash_dump_baseline_file="$crash_dump_baseline_file" \
|
|
902
|
+
_session_launch_lock="$launch_lock"
|
|
848
903
|
install_foreground_traps
|
|
849
904
|
fi
|
|
850
905
|
|
|
851
|
-
# Background starts are intentionally idempotent. A prior live node should not
|
|
852
|
-
# fall through to tmux's opaque "duplicate session" failure, and a stale tmux
|
|
853
|
-
# session whose BEAM/control plane is gone should be reclaimed before retry.
|
|
854
|
-
if [ "$mode" = "background" ] && "$tmux_bin" -L "$socket" -f "$conf" has-session -t "$session" 2>/dev/null; then
|
|
855
|
-
if [ "$(probe_control_liveness)" = "up" ]; then
|
|
856
|
-
# Write a record only when none exists. A live session's own start already
|
|
857
|
-
# wrote one — and it owns the BEAM-written AIUR_RECORD_WORKSPACE_ROOT_FILE
|
|
858
|
-
# path, which must not be overwritten by this invocation's temp file.
|
|
859
|
-
[ -f "$(aiur_instance_record_path)" ] || (AIUR_WORKSPACE_ROOT_FILE="" write_aiur_instance_record "$session" "$socket")
|
|
860
|
-
echo "aiur is already running in the background (tmux session ${session})." >&2
|
|
861
|
-
echo "Use: aiur status # inspect agents" >&2
|
|
862
|
-
echo "Use: aiur stop # stop it before starting a fresh session" >&2
|
|
863
|
-
rm -f "${launch_tempfiles[@]}" 2>/dev/null || true
|
|
864
|
-
return 0
|
|
865
|
-
fi
|
|
866
|
-
|
|
867
|
-
echo "aiur found stale tmux session ${session}; cleaning it up before restart" >&2
|
|
868
|
-
reap_aiur_agents "$socket" "$AIUR_AGENT_TMPFILE"
|
|
869
|
-
kill_beams_matching "-name ${AIUR_RELEASE_NODE}"
|
|
870
|
-
fi
|
|
871
|
-
|
|
872
906
|
# An orphaned BEAM (its tmux session gone) can still hold THIS instance's node
|
|
873
907
|
# name. With no live session on our (instance-keyed) socket, reap the name-holder
|
|
874
908
|
# so the launch isn't blocked by "name seems to be in use". The keyed name means
|
|
@@ -899,6 +933,7 @@ run_session() {
|
|
|
899
933
|
echo "❌ aiur failed to start; captured output:" >&2
|
|
900
934
|
tail -n 30 "$startup_capture" 2>/dev/null | sed 's/^/ /' >&2 || true
|
|
901
935
|
rm -f "${launch_tempfiles[@]}" 2>/dev/null || true
|
|
936
|
+
release_aiur_launch_lock "$launch_lock"
|
|
902
937
|
exit 1
|
|
903
938
|
fi
|
|
904
939
|
|
|
@@ -924,12 +959,16 @@ run_session() {
|
|
|
924
959
|
reap_aiur_agents "$socket" "$AIUR_AGENT_TMPFILE"
|
|
925
960
|
kill_beams_matching "-name ${AIUR_RELEASE_NODE}"
|
|
926
961
|
fi
|
|
962
|
+
release_aiur_launch_lock "$launch_lock"
|
|
963
|
+
_session_launch_lock=""
|
|
927
964
|
[ "$mode" = "foreground" ] && return 1
|
|
928
965
|
rm -f "${launch_tempfiles[@]}" 2>/dev/null || true
|
|
929
966
|
exit 1
|
|
930
967
|
fi
|
|
931
968
|
|
|
932
969
|
write_aiur_instance_record "$session" "$socket"
|
|
970
|
+
release_aiur_launch_lock "$launch_lock"
|
|
971
|
+
_session_launch_lock=""
|
|
933
972
|
print_config_status "$startup_capture"
|
|
934
973
|
print_dashboard_status "$no_dashboard" "$startup_capture"
|
|
935
974
|
# The `aiur run` version notice: a cheap local state read (the daemon's
|
|
@@ -976,10 +1015,18 @@ run_session() {
|
|
|
976
1015
|
# Foreground: attach the UI. Do not exec — that would drop the teardown trap.
|
|
977
1016
|
# Avoid process substitution here: some sandboxed non-TTY launchers reject
|
|
978
1017
|
# opening /dev/fd/* during the real manual-test wrapper path.
|
|
1018
|
+
attach_tmux_session "$tmux_bin" "$socket" "$conf" "$session"
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
attach_tmux_session() {
|
|
1022
|
+
local tmux_bin="$1" socket="$2" conf="$3" session="$4"
|
|
979
1023
|
local attach_stderr attach_code
|
|
980
1024
|
attach_stderr="$(mktemp "${TMPDIR:-/tmp}/aiur-attach-stderr.XXXXXX")"
|
|
981
|
-
"$tmux_bin" -L "$socket" -f "$conf" attach -t "$session" 2>"$attach_stderr"
|
|
982
|
-
|
|
1025
|
+
if "$tmux_bin" -L "$socket" -f "$conf" attach -t "$session" 2>"$attach_stderr"; then
|
|
1026
|
+
attach_code=0
|
|
1027
|
+
else
|
|
1028
|
+
attach_code=$?
|
|
1029
|
+
fi
|
|
983
1030
|
grep -v -F "[server exited]" "$attach_stderr" >&2 || true
|
|
984
1031
|
rm -f "$attach_stderr" 2>/dev/null || true
|
|
985
1032
|
return "$attach_code"
|
|
@@ -1002,12 +1049,31 @@ resolve_tmux_conf() {
|
|
|
1002
1049
|
# name (covers orphans from any release dir): under the unified identity a dev
|
|
1003
1050
|
# `_build` BEAM and an installed BEAM both claim the same node name, so a
|
|
1004
1051
|
# release-path pgrep misses whichever one this run didn't launch.
|
|
1052
|
+
# TERM the node(s) matching $pattern, give them $grace_ticks (0.1s) to exit
|
|
1053
|
+
# gracefully, then SIGKILL whatever remains. The default grace is short (3s)
|
|
1054
|
+
# because startup reclaim wants to clear a stale BEAM quickly; the stop paths
|
|
1055
|
+
# pass a longer grace so the BEAM can finish its own agent reap + session
|
|
1056
|
+
# deletion instead of being SIGKILLed mid-cleanup (which is what orphaned agent
|
|
1057
|
+
# processes on `aiur stop`). Callers may override the grace internally; invalid
|
|
1058
|
+
# or excessive values fall back to the 3s startup-reclaim default instead of
|
|
1059
|
+
# skipping the wait and jumping straight to SIGKILL.
|
|
1005
1060
|
kill_beams_matching() {
|
|
1006
|
-
local pattern="$1" pids pid waited=0
|
|
1061
|
+
local pattern="$1" grace_ticks="${2:-30}" pids pid waited=0
|
|
1062
|
+
case "$grace_ticks" in
|
|
1063
|
+
"" | *[!0-9]*) grace_ticks=30 ;;
|
|
1064
|
+
*)
|
|
1065
|
+
# Reject overlong digit strings before numeric comparison so shell integer
|
|
1066
|
+
# overflow cannot turn an excessive grace into an immediate SIGKILL.
|
|
1067
|
+
if [ "${#grace_ticks}" -gt 5 ] ||
|
|
1068
|
+
{ [ "${#grace_ticks}" -eq 5 ] && [ "$grace_ticks" -gt 36000 ]; }; then
|
|
1069
|
+
grace_ticks=30
|
|
1070
|
+
fi
|
|
1071
|
+
;;
|
|
1072
|
+
esac
|
|
1007
1073
|
pids="$(pgrep -f -- "$pattern" 2>/dev/null || true)"
|
|
1008
1074
|
[ -n "$pids" ] || return 0
|
|
1009
1075
|
for pid in $pids; do kill -TERM "$pid" 2>/dev/null || true; done
|
|
1010
|
-
while [ "$waited" -lt
|
|
1076
|
+
while [ "$waited" -lt "$grace_ticks" ]; do
|
|
1011
1077
|
pids="$(pgrep -f -- "$pattern" 2>/dev/null || true)"
|
|
1012
1078
|
[ -z "$pids" ] && break
|
|
1013
1079
|
sleep 0.1
|
|
@@ -1336,8 +1402,76 @@ aiur_instance_record_path() {
|
|
|
1336
1402
|
printf '%s/%s.instance' "$(aiur_instances_dir)" "$(aiur_state_slug)"
|
|
1337
1403
|
}
|
|
1338
1404
|
|
|
1405
|
+
aiur_launch_lock_path() {
|
|
1406
|
+
printf '%s/locks/%s.launch' "${AIUR_BG_STATE_DIR:?}" "$(aiur_state_slug)"
|
|
1407
|
+
}
|
|
1408
|
+
|
|
1409
|
+
acquire_aiur_launch_lock() {
|
|
1410
|
+
local lock="$1" owner_file="$1/owner" recovery_lock="$1.recovery"
|
|
1411
|
+
local owner tick lock_age
|
|
1412
|
+
local max_ticks="${AIUR_LAUNCH_LOCK_TICKS:-${AIUR_NODE_GRACE_TICKS:-1200}}"
|
|
1413
|
+
local ownerless_stale_seconds="${AIUR_OWNERLESS_LOCK_STALE_SECONDS:-120}"
|
|
1414
|
+
mkdir -p "$(dirname "$lock")" 2>/dev/null || return 1
|
|
1415
|
+
|
|
1416
|
+
for ((tick = 0; tick < max_ticks; tick++)); do
|
|
1417
|
+
if mkdir "$lock" 2>/dev/null; then
|
|
1418
|
+
printf '%s\n' "$$" >"$owner_file" || { rmdir "$lock" 2>/dev/null || true; return 1; }
|
|
1419
|
+
return 0
|
|
1420
|
+
fi
|
|
1421
|
+
|
|
1422
|
+
owner="$(sed -n '1p' "$owner_file" 2>/dev/null || true)"
|
|
1423
|
+
if [[ "$owner" =~ ^[0-9]+$ ]] && kill -0 "$owner" 2>/dev/null; then
|
|
1424
|
+
sleep 0.1
|
|
1425
|
+
continue
|
|
1426
|
+
fi
|
|
1427
|
+
|
|
1428
|
+
lock_age="$(aiur_lock_age_seconds "$lock")"
|
|
1429
|
+
if [ -z "$owner" ] && { [ -z "$lock_age" ] || [ "$lock_age" -lt "$ownerless_stale_seconds" ]; }; then
|
|
1430
|
+
sleep 0.1
|
|
1431
|
+
continue
|
|
1432
|
+
fi
|
|
1433
|
+
|
|
1434
|
+
# Only one contender may reclaim a dead lock. Re-read ownership while the
|
|
1435
|
+
# recovery mutex is held so another contender cannot delete a newly claimed
|
|
1436
|
+
# launch directory after losing the original stale-lock race.
|
|
1437
|
+
if mkdir "$recovery_lock" 2>/dev/null; then
|
|
1438
|
+
owner="$(sed -n '1p' "$owner_file" 2>/dev/null || true)"
|
|
1439
|
+
lock_age="$(aiur_lock_age_seconds "$lock")"
|
|
1440
|
+
if { [ -z "$owner" ] && [ -n "$lock_age" ] && [ "$lock_age" -ge "$ownerless_stale_seconds" ]; } || \
|
|
1441
|
+
{ [[ "$owner" =~ ^[0-9]+$ ]] && ! kill -0 "$owner" 2>/dev/null; }; then
|
|
1442
|
+
rm -f "$owner_file" 2>/dev/null || true
|
|
1443
|
+
rmdir "$lock" 2>/dev/null || true
|
|
1444
|
+
fi
|
|
1445
|
+
rmdir "$recovery_lock" 2>/dev/null || true
|
|
1446
|
+
fi
|
|
1447
|
+
sleep 0.1
|
|
1448
|
+
done
|
|
1449
|
+
|
|
1450
|
+
echo "aiur: another launch for this directory is still in progress; retry aiur" >&2
|
|
1451
|
+
return 1
|
|
1452
|
+
}
|
|
1453
|
+
|
|
1454
|
+
aiur_lock_age_seconds() {
|
|
1455
|
+
local path="$1" now modified
|
|
1456
|
+
now="$(date +%s 2>/dev/null || true)"
|
|
1457
|
+
modified="$(stat -c %Y "$path" 2>/dev/null || stat -f %m "$path" 2>/dev/null || true)"
|
|
1458
|
+
[[ "$now" =~ ^[0-9]+$ ]] && [[ "$modified" =~ ^[0-9]+$ ]] || return 0
|
|
1459
|
+
[ "$now" -ge "$modified" ] || return 0
|
|
1460
|
+
printf '%s' "$((now - modified))"
|
|
1461
|
+
}
|
|
1462
|
+
|
|
1463
|
+
release_aiur_launch_lock() {
|
|
1464
|
+
local lock="${1:-}" owner_file owner
|
|
1465
|
+
[ -n "$lock" ] || return 0
|
|
1466
|
+
owner_file="$lock/owner"
|
|
1467
|
+
owner="$(sed -n '1p' "$owner_file" 2>/dev/null || true)"
|
|
1468
|
+
[ "$owner" = "$$" ] || return 0
|
|
1469
|
+
rm -f "$owner_file" 2>/dev/null || true
|
|
1470
|
+
rmdir "$lock" 2>/dev/null || true
|
|
1471
|
+
}
|
|
1472
|
+
|
|
1339
1473
|
write_aiur_instance_record() {
|
|
1340
|
-
local session="$1" socket="$2" record_dir record tmp root
|
|
1474
|
+
local session="$1" socket="$2" write_mode="${3:-replace}" record_dir record tmp root
|
|
1341
1475
|
record_dir="$(aiur_instances_dir)"
|
|
1342
1476
|
record="$(aiur_instance_record_path)"
|
|
1343
1477
|
root="$(canonical_workspace_root "${AIUR_PROJECT_ROOT:-}")"
|
|
@@ -1354,7 +1488,14 @@ write_aiur_instance_record() {
|
|
|
1354
1488
|
printf 'AIUR_RECORD_WRITTEN_AT=%q\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || true)"
|
|
1355
1489
|
} >"$tmp" || { rm -f "$tmp" 2>/dev/null || true; return 0; }
|
|
1356
1490
|
chmod 0600 "$tmp" 2>/dev/null || true
|
|
1357
|
-
|
|
1491
|
+
if [ "$write_mode" = "if-absent" ]; then
|
|
1492
|
+
# Same-directory hard-link creation is atomic: a concurrent live launcher
|
|
1493
|
+
# can win without an attaching shell overwriting its workspace handoff.
|
|
1494
|
+
ln "$tmp" "$record" 2>/dev/null || true
|
|
1495
|
+
rm -f "$tmp" 2>/dev/null || true
|
|
1496
|
+
else
|
|
1497
|
+
mv "$tmp" "$record" 2>/dev/null || rm -f "$tmp" 2>/dev/null || true
|
|
1498
|
+
fi
|
|
1358
1499
|
}
|
|
1359
1500
|
|
|
1360
1501
|
# Marker `status` reads to tell "daemon crashed, agents may be orphaned" apart
|
|
@@ -1469,6 +1610,16 @@ record_beam_crash() {
|
|
|
1469
1610
|
|
|
1470
1611
|
canonical_workspace_root() {
|
|
1471
1612
|
local root="$1"
|
|
1613
|
+
# The BEAM hands off Config.workspace_root() verbatim, which may be a `~/...`
|
|
1614
|
+
# path (e.g. this repo's `workspace.root: ~/code/aiur-workspaces`). `cd` does
|
|
1615
|
+
# not expand `~` inside a quoted variable, so without this a `~`-rooted cwd
|
|
1616
|
+
# sweep compared against the literal `~/...` string, matched no /proc cwd, and
|
|
1617
|
+
# silently reaped nothing — the exact backstop that must catch workspace-rooted
|
|
1618
|
+
# agents on stop. Expand a leading `~` before canonicalizing so the sweep and
|
|
1619
|
+
# the /proc cwd values (always absolute) agree.
|
|
1620
|
+
case "$root" in
|
|
1621
|
+
"~" | "~/"*) root="${HOME:-}${root#\~}" ;;
|
|
1622
|
+
esac
|
|
1472
1623
|
if [ -d "$root" ]; then
|
|
1473
1624
|
(cd "$root" 2>/dev/null && pwd -P) || printf '%s\n' "$root"
|
|
1474
1625
|
else
|
|
@@ -1742,7 +1893,7 @@ wait_for_session_startup() {
|
|
|
1742
1893
|
_session_socket="" _session_name="" _session_conf="" _session_tmpfile=""
|
|
1743
1894
|
_session_capture="" _session_argv="" _session_release="" _session_tmux="" _session_node=""
|
|
1744
1895
|
_session_pidfile="" _session_watchdog_pid="" _session_workspace_root_file=""
|
|
1745
|
-
_session_alert_ledger_path_file="" _session_crash_dump_baseline_file=""
|
|
1896
|
+
_session_alert_ledger_path_file="" _session_crash_dump_baseline_file="" _session_launch_lock=""
|
|
1746
1897
|
_cleanup_ran=0
|
|
1747
1898
|
session_cleanup() {
|
|
1748
1899
|
[ "$_cleanup_ran" = 1 ] && return 0
|
|
@@ -1769,8 +1920,11 @@ session_cleanup() {
|
|
|
1769
1920
|
reap_aiur_agents "$_session_socket" "$_session_pidfile"
|
|
1770
1921
|
|
|
1771
1922
|
# Reap only this instance's BEAM. Multiple keyed instances can share the same
|
|
1772
|
-
# release dir, so release-path pgrep would terminate sibling workflows.
|
|
1773
|
-
|
|
1923
|
+
# release dir, so release-path pgrep would terminate sibling workflows. Give
|
|
1924
|
+
# the BEAM a generous TERM grace (the stop grace, not the 3s startup reclaim
|
|
1925
|
+
# default) so its own graceful shutdown — ProcessReaper reaping the agent tree
|
|
1926
|
+
# plus the BEAM-side workspace sweep — completes before any SIGKILL lands.
|
|
1927
|
+
[ -n "$_session_node" ] && kill_beams_matching "-name ${_session_node}" 300
|
|
1774
1928
|
|
|
1775
1929
|
# Final cwd-sweep backstop after the BEAM is gone: catches workspace-rooted
|
|
1776
1930
|
# agents or test children that registered too late, reparented during the
|
|
@@ -1808,6 +1962,8 @@ session_cleanup() {
|
|
|
1808
1962
|
"$_session_workspace_root_file" "$_session_alert_ledger_path_file" \
|
|
1809
1963
|
"$_session_crash_dump_baseline_file" 2>/dev/null || true
|
|
1810
1964
|
rm -f "$(aiur_instance_record_path)" 2>/dev/null || true
|
|
1965
|
+
release_aiur_launch_lock "$_session_launch_lock"
|
|
1966
|
+
_session_launch_lock=""
|
|
1811
1967
|
return $code
|
|
1812
1968
|
}
|
|
1813
1969
|
install_foreground_traps() {
|
|
@@ -1852,6 +2008,21 @@ probe_node_liveness() {
|
|
|
1852
2008
|
printf 'unknown'
|
|
1853
2009
|
}
|
|
1854
2010
|
|
|
2011
|
+
confirm_stale_tmux_session() {
|
|
2012
|
+
local tmux_bin="$1" socket="$2" conf="$3" session="$4"
|
|
2013
|
+
local max_ticks="${AIUR_STALE_SESSION_GRACE_TICKS:-30}" tick
|
|
2014
|
+
|
|
2015
|
+
for ((tick = 0; tick < max_ticks; tick++)); do
|
|
2016
|
+
"$tmux_bin" -L "$socket" -f "$conf" has-session -t "$session" 2>/dev/null || return 0
|
|
2017
|
+
[ "$(probe_control_liveness)" = "up" ] && return 1
|
|
2018
|
+
[ "$(probe_node_liveness)" = "down" ] || return 1
|
|
2019
|
+
sleep 0.1
|
|
2020
|
+
done
|
|
2021
|
+
|
|
2022
|
+
"$tmux_bin" -L "$socket" -f "$conf" has-session -t "$session" 2>/dev/null || return 0
|
|
2023
|
+
[ "$(probe_control_liveness)" != "up" ] && [ "$(probe_node_liveness)" = "down" ]
|
|
2024
|
+
}
|
|
2025
|
+
|
|
1855
2026
|
probe_named_node_liveness() {
|
|
1856
2027
|
local node="$1" old_node="${RELEASE_NODE:-}" state
|
|
1857
2028
|
RELEASE_NODE="$node"
|
|
@@ -2004,6 +2175,20 @@ kill_control_rpc_process() {
|
|
|
2004
2175
|
for p in "${tree[@]}"; do kill -KILL "$p" 2>/dev/null || true; done
|
|
2005
2176
|
}
|
|
2006
2177
|
|
|
2178
|
+
# Signalling the watchdog subshell alone leaves its `sleep` child running and
|
|
2179
|
+
# orphaned: bash forks the subshell, and the sleep is a separate process the
|
|
2180
|
+
# subshell's death does not reap. Kill the descendants too so a fast reply
|
|
2181
|
+
# leaves nothing behind that outlives the command.
|
|
2182
|
+
# Deepest-first: a bash subshell that is TERMed while waiting on a foreground
|
|
2183
|
+
# child defers the signal until that child exits, so signalling the wrapper
|
|
2184
|
+
# before the sleep it is waiting on can leave the sleep running.
|
|
2185
|
+
cancel_timeout_watchdog() {
|
|
2186
|
+
local pid="$1" p tree=() i
|
|
2187
|
+
[ -n "$pid" ] || return 0
|
|
2188
|
+
while IFS= read -r p; do tree+=("$p"); done < <(agent_pid_tree "$pid")
|
|
2189
|
+
for ((i = ${#tree[@]} - 1; i >= 0; i--)); do kill -TERM "${tree[i]}" 2>/dev/null || true; done
|
|
2190
|
+
}
|
|
2191
|
+
|
|
2007
2192
|
run_release_rpc_with_timeout() {
|
|
2008
2193
|
local expression="$1" timeout output_file timeout_file pid watchdog_pid status grouped=0
|
|
2009
2194
|
timeout="$(control_rpc_timeout_seconds)"
|
|
@@ -2019,13 +2204,18 @@ run_release_rpc_with_timeout() {
|
|
|
2019
2204
|
fi
|
|
2020
2205
|
pid=$!
|
|
2021
2206
|
|
|
2207
|
+
# The watchdog must not hold the caller's stdout/stderr. A caller that captures
|
|
2208
|
+
# our output (command substitution, a pipe, a language runtime's subprocess
|
|
2209
|
+
# capture) reads until EOF, and EOF arrives only when every process holding the
|
|
2210
|
+
# write end exits - including a watchdog that is merely sleeping. Closing those
|
|
2211
|
+
# descriptors here is what keeps the timeout a ceiling instead of a floor.
|
|
2022
2212
|
(
|
|
2023
2213
|
sleep "$timeout"
|
|
2024
2214
|
if kill -0 "$pid" 2>/dev/null; then
|
|
2025
2215
|
: >"$timeout_file"
|
|
2026
2216
|
kill_control_rpc_process "$pid" "$grouped"
|
|
2027
2217
|
fi
|
|
2028
|
-
) &
|
|
2218
|
+
) >/dev/null 2>&1 &
|
|
2029
2219
|
watchdog_pid=$!
|
|
2030
2220
|
|
|
2031
2221
|
if wait "$pid"; then
|
|
@@ -2039,7 +2229,7 @@ run_release_rpc_with_timeout() {
|
|
|
2039
2229
|
# strand descendants that ignored TERM after the root process exits.
|
|
2040
2230
|
:
|
|
2041
2231
|
else
|
|
2042
|
-
|
|
2232
|
+
cancel_timeout_watchdog "$watchdog_pid"
|
|
2043
2233
|
fi
|
|
2044
2234
|
wait "$watchdog_pid" 2>/dev/null || true
|
|
2045
2235
|
|
|
@@ -3196,8 +3386,13 @@ cmd_stop() {
|
|
|
3196
3386
|
fi
|
|
3197
3387
|
|
|
3198
3388
|
# Reap any BEAM holding our node name regardless of which release dir launched
|
|
3199
|
-
# it. Do not sweep by release dir: sibling instances share dev releases.
|
|
3200
|
-
|
|
3389
|
+
# it. Do not sweep by release dir: sibling instances share dev releases. Use
|
|
3390
|
+
# the generous stop TERM grace: the BEAM, once TERM'd, runs its own graceful
|
|
3391
|
+
# shutdown (ProcessReaper reaping the agent tree, opencode session deletes,
|
|
3392
|
+
# then the BEAM-side workspace sweep) and the short 3s startup default would
|
|
3393
|
+
# SIGKILL it mid-cleanup, orphaning exactly the agents this stop must reap.
|
|
3394
|
+
# The loop exits the moment the BEAM is gone, so a fast stop costs nothing.
|
|
3395
|
+
kill_beams_matching "-name ${AIUR_RELEASE_NODE}" 300
|
|
3201
3396
|
|
|
3202
3397
|
# Final cwd-sweep backstop after the BEAM is dead. The launcher's instance
|
|
3203
3398
|
# record points to the BEAM-written root handoff, so degraded stop never has
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "aiur-cli",
|
|
3
|
-
"version": "0.0.5
|
|
3
|
+
"version": "0.0.5",
|
|
4
4
|
"description": "AI Unit Runtime for Executors.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"homepage": "https://github.com/aiur-team/aiur",
|
|
@@ -35,8 +35,8 @@
|
|
|
35
35
|
],
|
|
36
36
|
"opencodeVersion": "1.17.10",
|
|
37
37
|
"optionalDependencies": {
|
|
38
|
-
"aiur-cli-linux-x64": "0.0.5
|
|
39
|
-
"aiur-cli-linux-arm64": "0.0.5
|
|
40
|
-
"aiur-cli-darwin-arm64": "0.0.5
|
|
38
|
+
"aiur-cli-linux-x64": "0.0.5",
|
|
39
|
+
"aiur-cli-linux-arm64": "0.0.5",
|
|
40
|
+
"aiur-cli-darwin-arm64": "0.0.5"
|
|
41
41
|
}
|
|
42
42
|
}
|