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,1398 +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
-
235
- schedule_provider_quota_cooldown() {
236
- local reason="${1:-provider-quota-limit}"
237
- [[ "${reason}" == "provider-quota-limit" ]] || return 0
238
- [[ -x "${provider_cooldown_script}" ]] || return 0
239
- [[ "${CODING_WORKER:-}" == "codex" ]] && return 0
240
-
241
- "${provider_cooldown_script}" schedule "${reason}" >/dev/null || true
242
- }
243
-
244
- clear_provider_quota_cooldown() {
245
- [[ -x "${provider_cooldown_script}" ]] || return 0
246
- [[ "${CODING_WORKER:-}" == "codex" ]] && return 0
247
-
248
- "${provider_cooldown_script}" clear >/dev/null || true
249
- }
250
-
251
- normalize_issue_failure_reason() {
252
- local current_reason="${1:-}"
253
-
254
- case "${current_reason}" in
255
- usage-limit|quota-switch-deferred|quota-switch-attempt-limit)
256
- if [[ "${CODING_WORKER:-}" == "codex" ]]; then
257
- printf 'provider-quota-limit\n'
258
- return 0
259
- fi
260
- ;;
261
- esac
262
-
263
- printf '%s\n' "${current_reason}"
264
- }
265
-
266
- issue_runtime_log_file() {
267
- if [[ -f "${run_dir}/${session}.log" ]]; then
268
- printf '%s\n' "${run_dir}/${session}.log"
269
- return 0
270
- fi
271
-
272
- find "${run_dir}" -maxdepth 1 -type f -name '*.log' 2>/dev/null | LC_ALL=C sort | tail -n 1
273
- }
274
-
275
- infer_issue_runtime_failure_from_log() {
276
- local log_file=""
277
-
278
- log_file="$(issue_runtime_log_file)"
279
- [[ -n "${log_file}" && -f "${log_file}" ]] || return 1
280
-
281
- if grep -Eiq 'stale-run no-codex-output-before-stall-threshold|no-codex-output-before-stall-threshold' "${log_file}" 2>/dev/null; then
282
- printf 'no-codex-output-before-stall-threshold\n'
283
- return 0
284
- fi
285
-
286
- if grep -Eiq 'stale-run no-codex-progress-before-stall-threshold|no-codex-progress-before-stall-threshold' "${log_file}" 2>/dev/null; then
287
- printf 'no-codex-progress-before-stall-threshold\n'
288
- return 0
289
- fi
290
-
291
- if grep -Eiq 'stale-run no-agent-output-before-stall-threshold|no-agent-output-before-stall-threshold' "${log_file}" 2>/dev/null; then
292
- printf 'no-agent-output-before-stall-threshold\n'
293
- return 0
294
- fi
295
-
296
- if grep -Eiq 'stale-run no-agent-progress-before-stall-threshold|no-agent-progress-before-stall-threshold' "${log_file}" 2>/dev/null; then
297
- printf 'no-agent-progress-before-stall-threshold\n'
298
- return 0
299
- fi
300
-
301
- 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
302
- printf 'worker-environment-blocked\n'
303
- return 0
304
- fi
305
-
306
- return 1
307
- }
308
-
309
- normalize_issue_result_contract() {
310
- [[ "$status" == "SUCCEEDED" ]] || return 0
311
-
312
- case "${result_outcome:-}:${result_action:-}" in
313
- implemented:host-publish-issue-pr)
314
- return 0
315
- ;;
316
- blocked:host-comment-blocker)
317
- return 0
318
- ;;
319
- reported:host-comment-scheduled-report)
320
- return 0
321
- ;;
322
- reported:host-comment-scheduled-alert)
323
- return 0
324
- ;;
325
- *)
326
- echo "invalid issue worker result contract for session ${session}: OUTCOME='${result_outcome:-}' ACTION='${result_action:-}'" >&2
327
- return 1
328
- ;;
329
- esac
330
- }
331
-
332
- post_issue_comment_if_present() {
333
- local comment_file="${run_dir}/issue-comment.md"
334
- [[ -s "$comment_file" ]] || return 0
335
- if issue_latest_comment_matches_artifact; then
336
- return 0
337
- fi
338
- flow_github_api_repo "${repo_slug}" "issues/${issue_id}/comments" --method POST -f body="$(cat "$comment_file")" >/dev/null || true
339
- }
340
-
341
- issue_latest_comment_matches_artifact() {
342
- local comment_file="${run_dir}/issue-comment.md"
343
- local comment_body issue_json
344
- [[ -s "${comment_file}" ]] || return 1
345
- comment_body="$(cat "${comment_file}")"
346
- issue_json="$(flow_github_issue_view_json "${repo_slug}" "${issue_id}" 2>/dev/null || true)"
347
- [[ -n "${issue_json}" ]] || return 1
348
- jq -e --arg body "${comment_body}" '((.comments // [])[-1]?.body // "") == $body' >/dev/null <<<"${issue_json}"
349
- }
350
-
351
- write_issue_comment_artifact() {
352
- local comment_body="${1:-}"
353
- local comment_file="${run_dir}/issue-comment.md"
354
- [[ -n "${comment_body}" ]] || return 1
355
- printf '%s\n' "${comment_body}" >"${comment_file}"
356
- }
357
-
358
- issue_has_no_publishable_delta() {
359
- local worktree_path="${WORKTREE:-}"
360
- local default_branch="${ACP_DEFAULT_BRANCH:-${F_LOSNING_DEFAULT_BRANCH:-main}}"
361
- local baseline_ref=""
362
- local ahead_count=""
363
- local dirty_state=""
364
- local ref=""
365
- local candidate_refs=(
366
- "origin/${default_branch}"
367
- "${default_branch}"
368
- "origin/main"
369
- "main"
370
- "origin/master"
371
- "master"
372
- )
373
- local seen_refs=" "
374
-
375
- [[ -n "${worktree_path}" && -d "${worktree_path}" ]] || return 1
376
- git -C "${worktree_path}" rev-parse --git-dir >/dev/null 2>&1 || return 1
377
-
378
- for ref in "${candidate_refs[@]}"; do
379
- [[ -n "${ref}" ]] || continue
380
- if [[ "${seen_refs}" == *" ${ref} "* ]]; then
381
- continue
382
- fi
383
- seen_refs="${seen_refs}${ref} "
384
- if git -C "${worktree_path}" rev-parse --verify "${ref}" >/dev/null 2>&1; then
385
- baseline_ref="${ref}"
386
- break
387
- fi
388
- done
389
-
390
- [[ -n "${baseline_ref}" ]] || return 1
391
-
392
- ahead_count="$(git -C "${worktree_path}" rev-list --count "${baseline_ref}..HEAD" 2>/dev/null || true)"
393
- case "${ahead_count}" in
394
- 0) ;;
395
- *) return 1 ;;
396
- esac
397
-
398
- dirty_state="$(git -C "${worktree_path}" status --porcelain --untracked-files=no 2>/dev/null || true)"
399
- [[ -z "${dirty_state}" ]] || return 1
400
-
401
- return 0
402
- }
403
-
404
- ensure_issue_blocked_comment_artifact() {
405
- local comment_file="${run_dir}/issue-comment.md"
406
- local blocker_reason=""
407
- local verification_file=""
408
- local comment_body=""
409
-
410
- [[ -s "${comment_file}" ]] && return 0
411
-
412
- if issue_has_no_publishable_delta; then
413
- blocker_reason="no-publishable-commits"
414
- fi
415
-
416
- if [[ -z "${blocker_reason}" ]]; then
417
- verification_file="$(issue_verification_file)"
418
- if [[ ! -f "${verification_file}" ]] || ! grep -q '"status":"pass"' "${verification_file}" 2>/dev/null; then
419
- blocker_reason="verification-guard-blocked"
420
- fi
421
- fi
422
-
423
- comment_body="$(build_issue_publish_blocker_comment "${blocker_reason}" "")"
424
- write_issue_comment_artifact "${comment_body}" || true
425
- }
426
-
427
- issue_verification_file() {
428
- printf '%s\n' "${run_dir}/verification.jsonl"
429
- }
430
-
431
- normalize_issue_runner_state() {
432
- local normalized_state="${1:?normalized state required}"
433
- local normalized_exit_code="${2:-}"
434
- local normalized_failure_reason="${3:-}"
435
- local runner_state_file="${run_dir}/runner.env"
436
- local thread_id=""
437
- local attempt="1"
438
- local resume_count="0"
439
- local last_exit_code=""
440
- local last_failure_reason=""
441
- local last_trigger_reason=""
442
- local auth_wait_started_at=""
443
- local last_auth_fingerprint=""
444
-
445
- [[ -f "${runner_state_file}" ]] || return 0
446
-
447
- set +u
448
- set -a
449
- # shellcheck source=/dev/null
450
- source "${runner_state_file}"
451
- set +a
452
- set -u
453
-
454
- thread_id="${THREAD_ID:-}"
455
- attempt="${ATTEMPT:-1}"
456
- resume_count="${RESUME_COUNT:-0}"
457
- last_exit_code="${LAST_EXIT_CODE:-}"
458
- last_failure_reason="${LAST_FAILURE_REASON:-}"
459
- last_trigger_reason="${LAST_TRIGGER_REASON:-}"
460
- auth_wait_started_at="${AUTH_WAIT_STARTED_AT:-}"
461
- last_auth_fingerprint="${LAST_AUTH_FINGERPRINT:-}"
462
-
463
- if [[ -n "${normalized_exit_code}" ]]; then
464
- last_exit_code="${normalized_exit_code}"
465
- fi
466
- if [[ -n "${normalized_failure_reason}" || "${normalized_state}" == "succeeded" ]]; then
467
- last_failure_reason="${normalized_failure_reason}"
468
- fi
469
-
470
- flow_resident_write_metadata "${runner_state_file}" \
471
- "RUNNER_STATE=${normalized_state}" \
472
- "THREAD_ID=${thread_id}" \
473
- "ATTEMPT=${attempt}" \
474
- "RESUME_COUNT=${resume_count}" \
475
- "LAST_EXIT_CODE=${last_exit_code}" \
476
- "LAST_FAILURE_REASON=${last_failure_reason}" \
477
- "LAST_TRIGGER_REASON=${last_trigger_reason}" \
478
- "AUTH_WAIT_STARTED_AT=${auth_wait_started_at}" \
479
- "LAST_AUTH_FINGERPRINT=${last_auth_fingerprint}" \
480
- "UPDATED_AT=$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
481
- }
482
-
483
- issue_has_recorded_verification() {
484
- local verification_file
485
- verification_file="$(issue_verification_file)"
486
- [[ -f "$verification_file" ]] || return 1
487
- grep -q '"status":"pass"' "$verification_file" 2>/dev/null
488
- }
489
-
490
- extract_issue_host_recovery_commands() {
491
- local prompt_file="${run_dir}/prompt.md"
492
- local worktree_path="${WORKTREE:-}"
493
- local verification_file
494
- verification_file="$(issue_verification_file)"
495
-
496
- [[ -n "$worktree_path" && -d "$worktree_path" ]] || return 0
497
-
498
- PROMPT_FILE="$prompt_file" \
499
- WORKTREE_PATH="$worktree_path" \
500
- VERIFICATION_FILE="$verification_file" \
501
- node <<'EOF'
502
- const fs = require('fs');
503
- const path = require('path');
504
- const cp = require('child_process');
505
-
506
- const promptFile = process.env.PROMPT_FILE || '';
507
- const worktreePath = process.env.WORKTREE_PATH || '';
508
- const verificationFile = process.env.VERIFICATION_FILE || '';
509
-
510
- const commands = [];
511
- const seen = new Set();
512
-
513
- const addCommand = (value) => {
514
- const command = String(value || '').trim();
515
- if (!command) return;
516
- if (seen.has(command)) return;
517
- seen.add(command);
518
- commands.push(command);
519
- };
520
-
521
- let recordedPassCommands = new Set();
522
- if (verificationFile && fs.existsSync(verificationFile)) {
523
- const raw = fs.readFileSync(verificationFile, 'utf8');
524
- for (const line of raw.split('\n')) {
525
- const trimmed = line.trim();
526
- if (!trimmed) continue;
527
- try {
528
- const entry = JSON.parse(trimmed);
529
- if (entry && entry.status === 'pass' && typeof entry.command === 'string') {
530
- recordedPassCommands.add(entry.command.trim());
531
- }
532
- } catch (_error) {
533
- // Ignore malformed history entries during recovery.
534
- }
535
- }
536
- }
537
-
538
- let packageJson = null;
539
- const packageJsonPath = path.join(worktreePath, 'package.json');
540
- if (fs.existsSync(packageJsonPath)) {
541
- try {
542
- packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
543
- } catch (_error) {
544
- packageJson = null;
545
- }
546
- }
547
-
548
- const gitChangedFiles = [];
549
- try {
550
- const raw = cp.execFileSync(
551
- 'git',
552
- [
553
- '-C',
554
- worktreePath,
555
- 'show',
556
- '--pretty=',
557
- '--name-only',
558
- 'HEAD',
559
- ],
560
- { encoding: 'utf8' },
561
- );
562
- for (const line of raw.split('\n')) {
563
- const file = line.trim();
564
- if (file) gitChangedFiles.push(file);
565
- }
566
- } catch (_error) {
567
- // Ignore; recovery can still use prompt-derived commands.
568
- }
569
-
570
- const changedFilesLower = gitChangedFiles.map((file) => file.toLowerCase());
571
- const repoHasScript = (scriptName) => Boolean(packageJson?.scripts && Object.prototype.hasOwnProperty.call(packageJson.scripts, scriptName));
572
- const rootTestScript = String(packageJson?.scripts?.test || '').trim();
573
- const rootTestScriptUsesNodeTest = /^node\s+--test(?:\s|$)/.test(rootTestScript);
574
- const rootTestScriptLooksWatchMode = /\B--watch(?:All)?(?:[=\s]|$)|(?:^|\s)vitest\s+watch(?:\s|$)/.test(rootTestScript);
575
- const commandLooksRunnable = (command) => {
576
- if (/^npm test(?:\s|$)?/.test(command)) return repoHasScript('test');
577
- if (/^pnpm test(?:\s|$)?/.test(command)) return repoHasScript('test');
578
- if (/^npm run\s+([a-z0-9:_-]+)/i.test(command)) return repoHasScript(command.match(/^npm run\s+([a-z0-9:_-]+)/i)[1]);
579
- if (/^pnpm run\s+([a-z0-9:_-]+)/i.test(command)) return repoHasScript(command.match(/^pnpm run\s+([a-z0-9:_-]+)/i)[1]);
580
- if (/^node\s+--test(?:\s|$)/.test(command)) return true;
581
- return true;
582
- };
583
- const rootTestFallbackCommand = () => {
584
- if (!rootTestScript) return '';
585
- if (rootTestScriptUsesNodeTest) return 'npm test';
586
- if (!rootTestScriptLooksWatchMode) return 'npm test';
587
- if (/\bjest\b/i.test(rootTestScript)) return 'npx jest --runInBand --watchAll=false';
588
- if (/\bvitest\b/i.test(rootTestScript)) return 'npx vitest run';
589
- return '';
590
- };
591
-
592
- if (promptFile && fs.existsSync(promptFile)) {
593
- const lines = fs.readFileSync(promptFile, 'utf8').split(/\r?\n/).slice(0, 40);
594
- for (const line of lines) {
595
- if (!/^\s*-\s+/.test(line)) continue;
596
- if (!/(?:\bRun\b|\balso run\b|\bafter code changes\b|\bevery completed cycle\b)/i.test(line)) continue;
597
-
598
- const trimmed = line.trim();
599
- if (/^- If /i.test(trimmed)) {
600
- const lowered = trimmed.toLowerCase();
601
- if (lowered.includes('cli') || lowered.includes('fixture')) {
602
- const cliOrFixtureTouched = changedFilesLower.some((file) => /(?:^|\/)(src\/summary-cli|fixtures?\/|fixtures?\.|cli\b)/.test(file));
603
- if (!cliOrFixtureTouched) {
604
- continue;
605
- }
606
- } else {
607
- continue;
608
- }
609
- }
610
-
611
- for (const match of line.matchAll(/`([^`]+)`/g)) {
612
- const command = String(match[1] || '').trim();
613
- if (commandLooksRunnable(command)) {
614
- addCommand(command);
615
- }
616
- }
617
- }
618
- }
619
-
620
- const changedTestFiles = [...new Set(gitChangedFiles.filter((file) => /\.(?:spec|test)\.[cm]?[jt]sx?$/.test(file)))];
621
- if (rootTestScriptUsesNodeTest) {
622
- for (const file of changedTestFiles) {
623
- addCommand(`node --test ${file}`);
624
- }
625
- }
626
-
627
- if (commands.length === 0) {
628
- const fallbackCommand = rootTestFallbackCommand();
629
- if (fallbackCommand) {
630
- addCommand(fallbackCommand);
631
- }
632
- }
633
-
634
- const filtered = commands.filter((command) => !recordedPassCommands.has(command));
635
- process.stdout.write(filtered.join('\n'));
636
- EOF
637
- }
638
-
639
- run_issue_host_verification_command() {
640
- local command_text="${1:?command text required}"
641
- local session_log_file="${run_dir}/${session}.log"
642
-
643
- {
644
- printf '\n[host-issue-recovery] command=%s\n' "$command_text"
645
- } >>"$session_log_file"
646
-
647
- 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
648
- if [[ -x "$record_verification_script" ]]; then
649
- bash "$record_verification_script" --run-dir "$run_dir" --status pass --command "$command_text" --note "host-recovery-after-missing-worker-verification" >/dev/null
650
- fi
651
- return 0
652
- fi
653
-
654
- if [[ -x "$record_verification_script" ]]; then
655
- bash "$record_verification_script" --run-dir "$run_dir" --status fail --command "$command_text" --note "host-recovery-after-missing-worker-verification" >/dev/null
656
- fi
657
- printf '[host-issue-recovery] command failed=%s\n' "$command_text" >>"$session_log_file"
658
- return 1
659
- }
660
-
661
- attempt_issue_host_verification_recovery() {
662
- local recovery_reason="${1:-missing-worker-verification}"
663
- local recovery_commands=""
664
- local command_text=""
665
-
666
- [[ "${result_outcome:-}" == "implemented" ]] || return 1
667
- [[ -n "${WORKTREE:-}" && -d "${WORKTREE:-}" ]] || return 1
668
-
669
- recovery_commands="$(extract_issue_host_recovery_commands)"
670
- [[ -n "$recovery_commands" ]] || return 1
671
-
672
- while IFS= read -r command_text; do
673
- [[ -n "$command_text" ]] || continue
674
- if ! run_issue_host_verification_command "$command_text"; then
675
- return 1
676
- fi
677
- done <<<"$recovery_commands"
678
-
679
- issue_result_contract_note="host-recovered-${recovery_reason}"
680
- return 0
681
- }
682
-
683
- classify_issue_publish_blocker() {
684
- local publish_out="${1:-}"
685
-
686
- if grep -Fq 'Scope guard blocked issue' <<<"$publish_out"; then
687
- printf 'scope-guard-blocked\n'
688
- return 0
689
- fi
690
-
691
- if grep -Fq 'Verification guard blocked branch publication.' <<<"$publish_out"; then
692
- printf 'verification-guard-blocked\n'
693
- return 0
694
- fi
695
-
696
- if grep -Fq 'Localization guard blocked branch publication.' <<<"$publish_out"; then
697
- printf 'localization-guard-blocked\n'
698
- return 0
699
- fi
700
-
701
- if grep -Fq 'has no commits ahead of' <<<"$publish_out"; then
702
- printf 'no-publishable-commits\n'
703
- return 0
704
- fi
705
-
706
- return 1
707
- }
708
-
709
- refresh_recurring_issue_checklist() {
710
- local sync_script="${shared_tools_dir}/sync-recurring-issue-checklist.sh"
711
- [[ -x "${sync_script}" ]] || return 1
712
- bash "${sync_script}" --repo-slug "${repo_slug}" --issue-id "${issue_id}" 2>/dev/null || return 1
713
- }
714
-
715
- build_issue_publish_blocker_comment() {
716
- local blocker_reason="${1:-}"
717
- local publish_out="${2:-}"
718
- local sync_out=""
719
- local checklist_total="0"
720
- local checklist_unchecked="0"
721
- local checklist_matched_prs=""
722
-
723
- if [[ "${blocker_reason}" == "no-publishable-commits" ]]; then
724
- sync_out="$(refresh_recurring_issue_checklist || true)"
725
- checklist_total="$(awk -F= '/^CHECKLIST_TOTAL=/{print $2; exit}' <<<"${sync_out:-}")"
726
- checklist_unchecked="$(awk -F= '/^CHECKLIST_UNCHECKED=/{print $2; exit}' <<<"${sync_out:-}")"
727
- checklist_matched_prs="$(awk -F= '/^CHECKLIST_MATCHED_PR_NUMBERS=/{print $2; exit}' <<<"${sync_out:-}")"
728
-
729
- case "${checklist_total}" in
730
- ''|*[!0-9]*) checklist_total="0" ;;
731
- esac
732
- case "${checklist_unchecked}" in
733
- ''|*[!0-9]*) checklist_unchecked="0" ;;
734
- esac
735
-
736
- if [[ "${checklist_total}" -gt 0 && "${checklist_unchecked}" -eq 0 ]]; then
737
- cat <<EOF
738
- # Blocker: All checklist items already completed
739
-
740
- All checklist items for issue #${issue_id} appear to be satisfied on the current baseline.
741
-
742
- Why it was blocked:
743
- - the worker completed a cycle, but the resulting branch had no commits ahead of \`origin/main\`
744
- - recurring automation should not force another PR when the requested checklist is already done
745
-
746
- Next step:
747
- - refresh the issue body with new unchecked improvement items before re-queueing this issue
748
- EOF
749
- if [[ -n "${checklist_matched_prs}" ]]; then
750
- printf '\nRecently matched PRs: #%s\n' "$(sed 's/,/, #/g' <<<"${checklist_matched_prs}")"
751
- fi
752
- return 0
753
- fi
754
-
755
- cat <<EOF
756
- # Blocker: Worker produced no publishable delta
757
-
758
- The worker finished its cycle, but the resulting branch had no commits ahead of \`origin/main\`.
759
-
760
- Why it was blocked:
761
- - the selected target likely overlapped work that is already on the current baseline, or
762
- - the worker ended with no net code/doc/test changes to publish
763
-
764
- Next step:
765
- - pick one remaining unchecked checklist item that is still missing on \`main\`
766
- - if the checklist is stale, refresh the issue body before re-queueing
767
- EOF
768
- return 0
769
- fi
770
-
771
- if [[ "${blocker_reason}" == "scope-guard-blocked" ]]; then
772
- cat <<EOF
773
- # Blocker: Change scope was too broad
774
-
775
- Host publication stopped this cycle because the branch touched too much surface area for a safe recurring issue PR.
776
-
777
- Why it was blocked:
778
- - recurring issues should ship one focused slice at a time
779
- - the publish scope guard detected a multi-surface change set
780
-
781
- \`\`\`text
782
- ${publish_out}
783
- \`\`\`
784
- EOF
785
- return 0
786
- fi
787
-
788
- if [[ "${blocker_reason}" == "verification-guard-blocked" ]]; then
789
- cat <<EOF
790
- # Blocker: Verification requirements were not satisfied
791
-
792
- Host publication stopped this cycle because the branch did not carry the required verification signal for a safe recurring issue PR.
793
-
794
- Why it was blocked:
795
- - the verification guard could not confirm the expected checks for this change
796
- - recurring issue publication should stop rather than open an unverifiable PR
797
-
798
- \`\`\`text
799
- ${publish_out}
800
- \`\`\`
801
- EOF
802
- return 0
803
- fi
804
-
805
- if [[ "${blocker_reason}" == "localization-guard-blocked" ]]; then
806
- cat <<EOF
807
- # Blocker: Localization requirements were not satisfied
808
-
809
- Host publication stopped this cycle because the branch updated locale resources but still left obvious hardcoded user-facing strings in the touched UI files.
810
-
811
- Why it was blocked:
812
- - the localization guard found remaining literals that should move behind translation keys
813
- - recurring issue publication should stop rather than open a partially localized UI change
814
-
815
- \`\`\`text
816
- ${publish_out}
817
- \`\`\`
818
- EOF
819
- return 0
820
- fi
821
-
822
- cat <<EOF
823
- Host-side publish blocked for session \`${session}\`.
824
-
825
- \`\`\`text
826
- ${publish_out}
827
- \`\`\`
828
- EOF
829
- }
830
-
831
- build_issue_runtime_blocker_comment() {
832
- local runtime_reason="${1:-worker-exit-failed}"
833
- local worker_name="${CODING_WORKER:-worker}"
834
-
835
- case "${runtime_reason}" in
836
- provider-quota-limit)
837
- if [[ "${worker_name}" == "codex" ]]; then
838
- cat <<EOF
839
- # Blocker: Provider quota is currently exhausted
840
-
841
- This recurring run stopped before implementation because the configured ${worker_name} account hit a provider-side usage limit.
842
-
843
- Why it was blocked:
844
- - the worker reached the current Codex usage cap for the active account
845
- - ACP records the quota hit, attempts safe account rotation when available, and then waits for the configured cooldown instead of looping indefinitely
846
-
847
- Next step:
848
- - wait for the current quota window to reset, or make another Codex account available to this profile
849
- EOF
850
- return 0
851
- fi
852
- cat <<EOF
853
- # Blocker: Provider quota is currently exhausted
854
-
855
- This recurring run stopped before implementation because the configured ${worker_name} account hit a provider-side rate limit.
856
-
857
- Why it was blocked:
858
- - the worker reached Anthropic's current request limit for this account
859
- - ACP recorded the quota hit and will retry after the configured cooldown instead of looping indefinitely
860
-
861
- Next step:
862
- - wait for the current quota window to reset, or switch this profile to another available provider/account
863
- EOF
864
- return 0
865
- ;;
866
- worker-environment-blocked)
867
- cat <<EOF
868
- # Blocker: Worker environment failed before a valid result contract was written
869
-
870
- This recurring run did not produce a usable ACP result file because the ${worker_name} execution environment failed mid-run.
871
-
872
- Why it was blocked:
873
- - the worker hit a sandbox/worktree runtime failure before it could write \`result.env\`
874
- - 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\`
875
-
876
- Next step:
877
- - refresh the worker runtime/worktree and rerun this cycle after the host-side environment issue is resolved
878
- EOF
879
- return 0
880
- ;;
881
- esac
882
-
883
- cat <<EOF
884
- # Blocker: Worker session failed before publish
885
-
886
- The worker exited before ACP could publish or reconcile a result for this cycle.
887
-
888
- Failure reason:
889
- - \`${runtime_reason}\`
890
-
891
- Next step:
892
- - inspect the run logs for this session and re-queue once the underlying worker issue is resolved
893
- EOF
894
- }
895
-
896
- infer_issue_blocked_failure_reason() {
897
- local comment_file="${run_dir}/issue-comment.md"
898
- local current_reason="${1:-}"
899
-
900
- if [[ -n "${current_reason:-}" && "${current_reason}" != "issue-worker-blocked" ]]; then
901
- printf '%s\n' "${current_reason}"
902
- return 0
903
- fi
904
-
905
- [[ -s "${comment_file}" ]] || {
906
- printf 'issue-worker-blocked\n'
907
- return 0
908
- }
909
-
910
- ISSUE_COMMENT_FILE="${comment_file}" node <<'EOF'
911
- const fs = require('fs');
912
-
913
- const path = process.env.ISSUE_COMMENT_FILE || '';
914
- const body = path ? fs.readFileSync(path, 'utf8') : '';
915
- let reason = '';
916
-
917
- const explicitFailureReason = body.match(/Failure reason:\s*[\r\n]+-\s*`([^`]+)`/i);
918
- if (explicitFailureReason) {
919
- reason = explicitFailureReason[1];
920
- } else if (/^# Blocker: Verification requirements were not satisfied$/im.test(body)) {
921
- reason = 'verification-guard-blocked';
922
- } else if (/^# Blocker: Localization requirements were not satisfied$/im.test(body)) {
923
- reason = 'localization-guard-blocked';
924
- } else if (
925
- /required (?:issue-contract )?verification does not currently pass/i.test(body) ||
926
- /Because the required `pnpm typecheck` did not pass/i.test(body) ||
927
- /- BLOCKED `pnpm typecheck`/i.test(body) ||
928
- /pnpm typecheck(?:`)? fails in unrelated existing file/i.test(body) ||
929
- /Blocked on required root verification/i.test(body) ||
930
- /required root (?:verification command|`pnpm test`)/i.test(body) ||
931
- /pnpm test` is currently failing outside this/i.test(body) ||
932
- /The required root test command failed/i.test(body) ||
933
- /did not commit because the issue contract requires verification to pass/i.test(body)
934
- ) {
935
- reason = 'verification-guard-blocked';
936
- } else if (/^# Blocker: (All checklist items already completed|Worker produced no publishable delta)$/im.test(body)) {
937
- reason = 'no-publishable-commits';
938
- } else if (/^# Blocker: Change scope was too broad$/im.test(body)) {
939
- reason = 'scope-guard-blocked';
940
- } else if (/^# Blocker: Provider quota is currently exhausted$/im.test(body)) {
941
- reason = 'provider-quota-limit';
942
- } else if (
943
- /blocked on external network access/i.test(body) &&
944
- (/What I ran:/i.test(body) ||
945
- /`pnpm audit`/i.test(body) ||
946
- /`gh issue view`/i.test(body)) &&
947
- (/failed with `ENOTFOUND`/i.test(body) ||
948
- /Exact failure:/i.test(body) ||
949
- /registry\.npmjs\.org/i.test(body) ||
950
- /api\.github\.com/i.test(body))
951
- ) {
952
- reason = 'worker-preflight-network-blocked';
953
- } else if (
954
- /blocked on external network access/i.test(body) ||
955
- /could not perform a safe offline bump/i.test(body) ||
956
- /failed to reach `api\.github\.com`/i.test(body) ||
957
- /failed with `ENOTFOUND`/i.test(body)
958
- ) {
959
- reason = 'external-network-access-blocked';
960
- } else if (
961
- /I’m blocked on the environment, not the issue scope/i.test(body) ||
962
- /Every local execution path I need for this cycle is failing immediately with `aborted`/i.test(body) ||
963
- /outside this session['’]s writable sandbox/i.test(body) ||
964
- /could not write to the host-required `\$ACP_RUN_DIR`/i.test(body) ||
965
- /cannot access local infrastructure from this sandbox/i.test(body) ||
966
- /sandbox socket connection errors/i.test(body) ||
967
- /connect EPERM 127\.0\.0\.1:6379/i.test(body) ||
968
- /local Postgres\/Redis services/i.test(body) ||
969
- /worker can(?:not|'t) connect to the local test Postgres and Redis services/i.test(body)
970
- ) {
971
- reason = 'worker-environment-blocked';
972
- } else if (/^# Blocker:/im.test(body)) {
973
- reason = 'issue-worker-blocked';
974
- }
975
-
976
- process.stdout.write(`${reason || 'issue-worker-blocked'}\n`);
977
- EOF
978
- }
979
-
980
- extract_recovery_worktree_from_publish_output() {
981
- local publish_out="${1:-}"
982
- awk -F= '/^RECOVERY_WORKTREE=/{print $2}' <<<"$publish_out" | tail -n 1
983
- }
984
-
985
- mark_reconciled() {
986
- local reconciled_at tmp_file
987
- if [[ -d "$run_dir" ]]; then
988
- reconciled_at="$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
989
- tmp_file="${run_dir}/reconciled.ok.tmp.$$"
990
- {
991
- printf 'STARTED_AT=%s\n' "${run_started_at}"
992
- printf 'RECONCILED_AT=%s\n' "${reconciled_at}"
993
- } >"${tmp_file}"
994
- mv "${tmp_file}" "${run_dir}/reconciled.ok"
995
- fi
996
- }
997
-
998
- update_resident_issue_metadata() {
999
- local metadata_file="${RESIDENT_WORKER_META_FILE:-}"
1000
- local finished_at=""
1001
- local task_count="${RESIDENT_TASK_COUNT:-1}"
1002
- local resident_worker_scope=""
1003
- local resident_worker_key=""
1004
- local resident_lane_kind=""
1005
- local resident_lane_value=""
1006
- local openclaw_agent_id=""
1007
- local openclaw_session_id=""
1008
- local openclaw_agent_dir=""
1009
- local openclaw_state_dir=""
1010
- local openclaw_config_path=""
1011
- local worktree_realpath=""
1012
- local last_worktree_reused=""
1013
-
1014
- [[ "${RESIDENT_WORKER_ENABLED:-}" == "yes" ]] || return 0
1015
- [[ -n "${metadata_file}" ]] || return 0
1016
-
1017
- finished_at="$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
1018
- resident_worker_scope="${RESIDENT_WORKER_SCOPE:-$(flow_resident_metadata_value "${metadata_file}" "RESIDENT_WORKER_SCOPE" 2>/dev/null || true)}"
1019
- resident_worker_key="${RESIDENT_WORKER_KEY:-$(flow_resident_metadata_value "${metadata_file}" "RESIDENT_WORKER_KEY" 2>/dev/null || true)}"
1020
- resident_lane_kind="${RESIDENT_LANE_KIND:-$(flow_resident_metadata_value "${metadata_file}" "RESIDENT_LANE_KIND" 2>/dev/null || true)}"
1021
- resident_lane_value="${RESIDENT_LANE_VALUE:-$(flow_resident_metadata_value "${metadata_file}" "RESIDENT_LANE_VALUE" 2>/dev/null || true)}"
1022
- if [[ -z "${resident_lane_kind}" ]]; then
1023
- resident_lane_kind="$(flow_resident_issue_lane_field_from_key "${resident_worker_key:-}" kind 2>/dev/null || true)"
1024
- fi
1025
- if [[ -z "${resident_lane_value}" ]]; then
1026
- resident_lane_value="$(flow_resident_issue_lane_field_from_key "${resident_worker_key:-}" value 2>/dev/null || true)"
1027
- fi
1028
- openclaw_agent_id="${RESIDENT_OPENCLAW_AGENT_ID:-${OPENCLAW_AGENT_ID:-$(flow_resident_metadata_value "${metadata_file}" "OPENCLAW_AGENT_ID" 2>/dev/null || true)}}"
1029
- openclaw_session_id="${RESIDENT_OPENCLAW_SESSION_ID:-${OPENCLAW_SESSION_ID:-$(flow_resident_metadata_value "${metadata_file}" "OPENCLAW_SESSION_ID" 2>/dev/null || true)}}"
1030
- openclaw_agent_dir="${RESIDENT_OPENCLAW_AGENT_DIR:-${OPENCLAW_AGENT_DIR:-$(flow_resident_metadata_value "${metadata_file}" "OPENCLAW_AGENT_DIR" 2>/dev/null || true)}}"
1031
- openclaw_state_dir="${RESIDENT_OPENCLAW_STATE_DIR:-${OPENCLAW_STATE_DIR:-$(flow_resident_metadata_value "${metadata_file}" "OPENCLAW_STATE_DIR" 2>/dev/null || true)}}"
1032
- openclaw_config_path="${RESIDENT_OPENCLAW_CONFIG_PATH:-${OPENCLAW_CONFIG_PATH:-$(flow_resident_metadata_value "${metadata_file}" "OPENCLAW_CONFIG_PATH" 2>/dev/null || true)}}"
1033
- worktree_realpath="${RESIDENT_WORKTREE_REALPATH:-$(flow_resident_metadata_value "${metadata_file}" "WORKTREE_REALPATH" 2>/dev/null || true)}"
1034
- last_worktree_reused="${RESIDENT_WORKTREE_REUSED:-$(flow_resident_metadata_value "${metadata_file}" "LAST_WORKTREE_REUSED" 2>/dev/null || true)}"
1035
-
1036
- flow_resident_write_metadata "${metadata_file}" \
1037
- "RESIDENT_WORKER_KIND=issue" \
1038
- "RESIDENT_WORKER_SCOPE=${resident_worker_scope:-lane}" \
1039
- "RESIDENT_WORKER_KEY=${resident_worker_key:-issue-${issue_id}}" \
1040
- "RESIDENT_LANE_KIND=${resident_lane_kind:-}" \
1041
- "RESIDENT_LANE_VALUE=${resident_lane_value:-}" \
1042
- "ISSUE_ID=${issue_id}" \
1043
- "ADAPTER_ID=${ADAPTER_ID:-}" \
1044
- "CODING_WORKER=${CODING_WORKER:-openclaw}" \
1045
- "WORKTREE=${WORKTREE:-}" \
1046
- "WORKTREE_REALPATH=${worktree_realpath:-${WORKTREE:-}}" \
1047
- "LAST_BRANCH=${BRANCH:-}" \
1048
- "OPENCLAW_AGENT_ID=${openclaw_agent_id:-}" \
1049
- "OPENCLAW_SESSION_ID=${openclaw_session_id:-}" \
1050
- "OPENCLAW_AGENT_DIR=${openclaw_agent_dir:-}" \
1051
- "OPENCLAW_STATE_DIR=${openclaw_state_dir:-}" \
1052
- "OPENCLAW_CONFIG_PATH=${openclaw_config_path:-}" \
1053
- "TASK_COUNT=${task_count}" \
1054
- "LAST_STARTED_AT=${STARTED_AT:-}" \
1055
- "LAST_FINISHED_AT=${finished_at}" \
1056
- "LAST_RUN_SESSION=${session}" \
1057
- "LAST_STATUS=${status}" \
1058
- "LAST_OUTCOME=${issue_summary_outcome:-}" \
1059
- "LAST_ACTION=${issue_summary_action:-}" \
1060
- "LAST_FAILURE_REASON=${issue_summary_failure_reason:-}" \
1061
- "LAST_WORKTREE_REUSED=${last_worktree_reused:-no}"
1062
- }
1063
-
1064
- cleanup_output_value() {
1065
- local cleanup_output="${1:-}"
1066
- local key="${2:?key required}"
1067
- awk -F= -v target_key="${key}" '$1 == target_key { print substr($0, index($0, "=") + 1); exit }' <<<"${cleanup_output}"
1068
- }
1069
-
1070
- warn_cleanup_issue_session() {
1071
- local cleanup_output="${1:-}"
1072
- local cleanup_exit="${2:-0}"
1073
- local cleanup_status=""
1074
- local cleanup_mode=""
1075
- local cleanup_error=""
1076
-
1077
- cleanup_status="$(cleanup_output_value "${cleanup_output}" "CLEANUP_STATUS")"
1078
- if [[ -z "${cleanup_status}" ]]; then
1079
- cleanup_status="${cleanup_exit}"
1080
- fi
1081
- [[ "${cleanup_status}" != "0" ]] || return 0
1082
-
1083
- cleanup_mode="$(cleanup_output_value "${cleanup_output}" "CLEANUP_MODE")"
1084
- cleanup_error="$(cleanup_output_value "${cleanup_output}" "CLEANUP_ERROR")"
1085
- printf '[%s] issue cleanup warning session=%s status=%s mode=%s\n' \
1086
- "$(date -u +"%Y-%m-%dT%H:%M:%SZ")" \
1087
- "${session}" \
1088
- "${cleanup_status}" \
1089
- "${cleanup_mode:-unknown}" >&2
1090
- if [[ -n "${cleanup_error}" ]]; then
1091
- printf '[%s] issue cleanup detail session=%s %s\n' \
1092
- "$(date -u +"%Y-%m-%dT%H:%M:%SZ")" \
1093
- "${session}" \
1094
- "${cleanup_error}" >&2
1095
- fi
1096
- }
1097
-
1098
- cleanup_issue_session() {
1099
- local -a cleanup_args=(
1100
- --repo-root "$repo_root"
1101
- --runs-root "$runs_root"
1102
- --history-root "$history_root"
1103
- --session "$session"
1104
- --worktree "${WORKTREE:-}"
1105
- --mode issue
1106
- )
1107
-
1108
- update_resident_issue_metadata
1109
- if [[ "${RESIDENT_WORKER_ENABLED:-}" == "yes" ]]; then
1110
- cleanup_args+=(--skip-worktree-cleanup)
1111
- fi
1112
-
1113
- local cleanup_output=""
1114
- local cleanup_exit="0"
1115
- if cleanup_output="$("${shared_tools_dir}/agent-project-cleanup-session" "${cleanup_args[@]}" 2>&1)"; then
1116
- cleanup_exit="0"
1117
- else
1118
- cleanup_exit="$?"
1119
- fi
1120
- warn_cleanup_issue_session "${cleanup_output}" "${cleanup_exit}"
1121
- }
1122
-
1123
- notify_issue_reconciled() {
1124
- issue_after_reconciled "$status" "${issue_summary_outcome:-}" "${issue_summary_action:-}" "${pr_number:-}" || true
1125
- }
1126
-
1127
- case "$status" in
1128
- SUCCEEDED)
1129
- clear_provider_quota_cooldown
1130
- if ! normalize_issue_result_contract; then
1131
- inferred_failure_reason="$(infer_issue_runtime_failure_from_log || true)"
1132
- if [[ -n "${inferred_failure_reason}" ]]; then
1133
- status="FAILED"
1134
- failure_reason="$(normalize_issue_failure_reason "${inferred_failure_reason}")"
1135
- issue_result_contract_note="missing-worker-result-recovered-${failure_reason}"
1136
- result_outcome="blocked"
1137
- result_action="host-comment-blocker"
1138
- normalize_issue_runner_state "failed" "${LAST_EXIT_CODE:-}" "${failure_reason}"
1139
- if [[ ! -s "${run_dir}/issue-comment.md" ]]; then
1140
- write_issue_comment_artifact "$(build_issue_runtime_blocker_comment "${failure_reason}")" || true
1141
- fi
1142
- post_issue_comment_if_present
1143
- require_transition "issue_schedule_retry" issue_schedule_retry "$failure_reason"
1144
- require_transition "issue_mark_ready" issue_mark_ready
1145
- issue_set_reconcile_summary "$status" "$result_outcome" "$result_action" "$failure_reason"
1146
- cleanup_issue_session
1147
- notify_issue_reconciled
1148
- mark_reconciled
1149
- printf 'STATUS=%s\n' "$status"
1150
- printf 'ISSUE_ID=%s\n' "$issue_id"
1151
- printf 'PR_NUMBER=%s\n' "$pr_number"
1152
- printf 'OUTCOME=%s\n' "$result_outcome"
1153
- printf 'ACTION=%s\n' "$result_action"
1154
- printf 'FAILURE_REASON=%s\n' "$failure_reason"
1155
- printf 'RESULT_CONTRACT_NOTE=%s\n' "$issue_result_contract_note"
1156
- exit 0
1157
- fi
1158
-
1159
- status="FAILED"
1160
- failure_reason="invalid-result-contract"
1161
- issue_result_contract_note="invalid-result-contract"
1162
- normalize_issue_runner_state "failed" "${LAST_EXIT_CODE:-}" "${failure_reason}"
1163
- post_issue_comment_if_present
1164
- require_transition "issue_schedule_retry" issue_schedule_retry "$failure_reason"
1165
- require_transition "issue_mark_ready" issue_mark_ready
1166
- result_outcome="invalid-contract"
1167
- result_action="queued-issue-retry"
1168
- issue_set_reconcile_summary "$status" "$result_outcome" "$result_action" "$failure_reason"
1169
- cleanup_issue_session
1170
- notify_issue_reconciled
1171
- mark_reconciled
1172
- printf 'STATUS=%s\n' "$status"
1173
- printf 'ISSUE_ID=%s\n' "$issue_id"
1174
- printf 'PR_NUMBER=%s\n' "$pr_number"
1175
- printf 'OUTCOME=%s\n' "$result_outcome"
1176
- printf 'ACTION=%s\n' "$result_action"
1177
- printf 'FAILURE_REASON=%s\n' "$failure_reason"
1178
- printf 'RESULT_CONTRACT_NOTE=%s\n' "$issue_result_contract_note"
1179
- exit 0
1180
- fi
1181
- require_transition "issue_before_success" issue_before_success
1182
- if [[ "$result_outcome" == "blocked" ]]; then
1183
- ensure_issue_blocked_comment_artifact
1184
- require_transition "issue_before_blocked" issue_before_blocked
1185
- post_issue_comment_if_present
1186
- if issue_should_close_as_superseded; then
1187
- require_transition "issue_clear_retry" issue_clear_retry
1188
- require_transition "issue_remove_running" issue_remove_running
1189
- require_transition "issue_close_as_superseded" issue_close_as_superseded
1190
- result_action="closed-superseded"
1191
- issue_set_reconcile_summary "$status" "$result_outcome" "$result_action" "${failure_reason:-}"
1192
- cleanup_issue_session
1193
- notify_issue_reconciled
1194
- mark_reconciled
1195
- printf 'STATUS=%s\n' "$status"
1196
- printf 'ISSUE_ID=%s\n' "$issue_id"
1197
- printf 'PR_NUMBER=%s\n' "$pr_number"
1198
- printf 'OUTCOME=%s\n' "$result_outcome"
1199
- printf 'ACTION=%s\n' "$result_action"
1200
- exit 0
1201
- fi
1202
- failure_reason="$(infer_issue_blocked_failure_reason "${failure_reason:-}")"
1203
- normalize_issue_runner_state "succeeded" "0" ""
1204
- require_transition "issue_schedule_retry" issue_schedule_retry "$failure_reason"
1205
- require_transition "issue_mark_blocked" issue_mark_blocked
1206
- issue_set_reconcile_summary "$status" "$result_outcome" "$result_action" "$failure_reason"
1207
- cleanup_issue_session
1208
- notify_issue_reconciled
1209
- mark_reconciled
1210
- printf 'STATUS=%s\n' "$status"
1211
- printf 'ISSUE_ID=%s\n' "$issue_id"
1212
- printf 'PR_NUMBER=%s\n' "$pr_number"
1213
- printf 'OUTCOME=%s\n' "$result_outcome"
1214
- printf 'ACTION=%s\n' "$result_action"
1215
- printf 'FAILURE_REASON=%s\n' "$failure_reason"
1216
- exit 0
1217
- fi
1218
-
1219
- if [[ "$result_outcome" == "reported" ]]; then
1220
- normalize_issue_runner_state "succeeded" "0" ""
1221
- post_issue_comment_if_present
1222
- require_transition "issue_clear_retry" issue_clear_retry
1223
- require_transition "issue_remove_running" issue_remove_running
1224
- cleanup_issue_session
1225
- notify_issue_reconciled
1226
- mark_reconciled
1227
- printf 'STATUS=%s\n' "$status"
1228
- printf 'ISSUE_ID=%s\n' "$issue_id"
1229
- printf 'PR_NUMBER=%s\n' "$pr_number"
1230
- printf 'OUTCOME=%s\n' "$result_outcome"
1231
- printf 'ACTION=%s\n' "$result_action"
1232
- exit 0
1233
- fi
1234
-
1235
- # Push branch to remote BEFORE publish to preserve commits
1236
- # even if worktree gets cleaned up during publish process
1237
- if [[ -n "${BRANCH:-}" && -n "${WORKTREE:-}" && -d "${WORKTREE:-}" ]]; then
1238
- if git -C "${WORKTREE}" rev-parse --git-dir >/dev/null 2>&1; then
1239
- if ! git -C "${WORKTREE}" push -u origin "${BRANCH}" 2>/dev/null; then
1240
- printf 'PRE_PUBLISH_PUSH=failed branch=%s\n' "${BRANCH}" >&2
1241
- else
1242
- printf 'PRE_PUBLISH_PUSH=ok branch=%s\n' "${BRANCH}" >&2
1243
- fi
1244
- fi
1245
- fi
1246
-
1247
- if ! issue_has_recorded_verification; then
1248
- attempt_issue_host_verification_recovery "missing-worker-verification" || true
1249
- fi
1250
-
1251
- publish_args=()
1252
- while IFS= read -r publish_arg; do
1253
- [[ -n "$publish_arg" ]] || continue
1254
- publish_args+=("$publish_arg")
1255
- done < <(issue_publish_extra_args || true)
1256
-
1257
- publish_cmd=(
1258
- "${shared_tools_dir}/agent-project-publish-issue-pr"
1259
- --repo-slug "$repo_slug"
1260
- --runs-root "$runs_root"
1261
- --history-root "$history_root"
1262
- --session "$session"
1263
- )
1264
- if [[ ${#publish_args[@]} -gt 0 ]]; then
1265
- publish_cmd+=("${publish_args[@]}")
1266
- fi
1267
-
1268
- if ! publish_out="$("${publish_cmd[@]}" 2>&1)"; then
1269
- publish_blocker_reason="$(classify_issue_publish_blocker "$publish_out" || true)"
1270
- if [[ "$publish_blocker_reason" == "verification-guard-blocked" ]]; then
1271
- recovered_worktree="$(extract_recovery_worktree_from_publish_output "$publish_out" || true)"
1272
- if [[ -n "$recovered_worktree" && -d "$recovered_worktree" ]]; then
1273
- WORKTREE="$recovered_worktree"
1274
- fi
1275
- if attempt_issue_host_verification_recovery "verification-guard-blocked"; then
1276
- if publish_out="$("${publish_cmd[@]}" 2>&1)"; then
1277
- pr_number="$(awk -F= '/^PR_NUMBER=/{print $2}' <<<"$publish_out")"
1278
- normalize_issue_runner_state "succeeded" "0" ""
1279
- require_transition "issue_clear_retry" issue_clear_retry
1280
- require_transition "issue_remove_running" issue_remove_running
1281
- if [[ -n "$pr_number" ]]; then
1282
- require_transition "issue_after_pr_created" issue_after_pr_created "$pr_number"
1283
- fi
1284
- cleanup_issue_session
1285
- notify_issue_reconciled
1286
- mark_reconciled
1287
- printf 'STATUS=%s\n' "$status"
1288
- printf 'ISSUE_ID=%s\n' "$issue_id"
1289
- printf 'PR_NUMBER=%s\n' "$pr_number"
1290
- printf 'OUTCOME=%s\n' "$result_outcome"
1291
- printf 'ACTION=%s\n' "$result_action"
1292
- if [[ -n "$issue_result_contract_note" ]]; then
1293
- printf 'RESULT_CONTRACT_NOTE=%s\n' "$issue_result_contract_note"
1294
- fi
1295
- exit 0
1296
- fi
1297
- publish_blocker_reason="$(classify_issue_publish_blocker "$publish_out" || true)"
1298
- fi
1299
- fi
1300
- blocker_body="$(build_issue_publish_blocker_comment "${publish_blocker_reason:-}" "${publish_out}")"
1301
- write_issue_comment_artifact "${blocker_body}" || true
1302
- post_issue_comment_if_present
1303
- if [[ -n "$publish_blocker_reason" ]]; then
1304
- require_transition "issue_before_blocked" issue_before_blocked
1305
- normalize_issue_runner_state "succeeded" "0" ""
1306
- require_transition "issue_schedule_retry" issue_schedule_retry "$publish_blocker_reason"
1307
- require_transition "issue_mark_blocked" issue_mark_blocked
1308
- result_outcome="blocked"
1309
- result_action="host-comment-blocker"
1310
- failure_reason="$publish_blocker_reason"
1311
- issue_set_reconcile_summary "$status" "$result_outcome" "$result_action" "$failure_reason"
1312
- cleanup_issue_session
1313
- notify_issue_reconciled
1314
- mark_reconciled
1315
- printf 'STATUS=%s\n' "$status"
1316
- printf 'ISSUE_ID=%s\n' "$issue_id"
1317
- printf 'PR_NUMBER=%s\n' "$pr_number"
1318
- printf 'OUTCOME=%s\n' "$result_outcome"
1319
- printf 'ACTION=%s\n' "$result_action"
1320
- printf 'FAILURE_REASON=%s\n' "$failure_reason"
1321
- printf 'PUBLISH_ERROR=%s\n' "$(printf '%s' "$publish_out" | tr '\n' ' ' | sed 's/ */ /g')"
1322
- exit 0
1323
- fi
1324
- require_transition "issue_schedule_retry" issue_schedule_retry "host-publish-failed"
1325
- require_transition "issue_mark_ready" issue_mark_ready
1326
- issue_set_reconcile_summary "$status" "" "" "host-publish-failed"
1327
- # Push branch to remote before cleanup to preserve commits for next retry
1328
- if [[ -n "${BRANCH:-}" && -d "${WORKTREE:-}" && ( -f "${WORKTREE:-}/.git" || -d "${WORKTREE:-}/.git" ) ]]; then
1329
- if ! git -C "$WORKTREE" push -u origin "$BRANCH" 2>/dev/null; then
1330
- printf 'WORKTREE_PUSH_BEFORE_CLEANUP=failed\n' >&2
1331
- else
1332
- printf 'WORKTREE_PUSH_BEFORE_CLEANUP=ok\n' >&2
1333
- fi
1334
- fi
1335
- cleanup_issue_session
1336
- notify_issue_reconciled
1337
- mark_reconciled
1338
- printf 'STATUS=%s\n' "$status"
1339
- printf 'ISSUE_ID=%s\n' "$issue_id"
1340
- printf 'PR_NUMBER=%s\n' "$pr_number"
1341
- printf 'PUBLISH_ERROR=%s\n' "$(printf '%s' "$publish_out" | tr '\n' ' ' | sed 's/ */ /g')"
1342
- exit 0
1343
- fi
1344
-
1345
- pr_number="$(awk -F= '/^PR_NUMBER=/{print $2}' <<<"$publish_out")"
1346
- normalize_issue_runner_state "succeeded" "0" ""
1347
- require_transition "issue_clear_retry" issue_clear_retry
1348
- require_transition "issue_remove_running" issue_remove_running
1349
- if [[ -n "$pr_number" ]]; then
1350
- require_transition "issue_after_pr_created" issue_after_pr_created "$pr_number"
1351
- fi
1352
- cleanup_issue_session
1353
- notify_issue_reconciled
1354
- ;;
1355
- FAILED)
1356
- failure_reason="$(normalize_issue_failure_reason "${failure_reason:-worker-exit-failed}")"
1357
- schedule_provider_quota_cooldown "${failure_reason}"
1358
- normalize_issue_runner_state "failed" "${LAST_EXIT_CODE:-}" "${failure_reason}"
1359
- if [[ "${result_outcome:-}" == "blocked" && "${result_action:-}" == "host-comment-blocker" ]]; then
1360
- if [[ ! -s "${run_dir}/issue-comment.md" ]]; then
1361
- write_issue_comment_artifact "$(build_issue_runtime_blocker_comment "${failure_reason}")" || true
1362
- fi
1363
- post_issue_comment_if_present
1364
- issue_set_reconcile_summary "$status" "$result_outcome" "$result_action" "$failure_reason"
1365
- elif [[ "${failure_reason}" == "provider-quota-limit" ]]; then
1366
- if [[ ! -s "${run_dir}/issue-comment.md" ]]; then
1367
- write_issue_comment_artifact "$(build_issue_runtime_blocker_comment "${failure_reason}")" || true
1368
- fi
1369
- post_issue_comment_if_present
1370
- issue_set_reconcile_summary "$status" "" "" "$failure_reason"
1371
- else
1372
- issue_set_reconcile_summary "$status" "" "" "$failure_reason"
1373
- fi
1374
- require_transition "issue_schedule_retry" issue_schedule_retry "${failure_reason}"
1375
- require_transition "issue_mark_ready" issue_mark_ready
1376
- cleanup_issue_session
1377
- notify_issue_reconciled
1378
- ;;
1379
- *)
1380
- ;;
1381
- esac
1382
-
1383
- mark_reconciled
1384
- printf 'STATUS=%s\n' "$status"
1385
- printf 'ISSUE_ID=%s\n' "$issue_id"
1386
- printf 'PR_NUMBER=%s\n' "$pr_number"
1387
- if [[ -n "${issue_summary_outcome:-}" ]]; then
1388
- printf 'OUTCOME=%s\n' "${issue_summary_outcome}"
1389
- fi
1390
- if [[ -n "${issue_summary_action:-}" ]]; then
1391
- printf 'ACTION=%s\n' "${issue_summary_action}"
1392
- fi
1393
- if [[ -n "$failure_reason" ]]; then
1394
- printf 'FAILURE_REASON=%s\n' "$failure_reason"
1395
- fi
1396
- if [[ -n "$issue_result_contract_note" ]]; then
1397
- printf 'RESULT_CONTRACT_NOTE=%s\n' "$issue_result_contract_note"
1398
- fi