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
@@ -1,8 +1,125 @@
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
+ # ─── Context pruning helpers ────────────────────────────────────────────────
7
+
8
+ # prune_context_section — Intelligently truncate a context section to fit a char budget.
9
+ # $1: section name (for logging/markers)
10
+ # $2: content string
11
+ # $3: max_chars (default 5000)
12
+ # For JSON content (starts with { or [): extracts summary fields via jq.
13
+ # For text content: sandwich approach — keeps first + last N lines.
14
+ # Outputs the (possibly truncated) content to stdout.
15
+ prune_context_section() {
16
+ local section_name="${1:-section}"
17
+ local content="${2:-}"
18
+ local max_chars="${3:-5000}"
19
+
20
+ [[ -z "$content" ]] && return 0
21
+
22
+ local content_len=${#content}
23
+ if [[ "$content_len" -le "$max_chars" ]]; then
24
+ printf '%s' "$content"
25
+ return 0
26
+ fi
27
+
28
+ # JSON content — try jq summary extraction
29
+ local first_char="${content:0:1}"
30
+ if [[ "$first_char" == "{" || "$first_char" == "[" ]]; then
31
+ local summary=""
32
+ # Try extracting summary/results fields
33
+ summary=$(printf '%s' "$content" | jq -r '
34
+ if type == "object" then
35
+ to_entries | map(
36
+ if (.value | type) == "array" then
37
+ "\(.key): \(.value | length) items"
38
+ elif (.value | type) == "object" then
39
+ "\(.key): \(.value | keys | join(", "))"
40
+ else
41
+ "\(.key): \(.value)"
42
+ end
43
+ ) | join("\n")
44
+ elif type == "array" then
45
+ .[:5] | map(tostring) | join("\n")
46
+ else . end
47
+ ' 2>/dev/null) || true
48
+
49
+ if [[ -n "$summary" && ${#summary} -le "$max_chars" ]]; then
50
+ printf '%s' "$summary"
51
+ return 0
52
+ fi
53
+ # jq failed or still too large — fall through to text truncation
54
+ fi
55
+
56
+ # Text content — sandwich approach (first N + last N lines)
57
+ local line_count=0
58
+ line_count=$(printf '%s\n' "$content" | wc -l | xargs)
59
+
60
+ # Calculate how many lines to keep from each end
61
+ # Approximate chars-per-line to figure out line budget
62
+ local avg_chars_per_line=80
63
+ if [[ "$line_count" -gt 0 ]]; then
64
+ avg_chars_per_line=$(( content_len / line_count ))
65
+ [[ "$avg_chars_per_line" -lt 20 ]] && avg_chars_per_line=20
66
+ fi
67
+ local total_lines_budget=$(( max_chars / avg_chars_per_line ))
68
+ [[ "$total_lines_budget" -lt 4 ]] && total_lines_budget=4
69
+ local half=$(( total_lines_budget / 2 ))
70
+
71
+ local head_part=""
72
+ local tail_part=""
73
+ head_part=$(printf '%s\n' "$content" | head -"$half")
74
+ tail_part=$(printf '%s\n' "$content" | tail -"$half")
75
+
76
+ printf '%s\n[... %s truncated: %d→%d chars ...]\n%s' \
77
+ "$head_part" "$section_name" "$content_len" "$max_chars" "$tail_part"
78
+ }
79
+
80
+ # guard_prompt_size — Warn and hard-truncate if prompt exceeds budget.
81
+ # $1: stage name (for logging)
82
+ # $2: prompt content
83
+ # $3: max_chars (default 100000)
84
+ # Outputs the (possibly truncated) prompt to stdout.
85
+ PIPELINE_PROMPT_BUDGET="${PIPELINE_PROMPT_BUDGET:-100000}"
86
+
87
+ guard_prompt_size() {
88
+ local stage_name="${1:-stage}"
89
+ local prompt="${2:-}"
90
+ local max_chars="${3:-$PIPELINE_PROMPT_BUDGET}"
91
+
92
+ local prompt_len=${#prompt}
93
+ if [[ "$prompt_len" -le "$max_chars" ]]; then
94
+ printf '%s' "$prompt"
95
+ return 0
96
+ fi
97
+
98
+ warn "${stage_name} prompt too large (${prompt_len} chars, budget ${max_chars}) — truncating"
99
+ emit_event "pipeline.prompt_truncated" \
100
+ "stage=$stage_name" \
101
+ "original=$prompt_len" \
102
+ "budget=$max_chars" 2>/dev/null || true
103
+
104
+ printf '%s\n\n... [CONTEXT TRUNCATED: %s prompt exceeded %d char budget. Focus on the goal and requirements.]' \
105
+ "${prompt:0:$max_chars}" "$stage_name" "$max_chars"
106
+ }
107
+
108
+ # ─── Safe git helpers ────────────────────────────────────────────────────────
109
+ # BASE_BRANCH may not exist locally (e.g. --local mode with no remote).
110
+ # These helpers return empty output instead of crashing under set -euo pipefail.
111
+ _safe_base_log() {
112
+ local branch="${BASE_BRANCH:-main}"
113
+ git rev-parse --verify "$branch" >/dev/null 2>&1 || { echo ""; return 0; }
114
+ git log "$@" "${branch}..HEAD" 2>/dev/null || true
115
+ }
116
+
117
+ _safe_base_diff() {
118
+ local branch="${BASE_BRANCH:-main}"
119
+ git rev-parse --verify "$branch" >/dev/null 2>&1 || { git diff HEAD~5 "$@" 2>/dev/null || true; return 0; }
120
+ git diff "${branch}...HEAD" "$@" 2>/dev/null || true
121
+ }
122
+
6
123
  show_stage_preview() {
7
124
  local stage_id="$1"
8
125
  echo ""
@@ -15,6 +132,7 @@ show_stage_preview() {
15
132
  test_first) echo -e " Generate tests from requirements (TDD mode) before implementation" ;;
16
133
  test) echo -e " Run test suite and check coverage" ;;
17
134
  review) echo -e " AI code review on the diff, post findings" ;;
135
+ compound_quality) echo -e " Adversarial review, negative tests, e2e, DoD audit" ;;
18
136
  pr) echo -e " Create GitHub PR with labels, reviewers, milestone" ;;
19
137
  merge) echo -e " Wait for CI checks, merge PR, optionally delete branch" ;;
20
138
  deploy) echo -e " Deploy to staging/production with rollback" ;;
@@ -162,6 +280,7 @@ ${ISSUE_BODY}
162
280
 
163
281
  # Inject architecture context (import graph, modules, test map)
164
282
  if [[ -n "$arch_context" ]]; then
283
+ arch_context=$(prune_context_section "architecture" "$arch_context" 5000)
165
284
  plan_prompt="${plan_prompt}
166
285
  ## Architecture Context
167
286
  ${arch_context}
@@ -173,6 +292,7 @@ ${arch_context}
173
292
  if [[ -f "$_context_bundle" ]]; then
174
293
  local _cb_content
175
294
  _cb_content=$(cat "$_context_bundle" 2>/dev/null | head -100 || true)
295
+ _cb_content=$(prune_context_section "context-bundle" "$_cb_content" 8000)
176
296
  if [[ -n "$_cb_content" ]]; then
177
297
  plan_prompt="${plan_prompt}
178
298
  ## Pipeline Context
@@ -188,6 +308,7 @@ ${_cb_content}
188
308
  if [[ -n "$plan_memory" && "$plan_memory" != *'"results":[]'* && "$plan_memory" != *'"error"'* ]]; then
189
309
  local memory_summary
190
310
  memory_summary=$(echo "$plan_memory" | jq -r '.results[]? | "- \(.)"' 2>/dev/null | head -10 || true)
311
+ memory_summary=$(prune_context_section "memory" "$memory_summary" 10000)
191
312
  if [[ -n "$memory_summary" ]]; then
192
313
  plan_prompt="${plan_prompt}
193
314
  ## Historical Context (from previous pipelines)
@@ -212,6 +333,7 @@ ${plan_hint}
212
333
  if [[ -x "$SCRIPT_DIR/sw-discovery.sh" ]]; then
213
334
  local plan_discoveries
214
335
  plan_discoveries=$("$SCRIPT_DIR/sw-discovery.sh" inject "*.md,*.json" 2>/dev/null | head -20 || true)
336
+ plan_discoveries=$(prune_context_section "discoveries" "$plan_discoveries" 3000)
215
337
  if [[ -n "$plan_discoveries" ]]; then
216
338
  plan_prompt="${plan_prompt}
217
339
  ## Discoveries from Other Pipelines
@@ -232,6 +354,7 @@ ${plan_discoveries}
232
354
  "Patterns: \((.patterns // []) | join(", "))",
233
355
  "Rules: \((.rules // []) | join("; "))"
234
356
  ' "$arch_file_plan" 2>/dev/null || true)
357
+ arch_patterns=$(prune_context_section "intelligence" "$arch_patterns" 5000)
235
358
  if [[ -n "$arch_patterns" ]]; then
236
359
  plan_prompt="${plan_prompt}
237
360
  ## Architecture Patterns
@@ -268,6 +391,12 @@ Focus on: threat modeling, OWASP top 10, input validation, authentication/author
268
391
  - Test command: ${TEST_CMD:-not configured}
269
392
  - Task type: ${TASK_TYPE:-feature}
270
393
 
394
+ ## Context Efficiency
395
+ - Batch independent tool calls in parallel when possible
396
+ - Read specific file sections (offset/limit) instead of entire large files
397
+ - Use targeted grep searches — avoid scanning entire codebases into context
398
+ - Delegate multi-file analysis to subagents when available
399
+
271
400
  ## Required Output
272
401
  Create a Markdown plan with these sections:
273
402
 
@@ -290,6 +419,9 @@ How to verify the implementation works.
290
419
  Checklist of completion criteria.
291
420
  "
292
421
 
422
+ # Guard total prompt size
423
+ plan_prompt=$(guard_prompt_size "plan" "$plan_prompt")
424
+
293
425
  local plan_model
294
426
  plan_model=$(jq -r --arg id "plan" '(.stages[] | select(.id == $id) | .config.model) // .defaults.model // "opus"' "$PIPELINE_CONFIG" 2>/dev/null) || true
295
427
  [[ -n "$MODEL" ]] && plan_model="$MODEL"
@@ -300,10 +432,22 @@ Checklist of completion criteria.
300
432
  fi
301
433
 
302
434
  local _token_log="${ARTIFACTS_DIR}/.claude-tokens-plan.log"
303
- claude --print --model "$plan_model" --max-turns 25 \
435
+ claude --print --model "$plan_model" --max-turns 25 --dangerously-skip-permissions \
304
436
  "$plan_prompt" < /dev/null > "$plan_file" 2>"$_token_log" || true
305
437
  parse_claude_tokens "$_token_log"
306
438
 
439
+ # Claude may write to disk via tools instead of stdout — rescue those files
440
+ local _plan_rescue
441
+ for _plan_rescue in "${PROJECT_ROOT}/PLAN.md" "${PROJECT_ROOT}/plan.md" \
442
+ "${PROJECT_ROOT}/implementation-plan.md"; do
443
+ if [[ -s "$_plan_rescue" ]] && [[ $(wc -l < "$plan_file" 2>/dev/null | xargs) -lt 10 ]]; then
444
+ info "Plan written to ${_plan_rescue} via tools — adopting as plan artifact"
445
+ cat "$_plan_rescue" >> "$plan_file"
446
+ rm -f "$_plan_rescue"
447
+ break
448
+ fi
449
+ done
450
+
307
451
  if [[ ! -s "$plan_file" ]]; then
308
452
  error "Plan generation failed — empty output"
309
453
  return 1
@@ -587,6 +731,7 @@ stage_design() {
587
731
  if type gather_architecture_context &>/dev/null; then
588
732
  arch_struct_context=$(gather_architecture_context "${PROJECT_ROOT:-.}" 2>/dev/null || true)
589
733
  fi
734
+ arch_struct_context=$(prune_context_section "architecture" "$arch_struct_context" 5000)
590
735
 
591
736
  # Memory integration — inject context if memory system available
592
737
  local memory_context=""
@@ -597,12 +742,14 @@ stage_design() {
597
742
  if [[ -z "$memory_context" ]] && [[ -x "$SCRIPT_DIR/sw-memory.sh" ]]; then
598
743
  memory_context=$(bash "$SCRIPT_DIR/sw-memory.sh" inject "design" 2>/dev/null) || true
599
744
  fi
745
+ memory_context=$(prune_context_section "memory" "$memory_context" 10000)
600
746
 
601
747
  # Inject cross-pipeline discoveries for design stage
602
748
  local design_discoveries=""
603
749
  if [[ -x "$SCRIPT_DIR/sw-discovery.sh" ]]; then
604
750
  design_discoveries=$("$SCRIPT_DIR/sw-discovery.sh" inject "*.md,*.ts,*.tsx,*.js" 2>/dev/null | head -20 || true)
605
751
  fi
752
+ design_discoveries=$(prune_context_section "discoveries" "$design_discoveries" 3000)
606
753
 
607
754
  # Inject architecture model patterns if available
608
755
  local arch_context=""
@@ -626,6 +773,7 @@ ${arch_patterns}
626
773
  ${arch_layers}}"
627
774
  fi
628
775
  fi
776
+ arch_context=$(prune_context_section "intelligence" "$arch_context" 5000)
629
777
 
630
778
  # Inject rejected design approaches and anti-patterns from memory
631
779
  local design_antipatterns=""
@@ -633,6 +781,7 @@ ${arch_layers}}"
633
781
  local rejected_designs
634
782
  rejected_designs=$(intelligence_search_memory "rejected design approaches anti-patterns for: ${GOAL:-}" "${HOME}/.shipwright/memory" 3 2>/dev/null) || true
635
783
  if [[ -n "$rejected_designs" ]]; then
784
+ rejected_designs=$(prune_context_section "antipatterns" "$rejected_designs" 5000)
636
785
  design_antipatterns="
637
786
  ## Rejected Approaches (from past reviews)
638
787
  These design approaches were rejected in past reviews. Avoid repeating them:
@@ -698,6 +847,9 @@ Produce this EXACT format:
698
847
 
699
848
  Be concrete and specific. Reference actual file paths in the codebase. Consider edge cases and failure modes."
700
849
 
850
+ # Guard total prompt size
851
+ design_prompt=$(guard_prompt_size "design" "$design_prompt")
852
+
701
853
  local design_model
702
854
  design_model=$(jq -r --arg id "design" '(.stages[] | select(.id == $id) | .config.model) // .defaults.model // "opus"' "$PIPELINE_CONFIG" 2>/dev/null) || true
703
855
  [[ -n "$MODEL" ]] && design_model="$MODEL"
@@ -708,10 +860,22 @@ Be concrete and specific. Reference actual file paths in the codebase. Consider
708
860
  fi
709
861
 
710
862
  local _token_log="${ARTIFACTS_DIR}/.claude-tokens-design.log"
711
- claude --print --model "$design_model" --max-turns 25 \
863
+ claude --print --model "$design_model" --max-turns 25 --dangerously-skip-permissions \
712
864
  "$design_prompt" < /dev/null > "$design_file" 2>"$_token_log" || true
713
865
  parse_claude_tokens "$_token_log"
714
866
 
867
+ # Claude may write to disk via tools instead of stdout — rescue those files
868
+ local _design_rescue
869
+ for _design_rescue in "${PROJECT_ROOT}/design-adr.md" "${PROJECT_ROOT}/design.md" \
870
+ "${PROJECT_ROOT}/ADR.md" "${PROJECT_ROOT}/DESIGN.md"; do
871
+ if [[ -s "$_design_rescue" ]] && [[ $(wc -l < "$design_file" 2>/dev/null | xargs) -lt 10 ]]; then
872
+ info "Design written to ${_design_rescue} via tools — adopting as design artifact"
873
+ cat "$_design_rescue" >> "$design_file"
874
+ rm -f "$_design_rescue"
875
+ break
876
+ fi
877
+ done
878
+
715
879
  if [[ ! -s "$design_file" ]]; then
716
880
  error "Design generation failed — empty output"
717
881
  return 1
@@ -739,7 +903,7 @@ Be concrete and specific. Reference actual file paths in the codebase. Consider
739
903
  files_to_modify=$(sed -n '/Files to modify/,/^-\|^#\|^$/p' "$design_file" 2>/dev/null | grep -E '^\s*-' | head -20 || true)
740
904
 
741
905
  if [[ -n "$files_to_create" || -n "$files_to_modify" ]]; then
742
- info "Design scope: ${DIM}$(echo "$files_to_create $files_to_modify" | grep -c '^\s*-' || echo 0) file(s)${RESET}"
906
+ info "Design scope: ${DIM}$(echo "$files_to_create $files_to_modify" | grep -c '^\s*-' || true) file(s)${RESET}"
743
907
  fi
744
908
 
745
909
  # Post design to GitHub issue
@@ -1077,8 +1241,9 @@ ${prevention_text}"
1077
1241
  loop_args+=(--resume)
1078
1242
  fi
1079
1243
 
1080
- # Skip permissions in CI (no interactive terminal)
1081
- [[ "${CI_MODE:-false}" == "true" ]] && loop_args+=(--skip-permissions)
1244
+ # Skip permissions pipeline runs headlessly (claude -p) and has no terminal
1245
+ # for interactive permission prompts. Without this flag, agents can't write files.
1246
+ loop_args+=(--skip-permissions)
1082
1247
 
1083
1248
  info "Starting build loop: ${DIM}shipwright loop${RESET} (max ${max_iter} iterations, ${agents} agent(s))"
1084
1249
 
@@ -1131,13 +1296,13 @@ ${prevention_text}"
1131
1296
 
1132
1297
  # Count commits made during build
1133
1298
  local commit_count
1134
- commit_count=$(git log --oneline "${BASE_BRANCH}..HEAD" 2>/dev/null | wc -l | xargs)
1299
+ commit_count=$(_safe_base_log --oneline | wc -l | xargs)
1135
1300
  info "Build produced ${BOLD}$commit_count${RESET} commit(s)"
1136
1301
 
1137
1302
  # Commit quality evaluation when intelligence is enabled
1138
1303
  if type intelligence_search_memory >/dev/null 2>&1 && command -v claude >/dev/null 2>&1 && [[ "${commit_count:-0}" -gt 0 ]]; then
1139
1304
  local commit_msgs
1140
- commit_msgs=$(git log --format="%s" "${BASE_BRANCH}..HEAD" 2>/dev/null | head -20)
1305
+ commit_msgs=$(_safe_base_log --format="%s" | head -20)
1141
1306
  local quality_score
1142
1307
  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.
1143
1308
 
@@ -1201,7 +1366,8 @@ stage_test() {
1201
1366
  # Post failure to GitHub with more context
1202
1367
  if [[ -n "$ISSUE_NUMBER" ]]; then
1203
1368
  local log_lines
1204
- log_lines=$(wc -l < "$test_log" 2>/dev/null || echo "0")
1369
+ log_lines=$(wc -l < "$test_log" 2>/dev/null || true)
1370
+ log_lines="${log_lines:-0}"
1205
1371
  local log_excerpt
1206
1372
  if [[ "$log_lines" -lt 60 ]]; then
1207
1373
  log_excerpt="$(cat "$test_log" 2>/dev/null || true)"
@@ -1276,8 +1442,7 @@ stage_review() {
1276
1442
  local diff_file="$ARTIFACTS_DIR/review-diff.patch"
1277
1443
  local review_file="$ARTIFACTS_DIR/review.md"
1278
1444
 
1279
- git diff "${BASE_BRANCH}...${GIT_BRANCH}" > "$diff_file" 2>/dev/null || \
1280
- git diff HEAD~5 > "$diff_file" 2>/dev/null || true
1445
+ _safe_base_diff > "$diff_file" 2>/dev/null || true
1281
1446
 
1282
1447
  if [[ ! -s "$diff_file" ]]; then
1283
1448
  warn "No diff found — skipping review"
@@ -1290,13 +1455,13 @@ stage_review() {
1290
1455
  fi
1291
1456
 
1292
1457
  local diff_stats
1293
- diff_stats=$(git diff --stat "${BASE_BRANCH}...${GIT_BRANCH}" 2>/dev/null | tail -1 || echo "")
1458
+ diff_stats=$(_safe_base_diff --stat | tail -1 || echo "")
1294
1459
  info "Running AI code review... ${DIM}($diff_stats)${RESET}"
1295
1460
 
1296
1461
  # Semantic risk scoring when intelligence is enabled
1297
1462
  if type intelligence_search_memory >/dev/null 2>&1 && command -v claude >/dev/null 2>&1; then
1298
1463
  local diff_files
1299
- diff_files=$(git diff --name-only "${BASE_BRANCH}...${GIT_BRANCH}" 2>/dev/null || true)
1464
+ diff_files=$(_safe_base_diff --name-only || true)
1300
1465
  local risk_score="low"
1301
1466
  # Fast heuristic: flag high-risk file patterns
1302
1467
  if echo "$diff_files" | grep -qiE 'migration|schema|auth|crypto|security|password|token|secret|\.env'; then
@@ -1343,6 +1508,7 @@ If no issues are found, write: \"Review clean — no issues found.\"
1343
1508
  if type intelligence_search_memory >/dev/null 2>&1; then
1344
1509
  local review_memory
1345
1510
  review_memory=$(intelligence_search_memory "code review findings anti-patterns for: ${GOAL:-}" "${HOME}/.shipwright/memory" 5 2>/dev/null) || true
1511
+ review_memory=$(prune_context_section "memory" "$review_memory" 10000)
1346
1512
  if [[ -n "$review_memory" ]]; then
1347
1513
  review_prompt+="
1348
1514
  ## Known Issues from Previous Reviews
@@ -1390,11 +1556,12 @@ $(cat "$dod_file")
1390
1556
  ## Diff to Review
1391
1557
  $(cat "$diff_file")"
1392
1558
 
1393
- # Build claude args — add --dangerously-skip-permissions in CI
1394
- local review_args=(--print --model "$review_model" --max-turns 25)
1395
- if [[ "${CI_MODE:-false}" == "true" ]]; then
1396
- review_args+=(--dangerously-skip-permissions)
1397
- fi
1559
+ # Guard total prompt size
1560
+ review_prompt=$(guard_prompt_size "review" "$review_prompt")
1561
+
1562
+ # Skip permissions — pipeline runs headlessly (claude -p) and has no terminal
1563
+ # for interactive permission prompts. Same rationale as build stage (line ~1083).
1564
+ local review_args=(--print --model "$review_model" --max-turns 25 --dangerously-skip-permissions)
1398
1565
 
1399
1566
  claude "${review_args[@]}" "$review_prompt" < /dev/null > "$review_file" 2>"${ARTIFACTS_DIR}/.claude-tokens-review.log" || true
1400
1567
  parse_claude_tokens "${ARTIFACTS_DIR}/.claude-tokens-review.log"
@@ -1539,15 +1706,143 @@ ${review_summary}
1539
1706
  log_stage "review" "AI review complete ($total_issues issues: $critical_count critical, $bug_count bugs, $warning_count suggestions)"
1540
1707
  }
1541
1708
 
1709
+ # ─── Compound Quality (fallback) ────────────────────────────────────────────
1710
+ # Basic implementation: adversarial review, negative testing, e2e checks, DoD audit.
1711
+ # If pipeline-intelligence.sh was sourced first, its enhanced version takes priority.
1712
+ if ! type stage_compound_quality >/dev/null 2>&1; then
1713
+ stage_compound_quality() {
1714
+ CURRENT_STAGE_ID="compound_quality"
1715
+
1716
+ # Read stage config from pipeline template
1717
+ local cfg
1718
+ cfg=$(jq -r '.stages[] | select(.id == "compound_quality") | .config // {}' "$PIPELINE_CONFIG" 2>/dev/null) || cfg="{}"
1719
+
1720
+ local do_adversarial do_negative do_e2e do_dod max_cycles blocking
1721
+ do_adversarial=$(echo "$cfg" | jq -r '.adversarial // false')
1722
+ do_negative=$(echo "$cfg" | jq -r '.negative // false')
1723
+ do_e2e=$(echo "$cfg" | jq -r '.e2e // false')
1724
+ do_dod=$(echo "$cfg" | jq -r '.dod_audit // false')
1725
+ max_cycles=$(echo "$cfg" | jq -r '.max_cycles // 1')
1726
+ blocking=$(echo "$cfg" | jq -r '.compound_quality_blocking // false')
1727
+
1728
+ local pass_count=0 fail_count=0 total=0
1729
+ local compound_log="$ARTIFACTS_DIR/compound-quality.log"
1730
+ : > "$compound_log"
1731
+
1732
+ # ── Adversarial review ──
1733
+ if [[ "$do_adversarial" == "true" ]]; then
1734
+ total=$((total + 1))
1735
+ info "Running adversarial review..."
1736
+ if [[ -x "$SCRIPT_DIR/sw-adversarial.sh" ]]; then
1737
+ if bash "$SCRIPT_DIR/sw-adversarial.sh" --repo "${REPO_DIR:-.}" >> "$compound_log" 2>&1; then
1738
+ pass_count=$((pass_count + 1))
1739
+ success "Adversarial review passed"
1740
+ else
1741
+ fail_count=$((fail_count + 1))
1742
+ warn "Adversarial review found issues"
1743
+ fi
1744
+ else
1745
+ warn "sw-adversarial.sh not found, skipping"
1746
+ fi
1747
+ fi
1748
+
1749
+ # ── Negative / edge-case testing ──
1750
+ if [[ "$do_negative" == "true" ]]; then
1751
+ total=$((total + 1))
1752
+ info "Running negative test pass..."
1753
+ if [[ -n "${TEST_CMD:-}" ]]; then
1754
+ if eval "$TEST_CMD" >> "$compound_log" 2>&1; then
1755
+ pass_count=$((pass_count + 1))
1756
+ success "Negative test pass passed"
1757
+ else
1758
+ fail_count=$((fail_count + 1))
1759
+ warn "Negative test pass found failures"
1760
+ fi
1761
+ else
1762
+ pass_count=$((pass_count + 1))
1763
+ info "No test command configured, skipping negative tests"
1764
+ fi
1765
+ fi
1766
+
1767
+ # ── E2E checks ──
1768
+ if [[ "$do_e2e" == "true" ]]; then
1769
+ total=$((total + 1))
1770
+ info "Running e2e checks..."
1771
+ if [[ -x "$SCRIPT_DIR/sw-e2e-orchestrator.sh" ]]; then
1772
+ if bash "$SCRIPT_DIR/sw-e2e-orchestrator.sh" run >> "$compound_log" 2>&1; then
1773
+ pass_count=$((pass_count + 1))
1774
+ success "E2E checks passed"
1775
+ else
1776
+ fail_count=$((fail_count + 1))
1777
+ warn "E2E checks found issues"
1778
+ fi
1779
+ else
1780
+ pass_count=$((pass_count + 1))
1781
+ info "sw-e2e-orchestrator.sh not found, skipping e2e"
1782
+ fi
1783
+ fi
1784
+
1785
+ # ── Definition of Done audit ──
1786
+ if [[ "$do_dod" == "true" ]]; then
1787
+ total=$((total + 1))
1788
+ info "Running definition-of-done audit..."
1789
+ if [[ -x "$SCRIPT_DIR/sw-quality.sh" ]]; then
1790
+ if bash "$SCRIPT_DIR/sw-quality.sh" validate >> "$compound_log" 2>&1; then
1791
+ pass_count=$((pass_count + 1))
1792
+ success "DoD audit passed"
1793
+ else
1794
+ fail_count=$((fail_count + 1))
1795
+ warn "DoD audit found gaps"
1796
+ fi
1797
+ else
1798
+ pass_count=$((pass_count + 1))
1799
+ info "sw-quality.sh not found, skipping DoD audit"
1800
+ fi
1801
+ fi
1802
+
1803
+ # ── Summary ──
1804
+ log_stage "compound_quality" "Compound quality: $pass_count/$total checks passed, $fail_count failed"
1805
+
1806
+ if [[ "$fail_count" -gt 0 && "$blocking" == "true" ]]; then
1807
+ error "Compound quality gate failed: $fail_count of $total checks failed"
1808
+ return 1
1809
+ fi
1810
+
1811
+ return 0
1812
+ }
1813
+ fi # end fallback stage_compound_quality
1814
+
1542
1815
  stage_pr() {
1543
1816
  CURRENT_STAGE_ID="pr"
1544
1817
  local plan_file="$ARTIFACTS_DIR/plan.md"
1545
1818
  local test_log="$ARTIFACTS_DIR/test-results.log"
1546
1819
  local review_file="$ARTIFACTS_DIR/review.md"
1547
1820
 
1821
+ # ── Skip PR in local/no-github mode ──
1822
+ if [[ "${NO_GITHUB:-false}" == "true" || "${SHIPWRIGHT_LOCAL:-}" == "1" || "${LOCAL_MODE:-false}" == "true" ]]; then
1823
+ info "Skipping PR stage — running in local/no-github mode"
1824
+ # Save a PR draft locally for reference
1825
+ local branch_name
1826
+ branch_name=$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "unknown")
1827
+ local commit_count
1828
+ commit_count=$(_safe_base_log --oneline | wc -l | xargs)
1829
+ {
1830
+ echo "# PR Draft (local mode)"
1831
+ echo ""
1832
+ echo "**Branch:** ${branch_name}"
1833
+ echo "**Commits:** ${commit_count:-0}"
1834
+ echo "**Goal:** ${GOAL:-N/A}"
1835
+ echo ""
1836
+ echo "## Changes"
1837
+ _safe_base_diff --stat || true
1838
+ } > ".claude/pr-draft.md" 2>/dev/null || true
1839
+ emit_event "pr.skipped" "issue=${ISSUE_NUMBER:-0}" "reason=local_mode"
1840
+ return 0
1841
+ fi
1842
+
1548
1843
  # ── PR Hygiene Checks (informational) ──
1549
1844
  local hygiene_commit_count
1550
- hygiene_commit_count=$(git log --oneline "${BASE_BRANCH}..HEAD" 2>/dev/null | wc -l | xargs)
1845
+ hygiene_commit_count=$(_safe_base_log --oneline | wc -l | xargs)
1551
1846
  hygiene_commit_count="${hygiene_commit_count:-0}"
1552
1847
 
1553
1848
  if [[ "$hygiene_commit_count" -gt 20 ]]; then
@@ -1556,7 +1851,7 @@ stage_pr() {
1556
1851
 
1557
1852
  # Check for WIP/fixup/squash commits (expanded patterns)
1558
1853
  local wip_commits
1559
- 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)
1854
+ 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)
1560
1855
  wip_commits="${wip_commits:-0}"
1561
1856
  if [[ "$wip_commits" -gt 0 ]]; then
1562
1857
  warn "Branch has ${wip_commits} WIP/fixup/squash/temp commit(s) — consider cleaning up"
@@ -1564,7 +1859,7 @@ stage_pr() {
1564
1859
 
1565
1860
  # ── PR Quality Gate: reject PRs with no real code changes ──
1566
1861
  local real_files
1567
- real_files=$(git diff --name-only "${BASE_BRANCH}...HEAD" 2>/dev/null | grep -v '^\.claude/' | grep -v '^\.github/' || true)
1862
+ real_files=$(_safe_base_diff --name-only | grep -v '^\.claude/' | grep -v '^\.github/' || true)
1568
1863
  if [[ -z "$real_files" ]]; then
1569
1864
  error "No real code changes detected — only pipeline artifacts (.claude/ logs)."
1570
1865
  error "The build agent did not produce meaningful changes. Skipping PR creation."
@@ -1614,7 +1909,7 @@ stage_pr() {
1614
1909
  if [[ "$sim_enabled" == "true" ]]; then
1615
1910
  info "Running developer simulation review..."
1616
1911
  local diff_for_sim
1617
- diff_for_sim=$(git diff "${BASE_BRANCH}...HEAD" 2>/dev/null || true)
1912
+ diff_for_sim=$(_safe_base_diff || true)
1618
1913
  if [[ -n "$diff_for_sim" ]]; then
1619
1914
  local sim_result
1620
1915
  sim_result=$(simulation_review "$diff_for_sim" "${GOAL:-}" 2>/dev/null || echo "")
@@ -1644,7 +1939,7 @@ stage_pr() {
1644
1939
  if [[ "$arch_enabled" == "true" ]]; then
1645
1940
  info "Validating architecture..."
1646
1941
  local diff_for_arch
1647
- diff_for_arch=$(git diff "${BASE_BRANCH}...HEAD" 2>/dev/null || true)
1942
+ diff_for_arch=$(_safe_base_diff || true)
1648
1943
  if [[ -n "$diff_for_arch" ]]; then
1649
1944
  local arch_result
1650
1945
  arch_result=$(architecture_validate_changes "$diff_for_arch" "" 2>/dev/null || echo "")
@@ -1668,10 +1963,11 @@ stage_pr() {
1668
1963
 
1669
1964
  # Pre-PR diff gate — verify meaningful code changes exist (not just bookkeeping)
1670
1965
  local real_changes
1671
- real_changes=$(git diff --name-only "origin/${BASE_BRANCH:-main}...HEAD" \
1966
+ real_changes=$(_safe_base_diff --name-only \
1672
1967
  -- . ':!.claude/loop-state.md' ':!.claude/pipeline-state.md' \
1673
1968
  ':!.claude/pipeline-artifacts/*' ':!**/progress.md' \
1674
- ':!**/error-summary.json' 2>/dev/null | wc -l | xargs || echo "0")
1969
+ ':!**/error-summary.json' | wc -l | xargs || true)
1970
+ real_changes="${real_changes:-0}"
1675
1971
  if [[ "${real_changes:-0}" -eq 0 ]]; then
1676
1972
  error "No meaningful code changes detected — only bookkeeping files modified"
1677
1973
  error "Refusing to create PR with zero real changes"
@@ -1726,10 +2022,10 @@ stage_pr() {
1726
2022
  [[ -n "${GITHUB_ISSUE:-}" ]] && closes_line="Closes ${GITHUB_ISSUE}"
1727
2023
 
1728
2024
  local diff_stats
1729
- diff_stats=$(git diff --stat "${BASE_BRANCH}...${GIT_BRANCH}" 2>/dev/null | tail -1 || echo "")
2025
+ diff_stats=$(_safe_base_diff --stat | tail -1 || echo "")
1730
2026
 
1731
2027
  local commit_count
1732
- commit_count=$(git log --oneline "${BASE_BRANCH}..HEAD" 2>/dev/null | wc -l | xargs)
2028
+ commit_count=$(_safe_base_log --oneline | wc -l | xargs)
1733
2029
 
1734
2030
  local total_dur=""
1735
2031
  if [[ -n "$PIPELINE_START_EPOCH" ]]; then
@@ -1774,7 +2070,7 @@ EOF
1774
2070
  risk_tier="low"
1775
2071
  if [[ -f "$REPO_DIR/config/policy.json" ]]; then
1776
2072
  local changed_files
1777
- changed_files=$(git diff --name-only "${BASE_BRANCH}...HEAD" 2>/dev/null || true)
2073
+ changed_files=$(_safe_base_diff --name-only || true)
1778
2074
  if [[ -n "$changed_files" ]]; then
1779
2075
  local policy_file="$REPO_DIR/config/policy.json"
1780
2076
  check_tier_match() {
@@ -1906,7 +2202,7 @@ EOF
1906
2202
  codeowners_json=$(gh_codeowners "$REPO_OWNER" "$REPO_NAME" 2>/dev/null || echo "[]")
1907
2203
  if [[ "$codeowners_json" != "[]" && -n "$codeowners_json" ]]; then
1908
2204
  local changed_files
1909
- changed_files=$(git diff --name-only "${BASE_BRANCH}...HEAD" 2>/dev/null || true)
2205
+ changed_files=$(_safe_base_diff --name-only || true)
1910
2206
  if [[ -n "$changed_files" ]]; then
1911
2207
  local co_reviewers
1912
2208
  co_reviewers=$(echo "$codeowners_json" | jq -r '.[].owners[]' 2>/dev/null | sort -u | head -3 || true)
@@ -1980,13 +2276,14 @@ stage_merge() {
1980
2276
  local merge_diff_file="${ARTIFACTS_DIR}/review-diff.patch"
1981
2277
  local merge_review_file="${ARTIFACTS_DIR}/review.md"
1982
2278
  if [[ ! -s "$merge_diff_file" ]]; then
1983
- git diff "${BASE_BRANCH}...${GIT_BRANCH}" > "$merge_diff_file" 2>/dev/null || \
1984
- git diff HEAD~5 > "$merge_diff_file" 2>/dev/null || true
2279
+ _safe_base_diff > "$merge_diff_file" 2>/dev/null || true
1985
2280
  fi
1986
2281
  if [[ -s "$merge_diff_file" ]]; then
1987
2282
  local _merge_critical _merge_sec _merge_blocking _merge_reject
1988
- _merge_critical=$(grep -ciE '\*\*\[?Critical\]?\*\*' "$merge_review_file" 2>/dev/null || echo "0")
1989
- _merge_sec=$(grep -ciE '\*\*\[?Security\]?\*\*' "$merge_review_file" 2>/dev/null || echo "0")
2283
+ _merge_critical=$(grep -ciE '\*\*\[?Critical\]?\*\*' "$merge_review_file" 2>/dev/null || true)
2284
+ _merge_critical="${_merge_critical:-0}"
2285
+ _merge_sec=$(grep -ciE '\*\*\[?Security\]?\*\*' "$merge_review_file" 2>/dev/null || true)
2286
+ _merge_sec="${_merge_sec:-0}"
1990
2287
  _merge_blocking=$((${_merge_critical:-0} + ${_merge_sec:-0}))
1991
2288
  [[ "$_merge_blocking" -gt 0 ]] && _merge_reject="Review found ${_merge_blocking} critical/security issue(s)"
1992
2289
  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
@@ -176,6 +176,13 @@ mark_stage_complete() {
176
176
  write_state
177
177
 
178
178
  record_stage_effectiveness "$stage_id" "complete"
179
+
180
+ # Record stage completion in SQLite pipeline_stages table
181
+ if type record_stage >/dev/null 2>&1; then
182
+ local _stage_secs
183
+ _stage_secs=$(get_stage_timing_seconds "$stage_id")
184
+ record_stage "${SHIPWRIGHT_PIPELINE_ID:-}" "$stage_id" "complete" "${_stage_secs:-0}" "" 2>/dev/null || true
185
+ fi
179
186
  # Update memory baselines and predictive baselines for stage durations
180
187
  if [[ "$stage_id" == "test" || "$stage_id" == "build" ]]; then
181
188
  local secs
@@ -354,6 +361,13 @@ mark_stage_failed() {
354
361
  log_stage "$stage_id" "failed (${timing})"
355
362
  write_state
356
363
 
364
+ # Record stage failure in SQLite pipeline_stages table
365
+ if type record_stage >/dev/null 2>&1; then
366
+ local _stage_secs
367
+ _stage_secs=$(get_stage_timing_seconds "$stage_id")
368
+ record_stage "${SHIPWRIGHT_PIPELINE_ID:-}" "$stage_id" "failed" "${_stage_secs:-0}" "" 2>/dev/null || true
369
+ fi
370
+
357
371
  # Update GitHub progress + comment failure
358
372
  if [[ -n "$ISSUE_NUMBER" ]]; then
359
373
  local body
File without changes
File without changes