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.
Files changed (48) hide show
  1. package/bin/pr-risk.sh +54 -10
  2. package/hooks/heartbeat-hooks.sh +166 -13
  3. package/package.json +8 -2
  4. package/references/commands.md +1 -0
  5. package/tools/bin/agent-project-cleanup-session +143 -2
  6. package/tools/bin/agent-project-heartbeat-loop +29 -2
  7. package/tools/bin/agent-project-publish-issue-pr +178 -62
  8. package/tools/bin/agent-project-reconcile-issue-session +230 -5
  9. package/tools/bin/agent-project-reconcile-pr-session +104 -13
  10. package/tools/bin/agent-project-run-claude-session +19 -1
  11. package/tools/bin/agent-project-run-codex-resilient +121 -16
  12. package/tools/bin/agent-project-run-codex-session +61 -11
  13. package/tools/bin/agent-project-run-openclaw-session +274 -7
  14. package/tools/bin/agent-project-sync-anchor-repo +13 -2
  15. package/tools/bin/agent-project-worker-status +19 -14
  16. package/tools/bin/cleanup-worktree.sh +4 -1
  17. package/tools/bin/dashboard-launchd-bootstrap.sh +16 -4
  18. package/tools/bin/ensure-runtime-sync.sh +182 -0
  19. package/tools/bin/flow-config-lib.sh +76 -30
  20. package/tools/bin/flow-resident-worker-lib.sh +28 -2
  21. package/tools/bin/flow-shell-lib.sh +28 -8
  22. package/tools/bin/heartbeat-safe-auto.sh +32 -0
  23. package/tools/bin/issue-publish-localization-guard.sh +142 -0
  24. package/tools/bin/prepare-worktree.sh +3 -1
  25. package/tools/bin/project-launchd-bootstrap.sh +17 -4
  26. package/tools/bin/project-runtime-supervisor.sh +7 -1
  27. package/tools/bin/project-runtimectl.sh +78 -15
  28. package/tools/bin/provider-cooldown-state.sh +1 -1
  29. package/tools/bin/render-flow-config.sh +16 -1
  30. package/tools/bin/reuse-issue-worktree.sh +46 -0
  31. package/tools/bin/run-codex-task.sh +2 -2
  32. package/tools/bin/scaffold-profile.sh +2 -2
  33. package/tools/bin/start-issue-worker.sh +118 -16
  34. package/tools/bin/start-resident-issue-loop.sh +1 -0
  35. package/tools/bin/sync-shared-agent-home.sh +26 -0
  36. package/tools/bin/test-smoke.sh +6 -1
  37. package/tools/dashboard/app.js +91 -3
  38. package/tools/dashboard/dashboard_snapshot.py +119 -0
  39. package/tools/dashboard/styles.css +43 -0
  40. package/tools/templates/issue-prompt-template.md +18 -66
  41. package/tools/templates/legacy/issue-prompt-template-pre-slim.md +109 -0
  42. package/bin/audit-issue-routing.sh +0 -74
  43. package/tools/bin/audit-agent-worktrees.sh +0 -310
  44. package/tools/bin/audit-issue-routing.sh +0 -11
  45. package/tools/bin/audit-retained-layout.sh +0 -58
  46. package/tools/bin/audit-retained-overlap.sh +0 -135
  47. package/tools/bin/audit-retained-worktrees.sh +0 -228
  48. 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 issue_json="$(gh issue view "${issue_id}" -R "${repo_slug}" --json number,state,title,body,url,labels,comments,createdAt,updatedAt 2>/dev/null)"; then
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}")" || return 1
481
- comment_pages_json="$(
482
- flow_github_api_repo "${repo_slug}" "issues/${issue_id}/comments?per_page=100" --paginate --slurp 2>/dev/null || printf '[]\n'
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 issues_json="$(gh issue list -R "${repo_slug}" --state "${state}" --limit "${limit}" --json number,createdAt,updatedAt,title,url,labels 2>/dev/null)"; then
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
- flow_github_api_repo "${repo_slug}" "issues?state=${state}&per_page=${per_page}" --paginate --slurp
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 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
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}")" || return 1
592
- issue_json="$(flow_github_api_repo "${repo_slug}" "issues/${pr_number}")" || return 1
593
- comment_pages_json="$(
594
- flow_github_api_repo "${repo_slug}" "issues/${pr_number}/comments?per_page=100" --paginate --slurp 2>/dev/null || printf '[]\n'
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
- flow_github_api_repo "${repo_slug}" "commits/${head_sha}/check-runs?per_page=100" 2>/dev/null || printf '{}\n'
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 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
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
- flow_github_api_repo "${repo_slug}" "pulls?state=${pulls_state}&per_page=${per_page}" --paginate --slurp
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 || printf '{}\n')"
764
- comment_pages_json="$(
765
- flow_github_api_repo "${repo_slug}" "issues/${pr_number}/comments?per_page=100" --paginate --slurp 2>/dev/null || printf '[]\n'
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
- flow_resident_sanitize_id "${adapter_id}-resident-session-issue-${issue_id}"
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
- [[ -n "${WORKTREE:-}" && -d "${WORKTREE:-}" && -e "${WORKTREE:-}/.git" ]] || exit 1
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
- (cd "${candidate}" && pwd -P)
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
- candidate="$(
162
- cd "$(dirname "${script_path}")/../.." 2>/dev/null && pwd -P
163
- )" || candidate=""
164
- if flow_is_skill_root "${candidate}"; then
165
- printf '%s\n' "${candidate}"
166
- return 0
167
- fi
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=""