shipwright-cli 1.10.0 → 2.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.
Files changed (108) hide show
  1. package/README.md +114 -36
  2. package/completions/_shipwright +212 -32
  3. package/completions/shipwright.bash +97 -25
  4. package/docs/strategy/01-market-research.md +619 -0
  5. package/docs/strategy/02-mission-and-brand.md +587 -0
  6. package/docs/strategy/03-gtm-and-roadmap.md +759 -0
  7. package/docs/strategy/QUICK-START.txt +289 -0
  8. package/docs/strategy/README.md +172 -0
  9. package/package.json +4 -2
  10. package/scripts/sw +208 -1
  11. package/scripts/sw-activity.sh +500 -0
  12. package/scripts/sw-adaptive.sh +925 -0
  13. package/scripts/sw-adversarial.sh +1 -1
  14. package/scripts/sw-architecture-enforcer.sh +1 -1
  15. package/scripts/sw-auth.sh +613 -0
  16. package/scripts/sw-autonomous.sh +664 -0
  17. package/scripts/sw-changelog.sh +704 -0
  18. package/scripts/sw-checkpoint.sh +1 -1
  19. package/scripts/sw-ci.sh +602 -0
  20. package/scripts/sw-cleanup.sh +1 -1
  21. package/scripts/sw-code-review.sh +637 -0
  22. package/scripts/sw-connect.sh +1 -1
  23. package/scripts/sw-context.sh +605 -0
  24. package/scripts/sw-cost.sh +1 -1
  25. package/scripts/sw-daemon.sh +432 -130
  26. package/scripts/sw-dashboard.sh +1 -1
  27. package/scripts/sw-db.sh +540 -0
  28. package/scripts/sw-decompose.sh +539 -0
  29. package/scripts/sw-deps.sh +551 -0
  30. package/scripts/sw-developer-simulation.sh +1 -1
  31. package/scripts/sw-discovery.sh +412 -0
  32. package/scripts/sw-docs-agent.sh +539 -0
  33. package/scripts/sw-docs.sh +1 -1
  34. package/scripts/sw-doctor.sh +59 -1
  35. package/scripts/sw-dora.sh +615 -0
  36. package/scripts/sw-durable.sh +710 -0
  37. package/scripts/sw-e2e-orchestrator.sh +535 -0
  38. package/scripts/sw-eventbus.sh +393 -0
  39. package/scripts/sw-feedback.sh +471 -0
  40. package/scripts/sw-fix.sh +1 -1
  41. package/scripts/sw-fleet-discover.sh +567 -0
  42. package/scripts/sw-fleet-viz.sh +404 -0
  43. package/scripts/sw-fleet.sh +8 -1
  44. package/scripts/sw-github-app.sh +596 -0
  45. package/scripts/sw-github-checks.sh +1 -1
  46. package/scripts/sw-github-deploy.sh +1 -1
  47. package/scripts/sw-github-graphql.sh +1 -1
  48. package/scripts/sw-guild.sh +569 -0
  49. package/scripts/sw-heartbeat.sh +1 -1
  50. package/scripts/sw-hygiene.sh +559 -0
  51. package/scripts/sw-incident.sh +617 -0
  52. package/scripts/sw-init.sh +88 -1
  53. package/scripts/sw-instrument.sh +699 -0
  54. package/scripts/sw-intelligence.sh +1 -1
  55. package/scripts/sw-jira.sh +1 -1
  56. package/scripts/sw-launchd.sh +363 -28
  57. package/scripts/sw-linear.sh +1 -1
  58. package/scripts/sw-logs.sh +1 -1
  59. package/scripts/sw-loop.sh +64 -3
  60. package/scripts/sw-memory.sh +1 -1
  61. package/scripts/sw-mission-control.sh +487 -0
  62. package/scripts/sw-model-router.sh +545 -0
  63. package/scripts/sw-otel.sh +596 -0
  64. package/scripts/sw-oversight.sh +689 -0
  65. package/scripts/sw-pipeline-composer.sh +1 -1
  66. package/scripts/sw-pipeline-vitals.sh +1 -1
  67. package/scripts/sw-pipeline.sh +687 -24
  68. package/scripts/sw-pm.sh +693 -0
  69. package/scripts/sw-pr-lifecycle.sh +522 -0
  70. package/scripts/sw-predictive.sh +1 -1
  71. package/scripts/sw-prep.sh +1 -1
  72. package/scripts/sw-ps.sh +1 -1
  73. package/scripts/sw-public-dashboard.sh +798 -0
  74. package/scripts/sw-quality.sh +595 -0
  75. package/scripts/sw-reaper.sh +1 -1
  76. package/scripts/sw-recruit.sh +573 -0
  77. package/scripts/sw-regression.sh +642 -0
  78. package/scripts/sw-release-manager.sh +736 -0
  79. package/scripts/sw-release.sh +706 -0
  80. package/scripts/sw-remote.sh +1 -1
  81. package/scripts/sw-replay.sh +520 -0
  82. package/scripts/sw-retro.sh +691 -0
  83. package/scripts/sw-scale.sh +444 -0
  84. package/scripts/sw-security-audit.sh +505 -0
  85. package/scripts/sw-self-optimize.sh +1 -1
  86. package/scripts/sw-session.sh +1 -1
  87. package/scripts/sw-setup.sh +1 -1
  88. package/scripts/sw-standup.sh +712 -0
  89. package/scripts/sw-status.sh +1 -1
  90. package/scripts/sw-strategic.sh +658 -0
  91. package/scripts/sw-stream.sh +450 -0
  92. package/scripts/sw-swarm.sh +583 -0
  93. package/scripts/sw-team-stages.sh +511 -0
  94. package/scripts/sw-templates.sh +1 -1
  95. package/scripts/sw-testgen.sh +515 -0
  96. package/scripts/sw-tmux-pipeline.sh +554 -0
  97. package/scripts/sw-tmux.sh +1 -1
  98. package/scripts/sw-trace.sh +485 -0
  99. package/scripts/sw-tracker-github.sh +188 -0
  100. package/scripts/sw-tracker-jira.sh +172 -0
  101. package/scripts/sw-tracker-linear.sh +251 -0
  102. package/scripts/sw-tracker.sh +117 -2
  103. package/scripts/sw-triage.sh +603 -0
  104. package/scripts/sw-upgrade.sh +1 -1
  105. package/scripts/sw-ux.sh +677 -0
  106. package/scripts/sw-webhook.sh +627 -0
  107. package/scripts/sw-widgets.sh +530 -0
  108. package/scripts/sw-worktree.sh +1 -1
@@ -0,0 +1,689 @@
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.0.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
+ # ─── Verdict Display ──────────────────────────────────────────────────────
335
+
336
+ cmd_verdict() {
337
+ local review_id=""
338
+
339
+ while [[ $# -gt 0 ]]; do
340
+ case "$1" in
341
+ --review) review_id="$2"; shift 2 ;;
342
+ -h|--help)
343
+ echo "Usage: oversight verdict --review <id>"
344
+ exit 0
345
+ ;;
346
+ *) error "Unknown option: $1"; exit 1 ;;
347
+ esac
348
+ done
349
+
350
+ if [[ -z "$review_id" ]]; then
351
+ error "Require --review <id>"
352
+ exit 1
353
+ fi
354
+
355
+ local review_file="${OVERSIGHT_ROOT}/${review_id}.json"
356
+ if [[ ! -f "$review_file" ]]; then
357
+ error "Review not found: $review_id"
358
+ exit 1
359
+ fi
360
+
361
+ local verdict
362
+ verdict=$(jq -r '.verdict' "$review_file")
363
+ local confidence
364
+ confidence=$(jq -r '.confidence_score' "$review_file")
365
+
366
+ echo ""
367
+ echo "═══════════════════════════════════════════════════════════════"
368
+ echo " Review: $review_id"
369
+ echo "═══════════════════════════════════════════════════════════════"
370
+ echo ""
371
+
372
+ local votes
373
+ votes=$(jq '.votes' "$review_file")
374
+ echo "Board Votes:"
375
+ echo "$votes" | jq -r 'to_entries | .[] | " \(.key): \(.value.decision) (confidence: \(.value.confidence))\n Reasoning: \(.value.reasoning)"'
376
+ echo ""
377
+
378
+ case "$verdict" in
379
+ approved)
380
+ echo -e "${GREEN}${BOLD}✓ APPROVED${RESET}"
381
+ ;;
382
+ rejected)
383
+ echo -e "${RED}${BOLD}✗ REJECTED${RESET}"
384
+ ;;
385
+ pending)
386
+ echo -e "${YELLOW}${BOLD}⊙ PENDING${RESET}"
387
+ ;;
388
+ deadlock)
389
+ echo -e "${YELLOW}${BOLD}↔ DEADLOCK${RESET}"
390
+ ;;
391
+ esac
392
+
393
+ echo " Confidence: ${confidence}"
394
+ echo ""
395
+ }
396
+
397
+ # ─── History ─────────────────────────────────────────────────────────────
398
+
399
+ cmd_history() {
400
+ local limit=20
401
+ local filter=""
402
+
403
+ while [[ $# -gt 0 ]]; do
404
+ case "$1" in
405
+ --limit) limit="$2"; shift 2 ;;
406
+ --filter) filter="$2"; shift 2 ;;
407
+ -h|--help)
408
+ echo "Usage: oversight history [--limit <N>] [--filter <verdict>]"
409
+ exit 0
410
+ ;;
411
+ *) error "Unknown option: $1"; exit 1 ;;
412
+ esac
413
+ done
414
+
415
+ _ensure_oversight_dirs
416
+
417
+ local count=0
418
+ for file in $(find "$OVERSIGHT_ROOT" -maxdepth 1 -name '*.json' -type f | sort -r); do
419
+ [[ ! -f "$file" ]] && continue
420
+
421
+ # Skip config and members files
422
+ local basename
423
+ basename=$(basename "$file")
424
+ if [[ "$basename" == "config.json" || "$basename" == "members.json" ]]; then
425
+ continue
426
+ fi
427
+
428
+ # Stop after limit
429
+ [[ $count -ge "$limit" ]] && break
430
+
431
+ local verdict
432
+ verdict=$(jq -r '.verdict' "$file" 2>/dev/null || echo "unknown")
433
+
434
+ if [[ -n "$filter" && "$verdict" != "$filter" ]]; then
435
+ continue
436
+ fi
437
+
438
+ local id
439
+ id="${basename%.json}"
440
+ local submitted
441
+ submitted=$(jq -r '.submitted_at' "$file" 2>/dev/null || echo "—")
442
+ local pr
443
+ pr=$(jq -r '.pr_number // "—"' "$file" 2>/dev/null)
444
+
445
+ echo "$id | $submitted | PR: $pr | Verdict: $verdict"
446
+ count=$((count + 1))
447
+ done
448
+
449
+ [[ $count -eq 0 ]] && echo "No reviews found"
450
+ }
451
+
452
+ # ─── Members List ────────────────────────────────────────────────────────
453
+
454
+ cmd_members() {
455
+ _init_members
456
+
457
+ echo ""
458
+ echo "═══════════════════════════════════════════════════════════════"
459
+ echo " Oversight Board Members"
460
+ echo "═══════════════════════════════════════════════════════════════"
461
+ echo ""
462
+
463
+ 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"
464
+ }
465
+
466
+ # ─── Configuration ──────────────────────────────────────────────────────
467
+
468
+ cmd_config() {
469
+ local action="show"
470
+ local key=""
471
+ local value=""
472
+
473
+ while [[ $# -gt 0 ]]; do
474
+ case "$1" in
475
+ get) action="get"; shift ;;
476
+ set) action="set"; shift ;;
477
+ show) action="show"; shift ;;
478
+ -h|--help)
479
+ echo "Usage: oversight config [get|set|show] [key] [value]"
480
+ exit 0
481
+ ;;
482
+ *)
483
+ if [[ "$action" == "get" && -z "$key" ]]; then
484
+ key="$1"; shift
485
+ elif [[ "$action" == "set" && -z "$key" ]]; then
486
+ key="$1"; shift
487
+ elif [[ "$action" == "set" && -z "$value" ]]; then
488
+ value="$1"; shift
489
+ else
490
+ error "Unknown option: $1"
491
+ exit 1
492
+ fi
493
+ ;;
494
+ esac
495
+ done
496
+
497
+ _init_board_config
498
+
499
+ case "$action" in
500
+ get)
501
+ if [[ -z "$key" ]]; then
502
+ error "Provide key for get"
503
+ exit 1
504
+ fi
505
+ jq -r ".$key // \"not found\"" "$BOARD_CONFIG"
506
+ ;;
507
+ set)
508
+ if [[ -z "$key" || -z "$value" ]]; then
509
+ error "Provide key and value for set"
510
+ exit 1
511
+ fi
512
+ local tmp_file="${BOARD_CONFIG}.tmp"
513
+ jq ".$key = \"$value\"" "$BOARD_CONFIG" > "$tmp_file"
514
+ mv "$tmp_file" "$BOARD_CONFIG"
515
+ success "Config updated: $key = $value"
516
+ ;;
517
+ show)
518
+ jq '.' "$BOARD_CONFIG"
519
+ ;;
520
+ esac
521
+ }
522
+
523
+ # ─── Appeal Process ─────────────────────────────────────────────────────
524
+
525
+ cmd_appeal() {
526
+ local review_id=""
527
+ local message=""
528
+
529
+ while [[ $# -gt 0 ]]; do
530
+ case "$1" in
531
+ --review) review_id="$2"; shift 2 ;;
532
+ --message) message="$2"; shift 2 ;;
533
+ -h|--help)
534
+ echo "Usage: oversight appeal --review <id> --message <text>"
535
+ exit 0
536
+ ;;
537
+ *) error "Unknown option: $1"; exit 1 ;;
538
+ esac
539
+ done
540
+
541
+ if [[ -z "$review_id" || -z "$message" ]]; then
542
+ error "Require --review and --message"
543
+ exit 1
544
+ fi
545
+
546
+ local review_file="${OVERSIGHT_ROOT}/${review_id}.json"
547
+ if [[ ! -f "$review_file" ]]; then
548
+ error "Review not found: $review_id"
549
+ exit 1
550
+ fi
551
+
552
+ local verdict
553
+ verdict=$(jq -r '.verdict' "$review_file")
554
+ if [[ "$verdict" != "rejected" ]]; then
555
+ error "Can only appeal rejected reviews"
556
+ exit 1
557
+ fi
558
+
559
+ local appeal_count
560
+ appeal_count=$(jq '.appeals | length' "$review_file" 2>/dev/null || echo 0)
561
+
562
+ local max_appeals
563
+ max_appeals=$(jq -r '.appeal_max_attempts // 3' "$BOARD_CONFIG")
564
+
565
+ if [[ $appeal_count -ge $max_appeals ]]; then
566
+ error "Maximum appeal attempts reached ($max_appeals)"
567
+ exit 1
568
+ fi
569
+
570
+ local tmp_file="${review_file}.tmp"
571
+ jq --arg message "$message" '.appeals += [{"message": $message, "appealed_at": "'$(now_iso)'"}]' "$review_file" > "$tmp_file"
572
+ mv "$tmp_file" "$review_file"
573
+
574
+ success "Appeal submitted ($((appeal_count + 1))/$max_appeals)"
575
+ emit_event "oversight_appeal_submitted" "review_id=$review_id" "appeal_number=$((appeal_count + 1))"
576
+ }
577
+
578
+ # ─── Statistics ──────────────────────────────────────────────────────────
579
+
580
+ cmd_stats() {
581
+ _ensure_oversight_dirs
582
+
583
+ local total_reviews=0
584
+ local approved=0
585
+ local rejected=0
586
+ local pending=0
587
+
588
+ for file in "$OVERSIGHT_ROOT"/*.json; do
589
+ [[ -f "$file" ]] || continue
590
+
591
+ # Skip config and members files
592
+ local basename
593
+ basename=$(basename "$file")
594
+ if [[ "$basename" == "config.json" || "$basename" == "members.json" ]]; then
595
+ continue
596
+ fi
597
+
598
+ total_reviews=$((total_reviews + 1))
599
+
600
+ local verdict
601
+ verdict=$(jq -r '.verdict' "$file" 2>/dev/null || echo "unknown")
602
+ case "$verdict" in
603
+ approved) approved=$((approved + 1)) ;;
604
+ rejected) rejected=$((rejected + 1)) ;;
605
+ pending) pending=$((pending + 1)) ;;
606
+ esac
607
+ done
608
+
609
+ echo ""
610
+ echo "═══════════════════════════════════════════════════════════════"
611
+ echo " Oversight Board Statistics"
612
+ echo "═══════════════════════════════════════════════════════════════"
613
+ echo ""
614
+ echo "Total Reviews: $total_reviews"
615
+ echo " Approved: $approved"
616
+ echo " Rejected: $rejected"
617
+ echo " Pending: $pending"
618
+ echo ""
619
+
620
+ if [[ $total_reviews -gt 0 ]]; then
621
+ local approval_rate
622
+ local total_decided=$((approved + rejected))
623
+ if [[ $total_decided -gt 0 ]]; then
624
+ approval_rate=$(echo "scale=1; $approved * 100 / $total_decided" | bc 2>/dev/null || echo "N/A")
625
+ echo "Approval Rate: ${approval_rate}%"
626
+ fi
627
+ fi
628
+ echo ""
629
+ }
630
+
631
+ # ─── Help ────────────────────────────────────────────────────────────────
632
+
633
+ show_help() {
634
+ echo ""
635
+ echo -e "${CYAN}${BOLD}shipwright oversight${RESET} — Quality Oversight Board"
636
+ echo ""
637
+ echo -e "${BOLD}USAGE${RESET}"
638
+ echo -e " ${CYAN}oversight${RESET} <command> [options]"
639
+ echo ""
640
+ echo -e "${BOLD}COMMANDS${RESET}"
641
+ echo -e " ${CYAN}review${RESET} Submit changes for board review (--pr, --commit, or --diff)"
642
+ echo -e " ${CYAN}vote${RESET} Record a vote (--review, --reviewer, --decision)"
643
+ echo -e " ${CYAN}verdict${RESET} Show review status and votes"
644
+ echo -e " ${CYAN}history${RESET} List past reviews and outcomes"
645
+ echo -e " ${CYAN}members${RESET} Show board members and specialties"
646
+ echo -e " ${CYAN}config${RESET} Get/set board configuration"
647
+ echo -e " ${CYAN}appeal${RESET} Appeal a rejected review"
648
+ echo -e " ${CYAN}stats${RESET} Review board statistics"
649
+ echo -e " ${CYAN}help${RESET} Show this help message"
650
+ echo ""
651
+ echo -e "${BOLD}EXAMPLES${RESET}"
652
+ echo -e " ${DIM}shipwright oversight review --pr 42 --description \"Feature: Auth\"${RESET}"
653
+ echo -e " ${DIM}shipwright oversight vote --review <id> --reviewer security --decision approve${RESET}"
654
+ echo -e " ${DIM}shipwright oversight verdict --review <id>${RESET}"
655
+ echo -e " ${DIM}shipwright oversight stats${RESET}"
656
+ echo ""
657
+ }
658
+
659
+ # ─── Main ────────────────────────────────────────────────────────────────
660
+
661
+ main() {
662
+ local cmd="${1:-help}"
663
+ shift 2>/dev/null || true
664
+
665
+ _ensure_oversight_dirs
666
+
667
+ case "$cmd" in
668
+ review) cmd_review "$@" ;;
669
+ vote) cmd_vote "$@" ;;
670
+ verdict) cmd_verdict "$@" ;;
671
+ history) cmd_history "$@" ;;
672
+ members) cmd_members "$@" ;;
673
+ config) cmd_config "$@" ;;
674
+ appeal) cmd_appeal "$@" ;;
675
+ stats) cmd_stats "$@" ;;
676
+ help|--help|-h)
677
+ show_help
678
+ ;;
679
+ *)
680
+ error "Unknown command: $cmd"
681
+ show_help
682
+ exit 1
683
+ ;;
684
+ esac
685
+ }
686
+
687
+ if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then
688
+ main "$@"
689
+ fi
@@ -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="1.10.0"
9
+ VERSION="2.0.0"
10
10
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
11
11
  REPO_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
12
12