shipwright-cli 2.4.0 → 3.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 (169) hide show
  1. package/README.md +16 -11
  2. package/completions/_shipwright +248 -94
  3. package/completions/shipwright.bash +68 -19
  4. package/completions/shipwright.fish +310 -42
  5. package/config/decision-tiers.json +55 -0
  6. package/config/defaults.json +111 -0
  7. package/config/event-schema.json +218 -0
  8. package/config/policy.json +21 -18
  9. package/dashboard/coverage/coverage-summary.json +14 -0
  10. package/dashboard/public/index.html +1 -1
  11. package/dashboard/server.ts +306 -17
  12. package/dashboard/src/components/charts/bar.test.ts +79 -0
  13. package/dashboard/src/components/charts/donut.test.ts +68 -0
  14. package/dashboard/src/components/charts/pipeline-rail.test.ts +117 -0
  15. package/dashboard/src/components/charts/sparkline.test.ts +125 -0
  16. package/dashboard/src/core/api.test.ts +309 -0
  17. package/dashboard/src/core/helpers.test.ts +301 -0
  18. package/dashboard/src/core/router.test.ts +307 -0
  19. package/dashboard/src/core/router.ts +7 -0
  20. package/dashboard/src/core/sse.test.ts +144 -0
  21. package/dashboard/src/views/metrics.test.ts +186 -0
  22. package/dashboard/src/views/overview.test.ts +173 -0
  23. package/dashboard/src/views/pipelines.test.ts +183 -0
  24. package/dashboard/src/views/team.test.ts +253 -0
  25. package/dashboard/vitest.config.ts +14 -5
  26. package/docs/TIPS.md +1 -1
  27. package/docs/patterns/README.md +1 -1
  28. package/package.json +7 -9
  29. package/scripts/adapters/docker-deploy.sh +1 -1
  30. package/scripts/adapters/tmux-adapter.sh +11 -1
  31. package/scripts/adapters/wezterm-adapter.sh +1 -1
  32. package/scripts/check-version-consistency.sh +1 -1
  33. package/scripts/lib/architecture.sh +127 -0
  34. package/scripts/lib/bootstrap.sh +75 -0
  35. package/scripts/lib/compat.sh +89 -6
  36. package/scripts/lib/config.sh +91 -0
  37. package/scripts/lib/daemon-adaptive.sh +3 -3
  38. package/scripts/lib/daemon-dispatch.sh +63 -17
  39. package/scripts/lib/daemon-failure.sh +0 -0
  40. package/scripts/lib/daemon-health.sh +1 -1
  41. package/scripts/lib/daemon-patrol.sh +64 -17
  42. package/scripts/lib/daemon-poll.sh +54 -25
  43. package/scripts/lib/daemon-state.sh +125 -23
  44. package/scripts/lib/daemon-triage.sh +31 -9
  45. package/scripts/lib/decide-autonomy.sh +295 -0
  46. package/scripts/lib/decide-scoring.sh +228 -0
  47. package/scripts/lib/decide-signals.sh +462 -0
  48. package/scripts/lib/fleet-failover.sh +63 -0
  49. package/scripts/lib/helpers.sh +29 -6
  50. package/scripts/lib/pipeline-detection.sh +2 -2
  51. package/scripts/lib/pipeline-github.sh +9 -9
  52. package/scripts/lib/pipeline-intelligence.sh +105 -38
  53. package/scripts/lib/pipeline-quality-checks.sh +17 -16
  54. package/scripts/lib/pipeline-quality.sh +1 -1
  55. package/scripts/lib/pipeline-stages.sh +440 -59
  56. package/scripts/lib/pipeline-state.sh +54 -4
  57. package/scripts/lib/policy.sh +0 -0
  58. package/scripts/lib/test-helpers.sh +247 -0
  59. package/scripts/postinstall.mjs +78 -12
  60. package/scripts/signals/example-collector.sh +36 -0
  61. package/scripts/sw +17 -7
  62. package/scripts/sw-activity.sh +1 -11
  63. package/scripts/sw-adaptive.sh +109 -85
  64. package/scripts/sw-adversarial.sh +4 -14
  65. package/scripts/sw-architecture-enforcer.sh +1 -11
  66. package/scripts/sw-auth.sh +8 -17
  67. package/scripts/sw-autonomous.sh +111 -49
  68. package/scripts/sw-changelog.sh +1 -11
  69. package/scripts/sw-checkpoint.sh +144 -20
  70. package/scripts/sw-ci.sh +2 -12
  71. package/scripts/sw-cleanup.sh +13 -17
  72. package/scripts/sw-code-review.sh +16 -36
  73. package/scripts/sw-connect.sh +5 -12
  74. package/scripts/sw-context.sh +9 -26
  75. package/scripts/sw-cost.sh +17 -18
  76. package/scripts/sw-daemon.sh +76 -71
  77. package/scripts/sw-dashboard.sh +57 -17
  78. package/scripts/sw-db.sh +524 -26
  79. package/scripts/sw-decide.sh +685 -0
  80. package/scripts/sw-decompose.sh +1 -11
  81. package/scripts/sw-deps.sh +15 -25
  82. package/scripts/sw-developer-simulation.sh +1 -11
  83. package/scripts/sw-discovery.sh +138 -30
  84. package/scripts/sw-doc-fleet.sh +7 -17
  85. package/scripts/sw-docs-agent.sh +6 -16
  86. package/scripts/sw-docs.sh +4 -12
  87. package/scripts/sw-doctor.sh +134 -43
  88. package/scripts/sw-dora.sh +11 -19
  89. package/scripts/sw-durable.sh +35 -52
  90. package/scripts/sw-e2e-orchestrator.sh +11 -27
  91. package/scripts/sw-eventbus.sh +115 -115
  92. package/scripts/sw-evidence.sh +114 -30
  93. package/scripts/sw-feedback.sh +3 -13
  94. package/scripts/sw-fix.sh +2 -20
  95. package/scripts/sw-fleet-discover.sh +1 -11
  96. package/scripts/sw-fleet-viz.sh +10 -18
  97. package/scripts/sw-fleet.sh +13 -17
  98. package/scripts/sw-github-app.sh +6 -16
  99. package/scripts/sw-github-checks.sh +1 -11
  100. package/scripts/sw-github-deploy.sh +1 -11
  101. package/scripts/sw-github-graphql.sh +2 -12
  102. package/scripts/sw-guild.sh +1 -11
  103. package/scripts/sw-heartbeat.sh +49 -12
  104. package/scripts/sw-hygiene.sh +45 -43
  105. package/scripts/sw-incident.sh +48 -74
  106. package/scripts/sw-init.sh +35 -37
  107. package/scripts/sw-instrument.sh +1 -11
  108. package/scripts/sw-intelligence.sh +368 -53
  109. package/scripts/sw-jira.sh +5 -14
  110. package/scripts/sw-launchd.sh +2 -12
  111. package/scripts/sw-linear.sh +8 -17
  112. package/scripts/sw-logs.sh +4 -12
  113. package/scripts/sw-loop.sh +905 -104
  114. package/scripts/sw-memory.sh +263 -20
  115. package/scripts/sw-mission-control.sh +2 -12
  116. package/scripts/sw-model-router.sh +73 -34
  117. package/scripts/sw-otel.sh +15 -23
  118. package/scripts/sw-oversight.sh +1 -11
  119. package/scripts/sw-patrol-meta.sh +5 -11
  120. package/scripts/sw-pipeline-composer.sh +7 -17
  121. package/scripts/sw-pipeline-vitals.sh +1 -11
  122. package/scripts/sw-pipeline.sh +550 -122
  123. package/scripts/sw-pm.sh +2 -12
  124. package/scripts/sw-pr-lifecycle.sh +33 -28
  125. package/scripts/sw-predictive.sh +16 -22
  126. package/scripts/sw-prep.sh +6 -16
  127. package/scripts/sw-ps.sh +1 -11
  128. package/scripts/sw-public-dashboard.sh +2 -12
  129. package/scripts/sw-quality.sh +85 -14
  130. package/scripts/sw-reaper.sh +1 -11
  131. package/scripts/sw-recruit.sh +15 -25
  132. package/scripts/sw-regression.sh +11 -21
  133. package/scripts/sw-release-manager.sh +19 -28
  134. package/scripts/sw-release.sh +8 -16
  135. package/scripts/sw-remote.sh +1 -11
  136. package/scripts/sw-replay.sh +48 -44
  137. package/scripts/sw-retro.sh +70 -92
  138. package/scripts/sw-review-rerun.sh +1 -1
  139. package/scripts/sw-scale.sh +174 -41
  140. package/scripts/sw-security-audit.sh +12 -22
  141. package/scripts/sw-self-optimize.sh +239 -23
  142. package/scripts/sw-session.sh +5 -15
  143. package/scripts/sw-setup.sh +8 -18
  144. package/scripts/sw-standup.sh +5 -15
  145. package/scripts/sw-status.sh +32 -23
  146. package/scripts/sw-strategic.sh +129 -13
  147. package/scripts/sw-stream.sh +1 -11
  148. package/scripts/sw-swarm.sh +76 -36
  149. package/scripts/sw-team-stages.sh +10 -20
  150. package/scripts/sw-templates.sh +4 -14
  151. package/scripts/sw-testgen.sh +3 -13
  152. package/scripts/sw-tmux-pipeline.sh +1 -19
  153. package/scripts/sw-tmux-role-color.sh +0 -10
  154. package/scripts/sw-tmux-status.sh +3 -11
  155. package/scripts/sw-tmux.sh +2 -20
  156. package/scripts/sw-trace.sh +1 -19
  157. package/scripts/sw-tracker-github.sh +0 -10
  158. package/scripts/sw-tracker-jira.sh +1 -11
  159. package/scripts/sw-tracker-linear.sh +1 -11
  160. package/scripts/sw-tracker.sh +7 -24
  161. package/scripts/sw-triage.sh +29 -39
  162. package/scripts/sw-upgrade.sh +5 -23
  163. package/scripts/sw-ux.sh +1 -19
  164. package/scripts/sw-webhook.sh +18 -32
  165. package/scripts/sw-widgets.sh +3 -21
  166. package/scripts/sw-worktree.sh +11 -27
  167. package/scripts/update-homebrew-sha.sh +73 -0
  168. package/templates/pipelines/tdd.json +72 -0
  169. package/scripts/sw-pipeline.sh.mock +0 -7
@@ -6,7 +6,7 @@
6
6
  set -euo pipefail
7
7
  trap 'echo "ERROR: $BASH_SOURCE:$LINENO exited with status $?" >&2' ERR
8
8
 
9
- VERSION="2.4.0"
9
+ VERSION="3.1.0"
10
10
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
11
11
  EVENTS_FILE="${HOME}/.shipwright/events.jsonl"
12
12
 
@@ -17,6 +17,9 @@ EVENTS_FILE="${HOME}/.shipwright/events.jsonl"
17
17
  # Canonical helpers (colors, output, events)
18
18
  # shellcheck source=lib/helpers.sh
19
19
  [[ -f "$SCRIPT_DIR/lib/helpers.sh" ]] && source "$SCRIPT_DIR/lib/helpers.sh"
20
+ # DB layer for dual-read (SQLite + JSONL fallback)
21
+ # shellcheck source=sw-db.sh
22
+ [[ -f "$SCRIPT_DIR/sw-db.sh" ]] && source "$SCRIPT_DIR/sw-db.sh"
20
23
  # Fallbacks when helpers not loaded (e.g. test env with overridden SCRIPT_DIR)
21
24
  [[ "$(type -t info 2>/dev/null)" == "function" ]] || info() { echo -e "\033[38;2;0;212;255m\033[1m▸\033[0m $*"; }
22
25
  [[ "$(type -t success 2>/dev/null)" == "function" ]] || success() { echo -e "\033[38;2;74;222;128m\033[1m✓\033[0m $*"; }
@@ -34,19 +37,9 @@ if [[ "$(type -t emit_event 2>/dev/null)" != "function" ]]; then
34
37
  echo "${payload}}" >> "${HOME}/.shipwright/events.jsonl"
35
38
  }
36
39
  fi
37
- CYAN="${CYAN:-\033[38;2;0;212;255m}"
38
- PURPLE="${PURPLE:-\033[38;2;124;58;237m}"
39
- BLUE="${BLUE:-\033[38;2;0;102;255m}"
40
- GREEN="${GREEN:-\033[38;2;74;222;128m}"
41
- YELLOW="${YELLOW:-\033[38;2;250;204;21m}"
42
- RED="${RED:-\033[38;2;248;113;113m}"
43
- DIM="${DIM:-\033[2m}"
44
- BOLD="${BOLD:-\033[1m}"
45
- RESET="${RESET:-\033[0m}"
46
-
47
40
  # Check if jq is available
48
41
  check_jq() {
49
- if ! command -v jq &>/dev/null; then
42
+ if ! command -v jq >/dev/null 2>&1; then
50
43
  error "jq is required. Install with: brew install jq"
51
44
  exit 1
52
45
  fi
@@ -85,22 +78,23 @@ color_status() {
85
78
  cmd_list() {
86
79
  check_jq
87
80
 
88
- if [[ ! -f "$EVENTS_FILE" ]]; then
89
- warn "No pipeline runs recorded yet (events file not found)"
81
+ local events_json
82
+ events_json=$(db_query_events "" 5000 2>/dev/null || echo "[]")
83
+ if [[ "$events_json" == "[]" ]] || [[ -z "$events_json" ]]; then
84
+ warn "No pipeline runs recorded yet (events not found)"
90
85
  exit 0
91
86
  fi
92
87
 
93
- info "Pipeline runs (${DIM}from $EVENTS_FILE${RESET})"
88
+ info "Pipeline runs (${DIM}DB + JSONL${RESET})"
94
89
  echo ""
95
90
 
96
91
  # Extract unique pipeline runs, sorted by start time
97
- # Use --slurpfile to handle parsing errors gracefully
98
- jq -r 'select(.type == "pipeline.started") | [.ts, .issue, .pipeline, .model, .goal] | @tsv' "$EVENTS_FILE" 2>/dev/null | \
92
+ echo "$events_json" | jq -r '.[] | select(.type == "pipeline.started") | [.ts, .issue, .pipeline, .model, .goal] | @tsv' 2>/dev/null | \
99
93
  sort -r | \
100
94
  while IFS=$'\t' read -r ts issue pipeline model goal; do
101
95
  # Find corresponding completion event
102
96
  local completion
103
- completion=$(jq -r "select(.type == \"pipeline.completed\" and .issue == $issue) | .result, .duration_s" "$EVENTS_FILE" 2>/dev/null | head -2)
97
+ completion=$(echo "$events_json" | jq -r ".[] | select(.type == \"pipeline.completed\" and .issue == $issue) | .result, .duration_s" 2>/dev/null | head -2)
104
98
 
105
99
  if [[ -n "$completion" ]]; then
106
100
  local result duration
@@ -137,14 +131,16 @@ cmd_show() {
137
131
  exit 1
138
132
  fi
139
133
 
140
- if [[ ! -f "$EVENTS_FILE" ]]; then
134
+ local events_json
135
+ events_json=$(db_query_events "" 5000 2>/dev/null || echo "[]")
136
+ if [[ "$events_json" == "[]" ]] || [[ -z "$events_json" ]]; then
141
137
  error "No events recorded yet"
142
138
  exit 1
143
139
  fi
144
140
 
145
141
  # Find pipeline run for this issue
146
142
  local pipeline_start
147
- pipeline_start=$(jq -r "select(.type == \"pipeline.started\" and .issue == $issue) | [.ts, .pipeline, .model, .goal] | @tsv" "$EVENTS_FILE" 2>/dev/null | head -1)
143
+ pipeline_start=$(echo "$events_json" | jq -r ".[] | select(.type == \"pipeline.started\" and .issue == $issue) | [.ts, .pipeline, .model, .goal] | @tsv" 2>/dev/null | head -1)
148
144
 
149
145
  if [[ -z "$pipeline_start" ]]; then
150
146
  error "No pipeline run found for issue #$issue"
@@ -166,7 +162,7 @@ cmd_show() {
166
162
 
167
163
  # Find all stage events for this issue
168
164
  echo -e " ${BOLD}Stages:${RESET}"
169
- jq -r "select(.issue == $issue and .type == \"stage.completed\") | [.ts, .stage, .duration_s // 0, .result // \"success\"] | @tsv" "$EVENTS_FILE" 2>/dev/null | \
165
+ echo "$events_json" | jq -r ".[] | select(.issue == $issue and .type == \"stage.completed\") | [.ts, .stage, .duration_s // 0, .result // \"success\"] | @tsv" 2>/dev/null | \
170
166
  while IFS=$'\t' read -r ts stage duration result; do
171
167
  local status_icon
172
168
  case "$result" in
@@ -185,7 +181,7 @@ cmd_show() {
185
181
 
186
182
  # Find pipeline completion
187
183
  local completion
188
- completion=$(jq -r "select(.type == \"pipeline.completed\" and .issue == $issue) | [.result // \"unknown\", .duration_s // 0, .input_tokens // 0, .output_tokens // 0] | @tsv" "$EVENTS_FILE" 2>/dev/null | head -1)
184
+ completion=$(echo "$events_json" | jq -r ".[] | select(.type == \"pipeline.completed\" and .issue == $issue) | [.result // \"unknown\", .duration_s // 0, .input_tokens // 0, .output_tokens // 0] | @tsv" 2>/dev/null | head -1)
189
185
 
190
186
  if [[ -n "$completion" ]]; then
191
187
  echo ""
@@ -214,13 +210,15 @@ cmd_narrative() {
214
210
  exit 1
215
211
  fi
216
212
 
217
- if [[ ! -f "$EVENTS_FILE" ]]; then
213
+ local events_json
214
+ events_json=$(db_query_events "" 5000 2>/dev/null || echo "[]")
215
+ if [[ "$events_json" == "[]" ]] || [[ -z "$events_json" ]]; then
218
216
  error "No events recorded yet"
219
217
  exit 1
220
218
  fi
221
219
 
222
220
  local pipeline_start
223
- pipeline_start=$(jq -r "select(.type == \"pipeline.started\" and .issue == $issue) | [.ts, .goal // \"\", .pipeline // \"standard\"] | @tsv" "$EVENTS_FILE" 2>/dev/null | head -1)
221
+ pipeline_start=$(echo "$events_json" | jq -r ".[] | select(.type == \"pipeline.started\" and .issue == $issue) | [.ts, .goal // \"\", .pipeline // \"standard\"] | @tsv" 2>/dev/null | head -1)
224
222
 
225
223
  if [[ -z "$pipeline_start" ]]; then
226
224
  error "No pipeline run found for issue #$issue"
@@ -234,7 +232,7 @@ cmd_narrative() {
234
232
 
235
233
  # Get pipeline completion
236
234
  local completion
237
- completion=$(jq -r "select(.type == \"pipeline.completed\" and .issue == $issue) | [.result // \"unknown\", .duration_s // 0, .input_tokens // 0, .output_tokens // 0] | @tsv" "$EVENTS_FILE" 2>/dev/null | head -1)
235
+ completion=$(echo "$events_json" | jq -r ".[] | select(.type == \"pipeline.completed\" and .issue == $issue) | [.result // \"unknown\", .duration_s // 0, .input_tokens // 0, .output_tokens // 0] | @tsv" 2>/dev/null | head -1)
238
236
 
239
237
  local result duration input_tokens output_tokens
240
238
  if [[ -n "$completion" ]]; then
@@ -251,7 +249,7 @@ cmd_narrative() {
251
249
 
252
250
  # Count stages
253
251
  local stage_count
254
- stage_count=$(jq -r "select(.issue == $issue and .type == \"stage.completed\") | .stage" "$EVENTS_FILE" 2>/dev/null | wc -l)
252
+ stage_count=$(echo "$events_json" | jq -r ".[] | select(.issue == $issue and .type == \"stage.completed\") | .stage" 2>/dev/null | wc -l)
255
253
 
256
254
  # Build narrative
257
255
  info "Pipeline Narrative"
@@ -267,9 +265,9 @@ cmd_narrative() {
267
265
 
268
266
  # Key events
269
267
  local retry_count build_iterations test_failures
270
- retry_count=$(jq -r "select(.issue == $issue and .type == \"stage.completed\" and .result == \"retry\") | .stage" "$EVENTS_FILE" 2>/dev/null | wc -l)
271
- build_iterations=$(jq -r "select(.issue == $issue and .type == \"build.iteration\") | .iteration" "$EVENTS_FILE" 2>/dev/null | tail -1)
272
- test_failures=$(jq -r "select(.issue == $issue and .type == \"test.failed\") | .test" "$EVENTS_FILE" 2>/dev/null | wc -l)
268
+ retry_count=$(echo "$events_json" | jq -r ".[] | select(.issue == $issue and .type == \"stage.completed\" and .result == \"retry\") | .stage" 2>/dev/null | wc -l)
269
+ build_iterations=$(echo "$events_json" | jq -r ".[] | select(.issue == $issue and .type == \"build.iteration\") | .iteration" 2>/dev/null | tail -1)
270
+ test_failures=$(echo "$events_json" | jq -r ".[] | select(.issue == $issue and .type == \"test.failed\") | .test" 2>/dev/null | wc -l)
273
271
 
274
272
  echo "Key Events:"
275
273
  [[ $retry_count -gt 0 ]] && echo " • $retry_count stage retries"
@@ -290,19 +288,21 @@ cmd_diff() {
290
288
  exit 1
291
289
  fi
292
290
 
293
- if ! command -v git &>/dev/null; then
291
+ if ! command -v git >/dev/null 2>&1; then
294
292
  error "git is required for diff subcommand"
295
293
  exit 1
296
294
  fi
297
295
 
298
- if [[ ! -f "$EVENTS_FILE" ]]; then
296
+ local events_json
297
+ events_json=$(db_query_events "" 5000 2>/dev/null || echo "[]")
298
+ if [[ "$events_json" == "[]" ]] || [[ -z "$events_json" ]]; then
299
299
  error "No events recorded yet"
300
300
  exit 1
301
301
  fi
302
302
 
303
303
  # Check if issue was processed
304
304
  local found
305
- found=$(jq -r "select(.issue == $issue and .type == \"pipeline.completed\") | .issue" "$EVENTS_FILE" | head -1)
305
+ found=$(echo "$events_json" | jq -r ".[] | select(.issue == $issue and .type == \"pipeline.completed\") | .issue" 2>/dev/null | head -1)
306
306
 
307
307
  if [[ -z "$found" ]]; then
308
308
  error "No pipeline run found for issue #$issue"
@@ -317,7 +317,7 @@ cmd_diff() {
317
317
 
318
318
  # Also try to find by branch name pattern
319
319
  local branch_pattern="issue-${issue}"
320
- if git show-ref --verify "refs/heads/$branch_pattern" &>/dev/null; then
320
+ if git show-ref --verify "refs/heads/$branch_pattern" >/dev/null 2>&1; then
321
321
  echo ""
322
322
  info "Commits on branch '$branch_pattern':"
323
323
  git log "$branch_pattern" --oneline || true
@@ -337,13 +337,15 @@ cmd_export() {
337
337
  exit 1
338
338
  fi
339
339
 
340
- if [[ ! -f "$EVENTS_FILE" ]]; then
340
+ local events_json
341
+ events_json=$(db_query_events "" 5000 2>/dev/null || echo "[]")
342
+ if [[ "$events_json" == "[]" ]] || [[ -z "$events_json" ]]; then
341
343
  error "No events recorded yet"
342
344
  exit 1
343
345
  fi
344
346
 
345
347
  local pipeline_start
346
- pipeline_start=$(jq -r "select(.type == \"pipeline.started\" and .issue == $issue) | [.ts, .goal // \"\", .pipeline // \"standard\"] | @tsv" "$EVENTS_FILE" 2>/dev/null | head -1)
348
+ pipeline_start=$(echo "$events_json" | jq -r ".[] | select(.type == \"pipeline.started\" and .issue == $issue) | [.ts, .goal // \"\", .pipeline // \"standard\"] | @tsv" 2>/dev/null | head -1)
347
349
 
348
350
  if [[ -z "$pipeline_start" ]]; then
349
351
  error "No pipeline run found for issue #$issue"
@@ -356,7 +358,7 @@ cmd_export() {
356
358
  pipeline_type=$(echo "$pipeline_start" | cut -f3)
357
359
 
358
360
  local completion
359
- completion=$(jq -r "select(.type == \"pipeline.completed\" and .issue == $issue) | [.result // \"unknown\", .duration_s // 0] | @tsv" "$EVENTS_FILE" 2>/dev/null | head -1)
361
+ completion=$(echo "$events_json" | jq -r ".[] | select(.type == \"pipeline.completed\" and .issue == $issue) | [.result // \"unknown\", .duration_s // 0] | @tsv" 2>/dev/null | head -1)
360
362
 
361
363
  local result duration
362
364
  if [[ -n "$completion" ]]; then
@@ -386,7 +388,7 @@ $goal
386
388
  EOF
387
389
 
388
390
  # Add stage rows
389
- jq -r "select(.issue == $issue and .type == \"stage.completed\") | [.stage, .duration_s // 0, .result // \"success\"] | @tsv" "$EVENTS_FILE" 2>/dev/null | \
391
+ echo "$events_json" | jq -r ".[] | select(.issue == $issue and .type == \"stage.completed\") | [.stage, .duration_s // 0, .result // \"success\"] | @tsv" 2>/dev/null | \
390
392
  while IFS=$'\t' read -r stage duration result; do
391
393
  printf "| %s | %s | %s |\n" "$stage" "$(format_duration "$duration")" "$result"
392
394
  done
@@ -402,7 +404,7 @@ EOF
402
404
 
403
405
  ## Events
404
406
 
405
- $(jq -r "select(.issue == $issue) | [.ts, .type] | @tsv" "$EVENTS_FILE" 2>/dev/null | awk '{print "- " $1 " — " $2}')
407
+ $(echo "$events_json" | jq -r ".[] | select(.issue == $issue) | [.ts, .type] | @tsv" 2>/dev/null | awk '{print "- " $1 " — " $2}')
406
408
 
407
409
  EOF
408
410
 
@@ -420,7 +422,9 @@ cmd_compare() {
420
422
  exit 1
421
423
  fi
422
424
 
423
- if [[ ! -f "$EVENTS_FILE" ]]; then
425
+ local events_json
426
+ events_json=$(db_query_events "" 5000 2>/dev/null || echo "[]")
427
+ if [[ "$events_json" == "[]" ]] || [[ -z "$events_json" ]]; then
424
428
  error "No events recorded yet"
425
429
  exit 1
426
430
  fi
@@ -430,8 +434,8 @@ cmd_compare() {
430
434
 
431
435
  # Get both runs
432
436
  local run1 run2
433
- run1=$(jq -r "select(.type == \"pipeline.started\" and .issue == $issue1) | [.goal // \"\", .pipeline, .model] | @tsv" "$EVENTS_FILE" 2>/dev/null | head -1)
434
- run2=$(jq -r "select(.type == \"pipeline.started\" and .issue == $issue2) | [.goal // \"\", .pipeline, .model] | @tsv" "$EVENTS_FILE" 2>/dev/null | head -1)
437
+ run1=$(echo "$events_json" | jq -r ".[] | select(.type == \"pipeline.started\" and .issue == $issue1) | [.goal // \"\", .pipeline, .model] | @tsv" 2>/dev/null | head -1)
438
+ run2=$(echo "$events_json" | jq -r ".[] | select(.type == \"pipeline.started\" and .issue == $issue2) | [.goal // \"\", .pipeline, .model] | @tsv" 2>/dev/null | head -1)
435
439
 
436
440
  if [[ -z "$run1" || -z "$run2" ]]; then
437
441
  error "Could not find both pipeline runs"
@@ -451,8 +455,8 @@ cmd_compare() {
451
455
 
452
456
  # Get completions
453
457
  local comp1 comp2
454
- comp1=$(jq -r "select(.type == \"pipeline.completed\" and .issue == $issue1) | [.result // \"unknown\", .duration_s // 0] | @tsv" "$EVENTS_FILE" 2>/dev/null | head -1)
455
- comp2=$(jq -r "select(.type == \"pipeline.completed\" and .issue == $issue2) | [.result // \"unknown\", .duration_s // 0] | @tsv" "$EVENTS_FILE" 2>/dev/null | head -1)
458
+ comp1=$(echo "$events_json" | jq -r ".[] | select(.type == \"pipeline.completed\" and .issue == $issue1) | [.result // \"unknown\", .duration_s // 0] | @tsv" 2>/dev/null | head -1)
459
+ comp2=$(echo "$events_json" | jq -r ".[] | select(.type == \"pipeline.completed\" and .issue == $issue2) | [.result // \"unknown\", .duration_s // 0] | @tsv" 2>/dev/null | head -1)
456
460
 
457
461
  local result1 duration1 result2 duration2
458
462
  result1=$(echo "$comp1" | cut -f1)
@@ -497,7 +501,7 @@ ${BOLD}EXAMPLES${RESET}
497
501
  shipwright replay export 42 # Markdown report for #42
498
502
  shipwright replay compare 42 43 # Compare two runs
499
503
 
500
- ${DIM}Pipeline events are read from: $EVENTS_FILE${RESET}
504
+ ${DIM}Pipeline events are read from: DB + JSONL fallback${RESET}
501
505
 
502
506
  EOF
503
507
  }
@@ -6,7 +6,7 @@
6
6
  set -euo pipefail
7
7
  trap 'echo "ERROR: $BASH_SOURCE:$LINENO exited with status $?" >&2' ERR
8
8
 
9
- VERSION="2.4.0"
9
+ VERSION="3.1.0"
10
10
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
11
11
 
12
12
  # ─── Cross-platform compatibility ──────────────────────────────────────────
@@ -16,6 +16,9 @@ _COMPAT="$SCRIPT_DIR/lib/compat.sh"
16
16
  # Canonical helpers (colors, output, events)
17
17
  # shellcheck source=lib/helpers.sh
18
18
  [[ -f "$SCRIPT_DIR/lib/helpers.sh" ]] && source "$SCRIPT_DIR/lib/helpers.sh"
19
+ # DB layer for dual-read (SQLite + JSONL fallback)
20
+ # shellcheck source=sw-db.sh
21
+ [[ -f "$SCRIPT_DIR/sw-db.sh" ]] && source "$SCRIPT_DIR/sw-db.sh"
19
22
  # Fallbacks when helpers not loaded (e.g. test env with overridden SCRIPT_DIR)
20
23
  [[ "$(type -t info 2>/dev/null)" == "function" ]] || info() { echo -e "\033[38;2;0;212;255m\033[1m▸\033[0m $*"; }
21
24
  [[ "$(type -t success 2>/dev/null)" == "function" ]] || success() { echo -e "\033[38;2;74;222;128m\033[1m✓\033[0m $*"; }
@@ -25,24 +28,7 @@ if [[ "$(type -t now_iso 2>/dev/null)" != "function" ]]; then
25
28
  now_iso() { date -u +"%Y-%m-%dT%H:%M:%SZ"; }
26
29
  now_epoch() { date +%s; }
27
30
  fi
28
- if [[ "$(type -t emit_event 2>/dev/null)" != "function" ]]; then
29
- emit_event() {
30
- local event_type="$1"; shift; mkdir -p "${HOME}/.shipwright"
31
- local payload="{\"ts\":\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\",\"type\":\"$event_type\""
32
- while [[ $# -gt 0 ]]; do local key="${1%%=*}" val="${1#*=}"; payload="${payload},\"${key}\":\"${val}\""; shift; done
33
- echo "${payload}}" >> "${HOME}/.shipwright/events.jsonl"
34
- }
35
- fi
36
- CYAN="${CYAN:-\033[38;2;0;212;255m}"
37
- PURPLE="${PURPLE:-\033[38;2;124;58;237m}"
38
- BLUE="${BLUE:-\033[38;2;0;102;255m}"
39
- GREEN="${GREEN:-\033[38;2;74;222;128m}"
40
- YELLOW="${YELLOW:-\033[38;2;250;204;21m}"
41
- RED="${RED:-\033[38;2;248;113;113m}"
42
- DIM="${DIM:-\033[2m}"
43
- BOLD="${BOLD:-\033[1m}"
44
- RESET="${RESET:-\033[0m}"
45
-
31
+ # epoch_to_iso from compat.sh (cross-platform: BSD date -r first, then GNU -d)
46
32
  epoch_to_iso() {
47
33
  local epoch="$1"
48
34
  date -u -r "$epoch" +"%Y-%m-%dT%H:%M:%SZ" 2>/dev/null || \
@@ -74,11 +60,9 @@ get_sprint_dates() {
74
60
  local to_date="${2:-}"
75
61
 
76
62
  if [[ -z "$from_date" ]]; then
77
- # Default: last 7 days
63
+ # Default: last 7 days (cross-platform: date_days_ago from compat.sh)
78
64
  to_date=$(date -u +"%Y-%m-%d")
79
- from_date=$(date -u -v-7d +"%Y-%m-%d" 2>/dev/null || \
80
- date -u -d "7 days ago" +"%Y-%m-%d" 2>/dev/null || \
81
- python3 -c "from datetime import datetime, timedelta; print((datetime.utcnow() - timedelta(days=7)).strftime('%Y-%m-%d'))")
65
+ from_date=$(date_days_ago 7 2>/dev/null || python3 -c "from datetime import datetime, timedelta; print((datetime.utcnow() - timedelta(days=7)).strftime('%Y-%m-%d'))")
82
66
  elif [[ -z "$to_date" ]]; then
83
67
  to_date=$(date -u +"%Y-%m-%d")
84
68
  fi
@@ -91,26 +75,19 @@ analyze_sprint_data() {
91
75
  local from_date="$1"
92
76
  local to_date="$2"
93
77
 
94
- local events_file="${HOME}/.shipwright/events.jsonl"
95
- if [[ ! -f "$events_file" ]]; then
96
- echo '{"pipelines":0,"succeeded":0,"failed":0,"retries":0,"avg_duration":0,"avg_stages":0,"slowest_stage":"","quality_score":0}'
97
- return 0
98
- fi
99
-
100
- if ! command -v jq &>/dev/null; then
78
+ if ! command -v jq >/dev/null 2>&1; then
101
79
  error "jq is required for sprint analysis"
102
80
  return 1
103
81
  fi
104
82
 
105
- # Convert dates to epoch
83
+ # Convert dates to epoch (cross-platform: date_to_epoch from compat.sh)
106
84
  local from_epoch to_epoch
107
- from_epoch=$(date -u -d "${from_date}T00:00:00Z" +%s 2>/dev/null || \
108
- date -u -r "$(date -d "${from_date}T00:00:00Z" +%s 2>/dev/null || echo 0)" +%s || echo 0)
109
- to_epoch=$(date -u -d "${to_date}T23:59:59Z" +%s 2>/dev/null || \
110
- date -u -r "$(date -d "${to_date}T23:59:59Z" +%s 2>/dev/null || echo 0)" +%s || echo 0)
85
+ from_epoch=$(date_to_epoch "${from_date}T00:00:00Z")
86
+ to_epoch=$(date_to_epoch "${to_date}T23:59:59Z")
111
87
 
112
- jq -s --argjson from "$from_epoch" --argjson to "$to_epoch" '
113
- [.[] | select(.ts_epoch >= $from and .ts_epoch <= $to)] as $events |
88
+ # Use db_query_events_since (SQLite + JSONL fallback)
89
+ db_query_events_since "$from_epoch" "" "$to_epoch" | jq --argjson from "$from_epoch" --argjson to "$to_epoch" '
90
+ (if type == "array" then . else [] end) as $events |
114
91
  [$events[] | select(.type == "pipeline.completed")] as $completed |
115
92
  ($completed | length) as $total_pipelines |
116
93
  [$completed[] | select(.result == "success")] as $successes |
@@ -132,7 +109,7 @@ analyze_sprint_data() {
132
109
  slowest_stage: $slowest,
133
110
  quality_score: $quality
134
111
  }
135
- ' "$events_file" 2>/dev/null || echo '{"pipelines":0,"succeeded":0,"failed":0,"retries":0,"avg_duration":0,"avg_stages":0,"slowest_stage":"","quality_score":0}'
112
+ ' 2>/dev/null || echo '{"pipelines":0,"succeeded":0,"failed":0,"retries":0,"avg_duration":0,"avg_stages":0,"slowest_stage":"","quality_score":0}'
136
113
  }
137
114
 
138
115
  # ─── Agent Performance Analysis ─────────────────────────────────────────────
@@ -140,23 +117,17 @@ analyze_agent_performance() {
140
117
  local from_date="$1"
141
118
  local to_date="$2"
142
119
 
143
- local events_file="${HOME}/.shipwright/events.jsonl"
144
- if [[ ! -f "$events_file" ]]; then
145
- echo '{"agents":[]}'
146
- return 0
147
- fi
148
-
149
- if ! command -v jq &>/dev/null; then
120
+ if ! command -v jq >/dev/null 2>&1; then
150
121
  echo '{"agents":[]}'
151
122
  return 0
152
123
  fi
153
124
 
154
125
  local from_epoch to_epoch
155
- from_epoch=$(date -u -d "${from_date}T00:00:00Z" +%s 2>/dev/null || echo 0)
156
- to_epoch=$(date -u -d "${to_date}T23:59:59Z" +%s 2>/dev/null || echo 0)
126
+ from_epoch=$(date_to_epoch "${from_date}T00:00:00Z")
127
+ to_epoch=$(date_to_epoch "${to_date}T23:59:59Z")
157
128
 
158
- jq -s --argjson from "$from_epoch" --argjson to "$to_epoch" '
159
- [.[] | select(.ts_epoch >= $from and .ts_epoch <= $to)] as $events |
129
+ db_query_events_since "$from_epoch" "" "$to_epoch" | jq --argjson from "$from_epoch" --argjson to "$to_epoch" '
130
+ (if type == "array" then . else [] end) as $events |
160
131
  [$events[] | select(.type == "pipeline.completed" and (.agent_id // .agent))] as $completions |
161
132
  $completions | group_by(.agent_id // .agent) | map({
162
133
  agent: .[0].agent_id // .[0].agent,
@@ -166,7 +137,7 @@ analyze_agent_performance() {
166
137
  avg_duration: (([.[].duration_s // 0] | add / length) | floor)
167
138
  }) | sort_by(-.completed) as $agent_stats |
168
139
  { agents: $agent_stats }
169
- ' "$events_file" 2>/dev/null || echo '{"agents":[]}'
140
+ ' 2>/dev/null || echo '{"agents":[]}'
170
141
  }
171
142
 
172
143
  # ─── Velocity & Trends ──────────────────────────────────────────────────────
@@ -174,21 +145,15 @@ analyze_velocity() {
174
145
  local from_date="$1"
175
146
  local to_date="$2"
176
147
 
177
- local events_file="${HOME}/.shipwright/events.jsonl"
178
- if [[ ! -f "$events_file" ]]; then
148
+ if ! command -v jq >/dev/null 2>&1; then
179
149
  echo '{"current":0,"previous":0,"trend":"→"}'
180
150
  return 0
181
151
  fi
182
152
 
183
- if ! command -v jq &>/dev/null; then
184
- echo '{"current":0,"previous":0,"trend":"→"}'
185
- return 0
186
- fi
187
-
188
- # Get current period
153
+ # Get current period (cross-platform: date_to_epoch from compat.sh)
189
154
  local from_epoch to_epoch prev_from_epoch prev_to_epoch
190
- from_epoch=$(date -u -d "${from_date}T00:00:00Z" +%s 2>/dev/null || echo 0)
191
- to_epoch=$(date -u -d "${to_date}T23:59:59Z" +%s 2>/dev/null || echo 0)
155
+ from_epoch=$(date_to_epoch "${from_date}T00:00:00Z")
156
+ to_epoch=$(date_to_epoch "${to_date}T23:59:59Z")
192
157
 
193
158
  # Get previous period (same duration before current)
194
159
  local duration_days
@@ -196,24 +161,26 @@ analyze_velocity() {
196
161
  prev_to_epoch=$from_epoch
197
162
  prev_from_epoch=$((from_epoch - (duration_days * 86400)))
198
163
 
199
- jq -s --argjson curr_from "$from_epoch" --argjson curr_to "$to_epoch" \
164
+ # Need full event range for current + previous; use db_query_events since prev_from may be before from
165
+ db_query_events "" 10000 | jq --argjson curr_from "$from_epoch" --argjson curr_to "$to_epoch" \
200
166
  --argjson prev_from "$prev_from_epoch" --argjson prev_to "$prev_to_epoch" '
201
- [.[] | select(.ts_epoch >= $curr_from and .ts_epoch <= $curr_to and .type == "pipeline.completed" and .result == "success")] | length as $current |
202
- [.[] | select(.ts_epoch >= $prev_from and .ts_epoch <= $prev_to and .type == "pipeline.completed" and .result == "success")] | length as $previous |
167
+ (if type == "array" then . else [] end) as $all |
168
+ [$all[] | select(.ts_epoch >= $curr_from and .ts_epoch <= $curr_to and .type == "pipeline.completed" and .result == "success")] | length as $current |
169
+ [$all[] | select(.ts_epoch >= $prev_from and .ts_epoch <= $prev_to and .type == "pipeline.completed" and .result == "success")] | length as $previous |
203
170
  (if $previous > 0 and $current > $previous then "↑" elif $current < $previous then "↓" else "→" end) as $trend |
204
171
  {
205
172
  current: $current,
206
173
  previous: $previous,
207
174
  trend: $trend
208
175
  }
209
- ' "$events_file" 2>/dev/null || echo '{"current":0,"previous":0,"trend":"→"}'
176
+ ' 2>/dev/null || echo '{"current":0,"previous":0,"trend":"→"}'
210
177
  }
211
178
 
212
179
  # ─── Generate Insights & Actions ────────────────────────────────────────────
213
180
  generate_improvement_actions() {
214
181
  local analysis_json="$1"
215
182
 
216
- if ! command -v jq &>/dev/null; then
183
+ if ! command -v jq >/dev/null 2>&1; then
217
184
  echo '{"actions":[]}'
218
185
  return 0
219
186
  fi
@@ -253,18 +220,18 @@ generate_improvement_actions() {
253
220
  create_action_issues() {
254
221
  local actions_json="$1"
255
222
 
256
- if ! command -v gh &>/dev/null; then
223
+ if ! command -v gh >/dev/null 2>&1; then
257
224
  warn "GitHub CLI (gh) not found. Skipping issue creation."
258
225
  return 1
259
226
  fi
260
227
 
261
- if ! command -v jq &>/dev/null; then
228
+ if ! command -v jq >/dev/null 2>&1; then
262
229
  warn "jq not found. Skipping issue creation."
263
230
  return 1
264
231
  fi
265
232
 
266
233
  local action_count
267
- action_count=$(echo "$actions_json" | jq '.actions | length')
234
+ action_count=$(echo "$actions_json" | jq '.actions | length' 2>/dev/null || echo "0")
268
235
 
269
236
  for ((i = 0; i < action_count; i++)); do
270
237
  local title description label priority
@@ -367,7 +334,7 @@ generate_retro_report() {
367
334
  } > "$report_file"
368
335
 
369
336
  # Add agent stats
370
- if command -v jq &>/dev/null; then
337
+ if command -v jq >/dev/null 2>&1; then
371
338
  local agent_count
372
339
  agent_count=$(echo "$agent_json" | jq '.agents | length' 2>/dev/null || echo 0)
373
340
  for ((i = 0; i < agent_count; i++)); do
@@ -446,12 +413,12 @@ cmd_run() {
446
413
 
447
414
  # Display summary
448
415
  echo -e "${BOLD}Sprint Summary${RESET}"
449
- if command -v jq &>/dev/null; then
416
+ if command -v jq >/dev/null 2>&1; then
450
417
  local pipelines succeeded failed quality_score
451
- pipelines=$(echo "$analysis" | jq -r '.pipelines')
452
- succeeded=$(echo "$analysis" | jq -r '.succeeded')
453
- failed=$(echo "$analysis" | jq -r '.failed')
454
- quality_score=$(echo "$analysis" | jq -r '.quality_score')
418
+ pipelines=$(echo "$analysis" | jq -r '.pipelines // 0')
419
+ succeeded=$(echo "$analysis" | jq -r '.succeeded // 0')
420
+ failed=$(echo "$analysis" | jq -r '.failed // 0')
421
+ quality_score=$(echo "$analysis" | jq -r '.quality_score // 0')
455
422
 
456
423
  echo "Pipelines: $pipelines total | ${GREEN}$succeeded succeeded${RESET} | ${RED}$failed failed${RESET}"
457
424
  echo "Success Rate: ${quality_score}%"
@@ -469,7 +436,7 @@ cmd_run() {
469
436
  [[ -f "$SCRIPT_DIR/sw-self-optimize.sh" ]] && "$SCRIPT_DIR/sw-self-optimize.sh" ingest-retro || true
470
437
 
471
438
  # Offer to create issues
472
- if command -v gh &>/dev/null; then
439
+ if command -v gh >/dev/null 2>&1; then
473
440
  echo ""
474
441
  info "Create improvement issues? (y/n)"
475
442
  read -r -t 5 response || response="n"
@@ -492,7 +459,7 @@ cmd_summary() {
492
459
  local analysis
493
460
  analysis=$(analyze_sprint_data "$from_date" "$to_date")
494
461
 
495
- if command -v jq &>/dev/null; then
462
+ if command -v jq >/dev/null 2>&1; then
496
463
  echo "$analysis" | jq '.'
497
464
  else
498
465
  echo "$analysis"
@@ -503,8 +470,9 @@ cmd_trends() {
503
470
  info "Multi-Sprint Trend Analysis"
504
471
  echo ""
505
472
 
506
- local events_file="${HOME}/.shipwright/events.jsonl"
507
- if [[ ! -f "$events_file" ]]; then
473
+ local event_check
474
+ event_check=$(db_query_events "" 1 2>/dev/null || echo "[]")
475
+ if [[ "$event_check" == "[]" ]] || [[ -z "$event_check" ]]; then
508
476
  error "No event data found. Run pipelines first."
509
477
  return 1
510
478
  fi
@@ -518,15 +486,13 @@ cmd_trends() {
518
486
  offset_end=$((i * 7))
519
487
  offset_start=$(((i + 1) * 7))
520
488
 
521
- end_date=$(date -u -v-${offset_end}d +"%Y-%m-%d" 2>/dev/null || \
522
- date -u -d "${offset_end} days ago" +"%Y-%m-%d" 2>/dev/null || echo "$today")
523
- start_date=$(date -u -v-${offset_start}d +"%Y-%m-%d" 2>/dev/null || \
524
- date -u -d "${offset_start} days ago" +"%Y-%m-%d" 2>/dev/null || echo "$today")
489
+ end_date=$(date_days_ago "$offset_end" 2>/dev/null || echo "$today")
490
+ start_date=$(date_days_ago "$offset_start" 2>/dev/null || echo "$today")
525
491
 
526
492
  local analysis
527
493
  analysis=$(analyze_sprint_data "$start_date" "$end_date")
528
494
 
529
- if command -v jq &>/dev/null; then
495
+ if command -v jq >/dev/null 2>&1; then
530
496
  local quality pipelines
531
497
  quality=$(echo "$analysis" | jq -r '.quality_score')
532
498
  pipelines=$(echo "$analysis" | jq -r '.pipelines')
@@ -548,7 +514,7 @@ cmd_agents() {
548
514
  local agent_perf
549
515
  agent_perf=$(analyze_agent_performance "$from_date" "$to_date")
550
516
 
551
- if command -v jq &>/dev/null; then
517
+ if command -v jq >/dev/null 2>&1; then
552
518
  echo "$agent_perf" | jq '.agents[] | "\(.agent): \(.completed) completed, \(.succeeded) succeeded, \(.failed) failed"' -r
553
519
  else
554
520
  echo "$agent_perf"
@@ -569,7 +535,7 @@ cmd_actions() {
569
535
  analysis=$(analyze_sprint_data "$from_date" "$to_date")
570
536
  improvements=$(generate_improvement_actions "$analysis")
571
537
 
572
- if command -v jq &>/dev/null; then
538
+ if command -v jq >/dev/null 2>&1; then
573
539
  echo "$improvements" | jq '.actions[] | "\(.priority | ascii_upcase): \(.title)\n \(.description)"' -r
574
540
  else
575
541
  echo "$improvements"
@@ -589,10 +555,10 @@ cmd_compare() {
589
555
  echo ""
590
556
 
591
557
  local analysis1 analysis2
592
- analysis1=$(analyze_sprint_data "$period1" "$(date -u -d "${period1} + 7 days" +"%Y-%m-%d")")
593
- analysis2=$(analyze_sprint_data "$period2" "$(date -u -d "${period2} + 7 days" +"%Y-%m-%d")")
558
+ analysis1=$(analyze_sprint_data "$period1" "$(date_add_days "$period1" 7)")
559
+ analysis2=$(analyze_sprint_data "$period2" "$(date_add_days "$period2" 7)")
594
560
 
595
- if command -v jq &>/dev/null; then
561
+ if command -v jq >/dev/null 2>&1; then
596
562
  echo "Sprint 1 (${period1}):"
597
563
  echo "$analysis1" | jq '.'
598
564
  echo ""
@@ -623,12 +589,13 @@ Usage: shipwright retro <subcommand> [options]
623
589
  Subcommands:
624
590
  run [--from DATE] [--to DATE] Run retrospective for sprint (default: last 7 days)
625
591
  summary [DATE1] [DATE2] Quick sprint summary stats
626
- trends Multi-sprint trend analysis (last 4 sprints)
592
+ trends Multi-sprint trend analysis (last 4 sprints)
627
593
  agents [DATE1] [DATE2] Agent performance breakdown
628
594
  actions [DATE1] [DATE2] List generated improvement actions
629
- compare DATE1 DATE2 Compare two sprint periods
630
- history Show past retrospective reports
631
- help Show this help message
595
+ compare DATE1 DATE2 Compare two sprint periods
596
+ history Show past retrospective reports
597
+ quality Show quality index trend (longitudinal)
598
+ help Show this help message
632
599
 
633
600
  Options:
634
601
  --from DATE Start date (YYYY-MM-DD)
@@ -673,6 +640,17 @@ if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then
673
640
  history)
674
641
  cmd_history "$@"
675
642
  ;;
643
+ quality)
644
+ if [[ -f "$SCRIPT_DIR/sw-self-optimize.sh" ]]; then
645
+ source "$SCRIPT_DIR/sw-self-optimize.sh" 2>/dev/null || true
646
+ cmd_quality_index 2>/dev/null || {
647
+ echo "No quality data yet. Run some pipelines first."
648
+ }
649
+ else
650
+ error "sw-self-optimize.sh not found"
651
+ exit 1
652
+ fi
653
+ ;;
676
654
  help|--help|-h)
677
655
  cmd_help
678
656
  ;;
@@ -7,7 +7,7 @@
7
7
  set -euo pipefail
8
8
  trap 'echo "ERROR: $BASH_SOURCE:$LINENO exited with status $?" >&2' ERR
9
9
 
10
- VERSION="2.4.0"
10
+ VERSION="3.1.0"
11
11
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
12
12
  REPO_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
13
13