shipwright-cli 1.7.1 → 1.10.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 (115) hide show
  1. package/.claude/agents/code-reviewer.md +90 -0
  2. package/.claude/agents/devops-engineer.md +142 -0
  3. package/.claude/agents/pipeline-agent.md +80 -0
  4. package/.claude/agents/shell-script-specialist.md +150 -0
  5. package/.claude/agents/test-specialist.md +196 -0
  6. package/.claude/hooks/post-tool-use.sh +45 -0
  7. package/.claude/hooks/pre-tool-use.sh +25 -0
  8. package/.claude/hooks/session-started.sh +37 -0
  9. package/README.md +212 -814
  10. package/claude-code/CLAUDE.md.shipwright +54 -0
  11. package/claude-code/hooks/notify-idle.sh +2 -2
  12. package/claude-code/hooks/session-start.sh +24 -0
  13. package/claude-code/hooks/task-completed.sh +6 -2
  14. package/claude-code/settings.json.template +12 -0
  15. package/dashboard/public/app.js +4422 -0
  16. package/dashboard/public/index.html +816 -0
  17. package/dashboard/public/styles.css +4755 -0
  18. package/dashboard/server.ts +4315 -0
  19. package/docs/KNOWN-ISSUES.md +18 -10
  20. package/docs/TIPS.md +38 -26
  21. package/docs/patterns/README.md +33 -23
  22. package/package.json +9 -5
  23. package/scripts/adapters/iterm2-adapter.sh +1 -1
  24. package/scripts/adapters/tmux-adapter.sh +52 -23
  25. package/scripts/adapters/wezterm-adapter.sh +26 -14
  26. package/scripts/lib/compat.sh +200 -0
  27. package/scripts/lib/helpers.sh +72 -0
  28. package/scripts/postinstall.mjs +72 -13
  29. package/scripts/{cct → sw} +118 -22
  30. package/scripts/sw-adversarial.sh +274 -0
  31. package/scripts/sw-architecture-enforcer.sh +330 -0
  32. package/scripts/sw-checkpoint.sh +468 -0
  33. package/scripts/sw-cleanup.sh +359 -0
  34. package/scripts/sw-connect.sh +619 -0
  35. package/scripts/{cct-cost.sh → sw-cost.sh} +368 -34
  36. package/scripts/sw-daemon.sh +5574 -0
  37. package/scripts/sw-dashboard.sh +477 -0
  38. package/scripts/sw-developer-simulation.sh +252 -0
  39. package/scripts/sw-docs.sh +635 -0
  40. package/scripts/sw-doctor.sh +907 -0
  41. package/scripts/{cct-fix.sh → sw-fix.sh} +10 -6
  42. package/scripts/{cct-fleet.sh → sw-fleet.sh} +498 -22
  43. package/scripts/sw-github-checks.sh +521 -0
  44. package/scripts/sw-github-deploy.sh +533 -0
  45. package/scripts/sw-github-graphql.sh +972 -0
  46. package/scripts/sw-heartbeat.sh +293 -0
  47. package/scripts/{cct-init.sh → sw-init.sh} +144 -11
  48. package/scripts/sw-intelligence.sh +1196 -0
  49. package/scripts/sw-jira.sh +643 -0
  50. package/scripts/sw-launchd.sh +364 -0
  51. package/scripts/sw-linear.sh +648 -0
  52. package/scripts/{cct-logs.sh → sw-logs.sh} +72 -2
  53. package/scripts/sw-loop.sh +2217 -0
  54. package/scripts/{cct-memory.sh → sw-memory.sh} +514 -36
  55. package/scripts/sw-patrol-meta.sh +417 -0
  56. package/scripts/sw-pipeline-composer.sh +455 -0
  57. package/scripts/sw-pipeline-vitals.sh +1096 -0
  58. package/scripts/sw-pipeline.sh +7593 -0
  59. package/scripts/sw-predictive.sh +820 -0
  60. package/scripts/{cct-prep.sh → sw-prep.sh} +339 -49
  61. package/scripts/{cct-ps.sh → sw-ps.sh} +9 -6
  62. package/scripts/{cct-reaper.sh → sw-reaper.sh} +10 -6
  63. package/scripts/sw-remote.sh +687 -0
  64. package/scripts/sw-self-optimize.sh +1048 -0
  65. package/scripts/sw-session.sh +541 -0
  66. package/scripts/sw-setup.sh +234 -0
  67. package/scripts/sw-status.sh +796 -0
  68. package/scripts/{cct-templates.sh → sw-templates.sh} +9 -4
  69. package/scripts/sw-tmux.sh +591 -0
  70. package/scripts/sw-tracker-jira.sh +277 -0
  71. package/scripts/sw-tracker-linear.sh +292 -0
  72. package/scripts/sw-tracker.sh +409 -0
  73. package/scripts/{cct-upgrade.sh → sw-upgrade.sh} +103 -46
  74. package/scripts/{cct-worktree.sh → sw-worktree.sh} +3 -0
  75. package/templates/pipelines/autonomous.json +35 -6
  76. package/templates/pipelines/cost-aware.json +21 -0
  77. package/templates/pipelines/deployed.json +40 -6
  78. package/templates/pipelines/enterprise.json +16 -2
  79. package/templates/pipelines/fast.json +19 -0
  80. package/templates/pipelines/full.json +28 -2
  81. package/templates/pipelines/hotfix.json +19 -0
  82. package/templates/pipelines/standard.json +31 -0
  83. package/tmux/{claude-teams-overlay.conf → shipwright-overlay.conf} +27 -9
  84. package/tmux/templates/accessibility.json +34 -0
  85. package/tmux/templates/api-design.json +35 -0
  86. package/tmux/templates/architecture.json +1 -0
  87. package/tmux/templates/bug-fix.json +9 -0
  88. package/tmux/templates/code-review.json +1 -0
  89. package/tmux/templates/compliance.json +36 -0
  90. package/tmux/templates/data-pipeline.json +36 -0
  91. package/tmux/templates/debt-paydown.json +34 -0
  92. package/tmux/templates/devops.json +1 -0
  93. package/tmux/templates/documentation.json +1 -0
  94. package/tmux/templates/exploration.json +1 -0
  95. package/tmux/templates/feature-dev.json +1 -0
  96. package/tmux/templates/full-stack.json +8 -0
  97. package/tmux/templates/i18n.json +34 -0
  98. package/tmux/templates/incident-response.json +36 -0
  99. package/tmux/templates/migration.json +1 -0
  100. package/tmux/templates/observability.json +35 -0
  101. package/tmux/templates/onboarding.json +33 -0
  102. package/tmux/templates/performance.json +35 -0
  103. package/tmux/templates/refactor.json +1 -0
  104. package/tmux/templates/release.json +35 -0
  105. package/tmux/templates/security-audit.json +8 -0
  106. package/tmux/templates/spike.json +34 -0
  107. package/tmux/templates/testing.json +1 -0
  108. package/tmux/tmux.conf +98 -9
  109. package/scripts/cct-cleanup.sh +0 -172
  110. package/scripts/cct-daemon.sh +0 -3189
  111. package/scripts/cct-doctor.sh +0 -414
  112. package/scripts/cct-loop.sh +0 -1332
  113. package/scripts/cct-pipeline.sh +0 -3844
  114. package/scripts/cct-session.sh +0 -284
  115. package/scripts/cct-status.sh +0 -169
@@ -0,0 +1,907 @@
1
+ #!/usr/bin/env bash
2
+ # ╔═══════════════════════════════════════════════════════════════════════════╗
3
+ # ║ sw-doctor.sh — Validate Shipwright setup ║
4
+ # ║ ║
5
+ # ║ Checks prerequisites, installed files, PATH, and common issues. ║
6
+ # ╚═══════════════════════════════════════════════════════════════════════════╝
7
+ VERSION="1.10.0"
8
+ set -euo pipefail
9
+ trap 'echo "ERROR: $BASH_SOURCE:$LINENO exited with status $?" >&2' ERR
10
+
11
+ # ─── Colors ──────────────────────────────────────────────────────────────────
12
+ CYAN='\033[38;2;0;212;255m'
13
+ PURPLE='\033[38;2;124;58;237m'
14
+ BLUE='\033[38;2;0;102;255m'
15
+ GREEN='\033[38;2;74;222;128m'
16
+ YELLOW='\033[38;2;250;204;21m'
17
+ RED='\033[38;2;248;113;113m'
18
+ DIM='\033[2m'
19
+ BOLD='\033[1m'
20
+ RESET='\033[0m'
21
+
22
+ # ─── Cross-platform compatibility ──────────────────────────────────────────
23
+ _COMPAT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/lib/compat.sh"
24
+ # shellcheck source=lib/compat.sh
25
+ [[ -f "$_COMPAT" ]] && source "$_COMPAT"
26
+
27
+ # ─── Helpers ─────────────────────────────────────────────────────────────────
28
+ info() { echo -e "${CYAN}${BOLD}▸${RESET} $*"; }
29
+ success() { echo -e "${GREEN}${BOLD}✓${RESET} $*"; }
30
+ warn() { echo -e "${YELLOW}${BOLD}⚠${RESET} $*"; }
31
+ error() { echo -e "${RED}${BOLD}✗${RESET} $*"; }
32
+
33
+ PASS=0
34
+ WARN=0
35
+ FAIL=0
36
+
37
+ check_pass() { success "$*"; PASS=$((PASS + 1)); }
38
+ check_warn() { warn "$*"; WARN=$((WARN + 1)); }
39
+ check_fail() { error "$*"; FAIL=$((FAIL + 1)); }
40
+
41
+ # ─── Header ─────────────────────────────────────────────────────────────────
42
+ echo ""
43
+ echo -e "${CYAN}${BOLD} Shipwright — Doctor${RESET}"
44
+ echo -e "${DIM} $(date '+%Y-%m-%d %H:%M:%S')${RESET}"
45
+ echo -e "${DIM} ══════════════════════════════════════════${RESET}"
46
+ echo ""
47
+
48
+ # ═════════════════════════════════════════════════════════════════════════════
49
+ # 1. Prerequisites
50
+ # ═════════════════════════════════════════════════════════════════════════════
51
+ echo -e "${PURPLE}${BOLD} PREREQUISITES${RESET}"
52
+ echo -e "${DIM} ──────────────────────────────────────────${RESET}"
53
+
54
+ # tmux
55
+ if command -v tmux &>/dev/null; then
56
+ TMUX_VERSION="$(tmux -V | grep -oE '[0-9]+\.[0-9a-z]+')"
57
+ TMUX_MAJOR="$(echo "$TMUX_VERSION" | cut -d. -f1)"
58
+ TMUX_MINOR="$(echo "$TMUX_VERSION" | cut -d. -f2 | tr -dc '0-9')"
59
+ if [[ "$TMUX_MAJOR" -ge 3 && "$TMUX_MINOR" -ge 3 ]] || [[ "$TMUX_MAJOR" -ge 4 ]]; then
60
+ check_pass "tmux ${TMUX_VERSION} (all features: passthrough, popups, extended-keys)"
61
+ elif [[ "$TMUX_MAJOR" -ge 3 && "$TMUX_MINOR" -ge 2 ]]; then
62
+ check_warn "tmux ${TMUX_VERSION} — 3.3+ recommended for allow-passthrough"
63
+ else
64
+ check_warn "tmux ${TMUX_VERSION} — 3.2+ required, 3.3+ recommended"
65
+ fi
66
+ else
67
+ check_fail "tmux not installed"
68
+ echo -e " ${DIM}brew install tmux (macOS)${RESET}"
69
+ echo -e " ${DIM}sudo apt install tmux (Ubuntu/Debian)${RESET}"
70
+ fi
71
+
72
+ # TPM (Tmux Plugin Manager)
73
+ if [[ -d "$HOME/.tmux/plugins/tpm" ]]; then
74
+ check_pass "TPM installed"
75
+ else
76
+ check_warn "TPM not installed — run: shipwright tmux install"
77
+ fi
78
+
79
+ # jq
80
+ if command -v jq &>/dev/null; then
81
+ check_pass "jq $(jq --version 2>&1 | tr -d 'jq-')"
82
+ else
83
+ check_fail "jq not installed — required for template parsing"
84
+ echo -e " ${DIM}brew install jq${RESET} (macOS)"
85
+ echo -e " ${DIM}sudo apt install jq${RESET} (Ubuntu/Debian)"
86
+ fi
87
+
88
+ # Claude Code CLI
89
+ if command -v claude &>/dev/null; then
90
+ check_pass "Claude Code CLI found"
91
+ else
92
+ check_fail "Claude Code CLI not found"
93
+ echo -e " ${DIM}npm install -g @anthropic-ai/claude-code${RESET}"
94
+ fi
95
+
96
+ # Node.js
97
+ if command -v node &>/dev/null; then
98
+ NODE_VERSION="$(node -v | tr -d 'v')"
99
+ NODE_MAJOR="$(echo "$NODE_VERSION" | cut -d. -f1)"
100
+ if [[ "$NODE_MAJOR" -ge 20 ]]; then
101
+ check_pass "Node.js ${NODE_VERSION}"
102
+ else
103
+ check_warn "Node.js ${NODE_VERSION} — 20+ recommended"
104
+ fi
105
+ else
106
+ check_fail "Node.js not found"
107
+ fi
108
+
109
+ # Git
110
+ if command -v git &>/dev/null; then
111
+ check_pass "git $(git --version | grep -oE '[0-9]+\.[0-9]+\.[0-9]+')"
112
+ else
113
+ check_fail "git not found"
114
+ fi
115
+
116
+ # Bash version
117
+ BASH_MAJOR="${BASH_VERSINFO[0]:-0}"
118
+ BASH_MINOR="${BASH_VERSINFO[1]:-0}"
119
+ if [[ "$BASH_MAJOR" -ge 5 ]]; then
120
+ check_pass "bash ${BASH_VERSION}"
121
+ elif [[ "$BASH_MAJOR" -ge 4 ]]; then
122
+ check_pass "bash ${BASH_VERSION}"
123
+ else
124
+ check_warn "bash ${BASH_VERSION} — 4.0+ required for associative arrays"
125
+ echo -e " ${DIM}brew install bash (macOS ships 3.2)${RESET}"
126
+ fi
127
+
128
+ # ═════════════════════════════════════════════════════════════════════════════
129
+ # 2. Installed Files
130
+ # ═════════════════════════════════════════════════════════════════════════════
131
+ echo ""
132
+ echo -e "${PURPLE}${BOLD} INSTALLED FILES${RESET}"
133
+ echo -e "${DIM} ──────────────────────────────────────────${RESET}"
134
+
135
+ # tmux overlay
136
+ if [[ -f "$HOME/.tmux/shipwright-overlay.conf" ]]; then
137
+ check_pass "Overlay: ~/.tmux/shipwright-overlay.conf"
138
+ else
139
+ check_fail "Overlay not found: ~/.tmux/shipwright-overlay.conf"
140
+ echo -e " ${DIM}Re-run install.sh to install it${RESET}"
141
+ fi
142
+
143
+ # Overlay sourced in tmux.conf
144
+ if [[ -f "$HOME/.tmux.conf" ]]; then
145
+ if grep -q "shipwright-overlay" "$HOME/.tmux.conf" 2>/dev/null; then
146
+ check_pass "Overlay sourced in ~/.tmux.conf"
147
+ else
148
+ check_warn "Overlay not sourced in ~/.tmux.conf"
149
+ echo -e " ${DIM}Add: source-file -q ~/.tmux/shipwright-overlay.conf${RESET}"
150
+ fi
151
+ else
152
+ check_warn "No ~/.tmux.conf found"
153
+ fi
154
+
155
+ # Claude settings
156
+ if [[ -f "$HOME/.claude/settings.json" ]]; then
157
+ check_pass "Settings: ~/.claude/settings.json"
158
+ else
159
+ check_warn "No ~/.claude/settings.json"
160
+ echo -e " ${DIM}Copy from settings.json.template${RESET}"
161
+ fi
162
+
163
+ # Hooks directory
164
+ HOOKS_DIR="$HOME/.claude/hooks"
165
+ if [[ -d "$HOOKS_DIR" ]]; then
166
+ hook_count=0
167
+ non_exec=0
168
+ while IFS= read -r hook; do
169
+ [[ -z "$hook" ]] && continue
170
+ hook_count=$((hook_count + 1))
171
+ if [[ ! -x "$hook" ]]; then
172
+ non_exec=$((non_exec + 1))
173
+ fi
174
+ done < <(find "$HOOKS_DIR" -maxdepth 1 -name '*.sh' -type f 2>/dev/null)
175
+
176
+ if [[ $hook_count -gt 0 && $non_exec -eq 0 ]]; then
177
+ check_pass "Hooks: ${hook_count} scripts, all executable"
178
+ elif [[ $hook_count -gt 0 && $non_exec -gt 0 ]]; then
179
+ check_warn "Hooks: ${non_exec}/${hook_count} scripts not executable"
180
+ echo -e " ${DIM}chmod +x ~/.claude/hooks/*.sh${RESET}"
181
+ else
182
+ check_warn "Hooks dir exists but no .sh scripts found"
183
+ fi
184
+ else
185
+ check_warn "No hooks directory at ~/.claude/hooks/"
186
+ fi
187
+
188
+ # Hook wiring validation — check hooks are configured in settings.json
189
+ if [[ -d "$HOOKS_DIR" && -f "$HOME/.claude/settings.json" ]] && jq -e '.' "$HOME/.claude/settings.json" &>/dev/null; then
190
+ wired=0 unwired=0 hook_total_check=0
191
+ # Colon-separated pairs: filename:EventName (Bash 3.2 compatible)
192
+ for pair in \
193
+ "teammate-idle.sh:TeammateIdle" \
194
+ "task-completed.sh:TaskCompleted" \
195
+ "notify-idle.sh:Notification" \
196
+ "pre-compact-save.sh:PreCompact" \
197
+ "session-start.sh:SessionStart"; do
198
+ hfile="" hevent=""
199
+ IFS=':' read -r hfile hevent <<< "$pair"
200
+ # Only check hooks that are actually installed
201
+ [[ -f "$HOOKS_DIR/$hfile" ]] || continue
202
+ hook_total_check=$((hook_total_check + 1))
203
+ if jq -e ".hooks.${hevent}" "$HOME/.claude/settings.json" &>/dev/null; then
204
+ wired=$((wired + 1))
205
+ else
206
+ unwired=$((unwired + 1))
207
+ check_warn "Hook ${hfile} not wired to ${hevent} event in settings.json"
208
+ fi
209
+ done
210
+ if [[ $hook_total_check -gt 0 && $unwired -eq 0 ]]; then
211
+ check_pass "Hooks wired in settings.json: ${wired}/${hook_total_check}"
212
+ elif [[ $unwired -gt 0 ]]; then
213
+ echo -e " ${DIM}Run: shipwright init to wire hooks${RESET}"
214
+ fi
215
+ fi
216
+
217
+ # ═════════════════════════════════════════════════════════════════════════════
218
+ # 3. Agent Teams
219
+ # ═════════════════════════════════════════════════════════════════════════════
220
+ echo ""
221
+ echo -e "${PURPLE}${BOLD} AGENT TEAMS${RESET}"
222
+ echo -e "${DIM} ──────────────────────────────────────────${RESET}"
223
+
224
+ # Agent teams env var in settings.json
225
+ SETTINGS_FILE="$HOME/.claude/settings.json"
226
+ if [[ -f "$SETTINGS_FILE" ]]; then
227
+ if grep -q 'CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS' "$SETTINGS_FILE" 2>/dev/null; then
228
+ check_pass "Agent teams enabled in settings.json"
229
+ else
230
+ check_fail "Agent teams NOT enabled in settings.json"
231
+ echo -e " ${DIM}Run: shipwright init${RESET}"
232
+ echo -e " ${DIM}Or add to ~/.claude/settings.json:${RESET}"
233
+ echo -e " ${DIM}\"env\": { \"CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS\": \"1\" }${RESET}"
234
+ fi
235
+ else
236
+ check_fail "No ~/.claude/settings.json — agent teams not configured"
237
+ echo -e " ${DIM}Run: shipwright init${RESET}"
238
+ fi
239
+
240
+ # CLAUDE.md with Shipwright instructions
241
+ GLOBAL_CLAUDE_MD="$HOME/.claude/CLAUDE.md"
242
+ if [[ -f "$GLOBAL_CLAUDE_MD" ]]; then
243
+ if grep -q "Shipwright" "$GLOBAL_CLAUDE_MD" 2>/dev/null; then
244
+ check_pass "CLAUDE.md contains Shipwright instructions"
245
+ else
246
+ check_warn "CLAUDE.md exists but missing Shipwright instructions"
247
+ echo -e " ${DIM}Run: shipwright init${RESET}"
248
+ fi
249
+ else
250
+ check_warn "No ~/.claude/CLAUDE.md — agents won't know Shipwright commands"
251
+ echo -e " ${DIM}Run: shipwright init${RESET}"
252
+ fi
253
+
254
+ # Team templates
255
+ TEMPLATES_DIR="$HOME/.shipwright/templates"
256
+ if [[ -d "$TEMPLATES_DIR" ]]; then
257
+ tpl_count=0
258
+ while IFS= read -r f; do
259
+ [[ -n "$f" ]] && tpl_count=$((tpl_count + 1))
260
+ done < <(find "$TEMPLATES_DIR" -maxdepth 1 -name '*.json' -type f 2>/dev/null)
261
+ if [[ $tpl_count -gt 0 ]]; then
262
+ check_pass "Team templates: ${tpl_count} installed"
263
+ else
264
+ check_warn "Template dir exists but no .json files found"
265
+ fi
266
+ else
267
+ check_warn "No team templates at ~/.shipwright/templates/"
268
+ echo -e " ${DIM}Run: shipwright init${RESET}"
269
+ fi
270
+
271
+ # Pipeline templates
272
+ PIPELINES_DIR="$HOME/.shipwright/pipelines"
273
+ if [[ -d "$PIPELINES_DIR" ]]; then
274
+ pip_count=0
275
+ while IFS= read -r f; do
276
+ [[ -n "$f" ]] && pip_count=$((pip_count + 1))
277
+ done < <(find "$PIPELINES_DIR" -maxdepth 1 -name '*.json' -type f 2>/dev/null)
278
+ if [[ $pip_count -gt 0 ]]; then
279
+ check_pass "Pipeline templates: ${pip_count} installed"
280
+ else
281
+ check_warn "Pipeline dir exists but no .json files found"
282
+ fi
283
+ else
284
+ check_warn "No pipeline templates at ~/.shipwright/pipelines/"
285
+ echo -e " ${DIM}Run: shipwright init${RESET}"
286
+ fi
287
+
288
+ # GitHub CLI
289
+ if command -v gh &>/dev/null; then
290
+ if gh auth status &>/dev/null; then
291
+ GH_USER="$(gh api user -q .login 2>/dev/null || echo "authenticated")"
292
+ check_pass "GitHub CLI: ${GH_USER}"
293
+ else
294
+ check_warn "GitHub CLI installed but not authenticated"
295
+ echo -e " ${DIM}gh auth login${RESET}"
296
+ fi
297
+ else
298
+ check_warn "GitHub CLI (gh) not installed — daemon/pipeline need it for PRs and issues"
299
+ echo -e " ${DIM}brew install gh${RESET} (macOS)"
300
+ echo -e " ${DIM}sudo apt install gh${RESET} (Ubuntu/Debian)"
301
+ fi
302
+
303
+ # ═════════════════════════════════════════════════════════════════════════════
304
+ # 4. PATH & CLI
305
+ # ═════════════════════════════════════════════════════════════════════════════
306
+ echo ""
307
+ echo -e "${PURPLE}${BOLD} PATH & CLI${RESET}"
308
+ echo -e "${DIM} ──────────────────────────────────────────${RESET}"
309
+
310
+ BIN_DIR="$HOME/.local/bin"
311
+
312
+ if echo "$PATH" | tr ':' '\n' | grep -q "$BIN_DIR"; then
313
+ check_pass "${BIN_DIR} is in PATH"
314
+ else
315
+ check_warn "${BIN_DIR} is NOT in PATH"
316
+ echo -e " ${DIM}Add to ~/.zshrc or ~/.bashrc:${RESET}"
317
+ echo -e " ${DIM}export PATH=\"\$HOME/.local/bin:\$PATH\"${RESET}"
318
+ fi
319
+
320
+ # Check sw subcommands are installed alongside the router
321
+ if command -v sw &>/dev/null; then
322
+ SW_DIR="$(dirname "$(command -v sw)")"
323
+ check_pass "shipwright router found at ${SW_DIR}/sw"
324
+
325
+ missing_subs=()
326
+ for sub in sw-session.sh sw-status.sh sw-cleanup.sh; do
327
+ if [[ ! -x "${SW_DIR}/${sub}" ]]; then
328
+ missing_subs+=("$sub")
329
+ fi
330
+ done
331
+
332
+ if [[ ${#missing_subs[@]} -eq 0 ]]; then
333
+ check_pass "All core subcommands installed"
334
+ else
335
+ check_warn "Missing subcommands: ${missing_subs[*]}"
336
+ echo -e " ${DIM}Re-run install.sh or shipwright upgrade --apply${RESET}"
337
+ fi
338
+ else
339
+ check_fail "shipwright command not found in PATH"
340
+ echo -e " ${DIM}Re-run install.sh to install the CLI${RESET}"
341
+ fi
342
+
343
+ # ═════════════════════════════════════════════════════════════════════════════
344
+ # 5. Pane Display
345
+ # ═════════════════════════════════════════════════════════════════════════════
346
+ echo ""
347
+ echo -e "${PURPLE}${BOLD} PANE DISPLAY${RESET}"
348
+ echo -e "${DIM} ──────────────────────────────────────────${RESET}"
349
+
350
+ # Check overlay file exists
351
+ if [[ -f "$HOME/.tmux/shipwright-overlay.conf" ]]; then
352
+ # Check for set-hook color enforcement
353
+ if grep -q "set-hook.*after-split-window" "$HOME/.tmux/shipwright-overlay.conf" 2>/dev/null; then
354
+ check_pass "Overlay has color hooks (set-hook)"
355
+ else
356
+ check_warn "Overlay missing color hooks — new panes may flash white"
357
+ echo -e " ${DIM}Run: shipwright upgrade --apply or shipwright init${RESET}"
358
+ fi
359
+ else
360
+ check_fail "Overlay not found — pane display features unavailable"
361
+ fi
362
+
363
+ # Check if set-hook commands are active in tmux
364
+ if [[ -n "${TMUX:-}" ]]; then
365
+ if tmux show-hooks -g 2>/dev/null | grep -q "after-split-window"; then
366
+ check_pass "set-hook commands active in tmux"
367
+ else
368
+ check_warn "set-hook commands not active — reload config: prefix + r"
369
+ fi
370
+
371
+ # Check default-terminal
372
+ TMUX_TERM="$(tmux show-option -gv default-terminal 2>/dev/null || echo "unknown")"
373
+ if [[ "$TMUX_TERM" == *"256color"* ]]; then
374
+ check_pass "default-terminal: $TMUX_TERM"
375
+ else
376
+ check_warn "default-terminal: $TMUX_TERM — 256color variant recommended"
377
+ echo -e " ${DIM}set -g default-terminal 'tmux-256color'${RESET}"
378
+ fi
379
+
380
+ # Check pane border includes cyan accent
381
+ BORDER_FMT="$(tmux show-option -gv pane-border-format 2>/dev/null || echo "")"
382
+ if echo "$BORDER_FMT" | grep -q "#00d4ff"; then
383
+ check_pass "Pane border format includes cyan accent"
384
+ else
385
+ check_warn "Pane border format missing cyan accent — overlay may not be loaded"
386
+ fi
387
+ # Check Claude Code compatibility settings
388
+ PASSTHROUGH="$(tmux show-option -gv allow-passthrough 2>/dev/null || echo "off")"
389
+ if [[ "$PASSTHROUGH" == "on" ]]; then
390
+ check_pass "allow-passthrough: on (DEC 2026 synchronized output)"
391
+ else
392
+ check_warn "allow-passthrough: ${PASSTHROUGH} — Claude Code may flicker"
393
+ echo -e " ${DIM}Fix: shipwright tmux fix${RESET}"
394
+ fi
395
+
396
+ EXTKEYS="$(tmux show-option -gv extended-keys 2>/dev/null || echo "off")"
397
+ if [[ "$EXTKEYS" == "on" ]]; then
398
+ check_pass "extended-keys: on"
399
+ else
400
+ check_warn "extended-keys: ${EXTKEYS} — some TUI key combos may not work"
401
+ fi
402
+
403
+ HIST_LIMIT="$(tmux show-option -gv history-limit 2>/dev/null || echo "2000")"
404
+ if [[ "$HIST_LIMIT" -ge 100000 ]]; then
405
+ check_pass "history-limit: ${HIST_LIMIT}"
406
+ else
407
+ check_warn "history-limit: ${HIST_LIMIT} — 250000+ recommended for Claude Code"
408
+ echo -e " ${DIM}Fix: shipwright tmux fix${RESET}"
409
+ fi
410
+ else
411
+ info "Not in tmux session — skipping runtime display checks"
412
+ fi
413
+
414
+ # ═════════════════════════════════════════════════════════════════════════════
415
+ # 6. Orphaned Sessions
416
+ # ═════════════════════════════════════════════════════════════════════════════
417
+ echo ""
418
+ echo -e "${PURPLE}${BOLD} ORPHAN CHECK${RESET}"
419
+ echo -e "${DIM} ──────────────────────────────────────────${RESET}"
420
+
421
+ orphaned_teams=0
422
+ TEAMS_DIR="$HOME/.claude/teams"
423
+ if [[ -d "$TEAMS_DIR" ]]; then
424
+ while IFS= read -r team_dir; do
425
+ [[ -z "$team_dir" ]] && continue
426
+ team_name="$(basename "$team_dir")"
427
+ config_file="${team_dir}/config.json"
428
+ if [[ ! -f "$config_file" ]]; then
429
+ orphaned_teams=$((orphaned_teams + 1))
430
+ check_warn "Orphaned team dir: ${team_name} (no config.json)"
431
+ fi
432
+ done < <(find "$TEAMS_DIR" -mindepth 1 -maxdepth 1 -type d 2>/dev/null)
433
+ fi
434
+
435
+ if [[ $orphaned_teams -eq 0 ]]; then
436
+ check_pass "No orphaned team sessions"
437
+ fi
438
+
439
+ # ═════════════════════════════════════════════════════════════════════════════
440
+ # 7. Environment & Resources
441
+ # ═════════════════════════════════════════════════════════════════════════════
442
+ echo ""
443
+ echo -e "${PURPLE}${BOLD} ENVIRONMENT${RESET}"
444
+ echo -e "${DIM} ──────────────────────────────────────────${RESET}"
445
+
446
+ # Expected directories
447
+ EXPECTED_DIRS=(
448
+ "$HOME/.claude"
449
+ "$HOME/.claude/hooks"
450
+ "$HOME/.shipwright"
451
+ "$HOME/.shipwright"
452
+ )
453
+ missing_dirs=0
454
+ for dir in "${EXPECTED_DIRS[@]}"; do
455
+ if [[ -d "$dir" ]]; then
456
+ check_pass "Directory: ${dir/#$HOME/\~}"
457
+ else
458
+ check_warn "Missing directory: ${dir/#$HOME/\~}"
459
+ echo -e " ${DIM}mkdir -p \"$dir\"${RESET}"
460
+ missing_dirs=$((missing_dirs + 1))
461
+ fi
462
+ done
463
+
464
+ # JSON validation for templates
465
+ if command -v jq &>/dev/null; then
466
+ json_errors=0
467
+ json_total=0
468
+ for tpl_dir in "$HOME/.shipwright/templates" "$HOME/.shipwright/pipelines"; do
469
+ if [[ -d "$tpl_dir" ]]; then
470
+ while IFS= read -r json_file; do
471
+ [[ -z "$json_file" ]] && continue
472
+ json_total=$((json_total + 1))
473
+ if ! jq -e . "$json_file" &>/dev/null; then
474
+ check_fail "Invalid JSON: ${json_file/#$HOME/\~}"
475
+ json_errors=$((json_errors + 1))
476
+ fi
477
+ done < <(find "$tpl_dir" -maxdepth 1 -name '*.json' -type f 2>/dev/null)
478
+ fi
479
+ done
480
+ if [[ $json_total -gt 0 && $json_errors -eq 0 ]]; then
481
+ check_pass "Template JSON: ${json_total} files valid"
482
+ elif [[ $json_total -eq 0 ]]; then
483
+ check_warn "No template JSON files found to validate"
484
+ fi
485
+ fi
486
+
487
+ # Terminal 256-color support
488
+ TERM_VAR="${TERM:-}"
489
+ if [[ "$TERM_VAR" == *"256color"* || "$TERM_VAR" == "xterm-kitty" || "$TERM_VAR" == "tmux-256color" ]]; then
490
+ check_pass "TERM=$TERM_VAR (256 colors)"
491
+ elif [[ -z "$TERM_VAR" ]]; then
492
+ check_warn "TERM not set — colors may not display correctly"
493
+ else
494
+ check_warn "TERM=$TERM_VAR — 256color variant recommended for full theme support"
495
+ echo -e " ${DIM}export TERM=xterm-256color${RESET}"
496
+ fi
497
+
498
+ # Disk space check (warn if < 1GB free)
499
+ if [[ "$(uname)" == "Darwin" ]]; then
500
+ FREE_GB="$(df -g "$HOME" 2>/dev/null | awk 'NR==2{print $4}')" || FREE_GB=""
501
+ else
502
+ # Linux: df -BG gives output in GB
503
+ FREE_GB="$(df -BG "$HOME" 2>/dev/null | awk 'NR==2{print $4}' | tr -d 'G')" || FREE_GB=""
504
+ fi
505
+ if [[ -n "$FREE_GB" && "$FREE_GB" =~ ^[0-9]+$ ]]; then
506
+ if [[ "$FREE_GB" -ge 5 ]]; then
507
+ check_pass "Disk space: ${FREE_GB}GB free"
508
+ elif [[ "$FREE_GB" -ge 1 ]]; then
509
+ check_warn "Disk space: ${FREE_GB}GB free — getting low"
510
+ else
511
+ check_fail "Disk space: ${FREE_GB}GB free — less than 1GB available"
512
+ echo -e " ${DIM}Free up disk space to avoid pipeline failures${RESET}"
513
+ fi
514
+ fi
515
+
516
+ # ═════════════════════════════════════════════════════════════════════════════
517
+ # 8. Terminal Compatibility
518
+ # ═════════════════════════════════════════════════════════════════════════════
519
+ echo ""
520
+ echo -e "${PURPLE}${BOLD} TERMINAL${RESET}"
521
+ echo -e "${DIM} ──────────────────────────────────────────${RESET}"
522
+
523
+ TERM_PROGRAM="${TERM_PROGRAM:-unknown}"
524
+
525
+ case "$TERM_PROGRAM" in
526
+ iTerm.app|iTerm2)
527
+ check_pass "iTerm2 — full support (true color, SGR mouse, focus events)"
528
+ # Verify mouse reporting is actually enabled in iTerm2 profile
529
+ ITERM_MOUSE="$(defaults read com.googlecode.iterm2 "New Bookmarks" 2>/dev/null | grep '"Mouse Reporting"' | head -1 | grep -oE '[0-9]+' || echo "unknown")"
530
+ if [[ "$ITERM_MOUSE" == "0" ]]; then
531
+ check_fail "iTerm2 mouse reporting is DISABLED — tmux cannot receive mouse clicks"
532
+ echo -e " ${DIM}Fix: iTerm2 → Preferences → Profiles → Terminal → enable 'Report mouse clicks & drags'${RESET}"
533
+ echo -e " ${DIM}Or run: ${CYAN}/usr/libexec/PlistBuddy -c \"Set ':New Bookmarks:0:Mouse Reporting' 1\" ~/Library/Preferences/com.googlecode.iterm2.plist${RESET}"
534
+ elif [[ "$ITERM_MOUSE" == "1" ]]; then
535
+ check_pass "iTerm2 mouse reporting: enabled"
536
+ fi
537
+ ;;
538
+ Apple_Terminal)
539
+ check_warn "Terminal.app — limited support"
540
+ echo -e " ${DIM}No true color (256 colors only), no SGR extended mouse.${RESET}"
541
+ echo -e " ${DIM}Mouse clicking works, but wide terminals (>223 cols) may mistrack.${RESET}"
542
+ echo -e " ${DIM}Recommended: use iTerm2 or WezTerm for best experience.${RESET}"
543
+ ;;
544
+ WezTerm)
545
+ check_pass "WezTerm — full support (true color, SGR mouse, focus events)"
546
+ ;;
547
+ tmux)
548
+ # Detect parent terminal when nested inside tmux
549
+ PARENT_TERM="${LC_TERMINAL:-unknown}"
550
+ check_pass "Running inside tmux — parent terminal: ${PARENT_TERM}"
551
+ # Check iTerm2 mouse reporting even when nested inside tmux
552
+ if [[ "$PARENT_TERM" == *iTerm* ]]; then
553
+ ITERM_MOUSE="$(defaults read com.googlecode.iterm2 "New Bookmarks" 2>/dev/null | grep '"Mouse Reporting"' | head -1 | grep -oE '[0-9]+' || echo "unknown")"
554
+ if [[ "$ITERM_MOUSE" == "0" ]]; then
555
+ check_fail "iTerm2 mouse reporting is DISABLED — tmux cannot receive mouse clicks"
556
+ echo -e " ${DIM}Fix: iTerm2 → Preferences → Profiles → Terminal → enable 'Report mouse clicks & drags'${RESET}"
557
+ echo -e " ${DIM}Or run: ${CYAN}shipwright init${RESET} (auto-fixes this)${RESET}"
558
+ elif [[ "$ITERM_MOUSE" == "1" ]]; then
559
+ check_pass "iTerm2 mouse reporting: enabled"
560
+ fi
561
+ fi
562
+ ;;
563
+ vscode)
564
+ check_warn "VS Code integrated terminal"
565
+ echo -e " ${DIM}Some pane border features may not render correctly.${RESET}"
566
+ echo -e " ${DIM}Consider running tmux in an external terminal.${RESET}"
567
+ ;;
568
+ Ghostty)
569
+ check_pass "Ghostty — full support (true color, SGR mouse)"
570
+ ;;
571
+ Alacritty)
572
+ check_pass "Alacritty — full support (true color, SGR mouse)"
573
+ ;;
574
+ kitty)
575
+ check_pass "kitty — full support (true color, extended keyboard)"
576
+ ;;
577
+ *)
578
+ info "Terminal: ${TERM_PROGRAM}"
579
+ ;;
580
+ esac
581
+
582
+ # Check mouse window clicking (tmux 3.4+ changed the default)
583
+ if command -v tmux &>/dev/null && [[ -n "${TMUX:-}" ]]; then
584
+ MOUSE_BIND="$(tmux list-keys 2>/dev/null | grep 'MouseDown1Status' | head -1 || true)"
585
+ if echo "$MOUSE_BIND" | grep -q 'select-window'; then
586
+ check_pass "Mouse window click: select-window (correct)"
587
+ elif echo "$MOUSE_BIND" | grep -q 'switch-client'; then
588
+ check_fail "Mouse window click: switch-client (broken — clicking windows won't work)"
589
+ echo -e " ${DIM}Fix: add to tmux.conf: bind -T root MouseDown1Status select-window -t =${RESET}"
590
+ echo -e " ${DIM}Or run: shipwright init${RESET}"
591
+ fi
592
+ fi
593
+
594
+ # ═════════════════════════════════════════════════════════════════════════════
595
+ # 9. Issue Tracker
596
+ # ═════════════════════════════════════════════════════════════════════════════
597
+ echo ""
598
+ echo -e "${PURPLE}${BOLD} ISSUE TRACKER${RESET}"
599
+ echo -e "${DIM} ──────────────────────────────────────────${RESET}"
600
+
601
+ TRACKER_CONFIG="${HOME}/.shipwright/tracker-config.json"
602
+ if [[ -f "$TRACKER_CONFIG" ]]; then
603
+ TRACKER_PROVIDER=$(jq -r '.provider // "none"' "$TRACKER_CONFIG" 2>/dev/null || echo "none")
604
+ if [[ "$TRACKER_PROVIDER" != "none" && -n "$TRACKER_PROVIDER" ]]; then
605
+ check_pass "Tracker provider: ${TRACKER_PROVIDER}"
606
+ # Validate provider-specific config
607
+ case "$TRACKER_PROVIDER" in
608
+ linear)
609
+ LINEAR_KEY=$(jq -r '.linear.api_key // empty' "$TRACKER_CONFIG" 2>/dev/null || true)
610
+ if [[ -n "$LINEAR_KEY" ]]; then
611
+ check_pass "Linear API key: configured"
612
+ else
613
+ check_warn "Linear API key: not set — set via shipwright tracker init or LINEAR_API_KEY env var"
614
+ fi
615
+ ;;
616
+ jira)
617
+ JIRA_URL=$(jq -r '.jira.base_url // empty' "$TRACKER_CONFIG" 2>/dev/null || true)
618
+ JIRA_TOKEN=$(jq -r '.jira.api_token // empty' "$TRACKER_CONFIG" 2>/dev/null || true)
619
+ if [[ -n "$JIRA_URL" && -n "$JIRA_TOKEN" ]]; then
620
+ check_pass "Jira: configured (${JIRA_URL})"
621
+ else
622
+ check_warn "Jira: incomplete config — run shipwright jira init"
623
+ fi
624
+ ;;
625
+ esac
626
+ else
627
+ info " No tracker configured ${DIM}(optional — run shipwright tracker init)${RESET}"
628
+ fi
629
+ else
630
+ info " No tracker configured ${DIM}(optional — run shipwright tracker init)${RESET}"
631
+ fi
632
+
633
+ # ═════════════════════════════════════════════════════════════════════════════
634
+ # 10. Agent Heartbeats & Checkpoints
635
+ # ═════════════════════════════════════════════════════════════════════════════
636
+ echo ""
637
+ echo -e "${PURPLE}${BOLD} HEARTBEATS & CHECKPOINTS${RESET}"
638
+ echo -e "${DIM} ──────────────────────────────────────────${RESET}"
639
+
640
+ HEARTBEAT_DIR="$HOME/.shipwright/heartbeats"
641
+ if [[ -d "$HEARTBEAT_DIR" ]]; then
642
+ check_pass "Heartbeat directory: ${HEARTBEAT_DIR/#$HOME/\~}"
643
+ # Check permissions
644
+ if [[ -w "$HEARTBEAT_DIR" ]]; then
645
+ check_pass "Heartbeat directory: writable"
646
+ else
647
+ check_fail "Heartbeat directory: not writable"
648
+ fi
649
+
650
+ # Count active/stale heartbeats
651
+ hb_active=0
652
+ hb_stale=0
653
+ for hb_file in "${HEARTBEAT_DIR}"/*.json; do
654
+ [[ -f "$hb_file" ]] || continue
655
+ hb_updated=$(jq -r '.updated_at // ""' "$hb_file" 2>/dev/null || true)
656
+ if [[ -n "$hb_updated" && "$hb_updated" != "null" ]]; then
657
+ hb_epoch=$(TZ=UTC date -j -f "%Y-%m-%dT%H:%M:%SZ" "$hb_updated" +%s 2>/dev/null || echo 0)
658
+ if [[ "$hb_epoch" -gt 0 ]]; then
659
+ now_e=$(date +%s)
660
+ hb_age=$((now_e - hb_epoch))
661
+ if [[ "$hb_age" -ge 120 ]]; then
662
+ hb_stale=$((hb_stale + 1))
663
+ else
664
+ hb_active=$((hb_active + 1))
665
+ fi
666
+ fi
667
+ fi
668
+ done
669
+ if [[ $hb_active -gt 0 ]]; then
670
+ check_pass "Active heartbeats: ${hb_active}"
671
+ fi
672
+ if [[ $hb_stale -gt 0 ]]; then
673
+ check_warn "Stale heartbeats: ${hb_stale} (>120s old)"
674
+ echo -e " ${DIM}Clean up with: shipwright heartbeat clear <job-id>${RESET}"
675
+ fi
676
+ else
677
+ info " No heartbeat directory ${DIM}(created automatically when agents run)${RESET}"
678
+ fi
679
+
680
+ # Checkpoint directory
681
+ CHECKPOINT_DIR=".claude/pipeline-artifacts/checkpoints"
682
+ if [[ -d "$CHECKPOINT_DIR" ]]; then
683
+ cp_count=0
684
+ for cp_file in "${CHECKPOINT_DIR}"/*-checkpoint.json; do
685
+ [[ -f "$cp_file" ]] || continue
686
+ cp_count=$((cp_count + 1))
687
+ done
688
+ if [[ $cp_count -gt 0 ]]; then
689
+ check_pass "Checkpoints: ${cp_count} saved"
690
+ else
691
+ check_pass "Checkpoint directory exists (no checkpoints saved)"
692
+ fi
693
+ else
694
+ info " No checkpoint directory ${DIM}(created on first checkpoint save)${RESET}"
695
+ fi
696
+
697
+ # ═════════════════════════════════════════════════════════════════════════════
698
+ # 11. Remote Machines
699
+ # ═════════════════════════════════════════════════════════════════════════════
700
+ echo ""
701
+ echo -e "${PURPLE}${BOLD} REMOTE MACHINES${RESET}"
702
+ echo -e "${DIM} ──────────────────────────────────────────${RESET}"
703
+
704
+ MACHINES_FILE="$HOME/.shipwright/machines.json"
705
+ if [[ -f "$MACHINES_FILE" ]]; then
706
+ machine_count=$(jq '.machines | length' "$MACHINES_FILE" 2>/dev/null || echo 0)
707
+ if [[ "$machine_count" -gt 0 ]]; then
708
+ check_pass "Registered machines: ${machine_count}"
709
+ # Check SSH connectivity (quick check, 5s timeout per machine)
710
+ if command -v ssh &>/dev/null; then
711
+ while IFS= read -r machine; do
712
+ [[ -z "$machine" ]] && continue
713
+ m_name=$(echo "$machine" | jq -r '.name // ""')
714
+ m_host=$(echo "$machine" | jq -r '.host // ""')
715
+ m_user=$(echo "$machine" | jq -r '.user // ""')
716
+ m_port=$(echo "$machine" | jq -r '.port // 22')
717
+
718
+ if [[ -n "$m_host" ]]; then
719
+ ssh_target="${m_user:+${m_user}@}${m_host}"
720
+ if ssh -o ConnectTimeout=5 -o BatchMode=yes -p "$m_port" "$ssh_target" true 2>/dev/null; then
721
+ check_pass "SSH: ${m_name} (${ssh_target}) reachable"
722
+ else
723
+ check_warn "SSH: ${m_name} (${ssh_target}) unreachable"
724
+ echo -e " ${DIM}Check SSH key and connectivity: ssh -p ${m_port} ${ssh_target}${RESET}"
725
+ fi
726
+ fi
727
+ done < <(jq -c '.machines[]' "$MACHINES_FILE" 2>/dev/null)
728
+ fi
729
+ else
730
+ info " No machines registered ${DIM}(add with: shipwright remote add)${RESET}"
731
+ fi
732
+ else
733
+ info " No remote machines ${DIM}(optional — run shipwright remote add)${RESET}"
734
+ fi
735
+
736
+ # ═════════════════════════════════════════════════════════════════════════════
737
+ # 12. Team Connectivity
738
+ # ═════════════════════════════════════════════════════════════════════════════
739
+ echo ""
740
+ echo -e "${PURPLE}${BOLD} TEAM CONNECTIVITY${RESET}"
741
+ echo -e "${DIM} ──────────────────────────────────────────${RESET}"
742
+
743
+ # Check connect process
744
+ CONNECT_PID_FILE="$HOME/.shipwright/connect.pid"
745
+ if [[ -f "$CONNECT_PID_FILE" ]]; then
746
+ CONNECT_PID=$(cat "$CONNECT_PID_FILE" 2>/dev/null || echo "")
747
+ if [[ -n "$CONNECT_PID" ]]; then
748
+ if kill -0 "$CONNECT_PID" 2>/dev/null; then
749
+ check_pass "Connect process: running (PID ${CONNECT_PID})"
750
+ else
751
+ check_warn "Connect process: PID file exists but process not running"
752
+ echo -e " ${DIM}Clean up with: rm ${CONNECT_PID_FILE}${RESET}"
753
+ fi
754
+ else
755
+ check_warn "Connect PID file exists but is empty"
756
+ fi
757
+ else
758
+ info " Team connect not configured ${DIM}(optional)${RESET}"
759
+ fi
760
+
761
+ # Check team config
762
+ TEAM_CONFIG="$HOME/.shipwright/team-config.json"
763
+ if [[ -f "$TEAM_CONFIG" ]]; then
764
+ if jq -e . "$TEAM_CONFIG" &>/dev/null; then
765
+ check_pass "Team config: valid JSON"
766
+
767
+ # Check dashboard_url field
768
+ DASHBOARD_URL=$(jq -r '.dashboard_url // empty' "$TEAM_CONFIG" 2>/dev/null || true)
769
+ if [[ -n "$DASHBOARD_URL" ]]; then
770
+ check_pass "Dashboard URL: configured"
771
+
772
+ # Try to reach dashboard with 3s timeout
773
+ if command -v curl &>/dev/null; then
774
+ if curl -s -m 3 "${DASHBOARD_URL}/api/health" &>/dev/null; then
775
+ check_pass "Dashboard reachable: ${DASHBOARD_URL}"
776
+ else
777
+ check_warn "Dashboard unreachable: ${DASHBOARD_URL}"
778
+ echo -e " ${DIM}Check if dashboard service is running or URL is correct${RESET}"
779
+ fi
780
+ else
781
+ info " curl not found — skipping dashboard health check"
782
+ fi
783
+ else
784
+ check_warn "Team config: missing dashboard_url field"
785
+ fi
786
+ else
787
+ check_fail "Team config: invalid JSON"
788
+ echo -e " ${DIM}Fix JSON syntax in ${TEAM_CONFIG}${RESET}"
789
+ fi
790
+ else
791
+ info " Team config not found ${DIM}(optional — run shipwright init)${RESET}"
792
+ fi
793
+
794
+ # Check developer registry
795
+ DEVELOPER_REGISTRY="$HOME/.shipwright/developer-registry.json"
796
+ if [[ -f "$DEVELOPER_REGISTRY" ]]; then
797
+ if jq -e . "$DEVELOPER_REGISTRY" &>/dev/null; then
798
+ check_pass "Developer registry: exists and valid"
799
+ else
800
+ check_fail "Developer registry: invalid JSON"
801
+ echo -e " ${DIM}Fix JSON syntax in ${DEVELOPER_REGISTRY}${RESET}"
802
+ fi
803
+ else
804
+ info " Developer registry not found ${DIM}(optional)${RESET}"
805
+ fi
806
+
807
+ # ═════════════════════════════════════════════════════════════════════════════
808
+ # 13. GitHub Integration
809
+ # ═════════════════════════════════════════════════════════════════════════════
810
+ echo ""
811
+ echo -e "${PURPLE}${BOLD} GITHUB INTEGRATION${RESET}"
812
+ echo -e "${DIM} ──────────────────────────────────────────${RESET}"
813
+
814
+ if command -v gh &>/dev/null; then
815
+ if gh auth status &>/dev/null 2>&1; then
816
+ check_pass "gh CLI authenticated"
817
+
818
+ # Check required scopes
819
+ gh_scopes=""
820
+ gh_scopes=$(gh auth status 2>&1 | grep -i "token scopes" || echo "")
821
+ if [[ -n "$gh_scopes" ]]; then
822
+ if echo "$gh_scopes" | grep -qi "repo"; then
823
+ check_pass "Token has 'repo' scope"
824
+ else
825
+ check_warn "'repo' scope not detected — some features may not work"
826
+ echo -e " ${DIM}Fix: gh auth refresh -s repo${RESET}"
827
+ fi
828
+ if echo "$gh_scopes" | grep -qi "read:org"; then
829
+ check_pass "Token has 'read:org' scope"
830
+ else
831
+ check_warn "'read:org' scope not detected — org data may be unavailable"
832
+ echo -e " ${DIM}Fix: gh auth refresh -s read:org${RESET}"
833
+ fi
834
+ fi
835
+
836
+ # Check GraphQL endpoint
837
+ if gh api graphql -f query='{viewer{login}}' &>/dev/null 2>&1; then
838
+ check_pass "GraphQL API accessible"
839
+ else
840
+ check_warn "GraphQL API not accessible — intelligence enrichment will use fallbacks"
841
+ fi
842
+
843
+ # Check code scanning API
844
+ dr_repo_owner=""
845
+ dr_repo_name=""
846
+ dr_repo_owner=$(git remote get-url origin 2>/dev/null | sed -E 's#.*[:/]([^/]+)/[^/]+(\.git)?$#\1#' || echo "")
847
+ dr_repo_name=$(git remote get-url origin 2>/dev/null | sed -E 's#.*/([^/]+)(\.git)?$#\1#' || echo "")
848
+ if [[ -n "$dr_repo_owner" && -n "$dr_repo_name" ]]; then
849
+ if gh api "repos/$dr_repo_owner/$dr_repo_name/code-scanning/alerts?per_page=1" &>/dev/null 2>&1; then
850
+ check_pass "Code scanning API accessible"
851
+ else
852
+ info " Code scanning API not available ${DIM}(may need GitHub Advanced Security)${RESET}"
853
+ fi
854
+ fi
855
+
856
+ # Check CODEOWNERS file
857
+ if [[ -f "CODEOWNERS" || -f ".github/CODEOWNERS" || -f "docs/CODEOWNERS" ]]; then
858
+ check_pass "CODEOWNERS file found"
859
+ else
860
+ info " No CODEOWNERS file ${DIM}(reviewer selection will use contributor data)${RESET}"
861
+ fi
862
+
863
+ # Check GitHub modules installed
864
+ _DOCTOR_SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
865
+ if [[ -f "$_DOCTOR_SCRIPT_DIR/sw-github-graphql.sh" ]]; then
866
+ check_pass "GitHub GraphQL module installed"
867
+ else
868
+ info " GitHub GraphQL module not found ${DIM}(scripts/sw-github-graphql.sh)${RESET}"
869
+ fi
870
+ if [[ -f "$_DOCTOR_SCRIPT_DIR/sw-github-checks.sh" ]]; then
871
+ check_pass "GitHub Checks module installed"
872
+ else
873
+ info " GitHub Checks module not found ${DIM}(scripts/sw-github-checks.sh)${RESET}"
874
+ fi
875
+ if [[ -f "$_DOCTOR_SCRIPT_DIR/sw-github-deploy.sh" ]]; then
876
+ check_pass "GitHub Deploy module installed"
877
+ else
878
+ info " GitHub Deploy module not found ${DIM}(scripts/sw-github-deploy.sh)${RESET}"
879
+ fi
880
+ else
881
+ check_warn "gh CLI not authenticated — run: ${DIM}gh auth login${RESET}"
882
+ fi
883
+ else
884
+ check_warn "gh CLI not installed — GitHub integration disabled"
885
+ echo -e " ${DIM}Install: brew install gh (macOS) or see https://cli.github.com${RESET}"
886
+ fi
887
+
888
+ # ═════════════════════════════════════════════════════════════════════════════
889
+ # Summary
890
+ # ═════════════════════════════════════════════════════════════════════════════
891
+ echo ""
892
+ echo -e "${DIM} ══════════════════════════════════════════${RESET}"
893
+ echo ""
894
+
895
+ TOTAL=$((PASS + WARN + FAIL))
896
+
897
+ echo -e " ${GREEN}${BOLD}${PASS}${RESET} passed ${YELLOW}${BOLD}${WARN}${RESET} warnings ${RED}${BOLD}${FAIL}${RESET} failed ${DIM}(${TOTAL} checks)${RESET}"
898
+ echo ""
899
+
900
+ if [[ $FAIL -gt 0 ]]; then
901
+ error "Some checks failed. Fix the issues above and re-run ${CYAN}shipwright doctor${RESET}"
902
+ elif [[ $WARN -gt 0 ]]; then
903
+ warn "Setup mostly OK, but there are warnings above"
904
+ else
905
+ success "Everything looks good!"
906
+ fi
907
+ echo ""