shipwright-cli 2.3.1 → 3.0.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.
- package/README.md +95 -28
- package/completions/_shipwright +1 -1
- package/completions/shipwright.bash +3 -8
- package/completions/shipwright.fish +1 -1
- package/config/defaults.json +111 -0
- package/config/event-schema.json +81 -0
- package/config/policy.json +155 -2
- package/config/policy.schema.json +162 -1
- package/dashboard/coverage/coverage-summary.json +14 -0
- package/dashboard/public/index.html +1 -1
- package/dashboard/server.ts +306 -17
- package/dashboard/src/components/charts/bar.test.ts +79 -0
- package/dashboard/src/components/charts/donut.test.ts +68 -0
- package/dashboard/src/components/charts/pipeline-rail.test.ts +117 -0
- package/dashboard/src/components/charts/sparkline.test.ts +125 -0
- package/dashboard/src/core/api.test.ts +309 -0
- package/dashboard/src/core/helpers.test.ts +301 -0
- package/dashboard/src/core/router.test.ts +307 -0
- package/dashboard/src/core/router.ts +7 -0
- package/dashboard/src/core/sse.test.ts +144 -0
- package/dashboard/src/views/metrics.test.ts +186 -0
- package/dashboard/src/views/overview.test.ts +173 -0
- package/dashboard/src/views/pipelines.test.ts +183 -0
- package/dashboard/src/views/team.test.ts +253 -0
- package/dashboard/vitest.config.ts +14 -5
- package/docs/TIPS.md +1 -1
- package/docs/patterns/README.md +1 -1
- package/package.json +15 -5
- package/scripts/adapters/docker-deploy.sh +1 -1
- package/scripts/adapters/tmux-adapter.sh +11 -1
- package/scripts/adapters/wezterm-adapter.sh +1 -1
- package/scripts/check-version-consistency.sh +1 -1
- package/scripts/lib/architecture.sh +126 -0
- package/scripts/lib/bootstrap.sh +75 -0
- package/scripts/lib/compat.sh +89 -6
- package/scripts/lib/config.sh +91 -0
- package/scripts/lib/daemon-adaptive.sh +3 -3
- package/scripts/lib/daemon-dispatch.sh +39 -16
- package/scripts/lib/daemon-health.sh +1 -1
- package/scripts/lib/daemon-patrol.sh +24 -12
- package/scripts/lib/daemon-poll.sh +37 -25
- package/scripts/lib/daemon-state.sh +115 -23
- package/scripts/lib/daemon-triage.sh +30 -8
- package/scripts/lib/fleet-failover.sh +63 -0
- package/scripts/lib/helpers.sh +30 -6
- package/scripts/lib/pipeline-detection.sh +2 -2
- package/scripts/lib/pipeline-github.sh +9 -9
- package/scripts/lib/pipeline-intelligence.sh +85 -35
- package/scripts/lib/pipeline-quality-checks.sh +16 -16
- package/scripts/lib/pipeline-quality.sh +1 -1
- package/scripts/lib/pipeline-stages.sh +242 -28
- package/scripts/lib/pipeline-state.sh +40 -4
- package/scripts/lib/test-helpers.sh +247 -0
- package/scripts/postinstall.mjs +3 -11
- package/scripts/sw +10 -4
- package/scripts/sw-activity.sh +1 -11
- package/scripts/sw-adaptive.sh +109 -85
- package/scripts/sw-adversarial.sh +4 -14
- package/scripts/sw-architecture-enforcer.sh +1 -11
- package/scripts/sw-auth.sh +8 -17
- package/scripts/sw-autonomous.sh +111 -49
- package/scripts/sw-changelog.sh +1 -11
- package/scripts/sw-checkpoint.sh +144 -20
- package/scripts/sw-ci.sh +2 -12
- package/scripts/sw-cleanup.sh +13 -17
- package/scripts/sw-code-review.sh +16 -36
- package/scripts/sw-connect.sh +5 -12
- package/scripts/sw-context.sh +9 -26
- package/scripts/sw-cost.sh +6 -16
- package/scripts/sw-daemon.sh +75 -70
- package/scripts/sw-dashboard.sh +57 -17
- package/scripts/sw-db.sh +506 -15
- package/scripts/sw-decompose.sh +1 -11
- package/scripts/sw-deps.sh +15 -25
- package/scripts/sw-developer-simulation.sh +1 -11
- package/scripts/sw-discovery.sh +112 -30
- package/scripts/sw-doc-fleet.sh +7 -17
- package/scripts/sw-docs-agent.sh +6 -16
- package/scripts/sw-docs.sh +4 -12
- package/scripts/sw-doctor.sh +134 -43
- package/scripts/sw-dora.sh +11 -19
- package/scripts/sw-durable.sh +35 -52
- package/scripts/sw-e2e-orchestrator.sh +11 -27
- package/scripts/sw-eventbus.sh +115 -115
- package/scripts/sw-evidence.sh +748 -0
- package/scripts/sw-feedback.sh +3 -13
- package/scripts/sw-fix.sh +2 -20
- package/scripts/sw-fleet-discover.sh +1 -11
- package/scripts/sw-fleet-viz.sh +10 -18
- package/scripts/sw-fleet.sh +13 -17
- package/scripts/sw-github-app.sh +6 -16
- package/scripts/sw-github-checks.sh +1 -11
- package/scripts/sw-github-deploy.sh +1 -11
- package/scripts/sw-github-graphql.sh +2 -12
- package/scripts/sw-guild.sh +1 -11
- package/scripts/sw-heartbeat.sh +49 -12
- package/scripts/sw-hygiene.sh +45 -43
- package/scripts/sw-incident.sh +284 -67
- package/scripts/sw-init.sh +35 -37
- package/scripts/sw-instrument.sh +1 -11
- package/scripts/sw-intelligence.sh +362 -51
- package/scripts/sw-jira.sh +5 -14
- package/scripts/sw-launchd.sh +2 -12
- package/scripts/sw-linear.sh +8 -17
- package/scripts/sw-logs.sh +4 -12
- package/scripts/sw-loop.sh +641 -90
- package/scripts/sw-memory.sh +243 -17
- package/scripts/sw-mission-control.sh +2 -12
- package/scripts/sw-model-router.sh +73 -34
- package/scripts/sw-otel.sh +11 -21
- package/scripts/sw-oversight.sh +1 -11
- package/scripts/sw-patrol-meta.sh +5 -11
- package/scripts/sw-pipeline-composer.sh +7 -17
- package/scripts/sw-pipeline-vitals.sh +1 -11
- package/scripts/sw-pipeline.sh +478 -122
- package/scripts/sw-pm.sh +2 -12
- package/scripts/sw-pr-lifecycle.sh +203 -29
- package/scripts/sw-predictive.sh +16 -22
- package/scripts/sw-prep.sh +6 -16
- package/scripts/sw-ps.sh +1 -11
- package/scripts/sw-public-dashboard.sh +2 -12
- package/scripts/sw-quality.sh +77 -10
- package/scripts/sw-reaper.sh +1 -11
- package/scripts/sw-recruit.sh +15 -25
- package/scripts/sw-regression.sh +11 -21
- package/scripts/sw-release-manager.sh +19 -28
- package/scripts/sw-release.sh +8 -16
- package/scripts/sw-remote.sh +1 -11
- package/scripts/sw-replay.sh +48 -44
- package/scripts/sw-retro.sh +70 -92
- package/scripts/sw-review-rerun.sh +220 -0
- package/scripts/sw-scale.sh +109 -32
- package/scripts/sw-security-audit.sh +12 -22
- package/scripts/sw-self-optimize.sh +239 -23
- package/scripts/sw-session.sh +3 -13
- package/scripts/sw-setup.sh +8 -18
- package/scripts/sw-standup.sh +5 -15
- package/scripts/sw-status.sh +32 -23
- package/scripts/sw-strategic.sh +129 -13
- package/scripts/sw-stream.sh +1 -11
- package/scripts/sw-swarm.sh +76 -36
- package/scripts/sw-team-stages.sh +10 -20
- package/scripts/sw-templates.sh +4 -14
- package/scripts/sw-testgen.sh +3 -13
- package/scripts/sw-tmux-pipeline.sh +1 -19
- package/scripts/sw-tmux-role-color.sh +0 -10
- package/scripts/sw-tmux-status.sh +3 -11
- package/scripts/sw-tmux.sh +2 -20
- package/scripts/sw-trace.sh +1 -19
- package/scripts/sw-tracker-github.sh +0 -10
- package/scripts/sw-tracker-jira.sh +1 -11
- package/scripts/sw-tracker-linear.sh +1 -11
- package/scripts/sw-tracker.sh +7 -24
- package/scripts/sw-triage.sh +24 -34
- package/scripts/sw-upgrade.sh +5 -23
- package/scripts/sw-ux.sh +1 -19
- package/scripts/sw-webhook.sh +18 -32
- package/scripts/sw-widgets.sh +3 -21
- package/scripts/sw-worktree.sh +11 -27
- package/scripts/update-homebrew-sha.sh +67 -0
- package/templates/pipelines/tdd.json +72 -0
- package/scripts/sw-pipeline.sh.mock +0 -7
package/scripts/sw-pm.sh
CHANGED
|
@@ -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="
|
|
9
|
+
VERSION="3.0.0"
|
|
10
10
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
11
11
|
|
|
12
12
|
# ─── Cross-platform compatibility ──────────────────────────────────────────
|
|
@@ -32,16 +32,6 @@ if [[ "$(type -t emit_event 2>/dev/null)" != "function" ]]; then
|
|
|
32
32
|
echo "${payload}}" >> "${HOME}/.shipwright/events.jsonl"
|
|
33
33
|
}
|
|
34
34
|
fi
|
|
35
|
-
CYAN="${CYAN:-\033[38;2;0;212;255m}"
|
|
36
|
-
PURPLE="${PURPLE:-\033[38;2;124;58;237m}"
|
|
37
|
-
BLUE="${BLUE:-\033[38;2;0;102;255m}"
|
|
38
|
-
GREEN="${GREEN:-\033[38;2;74;222;128m}"
|
|
39
|
-
YELLOW="${YELLOW:-\033[38;2;250;204;21m}"
|
|
40
|
-
RED="${RED:-\033[38;2;248;113;113m}"
|
|
41
|
-
DIM="${DIM:-\033[2m}"
|
|
42
|
-
BOLD="${BOLD:-\033[1m}"
|
|
43
|
-
RESET="${RESET:-\033[0m}"
|
|
44
|
-
|
|
45
35
|
# ─── PM History Storage ──────────────────────────────────────────────────────
|
|
46
36
|
PM_HISTORY="${HOME}/.shipwright/pm-history.json"
|
|
47
37
|
|
|
@@ -224,7 +214,7 @@ recommend_team() {
|
|
|
224
214
|
if [[ -n "$issue_title" ]]; then
|
|
225
215
|
local recruit_result
|
|
226
216
|
recruit_result=$(bash "$SCRIPT_DIR/sw-recruit.sh" team --json "$issue_title" 2>/dev/null) || true
|
|
227
|
-
if [[ -n "$recruit_result" ]] && echo "$recruit_result" | jq -e '.team'
|
|
217
|
+
if [[ -n "$recruit_result" ]] && echo "$recruit_result" | jq -e '.team' >/dev/null 2>&1; then
|
|
228
218
|
local recruit_roles recruit_model recruit_agents recruit_cost
|
|
229
219
|
recruit_roles=$(echo "$recruit_result" | jq -r '.team | join(",")')
|
|
230
220
|
recruit_model=$(echo "$recruit_result" | jq -r '.model // "sonnet"')
|
|
@@ -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="
|
|
9
|
+
VERSION="3.0.0"
|
|
10
10
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
11
11
|
REPO_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|
12
12
|
|
|
@@ -26,24 +26,6 @@ if [[ "$(type -t now_iso 2>/dev/null)" != "function" ]]; then
|
|
|
26
26
|
now_iso() { date -u +"%Y-%m-%dT%H:%M:%SZ"; }
|
|
27
27
|
now_epoch() { date +%s; }
|
|
28
28
|
fi
|
|
29
|
-
if [[ "$(type -t emit_event 2>/dev/null)" != "function" ]]; then
|
|
30
|
-
emit_event() {
|
|
31
|
-
local event_type="$1"; shift; mkdir -p "${HOME}/.shipwright"
|
|
32
|
-
local payload="{\"ts\":\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\",\"type\":\"$event_type\""
|
|
33
|
-
while [[ $# -gt 0 ]]; do local key="${1%%=*}" val="${1#*=}"; payload="${payload},\"${key}\":\"${val}\""; shift; done
|
|
34
|
-
echo "${payload}}" >> "${HOME}/.shipwright/events.jsonl"
|
|
35
|
-
}
|
|
36
|
-
fi
|
|
37
|
-
CYAN="${CYAN:-\033[38;2;0;212;255m}"
|
|
38
|
-
PURPLE="${PURPLE:-\033[38;2;124;58;237m}"
|
|
39
|
-
BLUE="${BLUE:-\033[38;2;0;102;255m}"
|
|
40
|
-
GREEN="${GREEN:-\033[38;2;74;222;128m}"
|
|
41
|
-
YELLOW="${YELLOW:-\033[38;2;250;204;21m}"
|
|
42
|
-
RED="${RED:-\033[38;2;248;113;113m}"
|
|
43
|
-
DIM="${DIM:-\033[2m}"
|
|
44
|
-
BOLD="${BOLD:-\033[1m}"
|
|
45
|
-
RESET="${RESET:-\033[0m}"
|
|
46
|
-
|
|
47
29
|
# ─── Configuration Helpers ──────────────────────────────────────────────────
|
|
48
30
|
|
|
49
31
|
get_pr_config() {
|
|
@@ -56,13 +38,38 @@ get_pr_config() {
|
|
|
56
38
|
|
|
57
39
|
get_pr_info() {
|
|
58
40
|
local pr_number="$1"
|
|
59
|
-
gh pr view "$pr_number" --json number,title,body,state,headRefName,baseRefName,statusCheckRollup,reviews,commits,createdAt,updatedAt 2>/dev/null || return 1
|
|
41
|
+
gh pr view "$pr_number" --json number,title,body,state,headRefName,baseRefName,statusCheckRollup,reviews,commits,createdAt,updatedAt,headRefOid 2>/dev/null || return 1
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
get_pr_head_sha() {
|
|
45
|
+
local pr_number="$1"
|
|
46
|
+
gh pr view "$pr_number" --json headRefOid --jq '.headRefOid' 2>/dev/null || return 1
|
|
60
47
|
}
|
|
61
48
|
|
|
62
49
|
get_pr_checks_status() {
|
|
63
50
|
local pr_number="$1"
|
|
64
51
|
# Returns: success, failure, pending, or unknown
|
|
65
|
-
gh pr checks
|
|
52
|
+
# gh pr checks requires --json flag to produce JSON output
|
|
53
|
+
local checks_json
|
|
54
|
+
checks_json=$(gh pr checks "$pr_number" --json name,state,conclusion 2>/dev/null || echo "[]")
|
|
55
|
+
|
|
56
|
+
# Handle empty or non-JSON response
|
|
57
|
+
if [[ -z "$checks_json" ]] || ! echo "$checks_json" | jq empty 2>/dev/null; then
|
|
58
|
+
echo "unknown"
|
|
59
|
+
return
|
|
60
|
+
fi
|
|
61
|
+
|
|
62
|
+
local total failed pending
|
|
63
|
+
total=$(echo "$checks_json" | jq 'length' 2>/dev/null || echo "0")
|
|
64
|
+
[[ "$total" -eq 0 ]] && { echo "unknown"; return; }
|
|
65
|
+
|
|
66
|
+
failed=$(echo "$checks_json" | jq '[.[] | select(.conclusion == "FAILURE" or .conclusion == "failure")] | length' 2>/dev/null || echo "0")
|
|
67
|
+
[[ "$failed" -gt 0 ]] && { echo "failure"; return; }
|
|
68
|
+
|
|
69
|
+
pending=$(echo "$checks_json" | jq '[.[] | select(.state == "PENDING" or .state == "QUEUED" or .state == "IN_PROGRESS")] | length' 2>/dev/null || echo "0")
|
|
70
|
+
[[ "$pending" -gt 0 ]] && { echo "pending"; return; }
|
|
71
|
+
|
|
72
|
+
echo "success"
|
|
66
73
|
}
|
|
67
74
|
|
|
68
75
|
has_merge_conflicts() {
|
|
@@ -95,6 +102,144 @@ get_pr_originating_issue() {
|
|
|
95
102
|
echo "$body" | grep -oiE '(closes|fixes|resolves) #[0-9]+' | grep -oE '[0-9]+' | head -1
|
|
96
103
|
}
|
|
97
104
|
|
|
105
|
+
# ─── Current-Head SHA Discipline ─────────────────────────────────────────────
|
|
106
|
+
# All check results and review approvals MUST correspond to the current PR head
|
|
107
|
+
# SHA. Stale evidence from older commits is never trusted. This is the single
|
|
108
|
+
# most important safety invariant in the Code Factory pattern.
|
|
109
|
+
|
|
110
|
+
validate_checks_for_head_sha() {
|
|
111
|
+
local pr_number="$1"
|
|
112
|
+
local head_sha="$2"
|
|
113
|
+
|
|
114
|
+
if [[ -z "$head_sha" ]]; then
|
|
115
|
+
error "No head SHA provided — cannot validate check freshness"
|
|
116
|
+
return 1
|
|
117
|
+
fi
|
|
118
|
+
|
|
119
|
+
local short_sha="${head_sha:0:7}"
|
|
120
|
+
|
|
121
|
+
# Get check runs for the current head SHA
|
|
122
|
+
local owner_repo
|
|
123
|
+
owner_repo=$(gh repo view --json nameWithOwner --jq '.nameWithOwner' 2>/dev/null || echo "")
|
|
124
|
+
if [[ -z "$owner_repo" ]]; then
|
|
125
|
+
warn "Could not detect repo — skipping SHA discipline check"
|
|
126
|
+
return 0
|
|
127
|
+
fi
|
|
128
|
+
|
|
129
|
+
local check_runs
|
|
130
|
+
check_runs=$(gh api "repos/${owner_repo}/commits/${head_sha}/check-runs" --jq '.check_runs' 2>/dev/null || echo "[]")
|
|
131
|
+
|
|
132
|
+
local total_checks
|
|
133
|
+
total_checks=$(echo "$check_runs" | jq 'length' 2>/dev/null || echo "0")
|
|
134
|
+
|
|
135
|
+
if [[ "$total_checks" -eq 0 ]]; then
|
|
136
|
+
warn "No check runs found for head SHA ${short_sha}"
|
|
137
|
+
return 0
|
|
138
|
+
fi
|
|
139
|
+
|
|
140
|
+
local failed_checks
|
|
141
|
+
failed_checks=$(echo "$check_runs" | jq '[.[] | select(.conclusion == "failure" or .conclusion == "cancelled")] | length' 2>/dev/null || echo "0")
|
|
142
|
+
|
|
143
|
+
local pending_checks
|
|
144
|
+
pending_checks=$(echo "$check_runs" | jq '[.[] | select(.status != "completed")] | length' 2>/dev/null || echo "0")
|
|
145
|
+
|
|
146
|
+
if [[ "$failed_checks" -gt 0 ]]; then
|
|
147
|
+
error "PR #${pr_number} has ${failed_checks} failed check(s) on current head ${short_sha}"
|
|
148
|
+
return 1
|
|
149
|
+
fi
|
|
150
|
+
|
|
151
|
+
if [[ "$pending_checks" -gt 0 ]]; then
|
|
152
|
+
warn "PR #${pr_number} has ${pending_checks} pending check(s) on head ${short_sha}"
|
|
153
|
+
return 1
|
|
154
|
+
fi
|
|
155
|
+
|
|
156
|
+
info "All ${total_checks} checks passed for current head SHA ${short_sha}"
|
|
157
|
+
return 0
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
validate_reviews_for_head_sha() {
|
|
161
|
+
local pr_number="$1"
|
|
162
|
+
local head_sha="$2"
|
|
163
|
+
|
|
164
|
+
if [[ -z "$head_sha" ]]; then
|
|
165
|
+
return 0
|
|
166
|
+
fi
|
|
167
|
+
|
|
168
|
+
local short_sha="${head_sha:0:7}"
|
|
169
|
+
|
|
170
|
+
# Get reviews and check they're not stale (submitted before the latest push)
|
|
171
|
+
local reviews_json
|
|
172
|
+
reviews_json=$(gh pr view "$pr_number" --json reviews --jq '.reviews' 2>/dev/null || echo "[]")
|
|
173
|
+
|
|
174
|
+
local latest_commit_date
|
|
175
|
+
latest_commit_date=$(gh pr view "$pr_number" --json commits --jq '.commits[-1].committedDate' 2>/dev/null || echo "")
|
|
176
|
+
|
|
177
|
+
if [[ -z "$latest_commit_date" ]]; then
|
|
178
|
+
return 0
|
|
179
|
+
fi
|
|
180
|
+
|
|
181
|
+
# Check if any approvals are stale (submitted before last commit)
|
|
182
|
+
local stale_approvals
|
|
183
|
+
stale_approvals=$(echo "$reviews_json" | jq --arg cutoff "$latest_commit_date" \
|
|
184
|
+
'[.[] | select(.state == "APPROVED" and .submittedAt < $cutoff)] | length' 2>/dev/null || echo "0")
|
|
185
|
+
|
|
186
|
+
if [[ "$stale_approvals" -gt 0 ]]; then
|
|
187
|
+
warn "PR #${pr_number} has ${stale_approvals} stale approval(s) from before head ${short_sha} — reviews should be refreshed"
|
|
188
|
+
fi
|
|
189
|
+
|
|
190
|
+
return 0
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
compute_risk_tier_for_pr() {
|
|
194
|
+
local pr_number="$1"
|
|
195
|
+
local policy_file="${REPO_DIR}/config/policy.json"
|
|
196
|
+
|
|
197
|
+
if [[ ! -f "$policy_file" ]]; then
|
|
198
|
+
echo "medium"
|
|
199
|
+
return
|
|
200
|
+
fi
|
|
201
|
+
|
|
202
|
+
local changed_files
|
|
203
|
+
changed_files=$(gh pr diff "$pr_number" --name-only 2>/dev/null || echo "")
|
|
204
|
+
|
|
205
|
+
if [[ -z "$changed_files" ]]; then
|
|
206
|
+
echo "low"
|
|
207
|
+
return
|
|
208
|
+
fi
|
|
209
|
+
|
|
210
|
+
local tier="low"
|
|
211
|
+
|
|
212
|
+
check_tier_match() {
|
|
213
|
+
local check_tier="$1"
|
|
214
|
+
local patterns
|
|
215
|
+
patterns=$(jq -r ".riskTierRules.${check_tier}[]? // empty" "$policy_file" 2>/dev/null)
|
|
216
|
+
[[ -z "$patterns" ]] && return 1
|
|
217
|
+
|
|
218
|
+
while IFS= read -r pattern; do
|
|
219
|
+
[[ -z "$pattern" ]] && continue
|
|
220
|
+
local regex
|
|
221
|
+
regex=$(echo "$pattern" | sed 's/\./\\./g; s/\*\*/DOUBLESTAR/g; s/\*/[^\/]*/g; s/DOUBLESTAR/.*/g')
|
|
222
|
+
while IFS= read -r file; do
|
|
223
|
+
[[ -z "$file" ]] && continue
|
|
224
|
+
if echo "$file" | grep -qE "^${regex}$"; then
|
|
225
|
+
return 0
|
|
226
|
+
fi
|
|
227
|
+
done <<< "$changed_files"
|
|
228
|
+
done <<< "$patterns"
|
|
229
|
+
return 1
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if check_tier_match "critical"; then
|
|
233
|
+
tier="critical"
|
|
234
|
+
elif check_tier_match "high"; then
|
|
235
|
+
tier="high"
|
|
236
|
+
elif check_tier_match "medium"; then
|
|
237
|
+
tier="medium"
|
|
238
|
+
fi
|
|
239
|
+
|
|
240
|
+
echo "$tier"
|
|
241
|
+
}
|
|
242
|
+
|
|
98
243
|
# ─── Review Pass ────────────────────────────────────────────────────────────
|
|
99
244
|
|
|
100
245
|
pr_review() {
|
|
@@ -144,25 +289,25 @@ pr_review() {
|
|
|
144
289
|
if echo "$diff_output" | grep -qE '(HACK|TODO|FIXME|XXX|BROKEN|DEBUG)'; then
|
|
145
290
|
warnings="${warnings}
|
|
146
291
|
- Found HACK/TODO/FIXME markers in code"
|
|
147
|
-
((issues_found
|
|
292
|
+
issues_found=$((issues_found + 1))
|
|
148
293
|
fi
|
|
149
294
|
|
|
150
295
|
if echo "$diff_output" | grep -qE 'console\.(log|warn|error)\('; then
|
|
151
296
|
warnings="${warnings}
|
|
152
297
|
- Found console.log statements (should use proper logging)"
|
|
153
|
-
((issues_found
|
|
298
|
+
issues_found=$((issues_found + 1))
|
|
154
299
|
fi
|
|
155
300
|
|
|
156
301
|
if [[ $line_additions -gt 500 ]]; then
|
|
157
302
|
warnings="${warnings}
|
|
158
303
|
- Large addition (${line_additions} lines) — consider splitting into smaller PRs"
|
|
159
|
-
((issues_found
|
|
304
|
+
issues_found=$((issues_found + 1))
|
|
160
305
|
fi
|
|
161
306
|
|
|
162
307
|
if [[ $file_count -gt 20 ]]; then
|
|
163
308
|
warnings="${warnings}
|
|
164
309
|
- Many files changed (${file_count}) — consider splitting"
|
|
165
|
-
((issues_found
|
|
310
|
+
issues_found=$((issues_found + 1))
|
|
166
311
|
fi
|
|
167
312
|
|
|
168
313
|
# Post review comment to PR
|
|
@@ -220,6 +365,35 @@ pr_merge() {
|
|
|
220
365
|
return 1
|
|
221
366
|
fi
|
|
222
367
|
|
|
368
|
+
# ── Current-head SHA discipline ──────────────────────────────────────────
|
|
369
|
+
# All evidence (checks, reviews) must be validated against the current head.
|
|
370
|
+
# Never merge on stale evidence from an older commit.
|
|
371
|
+
local head_sha
|
|
372
|
+
head_sha=$(echo "$pr_info" | jq -r '.headRefOid // empty' 2>/dev/null)
|
|
373
|
+
if [[ -z "$head_sha" ]]; then
|
|
374
|
+
head_sha=$(get_pr_head_sha "$pr_number")
|
|
375
|
+
fi
|
|
376
|
+
|
|
377
|
+
if [[ -n "$head_sha" ]]; then
|
|
378
|
+
local short_sha="${head_sha:0:7}"
|
|
379
|
+
info "Validating evidence for current head SHA: ${short_sha}"
|
|
380
|
+
|
|
381
|
+
if ! validate_checks_for_head_sha "$pr_number" "$head_sha"; then
|
|
382
|
+
error "PR #${pr_number} blocked — checks not passing for current head ${short_sha}"
|
|
383
|
+
emit_event "pr.merge_failed" "pr=${pr_number}" "reason=stale_checks" "head_sha=${short_sha}"
|
|
384
|
+
return 1
|
|
385
|
+
fi
|
|
386
|
+
|
|
387
|
+
validate_reviews_for_head_sha "$pr_number" "$head_sha"
|
|
388
|
+
else
|
|
389
|
+
warn "Could not determine head SHA — falling back to legacy check"
|
|
390
|
+
fi
|
|
391
|
+
|
|
392
|
+
# ── Risk tier enforcement ────────────────────────────────────────────────
|
|
393
|
+
local risk_tier
|
|
394
|
+
risk_tier=$(compute_risk_tier_for_pr "$pr_number")
|
|
395
|
+
info "Risk tier: ${risk_tier}"
|
|
396
|
+
|
|
223
397
|
# Check for merge conflicts
|
|
224
398
|
if has_merge_conflicts "$pr_number"; then
|
|
225
399
|
error "PR #${pr_number} has merge conflicts — manual intervention required"
|
|
@@ -227,7 +401,7 @@ pr_merge() {
|
|
|
227
401
|
return 1
|
|
228
402
|
fi
|
|
229
403
|
|
|
230
|
-
# Check CI status
|
|
404
|
+
# Check CI status (legacy check, supplementary to SHA-based validation)
|
|
231
405
|
local status_check_rollup
|
|
232
406
|
status_check_rollup=$(echo "$pr_info" | jq -r '.statusCheckRollup[].state' 2>/dev/null | sort | uniq)
|
|
233
407
|
if [[ -z "$status_check_rollup" ]] || echo "$status_check_rollup" | grep -qi "failure\|error"; then
|
|
@@ -246,10 +420,10 @@ pr_merge() {
|
|
|
246
420
|
fi
|
|
247
421
|
|
|
248
422
|
# Perform squash merge and delete branch
|
|
249
|
-
info "Merging PR #${pr_number} with squash..."
|
|
423
|
+
info "Merging PR #${pr_number} with squash (tier: ${risk_tier}, head: ${head_sha:0:7})..."
|
|
250
424
|
if gh pr merge "$pr_number" --squash --delete-branch 2>/dev/null; then
|
|
251
425
|
success "PR #${pr_number} merged and branch deleted"
|
|
252
|
-
emit_event "pr.merged" "pr=${pr_number}"
|
|
426
|
+
emit_event "pr.merged" "pr=${pr_number}" "risk_tier=${risk_tier}" "head_sha=${head_sha:0:7}"
|
|
253
427
|
|
|
254
428
|
# Post feedback to originating issue
|
|
255
429
|
local issue_number
|
|
@@ -315,7 +489,7 @@ ${DIM}— Shipwright auto-lifecycle manager${RESET}"
|
|
|
315
489
|
gh pr comment "$pr_number" --body "$close_comment" 2>/dev/null || true
|
|
316
490
|
gh pr close "$pr_number" 2>/dev/null && {
|
|
317
491
|
success "Closed PR #${pr_number}"
|
|
318
|
-
((closed_count
|
|
492
|
+
closed_count=$((closed_count + 1))
|
|
319
493
|
emit_event "pr.closed_stale" "pr=${pr_number}" "age_days=${age_days}"
|
|
320
494
|
}
|
|
321
495
|
fi
|
package/scripts/sw-predictive.sh
CHANGED
|
@@ -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="
|
|
9
|
+
VERSION="3.0.0"
|
|
10
10
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
11
11
|
REPO_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|
12
12
|
|
|
@@ -34,16 +34,6 @@ if [[ "$(type -t emit_event 2>/dev/null)" != "function" ]]; then
|
|
|
34
34
|
echo "${payload}}" >> "${HOME}/.shipwright/events.jsonl"
|
|
35
35
|
}
|
|
36
36
|
fi
|
|
37
|
-
CYAN="${CYAN:-\033[38;2;0;212;255m}"
|
|
38
|
-
PURPLE="${PURPLE:-\033[38;2;124;58;237m}"
|
|
39
|
-
BLUE="${BLUE:-\033[38;2;0;102;255m}"
|
|
40
|
-
GREEN="${GREEN:-\033[38;2;74;222;128m}"
|
|
41
|
-
YELLOW="${YELLOW:-\033[38;2;250;204;21m}"
|
|
42
|
-
RED="${RED:-\033[38;2;248;113;113m}"
|
|
43
|
-
DIM="${DIM:-\033[2m}"
|
|
44
|
-
BOLD="${BOLD:-\033[1m}"
|
|
45
|
-
RESET="${RESET:-\033[0m}"
|
|
46
|
-
|
|
47
37
|
# ─── Structured Event Log ──────────────────────────────────────────────────
|
|
48
38
|
EVENTS_FILE="${HOME}/.shipwright/events.jsonl"
|
|
49
39
|
|
|
@@ -169,7 +159,7 @@ _predictive_record_anomaly() {
|
|
|
169
159
|
'{ts: $ts, ts_epoch: $epoch, stage: $stage, metric: $metric, severity: $severity, value: $value, baseline: $baseline, confirmed: null}')
|
|
170
160
|
echo "$record" >> "$tracking_file"
|
|
171
161
|
# Rotate anomaly tracking to prevent unbounded growth
|
|
172
|
-
type rotate_jsonl
|
|
162
|
+
type rotate_jsonl >/dev/null 2>&1 && rotate_jsonl "$tracking_file" 5000
|
|
173
163
|
}
|
|
174
164
|
|
|
175
165
|
# predictive_confirm_anomaly <stage> <metric_name> <was_real_failure>
|
|
@@ -308,7 +298,7 @@ _predictive_github_risk_factors() {
|
|
|
308
298
|
local issue_json="$1"
|
|
309
299
|
local risk_factors='{"security_risk": 0, "churn_risk": 0, "contributor_risk": 0, "recurrence_risk": 0}'
|
|
310
300
|
|
|
311
|
-
type _gh_detect_repo
|
|
301
|
+
type _gh_detect_repo >/dev/null 2>&1 || { echo "$risk_factors"; return 0; }
|
|
312
302
|
_gh_detect_repo 2>/dev/null || { echo "$risk_factors"; return 0; }
|
|
313
303
|
|
|
314
304
|
local owner="${GH_OWNER:-}" repo="${GH_REPO:-}"
|
|
@@ -316,7 +306,7 @@ _predictive_github_risk_factors() {
|
|
|
316
306
|
|
|
317
307
|
# Security risk: active alerts
|
|
318
308
|
local sec_risk=0
|
|
319
|
-
if type gh_security_alerts
|
|
309
|
+
if type gh_security_alerts >/dev/null 2>&1; then
|
|
320
310
|
local alert_count
|
|
321
311
|
alert_count=$(gh_security_alerts "$owner" "$repo" 2>/dev/null | jq 'length' 2>/dev/null || echo "0")
|
|
322
312
|
if [[ "${alert_count:-0}" -gt 10 ]]; then
|
|
@@ -330,7 +320,7 @@ _predictive_github_risk_factors() {
|
|
|
330
320
|
|
|
331
321
|
# Recurrence risk: similar past issues
|
|
332
322
|
local rec_risk=0
|
|
333
|
-
if type gh_similar_issues
|
|
323
|
+
if type gh_similar_issues >/dev/null 2>&1; then
|
|
334
324
|
local title
|
|
335
325
|
title=$(echo "$issue_json" | jq -r '.title // ""' 2>/dev/null | head -c 100)
|
|
336
326
|
if [[ -n "$title" ]]; then
|
|
@@ -346,7 +336,7 @@ _predictive_github_risk_factors() {
|
|
|
346
336
|
|
|
347
337
|
# Contributor risk: low contributor count = bus factor risk
|
|
348
338
|
local cont_risk=0
|
|
349
|
-
if type gh_contributors
|
|
339
|
+
if type gh_contributors >/dev/null 2>&1; then
|
|
350
340
|
local contributor_count
|
|
351
341
|
contributor_count=$(gh_contributors "$owner" "$repo" 2>/dev/null | jq 'length' 2>/dev/null || echo "0")
|
|
352
342
|
if [[ "${contributor_count:-0}" -lt 2 ]]; then
|
|
@@ -368,7 +358,7 @@ predict_pipeline_risk() {
|
|
|
368
358
|
local issue_json="${1:-"{}"}"
|
|
369
359
|
local repo_context="${2:-}"
|
|
370
360
|
|
|
371
|
-
if [[ "$INTELLIGENCE_AVAILABLE" == "true" ]] && command -v _intelligence_call_claude
|
|
361
|
+
if [[ "$INTELLIGENCE_AVAILABLE" == "true" ]] && command -v _intelligence_call_claude >/dev/null 2>&1; then
|
|
372
362
|
local prompt
|
|
373
363
|
prompt="Analyze this issue for pipeline risk. Return ONLY valid JSON.
|
|
374
364
|
|
|
@@ -381,7 +371,7 @@ Return JSON format:
|
|
|
381
371
|
local result
|
|
382
372
|
result=$(_intelligence_call_claude "$prompt" 2>/dev/null || echo "")
|
|
383
373
|
|
|
384
|
-
if [[ -n "$result" ]] && echo "$result" | jq -e '.overall_risk'
|
|
374
|
+
if [[ -n "$result" ]] && echo "$result" | jq -e '.overall_risk' >/dev/null 2>&1; then
|
|
385
375
|
# Validate range
|
|
386
376
|
local risk
|
|
387
377
|
risk=$(echo "$result" | jq '.overall_risk')
|
|
@@ -503,7 +493,7 @@ $(head -100 "$file_path" 2>/dev/null || true)
|
|
|
503
493
|
return 0
|
|
504
494
|
fi
|
|
505
495
|
|
|
506
|
-
if [[ "$INTELLIGENCE_AVAILABLE" != "true" ]] || ! command -v _intelligence_call_claude
|
|
496
|
+
if [[ "$INTELLIGENCE_AVAILABLE" != "true" ]] || ! command -v _intelligence_call_claude >/dev/null 2>&1; then
|
|
507
497
|
echo '[]'
|
|
508
498
|
return 0
|
|
509
499
|
fi
|
|
@@ -524,7 +514,7 @@ Only return findings with severity 'high' or 'critical'. Return [] if nothing si
|
|
|
524
514
|
local result
|
|
525
515
|
result=$(_intelligence_call_claude "$prompt" 2>/dev/null || echo "")
|
|
526
516
|
|
|
527
|
-
if [[ -n "$result" ]] && echo "$result" | jq -e 'type == "array"'
|
|
517
|
+
if [[ -n "$result" ]] && echo "$result" | jq -e 'type == "array"' >/dev/null 2>&1; then
|
|
528
518
|
# Filter to only high/critical findings
|
|
529
519
|
local filtered
|
|
530
520
|
filtered=$(echo "$result" | jq '[.[] | select(.severity == "high" or .severity == "critical")]')
|
|
@@ -588,9 +578,13 @@ predict_detect_anomaly() {
|
|
|
588
578
|
return 0
|
|
589
579
|
fi
|
|
590
580
|
|
|
591
|
-
# Get per-metric thresholds (adaptive or default)
|
|
581
|
+
# Get per-metric thresholds (adaptive from intelligence/DB, or file-based, or default)
|
|
592
582
|
local metric_critical_mult metric_warning_mult
|
|
593
|
-
|
|
583
|
+
if [[ "$(type -t get_anomaly_threshold 2>/dev/null)" == "function" ]]; then
|
|
584
|
+
metric_critical_mult=$(get_anomaly_threshold)
|
|
585
|
+
else
|
|
586
|
+
metric_critical_mult=$(_predictive_get_anomaly_threshold "$metric_name")
|
|
587
|
+
fi
|
|
594
588
|
metric_warning_mult=$(_predictive_get_warning_multiplier "$metric_name")
|
|
595
589
|
|
|
596
590
|
# Calculate thresholds using awk for floating-point
|
package/scripts/sw-prep.sh
CHANGED
|
@@ -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="
|
|
9
|
+
VERSION="3.0.0"
|
|
10
10
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
11
11
|
|
|
12
12
|
# ─── Handle subcommands ───────────────────────────────────────────────────────
|
|
@@ -38,16 +38,6 @@ if [[ "$(type -t emit_event 2>/dev/null)" != "function" ]]; then
|
|
|
38
38
|
echo "${payload}}" >> "${HOME}/.shipwright/events.jsonl"
|
|
39
39
|
}
|
|
40
40
|
fi
|
|
41
|
-
CYAN="${CYAN:-\033[38;2;0;212;255m}"
|
|
42
|
-
PURPLE="${PURPLE:-\033[38;2;124;58;237m}"
|
|
43
|
-
BLUE="${BLUE:-\033[38;2;0;102;255m}"
|
|
44
|
-
GREEN="${GREEN:-\033[38;2;74;222;128m}"
|
|
45
|
-
YELLOW="${YELLOW:-\033[38;2;250;204;21m}"
|
|
46
|
-
RED="${RED:-\033[38;2;248;113;113m}"
|
|
47
|
-
DIM="${DIM:-\033[2m}"
|
|
48
|
-
BOLD="${BOLD:-\033[1m}"
|
|
49
|
-
RESET="${RESET:-\033[0m}"
|
|
50
|
-
|
|
51
41
|
# ─── Defaults ───────────────────────────────────────────────────────────────
|
|
52
42
|
FORCE=false
|
|
53
43
|
CHECK_ONLY=false
|
|
@@ -153,7 +143,7 @@ done
|
|
|
153
143
|
# ─── prep_init ──────────────────────────────────────────────────────────────
|
|
154
144
|
|
|
155
145
|
prep_init() {
|
|
156
|
-
if ! git rev-parse --is-inside-work-tree
|
|
146
|
+
if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
|
|
157
147
|
error "Not inside a git repository"
|
|
158
148
|
exit 1
|
|
159
149
|
fi
|
|
@@ -591,7 +581,7 @@ prep_extract_patterns() {
|
|
|
591
581
|
# ─── Intelligence Check ──────────────────────────────────────────────────
|
|
592
582
|
|
|
593
583
|
intelligence_available() {
|
|
594
|
-
command -v claude
|
|
584
|
+
command -v claude >/dev/null 2>&1 || return 1
|
|
595
585
|
# Honor --with-claude flag
|
|
596
586
|
$WITH_CLAUDE && return 0
|
|
597
587
|
# Check daemon config for intelligence.enabled
|
|
@@ -1407,7 +1397,7 @@ prep_generate_manifest() {
|
|
|
1407
1397
|
HEREDOC
|
|
1408
1398
|
|
|
1409
1399
|
# Validate JSON
|
|
1410
|
-
if command -v jq
|
|
1400
|
+
if command -v jq >/dev/null 2>&1; then
|
|
1411
1401
|
if ! jq empty "$filepath" 2>/dev/null; then
|
|
1412
1402
|
warn "prep-manifest.json may have invalid JSON — check manually"
|
|
1413
1403
|
fi
|
|
@@ -1422,7 +1412,7 @@ HEREDOC
|
|
|
1422
1412
|
prep_with_claude() {
|
|
1423
1413
|
if ! $WITH_CLAUDE; then return; fi
|
|
1424
1414
|
|
|
1425
|
-
if ! command -v claude
|
|
1415
|
+
if ! command -v claude >/dev/null 2>&1; then
|
|
1426
1416
|
warn "claude CLI not found — skipping deep analysis"
|
|
1427
1417
|
return
|
|
1428
1418
|
fi
|
|
@@ -1471,7 +1461,7 @@ prep_validate() {
|
|
|
1471
1461
|
local issues=0
|
|
1472
1462
|
|
|
1473
1463
|
# Check JSON files
|
|
1474
|
-
if command -v jq
|
|
1464
|
+
if command -v jq >/dev/null 2>&1; then
|
|
1475
1465
|
for f in "$PROJECT_ROOT/.claude/settings.json" "$PROJECT_ROOT/.claude/prep-manifest.json"; do
|
|
1476
1466
|
if [[ -f "$f" ]] && ! jq empty "$f" 2>/dev/null; then
|
|
1477
1467
|
warn "Invalid JSON: ${f##"$PROJECT_ROOT"/}"
|
package/scripts/sw-ps.sh
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
# ║ Displays a table of agents running in claude-* tmux windows with ║
|
|
6
6
|
# ║ PID, status, idle time, and pane references. ║
|
|
7
7
|
# ╚═══════════════════════════════════════════════════════════════════════════╝
|
|
8
|
-
VERSION="
|
|
8
|
+
VERSION="3.0.0"
|
|
9
9
|
set -euo pipefail
|
|
10
10
|
trap 'echo "ERROR: $BASH_SOURCE:$LINENO exited with status $?" >&2' ERR
|
|
11
11
|
|
|
@@ -31,16 +31,6 @@ if [[ "$(type -t emit_event 2>/dev/null)" != "function" ]]; then
|
|
|
31
31
|
echo "${payload}}" >> "${HOME}/.shipwright/events.jsonl"
|
|
32
32
|
}
|
|
33
33
|
fi
|
|
34
|
-
CYAN="${CYAN:-\033[38;2;0;212;255m}"
|
|
35
|
-
PURPLE="${PURPLE:-\033[38;2;124;58;237m}"
|
|
36
|
-
BLUE="${BLUE:-\033[38;2;0;102;255m}"
|
|
37
|
-
GREEN="${GREEN:-\033[38;2;74;222;128m}"
|
|
38
|
-
YELLOW="${YELLOW:-\033[38;2;250;204;21m}"
|
|
39
|
-
RED="${RED:-\033[38;2;248;113;113m}"
|
|
40
|
-
DIM="${DIM:-\033[2m}"
|
|
41
|
-
BOLD="${BOLD:-\033[1m}"
|
|
42
|
-
RESET="${RESET:-\033[0m}"
|
|
43
|
-
|
|
44
34
|
# ─── Format idle time ───────────────────────────────────────────────────────
|
|
45
35
|
format_idle() {
|
|
46
36
|
local seconds="$1"
|
|
@@ -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="
|
|
9
|
+
VERSION="3.0.0"
|
|
10
10
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
11
11
|
REPO_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|
12
12
|
|
|
@@ -34,16 +34,6 @@ if [[ "$(type -t emit_event 2>/dev/null)" != "function" ]]; then
|
|
|
34
34
|
echo "${payload}}" >> "${HOME}/.shipwright/events.jsonl"
|
|
35
35
|
}
|
|
36
36
|
fi
|
|
37
|
-
CYAN="${CYAN:-\033[38;2;0;212;255m}"
|
|
38
|
-
PURPLE="${PURPLE:-\033[38;2;124;58;237m}"
|
|
39
|
-
BLUE="${BLUE:-\033[38;2;0;102;255m}"
|
|
40
|
-
GREEN="${GREEN:-\033[38;2;74;222;128m}"
|
|
41
|
-
YELLOW="${YELLOW:-\033[38;2;250;204;21m}"
|
|
42
|
-
RED="${RED:-\033[38;2;248;113;113m}"
|
|
43
|
-
DIM="${DIM:-\033[2m}"
|
|
44
|
-
BOLD="${BOLD:-\033[1m}"
|
|
45
|
-
RESET="${RESET:-\033[0m}"
|
|
46
|
-
|
|
47
37
|
# ─── Paths ──────────────────────────────────────────────────────────────────
|
|
48
38
|
PUB_DASH_DIR="${HOME}/.shipwright/public-dashboard"
|
|
49
39
|
SHARE_LINKS_FILE="${PUB_DASH_DIR}/share-links.json"
|
|
@@ -133,7 +123,7 @@ gather_pipeline_state() {
|
|
|
133
123
|
# ─── Generate Token ─────────────────────────────────────────────────────────
|
|
134
124
|
generate_token() {
|
|
135
125
|
# Create a read-only token (32 hex chars)
|
|
136
|
-
if command -v openssl
|
|
126
|
+
if command -v openssl >/dev/null 2>&1; then
|
|
137
127
|
openssl rand -hex 16
|
|
138
128
|
else
|
|
139
129
|
# Fallback to simple pseudo-random
|