shipwright-cli 2.3.1 → 2.4.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 (102) hide show
  1. package/README.md +82 -20
  2. package/config/policy.json +160 -2
  3. package/config/policy.schema.json +162 -1
  4. package/package.json +14 -2
  5. package/scripts/sw +1 -1
  6. package/scripts/sw-activity.sh +1 -1
  7. package/scripts/sw-adaptive.sh +1 -1
  8. package/scripts/sw-adversarial.sh +1 -1
  9. package/scripts/sw-architecture-enforcer.sh +1 -1
  10. package/scripts/sw-auth.sh +1 -1
  11. package/scripts/sw-autonomous.sh +1 -1
  12. package/scripts/sw-changelog.sh +1 -1
  13. package/scripts/sw-checkpoint.sh +1 -1
  14. package/scripts/sw-ci.sh +1 -1
  15. package/scripts/sw-cleanup.sh +1 -1
  16. package/scripts/sw-code-review.sh +1 -1
  17. package/scripts/sw-connect.sh +1 -1
  18. package/scripts/sw-context.sh +1 -1
  19. package/scripts/sw-cost.sh +1 -1
  20. package/scripts/sw-daemon.sh +1 -1
  21. package/scripts/sw-dashboard.sh +1 -1
  22. package/scripts/sw-db.sh +1 -1
  23. package/scripts/sw-decompose.sh +1 -1
  24. package/scripts/sw-deps.sh +1 -1
  25. package/scripts/sw-developer-simulation.sh +1 -1
  26. package/scripts/sw-discovery.sh +1 -1
  27. package/scripts/sw-doc-fleet.sh +1 -1
  28. package/scripts/sw-docs-agent.sh +1 -1
  29. package/scripts/sw-docs.sh +1 -1
  30. package/scripts/sw-doctor.sh +1 -1
  31. package/scripts/sw-dora.sh +1 -1
  32. package/scripts/sw-durable.sh +1 -1
  33. package/scripts/sw-e2e-orchestrator.sh +1 -1
  34. package/scripts/sw-eventbus.sh +1 -1
  35. package/scripts/sw-evidence.sh +664 -0
  36. package/scripts/sw-feedback.sh +1 -1
  37. package/scripts/sw-fix.sh +1 -1
  38. package/scripts/sw-fleet-discover.sh +1 -1
  39. package/scripts/sw-fleet-viz.sh +1 -1
  40. package/scripts/sw-fleet.sh +1 -1
  41. package/scripts/sw-github-app.sh +1 -1
  42. package/scripts/sw-github-checks.sh +1 -1
  43. package/scripts/sw-github-deploy.sh +1 -1
  44. package/scripts/sw-github-graphql.sh +1 -1
  45. package/scripts/sw-guild.sh +1 -1
  46. package/scripts/sw-heartbeat.sh +1 -1
  47. package/scripts/sw-hygiene.sh +1 -1
  48. package/scripts/sw-incident.sh +244 -1
  49. package/scripts/sw-init.sh +1 -1
  50. package/scripts/sw-instrument.sh +1 -1
  51. package/scripts/sw-intelligence.sh +1 -1
  52. package/scripts/sw-jira.sh +1 -1
  53. package/scripts/sw-launchd.sh +1 -1
  54. package/scripts/sw-linear.sh +1 -1
  55. package/scripts/sw-logs.sh +1 -1
  56. package/scripts/sw-loop.sh +1 -1
  57. package/scripts/sw-memory.sh +1 -1
  58. package/scripts/sw-mission-control.sh +1 -1
  59. package/scripts/sw-model-router.sh +1 -1
  60. package/scripts/sw-otel.sh +1 -1
  61. package/scripts/sw-oversight.sh +1 -1
  62. package/scripts/sw-pipeline-composer.sh +1 -1
  63. package/scripts/sw-pipeline-vitals.sh +1 -1
  64. package/scripts/sw-pipeline.sh +1 -1
  65. package/scripts/sw-pm.sh +1 -1
  66. package/scripts/sw-pr-lifecycle.sh +177 -5
  67. package/scripts/sw-predictive.sh +1 -1
  68. package/scripts/sw-prep.sh +1 -1
  69. package/scripts/sw-ps.sh +1 -1
  70. package/scripts/sw-public-dashboard.sh +1 -1
  71. package/scripts/sw-quality.sh +1 -1
  72. package/scripts/sw-reaper.sh +1 -1
  73. package/scripts/sw-regression.sh +1 -1
  74. package/scripts/sw-release-manager.sh +1 -1
  75. package/scripts/sw-release.sh +1 -1
  76. package/scripts/sw-remote.sh +1 -1
  77. package/scripts/sw-replay.sh +1 -1
  78. package/scripts/sw-retro.sh +1 -1
  79. package/scripts/sw-review-rerun.sh +220 -0
  80. package/scripts/sw-scale.sh +1 -1
  81. package/scripts/sw-security-audit.sh +1 -1
  82. package/scripts/sw-self-optimize.sh +1 -1
  83. package/scripts/sw-session.sh +1 -1
  84. package/scripts/sw-setup.sh +1 -1
  85. package/scripts/sw-standup.sh +1 -1
  86. package/scripts/sw-status.sh +1 -1
  87. package/scripts/sw-strategic.sh +1 -1
  88. package/scripts/sw-stream.sh +1 -1
  89. package/scripts/sw-swarm.sh +1 -1
  90. package/scripts/sw-team-stages.sh +1 -1
  91. package/scripts/sw-templates.sh +1 -1
  92. package/scripts/sw-testgen.sh +1 -1
  93. package/scripts/sw-tmux-pipeline.sh +1 -1
  94. package/scripts/sw-tmux.sh +1 -1
  95. package/scripts/sw-trace.sh +1 -1
  96. package/scripts/sw-tracker.sh +1 -1
  97. package/scripts/sw-triage.sh +1 -1
  98. package/scripts/sw-upgrade.sh +1 -1
  99. package/scripts/sw-ux.sh +1 -1
  100. package/scripts/sw-webhook.sh +1 -1
  101. package/scripts/sw-widgets.sh +1 -1
  102. package/scripts/sw-worktree.sh +1 -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="2.4.0"
10
10
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
11
11
  REPO_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
12
12
 
@@ -56,7 +56,12 @@ get_pr_config() {
56
56
 
57
57
  get_pr_info() {
58
58
  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
59
+ gh pr view "$pr_number" --json number,title,body,state,headRefName,baseRefName,statusCheckRollup,reviews,commits,createdAt,updatedAt,headRefOid 2>/dev/null || return 1
60
+ }
61
+
62
+ get_pr_head_sha() {
63
+ local pr_number="$1"
64
+ gh pr view "$pr_number" --json headRefOid --jq '.headRefOid' 2>/dev/null || return 1
60
65
  }
61
66
 
62
67
  get_pr_checks_status() {
@@ -95,6 +100,144 @@ get_pr_originating_issue() {
95
100
  echo "$body" | grep -oiE '(closes|fixes|resolves) #[0-9]+' | grep -oE '[0-9]+' | head -1
96
101
  }
97
102
 
103
+ # ─── Current-Head SHA Discipline ─────────────────────────────────────────────
104
+ # All check results and review approvals MUST correspond to the current PR head
105
+ # SHA. Stale evidence from older commits is never trusted. This is the single
106
+ # most important safety invariant in the Code Factory pattern.
107
+
108
+ validate_checks_for_head_sha() {
109
+ local pr_number="$1"
110
+ local head_sha="$2"
111
+
112
+ if [[ -z "$head_sha" ]]; then
113
+ error "No head SHA provided — cannot validate check freshness"
114
+ return 1
115
+ fi
116
+
117
+ local short_sha="${head_sha:0:7}"
118
+
119
+ # Get check runs for the current head SHA
120
+ local owner_repo
121
+ owner_repo=$(gh repo view --json nameWithOwner --jq '.nameWithOwner' 2>/dev/null || echo "")
122
+ if [[ -z "$owner_repo" ]]; then
123
+ warn "Could not detect repo — skipping SHA discipline check"
124
+ return 0
125
+ fi
126
+
127
+ local check_runs
128
+ check_runs=$(gh api "repos/${owner_repo}/commits/${head_sha}/check-runs" --jq '.check_runs' 2>/dev/null || echo "[]")
129
+
130
+ local total_checks
131
+ total_checks=$(echo "$check_runs" | jq 'length' 2>/dev/null || echo "0")
132
+
133
+ if [[ "$total_checks" -eq 0 ]]; then
134
+ warn "No check runs found for head SHA ${short_sha}"
135
+ return 0
136
+ fi
137
+
138
+ local failed_checks
139
+ failed_checks=$(echo "$check_runs" | jq '[.[] | select(.conclusion == "failure" or .conclusion == "cancelled")] | length' 2>/dev/null || echo "0")
140
+
141
+ local pending_checks
142
+ pending_checks=$(echo "$check_runs" | jq '[.[] | select(.status != "completed")] | length' 2>/dev/null || echo "0")
143
+
144
+ if [[ "$failed_checks" -gt 0 ]]; then
145
+ error "PR #${pr_number} has ${failed_checks} failed check(s) on current head ${short_sha}"
146
+ return 1
147
+ fi
148
+
149
+ if [[ "$pending_checks" -gt 0 ]]; then
150
+ warn "PR #${pr_number} has ${pending_checks} pending check(s) on head ${short_sha}"
151
+ return 1
152
+ fi
153
+
154
+ info "All ${total_checks} checks passed for current head SHA ${short_sha}"
155
+ return 0
156
+ }
157
+
158
+ validate_reviews_for_head_sha() {
159
+ local pr_number="$1"
160
+ local head_sha="$2"
161
+
162
+ if [[ -z "$head_sha" ]]; then
163
+ return 0
164
+ fi
165
+
166
+ local short_sha="${head_sha:0:7}"
167
+
168
+ # Get reviews and check they're not stale (submitted before the latest push)
169
+ local reviews_json
170
+ reviews_json=$(gh pr view "$pr_number" --json reviews --jq '.reviews' 2>/dev/null || echo "[]")
171
+
172
+ local latest_commit_date
173
+ latest_commit_date=$(gh pr view "$pr_number" --json commits --jq '.commits[-1].committedDate' 2>/dev/null || echo "")
174
+
175
+ if [[ -z "$latest_commit_date" ]]; then
176
+ return 0
177
+ fi
178
+
179
+ # Check if any approvals are stale (submitted before last commit)
180
+ local stale_approvals
181
+ stale_approvals=$(echo "$reviews_json" | jq --arg cutoff "$latest_commit_date" \
182
+ '[.[] | select(.state == "APPROVED" and .submittedAt < $cutoff)] | length' 2>/dev/null || echo "0")
183
+
184
+ if [[ "$stale_approvals" -gt 0 ]]; then
185
+ warn "PR #${pr_number} has ${stale_approvals} stale approval(s) from before head ${short_sha} — reviews should be refreshed"
186
+ fi
187
+
188
+ return 0
189
+ }
190
+
191
+ compute_risk_tier_for_pr() {
192
+ local pr_number="$1"
193
+ local policy_file="${REPO_DIR}/config/policy.json"
194
+
195
+ if [[ ! -f "$policy_file" ]]; then
196
+ echo "medium"
197
+ return
198
+ fi
199
+
200
+ local changed_files
201
+ changed_files=$(gh pr diff "$pr_number" --name-only 2>/dev/null || echo "")
202
+
203
+ if [[ -z "$changed_files" ]]; then
204
+ echo "low"
205
+ return
206
+ fi
207
+
208
+ local tier="low"
209
+
210
+ check_tier_match() {
211
+ local check_tier="$1"
212
+ local patterns
213
+ patterns=$(jq -r ".riskTierRules.${check_tier}[]? // empty" "$policy_file" 2>/dev/null)
214
+ [[ -z "$patterns" ]] && return 1
215
+
216
+ while IFS= read -r pattern; do
217
+ [[ -z "$pattern" ]] && continue
218
+ local regex
219
+ regex=$(echo "$pattern" | sed 's/\./\\./g; s/\*\*/DOUBLESTAR/g; s/\*/[^\/]*/g; s/DOUBLESTAR/.*/g')
220
+ while IFS= read -r file; do
221
+ [[ -z "$file" ]] && continue
222
+ if echo "$file" | grep -qE "^${regex}$"; then
223
+ return 0
224
+ fi
225
+ done <<< "$changed_files"
226
+ done <<< "$patterns"
227
+ return 1
228
+ }
229
+
230
+ if check_tier_match "critical"; then
231
+ tier="critical"
232
+ elif check_tier_match "high"; then
233
+ tier="high"
234
+ elif check_tier_match "medium"; then
235
+ tier="medium"
236
+ fi
237
+
238
+ echo "$tier"
239
+ }
240
+
98
241
  # ─── Review Pass ────────────────────────────────────────────────────────────
99
242
 
100
243
  pr_review() {
@@ -220,6 +363,35 @@ pr_merge() {
220
363
  return 1
221
364
  fi
222
365
 
366
+ # ── Current-head SHA discipline ──────────────────────────────────────────
367
+ # All evidence (checks, reviews) must be validated against the current head.
368
+ # Never merge on stale evidence from an older commit.
369
+ local head_sha
370
+ head_sha=$(echo "$pr_info" | jq -r '.headRefOid // empty' 2>/dev/null)
371
+ if [[ -z "$head_sha" ]]; then
372
+ head_sha=$(get_pr_head_sha "$pr_number")
373
+ fi
374
+
375
+ if [[ -n "$head_sha" ]]; then
376
+ local short_sha="${head_sha:0:7}"
377
+ info "Validating evidence for current head SHA: ${short_sha}"
378
+
379
+ if ! validate_checks_for_head_sha "$pr_number" "$head_sha"; then
380
+ error "PR #${pr_number} blocked — checks not passing for current head ${short_sha}"
381
+ emit_event "pr.merge_failed" "pr=${pr_number}" "reason=stale_checks" "head_sha=${short_sha}"
382
+ return 1
383
+ fi
384
+
385
+ validate_reviews_for_head_sha "$pr_number" "$head_sha"
386
+ else
387
+ warn "Could not determine head SHA — falling back to legacy check"
388
+ fi
389
+
390
+ # ── Risk tier enforcement ────────────────────────────────────────────────
391
+ local risk_tier
392
+ risk_tier=$(compute_risk_tier_for_pr "$pr_number")
393
+ info "Risk tier: ${risk_tier}"
394
+
223
395
  # Check for merge conflicts
224
396
  if has_merge_conflicts "$pr_number"; then
225
397
  error "PR #${pr_number} has merge conflicts — manual intervention required"
@@ -227,7 +399,7 @@ pr_merge() {
227
399
  return 1
228
400
  fi
229
401
 
230
- # Check CI status
402
+ # Check CI status (legacy check, supplementary to SHA-based validation)
231
403
  local status_check_rollup
232
404
  status_check_rollup=$(echo "$pr_info" | jq -r '.statusCheckRollup[].state' 2>/dev/null | sort | uniq)
233
405
  if [[ -z "$status_check_rollup" ]] || echo "$status_check_rollup" | grep -qi "failure\|error"; then
@@ -246,10 +418,10 @@ pr_merge() {
246
418
  fi
247
419
 
248
420
  # Perform squash merge and delete branch
249
- info "Merging PR #${pr_number} with squash..."
421
+ info "Merging PR #${pr_number} with squash (tier: ${risk_tier}, head: ${head_sha:0:7})..."
250
422
  if gh pr merge "$pr_number" --squash --delete-branch 2>/dev/null; then
251
423
  success "PR #${pr_number} merged and branch deleted"
252
- emit_event "pr.merged" "pr=${pr_number}"
424
+ emit_event "pr.merged" "pr=${pr_number}" "risk_tier=${risk_tier}" "head_sha=${head_sha:0:7}"
253
425
 
254
426
  # Post feedback to originating issue
255
427
  local issue_number
@@ -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="2.4.0"
10
10
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
11
11
  REPO_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
12
12
 
@@ -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="2.4.0"
10
10
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
11
11
 
12
12
  # ─── Handle subcommands ───────────────────────────────────────────────────────
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="2.4.0"
9
9
  set -euo pipefail
10
10
  trap 'echo "ERROR: $BASH_SOURCE:$LINENO exited with status $?" >&2' ERR
11
11
 
@@ -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="2.4.0"
10
10
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
11
11
  REPO_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
12
12
 
@@ -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="2.4.0"
10
10
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
11
11
  REPO_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
12
12
 
@@ -11,7 +11,7 @@
11
11
  # ║ shipwright reaper --watch Continuous loop (default: 5s) ║
12
12
  # ║ shipwright reaper --dry-run Preview what would be reaped ║
13
13
  # ╚═══════════════════════════════════════════════════════════════════════════╝
14
- VERSION="2.3.1"
14
+ VERSION="2.4.0"
15
15
  set -euo pipefail
16
16
  trap 'echo "ERROR: $BASH_SOURCE:$LINENO exited with status $?" >&2' ERR
17
17
 
@@ -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="2.4.0"
10
10
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
11
11
  REPO_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
12
12
 
@@ -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="2.4.0"
10
10
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
11
11
  REPO_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
12
12
 
@@ -7,7 +7,7 @@ set -euo pipefail
7
7
  trap 'echo "ERROR: $BASH_SOURCE:$LINENO exited with status $?" >&2' ERR
8
8
  trap 'rm -f "${tmp_file:-}"' EXIT
9
9
 
10
- VERSION="2.3.1"
10
+ VERSION="2.4.0"
11
11
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
12
12
  REPO_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
13
13
 
@@ -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="2.4.0"
10
10
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
11
11
  REPO_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
12
12
 
@@ -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="2.4.0"
10
10
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
11
11
  EVENTS_FILE="${HOME}/.shipwright/events.jsonl"
12
12
 
@@ -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="2.4.0"
10
10
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
11
11
 
12
12
  # ─── Cross-platform compatibility ──────────────────────────────────────────
@@ -0,0 +1,220 @@
1
+ #!/usr/bin/env bash
2
+ # ╔═══════════════════════════════════════════════════════════════════════════╗
3
+ # ║ shipwright review-rerun — Canonical Rerun Comment Writer ║
4
+ # ║ SHA-deduped rerun requests · Single writer · No duplicate bot comments ║
5
+ # ║ Part of the Code Factory pattern for deterministic agent review loops ║
6
+ # ╚═══════════════════════════════════════════════════════════════════════════╝
7
+ set -euo pipefail
8
+ trap 'echo "ERROR: $BASH_SOURCE:$LINENO exited with status $?" >&2' ERR
9
+
10
+ VERSION="2.4.0"
11
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
12
+ REPO_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
13
+
14
+ # shellcheck source=lib/compat.sh
15
+ [[ -f "$SCRIPT_DIR/lib/compat.sh" ]] && source "$SCRIPT_DIR/lib/compat.sh"
16
+ # shellcheck source=lib/helpers.sh
17
+ [[ -f "$SCRIPT_DIR/lib/helpers.sh" ]] && source "$SCRIPT_DIR/lib/helpers.sh"
18
+ [[ "$(type -t info 2>/dev/null)" == "function" ]] || info() { echo -e "\033[38;2;0;212;255m\033[1m▸\033[0m $*"; }
19
+ [[ "$(type -t success 2>/dev/null)" == "function" ]] || success() { echo -e "\033[38;2;74;222;128m\033[1m✓\033[0m $*"; }
20
+ [[ "$(type -t warn 2>/dev/null)" == "function" ]] || warn() { echo -e "\033[38;2;250;204;21m\033[1m⚠\033[0m $*"; }
21
+ [[ "$(type -t error 2>/dev/null)" == "function" ]] || error() { echo -e "\033[38;2;248;113;113m\033[1m✗\033[0m $*" >&2; }
22
+ if [[ "$(type -t emit_event 2>/dev/null)" != "function" ]]; then
23
+ emit_event() {
24
+ local event_type="$1"; shift; mkdir -p "${HOME}/.shipwright"
25
+ local payload="{\"ts\":\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\",\"type\":\"$event_type\""
26
+ while [[ $# -gt 0 ]]; do local key="${1%%=*}" val="${1#*=}"; payload="${payload},\"${key}\":\"${val}\""; shift; done
27
+ echo "${payload}}" >> "${HOME}/.shipwright/events.jsonl"
28
+ }
29
+ fi
30
+
31
+ # Load marker from policy or use default
32
+ get_rerun_marker() {
33
+ local policy="${REPO_DIR}/config/policy.json"
34
+ if [[ -f "$policy" ]]; then
35
+ jq -r '.codeReviewAgent.rerunMarker // "<!-- shipwright-review-rerun -->"' "$policy" 2>/dev/null
36
+ else
37
+ echo "<!-- shipwright-review-rerun -->"
38
+ fi
39
+ }
40
+
41
+ # Check if a rerun was already requested for this SHA on this PR
42
+ rerun_already_requested() {
43
+ local pr_number="$1"
44
+ local head_sha="$2"
45
+ local marker
46
+ marker=$(get_rerun_marker)
47
+ local trigger="sha:${head_sha}"
48
+
49
+ local comments
50
+ comments=$(gh pr view "$pr_number" --json comments --jq '.comments[].body' 2>/dev/null || echo "")
51
+
52
+ if echo "$comments" | grep -qF "$marker" && echo "$comments" | grep -qF "$trigger"; then
53
+ return 0
54
+ fi
55
+ return 1
56
+ }
57
+
58
+ # Post a SHA-deduped rerun comment to a PR
59
+ request_rerun() {
60
+ local pr_number="$1"
61
+ local head_sha="$2"
62
+ local review_agent="${3:-shipwright}"
63
+
64
+ if [[ -z "$pr_number" || -z "$head_sha" ]]; then
65
+ error "Usage: sw-review-rerun.sh request <pr_number> <head_sha> [review_agent]"
66
+ return 1
67
+ fi
68
+
69
+ local marker
70
+ marker=$(get_rerun_marker)
71
+ local trigger="sha:${head_sha}"
72
+ local short_sha="${head_sha:0:7}"
73
+
74
+ if rerun_already_requested "$pr_number" "$head_sha"; then
75
+ info "Rerun already requested for PR #${pr_number} at SHA ${short_sha} — skipping"
76
+ return 0
77
+ fi
78
+
79
+ local body="${marker}
80
+ **Review Rerun Requested** (${short_sha})
81
+
82
+ @${review_agent} please re-review this PR at the current head.
83
+
84
+ ${trigger}
85
+ ---
86
+ *Canonical rerun request by Shipwright Code Factory. One writer, SHA-deduped.*"
87
+
88
+ if gh pr comment "$pr_number" --body "$body" 2>/dev/null; then
89
+ success "Rerun requested for PR #${pr_number} at SHA ${short_sha}"
90
+ emit_event "review.rerun_requested" "pr=${pr_number}" "head_sha=${short_sha}" "agent=${review_agent}"
91
+ return 0
92
+ else
93
+ error "Failed to post rerun comment on PR #${pr_number}"
94
+ return 1
95
+ fi
96
+ }
97
+
98
+ # Check current rerun state for a PR
99
+ check_rerun_state() {
100
+ local pr_number="$1"
101
+
102
+ local head_sha
103
+ head_sha=$(gh pr view "$pr_number" --json headRefOid --jq '.headRefOid' 2>/dev/null || echo "")
104
+
105
+ if [[ -z "$head_sha" ]]; then
106
+ error "Could not get head SHA for PR #${pr_number}"
107
+ return 1
108
+ fi
109
+
110
+ local short_sha="${head_sha:0:7}"
111
+
112
+ if rerun_already_requested "$pr_number" "$head_sha"; then
113
+ info "Rerun already requested for current head ${short_sha}"
114
+ else
115
+ info "No rerun requested for current head ${short_sha}"
116
+ fi
117
+
118
+ echo "head_sha=${head_sha}"
119
+ }
120
+
121
+ # Wait for a review agent check to complete on the current head
122
+ wait_for_review() {
123
+ local pr_number="$1"
124
+ local head_sha="$2"
125
+ local timeout_minutes="${3:-20}"
126
+
127
+ local policy="${REPO_DIR}/config/policy.json"
128
+ if [[ -f "$policy" ]]; then
129
+ timeout_minutes=$(jq -r ".codeReviewAgent.timeoutMinutes // ${timeout_minutes}" "$policy" 2>/dev/null || echo "$timeout_minutes")
130
+ fi
131
+
132
+ local short_sha="${head_sha:0:7}"
133
+ local deadline=$(($(date +%s) + timeout_minutes * 60))
134
+
135
+ info "Waiting for review completion on ${short_sha} (timeout: ${timeout_minutes}m)..."
136
+
137
+ while [[ $(date +%s) -lt "$deadline" ]]; do
138
+ local owner_repo
139
+ owner_repo=$(gh repo view --json nameWithOwner --jq '.nameWithOwner' 2>/dev/null || echo "")
140
+ [[ -z "$owner_repo" ]] && { warn "Cannot detect repo"; return 1; }
141
+
142
+ local review_checks
143
+ review_checks=$(gh api "repos/${owner_repo}/commits/${head_sha}/check-runs" \
144
+ --jq '.check_runs[] | select(.name | test("review|code.review"; "i")) | {name: .name, status: .status, conclusion: .conclusion}' 2>/dev/null || echo "")
145
+
146
+ if [[ -n "$review_checks" ]]; then
147
+ local all_complete="true"
148
+ local any_failure="false"
149
+ while IFS= read -r check; do
150
+ [[ -z "$check" ]] && continue
151
+ local status conclusion
152
+ status=$(echo "$check" | jq -r '.status' 2>/dev/null || echo "")
153
+ conclusion=$(echo "$check" | jq -r '.conclusion' 2>/dev/null || echo "")
154
+ if [[ "$status" != "completed" ]]; then
155
+ all_complete="false"
156
+ fi
157
+ if [[ "$conclusion" == "failure" || "$conclusion" == "action_required" ]]; then
158
+ any_failure="true"
159
+ fi
160
+ done <<< "$review_checks"
161
+
162
+ if [[ "$all_complete" == "true" ]]; then
163
+ if [[ "$any_failure" == "true" ]]; then
164
+ error "Review check failed for SHA ${short_sha}"
165
+ return 1
166
+ fi
167
+ success "Review check passed for SHA ${short_sha}"
168
+ return 0
169
+ fi
170
+ fi
171
+
172
+ sleep 30
173
+ done
174
+
175
+ error "Review timed out after ${timeout_minutes}m for SHA ${short_sha}"
176
+ return 1
177
+ }
178
+
179
+ show_help() {
180
+ cat << 'EOF'
181
+ Usage: shipwright review-rerun <command> [args]
182
+
183
+ Commands:
184
+ request <pr#> <sha> [agent] Post SHA-deduped rerun comment
185
+ check <pr#> Check rerun state for current head
186
+ wait <pr#> <sha> [timeout] Wait for review completion on SHA
187
+
188
+ Part of the Code Factory pattern — single canonical rerun writer
189
+ with SHA deduplication to prevent duplicate bot comments.
190
+ EOF
191
+ }
192
+
193
+ main() {
194
+ local subcommand="${1:-help}"
195
+ shift || true
196
+
197
+ case "$subcommand" in
198
+ request)
199
+ request_rerun "$@"
200
+ ;;
201
+ check)
202
+ check_rerun_state "$@"
203
+ ;;
204
+ wait)
205
+ wait_for_review "$@"
206
+ ;;
207
+ help|--help|-h)
208
+ show_help
209
+ ;;
210
+ *)
211
+ error "Unknown subcommand: $subcommand"
212
+ show_help
213
+ return 1
214
+ ;;
215
+ esac
216
+ }
217
+
218
+ if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then
219
+ main "$@"
220
+ 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="2.4.0"
10
10
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
11
11
 
12
12
  # ─── Dependency check ─────────────────────────────────────────────────────────
@@ -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="2.4.0"
10
10
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
11
11
  REPO_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
12
12
 
@@ -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="2.4.0"
10
10
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
11
11
  REPO_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
12
12
 
@@ -8,7 +8,7 @@
8
8
  # ║ Supports --template to scaffold from a team template and --terminal ║
9
9
  # ║ to select a terminal adapter (tmux, iterm2, wezterm). ║
10
10
  # ╚═══════════════════════════════════════════════════════════════════════════╝
11
- VERSION="2.3.1"
11
+ VERSION="2.4.0"
12
12
  set -euo pipefail
13
13
  trap 'echo "ERROR: $BASH_SOURCE:$LINENO exited with status $?" >&2' ERR
14
14
 
@@ -10,7 +10,7 @@
10
10
  set -euo pipefail
11
11
  trap 'echo "ERROR: $BASH_SOURCE:$LINENO exited with status $?" >&2' ERR
12
12
 
13
- VERSION="2.3.1"
13
+ VERSION="2.4.0"
14
14
 
15
15
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
16
16
  REPO_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
@@ -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="2.4.0"
10
10
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
11
11
  REPO_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
12
12
 
@@ -4,7 +4,7 @@
4
4
  # ║ ║
5
5
  # ║ Shows running teams, agent windows, and task progress. ║
6
6
  # ╚═══════════════════════════════════════════════════════════════════════════╝
7
- VERSION="2.3.1"
7
+ VERSION="2.4.0"
8
8
  set -euo pipefail
9
9
  trap 'echo "ERROR: $BASH_SOURCE:$LINENO exited with status $?" >&2' ERR
10
10
 
@@ -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.3.1"
10
+ VERSION="2.4.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)}"
@@ -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.3.1"
8
+ VERSION="2.4.0"
9
9
  set -euo pipefail
10
10
  trap 'echo "ERROR: $BASH_SOURCE:$LINENO exited with status $?" >&2' ERR
11
11