aiur-cli 0.0.8 → 0.0.9-nightly.3ae367c
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/bin/aiur.js +37 -17
- package/libexec/aiur-capabilities.sh +12 -0
- package/libexec/aiur-daemon-supervisor.sh +96 -0
- package/libexec/aiur-engine.sh +193 -4004
- package/libexec/aiur-epic.sh +41 -0
- package/libexec/aiur-executor-session.sh +38 -0
- package/libexec/aiur-experiments.sh +45 -0
- package/libexec/aiur-feature.sh +86 -0
- package/libexec/aiur-queue.sh +115 -0
- package/libexec/engine/commands.sh +327 -0
- package/libexec/engine/control.sh +464 -0
- package/libexec/engine/dotenv.sh +87 -0
- package/libexec/engine/executor_commands.sh +371 -0
- package/libexec/engine/handoff.sh +47 -0
- package/libexec/engine/identity.sh +377 -0
- package/libexec/engine/instance.sh +230 -0
- package/libexec/engine/lifecycle.sh +500 -0
- package/libexec/engine/oneshot.sh +173 -0
- package/libexec/engine/pre_pr.sh +238 -0
- package/libexec/engine/processes.sh +270 -0
- package/libexec/engine/reap.sh +270 -0
- package/libexec/engine/report_commands.sh +310 -0
- package/libexec/engine/run.sh +474 -0
- package/libexec/engine/session.sh +206 -0
- package/libexec/engine/test-private-state.sh +59 -0
- package/libexec/engine/upgrade.sh +220 -0
- package/package.json +12 -4
package/bin/aiur.js
CHANGED
|
@@ -165,8 +165,8 @@ function ensureTmux() {
|
|
|
165
165
|
// True only when the pinned opencode is on PATH; a wrong (e.g. newer,
|
|
166
166
|
// unverified) version must be replaced with the pin, so presence alone
|
|
167
167
|
// isn't enough.
|
|
168
|
-
function opencodeMatchesPin() {
|
|
169
|
-
const result = safeSpawn(
|
|
168
|
+
function opencodeMatchesPin(command = "opencode") {
|
|
169
|
+
const result = safeSpawn(command, ["--version"], { encoding: "utf8" });
|
|
170
170
|
return !result.error && result.status === 0 && (result.stdout || "").trim() === opencodeVersion();
|
|
171
171
|
}
|
|
172
172
|
|
|
@@ -185,40 +185,60 @@ function installOpencode() {
|
|
|
185
185
|
return !result.error && result.status === 0;
|
|
186
186
|
}
|
|
187
187
|
|
|
188
|
+
// npm's global bin may be shadowed by another OpenCode on the incoming PATH.
|
|
189
|
+
// Verify the actual installed executable before making it the release's choice.
|
|
190
|
+
function selectGlobalOpencode() {
|
|
191
|
+
const result = safeSpawn("npm", ["prefix", "-g"], { encoding: "utf8" });
|
|
192
|
+
if (result.error || result.status !== 0) return false;
|
|
193
|
+
const prefix = (result.stdout || "").trim();
|
|
194
|
+
if (!path.isAbsolute(prefix)) return false;
|
|
195
|
+
const bin = path.join(prefix, "bin");
|
|
196
|
+
if (!opencodeMatchesPin(path.join(bin, "opencode"))) return false;
|
|
197
|
+
process.env.PATH = `${bin}${path.delimiter}${process.env.PATH || ""}`;
|
|
198
|
+
return true;
|
|
199
|
+
}
|
|
200
|
+
|
|
188
201
|
// opencode powers the interactive "take the wheel" panes. Non-fatal: everything
|
|
189
202
|
// else works without it.
|
|
190
203
|
function ensureOpencode() {
|
|
191
|
-
if (opencodeMatchesPin()) return;
|
|
204
|
+
if (opencodeMatchesPin() || selectGlobalOpencode()) return;
|
|
192
205
|
if (process.env.AIUR_SKIP_OPENCODE_INSTALL !== "1") {
|
|
193
206
|
process.stderr.write(
|
|
194
207
|
`aiur: provisioning opencode@${opencodeVersion()} (one-time, for the interactive 'take the wheel' panes)…\n`,
|
|
195
208
|
);
|
|
196
209
|
installOpencode();
|
|
197
|
-
if (opencodeMatchesPin()) return;
|
|
210
|
+
if (opencodeMatchesPin() || selectGlobalOpencode()) return;
|
|
198
211
|
}
|
|
199
212
|
process.stderr.write(
|
|
200
|
-
|
|
213
|
+
`aiur: opencode@${opencodeVersion()} is unavailable. The interactive 'take the wheel' ` +
|
|
201
214
|
"feature needs it; everything else works without it. " +
|
|
202
|
-
|
|
215
|
+
`Install with: npm install -g opencode-ai@${opencodeVersion()}, ` +
|
|
216
|
+
"and put npm's global bin ahead of other opencode binaries on PATH.\n",
|
|
203
217
|
);
|
|
204
218
|
}
|
|
205
219
|
|
|
206
|
-
//
|
|
207
|
-
//
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
220
|
+
// Match the shared engine's first-argument dispatch: only session launch paths
|
|
221
|
+
// need interactive tools. Unknown commands still reach the engine's usage error.
|
|
222
|
+
function requiresInteractiveTools(argv) {
|
|
223
|
+
const command = argv[0] || "";
|
|
224
|
+
if (["", "run", "restart", "--bg"].includes(command)) return true;
|
|
225
|
+
const nonLaunchCommands = new Set([
|
|
226
|
+
"__identity", "help", "-h", "-help", "--h", "--help", "--version", "--todo", "--only",
|
|
227
|
+
"init", "pr", "findings", "ask", "asks", "status", "usage", "agents", "commands", "units",
|
|
228
|
+
"build-orders", "queue", "analytics", "experiments", "github-cost", "github-usage", "alerts", "watch", "set",
|
|
229
|
+
"upgrade", "pause", "resume", "park", "reset-budget", "message", "cleanup-stale", "stop",
|
|
230
|
+
"executor-answer", "executor-escalate", "executor-reclassify", "executor-moot", "executor-listen", "executor-wait",
|
|
231
|
+
"executor-emit", "executor-subscribe", "executor-unsubscribe", "executor-subscriptions",
|
|
232
|
+
"executor-roster", "executor-session", "executor-fast-forward", "executor-claim", "executor-release", "executor-revoke",
|
|
233
|
+
]);
|
|
234
|
+
if (nonLaunchCommands.has(command)) return false;
|
|
235
|
+
return command.startsWith("-") || fs.existsSync(command);
|
|
216
236
|
}
|
|
217
237
|
|
|
218
238
|
function main() {
|
|
219
239
|
const releaseDir = resolveReleaseDir();
|
|
220
240
|
|
|
221
|
-
if (
|
|
241
|
+
if (requiresInteractiveTools(process.argv.slice(2))) {
|
|
222
242
|
ensureTmux();
|
|
223
243
|
ensureOpencode();
|
|
224
244
|
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# Read-only capability report through the daemon's local control RPC.
|
|
2
|
+
cmd_capabilities() {
|
|
3
|
+
local opts="" arg
|
|
4
|
+
for arg in "$@"; do
|
|
5
|
+
case "$arg" in
|
|
6
|
+
--json) opts="json: true" ;;
|
|
7
|
+
-*) echo "aiur: capabilities received an unknown option: $arg" >&2; exit 64 ;;
|
|
8
|
+
*) echo "aiur: capabilities does not accept positional arguments" >&2; exit 64 ;;
|
|
9
|
+
esac
|
|
10
|
+
done
|
|
11
|
+
run_control_rpc "Aiur.AgentControlCLI.capabilities([$opts])"
|
|
12
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Sourced by the shared engine and the daemon pane.
|
|
3
|
+
|
|
4
|
+
supervisor_is_alive() {
|
|
5
|
+
local file="${1:-}" pid=""
|
|
6
|
+
[ -n "$file" ] && [ -r "$file" ] || return 1
|
|
7
|
+
IFS= read -r pid <"$file" || true
|
|
8
|
+
[[ "$pid" =~ ^[1-9][0-9]*$ ]] && kill -0 "$pid" 2>/dev/null
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
record_daemon_recovery() {
|
|
12
|
+
local name="$1" message="$2" since="$3" quiet="${4:-}" ledger="" timestamp
|
|
13
|
+
timestamp="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
|
14
|
+
printf '%s %s; down since %s\n' "$timestamp" "$message" "$since" >>"$AIUR_LOGS_ROOT/log/aiur.recovery"
|
|
15
|
+
[ -z "$quiet" ] || return 0
|
|
16
|
+
IFS= read -r ledger <"$AIUR_ALERT_LEDGER_PATH_FILE" || true
|
|
17
|
+
[ -n "$ledger" ] || return 0
|
|
18
|
+
mkdir -p "$(dirname "$ledger")"
|
|
19
|
+
printf '{"event":"alert","agent":"system","timestamp":"%s","name":"system.daemon.%s","topic":"system.daemon.%s","message":"%s","reason":"Daemon down since %s","severity":"warning","needs_attention":true}\n' \
|
|
20
|
+
"$timestamp" "$name" "$name" "$(json_escape_alert_field "$message")" "$since" >>"$ledger"
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
supervise_daemon() {
|
|
24
|
+
local supervisor_file="$1" capture="$2" baseline="$3"
|
|
25
|
+
shift 3
|
|
26
|
+
local status delay=1 started down_since="" child pane prev_dump giving_up crashes=() now t kept first_dump
|
|
27
|
+
local max_crashes="${AIUR_DAEMON_MAX_RESTARTS:-5}" crash_window=600
|
|
28
|
+
printf '%s\n' "$$" >"$supervisor_file"
|
|
29
|
+
release_bin="$AIUR_RELEASE_DIR/bin/aiur"
|
|
30
|
+
mkdir -p "$AIUR_LOGS_ROOT/log"
|
|
31
|
+
exec 3<&0
|
|
32
|
+
while :; do
|
|
33
|
+
started="$SECONDS"
|
|
34
|
+
(set +e; "$@" 2>&1 | tee -a "$capture"; exit "${PIPESTATUS[0]}") <&3 &
|
|
35
|
+
child=$!
|
|
36
|
+
if [ -n "$down_since" ]; then
|
|
37
|
+
# Bound readiness probes; a boot attempt is not evidence of recovery.
|
|
38
|
+
while kill -0 "$child" 2>/dev/null; do
|
|
39
|
+
if run_release_rpc_with_timeout 'if Enum.any?(Application.started_applications(), fn {app, _, _} -> app == :aiur end), do: IO.puts("__AIUR_RECOVERED__")' &&
|
|
40
|
+
[[ "$AIUR_CONTROL_RPC_OUTPUT" == *"__AIUR_RECOVERED__"* ]]; then
|
|
41
|
+
record_daemon_recovery restarted 'Daemon restarted after crash' "$down_since"
|
|
42
|
+
rm -f "$(aiur_crash_marker_path)"
|
|
43
|
+
down_since=""
|
|
44
|
+
break
|
|
45
|
+
fi
|
|
46
|
+
sleep 1
|
|
47
|
+
done
|
|
48
|
+
fi
|
|
49
|
+
if wait "$child"; then status=0; else status=$?; fi
|
|
50
|
+
# Initial boot failures stay visible to the launcher; normal halts stay stopped.
|
|
51
|
+
if [ "$status" -eq 0 ] || [ -f "$(aiur_stop_sentinel_path)" ] || ! grep -qx ready "$supervisor_file"; then
|
|
52
|
+
rm -f "$supervisor_file"
|
|
53
|
+
return "$status"
|
|
54
|
+
fi
|
|
55
|
+
# One needs-attention alert per outage; later attempts only reach the recovery log.
|
|
56
|
+
local quiet=1
|
|
57
|
+
[ -n "$down_since" ] || { down_since="$(date -u +%Y-%m-%dT%H:%M:%SZ)"; quiet=""; }
|
|
58
|
+
record_beam_crash "$RELEASE_NODE" "$AIUR_LOGS_ROOT" "$(aiur_crash_marker_path)" "$AIUR_ALERT_LEDGER_PATH_FILE" "$baseline"
|
|
59
|
+
now="$SECONDS"; kept=()
|
|
60
|
+
for t in "${crashes[@]}"; do [ "$((now - t))" -ge "$crash_window" ] || kept+=("$t"); done
|
|
61
|
+
crashes=("${kept[@]}" "$now")
|
|
62
|
+
giving_up=""
|
|
63
|
+
[ "${#crashes[@]}" -le "$max_crashes" ] || giving_up=1
|
|
64
|
+
if [ -n "$giving_up" ]; then
|
|
65
|
+
record_daemon_recovery gave-up "Daemon crashed ${#crashes[@]} times within ${crash_window}s; not restarting" "$down_since"
|
|
66
|
+
else
|
|
67
|
+
record_daemon_recovery down "Daemon down; retrying in ${delay}s (exit $status)" "$down_since" "$quiet"
|
|
68
|
+
fi
|
|
69
|
+
# Archive before another VM can overwrite even an operator-selected dump path.
|
|
70
|
+
if [ -f "$ERL_CRASH_DUMP" ]; then
|
|
71
|
+
mv "$ERL_CRASH_DUMP" "$ERL_CRASH_DUMP.$(date -u +%Y%m%dT%H%M%SZ).$$.$SECONDS" || return 1
|
|
72
|
+
# Keep only the first and the latest dump; the middle ones are GBs of repeats.
|
|
73
|
+
first_dump="" prev_dump=""
|
|
74
|
+
for t in "$ERL_CRASH_DUMP".*; do
|
|
75
|
+
[ -f "$t" ] || continue
|
|
76
|
+
[ -n "$first_dump" ] || { first_dump="$t"; continue; }
|
|
77
|
+
[ -z "$prev_dump" ] || rm -f "$prev_dump"
|
|
78
|
+
prev_dump="$t"
|
|
79
|
+
done
|
|
80
|
+
fi
|
|
81
|
+
[ -z "$giving_up" ] || { rm -f "$supervisor_file"; return "$status"; }
|
|
82
|
+
crash_dump_identity "$ERL_CRASH_DUMP" >"$baseline" || :
|
|
83
|
+
reap_aiur_agents "" "$AIUR_AGENT_TMPFILE"
|
|
84
|
+
reap_workspace_cwd_from_file "$AIUR_WORKSPACE_ROOT_FILE"
|
|
85
|
+
: >"$AIUR_AGENT_TMPFILE"
|
|
86
|
+
# Keep this pane, remove the dead VM's UI/agent panes before rebuilding the UI.
|
|
87
|
+
while IFS= read -r pane; do
|
|
88
|
+
[ "$pane" = "${TMUX_PANE:-}" ] || tmux -L "$AIUR_TMUX_SOCKET" kill-pane -t "$pane" 2>/dev/null || true
|
|
89
|
+
done < <(tmux -L "$AIUR_TMUX_SOCKET" list-panes -a -F '#{pane_id}' 2>/dev/null)
|
|
90
|
+
[ "$((SECONDS - started))" -lt 60 ] || delay=1
|
|
91
|
+
sleep "$delay"
|
|
92
|
+
[ ! -f "$(aiur_stop_sentinel_path)" ] || { rm -f "$supervisor_file"; return 0; }
|
|
93
|
+
[ "$delay" -ge 30 ] || delay=$((delay * 2))
|
|
94
|
+
[ "$delay" -le 30 ] || delay=30
|
|
95
|
+
done
|
|
96
|
+
}
|