agent-control-plane 0.1.16 → 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 (63) hide show
  1. package/README.md +93 -14
  2. package/bin/pr-risk.sh +28 -6
  3. package/hooks/heartbeat-hooks.sh +62 -22
  4. package/npm/bin/agent-control-plane.js +360 -10
  5. package/package.json +6 -3
  6. package/references/architecture.md +8 -0
  7. package/references/control-plane-map.md +6 -2
  8. package/references/release-checklist.md +0 -2
  9. package/tools/bin/agent-github-update-labels +6 -1
  10. package/tools/bin/agent-project-catch-up-issue-pr-links +118 -0
  11. package/tools/bin/agent-project-catch-up-merged-prs +78 -21
  12. package/tools/bin/agent-project-catch-up-scheduled-issue-retries +123 -0
  13. package/tools/bin/agent-project-cleanup-session +132 -4
  14. package/tools/bin/agent-project-heartbeat-loop +116 -1461
  15. package/tools/bin/agent-project-reconcile-issue-session +90 -117
  16. package/tools/bin/agent-project-reconcile-pr-session +76 -111
  17. package/tools/bin/agent-project-run-claude-session +12 -2
  18. package/tools/bin/agent-project-run-codex-resilient +86 -9
  19. package/tools/bin/agent-project-run-codex-session +16 -5
  20. package/tools/bin/agent-project-run-kilo-session +356 -14
  21. package/tools/bin/agent-project-run-ollama-session +658 -0
  22. package/tools/bin/agent-project-run-openclaw-session +37 -25
  23. package/tools/bin/agent-project-run-opencode-session +364 -14
  24. package/tools/bin/agent-project-run-pi-session +479 -0
  25. package/tools/bin/agent-project-worker-status +11 -8
  26. package/tools/bin/cleanup-worktree.sh +6 -1
  27. package/tools/bin/flow-config-lib.sh +196 -3
  28. package/tools/bin/flow-resident-worker-lib.sh +120 -2
  29. package/tools/bin/flow-shell-lib.sh +29 -2
  30. package/tools/bin/heartbeat-loop-cache-lib.sh +164 -0
  31. package/tools/bin/heartbeat-loop-counting-lib.sh +306 -0
  32. package/tools/bin/heartbeat-loop-pr-strategy-lib.sh +199 -0
  33. package/tools/bin/heartbeat-loop-scheduling-lib.sh +506 -0
  34. package/tools/bin/heartbeat-loop-worker-lib.sh +319 -0
  35. package/tools/bin/heartbeat-recovery-preflight.sh +13 -1
  36. package/tools/bin/heartbeat-safe-auto.sh +119 -20
  37. package/tools/bin/install-project-launchd.sh +19 -2
  38. package/tools/bin/prepare-worktree.sh +4 -4
  39. package/tools/bin/profile-activate.sh +2 -2
  40. package/tools/bin/profile-adopt.sh +2 -2
  41. package/tools/bin/project-init.sh +1 -1
  42. package/tools/bin/project-launchd-bootstrap.sh +11 -8
  43. package/tools/bin/project-runtimectl.sh +90 -7
  44. package/tools/bin/provider-cooldown-state.sh +14 -14
  45. package/tools/bin/reconcile-bootstrap-lib.sh +113 -0
  46. package/tools/bin/render-flow-config.sh +30 -33
  47. package/tools/bin/resident-issue-controller-lib.sh +448 -0
  48. package/tools/bin/resident-issue-queue-status.py +35 -0
  49. package/tools/bin/run-codex-task.sh +53 -4
  50. package/tools/bin/scaffold-profile.sh +18 -3
  51. package/tools/bin/start-issue-worker.sh +1 -1
  52. package/tools/bin/start-pr-fix-worker.sh +30 -0
  53. package/tools/bin/start-pr-review-worker.sh +31 -0
  54. package/tools/bin/start-resident-issue-loop.sh +27 -438
  55. package/tools/bin/sync-agent-repo.sh +2 -2
  56. package/tools/bin/sync-dependency-baseline.sh +3 -3
  57. package/tools/bin/sync-shared-agent-home.sh +4 -1
  58. package/tools/dashboard/app.js +7 -0
  59. package/tools/dashboard/dashboard_snapshot.py +13 -29
  60. package/tools/templates/pr-fix-template.md +3 -7
  61. package/tools/templates/pr-merge-repair-template.md +3 -7
  62. package/tools/templates/pr-review-template.md +2 -1
  63. package/SKILL.md +0 -149
@@ -133,6 +133,34 @@ PR_LINKED_ISSUE_ID="$(jq -r '.linkedIssueId // ""' <<<"$RISK_JSON")"
133
133
  PR_FILES_TEXT="$(jq -r '.files[] | "- " + .' <<<"$RISK_JSON")"
134
134
  PR_REPO_ROOT="$(flow_resolve_repo_root "${CONFIG_YAML}")"
135
135
  PR_DEPENDENCY_SOURCE_ROOT="${ACP_DEPENDENCY_SOURCE_ROOT:-${F_LOSNING_DEPENDENCY_SOURCE_ROOT:-$PR_REPO_ROOT}}"
136
+ render_pr_context_reads_text() {
137
+ local repo_root="${1:?repo root required}"
138
+ local -a candidate_paths=(
139
+ "${repo_root}/AGENTS.md"
140
+ "${repo_root}/openspec/AGENT_RULES.md"
141
+ "${repo_root}/openspec/AGENTS.md"
142
+ "${repo_root}/openspec/project.md"
143
+ "${repo_root}/openspec/CONVENTIONS.md"
144
+ "${repo_root}/docs/TESTING_AND_SEED_POLICY.md"
145
+ )
146
+ local -a existing_paths=()
147
+ local candidate_path=""
148
+
149
+ for candidate_path in "${candidate_paths[@]}"; do
150
+ if [[ -f "${candidate_path}" ]]; then
151
+ existing_paths+=("${candidate_path}")
152
+ fi
153
+ done
154
+
155
+ if [[ "${#existing_paths[@]}" -eq 0 ]]; then
156
+ printf '%s\n' '- No repo-specific context files were found under the expected AGENTS/OpenSpec/testing-doc locations; rely on the current diff and nearby source.'
157
+ return 0
158
+ fi
159
+
160
+ printf '%s\n' "${existing_paths[@]}" | sed 's/^/- `/' | sed 's/$/`/'
161
+ }
162
+
163
+ PR_CONTEXT_READS_TEXT="$(render_pr_context_reads_text "${PR_REPO_ROOT}")"
136
164
  PR_CHECK_FAILURES_TEXT="$(jq -r '(.checkFailures + .pendingChecks)[]? | "- " + .' <<<"$RISK_JSON")"
137
165
  if [[ -z "$PR_CHECK_FAILURES_TEXT" ]]; then
138
166
  PR_CHECK_FAILURES_TEXT="- none reported"
@@ -293,6 +321,7 @@ PR_MERGE_STATE_STATUS="$PR_MERGE_STATE_STATUS" \
293
321
  PR_MERGEABLE_STATUS="$PR_MERGEABLE_STATUS" \
294
322
  PR_CHECKS_TEXT="$PR_CHECKS_TEXT" \
295
323
  PR_FILES_TEXT="$PR_FILES_TEXT" \
324
+ PR_CONTEXT_READS_TEXT="$PR_CONTEXT_READS_TEXT" \
296
325
  PR_CHECK_FAILURES_TEXT="$PR_CHECK_FAILURES_TEXT" \
297
326
  PR_MISSING_REASONS_TEXT="$PR_MISSING_REASONS_TEXT" \
298
327
  PR_REVIEW_FINDINGS_TEXT="$PR_REVIEW_FINDINGS_TEXT" \
@@ -403,6 +432,7 @@ const replacements = {
403
432
  '{PR_MERGEABLE_STATUS}': process.env.PR_MERGEABLE_STATUS || '',
404
433
  '{PR_CHECKS_TEXT}': process.env.PR_CHECKS_TEXT || '',
405
434
  '{PR_FILES_TEXT}': process.env.PR_FILES_TEXT || '',
435
+ '{PR_CONTEXT_READS_TEXT}': process.env.PR_CONTEXT_READS_TEXT || '',
406
436
  '{PR_CHECK_FAILURES_TEXT}': process.env.PR_CHECK_FAILURES_TEXT || '',
407
437
  '{PR_MISSING_REASONS_TEXT}': process.env.PR_MISSING_REASONS_TEXT || '',
408
438
  '{PR_REVIEW_FINDINGS_TEXT}': process.env.PR_REVIEW_FINDINGS_TEXT || '',
@@ -123,6 +123,35 @@ PR_FILES_TEXT="$(jq -r '.files[] | "- " + .' <<<"$RISK_JSON")"
123
123
  PR_REPO_ROOT="$(flow_resolve_repo_root "${CONFIG_YAML}")"
124
124
  PR_DEPENDENCY_SOURCE_ROOT="${ACP_DEPENDENCY_SOURCE_ROOT:-${F_LOSNING_DEPENDENCY_SOURCE_ROOT:-$PR_REPO_ROOT}}"
125
125
 
126
+ render_pr_context_reads_text() {
127
+ local repo_root="${1:?repo root required}"
128
+ local -a candidate_paths=(
129
+ "${repo_root}/AGENTS.md"
130
+ "${repo_root}/openspec/AGENT_RULES.md"
131
+ "${repo_root}/openspec/AGENTS.md"
132
+ "${repo_root}/openspec/project.md"
133
+ "${repo_root}/openspec/CONVENTIONS.md"
134
+ "${repo_root}/docs/TESTING_AND_SEED_POLICY.md"
135
+ )
136
+ local -a existing_paths=()
137
+ local candidate_path=""
138
+
139
+ for candidate_path in "${candidate_paths[@]}"; do
140
+ if [[ -f "${candidate_path}" ]]; then
141
+ existing_paths+=("${candidate_path}")
142
+ fi
143
+ done
144
+
145
+ if [[ "${#existing_paths[@]}" -eq 0 ]]; then
146
+ printf '%s\n' '- No repo-specific context files were found under the expected AGENTS/OpenSpec/testing-doc locations; rely on the current diff and nearby source.'
147
+ return 0
148
+ fi
149
+
150
+ printf '%s\n' "${existing_paths[@]}" | sed 's/^/- `/' | sed 's/$/`/'
151
+ }
152
+
153
+ PR_CONTEXT_READS_TEXT="$(render_pr_context_reads_text "${PR_REPO_ROOT}")"
154
+
126
155
  case "$PR_AGENT_LANE" in
127
156
  double-check-1)
128
157
  PR_REVIEW_STAGE_TEXT="Independent agent double-check 1 of 2. A clean pass should advance this PR to the second review pass, not merge it yet."
@@ -157,6 +186,7 @@ PR_CHECKS_BYPASSED="$PR_CHECKS_BYPASSED" \
157
186
  PR_MERGE_STATE_STATUS="$PR_MERGE_STATE_STATUS" \
158
187
  PR_CHECKS_TEXT="$PR_CHECKS_TEXT" \
159
188
  PR_FILES_TEXT="$PR_FILES_TEXT" \
189
+ PR_CONTEXT_READS_TEXT="$PR_CONTEXT_READS_TEXT" \
160
190
  PR_REPO_ROOT="$PR_REPO_ROOT" \
161
191
  PR_DEPENDENCY_SOURCE_ROOT="$PR_DEPENDENCY_SOURCE_ROOT" \
162
192
  REPO_SLUG="$REPO_SLUG" \
@@ -183,6 +213,7 @@ const replacements = {
183
213
  '{PR_MERGE_STATE_STATUS}': process.env.PR_MERGE_STATE_STATUS || '',
184
214
  '{PR_CHECKS_TEXT}': process.env.PR_CHECKS_TEXT || '',
185
215
  '{PR_FILES_TEXT}': process.env.PR_FILES_TEXT || '',
216
+ '{PR_CONTEXT_READS_TEXT}': process.env.PR_CONTEXT_READS_TEXT || '',
186
217
  '{REPO_ROOT}': process.env.PR_REPO_ROOT || '',
187
218
  '{DEPENDENCY_SOURCE_ROOT}': process.env.PR_DEPENDENCY_SOURCE_ROOT || '',
188
219
  };
@@ -29,7 +29,7 @@ PENDING_LAUNCH_DIR="${ACP_PENDING_LAUNCH_DIR:-${F_LOSNING_PENDING_LAUNCH_DIR:-${
29
29
  SCHEDULED_STATE_DIR="${STATE_ROOT}/scheduled-issues"
30
30
  CONTROLLER_FILE="$(flow_resident_issue_controller_file "${CONFIG_YAML}" "${ISSUE_ID}")"
31
31
  RESIDENT_META_FILE="$(flow_resident_issue_meta_file "${CONFIG_YAML}" "${ISSUE_ID}")"
32
- CODING_WORKER="${ACP_CODING_WORKER:-${F_LOSNING_CODING_WORKER:-codex}}"
32
+ CODING_WORKER="${ACP_CODING_WORKER:-codex}"
33
33
  MAX_IMMEDIATE_CYCLES="$(flow_resident_issue_controller_max_immediate_cycles "${CONFIG_YAML}")"
34
34
  POLL_SECONDS="$(flow_resident_issue_controller_poll_seconds "${CONFIG_YAML}")"
35
35
  IDLE_TIMEOUT_SECONDS="$(flow_resident_issue_controller_idle_timeout_seconds "${CONFIG_YAML}")"
@@ -159,16 +159,32 @@ issue_pending_file() {
159
159
  printf '%s/issue-%s.pid\n' "${PENDING_LAUNCH_DIR}" "${issue_id}"
160
160
  }
161
161
 
162
- controller_unregister_pending_issue() {
163
- local issue_id="${1:-${ISSUE_ID:-}}"
164
- [[ -n "${issue_id}" ]] || return 0
165
- rm -f "$(issue_pending_file "${issue_id}")"
166
- }
167
-
168
- controller_register_pending_issue() {
169
- [[ -n "${ISSUE_ID:-}" ]] || return 0
170
- printf '%s\n' "$$" >"$(issue_pending_file "${ISSUE_ID}")"
171
- }
162
+ RESIDENT_CONTROLLER_LIB=""
163
+ for _rcl_candidate in \
164
+ "${SCRIPT_DIR}/resident-issue-controller-lib.sh" \
165
+ "${AGENT_CONTROL_PLANE_ROOT:-}/tools/bin/resident-issue-controller-lib.sh" \
166
+ "${ACP_ROOT:-}/tools/bin/resident-issue-controller-lib.sh" \
167
+ "${SHARED_AGENT_HOME:-}/tools/bin/resident-issue-controller-lib.sh"; do
168
+ if [[ -n "${_rcl_candidate}" && -f "${_rcl_candidate}" ]]; then
169
+ RESIDENT_CONTROLLER_LIB="${_rcl_candidate}"
170
+ break
171
+ fi
172
+ done
173
+ if [[ -n "${SHARED_AGENT_HOME:-}" && -z "${RESIDENT_CONTROLLER_LIB}" ]]; then
174
+ for _rcl_skill in "${AGENT_CONTROL_PLANE_SKILL_NAME:-agent-control-plane}" "${AGENT_CONTROL_PLANE_COMPAT_ALIAS:-}"; do
175
+ [[ -n "${_rcl_skill}" ]] || continue
176
+ _rcl_candidate="${SHARED_AGENT_HOME}/skills/openclaw/${_rcl_skill}/tools/bin/resident-issue-controller-lib.sh"
177
+ if [[ -f "${_rcl_candidate}" ]]; then
178
+ RESIDENT_CONTROLLER_LIB="${_rcl_candidate}"
179
+ break
180
+ fi
181
+ done
182
+ fi
183
+ if [[ -z "${RESIDENT_CONTROLLER_LIB}" ]]; then
184
+ echo "unable to locate resident-issue-controller-lib.sh" >&2
185
+ exit 1
186
+ fi
187
+ source "${RESIDENT_CONTROLLER_LIB}"
172
188
 
173
189
  issue_id_is_recurring() {
174
190
  local issue_id="${1:?issue id required}"
@@ -190,161 +206,6 @@ issue_id_is_scheduled() {
190
206
  issue_json_is_scheduled "$(issue_json_for "${issue_id}" 2>/dev/null || printf '{}\n')"
191
207
  }
192
208
 
193
- controller_refresh_execution_context() {
194
- unset \
195
- ACP_CODING_WORKER F_LOSNING_CODING_WORKER \
196
- ACP_CODEX_PROFILE_SAFE F_LOSNING_CODEX_PROFILE_SAFE \
197
- ACP_CODEX_PROFILE_BYPASS F_LOSNING_CODEX_PROFILE_BYPASS \
198
- ACP_CLAUDE_MODEL F_LOSNING_CLAUDE_MODEL \
199
- ACP_CLAUDE_PERMISSION_MODE F_LOSNING_CLAUDE_PERMISSION_MODE \
200
- ACP_CLAUDE_EFFORT F_LOSNING_CLAUDE_EFFORT \
201
- ACP_CLAUDE_TIMEOUT_SECONDS F_LOSNING_CLAUDE_TIMEOUT_SECONDS \
202
- ACP_CLAUDE_MAX_ATTEMPTS F_LOSNING_CLAUDE_MAX_ATTEMPTS \
203
- ACP_CLAUDE_RETRY_BACKOFF_SECONDS F_LOSNING_CLAUDE_RETRY_BACKOFF_SECONDS \
204
- ACP_OPENCLAW_MODEL F_LOSNING_OPENCLAW_MODEL \
205
- ACP_OPENCLAW_THINKING F_LOSNING_OPENCLAW_THINKING \
206
- ACP_OPENCLAW_TIMEOUT_SECONDS F_LOSNING_OPENCLAW_TIMEOUT_SECONDS \
207
- ACP_OPENCLAW_STALL_SECONDS F_LOSNING_OPENCLAW_STALL_SECONDS \
208
- ACP_ACTIVE_PROVIDER_POOL_NAME F_LOSNING_ACTIVE_PROVIDER_POOL_NAME \
209
- ACP_ACTIVE_PROVIDER_BACKEND F_LOSNING_ACTIVE_PROVIDER_BACKEND \
210
- ACP_ACTIVE_PROVIDER_MODEL F_LOSNING_ACTIVE_PROVIDER_MODEL \
211
- ACP_ACTIVE_PROVIDER_KEY F_LOSNING_ACTIVE_PROVIDER_KEY \
212
- ACP_PROVIDER_POOLS_EXHAUSTED F_LOSNING_PROVIDER_POOLS_EXHAUSTED \
213
- ACP_PROVIDER_POOL_SELECTION_REASON F_LOSNING_PROVIDER_POOL_SELECTION_REASON \
214
- ACP_PROVIDER_POOL_NEXT_ATTEMPT_EPOCH F_LOSNING_PROVIDER_POOL_NEXT_ATTEMPT_EPOCH \
215
- ACP_PROVIDER_POOL_NEXT_ATTEMPT_AT F_LOSNING_PROVIDER_POOL_NEXT_ATTEMPT_AT \
216
- ACP_PROVIDER_POOL_LAST_REASON F_LOSNING_PROVIDER_POOL_LAST_REASON
217
- flow_export_execution_env "${CONFIG_YAML}"
218
- flow_export_project_env_aliases
219
- CODING_WORKER="${ACP_CODING_WORKER:-${F_LOSNING_CODING_WORKER:-codex}}"
220
- controller_capture_active_provider_context
221
- }
222
-
223
- controller_refresh_issue_lane_context() {
224
- local is_scheduled="${1:-no}"
225
- local schedule_interval_seconds="${2:-0}"
226
-
227
- if [[ "${is_scheduled}" == "yes" ]]; then
228
- ACTIVE_RESIDENT_LANE_KIND="scheduled"
229
- ACTIVE_RESIDENT_LANE_VALUE="${schedule_interval_seconds}"
230
- else
231
- ACTIVE_RESIDENT_LANE_KIND="recurring"
232
- ACTIVE_RESIDENT_LANE_VALUE="general"
233
- fi
234
-
235
- ACTIVE_RESIDENT_WORKER_KEY="$(flow_resident_issue_lane_key "${CODING_WORKER}" "${MODE}" "${ACTIVE_RESIDENT_LANE_KIND}" "${ACTIVE_RESIDENT_LANE_VALUE}")"
236
- ACTIVE_RESIDENT_META_FILE="$(flow_resident_issue_lane_meta_file "${CONFIG_YAML}" "${ACTIVE_RESIDENT_WORKER_KEY}")"
237
- }
238
-
239
- controller_live_lane_peer() {
240
- [[ -n "${ACTIVE_RESIDENT_WORKER_KEY}" ]] || return 1
241
- flow_resident_live_issue_controller_for_key "${CONFIG_YAML}" "${ACTIVE_RESIDENT_WORKER_KEY}" "$$" || return 1
242
- }
243
-
244
- controller_yield_to_live_lane_peer() {
245
- local live_controller=""
246
- local controller_issue_id=""
247
- local controller_state=""
248
-
249
- live_controller="$(controller_live_lane_peer || true)"
250
- [[ -n "${live_controller}" ]] || return 1
251
-
252
- controller_issue_id="$(awk -F= '/^ISSUE_ID=/{print $2; exit}' <<<"${live_controller}")"
253
- controller_state="$(awk -F= '/^CONTROLLER_STATE=/{print $2; exit}' <<<"${live_controller}")"
254
-
255
- if [[ -n "${controller_issue_id}" && "${controller_issue_id}" != "${ISSUE_ID}" ]]; then
256
- flow_resident_issue_enqueue "${CONFIG_YAML}" "${ISSUE_ID}" "resident-live-lane" >/dev/null || true
257
- CONTROLLER_REASON="live-lane-controller-${controller_issue_id}-${controller_state:-running}"
258
- else
259
- CONTROLLER_REASON="duplicate-live-lane-controller"
260
- fi
261
-
262
- return 0
263
- }
264
-
265
- controller_capture_active_provider_context() {
266
- ACTIVE_PROVIDER_POOL_NAME="${ACP_ACTIVE_PROVIDER_POOL_NAME:-${F_LOSNING_ACTIVE_PROVIDER_POOL_NAME:-}}"
267
- ACTIVE_PROVIDER_BACKEND="${ACP_ACTIVE_PROVIDER_BACKEND:-${F_LOSNING_ACTIVE_PROVIDER_BACKEND:-${CODING_WORKER:-}}}"
268
- ACTIVE_PROVIDER_MODEL="${ACP_ACTIVE_PROVIDER_MODEL:-${F_LOSNING_ACTIVE_PROVIDER_MODEL:-}}"
269
- ACTIVE_PROVIDER_KEY="${ACP_ACTIVE_PROVIDER_KEY:-${F_LOSNING_ACTIVE_PROVIDER_KEY:-}}"
270
- ACTIVE_PROVIDER_SELECTION_REASON="${ACP_PROVIDER_POOL_SELECTION_REASON:-${F_LOSNING_PROVIDER_POOL_SELECTION_REASON:-}}"
271
- ACTIVE_PROVIDER_NEXT_ATTEMPT_EPOCH="${ACP_PROVIDER_POOL_NEXT_ATTEMPT_EPOCH:-${F_LOSNING_PROVIDER_POOL_NEXT_ATTEMPT_EPOCH:-}}"
272
- ACTIVE_PROVIDER_NEXT_ATTEMPT_AT="${ACP_PROVIDER_POOL_NEXT_ATTEMPT_AT:-${F_LOSNING_PROVIDER_POOL_NEXT_ATTEMPT_AT:-}}"
273
- ACTIVE_PROVIDER_LAST_REASON="${ACP_PROVIDER_POOL_LAST_REASON:-${F_LOSNING_PROVIDER_POOL_LAST_REASON:-}}"
274
-
275
- if [[ -z "${ACTIVE_PROVIDER_MODEL}" ]]; then
276
- case "${ACTIVE_PROVIDER_BACKEND}" in
277
- openclaw)
278
- ACTIVE_PROVIDER_MODEL="${ACP_OPENCLAW_MODEL:-${F_LOSNING_OPENCLAW_MODEL:-}}"
279
- ;;
280
- claude)
281
- ACTIVE_PROVIDER_MODEL="${ACP_CLAUDE_MODEL:-${F_LOSNING_CLAUDE_MODEL:-}}"
282
- ;;
283
- codex)
284
- ACTIVE_PROVIDER_MODEL="${ACP_CODEX_PROFILE_SAFE:-${F_LOSNING_CODEX_PROFILE_SAFE:-}}"
285
- ;;
286
- esac
287
- fi
288
-
289
- if [[ -z "${ACTIVE_PROVIDER_KEY}" && -n "${ACTIVE_PROVIDER_BACKEND}" && -n "${ACTIVE_PROVIDER_MODEL}" ]]; then
290
- ACTIVE_PROVIDER_KEY="$(flow_sanitize_provider_key "${ACTIVE_PROVIDER_BACKEND}-${ACTIVE_PROVIDER_MODEL}")"
291
- fi
292
- }
293
-
294
- controller_set_recorded_provider_from_active() {
295
- LAST_RECORDED_PROVIDER_POOL_NAME="${ACTIVE_PROVIDER_POOL_NAME}"
296
- LAST_RECORDED_PROVIDER_BACKEND="${ACTIVE_PROVIDER_BACKEND}"
297
- LAST_RECORDED_PROVIDER_MODEL="${ACTIVE_PROVIDER_MODEL}"
298
- LAST_RECORDED_PROVIDER_KEY="${ACTIVE_PROVIDER_KEY}"
299
- }
300
-
301
- controller_mark_provider_launched() {
302
- LAST_LAUNCHED_PROVIDER_POOL_NAME="${ACTIVE_PROVIDER_POOL_NAME}"
303
- LAST_LAUNCHED_PROVIDER_BACKEND="${ACTIVE_PROVIDER_BACKEND}"
304
- LAST_LAUNCHED_PROVIDER_MODEL="${ACTIVE_PROVIDER_MODEL}"
305
- LAST_LAUNCHED_PROVIDER_KEY="${ACTIVE_PROVIDER_KEY}"
306
-
307
- if [[ -z "${LAST_RECORDED_PROVIDER_KEY}" ]]; then
308
- controller_set_recorded_provider_from_active
309
- fi
310
- }
311
-
312
- controller_track_provider_selection() {
313
- local reason="${1:-provider-selection}"
314
- local now_at=""
315
-
316
- [[ -n "${ACTIVE_PROVIDER_KEY}" ]] || return 0
317
-
318
- if [[ -z "${LAST_RECORDED_PROVIDER_KEY}" ]]; then
319
- controller_set_recorded_provider_from_active
320
- return 0
321
- fi
322
-
323
- if [[ "${ACTIVE_PROVIDER_KEY}" == "${LAST_RECORDED_PROVIDER_KEY}" ]]; then
324
- return 0
325
- fi
326
-
327
- now_at="$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
328
- PROVIDER_SWITCH_COUNT=$((PROVIDER_SWITCH_COUNT + 1))
329
- LAST_PROVIDER_SWITCH_AT="${now_at}"
330
- LAST_PROVIDER_SWITCH_REASON="${reason}"
331
- LAST_PROVIDER_FROM_POOL_NAME="${LAST_RECORDED_PROVIDER_POOL_NAME}"
332
- LAST_PROVIDER_FROM_BACKEND="${LAST_RECORDED_PROVIDER_BACKEND}"
333
- LAST_PROVIDER_FROM_MODEL="${LAST_RECORDED_PROVIDER_MODEL}"
334
- LAST_PROVIDER_FROM_KEY="${LAST_RECORDED_PROVIDER_KEY}"
335
- LAST_PROVIDER_TO_POOL_NAME="${ACTIVE_PROVIDER_POOL_NAME}"
336
- LAST_PROVIDER_TO_BACKEND="${ACTIVE_PROVIDER_BACKEND}"
337
- LAST_PROVIDER_TO_MODEL="${ACTIVE_PROVIDER_MODEL}"
338
- LAST_PROVIDER_TO_KEY="${ACTIVE_PROVIDER_KEY}"
339
-
340
- if [[ "${reason}" == "provider-failover" ]]; then
341
- PROVIDER_FAILOVER_COUNT=$((PROVIDER_FAILOVER_COUNT + 1))
342
- LAST_PROVIDER_FAILOVER_AT="${now_at}"
343
- fi
344
-
345
- controller_set_recorded_provider_from_active
346
- }
347
-
348
209
  select_next_recurring_issue_id() {
349
210
  local candidate_id=""
350
211
 
@@ -371,274 +232,6 @@ select_next_recurring_issue_id() {
371
232
  return 1
372
233
  }
373
234
 
374
- controller_adopt_issue() {
375
- local next_issue_id="${1:?issue id required}"
376
- local previous_issue_id="${ISSUE_ID:-}"
377
- local previous_controller_file="${CONTROLLER_FILE:-}"
378
-
379
- if [[ -n "${previous_issue_id}" && "${previous_issue_id}" != "${next_issue_id}" ]]; then
380
- controller_unregister_pending_issue "${previous_issue_id}"
381
- if [[ -n "${previous_controller_file}" && -f "${previous_controller_file}" ]]; then
382
- rm -f "${previous_controller_file}"
383
- fi
384
- fi
385
-
386
- ISSUE_ID="${next_issue_id}"
387
- SESSION="${ISSUE_SESSION_PREFIX}${ISSUE_ID}"
388
- CONTROLLER_FILE="$(flow_resident_issue_controller_file "${CONFIG_YAML}" "${ISSUE_ID}")"
389
- RESIDENT_META_FILE="$(flow_resident_issue_meta_file "${CONFIG_YAML}" "${ISSUE_ID}")"
390
- CONTROLLER_LOOP_COUNT="0"
391
- NEXT_WAKE_EPOCH=""
392
- NEXT_WAKE_AT=""
393
- IDLE_WAIT_STARTED_EPOCH=""
394
- ACTIVE_RESIDENT_WORKER_KEY=""
395
- ACTIVE_RESIDENT_META_FILE=""
396
- ACTIVE_RESIDENT_LANE_KIND=""
397
- ACTIVE_RESIDENT_LANE_VALUE=""
398
- }
399
-
400
- controller_mark_issue_running() {
401
- local is_heavy="no"
402
-
403
- if declare -F heartbeat_issue_is_heavy >/dev/null 2>&1; then
404
- is_heavy="$(heartbeat_issue_is_heavy "${ISSUE_ID}" 2>/dev/null || printf 'no\n')"
405
- fi
406
-
407
- if declare -F heartbeat_mark_issue_running >/dev/null 2>&1; then
408
- heartbeat_mark_issue_running "${ISSUE_ID}" "${is_heavy}" >/dev/null 2>&1 || true
409
- fi
410
- }
411
-
412
- controller_rollback_issue_launch() {
413
- if declare -F heartbeat_issue_launch_failed >/dev/null 2>&1; then
414
- heartbeat_issue_launch_failed "${ISSUE_ID}" >/dev/null 2>&1 || true
415
- fi
416
- }
417
-
418
- controller_adopt_next_recurring_issue() {
419
- local next_issue_id=""
420
- local claim_out=""
421
- local claim_file=""
422
-
423
- claim_out="$(flow_resident_issue_claim_next "${CONFIG_YAML}" "${SESSION}" "${ISSUE_ID}" || true)"
424
- next_issue_id="$(awk -F= '/^ISSUE_ID=/{print $2}' <<<"${claim_out}")"
425
- claim_file="$(awk -F= '/^CLAIM_FILE=/{print $2}' <<<"${claim_out}")"
426
- if [[ -z "${next_issue_id}" ]]; then
427
- next_issue_id="$(select_next_recurring_issue_id || true)"
428
- fi
429
- [[ -n "${next_issue_id}" ]] || return 1
430
-
431
- controller_adopt_issue "${next_issue_id}"
432
- flow_resident_issue_release_claim "${claim_file}"
433
- CONTROLLER_REASON="adopted-next-recurring-issue"
434
- controller_write_state "adopting-issue" ""
435
- return 0
436
- }
437
-
438
- controller_wait_for_leased_issue() {
439
- local idle_timeout="${IDLE_TIMEOUT_SECONDS:-0}"
440
- local now_epoch=""
441
-
442
- case "${idle_timeout}" in
443
- ''|*[!0-9]*) idle_timeout="0" ;;
444
- esac
445
-
446
- if [[ "${idle_timeout}" -le 0 ]]; then
447
- return 1
448
- fi
449
-
450
- if [[ -z "${IDLE_WAIT_STARTED_EPOCH}" ]]; then
451
- IDLE_WAIT_STARTED_EPOCH="$(date +%s)"
452
- fi
453
-
454
- while true; do
455
- if controller_adopt_next_recurring_issue; then
456
- return 0
457
- fi
458
-
459
- now_epoch="$(date +%s)"
460
- if (( now_epoch - IDLE_WAIT_STARTED_EPOCH >= idle_timeout )); then
461
- CONTROLLER_REASON="idle-timeout"
462
- return 1
463
- fi
464
-
465
- controller_write_state "idle" ""
466
- sleep "${POLL_SECONDS}"
467
- done
468
- }
469
-
470
- controller_write_state() {
471
- local state="${1:?state required}"
472
- local reason="${2:-${CONTROLLER_REASON}}"
473
-
474
- CONTROLLER_STATE="${state}"
475
- CONTROLLER_REASON="${reason}"
476
- flow_resident_write_metadata "${CONTROLLER_FILE}" \
477
- "ISSUE_ID=${ISSUE_ID}" \
478
- "SESSION=${SESSION}" \
479
- "CONTROLLER_PID=$$" \
480
- "CONTROLLER_MODE=${MODE}" \
481
- "CONTROLLER_LOOP_COUNT=${CONTROLLER_LOOP_COUNT}" \
482
- "CONTROLLER_STATE=${CONTROLLER_STATE}" \
483
- "CONTROLLER_REASON=${CONTROLLER_REASON}" \
484
- "ACTIVE_RESIDENT_WORKER_KEY=${ACTIVE_RESIDENT_WORKER_KEY}" \
485
- "ACTIVE_RESIDENT_LANE_KIND=${ACTIVE_RESIDENT_LANE_KIND}" \
486
- "ACTIVE_RESIDENT_LANE_VALUE=${ACTIVE_RESIDENT_LANE_VALUE}" \
487
- "ACTIVE_PROVIDER_POOL_NAME=${ACTIVE_PROVIDER_POOL_NAME}" \
488
- "ACTIVE_PROVIDER_BACKEND=${ACTIVE_PROVIDER_BACKEND}" \
489
- "ACTIVE_PROVIDER_MODEL=${ACTIVE_PROVIDER_MODEL}" \
490
- "ACTIVE_PROVIDER_KEY=${ACTIVE_PROVIDER_KEY}" \
491
- "ACTIVE_PROVIDER_SELECTION_REASON=${ACTIVE_PROVIDER_SELECTION_REASON}" \
492
- "ACTIVE_PROVIDER_NEXT_ATTEMPT_EPOCH=${ACTIVE_PROVIDER_NEXT_ATTEMPT_EPOCH}" \
493
- "ACTIVE_PROVIDER_NEXT_ATTEMPT_AT=${ACTIVE_PROVIDER_NEXT_ATTEMPT_AT}" \
494
- "ACTIVE_PROVIDER_LAST_REASON=${ACTIVE_PROVIDER_LAST_REASON}" \
495
- "LAST_LAUNCHED_PROVIDER_POOL_NAME=${LAST_LAUNCHED_PROVIDER_POOL_NAME}" \
496
- "LAST_LAUNCHED_PROVIDER_BACKEND=${LAST_LAUNCHED_PROVIDER_BACKEND}" \
497
- "LAST_LAUNCHED_PROVIDER_MODEL=${LAST_LAUNCHED_PROVIDER_MODEL}" \
498
- "LAST_LAUNCHED_PROVIDER_KEY=${LAST_LAUNCHED_PROVIDER_KEY}" \
499
- "PROVIDER_SWITCH_COUNT=${PROVIDER_SWITCH_COUNT}" \
500
- "PROVIDER_FAILOVER_COUNT=${PROVIDER_FAILOVER_COUNT}" \
501
- "LAST_PROVIDER_SWITCH_AT=${LAST_PROVIDER_SWITCH_AT}" \
502
- "LAST_PROVIDER_SWITCH_REASON=${LAST_PROVIDER_SWITCH_REASON}" \
503
- "LAST_PROVIDER_FROM_POOL_NAME=${LAST_PROVIDER_FROM_POOL_NAME}" \
504
- "LAST_PROVIDER_FROM_BACKEND=${LAST_PROVIDER_FROM_BACKEND}" \
505
- "LAST_PROVIDER_FROM_MODEL=${LAST_PROVIDER_FROM_MODEL}" \
506
- "LAST_PROVIDER_FROM_KEY=${LAST_PROVIDER_FROM_KEY}" \
507
- "LAST_PROVIDER_TO_POOL_NAME=${LAST_PROVIDER_TO_POOL_NAME}" \
508
- "LAST_PROVIDER_TO_BACKEND=${LAST_PROVIDER_TO_BACKEND}" \
509
- "LAST_PROVIDER_TO_MODEL=${LAST_PROVIDER_TO_MODEL}" \
510
- "LAST_PROVIDER_TO_KEY=${LAST_PROVIDER_TO_KEY}" \
511
- "LAST_PROVIDER_FAILOVER_AT=${LAST_PROVIDER_FAILOVER_AT}" \
512
- "PROVIDER_WAIT_COUNT=${PROVIDER_WAIT_COUNT}" \
513
- "PROVIDER_WAIT_TOTAL_SECONDS=${PROVIDER_WAIT_TOTAL_SECONDS}" \
514
- "PROVIDER_LAST_WAIT_SECONDS=${PROVIDER_LAST_WAIT_SECONDS}" \
515
- "PROVIDER_LAST_WAIT_STARTED_AT=${PROVIDER_LAST_WAIT_STARTED_AT}" \
516
- "PROVIDER_LAST_WAIT_COMPLETED_AT=${PROVIDER_LAST_WAIT_COMPLETED_AT}" \
517
- "NEXT_WAKE_EPOCH=${NEXT_WAKE_EPOCH}" \
518
- "NEXT_WAKE_AT=${NEXT_WAKE_AT}" \
519
- "UPDATED_AT=$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
520
-
521
- if [[ "${CONTROLLER_STATE}" == "stopped" ]]; then
522
- controller_unregister_pending_issue "${ISSUE_ID}"
523
- elif flow_resident_issue_controller_counts_as_pending "${CONTROLLER_STATE}"; then
524
- controller_register_pending_issue
525
- else
526
- controller_unregister_pending_issue "${ISSUE_ID}"
527
- fi
528
- }
529
-
530
- controller_last_failure_reason() {
531
- local metadata_file="${ACTIVE_RESIDENT_META_FILE:-${RESIDENT_META_FILE:-}}"
532
- [[ -n "${metadata_file}" && -f "${metadata_file}" ]] || return 1
533
- awk -F= '/^LAST_FAILURE_REASON=/{print $2; exit}' "${metadata_file}" 2>/dev/null | tr -d '"' || true
534
- }
535
-
536
- controller_provider_state() {
537
- local provider_state_script="${FLOW_TOOLS_DIR}/provider-cooldown-state.sh"
538
- local provider_state=""
539
-
540
- if [[ ! -x "${provider_state_script}" ]]; then
541
- printf 'READY=yes\n'
542
- return 0
543
- fi
544
-
545
- provider_state="$(
546
- env \
547
- -u ACP_CODING_WORKER -u F_LOSNING_CODING_WORKER \
548
- -u ACP_CODEX_PROFILE_SAFE -u F_LOSNING_CODEX_PROFILE_SAFE \
549
- -u ACP_CODEX_PROFILE_BYPASS -u F_LOSNING_CODEX_PROFILE_BYPASS \
550
- -u ACP_CLAUDE_MODEL -u F_LOSNING_CLAUDE_MODEL \
551
- -u ACP_CLAUDE_PERMISSION_MODE -u F_LOSNING_CLAUDE_PERMISSION_MODE \
552
- -u ACP_CLAUDE_EFFORT -u F_LOSNING_CLAUDE_EFFORT \
553
- -u ACP_CLAUDE_TIMEOUT_SECONDS -u F_LOSNING_CLAUDE_TIMEOUT_SECONDS \
554
- -u ACP_CLAUDE_MAX_ATTEMPTS -u F_LOSNING_CLAUDE_MAX_ATTEMPTS \
555
- -u ACP_CLAUDE_RETRY_BACKOFF_SECONDS -u F_LOSNING_CLAUDE_RETRY_BACKOFF_SECONDS \
556
- -u ACP_OPENCLAW_MODEL -u F_LOSNING_OPENCLAW_MODEL \
557
- -u ACP_OPENCLAW_THINKING -u F_LOSNING_OPENCLAW_THINKING \
558
- -u ACP_OPENCLAW_TIMEOUT_SECONDS -u F_LOSNING_OPENCLAW_TIMEOUT_SECONDS \
559
- -u ACP_ACTIVE_PROVIDER_POOL_NAME -u F_LOSNING_ACTIVE_PROVIDER_POOL_NAME \
560
- -u ACP_ACTIVE_PROVIDER_BACKEND -u F_LOSNING_ACTIVE_PROVIDER_BACKEND \
561
- -u ACP_ACTIVE_PROVIDER_MODEL -u F_LOSNING_ACTIVE_PROVIDER_MODEL \
562
- -u ACP_ACTIVE_PROVIDER_KEY -u F_LOSNING_ACTIVE_PROVIDER_KEY \
563
- -u ACP_PROVIDER_POOLS_EXHAUSTED -u F_LOSNING_PROVIDER_POOLS_EXHAUSTED \
564
- -u ACP_PROVIDER_POOL_SELECTION_REASON -u F_LOSNING_PROVIDER_POOL_SELECTION_REASON \
565
- -u ACP_PROVIDER_POOL_NEXT_ATTEMPT_EPOCH -u F_LOSNING_PROVIDER_POOL_NEXT_ATTEMPT_EPOCH \
566
- -u ACP_PROVIDER_POOL_NEXT_ATTEMPT_AT -u F_LOSNING_PROVIDER_POOL_NEXT_ATTEMPT_AT \
567
- -u ACP_PROVIDER_POOL_LAST_REASON -u F_LOSNING_PROVIDER_POOL_LAST_REASON \
568
- "${provider_state_script}" get 2>/dev/null || true
569
- )"
570
- if [[ -z "${provider_state}" ]]; then
571
- printf 'READY=yes\n'
572
- return 0
573
- fi
574
-
575
- printf '%s\n' "${provider_state}"
576
- }
577
-
578
- controller_wait_for_provider_capacity() {
579
- local provider_state=""
580
- local provider_ready=""
581
- local provider_next_epoch=""
582
- local provider_next_at=""
583
- local now_epoch=""
584
- local remaining=""
585
- local sleep_seconds=""
586
- local wait_started_epoch=""
587
- local wait_completed_epoch=""
588
-
589
- PROVIDER_WAITED="no"
590
-
591
- while true; do
592
- provider_state="$(controller_provider_state)"
593
- provider_ready="$(flow_kv_get "${provider_state}" "READY")"
594
- if [[ "${provider_ready}" == "yes" ]]; then
595
- if [[ -n "${wait_started_epoch}" ]]; then
596
- wait_completed_epoch="$(date +%s)"
597
- if (( wait_completed_epoch >= wait_started_epoch )); then
598
- PROVIDER_LAST_WAIT_SECONDS=$((wait_completed_epoch - wait_started_epoch))
599
- PROVIDER_WAIT_TOTAL_SECONDS=$((PROVIDER_WAIT_TOTAL_SECONDS + PROVIDER_LAST_WAIT_SECONDS))
600
- PROVIDER_LAST_WAIT_COMPLETED_AT="$(date -u -r "${wait_completed_epoch}" +"%Y-%m-%dT%H:%M:%SZ")"
601
- fi
602
- fi
603
- NEXT_WAKE_EPOCH=""
604
- NEXT_WAKE_AT=""
605
- return 0
606
- fi
607
-
608
- provider_next_epoch="$(flow_kv_get "${provider_state}" "NEXT_ATTEMPT_EPOCH")"
609
- provider_next_at="$(flow_kv_get "${provider_state}" "NEXT_ATTEMPT_AT")"
610
- if ! [[ "${provider_next_epoch}" =~ ^[0-9]+$ ]] || [[ "${provider_next_epoch}" == "0" ]]; then
611
- return 1
612
- fi
613
-
614
- if [[ -z "${wait_started_epoch}" ]]; then
615
- wait_started_epoch="$(date +%s)"
616
- PROVIDER_WAIT_COUNT=$((PROVIDER_WAIT_COUNT + 1))
617
- PROVIDER_LAST_WAIT_STARTED_AT="$(date -u -r "${wait_started_epoch}" +"%Y-%m-%dT%H:%M:%SZ")"
618
- fi
619
-
620
- PROVIDER_WAITED="yes"
621
- NEXT_WAKE_EPOCH="${provider_next_epoch}"
622
- NEXT_WAKE_AT="${provider_next_at}"
623
- CONTROLLER_REASON="provider-cooldown"
624
- controller_write_state "waiting-provider" ""
625
-
626
- now_epoch="$(date +%s)"
627
- remaining=$((provider_next_epoch - now_epoch))
628
- sleep_seconds="${POLL_SECONDS}"
629
- if ! [[ "${sleep_seconds}" =~ ^[1-9][0-9]*$ ]]; then
630
- sleep_seconds="60"
631
- fi
632
- if (( remaining > 0 && remaining < sleep_seconds )); then
633
- sleep_seconds="${remaining}"
634
- fi
635
- if (( sleep_seconds <= 0 )); then
636
- sleep_seconds="1"
637
- fi
638
- sleep "${sleep_seconds}"
639
- done
640
- }
641
-
642
235
  record_scheduled_next_due() {
643
236
  local interval_seconds="${1:-0}"
644
237
  local state_file now_epoch next_due_epoch
@@ -662,10 +255,6 @@ UPDATED_AT=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
662
255
  EOF
663
256
  }
664
257
 
665
- controller_cleanup() {
666
- controller_write_state "stopped" "${CONTROLLER_REASON:-stopped}"
667
- }
668
-
669
258
  trap 'CONTROLLER_REASON="${CONTROLLER_REASON:-terminated}"; controller_cleanup' EXIT
670
259
  trap 'CONTROLLER_REASON="interrupted"; exit 0' INT TERM
671
260
 
@@ -6,11 +6,11 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
6
6
  source "${SCRIPT_DIR}/flow-config-lib.sh"
7
7
 
8
8
  CONFIG_YAML="$(resolve_flow_config_yaml "${BASH_SOURCE[0]}")"
9
- SOURCE_REPO_ROOT="${ACP_SOURCE_REPO_ROOT:-${F_LOSNING_SOURCE_REPO_ROOT:-$(flow_resolve_retained_repo_root "${CONFIG_YAML}")}}"
9
+ SOURCE_REPO_ROOT="${ACP_SOURCE_REPO_ROOT:-$(flow_resolve_retained_repo_root "${CONFIG_YAML}")}"
10
10
  CANONICAL_REPO_ROOT="$(flow_resolve_repo_root "${CONFIG_YAML}")"
11
11
  AGENT_REPO_ROOT="$(flow_resolve_agent_repo_root "${CONFIG_YAML}")"
12
12
  DEFAULT_BRANCH="$(flow_resolve_default_branch "${CONFIG_YAML}")"
13
- REMOTE_NAME="${ACP_REMOTE_NAME:-${F_LOSNING_REMOTE_NAME:-origin}}"
13
+ REMOTE_NAME="${ACP_REMOTE_NAME:-origin}"
14
14
  FLOW_SKILL_DIR="$(resolve_flow_skill_dir "${BASH_SOURCE[0]}")"
15
15
  FLOW_TOOLS_DIR="${FLOW_SKILL_DIR}/tools/bin"
16
16
 
@@ -13,9 +13,9 @@ STATE_ROOT="$(flow_resolve_state_root "${CONFIG_YAML}")"
13
13
  LOCK_DIR="${STATE_ROOT}/dependency-baseline.lock"
14
14
  PID_FILE="${LOCK_DIR}/pid"
15
15
  HASH_FILE="${STATE_ROOT}/dependency-baseline.sha256"
16
- PACKAGE_MANAGER_BIN="${ACP_PACKAGE_MANAGER_BIN:-${F_LOSNING_PACKAGE_MANAGER_BIN:-pnpm}}"
17
- WORKSPACE_BUILD_PACKAGES_RAW="${ACP_WORKSPACE_BUILD_PACKAGES:-${F_LOSNING_WORKSPACE_BUILD_PACKAGES:-}}"
18
- WORKSPACE_BUILD_ARTIFACTS_RAW="${ACP_WORKSPACE_BUILD_ARTIFACTS:-${F_LOSNING_WORKSPACE_BUILD_ARTIFACTS:-}}"
16
+ PACKAGE_MANAGER_BIN="${ACP_PACKAGE_MANAGER_BIN:-pnpm}"
17
+ WORKSPACE_BUILD_PACKAGES_RAW="${ACP_WORKSPACE_BUILD_PACKAGES:-}"
18
+ WORKSPACE_BUILD_ARTIFACTS_RAW="${ACP_WORKSPACE_BUILD_ARTIFACTS:-}"
19
19
  declare -a WORKSPACE_BUILD_PACKAGES=()
20
20
  declare -a WORKSPACE_BUILD_ARTIFACTS=()
21
21
 
@@ -207,7 +207,10 @@ sync_tree_rsync() {
207
207
  local target_dir="${2:?target dir required}"
208
208
  [[ -d "${source_dir}" ]] || return 0
209
209
  mkdir -p "${target_dir}"
210
- rsync -a --delete --exclude='.git/' "${source_dir}/" "${target_dir}/"
210
+ if rsync -a --delete --exclude='.git/' "${source_dir}/" "${target_dir}/"; then
211
+ return 0
212
+ fi
213
+ sync_tree_copy_mode "${source_dir}" "${target_dir}"
211
214
  }
212
215
 
213
216
  reset_runtime_skill_targets() {