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.
Files changed (121) hide show
  1. package/README.md +221 -55
  2. package/completions/_shipwright +264 -32
  3. package/completions/shipwright.bash +118 -26
  4. package/completions/shipwright.fish +80 -2
  5. package/dashboard/server.ts +208 -0
  6. package/docs/strategy/01-market-research.md +619 -0
  7. package/docs/strategy/02-mission-and-brand.md +587 -0
  8. package/docs/strategy/03-gtm-and-roadmap.md +759 -0
  9. package/docs/strategy/QUICK-START.txt +289 -0
  10. package/docs/strategy/README.md +172 -0
  11. package/docs/tmux-research/TMUX-ARCHITECTURE.md +567 -0
  12. package/docs/tmux-research/TMUX-AUDIT.md +925 -0
  13. package/docs/tmux-research/TMUX-BEST-PRACTICES-2025-2026.md +829 -0
  14. package/docs/tmux-research/TMUX-QUICK-REFERENCE.md +543 -0
  15. package/docs/tmux-research/TMUX-RESEARCH-INDEX.md +438 -0
  16. package/package.json +4 -2
  17. package/scripts/lib/helpers.sh +7 -0
  18. package/scripts/sw +323 -2
  19. package/scripts/sw-activity.sh +500 -0
  20. package/scripts/sw-adaptive.sh +925 -0
  21. package/scripts/sw-adversarial.sh +1 -1
  22. package/scripts/sw-architecture-enforcer.sh +1 -1
  23. package/scripts/sw-auth.sh +613 -0
  24. package/scripts/sw-autonomous.sh +754 -0
  25. package/scripts/sw-changelog.sh +704 -0
  26. package/scripts/sw-checkpoint.sh +1 -1
  27. package/scripts/sw-ci.sh +602 -0
  28. package/scripts/sw-cleanup.sh +1 -1
  29. package/scripts/sw-code-review.sh +698 -0
  30. package/scripts/sw-connect.sh +1 -1
  31. package/scripts/sw-context.sh +605 -0
  32. package/scripts/sw-cost.sh +44 -3
  33. package/scripts/sw-daemon.sh +568 -138
  34. package/scripts/sw-dashboard.sh +1 -1
  35. package/scripts/sw-db.sh +1380 -0
  36. package/scripts/sw-decompose.sh +539 -0
  37. package/scripts/sw-deps.sh +551 -0
  38. package/scripts/sw-developer-simulation.sh +1 -1
  39. package/scripts/sw-discovery.sh +412 -0
  40. package/scripts/sw-docs-agent.sh +539 -0
  41. package/scripts/sw-docs.sh +1 -1
  42. package/scripts/sw-doctor.sh +107 -1
  43. package/scripts/sw-dora.sh +615 -0
  44. package/scripts/sw-durable.sh +710 -0
  45. package/scripts/sw-e2e-orchestrator.sh +535 -0
  46. package/scripts/sw-eventbus.sh +393 -0
  47. package/scripts/sw-feedback.sh +479 -0
  48. package/scripts/sw-fix.sh +1 -1
  49. package/scripts/sw-fleet-discover.sh +567 -0
  50. package/scripts/sw-fleet-viz.sh +404 -0
  51. package/scripts/sw-fleet.sh +8 -1
  52. package/scripts/sw-github-app.sh +596 -0
  53. package/scripts/sw-github-checks.sh +4 -4
  54. package/scripts/sw-github-deploy.sh +1 -1
  55. package/scripts/sw-github-graphql.sh +1 -1
  56. package/scripts/sw-guild.sh +569 -0
  57. package/scripts/sw-heartbeat.sh +1 -1
  58. package/scripts/sw-hygiene.sh +559 -0
  59. package/scripts/sw-incident.sh +656 -0
  60. package/scripts/sw-init.sh +237 -24
  61. package/scripts/sw-instrument.sh +699 -0
  62. package/scripts/sw-intelligence.sh +1 -1
  63. package/scripts/sw-jira.sh +1 -1
  64. package/scripts/sw-launchd.sh +363 -28
  65. package/scripts/sw-linear.sh +1 -1
  66. package/scripts/sw-logs.sh +1 -1
  67. package/scripts/sw-loop.sh +267 -21
  68. package/scripts/sw-memory.sh +18 -1
  69. package/scripts/sw-mission-control.sh +487 -0
  70. package/scripts/sw-model-router.sh +545 -0
  71. package/scripts/sw-otel.sh +596 -0
  72. package/scripts/sw-oversight.sh +764 -0
  73. package/scripts/sw-pipeline-composer.sh +1 -1
  74. package/scripts/sw-pipeline-vitals.sh +1 -1
  75. package/scripts/sw-pipeline.sh +947 -35
  76. package/scripts/sw-pm.sh +758 -0
  77. package/scripts/sw-pr-lifecycle.sh +522 -0
  78. package/scripts/sw-predictive.sh +8 -1
  79. package/scripts/sw-prep.sh +1 -1
  80. package/scripts/sw-ps.sh +1 -1
  81. package/scripts/sw-public-dashboard.sh +798 -0
  82. package/scripts/sw-quality.sh +595 -0
  83. package/scripts/sw-reaper.sh +1 -1
  84. package/scripts/sw-recruit.sh +2248 -0
  85. package/scripts/sw-regression.sh +642 -0
  86. package/scripts/sw-release-manager.sh +736 -0
  87. package/scripts/sw-release.sh +706 -0
  88. package/scripts/sw-remote.sh +1 -1
  89. package/scripts/sw-replay.sh +520 -0
  90. package/scripts/sw-retro.sh +691 -0
  91. package/scripts/sw-scale.sh +444 -0
  92. package/scripts/sw-security-audit.sh +505 -0
  93. package/scripts/sw-self-optimize.sh +1 -1
  94. package/scripts/sw-session.sh +1 -1
  95. package/scripts/sw-setup.sh +263 -127
  96. package/scripts/sw-standup.sh +712 -0
  97. package/scripts/sw-status.sh +44 -2
  98. package/scripts/sw-strategic.sh +806 -0
  99. package/scripts/sw-stream.sh +450 -0
  100. package/scripts/sw-swarm.sh +620 -0
  101. package/scripts/sw-team-stages.sh +511 -0
  102. package/scripts/sw-templates.sh +4 -4
  103. package/scripts/sw-testgen.sh +566 -0
  104. package/scripts/sw-tmux-pipeline.sh +554 -0
  105. package/scripts/sw-tmux-role-color.sh +58 -0
  106. package/scripts/sw-tmux-status.sh +128 -0
  107. package/scripts/sw-tmux.sh +1 -1
  108. package/scripts/sw-trace.sh +485 -0
  109. package/scripts/sw-tracker-github.sh +188 -0
  110. package/scripts/sw-tracker-jira.sh +172 -0
  111. package/scripts/sw-tracker-linear.sh +251 -0
  112. package/scripts/sw-tracker.sh +117 -2
  113. package/scripts/sw-triage.sh +627 -0
  114. package/scripts/sw-upgrade.sh +1 -1
  115. package/scripts/sw-ux.sh +677 -0
  116. package/scripts/sw-webhook.sh +627 -0
  117. package/scripts/sw-widgets.sh +530 -0
  118. package/scripts/sw-worktree.sh +1 -1
  119. package/templates/pipelines/autonomous.json +2 -2
  120. package/tmux/shipwright-overlay.conf +35 -17
  121. 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