shipwright-cli 2.3.1 → 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.
Files changed (162) hide show
  1. package/README.md +95 -28
  2. package/completions/_shipwright +1 -1
  3. package/completions/shipwright.bash +3 -8
  4. package/completions/shipwright.fish +1 -1
  5. package/config/defaults.json +111 -0
  6. package/config/event-schema.json +81 -0
  7. package/config/policy.json +155 -2
  8. package/config/policy.schema.json +162 -1
  9. package/dashboard/coverage/coverage-summary.json +14 -0
  10. package/dashboard/public/index.html +1 -1
  11. package/dashboard/server.ts +306 -17
  12. package/dashboard/src/components/charts/bar.test.ts +79 -0
  13. package/dashboard/src/components/charts/donut.test.ts +68 -0
  14. package/dashboard/src/components/charts/pipeline-rail.test.ts +117 -0
  15. package/dashboard/src/components/charts/sparkline.test.ts +125 -0
  16. package/dashboard/src/core/api.test.ts +309 -0
  17. package/dashboard/src/core/helpers.test.ts +301 -0
  18. package/dashboard/src/core/router.test.ts +307 -0
  19. package/dashboard/src/core/router.ts +7 -0
  20. package/dashboard/src/core/sse.test.ts +144 -0
  21. package/dashboard/src/views/metrics.test.ts +186 -0
  22. package/dashboard/src/views/overview.test.ts +173 -0
  23. package/dashboard/src/views/pipelines.test.ts +183 -0
  24. package/dashboard/src/views/team.test.ts +253 -0
  25. package/dashboard/vitest.config.ts +14 -5
  26. package/docs/TIPS.md +1 -1
  27. package/docs/patterns/README.md +1 -1
  28. package/package.json +15 -5
  29. package/scripts/adapters/docker-deploy.sh +1 -1
  30. package/scripts/adapters/tmux-adapter.sh +11 -1
  31. package/scripts/adapters/wezterm-adapter.sh +1 -1
  32. package/scripts/check-version-consistency.sh +1 -1
  33. package/scripts/lib/architecture.sh +126 -0
  34. package/scripts/lib/bootstrap.sh +75 -0
  35. package/scripts/lib/compat.sh +89 -6
  36. package/scripts/lib/config.sh +91 -0
  37. package/scripts/lib/daemon-adaptive.sh +3 -3
  38. package/scripts/lib/daemon-dispatch.sh +39 -16
  39. package/scripts/lib/daemon-health.sh +1 -1
  40. package/scripts/lib/daemon-patrol.sh +24 -12
  41. package/scripts/lib/daemon-poll.sh +37 -25
  42. package/scripts/lib/daemon-state.sh +115 -23
  43. package/scripts/lib/daemon-triage.sh +30 -8
  44. package/scripts/lib/fleet-failover.sh +63 -0
  45. package/scripts/lib/helpers.sh +30 -6
  46. package/scripts/lib/pipeline-detection.sh +2 -2
  47. package/scripts/lib/pipeline-github.sh +9 -9
  48. package/scripts/lib/pipeline-intelligence.sh +85 -35
  49. package/scripts/lib/pipeline-quality-checks.sh +16 -16
  50. package/scripts/lib/pipeline-quality.sh +1 -1
  51. package/scripts/lib/pipeline-stages.sh +242 -28
  52. package/scripts/lib/pipeline-state.sh +40 -4
  53. package/scripts/lib/test-helpers.sh +247 -0
  54. package/scripts/postinstall.mjs +3 -11
  55. package/scripts/sw +10 -4
  56. package/scripts/sw-activity.sh +1 -11
  57. package/scripts/sw-adaptive.sh +109 -85
  58. package/scripts/sw-adversarial.sh +4 -14
  59. package/scripts/sw-architecture-enforcer.sh +1 -11
  60. package/scripts/sw-auth.sh +8 -17
  61. package/scripts/sw-autonomous.sh +111 -49
  62. package/scripts/sw-changelog.sh +1 -11
  63. package/scripts/sw-checkpoint.sh +144 -20
  64. package/scripts/sw-ci.sh +2 -12
  65. package/scripts/sw-cleanup.sh +13 -17
  66. package/scripts/sw-code-review.sh +16 -36
  67. package/scripts/sw-connect.sh +5 -12
  68. package/scripts/sw-context.sh +9 -26
  69. package/scripts/sw-cost.sh +6 -16
  70. package/scripts/sw-daemon.sh +75 -70
  71. package/scripts/sw-dashboard.sh +57 -17
  72. package/scripts/sw-db.sh +506 -15
  73. package/scripts/sw-decompose.sh +1 -11
  74. package/scripts/sw-deps.sh +15 -25
  75. package/scripts/sw-developer-simulation.sh +1 -11
  76. package/scripts/sw-discovery.sh +112 -30
  77. package/scripts/sw-doc-fleet.sh +7 -17
  78. package/scripts/sw-docs-agent.sh +6 -16
  79. package/scripts/sw-docs.sh +4 -12
  80. package/scripts/sw-doctor.sh +134 -43
  81. package/scripts/sw-dora.sh +11 -19
  82. package/scripts/sw-durable.sh +35 -52
  83. package/scripts/sw-e2e-orchestrator.sh +11 -27
  84. package/scripts/sw-eventbus.sh +115 -115
  85. package/scripts/sw-evidence.sh +748 -0
  86. package/scripts/sw-feedback.sh +3 -13
  87. package/scripts/sw-fix.sh +2 -20
  88. package/scripts/sw-fleet-discover.sh +1 -11
  89. package/scripts/sw-fleet-viz.sh +10 -18
  90. package/scripts/sw-fleet.sh +13 -17
  91. package/scripts/sw-github-app.sh +6 -16
  92. package/scripts/sw-github-checks.sh +1 -11
  93. package/scripts/sw-github-deploy.sh +1 -11
  94. package/scripts/sw-github-graphql.sh +2 -12
  95. package/scripts/sw-guild.sh +1 -11
  96. package/scripts/sw-heartbeat.sh +49 -12
  97. package/scripts/sw-hygiene.sh +45 -43
  98. package/scripts/sw-incident.sh +284 -67
  99. package/scripts/sw-init.sh +35 -37
  100. package/scripts/sw-instrument.sh +1 -11
  101. package/scripts/sw-intelligence.sh +362 -51
  102. package/scripts/sw-jira.sh +5 -14
  103. package/scripts/sw-launchd.sh +2 -12
  104. package/scripts/sw-linear.sh +8 -17
  105. package/scripts/sw-logs.sh +4 -12
  106. package/scripts/sw-loop.sh +641 -90
  107. package/scripts/sw-memory.sh +243 -17
  108. package/scripts/sw-mission-control.sh +2 -12
  109. package/scripts/sw-model-router.sh +73 -34
  110. package/scripts/sw-otel.sh +11 -21
  111. package/scripts/sw-oversight.sh +1 -11
  112. package/scripts/sw-patrol-meta.sh +5 -11
  113. package/scripts/sw-pipeline-composer.sh +7 -17
  114. package/scripts/sw-pipeline-vitals.sh +1 -11
  115. package/scripts/sw-pipeline.sh +478 -122
  116. package/scripts/sw-pm.sh +2 -12
  117. package/scripts/sw-pr-lifecycle.sh +203 -29
  118. package/scripts/sw-predictive.sh +16 -22
  119. package/scripts/sw-prep.sh +6 -16
  120. package/scripts/sw-ps.sh +1 -11
  121. package/scripts/sw-public-dashboard.sh +2 -12
  122. package/scripts/sw-quality.sh +77 -10
  123. package/scripts/sw-reaper.sh +1 -11
  124. package/scripts/sw-recruit.sh +15 -25
  125. package/scripts/sw-regression.sh +11 -21
  126. package/scripts/sw-release-manager.sh +19 -28
  127. package/scripts/sw-release.sh +8 -16
  128. package/scripts/sw-remote.sh +1 -11
  129. package/scripts/sw-replay.sh +48 -44
  130. package/scripts/sw-retro.sh +70 -92
  131. package/scripts/sw-review-rerun.sh +220 -0
  132. package/scripts/sw-scale.sh +109 -32
  133. package/scripts/sw-security-audit.sh +12 -22
  134. package/scripts/sw-self-optimize.sh +239 -23
  135. package/scripts/sw-session.sh +3 -13
  136. package/scripts/sw-setup.sh +8 -18
  137. package/scripts/sw-standup.sh +5 -15
  138. package/scripts/sw-status.sh +32 -23
  139. package/scripts/sw-strategic.sh +129 -13
  140. package/scripts/sw-stream.sh +1 -11
  141. package/scripts/sw-swarm.sh +76 -36
  142. package/scripts/sw-team-stages.sh +10 -20
  143. package/scripts/sw-templates.sh +4 -14
  144. package/scripts/sw-testgen.sh +3 -13
  145. package/scripts/sw-tmux-pipeline.sh +1 -19
  146. package/scripts/sw-tmux-role-color.sh +0 -10
  147. package/scripts/sw-tmux-status.sh +3 -11
  148. package/scripts/sw-tmux.sh +2 -20
  149. package/scripts/sw-trace.sh +1 -19
  150. package/scripts/sw-tracker-github.sh +0 -10
  151. package/scripts/sw-tracker-jira.sh +1 -11
  152. package/scripts/sw-tracker-linear.sh +1 -11
  153. package/scripts/sw-tracker.sh +7 -24
  154. package/scripts/sw-triage.sh +24 -34
  155. package/scripts/sw-upgrade.sh +5 -23
  156. package/scripts/sw-ux.sh +1 -19
  157. package/scripts/sw-webhook.sh +18 -32
  158. package/scripts/sw-widgets.sh +3 -21
  159. package/scripts/sw-worktree.sh +11 -27
  160. package/scripts/update-homebrew-sha.sh +67 -0
  161. package/templates/pipelines/tdd.json +72 -0
  162. package/scripts/sw-pipeline.sh.mock +0 -7
@@ -37,6 +37,22 @@ daemon_spawn_pipeline() {
37
37
 
38
38
  daemon_log INFO "Spawning pipeline for issue #${issue_num}: ${issue_title}"
39
39
 
40
+ # ── Budget gate: hard-stop if daily budget exhausted ──
41
+ if [[ -x "${SCRIPT_DIR}/sw-cost.sh" ]]; then
42
+ local remaining
43
+ remaining=$("${SCRIPT_DIR}/sw-cost.sh" remaining-budget 2>/dev/null || echo "")
44
+ if [[ -n "$remaining" && "$remaining" != "unlimited" ]]; then
45
+ if awk -v r="$remaining" 'BEGIN { exit !(r <= 0) }' 2>/dev/null; then
46
+ daemon_log WARN "Budget exhausted (remaining: \$${remaining}) — skipping issue #${issue_num}"
47
+ emit_event "daemon.budget_exhausted" "remaining=$remaining" "issue=$issue_num"
48
+ return 1
49
+ fi
50
+ if awk -v r="$remaining" 'BEGIN { exit !(r < 1.0) }' 2>/dev/null; then
51
+ daemon_log WARN "Budget low: \$${remaining} remaining"
52
+ fi
53
+ fi
54
+ fi
55
+
40
56
  # ── Issue decomposition (if decomposer available) ──
41
57
  local decompose_script="${SCRIPT_DIR}/sw-decompose.sh"
42
58
  if [[ -x "$decompose_script" && "$NO_GITHUB" != "true" ]]; then
@@ -45,7 +61,7 @@ daemon_spawn_pipeline() {
45
61
  if [[ "$decompose_result" == *"decomposed"* ]]; then
46
62
  daemon_log INFO "Issue #${issue_num} decomposed into subtasks — skipping pipeline"
47
63
  # Remove the shipwright label so decomposed parent doesn't re-queue
48
- gh issue edit "$issue_num" --remove-label "shipwright" 2>/dev/null || true
64
+ _timeout 30 gh issue edit "$issue_num" --remove-label "shipwright" 2>/dev/null || true
49
65
  return 0
50
66
  fi
51
67
  fi
@@ -54,14 +70,14 @@ daemon_spawn_pipeline() {
54
70
  local issue_goal="$issue_title"
55
71
  if [[ "$NO_GITHUB" != "true" ]]; then
56
72
  local issue_body_first
57
- issue_body_first=$(gh issue view "$issue_num" --json body --jq '.body' 2>/dev/null | head -3 | tr '\n' ' ' | cut -c1-200 || true)
73
+ issue_body_first=$(_timeout 30 gh issue view "$issue_num" --json body --jq '.body' 2>/dev/null | head -3 | tr '\n' ' ' | cut -c1-200 || true)
58
74
  if [[ -n "$issue_body_first" ]]; then
59
75
  issue_goal="${issue_title}: ${issue_body_first}"
60
76
  fi
61
77
  fi
62
78
 
63
79
  # ── Predictive risk assessment (if enabled) ──
64
- if [[ "${PREDICTION_ENABLED:-false}" == "true" ]] && type predict_pipeline_risk &>/dev/null 2>&1; then
80
+ if [[ "${PREDICTION_ENABLED:-false}" == "true" ]] && type predict_pipeline_risk >/dev/null 2>&1; then
65
81
  local issue_json_for_pred=""
66
82
  if [[ "$NO_GITHUB" != "true" ]]; then
67
83
  issue_json_for_pred=$(gh issue view "$issue_num" --json number,title,body,labels 2>/dev/null || echo "")
@@ -214,7 +230,7 @@ daemon_track_job() {
214
230
  local issue_num="$1" pid="$2" worktree="$3" title="${4:-}" repo="${5:-}" goal="${6:-}"
215
231
 
216
232
  # Write to SQLite (non-blocking, best-effort)
217
- if type db_save_job &>/dev/null; then
233
+ if type db_save_job >/dev/null 2>&1; then
218
234
  local job_id="daemon-${issue_num}-$(now_epoch)"
219
235
  db_save_job "$job_id" "$issue_num" "$title" "$pid" "$worktree" "" "${PIPELINE_TEMPLATE:-autonomous}" "$goal" 2>/dev/null || true
220
236
  fi
@@ -309,7 +325,7 @@ daemon_reap_completed() {
309
325
  emit_event "daemon.reap" "issue=$issue_num" "result=$result_str" "duration_s=$dur_s"
310
326
 
311
327
  # Update SQLite (mark job complete/failed)
312
- if type db_complete_job &>/dev/null && type db_fail_job &>/dev/null; then
328
+ if type db_complete_job >/dev/null 2>&1 && type db_fail_job >/dev/null 2>&1; then
313
329
  local _db_job_id="daemon-${issue_num}-${start_epoch}"
314
330
  if [[ "$exit_code" -eq 0 ]]; then
315
331
  db_complete_job "$_db_job_id" "$result_str" 2>/dev/null || true
@@ -343,7 +359,7 @@ daemon_reap_completed() {
343
359
  --method PATCH \
344
360
  --field status=completed \
345
361
  --field conclusion=cancelled \
346
- --silent 2>/dev/null || true
362
+ --silent --timeout 30 2>/dev/null || true
347
363
  fi
348
364
  fi
349
365
  done < <(jq -r 'keys[]' "$check_ids_file" 2>/dev/null || true)
@@ -352,13 +368,18 @@ daemon_reap_completed() {
352
368
  fi
353
369
 
354
370
  # Finalize memory (capture failure patterns for future runs)
355
- if type memory_finalize_pipeline &>/dev/null 2>&1; then
371
+ if type memory_finalize_pipeline >/dev/null 2>&1; then
356
372
  local _job_state _job_artifacts
357
373
  _job_state="${worktree:-.}/.claude/pipeline-state.md"
358
374
  _job_artifacts="${worktree:-.}/.claude/pipeline-artifacts"
359
375
  memory_finalize_pipeline "$_job_state" "$_job_artifacts" 2>/dev/null || true
360
376
  fi
361
377
 
378
+ # Trigger learning after pipeline reap
379
+ if type optimize_full_analysis &>/dev/null; then
380
+ optimize_full_analysis &>/dev/null &
381
+ fi
382
+
362
383
  # Clean up progress tracking for this job
363
384
  daemon_clear_progress "$issue_num"
364
385
 
@@ -400,13 +421,15 @@ daemon_reap_completed() {
400
421
  local current_active
401
422
  current_active=$(locked_get_active_count)
402
423
  if [[ "$current_active" -lt "$MAX_PARALLEL" ]]; then
403
- local next_issue
404
- next_issue=$(dequeue_next)
405
- if [[ -n "$next_issue" ]]; then
424
+ local next_issue_key
425
+ next_issue_key=$(dequeue_next)
426
+ if [[ -n "$next_issue_key" ]]; then
427
+ local next_issue_num="$next_issue_key" next_repo=""
428
+ [[ "$next_issue_key" == *:* ]] && next_repo="${next_issue_key%%:*}" && next_issue_num="${next_issue_key##*:}"
406
429
  local next_title
407
- next_title=$(jq -r --arg n "$next_issue" '.titles[$n] // ""' "$STATE_FILE" 2>/dev/null || true)
408
- daemon_log INFO "Dequeuing issue #${next_issue}: ${next_title}"
409
- daemon_spawn_pipeline "$next_issue" "$next_title"
430
+ next_title=$(jq -r --arg n "$next_issue_key" '.titles[$n] // ""' "$STATE_FILE" 2>/dev/null || true)
431
+ daemon_log INFO "Dequeuing issue #${next_issue_num}${next_repo:+, repo=${next_repo}}: ${next_title}"
432
+ daemon_spawn_pipeline "$next_issue_num" "$next_title" "$next_repo"
410
433
  fi
411
434
  fi
412
435
  done <<< "$jobs"
@@ -453,12 +476,12 @@ daemon_on_success() {
453
476
 
454
477
  if [[ "$NO_GITHUB" != "true" ]]; then
455
478
  # Remove watch label, add success label
456
- gh issue edit "$issue_num" \
479
+ _timeout 30 gh issue edit "$issue_num" \
457
480
  --remove-label "$ON_SUCCESS_REMOVE_LABEL" \
458
481
  --add-label "$ON_SUCCESS_ADD_LABEL" 2>/dev/null || true
459
482
 
460
483
  # Comment on issue
461
- gh issue comment "$issue_num" --body "## ✅ Pipeline Complete
484
+ _timeout 30 gh issue comment "$issue_num" --body "## ✅ Pipeline Complete
462
485
 
463
486
  The autonomous pipeline finished successfully.
464
487
 
@@ -471,7 +494,7 @@ Check the associated PR for the implementation." 2>/dev/null || true
471
494
 
472
495
  # Optionally close the issue
473
496
  if [[ "$ON_SUCCESS_CLOSE_ISSUE" == "true" ]]; then
474
- gh issue close "$issue_num" 2>/dev/null || true
497
+ _timeout 30 gh issue close "$issue_num" 2>/dev/null || true
475
498
  fi
476
499
  fi
477
500
 
@@ -11,7 +11,7 @@ _DAEMON_HEALTH_LOADED=1
11
11
  daemon_health_timeout_for_stage() {
12
12
  local stage="${1:-unknown}"
13
13
  local fallback="${2:-120}"
14
- if type policy_get &>/dev/null 2>&1; then
14
+ if type policy_get >/dev/null 2>&1; then
15
15
  local policy_val
16
16
  policy_val=$(policy_get ".daemon.stage_timeouts.$stage" "")
17
17
  if [[ -n "$policy_val" && "$policy_val" =~ ^[0-9]+$ ]]; then
@@ -45,10 +45,22 @@ daemon_patrol() {
45
45
  local findings=0
46
46
 
47
47
  # npm audit
48
- if [[ -f "package.json" ]] && command -v npm &>/dev/null; then
48
+ if [[ -f "package.json" ]] && command -v npm >/dev/null 2>&1; then
49
49
  local audit_json
50
- audit_json=$(npm audit --json 2>/dev/null || true)
51
- if [[ -n "$audit_json" ]]; then
50
+ audit_json=$(npm audit --json 2>/dev/null || echo '{}')
51
+ local audit_version
52
+ audit_version=$(echo "$audit_json" | jq -r '.auditReportVersion // 1')
53
+
54
+ local vuln_list
55
+ if [[ "$audit_version" == "2" ]]; then
56
+ # npm 7+ format: .vulnerabilities is an object keyed by package name
57
+ vuln_list=$(echo "$audit_json" | jq -c '[.vulnerabilities | to_entries[] | .value | {name: .name, severity: .severity, url: (.via[0].url // "N/A"), title: (.via[0].title // .name)}]' 2>/dev/null || echo '[]')
58
+ else
59
+ # npm 6 format: .advisories is an object keyed by advisory ID
60
+ vuln_list=$(echo "$audit_json" | jq -c '[.advisories | to_entries[] | .value | {name: .module_name, severity: .severity, url: .url, title: .title}]' 2>/dev/null || echo '[]')
61
+ fi
62
+
63
+ if [[ -n "$vuln_list" && "$vuln_list" != "[]" ]]; then
52
64
  while IFS= read -r vuln; do
53
65
  local severity name advisory_url title
54
66
  severity=$(echo "$vuln" | jq -r '.severity // "unknown"')
@@ -90,12 +102,12 @@ Auto-detected by \`shipwright daemon patrol\`." \
90
102
  else
91
103
  echo -e " ${RED}●${RESET} ${BOLD}${severity}${RESET}: ${title} in ${CYAN}${name}${RESET}"
92
104
  fi
93
- done < <(echo "$audit_json" | jq -c '.vulnerabilities | to_entries[] | .value' 2>/dev/null)
105
+ done < <(echo "$vuln_list" | jq -c '.[]' 2>/dev/null)
94
106
  fi
95
107
  fi
96
108
 
97
109
  # pip-audit
98
- if [[ -f "requirements.txt" ]] && command -v pip-audit &>/dev/null; then
110
+ if [[ -f "requirements.txt" ]] && command -v pip-audit >/dev/null 2>&1; then
99
111
  local pip_json
100
112
  pip_json=$(pip-audit --format=json 2>/dev/null || true)
101
113
  if [[ -n "$pip_json" ]]; then
@@ -106,7 +118,7 @@ Auto-detected by \`shipwright daemon patrol\`." \
106
118
  fi
107
119
 
108
120
  # cargo audit
109
- if [[ -f "Cargo.toml" ]] && command -v cargo-audit &>/dev/null; then
121
+ if [[ -f "Cargo.toml" ]] && command -v cargo-audit >/dev/null 2>&1; then
110
122
  local cargo_json
111
123
  cargo_json=$(cargo audit --json 2>/dev/null || true)
112
124
  if [[ -n "$cargo_json" ]]; then
@@ -117,8 +129,8 @@ Auto-detected by \`shipwright daemon patrol\`." \
117
129
  fi
118
130
 
119
131
  # Enrich with GitHub security alerts
120
- if type gh_security_alerts &>/dev/null 2>&1 && [[ "${NO_GITHUB:-false}" != "true" ]]; then
121
- if type _gh_detect_repo &>/dev/null 2>&1; then
132
+ if type gh_security_alerts >/dev/null 2>&1 && [[ "${NO_GITHUB:-false}" != "true" ]]; then
133
+ if type _gh_detect_repo >/dev/null 2>&1; then
122
134
  _gh_detect_repo 2>/dev/null || true
123
135
  fi
124
136
  local gh_owner="${GH_OWNER:-}" gh_repo="${GH_REPO:-}"
@@ -135,7 +147,7 @@ Auto-detected by \`shipwright daemon patrol\`." \
135
147
  fi
136
148
 
137
149
  # Enrich with GitHub Dependabot alerts
138
- if type gh_dependabot_alerts &>/dev/null 2>&1 && [[ "${NO_GITHUB:-false}" != "true" ]]; then
150
+ if type gh_dependabot_alerts >/dev/null 2>&1 && [[ "${NO_GITHUB:-false}" != "true" ]]; then
139
151
  local gh_owner="${GH_OWNER:-}" gh_repo="${GH_REPO:-}"
140
152
  if [[ -n "$gh_owner" && -n "$gh_repo" ]]; then
141
153
  local dep_alerts
@@ -162,7 +174,7 @@ Auto-detected by \`shipwright daemon patrol\`." \
162
174
  daemon_log INFO "Patrol: checking for stale dependencies"
163
175
  local findings=0
164
176
 
165
- if [[ -f "package.json" ]] && command -v npm &>/dev/null; then
177
+ if [[ -f "package.json" ]] && command -v npm >/dev/null 2>&1; then
166
178
  local outdated_json
167
179
  outdated_json=$(npm outdated --json 2>/dev/null || true)
168
180
  if [[ -n "$outdated_json" ]] && [[ "$outdated_json" != "{}" ]]; then
@@ -534,7 +546,7 @@ Auto-detected by \`shipwright daemon patrol\` on $(now_iso)." \
534
546
  failures_json=$(
535
547
  (
536
548
  source "$memory_script" > /dev/null 2>&1 || true
537
- if command -v memory_get_actionable_failures &>/dev/null; then
549
+ if command -v memory_get_actionable_failures >/dev/null 2>&1; then
538
550
  memory_get_actionable_failures "$PATROL_FAILURES_THRESHOLD"
539
551
  else
540
552
  echo "[]"
@@ -1046,7 +1058,7 @@ Auto-detected by \`shipwright daemon patrol\` on $(now_iso)." \
1046
1058
  echo ""
1047
1059
 
1048
1060
  # ── Stage 2: AI-Powered Confirmation (if enabled) ──
1049
- if [[ "${PREDICTION_ENABLED:-false}" == "true" ]] && type patrol_ai_analyze &>/dev/null 2>&1; then
1061
+ if [[ "${PREDICTION_ENABLED:-false}" == "true" ]] && type patrol_ai_analyze >/dev/null 2>&1; then
1050
1062
  daemon_log INFO "Intelligence: using AI patrol analysis (prediction enabled)"
1051
1063
  echo -e " ${BOLD}AI Deep Analysis${RESET}"
1052
1064
  # Sample recent source files for AI analysis
@@ -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 20 2>/dev/null) || {
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 20 2>/dev/null) || {
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 "$issue_num" --arg title "$issue_title" \
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 "$issue_num"; then
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 "$issue_num"
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 drain_issue
312
- drain_issue=$(dequeue_next)
313
- [[ -z "$drain_issue" ]] && break
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 "$drain_issue" '.titles[$n] // ""' "$STATE_FILE" 2>/dev/null || true)
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 "$drain_issue" \
319
- '.[] | select(.number == $n) | [.labels[].name] | join(",")' 2>/dev/null || echo "")
320
- drain_score=$(echo "$sorted_order" | grep "|${drain_issue}|" | cut -d'|' -f1 || echo "50")
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 #${drain_issue}, template=${drain_template}"
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 "$drain_issue" "$drain_title"
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 &>/dev/null 2>&1 && [[ -f "$STATE_FILE" ]]; then
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 &>/dev/null 2>&1; then
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 &>/dev/null; then
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=$(stat -f '%m' "$wt_path" 2>/dev/null || stat -c '%Y' "$wt_path" 2>/dev/null || echo "0")
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=$(stat -f '%m' "$artifact_dir" 2>/dev/null || stat -c '%Y' "$artifact_dir" 2>/dev/null || echo "0")
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 &>/dev/null; then
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=$(stat -f '%m' "$pipeline_state" 2>/dev/null || stat -c '%Y' "$pipeline_state" 2>/dev/null || echo "0")
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 &>/dev/null && [[ "${NO_GITHUB:-}" != "true" ]]; then
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