shipwright-cli 2.4.0 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (169) hide show
  1. package/README.md +16 -11
  2. package/completions/_shipwright +248 -94
  3. package/completions/shipwright.bash +68 -19
  4. package/completions/shipwright.fish +310 -42
  5. package/config/decision-tiers.json +55 -0
  6. package/config/defaults.json +111 -0
  7. package/config/event-schema.json +218 -0
  8. package/config/policy.json +21 -18
  9. package/dashboard/coverage/coverage-summary.json +14 -0
  10. package/dashboard/public/index.html +1 -1
  11. package/dashboard/server.ts +306 -17
  12. package/dashboard/src/components/charts/bar.test.ts +79 -0
  13. package/dashboard/src/components/charts/donut.test.ts +68 -0
  14. package/dashboard/src/components/charts/pipeline-rail.test.ts +117 -0
  15. package/dashboard/src/components/charts/sparkline.test.ts +125 -0
  16. package/dashboard/src/core/api.test.ts +309 -0
  17. package/dashboard/src/core/helpers.test.ts +301 -0
  18. package/dashboard/src/core/router.test.ts +307 -0
  19. package/dashboard/src/core/router.ts +7 -0
  20. package/dashboard/src/core/sse.test.ts +144 -0
  21. package/dashboard/src/views/metrics.test.ts +186 -0
  22. package/dashboard/src/views/overview.test.ts +173 -0
  23. package/dashboard/src/views/pipelines.test.ts +183 -0
  24. package/dashboard/src/views/team.test.ts +253 -0
  25. package/dashboard/vitest.config.ts +14 -5
  26. package/docs/TIPS.md +1 -1
  27. package/docs/patterns/README.md +1 -1
  28. package/package.json +7 -9
  29. package/scripts/adapters/docker-deploy.sh +1 -1
  30. package/scripts/adapters/tmux-adapter.sh +11 -1
  31. package/scripts/adapters/wezterm-adapter.sh +1 -1
  32. package/scripts/check-version-consistency.sh +1 -1
  33. package/scripts/lib/architecture.sh +127 -0
  34. package/scripts/lib/bootstrap.sh +75 -0
  35. package/scripts/lib/compat.sh +89 -6
  36. package/scripts/lib/config.sh +91 -0
  37. package/scripts/lib/daemon-adaptive.sh +3 -3
  38. package/scripts/lib/daemon-dispatch.sh +63 -17
  39. package/scripts/lib/daemon-failure.sh +0 -0
  40. package/scripts/lib/daemon-health.sh +1 -1
  41. package/scripts/lib/daemon-patrol.sh +64 -17
  42. package/scripts/lib/daemon-poll.sh +54 -25
  43. package/scripts/lib/daemon-state.sh +125 -23
  44. package/scripts/lib/daemon-triage.sh +31 -9
  45. package/scripts/lib/decide-autonomy.sh +295 -0
  46. package/scripts/lib/decide-scoring.sh +228 -0
  47. package/scripts/lib/decide-signals.sh +462 -0
  48. package/scripts/lib/fleet-failover.sh +63 -0
  49. package/scripts/lib/helpers.sh +29 -6
  50. package/scripts/lib/pipeline-detection.sh +2 -2
  51. package/scripts/lib/pipeline-github.sh +9 -9
  52. package/scripts/lib/pipeline-intelligence.sh +105 -38
  53. package/scripts/lib/pipeline-quality-checks.sh +17 -16
  54. package/scripts/lib/pipeline-quality.sh +1 -1
  55. package/scripts/lib/pipeline-stages.sh +440 -59
  56. package/scripts/lib/pipeline-state.sh +54 -4
  57. package/scripts/lib/policy.sh +0 -0
  58. package/scripts/lib/test-helpers.sh +247 -0
  59. package/scripts/postinstall.mjs +78 -12
  60. package/scripts/signals/example-collector.sh +36 -0
  61. package/scripts/sw +17 -7
  62. package/scripts/sw-activity.sh +1 -11
  63. package/scripts/sw-adaptive.sh +109 -85
  64. package/scripts/sw-adversarial.sh +4 -14
  65. package/scripts/sw-architecture-enforcer.sh +1 -11
  66. package/scripts/sw-auth.sh +8 -17
  67. package/scripts/sw-autonomous.sh +111 -49
  68. package/scripts/sw-changelog.sh +1 -11
  69. package/scripts/sw-checkpoint.sh +144 -20
  70. package/scripts/sw-ci.sh +2 -12
  71. package/scripts/sw-cleanup.sh +13 -17
  72. package/scripts/sw-code-review.sh +16 -36
  73. package/scripts/sw-connect.sh +5 -12
  74. package/scripts/sw-context.sh +9 -26
  75. package/scripts/sw-cost.sh +17 -18
  76. package/scripts/sw-daemon.sh +76 -71
  77. package/scripts/sw-dashboard.sh +57 -17
  78. package/scripts/sw-db.sh +524 -26
  79. package/scripts/sw-decide.sh +685 -0
  80. package/scripts/sw-decompose.sh +1 -11
  81. package/scripts/sw-deps.sh +15 -25
  82. package/scripts/sw-developer-simulation.sh +1 -11
  83. package/scripts/sw-discovery.sh +138 -30
  84. package/scripts/sw-doc-fleet.sh +7 -17
  85. package/scripts/sw-docs-agent.sh +6 -16
  86. package/scripts/sw-docs.sh +4 -12
  87. package/scripts/sw-doctor.sh +134 -43
  88. package/scripts/sw-dora.sh +11 -19
  89. package/scripts/sw-durable.sh +35 -52
  90. package/scripts/sw-e2e-orchestrator.sh +11 -27
  91. package/scripts/sw-eventbus.sh +115 -115
  92. package/scripts/sw-evidence.sh +114 -30
  93. package/scripts/sw-feedback.sh +3 -13
  94. package/scripts/sw-fix.sh +2 -20
  95. package/scripts/sw-fleet-discover.sh +1 -11
  96. package/scripts/sw-fleet-viz.sh +10 -18
  97. package/scripts/sw-fleet.sh +13 -17
  98. package/scripts/sw-github-app.sh +6 -16
  99. package/scripts/sw-github-checks.sh +1 -11
  100. package/scripts/sw-github-deploy.sh +1 -11
  101. package/scripts/sw-github-graphql.sh +2 -12
  102. package/scripts/sw-guild.sh +1 -11
  103. package/scripts/sw-heartbeat.sh +49 -12
  104. package/scripts/sw-hygiene.sh +45 -43
  105. package/scripts/sw-incident.sh +48 -74
  106. package/scripts/sw-init.sh +35 -37
  107. package/scripts/sw-instrument.sh +1 -11
  108. package/scripts/sw-intelligence.sh +368 -53
  109. package/scripts/sw-jira.sh +5 -14
  110. package/scripts/sw-launchd.sh +2 -12
  111. package/scripts/sw-linear.sh +8 -17
  112. package/scripts/sw-logs.sh +4 -12
  113. package/scripts/sw-loop.sh +905 -104
  114. package/scripts/sw-memory.sh +263 -20
  115. package/scripts/sw-mission-control.sh +2 -12
  116. package/scripts/sw-model-router.sh +73 -34
  117. package/scripts/sw-otel.sh +15 -23
  118. package/scripts/sw-oversight.sh +1 -11
  119. package/scripts/sw-patrol-meta.sh +5 -11
  120. package/scripts/sw-pipeline-composer.sh +7 -17
  121. package/scripts/sw-pipeline-vitals.sh +1 -11
  122. package/scripts/sw-pipeline.sh +550 -122
  123. package/scripts/sw-pm.sh +2 -12
  124. package/scripts/sw-pr-lifecycle.sh +33 -28
  125. package/scripts/sw-predictive.sh +16 -22
  126. package/scripts/sw-prep.sh +6 -16
  127. package/scripts/sw-ps.sh +1 -11
  128. package/scripts/sw-public-dashboard.sh +2 -12
  129. package/scripts/sw-quality.sh +85 -14
  130. package/scripts/sw-reaper.sh +1 -11
  131. package/scripts/sw-recruit.sh +15 -25
  132. package/scripts/sw-regression.sh +11 -21
  133. package/scripts/sw-release-manager.sh +19 -28
  134. package/scripts/sw-release.sh +8 -16
  135. package/scripts/sw-remote.sh +1 -11
  136. package/scripts/sw-replay.sh +48 -44
  137. package/scripts/sw-retro.sh +70 -92
  138. package/scripts/sw-review-rerun.sh +1 -1
  139. package/scripts/sw-scale.sh +174 -41
  140. package/scripts/sw-security-audit.sh +12 -22
  141. package/scripts/sw-self-optimize.sh +239 -23
  142. package/scripts/sw-session.sh +5 -15
  143. package/scripts/sw-setup.sh +8 -18
  144. package/scripts/sw-standup.sh +5 -15
  145. package/scripts/sw-status.sh +32 -23
  146. package/scripts/sw-strategic.sh +129 -13
  147. package/scripts/sw-stream.sh +1 -11
  148. package/scripts/sw-swarm.sh +76 -36
  149. package/scripts/sw-team-stages.sh +10 -20
  150. package/scripts/sw-templates.sh +4 -14
  151. package/scripts/sw-testgen.sh +3 -13
  152. package/scripts/sw-tmux-pipeline.sh +1 -19
  153. package/scripts/sw-tmux-role-color.sh +0 -10
  154. package/scripts/sw-tmux-status.sh +3 -11
  155. package/scripts/sw-tmux.sh +2 -20
  156. package/scripts/sw-trace.sh +1 -19
  157. package/scripts/sw-tracker-github.sh +0 -10
  158. package/scripts/sw-tracker-jira.sh +1 -11
  159. package/scripts/sw-tracker-linear.sh +1 -11
  160. package/scripts/sw-tracker.sh +7 -24
  161. package/scripts/sw-triage.sh +29 -39
  162. package/scripts/sw-upgrade.sh +5 -23
  163. package/scripts/sw-ux.sh +1 -19
  164. package/scripts/sw-webhook.sh +18 -32
  165. package/scripts/sw-widgets.sh +3 -21
  166. package/scripts/sw-worktree.sh +11 -27
  167. package/scripts/update-homebrew-sha.sh +73 -0
  168. package/templates/pipelines/tdd.json +72 -0
  169. package/scripts/sw-pipeline.sh.mock +0 -7
@@ -9,14 +9,14 @@ gh_init() {
9
9
  return
10
10
  fi
11
11
 
12
- if ! command -v gh &>/dev/null; then
12
+ if ! command -v gh >/dev/null 2>&1; then
13
13
  GH_AVAILABLE=false
14
14
  warn "gh CLI not found — GitHub integration disabled"
15
15
  return
16
16
  fi
17
17
 
18
18
  # Check if authenticated
19
- if ! gh auth status &>/dev/null 2>&1; then
19
+ if ! gh auth status >/dev/null 2>&1; then
20
20
  GH_AVAILABLE=false
21
21
  warn "gh not authenticated — GitHub integration disabled"
22
22
  return
@@ -46,7 +46,7 @@ gh_init() {
46
46
  gh_comment_issue() {
47
47
  [[ "$GH_AVAILABLE" != "true" ]] && return 0
48
48
  local issue_num="$1" body="$2"
49
- gh issue comment "$issue_num" --body "$body" 2>/dev/null || true
49
+ _timeout 30 gh issue comment "$issue_num" --body "$body" 2>/dev/null || true
50
50
  }
51
51
 
52
52
  # Post a progress-tracking comment and save its ID for later updates
@@ -56,7 +56,7 @@ gh_post_progress() {
56
56
  local issue_num="$1" body="$2"
57
57
  local result
58
58
  result=$(gh api "repos/${REPO_OWNER}/${REPO_NAME}/issues/${issue_num}/comments" \
59
- -f body="$body" --jq '.id' 2>/dev/null) || true
59
+ -f body="$body" --jq '.id' --timeout 30 2>/dev/null) || true
60
60
  if [[ -n "$result" && "$result" != "null" ]]; then
61
61
  PROGRESS_COMMENT_ID="$result"
62
62
  fi
@@ -68,7 +68,7 @@ gh_update_progress() {
68
68
  [[ "$GH_AVAILABLE" != "true" || -z "$PROGRESS_COMMENT_ID" ]] && return 0
69
69
  local body="$1"
70
70
  gh api "repos/${REPO_OWNER}/${REPO_NAME}/issues/comments/${PROGRESS_COMMENT_ID}" \
71
- -X PATCH -f body="$body" 2>/dev/null || true
71
+ -X PATCH -f body="$body" --timeout 30 2>/dev/null || true
72
72
  }
73
73
 
74
74
  # Add labels to an issue or PR
@@ -77,7 +77,7 @@ gh_add_labels() {
77
77
  [[ "$GH_AVAILABLE" != "true" ]] && return 0
78
78
  local issue_num="$1" labels="$2"
79
79
  [[ -z "$labels" ]] && return 0
80
- gh issue edit "$issue_num" --add-label "$labels" 2>/dev/null || true
80
+ _timeout 30 gh issue edit "$issue_num" --add-label "$labels" 2>/dev/null || true
81
81
  }
82
82
 
83
83
  # Remove a label from an issue
@@ -85,7 +85,7 @@ gh_add_labels() {
85
85
  gh_remove_label() {
86
86
  [[ "$GH_AVAILABLE" != "true" ]] && return 0
87
87
  local issue_num="$1" label="$2"
88
- gh issue edit "$issue_num" --remove-label "$label" 2>/dev/null || true
88
+ _timeout 30 gh issue edit "$issue_num" --remove-label "$label" 2>/dev/null || true
89
89
  }
90
90
 
91
91
  # Self-assign an issue
@@ -93,7 +93,7 @@ gh_remove_label() {
93
93
  gh_assign_self() {
94
94
  [[ "$GH_AVAILABLE" != "true" ]] && return 0
95
95
  local issue_num="$1"
96
- gh issue edit "$issue_num" --add-assignee "@me" 2>/dev/null || true
96
+ _timeout 30 gh issue edit "$issue_num" --add-assignee "@me" 2>/dev/null || true
97
97
  }
98
98
 
99
99
  # Get full issue metadata as JSON
@@ -101,7 +101,7 @@ gh_assign_self() {
101
101
  gh_get_issue_meta() {
102
102
  [[ "$GH_AVAILABLE" != "true" ]] && return 0
103
103
  local issue_num="$1"
104
- gh issue view "$issue_num" --json title,body,labels,milestone,assignees,comments,number,state 2>/dev/null || true
104
+ _timeout 30 gh issue view "$issue_num" --json title,body,labels,milestone,assignees,comments,number,state 2>/dev/null || true
105
105
  }
106
106
 
107
107
  # Build a progress table for GitHub comment
@@ -106,8 +106,45 @@ pipeline_should_skip_stage() {
106
106
  # Returns JSON with classified findings and routing recommendations.
107
107
  # ──────────────────────────────────────────────────────────────────────────────
108
108
  classify_quality_findings() {
109
- local findings_dir="$ARTIFACTS_DIR"
110
- local result_file="$ARTIFACTS_DIR/classified-findings.json"
109
+ local findings_dir="${1:-$ARTIFACTS_DIR}"
110
+ local result_file="$findings_dir/classified-findings.json"
111
+
112
+ # Build combined content for semantic classification
113
+ local content=""
114
+ if [[ -f "$findings_dir/adversarial-review.md" ]]; then
115
+ content="${content}
116
+ --- adversarial-review.md ---
117
+ $(head -500 "$findings_dir/adversarial-review.md" 2>/dev/null)"
118
+ fi
119
+ if [[ -f "$findings_dir/negative-review.md" ]]; then
120
+ content="${content}
121
+ --- negative-review.md ---
122
+ $(head -300 "$findings_dir/negative-review.md" 2>/dev/null)"
123
+ fi
124
+ if [[ -f "$findings_dir/security-audit.log" ]]; then
125
+ content="${content}
126
+ --- security-audit.log ---
127
+ $(cat "$findings_dir/security-audit.log" 2>/dev/null)"
128
+ fi
129
+ if [[ -f "$findings_dir/compound-architecture-validation.json" ]]; then
130
+ content="${content}
131
+ --- compound-architecture-validation.json ---
132
+ $(jq -r '.[] | "\(.severity): \(.message // .description // .)"' "$findings_dir/compound-architecture-validation.json" 2>/dev/null | head -50)"
133
+ fi
134
+
135
+ # Try semantic classification first when Claude is available
136
+ local route=""
137
+ if command -v claude &>/dev/null && [[ "${INTELLIGENCE_ENABLED:-false}" != "false" ]] && [[ -n "$content" ]]; then
138
+ local prompt="Classify these code review findings into exactly ONE primary category. Return ONLY a single word: security, architecture, correctness, performance, testing, documentation, style.
139
+
140
+ Findings:
141
+ $content"
142
+ local category
143
+ category=$(echo "$prompt" | timeout 30 claude -p --model sonnet 2>/dev/null | tr -d '[:space:]' | tr '[:upper:]' '[:lower:]')
144
+ if [[ "$category" =~ ^(security|architecture|correctness|performance|testing|documentation|style)$ ]]; then
145
+ route="$category"
146
+ fi
147
+ fi
111
148
 
112
149
  # Initialize counters
113
150
  local arch_count=0 security_count=0 correctness_count=0 performance_count=0 testing_count=0 style_count=0
@@ -173,40 +210,53 @@ classify_quality_findings() {
173
210
  fi
174
211
 
175
212
  # ── Determine routing ──
176
- # Priority order: security > architecture > correctness > performance > testing > style
177
- local route="correctness" # default
213
+ # Use semantic classification when available; else fall back to grep-derived priority
178
214
  local needs_backtrack=false
179
215
  local priority_findings=""
180
216
 
181
- if [[ "$security_count" -gt 0 ]]; then
182
- route="security"
183
- priority_findings="security:${security_count}"
184
- fi
217
+ if [[ -z "$route" ]]; then
218
+ # Fallback: grep-based priority order: security > architecture > correctness > performance > testing > style
219
+ route="correctness"
185
220
 
186
- if [[ "$arch_count" -gt 0 ]]; then
187
- if [[ "$route" == "correctness" ]]; then
188
- route="architecture"
189
- needs_backtrack=true
221
+ if [[ "$security_count" -gt 0 ]]; then
222
+ route="security"
223
+ priority_findings="security:${security_count}"
190
224
  fi
191
- priority_findings="${priority_findings:+${priority_findings},}architecture:${arch_count}"
192
- fi
193
225
 
194
- if [[ "$correctness_count" -gt 0 ]]; then
195
- priority_findings="${priority_findings:+${priority_findings},}correctness:${correctness_count}"
196
- fi
226
+ if [[ "$arch_count" -gt 0 ]]; then
227
+ if [[ "$route" == "correctness" ]]; then
228
+ route="architecture"
229
+ needs_backtrack=true
230
+ fi
231
+ priority_findings="${priority_findings:+${priority_findings},}architecture:${arch_count}"
232
+ fi
197
233
 
198
- if [[ "$performance_count" -gt 0 ]]; then
199
- if [[ "$route" == "correctness" && "$correctness_count" -eq 0 ]]; then
200
- route="performance"
234
+ if [[ "$correctness_count" -gt 0 ]]; then
235
+ priority_findings="${priority_findings:+${priority_findings},}correctness:${correctness_count}"
236
+ fi
237
+
238
+ if [[ "$performance_count" -gt 0 ]]; then
239
+ if [[ "$route" == "correctness" && "$correctness_count" -eq 0 ]]; then
240
+ route="performance"
241
+ fi
242
+ priority_findings="${priority_findings:+${priority_findings},}performance:${performance_count}"
201
243
  fi
202
- priority_findings="${priority_findings:+${priority_findings},}performance:${performance_count}"
203
- fi
204
244
 
205
- if [[ "$testing_count" -gt 0 ]]; then
206
- if [[ "$route" == "correctness" && "$correctness_count" -eq 0 && "$performance_count" -eq 0 ]]; then
207
- route="testing"
245
+ if [[ "$testing_count" -gt 0 ]]; then
246
+ if [[ "$route" == "correctness" && "$correctness_count" -eq 0 && "$performance_count" -eq 0 ]]; then
247
+ route="testing"
248
+ fi
249
+ priority_findings="${priority_findings:+${priority_findings},}testing:${testing_count}"
208
250
  fi
209
- priority_findings="${priority_findings:+${priority_findings},}testing:${testing_count}"
251
+ else
252
+ # Semantic route: build priority_findings from counts, set needs_backtrack for architecture
253
+ [[ "$route" == "architecture" ]] && needs_backtrack=true
254
+ [[ "$arch_count" -gt 0 ]] && priority_findings="architecture:${arch_count}"
255
+ [[ "$security_count" -gt 0 ]] && priority_findings="${priority_findings:+${priority_findings},}security:${security_count}"
256
+ [[ "$correctness_count" -gt 0 ]] && priority_findings="${priority_findings:+${priority_findings},}correctness:${correctness_count}"
257
+ [[ "$performance_count" -gt 0 ]] && priority_findings="${priority_findings:+${priority_findings},}performance:${performance_count}"
258
+ [[ "$testing_count" -gt 0 ]] && priority_findings="${priority_findings:+${priority_findings},}testing:${testing_count}"
259
+ [[ -z "$priority_findings" ]] && priority_findings="${route}:1"
210
260
  fi
211
261
 
212
262
  # Style findings don't affect routing or count toward failure threshold
@@ -749,7 +799,7 @@ pipeline_record_quality_score() {
749
799
  rm -f "$tmp_score"
750
800
 
751
801
  # Rotate quality scores file to prevent unbounded growth
752
- type rotate_jsonl &>/dev/null 2>&1 && rotate_jsonl "$scores_file" 5000
802
+ type rotate_jsonl >/dev/null 2>&1 && rotate_jsonl "$scores_file" 5000
753
803
 
754
804
  emit_event "pipeline.quality_score_recorded" \
755
805
  "issue=${ISSUE_NUMBER:-0}" \
@@ -1098,7 +1148,16 @@ stage_compound_quality() {
1098
1148
  _cq_real_changes=$(git diff --name-only "origin/${BASE_BRANCH:-main}...HEAD" \
1099
1149
  -- . ':!.claude/loop-state.md' ':!.claude/pipeline-state.md' \
1100
1150
  ':!.claude/pipeline-artifacts/*' ':!**/progress.md' \
1101
- ':!**/error-summary.json' 2>/dev/null | wc -l | xargs || echo "0")
1151
+ ':!**/error-summary.json' 2>/dev/null | wc -l || echo "0")
1152
+ _cq_real_changes=$(echo "$_cq_real_changes" | tr -d '[:space:]')
1153
+ [[ -z "$_cq_real_changes" ]] && _cq_real_changes=0
1154
+ # Fallback: if no remote, compare against first commit
1155
+ if [[ "$_cq_real_changes" -eq 0 ]] 2>/dev/null; then
1156
+ _cq_real_changes=$(git diff --name-only "$(git rev-list --max-parents=0 HEAD 2>/dev/null)...HEAD" \
1157
+ -- . ':!.claude/*' ':!**/progress.md' ':!**/error-summary.json' 2>/dev/null | wc -l || echo "0")
1158
+ _cq_real_changes=$(echo "$_cq_real_changes" | tr -d '[:space:]')
1159
+ [[ -z "$_cq_real_changes" ]] && _cq_real_changes=0
1160
+ fi
1102
1161
  if [[ "${_cq_real_changes:-0}" -eq 0 ]]; then
1103
1162
  error "Compound quality: no meaningful code changes found — failing quality gate"
1104
1163
  return 1
@@ -1117,7 +1176,7 @@ stage_compound_quality() {
1117
1176
 
1118
1177
  # Intelligent audit selection
1119
1178
  local audit_plan='{"adversarial":"targeted","architecture":"targeted","simulation":"targeted","security":"targeted","dod":"targeted"}'
1120
- if type pipeline_select_audits &>/dev/null 2>&1; then
1179
+ if type pipeline_select_audits >/dev/null 2>&1; then
1121
1180
  local _selected
1122
1181
  _selected=$(pipeline_select_audits 2>/dev/null) || true
1123
1182
  if [[ -n "$_selected" && "$_selected" != "null" ]]; then
@@ -1157,8 +1216,11 @@ stage_compound_quality() {
1157
1216
 
1158
1217
  # 2. Test coverage check
1159
1218
  local coverage_pct=0
1160
- coverage_pct=$(run_test_coverage_check 2>/dev/null) || coverage_pct=0
1219
+ coverage_pct=$(run_test_coverage_check 2>/dev/null | tr -d '[:space:][:cntrl:]') || coverage_pct=0
1161
1220
  coverage_pct="${coverage_pct:-0}"
1221
+ # Sanitize: strip anything non-numeric (ANSI codes, whitespace, etc.)
1222
+ coverage_pct=$(echo "$coverage_pct" | sed 's/[^0-9]//g')
1223
+ [[ -z "$coverage_pct" ]] && coverage_pct=0
1162
1224
 
1163
1225
  if [[ "$coverage_pct" != "skip" ]]; then
1164
1226
  if [[ "$coverage_pct" -lt "${PIPELINE_COVERAGE_THRESHOLD:-60}" ]]; then
@@ -1204,16 +1266,21 @@ stage_compound_quality() {
1204
1266
  fi
1205
1267
 
1206
1268
  # Vitals-driven adaptive cycle limit (preferred)
1269
+ # Respect the template's max_cycles as a ceiling — vitals can only reduce, not inflate
1207
1270
  local base_max_cycles="$max_cycles"
1208
- if type pipeline_adaptive_limit &>/dev/null 2>&1; then
1271
+ local template_max_cycles="$max_cycles"
1272
+ if type pipeline_adaptive_limit >/dev/null 2>&1; then
1209
1273
  local _cq_vitals=""
1210
- if type pipeline_compute_vitals &>/dev/null 2>&1; then
1274
+ if type pipeline_compute_vitals >/dev/null 2>&1; then
1211
1275
  _cq_vitals=$(pipeline_compute_vitals "$STATE_FILE" "$ARTIFACTS_DIR" "${ISSUE_NUMBER:-}" 2>/dev/null) || true
1212
1276
  fi
1213
1277
  local vitals_cq_limit
1214
1278
  vitals_cq_limit=$(pipeline_adaptive_limit "compound_quality" "$_cq_vitals" 2>/dev/null) || true
1215
1279
  if [[ -n "$vitals_cq_limit" && "$vitals_cq_limit" =~ ^[0-9]+$ && "$vitals_cq_limit" -gt 0 ]]; then
1216
- max_cycles="$vitals_cq_limit"
1280
+ # Cap at template max — don't let vitals override the pipeline template's intent
1281
+ if [[ "$vitals_cq_limit" -le "$template_max_cycles" ]]; then
1282
+ max_cycles="$vitals_cq_limit"
1283
+ fi
1217
1284
  if [[ "$max_cycles" != "$base_max_cycles" ]]; then
1218
1285
  info "Vitals-driven cycles: ${base_max_cycles} → ${max_cycles} (compound_quality)"
1219
1286
  fi
@@ -1270,7 +1337,7 @@ stage_compound_quality() {
1270
1337
  fi
1271
1338
 
1272
1339
  # 3. Developer Simulation (intelligence module)
1273
- if type simulation_review &>/dev/null 2>&1; then
1340
+ if type simulation_review >/dev/null 2>&1; then
1274
1341
  local sim_enabled
1275
1342
  sim_enabled=$(jq -r '.intelligence.simulation_enabled // false' "$PIPELINE_CONFIG" 2>/dev/null || echo "false")
1276
1343
  local daemon_cfg="${PROJECT_ROOT}/.claude/daemon-config.json"
@@ -1310,7 +1377,7 @@ stage_compound_quality() {
1310
1377
  fi
1311
1378
 
1312
1379
  # 4. Architecture Enforcer (intelligence module)
1313
- if type architecture_validate_changes &>/dev/null 2>&1; then
1380
+ if type architecture_validate_changes >/dev/null 2>&1; then
1314
1381
  local arch_enabled
1315
1382
  arch_enabled=$(jq -r '.intelligence.architecture_enabled // false' "$PIPELINE_CONFIG" 2>/dev/null || echo "false")
1316
1383
  local daemon_cfg="${PROJECT_ROOT}/.claude/daemon-config.json"
@@ -1474,7 +1541,7 @@ All quality checks clean:
1474
1541
 
1475
1542
  # DoD verification on successful pass
1476
1543
  local _dod_pass_rate=100
1477
- if type pipeline_verify_dod &>/dev/null 2>&1; then
1544
+ if type pipeline_verify_dod >/dev/null 2>&1; then
1478
1545
  pipeline_verify_dod "$ARTIFACTS_DIR" 2>/dev/null || true
1479
1546
  if [[ -f "$ARTIFACTS_DIR/dod-verification.json" ]]; then
1480
1547
  _dod_pass_rate=$(jq -r '.pass_rate // 100' "$ARTIFACTS_DIR/dod-verification.json" 2>/dev/null || echo "100")
@@ -1496,7 +1563,7 @@ All quality checks clean:
1496
1563
 
1497
1564
  # DoD verification on successful pass
1498
1565
  local _dod_pass_rate=100
1499
- if type pipeline_verify_dod &>/dev/null 2>&1; then
1566
+ if type pipeline_verify_dod >/dev/null 2>&1; then
1500
1567
  pipeline_verify_dod "$ARTIFACTS_DIR" 2>/dev/null || true
1501
1568
  if [[ -f "$ARTIFACTS_DIR/dod-verification.json" ]]; then
1502
1569
  _dod_pass_rate=$(jq -r '.pass_rate // 100' "$ARTIFACTS_DIR/dod-verification.json" 2>/dev/null || echo "100")
@@ -1590,7 +1657,7 @@ All quality checks clean:
1590
1657
 
1591
1658
  # DoD verification
1592
1659
  local _dod_pass_rate=0
1593
- if type pipeline_verify_dod &>/dev/null 2>&1; then
1660
+ if type pipeline_verify_dod >/dev/null 2>&1; then
1594
1661
  pipeline_verify_dod "$ARTIFACTS_DIR" 2>/dev/null || true
1595
1662
  if [[ -f "$ARTIFACTS_DIR/dod-verification.json" ]]; then
1596
1663
  _dod_pass_rate=$(jq -r '.pass_rate // 0' "$ARTIFACTS_DIR/dod-verification.json" 2>/dev/null || echo "0")
@@ -10,15 +10,15 @@ quality_check_security() {
10
10
  local tool_found=false
11
11
 
12
12
  # Try npm audit
13
- if [[ -f "package.json" ]] && command -v npm &>/dev/null; then
13
+ if [[ -f "package.json" ]] && command -v npm >/dev/null 2>&1; then
14
14
  tool_found=true
15
15
  npm audit --production 2>&1 | tee "$audit_log" || audit_exit=$?
16
16
  # Try pip-audit
17
- elif [[ -f "requirements.txt" || -f "pyproject.toml" ]] && command -v pip-audit &>/dev/null; then
17
+ elif [[ -f "requirements.txt" || -f "pyproject.toml" ]] && command -v pip-audit >/dev/null 2>&1; then
18
18
  tool_found=true
19
19
  pip-audit 2>&1 | tee "$audit_log" || audit_exit=$?
20
20
  # Try cargo audit
21
- elif [[ -f "Cargo.toml" ]] && command -v cargo-audit &>/dev/null; then
21
+ elif [[ -f "Cargo.toml" ]] && command -v cargo-audit >/dev/null 2>&1; then
22
22
  tool_found=true
23
23
  cargo audit 2>&1 | tee "$audit_log" || audit_exit=$?
24
24
  fi
@@ -170,7 +170,7 @@ quality_check_bundle_size() {
170
170
  mv "$tmp_bundle_hist" "$bundle_history_file" 2>/dev/null || true
171
171
 
172
172
  # Intelligence: identify top dependency bloaters
173
- if type intelligence_search_memory &>/dev/null 2>&1 && [[ -f "package.json" ]] && command -v jq &>/dev/null; then
173
+ if type intelligence_search_memory >/dev/null 2>&1 && [[ -f "package.json" ]] && command -v jq >/dev/null 2>&1; then
174
174
  local dep_sizes=""
175
175
  local deps
176
176
  deps=$(jq -r '.dependencies // {} | keys[]' package.json 2>/dev/null || true)
@@ -237,7 +237,7 @@ quality_check_perf_regression() {
237
237
  if [[ -f "$daemon_cfg" ]]; then
238
238
  intel_enabled=$(jq -r '.intelligence.enabled // false' "$daemon_cfg" 2>/dev/null || echo "false")
239
239
  fi
240
- if [[ "$intel_enabled" == "true" ]] && command -v claude &>/dev/null; then
240
+ if [[ "$intel_enabled" == "true" ]] && command -v claude >/dev/null 2>&1; then
241
241
  local tail_output
242
242
  tail_output=$(tail -30 "$test_log" 2>/dev/null || true)
243
243
  if [[ -n "$tail_output" ]]; then
@@ -378,7 +378,7 @@ quality_check_api_compat() {
378
378
 
379
379
  # Check for breaking changes: removed endpoints, changed methods
380
380
  local removed_endpoints=""
381
- if command -v jq &>/dev/null && [[ "$spec_file" == *.json ]]; then
381
+ if command -v jq >/dev/null 2>&1 && [[ "$spec_file" == *.json ]]; then
382
382
  local old_paths new_paths
383
383
  old_paths=$(echo "$old_spec" | jq -r '.paths | keys[]' 2>/dev/null | sort || true)
384
384
  new_paths=$(jq -r '.paths | keys[]' "$spec_file" 2>/dev/null | sort || true)
@@ -387,7 +387,7 @@ quality_check_api_compat() {
387
387
 
388
388
  # Enhanced schema diff: parameter changes, response schema, auth changes
389
389
  local param_changes="" schema_changes=""
390
- if command -v jq &>/dev/null && [[ "$spec_file" == *.json ]]; then
390
+ if command -v jq >/dev/null 2>&1 && [[ "$spec_file" == *.json ]]; then
391
391
  # Detect parameter changes on existing endpoints
392
392
  local common_paths
393
393
  common_paths=$(comm -12 <(echo "$old_spec" | jq -r '.paths | keys[]' 2>/dev/null | sort) <(jq -r '.paths | keys[]' "$spec_file" 2>/dev/null | sort) 2>/dev/null || true)
@@ -407,7 +407,7 @@ quality_check_api_compat() {
407
407
 
408
408
  # Intelligence: semantic API diff for complex changes
409
409
  local semantic_diff=""
410
- if type intelligence_search_memory &>/dev/null 2>&1 && command -v claude &>/dev/null; then
410
+ if type intelligence_search_memory >/dev/null 2>&1 && command -v claude >/dev/null 2>&1; then
411
411
  local spec_git_diff
412
412
  spec_git_diff=$(git diff "${BASE_BRANCH}...HEAD" -- "$spec_file" 2>/dev/null | head -200 || true)
413
413
  if [[ -n "$spec_git_diff" ]]; then
@@ -441,7 +441,7 @@ ${spec_git_diff}" --model haiku < /dev/null 2>/dev/null || true)
441
441
  if [[ -n "$removed_endpoints" || -n "$param_changes" ]]; then
442
442
  local issue_count=0
443
443
  [[ -n "$removed_endpoints" ]] && issue_count=$((issue_count + $(echo "$removed_endpoints" | wc -l | xargs)))
444
- [[ -n "$param_changes" ]] && issue_count=$((issue_count + $(echo "$param_changes" | grep -c '.' || true)))
444
+ [[ -n "$param_changes" ]] && issue_count=$((issue_count + $(echo "$param_changes" | grep -c '.' 2>/dev/null || true)))
445
445
  warn "API breaking changes: ${issue_count} issue(s) found"
446
446
  return 1
447
447
  fi
@@ -470,7 +470,7 @@ quality_check_coverage() {
470
470
  if [[ -f "$daemon_cfg_cov" ]]; then
471
471
  intel_enabled_cov=$(jq -r '.intelligence.enabled // false' "$daemon_cfg_cov" 2>/dev/null || echo "false")
472
472
  fi
473
- if [[ "$intel_enabled_cov" == "true" ]] && command -v claude &>/dev/null; then
473
+ if [[ "$intel_enabled_cov" == "true" ]] && command -v claude >/dev/null 2>&1; then
474
474
  local tail_cov_output
475
475
  tail_cov_output=$(tail -40 "$test_log" 2>/dev/null || true)
476
476
  if [[ -n "$tail_cov_output" ]]; then
@@ -559,7 +559,7 @@ run_adversarial_review() {
559
559
  fi
560
560
 
561
561
  # Delegate to sw-adversarial.sh module when available (uses intelligence cache)
562
- if type adversarial_review &>/dev/null 2>&1; then
562
+ if type adversarial_review >/dev/null 2>&1; then
563
563
  info "Using intelligence-backed adversarial review..."
564
564
  local json_result
565
565
  json_result=$(adversarial_review "$diff_content" "${GOAL:-}" 2>/dev/null || echo "[]")
@@ -605,7 +605,7 @@ run_adversarial_review() {
605
605
 
606
606
  # Inject previous adversarial findings from memory
607
607
  local adv_memory=""
608
- if type intelligence_search_memory &>/dev/null 2>&1; then
608
+ if type intelligence_search_memory >/dev/null 2>&1; then
609
609
  adv_memory=$(intelligence_search_memory "adversarial review security findings for: ${GOAL:-}" "${HOME}/.shipwright/memory" 5 2>/dev/null) || true
610
610
  fi
611
611
 
@@ -676,7 +676,7 @@ $(head -200 "$file" 2>/dev/null || true)
676
676
 
677
677
  # Inject previous negative prompting findings from memory
678
678
  local neg_memory=""
679
- if type intelligence_search_memory &>/dev/null 2>&1; then
679
+ if type intelligence_search_memory >/dev/null 2>&1; then
680
680
  neg_memory=$(intelligence_search_memory "negative prompting findings common concerns for: ${GOAL:-}" "${HOME}/.shipwright/memory" 5 2>/dev/null) || true
681
681
  fi
682
682
 
@@ -853,9 +853,9 @@ run_bash_compat_check() {
853
853
  while IFS= read -r filepath; do
854
854
  [[ -z "$filepath" ]] && continue
855
855
 
856
- # declare -A (associative arrays)
856
+ # declare -A (associative arrays; declare -a is bash 3.2 compatible)
857
857
  local declare_a_count
858
- declare_a_count=$(grep -c 'declare[[:space:]]*-[aA]' "$filepath" 2>/dev/null || true)
858
+ declare_a_count=$(grep -c 'declare[[:space:]]*-A' "$filepath" 2>/dev/null || true)
859
859
  if [[ "$declare_a_count" -gt 0 ]]; then
860
860
  violations=$((violations + declare_a_count))
861
861
  violation_details="${violation_details}${filepath}: declare -A (${declare_a_count} occurrences)
@@ -990,7 +990,8 @@ run_atomic_write_check() {
990
990
 
991
991
  # Check for direct redirection writes (> file) in state/config paths
992
992
  local bad_writes
993
- bad_writes=$(git show "HEAD:$filepath" 2>/dev/null | grep -c 'echo.*>' "$filepath" 2>/dev/null || true)
993
+ bad_writes=$(git show "HEAD:$filepath" 2>/dev/null | grep -c 'echo.*>' 2>/dev/null || true)
994
+ bad_writes="${bad_writes:-0}"
994
995
 
995
996
  if [[ "$bad_writes" -gt 0 ]]; then
996
997
  violations=$((violations + bad_writes))
@@ -5,7 +5,7 @@ _PIPELINE_QUALITY_LOADED=1
5
5
 
6
6
  # Policy overrides when config/policy.json exists
7
7
  [[ -f "${SCRIPT_DIR:-}/lib/policy.sh" ]] && source "${SCRIPT_DIR:-}/lib/policy.sh"
8
- if type policy_get &>/dev/null 2>&1; then
8
+ if type policy_get >/dev/null 2>&1; then
9
9
  PIPELINE_COVERAGE_THRESHOLD=$(policy_get ".pipeline.coverage_threshold_percent" "60")
10
10
  PIPELINE_QUALITY_GATE_THRESHOLD=$(policy_get ".pipeline.quality_gate_score_threshold" "70")
11
11
  QUALITY_COVERAGE_THRESHOLD=$(policy_get ".quality.coverage_threshold" "70")