shipwright-cli 2.4.0 → 3.0.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.
- package/README.md +16 -11
- package/completions/_shipwright +1 -1
- package/completions/shipwright.bash +3 -8
- package/completions/shipwright.fish +1 -1
- package/config/defaults.json +111 -0
- package/config/event-schema.json +81 -0
- package/config/policy.json +13 -18
- package/dashboard/coverage/coverage-summary.json +14 -0
- package/dashboard/public/index.html +1 -1
- package/dashboard/server.ts +306 -17
- package/dashboard/src/components/charts/bar.test.ts +79 -0
- package/dashboard/src/components/charts/donut.test.ts +68 -0
- package/dashboard/src/components/charts/pipeline-rail.test.ts +117 -0
- package/dashboard/src/components/charts/sparkline.test.ts +125 -0
- package/dashboard/src/core/api.test.ts +309 -0
- package/dashboard/src/core/helpers.test.ts +301 -0
- package/dashboard/src/core/router.test.ts +307 -0
- package/dashboard/src/core/router.ts +7 -0
- package/dashboard/src/core/sse.test.ts +144 -0
- package/dashboard/src/views/metrics.test.ts +186 -0
- package/dashboard/src/views/overview.test.ts +173 -0
- package/dashboard/src/views/pipelines.test.ts +183 -0
- package/dashboard/src/views/team.test.ts +253 -0
- package/dashboard/vitest.config.ts +14 -5
- package/docs/TIPS.md +1 -1
- package/docs/patterns/README.md +1 -1
- package/package.json +5 -7
- package/scripts/adapters/docker-deploy.sh +1 -1
- package/scripts/adapters/tmux-adapter.sh +11 -1
- package/scripts/adapters/wezterm-adapter.sh +1 -1
- package/scripts/check-version-consistency.sh +1 -1
- package/scripts/lib/architecture.sh +126 -0
- package/scripts/lib/bootstrap.sh +75 -0
- package/scripts/lib/compat.sh +89 -6
- package/scripts/lib/config.sh +91 -0
- package/scripts/lib/daemon-adaptive.sh +3 -3
- package/scripts/lib/daemon-dispatch.sh +39 -16
- package/scripts/lib/daemon-health.sh +1 -1
- package/scripts/lib/daemon-patrol.sh +24 -12
- package/scripts/lib/daemon-poll.sh +37 -25
- package/scripts/lib/daemon-state.sh +115 -23
- package/scripts/lib/daemon-triage.sh +30 -8
- package/scripts/lib/fleet-failover.sh +63 -0
- package/scripts/lib/helpers.sh +30 -6
- package/scripts/lib/pipeline-detection.sh +2 -2
- package/scripts/lib/pipeline-github.sh +9 -9
- package/scripts/lib/pipeline-intelligence.sh +85 -35
- package/scripts/lib/pipeline-quality-checks.sh +16 -16
- package/scripts/lib/pipeline-quality.sh +1 -1
- package/scripts/lib/pipeline-stages.sh +242 -28
- package/scripts/lib/pipeline-state.sh +40 -4
- package/scripts/lib/test-helpers.sh +247 -0
- package/scripts/postinstall.mjs +3 -11
- package/scripts/sw +10 -4
- package/scripts/sw-activity.sh +1 -11
- package/scripts/sw-adaptive.sh +109 -85
- package/scripts/sw-adversarial.sh +4 -14
- package/scripts/sw-architecture-enforcer.sh +1 -11
- package/scripts/sw-auth.sh +8 -17
- package/scripts/sw-autonomous.sh +111 -49
- package/scripts/sw-changelog.sh +1 -11
- package/scripts/sw-checkpoint.sh +144 -20
- package/scripts/sw-ci.sh +2 -12
- package/scripts/sw-cleanup.sh +13 -17
- package/scripts/sw-code-review.sh +16 -36
- package/scripts/sw-connect.sh +5 -12
- package/scripts/sw-context.sh +9 -26
- package/scripts/sw-cost.sh +6 -16
- package/scripts/sw-daemon.sh +75 -70
- package/scripts/sw-dashboard.sh +57 -17
- package/scripts/sw-db.sh +506 -15
- package/scripts/sw-decompose.sh +1 -11
- package/scripts/sw-deps.sh +15 -25
- package/scripts/sw-developer-simulation.sh +1 -11
- package/scripts/sw-discovery.sh +112 -30
- package/scripts/sw-doc-fleet.sh +7 -17
- package/scripts/sw-docs-agent.sh +6 -16
- package/scripts/sw-docs.sh +4 -12
- package/scripts/sw-doctor.sh +134 -43
- package/scripts/sw-dora.sh +11 -19
- package/scripts/sw-durable.sh +35 -52
- package/scripts/sw-e2e-orchestrator.sh +11 -27
- package/scripts/sw-eventbus.sh +115 -115
- package/scripts/sw-evidence.sh +114 -30
- package/scripts/sw-feedback.sh +3 -13
- package/scripts/sw-fix.sh +2 -20
- package/scripts/sw-fleet-discover.sh +1 -11
- package/scripts/sw-fleet-viz.sh +10 -18
- package/scripts/sw-fleet.sh +13 -17
- package/scripts/sw-github-app.sh +6 -16
- package/scripts/sw-github-checks.sh +1 -11
- package/scripts/sw-github-deploy.sh +1 -11
- package/scripts/sw-github-graphql.sh +2 -12
- package/scripts/sw-guild.sh +1 -11
- package/scripts/sw-heartbeat.sh +49 -12
- package/scripts/sw-hygiene.sh +45 -43
- package/scripts/sw-incident.sh +48 -74
- package/scripts/sw-init.sh +35 -37
- package/scripts/sw-instrument.sh +1 -11
- package/scripts/sw-intelligence.sh +362 -51
- package/scripts/sw-jira.sh +5 -14
- package/scripts/sw-launchd.sh +2 -12
- package/scripts/sw-linear.sh +8 -17
- package/scripts/sw-logs.sh +4 -12
- package/scripts/sw-loop.sh +641 -90
- package/scripts/sw-memory.sh +243 -17
- package/scripts/sw-mission-control.sh +2 -12
- package/scripts/sw-model-router.sh +73 -34
- package/scripts/sw-otel.sh +11 -21
- package/scripts/sw-oversight.sh +1 -11
- package/scripts/sw-patrol-meta.sh +5 -11
- package/scripts/sw-pipeline-composer.sh +7 -17
- package/scripts/sw-pipeline-vitals.sh +1 -11
- package/scripts/sw-pipeline.sh +478 -122
- package/scripts/sw-pm.sh +2 -12
- package/scripts/sw-pr-lifecycle.sh +27 -25
- package/scripts/sw-predictive.sh +16 -22
- package/scripts/sw-prep.sh +6 -16
- package/scripts/sw-ps.sh +1 -11
- package/scripts/sw-public-dashboard.sh +2 -12
- package/scripts/sw-quality.sh +77 -10
- package/scripts/sw-reaper.sh +1 -11
- package/scripts/sw-recruit.sh +15 -25
- package/scripts/sw-regression.sh +11 -21
- package/scripts/sw-release-manager.sh +19 -28
- package/scripts/sw-release.sh +8 -16
- package/scripts/sw-remote.sh +1 -11
- package/scripts/sw-replay.sh +48 -44
- package/scripts/sw-retro.sh +70 -92
- package/scripts/sw-review-rerun.sh +1 -1
- package/scripts/sw-scale.sh +109 -32
- package/scripts/sw-security-audit.sh +12 -22
- package/scripts/sw-self-optimize.sh +239 -23
- package/scripts/sw-session.sh +3 -13
- package/scripts/sw-setup.sh +8 -18
- package/scripts/sw-standup.sh +5 -15
- package/scripts/sw-status.sh +32 -23
- package/scripts/sw-strategic.sh +129 -13
- package/scripts/sw-stream.sh +1 -11
- package/scripts/sw-swarm.sh +76 -36
- package/scripts/sw-team-stages.sh +10 -20
- package/scripts/sw-templates.sh +4 -14
- package/scripts/sw-testgen.sh +3 -13
- package/scripts/sw-tmux-pipeline.sh +1 -19
- package/scripts/sw-tmux-role-color.sh +0 -10
- package/scripts/sw-tmux-status.sh +3 -11
- package/scripts/sw-tmux.sh +2 -20
- package/scripts/sw-trace.sh +1 -19
- package/scripts/sw-tracker-github.sh +0 -10
- package/scripts/sw-tracker-jira.sh +1 -11
- package/scripts/sw-tracker-linear.sh +1 -11
- package/scripts/sw-tracker.sh +7 -24
- package/scripts/sw-triage.sh +24 -34
- package/scripts/sw-upgrade.sh +5 -23
- package/scripts/sw-ux.sh +1 -19
- package/scripts/sw-webhook.sh +18 -32
- package/scripts/sw-widgets.sh +3 -21
- package/scripts/sw-worktree.sh +11 -27
- package/scripts/update-homebrew-sha.sh +67 -0
- package/templates/pipelines/tdd.json +72 -0
- package/scripts/sw-pipeline.sh.mock +0 -7
|
@@ -53,7 +53,7 @@ daemon_poll_issues() {
|
|
|
53
53
|
--owner "$ORG" \
|
|
54
54
|
--state open \
|
|
55
55
|
--json repository,number,title,labels,body,createdAt \
|
|
56
|
-
--limit
|
|
56
|
+
--limit "${ISSUE_LIMIT:-100}" 2>/dev/null) || {
|
|
57
57
|
# Handle rate limiting with exponential backoff
|
|
58
58
|
if [[ $BACKOFF_SECS -eq 0 ]]; then
|
|
59
59
|
BACKOFF_SECS=30
|
|
@@ -80,7 +80,7 @@ daemon_poll_issues() {
|
|
|
80
80
|
--label "$WATCH_LABEL" \
|
|
81
81
|
--state open \
|
|
82
82
|
--json number,title,labels,body,createdAt \
|
|
83
|
-
--limit
|
|
83
|
+
--limit 100 2>/dev/null) || {
|
|
84
84
|
# Handle rate limiting with exponential backoff
|
|
85
85
|
if [[ $BACKOFF_SECS -eq 0 ]]; then
|
|
86
86
|
BACKOFF_SECS=30
|
|
@@ -212,18 +212,22 @@ daemon_poll_issues() {
|
|
|
212
212
|
while IFS='|' read -r score issue_num repo_name; do
|
|
213
213
|
[[ -z "$issue_num" ]] && continue
|
|
214
214
|
|
|
215
|
+
local issue_key
|
|
216
|
+
issue_key="$issue_num"
|
|
217
|
+
[[ -n "$repo_name" ]] && issue_key="${repo_name}:${issue_num}"
|
|
218
|
+
|
|
215
219
|
local issue_title labels_csv
|
|
216
|
-
issue_title=$(echo "$issues_json" | jq -r --argjson n "$issue_num" '.[] | select(.number == $n) | .title')
|
|
217
|
-
labels_csv=$(echo "$issues_json" | jq -r --argjson n "$issue_num" '.[] | select(.number == $n) | [.labels[].name] | join(",")')
|
|
220
|
+
issue_title=$(echo "$issues_json" | jq -r --argjson n "$issue_num" --arg repo "$repo_name" '.[] | select(.number == $n) | select($repo == "" or (.repository.nameWithOwner // "") == $repo) | .title')
|
|
221
|
+
labels_csv=$(echo "$issues_json" | jq -r --argjson n "$issue_num" --arg repo "$repo_name" '.[] | select(.number == $n) | select($repo == "" or (.repository.nameWithOwner // "") == $repo) | [.labels[].name] | join(",")')
|
|
218
222
|
|
|
219
|
-
# Cache title in state for dashboard visibility
|
|
223
|
+
# Cache title in state for dashboard visibility (use issue_key for org mode)
|
|
220
224
|
if [[ -n "$issue_title" ]]; then
|
|
221
|
-
locked_state_update --arg num "$
|
|
225
|
+
locked_state_update --arg num "$issue_key" --arg title "$issue_title" \
|
|
222
226
|
'.titles[$num] = $title'
|
|
223
227
|
fi
|
|
224
228
|
|
|
225
229
|
# Skip if already inflight
|
|
226
|
-
if daemon_is_inflight "$
|
|
230
|
+
if daemon_is_inflight "$issue_key"; then
|
|
227
231
|
continue
|
|
228
232
|
fi
|
|
229
233
|
|
|
@@ -263,7 +267,7 @@ daemon_poll_issues() {
|
|
|
263
267
|
# Check capacity
|
|
264
268
|
active_count=$(locked_get_active_count)
|
|
265
269
|
if [[ "$active_count" -ge "$MAX_PARALLEL" ]]; then
|
|
266
|
-
enqueue_issue "$
|
|
270
|
+
enqueue_issue "$issue_key"
|
|
267
271
|
continue
|
|
268
272
|
fi
|
|
269
273
|
|
|
@@ -308,24 +312,26 @@ daemon_poll_issues() {
|
|
|
308
312
|
local drain_active
|
|
309
313
|
drain_active=$(locked_get_active_count)
|
|
310
314
|
while [[ "$drain_active" -lt "$MAX_PARALLEL" ]]; do
|
|
311
|
-
local
|
|
312
|
-
|
|
313
|
-
[[ -z "$
|
|
315
|
+
local drain_issue_key
|
|
316
|
+
drain_issue_key=$(dequeue_next)
|
|
317
|
+
[[ -z "$drain_issue_key" ]] && break
|
|
318
|
+
local drain_issue_num="$drain_issue_key" drain_repo=""
|
|
319
|
+
[[ "$drain_issue_key" == *:* ]] && drain_repo="${drain_issue_key%%:*}" && drain_issue_num="${drain_issue_key##*:}"
|
|
314
320
|
local drain_title
|
|
315
|
-
drain_title=$(jq -r --arg n "$
|
|
321
|
+
drain_title=$(jq -r --arg n "$drain_issue_key" '.titles[$n] // ""' "$STATE_FILE" 2>/dev/null || true)
|
|
316
322
|
|
|
317
323
|
local drain_labels drain_score drain_template
|
|
318
|
-
drain_labels=$(echo "$issues_json" | jq -r --argjson n "$
|
|
319
|
-
'.[] | select(.number == $n) | [.labels[].name] | join(",")' 2>/dev/null || echo "")
|
|
320
|
-
drain_score=$(echo "$sorted_order" | grep "|${
|
|
324
|
+
drain_labels=$(echo "$issues_json" | jq -r --argjson n "$drain_issue_num" --arg repo "$drain_repo" \
|
|
325
|
+
'.[] | select(.number == $n) | select($repo == "" or (.repository.nameWithOwner // "") == $repo) | [.labels[].name] | join(",")' 2>/dev/null || echo "")
|
|
326
|
+
drain_score=$(echo "$sorted_order" | grep "|${drain_issue_num}|" | cut -d'|' -f1 || echo "50")
|
|
321
327
|
drain_template=$(select_pipeline_template "$drain_labels" "${drain_score:-50}" 2>/dev/null | tail -1)
|
|
322
328
|
drain_template=$(printf '%s' "$drain_template" | sed $'s/\x1b\\[[0-9;]*m//g' | tr -cd '[:alnum:]-_')
|
|
323
329
|
[[ -z "$drain_template" ]] && drain_template="$PIPELINE_TEMPLATE"
|
|
324
330
|
|
|
325
|
-
daemon_log INFO "Draining queue: issue #${
|
|
331
|
+
daemon_log INFO "Draining queue: issue #${drain_issue_num}${drain_repo:+, repo=${drain_repo}}, template=${drain_template}"
|
|
326
332
|
local orig_template="$PIPELINE_TEMPLATE"
|
|
327
333
|
PIPELINE_TEMPLATE="$drain_template"
|
|
328
|
-
daemon_spawn_pipeline "$
|
|
334
|
+
daemon_spawn_pipeline "$drain_issue_num" "$drain_title" "$drain_repo"
|
|
329
335
|
PIPELINE_TEMPLATE="$orig_template"
|
|
330
336
|
drain_active=$(locked_get_active_count)
|
|
331
337
|
done
|
|
@@ -692,7 +698,7 @@ daemon_auto_scale() {
|
|
|
692
698
|
|
|
693
699
|
# ── Vitals-driven scaling factor ──
|
|
694
700
|
local max_by_vitals="$MAX_WORKERS"
|
|
695
|
-
if type pipeline_compute_vitals
|
|
701
|
+
if type pipeline_compute_vitals >/dev/null 2>&1 && [[ -f "$STATE_FILE" ]]; then
|
|
696
702
|
local _total_health=0 _health_count=0
|
|
697
703
|
while IFS= read -r _job; do
|
|
698
704
|
local _job_issue _job_worktree
|
|
@@ -813,7 +819,7 @@ daemon_self_optimize() {
|
|
|
813
819
|
fi
|
|
814
820
|
|
|
815
821
|
# ── Intelligence-powered optimization (if enabled) ──
|
|
816
|
-
if [[ "${OPTIMIZATION_ENABLED:-false}" == "true" ]] && type optimize_full_analysis
|
|
822
|
+
if [[ "${OPTIMIZATION_ENABLED:-false}" == "true" ]] && type optimize_full_analysis >/dev/null 2>&1; then
|
|
817
823
|
daemon_log INFO "Running intelligence-powered optimization"
|
|
818
824
|
optimize_full_analysis 2>/dev/null || {
|
|
819
825
|
daemon_log WARN "Intelligence optimization failed — falling back to DORA-based tuning"
|
|
@@ -968,7 +974,7 @@ daemon_cleanup_stale() {
|
|
|
968
974
|
now_e=$(now_epoch)
|
|
969
975
|
|
|
970
976
|
# ── 1. Clean old git worktrees ──
|
|
971
|
-
if command -v git
|
|
977
|
+
if command -v git >/dev/null 2>&1; then
|
|
972
978
|
while IFS= read -r line; do
|
|
973
979
|
local wt_path
|
|
974
980
|
wt_path=$(echo "$line" | awk '{print $1}')
|
|
@@ -976,7 +982,7 @@ daemon_cleanup_stale() {
|
|
|
976
982
|
[[ "$wt_path" == *"daemon-issue-"* ]] || continue
|
|
977
983
|
# Check worktree age via directory mtime
|
|
978
984
|
local mtime
|
|
979
|
-
mtime=$(
|
|
985
|
+
mtime=$(file_mtime "$wt_path")
|
|
980
986
|
if [[ $((now_e - mtime)) -gt $age_secs ]]; then
|
|
981
987
|
daemon_log INFO "Removing stale worktree: ${wt_path}"
|
|
982
988
|
git worktree remove "$wt_path" --force 2>/dev/null || true
|
|
@@ -1003,7 +1009,7 @@ daemon_cleanup_stale() {
|
|
|
1003
1009
|
while IFS= read -r artifact_dir; do
|
|
1004
1010
|
[[ -d "$artifact_dir" ]] || continue
|
|
1005
1011
|
local mtime
|
|
1006
|
-
mtime=$(
|
|
1012
|
+
mtime=$(file_mtime "$artifact_dir")
|
|
1007
1013
|
if [[ $((now_e - mtime)) -gt $age_secs ]]; then
|
|
1008
1014
|
daemon_log INFO "Removing stale artifact: ${artifact_dir}"
|
|
1009
1015
|
rm -rf "$artifact_dir"
|
|
@@ -1013,7 +1019,7 @@ daemon_cleanup_stale() {
|
|
|
1013
1019
|
fi
|
|
1014
1020
|
|
|
1015
1021
|
# ── 3. Clean orphaned daemon/* branches (no matching worktree or active job) ──
|
|
1016
|
-
if command -v git
|
|
1022
|
+
if command -v git >/dev/null 2>&1; then
|
|
1017
1023
|
while IFS= read -r branch; do
|
|
1018
1024
|
[[ -z "$branch" ]] && continue
|
|
1019
1025
|
branch="${branch## }" # trim leading spaces
|
|
@@ -1075,7 +1081,7 @@ daemon_cleanup_stale() {
|
|
|
1075
1081
|
ps_status=$(sed -n 's/^status: *//p' "$pipeline_state" 2>/dev/null | head -1 | tr -d ' ')
|
|
1076
1082
|
if [[ "$ps_status" == "running" ]]; then
|
|
1077
1083
|
local ps_mtime
|
|
1078
|
-
ps_mtime=$(
|
|
1084
|
+
ps_mtime=$(file_mtime "$pipeline_state")
|
|
1079
1085
|
local ps_age=$((now_e - ps_mtime))
|
|
1080
1086
|
# If pipeline-state.md has been "running" for more than 2 hours and no active job
|
|
1081
1087
|
if [[ "$ps_age" -gt 7200 ]]; then
|
|
@@ -1098,7 +1104,7 @@ daemon_cleanup_stale() {
|
|
|
1098
1104
|
fi
|
|
1099
1105
|
|
|
1100
1106
|
# ── 7. Clean remote branches for merged pipeline/* branches ──
|
|
1101
|
-
if command -v git
|
|
1107
|
+
if command -v git >/dev/null 2>&1 && [[ "${NO_GITHUB:-}" != "true" ]]; then
|
|
1102
1108
|
while IFS= read -r branch; do
|
|
1103
1109
|
[[ -z "$branch" ]] && continue
|
|
1104
1110
|
branch="${branch## }"
|
|
@@ -1138,6 +1144,12 @@ daemon_poll_loop() {
|
|
|
1138
1144
|
daemon_reap_completed || daemon_log WARN "daemon_reap_completed failed — continuing"
|
|
1139
1145
|
daemon_health_check || daemon_log WARN "daemon_health_check failed — continuing"
|
|
1140
1146
|
|
|
1147
|
+
# Fleet failover: re-queue work from offline machines
|
|
1148
|
+
if [[ -f "$HOME/.shipwright/machines.json" ]]; then
|
|
1149
|
+
[[ -f "$SCRIPT_DIR/lib/fleet-failover.sh" ]] && source "$SCRIPT_DIR/lib/fleet-failover.sh" 2>/dev/null || true
|
|
1150
|
+
fleet_failover_check 2>/dev/null || true
|
|
1151
|
+
fi
|
|
1152
|
+
|
|
1141
1153
|
# Increment cycle counter (must be before all modulo checks)
|
|
1142
1154
|
POLL_CYCLE_COUNT=$((POLL_CYCLE_COUNT + 1))
|
|
1143
1155
|
|
|
@@ -3,6 +3,10 @@
|
|
|
3
3
|
[[ -n "${_DAEMON_STATE_LOADED:-}" ]] && return 0
|
|
4
4
|
_DAEMON_STATE_LOADED=1
|
|
5
5
|
|
|
6
|
+
# SQLite persistence (DB as primary read path)
|
|
7
|
+
_DAEMON_STATE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
8
|
+
[[ -f "${_DAEMON_STATE_DIR}/../sw-db.sh" ]] && source "${_DAEMON_STATE_DIR}/../sw-db.sh"
|
|
9
|
+
|
|
6
10
|
daemon_log() {
|
|
7
11
|
local level="$1"
|
|
8
12
|
shift
|
|
@@ -58,8 +62,8 @@ notify() {
|
|
|
58
62
|
-d "$payload" "$SLACK_WEBHOOK" >/dev/null 2>&1 || true
|
|
59
63
|
fi
|
|
60
64
|
|
|
61
|
-
# Custom webhook (env var SHIPWRIGHT_WEBHOOK_URL
|
|
62
|
-
local _webhook_url="${SHIPWRIGHT_WEBHOOK_URL
|
|
65
|
+
# Custom webhook (env var SHIPWRIGHT_WEBHOOK_URL)
|
|
66
|
+
local _webhook_url="${SHIPWRIGHT_WEBHOOK_URL:-}"
|
|
63
67
|
if [[ -n "$_webhook_url" ]]; then
|
|
64
68
|
local payload
|
|
65
69
|
payload=$(jq -n \
|
|
@@ -123,7 +127,7 @@ daemon_preflight_auth_check() {
|
|
|
123
127
|
|
|
124
128
|
# gh auth check
|
|
125
129
|
if [[ "${NO_GITHUB:-false}" != "true" ]]; then
|
|
126
|
-
if ! gh auth status
|
|
130
|
+
if ! gh auth status >/dev/null 2>&1; then
|
|
127
131
|
daemon_log ERROR "GitHub auth check failed — auto-pausing daemon"
|
|
128
132
|
local pause_json
|
|
129
133
|
pause_json=$(jq -n --arg reason "gh_auth_failure" --arg ts "$(now_iso)" \
|
|
@@ -189,7 +193,7 @@ preflight_checks() {
|
|
|
189
193
|
local optional_tools=("tmux" "curl")
|
|
190
194
|
|
|
191
195
|
for tool in "${required_tools[@]}"; do
|
|
192
|
-
if command -v "$tool"
|
|
196
|
+
if command -v "$tool" >/dev/null 2>&1; then
|
|
193
197
|
echo -e " ${GREEN}✓${RESET} $tool"
|
|
194
198
|
else
|
|
195
199
|
echo -e " ${RED}✗${RESET} $tool ${RED}(required)${RESET}"
|
|
@@ -198,7 +202,7 @@ preflight_checks() {
|
|
|
198
202
|
done
|
|
199
203
|
|
|
200
204
|
for tool in "${optional_tools[@]}"; do
|
|
201
|
-
if command -v "$tool"
|
|
205
|
+
if command -v "$tool" >/dev/null 2>&1; then
|
|
202
206
|
echo -e " ${GREEN}✓${RESET} $tool"
|
|
203
207
|
else
|
|
204
208
|
echo -e " ${DIM}○${RESET} $tool ${DIM}(optional — some features disabled)${RESET}"
|
|
@@ -207,7 +211,7 @@ preflight_checks() {
|
|
|
207
211
|
|
|
208
212
|
# 2. Git state
|
|
209
213
|
echo ""
|
|
210
|
-
if git rev-parse --is-inside-work-tree
|
|
214
|
+
if git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
|
|
211
215
|
echo -e " ${GREEN}✓${RESET} Inside git repo"
|
|
212
216
|
else
|
|
213
217
|
echo -e " ${RED}✗${RESET} Not inside a git repository"
|
|
@@ -215,7 +219,7 @@ preflight_checks() {
|
|
|
215
219
|
fi
|
|
216
220
|
|
|
217
221
|
# Check base branch exists
|
|
218
|
-
if git rev-parse --verify "$BASE_BRANCH"
|
|
222
|
+
if git rev-parse --verify "$BASE_BRANCH" >/dev/null 2>&1; then
|
|
219
223
|
echo -e " ${GREEN}✓${RESET} Base branch: $BASE_BRANCH"
|
|
220
224
|
else
|
|
221
225
|
echo -e " ${RED}✗${RESET} Base branch not found: $BASE_BRANCH"
|
|
@@ -224,7 +228,7 @@ preflight_checks() {
|
|
|
224
228
|
|
|
225
229
|
# 3. GitHub auth (required for daemon — it needs to poll issues)
|
|
226
230
|
if [[ "$NO_GITHUB" != "true" ]]; then
|
|
227
|
-
if gh auth status
|
|
231
|
+
if gh auth status >/dev/null 2>&1; then
|
|
228
232
|
echo -e " ${GREEN}✓${RESET} GitHub authenticated"
|
|
229
233
|
else
|
|
230
234
|
echo -e " ${RED}✗${RESET} GitHub not authenticated (required for daemon)"
|
|
@@ -286,15 +290,48 @@ atomic_write_state() {
|
|
|
286
290
|
}
|
|
287
291
|
}
|
|
288
292
|
|
|
293
|
+
# Sync active_jobs from state JSON to DB (dual-write, best-effort)
|
|
294
|
+
_sync_state_to_db() {
|
|
295
|
+
local state_json="$1"
|
|
296
|
+
[[ -z "$state_json" ]] && return 0
|
|
297
|
+
if ! type db_save_job >/dev/null 2>&1 || ! db_available 2>/dev/null; then
|
|
298
|
+
return 0
|
|
299
|
+
fi
|
|
300
|
+
local start_epoch job_id
|
|
301
|
+
while IFS= read -r job; do
|
|
302
|
+
[[ -z "$job" || "$job" == "null" ]] && continue
|
|
303
|
+
local issue title pid worktree branch template goal started_at
|
|
304
|
+
issue=$(echo "$job" | jq -r '.issue // 0' 2>/dev/null)
|
|
305
|
+
title=$(echo "$job" | jq -r '.title // ""' 2>/dev/null)
|
|
306
|
+
pid=$(echo "$job" | jq -r '.pid // 0' 2>/dev/null)
|
|
307
|
+
worktree=$(echo "$job" | jq -r '.worktree // ""' 2>/dev/null)
|
|
308
|
+
branch=$(echo "$job" | jq -r '.branch // ""' 2>/dev/null)
|
|
309
|
+
template=$(echo "$job" | jq -r '.template // "autonomous"' 2>/dev/null)
|
|
310
|
+
goal=$(echo "$job" | jq -r '.goal // ""' 2>/dev/null)
|
|
311
|
+
started_at=$(echo "$job" | jq -r '.started_at // ""' 2>/dev/null)
|
|
312
|
+
if [[ -z "$issue" || "$issue" == "0" ]] || [[ ! "$issue" =~ ^[0-9]+$ ]]; then
|
|
313
|
+
continue
|
|
314
|
+
fi
|
|
315
|
+
start_epoch=0
|
|
316
|
+
if [[ -n "$started_at" ]]; then
|
|
317
|
+
start_epoch=$(TZ=UTC date -j -f "%Y-%m-%dT%H:%M:%SZ" "$started_at" +%s 2>/dev/null || date -d "$started_at" +%s 2>/dev/null || echo "0")
|
|
318
|
+
fi
|
|
319
|
+
[[ -z "$start_epoch" ]] && start_epoch=0
|
|
320
|
+
job_id="daemon-${issue}-${start_epoch}"
|
|
321
|
+
db_save_job "$job_id" "$issue" "$title" "$pid" "$worktree" "$branch" "$template" "$goal" 2>/dev/null || true
|
|
322
|
+
done < <(echo "$state_json" | jq -c '.active_jobs[]? // empty' 2>/dev/null || true)
|
|
323
|
+
}
|
|
324
|
+
|
|
289
325
|
# Locked read-modify-write: prevents TOCTOU race on state file.
|
|
290
326
|
# Usage: locked_state_update '.queued += [42]'
|
|
291
327
|
# The jq expression is applied to the current state file atomically.
|
|
328
|
+
# Dual-write: also syncs active_jobs to DB when available.
|
|
292
329
|
locked_state_update() {
|
|
293
330
|
local jq_expr="$1"
|
|
294
331
|
shift
|
|
295
332
|
local lock_file="${STATE_FILE}.lock"
|
|
296
333
|
(
|
|
297
|
-
if command -v flock
|
|
334
|
+
if command -v flock >/dev/null 2>&1; then
|
|
298
335
|
flock -w 5 200 2>/dev/null || {
|
|
299
336
|
daemon_log ERROR "locked_state_update: lock acquisition timed out — aborting"
|
|
300
337
|
return 1
|
|
@@ -309,6 +346,7 @@ locked_state_update() {
|
|
|
309
346
|
daemon_log ERROR "locked_state_update: atomic_write_state failed"
|
|
310
347
|
return 1
|
|
311
348
|
}
|
|
349
|
+
_sync_state_to_db "$tmp" 2>/dev/null || true
|
|
312
350
|
) 200>"$lock_file"
|
|
313
351
|
}
|
|
314
352
|
|
|
@@ -343,7 +381,7 @@ init_state() {
|
|
|
343
381
|
}')
|
|
344
382
|
local lock_file="${STATE_FILE}.lock"
|
|
345
383
|
(
|
|
346
|
-
if command -v flock
|
|
384
|
+
if command -v flock >/dev/null 2>&1; then
|
|
347
385
|
flock -w 5 200 2>/dev/null || {
|
|
348
386
|
daemon_log ERROR "init_state: lock acquisition timed out"
|
|
349
387
|
return 1
|
|
@@ -358,6 +396,11 @@ init_state() {
|
|
|
358
396
|
--arg started "$(now_iso)" \
|
|
359
397
|
'.pid = ($pid | tonumber) | .started_at = $started'
|
|
360
398
|
fi
|
|
399
|
+
|
|
400
|
+
# Ensure DB schema is initialized when available
|
|
401
|
+
if type migrate_schema >/dev/null 2>&1 && db_available 2>/dev/null; then
|
|
402
|
+
migrate_schema 2>/dev/null || true
|
|
403
|
+
fi
|
|
361
404
|
}
|
|
362
405
|
|
|
363
406
|
update_state_field() {
|
|
@@ -369,13 +412,15 @@ update_state_field() {
|
|
|
369
412
|
# ─── Inflight Check ─────────────────────────────────────────────────────────
|
|
370
413
|
|
|
371
414
|
daemon_is_inflight() {
|
|
372
|
-
local
|
|
415
|
+
local issue_key="$1"
|
|
416
|
+
local issue_num="$issue_key"
|
|
417
|
+
[[ "$issue_key" == *:* ]] && issue_num="${issue_key##*:}"
|
|
373
418
|
|
|
374
419
|
if [[ ! -f "$STATE_FILE" ]]; then
|
|
375
420
|
return 1
|
|
376
421
|
fi
|
|
377
422
|
|
|
378
|
-
# Check active_jobs
|
|
423
|
+
# Check active_jobs (stored with numeric .issue)
|
|
379
424
|
local active_match
|
|
380
425
|
active_match=$(jq -r --argjson num "$issue_num" \
|
|
381
426
|
'.active_jobs[] | select(.issue == $num) | .issue' \
|
|
@@ -384,10 +429,10 @@ daemon_is_inflight() {
|
|
|
384
429
|
return 0
|
|
385
430
|
fi
|
|
386
431
|
|
|
387
|
-
# Check queued
|
|
432
|
+
# Check queued (stores full key e.g. "owner/repo:42" or "42")
|
|
388
433
|
local queued_match
|
|
389
|
-
queued_match=$(jq -r --
|
|
390
|
-
'.queued[] | select(. == $
|
|
434
|
+
queued_match=$(jq -r --arg key "$issue_key" \
|
|
435
|
+
'.queued[] | select(. == $key)' \
|
|
391
436
|
"$STATE_FILE" 2>/dev/null || true)
|
|
392
437
|
if [[ -n "$queued_match" ]]; then
|
|
393
438
|
return 0
|
|
@@ -417,7 +462,7 @@ locked_get_active_count() {
|
|
|
417
462
|
local count
|
|
418
463
|
count=$(
|
|
419
464
|
(
|
|
420
|
-
if command -v flock
|
|
465
|
+
if command -v flock >/dev/null 2>&1; then
|
|
421
466
|
flock -w 5 200 2>/dev/null || {
|
|
422
467
|
daemon_log WARN "locked_get_active_count: lock timeout — returning MAX_PARALLEL as safe default" >&2
|
|
423
468
|
echo "$MAX_PARALLEL"
|
|
@@ -433,13 +478,30 @@ locked_get_active_count() {
|
|
|
433
478
|
# ─── Queue Management ───────────────────────────────────────────────────────
|
|
434
479
|
|
|
435
480
|
enqueue_issue() {
|
|
436
|
-
local
|
|
437
|
-
locked_state_update --
|
|
438
|
-
'.queued += [$
|
|
439
|
-
|
|
481
|
+
local issue_key="$1"
|
|
482
|
+
locked_state_update --arg key "$issue_key" \
|
|
483
|
+
'.queued += [$key] | .queued |= unique'
|
|
484
|
+
if type db_enqueue_issue >/dev/null 2>&1; then
|
|
485
|
+
db_enqueue_issue "$issue_key" 2>/dev/null || true
|
|
486
|
+
fi
|
|
487
|
+
daemon_log INFO "Queued issue ${issue_key} (at capacity)"
|
|
440
488
|
}
|
|
441
489
|
|
|
442
490
|
dequeue_next() {
|
|
491
|
+
# Try DB first when available
|
|
492
|
+
if type db_dequeue_next >/dev/null 2>&1 && db_available 2>/dev/null; then
|
|
493
|
+
local next
|
|
494
|
+
next=$(db_dequeue_next 2>/dev/null || true)
|
|
495
|
+
if [[ -n "$next" ]]; then
|
|
496
|
+
# Also update JSON file for backward compat
|
|
497
|
+
if [[ -f "$STATE_FILE" ]]; then
|
|
498
|
+
locked_state_update --arg key "$next" '.queued = [.queued[] | select(. != $key)]'
|
|
499
|
+
fi
|
|
500
|
+
echo "$next"
|
|
501
|
+
return
|
|
502
|
+
fi
|
|
503
|
+
fi
|
|
504
|
+
|
|
443
505
|
if [[ ! -f "$STATE_FILE" ]]; then
|
|
444
506
|
return
|
|
445
507
|
fi
|
|
@@ -447,8 +509,10 @@ dequeue_next() {
|
|
|
447
509
|
local next
|
|
448
510
|
next=$(jq -r '.queued[0] // empty' "$STATE_FILE" 2>/dev/null || true)
|
|
449
511
|
if [[ -n "$next" ]]; then
|
|
450
|
-
# Remove from queue (locked to prevent race with enqueue)
|
|
451
512
|
locked_state_update '.queued = .queued[1:]'
|
|
513
|
+
if type db_remove_from_queue >/dev/null 2>&1; then
|
|
514
|
+
db_remove_from_queue "$next" 2>/dev/null || true
|
|
515
|
+
fi
|
|
452
516
|
echo "$next"
|
|
453
517
|
fi
|
|
454
518
|
}
|
|
@@ -496,6 +560,22 @@ untrack_priority_job() {
|
|
|
496
560
|
|
|
497
561
|
# ─── Distributed Issue Claiming ───────────────────────────────────────────
|
|
498
562
|
|
|
563
|
+
# Verify we have exclusive claim: exactly one claimed:* label matching our machine
|
|
564
|
+
_verify_claim_exclusive() {
|
|
565
|
+
local issue_num="$1" machine_name="$2"
|
|
566
|
+
local claimed_labels
|
|
567
|
+
claimed_labels=$(gh issue view "$issue_num" --json labels --jq \
|
|
568
|
+
'[.labels[].name | select(startswith("claimed:"))]' 2>/dev/null || echo "[]")
|
|
569
|
+
local count
|
|
570
|
+
count=$(echo "$claimed_labels" | jq 'length' 2>/dev/null || echo "0")
|
|
571
|
+
if [[ "$count" != "1" ]]; then
|
|
572
|
+
return 1 # Competing claims (multiple or none)
|
|
573
|
+
fi
|
|
574
|
+
local sole_claim
|
|
575
|
+
sole_claim=$(echo "$claimed_labels" | jq -r '.[0]' 2>/dev/null || echo "")
|
|
576
|
+
[[ "$sole_claim" == "claimed:${machine_name}" ]]
|
|
577
|
+
}
|
|
578
|
+
|
|
499
579
|
claim_issue() {
|
|
500
580
|
local issue_num="$1"
|
|
501
581
|
local machine_name="$2"
|
|
@@ -509,9 +589,15 @@ claim_issue() {
|
|
|
509
589
|
-d "$(jq -n --argjson issue "$issue_num" --arg machine "$machine_name" \
|
|
510
590
|
'{issue: $issue, machine: $machine}')" 2>/dev/null || echo "")
|
|
511
591
|
|
|
512
|
-
if [[ -n "$resp" ]] && echo "$resp" | jq -e '.approved == true'
|
|
592
|
+
if [[ -n "$resp" ]] && echo "$resp" | jq -e '.approved == true' >/dev/null 2>&1; then
|
|
593
|
+
# VERIFY: re-read labels, ensure only our claim exists
|
|
594
|
+
if ! _verify_claim_exclusive "$issue_num" "$machine_name"; then
|
|
595
|
+
daemon_log INFO "Issue #${issue_num} claim race lost (competing claim) — removing our label"
|
|
596
|
+
gh issue edit "$issue_num" --remove-label "claimed:${machine_name}" 2>/dev/null || true
|
|
597
|
+
return 1
|
|
598
|
+
fi
|
|
513
599
|
return 0
|
|
514
|
-
elif [[ -n "$resp" ]] && echo "$resp" | jq -e '.approved == false'
|
|
600
|
+
elif [[ -n "$resp" ]] && echo "$resp" | jq -e '.approved == false' >/dev/null 2>&1; then
|
|
515
601
|
local claimed_by
|
|
516
602
|
claimed_by=$(echo "$resp" | jq -r '.claimed_by // "another machine"')
|
|
517
603
|
daemon_log INFO "Issue #${issue_num} claimed by ${claimed_by} (via dashboard)"
|
|
@@ -530,6 +616,12 @@ claim_issue() {
|
|
|
530
616
|
fi
|
|
531
617
|
|
|
532
618
|
gh issue edit "$issue_num" --add-label "claimed:${machine_name}" 2>/dev/null || return 1
|
|
619
|
+
# VERIFY: re-read labels, ensure only our claim exists
|
|
620
|
+
if ! _verify_claim_exclusive "$issue_num" "$machine_name"; then
|
|
621
|
+
daemon_log INFO "Issue #${issue_num} claim race lost (competing claim) — removing our label"
|
|
622
|
+
gh issue edit "$issue_num" --remove-label "claimed:${machine_name}" 2>/dev/null || true
|
|
623
|
+
return 1
|
|
624
|
+
fi
|
|
533
625
|
return 0
|
|
534
626
|
}
|
|
535
627
|
|
|
@@ -21,7 +21,7 @@ triage_score_issue() {
|
|
|
21
21
|
issue_body=$(echo "$issue_json" | jq -r '.body // ""')
|
|
22
22
|
|
|
23
23
|
# ── Intelligence-powered triage (if enabled) ──
|
|
24
|
-
if [[ "${INTELLIGENCE_ENABLED:-false}" == "true" ]] && type intelligence_analyze_issue
|
|
24
|
+
if [[ "${INTELLIGENCE_ENABLED:-false}" == "true" ]] && type intelligence_analyze_issue >/dev/null 2>&1; then
|
|
25
25
|
daemon_log INFO "Intelligence: using AI triage (intelligence enabled)" >&2
|
|
26
26
|
local analysis
|
|
27
27
|
analysis=$(intelligence_analyze_issue "$issue_json" 2>/dev/null || echo "")
|
|
@@ -143,10 +143,14 @@ triage_score_issue() {
|
|
|
143
143
|
# Check if this issue blocks others (search issue references)
|
|
144
144
|
if [[ "$NO_GITHUB" != "true" ]]; then
|
|
145
145
|
local mentions
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
146
|
+
local repo_nwo
|
|
147
|
+
repo_nwo=$(gh repo view --json nameWithOwner --jq '.nameWithOwner' 2>/dev/null || echo "")
|
|
148
|
+
if [[ -n "$repo_nwo" ]]; then
|
|
149
|
+
mentions=$(gh api "repos/${repo_nwo}/issues/${issue_num}/timeline" --paginate -q '
|
|
150
|
+
[.[] | select(.event == "cross-referenced") | .source.issue.body // ""] |
|
|
151
|
+
map(select(test("blocked by #'"${issue_num}"'|depends on #'"${issue_num}"'"; "i"))) | length
|
|
152
|
+
' 2>/dev/null || echo "0")
|
|
153
|
+
fi
|
|
150
154
|
mentions=${mentions:-0}
|
|
151
155
|
if [[ "$mentions" -gt 0 ]]; then
|
|
152
156
|
dep_score=15
|
|
@@ -212,7 +216,7 @@ select_pipeline_template() {
|
|
|
212
216
|
fi
|
|
213
217
|
|
|
214
218
|
# ── Intelligence-composed pipeline (if enabled) ──
|
|
215
|
-
if [[ "${COMPOSER_ENABLED:-false}" == "true" ]] && type composer_create_pipeline
|
|
219
|
+
if [[ "${COMPOSER_ENABLED:-false}" == "true" ]] && type composer_create_pipeline >/dev/null 2>&1; then
|
|
216
220
|
daemon_log INFO "Intelligence: using AI pipeline composition (composer enabled)" >&2
|
|
217
221
|
local analysis="${INTELLIGENCE_ANALYSIS:-{}}"
|
|
218
222
|
local repo_context=""
|
|
@@ -301,8 +305,8 @@ select_pipeline_template() {
|
|
|
301
305
|
fi
|
|
302
306
|
|
|
303
307
|
# ── Branch protection escalation (highest priority) ──
|
|
304
|
-
if type gh_branch_protection
|
|
305
|
-
if type _gh_detect_repo
|
|
308
|
+
if type gh_branch_protection >/dev/null 2>&1 && [[ "${NO_GITHUB:-false}" != "true" ]]; then
|
|
309
|
+
if type _gh_detect_repo >/dev/null 2>&1; then
|
|
306
310
|
_gh_detect_repo 2>/dev/null || true
|
|
307
311
|
fi
|
|
308
312
|
local gh_owner="${GH_OWNER:-}" gh_repo="${GH_REPO:-}"
|
|
@@ -386,6 +390,24 @@ select_pipeline_template() {
|
|
|
386
390
|
fi
|
|
387
391
|
fi
|
|
388
392
|
|
|
393
|
+
# ── Thompson sampling (outcome-based learning, when DB available) ──
|
|
394
|
+
if type thompson_select_template >/dev/null 2>&1; then
|
|
395
|
+
local _complexity="medium"
|
|
396
|
+
[[ "$score" -ge 70 ]] && _complexity="low"
|
|
397
|
+
[[ "$score" -lt 40 ]] && _complexity="high"
|
|
398
|
+
local _thompson_result
|
|
399
|
+
_thompson_result=$(thompson_select_template "$_complexity" 2>/dev/null || echo "")
|
|
400
|
+
if [[ -n "${_thompson_result:-}" && "${_thompson_result:-}" != "standard" ]]; then
|
|
401
|
+
daemon_log INFO "Thompson sampling: $_thompson_result (complexity=$_complexity)" >&2
|
|
402
|
+
echo "$_thompson_result"
|
|
403
|
+
return
|
|
404
|
+
fi
|
|
405
|
+
if [[ -n "${_thompson_result:-}" ]]; then
|
|
406
|
+
echo "$_thompson_result"
|
|
407
|
+
return
|
|
408
|
+
fi
|
|
409
|
+
fi
|
|
410
|
+
|
|
389
411
|
# ── Learned template weights ──
|
|
390
412
|
local _tw_file="${HOME}/.shipwright/optimization/template-weights.json"
|
|
391
413
|
if [[ -f "$_tw_file" ]]; then
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# fleet-failover.sh — Re-queue work from offline fleet machines
|
|
2
|
+
# When a machine goes offline, release its claimed issues so they can be picked up again.
|
|
3
|
+
# Source from daemon-poll or sw-fleet. Works standalone with gh + jq.
|
|
4
|
+
[[ -n "${_FLEET_FAILOVER_LOADED:-}" ]] && return 0
|
|
5
|
+
_FLEET_FAILOVER_LOADED=1
|
|
6
|
+
|
|
7
|
+
fleet_failover_check() {
|
|
8
|
+
local health_file="$HOME/.shipwright/machine-health.json"
|
|
9
|
+
[[ ! -f "$health_file" ]] && return 0
|
|
10
|
+
|
|
11
|
+
[[ "${NO_GITHUB:-false}" == "true" ]] && return 0
|
|
12
|
+
command -v gh >/dev/null 2>&1 || return 0
|
|
13
|
+
command -v jq >/dev/null 2>&1 || return 0
|
|
14
|
+
|
|
15
|
+
# Find offline machines (health file: .[machine_name] = {status, checked_at})
|
|
16
|
+
local offline_machines
|
|
17
|
+
offline_machines=$(jq -r 'to_entries[] | select(.value.status == "offline") | .key' "$health_file" 2>/dev/null)
|
|
18
|
+
[[ -z "$offline_machines" ]] && return 0
|
|
19
|
+
|
|
20
|
+
while IFS= read -r machine; do
|
|
21
|
+
[[ -z "$machine" ]] && continue
|
|
22
|
+
|
|
23
|
+
# Find issues claimed by this offline machine via GitHub label
|
|
24
|
+
local orphaned_issues
|
|
25
|
+
orphaned_issues=$(gh search issues \
|
|
26
|
+
"label:claimed:${machine}" \
|
|
27
|
+
is:open \
|
|
28
|
+
--json number,repository \
|
|
29
|
+
--limit 100 2>/dev/null | jq -r '.[] | "\(.repository.nameWithOwner):\(.number)"' 2>/dev/null)
|
|
30
|
+
[[ -z "$orphaned_issues" ]] && continue
|
|
31
|
+
|
|
32
|
+
while IFS= read -r issue_key; do
|
|
33
|
+
[[ -z "$issue_key" ]] && continue
|
|
34
|
+
|
|
35
|
+
local issue_num="${issue_key##*:}"
|
|
36
|
+
local repo="${issue_key%:*}"
|
|
37
|
+
[[ "$repo" == "$issue_key" ]] && repo=""
|
|
38
|
+
|
|
39
|
+
# Log and emit
|
|
40
|
+
if [[ "$(type -t info 2>/dev/null)" == "function" ]]; then
|
|
41
|
+
info "Failover: re-queuing issue #${issue_num} from offline machine ${machine}"
|
|
42
|
+
fi
|
|
43
|
+
if [[ "$(type -t emit_event 2>/dev/null)" == "function" ]]; then
|
|
44
|
+
emit_event "fleet.failover" "{\"issue\":\"$issue_num\",\"from_machine\":\"$machine\"}"
|
|
45
|
+
fi
|
|
46
|
+
|
|
47
|
+
# Release the claim (remove label) — idempotent
|
|
48
|
+
if [[ -n "$repo" ]]; then
|
|
49
|
+
gh issue edit "$issue_num" --repo "$repo" --remove-label "claimed:${machine}" 2>/dev/null || true
|
|
50
|
+
else
|
|
51
|
+
gh issue edit "$issue_num" --remove-label "claimed:${machine}" 2>/dev/null || true
|
|
52
|
+
fi
|
|
53
|
+
|
|
54
|
+
# When running in daemon context: enqueue so we pick it up if we watch this repo
|
|
55
|
+
# In org mode WATCH_MODE=org, enqueue uses owner/repo:num; in repo mode just num
|
|
56
|
+
if [[ -f "${STATE_FILE:-$HOME/.shipwright/daemon-state.json}" ]] && type enqueue_issue >/dev/null 2>&1; then
|
|
57
|
+
local queue_key="$issue_num"
|
|
58
|
+
[[ -n "$repo" ]] && queue_key="${repo}:${issue_num}"
|
|
59
|
+
enqueue_issue "$queue_key" 2>/dev/null || true
|
|
60
|
+
fi
|
|
61
|
+
done <<< "$orphaned_issues"
|
|
62
|
+
done <<< "$offline_machines"
|
|
63
|
+
}
|