agent-control-plane 0.2.0 → 0.4.9
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 +69 -19
- package/assets/workflow-catalog.json +1 -1
- package/bin/pr-risk.sh +22 -7
- package/bin/sync-pr-labels.sh +1 -1
- package/hooks/heartbeat-hooks.sh +125 -12
- package/hooks/issue-reconcile-hooks.sh +1 -1
- package/hooks/pr-reconcile-hooks.sh +1 -1
- package/npm/bin/agent-control-plane.js +296 -61
- package/package.json +11 -7
- package/tools/bin/agent-github-update-labels +36 -2
- package/tools/bin/agent-project-catch-up-merged-prs +4 -2
- package/tools/bin/agent-project-cleanup-session +49 -5
- package/tools/bin/agent-project-heartbeat-loop +119 -1471
- package/tools/bin/agent-project-publish-issue-pr +6 -3
- package/tools/bin/agent-project-reconcile-issue-session +78 -106
- package/tools/bin/agent-project-reconcile-pr-session +166 -143
- package/tools/bin/agent-project-retry-state +18 -7
- package/tools/bin/agent-project-run-claude-session +10 -0
- package/tools/bin/agent-project-run-codex-resilient +99 -14
- 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-sync-source-repo-main +163 -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 +1257 -34
- package/tools/bin/flow-resident-worker-lib.sh +119 -1
- package/tools/bin/flow-shell-lib.sh +56 -0
- package/tools/bin/github-core-rate-limit-state.sh +77 -0
- package/tools/bin/github-write-outbox.sh +470 -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 +56 -3
- package/tools/bin/install-project-launchd.sh +17 -2
- package/tools/bin/project-init.sh +21 -1
- package/tools/bin/project-launchd-bootstrap.sh +16 -9
- package/tools/bin/project-runtimectl.sh +46 -2
- package/tools/bin/reconcile-bootstrap-lib.sh +113 -0
- package/tools/bin/resident-issue-controller-lib.sh +448 -0
- package/tools/bin/scaffold-profile.sh +61 -3
- package/tools/bin/start-pr-fix-worker.sh +47 -10
- package/tools/bin/start-resident-issue-loop.sh +28 -439
- package/tools/dashboard/app.js +37 -1
- package/tools/dashboard/dashboard_snapshot.py +65 -26
- package/tools/templates/pr-fix-template.md +3 -1
- package/tools/templates/pr-merge-repair-template.md +2 -1
- package/SKILL.md +0 -149
- package/references/architecture.md +0 -217
- package/references/commands.md +0 -128
- package/references/control-plane-map.md +0 -124
- package/references/docs-map.md +0 -73
- package/references/release-checklist.md +0 -65
- package/references/repo-map.md +0 -36
- package/tools/bin/split-retained-slice.sh +0 -124
|
@@ -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
|
|
@@ -604,12 +615,35 @@ else
|
|
|
604
615
|
exit "${loop_status}"
|
|
605
616
|
fi
|
|
606
617
|
|
|
618
|
+
# ── Flush local GitHub write outbox ────────────────────────────────────────────
|
|
619
|
+
GITHUB_OUTBOX_FLUSH_LIMIT="${ACP_GITHUB_OUTBOX_FLUSH_LIMIT:-${F_LOSNING_GITHUB_OUTBOX_FLUSH_LIMIT:-25}}"
|
|
620
|
+
GITHUB_OUTBOX_FLUSH_TIMEOUT_SECONDS="${ACP_GITHUB_OUTBOX_FLUSH_TIMEOUT_SECONDS:-${F_LOSNING_GITHUB_OUTBOX_FLUSH_TIMEOUT_SECONDS:-30}}"
|
|
621
|
+
if [[ -x "${FLOW_TOOLS_DIR}/github-write-outbox.sh" ]]; then
|
|
622
|
+
printf '[%s] github-outbox flush start\n' "$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
|
|
623
|
+
if run_with_timeout "${GITHUB_OUTBOX_FLUSH_TIMEOUT_SECONDS}" \
|
|
624
|
+
env \
|
|
625
|
+
ACP_STATE_ROOT="$STATE_ROOT" \
|
|
626
|
+
F_LOSNING_STATE_ROOT="$STATE_ROOT" \
|
|
627
|
+
ACP_RUNS_ROOT="$RUNS_ROOT" \
|
|
628
|
+
F_LOSNING_RUNS_ROOT="$RUNS_ROOT" \
|
|
629
|
+
bash "${FLOW_TOOLS_DIR}/github-write-outbox.sh" flush --limit "${GITHUB_OUTBOX_FLUSH_LIMIT}"; then
|
|
630
|
+
printf '[%s] github-outbox flush end status=0\n' "$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
|
|
631
|
+
else
|
|
632
|
+
github_outbox_status=$?
|
|
633
|
+
if [[ "${github_outbox_status}" -eq 124 ]]; then
|
|
634
|
+
printf 'GITHUB_OUTBOX_FLUSH_TIMEOUT=yes\n'
|
|
635
|
+
fi
|
|
636
|
+
printf '[%s] github-outbox flush end status=%s\n' "$(date -u +"%Y-%m-%dT%H:%M:%SZ")" "${github_outbox_status}"
|
|
637
|
+
fi
|
|
638
|
+
fi
|
|
639
|
+
|
|
607
640
|
# ── Throttled catch-up passes ──────────────────────────────────────────────────
|
|
608
641
|
# These scripts fetch merged/closed PRs and linked issues which change rarely.
|
|
609
642
|
# Run them at most once every CATCHUP_INTERVAL_SECONDS (default 300 = 5 min)
|
|
610
643
|
# to avoid burning API quota on every heartbeat cycle.
|
|
611
644
|
CATCHUP_INTERVAL_SECONDS="${ACP_CATCHUP_INTERVAL_SECONDS:-${F_LOSNING_CATCHUP_INTERVAL_SECONDS:-300}}"
|
|
612
645
|
CATCHUP_STAMP_FILE="${STATE_ROOT}/last-catchup-timestamp"
|
|
646
|
+
SOURCE_REPO_SYNC_TIMEOUT_SECONDS="${ACP_SOURCE_REPO_SYNC_TIMEOUT_SECONDS:-${F_LOSNING_SOURCE_REPO_SYNC_TIMEOUT_SECONDS:-45}}"
|
|
613
647
|
_catchup_now="$(date +%s)"
|
|
614
648
|
_catchup_last="0"
|
|
615
649
|
if [[ -f "${CATCHUP_STAMP_FILE}" ]]; then
|
|
@@ -637,6 +671,25 @@ if [[ "${_catchup_age}" -ge "${CATCHUP_INTERVAL_SECONDS}" ]]; then
|
|
|
637
671
|
printf '[%s] merged-pr catchup end status=%s\n' "$(date -u +"%Y-%m-%dT%H:%M:%SZ")" "${catchup_status}"
|
|
638
672
|
fi
|
|
639
673
|
|
|
674
|
+
if [[ -x "${FLOW_TOOLS_DIR}/agent-project-sync-source-repo-main" ]]; then
|
|
675
|
+
printf '[%s] source-repo main sync start\n' "$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
|
|
676
|
+
if run_with_timeout "${SOURCE_REPO_SYNC_TIMEOUT_SECONDS}" \
|
|
677
|
+
env \
|
|
678
|
+
ACP_RUNS_ROOT="$RUNS_ROOT" \
|
|
679
|
+
F_LOSNING_RUNS_ROOT="$RUNS_ROOT" \
|
|
680
|
+
ACP_STATE_ROOT="$STATE_ROOT" \
|
|
681
|
+
F_LOSNING_STATE_ROOT="$STATE_ROOT" \
|
|
682
|
+
bash "${FLOW_TOOLS_DIR}/agent-project-sync-source-repo-main"; then
|
|
683
|
+
printf '[%s] source-repo main sync end status=0\n' "$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
|
|
684
|
+
else
|
|
685
|
+
source_repo_sync_status=$?
|
|
686
|
+
if [[ "${source_repo_sync_status}" -eq 124 ]]; then
|
|
687
|
+
printf 'SOURCE_REPO_SYNC_TIMEOUT=yes\n'
|
|
688
|
+
fi
|
|
689
|
+
printf '[%s] source-repo main sync end status=%s\n' "$(date -u +"%Y-%m-%dT%H:%M:%SZ")" "${source_repo_sync_status}"
|
|
690
|
+
fi
|
|
691
|
+
fi
|
|
692
|
+
|
|
640
693
|
printf '[%s] linked-pr issue catchup start\n' "$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
|
|
641
694
|
if run_with_timeout "${CATCHUP_TIMEOUT_SECONDS}" \
|
|
642
695
|
env \
|
|
@@ -129,8 +129,23 @@ LABEL="${label_override:-${ACP_PROJECT_RUNTIME_LAUNCHD_LABEL:-ai.agent.project.$
|
|
|
129
129
|
BASE_PATH="$(build_launchd_base_path)"
|
|
130
130
|
CODING_WORKER_OVERRIDE="${ACP_PROJECT_RUNTIME_CODING_WORKER:-${ACP_CODING_WORKER:-}}"
|
|
131
131
|
SYNC_SCRIPT="${ACP_PROJECT_RUNTIME_SYNC_SCRIPT:-${FLOW_SKILL_DIR}/tools/bin/sync-shared-agent-home.sh}"
|
|
132
|
-
|
|
133
|
-
|
|
132
|
+
RUNTIME_SKILL_DIR="${RUNTIME_HOME}/skills/openclaw/agent-control-plane"
|
|
133
|
+
BOOTSTRAP_SCRIPT="${ACP_PROJECT_RUNTIME_BOOTSTRAP_SCRIPT:-}"
|
|
134
|
+
if [[ -z "${BOOTSTRAP_SCRIPT}" ]]; then
|
|
135
|
+
if [[ -x "${RUNTIME_SKILL_DIR}/tools/bin/project-launchd-bootstrap.sh" ]]; then
|
|
136
|
+
BOOTSTRAP_SCRIPT="${RUNTIME_SKILL_DIR}/tools/bin/project-launchd-bootstrap.sh"
|
|
137
|
+
else
|
|
138
|
+
BOOTSTRAP_SCRIPT="${FLOW_SKILL_DIR}/tools/bin/project-launchd-bootstrap.sh"
|
|
139
|
+
fi
|
|
140
|
+
fi
|
|
141
|
+
SUPERVISOR_SCRIPT="${ACP_PROJECT_RUNTIME_SUPERVISOR_SCRIPT:-}"
|
|
142
|
+
if [[ -z "${SUPERVISOR_SCRIPT}" ]]; then
|
|
143
|
+
if [[ -x "${RUNTIME_SKILL_DIR}/tools/bin/project-runtime-supervisor.sh" ]]; then
|
|
144
|
+
SUPERVISOR_SCRIPT="${RUNTIME_SKILL_DIR}/tools/bin/project-runtime-supervisor.sh"
|
|
145
|
+
else
|
|
146
|
+
SUPERVISOR_SCRIPT="${FLOW_SKILL_DIR}/tools/bin/project-runtime-supervisor.sh"
|
|
147
|
+
fi
|
|
148
|
+
fi
|
|
134
149
|
STATE_ROOT="$(flow_resolve_state_root "${CONFIG_YAML}")"
|
|
135
150
|
SUPERVISOR_PID_FILE="${STATE_ROOT}/runtime-supervisor.pid"
|
|
136
151
|
ENV_FILE="${ACP_PROJECT_RUNTIME_ENV_FILE:-${PROFILE_REGISTRY_ROOT}/${PROFILE_ID}/runtime.env}"
|
|
@@ -34,7 +34,12 @@ runtime copy.
|
|
|
34
34
|
|
|
35
35
|
Common options:
|
|
36
36
|
--profile-id <id> Profile id, e.g. billing-api
|
|
37
|
-
--repo-slug <owner/repo>
|
|
37
|
+
--repo-slug <owner/repo> Forge repo slug
|
|
38
|
+
--forge-provider <github|gitea> Forge provider for this profile
|
|
39
|
+
--gitea-base-url <url> Base URL for a local/self-hosted Gitea instance
|
|
40
|
+
--gitea-token <token> Gitea API token written to profile runtime.env
|
|
41
|
+
--gitea-username <user> Gitea username written to profile runtime.env
|
|
42
|
+
--gitea-password <pass> Gitea password written to profile runtime.env
|
|
38
43
|
--profile-home <path> Installed profile registry root
|
|
39
44
|
--repo-root <path> Canonical repo root
|
|
40
45
|
--agent-repo-root <path> Agent-owned anchor repo root
|
|
@@ -68,6 +73,11 @@ EOF
|
|
|
68
73
|
|
|
69
74
|
profile_id=""
|
|
70
75
|
repo_slug=""
|
|
76
|
+
forge_provider=""
|
|
77
|
+
gitea_base_url=""
|
|
78
|
+
gitea_token=""
|
|
79
|
+
gitea_username=""
|
|
80
|
+
gitea_password=""
|
|
71
81
|
profile_home=""
|
|
72
82
|
repo_root=""
|
|
73
83
|
agent_repo_root=""
|
|
@@ -98,6 +108,11 @@ while [[ $# -gt 0 ]]; do
|
|
|
98
108
|
case "$1" in
|
|
99
109
|
--profile-id) profile_id="${2:-}"; shift 2 ;;
|
|
100
110
|
--repo-slug) repo_slug="${2:-}"; shift 2 ;;
|
|
111
|
+
--forge-provider) forge_provider="${2:-}"; shift 2 ;;
|
|
112
|
+
--gitea-base-url) gitea_base_url="${2:-}"; shift 2 ;;
|
|
113
|
+
--gitea-token) gitea_token="${2:-}"; shift 2 ;;
|
|
114
|
+
--gitea-username) gitea_username="${2:-}"; shift 2 ;;
|
|
115
|
+
--gitea-password) gitea_password="${2:-}"; shift 2 ;;
|
|
101
116
|
--profile-home) profile_home="${2:-}"; shift 2 ;;
|
|
102
117
|
--repo-root) repo_root="${2:-}"; shift 2 ;;
|
|
103
118
|
--agent-repo-root) agent_repo_root="${2:-}"; shift 2 ;;
|
|
@@ -144,6 +159,11 @@ SOURCE_HOME="${source_home:-${ACP_PROJECT_INIT_SOURCE_HOME:-$(cd "${FLOW_SKILL_D
|
|
|
144
159
|
RUNTIME_HOME="${runtime_home:-${ACP_PROJECT_INIT_RUNTIME_HOME:-${HOME}/.agent-runtime/runtime-home}}"
|
|
145
160
|
|
|
146
161
|
scaffold_cmd=(bash "${SCAFFOLD_SCRIPT}" --profile-id "${profile_id}" --repo-slug "${repo_slug}")
|
|
162
|
+
[[ -n "${forge_provider}" ]] && scaffold_cmd+=(--forge-provider "${forge_provider}")
|
|
163
|
+
[[ -n "${gitea_base_url}" ]] && scaffold_cmd+=(--gitea-base-url "${gitea_base_url}")
|
|
164
|
+
[[ -n "${gitea_token}" ]] && scaffold_cmd+=(--gitea-token "${gitea_token}")
|
|
165
|
+
[[ -n "${gitea_username}" ]] && scaffold_cmd+=(--gitea-username "${gitea_username}")
|
|
166
|
+
[[ -n "${gitea_password}" ]] && scaffold_cmd+=(--gitea-password "${gitea_password}")
|
|
147
167
|
[[ -n "${profile_home}" ]] && scaffold_cmd+=(--profile-home "${profile_home}")
|
|
148
168
|
[[ -n "${repo_root}" ]] && scaffold_cmd+=(--repo-root "${repo_root}")
|
|
149
169
|
[[ -n "${agent_repo_root}" ]] && scaffold_cmd+=(--agent-repo-root "${agent_repo_root}")
|
|
@@ -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
|
|
@@ -51,7 +54,11 @@ if [[ -x "${ENSURE_SYNC_SCRIPT}" ]]; then
|
|
|
51
54
|
if [[ "${ALWAYS_SYNC}" == "1" ]]; then
|
|
52
55
|
ensure_args=(--force "${ensure_args[@]}")
|
|
53
56
|
fi
|
|
54
|
-
|
|
57
|
+
if [[ "${FLOW_SKILL_DIR}" == "${RUNTIME_HOME}"/* ]]; then
|
|
58
|
+
printf 'RUNTIME_SYNC_SKIPPED=active-runtime-home\n'
|
|
59
|
+
else
|
|
60
|
+
bash "${ENSURE_SYNC_SCRIPT}" "${ensure_args[@]}"
|
|
61
|
+
fi
|
|
55
62
|
elif [[ "${ALWAYS_SYNC}" == "1" || ! -x "${RUNTIME_HEARTBEAT_SCRIPT}" ]]; then
|
|
56
63
|
if [[ -z "${SOURCE_HOME}" ]]; then
|
|
57
64
|
SOURCE_HOME="${FLOW_SKILL_DIR}"
|