shipwright-cli 2.4.0 → 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 (161) hide show
  1. package/README.md +16 -11
  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 +13 -18
  8. package/dashboard/coverage/coverage-summary.json +14 -0
  9. package/dashboard/public/index.html +1 -1
  10. package/dashboard/server.ts +306 -17
  11. package/dashboard/src/components/charts/bar.test.ts +79 -0
  12. package/dashboard/src/components/charts/donut.test.ts +68 -0
  13. package/dashboard/src/components/charts/pipeline-rail.test.ts +117 -0
  14. package/dashboard/src/components/charts/sparkline.test.ts +125 -0
  15. package/dashboard/src/core/api.test.ts +309 -0
  16. package/dashboard/src/core/helpers.test.ts +301 -0
  17. package/dashboard/src/core/router.test.ts +307 -0
  18. package/dashboard/src/core/router.ts +7 -0
  19. package/dashboard/src/core/sse.test.ts +144 -0
  20. package/dashboard/src/views/metrics.test.ts +186 -0
  21. package/dashboard/src/views/overview.test.ts +173 -0
  22. package/dashboard/src/views/pipelines.test.ts +183 -0
  23. package/dashboard/src/views/team.test.ts +253 -0
  24. package/dashboard/vitest.config.ts +14 -5
  25. package/docs/TIPS.md +1 -1
  26. package/docs/patterns/README.md +1 -1
  27. package/package.json +5 -7
  28. package/scripts/adapters/docker-deploy.sh +1 -1
  29. package/scripts/adapters/tmux-adapter.sh +11 -1
  30. package/scripts/adapters/wezterm-adapter.sh +1 -1
  31. package/scripts/check-version-consistency.sh +1 -1
  32. package/scripts/lib/architecture.sh +126 -0
  33. package/scripts/lib/bootstrap.sh +75 -0
  34. package/scripts/lib/compat.sh +89 -6
  35. package/scripts/lib/config.sh +91 -0
  36. package/scripts/lib/daemon-adaptive.sh +3 -3
  37. package/scripts/lib/daemon-dispatch.sh +39 -16
  38. package/scripts/lib/daemon-health.sh +1 -1
  39. package/scripts/lib/daemon-patrol.sh +24 -12
  40. package/scripts/lib/daemon-poll.sh +37 -25
  41. package/scripts/lib/daemon-state.sh +115 -23
  42. package/scripts/lib/daemon-triage.sh +30 -8
  43. package/scripts/lib/fleet-failover.sh +63 -0
  44. package/scripts/lib/helpers.sh +30 -6
  45. package/scripts/lib/pipeline-detection.sh +2 -2
  46. package/scripts/lib/pipeline-github.sh +9 -9
  47. package/scripts/lib/pipeline-intelligence.sh +85 -35
  48. package/scripts/lib/pipeline-quality-checks.sh +16 -16
  49. package/scripts/lib/pipeline-quality.sh +1 -1
  50. package/scripts/lib/pipeline-stages.sh +242 -28
  51. package/scripts/lib/pipeline-state.sh +40 -4
  52. package/scripts/lib/test-helpers.sh +247 -0
  53. package/scripts/postinstall.mjs +3 -11
  54. package/scripts/sw +10 -4
  55. package/scripts/sw-activity.sh +1 -11
  56. package/scripts/sw-adaptive.sh +109 -85
  57. package/scripts/sw-adversarial.sh +4 -14
  58. package/scripts/sw-architecture-enforcer.sh +1 -11
  59. package/scripts/sw-auth.sh +8 -17
  60. package/scripts/sw-autonomous.sh +111 -49
  61. package/scripts/sw-changelog.sh +1 -11
  62. package/scripts/sw-checkpoint.sh +144 -20
  63. package/scripts/sw-ci.sh +2 -12
  64. package/scripts/sw-cleanup.sh +13 -17
  65. package/scripts/sw-code-review.sh +16 -36
  66. package/scripts/sw-connect.sh +5 -12
  67. package/scripts/sw-context.sh +9 -26
  68. package/scripts/sw-cost.sh +6 -16
  69. package/scripts/sw-daemon.sh +75 -70
  70. package/scripts/sw-dashboard.sh +57 -17
  71. package/scripts/sw-db.sh +506 -15
  72. package/scripts/sw-decompose.sh +1 -11
  73. package/scripts/sw-deps.sh +15 -25
  74. package/scripts/sw-developer-simulation.sh +1 -11
  75. package/scripts/sw-discovery.sh +112 -30
  76. package/scripts/sw-doc-fleet.sh +7 -17
  77. package/scripts/sw-docs-agent.sh +6 -16
  78. package/scripts/sw-docs.sh +4 -12
  79. package/scripts/sw-doctor.sh +134 -43
  80. package/scripts/sw-dora.sh +11 -19
  81. package/scripts/sw-durable.sh +35 -52
  82. package/scripts/sw-e2e-orchestrator.sh +11 -27
  83. package/scripts/sw-eventbus.sh +115 -115
  84. package/scripts/sw-evidence.sh +114 -30
  85. package/scripts/sw-feedback.sh +3 -13
  86. package/scripts/sw-fix.sh +2 -20
  87. package/scripts/sw-fleet-discover.sh +1 -11
  88. package/scripts/sw-fleet-viz.sh +10 -18
  89. package/scripts/sw-fleet.sh +13 -17
  90. package/scripts/sw-github-app.sh +6 -16
  91. package/scripts/sw-github-checks.sh +1 -11
  92. package/scripts/sw-github-deploy.sh +1 -11
  93. package/scripts/sw-github-graphql.sh +2 -12
  94. package/scripts/sw-guild.sh +1 -11
  95. package/scripts/sw-heartbeat.sh +49 -12
  96. package/scripts/sw-hygiene.sh +45 -43
  97. package/scripts/sw-incident.sh +48 -74
  98. package/scripts/sw-init.sh +35 -37
  99. package/scripts/sw-instrument.sh +1 -11
  100. package/scripts/sw-intelligence.sh +362 -51
  101. package/scripts/sw-jira.sh +5 -14
  102. package/scripts/sw-launchd.sh +2 -12
  103. package/scripts/sw-linear.sh +8 -17
  104. package/scripts/sw-logs.sh +4 -12
  105. package/scripts/sw-loop.sh +641 -90
  106. package/scripts/sw-memory.sh +243 -17
  107. package/scripts/sw-mission-control.sh +2 -12
  108. package/scripts/sw-model-router.sh +73 -34
  109. package/scripts/sw-otel.sh +11 -21
  110. package/scripts/sw-oversight.sh +1 -11
  111. package/scripts/sw-patrol-meta.sh +5 -11
  112. package/scripts/sw-pipeline-composer.sh +7 -17
  113. package/scripts/sw-pipeline-vitals.sh +1 -11
  114. package/scripts/sw-pipeline.sh +478 -122
  115. package/scripts/sw-pm.sh +2 -12
  116. package/scripts/sw-pr-lifecycle.sh +27 -25
  117. package/scripts/sw-predictive.sh +16 -22
  118. package/scripts/sw-prep.sh +6 -16
  119. package/scripts/sw-ps.sh +1 -11
  120. package/scripts/sw-public-dashboard.sh +2 -12
  121. package/scripts/sw-quality.sh +77 -10
  122. package/scripts/sw-reaper.sh +1 -11
  123. package/scripts/sw-recruit.sh +15 -25
  124. package/scripts/sw-regression.sh +11 -21
  125. package/scripts/sw-release-manager.sh +19 -28
  126. package/scripts/sw-release.sh +8 -16
  127. package/scripts/sw-remote.sh +1 -11
  128. package/scripts/sw-replay.sh +48 -44
  129. package/scripts/sw-retro.sh +70 -92
  130. package/scripts/sw-review-rerun.sh +1 -1
  131. package/scripts/sw-scale.sh +109 -32
  132. package/scripts/sw-security-audit.sh +12 -22
  133. package/scripts/sw-self-optimize.sh +239 -23
  134. package/scripts/sw-session.sh +3 -13
  135. package/scripts/sw-setup.sh +8 -18
  136. package/scripts/sw-standup.sh +5 -15
  137. package/scripts/sw-status.sh +32 -23
  138. package/scripts/sw-strategic.sh +129 -13
  139. package/scripts/sw-stream.sh +1 -11
  140. package/scripts/sw-swarm.sh +76 -36
  141. package/scripts/sw-team-stages.sh +10 -20
  142. package/scripts/sw-templates.sh +4 -14
  143. package/scripts/sw-testgen.sh +3 -13
  144. package/scripts/sw-tmux-pipeline.sh +1 -19
  145. package/scripts/sw-tmux-role-color.sh +0 -10
  146. package/scripts/sw-tmux-status.sh +3 -11
  147. package/scripts/sw-tmux.sh +2 -20
  148. package/scripts/sw-trace.sh +1 -19
  149. package/scripts/sw-tracker-github.sh +0 -10
  150. package/scripts/sw-tracker-jira.sh +1 -11
  151. package/scripts/sw-tracker-linear.sh +1 -11
  152. package/scripts/sw-tracker.sh +7 -24
  153. package/scripts/sw-triage.sh +24 -34
  154. package/scripts/sw-upgrade.sh +5 -23
  155. package/scripts/sw-ux.sh +1 -19
  156. package/scripts/sw-webhook.sh +18 -32
  157. package/scripts/sw-widgets.sh +3 -21
  158. package/scripts/sw-worktree.sh +11 -27
  159. package/scripts/update-homebrew-sha.sh +67 -0
  160. package/templates/pipelines/tdd.json +72 -0
  161. package/scripts/sw-pipeline.sh.mock +0 -7
@@ -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.0.0"
11
11
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
12
12
  REPO_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
13
13
 
@@ -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
@@ -266,7 +256,7 @@ ingest_strategic_findings() {
266
256
  complexity=$(echo "$line" | jq -r '.complexity // "standard"' 2>/dev/null) || true
267
257
 
268
258
  local issue_num=""
269
- if [[ "${NO_GITHUB:-false}" != "true" ]] && command -v gh &>/dev/null; then
259
+ if [[ "${NO_GITHUB:-false}" != "true" ]] && command -v gh >/dev/null 2>&1; then
270
260
  issue_num=$(gh issue list --state open --search "$title" --json number -q '.[0].number' 2>/dev/null || echo "")
271
261
  fi
272
262
 
@@ -285,6 +275,100 @@ ingest_strategic_findings() {
285
275
  echo "$findings"
286
276
  }
287
277
 
278
+ # ─── Codebase Analysis (Claude fallback) ────────────────────────────────────
279
+ # When Claude is unavailable, scan the codebase for real findings.
280
+ # Outputs pipe-delimited lines: title|type|complexity (converted to JSON by caller).
281
+ autonomous_heuristic_analysis() {
282
+ local findings=()
283
+ local repo_root
284
+ repo_root=$(git rev-parse --show-toplevel 2>/dev/null || echo ".")
285
+
286
+ # 1. Find scripts with no test files
287
+ local untested_scripts=0
288
+ for script in "$repo_root"/scripts/sw-*.sh; do
289
+ [[ -e "$script" ]] || continue
290
+ [[ "$script" == *-test.sh ]] && continue
291
+ local base
292
+ base=$(basename "$script" .sh)
293
+ if [[ ! -f "$repo_root/scripts/${base}-test.sh" ]]; then
294
+ untested_scripts=$((untested_scripts + 1))
295
+ if [[ "$untested_scripts" -le 2 ]]; then
296
+ findings+=("Add test coverage for ${base}.sh|testing|5")
297
+ fi
298
+ fi
299
+ done
300
+
301
+ # 2. Find large files that could be refactored
302
+ while IFS= read -r line; do
303
+ [[ -z "$line" ]] && continue
304
+ local lcount file
305
+ lcount=$(echo "$line" | awk '{print $1}')
306
+ file=$(echo "$line" | awk '{$1=""; print substr($0,2)}')
307
+ if [[ "$lcount" =~ ^[0-9]+$ ]] && [[ "$lcount" -gt 1500 ]]; then
308
+ local base
309
+ base=$(basename "$file")
310
+ findings+=("Refactor ${base} (${lcount} lines) into smaller modules|refactor|7")
311
+ fi
312
+ done < <(find "$repo_root/scripts" -name "*.sh" -not -name "*-test.sh" -exec wc -l {} \; 2>/dev/null | sort -rn | head -5)
313
+
314
+ # 3. Check for shellcheck warnings
315
+ if command -v shellcheck &>/dev/null; then
316
+ local sc_warnings
317
+ sc_warnings=$(shellcheck -S warning "$repo_root"/scripts/sw-*.sh 2>/dev/null | grep -c "^In " || true)
318
+ if [[ "${sc_warnings:-0}" -gt 10 ]]; then
319
+ findings+=("Fix ${sc_warnings} shellcheck warnings across scripts|quality|4")
320
+ fi
321
+ fi
322
+
323
+ # 4. Check for TODO/FIXME in production code
324
+ local todo_count
325
+ todo_count=$(find "$repo_root/scripts" -name "sw-*.sh" ! -name "*-test.sh" -exec grep -oh 'TODO\|FIXME\|HACK\|XXX' {} \; 2>/dev/null | wc -l | tr -d ' ')
326
+ if [[ "${todo_count:-0}" -gt 5 ]]; then
327
+ findings+=("Address ${todo_count} TODO/FIXME items in production scripts|quality|3")
328
+ fi
329
+
330
+ # 5. Check recent failures from events
331
+ local events_file="$HOME/.shipwright/events.jsonl"
332
+ if [[ -f "$events_file" ]]; then
333
+ local recent_failures
334
+ recent_failures=$(tail -200 "$events_file" 2>/dev/null | grep '"result":"failure"' | wc -l | tr -d ' ')
335
+ if [[ "${recent_failures:-0}" -gt 3 ]]; then
336
+ local common_stage
337
+ common_stage=$(tail -200 "$events_file" 2>/dev/null | grep '"result":"failure"' | grep -oE '"stage":"[^"]*"' | sort | uniq -c | sort -rn | head -1 | sed 's/.*"stage":"\([^"]*\)".*/\1/' || true)
338
+ findings+=("Investigate ${recent_failures} recent pipeline failures${common_stage:+ (common: ${common_stage})}|bug|6")
339
+ fi
340
+ fi
341
+
342
+ # Output findings (at most 5) — pipe-delimited: title|type|complexity
343
+ local count=0
344
+ for f in "${findings[@]}"; do
345
+ [[ "$count" -ge 5 ]] && break
346
+ echo "$f"
347
+ count=$((count + 1))
348
+ done
349
+ }
350
+
351
+ # Convert pipe-delimited heuristic output (title|type|complexity) to JSON array.
352
+ autonomous_heuristic_to_json() {
353
+ local json="[]"
354
+ while IFS= read -r line; do
355
+ [[ -z "$line" ]] || [[ "$line" != *"|"* ]] && continue
356
+ local title type complexity
357
+ title="${line%%|*}"
358
+ line="${line#*|}"
359
+ type="${line%%|*}"
360
+ complexity="${line##*|}"
361
+ local effort="M"
362
+ [[ "$complexity" =~ ^[0-9]+$ ]] && {
363
+ [[ "$complexity" -le 4 ]] && effort="S"
364
+ [[ "$complexity" -ge 7 ]] && effort="L"
365
+ }
366
+ json=$(echo "$json" | jq -c --arg t "$title" --arg c "$type" --arg e "$effort" \
367
+ '. + [{title: $t, description: $t, priority: "medium", effort: $e, labels: [$c], category: $c}]' 2>/dev/null || echo "$json")
368
+ done
369
+ echo "$json"
370
+ }
371
+
288
372
  # ─── Analysis Cycle ────────────────────────────────────────────────────────
289
373
 
290
374
  run_analysis_cycle() {
@@ -297,7 +381,7 @@ run_analysis_cycle() {
297
381
  findings=$(mktemp "${TMPDIR:-/tmp}/sw-autonomous-findings.XXXXXX")
298
382
 
299
383
  # Use Claude to analyze the codebase
300
- if command -v claude &>/dev/null; then
384
+ if command -v claude >/dev/null 2>&1; then
301
385
  info "Running codebase analysis with Claude..."
302
386
 
303
387
  claude -p 'You are Shipwright'"'"'s autonomous PM. Analyze this repository for:
@@ -313,35 +397,13 @@ For each finding, output JSON with fields: title, description, priority (critica
313
397
  Output ONLY a JSON array, no other text.' --max-turns 3 > "$findings" 2>/dev/null || true
314
398
 
315
399
  else
316
- warn "Claude CLI not available, using static heuristics..."
317
-
318
- # Static heuristics for analysis
319
- {
320
- local has_tests=$(find . -type f -name "*test*" -o -name "*spec*" | wc -l || echo "0")
321
- local shell_scripts=$(find scripts -type f -name "*.sh" | wc -l || echo "0")
322
-
323
- jq -n \
324
- --argjson test_count "$has_tests" \
325
- --argjson script_count "$shell_scripts" \
326
- '[
327
- {
328
- title: "Add comprehensive test coverage for critical paths",
329
- description: "Several scripts lack unit test coverage",
330
- priority: "high",
331
- effort: "L",
332
- labels: ["test", "quality"],
333
- category: "test"
334
- },
335
- {
336
- title: "Simplify error handling in daemon.sh",
337
- description: "Daemon error handling could be more robust",
338
- priority: "medium",
339
- effort: "M",
340
- labels: ["refactor", "self-improvement"],
341
- category: "self-improvement"
342
- }
343
- ]'
344
- } > "$findings"
400
+ warn "Claude CLI not available, using codebase-aware analysis..."
401
+
402
+ autonomous_heuristic_analysis | autonomous_heuristic_to_json > "$findings"
403
+ if [[ ! -s "$findings" ]] || [[ "$(cat "$findings")" == "[]" ]]; then
404
+ # No findings from heuristic analysis
405
+ echo "[]" > "$findings"
406
+ fi
345
407
  fi
346
408
 
347
409
  # Ingest strategic findings and merge with Claude/heuristic findings
@@ -354,10 +416,10 @@ Output ONLY a JSON array, no other text.' --max-turns 3 > "$findings" 2>/dev/nul
354
416
  local claude_array
355
417
  claude_array=$(jq -c '.' "$findings" 2>/dev/null || echo "[]")
356
418
  # Extract JSON array from potential markdown-wrapped output
357
- if ! echo "$claude_array" | jq -e 'type == "array"' &>/dev/null; then
419
+ if ! echo "$claude_array" | jq -e 'type == "array"' >/dev/null 2>&1; then
358
420
  claude_array=$(sed -n '/^\[/,/^\]/p' "$findings" 2>/dev/null | jq -c '.' 2>/dev/null || echo "[]")
359
421
  fi
360
- if ! echo "$claude_array" | jq -e 'type == "array"' &>/dev/null; then
422
+ if ! echo "$claude_array" | jq -e 'type == "array"' >/dev/null 2>&1; then
361
423
  claude_array="[]"
362
424
  fi
363
425
 
@@ -485,13 +547,13 @@ trigger_pipeline_for_finding() {
485
547
  fi
486
548
 
487
549
  if [[ "$daemon_aware" == "true" ]] && daemon_is_running; then
488
- # Daemon running: label ready-to-build and let daemon pick it up
550
+ # Daemon running: add shipwright (daemon watch label) + ready-to-build (status), let daemon pick it up
489
551
  if [[ "$NO_GITHUB" != "true" ]]; then
490
- gh issue edit "$issue_num" --add-label "ready-to-build" 2>/dev/null || {
491
- warn "Failed to add ready-to-build label to #${issue_num}"
552
+ gh issue edit "$issue_num" --add-label "shipwright" --add-label "ready-to-build" 2>/dev/null || {
553
+ warn "Failed to add shipwright/ready-to-build labels to #${issue_num}"
492
554
  }
493
555
  fi
494
- info "Delegated issue #${issue_num} to daemon (labeled ready-to-build)"
556
+ info "Delegated issue #${issue_num} to daemon (labeled shipwright, ready-to-build)"
495
557
  emit_event "autonomous.delegated_to_daemon" "issue=$issue_num" "title=$title"
496
558
  return 0
497
559
  fi
@@ -597,7 +659,7 @@ process_findings() {
597
659
 
598
660
  # Strategic findings: issue already created by strategic agent; trigger pipeline and register, skip create
599
661
  if [[ "$source" == "strategic" ]]; then
600
- [[ -z "$issue_num" && "${NO_GITHUB:-false}" != "true" ]] && command -v gh &>/dev/null && \
662
+ [[ -z "$issue_num" && "${NO_GITHUB:-false}" != "true" ]] && command -v gh >/dev/null 2>&1 && \
601
663
  issue_num=$(gh issue list --state open --search "$title" --json number -q '.[0].number' 2>/dev/null || echo "")
602
664
  if [[ -n "$issue_num" && "$issue_num" =~ ^[0-9]+$ ]]; then
603
665
  trigger_pipeline_for_finding "$issue_num" "$title"
@@ -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.0.0"
11
11
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
12
12
  REPO_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
13
13
 
@@ -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
  # ─── Commit Parsing ──────────────────────────────────────────────────────────
48
38
 
49
39
  # Extract conventional commit type (feat, fix, perf, security, etc.)
@@ -8,8 +8,8 @@
8
8
  set -euo pipefail
9
9
  trap 'echo "ERROR: $BASH_SOURCE:$LINENO exited with status $?" >&2' ERR
10
10
 
11
- VERSION="2.4.0"
12
- SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
11
+ VERSION="3.0.0"
12
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd)"
13
13
 
14
14
  # ─── Cross-platform compatibility ──────────────────────────────────────────
15
15
  # shellcheck source=lib/compat.sh
@@ -35,15 +35,6 @@ if [[ "$(type -t emit_event 2>/dev/null)" != "function" ]]; then
35
35
  echo "${payload}}" >> "${HOME}/.shipwright/events.jsonl"
36
36
  }
37
37
  fi
38
- CYAN="${CYAN:-\033[38;2;0;212;255m}"
39
- PURPLE="${PURPLE:-\033[38;2;124;58;237m}"
40
- BLUE="${BLUE:-\033[38;2;0;102;255m}"
41
- GREEN="${GREEN:-\033[38;2;74;222;128m}"
42
- YELLOW="${YELLOW:-\033[38;2;250;204;21m}"
43
- RED="${RED:-\033[38;2;248;113;113m}"
44
- DIM="${DIM:-\033[2m}"
45
- BOLD="${BOLD:-\033[1m}"
46
- RESET="${RESET:-\033[0m}"
47
38
 
48
39
  # ─── Checkpoint Directory ───────────────────────────────────────────────────
49
40
  CHECKPOINT_DIR=".claude/pipeline-artifacts/checkpoints"
@@ -57,6 +48,102 @@ checkpoint_file() {
57
48
  echo "${CHECKPOINT_DIR}/${stage}-checkpoint.json"
58
49
  }
59
50
 
51
+ # Context file for Claude prompt reconstruction on resume
52
+ claude_context_file() {
53
+ local stage="$1"
54
+ echo "${CHECKPOINT_DIR}/${stage}-claude-context.json"
55
+ }
56
+
57
+ # ─── Save/Restore Claude Context (for meaningful resume) ────────────────────────
58
+ # Captures goal, findings, modified files, test output so resume can reconstruct prompt.
59
+ checkpoint_save_context() {
60
+ local stage="${1:-build}"
61
+ local context_file
62
+ context_file="$(claude_context_file "$stage")"
63
+ ensure_checkpoint_dir
64
+
65
+ local context="{}"
66
+
67
+ # Save current goal (from env or file)
68
+ local goal_content=""
69
+ if [[ -n "${SW_LOOP_GOAL:-}" ]]; then
70
+ goal_content="${SW_LOOP_GOAL}"
71
+ elif [[ -n "${SW_LOOP_GOAL_FILE:-}" && -f "${SW_LOOP_GOAL_FILE:-}" ]]; then
72
+ goal_content="$(cat "${SW_LOOP_GOAL_FILE}" 2>/dev/null || true)"
73
+ fi
74
+ if [[ -n "$goal_content" ]]; then
75
+ context=$(printf '%s' "$context" | jq --arg g "$goal_content" '.goal = $g' 2>/dev/null || echo "$context")
76
+ fi
77
+
78
+ # Save accumulated findings
79
+ local findings_content=""
80
+ if [[ -n "${SW_LOOP_FINDINGS:-}" ]]; then
81
+ findings_content="${SW_LOOP_FINDINGS}"
82
+ elif [[ -n "${SW_LOOP_FINDINGS_FILE:-}" && -f "${SW_LOOP_FINDINGS_FILE:-}" ]]; then
83
+ findings_content="$(cat "${SW_LOOP_FINDINGS_FILE}" 2>/dev/null || true)"
84
+ fi
85
+ if [[ -n "$findings_content" ]]; then
86
+ context=$(printf '%s' "$context" | jq --arg f "$findings_content" '.findings = $f' 2>/dev/null || echo "$context")
87
+ fi
88
+
89
+ # Save files modified (git diff)
90
+ local modified
91
+ modified="$(git diff --name-only HEAD 2>/dev/null | head -50 || true)"
92
+ if [[ -z "$modified" ]]; then
93
+ modified="${SW_LOOP_MODIFIED:-}"
94
+ fi
95
+ context=$(printf '%s' "$context" | jq --arg m "${modified:-}" '.modified_files = $m' 2>/dev/null || echo "$context")
96
+
97
+ # Save last test output
98
+ local test_content=""
99
+ if [[ -n "${SW_LOOP_TEST_OUTPUT:-}" ]]; then
100
+ test_content="${SW_LOOP_TEST_OUTPUT}"
101
+ elif [[ -n "${SW_LOOP_TEST_OUTPUT_FILE:-}" && -f "${SW_LOOP_TEST_OUTPUT_FILE:-}" ]]; then
102
+ test_content="$(tail -100 "${SW_LOOP_TEST_OUTPUT_FILE}" 2>/dev/null || true)"
103
+ fi
104
+ if [[ -n "$test_content" ]]; then
105
+ context=$(printf '%s' "$context" | jq --arg t "$test_content" '.last_test_output = $t' 2>/dev/null || echo "$context")
106
+ fi
107
+
108
+ # Save iteration count and status
109
+ local iter="${SW_LOOP_ITERATION:-0}"
110
+ local status="${SW_LOOP_STATUS:-running}"
111
+ context=$(printf '%s' "$context" | jq --argjson i "${iter}" --arg s "$status" '.iteration = ($i | tonumber) | .status = $s' 2>/dev/null || echo "$context")
112
+
113
+ printf '%s' "$context" > "$context_file" 2>/dev/null || true
114
+ }
115
+
116
+ checkpoint_restore_context() {
117
+ local stage="${1:-build}"
118
+ local context_file
119
+ context_file="$(claude_context_file "$stage")"
120
+
121
+ [[ ! -f "$context_file" ]] && return 1
122
+
123
+ local goal findings modified test_output iter status
124
+ goal="$(jq -r '.goal // empty' "$context_file" 2>/dev/null)"
125
+ findings="$(jq -r '.findings // empty' "$context_file" 2>/dev/null)"
126
+ modified="$(jq -r '.modified_files // empty' "$context_file" 2>/dev/null)"
127
+ test_output="$(jq -r '.last_test_output // empty' "$context_file" 2>/dev/null)"
128
+ iter="$(jq -r '.iteration // 0' "$context_file" 2>/dev/null)"
129
+ status="$(jq -r '.status // empty' "$context_file" 2>/dev/null)"
130
+
131
+ # Export both RESTORED_* (for restore-context CLI) and SW_LOOP_* (for sw-loop.sh)
132
+ export RESTORED_GOAL="$goal"
133
+ export RESTORED_FINDINGS="$findings"
134
+ export RESTORED_MODIFIED="$modified"
135
+ export RESTORED_TEST_OUTPUT="$test_output"
136
+ export RESTORED_ITERATION="$iter"
137
+ export RESTORED_STATUS="$status"
138
+ export SW_LOOP_GOAL="$goal"
139
+ export SW_LOOP_FINDINGS="$findings"
140
+ export SW_LOOP_MODIFIED="$modified"
141
+ export SW_LOOP_TEST_OUTPUT="$test_output"
142
+ export SW_LOOP_ITERATION="$iter"
143
+ export SW_LOOP_STATUS="$status"
144
+ return 0
145
+ }
146
+
60
147
  # ─── Save ────────────────────────────────────────────────────────────────────
61
148
 
62
149
  cmd_save() {
@@ -314,7 +401,10 @@ cmd_clear() {
314
401
  local file
315
402
  for file in "${CHECKPOINT_DIR}"/*-checkpoint.json; do
316
403
  [[ -f "$file" ]] || continue
404
+ local stage_name
405
+ stage_name="$(basename "$file" -checkpoint.json)"
317
406
  rm -f "$file"
407
+ rm -f "$(claude_context_file "$stage_name")" 2>/dev/null || true
318
408
  count=$((count + 1))
319
409
  done
320
410
  success "Cleared ${count} checkpoint(s)"
@@ -334,6 +424,7 @@ cmd_clear() {
334
424
 
335
425
  if [[ -f "$target" ]]; then
336
426
  rm -f "$target"
427
+ rm -f "$(claude_context_file "$stage")" 2>/dev/null || true
337
428
  success "Cleared checkpoint for stage ${BOLD}${stage}${RESET}"
338
429
  else
339
430
  warn "No checkpoint found for stage ${BOLD}${stage}${RESET}"
@@ -399,7 +490,7 @@ cmd_expire() {
399
490
  else
400
491
  # Fallback: check file mtime
401
492
  local mtime
402
- mtime=$(stat -f '%m' "$file" 2>/dev/null || stat -c '%Y' "$file" 2>/dev/null || echo "0")
493
+ mtime=$(file_mtime "$file")
403
494
  if [[ "$mtime" -gt 0 && $((now_e - mtime)) -gt $max_secs ]]; then
404
495
  rm -f "$file"
405
496
  expired=$((expired + 1))
@@ -421,8 +512,10 @@ show_help() {
421
512
  echo -e " ${CYAN}shipwright checkpoint${RESET} <command> [options]"
422
513
  echo ""
423
514
  echo -e "${BOLD}COMMANDS${RESET}"
424
- echo -e " ${CYAN}save${RESET} Save a checkpoint for a stage"
425
- echo -e " ${CYAN}restore${RESET} Restore a checkpoint (prints JSON to stdout)"
515
+ echo -e " ${CYAN}save${RESET} Save a checkpoint for a stage"
516
+ echo -e " ${CYAN}restore${RESET} Restore a checkpoint (prints JSON to stdout)"
517
+ echo -e " ${CYAN}save-context${RESET} Save Claude context (goal, findings, test output) for resume"
518
+ echo -e " ${CYAN}restore-context${RESET} Restore Claude context (exports RESTORED_* and SW_LOOP_* vars)"
426
519
  echo -e " ${CYAN}list${RESET} Show all available checkpoints"
427
520
  echo -e " ${CYAN}clear${RESET} Remove checkpoint(s)"
428
521
  echo -e " ${CYAN}expire${RESET} Remove checkpoints older than N hours"
@@ -455,6 +548,32 @@ show_help() {
455
548
  echo -e " ${DIM}shipwright checkpoint expire --hours 48${RESET}"
456
549
  }
457
550
 
551
+ # ─── Save Context / Restore Context (subcommands) ─────────────────────────────
552
+
553
+ cmd_save_context() {
554
+ local stage="build"
555
+ while [[ $# -gt 0 ]]; do
556
+ case "$1" in
557
+ --stage) stage="${2:-build}"; shift 2 ;;
558
+ --stage=*) stage="${1#--stage=}"; shift ;;
559
+ *) shift ;;
560
+ esac
561
+ done
562
+ checkpoint_save_context "$stage"
563
+ }
564
+
565
+ cmd_restore_context() {
566
+ local stage="build"
567
+ while [[ $# -gt 0 ]]; do
568
+ case "$1" in
569
+ --stage) stage="${2:-build}"; shift 2 ;;
570
+ --stage=*) stage="${1#--stage=}"; shift ;;
571
+ *) shift ;;
572
+ esac
573
+ done
574
+ checkpoint_restore_context "$stage"
575
+ }
576
+
458
577
  # ─── Command Router ─────────────────────────────────────────────────────────
459
578
 
460
579
  main() {
@@ -462,11 +581,13 @@ main() {
462
581
  shift 2>/dev/null || true
463
582
 
464
583
  case "$cmd" in
465
- save) cmd_save "$@" ;;
466
- restore) cmd_restore "$@" ;;
467
- list) cmd_list ;;
468
- clear) cmd_clear "$@" ;;
469
- expire) cmd_expire "$@" ;;
584
+ save) cmd_save "$@" ;;
585
+ restore) cmd_restore "$@" ;;
586
+ save-context) cmd_save_context "$@" ;;
587
+ restore-context) cmd_restore_context "$@" ;;
588
+ list) cmd_list ;;
589
+ clear) cmd_clear "$@" ;;
590
+ expire) cmd_expire "$@" ;;
470
591
  help|--help|-h) show_help ;;
471
592
  *)
472
593
  error "Unknown command: ${cmd}"
@@ -477,4 +598,7 @@ main() {
477
598
  esac
478
599
  }
479
600
 
480
- main "$@"
601
+ # Only run main when executed directly (not when sourced)
602
+ if [[ -n "${BASH_SOURCE[0]:-}" && "${BASH_SOURCE[0]}" == "${0}" ]]; then
603
+ main "$@"
604
+ fi
package/scripts/sw-ci.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.4.0"
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
  # ─── Generate Workflow YAML from Pipeline Template ──────────────────────────
48
38
 
49
39
  cmd_generate() {
@@ -464,7 +454,7 @@ cmd_validate() {
464
454
  info "Validating workflow YAML: $workflow_file"
465
455
 
466
456
  # Check for valid YAML structure
467
- if ! jq -e 'type' <<< "$(yq eval -o=json "$workflow_file" 2>/dev/null || echo '{}')" &>/dev/null; then
457
+ if ! jq -e 'type' <<< "$(yq eval -o=json "$workflow_file" 2>/dev/null || echo '{}')" >/dev/null 2>&1; then
468
458
  # Fallback: basic validation
469
459
  if grep -q "^name:" "$workflow_file" && grep -q "^on:" "$workflow_file" && grep -q "^jobs:" "$workflow_file"; then
470
460
  success "Workflow structure looks valid"
@@ -5,7 +5,7 @@
5
5
  # ║ Default: dry-run (shows what would be cleaned). ║
6
6
  # ║ Use --force to actually kill sessions and remove files. ║
7
7
  # ╚═══════════════════════════════════════════════════════════════════════════╝
8
- VERSION="2.4.0"
8
+ VERSION="3.0.0"
9
9
  set -euo pipefail
10
10
  trap 'echo "ERROR: $BASH_SOURCE:$LINENO exited with status $?" >&2' ERR
11
11
 
@@ -14,6 +14,12 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
14
14
  # Canonical helpers (colors, output, events)
15
15
  # shellcheck source=lib/helpers.sh
16
16
  [[ -f "$SCRIPT_DIR/lib/helpers.sh" ]] && source "$SCRIPT_DIR/lib/helpers.sh"
17
+ # shellcheck source=lib/compat.sh
18
+ [[ -f "$SCRIPT_DIR/lib/compat.sh" ]] && source "$SCRIPT_DIR/lib/compat.sh"
19
+ [[ -f "$SCRIPT_DIR/lib/config.sh" ]] && source "$SCRIPT_DIR/lib/config.sh"
20
+
21
+ PROJECT_ROOT="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
22
+
17
23
  # Fallbacks when helpers not loaded (e.g. test env with overridden SCRIPT_DIR)
18
24
  [[ "$(type -t info 2>/dev/null)" == "function" ]] || info() { echo -e "\033[38;2;0;212;255m\033[1m▸\033[0m $*"; }
19
25
  [[ "$(type -t success 2>/dev/null)" == "function" ]] || success() { echo -e "\033[38;2;74;222;128m\033[1m✓\033[0m $*"; }
@@ -31,16 +37,6 @@ if [[ "$(type -t emit_event 2>/dev/null)" != "function" ]]; then
31
37
  echo "${payload}}" >> "${HOME}/.shipwright/events.jsonl"
32
38
  }
33
39
  fi
34
- CYAN="${CYAN:-\033[38;2;0;212;255m}"
35
- PURPLE="${PURPLE:-\033[38;2;124;58;237m}"
36
- BLUE="${BLUE:-\033[38;2;0;102;255m}"
37
- GREEN="${GREEN:-\033[38;2;74;222;128m}"
38
- YELLOW="${YELLOW:-\033[38;2;250;204;21m}"
39
- RED="${RED:-\033[38;2;248;113;113m}"
40
- DIM="${DIM:-\033[2m}"
41
- BOLD="${BOLD:-\033[1m}"
42
- RESET="${RESET:-\033[0m}"
43
-
44
40
  # ─── Parse Args ──────────────────────────────────────────────────────────────
45
41
 
46
42
  FORCE=false
@@ -181,7 +177,7 @@ echo ""
181
177
  echo -e "${BOLD}Pipeline Artifacts${RESET} ${DIM}.claude/pipeline-artifacts/${RESET}"
182
178
  echo -e "${DIM}────────────────────────────────────────${RESET}"
183
179
 
184
- PIPELINE_ARTIFACTS=".claude/pipeline-artifacts"
180
+ PIPELINE_ARTIFACTS="$PROJECT_ROOT/.claude/pipeline-artifacts"
185
181
  if [[ -d "$PIPELINE_ARTIFACTS" ]]; then
186
182
  artifact_file_count=$(find "$PIPELINE_ARTIFACTS" -type f 2>/dev/null | wc -l | tr -d ' ')
187
183
  if [[ "${artifact_file_count:-0}" -gt 0 ]]; then
@@ -211,7 +207,7 @@ echo ""
211
207
  echo -e "${BOLD}Checkpoints${RESET} ${DIM}.claude/pipeline-artifacts/checkpoints/${RESET}"
212
208
  echo -e "${DIM}────────────────────────────────────────${RESET}"
213
209
 
214
- CHECKPOINT_DIR=".claude/pipeline-artifacts/checkpoints"
210
+ CHECKPOINT_DIR="$PROJECT_ROOT/.claude/pipeline-artifacts/checkpoints"
215
211
  if [[ -d "$CHECKPOINT_DIR" ]]; then
216
212
  cp_file_count=0
217
213
  for cp_file in "${CHECKPOINT_DIR}"/*-checkpoint.json; do
@@ -242,7 +238,7 @@ echo ""
242
238
  echo -e "${BOLD}Pipeline State${RESET} ${DIM}.claude/pipeline-state.md${RESET}"
243
239
  echo -e "${DIM}────────────────────────────────────────${RESET}"
244
240
 
245
- PIPELINE_STATE=".claude/pipeline-state.md"
241
+ PIPELINE_STATE="$PROJECT_ROOT/.claude/pipeline-state.md"
246
242
  if [[ -f "$PIPELINE_STATE" ]]; then
247
243
  state_status=$(sed -n 's/^status: *//p' "$PIPELINE_STATE" | head -1 || true)
248
244
  state_issue=$(sed -n 's/^issue: *//p' "$PIPELINE_STATE" | head -1 || true)
@@ -277,11 +273,11 @@ echo -e "${DIM}─────────────────────
277
273
  HEARTBEAT_DIR="${HOME}/.shipwright/heartbeats"
278
274
  if [[ -d "$HEARTBEAT_DIR" ]]; then
279
275
  now_e=$(date +%s)
280
- stale_threshold=3600 # 1 hour
276
+ stale_threshold=$(_config_get_int "cleanup.heartbeat_stale_seconds" 3600)
281
277
 
282
278
  while IFS= read -r hb_file; do
283
279
  [[ -f "$hb_file" ]] || continue
284
- hb_mtime=$(stat -f '%m' "$hb_file" 2>/dev/null || stat -c '%Y' "$hb_file" 2>/dev/null || echo "0")
280
+ hb_mtime=$(file_mtime "$hb_file")
285
281
  if [[ $((now_e - hb_mtime)) -gt $stale_threshold ]]; then
286
282
  HEARTBEATS_FOUND=$((HEARTBEATS_FOUND + 1))
287
283
  hb_name=$(basename "$hb_file" .json)
@@ -310,7 +306,7 @@ echo ""
310
306
  echo -e "${BOLD}Orphaned Branches${RESET} ${DIM}pipeline/* and daemon/*${RESET}"
311
307
  echo -e "${DIM}────────────────────────────────────────${RESET}"
312
308
 
313
- if command -v git &>/dev/null && git rev-parse --is-inside-work-tree &>/dev/null; then
309
+ if command -v git >/dev/null 2>&1 && git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
314
310
  # Collect active worktree paths
315
311
  active_worktrees=""
316
312
  while IFS= read -r wt_line; do