shipwright-cli 2.2.2 → 2.3.1

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 (151) hide show
  1. package/README.md +12 -11
  2. package/dashboard/public/index.html +224 -8
  3. package/dashboard/public/styles.css +1078 -4
  4. package/dashboard/server.ts +1100 -15
  5. package/dashboard/src/canvas/interactions.ts +74 -0
  6. package/dashboard/src/canvas/layout.ts +85 -0
  7. package/dashboard/src/canvas/overlays.ts +117 -0
  8. package/dashboard/src/canvas/particles.ts +105 -0
  9. package/dashboard/src/canvas/renderer.ts +191 -0
  10. package/dashboard/src/components/charts/bar.ts +54 -0
  11. package/dashboard/src/components/charts/donut.ts +25 -0
  12. package/dashboard/src/components/charts/pipeline-rail.ts +105 -0
  13. package/dashboard/src/components/charts/sparkline.ts +82 -0
  14. package/dashboard/src/components/header.ts +616 -0
  15. package/dashboard/src/components/modal.ts +413 -0
  16. package/dashboard/src/components/terminal.ts +144 -0
  17. package/dashboard/src/core/api.test.ts +362 -0
  18. package/dashboard/src/core/api.ts +381 -0
  19. package/dashboard/src/core/helpers.ts +118 -0
  20. package/dashboard/src/core/router.test.ts +266 -0
  21. package/dashboard/src/core/router.ts +190 -0
  22. package/dashboard/src/core/sse.ts +38 -0
  23. package/dashboard/src/core/state.test.ts +235 -0
  24. package/dashboard/src/core/state.ts +150 -0
  25. package/dashboard/src/core/ws.test.ts +216 -0
  26. package/dashboard/src/core/ws.ts +143 -0
  27. package/dashboard/src/design/icons.test.ts +105 -0
  28. package/dashboard/src/design/icons.ts +131 -0
  29. package/dashboard/src/design/tokens.test.ts +204 -0
  30. package/dashboard/src/design/tokens.ts +160 -0
  31. package/dashboard/src/main.ts +68 -0
  32. package/dashboard/src/types/api.ts +337 -0
  33. package/dashboard/src/views/activity.ts +185 -0
  34. package/dashboard/src/views/agent-cockpit.ts +236 -0
  35. package/dashboard/src/views/agents.ts +72 -0
  36. package/dashboard/src/views/fleet-map.ts +299 -0
  37. package/dashboard/src/views/insights.ts +298 -0
  38. package/dashboard/src/views/machines.ts +162 -0
  39. package/dashboard/src/views/metrics.ts +420 -0
  40. package/dashboard/src/views/overview.ts +409 -0
  41. package/dashboard/src/views/pipeline-theater.ts +219 -0
  42. package/dashboard/src/views/pipelines.ts +595 -0
  43. package/dashboard/src/views/team.ts +362 -0
  44. package/dashboard/src/views/timeline.ts +389 -0
  45. package/dashboard/tsconfig.json +21 -0
  46. package/dashboard/vitest.config.ts +27 -0
  47. package/docs/AGI-WHATS-NEXT.md +15 -15
  48. package/package.json +16 -2
  49. package/scripts/lib/helpers.sh +30 -0
  50. package/scripts/lib/pipeline-quality-checks.sh +1 -1
  51. package/scripts/lib/pipeline-stages.sh +59 -0
  52. package/scripts/sw +86 -167
  53. package/scripts/sw-activity.sh +1 -1
  54. package/scripts/sw-adaptive.sh +1 -1
  55. package/scripts/sw-adversarial.sh +1 -1
  56. package/scripts/sw-architecture-enforcer.sh +1 -1
  57. package/scripts/sw-auth.sh +14 -6
  58. package/scripts/sw-autonomous.sh +230 -13
  59. package/scripts/sw-changelog.sh +2 -2
  60. package/scripts/sw-checkpoint.sh +1 -1
  61. package/scripts/sw-ci.sh +1 -1
  62. package/scripts/sw-cleanup.sh +1 -1
  63. package/scripts/sw-code-review.sh +1 -1
  64. package/scripts/sw-connect.sh +1 -1
  65. package/scripts/sw-context.sh +1 -1
  66. package/scripts/sw-cost.sh +1 -1
  67. package/scripts/sw-daemon.sh +2 -2
  68. package/scripts/sw-dashboard.sh +1 -1
  69. package/scripts/sw-db.sh +1 -1
  70. package/scripts/sw-decompose.sh +1 -1
  71. package/scripts/sw-deps.sh +1 -1
  72. package/scripts/sw-developer-simulation.sh +1 -1
  73. package/scripts/sw-discovery.sh +1 -1
  74. package/scripts/sw-doc-fleet.sh +1 -1
  75. package/scripts/sw-docs-agent.sh +1 -1
  76. package/scripts/sw-docs.sh +1 -1
  77. package/scripts/sw-doctor.sh +8 -1
  78. package/scripts/sw-dora.sh +1 -1
  79. package/scripts/sw-durable.sh +1 -1
  80. package/scripts/sw-e2e-orchestrator.sh +1 -1
  81. package/scripts/sw-eventbus.sh +1 -1
  82. package/scripts/sw-feedback.sh +1 -1
  83. package/scripts/sw-fix.sh +6 -5
  84. package/scripts/sw-fleet-discover.sh +1 -1
  85. package/scripts/sw-fleet-viz.sh +1 -1
  86. package/scripts/sw-fleet.sh +1 -1
  87. package/scripts/sw-github-app.sh +5 -2
  88. package/scripts/sw-github-checks.sh +1 -1
  89. package/scripts/sw-github-deploy.sh +1 -1
  90. package/scripts/sw-github-graphql.sh +1 -1
  91. package/scripts/sw-guild.sh +1 -1
  92. package/scripts/sw-heartbeat.sh +1 -1
  93. package/scripts/sw-hygiene.sh +1 -1
  94. package/scripts/sw-incident.sh +1 -1
  95. package/scripts/sw-init.sh +112 -9
  96. package/scripts/sw-instrument.sh +6 -1
  97. package/scripts/sw-intelligence.sh +5 -1
  98. package/scripts/sw-jira.sh +1 -1
  99. package/scripts/sw-launchd.sh +1 -1
  100. package/scripts/sw-linear.sh +20 -9
  101. package/scripts/sw-logs.sh +1 -1
  102. package/scripts/sw-loop.sh +2 -1
  103. package/scripts/sw-memory.sh +10 -1
  104. package/scripts/sw-mission-control.sh +1 -1
  105. package/scripts/sw-model-router.sh +4 -1
  106. package/scripts/sw-otel.sh +1 -1
  107. package/scripts/sw-oversight.sh +1 -1
  108. package/scripts/sw-pipeline-composer.sh +3 -1
  109. package/scripts/sw-pipeline-vitals.sh +4 -6
  110. package/scripts/sw-pipeline.sh +4 -1
  111. package/scripts/sw-pm.sh +5 -2
  112. package/scripts/sw-pr-lifecycle.sh +1 -1
  113. package/scripts/sw-predictive.sh +4 -1
  114. package/scripts/sw-prep.sh +3 -2
  115. package/scripts/sw-ps.sh +1 -1
  116. package/scripts/sw-public-dashboard.sh +10 -4
  117. package/scripts/sw-quality.sh +1 -1
  118. package/scripts/sw-reaper.sh +1 -1
  119. package/scripts/sw-recruit.sh +16 -0
  120. package/scripts/sw-regression.sh +2 -1
  121. package/scripts/sw-release-manager.sh +1 -1
  122. package/scripts/sw-release.sh +7 -5
  123. package/scripts/sw-remote.sh +1 -1
  124. package/scripts/sw-replay.sh +1 -1
  125. package/scripts/sw-retro.sh +4 -1
  126. package/scripts/sw-scale.sh +4 -1
  127. package/scripts/sw-security-audit.sh +1 -1
  128. package/scripts/sw-self-optimize.sh +113 -1
  129. package/scripts/sw-session.sh +1 -1
  130. package/scripts/sw-setup.sh +1 -1
  131. package/scripts/sw-standup.sh +2 -1
  132. package/scripts/sw-status.sh +1 -1
  133. package/scripts/sw-strategic.sh +2 -1
  134. package/scripts/sw-stream.sh +1 -1
  135. package/scripts/sw-swarm.sh +6 -1
  136. package/scripts/sw-team-stages.sh +1 -1
  137. package/scripts/sw-templates.sh +1 -1
  138. package/scripts/sw-testgen.sh +3 -2
  139. package/scripts/sw-tmux-pipeline.sh +2 -1
  140. package/scripts/sw-tmux.sh +1 -1
  141. package/scripts/sw-trace.sh +1 -1
  142. package/scripts/sw-tracker-jira.sh +1 -0
  143. package/scripts/sw-tracker-linear.sh +1 -0
  144. package/scripts/sw-tracker.sh +1 -1
  145. package/scripts/sw-triage.sh +198 -11
  146. package/scripts/sw-upgrade.sh +1 -1
  147. package/scripts/sw-ux.sh +1 -1
  148. package/scripts/sw-webhook.sh +1 -1
  149. package/scripts/sw-widgets.sh +2 -2
  150. package/scripts/sw-worktree.sh +1 -1
  151. package/dashboard/public/app.js +0 -4422
@@ -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.2.2"
9
+ VERSION="2.3.1"
10
10
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
11
11
  REPO_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
12
12
 
@@ -165,6 +165,125 @@ analyze_effort() {
165
165
  esac
166
166
  }
167
167
 
168
+ # analyze_with_ai <title> <body>
169
+ # AI-driven triage via intelligence engine. Returns JSON or empty on failure.
170
+ # Schema: {type, complexity, risk, effort, labels[], summary}
171
+ # Always falls back to keyword-based analysis when AI unavailable.
172
+ analyze_with_ai() {
173
+ local title="$1"
174
+ local body="$2"
175
+ local combined="${title} ${body}"
176
+
177
+ # Check sw-intelligence.sh is sourceable and claude CLI exists
178
+ if [[ ! -f "${SCRIPT_DIR}/sw-intelligence.sh" ]]; then
179
+ return 1
180
+ fi
181
+ if ! command -v claude &>/dev/null; then
182
+ return 1
183
+ fi
184
+
185
+ # Source intelligence (provides _intelligence_call_claude, compute_md5)
186
+ if ! source "${SCRIPT_DIR}/sw-intelligence.sh" 2>/dev/null; then
187
+ return 1
188
+ fi
189
+
190
+ local prompt
191
+ prompt="Analyze this GitHub issue and return ONLY a valid JSON object (no markdown, no explanation).
192
+
193
+ Title: ${title}
194
+
195
+ Body: ${body}
196
+
197
+ Return JSON with exactly these fields:
198
+ {
199
+ \"type\": \"<bug|feature|security|performance|refactor|docs|chore>\",
200
+ \"complexity\": \"<trivial|simple|moderate|complex|epic>\",
201
+ \"risk\": \"<low|medium|high|critical>\",
202
+ \"effort\": \"<xs|s|m|l|xl>\",
203
+ \"labels\": [\"type:X\", \"complexity:X\", \"risk:X\", \"priority:X\", \"effort:X\"],
204
+ \"summary\": \"<brief one-line summary>\"
205
+ }"
206
+
207
+ local cache_key
208
+ cache_key="triage_analyze_$(compute_md5 --string "$combined" 2>/dev/null || echo "$(echo "$combined" | md5 2>/dev/null | cut -c1-16)")"
209
+
210
+ local result
211
+ result=$(_intelligence_call_claude "$prompt" "$cache_key" 2>/dev/null) || true
212
+
213
+ if [[ -z "$result" ]] || echo "$result" | jq -e '.' &>/dev/null; then
214
+ : # result is empty or valid JSON
215
+ else
216
+ return 1
217
+ fi
218
+
219
+ # Validate required fields and normalize
220
+ local type_val complexity_val risk_val effort_val labels_val
221
+ type_val=$(echo "$result" | jq -r '.type // empty' 2>/dev/null)
222
+ complexity_val=$(echo "$result" | jq -r '.complexity // empty' 2>/dev/null)
223
+ risk_val=$(echo "$result" | jq -r '.risk // empty' 2>/dev/null)
224
+ effort_val=$(echo "$result" | jq -r '.effort // empty' 2>/dev/null)
225
+ labels_val=$(echo "$result" | jq -r '.labels // []' 2>/dev/null)
226
+
227
+ # Reject if we got an error object
228
+ if echo "$result" | jq -e '.error' &>/dev/null; then
229
+ return 1
230
+ fi
231
+
232
+ # Need at least type or complexity to consider AI result useful
233
+ if [[ -z "$type_val" && -z "$complexity_val" && -z "$risk_val" ]]; then
234
+ return 1
235
+ fi
236
+
237
+ # Normalize to valid triage schema values
238
+ local valid_type
239
+ case "$(echo "$type_val" | tr '[:upper:]' '[:lower:]')" in
240
+ bug) valid_type="bug" ;;
241
+ feature) valid_type="feature" ;;
242
+ security) valid_type="security" ;;
243
+ performance) valid_type="performance" ;;
244
+ refactor) valid_type="refactor" ;;
245
+ docs) valid_type="docs" ;;
246
+ chore) valid_type="chore" ;;
247
+ *) valid_type="" ;;
248
+ esac
249
+ local valid_complexity
250
+ case "$(echo "$complexity_val" | tr '[:upper:]' '[:lower:]')" in
251
+ trivial) valid_complexity="trivial" ;;
252
+ simple) valid_complexity="simple" ;;
253
+ moderate) valid_complexity="moderate" ;;
254
+ complex) valid_complexity="complex" ;;
255
+ epic) valid_complexity="epic" ;;
256
+ *) valid_complexity="" ;;
257
+ esac
258
+ local valid_risk
259
+ case "$(echo "$risk_val" | tr '[:upper:]' '[:lower:]')" in
260
+ low) valid_risk="low" ;;
261
+ medium) valid_risk="medium" ;;
262
+ high) valid_risk="high" ;;
263
+ critical) valid_risk="critical" ;;
264
+ *) valid_risk="" ;;
265
+ esac
266
+ local valid_effort
267
+ case "$(echo "$effort_val" | tr '[:upper:]' '[:lower:]')" in
268
+ xs) valid_effort="xs" ;;
269
+ s) valid_effort="s" ;;
270
+ m) valid_effort="m" ;;
271
+ l) valid_effort="l" ;;
272
+ xl) valid_effort="xl" ;;
273
+ *) valid_effort="" ;;
274
+ esac
275
+
276
+ jq -n \
277
+ --arg type "${valid_type:-}" \
278
+ --arg complexity "${valid_complexity:-}" \
279
+ --arg risk "${valid_risk:-}" \
280
+ --arg effort "${valid_effort:-}" \
281
+ --argjson labels "${labels_val:-[]}" \
282
+ --arg summary "$(echo "$result" | jq -r '.summary // ""' 2>/dev/null)" \
283
+ '{type: $type, complexity: $complexity, risk: $risk, effort: $effort, labels: $labels, summary: $summary}'
284
+ return 0
285
+ }
286
+
168
287
  # suggest_labels <type> <complexity> <risk> <effort>
169
288
  # Generates label recommendations
170
289
  suggest_labels() {
@@ -199,9 +318,37 @@ suggest_labels() {
199
318
 
200
319
  # ─── Subcommand: analyze ──────────────────────────────────────────────────
201
320
 
321
+ # Check if AI triage should be used (TRIAGE_AI env, --ai flag, or daemon-config)
322
+ _triage_use_ai() {
323
+ if [[ "${TRIAGE_AI:-}" == "1" || "${TRIAGE_AI:-}" == "true" ]]; then
324
+ return 0
325
+ fi
326
+ local config="${REPO_DIR}/.claude/daemon-config.json"
327
+ if [[ -f "$config" ]]; then
328
+ local enabled
329
+ enabled=$(jq -r '.intelligence.enabled // false' "$config" 2>/dev/null || echo "false")
330
+ [[ "$enabled" == "true" ]]
331
+ else
332
+ return 1
333
+ fi
334
+ }
335
+
202
336
  cmd_analyze() {
203
- local issue="${1:-}"
204
- [[ -z "$issue" ]] && { error "Usage: triage analyze <issue>"; exit 1; }
337
+ local issue=""
338
+ local use_ai=false
339
+
340
+ # Parse args for --ai and issue number
341
+ while [[ $# -gt 0 ]]; do
342
+ case "$1" in
343
+ --ai) use_ai=true; shift ;;
344
+ *) [[ -z "$issue" ]] && issue="$1"; shift ;;
345
+ esac
346
+ done
347
+
348
+ [[ -z "$issue" ]] && { error "Usage: triage analyze [--ai] <issue>"; exit 1; }
349
+
350
+ # Enable AI if --ai flag or config
351
+ _triage_use_ai && use_ai=true
205
352
 
206
353
  check_gh
207
354
 
@@ -223,13 +370,45 @@ cmd_analyze() {
223
370
 
224
371
  local combined_text="${title} ${body}"
225
372
 
226
- # Analyze
227
- local type complexity risk effort labels
228
- type=$(analyze_type "$combined_text")
229
- complexity=$(analyze_complexity "$combined_text")
230
- risk=$(analyze_risk "$combined_text")
231
- effort=$(analyze_effort "$complexity" "$risk")
232
- labels=$(suggest_labels "$type" "$complexity" "$risk" "$effort")
373
+ # Keyword-based analysis (always run for fallback)
374
+ local kw_type kw_complexity kw_risk kw_effort kw_labels
375
+ kw_type=$(analyze_type "$combined_text")
376
+ kw_complexity=$(analyze_complexity "$combined_text")
377
+ kw_risk=$(analyze_risk "$combined_text")
378
+ kw_effort=$(analyze_effort "$kw_complexity" "$kw_risk")
379
+ kw_labels=$(suggest_labels "$kw_type" "$kw_complexity" "$kw_risk" "$kw_effort")
380
+
381
+ # Try AI analysis first when enabled
382
+ local type="$kw_type" complexity="$kw_complexity" risk="$kw_risk" effort="$kw_effort" labels="$kw_labels"
383
+ if $use_ai; then
384
+ local ai_result
385
+ if ai_result=$(analyze_with_ai "$title" "$body" 2>/dev/null); then
386
+ local ai_type ai_complexity ai_risk ai_effort ai_labels
387
+ ai_type=$(echo "$ai_result" | jq -r '.type // empty')
388
+ ai_complexity=$(echo "$ai_result" | jq -r '.complexity // empty')
389
+ ai_risk=$(echo "$ai_result" | jq -r '.risk // empty')
390
+ ai_effort=$(echo "$ai_result" | jq -r '.effort // empty')
391
+ ai_labels=$(echo "$ai_result" | jq -r '.labels // []' 2>/dev/null)
392
+
393
+ # Merge: AI takes precedence where available
394
+ [[ -n "$ai_type" ]] && type="$ai_type"
395
+ [[ -n "$ai_complexity" ]] && complexity="$ai_complexity"
396
+ [[ -n "$ai_risk" ]] && risk="$ai_risk"
397
+ if [[ -n "$ai_effort" ]]; then
398
+ effort="$ai_effort"
399
+ else
400
+ effort=$(analyze_effort "$complexity" "$risk")
401
+ fi
402
+ if [[ -n "$ai_labels" && "$ai_labels" != "[]" ]]; then
403
+ labels=$(echo "$ai_labels" | jq -r 'join(" ")')
404
+ else
405
+ labels=$(suggest_labels "$type" "$complexity" "$risk" "$effort")
406
+ fi
407
+ info "AI triage applied"
408
+ else
409
+ warn "AI triage unavailable, using keyword analysis"
410
+ fi
411
+ fi
233
412
 
234
413
  # Output as structured JSON
235
414
  cat << EOF
@@ -575,7 +754,7 @@ cmd_help() {
575
754
  echo -e " ${CYAN}shipwright triage${RESET} <subcommand> [options]"
576
755
  echo ""
577
756
  echo -e "${BOLD}SUBCOMMANDS${RESET}"
578
- echo -e " ${CYAN}analyze <issue>${RESET} Analyze issue and suggest labels (outputs JSON)"
757
+ echo -e " ${CYAN}analyze [--ai] <issue>${RESET} Analyze issue and suggest labels (outputs JSON)"
579
758
  echo -e " ${CYAN}label <issue>${RESET} Apply suggested labels to issue"
580
759
  echo -e " ${CYAN}prioritize${RESET} Score and rank all open issues by priority"
581
760
  echo -e " ${CYAN}team <issue>${RESET} Recommend team size & pipeline template"
@@ -585,6 +764,8 @@ cmd_help() {
585
764
  echo ""
586
765
  echo -e "${BOLD}EXAMPLES${RESET}"
587
766
  echo -e " ${DIM}shipwright triage analyze 42${RESET}"
767
+ echo -e " ${DIM}shipwright triage analyze --ai 42${RESET}"
768
+ echo -e " ${DIM}shipwright triage --ai analyze 42${RESET}"
588
769
  echo -e " ${DIM}shipwright triage label 42${RESET}"
589
770
  echo -e " ${DIM}shipwright triage prioritize${RESET}"
590
771
  echo -e " ${DIM}shipwright triage team 42${RESET}"
@@ -596,6 +777,12 @@ cmd_help() {
596
777
  # ─── Main ──────────────────────────────────────────────────────────────────
597
778
 
598
779
  main() {
780
+ # Parse global --ai flag (enables AI triage for this invocation)
781
+ if [[ "${1:-}" == "--ai" ]]; then
782
+ export TRIAGE_AI=1
783
+ shift
784
+ fi
785
+
599
786
  local cmd="${1:-help}"
600
787
  shift 2>/dev/null || true
601
788
 
@@ -2,7 +2,7 @@
2
2
  # ╔═══════════════════════════════════════════════════════════════════════════╗
3
3
  # ║ sw upgrade — Detect and apply updates from the repo ║
4
4
  # ╚═══════════════════════════════════════════════════════════════════════════╝
5
- VERSION="2.2.2"
5
+ VERSION="2.3.1"
6
6
  set -euo pipefail
7
7
  trap 'echo "ERROR: $BASH_SOURCE:$LINENO exited with status $?" >&2' ERR
8
8
 
package/scripts/sw-ux.sh CHANGED
@@ -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.2.2"
9
+ VERSION="2.3.1"
10
10
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
11
11
 
12
12
  # ─── Cross-platform compatibility ──────────────────────────────────────────
@@ -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.2.2"
9
+ VERSION="2.3.1"
10
10
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
11
11
 
12
12
  # ─── Cross-platform compatibility ──────────────────────────────────────────
@@ -8,7 +8,7 @@
8
8
  set -euo pipefail
9
9
  trap 'echo "ERROR: $BASH_SOURCE:$LINENO exited with status $?" >&2' ERR
10
10
 
11
- VERSION="2.2.2"
11
+ VERSION="2.3.1"
12
12
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
13
13
  REPO_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
14
14
 
@@ -357,7 +357,7 @@ To add these badges to your README.md:
357
357
  \`\`\`
358
358
 
359
359
  ---
360
- Generated by [Shipwright](https://github.com/sethdford/shipwright)
360
+ Generated by [Shipwright]($(_sw_github_url))
361
361
  EOF
362
362
  }
363
363
 
@@ -5,7 +5,7 @@
5
5
  # ║ Each agent gets its own worktree so parallel agents don't clobber ║
6
6
  # ║ each other's files. Worktrees live in .worktrees/ relative to root. ║
7
7
  # ╚═══════════════════════════════════════════════════════════════════════════╝
8
- VERSION="2.2.2"
8
+ VERSION="2.3.1"
9
9
  set -euo pipefail
10
10
  trap 'echo "ERROR: $BASH_SOURCE:$LINENO exited with status $?" >&2' ERR
11
11