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,712 @@
1
+ #!/usr/bin/env bash
2
+ # ╔═══════════════════════════════════════════════════════════════════════════╗
3
+ # ║ shipwright standup — Automated Daily Standups for AI Agent Teams ║
4
+ # ║ Gather status, identify blockers, summarize work, deliver reports ║
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
+ now_epoch() { date +%s; }
36
+
37
+ # ─── Constants ──────────────────────────────────────────────────────────────
38
+ STANDUP_DIR="${HOME}/.shipwright/standups"
39
+ EVENTS_FILE="${HOME}/.shipwright/events.jsonl"
40
+ DAEMON_STATE="${HOME}/.shipwright/daemon-state.json"
41
+ HEARTBEATS_DIR="${HOME}/.shipwright/heartbeats"
42
+
43
+ # Seconds in 24 hours
44
+ SECONDS_24H=86400
45
+
46
+ # ─── Ensure directories exist ───────────────────────────────────────────────
47
+ ensure_dirs() {
48
+ mkdir -p "$STANDUP_DIR"
49
+ }
50
+
51
+ # ─── Epoch conversion helper ────────────────────────────────────────────────
52
+ # Convert ISO 8601 timestamp to epoch seconds (works on macOS and Linux)
53
+ iso_to_epoch() {
54
+ local iso="$1"
55
+ if TZ=UTC date -j -f "%Y-%m-%dT%H:%M:%SZ" "$iso" +%s &>/dev/null 2>&1; then
56
+ TZ=UTC date -j -f "%Y-%m-%dT%H:%M:%SZ" "$iso" +%s 2>/dev/null || echo 0
57
+ else
58
+ date -d "$iso" +%s 2>/dev/null || echo 0
59
+ fi
60
+ }
61
+
62
+ # ─── Gather Yesterday's Work ────────────────────────────────────────────────
63
+ # Scan events.jsonl for commits, PRs, tests, reviews in the last 24h
64
+ cmd_yesterday() {
65
+ ensure_dirs
66
+
67
+ local now_epoch
68
+ now_epoch="$(now_epoch)"
69
+ local cutoff=$((now_epoch - SECONDS_24H))
70
+
71
+ local report_file="${STANDUP_DIR}/yesterday-$(date +%Y-%m-%d).txt"
72
+
73
+ {
74
+ echo "╔════════════════════════════════════════════════════════════════════╗"
75
+ echo "║ Yesterday's Work (Last 24 Hours) ║"
76
+ echo "╚════════════════════════════════════════════════════════════════════╝"
77
+ echo ""
78
+
79
+ if [[ ! -f "$EVENTS_FILE" ]]; then
80
+ echo "No events recorded yet."
81
+ echo ""
82
+ return 0
83
+ fi
84
+
85
+ # Group events by job/agent
86
+ local commits=0
87
+ local prs=0
88
+ local tests_run=0
89
+ local tests_passed=0
90
+ local tests_failed=0
91
+ local reviews=0
92
+
93
+ while IFS= read -r line; do
94
+ [[ -z "$line" ]] && continue
95
+
96
+ local ts_epoch
97
+ ts_epoch=$(echo "$line" | jq -r '.ts_epoch // 0' 2>/dev/null || echo 0)
98
+
99
+ if [[ "$ts_epoch" -lt "$cutoff" ]]; then
100
+ continue
101
+ fi
102
+
103
+ local event_type
104
+ event_type=$(echo "$line" | jq -r '.type // ""' 2>/dev/null || true)
105
+
106
+ case "$event_type" in
107
+ pipeline_commit)
108
+ commits=$((commits + 1))
109
+ ;;
110
+ pipeline_pr)
111
+ prs=$((prs + 1))
112
+ ;;
113
+ test_run)
114
+ tests_run=$((tests_run + 1))
115
+ ;;
116
+ test_pass)
117
+ tests_passed=$((tests_passed + 1))
118
+ ;;
119
+ test_fail)
120
+ tests_failed=$((tests_failed + 1))
121
+ ;;
122
+ pr_review)
123
+ reviews=$((reviews + 1))
124
+ ;;
125
+ esac
126
+ done < "$EVENTS_FILE"
127
+
128
+ echo "Commits: ${GREEN}${commits}${RESET}"
129
+ echo "PRs Created: ${GREEN}${prs}${RESET}"
130
+ echo "Tests Run: ${GREEN}${tests_run}${RESET}"
131
+ echo " ├─ Passed: ${GREEN}${tests_passed}${RESET}"
132
+ echo " └─ Failed: ${RED}${tests_failed}${RESET}"
133
+ echo "Reviews: ${GREEN}${reviews}${RESET}"
134
+ echo ""
135
+
136
+ } | tee "$report_file"
137
+
138
+ success "Yesterday's report saved to ${STANDUP_DIR}/yesterday-*.txt"
139
+ }
140
+
141
+ # ─── Gather Today's Plan ────────────────────────────────────────────────────
142
+ # Read daemon state to see queued issues and active pipelines
143
+ cmd_today() {
144
+ ensure_dirs
145
+
146
+ local report_file="${STANDUP_DIR}/today-$(date +%Y-%m-%d).txt"
147
+
148
+ {
149
+ echo "╔════════════════════════════════════════════════════════════════════╗"
150
+ echo "║ Today's Plan (Active & Queued Work) ║"
151
+ echo "╚════════════════════════════════════════════════════════════════════╝"
152
+ echo ""
153
+
154
+ if [[ ! -f "$DAEMON_STATE" ]]; then
155
+ echo "No daemon state available."
156
+ echo ""
157
+ return 0
158
+ fi
159
+
160
+ # Active jobs
161
+ local active_jobs
162
+ active_jobs=$(jq -r '.active_jobs // []' "$DAEMON_STATE" 2>/dev/null | jq length)
163
+
164
+ echo "Active Pipelines: ${CYAN}${active_jobs}${RESET}"
165
+ if [[ "$active_jobs" -gt 0 ]]; then
166
+ jq -r '.active_jobs[] | " • Issue \(.issue_number): \(.title)"' "$DAEMON_STATE" 2>/dev/null | head -5
167
+ if [[ "$active_jobs" -gt 5 ]]; then
168
+ echo " ... and $((active_jobs - 5)) more"
169
+ fi
170
+ fi
171
+ echo ""
172
+
173
+ # Queued jobs
174
+ local queued
175
+ queued=$(jq -r '.queued // []' "$DAEMON_STATE" 2>/dev/null | jq length)
176
+
177
+ echo "Queued Issues: ${YELLOW}${queued}${RESET}"
178
+ if [[ "$queued" -gt 0 ]]; then
179
+ jq -r '.queued[] | " • Issue \(.issue_number): \(.title)"' "$DAEMON_STATE" 2>/dev/null | head -5
180
+ if [[ "$queued" -gt 5 ]]; then
181
+ echo " ... and $((queued - 5)) more"
182
+ fi
183
+ fi
184
+ echo ""
185
+
186
+ } | tee "$report_file"
187
+
188
+ success "Today's plan saved to ${STANDUP_DIR}/today-*.txt"
189
+ }
190
+
191
+ # ─── Detect Blockers ────────────────────────────────────────────────────────
192
+ # Identify stalled pipelines, failed stages, resource constraints
193
+ cmd_blockers() {
194
+ ensure_dirs
195
+
196
+ local report_file="${STANDUP_DIR}/blockers-$(date +%Y-%m-%d).txt"
197
+
198
+ {
199
+ echo "╔════════════════════════════════════════════════════════════════════╗"
200
+ echo "║ Current Blockers ║"
201
+ echo "╚════════════════════════════════════════════════════════════════════╝"
202
+ echo ""
203
+
204
+ local blocker_count=0
205
+
206
+ # Check for stale heartbeats (agents not responding)
207
+ if [[ -d "$HEARTBEATS_DIR" ]]; then
208
+ local now_epoch
209
+ now_epoch="$(now_epoch)"
210
+
211
+ for hb_file in "${HEARTBEATS_DIR}"/*.json; do
212
+ [[ ! -f "$hb_file" ]] && continue
213
+
214
+ local updated_at
215
+ updated_at=$(jq -r '.updated_at' "$hb_file" 2>/dev/null || true)
216
+ [[ -z "$updated_at" || "$updated_at" == "null" ]] && continue
217
+
218
+ local hb_epoch
219
+ hb_epoch="$(iso_to_epoch "$updated_at")"
220
+ local age=$((now_epoch - hb_epoch))
221
+
222
+ # If older than 5 minutes, it's stale
223
+ if [[ "$age" -gt 300 ]]; then
224
+ local job_id
225
+ job_id="$(basename "$hb_file" .json)"
226
+ local stage
227
+ stage=$(jq -r '.stage // "unknown"' "$hb_file" 2>/dev/null || echo "unknown")
228
+
229
+ echo "${RED}✗ STALE AGENT${RESET}: ${job_id} (stage: ${stage}, silent for ${age}s)"
230
+ blocker_count=$((blocker_count + 1))
231
+ fi
232
+ done
233
+ fi
234
+
235
+ # Check for failed pipeline stages in events
236
+ if [[ -f "$EVENTS_FILE" ]]; then
237
+ local failed_stages
238
+ failed_stages=$(grep '"type":"stage_failed"' "$EVENTS_FILE" 2>/dev/null | tail -5 || true)
239
+
240
+ if [[ -n "$failed_stages" ]]; then
241
+ echo ""
242
+ echo "${RED}Failed Pipeline Stages:${RESET}"
243
+ echo "$failed_stages" | jq -r '"\(.type): \(.stage // "unknown") - \(.reason // "")"' 2>/dev/null | while read -r line; do
244
+ echo " • $line"
245
+ blocker_count=$((blocker_count + 1))
246
+ done
247
+ fi
248
+ fi
249
+
250
+ if [[ "$blocker_count" -eq 0 ]]; then
251
+ echo "${GREEN}✓ No blockers detected${RESET}"
252
+ fi
253
+ echo ""
254
+
255
+ } | tee "$report_file"
256
+
257
+ success "Blockers report saved to ${STANDUP_DIR}/blockers-*.txt"
258
+ }
259
+
260
+ # ─── Gather Velocity & Burn-Down Metrics ────────────────────────────────────
261
+ cmd_velocity() {
262
+ ensure_dirs
263
+
264
+ local report_file="${STANDUP_DIR}/velocity-$(date +%Y-%m-%d).txt"
265
+
266
+ {
267
+ echo "╔════════════════════════════════════════════════════════════════════╗"
268
+ echo "║ Sprint Velocity & Burn-Down ║"
269
+ echo "╚════════════════════════════════════════════════════════════════════╝"
270
+ echo ""
271
+
272
+ if [[ ! -f "$DAEMON_STATE" ]]; then
273
+ echo "No daemon state available."
274
+ echo ""
275
+ return 0
276
+ fi
277
+
278
+ # Completed in the last 24h
279
+ local completed_24h=0
280
+ local total_completed=0
281
+
282
+ if [[ -f "$EVENTS_FILE" ]]; then
283
+ local now_epoch
284
+ now_epoch="$(now_epoch)"
285
+ local cutoff=$((now_epoch - SECONDS_24H))
286
+
287
+ while IFS= read -r line; do
288
+ [[ -z "$line" ]] && continue
289
+ local ts_epoch
290
+ ts_epoch=$(echo "$line" | jq -r '.ts_epoch // 0' 2>/dev/null || echo 0)
291
+
292
+ if [[ "$ts_epoch" -ge "$cutoff" ]]; then
293
+ local event_type
294
+ event_type=$(echo "$line" | jq -r '.type // ""' 2>/dev/null || true)
295
+ if [[ "$event_type" == "pipeline_completed" ]]; then
296
+ completed_24h=$((completed_24h + 1))
297
+ fi
298
+ fi
299
+
300
+ local event_type
301
+ event_type=$(echo "$line" | jq -r '.type // ""' 2>/dev/null || true)
302
+ if [[ "$event_type" == "pipeline_completed" ]]; then
303
+ total_completed=$((total_completed + 1))
304
+ fi
305
+ done < "$EVENTS_FILE"
306
+ fi
307
+
308
+ local active_jobs
309
+ active_jobs=$(jq -r '.active_jobs // []' "$DAEMON_STATE" 2>/dev/null | jq length)
310
+ local queued
311
+ queued=$(jq -r '.queued // []' "$DAEMON_STATE" 2>/dev/null | jq length)
312
+
313
+ local total_work=$((active_jobs + queued))
314
+
315
+ echo "Completed (24h): ${GREEN}${completed_24h}${RESET}"
316
+ echo "Total Completed: ${GREEN}${total_completed}${RESET}"
317
+ echo "Active: ${CYAN}${active_jobs}${RESET}"
318
+ echo "Queued: ${YELLOW}${queued}${RESET}"
319
+ echo "Work in Progress: ${BLUE}${total_work}${RESET}"
320
+ echo ""
321
+
322
+ if [[ "$total_work" -gt 0 && "$completed_24h" -gt 0 ]]; then
323
+ local days_remaining=$(((total_work * SECONDS_24H) / (completed_24h * 3600)))
324
+ [[ "$days_remaining" -lt 1 ]] && days_remaining=1
325
+ echo "Estimated Completion: ${CYAN}~${days_remaining} day(s)${RESET}"
326
+ fi
327
+ echo ""
328
+
329
+ } | tee "$report_file"
330
+
331
+ success "Velocity report saved to ${STANDUP_DIR}/velocity-*.txt"
332
+ }
333
+
334
+ # ─── Full Standup Digest ────────────────────────────────────────────────────
335
+ cmd_digest() {
336
+ ensure_dirs
337
+
338
+ local report_file="${STANDUP_DIR}/digest-$(date +%Y-%m-%d-%H%M%S).txt"
339
+
340
+ {
341
+ echo ""
342
+ echo "╔════════════════════════════════════════════════════════════════════╗"
343
+ echo "║ ${BOLD}DAILY STANDUP DIGEST${RESET} ${DIM}$(date '+%A, %B %d, %Y')${RESET} ║"
344
+ echo "╚════════════════════════════════════════════════════════════════════╝"
345
+ echo ""
346
+
347
+ # Yesterday's summary
348
+ echo "${CYAN}${BOLD}YESTERDAY'S ACCOMPLISHMENTS${RESET}"
349
+ echo "──────────────────────────────────────────"
350
+ if [[ -f "$EVENTS_FILE" ]]; then
351
+ local now_epoch
352
+ now_epoch="$(now_epoch)"
353
+ local cutoff=$((now_epoch - SECONDS_24H))
354
+
355
+ local commits=0
356
+ local prs=0
357
+ local tests_passed=0
358
+ local tests_failed=0
359
+
360
+ while IFS= read -r line; do
361
+ [[ -z "$line" ]] && continue
362
+ local ts_epoch
363
+ ts_epoch=$(echo "$line" | jq -r '.ts_epoch // 0' 2>/dev/null || echo 0)
364
+
365
+ if [[ "$ts_epoch" -lt "$cutoff" ]]; then
366
+ continue
367
+ fi
368
+
369
+ local event_type
370
+ event_type=$(echo "$line" | jq -r '.type // ""' 2>/dev/null || true)
371
+
372
+ case "$event_type" in
373
+ pipeline_commit) commits=$((commits + 1)) ;;
374
+ pipeline_pr) prs=$((prs + 1)) ;;
375
+ test_pass) tests_passed=$((tests_passed + 1)) ;;
376
+ test_fail) tests_failed=$((tests_failed + 1)) ;;
377
+ esac
378
+ done < "$EVENTS_FILE"
379
+
380
+ echo " • ${commits} commits"
381
+ echo " • ${prs} PRs created/merged"
382
+ echo " • ${tests_passed} tests passed"
383
+ if [[ "$tests_failed" -gt 0 ]]; then
384
+ echo " • ${RED}${tests_failed} tests failed${RESET}"
385
+ fi
386
+ else
387
+ echo " No events recorded yet"
388
+ fi
389
+ echo ""
390
+
391
+ # Today's focus
392
+ echo "${CYAN}${BOLD}TODAY'S FOCUS${RESET}"
393
+ echo "──────────────────────────────────────────"
394
+ if [[ -f "$DAEMON_STATE" ]]; then
395
+ local active_jobs
396
+ active_jobs=$(jq -r '.active_jobs // []' "$DAEMON_STATE" 2>/dev/null | jq length)
397
+ local queued
398
+ queued=$(jq -r '.queued // []' "$DAEMON_STATE" 2>/dev/null | jq length)
399
+
400
+ echo " • ${CYAN}${active_jobs}${RESET} active pipelines"
401
+ echo " • ${YELLOW}${queued}${RESET} queued issues"
402
+
403
+ if [[ "$active_jobs" -gt 0 ]]; then
404
+ echo ""
405
+ echo " Active Issues:"
406
+ jq -r '.active_jobs[] | " → Issue #\(.issue_number): \(.title // "untitled")"' "$DAEMON_STATE" 2>/dev/null | head -3
407
+ if [[ "$active_jobs" -gt 3 ]]; then
408
+ echo " ... and more"
409
+ fi
410
+ fi
411
+ else
412
+ echo " No daemon activity"
413
+ fi
414
+ echo ""
415
+
416
+ # Blockers
417
+ echo "${CYAN}${BOLD}BLOCKERS & RISKS${RESET}"
418
+ echo "──────────────────────────────────────────"
419
+ local blocker_count=0
420
+
421
+ if [[ -d "$HEARTBEATS_DIR" ]]; then
422
+ local now_epoch
423
+ now_epoch="$(now_epoch)"
424
+
425
+ for hb_file in "${HEARTBEATS_DIR}"/*.json; do
426
+ [[ ! -f "$hb_file" ]] && continue
427
+
428
+ local updated_at
429
+ updated_at=$(jq -r '.updated_at' "$hb_file" 2>/dev/null || true)
430
+ [[ -z "$updated_at" || "$updated_at" == "null" ]] && continue
431
+
432
+ local hb_epoch
433
+ hb_epoch="$(iso_to_epoch "$updated_at")"
434
+ local age=$((now_epoch - hb_epoch))
435
+
436
+ if [[ "$age" -gt 300 ]]; then
437
+ local job_id
438
+ job_id="$(basename "$hb_file" .json)"
439
+ echo " ${RED}✗${RESET} Stale agent: ${job_id} (${age}s silent)"
440
+ blocker_count=$((blocker_count + 1))
441
+ fi
442
+ done
443
+ fi
444
+
445
+ if [[ "$blocker_count" -eq 0 ]]; then
446
+ echo " ${GREEN}✓ No critical blockers${RESET}"
447
+ fi
448
+ echo ""
449
+
450
+ # System health
451
+ echo "${CYAN}${BOLD}SYSTEM HEALTH${RESET}"
452
+ echo "──────────────────────────────────────────"
453
+
454
+ local daemon_running="false"
455
+ if [[ -f "${HOME}/.shipwright/daemon.pid" ]]; then
456
+ local daemon_pid
457
+ daemon_pid=$(cat "${HOME}/.shipwright/daemon.pid" 2>/dev/null || true)
458
+ if [[ -n "$daemon_pid" ]] && kill -0 "$daemon_pid" 2>/dev/null; then
459
+ daemon_running="true"
460
+ fi
461
+ fi
462
+
463
+ if [[ "$daemon_running" == "true" ]]; then
464
+ echo " ${GREEN}✓${RESET} Daemon running"
465
+ else
466
+ echo " ${RED}✗${RESET} Daemon not running"
467
+ fi
468
+
469
+ local hb_count=0
470
+ [[ -d "$HEARTBEATS_DIR" ]] && hb_count=$(find "$HEARTBEATS_DIR" -name "*.json" -type f 2>/dev/null | wc -l || true)
471
+ echo " • ${hb_count} active agents"
472
+
473
+ echo ""
474
+ echo "─────────────────────────────────────────"
475
+ echo "Generated: $(date '+%Y-%m-%d %H:%M:%S %Z')"
476
+ echo ""
477
+
478
+ } | tee "$report_file"
479
+
480
+ success "Full digest saved to ${report_file}"
481
+ }
482
+
483
+ # ─── Notify via Webhook (Slack-compatible) ──────────────────────────────────
484
+ cmd_notify() {
485
+ local webhook_url="${1:-}"
486
+ local message_file="${2:-}"
487
+
488
+ if [[ -z "$webhook_url" ]]; then
489
+ error "Usage: shipwright standup notify <webhook_url> [message_file]"
490
+ exit 1
491
+ fi
492
+
493
+ if [[ -z "$message_file" ]]; then
494
+ # Generate a digest
495
+ message_file=$(mktemp)
496
+ cmd_digest > "$message_file" 2>&1 || true
497
+ fi
498
+
499
+ if [[ ! -f "$message_file" ]]; then
500
+ error "Message file not found: $message_file"
501
+ exit 1
502
+ fi
503
+
504
+ # Read message and format for Slack
505
+ local text
506
+ text=$(cat "$message_file" | head -100)
507
+
508
+ # Build JSON payload
509
+ local payload
510
+ payload=$(jq -n \
511
+ --arg text "$(printf '%s' "$text")" \
512
+ '{
513
+ text: "Daily Standup",
514
+ blocks: [
515
+ {
516
+ type: "section",
517
+ text: {
518
+ type: "mrkdwn",
519
+ text: $text
520
+ }
521
+ }
522
+ ]
523
+ }')
524
+
525
+ if command -v curl &>/dev/null; then
526
+ if curl -s -X POST -H 'Content-type: application/json' \
527
+ --data "$payload" "$webhook_url" &>/dev/null; then
528
+ success "Standup delivered to webhook"
529
+ else
530
+ error "Failed to deliver standup to webhook"
531
+ return 1
532
+ fi
533
+ else
534
+ error "curl not found, cannot send webhook"
535
+ return 1
536
+ fi
537
+ }
538
+
539
+ # ─── List Past Standups ──────────────────────────────────────────────────────
540
+ cmd_history() {
541
+ ensure_dirs
542
+
543
+ info "Past Standup Reports:"
544
+ echo ""
545
+
546
+ if [[ -d "$STANDUP_DIR" ]]; then
547
+ ls -lhT "$STANDUP_DIR"/*.txt 2>/dev/null | tail -20 || warn "No reports found"
548
+ else
549
+ warn "No standup history yet"
550
+ fi
551
+ }
552
+
553
+ # ─── Schedule Automatic Standups ────────────────────────────────────────────
554
+ cmd_schedule() {
555
+ local time="${1:-09:00}"
556
+
557
+ info "Setting up daily standup at ${time}..."
558
+
559
+ # Validate time format (HH:MM)
560
+ if ! [[ "$time" =~ ^[0-9]{2}:[0-9]{2}$ ]]; then
561
+ error "Invalid time format. Use HH:MM (24-hour format)"
562
+ exit 1
563
+ fi
564
+
565
+ ensure_dirs
566
+
567
+ # Create a wrapper script for cron
568
+ local cron_script="${STANDUP_DIR}/run-standup.sh"
569
+ {
570
+ echo "#!/usr/bin/env bash"
571
+ echo "set -euo pipefail"
572
+ echo "SHIPWRIGHT_SCRIPTS=\"\${1:-${SCRIPT_DIR}}\""
573
+ echo "cd \"\$SHIPWRIGHT_SCRIPTS/..\""
574
+ echo "\"${SCRIPT_DIR}/sw-standup.sh\" digest > /dev/null 2>&1"
575
+ echo "# Uncomment to send to Slack:"
576
+ echo "# \"${SCRIPT_DIR}/sw-standup.sh\" notify \"\${SLACK_WEBHOOK_URL}\" >> \"${STANDUP_DIR}/notify.log\" 2>&1"
577
+ } > "$cron_script"
578
+
579
+ chmod +x "$cron_script"
580
+
581
+ # Install cron job or launchd plist on macOS
582
+ if [[ "$(uname)" == "Darwin" ]]; then
583
+ # Create launchd plist
584
+ local plist="${HOME}/Library/LaunchAgents/com.shipwright.standup.plist"
585
+
586
+ {
587
+ echo "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
588
+ echo "<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">"
589
+ echo "<plist version=\"1.0\">"
590
+ echo "<dict>"
591
+ echo " <key>Label</key>"
592
+ echo " <string>com.shipwright.standup</string>"
593
+ echo " <key>ProgramArguments</key>"
594
+ echo " <array>"
595
+ echo " <string>${cron_script}</string>"
596
+ echo " <string>${SCRIPT_DIR}</string>"
597
+ echo " </array>"
598
+ echo " <key>StartCalendarInterval</key>"
599
+ echo " <dict>"
600
+ echo " <key>Hour</key>"
601
+ echo " <integer>${time%%:*}</integer>"
602
+ echo " <key>Minute</key>"
603
+ echo " <integer>${time##*:}</integer>"
604
+ echo " </dict>"
605
+ echo " <key>StandardErrorPath</key>"
606
+ echo " <string>${STANDUP_DIR}/launchd.log</string>"
607
+ echo " <key>StandardOutPath</key>"
608
+ echo " <string>${STANDUP_DIR}/launchd.log</string>"
609
+ echo "</dict>"
610
+ echo "</plist>"
611
+ } > "$plist"
612
+
613
+ success "Scheduled daily standup at ${time} (launchd)"
614
+ info "To activate: launchctl load ${plist}"
615
+ info "To deactivate: launchctl unload ${plist}"
616
+ else
617
+ # Linux: add to crontab
618
+ local hour="${time%%:*}"
619
+ local minute="${time##*:}"
620
+ local cron_entry="${minute} ${hour} * * * bash \"${cron_script}\" \"${SCRIPT_DIR}\" >> \"${STANDUP_DIR}/cron.log\" 2>&1"
621
+
622
+ warn "Please add this line to your crontab:"
623
+ echo ""
624
+ echo " ${cron_entry}"
625
+ echo ""
626
+ fi
627
+ }
628
+
629
+ # ─── Help ───────────────────────────────────────────────────────────────────
630
+ show_help() {
631
+ echo ""
632
+ echo -e "${CYAN}${BOLD} Shipwright Standup${RESET} ${DIM}v${VERSION}${RESET}"
633
+ echo -e "${DIM} ══════════════════════════════════════════${RESET}"
634
+ echo ""
635
+ echo -e " ${BOLD}USAGE${RESET}"
636
+ echo -e " shipwright standup <command> [options]"
637
+ echo ""
638
+ echo -e " ${BOLD}COMMANDS${RESET}"
639
+ echo -e " ${CYAN}run${RESET} Generate and display standup now"
640
+ echo -e " ${CYAN}digest${RESET} Full formatted standup digest"
641
+ echo -e " ${CYAN}yesterday${RESET} Summarize last 24 hours of work"
642
+ echo -e " ${CYAN}today${RESET} Show today's planned work"
643
+ echo -e " ${CYAN}blockers${RESET} Identify current blockers and risks"
644
+ echo -e " ${CYAN}velocity${RESET} Sprint velocity and burn-down metrics"
645
+ echo -e " ${CYAN}history${RESET} List past standup reports"
646
+ echo -e " ${CYAN}notify${RESET} Send standup to webhook (Slack-compatible)"
647
+ echo -e " ${CYAN}schedule${RESET} Set daily standup time (cron/launchd)"
648
+ echo -e " ${CYAN}help${RESET} Show this help message"
649
+ echo ""
650
+ echo -e " ${BOLD}NOTIFY OPTIONS${RESET}"
651
+ echo -e " shipwright standup notify <webhook_url> [message_file]"
652
+ echo ""
653
+ echo -e " ${BOLD}SCHEDULE OPTIONS${RESET}"
654
+ echo -e " shipwright standup schedule [HH:MM] ${DIM}(default: 09:00)${RESET}"
655
+ echo ""
656
+ echo -e " ${BOLD}EXAMPLES${RESET}"
657
+ echo -e " ${DIM}# Daily standup now${RESET}"
658
+ echo -e " shipwright standup digest"
659
+ echo ""
660
+ echo -e " ${DIM}# Send to Slack webhook${RESET}"
661
+ echo -e " shipwright standup notify https://hooks.slack.com/..."
662
+ echo ""
663
+ echo -e " ${DIM}# Schedule for 9:30 AM daily${RESET}"
664
+ echo -e " shipwright standup schedule 09:30"
665
+ echo ""
666
+ }
667
+
668
+ # ─── Main ───────────────────────────────────────────────────────────────────
669
+ main() {
670
+ local cmd="${1:-help}"
671
+ shift 2>/dev/null || true
672
+
673
+ case "$cmd" in
674
+ run|digest)
675
+ cmd_digest
676
+ ;;
677
+ yesterday)
678
+ cmd_yesterday
679
+ ;;
680
+ today)
681
+ cmd_today
682
+ ;;
683
+ blockers)
684
+ cmd_blockers
685
+ ;;
686
+ velocity|metrics)
687
+ cmd_velocity
688
+ ;;
689
+ history)
690
+ cmd_history
691
+ ;;
692
+ notify)
693
+ cmd_notify "$@"
694
+ ;;
695
+ schedule)
696
+ cmd_schedule "$@"
697
+ ;;
698
+ help|--help|-h)
699
+ show_help
700
+ ;;
701
+ *)
702
+ error "Unknown command: ${cmd}"
703
+ echo ""
704
+ show_help
705
+ exit 1
706
+ ;;
707
+ esac
708
+ }
709
+
710
+ if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then
711
+ main "$@"
712
+ fi