shipwright-cli 1.9.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/hooks/post-tool-use.sh +12 -5
- package/README.md +114 -36
- package/completions/_shipwright +212 -32
- package/completions/shipwright.bash +97 -25
- package/docs/strategy/01-market-research.md +619 -0
- package/docs/strategy/02-mission-and-brand.md +587 -0
- package/docs/strategy/03-gtm-and-roadmap.md +759 -0
- package/docs/strategy/QUICK-START.txt +289 -0
- package/docs/strategy/README.md +172 -0
- package/package.json +4 -2
- package/scripts/sw +217 -2
- package/scripts/sw-activity.sh +500 -0
- package/scripts/sw-adaptive.sh +925 -0
- package/scripts/sw-adversarial.sh +1 -1
- package/scripts/sw-architecture-enforcer.sh +1 -1
- package/scripts/sw-auth.sh +613 -0
- package/scripts/sw-autonomous.sh +664 -0
- package/scripts/sw-changelog.sh +704 -0
- package/scripts/sw-checkpoint.sh +79 -1
- package/scripts/sw-ci.sh +602 -0
- package/scripts/sw-cleanup.sh +192 -7
- package/scripts/sw-code-review.sh +637 -0
- package/scripts/sw-connect.sh +1 -1
- package/scripts/sw-context.sh +605 -0
- package/scripts/sw-cost.sh +1 -1
- package/scripts/sw-daemon.sh +812 -138
- package/scripts/sw-dashboard.sh +1 -1
- package/scripts/sw-db.sh +540 -0
- package/scripts/sw-decompose.sh +539 -0
- package/scripts/sw-deps.sh +551 -0
- package/scripts/sw-developer-simulation.sh +1 -1
- package/scripts/sw-discovery.sh +412 -0
- package/scripts/sw-docs-agent.sh +539 -0
- package/scripts/sw-docs.sh +1 -1
- package/scripts/sw-doctor.sh +59 -1
- package/scripts/sw-dora.sh +615 -0
- package/scripts/sw-durable.sh +710 -0
- package/scripts/sw-e2e-orchestrator.sh +535 -0
- package/scripts/sw-eventbus.sh +393 -0
- package/scripts/sw-feedback.sh +471 -0
- package/scripts/sw-fix.sh +1 -1
- package/scripts/sw-fleet-discover.sh +567 -0
- package/scripts/sw-fleet-viz.sh +404 -0
- package/scripts/sw-fleet.sh +8 -1
- package/scripts/sw-github-app.sh +596 -0
- package/scripts/sw-github-checks.sh +1 -1
- package/scripts/sw-github-deploy.sh +1 -1
- package/scripts/sw-github-graphql.sh +1 -1
- package/scripts/sw-guild.sh +569 -0
- package/scripts/sw-heartbeat.sh +1 -1
- package/scripts/sw-hygiene.sh +559 -0
- package/scripts/sw-incident.sh +617 -0
- package/scripts/sw-init.sh +88 -1
- package/scripts/sw-instrument.sh +699 -0
- package/scripts/sw-intelligence.sh +1 -1
- package/scripts/sw-jira.sh +1 -1
- package/scripts/sw-launchd.sh +366 -31
- package/scripts/sw-linear.sh +1 -1
- package/scripts/sw-logs.sh +1 -1
- package/scripts/sw-loop.sh +507 -51
- package/scripts/sw-memory.sh +198 -3
- package/scripts/sw-mission-control.sh +487 -0
- package/scripts/sw-model-router.sh +545 -0
- package/scripts/sw-otel.sh +596 -0
- package/scripts/sw-oversight.sh +689 -0
- package/scripts/sw-pipeline-composer.sh +8 -8
- package/scripts/sw-pipeline-vitals.sh +1096 -0
- package/scripts/sw-pipeline.sh +2451 -180
- package/scripts/sw-pm.sh +693 -0
- package/scripts/sw-pr-lifecycle.sh +522 -0
- package/scripts/sw-predictive.sh +1 -1
- package/scripts/sw-prep.sh +1 -1
- package/scripts/sw-ps.sh +4 -3
- package/scripts/sw-public-dashboard.sh +798 -0
- package/scripts/sw-quality.sh +595 -0
- package/scripts/sw-reaper.sh +5 -3
- package/scripts/sw-recruit.sh +573 -0
- package/scripts/sw-regression.sh +642 -0
- package/scripts/sw-release-manager.sh +736 -0
- package/scripts/sw-release.sh +706 -0
- package/scripts/sw-remote.sh +1 -1
- package/scripts/sw-replay.sh +520 -0
- package/scripts/sw-retro.sh +691 -0
- package/scripts/sw-scale.sh +444 -0
- package/scripts/sw-security-audit.sh +505 -0
- package/scripts/sw-self-optimize.sh +109 -8
- package/scripts/sw-session.sh +31 -9
- package/scripts/sw-setup.sh +1 -1
- package/scripts/sw-standup.sh +712 -0
- package/scripts/sw-status.sh +192 -1
- package/scripts/sw-strategic.sh +658 -0
- package/scripts/sw-stream.sh +450 -0
- package/scripts/sw-swarm.sh +583 -0
- package/scripts/sw-team-stages.sh +511 -0
- package/scripts/sw-templates.sh +1 -1
- package/scripts/sw-testgen.sh +515 -0
- package/scripts/sw-tmux-pipeline.sh +554 -0
- package/scripts/sw-tmux.sh +1 -1
- package/scripts/sw-trace.sh +485 -0
- package/scripts/sw-tracker-github.sh +188 -0
- package/scripts/sw-tracker-jira.sh +172 -0
- package/scripts/sw-tracker-linear.sh +251 -0
- package/scripts/sw-tracker.sh +117 -2
- package/scripts/sw-triage.sh +603 -0
- package/scripts/sw-upgrade.sh +1 -1
- package/scripts/sw-ux.sh +677 -0
- package/scripts/sw-webhook.sh +627 -0
- package/scripts/sw-widgets.sh +530 -0
- package/scripts/sw-worktree.sh +1 -1
- package/templates/pipelines/autonomous.json +8 -1
- package/templates/pipelines/cost-aware.json +21 -0
- package/templates/pipelines/deployed.json +40 -6
- package/templates/pipelines/enterprise.json +16 -2
- package/templates/pipelines/fast.json +19 -0
- package/templates/pipelines/full.json +16 -2
- package/templates/pipelines/hotfix.json +19 -0
- package/templates/pipelines/standard.json +19 -0
|
@@ -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.0.0"
|
|
10
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
11
|
+
REPO_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|
12
|
+
|
|
13
|
+
# ─── Colors (matches Seth's tmux theme) ─────────────────────────────────────
|
|
14
|
+
CYAN='\033[38;2;0;212;255m' # #00d4ff — primary accent
|
|
15
|
+
PURPLE='\033[38;2;124;58;237m' # #7c3aed — secondary
|
|
16
|
+
BLUE='\033[38;2;0;102;255m' # #0066ff — tertiary
|
|
17
|
+
GREEN='\033[38;2;74;222;128m' # success
|
|
18
|
+
YELLOW='\033[38;2;250;204;21m' # warning
|
|
19
|
+
RED='\033[38;2;248;113;113m' # error
|
|
20
|
+
DIM='\033[2m'
|
|
21
|
+
BOLD='\033[1m'
|
|
22
|
+
RESET='\033[0m'
|
|
23
|
+
|
|
24
|
+
# ─── Cross-platform compatibility ──────────────────────────────────────────
|
|
25
|
+
# shellcheck source=lib/compat.sh
|
|
26
|
+
[[ -f "$SCRIPT_DIR/lib/compat.sh" ]] && source "$SCRIPT_DIR/lib/compat.sh"
|
|
27
|
+
|
|
28
|
+
# ─── Output Helpers ─────────────────────────────────────────────────────────
|
|
29
|
+
info() { echo -e "${CYAN}${BOLD}▸${RESET} $*"; }
|
|
30
|
+
success() { echo -e "${GREEN}${BOLD}✓${RESET} $*"; }
|
|
31
|
+
warn() { echo -e "${YELLOW}${BOLD}⚠${RESET} $*"; }
|
|
32
|
+
error() { echo -e "${RED}${BOLD}✗${RESET} $*" >&2; }
|
|
33
|
+
|
|
34
|
+
now_iso() { date -u +"%Y-%m-%dT%H:%M:%SZ"; }
|
|
35
|
+
now_epoch() { date +%s; }
|
|
36
|
+
|
|
37
|
+
# ─── 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
|