shipwright-cli 1.10.0 → 2.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 (108) hide show
  1. package/README.md +114 -36
  2. package/completions/_shipwright +212 -32
  3. package/completions/shipwright.bash +97 -25
  4. package/docs/strategy/01-market-research.md +619 -0
  5. package/docs/strategy/02-mission-and-brand.md +587 -0
  6. package/docs/strategy/03-gtm-and-roadmap.md +759 -0
  7. package/docs/strategy/QUICK-START.txt +289 -0
  8. package/docs/strategy/README.md +172 -0
  9. package/package.json +4 -2
  10. package/scripts/sw +208 -1
  11. package/scripts/sw-activity.sh +500 -0
  12. package/scripts/sw-adaptive.sh +925 -0
  13. package/scripts/sw-adversarial.sh +1 -1
  14. package/scripts/sw-architecture-enforcer.sh +1 -1
  15. package/scripts/sw-auth.sh +613 -0
  16. package/scripts/sw-autonomous.sh +664 -0
  17. package/scripts/sw-changelog.sh +704 -0
  18. package/scripts/sw-checkpoint.sh +1 -1
  19. package/scripts/sw-ci.sh +602 -0
  20. package/scripts/sw-cleanup.sh +1 -1
  21. package/scripts/sw-code-review.sh +637 -0
  22. package/scripts/sw-connect.sh +1 -1
  23. package/scripts/sw-context.sh +605 -0
  24. package/scripts/sw-cost.sh +1 -1
  25. package/scripts/sw-daemon.sh +432 -130
  26. package/scripts/sw-dashboard.sh +1 -1
  27. package/scripts/sw-db.sh +540 -0
  28. package/scripts/sw-decompose.sh +539 -0
  29. package/scripts/sw-deps.sh +551 -0
  30. package/scripts/sw-developer-simulation.sh +1 -1
  31. package/scripts/sw-discovery.sh +412 -0
  32. package/scripts/sw-docs-agent.sh +539 -0
  33. package/scripts/sw-docs.sh +1 -1
  34. package/scripts/sw-doctor.sh +59 -1
  35. package/scripts/sw-dora.sh +615 -0
  36. package/scripts/sw-durable.sh +710 -0
  37. package/scripts/sw-e2e-orchestrator.sh +535 -0
  38. package/scripts/sw-eventbus.sh +393 -0
  39. package/scripts/sw-feedback.sh +471 -0
  40. package/scripts/sw-fix.sh +1 -1
  41. package/scripts/sw-fleet-discover.sh +567 -0
  42. package/scripts/sw-fleet-viz.sh +404 -0
  43. package/scripts/sw-fleet.sh +8 -1
  44. package/scripts/sw-github-app.sh +596 -0
  45. package/scripts/sw-github-checks.sh +1 -1
  46. package/scripts/sw-github-deploy.sh +1 -1
  47. package/scripts/sw-github-graphql.sh +1 -1
  48. package/scripts/sw-guild.sh +569 -0
  49. package/scripts/sw-heartbeat.sh +1 -1
  50. package/scripts/sw-hygiene.sh +559 -0
  51. package/scripts/sw-incident.sh +617 -0
  52. package/scripts/sw-init.sh +88 -1
  53. package/scripts/sw-instrument.sh +699 -0
  54. package/scripts/sw-intelligence.sh +1 -1
  55. package/scripts/sw-jira.sh +1 -1
  56. package/scripts/sw-launchd.sh +363 -28
  57. package/scripts/sw-linear.sh +1 -1
  58. package/scripts/sw-logs.sh +1 -1
  59. package/scripts/sw-loop.sh +64 -3
  60. package/scripts/sw-memory.sh +1 -1
  61. package/scripts/sw-mission-control.sh +487 -0
  62. package/scripts/sw-model-router.sh +545 -0
  63. package/scripts/sw-otel.sh +596 -0
  64. package/scripts/sw-oversight.sh +689 -0
  65. package/scripts/sw-pipeline-composer.sh +1 -1
  66. package/scripts/sw-pipeline-vitals.sh +1 -1
  67. package/scripts/sw-pipeline.sh +687 -24
  68. package/scripts/sw-pm.sh +693 -0
  69. package/scripts/sw-pr-lifecycle.sh +522 -0
  70. package/scripts/sw-predictive.sh +1 -1
  71. package/scripts/sw-prep.sh +1 -1
  72. package/scripts/sw-ps.sh +1 -1
  73. package/scripts/sw-public-dashboard.sh +798 -0
  74. package/scripts/sw-quality.sh +595 -0
  75. package/scripts/sw-reaper.sh +1 -1
  76. package/scripts/sw-recruit.sh +573 -0
  77. package/scripts/sw-regression.sh +642 -0
  78. package/scripts/sw-release-manager.sh +736 -0
  79. package/scripts/sw-release.sh +706 -0
  80. package/scripts/sw-remote.sh +1 -1
  81. package/scripts/sw-replay.sh +520 -0
  82. package/scripts/sw-retro.sh +691 -0
  83. package/scripts/sw-scale.sh +444 -0
  84. package/scripts/sw-security-audit.sh +505 -0
  85. package/scripts/sw-self-optimize.sh +1 -1
  86. package/scripts/sw-session.sh +1 -1
  87. package/scripts/sw-setup.sh +1 -1
  88. package/scripts/sw-standup.sh +712 -0
  89. package/scripts/sw-status.sh +1 -1
  90. package/scripts/sw-strategic.sh +658 -0
  91. package/scripts/sw-stream.sh +450 -0
  92. package/scripts/sw-swarm.sh +583 -0
  93. package/scripts/sw-team-stages.sh +511 -0
  94. package/scripts/sw-templates.sh +1 -1
  95. package/scripts/sw-testgen.sh +515 -0
  96. package/scripts/sw-tmux-pipeline.sh +554 -0
  97. package/scripts/sw-tmux.sh +1 -1
  98. package/scripts/sw-trace.sh +485 -0
  99. package/scripts/sw-tracker-github.sh +188 -0
  100. package/scripts/sw-tracker-jira.sh +172 -0
  101. package/scripts/sw-tracker-linear.sh +251 -0
  102. package/scripts/sw-tracker.sh +117 -2
  103. package/scripts/sw-triage.sh +603 -0
  104. package/scripts/sw-upgrade.sh +1 -1
  105. package/scripts/sw-ux.sh +677 -0
  106. package/scripts/sw-webhook.sh +627 -0
  107. package/scripts/sw-widgets.sh +530 -0
  108. package/scripts/sw-worktree.sh +1 -1
@@ -0,0 +1,522 @@
1
+ #!/usr/bin/env bash
2
+ # ╔═══════════════════════════════════════════════════════════════════════════╗
3
+ # ║ shipwright pr-lifecycle — Autonomous PR Management ║
4
+ # ║ Auto-review · Auto-merge · Stale cleanup · Issue feedback ║
5
+ # ╚═══════════════════════════════════════════════════════════════════════════╝
6
+ set -euo pipefail
7
+ trap 'echo "ERROR: $BASH_SOURCE:$LINENO exited with status $?" >&2' ERR
8
+
9
+ VERSION="2.0.0"
10
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
11
+ REPO_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
12
+
13
+ # ─── Colors (matches Seth's tmux theme) ─────────────────────────────────────
14
+ CYAN='\033[38;2;0;212;255m' # #00d4ff — primary accent
15
+ PURPLE='\033[38;2;124;58;237m' # #7c3aed — secondary
16
+ BLUE='\033[38;2;0;102;255m' # #0066ff — tertiary
17
+ GREEN='\033[38;2;74;222;128m' # success
18
+ YELLOW='\033[38;2;250;204;21m' # warning
19
+ RED='\033[38;2;248;113;113m' # error
20
+ DIM='\033[2m'
21
+ BOLD='\033[1m'
22
+ RESET='\033[0m'
23
+
24
+ # ─── Cross-platform compatibility ──────────────────────────────────────────
25
+ # shellcheck source=lib/compat.sh
26
+ [[ -f "$SCRIPT_DIR/lib/compat.sh" ]] && source "$SCRIPT_DIR/lib/compat.sh"
27
+
28
+ # ─── Output Helpers ─────────────────────────────────────────────────────────
29
+ info() { echo -e "${CYAN}${BOLD}▸${RESET} $*"; }
30
+ success() { echo -e "${GREEN}${BOLD}✓${RESET} $*"; }
31
+ warn() { echo -e "${YELLOW}${BOLD}⚠${RESET} $*"; }
32
+ error() { echo -e "${RED}${BOLD}✗${RESET} $*" >&2; }
33
+
34
+ now_iso() { date -u +"%Y-%m-%dT%H:%M:%SZ"; }
35
+ now_epoch() { date +%s; }
36
+
37
+ # ─── Structured Event Log ──────────────────────────────────────────────────
38
+ EVENTS_FILE="${HOME}/.shipwright/events.jsonl"
39
+
40
+ emit_event() {
41
+ local event_type="$1"
42
+ shift
43
+ local json_fields=""
44
+ for kv in "$@"; do
45
+ local key="${kv%%=*}"
46
+ local val="${kv#*=}"
47
+ if [[ "$val" =~ ^-?[0-9]+\.?[0-9]*$ ]]; then
48
+ json_fields="${json_fields},\"${key}\":${val}"
49
+ else
50
+ local escaped_val
51
+ escaped_val=$(printf '%s' "$val" | jq -Rs '.' 2>/dev/null || printf '"%s"' "${val//\"/\\\"}")
52
+ json_fields="${json_fields},\"${key}\":${escaped_val}"
53
+ fi
54
+ done
55
+ mkdir -p "${HOME}/.shipwright"
56
+ echo "{\"ts\":\"$(now_iso)\",\"ts_epoch\":$(now_epoch),\"type\":\"${event_type}\"${json_fields}}" >> "$EVENTS_FILE"
57
+ }
58
+
59
+ # ─── Configuration Helpers ──────────────────────────────────────────────────
60
+
61
+ get_pr_config() {
62
+ local key="$1"
63
+ local default="${2:-}"
64
+ jq -r ".pr_lifecycle.${key} // \"${default}\"" "$REPO_DIR/.claude/daemon-config.json" 2>/dev/null || echo "$default"
65
+ }
66
+
67
+ # ─── GitHub API Wrappers ────────────────────────────────────────────────────
68
+
69
+ get_pr_info() {
70
+ local pr_number="$1"
71
+ gh pr view "$pr_number" --json number,title,body,state,headRefName,baseRefName,statusCheckRollup,reviews,commits,createdAt,updatedAt 2>/dev/null || return 1
72
+ }
73
+
74
+ get_pr_checks_status() {
75
+ local pr_number="$1"
76
+ # Returns: success, failure, pending, or unknown
77
+ gh pr checks "$pr_number" 2>/dev/null | jq -r '.[] | select(.status == "completed") | .conclusion' | sort | uniq -c | sort -rn | head -1 || echo "unknown"
78
+ }
79
+
80
+ has_merge_conflicts() {
81
+ local pr_number="$1"
82
+ gh pr view "$pr_number" --json mergeStateStatus 2>/dev/null | jq -r '.mergeStateStatus' | grep -qi "conflicting" && return 0 || return 1
83
+ }
84
+
85
+ get_pr_reviews() {
86
+ local pr_number="$1"
87
+ # Returns approved, changes_requested, pending, or none
88
+ gh pr view "$pr_number" --json reviews 2>/dev/null | jq -r '.reviews[] | .state' 2>/dev/null | sort | uniq || echo "none"
89
+ }
90
+
91
+ get_pr_age_seconds() {
92
+ local pr_number="$1"
93
+ local created_at
94
+ created_at=$(gh pr view "$pr_number" --json createdAt 2>/dev/null | jq -r '.createdAt' | head -1)
95
+ [[ -z "$created_at" ]] && return 1
96
+ local created_epoch
97
+ created_epoch=$(date -d "$created_at" +%s 2>/dev/null || date -jf "%Y-%m-%dT%H:%M:%SZ" "$created_at" +%s 2>/dev/null || echo "0")
98
+ [[ "$created_epoch" -eq 0 ]] && return 1
99
+ echo $(($(now_epoch) - created_epoch))
100
+ }
101
+
102
+ get_pr_originating_issue() {
103
+ local pr_number="$1"
104
+ # Search PR body for issue reference (closes #N, fixes #N, etc.)
105
+ local body
106
+ body=$(gh pr view "$pr_number" --json body 2>/dev/null | jq -r '.body')
107
+ echo "$body" | grep -oiE '(closes|fixes|resolves) #[0-9]+' | grep -oE '[0-9]+' | head -1
108
+ }
109
+
110
+ # ─── Review Pass ────────────────────────────────────────────────────────────
111
+
112
+ pr_review() {
113
+ local pr_number="$1"
114
+ info "Running review pass on PR #${pr_number}..."
115
+
116
+ # Get PR info
117
+ local pr_info
118
+ pr_info=$(get_pr_info "$pr_number") || {
119
+ error "Failed to fetch PR #${pr_number}"
120
+ return 1
121
+ }
122
+
123
+ local title branch
124
+ title=$(echo "$pr_info" | jq -r '.title')
125
+ branch=$(echo "$pr_info" | jq -r '.headRefName')
126
+
127
+ info "PR: ${title} (branch: ${branch})"
128
+
129
+ # Get diff
130
+ local diff_output
131
+ diff_output=$(gh pr diff "$pr_number" 2>/dev/null) || {
132
+ error "Failed to get PR diff"
133
+ return 1
134
+ }
135
+
136
+ if [[ -z "$diff_output" ]]; then
137
+ warn "No diff found for PR #${pr_number}"
138
+ return 1
139
+ fi
140
+
141
+ # Evaluate quality criteria
142
+ local issues_found=0
143
+ local file_count
144
+ file_count=$(echo "$diff_output" | grep -c '^diff --git' || echo "0")
145
+
146
+ local line_additions
147
+ line_additions=$(echo "$diff_output" | grep -c '^+' || echo "0")
148
+
149
+ local line_deletions
150
+ line_deletions=$(echo "$diff_output" | grep -c '^-' || echo "0")
151
+
152
+ info "Diff analysis: ${file_count} files, +${line_additions}/-${line_deletions} lines"
153
+
154
+ # Check for concerning patterns
155
+ local warnings=""
156
+ if echo "$diff_output" | grep -qE '(HACK|TODO|FIXME|XXX|BROKEN|DEBUG)'; then
157
+ warnings="${warnings}
158
+ - Found HACK/TODO/FIXME markers in code"
159
+ ((issues_found++))
160
+ fi
161
+
162
+ if echo "$diff_output" | grep -qE 'console\.(log|warn|error)\('; then
163
+ warnings="${warnings}
164
+ - Found console.log statements (should use proper logging)"
165
+ ((issues_found++))
166
+ fi
167
+
168
+ if [[ $line_additions -gt 500 ]]; then
169
+ warnings="${warnings}
170
+ - Large addition (${line_additions} lines) — consider splitting into smaller PRs"
171
+ ((issues_found++))
172
+ fi
173
+
174
+ if [[ $file_count -gt 20 ]]; then
175
+ warnings="${warnings}
176
+ - Many files changed (${file_count}) — consider splitting"
177
+ ((issues_found++))
178
+ fi
179
+
180
+ # Post review comment to PR
181
+ local review_body="## Shipwright Auto-Review
182
+
183
+ **Status:** Review complete
184
+ **Files changed:** ${file_count}
185
+ **Lines added/removed:** +${line_additions}/-${line_deletions}"
186
+
187
+ if [[ $issues_found -gt 0 ]]; then
188
+ review_body="${review_body}
189
+
190
+ **Issues found:** ${issues_found}
191
+ ${warnings}"
192
+ else
193
+ review_body="${review_body}
194
+
195
+ **Issues found:** 0
196
+ ✓ No concerning patterns detected"
197
+ fi
198
+
199
+ gh pr comment "$pr_number" --body "$review_body" 2>/dev/null || warn "Failed to post review comment"
200
+
201
+ success "Review complete for PR #${pr_number} (${issues_found} issues found)"
202
+ emit_event "pr.review_complete" "pr=${pr_number}" "issues_found=${issues_found}"
203
+ }
204
+
205
+ # ─── Auto-Merge ──────────────────────────────────────────────────────────────
206
+
207
+ pr_merge() {
208
+ local pr_number="$1"
209
+ info "Attempting auto-merge of PR #${pr_number}..."
210
+
211
+ # Check if auto-merge is enabled
212
+ local auto_merge_enabled
213
+ auto_merge_enabled=$(get_pr_config "auto_merge_enabled" "false")
214
+ if [[ "$auto_merge_enabled" != "true" ]]; then
215
+ warn "Auto-merge is disabled in configuration"
216
+ return 1
217
+ fi
218
+
219
+ # Get PR info
220
+ local pr_info
221
+ pr_info=$(get_pr_info "$pr_number") || {
222
+ error "Failed to fetch PR #${pr_number}"
223
+ return 1
224
+ }
225
+
226
+ local state branch
227
+ state=$(echo "$pr_info" | jq -r '.state')
228
+ branch=$(echo "$pr_info" | jq -r '.headRefName')
229
+
230
+ if [[ "$state" != "OPEN" ]]; then
231
+ warn "PR #${pr_number} is not open (state: ${state})"
232
+ return 1
233
+ fi
234
+
235
+ # Check for merge conflicts
236
+ if has_merge_conflicts "$pr_number"; then
237
+ error "PR #${pr_number} has merge conflicts — manual intervention required"
238
+ emit_event "pr.merge_failed" "pr=${pr_number}" "reason=merge_conflicts"
239
+ return 1
240
+ fi
241
+
242
+ # Check CI status
243
+ local status_check_rollup
244
+ status_check_rollup=$(echo "$pr_info" | jq -r '.statusCheckRollup[].state' 2>/dev/null | sort | uniq)
245
+ if [[ -z "$status_check_rollup" ]] || echo "$status_check_rollup" | grep -qi "failure\|error"; then
246
+ error "PR #${pr_number} has failing CI checks"
247
+ emit_event "pr.merge_failed" "pr=${pr_number}" "reason=ci_failure"
248
+ return 1
249
+ fi
250
+
251
+ # Check reviews
252
+ local reviews
253
+ reviews=$(get_pr_reviews "$pr_number")
254
+ if [[ "$reviews" == *"CHANGES_REQUESTED"* ]]; then
255
+ error "PR #${pr_number} has requested changes"
256
+ emit_event "pr.merge_failed" "pr=${pr_number}" "reason=changes_requested"
257
+ return 1
258
+ fi
259
+
260
+ # Perform squash merge and delete branch
261
+ info "Merging PR #${pr_number} with squash..."
262
+ if gh pr merge "$pr_number" --squash --delete-branch 2>/dev/null; then
263
+ success "PR #${pr_number} merged and branch deleted"
264
+ emit_event "pr.merged" "pr=${pr_number}"
265
+
266
+ # Post feedback to originating issue
267
+ local issue_number
268
+ issue_number=$(get_pr_originating_issue "$pr_number")
269
+ if [[ -n "$issue_number" ]]; then
270
+ pr_feedback_to_issue "$issue_number" "$pr_number"
271
+ fi
272
+ return 0
273
+ else
274
+ error "Failed to merge PR #${pr_number}"
275
+ emit_event "pr.merge_failed" "pr=${pr_number}" "reason=merge_command_failed"
276
+ return 1
277
+ fi
278
+ }
279
+
280
+ # ─── Stale PR Cleanup ────────────────────────────────────────────────────────
281
+
282
+ pr_cleanup() {
283
+ info "Cleaning up stale pull requests..."
284
+
285
+ local stale_days
286
+ stale_days=$(get_pr_config "stale_days" "14")
287
+
288
+ local stale_seconds
289
+ stale_seconds=$((stale_days * 86400))
290
+
291
+ # List all open Shipwright PRs
292
+ local pr_list
293
+ pr_list=$(gh pr list --state open --search "author:@me" --json number,title,createdAt 2>/dev/null || echo "[]")
294
+
295
+ local closed_count=0
296
+ while IFS= read -r line; do
297
+ [[ -z "$line" ]] && continue
298
+
299
+ local pr_number
300
+ pr_number=$(echo "$line" | jq -r '.number')
301
+ local title
302
+ title=$(echo "$line" | jq -r '.title')
303
+
304
+ # Check if this is a Shipwright PR (look for issue references or pipeline markers)
305
+ if ! echo "$title" | grep -qiE '(pipeline|issue|shipwright)'; then
306
+ continue
307
+ fi
308
+
309
+ local age_seconds
310
+ age_seconds=$(get_pr_age_seconds "$pr_number") || continue
311
+
312
+ if [[ $age_seconds -gt $stale_seconds ]]; then
313
+ local age_days=$((age_seconds / 86400))
314
+ info "Closing stale PR #${pr_number}: ${title} (${age_days} days old)"
315
+
316
+ local close_comment="## Auto-Closed: Stale PR
317
+
318
+ This PR was automatically closed because it has been open for ${age_days} days without activity.
319
+
320
+ If this PR is still needed:
321
+ 1. Reopen it with \`gh pr reopen ${pr_number}\`
322
+ 2. Update the branch
323
+ 3. Request review
324
+
325
+ ${DIM}— Shipwright auto-lifecycle manager${RESET}"
326
+
327
+ gh pr comment "$pr_number" --body "$close_comment" 2>/dev/null || true
328
+ gh pr close "$pr_number" 2>/dev/null && {
329
+ success "Closed PR #${pr_number}"
330
+ ((closed_count++))
331
+ emit_event "pr.closed_stale" "pr=${pr_number}" "age_days=${age_days}"
332
+ }
333
+ fi
334
+ done < <(echo "$pr_list" | jq -c '.[]' 2>/dev/null || true)
335
+
336
+ if [[ $closed_count -eq 0 ]]; then
337
+ info "No stale PRs found"
338
+ else
339
+ success "Cleaned up ${closed_count} stale PR(s)"
340
+ fi
341
+ }
342
+
343
+ # ─── Issue Feedback on Merge ──────────────────────────────────────────────────
344
+
345
+ pr_feedback_to_issue() {
346
+ local issue_number="$1"
347
+ local pr_number="$2"
348
+
349
+ [[ -z "$issue_number" ]] && return 0
350
+
351
+ info "Posting merge feedback to issue #${issue_number}..."
352
+
353
+ local feedback_body="## PR Merged
354
+
355
+ The pull request #${pr_number} has been successfully merged into the base branch.
356
+
357
+ **Summary:**
358
+ - Squash merged with clean history
359
+ - Feature branch deleted
360
+ - Ready for deployment
361
+
362
+ ${DIM}— Shipwright PR Lifecycle Manager${RESET}"
363
+
364
+ if gh issue comment "$issue_number" --body "$feedback_body" 2>/dev/null; then
365
+ success "Posted feedback to issue #${issue_number}"
366
+ emit_event "issue.pr_merged_feedback" "issue=${issue_number}" "pr=${pr_number}"
367
+ else
368
+ warn "Failed to post feedback to issue #${issue_number}"
369
+ fi
370
+ }
371
+
372
+ # ─── Status Dashboard ────────────────────────────────────────────────────────
373
+
374
+ pr_status() {
375
+ info "Shipwright Pull Requests Status"
376
+ echo ""
377
+
378
+ # Get all open Shipwright PRs
379
+ local pr_list
380
+ pr_list=$(gh pr list --state open --search "author:@me" --json number,title,state,createdAt,reviewDecision,statusCheckRollup 2>/dev/null || echo "[]")
381
+
382
+ if [[ $(echo "$pr_list" | jq 'length') -eq 0 ]]; then
383
+ echo "No open pull requests"
384
+ return 0
385
+ fi
386
+
387
+ echo -e "${BOLD}PR #${TAB}Title${TAB}Age${TAB}Reviews${TAB}CI Status${RESET}"
388
+ echo "─────────────────────────────────────────────────────────"
389
+
390
+ while IFS= read -r line; do
391
+ [[ -z "$line" ]] && continue
392
+
393
+ local pr_number title created_at review_decision
394
+ pr_number=$(echo "$line" | jq -r '.number')
395
+ title=$(echo "$line" | jq -r '.title' | cut -c1-40)
396
+ created_at=$(echo "$line" | jq -r '.createdAt')
397
+ review_decision=$(echo "$line" | jq -r '.reviewDecision // "PENDING"')
398
+
399
+ # Calculate age
400
+ local created_epoch
401
+ created_epoch=$(date -d "$created_at" +%s 2>/dev/null || date -jf "%Y-%m-%dT%H:%M:%SZ" "$created_at" +%s 2>/dev/null || echo "0")
402
+ local age_hours=$((( $(now_epoch) - created_epoch) / 3600))
403
+ local age_str="${age_hours}h"
404
+ [[ $age_hours -gt 24 ]] && age_str="$((age_hours / 24))d"
405
+
406
+ # Get checks status
407
+ local checks_status="unknown"
408
+ if echo "$line" | jq -e '.statusCheckRollup[0]' > /dev/null 2>&1; then
409
+ checks_status=$(echo "$line" | jq -r '.statusCheckRollup[0].state' 2>/dev/null)
410
+ fi
411
+
412
+ # Color-code output
413
+ local status_color="$DIM"
414
+ case "$checks_status" in
415
+ success) status_color="$GREEN" ;;
416
+ failure) status_color="$RED" ;;
417
+ pending) status_color="$YELLOW" ;;
418
+ esac
419
+
420
+ printf "%s%-5s${DIM}│${RESET} %-40s %-5s %-10s %s${status_color}%s${RESET}\n" \
421
+ "$status_color" "#${pr_number}" "$title" "$age_str" "$review_decision" "$DIM" "$checks_status"
422
+ done < <(echo "$pr_list" | jq -c '.[]' 2>/dev/null || true)
423
+
424
+ echo ""
425
+ echo "Legend: APPROVED = Ready to merge | PENDING = Waiting for review | CHANGES_REQUESTED = Needs work"
426
+ }
427
+
428
+ # ─── Patrol Mode (for daemon integration) ────────────────────────────────────
429
+
430
+ pr_patrol() {
431
+ info "Running PR lifecycle patrol..."
432
+
433
+ # Review all open PRs
434
+ info "Phase 1: Reviewing open PRs..."
435
+ local pr_list
436
+ pr_list=$(gh pr list --state open --search "author:@me" --json number 2>/dev/null || echo "[]")
437
+ while IFS= read -r line; do
438
+ [[ -z "$line" ]] && continue
439
+ local pr_number
440
+ pr_number=$(echo "$line" | jq -r '.number')
441
+ pr_review "$pr_number" || true
442
+ done < <(echo "$pr_list" | jq -c '.[]' 2>/dev/null || true)
443
+
444
+ # Attempt merges
445
+ info "Phase 2: Attempting auto-merges..."
446
+ pr_list=$(gh pr list --state open --search "author:@me" --json number 2>/dev/null || echo "[]")
447
+ while IFS= read -r line; do
448
+ [[ -z "$line" ]] && continue
449
+ local pr_number
450
+ pr_number=$(echo "$line" | jq -r '.number')
451
+ pr_merge "$pr_number" || true
452
+ done < <(echo "$pr_list" | jq -c '.[]' 2>/dev/null || true)
453
+
454
+ # Cleanup stale PRs
455
+ info "Phase 3: Cleaning up stale PRs..."
456
+ pr_cleanup || true
457
+
458
+ success "PR lifecycle patrol complete"
459
+ emit_event "pr_patrol.complete"
460
+ }
461
+
462
+ # ─── Help ────────────────────────────────────────────────────────────────────
463
+
464
+ show_help() {
465
+ echo ""
466
+ echo -e "${BOLD}shipwright pr <command>${RESET}"
467
+ echo ""
468
+ echo -e "${BOLD}COMMANDS${RESET}"
469
+ echo -e " ${CYAN}review <number>${RESET} Run review pass on a PR (checks code quality, posts findings)"
470
+ echo -e " ${CYAN}merge <number>${RESET} Attempt auto-merge (checks CI, conflicts, reviews, then merges)"
471
+ echo -e " ${CYAN}cleanup${RESET} Close stale PRs (older than configured days, default 14)"
472
+ echo -e " ${CYAN}status${RESET} Show all open Shipwright PRs with lifecycle state"
473
+ echo -e " ${CYAN}patrol${RESET} Run full PR lifecycle patrol (review + merge + cleanup)"
474
+ echo -e " ${CYAN}help${RESET} Show this help"
475
+ echo ""
476
+ echo -e "${BOLD}EXAMPLES${RESET}"
477
+ echo -e " ${DIM}shipwright pr review 42${RESET} # Review PR #42"
478
+ echo -e " ${DIM}shipwright pr merge 42${RESET} # Try to merge PR #42"
479
+ echo -e " ${DIM}shipwright pr cleanup${RESET} # Close stale PRs"
480
+ echo -e " ${DIM}shipwright pr status${RESET} # Show all open PRs"
481
+ echo -e " ${DIM}shipwright pr patrol${RESET} # Full lifecycle management"
482
+ echo ""
483
+ }
484
+
485
+ # ─── Main ────────────────────────────────────────────────────────────────────
486
+
487
+ main() {
488
+ local cmd="${1:-help}"
489
+ shift 2>/dev/null || true
490
+
491
+ case "$cmd" in
492
+ review)
493
+ [[ -z "${1:-}" ]] && { error "PR number required"; show_help; exit 1; }
494
+ pr_review "$1"
495
+ ;;
496
+ merge)
497
+ [[ -z "${1:-}" ]] && { error "PR number required"; show_help; exit 1; }
498
+ pr_merge "$1"
499
+ ;;
500
+ cleanup)
501
+ pr_cleanup
502
+ ;;
503
+ status)
504
+ pr_status
505
+ ;;
506
+ patrol)
507
+ pr_patrol
508
+ ;;
509
+ help|--help|-h)
510
+ show_help
511
+ ;;
512
+ *)
513
+ error "Unknown command: ${cmd}"
514
+ show_help
515
+ exit 1
516
+ ;;
517
+ esac
518
+ }
519
+
520
+ if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then
521
+ main "$@"
522
+ 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="1.10.0"
9
+ VERSION="2.0.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="1.10.0"
9
+ VERSION="2.0.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="1.10.0"
8
+ VERSION="2.0.0"
9
9
  set -euo pipefail
10
10
  trap 'echo "ERROR: $BASH_SOURCE:$LINENO exited with status $?" >&2' ERR
11
11