shipwright-cli 3.0.0 → 3.2.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 (143) hide show
  1. package/README.md +21 -7
  2. package/completions/_shipwright +247 -93
  3. package/completions/shipwright.bash +69 -15
  4. package/completions/shipwright.fish +309 -41
  5. package/config/decision-tiers.json +55 -0
  6. package/config/defaults.json +25 -2
  7. package/config/event-schema.json +142 -5
  8. package/config/policy.json +8 -0
  9. package/dashboard/public/index.html +6 -0
  10. package/dashboard/public/styles.css +76 -0
  11. package/dashboard/server.ts +51 -0
  12. package/dashboard/src/core/api.ts +5 -0
  13. package/dashboard/src/types/api.ts +10 -0
  14. package/dashboard/src/views/metrics.ts +69 -1
  15. package/package.json +3 -3
  16. package/scripts/lib/architecture.sh +2 -1
  17. package/scripts/lib/bootstrap.sh +0 -0
  18. package/scripts/lib/config.sh +0 -0
  19. package/scripts/lib/daemon-adaptive.sh +4 -2
  20. package/scripts/lib/daemon-dispatch.sh +24 -1
  21. package/scripts/lib/daemon-failure.sh +0 -0
  22. package/scripts/lib/daemon-health.sh +0 -0
  23. package/scripts/lib/daemon-patrol.sh +42 -7
  24. package/scripts/lib/daemon-poll.sh +17 -0
  25. package/scripts/lib/daemon-state.sh +17 -0
  26. package/scripts/lib/daemon-triage.sh +1 -1
  27. package/scripts/lib/decide-autonomy.sh +295 -0
  28. package/scripts/lib/decide-scoring.sh +228 -0
  29. package/scripts/lib/decide-signals.sh +462 -0
  30. package/scripts/lib/fleet-failover.sh +0 -0
  31. package/scripts/lib/helpers.sh +19 -18
  32. package/scripts/lib/pipeline-detection.sh +1 -1
  33. package/scripts/lib/pipeline-github.sh +0 -0
  34. package/scripts/lib/pipeline-intelligence.sh +23 -4
  35. package/scripts/lib/pipeline-quality-checks.sh +11 -6
  36. package/scripts/lib/pipeline-quality.sh +0 -0
  37. package/scripts/lib/pipeline-stages.sh +330 -33
  38. package/scripts/lib/pipeline-state.sh +14 -0
  39. package/scripts/lib/policy.sh +0 -0
  40. package/scripts/lib/test-helpers.sh +0 -0
  41. package/scripts/postinstall.mjs +75 -1
  42. package/scripts/signals/example-collector.sh +36 -0
  43. package/scripts/sw +8 -4
  44. package/scripts/sw-activity.sh +1 -7
  45. package/scripts/sw-adaptive.sh +7 -7
  46. package/scripts/sw-adversarial.sh +1 -1
  47. package/scripts/sw-architecture-enforcer.sh +1 -1
  48. package/scripts/sw-auth.sh +1 -1
  49. package/scripts/sw-autonomous.sh +1 -1
  50. package/scripts/sw-changelog.sh +1 -1
  51. package/scripts/sw-checkpoint.sh +1 -1
  52. package/scripts/sw-ci.sh +11 -6
  53. package/scripts/sw-cleanup.sh +1 -1
  54. package/scripts/sw-code-review.sh +36 -17
  55. package/scripts/sw-connect.sh +1 -1
  56. package/scripts/sw-context.sh +1 -1
  57. package/scripts/sw-cost.sh +71 -5
  58. package/scripts/sw-daemon.sh +6 -3
  59. package/scripts/sw-dashboard.sh +1 -1
  60. package/scripts/sw-db.sh +53 -38
  61. package/scripts/sw-decide.sh +685 -0
  62. package/scripts/sw-decompose.sh +1 -1
  63. package/scripts/sw-deps.sh +1 -1
  64. package/scripts/sw-developer-simulation.sh +1 -1
  65. package/scripts/sw-discovery.sh +80 -4
  66. package/scripts/sw-doc-fleet.sh +1 -1
  67. package/scripts/sw-docs-agent.sh +1 -1
  68. package/scripts/sw-docs.sh +1 -1
  69. package/scripts/sw-doctor.sh +1 -1
  70. package/scripts/sw-dora.sh +1 -1
  71. package/scripts/sw-durable.sh +9 -5
  72. package/scripts/sw-e2e-orchestrator.sh +1 -1
  73. package/scripts/sw-eventbus.sh +7 -4
  74. package/scripts/sw-evidence.sh +1 -1
  75. package/scripts/sw-feedback.sh +1 -1
  76. package/scripts/sw-fix.sh +1 -1
  77. package/scripts/sw-fleet-discover.sh +1 -1
  78. package/scripts/sw-fleet-viz.sh +6 -4
  79. package/scripts/sw-fleet.sh +1 -1
  80. package/scripts/sw-github-app.sh +3 -2
  81. package/scripts/sw-github-checks.sh +1 -1
  82. package/scripts/sw-github-deploy.sh +1 -1
  83. package/scripts/sw-github-graphql.sh +1 -1
  84. package/scripts/sw-guild.sh +1 -1
  85. package/scripts/sw-heartbeat.sh +1 -1
  86. package/scripts/sw-hygiene.sh +5 -3
  87. package/scripts/sw-incident.sh +9 -5
  88. package/scripts/sw-init.sh +1 -1
  89. package/scripts/sw-instrument.sh +1 -1
  90. package/scripts/sw-intelligence.sh +11 -6
  91. package/scripts/sw-jira.sh +1 -1
  92. package/scripts/sw-launchd.sh +1 -1
  93. package/scripts/sw-linear.sh +1 -1
  94. package/scripts/sw-logs.sh +1 -1
  95. package/scripts/sw-loop.sh +338 -32
  96. package/scripts/sw-memory.sh +23 -6
  97. package/scripts/sw-mission-control.sh +1 -1
  98. package/scripts/sw-model-router.sh +3 -2
  99. package/scripts/sw-otel.sh +8 -4
  100. package/scripts/sw-oversight.sh +1 -1
  101. package/scripts/sw-pipeline-composer.sh +3 -1
  102. package/scripts/sw-pipeline-vitals.sh +11 -6
  103. package/scripts/sw-pipeline.sh +92 -8
  104. package/scripts/sw-pm.sh +5 -4
  105. package/scripts/sw-pr-lifecycle.sh +7 -4
  106. package/scripts/sw-predictive.sh +11 -5
  107. package/scripts/sw-prep.sh +1 -1
  108. package/scripts/sw-ps.sh +1 -1
  109. package/scripts/sw-public-dashboard.sh +3 -2
  110. package/scripts/sw-quality.sh +21 -10
  111. package/scripts/sw-reaper.sh +1 -1
  112. package/scripts/sw-recruit.sh +1 -1
  113. package/scripts/sw-regression.sh +1 -1
  114. package/scripts/sw-release-manager.sh +1 -1
  115. package/scripts/sw-release.sh +1 -1
  116. package/scripts/sw-remote.sh +1 -1
  117. package/scripts/sw-replay.sh +1 -1
  118. package/scripts/sw-retro.sh +1 -1
  119. package/scripts/sw-review-rerun.sh +1 -1
  120. package/scripts/sw-scale.sh +69 -11
  121. package/scripts/sw-security-audit.sh +1 -1
  122. package/scripts/sw-self-optimize.sh +168 -4
  123. package/scripts/sw-session.sh +3 -3
  124. package/scripts/sw-setup.sh +1 -1
  125. package/scripts/sw-standup.sh +1 -1
  126. package/scripts/sw-status.sh +1 -1
  127. package/scripts/sw-strategic.sh +11 -6
  128. package/scripts/sw-stream.sh +7 -4
  129. package/scripts/sw-swarm.sh +3 -2
  130. package/scripts/sw-team-stages.sh +1 -1
  131. package/scripts/sw-templates.sh +3 -3
  132. package/scripts/sw-testgen.sh +11 -6
  133. package/scripts/sw-tmux-pipeline.sh +1 -1
  134. package/scripts/sw-tmux.sh +35 -1
  135. package/scripts/sw-trace.sh +1 -1
  136. package/scripts/sw-tracker.sh +1 -1
  137. package/scripts/sw-triage.sh +7 -7
  138. package/scripts/sw-upgrade.sh +1 -1
  139. package/scripts/sw-ux.sh +1 -1
  140. package/scripts/sw-webhook.sh +3 -2
  141. package/scripts/sw-widgets.sh +7 -4
  142. package/scripts/sw-worktree.sh +1 -1
  143. package/scripts/update-homebrew-sha.sh +21 -15
@@ -0,0 +1,685 @@
1
+ #!/usr/bin/env bash
2
+ # ╔═══════════════════════════════════════════════════════════════════════════╗
3
+ # ║ Shipwright Autonomous Decision Engine ║
4
+ # ║ Collects signals, scores value, enforces tiered autonomy, learns ║
5
+ # ╚═══════════════════════════════════════════════════════════════════════════╝
6
+ set -euo pipefail
7
+
8
+ VERSION="3.2.0"
9
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
10
+
11
+ # ─── Dependencies ─────────────────────────────────────────────────────────────
12
+ source "$SCRIPT_DIR/lib/helpers.sh"
13
+ [[ -f "$SCRIPT_DIR/lib/compat.sh" ]] && source "$SCRIPT_DIR/lib/compat.sh"
14
+ source "$SCRIPT_DIR/lib/policy.sh"
15
+ source "$SCRIPT_DIR/lib/decide-signals.sh"
16
+ source "$SCRIPT_DIR/lib/decide-scoring.sh"
17
+ source "$SCRIPT_DIR/lib/decide-autonomy.sh"
18
+
19
+ # ─── Config ───────────────────────────────────────────────────────────────────
20
+ DECISION_ENABLED=$(policy_get ".decision.enabled" "false")
21
+ DEDUP_WINDOW_DAYS=$(policy_get ".decision.dedup_window_days" "7")
22
+ OUTCOME_LEARNING=$(policy_get ".decision.outcome_learning_enabled" "true")
23
+ OUTCOME_MIN_SAMPLES=$(policy_get ".decision.outcome_min_samples" "10")
24
+
25
+ REPO_DIR="${_REPO_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || echo '.')}"
26
+ DRAFTS_DIR="${REPO_DIR}/.claude/decision-drafts"
27
+
28
+ # ─── Help ─────────────────────────────────────────────────────────────────────
29
+
30
+ show_help() {
31
+ echo -e "${CYAN}${BOLD}shipwright decide${RESET} — Autonomous Decision Engine"
32
+ echo ""
33
+ echo -e "${BOLD}Usage:${RESET} shipwright decide <command> [options]"
34
+ echo ""
35
+ echo -e "${BOLD}Commands:${RESET}"
36
+ echo -e " ${CYAN}run${RESET} [--dry-run] [--once] Run a decision cycle"
37
+ echo -e " ${CYAN}status${RESET} Show today's decisions and limits"
38
+ echo -e " ${CYAN}log${RESET} [--days N] Decision history with outcomes"
39
+ echo -e " ${CYAN}tiers${RESET} Show configured autonomy tiers"
40
+ echo -e " ${CYAN}candidates${RESET} [--signal X] Show current candidates without acting"
41
+ echo -e " ${CYAN}approve${RESET} <id> Approve a proposed candidate"
42
+ echo -e " ${CYAN}reject${RESET} <id> [--reason ..] Reject with feedback"
43
+ echo -e " ${CYAN}tune${RESET} Run outcome-based weight adjustment"
44
+ echo -e " ${CYAN}halt${RESET} Emergency halt all decisions"
45
+ echo -e " ${CYAN}resume${RESET} Resume after halt"
46
+ echo -e " ${CYAN}help${RESET} Show this help"
47
+ echo ""
48
+ echo -e "${BOLD}Examples:${RESET}"
49
+ echo -e " ${DIM}shipwright decide run --dry-run${RESET} Preview decisions without creating issues"
50
+ echo -e " ${DIM}shipwright decide candidates${RESET} See what the engine would propose"
51
+ echo -e " ${DIM}shipwright decide status${RESET} Check daily limits and recent decisions"
52
+ echo ""
53
+ }
54
+
55
+ # ─── Deduplication ────────────────────────────────────────────────────────────
56
+
57
+ _dedup_against_issues() {
58
+ local candidates="$1"
59
+
60
+ # Deduplicate against open GitHub issues
61
+ local open_titles=""
62
+ if [[ "${NO_GITHUB:-false}" != "true" ]]; then
63
+ open_titles=$(gh issue list --label "shipwright" --state open --json title -q '.[].title' 2>/dev/null || echo "")
64
+ fi
65
+
66
+ # Deduplicate against recent decisions (DEDUP_WINDOW_DAYS)
67
+ local recent_dedup_keys=""
68
+ local window_seconds=$((DEDUP_WINDOW_DAYS * 86400))
69
+ local cutoff=$(($(now_epoch) - window_seconds))
70
+ for log_file in "${DECISIONS_DIR}"/daily-log-*.jsonl; do
71
+ [[ -f "$log_file" ]] || continue
72
+ local file_keys
73
+ file_keys=$(jq -r 'select(.epoch // 0 >= '"$cutoff"') | .dedup_key // empty' "$log_file" 2>/dev/null || true)
74
+ recent_dedup_keys="${recent_dedup_keys}${file_keys}"$'\n'
75
+ done
76
+
77
+ # Filter candidates
78
+ echo "$candidates" | jq -c '.[]' 2>/dev/null | while IFS= read -r candidate; do
79
+ local dedup_key title
80
+ dedup_key=$(echo "$candidate" | jq -r '.dedup_key // ""')
81
+ title=$(echo "$candidate" | jq -r '.title // ""')
82
+
83
+ # Check against recent decision dedup keys
84
+ if [[ -n "$dedup_key" ]] && echo "$recent_dedup_keys" | grep -qF "$dedup_key" 2>/dev/null; then
85
+ continue
86
+ fi
87
+
88
+ # Check against open issue titles (substring match)
89
+ local is_dup=false
90
+ if [[ -n "$open_titles" && -n "$title" ]]; then
91
+ while IFS= read -r existing_title; do
92
+ [[ -z "$existing_title" ]] && continue
93
+ if [[ "$existing_title" == *"$title"* ]] || [[ "$title" == *"$existing_title"* ]]; then
94
+ is_dup=true
95
+ break
96
+ fi
97
+ done <<< "$open_titles"
98
+ fi
99
+
100
+ [[ "$is_dup" == "true" ]] && continue
101
+ echo "$candidate"
102
+ done | jq -s '.'
103
+ }
104
+
105
+ # ─── Execute Decision ────────────────────────────────────────────────────────
106
+
107
+ _execute_decision() {
108
+ local candidate="$1"
109
+ local tier="$2"
110
+ local dry_run="${3:-false}"
111
+
112
+ local id title category labels
113
+ id=$(echo "$candidate" | jq -r '.id')
114
+ title=$(echo "$candidate" | jq -r '.title')
115
+ category=$(echo "$candidate" | jq -r '.category')
116
+ labels=$(autonomy_get_labels "$tier")
117
+
118
+ local description
119
+ description=$(echo "$candidate" | jq -r '.description // ""')
120
+ local value_score
121
+ value_score=$(echo "$candidate" | jq -r '.value_score // 0')
122
+ local dedup_key
123
+ dedup_key=$(echo "$candidate" | jq -r '.dedup_key // ""')
124
+
125
+ local action=""
126
+ local issue_number=""
127
+
128
+ if [[ "$dry_run" == "true" ]]; then
129
+ case "$tier" in
130
+ auto) echo -e " ${GREEN}AUTO${RESET} [${value_score}] ${title}" ;;
131
+ propose) echo -e " ${YELLOW}PROPOSE${RESET} [${value_score}] ${title}" ;;
132
+ draft) echo -e " ${DIM}DRAFT${RESET} [${value_score}] ${title}" ;;
133
+ esac
134
+ return 0
135
+ fi
136
+
137
+ case "$tier" in
138
+ auto)
139
+ if [[ "${NO_GITHUB:-false}" != "true" ]]; then
140
+ local body
141
+ body="## ${title}
142
+
143
+ ${description}
144
+
145
+ | Field | Value |
146
+ |-------|-------|
147
+ | Category | \`${category}\` |
148
+ | Value Score | **${value_score}** |
149
+ | Decision ID | \`${id}\` |
150
+
151
+ Auto-created by \`shipwright decide\` at $(now_iso)."
152
+
153
+ issue_number=$(gh issue create \
154
+ --title "$title" \
155
+ --body "$body" \
156
+ --label "$labels" 2>/dev/null | grep -oE '[0-9]+$' || echo "")
157
+ action="issue_created"
158
+ success "AUTO: Created issue #${issue_number} — ${title}"
159
+ else
160
+ info "AUTO (local): ${title}"
161
+ action="issue_created_local"
162
+ fi
163
+ ;;
164
+ propose)
165
+ if [[ "${NO_GITHUB:-false}" != "true" ]]; then
166
+ local body
167
+ body="## ${title}
168
+
169
+ ${description}
170
+
171
+ | Field | Value |
172
+ |-------|-------|
173
+ | Category | \`${category}\` |
174
+ | Value Score | **${value_score}** |
175
+ | Decision ID | \`${id}\` |
176
+
177
+ > This issue was proposed by the decision engine. Add the \`ready-to-build\` label to approve.
178
+
179
+ Proposed by \`shipwright decide\` at $(now_iso)."
180
+
181
+ issue_number=$(gh issue create \
182
+ --title "$title" \
183
+ --body "$body" \
184
+ --label "$labels" 2>/dev/null | grep -oE '[0-9]+$' || echo "")
185
+ action="issue_proposed"
186
+ info "PROPOSE: Created issue #${issue_number} — ${title}"
187
+ else
188
+ info "PROPOSE (local): ${title}"
189
+ action="issue_proposed_local"
190
+ fi
191
+ ;;
192
+ draft)
193
+ mkdir -p "$DRAFTS_DIR"
194
+ local draft_file="${DRAFTS_DIR}/${id}.json"
195
+ local tmp
196
+ tmp=$(mktemp)
197
+ echo "$candidate" | jq '. + {tier: "draft", drafted_at: "'"$(now_iso)"'"}' > "$tmp" && mv "$tmp" "$draft_file"
198
+ action="draft_written"
199
+ echo -e " ${DIM}DRAFT: ${title} -> ${draft_file}${RESET}"
200
+ ;;
201
+ esac
202
+
203
+ # Record decision
204
+ local decision_record
205
+ decision_record=$(jq -n \
206
+ --arg id "$id" \
207
+ --arg title "$title" \
208
+ --arg category "$category" \
209
+ --arg tier "$tier" \
210
+ --arg action "$action" \
211
+ --arg issue "${issue_number:-}" \
212
+ --argjson score "$value_score" \
213
+ --arg dedup "$dedup_key" \
214
+ --arg ts "$(now_iso)" \
215
+ --argjson epoch "$(now_epoch)" \
216
+ '{id:$id, title:$title, category:$category, tier:$tier, action:$action, issue_number:$issue, value_score:$score, dedup_key:$dedup, decided_at:$ts, epoch:$epoch, estimated_cost_usd: (if $tier == "auto" then 5.0 elif $tier == "propose" then 0.01 else 0 end)}')
217
+
218
+ autonomy_record_decision "$decision_record"
219
+ emit_event "decision.executed" "id=$id" "tier=$tier" "action=$action" "score=$value_score"
220
+ }
221
+
222
+ # ─── Run Decision Cycle ──────────────────────────────────────────────────────
223
+
224
+ decide_run() {
225
+ local dry_run=false
226
+ local once=false
227
+ while [[ $# -gt 0 ]]; do
228
+ case "$1" in
229
+ --dry-run) dry_run=true; shift ;;
230
+ --once) once=true; shift ;;
231
+ *) shift ;;
232
+ esac
233
+ done
234
+
235
+ echo -e "${PURPLE}${BOLD}━━━ Decision Engine ━━━${RESET}"
236
+ echo ""
237
+
238
+ if [[ "$dry_run" == "true" ]]; then
239
+ echo -e " ${YELLOW}DRY RUN${RESET} — no issues will be created"
240
+ echo ""
241
+ fi
242
+
243
+ # Step 1: Check halt
244
+ if ! autonomy_check_halt; then
245
+ local halt_reason
246
+ halt_reason=$(jq -r '.reason // "unknown"' "$HALT_FILE" 2>/dev/null || echo "unknown")
247
+ error "Decision engine halted: ${halt_reason}"
248
+ echo -e " ${DIM}Run 'shipwright decide resume' to resume${RESET}"
249
+ return 1
250
+ fi
251
+
252
+ # Step 2: Rate limit
253
+ if [[ "$dry_run" != "true" ]] && ! autonomy_check_rate_limit; then
254
+ local last_ts
255
+ last_ts=$(jq -r '.decided_at // "unknown"' "$LAST_DECISION_FILE" 2>/dev/null || echo "unknown")
256
+ warn "Rate limited — last decision at ${last_ts}"
257
+ local cooldown
258
+ cooldown=$(echo "${TIER_LIMITS:-{}}" | jq -r '.cooldown_seconds // 300')
259
+ echo -e " ${DIM}Cooldown: ${cooldown}s between cycles${RESET}"
260
+ return 0
261
+ fi
262
+
263
+ # Step 3: Load tiers
264
+ if ! autonomy_load_tiers; then
265
+ error "Cannot load tier config — run 'shipwright decide tiers' to debug"
266
+ return 1
267
+ fi
268
+ scoring_load_weights
269
+
270
+ # Step 4: Collect signals
271
+ info "Collecting signals..."
272
+ local candidates
273
+ candidates=$(signals_collect_all)
274
+ local raw_count
275
+ raw_count=$(echo "$candidates" | jq 'length' 2>/dev/null || echo "0")
276
+ info "Found ${raw_count} raw candidate(s)"
277
+
278
+ if [[ "${raw_count:-0}" -eq 0 ]]; then
279
+ success "No candidates — nothing to decide"
280
+ emit_event "decision.cycle_complete" "candidates=0" "decisions=0"
281
+ return 0
282
+ fi
283
+
284
+ # Step 5: Deduplicate
285
+ info "Deduplicating..."
286
+ local unique_candidates
287
+ unique_candidates=$(_dedup_against_issues "$candidates")
288
+ local unique_count
289
+ unique_count=$(echo "$unique_candidates" | jq 'length' 2>/dev/null || echo "0")
290
+ info "${unique_count} candidate(s) after dedup"
291
+
292
+ if [[ "${unique_count:-0}" -eq 0 ]]; then
293
+ success "All candidates already tracked — nothing new"
294
+ emit_event "decision.cycle_complete" "candidates=0" "decisions=0"
295
+ return 0
296
+ fi
297
+
298
+ # Step 6: Score and sort
299
+ info "Scoring candidates..."
300
+ local scored_candidates="[]"
301
+ while IFS= read -r candidate; do
302
+ local scored
303
+ scored=$(score_candidate "$candidate")
304
+ scored_candidates=$(echo "$scored_candidates" | jq --argjson c "$scored" '. + [$c]')
305
+ done < <(echo "$unique_candidates" | jq -c '.[]' 2>/dev/null)
306
+
307
+ # Sort by value_score descending
308
+ scored_candidates=$(echo "$scored_candidates" | jq 'sort_by(-.value_score)')
309
+
310
+ # Step 7: Execute decisions
311
+ local decisions_made=0
312
+ echo ""
313
+ echo -e "${BOLD}Decisions:${RESET}"
314
+
315
+ while IFS= read -r candidate; do
316
+ local category risk_score
317
+ category=$(echo "$candidate" | jq -r '.category // "unknown"')
318
+ risk_score=$(echo "$candidate" | jq -r '.risk_score // 50')
319
+
320
+ # Resolve tier
321
+ local tier
322
+ tier=$(autonomy_resolve_tier "$category")
323
+
324
+ # Check risk ceiling
325
+ if ! autonomy_check_risk_ceiling "$category" "$risk_score"; then
326
+ local ceiling
327
+ ceiling=$(echo "$CATEGORY_RULES" | jq -r --arg cat "$category" '.[$cat].risk_ceiling // 100')
328
+ echo -e " ${DIM}SKIP (risk ${risk_score} > ceiling ${ceiling}): $(echo "$candidate" | jq -r '.title')${RESET}"
329
+ continue
330
+ fi
331
+
332
+ # Check budget
333
+ if [[ "$dry_run" != "true" ]] && ! autonomy_check_budget "$tier"; then
334
+ warn "Budget exhausted — stopping"
335
+ break
336
+ fi
337
+
338
+ # Execute
339
+ _execute_decision "$candidate" "$tier" "$dry_run"
340
+ decisions_made=$((decisions_made + 1))
341
+
342
+ done < <(echo "$scored_candidates" | jq -c '.[]' 2>/dev/null)
343
+
344
+ # Step 8: Check consecutive failures
345
+ if [[ "$dry_run" != "true" ]]; then
346
+ autonomy_check_consecutive_failures || true
347
+ fi
348
+
349
+ echo ""
350
+ echo -e "${PURPLE}${BOLD}━━━ Cycle Complete ━━━${RESET}"
351
+ echo -e " Candidates: ${raw_count} raw, ${unique_count} unique"
352
+ echo -e " Decisions: ${decisions_made}"
353
+ if [[ "$dry_run" == "true" ]]; then
354
+ echo -e " ${DIM}(dry run — no changes made)${RESET}"
355
+ fi
356
+ echo ""
357
+
358
+ emit_event "decision.cycle_complete" "candidates=${unique_count}" "decisions=${decisions_made}" "dry_run=$dry_run"
359
+
360
+ # Clear pending signals after successful cycle
361
+ if [[ "$dry_run" != "true" ]]; then
362
+ signals_clear_pending
363
+ fi
364
+ }
365
+
366
+ # ─── Status ───────────────────────────────────────────────────────────────────
367
+
368
+ decide_status() {
369
+ echo -e "${CYAN}${BOLD}Decision Engine Status${RESET}"
370
+ echo ""
371
+
372
+ # Halt state
373
+ if [[ -f "$HALT_FILE" ]]; then
374
+ local reason
375
+ reason=$(jq -r '.reason // "unknown"' "$HALT_FILE" 2>/dev/null || echo "unknown")
376
+ local halted_at
377
+ halted_at=$(jq -r '.halted_at // "unknown"' "$HALT_FILE" 2>/dev/null || echo "unknown")
378
+ echo -e " ${RED}${BOLD}HALTED${RESET}: ${reason}"
379
+ echo -e " ${DIM}Since: ${halted_at}${RESET}"
380
+ else
381
+ echo -e " Status: ${GREEN}active${RESET}"
382
+ fi
383
+
384
+ # Load tiers for limits
385
+ autonomy_load_tiers 2>/dev/null || true
386
+
387
+ echo ""
388
+ local summary
389
+ summary=$(autonomy_daily_summary)
390
+ local total auto propose draft remaining_issues
391
+ total=$(echo "$summary" | jq '.total // 0')
392
+ auto=$(echo "$summary" | jq '.auto // 0')
393
+ propose=$(echo "$summary" | jq '.propose // 0')
394
+ draft=$(echo "$summary" | jq '.draft // 0')
395
+ remaining_issues=$(echo "$summary" | jq '.budget_remaining.issues // 15')
396
+
397
+ echo -e " ${BOLD}Today's Decisions:${RESET}"
398
+ echo -e " Total: ${total}"
399
+ echo -e " Auto: ${auto}"
400
+ echo -e " Proposed: ${propose}"
401
+ echo -e " Drafted: ${draft}"
402
+ echo ""
403
+ echo -e " ${BOLD}Budget Remaining:${RESET}"
404
+ echo -e " Issues: ${remaining_issues}"
405
+ echo ""
406
+
407
+ # Weights
408
+ scoring_load_weights
409
+ echo -e " ${BOLD}Scoring Weights:${RESET}"
410
+ echo -e " Impact: ${_W_IMPACT}"
411
+ echo -e " Urgency: ${_W_URGENCY}"
412
+ echo -e " Effort: ${_W_EFFORT}"
413
+ echo -e " Confidence: ${_W_CONFIDENCE}"
414
+ echo -e " Risk: ${_W_RISK}"
415
+ echo ""
416
+ }
417
+
418
+ # ─── Log ──────────────────────────────────────────────────────────────────────
419
+
420
+ decide_log() {
421
+ local days=7
422
+ while [[ $# -gt 0 ]]; do
423
+ case "$1" in
424
+ --days) days="$2"; shift 2 ;;
425
+ *) shift ;;
426
+ esac
427
+ done
428
+
429
+ echo -e "${CYAN}${BOLD}Decision Log (last ${days} days)${RESET}"
430
+ echo ""
431
+
432
+ local found=false
433
+ for i in $(seq 0 $((days - 1))); do
434
+ local date_str
435
+ date_str=$(date -u -v-${i}d +%Y-%m-%d 2>/dev/null || date -u -d "${i} days ago" +%Y-%m-%d 2>/dev/null || continue)
436
+ local log_file="${DECISIONS_DIR}/daily-log-${date_str}.jsonl"
437
+ [[ ! -f "$log_file" ]] && continue
438
+
439
+ found=true
440
+ echo -e " ${BOLD}${date_str}${RESET}"
441
+ while IFS= read -r entry; do
442
+ local tier action title score outcome
443
+ tier=$(echo "$entry" | jq -r '.tier // "?"')
444
+ action=$(echo "$entry" | jq -r '.action // "?"')
445
+ title=$(echo "$entry" | jq -r '.title // "?"')
446
+ score=$(echo "$entry" | jq -r '.value_score // "?"')
447
+ outcome=$(echo "$entry" | jq -r '.outcome // "-"')
448
+
449
+ local tier_color="${DIM}"
450
+ case "$tier" in
451
+ auto) tier_color="${GREEN}" ;;
452
+ propose) tier_color="${YELLOW}" ;;
453
+ esac
454
+
455
+ echo -e " ${tier_color}${tier}${RESET} [${score}] ${title} ${DIM}(${outcome})${RESET}"
456
+ done < "$log_file"
457
+ echo ""
458
+ done
459
+
460
+ if [[ "$found" == "false" ]]; then
461
+ echo -e " ${DIM}No decisions in the last ${days} days${RESET}"
462
+ fi
463
+ }
464
+
465
+ # ─── Tiers ────────────────────────────────────────────────────────────────────
466
+
467
+ decide_tiers() {
468
+ echo -e "${CYAN}${BOLD}Autonomy Tiers${RESET}"
469
+ echo ""
470
+
471
+ if ! autonomy_load_tiers; then
472
+ error "Cannot load tiers config"
473
+ echo -e " ${DIM}Expected at: config/decision-tiers.json${RESET}"
474
+ return 1
475
+ fi
476
+
477
+ # Display tier definitions
478
+ for tier in auto propose draft; do
479
+ local desc labels
480
+ desc=$(echo "$TIERS_DATA" | jq -r --arg t "$tier" '.tiers[$t].description // "N/A"')
481
+ labels=$(echo "$TIERS_DATA" | jq -r --arg t "$tier" '.tiers[$t].labels // [] | join(", ")')
482
+ local color="${DIM}"
483
+ case "$tier" in
484
+ auto) color="${GREEN}" ;;
485
+ propose) color="${YELLOW}" ;;
486
+ draft) color="${DIM}" ;;
487
+ esac
488
+ echo -e " ${color}${BOLD}${tier}${RESET}: ${desc}"
489
+ [[ -n "$labels" ]] && echo -e " ${DIM}Labels: ${labels}${RESET}"
490
+ done
491
+ echo ""
492
+
493
+ # Display category rules
494
+ echo -e " ${BOLD}Category Rules:${RESET}"
495
+ echo "$CATEGORY_RULES" | jq -r 'to_entries[] | " \(.key): tier=\(.value.tier), ceiling=\(.value.risk_ceiling)"' 2>/dev/null
496
+ echo ""
497
+
498
+ # Display limits
499
+ echo -e " ${BOLD}Limits:${RESET}"
500
+ echo "$TIER_LIMITS" | jq -r 'to_entries[] | " \(.key): \(.value)"' 2>/dev/null
501
+ echo ""
502
+ }
503
+
504
+ # ─── Candidates ───────────────────────────────────────────────────────────────
505
+
506
+ decide_candidates() {
507
+ local signal_filter=""
508
+ while [[ $# -gt 0 ]]; do
509
+ case "$1" in
510
+ --signal) signal_filter="$2"; shift 2 ;;
511
+ *) shift ;;
512
+ esac
513
+ done
514
+
515
+ echo -e "${CYAN}${BOLD}Current Candidates${RESET}"
516
+ echo ""
517
+
518
+ if ! autonomy_load_tiers; then
519
+ error "Cannot load tiers config"
520
+ return 1
521
+ fi
522
+ scoring_load_weights
523
+
524
+ info "Collecting signals..."
525
+ local candidates
526
+ candidates=$(signals_collect_all)
527
+
528
+ if [[ -n "$signal_filter" ]]; then
529
+ candidates=$(echo "$candidates" | jq --arg s "$signal_filter" '[.[] | select(.signal == $s)]')
530
+ fi
531
+
532
+ local count
533
+ count=$(echo "$candidates" | jq 'length' 2>/dev/null || echo "0")
534
+ info "Found ${count} candidate(s)"
535
+
536
+ if [[ "${count:-0}" -eq 0 ]]; then
537
+ echo -e " ${DIM}No candidates found${RESET}"
538
+ return 0
539
+ fi
540
+
541
+ echo ""
542
+ while IFS= read -r candidate; do
543
+ local scored
544
+ scored=$(score_candidate "$candidate")
545
+ local title signal category score tier
546
+ title=$(echo "$scored" | jq -r '.title')
547
+ signal=$(echo "$scored" | jq -r '.signal')
548
+ category=$(echo "$scored" | jq -r '.category')
549
+ score=$(echo "$scored" | jq -r '.value_score')
550
+ tier=$(autonomy_resolve_tier "$category")
551
+
552
+ local color="${DIM}"
553
+ case "$tier" in
554
+ auto) color="${GREEN}" ;;
555
+ propose) color="${YELLOW}" ;;
556
+ esac
557
+
558
+ echo -e " ${color}${tier}${RESET} [${score}] ${title} ${DIM}(${signal}/${category})${RESET}"
559
+ done < <(echo "$candidates" | jq -c '.[]' 2>/dev/null)
560
+ echo ""
561
+ }
562
+
563
+ # ─── Approve / Reject ────────────────────────────────────────────────────────
564
+
565
+ decide_approve() {
566
+ local id="${1:-}"
567
+ if [[ -z "$id" ]]; then
568
+ error "Usage: shipwright decide approve <decision-id>"
569
+ return 1
570
+ fi
571
+
572
+ if [[ "${NO_GITHUB:-false}" == "true" ]]; then
573
+ error "Cannot approve in local mode (NO_GITHUB=true)"
574
+ return 1
575
+ fi
576
+
577
+ # Find the issue number for this decision
578
+ local daily_log
579
+ daily_log=$(_daily_log_file)
580
+ if [[ ! -f "$daily_log" ]]; then
581
+ error "No decisions today — check 'shipwright decide log'"
582
+ return 1
583
+ fi
584
+
585
+ local issue_number
586
+ issue_number=$(jq -r --arg id "$id" 'select(.id == $id) | .issue_number // empty' "$daily_log" 2>/dev/null | head -1)
587
+ if [[ -z "$issue_number" ]]; then
588
+ error "Decision '${id}' not found in today's log"
589
+ return 1
590
+ fi
591
+
592
+ gh issue edit "$issue_number" --add-label "ready-to-build" 2>/dev/null || {
593
+ error "Failed to add ready-to-build label to issue #${issue_number}"
594
+ return 1
595
+ }
596
+ success "Approved: issue #${issue_number} now has ready-to-build label"
597
+ autonomy_record_outcome "$id" "approved"
598
+ emit_event "decision.approved" "id=$id" "issue=$issue_number"
599
+ }
600
+
601
+ decide_reject() {
602
+ local id="${1:-}"
603
+ local reason=""
604
+ shift 2>/dev/null || true
605
+ while [[ $# -gt 0 ]]; do
606
+ case "$1" in
607
+ --reason) reason="$2"; shift 2 ;;
608
+ *) shift ;;
609
+ esac
610
+ done
611
+
612
+ if [[ -z "$id" ]]; then
613
+ error "Usage: shipwright decide reject <decision-id> [--reason \"...\"]"
614
+ return 1
615
+ fi
616
+
617
+ autonomy_record_outcome "$id" "rejected" "$reason"
618
+ success "Rejected: ${id}${reason:+ — $reason}"
619
+ emit_event "decision.rejected" "id=$id" "reason=$reason"
620
+ }
621
+
622
+ # ─── Tune ─────────────────────────────────────────────────────────────────────
623
+
624
+ decide_tune() {
625
+ echo -e "${CYAN}${BOLD}Outcome-Based Weight Tuning${RESET}"
626
+ echo ""
627
+
628
+ if [[ ! -f "${OUTCOMES_FILE}" ]]; then
629
+ warn "No outcomes recorded yet — need at least ${OUTCOME_MIN_SAMPLES} samples"
630
+ return 0
631
+ fi
632
+
633
+ local sample_count
634
+ sample_count=$(wc -l < "$OUTCOMES_FILE" 2>/dev/null | tr -d ' ')
635
+ info "Outcomes: ${sample_count}"
636
+
637
+ if [[ "$sample_count" -lt "$OUTCOME_MIN_SAMPLES" ]]; then
638
+ warn "Need ${OUTCOME_MIN_SAMPLES} samples, have ${sample_count} — skipping"
639
+ return 0
640
+ fi
641
+
642
+ scoring_load_weights
643
+ echo -e " ${BOLD}Before:${RESET} impact=${_W_IMPACT} urgency=${_W_URGENCY} effort=${_W_EFFORT} conf=${_W_CONFIDENCE} risk=${_W_RISK}"
644
+
645
+ # Process recent outcomes
646
+ local processed=0
647
+ while IFS= read -r outcome; do
648
+ scoring_update_weights "$outcome"
649
+ processed=$((processed + 1))
650
+ done < <(tail -20 "$OUTCOMES_FILE")
651
+
652
+ echo -e " ${BOLD}After:${RESET} impact=${_W_IMPACT} urgency=${_W_URGENCY} effort=${_W_EFFORT} conf=${_W_CONFIDENCE} risk=${_W_RISK}"
653
+ echo -e " ${DIM}Processed ${processed} outcome(s)${RESET}"
654
+
655
+ emit_event "decision.tuned" "samples=$processed"
656
+ success "Weights updated"
657
+ }
658
+
659
+ # ─── Command Router ──────────────────────────────────────────────────────────
660
+
661
+ main() {
662
+ local cmd="${1:-help}"
663
+ shift 2>/dev/null || true
664
+
665
+ case "$cmd" in
666
+ run) decide_run "$@" ;;
667
+ status) decide_status ;;
668
+ log) decide_log "$@" ;;
669
+ tiers) decide_tiers ;;
670
+ candidates) decide_candidates "$@" ;;
671
+ approve) decide_approve "$@" ;;
672
+ reject) decide_reject "$@" ;;
673
+ tune) decide_tune ;;
674
+ halt) autonomy_halt "${1:-manual halt}"; success "Decision engine halted" ;;
675
+ resume) autonomy_resume; success "Decision engine resumed" ;;
676
+ help|--help|-h) show_help ;;
677
+ *)
678
+ error "Unknown command: ${cmd}"
679
+ show_help
680
+ exit 1
681
+ ;;
682
+ esac
683
+ }
684
+
685
+ main "$@"
@@ -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="3.0.0"
9
+ VERSION="3.2.0"
10
10
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
11
11
  REPO_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
12
12