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