shipwright-cli 3.1.0 → 3.3.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 (283) hide show
  1. package/.claude/agents/code-reviewer.md +2 -0
  2. package/.claude/agents/devops-engineer.md +2 -0
  3. package/.claude/agents/doc-fleet-agent.md +2 -0
  4. package/.claude/agents/pipeline-agent.md +2 -0
  5. package/.claude/agents/shell-script-specialist.md +2 -0
  6. package/.claude/agents/test-specialist.md +2 -0
  7. package/.claude/hooks/agent-crash-capture.sh +32 -0
  8. package/.claude/hooks/post-tool-use.sh +3 -2
  9. package/.claude/hooks/pre-tool-use.sh +35 -3
  10. package/README.md +22 -8
  11. package/claude-code/hooks/config-change.sh +18 -0
  12. package/claude-code/hooks/instructions-reloaded.sh +7 -0
  13. package/claude-code/hooks/worktree-create.sh +25 -0
  14. package/claude-code/hooks/worktree-remove.sh +20 -0
  15. package/config/code-constitution.json +130 -0
  16. package/config/defaults.json +25 -2
  17. package/config/policy.json +1 -1
  18. package/dashboard/middleware/auth.ts +134 -0
  19. package/dashboard/middleware/constants.ts +21 -0
  20. package/dashboard/public/index.html +8 -6
  21. package/dashboard/public/styles.css +176 -97
  22. package/dashboard/routes/auth.ts +38 -0
  23. package/dashboard/server.ts +117 -25
  24. package/dashboard/services/config.ts +26 -0
  25. package/dashboard/services/db.ts +118 -0
  26. package/dashboard/src/canvas/pixel-agent.ts +298 -0
  27. package/dashboard/src/canvas/pixel-sprites.ts +440 -0
  28. package/dashboard/src/canvas/shipyard-effects.ts +367 -0
  29. package/dashboard/src/canvas/shipyard-scene.ts +616 -0
  30. package/dashboard/src/canvas/submarine-layout.ts +267 -0
  31. package/dashboard/src/components/header.ts +8 -7
  32. package/dashboard/src/core/api.ts +5 -0
  33. package/dashboard/src/core/router.ts +1 -0
  34. package/dashboard/src/design/submarine-theme.ts +253 -0
  35. package/dashboard/src/main.ts +2 -0
  36. package/dashboard/src/types/api.ts +12 -1
  37. package/dashboard/src/views/activity.ts +2 -1
  38. package/dashboard/src/views/metrics.ts +69 -1
  39. package/dashboard/src/views/shipyard.ts +39 -0
  40. package/dashboard/types/index.ts +166 -0
  41. package/docs/plans/2026-02-28-compound-audit-and-shipyard-design.md +186 -0
  42. package/docs/plans/2026-02-28-skipper-shipwright-implementation-plan.md +1182 -0
  43. package/docs/plans/2026-02-28-skipper-shipwright-integration-design.md +531 -0
  44. package/docs/plans/2026-03-01-ai-powered-skill-injection-design.md +298 -0
  45. package/docs/plans/2026-03-01-ai-powered-skill-injection-plan.md +1109 -0
  46. package/docs/plans/2026-03-01-capabilities-cleanup-plan.md +658 -0
  47. package/docs/plans/2026-03-01-clean-architecture-plan.md +924 -0
  48. package/docs/plans/2026-03-01-compound-audit-cascade-design.md +191 -0
  49. package/docs/plans/2026-03-01-compound-audit-cascade-plan.md +921 -0
  50. package/docs/plans/2026-03-01-deep-integration-plan.md +851 -0
  51. package/docs/plans/2026-03-01-pipeline-audit-trail-design.md +145 -0
  52. package/docs/plans/2026-03-01-pipeline-audit-trail-plan.md +770 -0
  53. package/docs/plans/2026-03-01-refined-depths-brand-design.md +382 -0
  54. package/docs/plans/2026-03-01-refined-depths-implementation.md +599 -0
  55. package/docs/plans/2026-03-01-skipper-kernel-integration-design.md +203 -0
  56. package/docs/plans/2026-03-01-unified-platform-design.md +272 -0
  57. package/docs/plans/2026-03-07-claude-code-feature-integration-design.md +189 -0
  58. package/docs/plans/2026-03-07-claude-code-feature-integration-plan.md +1165 -0
  59. package/docs/research/BACKLOG_QUICK_REFERENCE.md +352 -0
  60. package/docs/research/CUTTING_EDGE_RESEARCH_2026.md +546 -0
  61. package/docs/research/RESEARCH_INDEX.md +439 -0
  62. package/docs/research/RESEARCH_SOURCES.md +440 -0
  63. package/docs/research/RESEARCH_SUMMARY.txt +275 -0
  64. package/docs/superpowers/specs/2026-03-10-pipeline-quality-revolution-design.md +341 -0
  65. package/package.json +2 -2
  66. package/scripts/lib/adaptive-model.sh +427 -0
  67. package/scripts/lib/adaptive-timeout.sh +316 -0
  68. package/scripts/lib/audit-trail.sh +309 -0
  69. package/scripts/lib/auto-recovery.sh +471 -0
  70. package/scripts/lib/bandit-selector.sh +431 -0
  71. package/scripts/lib/bootstrap.sh +104 -2
  72. package/scripts/lib/causal-graph.sh +455 -0
  73. package/scripts/lib/compat.sh +126 -0
  74. package/scripts/lib/compound-audit.sh +337 -0
  75. package/scripts/lib/constitutional.sh +454 -0
  76. package/scripts/lib/context-budget.sh +359 -0
  77. package/scripts/lib/convergence.sh +594 -0
  78. package/scripts/lib/cost-optimizer.sh +634 -0
  79. package/scripts/lib/daemon-adaptive.sh +14 -2
  80. package/scripts/lib/daemon-dispatch.sh +106 -17
  81. package/scripts/lib/daemon-failure.sh +34 -4
  82. package/scripts/lib/daemon-patrol.sh +25 -4
  83. package/scripts/lib/daemon-poll-github.sh +361 -0
  84. package/scripts/lib/daemon-poll-health.sh +299 -0
  85. package/scripts/lib/daemon-poll.sh +27 -611
  86. package/scripts/lib/daemon-state.sh +119 -66
  87. package/scripts/lib/daemon-triage.sh +10 -0
  88. package/scripts/lib/dod-scorecard.sh +442 -0
  89. package/scripts/lib/error-actionability.sh +300 -0
  90. package/scripts/lib/formal-spec.sh +461 -0
  91. package/scripts/lib/helpers.sh +180 -5
  92. package/scripts/lib/intent-analysis.sh +409 -0
  93. package/scripts/lib/loop-convergence.sh +350 -0
  94. package/scripts/lib/loop-iteration.sh +682 -0
  95. package/scripts/lib/loop-progress.sh +48 -0
  96. package/scripts/lib/loop-restart.sh +185 -0
  97. package/scripts/lib/memory-effectiveness.sh +506 -0
  98. package/scripts/lib/mutation-executor.sh +352 -0
  99. package/scripts/lib/outcome-feedback.sh +521 -0
  100. package/scripts/lib/pipeline-cli.sh +336 -0
  101. package/scripts/lib/pipeline-commands.sh +1216 -0
  102. package/scripts/lib/pipeline-detection.sh +101 -3
  103. package/scripts/lib/pipeline-execution.sh +897 -0
  104. package/scripts/lib/pipeline-github.sh +28 -3
  105. package/scripts/lib/pipeline-intelligence-compound.sh +431 -0
  106. package/scripts/lib/pipeline-intelligence-scoring.sh +407 -0
  107. package/scripts/lib/pipeline-intelligence-skip.sh +181 -0
  108. package/scripts/lib/pipeline-intelligence.sh +104 -1138
  109. package/scripts/lib/pipeline-quality-bash-compat.sh +182 -0
  110. package/scripts/lib/pipeline-quality-checks.sh +17 -711
  111. package/scripts/lib/pipeline-quality-gates.sh +563 -0
  112. package/scripts/lib/pipeline-stages-build.sh +730 -0
  113. package/scripts/lib/pipeline-stages-delivery.sh +965 -0
  114. package/scripts/lib/pipeline-stages-intake.sh +1133 -0
  115. package/scripts/lib/pipeline-stages-monitor.sh +407 -0
  116. package/scripts/lib/pipeline-stages-review.sh +1022 -0
  117. package/scripts/lib/pipeline-stages.sh +161 -2901
  118. package/scripts/lib/pipeline-state.sh +36 -5
  119. package/scripts/lib/pipeline-util.sh +487 -0
  120. package/scripts/lib/policy-learner.sh +438 -0
  121. package/scripts/lib/process-reward.sh +493 -0
  122. package/scripts/lib/project-detect.sh +649 -0
  123. package/scripts/lib/quality-profile.sh +334 -0
  124. package/scripts/lib/recruit-commands.sh +885 -0
  125. package/scripts/lib/recruit-learning.sh +739 -0
  126. package/scripts/lib/recruit-roles.sh +648 -0
  127. package/scripts/lib/reward-aggregator.sh +458 -0
  128. package/scripts/lib/rl-optimizer.sh +362 -0
  129. package/scripts/lib/root-cause.sh +427 -0
  130. package/scripts/lib/scope-enforcement.sh +445 -0
  131. package/scripts/lib/session-restart.sh +493 -0
  132. package/scripts/lib/skill-memory.sh +300 -0
  133. package/scripts/lib/skill-registry.sh +775 -0
  134. package/scripts/lib/spec-driven.sh +476 -0
  135. package/scripts/lib/test-helpers.sh +18 -7
  136. package/scripts/lib/test-holdout.sh +429 -0
  137. package/scripts/lib/test-optimizer.sh +511 -0
  138. package/scripts/shipwright-file-suggest.sh +45 -0
  139. package/scripts/skills/adversarial-quality.md +61 -0
  140. package/scripts/skills/api-design.md +44 -0
  141. package/scripts/skills/architecture-design.md +50 -0
  142. package/scripts/skills/brainstorming.md +43 -0
  143. package/scripts/skills/data-pipeline.md +44 -0
  144. package/scripts/skills/deploy-safety.md +64 -0
  145. package/scripts/skills/documentation.md +38 -0
  146. package/scripts/skills/frontend-design.md +45 -0
  147. package/scripts/skills/generated/.gitkeep +0 -0
  148. package/scripts/skills/generated/_refinements/.gitkeep +0 -0
  149. package/scripts/skills/generated/_refinements/adversarial-quality.patch.md +3 -0
  150. package/scripts/skills/generated/_refinements/architecture-design.patch.md +3 -0
  151. package/scripts/skills/generated/_refinements/brainstorming.patch.md +3 -0
  152. package/scripts/skills/generated/cli-version-management.md +29 -0
  153. package/scripts/skills/generated/collection-system-validation.md +99 -0
  154. package/scripts/skills/generated/large-scale-c-refactoring-coordination.md +97 -0
  155. package/scripts/skills/generated/pattern-matching-similarity-scoring.md +195 -0
  156. package/scripts/skills/generated/test-parallelization-detection.md +65 -0
  157. package/scripts/skills/observability.md +79 -0
  158. package/scripts/skills/performance.md +48 -0
  159. package/scripts/skills/pr-quality.md +49 -0
  160. package/scripts/skills/product-thinking.md +43 -0
  161. package/scripts/skills/security-audit.md +49 -0
  162. package/scripts/skills/systematic-debugging.md +40 -0
  163. package/scripts/skills/testing-strategy.md +47 -0
  164. package/scripts/skills/two-stage-review.md +52 -0
  165. package/scripts/skills/validation-thoroughness.md +55 -0
  166. package/scripts/sw +9 -3
  167. package/scripts/sw-activity.sh +9 -8
  168. package/scripts/sw-adaptive.sh +8 -7
  169. package/scripts/sw-adversarial.sh +2 -1
  170. package/scripts/sw-architecture-enforcer.sh +3 -1
  171. package/scripts/sw-auth.sh +12 -2
  172. package/scripts/sw-autonomous.sh +5 -1
  173. package/scripts/sw-changelog.sh +4 -1
  174. package/scripts/sw-checkpoint.sh +2 -1
  175. package/scripts/sw-ci.sh +15 -6
  176. package/scripts/sw-cleanup.sh +4 -26
  177. package/scripts/sw-code-review.sh +45 -20
  178. package/scripts/sw-connect.sh +2 -1
  179. package/scripts/sw-context.sh +2 -1
  180. package/scripts/sw-cost.sh +107 -5
  181. package/scripts/sw-daemon.sh +71 -11
  182. package/scripts/sw-dashboard.sh +3 -1
  183. package/scripts/sw-db.sh +71 -20
  184. package/scripts/sw-decide.sh +8 -2
  185. package/scripts/sw-decompose.sh +360 -17
  186. package/scripts/sw-deps.sh +4 -1
  187. package/scripts/sw-developer-simulation.sh +4 -1
  188. package/scripts/sw-discovery.sh +378 -5
  189. package/scripts/sw-doc-fleet.sh +4 -1
  190. package/scripts/sw-docs-agent.sh +3 -1
  191. package/scripts/sw-docs.sh +2 -1
  192. package/scripts/sw-doctor.sh +453 -2
  193. package/scripts/sw-dora.sh +4 -1
  194. package/scripts/sw-durable.sh +12 -7
  195. package/scripts/sw-e2e-orchestrator.sh +17 -16
  196. package/scripts/sw-eventbus.sh +13 -4
  197. package/scripts/sw-evidence.sh +364 -12
  198. package/scripts/sw-feedback.sh +550 -9
  199. package/scripts/sw-fix.sh +20 -1
  200. package/scripts/sw-fleet-discover.sh +6 -2
  201. package/scripts/sw-fleet-viz.sh +9 -4
  202. package/scripts/sw-fleet.sh +5 -1
  203. package/scripts/sw-github-app.sh +18 -4
  204. package/scripts/sw-github-checks.sh +3 -2
  205. package/scripts/sw-github-deploy.sh +3 -2
  206. package/scripts/sw-github-graphql.sh +18 -7
  207. package/scripts/sw-guild.sh +5 -1
  208. package/scripts/sw-heartbeat.sh +5 -30
  209. package/scripts/sw-hello.sh +67 -0
  210. package/scripts/sw-hygiene.sh +10 -3
  211. package/scripts/sw-incident.sh +273 -5
  212. package/scripts/sw-init.sh +18 -2
  213. package/scripts/sw-instrument.sh +10 -2
  214. package/scripts/sw-intelligence.sh +44 -7
  215. package/scripts/sw-jira.sh +5 -1
  216. package/scripts/sw-launchd.sh +2 -1
  217. package/scripts/sw-linear.sh +4 -1
  218. package/scripts/sw-logs.sh +4 -1
  219. package/scripts/sw-loop.sh +436 -1076
  220. package/scripts/sw-memory.sh +357 -3
  221. package/scripts/sw-mission-control.sh +6 -1
  222. package/scripts/sw-model-router.sh +483 -27
  223. package/scripts/sw-otel.sh +15 -4
  224. package/scripts/sw-oversight.sh +14 -5
  225. package/scripts/sw-patrol-meta.sh +334 -0
  226. package/scripts/sw-pipeline-composer.sh +7 -1
  227. package/scripts/sw-pipeline-vitals.sh +12 -6
  228. package/scripts/sw-pipeline.sh +54 -2653
  229. package/scripts/sw-pm.sh +16 -8
  230. package/scripts/sw-pr-lifecycle.sh +2 -1
  231. package/scripts/sw-predictive.sh +17 -5
  232. package/scripts/sw-prep.sh +185 -2
  233. package/scripts/sw-ps.sh +5 -25
  234. package/scripts/sw-public-dashboard.sh +17 -4
  235. package/scripts/sw-quality.sh +14 -6
  236. package/scripts/sw-reaper.sh +8 -25
  237. package/scripts/sw-recruit.sh +156 -2303
  238. package/scripts/sw-regression.sh +19 -12
  239. package/scripts/sw-release-manager.sh +3 -1
  240. package/scripts/sw-release.sh +4 -1
  241. package/scripts/sw-remote.sh +3 -1
  242. package/scripts/sw-replay.sh +7 -1
  243. package/scripts/sw-retro.sh +158 -1
  244. package/scripts/sw-review-rerun.sh +3 -1
  245. package/scripts/sw-scale.sh +14 -5
  246. package/scripts/sw-security-audit.sh +6 -1
  247. package/scripts/sw-self-optimize.sh +173 -6
  248. package/scripts/sw-session.sh +9 -3
  249. package/scripts/sw-setup.sh +3 -1
  250. package/scripts/sw-stall-detector.sh +406 -0
  251. package/scripts/sw-standup.sh +15 -7
  252. package/scripts/sw-status.sh +3 -1
  253. package/scripts/sw-strategic.sh +14 -6
  254. package/scripts/sw-stream.sh +13 -4
  255. package/scripts/sw-swarm.sh +20 -7
  256. package/scripts/sw-team-stages.sh +13 -6
  257. package/scripts/sw-templates.sh +7 -31
  258. package/scripts/sw-testgen.sh +17 -6
  259. package/scripts/sw-tmux-pipeline.sh +4 -1
  260. package/scripts/sw-tmux-role-color.sh +2 -0
  261. package/scripts/sw-tmux-status.sh +1 -1
  262. package/scripts/sw-tmux.sh +37 -1
  263. package/scripts/sw-trace.sh +3 -1
  264. package/scripts/sw-tracker-github.sh +3 -0
  265. package/scripts/sw-tracker-jira.sh +3 -0
  266. package/scripts/sw-tracker-linear.sh +3 -0
  267. package/scripts/sw-tracker.sh +3 -1
  268. package/scripts/sw-triage.sh +3 -2
  269. package/scripts/sw-upgrade.sh +3 -1
  270. package/scripts/sw-ux.sh +5 -2
  271. package/scripts/sw-webhook.sh +5 -2
  272. package/scripts/sw-widgets.sh +9 -4
  273. package/scripts/sw-worktree.sh +15 -3
  274. package/scripts/test-skill-injection.sh +1233 -0
  275. package/templates/pipelines/autonomous.json +27 -3
  276. package/templates/pipelines/cost-aware.json +34 -8
  277. package/templates/pipelines/deployed.json +12 -0
  278. package/templates/pipelines/enterprise.json +12 -0
  279. package/templates/pipelines/fast.json +6 -0
  280. package/templates/pipelines/full.json +27 -3
  281. package/templates/pipelines/hotfix.json +6 -0
  282. package/templates/pipelines/standard.json +12 -0
  283. package/templates/pipelines/tdd.json +12 -0
@@ -0,0 +1,461 @@
1
+ #!/usr/bin/env bash
2
+ # Module guard - prevent double-sourcing
3
+ [[ -n "${_FORMAL_SPEC_LOADED:-}" ]] && return 0
4
+ _FORMAL_SPEC_LOADED=1
5
+
6
+ # ╔═══════════════════════════════════════════════════════════════════════════╗
7
+ # ║ shipwright formal-spec — Lightweight Formal Specification System ║
8
+ # ║ Extract pre/post-conditions from docstrings, verify against code, ║
9
+ # ║ inject spec context into pipeline prompts for provable correctness ║
10
+ # ╚═══════════════════════════════════════════════════════════════════════════╝
11
+
12
+ # shellcheck disable=SC2034
13
+ VERSION="3.3.0"
14
+
15
+ # ─── Output Helpers ──────────────────────────────────────────────────────────
16
+ [[ "$(type -t info 2>/dev/null)" == "function" ]] || info() { echo -e "\033[38;2;0;212;255m\033[1m▸\033[0m $*"; }
17
+ [[ "$(type -t success 2>/dev/null)" == "function" ]] || success() { echo -e "\033[38;2;74;222;128m\033[1m✓\033[0m $*"; }
18
+ [[ "$(type -t warn 2>/dev/null)" == "function" ]] || warn() { echo -e "\033[38;2;250;204;21m\033[1m⚠\033[0m $*"; }
19
+ [[ "$(type -t error 2>/dev/null)" == "function" ]] || error() { echo -e "\033[38;2;248;113;113m\033[1m✗\033[0m $*" >&2; }
20
+ if [[ "$(type -t now_iso 2>/dev/null)" != "function" ]]; then
21
+ now_iso() { date -u +"%Y-%m-%dT%H:%M:%SZ"; }
22
+ now_epoch() { date +%s; }
23
+ fi
24
+
25
+ # ─── Configuration ───────────────────────────────────────────────────────────
26
+
27
+ FORMAL_SPECS_FILE="${FORMAL_SPECS_FILE:-.claude/formal-specs.json}"
28
+ FORMAL_SPEC_REPORT="${FORMAL_SPEC_REPORT:-.claude/pipeline-artifacts/formal-spec-report.json}"
29
+
30
+ # ─── Extract Specs ───────────────────────────────────────────────────────────
31
+ # Extract pre/post-conditions and invariants from code comments/docstrings.
32
+ # Input: $1 = file or directory to scan
33
+ # Output: JSON specs written to $FORMAL_SPECS_FILE, path echoed
34
+
35
+ formal_spec_extract() {
36
+ local target="${1:-.}"
37
+ local output_file="${2:-$FORMAL_SPECS_FILE}"
38
+ local tmp_file
39
+ tmp_file=$(mktemp 2>/dev/null || echo "${TMPDIR:-/tmp}/formal-spec-extract.$$.tmp")
40
+
41
+ # Ensure output directory exists
42
+ local out_dir
43
+ out_dir=$(dirname "$output_file")
44
+ mkdir -p "$out_dir" 2>/dev/null || true
45
+
46
+ echo '{"specs":[],"extracted_at":"'"$(now_iso)"'"}' > "$tmp_file"
47
+
48
+ local file_list
49
+ if [[ -d "$target" ]]; then
50
+ file_list=$(find "$target" -type f \( -name "*.js" -o -name "*.ts" -o -name "*.py" -o -name "*.sh" -o -name "*.go" -o -name "*.java" \) 2>/dev/null | head -200)
51
+ elif [[ -f "$target" ]]; then
52
+ file_list="$target"
53
+ else
54
+ echo "$output_file"
55
+ return 0
56
+ fi
57
+
58
+ local specs_json='[]'
59
+
60
+ while IFS= read -r file; do
61
+ [[ -z "$file" || ! -f "$file" ]] && continue
62
+
63
+ local preconditions="" postconditions="" invariants=""
64
+ local func_name=""
65
+
66
+ # Extract JSDoc @precondition, @postcondition, @invariant tags
67
+ preconditions=$(grep -n '@precondition' "$file" 2>/dev/null || true)
68
+ postconditions=$(grep -n '@postcondition' "$file" 2>/dev/null || true)
69
+ invariants=$(grep -n '@invariant' "$file" 2>/dev/null || true)
70
+
71
+ # Extract Python docstring Precondition:, Postcondition:, Invariant: sections
72
+ if [[ -z "$preconditions" ]]; then
73
+ preconditions=$(grep -n 'Precondition:' "$file" 2>/dev/null || true)
74
+ fi
75
+ if [[ -z "$postconditions" ]]; then
76
+ postconditions=$(grep -n 'Postcondition:' "$file" 2>/dev/null || true)
77
+ fi
78
+ if [[ -z "$invariants" ]]; then
79
+ invariants=$(grep -n 'Invariant:' "$file" 2>/dev/null || true)
80
+ fi
81
+
82
+ # Skip files with no specs
83
+ if [[ -z "$preconditions" && -z "$postconditions" && -z "$invariants" ]]; then
84
+ continue
85
+ fi
86
+
87
+ # Build spec entries for this file
88
+ local line_num condition spec_type
89
+
90
+ # Process preconditions
91
+ while IFS= read -r line; do
92
+ [[ -z "$line" ]] && continue
93
+ line_num=$(echo "$line" | cut -d: -f1)
94
+ condition=$(echo "$line" | sed 's/^[0-9]*://' | sed 's/.*@precondition[[:space:]]*//' | sed 's/.*Precondition:[[:space:]]*//' | sed 's/^[[:space:]]*//' | sed 's/[[:space:]]*\*\///')
95
+
96
+ # Find nearest function name (look ahead up to 10 lines)
97
+ func_name=$(_find_function_name "$file" "$line_num")
98
+
99
+ specs_json=$(echo "$specs_json" | jq --arg file "$file" --arg fn "$func_name" \
100
+ --arg cond "$condition" --argjson ln "$line_num" --arg type "precondition" \
101
+ '. + [{"file":$file,"function":$fn,"type":$type,"condition":$cond,"line":$ln}]' 2>/dev/null || echo "$specs_json")
102
+ done <<< "$preconditions"
103
+
104
+ # Process postconditions
105
+ while IFS= read -r line; do
106
+ [[ -z "$line" ]] && continue
107
+ line_num=$(echo "$line" | cut -d: -f1)
108
+ condition=$(echo "$line" | sed 's/^[0-9]*://' | sed 's/.*@postcondition[[:space:]]*//' | sed 's/.*Postcondition:[[:space:]]*//' | sed 's/^[[:space:]]*//' | sed 's/[[:space:]]*\*\///')
109
+
110
+ func_name=$(_find_function_name "$file" "$line_num")
111
+
112
+ specs_json=$(echo "$specs_json" | jq --arg file "$file" --arg fn "$func_name" \
113
+ --arg cond "$condition" --argjson ln "$line_num" --arg type "postcondition" \
114
+ '. + [{"file":$file,"function":$fn,"type":$type,"condition":$cond,"line":$ln}]' 2>/dev/null || echo "$specs_json")
115
+ done <<< "$postconditions"
116
+
117
+ # Process invariants
118
+ while IFS= read -r line; do
119
+ [[ -z "$line" ]] && continue
120
+ line_num=$(echo "$line" | cut -d: -f1)
121
+ condition=$(echo "$line" | sed 's/^[0-9]*://' | sed 's/.*@invariant[[:space:]]*//' | sed 's/.*Invariant:[[:space:]]*//' | sed 's/^[[:space:]]*//' | sed 's/[[:space:]]*\*\///')
122
+
123
+ func_name=$(_find_function_name "$file" "$line_num")
124
+
125
+ specs_json=$(echo "$specs_json" | jq --arg file "$file" --arg fn "$func_name" \
126
+ --arg cond "$condition" --argjson ln "$line_num" --arg type "invariant" \
127
+ '. + [{"file":$file,"function":$fn,"type":$type,"condition":$cond,"line":$ln}]' 2>/dev/null || echo "$specs_json")
128
+ done <<< "$invariants"
129
+
130
+ done <<< "$file_list"
131
+
132
+ # Write final output
133
+ jq -n --argjson specs "$specs_json" --arg ts "$(now_iso)" \
134
+ '{"specs":$specs,"extracted_at":$ts,"count":($specs|length)}' > "$output_file" 2>/dev/null || {
135
+ echo '{"specs":[],"extracted_at":"'"$(now_iso)"'","count":0}' > "$output_file"
136
+ }
137
+
138
+ rm -f "$tmp_file" 2>/dev/null || true
139
+
140
+ if [[ "$(type -t emit_event 2>/dev/null)" == "function" ]]; then
141
+ local spec_count
142
+ spec_count=$(jq -r '.count // 0' "$output_file" 2>/dev/null || echo "0")
143
+ emit_event "formal_spec.extracted" "count=$spec_count" "target=$target" 2>/dev/null || true
144
+ fi
145
+
146
+ echo "$output_file"
147
+ }
148
+
149
+ # Helper: find nearest function name from a line number
150
+ _find_function_name() {
151
+ local file="$1"
152
+ local line_num="$2"
153
+ local search_end=$((line_num + 15))
154
+ local fn_name="unknown"
155
+
156
+ # Look ahead for function declaration
157
+ local snippet
158
+ snippet=$(sed -n "${line_num},${search_end}p" "$file" 2>/dev/null || true)
159
+
160
+ # JS/TS: function name() or const name = or name() {
161
+ local match
162
+ match=$(echo "$snippet" | grep -oE '(function\s+[a-zA-Z_][a-zA-Z0-9_]*|const\s+[a-zA-Z_][a-zA-Z0-9_]*\s*=|[a-zA-Z_][a-zA-Z0-9_]*\s*\()' | head -1 || true)
163
+ if [[ -n "$match" ]]; then
164
+ fn_name=$(echo "$match" | sed 's/function[[:space:]]*//' | sed 's/const[[:space:]]*//' | sed 's/[[:space:]]*=.*//' | sed 's/[[:space:]]*($//')
165
+ fi
166
+
167
+ # Python: def name(
168
+ if [[ "$fn_name" == "unknown" ]]; then
169
+ match=$(echo "$snippet" | grep -oE 'def\s+[a-zA-Z_][a-zA-Z0-9_]*' | head -1 || true)
170
+ if [[ -n "$match" ]]; then
171
+ fn_name=$(echo "$match" | sed 's/def[[:space:]]*//')
172
+ fi
173
+ fi
174
+
175
+ # Bash: name() {
176
+ if [[ "$fn_name" == "unknown" ]]; then
177
+ match=$(echo "$snippet" | grep -oE '^[a-zA-Z_][a-zA-Z0-9_]*\s*\(\)' | head -1 || true)
178
+ if [[ -n "$match" ]]; then
179
+ fn_name=$(echo "$match" | sed 's/[[:space:]]*()$//')
180
+ fi
181
+ fi
182
+
183
+ echo "$fn_name"
184
+ }
185
+
186
+ # ─── Verify Specs ────────────────────────────────────────────────────────────
187
+ # Verify extracted specs against code behavior via grep/pattern matching.
188
+ # Input: $1 = specs file (JSON), $2 = project root
189
+ # Output: compliance report JSON, path echoed
190
+
191
+ formal_spec_verify() {
192
+ local specs_file="${1:-$FORMAL_SPECS_FILE}"
193
+ local project_root="${2:-.}"
194
+ local report_file="${3:-$FORMAL_SPEC_REPORT}"
195
+
196
+ if [[ ! -f "$specs_file" ]]; then
197
+ warn "No specs file found at $specs_file"
198
+ echo "$report_file"
199
+ return 0
200
+ fi
201
+
202
+ local out_dir
203
+ out_dir=$(dirname "$report_file")
204
+ mkdir -p "$out_dir" 2>/dev/null || true
205
+
206
+ local total=0 verified=0 violations=0 unchecked=0
207
+ local violations_json='[]'
208
+
209
+ local spec_count
210
+ spec_count=$(jq -r '.count // 0' "$specs_file" 2>/dev/null || echo "0")
211
+
212
+ if [[ "$spec_count" -eq 0 ]]; then
213
+ jq -n '{"total":0,"verified":0,"violations":0,"unchecked":0,"compliance_pct":100,"details":[],"verified_at":"'"$(now_iso)"'"}' > "$report_file" 2>/dev/null
214
+ echo "$report_file"
215
+ return 0
216
+ fi
217
+
218
+ # Process each spec
219
+ local i=0
220
+ while [[ "$i" -lt "$spec_count" ]]; do
221
+ total=$((total + 1))
222
+ local spec_file spec_fn spec_type spec_cond
223
+ spec_file=$(jq -r ".specs[$i].file // \"\"" "$specs_file" 2>/dev/null || true)
224
+ spec_fn=$(jq -r ".specs[$i].function // \"\"" "$specs_file" 2>/dev/null || true)
225
+ spec_type=$(jq -r ".specs[$i].type // \"\"" "$specs_file" 2>/dev/null || true)
226
+ spec_cond=$(jq -r ".specs[$i].condition // \"\"" "$specs_file" 2>/dev/null || true)
227
+
228
+ if [[ ! -f "$spec_file" ]]; then
229
+ unchecked=$((unchecked + 1))
230
+ i=$((i + 1))
231
+ continue
232
+ fi
233
+
234
+ local status="unchecked"
235
+
236
+ case "$spec_type" in
237
+ precondition)
238
+ # Check if function validates the precondition
239
+ # Look for validation patterns: if (!param), assert, throw, guard clauses
240
+ if _check_precondition "$spec_file" "$spec_fn" "$spec_cond"; then
241
+ status="verified"
242
+ verified=$((verified + 1))
243
+ else
244
+ status="violation"
245
+ violations=$((violations + 1))
246
+ violations_json=$(echo "$violations_json" | jq --arg file "$spec_file" --arg fn "$spec_fn" \
247
+ --arg type "$spec_type" --arg cond "$spec_cond" \
248
+ '. + [{"file":$file,"function":$fn,"type":$type,"condition":$cond,"status":"missing_validation"}]' 2>/dev/null || echo "$violations_json")
249
+ fi
250
+ ;;
251
+ postcondition)
252
+ # Check if function has return value matching expected pattern
253
+ if _check_postcondition "$spec_file" "$spec_fn" "$spec_cond"; then
254
+ status="verified"
255
+ verified=$((verified + 1))
256
+ else
257
+ status="violation"
258
+ violations=$((violations + 1))
259
+ violations_json=$(echo "$violations_json" | jq --arg file "$spec_file" --arg fn "$spec_fn" \
260
+ --arg type "$spec_type" --arg cond "$spec_cond" \
261
+ '. + [{"file":$file,"function":$fn,"type":$type,"condition":$cond,"status":"missing_guarantee"}]' 2>/dev/null || echo "$violations_json")
262
+ fi
263
+ ;;
264
+ invariant)
265
+ # Check for invariant violations in the code
266
+ if _check_invariant "$spec_file" "$spec_fn" "$spec_cond"; then
267
+ status="verified"
268
+ verified=$((verified + 1))
269
+ else
270
+ status="violation"
271
+ violations=$((violations + 1))
272
+ violations_json=$(echo "$violations_json" | jq --arg file "$spec_file" --arg fn "$spec_fn" \
273
+ --arg type "$spec_type" --arg cond "$spec_cond" \
274
+ '. + [{"file":$file,"function":$fn,"type":$type,"condition":$cond,"status":"invariant_broken"}]' 2>/dev/null || echo "$violations_json")
275
+ fi
276
+ ;;
277
+ *)
278
+ unchecked=$((unchecked + 1))
279
+ ;;
280
+ esac
281
+
282
+ i=$((i + 1))
283
+ done
284
+
285
+ # Calculate compliance percentage
286
+ local compliance_pct=100
287
+ if [[ "$total" -gt 0 ]]; then
288
+ compliance_pct=$(( (verified * 100) / total ))
289
+ fi
290
+
291
+ # Write report
292
+ jq -n --argjson total "$total" --argjson verified "$verified" \
293
+ --argjson violations "$violations" --argjson unchecked "$unchecked" \
294
+ --argjson pct "$compliance_pct" --argjson details "$violations_json" \
295
+ --arg ts "$(now_iso)" \
296
+ '{"total":$total,"verified":$verified,"violations":$violations,"unchecked":$unchecked,"compliance_pct":$pct,"details":$details,"verified_at":$ts}' \
297
+ > "$report_file" 2>/dev/null
298
+
299
+ if [[ "$(type -t emit_event 2>/dev/null)" == "function" ]]; then
300
+ emit_event "formal_spec.verified" \
301
+ "total=$total" "verified=$verified" "violations=$violations" \
302
+ "compliance_pct=$compliance_pct" 2>/dev/null || true
303
+ fi
304
+
305
+ echo "$report_file"
306
+ }
307
+
308
+ # ─── Verification Helpers ────────────────────────────────────────────────────
309
+
310
+ _check_precondition() {
311
+ local file="$1" fn="$2" cond="$3"
312
+
313
+ # Extract keywords from condition to search for validation
314
+ local keywords
315
+ keywords=$(echo "$cond" | grep -oE '[a-zA-Z_][a-zA-Z0-9_]*' | head -5 || true)
316
+
317
+ local fn_body
318
+ fn_body=$(_extract_function_body "$file" "$fn")
319
+ [[ -z "$fn_body" ]] && return 1
320
+
321
+ # Look for validation patterns: if, assert, throw, guard, check, validate
322
+ local has_validation=false
323
+ while IFS= read -r kw; do
324
+ [[ -z "$kw" ]] && continue
325
+ if echo "$fn_body" | grep -qE "(if.*${kw}|assert.*${kw}|throw.*${kw}|check.*${kw}|validate.*${kw}|guard.*${kw}|${kw}.*!=.*null|${kw}.*!==.*undefined)" 2>/dev/null; then
326
+ has_validation=true
327
+ break
328
+ fi
329
+ done <<< "$keywords"
330
+
331
+ [[ "$has_validation" == "true" ]]
332
+ }
333
+
334
+ _check_postcondition() {
335
+ local file="$1" fn="$2" cond="$3"
336
+
337
+ local fn_body
338
+ fn_body=$(_extract_function_body "$file" "$fn")
339
+ [[ -z "$fn_body" ]] && return 1
340
+
341
+ # Check that function has a return statement
342
+ if echo "$fn_body" | grep -qE '(return |echo |print\(|yield )' 2>/dev/null; then
343
+ return 0
344
+ fi
345
+
346
+ return 1
347
+ }
348
+
349
+ _check_invariant() {
350
+ local file="$1" fn="$2" cond="$3"
351
+
352
+ # Extract the invariant pattern (e.g., "counter >= 0" -> look for "counter < 0")
353
+ local negated
354
+ negated=$(_negate_condition "$cond")
355
+
356
+ if [[ -n "$negated" ]]; then
357
+ # If we find the negation in the code, invariant is broken
358
+ if grep -qE "$negated" "$file" 2>/dev/null; then
359
+ return 1
360
+ fi
361
+ fi
362
+
363
+ # No violation found = invariant holds
364
+ return 0
365
+ }
366
+
367
+ _negate_condition() {
368
+ local cond="$1"
369
+
370
+ # Simple negation patterns
371
+ if echo "$cond" | grep -qE '>= 0|>= zero|non-negative|not negative' 2>/dev/null; then
372
+ local var
373
+ var=$(echo "$cond" | grep -oE '[a-zA-Z_][a-zA-Z0-9_]*' | head -1 || true)
374
+ [[ -n "$var" ]] && echo "${var}[[:space:]]*<[[:space:]]*0" && return 0
375
+ fi
376
+
377
+ if echo "$cond" | grep -qE 'not null|non-null|!= null|!== null' 2>/dev/null; then
378
+ local var
379
+ var=$(echo "$cond" | grep -oE '[a-zA-Z_][a-zA-Z0-9_]*' | head -1 || true)
380
+ [[ -n "$var" ]] && echo "${var}[[:space:]]*=[[:space:]]*null" && return 0
381
+ fi
382
+
383
+ if echo "$cond" | grep -qE 'not empty|non-empty' 2>/dev/null; then
384
+ local var
385
+ var=$(echo "$cond" | grep -oE '[a-zA-Z_][a-zA-Z0-9_]*' | head -1 || true)
386
+ [[ -n "$var" ]] && echo "${var}[[:space:]]*=[[:space:]]*[\"']{2}" && return 0
387
+ fi
388
+
389
+ echo ""
390
+ }
391
+
392
+ _extract_function_body() {
393
+ local file="$1" fn="$2"
394
+
395
+ [[ -z "$fn" || "$fn" == "unknown" ]] && return 0
396
+
397
+ # Find the function start line and extract ~50 lines
398
+ local start_line
399
+ start_line=$(grep -n -E "(function[[:space:]]+${fn}|${fn}[[:space:]]*\(|def[[:space:]]+${fn}|${fn}[[:space:]]*\(\))" "$file" 2>/dev/null | head -1 | cut -d: -f1 || true)
400
+
401
+ if [[ -n "$start_line" ]]; then
402
+ local end_line=$((start_line + 50))
403
+ sed -n "${start_line},${end_line}p" "$file" 2>/dev/null || true
404
+ fi
405
+ }
406
+
407
+ # ─── Inject Specs ────────────────────────────────────────────────────────────
408
+ # Add formal spec context to pipeline prompts.
409
+ # Input: $1 = specs file, $2 = changed files (newline-separated)
410
+ # Output: prompt text string
411
+
412
+ formal_spec_inject() {
413
+ local specs_file="${1:-$FORMAL_SPECS_FILE}"
414
+ local changed_files="${2:-}"
415
+
416
+ if [[ ! -f "$specs_file" ]]; then
417
+ return 0
418
+ fi
419
+
420
+ local spec_count
421
+ spec_count=$(jq -r '.count // 0' "$specs_file" 2>/dev/null || echo "0")
422
+ [[ "$spec_count" -eq 0 ]] && return 0
423
+
424
+ local prompt_text=""
425
+ prompt_text+="## Formal Specifications"$'\n'
426
+ prompt_text+="The following functions have formal specifications. Ensure your changes maintain these contracts:"$'\n'$'\n'
427
+
428
+ local relevant_specs='[]'
429
+
430
+ if [[ -n "$changed_files" ]]; then
431
+ # Filter specs to only changed files
432
+ while IFS= read -r cf; do
433
+ [[ -z "$cf" ]] && continue
434
+ local file_specs
435
+ file_specs=$(jq --arg f "$cf" '[.specs[] | select(.file == $f)]' "$specs_file" 2>/dev/null || echo "[]")
436
+ if [[ "$file_specs" != "[]" ]]; then
437
+ relevant_specs=$(echo "$relevant_specs" "$file_specs" | jq -s 'add' 2>/dev/null || echo "$relevant_specs")
438
+ fi
439
+ done <<< "$changed_files"
440
+ else
441
+ relevant_specs=$(jq '.specs' "$specs_file" 2>/dev/null || echo "[]")
442
+ fi
443
+
444
+ local rel_count
445
+ rel_count=$(echo "$relevant_specs" | jq 'length' 2>/dev/null || echo "0")
446
+ [[ "$rel_count" -eq 0 ]] && return 0
447
+
448
+ local j=0
449
+ while [[ "$j" -lt "$rel_count" ]]; do
450
+ local s_file s_fn s_type s_cond
451
+ s_file=$(echo "$relevant_specs" | jq -r ".[$j].file // \"\"" 2>/dev/null || true)
452
+ s_fn=$(echo "$relevant_specs" | jq -r ".[$j].function // \"\"" 2>/dev/null || true)
453
+ s_type=$(echo "$relevant_specs" | jq -r ".[$j].type // \"\"" 2>/dev/null || true)
454
+ s_cond=$(echo "$relevant_specs" | jq -r ".[$j].condition // \"\"" 2>/dev/null || true)
455
+
456
+ prompt_text+="- **${s_file}::${s_fn}** — ${s_type}: ${s_cond}"$'\n'
457
+ j=$((j + 1))
458
+ done
459
+
460
+ echo "$prompt_text"
461
+ }
@@ -83,16 +83,24 @@ emit_event() {
83
83
  json_fields="${json_fields},\"${key}\":\"${val}\""
84
84
  fi
85
85
  done
86
- mkdir -p "${HOME}/.shipwright"
86
+ mkdir -p "${HOME}/.shipwright" 2>/dev/null || true
87
87
  local _event_line="{\"ts\":\"$(now_iso)\",\"ts_epoch\":$(now_epoch),\"type\":\"${event_type}\"${json_fields}}"
88
88
  # Use flock to prevent concurrent write corruption
89
- local _lock_file="${EVENTS_FILE}.lock"
89
+ # Lock file uses TMPDIR as fallback so sandbox-restricted envs don't block event logging
90
+ local _lock_file
91
+ if [[ -w "$(dirname "${EVENTS_FILE}")" ]] 2>/dev/null; then
92
+ _lock_file="${EVENTS_FILE}.lock"
93
+ else
94
+ _lock_file="${TMPDIR:-/tmp}/sw-events-$$.lock"
95
+ fi
90
96
  (
91
97
  if command -v flock >/dev/null 2>&1; then
92
- flock -w 2 200 2>/dev/null || true
98
+ if ! flock -w 2 200 2>/dev/null; then
99
+ echo "WARN: emit_event lock timeout — concurrent write possible" >&2
100
+ fi
93
101
  fi
94
102
  echo "$_event_line" >> "$EVENTS_FILE"
95
- ) 200>"$_lock_file"
103
+ ) 200>"$_lock_file" 2>/dev/null || true
96
104
 
97
105
  # Schema validation — auto-detect config repo from BASH_SOURCE location
98
106
  local _schema_dir="${_CONFIG_REPO_DIR:-}"
@@ -175,11 +183,160 @@ rotate_jsonl() {
175
183
  current_lines=$(wc -l < "$file" 2>/dev/null | tr -d ' ')
176
184
  if [[ "$current_lines" -gt "$max_lines" ]]; then
177
185
  local tmp_rotate
178
- tmp_rotate=$(mktemp)
186
+ tmp_rotate=$(mktemp "${TMPDIR:-/tmp}/sw-rotate.XXXXXX") || return 0
179
187
  tail -n "$max_lines" "$file" > "$tmp_rotate" && mv "$tmp_rotate" "$file" || rm -f "$tmp_rotate"
180
188
  fi
181
189
  }
182
190
 
191
+ # ─── Atomic Write Helpers ────────────────────────────────────────
192
+ # atomic_write: Write data to a file atomically (write to tmp, validate, mv)
193
+ # Usage: atomic_write <target_file> <data>
194
+ atomic_write() {
195
+ local target="$1"
196
+ local data="$2"
197
+
198
+ [[ -z "$target" ]] && { error "atomic_write: target file not specified"; return 1; }
199
+
200
+ local tmp
201
+ tmp=$(mktemp "${target}.tmp.XXXXXX") || return 1
202
+
203
+ # Write to tmp file
204
+ echo -n "$data" > "$tmp" || { rm -f "$tmp"; return 1; }
205
+
206
+ # Atomically move into place
207
+ mv "$tmp" "$target" || { rm -f "$tmp"; return 1; }
208
+
209
+ return 0
210
+ }
211
+
212
+ # atomic_append: Append a line to a JSONL file atomically
213
+ # Usage: atomic_append <target_file> <json_line>
214
+ # Thread-safe via flock; validates line before appending
215
+ atomic_append() {
216
+ local target="$1"
217
+ local line="$2"
218
+
219
+ [[ -z "$target" ]] && { error "atomic_append: target file not specified"; return 1; }
220
+ [[ -z "$line" ]] && { error "atomic_append: line not specified"; return 1; }
221
+
222
+ # Validate JSON line
223
+ if ! echo "$line" | jq -e . >/dev/null 2>&1; then
224
+ error "atomic_append: invalid JSON: $line"
225
+ return 1
226
+ fi
227
+
228
+ local tmp lock_file
229
+ tmp=$(mktemp "${target}.tmp.XXXXXX") || return 1
230
+ lock_file="${target}.lock"
231
+
232
+ (
233
+ # Acquire exclusive lock with 5s timeout
234
+ if ! flock -w 5 200 2>/dev/null; then
235
+ error "atomic_append: failed to acquire lock on $target"
236
+ return 1
237
+ fi
238
+
239
+ # Append to tmp file
240
+ echo "$line" > "$tmp" || { rm -f "$tmp"; return 1; }
241
+
242
+ # Append tmp to target (atomic cat)
243
+ cat "$tmp" >> "$target" 2>/dev/null || { rm -f "$tmp"; return 1; }
244
+
245
+ rm -f "$tmp"
246
+ return 0
247
+ ) 200>"$lock_file"
248
+ }
249
+
250
+ # ─── Tmpfile Tracking & Cleanup ──────────────────────────────────
251
+ # Registers a temp file for automatic cleanup on exit
252
+ # Usage: register_tmpfile <tmpfile_path>
253
+ # Set up trap handler: trap '_cleanup_tmpfiles' EXIT
254
+ _REGISTERED_TMPFILES=()
255
+
256
+ register_tmpfile() {
257
+ local tmpfile="$1"
258
+ [[ -z "$tmpfile" ]] && { error "register_tmpfile: path not specified"; return 1; }
259
+ _REGISTERED_TMPFILES+=("$tmpfile")
260
+ }
261
+
262
+ # Cleanup all registered temp files
263
+ _cleanup_tmpfiles() {
264
+ for f in "${_REGISTERED_TMPFILES[@]}"; do
265
+ [[ -f "$f" ]] && rm -f "$f"
266
+ [[ -d "$f" ]] && rm -rf "$f"
267
+ done
268
+ }
269
+
270
+ # ─── Disk Space Check ───────────────────────────────────────────
271
+ # Validates minimum free disk space before critical writes
272
+ # Usage: check_disk_space <path> [min_mb]
273
+ check_disk_space() {
274
+ local target_path="${1:-.}"
275
+ local min_mb="${2:-100}" # Default 100MB minimum
276
+
277
+ # Get available space in KB
278
+ local free_kb
279
+ free_kb=$(df -k "$target_path" 2>/dev/null | tail -1 | awk '{print $4}')
280
+
281
+ if [[ -z "$free_kb" ]] || [[ ! "$free_kb" =~ ^[0-9]+$ ]]; then
282
+ warn "Could not determine free disk space — proceeding anyway"
283
+ return 0
284
+ fi
285
+
286
+ local free_mb=$((free_kb / 1024))
287
+ if [[ "$free_mb" -lt "$min_mb" ]]; then
288
+ error "Insufficient disk space: ${free_mb}MB free, need ${min_mb}MB minimum"
289
+ return 1
290
+ fi
291
+
292
+ return 0
293
+ }
294
+
295
+ # ─── GitHub API Retry Helper ────────────────────────────────────
296
+ # Retries gh CLI calls with exponential backoff on 403 (rate limit)
297
+ # Usage: gh_with_retry <max_attempts> gh issue view <args>
298
+ # Returns: command output on success, empty on failure
299
+ gh_with_retry() {
300
+ local max_attempts="${1:-4}"
301
+ shift
302
+ local attempt=1
303
+ local backoff_secs=30
304
+
305
+ while [[ "$attempt" -le "$max_attempts" ]]; do
306
+ # Execute gh command
307
+ local output result
308
+ output=$("$@" 2>&1)
309
+ result=$?
310
+
311
+ # Success
312
+ if [[ "$result" -eq 0 ]]; then
313
+ echo "$output"
314
+ return 0
315
+ fi
316
+
317
+ # Check for rate limit (403) or API error
318
+ if echo "$output" | grep -qE "HTTP 403|API rate limit|rate limited|You have exceeded"; then
319
+ if [[ "$attempt" -lt "$max_attempts" ]]; then
320
+ warn "GitHub API rate limit detected — backing off ${backoff_secs}s (attempt $attempt/$max_attempts)"
321
+ emit_event "github.rate_limited" "attempt=$attempt" "backoff=$backoff_secs"
322
+ sleep "$backoff_secs"
323
+ backoff_secs=$((backoff_secs * 2))
324
+ [[ "$backoff_secs" -gt 300 ]] && backoff_secs=300
325
+ fi
326
+ else
327
+ # Non-rate-limit error — fail immediately
328
+ return "$result"
329
+ fi
330
+
331
+ attempt=$((attempt + 1))
332
+ done
333
+
334
+ # Exhausted all retries
335
+ error "GitHub API call failed after $max_attempts attempts: ${output##*$'\n'}"
336
+ emit_event "github.api_failed" "attempts=$max_attempts"
337
+ return 1
338
+ }
339
+
183
340
  # ─── Project Identity ────────────────────────────────────────────
184
341
  # Auto-detect GitHub owner/repo from git remote, with fallbacks
185
342
  _sw_github_repo() {
@@ -210,3 +367,21 @@ _sw_github_url() {
210
367
  echo "https://github.com/${repo}"
211
368
  }
212
369
 
370
+ # ─── Secret Sanitization ─────────────────────────────────────────────
371
+ # Redacts sensitive data from strings before logging
372
+ # Redacts: ANTHROPIC_API_KEY, GITHUB_TOKEN, sk-* patterns, Bearer tokens
373
+ sanitize_secrets() {
374
+ local text="$1"
375
+ # Redact ANTHROPIC_API_KEY=... (until whitespace or quote)
376
+ text="$(echo "$text" | sed 's/ANTHROPIC_API_KEY=[^ "]*\|ANTHROPIC_API_KEY=[^ ]*/ANTHROPIC_API_KEY=***REDACTED***/g')"
377
+ # Redact GITHUB_TOKEN=... (until whitespace or quote)
378
+ text="$(echo "$text" | sed 's/GITHUB_TOKEN=[^ "]*\|GITHUB_TOKEN=[^ ]*/GITHUB_TOKEN=***REDACTED***/g')"
379
+ # Redact sk-* patterns (Anthropic API key format)
380
+ text="$(echo "$text" | sed 's/sk-[a-zA-Z0-9_-]*/sk-***REDACTED***/g')"
381
+ # Redact Bearer tokens
382
+ text="$(echo "$text" | sed 's/Bearer [a-zA-Z0-9_.-]*/Bearer ***REDACTED***/g')"
383
+ # Redact oauth tokens (gh_...)
384
+ text="$(echo "$text" | sed 's/gh_[a-zA-Z0-9_]*/gh_***REDACTED***/g')"
385
+ echo "$text"
386
+ }
387
+