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
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
5
|
+
# shellcheck source=/dev/null
|
|
6
|
+
source "${SCRIPT_DIR}/flow-shell-lib.sh"
|
|
7
|
+
|
|
8
|
+
usage() {
|
|
9
|
+
cat <<'EOF'
|
|
10
|
+
Usage:
|
|
11
|
+
ensure-runtime-sync.sh [--source-home <path>] [--runtime-home <path>] [--force] [--quiet]
|
|
12
|
+
|
|
13
|
+
Detect source/runtime drift for the published agent runtime and run
|
|
14
|
+
`sync-shared-agent-home.sh` only when needed.
|
|
15
|
+
EOF
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
source_home=""
|
|
19
|
+
runtime_home=""
|
|
20
|
+
force_sync="0"
|
|
21
|
+
quiet="0"
|
|
22
|
+
|
|
23
|
+
path_looks_like_skill_alias_root() {
|
|
24
|
+
local candidate="${1:-}"
|
|
25
|
+
local skill_name=""
|
|
26
|
+
|
|
27
|
+
for skill_name in "$(flow_canonical_skill_name)" "$(flow_compat_skill_alias)"; do
|
|
28
|
+
[[ -n "${skill_name}" ]] || continue
|
|
29
|
+
[[ "${candidate}" == */skills/openclaw/"${skill_name}" ]] && return 0
|
|
30
|
+
done
|
|
31
|
+
|
|
32
|
+
return 1
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
read_stamped_source_home() {
|
|
36
|
+
local stamp_path="${1:-}"
|
|
37
|
+
local stamped=""
|
|
38
|
+
|
|
39
|
+
[[ -f "${stamp_path}" ]] || return 0
|
|
40
|
+
stamped="$(awk -F= '/^SOURCE_HOME=/{print $2; exit}' "${stamp_path}" 2>/dev/null | tr -d "[:space:]'\" " || true)"
|
|
41
|
+
[[ -n "${stamped}" ]] || return 0
|
|
42
|
+
printf '%s\n' "${stamped}"
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
while [[ $# -gt 0 ]]; do
|
|
46
|
+
case "$1" in
|
|
47
|
+
--source-home) source_home="${2:-}"; shift 2 ;;
|
|
48
|
+
--runtime-home) runtime_home="${2:-}"; shift 2 ;;
|
|
49
|
+
--force) force_sync="1"; shift ;;
|
|
50
|
+
--quiet) quiet="1"; shift ;;
|
|
51
|
+
--help|-h) usage; exit 0 ;;
|
|
52
|
+
*) echo "Unknown argument: $1" >&2; usage >&2; exit 64 ;;
|
|
53
|
+
esac
|
|
54
|
+
done
|
|
55
|
+
|
|
56
|
+
FLOW_SKILL_DIR="$(resolve_flow_skill_dir "${BASH_SOURCE[0]}")"
|
|
57
|
+
SYNC_SCRIPT="${ACP_RUNTIME_SYNC_SCRIPT:-${AGENT_CONTROL_PLANE_SYNC_SCRIPT:-${SCRIPT_DIR}/sync-shared-agent-home.sh}}"
|
|
58
|
+
|
|
59
|
+
if [[ -z "${runtime_home}" ]]; then
|
|
60
|
+
runtime_home="${ACP_RUNTIME_SYNC_RUNTIME_HOME:-${AGENT_RUNTIME_HOME:-$(resolve_runtime_home)}}"
|
|
61
|
+
fi
|
|
62
|
+
|
|
63
|
+
runtime_home="$(mkdir -p "${runtime_home}" && cd "${runtime_home}" && pwd -P)"
|
|
64
|
+
stamp_file="${runtime_home}/.agent-control-plane-runtime-sync.env"
|
|
65
|
+
|
|
66
|
+
if [[ -z "${source_home}" ]]; then
|
|
67
|
+
source_home="${ACP_RUNTIME_SYNC_SOURCE_HOME:-${AGENT_FLOW_SOURCE_HOME:-}}"
|
|
68
|
+
if [[ -z "${source_home}" ]]; then
|
|
69
|
+
if path_looks_like_skill_alias_root "${FLOW_SKILL_DIR}"; then
|
|
70
|
+
source_home="$(read_stamped_source_home "${stamp_file}")"
|
|
71
|
+
if [[ -z "${source_home}" ]]; then
|
|
72
|
+
source_home="$(resolve_shared_agent_home "${FLOW_SKILL_DIR}")"
|
|
73
|
+
fi
|
|
74
|
+
else
|
|
75
|
+
source_home="${FLOW_SKILL_DIR}"
|
|
76
|
+
fi
|
|
77
|
+
fi
|
|
78
|
+
fi
|
|
79
|
+
|
|
80
|
+
source_home="$(cd "${source_home}" && pwd -P)"
|
|
81
|
+
|
|
82
|
+
resolve_source_skill_dir() {
|
|
83
|
+
local candidate=""
|
|
84
|
+
local skill_name=""
|
|
85
|
+
local root="${1:?source home required}"
|
|
86
|
+
|
|
87
|
+
for skill_name in "$(flow_canonical_skill_name)" "$(flow_compat_skill_alias)"; do
|
|
88
|
+
[[ -n "${skill_name}" ]] || continue
|
|
89
|
+
candidate="${root}/skills/openclaw/${skill_name}"
|
|
90
|
+
if flow_is_skill_root "${candidate}"; then
|
|
91
|
+
printf '%s\n' "${candidate}"
|
|
92
|
+
return 0
|
|
93
|
+
fi
|
|
94
|
+
done
|
|
95
|
+
|
|
96
|
+
if flow_is_skill_root "${root}"; then
|
|
97
|
+
printf '%s\n' "${root}"
|
|
98
|
+
return 0
|
|
99
|
+
fi
|
|
100
|
+
|
|
101
|
+
return 1
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
compute_fingerprint() {
|
|
105
|
+
python3 - "$@" <<'PY'
|
|
106
|
+
import hashlib
|
|
107
|
+
import os
|
|
108
|
+
import sys
|
|
109
|
+
|
|
110
|
+
h = hashlib.sha256()
|
|
111
|
+
|
|
112
|
+
for root in sys.argv[1:]:
|
|
113
|
+
if not root or not os.path.isdir(root):
|
|
114
|
+
continue
|
|
115
|
+
root = os.path.realpath(root)
|
|
116
|
+
h.update(root.encode("utf-8", "surrogateescape"))
|
|
117
|
+
h.update(b"\0")
|
|
118
|
+
for dirpath, dirnames, filenames in os.walk(root):
|
|
119
|
+
dirnames[:] = sorted(d for d in dirnames if d != ".git")
|
|
120
|
+
for name in sorted(filenames):
|
|
121
|
+
path = os.path.join(dirpath, name)
|
|
122
|
+
try:
|
|
123
|
+
stat_result = os.stat(path)
|
|
124
|
+
except OSError:
|
|
125
|
+
continue
|
|
126
|
+
relpath = os.path.relpath(path, root)
|
|
127
|
+
h.update(relpath.encode("utf-8", "surrogateescape"))
|
|
128
|
+
h.update(b"\0")
|
|
129
|
+
h.update(str(stat_result.st_mtime_ns).encode("ascii"))
|
|
130
|
+
h.update(b"\0")
|
|
131
|
+
h.update(str(stat_result.st_size).encode("ascii"))
|
|
132
|
+
h.update(b"\0")
|
|
133
|
+
|
|
134
|
+
print(h.hexdigest())
|
|
135
|
+
PY
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
source_skill_dir="$(resolve_source_skill_dir "${source_home}")"
|
|
139
|
+
source_tools_dir="${source_home}/tools"
|
|
140
|
+
source_quota_manager_dir="${source_home}/skills/openclaw/codex-quota-manager"
|
|
141
|
+
runtime_skill_dir="${runtime_home}/skills/openclaw/$(flow_canonical_skill_name)"
|
|
142
|
+
|
|
143
|
+
source_fingerprint="$(
|
|
144
|
+
compute_fingerprint \
|
|
145
|
+
"${source_tools_dir}" \
|
|
146
|
+
"${source_quota_manager_dir}" \
|
|
147
|
+
"${source_skill_dir}"
|
|
148
|
+
)"
|
|
149
|
+
|
|
150
|
+
existing_fingerprint=""
|
|
151
|
+
if [[ -f "${stamp_file}" ]]; then
|
|
152
|
+
existing_fingerprint="$(awk -F= '/^SOURCE_FINGERPRINT=/{print $2}' "${stamp_file}" 2>/dev/null | tr -d "[:space:]'\"" || true)"
|
|
153
|
+
fi
|
|
154
|
+
|
|
155
|
+
sync_status="unchanged"
|
|
156
|
+
if [[ "${force_sync}" == "1" || ! -d "${runtime_skill_dir}" || "${existing_fingerprint}" != "${source_fingerprint}" ]]; then
|
|
157
|
+
bash "${SYNC_SCRIPT}" "${source_home}" "${runtime_home}" >/dev/null
|
|
158
|
+
sync_status="updated"
|
|
159
|
+
fi
|
|
160
|
+
|
|
161
|
+
updated_at="$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
|
|
162
|
+
tmp_file="${stamp_file}.tmp.$$"
|
|
163
|
+
{
|
|
164
|
+
printf 'SOURCE_HOME=%q\n' "${source_home}"
|
|
165
|
+
printf 'SOURCE_SKILL_DIR=%q\n' "${source_skill_dir}"
|
|
166
|
+
printf 'RUNTIME_HOME=%q\n' "${runtime_home}"
|
|
167
|
+
printf 'RUNTIME_SKILL_DIR=%q\n' "${runtime_skill_dir}"
|
|
168
|
+
printf 'SOURCE_FINGERPRINT=%q\n' "${source_fingerprint}"
|
|
169
|
+
printf 'SYNC_STATUS=%q\n' "${sync_status}"
|
|
170
|
+
printf 'UPDATED_AT=%q\n' "${updated_at}"
|
|
171
|
+
} >"${tmp_file}"
|
|
172
|
+
mv "${tmp_file}" "${stamp_file}"
|
|
173
|
+
|
|
174
|
+
if [[ "${quiet}" != "1" ]]; then
|
|
175
|
+
printf 'SYNC_STATUS=%s\n' "${sync_status}"
|
|
176
|
+
printf 'SOURCE_HOME=%s\n' "${source_home}"
|
|
177
|
+
printf 'SOURCE_SKILL_DIR=%s\n' "${source_skill_dir}"
|
|
178
|
+
printf 'RUNTIME_HOME=%s\n' "${runtime_home}"
|
|
179
|
+
printf 'RUNTIME_SKILL_DIR=%s\n' "${runtime_skill_dir}"
|
|
180
|
+
printf 'SOURCE_FINGERPRINT=%s\n' "${source_fingerprint}"
|
|
181
|
+
printf 'STAMP_FILE=%s\n' "${stamp_file}"
|
|
182
|
+
fi
|
|
@@ -291,6 +291,13 @@ flow_export_github_cli_auth_env() {
|
|
|
291
291
|
return 0
|
|
292
292
|
fi
|
|
293
293
|
|
|
294
|
+
if command -v gh >/dev/null 2>&1; then
|
|
295
|
+
if env -u GH_TOKEN -u GITHUB_TOKEN gh auth status >/dev/null 2>&1 \
|
|
296
|
+
|| env -u GH_TOKEN -u GITHUB_TOKEN gh api user --jq .login >/dev/null 2>&1; then
|
|
297
|
+
return 0
|
|
298
|
+
fi
|
|
299
|
+
fi
|
|
300
|
+
|
|
294
301
|
token="$(flow_git_credential_token_for_repo_slug "${repo_slug}" || true)"
|
|
295
302
|
if [[ -n "${token}" ]]; then
|
|
296
303
|
export GH_TOKEN="${token}"
|
|
@@ -302,6 +309,28 @@ flow_export_github_cli_auth_env() {
|
|
|
302
309
|
fi
|
|
303
310
|
}
|
|
304
311
|
|
|
312
|
+
flow_github_graphql_available() {
|
|
313
|
+
local repo_slug="${1:-}"
|
|
314
|
+
local remaining=""
|
|
315
|
+
|
|
316
|
+
if [[ "${FLOW_GITHUB_GRAPHQL_AVAILABLE_CACHE:-}" == "yes" ]]; then
|
|
317
|
+
return 0
|
|
318
|
+
fi
|
|
319
|
+
if [[ "${FLOW_GITHUB_GRAPHQL_AVAILABLE_CACHE:-}" == "no" ]]; then
|
|
320
|
+
return 1
|
|
321
|
+
fi
|
|
322
|
+
|
|
323
|
+
flow_export_github_cli_auth_env "${repo_slug}"
|
|
324
|
+
remaining="$(gh api rate_limit --jq '.resources.graphql.remaining' 2>/dev/null || true)"
|
|
325
|
+
if [[ "${remaining}" =~ ^[0-9]+$ ]] && (( remaining > 0 )); then
|
|
326
|
+
FLOW_GITHUB_GRAPHQL_AVAILABLE_CACHE="yes"
|
|
327
|
+
return 0
|
|
328
|
+
fi
|
|
329
|
+
|
|
330
|
+
FLOW_GITHUB_GRAPHQL_AVAILABLE_CACHE="no"
|
|
331
|
+
return 1
|
|
332
|
+
}
|
|
333
|
+
|
|
305
334
|
flow_github_repo_id_cache_var() {
|
|
306
335
|
local repo_slug="${1:-}"
|
|
307
336
|
local sanitized="${repo_slug//[^A-Za-z0-9]/_}"
|
|
@@ -455,6 +484,22 @@ flow_github_api_repo() {
|
|
|
455
484
|
return "${status}"
|
|
456
485
|
}
|
|
457
486
|
|
|
487
|
+
flow_json_or_default() {
|
|
488
|
+
local raw_value="${1-}"
|
|
489
|
+
local default_value="${2:-null}"
|
|
490
|
+
|
|
491
|
+
if [[ -z "${raw_value}" ]]; then
|
|
492
|
+
printf '%s\n' "${default_value}"
|
|
493
|
+
return 0
|
|
494
|
+
fi
|
|
495
|
+
|
|
496
|
+
if jq -e . >/dev/null 2>&1 <<<"${raw_value}"; then
|
|
497
|
+
printf '%s\n' "${raw_value}"
|
|
498
|
+
else
|
|
499
|
+
printf '%s\n' "${default_value}"
|
|
500
|
+
fi
|
|
501
|
+
}
|
|
502
|
+
|
|
458
503
|
flow_github_urlencode() {
|
|
459
504
|
local raw_value="${1:-}"
|
|
460
505
|
|
|
@@ -472,15 +517,16 @@ flow_github_issue_view_json() {
|
|
|
472
517
|
local issue_json=""
|
|
473
518
|
local comment_pages_json=""
|
|
474
519
|
|
|
475
|
-
if
|
|
520
|
+
if flow_github_graphql_available "${repo_slug}" \
|
|
521
|
+
&& issue_json="$(gh issue view "${issue_id}" -R "${repo_slug}" --json number,state,title,body,url,labels,comments,createdAt,updatedAt 2>/dev/null)"; then
|
|
476
522
|
printf '%s\n' "${issue_json}"
|
|
477
523
|
return 0
|
|
478
524
|
fi
|
|
479
525
|
|
|
480
|
-
issue_json="$(flow_github_api_repo "${repo_slug}" "issues/${issue_id}"
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
)"
|
|
526
|
+
issue_json="$(flow_github_api_repo "${repo_slug}" "issues/${issue_id}" 2>/dev/null || true)"
|
|
527
|
+
issue_json="$(flow_json_or_default "${issue_json}" '{}')"
|
|
528
|
+
comment_pages_json="$(flow_github_api_repo "${repo_slug}" "issues/${issue_id}/comments?per_page=100" --paginate --slurp 2>/dev/null || true)"
|
|
529
|
+
comment_pages_json="$(flow_json_or_default "${comment_pages_json}" '[]')"
|
|
484
530
|
|
|
485
531
|
ISSUE_JSON="${issue_json}" COMMENT_PAGES_JSON="${comment_pages_json}" python3 - <<'PY'
|
|
486
532
|
import json
|
|
@@ -527,7 +573,8 @@ flow_github_issue_list_json() {
|
|
|
527
573
|
local issues_json=""
|
|
528
574
|
local per_page="100"
|
|
529
575
|
|
|
530
|
-
if
|
|
576
|
+
if flow_github_graphql_available "${repo_slug}" \
|
|
577
|
+
&& issues_json="$(gh issue list -R "${repo_slug}" --state "${state}" --limit "${limit}" --json number,createdAt,updatedAt,title,url,labels 2>/dev/null)"; then
|
|
531
578
|
printf '%s\n' "${issues_json}"
|
|
532
579
|
return 0
|
|
533
580
|
fi
|
|
@@ -536,9 +583,8 @@ flow_github_issue_list_json() {
|
|
|
536
583
|
per_page="${limit}"
|
|
537
584
|
fi
|
|
538
585
|
|
|
539
|
-
issues_json="$(
|
|
540
|
-
|
|
541
|
-
)" || return 1
|
|
586
|
+
issues_json="$(flow_github_api_repo "${repo_slug}" "issues?state=${state}&per_page=${per_page}" --paginate --slurp 2>/dev/null || true)"
|
|
587
|
+
issues_json="$(flow_json_or_default "${issues_json}" '[]')"
|
|
542
588
|
|
|
543
589
|
ISSUE_PAGES_JSON="${issues_json}" ISSUE_LIMIT="${limit}" python3 - <<'PY'
|
|
544
590
|
import json
|
|
@@ -583,16 +629,18 @@ flow_github_pr_view_json() {
|
|
|
583
629
|
local check_runs_json="{}"
|
|
584
630
|
local status_json="{}"
|
|
585
631
|
|
|
586
|
-
if
|
|
632
|
+
if flow_github_graphql_available "${repo_slug}" \
|
|
633
|
+
&& pr_json="$(gh pr view "${pr_number}" -R "${repo_slug}" --json number,title,body,url,headRefName,baseRefName,mergeStateStatus,statusCheckRollup,labels,comments,state,isDraft 2>/dev/null)"; then
|
|
587
634
|
printf '%s\n' "${pr_json}"
|
|
588
635
|
return 0
|
|
589
636
|
fi
|
|
590
637
|
|
|
591
|
-
pr_json="$(flow_github_api_repo "${repo_slug}" "pulls/${pr_number}"
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
)"
|
|
638
|
+
pr_json="$(flow_github_api_repo "${repo_slug}" "pulls/${pr_number}" 2>/dev/null || true)"
|
|
639
|
+
pr_json="$(flow_json_or_default "${pr_json}" '{}')"
|
|
640
|
+
issue_json="$(flow_github_api_repo "${repo_slug}" "issues/${pr_number}" 2>/dev/null || true)"
|
|
641
|
+
issue_json="$(flow_json_or_default "${issue_json}" '{}')"
|
|
642
|
+
comment_pages_json="$(flow_github_api_repo "${repo_slug}" "issues/${pr_number}/comments?per_page=100" --paginate --slurp 2>/dev/null || true)"
|
|
643
|
+
comment_pages_json="$(flow_json_or_default "${comment_pages_json}" '[]')"
|
|
596
644
|
head_sha="$(
|
|
597
645
|
PR_JSON="${pr_json}" python3 - <<'PY'
|
|
598
646
|
import json
|
|
@@ -604,12 +652,10 @@ print(head.get("sha") or "")
|
|
|
604
652
|
PY
|
|
605
653
|
)"
|
|
606
654
|
if [[ -n "${head_sha}" ]]; then
|
|
607
|
-
check_runs_json="$(
|
|
608
|
-
|
|
609
|
-
)"
|
|
610
|
-
status_json="$(
|
|
611
|
-
flow_github_api_repo "${repo_slug}" "commits/${head_sha}/status" 2>/dev/null || printf '{}\n'
|
|
612
|
-
)"
|
|
655
|
+
check_runs_json="$(flow_github_api_repo "${repo_slug}" "commits/${head_sha}/check-runs?per_page=100" 2>/dev/null || true)"
|
|
656
|
+
check_runs_json="$(flow_json_or_default "${check_runs_json}" '{}')"
|
|
657
|
+
status_json="$(flow_github_api_repo "${repo_slug}" "commits/${head_sha}/status" 2>/dev/null || true)"
|
|
658
|
+
status_json="$(flow_json_or_default "${status_json}" '{}')"
|
|
613
659
|
fi
|
|
614
660
|
|
|
615
661
|
PR_JSON="${pr_json}" ISSUE_JSON="${issue_json}" COMMENT_PAGES_JSON="${comment_pages_json}" CHECK_RUNS_JSON="${check_runs_json}" STATUS_JSON="${status_json}" python3 - <<'PY'
|
|
@@ -698,7 +744,8 @@ flow_github_pr_list_json() {
|
|
|
698
744
|
local comment_pages_json=""
|
|
699
745
|
local pr_number=""
|
|
700
746
|
|
|
701
|
-
if
|
|
747
|
+
if flow_github_graphql_available "${repo_slug}" \
|
|
748
|
+
&& pr_json="$(gh pr list -R "${repo_slug}" --state "${state}" --limit "${limit}" --json number,title,body,url,headRefName,labels,comments,createdAt,mergedAt,isDraft 2>/dev/null)"; then
|
|
702
749
|
printf '%s\n' "${pr_json}"
|
|
703
750
|
return 0
|
|
704
751
|
fi
|
|
@@ -710,9 +757,8 @@ flow_github_pr_list_json() {
|
|
|
710
757
|
per_page="${limit}"
|
|
711
758
|
fi
|
|
712
759
|
|
|
713
|
-
pull_pages_json="$(
|
|
714
|
-
|
|
715
|
-
)" || return 1
|
|
760
|
+
pull_pages_json="$(flow_github_api_repo "${repo_slug}" "pulls?state=${pulls_state}&per_page=${per_page}" --paginate --slurp 2>/dev/null || true)"
|
|
761
|
+
pull_pages_json="$(flow_json_or_default "${pull_pages_json}" '[]')"
|
|
716
762
|
|
|
717
763
|
selected_prs_json="$(
|
|
718
764
|
PULL_PAGES_JSON="${pull_pages_json}" PR_LIMIT="${limit}" PR_STATE_FILTER="${state}" python3 - <<'PY'
|
|
@@ -751,7 +797,7 @@ for pr in pulls:
|
|
|
751
797
|
|
|
752
798
|
print(json.dumps(result))
|
|
753
799
|
PY
|
|
754
|
-
)"
|
|
800
|
+
)" || selected_prs_json='[]'
|
|
755
801
|
|
|
756
802
|
item_jsonl_file="$(mktemp)"
|
|
757
803
|
trap 'rm -f "${item_jsonl_file}"' RETURN
|
|
@@ -760,10 +806,10 @@ PY
|
|
|
760
806
|
[[ -n "${current_pr_json}" ]] || continue
|
|
761
807
|
pr_number="$(jq -r '.number // ""' <<<"${current_pr_json}")"
|
|
762
808
|
[[ -n "${pr_number}" ]] || continue
|
|
763
|
-
issue_json="$(flow_github_api_repo "${repo_slug}" "issues/${pr_number}" 2>/dev/null ||
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
)"
|
|
809
|
+
issue_json="$(flow_github_api_repo "${repo_slug}" "issues/${pr_number}" 2>/dev/null || true)"
|
|
810
|
+
issue_json="$(flow_json_or_default "${issue_json}" '{}')"
|
|
811
|
+
comment_pages_json="$(flow_github_api_repo "${repo_slug}" "issues/${pr_number}/comments?per_page=100" --paginate --slurp 2>/dev/null || true)"
|
|
812
|
+
comment_pages_json="$(flow_json_or_default "${comment_pages_json}" '[]')"
|
|
767
813
|
PR_JSON="${current_pr_json}" ISSUE_JSON="${issue_json}" COMMENT_PAGES_JSON="${comment_pages_json}" python3 - <<'PY' >>"${item_jsonl_file}"
|
|
768
814
|
import json
|
|
769
815
|
import os
|
|
@@ -547,14 +547,20 @@ flow_resident_issue_lane_openclaw_agent_id() {
|
|
|
547
547
|
flow_resident_issue_openclaw_session_id() {
|
|
548
548
|
local config_file="${1:-}"
|
|
549
549
|
local issue_id="${2:?issue id required}"
|
|
550
|
+
local task_count="${3:-}"
|
|
550
551
|
local adapter_id=""
|
|
552
|
+
local base_id=""
|
|
551
553
|
|
|
552
554
|
if [[ -z "${config_file}" ]]; then
|
|
553
555
|
config_file="$(resolve_flow_config_yaml "${BASH_SOURCE[1]:-${BASH_SOURCE[0]}}")"
|
|
554
556
|
fi
|
|
555
557
|
|
|
556
558
|
adapter_id="$(flow_resolve_adapter_id "${config_file}")"
|
|
557
|
-
|
|
559
|
+
base_id="${adapter_id}-resident-session-issue-${issue_id}"
|
|
560
|
+
if [[ -n "${task_count}" ]]; then
|
|
561
|
+
base_id="${base_id}-cycle-${task_count}"
|
|
562
|
+
fi
|
|
563
|
+
flow_resident_sanitize_id "${base_id}"
|
|
558
564
|
}
|
|
559
565
|
|
|
560
566
|
flow_resident_issue_lane_openclaw_session_id() {
|
|
@@ -629,6 +635,26 @@ print(int(dt.timestamp()))
|
|
|
629
635
|
PY
|
|
630
636
|
}
|
|
631
637
|
|
|
638
|
+
flow_resident_issue_worktree_is_usable() {
|
|
639
|
+
local worktree="${1:-}"
|
|
640
|
+
local worktree_realpath="${2:-}"
|
|
641
|
+
local resolved_worktree=""
|
|
642
|
+
local resolved_realpath=""
|
|
643
|
+
|
|
644
|
+
[[ -n "${worktree}" && -d "${worktree}" ]] || return 1
|
|
645
|
+
[[ -e "${worktree}/.git" ]] || return 1
|
|
646
|
+
|
|
647
|
+
if [[ -n "${worktree_realpath}" ]]; then
|
|
648
|
+
[[ -d "${worktree_realpath}" && -e "${worktree_realpath}/.git" ]] || return 1
|
|
649
|
+
resolved_worktree="$(cd "${worktree}" 2>/dev/null && pwd -P || true)"
|
|
650
|
+
resolved_realpath="$(cd "${worktree_realpath}" 2>/dev/null && pwd -P || true)"
|
|
651
|
+
[[ -n "${resolved_worktree}" && -n "${resolved_realpath}" ]] || return 1
|
|
652
|
+
[[ "${resolved_worktree}" == "${resolved_realpath}" ]] || return 1
|
|
653
|
+
fi
|
|
654
|
+
|
|
655
|
+
return 0
|
|
656
|
+
}
|
|
657
|
+
|
|
632
658
|
flow_resident_issue_can_reuse() {
|
|
633
659
|
local metadata_file="${1:?metadata file required}"
|
|
634
660
|
local max_tasks="${2:-0}"
|
|
@@ -648,7 +674,7 @@ flow_resident_issue_can_reuse() {
|
|
|
648
674
|
source "${metadata_file}"
|
|
649
675
|
set +a
|
|
650
676
|
|
|
651
|
-
|
|
677
|
+
flow_resident_issue_worktree_is_usable "${WORKTREE:-}" "${WORKTREE_REALPATH:-}" || exit 1
|
|
652
678
|
|
|
653
679
|
task_count="${TASK_COUNT:-0}"
|
|
654
680
|
case "${task_count}" in
|
|
@@ -137,13 +137,28 @@ flow_is_skill_root() {
|
|
|
137
137
|
|
|
138
138
|
flow_print_dir() {
|
|
139
139
|
local candidate="${1:-}"
|
|
140
|
+
local parent_dir=""
|
|
141
|
+
local base_name=""
|
|
140
142
|
[[ -n "${candidate}" ]] || return 1
|
|
141
|
-
|
|
143
|
+
if [[ -e "${candidate}" ]]; then
|
|
144
|
+
(cd "${candidate}" && pwd -P)
|
|
145
|
+
return 0
|
|
146
|
+
fi
|
|
147
|
+
|
|
148
|
+
parent_dir="$(dirname "${candidate}")"
|
|
149
|
+
base_name="$(basename "${candidate}")"
|
|
150
|
+
if [[ -d "${parent_dir}" ]]; then
|
|
151
|
+
printf '%s/%s\n' "$(cd "${parent_dir}" && pwd -P)" "${base_name}"
|
|
152
|
+
return 0
|
|
153
|
+
fi
|
|
154
|
+
|
|
155
|
+
printf '%s\n' "${candidate}"
|
|
142
156
|
}
|
|
143
157
|
|
|
144
158
|
resolve_flow_skill_dir() {
|
|
145
159
|
local script_path="${1:-}"
|
|
146
160
|
local candidate=""
|
|
161
|
+
local search_dir=""
|
|
147
162
|
local skill_name=""
|
|
148
163
|
|
|
149
164
|
for candidate in \
|
|
@@ -158,13 +173,18 @@ resolve_flow_skill_dir() {
|
|
|
158
173
|
done
|
|
159
174
|
|
|
160
175
|
if [[ -n "${script_path}" ]]; then
|
|
161
|
-
|
|
162
|
-
cd "$(dirname "${script_path}")
|
|
163
|
-
)" ||
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
176
|
+
search_dir="$(
|
|
177
|
+
cd "$(dirname "${script_path}")" 2>/dev/null && pwd -P
|
|
178
|
+
)" || search_dir=""
|
|
179
|
+
while [[ -n "${search_dir}" && "${search_dir}" != "/" ]]; do
|
|
180
|
+
if flow_is_skill_root "${search_dir}"; then
|
|
181
|
+
printf '%s\n' "${search_dir}"
|
|
182
|
+
return 0
|
|
183
|
+
fi
|
|
184
|
+
candidate="$(dirname "${search_dir}")"
|
|
185
|
+
[[ "${candidate}" != "${search_dir}" ]] || break
|
|
186
|
+
search_dir="${candidate}"
|
|
187
|
+
done
|
|
168
188
|
fi
|
|
169
189
|
|
|
170
190
|
if [[ -n "${SHARED_AGENT_HOME:-}" ]]; then
|
|
@@ -57,6 +57,7 @@ CODEX_QUOTA_BIN="$(flow_resolve_codex_quota_bin "${FLOW_SKILL_DIR}")"
|
|
|
57
57
|
CODEX_QUOTA_MANAGER_SCRIPT="$(flow_resolve_codex_quota_manager_script "${FLOW_SKILL_DIR}")"
|
|
58
58
|
CODEX_QUOTA_CACHE_DIR="${XDG_CACHE_HOME:-$HOME/.cache}/codex-quota-manager"
|
|
59
59
|
CODEX_QUOTA_FULL_CACHE_FILE="${CODEX_QUOTA_MANAGER_FULL_CACHE_FILE:-${CODEX_QUOTA_CACHE_DIR}/codex-full-quota.json}"
|
|
60
|
+
CODEX_QUOTA_CACHE_MAX_AGE_SECONDS="${CODEX_QUOTA_CACHE_MAX_AGE_SECONDS:-${ACP_CODEX_QUOTA_CACHE_MAX_AGE_SECONDS:-${F_LOSNING_CODEX_QUOTA_CACHE_MAX_AGE_SECONDS:-900}}}"
|
|
60
61
|
DYNAMIC_CONCURRENCY_ENABLED="${ACP_DYNAMIC_CONCURRENCY_ENABLED:-${F_LOSNING_DYNAMIC_CONCURRENCY_ENABLED:-1}}"
|
|
61
62
|
ALLOW_INFRA_CI_BYPASS="${ACP_ALLOW_INFRA_CI_BYPASS:-${F_LOSNING_ALLOW_INFRA_CI_BYPASS:-1}}"
|
|
62
63
|
RETAINED_WORKTREE_AUDIT_ENABLED="${ACP_RETAINED_WORKTREE_AUDIT_ENABLED:-${F_LOSNING_RETAINED_WORKTREE_AUDIT_ENABLED:-1}}"
|
|
@@ -299,6 +300,24 @@ EFFECTIVE_QUOTA_POOLS=""
|
|
|
299
300
|
|
|
300
301
|
printf 'CODEX_QUOTA_ROTATION_STRATEGY=%s\n' "${CODEX_QUOTA_ROTATION_STRATEGY}"
|
|
301
302
|
|
|
303
|
+
local quota_cache_age_seconds=""
|
|
304
|
+
quota_cache_age_seconds="$(
|
|
305
|
+
/opt/homebrew/bin/python3 - "${CODEX_QUOTA_FULL_CACHE_FILE}" <<'PY' 2>/dev/null || true
|
|
306
|
+
import os
|
|
307
|
+
import sys
|
|
308
|
+
import time
|
|
309
|
+
|
|
310
|
+
path = sys.argv[1]
|
|
311
|
+
try:
|
|
312
|
+
stat = os.stat(path)
|
|
313
|
+
except OSError:
|
|
314
|
+
sys.exit(1)
|
|
315
|
+
|
|
316
|
+
age = max(0, int(time.time() - stat.st_mtime))
|
|
317
|
+
print(age)
|
|
318
|
+
PY
|
|
319
|
+
)"
|
|
320
|
+
|
|
302
321
|
if [[ ! -f "${CODEX_QUOTA_FULL_CACHE_FILE}" || ! -s "${CODEX_QUOTA_FULL_CACHE_FILE}" ]] || ! command -v jq >/dev/null 2>&1; then
|
|
303
322
|
printf 'DYNAMIC_CONCURRENCY_QUOTA_MODE=failure-driven-static\n'
|
|
304
323
|
printf 'EFFECTIVE_MAX_CONCURRENT_WORKERS=%s\n' "${EFFECTIVE_MAX_CONCURRENT_WORKERS}"
|
|
@@ -308,6 +327,19 @@ EFFECTIVE_QUOTA_POOLS=""
|
|
|
308
327
|
return 0
|
|
309
328
|
fi
|
|
310
329
|
|
|
330
|
+
if [[ "${CODEX_QUOTA_CACHE_MAX_AGE_SECONDS}" =~ ^[0-9]+$ ]] \
|
|
331
|
+
&& [[ "${quota_cache_age_seconds:-}" =~ ^[0-9]+$ ]] \
|
|
332
|
+
&& (( quota_cache_age_seconds > CODEX_QUOTA_CACHE_MAX_AGE_SECONDS )); then
|
|
333
|
+
printf 'DYNAMIC_CONCURRENCY_QUOTA_MODE=failure-driven-static\n'
|
|
334
|
+
printf 'DYNAMIC_CONCURRENCY_QUOTA_CACHE_STALE=yes\n'
|
|
335
|
+
printf 'DYNAMIC_CONCURRENCY_QUOTA_CACHE_AGE_SECONDS=%s\n' "${quota_cache_age_seconds}"
|
|
336
|
+
printf 'EFFECTIVE_MAX_CONCURRENT_WORKERS=%s\n' "${EFFECTIVE_MAX_CONCURRENT_WORKERS}"
|
|
337
|
+
printf 'EFFECTIVE_MAX_CONCURRENT_PR_WORKERS=%s\n' "${EFFECTIVE_MAX_CONCURRENT_PR_WORKERS}"
|
|
338
|
+
printf 'EFFECTIVE_MAX_RECURRING_ISSUE_WORKERS=%s\n' "${EFFECTIVE_MAX_RECURRING_ISSUE_WORKERS}"
|
|
339
|
+
printf 'EFFECTIVE_MAX_LAUNCHES_PER_HEARTBEAT=%s\n' "${EFFECTIVE_MAX_LAUNCHES_PER_HEARTBEAT}"
|
|
340
|
+
return 0
|
|
341
|
+
fi
|
|
342
|
+
|
|
311
343
|
local healthy_pools=""
|
|
312
344
|
local rotation_pools=""
|
|
313
345
|
local effective_pools=""
|