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.
Files changed (169) hide show
  1. package/README.md +16 -11
  2. package/completions/_shipwright +248 -94
  3. package/completions/shipwright.bash +68 -19
  4. package/completions/shipwright.fish +310 -42
  5. package/config/decision-tiers.json +55 -0
  6. package/config/defaults.json +111 -0
  7. package/config/event-schema.json +218 -0
  8. package/config/policy.json +21 -18
  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 +7 -9
  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 +127 -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 +63 -17
  39. package/scripts/lib/daemon-failure.sh +0 -0
  40. package/scripts/lib/daemon-health.sh +1 -1
  41. package/scripts/lib/daemon-patrol.sh +64 -17
  42. package/scripts/lib/daemon-poll.sh +54 -25
  43. package/scripts/lib/daemon-state.sh +125 -23
  44. package/scripts/lib/daemon-triage.sh +31 -9
  45. package/scripts/lib/decide-autonomy.sh +295 -0
  46. package/scripts/lib/decide-scoring.sh +228 -0
  47. package/scripts/lib/decide-signals.sh +462 -0
  48. package/scripts/lib/fleet-failover.sh +63 -0
  49. package/scripts/lib/helpers.sh +29 -6
  50. package/scripts/lib/pipeline-detection.sh +2 -2
  51. package/scripts/lib/pipeline-github.sh +9 -9
  52. package/scripts/lib/pipeline-intelligence.sh +105 -38
  53. package/scripts/lib/pipeline-quality-checks.sh +17 -16
  54. package/scripts/lib/pipeline-quality.sh +1 -1
  55. package/scripts/lib/pipeline-stages.sh +440 -59
  56. package/scripts/lib/pipeline-state.sh +54 -4
  57. package/scripts/lib/policy.sh +0 -0
  58. package/scripts/lib/test-helpers.sh +247 -0
  59. package/scripts/postinstall.mjs +78 -12
  60. package/scripts/signals/example-collector.sh +36 -0
  61. package/scripts/sw +17 -7
  62. package/scripts/sw-activity.sh +1 -11
  63. package/scripts/sw-adaptive.sh +109 -85
  64. package/scripts/sw-adversarial.sh +4 -14
  65. package/scripts/sw-architecture-enforcer.sh +1 -11
  66. package/scripts/sw-auth.sh +8 -17
  67. package/scripts/sw-autonomous.sh +111 -49
  68. package/scripts/sw-changelog.sh +1 -11
  69. package/scripts/sw-checkpoint.sh +144 -20
  70. package/scripts/sw-ci.sh +2 -12
  71. package/scripts/sw-cleanup.sh +13 -17
  72. package/scripts/sw-code-review.sh +16 -36
  73. package/scripts/sw-connect.sh +5 -12
  74. package/scripts/sw-context.sh +9 -26
  75. package/scripts/sw-cost.sh +17 -18
  76. package/scripts/sw-daemon.sh +76 -71
  77. package/scripts/sw-dashboard.sh +57 -17
  78. package/scripts/sw-db.sh +524 -26
  79. package/scripts/sw-decide.sh +685 -0
  80. package/scripts/sw-decompose.sh +1 -11
  81. package/scripts/sw-deps.sh +15 -25
  82. package/scripts/sw-developer-simulation.sh +1 -11
  83. package/scripts/sw-discovery.sh +138 -30
  84. package/scripts/sw-doc-fleet.sh +7 -17
  85. package/scripts/sw-docs-agent.sh +6 -16
  86. package/scripts/sw-docs.sh +4 -12
  87. package/scripts/sw-doctor.sh +134 -43
  88. package/scripts/sw-dora.sh +11 -19
  89. package/scripts/sw-durable.sh +35 -52
  90. package/scripts/sw-e2e-orchestrator.sh +11 -27
  91. package/scripts/sw-eventbus.sh +115 -115
  92. package/scripts/sw-evidence.sh +114 -30
  93. package/scripts/sw-feedback.sh +3 -13
  94. package/scripts/sw-fix.sh +2 -20
  95. package/scripts/sw-fleet-discover.sh +1 -11
  96. package/scripts/sw-fleet-viz.sh +10 -18
  97. package/scripts/sw-fleet.sh +13 -17
  98. package/scripts/sw-github-app.sh +6 -16
  99. package/scripts/sw-github-checks.sh +1 -11
  100. package/scripts/sw-github-deploy.sh +1 -11
  101. package/scripts/sw-github-graphql.sh +2 -12
  102. package/scripts/sw-guild.sh +1 -11
  103. package/scripts/sw-heartbeat.sh +49 -12
  104. package/scripts/sw-hygiene.sh +45 -43
  105. package/scripts/sw-incident.sh +48 -74
  106. package/scripts/sw-init.sh +35 -37
  107. package/scripts/sw-instrument.sh +1 -11
  108. package/scripts/sw-intelligence.sh +368 -53
  109. package/scripts/sw-jira.sh +5 -14
  110. package/scripts/sw-launchd.sh +2 -12
  111. package/scripts/sw-linear.sh +8 -17
  112. package/scripts/sw-logs.sh +4 -12
  113. package/scripts/sw-loop.sh +905 -104
  114. package/scripts/sw-memory.sh +263 -20
  115. package/scripts/sw-mission-control.sh +2 -12
  116. package/scripts/sw-model-router.sh +73 -34
  117. package/scripts/sw-otel.sh +15 -23
  118. package/scripts/sw-oversight.sh +1 -11
  119. package/scripts/sw-patrol-meta.sh +5 -11
  120. package/scripts/sw-pipeline-composer.sh +7 -17
  121. package/scripts/sw-pipeline-vitals.sh +1 -11
  122. package/scripts/sw-pipeline.sh +550 -122
  123. package/scripts/sw-pm.sh +2 -12
  124. package/scripts/sw-pr-lifecycle.sh +33 -28
  125. package/scripts/sw-predictive.sh +16 -22
  126. package/scripts/sw-prep.sh +6 -16
  127. package/scripts/sw-ps.sh +1 -11
  128. package/scripts/sw-public-dashboard.sh +2 -12
  129. package/scripts/sw-quality.sh +85 -14
  130. package/scripts/sw-reaper.sh +1 -11
  131. package/scripts/sw-recruit.sh +15 -25
  132. package/scripts/sw-regression.sh +11 -21
  133. package/scripts/sw-release-manager.sh +19 -28
  134. package/scripts/sw-release.sh +8 -16
  135. package/scripts/sw-remote.sh +1 -11
  136. package/scripts/sw-replay.sh +48 -44
  137. package/scripts/sw-retro.sh +70 -92
  138. package/scripts/sw-review-rerun.sh +1 -1
  139. package/scripts/sw-scale.sh +174 -41
  140. package/scripts/sw-security-audit.sh +12 -22
  141. package/scripts/sw-self-optimize.sh +239 -23
  142. package/scripts/sw-session.sh +5 -15
  143. package/scripts/sw-setup.sh +8 -18
  144. package/scripts/sw-standup.sh +5 -15
  145. package/scripts/sw-status.sh +32 -23
  146. package/scripts/sw-strategic.sh +129 -13
  147. package/scripts/sw-stream.sh +1 -11
  148. package/scripts/sw-swarm.sh +76 -36
  149. package/scripts/sw-team-stages.sh +10 -20
  150. package/scripts/sw-templates.sh +4 -14
  151. package/scripts/sw-testgen.sh +3 -13
  152. package/scripts/sw-tmux-pipeline.sh +1 -19
  153. package/scripts/sw-tmux-role-color.sh +0 -10
  154. package/scripts/sw-tmux-status.sh +3 -11
  155. package/scripts/sw-tmux.sh +2 -20
  156. package/scripts/sw-trace.sh +1 -19
  157. package/scripts/sw-tracker-github.sh +0 -10
  158. package/scripts/sw-tracker-jira.sh +1 -11
  159. package/scripts/sw-tracker-linear.sh +1 -11
  160. package/scripts/sw-tracker.sh +7 -24
  161. package/scripts/sw-triage.sh +29 -39
  162. package/scripts/sw-upgrade.sh +5 -23
  163. package/scripts/sw-ux.sh +1 -19
  164. package/scripts/sw-webhook.sh +18 -32
  165. package/scripts/sw-widgets.sh +3 -21
  166. package/scripts/sw-worktree.sh +11 -27
  167. package/scripts/update-homebrew-sha.sh +73 -0
  168. package/templates/pipelines/tdd.json +72 -0
  169. package/scripts/sw-pipeline.sh.mock +0 -7
@@ -4,7 +4,7 @@
4
4
  # ║ ║
5
5
  # ║ Shows running teams, agent windows, and task progress. ║
6
6
  # ╚═══════════════════════════════════════════════════════════════════════════╝
7
- VERSION="2.4.0"
7
+ VERSION="3.1.0"
8
8
  set -euo pipefail
9
9
  trap 'echo "ERROR: $BASH_SOURCE:$LINENO exited with status $?" >&2' ERR
10
10
 
@@ -17,6 +17,24 @@ _COMPAT="$SCRIPT_DIR/lib/compat.sh"
17
17
  # Canonical helpers (colors, output, events)
18
18
  # shellcheck source=lib/helpers.sh
19
19
  [[ -f "$SCRIPT_DIR/lib/helpers.sh" ]] && source "$SCRIPT_DIR/lib/helpers.sh"
20
+ [[ -f "$SCRIPT_DIR/lib/config.sh" ]] && source "$SCRIPT_DIR/lib/config.sh"
21
+
22
+ # Portable ISO timestamp parsing (macOS uses -j -f, Linux uses -d)
23
+ _parse_iso_epoch() {
24
+ local ts="$1"
25
+ if date -j -f "%Y-%m-%dT%H:%M:%SZ" "$ts" "+%s" 2>/dev/null; then
26
+ return
27
+ fi
28
+ # Linux: date -d handles ISO format
29
+ date -d "$ts" "+%s" 2>/dev/null || echo "0"
30
+ }
31
+ _format_iso_time() {
32
+ local ts="$1" fmt="${2:-+%H:%M}"
33
+ if date -j -f "%Y-%m-%dT%H:%M:%SZ" "$ts" "$fmt" 2>/dev/null; then
34
+ return
35
+ fi
36
+ date -d "$ts" "$fmt" 2>/dev/null || echo ""
37
+ }
20
38
  # Fallbacks when helpers not loaded (e.g. test env with overridden SCRIPT_DIR)
21
39
  [[ "$(type -t info 2>/dev/null)" == "function" ]] || info() { echo -e "\033[38;2;0;212;255m\033[1m▸\033[0m $*"; }
22
40
  [[ "$(type -t success 2>/dev/null)" == "function" ]] || success() { echo -e "\033[38;2;74;222;128m\033[1m✓\033[0m $*"; }
@@ -34,15 +52,6 @@ if [[ "$(type -t emit_event 2>/dev/null)" != "function" ]]; then
34
52
  echo "${payload}}" >> "${HOME}/.shipwright/events.jsonl"
35
53
  }
36
54
  fi
37
- CYAN="${CYAN:-\033[38;2;0;212;255m}"
38
- PURPLE="${PURPLE:-\033[38;2;124;58;237m}"
39
- BLUE="${BLUE:-\033[38;2;0;102;255m}"
40
- GREEN="${GREEN:-\033[38;2;74;222;128m}"
41
- YELLOW="${YELLOW:-\033[38;2;250;204;21m}"
42
- RED="${RED:-\033[38;2;248;113;113m}"
43
- DIM="${DIM:-\033[2m}"
44
- BOLD="${BOLD:-\033[1m}"
45
- RESET="${RESET:-\033[0m}"
46
55
 
47
56
  # ─── Argument Parsing ─────────────────────────────────────────────────────────
48
57
  JSON_OUTPUT="false"
@@ -63,14 +72,14 @@ done
63
72
 
64
73
  # ─── JSON Output Mode ─────────────────────────────────────────────────────────
65
74
  if [[ "$JSON_OUTPUT" == "true" ]]; then
66
- if ! command -v jq &>/dev/null; then
75
+ if ! command -v jq >/dev/null 2>&1; then
67
76
  echo "Error: jq is required for --json output" >&2
68
77
  exit 1
69
78
  fi
70
79
 
71
80
  # -- tmux windows --
72
81
  WINDOWS_JSON="[]"
73
- if command -v tmux &>/dev/null; then
82
+ if command -v tmux >/dev/null 2>&1; then
74
83
  WINDOWS_JSON=$(tmux list-windows -a -F '#{session_name}:#{window_index}|#{window_name}|#{window_panes}|#{window_active}' 2>/dev/null | \
75
84
  while IFS='|' read -r sw wn pc act; do
76
85
  is_claude="false"
@@ -195,7 +204,7 @@ if [[ "$JSON_OUTPUT" == "true" ]]; then
195
204
  _team_cfg="${HOME}/.shipwright/team-config.json"
196
205
  if [[ -f "$_team_cfg" ]]; then
197
206
  _dash_url=$(jq -r '.dashboard_url // ""' "$_team_cfg" 2>/dev/null || true)
198
- if [[ -n "$_dash_url" ]] && command -v curl &>/dev/null; then
207
+ if [[ -n "$_dash_url" ]] && command -v curl >/dev/null 2>&1; then
199
208
  _api_resp=$(curl -s --max-time 3 "${_dash_url}/api/status" 2>/dev/null || echo "")
200
209
  if [[ -n "$_api_resp" ]] && echo "$_api_resp" | jq empty 2>/dev/null; then
201
210
  _online=$(echo "$_api_resp" | jq '.total_online // 0' 2>/dev/null || echo "0")
@@ -211,7 +220,7 @@ if [[ "$JSON_OUTPUT" == "true" ]]; then
211
220
  # -- database --
212
221
  DATABASE_JSON="null"
213
222
  _db_file="${HOME}/.shipwright/shipwright.db"
214
- if command -v sqlite3 &>/dev/null && [[ -f "$_db_file" ]]; then
223
+ if command -v sqlite3 >/dev/null 2>&1 && [[ -f "$_db_file" ]]; then
215
224
  _db_ver=$(sqlite3 "$_db_file" "SELECT MAX(version) FROM _schema;" 2>/dev/null || echo "0")
216
225
  _db_wal=$(sqlite3 "$_db_file" "PRAGMA journal_mode;" 2>/dev/null || echo "unknown")
217
226
  _db_events=$(sqlite3 "$_db_file" "SELECT COUNT(*) FROM events;" 2>/dev/null || echo "0")
@@ -433,7 +442,7 @@ if [[ -f "$STATE_FILE" ]]; then
433
442
  # Calculate uptime
434
443
  uptime_str=""
435
444
  if [[ "$started_at" != "unknown" && "$started_at" != "null" ]]; then
436
- start_epoch=$(TZ=UTC date -j -f "%Y-%m-%dT%H:%M:%SZ" "$started_at" +%s 2>/dev/null || echo 0)
445
+ start_epoch=$(_parse_iso_epoch "$started_at")
437
446
  if [[ "$start_epoch" -gt 0 ]]; then
438
447
  now_e=$(date +%s)
439
448
  elapsed=$((now_e - start_epoch))
@@ -471,7 +480,7 @@ if [[ -f "$STATE_FILE" ]]; then
471
480
  # Time elapsed
472
481
  age_str=""
473
482
  if [[ -n "$a_started" && "$a_started" != "null" ]]; then
474
- s_epoch=$(TZ=UTC date -j -f "%Y-%m-%dT%H:%M:%SZ" "$a_started" +%s 2>/dev/null || echo 0)
483
+ s_epoch=$(_parse_iso_epoch "$a_started")
475
484
  if [[ "$s_epoch" -gt 0 ]]; then
476
485
  now_e=$(date +%s)
477
486
  el=$((now_e - s_epoch))
@@ -592,7 +601,7 @@ if [[ -f "$STATE_FILE" ]]; then
592
601
  # Format timestamp as HH:MM
593
602
  evt_time=""
594
603
  if [[ -n "$evt_ts" && "$evt_ts" != "null" ]]; then
595
- evt_time=$(TZ=UTC date -j -f "%Y-%m-%dT%H:%M:%SZ" "$evt_ts" +"%H:%M" 2>/dev/null || echo "")
604
+ evt_time=$(_format_iso_time "$evt_ts" "+%H:%M")
596
605
  fi
597
606
 
598
607
  case "$evt_type" in
@@ -678,7 +687,7 @@ if [[ -d "$HEARTBEAT_DIR" ]]; then
678
687
 
679
688
  for hb_file in "${HEARTBEAT_DIR}"/*.json; do
680
689
  [[ -f "$hb_file" ]] || continue
681
- local_job_id="$(basename "$hb_file" .json)"
690
+ job_id="$(basename "$hb_file" .json)"
682
691
  hb_pid=$(jq -r '.pid // ""' "$hb_file" 2>/dev/null || true)
683
692
  hb_stage=$(jq -r '.stage // ""' "$hb_file" 2>/dev/null || true)
684
693
  hb_issue=$(jq -r '.issue // ""' "$hb_file" 2>/dev/null || true)
@@ -696,7 +705,7 @@ if [[ -d "$HEARTBEAT_DIR" ]]; then
696
705
  # Calculate age
697
706
  hb_age_str=""
698
707
  if [[ -n "$hb_updated" && "$hb_updated" != "null" ]]; then
699
- hb_epoch=$(TZ=UTC date -j -f "%Y-%m-%dT%H:%M:%SZ" "$hb_updated" +%s 2>/dev/null || echo 0)
708
+ hb_epoch=$(_parse_iso_epoch "$hb_updated")
700
709
  if [[ "$hb_epoch" -gt 0 ]]; then
701
710
  now_e=$(date +%s)
702
711
  hb_age=$((now_e - hb_epoch))
@@ -714,7 +723,7 @@ if [[ -d "$HEARTBEAT_DIR" ]]; then
714
723
  hb_icon="${RED}●${RESET}"
715
724
  fi
716
725
 
717
- echo -e " ${hb_icon} ${BOLD}${local_job_id}${RESET} ${DIM}pid:${hb_pid}${RESET}"
726
+ echo -e " ${hb_icon} ${BOLD}${job_id}${RESET} ${DIM}pid:${hb_pid}${RESET}"
718
727
  detail_line=" "
719
728
  [[ -n "$hb_issue" && "$hb_issue" != "null" && "$hb_issue" != "0" ]] && detail_line+="${CYAN}#${hb_issue}${RESET} "
720
729
  [[ -n "$hb_stage" && "$hb_stage" != "null" ]] && detail_line+="${BLUE}${hb_stage}${RESET} "
@@ -753,7 +762,7 @@ fi
753
762
  # ─── Database ────────────────────────────────────────────────────────────
754
763
 
755
764
  _DB_FILE="${HOME}/.shipwright/shipwright.db"
756
- if command -v sqlite3 &>/dev/null && [[ -f "$_DB_FILE" ]]; then
765
+ if command -v sqlite3 >/dev/null 2>&1 && [[ -f "$_DB_FILE" ]]; then
757
766
  echo ""
758
767
  echo -e "${PURPLE}${BOLD} DATABASE${RESET} ${DIM}~/.shipwright/shipwright.db${RESET}"
759
768
  echo -e "${DIM} ──────────────────────────────────────────${RESET}"
@@ -773,14 +782,14 @@ fi
773
782
  # ─── Connected Developers ─────────────────────────────────────────────────
774
783
 
775
784
  # Check if curl and jq are available
776
- if command -v curl &>/dev/null && command -v jq &>/dev/null; then
785
+ if command -v curl >/dev/null 2>&1 && command -v jq >/dev/null 2>&1; then
777
786
  # Read dashboard URL from config, fall back to default
778
787
  TEAM_CONFIG="${HOME}/.shipwright/team-config.json"
779
788
  DASHBOARD_URL=""
780
789
  if [[ -f "$TEAM_CONFIG" ]]; then
781
790
  DASHBOARD_URL=$(jq -r '.dashboard_url // ""' "$TEAM_CONFIG" 2>/dev/null || true)
782
791
  fi
783
- [[ -z "$DASHBOARD_URL" ]] && DASHBOARD_URL="http://localhost:8767"
792
+ [[ -z "$DASHBOARD_URL" ]] && DASHBOARD_URL="http://localhost:$(_config_get_int "dashboard.port" 8767)"
784
793
 
785
794
  # Try to reach the dashboard /api/team endpoint with 3s timeout
786
795
  api_response=$(curl -s --max-time 3 "$DASHBOARD_URL/api/team" 2>/dev/null || true)
@@ -7,7 +7,7 @@
7
7
  # When sourced, do NOT add set -euo pipefail — the parent handles that.
8
8
  # When run directly, main() sets up the error handling.
9
9
 
10
- VERSION="2.4.0"
10
+ VERSION="3.1.0"
11
11
 
12
12
  # ─── Paths (set defaults if not provided by parent) ──────────────────────────
13
13
  SCRIPT_DIR="${SCRIPT_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)}"
@@ -34,15 +34,14 @@ if [[ "$(type -t now_iso 2>/dev/null)" != "function" ]]; then
34
34
  now_iso() { date -u +"%Y-%m-%dT%H:%M:%SZ"; }
35
35
  now_epoch() { date +%s; }
36
36
  fi
37
- CYAN="${CYAN:-\033[38;2;0;212;255m}"
38
- PURPLE="${PURPLE:-\033[38;2;124;58;237m}"
39
- GREEN="${GREEN:-\033[38;2;74;222;128m}"
40
- YELLOW="${YELLOW:-\033[38;2;250;204;21m}"
41
- RED="${RED:-\033[38;2;248;113;113m}"
42
- DIM="${DIM:-\033[2m}"
43
- BOLD="${BOLD:-\033[1m}"
44
- RESET="${RESET:-\033[0m}"
45
-
37
+ # Color fallbacks when helpers not loaded (e.g. test env with overridden SCRIPT_DIR)
38
+ [[ -z "${PURPLE+set}" ]] && PURPLE='\033[38;2;124;58;237m'
39
+ [[ -z "${BOLD+set}" ]] && BOLD='\033[1m'
40
+ [[ -z "${DIM+set}" ]] && DIM='\033[2m'
41
+ [[ -z "${RESET+set}" ]] && RESET='\033[0m'
42
+ [[ -z "${YELLOW+set}" ]] && YELLOW='\033[38;2;250;204;21m'
43
+ [[ -z "${GREEN+set}" ]] && GREEN='\033[38;2;74;222;128m'
44
+ [[ -z "${RED+set}" ]] && RED='\033[38;2;248;113;113m'
46
45
  # ─── Constants (policy overrides when config/policy.json exists) ─────────────
47
46
  STRATEGIC_MAX_ISSUES=5
48
47
  STRATEGIC_COOLDOWN_SECONDS=14400 # 4 hours
@@ -52,7 +51,7 @@ STRATEGIC_STRATEGY_LINES=200
52
51
  STRATEGIC_LABELS="auto-patrol,ready-to-build,strategic,shipwright"
53
52
  STRATEGIC_OVERLAP_THRESHOLD=60 # Skip if >60% word overlap
54
53
  [[ -f "${SCRIPT_DIR:-}/lib/policy.sh" ]] && source "${SCRIPT_DIR:-}/lib/policy.sh"
55
- if type policy_get &>/dev/null 2>&1; then
54
+ if type policy_get >/dev/null 2>&1; then
56
55
  STRATEGIC_MAX_ISSUES=$(policy_get ".strategic.max_issues_per_cycle" "5")
57
56
  STRATEGIC_COOLDOWN_SECONDS=$(policy_get ".strategic.cooldown_seconds" "14400")
58
57
  STRATEGIC_STRATEGY_LINES=$(policy_get ".strategic.strategy_lines" "200")
@@ -118,6 +117,55 @@ strategic_load_title_cache() {
118
117
  ${closed_titles}"
119
118
  }
120
119
 
120
+ # ─── Outcome Tracking (Learning Loop) ────────────────────────────────────────
121
+ # Tracks which strategic issues shipped vs closed unshipped, so we learn from outcomes.
122
+ strategic_track_outcomes() {
123
+ local outcomes_file="$HOME/.shipwright/strategic/outcomes.jsonl"
124
+ mkdir -p "$HOME/.shipwright/strategic"
125
+
126
+ if [[ "${NO_GITHUB:-false}" == "true" ]]; then
127
+ return 0
128
+ fi
129
+
130
+ local strategic_issues
131
+ strategic_issues=$(gh issue list --label "strategic" --state all --json number,title,state,closedAt,labels --limit 50 2>/dev/null) || return 0
132
+
133
+ [[ -z "$strategic_issues" || "$strategic_issues" == "[]" ]] && return 0
134
+
135
+ touch "$outcomes_file" 2>/dev/null || true
136
+
137
+ while IFS= read -r issue; do
138
+ [[ -z "$issue" || "$issue" == "null" ]] && continue
139
+ local num title state
140
+ num=$(echo "$issue" | jq -r '.number')
141
+ title=$(echo "$issue" | jq -r '.title')
142
+ state=$(echo "$issue" | jq -r '.state')
143
+
144
+ # Check if already tracked
145
+ if grep -q "\"issue\":$num" "$outcomes_file" 2>/dev/null; then
146
+ continue
147
+ fi
148
+
149
+ # Determine outcome
150
+ local outcome="pending"
151
+ local success=false
152
+ if [[ "$state" == "CLOSED" ]]; then
153
+ local merged_prs
154
+ merged_prs=$(gh pr list --search "closes #$num" --state merged --json number --limit 1 2>/dev/null)
155
+ if [[ "$(echo "$merged_prs" | jq 'length' 2>/dev/null)" -gt 0 ]]; then
156
+ outcome="shipped"
157
+ success=true
158
+ else
159
+ outcome="closed_unshipped"
160
+ fi
161
+ fi
162
+
163
+ local title_escaped
164
+ title_escaped=$(echo "$title" | jq -R . 2>/dev/null || echo "null")
165
+ echo "{\"issue\":$num,\"title\":$title_escaped,\"outcome\":\"$outcome\",\"success\":$success,\"tracked_at\":\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\"}" >> "$outcomes_file"
166
+ done < <(echo "$strategic_issues" | jq -c '.[]' 2>/dev/null)
167
+ }
168
+
121
169
  # Check if a title has >threshold% overlap with any cached title.
122
170
  # Returns 0 (true) if a near-duplicate is found, 1 (false) otherwise.
123
171
  strategic_is_near_duplicate() {
@@ -340,6 +388,25 @@ strategic_build_prompt() {
340
388
  recent_closed="(GitHub access disabled)"
341
389
  fi
342
390
 
391
+ # Outcome history (shipped vs closed_unshipped) — learning loop
392
+ local outcomes_section="(No past outcomes yet — run a few cycles to build history)"
393
+ local outcomes_file="$HOME/.shipwright/strategic/outcomes.jsonl"
394
+ if [[ -f "$outcomes_file" ]]; then
395
+ local shipped failed
396
+ shipped=$(grep '"outcome":"shipped"' "$outcomes_file" 2>/dev/null | tail -10 | jq -r '.title' 2>/dev/null | sed 's/^/ - SUCCEEDED: /' || true)
397
+ failed=$(grep -E '"outcome":"closed_unshipped"' "$outcomes_file" 2>/dev/null | tail -10 | jq -r '.title' 2>/dev/null | sed 's/^/ - FAILED: /' || true)
398
+ if [[ -n "$shipped" || -n "$failed" ]]; then
399
+ outcomes_section="
400
+ Learn from these outcomes. Suggest more things like the successes, avoid patterns similar to failures.
401
+
402
+ SUCCEEDED (shipped with merged PR):
403
+ ${shipped:- (none yet)}
404
+
405
+ FAILED (closed without shipping):
406
+ ${failed:- (none yet)}"
407
+ fi
408
+ fi
409
+
343
410
  # Platform health (hygiene + platform-refactor scan) — for AGI-level self-improvement
344
411
  local platform_health_section="(No platform hygiene data — run \`shipwright hygiene platform-refactor\` or \`shipwright hygiene scan\` to generate .claude/platform-hygiene.json)"
345
412
  if [[ -f "${repo_dir}/.claude/platform-hygiene.json" ]]; then
@@ -381,6 +448,9 @@ ${recent_closed}
381
448
  ## Platform Health (refactor / hardcoded / AGI-level readiness)
382
449
  ${platform_health_section}
383
450
 
451
+ ## Past Strategic Suggestions and Outcomes (learn from these)
452
+ ${outcomes_section}
453
+
384
454
  ## Your Task
385
455
  Based on the strategy priorities and current data, recommend 1-3 concrete improvements to build next. Each should be a single, well-scoped task completable by one autonomous pipeline run.
386
456
 
@@ -395,6 +465,7 @@ ACCEPTANCE: <bullet list of acceptance criteria, one per line starting with "- "
395
465
  ---
396
466
 
397
467
  Rules:
468
+ - Learn from PAST STRATEGIC SUGGESTIONS: prefer patterns that succeeded, avoid patterns similar to failures
398
469
  - Do NOT duplicate any open issue OR any recently completed issue
399
470
  - Prioritize based on STRATEGY.md priorities (P0 > P1 > P2 > ...)
400
471
  - Focus on concrete, actionable improvements (not vague goals)
@@ -416,7 +487,7 @@ strategic_call_api() {
416
487
  return 1
417
488
  fi
418
489
 
419
- if ! command -v claude &>/dev/null; then
490
+ if ! command -v claude >/dev/null 2>&1; then
420
491
  error "Claude Code CLI not found — install with: npm install -g @anthropic-ai/claude-code"
421
492
  return 1
422
493
  fi
@@ -659,6 +730,10 @@ strategic_run() {
659
730
  return 1
660
731
  fi
661
732
 
733
+ # Track outcomes of past strategic issues (learning loop)
734
+ info "Tracking outcomes of past strategic issues..."
735
+ strategic_track_outcomes || true
736
+
662
737
  # Load existing issue titles for semantic dedup
663
738
  info "Loading issue title cache for dedup..."
664
739
  strategic_load_title_cache
@@ -757,6 +832,45 @@ strategic_status() {
757
832
  echo ""
758
833
  }
759
834
 
835
+ # ─── Outcomes Command ─────────────────────────────────────────────────────────
836
+ strategic_outcomes() {
837
+ local outcomes_file="$HOME/.shipwright/strategic/outcomes.jsonl"
838
+
839
+ echo -e "\n${PURPLE}${BOLD}━━━ Strategic Outcomes (Learning Loop) ━━━${RESET}\n"
840
+
841
+ if [[ ! -f "$outcomes_file" ]]; then
842
+ info "No outcomes tracked yet. Run \`shipwright strategic run\` to start the learning loop."
843
+ echo ""
844
+ return 0
845
+ fi
846
+
847
+ local shipped_count failed_count pending_count
848
+ shipped_count=$(grep -c '"outcome":"shipped"' "$outcomes_file" 2>/dev/null || echo "0")
849
+ failed_count=$(grep -cE '"outcome":"closed_unshipped"' "$outcomes_file" 2>/dev/null || echo "0")
850
+ pending_count=$(grep -c '"outcome":"pending"' "$outcomes_file" 2>/dev/null || echo "0")
851
+
852
+ echo -e " ${GREEN}Shipped:${RESET} $shipped_count (closed with merged PR)"
853
+ echo -e " ${RED}Closed unshipped:${RESET} $failed_count (closed without merge)"
854
+ echo -e " ${DIM}Pending:${RESET} $pending_count (still open)"
855
+ echo ""
856
+
857
+ echo -e " ${BOLD}Recent shipped:${RESET}"
858
+ grep '"outcome":"shipped"' "$outcomes_file" 2>/dev/null | tail -5 | while IFS= read -r line; do
859
+ local title
860
+ title=$(echo "$line" | jq -r '.title // "?"' 2>/dev/null)
861
+ echo -e " ${GREEN}✓${RESET} $title"
862
+ done
863
+ echo ""
864
+
865
+ echo -e " ${BOLD}Recent failed (closed without shipping):${RESET}"
866
+ grep -E '"outcome":"closed_unshipped"' "$outcomes_file" 2>/dev/null | tail -5 | while IFS= read -r line; do
867
+ local title
868
+ title=$(echo "$line" | jq -r '.title // "?"' 2>/dev/null)
869
+ echo -e " ${RED}✗${RESET} $title"
870
+ done
871
+ echo ""
872
+ }
873
+
760
874
  # ─── Help ─────────────────────────────────────────────────────────────────────
761
875
  strategic_show_help() {
762
876
  echo -e "${PURPLE}${BOLD}Shipwright Strategic Intelligence Agent${RESET} v${VERSION}\n"
@@ -765,7 +879,8 @@ strategic_show_help() {
765
879
  echo -e " sw-strategic.sh <command>\n"
766
880
  echo -e "${BOLD}Commands:${RESET}"
767
881
  echo -e " run [--force] Run a strategic analysis cycle (--force bypasses cooldown)"
768
- echo -e " status Show last run stats and cooldown"
882
+ echo -e " status Show last run stats and cooldown"
883
+ echo -e " outcomes Show outcome tracking (shipped vs failed suggestions)"
769
884
  echo -e " help Show this help\n"
770
885
  echo -e "${BOLD}Environment:${RESET}"
771
886
  echo -e " CLAUDE_CODE_OAUTH_TOKEN Required for Claude access"
@@ -806,6 +921,7 @@ if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then
806
921
  case "$cmd" in
807
922
  run) strategic_run "$@" ;;
808
923
  status) strategic_status ;;
924
+ outcomes) strategic_outcomes ;;
809
925
  help) strategic_show_help ;;
810
926
  *)
811
927
  error "Unknown command: $cmd"
@@ -5,7 +5,7 @@
5
5
  # ║ Streams tmux pane output in real-time to the dashboard or CLI. ║
6
6
  # ║ Captures output periodically, tags by agent/team, supports replay. ║
7
7
  # ╚═══════════════════════════════════════════════════════════════════════════╝
8
- VERSION="2.4.0"
8
+ VERSION="3.1.0"
9
9
  set -euo pipefail
10
10
  trap 'echo "ERROR: $BASH_SOURCE:$LINENO exited with status $?" >&2' ERR
11
11
 
@@ -35,16 +35,6 @@ if [[ "$(type -t emit_event 2>/dev/null)" != "function" ]]; then
35
35
  echo "${payload}}" >> "${HOME}/.shipwright/events.jsonl"
36
36
  }
37
37
  fi
38
- CYAN="${CYAN:-\033[38;2;0;212;255m}"
39
- PURPLE="${PURPLE:-\033[38;2;124;58;237m}"
40
- BLUE="${BLUE:-\033[38;2;0;102;255m}"
41
- GREEN="${GREEN:-\033[38;2;74;222;128m}"
42
- YELLOW="${YELLOW:-\033[38;2;250;204;21m}"
43
- RED="${RED:-\033[38;2;248;113;113m}"
44
- DIM="${DIM:-\033[2m}"
45
- BOLD="${BOLD:-\033[1m}"
46
- RESET="${RESET:-\033[0m}"
47
-
48
38
  # ─── Stream configuration ─────────────────────────────────────────────────────
49
39
  STREAM_CONFIG="${HOME}/.shipwright/stream-config.json"
50
40
  STREAM_DIR="${HOME}/.shipwright/streams"
@@ -6,7 +6,7 @@
6
6
  set -euo pipefail
7
7
  trap 'echo "ERROR: $BASH_SOURCE:$LINENO exited with status $?" >&2' ERR
8
8
 
9
- VERSION="2.4.0"
9
+ VERSION="3.1.0"
10
10
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
11
11
  REPO_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
12
12
 
@@ -33,16 +33,6 @@ if [[ "$(type -t emit_event 2>/dev/null)" != "function" ]]; then
33
33
  echo "${payload}}" >> "${HOME}/.shipwright/events.jsonl"
34
34
  }
35
35
  fi
36
- CYAN="${CYAN:-\033[38;2;0;212;255m}"
37
- PURPLE="${PURPLE:-\033[38;2;124;58;237m}"
38
- BLUE="${BLUE:-\033[38;2;0;102;255m}"
39
- GREEN="${GREEN:-\033[38;2;74;222;128m}"
40
- YELLOW="${YELLOW:-\033[38;2;250;204;21m}"
41
- RED="${RED:-\033[38;2;248;113;113m}"
42
- DIM="${DIM:-\033[2m}"
43
- BOLD="${BOLD:-\033[1m}"
44
- RESET="${RESET:-\033[0m}"
45
-
46
36
  # ─── Constants ──────────────────────────────────────────────────────────────
47
37
  SWARM_DIR="${HOME}/.shipwright/swarm"
48
38
  REGISTRY_FILE="${SWARM_DIR}/registry.json"
@@ -197,12 +187,12 @@ cmd_spawn() {
197
187
  mv "$tmp_file" "$REGISTRY_FILE" || { rm -f "$tmp_file"; error "Failed to update registry"; return 1; }
198
188
  record_metric "$agent_id" "spawn" "1" "$agent_type"
199
189
 
200
- # Create real tmux session for the agent (so scale/loop can send commands)
201
- if command -v tmux &>/dev/null; then
190
+ # Create real tmux session for the agent (run actual daemon)
191
+ if command -v tmux >/dev/null 2>&1; then
202
192
  local session_name="swarm-${agent_id}"
203
193
  if ! tmux has-session -t "$session_name" 2>/dev/null; then
204
194
  tmux new-session -d -s "$session_name" -c "$REPO_DIR" \
205
- "echo \"Agent $agent_id ready (type: $agent_type)\"; while true; do sleep 3600; done" 2>/dev/null && \
195
+ "SW_AGENT_ROLE=builder bash scripts/sw-daemon.sh start --role builder 2>&1 | tee /tmp/sw-swarm-${agent_id}.log" 2>/dev/null && \
206
196
  info "Tmux session created: $session_name" || warn "Tmux session creation failed (agent still in registry)"
207
197
  fi
208
198
  fi
@@ -250,7 +240,7 @@ cmd_retire() {
250
240
 
251
241
  # Kill real tmux session if present
252
242
  local session_name="swarm-${agent_id}"
253
- if command -v tmux &>/dev/null && tmux has-session -t "$session_name" 2>/dev/null; then
243
+ if command -v tmux >/dev/null 2>&1 && tmux has-session -t "$session_name" 2>/dev/null; then
254
244
  tmux kill-session -t "$session_name" 2>/dev/null && info "Tmux session killed: $session_name" || warn "Tmux kill failed for $session_name"
255
245
  fi
256
246
 
@@ -342,37 +332,87 @@ cmd_health() {
342
332
  fi
343
333
  }
344
334
 
335
+ # ─── Swarm spawn helper (for cmd_scale) ───────────────────────────────────
336
+ swarm_spawn_agent() {
337
+ local role="${1:-builder}"
338
+ local count="${2:-1}"
339
+ local i
340
+ for i in $(seq 1 "$count"); do
341
+ cmd_spawn "standard" 2>/dev/null || true
342
+ done
343
+ }
344
+
345
345
  # ─── Auto-scale logic ────────────────────────────────────────────────────
346
346
  cmd_scale() {
347
+ local target="${1:-auto}"
348
+
347
349
  ensure_dirs
348
350
  init_registry
349
351
  init_config
350
352
 
351
- local auto_scale_enabled
352
- auto_scale_enabled=$(jq -r '.auto_scaling_enabled' "$CONFIG_FILE")
353
+ if [[ "$target" == "auto" ]]; then
354
+ # Auto-scale based on queue depth
355
+ local queue_depth=0
356
+ local state_file="$HOME/.shipwright/daemon-state.json"
357
+ if [[ -f "$state_file" ]]; then
358
+ queue_depth=$(jq '.queued | length' "$state_file" 2>/dev/null || echo "0")
359
+ fi
353
360
 
354
- if [[ "$auto_scale_enabled" != "true" ]]; then
355
- warn "Auto-scaling is disabled"
356
- return 0
357
- fi
361
+ local current_agents
362
+ current_agents=$(tmux list-sessions -F '#{session_name}' 2>/dev/null | grep -cE '^shipwright-sw-agent|^swarm-' || echo "0")
358
363
 
359
- local min_agents max_agents target_util
360
- min_agents=$(jq -r '.min_agents // 1' "$CONFIG_FILE")
361
- max_agents=$(jq -r '.max_agents // 8' "$CONFIG_FILE")
362
- target_util=$(jq -r '.target_utilization // 0.75' "$CONFIG_FILE")
364
+ local target_agents=1
365
+ if [[ "$queue_depth" -gt 5 ]]; then
366
+ target_agents=3
367
+ elif [[ "$queue_depth" -gt 2 ]]; then
368
+ target_agents=2
369
+ fi
363
370
 
364
- local active_count
365
- active_count=$(jq -r '.active_count // 0' "$REGISTRY_FILE")
371
+ if [[ "$current_agents" -lt "$target_agents" ]]; then
372
+ local to_spawn=$((target_agents - current_agents))
373
+ echo "Queue depth: $queue_depth, scaling up by $to_spawn"
374
+ swarm_spawn_agent "builder" "$to_spawn"
375
+ elif [[ "$current_agents" -gt "$target_agents" && "$current_agents" -gt 1 ]]; then
376
+ local to_retire=$((current_agents - target_agents))
377
+ echo "Queue depth: $queue_depth, scaling down by $to_retire"
378
+ # Retire oldest agents from registry (tmux sessions are managed by scale down)
379
+ local registry_count
380
+ registry_count=$(jq -r '.agents | length' "$REGISTRY_FILE" 2>/dev/null || echo "0")
381
+ local retired=0
382
+ if [[ "$registry_count" -gt 0 ]] && [[ "$to_retire" -gt 0 ]]; then
383
+ local agent_id
384
+ agent_id=$(jq -r '.agents[0].id // empty' "$REGISTRY_FILE" 2>/dev/null)
385
+ if [[ -n "$agent_id" ]]; then
386
+ cmd_retire "$agent_id" 2>/dev/null && retired=1 || true
387
+ fi
388
+ fi
389
+ else
390
+ echo "Queue depth: $queue_depth, agents: $current_agents (optimal)"
391
+ fi
392
+ else
393
+ # Scale to specific count - show current state
394
+ local auto_scale_enabled
395
+ auto_scale_enabled=$(jq -r '.auto_scaling_enabled' "$CONFIG_FILE")
366
396
 
367
- # TODO: Implement queue depth and resource monitoring
368
- # For now, just show current state
369
- info "Auto-Scaling Analysis"
370
- echo ""
371
- echo -e " Current agents: ${CYAN}${active_count}/${max_agents}${RESET}"
372
- echo -e " Min agents: ${CYAN}${min_agents}${RESET}"
373
- echo -e " Target utilization: ${CYAN}${target_util}${RESET}"
374
- echo ""
375
- echo -e " ${DIM}Queue depth monitoring and scaling recommendations require active pipeline${RESET}"
397
+ if [[ "$auto_scale_enabled" != "true" ]]; then
398
+ warn "Auto-scaling is disabled"
399
+ fi
400
+
401
+ local min_agents max_agents target_util
402
+ min_agents=$(jq -r '.min_agents // 1' "$CONFIG_FILE")
403
+ max_agents=$(jq -r '.max_agents // 8' "$CONFIG_FILE")
404
+ target_util=$(jq -r '.target_utilization // 0.75' "$CONFIG_FILE")
405
+
406
+ local active_count
407
+ active_count=$(jq -r '.active_count // 0' "$REGISTRY_FILE")
408
+
409
+ info "Auto-Scaling Analysis"
410
+ echo ""
411
+ echo -e " Current agents: ${CYAN}${active_count}/${max_agents}${RESET}"
412
+ echo -e " Min agents: ${CYAN}${min_agents}${RESET}"
413
+ echo -e " Target utilization: ${CYAN}${target_util}${RESET}"
414
+ echo ""
415
+ fi
376
416
  }
377
417
 
378
418
  # ─── Performance leaderboard ──────────────────────────────────────────────
@@ -6,7 +6,7 @@
6
6
  set -euo pipefail
7
7
  trap 'echo "ERROR: $BASH_SOURCE:$LINENO exited with status $?" >&2' ERR
8
8
 
9
- VERSION="2.4.0"
9
+ VERSION="3.1.0"
10
10
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
11
11
  REPO_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
12
12
 
@@ -34,16 +34,6 @@ if [[ "$(type -t emit_event 2>/dev/null)" != "function" ]]; then
34
34
  echo "${payload}}" >> "${HOME}/.shipwright/events.jsonl"
35
35
  }
36
36
  fi
37
- CYAN="${CYAN:-\033[38;2;0;212;255m}"
38
- PURPLE="${PURPLE:-\033[38;2;124;58;237m}"
39
- BLUE="${BLUE:-\033[38;2;0;102;255m}"
40
- GREEN="${GREEN:-\033[38;2;74;222;128m}"
41
- YELLOW="${YELLOW:-\033[38;2;250;204;21m}"
42
- RED="${RED:-\033[38;2;248;113;113m}"
43
- DIM="${DIM:-\033[2m}"
44
- BOLD="${BOLD:-\033[1m}"
45
- RESET="${RESET:-\033[0m}"
46
-
47
37
  # ─── Structured Event Log ──────────────────────────────────────────────────
48
38
  EVENTS_FILE="${HOME}/.shipwright/events.jsonl"
49
39
 
@@ -182,7 +172,7 @@ cmd_delegate() {
182
172
  result: null
183
173
  }')
184
174
  tasks=$(echo "$tasks" | jq ". += [$task_json]")
185
- ((file_count++))
175
+ file_count=$((file_count + 1))
186
176
  done < <(echo "$changed_files")
187
177
 
188
178
  # Output delegation result
@@ -216,7 +206,7 @@ cmd_status() {
216
206
  for team_file in "$TEAM_STATE_DIR"/*.json; do
217
207
  [[ -f "$team_file" ]] || continue
218
208
  local ts
219
- ts=$(stat -f %Bm "$team_file" 2>/dev/null || stat -c %Y "$team_file" 2>/dev/null || echo 0)
209
+ ts=$(file_mtime "$team_file")
220
210
  local name
221
211
  name=$(basename "$team_file" .json)
222
212
  local status
@@ -255,7 +245,7 @@ cmd_status() {
255
245
  local spec_status
256
246
  spec_status=$(echo "$team_json" | jq -r ".specialist_status[$spec_idx] // \"pending\"" 2>/dev/null || echo "pending")
257
247
  printf " ${DIM}%-3d${RESET} %-20s %-15s\n" "$((spec_idx + 1))" "$spec" "$spec_status"
258
- ((spec_idx++))
248
+ spec_idx=$((spec_idx + 1))
259
249
  done < <(echo "$specs")
260
250
  echo ""
261
251
  }
@@ -292,11 +282,11 @@ cmd_vote() {
292
282
  local verdict
293
283
  verdict=$(echo "$team_json" | jq -r ".verdicts[\"$spec\"]? // \"neutral\"" 2>/dev/null || echo "neutral")
294
284
  case "$verdict" in
295
- approve) ((approve_count++)) ;;
296
- reject) ((reject_count++)) ;;
297
- *) ((neutral_count++)) ;;
285
+ approve) approve_count=$((approve_count + 1)) ;;
286
+ reject) reject_count=$((reject_count + 1)) ;;
287
+ *) neutral_count=$((neutral_count + 1)) ;;
298
288
  esac
299
- ((total++))
289
+ total=$((total + 1))
300
290
  done < <(echo "$specs")
301
291
 
302
292
  # Consensus: majority vote with leader tiebreak
@@ -370,9 +360,9 @@ cmd_aggregate() {
370
360
 
371
361
  if [[ -n "$result" ]]; then
372
362
  if echo "$result" | jq -e '.success' >/dev/null 2>&1; then
373
- ((success_count++))
363
+ success_count=$((success_count + 1))
374
364
  else
375
- ((failure_count++))
365
+ failure_count=$((failure_count + 1))
376
366
  fi
377
367
  results=$(echo "$results" | jq ". += [$result]")
378
368
  fi