agent-control-plane 0.3.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (106) hide show
  1. package/README.md +141 -28
  2. package/assets/workflow-catalog.json +1 -1
  3. package/bin/pr-risk.sh +22 -7
  4. package/bin/sync-pr-labels.sh +1 -1
  5. package/hooks/heartbeat-hooks.sh +125 -12
  6. package/hooks/issue-reconcile-hooks.sh +1 -1
  7. package/hooks/pr-reconcile-hooks.sh +1 -1
  8. package/npm/bin/agent-control-plane.js +257 -59
  9. package/package.json +39 -32
  10. package/tools/bin/debug-session.sh +106 -0
  11. package/tools/bin/flow-config-lib.sh +1203 -60
  12. package/tools/bin/flow-runtime-doctor-linux.sh +136 -0
  13. package/tools/bin/flow-runtime-doctor.sh +5 -1
  14. package/tools/bin/flow-shell-lib.sh +32 -0
  15. package/tools/bin/github-core-rate-limit-state.sh +77 -0
  16. package/tools/bin/github-write-outbox.sh +470 -0
  17. package/tools/bin/heartbeat-loop-scheduling-lib.sh +7 -7
  18. package/tools/bin/heartbeat-safe-auto.sh +42 -0
  19. package/tools/bin/install-project-launchd.sh +17 -2
  20. package/tools/bin/install-project-systemd.sh +255 -0
  21. package/tools/bin/project-init.sh +21 -1
  22. package/tools/bin/project-launchd-bootstrap.sh +5 -1
  23. package/tools/bin/project-runtimectl.sh +91 -2
  24. package/tools/bin/project-systemd-bootstrap.sh +74 -0
  25. package/tools/bin/scaffold-profile.sh +61 -3
  26. package/tools/bin/uninstall-project-systemd.sh +87 -0
  27. package/tools/dashboard/app.js +228 -6
  28. package/tools/dashboard/dashboard_snapshot.py +55 -0
  29. package/tools/dashboard/issue_queue_state.py +101 -0
  30. package/tools/dashboard/server.py +123 -1
  31. package/tools/dashboard/styles.css +526 -455
  32. package/tools/templates/pr-fix-template.md +3 -1
  33. package/tools/templates/pr-merge-repair-template.md +2 -1
  34. package/references/architecture.md +0 -217
  35. package/references/commands.md +0 -128
  36. package/references/control-plane-map.md +0 -124
  37. package/references/docs-map.md +0 -73
  38. package/references/release-checklist.md +0 -65
  39. package/references/repo-map.md +0 -36
  40. package/tools/bin/agent-cleanup-worktree +0 -247
  41. package/tools/bin/agent-github-update-labels +0 -71
  42. package/tools/bin/agent-init-worktree +0 -216
  43. package/tools/bin/agent-project-archive-run +0 -52
  44. package/tools/bin/agent-project-capture-worker +0 -46
  45. package/tools/bin/agent-project-catch-up-issue-pr-links +0 -118
  46. package/tools/bin/agent-project-catch-up-merged-prs +0 -194
  47. package/tools/bin/agent-project-catch-up-scheduled-issue-retries +0 -123
  48. package/tools/bin/agent-project-cleanup-session +0 -513
  49. package/tools/bin/agent-project-detached-launch +0 -127
  50. package/tools/bin/agent-project-heartbeat-loop +0 -1029
  51. package/tools/bin/agent-project-open-issue-worktree +0 -89
  52. package/tools/bin/agent-project-open-pr-worktree +0 -80
  53. package/tools/bin/agent-project-publish-issue-pr +0 -465
  54. package/tools/bin/agent-project-reconcile-issue-session +0 -1398
  55. package/tools/bin/agent-project-reconcile-pr-session +0 -1230
  56. package/tools/bin/agent-project-retry-state +0 -147
  57. package/tools/bin/agent-project-run-claude-session +0 -805
  58. package/tools/bin/agent-project-run-codex-resilient +0 -955
  59. package/tools/bin/agent-project-run-codex-session +0 -435
  60. package/tools/bin/agent-project-run-kilo-session +0 -369
  61. package/tools/bin/agent-project-run-ollama-session +0 -658
  62. package/tools/bin/agent-project-run-openclaw-session +0 -1309
  63. package/tools/bin/agent-project-run-opencode-session +0 -377
  64. package/tools/bin/agent-project-run-pi-session +0 -479
  65. package/tools/bin/agent-project-sync-anchor-repo +0 -139
  66. package/tools/bin/agent-project-worker-status +0 -188
  67. package/tools/bin/branch-verification-guard.sh +0 -364
  68. package/tools/bin/capture-worker.sh +0 -18
  69. package/tools/bin/cleanup-worktree.sh +0 -52
  70. package/tools/bin/codex-quota +0 -31
  71. package/tools/bin/create-follow-up-issue.sh +0 -114
  72. package/tools/bin/dashboard-launchd-bootstrap.sh +0 -50
  73. package/tools/bin/issue-publish-localization-guard.sh +0 -142
  74. package/tools/bin/issue-publish-scope-guard.sh +0 -242
  75. package/tools/bin/issue-requires-local-workspace-install.sh +0 -31
  76. package/tools/bin/issue-resource-class.sh +0 -12
  77. package/tools/bin/kick-scheduler.sh +0 -75
  78. package/tools/bin/label-follow-up-issues.sh +0 -14
  79. package/tools/bin/new-pr-worktree.sh +0 -50
  80. package/tools/bin/new-worktree.sh +0 -49
  81. package/tools/bin/pr-risk.sh +0 -12
  82. package/tools/bin/prepare-worktree.sh +0 -142
  83. package/tools/bin/provider-cooldown-state.sh +0 -204
  84. package/tools/bin/publish-issue-worker.sh +0 -31
  85. package/tools/bin/reconcile-bootstrap-lib.sh +0 -113
  86. package/tools/bin/reconcile-issue-worker.sh +0 -34
  87. package/tools/bin/reconcile-pr-worker.sh +0 -34
  88. package/tools/bin/record-verification.sh +0 -71
  89. package/tools/bin/render-flow-config.sh +0 -98
  90. package/tools/bin/resident-issue-controller-lib.sh +0 -448
  91. package/tools/bin/resident-issue-queue-status.py +0 -35
  92. package/tools/bin/retry-state.sh +0 -31
  93. package/tools/bin/reuse-issue-worktree.sh +0 -121
  94. package/tools/bin/run-codex-bypass.sh +0 -3
  95. package/tools/bin/run-codex-safe.sh +0 -3
  96. package/tools/bin/run-codex-task.sh +0 -280
  97. package/tools/bin/serve-dashboard.sh +0 -5
  98. package/tools/bin/split-retained-slice.sh +0 -124
  99. package/tools/bin/start-issue-worker.sh +0 -943
  100. package/tools/bin/start-pr-fix-worker.sh +0 -491
  101. package/tools/bin/start-pr-merge-repair-worker.sh +0 -8
  102. package/tools/bin/start-pr-review-worker.sh +0 -261
  103. package/tools/bin/start-resident-issue-loop.sh +0 -499
  104. package/tools/bin/update-github-labels.sh +0 -14
  105. package/tools/bin/worker-status.sh +0 -19
  106. package/tools/bin/workflow-catalog.sh +0 -77
@@ -0,0 +1,470 @@
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-config-lib.sh"
7
+
8
+ usage() {
9
+ cat <<'EOF'
10
+ Usage:
11
+ github-write-outbox.sh enqueue-labels --repo-slug <owner/repo> --number <id> [--add LABEL]... [--remove LABEL]...
12
+ github-write-outbox.sh enqueue-comment --repo-slug <owner/repo> --number <id> --kind issue|pr --body-file <path>
13
+ github-write-outbox.sh enqueue-approval --repo-slug <owner/repo> --number <id> [--body <text>]
14
+ github-write-outbox.sh flush [--limit <n>]
15
+
16
+ Persist GitHub write intents locally so ACP can continue operating while GitHub
17
+ is unavailable or rate-limited.
18
+ EOF
19
+ }
20
+
21
+ CONFIG_YAML="$(resolve_flow_config_yaml "${BASH_SOURCE[0]}")"
22
+ STATE_ROOT="$(flow_resolve_state_root "${CONFIG_YAML}")"
23
+ OUTBOX_ROOT="${ACP_GITHUB_OUTBOX_ROOT:-${STATE_ROOT}/github-outbox}"
24
+ PENDING_DIR="${OUTBOX_ROOT}/pending"
25
+ SENT_DIR="${OUTBOX_ROOT}/sent"
26
+ FAILED_DIR="${OUTBOX_ROOT}/failed"
27
+ PYTHON_BIN="$(flow_resolve_python_bin || true)"
28
+ ACTION="${1:-}"
29
+ DEFAULT_APPROVAL_BODY="Automated final review passed. Safe low-risk scope, green checks, and host-side merge approved."
30
+
31
+ mkdir -p "${PENDING_DIR}" "${SENT_DIR}" "${FAILED_DIR}"
32
+
33
+ json_hash() {
34
+ local payload="${1:-}"
35
+
36
+ if command -v shasum >/dev/null 2>&1; then
37
+ printf '%s' "${payload}" | shasum -a 256 | awk '{print $1}'
38
+ return 0
39
+ fi
40
+
41
+ if command -v sha256sum >/dev/null 2>&1; then
42
+ printf '%s' "${payload}" | sha256sum | awk '{print $1}'
43
+ return 0
44
+ fi
45
+
46
+ if [[ -n "${PYTHON_BIN:-}" ]]; then
47
+ PAYLOAD="${payload}" "${PYTHON_BIN}" - <<'PY'
48
+ import hashlib
49
+ import os
50
+
51
+ print(hashlib.sha256((os.environ.get("PAYLOAD", "")).encode("utf-8")).hexdigest())
52
+ PY
53
+ return 0
54
+ fi
55
+
56
+ return 1
57
+ }
58
+
59
+ outbox_move_sent() {
60
+ local intent_file="${1:?intent file required}"
61
+ mv "${intent_file}" "${SENT_DIR}/$(basename "${intent_file}")"
62
+ }
63
+
64
+ outbox_move_failed() {
65
+ local intent_file="${1:?intent file required}"
66
+ mv "${intent_file}" "${FAILED_DIR}/$(basename "${intent_file}")"
67
+ }
68
+
69
+ enqueue_labels() {
70
+ local repo_slug=""
71
+ local number=""
72
+ local add_file=""
73
+ local remove_file=""
74
+ local add_json="[]"
75
+ local remove_json="[]"
76
+ local created_at=""
77
+ local payload=""
78
+ local digest=""
79
+ local intent_file=""
80
+
81
+ add_file="$(mktemp)"
82
+ remove_file="$(mktemp)"
83
+ trap 'rm -f "${add_file}" "${remove_file}"' RETURN
84
+
85
+ shift
86
+ while [[ $# -gt 0 ]]; do
87
+ case "$1" in
88
+ --repo-slug) repo_slug="${2:-}"; shift 2 ;;
89
+ --number) number="${2:-}"; shift 2 ;;
90
+ --add)
91
+ printf '%s\n' "${2:?missing label after --add}" >>"${add_file}"
92
+ shift 2
93
+ ;;
94
+ --remove)
95
+ printf '%s\n' "${2:?missing label after --remove}" >>"${remove_file}"
96
+ shift 2
97
+ ;;
98
+ --help|-h) usage; exit 0 ;;
99
+ *)
100
+ echo "unknown argument: $1" >&2
101
+ exit 1
102
+ ;;
103
+ esac
104
+ done
105
+
106
+ [[ -n "${repo_slug}" && -n "${number}" ]] || {
107
+ usage >&2
108
+ exit 1
109
+ }
110
+
111
+ add_json="$(jq -R . <"${add_file}" | jq -s 'map(select(length > 0)) | unique')"
112
+ remove_json="$(jq -R . <"${remove_file}" | jq -s 'map(select(length > 0)) | unique')"
113
+ created_at="$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
114
+ payload="$(
115
+ jq -cn \
116
+ --arg type "labels" \
117
+ --arg repo_slug "${repo_slug}" \
118
+ --arg number "${number}" \
119
+ --arg created_at "${created_at}" \
120
+ --argjson add "${add_json}" \
121
+ --argjson remove "${remove_json}" \
122
+ '{
123
+ type: $type,
124
+ repo_slug: $repo_slug,
125
+ number: $number,
126
+ created_at: $created_at,
127
+ add: $add,
128
+ remove: $remove
129
+ }'
130
+ )"
131
+ digest="$(json_hash "${payload}")"
132
+ intent_file="${PENDING_DIR}/labels-${number}-${digest}.json"
133
+ if [[ ! -f "${intent_file}" ]]; then
134
+ printf '%s\n' "${payload}" >"${intent_file}"
135
+ fi
136
+ printf 'OUTBOX_FILE=%s\n' "${intent_file}"
137
+ }
138
+
139
+ enqueue_comment() {
140
+ local repo_slug=""
141
+ local number=""
142
+ local kind=""
143
+ local body_file=""
144
+ local body=""
145
+ local body_sha=""
146
+ local created_at=""
147
+ local payload=""
148
+ local intent_file=""
149
+
150
+ shift
151
+ while [[ $# -gt 0 ]]; do
152
+ case "$1" in
153
+ --repo-slug) repo_slug="${2:-}"; shift 2 ;;
154
+ --number) number="${2:-}"; shift 2 ;;
155
+ --kind) kind="${2:-}"; shift 2 ;;
156
+ --body-file) body_file="${2:-}"; shift 2 ;;
157
+ --help|-h) usage; exit 0 ;;
158
+ *)
159
+ echo "unknown argument: $1" >&2
160
+ exit 1
161
+ ;;
162
+ esac
163
+ done
164
+
165
+ [[ -n "${repo_slug}" && -n "${number}" && -n "${kind}" && -n "${body_file}" ]] || {
166
+ usage >&2
167
+ exit 1
168
+ }
169
+ [[ "${kind}" == "issue" || "${kind}" == "pr" ]] || {
170
+ echo "unsupported comment kind: ${kind}" >&2
171
+ exit 1
172
+ }
173
+ [[ -f "${body_file}" ]] || {
174
+ echo "missing comment body file: ${body_file}" >&2
175
+ exit 1
176
+ }
177
+
178
+ body="$(cat "${body_file}")"
179
+ [[ -n "${body}" ]] || {
180
+ echo "empty comment body" >&2
181
+ exit 1
182
+ }
183
+
184
+ body_sha="$(json_hash "${body}")"
185
+ created_at="$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
186
+ payload="$(
187
+ jq -cn \
188
+ --arg type "comment" \
189
+ --arg repo_slug "${repo_slug}" \
190
+ --arg number "${number}" \
191
+ --arg kind "${kind}" \
192
+ --arg body "${body}" \
193
+ --arg body_sha "${body_sha}" \
194
+ --arg created_at "${created_at}" \
195
+ '{
196
+ type: $type,
197
+ repo_slug: $repo_slug,
198
+ number: $number,
199
+ kind: $kind,
200
+ body: $body,
201
+ body_sha: $body_sha,
202
+ created_at: $created_at
203
+ }'
204
+ )"
205
+ intent_file="${PENDING_DIR}/comment-${kind}-${number}-${body_sha}.json"
206
+ if [[ ! -f "${intent_file}" ]]; then
207
+ printf '%s\n' "${payload}" >"${intent_file}"
208
+ fi
209
+ printf 'OUTBOX_FILE=%s\n' "${intent_file}"
210
+ }
211
+
212
+ enqueue_approval() {
213
+ local repo_slug=""
214
+ local number=""
215
+ local body="${DEFAULT_APPROVAL_BODY}"
216
+ local body_sha=""
217
+ local created_at=""
218
+ local payload=""
219
+ local intent_file=""
220
+
221
+ shift
222
+ while [[ $# -gt 0 ]]; do
223
+ case "$1" in
224
+ --repo-slug) repo_slug="${2:-}"; shift 2 ;;
225
+ --number) number="${2:-}"; shift 2 ;;
226
+ --body) body="${2:-}"; shift 2 ;;
227
+ --help|-h) usage; exit 0 ;;
228
+ *)
229
+ echo "unknown argument: $1" >&2
230
+ exit 1
231
+ ;;
232
+ esac
233
+ done
234
+
235
+ [[ -n "${repo_slug}" && -n "${number}" && -n "${body}" ]] || {
236
+ usage >&2
237
+ exit 1
238
+ }
239
+
240
+ body_sha="$(json_hash "${body}")"
241
+ created_at="$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
242
+ payload="$(
243
+ jq -cn \
244
+ --arg type "approval" \
245
+ --arg repo_slug "${repo_slug}" \
246
+ --arg number "${number}" \
247
+ --arg body "${body}" \
248
+ --arg body_sha "${body_sha}" \
249
+ --arg created_at "${created_at}" \
250
+ '{
251
+ type: $type,
252
+ repo_slug: $repo_slug,
253
+ number: $number,
254
+ body: $body,
255
+ body_sha: $body_sha,
256
+ created_at: $created_at
257
+ }'
258
+ )"
259
+ intent_file="${PENDING_DIR}/approval-${number}-${body_sha}.json"
260
+ if [[ ! -f "${intent_file}" ]]; then
261
+ printf '%s\n' "${payload}" >"${intent_file}"
262
+ fi
263
+ printf 'OUTBOX_FILE=%s\n' "${intent_file}"
264
+ }
265
+
266
+ flush_comment_intent() {
267
+ local intent_file="${1:?intent file required}"
268
+ local repo_slug=""
269
+ local number=""
270
+ local kind=""
271
+ local body=""
272
+ local existing_json=""
273
+ local post_payload=""
274
+
275
+ repo_slug="$(jq -r '.repo_slug // ""' "${intent_file}")"
276
+ number="$(jq -r '.number // ""' "${intent_file}")"
277
+ kind="$(jq -r '.kind // ""' "${intent_file}")"
278
+ body="$(jq -r '.body // ""' "${intent_file}")"
279
+
280
+ [[ -n "${repo_slug}" && -n "${number}" && -n "${kind}" && -n "${body}" ]] || return 65
281
+
282
+ if [[ "${kind}" == "pr" ]]; then
283
+ existing_json="$(flow_github_pr_view_json "${repo_slug}" "${number}" 2>/dev/null || true)"
284
+ else
285
+ existing_json="$(flow_github_issue_view_json "${repo_slug}" "${number}" 2>/dev/null || true)"
286
+ fi
287
+
288
+ if [[ -n "${existing_json}" ]] && jq -e --arg body "${body}" 'any(.comments[]?; .body == $body)' >/dev/null <<<"${existing_json}" 2>/dev/null; then
289
+ return 0
290
+ fi
291
+
292
+ post_payload="$(jq -cn --arg body "${body}" '{body: $body}')"
293
+ if printf '%s' "${post_payload}" | flow_github_api_repo "${repo_slug}" "issues/${number}/comments" --method POST --input - >/dev/null 2>&1; then
294
+ return 0
295
+ fi
296
+
297
+ if flow_github_core_rate_limit_active; then
298
+ return 75
299
+ fi
300
+
301
+ return 1
302
+ }
303
+
304
+ flush_labels_intent() {
305
+ local intent_file="${1:?intent file required}"
306
+ local repo_slug=""
307
+ local number=""
308
+ local -a args=()
309
+ local label=""
310
+
311
+ repo_slug="$(jq -r '.repo_slug // ""' "${intent_file}")"
312
+ number="$(jq -r '.number // ""' "${intent_file}")"
313
+ [[ -n "${repo_slug}" && -n "${number}" ]] || return 65
314
+
315
+ args=(--repo-slug "${repo_slug}" --number "${number}")
316
+ while IFS= read -r label; do
317
+ [[ -n "${label}" ]] || continue
318
+ args+=(--add "${label}")
319
+ done < <(jq -r '.add[]? // empty' "${intent_file}")
320
+ while IFS= read -r label; do
321
+ [[ -n "${label}" ]] || continue
322
+ args+=(--remove "${label}")
323
+ done < <(jq -r '.remove[]? // empty' "${intent_file}")
324
+
325
+ if ACP_GITHUB_OUTBOX_DISABLE_ENQUEUE=1 bash "${SCRIPT_DIR}/agent-github-update-labels" "${args[@]}" >/dev/null 2>&1; then
326
+ return 0
327
+ fi
328
+
329
+ if flow_github_core_rate_limit_active; then
330
+ return 75
331
+ fi
332
+
333
+ return 1
334
+ }
335
+
336
+ flush_approval_intent() {
337
+ local intent_file="${1:?intent file required}"
338
+ local repo_slug=""
339
+ local number=""
340
+ local body=""
341
+ local reviews_json="[]"
342
+ local post_payload=""
343
+
344
+ repo_slug="$(jq -r '.repo_slug // ""' "${intent_file}")"
345
+ number="$(jq -r '.number // ""' "${intent_file}")"
346
+ body="$(jq -r '.body // ""' "${intent_file}")"
347
+
348
+ [[ -n "${repo_slug}" && -n "${number}" && -n "${body}" ]] || return 65
349
+
350
+ if reviews_json="$(flow_github_api_repo "${repo_slug}" "pulls/${number}/reviews?per_page=100" 2>/dev/null)"; then
351
+ reviews_json="$(flow_json_or_default "${reviews_json}" '[]')"
352
+ if jq -e --arg body "${body}" 'any(.[]?; (.state // "") == "APPROVED" and (.body // "") == $body)' >/dev/null <<<"${reviews_json}" 2>/dev/null; then
353
+ return 0
354
+ fi
355
+ elif flow_github_core_rate_limit_active; then
356
+ return 75
357
+ fi
358
+
359
+ post_payload="$(jq -cn --arg event "APPROVE" --arg body "${body}" '{event: $event, body: $body}')"
360
+ if printf '%s' "${post_payload}" | flow_github_api_repo "${repo_slug}" "pulls/${number}/reviews" --method POST --input - >/dev/null 2>&1; then
361
+ return 0
362
+ fi
363
+
364
+ if flow_github_core_rate_limit_active; then
365
+ return 75
366
+ fi
367
+
368
+ return 1
369
+ }
370
+
371
+ flush_outbox() {
372
+ local limit="25"
373
+ local processed="0"
374
+ local intent_file=""
375
+ local intent_type=""
376
+ local status="0"
377
+
378
+ shift
379
+ while [[ $# -gt 0 ]]; do
380
+ case "$1" in
381
+ --limit) limit="${2:-25}"; shift 2 ;;
382
+ --help|-h) usage; exit 0 ;;
383
+ *)
384
+ echo "unknown argument: $1" >&2
385
+ exit 1
386
+ ;;
387
+ esac
388
+ done
389
+
390
+ [[ -d "${PENDING_DIR}" ]] || exit 0
391
+ flow_github_core_rate_limit_active && exit 0
392
+
393
+ while IFS= read -r intent_file; do
394
+ [[ -n "${intent_file}" ]] || continue
395
+ if (( processed >= limit )); then
396
+ break
397
+ fi
398
+
399
+ intent_type="$(jq -r '.type // ""' "${intent_file}" 2>/dev/null || true)"
400
+ case "${intent_type}" in
401
+ labels)
402
+ if flush_labels_intent "${intent_file}"; then
403
+ status="0"
404
+ else
405
+ status="$?"
406
+ fi
407
+ ;;
408
+ comment)
409
+ if flush_comment_intent "${intent_file}"; then
410
+ status="0"
411
+ else
412
+ status="$?"
413
+ fi
414
+ ;;
415
+ approval)
416
+ if flush_approval_intent "${intent_file}"; then
417
+ status="0"
418
+ else
419
+ status="$?"
420
+ fi
421
+ ;;
422
+ *)
423
+ status="65"
424
+ ;;
425
+ esac
426
+
427
+ case "${status}" in
428
+ 0)
429
+ outbox_move_sent "${intent_file}"
430
+ ;;
431
+ 65)
432
+ outbox_move_failed "${intent_file}"
433
+ ;;
434
+ 75)
435
+ break
436
+ ;;
437
+ *)
438
+ break
439
+ ;;
440
+ esac
441
+
442
+ processed=$((processed + 1))
443
+ done < <(find "${PENDING_DIR}" -mindepth 1 -maxdepth 1 -type f -name '*.json' 2>/dev/null | sort)
444
+
445
+ printf 'OUTBOX_FLUSHED=%s\n' "${processed}"
446
+ }
447
+
448
+ case "${ACTION}" in
449
+ enqueue-labels)
450
+ enqueue_labels "$@"
451
+ ;;
452
+ enqueue-comment)
453
+ enqueue_comment "$@"
454
+ ;;
455
+ enqueue-approval)
456
+ enqueue_approval "$@"
457
+ ;;
458
+ flush)
459
+ flush_outbox "$@"
460
+ ;;
461
+ --help|-h|"")
462
+ usage
463
+ exit 0
464
+ ;;
465
+ *)
466
+ echo "unknown action: ${ACTION}" >&2
467
+ usage >&2
468
+ exit 1
469
+ ;;
470
+ esac
@@ -268,9 +268,9 @@ record_scheduled_issue_launch() {
268
268
  cat >"$state_file" <<EOF
269
269
  INTERVAL_SECONDS=${interval_seconds}
270
270
  LAST_STARTED_EPOCH=${now_epoch}
271
- LAST_STARTED_AT=$(date -u -r "$now_epoch" +"%Y-%m-%dT%H:%M:%SZ")
271
+ LAST_STARTED_AT=$(flow_format_epoch_utc "$now_epoch")
272
272
  NEXT_DUE_EPOCH=${next_due_epoch}
273
- NEXT_DUE_AT=$(date -u -r "$next_due_epoch" +"%Y-%m-%dT%H:%M:%SZ")
273
+ NEXT_DUE_AT=$(flow_format_epoch_utc "$next_due_epoch")
274
274
  UPDATED_AT=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
275
275
  EOF
276
276
  }
@@ -299,12 +299,12 @@ record_scheduled_issue_result() {
299
299
  cat >"$state_file" <<EOF
300
300
  INTERVAL_SECONDS=${interval_seconds}
301
301
  LAST_STARTED_EPOCH=${last_started_epoch}
302
- LAST_STARTED_AT=$(if [[ "$last_started_epoch" =~ ^[0-9]+$ ]] && (( last_started_epoch > 0 )); then date -u -r "$last_started_epoch" +"%Y-%m-%dT%H:%M:%SZ"; fi)
302
+ LAST_STARTED_AT=$(if [[ "$last_started_epoch" =~ ^[0-9]+$ ]] && (( last_started_epoch > 0 )); then flow_format_epoch_utc "$last_started_epoch"; fi)
303
303
  LAST_RESULT_STATUS=${result_status}
304
304
  LAST_RESULT_EPOCH=${now_epoch}
305
- LAST_RESULT_AT=$(date -u -r "$now_epoch" +"%Y-%m-%dT%H:%M:%SZ")
305
+ LAST_RESULT_AT=$(flow_format_epoch_utc "$now_epoch")
306
306
  NEXT_DUE_EPOCH=${next_due_epoch}
307
- NEXT_DUE_AT=$(if [[ "$next_due_epoch" =~ ^[0-9]+$ ]] && (( next_due_epoch > 0 )); then date -u -r "$next_due_epoch" +"%Y-%m-%dT%H:%M:%SZ"; fi)
307
+ NEXT_DUE_AT=$(if [[ "$next_due_epoch" =~ ^[0-9]+$ ]] && (( next_due_epoch > 0 )); then flow_format_epoch_utc "$next_due_epoch"; fi)
308
308
  UPDATED_AT=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
309
309
  EOF
310
310
  }
@@ -361,14 +361,14 @@ record_blocked_recovery_issue_launch() {
361
361
  next_due_at=""
362
362
  if [[ "${blocked_recovery_cooldown_seconds:-}" =~ ^[1-9][0-9]*$ ]]; then
363
363
  next_due_epoch=$((now_epoch + blocked_recovery_cooldown_seconds))
364
- next_due_at="$(date -u -r "$next_due_epoch" +"%Y-%m-%dT%H:%M:%SZ")"
364
+ next_due_at="$(flow_format_epoch_utc "$next_due_epoch")"
365
365
  fi
366
366
 
367
367
  state_file="$(blocked_recovery_state_file "$issue_id")"
368
368
  cat >"$state_file" <<EOF
369
369
  LANE=blocked-recovery
370
370
  LAST_STARTED_EPOCH=${now_epoch}
371
- LAST_STARTED_AT=$(date -u -r "$now_epoch" +"%Y-%m-%dT%H:%M:%SZ")
371
+ LAST_STARTED_AT=$(flow_format_epoch_utc "$now_epoch")
372
372
  NEXT_DUE_EPOCH=${next_due_epoch}
373
373
  NEXT_DUE_AT=${next_due_at}
374
374
  UPDATED_AT=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
@@ -615,12 +615,35 @@ else
615
615
  exit "${loop_status}"
616
616
  fi
617
617
 
618
+ # ── Flush local GitHub write outbox ────────────────────────────────────────────
619
+ GITHUB_OUTBOX_FLUSH_LIMIT="${ACP_GITHUB_OUTBOX_FLUSH_LIMIT:-${F_LOSNING_GITHUB_OUTBOX_FLUSH_LIMIT:-25}}"
620
+ GITHUB_OUTBOX_FLUSH_TIMEOUT_SECONDS="${ACP_GITHUB_OUTBOX_FLUSH_TIMEOUT_SECONDS:-${F_LOSNING_GITHUB_OUTBOX_FLUSH_TIMEOUT_SECONDS:-30}}"
621
+ if [[ -x "${FLOW_TOOLS_DIR}/github-write-outbox.sh" ]]; then
622
+ printf '[%s] github-outbox flush start\n' "$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
623
+ if run_with_timeout "${GITHUB_OUTBOX_FLUSH_TIMEOUT_SECONDS}" \
624
+ env \
625
+ ACP_STATE_ROOT="$STATE_ROOT" \
626
+ F_LOSNING_STATE_ROOT="$STATE_ROOT" \
627
+ ACP_RUNS_ROOT="$RUNS_ROOT" \
628
+ F_LOSNING_RUNS_ROOT="$RUNS_ROOT" \
629
+ bash "${FLOW_TOOLS_DIR}/github-write-outbox.sh" flush --limit "${GITHUB_OUTBOX_FLUSH_LIMIT}"; then
630
+ printf '[%s] github-outbox flush end status=0\n' "$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
631
+ else
632
+ github_outbox_status=$?
633
+ if [[ "${github_outbox_status}" -eq 124 ]]; then
634
+ printf 'GITHUB_OUTBOX_FLUSH_TIMEOUT=yes\n'
635
+ fi
636
+ printf '[%s] github-outbox flush end status=%s\n' "$(date -u +"%Y-%m-%dT%H:%M:%SZ")" "${github_outbox_status}"
637
+ fi
638
+ fi
639
+
618
640
  # ── Throttled catch-up passes ──────────────────────────────────────────────────
619
641
  # These scripts fetch merged/closed PRs and linked issues which change rarely.
620
642
  # Run them at most once every CATCHUP_INTERVAL_SECONDS (default 300 = 5 min)
621
643
  # to avoid burning API quota on every heartbeat cycle.
622
644
  CATCHUP_INTERVAL_SECONDS="${ACP_CATCHUP_INTERVAL_SECONDS:-${F_LOSNING_CATCHUP_INTERVAL_SECONDS:-300}}"
623
645
  CATCHUP_STAMP_FILE="${STATE_ROOT}/last-catchup-timestamp"
646
+ SOURCE_REPO_SYNC_TIMEOUT_SECONDS="${ACP_SOURCE_REPO_SYNC_TIMEOUT_SECONDS:-${F_LOSNING_SOURCE_REPO_SYNC_TIMEOUT_SECONDS:-45}}"
624
647
  _catchup_now="$(date +%s)"
625
648
  _catchup_last="0"
626
649
  if [[ -f "${CATCHUP_STAMP_FILE}" ]]; then
@@ -648,6 +671,25 @@ if [[ "${_catchup_age}" -ge "${CATCHUP_INTERVAL_SECONDS}" ]]; then
648
671
  printf '[%s] merged-pr catchup end status=%s\n' "$(date -u +"%Y-%m-%dT%H:%M:%SZ")" "${catchup_status}"
649
672
  fi
650
673
 
674
+ if [[ -x "${FLOW_TOOLS_DIR}/agent-project-sync-source-repo-main" ]]; then
675
+ printf '[%s] source-repo main sync start\n' "$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
676
+ if run_with_timeout "${SOURCE_REPO_SYNC_TIMEOUT_SECONDS}" \
677
+ env \
678
+ ACP_RUNS_ROOT="$RUNS_ROOT" \
679
+ F_LOSNING_RUNS_ROOT="$RUNS_ROOT" \
680
+ ACP_STATE_ROOT="$STATE_ROOT" \
681
+ F_LOSNING_STATE_ROOT="$STATE_ROOT" \
682
+ bash "${FLOW_TOOLS_DIR}/agent-project-sync-source-repo-main"; then
683
+ printf '[%s] source-repo main sync end status=0\n' "$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
684
+ else
685
+ source_repo_sync_status=$?
686
+ if [[ "${source_repo_sync_status}" -eq 124 ]]; then
687
+ printf 'SOURCE_REPO_SYNC_TIMEOUT=yes\n'
688
+ fi
689
+ printf '[%s] source-repo main sync end status=%s\n' "$(date -u +"%Y-%m-%dT%H:%M:%SZ")" "${source_repo_sync_status}"
690
+ fi
691
+ fi
692
+
651
693
  printf '[%s] linked-pr issue catchup start\n' "$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
652
694
  if run_with_timeout "${CATCHUP_TIMEOUT_SECONDS}" \
653
695
  env \
@@ -129,8 +129,23 @@ LABEL="${label_override:-${ACP_PROJECT_RUNTIME_LAUNCHD_LABEL:-ai.agent.project.$
129
129
  BASE_PATH="$(build_launchd_base_path)"
130
130
  CODING_WORKER_OVERRIDE="${ACP_PROJECT_RUNTIME_CODING_WORKER:-${ACP_CODING_WORKER:-}}"
131
131
  SYNC_SCRIPT="${ACP_PROJECT_RUNTIME_SYNC_SCRIPT:-${FLOW_SKILL_DIR}/tools/bin/sync-shared-agent-home.sh}"
132
- BOOTSTRAP_SCRIPT="${ACP_PROJECT_RUNTIME_BOOTSTRAP_SCRIPT:-${FLOW_SKILL_DIR}/tools/bin/project-launchd-bootstrap.sh}"
133
- SUPERVISOR_SCRIPT="${ACP_PROJECT_RUNTIME_SUPERVISOR_SCRIPT:-${FLOW_SKILL_DIR}/tools/bin/project-runtime-supervisor.sh}"
132
+ RUNTIME_SKILL_DIR="${RUNTIME_HOME}/skills/openclaw/agent-control-plane"
133
+ BOOTSTRAP_SCRIPT="${ACP_PROJECT_RUNTIME_BOOTSTRAP_SCRIPT:-}"
134
+ if [[ -z "${BOOTSTRAP_SCRIPT}" ]]; then
135
+ if [[ -x "${RUNTIME_SKILL_DIR}/tools/bin/project-launchd-bootstrap.sh" ]]; then
136
+ BOOTSTRAP_SCRIPT="${RUNTIME_SKILL_DIR}/tools/bin/project-launchd-bootstrap.sh"
137
+ else
138
+ BOOTSTRAP_SCRIPT="${FLOW_SKILL_DIR}/tools/bin/project-launchd-bootstrap.sh"
139
+ fi
140
+ fi
141
+ SUPERVISOR_SCRIPT="${ACP_PROJECT_RUNTIME_SUPERVISOR_SCRIPT:-}"
142
+ if [[ -z "${SUPERVISOR_SCRIPT}" ]]; then
143
+ if [[ -x "${RUNTIME_SKILL_DIR}/tools/bin/project-runtime-supervisor.sh" ]]; then
144
+ SUPERVISOR_SCRIPT="${RUNTIME_SKILL_DIR}/tools/bin/project-runtime-supervisor.sh"
145
+ else
146
+ SUPERVISOR_SCRIPT="${FLOW_SKILL_DIR}/tools/bin/project-runtime-supervisor.sh"
147
+ fi
148
+ fi
134
149
  STATE_ROOT="$(flow_resolve_state_root "${CONFIG_YAML}")"
135
150
  SUPERVISOR_PID_FILE="${STATE_ROOT}/runtime-supervisor.pid"
136
151
  ENV_FILE="${ACP_PROJECT_RUNTIME_ENV_FILE:-${PROFILE_REGISTRY_ROOT}/${PROFILE_ID}/runtime.env}"