shipwright-cli 2.4.0 → 3.1.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 +248 -94
- package/completions/shipwright.bash +68 -19
- package/completions/shipwright.fish +310 -42
- package/config/decision-tiers.json +55 -0
- package/config/defaults.json +111 -0
- package/config/event-schema.json +218 -0
- package/config/policy.json +21 -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 +7 -9
- 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 +127 -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 +63 -17
- package/scripts/lib/daemon-failure.sh +0 -0
- package/scripts/lib/daemon-health.sh +1 -1
- package/scripts/lib/daemon-patrol.sh +64 -17
- package/scripts/lib/daemon-poll.sh +54 -25
- package/scripts/lib/daemon-state.sh +125 -23
- package/scripts/lib/daemon-triage.sh +31 -9
- package/scripts/lib/decide-autonomy.sh +295 -0
- package/scripts/lib/decide-scoring.sh +228 -0
- package/scripts/lib/decide-signals.sh +462 -0
- package/scripts/lib/fleet-failover.sh +63 -0
- package/scripts/lib/helpers.sh +29 -6
- package/scripts/lib/pipeline-detection.sh +2 -2
- package/scripts/lib/pipeline-github.sh +9 -9
- package/scripts/lib/pipeline-intelligence.sh +105 -38
- package/scripts/lib/pipeline-quality-checks.sh +17 -16
- package/scripts/lib/pipeline-quality.sh +1 -1
- package/scripts/lib/pipeline-stages.sh +440 -59
- package/scripts/lib/pipeline-state.sh +54 -4
- package/scripts/lib/policy.sh +0 -0
- package/scripts/lib/test-helpers.sh +247 -0
- package/scripts/postinstall.mjs +78 -12
- package/scripts/signals/example-collector.sh +36 -0
- package/scripts/sw +17 -7
- 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 +17 -18
- package/scripts/sw-daemon.sh +76 -71
- package/scripts/sw-dashboard.sh +57 -17
- package/scripts/sw-db.sh +524 -26
- package/scripts/sw-decide.sh +685 -0
- 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 +138 -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 +368 -53
- 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 +905 -104
- package/scripts/sw-memory.sh +263 -20
- package/scripts/sw-mission-control.sh +2 -12
- package/scripts/sw-model-router.sh +73 -34
- package/scripts/sw-otel.sh +15 -23
- 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 +550 -122
- package/scripts/sw-pm.sh +2 -12
- package/scripts/sw-pr-lifecycle.sh +33 -28
- 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 +85 -14
- 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 +174 -41
- package/scripts/sw-security-audit.sh +12 -22
- package/scripts/sw-self-optimize.sh +239 -23
- package/scripts/sw-session.sh +5 -15
- 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 +29 -39
- 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 +73 -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
|
|
|
@@ -1184,6 +1196,23 @@ daemon_poll_loop() {
|
|
|
1184
1196
|
daemon_patrol --once || daemon_log WARN "daemon_patrol failed — continuing"
|
|
1185
1197
|
LAST_PATROL_EPOCH=$now_e
|
|
1186
1198
|
fi
|
|
1199
|
+
|
|
1200
|
+
# Decision engine cycle (if enabled)
|
|
1201
|
+
local _decision_enabled
|
|
1202
|
+
_decision_enabled=$(policy_get ".decision.enabled" "false" 2>/dev/null || echo "false")
|
|
1203
|
+
if [[ "$_decision_enabled" == "true" ]]; then
|
|
1204
|
+
local _decision_interval
|
|
1205
|
+
_decision_interval=$(policy_get ".decision.cycle_interval_seconds" "1800" 2>/dev/null || echo "1800")
|
|
1206
|
+
local _last_decision_epoch="${_LAST_DECISION_EPOCH:-0}"
|
|
1207
|
+
if [[ $((now_e - _last_decision_epoch)) -ge "$_decision_interval" ]]; then
|
|
1208
|
+
daemon_log INFO "Running decision engine cycle"
|
|
1209
|
+
if [[ -f "$SCRIPT_DIR/sw-decide.sh" ]]; then
|
|
1210
|
+
DECISION_ENGINE_ENABLED=true bash "$SCRIPT_DIR/sw-decide.sh" run --once 2>/dev/null || \
|
|
1211
|
+
daemon_log WARN "Decision engine cycle failed — continuing"
|
|
1212
|
+
fi
|
|
1213
|
+
_LAST_DECISION_EPOCH=$now_e
|
|
1214
|
+
fi
|
|
1215
|
+
fi
|
|
1187
1216
|
fi
|
|
1188
1217
|
|
|
1189
1218
|
# ── Adaptive poll interval: adjust sleep based on queue state ──
|
|
@@ -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
|
|
@@ -352,12 +390,27 @@ init_state() {
|
|
|
352
390
|
atomic_write_state "$init_json"
|
|
353
391
|
) 200>"$lock_file"
|
|
354
392
|
else
|
|
393
|
+
# Validate existing state file JSON before using it
|
|
394
|
+
if ! jq '.' "$STATE_FILE" >/dev/null 2>&1; then
|
|
395
|
+
daemon_log WARN "Corrupted state file detected — backing up and resetting"
|
|
396
|
+
cp "$STATE_FILE" "${STATE_FILE}.corrupted.$(date +%s)" 2>/dev/null || true
|
|
397
|
+
rm -f "$STATE_FILE"
|
|
398
|
+
# Re-initialize as fresh state (recursive call with file removed)
|
|
399
|
+
init_state
|
|
400
|
+
return
|
|
401
|
+
fi
|
|
402
|
+
|
|
355
403
|
# Update PID and start time in existing state
|
|
356
404
|
locked_state_update \
|
|
357
405
|
--arg pid "$$" \
|
|
358
406
|
--arg started "$(now_iso)" \
|
|
359
407
|
'.pid = ($pid | tonumber) | .started_at = $started'
|
|
360
408
|
fi
|
|
409
|
+
|
|
410
|
+
# Ensure DB schema is initialized when available
|
|
411
|
+
if type migrate_schema >/dev/null 2>&1 && db_available 2>/dev/null; then
|
|
412
|
+
migrate_schema 2>/dev/null || true
|
|
413
|
+
fi
|
|
361
414
|
}
|
|
362
415
|
|
|
363
416
|
update_state_field() {
|
|
@@ -369,13 +422,15 @@ update_state_field() {
|
|
|
369
422
|
# ─── Inflight Check ─────────────────────────────────────────────────────────
|
|
370
423
|
|
|
371
424
|
daemon_is_inflight() {
|
|
372
|
-
local
|
|
425
|
+
local issue_key="$1"
|
|
426
|
+
local issue_num="$issue_key"
|
|
427
|
+
[[ "$issue_key" == *:* ]] && issue_num="${issue_key##*:}"
|
|
373
428
|
|
|
374
429
|
if [[ ! -f "$STATE_FILE" ]]; then
|
|
375
430
|
return 1
|
|
376
431
|
fi
|
|
377
432
|
|
|
378
|
-
# Check active_jobs
|
|
433
|
+
# Check active_jobs (stored with numeric .issue)
|
|
379
434
|
local active_match
|
|
380
435
|
active_match=$(jq -r --argjson num "$issue_num" \
|
|
381
436
|
'.active_jobs[] | select(.issue == $num) | .issue' \
|
|
@@ -384,10 +439,10 @@ daemon_is_inflight() {
|
|
|
384
439
|
return 0
|
|
385
440
|
fi
|
|
386
441
|
|
|
387
|
-
# Check queued
|
|
442
|
+
# Check queued (stores full key e.g. "owner/repo:42" or "42")
|
|
388
443
|
local queued_match
|
|
389
|
-
queued_match=$(jq -r --
|
|
390
|
-
'.queued[] | select(. == $
|
|
444
|
+
queued_match=$(jq -r --arg key "$issue_key" \
|
|
445
|
+
'.queued[] | select(. == $key)' \
|
|
391
446
|
"$STATE_FILE" 2>/dev/null || true)
|
|
392
447
|
if [[ -n "$queued_match" ]]; then
|
|
393
448
|
return 0
|
|
@@ -417,7 +472,7 @@ locked_get_active_count() {
|
|
|
417
472
|
local count
|
|
418
473
|
count=$(
|
|
419
474
|
(
|
|
420
|
-
if command -v flock
|
|
475
|
+
if command -v flock >/dev/null 2>&1; then
|
|
421
476
|
flock -w 5 200 2>/dev/null || {
|
|
422
477
|
daemon_log WARN "locked_get_active_count: lock timeout — returning MAX_PARALLEL as safe default" >&2
|
|
423
478
|
echo "$MAX_PARALLEL"
|
|
@@ -433,13 +488,30 @@ locked_get_active_count() {
|
|
|
433
488
|
# ─── Queue Management ───────────────────────────────────────────────────────
|
|
434
489
|
|
|
435
490
|
enqueue_issue() {
|
|
436
|
-
local
|
|
437
|
-
locked_state_update --
|
|
438
|
-
'.queued += [$
|
|
439
|
-
|
|
491
|
+
local issue_key="$1"
|
|
492
|
+
locked_state_update --arg key "$issue_key" \
|
|
493
|
+
'.queued += [$key] | .queued |= unique'
|
|
494
|
+
if type db_enqueue_issue >/dev/null 2>&1; then
|
|
495
|
+
db_enqueue_issue "$issue_key" 2>/dev/null || true
|
|
496
|
+
fi
|
|
497
|
+
daemon_log INFO "Queued issue ${issue_key} (at capacity)"
|
|
440
498
|
}
|
|
441
499
|
|
|
442
500
|
dequeue_next() {
|
|
501
|
+
# Try DB first when available
|
|
502
|
+
if type db_dequeue_next >/dev/null 2>&1 && db_available 2>/dev/null; then
|
|
503
|
+
local next
|
|
504
|
+
next=$(db_dequeue_next 2>/dev/null || true)
|
|
505
|
+
if [[ -n "$next" ]]; then
|
|
506
|
+
# Also update JSON file for backward compat
|
|
507
|
+
if [[ -f "$STATE_FILE" ]]; then
|
|
508
|
+
locked_state_update --arg key "$next" '.queued = [.queued[] | select(. != $key)]'
|
|
509
|
+
fi
|
|
510
|
+
echo "$next"
|
|
511
|
+
return
|
|
512
|
+
fi
|
|
513
|
+
fi
|
|
514
|
+
|
|
443
515
|
if [[ ! -f "$STATE_FILE" ]]; then
|
|
444
516
|
return
|
|
445
517
|
fi
|
|
@@ -447,8 +519,10 @@ dequeue_next() {
|
|
|
447
519
|
local next
|
|
448
520
|
next=$(jq -r '.queued[0] // empty' "$STATE_FILE" 2>/dev/null || true)
|
|
449
521
|
if [[ -n "$next" ]]; then
|
|
450
|
-
# Remove from queue (locked to prevent race with enqueue)
|
|
451
522
|
locked_state_update '.queued = .queued[1:]'
|
|
523
|
+
if type db_remove_from_queue >/dev/null 2>&1; then
|
|
524
|
+
db_remove_from_queue "$next" 2>/dev/null || true
|
|
525
|
+
fi
|
|
452
526
|
echo "$next"
|
|
453
527
|
fi
|
|
454
528
|
}
|
|
@@ -496,6 +570,22 @@ untrack_priority_job() {
|
|
|
496
570
|
|
|
497
571
|
# ─── Distributed Issue Claiming ───────────────────────────────────────────
|
|
498
572
|
|
|
573
|
+
# Verify we have exclusive claim: exactly one claimed:* label matching our machine
|
|
574
|
+
_verify_claim_exclusive() {
|
|
575
|
+
local issue_num="$1" machine_name="$2"
|
|
576
|
+
local claimed_labels
|
|
577
|
+
claimed_labels=$(gh issue view "$issue_num" --json labels --jq \
|
|
578
|
+
'[.labels[].name | select(startswith("claimed:"))]' 2>/dev/null || echo "[]")
|
|
579
|
+
local count
|
|
580
|
+
count=$(echo "$claimed_labels" | jq 'length' 2>/dev/null || echo "0")
|
|
581
|
+
if [[ "$count" != "1" ]]; then
|
|
582
|
+
return 1 # Competing claims (multiple or none)
|
|
583
|
+
fi
|
|
584
|
+
local sole_claim
|
|
585
|
+
sole_claim=$(echo "$claimed_labels" | jq -r '.[0]' 2>/dev/null || echo "")
|
|
586
|
+
[[ "$sole_claim" == "claimed:${machine_name}" ]]
|
|
587
|
+
}
|
|
588
|
+
|
|
499
589
|
claim_issue() {
|
|
500
590
|
local issue_num="$1"
|
|
501
591
|
local machine_name="$2"
|
|
@@ -509,9 +599,15 @@ claim_issue() {
|
|
|
509
599
|
-d "$(jq -n --argjson issue "$issue_num" --arg machine "$machine_name" \
|
|
510
600
|
'{issue: $issue, machine: $machine}')" 2>/dev/null || echo "")
|
|
511
601
|
|
|
512
|
-
if [[ -n "$resp" ]] && echo "$resp" | jq -e '.approved == true'
|
|
602
|
+
if [[ -n "$resp" ]] && echo "$resp" | jq -e '.approved == true' >/dev/null 2>&1; then
|
|
603
|
+
# VERIFY: re-read labels, ensure only our claim exists
|
|
604
|
+
if ! _verify_claim_exclusive "$issue_num" "$machine_name"; then
|
|
605
|
+
daemon_log INFO "Issue #${issue_num} claim race lost (competing claim) — removing our label"
|
|
606
|
+
gh issue edit "$issue_num" --remove-label "claimed:${machine_name}" 2>/dev/null || true
|
|
607
|
+
return 1
|
|
608
|
+
fi
|
|
513
609
|
return 0
|
|
514
|
-
elif [[ -n "$resp" ]] && echo "$resp" | jq -e '.approved == false'
|
|
610
|
+
elif [[ -n "$resp" ]] && echo "$resp" | jq -e '.approved == false' >/dev/null 2>&1; then
|
|
515
611
|
local claimed_by
|
|
516
612
|
claimed_by=$(echo "$resp" | jq -r '.claimed_by // "another machine"')
|
|
517
613
|
daemon_log INFO "Issue #${issue_num} claimed by ${claimed_by} (via dashboard)"
|
|
@@ -530,6 +626,12 @@ claim_issue() {
|
|
|
530
626
|
fi
|
|
531
627
|
|
|
532
628
|
gh issue edit "$issue_num" --add-label "claimed:${machine_name}" 2>/dev/null || return 1
|
|
629
|
+
# VERIFY: re-read labels, ensure only our claim exists
|
|
630
|
+
if ! _verify_claim_exclusive "$issue_num" "$machine_name"; then
|
|
631
|
+
daemon_log INFO "Issue #${issue_num} claim race lost (competing claim) — removing our label"
|
|
632
|
+
gh issue edit "$issue_num" --remove-label "claimed:${machine_name}" 2>/dev/null || true
|
|
633
|
+
return 1
|
|
634
|
+
fi
|
|
533
635
|
return 0
|
|
534
636
|
}
|
|
535
637
|
|
|
@@ -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=""
|
|
@@ -246,7 +250,7 @@ select_pipeline_template() {
|
|
|
246
250
|
_dora_events=$(tail -500 "${EVENTS_FILE:-$HOME/.shipwright/events.jsonl}" \
|
|
247
251
|
| grep '"type":"pipeline.completed"' 2>/dev/null \
|
|
248
252
|
| tail -5 || true)
|
|
249
|
-
_dora_total=$(echo "$_dora_events" | grep -c '.' 2>/dev/null ||
|
|
253
|
+
_dora_total=$(echo "$_dora_events" | grep -c '.' 2>/dev/null || true)
|
|
250
254
|
_dora_total="${_dora_total:-0}"
|
|
251
255
|
if [[ "$_dora_total" -ge 3 ]]; then
|
|
252
256
|
_dora_failures=$(echo "$_dora_events" | grep -c '"result":"failure"' 2>/dev/null || true)
|
|
@@ -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
|