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
@@ -0,0 +1,306 @@
1
+ #!/usr/bin/env bash
2
+ # heartbeat-loop-counting-lib.sh — worker counting, pending launch counts, capacity queries
3
+
4
+ running_heavy_issue_workers() {
5
+ local session issue_id is_heavy count=0
6
+ ensure_running_issue_workers_cache
7
+ while IFS= read -r session; do
8
+ [[ -n "$session" ]] || continue
9
+ issue_id="$(issue_id_from_session "$session" || true)"
10
+ [[ -n "$issue_id" ]] || continue
11
+ is_heavy="$(cached_issue_attr heavy "$issue_id")"
12
+ if [[ "$is_heavy" == "yes" ]]; then
13
+ count=$((count + 1))
14
+ fi
15
+ done <<<"$running_issue_workers_cache"
16
+ printf '%s\n' "$count"
17
+ }
18
+
19
+ pending_issue_launch_count() {
20
+ local pending_file issue_id count=0
21
+ for pending_file in "${pending_launch_dir}"/issue-*.pid; do
22
+ [[ -f "$pending_file" ]] || continue
23
+ issue_id="${pending_file##*/issue-}"
24
+ issue_id="${issue_id%.pid}"
25
+ [[ -n "$issue_id" ]] || continue
26
+ if tmux has-session -t "${issue_prefix}${issue_id}" 2>/dev/null; then
27
+ continue
28
+ fi
29
+ if pending_issue_launch_counts_toward_capacity "$issue_id"; then
30
+ count=$((count + 1))
31
+ fi
32
+ done
33
+ printf '%s\n' "$count"
34
+ }
35
+
36
+ pending_pr_launch_count() {
37
+ local pending_file pr_id count=0
38
+ for pending_file in "${pending_launch_dir}"/pr-*.pid; do
39
+ [[ -f "$pending_file" ]] || continue
40
+ pr_id="${pending_file##*/pr-}"
41
+ pr_id="${pr_id%.pid}"
42
+ [[ -n "$pr_id" ]] || continue
43
+ if tmux has-session -t "${pr_prefix}${pr_id}" 2>/dev/null; then
44
+ continue
45
+ fi
46
+ if pending_pr_launch_active "$pr_id"; then
47
+ count=$((count + 1))
48
+ fi
49
+ done
50
+ printf '%s\n' "$count"
51
+ }
52
+
53
+ pending_heavy_issue_launch_count() {
54
+ local pending_file issue_id count=0
55
+ for pending_file in "${pending_launch_dir}"/issue-*.pid; do
56
+ [[ -f "$pending_file" ]] || continue
57
+ issue_id="${pending_file##*/issue-}"
58
+ issue_id="${issue_id%.pid}"
59
+ [[ -n "$issue_id" ]] || continue
60
+ if tmux has-session -t "${issue_prefix}${issue_id}" 2>/dev/null; then
61
+ continue
62
+ fi
63
+ if pending_issue_launch_counts_toward_capacity "$issue_id" && [[ "$(cached_issue_attr heavy "$issue_id")" == "yes" ]]; then
64
+ count=$((count + 1))
65
+ fi
66
+ done
67
+ printf '%s\n' "$count"
68
+ }
69
+
70
+ pending_scheduled_issue_launch_count() {
71
+ local pending_file issue_id count=0
72
+ for pending_file in "${pending_launch_dir}"/issue-*.pid; do
73
+ [[ -f "$pending_file" ]] || continue
74
+ issue_id="${pending_file##*/issue-}"
75
+ issue_id="${issue_id%.pid}"
76
+ [[ -n "$issue_id" ]] || continue
77
+ if tmux has-session -t "${issue_prefix}${issue_id}" 2>/dev/null; then
78
+ continue
79
+ fi
80
+ if pending_issue_launch_counts_toward_capacity "$issue_id" && [[ "$(cached_issue_attr scheduled "$issue_id")" == "yes" ]]; then
81
+ count=$((count + 1))
82
+ fi
83
+ done
84
+ printf '%s\n' "$count"
85
+ }
86
+
87
+ pending_scheduled_heavy_issue_launch_count() {
88
+ local pending_file issue_id count=0
89
+ for pending_file in "${pending_launch_dir}"/issue-*.pid; do
90
+ [[ -f "$pending_file" ]] || continue
91
+ issue_id="${pending_file##*/issue-}"
92
+ issue_id="${issue_id%.pid}"
93
+ [[ -n "$issue_id" ]] || continue
94
+ if tmux has-session -t "${issue_prefix}${issue_id}" 2>/dev/null; then
95
+ continue
96
+ fi
97
+ if pending_issue_launch_counts_toward_capacity "$issue_id" \
98
+ && [[ "$(cached_issue_attr scheduled "$issue_id")" == "yes" ]] \
99
+ && [[ "$(cached_issue_attr heavy "$issue_id")" == "yes" ]]; then
100
+ count=$((count + 1))
101
+ fi
102
+ done
103
+ printf '%s\n' "$count"
104
+ }
105
+
106
+ pending_recurring_issue_launch_count() {
107
+ local pending_file issue_id count=0
108
+ for pending_file in "${pending_launch_dir}"/issue-*.pid; do
109
+ [[ -f "$pending_file" ]] || continue
110
+ issue_id="${pending_file##*/issue-}"
111
+ issue_id="${issue_id%.pid}"
112
+ [[ -n "$issue_id" ]] || continue
113
+ if tmux has-session -t "${issue_prefix}${issue_id}" 2>/dev/null; then
114
+ continue
115
+ fi
116
+ if pending_issue_launch_counts_toward_capacity "$issue_id" \
117
+ && [[ "$(cached_issue_attr scheduled "$issue_id")" != "yes" ]] \
118
+ && [[ "$(cached_issue_attr recurring "$issue_id")" == "yes" ]]; then
119
+ count=$((count + 1))
120
+ fi
121
+ done
122
+ printf '%s\n' "$count"
123
+ }
124
+
125
+ pending_blocked_recovery_issue_launch_count() {
126
+ local pending_file issue_id count=0
127
+ for pending_file in "${pending_launch_dir}"/issue-*.pid; do
128
+ [[ -f "$pending_file" ]] || continue
129
+ issue_id="${pending_file##*/issue-}"
130
+ issue_id="${issue_id%.pid}"
131
+ [[ -n "$issue_id" ]] || continue
132
+ if tmux has-session -t "${issue_prefix}${issue_id}" 2>/dev/null; then
133
+ continue
134
+ fi
135
+ if pending_issue_launch_counts_toward_capacity "$issue_id" && blocked_recovery_issue_has_state "$issue_id"; then
136
+ count=$((count + 1))
137
+ fi
138
+ done
139
+ printf '%s\n' "$count"
140
+ }
141
+
142
+ pending_exclusive_issue_launch_count() {
143
+ local pending_file issue_id count=0
144
+ for pending_file in "${pending_launch_dir}"/issue-*.pid; do
145
+ [[ -f "$pending_file" ]] || continue
146
+ issue_id="${pending_file##*/issue-}"
147
+ issue_id="${issue_id%.pid}"
148
+ [[ -n "$issue_id" ]] || continue
149
+ if tmux has-session -t "${issue_prefix}${issue_id}" 2>/dev/null; then
150
+ continue
151
+ fi
152
+ if pending_issue_launch_counts_toward_capacity "$issue_id" && [[ "$(cached_issue_attr exclusive "$issue_id")" == "yes" ]]; then
153
+ count=$((count + 1))
154
+ fi
155
+ done
156
+ printf '%s\n' "$count"
157
+ }
158
+
159
+ pending_exclusive_pr_launch_count() {
160
+ local pending_file pr_id count=0
161
+ for pending_file in "${pending_launch_dir}"/pr-*.pid; do
162
+ [[ -f "$pending_file" ]] || continue
163
+ pr_id="${pending_file##*/pr-}"
164
+ pr_id="${pr_id%.pid}"
165
+ [[ -n "$pr_id" ]] || continue
166
+ if tmux has-session -t "${pr_prefix}${pr_id}" 2>/dev/null; then
167
+ continue
168
+ fi
169
+ if pending_pr_launch_active "$pr_id" && [[ "$(cached_pr_is_exclusive "$pr_id")" == "yes" ]]; then
170
+ count=$((count + 1))
171
+ fi
172
+ done
173
+ printf '%s\n' "$count"
174
+ }
175
+
176
+ running_non_recurring_issue_workers() {
177
+ local session issue_id is_recurring is_scheduled count=0
178
+ ensure_running_issue_workers_cache
179
+ while IFS= read -r session; do
180
+ [[ -n "$session" ]] || continue
181
+ issue_id="$(issue_id_from_session "$session" || true)"
182
+ [[ -n "$issue_id" ]] || continue
183
+ is_scheduled="$(cached_issue_attr scheduled "$issue_id")"
184
+ if [[ "$is_scheduled" == "yes" ]]; then
185
+ continue
186
+ fi
187
+ is_recurring="$(cached_issue_attr recurring "$issue_id")"
188
+ if [[ "$is_recurring" != "yes" ]]; then
189
+ count=$((count + 1))
190
+ fi
191
+ done <<<"$running_issue_workers_cache"
192
+ printf '%s\n' "$count"
193
+ }
194
+
195
+ running_recurring_issue_workers() {
196
+ local session issue_id is_recurring is_scheduled count=0
197
+ ensure_running_issue_workers_cache
198
+ while IFS= read -r session; do
199
+ [[ -n "$session" ]] || continue
200
+ issue_id="$(issue_id_from_session "$session" || true)"
201
+ [[ -n "$issue_id" ]] || continue
202
+ is_scheduled="$(cached_issue_attr scheduled "$issue_id")"
203
+ if [[ "$is_scheduled" == "yes" ]]; then
204
+ continue
205
+ fi
206
+ is_recurring="$(cached_issue_attr recurring "$issue_id")"
207
+ if [[ "$is_recurring" == "yes" ]]; then
208
+ count=$((count + 1))
209
+ fi
210
+ done <<<"$running_issue_workers_cache"
211
+ # Also count pending recurring launches that are still in progress
212
+ # (prevents infinite respawning when workers die before creating tmux sessions)
213
+ count=$((count + $(pending_recurring_issue_launch_count)))
214
+ printf '%s\n' "$count"
215
+ }
216
+
217
+ running_blocked_recovery_issue_workers() {
218
+ local session issue_id count=0
219
+ ensure_running_issue_workers_cache
220
+ while IFS= read -r session; do
221
+ [[ -n "$session" ]] || continue
222
+ issue_id="$(issue_id_from_session "$session" || true)"
223
+ [[ -n "$issue_id" ]] || continue
224
+ if blocked_recovery_issue_has_state "$issue_id"; then
225
+ count=$((count + 1))
226
+ fi
227
+ done <<<"$running_issue_workers_cache"
228
+ printf '%s\n' "$count"
229
+ }
230
+
231
+ running_exclusive_issue_workers() {
232
+ local session issue_id is_exclusive count=0
233
+ ensure_running_issue_workers_cache
234
+ while IFS= read -r session; do
235
+ [[ -n "$session" ]] || continue
236
+ issue_id="$(issue_id_from_session "$session" || true)"
237
+ [[ -n "$issue_id" ]] || continue
238
+ is_exclusive="$(cached_issue_attr exclusive "$issue_id")"
239
+ if [[ "$is_exclusive" == "yes" ]]; then
240
+ count=$((count + 1))
241
+ fi
242
+ done <<<"$running_issue_workers_cache"
243
+ printf '%s\n' "$count"
244
+ }
245
+
246
+ running_exclusive_pr_workers() {
247
+ local session pr_id is_exclusive count=0
248
+ ensure_running_pr_workers_cache
249
+ while IFS= read -r session; do
250
+ [[ -n "$session" ]] || continue
251
+ pr_id="$(pr_id_from_session "$session" || true)"
252
+ [[ -n "$pr_id" ]] || continue
253
+ is_exclusive="$(cached_pr_is_exclusive "$pr_id")"
254
+ if [[ "$is_exclusive" == "yes" ]]; then
255
+ count=$((count + 1))
256
+ fi
257
+ done <<<"$running_pr_workers_cache"
258
+ printf '%s\n' "$count"
259
+ }
260
+
261
+ running_scheduled_issue_workers() {
262
+ local session issue_id is_scheduled count=0
263
+ ensure_running_issue_workers_cache
264
+ while IFS= read -r session; do
265
+ [[ -n "$session" ]] || continue
266
+ issue_id="$(issue_id_from_session "$session" || true)"
267
+ [[ -n "$issue_id" ]] || continue
268
+ is_scheduled="$(cached_issue_attr scheduled "$issue_id")"
269
+ if [[ "$is_scheduled" == "yes" ]]; then
270
+ count=$((count + 1))
271
+ fi
272
+ done <<<"$running_issue_workers_cache"
273
+ printf '%s\n' "$count"
274
+ }
275
+
276
+ running_scheduled_heavy_issue_workers() {
277
+ local session issue_id is_scheduled is_heavy count=0
278
+ ensure_running_issue_workers_cache
279
+ while IFS= read -r session; do
280
+ [[ -n "$session" ]] || continue
281
+ issue_id="$(issue_id_from_session "$session" || true)"
282
+ [[ -n "$issue_id" ]] || continue
283
+ is_scheduled="$(cached_issue_attr scheduled "$issue_id")"
284
+ is_heavy="$(cached_issue_attr heavy "$issue_id")"
285
+ if [[ "$is_scheduled" == "yes" && "$is_heavy" == "yes" ]]; then
286
+ count=$((count + 1))
287
+ fi
288
+ done <<<"$running_issue_workers_cache"
289
+ printf '%s\n' "$count"
290
+ }
291
+
292
+ ready_non_recurring_issue_count() {
293
+ local issue_id is_recurring count=0
294
+ ensure_ready_issue_ids_cache
295
+ while IFS= read -r issue_id; do
296
+ [[ -n "$issue_id" ]] || continue
297
+ if [[ "$(cached_issue_attr scheduled "$issue_id")" == "yes" ]]; then
298
+ continue
299
+ fi
300
+ is_recurring="$(cached_issue_attr recurring "$issue_id")"
301
+ if [[ "$is_recurring" != "yes" ]]; then
302
+ count=$((count + 1))
303
+ fi
304
+ done <<<"$ready_issue_ids_cache"
305
+ printf '%s\n' "$count"
306
+ }
@@ -0,0 +1,199 @@
1
+ #!/usr/bin/env bash
2
+ # heartbeat-loop-pr-strategy-lib.sh — PR candidate selection and strategy.
3
+ #
4
+ # Implements the priority-ordered lane dispatch for PR workers:
5
+ # double-check-2 > double-check-1 > automerge > merge-repair > fix > ci-refresh.
6
+ #
7
+ # Depends on: heartbeat-loop-worker-lib.sh, heartbeat-loop-cache-lib.sh,
8
+ # heartbeat-loop-scheduling-lib.sh
9
+
10
+ next_pr_candidate_json() {
11
+ local target_lane pr_number risk_json lane
12
+ ensure_open_agent_pr_ids_cache
13
+ for target_lane in double-check-2 double-check-1 automerge merge-repair fix ci-refresh; do
14
+ while IFS= read -r pr_number; do
15
+ [[ -n "$pr_number" ]] || continue
16
+ if tmux has-session -t "${pr_prefix}${pr_number}" 2>/dev/null; then
17
+ continue
18
+ fi
19
+ if pr_launch_reserved "$pr_number"; then
20
+ continue
21
+ fi
22
+ if pending_pr_launch_active "$pr_number"; then
23
+ continue
24
+ fi
25
+ if ! retry_ready pr "$pr_number"; then
26
+ continue
27
+ fi
28
+ risk_json="$(cached_pr_risk_json "$pr_number")"
29
+ lane="$(jq -r '.agentLane' <<<"$risk_json")"
30
+ if [[ "$lane" == "$target_lane" ]]; then
31
+ printf '%s\n' "$risk_json"
32
+ return 0
33
+ fi
34
+ done <<<"$open_agent_pr_ids_cache"
35
+ done
36
+ }
37
+
38
+ next_priority_review_pr_candidate_json() {
39
+ local target_lane pr_number risk_json lane
40
+ ensure_open_agent_pr_ids_cache
41
+ for target_lane in double-check-2 double-check-1; do
42
+ while IFS= read -r pr_number; do
43
+ [[ -n "$pr_number" ]] || continue
44
+ if tmux has-session -t "${pr_prefix}${pr_number}" 2>/dev/null; then
45
+ continue
46
+ fi
47
+ if pr_launch_reserved "$pr_number"; then
48
+ continue
49
+ fi
50
+ if pending_pr_launch_active "$pr_number"; then
51
+ continue
52
+ fi
53
+ if ! retry_ready pr "$pr_number"; then
54
+ continue
55
+ fi
56
+ risk_json="$(cached_pr_risk_json "$pr_number")"
57
+ lane="$(jq -r '.agentLane' <<<"$risk_json")"
58
+ if [[ "$lane" == "$target_lane" ]]; then
59
+ printf '%s\n' "$risk_json"
60
+ return 0
61
+ fi
62
+ done <<<"$open_agent_pr_ids_cache"
63
+ done
64
+ }
65
+
66
+ eligible_pr_backlog_count() {
67
+ local pr_number risk_json lane count=0
68
+ ensure_open_agent_pr_ids_cache
69
+ while IFS= read -r pr_number; do
70
+ [[ -n "$pr_number" ]] || continue
71
+ if tmux has-session -t "${pr_prefix}${pr_number}" 2>/dev/null; then
72
+ continue
73
+ fi
74
+ if pr_launch_reserved "$pr_number"; then
75
+ continue
76
+ fi
77
+ if pending_pr_launch_active "$pr_number"; then
78
+ continue
79
+ fi
80
+ if ! retry_ready pr "$pr_number"; then
81
+ continue
82
+ fi
83
+ risk_json="$(cached_pr_risk_json "$pr_number")"
84
+ lane="$(jq -r '.agentLane' <<<"$risk_json")"
85
+ case "$lane" in
86
+ double-check-1|double-check-2|automerge|merge-repair|fix)
87
+ count=$((count + 1))
88
+ ;;
89
+ esac
90
+ done <<<"$open_agent_pr_ids_cache"
91
+ printf '%s\n' "$count"
92
+ }
93
+
94
+ priority_review_backlog_count() {
95
+ local pr_number risk_json lane count=0
96
+ ensure_open_agent_pr_ids_cache
97
+ while IFS= read -r pr_number; do
98
+ [[ -n "$pr_number" ]] || continue
99
+ if tmux has-session -t "${pr_prefix}${pr_number}" 2>/dev/null; then
100
+ continue
101
+ fi
102
+ if pr_launch_reserved "$pr_number"; then
103
+ continue
104
+ fi
105
+ if pending_pr_launch_active "$pr_number"; then
106
+ continue
107
+ fi
108
+ if ! retry_ready pr "$pr_number"; then
109
+ continue
110
+ fi
111
+ risk_json="$(cached_pr_risk_json "$pr_number")"
112
+ lane="$(jq -r '.agentLane' <<<"$risk_json")"
113
+ case "$lane" in
114
+ double-check-1|double-check-2)
115
+ count=$((count + 1))
116
+ ;;
117
+ esac
118
+ done <<<"$open_agent_pr_ids_cache"
119
+ printf '%s\n' "$count"
120
+ }
121
+
122
+ next_exclusive_pr_candidate_json() {
123
+ local target_lane pr_number risk_json lane
124
+ ensure_exclusive_pr_ids_cache
125
+ for target_lane in double-check-2 double-check-1 automerge merge-repair fix ci-refresh; do
126
+ while IFS= read -r pr_number; do
127
+ [[ -n "$pr_number" ]] || continue
128
+ if tmux has-session -t "${pr_prefix}${pr_number}" 2>/dev/null; then
129
+ continue
130
+ fi
131
+ if pr_launch_reserved "$pr_number"; then
132
+ continue
133
+ fi
134
+ if pending_pr_launch_active "$pr_number"; then
135
+ continue
136
+ fi
137
+ if ! retry_ready pr "$pr_number"; then
138
+ continue
139
+ fi
140
+ risk_json="$(cached_pr_risk_json "$pr_number")"
141
+ lane="$(jq -r '.agentLane' <<<"$risk_json")"
142
+ # Skip PRs requiring human review; they should not hold exclusive lock
143
+ if [[ "$lane" == "human-review" ]]; then
144
+ continue
145
+ fi
146
+ if [[ "$lane" == "$target_lane" ]]; then
147
+ printf '%s\n' "$risk_json"
148
+ return 0
149
+ fi
150
+ done <<<"$exclusive_pr_ids_cache"
151
+ done
152
+ }
153
+
154
+ next_exclusive_issue_id() {
155
+ local issue_id
156
+ ensure_exclusive_issue_ids_cache
157
+ while IFS= read -r issue_id; do
158
+ [[ -n "$issue_id" ]] || continue
159
+ if tmux has-session -t "${issue_prefix}${issue_id}" 2>/dev/null; then
160
+ continue
161
+ fi
162
+ if pending_issue_launch_active "$issue_id"; then
163
+ continue
164
+ fi
165
+ if ! retry_ready issue "$issue_id"; then
166
+ continue
167
+ fi
168
+ printf '%s\n' "$issue_id"
169
+ return 0
170
+ done <<<"$exclusive_issue_ids_cache"
171
+ }
172
+
173
+ count_pr_lane() {
174
+ local target_lane="${1:?target lane required}"
175
+ local pr_number risk_json lane count=0
176
+ ensure_open_agent_pr_ids_cache
177
+ while IFS= read -r pr_number; do
178
+ [[ -n "$pr_number" ]] || continue
179
+ risk_json="$(cached_pr_risk_json "$pr_number")"
180
+ lane="$(jq -r '.agentLane' <<<"$risk_json")"
181
+ if [[ "$lane" == "$target_lane" ]]; then
182
+ count=$((count + 1))
183
+ fi
184
+ done <<<"$open_agent_pr_ids_cache"
185
+ printf '%s\n' "$count"
186
+ }
187
+
188
+ human_review_pr_ids() {
189
+ local pr_number risk_json lane
190
+ ensure_open_agent_pr_ids_cache
191
+ while IFS= read -r pr_number; do
192
+ [[ -n "$pr_number" ]] || continue
193
+ risk_json="$(cached_pr_risk_json "$pr_number")"
194
+ lane="$(jq -r '.agentLane' <<<"$risk_json")"
195
+ if [[ "$lane" == "human-review" ]]; then
196
+ printf '%s\n' "$pr_number"
197
+ fi
198
+ done <<<"$open_agent_pr_ids_cache"
199
+ }