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
package/scripts/sw-pm.sh CHANGED
@@ -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.3.1"
9
+ VERSION="3.0.0"
10
10
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
11
11
 
12
12
  # ─── Cross-platform compatibility ──────────────────────────────────────────
@@ -32,16 +32,6 @@ if [[ "$(type -t emit_event 2>/dev/null)" != "function" ]]; then
32
32
  echo "${payload}}" >> "${HOME}/.shipwright/events.jsonl"
33
33
  }
34
34
  fi
35
- CYAN="${CYAN:-\033[38;2;0;212;255m}"
36
- PURPLE="${PURPLE:-\033[38;2;124;58;237m}"
37
- BLUE="${BLUE:-\033[38;2;0;102;255m}"
38
- GREEN="${GREEN:-\033[38;2;74;222;128m}"
39
- YELLOW="${YELLOW:-\033[38;2;250;204;21m}"
40
- RED="${RED:-\033[38;2;248;113;113m}"
41
- DIM="${DIM:-\033[2m}"
42
- BOLD="${BOLD:-\033[1m}"
43
- RESET="${RESET:-\033[0m}"
44
-
45
35
  # ─── PM History Storage ──────────────────────────────────────────────────────
46
36
  PM_HISTORY="${HOME}/.shipwright/pm-history.json"
47
37
 
@@ -224,7 +214,7 @@ recommend_team() {
224
214
  if [[ -n "$issue_title" ]]; then
225
215
  local recruit_result
226
216
  recruit_result=$(bash "$SCRIPT_DIR/sw-recruit.sh" team --json "$issue_title" 2>/dev/null) || true
227
- if [[ -n "$recruit_result" ]] && echo "$recruit_result" | jq -e '.team' &>/dev/null 2>&1; then
217
+ if [[ -n "$recruit_result" ]] && echo "$recruit_result" | jq -e '.team' >/dev/null 2>&1; then
228
218
  local recruit_roles recruit_model recruit_agents recruit_cost
229
219
  recruit_roles=$(echo "$recruit_result" | jq -r '.team | join(",")')
230
220
  recruit_model=$(echo "$recruit_result" | jq -r '.model // "sonnet"')
@@ -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.3.1"
9
+ VERSION="3.0.0"
10
10
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
11
11
  REPO_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
12
12
 
@@ -26,24 +26,6 @@ if [[ "$(type -t now_iso 2>/dev/null)" != "function" ]]; then
26
26
  now_iso() { date -u +"%Y-%m-%dT%H:%M:%SZ"; }
27
27
  now_epoch() { date +%s; }
28
28
  fi
29
- if [[ "$(type -t emit_event 2>/dev/null)" != "function" ]]; then
30
- emit_event() {
31
- local event_type="$1"; shift; mkdir -p "${HOME}/.shipwright"
32
- local payload="{\"ts\":\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\",\"type\":\"$event_type\""
33
- while [[ $# -gt 0 ]]; do local key="${1%%=*}" val="${1#*=}"; payload="${payload},\"${key}\":\"${val}\""; shift; done
34
- echo "${payload}}" >> "${HOME}/.shipwright/events.jsonl"
35
- }
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
29
  # ─── Configuration Helpers ──────────────────────────────────────────────────
48
30
 
49
31
  get_pr_config() {
@@ -56,13 +38,38 @@ get_pr_config() {
56
38
 
57
39
  get_pr_info() {
58
40
  local pr_number="$1"
59
- gh pr view "$pr_number" --json number,title,body,state,headRefName,baseRefName,statusCheckRollup,reviews,commits,createdAt,updatedAt 2>/dev/null || return 1
41
+ gh pr view "$pr_number" --json number,title,body,state,headRefName,baseRefName,statusCheckRollup,reviews,commits,createdAt,updatedAt,headRefOid 2>/dev/null || return 1
42
+ }
43
+
44
+ get_pr_head_sha() {
45
+ local pr_number="$1"
46
+ gh pr view "$pr_number" --json headRefOid --jq '.headRefOid' 2>/dev/null || return 1
60
47
  }
61
48
 
62
49
  get_pr_checks_status() {
63
50
  local pr_number="$1"
64
51
  # Returns: success, failure, pending, or unknown
65
- gh pr checks "$pr_number" 2>/dev/null | jq -r '.[] | select(.status == "completed") | .conclusion' | sort | uniq -c | sort -rn | head -1 || echo "unknown"
52
+ # gh pr checks requires --json flag to produce JSON output
53
+ local checks_json
54
+ checks_json=$(gh pr checks "$pr_number" --json name,state,conclusion 2>/dev/null || echo "[]")
55
+
56
+ # Handle empty or non-JSON response
57
+ if [[ -z "$checks_json" ]] || ! echo "$checks_json" | jq empty 2>/dev/null; then
58
+ echo "unknown"
59
+ return
60
+ fi
61
+
62
+ local total failed pending
63
+ total=$(echo "$checks_json" | jq 'length' 2>/dev/null || echo "0")
64
+ [[ "$total" -eq 0 ]] && { echo "unknown"; return; }
65
+
66
+ failed=$(echo "$checks_json" | jq '[.[] | select(.conclusion == "FAILURE" or .conclusion == "failure")] | length' 2>/dev/null || echo "0")
67
+ [[ "$failed" -gt 0 ]] && { echo "failure"; return; }
68
+
69
+ pending=$(echo "$checks_json" | jq '[.[] | select(.state == "PENDING" or .state == "QUEUED" or .state == "IN_PROGRESS")] | length' 2>/dev/null || echo "0")
70
+ [[ "$pending" -gt 0 ]] && { echo "pending"; return; }
71
+
72
+ echo "success"
66
73
  }
67
74
 
68
75
  has_merge_conflicts() {
@@ -95,6 +102,144 @@ get_pr_originating_issue() {
95
102
  echo "$body" | grep -oiE '(closes|fixes|resolves) #[0-9]+' | grep -oE '[0-9]+' | head -1
96
103
  }
97
104
 
105
+ # ─── Current-Head SHA Discipline ─────────────────────────────────────────────
106
+ # All check results and review approvals MUST correspond to the current PR head
107
+ # SHA. Stale evidence from older commits is never trusted. This is the single
108
+ # most important safety invariant in the Code Factory pattern.
109
+
110
+ validate_checks_for_head_sha() {
111
+ local pr_number="$1"
112
+ local head_sha="$2"
113
+
114
+ if [[ -z "$head_sha" ]]; then
115
+ error "No head SHA provided — cannot validate check freshness"
116
+ return 1
117
+ fi
118
+
119
+ local short_sha="${head_sha:0:7}"
120
+
121
+ # Get check runs for the current head SHA
122
+ local owner_repo
123
+ owner_repo=$(gh repo view --json nameWithOwner --jq '.nameWithOwner' 2>/dev/null || echo "")
124
+ if [[ -z "$owner_repo" ]]; then
125
+ warn "Could not detect repo — skipping SHA discipline check"
126
+ return 0
127
+ fi
128
+
129
+ local check_runs
130
+ check_runs=$(gh api "repos/${owner_repo}/commits/${head_sha}/check-runs" --jq '.check_runs' 2>/dev/null || echo "[]")
131
+
132
+ local total_checks
133
+ total_checks=$(echo "$check_runs" | jq 'length' 2>/dev/null || echo "0")
134
+
135
+ if [[ "$total_checks" -eq 0 ]]; then
136
+ warn "No check runs found for head SHA ${short_sha}"
137
+ return 0
138
+ fi
139
+
140
+ local failed_checks
141
+ failed_checks=$(echo "$check_runs" | jq '[.[] | select(.conclusion == "failure" or .conclusion == "cancelled")] | length' 2>/dev/null || echo "0")
142
+
143
+ local pending_checks
144
+ pending_checks=$(echo "$check_runs" | jq '[.[] | select(.status != "completed")] | length' 2>/dev/null || echo "0")
145
+
146
+ if [[ "$failed_checks" -gt 0 ]]; then
147
+ error "PR #${pr_number} has ${failed_checks} failed check(s) on current head ${short_sha}"
148
+ return 1
149
+ fi
150
+
151
+ if [[ "$pending_checks" -gt 0 ]]; then
152
+ warn "PR #${pr_number} has ${pending_checks} pending check(s) on head ${short_sha}"
153
+ return 1
154
+ fi
155
+
156
+ info "All ${total_checks} checks passed for current head SHA ${short_sha}"
157
+ return 0
158
+ }
159
+
160
+ validate_reviews_for_head_sha() {
161
+ local pr_number="$1"
162
+ local head_sha="$2"
163
+
164
+ if [[ -z "$head_sha" ]]; then
165
+ return 0
166
+ fi
167
+
168
+ local short_sha="${head_sha:0:7}"
169
+
170
+ # Get reviews and check they're not stale (submitted before the latest push)
171
+ local reviews_json
172
+ reviews_json=$(gh pr view "$pr_number" --json reviews --jq '.reviews' 2>/dev/null || echo "[]")
173
+
174
+ local latest_commit_date
175
+ latest_commit_date=$(gh pr view "$pr_number" --json commits --jq '.commits[-1].committedDate' 2>/dev/null || echo "")
176
+
177
+ if [[ -z "$latest_commit_date" ]]; then
178
+ return 0
179
+ fi
180
+
181
+ # Check if any approvals are stale (submitted before last commit)
182
+ local stale_approvals
183
+ stale_approvals=$(echo "$reviews_json" | jq --arg cutoff "$latest_commit_date" \
184
+ '[.[] | select(.state == "APPROVED" and .submittedAt < $cutoff)] | length' 2>/dev/null || echo "0")
185
+
186
+ if [[ "$stale_approvals" -gt 0 ]]; then
187
+ warn "PR #${pr_number} has ${stale_approvals} stale approval(s) from before head ${short_sha} — reviews should be refreshed"
188
+ fi
189
+
190
+ return 0
191
+ }
192
+
193
+ compute_risk_tier_for_pr() {
194
+ local pr_number="$1"
195
+ local policy_file="${REPO_DIR}/config/policy.json"
196
+
197
+ if [[ ! -f "$policy_file" ]]; then
198
+ echo "medium"
199
+ return
200
+ fi
201
+
202
+ local changed_files
203
+ changed_files=$(gh pr diff "$pr_number" --name-only 2>/dev/null || echo "")
204
+
205
+ if [[ -z "$changed_files" ]]; then
206
+ echo "low"
207
+ return
208
+ fi
209
+
210
+ local tier="low"
211
+
212
+ check_tier_match() {
213
+ local check_tier="$1"
214
+ local patterns
215
+ patterns=$(jq -r ".riskTierRules.${check_tier}[]? // empty" "$policy_file" 2>/dev/null)
216
+ [[ -z "$patterns" ]] && return 1
217
+
218
+ while IFS= read -r pattern; do
219
+ [[ -z "$pattern" ]] && continue
220
+ local regex
221
+ regex=$(echo "$pattern" | sed 's/\./\\./g; s/\*\*/DOUBLESTAR/g; s/\*/[^\/]*/g; s/DOUBLESTAR/.*/g')
222
+ while IFS= read -r file; do
223
+ [[ -z "$file" ]] && continue
224
+ if echo "$file" | grep -qE "^${regex}$"; then
225
+ return 0
226
+ fi
227
+ done <<< "$changed_files"
228
+ done <<< "$patterns"
229
+ return 1
230
+ }
231
+
232
+ if check_tier_match "critical"; then
233
+ tier="critical"
234
+ elif check_tier_match "high"; then
235
+ tier="high"
236
+ elif check_tier_match "medium"; then
237
+ tier="medium"
238
+ fi
239
+
240
+ echo "$tier"
241
+ }
242
+
98
243
  # ─── Review Pass ────────────────────────────────────────────────────────────
99
244
 
100
245
  pr_review() {
@@ -144,25 +289,25 @@ pr_review() {
144
289
  if echo "$diff_output" | grep -qE '(HACK|TODO|FIXME|XXX|BROKEN|DEBUG)'; then
145
290
  warnings="${warnings}
146
291
  - Found HACK/TODO/FIXME markers in code"
147
- ((issues_found++))
292
+ issues_found=$((issues_found + 1))
148
293
  fi
149
294
 
150
295
  if echo "$diff_output" | grep -qE 'console\.(log|warn|error)\('; then
151
296
  warnings="${warnings}
152
297
  - Found console.log statements (should use proper logging)"
153
- ((issues_found++))
298
+ issues_found=$((issues_found + 1))
154
299
  fi
155
300
 
156
301
  if [[ $line_additions -gt 500 ]]; then
157
302
  warnings="${warnings}
158
303
  - Large addition (${line_additions} lines) — consider splitting into smaller PRs"
159
- ((issues_found++))
304
+ issues_found=$((issues_found + 1))
160
305
  fi
161
306
 
162
307
  if [[ $file_count -gt 20 ]]; then
163
308
  warnings="${warnings}
164
309
  - Many files changed (${file_count}) — consider splitting"
165
- ((issues_found++))
310
+ issues_found=$((issues_found + 1))
166
311
  fi
167
312
 
168
313
  # Post review comment to PR
@@ -220,6 +365,35 @@ pr_merge() {
220
365
  return 1
221
366
  fi
222
367
 
368
+ # ── Current-head SHA discipline ──────────────────────────────────────────
369
+ # All evidence (checks, reviews) must be validated against the current head.
370
+ # Never merge on stale evidence from an older commit.
371
+ local head_sha
372
+ head_sha=$(echo "$pr_info" | jq -r '.headRefOid // empty' 2>/dev/null)
373
+ if [[ -z "$head_sha" ]]; then
374
+ head_sha=$(get_pr_head_sha "$pr_number")
375
+ fi
376
+
377
+ if [[ -n "$head_sha" ]]; then
378
+ local short_sha="${head_sha:0:7}"
379
+ info "Validating evidence for current head SHA: ${short_sha}"
380
+
381
+ if ! validate_checks_for_head_sha "$pr_number" "$head_sha"; then
382
+ error "PR #${pr_number} blocked — checks not passing for current head ${short_sha}"
383
+ emit_event "pr.merge_failed" "pr=${pr_number}" "reason=stale_checks" "head_sha=${short_sha}"
384
+ return 1
385
+ fi
386
+
387
+ validate_reviews_for_head_sha "$pr_number" "$head_sha"
388
+ else
389
+ warn "Could not determine head SHA — falling back to legacy check"
390
+ fi
391
+
392
+ # ── Risk tier enforcement ────────────────────────────────────────────────
393
+ local risk_tier
394
+ risk_tier=$(compute_risk_tier_for_pr "$pr_number")
395
+ info "Risk tier: ${risk_tier}"
396
+
223
397
  # Check for merge conflicts
224
398
  if has_merge_conflicts "$pr_number"; then
225
399
  error "PR #${pr_number} has merge conflicts — manual intervention required"
@@ -227,7 +401,7 @@ pr_merge() {
227
401
  return 1
228
402
  fi
229
403
 
230
- # Check CI status
404
+ # Check CI status (legacy check, supplementary to SHA-based validation)
231
405
  local status_check_rollup
232
406
  status_check_rollup=$(echo "$pr_info" | jq -r '.statusCheckRollup[].state' 2>/dev/null | sort | uniq)
233
407
  if [[ -z "$status_check_rollup" ]] || echo "$status_check_rollup" | grep -qi "failure\|error"; then
@@ -246,10 +420,10 @@ pr_merge() {
246
420
  fi
247
421
 
248
422
  # Perform squash merge and delete branch
249
- info "Merging PR #${pr_number} with squash..."
423
+ info "Merging PR #${pr_number} with squash (tier: ${risk_tier}, head: ${head_sha:0:7})..."
250
424
  if gh pr merge "$pr_number" --squash --delete-branch 2>/dev/null; then
251
425
  success "PR #${pr_number} merged and branch deleted"
252
- emit_event "pr.merged" "pr=${pr_number}"
426
+ emit_event "pr.merged" "pr=${pr_number}" "risk_tier=${risk_tier}" "head_sha=${head_sha:0:7}"
253
427
 
254
428
  # Post feedback to originating issue
255
429
  local issue_number
@@ -315,7 +489,7 @@ ${DIM}— Shipwright auto-lifecycle manager${RESET}"
315
489
  gh pr comment "$pr_number" --body "$close_comment" 2>/dev/null || true
316
490
  gh pr close "$pr_number" 2>/dev/null && {
317
491
  success "Closed PR #${pr_number}"
318
- ((closed_count++))
492
+ closed_count=$((closed_count + 1))
319
493
  emit_event "pr.closed_stale" "pr=${pr_number}" "age_days=${age_days}"
320
494
  }
321
495
  fi
@@ -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.3.1"
9
+ VERSION="3.0.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
 
@@ -169,7 +159,7 @@ _predictive_record_anomaly() {
169
159
  '{ts: $ts, ts_epoch: $epoch, stage: $stage, metric: $metric, severity: $severity, value: $value, baseline: $baseline, confirmed: null}')
170
160
  echo "$record" >> "$tracking_file"
171
161
  # Rotate anomaly tracking to prevent unbounded growth
172
- type rotate_jsonl &>/dev/null 2>&1 && rotate_jsonl "$tracking_file" 5000
162
+ type rotate_jsonl >/dev/null 2>&1 && rotate_jsonl "$tracking_file" 5000
173
163
  }
174
164
 
175
165
  # predictive_confirm_anomaly <stage> <metric_name> <was_real_failure>
@@ -308,7 +298,7 @@ _predictive_github_risk_factors() {
308
298
  local issue_json="$1"
309
299
  local risk_factors='{"security_risk": 0, "churn_risk": 0, "contributor_risk": 0, "recurrence_risk": 0}'
310
300
 
311
- type _gh_detect_repo &>/dev/null 2>&1 || { echo "$risk_factors"; return 0; }
301
+ type _gh_detect_repo >/dev/null 2>&1 || { echo "$risk_factors"; return 0; }
312
302
  _gh_detect_repo 2>/dev/null || { echo "$risk_factors"; return 0; }
313
303
 
314
304
  local owner="${GH_OWNER:-}" repo="${GH_REPO:-}"
@@ -316,7 +306,7 @@ _predictive_github_risk_factors() {
316
306
 
317
307
  # Security risk: active alerts
318
308
  local sec_risk=0
319
- if type gh_security_alerts &>/dev/null 2>&1; then
309
+ if type gh_security_alerts >/dev/null 2>&1; then
320
310
  local alert_count
321
311
  alert_count=$(gh_security_alerts "$owner" "$repo" 2>/dev/null | jq 'length' 2>/dev/null || echo "0")
322
312
  if [[ "${alert_count:-0}" -gt 10 ]]; then
@@ -330,7 +320,7 @@ _predictive_github_risk_factors() {
330
320
 
331
321
  # Recurrence risk: similar past issues
332
322
  local rec_risk=0
333
- if type gh_similar_issues &>/dev/null 2>&1; then
323
+ if type gh_similar_issues >/dev/null 2>&1; then
334
324
  local title
335
325
  title=$(echo "$issue_json" | jq -r '.title // ""' 2>/dev/null | head -c 100)
336
326
  if [[ -n "$title" ]]; then
@@ -346,7 +336,7 @@ _predictive_github_risk_factors() {
346
336
 
347
337
  # Contributor risk: low contributor count = bus factor risk
348
338
  local cont_risk=0
349
- if type gh_contributors &>/dev/null 2>&1; then
339
+ if type gh_contributors >/dev/null 2>&1; then
350
340
  local contributor_count
351
341
  contributor_count=$(gh_contributors "$owner" "$repo" 2>/dev/null | jq 'length' 2>/dev/null || echo "0")
352
342
  if [[ "${contributor_count:-0}" -lt 2 ]]; then
@@ -368,7 +358,7 @@ predict_pipeline_risk() {
368
358
  local issue_json="${1:-"{}"}"
369
359
  local repo_context="${2:-}"
370
360
 
371
- if [[ "$INTELLIGENCE_AVAILABLE" == "true" ]] && command -v _intelligence_call_claude &>/dev/null; then
361
+ if [[ "$INTELLIGENCE_AVAILABLE" == "true" ]] && command -v _intelligence_call_claude >/dev/null 2>&1; then
372
362
  local prompt
373
363
  prompt="Analyze this issue for pipeline risk. Return ONLY valid JSON.
374
364
 
@@ -381,7 +371,7 @@ Return JSON format:
381
371
  local result
382
372
  result=$(_intelligence_call_claude "$prompt" 2>/dev/null || echo "")
383
373
 
384
- if [[ -n "$result" ]] && echo "$result" | jq -e '.overall_risk' &>/dev/null; then
374
+ if [[ -n "$result" ]] && echo "$result" | jq -e '.overall_risk' >/dev/null 2>&1; then
385
375
  # Validate range
386
376
  local risk
387
377
  risk=$(echo "$result" | jq '.overall_risk')
@@ -503,7 +493,7 @@ $(head -100 "$file_path" 2>/dev/null || true)
503
493
  return 0
504
494
  fi
505
495
 
506
- if [[ "$INTELLIGENCE_AVAILABLE" != "true" ]] || ! command -v _intelligence_call_claude &>/dev/null; then
496
+ if [[ "$INTELLIGENCE_AVAILABLE" != "true" ]] || ! command -v _intelligence_call_claude >/dev/null 2>&1; then
507
497
  echo '[]'
508
498
  return 0
509
499
  fi
@@ -524,7 +514,7 @@ Only return findings with severity 'high' or 'critical'. Return [] if nothing si
524
514
  local result
525
515
  result=$(_intelligence_call_claude "$prompt" 2>/dev/null || echo "")
526
516
 
527
- if [[ -n "$result" ]] && echo "$result" | jq -e 'type == "array"' &>/dev/null; then
517
+ if [[ -n "$result" ]] && echo "$result" | jq -e 'type == "array"' >/dev/null 2>&1; then
528
518
  # Filter to only high/critical findings
529
519
  local filtered
530
520
  filtered=$(echo "$result" | jq '[.[] | select(.severity == "high" or .severity == "critical")]')
@@ -588,9 +578,13 @@ predict_detect_anomaly() {
588
578
  return 0
589
579
  fi
590
580
 
591
- # Get per-metric thresholds (adaptive or default)
581
+ # Get per-metric thresholds (adaptive from intelligence/DB, or file-based, or default)
592
582
  local metric_critical_mult metric_warning_mult
593
- metric_critical_mult=$(_predictive_get_anomaly_threshold "$metric_name")
583
+ if [[ "$(type -t get_anomaly_threshold 2>/dev/null)" == "function" ]]; then
584
+ metric_critical_mult=$(get_anomaly_threshold)
585
+ else
586
+ metric_critical_mult=$(_predictive_get_anomaly_threshold "$metric_name")
587
+ fi
594
588
  metric_warning_mult=$(_predictive_get_warning_multiplier "$metric_name")
595
589
 
596
590
  # Calculate thresholds using awk for floating-point
@@ -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.3.1"
9
+ VERSION="3.0.0"
10
10
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
11
11
 
12
12
  # ─── Handle subcommands ───────────────────────────────────────────────────────
@@ -38,16 +38,6 @@ if [[ "$(type -t emit_event 2>/dev/null)" != "function" ]]; then
38
38
  echo "${payload}}" >> "${HOME}/.shipwright/events.jsonl"
39
39
  }
40
40
  fi
41
- CYAN="${CYAN:-\033[38;2;0;212;255m}"
42
- PURPLE="${PURPLE:-\033[38;2;124;58;237m}"
43
- BLUE="${BLUE:-\033[38;2;0;102;255m}"
44
- GREEN="${GREEN:-\033[38;2;74;222;128m}"
45
- YELLOW="${YELLOW:-\033[38;2;250;204;21m}"
46
- RED="${RED:-\033[38;2;248;113;113m}"
47
- DIM="${DIM:-\033[2m}"
48
- BOLD="${BOLD:-\033[1m}"
49
- RESET="${RESET:-\033[0m}"
50
-
51
41
  # ─── Defaults ───────────────────────────────────────────────────────────────
52
42
  FORCE=false
53
43
  CHECK_ONLY=false
@@ -153,7 +143,7 @@ done
153
143
  # ─── prep_init ──────────────────────────────────────────────────────────────
154
144
 
155
145
  prep_init() {
156
- if ! git rev-parse --is-inside-work-tree &>/dev/null; then
146
+ if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
157
147
  error "Not inside a git repository"
158
148
  exit 1
159
149
  fi
@@ -591,7 +581,7 @@ prep_extract_patterns() {
591
581
  # ─── Intelligence Check ──────────────────────────────────────────────────
592
582
 
593
583
  intelligence_available() {
594
- command -v claude &>/dev/null || return 1
584
+ command -v claude >/dev/null 2>&1 || return 1
595
585
  # Honor --with-claude flag
596
586
  $WITH_CLAUDE && return 0
597
587
  # Check daemon config for intelligence.enabled
@@ -1407,7 +1397,7 @@ prep_generate_manifest() {
1407
1397
  HEREDOC
1408
1398
 
1409
1399
  # Validate JSON
1410
- if command -v jq &>/dev/null; then
1400
+ if command -v jq >/dev/null 2>&1; then
1411
1401
  if ! jq empty "$filepath" 2>/dev/null; then
1412
1402
  warn "prep-manifest.json may have invalid JSON — check manually"
1413
1403
  fi
@@ -1422,7 +1412,7 @@ HEREDOC
1422
1412
  prep_with_claude() {
1423
1413
  if ! $WITH_CLAUDE; then return; fi
1424
1414
 
1425
- if ! command -v claude &>/dev/null; then
1415
+ if ! command -v claude >/dev/null 2>&1; then
1426
1416
  warn "claude CLI not found — skipping deep analysis"
1427
1417
  return
1428
1418
  fi
@@ -1471,7 +1461,7 @@ prep_validate() {
1471
1461
  local issues=0
1472
1462
 
1473
1463
  # Check JSON files
1474
- if command -v jq &>/dev/null; then
1464
+ if command -v jq >/dev/null 2>&1; then
1475
1465
  for f in "$PROJECT_ROOT/.claude/settings.json" "$PROJECT_ROOT/.claude/prep-manifest.json"; do
1476
1466
  if [[ -f "$f" ]] && ! jq empty "$f" 2>/dev/null; then
1477
1467
  warn "Invalid JSON: ${f##"$PROJECT_ROOT"/}"
package/scripts/sw-ps.sh CHANGED
@@ -5,7 +5,7 @@
5
5
  # ║ Displays a table of agents running in claude-* tmux windows with ║
6
6
  # ║ PID, status, idle time, and pane references. ║
7
7
  # ╚═══════════════════════════════════════════════════════════════════════════╝
8
- VERSION="2.3.1"
8
+ VERSION="3.0.0"
9
9
  set -euo pipefail
10
10
  trap 'echo "ERROR: $BASH_SOURCE:$LINENO exited with status $?" >&2' ERR
11
11
 
@@ -31,16 +31,6 @@ if [[ "$(type -t emit_event 2>/dev/null)" != "function" ]]; then
31
31
  echo "${payload}}" >> "${HOME}/.shipwright/events.jsonl"
32
32
  }
33
33
  fi
34
- CYAN="${CYAN:-\033[38;2;0;212;255m}"
35
- PURPLE="${PURPLE:-\033[38;2;124;58;237m}"
36
- BLUE="${BLUE:-\033[38;2;0;102;255m}"
37
- GREEN="${GREEN:-\033[38;2;74;222;128m}"
38
- YELLOW="${YELLOW:-\033[38;2;250;204;21m}"
39
- RED="${RED:-\033[38;2;248;113;113m}"
40
- DIM="${DIM:-\033[2m}"
41
- BOLD="${BOLD:-\033[1m}"
42
- RESET="${RESET:-\033[0m}"
43
-
44
34
  # ─── Format idle time ───────────────────────────────────────────────────────
45
35
  format_idle() {
46
36
  local seconds="$1"
@@ -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.3.1"
9
+ VERSION="3.0.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
  # ─── Paths ──────────────────────────────────────────────────────────────────
48
38
  PUB_DASH_DIR="${HOME}/.shipwright/public-dashboard"
49
39
  SHARE_LINKS_FILE="${PUB_DASH_DIR}/share-links.json"
@@ -133,7 +123,7 @@ gather_pipeline_state() {
133
123
  # ─── Generate Token ─────────────────────────────────────────────────────────
134
124
  generate_token() {
135
125
  # Create a read-only token (32 hex chars)
136
- if command -v openssl &>/dev/null; then
126
+ if command -v openssl >/dev/null 2>&1; then
137
127
  openssl rand -hex 16
138
128
  else
139
129
  # Fallback to simple pseudo-random