agent-control-plane 0.3.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.
- package/README.md +69 -19
- package/assets/workflow-catalog.json +1 -1
- package/bin/pr-risk.sh +22 -7
- package/bin/sync-pr-labels.sh +1 -1
- package/hooks/heartbeat-hooks.sh +125 -12
- package/hooks/issue-reconcile-hooks.sh +1 -1
- package/hooks/pr-reconcile-hooks.sh +1 -1
- package/npm/bin/agent-control-plane.js +256 -58
- package/package.json +7 -6
- package/tools/bin/agent-github-update-labels +36 -2
- package/tools/bin/agent-project-catch-up-merged-prs +3 -2
- package/tools/bin/agent-project-publish-issue-pr +6 -3
- package/tools/bin/agent-project-reconcile-issue-session +12 -1
- package/tools/bin/agent-project-reconcile-pr-session +90 -32
- package/tools/bin/agent-project-retry-state +18 -7
- package/tools/bin/agent-project-run-codex-resilient +13 -5
- package/tools/bin/agent-project-sync-source-repo-main +163 -0
- package/tools/bin/flow-config-lib.sh +1203 -60
- package/tools/bin/flow-shell-lib.sh +32 -0
- package/tools/bin/github-core-rate-limit-state.sh +77 -0
- package/tools/bin/github-write-outbox.sh +470 -0
- package/tools/bin/heartbeat-loop-scheduling-lib.sh +7 -7
- package/tools/bin/heartbeat-safe-auto.sh +42 -0
- package/tools/bin/install-project-launchd.sh +17 -2
- package/tools/bin/project-init.sh +21 -1
- package/tools/bin/project-launchd-bootstrap.sh +5 -1
- package/tools/bin/project-runtimectl.sh +46 -2
- package/tools/bin/resident-issue-controller-lib.sh +2 -2
- package/tools/bin/scaffold-profile.sh +61 -3
- package/tools/bin/start-pr-fix-worker.sh +47 -10
- package/tools/bin/start-resident-issue-loop.sh +2 -2
- package/tools/dashboard/app.js +30 -1
- package/tools/dashboard/dashboard_snapshot.py +55 -0
- package/tools/templates/pr-fix-template.md +3 -1
- package/tools/templates/pr-merge-repair-template.md +2 -1
- package/references/architecture.md +0 -217
- package/references/commands.md +0 -128
- package/references/control-plane-map.md +0 -124
- package/references/docs-map.md +0 -73
- package/references/release-checklist.md +0 -65
- package/references/repo-map.md +0 -36
- package/tools/bin/resident-issue-queue-status.py +0 -35
- package/tools/bin/split-retained-slice.sh +0 -124
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-control-plane",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.9",
|
|
4
4
|
"description": "Help a repo keep GitHub-driven coding agents running reliably without constant human babysitting",
|
|
5
5
|
"homepage": "https://github.com/ducminhnguyen0319/agent-control-plane",
|
|
6
6
|
"bugs": {
|
|
@@ -27,11 +27,12 @@
|
|
|
27
27
|
"bin/sync-pr-labels.sh",
|
|
28
28
|
"hooks",
|
|
29
29
|
"npm/bin",
|
|
30
|
-
"references",
|
|
31
30
|
"tools/bin",
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
31
|
+
"!tools/bin/audit-*.sh",
|
|
32
|
+
"!tools/bin/check-skill-contracts.sh",
|
|
33
|
+
"!tools/bin/split-retained-slice.sh",
|
|
34
|
+
"!tools/bin/render-dashboard-snapshot.py",
|
|
35
|
+
"!tools/bin/resident-issue-queue-status.py",
|
|
35
36
|
"tools/dashboard/app.js",
|
|
36
37
|
"tools/dashboard/dashboard_snapshot.py",
|
|
37
38
|
"tools/dashboard/index.html",
|
|
@@ -47,7 +48,7 @@
|
|
|
47
48
|
"scripts": {
|
|
48
49
|
"doctor": "node ./npm/bin/agent-control-plane.js doctor",
|
|
49
50
|
"smoke": "node ./npm/bin/agent-control-plane.js smoke",
|
|
50
|
-
"test": "
|
|
51
|
+
"test": "node -e \"const { spawnSync } = require('node:child_process'); const result = spawnSync('bash', ['tools/tests/run-all.sh'], { stdio: 'inherit' }); if (result.error) throw result.error; process.exit(result.status ?? 1);\""
|
|
51
52
|
},
|
|
52
53
|
"publishConfig": {
|
|
53
54
|
"access": "public",
|
|
@@ -20,6 +20,7 @@ number=""
|
|
|
20
20
|
add_file="$(mktemp)"
|
|
21
21
|
remove_file="$(mktemp)"
|
|
22
22
|
trap 'rm -f "$add_file" "$remove_file"' EXIT
|
|
23
|
+
github_outbox_script="${SCRIPT_DIR}/github-write-outbox.sh"
|
|
23
24
|
|
|
24
25
|
while [[ $# -gt 0 ]]; do
|
|
25
26
|
case "$1" in
|
|
@@ -47,12 +48,40 @@ if [[ -z "$repo_slug" || -z "$number" ]]; then
|
|
|
47
48
|
exit 1
|
|
48
49
|
fi
|
|
49
50
|
|
|
51
|
+
enqueue_label_update() {
|
|
52
|
+
local -a args=()
|
|
53
|
+
local label=""
|
|
54
|
+
|
|
55
|
+
[[ "${ACP_GITHUB_OUTBOX_DISABLE_ENQUEUE:-0}" != "1" ]] || return 1
|
|
56
|
+
[[ -x "${github_outbox_script}" ]] || return 1
|
|
57
|
+
|
|
58
|
+
args=(enqueue-labels --repo-slug "${repo_slug}" --number "${number}")
|
|
59
|
+
while IFS= read -r label; do
|
|
60
|
+
[[ -n "${label}" ]] || continue
|
|
61
|
+
args+=(--add "${label}")
|
|
62
|
+
done <"${add_file}"
|
|
63
|
+
while IFS= read -r label; do
|
|
64
|
+
[[ -n "${label}" ]] || continue
|
|
65
|
+
args+=(--remove "${label}")
|
|
66
|
+
done <"${remove_file}"
|
|
67
|
+
|
|
68
|
+
"${github_outbox_script}" "${args[@]}" >/dev/null
|
|
69
|
+
}
|
|
70
|
+
|
|
50
71
|
resource="issues/${number}"
|
|
72
|
+
if flow_github_core_rate_limit_active; then
|
|
73
|
+
enqueue_label_update && exit 0
|
|
74
|
+
exit 1
|
|
75
|
+
fi
|
|
76
|
+
|
|
51
77
|
# Use caller-provided cached JSON if available to skip the GET call
|
|
52
78
|
if [[ -n "${ACP_CACHED_ISSUE_JSON:-}" ]]; then
|
|
53
79
|
current_json="${ACP_CACHED_ISSUE_JSON}"
|
|
54
80
|
else
|
|
55
|
-
current_json="$(flow_github_api_repo "${repo_slug}" "${resource}")"
|
|
81
|
+
if ! current_json="$(flow_github_api_repo "${repo_slug}" "${resource}")"; then
|
|
82
|
+
enqueue_label_update && exit 0
|
|
83
|
+
exit 1
|
|
84
|
+
fi
|
|
56
85
|
fi
|
|
57
86
|
add_json="$(jq -R . <"$add_file" | jq -s .)"
|
|
58
87
|
remove_json="$(jq -R . <"$remove_file" | jq -s .)"
|
|
@@ -68,4 +97,9 @@ process.stdout.write(JSON.stringify({ labels: Array.from(labels).sort() }));
|
|
|
68
97
|
EOF
|
|
69
98
|
)"
|
|
70
99
|
|
|
71
|
-
printf '%s' "$payload" | flow_github_api_repo "${repo_slug}" "${resource}" --method PATCH --input - >/dev/null
|
|
100
|
+
if printf '%s' "$payload" | flow_github_api_repo "${repo_slug}" "${resource}" --method PATCH --input - >/dev/null; then
|
|
101
|
+
exit 0
|
|
102
|
+
fi
|
|
103
|
+
|
|
104
|
+
enqueue_label_update && exit 0
|
|
105
|
+
exit 1
|
|
@@ -55,8 +55,9 @@ for hook_name in "${optional_hooks[@]}"; do
|
|
|
55
55
|
fi
|
|
56
56
|
done
|
|
57
57
|
|
|
58
|
-
|
|
59
|
-
|
|
58
|
+
forge_scope="$(printf '%s' "${ACP_FORGE_PROVIDER:-${F_LOSNING_FORGE_PROVIDER:-github}}" | tr -c '[:alnum:]._-' '-')"
|
|
59
|
+
merged_ledger_dir="${state_root}/merged-pr-catchup-${forge_scope}"
|
|
60
|
+
closed_ledger_dir="${state_root}/closed-pr-catchup-${forge_scope}"
|
|
60
61
|
mkdir -p "$merged_ledger_dir" "$closed_ledger_dir"
|
|
61
62
|
|
|
62
63
|
get_pr_risk_json() {
|
|
@@ -186,9 +186,12 @@ fi
|
|
|
186
186
|
resolve_actor_login() {
|
|
187
187
|
local login=""
|
|
188
188
|
|
|
189
|
-
|
|
190
|
-
login
|
|
191
|
-
|
|
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' \
|
|
@@ -231,6 +231,7 @@ if [[ -n "$hook_file" && -f "$hook_file" ]]; then
|
|
|
231
231
|
fi
|
|
232
232
|
|
|
233
233
|
provider_cooldown_script="${shared_tools_dir}/provider-cooldown-state.sh"
|
|
234
|
+
github_write_outbox_script="${shared_tools_dir}/github-write-outbox.sh"
|
|
234
235
|
|
|
235
236
|
schedule_provider_quota_cooldown() {
|
|
236
237
|
local reason="${1:-provider-quota-limit}"
|
|
@@ -335,7 +336,17 @@ post_issue_comment_if_present() {
|
|
|
335
336
|
if issue_latest_comment_matches_artifact; then
|
|
336
337
|
return 0
|
|
337
338
|
fi
|
|
338
|
-
flow_github_api_repo "${repo_slug}" "issues/${issue_id}/comments" --method POST -f body="$(cat "$comment_file")" >/dev/null
|
|
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
|
|
339
350
|
}
|
|
340
351
|
|
|
341
352
|
issue_latest_comment_matches_artifact() {
|
|
@@ -151,6 +151,9 @@ if [[ -n "$hook_file" && -f "$hook_file" ]]; then
|
|
|
151
151
|
fi
|
|
152
152
|
|
|
153
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."
|
|
154
157
|
|
|
155
158
|
schedule_provider_quota_cooldown() {
|
|
156
159
|
local reason="${1:-provider-quota-limit}"
|
|
@@ -172,6 +175,26 @@ blocked_runtime_reason=""
|
|
|
172
175
|
host_github_rate_limited="no"
|
|
173
176
|
host_github_rate_limit_detail=""
|
|
174
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
|
+
|
|
175
198
|
owner="${repo_slug%%/*}"
|
|
176
199
|
repo="${repo_slug#*/}"
|
|
177
200
|
pr_view_json="$(flow_github_pr_view_json "$repo_slug" "$pr_number")"
|
|
@@ -365,9 +388,27 @@ post_pr_comment_if_present() {
|
|
|
365
388
|
if pr_comment_already_posted; then
|
|
366
389
|
return 0
|
|
367
390
|
fi
|
|
368
|
-
if
|
|
369
|
-
return
|
|
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
|
|
370
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
|
|
371
412
|
}
|
|
372
413
|
|
|
373
414
|
pr_comment_already_posted() {
|
|
@@ -390,6 +431,9 @@ record_host_github_rate_limit() {
|
|
|
390
431
|
host_github_rate_limited="yes"
|
|
391
432
|
host_github_rate_limit_detail="${output}"
|
|
392
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
|
|
393
437
|
}
|
|
394
438
|
|
|
395
439
|
host_github_post_issue_comment() {
|
|
@@ -397,9 +441,13 @@ host_github_post_issue_comment() {
|
|
|
397
441
|
local body="${2:-}"
|
|
398
442
|
local output=""
|
|
399
443
|
|
|
444
|
+
if host_github_rate_limit_state_active; then
|
|
445
|
+
return 1
|
|
446
|
+
fi
|
|
447
|
+
|
|
400
448
|
flow_export_github_cli_auth_env "${repo_slug}"
|
|
401
449
|
if output="$(
|
|
402
|
-
|
|
450
|
+
flow_github_api_repo "${repo_slug}" "issues/${issue_number}/comments" \
|
|
403
451
|
--method POST \
|
|
404
452
|
-f body="${body}" 2>&1
|
|
405
453
|
)"; then
|
|
@@ -418,23 +466,25 @@ host_github_post_issue_comment() {
|
|
|
418
466
|
host_github_submit_pr_approval() {
|
|
419
467
|
local output=""
|
|
420
468
|
|
|
469
|
+
if host_github_rate_limit_state_active; then
|
|
470
|
+
enqueue_pr_approval_intent || true
|
|
471
|
+
return 1
|
|
472
|
+
fi
|
|
473
|
+
|
|
421
474
|
flow_export_github_cli_auth_env "${repo_slug}"
|
|
422
475
|
if output="$(
|
|
423
|
-
|
|
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
|
|
476
|
+
flow_github_pr_review_approve "${repo_slug}" "${pr_number}" "${automated_pr_approval_body}" 2>&1
|
|
428
477
|
)"; then
|
|
429
478
|
return 0
|
|
430
479
|
fi
|
|
431
480
|
|
|
432
|
-
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
|
|
433
482
|
return 0
|
|
434
483
|
fi
|
|
435
484
|
|
|
436
485
|
if host_github_output_indicates_rate_limit "${output}"; then
|
|
437
486
|
record_host_github_rate_limit "${output}"
|
|
487
|
+
enqueue_pr_approval_intent || true
|
|
438
488
|
return 1
|
|
439
489
|
fi
|
|
440
490
|
|
|
@@ -843,13 +893,21 @@ merge_state_prepared() {
|
|
|
843
893
|
}
|
|
844
894
|
|
|
845
895
|
current_github_login() {
|
|
896
|
+
if host_github_rate_limit_state_active; then
|
|
897
|
+
printf '\n'
|
|
898
|
+
return 0
|
|
899
|
+
fi
|
|
846
900
|
flow_export_github_cli_auth_env "${repo_slug}"
|
|
847
|
-
|
|
901
|
+
flow_github_current_login
|
|
848
902
|
}
|
|
849
903
|
|
|
850
904
|
pr_author_login() {
|
|
905
|
+
if host_github_rate_limit_state_active; then
|
|
906
|
+
printf '\n'
|
|
907
|
+
return 0
|
|
908
|
+
fi
|
|
851
909
|
flow_export_github_cli_auth_env "${repo_slug}"
|
|
852
|
-
|
|
910
|
+
flow_github_pr_author_login "${repo_slug}" "${pr_number}"
|
|
853
911
|
}
|
|
854
912
|
|
|
855
913
|
pr_is_self_authored_for_current_actor() {
|
|
@@ -862,8 +920,12 @@ pr_is_self_authored_for_current_actor() {
|
|
|
862
920
|
}
|
|
863
921
|
|
|
864
922
|
pr_remote_head_oid() {
|
|
923
|
+
if host_github_rate_limit_state_active; then
|
|
924
|
+
printf '\n'
|
|
925
|
+
return 0
|
|
926
|
+
fi
|
|
865
927
|
flow_export_github_cli_auth_env "${repo_slug}"
|
|
866
|
-
|
|
928
|
+
flow_github_pr_head_oid "${repo_slug}" "${pr_number}"
|
|
867
929
|
}
|
|
868
930
|
|
|
869
931
|
pr_remote_already_has_final_head() {
|
|
@@ -885,29 +947,25 @@ approve_and_merge() {
|
|
|
885
947
|
fi
|
|
886
948
|
fi
|
|
887
949
|
|
|
950
|
+
if host_github_rate_limit_state_active; then
|
|
951
|
+
return 2
|
|
952
|
+
fi
|
|
953
|
+
|
|
888
954
|
flow_export_github_cli_auth_env "${repo_slug}"
|
|
889
|
-
if
|
|
890
|
-
|
|
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
|
|
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
|
|
908
957
|
fi
|
|
909
958
|
|
|
910
|
-
|
|
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
|
|
911
969
|
}
|
|
912
970
|
|
|
913
971
|
cleanup_pr_session() {
|
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
2
|
set -euo pipefail
|
|
3
3
|
|
|
4
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
5
|
+
# shellcheck source=/dev/null
|
|
6
|
+
source "${SCRIPT_DIR}/flow-shell-lib.sh"
|
|
7
|
+
|
|
4
8
|
usage() {
|
|
5
9
|
cat <<'EOF'
|
|
6
10
|
Usage:
|
|
7
|
-
agent-project-retry-state --state-root <path> --kind issue|pr|provider --item-id <id> --action get|schedule|clear [--reason <text>] [--cooldowns <csv>]
|
|
11
|
+
agent-project-retry-state --state-root <path> --kind issue|pr|provider|github --item-id <id> --action get|schedule|clear [--reason <text>] [--cooldowns <csv>] [--next-at-epoch <unix-seconds>]
|
|
8
12
|
|
|
9
13
|
Generic retry/cooldown state manager for project adapters.
|
|
10
14
|
|
|
@@ -12,6 +16,7 @@ Examples:
|
|
|
12
16
|
agent-project-retry-state --state-root /tmp/state --kind issue --item-id 123 --action get
|
|
13
17
|
agent-project-retry-state --state-root /tmp/state --kind pr --item-id 77 --action schedule --reason worker-exit-failed
|
|
14
18
|
agent-project-retry-state --state-root /tmp/state --kind provider --item-id openclaw-stepfun-free --action schedule --reason provider-quota-limit
|
|
19
|
+
agent-project-retry-state --state-root /tmp/state --kind github --item-id core-api --action schedule --reason github-api-rate-limit --next-at-epoch 4102444800
|
|
15
20
|
EOF
|
|
16
21
|
}
|
|
17
22
|
|
|
@@ -21,6 +26,7 @@ item_id=""
|
|
|
21
26
|
action=""
|
|
22
27
|
reason=""
|
|
23
28
|
cooldowns_csv="${AGENT_PROJECT_RETRY_COOLDOWNS:-300,900,1800,3600}"
|
|
29
|
+
next_at_epoch_override=""
|
|
24
30
|
|
|
25
31
|
while [[ $# -gt 0 ]]; do
|
|
26
32
|
case "$1" in
|
|
@@ -30,6 +36,7 @@ while [[ $# -gt 0 ]]; do
|
|
|
30
36
|
--action) action="${2:-}"; shift 2 ;;
|
|
31
37
|
--reason) reason="${2:-}"; shift 2 ;;
|
|
32
38
|
--cooldowns) cooldowns_csv="${2:-}"; shift 2 ;;
|
|
39
|
+
--next-at-epoch) next_at_epoch_override="${2:-}"; shift 2 ;;
|
|
33
40
|
--help|-h) usage; exit 0 ;;
|
|
34
41
|
*) echo "Unknown argument: $1" >&2; usage >&2; exit 1 ;;
|
|
35
42
|
esac
|
|
@@ -41,8 +48,8 @@ if [[ -z "$state_root" || -z "$kind" || -z "$item_id" || -z "$action" ]]; then
|
|
|
41
48
|
fi
|
|
42
49
|
|
|
43
50
|
case "$kind" in
|
|
44
|
-
issue|pr|provider) ;;
|
|
45
|
-
*) echo "--kind must be issue, pr, or
|
|
51
|
+
issue|pr|provider|github) ;;
|
|
52
|
+
*) echo "--kind must be issue, pr, provider, or github" >&2; exit 1 ;;
|
|
46
53
|
esac
|
|
47
54
|
|
|
48
55
|
case "$action" in
|
|
@@ -98,7 +105,7 @@ write_state() {
|
|
|
98
105
|
|
|
99
106
|
new_updated_at="$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
|
|
100
107
|
if [[ "$new_next_epoch" != "0" ]]; then
|
|
101
|
-
new_next_at="$(
|
|
108
|
+
new_next_at="$(flow_format_epoch_utc "$new_next_epoch")"
|
|
102
109
|
fi
|
|
103
110
|
|
|
104
111
|
cat >"$state_file" <<EOF
|
|
@@ -115,10 +122,14 @@ case "$action" in
|
|
|
115
122
|
;;
|
|
116
123
|
schedule)
|
|
117
124
|
attempts=$((attempts + 1))
|
|
118
|
-
|
|
119
|
-
|
|
125
|
+
if [[ "${next_at_epoch_override}" =~ ^[0-9]+$ ]] && (( next_at_epoch_override > now_epoch )); then
|
|
126
|
+
next_attempt_epoch="${next_at_epoch_override}"
|
|
127
|
+
else
|
|
128
|
+
cooldown_seconds="$(cooldown_seconds_for_attempt "$attempts")"
|
|
129
|
+
next_attempt_epoch=$((now_epoch + cooldown_seconds))
|
|
130
|
+
fi
|
|
120
131
|
write_state "$attempts" "$next_attempt_epoch" "$reason"
|
|
121
|
-
next_attempt_at="$(
|
|
132
|
+
next_attempt_at="$(flow_format_epoch_utc "$next_attempt_epoch")"
|
|
122
133
|
last_reason="$reason"
|
|
123
134
|
updated_at="$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
|
|
124
135
|
;;
|
|
@@ -136,6 +136,15 @@ config_yaml="$(resolve_flow_config_yaml "${BASH_SOURCE[0]}")"
|
|
|
136
136
|
issue_session_prefix="$(flow_resolve_issue_session_prefix "${config_yaml}")"
|
|
137
137
|
pr_session_prefix="$(flow_resolve_pr_session_prefix "${config_yaml}")"
|
|
138
138
|
|
|
139
|
+
# Keep npm-backed verification steps isolated from any broken user-global cache state.
|
|
140
|
+
npm_cache_dir="${NPM_CONFIG_CACHE:-${npm_config_cache:-}}"
|
|
141
|
+
if [[ -z "${npm_cache_dir}" ]]; then
|
|
142
|
+
npm_cache_dir="${ACP_NPM_CACHE_DIR:-${F_LOSNING_NPM_CACHE_DIR:-${HOME}/.agent-runtime/npm-cache}}"
|
|
143
|
+
fi
|
|
144
|
+
export NPM_CONFIG_CACHE="${npm_cache_dir}"
|
|
145
|
+
export npm_config_cache="${npm_cache_dir}"
|
|
146
|
+
mkdir -p "${npm_cache_dir}" 2>/dev/null || true
|
|
147
|
+
|
|
139
148
|
thread_id=""
|
|
140
149
|
attempt=0
|
|
141
150
|
resume_count=0
|
|
@@ -623,11 +632,10 @@ stream_codex_exec() {
|
|
|
623
632
|
rm -f "$stream_fifo"
|
|
624
633
|
rm -f "$progress_file"
|
|
625
634
|
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
fi
|
|
635
|
+
set +e
|
|
636
|
+
wait "$producer_pid" 2>/dev/null
|
|
637
|
+
last_exit_code="$?"
|
|
638
|
+
set -e
|
|
631
639
|
|
|
632
640
|
update_thread_id_from_output "$last_attempt_start_size"
|
|
633
641
|
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
5
|
+
# shellcheck source=/dev/null
|
|
6
|
+
source "${SCRIPT_DIR}/flow-config-lib.sh"
|
|
7
|
+
|
|
8
|
+
CONFIG_YAML="${ACP_SOURCE_REPO_SYNC_CONFIG_YAML:-$(resolve_flow_config_yaml "${BASH_SOURCE[0]}")}"
|
|
9
|
+
REPO_SLUG="$(flow_resolve_repo_slug "${CONFIG_YAML}")"
|
|
10
|
+
SOURCE_REPO_ROOT="$(flow_resolve_source_repo_root "${CONFIG_YAML}")"
|
|
11
|
+
DEFAULT_BRANCH="$(flow_resolve_default_branch "${CONFIG_YAML}")"
|
|
12
|
+
STATE_ROOT="$(flow_resolve_state_root "${CONFIG_YAML}")"
|
|
13
|
+
SYNC_STATE_FILE="${STATE_ROOT}/source-repo-main-sync.env"
|
|
14
|
+
FORGE_PROVIDER="$(flow_forge_provider)"
|
|
15
|
+
REMOTE_OVERRIDE="${ACP_SOURCE_SYNC_REMOTE:-${F_LOSNING_SOURCE_SYNC_REMOTE:-}}"
|
|
16
|
+
|
|
17
|
+
write_state() {
|
|
18
|
+
local status="${1:-}"
|
|
19
|
+
local remote_name="${2:-}"
|
|
20
|
+
local remote_sha="${3:-}"
|
|
21
|
+
local local_sha="${4:-}"
|
|
22
|
+
local detail="${5:-}"
|
|
23
|
+
|
|
24
|
+
mkdir -p "$(dirname "${SYNC_STATE_FILE}")"
|
|
25
|
+
{
|
|
26
|
+
printf 'STATUS=%s\n' "${status}"
|
|
27
|
+
printf 'UPDATED_AT=%s\n' "$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
|
|
28
|
+
printf 'SOURCE_REPO_ROOT=%s\n' "${SOURCE_REPO_ROOT}"
|
|
29
|
+
printf 'DEFAULT_BRANCH=%s\n' "${DEFAULT_BRANCH}"
|
|
30
|
+
printf 'REMOTE_NAME=%s\n' "${remote_name}"
|
|
31
|
+
printf 'REMOTE_SHA=%s\n' "${remote_sha}"
|
|
32
|
+
printf 'LOCAL_SHA=%s\n' "${local_sha}"
|
|
33
|
+
printf 'DETAIL=%s\n' "${detail}"
|
|
34
|
+
} >"${SYNC_STATE_FILE}"
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
git_ref_sha() {
|
|
38
|
+
local repo_root="${1:?repo root required}"
|
|
39
|
+
local ref_name="${2:?ref required}"
|
|
40
|
+
git -C "${repo_root}" rev-parse --verify --quiet "${ref_name}" 2>/dev/null || true
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
git_has_remote() {
|
|
44
|
+
local repo_root="${1:?repo root required}"
|
|
45
|
+
local remote_name="${2:?remote required}"
|
|
46
|
+
git -C "${repo_root}" remote get-url "${remote_name}" >/dev/null 2>&1
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
discover_remote_name() {
|
|
50
|
+
local remote_name=""
|
|
51
|
+
|
|
52
|
+
if [[ -n "${REMOTE_OVERRIDE}" ]] && git_has_remote "${SOURCE_REPO_ROOT}" "${REMOTE_OVERRIDE}"; then
|
|
53
|
+
printf '%s\n' "${REMOTE_OVERRIDE}"
|
|
54
|
+
return 0
|
|
55
|
+
fi
|
|
56
|
+
|
|
57
|
+
case "${FORGE_PROVIDER}" in
|
|
58
|
+
gitea)
|
|
59
|
+
if git_has_remote "${SOURCE_REPO_ROOT}" "gitea"; then
|
|
60
|
+
printf 'gitea\n'
|
|
61
|
+
return 0
|
|
62
|
+
fi
|
|
63
|
+
;;
|
|
64
|
+
github)
|
|
65
|
+
if git_has_remote "${SOURCE_REPO_ROOT}" "origin"; then
|
|
66
|
+
printf 'origin\n'
|
|
67
|
+
return 0
|
|
68
|
+
fi
|
|
69
|
+
;;
|
|
70
|
+
esac
|
|
71
|
+
|
|
72
|
+
while IFS= read -r remote_name; do
|
|
73
|
+
[[ -n "${remote_name}" ]] || continue
|
|
74
|
+
if [[ "$(flow_git_remote_repo_slug "${SOURCE_REPO_ROOT}" "${remote_name}" 2>/dev/null || true)" == "${REPO_SLUG}" ]]; then
|
|
75
|
+
printf '%s\n' "${remote_name}"
|
|
76
|
+
return 0
|
|
77
|
+
fi
|
|
78
|
+
done < <(git -C "${SOURCE_REPO_ROOT}" remote)
|
|
79
|
+
|
|
80
|
+
return 1
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if [[ -z "${SOURCE_REPO_ROOT}" ]]; then
|
|
84
|
+
write_state "skipped" "" "" "" "source-repo-root-unset"
|
|
85
|
+
printf 'SOURCE_REPO_SYNC_STATUS=skipped\nSOURCE_REPO_SYNC_REASON=source-repo-root-unset\n'
|
|
86
|
+
exit 0
|
|
87
|
+
fi
|
|
88
|
+
|
|
89
|
+
if [[ ! -d "${SOURCE_REPO_ROOT}/.git" && ! -f "${SOURCE_REPO_ROOT}/.git" ]]; then
|
|
90
|
+
write_state "skipped" "" "" "" "source-repo-not-git"
|
|
91
|
+
printf 'SOURCE_REPO_SYNC_STATUS=skipped\nSOURCE_REPO_SYNC_REASON=source-repo-not-git\nSOURCE_REPO_ROOT=%s\n' "${SOURCE_REPO_ROOT}"
|
|
92
|
+
exit 0
|
|
93
|
+
fi
|
|
94
|
+
|
|
95
|
+
REMOTE_NAME="$(discover_remote_name || true)"
|
|
96
|
+
if [[ -z "${REMOTE_NAME}" ]]; then
|
|
97
|
+
write_state "skipped" "" "" "" "remote-not-found"
|
|
98
|
+
printf 'SOURCE_REPO_SYNC_STATUS=skipped\nSOURCE_REPO_SYNC_REASON=remote-not-found\nSOURCE_REPO_ROOT=%s\n' "${SOURCE_REPO_ROOT}"
|
|
99
|
+
exit 0
|
|
100
|
+
fi
|
|
101
|
+
|
|
102
|
+
if ! git -C "${SOURCE_REPO_ROOT}" fetch "${REMOTE_NAME}" "+refs/heads/${DEFAULT_BRANCH}:refs/remotes/${REMOTE_NAME}/${DEFAULT_BRANCH}" --prune >/dev/null 2>&1; then
|
|
103
|
+
write_state "failed" "${REMOTE_NAME}" "" "" "fetch-failed"
|
|
104
|
+
printf 'SOURCE_REPO_SYNC_STATUS=failed\nSOURCE_REPO_SYNC_REASON=fetch-failed\nSOURCE_REPO_ROOT=%s\nREMOTE_NAME=%s\n' "${SOURCE_REPO_ROOT}" "${REMOTE_NAME}"
|
|
105
|
+
exit 1
|
|
106
|
+
fi
|
|
107
|
+
|
|
108
|
+
remote_sha="$(git_ref_sha "${SOURCE_REPO_ROOT}" "refs/remotes/${REMOTE_NAME}/${DEFAULT_BRANCH}")"
|
|
109
|
+
local_branch_sha="$(git_ref_sha "${SOURCE_REPO_ROOT}" "refs/heads/${DEFAULT_BRANCH}")"
|
|
110
|
+
current_branch="$(git -C "${SOURCE_REPO_ROOT}" symbolic-ref --quiet --short HEAD 2>/dev/null || true)"
|
|
111
|
+
|
|
112
|
+
if [[ -z "${remote_sha}" || -z "${local_branch_sha}" ]]; then
|
|
113
|
+
write_state "failed" "${REMOTE_NAME}" "${remote_sha}" "${local_branch_sha}" "missing-branch-ref"
|
|
114
|
+
printf 'SOURCE_REPO_SYNC_STATUS=failed\nSOURCE_REPO_SYNC_REASON=missing-branch-ref\nSOURCE_REPO_ROOT=%s\nREMOTE_NAME=%s\n' "${SOURCE_REPO_ROOT}" "${REMOTE_NAME}"
|
|
115
|
+
exit 1
|
|
116
|
+
fi
|
|
117
|
+
|
|
118
|
+
if [[ "${remote_sha}" == "${local_branch_sha}" ]]; then
|
|
119
|
+
write_state "unchanged" "${REMOTE_NAME}" "${remote_sha}" "${local_branch_sha}" "already-current"
|
|
120
|
+
printf 'SOURCE_REPO_SYNC_STATUS=unchanged\nSOURCE_REPO_ROOT=%s\nREMOTE_NAME=%s\nSOURCE_REPO_SYNC_SHA=%s\n' "${SOURCE_REPO_ROOT}" "${REMOTE_NAME}" "${remote_sha}"
|
|
121
|
+
exit 0
|
|
122
|
+
fi
|
|
123
|
+
|
|
124
|
+
if [[ "${current_branch}" == "${DEFAULT_BRANCH}" ]]; then
|
|
125
|
+
if [[ -n "$(git -C "${SOURCE_REPO_ROOT}" status --porcelain 2>/dev/null || true)" ]]; then
|
|
126
|
+
write_state "blocked" "${REMOTE_NAME}" "${remote_sha}" "${local_branch_sha}" "working-tree-dirty"
|
|
127
|
+
printf 'SOURCE_REPO_SYNC_STATUS=blocked\nSOURCE_REPO_SYNC_REASON=working-tree-dirty\nSOURCE_REPO_ROOT=%s\nREMOTE_NAME=%s\nLOCAL_SHA=%s\nREMOTE_SHA=%s\n' \
|
|
128
|
+
"${SOURCE_REPO_ROOT}" "${REMOTE_NAME}" "${local_branch_sha}" "${remote_sha}"
|
|
129
|
+
exit 0
|
|
130
|
+
fi
|
|
131
|
+
|
|
132
|
+
if git -C "${SOURCE_REPO_ROOT}" merge-base --is-ancestor "${local_branch_sha}" "${remote_sha}" >/dev/null 2>&1; then
|
|
133
|
+
git -C "${SOURCE_REPO_ROOT}" merge --ff-only "refs/remotes/${REMOTE_NAME}/${DEFAULT_BRANCH}" >/dev/null
|
|
134
|
+
updated_sha="$(git_ref_sha "${SOURCE_REPO_ROOT}" "refs/heads/${DEFAULT_BRANCH}")"
|
|
135
|
+
write_state "updated" "${REMOTE_NAME}" "${remote_sha}" "${updated_sha}" "fast-forward-checked-out-branch"
|
|
136
|
+
printf 'SOURCE_REPO_SYNC_STATUS=updated\nSOURCE_REPO_ROOT=%s\nREMOTE_NAME=%s\nSOURCE_REPO_SYNC_SHA=%s\n' "${SOURCE_REPO_ROOT}" "${REMOTE_NAME}" "${updated_sha}"
|
|
137
|
+
exit 0
|
|
138
|
+
fi
|
|
139
|
+
|
|
140
|
+
if git -C "${SOURCE_REPO_ROOT}" merge --no-edit "refs/remotes/${REMOTE_NAME}/${DEFAULT_BRANCH}" >/dev/null 2>&1; then
|
|
141
|
+
updated_sha="$(git_ref_sha "${SOURCE_REPO_ROOT}" "refs/heads/${DEFAULT_BRANCH}")"
|
|
142
|
+
write_state "updated" "${REMOTE_NAME}" "${remote_sha}" "${updated_sha}" "merge-checked-out-branch"
|
|
143
|
+
printf 'SOURCE_REPO_SYNC_STATUS=updated\nSOURCE_REPO_ROOT=%s\nREMOTE_NAME=%s\nSOURCE_REPO_SYNC_SHA=%s\n' "${SOURCE_REPO_ROOT}" "${REMOTE_NAME}" "${updated_sha}"
|
|
144
|
+
exit 0
|
|
145
|
+
fi
|
|
146
|
+
|
|
147
|
+
git -C "${SOURCE_REPO_ROOT}" merge --abort >/dev/null 2>&1 || true
|
|
148
|
+
write_state "blocked" "${REMOTE_NAME}" "${remote_sha}" "${local_branch_sha}" "merge-conflict"
|
|
149
|
+
printf 'SOURCE_REPO_SYNC_STATUS=blocked\nSOURCE_REPO_SYNC_REASON=merge-conflict\nSOURCE_REPO_ROOT=%s\nREMOTE_NAME=%s\nLOCAL_SHA=%s\nREMOTE_SHA=%s\n' \
|
|
150
|
+
"${SOURCE_REPO_ROOT}" "${REMOTE_NAME}" "${local_branch_sha}" "${remote_sha}"
|
|
151
|
+
exit 0
|
|
152
|
+
fi
|
|
153
|
+
|
|
154
|
+
if ! git -C "${SOURCE_REPO_ROOT}" merge-base --is-ancestor "${local_branch_sha}" "${remote_sha}" >/dev/null 2>&1; then
|
|
155
|
+
write_state "blocked" "${REMOTE_NAME}" "${remote_sha}" "${local_branch_sha}" "local-main-diverged"
|
|
156
|
+
printf 'SOURCE_REPO_SYNC_STATUS=blocked\nSOURCE_REPO_SYNC_REASON=local-main-diverged\nSOURCE_REPO_ROOT=%s\nREMOTE_NAME=%s\nLOCAL_SHA=%s\nREMOTE_SHA=%s\n' \
|
|
157
|
+
"${SOURCE_REPO_ROOT}" "${REMOTE_NAME}" "${local_branch_sha}" "${remote_sha}"
|
|
158
|
+
exit 0
|
|
159
|
+
fi
|
|
160
|
+
|
|
161
|
+
git -C "${SOURCE_REPO_ROOT}" update-ref "refs/heads/${DEFAULT_BRANCH}" "${remote_sha}" "${local_branch_sha}"
|
|
162
|
+
write_state "updated" "${REMOTE_NAME}" "${remote_sha}" "${remote_sha}" "fast-forward-local-ref"
|
|
163
|
+
printf 'SOURCE_REPO_SYNC_STATUS=updated\nSOURCE_REPO_ROOT=%s\nREMOTE_NAME=%s\nSOURCE_REPO_SYNC_SHA=%s\n' "${SOURCE_REPO_ROOT}" "${REMOTE_NAME}" "${remote_sha}"
|