agent-control-plane 0.1.9 → 0.1.13
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/hooks/heartbeat-hooks.sh +147 -8
- package/hooks/issue-reconcile-hooks.sh +46 -0
- package/npm/bin/agent-control-plane.js +89 -8
- package/package.json +8 -2
- package/references/commands.md +1 -0
- package/tools/bin/agent-project-cleanup-session +133 -0
- package/tools/bin/agent-project-publish-issue-pr +178 -62
- package/tools/bin/agent-project-reconcile-issue-session +171 -3
- package/tools/bin/agent-project-run-codex-resilient +121 -16
- package/tools/bin/agent-project-run-codex-session +118 -10
- package/tools/bin/agent-project-run-openclaw-session +82 -8
- package/tools/bin/branch-verification-guard.sh +15 -2
- 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 +15 -1
- package/tools/bin/heartbeat-safe-auto.sh +32 -0
- package/tools/bin/issue-publish-localization-guard.sh +142 -0
- 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/reuse-issue-worktree.sh +46 -0
- package/tools/bin/start-issue-worker.sh +110 -30
- package/tools/bin/start-resident-issue-loop.sh +1 -0
- package/tools/bin/sync-shared-agent-home.sh +50 -10
- package/tools/bin/test-smoke.sh +6 -1
- package/tools/dashboard/app.js +71 -1
- package/tools/dashboard/dashboard_snapshot.py +74 -0
- package/tools/dashboard/styles.css +43 -0
- package/tools/templates/issue-prompt-template.md +20 -65
- 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
package/hooks/heartbeat-hooks.sh
CHANGED
|
@@ -15,15 +15,87 @@ FLOW_TOOLS_DIR="${FLOW_SKILL_DIR}/tools/bin"
|
|
|
15
15
|
DETACHED_LAUNCH_BIN="${FLOW_TOOLS_DIR}/agent-project-detached-launch"
|
|
16
16
|
RESIDENT_ISSUE_LOOP_BIN="${FLOW_TOOLS_DIR}/start-resident-issue-loop.sh"
|
|
17
17
|
REPO_SLUG="$(flow_resolve_repo_slug "${CONFIG_YAML}")"
|
|
18
|
+
AGENT_REPO_ROOT="$(flow_resolve_agent_repo_root "${CONFIG_YAML}")"
|
|
19
|
+
DEFAULT_BRANCH="$(flow_resolve_default_branch "${CONFIG_YAML}")"
|
|
20
|
+
STATE_ROOT="$(flow_resolve_state_root "${CONFIG_YAML}")"
|
|
21
|
+
PENDING_LAUNCH_DIR="${ACP_PENDING_LAUNCH_DIR:-${F_LOSNING_PENDING_LAUNCH_DIR:-${STATE_ROOT}/pending-launches}}"
|
|
18
22
|
AGENT_PR_PREFIXES_JSON="$(flow_managed_pr_prefixes_json "${CONFIG_YAML}")"
|
|
19
23
|
AGENT_PR_ISSUE_CAPTURE_REGEX="$(flow_managed_issue_branch_regex "${CONFIG_YAML}")"
|
|
20
24
|
AGENT_PR_HANDOFF_LABEL="${AGENT_PR_HANDOFF_LABEL:-agent-handoff}"
|
|
21
25
|
AGENT_EXCLUSIVE_LABEL="${AGENT_EXCLUSIVE_LABEL:-agent-exclusive}"
|
|
22
26
|
CODING_WORKER="${ACP_CODING_WORKER:-${F_LOSNING_CODING_WORKER:-codex}}"
|
|
27
|
+
HEARTBEAT_ISSUE_JSON_CACHE_DIR="${TMPDIR:-/tmp}/heartbeat-issue-json.$$"
|
|
28
|
+
|
|
29
|
+
heartbeat_issue_retry_state_file() {
|
|
30
|
+
local issue_id="${1:?issue id required}"
|
|
31
|
+
printf '%s/retries/issues/%s.env\n' "${STATE_ROOT}" "${issue_id}"
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
heartbeat_reason_requires_baseline_change() {
|
|
35
|
+
local reason="${1:-}"
|
|
36
|
+
case "${reason}" in
|
|
37
|
+
verification-guard-blocked|no-publishable-commits|no-publishable-delta)
|
|
38
|
+
return 0
|
|
39
|
+
;;
|
|
40
|
+
*)
|
|
41
|
+
return 1
|
|
42
|
+
;;
|
|
43
|
+
esac
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
heartbeat_current_baseline_head_sha() {
|
|
47
|
+
local head_sha=""
|
|
48
|
+
if [[ -d "${AGENT_REPO_ROOT}" ]]; then
|
|
49
|
+
head_sha="$(git -C "${AGENT_REPO_ROOT}" rev-parse --verify --quiet "origin/${DEFAULT_BRANCH}" 2>/dev/null || true)"
|
|
50
|
+
if [[ -z "${head_sha}" ]]; then
|
|
51
|
+
head_sha="$(git -C "${AGENT_REPO_ROOT}" rev-parse --verify --quiet "${DEFAULT_BRANCH}" 2>/dev/null || true)"
|
|
52
|
+
fi
|
|
53
|
+
fi
|
|
54
|
+
printf '%s\n' "${head_sha}"
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
heartbeat_retry_reason_is_baseline_blocked() {
|
|
58
|
+
local issue_id="${1:?issue id required}"
|
|
59
|
+
local reason="${2:-}"
|
|
60
|
+
local state_file baseline_head current_head
|
|
61
|
+
|
|
62
|
+
heartbeat_reason_requires_baseline_change "${reason}" || return 1
|
|
63
|
+
state_file="$(heartbeat_issue_retry_state_file "${issue_id}")"
|
|
64
|
+
[[ -f "${state_file}" ]] || return 1
|
|
65
|
+
|
|
66
|
+
baseline_head="$(awk -F= '/^BASELINE_HEAD_SHA=/{print substr($0, index($0, "=") + 1); exit}' "${state_file}" 2>/dev/null | tr -d '\r' || true)"
|
|
67
|
+
[[ -n "${baseline_head}" ]] || return 1
|
|
68
|
+
current_head="$(heartbeat_current_baseline_head_sha)"
|
|
69
|
+
[[ -n "${current_head}" ]] || return 1
|
|
70
|
+
|
|
71
|
+
[[ "${baseline_head}" == "${current_head}" ]]
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
heartbeat_issue_json_cached() {
|
|
75
|
+
local issue_id="${1:?issue id required}"
|
|
76
|
+
local cache_file=""
|
|
77
|
+
local issue_json=""
|
|
78
|
+
|
|
79
|
+
if [[ ! -d "${HEARTBEAT_ISSUE_JSON_CACHE_DIR}" ]]; then
|
|
80
|
+
mkdir -p "${HEARTBEAT_ISSUE_JSON_CACHE_DIR}"
|
|
81
|
+
fi
|
|
82
|
+
|
|
83
|
+
cache_file="${HEARTBEAT_ISSUE_JSON_CACHE_DIR}/${issue_id}.json"
|
|
84
|
+
if [[ -f "${cache_file}" ]]; then
|
|
85
|
+
cat "${cache_file}"
|
|
86
|
+
return 0
|
|
87
|
+
fi
|
|
88
|
+
|
|
89
|
+
issue_json="$(flow_github_issue_view_json "$REPO_SLUG" "$issue_id" 2>/dev/null || true)"
|
|
90
|
+
printf '%s' "${issue_json}" >"${cache_file}"
|
|
91
|
+
printf '%s\n' "${issue_json}"
|
|
92
|
+
}
|
|
23
93
|
|
|
24
94
|
heartbeat_open_agent_pr_issue_ids() {
|
|
25
|
-
|
|
26
|
-
|
|
95
|
+
local pr_issue_ids_json=""
|
|
96
|
+
pr_issue_ids_json="$(
|
|
97
|
+
flow_github_pr_list_json "$REPO_SLUG" open 100 \
|
|
98
|
+
| jq --argjson agentPrPrefixes "${AGENT_PR_PREFIXES_JSON}" --arg handoffLabel "${AGENT_PR_HANDOFF_LABEL}" --arg branchIssueRegex "${AGENT_PR_ISSUE_CAPTURE_REGEX}" '
|
|
27
99
|
map(
|
|
28
100
|
. as $pr
|
|
29
101
|
| select(
|
|
@@ -48,6 +120,13 @@ heartbeat_open_agent_pr_issue_ids() {
|
|
|
48
120
|
)
|
|
49
121
|
| unique
|
|
50
122
|
'
|
|
123
|
+
)"
|
|
124
|
+
|
|
125
|
+
if [[ -z "${pr_issue_ids_json:-}" ]]; then
|
|
126
|
+
printf '[]\n'
|
|
127
|
+
else
|
|
128
|
+
printf '%s\n' "${pr_issue_ids_json}"
|
|
129
|
+
fi
|
|
51
130
|
}
|
|
52
131
|
|
|
53
132
|
heartbeat_list_ready_issue_ids() {
|
|
@@ -120,11 +199,14 @@ heartbeat_issue_blocked_recovery_reason() {
|
|
|
120
199
|
retry_out="$("${FLOW_TOOLS_DIR}/retry-state.sh" issue "$issue_id" get 2>/dev/null || true)"
|
|
121
200
|
retry_reason="$(awk -F= '/^LAST_REASON=/{print $2}' <<<"${retry_out:-}")"
|
|
122
201
|
if [[ -n "${retry_reason:-}" && "${retry_reason}" != "issue-worker-blocked" ]]; then
|
|
202
|
+
if heartbeat_retry_reason_is_baseline_blocked "${issue_id}" "${retry_reason}"; then
|
|
203
|
+
return 0
|
|
204
|
+
fi
|
|
123
205
|
printf '%s\n' "$retry_reason"
|
|
124
206
|
return 0
|
|
125
207
|
fi
|
|
126
208
|
|
|
127
|
-
issue_json="$(
|
|
209
|
+
issue_json="$(heartbeat_issue_json_cached "$issue_id")"
|
|
128
210
|
if [[ -z "${issue_json:-}" ]]; then
|
|
129
211
|
return 0
|
|
130
212
|
fi
|
|
@@ -163,6 +245,8 @@ if (explicitFailureReason) {
|
|
|
163
245
|
reason = 'scope-guard-blocked';
|
|
164
246
|
} else if (/verification guard/i.test(body)) {
|
|
165
247
|
reason = 'verification-guard-blocked';
|
|
248
|
+
} else if (/localization guard/i.test(body) || /^# Blocker: Localization requirements were not satisfied$/im.test(body)) {
|
|
249
|
+
reason = 'localization-guard-blocked';
|
|
166
250
|
} else if (/missing referenced OpenSpec paths/i.test(body)) {
|
|
167
251
|
reason = 'missing-openspec-paths';
|
|
168
252
|
} else if (/superseded by focused follow-up issues/i.test(body)) {
|
|
@@ -248,7 +332,7 @@ heartbeat_issue_is_heavy() {
|
|
|
248
332
|
heartbeat_issue_is_recurring() {
|
|
249
333
|
local issue_id="${1:?issue id required}"
|
|
250
334
|
local issue_json
|
|
251
|
-
issue_json="$(
|
|
335
|
+
issue_json="$(heartbeat_issue_json_cached "$issue_id")"
|
|
252
336
|
if [[ -n "$issue_json" ]] && jq -e 'any(.labels[]?; .name == "agent-keep-open")' >/dev/null <<<"$issue_json"; then
|
|
253
337
|
printf 'yes\n'
|
|
254
338
|
else
|
|
@@ -259,7 +343,7 @@ heartbeat_issue_is_recurring() {
|
|
|
259
343
|
heartbeat_issue_schedule_interval_seconds() {
|
|
260
344
|
local issue_id="${1:?issue id required}"
|
|
261
345
|
local issue_json issue_body
|
|
262
|
-
issue_json="$(
|
|
346
|
+
issue_json="$(heartbeat_issue_json_cached "$issue_id")"
|
|
263
347
|
if [[ -z "$issue_json" ]]; then
|
|
264
348
|
issue_json='{}'
|
|
265
349
|
fi
|
|
@@ -282,7 +366,7 @@ EOF
|
|
|
282
366
|
heartbeat_issue_schedule_token() {
|
|
283
367
|
local issue_id="${1:?issue id required}"
|
|
284
368
|
local issue_json issue_body
|
|
285
|
-
issue_json="$(
|
|
369
|
+
issue_json="$(heartbeat_issue_json_cached "$issue_id")"
|
|
286
370
|
if [[ -z "$issue_json" ]]; then
|
|
287
371
|
issue_json='{}'
|
|
288
372
|
fi
|
|
@@ -321,7 +405,7 @@ heartbeat_issue_is_scheduled() {
|
|
|
321
405
|
heartbeat_issue_is_exclusive() {
|
|
322
406
|
local issue_id="${1:?issue id required}"
|
|
323
407
|
local issue_json
|
|
324
|
-
issue_json="$(
|
|
408
|
+
issue_json="$(heartbeat_issue_json_cached "$issue_id")"
|
|
325
409
|
if [[ -n "$issue_json" ]] && jq -e --arg exclusiveLabel "${AGENT_EXCLUSIVE_LABEL}" 'any(.labels[]?; .name == $exclusiveLabel)' >/dev/null <<<"$issue_json"; then
|
|
326
410
|
printf 'yes\n'
|
|
327
411
|
else
|
|
@@ -379,7 +463,7 @@ heartbeat_sync_issue_labels() {
|
|
|
379
463
|
local -a add_args=()
|
|
380
464
|
local -a update_args=()
|
|
381
465
|
|
|
382
|
-
issue_json="$(
|
|
466
|
+
issue_json="$(heartbeat_issue_json_cached "$issue_id")"
|
|
383
467
|
if [[ -z "$issue_json" ]]; then
|
|
384
468
|
return 0
|
|
385
469
|
fi
|
|
@@ -501,6 +585,52 @@ heartbeat_issue_resident_worker_key() {
|
|
|
501
585
|
flow_resident_issue_lane_key "${CODING_WORKER}" "safe" "${lane_kind}" "${lane_value}"
|
|
502
586
|
}
|
|
503
587
|
|
|
588
|
+
heartbeat_pending_issue_launch_pid() {
|
|
589
|
+
local issue_id="${1:?issue id required}"
|
|
590
|
+
local pending_file pid=""
|
|
591
|
+
|
|
592
|
+
pending_file="${PENDING_LAUNCH_DIR}/issue-${issue_id}.pid"
|
|
593
|
+
[[ -f "${pending_file}" ]] || return 1
|
|
594
|
+
|
|
595
|
+
pid="$(tr -d '[:space:]' <"${pending_file}" 2>/dev/null || true)"
|
|
596
|
+
[[ "${pid}" =~ ^[0-9]+$ ]] || return 1
|
|
597
|
+
kill -0 "${pid}" 2>/dev/null || return 1
|
|
598
|
+
|
|
599
|
+
printf '%s\n' "${pid}"
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
heartbeat_pending_resident_lane_launch_issue_id() {
|
|
603
|
+
local issue_id="${1:?issue id required}"
|
|
604
|
+
local worker_key=""
|
|
605
|
+
local pending_file=""
|
|
606
|
+
local candidate_issue_id=""
|
|
607
|
+
local candidate_worker_key=""
|
|
608
|
+
|
|
609
|
+
worker_key="$(heartbeat_issue_resident_worker_key "${issue_id}")"
|
|
610
|
+
[[ -n "${worker_key}" ]] || return 1
|
|
611
|
+
[[ -d "${PENDING_LAUNCH_DIR}" ]] || return 1
|
|
612
|
+
|
|
613
|
+
for pending_file in "${PENDING_LAUNCH_DIR}"/issue-*.pid; do
|
|
614
|
+
[[ -f "${pending_file}" ]] || continue
|
|
615
|
+
candidate_issue_id="${pending_file##*/issue-}"
|
|
616
|
+
candidate_issue_id="${candidate_issue_id%.pid}"
|
|
617
|
+
[[ -n "${candidate_issue_id}" ]] || continue
|
|
618
|
+
if ! heartbeat_pending_issue_launch_pid "${candidate_issue_id}" >/dev/null 2>&1; then
|
|
619
|
+
rm -f "${pending_file}" 2>/dev/null || true
|
|
620
|
+
continue
|
|
621
|
+
fi
|
|
622
|
+
if [[ "$(heartbeat_issue_uses_resident_loop "${candidate_issue_id}")" != "yes" ]]; then
|
|
623
|
+
continue
|
|
624
|
+
fi
|
|
625
|
+
candidate_worker_key="$(heartbeat_issue_resident_worker_key "${candidate_issue_id}")"
|
|
626
|
+
[[ -n "${candidate_worker_key}" && "${candidate_worker_key}" == "${worker_key}" ]] || continue
|
|
627
|
+
printf '%s\n' "${candidate_issue_id}"
|
|
628
|
+
return 0
|
|
629
|
+
done
|
|
630
|
+
|
|
631
|
+
return 1
|
|
632
|
+
}
|
|
633
|
+
|
|
504
634
|
heartbeat_live_issue_controller_for_lane() {
|
|
505
635
|
local issue_id="${1:?issue id required}"
|
|
506
636
|
local worker_key=""
|
|
@@ -548,11 +678,20 @@ heartbeat_enqueue_issue_for_resident_controller() {
|
|
|
548
678
|
|
|
549
679
|
heartbeat_start_issue_worker() {
|
|
550
680
|
local issue_id="${1:?issue id required}"
|
|
681
|
+
local pending_lane_issue_id=""
|
|
551
682
|
if [[ "$(heartbeat_issue_uses_resident_loop "${issue_id}")" == "yes" ]]; then
|
|
552
683
|
if heartbeat_enqueue_issue_for_live_resident_lane "${issue_id}"; then
|
|
553
684
|
printf 'LAUNCH_MODE=resident-lease\n'
|
|
554
685
|
return 0
|
|
555
686
|
fi
|
|
687
|
+
pending_lane_issue_id="$(heartbeat_pending_resident_lane_launch_issue_id "${issue_id}" || true)"
|
|
688
|
+
if [[ -n "${pending_lane_issue_id}" ]]; then
|
|
689
|
+
if [[ "${pending_lane_issue_id}" != "${issue_id}" ]]; then
|
|
690
|
+
flow_resident_issue_enqueue "${CONFIG_YAML}" "${issue_id}" "heartbeat-pending-lane" >/dev/null
|
|
691
|
+
fi
|
|
692
|
+
printf 'LAUNCH_MODE=resident-pending-lane\n'
|
|
693
|
+
return 0
|
|
694
|
+
fi
|
|
556
695
|
if heartbeat_enqueue_issue_for_resident_controller "${issue_id}"; then
|
|
557
696
|
printf 'LAUNCH_MODE=resident-lease\n'
|
|
558
697
|
return 0
|
|
@@ -12,7 +12,9 @@ ADAPTER_BIN_DIR="${FLOW_SKILL_DIR}/bin"
|
|
|
12
12
|
FLOW_TOOLS_DIR="${FLOW_SKILL_DIR}/tools/bin"
|
|
13
13
|
REPO_SLUG="$(flow_resolve_repo_slug "${CONFIG_YAML}")"
|
|
14
14
|
AGENT_ROOT="$(flow_resolve_agent_root "${CONFIG_YAML}")"
|
|
15
|
+
AGENT_REPO_ROOT="$(flow_resolve_agent_repo_root "${CONFIG_YAML}")"
|
|
15
16
|
STATE_ROOT="$(flow_resolve_state_root "${CONFIG_YAML}")"
|
|
17
|
+
DEFAULT_BRANCH="$(flow_resolve_default_branch "${CONFIG_YAML}")"
|
|
16
18
|
RUNS_ROOT="$(flow_resolve_runs_root "${CONFIG_YAML}")"
|
|
17
19
|
BLOCKED_RECOVERY_STATE_DIR="${STATE_ROOT}/blocked-recovery-issues"
|
|
18
20
|
|
|
@@ -26,6 +28,49 @@ issue_clear_blocked_recovery_state() {
|
|
|
26
28
|
rm -f "${BLOCKED_RECOVERY_STATE_DIR}/${ISSUE_ID}.env" 2>/dev/null || true
|
|
27
29
|
}
|
|
28
30
|
|
|
31
|
+
issue_retry_state_file() {
|
|
32
|
+
printf '%s/retries/issues/%s.env\n' "${STATE_ROOT}" "${ISSUE_ID}"
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
issue_reason_requires_baseline_change() {
|
|
36
|
+
local reason="${1:-}"
|
|
37
|
+
case "${reason}" in
|
|
38
|
+
verification-guard-blocked|no-publishable-commits|no-publishable-delta)
|
|
39
|
+
return 0
|
|
40
|
+
;;
|
|
41
|
+
*)
|
|
42
|
+
return 1
|
|
43
|
+
;;
|
|
44
|
+
esac
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
issue_current_baseline_head_sha() {
|
|
48
|
+
local head_sha=""
|
|
49
|
+
if [[ -d "${AGENT_REPO_ROOT}" ]]; then
|
|
50
|
+
head_sha="$(git -C "${AGENT_REPO_ROOT}" rev-parse --verify --quiet "origin/${DEFAULT_BRANCH}" 2>/dev/null || true)"
|
|
51
|
+
if [[ -z "${head_sha}" ]]; then
|
|
52
|
+
head_sha="$(git -C "${AGENT_REPO_ROOT}" rev-parse --verify --quiet "${DEFAULT_BRANCH}" 2>/dev/null || true)"
|
|
53
|
+
fi
|
|
54
|
+
fi
|
|
55
|
+
printf '%s\n' "${head_sha}"
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
issue_record_retry_baseline_gate() {
|
|
59
|
+
local reason="${1:-}"
|
|
60
|
+
local state_file head_sha tmp_file
|
|
61
|
+
|
|
62
|
+
issue_reason_requires_baseline_change "${reason}" || return 0
|
|
63
|
+
state_file="$(issue_retry_state_file)"
|
|
64
|
+
[[ -f "${state_file}" ]] || return 0
|
|
65
|
+
head_sha="$(issue_current_baseline_head_sha)"
|
|
66
|
+
[[ -n "${head_sha}" ]] || return 0
|
|
67
|
+
|
|
68
|
+
tmp_file="$(mktemp)"
|
|
69
|
+
grep -v '^BASELINE_HEAD_SHA=' "${state_file}" >"${tmp_file}" || true
|
|
70
|
+
printf 'BASELINE_HEAD_SHA=%s\n' "${head_sha}" >>"${tmp_file}"
|
|
71
|
+
mv "${tmp_file}" "${state_file}"
|
|
72
|
+
}
|
|
73
|
+
|
|
29
74
|
issue_has_schedule_cadence() {
|
|
30
75
|
local issue_json issue_body
|
|
31
76
|
issue_json="$(flow_github_issue_view_json "${REPO_SLUG}" "${ISSUE_ID}" 2>/dev/null || true)"
|
|
@@ -155,6 +200,7 @@ issue_schedule_retry() {
|
|
|
155
200
|
return 0
|
|
156
201
|
fi
|
|
157
202
|
"${FLOW_TOOLS_DIR}/retry-state.sh" issue "$ISSUE_ID" schedule "$reason" >/dev/null || true
|
|
203
|
+
issue_record_retry_baseline_gate "${reason}"
|
|
158
204
|
}
|
|
159
205
|
|
|
160
206
|
issue_mark_ready() {
|
|
@@ -156,7 +156,7 @@ function createExecutionContext(stage) {
|
|
|
156
156
|
}
|
|
157
157
|
|
|
158
158
|
function runScriptWithContext(context, scriptRelativePath, forwardedArgs, options = {}) {
|
|
159
|
-
const scriptPath = path.join(packageRoot, scriptRelativePath);
|
|
159
|
+
const scriptPath = options.scriptPath || path.join(packageRoot, scriptRelativePath);
|
|
160
160
|
const stdio = options.stdio || "inherit";
|
|
161
161
|
const result = spawnSync("bash", [scriptPath, ...forwardedArgs], {
|
|
162
162
|
stdio,
|
|
@@ -175,11 +175,78 @@ function runScriptWithContext(context, scriptRelativePath, forwardedArgs, option
|
|
|
175
175
|
};
|
|
176
176
|
}
|
|
177
177
|
|
|
178
|
+
function resolvePersistentSourceHome(context) {
|
|
179
|
+
if (process.env.ACP_PROJECT_RUNTIME_SOURCE_HOME) {
|
|
180
|
+
return process.env.ACP_PROJECT_RUNTIME_SOURCE_HOME;
|
|
181
|
+
}
|
|
182
|
+
if (fs.existsSync(path.join(packageRoot, ".git"))) {
|
|
183
|
+
return packageRoot;
|
|
184
|
+
}
|
|
185
|
+
return context.runtimeHome;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function runtimeSkillRoot(context) {
|
|
189
|
+
return path.join(context.runtimeHome, "skills", "openclaw", skillName);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function createRuntimeExecutionContext(context) {
|
|
193
|
+
const stableSkillRoot = runtimeSkillRoot(context);
|
|
194
|
+
const persistentSourceHome = resolvePersistentSourceHome(context);
|
|
195
|
+
const runtimeScriptEnv = {
|
|
196
|
+
ACP_PROJECT_RUNTIME_SYNC_SCRIPT:
|
|
197
|
+
context.env.ACP_PROJECT_RUNTIME_SYNC_SCRIPT || path.join(stableSkillRoot, "tools", "bin", "sync-shared-agent-home.sh"),
|
|
198
|
+
ACP_PROJECT_RUNTIME_ENSURE_SYNC_SCRIPT:
|
|
199
|
+
context.env.ACP_PROJECT_RUNTIME_ENSURE_SYNC_SCRIPT || path.join(stableSkillRoot, "tools", "bin", "ensure-runtime-sync.sh"),
|
|
200
|
+
ACP_PROJECT_RUNTIME_BOOTSTRAP_SCRIPT:
|
|
201
|
+
context.env.ACP_PROJECT_RUNTIME_BOOTSTRAP_SCRIPT || path.join(stableSkillRoot, "tools", "bin", "project-launchd-bootstrap.sh"),
|
|
202
|
+
ACP_PROJECT_RUNTIME_SUPERVISOR_SCRIPT:
|
|
203
|
+
context.env.ACP_PROJECT_RUNTIME_SUPERVISOR_SCRIPT || path.join(stableSkillRoot, "tools", "bin", "project-runtime-supervisor.sh"),
|
|
204
|
+
ACP_PROJECT_RUNTIME_KICK_SCRIPT:
|
|
205
|
+
context.env.ACP_PROJECT_RUNTIME_KICK_SCRIPT || path.join(stableSkillRoot, "tools", "bin", "kick-scheduler.sh")
|
|
206
|
+
};
|
|
207
|
+
return {
|
|
208
|
+
...context,
|
|
209
|
+
stableSkillRoot,
|
|
210
|
+
persistentSourceHome,
|
|
211
|
+
env: {
|
|
212
|
+
...context.env,
|
|
213
|
+
SHARED_AGENT_HOME: context.runtimeHome,
|
|
214
|
+
AGENT_CONTROL_PLANE_ROOT: stableSkillRoot,
|
|
215
|
+
ACP_ROOT: stableSkillRoot,
|
|
216
|
+
AGENT_FLOW_SOURCE_ROOT: stableSkillRoot,
|
|
217
|
+
ACP_PROJECT_INIT_SOURCE_HOME: persistentSourceHome,
|
|
218
|
+
ACP_PROJECT_RUNTIME_SOURCE_HOME: persistentSourceHome,
|
|
219
|
+
ACP_DASHBOARD_SOURCE_HOME: persistentSourceHome,
|
|
220
|
+
...runtimeScriptEnv
|
|
221
|
+
}
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function syncRuntimeHome(context, options = {}) {
|
|
226
|
+
const result = runScriptWithContext(context, "tools/bin/sync-shared-agent-home.sh", [], {
|
|
227
|
+
stdio: options.stdio || "inherit"
|
|
228
|
+
});
|
|
229
|
+
if (result.status !== 0) {
|
|
230
|
+
throw new Error("failed to sync runtime home before command execution");
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
178
234
|
function runCommand(scriptRelativePath, forwardedArgs) {
|
|
179
235
|
const stage = stageSharedHome();
|
|
180
236
|
const context = createExecutionContext(stage);
|
|
181
237
|
|
|
182
238
|
try {
|
|
239
|
+
if (scriptRelativePath !== "tools/bin/sync-shared-agent-home.sh") {
|
|
240
|
+
syncRuntimeHome(context, { stdio: "inherit" });
|
|
241
|
+
const runtimeContext = createRuntimeExecutionContext(context);
|
|
242
|
+
const runtimeScriptPath = path.join(runtimeContext.stableSkillRoot, scriptRelativePath);
|
|
243
|
+
const result = runScriptWithContext(runtimeContext, scriptRelativePath, forwardedArgs, {
|
|
244
|
+
stdio: "inherit",
|
|
245
|
+
scriptPath: runtimeScriptPath
|
|
246
|
+
});
|
|
247
|
+
return result.status;
|
|
248
|
+
}
|
|
249
|
+
|
|
183
250
|
const result = runScriptWithContext(context, scriptRelativePath, forwardedArgs, { stdio: "inherit" });
|
|
184
251
|
return result.status;
|
|
185
252
|
} finally {
|
|
@@ -1470,8 +1537,8 @@ async function maybeRunFinalSetupFixups(options, scopedContext, config, currentS
|
|
|
1470
1537
|
runtimeStartReason = `doctor-${doctorKv.DOCTOR_STATUS || "not-ok"}`;
|
|
1471
1538
|
} else {
|
|
1472
1539
|
actions.push("runtime-start");
|
|
1473
|
-
runSetupStep(scopedContext, "Start the runtime", "tools/bin/project-runtimectl.sh", ["start", "--profile-id", config.profileId]);
|
|
1474
|
-
const runtimeStatusOutput = runSetupStep(scopedContext, "Read back runtime status", "tools/bin/project-runtimectl.sh", ["status", "--profile-id", config.profileId]);
|
|
1540
|
+
runSetupStep(scopedContext, "Start the runtime", "tools/bin/project-runtimectl.sh", ["start", "--profile-id", config.profileId], { useRuntimeCopy: true });
|
|
1541
|
+
const runtimeStatusOutput = runSetupStep(scopedContext, "Read back runtime status", "tools/bin/project-runtimectl.sh", ["status", "--profile-id", config.profileId], { useRuntimeCopy: true });
|
|
1475
1542
|
runtimeStatusKv = parseKvOutput(runtimeStatusOutput);
|
|
1476
1543
|
runtimeStartStatus = "ok";
|
|
1477
1544
|
runtimeStartReason = "";
|
|
@@ -1480,7 +1547,7 @@ async function maybeRunFinalSetupFixups(options, scopedContext, config, currentS
|
|
|
1480
1547
|
|
|
1481
1548
|
if (config.installLaunchd && process.platform === "darwin" && launchdInstallStatus !== "ok" && runtimeStartStatus === "ok") {
|
|
1482
1549
|
actions.push("launchd-install");
|
|
1483
|
-
runSetupStep(scopedContext, "Install macOS autostart", "tools/bin/install-project-launchd.sh", ["--profile-id", config.profileId]);
|
|
1550
|
+
runSetupStep(scopedContext, "Install macOS autostart", "tools/bin/install-project-launchd.sh", ["--profile-id", config.profileId], { useRuntimeCopy: true });
|
|
1484
1551
|
launchdInstallStatus = "ok";
|
|
1485
1552
|
launchdInstallReason = "";
|
|
1486
1553
|
}
|
|
@@ -1603,7 +1670,21 @@ async function collectSetupConfig(options, context) {
|
|
|
1603
1670
|
|
|
1604
1671
|
function runSetupStep(context, title, scriptRelativePath, args, options = {}) {
|
|
1605
1672
|
console.log(`\n== ${title} ==`);
|
|
1606
|
-
|
|
1673
|
+
let executionContext = context;
|
|
1674
|
+
let scriptPath = undefined;
|
|
1675
|
+
|
|
1676
|
+
if (options.useRuntimeCopy) {
|
|
1677
|
+
syncRuntimeHome(context, { stdio: "pipe" });
|
|
1678
|
+
executionContext = createRuntimeExecutionContext(context);
|
|
1679
|
+
scriptPath = path.join(executionContext.stableSkillRoot, scriptRelativePath);
|
|
1680
|
+
}
|
|
1681
|
+
|
|
1682
|
+
const result = runScriptWithContext(executionContext, scriptRelativePath, args, {
|
|
1683
|
+
stdio: "pipe",
|
|
1684
|
+
env: options.env,
|
|
1685
|
+
cwd: options.cwd,
|
|
1686
|
+
scriptPath
|
|
1687
|
+
});
|
|
1607
1688
|
if (result.status !== 0) {
|
|
1608
1689
|
printFailureDetails(result);
|
|
1609
1690
|
throw new Error(`${title} failed`);
|
|
@@ -1868,8 +1949,8 @@ async function runSetupFlow(forwardedArgs) {
|
|
|
1868
1949
|
runtimeStartReason = "gh-auth-not-ready";
|
|
1869
1950
|
console.log("runtime start skipped: GitHub CLI is not authenticated yet. Run `gh auth login` and start the runtime afterwards.");
|
|
1870
1951
|
} else {
|
|
1871
|
-
runSetupStep(scopedContext, "Start the runtime", "tools/bin/project-runtimectl.sh", ["start", "--profile-id", config.profileId]);
|
|
1872
|
-
const runtimeStatusOutput = runSetupStep(scopedContext, "Read back runtime status", "tools/bin/project-runtimectl.sh", ["status", "--profile-id", config.profileId]);
|
|
1952
|
+
runSetupStep(scopedContext, "Start the runtime", "tools/bin/project-runtimectl.sh", ["start", "--profile-id", config.profileId], { useRuntimeCopy: true });
|
|
1953
|
+
const runtimeStatusOutput = runSetupStep(scopedContext, "Read back runtime status", "tools/bin/project-runtimectl.sh", ["status", "--profile-id", config.profileId], { useRuntimeCopy: true });
|
|
1873
1954
|
runtimeStatusKv = parseKvOutput(runtimeStatusOutput);
|
|
1874
1955
|
runtimeStartStatus = "ok";
|
|
1875
1956
|
runtimeStartReason = "";
|
|
@@ -1886,7 +1967,7 @@ async function runSetupFlow(forwardedArgs) {
|
|
|
1886
1967
|
launchdInstallReason = "runtime-not-started";
|
|
1887
1968
|
console.log("launchd install skipped: runtime was not started successfully in this setup run.");
|
|
1888
1969
|
} else {
|
|
1889
|
-
runSetupStep(scopedContext, "Install macOS autostart", "tools/bin/install-project-launchd.sh", ["--profile-id", config.profileId]);
|
|
1970
|
+
runSetupStep(scopedContext, "Install macOS autostart", "tools/bin/install-project-launchd.sh", ["--profile-id", config.profileId], { useRuntimeCopy: true });
|
|
1890
1971
|
launchdInstallStatus = "ok";
|
|
1891
1972
|
launchdInstallReason = "";
|
|
1892
1973
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-control-plane",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.13",
|
|
4
4
|
"description": "Help a repo keep GitHub-driven coding agents running reliably without constant human babysitting",
|
|
5
5
|
"homepage": "https://github.com/ducminhnguyen0319/agent-control-plane",
|
|
6
6
|
"bugs": {
|
|
@@ -21,11 +21,17 @@
|
|
|
21
21
|
"README.md",
|
|
22
22
|
"SKILL.md",
|
|
23
23
|
"assets/workflow-catalog.json",
|
|
24
|
-
"bin",
|
|
24
|
+
"bin/agent-control-plane",
|
|
25
|
+
"bin/issue-resource-class.sh",
|
|
26
|
+
"bin/label-follow-up-issues.sh",
|
|
27
|
+
"bin/pr-risk.sh",
|
|
28
|
+
"bin/sync-pr-labels.sh",
|
|
25
29
|
"hooks",
|
|
26
30
|
"npm/bin",
|
|
27
31
|
"references",
|
|
28
32
|
"tools/bin",
|
|
33
|
+
"!tools/bin/audit-*.sh",
|
|
34
|
+
"!tools/bin/check-skill-contracts.sh",
|
|
29
35
|
"tools/dashboard/app.js",
|
|
30
36
|
"tools/dashboard/dashboard_snapshot.py",
|
|
31
37
|
"tools/dashboard/index.html",
|
package/references/commands.md
CHANGED
|
@@ -76,6 +76,7 @@ tools/bin/profile-smoke.sh
|
|
|
76
76
|
tools/bin/test-smoke.sh
|
|
77
77
|
tools/bin/profile-adopt.sh --profile-id <id>
|
|
78
78
|
tools/bin/project-runtimectl.sh status --profile-id <id>
|
|
79
|
+
tools/bin/project-runtimectl.sh sync --profile-id <id>
|
|
79
80
|
tools/bin/project-runtimectl.sh stop --profile-id <id>
|
|
80
81
|
tools/bin/project-runtimectl.sh start --profile-id <id>
|
|
81
82
|
tools/bin/project-runtimectl.sh restart --profile-id <id>
|
|
@@ -15,6 +15,7 @@ shared_agent_home="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
|
|
15
15
|
repo_root="${AGENT_PROJECT_REPO_ROOT:-}"
|
|
16
16
|
runs_root="${AGENT_PROJECT_RUNS_ROOT:-}"
|
|
17
17
|
history_root="${AGENT_PROJECT_HISTORY_ROOT:-}"
|
|
18
|
+
state_root="${AGENT_PROJECT_STATE_ROOT:-${F_LOSNING_STATE_ROOT:-}}"
|
|
18
19
|
session=""
|
|
19
20
|
worktree_path=""
|
|
20
21
|
mode="generic"
|
|
@@ -30,6 +31,7 @@ while [[ $# -gt 0 ]]; do
|
|
|
30
31
|
--repo-root) repo_root="${2:-}"; shift 2 ;;
|
|
31
32
|
--runs-root) runs_root="${2:-}"; shift 2 ;;
|
|
32
33
|
--history-root) history_root="${2:-}"; shift 2 ;;
|
|
34
|
+
--state-root) state_root="${2:-}"; shift 2 ;;
|
|
33
35
|
--session) session="${2:-}"; shift 2 ;;
|
|
34
36
|
--worktree) worktree_path="${2:-}"; shift 2 ;;
|
|
35
37
|
--mode) mode="${2:-}"; shift 2 ;;
|
|
@@ -141,6 +143,131 @@ path_is_within_root() {
|
|
|
141
143
|
[[ "$target_canonical" == "$root_canonical" || "$target_canonical" == "$root_canonical"/* ]]
|
|
142
144
|
}
|
|
143
145
|
|
|
146
|
+
derive_state_root() {
|
|
147
|
+
if [[ -n "${state_root}" && -d "${state_root}" ]]; then
|
|
148
|
+
printf '%s\n' "${state_root}"
|
|
149
|
+
return 0
|
|
150
|
+
fi
|
|
151
|
+
|
|
152
|
+
if [[ -n "${runs_root}" && -d "${runs_root}" ]]; then
|
|
153
|
+
local candidate_root=""
|
|
154
|
+
candidate_root="$(cd "${runs_root}/.." 2>/dev/null && pwd -P || true)"
|
|
155
|
+
if [[ -n "${candidate_root}" && -d "${candidate_root}/state" ]]; then
|
|
156
|
+
printf '%s/state\n' "${candidate_root}"
|
|
157
|
+
return 0
|
|
158
|
+
fi
|
|
159
|
+
fi
|
|
160
|
+
|
|
161
|
+
return 1
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
resident_worktree_protected() {
|
|
165
|
+
local candidate_path="${1:-}"
|
|
166
|
+
local resolved_candidate=""
|
|
167
|
+
local resolved_state_root=""
|
|
168
|
+
local metadata_file=""
|
|
169
|
+
local resident_worktree=""
|
|
170
|
+
local resident_realpath=""
|
|
171
|
+
local resolved_resident=""
|
|
172
|
+
|
|
173
|
+
[[ -n "${candidate_path}" && -d "${candidate_path}" ]] || return 1
|
|
174
|
+
resolved_candidate="$(canonicalize_existing_dir "${candidate_path}" || true)"
|
|
175
|
+
[[ -n "${resolved_candidate}" ]] || return 1
|
|
176
|
+
|
|
177
|
+
resolved_state_root="$(derive_state_root || true)"
|
|
178
|
+
[[ -n "${resolved_state_root}" && -d "${resolved_state_root}/resident-workers/issues" ]] || return 1
|
|
179
|
+
|
|
180
|
+
for metadata_file in "${resolved_state_root}"/resident-workers/issues/*/metadata.env; do
|
|
181
|
+
[[ -f "${metadata_file}" ]] || continue
|
|
182
|
+
resident_worktree=""
|
|
183
|
+
resident_realpath=""
|
|
184
|
+
set +u
|
|
185
|
+
set -a
|
|
186
|
+
# shellcheck source=/dev/null
|
|
187
|
+
source "${metadata_file}"
|
|
188
|
+
set +a
|
|
189
|
+
set -u
|
|
190
|
+
resident_worktree="${WORKTREE_REALPATH:-${WORKTREE:-}}"
|
|
191
|
+
[[ -n "${resident_worktree}" && -d "${resident_worktree}" ]] || continue
|
|
192
|
+
resolved_resident="$(canonicalize_existing_dir "${resident_worktree}" || true)"
|
|
193
|
+
[[ -n "${resolved_resident}" ]] || continue
|
|
194
|
+
if [[ "${resolved_candidate}" == "${resolved_resident}" ]]; then
|
|
195
|
+
return 0
|
|
196
|
+
fi
|
|
197
|
+
done
|
|
198
|
+
|
|
199
|
+
return 1
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
active_resident_run_worktree_protected() {
|
|
203
|
+
local candidate_path="${1:-}"
|
|
204
|
+
local resolved_candidate=""
|
|
205
|
+
local run_meta=""
|
|
206
|
+
local run_dir=""
|
|
207
|
+
local session_name=""
|
|
208
|
+
local resident_enabled=""
|
|
209
|
+
local resident_worktree=""
|
|
210
|
+
local runner_state_file=""
|
|
211
|
+
local runner_state=""
|
|
212
|
+
local active_session="false"
|
|
213
|
+
local resolved_resident=""
|
|
214
|
+
|
|
215
|
+
[[ -n "${candidate_path}" && -d "${candidate_path}" ]] || return 1
|
|
216
|
+
[[ -n "${runs_root}" && -d "${runs_root}" ]] || return 1
|
|
217
|
+
|
|
218
|
+
resolved_candidate="$(canonicalize_existing_dir "${candidate_path}" || true)"
|
|
219
|
+
[[ -n "${resolved_candidate}" ]] || return 1
|
|
220
|
+
|
|
221
|
+
for run_meta in "${runs_root}"/*/run.env; do
|
|
222
|
+
[[ -f "${run_meta}" ]] || continue
|
|
223
|
+
|
|
224
|
+
session_name=""
|
|
225
|
+
resident_enabled=""
|
|
226
|
+
resident_worktree=""
|
|
227
|
+
runner_state=""
|
|
228
|
+
active_session="false"
|
|
229
|
+
|
|
230
|
+
set +u
|
|
231
|
+
set -a
|
|
232
|
+
# shellcheck source=/dev/null
|
|
233
|
+
source "${run_meta}"
|
|
234
|
+
set +a
|
|
235
|
+
set -u
|
|
236
|
+
|
|
237
|
+
resident_enabled="${RESIDENT_WORKER_ENABLED:-no}"
|
|
238
|
+
[[ "${resident_enabled}" == "yes" ]] || continue
|
|
239
|
+
|
|
240
|
+
resident_worktree="${WORKTREE_REALPATH:-${WORKTREE:-}}"
|
|
241
|
+
[[ -n "${resident_worktree}" && -d "${resident_worktree}" ]] || continue
|
|
242
|
+
resolved_resident="$(canonicalize_existing_dir "${resident_worktree}" || true)"
|
|
243
|
+
[[ -n "${resolved_resident}" ]] || continue
|
|
244
|
+
[[ "${resolved_candidate}" == "${resolved_resident}" ]] || continue
|
|
245
|
+
|
|
246
|
+
run_dir="$(dirname "${run_meta}")"
|
|
247
|
+
runner_state_file="${run_dir}/runner.env"
|
|
248
|
+
if [[ -f "${runner_state_file}" ]]; then
|
|
249
|
+
set +u
|
|
250
|
+
set -a
|
|
251
|
+
# shellcheck source=/dev/null
|
|
252
|
+
source "${runner_state_file}"
|
|
253
|
+
set +a
|
|
254
|
+
set -u
|
|
255
|
+
runner_state="${RUNNER_STATE:-}"
|
|
256
|
+
fi
|
|
257
|
+
|
|
258
|
+
session_name="${SESSION:-}"
|
|
259
|
+
if [[ -n "${session_name}" ]] && tmux has-session -t "${session_name}" 2>/dev/null; then
|
|
260
|
+
active_session="true"
|
|
261
|
+
fi
|
|
262
|
+
|
|
263
|
+
if [[ "${runner_state}" == "running" || "${active_session}" == "true" ]]; then
|
|
264
|
+
return 0
|
|
265
|
+
fi
|
|
266
|
+
done
|
|
267
|
+
|
|
268
|
+
return 1
|
|
269
|
+
}
|
|
270
|
+
|
|
144
271
|
cleanup_with_branch_tool() {
|
|
145
272
|
local include_path="${1:-yes}"
|
|
146
273
|
local -a cleanup_args
|
|
@@ -185,6 +312,9 @@ cleanup_orphan_worktree_dir() {
|
|
|
185
312
|
|
|
186
313
|
if [[ "$active_tmux_session" == "true" ]]; then
|
|
187
314
|
cleanup_mode="deferred-active-session"
|
|
315
|
+
elif [[ "$skip_worktree_cleanup" != "true" && -n "${worktree_path}" ]] \
|
|
316
|
+
&& { resident_worktree_protected "${worktree_path}" || active_resident_run_worktree_protected "${worktree_path}"; }; then
|
|
317
|
+
cleanup_mode="protected-resident-worktree"
|
|
188
318
|
elif [[ "$skip_worktree_cleanup" != "true" && -n "$branch_name" ]]; then
|
|
189
319
|
if cleanup_output="$(cleanup_with_branch_tool yes 2>&1)"; then
|
|
190
320
|
cleanup_mode="branch"
|
|
@@ -207,6 +337,9 @@ elif [[ "$skip_worktree_cleanup" != "true" && -n "$worktree_path" ]] && git -C "
|
|
|
207
337
|
git -C "$repo_root" worktree remove "$worktree_path" --force || true
|
|
208
338
|
git -C "$repo_root" worktree prune
|
|
209
339
|
cleanup_mode="worktree"
|
|
340
|
+
elif [[ "$skip_worktree_cleanup" != "true" ]] && cleanup_orphan_worktree_dir; then
|
|
341
|
+
orphan_fallback_used="true"
|
|
342
|
+
cleanup_mode="orphan-worktree"
|
|
210
343
|
elif [[ "$skip_worktree_cleanup" == "true" ]]; then
|
|
211
344
|
cleanup_mode="archived-only"
|
|
212
345
|
fi
|