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.
- package/README.md +21 -7
- package/completions/_shipwright +247 -93
- package/completions/shipwright.bash +69 -15
- package/completions/shipwright.fish +309 -41
- package/config/decision-tiers.json +55 -0
- package/config/defaults.json +25 -2
- package/config/event-schema.json +142 -5
- package/config/policy.json +8 -0
- package/dashboard/public/index.html +6 -0
- package/dashboard/public/styles.css +76 -0
- package/dashboard/server.ts +51 -0
- package/dashboard/src/core/api.ts +5 -0
- package/dashboard/src/types/api.ts +10 -0
- package/dashboard/src/views/metrics.ts +69 -1
- package/package.json +3 -3
- package/scripts/lib/architecture.sh +2 -1
- package/scripts/lib/bootstrap.sh +0 -0
- package/scripts/lib/config.sh +0 -0
- package/scripts/lib/daemon-adaptive.sh +4 -2
- package/scripts/lib/daemon-dispatch.sh +24 -1
- package/scripts/lib/daemon-failure.sh +0 -0
- package/scripts/lib/daemon-health.sh +0 -0
- package/scripts/lib/daemon-patrol.sh +42 -7
- package/scripts/lib/daemon-poll.sh +17 -0
- package/scripts/lib/daemon-state.sh +17 -0
- package/scripts/lib/daemon-triage.sh +1 -1
- package/scripts/lib/decide-autonomy.sh +295 -0
- package/scripts/lib/decide-scoring.sh +228 -0
- package/scripts/lib/decide-signals.sh +462 -0
- package/scripts/lib/fleet-failover.sh +0 -0
- package/scripts/lib/helpers.sh +19 -18
- package/scripts/lib/pipeline-detection.sh +1 -1
- package/scripts/lib/pipeline-github.sh +0 -0
- package/scripts/lib/pipeline-intelligence.sh +23 -4
- package/scripts/lib/pipeline-quality-checks.sh +11 -6
- package/scripts/lib/pipeline-quality.sh +0 -0
- package/scripts/lib/pipeline-stages.sh +330 -33
- package/scripts/lib/pipeline-state.sh +14 -0
- package/scripts/lib/policy.sh +0 -0
- package/scripts/lib/test-helpers.sh +0 -0
- package/scripts/postinstall.mjs +75 -1
- package/scripts/signals/example-collector.sh +36 -0
- package/scripts/sw +8 -4
- package/scripts/sw-activity.sh +1 -7
- package/scripts/sw-adaptive.sh +7 -7
- package/scripts/sw-adversarial.sh +1 -1
- package/scripts/sw-architecture-enforcer.sh +1 -1
- package/scripts/sw-auth.sh +1 -1
- package/scripts/sw-autonomous.sh +1 -1
- package/scripts/sw-changelog.sh +1 -1
- package/scripts/sw-checkpoint.sh +1 -1
- package/scripts/sw-ci.sh +11 -6
- package/scripts/sw-cleanup.sh +1 -1
- package/scripts/sw-code-review.sh +36 -17
- package/scripts/sw-connect.sh +1 -1
- package/scripts/sw-context.sh +1 -1
- package/scripts/sw-cost.sh +71 -5
- package/scripts/sw-daemon.sh +6 -3
- package/scripts/sw-dashboard.sh +1 -1
- package/scripts/sw-db.sh +53 -38
- package/scripts/sw-decide.sh +685 -0
- package/scripts/sw-decompose.sh +1 -1
- package/scripts/sw-deps.sh +1 -1
- package/scripts/sw-developer-simulation.sh +1 -1
- package/scripts/sw-discovery.sh +80 -4
- package/scripts/sw-doc-fleet.sh +1 -1
- package/scripts/sw-docs-agent.sh +1 -1
- package/scripts/sw-docs.sh +1 -1
- package/scripts/sw-doctor.sh +1 -1
- package/scripts/sw-dora.sh +1 -1
- package/scripts/sw-durable.sh +9 -5
- package/scripts/sw-e2e-orchestrator.sh +1 -1
- package/scripts/sw-eventbus.sh +7 -4
- package/scripts/sw-evidence.sh +1 -1
- package/scripts/sw-feedback.sh +1 -1
- package/scripts/sw-fix.sh +1 -1
- package/scripts/sw-fleet-discover.sh +1 -1
- package/scripts/sw-fleet-viz.sh +6 -4
- package/scripts/sw-fleet.sh +1 -1
- package/scripts/sw-github-app.sh +3 -2
- package/scripts/sw-github-checks.sh +1 -1
- package/scripts/sw-github-deploy.sh +1 -1
- package/scripts/sw-github-graphql.sh +1 -1
- package/scripts/sw-guild.sh +1 -1
- package/scripts/sw-heartbeat.sh +1 -1
- package/scripts/sw-hygiene.sh +5 -3
- package/scripts/sw-incident.sh +9 -5
- package/scripts/sw-init.sh +1 -1
- package/scripts/sw-instrument.sh +1 -1
- package/scripts/sw-intelligence.sh +11 -6
- package/scripts/sw-jira.sh +1 -1
- package/scripts/sw-launchd.sh +1 -1
- package/scripts/sw-linear.sh +1 -1
- package/scripts/sw-logs.sh +1 -1
- package/scripts/sw-loop.sh +338 -32
- package/scripts/sw-memory.sh +23 -6
- package/scripts/sw-mission-control.sh +1 -1
- package/scripts/sw-model-router.sh +3 -2
- package/scripts/sw-otel.sh +8 -4
- package/scripts/sw-oversight.sh +1 -1
- package/scripts/sw-pipeline-composer.sh +3 -1
- package/scripts/sw-pipeline-vitals.sh +11 -6
- package/scripts/sw-pipeline.sh +92 -8
- package/scripts/sw-pm.sh +5 -4
- package/scripts/sw-pr-lifecycle.sh +7 -4
- package/scripts/sw-predictive.sh +11 -5
- package/scripts/sw-prep.sh +1 -1
- package/scripts/sw-ps.sh +1 -1
- package/scripts/sw-public-dashboard.sh +3 -2
- package/scripts/sw-quality.sh +21 -10
- package/scripts/sw-reaper.sh +1 -1
- package/scripts/sw-recruit.sh +1 -1
- package/scripts/sw-regression.sh +1 -1
- package/scripts/sw-release-manager.sh +1 -1
- package/scripts/sw-release.sh +1 -1
- package/scripts/sw-remote.sh +1 -1
- package/scripts/sw-replay.sh +1 -1
- package/scripts/sw-retro.sh +1 -1
- package/scripts/sw-review-rerun.sh +1 -1
- package/scripts/sw-scale.sh +69 -11
- package/scripts/sw-security-audit.sh +1 -1
- package/scripts/sw-self-optimize.sh +168 -4
- package/scripts/sw-session.sh +3 -3
- package/scripts/sw-setup.sh +1 -1
- package/scripts/sw-standup.sh +1 -1
- package/scripts/sw-status.sh +1 -1
- package/scripts/sw-strategic.sh +11 -6
- package/scripts/sw-stream.sh +7 -4
- package/scripts/sw-swarm.sh +3 -2
- package/scripts/sw-team-stages.sh +1 -1
- package/scripts/sw-templates.sh +3 -3
- package/scripts/sw-testgen.sh +11 -6
- package/scripts/sw-tmux-pipeline.sh +1 -1
- package/scripts/sw-tmux.sh +35 -1
- package/scripts/sw-trace.sh +1 -1
- package/scripts/sw-tracker.sh +1 -1
- package/scripts/sw-triage.sh +7 -7
- package/scripts/sw-upgrade.sh +1 -1
- package/scripts/sw-ux.sh +1 -1
- package/scripts/sw-webhook.sh +3 -2
- package/scripts/sw-widgets.sh +7 -4
- package/scripts/sw-worktree.sh +1 -1
- 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 "$@"
|
package/scripts/sw-decompose.sh
CHANGED