agent-control-plane 0.2.0 → 0.3.0
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/npm/bin/agent-control-plane.js +39 -2
- package/package.json +6 -3
- package/tools/bin/agent-project-catch-up-merged-prs +1 -0
- package/tools/bin/agent-project-cleanup-session +49 -5
- package/tools/bin/agent-project-heartbeat-loop +119 -1471
- package/tools/bin/agent-project-reconcile-issue-session +66 -105
- package/tools/bin/agent-project-reconcile-pr-session +76 -111
- package/tools/bin/agent-project-run-claude-session +10 -0
- package/tools/bin/agent-project-run-codex-resilient +86 -9
- package/tools/bin/agent-project-run-codex-session +16 -5
- package/tools/bin/agent-project-run-kilo-session +10 -0
- package/tools/bin/agent-project-run-openclaw-session +10 -0
- package/tools/bin/agent-project-run-opencode-session +10 -0
- package/tools/bin/agent-project-worker-status +10 -7
- package/tools/bin/cleanup-worktree.sh +6 -1
- package/tools/bin/flow-config-lib.sh +80 -0
- package/tools/bin/flow-resident-worker-lib.sh +119 -1
- package/tools/bin/flow-shell-lib.sh +24 -0
- package/tools/bin/heartbeat-loop-cache-lib.sh +164 -0
- package/tools/bin/heartbeat-loop-counting-lib.sh +306 -0
- package/tools/bin/heartbeat-loop-pr-strategy-lib.sh +199 -0
- package/tools/bin/heartbeat-loop-scheduling-lib.sh +506 -0
- package/tools/bin/heartbeat-loop-worker-lib.sh +319 -0
- package/tools/bin/heartbeat-recovery-preflight.sh +12 -1
- package/tools/bin/heartbeat-safe-auto.sh +14 -3
- package/tools/bin/project-launchd-bootstrap.sh +11 -8
- package/tools/bin/reconcile-bootstrap-lib.sh +113 -0
- package/tools/bin/resident-issue-controller-lib.sh +448 -0
- package/tools/bin/resident-issue-queue-status.py +35 -0
- package/tools/bin/start-resident-issue-loop.sh +26 -437
- package/tools/dashboard/app.js +7 -0
- package/tools/dashboard/dashboard_snapshot.py +13 -29
- package/SKILL.md +0 -149
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# heartbeat-loop-worker-lib.sh — tmux session queries, worker enumeration, and cache helpers
|
|
3
|
+
|
|
4
|
+
all_tmux_sessions() {
|
|
5
|
+
ensure_tmux_sessions_cache
|
|
6
|
+
printf '%s\n' "$tmux_sessions_cache"
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
session_matches_prefix() {
|
|
10
|
+
local session="${1:?session required}"
|
|
11
|
+
[[ "$session" == "${issue_prefix}"* || "$session" == "${pr_prefix}"* ]]
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
session_runner_state() {
|
|
15
|
+
local session="${1:?session required}"
|
|
16
|
+
local runner_state_file="${runs_root}/${session}/runner.env"
|
|
17
|
+
if [[ ! -f "$runner_state_file" ]]; then
|
|
18
|
+
return 1
|
|
19
|
+
fi
|
|
20
|
+
awk -F= '/^RUNNER_STATE=/{print $2; exit}' "$runner_state_file"
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
session_is_auth_waiting() {
|
|
24
|
+
local session="${1:?session required}"
|
|
25
|
+
local runner_state=""
|
|
26
|
+
runner_state="$(session_runner_state "$session" || true)"
|
|
27
|
+
[[ "$runner_state" == "waiting-auth-refresh" || "$runner_state" == "switching-account" ]]
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
all_running_workers() {
|
|
31
|
+
ensure_all_running_workers_cache
|
|
32
|
+
printf '%s\n' "$all_running_workers_cache"
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
running_issue_workers() {
|
|
36
|
+
ensure_running_issue_workers_cache
|
|
37
|
+
printf '%s\n' "$running_issue_workers_cache"
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
running_pr_workers() {
|
|
41
|
+
ensure_running_pr_workers_cache
|
|
42
|
+
printf '%s\n' "$running_pr_workers_cache"
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
auth_wait_workers() {
|
|
46
|
+
ensure_auth_wait_workers_cache
|
|
47
|
+
printf '%s\n' "$auth_wait_workers_cache"
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
pending_launch_pid() {
|
|
51
|
+
local kind="${1:?kind required}"
|
|
52
|
+
local item_id="${2:?item id required}"
|
|
53
|
+
local pending_file pid
|
|
54
|
+
|
|
55
|
+
pending_file="${pending_launch_dir}/${kind}-${item_id}.pid"
|
|
56
|
+
if [[ ! -f "$pending_file" ]]; then
|
|
57
|
+
return 1
|
|
58
|
+
fi
|
|
59
|
+
|
|
60
|
+
pid="$(tr -d '[:space:]' <"$pending_file" 2>/dev/null || true)"
|
|
61
|
+
if [[ -z "$pid" ]]; then
|
|
62
|
+
rm -f "$pending_file"
|
|
63
|
+
return 1
|
|
64
|
+
fi
|
|
65
|
+
|
|
66
|
+
if kill -0 "$pid" 2>/dev/null; then
|
|
67
|
+
printf '%s\n' "$pid"
|
|
68
|
+
return 0
|
|
69
|
+
fi
|
|
70
|
+
|
|
71
|
+
rm -f "$pending_file"
|
|
72
|
+
return 1
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
pending_issue_launch_active() {
|
|
76
|
+
local issue_id="${1:?issue id required}"
|
|
77
|
+
if tmux has-session -t "${issue_prefix}${issue_id}" 2>/dev/null; then
|
|
78
|
+
rm -f "${pending_launch_dir}/issue-${issue_id}.pid" 2>/dev/null || true
|
|
79
|
+
return 1
|
|
80
|
+
fi
|
|
81
|
+
pending_launch_pid issue "$issue_id" >/dev/null
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
pending_pr_launch_active() {
|
|
85
|
+
local pr_id="${1:?pr id required}"
|
|
86
|
+
if tmux has-session -t "${pr_prefix}${pr_id}" 2>/dev/null; then
|
|
87
|
+
rm -f "${pending_launch_dir}/pr-${pr_id}.pid" 2>/dev/null || true
|
|
88
|
+
return 1
|
|
89
|
+
fi
|
|
90
|
+
pending_launch_pid pr "$pr_id" >/dev/null
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
pending_issue_launch_counts_toward_capacity() {
|
|
94
|
+
local issue_id="${1:?issue id required}"
|
|
95
|
+
local controller_state=""
|
|
96
|
+
|
|
97
|
+
if ! pending_issue_launch_active "${issue_id}"; then
|
|
98
|
+
return 1
|
|
99
|
+
fi
|
|
100
|
+
|
|
101
|
+
controller_state="$(resident_issue_controller_state "${issue_id}" || true)"
|
|
102
|
+
if [[ -n "${controller_state}" ]]; then
|
|
103
|
+
case "${controller_state}" in
|
|
104
|
+
idle|sleeping|waiting-due|waiting-open-pr|waiting-provider)
|
|
105
|
+
return 1
|
|
106
|
+
;;
|
|
107
|
+
esac
|
|
108
|
+
fi
|
|
109
|
+
|
|
110
|
+
return 0
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
resident_issue_controller_file() {
|
|
114
|
+
local issue_id="${1:?issue id required}"
|
|
115
|
+
printf '%s/resident-workers/issues/%s/controller.env\n' "${state_root}" "${issue_id}"
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
resident_issue_controller_state() {
|
|
119
|
+
local issue_id="${1:?issue id required}"
|
|
120
|
+
local controller_file state=""
|
|
121
|
+
|
|
122
|
+
controller_file="$(resident_issue_controller_file "$issue_id")"
|
|
123
|
+
[[ -f "${controller_file}" ]] || return 1
|
|
124
|
+
|
|
125
|
+
state="$(awk -F= '/^CONTROLLER_STATE=/{print $2; exit}' "${controller_file}" 2>/dev/null | tr -d '"' || true)"
|
|
126
|
+
[[ -n "${state}" ]] || return 1
|
|
127
|
+
printf '%s\n' "${state}"
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
issue_id_from_session() {
|
|
131
|
+
local session="${1:?session required}"
|
|
132
|
+
local issue_id=""
|
|
133
|
+
if [[ "$session" == "${issue_prefix}"* ]]; then
|
|
134
|
+
issue_id="${session#${issue_prefix}}"
|
|
135
|
+
fi
|
|
136
|
+
if [[ "$issue_id" =~ ^[0-9]+$ ]]; then
|
|
137
|
+
printf '%s\n' "$issue_id"
|
|
138
|
+
return 0
|
|
139
|
+
fi
|
|
140
|
+
return 1
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
pr_id_from_session() {
|
|
144
|
+
local session="${1:?session required}"
|
|
145
|
+
local pr_id=""
|
|
146
|
+
if [[ "$session" == "${pr_prefix}"* ]]; then
|
|
147
|
+
pr_id="${session#${pr_prefix}}"
|
|
148
|
+
fi
|
|
149
|
+
if [[ "$pr_id" =~ ^[0-9]+$ ]]; then
|
|
150
|
+
printf '%s\n' "$pr_id"
|
|
151
|
+
return 0
|
|
152
|
+
fi
|
|
153
|
+
return 1
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
worker_count() {
|
|
157
|
+
local workers="${1:-}"
|
|
158
|
+
if [[ -z "$workers" ]]; then
|
|
159
|
+
printf '0\n'
|
|
160
|
+
return
|
|
161
|
+
fi
|
|
162
|
+
printf '%s\n' "$workers" | sed '/^$/d' | wc -l | tr -d ' '
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
retry_ready() {
|
|
166
|
+
local kind="${1:?kind required}"
|
|
167
|
+
local item_id="${2:?item id required}"
|
|
168
|
+
local retry_out ready
|
|
169
|
+
|
|
170
|
+
retry_out="$(
|
|
171
|
+
"${shared_agent_home}/tools/bin/agent-project-retry-state" \
|
|
172
|
+
--state-root "$state_root" \
|
|
173
|
+
--kind "$kind" \
|
|
174
|
+
--item-id "$item_id" \
|
|
175
|
+
--action get
|
|
176
|
+
)"
|
|
177
|
+
ready="$(awk -F= '/^READY=/{print $2}' <<<"$retry_out")"
|
|
178
|
+
[[ "$ready" == "yes" ]]
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
provider_cooldown_state() {
|
|
182
|
+
"${shared_agent_home}/tools/bin/provider-cooldown-state.sh" get
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
completed_workers() {
|
|
186
|
+
ensure_completed_workers_cache
|
|
187
|
+
printf '%s\n' "$completed_workers_cache"
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
reconciled_marker_matches_run() {
|
|
191
|
+
local run_dir="${1:?run dir required}"
|
|
192
|
+
local marker_file="${run_dir}/reconciled.ok"
|
|
193
|
+
local run_env="${run_dir}/run.env"
|
|
194
|
+
local marker_started_at=""
|
|
195
|
+
local run_started_at=""
|
|
196
|
+
|
|
197
|
+
[[ -f "${marker_file}" && -f "${run_env}" ]] || return 1
|
|
198
|
+
|
|
199
|
+
marker_started_at="$(awk -F= '/^STARTED_AT=/{print $2}' "${marker_file}" 2>/dev/null | tr -d '"' | tail -n 1 || true)"
|
|
200
|
+
run_started_at="$(awk -F= '/^STARTED_AT=/{print $2}' "${run_env}" 2>/dev/null | tr -d '"' | tail -n 1 || true)"
|
|
201
|
+
|
|
202
|
+
[[ -n "${marker_started_at}" && -n "${run_started_at}" && "${marker_started_at}" == "${run_started_at}" ]]
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
ensure_tmux_sessions_cache() {
|
|
206
|
+
if [[ "$tmux_sessions_cache_loaded" != "yes" ]]; then
|
|
207
|
+
tmux_sessions_cache="$(tmux list-sessions -F '#S' 2>/dev/null || true)"
|
|
208
|
+
tmux_sessions_cache_loaded="yes"
|
|
209
|
+
fi
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
ensure_all_running_workers_cache() {
|
|
213
|
+
local session
|
|
214
|
+
if [[ "$all_running_workers_cache_loaded" == "yes" ]]; then
|
|
215
|
+
return 0
|
|
216
|
+
fi
|
|
217
|
+
ensure_tmux_sessions_cache
|
|
218
|
+
all_running_workers_cache=""
|
|
219
|
+
while IFS= read -r session; do
|
|
220
|
+
[[ -n "$session" ]] || continue
|
|
221
|
+
if session_matches_prefix "$session"; then
|
|
222
|
+
all_running_workers_cache+="${session}"$'\n'
|
|
223
|
+
fi
|
|
224
|
+
done <<<"$tmux_sessions_cache"
|
|
225
|
+
all_running_workers_cache="${all_running_workers_cache%$'\n'}"
|
|
226
|
+
all_running_workers_cache_loaded="yes"
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
ensure_auth_wait_workers_cache() {
|
|
230
|
+
local session
|
|
231
|
+
if [[ "$auth_wait_workers_cache_loaded" == "yes" ]]; then
|
|
232
|
+
return 0
|
|
233
|
+
fi
|
|
234
|
+
ensure_tmux_sessions_cache
|
|
235
|
+
auth_wait_workers_cache=""
|
|
236
|
+
while IFS= read -r session; do
|
|
237
|
+
[[ -n "$session" ]] || continue
|
|
238
|
+
session_matches_prefix "$session" || continue
|
|
239
|
+
if session_is_auth_waiting "$session"; then
|
|
240
|
+
auth_wait_workers_cache+="${session}"$'\n'
|
|
241
|
+
fi
|
|
242
|
+
done <<<"$tmux_sessions_cache"
|
|
243
|
+
auth_wait_workers_cache="${auth_wait_workers_cache%$'\n'}"
|
|
244
|
+
auth_wait_workers_cache_loaded="yes"
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
ensure_running_issue_workers_cache() {
|
|
248
|
+
local session
|
|
249
|
+
if [[ "$running_issue_workers_cache_loaded" == "yes" ]]; then
|
|
250
|
+
return 0
|
|
251
|
+
fi
|
|
252
|
+
ensure_tmux_sessions_cache
|
|
253
|
+
running_issue_workers_cache=""
|
|
254
|
+
while IFS= read -r session; do
|
|
255
|
+
[[ -n "$session" ]] || continue
|
|
256
|
+
if [[ "$session" == "${issue_prefix}"* ]]; then
|
|
257
|
+
if session_is_auth_waiting "$session"; then
|
|
258
|
+
continue
|
|
259
|
+
fi
|
|
260
|
+
running_issue_workers_cache+="${session}"$'\n'
|
|
261
|
+
fi
|
|
262
|
+
done <<<"$tmux_sessions_cache"
|
|
263
|
+
running_issue_workers_cache="${running_issue_workers_cache%$'\n'}"
|
|
264
|
+
running_issue_workers_cache_loaded="yes"
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
ensure_running_pr_workers_cache() {
|
|
268
|
+
local session
|
|
269
|
+
if [[ "$running_pr_workers_cache_loaded" == "yes" ]]; then
|
|
270
|
+
return 0
|
|
271
|
+
fi
|
|
272
|
+
ensure_tmux_sessions_cache
|
|
273
|
+
running_pr_workers_cache=""
|
|
274
|
+
while IFS= read -r session; do
|
|
275
|
+
[[ -n "$session" ]] || continue
|
|
276
|
+
if [[ "$session" == "${pr_prefix}"* ]]; then
|
|
277
|
+
if session_is_auth_waiting "$session"; then
|
|
278
|
+
continue
|
|
279
|
+
fi
|
|
280
|
+
running_pr_workers_cache+="${session}"$'\n'
|
|
281
|
+
fi
|
|
282
|
+
done <<<"$tmux_sessions_cache"
|
|
283
|
+
running_pr_workers_cache="${running_pr_workers_cache%$'\n'}"
|
|
284
|
+
running_pr_workers_cache_loaded="yes"
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
ensure_completed_workers_cache() {
|
|
288
|
+
local dir session issue_id status_line status
|
|
289
|
+
if [[ "$completed_workers_cache_loaded" == "yes" ]]; then
|
|
290
|
+
return 0
|
|
291
|
+
fi
|
|
292
|
+
completed_workers_cache=""
|
|
293
|
+
for dir in "$runs_root"/*; do
|
|
294
|
+
[[ -d "$dir" ]] || continue
|
|
295
|
+
session="${dir##*/}"
|
|
296
|
+
session_matches_prefix "$session" || continue
|
|
297
|
+
if reconciled_marker_matches_run "$dir"; then
|
|
298
|
+
continue
|
|
299
|
+
fi
|
|
300
|
+
if [[ "$session" == "${issue_prefix}"* ]]; then
|
|
301
|
+
issue_id="$(issue_id_from_session "$session" || true)"
|
|
302
|
+
if [[ -n "${issue_id}" ]] && pending_issue_launch_active "${issue_id}"; then
|
|
303
|
+
continue
|
|
304
|
+
fi
|
|
305
|
+
fi
|
|
306
|
+
status_line="$(
|
|
307
|
+
"${shared_agent_home}/tools/bin/agent-project-worker-status" \
|
|
308
|
+
--runs-root "$runs_root" \
|
|
309
|
+
--session "$session" \
|
|
310
|
+
| awk -F= '/^STATUS=/{print $2}' || true
|
|
311
|
+
)"
|
|
312
|
+
status="${status_line:-UNKNOWN}"
|
|
313
|
+
if [[ "$status" == "SUCCEEDED" || "$status" == "FAILED" ]]; then
|
|
314
|
+
completed_workers_cache+="${session}"$'\n'
|
|
315
|
+
fi
|
|
316
|
+
done
|
|
317
|
+
completed_workers_cache="${completed_workers_cache%$'\n'}"
|
|
318
|
+
completed_workers_cache_loaded="yes"
|
|
319
|
+
}
|
|
@@ -2,14 +2,25 @@
|
|
|
2
2
|
set -euo pipefail
|
|
3
3
|
|
|
4
4
|
FLOW_TOOLS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
5
|
+
FLOW_SHELL_LIB="${FLOW_TOOLS_DIR}/flow-shell-lib.sh"
|
|
5
6
|
TEST_DIR="${FLOW_TOOLS_DIR%/bin}/tests"
|
|
6
7
|
TEST_TIMEOUT_SECONDS="${F_LOSNING_HEARTBEAT_PREFLIGHT_TEST_TIMEOUT_SECONDS:-120}"
|
|
8
|
+
python_bin=""
|
|
9
|
+
|
|
10
|
+
# shellcheck source=/dev/null
|
|
11
|
+
source "${FLOW_SHELL_LIB}"
|
|
12
|
+
|
|
13
|
+
python_bin="$(flow_resolve_python_bin || true)"
|
|
14
|
+
if [[ -z "${python_bin}" || ! -x "${python_bin}" ]]; then
|
|
15
|
+
echo "unable to resolve a runnable python interpreter for heartbeat-recovery-preflight.sh" >&2
|
|
16
|
+
exit 1
|
|
17
|
+
fi
|
|
7
18
|
|
|
8
19
|
run_with_timeout() {
|
|
9
20
|
local timeout_seconds="${1:?timeout seconds required}"
|
|
10
21
|
shift
|
|
11
22
|
|
|
12
|
-
|
|
23
|
+
"${python_bin}" - "$timeout_seconds" "$@" <<'PY'
|
|
13
24
|
import os
|
|
14
25
|
import signal
|
|
15
26
|
import subprocess
|
|
@@ -38,6 +38,7 @@ CATCHUP_TIMEOUT_SECONDS="${ACP_CATCHUP_TIMEOUT_SECONDS:-${F_LOSNING_CATCHUP_TIME
|
|
|
38
38
|
HEARTBEAT_LOOP_TIMEOUT_SECONDS="${ACP_HEARTBEAT_LOOP_TIMEOUT_SECONDS:-${F_LOSNING_HEARTBEAT_LOOP_TIMEOUT_SECONDS:-720}}"
|
|
39
39
|
CONFIG_YAML="$(resolve_flow_config_yaml "${BASH_SOURCE[0]}")"
|
|
40
40
|
SHARED_AGENT_HOME="$(resolve_shared_agent_home "${FLOW_SKILL_DIR}")"
|
|
41
|
+
RUNTIME_HOME_DIR="$(resolve_runtime_home)"
|
|
41
42
|
FLOW_TOOLS_DIR="${FLOW_SKILL_DIR}/tools/bin"
|
|
42
43
|
ISSUE_SESSION_PREFIX="$(flow_resolve_issue_session_prefix "${CONFIG_YAML}")"
|
|
43
44
|
PR_SESSION_PREFIX="$(flow_resolve_pr_session_prefix "${CONFIG_YAML}")"
|
|
@@ -72,9 +73,15 @@ SHARED_LOOP_PID_FILE="${STATE_ROOT}/shared-heartbeat-loop.pid"
|
|
|
72
73
|
SHARED_LOOP_STATUS_FILE="${STATE_ROOT}/shared-heartbeat-loop.env"
|
|
73
74
|
QUOTA_LOCK_DIR="${STATE_ROOT}/quota-preflight.lock"
|
|
74
75
|
QUOTA_PID_FILE="${QUOTA_LOCK_DIR}/pid"
|
|
76
|
+
python_bin="$(flow_resolve_python_bin || true)"
|
|
75
77
|
|
|
76
78
|
mkdir -p "${AGENT_ROOT}" "${RUNS_ROOT}" "${STATE_ROOT}" "${HISTORY_ROOT}" "${WORKTREE_ROOT}" "${MEMORY_DIR}"
|
|
77
79
|
|
|
80
|
+
if [[ -z "${python_bin}" || ! -x "${python_bin}" ]]; then
|
|
81
|
+
echo "unable to resolve a runnable python interpreter for heartbeat-safe-auto.sh" >&2
|
|
82
|
+
exit 1
|
|
83
|
+
fi
|
|
84
|
+
|
|
78
85
|
acquire_lock() {
|
|
79
86
|
mkdir -p "${STATE_ROOT}"
|
|
80
87
|
|
|
@@ -156,7 +163,7 @@ run_with_timeout() {
|
|
|
156
163
|
local timeout_seconds="${1:?timeout seconds required}"
|
|
157
164
|
shift
|
|
158
165
|
|
|
159
|
-
|
|
166
|
+
"${python_bin}" - "${timeout_seconds}" "$@" <<'PY'
|
|
160
167
|
import os
|
|
161
168
|
from pathlib import Path
|
|
162
169
|
import signal
|
|
@@ -330,7 +337,7 @@ EFFECTIVE_QUOTA_POOLS=""
|
|
|
330
337
|
|
|
331
338
|
local quota_cache_age_seconds=""
|
|
332
339
|
quota_cache_age_seconds="$(
|
|
333
|
-
|
|
340
|
+
"${python_bin}" - "${CODEX_QUOTA_FULL_CACHE_FILE}" <<'PY' 2>/dev/null || true
|
|
334
341
|
import os
|
|
335
342
|
import sys
|
|
336
343
|
import time
|
|
@@ -506,7 +513,11 @@ run_codex_quota_preflight
|
|
|
506
513
|
# Sync skill files to runtime-home if source has changed since last sync.
|
|
507
514
|
# This ensures start-issue-worker.sh and other scripts are always up to date.
|
|
508
515
|
if [[ -x "${FLOW_TOOLS_DIR}/ensure-runtime-sync.sh" ]]; then
|
|
509
|
-
"${
|
|
516
|
+
if [[ "${FLOW_SKILL_DIR}" == "${RUNTIME_HOME_DIR}"/* ]]; then
|
|
517
|
+
printf 'RUNTIME_SYNC_SKIPPED=active-runtime-home\n'
|
|
518
|
+
else
|
|
519
|
+
"${FLOW_TOOLS_DIR}/ensure-runtime-sync.sh" --quiet 2>/dev/null || true
|
|
520
|
+
fi
|
|
510
521
|
fi
|
|
511
522
|
|
|
512
523
|
acquire_lock
|
|
@@ -4,16 +4,9 @@ set -euo pipefail
|
|
|
4
4
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
5
5
|
FLOW_SKILL_DIR="$(cd "${SCRIPT_DIR}/../.." && pwd)"
|
|
6
6
|
HOME_DIR="${ACP_PROJECT_RUNTIME_HOME_DIR:-${HOME:-}}"
|
|
7
|
-
SOURCE_HOME="${ACP_PROJECT_RUNTIME_SOURCE_HOME:-}"
|
|
8
|
-
RUNTIME_HOME="${ACP_PROJECT_RUNTIME_RUNTIME_HOME:-${HOME_DIR}/.agent-runtime/runtime-home}"
|
|
9
7
|
PROFILE_REGISTRY_ROOT="${ACP_PROJECT_RUNTIME_PROFILE_REGISTRY_ROOT:-${ACP_PROFILE_REGISTRY_ROOT:-${HOME_DIR}/.agent-runtime/control-plane/profiles}}"
|
|
10
8
|
PROFILE_ID="${ACP_PROJECT_RUNTIME_PROFILE_ID:-${ACP_PROJECT_ID:-${AGENT_PROJECT_ID:-}}}"
|
|
11
9
|
ENV_FILE="${ACP_PROJECT_RUNTIME_ENV_FILE:-${PROFILE_REGISTRY_ROOT}/${PROFILE_ID}/runtime.env}"
|
|
12
|
-
BASE_PATH="${ACP_PROJECT_RUNTIME_PATH:-/opt/homebrew/bin:/usr/bin:/bin:/usr/sbin:/sbin}"
|
|
13
|
-
SYNC_SCRIPT="${ACP_PROJECT_RUNTIME_SYNC_SCRIPT:-${FLOW_SKILL_DIR}/tools/bin/sync-shared-agent-home.sh}"
|
|
14
|
-
ENSURE_SYNC_SCRIPT="${ACP_PROJECT_RUNTIME_ENSURE_SYNC_SCRIPT:-${FLOW_SKILL_DIR}/tools/bin/ensure-runtime-sync.sh}"
|
|
15
|
-
RUNTIME_HEARTBEAT_SCRIPT="${ACP_PROJECT_RUNTIME_HEARTBEAT_SCRIPT:-${RUNTIME_HOME}/skills/openclaw/agent-control-plane/tools/bin/heartbeat-safe-auto.sh}"
|
|
16
|
-
ALWAYS_SYNC="${ACP_PROJECT_RUNTIME_ALWAYS_SYNC:-0}"
|
|
17
10
|
|
|
18
11
|
if [[ -z "${HOME_DIR}" ]]; then
|
|
19
12
|
echo "project launchd bootstrap requires HOME or ACP_PROJECT_RUNTIME_HOME_DIR" >&2
|
|
@@ -26,7 +19,6 @@ if [[ -z "${PROFILE_ID}" ]]; then
|
|
|
26
19
|
fi
|
|
27
20
|
|
|
28
21
|
export HOME="${HOME_DIR}"
|
|
29
|
-
export PATH="${BASE_PATH}"
|
|
30
22
|
export ACP_PROFILE_REGISTRY_ROOT="${PROFILE_REGISTRY_ROOT}"
|
|
31
23
|
export ACP_PROJECT_ID="${PROFILE_ID}"
|
|
32
24
|
export AGENT_PROJECT_ID="${PROFILE_ID}"
|
|
@@ -38,6 +30,17 @@ if [[ -f "${ENV_FILE}" ]]; then
|
|
|
38
30
|
set +a
|
|
39
31
|
fi
|
|
40
32
|
|
|
33
|
+
# Resolve launch paths after runtime.env overrides are loaded so launchd can
|
|
34
|
+
# pin the project runtime to a source checkout or alternate runtime home.
|
|
35
|
+
SOURCE_HOME="${ACP_PROJECT_RUNTIME_SOURCE_HOME:-}"
|
|
36
|
+
RUNTIME_HOME="${ACP_PROJECT_RUNTIME_RUNTIME_HOME:-${HOME_DIR}/.agent-runtime/runtime-home}"
|
|
37
|
+
BASE_PATH="${ACP_PROJECT_RUNTIME_PATH:-/opt/homebrew/bin:/usr/bin:/bin:/usr/sbin:/sbin}"
|
|
38
|
+
SYNC_SCRIPT="${ACP_PROJECT_RUNTIME_SYNC_SCRIPT:-${FLOW_SKILL_DIR}/tools/bin/sync-shared-agent-home.sh}"
|
|
39
|
+
ENSURE_SYNC_SCRIPT="${ACP_PROJECT_RUNTIME_ENSURE_SYNC_SCRIPT:-${FLOW_SKILL_DIR}/tools/bin/ensure-runtime-sync.sh}"
|
|
40
|
+
RUNTIME_HEARTBEAT_SCRIPT="${ACP_PROJECT_RUNTIME_HEARTBEAT_SCRIPT:-${RUNTIME_HOME}/skills/openclaw/agent-control-plane/tools/bin/heartbeat-safe-auto.sh}"
|
|
41
|
+
ALWAYS_SYNC="${ACP_PROJECT_RUNTIME_ALWAYS_SYNC:-0}"
|
|
42
|
+
export PATH="${BASE_PATH}"
|
|
43
|
+
|
|
41
44
|
if [[ ! -x "${ENSURE_SYNC_SCRIPT}" && ! -x "${SYNC_SCRIPT}" ]]; then
|
|
42
45
|
echo "project launchd bootstrap missing sync helper: ${ENSURE_SYNC_SCRIPT}" >&2
|
|
43
46
|
exit 65
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# reconcile-bootstrap-lib.sh — shared bootstrap helpers for reconcile scripts.
|
|
3
|
+
# Sourced by both agent-project-reconcile-pr-session and
|
|
4
|
+
# agent-project-reconcile-issue-session to avoid duplicating the bootstrap
|
|
5
|
+
# preamble.
|
|
6
|
+
|
|
7
|
+
bootstrap_flow_shell_lib() {
|
|
8
|
+
local candidate=""
|
|
9
|
+
local skill_name=""
|
|
10
|
+
|
|
11
|
+
for candidate in \
|
|
12
|
+
"${SCRIPT_DIR}/flow-shell-lib.sh" \
|
|
13
|
+
"${AGENT_CONTROL_PLANE_ROOT:-}/tools/bin/flow-shell-lib.sh" \
|
|
14
|
+
"${ACP_ROOT:-}/tools/bin/flow-shell-lib.sh" \
|
|
15
|
+
"${F_LOSNING_FLOW_ROOT:-}/tools/bin/flow-shell-lib.sh" \
|
|
16
|
+
"${AGENT_FLOW_SKILL_ROOT:-}/tools/bin/flow-shell-lib.sh" \
|
|
17
|
+
"${SHARED_AGENT_HOME:-}/tools/bin/flow-shell-lib.sh" \
|
|
18
|
+
"$(pwd)/tools/bin/flow-shell-lib.sh"; do
|
|
19
|
+
if [[ -n "${candidate}" && -f "${candidate}" ]]; then
|
|
20
|
+
printf '%s\n' "${candidate}"
|
|
21
|
+
return 0
|
|
22
|
+
fi
|
|
23
|
+
done
|
|
24
|
+
|
|
25
|
+
if [[ -n "${SHARED_AGENT_HOME:-}" ]]; then
|
|
26
|
+
for skill_name in "${AGENT_CONTROL_PLANE_SKILL_NAME:-agent-control-plane}" "${AGENT_CONTROL_PLANE_COMPAT_ALIAS:-}"; do
|
|
27
|
+
[[ -n "${skill_name}" ]] || continue
|
|
28
|
+
candidate="${SHARED_AGENT_HOME}/skills/openclaw/${skill_name}/tools/bin/flow-shell-lib.sh"
|
|
29
|
+
if [[ -f "${candidate}" ]]; then
|
|
30
|
+
printf '%s\n' "${candidate}"
|
|
31
|
+
return 0
|
|
32
|
+
fi
|
|
33
|
+
done
|
|
34
|
+
fi
|
|
35
|
+
|
|
36
|
+
echo "unable to locate flow-shell-lib.sh for reconcile bootstrap" >&2
|
|
37
|
+
return 1
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
FLOW_SHELL_LIB_PATH="$(bootstrap_flow_shell_lib)"
|
|
41
|
+
BOOTSTRAP_TOOLS_DIR="$(cd "$(dirname "${FLOW_SHELL_LIB_PATH}")" && pwd)"
|
|
42
|
+
# shellcheck source=/dev/null
|
|
43
|
+
source "${FLOW_SHELL_LIB_PATH}"
|
|
44
|
+
|
|
45
|
+
resolve_reconcile_tools_dir() {
|
|
46
|
+
local candidate_root=""
|
|
47
|
+
local skill_name=""
|
|
48
|
+
|
|
49
|
+
for candidate_root in \
|
|
50
|
+
"${AGENT_CONTROL_PLANE_ROOT:-}" \
|
|
51
|
+
"${ACP_ROOT:-}" \
|
|
52
|
+
"${F_LOSNING_FLOW_ROOT:-}" \
|
|
53
|
+
"${AGENT_FLOW_SKILL_ROOT:-}"; do
|
|
54
|
+
if [[ -n "${candidate_root}" && -d "${candidate_root}/tools/bin" ]]; then
|
|
55
|
+
printf '%s/tools/bin\n' "${candidate_root}"
|
|
56
|
+
return 0
|
|
57
|
+
fi
|
|
58
|
+
done
|
|
59
|
+
|
|
60
|
+
if [[ -n "${SHARED_AGENT_HOME:-}" ]]; then
|
|
61
|
+
if [[ -d "${SHARED_AGENT_HOME}/tools/bin" ]]; then
|
|
62
|
+
printf '%s/tools/bin\n' "${SHARED_AGENT_HOME}"
|
|
63
|
+
return 0
|
|
64
|
+
fi
|
|
65
|
+
for skill_name in "${AGENT_CONTROL_PLANE_SKILL_NAME:-agent-control-plane}" "${AGENT_CONTROL_PLANE_COMPAT_ALIAS:-}"; do
|
|
66
|
+
[[ -n "${skill_name}" ]] || continue
|
|
67
|
+
candidate_root="${SHARED_AGENT_HOME}/skills/openclaw/${skill_name}"
|
|
68
|
+
if [[ -d "${candidate_root}/tools/bin" ]]; then
|
|
69
|
+
printf '%s/tools/bin\n' "${candidate_root}"
|
|
70
|
+
return 0
|
|
71
|
+
fi
|
|
72
|
+
done
|
|
73
|
+
fi
|
|
74
|
+
|
|
75
|
+
if [[ -d "${SCRIPT_DIR}" ]]; then
|
|
76
|
+
printf '%s\n' "${SCRIPT_DIR}"
|
|
77
|
+
return 0
|
|
78
|
+
fi
|
|
79
|
+
|
|
80
|
+
printf '%s\n' "${BOOTSTRAP_TOOLS_DIR}"
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
shared_tools_dir="$(resolve_reconcile_tools_dir)"
|
|
84
|
+
resolve_reconcile_helper_path() {
|
|
85
|
+
local helper_name="${1:?helper name required}"
|
|
86
|
+
local candidate=""
|
|
87
|
+
|
|
88
|
+
for candidate in \
|
|
89
|
+
"${SCRIPT_DIR}/${helper_name}" \
|
|
90
|
+
"${BOOTSTRAP_TOOLS_DIR}/${helper_name}" \
|
|
91
|
+
"${shared_tools_dir}/${helper_name}"; do
|
|
92
|
+
if [[ -n "${candidate}" && -f "${candidate}" ]]; then
|
|
93
|
+
printf '%s\n' "${candidate}"
|
|
94
|
+
return 0
|
|
95
|
+
fi
|
|
96
|
+
done
|
|
97
|
+
|
|
98
|
+
echo "unable to locate ${helper_name} for reconcile bootstrap" >&2
|
|
99
|
+
return 1
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
FLOW_CONFIG_LIB_PATH="$(resolve_reconcile_helper_path "flow-config-lib.sh")"
|
|
103
|
+
# shellcheck source=/dev/null
|
|
104
|
+
source "${FLOW_CONFIG_LIB_PATH}"
|
|
105
|
+
|
|
106
|
+
require_transition() {
|
|
107
|
+
local step="${1:?step required}"
|
|
108
|
+
shift
|
|
109
|
+
if ! "$@"; then
|
|
110
|
+
echo "reconcile transition failed: ${step}" >&2
|
|
111
|
+
exit 1
|
|
112
|
+
fi
|
|
113
|
+
}
|