agent-control-plane 0.4.9 → 0.6.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 (80) hide show
  1. package/README.md +72 -9
  2. package/npm/bin/agent-control-plane.js +1 -1
  3. package/package.json +39 -33
  4. package/tools/bin/debug-session.sh +106 -0
  5. package/tools/bin/flow-runtime-doctor-linux.sh +136 -0
  6. package/tools/bin/flow-runtime-doctor.sh +5 -1
  7. package/tools/bin/install-project-systemd.sh +255 -0
  8. package/tools/bin/project-runtimectl.sh +45 -0
  9. package/tools/bin/project-systemd-bootstrap.sh +74 -0
  10. package/tools/bin/uninstall-project-systemd.sh +87 -0
  11. package/tools/dashboard/app.js +198 -5
  12. package/tools/dashboard/issue_queue_state.py +101 -0
  13. package/tools/dashboard/server.py +123 -1
  14. package/tools/dashboard/styles.css +526 -455
  15. package/tools/bin/agent-cleanup-worktree +0 -247
  16. package/tools/bin/agent-github-update-labels +0 -105
  17. package/tools/bin/agent-init-worktree +0 -216
  18. package/tools/bin/agent-project-archive-run +0 -52
  19. package/tools/bin/agent-project-capture-worker +0 -46
  20. package/tools/bin/agent-project-catch-up-issue-pr-links +0 -118
  21. package/tools/bin/agent-project-catch-up-merged-prs +0 -195
  22. package/tools/bin/agent-project-catch-up-scheduled-issue-retries +0 -123
  23. package/tools/bin/agent-project-cleanup-session +0 -513
  24. package/tools/bin/agent-project-detached-launch +0 -127
  25. package/tools/bin/agent-project-heartbeat-loop +0 -1029
  26. package/tools/bin/agent-project-open-issue-worktree +0 -89
  27. package/tools/bin/agent-project-open-pr-worktree +0 -80
  28. package/tools/bin/agent-project-publish-issue-pr +0 -468
  29. package/tools/bin/agent-project-reconcile-issue-session +0 -1409
  30. package/tools/bin/agent-project-reconcile-pr-session +0 -1288
  31. package/tools/bin/agent-project-retry-state +0 -158
  32. package/tools/bin/agent-project-run-claude-session +0 -805
  33. package/tools/bin/agent-project-run-codex-resilient +0 -963
  34. package/tools/bin/agent-project-run-codex-session +0 -435
  35. package/tools/bin/agent-project-run-kilo-session +0 -369
  36. package/tools/bin/agent-project-run-ollama-session +0 -658
  37. package/tools/bin/agent-project-run-openclaw-session +0 -1309
  38. package/tools/bin/agent-project-run-opencode-session +0 -377
  39. package/tools/bin/agent-project-run-pi-session +0 -479
  40. package/tools/bin/agent-project-sync-anchor-repo +0 -139
  41. package/tools/bin/agent-project-sync-source-repo-main +0 -163
  42. package/tools/bin/agent-project-worker-status +0 -188
  43. package/tools/bin/branch-verification-guard.sh +0 -364
  44. package/tools/bin/capture-worker.sh +0 -18
  45. package/tools/bin/cleanup-worktree.sh +0 -52
  46. package/tools/bin/codex-quota +0 -31
  47. package/tools/bin/create-follow-up-issue.sh +0 -114
  48. package/tools/bin/dashboard-launchd-bootstrap.sh +0 -50
  49. package/tools/bin/issue-publish-localization-guard.sh +0 -142
  50. package/tools/bin/issue-publish-scope-guard.sh +0 -242
  51. package/tools/bin/issue-requires-local-workspace-install.sh +0 -31
  52. package/tools/bin/issue-resource-class.sh +0 -12
  53. package/tools/bin/kick-scheduler.sh +0 -75
  54. package/tools/bin/label-follow-up-issues.sh +0 -14
  55. package/tools/bin/new-pr-worktree.sh +0 -50
  56. package/tools/bin/new-worktree.sh +0 -49
  57. package/tools/bin/pr-risk.sh +0 -12
  58. package/tools/bin/prepare-worktree.sh +0 -142
  59. package/tools/bin/provider-cooldown-state.sh +0 -204
  60. package/tools/bin/publish-issue-worker.sh +0 -31
  61. package/tools/bin/reconcile-bootstrap-lib.sh +0 -113
  62. package/tools/bin/reconcile-issue-worker.sh +0 -34
  63. package/tools/bin/reconcile-pr-worker.sh +0 -34
  64. package/tools/bin/record-verification.sh +0 -71
  65. package/tools/bin/render-flow-config.sh +0 -98
  66. package/tools/bin/resident-issue-controller-lib.sh +0 -448
  67. package/tools/bin/retry-state.sh +0 -31
  68. package/tools/bin/reuse-issue-worktree.sh +0 -121
  69. package/tools/bin/run-codex-bypass.sh +0 -3
  70. package/tools/bin/run-codex-safe.sh +0 -3
  71. package/tools/bin/run-codex-task.sh +0 -280
  72. package/tools/bin/serve-dashboard.sh +0 -5
  73. package/tools/bin/start-issue-worker.sh +0 -943
  74. package/tools/bin/start-pr-fix-worker.sh +0 -528
  75. package/tools/bin/start-pr-merge-repair-worker.sh +0 -8
  76. package/tools/bin/start-pr-review-worker.sh +0 -261
  77. package/tools/bin/start-resident-issue-loop.sh +0 -499
  78. package/tools/bin/update-github-labels.sh +0 -14
  79. package/tools/bin/worker-status.sh +0 -19
  80. package/tools/bin/workflow-catalog.sh +0 -77
@@ -1,1288 +0,0 @@
1
- #!/usr/bin/env bash
2
- set -euo pipefail
3
-
4
- SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
5
- RECONCILE_BOOTSTRAP_LIB=""
6
- for _rbl_candidate in \
7
- "${SCRIPT_DIR}/reconcile-bootstrap-lib.sh" \
8
- "${AGENT_CONTROL_PLANE_ROOT:-}/tools/bin/reconcile-bootstrap-lib.sh" \
9
- "${ACP_ROOT:-}/tools/bin/reconcile-bootstrap-lib.sh" \
10
- "${SHARED_AGENT_HOME:-}/tools/bin/reconcile-bootstrap-lib.sh"; do
11
- if [[ -n "${_rbl_candidate}" && -f "${_rbl_candidate}" ]]; then
12
- RECONCILE_BOOTSTRAP_LIB="${_rbl_candidate}"
13
- break
14
- fi
15
- done
16
- if [[ -n "${SHARED_AGENT_HOME:-}" && -z "${RECONCILE_BOOTSTRAP_LIB}" ]]; then
17
- for _rbl_skill in "${AGENT_CONTROL_PLANE_SKILL_NAME:-agent-control-plane}" "${AGENT_CONTROL_PLANE_COMPAT_ALIAS:-}"; do
18
- [[ -n "${_rbl_skill}" ]] || continue
19
- _rbl_candidate="${SHARED_AGENT_HOME}/skills/openclaw/${_rbl_skill}/tools/bin/reconcile-bootstrap-lib.sh"
20
- if [[ -f "${_rbl_candidate}" ]]; then
21
- RECONCILE_BOOTSTRAP_LIB="${_rbl_candidate}"
22
- break
23
- fi
24
- done
25
- fi
26
- if [[ -z "${RECONCILE_BOOTSTRAP_LIB}" ]]; then
27
- echo "unable to locate reconcile-bootstrap-lib.sh" >&2
28
- exit 1
29
- fi
30
- # shellcheck source=/dev/null
31
- source "${RECONCILE_BOOTSTRAP_LIB}"
32
-
33
- usage() {
34
- cat <<'EOF'
35
- Usage:
36
- agent-project-reconcile-pr-session --session <id> --repo-slug <owner/repo> --repo-root <path> --runs-root <path> --history-root <path> [--hook-file <path>]
37
-
38
- Reconcile a completed PR worker run using shared lifecycle control flow while
39
- allowing project adapters to inject policy hooks.
40
- EOF
41
- }
42
-
43
- verification_guard_script="${shared_tools_dir}/branch-verification-guard.sh"
44
- session=""
45
- repo_slug=""
46
- repo_root=""
47
- runs_root=""
48
- history_root=""
49
- hook_file=""
50
-
51
- while [[ $# -gt 0 ]]; do
52
- case "$1" in
53
- --session) session="${2:-}"; shift 2 ;;
54
- --repo-slug) repo_slug="${2:-}"; shift 2 ;;
55
- --repo-root) repo_root="${2:-}"; shift 2 ;;
56
- --runs-root) runs_root="${2:-}"; shift 2 ;;
57
- --history-root) history_root="${2:-}"; shift 2 ;;
58
- --hook-file) hook_file="${2:-}"; shift 2 ;;
59
- --help|-h) usage; exit 0 ;;
60
- *) echo "Unknown argument: $1" >&2; usage >&2; exit 1 ;;
61
- esac
62
- done
63
-
64
- if [[ -z "$session" || -z "$repo_slug" || -z "$repo_root" || -z "$runs_root" || -z "$history_root" ]]; then
65
- usage >&2
66
- exit 1
67
- fi
68
-
69
- status_out="$(
70
- "${shared_tools_dir}/agent-project-worker-status" \
71
- --runs-root "$runs_root" \
72
- --session "$session"
73
- )"
74
- status="$(awk -F= '/^STATUS=/{print $2}' <<<"$status_out")"
75
- failure_reason="$(awk -F= '/^FAILURE_REASON=/{print $2}' <<<"$status_out" | tail -n 1)"
76
-
77
- meta_file="$(awk -F= '/^META_FILE=/{print $2}' <<<"$status_out")"
78
- if [[ -z "$meta_file" || ! -f "$meta_file" ]]; then
79
- echo "missing metadata for session $session" >&2
80
- exit 1
81
- fi
82
-
83
- run_dir="$(dirname "$meta_file")"
84
-
85
- set -a
86
- # shellcheck source=/dev/null
87
- source "$meta_file"
88
- set +a
89
- pr_worktree="${WORKTREE:-}"
90
-
91
- pr_number="${PR_NUMBER:-}"
92
- if [[ -z "$pr_number" ]]; then
93
- echo "session $session is missing PR_NUMBER" >&2
94
- exit 1
95
- fi
96
-
97
- result_outcome=""
98
- result_action=""
99
- result_issue_id="${ISSUE_ID:-}"
100
- result_detail=""
101
- run_started_at="${STARTED_AT:-}"
102
- expected_run_started_at="${ACP_EXPECTED_RUN_STARTED_AT:-${F_LOSNING_EXPECTED_RUN_STARTED_AT:-}}"
103
- host_blocker_file="${run_dir}/host-blocker.md"
104
- prompt_file="${run_dir}/prompt.md"
105
- pr_comment_file="${run_dir}/pr-comment.md"
106
- session_log_file="${run_dir}/${session}.log"
107
- record_verification_script="${FLOW_TOOLS_DIR:-${shared_tools_dir}}/record-verification.sh"
108
- result_file_candidate="${run_dir}/result.env"
109
- if [[ ! -f "$result_file_candidate" && -n "${RESULT_FILE:-}" && -f "${RESULT_FILE}" ]]; then
110
- result_file_candidate="${RESULT_FILE}"
111
- fi
112
- if [[ -f "$result_file_candidate" ]]; then
113
- set -a
114
- # shellcheck source=/dev/null
115
- source "$result_file_candidate"
116
- set +a
117
- result_outcome="${OUTCOME:-}"
118
- result_action="${ACTION:-}"
119
- result_detail="${DETAIL:-}"
120
- result_issue_id="${ISSUE_ID:-${result_issue_id}}"
121
- fi
122
-
123
- if [[ -n "${expected_run_started_at}" && "${expected_run_started_at}" != "${run_started_at}" ]]; then
124
- printf 'STATUS=STALE-RUN-SKIPPED\n'
125
- printf 'SESSION=%s\n' "$session"
126
- printf 'EXPECTED_STARTED_AT=%s\n' "${expected_run_started_at}"
127
- printf 'ACTUAL_STARTED_AT=%s\n' "${run_started_at}"
128
- exit 0
129
- fi
130
-
131
- pr_schedule_retry() { :; }
132
- pr_clear_retry() { :; }
133
- pr_cleanup_linked_issue_session() { :; }
134
- pr_cleanup_merged_residue() { :; }
135
- pr_linked_issue_should_close() { printf 'yes\n'; }
136
- pr_after_merged() { :; }
137
- pr_after_closed() { :; }
138
- pr_automerge_allowed() { printf 'no\n'; }
139
- pr_review_pass_action() { printf 'merge\n'; }
140
- pr_after_double_check_advanced() { :; }
141
- pr_after_updated_branch() { :; }
142
- pr_after_blocked() { :; }
143
- pr_after_succeeded() { :; }
144
- pr_after_failed() { :; }
145
- pr_after_reconciled() { :; }
146
- pr_result_contract_note=""
147
-
148
- if [[ -n "$hook_file" && -f "$hook_file" ]]; then
149
- # shellcheck source=/dev/null
150
- source "$hook_file"
151
- fi
152
-
153
- provider_cooldown_script="${shared_tools_dir}/provider-cooldown-state.sh"
154
- github_core_rate_limit_script="${shared_tools_dir}/github-core-rate-limit-state.sh"
155
- github_write_outbox_script="${shared_tools_dir}/github-write-outbox.sh"
156
- automated_pr_approval_body="Automated final review passed. Safe low-risk scope, green checks, and host-side merge approved."
157
-
158
- schedule_provider_quota_cooldown() {
159
- local reason="${1:-provider-quota-limit}"
160
- [[ "${failure_reason:-}" == "provider-quota-limit" ]] || return 0
161
- [[ -x "${provider_cooldown_script}" ]] || return 0
162
- [[ "${CODING_WORKER:-}" == "codex" ]] && return 0
163
-
164
- "${provider_cooldown_script}" schedule "${reason}" >/dev/null || true
165
- }
166
-
167
- clear_provider_quota_cooldown() {
168
- [[ -x "${provider_cooldown_script}" ]] || return 0
169
- [[ "${CODING_WORKER:-}" == "codex" ]] && return 0
170
-
171
- "${provider_cooldown_script}" clear >/dev/null || true
172
- }
173
-
174
- blocked_runtime_reason=""
175
- host_github_rate_limited="no"
176
- host_github_rate_limit_detail=""
177
-
178
- host_github_rate_limit_state_active() {
179
- local state_out=""
180
- local ready=""
181
- local next_attempt_at=""
182
-
183
- [[ -x "${github_core_rate_limit_script}" ]] || return 1
184
- state_out="$("${github_core_rate_limit_script}" get 2>/dev/null || true)"
185
- ready="$(awk -F= '/^READY=/{print $2; exit}' <<<"${state_out}")"
186
- [[ "${ready}" == "no" ]] || return 1
187
-
188
- next_attempt_at="$(awk -F= '/^NEXT_ATTEMPT_AT=/{print $2; exit}' <<<"${state_out}")"
189
- host_github_rate_limited="yes"
190
- if [[ -n "${next_attempt_at}" ]]; then
191
- host_github_rate_limit_detail="GitHub core API rate limit cooldown is active and resets at ${next_attempt_at}."
192
- else
193
- host_github_rate_limit_detail="GitHub core API rate limit cooldown is active."
194
- fi
195
- return 0
196
- }
197
-
198
- owner="${repo_slug%%/*}"
199
- repo="${repo_slug#*/}"
200
- pr_view_json="$(flow_github_pr_view_json "$repo_slug" "$pr_number")"
201
- pr_state="$(jq -r '.state' <<<"$pr_view_json")"
202
- pr_base_ref="$(jq -r '.baseRefName // empty' <<<"$pr_view_json")"
203
- if [[ -z "${pr_base_ref}" ]]; then
204
- pr_base_ref="main"
205
- fi
206
-
207
- if [[ "$status" == "RUNNING" && "$pr_state" != "MERGED" && "$pr_state" != "CLOSED" ]]; then
208
- printf 'STATUS=%s\n' "$status"
209
- exit 0
210
- fi
211
-
212
- cleanup_output_value() {
213
- local cleanup_output="${1:-}"
214
- local key="${2:?key required}"
215
- awk -F= -v target_key="${key}" '$1 == target_key { print substr($0, index($0, "=") + 1); exit }' <<<"${cleanup_output}"
216
- }
217
-
218
- warn_cleanup_pr_session() {
219
- local cleanup_output="${1:-}"
220
- local cleanup_exit="${2:-0}"
221
- local cleanup_status=""
222
- local cleanup_mode=""
223
- local cleanup_error=""
224
-
225
- cleanup_status="$(cleanup_output_value "${cleanup_output}" "CLEANUP_STATUS")"
226
- if [[ -z "${cleanup_status}" ]]; then
227
- cleanup_status="${cleanup_exit}"
228
- fi
229
- [[ "${cleanup_status}" != "0" ]] || return 0
230
-
231
- cleanup_mode="$(cleanup_output_value "${cleanup_output}" "CLEANUP_MODE")"
232
- cleanup_error="$(cleanup_output_value "${cleanup_output}" "CLEANUP_ERROR")"
233
- printf '[%s] pr cleanup warning session=%s status=%s mode=%s\n' \
234
- "$(date -u +"%Y-%m-%dT%H:%M:%SZ")" \
235
- "${session}" \
236
- "${cleanup_status}" \
237
- "${cleanup_mode:-unknown}" >&2
238
- if [[ -n "${cleanup_error}" ]]; then
239
- printf '[%s] pr cleanup detail session=%s %s\n' \
240
- "$(date -u +"%Y-%m-%dT%H:%M:%SZ")" \
241
- "${session}" \
242
- "${cleanup_error}" >&2
243
- fi
244
- }
245
-
246
- review_pass_action_from_result_action() {
247
- case "${1:-}" in
248
- host-advance-double-check-2)
249
- printf 'advance-double-check-2\n'
250
- ;;
251
- host-await-human-review)
252
- printf 'wait-human\n'
253
- ;;
254
- host-approve-and-merge|"")
255
- printf 'merge\n'
256
- ;;
257
- *)
258
- return 1
259
- ;;
260
- esac
261
- }
262
-
263
- normalize_pr_result_contract() {
264
- [[ "$status" == "SUCCEEDED" ]] || return 0
265
- [[ "$pr_state" == "MERGED" ]] && return 0
266
-
267
- case "${result_outcome:-}" in
268
- approved-local-review-passed)
269
- local review_pass_action expected_action legacy_action=""
270
- if [[ -n "${result_action:-}" ]]; then
271
- if review_pass_action="$(review_pass_action_from_result_action "${result_action}" 2>/dev/null)"; then
272
- :
273
- elif [[ "${result_action}" == "host-approve-and-merge" ]]; then
274
- review_pass_action="merge"
275
- pr_result_contract_note="normalized-legacy-review-action"
276
- else
277
- echo "invalid PR review result contract for session ${session}: ACTION='${result_action}'" >&2
278
- return 1
279
- fi
280
- else
281
- review_pass_action="$(pr_review_pass_action "$pr_number")"
282
- fi
283
- case "$review_pass_action" in
284
- advance-double-check-2)
285
- expected_action="host-advance-double-check-2"
286
- legacy_action="host-approve-and-merge"
287
- ;;
288
- wait-human)
289
- expected_action="host-await-human-review"
290
- ;;
291
- merge|"")
292
- expected_action="host-approve-and-merge"
293
- ;;
294
- *)
295
- echo "unsupported review pass action for PR ${pr_number}: ${review_pass_action}" >&2
296
- return 1
297
- ;;
298
- esac
299
- if [[ -z "${result_action:-}" ]]; then
300
- result_action="$expected_action"
301
- pr_result_contract_note="normalized-missing-review-action"
302
- return 0
303
- fi
304
- if [[ "$result_action" == "$expected_action" ]]; then
305
- return 0
306
- fi
307
- if [[ -n "$legacy_action" && "$result_action" == "$legacy_action" ]]; then
308
- result_action="$expected_action"
309
- pr_result_contract_note="normalized-legacy-review-action"
310
- return 0
311
- fi
312
- echo "invalid PR review result contract for session ${session}: OUTCOME='${result_outcome}' ACTION='${result_action}' EXPECTED='${expected_action}'" >&2
313
- return 1
314
- ;;
315
- updated-branch)
316
- if [[ -z "${result_action:-}" ]]; then
317
- result_action="host-push-pr-branch"
318
- pr_result_contract_note="normalized-missing-updated-branch-action"
319
- return 0
320
- fi
321
- if [[ "$result_action" == "host-push-pr-branch" ]]; then
322
- return 0
323
- fi
324
- echo "invalid PR updated-branch result contract for session ${session}: ACTION='${result_action}'" >&2
325
- return 1
326
- ;;
327
- no-change-needed)
328
- if [[ -z "${result_action:-}" ]]; then
329
- result_action="host-refresh-pr-state"
330
- pr_result_contract_note="normalized-missing-no-change-needed-action"
331
- return 0
332
- fi
333
- if [[ "$result_action" == "host-refresh-pr-state" ]]; then
334
- return 0
335
- fi
336
- echo "invalid PR no-change-needed result contract for session ${session}: ACTION='${result_action}'" >&2
337
- return 1
338
- ;;
339
- blocked)
340
- if [[ -z "${result_action:-}" ]]; then
341
- result_action="host-comment-pr-blocker"
342
- pr_result_contract_note="normalized-missing-blocked-action"
343
- return 0
344
- fi
345
- case "$result_action" in
346
- host-comment-pr-blocker)
347
- return 0
348
- ;;
349
- host-comment-blocker)
350
- result_action="host-comment-pr-blocker"
351
- pr_result_contract_note="normalized-legacy-blocked-action"
352
- return 0
353
- ;;
354
- requested-changes-or-blocked)
355
- result_action="host-comment-pr-blocker"
356
- pr_result_contract_note="normalized-legacy-blocked-action"
357
- return 0
358
- ;;
359
- *)
360
- echo "invalid PR blocked result contract for session ${session}: ACTION='${result_action}'" >&2
361
- return 1
362
- ;;
363
- esac
364
- ;;
365
- *)
366
- echo "invalid PR worker result contract for session ${session}: OUTCOME='${result_outcome:-}' ACTION='${result_action:-}'" >&2
367
- return 1
368
- ;;
369
- esac
370
- }
371
-
372
- mark_reconciled() {
373
- local reconciled_at tmp_file
374
- if [[ -d "$run_dir" ]]; then
375
- reconciled_at="$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
376
- tmp_file="${run_dir}/reconciled.ok.tmp.$$"
377
- {
378
- printf 'STARTED_AT=%s\n' "${run_started_at}"
379
- printf 'RECONCILED_AT=%s\n' "${reconciled_at}"
380
- } >"${tmp_file}"
381
- mv "${tmp_file}" "${run_dir}/reconciled.ok"
382
- fi
383
- }
384
-
385
- post_pr_comment_if_present() {
386
- local comment_file="${run_dir}/pr-comment.md"
387
- [[ -s "$comment_file" ]] || return 0
388
- if pr_comment_already_posted; then
389
- return 0
390
- fi
391
- if host_github_post_issue_comment "${pr_number}" "$(cat "$comment_file")"; then
392
- return 0
393
- fi
394
- if [[ -x "${github_write_outbox_script}" ]]; then
395
- if "${github_write_outbox_script}" enqueue-comment \
396
- --repo-slug "${repo_slug}" \
397
- --number "${pr_number}" \
398
- --kind pr \
399
- --body-file "${comment_file}" >/dev/null 2>&1; then
400
- return 0
401
- fi
402
- fi
403
- return 1
404
- }
405
-
406
- enqueue_pr_approval_intent() {
407
- [[ -x "${github_write_outbox_script}" ]] || return 1
408
- "${github_write_outbox_script}" enqueue-approval \
409
- --repo-slug "${repo_slug}" \
410
- --number "${pr_number}" \
411
- --body "${automated_pr_approval_body}" >/dev/null 2>&1
412
- }
413
-
414
- pr_comment_already_posted() {
415
- local comment_file="${run_dir}/pr-comment.md"
416
- [[ -s "$comment_file" ]] || return 1
417
- local comment_body comments_json
418
- comment_body="$(cat "$comment_file")"
419
- comments_json="$(flow_github_pr_view_json "$repo_slug" "$pr_number" 2>/dev/null || true)"
420
- [[ -n "$comments_json" ]] || return 1
421
- jq -e --arg body "$comment_body" 'any(.comments[]?; .body == $body)' >/dev/null <<<"$comments_json"
422
- }
423
-
424
- host_github_output_indicates_rate_limit() {
425
- grep -Eiq 'API rate limit exceeded|secondary rate limit|rate limit exceeded|HTTP 403' <<<"${1:-}"
426
- }
427
-
428
- record_host_github_rate_limit() {
429
- local output="${1:-}"
430
- local detail_file="${run_dir}/host-github-rate-limit.log"
431
- host_github_rate_limited="yes"
432
- host_github_rate_limit_detail="${output}"
433
- printf '%s\n' "${output}" >"${detail_file}"
434
- if [[ -x "${github_core_rate_limit_script}" ]]; then
435
- "${github_core_rate_limit_script}" schedule "github-api-rate-limit" >/dev/null 2>&1 || true
436
- fi
437
- }
438
-
439
- host_github_post_issue_comment() {
440
- local issue_number="${1:?issue number required}"
441
- local body="${2:-}"
442
- local output=""
443
-
444
- if host_github_rate_limit_state_active; then
445
- return 1
446
- fi
447
-
448
- flow_export_github_cli_auth_env "${repo_slug}"
449
- if output="$(
450
- flow_github_api_repo "${repo_slug}" "issues/${issue_number}/comments" \
451
- --method POST \
452
- -f body="${body}" 2>&1
453
- )"; then
454
- return 0
455
- fi
456
-
457
- if host_github_output_indicates_rate_limit "${output}"; then
458
- record_host_github_rate_limit "${output}"
459
- return 1
460
- fi
461
-
462
- printf '%s\n' "${output}" >&2
463
- return 1
464
- }
465
-
466
- host_github_submit_pr_approval() {
467
- local output=""
468
-
469
- if host_github_rate_limit_state_active; then
470
- enqueue_pr_approval_intent || true
471
- return 1
472
- fi
473
-
474
- flow_export_github_cli_auth_env "${repo_slug}"
475
- if output="$(
476
- flow_github_pr_review_approve "${repo_slug}" "${pr_number}" "${automated_pr_approval_body}" 2>&1
477
- )"; then
478
- return 0
479
- fi
480
-
481
- if grep -q "Can not approve your own pull request" <<<"${output}" || grep -q "approve your own pull is not allowed" <<<"${output}"; then
482
- return 0
483
- fi
484
-
485
- if host_github_output_indicates_rate_limit "${output}"; then
486
- record_host_github_rate_limit "${output}"
487
- enqueue_pr_approval_intent || true
488
- return 1
489
- fi
490
-
491
- printf '%s\n' "${output}" >&2
492
- return 1
493
- }
494
-
495
- append_host_rate_limit_comment() {
496
- local detail="${1:-GitHub API rate limit blocked host actions.}"
497
- local reset_line=""
498
-
499
- if grep -Eiq 'resets at ' <<<"${detail}"; then
500
- reset_line="$(grep -Eio 'resets at [^.]+' <<<"${detail}" | head -n 1 || true)"
501
- fi
502
-
503
- {
504
- if [[ -s "${pr_comment_file}" ]]; then
505
- printf '\n\n'
506
- fi
507
- printf '## Host action blocked\n\n'
508
- printf 'GitHub API rate limit blocked ACP from posting the PR review outcome or merge action.\n'
509
- if [[ -n "${reset_line}" ]]; then
510
- printf '\n- %s\n' "${reset_line}"
511
- fi
512
- printf -- '- ACP kept the local review artifacts and scheduled an automatic retry for the host action.\n'
513
- } >>"${pr_comment_file}"
514
- }
515
-
516
- handle_host_github_rate_limit_retry() {
517
- local reason="${1:-github-api-rate-limit}"
518
- local result_action_override="${2:-host-rate-limit-retry}"
519
-
520
- append_host_rate_limit_comment "${host_github_rate_limit_detail:-}"
521
- require_transition "pr_schedule_retry" pr_schedule_retry "${reason}"
522
- require_transition "pr_after_blocked" pr_after_blocked "${pr_number}"
523
- cleanup_pr_session
524
- result_outcome="blocked"
525
- result_action="${result_action_override}"
526
- failure_reason="${reason}"
527
- notify_pr_reconciled
528
- mark_reconciled
529
- printf 'STATUS=FAILED\n'
530
- printf 'PR_NUMBER=%s\n' "${pr_number}"
531
- printf 'PR_STATE=%s\n' "${pr_state}"
532
- printf 'OUTCOME=%s\n' "${result_outcome}"
533
- printf 'ACTION=%s\n' "${result_action}"
534
- printf 'FAILURE_REASON=%s\n' "${failure_reason}"
535
- exit 0
536
- }
537
-
538
- maybe_handle_host_github_rate_limit() {
539
- local reason="${1:-github-api-rate-limit}"
540
- local result_action_override="${2:-host-rate-limit-retry}"
541
- if [[ "${host_github_rate_limited}" == "yes" ]]; then
542
- handle_host_github_rate_limit_retry "${reason}" "${result_action_override}"
543
- fi
544
- return 1
545
- }
546
-
547
- blocked_result_indicates_local_bind_failure() {
548
- local candidate_file
549
- for candidate_file in "$pr_comment_file" "$session_log_file"; do
550
- [[ -f "$candidate_file" ]] || continue
551
- if grep -Eiq 'listen EPERM|failed to bind to local ports|Process from config\.webServer exited early' "$candidate_file"; then
552
- return 0
553
- fi
554
- done
555
- return 1
556
- }
557
-
558
- classify_pr_blocked_runtime_reason() {
559
- if [[ "${result_detail:-}" == "worker-tool-exec-empty-command" ]]; then
560
- printf 'worker-tool-exec-empty-command\n'
561
- return 0
562
- fi
563
-
564
- if [[ -f "$session_log_file" ]] && grep -Fq '[tools] exec failed: Provide a command to start.' "$session_log_file"; then
565
- printf 'worker-tool-exec-empty-command\n'
566
- return 0
567
- fi
568
-
569
- if [[ -f "$session_log_file" ]] && grep -Eiq 'no-codex-output-before-stall-threshold|no-codex-progress-before-stall-threshold' "$session_log_file" 2>/dev/null; then
570
- printf 'codex-stalled\n'
571
- return 0
572
- fi
573
-
574
- if [[ -f "$session_log_file" ]] && grep -Eiq 'no-agent-output-before-stall-threshold|no-agent-progress-before-stall-threshold' "$session_log_file" 2>/dev/null; then
575
- printf 'agent-stalled\n'
576
- return 0
577
- fi
578
-
579
- if [[ -f "$session_log_file" ]] && grep -Eiq 'provider-quota-limit|quota.*exhausted|rate.limit.*exceeded' "$session_log_file" 2>/dev/null; then
580
- printf 'provider-quota-limit\n'
581
- return 0
582
- fi
583
-
584
- if [[ -f "$pr_comment_file" ]] && grep -Eiq 'no-codex-output-before-stall-threshold|no-codex-progress-before-stall-threshold' "$pr_comment_file" 2>/dev/null; then
585
- printf 'codex-stalled\n'
586
- return 0
587
- fi
588
-
589
- if [[ -f "$pr_comment_file" ]] && grep -Eiq 'no-agent-output-before-stall-threshold|no-agent-progress-before-stall-threshold' "$pr_comment_file" 2>/dev/null; then
590
- printf 'agent-stalled\n'
591
- return 0
592
- fi
593
-
594
- return 1
595
- }
596
-
597
- extract_preapproved_host_recovery_commands() {
598
- [[ -f "$prompt_file" ]] || return 0
599
- sed -n 's/^.*loopback retry command: `\(.*\)`$/\1/p' "$prompt_file"
600
- }
601
-
602
- record_host_verification_result() {
603
- local verification_status="${1:?verification status required}"
604
- local command_text="${2:?command text required}"
605
- local note_text="${3:-}"
606
-
607
- if [[ ! -x "$record_verification_script" ]]; then
608
- return 0
609
- fi
610
-
611
- if [[ -n "$note_text" ]]; then
612
- bash "$record_verification_script" --run-dir "$run_dir" --status "$verification_status" --command "$command_text" --note "$note_text" >/dev/null
613
- else
614
- bash "$record_verification_script" --run-dir "$run_dir" --status "$verification_status" --command "$command_text" >/dev/null
615
- fi
616
- }
617
-
618
- run_host_verification_command() {
619
- local command_text="${1:?command text required}"
620
-
621
- {
622
- printf '\n[host-recovery] command=%s\n' "$command_text"
623
- } >>"$session_log_file"
624
-
625
- if WORKTREE_DIR="$pr_worktree" HOST_COMMAND="$command_text" bash -lc 'set -euo pipefail; cd "$WORKTREE_DIR"; eval "$HOST_COMMAND"' >>"$session_log_file" 2>&1; then
626
- record_host_verification_result "pass" "$command_text" "host-recovery-after-sandbox-bind-failure"
627
- return 0
628
- fi
629
-
630
- record_host_verification_result "fail" "$command_text" "host-recovery-after-sandbox-bind-failure"
631
- printf '[host-recovery] command failed=%s\n' "$command_text" >>"$session_log_file"
632
- return 1
633
- }
634
-
635
- rewrite_pr_comment_for_host_recovery() {
636
- local commands_text="${1:-}"
637
- local tmp_comment_file="${pr_comment_file}.tmp.$$"
638
- local command_text=""
639
-
640
- if [[ -f "$pr_comment_file" ]]; then
641
- awk '
642
- /^\*\*Blocker\*\*$/ { exit }
643
- { print }
644
- ' "$pr_comment_file" >"$tmp_comment_file"
645
- else
646
- : >"$tmp_comment_file"
647
- fi
648
-
649
- {
650
- if [[ -s "$tmp_comment_file" ]]; then
651
- printf '\n\n'
652
- fi
653
- printf '**Host Recovery**\n'
654
- printf -- '- Host reran the pre-approved verification outside the worker sandbox after sandbox-only local port bind failures.\n'
655
- while IFS= read -r command_text; do
656
- [[ -n "$command_text" ]] || continue
657
- printf -- '- ✅ `%s`\n' "$command_text"
658
- done <<<"$commands_text"
659
- } >>"$tmp_comment_file"
660
-
661
- mv "$tmp_comment_file" "$pr_comment_file"
662
- }
663
-
664
- persist_updated_branch_result_contract() {
665
- cat >"${run_dir}/result.env" <<EOF
666
- OUTCOME=updated-branch
667
- ACTION=host-push-pr-branch
668
- PR_NUMBER=${pr_number}
669
- ISSUE_ID=${result_issue_id}
670
- EOF
671
- }
672
-
673
- attempt_blocked_pr_host_verification_recovery() {
674
- local recovery_commands=""
675
- local command_text=""
676
-
677
- [[ "${result_outcome:-}" == "blocked" ]] || return 1
678
- [[ -n "$pr_worktree" && -d "$pr_worktree" ]] || return 1
679
- blocked_result_indicates_local_bind_failure || return 1
680
-
681
- recovery_commands="$(extract_preapproved_host_recovery_commands)"
682
- [[ -n "$recovery_commands" ]] || return 1
683
-
684
- while IFS= read -r command_text; do
685
- [[ -n "$command_text" ]] || continue
686
- if ! run_host_verification_command "$command_text"; then
687
- return 1
688
- fi
689
- done <<<"$recovery_commands"
690
-
691
- rewrite_pr_comment_for_host_recovery "$recovery_commands"
692
- persist_updated_branch_result_contract
693
- result_outcome="updated-branch"
694
- result_action="host-push-pr-branch"
695
- failure_reason=""
696
- pr_result_contract_note="host-recovered-sandbox-bind-failure"
697
- return 0
698
- }
699
-
700
- close_linked_issue_if_open() {
701
- local issue_id="${1:-}"
702
- [[ -n "$issue_id" ]] || return 0
703
-
704
- local issue_state
705
- issue_state="$(flow_github_issue_view_json "$repo_slug" "$issue_id" 2>/dev/null | jq -r '.state // empty' || true)"
706
- if [[ "$issue_state" == "OPEN" ]]; then
707
- flow_github_issue_close "$repo_slug" "$issue_id" "Closed automatically after PR #${pr_number} merged." >/dev/null 2>&1 || true
708
- fi
709
- }
710
-
711
- push_pr_branch() {
712
- if [[ -z "$pr_worktree" || ! -d "$pr_worktree" ]]; then
713
- echo "missing PR worktree for session $session" >&2
714
- return 1
715
- fi
716
- if [[ -z "${PR_HEAD_REF:-}" ]]; then
717
- echo "session $session is missing PR_HEAD_REF" >&2
718
- return 1
719
- fi
720
-
721
- git -C "$pr_worktree" push origin "HEAD:${PR_HEAD_REF}"
722
- }
723
-
724
- pr_branch_commits_ahead_remote_count() {
725
- if [[ -z "$pr_worktree" || ! -d "$pr_worktree" ]]; then
726
- echo "missing PR worktree for session $session" >&2
727
- return 1
728
- fi
729
- if [[ -z "${PR_HEAD_REF:-}" ]]; then
730
- echo "session $session is missing PR_HEAD_REF" >&2
731
- return 1
732
- fi
733
- if ! git -C "$pr_worktree" fetch origin "+refs/heads/${PR_HEAD_REF}:refs/remotes/origin/${PR_HEAD_REF}" --prune >/dev/null 2>&1; then
734
- echo "unable to refresh remote tracking ref for PR head ${PR_HEAD_REF}" >&2
735
- return 1
736
- fi
737
-
738
- git -C "$pr_worktree" rev-list --count "origin/${PR_HEAD_REF}..HEAD"
739
- }
740
-
741
- guard_pr_branch_verification() {
742
- local guard_output=""
743
- if [[ -z "$pr_worktree" || ! -d "$pr_worktree" ]]; then
744
- echo "missing PR worktree for session $session" >&2
745
- return 1
746
- fi
747
-
748
- if guard_output="$(
749
- "${verification_guard_script}" \
750
- --worktree "$pr_worktree" \
751
- --base-ref "origin/${pr_base_ref}" \
752
- --run-dir "$run_dir" 2>&1
753
- )"; then
754
- rm -f "$host_blocker_file"
755
- return 0
756
- fi
757
-
758
- printf '%s\n' "$guard_output" >"$host_blocker_file"
759
- printf '%s\n' "$guard_output" >&2
760
- return 1
761
- }
762
-
763
- collect_stageable_paths() {
764
- if [[ -z "$pr_worktree" || ! -d "$pr_worktree" ]]; then
765
- return 0
766
- fi
767
-
768
- {
769
- git -C "$pr_worktree" diff --name-only --relative
770
- git -C "$pr_worktree" diff --cached --name-only --relative
771
- git -C "$pr_worktree" ls-files --others --exclude-standard 2>/dev/null || true
772
- } | awk '
773
- NF == 0 { next }
774
- $0 ~ /(^|\/)node_modules(\/|$)/ { next }
775
- $0 ~ /^\.openclaw-artifacts(\/|$)/ { next }
776
- !seen[$0]++ { print $0 }
777
- ' | sort
778
- }
779
-
780
- host_commit_pr_worktree_changes() {
781
- local stageable_paths
782
- local commit_message_file="${run_dir}/commit-message.txt"
783
- local commit_message=""
784
-
785
- if [[ -z "$pr_worktree" || ! -d "$pr_worktree" ]]; then
786
- echo "missing PR worktree for session $session" >&2
787
- return 1
788
- fi
789
-
790
- stageable_paths="$(collect_stageable_paths)"
791
- if [[ -n "$stageable_paths" ]]; then
792
- while IFS= read -r relative_path; do
793
- [[ -n "$relative_path" ]] || continue
794
- git -C "$pr_worktree" add -- "$relative_path"
795
- done <<<"$stageable_paths"
796
- fi
797
-
798
- if git -C "$pr_worktree" diff --cached --quiet && git -C "$pr_worktree" diff --quiet; then
799
- return 0
800
- fi
801
-
802
- if [[ -f "$commit_message_file" ]]; then
803
- commit_message="$(tr -d '\r' <"$commit_message_file" | sed -n '1p')"
804
- fi
805
-
806
- if [[ -z "$commit_message" ]]; then
807
- if git -C "$pr_worktree" rev-parse -q --verify MERGE_HEAD >/dev/null 2>&1; then
808
- commit_message="fix(pr): resolve merge drift for PR #${pr_number}"
809
- else
810
- commit_message="fix(pr): apply repair for PR #${pr_number}"
811
- fi
812
- fi
813
-
814
- git -C "$pr_worktree" commit -m "$commit_message" >/dev/null
815
- }
816
-
817
- remaining_merge_conflict_paths() {
818
- local base_ref="${1:?base ref required}"
819
-
820
- if [[ -z "$pr_worktree" || ! -d "$pr_worktree" ]]; then
821
- printf '%s\n' "__missing_worktree__"
822
- return 0
823
- fi
824
-
825
- if git -C "$pr_worktree" rev-parse -q --verify MERGE_HEAD >/dev/null 2>&1; then
826
- git -C "$pr_worktree" ls-files -u \
827
- | awk '
828
- NF >= 4 {
829
- path=$4
830
- if (!(path in seen)) {
831
- seen[path]=1
832
- print path
833
- }
834
- }
835
- '
836
- return 0
837
- fi
838
-
839
- if ! git -C "$repo_root" fetch origin "+refs/heads/${base_ref}:refs/remotes/origin/${base_ref}" --prune >/dev/null 2>&1; then
840
- printf '%s\n' "__unable_to_fetch_base_ref__:${base_ref}"
841
- return 0
842
- fi
843
-
844
- local base_sha
845
- base_sha="$(git -C "$pr_worktree" merge-base HEAD "origin/${base_ref}" 2>/dev/null || true)"
846
- if [[ -z "$base_sha" ]]; then
847
- printf '%s\n' "__unable_to_compute_merge_base__:${base_ref}"
848
- return 0
849
- fi
850
-
851
- git -C "$pr_worktree" merge-tree "$base_sha" HEAD "origin/${base_ref}" 2>/dev/null \
852
- | awk '
853
- /^changed in both$/ { capture=1; next }
854
- capture && /^( base| our| their) / {
855
- path=$NF
856
- if (!(path in seen)) {
857
- seen[path]=1
858
- print path
859
- }
860
- }
861
- capture && /^@@ / { capture=0 }
862
- '
863
- }
864
-
865
- append_host_guard_comment() {
866
- local base_ref="${1:?base ref required}"
867
- local conflict_paths="${2:-}"
868
- local comment_file="${run_dir}/pr-comment.md"
869
- local host_blocker_body=""
870
-
871
- if [[ -s "$comment_file" ]]; then
872
- printf '\n\n' >>"$comment_file"
873
- fi
874
-
875
- host_blocker_body="$(cat <<EOF
876
- ## PR repair host guard
877
-
878
- Host rejected this repair and did not push it because local merge simulation against \`${base_ref}\` still reports unresolved conflict paths:
879
-
880
- $(printf '%s\n' "$conflict_paths" | sed 's/^/- /')
881
-
882
- No partial repair commit was pushed to the PR branch. The PR stays in fix lane and will retry after cooldown.
883
- EOF
884
- )"
885
-
886
- printf '%s\n' "$host_blocker_body" >>"$comment_file"
887
- printf '%s\n' "$host_blocker_body" >"$host_blocker_file"
888
- }
889
-
890
- merge_state_prepared() {
891
- [[ -n "$pr_worktree" && -d "$pr_worktree" ]] || return 1
892
- git -C "$pr_worktree" rev-parse -q --verify MERGE_HEAD >/dev/null 2>&1
893
- }
894
-
895
- current_github_login() {
896
- if host_github_rate_limit_state_active; then
897
- printf '\n'
898
- return 0
899
- fi
900
- flow_export_github_cli_auth_env "${repo_slug}"
901
- flow_github_current_login
902
- }
903
-
904
- pr_author_login() {
905
- if host_github_rate_limit_state_active; then
906
- printf '\n'
907
- return 0
908
- fi
909
- flow_export_github_cli_auth_env "${repo_slug}"
910
- flow_github_pr_author_login "${repo_slug}" "${pr_number}"
911
- }
912
-
913
- pr_is_self_authored_for_current_actor() {
914
- local actor_login=""
915
- local author_login=""
916
-
917
- actor_login="$(current_github_login)"
918
- author_login="$(pr_author_login)"
919
- [[ -n "${actor_login}" && -n "${author_login}" && "${actor_login}" == "${author_login}" ]]
920
- }
921
-
922
- pr_remote_head_oid() {
923
- if host_github_rate_limit_state_active; then
924
- printf '\n'
925
- return 0
926
- fi
927
- flow_export_github_cli_auth_env "${repo_slug}"
928
- flow_github_pr_head_oid "${repo_slug}" "${pr_number}"
929
- }
930
-
931
- pr_remote_already_has_final_head() {
932
- local final_head="${FINAL_HEAD:-}"
933
- local remote_head=""
934
-
935
- [[ -n "${final_head}" ]] || return 1
936
- remote_head="$(pr_remote_head_oid)"
937
- [[ -n "${remote_head}" && "${remote_head}" == "${final_head}" ]]
938
- }
939
-
940
- approve_and_merge() {
941
- if ! pr_is_self_authored_for_current_actor; then
942
- if ! host_github_submit_pr_approval; then
943
- if [[ "${host_github_rate_limited}" == "yes" ]]; then
944
- return 2
945
- fi
946
- return 1
947
- fi
948
- fi
949
-
950
- if host_github_rate_limit_state_active; then
951
- return 2
952
- fi
953
-
954
- flow_export_github_cli_auth_env "${repo_slug}"
955
- if flow_github_pr_merge "$repo_slug" "$pr_number" "squash" "yes" >"${run_dir}/host-github-merge.out" 2>"${run_dir}/host-github-merge.err"; then
956
- return 0
957
- fi
958
-
959
- local merge_output=""
960
- merge_output="$(cat "${run_dir}/host-github-merge.err" 2>/dev/null || true)"
961
- if host_github_output_indicates_rate_limit "${merge_output}"; then
962
- record_host_github_rate_limit "${merge_output}"
963
- return 2
964
- fi
965
- if [[ -n "${merge_output}" ]]; then
966
- printf '%s\n' "${merge_output}" >&2
967
- fi
968
- return 1
969
- }
970
-
971
- cleanup_pr_session() {
972
- local cleanup_output=""
973
- local cleanup_exit="0"
974
-
975
- if cleanup_output="$(
976
- "${shared_tools_dir}/agent-project-cleanup-session" \
977
- --repo-root "$repo_root" \
978
- --runs-root "$runs_root" \
979
- --history-root "$history_root" \
980
- --session "$session" \
981
- --worktree "$pr_worktree" \
982
- --mode pr 2>&1
983
- )"; then
984
- cleanup_exit="0"
985
- else
986
- cleanup_exit="$?"
987
- fi
988
-
989
- warn_cleanup_pr_session "${cleanup_output}" "${cleanup_exit}"
990
- }
991
-
992
- notify_pr_reconciled() {
993
- pr_after_reconciled "$status" "$pr_state" "${result_outcome:-}" "${result_action:-}" "$pr_number" || true
994
- }
995
-
996
- handle_verification_guard_block() {
997
- failure_reason="verification-guard-blocked"
998
- require_transition "pr_schedule_retry" pr_schedule_retry "$failure_reason"
999
- require_transition "pr_after_blocked" pr_after_blocked "$pr_number"
1000
- cleanup_pr_session
1001
- result_outcome="blocked"
1002
- result_action="host-verification-guard-blocked"
1003
- notify_pr_reconciled
1004
- }
1005
-
1006
- handle_linked_issue_merge_cleanup() {
1007
- local issue_id="${1:-}"
1008
- [[ -n "$issue_id" ]] || return 0
1009
- pr_cleanup_linked_issue_session "$issue_id" || true
1010
- if [[ "$(pr_linked_issue_should_close "$issue_id" || printf 'yes\n')" == "yes" ]]; then
1011
- close_linked_issue_if_open "$issue_id"
1012
- fi
1013
- }
1014
-
1015
- handle_updated_branch_result() {
1016
- if [[ -z "$pr_worktree" || ! -d "$pr_worktree" ]]; then
1017
- if pr_remote_already_has_final_head; then
1018
- post_pr_comment_if_present || maybe_handle_host_github_rate_limit "github-api-rate-limit" "host-comment-rate-limit-retry"
1019
- require_transition "pr_clear_retry" pr_clear_retry
1020
- require_transition "pr_after_updated_branch" pr_after_updated_branch "$pr_number"
1021
- cleanup_pr_session
1022
- result_action="${result_action:-host-push-pr-branch}"
1023
- notify_pr_reconciled
1024
- elif pr_comment_already_posted; then
1025
- require_transition "pr_clear_retry" pr_clear_retry
1026
- require_transition "pr_after_updated_branch" pr_after_updated_branch "$pr_number"
1027
- cleanup_pr_session
1028
- result_action="${result_action:-pushed-branch-for-ci}"
1029
- notify_pr_reconciled
1030
- else
1031
- require_transition "pr_schedule_retry" pr_schedule_retry "missing-pr-worktree-before-publish"
1032
- require_transition "pr_after_blocked" pr_after_blocked "$pr_number"
1033
- cleanup_pr_session
1034
- result_outcome="blocked"
1035
- result_action="host-missing-pr-worktree"
1036
- notify_pr_reconciled
1037
- fi
1038
- return 0
1039
- fi
1040
-
1041
- if ! guard_pr_branch_verification; then
1042
- handle_verification_guard_block
1043
- return 0
1044
- fi
1045
-
1046
- host_commit_pr_worktree_changes
1047
- remaining_conflict_paths="$(remaining_merge_conflict_paths "$pr_base_ref" || true)"
1048
- if [[ -n "$remaining_conflict_paths" ]]; then
1049
- append_host_guard_comment "$pr_base_ref" "$remaining_conflict_paths"
1050
- # Keep host-guard notes local when nothing was pushed. Posting them to GitHub
1051
- # makes unchanged PR heads look like successful repairs and can re-queue review.
1052
- require_transition "pr_schedule_retry" pr_schedule_retry "remaining-merge-conflicts-after-updated-branch"
1053
- require_transition "pr_after_blocked" pr_after_blocked "$pr_number"
1054
- cleanup_pr_session
1055
- result_outcome="blocked"
1056
- result_action="host-rejected-partial-repair"
1057
- notify_pr_reconciled
1058
- return 0
1059
- fi
1060
-
1061
- if ! ahead_count="$(pr_branch_commits_ahead_remote_count)"; then
1062
- failure_reason="updated-branch-remote-ref-unavailable"
1063
- require_transition "pr_schedule_retry" pr_schedule_retry "$failure_reason"
1064
- require_transition "pr_after_blocked" pr_after_blocked "$pr_number"
1065
- cleanup_pr_session
1066
- result_outcome="blocked"
1067
- result_action="host-branch-ahead-check-failed"
1068
- notify_pr_reconciled
1069
- return 0
1070
- fi
1071
-
1072
- if [[ "${ahead_count}" == "0" ]]; then
1073
- failure_reason="updated-branch-no-commits-ahead"
1074
- require_transition "pr_schedule_retry" pr_schedule_retry "$failure_reason"
1075
- require_transition "pr_after_blocked" pr_after_blocked "$pr_number"
1076
- cleanup_pr_session
1077
- result_outcome="blocked"
1078
- result_action="host-noop-updated-branch"
1079
- notify_pr_reconciled
1080
- return 0
1081
- fi
1082
-
1083
- push_pr_branch
1084
- post_pr_comment_if_present || maybe_handle_host_github_rate_limit "github-api-rate-limit" "host-comment-rate-limit-retry"
1085
- require_transition "pr_clear_retry" pr_clear_retry
1086
- require_transition "pr_after_updated_branch" pr_after_updated_branch "$pr_number"
1087
- cleanup_pr_session
1088
- result_action="${result_action:-pushed-branch-for-ci}"
1089
- notify_pr_reconciled
1090
- }
1091
-
1092
- if [[ "$status" == "SUCCEEDED" ]] && ! normalize_pr_result_contract; then
1093
- status="FAILED"
1094
- failure_reason="invalid-result-contract"
1095
- pr_result_contract_note="invalid-result-contract"
1096
- require_transition "pr_schedule_retry" pr_schedule_retry "$failure_reason"
1097
- require_transition "pr_after_failed" pr_after_failed "$pr_number"
1098
- cleanup_pr_session
1099
- result_outcome="invalid-contract"
1100
- result_action="queued-pr-retry"
1101
- notify_pr_reconciled
1102
- elif [[ "$pr_state" == "MERGED" ]]; then
1103
- status="SUCCEEDED"
1104
- failure_reason=""
1105
- clear_provider_quota_cooldown
1106
- require_transition "pr_clear_retry" pr_clear_retry
1107
- require_transition "handle_linked_issue_merge_cleanup" handle_linked_issue_merge_cleanup "$result_issue_id"
1108
- require_transition "pr_after_merged" pr_after_merged "$pr_number"
1109
- require_transition "pr_cleanup_merged_residue" pr_cleanup_merged_residue "$pr_number"
1110
- cleanup_pr_session
1111
- result_outcome="${result_outcome:-merged}"
1112
- result_action="${result_action:-approved-and-merged}"
1113
- notify_pr_reconciled
1114
- elif [[ "$pr_state" == "CLOSED" ]]; then
1115
- status="SUCCEEDED"
1116
- failure_reason=""
1117
- clear_provider_quota_cooldown
1118
- require_transition "pr_clear_retry" pr_clear_retry
1119
- require_transition "pr_after_closed" pr_after_closed "$pr_number"
1120
- cleanup_pr_session
1121
- result_outcome="${result_outcome:-closed}"
1122
- result_action="${result_action:-cleaned-closed-pr}"
1123
- notify_pr_reconciled
1124
- elif [[ "$status" == "SUCCEEDED" && "$result_outcome" == "approved-local-review-passed" ]]; then
1125
- if ! review_pass_action="$(review_pass_action_from_result_action "${result_action:-}" 2>/dev/null)"; then
1126
- review_pass_action="$(pr_review_pass_action "$pr_number")"
1127
- fi
1128
- case "$review_pass_action" in
1129
- advance-double-check-2)
1130
- require_transition "pr_clear_retry" pr_clear_retry
1131
- require_transition "pr_after_double_check_advanced" pr_after_double_check_advanced "$pr_number" "2"
1132
- cleanup_pr_session
1133
- result_outcome="double-check-1-approved"
1134
- result_action="queued-double-check-2"
1135
- notify_pr_reconciled
1136
- ;;
1137
- wait-human)
1138
- require_transition "pr_clear_retry" pr_clear_retry
1139
- require_transition "pr_after_succeeded" pr_after_succeeded "$pr_number"
1140
- cleanup_pr_session
1141
- result_outcome="waiting-human-review"
1142
- result_action="queued-human-review"
1143
- notify_pr_reconciled
1144
- ;;
1145
- merge|"")
1146
- if [[ "$(pr_automerge_allowed "$pr_number")" != "yes" ]]; then
1147
- echo "PR ${pr_number} is no longer eligible for auto-merge" >&2
1148
- exit 1
1149
- fi
1150
-
1151
- require_transition "pr_clear_retry" pr_clear_retry
1152
- if ! approve_and_merge; then
1153
- if [[ "${host_github_rate_limited}" == "yes" ]]; then
1154
- handle_host_github_rate_limit_retry "github-api-rate-limit" "host-merge-rate-limit-retry"
1155
- fi
1156
- exit 1
1157
- fi
1158
- pr_state="MERGED"
1159
- if [[ "$pr_state" != "MERGED" ]]; then
1160
- echo "PR ${pr_number} did not merge successfully" >&2
1161
- exit 1
1162
- fi
1163
-
1164
- require_transition "handle_linked_issue_merge_cleanup" handle_linked_issue_merge_cleanup "$result_issue_id"
1165
- require_transition "pr_after_merged" pr_after_merged "$pr_number"
1166
- require_transition "pr_cleanup_merged_residue" pr_cleanup_merged_residue "$pr_number"
1167
- cleanup_pr_session
1168
- result_outcome="merged"
1169
- result_action="approved-and-merged"
1170
- notify_pr_reconciled
1171
- ;;
1172
- *)
1173
- echo "unsupported review pass action for PR ${pr_number}: ${review_pass_action}" >&2
1174
- exit 1
1175
- ;;
1176
- esac
1177
- elif [[ "$status" == "SUCCEEDED" && "$result_outcome" == "updated-branch" ]]; then
1178
- handle_updated_branch_result
1179
- elif [[ "$status" == "SUCCEEDED" && "$result_outcome" == "no-change-needed" ]]; then
1180
- if merge_state_prepared; then
1181
- remaining_conflict_paths="$(remaining_merge_conflict_paths "$pr_base_ref" || true)"
1182
- if [[ -n "$remaining_conflict_paths" ]]; then
1183
- append_host_guard_comment "$pr_base_ref" "$remaining_conflict_paths"
1184
- post_pr_comment_if_present
1185
- require_transition "pr_schedule_retry" pr_schedule_retry "remaining-merge-conflicts-after-no-change-needed"
1186
- require_transition "pr_after_blocked" pr_after_blocked "$pr_number"
1187
- cleanup_pr_session
1188
- result_outcome="blocked"
1189
- result_action="host-rejected-no-change-needed"
1190
- notify_pr_reconciled
1191
- else
1192
- if ! guard_pr_branch_verification; then
1193
- handle_verification_guard_block
1194
- else
1195
- host_commit_pr_worktree_changes
1196
- if ! ahead_count="$(pr_branch_commits_ahead_remote_count)"; then
1197
- failure_reason="no-change-needed-remote-ref-unavailable"
1198
- require_transition "pr_schedule_retry" pr_schedule_retry "$failure_reason"
1199
- require_transition "pr_after_blocked" pr_after_blocked "$pr_number"
1200
- cleanup_pr_session
1201
- result_outcome="blocked"
1202
- result_action="host-branch-ahead-check-failed"
1203
- elif [[ "${ahead_count}" == "0" ]]; then
1204
- failure_reason="no-change-needed-promoted-without-branch-advance"
1205
- require_transition "pr_schedule_retry" pr_schedule_retry "$failure_reason"
1206
- require_transition "pr_after_blocked" pr_after_blocked "$pr_number"
1207
- cleanup_pr_session
1208
- result_outcome="blocked"
1209
- result_action="host-rejected-noop-promotion"
1210
- else
1211
- push_pr_branch
1212
- post_pr_comment_if_present || maybe_handle_host_github_rate_limit "github-api-rate-limit" "host-comment-rate-limit-retry"
1213
- require_transition "pr_clear_retry" pr_clear_retry
1214
- require_transition "pr_after_updated_branch" pr_after_updated_branch "$pr_number"
1215
- cleanup_pr_session
1216
- result_outcome="updated-branch"
1217
- result_action="host-promoted-no-change-needed-to-updated-branch"
1218
- fi
1219
- notify_pr_reconciled
1220
- fi
1221
- fi
1222
- else
1223
- remaining_conflict_paths="$(remaining_merge_conflict_paths "$pr_base_ref" || true)"
1224
- if [[ -n "$remaining_conflict_paths" ]]; then
1225
- append_host_guard_comment "$pr_base_ref" "$remaining_conflict_paths"
1226
- # Keep host-guard notes local when nothing was pushed. Posting them to GitHub
1227
- # makes unchanged PR heads look like successful repairs and can re-queue review.
1228
- require_transition "pr_schedule_retry" pr_schedule_retry "remaining-merge-conflicts-after-no-change-needed"
1229
- require_transition "pr_after_blocked" pr_after_blocked "$pr_number"
1230
- cleanup_pr_session
1231
- result_outcome="blocked"
1232
- result_action="host-rejected-no-change-needed"
1233
- notify_pr_reconciled
1234
- else
1235
- post_pr_comment_if_present || maybe_handle_host_github_rate_limit "github-api-rate-limit" "host-comment-rate-limit-retry"
1236
- require_transition "pr_clear_retry" pr_clear_retry
1237
- require_transition "pr_after_succeeded" pr_after_succeeded "$pr_number"
1238
- cleanup_pr_session
1239
- result_action="${result_action:-refreshed-pr-state}"
1240
- notify_pr_reconciled
1241
- fi
1242
- fi
1243
- elif [[ "$status" == "SUCCEEDED" && "$result_outcome" == "blocked" ]]; then
1244
- blocked_runtime_reason="$(classify_pr_blocked_runtime_reason || true)"
1245
- if [[ -n "${blocked_runtime_reason:-}" ]]; then
1246
- status="FAILED"
1247
- failure_reason="${blocked_runtime_reason}"
1248
- require_transition "pr_schedule_retry" pr_schedule_retry "$failure_reason"
1249
- require_transition "pr_after_failed" pr_after_failed "$pr_number"
1250
- cleanup_pr_session
1251
- result_action="queued-pr-retry"
1252
- notify_pr_reconciled
1253
- elif attempt_blocked_pr_host_verification_recovery; then
1254
- handle_updated_branch_result
1255
- else
1256
- post_pr_comment_if_present || maybe_handle_host_github_rate_limit "github-api-rate-limit" "host-comment-rate-limit-retry"
1257
- require_transition "pr_clear_retry" pr_clear_retry
1258
- require_transition "pr_after_blocked" pr_after_blocked "$pr_number"
1259
- cleanup_pr_session
1260
- result_action="${result_action:-queued-pr-fix}"
1261
- notify_pr_reconciled
1262
- fi
1263
- elif [[ "$status" == "SUCCEEDED" ]]; then
1264
- clear_provider_quota_cooldown
1265
- require_transition "pr_clear_retry" pr_clear_retry
1266
- require_transition "pr_after_succeeded" pr_after_succeeded "$pr_number"
1267
- cleanup_pr_session
1268
- notify_pr_reconciled
1269
- elif [[ "$status" == "FAILED" ]]; then
1270
- schedule_provider_quota_cooldown "${failure_reason:-worker-exit-failed}"
1271
- require_transition "pr_schedule_retry" pr_schedule_retry "${failure_reason:-worker-exit-failed}"
1272
- require_transition "pr_after_failed" pr_after_failed "$pr_number"
1273
- cleanup_pr_session
1274
- notify_pr_reconciled
1275
- fi
1276
-
1277
- mark_reconciled
1278
- printf 'STATUS=%s\n' "$status"
1279
- printf 'PR_NUMBER=%s\n' "$pr_number"
1280
- printf 'PR_STATE=%s\n' "$pr_state"
1281
- printf 'OUTCOME=%s\n' "${result_outcome:-unknown}"
1282
- printf 'ACTION=%s\n' "${result_action:-unknown}"
1283
- if [[ -n "$failure_reason" ]]; then
1284
- printf 'FAILURE_REASON=%s\n' "$failure_reason"
1285
- fi
1286
- if [[ -n "$pr_result_contract_note" ]]; then
1287
- printf 'RESULT_CONTRACT_NOTE=%s\n' "$pr_result_contract_note"
1288
- fi