agent-control-plane 0.4.9 → 0.7.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 (87) hide show
  1. package/README.md +109 -13
  2. package/npm/bin/agent-control-plane.js +1 -1
  3. package/package.json +39 -33
  4. package/tools/bin/debug-session.sh +106 -0
  5. package/tools/bin/flow-config-lib.sh +13 -3508
  6. package/tools/bin/flow-execution-lib.sh +243 -0
  7. package/tools/bin/flow-forge-lib.sh +1770 -0
  8. package/tools/bin/flow-profile-lib.sh +335 -0
  9. package/tools/bin/flow-provider-lib.sh +981 -0
  10. package/tools/bin/flow-runtime-doctor-linux.sh +136 -0
  11. package/tools/bin/flow-runtime-doctor.sh +5 -1
  12. package/tools/bin/flow-session-lib.sh +317 -0
  13. package/tools/bin/install-project-systemd.sh +255 -0
  14. package/tools/bin/project-runtimectl.sh +45 -0
  15. package/tools/bin/project-systemd-bootstrap.sh +74 -0
  16. package/tools/bin/uninstall-project-systemd.sh +87 -0
  17. package/tools/dashboard/app.js +238 -8
  18. package/tools/dashboard/issue_queue_state.py +101 -0
  19. package/tools/dashboard/requirements.txt +3 -0
  20. package/tools/dashboard/server.py +250 -30
  21. package/tools/dashboard/styles.css +526 -455
  22. package/tools/bin/agent-cleanup-worktree +0 -247
  23. package/tools/bin/agent-github-update-labels +0 -105
  24. package/tools/bin/agent-init-worktree +0 -216
  25. package/tools/bin/agent-project-archive-run +0 -52
  26. package/tools/bin/agent-project-capture-worker +0 -46
  27. package/tools/bin/agent-project-catch-up-issue-pr-links +0 -118
  28. package/tools/bin/agent-project-catch-up-merged-prs +0 -195
  29. package/tools/bin/agent-project-catch-up-scheduled-issue-retries +0 -123
  30. package/tools/bin/agent-project-cleanup-session +0 -513
  31. package/tools/bin/agent-project-detached-launch +0 -127
  32. package/tools/bin/agent-project-heartbeat-loop +0 -1029
  33. package/tools/bin/agent-project-open-issue-worktree +0 -89
  34. package/tools/bin/agent-project-open-pr-worktree +0 -80
  35. package/tools/bin/agent-project-publish-issue-pr +0 -468
  36. package/tools/bin/agent-project-reconcile-issue-session +0 -1409
  37. package/tools/bin/agent-project-reconcile-pr-session +0 -1288
  38. package/tools/bin/agent-project-retry-state +0 -158
  39. package/tools/bin/agent-project-run-claude-session +0 -805
  40. package/tools/bin/agent-project-run-codex-resilient +0 -963
  41. package/tools/bin/agent-project-run-codex-session +0 -435
  42. package/tools/bin/agent-project-run-kilo-session +0 -369
  43. package/tools/bin/agent-project-run-ollama-session +0 -658
  44. package/tools/bin/agent-project-run-openclaw-session +0 -1309
  45. package/tools/bin/agent-project-run-opencode-session +0 -377
  46. package/tools/bin/agent-project-run-pi-session +0 -479
  47. package/tools/bin/agent-project-sync-anchor-repo +0 -139
  48. package/tools/bin/agent-project-sync-source-repo-main +0 -163
  49. package/tools/bin/agent-project-worker-status +0 -188
  50. package/tools/bin/branch-verification-guard.sh +0 -364
  51. package/tools/bin/capture-worker.sh +0 -18
  52. package/tools/bin/cleanup-worktree.sh +0 -52
  53. package/tools/bin/codex-quota +0 -31
  54. package/tools/bin/create-follow-up-issue.sh +0 -114
  55. package/tools/bin/dashboard-launchd-bootstrap.sh +0 -50
  56. package/tools/bin/issue-publish-localization-guard.sh +0 -142
  57. package/tools/bin/issue-publish-scope-guard.sh +0 -242
  58. package/tools/bin/issue-requires-local-workspace-install.sh +0 -31
  59. package/tools/bin/issue-resource-class.sh +0 -12
  60. package/tools/bin/kick-scheduler.sh +0 -75
  61. package/tools/bin/label-follow-up-issues.sh +0 -14
  62. package/tools/bin/new-pr-worktree.sh +0 -50
  63. package/tools/bin/new-worktree.sh +0 -49
  64. package/tools/bin/pr-risk.sh +0 -12
  65. package/tools/bin/prepare-worktree.sh +0 -142
  66. package/tools/bin/provider-cooldown-state.sh +0 -204
  67. package/tools/bin/publish-issue-worker.sh +0 -31
  68. package/tools/bin/reconcile-bootstrap-lib.sh +0 -113
  69. package/tools/bin/reconcile-issue-worker.sh +0 -34
  70. package/tools/bin/reconcile-pr-worker.sh +0 -34
  71. package/tools/bin/record-verification.sh +0 -71
  72. package/tools/bin/render-flow-config.sh +0 -98
  73. package/tools/bin/resident-issue-controller-lib.sh +0 -448
  74. package/tools/bin/retry-state.sh +0 -31
  75. package/tools/bin/reuse-issue-worktree.sh +0 -121
  76. package/tools/bin/run-codex-bypass.sh +0 -3
  77. package/tools/bin/run-codex-safe.sh +0 -3
  78. package/tools/bin/run-codex-task.sh +0 -280
  79. package/tools/bin/serve-dashboard.sh +0 -5
  80. package/tools/bin/start-issue-worker.sh +0 -943
  81. package/tools/bin/start-pr-fix-worker.sh +0 -528
  82. package/tools/bin/start-pr-merge-repair-worker.sh +0 -8
  83. package/tools/bin/start-pr-review-worker.sh +0 -261
  84. package/tools/bin/start-resident-issue-loop.sh +0 -499
  85. package/tools/bin/update-github-labels.sh +0 -14
  86. package/tools/bin/worker-status.sh +0 -19
  87. package/tools/bin/workflow-catalog.sh +0 -77
@@ -1,1409 +0,0 @@
1
- #!/usr/bin/env bash
2
- set -euo pipefail
3
-
4
- SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
5
- RECONCILE_BOOTSTRAP_LIB=""
6
- for _rbl_candidate in \
7
- "${SCRIPT_DIR}/reconcile-bootstrap-lib.sh" \
8
- "${AGENT_CONTROL_PLANE_ROOT:-}/tools/bin/reconcile-bootstrap-lib.sh" \
9
- "${ACP_ROOT:-}/tools/bin/reconcile-bootstrap-lib.sh" \
10
- "${SHARED_AGENT_HOME:-}/tools/bin/reconcile-bootstrap-lib.sh"; do
11
- if [[ -n "${_rbl_candidate}" && -f "${_rbl_candidate}" ]]; then
12
- RECONCILE_BOOTSTRAP_LIB="${_rbl_candidate}"
13
- break
14
- fi
15
- done
16
- if [[ -n "${SHARED_AGENT_HOME:-}" && -z "${RECONCILE_BOOTSTRAP_LIB}" ]]; then
17
- for _rbl_skill in "${AGENT_CONTROL_PLANE_SKILL_NAME:-agent-control-plane}" "${AGENT_CONTROL_PLANE_COMPAT_ALIAS:-}"; do
18
- [[ -n "${_rbl_skill}" ]] || continue
19
- _rbl_candidate="${SHARED_AGENT_HOME}/skills/openclaw/${_rbl_skill}/tools/bin/reconcile-bootstrap-lib.sh"
20
- if [[ -f "${_rbl_candidate}" ]]; then
21
- RECONCILE_BOOTSTRAP_LIB="${_rbl_candidate}"
22
- break
23
- fi
24
- done
25
- fi
26
- if [[ -z "${RECONCILE_BOOTSTRAP_LIB}" ]]; then
27
- echo "unable to locate reconcile-bootstrap-lib.sh" >&2
28
- exit 1
29
- fi
30
- # shellcheck source=/dev/null
31
- source "${RECONCILE_BOOTSTRAP_LIB}"
32
-
33
- usage() {
34
- cat <<'EOF'
35
- Usage:
36
- agent-project-reconcile-issue-session --session <id> --repo-slug <owner/repo> --repo-root <path> --runs-root <path> --history-root <path> [--hook-file <path>]
37
-
38
- Reconcile a completed issue worker run using shared lifecycle control flow while
39
- allowing project adapters to inject policy hooks.
40
- EOF
41
- }
42
-
43
- FLOW_RESIDENT_WORKER_LIB_PATH="$(resolve_reconcile_helper_path "flow-resident-worker-lib.sh")"
44
- # shellcheck source=/dev/null
45
- source "${FLOW_RESIDENT_WORKER_LIB_PATH}"
46
- session=""
47
- repo_slug=""
48
- repo_root=""
49
- runs_root=""
50
- history_root=""
51
- hook_file=""
52
- record_verification_script="${shared_tools_dir}/record-verification.sh"
53
-
54
- while [[ $# -gt 0 ]]; do
55
- case "$1" in
56
- --session) session="${2:-}"; shift 2 ;;
57
- --repo-slug) repo_slug="${2:-}"; shift 2 ;;
58
- --repo-root) repo_root="${2:-}"; shift 2 ;;
59
- --runs-root) runs_root="${2:-}"; shift 2 ;;
60
- --history-root) history_root="${2:-}"; shift 2 ;;
61
- --hook-file) hook_file="${2:-}"; shift 2 ;;
62
- --help|-h) usage; exit 0 ;;
63
- *) echo "Unknown argument: $1" >&2; usage >&2; exit 1 ;;
64
- esac
65
- done
66
-
67
- if [[ -z "$session" || -z "$repo_slug" || -z "$repo_root" || -z "$runs_root" || -z "$history_root" ]]; then
68
- usage >&2
69
- exit 1
70
- fi
71
-
72
- status_out="$(
73
- "${shared_tools_dir}/agent-project-worker-status" \
74
- --runs-root "$runs_root" \
75
- --session "$session"
76
- )"
77
- status="$(awk -F= '/^STATUS=/{print $2}' <<<"$status_out")"
78
- failure_reason="$(awk -F= '/^FAILURE_REASON=/{print $2}' <<<"$status_out" | tail -n 1)"
79
-
80
- if [[ "$status" == "RUNNING" ]]; then
81
- printf 'STATUS=%s\n' "$status"
82
- exit 0
83
- fi
84
-
85
- find_archived_session_dir() {
86
- local root="${1:-}"
87
- local target_session="${2:-}"
88
- [[ -n "$root" && -d "$root" && -n "$target_session" ]] || return 1
89
-
90
- find "$root" -mindepth 1 -maxdepth 1 -type d -name "${target_session}-*" ! -name "${target_session}-stale-*" 2>/dev/null \
91
- | sort -r \
92
- | head -n 1
93
- }
94
-
95
- meta_file="$(awk -F= '/^META_FILE=/{print $2}' <<<"$status_out")"
96
- if [[ -z "$meta_file" || ! -f "$meta_file" ]]; then
97
- archived_run_dir="$(find_archived_session_dir "$history_root" "$session" || true)"
98
- if [[ -n "$archived_run_dir" && -f "${archived_run_dir}/run.env" ]]; then
99
- meta_file="${archived_run_dir}/run.env"
100
- if [[ "$status" == "UNKNOWN" && -f "${archived_run_dir}/runner.env" ]]; then
101
- set -a
102
- # shellcheck source=/dev/null
103
- source "${archived_run_dir}/runner.env"
104
- set +a
105
- case "${RUNNER_STATE:-}" in
106
- succeeded)
107
- status="SUCCEEDED"
108
- ;;
109
- failed)
110
- status="FAILED"
111
- failure_reason="${LAST_FAILURE_REASON:-${failure_reason:-}}"
112
- ;;
113
- esac
114
- fi
115
- if [[ "$status" == "UNKNOWN" && -f "${archived_run_dir}/result.env" ]]; then
116
- status="SUCCEEDED"
117
- fi
118
- fi
119
- fi
120
- if [[ -z "$meta_file" || ! -f "$meta_file" ]]; then
121
- echo "missing metadata for session $session" >&2
122
- exit 1
123
- fi
124
-
125
- run_dir="$(dirname "$meta_file")"
126
-
127
- set -a
128
- # shellcheck source=/dev/null
129
- source "$meta_file"
130
- set +a
131
-
132
- result_outcome=""
133
- result_action=""
134
- run_started_at="${STARTED_AT:-}"
135
- expected_run_started_at="${ACP_EXPECTED_RUN_STARTED_AT:-${F_LOSNING_EXPECTED_RUN_STARTED_AT:-}}"
136
- result_file_candidate="${run_dir}/result.env"
137
- if [[ ! -f "$result_file_candidate" && -n "${RESULT_FILE:-}" && -f "${RESULT_FILE:-}" ]]; then
138
- result_file_candidate="${RESULT_FILE}"
139
- fi
140
- if [[ -f "$result_file_candidate" ]]; then
141
- set -a
142
- # shellcheck source=/dev/null
143
- source "$result_file_candidate"
144
- set +a
145
- result_outcome="${OUTCOME:-}"
146
- result_action="${ACTION:-}"
147
- fi
148
-
149
- if [[ -n "${expected_run_started_at}" && "${expected_run_started_at}" != "${run_started_at}" ]]; then
150
- printf 'STATUS=STALE-RUN-SKIPPED\n'
151
- printf 'SESSION=%s\n' "$session"
152
- printf 'EXPECTED_STARTED_AT=%s\n' "${expected_run_started_at}"
153
- printf 'ACTUAL_STARTED_AT=%s\n' "${run_started_at}"
154
- exit 0
155
- fi
156
-
157
- issue_summary_outcome=""
158
- issue_summary_action=""
159
- issue_summary_failure_reason=""
160
-
161
- issue_set_reconcile_summary() {
162
- local summary_status="${1:-${status:-}}"
163
- local summary_outcome="__ISSUE_DEFAULT__"
164
- local summary_action="__ISSUE_DEFAULT__"
165
- local summary_failure_reason="__ISSUE_DEFAULT__"
166
-
167
- if [[ $# -ge 2 ]]; then
168
- summary_outcome="${2}"
169
- fi
170
- if [[ $# -ge 3 ]]; then
171
- summary_action="${3}"
172
- fi
173
- if [[ $# -ge 4 ]]; then
174
- summary_failure_reason="${4}"
175
- fi
176
-
177
- if [[ "${summary_outcome}" == "__ISSUE_DEFAULT__" ]]; then
178
- if [[ "${summary_status}" == "SUCCEEDED" ]]; then
179
- summary_outcome="${result_outcome:-}"
180
- else
181
- summary_outcome=""
182
- fi
183
- fi
184
-
185
- if [[ "${summary_action}" == "__ISSUE_DEFAULT__" ]]; then
186
- if [[ "${summary_status}" == "SUCCEEDED" ]]; then
187
- summary_action="${result_action:-}"
188
- else
189
- summary_action=""
190
- fi
191
- fi
192
-
193
- if [[ "${summary_failure_reason}" == "__ISSUE_DEFAULT__" ]]; then
194
- summary_failure_reason="${failure_reason:-}"
195
- fi
196
-
197
- issue_summary_outcome="${summary_outcome}"
198
- issue_summary_action="${summary_action}"
199
- issue_summary_failure_reason="${summary_failure_reason}"
200
- }
201
-
202
- issue_set_reconcile_summary "$status"
203
-
204
- issue_id="${ISSUE_ID:-}"
205
- if [[ -z "$issue_id" ]]; then
206
- echo "session $session is missing ISSUE_ID" >&2
207
- exit 1
208
- fi
209
-
210
- owner="${repo_slug%%/*}"
211
- repo="${repo_slug#*/}"
212
- pr_number=""
213
-
214
- issue_before_success() { :; }
215
- issue_before_blocked() { :; }
216
- issue_schedule_retry() { :; }
217
- issue_mark_ready() { :; }
218
- issue_clear_retry() { :; }
219
- issue_remove_running() { :; }
220
- issue_mark_blocked() { issue_remove_running; }
221
- issue_should_close_as_superseded() { return 1; }
222
- issue_close_as_superseded() { :; }
223
- issue_after_pr_created() { :; }
224
- issue_after_reconciled() { :; }
225
- issue_publish_extra_args() { :; }
226
- issue_result_contract_note=""
227
-
228
- if [[ -n "$hook_file" && -f "$hook_file" ]]; then
229
- # shellcheck source=/dev/null
230
- source "$hook_file"
231
- fi
232
-
233
- provider_cooldown_script="${shared_tools_dir}/provider-cooldown-state.sh"
234
- github_write_outbox_script="${shared_tools_dir}/github-write-outbox.sh"
235
-
236
- schedule_provider_quota_cooldown() {
237
- local reason="${1:-provider-quota-limit}"
238
- [[ "${reason}" == "provider-quota-limit" ]] || return 0
239
- [[ -x "${provider_cooldown_script}" ]] || return 0
240
- [[ "${CODING_WORKER:-}" == "codex" ]] && return 0
241
-
242
- "${provider_cooldown_script}" schedule "${reason}" >/dev/null || true
243
- }
244
-
245
- clear_provider_quota_cooldown() {
246
- [[ -x "${provider_cooldown_script}" ]] || return 0
247
- [[ "${CODING_WORKER:-}" == "codex" ]] && return 0
248
-
249
- "${provider_cooldown_script}" clear >/dev/null || true
250
- }
251
-
252
- normalize_issue_failure_reason() {
253
- local current_reason="${1:-}"
254
-
255
- case "${current_reason}" in
256
- usage-limit|quota-switch-deferred|quota-switch-attempt-limit)
257
- if [[ "${CODING_WORKER:-}" == "codex" ]]; then
258
- printf 'provider-quota-limit\n'
259
- return 0
260
- fi
261
- ;;
262
- esac
263
-
264
- printf '%s\n' "${current_reason}"
265
- }
266
-
267
- issue_runtime_log_file() {
268
- if [[ -f "${run_dir}/${session}.log" ]]; then
269
- printf '%s\n' "${run_dir}/${session}.log"
270
- return 0
271
- fi
272
-
273
- find "${run_dir}" -maxdepth 1 -type f -name '*.log' 2>/dev/null | LC_ALL=C sort | tail -n 1
274
- }
275
-
276
- infer_issue_runtime_failure_from_log() {
277
- local log_file=""
278
-
279
- log_file="$(issue_runtime_log_file)"
280
- [[ -n "${log_file}" && -f "${log_file}" ]] || return 1
281
-
282
- if grep -Eiq 'stale-run no-codex-output-before-stall-threshold|no-codex-output-before-stall-threshold' "${log_file}" 2>/dev/null; then
283
- printf 'no-codex-output-before-stall-threshold\n'
284
- return 0
285
- fi
286
-
287
- if grep -Eiq 'stale-run no-codex-progress-before-stall-threshold|no-codex-progress-before-stall-threshold' "${log_file}" 2>/dev/null; then
288
- printf 'no-codex-progress-before-stall-threshold\n'
289
- return 0
290
- fi
291
-
292
- if grep -Eiq 'stale-run no-agent-output-before-stall-threshold|no-agent-output-before-stall-threshold' "${log_file}" 2>/dev/null; then
293
- printf 'no-agent-output-before-stall-threshold\n'
294
- return 0
295
- fi
296
-
297
- if grep -Eiq 'stale-run no-agent-progress-before-stall-threshold|no-agent-progress-before-stall-threshold' "${log_file}" 2>/dev/null; then
298
- printf 'no-agent-progress-before-stall-threshold\n'
299
- return 0
300
- fi
301
-
302
- if grep -Eiq 'Ignoring invalid cwd .* No such file or directory|/tmp is absolute|Custom tool call output is missing' "${log_file}" 2>/dev/null; then
303
- printf 'worker-environment-blocked\n'
304
- return 0
305
- fi
306
-
307
- return 1
308
- }
309
-
310
- normalize_issue_result_contract() {
311
- [[ "$status" == "SUCCEEDED" ]] || return 0
312
-
313
- case "${result_outcome:-}:${result_action:-}" in
314
- implemented:host-publish-issue-pr)
315
- return 0
316
- ;;
317
- blocked:host-comment-blocker)
318
- return 0
319
- ;;
320
- reported:host-comment-scheduled-report)
321
- return 0
322
- ;;
323
- reported:host-comment-scheduled-alert)
324
- return 0
325
- ;;
326
- *)
327
- echo "invalid issue worker result contract for session ${session}: OUTCOME='${result_outcome:-}' ACTION='${result_action:-}'" >&2
328
- return 1
329
- ;;
330
- esac
331
- }
332
-
333
- post_issue_comment_if_present() {
334
- local comment_file="${run_dir}/issue-comment.md"
335
- [[ -s "$comment_file" ]] || return 0
336
- if issue_latest_comment_matches_artifact; then
337
- return 0
338
- fi
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
350
- }
351
-
352
- issue_latest_comment_matches_artifact() {
353
- local comment_file="${run_dir}/issue-comment.md"
354
- local comment_body issue_json
355
- [[ -s "${comment_file}" ]] || return 1
356
- comment_body="$(cat "${comment_file}")"
357
- issue_json="$(flow_github_issue_view_json "${repo_slug}" "${issue_id}" 2>/dev/null || true)"
358
- [[ -n "${issue_json}" ]] || return 1
359
- jq -e --arg body "${comment_body}" '((.comments // [])[-1]?.body // "") == $body' >/dev/null <<<"${issue_json}"
360
- }
361
-
362
- write_issue_comment_artifact() {
363
- local comment_body="${1:-}"
364
- local comment_file="${run_dir}/issue-comment.md"
365
- [[ -n "${comment_body}" ]] || return 1
366
- printf '%s\n' "${comment_body}" >"${comment_file}"
367
- }
368
-
369
- issue_has_no_publishable_delta() {
370
- local worktree_path="${WORKTREE:-}"
371
- local default_branch="${ACP_DEFAULT_BRANCH:-${F_LOSNING_DEFAULT_BRANCH:-main}}"
372
- local baseline_ref=""
373
- local ahead_count=""
374
- local dirty_state=""
375
- local ref=""
376
- local candidate_refs=(
377
- "origin/${default_branch}"
378
- "${default_branch}"
379
- "origin/main"
380
- "main"
381
- "origin/master"
382
- "master"
383
- )
384
- local seen_refs=" "
385
-
386
- [[ -n "${worktree_path}" && -d "${worktree_path}" ]] || return 1
387
- git -C "${worktree_path}" rev-parse --git-dir >/dev/null 2>&1 || return 1
388
-
389
- for ref in "${candidate_refs[@]}"; do
390
- [[ -n "${ref}" ]] || continue
391
- if [[ "${seen_refs}" == *" ${ref} "* ]]; then
392
- continue
393
- fi
394
- seen_refs="${seen_refs}${ref} "
395
- if git -C "${worktree_path}" rev-parse --verify "${ref}" >/dev/null 2>&1; then
396
- baseline_ref="${ref}"
397
- break
398
- fi
399
- done
400
-
401
- [[ -n "${baseline_ref}" ]] || return 1
402
-
403
- ahead_count="$(git -C "${worktree_path}" rev-list --count "${baseline_ref}..HEAD" 2>/dev/null || true)"
404
- case "${ahead_count}" in
405
- 0) ;;
406
- *) return 1 ;;
407
- esac
408
-
409
- dirty_state="$(git -C "${worktree_path}" status --porcelain --untracked-files=no 2>/dev/null || true)"
410
- [[ -z "${dirty_state}" ]] || return 1
411
-
412
- return 0
413
- }
414
-
415
- ensure_issue_blocked_comment_artifact() {
416
- local comment_file="${run_dir}/issue-comment.md"
417
- local blocker_reason=""
418
- local verification_file=""
419
- local comment_body=""
420
-
421
- [[ -s "${comment_file}" ]] && return 0
422
-
423
- if issue_has_no_publishable_delta; then
424
- blocker_reason="no-publishable-commits"
425
- fi
426
-
427
- if [[ -z "${blocker_reason}" ]]; then
428
- verification_file="$(issue_verification_file)"
429
- if [[ ! -f "${verification_file}" ]] || ! grep -q '"status":"pass"' "${verification_file}" 2>/dev/null; then
430
- blocker_reason="verification-guard-blocked"
431
- fi
432
- fi
433
-
434
- comment_body="$(build_issue_publish_blocker_comment "${blocker_reason}" "")"
435
- write_issue_comment_artifact "${comment_body}" || true
436
- }
437
-
438
- issue_verification_file() {
439
- printf '%s\n' "${run_dir}/verification.jsonl"
440
- }
441
-
442
- normalize_issue_runner_state() {
443
- local normalized_state="${1:?normalized state required}"
444
- local normalized_exit_code="${2:-}"
445
- local normalized_failure_reason="${3:-}"
446
- local runner_state_file="${run_dir}/runner.env"
447
- local thread_id=""
448
- local attempt="1"
449
- local resume_count="0"
450
- local last_exit_code=""
451
- local last_failure_reason=""
452
- local last_trigger_reason=""
453
- local auth_wait_started_at=""
454
- local last_auth_fingerprint=""
455
-
456
- [[ -f "${runner_state_file}" ]] || return 0
457
-
458
- set +u
459
- set -a
460
- # shellcheck source=/dev/null
461
- source "${runner_state_file}"
462
- set +a
463
- set -u
464
-
465
- thread_id="${THREAD_ID:-}"
466
- attempt="${ATTEMPT:-1}"
467
- resume_count="${RESUME_COUNT:-0}"
468
- last_exit_code="${LAST_EXIT_CODE:-}"
469
- last_failure_reason="${LAST_FAILURE_REASON:-}"
470
- last_trigger_reason="${LAST_TRIGGER_REASON:-}"
471
- auth_wait_started_at="${AUTH_WAIT_STARTED_AT:-}"
472
- last_auth_fingerprint="${LAST_AUTH_FINGERPRINT:-}"
473
-
474
- if [[ -n "${normalized_exit_code}" ]]; then
475
- last_exit_code="${normalized_exit_code}"
476
- fi
477
- if [[ -n "${normalized_failure_reason}" || "${normalized_state}" == "succeeded" ]]; then
478
- last_failure_reason="${normalized_failure_reason}"
479
- fi
480
-
481
- flow_resident_write_metadata "${runner_state_file}" \
482
- "RUNNER_STATE=${normalized_state}" \
483
- "THREAD_ID=${thread_id}" \
484
- "ATTEMPT=${attempt}" \
485
- "RESUME_COUNT=${resume_count}" \
486
- "LAST_EXIT_CODE=${last_exit_code}" \
487
- "LAST_FAILURE_REASON=${last_failure_reason}" \
488
- "LAST_TRIGGER_REASON=${last_trigger_reason}" \
489
- "AUTH_WAIT_STARTED_AT=${auth_wait_started_at}" \
490
- "LAST_AUTH_FINGERPRINT=${last_auth_fingerprint}" \
491
- "UPDATED_AT=$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
492
- }
493
-
494
- issue_has_recorded_verification() {
495
- local verification_file
496
- verification_file="$(issue_verification_file)"
497
- [[ -f "$verification_file" ]] || return 1
498
- grep -q '"status":"pass"' "$verification_file" 2>/dev/null
499
- }
500
-
501
- extract_issue_host_recovery_commands() {
502
- local prompt_file="${run_dir}/prompt.md"
503
- local worktree_path="${WORKTREE:-}"
504
- local verification_file
505
- verification_file="$(issue_verification_file)"
506
-
507
- [[ -n "$worktree_path" && -d "$worktree_path" ]] || return 0
508
-
509
- PROMPT_FILE="$prompt_file" \
510
- WORKTREE_PATH="$worktree_path" \
511
- VERIFICATION_FILE="$verification_file" \
512
- node <<'EOF'
513
- const fs = require('fs');
514
- const path = require('path');
515
- const cp = require('child_process');
516
-
517
- const promptFile = process.env.PROMPT_FILE || '';
518
- const worktreePath = process.env.WORKTREE_PATH || '';
519
- const verificationFile = process.env.VERIFICATION_FILE || '';
520
-
521
- const commands = [];
522
- const seen = new Set();
523
-
524
- const addCommand = (value) => {
525
- const command = String(value || '').trim();
526
- if (!command) return;
527
- if (seen.has(command)) return;
528
- seen.add(command);
529
- commands.push(command);
530
- };
531
-
532
- let recordedPassCommands = new Set();
533
- if (verificationFile && fs.existsSync(verificationFile)) {
534
- const raw = fs.readFileSync(verificationFile, 'utf8');
535
- for (const line of raw.split('\n')) {
536
- const trimmed = line.trim();
537
- if (!trimmed) continue;
538
- try {
539
- const entry = JSON.parse(trimmed);
540
- if (entry && entry.status === 'pass' && typeof entry.command === 'string') {
541
- recordedPassCommands.add(entry.command.trim());
542
- }
543
- } catch (_error) {
544
- // Ignore malformed history entries during recovery.
545
- }
546
- }
547
- }
548
-
549
- let packageJson = null;
550
- const packageJsonPath = path.join(worktreePath, 'package.json');
551
- if (fs.existsSync(packageJsonPath)) {
552
- try {
553
- packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
554
- } catch (_error) {
555
- packageJson = null;
556
- }
557
- }
558
-
559
- const gitChangedFiles = [];
560
- try {
561
- const raw = cp.execFileSync(
562
- 'git',
563
- [
564
- '-C',
565
- worktreePath,
566
- 'show',
567
- '--pretty=',
568
- '--name-only',
569
- 'HEAD',
570
- ],
571
- { encoding: 'utf8' },
572
- );
573
- for (const line of raw.split('\n')) {
574
- const file = line.trim();
575
- if (file) gitChangedFiles.push(file);
576
- }
577
- } catch (_error) {
578
- // Ignore; recovery can still use prompt-derived commands.
579
- }
580
-
581
- const changedFilesLower = gitChangedFiles.map((file) => file.toLowerCase());
582
- const repoHasScript = (scriptName) => Boolean(packageJson?.scripts && Object.prototype.hasOwnProperty.call(packageJson.scripts, scriptName));
583
- const rootTestScript = String(packageJson?.scripts?.test || '').trim();
584
- const rootTestScriptUsesNodeTest = /^node\s+--test(?:\s|$)/.test(rootTestScript);
585
- const rootTestScriptLooksWatchMode = /\B--watch(?:All)?(?:[=\s]|$)|(?:^|\s)vitest\s+watch(?:\s|$)/.test(rootTestScript);
586
- const commandLooksRunnable = (command) => {
587
- if (/^npm test(?:\s|$)?/.test(command)) return repoHasScript('test');
588
- if (/^pnpm test(?:\s|$)?/.test(command)) return repoHasScript('test');
589
- if (/^npm run\s+([a-z0-9:_-]+)/i.test(command)) return repoHasScript(command.match(/^npm run\s+([a-z0-9:_-]+)/i)[1]);
590
- if (/^pnpm run\s+([a-z0-9:_-]+)/i.test(command)) return repoHasScript(command.match(/^pnpm run\s+([a-z0-9:_-]+)/i)[1]);
591
- if (/^node\s+--test(?:\s|$)/.test(command)) return true;
592
- return true;
593
- };
594
- const rootTestFallbackCommand = () => {
595
- if (!rootTestScript) return '';
596
- if (rootTestScriptUsesNodeTest) return 'npm test';
597
- if (!rootTestScriptLooksWatchMode) return 'npm test';
598
- if (/\bjest\b/i.test(rootTestScript)) return 'npx jest --runInBand --watchAll=false';
599
- if (/\bvitest\b/i.test(rootTestScript)) return 'npx vitest run';
600
- return '';
601
- };
602
-
603
- if (promptFile && fs.existsSync(promptFile)) {
604
- const lines = fs.readFileSync(promptFile, 'utf8').split(/\r?\n/).slice(0, 40);
605
- for (const line of lines) {
606
- if (!/^\s*-\s+/.test(line)) continue;
607
- if (!/(?:\bRun\b|\balso run\b|\bafter code changes\b|\bevery completed cycle\b)/i.test(line)) continue;
608
-
609
- const trimmed = line.trim();
610
- if (/^- If /i.test(trimmed)) {
611
- const lowered = trimmed.toLowerCase();
612
- if (lowered.includes('cli') || lowered.includes('fixture')) {
613
- const cliOrFixtureTouched = changedFilesLower.some((file) => /(?:^|\/)(src\/summary-cli|fixtures?\/|fixtures?\.|cli\b)/.test(file));
614
- if (!cliOrFixtureTouched) {
615
- continue;
616
- }
617
- } else {
618
- continue;
619
- }
620
- }
621
-
622
- for (const match of line.matchAll(/`([^`]+)`/g)) {
623
- const command = String(match[1] || '').trim();
624
- if (commandLooksRunnable(command)) {
625
- addCommand(command);
626
- }
627
- }
628
- }
629
- }
630
-
631
- const changedTestFiles = [...new Set(gitChangedFiles.filter((file) => /\.(?:spec|test)\.[cm]?[jt]sx?$/.test(file)))];
632
- if (rootTestScriptUsesNodeTest) {
633
- for (const file of changedTestFiles) {
634
- addCommand(`node --test ${file}`);
635
- }
636
- }
637
-
638
- if (commands.length === 0) {
639
- const fallbackCommand = rootTestFallbackCommand();
640
- if (fallbackCommand) {
641
- addCommand(fallbackCommand);
642
- }
643
- }
644
-
645
- const filtered = commands.filter((command) => !recordedPassCommands.has(command));
646
- process.stdout.write(filtered.join('\n'));
647
- EOF
648
- }
649
-
650
- run_issue_host_verification_command() {
651
- local command_text="${1:?command text required}"
652
- local session_log_file="${run_dir}/${session}.log"
653
-
654
- {
655
- printf '\n[host-issue-recovery] command=%s\n' "$command_text"
656
- } >>"$session_log_file"
657
-
658
- if WORKTREE_DIR="${WORKTREE:?missing worktree for host recovery}" HOST_COMMAND="$command_text" bash -lc 'set -euo pipefail; cd "$WORKTREE_DIR"; eval "$HOST_COMMAND"' >>"$session_log_file" 2>&1; then
659
- if [[ -x "$record_verification_script" ]]; then
660
- bash "$record_verification_script" --run-dir "$run_dir" --status pass --command "$command_text" --note "host-recovery-after-missing-worker-verification" >/dev/null
661
- fi
662
- return 0
663
- fi
664
-
665
- if [[ -x "$record_verification_script" ]]; then
666
- bash "$record_verification_script" --run-dir "$run_dir" --status fail --command "$command_text" --note "host-recovery-after-missing-worker-verification" >/dev/null
667
- fi
668
- printf '[host-issue-recovery] command failed=%s\n' "$command_text" >>"$session_log_file"
669
- return 1
670
- }
671
-
672
- attempt_issue_host_verification_recovery() {
673
- local recovery_reason="${1:-missing-worker-verification}"
674
- local recovery_commands=""
675
- local command_text=""
676
-
677
- [[ "${result_outcome:-}" == "implemented" ]] || return 1
678
- [[ -n "${WORKTREE:-}" && -d "${WORKTREE:-}" ]] || return 1
679
-
680
- recovery_commands="$(extract_issue_host_recovery_commands)"
681
- [[ -n "$recovery_commands" ]] || return 1
682
-
683
- while IFS= read -r command_text; do
684
- [[ -n "$command_text" ]] || continue
685
- if ! run_issue_host_verification_command "$command_text"; then
686
- return 1
687
- fi
688
- done <<<"$recovery_commands"
689
-
690
- issue_result_contract_note="host-recovered-${recovery_reason}"
691
- return 0
692
- }
693
-
694
- classify_issue_publish_blocker() {
695
- local publish_out="${1:-}"
696
-
697
- if grep -Fq 'Scope guard blocked issue' <<<"$publish_out"; then
698
- printf 'scope-guard-blocked\n'
699
- return 0
700
- fi
701
-
702
- if grep -Fq 'Verification guard blocked branch publication.' <<<"$publish_out"; then
703
- printf 'verification-guard-blocked\n'
704
- return 0
705
- fi
706
-
707
- if grep -Fq 'Localization guard blocked branch publication.' <<<"$publish_out"; then
708
- printf 'localization-guard-blocked\n'
709
- return 0
710
- fi
711
-
712
- if grep -Fq 'has no commits ahead of' <<<"$publish_out"; then
713
- printf 'no-publishable-commits\n'
714
- return 0
715
- fi
716
-
717
- return 1
718
- }
719
-
720
- refresh_recurring_issue_checklist() {
721
- local sync_script="${shared_tools_dir}/sync-recurring-issue-checklist.sh"
722
- [[ -x "${sync_script}" ]] || return 1
723
- bash "${sync_script}" --repo-slug "${repo_slug}" --issue-id "${issue_id}" 2>/dev/null || return 1
724
- }
725
-
726
- build_issue_publish_blocker_comment() {
727
- local blocker_reason="${1:-}"
728
- local publish_out="${2:-}"
729
- local sync_out=""
730
- local checklist_total="0"
731
- local checklist_unchecked="0"
732
- local checklist_matched_prs=""
733
-
734
- if [[ "${blocker_reason}" == "no-publishable-commits" ]]; then
735
- sync_out="$(refresh_recurring_issue_checklist || true)"
736
- checklist_total="$(awk -F= '/^CHECKLIST_TOTAL=/{print $2; exit}' <<<"${sync_out:-}")"
737
- checklist_unchecked="$(awk -F= '/^CHECKLIST_UNCHECKED=/{print $2; exit}' <<<"${sync_out:-}")"
738
- checklist_matched_prs="$(awk -F= '/^CHECKLIST_MATCHED_PR_NUMBERS=/{print $2; exit}' <<<"${sync_out:-}")"
739
-
740
- case "${checklist_total}" in
741
- ''|*[!0-9]*) checklist_total="0" ;;
742
- esac
743
- case "${checklist_unchecked}" in
744
- ''|*[!0-9]*) checklist_unchecked="0" ;;
745
- esac
746
-
747
- if [[ "${checklist_total}" -gt 0 && "${checklist_unchecked}" -eq 0 ]]; then
748
- cat <<EOF
749
- # Blocker: All checklist items already completed
750
-
751
- All checklist items for issue #${issue_id} appear to be satisfied on the current baseline.
752
-
753
- Why it was blocked:
754
- - the worker completed a cycle, but the resulting branch had no commits ahead of \`origin/main\`
755
- - recurring automation should not force another PR when the requested checklist is already done
756
-
757
- Next step:
758
- - refresh the issue body with new unchecked improvement items before re-queueing this issue
759
- EOF
760
- if [[ -n "${checklist_matched_prs}" ]]; then
761
- printf '\nRecently matched PRs: #%s\n' "$(sed 's/,/, #/g' <<<"${checklist_matched_prs}")"
762
- fi
763
- return 0
764
- fi
765
-
766
- cat <<EOF
767
- # Blocker: Worker produced no publishable delta
768
-
769
- The worker finished its cycle, but the resulting branch had no commits ahead of \`origin/main\`.
770
-
771
- Why it was blocked:
772
- - the selected target likely overlapped work that is already on the current baseline, or
773
- - the worker ended with no net code/doc/test changes to publish
774
-
775
- Next step:
776
- - pick one remaining unchecked checklist item that is still missing on \`main\`
777
- - if the checklist is stale, refresh the issue body before re-queueing
778
- EOF
779
- return 0
780
- fi
781
-
782
- if [[ "${blocker_reason}" == "scope-guard-blocked" ]]; then
783
- cat <<EOF
784
- # Blocker: Change scope was too broad
785
-
786
- Host publication stopped this cycle because the branch touched too much surface area for a safe recurring issue PR.
787
-
788
- Why it was blocked:
789
- - recurring issues should ship one focused slice at a time
790
- - the publish scope guard detected a multi-surface change set
791
-
792
- \`\`\`text
793
- ${publish_out}
794
- \`\`\`
795
- EOF
796
- return 0
797
- fi
798
-
799
- if [[ "${blocker_reason}" == "verification-guard-blocked" ]]; then
800
- cat <<EOF
801
- # Blocker: Verification requirements were not satisfied
802
-
803
- Host publication stopped this cycle because the branch did not carry the required verification signal for a safe recurring issue PR.
804
-
805
- Why it was blocked:
806
- - the verification guard could not confirm the expected checks for this change
807
- - recurring issue publication should stop rather than open an unverifiable PR
808
-
809
- \`\`\`text
810
- ${publish_out}
811
- \`\`\`
812
- EOF
813
- return 0
814
- fi
815
-
816
- if [[ "${blocker_reason}" == "localization-guard-blocked" ]]; then
817
- cat <<EOF
818
- # Blocker: Localization requirements were not satisfied
819
-
820
- Host publication stopped this cycle because the branch updated locale resources but still left obvious hardcoded user-facing strings in the touched UI files.
821
-
822
- Why it was blocked:
823
- - the localization guard found remaining literals that should move behind translation keys
824
- - recurring issue publication should stop rather than open a partially localized UI change
825
-
826
- \`\`\`text
827
- ${publish_out}
828
- \`\`\`
829
- EOF
830
- return 0
831
- fi
832
-
833
- cat <<EOF
834
- Host-side publish blocked for session \`${session}\`.
835
-
836
- \`\`\`text
837
- ${publish_out}
838
- \`\`\`
839
- EOF
840
- }
841
-
842
- build_issue_runtime_blocker_comment() {
843
- local runtime_reason="${1:-worker-exit-failed}"
844
- local worker_name="${CODING_WORKER:-worker}"
845
-
846
- case "${runtime_reason}" in
847
- provider-quota-limit)
848
- if [[ "${worker_name}" == "codex" ]]; then
849
- cat <<EOF
850
- # Blocker: Provider quota is currently exhausted
851
-
852
- This recurring run stopped before implementation because the configured ${worker_name} account hit a provider-side usage limit.
853
-
854
- Why it was blocked:
855
- - the worker reached the current Codex usage cap for the active account
856
- - ACP records the quota hit, attempts safe account rotation when available, and then waits for the configured cooldown instead of looping indefinitely
857
-
858
- Next step:
859
- - wait for the current quota window to reset, or make another Codex account available to this profile
860
- EOF
861
- return 0
862
- fi
863
- cat <<EOF
864
- # Blocker: Provider quota is currently exhausted
865
-
866
- This recurring run stopped before implementation because the configured ${worker_name} account hit a provider-side rate limit.
867
-
868
- Why it was blocked:
869
- - the worker reached Anthropic's current request limit for this account
870
- - ACP recorded the quota hit and will retry after the configured cooldown instead of looping indefinitely
871
-
872
- Next step:
873
- - wait for the current quota window to reset, or switch this profile to another available provider/account
874
- EOF
875
- return 0
876
- ;;
877
- worker-environment-blocked)
878
- cat <<EOF
879
- # Blocker: Worker environment failed before a valid result contract was written
880
-
881
- This recurring run did not produce a usable ACP result file because the ${worker_name} execution environment failed mid-run.
882
-
883
- Why it was blocked:
884
- - the worker hit a sandbox/worktree runtime failure before it could write \`result.env\`
885
- - ACP detected the runtime signature from the session log and converted the missing contract into a concrete blocker instead of retrying with a generic \`invalid-result-contract\`
886
-
887
- Next step:
888
- - refresh the worker runtime/worktree and rerun this cycle after the host-side environment issue is resolved
889
- EOF
890
- return 0
891
- ;;
892
- esac
893
-
894
- cat <<EOF
895
- # Blocker: Worker session failed before publish
896
-
897
- The worker exited before ACP could publish or reconcile a result for this cycle.
898
-
899
- Failure reason:
900
- - \`${runtime_reason}\`
901
-
902
- Next step:
903
- - inspect the run logs for this session and re-queue once the underlying worker issue is resolved
904
- EOF
905
- }
906
-
907
- infer_issue_blocked_failure_reason() {
908
- local comment_file="${run_dir}/issue-comment.md"
909
- local current_reason="${1:-}"
910
-
911
- if [[ -n "${current_reason:-}" && "${current_reason}" != "issue-worker-blocked" ]]; then
912
- printf '%s\n' "${current_reason}"
913
- return 0
914
- fi
915
-
916
- [[ -s "${comment_file}" ]] || {
917
- printf 'issue-worker-blocked\n'
918
- return 0
919
- }
920
-
921
- ISSUE_COMMENT_FILE="${comment_file}" node <<'EOF'
922
- const fs = require('fs');
923
-
924
- const path = process.env.ISSUE_COMMENT_FILE || '';
925
- const body = path ? fs.readFileSync(path, 'utf8') : '';
926
- let reason = '';
927
-
928
- const explicitFailureReason = body.match(/Failure reason:\s*[\r\n]+-\s*`([^`]+)`/i);
929
- if (explicitFailureReason) {
930
- reason = explicitFailureReason[1];
931
- } else if (/^# Blocker: Verification requirements were not satisfied$/im.test(body)) {
932
- reason = 'verification-guard-blocked';
933
- } else if (/^# Blocker: Localization requirements were not satisfied$/im.test(body)) {
934
- reason = 'localization-guard-blocked';
935
- } else if (
936
- /required (?:issue-contract )?verification does not currently pass/i.test(body) ||
937
- /Because the required `pnpm typecheck` did not pass/i.test(body) ||
938
- /- BLOCKED `pnpm typecheck`/i.test(body) ||
939
- /pnpm typecheck(?:`)? fails in unrelated existing file/i.test(body) ||
940
- /Blocked on required root verification/i.test(body) ||
941
- /required root (?:verification command|`pnpm test`)/i.test(body) ||
942
- /pnpm test` is currently failing outside this/i.test(body) ||
943
- /The required root test command failed/i.test(body) ||
944
- /did not commit because the issue contract requires verification to pass/i.test(body)
945
- ) {
946
- reason = 'verification-guard-blocked';
947
- } else if (/^# Blocker: (All checklist items already completed|Worker produced no publishable delta)$/im.test(body)) {
948
- reason = 'no-publishable-commits';
949
- } else if (/^# Blocker: Change scope was too broad$/im.test(body)) {
950
- reason = 'scope-guard-blocked';
951
- } else if (/^# Blocker: Provider quota is currently exhausted$/im.test(body)) {
952
- reason = 'provider-quota-limit';
953
- } else if (
954
- /blocked on external network access/i.test(body) &&
955
- (/What I ran:/i.test(body) ||
956
- /`pnpm audit`/i.test(body) ||
957
- /`gh issue view`/i.test(body)) &&
958
- (/failed with `ENOTFOUND`/i.test(body) ||
959
- /Exact failure:/i.test(body) ||
960
- /registry\.npmjs\.org/i.test(body) ||
961
- /api\.github\.com/i.test(body))
962
- ) {
963
- reason = 'worker-preflight-network-blocked';
964
- } else if (
965
- /blocked on external network access/i.test(body) ||
966
- /could not perform a safe offline bump/i.test(body) ||
967
- /failed to reach `api\.github\.com`/i.test(body) ||
968
- /failed with `ENOTFOUND`/i.test(body)
969
- ) {
970
- reason = 'external-network-access-blocked';
971
- } else if (
972
- /I’m blocked on the environment, not the issue scope/i.test(body) ||
973
- /Every local execution path I need for this cycle is failing immediately with `aborted`/i.test(body) ||
974
- /outside this session['’]s writable sandbox/i.test(body) ||
975
- /could not write to the host-required `\$ACP_RUN_DIR`/i.test(body) ||
976
- /cannot access local infrastructure from this sandbox/i.test(body) ||
977
- /sandbox socket connection errors/i.test(body) ||
978
- /connect EPERM 127\.0\.0\.1:6379/i.test(body) ||
979
- /local Postgres\/Redis services/i.test(body) ||
980
- /worker can(?:not|'t) connect to the local test Postgres and Redis services/i.test(body)
981
- ) {
982
- reason = 'worker-environment-blocked';
983
- } else if (/^# Blocker:/im.test(body)) {
984
- reason = 'issue-worker-blocked';
985
- }
986
-
987
- process.stdout.write(`${reason || 'issue-worker-blocked'}\n`);
988
- EOF
989
- }
990
-
991
- extract_recovery_worktree_from_publish_output() {
992
- local publish_out="${1:-}"
993
- awk -F= '/^RECOVERY_WORKTREE=/{print $2}' <<<"$publish_out" | tail -n 1
994
- }
995
-
996
- mark_reconciled() {
997
- local reconciled_at tmp_file
998
- if [[ -d "$run_dir" ]]; then
999
- reconciled_at="$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
1000
- tmp_file="${run_dir}/reconciled.ok.tmp.$$"
1001
- {
1002
- printf 'STARTED_AT=%s\n' "${run_started_at}"
1003
- printf 'RECONCILED_AT=%s\n' "${reconciled_at}"
1004
- } >"${tmp_file}"
1005
- mv "${tmp_file}" "${run_dir}/reconciled.ok"
1006
- fi
1007
- }
1008
-
1009
- update_resident_issue_metadata() {
1010
- local metadata_file="${RESIDENT_WORKER_META_FILE:-}"
1011
- local finished_at=""
1012
- local task_count="${RESIDENT_TASK_COUNT:-1}"
1013
- local resident_worker_scope=""
1014
- local resident_worker_key=""
1015
- local resident_lane_kind=""
1016
- local resident_lane_value=""
1017
- local openclaw_agent_id=""
1018
- local openclaw_session_id=""
1019
- local openclaw_agent_dir=""
1020
- local openclaw_state_dir=""
1021
- local openclaw_config_path=""
1022
- local worktree_realpath=""
1023
- local last_worktree_reused=""
1024
-
1025
- [[ "${RESIDENT_WORKER_ENABLED:-}" == "yes" ]] || return 0
1026
- [[ -n "${metadata_file}" ]] || return 0
1027
-
1028
- finished_at="$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
1029
- resident_worker_scope="${RESIDENT_WORKER_SCOPE:-$(flow_resident_metadata_value "${metadata_file}" "RESIDENT_WORKER_SCOPE" 2>/dev/null || true)}"
1030
- resident_worker_key="${RESIDENT_WORKER_KEY:-$(flow_resident_metadata_value "${metadata_file}" "RESIDENT_WORKER_KEY" 2>/dev/null || true)}"
1031
- resident_lane_kind="${RESIDENT_LANE_KIND:-$(flow_resident_metadata_value "${metadata_file}" "RESIDENT_LANE_KIND" 2>/dev/null || true)}"
1032
- resident_lane_value="${RESIDENT_LANE_VALUE:-$(flow_resident_metadata_value "${metadata_file}" "RESIDENT_LANE_VALUE" 2>/dev/null || true)}"
1033
- if [[ -z "${resident_lane_kind}" ]]; then
1034
- resident_lane_kind="$(flow_resident_issue_lane_field_from_key "${resident_worker_key:-}" kind 2>/dev/null || true)"
1035
- fi
1036
- if [[ -z "${resident_lane_value}" ]]; then
1037
- resident_lane_value="$(flow_resident_issue_lane_field_from_key "${resident_worker_key:-}" value 2>/dev/null || true)"
1038
- fi
1039
- openclaw_agent_id="${RESIDENT_OPENCLAW_AGENT_ID:-${OPENCLAW_AGENT_ID:-$(flow_resident_metadata_value "${metadata_file}" "OPENCLAW_AGENT_ID" 2>/dev/null || true)}}"
1040
- openclaw_session_id="${RESIDENT_OPENCLAW_SESSION_ID:-${OPENCLAW_SESSION_ID:-$(flow_resident_metadata_value "${metadata_file}" "OPENCLAW_SESSION_ID" 2>/dev/null || true)}}"
1041
- openclaw_agent_dir="${RESIDENT_OPENCLAW_AGENT_DIR:-${OPENCLAW_AGENT_DIR:-$(flow_resident_metadata_value "${metadata_file}" "OPENCLAW_AGENT_DIR" 2>/dev/null || true)}}"
1042
- openclaw_state_dir="${RESIDENT_OPENCLAW_STATE_DIR:-${OPENCLAW_STATE_DIR:-$(flow_resident_metadata_value "${metadata_file}" "OPENCLAW_STATE_DIR" 2>/dev/null || true)}}"
1043
- openclaw_config_path="${RESIDENT_OPENCLAW_CONFIG_PATH:-${OPENCLAW_CONFIG_PATH:-$(flow_resident_metadata_value "${metadata_file}" "OPENCLAW_CONFIG_PATH" 2>/dev/null || true)}}"
1044
- worktree_realpath="${RESIDENT_WORKTREE_REALPATH:-$(flow_resident_metadata_value "${metadata_file}" "WORKTREE_REALPATH" 2>/dev/null || true)}"
1045
- last_worktree_reused="${RESIDENT_WORKTREE_REUSED:-$(flow_resident_metadata_value "${metadata_file}" "LAST_WORKTREE_REUSED" 2>/dev/null || true)}"
1046
-
1047
- flow_resident_write_metadata "${metadata_file}" \
1048
- "RESIDENT_WORKER_KIND=issue" \
1049
- "RESIDENT_WORKER_SCOPE=${resident_worker_scope:-lane}" \
1050
- "RESIDENT_WORKER_KEY=${resident_worker_key:-issue-${issue_id}}" \
1051
- "RESIDENT_LANE_KIND=${resident_lane_kind:-}" \
1052
- "RESIDENT_LANE_VALUE=${resident_lane_value:-}" \
1053
- "ISSUE_ID=${issue_id}" \
1054
- "ADAPTER_ID=${ADAPTER_ID:-}" \
1055
- "CODING_WORKER=${CODING_WORKER:-openclaw}" \
1056
- "WORKTREE=${WORKTREE:-}" \
1057
- "WORKTREE_REALPATH=${worktree_realpath:-${WORKTREE:-}}" \
1058
- "LAST_BRANCH=${BRANCH:-}" \
1059
- "OPENCLAW_AGENT_ID=${openclaw_agent_id:-}" \
1060
- "OPENCLAW_SESSION_ID=${openclaw_session_id:-}" \
1061
- "OPENCLAW_AGENT_DIR=${openclaw_agent_dir:-}" \
1062
- "OPENCLAW_STATE_DIR=${openclaw_state_dir:-}" \
1063
- "OPENCLAW_CONFIG_PATH=${openclaw_config_path:-}" \
1064
- "TASK_COUNT=${task_count}" \
1065
- "LAST_STARTED_AT=${STARTED_AT:-}" \
1066
- "LAST_FINISHED_AT=${finished_at}" \
1067
- "LAST_RUN_SESSION=${session}" \
1068
- "LAST_STATUS=${status}" \
1069
- "LAST_OUTCOME=${issue_summary_outcome:-}" \
1070
- "LAST_ACTION=${issue_summary_action:-}" \
1071
- "LAST_FAILURE_REASON=${issue_summary_failure_reason:-}" \
1072
- "LAST_WORKTREE_REUSED=${last_worktree_reused:-no}"
1073
- }
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
-
1109
- cleanup_issue_session() {
1110
- local -a cleanup_args=(
1111
- --repo-root "$repo_root"
1112
- --runs-root "$runs_root"
1113
- --history-root "$history_root"
1114
- --session "$session"
1115
- --worktree "${WORKTREE:-}"
1116
- --mode issue
1117
- )
1118
-
1119
- update_resident_issue_metadata
1120
- if [[ "${RESIDENT_WORKER_ENABLED:-}" == "yes" ]]; then
1121
- cleanup_args+=(--skip-worktree-cleanup)
1122
- fi
1123
-
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}"
1132
- }
1133
-
1134
- notify_issue_reconciled() {
1135
- issue_after_reconciled "$status" "${issue_summary_outcome:-}" "${issue_summary_action:-}" "${pr_number:-}" || true
1136
- }
1137
-
1138
- case "$status" in
1139
- SUCCEEDED)
1140
- clear_provider_quota_cooldown
1141
- if ! normalize_issue_result_contract; then
1142
- inferred_failure_reason="$(infer_issue_runtime_failure_from_log || true)"
1143
- if [[ -n "${inferred_failure_reason}" ]]; then
1144
- status="FAILED"
1145
- failure_reason="$(normalize_issue_failure_reason "${inferred_failure_reason}")"
1146
- issue_result_contract_note="missing-worker-result-recovered-${failure_reason}"
1147
- result_outcome="blocked"
1148
- result_action="host-comment-blocker"
1149
- normalize_issue_runner_state "failed" "${LAST_EXIT_CODE:-}" "${failure_reason}"
1150
- if [[ ! -s "${run_dir}/issue-comment.md" ]]; then
1151
- write_issue_comment_artifact "$(build_issue_runtime_blocker_comment "${failure_reason}")" || true
1152
- fi
1153
- post_issue_comment_if_present
1154
- require_transition "issue_schedule_retry" issue_schedule_retry "$failure_reason"
1155
- require_transition "issue_mark_ready" issue_mark_ready
1156
- issue_set_reconcile_summary "$status" "$result_outcome" "$result_action" "$failure_reason"
1157
- cleanup_issue_session
1158
- notify_issue_reconciled
1159
- mark_reconciled
1160
- printf 'STATUS=%s\n' "$status"
1161
- printf 'ISSUE_ID=%s\n' "$issue_id"
1162
- printf 'PR_NUMBER=%s\n' "$pr_number"
1163
- printf 'OUTCOME=%s\n' "$result_outcome"
1164
- printf 'ACTION=%s\n' "$result_action"
1165
- printf 'FAILURE_REASON=%s\n' "$failure_reason"
1166
- printf 'RESULT_CONTRACT_NOTE=%s\n' "$issue_result_contract_note"
1167
- exit 0
1168
- fi
1169
-
1170
- status="FAILED"
1171
- failure_reason="invalid-result-contract"
1172
- issue_result_contract_note="invalid-result-contract"
1173
- normalize_issue_runner_state "failed" "${LAST_EXIT_CODE:-}" "${failure_reason}"
1174
- post_issue_comment_if_present
1175
- require_transition "issue_schedule_retry" issue_schedule_retry "$failure_reason"
1176
- require_transition "issue_mark_ready" issue_mark_ready
1177
- result_outcome="invalid-contract"
1178
- result_action="queued-issue-retry"
1179
- issue_set_reconcile_summary "$status" "$result_outcome" "$result_action" "$failure_reason"
1180
- cleanup_issue_session
1181
- notify_issue_reconciled
1182
- mark_reconciled
1183
- printf 'STATUS=%s\n' "$status"
1184
- printf 'ISSUE_ID=%s\n' "$issue_id"
1185
- printf 'PR_NUMBER=%s\n' "$pr_number"
1186
- printf 'OUTCOME=%s\n' "$result_outcome"
1187
- printf 'ACTION=%s\n' "$result_action"
1188
- printf 'FAILURE_REASON=%s\n' "$failure_reason"
1189
- printf 'RESULT_CONTRACT_NOTE=%s\n' "$issue_result_contract_note"
1190
- exit 0
1191
- fi
1192
- require_transition "issue_before_success" issue_before_success
1193
- if [[ "$result_outcome" == "blocked" ]]; then
1194
- ensure_issue_blocked_comment_artifact
1195
- require_transition "issue_before_blocked" issue_before_blocked
1196
- post_issue_comment_if_present
1197
- if issue_should_close_as_superseded; then
1198
- require_transition "issue_clear_retry" issue_clear_retry
1199
- require_transition "issue_remove_running" issue_remove_running
1200
- require_transition "issue_close_as_superseded" issue_close_as_superseded
1201
- result_action="closed-superseded"
1202
- issue_set_reconcile_summary "$status" "$result_outcome" "$result_action" "${failure_reason:-}"
1203
- cleanup_issue_session
1204
- notify_issue_reconciled
1205
- mark_reconciled
1206
- printf 'STATUS=%s\n' "$status"
1207
- printf 'ISSUE_ID=%s\n' "$issue_id"
1208
- printf 'PR_NUMBER=%s\n' "$pr_number"
1209
- printf 'OUTCOME=%s\n' "$result_outcome"
1210
- printf 'ACTION=%s\n' "$result_action"
1211
- exit 0
1212
- fi
1213
- failure_reason="$(infer_issue_blocked_failure_reason "${failure_reason:-}")"
1214
- normalize_issue_runner_state "succeeded" "0" ""
1215
- require_transition "issue_schedule_retry" issue_schedule_retry "$failure_reason"
1216
- require_transition "issue_mark_blocked" issue_mark_blocked
1217
- issue_set_reconcile_summary "$status" "$result_outcome" "$result_action" "$failure_reason"
1218
- cleanup_issue_session
1219
- notify_issue_reconciled
1220
- mark_reconciled
1221
- printf 'STATUS=%s\n' "$status"
1222
- printf 'ISSUE_ID=%s\n' "$issue_id"
1223
- printf 'PR_NUMBER=%s\n' "$pr_number"
1224
- printf 'OUTCOME=%s\n' "$result_outcome"
1225
- printf 'ACTION=%s\n' "$result_action"
1226
- printf 'FAILURE_REASON=%s\n' "$failure_reason"
1227
- exit 0
1228
- fi
1229
-
1230
- if [[ "$result_outcome" == "reported" ]]; then
1231
- normalize_issue_runner_state "succeeded" "0" ""
1232
- post_issue_comment_if_present
1233
- require_transition "issue_clear_retry" issue_clear_retry
1234
- require_transition "issue_remove_running" issue_remove_running
1235
- cleanup_issue_session
1236
- notify_issue_reconciled
1237
- mark_reconciled
1238
- printf 'STATUS=%s\n' "$status"
1239
- printf 'ISSUE_ID=%s\n' "$issue_id"
1240
- printf 'PR_NUMBER=%s\n' "$pr_number"
1241
- printf 'OUTCOME=%s\n' "$result_outcome"
1242
- printf 'ACTION=%s\n' "$result_action"
1243
- exit 0
1244
- fi
1245
-
1246
- # Push branch to remote BEFORE publish to preserve commits
1247
- # even if worktree gets cleaned up during publish process
1248
- if [[ -n "${BRANCH:-}" && -n "${WORKTREE:-}" && -d "${WORKTREE:-}" ]]; then
1249
- if git -C "${WORKTREE}" rev-parse --git-dir >/dev/null 2>&1; then
1250
- if ! git -C "${WORKTREE}" push -u origin "${BRANCH}" 2>/dev/null; then
1251
- printf 'PRE_PUBLISH_PUSH=failed branch=%s\n' "${BRANCH}" >&2
1252
- else
1253
- printf 'PRE_PUBLISH_PUSH=ok branch=%s\n' "${BRANCH}" >&2
1254
- fi
1255
- fi
1256
- fi
1257
-
1258
- if ! issue_has_recorded_verification; then
1259
- attempt_issue_host_verification_recovery "missing-worker-verification" || true
1260
- fi
1261
-
1262
- publish_args=()
1263
- while IFS= read -r publish_arg; do
1264
- [[ -n "$publish_arg" ]] || continue
1265
- publish_args+=("$publish_arg")
1266
- done < <(issue_publish_extra_args || true)
1267
-
1268
- publish_cmd=(
1269
- "${shared_tools_dir}/agent-project-publish-issue-pr"
1270
- --repo-slug "$repo_slug"
1271
- --runs-root "$runs_root"
1272
- --history-root "$history_root"
1273
- --session "$session"
1274
- )
1275
- if [[ ${#publish_args[@]} -gt 0 ]]; then
1276
- publish_cmd+=("${publish_args[@]}")
1277
- fi
1278
-
1279
- if ! publish_out="$("${publish_cmd[@]}" 2>&1)"; then
1280
- publish_blocker_reason="$(classify_issue_publish_blocker "$publish_out" || true)"
1281
- if [[ "$publish_blocker_reason" == "verification-guard-blocked" ]]; then
1282
- recovered_worktree="$(extract_recovery_worktree_from_publish_output "$publish_out" || true)"
1283
- if [[ -n "$recovered_worktree" && -d "$recovered_worktree" ]]; then
1284
- WORKTREE="$recovered_worktree"
1285
- fi
1286
- if attempt_issue_host_verification_recovery "verification-guard-blocked"; then
1287
- if publish_out="$("${publish_cmd[@]}" 2>&1)"; then
1288
- pr_number="$(awk -F= '/^PR_NUMBER=/{print $2}' <<<"$publish_out")"
1289
- normalize_issue_runner_state "succeeded" "0" ""
1290
- require_transition "issue_clear_retry" issue_clear_retry
1291
- require_transition "issue_remove_running" issue_remove_running
1292
- if [[ -n "$pr_number" ]]; then
1293
- require_transition "issue_after_pr_created" issue_after_pr_created "$pr_number"
1294
- fi
1295
- cleanup_issue_session
1296
- notify_issue_reconciled
1297
- mark_reconciled
1298
- printf 'STATUS=%s\n' "$status"
1299
- printf 'ISSUE_ID=%s\n' "$issue_id"
1300
- printf 'PR_NUMBER=%s\n' "$pr_number"
1301
- printf 'OUTCOME=%s\n' "$result_outcome"
1302
- printf 'ACTION=%s\n' "$result_action"
1303
- if [[ -n "$issue_result_contract_note" ]]; then
1304
- printf 'RESULT_CONTRACT_NOTE=%s\n' "$issue_result_contract_note"
1305
- fi
1306
- exit 0
1307
- fi
1308
- publish_blocker_reason="$(classify_issue_publish_blocker "$publish_out" || true)"
1309
- fi
1310
- fi
1311
- blocker_body="$(build_issue_publish_blocker_comment "${publish_blocker_reason:-}" "${publish_out}")"
1312
- write_issue_comment_artifact "${blocker_body}" || true
1313
- post_issue_comment_if_present
1314
- if [[ -n "$publish_blocker_reason" ]]; then
1315
- require_transition "issue_before_blocked" issue_before_blocked
1316
- normalize_issue_runner_state "succeeded" "0" ""
1317
- require_transition "issue_schedule_retry" issue_schedule_retry "$publish_blocker_reason"
1318
- require_transition "issue_mark_blocked" issue_mark_blocked
1319
- result_outcome="blocked"
1320
- result_action="host-comment-blocker"
1321
- failure_reason="$publish_blocker_reason"
1322
- issue_set_reconcile_summary "$status" "$result_outcome" "$result_action" "$failure_reason"
1323
- cleanup_issue_session
1324
- notify_issue_reconciled
1325
- mark_reconciled
1326
- printf 'STATUS=%s\n' "$status"
1327
- printf 'ISSUE_ID=%s\n' "$issue_id"
1328
- printf 'PR_NUMBER=%s\n' "$pr_number"
1329
- printf 'OUTCOME=%s\n' "$result_outcome"
1330
- printf 'ACTION=%s\n' "$result_action"
1331
- printf 'FAILURE_REASON=%s\n' "$failure_reason"
1332
- printf 'PUBLISH_ERROR=%s\n' "$(printf '%s' "$publish_out" | tr '\n' ' ' | sed 's/ */ /g')"
1333
- exit 0
1334
- fi
1335
- require_transition "issue_schedule_retry" issue_schedule_retry "host-publish-failed"
1336
- require_transition "issue_mark_ready" issue_mark_ready
1337
- issue_set_reconcile_summary "$status" "" "" "host-publish-failed"
1338
- # Push branch to remote before cleanup to preserve commits for next retry
1339
- if [[ -n "${BRANCH:-}" && -d "${WORKTREE:-}" && ( -f "${WORKTREE:-}/.git" || -d "${WORKTREE:-}/.git" ) ]]; then
1340
- if ! git -C "$WORKTREE" push -u origin "$BRANCH" 2>/dev/null; then
1341
- printf 'WORKTREE_PUSH_BEFORE_CLEANUP=failed\n' >&2
1342
- else
1343
- printf 'WORKTREE_PUSH_BEFORE_CLEANUP=ok\n' >&2
1344
- fi
1345
- fi
1346
- cleanup_issue_session
1347
- notify_issue_reconciled
1348
- mark_reconciled
1349
- printf 'STATUS=%s\n' "$status"
1350
- printf 'ISSUE_ID=%s\n' "$issue_id"
1351
- printf 'PR_NUMBER=%s\n' "$pr_number"
1352
- printf 'PUBLISH_ERROR=%s\n' "$(printf '%s' "$publish_out" | tr '\n' ' ' | sed 's/ */ /g')"
1353
- exit 0
1354
- fi
1355
-
1356
- pr_number="$(awk -F= '/^PR_NUMBER=/{print $2}' <<<"$publish_out")"
1357
- normalize_issue_runner_state "succeeded" "0" ""
1358
- require_transition "issue_clear_retry" issue_clear_retry
1359
- require_transition "issue_remove_running" issue_remove_running
1360
- if [[ -n "$pr_number" ]]; then
1361
- require_transition "issue_after_pr_created" issue_after_pr_created "$pr_number"
1362
- fi
1363
- cleanup_issue_session
1364
- notify_issue_reconciled
1365
- ;;
1366
- FAILED)
1367
- failure_reason="$(normalize_issue_failure_reason "${failure_reason:-worker-exit-failed}")"
1368
- schedule_provider_quota_cooldown "${failure_reason}"
1369
- normalize_issue_runner_state "failed" "${LAST_EXIT_CODE:-}" "${failure_reason}"
1370
- if [[ "${result_outcome:-}" == "blocked" && "${result_action:-}" == "host-comment-blocker" ]]; then
1371
- if [[ ! -s "${run_dir}/issue-comment.md" ]]; then
1372
- write_issue_comment_artifact "$(build_issue_runtime_blocker_comment "${failure_reason}")" || true
1373
- fi
1374
- post_issue_comment_if_present
1375
- issue_set_reconcile_summary "$status" "$result_outcome" "$result_action" "$failure_reason"
1376
- elif [[ "${failure_reason}" == "provider-quota-limit" ]]; then
1377
- if [[ ! -s "${run_dir}/issue-comment.md" ]]; then
1378
- write_issue_comment_artifact "$(build_issue_runtime_blocker_comment "${failure_reason}")" || true
1379
- fi
1380
- post_issue_comment_if_present
1381
- issue_set_reconcile_summary "$status" "" "" "$failure_reason"
1382
- else
1383
- issue_set_reconcile_summary "$status" "" "" "$failure_reason"
1384
- fi
1385
- require_transition "issue_schedule_retry" issue_schedule_retry "${failure_reason}"
1386
- require_transition "issue_mark_ready" issue_mark_ready
1387
- cleanup_issue_session
1388
- notify_issue_reconciled
1389
- ;;
1390
- *)
1391
- ;;
1392
- esac
1393
-
1394
- mark_reconciled
1395
- printf 'STATUS=%s\n' "$status"
1396
- printf 'ISSUE_ID=%s\n' "$issue_id"
1397
- printf 'PR_NUMBER=%s\n' "$pr_number"
1398
- if [[ -n "${issue_summary_outcome:-}" ]]; then
1399
- printf 'OUTCOME=%s\n' "${issue_summary_outcome}"
1400
- fi
1401
- if [[ -n "${issue_summary_action:-}" ]]; then
1402
- printf 'ACTION=%s\n' "${issue_summary_action}"
1403
- fi
1404
- if [[ -n "$failure_reason" ]]; then
1405
- printf 'FAILURE_REASON=%s\n' "$failure_reason"
1406
- fi
1407
- if [[ -n "$issue_result_contract_note" ]]; then
1408
- printf 'RESULT_CONTRACT_NOTE=%s\n' "$issue_result_contract_note"
1409
- fi