shipwright-cli 3.0.0 → 3.2.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 (143) hide show
  1. package/README.md +21 -7
  2. package/completions/_shipwright +247 -93
  3. package/completions/shipwright.bash +69 -15
  4. package/completions/shipwright.fish +309 -41
  5. package/config/decision-tiers.json +55 -0
  6. package/config/defaults.json +25 -2
  7. package/config/event-schema.json +142 -5
  8. package/config/policy.json +8 -0
  9. package/dashboard/public/index.html +6 -0
  10. package/dashboard/public/styles.css +76 -0
  11. package/dashboard/server.ts +51 -0
  12. package/dashboard/src/core/api.ts +5 -0
  13. package/dashboard/src/types/api.ts +10 -0
  14. package/dashboard/src/views/metrics.ts +69 -1
  15. package/package.json +3 -3
  16. package/scripts/lib/architecture.sh +2 -1
  17. package/scripts/lib/bootstrap.sh +0 -0
  18. package/scripts/lib/config.sh +0 -0
  19. package/scripts/lib/daemon-adaptive.sh +4 -2
  20. package/scripts/lib/daemon-dispatch.sh +24 -1
  21. package/scripts/lib/daemon-failure.sh +0 -0
  22. package/scripts/lib/daemon-health.sh +0 -0
  23. package/scripts/lib/daemon-patrol.sh +42 -7
  24. package/scripts/lib/daemon-poll.sh +17 -0
  25. package/scripts/lib/daemon-state.sh +17 -0
  26. package/scripts/lib/daemon-triage.sh +1 -1
  27. package/scripts/lib/decide-autonomy.sh +295 -0
  28. package/scripts/lib/decide-scoring.sh +228 -0
  29. package/scripts/lib/decide-signals.sh +462 -0
  30. package/scripts/lib/fleet-failover.sh +0 -0
  31. package/scripts/lib/helpers.sh +19 -18
  32. package/scripts/lib/pipeline-detection.sh +1 -1
  33. package/scripts/lib/pipeline-github.sh +0 -0
  34. package/scripts/lib/pipeline-intelligence.sh +23 -4
  35. package/scripts/lib/pipeline-quality-checks.sh +11 -6
  36. package/scripts/lib/pipeline-quality.sh +0 -0
  37. package/scripts/lib/pipeline-stages.sh +330 -33
  38. package/scripts/lib/pipeline-state.sh +14 -0
  39. package/scripts/lib/policy.sh +0 -0
  40. package/scripts/lib/test-helpers.sh +0 -0
  41. package/scripts/postinstall.mjs +75 -1
  42. package/scripts/signals/example-collector.sh +36 -0
  43. package/scripts/sw +8 -4
  44. package/scripts/sw-activity.sh +1 -7
  45. package/scripts/sw-adaptive.sh +7 -7
  46. package/scripts/sw-adversarial.sh +1 -1
  47. package/scripts/sw-architecture-enforcer.sh +1 -1
  48. package/scripts/sw-auth.sh +1 -1
  49. package/scripts/sw-autonomous.sh +1 -1
  50. package/scripts/sw-changelog.sh +1 -1
  51. package/scripts/sw-checkpoint.sh +1 -1
  52. package/scripts/sw-ci.sh +11 -6
  53. package/scripts/sw-cleanup.sh +1 -1
  54. package/scripts/sw-code-review.sh +36 -17
  55. package/scripts/sw-connect.sh +1 -1
  56. package/scripts/sw-context.sh +1 -1
  57. package/scripts/sw-cost.sh +71 -5
  58. package/scripts/sw-daemon.sh +6 -3
  59. package/scripts/sw-dashboard.sh +1 -1
  60. package/scripts/sw-db.sh +53 -38
  61. package/scripts/sw-decide.sh +685 -0
  62. package/scripts/sw-decompose.sh +1 -1
  63. package/scripts/sw-deps.sh +1 -1
  64. package/scripts/sw-developer-simulation.sh +1 -1
  65. package/scripts/sw-discovery.sh +80 -4
  66. package/scripts/sw-doc-fleet.sh +1 -1
  67. package/scripts/sw-docs-agent.sh +1 -1
  68. package/scripts/sw-docs.sh +1 -1
  69. package/scripts/sw-doctor.sh +1 -1
  70. package/scripts/sw-dora.sh +1 -1
  71. package/scripts/sw-durable.sh +9 -5
  72. package/scripts/sw-e2e-orchestrator.sh +1 -1
  73. package/scripts/sw-eventbus.sh +7 -4
  74. package/scripts/sw-evidence.sh +1 -1
  75. package/scripts/sw-feedback.sh +1 -1
  76. package/scripts/sw-fix.sh +1 -1
  77. package/scripts/sw-fleet-discover.sh +1 -1
  78. package/scripts/sw-fleet-viz.sh +6 -4
  79. package/scripts/sw-fleet.sh +1 -1
  80. package/scripts/sw-github-app.sh +3 -2
  81. package/scripts/sw-github-checks.sh +1 -1
  82. package/scripts/sw-github-deploy.sh +1 -1
  83. package/scripts/sw-github-graphql.sh +1 -1
  84. package/scripts/sw-guild.sh +1 -1
  85. package/scripts/sw-heartbeat.sh +1 -1
  86. package/scripts/sw-hygiene.sh +5 -3
  87. package/scripts/sw-incident.sh +9 -5
  88. package/scripts/sw-init.sh +1 -1
  89. package/scripts/sw-instrument.sh +1 -1
  90. package/scripts/sw-intelligence.sh +11 -6
  91. package/scripts/sw-jira.sh +1 -1
  92. package/scripts/sw-launchd.sh +1 -1
  93. package/scripts/sw-linear.sh +1 -1
  94. package/scripts/sw-logs.sh +1 -1
  95. package/scripts/sw-loop.sh +338 -32
  96. package/scripts/sw-memory.sh +23 -6
  97. package/scripts/sw-mission-control.sh +1 -1
  98. package/scripts/sw-model-router.sh +3 -2
  99. package/scripts/sw-otel.sh +8 -4
  100. package/scripts/sw-oversight.sh +1 -1
  101. package/scripts/sw-pipeline-composer.sh +3 -1
  102. package/scripts/sw-pipeline-vitals.sh +11 -6
  103. package/scripts/sw-pipeline.sh +92 -8
  104. package/scripts/sw-pm.sh +5 -4
  105. package/scripts/sw-pr-lifecycle.sh +7 -4
  106. package/scripts/sw-predictive.sh +11 -5
  107. package/scripts/sw-prep.sh +1 -1
  108. package/scripts/sw-ps.sh +1 -1
  109. package/scripts/sw-public-dashboard.sh +3 -2
  110. package/scripts/sw-quality.sh +21 -10
  111. package/scripts/sw-reaper.sh +1 -1
  112. package/scripts/sw-recruit.sh +1 -1
  113. package/scripts/sw-regression.sh +1 -1
  114. package/scripts/sw-release-manager.sh +1 -1
  115. package/scripts/sw-release.sh +1 -1
  116. package/scripts/sw-remote.sh +1 -1
  117. package/scripts/sw-replay.sh +1 -1
  118. package/scripts/sw-retro.sh +1 -1
  119. package/scripts/sw-review-rerun.sh +1 -1
  120. package/scripts/sw-scale.sh +69 -11
  121. package/scripts/sw-security-audit.sh +1 -1
  122. package/scripts/sw-self-optimize.sh +168 -4
  123. package/scripts/sw-session.sh +3 -3
  124. package/scripts/sw-setup.sh +1 -1
  125. package/scripts/sw-standup.sh +1 -1
  126. package/scripts/sw-status.sh +1 -1
  127. package/scripts/sw-strategic.sh +11 -6
  128. package/scripts/sw-stream.sh +7 -4
  129. package/scripts/sw-swarm.sh +3 -2
  130. package/scripts/sw-team-stages.sh +1 -1
  131. package/scripts/sw-templates.sh +3 -3
  132. package/scripts/sw-testgen.sh +11 -6
  133. package/scripts/sw-tmux-pipeline.sh +1 -1
  134. package/scripts/sw-tmux.sh +35 -1
  135. package/scripts/sw-trace.sh +1 -1
  136. package/scripts/sw-tracker.sh +1 -1
  137. package/scripts/sw-triage.sh +7 -7
  138. package/scripts/sw-upgrade.sh +1 -1
  139. package/scripts/sw-ux.sh +1 -1
  140. package/scripts/sw-webhook.sh +3 -2
  141. package/scripts/sw-widgets.sh +7 -4
  142. package/scripts/sw-worktree.sh +1 -1
  143. package/scripts/update-homebrew-sha.sh +21 -15
@@ -11,8 +11,11 @@ import {
11
11
  readFileSync,
12
12
  writeFileSync,
13
13
  appendFileSync,
14
+ chmodSync,
15
+ readdirSync,
14
16
  } from "fs";
15
- import { join } from "path";
17
+ import { join, basename } from "path";
18
+ import { execSync } from "child_process";
16
19
 
17
20
  const HOME = process.env.HOME || process.env.USERPROFILE;
18
21
  const PKG_DIR = join(import.meta.dirname, "..");
@@ -130,6 +133,77 @@ try {
130
133
  success("Migrated legacy config (originals preserved)");
131
134
  }
132
135
 
136
+ // Set executable bits on all scripts (npm strips them on some platforms)
137
+ const scriptsDir = join(PKG_DIR, "scripts");
138
+ if (existsSync(scriptsDir)) {
139
+ let madeExecutable = 0;
140
+ for (const file of readdirSync(scriptsDir)) {
141
+ const fp = join(scriptsDir, file);
142
+ try {
143
+ chmodSync(fp, 0o755);
144
+ madeExecutable++;
145
+ } catch (_) {
146
+ // skip non-files
147
+ }
148
+ }
149
+ const libDir = join(scriptsDir, "lib");
150
+ if (existsSync(libDir)) {
151
+ for (const file of readdirSync(libDir)) {
152
+ try {
153
+ chmodSync(join(libDir, file), 0o755);
154
+ madeExecutable++;
155
+ } catch (_) {}
156
+ }
157
+ }
158
+ success(`Set executable bits on ${madeExecutable} scripts`);
159
+ }
160
+
161
+ // Install shell completions for the user's current shell
162
+ const completionsDir = join(PKG_DIR, "completions");
163
+ if (existsSync(completionsDir)) {
164
+ const shell = basename(process.env.SHELL || "/bin/bash");
165
+ try {
166
+ if (shell === "bash") {
167
+ const dest =
168
+ process.env.BASH_COMPLETION_USER_DIR ||
169
+ join(
170
+ process.env.XDG_DATA_HOME || join(HOME, ".local", "share"),
171
+ "bash-completion",
172
+ "completions",
173
+ );
174
+ ensureDir(dest);
175
+ cpSync(
176
+ join(completionsDir, "shipwright.bash"),
177
+ join(dest, "shipwright"),
178
+ );
179
+ cpSync(join(completionsDir, "shipwright.bash"), join(dest, "sw"));
180
+ success(`Installed bash completions to ${dest}`);
181
+ } else if (shell === "zsh") {
182
+ const dest = join(HOME, ".zfunc");
183
+ ensureDir(dest);
184
+ cpSync(join(completionsDir, "_shipwright"), join(dest, "_shipwright"));
185
+ cpSync(join(completionsDir, "_shipwright"), join(dest, "_sw"));
186
+ success(`Installed zsh completions to ${dest}`);
187
+ } else if (shell === "fish") {
188
+ const dest = join(
189
+ process.env.XDG_CONFIG_HOME || join(HOME, ".config"),
190
+ "fish",
191
+ "completions",
192
+ );
193
+ ensureDir(dest);
194
+ cpSync(
195
+ join(completionsDir, "shipwright.fish"),
196
+ join(dest, "shipwright.fish"),
197
+ );
198
+ cpSync(join(completionsDir, "shipwright.fish"), join(dest, "sw.fish"));
199
+ success(`Installed fish completions to ${dest}`);
200
+ }
201
+ } catch (e) {
202
+ warn(`Could not auto-install completions: ${e.message}`);
203
+ info(`Run: shipwright init (or: bash scripts/install-completions.sh)`);
204
+ }
205
+ }
206
+
133
207
  // Print success banner
134
208
  console.log();
135
209
  console.log(`${GREEN}${BOLD}Shipwright CLI installed!${RESET} Next steps:`);
@@ -0,0 +1,36 @@
1
+ #!/usr/bin/env bash
2
+ # ╔═══════════════════════════════════════════════════════════════════════════╗
3
+ # ║ Example External Signal Collector for Shipwright Decision Engine ║
4
+ # ║ Place custom collectors in scripts/signals/ — they're auto-discovered ║
5
+ # ╚═══════════════════════════════════════════════════════════════════════════╝
6
+ #
7
+ # Output: one JSON candidate per line (JSONL).
8
+ # Required fields: id, signal, category, title, description, risk_score,
9
+ # confidence, dedup_key
10
+ # Optional fields: evidence (object)
11
+ #
12
+ # The decision engine collects output from all scripts/signals/*.sh files,
13
+ # validates each line as JSON, and includes valid candidates in the scoring
14
+ # pipeline.
15
+ #
16
+ # Categories (determines autonomy tier):
17
+ # auto: deps_patch, deps_minor, security_patch, test_coverage,
18
+ # doc_sync, dead_code
19
+ # propose: refactor_hotspot, architecture_drift, performance_regression,
20
+ # deps_major, security_critical, recurring_failure, dora_regression
21
+ # draft: new_feature, breaking_change, business_logic, api_change,
22
+ # data_model_change
23
+ #
24
+ # Example: detect a custom condition and emit a candidate
25
+ #
26
+
27
+ set -euo pipefail
28
+
29
+ # Example: check if a TODO count exceeds a threshold
30
+ TODO_COUNT=$(grep -r "TODO" --include="*.ts" --include="*.js" --include="*.sh" . 2>/dev/null | wc -l | tr -d ' ' || echo "0")
31
+
32
+ if [[ "${TODO_COUNT:-0}" -gt 50 ]]; then
33
+ cat <<EOF
34
+ {"id":"custom-todo-cleanup","signal":"custom","category":"dead_code","title":"Clean up ${TODO_COUNT} TODOs","description":"Codebase has ${TODO_COUNT} TODO comments — consider a cleanup sprint","evidence":{"todo_count":${TODO_COUNT}},"risk_score":15,"confidence":"0.70","dedup_key":"custom:todo:cleanup"}
35
+ EOF
36
+ fi
package/scripts/sw CHANGED
@@ -5,7 +5,7 @@
5
5
  # ╚═══════════════════════════════════════════════════════════════════════════╝
6
6
  set -euo pipefail
7
7
 
8
- VERSION="3.0.0"
8
+ VERSION="3.2.0"
9
9
 
10
10
  # Resolve symlinks (required for npm global install where bin/ symlinks to node_modules/)
11
11
  SOURCE="${BASH_SOURCE[0]}"
@@ -193,7 +193,8 @@ route_quality() {
193
193
  security-audit|audit) exec "$SCRIPT_DIR/sw-security-audit.sh" "$@" ;;
194
194
  testgen) exec "$SCRIPT_DIR/sw-testgen.sh" "$@" ;;
195
195
  hygiene) exec "$SCRIPT_DIR/sw-hygiene.sh" "$@" ;;
196
- help|*) echo "Usage: shipwright quality {code-review|security-audit|testgen|hygiene}"; exit 1 ;;
196
+ validate|gate) exec "$SCRIPT_DIR/sw-quality.sh" "$@" ;;
197
+ help|*) echo "Usage: shipwright quality {code-review|security-audit|testgen|hygiene|validate}"; exit 1 ;;
197
198
  esac
198
199
  }
199
200
 
@@ -530,14 +531,17 @@ main() {
530
531
  evidence|ev)
531
532
  exec "$SCRIPT_DIR/sw-evidence.sh" "$@"
532
533
  ;;
534
+ decide)
535
+ exec "$SCRIPT_DIR/sw-decide.sh" "$@"
536
+ ;;
533
537
  otel)
534
538
  exec "$SCRIPT_DIR/sw-otel.sh" "$@"
535
539
  ;;
536
540
  triage)
537
541
  exec "$SCRIPT_DIR/sw-triage.sh" "$@"
538
542
  ;;
539
- quality)
540
- exec "$SCRIPT_DIR/sw-quality.sh" "$@"
543
+ pipeline-composer|composer)
544
+ exec "$SCRIPT_DIR/sw-pipeline-composer.sh" "$@"
541
545
  ;;
542
546
  oversight)
543
547
  exec "$SCRIPT_DIR/sw-oversight.sh" "$@"
@@ -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="3.0.0"
9
+ VERSION="3.2.0"
10
10
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
11
11
 
12
12
  # ─── Cross-platform compatibility ──────────────────────────────────────────
@@ -332,7 +332,6 @@ cmd_stats() {
332
332
  local end_time=""
333
333
 
334
334
  # Read directly to avoid subshell issues
335
- echo "DEBUG: Starting read loop..." >&2
336
335
  while IFS= read -r line; do
337
336
  [ -z "$line" ] && continue
338
337
 
@@ -363,14 +362,9 @@ cmd_stats() {
363
362
  end_time="$ts"
364
363
  done < <(grep -v '^$' "$EVENTS_FILE" 2>/dev/null)
365
364
 
366
- echo "DEBUG: Read complete, total=$total_events" >&2
367
- echo "DEBUG: agents_seen length: ${#agents_seen}" >&2
368
365
  local unique_agents
369
- echo "DEBUG: About to compute unique..." >&2
370
366
  unique_agents=$(sort -u <<< "$agents_seen" | grep -v '^$' | wc -l | tr -d ' ')
371
- echo "DEBUG: unique_agents=$unique_agents" >&2
372
367
 
373
- echo "DEBUG: About to print results..." >&2
374
368
  printf "${BOLD}Total Events:${RESET} %d\n" "$total_events"
375
369
  printf "${BOLD}Commits:${RESET} %d\n" "$commits"
376
370
  printf "${BOLD}Tests:${RESET} %d\n" "$tests"
@@ -1,12 +1,12 @@
1
1
  #!/usr/bin/env bash
2
2
  # ╔═══════════════════════════════════════════════════════════════════════════╗
3
3
  # ║ shipwright adaptive — data-driven pipeline tuning ║
4
- # ║ Replace 83+ hardcoded values with learned defaults from historical runs
4
+ # ║ Replace static defaults with learned values from historical runs
5
5
  # ╚═══════════════════════════════════════════════════════════════════════════╝
6
6
  set -euo pipefail
7
7
  trap 'echo "ERROR: $BASH_SOURCE:$LINENO exited with status $?" >&2' ERR
8
8
 
9
- VERSION="3.0.0"
9
+ VERSION="3.2.0"
10
10
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
11
11
 
12
12
  # ─── Cross-platform compatibility ──────────────────────────────────────────
@@ -712,10 +712,10 @@ cmd_compare() {
712
712
  esac
713
713
  done
714
714
 
715
- info "Learned vs Hardcoded Values for ${CYAN}${repo}${RESET}"
715
+ info "Learned vs Default Values for ${CYAN}${repo}${RESET}"
716
716
  echo ""
717
717
 
718
- printf "%-25s %-15s %-15s %-15s\n" "Metric" "Hardcoded" "Learned" "Difference"
718
+ printf "%-25s %-15s %-15s %-15s\n" "Metric" "Default" "Learned" "Difference"
719
719
  printf "%s\n" "$(printf '%.0s─' {1..70})"
720
720
 
721
721
  # Timeout
@@ -841,7 +841,7 @@ ${BOLD}USAGE${RESET}
841
841
 
842
842
  ${BOLD}SUBCOMMANDS${RESET}
843
843
  ${CYAN}get${RESET} <metric> [--stage S] [--repo R] [--complexity C] [--default V]
844
- Return adaptive value for a metric (replaces hardcoded defaults)
844
+ Return adaptive value for a metric (replaces static defaults)
845
845
  Metrics: timeout, iterations, model, team_size, template, poll_interval,
846
846
  retry_limit, quality_threshold, coverage_min
847
847
 
@@ -852,7 +852,7 @@ ${BOLD}SUBCOMMANDS${RESET}
852
852
  Rebuild models from events.jsonl (run after significant pipeline activity)
853
853
 
854
854
  ${CYAN}compare${RESET} [--repo REPO]
855
- Side-by-side table: learned vs hardcoded values
855
+ Side-by-side table: learned vs default values
856
856
 
857
857
  ${CYAN}recommend${RESET} --issue N [--repo REPO]
858
858
  Full JSON recommendation for an issue (template, model, team_size, etc.)
@@ -876,7 +876,7 @@ ${BOLD}EXAMPLES${RESET}
876
876
  # Get complete recommendation for issue #42
877
877
  sw adaptive recommend --issue 42
878
878
 
879
- # Compare learned vs hardcoded
879
+ # Compare learned vs defaults
880
880
  sw adaptive compare
881
881
 
882
882
  ${BOLD}STORAGE${RESET}
@@ -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="3.0.0"
9
+ VERSION="3.2.0"
10
10
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
11
11
  REPO_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
12
12
 
@@ -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="3.0.0"
9
+ VERSION="3.2.0"
10
10
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
11
11
  REPO_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
12
12
 
@@ -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="3.0.0"
9
+ VERSION="3.2.0"
10
10
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
11
11
  REPO_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
12
12
 
@@ -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="3.0.0"
10
+ VERSION="3.2.0"
11
11
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
12
12
  REPO_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
13
13
 
@@ -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="3.0.0"
10
+ VERSION="3.2.0"
11
11
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
12
12
  REPO_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
13
13
 
@@ -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="3.0.0"
11
+ VERSION="3.2.0"
12
12
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd)"
13
13
 
14
14
  # ─── Cross-platform compatibility ──────────────────────────────────────────
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="3.0.0"
9
+ VERSION="3.2.0"
10
10
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
11
11
  REPO_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
12
12
 
@@ -227,15 +227,20 @@ cmd_analyze() {
227
227
  info "Analyzing workflow efficiency"
228
228
 
229
229
  local job_count step_count matrix_enabled
230
- job_count=$(grep -c "^ [a-z_-]*:" "$workflow_file" 2>/dev/null || echo "0")
231
- step_count=$(grep -c " - name:" "$workflow_file" 2>/dev/null || echo "0")
232
- matrix_enabled=$(grep -c "matrix:" "$workflow_file" 2>/dev/null || echo "0")
230
+ job_count=$(grep -c "^ [a-z_-]*:" "$workflow_file" 2>/dev/null || true)
231
+ job_count="${job_count:-0}"
232
+ step_count=$(grep -c " - name:" "$workflow_file" 2>/dev/null || true)
233
+ step_count="${step_count:-0}"
234
+ matrix_enabled=$(grep -c "matrix:" "$workflow_file" 2>/dev/null || true)
235
+ matrix_enabled="${matrix_enabled:-0}"
233
236
 
234
237
  local has_cache
235
- has_cache=$(grep -c "actions/cache" "$workflow_file" 2>/dev/null || echo "0")
238
+ has_cache=$(grep -c "actions/cache" "$workflow_file" 2>/dev/null || true)
239
+ has_cache="${has_cache:-0}"
236
240
 
237
241
  local has_timeout
238
- has_timeout=$(grep -c "timeout-minutes:" "$workflow_file" 2>/dev/null || echo "0")
242
+ has_timeout=$(grep -c "timeout-minutes:" "$workflow_file" 2>/dev/null || true)
243
+ has_timeout="${has_timeout:-0}"
239
244
 
240
245
  echo ""
241
246
  echo -e "${BOLD}Workflow Analysis: $(basename "$workflow_file")${RESET}"
@@ -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="3.0.0"
8
+ VERSION="3.2.0"
9
9
  set -euo pipefail
10
10
  trap 'echo "ERROR: $BASH_SOURCE:$LINENO exited with status $?" >&2' ERR
11
11
 
@@ -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="3.0.0"
9
+ VERSION="3.2.0"
10
10
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
11
11
  REPO_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
12
12
 
@@ -72,7 +72,8 @@ detect_code_smells() {
72
72
 
73
73
  # Check 1: Long functions (>60 lines in bash)
74
74
  local func_count
75
- func_count=$(grep -c "^[a-zA-Z_][a-zA-Z0-9_]*().*{" "$target_file" 2>/dev/null || echo "0")
75
+ func_count=$(grep -c "^[a-zA-Z_][a-zA-Z0-9_]*().*{" "$target_file" 2>/dev/null || true)
76
+ func_count="${func_count:-0}"
76
77
  if [[ "$func_count" -gt 0 ]]; then
77
78
  while IFS= read -r line; do
78
79
  if [[ "$line" =~ ^[a-zA-Z_][a-zA-Z0-9_]*\(\).*\{ ]]; then
@@ -106,7 +107,8 @@ detect_code_smells() {
106
107
 
107
108
  # Check 3: Duplicate code patterns (repeated >3 times)
108
109
  local dup_count=0
109
- dup_count=$(grep -c '^\s*\(cd\|cd\|mkdir\|rm\|echo\)' "$target_file" 2>/dev/null || echo "0")
110
+ dup_count=$(grep -c '^\s*\(cd\|cd\|mkdir\|rm\|echo\)' "$target_file" 2>/dev/null || true)
111
+ dup_count="${dup_count:-0}"
110
112
  if [[ $dup_count -gt 3 ]]; then
111
113
  issues+=("REPEATED_PATTERNS: Common operations appear $dup_count times (consider helper functions)")
112
114
  fi
@@ -139,7 +141,8 @@ check_solid_principles() {
139
141
 
140
142
  # Single Responsibility: Check if scripts do multiple unrelated things
141
143
  local sourced_count
142
- sourced_count=$(grep -c '^\s*source\|^\s*\.\s' "$target_file" 2>/dev/null || echo "0")
144
+ sourced_count=$(grep -c '^\s*source\|^\s*\.\s' "$target_file" 2>/dev/null || true)
145
+ sourced_count="${sourced_count:-0}"
143
146
  if [[ $sourced_count -gt 3 ]]; then
144
147
  violations+=("SRP_VIOLATION: Script sources $sourced_count modules (too many responsibilities)")
145
148
  fi
@@ -159,7 +162,8 @@ check_solid_principles() {
159
162
  if [[ "$line" =~ ^[a-zA-Z_][a-zA-Z0-9_]*\(\) ]]; then
160
163
  local func_name="${line%%(*}"
161
164
  local body
162
- body=$(awk "/^${func_name}\\(\\)/,/^}/" "$target_file" | grep -c '\$[0-9]' || echo "0")
165
+ body=$(awk "/^${func_name}\\(\\)/,/^}/" "$target_file" | grep -c '\$[0-9]' || true)
166
+ body="${body:-0}"
163
167
  if [[ $body -gt 5 ]]; then
164
168
  violations+=("ISP_VIOLATION: Function $func_name uses >5 parameters")
165
169
  fi
@@ -227,7 +231,9 @@ analyze_complexity() {
227
231
 
228
232
  # Cyclomatic complexity: count decision points (if, elif, case, &&, ||)
229
233
  local cc=1
230
- cc=$((cc + $(sed -n "${start_line},${end_line}p" "$target_file" | grep -cE '\s(if|elif|case|&&|\|\|)' || echo 0)))
234
+ local _cc_count
235
+ _cc_count=$(sed -n "${start_line},${end_line}p" "$target_file" | grep -cE '\s(if|elif|case|&&|\|\|)' || true)
236
+ cc=$((cc + ${_cc_count:-0}))
231
237
 
232
238
  # Lines of code
233
239
  local loc=$((end_line - start_line))
@@ -266,8 +272,12 @@ check_style_consistency() {
266
272
  local has_trap=false
267
273
  local has_set_e=false
268
274
 
269
- [[ $(grep -c 'trap.*ERR' "$target_file" 2>/dev/null || echo 0) -gt 0 ]] && has_trap=true
270
- [[ $(grep -c 'set -e' "$target_file" 2>/dev/null || echo 0) -gt 0 ]] && has_set_e=true
275
+ local _trap_count
276
+ _trap_count=$(grep -c 'trap.*ERR' "$target_file" 2>/dev/null || true)
277
+ [[ "${_trap_count:-0}" -gt 0 ]] && has_trap=true
278
+ local _set_e_count
279
+ _set_e_count=$(grep -c 'set -e' "$target_file" 2>/dev/null || true)
280
+ [[ "${_set_e_count:-0}" -gt 0 ]] && has_set_e=true
271
281
 
272
282
  if [[ "$has_set_e" == "true" ]] && [[ "$has_trap" == "false" ]]; then
273
283
  issues+=("STYLE: Missing ERR trap despite 'set -e' (inconsistent error handling)")
@@ -276,8 +286,10 @@ check_style_consistency() {
276
286
  # Check for inconsistent quote usage
277
287
  local single_quotes
278
288
  local double_quotes
279
- single_quotes=$(grep -o "'" "$target_file" 2>/dev/null | wc -l || echo 0)
280
- double_quotes=$(grep -o '"' "$target_file" 2>/dev/null | wc -l || echo 0)
289
+ single_quotes=$(grep -o "'" "$target_file" 2>/dev/null | wc -l || true)
290
+ single_quotes="${single_quotes:-0}"
291
+ double_quotes=$(grep -o '"' "$target_file" 2>/dev/null | wc -l || true)
292
+ double_quotes="${double_quotes:-0}"
281
293
  if [[ $single_quotes -gt $((double_quotes * 3)) ]] || [[ $double_quotes -gt $((single_quotes * 3)) ]]; then
282
294
  issues+=("STYLE: Inconsistent quote style (mix of single and double quotes)")
283
295
  fi
@@ -285,8 +297,10 @@ check_style_consistency() {
285
297
  # Check for inconsistent spacing/indentation
286
298
  local tab_count
287
299
  local space_count
288
- tab_count=$(grep -c $'^\t' "$target_file" 2>/dev/null || echo 0)
289
- space_count=$(grep -c '^ ' "$target_file" 2>/dev/null || echo 0)
300
+ tab_count=$(grep -c $'^\t' "$target_file" 2>/dev/null || true)
301
+ tab_count="${tab_count:-0}"
302
+ space_count=$(grep -c '^ ' "$target_file" 2>/dev/null || true)
303
+ space_count="${space_count:-0}"
290
304
  if [[ $tab_count -gt 0 ]] && [[ $space_count -gt 0 ]]; then
291
305
  issues+=("STYLE: Mixed tabs and spaces")
292
306
  fi
@@ -333,7 +347,8 @@ auto_fix() {
333
347
 
334
348
  # Fix 2: Trailing whitespace
335
349
  local trailing_ws
336
- trailing_ws=$(grep -c '[[:space:]]$' "$target_file" 2>/dev/null || echo "0")
350
+ trailing_ws=$(grep -c '[[:space:]]$' "$target_file" 2>/dev/null || true)
351
+ trailing_ws="${trailing_ws:-0}"
337
352
  if [[ $trailing_ws -gt 0 ]]; then
338
353
  sed -i '' 's/[[:space:]]*$//' "$target_file"
339
354
  info "Removed $trailing_ws lines of trailing whitespace"
@@ -349,7 +364,8 @@ auto_fix() {
349
364
 
350
365
  # Fix 4: Consistent spacing around operators (simple cases)
351
366
  local spacing_fixes=0
352
- spacing_fixes=$(grep -c '==' "$target_file" 2>/dev/null || echo "0")
367
+ spacing_fixes=$(grep -c '==' "$target_file" 2>/dev/null || true)
368
+ spacing_fixes="${spacing_fixes:-0}"
353
369
  if [[ $spacing_fixes -gt 0 ]]; then
354
370
  info "Flagged $spacing_fixes operator spacing cases (manual review recommended)"
355
371
  fi
@@ -507,9 +523,12 @@ scan_codebase() {
507
523
  local solids=0
508
524
  local arch_issues=0
509
525
 
510
- smells=$(detect_code_smells "$file" 2>/dev/null | wc -l || echo 0)
511
- solids=$(check_solid_principles "$file" 2>/dev/null | wc -l || echo 0)
512
- arch_issues=$(check_architecture_boundaries "$file" 2>/dev/null | wc -l || echo 0)
526
+ smells=$(detect_code_smells "$file" 2>/dev/null | wc -l || true)
527
+ smells="${smells:-0}"
528
+ solids=$(check_solid_principles "$file" 2>/dev/null | wc -l || true)
529
+ solids="${solids:-0}"
530
+ arch_issues=$(check_architecture_boundaries "$file" 2>/dev/null | wc -l || true)
531
+ arch_issues="${arch_issues:-0}"
513
532
 
514
533
  local file_issues=$((smells + solids + arch_issues))
515
534
  total_issues=$((total_issues + file_issues))
@@ -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="3.0.0"
11
+ VERSION="3.2.0"
12
12
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
13
13
 
14
14
  # ─── 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="3.0.0"
9
+ VERSION="3.2.0"
10
10
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
11
11
  REPO_DIR="${SHIPWRIGHT_REPO_DIR:-$(cd "$SCRIPT_DIR/.." && pwd)}"
12
12
 
@@ -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="3.0.0"
9
+ VERSION="3.2.0"
10
10
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
11
11
  REPO_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
12
12
 
@@ -142,9 +142,9 @@ cost_record() {
142
142
  local cost_usd
143
143
  cost_usd=$(cost_calculate "$input_tokens" "$output_tokens" "$model")
144
144
 
145
- # Try SQLite first
145
+ # Try SQLite first (arg order must match db_record_cost signature: tokens, tokens, model, cost, stage, issue)
146
146
  if type db_record_cost >/dev/null 2>&1; then
147
- db_record_cost "$input_tokens" "$output_tokens" "$model" "$stage" "$cost_usd" "$issue" 2>/dev/null || true
147
+ db_record_cost "$input_tokens" "$output_tokens" "$model" "$cost_usd" "$stage" "$issue" 2>/dev/null || true
148
148
  fi
149
149
 
150
150
  # Always write to JSON (dual-write period)
@@ -154,7 +154,7 @@ cost_record() {
154
154
  fi
155
155
  local tmp_file
156
156
  tmp_file=$(mktemp "${COST_FILE}.tmp.XXXXXX")
157
- jq --argjson input "$input_tokens" \
157
+ if ! jq --argjson input "$input_tokens" \
158
158
  --argjson output "$output_tokens" \
159
159
  --arg model "$model" \
160
160
  --arg stage "$stage" \
@@ -172,7 +172,16 @@ cost_record() {
172
172
  ts: $ts,
173
173
  ts_epoch: $epoch
174
174
  }] | .entries = (.entries | .[-1000:])' \
175
- "$COST_FILE" > "$tmp_file" && mv "$tmp_file" "$COST_FILE" || rm -f "$tmp_file"
175
+ "$COST_FILE" > "$tmp_file" 2>/dev/null; then
176
+ error "Cost jq transformation failed — entry may be lost"
177
+ rm -f "$tmp_file"
178
+ # Continue without updating cost file
179
+ else
180
+ mv "$tmp_file" "$COST_FILE" || {
181
+ error "Failed to update cost file"
182
+ rm -f "$tmp_file"
183
+ }
184
+ fi
176
185
  ) 200>"${COST_FILE}.lock"
177
186
 
178
187
  emit_event "cost.record" \
@@ -264,6 +273,11 @@ cost_remaining_budget() {
264
273
  budget_usd=$(jq -r '.daily_budget_usd' "$BUDGET_FILE" 2>/dev/null || echo "0")
265
274
 
266
275
  if [[ "$budget_enabled" != "true" || "$budget_usd" == "0" ]]; then
276
+ if [[ -z "${_BUDGET_UNCONFIGURED_WARNED:-}" ]]; then
277
+ info "Budget not configured — unlimited. Use 'shipwright cost budget set <amount>'"
278
+ emit_event "cost.budget_unconfigured" "status=unlimited"
279
+ _BUDGET_UNCONFIGURED_WARNED=1
280
+ fi
267
281
  echo "unlimited"
268
282
  return 0
269
283
  fi
@@ -785,6 +799,54 @@ cost_dashboard() {
785
799
  fi
786
800
  fi
787
801
 
802
+ # Context efficiency (from loop.context_efficiency events)
803
+ local events_file="${HOME}/.shipwright/events.jsonl"
804
+ if [[ -f "$events_file" ]]; then
805
+ local ctx_events
806
+ ctx_events=$(grep '"type":"loop.context_efficiency"' "$events_file" 2>/dev/null | tail -200 || true)
807
+ local ctx_count
808
+ ctx_count=$(echo "$ctx_events" | grep -c '"loop.context_efficiency"' 2>/dev/null || echo "0")
809
+ ctx_count="${ctx_count:-0}"
810
+
811
+ if [[ "$ctx_count" -gt 0 ]]; then
812
+ # Parse metrics using awk (Bash 3.2 safe — no arrays needed)
813
+ local avg_utilization avg_trim_ratio total_raw total_trimmed trim_count
814
+ eval "$(echo "$ctx_events" | awk -F'"' '
815
+ /"loop.context_efficiency"/ {
816
+ for (i=1; i<=NF; i++) {
817
+ if ($i == "budget_utilization") util = $(i+2)
818
+ if ($i == "trim_ratio") ratio = $(i+2)
819
+ if ($i == "raw_prompt_chars") raw = $(i+2)
820
+ if ($i == "trimmed_prompt_chars") trimmed = $(i+2)
821
+ }
822
+ total_util += util; total_ratio += ratio
823
+ total_raw += raw; total_trimmed += trimmed
824
+ if (ratio + 0 > 0) trim_events++
825
+ n++
826
+ }
827
+ END {
828
+ if (n > 0) {
829
+ printf "avg_utilization=%.1f\n", total_util / n
830
+ printf "avg_trim_ratio=%.1f\n", total_ratio / n
831
+ } else {
832
+ printf "avg_utilization=0\navg_trim_ratio=0\n"
833
+ }
834
+ printf "total_raw=%d\ntotal_trimmed=%d\ntrim_count=%d\n", total_raw, total_trimmed, (trim_events+0)
835
+ }
836
+ ')"
837
+
838
+ local total_discarded=$(( total_raw - total_trimmed ))
839
+
840
+ echo -e "${BOLD} CONTEXT EFFICIENCY${RESET}"
841
+ echo -e " Avg budget used ${avg_utilization}%"
842
+ echo -e " Avg trim ratio ${avg_trim_ratio}%"
843
+ echo -e " Chars generated $(printf "%'d" "$total_raw")"
844
+ echo -e " Chars discarded $(printf "%'d" "$total_discarded")"
845
+ echo -e " Trim events ${trim_count} / ${ctx_count} iterations"
846
+ echo ""
847
+ fi
848
+ fi
849
+
788
850
  echo -e "${PURPLE}${BOLD}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}"
789
851
  echo ""
790
852
  }
@@ -894,6 +956,8 @@ show_help() {
894
956
  }
895
957
 
896
958
  # ─── Command Router ─────────────────────────────────────────────────────────
959
+ # Only run CLI when executed directly (not when sourced by sw-pipeline.sh)
960
+ if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then
897
961
 
898
962
  SUBCOMMAND="${1:-help}"
899
963
  shift 2>/dev/null || true
@@ -943,3 +1007,5 @@ case "$SUBCOMMAND" in
943
1007
  exit 1
944
1008
  ;;
945
1009
  esac
1010
+
1011
+ fi # end source guard