agent-control-plane 0.1.9 → 0.1.13
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/hooks/heartbeat-hooks.sh +147 -8
- package/hooks/issue-reconcile-hooks.sh +46 -0
- package/npm/bin/agent-control-plane.js +89 -8
- package/package.json +8 -2
- package/references/commands.md +1 -0
- package/tools/bin/agent-project-cleanup-session +133 -0
- package/tools/bin/agent-project-publish-issue-pr +178 -62
- package/tools/bin/agent-project-reconcile-issue-session +171 -3
- package/tools/bin/agent-project-run-codex-resilient +121 -16
- package/tools/bin/agent-project-run-codex-session +118 -10
- package/tools/bin/agent-project-run-openclaw-session +82 -8
- package/tools/bin/branch-verification-guard.sh +15 -2
- package/tools/bin/cleanup-worktree.sh +4 -1
- package/tools/bin/dashboard-launchd-bootstrap.sh +16 -4
- package/tools/bin/ensure-runtime-sync.sh +182 -0
- package/tools/bin/flow-config-lib.sh +76 -30
- package/tools/bin/flow-resident-worker-lib.sh +28 -2
- package/tools/bin/flow-shell-lib.sh +15 -1
- package/tools/bin/heartbeat-safe-auto.sh +32 -0
- package/tools/bin/issue-publish-localization-guard.sh +142 -0
- package/tools/bin/project-launchd-bootstrap.sh +17 -4
- package/tools/bin/project-runtime-supervisor.sh +7 -1
- package/tools/bin/project-runtimectl.sh +78 -15
- package/tools/bin/reuse-issue-worktree.sh +46 -0
- package/tools/bin/start-issue-worker.sh +110 -30
- package/tools/bin/start-resident-issue-loop.sh +1 -0
- package/tools/bin/sync-shared-agent-home.sh +50 -10
- package/tools/bin/test-smoke.sh +6 -1
- package/tools/dashboard/app.js +71 -1
- package/tools/dashboard/dashboard_snapshot.py +74 -0
- package/tools/dashboard/styles.css +43 -0
- package/tools/templates/issue-prompt-template.md +20 -65
- package/tools/templates/legacy/issue-prompt-template-pre-slim.md +109 -0
- package/bin/audit-issue-routing.sh +0 -74
- package/tools/bin/audit-agent-worktrees.sh +0 -310
- package/tools/bin/audit-issue-routing.sh +0 -11
- package/tools/bin/audit-retained-layout.sh +0 -58
- package/tools/bin/audit-retained-overlap.sh +0 -135
- package/tools/bin/audit-retained-worktrees.sh +0 -228
- package/tools/bin/check-skill-contracts.sh +0 -324
|
@@ -24,8 +24,10 @@ base_branch="main"
|
|
|
24
24
|
remote_name="origin"
|
|
25
25
|
dry_run="false"
|
|
26
26
|
keep_open_label=""
|
|
27
|
+
meta_file_from_archive="no"
|
|
27
28
|
shared_agent_home="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
|
28
29
|
scope_guard_script="${shared_agent_home}/tools/bin/issue-publish-scope-guard.sh"
|
|
30
|
+
localization_guard_script="${shared_agent_home}/tools/bin/issue-publish-localization-guard.sh"
|
|
29
31
|
verification_guard_script="${shared_agent_home}/tools/bin/branch-verification-guard.sh"
|
|
30
32
|
|
|
31
33
|
while [[ $# -gt 0 ]]; do
|
|
@@ -51,11 +53,16 @@ fi
|
|
|
51
53
|
find_archived_session_dir() {
|
|
52
54
|
local root="${1:-}"
|
|
53
55
|
local target_session="${2:-}"
|
|
56
|
+
local latest=""
|
|
54
57
|
[[ -n "$root" && -d "$root" && -n "$target_session" ]] || return 1
|
|
55
58
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
+
latest="$(
|
|
60
|
+
command find "$root" -mindepth 1 -maxdepth 1 -type d -name "${target_session}-*" ! -name "${target_session}-stale-*" 2>/dev/null \
|
|
61
|
+
| LC_ALL=C sort -r \
|
|
62
|
+
| sed -n '1p'
|
|
63
|
+
)"
|
|
64
|
+
[[ -n "$latest" ]] || return 1
|
|
65
|
+
printf '%s\n' "$latest"
|
|
59
66
|
}
|
|
60
67
|
|
|
61
68
|
find_existing_worktree_for_branch() {
|
|
@@ -88,6 +95,64 @@ find_existing_worktree_for_branch() {
|
|
|
88
95
|
return 1
|
|
89
96
|
}
|
|
90
97
|
|
|
98
|
+
worktree_is_git_repo() {
|
|
99
|
+
local worktree_path="${1:-}"
|
|
100
|
+
[[ -n "$worktree_path" && -d "$worktree_path" ]] || return 1
|
|
101
|
+
git -C "$worktree_path" rev-parse --is-inside-work-tree >/dev/null 2>&1
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
worktree_matches_publish_target() {
|
|
105
|
+
local worktree_path="${1:-}"
|
|
106
|
+
local current_branch=""
|
|
107
|
+
local current_head=""
|
|
108
|
+
|
|
109
|
+
worktree_is_git_repo "$worktree_path" || return 1
|
|
110
|
+
|
|
111
|
+
current_head="$(git -C "$worktree_path" rev-parse HEAD 2>/dev/null || true)"
|
|
112
|
+
current_branch="$(git -C "$worktree_path" symbolic-ref --quiet --short HEAD 2>/dev/null || true)"
|
|
113
|
+
|
|
114
|
+
if [[ -n "${FINAL_HEAD:-}" && -n "$current_head" && "$current_head" == "$FINAL_HEAD" ]]; then
|
|
115
|
+
return 0
|
|
116
|
+
fi
|
|
117
|
+
|
|
118
|
+
if [[ -n "${BRANCH:-}" && -n "$current_branch" && "$current_branch" == "$BRANCH" ]]; then
|
|
119
|
+
return 0
|
|
120
|
+
fi
|
|
121
|
+
|
|
122
|
+
return 1
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
recover_worktree_from_remote_branch() {
|
|
126
|
+
local repo_root="${1:-}"
|
|
127
|
+
local worktree_root="${2:-}"
|
|
128
|
+
local branch_name="${3:-}"
|
|
129
|
+
local remote="${4:-}"
|
|
130
|
+
local remote_ref=""
|
|
131
|
+
local fallback_worktree=""
|
|
132
|
+
|
|
133
|
+
[[ -n "$repo_root" && -d "$repo_root" && -n "$worktree_root" && -n "$branch_name" && -n "$remote" ]] || return 1
|
|
134
|
+
|
|
135
|
+
remote_ref="refs/remotes/${remote}/${branch_name}"
|
|
136
|
+
git -C "$repo_root" fetch "$remote" "$branch_name" >/dev/null 2>&1 || true
|
|
137
|
+
git -C "$repo_root" rev-parse --verify "$remote_ref" >/dev/null 2>&1 || return 1
|
|
138
|
+
|
|
139
|
+
fallback_worktree="$(mktemp -d "${worktree_root}/recovery-XXXXXX")"
|
|
140
|
+
git -C "$repo_root" worktree add --detach "$fallback_worktree" "$remote_ref" >/dev/null 2>&1 || {
|
|
141
|
+
rm -rf "$fallback_worktree" 2>/dev/null || true
|
|
142
|
+
return 1
|
|
143
|
+
}
|
|
144
|
+
worktree_is_git_repo "$fallback_worktree" || {
|
|
145
|
+
rm -rf "$fallback_worktree" 2>/dev/null || true
|
|
146
|
+
return 1
|
|
147
|
+
}
|
|
148
|
+
git -C "$fallback_worktree" checkout -B "$branch_name" "$remote_ref" >/dev/null 2>&1 || {
|
|
149
|
+
rm -rf "$fallback_worktree" 2>/dev/null || true
|
|
150
|
+
return 1
|
|
151
|
+
}
|
|
152
|
+
git -C "$fallback_worktree" branch --set-upstream-to "${remote}/${branch_name}" "$branch_name" >/dev/null 2>&1 || true
|
|
153
|
+
printf '%s\n' "$fallback_worktree"
|
|
154
|
+
}
|
|
155
|
+
|
|
91
156
|
status_out="$(
|
|
92
157
|
"${shared_agent_home}/tools/bin/agent-project-worker-status" \
|
|
93
158
|
--runs-root "$runs_root" \
|
|
@@ -98,6 +163,7 @@ if [[ -z "$meta_file" || ! -f "$meta_file" ]]; then
|
|
|
98
163
|
archived_run_dir="$(find_archived_session_dir "$history_root" "$session" || true)"
|
|
99
164
|
if [[ -n "$archived_run_dir" && -f "${archived_run_dir}/run.env" ]]; then
|
|
100
165
|
meta_file="${archived_run_dir}/run.env"
|
|
166
|
+
meta_file_from_archive="yes"
|
|
101
167
|
fi
|
|
102
168
|
fi
|
|
103
169
|
if [[ -z "$meta_file" || ! -f "$meta_file" ]]; then
|
|
@@ -117,10 +183,34 @@ if [[ -z "${ISSUE_ID:-}" || -z "${BRANCH:-}" ]]; then
|
|
|
117
183
|
exit 1
|
|
118
184
|
fi
|
|
119
185
|
|
|
186
|
+
resolve_actor_login() {
|
|
187
|
+
local login=""
|
|
188
|
+
|
|
189
|
+
flow_export_github_cli_auth_env "${repo_slug}"
|
|
190
|
+
login="$(gh api user --jq .login 2>/dev/null || true)"
|
|
191
|
+
if [[ -z "${login}" ]]; then
|
|
192
|
+
login="$(
|
|
193
|
+
gh auth status 2>/dev/null \
|
|
194
|
+
| sed -n 's/^ ✓ Logged in to github.com account \([^ ]*\) (.*/\1/p' \
|
|
195
|
+
| head -n 1
|
|
196
|
+
)"
|
|
197
|
+
fi
|
|
198
|
+
if [[ -z "${login}" ]]; then
|
|
199
|
+
login="${USER:-}"
|
|
200
|
+
fi
|
|
201
|
+
printf '%s\n' "${login}"
|
|
202
|
+
}
|
|
203
|
+
|
|
120
204
|
issue_json="$(flow_github_issue_view_json "$repo_slug" "$ISSUE_ID")"
|
|
121
205
|
issue_title="$(jq -r '.title' <<<"$issue_json")"
|
|
122
206
|
issue_url="$(jq -r '.url' <<<"$issue_json")"
|
|
123
|
-
|
|
207
|
+
if [[ -z "${issue_title}" || "${issue_title}" == "null" ]]; then
|
|
208
|
+
issue_title="Issue #${ISSUE_ID}"
|
|
209
|
+
fi
|
|
210
|
+
if [[ -z "${issue_url}" || "${issue_url}" == "null" ]]; then
|
|
211
|
+
issue_url="${ISSUE_URL:-}"
|
|
212
|
+
fi
|
|
213
|
+
actor_login="${GITHUB_ACTOR:-$(resolve_actor_login)}"
|
|
124
214
|
issue_keep_open="no"
|
|
125
215
|
if [[ -n "$keep_open_label" ]] && jq -e --arg label "$keep_open_label" 'any(.labels[]?; .name == $label)' >/dev/null <<<"$issue_json"; then
|
|
126
216
|
issue_keep_open="yes"
|
|
@@ -206,73 +296,93 @@ if [[ -n "$pr_json" ]]; then
|
|
|
206
296
|
pr_number="$(jq -r '.number' <<<"$pr_json")"
|
|
207
297
|
pr_url="$(jq -r '.url' <<<"$pr_json")"
|
|
208
298
|
else
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
299
|
+
local_repo_root="$(flow_resolve_agent_repo_root "${CONFIG_YAML}")"
|
|
300
|
+
local_worktree_root="$(flow_resolve_worktree_root "${CONFIG_YAML}")"
|
|
301
|
+
remote_branch_ref="refs/remotes/${remote_name}/${BRANCH}"
|
|
302
|
+
existing_worktree=""
|
|
303
|
+
fallback_worktree=""
|
|
304
|
+
remote_head=""
|
|
305
|
+
current_head=""
|
|
306
|
+
|
|
307
|
+
if [[ "${meta_file_from_archive}" == "yes" && -n "${WORKTREE:-}" ]]; then
|
|
308
|
+
printf 'WORKTREE_RECOVERY=ignored-archived-pointer\n' >&2
|
|
309
|
+
printf 'RECOVERY_WORKTREE=%s\n' "${WORKTREE}" >&2
|
|
310
|
+
WORKTREE=""
|
|
311
|
+
fi
|
|
312
|
+
|
|
313
|
+
if [[ -n "${WORKTREE:-}" ]] && ! worktree_matches_publish_target "${WORKTREE}"; then
|
|
314
|
+
printf 'WORKTREE_RECOVERY=ignored-stale\n' >&2
|
|
315
|
+
printf 'RECOVERY_WORKTREE=%s\n' "${WORKTREE}" >&2
|
|
316
|
+
WORKTREE=""
|
|
317
|
+
fi
|
|
318
|
+
|
|
319
|
+
git -C "$local_repo_root" worktree prune >/dev/null 2>&1 || true
|
|
320
|
+
|
|
321
|
+
if [[ -z "${WORKTREE:-}" ]]; then
|
|
322
|
+
existing_worktree="$(find_existing_worktree_for_branch "$local_repo_root" "$BRANCH" || true)"
|
|
323
|
+
if [[ -n "$existing_worktree" ]] && worktree_matches_publish_target "$existing_worktree"; then
|
|
324
|
+
WORKTREE="$existing_worktree"
|
|
325
|
+
printf 'WORKTREE_RECOVERY=reused-existing\n' >&2
|
|
326
|
+
printf 'RECOVERY_WORKTREE=%s\n' "$existing_worktree" >&2
|
|
327
|
+
fi
|
|
328
|
+
fi
|
|
329
|
+
|
|
330
|
+
if [[ -z "${WORKTREE:-}" && "${meta_file_from_archive}" == "yes" ]]; then
|
|
331
|
+
fallback_worktree="$(recover_worktree_from_remote_branch "$local_repo_root" "$local_worktree_root" "$BRANCH" "$remote_name" || true)"
|
|
332
|
+
if [[ -n "$fallback_worktree" ]]; then
|
|
333
|
+
WORKTREE="$fallback_worktree"
|
|
334
|
+
printf 'WORKTREE_RECOVERY=from-remote\n' >&2
|
|
335
|
+
printf 'RECOVERY_WORKTREE=%s\n' "$fallback_worktree" >&2
|
|
336
|
+
fi
|
|
337
|
+
fi
|
|
338
|
+
|
|
339
|
+
if [[ -z "${WORKTREE:-}" ]]; then
|
|
340
|
+
if ! git -C "$local_repo_root" rev-parse --verify "${BRANCH}" >/dev/null 2>&1; then
|
|
341
|
+
if [[ -n "${FINAL_HEAD:-}" ]] && git -C "$local_repo_root" cat-file -e "${FINAL_HEAD}^{commit}" >/dev/null 2>&1; then
|
|
342
|
+
git -C "$local_repo_root" branch -f "$BRANCH" "$FINAL_HEAD" >/dev/null 2>&1 || true
|
|
221
343
|
printf 'BRANCH_RECOVERY=from-final-head\n' >&2
|
|
222
344
|
printf 'RECOVERY_HEAD=%s\n' "$FINAL_HEAD" >&2
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
# Check if the branch exists in the baseline repo
|
|
226
|
-
if ! git -C "$local_repo_root" rev-parse --verify "${BRANCH}" >/dev/null 2>&1; then
|
|
227
|
-
recover_branch_from_final_head || true
|
|
228
345
|
fi
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
346
|
+
fi
|
|
347
|
+
|
|
348
|
+
existing_worktree="$(find_existing_worktree_for_branch "$local_repo_root" "$BRANCH" || true)"
|
|
349
|
+
if [[ -n "$existing_worktree" ]] && worktree_is_git_repo "$existing_worktree"; then
|
|
350
|
+
WORKTREE="$existing_worktree"
|
|
351
|
+
printf 'WORKTREE_RECOVERY=reused-existing\n' >&2
|
|
352
|
+
printf 'RECOVERY_WORKTREE=%s\n' "$existing_worktree" >&2
|
|
353
|
+
else
|
|
354
|
+
fallback_worktree="$(mktemp -d "${local_worktree_root}/recovery-XXXXXX")"
|
|
355
|
+
if git -C "$local_repo_root" worktree add "$fallback_worktree" "$BRANCH" >/dev/null 2>&1 \
|
|
356
|
+
|| git -C "$local_repo_root" worktree add "$fallback_worktree" "$remote_branch_ref" >/dev/null 2>&1; then
|
|
357
|
+
if worktree_is_git_repo "$fallback_worktree"; then
|
|
358
|
+
WORKTREE="$fallback_worktree"
|
|
359
|
+
printf 'WORKTREE_RECOVERY=created\n' >&2
|
|
360
|
+
printf 'RECOVERY_WORKTREE=%s\n' "$fallback_worktree" >&2
|
|
237
361
|
else
|
|
238
|
-
|
|
239
|
-
git -C "$local_repo_root" worktree add "$fallback_worktree" "$BRANCH" 2>/dev/null || true
|
|
240
|
-
# Validate the worktree is a real git repo, not just an empty directory
|
|
241
|
-
if [[ -f "$fallback_worktree/.git" || -d "$fallback_worktree/.git" ]]; then
|
|
242
|
-
WORKTREE="$fallback_worktree"
|
|
243
|
-
printf 'WORKTREE_RECOVERY=created\n' >&2
|
|
244
|
-
printf 'RECOVERY_WORKTREE=%s\n' "$fallback_worktree" >&2
|
|
245
|
-
else
|
|
246
|
-
rm -rf "$fallback_worktree" 2>/dev/null || true
|
|
247
|
-
fallback_worktree=""
|
|
248
|
-
fi
|
|
249
|
-
fi
|
|
250
|
-
fi
|
|
251
|
-
# Check if branch exists on remote
|
|
252
|
-
if [[ -z "${fallback_worktree}" ]]; then
|
|
253
|
-
git -C "$local_repo_root" fetch "$remote_name" 2>/dev/null || true
|
|
254
|
-
if git -C "$local_repo_root" rev-parse --verify "refs/remotes/${remote_name}/${BRANCH}" >/dev/null 2>&1; then
|
|
255
|
-
fallback_worktree="$(mktemp -d "${local_worktree_root}/recovery-XXXXXX")"
|
|
256
|
-
git -C "$local_repo_root" worktree add "$fallback_worktree" "refs/remotes/${remote_name}/${BRANCH}" 2>/dev/null || true
|
|
257
|
-
# Validate the worktree is a real git repo, not just an empty directory
|
|
258
|
-
if [[ -f "$fallback_worktree/.git" || -d "$fallback_worktree/.git" ]]; then
|
|
259
|
-
WORKTREE="$fallback_worktree"
|
|
260
|
-
printf 'WORKTREE_RECOVERY=from-remote\n' >&2
|
|
261
|
-
printf 'RECOVERY_WORKTREE=%s\n' "$fallback_worktree" >&2
|
|
262
|
-
else
|
|
263
|
-
rm -rf "$fallback_worktree" 2>/dev/null || true
|
|
264
|
-
fallback_worktree=""
|
|
265
|
-
fi
|
|
362
|
+
rm -rf "$fallback_worktree" 2>/dev/null || true
|
|
266
363
|
fi
|
|
364
|
+
else
|
|
365
|
+
rm -rf "$fallback_worktree" 2>/dev/null || true
|
|
267
366
|
fi
|
|
268
367
|
fi
|
|
269
|
-
if [[ -z "${WORKTREE:-}" || ! -d "${WORKTREE:-}" ]]; then
|
|
270
|
-
echo "session $session is missing a publishable worktree (branch ${BRANCH:-unknown} not found locally or on remote ${remote_name})" >&2
|
|
271
|
-
exit 1
|
|
272
|
-
fi
|
|
273
368
|
fi
|
|
274
369
|
|
|
275
|
-
|
|
370
|
+
if [[ -z "${WORKTREE:-}" || ! -d "${WORKTREE:-}" ]]; then
|
|
371
|
+
echo "session $session is missing a publishable worktree (branch ${BRANCH:-unknown} not found locally or on remote ${remote_name})" >&2
|
|
372
|
+
exit 1
|
|
373
|
+
fi
|
|
374
|
+
|
|
375
|
+
git -C "$WORKTREE" fetch "$remote_name" "$base_branch" "$BRANCH" --prune >/dev/null 2>&1 || git -C "$WORKTREE" fetch "$remote_name" "$base_branch" --prune >/dev/null 2>&1 || true
|
|
376
|
+
if git -C "$local_repo_root" rev-parse --verify "${remote_branch_ref}" >/dev/null 2>&1; then
|
|
377
|
+
remote_head="$(git -C "$local_repo_root" rev-parse "${remote_branch_ref}" 2>/dev/null || true)"
|
|
378
|
+
current_head="$(git -C "$WORKTREE" rev-parse HEAD 2>/dev/null || true)"
|
|
379
|
+
if [[ -n "${remote_head}" && "${current_head}" != "${remote_head}" ]]; then
|
|
380
|
+
git -C "$WORKTREE" checkout -B "$BRANCH" "${remote_branch_ref}" >/dev/null 2>&1 || true
|
|
381
|
+
git -C "$WORKTREE" branch --set-upstream-to "${remote_name}/${BRANCH}" "$BRANCH" >/dev/null 2>&1 || true
|
|
382
|
+
printf 'WORKTREE_RECOVERY=aligned-remote\n' >&2
|
|
383
|
+
printf 'RECOVERY_HEAD=%s\n' "${remote_head}" >&2
|
|
384
|
+
fi
|
|
385
|
+
fi
|
|
276
386
|
|
|
277
387
|
head_sha="$(git -C "$WORKTREE" rev-parse HEAD)"
|
|
278
388
|
ahead_count="$(git -C "$WORKTREE" rev-list --count "${remote_name}/${base_branch}"..HEAD)"
|
|
@@ -286,6 +396,12 @@ else
|
|
|
286
396
|
--base-ref "${remote_name}/${base_branch}" \
|
|
287
397
|
--issue-id "$ISSUE_ID"
|
|
288
398
|
|
|
399
|
+
if [[ -x "${localization_guard_script}" ]]; then
|
|
400
|
+
"${localization_guard_script}" \
|
|
401
|
+
--worktree "$WORKTREE" \
|
|
402
|
+
--base-ref "${remote_name}/${base_branch}"
|
|
403
|
+
fi
|
|
404
|
+
|
|
289
405
|
"${verification_guard_script}" \
|
|
290
406
|
--worktree "$WORKTREE" \
|
|
291
407
|
--base-ref "${remote_name}/${base_branch}" \
|
|
@@ -305,7 +305,7 @@ provider_cooldown_script="${shared_tools_dir}/provider-cooldown-state.sh"
|
|
|
305
305
|
|
|
306
306
|
schedule_provider_quota_cooldown() {
|
|
307
307
|
local reason="${1:-provider-quota-limit}"
|
|
308
|
-
[[ "${
|
|
308
|
+
[[ "${reason}" == "provider-quota-limit" ]] || return 0
|
|
309
309
|
[[ -x "${provider_cooldown_script}" ]] || return 0
|
|
310
310
|
|
|
311
311
|
"${provider_cooldown_script}" schedule "${reason}" >/dev/null || true
|
|
@@ -317,6 +317,54 @@ clear_provider_quota_cooldown() {
|
|
|
317
317
|
"${provider_cooldown_script}" clear >/dev/null || true
|
|
318
318
|
}
|
|
319
319
|
|
|
320
|
+
normalize_issue_failure_reason() {
|
|
321
|
+
local current_reason="${1:-}"
|
|
322
|
+
|
|
323
|
+
case "${current_reason}" in
|
|
324
|
+
usage-limit|quota-switch-deferred|quota-switch-attempt-limit)
|
|
325
|
+
if [[ "${CODING_WORKER:-}" == "codex" ]]; then
|
|
326
|
+
printf 'provider-quota-limit\n'
|
|
327
|
+
return 0
|
|
328
|
+
fi
|
|
329
|
+
;;
|
|
330
|
+
esac
|
|
331
|
+
|
|
332
|
+
printf '%s\n' "${current_reason}"
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
issue_runtime_log_file() {
|
|
336
|
+
if [[ -f "${run_dir}/${session}.log" ]]; then
|
|
337
|
+
printf '%s\n' "${run_dir}/${session}.log"
|
|
338
|
+
return 0
|
|
339
|
+
fi
|
|
340
|
+
|
|
341
|
+
find "${run_dir}" -maxdepth 1 -type f -name '*.log' 2>/dev/null | LC_ALL=C sort | tail -n 1
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
infer_issue_runtime_failure_from_log() {
|
|
345
|
+
local log_file=""
|
|
346
|
+
|
|
347
|
+
log_file="$(issue_runtime_log_file)"
|
|
348
|
+
[[ -n "${log_file}" && -f "${log_file}" ]] || return 1
|
|
349
|
+
|
|
350
|
+
if grep -Eiq 'stale-run no-codex-output-before-stall-threshold|no-codex-output-before-stall-threshold' "${log_file}" 2>/dev/null; then
|
|
351
|
+
printf 'no-codex-output-before-stall-threshold\n'
|
|
352
|
+
return 0
|
|
353
|
+
fi
|
|
354
|
+
|
|
355
|
+
if grep -Eiq 'stale-run no-codex-progress-before-stall-threshold|no-codex-progress-before-stall-threshold' "${log_file}" 2>/dev/null; then
|
|
356
|
+
printf 'no-codex-progress-before-stall-threshold\n'
|
|
357
|
+
return 0
|
|
358
|
+
fi
|
|
359
|
+
|
|
360
|
+
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
|
|
361
|
+
printf 'worker-environment-blocked\n'
|
|
362
|
+
return 0
|
|
363
|
+
fi
|
|
364
|
+
|
|
365
|
+
return 1
|
|
366
|
+
}
|
|
367
|
+
|
|
320
368
|
normalize_issue_result_contract() {
|
|
321
369
|
[[ "$status" == "SUCCEEDED" ]] || return 0
|
|
322
370
|
|
|
@@ -691,6 +739,11 @@ classify_issue_publish_blocker() {
|
|
|
691
739
|
return 0
|
|
692
740
|
fi
|
|
693
741
|
|
|
742
|
+
if grep -Fq 'Localization guard blocked branch publication.' <<<"$publish_out"; then
|
|
743
|
+
printf 'localization-guard-blocked\n'
|
|
744
|
+
return 0
|
|
745
|
+
fi
|
|
746
|
+
|
|
694
747
|
if grep -Fq 'has no commits ahead of' <<<"$publish_out"; then
|
|
695
748
|
printf 'no-publishable-commits\n'
|
|
696
749
|
return 0
|
|
@@ -788,6 +841,23 @@ Why it was blocked:
|
|
|
788
841
|
- the verification guard could not confirm the expected checks for this change
|
|
789
842
|
- recurring issue publication should stop rather than open an unverifiable PR
|
|
790
843
|
|
|
844
|
+
\`\`\`text
|
|
845
|
+
${publish_out}
|
|
846
|
+
\`\`\`
|
|
847
|
+
EOF
|
|
848
|
+
return 0
|
|
849
|
+
fi
|
|
850
|
+
|
|
851
|
+
if [[ "${blocker_reason}" == "localization-guard-blocked" ]]; then
|
|
852
|
+
cat <<EOF
|
|
853
|
+
# Blocker: Localization requirements were not satisfied
|
|
854
|
+
|
|
855
|
+
Host publication stopped this cycle because the branch updated locale resources but still left obvious hardcoded user-facing strings in the touched UI files.
|
|
856
|
+
|
|
857
|
+
Why it was blocked:
|
|
858
|
+
- the localization guard found remaining literals that should move behind translation keys
|
|
859
|
+
- recurring issue publication should stop rather than open a partially localized UI change
|
|
860
|
+
|
|
791
861
|
\`\`\`text
|
|
792
862
|
${publish_out}
|
|
793
863
|
\`\`\`
|
|
@@ -810,6 +880,21 @@ build_issue_runtime_blocker_comment() {
|
|
|
810
880
|
|
|
811
881
|
case "${runtime_reason}" in
|
|
812
882
|
provider-quota-limit)
|
|
883
|
+
if [[ "${worker_name}" == "codex" ]]; then
|
|
884
|
+
cat <<EOF
|
|
885
|
+
# Blocker: Provider quota is currently exhausted
|
|
886
|
+
|
|
887
|
+
This recurring run stopped before implementation because the configured ${worker_name} account hit a provider-side usage limit.
|
|
888
|
+
|
|
889
|
+
Why it was blocked:
|
|
890
|
+
- the worker reached the current Codex usage cap for the active account
|
|
891
|
+
- ACP records the quota hit, attempts safe account rotation when available, and then waits for the configured cooldown instead of looping indefinitely
|
|
892
|
+
|
|
893
|
+
Next step:
|
|
894
|
+
- wait for the current quota window to reset, or make another Codex account available to this profile
|
|
895
|
+
EOF
|
|
896
|
+
return 0
|
|
897
|
+
fi
|
|
813
898
|
cat <<EOF
|
|
814
899
|
# Blocker: Provider quota is currently exhausted
|
|
815
900
|
|
|
@@ -821,6 +906,21 @@ Why it was blocked:
|
|
|
821
906
|
|
|
822
907
|
Next step:
|
|
823
908
|
- wait for the current quota window to reset, or switch this profile to another available provider/account
|
|
909
|
+
EOF
|
|
910
|
+
return 0
|
|
911
|
+
;;
|
|
912
|
+
worker-environment-blocked)
|
|
913
|
+
cat <<EOF
|
|
914
|
+
# Blocker: Worker environment failed before a valid result contract was written
|
|
915
|
+
|
|
916
|
+
This recurring run did not produce a usable ACP result file because the ${worker_name} execution environment failed mid-run.
|
|
917
|
+
|
|
918
|
+
Why it was blocked:
|
|
919
|
+
- the worker hit a sandbox/worktree runtime failure before it could write \`result.env\`
|
|
920
|
+
- 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\`
|
|
921
|
+
|
|
922
|
+
Next step:
|
|
923
|
+
- refresh the worker runtime/worktree and rerun this cycle after the host-side environment issue is resolved
|
|
824
924
|
EOF
|
|
825
925
|
return 0
|
|
826
926
|
;;
|
|
@@ -865,12 +965,45 @@ if (explicitFailureReason) {
|
|
|
865
965
|
reason = explicitFailureReason[1];
|
|
866
966
|
} else if (/^# Blocker: Verification requirements were not satisfied$/im.test(body)) {
|
|
867
967
|
reason = 'verification-guard-blocked';
|
|
968
|
+
} else if (/^# Blocker: Localization requirements were not satisfied$/im.test(body)) {
|
|
969
|
+
reason = 'localization-guard-blocked';
|
|
970
|
+
} else if (
|
|
971
|
+
/required (?:issue-contract )?verification does not currently pass/i.test(body) ||
|
|
972
|
+
/Because the required `pnpm typecheck` did not pass/i.test(body) ||
|
|
973
|
+
/- BLOCKED `pnpm typecheck`/i.test(body) ||
|
|
974
|
+
/pnpm typecheck(?:`)? fails in unrelated existing file/i.test(body) ||
|
|
975
|
+
/Blocked on required root verification/i.test(body) ||
|
|
976
|
+
/required root (?:verification command|`pnpm test`)/i.test(body) ||
|
|
977
|
+
/pnpm test` is currently failing outside this/i.test(body) ||
|
|
978
|
+
/The required root test command failed/i.test(body) ||
|
|
979
|
+
/did not commit because the issue contract requires verification to pass/i.test(body)
|
|
980
|
+
) {
|
|
981
|
+
reason = 'verification-guard-blocked';
|
|
868
982
|
} else if (/^# Blocker: (All checklist items already completed|Worker produced no publishable delta)$/im.test(body)) {
|
|
869
983
|
reason = 'no-publishable-commits';
|
|
870
984
|
} else if (/^# Blocker: Change scope was too broad$/im.test(body)) {
|
|
871
985
|
reason = 'scope-guard-blocked';
|
|
872
986
|
} else if (/^# Blocker: Provider quota is currently exhausted$/im.test(body)) {
|
|
873
987
|
reason = 'provider-quota-limit';
|
|
988
|
+
} else if (
|
|
989
|
+
/blocked on external network access/i.test(body) ||
|
|
990
|
+
/could not perform a safe offline bump/i.test(body) ||
|
|
991
|
+
/failed to reach `api\.github\.com`/i.test(body) ||
|
|
992
|
+
/failed with `ENOTFOUND`/i.test(body)
|
|
993
|
+
) {
|
|
994
|
+
reason = 'external-network-access-blocked';
|
|
995
|
+
} else if (
|
|
996
|
+
/I’m blocked on the environment, not the issue scope/i.test(body) ||
|
|
997
|
+
/Every local execution path I need for this cycle is failing immediately with `aborted`/i.test(body) ||
|
|
998
|
+
/outside this session['’]s writable sandbox/i.test(body) ||
|
|
999
|
+
/could not write to the host-required `\$ACP_RUN_DIR`/i.test(body) ||
|
|
1000
|
+
/cannot access local infrastructure from this sandbox/i.test(body) ||
|
|
1001
|
+
/sandbox socket connection errors/i.test(body) ||
|
|
1002
|
+
/connect EPERM 127\.0\.0\.1:6379/i.test(body) ||
|
|
1003
|
+
/local Postgres\/Redis services/i.test(body) ||
|
|
1004
|
+
/worker can(?:not|'t) connect to the local test Postgres and Redis services/i.test(body)
|
|
1005
|
+
) {
|
|
1006
|
+
reason = 'worker-environment-blocked';
|
|
874
1007
|
} else if (/^# Blocker:/im.test(body)) {
|
|
875
1008
|
reason = 'issue-worker-blocked';
|
|
876
1009
|
}
|
|
@@ -998,6 +1131,34 @@ case "$status" in
|
|
|
998
1131
|
SUCCEEDED)
|
|
999
1132
|
clear_provider_quota_cooldown
|
|
1000
1133
|
if ! normalize_issue_result_contract; then
|
|
1134
|
+
inferred_failure_reason="$(infer_issue_runtime_failure_from_log || true)"
|
|
1135
|
+
if [[ -n "${inferred_failure_reason}" ]]; then
|
|
1136
|
+
status="FAILED"
|
|
1137
|
+
failure_reason="$(normalize_issue_failure_reason "${inferred_failure_reason}")"
|
|
1138
|
+
issue_result_contract_note="missing-worker-result-recovered-${failure_reason}"
|
|
1139
|
+
result_outcome="blocked"
|
|
1140
|
+
result_action="host-comment-blocker"
|
|
1141
|
+
normalize_issue_runner_state "failed" "${LAST_EXIT_CODE:-}" "${failure_reason}"
|
|
1142
|
+
if [[ ! -s "${run_dir}/issue-comment.md" ]]; then
|
|
1143
|
+
write_issue_comment_artifact "$(build_issue_runtime_blocker_comment "${failure_reason}")" || true
|
|
1144
|
+
fi
|
|
1145
|
+
post_issue_comment_if_present
|
|
1146
|
+
require_transition "issue_schedule_retry" issue_schedule_retry "$failure_reason"
|
|
1147
|
+
require_transition "issue_mark_ready" issue_mark_ready
|
|
1148
|
+
issue_set_reconcile_summary "$status" "$result_outcome" "$result_action" "$failure_reason"
|
|
1149
|
+
cleanup_issue_session
|
|
1150
|
+
notify_issue_reconciled
|
|
1151
|
+
mark_reconciled
|
|
1152
|
+
printf 'STATUS=%s\n' "$status"
|
|
1153
|
+
printf 'ISSUE_ID=%s\n' "$issue_id"
|
|
1154
|
+
printf 'PR_NUMBER=%s\n' "$pr_number"
|
|
1155
|
+
printf 'OUTCOME=%s\n' "$result_outcome"
|
|
1156
|
+
printf 'ACTION=%s\n' "$result_action"
|
|
1157
|
+
printf 'FAILURE_REASON=%s\n' "$failure_reason"
|
|
1158
|
+
printf 'RESULT_CONTRACT_NOTE=%s\n' "$issue_result_contract_note"
|
|
1159
|
+
exit 0
|
|
1160
|
+
fi
|
|
1161
|
+
|
|
1001
1162
|
status="FAILED"
|
|
1002
1163
|
failure_reason="invalid-result-contract"
|
|
1003
1164
|
issue_result_contract_note="invalid-result-contract"
|
|
@@ -1195,10 +1356,17 @@ case "$status" in
|
|
|
1195
1356
|
notify_issue_reconciled
|
|
1196
1357
|
;;
|
|
1197
1358
|
FAILED)
|
|
1198
|
-
failure_reason="${failure_reason:-worker-exit-failed}"
|
|
1359
|
+
failure_reason="$(normalize_issue_failure_reason "${failure_reason:-worker-exit-failed}")"
|
|
1199
1360
|
schedule_provider_quota_cooldown "${failure_reason}"
|
|
1200
1361
|
normalize_issue_runner_state "failed" "${LAST_EXIT_CODE:-}" "${failure_reason}"
|
|
1201
|
-
if [[ "${result_outcome:-}" == "blocked" && "${result_action:-}" == "host-comment-blocker" ]]
|
|
1362
|
+
if [[ "${result_outcome:-}" == "blocked" && "${result_action:-}" == "host-comment-blocker" ]] \
|
|
1363
|
+
|| [[ "${failure_reason}" == "provider-quota-limit" ]]; then
|
|
1364
|
+
if [[ -z "${result_outcome:-}" ]]; then
|
|
1365
|
+
result_outcome="blocked"
|
|
1366
|
+
fi
|
|
1367
|
+
if [[ -z "${result_action:-}" ]]; then
|
|
1368
|
+
result_action="host-comment-blocker"
|
|
1369
|
+
fi
|
|
1202
1370
|
if [[ ! -s "${run_dir}/issue-comment.md" ]]; then
|
|
1203
1371
|
write_issue_comment_artifact "$(build_issue_runtime_blocker_comment "${failure_reason}")" || true
|
|
1204
1372
|
fi
|