shipwright-cli 2.3.1 → 3.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.
Files changed (162) hide show
  1. package/README.md +95 -28
  2. package/completions/_shipwright +1 -1
  3. package/completions/shipwright.bash +3 -8
  4. package/completions/shipwright.fish +1 -1
  5. package/config/defaults.json +111 -0
  6. package/config/event-schema.json +81 -0
  7. package/config/policy.json +155 -2
  8. package/config/policy.schema.json +162 -1
  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 +15 -5
  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 +126 -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 +39 -16
  39. package/scripts/lib/daemon-health.sh +1 -1
  40. package/scripts/lib/daemon-patrol.sh +24 -12
  41. package/scripts/lib/daemon-poll.sh +37 -25
  42. package/scripts/lib/daemon-state.sh +115 -23
  43. package/scripts/lib/daemon-triage.sh +30 -8
  44. package/scripts/lib/fleet-failover.sh +63 -0
  45. package/scripts/lib/helpers.sh +30 -6
  46. package/scripts/lib/pipeline-detection.sh +2 -2
  47. package/scripts/lib/pipeline-github.sh +9 -9
  48. package/scripts/lib/pipeline-intelligence.sh +85 -35
  49. package/scripts/lib/pipeline-quality-checks.sh +16 -16
  50. package/scripts/lib/pipeline-quality.sh +1 -1
  51. package/scripts/lib/pipeline-stages.sh +242 -28
  52. package/scripts/lib/pipeline-state.sh +40 -4
  53. package/scripts/lib/test-helpers.sh +247 -0
  54. package/scripts/postinstall.mjs +3 -11
  55. package/scripts/sw +10 -4
  56. package/scripts/sw-activity.sh +1 -11
  57. package/scripts/sw-adaptive.sh +109 -85
  58. package/scripts/sw-adversarial.sh +4 -14
  59. package/scripts/sw-architecture-enforcer.sh +1 -11
  60. package/scripts/sw-auth.sh +8 -17
  61. package/scripts/sw-autonomous.sh +111 -49
  62. package/scripts/sw-changelog.sh +1 -11
  63. package/scripts/sw-checkpoint.sh +144 -20
  64. package/scripts/sw-ci.sh +2 -12
  65. package/scripts/sw-cleanup.sh +13 -17
  66. package/scripts/sw-code-review.sh +16 -36
  67. package/scripts/sw-connect.sh +5 -12
  68. package/scripts/sw-context.sh +9 -26
  69. package/scripts/sw-cost.sh +6 -16
  70. package/scripts/sw-daemon.sh +75 -70
  71. package/scripts/sw-dashboard.sh +57 -17
  72. package/scripts/sw-db.sh +506 -15
  73. package/scripts/sw-decompose.sh +1 -11
  74. package/scripts/sw-deps.sh +15 -25
  75. package/scripts/sw-developer-simulation.sh +1 -11
  76. package/scripts/sw-discovery.sh +112 -30
  77. package/scripts/sw-doc-fleet.sh +7 -17
  78. package/scripts/sw-docs-agent.sh +6 -16
  79. package/scripts/sw-docs.sh +4 -12
  80. package/scripts/sw-doctor.sh +134 -43
  81. package/scripts/sw-dora.sh +11 -19
  82. package/scripts/sw-durable.sh +35 -52
  83. package/scripts/sw-e2e-orchestrator.sh +11 -27
  84. package/scripts/sw-eventbus.sh +115 -115
  85. package/scripts/sw-evidence.sh +748 -0
  86. package/scripts/sw-feedback.sh +3 -13
  87. package/scripts/sw-fix.sh +2 -20
  88. package/scripts/sw-fleet-discover.sh +1 -11
  89. package/scripts/sw-fleet-viz.sh +10 -18
  90. package/scripts/sw-fleet.sh +13 -17
  91. package/scripts/sw-github-app.sh +6 -16
  92. package/scripts/sw-github-checks.sh +1 -11
  93. package/scripts/sw-github-deploy.sh +1 -11
  94. package/scripts/sw-github-graphql.sh +2 -12
  95. package/scripts/sw-guild.sh +1 -11
  96. package/scripts/sw-heartbeat.sh +49 -12
  97. package/scripts/sw-hygiene.sh +45 -43
  98. package/scripts/sw-incident.sh +284 -67
  99. package/scripts/sw-init.sh +35 -37
  100. package/scripts/sw-instrument.sh +1 -11
  101. package/scripts/sw-intelligence.sh +362 -51
  102. package/scripts/sw-jira.sh +5 -14
  103. package/scripts/sw-launchd.sh +2 -12
  104. package/scripts/sw-linear.sh +8 -17
  105. package/scripts/sw-logs.sh +4 -12
  106. package/scripts/sw-loop.sh +641 -90
  107. package/scripts/sw-memory.sh +243 -17
  108. package/scripts/sw-mission-control.sh +2 -12
  109. package/scripts/sw-model-router.sh +73 -34
  110. package/scripts/sw-otel.sh +11 -21
  111. package/scripts/sw-oversight.sh +1 -11
  112. package/scripts/sw-patrol-meta.sh +5 -11
  113. package/scripts/sw-pipeline-composer.sh +7 -17
  114. package/scripts/sw-pipeline-vitals.sh +1 -11
  115. package/scripts/sw-pipeline.sh +478 -122
  116. package/scripts/sw-pm.sh +2 -12
  117. package/scripts/sw-pr-lifecycle.sh +203 -29
  118. package/scripts/sw-predictive.sh +16 -22
  119. package/scripts/sw-prep.sh +6 -16
  120. package/scripts/sw-ps.sh +1 -11
  121. package/scripts/sw-public-dashboard.sh +2 -12
  122. package/scripts/sw-quality.sh +77 -10
  123. package/scripts/sw-reaper.sh +1 -11
  124. package/scripts/sw-recruit.sh +15 -25
  125. package/scripts/sw-regression.sh +11 -21
  126. package/scripts/sw-release-manager.sh +19 -28
  127. package/scripts/sw-release.sh +8 -16
  128. package/scripts/sw-remote.sh +1 -11
  129. package/scripts/sw-replay.sh +48 -44
  130. package/scripts/sw-retro.sh +70 -92
  131. package/scripts/sw-review-rerun.sh +220 -0
  132. package/scripts/sw-scale.sh +109 -32
  133. package/scripts/sw-security-audit.sh +12 -22
  134. package/scripts/sw-self-optimize.sh +239 -23
  135. package/scripts/sw-session.sh +3 -13
  136. package/scripts/sw-setup.sh +8 -18
  137. package/scripts/sw-standup.sh +5 -15
  138. package/scripts/sw-status.sh +32 -23
  139. package/scripts/sw-strategic.sh +129 -13
  140. package/scripts/sw-stream.sh +1 -11
  141. package/scripts/sw-swarm.sh +76 -36
  142. package/scripts/sw-team-stages.sh +10 -20
  143. package/scripts/sw-templates.sh +4 -14
  144. package/scripts/sw-testgen.sh +3 -13
  145. package/scripts/sw-tmux-pipeline.sh +1 -19
  146. package/scripts/sw-tmux-role-color.sh +0 -10
  147. package/scripts/sw-tmux-status.sh +3 -11
  148. package/scripts/sw-tmux.sh +2 -20
  149. package/scripts/sw-trace.sh +1 -19
  150. package/scripts/sw-tracker-github.sh +0 -10
  151. package/scripts/sw-tracker-jira.sh +1 -11
  152. package/scripts/sw-tracker-linear.sh +1 -11
  153. package/scripts/sw-tracker.sh +7 -24
  154. package/scripts/sw-triage.sh +24 -34
  155. package/scripts/sw-upgrade.sh +5 -23
  156. package/scripts/sw-ux.sh +1 -19
  157. package/scripts/sw-webhook.sh +18 -32
  158. package/scripts/sw-widgets.sh +3 -21
  159. package/scripts/sw-worktree.sh +11 -27
  160. package/scripts/update-homebrew-sha.sh +67 -0
  161. package/templates/pipelines/tdd.json +72 -0
  162. 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.3.1"
9
+ VERSION="3.0.0"
10
10
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
11
11
  REPO_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
12
12
 
@@ -16,6 +16,10 @@ REPO_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
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"
22
+ [[ -f "$SCRIPT_DIR/lib/config.sh" ]] && source "$SCRIPT_DIR/lib/config.sh"
19
23
  # Fallbacks when helpers not loaded (e.g. test env with overridden SCRIPT_DIR)
20
24
  [[ "$(type -t info 2>/dev/null)" == "function" ]] || info() { echo -e "\033[38;2;0;212;255m\033[1m▸\033[0m $*"; }
21
25
  [[ "$(type -t success 2>/dev/null)" == "function" ]] || success() { echo -e "\033[38;2;74;222;128m\033[1m✓\033[0m $*"; }
@@ -25,24 +29,6 @@ if [[ "$(type -t now_iso 2>/dev/null)" != "function" ]]; then
25
29
  now_iso() { date -u +"%Y-%m-%dT%H:%M:%SZ"; }
26
30
  now_epoch() { date +%s; }
27
31
  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
-
46
32
  format_duration() {
47
33
  local secs="$1"
48
34
  if [[ "$secs" -ge 3600 ]]; then
@@ -93,50 +79,25 @@ detect_pipeline_failures() {
93
79
  local since="${1:-3600}" # Last N seconds
94
80
  local cutoff_time=$(($(now_epoch) - since))
95
81
 
96
- [[ ! -f "$EVENTS_FILE" ]] && return 0
97
-
98
- awk -v cutoff="$cutoff_time" -F'"' '
99
- BEGIN { count=0 }
100
- /pipeline\.failed|stage\.failed|test\.failed|deploy\.failed/ {
101
- for (i=1; i<=NF; i++) {
102
- if ($i ~ /ts_epoch/) {
103
- ts_epoch_val=$(i+2)
104
- gsub(/^[^0-9]*/, "", ts_epoch_val)
105
- gsub(/[^0-9].*/, "", ts_epoch_val)
106
- if (ts_epoch_val+0 > cutoff) {
107
- print $0
108
- count++
109
- }
110
- }
111
- }
112
- }
113
- END { exit (count > 0 ? 0 : 1) }
114
- ' "$EVENTS_FILE"
82
+ db_query_events_since "$cutoff_time" 2>/dev/null | jq -e 'map(select((.type | tostring) | (test("failed") or test("error") or test("timeout")))) | length > 0' >/dev/null 2>/dev/null && return 0 || return 1
115
83
  }
116
84
 
117
85
  get_recent_failures() {
118
86
  local since="${1:-3600}"
119
87
  local cutoff_time=$(($(now_epoch) - since))
120
88
 
121
- [[ ! -f "$EVENTS_FILE" ]] && echo "[]" && return 0
122
-
123
- jq -s --arg cutoff "$cutoff_time" '
124
- map(
125
- select(
126
- (.ts_epoch | tonumber) > ($cutoff | tonumber) and
127
- (.type | contains("failed") or contains("error") or contains("timeout"))
128
- ) |
129
- {
130
- ts: .ts,
131
- ts_epoch: .ts_epoch,
132
- type: .type,
133
- issue: .issue,
134
- stage: .stage,
135
- reason: .reason,
136
- error: .error
137
- }
138
- )
139
- ' "$EVENTS_FILE" 2>/dev/null || echo "[]"
89
+ db_query_events_since "$cutoff_time" 2>/dev/null | jq '
90
+ map(select((.type | tostring) | (test("failed") or test("error") or test("timeout")))) |
91
+ map({
92
+ ts: .ts,
93
+ ts_epoch: .ts_epoch,
94
+ type: .type,
95
+ issue: .issue,
96
+ stage: .stage,
97
+ reason: .reason,
98
+ error: .error
99
+ })
100
+ ' 2>/dev/null || echo "[]"
140
101
  }
141
102
 
142
103
  # ─── Severity Classification ───────────────────────────────────────────────
@@ -226,7 +187,7 @@ create_hotfix_issue() {
226
187
  local severity="$2"
227
188
  local root_cause="$3"
228
189
 
229
- if ! command -v gh &>/dev/null; then
190
+ if ! command -v gh >/dev/null 2>&1; then
230
191
  warn "gh CLI not found, skipping GitHub issue creation"
231
192
  return 1
232
193
  fi
@@ -245,7 +206,7 @@ This issue was automatically created by the incident commander.
245
206
 
246
207
  # shipwright label so daemon picks up; hotfix for routing
247
208
  local issue_url
248
- issue_url=$(gh issue create --title "$title" --body "$body" --label "hotfix,shipwright" 2>/dev/null || echo "")
209
+ issue_url=$(gh issue create --title "$title" --body "$body" --label "$(_config_get "labels.incident_labels" "hotfix,shipwright")" 2>/dev/null || echo "")
249
210
 
250
211
  if [[ -n "$issue_url" ]]; then
251
212
  success "Created hotfix issue: $issue_url"
@@ -314,7 +275,7 @@ cmd_watch() {
314
275
  local failures_json
315
276
  failures_json=$(get_recent_failures "$interval")
316
277
  local failure_count
317
- failure_count=$(echo "$failures_json" | jq 'length')
278
+ failure_count=$(echo "$failures_json" | jq 'length' 2>/dev/null || echo "0")
318
279
 
319
280
  if [[ "$failure_count" -gt 0 ]]; then
320
281
  info "Detected $failure_count failure(s)"
@@ -334,6 +295,9 @@ cmd_watch() {
334
295
  info "Incident $incident_id created (severity: $severity)"
335
296
  emit_event "incident.detected" "incident_id=$incident_id" "severity=$severity"
336
297
 
298
+ # Create harness gap for test case tracking (Code Factory pattern)
299
+ create_harness_gap "$incident_id" "$severity" "$root_cause" 2>/dev/null || true
300
+
337
301
  # Auto-response for P0/P1: hotfix issue, trigger pipeline, optional rollback
338
302
  if [[ "$severity" == "P0" ]] || [[ "$severity" == "P1" ]]; then
339
303
  local auto_rollback
@@ -507,14 +471,14 @@ cmd_stats() {
507
471
  mttr=$(jq -r '.mttr_seconds // 0' "$incident_file" 2>/dev/null || echo "0")
508
472
 
509
473
  case "$sev" in
510
- P0) ((p0_count++)) ;;
511
- P1) ((p1_count++)) ;;
512
- P2) ((p2_count++)) ;;
513
- *) ((p3_count++)) ;;
474
+ P0) p0_count=$((p0_count + 1)) ;;
475
+ P1) p1_count=$((p1_count + 1)) ;;
476
+ P2) p2_count=$((p2_count + 1)) ;;
477
+ *) p3_count=$((p3_count + 1)) ;;
514
478
  esac
515
479
 
516
480
  if [[ "$status" == "resolved" ]]; then
517
- ((resolved_count++))
481
+ resolved_count=$((resolved_count + 1))
518
482
  mttr_sum=$((mttr_sum + mttr))
519
483
  fi
520
484
  done <<< "$incident_files"
@@ -556,6 +520,242 @@ cmd_stats() {
556
520
  esac
557
521
  }
558
522
 
523
+ # ─── Harness Gap Loop ─────────────────────────────────────────────────────
524
+ # Code Factory pattern: production regression → harness gap issue → test case
525
+ # added → SLA tracked. Every incident must produce a test case within SLA.
526
+
527
+ HARNESS_GAPS_DIR="${INCIDENTS_DIR}/harness-gaps"
528
+
529
+ load_harness_gap_policy() {
530
+ local policy="${REPO_DIR}/config/policy.json"
531
+ if [[ -f "$policy" ]]; then
532
+ HARNESS_GAP_ENABLED=$(jq -r '.harnessGapPolicy.enabled // false' "$policy" 2>/dev/null || echo "false")
533
+ HARNESS_GAP_P0_SLA=$(jq -r '.harnessGapPolicy.p0SlaHours // 24' "$policy" 2>/dev/null || echo "24")
534
+ HARNESS_GAP_P1_SLA=$(jq -r '.harnessGapPolicy.p1SlaHours // 72' "$policy" 2>/dev/null || echo "72")
535
+ HARNESS_GAP_P2_SLA=$(jq -r '.harnessGapPolicy.p2SlaHours // 168' "$policy" 2>/dev/null || echo "168")
536
+ HARNESS_GAP_AUTO_CREATE=$(jq -r '.harnessGapPolicy.autoCreateGapIssue // true' "$policy" 2>/dev/null || echo "true")
537
+ HARNESS_GAP_REQUIRE_TEST=$(jq -r '.harnessGapPolicy.requireTestCaseBeforeClose // true' "$policy" 2>/dev/null || echo "true")
538
+ else
539
+ HARNESS_GAP_ENABLED="false"
540
+ fi
541
+ }
542
+
543
+ create_harness_gap() {
544
+ local incident_id="$1"
545
+ local severity="$2"
546
+ local root_cause="$3"
547
+
548
+ mkdir -p "$HARNESS_GAPS_DIR"
549
+ load_harness_gap_policy
550
+
551
+ if [[ "$HARNESS_GAP_ENABLED" != "true" ]]; then
552
+ return 0
553
+ fi
554
+
555
+ local gap_id="gap-${incident_id}"
556
+ local gap_file="${HARNESS_GAPS_DIR}/${gap_id}.json"
557
+ local sla_hours
558
+
559
+ case "$severity" in
560
+ P0) sla_hours="$HARNESS_GAP_P0_SLA" ;;
561
+ P1) sla_hours="$HARNESS_GAP_P1_SLA" ;;
562
+ P2) sla_hours="$HARNESS_GAP_P2_SLA" ;;
563
+ *) sla_hours="$HARNESS_GAP_P2_SLA" ;;
564
+ esac
565
+
566
+ local created_at
567
+ created_at=$(now_iso)
568
+ local created_epoch
569
+ created_epoch=$(now_epoch)
570
+ local sla_deadline_epoch=$((created_epoch + sla_hours * 3600))
571
+
572
+ cat > "$gap_file" << EOF
573
+ {
574
+ "gap_id": "${gap_id}",
575
+ "incident_id": "${incident_id}",
576
+ "severity": "${severity}",
577
+ "root_cause": "${root_cause}",
578
+ "created_at": "${created_at}",
579
+ "created_epoch": ${created_epoch},
580
+ "sla_hours": ${sla_hours},
581
+ "sla_deadline_epoch": ${sla_deadline_epoch},
582
+ "status": "open",
583
+ "test_case_file": null,
584
+ "github_issue": null,
585
+ "resolved_at": null
586
+ }
587
+ EOF
588
+
589
+ info "Harness gap created: ${gap_id} (SLA: ${sla_hours}h)"
590
+ emit_event "harness_gap.created" "gap_id=${gap_id}" "incident=${incident_id}" "sla_hours=${sla_hours}"
591
+
592
+ # Auto-create GitHub issue for gap tracking
593
+ if [[ "$HARNESS_GAP_AUTO_CREATE" == "true" ]] && command -v gh >/dev/null 2>&1; then
594
+ local title="[HARNESS GAP] ${severity}: Add test case for ${root_cause}"
595
+ local body="## Harness Gap
596
+
597
+ **Incident:** \`${incident_id}\`
598
+ **Severity:** ${severity}
599
+ **Root Cause:** ${root_cause}
600
+ **SLA:** ${sla_hours} hours
601
+
602
+ ## Required Action
603
+ Add a regression test case that covers this failure scenario.
604
+
605
+ ## Acceptance Criteria
606
+ - [ ] Test case file created in \`scripts/\`
607
+ - [ ] Test reproduces the original failure condition
608
+ - [ ] Test passes after the fix is applied
609
+ - [ ] Gap record resolved via \`shipwright incident gap resolve ${gap_id} <test_file>\`
610
+
611
+ ## Context
612
+ This gap was automatically created by the Shipwright incident commander.
613
+ Part of the Code Factory harness-gap loop: every production regression
614
+ must produce a harness test case within the SLA window.
615
+
616
+ ---
617
+ *Auto-generated by Shipwright Code Factory*"
618
+
619
+ local issue_url
620
+ issue_url=$(gh issue create --title "$title" --body "$body" --label "$(_config_get "labels.harness_gap_labels" "harness-gap,shipwright")" 2>/dev/null || echo "")
621
+ if [[ -n "$issue_url" ]]; then
622
+ local issue_num
623
+ issue_num=$(echo "$issue_url" | sed -n 's|.*/issues/\([0-9]*\)|\1|p')
624
+ jq --arg issue "$issue_num" '.github_issue = $issue' "$gap_file" > "${gap_file}.tmp" && mv "${gap_file}.tmp" "$gap_file"
625
+ success "Created harness gap issue: $issue_url"
626
+ fi
627
+ fi
628
+ }
629
+
630
+ resolve_harness_gap() {
631
+ local gap_id="$1"
632
+ local test_case_file="${2:-}"
633
+
634
+ local gap_file="${HARNESS_GAPS_DIR}/${gap_id}.json"
635
+ if [[ ! -f "$gap_file" ]]; then
636
+ error "Harness gap not found: ${gap_id}"
637
+ return 1
638
+ fi
639
+
640
+ load_harness_gap_policy
641
+
642
+ if [[ "$HARNESS_GAP_REQUIRE_TEST" == "true" && -z "$test_case_file" ]]; then
643
+ error "Test case file required to resolve gap (policy: requireTestCaseBeforeClose=true)"
644
+ echo "Usage: shipwright incident gap resolve ${gap_id} <test_case_file>"
645
+ return 1
646
+ fi
647
+
648
+ if [[ -n "$test_case_file" && ! -f "$test_case_file" ]]; then
649
+ error "Test case file not found: ${test_case_file}"
650
+ return 1
651
+ fi
652
+
653
+ local resolved_at
654
+ resolved_at=$(now_iso)
655
+ jq --arg resolved_at "$resolved_at" --arg test_file "${test_case_file:-null}" \
656
+ '.status = "resolved" | .resolved_at = $resolved_at | .test_case_file = $test_file' \
657
+ "$gap_file" > "${gap_file}.tmp" && mv "${gap_file}.tmp" "$gap_file"
658
+
659
+ success "Harness gap resolved: ${gap_id}"
660
+ emit_event "harness_gap.resolved" "gap_id=${gap_id}" "test_file=${test_case_file:-none}"
661
+
662
+ # Close the GitHub issue if it exists
663
+ local github_issue
664
+ github_issue=$(jq -r '.github_issue // empty' "$gap_file" 2>/dev/null)
665
+ if [[ -n "$github_issue" ]] && command -v gh >/dev/null 2>&1; then
666
+ gh issue close "$github_issue" --comment "Harness gap resolved. Test case: \`${test_case_file:-none}\`" 2>/dev/null || true
667
+ fi
668
+ }
669
+
670
+ cmd_gap() {
671
+ local subcmd="${1:-list}"
672
+ shift || true
673
+
674
+ mkdir -p "$HARNESS_GAPS_DIR"
675
+
676
+ case "$subcmd" in
677
+ create)
678
+ create_harness_gap "$@"
679
+ ;;
680
+ resolve)
681
+ resolve_harness_gap "$@"
682
+ ;;
683
+ list)
684
+ local current_epoch
685
+ current_epoch=$(now_epoch)
686
+ echo -e "${BOLD}Harness Gaps${RESET}"
687
+ echo -e "${DIM}────────────────────────────────────────────────────────────────${RESET}"
688
+
689
+ local gap_files
690
+ gap_files=$(find "$HARNESS_GAPS_DIR" -name 'gap-*.json' -type f 2>/dev/null || true)
691
+
692
+ if [[ -z "$gap_files" ]]; then
693
+ info "No harness gaps recorded"
694
+ return 0
695
+ fi
696
+
697
+ while IFS= read -r gf; do
698
+ [[ -z "$gf" ]] && continue
699
+ local gid sev status sla_deadline
700
+ gid=$(jq -r '.gap_id // "unknown"' "$gf" 2>/dev/null)
701
+ sev=$(jq -r '.severity // "P3"' "$gf" 2>/dev/null)
702
+ status=$(jq -r '.status // "open"' "$gf" 2>/dev/null)
703
+ sla_deadline=$(jq -r '.sla_deadline_epoch // 0' "$gf" 2>/dev/null)
704
+
705
+ local sla_remaining=""
706
+ if [[ "$status" == "open" ]]; then
707
+ local remaining=$((sla_deadline - current_epoch))
708
+ if [[ "$remaining" -lt 0 ]]; then
709
+ sla_remaining="${RED}OVERDUE${RESET}"
710
+ else
711
+ sla_remaining="$(format_duration "$remaining") remaining"
712
+ fi
713
+ else
714
+ sla_remaining="${GREEN}resolved${RESET}"
715
+ fi
716
+
717
+ printf " %-20s %-4s %-8s %b\n" "$gid" "$sev" "$status" "$sla_remaining"
718
+ done <<< "$gap_files"
719
+ ;;
720
+ sla)
721
+ # Show SLA compliance metrics
722
+ load_harness_gap_policy
723
+ local current_epoch
724
+ current_epoch=$(now_epoch)
725
+ local total=0 resolved=0 overdue=0 within_sla=0
726
+
727
+ local gap_files
728
+ gap_files=$(find "$HARNESS_GAPS_DIR" -name 'gap-*.json' -type f 2>/dev/null || true)
729
+
730
+ while IFS= read -r gf; do
731
+ [[ -z "$gf" ]] && continue
732
+ total=$((total + 1))
733
+ local status sla_deadline
734
+ status=$(jq -r '.status // "open"' "$gf" 2>/dev/null)
735
+ sla_deadline=$(jq -r '.sla_deadline_epoch // 0' "$gf" 2>/dev/null)
736
+
737
+ if [[ "$status" == "resolved" ]]; then
738
+ resolved=$((resolved + 1))
739
+ within_sla=$((within_sla + 1))
740
+ elif [[ "$current_epoch" -gt "$sla_deadline" ]]; then
741
+ overdue=$((overdue + 1))
742
+ fi
743
+ done <<< "$gap_files"
744
+
745
+ echo -e "${BOLD}Harness Gap SLA Compliance${RESET}"
746
+ echo -e "${DIM}────────────────────────────────────────────────────────────────${RESET}"
747
+ echo "Total gaps: $total"
748
+ echo "Resolved: $resolved"
749
+ echo "Overdue: $overdue"
750
+ echo "SLA compliance: $( [[ $total -gt 0 ]] && echo "$((within_sla * 100 / total))%" || echo "N/A" )"
751
+ ;;
752
+ *)
753
+ echo "Usage: shipwright incident gap <create|resolve|list|sla>"
754
+ return 1
755
+ ;;
756
+ esac
757
+ }
758
+
559
759
  # ─── Stop Command ──────────────────────────────────────────────────────────
560
760
 
561
761
  cmd_stop() {
@@ -589,6 +789,7 @@ show_help() {
589
789
  echo -e " ${CYAN}show${RESET} <incident-id> Show details for an incident"
590
790
  echo -e " ${CYAN}report${RESET} <incident-id> Generate post-mortem report"
591
791
  echo -e " ${CYAN}stats${RESET} [format] Show incident statistics (table|json)"
792
+ echo -e " ${CYAN}gap${RESET} <cmd> Harness gap loop (list|create|resolve|sla)"
592
793
  echo -e " ${CYAN}config${RESET} <cmd> Configure incident response (show|set)"
593
794
  echo -e " ${CYAN}help${RESET} Show this help"
594
795
  echo ""
@@ -627,9 +828,25 @@ main() {
627
828
  stats)
628
829
  cmd_stats "$@"
629
830
  ;;
831
+ gap)
832
+ cmd_gap "$@"
833
+ ;;
630
834
  config)
631
- error "config command not yet implemented"
632
- return 1
835
+ local policy="${REPO_DIR}/config/policy.json"
836
+ if [[ ! -f "$policy" ]]; then
837
+ warn "No policy file found at ${policy}"
838
+ echo " Use: shipwright init to create one"
839
+ return 1
840
+ fi
841
+ echo -e "${BOLD}Incident & Harness Gap Configuration${RESET}"
842
+ echo ""
843
+ echo -e " Policy file: ${DIM}${policy}${RESET}"
844
+ echo -e " Harness gap enabled: $(jq -r '.harnessGapPolicy.enabled // false' "$policy" 2>/dev/null)"
845
+ echo -e " P0 SLA (hours): $(jq -r '.harnessGapPolicy.p0SlaHours // 24' "$policy" 2>/dev/null)"
846
+ echo -e " P1 SLA (hours): $(jq -r '.harnessGapPolicy.p1SlaHours // 72' "$policy" 2>/dev/null)"
847
+ echo -e " P2 SLA (hours): $(jq -r '.harnessGapPolicy.p2SlaHours // 168' "$policy" 2>/dev/null)"
848
+ echo -e " Auto-create gap issues: $(jq -r '.harnessGapPolicy.autoCreateGapIssue // true' "$policy" 2>/dev/null)"
849
+ echo -e " Require test before close: $(jq -r '.harnessGapPolicy.requireTestCaseBeforeClose // true' "$policy" 2>/dev/null)"
633
850
  ;;
634
851
  help|--help|-h)
635
852
  show_help
@@ -8,7 +8,7 @@
8
8
  # ║ ║
9
9
  # ║ --deploy Detect platform and generate deployed.json template ║
10
10
  # ╚═══════════════════════════════════════════════════════════════════════════╝
11
- VERSION="2.3.1"
11
+ VERSION="3.0.0"
12
12
  set -euo pipefail
13
13
  trap 'echo "ERROR: $BASH_SOURCE:$LINENO exited with status $?" >&2' ERR
14
14
  trap 'rm -f "${tmp:-}"' EXIT
@@ -33,24 +33,6 @@ if [[ "$(type -t now_iso 2>/dev/null)" != "function" ]]; then
33
33
  now_iso() { date -u +"%Y-%m-%dT%H:%M:%SZ"; }
34
34
  now_epoch() { date +%s; }
35
35
  fi
36
- if [[ "$(type -t emit_event 2>/dev/null)" != "function" ]]; then
37
- emit_event() {
38
- local event_type="$1"; shift; mkdir -p "${HOME}/.shipwright"
39
- local payload="{\"ts\":\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\",\"type\":\"$event_type\""
40
- while [[ $# -gt 0 ]]; do local key="${1%%=*}" val="${1#*=}"; payload="${payload},\"${key}\":\"${val}\""; shift; done
41
- echo "${payload}}" >> "${HOME}/.shipwright/events.jsonl"
42
- }
43
- fi
44
- CYAN="${CYAN:-\033[38;2;0;212;255m}"
45
- PURPLE="${PURPLE:-\033[38;2;124;58;237m}"
46
- BLUE="${BLUE:-\033[38;2;0;102;255m}"
47
- GREEN="${GREEN:-\033[38;2;74;222;128m}"
48
- YELLOW="${YELLOW:-\033[38;2;250;204;21m}"
49
- RED="${RED:-\033[38;2;248;113;113m}"
50
- DIM="${DIM:-\033[2m}"
51
- BOLD="${BOLD:-\033[1m}"
52
- RESET="${RESET:-\033[0m}"
53
-
54
36
  # ─── Flag parsing ───────────────────────────────────────────────────────────
55
37
  DEPLOY_SETUP=false
56
38
  DEPLOY_PLATFORM=""
@@ -111,7 +93,7 @@ if [[ "$REPAIR_MODE" == "true" ]]; then
111
93
  # Strip legacy overlay source lines from user's tmux.conf
112
94
  if [[ -f "$HOME/.tmux.conf" ]] && grep -q "claude-teams-overlay" "$HOME/.tmux.conf" 2>/dev/null; then
113
95
  tmp=$(mktemp)
114
- grep -v "claude-teams-overlay" "$HOME/.tmux.conf" > "$tmp" && mv "$tmp" "$HOME/.tmux.conf"
96
+ grep -v "claude-teams-overlay" "$HOME/.tmux.conf" > "$tmp" && mv "$tmp" "$HOME/.tmux.conf" || rm -f "$tmp"
115
97
  success "Removed legacy claude-teams-overlay references from ~/.tmux.conf"
116
98
  fi
117
99
  fi
@@ -307,14 +289,14 @@ if [[ $_verify_fail -eq 0 ]]; then
307
289
  fi
308
290
 
309
291
  # ─── CLI Bootstrap (symlinks + PATH) ─────────────────────────────────────────
310
- # Install sw/shipwright/cct symlinks so the CLI works from anywhere
292
+ # Install sw/shipwright symlinks so the CLI works from anywhere
311
293
  BIN_DIR="$HOME/.local/bin"
312
294
  mkdir -p "$BIN_DIR"
313
295
 
314
296
  SW_SRC="$SCRIPT_DIR/sw"
315
297
  if [[ -f "$SW_SRC" ]]; then
316
298
  _cli_changed=false
317
- for _cmd in sw shipwright cct; do
299
+ for _cmd in sw shipwright; do
318
300
  _dest="$BIN_DIR/$_cmd"
319
301
  if [[ -L "$_dest" ]] && [[ "$(readlink "$_dest")" == "$SW_SRC" ]]; then
320
302
  continue
@@ -322,8 +304,13 @@ if [[ -f "$SW_SRC" ]]; then
322
304
  ln -sf "$SW_SRC" "$_dest"
323
305
  _cli_changed=true
324
306
  done
307
+ # Clean up legacy cct symlink if present
308
+ if [[ -L "$BIN_DIR/cct" ]]; then
309
+ rm -f "$BIN_DIR/cct"
310
+ _cli_changed=true
311
+ fi
325
312
  if [[ "$_cli_changed" == "true" ]]; then
326
- success "CLI symlinks: sw, shipwright, cct → $BIN_DIR"
313
+ success "CLI symlinks: sw, shipwright → $BIN_DIR"
327
314
  else
328
315
  success "CLI symlinks already correct"
329
316
  fi
@@ -390,6 +377,13 @@ if [[ -d "$PIPELINES_SRC" ]]; then
390
377
  success "Installed ${pip_count} pipeline templates → ~/.shipwright/pipelines/"
391
378
  fi
392
379
 
380
+ # ─── Bootstrap optimization & memory (cold-start) ─────────────────────────────
381
+ if [[ -f "$SCRIPT_DIR/lib/bootstrap.sh" ]]; then
382
+ source "$SCRIPT_DIR/lib/bootstrap.sh"
383
+ bootstrap_optimization 2>/dev/null || true
384
+ bootstrap_memory 2>/dev/null || true
385
+ fi
386
+
393
387
  # ─── Shell Completions ────────────────────────────────────────────────────────
394
388
  # Detect shell type and install completions to the correct location
395
389
  # Detect the user's login shell (not the script's running shell).
@@ -493,11 +487,11 @@ if [[ -f "$SETTINGS_FILE" ]]; then
493
487
  success "Agent teams already enabled in settings.json"
494
488
  else
495
489
  # Try to add using jq
496
- if jq -e '.env' "$SETTINGS_FILE" &>/dev/null 2>&1; then
490
+ if jq -e '.env' "$SETTINGS_FILE" >/dev/null 2>&1; then
497
491
  tmp=$(mktemp)
498
492
  jq '.env["CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS"] = "1"' "$SETTINGS_FILE" > "$tmp" && mv "$tmp" "$SETTINGS_FILE"
499
493
  success "Enabled agent teams in existing settings.json"
500
- elif jq -e '.' "$SETTINGS_FILE" &>/dev/null 2>&1; then
494
+ elif jq -e '.' "$SETTINGS_FILE" >/dev/null 2>&1; then
501
495
  tmp=$(mktemp)
502
496
  jq '. + {"env": {"CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS": "1"}}' "$SETTINGS_FILE" > "$tmp" && mv "$tmp" "$SETTINGS_FILE"
503
497
  success "Added agent teams env to settings.json"
@@ -552,17 +546,17 @@ fi
552
546
 
553
547
  # ─── Wire Hooks into settings.json ──────────────────────────────────────────
554
548
  # Ensure each installed hook has a matching event config in settings.json
555
- if [[ -f "$SETTINGS_FILE" ]] && jq -e '.' "$SETTINGS_FILE" &>/dev/null; then
549
+ if [[ -f "$SETTINGS_FILE" ]] && jq -e '.' "$SETTINGS_FILE" >/dev/null 2>&1; then
556
550
  hooks_wired=0
557
551
 
558
552
  # Ensure .hooks object exists
559
- if ! jq -e '.hooks' "$SETTINGS_FILE" &>/dev/null; then
553
+ if ! jq -e '.hooks' "$SETTINGS_FILE" >/dev/null 2>&1; then
560
554
  tmp=$(mktemp)
561
555
  jq '.hooks = {}' "$SETTINGS_FILE" > "$tmp" && mv "$tmp" "$SETTINGS_FILE"
562
556
  fi
563
557
 
564
558
  # TeammateIdle
565
- if [[ -f "$CLAUDE_DIR/hooks/teammate-idle.sh" ]] && ! jq -e '.hooks.TeammateIdle' "$SETTINGS_FILE" &>/dev/null; then
559
+ if [[ -f "$CLAUDE_DIR/hooks/teammate-idle.sh" ]] && ! jq -e '.hooks.TeammateIdle' "$SETTINGS_FILE" >/dev/null 2>&1; then
566
560
  tmp=$(mktemp)
567
561
  jq '.hooks.TeammateIdle = [{"hooks": [{"type": "command", "command": "~/.claude/hooks/teammate-idle.sh", "timeout": 30, "statusMessage": "Running typecheck before idle..."}]}]' \
568
562
  "$SETTINGS_FILE" > "$tmp" && mv "$tmp" "$SETTINGS_FILE"
@@ -570,7 +564,7 @@ if [[ -f "$SETTINGS_FILE" ]] && jq -e '.' "$SETTINGS_FILE" &>/dev/null; then
570
564
  fi
571
565
 
572
566
  # TaskCompleted
573
- if [[ -f "$CLAUDE_DIR/hooks/task-completed.sh" ]] && ! jq -e '.hooks.TaskCompleted' "$SETTINGS_FILE" &>/dev/null; then
567
+ if [[ -f "$CLAUDE_DIR/hooks/task-completed.sh" ]] && ! jq -e '.hooks.TaskCompleted' "$SETTINGS_FILE" >/dev/null 2>&1; then
574
568
  tmp=$(mktemp)
575
569
  jq '.hooks.TaskCompleted = [{"hooks": [{"type": "command", "command": "~/.claude/hooks/task-completed.sh", "timeout": 60, "statusMessage": "Running quality checks..."}]}]' \
576
570
  "$SETTINGS_FILE" > "$tmp" && mv "$tmp" "$SETTINGS_FILE"
@@ -578,7 +572,7 @@ if [[ -f "$SETTINGS_FILE" ]] && jq -e '.' "$SETTINGS_FILE" &>/dev/null; then
578
572
  fi
579
573
 
580
574
  # Notification
581
- if [[ -f "$CLAUDE_DIR/hooks/notify-idle.sh" ]] && ! jq -e '.hooks.Notification' "$SETTINGS_FILE" &>/dev/null; then
575
+ if [[ -f "$CLAUDE_DIR/hooks/notify-idle.sh" ]] && ! jq -e '.hooks.Notification' "$SETTINGS_FILE" >/dev/null 2>&1; then
582
576
  tmp=$(mktemp)
583
577
  jq '.hooks.Notification = [{"hooks": [{"type": "command", "command": "~/.claude/hooks/notify-idle.sh", "async": true}]}]' \
584
578
  "$SETTINGS_FILE" > "$tmp" && mv "$tmp" "$SETTINGS_FILE"
@@ -586,7 +580,7 @@ if [[ -f "$SETTINGS_FILE" ]] && jq -e '.' "$SETTINGS_FILE" &>/dev/null; then
586
580
  fi
587
581
 
588
582
  # PreCompact
589
- if [[ -f "$CLAUDE_DIR/hooks/pre-compact-save.sh" ]] && ! jq -e '.hooks.PreCompact' "$SETTINGS_FILE" &>/dev/null; then
583
+ if [[ -f "$CLAUDE_DIR/hooks/pre-compact-save.sh" ]] && ! jq -e '.hooks.PreCompact' "$SETTINGS_FILE" >/dev/null 2>&1; then
590
584
  tmp=$(mktemp)
591
585
  jq '.hooks.PreCompact = [{"matcher": "auto", "hooks": [{"type": "command", "command": "~/.claude/hooks/pre-compact-save.sh", "statusMessage": "Saving context before compaction..."}]}]' \
592
586
  "$SETTINGS_FILE" > "$tmp" && mv "$tmp" "$SETTINGS_FILE"
@@ -594,7 +588,7 @@ if [[ -f "$SETTINGS_FILE" ]] && jq -e '.' "$SETTINGS_FILE" &>/dev/null; then
594
588
  fi
595
589
 
596
590
  # SessionStart
597
- if [[ -f "$CLAUDE_DIR/hooks/session-start.sh" ]] && ! jq -e '.hooks.SessionStart' "$SETTINGS_FILE" &>/dev/null; then
591
+ if [[ -f "$CLAUDE_DIR/hooks/session-start.sh" ]] && ! jq -e '.hooks.SessionStart' "$SETTINGS_FILE" >/dev/null 2>&1; then
598
592
  tmp=$(mktemp)
599
593
  jq '.hooks.SessionStart = [{"hooks": [{"type": "command", "command": "~/.claude/hooks/session-start.sh", "timeout": 5}]}]' \
600
594
  "$SETTINGS_FILE" > "$tmp" && mv "$tmp" "$SETTINGS_FILE"
@@ -644,8 +638,8 @@ fi
644
638
 
645
639
  # ─── GitHub CLI Authentication ────────────────────────────────────────────────
646
640
  # gh auth is required for daemon, pipeline, PR creation, and issue management
647
- if command -v gh &>/dev/null; then
648
- if gh auth status &>/dev/null 2>&1; then
641
+ if command -v gh >/dev/null 2>&1; then
642
+ if gh auth status >/dev/null 2>&1; then
649
643
  success "GitHub CLI authenticated"
650
644
  else
651
645
  warn "GitHub CLI installed but not authenticated"
@@ -669,13 +663,13 @@ if [[ -n "${TMUX:-}" ]]; then
669
663
  fi
670
664
 
671
665
  # ─── Bun (required for dashboard) ──────────────────────────────────────────
672
- if command -v bun &>/dev/null || [[ -x "$HOME/.bun/bin/bun" ]]; then
666
+ if command -v bun >/dev/null 2>&1 || [[ -x "$HOME/.bun/bin/bun" ]]; then
673
667
  _bun_cmd="bun"
674
668
  [[ -x "$HOME/.bun/bin/bun" ]] && _bun_cmd="$HOME/.bun/bin/bun"
675
669
  success "Bun $($_bun_cmd --version 2>/dev/null || echo "installed") — dashboard ready"
676
670
  else
677
671
  info "Installing Bun (required for ${BOLD}shipwright dashboard${RESET})..."
678
- if curl -fsSL https://bun.sh/install | bash 2>/dev/null; then
672
+ if curl -fsSL --connect-timeout 10 --max-time 120 https://bun.sh/install | bash 2>/dev/null; then
679
673
  export PATH="$HOME/.bun/bin:$PATH"
680
674
  success "Bun installed — dashboard ready"
681
675
  else
@@ -781,7 +775,11 @@ else
781
775
  fi
782
776
 
783
777
  # Confirm with user
784
- read -rp "$(echo -e "${CYAN}${BOLD}▸${RESET} Configure deploy for ${BOLD}${DEPLOY_PLATFORM}${RESET}? [Y/n] ")" confirm
778
+ if [[ -t 0 ]]; then
779
+ read -rp "$(echo -e "${CYAN}${BOLD}▸${RESET} Configure deploy for ${BOLD}${DEPLOY_PLATFORM}${RESET}? [Y/n] ")" confirm
780
+ else
781
+ confirm="y"
782
+ fi
785
783
  if [[ "$(echo "$confirm" | tr '[:upper:]' '[:lower:]')" == "n" ]]; then
786
784
  info "Aborted. Use --platform to specify manually."
787
785
  exit 0
@@ -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.3.1"
9
+ VERSION="3.0.0"
10
10
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
11
11
  REPO_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
12
12
 
@@ -34,16 +34,6 @@ if [[ "$(type -t emit_event 2>/dev/null)" != "function" ]]; then
34
34
  echo "${payload}}" >> "${HOME}/.shipwright/events.jsonl"
35
35
  }
36
36
  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
37
  format_duration() {
48
38
  local secs="$1"
49
39
  if [[ "$secs" -ge 3600 ]]; then