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.
Files changed (33) hide show
  1. package/npm/bin/agent-control-plane.js +39 -2
  2. package/package.json +6 -3
  3. package/tools/bin/agent-project-catch-up-merged-prs +1 -0
  4. package/tools/bin/agent-project-cleanup-session +49 -5
  5. package/tools/bin/agent-project-heartbeat-loop +119 -1471
  6. package/tools/bin/agent-project-reconcile-issue-session +66 -105
  7. package/tools/bin/agent-project-reconcile-pr-session +76 -111
  8. package/tools/bin/agent-project-run-claude-session +10 -0
  9. package/tools/bin/agent-project-run-codex-resilient +86 -9
  10. package/tools/bin/agent-project-run-codex-session +16 -5
  11. package/tools/bin/agent-project-run-kilo-session +10 -0
  12. package/tools/bin/agent-project-run-openclaw-session +10 -0
  13. package/tools/bin/agent-project-run-opencode-session +10 -0
  14. package/tools/bin/agent-project-worker-status +10 -7
  15. package/tools/bin/cleanup-worktree.sh +6 -1
  16. package/tools/bin/flow-config-lib.sh +80 -0
  17. package/tools/bin/flow-resident-worker-lib.sh +119 -1
  18. package/tools/bin/flow-shell-lib.sh +24 -0
  19. package/tools/bin/heartbeat-loop-cache-lib.sh +164 -0
  20. package/tools/bin/heartbeat-loop-counting-lib.sh +306 -0
  21. package/tools/bin/heartbeat-loop-pr-strategy-lib.sh +199 -0
  22. package/tools/bin/heartbeat-loop-scheduling-lib.sh +506 -0
  23. package/tools/bin/heartbeat-loop-worker-lib.sh +319 -0
  24. package/tools/bin/heartbeat-recovery-preflight.sh +12 -1
  25. package/tools/bin/heartbeat-safe-auto.sh +14 -3
  26. package/tools/bin/project-launchd-bootstrap.sh +11 -8
  27. package/tools/bin/reconcile-bootstrap-lib.sh +113 -0
  28. package/tools/bin/resident-issue-controller-lib.sh +448 -0
  29. package/tools/bin/resident-issue-queue-status.py +35 -0
  30. package/tools/bin/start-resident-issue-loop.sh +26 -437
  31. package/tools/dashboard/app.js +7 -0
  32. package/tools/dashboard/dashboard_snapshot.py +13 -29
  33. 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
- /opt/homebrew/bin/python3 - "$timeout_seconds" "$@" <<'PY'
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
- /opt/homebrew/bin/python3 - "${timeout_seconds}" "$@" <<'PY'
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
- /opt/homebrew/bin/python3 - "${CODEX_QUOTA_FULL_CACHE_FILE}" <<'PY' 2>/dev/null || true
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
- "${FLOW_TOOLS_DIR}/ensure-runtime-sync.sh" --quiet 2>/dev/null || true
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
+ }