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,118 +0,0 @@
1
- #!/usr/bin/env bash
2
- set -euo pipefail
3
-
4
- usage() {
5
- cat <<'EOF'
6
- Usage:
7
- agent-project-catch-up-issue-pr-links --repo-slug <owner/repo> --state-root <path> --hook-file <path> [--limit <n>]
8
-
9
- Clear stale issue retry state when an issue already has a linked PR comment and
10
- that PR still exists (open, closed, or merged).
11
- EOF
12
- }
13
-
14
- repo_slug=""
15
- state_root=""
16
- hook_file=""
17
- limit="100"
18
-
19
- while [[ $# -gt 0 ]]; do
20
- case "$1" in
21
- --repo-slug) repo_slug="${2:-}"; shift 2 ;;
22
- --state-root) state_root="${2:-}"; shift 2 ;;
23
- --hook-file) hook_file="${2:-}"; shift 2 ;;
24
- --limit) limit="${2:-}"; shift 2 ;;
25
- --help|-h) usage; exit 0 ;;
26
- *) echo "Unknown argument: $1" >&2; usage >&2; exit 1 ;;
27
- esac
28
- done
29
-
30
- if [[ -z "$repo_slug" || -z "$state_root" || -z "$hook_file" ]]; then
31
- usage >&2
32
- exit 1
33
- fi
34
-
35
- if [[ ! -f "$hook_file" ]]; then
36
- echo "missing hook file: $hook_file" >&2
37
- exit 1
38
- fi
39
-
40
- # shellcheck source=/dev/null
41
- source "$hook_file"
42
-
43
- if ! declare -F issue_clear_retry >/dev/null 2>&1; then
44
- issue_clear_retry() { :; }
45
- fi
46
-
47
- ledger_dir="${state_root}/linked-pr-issue-catchup"
48
- retry_dir="${state_root}/retries/issues"
49
- mkdir -p "$ledger_dir" "$retry_dir"
50
-
51
- extract_latest_linked_pr() {
52
- local issue_json="${1:-}"
53
- ISSUE_JSON="$issue_json" python3 - <<'PY'
54
- import json
55
- import os
56
- import re
57
-
58
- issue = json.loads(os.environ.get("ISSUE_JSON", "{}") or "{}")
59
- latest = ""
60
- latest_at = ""
61
- for comment in issue.get("comments", []) or []:
62
- body = comment.get("body") or ""
63
- match = None
64
- for candidate in re.finditer(r"Opened PR #(\d+)", body):
65
- match = candidate
66
- if not match:
67
- continue
68
- created = comment.get("createdAt") or ""
69
- pr_number = match.group(1)
70
- if created >= latest_at:
71
- latest_at = created
72
- latest = pr_number
73
-
74
- print(latest)
75
- PY
76
- }
77
-
78
- pr_exists() {
79
- local pr_number="${1:?pr number required}"
80
- local pr_json=""
81
- pr_json="$(flow_github_pr_view_json "$repo_slug" "$pr_number" 2>/dev/null || true)"
82
- [[ -n "$pr_json" && "$pr_json" != "{}" ]]
83
- }
84
-
85
- for retry_file in "$retry_dir"/*.env; do
86
- [[ -f "$retry_file" ]] || continue
87
- issue_id="$(basename "${retry_file%.env}")"
88
- [[ -n "$issue_id" ]] || continue
89
-
90
- retry_reason="$(awk -F= '/^LAST_REASON=/{print $2; exit}' "$retry_file" 2>/dev/null | tr -d '\r' || true)"
91
- if [[ "$retry_reason" != "host-publish-failed" ]]; then
92
- continue
93
- fi
94
-
95
- ledger_file="${ledger_dir}/${issue_id}.env"
96
- if [[ -f "$ledger_file" ]]; then
97
- continue
98
- fi
99
-
100
- issue_json="$(flow_github_issue_view_json "$repo_slug" "$issue_id" 2>/dev/null || true)"
101
- [[ -n "$issue_json" && "$issue_json" != "{}" ]] || continue
102
-
103
- linked_pr="$(extract_latest_linked_pr "$issue_json")"
104
- [[ -n "$linked_pr" ]] || continue
105
- if ! pr_exists "$linked_pr"; then
106
- continue
107
- fi
108
-
109
- ISSUE_ID="$issue_id" issue_clear_retry || true
110
-
111
- {
112
- printf 'ISSUE_ID=%s\n' "$issue_id"
113
- printf 'LINKED_PR=%s\n' "$linked_pr"
114
- printf 'PROCESSED_AT=%s\n' "$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
115
- } >"$ledger_file"
116
-
117
- printf 'CATCHUP_LINKED_PR_ISSUE=%s\n' "$issue_id"
118
- done
@@ -1,194 +0,0 @@
1
- #!/usr/bin/env bash
2
- set -euo pipefail
3
-
4
- usage() {
5
- cat <<'EOF'
6
- Usage:
7
- agent-project-catch-up-merged-prs --repo-slug <owner/repo> --state-root <path> --hook-file <path> [--limit <n>]
8
-
9
- Catch up merge-cleanup for managed PRs that were merged manually after their
10
- worker session had already been reconciled and cleaned up.
11
- EOF
12
- }
13
-
14
- repo_slug=""
15
- state_root=""
16
- hook_file=""
17
- limit="100"
18
-
19
- while [[ $# -gt 0 ]]; do
20
- case "$1" in
21
- --repo-slug) repo_slug="${2:-}"; shift 2 ;;
22
- --state-root) state_root="${2:-}"; shift 2 ;;
23
- --hook-file) hook_file="${2:-}"; shift 2 ;;
24
- --limit) limit="${2:-}"; shift 2 ;;
25
- --help|-h) usage; exit 0 ;;
26
- *) echo "Unknown argument: $1" >&2; usage >&2; exit 1 ;;
27
- esac
28
- done
29
-
30
- if [[ -z "$repo_slug" || -z "$state_root" || -z "$hook_file" ]]; then
31
- usage >&2
32
- exit 1
33
- fi
34
-
35
- if [[ ! -f "$hook_file" ]]; then
36
- echo "missing hook file: $hook_file" >&2
37
- exit 1
38
- fi
39
-
40
- # shellcheck source=/dev/null
41
- source "$hook_file"
42
-
43
- optional_hooks=(
44
- pr_clear_retry
45
- pr_cleanup_linked_issue_session
46
- pr_cleanup_merged_residue
47
- pr_linked_issue_should_close
48
- pr_after_merged
49
- pr_after_closed
50
- )
51
-
52
- for hook_name in "${optional_hooks[@]}"; do
53
- if ! declare -F "$hook_name" >/dev/null 2>&1; then
54
- eval "${hook_name}() { :; }"
55
- fi
56
- done
57
-
58
- merged_ledger_dir="${state_root}/merged-pr-catchup"
59
- closed_ledger_dir="${state_root}/closed-pr-catchup"
60
- mkdir -p "$merged_ledger_dir" "$closed_ledger_dir"
61
-
62
- get_pr_risk_json() {
63
- local pr_number="${1:?pr number required}"
64
- if declare -F heartbeat_pr_risk_json >/dev/null 2>&1; then
65
- heartbeat_pr_risk_json "$pr_number"
66
- return 0
67
- fi
68
- if [[ -n "${ADAPTER_BIN_DIR:-}" && -x "${ADAPTER_BIN_DIR}/pr-risk.sh" ]]; then
69
- "${ADAPTER_BIN_DIR}/pr-risk.sh" "$pr_number"
70
- return 0
71
- fi
72
- echo "missing PR risk provider for catch-up" >&2
73
- return 1
74
- }
75
-
76
- close_issue_if_needed() {
77
- local pr_number="${1:?pr number required}"
78
- local issue_id="${2:-}"
79
- [[ -n "$issue_id" ]] || return 0
80
-
81
- local should_close="yes"
82
- should_close="$(pr_linked_issue_should_close "$issue_id" || printf 'yes\n')"
83
- if [[ "$should_close" != "yes" ]]; then
84
- return 0
85
- fi
86
-
87
- pr_cleanup_linked_issue_session "$issue_id" || true
88
-
89
- local issue_state=""
90
- issue_state="$(flow_github_issue_view_json "$repo_slug" "$issue_id" 2>/dev/null | jq -r '.state // empty' || true)"
91
- if [[ "$issue_state" == "OPEN" ]]; then
92
- flow_github_issue_close "$repo_slug" "$issue_id" "Closed automatically after PR #${pr_number} merged." >/dev/null 2>&1 || true
93
- printf 'CATCHUP_CLOSED_ISSUE=%s\n' "$issue_id"
94
- fi
95
- }
96
-
97
- process_terminal_pr() {
98
- local pr_number="${1:?pr number required}"
99
- local linked_issue_id="${2:-}"
100
- local merged_at="${3:-}"
101
- local processed_state="${4:?processed state required}"
102
- local ledger_dir=""
103
- local ledger_file=""
104
-
105
- case "$processed_state" in
106
- merged) ledger_dir="$merged_ledger_dir" ;;
107
- closed) ledger_dir="$closed_ledger_dir" ;;
108
- *)
109
- echo "unsupported terminal PR state: ${processed_state}" >&2
110
- return 1
111
- ;;
112
- esac
113
-
114
- ledger_file="${ledger_dir}/${pr_number}.env"
115
- if [[ -f "$ledger_file" ]]; then
116
- return 0
117
- fi
118
-
119
- PR_NUMBER="$pr_number" pr_clear_retry || true
120
- if [[ "$processed_state" == "merged" ]]; then
121
- close_issue_if_needed "$pr_number" "$linked_issue_id"
122
- if ! PR_NUMBER="$pr_number" pr_after_merged "$pr_number"; then
123
- printf 'CATCHUP_FAILED_PR=%s\n' "$pr_number" >&2
124
- return 1
125
- fi
126
- else
127
- if ! PR_NUMBER="$pr_number" pr_after_closed "$pr_number"; then
128
- printf 'CATCHUP_FAILED_CLOSED_PR=%s\n' "$pr_number" >&2
129
- return 1
130
- fi
131
- fi
132
-
133
- if ! PR_NUMBER="$pr_number" pr_cleanup_merged_residue "$pr_number"; then
134
- printf 'CATCHUP_FAILED_RESIDUE=%s\n' "$pr_number" >&2
135
- return 1
136
- fi
137
-
138
- {
139
- printf 'PR_NUMBER=%s\n' "$pr_number"
140
- printf 'ISSUE_ID=%s\n' "$linked_issue_id"
141
- printf 'PR_STATE=%s\n' "$processed_state"
142
- printf 'MERGED_AT=%s\n' "$merged_at"
143
- printf 'PROCESSED_AT=%s\n' "$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
144
- } >"$ledger_file"
145
-
146
- if [[ "$processed_state" == "merged" ]]; then
147
- printf 'CATCHUP_MERGED_PR=%s\n' "$pr_number"
148
- else
149
- printf 'CATCHUP_CLOSED_PR=%s\n' "$pr_number"
150
- fi
151
- }
152
-
153
- merged_prs_json="$(flow_github_pr_list_json "$repo_slug" merged "$limit")"
154
- closed_prs_json="$(flow_github_pr_list_json "$repo_slug" closed "$limit")"
155
-
156
- while IFS= read -r pr_number; do
157
- [[ -n "$pr_number" ]] || continue
158
-
159
- risk_json="$(get_pr_risk_json "$pr_number" 2>/dev/null || true)"
160
- if [[ -z "$risk_json" ]]; then
161
- continue
162
- fi
163
-
164
- is_managed="$(jq -r '.isManagedByAgent // .isAgentBranch // false' <<<"$risk_json" 2>/dev/null || printf 'false\n')"
165
- if [[ "$is_managed" != "true" ]]; then
166
- continue
167
- fi
168
-
169
- linked_issue_id="$(jq -r '.linkedIssueId // empty' <<<"$risk_json" 2>/dev/null || true)"
170
- merged_at="$(jq -r --arg pr "$pr_number" 'map(select((.number | tostring) == $pr)) | .[0].mergedAt // ""' <<<"$merged_prs_json")"
171
- process_terminal_pr "$pr_number" "$linked_issue_id" "$merged_at" merged || true
172
- done < <(jq -r 'sort_by(.mergedAt) | reverse | .[].number' <<<"$merged_prs_json")
173
-
174
- while IFS= read -r pr_number; do
175
- [[ -n "$pr_number" ]] || continue
176
-
177
- merged_at="$(jq -r --arg pr "$pr_number" 'map(select((.number | tostring) == $pr)) | .[0].mergedAt // ""' <<<"$closed_prs_json")"
178
- if [[ -n "$merged_at" ]]; then
179
- continue
180
- fi
181
-
182
- risk_json="$(get_pr_risk_json "$pr_number" 2>/dev/null || true)"
183
- if [[ -z "$risk_json" ]]; then
184
- continue
185
- fi
186
-
187
- is_managed="$(jq -r '.isManagedByAgent // .isAgentBranch // false' <<<"$risk_json" 2>/dev/null || printf 'false\n')"
188
- if [[ "$is_managed" != "true" ]]; then
189
- continue
190
- fi
191
-
192
- linked_issue_id="$(jq -r '.linkedIssueId // empty' <<<"$risk_json" 2>/dev/null || true)"
193
- process_terminal_pr "$pr_number" "$linked_issue_id" "" closed || true
194
- done < <(jq -r 'sort_by(.createdAt) | reverse | .[].number' <<<"$closed_prs_json")
@@ -1,123 +0,0 @@
1
- #!/usr/bin/env bash
2
- set -euo pipefail
3
-
4
- usage() {
5
- cat <<'EOF'
6
- Usage:
7
- agent-project-catch-up-scheduled-issue-retries --repo-slug <owner/repo> --state-root <path> --hook-file <path> [--limit <n>]
8
-
9
- Clear stale retry state for recurring scheduled reporting issues once GitHub
10
- already reflects the latest terminal status via labels and the issue is not
11
- currently running.
12
- EOF
13
- }
14
-
15
- repo_slug=""
16
- state_root=""
17
- hook_file=""
18
- limit="100"
19
-
20
- while [[ $# -gt 0 ]]; do
21
- case "$1" in
22
- --repo-slug) repo_slug="${2:-}"; shift 2 ;;
23
- --state-root) state_root="${2:-}"; shift 2 ;;
24
- --hook-file) hook_file="${2:-}"; shift 2 ;;
25
- --limit) limit="${2:-}"; shift 2 ;;
26
- --help|-h) usage; exit 0 ;;
27
- *) echo "Unknown argument: $1" >&2; usage >&2; exit 1 ;;
28
- esac
29
- done
30
-
31
- if [[ -z "$repo_slug" || -z "$state_root" || -z "$hook_file" ]]; then
32
- usage >&2
33
- exit 1
34
- fi
35
-
36
- if [[ ! -f "$hook_file" ]]; then
37
- echo "missing hook file: $hook_file" >&2
38
- exit 1
39
- fi
40
-
41
- # shellcheck source=/dev/null
42
- source "$hook_file"
43
-
44
- if ! declare -F issue_clear_retry >/dev/null 2>&1; then
45
- issue_clear_retry() { :; }
46
- fi
47
-
48
- ledger_dir="${state_root}/scheduled-issue-retry-catchup"
49
- retry_dir="${state_root}/retries/issues"
50
- mkdir -p "$ledger_dir" "$retry_dir"
51
-
52
- issue_has_terminal_scheduled_status() {
53
- local issue_json="${1:-}"
54
- ISSUE_JSON="$issue_json" python3 - <<'PY'
55
- import json
56
- import os
57
- import re
58
- import sys
59
-
60
- issue = json.loads(os.environ.get("ISSUE_JSON", "{}") or "{}")
61
- state = (issue.get("state") or "").upper()
62
- labels = {str(item.get("name") or "") for item in (issue.get("labels") or [])}
63
- body = issue.get("body") or ""
64
-
65
- has_schedule = bool(re.search(r'^\s*(?:Agent schedule|Schedule|Cadence)\s*:\s*(?:every\s+)?\d+\s*[mhd]\s*$', body, re.I | re.M))
66
- if not has_schedule and "agent-scheduled" not in labels:
67
- sys.exit(1)
68
-
69
- if state != "OPEN":
70
- sys.exit(1)
71
-
72
- if "agent-running" in labels:
73
- sys.exit(1)
74
-
75
- terminal_status_labels = {
76
- "health-ok",
77
- "health-not-ok",
78
- "checks-ok",
79
- "checks-not-ok",
80
- "smoke-ok",
81
- "smoke-not-ok",
82
- }
83
- if not (labels & terminal_status_labels):
84
- sys.exit(1)
85
-
86
- sys.exit(0)
87
- PY
88
- }
89
-
90
- processed=0
91
- for retry_file in "$retry_dir"/*.env; do
92
- [[ -f "$retry_file" ]] || continue
93
- if [[ "$processed" -ge "$limit" ]]; then
94
- break
95
- fi
96
-
97
- issue_id="$(basename "${retry_file%.env}")"
98
- [[ -n "$issue_id" ]] || continue
99
-
100
- ledger_file="${ledger_dir}/${issue_id}.env"
101
- if [[ -f "$ledger_file" ]]; then
102
- continue
103
- fi
104
-
105
- issue_json="$(flow_github_issue_view_json "$repo_slug" "$issue_id" 2>/dev/null || true)"
106
- [[ -n "$issue_json" && "$issue_json" != "{}" ]] || continue
107
-
108
- if ! issue_has_terminal_scheduled_status "$issue_json"; then
109
- continue
110
- fi
111
-
112
- retry_reason="$(awk -F= '/^LAST_REASON=/{print $2; exit}' "$retry_file" 2>/dev/null | tr -d '\r' || true)"
113
- ISSUE_ID="$issue_id" issue_clear_retry || true
114
-
115
- {
116
- printf 'ISSUE_ID=%s\n' "$issue_id"
117
- printf 'LAST_REASON=%s\n' "$retry_reason"
118
- printf 'PROCESSED_AT=%s\n' "$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
119
- } >"$ledger_file"
120
-
121
- printf 'CATCHUP_SCHEDULED_ISSUE=%s\n' "$issue_id"
122
- processed=$((processed + 1))
123
- done