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
@@ -1,8 +1,23 @@
1
- # pipeline-stages.sh — Stage implementations (intake, plan, build, test, review, pr, merge, deploy, validate, monitor) for sw-pipeline.sh
1
+ # pipeline-stages.sh — Stage implementations (intake, plan, build, test, review, compound_quality, pr, merge, deploy, validate, monitor) for sw-pipeline.sh
2
2
  # Source from sw-pipeline.sh. Requires all pipeline globals and state/github/detection/quality modules.
3
3
  [[ -n "${_PIPELINE_STAGES_LOADED:-}" ]] && return 0
4
4
  _PIPELINE_STAGES_LOADED=1
5
5
 
6
+ # ─── Safe git helpers ────────────────────────────────────────────────────────
7
+ # BASE_BRANCH may not exist locally (e.g. --local mode with no remote).
8
+ # These helpers return empty output instead of crashing under set -euo pipefail.
9
+ _safe_base_log() {
10
+ local branch="${BASE_BRANCH:-main}"
11
+ git rev-parse --verify "$branch" >/dev/null 2>&1 || { echo ""; return 0; }
12
+ git log "$@" "${branch}..HEAD" 2>/dev/null || true
13
+ }
14
+
15
+ _safe_base_diff() {
16
+ local branch="${BASE_BRANCH:-main}"
17
+ git rev-parse --verify "$branch" >/dev/null 2>&1 || { git diff HEAD~5 "$@" 2>/dev/null || true; return 0; }
18
+ git diff "${branch}...HEAD" "$@" 2>/dev/null || true
19
+ }
20
+
6
21
  show_stage_preview() {
7
22
  local stage_id="$1"
8
23
  echo ""
@@ -12,8 +27,10 @@ show_stage_preview() {
12
27
  plan) echo -e " Generate plan via Claude, post task checklist to issue" ;;
13
28
  design) echo -e " Generate Architecture Decision Record (ADR), evaluate alternatives" ;;
14
29
  build) echo -e " Delegate to ${CYAN}shipwright loop${RESET} for autonomous building" ;;
30
+ test_first) echo -e " Generate tests from requirements (TDD mode) before implementation" ;;
15
31
  test) echo -e " Run test suite and check coverage" ;;
16
32
  review) echo -e " AI code review on the diff, post findings" ;;
33
+ compound_quality) echo -e " Adversarial review, negative tests, e2e, DoD audit" ;;
17
34
  pr) echo -e " Create GitHub PR with labels, reviewers, milestone" ;;
18
35
  merge) echo -e " Wait for CI checks, merge PR, optionally delete branch" ;;
19
36
  deploy) echo -e " Deploy to staging/production with rollback" ;;
@@ -125,7 +142,7 @@ stage_plan() {
125
142
  CURRENT_STAGE_ID="plan"
126
143
  local plan_file="$ARTIFACTS_DIR/plan.md"
127
144
 
128
- if ! command -v claude &>/dev/null; then
145
+ if ! command -v claude >/dev/null 2>&1; then
129
146
  error "Claude CLI not found — cannot generate plan"
130
147
  return 1
131
148
  fi
@@ -138,6 +155,12 @@ stage_plan() {
138
155
  "$context_script" gather --goal "$GOAL" --stage plan 2>/dev/null || true
139
156
  fi
140
157
 
158
+ # Gather rich architecture context (call-graph, dependencies)
159
+ local arch_context=""
160
+ if type gather_architecture_context &>/dev/null; then
161
+ arch_context=$(gather_architecture_context "${PROJECT_ROOT:-.}" 2>/dev/null || true)
162
+ fi
163
+
141
164
  # Build rich prompt with all available context
142
165
  local plan_prompt="You are an autonomous development agent. Analyze this codebase and create a detailed implementation plan.
143
166
 
@@ -153,6 +176,14 @@ ${ISSUE_BODY}
153
176
  "
154
177
  fi
155
178
 
179
+ # Inject architecture context (import graph, modules, test map)
180
+ if [[ -n "$arch_context" ]]; then
181
+ plan_prompt="${plan_prompt}
182
+ ## Architecture Context
183
+ ${arch_context}
184
+ "
185
+ fi
186
+
156
187
  # Inject context bundle from context engine (if available)
157
188
  local _context_bundle="${ARTIFACTS_DIR}/context-bundle.md"
158
189
  if [[ -f "$_context_bundle" ]]; then
@@ -167,7 +198,7 @@ ${_cb_content}
167
198
  fi
168
199
 
169
200
  # Inject intelligence memory context for similar past plans
170
- if type intelligence_search_memory &>/dev/null 2>&1; then
201
+ if type intelligence_search_memory >/dev/null 2>&1; then
171
202
  local plan_memory
172
203
  plan_memory=$(intelligence_search_memory "plan stage for ${TASK_TYPE:-feature}: ${GOAL:-}" "${HOME}/.shipwright/memory" 5 2>/dev/null) || true
173
204
  if [[ -n "$plan_memory" && "$plan_memory" != *'"results":[]'* && "$plan_memory" != *'"error"'* ]]; then
@@ -285,10 +316,22 @@ Checklist of completion criteria.
285
316
  fi
286
317
 
287
318
  local _token_log="${ARTIFACTS_DIR}/.claude-tokens-plan.log"
288
- claude --print --model "$plan_model" --max-turns 25 \
319
+ claude --print --model "$plan_model" --max-turns 25 --dangerously-skip-permissions \
289
320
  "$plan_prompt" < /dev/null > "$plan_file" 2>"$_token_log" || true
290
321
  parse_claude_tokens "$_token_log"
291
322
 
323
+ # Claude may write to disk via tools instead of stdout — rescue those files
324
+ local _plan_rescue
325
+ for _plan_rescue in "${PROJECT_ROOT}/PLAN.md" "${PROJECT_ROOT}/plan.md" \
326
+ "${PROJECT_ROOT}/implementation-plan.md"; do
327
+ if [[ -s "$_plan_rescue" ]] && [[ $(wc -l < "$plan_file" 2>/dev/null | xargs) -lt 10 ]]; then
328
+ info "Plan written to ${_plan_rescue} via tools — adopting as plan artifact"
329
+ cat "$_plan_rescue" >> "$plan_file"
330
+ rm -f "$_plan_rescue"
331
+ break
332
+ fi
333
+ done
334
+
292
335
  if [[ ! -s "$plan_file" ]]; then
293
336
  error "Plan generation failed — empty output"
294
337
  return 1
@@ -392,7 +435,7 @@ CC_TASKS_EOF
392
435
 
393
436
  # ── Plan Validation Gate ──
394
437
  # Ask Claude to validate the plan before proceeding
395
- if command -v claude &>/dev/null && [[ -s "$plan_file" ]]; then
438
+ if command -v claude >/dev/null 2>&1 && [[ -s "$plan_file" ]]; then
396
439
  local validation_attempts=0
397
440
  local max_validation_attempts=2
398
441
  local plan_valid=false
@@ -405,7 +448,7 @@ CC_TASKS_EOF
405
448
  local validation_extra=""
406
449
 
407
450
  # Inject rejected plan history from memory
408
- if type intelligence_search_memory &>/dev/null 2>&1; then
451
+ if type intelligence_search_memory >/dev/null 2>&1; then
409
452
  local rejected_plans
410
453
  rejected_plans=$(intelligence_search_memory "rejected plan validation failures for: ${GOAL:-}" "${HOME}/.shipwright/memory" 3 2>/dev/null) || true
411
454
  if [[ -n "$rejected_plans" ]]; then
@@ -560,16 +603,22 @@ stage_design() {
560
603
  return 0
561
604
  fi
562
605
 
563
- if ! command -v claude &>/dev/null; then
606
+ if ! command -v claude >/dev/null 2>&1; then
564
607
  error "Claude CLI not found — cannot generate design"
565
608
  return 1
566
609
  fi
567
610
 
568
611
  info "Generating Architecture Decision Record..."
569
612
 
613
+ # Gather rich architecture context (call-graph, dependencies)
614
+ local arch_struct_context=""
615
+ if type gather_architecture_context &>/dev/null; then
616
+ arch_struct_context=$(gather_architecture_context "${PROJECT_ROOT:-.}" 2>/dev/null || true)
617
+ fi
618
+
570
619
  # Memory integration — inject context if memory system available
571
620
  local memory_context=""
572
- if type intelligence_search_memory &>/dev/null 2>&1; then
621
+ if type intelligence_search_memory >/dev/null 2>&1; then
573
622
  local mem_dir="${HOME}/.shipwright/memory"
574
623
  memory_context=$(intelligence_search_memory "design stage architecture patterns for: ${GOAL:-}" "$mem_dir" 5 2>/dev/null) || true
575
624
  fi
@@ -608,7 +657,7 @@ ${arch_layers}}"
608
657
 
609
658
  # Inject rejected design approaches and anti-patterns from memory
610
659
  local design_antipatterns=""
611
- if type intelligence_search_memory &>/dev/null 2>&1; then
660
+ if type intelligence_search_memory >/dev/null 2>&1; then
612
661
  local rejected_designs
613
662
  rejected_designs=$(intelligence_search_memory "rejected design approaches anti-patterns for: ${GOAL:-}" "${HOME}/.shipwright/memory" 3 2>/dev/null) || true
614
663
  if [[ -n "$rejected_designs" ]]; then
@@ -636,7 +685,10 @@ $(cat "$plan_file")
636
685
  - Language: ${project_lang}
637
686
  - Test command: ${TEST_CMD:-not configured}
638
687
  - Task type: ${TASK_TYPE:-feature}
639
- ${memory_context:+
688
+ ${arch_struct_context:+
689
+ ## Architecture Context (import graph, modules, test map)
690
+ ${arch_struct_context}
691
+ }${memory_context:+
640
692
  ## Historical Context (from memory)
641
693
  ${memory_context}
642
694
  }${arch_context:+
@@ -684,10 +736,22 @@ Be concrete and specific. Reference actual file paths in the codebase. Consider
684
736
  fi
685
737
 
686
738
  local _token_log="${ARTIFACTS_DIR}/.claude-tokens-design.log"
687
- claude --print --model "$design_model" --max-turns 25 \
739
+ claude --print --model "$design_model" --max-turns 25 --dangerously-skip-permissions \
688
740
  "$design_prompt" < /dev/null > "$design_file" 2>"$_token_log" || true
689
741
  parse_claude_tokens "$_token_log"
690
742
 
743
+ # Claude may write to disk via tools instead of stdout — rescue those files
744
+ local _design_rescue
745
+ for _design_rescue in "${PROJECT_ROOT}/design-adr.md" "${PROJECT_ROOT}/design.md" \
746
+ "${PROJECT_ROOT}/ADR.md" "${PROJECT_ROOT}/DESIGN.md"; do
747
+ if [[ -s "$_design_rescue" ]] && [[ $(wc -l < "$design_file" 2>/dev/null | xargs) -lt 10 ]]; then
748
+ info "Design written to ${_design_rescue} via tools — adopting as design artifact"
749
+ cat "$_design_rescue" >> "$design_file"
750
+ rm -f "$_design_rescue"
751
+ break
752
+ fi
753
+ done
754
+
691
755
  if [[ ! -s "$design_file" ]]; then
692
756
  error "Design generation failed — empty output"
693
757
  return 1
@@ -715,7 +779,7 @@ Be concrete and specific. Reference actual file paths in the codebase. Consider
715
779
  files_to_modify=$(sed -n '/Files to modify/,/^-\|^#\|^$/p' "$design_file" 2>/dev/null | grep -E '^\s*-' | head -20 || true)
716
780
 
717
781
  if [[ -n "$files_to_create" || -n "$files_to_modify" ]]; then
718
- info "Design scope: ${DIM}$(echo "$files_to_create $files_to_modify" | grep -c '^\s*-' || echo 0) file(s)${RESET}"
782
+ info "Design scope: ${DIM}$(echo "$files_to_create $files_to_modify" | grep -c '^\s*-' || true) file(s)${RESET}"
719
783
  fi
720
784
 
721
785
  # Post design to GitHub issue
@@ -741,6 +805,117 @@ _Generated by \`shipwright pipeline\` design stage at $(now_iso)_"
741
805
  log_stage "design" "Generated design.md (${line_count} lines)"
742
806
  }
743
807
 
808
+ # ─── TDD: Generate tests before implementation ─────────────────────────────────
809
+ stage_test_first() {
810
+ CURRENT_STAGE_ID="test_first"
811
+ info "Generating tests from requirements (TDD mode)"
812
+
813
+ local plan_file="${ARTIFACTS_DIR}/plan.md"
814
+ local goal_file="${PROJECT_ROOT}/.claude/goal.md"
815
+ local requirements=""
816
+ if [[ -f "$plan_file" ]]; then
817
+ requirements=$(cat "$plan_file" 2>/dev/null || true)
818
+ elif [[ -f "$goal_file" ]]; then
819
+ requirements=$(cat "$goal_file" 2>/dev/null || true)
820
+ else
821
+ requirements="${GOAL:-}: ${ISSUE_BODY:-}"
822
+ fi
823
+
824
+ local tdd_prompt="You are writing tests BEFORE implementation (TDD).
825
+
826
+ Based on the following plan/requirements, generate test files that define the expected behavior. These tests should FAIL initially (since the implementation doesn't exist yet) but define the correct interface and behavior.
827
+
828
+ Requirements:
829
+ ${requirements}
830
+
831
+ Instructions:
832
+ 1. Create test files for each component mentioned in the plan
833
+ 2. Tests should verify the PUBLIC interface and expected behavior
834
+ 3. Include edge cases and error handling tests
835
+ 4. Tests should be runnable with the project's test framework
836
+ 5. Mark tests that need implementation with clear TODO comments
837
+ 6. Do NOT write implementation code — only tests
838
+
839
+ Output format: For each test file, use a fenced code block with the file path as the language identifier (e.g. \`\`\`tests/auth.test.ts):
840
+ \`\`\`path/to/test.test.ts
841
+ // file content
842
+ \`\`\`
843
+
844
+ Create files in the appropriate project directories (e.g. tests/, __tests__/, src/**/*.test.ts) per project convention."
845
+
846
+ local model="${CLAUDE_MODEL:-${MODEL:-sonnet}}"
847
+ [[ -z "$model" || "$model" == "null" ]] && model="sonnet"
848
+
849
+ local output=""
850
+ output=$(echo "$tdd_prompt" | timeout 120 claude --print --model "$model" 2>/dev/null) || {
851
+ warn "TDD test generation failed, falling back to standard build"
852
+ return 1
853
+ }
854
+
855
+ # Parse output: extract fenced code blocks and write to files
856
+ local wrote_any=false
857
+ local block_path="" in_block=false block_content=""
858
+ while IFS= read -r line; do
859
+ if [[ "$line" =~ ^\`\`\`([a-zA-Z0-9_/\.\-]+)$ ]]; then
860
+ if [[ -n "$block_path" && -n "$block_content" ]]; then
861
+ local out_file="${PROJECT_ROOT}/${block_path}"
862
+ local out_dir
863
+ out_dir=$(dirname "$out_file")
864
+ mkdir -p "$out_dir" 2>/dev/null || true
865
+ if echo "$block_content" > "$out_file" 2>/dev/null; then
866
+ wrote_any=true
867
+ info " Wrote: $block_path"
868
+ fi
869
+ fi
870
+ block_path="${BASH_REMATCH[1]}"
871
+ block_content=""
872
+ in_block=true
873
+ elif [[ "$line" == "\`\`\`" && "$in_block" == "true" ]]; then
874
+ if [[ -n "$block_path" && -n "$block_content" ]]; then
875
+ local out_file="${PROJECT_ROOT}/${block_path}"
876
+ local out_dir
877
+ out_dir=$(dirname "$out_file")
878
+ mkdir -p "$out_dir" 2>/dev/null || true
879
+ if echo "$block_content" > "$out_file" 2>/dev/null; then
880
+ wrote_any=true
881
+ info " Wrote: $block_path"
882
+ fi
883
+ fi
884
+ block_path=""
885
+ block_content=""
886
+ in_block=false
887
+ elif [[ "$in_block" == "true" && -n "$block_path" ]]; then
888
+ [[ -n "$block_content" ]] && block_content="${block_content}"$'\n'
889
+ block_content="${block_content}${line}"
890
+ fi
891
+ done <<< "$output"
892
+
893
+ # Flush last block if unclosed
894
+ if [[ -n "$block_path" && -n "$block_content" ]]; then
895
+ local out_file="${PROJECT_ROOT}/${block_path}"
896
+ local out_dir
897
+ out_dir=$(dirname "$out_file")
898
+ mkdir -p "$out_dir" 2>/dev/null || true
899
+ if echo "$block_content" > "$out_file" 2>/dev/null; then
900
+ wrote_any=true
901
+ info " Wrote: $block_path"
902
+ fi
903
+ fi
904
+
905
+ if [[ "$wrote_any" == "true" ]]; then
906
+ if (cd "$PROJECT_ROOT" && git diff --name-only 2>/dev/null | grep -qE 'test|spec'); then
907
+ git add -A 2>/dev/null || true
908
+ git commit -m "test: TDD - define expected behavior before implementation" 2>/dev/null || true
909
+ emit_event "tdd.tests_generated" "{\"stage\":\"test_first\"}"
910
+ fi
911
+ success "TDD tests generated"
912
+ else
913
+ warn "No test files extracted from TDD output — check format"
914
+ fi
915
+
916
+ return 0
917
+ }
918
+
744
919
  stage_build() {
745
920
  local plan_file="$ARTIFACTS_DIR/plan.md"
746
921
  local design_file="$ARTIFACTS_DIR/design.md"
@@ -749,7 +924,7 @@ stage_build() {
749
924
 
750
925
  # Memory integration — inject context if memory system available
751
926
  local memory_context=""
752
- if type intelligence_search_memory &>/dev/null 2>&1; then
927
+ if type intelligence_search_memory >/dev/null 2>&1; then
753
928
  local mem_dir="${HOME}/.shipwright/memory"
754
929
  memory_context=$(intelligence_search_memory "build stage for: ${GOAL:-}" "$mem_dir" 5 2>/dev/null) || true
755
930
  fi
@@ -761,6 +936,13 @@ stage_build() {
761
936
  local enriched_goal
762
937
  enriched_goal=$(_pipeline_compact_goal "$GOAL" "$plan_file" "$design_file")
763
938
 
939
+ # TDD: when test_first ran, tell build to make existing tests pass
940
+ if [[ "${TDD_ENABLED:-false}" == "true" || "${PIPELINE_TDD:-}" == "true" ]]; then
941
+ enriched_goal="${enriched_goal}
942
+
943
+ IMPORTANT (TDD mode): Test files already exist and define the expected behavior. Write implementation code to make ALL tests pass. Do not delete or modify the test files."
944
+ fi
945
+
764
946
  # Inject memory context
765
947
  if [[ -n "$memory_context" ]]; then
766
948
  enriched_goal="${enriched_goal}
@@ -790,7 +972,7 @@ $(cat "$TASKS_FILE")"
790
972
  fi
791
973
 
792
974
  # Inject file hotspots from GitHub intelligence
793
- if [[ "${NO_GITHUB:-}" != "true" ]] && type gh_file_change_frequency &>/dev/null 2>&1; then
975
+ if [[ "${NO_GITHUB:-}" != "true" ]] && type gh_file_change_frequency >/dev/null 2>&1; then
794
976
  local build_hotspots
795
977
  build_hotspots=$(gh_file_change_frequency 2>/dev/null | head -5 || true)
796
978
  if [[ -n "$build_hotspots" ]]; then
@@ -802,7 +984,7 @@ ${build_hotspots}"
802
984
  fi
803
985
 
804
986
  # Inject security alerts context
805
- if [[ "${NO_GITHUB:-}" != "true" ]] && type gh_security_alerts &>/dev/null 2>&1; then
987
+ if [[ "${NO_GITHUB:-}" != "true" ]] && type gh_security_alerts >/dev/null 2>&1; then
806
988
  local build_alerts
807
989
  build_alerts=$(gh_security_alerts 2>/dev/null | head -3 || true)
808
990
  if [[ -n "$build_alerts" ]]; then
@@ -930,8 +1112,14 @@ ${prevention_text}"
930
1112
  # Definition of Done: use plan-extracted DoD if available
931
1113
  [[ -s "$dod_file" ]] && loop_args+=(--definition-of-done "$dod_file")
932
1114
 
933
- # Skip permissions in CI (no interactive terminal)
934
- [[ "${CI_MODE:-false}" == "true" ]] && loop_args+=(--skip-permissions)
1115
+ # Checkpoint resume: when pipeline resumed from build-stage checkpoint, pass --resume to loop
1116
+ if [[ "${RESUME_FROM_CHECKPOINT:-false}" == "true" && "${checkpoint_stage:-}" == "build" ]]; then
1117
+ loop_args+=(--resume)
1118
+ fi
1119
+
1120
+ # Skip permissions — pipeline runs headlessly (claude -p) and has no terminal
1121
+ # for interactive permission prompts. Without this flag, agents can't write files.
1122
+ loop_args+=(--skip-permissions)
935
1123
 
936
1124
  info "Starting build loop: ${DIM}shipwright loop${RESET} (max ${max_iter} iterations, ${agents} agent(s))"
937
1125
 
@@ -967,7 +1155,7 @@ ${prevention_text}"
967
1155
 
968
1156
  # Read accumulated token counts from build loop (written by sw-loop.sh)
969
1157
  local _loop_token_file="${PROJECT_ROOT}/.claude/loop-logs/loop-tokens.json"
970
- if [[ -f "$_loop_token_file" ]] && command -v jq &>/dev/null; then
1158
+ if [[ -f "$_loop_token_file" ]] && command -v jq >/dev/null 2>&1; then
971
1159
  local _loop_in _loop_out _loop_cost
972
1160
  _loop_in=$(jq -r '.input_tokens // 0' "$_loop_token_file" 2>/dev/null || echo "0")
973
1161
  _loop_out=$(jq -r '.output_tokens // 0' "$_loop_token_file" 2>/dev/null || echo "0")
@@ -984,13 +1172,13 @@ ${prevention_text}"
984
1172
 
985
1173
  # Count commits made during build
986
1174
  local commit_count
987
- commit_count=$(git log --oneline "${BASE_BRANCH}..HEAD" 2>/dev/null | wc -l | xargs)
1175
+ commit_count=$(_safe_base_log --oneline | wc -l | xargs)
988
1176
  info "Build produced ${BOLD}$commit_count${RESET} commit(s)"
989
1177
 
990
1178
  # Commit quality evaluation when intelligence is enabled
991
- if type intelligence_search_memory &>/dev/null 2>&1 && command -v claude &>/dev/null && [[ "${commit_count:-0}" -gt 0 ]]; then
1179
+ if type intelligence_search_memory >/dev/null 2>&1 && command -v claude >/dev/null 2>&1 && [[ "${commit_count:-0}" -gt 0 ]]; then
992
1180
  local commit_msgs
993
- commit_msgs=$(git log --format="%s" "${BASE_BRANCH}..HEAD" 2>/dev/null | head -20)
1181
+ commit_msgs=$(_safe_base_log --format="%s" | head -20)
994
1182
  local quality_score
995
1183
  quality_score=$(claude --print --output-format text -p "Rate the quality of these git commit messages on a scale of 0-100. Consider: focus (one thing per commit), clarity (describes the why), atomicity (small logical units). Reply with ONLY a number 0-100.
996
1184
 
@@ -1086,6 +1274,14 @@ ${log_excerpt}
1086
1274
  fi
1087
1275
  fi
1088
1276
 
1277
+ # Emit test.completed with coverage for adaptive learning
1278
+ if [[ -n "$coverage" ]]; then
1279
+ emit_event "test.completed" \
1280
+ "issue=${ISSUE_NUMBER:-0}" \
1281
+ "stage=test" \
1282
+ "coverage=$coverage"
1283
+ fi
1284
+
1089
1285
  # Post test results to GitHub
1090
1286
  if [[ -n "$ISSUE_NUMBER" ]]; then
1091
1287
  local test_summary
@@ -1121,27 +1317,26 @@ stage_review() {
1121
1317
  local diff_file="$ARTIFACTS_DIR/review-diff.patch"
1122
1318
  local review_file="$ARTIFACTS_DIR/review.md"
1123
1319
 
1124
- git diff "${BASE_BRANCH}...${GIT_BRANCH}" > "$diff_file" 2>/dev/null || \
1125
- git diff HEAD~5 > "$diff_file" 2>/dev/null || true
1320
+ _safe_base_diff > "$diff_file" 2>/dev/null || true
1126
1321
 
1127
1322
  if [[ ! -s "$diff_file" ]]; then
1128
1323
  warn "No diff found — skipping review"
1129
1324
  return 0
1130
1325
  fi
1131
1326
 
1132
- if ! command -v claude &>/dev/null; then
1327
+ if ! command -v claude >/dev/null 2>&1; then
1133
1328
  warn "Claude CLI not found — skipping AI review"
1134
1329
  return 0
1135
1330
  fi
1136
1331
 
1137
1332
  local diff_stats
1138
- diff_stats=$(git diff --stat "${BASE_BRANCH}...${GIT_BRANCH}" 2>/dev/null | tail -1 || echo "")
1333
+ diff_stats=$(_safe_base_diff --stat | tail -1 || echo "")
1139
1334
  info "Running AI code review... ${DIM}($diff_stats)${RESET}"
1140
1335
 
1141
1336
  # Semantic risk scoring when intelligence is enabled
1142
- if type intelligence_search_memory &>/dev/null 2>&1 && command -v claude &>/dev/null; then
1337
+ if type intelligence_search_memory >/dev/null 2>&1 && command -v claude >/dev/null 2>&1; then
1143
1338
  local diff_files
1144
- diff_files=$(git diff --name-only "${BASE_BRANCH}...${GIT_BRANCH}" 2>/dev/null || true)
1339
+ diff_files=$(_safe_base_diff --name-only || true)
1145
1340
  local risk_score="low"
1146
1341
  # Fast heuristic: flag high-risk file patterns
1147
1342
  if echo "$diff_files" | grep -qiE 'migration|schema|auth|crypto|security|password|token|secret|\.env'; then
@@ -1185,7 +1380,7 @@ If no issues are found, write: \"Review clean — no issues found.\"
1185
1380
  "
1186
1381
 
1187
1382
  # Inject previous review findings and anti-patterns from memory
1188
- if type intelligence_search_memory &>/dev/null 2>&1; then
1383
+ if type intelligence_search_memory >/dev/null 2>&1; then
1189
1384
  local review_memory
1190
1385
  review_memory=$(intelligence_search_memory "code review findings anti-patterns for: ${GOAL:-}" "${HOME}/.shipwright/memory" 5 2>/dev/null) || true
1191
1386
  if [[ -n "$review_memory" ]]; then
@@ -1211,7 +1406,7 @@ ${conventions}
1211
1406
  fi
1212
1407
 
1213
1408
  # Inject CODEOWNERS focus areas for review
1214
- if [[ "${NO_GITHUB:-}" != "true" ]] && type gh_codeowners &>/dev/null 2>&1; then
1409
+ if [[ "${NO_GITHUB:-}" != "true" ]] && type gh_codeowners >/dev/null 2>&1; then
1215
1410
  local review_owners
1216
1411
  review_owners=$(gh_codeowners 2>/dev/null | head -10 || true)
1217
1412
  if [[ -n "$review_owners" ]]; then
@@ -1235,11 +1430,9 @@ $(cat "$dod_file")
1235
1430
  ## Diff to Review
1236
1431
  $(cat "$diff_file")"
1237
1432
 
1238
- # Build claude args add --dangerously-skip-permissions in CI
1239
- local review_args=(--print --model "$review_model" --max-turns 25)
1240
- if [[ "${CI_MODE:-false}" == "true" ]]; then
1241
- review_args+=(--dangerously-skip-permissions)
1242
- fi
1433
+ # Skip permissionspipeline runs headlessly (claude -p) and has no terminal
1434
+ # for interactive permission prompts. Same rationale as build stage (line ~1083).
1435
+ local review_args=(--print --model "$review_model" --max-turns 25 --dangerously-skip-permissions)
1243
1436
 
1244
1437
  claude "${review_args[@]}" "$review_prompt" < /dev/null > "$review_file" 2>"${ARTIFACTS_DIR}/.claude-tokens-review.log" || true
1245
1438
  parse_claude_tokens "${ARTIFACTS_DIR}/.claude-tokens-review.log"
@@ -1384,15 +1577,143 @@ ${review_summary}
1384
1577
  log_stage "review" "AI review complete ($total_issues issues: $critical_count critical, $bug_count bugs, $warning_count suggestions)"
1385
1578
  }
1386
1579
 
1580
+ # ─── Compound Quality (fallback) ────────────────────────────────────────────
1581
+ # Basic implementation: adversarial review, negative testing, e2e checks, DoD audit.
1582
+ # If pipeline-intelligence.sh was sourced first, its enhanced version takes priority.
1583
+ if ! type stage_compound_quality >/dev/null 2>&1; then
1584
+ stage_compound_quality() {
1585
+ CURRENT_STAGE_ID="compound_quality"
1586
+
1587
+ # Read stage config from pipeline template
1588
+ local cfg
1589
+ cfg=$(jq -r '.stages[] | select(.id == "compound_quality") | .config // {}' "$PIPELINE_CONFIG" 2>/dev/null) || cfg="{}"
1590
+
1591
+ local do_adversarial do_negative do_e2e do_dod max_cycles blocking
1592
+ do_adversarial=$(echo "$cfg" | jq -r '.adversarial // false')
1593
+ do_negative=$(echo "$cfg" | jq -r '.negative // false')
1594
+ do_e2e=$(echo "$cfg" | jq -r '.e2e // false')
1595
+ do_dod=$(echo "$cfg" | jq -r '.dod_audit // false')
1596
+ max_cycles=$(echo "$cfg" | jq -r '.max_cycles // 1')
1597
+ blocking=$(echo "$cfg" | jq -r '.compound_quality_blocking // false')
1598
+
1599
+ local pass_count=0 fail_count=0 total=0
1600
+ local compound_log="$ARTIFACTS_DIR/compound-quality.log"
1601
+ : > "$compound_log"
1602
+
1603
+ # ── Adversarial review ──
1604
+ if [[ "$do_adversarial" == "true" ]]; then
1605
+ total=$((total + 1))
1606
+ info "Running adversarial review..."
1607
+ if [[ -x "$SCRIPT_DIR/sw-adversarial.sh" ]]; then
1608
+ if bash "$SCRIPT_DIR/sw-adversarial.sh" --repo "${REPO_DIR:-.}" >> "$compound_log" 2>&1; then
1609
+ pass_count=$((pass_count + 1))
1610
+ success "Adversarial review passed"
1611
+ else
1612
+ fail_count=$((fail_count + 1))
1613
+ warn "Adversarial review found issues"
1614
+ fi
1615
+ else
1616
+ warn "sw-adversarial.sh not found, skipping"
1617
+ fi
1618
+ fi
1619
+
1620
+ # ── Negative / edge-case testing ──
1621
+ if [[ "$do_negative" == "true" ]]; then
1622
+ total=$((total + 1))
1623
+ info "Running negative test pass..."
1624
+ if [[ -n "${TEST_CMD:-}" ]]; then
1625
+ if eval "$TEST_CMD" >> "$compound_log" 2>&1; then
1626
+ pass_count=$((pass_count + 1))
1627
+ success "Negative test pass passed"
1628
+ else
1629
+ fail_count=$((fail_count + 1))
1630
+ warn "Negative test pass found failures"
1631
+ fi
1632
+ else
1633
+ pass_count=$((pass_count + 1))
1634
+ info "No test command configured, skipping negative tests"
1635
+ fi
1636
+ fi
1637
+
1638
+ # ── E2E checks ──
1639
+ if [[ "$do_e2e" == "true" ]]; then
1640
+ total=$((total + 1))
1641
+ info "Running e2e checks..."
1642
+ if [[ -x "$SCRIPT_DIR/sw-e2e-orchestrator.sh" ]]; then
1643
+ if bash "$SCRIPT_DIR/sw-e2e-orchestrator.sh" run >> "$compound_log" 2>&1; then
1644
+ pass_count=$((pass_count + 1))
1645
+ success "E2E checks passed"
1646
+ else
1647
+ fail_count=$((fail_count + 1))
1648
+ warn "E2E checks found issues"
1649
+ fi
1650
+ else
1651
+ pass_count=$((pass_count + 1))
1652
+ info "sw-e2e-orchestrator.sh not found, skipping e2e"
1653
+ fi
1654
+ fi
1655
+
1656
+ # ── Definition of Done audit ──
1657
+ if [[ "$do_dod" == "true" ]]; then
1658
+ total=$((total + 1))
1659
+ info "Running definition-of-done audit..."
1660
+ if [[ -x "$SCRIPT_DIR/sw-quality.sh" ]]; then
1661
+ if bash "$SCRIPT_DIR/sw-quality.sh" validate >> "$compound_log" 2>&1; then
1662
+ pass_count=$((pass_count + 1))
1663
+ success "DoD audit passed"
1664
+ else
1665
+ fail_count=$((fail_count + 1))
1666
+ warn "DoD audit found gaps"
1667
+ fi
1668
+ else
1669
+ pass_count=$((pass_count + 1))
1670
+ info "sw-quality.sh not found, skipping DoD audit"
1671
+ fi
1672
+ fi
1673
+
1674
+ # ── Summary ──
1675
+ log_stage "compound_quality" "Compound quality: $pass_count/$total checks passed, $fail_count failed"
1676
+
1677
+ if [[ "$fail_count" -gt 0 && "$blocking" == "true" ]]; then
1678
+ error "Compound quality gate failed: $fail_count of $total checks failed"
1679
+ return 1
1680
+ fi
1681
+
1682
+ return 0
1683
+ }
1684
+ fi # end fallback stage_compound_quality
1685
+
1387
1686
  stage_pr() {
1388
1687
  CURRENT_STAGE_ID="pr"
1389
1688
  local plan_file="$ARTIFACTS_DIR/plan.md"
1390
1689
  local test_log="$ARTIFACTS_DIR/test-results.log"
1391
1690
  local review_file="$ARTIFACTS_DIR/review.md"
1392
1691
 
1692
+ # ── Skip PR in local/no-github mode ──
1693
+ if [[ "${NO_GITHUB:-false}" == "true" || "${SHIPWRIGHT_LOCAL:-}" == "1" || "${LOCAL_MODE:-false}" == "true" ]]; then
1694
+ info "Skipping PR stage — running in local/no-github mode"
1695
+ # Save a PR draft locally for reference
1696
+ local branch_name
1697
+ branch_name=$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "unknown")
1698
+ local commit_count
1699
+ commit_count=$(_safe_base_log --oneline | wc -l | xargs)
1700
+ {
1701
+ echo "# PR Draft (local mode)"
1702
+ echo ""
1703
+ echo "**Branch:** ${branch_name}"
1704
+ echo "**Commits:** ${commit_count:-0}"
1705
+ echo "**Goal:** ${GOAL:-N/A}"
1706
+ echo ""
1707
+ echo "## Changes"
1708
+ _safe_base_diff --stat || true
1709
+ } > ".claude/pr-draft.md" 2>/dev/null || true
1710
+ emit_event "pr.skipped" "issue=${ISSUE_NUMBER:-0}" "reason=local_mode"
1711
+ return 0
1712
+ fi
1713
+
1393
1714
  # ── PR Hygiene Checks (informational) ──
1394
1715
  local hygiene_commit_count
1395
- hygiene_commit_count=$(git log --oneline "${BASE_BRANCH}..HEAD" 2>/dev/null | wc -l | xargs)
1716
+ hygiene_commit_count=$(_safe_base_log --oneline | wc -l | xargs)
1396
1717
  hygiene_commit_count="${hygiene_commit_count:-0}"
1397
1718
 
1398
1719
  if [[ "$hygiene_commit_count" -gt 20 ]]; then
@@ -1401,7 +1722,7 @@ stage_pr() {
1401
1722
 
1402
1723
  # Check for WIP/fixup/squash commits (expanded patterns)
1403
1724
  local wip_commits
1404
- wip_commits=$(git log --oneline "${BASE_BRANCH}..HEAD" 2>/dev/null | grep -ciE '^[0-9a-f]+ (WIP|fixup!|squash!|TODO|HACK|TEMP|BROKEN|wip[:-]|temp[:-]|broken[:-]|do not merge)' || true)
1725
+ wip_commits=$(_safe_base_log --oneline | grep -ciE '^[0-9a-f]+ (WIP|fixup!|squash!|TODO|HACK|TEMP|BROKEN|wip[:-]|temp[:-]|broken[:-]|do not merge)' || true)
1405
1726
  wip_commits="${wip_commits:-0}"
1406
1727
  if [[ "$wip_commits" -gt 0 ]]; then
1407
1728
  warn "Branch has ${wip_commits} WIP/fixup/squash/temp commit(s) — consider cleaning up"
@@ -1409,7 +1730,7 @@ stage_pr() {
1409
1730
 
1410
1731
  # ── PR Quality Gate: reject PRs with no real code changes ──
1411
1732
  local real_files
1412
- real_files=$(git diff --name-only "${BASE_BRANCH}...HEAD" 2>/dev/null | grep -v '^\.claude/' | grep -v '^\.github/' || true)
1733
+ real_files=$(_safe_base_diff --name-only | grep -v '^\.claude/' | grep -v '^\.github/' || true)
1413
1734
  if [[ -z "$real_files" ]]; then
1414
1735
  error "No real code changes detected — only pipeline artifacts (.claude/ logs)."
1415
1736
  error "The build agent did not produce meaningful changes. Skipping PR creation."
@@ -1448,7 +1769,7 @@ stage_pr() {
1448
1769
 
1449
1770
  # ── Developer Simulation (pre-PR review) ──
1450
1771
  local simulation_summary=""
1451
- if type simulation_review &>/dev/null 2>&1; then
1772
+ if type simulation_review >/dev/null 2>&1; then
1452
1773
  local sim_enabled
1453
1774
  sim_enabled=$(jq -r '.intelligence.simulation_enabled // false' "$PIPELINE_CONFIG" 2>/dev/null || echo "false")
1454
1775
  # Also check daemon-config
@@ -1459,7 +1780,7 @@ stage_pr() {
1459
1780
  if [[ "$sim_enabled" == "true" ]]; then
1460
1781
  info "Running developer simulation review..."
1461
1782
  local diff_for_sim
1462
- diff_for_sim=$(git diff "${BASE_BRANCH}...HEAD" 2>/dev/null || true)
1783
+ diff_for_sim=$(_safe_base_diff || true)
1463
1784
  if [[ -n "$diff_for_sim" ]]; then
1464
1785
  local sim_result
1465
1786
  sim_result=$(simulation_review "$diff_for_sim" "${GOAL:-}" 2>/dev/null || echo "")
@@ -1479,7 +1800,7 @@ stage_pr() {
1479
1800
 
1480
1801
  # ── Architecture Validation (pre-PR check) ──
1481
1802
  local arch_summary=""
1482
- if type architecture_validate_changes &>/dev/null 2>&1; then
1803
+ if type architecture_validate_changes >/dev/null 2>&1; then
1483
1804
  local arch_enabled
1484
1805
  arch_enabled=$(jq -r '.intelligence.architecture_enabled // false' "$PIPELINE_CONFIG" 2>/dev/null || echo "false")
1485
1806
  local daemon_cfg=".claude/daemon-config.json"
@@ -1489,7 +1810,7 @@ stage_pr() {
1489
1810
  if [[ "$arch_enabled" == "true" ]]; then
1490
1811
  info "Validating architecture..."
1491
1812
  local diff_for_arch
1492
- diff_for_arch=$(git diff "${BASE_BRANCH}...HEAD" 2>/dev/null || true)
1813
+ diff_for_arch=$(_safe_base_diff || true)
1493
1814
  if [[ -n "$diff_for_arch" ]]; then
1494
1815
  local arch_result
1495
1816
  arch_result=$(architecture_validate_changes "$diff_for_arch" "" 2>/dev/null || echo "")
@@ -1513,10 +1834,10 @@ stage_pr() {
1513
1834
 
1514
1835
  # Pre-PR diff gate — verify meaningful code changes exist (not just bookkeeping)
1515
1836
  local real_changes
1516
- real_changes=$(git diff --name-only "origin/${BASE_BRANCH:-main}...HEAD" \
1837
+ real_changes=$(_safe_base_diff --name-only \
1517
1838
  -- . ':!.claude/loop-state.md' ':!.claude/pipeline-state.md' \
1518
1839
  ':!.claude/pipeline-artifacts/*' ':!**/progress.md' \
1519
- ':!**/error-summary.json' 2>/dev/null | wc -l | xargs || echo "0")
1840
+ ':!**/error-summary.json' | wc -l | xargs || echo "0")
1520
1841
  if [[ "${real_changes:-0}" -eq 0 ]]; then
1521
1842
  error "No meaningful code changes detected — only bookkeeping files modified"
1522
1843
  error "Refusing to create PR with zero real changes"
@@ -1571,10 +1892,10 @@ stage_pr() {
1571
1892
  [[ -n "${GITHUB_ISSUE:-}" ]] && closes_line="Closes ${GITHUB_ISSUE}"
1572
1893
 
1573
1894
  local diff_stats
1574
- diff_stats=$(git diff --stat "${BASE_BRANCH}...${GIT_BRANCH}" 2>/dev/null | tail -1 || echo "")
1895
+ diff_stats=$(_safe_base_diff --stat | tail -1 || echo "")
1575
1896
 
1576
1897
  local commit_count
1577
- commit_count=$(git log --oneline "${BASE_BRANCH}..HEAD" 2>/dev/null | wc -l | xargs)
1898
+ commit_count=$(_safe_base_log --oneline | wc -l | xargs)
1578
1899
 
1579
1900
  local total_dur=""
1580
1901
  if [[ -n "$PIPELINE_START_EPOCH" ]]; then
@@ -1614,6 +1935,65 @@ Generated by \`shipwright pipeline\`
1614
1935
  EOF
1615
1936
  )"
1616
1937
 
1938
+ # Verify required evidence before PR (merge policy enforcement)
1939
+ local risk_tier
1940
+ risk_tier="low"
1941
+ if [[ -f "$REPO_DIR/config/policy.json" ]]; then
1942
+ local changed_files
1943
+ changed_files=$(_safe_base_diff --name-only || true)
1944
+ if [[ -n "$changed_files" ]]; then
1945
+ local policy_file="$REPO_DIR/config/policy.json"
1946
+ check_tier_match() {
1947
+ local tier="$1"
1948
+ local patterns
1949
+ patterns=$(jq -r ".riskTierRules.${tier}[]? // empty" "$policy_file" 2>/dev/null)
1950
+ [[ -z "$patterns" ]] && return 1
1951
+ while IFS= read -r pattern; do
1952
+ [[ -z "$pattern" ]] && continue
1953
+ local regex
1954
+ regex=$(echo "$pattern" | sed 's/\./\\./g; s/\*\*/DOUBLESTAR/g; s/\*/[^\/]*/g; s/DOUBLESTAR/.*/g')
1955
+ while IFS= read -r file; do
1956
+ [[ -z "$file" ]] && continue
1957
+ if echo "$file" | grep -qE "^${regex}$"; then
1958
+ return 0
1959
+ fi
1960
+ done <<< "$changed_files"
1961
+ done <<< "$patterns"
1962
+ return 1
1963
+ }
1964
+ check_tier_match "critical" && risk_tier="critical"
1965
+ check_tier_match "high" && [[ "$risk_tier" != "critical" ]] && risk_tier="high"
1966
+ check_tier_match "medium" && [[ "$risk_tier" != "critical" && "$risk_tier" != "high" ]] && risk_tier="medium"
1967
+ fi
1968
+ fi
1969
+
1970
+ local required_evidence
1971
+ required_evidence=$(jq -r ".mergePolicy.\"$risk_tier\".requiredEvidence // [] | .[]" "$REPO_DIR/config/policy.json" 2>/dev/null)
1972
+
1973
+ if [[ -n "$required_evidence" ]]; then
1974
+ local evidence_dir="$REPO_DIR/.claude/evidence"
1975
+ local missing_evidence=()
1976
+ while IFS= read -r etype; do
1977
+ [[ -z "$etype" ]] && continue
1978
+ local has_evidence=false
1979
+ for f in "$evidence_dir"/*"$etype"*; do
1980
+ [[ -f "$f" ]] && has_evidence=true && break
1981
+ done
1982
+ [[ "$has_evidence" != "true" ]] && missing_evidence+=("$etype")
1983
+ done <<< "$required_evidence"
1984
+
1985
+ if [[ ${#missing_evidence[@]} -gt 0 ]]; then
1986
+ warn "Missing required evidence for $risk_tier tier: ${missing_evidence[*]}"
1987
+ emit_event "evidence.missing" "{\"tier\":\"$risk_tier\",\"missing\":\"${missing_evidence[*]}\"}"
1988
+ # Collect missing evidence
1989
+ if [[ -x "$SCRIPT_DIR/sw-evidence.sh" ]]; then
1990
+ for etype in "${missing_evidence[@]}"; do
1991
+ (cd "$REPO_DIR" && bash "$SCRIPT_DIR/sw-evidence.sh" capture "$etype" 2>/dev/null) || warn "Failed to collect $etype evidence"
1992
+ done
1993
+ fi
1994
+ fi
1995
+ fi
1996
+
1617
1997
  # Build gh pr create args
1618
1998
  local pr_args=(--title "$pr_title" --body "$pr_body" --base "$BASE_BRANCH")
1619
1999
 
@@ -1687,12 +2067,12 @@ EOF
1687
2067
  local reviewer_assigned=false
1688
2068
 
1689
2069
  # Try CODEOWNERS-based routing via GraphQL API
1690
- if type gh_codeowners &>/dev/null 2>&1 && [[ -n "$REPO_OWNER" && -n "$REPO_NAME" ]]; then
2070
+ if type gh_codeowners >/dev/null 2>&1 && [[ -n "$REPO_OWNER" && -n "$REPO_NAME" ]]; then
1691
2071
  local codeowners_json
1692
2072
  codeowners_json=$(gh_codeowners "$REPO_OWNER" "$REPO_NAME" 2>/dev/null || echo "[]")
1693
2073
  if [[ "$codeowners_json" != "[]" && -n "$codeowners_json" ]]; then
1694
2074
  local changed_files
1695
- changed_files=$(git diff --name-only "${BASE_BRANCH}...HEAD" 2>/dev/null || true)
2075
+ changed_files=$(_safe_base_diff --name-only || true)
1696
2076
  if [[ -n "$changed_files" ]]; then
1697
2077
  local co_reviewers
1698
2078
  co_reviewers=$(echo "$codeowners_json" | jq -r '.[].owners[]' 2>/dev/null | sort -u | head -3 || true)
@@ -1710,7 +2090,7 @@ EOF
1710
2090
  fi
1711
2091
 
1712
2092
  # Fallback: contributor-based routing via GraphQL API
1713
- if [[ "$reviewer_assigned" != "true" ]] && type gh_contributors &>/dev/null 2>&1 && [[ -n "$REPO_OWNER" && -n "$REPO_NAME" ]]; then
2093
+ if [[ "$reviewer_assigned" != "true" ]] && type gh_contributors >/dev/null 2>&1 && [[ -n "$REPO_OWNER" && -n "$REPO_NAME" ]]; then
1714
2094
  local contributors_json
1715
2095
  contributors_json=$(gh_contributors "$REPO_OWNER" "$REPO_NAME" 2>/dev/null || echo "[]")
1716
2096
  local top_contributor
@@ -1766,13 +2146,14 @@ stage_merge() {
1766
2146
  local merge_diff_file="${ARTIFACTS_DIR}/review-diff.patch"
1767
2147
  local merge_review_file="${ARTIFACTS_DIR}/review.md"
1768
2148
  if [[ ! -s "$merge_diff_file" ]]; then
1769
- git diff "${BASE_BRANCH}...${GIT_BRANCH}" > "$merge_diff_file" 2>/dev/null || \
1770
- git diff HEAD~5 > "$merge_diff_file" 2>/dev/null || true
2149
+ _safe_base_diff > "$merge_diff_file" 2>/dev/null || true
1771
2150
  fi
1772
2151
  if [[ -s "$merge_diff_file" ]]; then
1773
2152
  local _merge_critical _merge_sec _merge_blocking _merge_reject
1774
- _merge_critical=$(grep -ciE '\*\*\[?Critical\]?\*\*' "$merge_review_file" 2>/dev/null || echo "0")
1775
- _merge_sec=$(grep -ciE '\*\*\[?Security\]?\*\*' "$merge_review_file" 2>/dev/null || echo "0")
2153
+ _merge_critical=$(grep -ciE '\*\*\[?Critical\]?\*\*' "$merge_review_file" 2>/dev/null || true)
2154
+ _merge_critical="${_merge_critical:-0}"
2155
+ _merge_sec=$(grep -ciE '\*\*\[?Security\]?\*\*' "$merge_review_file" 2>/dev/null || true)
2156
+ _merge_sec="${_merge_sec:-0}"
1776
2157
  _merge_blocking=$((${_merge_critical:-0} + ${_merge_sec:-0}))
1777
2158
  [[ "$_merge_blocking" -gt 0 ]] && _merge_reject="Review found ${_merge_blocking} critical/security issue(s)"
1778
2159
  if ! bash "$SCRIPT_DIR/sw-oversight.sh" gate --diff "$merge_diff_file" --description "${GOAL:-Pipeline merge}" --reject-if "${_merge_reject:-}" >/dev/null 2>&1; then
@@ -1816,7 +2197,7 @@ stage_merge() {
1816
2197
  fi
1817
2198
 
1818
2199
  # ── Branch Protection Check ──
1819
- if type gh_branch_protection &>/dev/null 2>&1 && [[ -n "$REPO_OWNER" && -n "$REPO_NAME" ]]; then
2200
+ if type gh_branch_protection >/dev/null 2>&1 && [[ -n "$REPO_OWNER" && -n "$REPO_NAME" ]]; then
1820
2201
  local protection_json
1821
2202
  protection_json=$(gh_branch_protection "$REPO_OWNER" "$REPO_NAME" "${BASE_BRANCH:-main}" 2>/dev/null || echo '{"protected": false}')
1822
2203
  local is_protected
@@ -1912,13 +2293,13 @@ stage_merge() {
1912
2293
  check_status=$(gh pr checks "$pr_number" --json 'bucket,name' --jq '[.[] | .bucket] | unique | sort' 2>/dev/null || echo '["pending"]')
1913
2294
 
1914
2295
  # If all checks passed (only "pass" in buckets)
1915
- if echo "$check_status" | jq -e '. == ["pass"]' &>/dev/null; then
2296
+ if echo "$check_status" | jq -e '. == ["pass"]' >/dev/null 2>&1; then
1916
2297
  success "All CI checks passed"
1917
2298
  break
1918
2299
  fi
1919
2300
 
1920
2301
  # If any check failed
1921
- if echo "$check_status" | jq -e 'any(. == "fail")' &>/dev/null; then
2302
+ if echo "$check_status" | jq -e 'any(. == "fail")' >/dev/null 2>&1; then
1922
2303
  error "CI checks failed — aborting merge"
1923
2304
  return 1
1924
2305
  fi
@@ -2018,7 +2399,7 @@ stage_deploy() {
2018
2399
  # Create GitHub deployment tracking
2019
2400
  local gh_deploy_env="production"
2020
2401
  [[ -n "$staging_cmd" && -z "$prod_cmd" ]] && gh_deploy_env="staging"
2021
- if [[ "${NO_GITHUB:-false}" != "true" ]] && type gh_deploy_pipeline_start &>/dev/null 2>&1; then
2402
+ if [[ "${NO_GITHUB:-false}" != "true" ]] && type gh_deploy_pipeline_start >/dev/null 2>&1; then
2022
2403
  if [[ -n "$REPO_OWNER" && -n "$REPO_NAME" ]]; then
2023
2404
  gh_deploy_pipeline_start "$REPO_OWNER" "$REPO_NAME" "${GIT_BRANCH:-HEAD}" "$gh_deploy_env" 2>/dev/null || true
2024
2405
  info "GitHub Deployment: tracking as $gh_deploy_env"
@@ -2151,7 +2532,7 @@ stage_deploy() {
2151
2532
  error "Staging deploy failed"
2152
2533
  [[ -n "$ISSUE_NUMBER" ]] && gh_comment_issue "$ISSUE_NUMBER" "Staging deploy failed"
2153
2534
  # Mark GitHub deployment as failed
2154
- if [[ "${NO_GITHUB:-false}" != "true" ]] && type gh_deploy_pipeline_complete &>/dev/null 2>&1; then
2535
+ if [[ "${NO_GITHUB:-false}" != "true" ]] && type gh_deploy_pipeline_complete >/dev/null 2>&1; then
2155
2536
  gh_deploy_pipeline_complete "$REPO_OWNER" "$REPO_NAME" "$gh_deploy_env" false "Staging deploy failed" 2>/dev/null || true
2156
2537
  fi
2157
2538
  return 1
@@ -2169,7 +2550,7 @@ stage_deploy() {
2169
2550
  fi
2170
2551
  [[ -n "$ISSUE_NUMBER" ]] && gh_comment_issue "$ISSUE_NUMBER" "Production deploy failed — rollback ${rollback_cmd:+attempted}"
2171
2552
  # Mark GitHub deployment as failed
2172
- if [[ "${NO_GITHUB:-false}" != "true" ]] && type gh_deploy_pipeline_complete &>/dev/null 2>&1; then
2553
+ if [[ "${NO_GITHUB:-false}" != "true" ]] && type gh_deploy_pipeline_complete >/dev/null 2>&1; then
2173
2554
  gh_deploy_pipeline_complete "$REPO_OWNER" "$REPO_NAME" "$gh_deploy_env" false "Production deploy failed" 2>/dev/null || true
2174
2555
  fi
2175
2556
  return 1
@@ -2184,7 +2565,7 @@ stage_deploy() {
2184
2565
  fi
2185
2566
 
2186
2567
  # Mark GitHub deployment as successful
2187
- if [[ "${NO_GITHUB:-false}" != "true" ]] && type gh_deploy_pipeline_complete &>/dev/null 2>&1; then
2568
+ if [[ "${NO_GITHUB:-false}" != "true" ]] && type gh_deploy_pipeline_complete >/dev/null 2>&1; then
2188
2569
  if [[ -n "$REPO_OWNER" && -n "$REPO_NAME" ]]; then
2189
2570
  gh_deploy_pipeline_complete "$REPO_OWNER" "$REPO_NAME" "$gh_deploy_env" true "" 2>/dev/null || true
2190
2571
  fi