shipwright-cli 1.10.0 → 2.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.
- package/README.md +221 -55
- package/completions/_shipwright +264 -32
- package/completions/shipwright.bash +118 -26
- package/completions/shipwright.fish +80 -2
- package/dashboard/server.ts +208 -0
- package/docs/strategy/01-market-research.md +619 -0
- package/docs/strategy/02-mission-and-brand.md +587 -0
- package/docs/strategy/03-gtm-and-roadmap.md +759 -0
- package/docs/strategy/QUICK-START.txt +289 -0
- package/docs/strategy/README.md +172 -0
- package/docs/tmux-research/TMUX-ARCHITECTURE.md +567 -0
- package/docs/tmux-research/TMUX-AUDIT.md +925 -0
- package/docs/tmux-research/TMUX-BEST-PRACTICES-2025-2026.md +829 -0
- package/docs/tmux-research/TMUX-QUICK-REFERENCE.md +543 -0
- package/docs/tmux-research/TMUX-RESEARCH-INDEX.md +438 -0
- package/package.json +4 -2
- package/scripts/lib/helpers.sh +7 -0
- package/scripts/sw +323 -2
- package/scripts/sw-activity.sh +500 -0
- package/scripts/sw-adaptive.sh +925 -0
- package/scripts/sw-adversarial.sh +1 -1
- package/scripts/sw-architecture-enforcer.sh +1 -1
- package/scripts/sw-auth.sh +613 -0
- package/scripts/sw-autonomous.sh +754 -0
- package/scripts/sw-changelog.sh +704 -0
- package/scripts/sw-checkpoint.sh +1 -1
- package/scripts/sw-ci.sh +602 -0
- package/scripts/sw-cleanup.sh +1 -1
- package/scripts/sw-code-review.sh +698 -0
- package/scripts/sw-connect.sh +1 -1
- package/scripts/sw-context.sh +605 -0
- package/scripts/sw-cost.sh +44 -3
- package/scripts/sw-daemon.sh +568 -138
- package/scripts/sw-dashboard.sh +1 -1
- package/scripts/sw-db.sh +1380 -0
- package/scripts/sw-decompose.sh +539 -0
- package/scripts/sw-deps.sh +551 -0
- package/scripts/sw-developer-simulation.sh +1 -1
- package/scripts/sw-discovery.sh +412 -0
- package/scripts/sw-docs-agent.sh +539 -0
- package/scripts/sw-docs.sh +1 -1
- package/scripts/sw-doctor.sh +107 -1
- package/scripts/sw-dora.sh +615 -0
- package/scripts/sw-durable.sh +710 -0
- package/scripts/sw-e2e-orchestrator.sh +535 -0
- package/scripts/sw-eventbus.sh +393 -0
- package/scripts/sw-feedback.sh +479 -0
- package/scripts/sw-fix.sh +1 -1
- package/scripts/sw-fleet-discover.sh +567 -0
- package/scripts/sw-fleet-viz.sh +404 -0
- package/scripts/sw-fleet.sh +8 -1
- package/scripts/sw-github-app.sh +596 -0
- package/scripts/sw-github-checks.sh +4 -4
- package/scripts/sw-github-deploy.sh +1 -1
- package/scripts/sw-github-graphql.sh +1 -1
- package/scripts/sw-guild.sh +569 -0
- package/scripts/sw-heartbeat.sh +1 -1
- package/scripts/sw-hygiene.sh +559 -0
- package/scripts/sw-incident.sh +656 -0
- package/scripts/sw-init.sh +237 -24
- package/scripts/sw-instrument.sh +699 -0
- package/scripts/sw-intelligence.sh +1 -1
- package/scripts/sw-jira.sh +1 -1
- package/scripts/sw-launchd.sh +363 -28
- package/scripts/sw-linear.sh +1 -1
- package/scripts/sw-logs.sh +1 -1
- package/scripts/sw-loop.sh +267 -21
- package/scripts/sw-memory.sh +18 -1
- package/scripts/sw-mission-control.sh +487 -0
- package/scripts/sw-model-router.sh +545 -0
- package/scripts/sw-otel.sh +596 -0
- package/scripts/sw-oversight.sh +764 -0
- package/scripts/sw-pipeline-composer.sh +1 -1
- package/scripts/sw-pipeline-vitals.sh +1 -1
- package/scripts/sw-pipeline.sh +947 -35
- package/scripts/sw-pm.sh +758 -0
- package/scripts/sw-pr-lifecycle.sh +522 -0
- package/scripts/sw-predictive.sh +8 -1
- package/scripts/sw-prep.sh +1 -1
- package/scripts/sw-ps.sh +1 -1
- package/scripts/sw-public-dashboard.sh +798 -0
- package/scripts/sw-quality.sh +595 -0
- package/scripts/sw-reaper.sh +1 -1
- package/scripts/sw-recruit.sh +2248 -0
- package/scripts/sw-regression.sh +642 -0
- package/scripts/sw-release-manager.sh +736 -0
- package/scripts/sw-release.sh +706 -0
- package/scripts/sw-remote.sh +1 -1
- package/scripts/sw-replay.sh +520 -0
- package/scripts/sw-retro.sh +691 -0
- package/scripts/sw-scale.sh +444 -0
- package/scripts/sw-security-audit.sh +505 -0
- package/scripts/sw-self-optimize.sh +1 -1
- package/scripts/sw-session.sh +1 -1
- package/scripts/sw-setup.sh +263 -127
- package/scripts/sw-standup.sh +712 -0
- package/scripts/sw-status.sh +44 -2
- package/scripts/sw-strategic.sh +806 -0
- package/scripts/sw-stream.sh +450 -0
- package/scripts/sw-swarm.sh +620 -0
- package/scripts/sw-team-stages.sh +511 -0
- package/scripts/sw-templates.sh +4 -4
- package/scripts/sw-testgen.sh +566 -0
- package/scripts/sw-tmux-pipeline.sh +554 -0
- package/scripts/sw-tmux-role-color.sh +58 -0
- package/scripts/sw-tmux-status.sh +128 -0
- package/scripts/sw-tmux.sh +1 -1
- package/scripts/sw-trace.sh +485 -0
- package/scripts/sw-tracker-github.sh +188 -0
- package/scripts/sw-tracker-jira.sh +172 -0
- package/scripts/sw-tracker-linear.sh +251 -0
- package/scripts/sw-tracker.sh +117 -2
- package/scripts/sw-triage.sh +627 -0
- package/scripts/sw-upgrade.sh +1 -1
- package/scripts/sw-ux.sh +677 -0
- package/scripts/sw-webhook.sh +627 -0
- package/scripts/sw-widgets.sh +530 -0
- package/scripts/sw-worktree.sh +1 -1
- package/templates/pipelines/autonomous.json +2 -2
- package/tmux/shipwright-overlay.conf +35 -17
- package/tmux/tmux.conf +23 -21
|
@@ -0,0 +1,764 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# ╔═══════════════════════════════════════════════════════════════════════════╗
|
|
3
|
+
# ║ shipwright oversight — Quality Oversight Board ║
|
|
4
|
+
# ║ Multi-agent review council · Voting system · Architecture governance ║
|
|
5
|
+
# ║ Security review · Performance review · Verdict aggregation ║
|
|
6
|
+
# ╚═══════════════════════════════════════════════════════════════════════════╝
|
|
7
|
+
set -euo pipefail
|
|
8
|
+
trap 'echo "ERROR: $BASH_SOURCE:$LINENO exited with status $?" >&2' ERR
|
|
9
|
+
|
|
10
|
+
VERSION="2.1.0"
|
|
11
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
12
|
+
REPO_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|
13
|
+
|
|
14
|
+
# ─── Colors (matches Seth's tmux theme) ─────────────────────────────────────
|
|
15
|
+
CYAN='\033[38;2;0;212;255m' # #00d4ff — primary accent
|
|
16
|
+
PURPLE='\033[38;2;124;58;237m' # #7c3aed — secondary
|
|
17
|
+
BLUE='\033[38;2;0;102;255m' # #0066ff — tertiary
|
|
18
|
+
GREEN='\033[38;2;74;222;128m' # success
|
|
19
|
+
YELLOW='\033[38;2;250;204;21m' # warning
|
|
20
|
+
RED='\033[38;2;248;113;113m' # error
|
|
21
|
+
DIM='\033[2m'
|
|
22
|
+
BOLD='\033[1m'
|
|
23
|
+
RESET='\033[0m'
|
|
24
|
+
|
|
25
|
+
# ─── Cross-platform compatibility ──────────────────────────────────────────
|
|
26
|
+
# shellcheck source=lib/compat.sh
|
|
27
|
+
[[ -f "$SCRIPT_DIR/lib/compat.sh" ]] && source "$SCRIPT_DIR/lib/compat.sh"
|
|
28
|
+
|
|
29
|
+
# ─── Output Helpers ─────────────────────────────────────────────────────────
|
|
30
|
+
info() { echo -e "${CYAN}${BOLD}▸${RESET} $*"; }
|
|
31
|
+
success() { echo -e "${GREEN}${BOLD}✓${RESET} $*"; }
|
|
32
|
+
warn() { echo -e "${YELLOW}${BOLD}⚠${RESET} $*"; }
|
|
33
|
+
error() { echo -e "${RED}${BOLD}✗${RESET} $*" >&2; }
|
|
34
|
+
|
|
35
|
+
now_iso() { date -u +"%Y-%m-%dT%H:%M:%SZ"; }
|
|
36
|
+
now_epoch() { date +%s; }
|
|
37
|
+
|
|
38
|
+
# ─── Structured Event Log ────────────────────────────────────────────────
|
|
39
|
+
EVENTS_FILE="${HOME}/.shipwright/events.jsonl"
|
|
40
|
+
|
|
41
|
+
emit_event() {
|
|
42
|
+
local event_type="$1"; shift
|
|
43
|
+
local json_fields=""
|
|
44
|
+
for kv in "$@"; do
|
|
45
|
+
local key="${kv%%=*}"; local val="${kv#*=}"
|
|
46
|
+
if [[ "$val" =~ ^-?[0-9]+\.?[0-9]*$ ]]; then
|
|
47
|
+
json_fields="${json_fields},\"${key}\":${val}"
|
|
48
|
+
else
|
|
49
|
+
val="${val//\"/\\\"}"; json_fields="${json_fields},\"${key}\":\"${val}\""
|
|
50
|
+
fi
|
|
51
|
+
done
|
|
52
|
+
mkdir -p "${HOME}/.shipwright"
|
|
53
|
+
echo "{\"ts\":\"$(now_iso)\",\"ts_epoch\":$(now_epoch),\"type\":\"${event_type}\"${json_fields}}" >> "$EVENTS_FILE"
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
# ─── State & Configuration ────────────────────────────────────────────────
|
|
57
|
+
OVERSIGHT_ROOT="${HOME}/.shipwright/oversight"
|
|
58
|
+
BOARD_CONFIG="${OVERSIGHT_ROOT}/config.json"
|
|
59
|
+
REVIEW_LOG="${OVERSIGHT_ROOT}/reviews.jsonl"
|
|
60
|
+
HISTORY_DIR="${OVERSIGHT_ROOT}/history"
|
|
61
|
+
MEMBERS_FILE="${OVERSIGHT_ROOT}/members.json"
|
|
62
|
+
|
|
63
|
+
# ─── Initialization ─────────────────────────────────────────────────────────
|
|
64
|
+
|
|
65
|
+
_ensure_oversight_dirs() {
|
|
66
|
+
mkdir -p "$OVERSIGHT_ROOT" "$HISTORY_DIR"
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
_init_board_config() {
|
|
70
|
+
_ensure_oversight_dirs
|
|
71
|
+
if [[ ! -f "$BOARD_CONFIG" ]]; then
|
|
72
|
+
cat > "$BOARD_CONFIG" <<'EOF'
|
|
73
|
+
{
|
|
74
|
+
"quorum": 0.5,
|
|
75
|
+
"reviewers": ["code_quality", "security", "performance", "architecture"],
|
|
76
|
+
"strictness": "normal",
|
|
77
|
+
"enabled": true,
|
|
78
|
+
"appeal_max_attempts": 3
|
|
79
|
+
}
|
|
80
|
+
EOF
|
|
81
|
+
success "Initialized oversight board config"
|
|
82
|
+
fi
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
_init_members() {
|
|
86
|
+
_ensure_oversight_dirs
|
|
87
|
+
if [[ ! -f "$MEMBERS_FILE" ]]; then
|
|
88
|
+
cat > "$MEMBERS_FILE" <<'EOF'
|
|
89
|
+
{
|
|
90
|
+
"code_quality": {
|
|
91
|
+
"role": "Code Quality Reviewer",
|
|
92
|
+
"expertise": ["readability", "maintainability", "style", "structure"],
|
|
93
|
+
"reviews": 0,
|
|
94
|
+
"avg_confidence": 0.0
|
|
95
|
+
},
|
|
96
|
+
"security": {
|
|
97
|
+
"role": "Security Specialist",
|
|
98
|
+
"expertise": ["owasp", "credentials", "injection", "defaults", "cwe"],
|
|
99
|
+
"reviews": 0,
|
|
100
|
+
"avg_confidence": 0.0
|
|
101
|
+
},
|
|
102
|
+
"performance": {
|
|
103
|
+
"role": "Performance Engineer",
|
|
104
|
+
"expertise": ["n+1_queries", "memory_leaks", "caching", "algorithms"],
|
|
105
|
+
"reviews": 0,
|
|
106
|
+
"avg_confidence": 0.0
|
|
107
|
+
},
|
|
108
|
+
"architecture": {
|
|
109
|
+
"role": "Architecture Enforcer",
|
|
110
|
+
"expertise": ["layer_boundaries", "dependency_direction", "naming", "modules"],
|
|
111
|
+
"reviews": 0,
|
|
112
|
+
"avg_confidence": 0.0
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
EOF
|
|
116
|
+
success "Initialized oversight board members"
|
|
117
|
+
fi
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
# ─── Review Submission ───────────────────────────────────────────────────
|
|
121
|
+
|
|
122
|
+
cmd_review() {
|
|
123
|
+
local pr_number=""
|
|
124
|
+
local commit=""
|
|
125
|
+
local diff_file=""
|
|
126
|
+
local description=""
|
|
127
|
+
|
|
128
|
+
while [[ $# -gt 0 ]]; do
|
|
129
|
+
case "$1" in
|
|
130
|
+
--pr) pr_number="$2"; shift 2 ;;
|
|
131
|
+
--commit) commit="$2"; shift 2 ;;
|
|
132
|
+
--diff) diff_file="$2"; shift 2 ;;
|
|
133
|
+
--description) description="$2"; shift 2 ;;
|
|
134
|
+
-h|--help)
|
|
135
|
+
echo "Usage: oversight review [--pr <N>|--commit <ref>|--diff <file>] [--description <text>]"
|
|
136
|
+
exit 0
|
|
137
|
+
;;
|
|
138
|
+
*) error "Unknown option: $1"; exit 1 ;;
|
|
139
|
+
esac
|
|
140
|
+
done
|
|
141
|
+
|
|
142
|
+
_init_board_config
|
|
143
|
+
_init_members
|
|
144
|
+
|
|
145
|
+
if [[ -z "$pr_number" && -z "$commit" && -z "$diff_file" ]]; then
|
|
146
|
+
error "Provide --pr, --commit, or --diff"
|
|
147
|
+
exit 1
|
|
148
|
+
fi
|
|
149
|
+
|
|
150
|
+
local review_id
|
|
151
|
+
review_id=$(date +%s)_$(head -c8 /dev/urandom | od -A n -t x1 | tr -d ' ')
|
|
152
|
+
|
|
153
|
+
local review_file="${OVERSIGHT_ROOT}/${review_id}.json"
|
|
154
|
+
|
|
155
|
+
# Build review record
|
|
156
|
+
cat > "$review_file" <<EOF
|
|
157
|
+
{
|
|
158
|
+
"id": "$review_id",
|
|
159
|
+
"submitted_at": "$(now_iso)",
|
|
160
|
+
"pr_number": ${pr_number:-null},
|
|
161
|
+
"commit": ${commit:-null},
|
|
162
|
+
"diff_file": ${diff_file:-null},
|
|
163
|
+
"description": "${description//\"/\\\"}",
|
|
164
|
+
"votes": {},
|
|
165
|
+
"verdict": null,
|
|
166
|
+
"confidence_score": 0.0,
|
|
167
|
+
"appeals": []
|
|
168
|
+
}
|
|
169
|
+
EOF
|
|
170
|
+
|
|
171
|
+
emit_event "oversight_review_submitted" "review_id=$review_id" "pr=$pr_number" "commit=$commit"
|
|
172
|
+
|
|
173
|
+
info "Review submitted: $review_id"
|
|
174
|
+
echo " PR: ${pr_number:-—}"
|
|
175
|
+
echo " Commit: ${commit:-—}"
|
|
176
|
+
echo " Diff: ${diff_file:-—}"
|
|
177
|
+
echo ""
|
|
178
|
+
echo "Board members will review and vote:"
|
|
179
|
+
jq -r '.[] | " • \(.role)"' "$MEMBERS_FILE"
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
# ─── Vote Recording ────────────────────────────────────────────────────
|
|
183
|
+
|
|
184
|
+
cmd_vote() {
|
|
185
|
+
local review_id=""
|
|
186
|
+
local reviewer=""
|
|
187
|
+
local decision="" # approve, reject, abstain
|
|
188
|
+
local reasoning=""
|
|
189
|
+
local confidence=0.0
|
|
190
|
+
|
|
191
|
+
while [[ $# -gt 0 ]]; do
|
|
192
|
+
case "$1" in
|
|
193
|
+
--review) review_id="$2"; shift 2 ;;
|
|
194
|
+
--reviewer) reviewer="$2"; shift 2 ;;
|
|
195
|
+
--decision) decision="$2"; shift 2 ;;
|
|
196
|
+
--reasoning) reasoning="$2"; shift 2 ;;
|
|
197
|
+
--confidence) confidence="$2"; shift 2 ;;
|
|
198
|
+
-h|--help)
|
|
199
|
+
echo "Usage: oversight vote --review <id> --reviewer <name> --decision [approve|reject|abstain] --reasoning <text> [--confidence <0.0-1.0>]"
|
|
200
|
+
exit 0
|
|
201
|
+
;;
|
|
202
|
+
*) error "Unknown option: $1"; exit 1 ;;
|
|
203
|
+
esac
|
|
204
|
+
done
|
|
205
|
+
|
|
206
|
+
if [[ -z "$review_id" || -z "$reviewer" || -z "$decision" ]]; then
|
|
207
|
+
error "Require --review, --reviewer, --decision"
|
|
208
|
+
exit 1
|
|
209
|
+
fi
|
|
210
|
+
|
|
211
|
+
local review_file="${OVERSIGHT_ROOT}/${review_id}.json"
|
|
212
|
+
if [[ ! -f "$review_file" ]]; then
|
|
213
|
+
error "Review not found: $review_id"
|
|
214
|
+
exit 1
|
|
215
|
+
fi
|
|
216
|
+
|
|
217
|
+
# Validate decision
|
|
218
|
+
case "$decision" in
|
|
219
|
+
approve|reject|abstain) ;;
|
|
220
|
+
*) error "Invalid decision: $decision (must be approve, reject, or abstain)"; exit 1 ;;
|
|
221
|
+
esac
|
|
222
|
+
|
|
223
|
+
# Update review with vote
|
|
224
|
+
local tmp_file="${review_file}.tmp"
|
|
225
|
+
jq --arg reviewer "$reviewer" \
|
|
226
|
+
--arg decision "$decision" \
|
|
227
|
+
--arg reasoning "${reasoning//\"/\\\"}" \
|
|
228
|
+
--arg confidence "$confidence" \
|
|
229
|
+
'.votes[$reviewer] = {
|
|
230
|
+
"decision": $decision,
|
|
231
|
+
"reasoning": $reasoning,
|
|
232
|
+
"confidence": ($confidence | tonumber),
|
|
233
|
+
"voted_at": "'$(now_iso)'"
|
|
234
|
+
}' "$review_file" > "$tmp_file"
|
|
235
|
+
mv "$tmp_file" "$review_file"
|
|
236
|
+
|
|
237
|
+
success "Vote recorded: $reviewer → $decision"
|
|
238
|
+
emit_event "oversight_vote_recorded" "review_id=$review_id" "reviewer=$reviewer" "decision=$decision" "confidence=$confidence"
|
|
239
|
+
|
|
240
|
+
_update_verdict "$review_id"
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
# ─── Verdict Calculation ─────────────────────────────────────────────────
|
|
244
|
+
|
|
245
|
+
_update_verdict() {
|
|
246
|
+
local review_id="$1"
|
|
247
|
+
local review_file="${OVERSIGHT_ROOT}/${review_id}.json"
|
|
248
|
+
|
|
249
|
+
if [[ ! -f "$review_file" ]]; then
|
|
250
|
+
return 1
|
|
251
|
+
fi
|
|
252
|
+
|
|
253
|
+
local votes
|
|
254
|
+
votes=$(jq '.votes' "$review_file")
|
|
255
|
+
|
|
256
|
+
local approve_count=0
|
|
257
|
+
local reject_count=0
|
|
258
|
+
local abstain_count=0
|
|
259
|
+
local total_confidence=0.0
|
|
260
|
+
local reviewer_count=0
|
|
261
|
+
|
|
262
|
+
while IFS= read -r reviewer_data; do
|
|
263
|
+
local decision
|
|
264
|
+
decision=$(echo "$reviewer_data" | jq -r '.decision')
|
|
265
|
+
local confidence
|
|
266
|
+
confidence=$(echo "$reviewer_data" | jq -r '.confidence')
|
|
267
|
+
|
|
268
|
+
case "$decision" in
|
|
269
|
+
approve) approve_count=$((approve_count + 1)) ;;
|
|
270
|
+
reject) reject_count=$((reject_count + 1)) ;;
|
|
271
|
+
abstain) abstain_count=$((abstain_count + 1)) ;;
|
|
272
|
+
esac
|
|
273
|
+
|
|
274
|
+
total_confidence=$(echo "$total_confidence + $confidence" | bc 2>/dev/null || echo "0")
|
|
275
|
+
reviewer_count=$((reviewer_count + 1))
|
|
276
|
+
done < <(echo "$votes" | jq -c '.[]')
|
|
277
|
+
|
|
278
|
+
local quorum
|
|
279
|
+
quorum=$(jq -r '.quorum // 0.5' "$BOARD_CONFIG")
|
|
280
|
+
|
|
281
|
+
local active_votes=$((approve_count + reject_count))
|
|
282
|
+
local total_votes=$((approve_count + reject_count + abstain_count))
|
|
283
|
+
|
|
284
|
+
local verdict="pending"
|
|
285
|
+
local confidence_score=0.0
|
|
286
|
+
|
|
287
|
+
if [[ $total_votes -gt 0 ]]; then
|
|
288
|
+
if [[ $reviewer_count -gt 0 ]]; then
|
|
289
|
+
confidence_score=$(echo "$total_confidence / $reviewer_count" | bc -l 2>/dev/null | cut -c1-5 || echo "0.5")
|
|
290
|
+
fi
|
|
291
|
+
|
|
292
|
+
if [[ $active_votes -gt 0 ]]; then
|
|
293
|
+
local approve_ratio
|
|
294
|
+
approve_ratio=$(echo "$approve_count / $active_votes" | bc -l 2>/dev/null || echo "0")
|
|
295
|
+
|
|
296
|
+
local quorum_num
|
|
297
|
+
quorum_num=$(echo "$quorum * 100" | bc 2>/dev/null || echo "50")
|
|
298
|
+
|
|
299
|
+
local approve_pct
|
|
300
|
+
approve_pct=$(echo "$approve_ratio * 100" | bc 2>/dev/null || echo "0")
|
|
301
|
+
|
|
302
|
+
# Check if quorum met and decision reached
|
|
303
|
+
local quorum_met=0
|
|
304
|
+
if (( $(echo "$active_votes >= $total_votes * $quorum" | bc -l) )); then
|
|
305
|
+
quorum_met=1
|
|
306
|
+
fi
|
|
307
|
+
|
|
308
|
+
if [[ $quorum_met -eq 1 ]]; then
|
|
309
|
+
# Simple majority among active votes
|
|
310
|
+
if [[ $approve_count -gt $reject_count ]]; then
|
|
311
|
+
verdict="approved"
|
|
312
|
+
elif [[ $reject_count -gt $approve_count ]]; then
|
|
313
|
+
verdict="rejected"
|
|
314
|
+
else
|
|
315
|
+
verdict="deadlock"
|
|
316
|
+
fi
|
|
317
|
+
fi
|
|
318
|
+
fi
|
|
319
|
+
fi
|
|
320
|
+
|
|
321
|
+
# Update verdict in review file
|
|
322
|
+
local tmp_file="${review_file}.tmp"
|
|
323
|
+
jq --arg verdict "$verdict" \
|
|
324
|
+
--arg confidence "$confidence_score" \
|
|
325
|
+
'.verdict = $verdict | .confidence_score = ($confidence | tonumber)' \
|
|
326
|
+
"$review_file" > "$tmp_file"
|
|
327
|
+
mv "$tmp_file" "$review_file"
|
|
328
|
+
|
|
329
|
+
if [[ "$verdict" != "pending" ]]; then
|
|
330
|
+
emit_event "oversight_verdict_rendered" "review_id=$review_id" "verdict=$verdict" "confidence=$confidence_score"
|
|
331
|
+
fi
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
# ─── Pipeline gate: submit review, record vote(s), output verdict ───────────
|
|
335
|
+
# Usage: oversight gate --diff <file> [--description <text>] [--reject-if <reason>]
|
|
336
|
+
# Outputs: approved | rejected | deadlock | pending (for pipeline to block on non-approved)
|
|
337
|
+
cmd_gate() {
|
|
338
|
+
local diff_file=""
|
|
339
|
+
local description=""
|
|
340
|
+
local reject_if=""
|
|
341
|
+
|
|
342
|
+
while [[ $# -gt 0 ]]; do
|
|
343
|
+
case "$1" in
|
|
344
|
+
--diff) diff_file="$2"; shift 2 ;;
|
|
345
|
+
--description) description="$2"; shift 2 ;;
|
|
346
|
+
--reject-if) reject_if="$2"; shift 2 ;;
|
|
347
|
+
-h|--help)
|
|
348
|
+
echo "Usage: oversight gate --diff <file> [--description <text>] [--reject-if <reason>]"
|
|
349
|
+
echo "Outputs verdict: approved | rejected | deadlock | pending"
|
|
350
|
+
exit 0
|
|
351
|
+
;;
|
|
352
|
+
*) error "Unknown option: $1"; exit 1 ;;
|
|
353
|
+
esac
|
|
354
|
+
done
|
|
355
|
+
|
|
356
|
+
if [[ -z "$diff_file" || ! -f "$diff_file" ]]; then
|
|
357
|
+
error "Provide --diff <file> (must exist)"
|
|
358
|
+
exit 1
|
|
359
|
+
fi
|
|
360
|
+
|
|
361
|
+
_init_board_config
|
|
362
|
+
_init_members
|
|
363
|
+
|
|
364
|
+
local review_id
|
|
365
|
+
review_id=$(date +%s)_$(head -c8 /dev/urandom 2>/dev/null | od -A n -t x1 | tr -d ' ' || echo "$$")
|
|
366
|
+
local review_file="${OVERSIGHT_ROOT}/${review_id}.json"
|
|
367
|
+
|
|
368
|
+
# Build review record safely via jq (no JSON injection from description/diff_file)
|
|
369
|
+
jq -n \
|
|
370
|
+
--arg id "$review_id" \
|
|
371
|
+
--arg submitted "$(now_iso)" \
|
|
372
|
+
--arg diff "$diff_file" \
|
|
373
|
+
--arg desc "$description" \
|
|
374
|
+
'{id: $id, submitted_at: $submitted, pr_number: null, commit: null, diff_file: $diff, description: $desc, votes: {}, verdict: null, confidence_score: 0.0, appeals: []}' \
|
|
375
|
+
> "$review_file"
|
|
376
|
+
|
|
377
|
+
# Single pipeline voter: reject if --reject-if given, else approve
|
|
378
|
+
local decision="approve"
|
|
379
|
+
local reasoning="Pipeline review passed"
|
|
380
|
+
if [[ -n "$reject_if" ]]; then
|
|
381
|
+
decision="reject"
|
|
382
|
+
reasoning="$reject_if"
|
|
383
|
+
fi
|
|
384
|
+
|
|
385
|
+
local tmp_file="${review_file}.tmp"
|
|
386
|
+
jq --arg reviewer "pipeline" \
|
|
387
|
+
--arg decision "$decision" \
|
|
388
|
+
--arg reasoning "${reasoning//\"/\\\"}" \
|
|
389
|
+
--arg confidence "0.9" \
|
|
390
|
+
'.votes[$reviewer] = {
|
|
391
|
+
"decision": $decision,
|
|
392
|
+
"reasoning": $reasoning,
|
|
393
|
+
"confidence": ($confidence | tonumber),
|
|
394
|
+
"voted_at": "'$(now_iso)'"
|
|
395
|
+
}' "$review_file" > "$tmp_file"
|
|
396
|
+
mv "$tmp_file" "$review_file"
|
|
397
|
+
|
|
398
|
+
_update_verdict "$review_id"
|
|
399
|
+
|
|
400
|
+
local verdict
|
|
401
|
+
verdict=$(jq -r '.verdict // "pending"' "$review_file")
|
|
402
|
+
echo "$verdict"
|
|
403
|
+
if [[ "$verdict" == "rejected" || "$verdict" == "deadlock" ]]; then
|
|
404
|
+
exit 1
|
|
405
|
+
fi
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
# ─── Verdict Display ──────────────────────────────────────────────────────
|
|
409
|
+
|
|
410
|
+
cmd_verdict() {
|
|
411
|
+
local review_id=""
|
|
412
|
+
|
|
413
|
+
while [[ $# -gt 0 ]]; do
|
|
414
|
+
case "$1" in
|
|
415
|
+
--review) review_id="$2"; shift 2 ;;
|
|
416
|
+
-h|--help)
|
|
417
|
+
echo "Usage: oversight verdict --review <id>"
|
|
418
|
+
exit 0
|
|
419
|
+
;;
|
|
420
|
+
*) error "Unknown option: $1"; exit 1 ;;
|
|
421
|
+
esac
|
|
422
|
+
done
|
|
423
|
+
|
|
424
|
+
if [[ -z "$review_id" ]]; then
|
|
425
|
+
error "Require --review <id>"
|
|
426
|
+
exit 1
|
|
427
|
+
fi
|
|
428
|
+
|
|
429
|
+
local review_file="${OVERSIGHT_ROOT}/${review_id}.json"
|
|
430
|
+
if [[ ! -f "$review_file" ]]; then
|
|
431
|
+
error "Review not found: $review_id"
|
|
432
|
+
exit 1
|
|
433
|
+
fi
|
|
434
|
+
|
|
435
|
+
local verdict
|
|
436
|
+
verdict=$(jq -r '.verdict' "$review_file")
|
|
437
|
+
local confidence
|
|
438
|
+
confidence=$(jq -r '.confidence_score' "$review_file")
|
|
439
|
+
|
|
440
|
+
echo ""
|
|
441
|
+
echo "═══════════════════════════════════════════════════════════════"
|
|
442
|
+
echo " Review: $review_id"
|
|
443
|
+
echo "═══════════════════════════════════════════════════════════════"
|
|
444
|
+
echo ""
|
|
445
|
+
|
|
446
|
+
local votes
|
|
447
|
+
votes=$(jq '.votes' "$review_file")
|
|
448
|
+
echo "Board Votes:"
|
|
449
|
+
echo "$votes" | jq -r 'to_entries | .[] | " \(.key): \(.value.decision) (confidence: \(.value.confidence))\n Reasoning: \(.value.reasoning)"'
|
|
450
|
+
echo ""
|
|
451
|
+
|
|
452
|
+
case "$verdict" in
|
|
453
|
+
approved)
|
|
454
|
+
echo -e "${GREEN}${BOLD}✓ APPROVED${RESET}"
|
|
455
|
+
;;
|
|
456
|
+
rejected)
|
|
457
|
+
echo -e "${RED}${BOLD}✗ REJECTED${RESET}"
|
|
458
|
+
;;
|
|
459
|
+
pending)
|
|
460
|
+
echo -e "${YELLOW}${BOLD}⊙ PENDING${RESET}"
|
|
461
|
+
;;
|
|
462
|
+
deadlock)
|
|
463
|
+
echo -e "${YELLOW}${BOLD}↔ DEADLOCK${RESET}"
|
|
464
|
+
;;
|
|
465
|
+
esac
|
|
466
|
+
|
|
467
|
+
echo " Confidence: ${confidence}"
|
|
468
|
+
echo ""
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
# ─── History ─────────────────────────────────────────────────────────────
|
|
472
|
+
|
|
473
|
+
cmd_history() {
|
|
474
|
+
local limit=20
|
|
475
|
+
local filter=""
|
|
476
|
+
|
|
477
|
+
while [[ $# -gt 0 ]]; do
|
|
478
|
+
case "$1" in
|
|
479
|
+
--limit) limit="$2"; shift 2 ;;
|
|
480
|
+
--filter) filter="$2"; shift 2 ;;
|
|
481
|
+
-h|--help)
|
|
482
|
+
echo "Usage: oversight history [--limit <N>] [--filter <verdict>]"
|
|
483
|
+
exit 0
|
|
484
|
+
;;
|
|
485
|
+
*) error "Unknown option: $1"; exit 1 ;;
|
|
486
|
+
esac
|
|
487
|
+
done
|
|
488
|
+
|
|
489
|
+
_ensure_oversight_dirs
|
|
490
|
+
|
|
491
|
+
local count=0
|
|
492
|
+
for file in $(find "$OVERSIGHT_ROOT" -maxdepth 1 -name '*.json' -type f | sort -r); do
|
|
493
|
+
[[ ! -f "$file" ]] && continue
|
|
494
|
+
|
|
495
|
+
# Skip config and members files
|
|
496
|
+
local basename
|
|
497
|
+
basename=$(basename "$file")
|
|
498
|
+
if [[ "$basename" == "config.json" || "$basename" == "members.json" ]]; then
|
|
499
|
+
continue
|
|
500
|
+
fi
|
|
501
|
+
|
|
502
|
+
# Stop after limit
|
|
503
|
+
[[ $count -ge "$limit" ]] && break
|
|
504
|
+
|
|
505
|
+
local verdict
|
|
506
|
+
verdict=$(jq -r '.verdict' "$file" 2>/dev/null || echo "unknown")
|
|
507
|
+
|
|
508
|
+
if [[ -n "$filter" && "$verdict" != "$filter" ]]; then
|
|
509
|
+
continue
|
|
510
|
+
fi
|
|
511
|
+
|
|
512
|
+
local id
|
|
513
|
+
id="${basename%.json}"
|
|
514
|
+
local submitted
|
|
515
|
+
submitted=$(jq -r '.submitted_at' "$file" 2>/dev/null || echo "—")
|
|
516
|
+
local pr
|
|
517
|
+
pr=$(jq -r '.pr_number // "—"' "$file" 2>/dev/null)
|
|
518
|
+
|
|
519
|
+
echo "$id | $submitted | PR: $pr | Verdict: $verdict"
|
|
520
|
+
count=$((count + 1))
|
|
521
|
+
done
|
|
522
|
+
|
|
523
|
+
[[ $count -eq 0 ]] && echo "No reviews found"
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
# ─── Members List ────────────────────────────────────────────────────────
|
|
527
|
+
|
|
528
|
+
cmd_members() {
|
|
529
|
+
_init_members
|
|
530
|
+
|
|
531
|
+
echo ""
|
|
532
|
+
echo "═══════════════════════════════════════════════════════════════"
|
|
533
|
+
echo " Oversight Board Members"
|
|
534
|
+
echo "═══════════════════════════════════════════════════════════════"
|
|
535
|
+
echo ""
|
|
536
|
+
|
|
537
|
+
jq -r 'to_entries | .[] | "\(.value.role) (\(.key))\n Expertise: \(.value.expertise | join(", "))\n Reviews: \(.value.reviews) | Avg Confidence: \(.value.avg_confidence | tostring)\n"' "$MEMBERS_FILE"
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
# ─── Configuration ──────────────────────────────────────────────────────
|
|
541
|
+
|
|
542
|
+
cmd_config() {
|
|
543
|
+
local action="show"
|
|
544
|
+
local key=""
|
|
545
|
+
local value=""
|
|
546
|
+
|
|
547
|
+
while [[ $# -gt 0 ]]; do
|
|
548
|
+
case "$1" in
|
|
549
|
+
get) action="get"; shift ;;
|
|
550
|
+
set) action="set"; shift ;;
|
|
551
|
+
show) action="show"; shift ;;
|
|
552
|
+
-h|--help)
|
|
553
|
+
echo "Usage: oversight config [get|set|show] [key] [value]"
|
|
554
|
+
exit 0
|
|
555
|
+
;;
|
|
556
|
+
*)
|
|
557
|
+
if [[ "$action" == "get" && -z "$key" ]]; then
|
|
558
|
+
key="$1"; shift
|
|
559
|
+
elif [[ "$action" == "set" && -z "$key" ]]; then
|
|
560
|
+
key="$1"; shift
|
|
561
|
+
elif [[ "$action" == "set" && -z "$value" ]]; then
|
|
562
|
+
value="$1"; shift
|
|
563
|
+
else
|
|
564
|
+
error "Unknown option: $1"
|
|
565
|
+
exit 1
|
|
566
|
+
fi
|
|
567
|
+
;;
|
|
568
|
+
esac
|
|
569
|
+
done
|
|
570
|
+
|
|
571
|
+
_init_board_config
|
|
572
|
+
|
|
573
|
+
case "$action" in
|
|
574
|
+
get)
|
|
575
|
+
if [[ -z "$key" ]]; then
|
|
576
|
+
error "Provide key for get"
|
|
577
|
+
exit 1
|
|
578
|
+
fi
|
|
579
|
+
jq -r ".$key // \"not found\"" "$BOARD_CONFIG"
|
|
580
|
+
;;
|
|
581
|
+
set)
|
|
582
|
+
if [[ -z "$key" || -z "$value" ]]; then
|
|
583
|
+
error "Provide key and value for set"
|
|
584
|
+
exit 1
|
|
585
|
+
fi
|
|
586
|
+
local tmp_file="${BOARD_CONFIG}.tmp"
|
|
587
|
+
jq ".$key = \"$value\"" "$BOARD_CONFIG" > "$tmp_file"
|
|
588
|
+
mv "$tmp_file" "$BOARD_CONFIG"
|
|
589
|
+
success "Config updated: $key = $value"
|
|
590
|
+
;;
|
|
591
|
+
show)
|
|
592
|
+
jq '.' "$BOARD_CONFIG"
|
|
593
|
+
;;
|
|
594
|
+
esac
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
# ─── Appeal Process ─────────────────────────────────────────────────────
|
|
598
|
+
|
|
599
|
+
cmd_appeal() {
|
|
600
|
+
local review_id=""
|
|
601
|
+
local message=""
|
|
602
|
+
|
|
603
|
+
while [[ $# -gt 0 ]]; do
|
|
604
|
+
case "$1" in
|
|
605
|
+
--review) review_id="$2"; shift 2 ;;
|
|
606
|
+
--message) message="$2"; shift 2 ;;
|
|
607
|
+
-h|--help)
|
|
608
|
+
echo "Usage: oversight appeal --review <id> --message <text>"
|
|
609
|
+
exit 0
|
|
610
|
+
;;
|
|
611
|
+
*) error "Unknown option: $1"; exit 1 ;;
|
|
612
|
+
esac
|
|
613
|
+
done
|
|
614
|
+
|
|
615
|
+
if [[ -z "$review_id" || -z "$message" ]]; then
|
|
616
|
+
error "Require --review and --message"
|
|
617
|
+
exit 1
|
|
618
|
+
fi
|
|
619
|
+
|
|
620
|
+
local review_file="${OVERSIGHT_ROOT}/${review_id}.json"
|
|
621
|
+
if [[ ! -f "$review_file" ]]; then
|
|
622
|
+
error "Review not found: $review_id"
|
|
623
|
+
exit 1
|
|
624
|
+
fi
|
|
625
|
+
|
|
626
|
+
local verdict
|
|
627
|
+
verdict=$(jq -r '.verdict' "$review_file")
|
|
628
|
+
if [[ "$verdict" != "rejected" ]]; then
|
|
629
|
+
error "Can only appeal rejected reviews"
|
|
630
|
+
exit 1
|
|
631
|
+
fi
|
|
632
|
+
|
|
633
|
+
local appeal_count
|
|
634
|
+
appeal_count=$(jq '.appeals | length' "$review_file" 2>/dev/null || echo 0)
|
|
635
|
+
|
|
636
|
+
local max_appeals
|
|
637
|
+
max_appeals=$(jq -r '.appeal_max_attempts // 3' "$BOARD_CONFIG")
|
|
638
|
+
|
|
639
|
+
if [[ $appeal_count -ge $max_appeals ]]; then
|
|
640
|
+
error "Maximum appeal attempts reached ($max_appeals)"
|
|
641
|
+
exit 1
|
|
642
|
+
fi
|
|
643
|
+
|
|
644
|
+
local tmp_file="${review_file}.tmp"
|
|
645
|
+
jq --arg message "$message" '.appeals += [{"message": $message, "appealed_at": "'$(now_iso)'"}]' "$review_file" > "$tmp_file"
|
|
646
|
+
mv "$tmp_file" "$review_file"
|
|
647
|
+
|
|
648
|
+
success "Appeal submitted ($((appeal_count + 1))/$max_appeals)"
|
|
649
|
+
emit_event "oversight_appeal_submitted" "review_id=$review_id" "appeal_number=$((appeal_count + 1))"
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
# ─── Statistics ──────────────────────────────────────────────────────────
|
|
653
|
+
|
|
654
|
+
cmd_stats() {
|
|
655
|
+
_ensure_oversight_dirs
|
|
656
|
+
|
|
657
|
+
local total_reviews=0
|
|
658
|
+
local approved=0
|
|
659
|
+
local rejected=0
|
|
660
|
+
local pending=0
|
|
661
|
+
|
|
662
|
+
for file in "$OVERSIGHT_ROOT"/*.json; do
|
|
663
|
+
[[ -f "$file" ]] || continue
|
|
664
|
+
|
|
665
|
+
# Skip config and members files
|
|
666
|
+
local basename
|
|
667
|
+
basename=$(basename "$file")
|
|
668
|
+
if [[ "$basename" == "config.json" || "$basename" == "members.json" ]]; then
|
|
669
|
+
continue
|
|
670
|
+
fi
|
|
671
|
+
|
|
672
|
+
total_reviews=$((total_reviews + 1))
|
|
673
|
+
|
|
674
|
+
local verdict
|
|
675
|
+
verdict=$(jq -r '.verdict' "$file" 2>/dev/null || echo "unknown")
|
|
676
|
+
case "$verdict" in
|
|
677
|
+
approved) approved=$((approved + 1)) ;;
|
|
678
|
+
rejected) rejected=$((rejected + 1)) ;;
|
|
679
|
+
pending) pending=$((pending + 1)) ;;
|
|
680
|
+
esac
|
|
681
|
+
done
|
|
682
|
+
|
|
683
|
+
echo ""
|
|
684
|
+
echo "═══════════════════════════════════════════════════════════════"
|
|
685
|
+
echo " Oversight Board Statistics"
|
|
686
|
+
echo "═══════════════════════════════════════════════════════════════"
|
|
687
|
+
echo ""
|
|
688
|
+
echo "Total Reviews: $total_reviews"
|
|
689
|
+
echo " Approved: $approved"
|
|
690
|
+
echo " Rejected: $rejected"
|
|
691
|
+
echo " Pending: $pending"
|
|
692
|
+
echo ""
|
|
693
|
+
|
|
694
|
+
if [[ $total_reviews -gt 0 ]]; then
|
|
695
|
+
local approval_rate
|
|
696
|
+
local total_decided=$((approved + rejected))
|
|
697
|
+
if [[ $total_decided -gt 0 ]]; then
|
|
698
|
+
approval_rate=$(echo "scale=1; $approved * 100 / $total_decided" | bc 2>/dev/null || echo "N/A")
|
|
699
|
+
echo "Approval Rate: ${approval_rate}%"
|
|
700
|
+
fi
|
|
701
|
+
fi
|
|
702
|
+
echo ""
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
# ─── Help ────────────────────────────────────────────────────────────────
|
|
706
|
+
|
|
707
|
+
show_help() {
|
|
708
|
+
echo ""
|
|
709
|
+
echo -e "${CYAN}${BOLD}shipwright oversight${RESET} — Quality Oversight Board"
|
|
710
|
+
echo ""
|
|
711
|
+
echo -e "${BOLD}USAGE${RESET}"
|
|
712
|
+
echo -e " ${CYAN}oversight${RESET} <command> [options]"
|
|
713
|
+
echo ""
|
|
714
|
+
echo -e "${BOLD}COMMANDS${RESET}"
|
|
715
|
+
echo -e " ${CYAN}review${RESET} Submit changes for board review (--pr, --commit, or --diff)"
|
|
716
|
+
echo -e " ${CYAN}vote${RESET} Record a vote (--review, --reviewer, --decision)"
|
|
717
|
+
echo -e " ${CYAN}verdict${RESET} Show review status and votes"
|
|
718
|
+
echo -e " ${CYAN}history${RESET} List past reviews and outcomes"
|
|
719
|
+
echo -e " ${CYAN}members${RESET} Show board members and specialties"
|
|
720
|
+
echo -e " ${CYAN}config${RESET} Get/set board configuration"
|
|
721
|
+
echo -e " ${CYAN}appeal${RESET} Appeal a rejected review"
|
|
722
|
+
echo -e " ${CYAN}stats${RESET} Review board statistics"
|
|
723
|
+
echo -e " ${CYAN}help${RESET} Show this help message"
|
|
724
|
+
echo ""
|
|
725
|
+
echo -e "${BOLD}EXAMPLES${RESET}"
|
|
726
|
+
echo -e " ${DIM}shipwright oversight review --pr 42 --description \"Feature: Auth\"${RESET}"
|
|
727
|
+
echo -e " ${DIM}shipwright oversight vote --review <id> --reviewer security --decision approve${RESET}"
|
|
728
|
+
echo -e " ${DIM}shipwright oversight verdict --review <id>${RESET}"
|
|
729
|
+
echo -e " ${DIM}shipwright oversight stats${RESET}"
|
|
730
|
+
echo ""
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
# ─── Main ────────────────────────────────────────────────────────────────
|
|
734
|
+
|
|
735
|
+
main() {
|
|
736
|
+
local cmd="${1:-help}"
|
|
737
|
+
shift 2>/dev/null || true
|
|
738
|
+
|
|
739
|
+
_ensure_oversight_dirs
|
|
740
|
+
|
|
741
|
+
case "$cmd" in
|
|
742
|
+
review) cmd_review "$@" ;;
|
|
743
|
+
vote) cmd_vote "$@" ;;
|
|
744
|
+
gate) cmd_gate "$@" ;;
|
|
745
|
+
verdict) cmd_verdict "$@" ;;
|
|
746
|
+
history) cmd_history "$@" ;;
|
|
747
|
+
members) cmd_members "$@" ;;
|
|
748
|
+
config) cmd_config "$@" ;;
|
|
749
|
+
appeal) cmd_appeal "$@" ;;
|
|
750
|
+
stats) cmd_stats "$@" ;;
|
|
751
|
+
help|--help|-h)
|
|
752
|
+
show_help
|
|
753
|
+
;;
|
|
754
|
+
*)
|
|
755
|
+
error "Unknown command: $cmd"
|
|
756
|
+
show_help
|
|
757
|
+
exit 1
|
|
758
|
+
;;
|
|
759
|
+
esac
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then
|
|
763
|
+
main "$@"
|
|
764
|
+
fi
|