agent-control-plane 0.2.0 → 0.4.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/README.md +69 -19
  2. package/assets/workflow-catalog.json +1 -1
  3. package/bin/pr-risk.sh +22 -7
  4. package/bin/sync-pr-labels.sh +1 -1
  5. package/hooks/heartbeat-hooks.sh +125 -12
  6. package/hooks/issue-reconcile-hooks.sh +1 -1
  7. package/hooks/pr-reconcile-hooks.sh +1 -1
  8. package/npm/bin/agent-control-plane.js +296 -61
  9. package/package.json +11 -7
  10. package/tools/bin/agent-github-update-labels +36 -2
  11. package/tools/bin/agent-project-catch-up-merged-prs +4 -2
  12. package/tools/bin/agent-project-cleanup-session +49 -5
  13. package/tools/bin/agent-project-heartbeat-loop +119 -1471
  14. package/tools/bin/agent-project-publish-issue-pr +6 -3
  15. package/tools/bin/agent-project-reconcile-issue-session +78 -106
  16. package/tools/bin/agent-project-reconcile-pr-session +166 -143
  17. package/tools/bin/agent-project-retry-state +18 -7
  18. package/tools/bin/agent-project-run-claude-session +10 -0
  19. package/tools/bin/agent-project-run-codex-resilient +99 -14
  20. package/tools/bin/agent-project-run-codex-session +16 -5
  21. package/tools/bin/agent-project-run-kilo-session +10 -0
  22. package/tools/bin/agent-project-run-openclaw-session +10 -0
  23. package/tools/bin/agent-project-run-opencode-session +10 -0
  24. package/tools/bin/agent-project-sync-source-repo-main +163 -0
  25. package/tools/bin/agent-project-worker-status +10 -7
  26. package/tools/bin/cleanup-worktree.sh +6 -1
  27. package/tools/bin/flow-config-lib.sh +1257 -34
  28. package/tools/bin/flow-resident-worker-lib.sh +119 -1
  29. package/tools/bin/flow-shell-lib.sh +56 -0
  30. package/tools/bin/github-core-rate-limit-state.sh +77 -0
  31. package/tools/bin/github-write-outbox.sh +470 -0
  32. package/tools/bin/heartbeat-loop-cache-lib.sh +164 -0
  33. package/tools/bin/heartbeat-loop-counting-lib.sh +306 -0
  34. package/tools/bin/heartbeat-loop-pr-strategy-lib.sh +199 -0
  35. package/tools/bin/heartbeat-loop-scheduling-lib.sh +506 -0
  36. package/tools/bin/heartbeat-loop-worker-lib.sh +319 -0
  37. package/tools/bin/heartbeat-recovery-preflight.sh +12 -1
  38. package/tools/bin/heartbeat-safe-auto.sh +56 -3
  39. package/tools/bin/install-project-launchd.sh +17 -2
  40. package/tools/bin/project-init.sh +21 -1
  41. package/tools/bin/project-launchd-bootstrap.sh +16 -9
  42. package/tools/bin/project-runtimectl.sh +46 -2
  43. package/tools/bin/reconcile-bootstrap-lib.sh +113 -0
  44. package/tools/bin/resident-issue-controller-lib.sh +448 -0
  45. package/tools/bin/scaffold-profile.sh +61 -3
  46. package/tools/bin/start-pr-fix-worker.sh +47 -10
  47. package/tools/bin/start-resident-issue-loop.sh +28 -439
  48. package/tools/dashboard/app.js +37 -1
  49. package/tools/dashboard/dashboard_snapshot.py +65 -26
  50. package/tools/templates/pr-fix-template.md +3 -1
  51. package/tools/templates/pr-merge-repair-template.md +2 -1
  52. package/SKILL.md +0 -149
  53. package/references/architecture.md +0 -217
  54. package/references/commands.md +0 -128
  55. package/references/control-plane-map.md +0 -124
  56. package/references/docs-map.md +0 -73
  57. package/references/release-checklist.md +0 -65
  58. package/references/repo-map.md +0 -36
  59. package/tools/bin/split-retained-slice.sh +0 -124
@@ -186,9 +186,12 @@ fi
186
186
  resolve_actor_login() {
187
187
  local login=""
188
188
 
189
- flow_export_github_cli_auth_env "${repo_slug}"
190
- login="$(gh api user --jq .login 2>/dev/null || true)"
191
- if [[ -z "${login}" ]]; then
189
+ login="$(flow_github_current_login 2>/dev/null || true)"
190
+ if [[ -z "${login}" && ! "$(flow_forge_provider)" =~ ^gitea$ ]]; then
191
+ flow_export_github_cli_auth_env "${repo_slug}"
192
+ login="$(gh api user --jq .login 2>/dev/null || true)"
193
+ fi
194
+ if [[ -z "${login}" && ! "$(flow_forge_provider)" =~ ^gitea$ ]]; then
192
195
  login="$(
193
196
  gh auth status 2>/dev/null \
194
197
  | sed -n 's/^ ✓ Logged in to github.com account \([^ ]*\) (.*/\1/p' \
@@ -2,82 +2,33 @@
2
2
  set -euo pipefail
3
3
 
4
4
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
5
-
6
- bootstrap_flow_shell_lib() {
7
- local candidate=""
8
- local skill_name=""
9
-
10
- for candidate in \
11
- "${SCRIPT_DIR}/flow-shell-lib.sh" \
12
- "${AGENT_CONTROL_PLANE_ROOT:-}/tools/bin/flow-shell-lib.sh" \
13
- "${ACP_ROOT:-}/tools/bin/flow-shell-lib.sh" \
14
- "${F_LOSNING_FLOW_ROOT:-}/tools/bin/flow-shell-lib.sh" \
15
- "${AGENT_FLOW_SKILL_ROOT:-}/tools/bin/flow-shell-lib.sh" \
16
- "${SHARED_AGENT_HOME:-}/tools/bin/flow-shell-lib.sh" \
17
- "$(pwd)/tools/bin/flow-shell-lib.sh"; do
18
- if [[ -n "${candidate}" && -f "${candidate}" ]]; then
19
- printf '%s\n' "${candidate}"
20
- return 0
21
- fi
22
- done
23
-
24
- if [[ -n "${SHARED_AGENT_HOME:-}" ]]; then
25
- for skill_name in "${AGENT_CONTROL_PLANE_SKILL_NAME:-agent-control-plane}" "${AGENT_CONTROL_PLANE_COMPAT_ALIAS:-}"; do
26
- [[ -n "${skill_name}" ]] || continue
27
- candidate="${SHARED_AGENT_HOME}/skills/openclaw/${skill_name}/tools/bin/flow-shell-lib.sh"
28
- if [[ -f "${candidate}" ]]; then
29
- printf '%s\n' "${candidate}"
30
- return 0
31
- fi
32
- done
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
33
14
  fi
34
-
35
- echo "unable to locate flow-shell-lib.sh for reconcile bootstrap" >&2
36
- return 1
37
- }
38
-
39
- FLOW_SHELL_LIB_PATH="$(bootstrap_flow_shell_lib)"
40
- BOOTSTRAP_TOOLS_DIR="$(cd "$(dirname "${FLOW_SHELL_LIB_PATH}")" && pwd)"
41
- # shellcheck source=/dev/null
42
- source "${FLOW_SHELL_LIB_PATH}"
43
-
44
- resolve_reconcile_tools_dir() {
45
- local candidate_root=""
46
- local skill_name=""
47
-
48
- for candidate_root in \
49
- "${AGENT_CONTROL_PLANE_ROOT:-}" \
50
- "${ACP_ROOT:-}" \
51
- "${F_LOSNING_FLOW_ROOT:-}" \
52
- "${AGENT_FLOW_SKILL_ROOT:-}"; do
53
- if [[ -n "${candidate_root}" && -d "${candidate_root}/tools/bin" ]]; then
54
- printf '%s/tools/bin\n' "${candidate_root}"
55
- return 0
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
56
23
  fi
57
24
  done
58
-
59
- if [[ -n "${SHARED_AGENT_HOME:-}" ]]; then
60
- if [[ -d "${SHARED_AGENT_HOME}/tools/bin" ]]; then
61
- printf '%s/tools/bin\n' "${SHARED_AGENT_HOME}"
62
- return 0
63
- fi
64
- for skill_name in "${AGENT_CONTROL_PLANE_SKILL_NAME:-agent-control-plane}" "${AGENT_CONTROL_PLANE_COMPAT_ALIAS:-}"; do
65
- [[ -n "${skill_name}" ]] || continue
66
- candidate_root="${SHARED_AGENT_HOME}/skills/openclaw/${skill_name}"
67
- if [[ -d "${candidate_root}/tools/bin" ]]; then
68
- printf '%s/tools/bin\n' "${candidate_root}"
69
- return 0
70
- fi
71
- done
72
- fi
73
-
74
- if [[ -d "${SCRIPT_DIR}" ]]; then
75
- printf '%s\n' "${SCRIPT_DIR}"
76
- return 0
77
- fi
78
-
79
- printf '%s\n' "${BOOTSTRAP_TOOLS_DIR}"
80
- }
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}"
81
32
 
82
33
  usage() {
83
34
  cat <<'EOF'
@@ -89,30 +40,8 @@ allowing project adapters to inject policy hooks.
89
40
  EOF
90
41
  }
91
42
 
92
- shared_tools_dir="$(resolve_reconcile_tools_dir)"
93
- resolve_reconcile_helper_path() {
94
- local helper_name="${1:?helper name required}"
95
- local candidate=""
96
-
97
- for candidate in \
98
- "${SCRIPT_DIR}/${helper_name}" \
99
- "${BOOTSTRAP_TOOLS_DIR}/${helper_name}" \
100
- "${shared_tools_dir}/${helper_name}"; do
101
- if [[ -n "${candidate}" && -f "${candidate}" ]]; then
102
- printf '%s\n' "${candidate}"
103
- return 0
104
- fi
105
- done
106
-
107
- echo "unable to locate ${helper_name} for reconcile bootstrap" >&2
108
- return 1
109
- }
110
-
111
- FLOW_CONFIG_LIB_PATH="$(resolve_reconcile_helper_path "flow-config-lib.sh")"
112
43
  FLOW_RESIDENT_WORKER_LIB_PATH="$(resolve_reconcile_helper_path "flow-resident-worker-lib.sh")"
113
44
  # shellcheck source=/dev/null
114
- source "${FLOW_CONFIG_LIB_PATH}"
115
- # shellcheck source=/dev/null
116
45
  source "${FLOW_RESIDENT_WORKER_LIB_PATH}"
117
46
  session=""
118
47
  repo_slug=""
@@ -302,6 +231,7 @@ if [[ -n "$hook_file" && -f "$hook_file" ]]; then
302
231
  fi
303
232
 
304
233
  provider_cooldown_script="${shared_tools_dir}/provider-cooldown-state.sh"
234
+ github_write_outbox_script="${shared_tools_dir}/github-write-outbox.sh"
305
235
 
306
236
  schedule_provider_quota_cooldown() {
307
237
  local reason="${1:-provider-quota-limit}"
@@ -406,7 +336,17 @@ post_issue_comment_if_present() {
406
336
  if issue_latest_comment_matches_artifact; then
407
337
  return 0
408
338
  fi
409
- flow_github_api_repo "${repo_slug}" "issues/${issue_id}/comments" --method POST -f body="$(cat "$comment_file")" >/dev/null || true
339
+ if flow_github_api_repo "${repo_slug}" "issues/${issue_id}/comments" --method POST -f body="$(cat "$comment_file")" >/dev/null 2>&1; then
340
+ return 0
341
+ fi
342
+ if [[ -x "${github_write_outbox_script}" ]]; then
343
+ "${github_write_outbox_script}" enqueue-comment \
344
+ --repo-slug "${repo_slug}" \
345
+ --number "${issue_id}" \
346
+ --kind issue \
347
+ --body-file "${comment_file}" >/dev/null 2>&1 || true
348
+ fi
349
+ return 0
410
350
  }
411
351
 
412
352
  issue_latest_comment_matches_artifact() {
@@ -1053,15 +993,6 @@ extract_recovery_worktree_from_publish_output() {
1053
993
  awk -F= '/^RECOVERY_WORKTREE=/{print $2}' <<<"$publish_out" | tail -n 1
1054
994
  }
1055
995
 
1056
- require_transition() {
1057
- local step="${1:?step required}"
1058
- shift
1059
- if ! "$@"; then
1060
- echo "reconcile transition failed: ${step}" >&2
1061
- exit 1
1062
- fi
1063
- }
1064
-
1065
996
  mark_reconciled() {
1066
997
  local reconciled_at tmp_file
1067
998
  if [[ -d "$run_dir" ]]; then
@@ -1141,6 +1072,40 @@ update_resident_issue_metadata() {
1141
1072
  "LAST_WORKTREE_REUSED=${last_worktree_reused:-no}"
1142
1073
  }
1143
1074
 
1075
+ cleanup_output_value() {
1076
+ local cleanup_output="${1:-}"
1077
+ local key="${2:?key required}"
1078
+ awk -F= -v target_key="${key}" '$1 == target_key { print substr($0, index($0, "=") + 1); exit }' <<<"${cleanup_output}"
1079
+ }
1080
+
1081
+ warn_cleanup_issue_session() {
1082
+ local cleanup_output="${1:-}"
1083
+ local cleanup_exit="${2:-0}"
1084
+ local cleanup_status=""
1085
+ local cleanup_mode=""
1086
+ local cleanup_error=""
1087
+
1088
+ cleanup_status="$(cleanup_output_value "${cleanup_output}" "CLEANUP_STATUS")"
1089
+ if [[ -z "${cleanup_status}" ]]; then
1090
+ cleanup_status="${cleanup_exit}"
1091
+ fi
1092
+ [[ "${cleanup_status}" != "0" ]] || return 0
1093
+
1094
+ cleanup_mode="$(cleanup_output_value "${cleanup_output}" "CLEANUP_MODE")"
1095
+ cleanup_error="$(cleanup_output_value "${cleanup_output}" "CLEANUP_ERROR")"
1096
+ printf '[%s] issue cleanup warning session=%s status=%s mode=%s\n' \
1097
+ "$(date -u +"%Y-%m-%dT%H:%M:%SZ")" \
1098
+ "${session}" \
1099
+ "${cleanup_status}" \
1100
+ "${cleanup_mode:-unknown}" >&2
1101
+ if [[ -n "${cleanup_error}" ]]; then
1102
+ printf '[%s] issue cleanup detail session=%s %s\n' \
1103
+ "$(date -u +"%Y-%m-%dT%H:%M:%SZ")" \
1104
+ "${session}" \
1105
+ "${cleanup_error}" >&2
1106
+ fi
1107
+ }
1108
+
1144
1109
  cleanup_issue_session() {
1145
1110
  local -a cleanup_args=(
1146
1111
  --repo-root "$repo_root"
@@ -1156,7 +1121,14 @@ cleanup_issue_session() {
1156
1121
  cleanup_args+=(--skip-worktree-cleanup)
1157
1122
  fi
1158
1123
 
1159
- "${shared_tools_dir}/agent-project-cleanup-session" "${cleanup_args[@]}" >/dev/null
1124
+ local cleanup_output=""
1125
+ local cleanup_exit="0"
1126
+ if cleanup_output="$("${shared_tools_dir}/agent-project-cleanup-session" "${cleanup_args[@]}" 2>&1)"; then
1127
+ cleanup_exit="0"
1128
+ else
1129
+ cleanup_exit="$?"
1130
+ fi
1131
+ warn_cleanup_issue_session "${cleanup_output}" "${cleanup_exit}"
1160
1132
  }
1161
1133
 
1162
1134
  notify_issue_reconciled() {
@@ -2,82 +2,33 @@
2
2
  set -euo pipefail
3
3
 
4
4
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
5
-
6
- bootstrap_flow_shell_lib() {
7
- local candidate=""
8
- local skill_name=""
9
-
10
- for candidate in \
11
- "${SCRIPT_DIR}/flow-shell-lib.sh" \
12
- "${AGENT_CONTROL_PLANE_ROOT:-}/tools/bin/flow-shell-lib.sh" \
13
- "${ACP_ROOT:-}/tools/bin/flow-shell-lib.sh" \
14
- "${F_LOSNING_FLOW_ROOT:-}/tools/bin/flow-shell-lib.sh" \
15
- "${AGENT_FLOW_SKILL_ROOT:-}/tools/bin/flow-shell-lib.sh" \
16
- "${SHARED_AGENT_HOME:-}/tools/bin/flow-shell-lib.sh" \
17
- "$(pwd)/tools/bin/flow-shell-lib.sh"; do
18
- if [[ -n "${candidate}" && -f "${candidate}" ]]; then
19
- printf '%s\n' "${candidate}"
20
- return 0
21
- fi
22
- done
23
-
24
- if [[ -n "${SHARED_AGENT_HOME:-}" ]]; then
25
- for skill_name in "${AGENT_CONTROL_PLANE_SKILL_NAME:-agent-control-plane}" "${AGENT_CONTROL_PLANE_COMPAT_ALIAS:-}"; do
26
- [[ -n "${skill_name}" ]] || continue
27
- candidate="${SHARED_AGENT_HOME}/skills/openclaw/${skill_name}/tools/bin/flow-shell-lib.sh"
28
- if [[ -f "${candidate}" ]]; then
29
- printf '%s\n' "${candidate}"
30
- return 0
31
- fi
32
- done
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
33
14
  fi
34
-
35
- echo "unable to locate flow-shell-lib.sh for reconcile bootstrap" >&2
36
- return 1
37
- }
38
-
39
- FLOW_SHELL_LIB_PATH="$(bootstrap_flow_shell_lib)"
40
- BOOTSTRAP_TOOLS_DIR="$(cd "$(dirname "${FLOW_SHELL_LIB_PATH}")" && pwd)"
41
- # shellcheck source=/dev/null
42
- source "${FLOW_SHELL_LIB_PATH}"
43
-
44
- resolve_reconcile_tools_dir() {
45
- local candidate_root=""
46
- local skill_name=""
47
-
48
- for candidate_root in \
49
- "${AGENT_CONTROL_PLANE_ROOT:-}" \
50
- "${ACP_ROOT:-}" \
51
- "${F_LOSNING_FLOW_ROOT:-}" \
52
- "${AGENT_FLOW_SKILL_ROOT:-}"; do
53
- if [[ -n "${candidate_root}" && -d "${candidate_root}/tools/bin" ]]; then
54
- printf '%s/tools/bin\n' "${candidate_root}"
55
- return 0
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
56
23
  fi
57
24
  done
58
-
59
- if [[ -n "${SHARED_AGENT_HOME:-}" ]]; then
60
- if [[ -d "${SHARED_AGENT_HOME}/tools/bin" ]]; then
61
- printf '%s/tools/bin\n' "${SHARED_AGENT_HOME}"
62
- return 0
63
- fi
64
- for skill_name in "${AGENT_CONTROL_PLANE_SKILL_NAME:-agent-control-plane}" "${AGENT_CONTROL_PLANE_COMPAT_ALIAS:-}"; do
65
- [[ -n "${skill_name}" ]] || continue
66
- candidate_root="${SHARED_AGENT_HOME}/skills/openclaw/${skill_name}"
67
- if [[ -d "${candidate_root}/tools/bin" ]]; then
68
- printf '%s/tools/bin\n' "${candidate_root}"
69
- return 0
70
- fi
71
- done
72
- fi
73
-
74
- if [[ -d "${SCRIPT_DIR}" ]]; then
75
- printf '%s\n' "${SCRIPT_DIR}"
76
- return 0
77
- fi
78
-
79
- printf '%s\n' "${BOOTSTRAP_TOOLS_DIR}"
80
- }
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}"
81
32
 
82
33
  usage() {
83
34
  cat <<'EOF'
@@ -89,28 +40,6 @@ allowing project adapters to inject policy hooks.
89
40
  EOF
90
41
  }
91
42
 
92
- shared_tools_dir="$(resolve_reconcile_tools_dir)"
93
- resolve_reconcile_helper_path() {
94
- local helper_name="${1:?helper name required}"
95
- local candidate=""
96
-
97
- for candidate in \
98
- "${SCRIPT_DIR}/${helper_name}" \
99
- "${BOOTSTRAP_TOOLS_DIR}/${helper_name}" \
100
- "${shared_tools_dir}/${helper_name}"; do
101
- if [[ -n "${candidate}" && -f "${candidate}" ]]; then
102
- printf '%s\n' "${candidate}"
103
- return 0
104
- fi
105
- done
106
-
107
- echo "unable to locate ${helper_name} for reconcile bootstrap" >&2
108
- return 1
109
- }
110
-
111
- FLOW_CONFIG_LIB_PATH="$(resolve_reconcile_helper_path "flow-config-lib.sh")"
112
- # shellcheck source=/dev/null
113
- source "${FLOW_CONFIG_LIB_PATH}"
114
43
  verification_guard_script="${shared_tools_dir}/branch-verification-guard.sh"
115
44
  session=""
116
45
  repo_slug=""
@@ -222,6 +151,9 @@ if [[ -n "$hook_file" && -f "$hook_file" ]]; then
222
151
  fi
223
152
 
224
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."
225
157
 
226
158
  schedule_provider_quota_cooldown() {
227
159
  local reason="${1:-provider-quota-limit}"
@@ -243,6 +175,26 @@ blocked_runtime_reason=""
243
175
  host_github_rate_limited="no"
244
176
  host_github_rate_limit_detail=""
245
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
+
246
198
  owner="${repo_slug%%/*}"
247
199
  repo="${repo_slug#*/}"
248
200
  pr_view_json="$(flow_github_pr_view_json "$repo_slug" "$pr_number")"
@@ -257,6 +209,40 @@ if [[ "$status" == "RUNNING" && "$pr_state" != "MERGED" && "$pr_state" != "CLOSE
257
209
  exit 0
258
210
  fi
259
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
+
260
246
  review_pass_action_from_result_action() {
261
247
  case "${1:-}" in
262
248
  host-advance-double-check-2)
@@ -402,9 +388,27 @@ post_pr_comment_if_present() {
402
388
  if pr_comment_already_posted; then
403
389
  return 0
404
390
  fi
405
- if ! host_github_post_issue_comment "${pr_number}" "$(cat "$comment_file")"; then
406
- return 1
391
+ if host_github_post_issue_comment "${pr_number}" "$(cat "$comment_file")"; then
392
+ return 0
407
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
408
412
  }
409
413
 
410
414
  pr_comment_already_posted() {
@@ -427,6 +431,9 @@ record_host_github_rate_limit() {
427
431
  host_github_rate_limited="yes"
428
432
  host_github_rate_limit_detail="${output}"
429
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
430
437
  }
431
438
 
432
439
  host_github_post_issue_comment() {
@@ -434,9 +441,13 @@ host_github_post_issue_comment() {
434
441
  local body="${2:-}"
435
442
  local output=""
436
443
 
444
+ if host_github_rate_limit_state_active; then
445
+ return 1
446
+ fi
447
+
437
448
  flow_export_github_cli_auth_env "${repo_slug}"
438
449
  if output="$(
439
- gh api "repos/${repo_slug}/issues/${issue_number}/comments" \
450
+ flow_github_api_repo "${repo_slug}" "issues/${issue_number}/comments" \
440
451
  --method POST \
441
452
  -f body="${body}" 2>&1
442
453
  )"; then
@@ -455,23 +466,25 @@ host_github_post_issue_comment() {
455
466
  host_github_submit_pr_approval() {
456
467
  local output=""
457
468
 
469
+ if host_github_rate_limit_state_active; then
470
+ enqueue_pr_approval_intent || true
471
+ return 1
472
+ fi
473
+
458
474
  flow_export_github_cli_auth_env "${repo_slug}"
459
475
  if output="$(
460
- gh api "repos/${repo_slug}/pulls/${pr_number}/reviews" \
461
- --method POST \
462
- -f event=APPROVE \
463
- -f body="Automated final review passed. Safe low-risk scope, green checks, and host-side merge approved." \
464
- 2>&1
476
+ flow_github_pr_review_approve "${repo_slug}" "${pr_number}" "${automated_pr_approval_body}" 2>&1
465
477
  )"; then
466
478
  return 0
467
479
  fi
468
480
 
469
- if grep -q "Can not approve your own pull request" <<<"${output}"; then
481
+ if grep -q "Can not approve your own pull request" <<<"${output}" || grep -q "approve your own pull is not allowed" <<<"${output}"; then
470
482
  return 0
471
483
  fi
472
484
 
473
485
  if host_github_output_indicates_rate_limit "${output}"; then
474
486
  record_host_github_rate_limit "${output}"
487
+ enqueue_pr_approval_intent || true
475
488
  return 1
476
489
  fi
477
490
 
@@ -684,15 +697,6 @@ attempt_blocked_pr_host_verification_recovery() {
684
697
  return 0
685
698
  }
686
699
 
687
- require_transition() {
688
- local step="${1:?step required}"
689
- shift
690
- if ! "$@"; then
691
- echo "reconcile transition failed: ${step}" >&2
692
- exit 1
693
- fi
694
- }
695
-
696
700
  close_linked_issue_if_open() {
697
701
  local issue_id="${1:-}"
698
702
  [[ -n "$issue_id" ]] || return 0
@@ -889,13 +893,21 @@ merge_state_prepared() {
889
893
  }
890
894
 
891
895
  current_github_login() {
896
+ if host_github_rate_limit_state_active; then
897
+ printf '\n'
898
+ return 0
899
+ fi
892
900
  flow_export_github_cli_auth_env "${repo_slug}"
893
- gh api user --jq '.login // ""' 2>/dev/null || true
901
+ flow_github_current_login
894
902
  }
895
903
 
896
904
  pr_author_login() {
905
+ if host_github_rate_limit_state_active; then
906
+ printf '\n'
907
+ return 0
908
+ fi
897
909
  flow_export_github_cli_auth_env "${repo_slug}"
898
- gh pr view "${pr_number}" -R "${repo_slug}" --json author --jq '.author.login // ""' 2>/dev/null || true
910
+ flow_github_pr_author_login "${repo_slug}" "${pr_number}"
899
911
  }
900
912
 
901
913
  pr_is_self_authored_for_current_actor() {
@@ -908,8 +920,12 @@ pr_is_self_authored_for_current_actor() {
908
920
  }
909
921
 
910
922
  pr_remote_head_oid() {
923
+ if host_github_rate_limit_state_active; then
924
+ printf '\n'
925
+ return 0
926
+ fi
911
927
  flow_export_github_cli_auth_env "${repo_slug}"
912
- gh pr view "${pr_number}" -R "${repo_slug}" --json headRefOid --jq '.headRefOid // ""' 2>/dev/null || true
928
+ flow_github_pr_head_oid "${repo_slug}" "${pr_number}"
913
929
  }
914
930
 
915
931
  pr_remote_already_has_final_head() {
@@ -931,39 +947,46 @@ approve_and_merge() {
931
947
  fi
932
948
  fi
933
949
 
950
+ if host_github_rate_limit_state_active; then
951
+ return 2
952
+ fi
953
+
934
954
  flow_export_github_cli_auth_env "${repo_slug}"
935
- 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
936
- local merge_output=""
937
- merge_output="$(cat "${run_dir}/host-github-merge.err" 2>/dev/null || true)"
938
- if host_github_output_indicates_rate_limit "${merge_output}"; then
939
- record_host_github_rate_limit "${merge_output}"
940
- return 2
941
- fi
942
- if flow_github_pr_merge "$repo_slug" "$pr_number" "squash" "yes" 2>"${run_dir}/host-github-merge.err"; then
943
- return 0
944
- fi
945
- merge_output="$(cat "${run_dir}/host-github-merge.err" 2>/dev/null || true)"
946
- if host_github_output_indicates_rate_limit "${merge_output}"; then
947
- record_host_github_rate_limit "${merge_output}"
948
- return 2
949
- fi
950
- if [[ -n "${merge_output}" ]]; then
951
- printf '%s\n' "${merge_output}" >&2
952
- fi
953
- return 1
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
954
957
  fi
955
958
 
956
- return 0
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
957
969
  }
958
970
 
959
971
  cleanup_pr_session() {
960
- "${shared_tools_dir}/agent-project-cleanup-session" \
961
- --repo-root "$repo_root" \
962
- --runs-root "$runs_root" \
963
- --history-root "$history_root" \
964
- --session "$session" \
965
- --worktree "$pr_worktree" \
966
- --mode pr >/dev/null || true
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}"
967
990
  }
968
991
 
969
992
  notify_pr_reconciled() {