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
@@ -1,513 +0,0 @@
1
- #!/usr/bin/env bash
2
- set -euo pipefail
3
-
4
- usage() {
5
- cat <<'EOF'
6
- Usage:
7
- agent-project-cleanup-session --repo-root <path> [--runs-root <path> --history-root <path> --session <id>] [--worktree <path>] [--mode generic|issue|pr] [--remote <name>] [--remove-file <path>] [--keep-remote] [--allow-unmerged] [--skip-worktree-cleanup]
8
-
9
- Cleanup a project-lane worktree/branch using shared agent helpers, then archive
10
- the session run directory when one is provided.
11
- EOF
12
- }
13
-
14
- shared_agent_home="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
15
- repo_root="${AGENT_PROJECT_REPO_ROOT:-}"
16
- runs_root="${AGENT_PROJECT_RUNS_ROOT:-}"
17
- history_root="${AGENT_PROJECT_HISTORY_ROOT:-}"
18
- state_root="${AGENT_PROJECT_STATE_ROOT:-${F_LOSNING_STATE_ROOT:-}}"
19
- session=""
20
- worktree_path=""
21
- mode="generic"
22
- remote_name="origin"
23
- remove_file=""
24
- keep_remote="auto"
25
- allow_unmerged="auto"
26
- merged_base=""
27
- skip_worktree_cleanup="false"
28
-
29
- while [[ $# -gt 0 ]]; do
30
- case "$1" in
31
- --repo-root) repo_root="${2:-}"; shift 2 ;;
32
- --runs-root) runs_root="${2:-}"; shift 2 ;;
33
- --history-root) history_root="${2:-}"; shift 2 ;;
34
- --state-root) state_root="${2:-}"; shift 2 ;;
35
- --session) session="${2:-}"; shift 2 ;;
36
- --worktree) worktree_path="${2:-}"; shift 2 ;;
37
- --mode) mode="${2:-}"; shift 2 ;;
38
- --remote) remote_name="${2:-}"; shift 2 ;;
39
- --remove-file) remove_file="${2:-}"; shift 2 ;;
40
- --merged-base) merged_base="${2:-}"; shift 2 ;;
41
- --keep-remote) keep_remote="true"; shift ;;
42
- --allow-unmerged) allow_unmerged="true"; shift ;;
43
- --skip-worktree-cleanup) skip_worktree_cleanup="true"; shift ;;
44
- --help|-h) usage; exit 0 ;;
45
- *) echo "Unknown argument: $1" >&2; usage >&2; exit 1 ;;
46
- esac
47
- done
48
-
49
- if [[ -z "$repo_root" ]]; then
50
- echo "--repo-root is required" >&2
51
- usage >&2
52
- exit 1
53
- fi
54
-
55
- case "$mode" in
56
- generic|issue|pr) ;;
57
- *)
58
- echo "--mode must be generic, issue, or pr" >&2
59
- exit 1
60
- ;;
61
- esac
62
-
63
- if [[ -z "$worktree_path" && -z "$session" ]]; then
64
- echo "Provide at least one of --worktree or --session" >&2
65
- usage >&2
66
- exit 1
67
- fi
68
-
69
- if [[ -n "$session" && ( -z "$runs_root" || -z "$history_root" ) ]]; then
70
- echo "--runs-root and --history-root are required when --session is set" >&2
71
- usage >&2
72
- exit 1
73
- fi
74
-
75
- if [[ "$keep_remote" == "auto" ]]; then
76
- case "$mode" in
77
- issue) keep_remote="true" ;;
78
- *) keep_remote="false" ;;
79
- esac
80
- fi
81
-
82
- if [[ "$allow_unmerged" == "auto" ]]; then
83
- case "$mode" in
84
- issue|pr) allow_unmerged="true" ;;
85
- *) allow_unmerged="false" ;;
86
- esac
87
- fi
88
-
89
- meta_file=""
90
- branch_name=""
91
- result_file=""
92
- cleanup_warning=""
93
- cleanup_status="0"
94
- cleanup_error=""
95
- cleanup_mode="noop"
96
- orphan_fallback_used="false"
97
- active_tmux_session="false"
98
- archived_dir=""
99
-
100
- if [[ -n "$session" ]]; then
101
- meta_file="${runs_root}/${session}/run.env"
102
- if [[ -f "$meta_file" ]]; then
103
- set -a
104
- # shellcheck source=/dev/null
105
- source "$meta_file"
106
- set +a
107
- branch_name="${BRANCH:-}"
108
- result_file="${RESULT_FILE:-}"
109
- if [[ -z "$worktree_path" ]]; then
110
- worktree_path="${WORKTREE:-}"
111
- fi
112
- fi
113
- fi
114
-
115
- if [[ -z "$remove_file" ]]; then
116
- remove_file="$result_file"
117
- fi
118
-
119
- if [[ -n "$session" ]] && tmux has-session -t "$session" 2>/dev/null; then
120
- active_tmux_session="true"
121
- fi
122
-
123
- cleanup_tool="${shared_agent_home}/tools/bin/agent-cleanup-worktree"
124
- archive_tool="${shared_agent_home}/tools/bin/agent-project-archive-run"
125
-
126
- canonicalize_existing_dir() {
127
- local target="${1:-}"
128
- [[ -n "$target" && -d "$target" ]] || return 1
129
- (
130
- cd "$target"
131
- pwd -P
132
- )
133
- }
134
-
135
- canonicalize_dir_or_parent_join() {
136
- local target="${1:-}"
137
- local resolved=""
138
-
139
- [[ -n "${target}" ]] || return 1
140
-
141
- resolved="$(canonicalize_existing_dir "${target}" || true)"
142
- if [[ -n "${resolved}" ]]; then
143
- printf '%s\n' "${resolved}"
144
- return 0
145
- fi
146
-
147
- (
148
- cd "$(dirname "${target}")" 2>/dev/null
149
- printf '%s/%s\n' "$(pwd -P)" "$(basename "${target}")"
150
- ) || return 1
151
- }
152
-
153
- path_is_within_root() {
154
- local target="${1:-}"
155
- local root="${2:-}"
156
- local target_canonical=""
157
- local root_canonical=""
158
-
159
- target_canonical="$(canonicalize_existing_dir "$target" || true)"
160
- root_canonical="$(canonicalize_existing_dir "$root" || true)"
161
- [[ -n "$target_canonical" && -n "$root_canonical" ]] || return 1
162
- [[ "$target_canonical" == "$root_canonical" || "$target_canonical" == "$root_canonical"/* ]]
163
- }
164
-
165
- derive_state_root() {
166
- if [[ -n "${state_root}" && -d "${state_root}" ]]; then
167
- printf '%s\n' "${state_root}"
168
- return 0
169
- fi
170
-
171
- if [[ -n "${runs_root}" && -d "${runs_root}" ]]; then
172
- local candidate_root=""
173
- candidate_root="$(cd "${runs_root}/.." 2>/dev/null && pwd -P || true)"
174
- if [[ -n "${candidate_root}" && -d "${candidate_root}/state" ]]; then
175
- printf '%s/state\n' "${candidate_root}"
176
- return 0
177
- fi
178
- fi
179
-
180
- return 1
181
- }
182
-
183
- resident_worktree_protected() {
184
- local candidate_path="${1:-}"
185
- local resolved_candidate=""
186
- local resolved_state_root=""
187
- local metadata_file=""
188
- local resident_worktree=""
189
- local resident_realpath=""
190
- local resolved_resident=""
191
- local resident_lane_kind=""
192
- local resident_last_status=""
193
-
194
- [[ -n "${candidate_path}" && -d "${candidate_path}" ]] || return 1
195
- resolved_candidate="$(canonicalize_existing_dir "${candidate_path}" || true)"
196
- [[ -n "${resolved_candidate}" ]] || return 1
197
-
198
- resolved_state_root="$(derive_state_root || true)"
199
- [[ -n "${resolved_state_root}" && -d "${resolved_state_root}/resident-workers/issues" ]] || return 1
200
-
201
- for metadata_file in "${resolved_state_root}"/resident-workers/issues/*/metadata.env; do
202
- [[ -f "${metadata_file}" ]] || continue
203
- resident_worktree=""
204
- resident_realpath=""
205
- resident_lane_kind=""
206
- resident_last_status=""
207
- set +u
208
- set -a
209
- # shellcheck source=/dev/null
210
- source "${metadata_file}"
211
- set +a
212
- set -u
213
- resident_lane_kind="${RESIDENT_LANE_KIND:-}"
214
- resident_last_status="${LAST_STATUS:-}"
215
- if [[ "${resident_lane_kind}" != "recurring" && "${resident_last_status}" != "running" ]]; then
216
- continue
217
- fi
218
- resident_worktree="${WORKTREE_REALPATH:-${WORKTREE:-}}"
219
- [[ -n "${resident_worktree}" && -d "${resident_worktree}" ]] || continue
220
- resolved_resident="$(canonicalize_existing_dir "${resident_worktree}" || true)"
221
- [[ -n "${resolved_resident}" ]] || continue
222
- if [[ "${resolved_candidate}" == "${resolved_resident}" ]]; then
223
- return 0
224
- fi
225
- done
226
-
227
- return 1
228
- }
229
-
230
- active_resident_run_worktree_protected() {
231
- local candidate_path="${1:-}"
232
- local resolved_candidate=""
233
- local run_meta=""
234
- local run_dir=""
235
- local session_name=""
236
- local resident_enabled=""
237
- local resident_worktree=""
238
- local runner_state_file=""
239
- local runner_state=""
240
- local active_session="false"
241
- local resolved_resident=""
242
-
243
- [[ -n "${candidate_path}" && -d "${candidate_path}" ]] || return 1
244
- [[ -n "${runs_root}" && -d "${runs_root}" ]] || return 1
245
-
246
- resolved_candidate="$(canonicalize_existing_dir "${candidate_path}" || true)"
247
- [[ -n "${resolved_candidate}" ]] || return 1
248
-
249
- for run_meta in "${runs_root}"/*/run.env; do
250
- [[ -f "${run_meta}" ]] || continue
251
-
252
- session_name=""
253
- resident_enabled=""
254
- resident_worktree=""
255
- runner_state=""
256
- active_session="false"
257
-
258
- set +u
259
- set -a
260
- # shellcheck source=/dev/null
261
- source "${run_meta}"
262
- set +a
263
- set -u
264
-
265
- resident_enabled="${RESIDENT_WORKER_ENABLED:-no}"
266
- [[ "${resident_enabled}" == "yes" ]] || continue
267
-
268
- resident_worktree="${WORKTREE_REALPATH:-${WORKTREE:-}}"
269
- [[ -n "${resident_worktree}" && -d "${resident_worktree}" ]] || continue
270
- resolved_resident="$(canonicalize_existing_dir "${resident_worktree}" || true)"
271
- [[ -n "${resolved_resident}" ]] || continue
272
- [[ "${resolved_candidate}" == "${resolved_resident}" ]] || continue
273
-
274
- run_dir="$(dirname "${run_meta}")"
275
- runner_state_file="${run_dir}/runner.env"
276
- if [[ -f "${runner_state_file}" ]]; then
277
- set +u
278
- set -a
279
- # shellcheck source=/dev/null
280
- source "${runner_state_file}"
281
- set +a
282
- set -u
283
- runner_state="${RUNNER_STATE:-}"
284
- fi
285
-
286
- session_name="${SESSION:-}"
287
- if [[ -n "${session_name}" ]] && tmux has-session -t "${session_name}" 2>/dev/null; then
288
- active_session="true"
289
- fi
290
-
291
- if [[ "${runner_state}" == "running" || "${active_session}" == "true" ]]; then
292
- return 0
293
- fi
294
- done
295
-
296
- return 1
297
- }
298
-
299
- clear_resident_worktree_realpath_references() {
300
- local removed_path="${1:-}"
301
- local resolved_removed=""
302
- local resolved_state_root=""
303
- local metadata_file=""
304
- local resident_realpath=""
305
- local tmp_file=""
306
- local metadata_worktree_realpath=""
307
-
308
- [[ -n "${removed_path}" ]] || return 0
309
-
310
- resolved_removed="$(canonicalize_dir_or_parent_join "${removed_path}" || true)"
311
- [[ -n "${resolved_removed}" ]] || return 0
312
-
313
- resolved_state_root="$(derive_state_root || true)"
314
- [[ -n "${resolved_state_root}" && -d "${resolved_state_root}/resident-workers/issues" ]] || return 0
315
-
316
- for metadata_file in "${resolved_state_root}"/resident-workers/issues/*/metadata.env; do
317
- [[ -f "${metadata_file}" ]] || continue
318
- metadata_worktree_realpath=""
319
- set +u
320
- set -a
321
- # shellcheck source=/dev/null
322
- source "${metadata_file}"
323
- set +a
324
- set -u
325
- resident_realpath="${WORKTREE_REALPATH:-${metadata_worktree_realpath:-}}"
326
- [[ -n "${resident_realpath}" ]] || continue
327
- resident_realpath="$(canonicalize_dir_or_parent_join "${resident_realpath}" || true)"
328
- [[ -n "${resident_realpath}" ]] || continue
329
- if [[ "${resident_realpath}" != "${resolved_removed}" ]]; then
330
- continue
331
- fi
332
-
333
- tmp_file="${metadata_file}.tmp.$$"
334
- awk '
335
- BEGIN { replaced = 0 }
336
- /^WORKTREE_REALPATH=/ {
337
- print "WORKTREE_REALPATH='\'''\''"
338
- replaced = 1
339
- next
340
- }
341
- { print }
342
- END {
343
- if (replaced == 0) {
344
- print "WORKTREE_REALPATH='\'''\''"
345
- }
346
- }
347
- ' "${metadata_file}" >"${tmp_file}"
348
- mv "${tmp_file}" "${metadata_file}"
349
- done
350
- }
351
-
352
- cleanup_with_branch_tool() {
353
- local include_path="${1:-yes}"
354
- local -a cleanup_args
355
-
356
- cleanup_args=(--branch "$branch_name")
357
- if [[ -n "$remote_name" ]]; then
358
- cleanup_args+=(--remote "$remote_name")
359
- fi
360
- if [[ -n "$merged_base" ]]; then
361
- cleanup_args+=(--merged-base "$merged_base")
362
- fi
363
- if [[ "$include_path" == "yes" && -n "$worktree_path" && -d "$worktree_path" ]]; then
364
- cleanup_args+=(--path "$worktree_path")
365
- fi
366
- if [[ "$keep_remote" == "true" ]]; then
367
- cleanup_args+=(--keep-remote)
368
- fi
369
- if [[ "$allow_unmerged" == "true" ]]; then
370
- cleanup_args+=(--allow-unmerged)
371
- fi
372
-
373
- (
374
- cd "$repo_root"
375
- "$cleanup_tool" "${cleanup_args[@]}"
376
- )
377
- }
378
-
379
- cleanup_orphan_worktree_dir() {
380
- local worktree_root="${AGENT_PROJECT_WORKTREE_ROOT:-${F_LOSNING_WORKTREE_ROOT:-}}"
381
- local worktree_canonical=""
382
-
383
- [[ -n "$worktree_path" && -d "$worktree_path" ]] || return 1
384
- [[ ! -e "$worktree_path/.git" ]] || return 1
385
- [[ -n "$worktree_root" ]] || return 1
386
- path_is_within_root "$worktree_path" "$worktree_root" || return 1
387
-
388
- worktree_canonical="$(canonicalize_existing_dir "$worktree_path" || true)"
389
- [[ -n "$worktree_canonical" ]] || return 1
390
- rm -rf "$worktree_canonical"
391
- git -C "$repo_root" worktree prune >/dev/null 2>&1 || true
392
- }
393
-
394
- worktree_path_is_registered() {
395
- local candidate_path="${1:-}"
396
- [[ -n "${candidate_path}" ]] || return 1
397
-
398
- git -C "$repo_root" worktree list --porcelain 2>/dev/null \
399
- | grep -F -x -q -- "worktree ${candidate_path}"
400
- }
401
-
402
- write_cleanup_warning_artifact() {
403
- local target_dir=""
404
- local notice_file=""
405
- local cleanup_error_line=""
406
- local recorded_at=""
407
-
408
- [[ "${cleanup_status}" != "0" ]] || return 0
409
-
410
- if [[ -n "${archived_dir}" && -d "${archived_dir}" ]]; then
411
- target_dir="${archived_dir}"
412
- elif [[ -n "${session}" && -n "${runs_root}" && -d "${runs_root}/${session}" ]]; then
413
- target_dir="${runs_root}/${session}"
414
- fi
415
-
416
- [[ -n "${target_dir}" && -d "${target_dir}" ]] || return 0
417
-
418
- cleanup_error_line="$(printf '%s' "${cleanup_error}" | tr '\n' ' ' | sed 's/ */ /g')"
419
- recorded_at="$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
420
- notice_file="${target_dir}/cleanup-warning.txt"
421
- {
422
- printf 'recorded_at=%s\n' "${recorded_at}"
423
- printf 'session=%s\n' "${session}"
424
- printf 'mode=%s\n' "${mode}"
425
- printf 'worktree=%s\n' "${worktree_path}"
426
- printf 'branch=%s\n' "${branch_name}"
427
- printf 'cleanup_mode=%s\n' "${cleanup_mode}"
428
- printf 'cleanup_status=%s\n' "${cleanup_status}"
429
- if [[ -n "${cleanup_error_line}" ]]; then
430
- printf 'cleanup_error=%s\n' "${cleanup_error_line}"
431
- fi
432
- } >"${notice_file}"
433
- }
434
-
435
- if [[ "$active_tmux_session" == "true" ]]; then
436
- cleanup_mode="deferred-active-session"
437
- elif [[ "$skip_worktree_cleanup" != "true" && -n "${worktree_path}" ]] \
438
- && { resident_worktree_protected "${worktree_path}" || active_resident_run_worktree_protected "${worktree_path}"; }; then
439
- cleanup_mode="protected-resident-worktree"
440
- elif [[ "$skip_worktree_cleanup" != "true" && -n "$branch_name" ]]; then
441
- if cleanup_output="$(cleanup_with_branch_tool yes 2>&1)"; then
442
- cleanup_mode="branch"
443
- else
444
- cleanup_status="$?"
445
- cleanup_error="${cleanup_output}"
446
- if cleanup_orphan_worktree_dir; then
447
- orphan_fallback_used="true"
448
- if cleanup_retry_output="$(cleanup_with_branch_tool no 2>&1)"; then
449
- cleanup_mode="orphan-worktree"
450
- cleanup_status="0"
451
- cleanup_warning="${cleanup_error}"
452
- else
453
- cleanup_status="$?"
454
- cleanup_error="${cleanup_error}"$'\n'"${cleanup_retry_output}"
455
- fi
456
- fi
457
- fi
458
- elif [[ "$skip_worktree_cleanup" != "true" && -n "$worktree_path" ]] && worktree_path_is_registered "$worktree_path"; then
459
- git -C "$repo_root" worktree remove "$worktree_path" --force || true
460
- git -C "$repo_root" worktree prune
461
- cleanup_mode="worktree"
462
- elif [[ "$skip_worktree_cleanup" != "true" ]] && cleanup_orphan_worktree_dir; then
463
- orphan_fallback_used="true"
464
- cleanup_mode="orphan-worktree"
465
- elif [[ "$skip_worktree_cleanup" == "true" ]]; then
466
- cleanup_mode="archived-only"
467
- fi
468
-
469
- archive_output=""
470
- if [[ -n "$session" && "$active_tmux_session" != "true" ]]; then
471
- archive_output="$(
472
- "$archive_tool" \
473
- --runs-root "$runs_root" \
474
- --history-root "$history_root" \
475
- --session "$session" \
476
- --remove-file "${remove_file:-}"
477
- )"
478
- archived_dir="$(awk -F= '/^ARCHIVED_DIR=/{print substr($0, index($0, "=") + 1); exit}' <<<"${archive_output}")"
479
- fi
480
-
481
- if [[ "$skip_worktree_cleanup" != "true" && -n "$worktree_path" && ! -d "$worktree_path" ]]; then
482
- clear_resident_worktree_realpath_references "$worktree_path"
483
- fi
484
-
485
- write_cleanup_warning_artifact
486
-
487
- printf 'SESSION=%s\n' "$session"
488
- printf 'MODE=%s\n' "$mode"
489
- printf 'WORKTREE=%s\n' "$worktree_path"
490
- printf 'BRANCH=%s\n' "$branch_name"
491
- printf 'KEEP_REMOTE=%s\n' "$keep_remote"
492
- printf 'ALLOW_UNMERGED=%s\n' "$allow_unmerged"
493
- printf 'SKIP_WORKTREE_CLEANUP=%s\n' "$skip_worktree_cleanup"
494
- printf 'ACTIVE_TMUX_SESSION=%s\n' "$active_tmux_session"
495
- printf 'CLEANUP_MODE=%s\n' "$cleanup_mode"
496
- printf 'ORPHAN_FALLBACK_USED=%s\n' "$orphan_fallback_used"
497
- printf 'CLEANUP_STATUS=%s\n' "$cleanup_status"
498
- if [[ -n "$meta_file" ]]; then
499
- printf 'META_FILE=%s\n' "$meta_file"
500
- fi
501
- if [[ -n "$archive_output" ]]; then
502
- printf '%s\n' "$archive_output"
503
- fi
504
- if [[ -n "$cleanup_warning" ]]; then
505
- printf 'CLEANUP_WARNING=%s\n' "$(printf '%s' "$cleanup_warning" | tr '\n' ' ' | sed 's/ */ /g')"
506
- fi
507
- if [[ "$cleanup_status" != "0" && -n "$cleanup_error" ]]; then
508
- printf 'CLEANUP_ERROR=%s\n' "$(printf '%s' "$cleanup_error" | tr '\n' ' ' | sed 's/ */ /g')"
509
- fi
510
-
511
- if [[ "$cleanup_status" != "0" ]]; then
512
- exit "$cleanup_status"
513
- fi
@@ -1,127 +0,0 @@
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
- CONFIG_YAML="$(resolve_flow_config_yaml "${BASH_SOURCE[0]}")"
8
- STATE_ROOT="$(flow_resolve_state_root "${CONFIG_YAML}")"
9
- AGENT_REPO_ROOT="$(flow_resolve_agent_repo_root "${CONFIG_YAML}")"
10
- REPO_ROOT="$(flow_resolve_repo_root "${CONFIG_YAML}")"
11
-
12
- usage() {
13
- cat <<'EOF'
14
- Usage:
15
- agent-project-detached-launch [--pending-key <kind-id>] <name> <command> [args...]
16
-
17
- Detach a worker bootstrap command from the current heartbeat process so the
18
- heartbeat can keep launching additional workers without waiting for expensive
19
- worktree/bootstrap/setup steps to finish.
20
- EOF
21
- }
22
-
23
- pending_key=""
24
-
25
- while [[ $# -gt 0 ]]; do
26
- case "$1" in
27
- --pending-key)
28
- pending_key="${2:-}"
29
- shift 2
30
- ;;
31
- --help|-h)
32
- usage
33
- exit 0
34
- ;;
35
- *)
36
- break
37
- ;;
38
- esac
39
- done
40
-
41
- if [[ $# -lt 2 ]]; then
42
- usage >&2
43
- exit 1
44
- fi
45
-
46
- launch_name="${1:-}"
47
- shift
48
-
49
- if [[ -z "${launch_name}" ]]; then
50
- usage >&2
51
- exit 1
52
- fi
53
-
54
- log_root="${ACP_LAUNCHER_LOG_ROOT:-${F_LOSNING_LAUNCHER_LOG_ROOT:-${STATE_ROOT}/launcher-logs}}"
55
- mkdir -p "${log_root}"
56
-
57
- safe_name="$(printf '%s' "${launch_name}" | tr '/[:space:]' '__' | tr -cd '[:alnum:]_.-')"
58
- if [[ -z "${safe_name}" ]]; then
59
- safe_name="launch"
60
- fi
61
-
62
- timestamp="$(date -u +%Y%m%dT%H%M%SZ)"
63
- log_file="${log_root}/${timestamp}-${safe_name}.log"
64
- pending_file=""
65
- pending_dir="${ACP_PENDING_LAUNCH_DIR:-${F_LOSNING_PENDING_LAUNCH_DIR:-${STATE_ROOT}/pending-launches}}"
66
- if [[ -n "${pending_key}" ]]; then
67
- mkdir -p "${pending_dir}"
68
- pending_file="${pending_dir}/${pending_key}.pid"
69
- fi
70
-
71
- launch_cwd="${ACP_LAUNCH_CWD:-${F_LOSNING_LAUNCH_CWD:-}}"
72
- if [[ -z "${launch_cwd}" ]]; then
73
- for candidate in "${AGENT_REPO_ROOT}" "${REPO_ROOT}" "${STATE_ROOT}" "${HOME:-}" "/"; do
74
- if [[ -n "${candidate}" && -d "${candidate}" ]]; then
75
- launch_cwd="${candidate}"
76
- break
77
- fi
78
- done
79
- fi
80
-
81
- if [[ -z "${launch_cwd}" || ! -d "${launch_cwd}" ]]; then
82
- echo "could not determine a stable working directory for detached launch" >&2
83
- exit 1
84
- fi
85
-
86
- python_bin="${PYTHON_BIN:-$(command -v python3 || true)}"
87
- if [[ -z "${python_bin}" ]]; then
88
- echo "python3 is required for detached launch" >&2
89
- exit 1
90
- fi
91
-
92
- launch_pid="$(
93
- "${python_bin}" - "${log_file}" "${pending_file}" "${launch_cwd}" "$@" <<'PY'
94
- import subprocess
95
- import sys
96
- from pathlib import Path
97
-
98
- log_file = Path(sys.argv[1])
99
- pending_file = sys.argv[2]
100
- launch_cwd = sys.argv[3]
101
- argv = sys.argv[4:]
102
-
103
- with log_file.open("ab", buffering=0) as log_handle:
104
- proc = subprocess.Popen(
105
- argv,
106
- stdin=subprocess.DEVNULL,
107
- stdout=log_handle,
108
- stderr=subprocess.STDOUT,
109
- cwd=launch_cwd,
110
- start_new_session=True,
111
- )
112
-
113
- if pending_file:
114
- Path(pending_file).write_text(f"{proc.pid}\n", encoding="utf-8")
115
-
116
- print(proc.pid)
117
- PY
118
- )"
119
-
120
- printf 'LAUNCH_MODE=detached\n'
121
- printf 'LAUNCH_NAME=%s\n' "${launch_name}"
122
- printf 'LAUNCH_PID=%s\n' "${launch_pid}"
123
- printf 'LAUNCH_LOG=%s\n' "${log_file}"
124
- printf 'LAUNCH_CWD=%s\n' "${launch_cwd}"
125
- if [[ -n "${pending_file}" ]]; then
126
- printf 'LAUNCH_PENDING_FILE=%s\n' "${pending_file}"
127
- fi