shipwright-cli 1.10.0 → 2.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 (121) hide show
  1. package/README.md +221 -55
  2. package/completions/_shipwright +264 -32
  3. package/completions/shipwright.bash +118 -26
  4. package/completions/shipwright.fish +80 -2
  5. package/dashboard/server.ts +208 -0
  6. package/docs/strategy/01-market-research.md +619 -0
  7. package/docs/strategy/02-mission-and-brand.md +587 -0
  8. package/docs/strategy/03-gtm-and-roadmap.md +759 -0
  9. package/docs/strategy/QUICK-START.txt +289 -0
  10. package/docs/strategy/README.md +172 -0
  11. package/docs/tmux-research/TMUX-ARCHITECTURE.md +567 -0
  12. package/docs/tmux-research/TMUX-AUDIT.md +925 -0
  13. package/docs/tmux-research/TMUX-BEST-PRACTICES-2025-2026.md +829 -0
  14. package/docs/tmux-research/TMUX-QUICK-REFERENCE.md +543 -0
  15. package/docs/tmux-research/TMUX-RESEARCH-INDEX.md +438 -0
  16. package/package.json +4 -2
  17. package/scripts/lib/helpers.sh +7 -0
  18. package/scripts/sw +323 -2
  19. package/scripts/sw-activity.sh +500 -0
  20. package/scripts/sw-adaptive.sh +925 -0
  21. package/scripts/sw-adversarial.sh +1 -1
  22. package/scripts/sw-architecture-enforcer.sh +1 -1
  23. package/scripts/sw-auth.sh +613 -0
  24. package/scripts/sw-autonomous.sh +754 -0
  25. package/scripts/sw-changelog.sh +704 -0
  26. package/scripts/sw-checkpoint.sh +1 -1
  27. package/scripts/sw-ci.sh +602 -0
  28. package/scripts/sw-cleanup.sh +1 -1
  29. package/scripts/sw-code-review.sh +698 -0
  30. package/scripts/sw-connect.sh +1 -1
  31. package/scripts/sw-context.sh +605 -0
  32. package/scripts/sw-cost.sh +44 -3
  33. package/scripts/sw-daemon.sh +568 -138
  34. package/scripts/sw-dashboard.sh +1 -1
  35. package/scripts/sw-db.sh +1380 -0
  36. package/scripts/sw-decompose.sh +539 -0
  37. package/scripts/sw-deps.sh +551 -0
  38. package/scripts/sw-developer-simulation.sh +1 -1
  39. package/scripts/sw-discovery.sh +412 -0
  40. package/scripts/sw-docs-agent.sh +539 -0
  41. package/scripts/sw-docs.sh +1 -1
  42. package/scripts/sw-doctor.sh +107 -1
  43. package/scripts/sw-dora.sh +615 -0
  44. package/scripts/sw-durable.sh +710 -0
  45. package/scripts/sw-e2e-orchestrator.sh +535 -0
  46. package/scripts/sw-eventbus.sh +393 -0
  47. package/scripts/sw-feedback.sh +479 -0
  48. package/scripts/sw-fix.sh +1 -1
  49. package/scripts/sw-fleet-discover.sh +567 -0
  50. package/scripts/sw-fleet-viz.sh +404 -0
  51. package/scripts/sw-fleet.sh +8 -1
  52. package/scripts/sw-github-app.sh +596 -0
  53. package/scripts/sw-github-checks.sh +4 -4
  54. package/scripts/sw-github-deploy.sh +1 -1
  55. package/scripts/sw-github-graphql.sh +1 -1
  56. package/scripts/sw-guild.sh +569 -0
  57. package/scripts/sw-heartbeat.sh +1 -1
  58. package/scripts/sw-hygiene.sh +559 -0
  59. package/scripts/sw-incident.sh +656 -0
  60. package/scripts/sw-init.sh +237 -24
  61. package/scripts/sw-instrument.sh +699 -0
  62. package/scripts/sw-intelligence.sh +1 -1
  63. package/scripts/sw-jira.sh +1 -1
  64. package/scripts/sw-launchd.sh +363 -28
  65. package/scripts/sw-linear.sh +1 -1
  66. package/scripts/sw-logs.sh +1 -1
  67. package/scripts/sw-loop.sh +267 -21
  68. package/scripts/sw-memory.sh +18 -1
  69. package/scripts/sw-mission-control.sh +487 -0
  70. package/scripts/sw-model-router.sh +545 -0
  71. package/scripts/sw-otel.sh +596 -0
  72. package/scripts/sw-oversight.sh +764 -0
  73. package/scripts/sw-pipeline-composer.sh +1 -1
  74. package/scripts/sw-pipeline-vitals.sh +1 -1
  75. package/scripts/sw-pipeline.sh +947 -35
  76. package/scripts/sw-pm.sh +758 -0
  77. package/scripts/sw-pr-lifecycle.sh +522 -0
  78. package/scripts/sw-predictive.sh +8 -1
  79. package/scripts/sw-prep.sh +1 -1
  80. package/scripts/sw-ps.sh +1 -1
  81. package/scripts/sw-public-dashboard.sh +798 -0
  82. package/scripts/sw-quality.sh +595 -0
  83. package/scripts/sw-reaper.sh +1 -1
  84. package/scripts/sw-recruit.sh +2248 -0
  85. package/scripts/sw-regression.sh +642 -0
  86. package/scripts/sw-release-manager.sh +736 -0
  87. package/scripts/sw-release.sh +706 -0
  88. package/scripts/sw-remote.sh +1 -1
  89. package/scripts/sw-replay.sh +520 -0
  90. package/scripts/sw-retro.sh +691 -0
  91. package/scripts/sw-scale.sh +444 -0
  92. package/scripts/sw-security-audit.sh +505 -0
  93. package/scripts/sw-self-optimize.sh +1 -1
  94. package/scripts/sw-session.sh +1 -1
  95. package/scripts/sw-setup.sh +263 -127
  96. package/scripts/sw-standup.sh +712 -0
  97. package/scripts/sw-status.sh +44 -2
  98. package/scripts/sw-strategic.sh +806 -0
  99. package/scripts/sw-stream.sh +450 -0
  100. package/scripts/sw-swarm.sh +620 -0
  101. package/scripts/sw-team-stages.sh +511 -0
  102. package/scripts/sw-templates.sh +4 -4
  103. package/scripts/sw-testgen.sh +566 -0
  104. package/scripts/sw-tmux-pipeline.sh +554 -0
  105. package/scripts/sw-tmux-role-color.sh +58 -0
  106. package/scripts/sw-tmux-status.sh +128 -0
  107. package/scripts/sw-tmux.sh +1 -1
  108. package/scripts/sw-trace.sh +485 -0
  109. package/scripts/sw-tracker-github.sh +188 -0
  110. package/scripts/sw-tracker-jira.sh +172 -0
  111. package/scripts/sw-tracker-linear.sh +251 -0
  112. package/scripts/sw-tracker.sh +117 -2
  113. package/scripts/sw-triage.sh +627 -0
  114. package/scripts/sw-upgrade.sh +1 -1
  115. package/scripts/sw-ux.sh +677 -0
  116. package/scripts/sw-webhook.sh +627 -0
  117. package/scripts/sw-widgets.sh +530 -0
  118. package/scripts/sw-worktree.sh +1 -1
  119. package/templates/pipelines/autonomous.json +2 -2
  120. package/tmux/shipwright-overlay.conf +35 -17
  121. package/tmux/tmux.conf +23 -21
@@ -0,0 +1,128 @@
1
+ #!/usr/bin/env bash
2
+ # ╔═══════════════════════════════════════════════════════════════════════════╗
3
+ # ║ sw-tmux-status.sh — Status bar widgets for tmux ║
4
+ # ║ ║
5
+ # ║ Called by tmux via #() in status-right. Must be FAST (<100ms). ║
6
+ # ║ Reads pipeline state from .claude/pipeline-state.md and heartbeats ║
7
+ # ║ from ~/.shipwright/heartbeats/. Outputs styled tmux format strings. ║
8
+ # ╚═══════════════════════════════════════════════════════════════════════════╝
9
+ set -euo pipefail
10
+
11
+ # ─── Stage colors (match Shipwright brand palette) ────────────────────────
12
+ # Each pipeline stage gets a distinct color for instant visual recognition
13
+ stage_color() {
14
+ case "${1:-}" in
15
+ intake) echo "#71717a" ;; # muted — gathering
16
+ plan) echo "#7c3aed" ;; # purple — thinking
17
+ design) echo "#7c3aed" ;; # purple — thinking
18
+ build) echo "#0066ff" ;; # blue — working
19
+ test) echo "#facc15" ;; # yellow — validating
20
+ review) echo "#f97316" ;; # orange — scrutinizing
21
+ compound_quality) echo "#f97316" ;; # orange — scrutinizing
22
+ pr) echo "#00d4ff" ;; # cyan — shipping
23
+ merge) echo "#00d4ff" ;; # cyan — shipping
24
+ deploy) echo "#4ade80" ;; # green — deploying
25
+ validate) echo "#4ade80" ;; # green — verifying
26
+ monitor) echo "#4ade80" ;; # green — watching
27
+ *) echo "#71717a" ;; # muted fallback
28
+ esac
29
+ }
30
+
31
+ # ─── Stage icons ──────────────────────────────────────────────────────────
32
+ stage_icon() {
33
+ case "${1:-}" in
34
+ intake) echo "◇" ;;
35
+ plan) echo "◆" ;;
36
+ design) echo "△" ;;
37
+ build) echo "⚙" ;;
38
+ test) echo "⚡" ;;
39
+ review) echo "◎" ;;
40
+ compound_quality) echo "◎" ;;
41
+ pr) echo "↑" ;;
42
+ merge) echo "⊕" ;;
43
+ deploy) echo "▲" ;;
44
+ validate) echo "✦" ;;
45
+ monitor) echo "◉" ;;
46
+ *) echo "·" ;;
47
+ esac
48
+ }
49
+
50
+ # ─── Pipeline stage widget ────────────────────────────────────────────────
51
+ # Reads current pipeline stage from state file, outputs tmux format string
52
+ pipeline_widget() {
53
+ local state_file=".claude/pipeline-state.md"
54
+
55
+ # Try current directory, then walk up to find repo root
56
+ if [[ ! -f "$state_file" ]]; then
57
+ local dir
58
+ dir="$(pwd)"
59
+ while [[ "$dir" != "/" ]]; do
60
+ if [[ -f "$dir/$state_file" ]]; then
61
+ state_file="$dir/$state_file"
62
+ break
63
+ fi
64
+ dir="$(dirname "$dir")"
65
+ done
66
+ fi
67
+
68
+ [[ -f "$state_file" ]] || return 0
69
+
70
+ # Extract current stage — look for "Stage:" or "## Stage:" pattern
71
+ local stage=""
72
+ stage="$(grep -iE '^\*?\*?(current )?stage:?\*?\*?' "$state_file" 2>/dev/null | head -1 | sed 's/.*: *//' | tr -d '*' | tr '[:upper:]' '[:lower:]' | tr -d ' ')" || true
73
+
74
+ [[ -n "$stage" ]] || return 0
75
+
76
+ local color icon
77
+ color="$(stage_color "$stage")"
78
+ icon="$(stage_icon "$stage")"
79
+ local label
80
+ label="$(echo "$stage" | tr '[:lower:]' '[:upper:]')"
81
+
82
+ # Output: colored badge with icon
83
+ echo "#[fg=#1e1e32,bg=${color},bold] ${icon} ${label} #[fg=${color},bg=#1a1a2e]"
84
+ }
85
+
86
+ # ─── Agent count widget ──────────────────────────────────────────────────
87
+ # Shows number of active agents from heartbeat files
88
+ agent_widget() {
89
+ local hb_dir="${HOME}/.shipwright/heartbeats"
90
+ [[ -d "$hb_dir" ]] || return 0
91
+
92
+ local now count=0
93
+ now="$(date +%s)"
94
+
95
+ for hb in "$hb_dir"/*.json; do
96
+ [[ -f "$hb" ]] || continue
97
+ # Heartbeat is alive if updated within last 60 seconds
98
+ local mtime
99
+ if [[ "$(uname)" == "Darwin" ]]; then
100
+ mtime="$(stat -f %m "$hb" 2>/dev/null || echo 0)"
101
+ else
102
+ mtime="$(stat -c %Y "$hb" 2>/dev/null || echo 0)"
103
+ fi
104
+ if (( now - mtime < 60 )); then
105
+ count=$((count + 1))
106
+ fi
107
+ done
108
+
109
+ if [[ $count -gt 0 ]]; then
110
+ echo "#[fg=#1e1e32,bg=#7c3aed,bold] λ${count} #[fg=#7c3aed,bg=#1a1a2e]"
111
+ fi
112
+ }
113
+
114
+ # ─── Dispatch ─────────────────────────────────────────────────────────────
115
+ case "${1:-pipeline}" in
116
+ pipeline) pipeline_widget ;;
117
+ agents) agent_widget ;;
118
+ all)
119
+ # Combine both widgets
120
+ local p a
121
+ p="$(pipeline_widget)"
122
+ a="$(agent_widget)"
123
+ echo "${a}${p}"
124
+ ;;
125
+ *)
126
+ echo ""
127
+ ;;
128
+ esac
@@ -11,7 +11,7 @@
11
11
  # ║ shipwright tmux fix — Auto-fix common issues ║
12
12
  # ║ shipwright tmux reload — Reload tmux config ║
13
13
  # ╚═══════════════════════════════════════════════════════════════════════════╝
14
- VERSION="1.10.0"
14
+ VERSION="2.1.0"
15
15
  set -euo pipefail
16
16
  trap 'echo "ERROR: $BASH_SOURCE:$LINENO exited with status $?" >&2' ERR
17
17
 
@@ -0,0 +1,485 @@
1
+ #!/usr/bin/env bash
2
+ # ╔═══════════════════════════════════════════════════════════════════════════╗
3
+ # ║ shipwright trace — E2E Traceability (Issue → Commit → PR → Deploy) ║
4
+ # ║ Query and link the full chain from GitHub issue to production ║
5
+ # ╚═══════════════════════════════════════════════════════════════════════════╝
6
+ set -euo pipefail
7
+ trap 'echo "ERROR: $BASH_SOURCE:$LINENO exited with status $?" >&2' ERR
8
+
9
+ VERSION="2.1.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
+
36
+ # ─── Data Paths ─────────────────────────────────────────────────────────────
37
+ EVENTS_FILE="${HOME}/.shipwright/events.jsonl"
38
+ SHIPWRIGHT_DIR="${REPO_DIR}/.claude/pipeline-artifacts"
39
+
40
+ # ─── Helper: Extract GitHub repo owner/name ──────────────────────────────────
41
+ get_gh_repo() {
42
+ gh repo view --json nameWithOwner -q 2>/dev/null || echo ""
43
+ }
44
+
45
+ # ─── Helper: Extract issue number from branch name ──────────────────────────
46
+ issue_from_branch() {
47
+ local branch="$1"
48
+ # Handle feat/...-N, fix/...-N, issue-N patterns
49
+ if [[ "$branch" =~ -([0-9]+)$ ]]; then
50
+ echo "${BASH_REMATCH[1]}"
51
+ fi
52
+ }
53
+
54
+ # ─── trace_show: Full chain for a single issue ──────────────────────────────
55
+ trace_show() {
56
+ local issue="$1"
57
+
58
+ if [[ ! "$issue" =~ ^[0-9]+$ ]]; then
59
+ error "Issue must be a number"
60
+ return 1
61
+ fi
62
+
63
+ info "Tracing issue #${issue}..."
64
+ echo ""
65
+
66
+ # Get issue details from GitHub
67
+ local issue_data
68
+ if ! issue_data=$(gh issue view "$issue" --json "title,state,assignees,labels,url,createdAt,closedAt" 2>/dev/null); then
69
+ error "Could not fetch issue #${issue}. Check permissions or issue number."
70
+ return 1
71
+ fi
72
+
73
+ local title state url
74
+ title=$(echo "$issue_data" | jq -r '.title')
75
+ state=$(echo "$issue_data" | jq -r '.state')
76
+ url=$(echo "$issue_data" | jq -r '.url')
77
+
78
+ # ─── Issue Section ─────────────────────────────────────────────────────
79
+ echo -e "${BOLD}ISSUE${RESET}"
80
+ echo -e " ${CYAN}#${issue}${RESET} ${BOLD}${title}${RESET}"
81
+ echo -e " State: ${CYAN}${state}${RESET} • URL: ${BLUE}${url}${RESET}"
82
+ echo ""
83
+
84
+ # ─── Pipeline Section ──────────────────────────────────────────────────
85
+ echo -e "${BOLD}PIPELINE${RESET}"
86
+
87
+ # Find pipeline events for this issue
88
+ if [[ ! -f "$EVENTS_FILE" ]]; then
89
+ warn "No events log found at $EVENTS_FILE"
90
+ else
91
+ local pipeline_started
92
+ pipeline_started=$(grep "\"issue\":${issue}" "$EVENTS_FILE" | head -1)
93
+
94
+ if [[ -z "$pipeline_started" ]]; then
95
+ echo -e " ${DIM}No pipeline run found for this issue${RESET}"
96
+ else
97
+ local ts job_id stage
98
+ ts=$(echo "$pipeline_started" | jq -r '.ts // "unknown"')
99
+ job_id=$(echo "$pipeline_started" | jq -r '.job_id // "unknown"')
100
+ stage=$(echo "$pipeline_started" | jq -r '.stage // "intake"')
101
+
102
+ echo -e " Job ID: ${CYAN}${job_id}${RESET}"
103
+ echo -e " Started: ${DIM}${ts}${RESET}"
104
+
105
+ # Find max stage reached
106
+ local max_stage
107
+ max_stage=$(grep "\"job_id\":\"${job_id}\"" "$EVENTS_FILE" \
108
+ | jq -r '.stage // ""' 2>/dev/null \
109
+ | grep -v '^$' | tail -1)
110
+
111
+ if [[ -n "$max_stage" ]]; then
112
+ echo -e " Last Stage: ${GREEN}${max_stage}${RESET}"
113
+ fi
114
+ fi
115
+ fi
116
+ echo ""
117
+
118
+ # ─── Feature Branch Section ────────────────────────────────────────────
119
+ echo -e "${BOLD}FEATURE BRANCH${RESET}"
120
+
121
+ local feature_branch
122
+ feature_branch="feat/issue-${issue}"
123
+
124
+ # Check if worktree exists
125
+ local worktree_path="${REPO_DIR}/.worktrees/issue-${issue}"
126
+ if [[ -d "$worktree_path" ]]; then
127
+ echo -e " Worktree: ${GREEN}${worktree_path}${RESET}"
128
+
129
+ # Get commits from worktree
130
+ cd "$worktree_path" 2>/dev/null || true
131
+ local commit_count
132
+ commit_count=$(git rev-list --count main..HEAD 2>/dev/null || echo "0")
133
+ echo -e " Commits: ${CYAN}${commit_count}${RESET}"
134
+
135
+ # Show recent commits
136
+ if [[ "$commit_count" -gt 0 ]]; then
137
+ echo -e " ${DIM}Recent commits:${RESET}"
138
+ git log --oneline -5 main..HEAD 2>/dev/null | while read -r sha msg; do
139
+ echo -e " ${CYAN}${sha:0:7}${RESET} ${DIM}${msg}${RESET}"
140
+ done
141
+ fi
142
+ cd - >/dev/null 2>&1 || true
143
+ else
144
+ # Try to find any branch matching the issue
145
+ local branches
146
+ branches=$(git branch -r --list "*issue-${issue}*" 2>/dev/null || echo "")
147
+ if [[ -z "$branches" ]]; then
148
+ echo -e " ${DIM}No branch found${RESET}"
149
+ else
150
+ echo "$branches" | while read -r branch; do
151
+ branch=$(echo "$branch" | xargs)
152
+ echo -e " ${CYAN}${branch}${RESET}"
153
+ done
154
+ fi
155
+ fi
156
+ echo ""
157
+
158
+ # ─── Pull Request Section ──────────────────────────────────────────────
159
+ echo -e "${BOLD}PULL REQUEST${RESET}"
160
+
161
+ # Search for PR linked to this issue
162
+ local pr_data
163
+ pr_data=$(gh pr list --state all --search "issue:${issue}" --json "number,title,state,mergedAt,url" -L 1 2>/dev/null || echo "")
164
+
165
+ if [[ -z "$pr_data" ]] || [[ "$pr_data" == "[]" ]]; then
166
+ # Fallback: look for PR with matching branch
167
+ pr_data=$(gh pr list --state all --search "head:feat/issue-${issue}" --json "number,title,state,mergedAt,url" -L 1 2>/dev/null || echo "")
168
+ fi
169
+
170
+ if [[ -z "$pr_data" ]] || [[ "$pr_data" == "[]" ]]; then
171
+ echo -e " ${DIM}No PR found${RESET}"
172
+ else
173
+ local pr_num pr_title pr_state merged_at pr_url
174
+ pr_num=$(echo "$pr_data" | jq -r '.[0].number // "unknown"')
175
+ pr_title=$(echo "$pr_data" | jq -r '.[0].title // "unknown"')
176
+ pr_state=$(echo "$pr_data" | jq -r '.[0].state // "unknown"')
177
+ merged_at=$(echo "$pr_data" | jq -r '.[0].mergedAt // ""')
178
+ pr_url=$(echo "$pr_data" | jq -r '.[0].url // ""')
179
+
180
+ echo -e " ${CYAN}#${pr_num}${RESET} ${pr_title}"
181
+ echo -e " State: ${CYAN}${pr_state}${RESET} • URL: ${BLUE}${pr_url}${RESET}"
182
+
183
+ if [[ -n "$merged_at" && "$merged_at" != "null" ]]; then
184
+ echo -e " Merged: ${GREEN}${merged_at}${RESET}"
185
+ fi
186
+ fi
187
+ echo ""
188
+
189
+ # ─── Deployment Section ────────────────────────────────────────────────
190
+ echo -e "${BOLD}DEPLOYMENT${RESET}"
191
+
192
+ # Check if deployment tracking exists
193
+ if [[ -f "${SHIPWRIGHT_DIR}/deployment.json" ]]; then
194
+ local deploy_env deploy_status
195
+ deploy_env=$(jq -r '.environment // "unknown"' "${SHIPWRIGHT_DIR}/deployment.json" 2>/dev/null || echo "")
196
+ deploy_status=$(jq -r '.status // "unknown"' "${SHIPWRIGHT_DIR}/deployment.json" 2>/dev/null || echo "")
197
+
198
+ if [[ -n "$deploy_env" ]] && [[ "$deploy_env" != "null" ]]; then
199
+ echo -e " Environment: ${CYAN}${deploy_env}${RESET}"
200
+ echo -e " Status: ${GREEN}${deploy_status}${RESET}"
201
+ else
202
+ echo -e " ${DIM}No deployment tracked${RESET}"
203
+ fi
204
+ else
205
+ echo -e " ${DIM}No deployment tracked${RESET}"
206
+ fi
207
+ echo ""
208
+ }
209
+
210
+ # ─── trace_list: Recent pipeline activity ──────────────────────────────────
211
+ trace_list() {
212
+ local limit="${1:-10}"
213
+
214
+ if [[ ! -f "$EVENTS_FILE" ]]; then
215
+ warn "No events log found"
216
+ return 1
217
+ fi
218
+
219
+ info "Recent pipeline runs (last ${limit})..."
220
+ echo ""
221
+
222
+ # Extract unique job_ids with their issues
223
+ local jobs
224
+ jobs=$(grep '"job_id"' "$EVENTS_FILE" | jq -r '.job_id' | sort -u | tail -n "$limit")
225
+
226
+ if [[ -z "$jobs" ]]; then
227
+ echo " ${DIM}No pipeline runs found${RESET}"
228
+ return 0
229
+ fi
230
+
231
+ local count=0
232
+ echo -e "${BOLD}JOB_ID ISSUE STAGE STATUS DURATION${RESET}"
233
+ echo -e "${DIM}──────────────────────────────────────────────────────────────────────────────${RESET}"
234
+
235
+ while read -r job_id; do
236
+ ((count++ <= limit)) || break
237
+
238
+ # Get first and last event for this job
239
+ local first_event last_event issue stage status duration_s
240
+ first_event=$(grep "\"job_id\":\"${job_id}\"" "$EVENTS_FILE" | head -1)
241
+ last_event=$(grep "\"job_id\":\"${job_id}\"" "$EVENTS_FILE" | tail -1)
242
+
243
+ issue=$(echo "$first_event" | jq -r '.issue // ""')
244
+ stage=$(echo "$last_event" | jq -r '.stage // "?"')
245
+ status=$(echo "$last_event" | jq -r '.status // "?"')
246
+ duration_s=$(echo "$last_event" | jq -r '.duration_secs // 0')
247
+
248
+ # Format duration
249
+ local duration_fmt
250
+ if [[ "$duration_s" -gt 3600 ]]; then
251
+ duration_fmt="$(( duration_s / 3600 ))h $(( (duration_s % 3600) / 60 ))m"
252
+ elif [[ "$duration_s" -gt 60 ]]; then
253
+ duration_fmt="$(( duration_s / 60 ))m"
254
+ else
255
+ duration_fmt="${duration_s}s"
256
+ fi
257
+
258
+ # Color status
259
+ local status_color
260
+ case "$status" in
261
+ completed|success) status_color="${GREEN}${status}${RESET}" ;;
262
+ failed|error) status_color="${RED}${status}${RESET}" ;;
263
+ running|in_progress) status_color="${CYAN}${status}${RESET}" ;;
264
+ *) status_color="$status" ;;
265
+ esac
266
+
267
+ printf "%-32s #%-4s %-15s %-12s %s\n" \
268
+ "${job_id:0:30}" \
269
+ "${issue:-?}" \
270
+ "$stage" \
271
+ "$status_color" \
272
+ "$duration_fmt"
273
+ done <<< "$jobs"
274
+
275
+ echo ""
276
+ }
277
+
278
+ # ─── trace_search: Find issue/pipeline by commit ─────────────────────────────
279
+ trace_search() {
280
+ local commit_sha="$1"
281
+
282
+ if [[ ! "$commit_sha" =~ ^[a-f0-9]{6,40}$ ]]; then
283
+ error "Commit SHA must be 6-40 hex characters"
284
+ return 1
285
+ fi
286
+
287
+ info "Searching for commit ${CYAN}${commit_sha:0:8}${RESET}..."
288
+ echo ""
289
+
290
+ # Find which branch contains this commit
291
+ local branch
292
+ branch=$(git branch -r --contains "$commit_sha" 2>/dev/null | head -1 | xargs || echo "")
293
+
294
+ if [[ -z "$branch" ]]; then
295
+ warn "Commit not found in any tracked branch"
296
+ return 1
297
+ fi
298
+
299
+ # Try to extract issue number from branch name
300
+ local issue
301
+ issue=$(issue_from_branch "$branch")
302
+
303
+ echo -e "${BOLD}COMMIT${RESET}"
304
+ echo -e " SHA: ${CYAN}${commit_sha:0:8}${RESET}"
305
+ echo -e " Branch: ${CYAN}${branch}${RESET}"
306
+ echo ""
307
+
308
+ if [[ -n "$issue" ]]; then
309
+ echo -e "${BOLD}LINKED ISSUE${RESET}"
310
+ echo -e " Issue: ${CYAN}#${issue}${RESET}"
311
+ echo ""
312
+
313
+ # Show full trace for this issue
314
+ trace_show "$issue" || true
315
+ else
316
+ warn "Could not extract issue number from branch name"
317
+ echo " Branch: ${CYAN}${branch}${RESET}"
318
+ echo ""
319
+ fi
320
+ }
321
+
322
+ # ─── trace_export: Generate markdown report ──────────────────────────────────
323
+ trace_export() {
324
+ local issue="$1"
325
+ local output_file="${2:-trace-issue-${issue}.md}"
326
+
327
+ if [[ ! "$issue" =~ ^[0-9]+$ ]]; then
328
+ error "Issue must be a number"
329
+ return 1
330
+ fi
331
+
332
+ info "Exporting trace for issue #${issue} to ${CYAN}${output_file}${RESET}..."
333
+
334
+ # Get issue details
335
+ local issue_data title state url created_at
336
+ if ! issue_data=$(gh issue view "$issue" --json "title,state,url,createdAt,closedAt" 2>/dev/null); then
337
+ error "Could not fetch issue #${issue}"
338
+ return 1
339
+ fi
340
+
341
+ title=$(echo "$issue_data" | jq -r '.title')
342
+ state=$(echo "$issue_data" | jq -r '.state')
343
+ url=$(echo "$issue_data" | jq -r '.url')
344
+ created_at=$(echo "$issue_data" | jq -r '.createdAt')
345
+
346
+ # Build markdown report
347
+ local report=""
348
+ report+="# Traceability Report: Issue #${issue}\n\n"
349
+ report+="## Issue\n\n"
350
+ report+="- **Title**: ${title}\n"
351
+ report+="- **State**: ${state}\n"
352
+ report+="- **URL**: [${url}](${url})\n"
353
+ report+="- **Created**: ${created_at}\n"
354
+ report+="- **Report Generated**: $(now_iso)\n\n"
355
+
356
+ # Pipeline section
357
+ report+="## Pipeline\n\n"
358
+ if [[ -f "$EVENTS_FILE" ]]; then
359
+ local job_data job_id ts stage max_stage
360
+ job_data=$(grep "\"issue\":${issue}" "$EVENTS_FILE" 2>/dev/null | head -1)
361
+
362
+ if [[ -n "$job_data" ]]; then
363
+ job_id=$(echo "$job_data" | jq -r '.job_id // "unknown"')
364
+ ts=$(echo "$job_data" | jq -r '.ts // "unknown"')
365
+ stage=$(echo "$job_data" | jq -r '.stage // "unknown"')
366
+
367
+ report+="- **Job ID**: \`${job_id}\`\n"
368
+ report+="- **Started**: ${ts}\n"
369
+
370
+ # Find final stage
371
+ max_stage=$(grep "\"job_id\":\"${job_id}\"" "$EVENTS_FILE" 2>/dev/null \
372
+ | jq -r '.stage // ""' | tail -1)
373
+ report+="- **Final Stage**: ${max_stage}\n\n"
374
+ else
375
+ report+="No pipeline run found.\n\n"
376
+ fi
377
+ else
378
+ report+="No events log available.\n\n"
379
+ fi
380
+
381
+ # Commits section
382
+ report+="## Commits\n\n"
383
+ local commit_count
384
+ commit_count=$(git rev-list --count main..feat/issue-"${issue}" 2>/dev/null || echo "0")
385
+
386
+ if [[ "$commit_count" -gt 0 ]]; then
387
+ report+="Found ${commit_count} commits on feature branch:\n\n"
388
+ report+="\`\`\`\n"
389
+ report+=$(git log --oneline main..feat/issue-"${issue}" 2>/dev/null || echo "(no commits)")
390
+ report+="\n\`\`\`\n\n"
391
+ else
392
+ report+="No commits on feature branch.\n\n"
393
+ fi
394
+
395
+ # PR section
396
+ report+="## Pull Request\n\n"
397
+ local pr_data
398
+ pr_data=$(gh pr list --state all --search "issue:${issue}" --json "number,title,state,url" -L 1 2>/dev/null || echo "")
399
+
400
+ if [[ -n "$pr_data" ]] && [[ "$pr_data" != "[]" ]]; then
401
+ local pr_num pr_title pr_state pr_url
402
+ pr_num=$(echo "$pr_data" | jq -r '.[0].number // "unknown"')
403
+ pr_title=$(echo "$pr_data" | jq -r '.[0].title // "unknown"')
404
+ pr_state=$(echo "$pr_data" | jq -r '.[0].state // "unknown"')
405
+ pr_url=$(echo "$pr_data" | jq -r '.[0].url // "unknown"')
406
+
407
+ report+="- **Number**: [#${pr_num}](${pr_url})\n"
408
+ report+="- **Title**: ${pr_title}\n"
409
+ report+="- **State**: ${pr_state}\n"
410
+ else
411
+ report+="No PR found.\n"
412
+ fi
413
+ report+="\n"
414
+
415
+ # Write report
416
+ echo -e "$report" > "$output_file"
417
+ success "Exported to ${CYAN}${output_file}${RESET}"
418
+ echo ""
419
+ }
420
+
421
+ # ─── Show help ──────────────────────────────────────────────────────────────
422
+ show_help() {
423
+ echo -e "${BOLD}shipwright trace${RESET} — E2E Traceability (Issue → Commit → PR → Deploy)"
424
+ echo ""
425
+ echo -e "${BOLD}USAGE${RESET}"
426
+ echo -e " ${CYAN}shipwright trace${RESET} <command> [options]"
427
+ echo ""
428
+ echo -e "${BOLD}COMMANDS${RESET}"
429
+ echo -e " ${CYAN}show <issue>${RESET} Show full traceability chain for an issue"
430
+ echo -e " ${CYAN}list [limit]${RESET} Show recent pipeline runs (default: 10)"
431
+ echo -e " ${CYAN}search --commit <sha>${RESET} Find issue/pipeline for a commit"
432
+ echo -e " ${CYAN}export <issue> [file]${RESET} Export traceability as markdown"
433
+ echo ""
434
+ echo -e "${BOLD}EXAMPLES${RESET}"
435
+ echo -e " ${DIM}shipwright trace show 42${RESET}"
436
+ echo -e " ${DIM}shipwright trace list 20${RESET}"
437
+ echo -e " ${DIM}shipwright trace search --commit abc1234${RESET}"
438
+ echo -e " ${DIM}shipwright trace export 42 trace-issue-42.md${RESET}"
439
+ echo ""
440
+ }
441
+
442
+ # ─── Main ───────────────────────────────────────────────────────────────────
443
+ main() {
444
+ local cmd="${1:-help}"
445
+ shift 2>/dev/null || true
446
+
447
+ case "$cmd" in
448
+ show)
449
+ if [[ -z "${1:-}" ]]; then
450
+ error "Issue number required"
451
+ return 1
452
+ fi
453
+ trace_show "$1"
454
+ ;;
455
+ list)
456
+ trace_list "${1:-10}"
457
+ ;;
458
+ search)
459
+ if [[ "${1:-}" != "--commit" ]] || [[ -z "${2:-}" ]]; then
460
+ error "Usage: shipwright trace search --commit <sha>"
461
+ return 1
462
+ fi
463
+ trace_search "$2"
464
+ ;;
465
+ export)
466
+ if [[ -z "${1:-}" ]]; then
467
+ error "Issue number required"
468
+ return 1
469
+ fi
470
+ trace_export "$1" "${2:-}"
471
+ ;;
472
+ help|--help|-h)
473
+ show_help
474
+ ;;
475
+ *)
476
+ error "Unknown command: ${cmd}"
477
+ show_help
478
+ return 1
479
+ ;;
480
+ esac
481
+ }
482
+
483
+ if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then
484
+ main "$@"
485
+ fi