shipwright-cli 3.0.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 (135) hide show
  1. package/README.md +3 -3
  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/event-schema.json +142 -5
  7. package/config/policy.json +8 -0
  8. package/package.json +3 -3
  9. package/scripts/lib/architecture.sh +2 -1
  10. package/scripts/lib/bootstrap.sh +0 -0
  11. package/scripts/lib/config.sh +0 -0
  12. package/scripts/lib/daemon-adaptive.sh +0 -0
  13. package/scripts/lib/daemon-dispatch.sh +24 -1
  14. package/scripts/lib/daemon-failure.sh +0 -0
  15. package/scripts/lib/daemon-health.sh +0 -0
  16. package/scripts/lib/daemon-patrol.sh +40 -5
  17. package/scripts/lib/daemon-poll.sh +17 -0
  18. package/scripts/lib/daemon-state.sh +10 -0
  19. package/scripts/lib/daemon-triage.sh +1 -1
  20. package/scripts/lib/decide-autonomy.sh +295 -0
  21. package/scripts/lib/decide-scoring.sh +228 -0
  22. package/scripts/lib/decide-signals.sh +462 -0
  23. package/scripts/lib/fleet-failover.sh +0 -0
  24. package/scripts/lib/helpers.sh +16 -17
  25. package/scripts/lib/pipeline-detection.sh +0 -0
  26. package/scripts/lib/pipeline-github.sh +0 -0
  27. package/scripts/lib/pipeline-intelligence.sh +20 -3
  28. package/scripts/lib/pipeline-quality-checks.sh +3 -2
  29. package/scripts/lib/pipeline-quality.sh +0 -0
  30. package/scripts/lib/pipeline-stages.sh +199 -32
  31. package/scripts/lib/pipeline-state.sh +14 -0
  32. package/scripts/lib/policy.sh +0 -0
  33. package/scripts/lib/test-helpers.sh +0 -0
  34. package/scripts/postinstall.mjs +75 -1
  35. package/scripts/signals/example-collector.sh +36 -0
  36. package/scripts/sw +8 -4
  37. package/scripts/sw-activity.sh +1 -1
  38. package/scripts/sw-adaptive.sh +1 -1
  39. package/scripts/sw-adversarial.sh +1 -1
  40. package/scripts/sw-architecture-enforcer.sh +1 -1
  41. package/scripts/sw-auth.sh +1 -1
  42. package/scripts/sw-autonomous.sh +1 -1
  43. package/scripts/sw-changelog.sh +1 -1
  44. package/scripts/sw-checkpoint.sh +1 -1
  45. package/scripts/sw-ci.sh +1 -1
  46. package/scripts/sw-cleanup.sh +1 -1
  47. package/scripts/sw-code-review.sh +1 -1
  48. package/scripts/sw-connect.sh +1 -1
  49. package/scripts/sw-context.sh +1 -1
  50. package/scripts/sw-cost.sh +12 -3
  51. package/scripts/sw-daemon.sh +2 -2
  52. package/scripts/sw-dashboard.sh +1 -1
  53. package/scripts/sw-db.sh +41 -34
  54. package/scripts/sw-decide.sh +685 -0
  55. package/scripts/sw-decompose.sh +1 -1
  56. package/scripts/sw-deps.sh +1 -1
  57. package/scripts/sw-developer-simulation.sh +1 -1
  58. package/scripts/sw-discovery.sh +27 -1
  59. package/scripts/sw-doc-fleet.sh +1 -1
  60. package/scripts/sw-docs-agent.sh +1 -1
  61. package/scripts/sw-docs.sh +1 -1
  62. package/scripts/sw-doctor.sh +1 -1
  63. package/scripts/sw-dora.sh +1 -1
  64. package/scripts/sw-durable.sh +1 -1
  65. package/scripts/sw-e2e-orchestrator.sh +1 -1
  66. package/scripts/sw-eventbus.sh +1 -1
  67. package/scripts/sw-evidence.sh +1 -1
  68. package/scripts/sw-feedback.sh +1 -1
  69. package/scripts/sw-fix.sh +1 -1
  70. package/scripts/sw-fleet-discover.sh +1 -1
  71. package/scripts/sw-fleet-viz.sh +1 -1
  72. package/scripts/sw-fleet.sh +1 -1
  73. package/scripts/sw-github-app.sh +1 -1
  74. package/scripts/sw-github-checks.sh +1 -1
  75. package/scripts/sw-github-deploy.sh +1 -1
  76. package/scripts/sw-github-graphql.sh +1 -1
  77. package/scripts/sw-guild.sh +1 -1
  78. package/scripts/sw-heartbeat.sh +1 -1
  79. package/scripts/sw-hygiene.sh +1 -1
  80. package/scripts/sw-incident.sh +1 -1
  81. package/scripts/sw-init.sh +1 -1
  82. package/scripts/sw-instrument.sh +1 -1
  83. package/scripts/sw-intelligence.sh +9 -5
  84. package/scripts/sw-jira.sh +1 -1
  85. package/scripts/sw-launchd.sh +1 -1
  86. package/scripts/sw-linear.sh +1 -1
  87. package/scripts/sw-logs.sh +1 -1
  88. package/scripts/sw-loop.sh +267 -17
  89. package/scripts/sw-memory.sh +22 -5
  90. package/scripts/sw-mission-control.sh +1 -1
  91. package/scripts/sw-model-router.sh +1 -1
  92. package/scripts/sw-otel.sh +5 -3
  93. package/scripts/sw-oversight.sh +1 -1
  94. package/scripts/sw-pipeline-composer.sh +1 -1
  95. package/scripts/sw-pipeline-vitals.sh +1 -1
  96. package/scripts/sw-pipeline.sh +73 -1
  97. package/scripts/sw-pm.sh +1 -1
  98. package/scripts/sw-pr-lifecycle.sh +7 -4
  99. package/scripts/sw-predictive.sh +1 -1
  100. package/scripts/sw-prep.sh +1 -1
  101. package/scripts/sw-ps.sh +1 -1
  102. package/scripts/sw-public-dashboard.sh +1 -1
  103. package/scripts/sw-quality.sh +9 -5
  104. package/scripts/sw-reaper.sh +1 -1
  105. package/scripts/sw-regression.sh +1 -1
  106. package/scripts/sw-release-manager.sh +1 -1
  107. package/scripts/sw-release.sh +1 -1
  108. package/scripts/sw-remote.sh +1 -1
  109. package/scripts/sw-replay.sh +1 -1
  110. package/scripts/sw-retro.sh +1 -1
  111. package/scripts/sw-review-rerun.sh +1 -1
  112. package/scripts/sw-scale.sh +66 -10
  113. package/scripts/sw-security-audit.sh +1 -1
  114. package/scripts/sw-self-optimize.sh +1 -1
  115. package/scripts/sw-session.sh +3 -3
  116. package/scripts/sw-setup.sh +1 -1
  117. package/scripts/sw-standup.sh +1 -1
  118. package/scripts/sw-status.sh +1 -1
  119. package/scripts/sw-strategic.sh +1 -1
  120. package/scripts/sw-stream.sh +1 -1
  121. package/scripts/sw-swarm.sh +1 -1
  122. package/scripts/sw-team-stages.sh +1 -1
  123. package/scripts/sw-templates.sh +1 -1
  124. package/scripts/sw-testgen.sh +1 -1
  125. package/scripts/sw-tmux-pipeline.sh +1 -1
  126. package/scripts/sw-tmux.sh +1 -1
  127. package/scripts/sw-trace.sh +1 -1
  128. package/scripts/sw-tracker.sh +1 -1
  129. package/scripts/sw-triage.sh +6 -6
  130. package/scripts/sw-upgrade.sh +1 -1
  131. package/scripts/sw-ux.sh +1 -1
  132. package/scripts/sw-webhook.sh +1 -1
  133. package/scripts/sw-widgets.sh +1 -1
  134. package/scripts/sw-worktree.sh +1 -1
  135. package/scripts/update-homebrew-sha.sh +21 -15
@@ -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 ""
@@ -15,6 +30,7 @@ show_stage_preview() {
15
30
  test_first) echo -e " Generate tests from requirements (TDD mode) before implementation" ;;
16
31
  test) echo -e " Run test suite and check coverage" ;;
17
32
  review) echo -e " AI code review on the diff, post findings" ;;
33
+ compound_quality) echo -e " Adversarial review, negative tests, e2e, DoD audit" ;;
18
34
  pr) echo -e " Create GitHub PR with labels, reviewers, milestone" ;;
19
35
  merge) echo -e " Wait for CI checks, merge PR, optionally delete branch" ;;
20
36
  deploy) echo -e " Deploy to staging/production with rollback" ;;
@@ -300,10 +316,22 @@ Checklist of completion criteria.
300
316
  fi
301
317
 
302
318
  local _token_log="${ARTIFACTS_DIR}/.claude-tokens-plan.log"
303
- claude --print --model "$plan_model" --max-turns 25 \
319
+ claude --print --model "$plan_model" --max-turns 25 --dangerously-skip-permissions \
304
320
  "$plan_prompt" < /dev/null > "$plan_file" 2>"$_token_log" || true
305
321
  parse_claude_tokens "$_token_log"
306
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
+
307
335
  if [[ ! -s "$plan_file" ]]; then
308
336
  error "Plan generation failed — empty output"
309
337
  return 1
@@ -708,10 +736,22 @@ Be concrete and specific. Reference actual file paths in the codebase. Consider
708
736
  fi
709
737
 
710
738
  local _token_log="${ARTIFACTS_DIR}/.claude-tokens-design.log"
711
- claude --print --model "$design_model" --max-turns 25 \
739
+ claude --print --model "$design_model" --max-turns 25 --dangerously-skip-permissions \
712
740
  "$design_prompt" < /dev/null > "$design_file" 2>"$_token_log" || true
713
741
  parse_claude_tokens "$_token_log"
714
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
+
715
755
  if [[ ! -s "$design_file" ]]; then
716
756
  error "Design generation failed — empty output"
717
757
  return 1
@@ -739,7 +779,7 @@ Be concrete and specific. Reference actual file paths in the codebase. Consider
739
779
  files_to_modify=$(sed -n '/Files to modify/,/^-\|^#\|^$/p' "$design_file" 2>/dev/null | grep -E '^\s*-' | head -20 || true)
740
780
 
741
781
  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}"
782
+ info "Design scope: ${DIM}$(echo "$files_to_create $files_to_modify" | grep -c '^\s*-' || true) file(s)${RESET}"
743
783
  fi
744
784
 
745
785
  # Post design to GitHub issue
@@ -1077,8 +1117,9 @@ ${prevention_text}"
1077
1117
  loop_args+=(--resume)
1078
1118
  fi
1079
1119
 
1080
- # Skip permissions in CI (no interactive terminal)
1081
- [[ "${CI_MODE:-false}" == "true" ]] && loop_args+=(--skip-permissions)
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)
1082
1123
 
1083
1124
  info "Starting build loop: ${DIM}shipwright loop${RESET} (max ${max_iter} iterations, ${agents} agent(s))"
1084
1125
 
@@ -1131,13 +1172,13 @@ ${prevention_text}"
1131
1172
 
1132
1173
  # Count commits made during build
1133
1174
  local commit_count
1134
- commit_count=$(git log --oneline "${BASE_BRANCH}..HEAD" 2>/dev/null | wc -l | xargs)
1175
+ commit_count=$(_safe_base_log --oneline | wc -l | xargs)
1135
1176
  info "Build produced ${BOLD}$commit_count${RESET} commit(s)"
1136
1177
 
1137
1178
  # Commit quality evaluation when intelligence is enabled
1138
1179
  if type intelligence_search_memory >/dev/null 2>&1 && command -v claude >/dev/null 2>&1 && [[ "${commit_count:-0}" -gt 0 ]]; then
1139
1180
  local commit_msgs
1140
- commit_msgs=$(git log --format="%s" "${BASE_BRANCH}..HEAD" 2>/dev/null | head -20)
1181
+ commit_msgs=$(_safe_base_log --format="%s" | head -20)
1141
1182
  local quality_score
1142
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.
1143
1184
 
@@ -1276,8 +1317,7 @@ stage_review() {
1276
1317
  local diff_file="$ARTIFACTS_DIR/review-diff.patch"
1277
1318
  local review_file="$ARTIFACTS_DIR/review.md"
1278
1319
 
1279
- git diff "${BASE_BRANCH}...${GIT_BRANCH}" > "$diff_file" 2>/dev/null || \
1280
- git diff HEAD~5 > "$diff_file" 2>/dev/null || true
1320
+ _safe_base_diff > "$diff_file" 2>/dev/null || true
1281
1321
 
1282
1322
  if [[ ! -s "$diff_file" ]]; then
1283
1323
  warn "No diff found — skipping review"
@@ -1290,13 +1330,13 @@ stage_review() {
1290
1330
  fi
1291
1331
 
1292
1332
  local diff_stats
1293
- 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 "")
1294
1334
  info "Running AI code review... ${DIM}($diff_stats)${RESET}"
1295
1335
 
1296
1336
  # Semantic risk scoring when intelligence is enabled
1297
1337
  if type intelligence_search_memory >/dev/null 2>&1 && command -v claude >/dev/null 2>&1; then
1298
1338
  local diff_files
1299
- diff_files=$(git diff --name-only "${BASE_BRANCH}...${GIT_BRANCH}" 2>/dev/null || true)
1339
+ diff_files=$(_safe_base_diff --name-only || true)
1300
1340
  local risk_score="low"
1301
1341
  # Fast heuristic: flag high-risk file patterns
1302
1342
  if echo "$diff_files" | grep -qiE 'migration|schema|auth|crypto|security|password|token|secret|\.env'; then
@@ -1390,11 +1430,9 @@ $(cat "$dod_file")
1390
1430
  ## Diff to Review
1391
1431
  $(cat "$diff_file")"
1392
1432
 
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
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)
1398
1436
 
1399
1437
  claude "${review_args[@]}" "$review_prompt" < /dev/null > "$review_file" 2>"${ARTIFACTS_DIR}/.claude-tokens-review.log" || true
1400
1438
  parse_claude_tokens "${ARTIFACTS_DIR}/.claude-tokens-review.log"
@@ -1539,15 +1577,143 @@ ${review_summary}
1539
1577
  log_stage "review" "AI review complete ($total_issues issues: $critical_count critical, $bug_count bugs, $warning_count suggestions)"
1540
1578
  }
1541
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
+
1542
1686
  stage_pr() {
1543
1687
  CURRENT_STAGE_ID="pr"
1544
1688
  local plan_file="$ARTIFACTS_DIR/plan.md"
1545
1689
  local test_log="$ARTIFACTS_DIR/test-results.log"
1546
1690
  local review_file="$ARTIFACTS_DIR/review.md"
1547
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
+
1548
1714
  # ── PR Hygiene Checks (informational) ──
1549
1715
  local hygiene_commit_count
1550
- 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)
1551
1717
  hygiene_commit_count="${hygiene_commit_count:-0}"
1552
1718
 
1553
1719
  if [[ "$hygiene_commit_count" -gt 20 ]]; then
@@ -1556,7 +1722,7 @@ stage_pr() {
1556
1722
 
1557
1723
  # Check for WIP/fixup/squash commits (expanded patterns)
1558
1724
  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)
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)
1560
1726
  wip_commits="${wip_commits:-0}"
1561
1727
  if [[ "$wip_commits" -gt 0 ]]; then
1562
1728
  warn "Branch has ${wip_commits} WIP/fixup/squash/temp commit(s) — consider cleaning up"
@@ -1564,7 +1730,7 @@ stage_pr() {
1564
1730
 
1565
1731
  # ── PR Quality Gate: reject PRs with no real code changes ──
1566
1732
  local real_files
1567
- 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)
1568
1734
  if [[ -z "$real_files" ]]; then
1569
1735
  error "No real code changes detected — only pipeline artifacts (.claude/ logs)."
1570
1736
  error "The build agent did not produce meaningful changes. Skipping PR creation."
@@ -1614,7 +1780,7 @@ stage_pr() {
1614
1780
  if [[ "$sim_enabled" == "true" ]]; then
1615
1781
  info "Running developer simulation review..."
1616
1782
  local diff_for_sim
1617
- diff_for_sim=$(git diff "${BASE_BRANCH}...HEAD" 2>/dev/null || true)
1783
+ diff_for_sim=$(_safe_base_diff || true)
1618
1784
  if [[ -n "$diff_for_sim" ]]; then
1619
1785
  local sim_result
1620
1786
  sim_result=$(simulation_review "$diff_for_sim" "${GOAL:-}" 2>/dev/null || echo "")
@@ -1644,7 +1810,7 @@ stage_pr() {
1644
1810
  if [[ "$arch_enabled" == "true" ]]; then
1645
1811
  info "Validating architecture..."
1646
1812
  local diff_for_arch
1647
- diff_for_arch=$(git diff "${BASE_BRANCH}...HEAD" 2>/dev/null || true)
1813
+ diff_for_arch=$(_safe_base_diff || true)
1648
1814
  if [[ -n "$diff_for_arch" ]]; then
1649
1815
  local arch_result
1650
1816
  arch_result=$(architecture_validate_changes "$diff_for_arch" "" 2>/dev/null || echo "")
@@ -1668,10 +1834,10 @@ stage_pr() {
1668
1834
 
1669
1835
  # Pre-PR diff gate — verify meaningful code changes exist (not just bookkeeping)
1670
1836
  local real_changes
1671
- real_changes=$(git diff --name-only "origin/${BASE_BRANCH:-main}...HEAD" \
1837
+ real_changes=$(_safe_base_diff --name-only \
1672
1838
  -- . ':!.claude/loop-state.md' ':!.claude/pipeline-state.md' \
1673
1839
  ':!.claude/pipeline-artifacts/*' ':!**/progress.md' \
1674
- ':!**/error-summary.json' 2>/dev/null | wc -l | xargs || echo "0")
1840
+ ':!**/error-summary.json' | wc -l | xargs || echo "0")
1675
1841
  if [[ "${real_changes:-0}" -eq 0 ]]; then
1676
1842
  error "No meaningful code changes detected — only bookkeeping files modified"
1677
1843
  error "Refusing to create PR with zero real changes"
@@ -1726,10 +1892,10 @@ stage_pr() {
1726
1892
  [[ -n "${GITHUB_ISSUE:-}" ]] && closes_line="Closes ${GITHUB_ISSUE}"
1727
1893
 
1728
1894
  local diff_stats
1729
- 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 "")
1730
1896
 
1731
1897
  local commit_count
1732
- commit_count=$(git log --oneline "${BASE_BRANCH}..HEAD" 2>/dev/null | wc -l | xargs)
1898
+ commit_count=$(_safe_base_log --oneline | wc -l | xargs)
1733
1899
 
1734
1900
  local total_dur=""
1735
1901
  if [[ -n "$PIPELINE_START_EPOCH" ]]; then
@@ -1774,7 +1940,7 @@ EOF
1774
1940
  risk_tier="low"
1775
1941
  if [[ -f "$REPO_DIR/config/policy.json" ]]; then
1776
1942
  local changed_files
1777
- changed_files=$(git diff --name-only "${BASE_BRANCH}...HEAD" 2>/dev/null || true)
1943
+ changed_files=$(_safe_base_diff --name-only || true)
1778
1944
  if [[ -n "$changed_files" ]]; then
1779
1945
  local policy_file="$REPO_DIR/config/policy.json"
1780
1946
  check_tier_match() {
@@ -1906,7 +2072,7 @@ EOF
1906
2072
  codeowners_json=$(gh_codeowners "$REPO_OWNER" "$REPO_NAME" 2>/dev/null || echo "[]")
1907
2073
  if [[ "$codeowners_json" != "[]" && -n "$codeowners_json" ]]; then
1908
2074
  local changed_files
1909
- changed_files=$(git diff --name-only "${BASE_BRANCH}...HEAD" 2>/dev/null || true)
2075
+ changed_files=$(_safe_base_diff --name-only || true)
1910
2076
  if [[ -n "$changed_files" ]]; then
1911
2077
  local co_reviewers
1912
2078
  co_reviewers=$(echo "$codeowners_json" | jq -r '.[].owners[]' 2>/dev/null | sort -u | head -3 || true)
@@ -1980,13 +2146,14 @@ stage_merge() {
1980
2146
  local merge_diff_file="${ARTIFACTS_DIR}/review-diff.patch"
1981
2147
  local merge_review_file="${ARTIFACTS_DIR}/review.md"
1982
2148
  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
2149
+ _safe_base_diff > "$merge_diff_file" 2>/dev/null || true
1985
2150
  fi
1986
2151
  if [[ -s "$merge_diff_file" ]]; then
1987
2152
  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")
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}"
1990
2157
  _merge_blocking=$((${_merge_critical:-0} + ${_merge_sec:-0}))
1991
2158
  [[ "$_merge_blocking" -gt 0 ]] && _merge_reject="Review found ${_merge_blocking} critical/security issue(s)"
1992
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
@@ -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
@@ -11,8 +11,11 @@ import {
11
11
  readFileSync,
12
12
  writeFileSync,
13
13
  appendFileSync,
14
+ chmodSync,
15
+ readdirSync,
14
16
  } from "fs";
15
- import { join } from "path";
17
+ import { join, basename } from "path";
18
+ import { execSync } from "child_process";
16
19
 
17
20
  const HOME = process.env.HOME || process.env.USERPROFILE;
18
21
  const PKG_DIR = join(import.meta.dirname, "..");
@@ -130,6 +133,77 @@ try {
130
133
  success("Migrated legacy config (originals preserved)");
131
134
  }
132
135
 
136
+ // Set executable bits on all scripts (npm strips them on some platforms)
137
+ const scriptsDir = join(PKG_DIR, "scripts");
138
+ if (existsSync(scriptsDir)) {
139
+ let madeExecutable = 0;
140
+ for (const file of readdirSync(scriptsDir)) {
141
+ const fp = join(scriptsDir, file);
142
+ try {
143
+ chmodSync(fp, 0o755);
144
+ madeExecutable++;
145
+ } catch (_) {
146
+ // skip non-files
147
+ }
148
+ }
149
+ const libDir = join(scriptsDir, "lib");
150
+ if (existsSync(libDir)) {
151
+ for (const file of readdirSync(libDir)) {
152
+ try {
153
+ chmodSync(join(libDir, file), 0o755);
154
+ madeExecutable++;
155
+ } catch (_) {}
156
+ }
157
+ }
158
+ success(`Set executable bits on ${madeExecutable} scripts`);
159
+ }
160
+
161
+ // Install shell completions for the user's current shell
162
+ const completionsDir = join(PKG_DIR, "completions");
163
+ if (existsSync(completionsDir)) {
164
+ const shell = basename(process.env.SHELL || "/bin/bash");
165
+ try {
166
+ if (shell === "bash") {
167
+ const dest =
168
+ process.env.BASH_COMPLETION_USER_DIR ||
169
+ join(
170
+ process.env.XDG_DATA_HOME || join(HOME, ".local", "share"),
171
+ "bash-completion",
172
+ "completions",
173
+ );
174
+ ensureDir(dest);
175
+ cpSync(
176
+ join(completionsDir, "shipwright.bash"),
177
+ join(dest, "shipwright"),
178
+ );
179
+ cpSync(join(completionsDir, "shipwright.bash"), join(dest, "sw"));
180
+ success(`Installed bash completions to ${dest}`);
181
+ } else if (shell === "zsh") {
182
+ const dest = join(HOME, ".zfunc");
183
+ ensureDir(dest);
184
+ cpSync(join(completionsDir, "_shipwright"), join(dest, "_shipwright"));
185
+ cpSync(join(completionsDir, "_shipwright"), join(dest, "_sw"));
186
+ success(`Installed zsh completions to ${dest}`);
187
+ } else if (shell === "fish") {
188
+ const dest = join(
189
+ process.env.XDG_CONFIG_HOME || join(HOME, ".config"),
190
+ "fish",
191
+ "completions",
192
+ );
193
+ ensureDir(dest);
194
+ cpSync(
195
+ join(completionsDir, "shipwright.fish"),
196
+ join(dest, "shipwright.fish"),
197
+ );
198
+ cpSync(join(completionsDir, "shipwright.fish"), join(dest, "sw.fish"));
199
+ success(`Installed fish completions to ${dest}`);
200
+ }
201
+ } catch (e) {
202
+ warn(`Could not auto-install completions: ${e.message}`);
203
+ info(`Run: shipwright init (or: bash scripts/install-completions.sh)`);
204
+ }
205
+ }
206
+
133
207
  // Print success banner
134
208
  console.log();
135
209
  console.log(`${GREEN}${BOLD}Shipwright CLI installed!${RESET} Next steps:`);
@@ -0,0 +1,36 @@
1
+ #!/usr/bin/env bash
2
+ # ╔═══════════════════════════════════════════════════════════════════════════╗
3
+ # ║ Example External Signal Collector for Shipwright Decision Engine ║
4
+ # ║ Place custom collectors in scripts/signals/ — they're auto-discovered ║
5
+ # ╚═══════════════════════════════════════════════════════════════════════════╝
6
+ #
7
+ # Output: one JSON candidate per line (JSONL).
8
+ # Required fields: id, signal, category, title, description, risk_score,
9
+ # confidence, dedup_key
10
+ # Optional fields: evidence (object)
11
+ #
12
+ # The decision engine collects output from all scripts/signals/*.sh files,
13
+ # validates each line as JSON, and includes valid candidates in the scoring
14
+ # pipeline.
15
+ #
16
+ # Categories (determines autonomy tier):
17
+ # auto: deps_patch, deps_minor, security_patch, test_coverage,
18
+ # doc_sync, dead_code
19
+ # propose: refactor_hotspot, architecture_drift, performance_regression,
20
+ # deps_major, security_critical, recurring_failure, dora_regression
21
+ # draft: new_feature, breaking_change, business_logic, api_change,
22
+ # data_model_change
23
+ #
24
+ # Example: detect a custom condition and emit a candidate
25
+ #
26
+
27
+ set -euo pipefail
28
+
29
+ # Example: check if a TODO count exceeds a threshold
30
+ TODO_COUNT=$(grep -r "TODO" --include="*.ts" --include="*.js" --include="*.sh" . 2>/dev/null | wc -l | tr -d ' ' || echo "0")
31
+
32
+ if [[ "${TODO_COUNT:-0}" -gt 50 ]]; then
33
+ cat <<EOF
34
+ {"id":"custom-todo-cleanup","signal":"custom","category":"dead_code","title":"Clean up ${TODO_COUNT} TODOs","description":"Codebase has ${TODO_COUNT} TODO comments — consider a cleanup sprint","evidence":{"todo_count":${TODO_COUNT}},"risk_score":15,"confidence":"0.70","dedup_key":"custom:todo:cleanup"}
35
+ EOF
36
+ fi
package/scripts/sw CHANGED
@@ -5,7 +5,7 @@
5
5
  # ╚═══════════════════════════════════════════════════════════════════════════╝
6
6
  set -euo pipefail
7
7
 
8
- VERSION="3.0.0"
8
+ VERSION="3.1.0"
9
9
 
10
10
  # Resolve symlinks (required for npm global install where bin/ symlinks to node_modules/)
11
11
  SOURCE="${BASH_SOURCE[0]}"
@@ -193,7 +193,8 @@ route_quality() {
193
193
  security-audit|audit) exec "$SCRIPT_DIR/sw-security-audit.sh" "$@" ;;
194
194
  testgen) exec "$SCRIPT_DIR/sw-testgen.sh" "$@" ;;
195
195
  hygiene) exec "$SCRIPT_DIR/sw-hygiene.sh" "$@" ;;
196
- help|*) echo "Usage: shipwright quality {code-review|security-audit|testgen|hygiene}"; exit 1 ;;
196
+ validate|gate) exec "$SCRIPT_DIR/sw-quality.sh" "$@" ;;
197
+ help|*) echo "Usage: shipwright quality {code-review|security-audit|testgen|hygiene|validate}"; exit 1 ;;
197
198
  esac
198
199
  }
199
200
 
@@ -530,14 +531,17 @@ main() {
530
531
  evidence|ev)
531
532
  exec "$SCRIPT_DIR/sw-evidence.sh" "$@"
532
533
  ;;
534
+ decide)
535
+ exec "$SCRIPT_DIR/sw-decide.sh" "$@"
536
+ ;;
533
537
  otel)
534
538
  exec "$SCRIPT_DIR/sw-otel.sh" "$@"
535
539
  ;;
536
540
  triage)
537
541
  exec "$SCRIPT_DIR/sw-triage.sh" "$@"
538
542
  ;;
539
- quality)
540
- exec "$SCRIPT_DIR/sw-quality.sh" "$@"
543
+ pipeline-composer|composer)
544
+ exec "$SCRIPT_DIR/sw-pipeline-composer.sh" "$@"
541
545
  ;;
542
546
  oversight)
543
547
  exec "$SCRIPT_DIR/sw-oversight.sh" "$@"
@@ -6,7 +6,7 @@
6
6
  set -euo pipefail
7
7
  trap 'echo "ERROR: $BASH_SOURCE:$LINENO exited with status $?" >&2' ERR
8
8
 
9
- VERSION="3.0.0"
9
+ VERSION="3.1.0"
10
10
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
11
11
 
12
12
  # ─── Cross-platform compatibility ──────────────────────────────────────────
@@ -6,7 +6,7 @@
6
6
  set -euo pipefail
7
7
  trap 'echo "ERROR: $BASH_SOURCE:$LINENO exited with status $?" >&2' ERR
8
8
 
9
- VERSION="3.0.0"
9
+ VERSION="3.1.0"
10
10
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
11
11
 
12
12
  # ─── Cross-platform compatibility ──────────────────────────────────────────
@@ -6,7 +6,7 @@
6
6
  set -euo pipefail
7
7
  trap 'echo "ERROR: $BASH_SOURCE:$LINENO exited with status $?" >&2' ERR
8
8
 
9
- VERSION="3.0.0"
9
+ VERSION="3.1.0"
10
10
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
11
11
  REPO_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
12
12
 
@@ -6,7 +6,7 @@
6
6
  set -euo pipefail
7
7
  trap 'echo "ERROR: $BASH_SOURCE:$LINENO exited with status $?" >&2' ERR
8
8
 
9
- VERSION="3.0.0"
9
+ VERSION="3.1.0"
10
10
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
11
11
  REPO_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
12
12
 
@@ -6,7 +6,7 @@
6
6
  set -euo pipefail
7
7
  trap 'echo "ERROR: $BASH_SOURCE:$LINENO exited with status $?" >&2' ERR
8
8
 
9
- VERSION="3.0.0"
9
+ VERSION="3.1.0"
10
10
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
11
11
  REPO_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
12
12