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.
Files changed (59) hide show
  1. package/README.md +69 -19
  2. package/assets/workflow-catalog.json +1 -1
  3. package/bin/pr-risk.sh +22 -7
  4. package/bin/sync-pr-labels.sh +1 -1
  5. package/hooks/heartbeat-hooks.sh +125 -12
  6. package/hooks/issue-reconcile-hooks.sh +1 -1
  7. package/hooks/pr-reconcile-hooks.sh +1 -1
  8. package/npm/bin/agent-control-plane.js +296 -61
  9. package/package.json +11 -7
  10. package/tools/bin/agent-github-update-labels +36 -2
  11. package/tools/bin/agent-project-catch-up-merged-prs +4 -2
  12. package/tools/bin/agent-project-cleanup-session +49 -5
  13. package/tools/bin/agent-project-heartbeat-loop +119 -1471
  14. package/tools/bin/agent-project-publish-issue-pr +6 -3
  15. package/tools/bin/agent-project-reconcile-issue-session +78 -106
  16. package/tools/bin/agent-project-reconcile-pr-session +166 -143
  17. package/tools/bin/agent-project-retry-state +18 -7
  18. package/tools/bin/agent-project-run-claude-session +10 -0
  19. package/tools/bin/agent-project-run-codex-resilient +99 -14
  20. package/tools/bin/agent-project-run-codex-session +16 -5
  21. package/tools/bin/agent-project-run-kilo-session +10 -0
  22. package/tools/bin/agent-project-run-openclaw-session +10 -0
  23. package/tools/bin/agent-project-run-opencode-session +10 -0
  24. package/tools/bin/agent-project-sync-source-repo-main +163 -0
  25. package/tools/bin/agent-project-worker-status +10 -7
  26. package/tools/bin/cleanup-worktree.sh +6 -1
  27. package/tools/bin/flow-config-lib.sh +1257 -34
  28. package/tools/bin/flow-resident-worker-lib.sh +119 -1
  29. package/tools/bin/flow-shell-lib.sh +56 -0
  30. package/tools/bin/github-core-rate-limit-state.sh +77 -0
  31. package/tools/bin/github-write-outbox.sh +470 -0
  32. package/tools/bin/heartbeat-loop-cache-lib.sh +164 -0
  33. package/tools/bin/heartbeat-loop-counting-lib.sh +306 -0
  34. package/tools/bin/heartbeat-loop-pr-strategy-lib.sh +199 -0
  35. package/tools/bin/heartbeat-loop-scheduling-lib.sh +506 -0
  36. package/tools/bin/heartbeat-loop-worker-lib.sh +319 -0
  37. package/tools/bin/heartbeat-recovery-preflight.sh +12 -1
  38. package/tools/bin/heartbeat-safe-auto.sh +56 -3
  39. package/tools/bin/install-project-launchd.sh +17 -2
  40. package/tools/bin/project-init.sh +21 -1
  41. package/tools/bin/project-launchd-bootstrap.sh +16 -9
  42. package/tools/bin/project-runtimectl.sh +46 -2
  43. package/tools/bin/reconcile-bootstrap-lib.sh +113 -0
  44. package/tools/bin/resident-issue-controller-lib.sh +448 -0
  45. package/tools/bin/scaffold-profile.sh +61 -3
  46. package/tools/bin/start-pr-fix-worker.sh +47 -10
  47. package/tools/bin/start-resident-issue-loop.sh +28 -439
  48. package/tools/dashboard/app.js +37 -1
  49. package/tools/dashboard/dashboard_snapshot.py +65 -26
  50. package/tools/templates/pr-fix-template.md +3 -1
  51. package/tools/templates/pr-merge-repair-template.md +2 -1
  52. package/SKILL.md +0 -149
  53. package/references/architecture.md +0 -217
  54. package/references/commands.md +0 -128
  55. package/references/control-plane-map.md +0 -124
  56. package/references/docs-map.md +0 -73
  57. package/references/release-checklist.md +0 -65
  58. package/references/repo-map.md +0 -36
  59. 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
- /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
@@ -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
- BOOTSTRAP_SCRIPT="${ACP_PROJECT_RUNTIME_BOOTSTRAP_SCRIPT:-${FLOW_SKILL_DIR}/tools/bin/project-launchd-bootstrap.sh}"
133
- SUPERVISOR_SCRIPT="${ACP_PROJECT_RUNTIME_SUPERVISOR_SCRIPT:-${FLOW_SKILL_DIR}/tools/bin/project-runtime-supervisor.sh}"
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> GitHub repo slug
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
- bash "${ENSURE_SYNC_SCRIPT}" "${ensure_args[@]}"
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}"