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.
- package/README.md +109 -13
- package/npm/bin/agent-control-plane.js +1 -1
- package/package.json +39 -33
- package/tools/bin/debug-session.sh +106 -0
- package/tools/bin/flow-config-lib.sh +13 -3508
- package/tools/bin/flow-execution-lib.sh +243 -0
- package/tools/bin/flow-forge-lib.sh +1770 -0
- package/tools/bin/flow-profile-lib.sh +335 -0
- package/tools/bin/flow-provider-lib.sh +981 -0
- package/tools/bin/flow-runtime-doctor-linux.sh +136 -0
- package/tools/bin/flow-runtime-doctor.sh +5 -1
- package/tools/bin/flow-session-lib.sh +317 -0
- package/tools/bin/install-project-systemd.sh +255 -0
- package/tools/bin/project-runtimectl.sh +45 -0
- package/tools/bin/project-systemd-bootstrap.sh +74 -0
- package/tools/bin/uninstall-project-systemd.sh +87 -0
- package/tools/dashboard/app.js +238 -8
- package/tools/dashboard/issue_queue_state.py +101 -0
- package/tools/dashboard/requirements.txt +3 -0
- package/tools/dashboard/server.py +250 -30
- package/tools/dashboard/styles.css +526 -455
- package/tools/bin/agent-cleanup-worktree +0 -247
- package/tools/bin/agent-github-update-labels +0 -105
- package/tools/bin/agent-init-worktree +0 -216
- package/tools/bin/agent-project-archive-run +0 -52
- package/tools/bin/agent-project-capture-worker +0 -46
- package/tools/bin/agent-project-catch-up-issue-pr-links +0 -118
- package/tools/bin/agent-project-catch-up-merged-prs +0 -195
- package/tools/bin/agent-project-catch-up-scheduled-issue-retries +0 -123
- package/tools/bin/agent-project-cleanup-session +0 -513
- package/tools/bin/agent-project-detached-launch +0 -127
- package/tools/bin/agent-project-heartbeat-loop +0 -1029
- package/tools/bin/agent-project-open-issue-worktree +0 -89
- package/tools/bin/agent-project-open-pr-worktree +0 -80
- package/tools/bin/agent-project-publish-issue-pr +0 -468
- package/tools/bin/agent-project-reconcile-issue-session +0 -1409
- package/tools/bin/agent-project-reconcile-pr-session +0 -1288
- package/tools/bin/agent-project-retry-state +0 -158
- package/tools/bin/agent-project-run-claude-session +0 -805
- package/tools/bin/agent-project-run-codex-resilient +0 -963
- package/tools/bin/agent-project-run-codex-session +0 -435
- package/tools/bin/agent-project-run-kilo-session +0 -369
- package/tools/bin/agent-project-run-ollama-session +0 -658
- package/tools/bin/agent-project-run-openclaw-session +0 -1309
- package/tools/bin/agent-project-run-opencode-session +0 -377
- package/tools/bin/agent-project-run-pi-session +0 -479
- package/tools/bin/agent-project-sync-anchor-repo +0 -139
- package/tools/bin/agent-project-sync-source-repo-main +0 -163
- package/tools/bin/agent-project-worker-status +0 -188
- package/tools/bin/branch-verification-guard.sh +0 -364
- package/tools/bin/capture-worker.sh +0 -18
- package/tools/bin/cleanup-worktree.sh +0 -52
- package/tools/bin/codex-quota +0 -31
- package/tools/bin/create-follow-up-issue.sh +0 -114
- package/tools/bin/dashboard-launchd-bootstrap.sh +0 -50
- package/tools/bin/issue-publish-localization-guard.sh +0 -142
- package/tools/bin/issue-publish-scope-guard.sh +0 -242
- package/tools/bin/issue-requires-local-workspace-install.sh +0 -31
- package/tools/bin/issue-resource-class.sh +0 -12
- package/tools/bin/kick-scheduler.sh +0 -75
- package/tools/bin/label-follow-up-issues.sh +0 -14
- package/tools/bin/new-pr-worktree.sh +0 -50
- package/tools/bin/new-worktree.sh +0 -49
- package/tools/bin/pr-risk.sh +0 -12
- package/tools/bin/prepare-worktree.sh +0 -142
- package/tools/bin/provider-cooldown-state.sh +0 -204
- package/tools/bin/publish-issue-worker.sh +0 -31
- package/tools/bin/reconcile-bootstrap-lib.sh +0 -113
- package/tools/bin/reconcile-issue-worker.sh +0 -34
- package/tools/bin/reconcile-pr-worker.sh +0 -34
- package/tools/bin/record-verification.sh +0 -71
- package/tools/bin/render-flow-config.sh +0 -98
- package/tools/bin/resident-issue-controller-lib.sh +0 -448
- package/tools/bin/retry-state.sh +0 -31
- package/tools/bin/reuse-issue-worktree.sh +0 -121
- package/tools/bin/run-codex-bypass.sh +0 -3
- package/tools/bin/run-codex-safe.sh +0 -3
- package/tools/bin/run-codex-task.sh +0 -280
- package/tools/bin/serve-dashboard.sh +0 -5
- package/tools/bin/start-issue-worker.sh +0 -943
- package/tools/bin/start-pr-fix-worker.sh +0 -528
- package/tools/bin/start-pr-merge-repair-worker.sh +0 -8
- package/tools/bin/start-pr-review-worker.sh +0 -261
- package/tools/bin/start-resident-issue-loop.sh +0 -499
- package/tools/bin/update-github-labels.sh +0 -14
- package/tools/bin/worker-status.sh +0 -19
- 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
|