agent-control-plane 0.1.8 → 0.1.12
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/bin/pr-risk.sh +54 -10
- package/hooks/heartbeat-hooks.sh +166 -13
- package/package.json +8 -2
- package/references/commands.md +1 -0
- package/tools/bin/agent-project-cleanup-session +143 -2
- package/tools/bin/agent-project-heartbeat-loop +29 -2
- package/tools/bin/agent-project-publish-issue-pr +178 -62
- package/tools/bin/agent-project-reconcile-issue-session +230 -5
- package/tools/bin/agent-project-reconcile-pr-session +104 -13
- package/tools/bin/agent-project-run-claude-session +19 -1
- package/tools/bin/agent-project-run-codex-resilient +121 -16
- package/tools/bin/agent-project-run-codex-session +61 -11
- package/tools/bin/agent-project-run-openclaw-session +274 -7
- package/tools/bin/agent-project-sync-anchor-repo +13 -2
- package/tools/bin/agent-project-worker-status +19 -14
- 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 +28 -8
- package/tools/bin/heartbeat-safe-auto.sh +32 -0
- package/tools/bin/issue-publish-localization-guard.sh +142 -0
- package/tools/bin/prepare-worktree.sh +3 -1
- 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/provider-cooldown-state.sh +1 -1
- package/tools/bin/render-flow-config.sh +16 -1
- package/tools/bin/reuse-issue-worktree.sh +46 -0
- package/tools/bin/run-codex-task.sh +2 -2
- package/tools/bin/scaffold-profile.sh +2 -2
- package/tools/bin/start-issue-worker.sh +118 -16
- package/tools/bin/start-resident-issue-loop.sh +1 -0
- package/tools/bin/sync-shared-agent-home.sh +26 -0
- package/tools/bin/test-smoke.sh +6 -1
- package/tools/dashboard/app.js +91 -3
- package/tools/dashboard/dashboard_snapshot.py +119 -0
- package/tools/dashboard/styles.css +43 -0
- package/tools/templates/issue-prompt-template.md +18 -66
- 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}" \
|
|
@@ -202,6 +202,8 @@ set +a
|
|
|
202
202
|
|
|
203
203
|
result_outcome=""
|
|
204
204
|
result_action=""
|
|
205
|
+
run_started_at="${STARTED_AT:-}"
|
|
206
|
+
expected_run_started_at="${ACP_EXPECTED_RUN_STARTED_AT:-${F_LOSNING_EXPECTED_RUN_STARTED_AT:-}}"
|
|
205
207
|
result_file_candidate="${run_dir}/result.env"
|
|
206
208
|
if [[ ! -f "$result_file_candidate" && -n "${RESULT_FILE:-}" && -f "${RESULT_FILE:-}" ]]; then
|
|
207
209
|
result_file_candidate="${RESULT_FILE}"
|
|
@@ -215,6 +217,14 @@ if [[ -f "$result_file_candidate" ]]; then
|
|
|
215
217
|
result_action="${ACTION:-}"
|
|
216
218
|
fi
|
|
217
219
|
|
|
220
|
+
if [[ -n "${expected_run_started_at}" && "${expected_run_started_at}" != "${run_started_at}" ]]; then
|
|
221
|
+
printf 'STATUS=STALE-RUN-SKIPPED\n'
|
|
222
|
+
printf 'SESSION=%s\n' "$session"
|
|
223
|
+
printf 'EXPECTED_STARTED_AT=%s\n' "${expected_run_started_at}"
|
|
224
|
+
printf 'ACTUAL_STARTED_AT=%s\n' "${run_started_at}"
|
|
225
|
+
exit 0
|
|
226
|
+
fi
|
|
227
|
+
|
|
218
228
|
issue_summary_outcome=""
|
|
219
229
|
issue_summary_action=""
|
|
220
230
|
issue_summary_failure_reason=""
|
|
@@ -295,7 +305,7 @@ provider_cooldown_script="${shared_tools_dir}/provider-cooldown-state.sh"
|
|
|
295
305
|
|
|
296
306
|
schedule_provider_quota_cooldown() {
|
|
297
307
|
local reason="${1:-provider-quota-limit}"
|
|
298
|
-
[[ "${
|
|
308
|
+
[[ "${reason}" == "provider-quota-limit" ]] || return 0
|
|
299
309
|
[[ -x "${provider_cooldown_script}" ]] || return 0
|
|
300
310
|
|
|
301
311
|
"${provider_cooldown_script}" schedule "${reason}" >/dev/null || true
|
|
@@ -307,6 +317,54 @@ clear_provider_quota_cooldown() {
|
|
|
307
317
|
"${provider_cooldown_script}" clear >/dev/null || true
|
|
308
318
|
}
|
|
309
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
|
+
|
|
310
368
|
normalize_issue_result_contract() {
|
|
311
369
|
[[ "$status" == "SUCCEEDED" ]] || return 0
|
|
312
370
|
|
|
@@ -681,6 +739,11 @@ classify_issue_publish_blocker() {
|
|
|
681
739
|
return 0
|
|
682
740
|
fi
|
|
683
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
|
+
|
|
684
747
|
if grep -Fq 'has no commits ahead of' <<<"$publish_out"; then
|
|
685
748
|
printf 'no-publishable-commits\n'
|
|
686
749
|
return 0
|
|
@@ -778,6 +841,23 @@ Why it was blocked:
|
|
|
778
841
|
- the verification guard could not confirm the expected checks for this change
|
|
779
842
|
- recurring issue publication should stop rather than open an unverifiable PR
|
|
780
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
|
+
|
|
781
861
|
\`\`\`text
|
|
782
862
|
${publish_out}
|
|
783
863
|
\`\`\`
|
|
@@ -800,6 +880,21 @@ build_issue_runtime_blocker_comment() {
|
|
|
800
880
|
|
|
801
881
|
case "${runtime_reason}" in
|
|
802
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
|
|
803
898
|
cat <<EOF
|
|
804
899
|
# Blocker: Provider quota is currently exhausted
|
|
805
900
|
|
|
@@ -811,6 +906,21 @@ Why it was blocked:
|
|
|
811
906
|
|
|
812
907
|
Next step:
|
|
813
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
|
|
814
924
|
EOF
|
|
815
925
|
return 0
|
|
816
926
|
;;
|
|
@@ -829,6 +939,79 @@ Next step:
|
|
|
829
939
|
EOF
|
|
830
940
|
}
|
|
831
941
|
|
|
942
|
+
infer_issue_blocked_failure_reason() {
|
|
943
|
+
local comment_file="${run_dir}/issue-comment.md"
|
|
944
|
+
local current_reason="${1:-}"
|
|
945
|
+
|
|
946
|
+
if [[ -n "${current_reason:-}" && "${current_reason}" != "issue-worker-blocked" ]]; then
|
|
947
|
+
printf '%s\n' "${current_reason}"
|
|
948
|
+
return 0
|
|
949
|
+
fi
|
|
950
|
+
|
|
951
|
+
[[ -s "${comment_file}" ]] || {
|
|
952
|
+
printf 'issue-worker-blocked\n'
|
|
953
|
+
return 0
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
ISSUE_COMMENT_FILE="${comment_file}" node <<'EOF'
|
|
957
|
+
const fs = require('fs');
|
|
958
|
+
|
|
959
|
+
const path = process.env.ISSUE_COMMENT_FILE || '';
|
|
960
|
+
const body = path ? fs.readFileSync(path, 'utf8') : '';
|
|
961
|
+
let reason = '';
|
|
962
|
+
|
|
963
|
+
const explicitFailureReason = body.match(/Failure reason:\s*[\r\n]+-\s*`([^`]+)`/i);
|
|
964
|
+
if (explicitFailureReason) {
|
|
965
|
+
reason = explicitFailureReason[1];
|
|
966
|
+
} else if (/^# Blocker: Verification requirements were not satisfied$/im.test(body)) {
|
|
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';
|
|
982
|
+
} else if (/^# Blocker: (All checklist items already completed|Worker produced no publishable delta)$/im.test(body)) {
|
|
983
|
+
reason = 'no-publishable-commits';
|
|
984
|
+
} else if (/^# Blocker: Change scope was too broad$/im.test(body)) {
|
|
985
|
+
reason = 'scope-guard-blocked';
|
|
986
|
+
} else if (/^# Blocker: Provider quota is currently exhausted$/im.test(body)) {
|
|
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';
|
|
1007
|
+
} else if (/^# Blocker:/im.test(body)) {
|
|
1008
|
+
reason = 'issue-worker-blocked';
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
process.stdout.write(`${reason || 'issue-worker-blocked'}\n`);
|
|
1012
|
+
EOF
|
|
1013
|
+
}
|
|
1014
|
+
|
|
832
1015
|
extract_recovery_worktree_from_publish_output() {
|
|
833
1016
|
local publish_out="${1:-}"
|
|
834
1017
|
awk -F= '/^RECOVERY_WORKTREE=/{print $2}' <<<"$publish_out" | tail -n 1
|
|
@@ -844,8 +1027,15 @@ require_transition() {
|
|
|
844
1027
|
}
|
|
845
1028
|
|
|
846
1029
|
mark_reconciled() {
|
|
1030
|
+
local reconciled_at tmp_file
|
|
847
1031
|
if [[ -d "$run_dir" ]]; then
|
|
848
|
-
|
|
1032
|
+
reconciled_at="$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
|
|
1033
|
+
tmp_file="${run_dir}/reconciled.ok.tmp.$$"
|
|
1034
|
+
{
|
|
1035
|
+
printf 'STARTED_AT=%s\n' "${run_started_at}"
|
|
1036
|
+
printf 'RECONCILED_AT=%s\n' "${reconciled_at}"
|
|
1037
|
+
} >"${tmp_file}"
|
|
1038
|
+
mv "${tmp_file}" "${run_dir}/reconciled.ok"
|
|
849
1039
|
fi
|
|
850
1040
|
}
|
|
851
1041
|
|
|
@@ -941,6 +1131,34 @@ case "$status" in
|
|
|
941
1131
|
SUCCEEDED)
|
|
942
1132
|
clear_provider_quota_cooldown
|
|
943
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
|
+
|
|
944
1162
|
status="FAILED"
|
|
945
1163
|
failure_reason="invalid-result-contract"
|
|
946
1164
|
issue_result_contract_note="invalid-result-contract"
|
|
@@ -984,7 +1202,7 @@ case "$status" in
|
|
|
984
1202
|
printf 'ACTION=%s\n' "$result_action"
|
|
985
1203
|
exit 0
|
|
986
1204
|
fi
|
|
987
|
-
failure_reason="
|
|
1205
|
+
failure_reason="$(infer_issue_blocked_failure_reason "${failure_reason:-}")"
|
|
988
1206
|
normalize_issue_runner_state "succeeded" "0" ""
|
|
989
1207
|
require_transition "issue_schedule_retry" issue_schedule_retry "$failure_reason"
|
|
990
1208
|
require_transition "issue_mark_blocked" issue_mark_blocked
|
|
@@ -1138,10 +1356,17 @@ case "$status" in
|
|
|
1138
1356
|
notify_issue_reconciled
|
|
1139
1357
|
;;
|
|
1140
1358
|
FAILED)
|
|
1141
|
-
failure_reason="${failure_reason:-worker-exit-failed}"
|
|
1359
|
+
failure_reason="$(normalize_issue_failure_reason "${failure_reason:-worker-exit-failed}")"
|
|
1142
1360
|
schedule_provider_quota_cooldown "${failure_reason}"
|
|
1143
1361
|
normalize_issue_runner_state "failed" "${LAST_EXIT_CODE:-}" "${failure_reason}"
|
|
1144
|
-
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
|
|
1145
1370
|
if [[ ! -s "${run_dir}/issue-comment.md" ]]; then
|
|
1146
1371
|
write_issue_comment_artifact "$(build_issue_runtime_blocker_comment "${failure_reason}")" || true
|
|
1147
1372
|
fi
|