shipwright-cli 1.7.1 → 1.10.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/.claude/agents/code-reviewer.md +90 -0
- package/.claude/agents/devops-engineer.md +142 -0
- package/.claude/agents/pipeline-agent.md +80 -0
- package/.claude/agents/shell-script-specialist.md +150 -0
- package/.claude/agents/test-specialist.md +196 -0
- package/.claude/hooks/post-tool-use.sh +45 -0
- package/.claude/hooks/pre-tool-use.sh +25 -0
- package/.claude/hooks/session-started.sh +37 -0
- package/README.md +212 -814
- package/claude-code/CLAUDE.md.shipwright +54 -0
- package/claude-code/hooks/notify-idle.sh +2 -2
- package/claude-code/hooks/session-start.sh +24 -0
- package/claude-code/hooks/task-completed.sh +6 -2
- package/claude-code/settings.json.template +12 -0
- package/dashboard/public/app.js +4422 -0
- package/dashboard/public/index.html +816 -0
- package/dashboard/public/styles.css +4755 -0
- package/dashboard/server.ts +4315 -0
- package/docs/KNOWN-ISSUES.md +18 -10
- package/docs/TIPS.md +38 -26
- package/docs/patterns/README.md +33 -23
- package/package.json +9 -5
- package/scripts/adapters/iterm2-adapter.sh +1 -1
- package/scripts/adapters/tmux-adapter.sh +52 -23
- package/scripts/adapters/wezterm-adapter.sh +26 -14
- package/scripts/lib/compat.sh +200 -0
- package/scripts/lib/helpers.sh +72 -0
- package/scripts/postinstall.mjs +72 -13
- package/scripts/{cct → sw} +118 -22
- package/scripts/sw-adversarial.sh +274 -0
- package/scripts/sw-architecture-enforcer.sh +330 -0
- package/scripts/sw-checkpoint.sh +468 -0
- package/scripts/sw-cleanup.sh +359 -0
- package/scripts/sw-connect.sh +619 -0
- package/scripts/{cct-cost.sh → sw-cost.sh} +368 -34
- package/scripts/sw-daemon.sh +5574 -0
- package/scripts/sw-dashboard.sh +477 -0
- package/scripts/sw-developer-simulation.sh +252 -0
- package/scripts/sw-docs.sh +635 -0
- package/scripts/sw-doctor.sh +907 -0
- package/scripts/{cct-fix.sh → sw-fix.sh} +10 -6
- package/scripts/{cct-fleet.sh → sw-fleet.sh} +498 -22
- package/scripts/sw-github-checks.sh +521 -0
- package/scripts/sw-github-deploy.sh +533 -0
- package/scripts/sw-github-graphql.sh +972 -0
- package/scripts/sw-heartbeat.sh +293 -0
- package/scripts/{cct-init.sh → sw-init.sh} +144 -11
- package/scripts/sw-intelligence.sh +1196 -0
- package/scripts/sw-jira.sh +643 -0
- package/scripts/sw-launchd.sh +364 -0
- package/scripts/sw-linear.sh +648 -0
- package/scripts/{cct-logs.sh → sw-logs.sh} +72 -2
- package/scripts/sw-loop.sh +2217 -0
- package/scripts/{cct-memory.sh → sw-memory.sh} +514 -36
- package/scripts/sw-patrol-meta.sh +417 -0
- package/scripts/sw-pipeline-composer.sh +455 -0
- package/scripts/sw-pipeline-vitals.sh +1096 -0
- package/scripts/sw-pipeline.sh +7593 -0
- package/scripts/sw-predictive.sh +820 -0
- package/scripts/{cct-prep.sh → sw-prep.sh} +339 -49
- package/scripts/{cct-ps.sh → sw-ps.sh} +9 -6
- package/scripts/{cct-reaper.sh → sw-reaper.sh} +10 -6
- package/scripts/sw-remote.sh +687 -0
- package/scripts/sw-self-optimize.sh +1048 -0
- package/scripts/sw-session.sh +541 -0
- package/scripts/sw-setup.sh +234 -0
- package/scripts/sw-status.sh +796 -0
- package/scripts/{cct-templates.sh → sw-templates.sh} +9 -4
- package/scripts/sw-tmux.sh +591 -0
- package/scripts/sw-tracker-jira.sh +277 -0
- package/scripts/sw-tracker-linear.sh +292 -0
- package/scripts/sw-tracker.sh +409 -0
- package/scripts/{cct-upgrade.sh → sw-upgrade.sh} +103 -46
- package/scripts/{cct-worktree.sh → sw-worktree.sh} +3 -0
- package/templates/pipelines/autonomous.json +35 -6
- package/templates/pipelines/cost-aware.json +21 -0
- package/templates/pipelines/deployed.json +40 -6
- package/templates/pipelines/enterprise.json +16 -2
- package/templates/pipelines/fast.json +19 -0
- package/templates/pipelines/full.json +28 -2
- package/templates/pipelines/hotfix.json +19 -0
- package/templates/pipelines/standard.json +31 -0
- package/tmux/{claude-teams-overlay.conf → shipwright-overlay.conf} +27 -9
- package/tmux/templates/accessibility.json +34 -0
- package/tmux/templates/api-design.json +35 -0
- package/tmux/templates/architecture.json +1 -0
- package/tmux/templates/bug-fix.json +9 -0
- package/tmux/templates/code-review.json +1 -0
- package/tmux/templates/compliance.json +36 -0
- package/tmux/templates/data-pipeline.json +36 -0
- package/tmux/templates/debt-paydown.json +34 -0
- package/tmux/templates/devops.json +1 -0
- package/tmux/templates/documentation.json +1 -0
- package/tmux/templates/exploration.json +1 -0
- package/tmux/templates/feature-dev.json +1 -0
- package/tmux/templates/full-stack.json +8 -0
- package/tmux/templates/i18n.json +34 -0
- package/tmux/templates/incident-response.json +36 -0
- package/tmux/templates/migration.json +1 -0
- package/tmux/templates/observability.json +35 -0
- package/tmux/templates/onboarding.json +33 -0
- package/tmux/templates/performance.json +35 -0
- package/tmux/templates/refactor.json +1 -0
- package/tmux/templates/release.json +35 -0
- package/tmux/templates/security-audit.json +8 -0
- package/tmux/templates/spike.json +34 -0
- package/tmux/templates/testing.json +1 -0
- package/tmux/tmux.conf +98 -9
- package/scripts/cct-cleanup.sh +0 -172
- package/scripts/cct-daemon.sh +0 -3189
- package/scripts/cct-doctor.sh +0 -414
- package/scripts/cct-loop.sh +0 -1332
- package/scripts/cct-pipeline.sh +0 -3844
- package/scripts/cct-session.sh +0 -284
- package/scripts/cct-status.sh +0 -169
|
@@ -0,0 +1,1096 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# ╔═══════════════════════════════════════════════════════════════════════════╗
|
|
3
|
+
# ║ shipwright vitals — Pipeline Vitals Engine ║
|
|
4
|
+
# ║ Real-time health scoring · Adaptive limits · Budget trajectory ║
|
|
5
|
+
# ╚═══════════════════════════════════════════════════════════════════════════╝
|
|
6
|
+
set -euo pipefail
|
|
7
|
+
trap 'echo "ERROR: $BASH_SOURCE:$LINENO exited with status $?" >&2' ERR
|
|
8
|
+
|
|
9
|
+
VERSION="1.10.0"
|
|
10
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
11
|
+
REPO_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|
12
|
+
|
|
13
|
+
# ─── Colors (matches Seth's tmux theme) ─────────────────────────────────────
|
|
14
|
+
CYAN='\033[38;2;0;212;255m' # #00d4ff — primary accent
|
|
15
|
+
PURPLE='\033[38;2;124;58;237m' # #7c3aed — secondary
|
|
16
|
+
BLUE='\033[38;2;0;102;255m' # #0066ff — tertiary
|
|
17
|
+
GREEN='\033[38;2;74;222;128m' # success
|
|
18
|
+
YELLOW='\033[38;2;250;204;21m' # warning
|
|
19
|
+
RED='\033[38;2;248;113;113m' # error
|
|
20
|
+
DIM='\033[2m'
|
|
21
|
+
BOLD='\033[1m'
|
|
22
|
+
RESET='\033[0m'
|
|
23
|
+
|
|
24
|
+
# ─── Cross-platform compatibility ──────────────────────────────────────────
|
|
25
|
+
# shellcheck source=lib/compat.sh
|
|
26
|
+
[[ -f "$SCRIPT_DIR/lib/compat.sh" ]] && source "$SCRIPT_DIR/lib/compat.sh"
|
|
27
|
+
|
|
28
|
+
# ─── Output Helpers ─────────────────────────────────────────────────────────
|
|
29
|
+
info() { echo -e "${CYAN}${BOLD}▸${RESET} $*"; }
|
|
30
|
+
success() { echo -e "${GREEN}${BOLD}✓${RESET} $*"; }
|
|
31
|
+
warn() { echo -e "${YELLOW}${BOLD}⚠${RESET} $*"; }
|
|
32
|
+
error() { echo -e "${RED}${BOLD}✗${RESET} $*" >&2; }
|
|
33
|
+
|
|
34
|
+
now_iso() { date -u +"%Y-%m-%dT%H:%M:%SZ"; }
|
|
35
|
+
now_epoch() { date +%s; }
|
|
36
|
+
|
|
37
|
+
# ─── Structured Event Log ──────────────────────────────────────────────────
|
|
38
|
+
EVENTS_FILE="${HOME}/.shipwright/events.jsonl"
|
|
39
|
+
|
|
40
|
+
emit_event() {
|
|
41
|
+
local event_type="$1"
|
|
42
|
+
shift
|
|
43
|
+
local json_fields=""
|
|
44
|
+
for kv in "$@"; do
|
|
45
|
+
local key="${kv%%=*}"
|
|
46
|
+
local val="${kv#*=}"
|
|
47
|
+
if [[ "$val" =~ ^-?[0-9]+\.?[0-9]*$ ]]; then
|
|
48
|
+
json_fields="${json_fields},\"${key}\":${val}"
|
|
49
|
+
else
|
|
50
|
+
val="${val//\"/\\\"}"
|
|
51
|
+
json_fields="${json_fields},\"${key}\":\"${val}\""
|
|
52
|
+
fi
|
|
53
|
+
done
|
|
54
|
+
mkdir -p "$(dirname "$EVENTS_FILE")"
|
|
55
|
+
local tmp_event="${EVENTS_FILE}.tmp.$$"
|
|
56
|
+
printf '{"ts":"%s","type":"%s"%s}\n' "$(now_iso)" "$event_type" "$json_fields" >> "$tmp_event" 2>/dev/null \
|
|
57
|
+
&& cat "$tmp_event" >> "$EVENTS_FILE" 2>/dev/null
|
|
58
|
+
rm -f "$tmp_event"
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
# ─── Constants ──────────────────────────────────────────────────────────────
|
|
62
|
+
PROGRESS_DIR="${HOME}/.shipwright/progress"
|
|
63
|
+
COST_DIR="${HOME}/.shipwright"
|
|
64
|
+
COST_FILE="${COST_DIR}/costs.json"
|
|
65
|
+
BUDGET_FILE="${COST_DIR}/budget.json"
|
|
66
|
+
OPTIMIZATION_DIR="${HOME}/.shipwright/optimization"
|
|
67
|
+
|
|
68
|
+
# Signal weights for health score (configurable via env vars)
|
|
69
|
+
WEIGHT_MOMENTUM="${VITALS_WEIGHT_MOMENTUM:-35}"
|
|
70
|
+
WEIGHT_CONVERGENCE="${VITALS_WEIGHT_CONVERGENCE:-30}"
|
|
71
|
+
WEIGHT_BUDGET="${VITALS_WEIGHT_BUDGET:-20}"
|
|
72
|
+
WEIGHT_ERROR_MATURITY="${VITALS_WEIGHT_ERROR_MATURITY:-15}"
|
|
73
|
+
|
|
74
|
+
# ─── Helper: safe numeric extraction ────────────────────────────────────────
|
|
75
|
+
_safe_num() {
|
|
76
|
+
local val="${1:-0}"
|
|
77
|
+
if [[ "$val" =~ ^-?[0-9]+\.?[0-9]*$ ]]; then
|
|
78
|
+
echo "$val"
|
|
79
|
+
else
|
|
80
|
+
echo "0"
|
|
81
|
+
fi
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
# ─── Momentum Score ─────────────────────────────────────────────────────────
|
|
85
|
+
# Compares current snapshot to previous snapshots to detect forward progress
|
|
86
|
+
_compute_momentum() {
|
|
87
|
+
local progress_file="$1"
|
|
88
|
+
local current_stage="${2:-unknown}"
|
|
89
|
+
local current_iteration="${3:-0}"
|
|
90
|
+
local current_diff="${4:-0}"
|
|
91
|
+
|
|
92
|
+
# No history — assume neutral
|
|
93
|
+
if [[ ! -f "$progress_file" ]]; then
|
|
94
|
+
echo "50"
|
|
95
|
+
return
|
|
96
|
+
fi
|
|
97
|
+
|
|
98
|
+
local snapshots_count
|
|
99
|
+
snapshots_count=$(jq '.snapshots | length' "$progress_file" 2>/dev/null || echo "0")
|
|
100
|
+
snapshots_count=$(_safe_num "$snapshots_count")
|
|
101
|
+
|
|
102
|
+
if [[ "$snapshots_count" -lt 2 ]]; then
|
|
103
|
+
# If we have 1 snapshot, check if stage advanced from intake
|
|
104
|
+
if [[ "$snapshots_count" -eq 1 ]]; then
|
|
105
|
+
local last_stage
|
|
106
|
+
last_stage=$(jq -r '.snapshots[-1].stage // ""' "$progress_file" 2>/dev/null || echo "")
|
|
107
|
+
if [[ -n "$last_stage" && "$last_stage" != "intake" && "$last_stage" != "unknown" ]]; then
|
|
108
|
+
echo "60"
|
|
109
|
+
return
|
|
110
|
+
fi
|
|
111
|
+
fi
|
|
112
|
+
echo "50"
|
|
113
|
+
return
|
|
114
|
+
fi
|
|
115
|
+
|
|
116
|
+
# Get last 3 snapshots for comparison
|
|
117
|
+
local score=50
|
|
118
|
+
|
|
119
|
+
local prev_stage prev_iteration prev_diff no_progress_count
|
|
120
|
+
prev_stage=$(jq -r '.snapshots[-1].stage // "unknown"' "$progress_file" 2>/dev/null || echo "unknown")
|
|
121
|
+
prev_iteration=$(jq -r '.snapshots[-1].iteration // 0' "$progress_file" 2>/dev/null || echo "0")
|
|
122
|
+
prev_iteration=$(_safe_num "$prev_iteration")
|
|
123
|
+
prev_diff=$(jq -r '.snapshots[-1].diff_lines // 0' "$progress_file" 2>/dev/null || echo "0")
|
|
124
|
+
prev_diff=$(_safe_num "$prev_diff")
|
|
125
|
+
no_progress_count=$(jq -r '.no_progress_count // 0' "$progress_file" 2>/dev/null || echo "0")
|
|
126
|
+
no_progress_count=$(_safe_num "$no_progress_count")
|
|
127
|
+
|
|
128
|
+
# Stage advancement: +30 points
|
|
129
|
+
if [[ "$current_stage" != "$prev_stage" && "$current_stage" != "unknown" ]]; then
|
|
130
|
+
score=$((score + 30))
|
|
131
|
+
fi
|
|
132
|
+
|
|
133
|
+
# Iteration progress: +10 points per increment
|
|
134
|
+
current_iteration=$(_safe_num "$current_iteration")
|
|
135
|
+
local iter_delta=$((current_iteration - prev_iteration))
|
|
136
|
+
if [[ "$iter_delta" -gt 0 ]]; then
|
|
137
|
+
local iter_bonus=$((iter_delta * 10))
|
|
138
|
+
[[ "$iter_bonus" -gt 30 ]] && iter_bonus=30
|
|
139
|
+
score=$((score + iter_bonus))
|
|
140
|
+
fi
|
|
141
|
+
|
|
142
|
+
# Diff growth: +5 points per 50 new lines
|
|
143
|
+
current_diff=$(_safe_num "$current_diff")
|
|
144
|
+
local diff_delta=$((current_diff - prev_diff))
|
|
145
|
+
if [[ "$diff_delta" -gt 0 ]]; then
|
|
146
|
+
local diff_bonus=$(( (diff_delta / 50) * 5 ))
|
|
147
|
+
[[ "$diff_bonus" -gt 20 ]] && diff_bonus=20
|
|
148
|
+
score=$((score + diff_bonus))
|
|
149
|
+
fi
|
|
150
|
+
|
|
151
|
+
# No change penalty: -20 per stagnant check
|
|
152
|
+
if [[ "$no_progress_count" -gt 0 ]]; then
|
|
153
|
+
local stagnant_penalty=$((no_progress_count * 20))
|
|
154
|
+
score=$((score - stagnant_penalty))
|
|
155
|
+
fi
|
|
156
|
+
|
|
157
|
+
# Clamp to 0-100
|
|
158
|
+
[[ "$score" -lt 0 ]] && score=0
|
|
159
|
+
[[ "$score" -gt 100 ]] && score=100
|
|
160
|
+
|
|
161
|
+
echo "$score"
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
# ─── Convergence Score ──────────────────────────────────────────────────────
|
|
165
|
+
# Tracks error/issue counts trend across cycles
|
|
166
|
+
_compute_convergence() {
|
|
167
|
+
local error_log="$1"
|
|
168
|
+
local progress_file="$2"
|
|
169
|
+
|
|
170
|
+
# No error log — assume perfect convergence
|
|
171
|
+
if [[ ! -f "$error_log" ]]; then
|
|
172
|
+
echo "100"
|
|
173
|
+
return
|
|
174
|
+
fi
|
|
175
|
+
|
|
176
|
+
local total_errors
|
|
177
|
+
total_errors=$(wc -l < "$error_log" 2>/dev/null | tr -d ' ' || echo "0")
|
|
178
|
+
total_errors=$(_safe_num "$total_errors")
|
|
179
|
+
|
|
180
|
+
if [[ "$total_errors" -eq 0 ]]; then
|
|
181
|
+
echo "100"
|
|
182
|
+
return
|
|
183
|
+
fi
|
|
184
|
+
|
|
185
|
+
# Compare error counts across snapshots to detect trend
|
|
186
|
+
if [[ -f "$progress_file" ]]; then
|
|
187
|
+
local snapshots_count
|
|
188
|
+
snapshots_count=$(jq '.snapshots | length' "$progress_file" 2>/dev/null || echo "0")
|
|
189
|
+
snapshots_count=$(_safe_num "$snapshots_count")
|
|
190
|
+
|
|
191
|
+
if [[ "$snapshots_count" -ge 2 ]]; then
|
|
192
|
+
# Count errors with non-empty signatures in snapshots
|
|
193
|
+
local early_errors late_errors
|
|
194
|
+
early_errors=$(jq '[.snapshots[:($snapshots_count/2 | floor)] | .[] | select(.last_error != "")] | length' \
|
|
195
|
+
--argjson snapshots_count "$snapshots_count" "$progress_file" 2>/dev/null || echo "0")
|
|
196
|
+
early_errors=$(_safe_num "$early_errors")
|
|
197
|
+
late_errors=$(jq '[.snapshots[($snapshots_count/2 | floor):] | .[] | select(.last_error != "")] | length' \
|
|
198
|
+
--argjson snapshots_count "$snapshots_count" "$progress_file" 2>/dev/null || echo "0")
|
|
199
|
+
late_errors=$(_safe_num "$late_errors")
|
|
200
|
+
|
|
201
|
+
if [[ "$early_errors" -gt 0 ]]; then
|
|
202
|
+
local reduction_pct=$(( (early_errors - late_errors) * 100 / early_errors ))
|
|
203
|
+
if [[ "$reduction_pct" -gt 50 ]]; then
|
|
204
|
+
echo "100"
|
|
205
|
+
return
|
|
206
|
+
elif [[ "$reduction_pct" -gt 0 ]]; then
|
|
207
|
+
echo "75"
|
|
208
|
+
return
|
|
209
|
+
elif [[ "$reduction_pct" -eq 0 ]]; then
|
|
210
|
+
echo "40"
|
|
211
|
+
return
|
|
212
|
+
fi
|
|
213
|
+
fi
|
|
214
|
+
fi
|
|
215
|
+
fi
|
|
216
|
+
|
|
217
|
+
# Fallback: decreasing = good, based on recent no_progress_count
|
|
218
|
+
if [[ -f "$progress_file" ]]; then
|
|
219
|
+
local no_prog
|
|
220
|
+
no_prog=$(jq -r '.no_progress_count // 0' "$progress_file" 2>/dev/null || echo "0")
|
|
221
|
+
no_prog=$(_safe_num "$no_prog")
|
|
222
|
+
if [[ "$no_prog" -ge 3 ]]; then
|
|
223
|
+
echo "10"
|
|
224
|
+
return
|
|
225
|
+
fi
|
|
226
|
+
fi
|
|
227
|
+
|
|
228
|
+
echo "40"
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
# ─── Budget Score ───────────────────────────────────────────────────────────
|
|
232
|
+
# Calculates budget health based on remaining vs burn rate
|
|
233
|
+
_compute_budget() {
|
|
234
|
+
local cost_file="${COST_FILE}"
|
|
235
|
+
local budget_file="${BUDGET_FILE}"
|
|
236
|
+
|
|
237
|
+
# Check if budget is enabled
|
|
238
|
+
if [[ ! -f "$budget_file" ]]; then
|
|
239
|
+
echo "100"
|
|
240
|
+
return
|
|
241
|
+
fi
|
|
242
|
+
|
|
243
|
+
local budget_enabled budget_usd
|
|
244
|
+
budget_enabled=$(jq -r '.enabled' "$budget_file" 2>/dev/null || echo "false")
|
|
245
|
+
budget_usd=$(jq -r '.daily_budget_usd' "$budget_file" 2>/dev/null || echo "0")
|
|
246
|
+
|
|
247
|
+
if [[ "$budget_enabled" != "true" || "$budget_usd" == "0" ]]; then
|
|
248
|
+
echo "100"
|
|
249
|
+
return
|
|
250
|
+
fi
|
|
251
|
+
|
|
252
|
+
# Calculate today's spending
|
|
253
|
+
local today_start
|
|
254
|
+
today_start=$(date -u +"%Y-%m-%dT00:00:00Z")
|
|
255
|
+
local today_epoch
|
|
256
|
+
today_epoch=$(date -u -jf "%Y-%m-%dT%H:%M:%SZ" "$today_start" +%s 2>/dev/null \
|
|
257
|
+
|| date -u -d "$today_start" +%s 2>/dev/null || echo "0")
|
|
258
|
+
|
|
259
|
+
local today_spent="0"
|
|
260
|
+
if [[ -f "$cost_file" ]]; then
|
|
261
|
+
today_spent=$(jq --argjson cutoff "$today_epoch" \
|
|
262
|
+
'[.entries[] | select(.ts_epoch >= $cutoff) | .cost_usd] | add // 0' \
|
|
263
|
+
"$cost_file" 2>/dev/null || echo "0")
|
|
264
|
+
fi
|
|
265
|
+
today_spent=$(_safe_num "$today_spent")
|
|
266
|
+
budget_usd=$(_safe_num "$budget_usd")
|
|
267
|
+
|
|
268
|
+
if [[ "$budget_usd" == "0" ]]; then
|
|
269
|
+
echo "100"
|
|
270
|
+
return
|
|
271
|
+
fi
|
|
272
|
+
|
|
273
|
+
# Score = remaining / budget * 100
|
|
274
|
+
local remaining_pct
|
|
275
|
+
remaining_pct=$(awk -v budget="$budget_usd" -v spent="$today_spent" \
|
|
276
|
+
'BEGIN { if (budget > 0) printf "%.0f", ((budget - spent) / budget) * 100; else print 100 }')
|
|
277
|
+
remaining_pct=$(_safe_num "$remaining_pct")
|
|
278
|
+
|
|
279
|
+
[[ "$remaining_pct" -lt 0 ]] && remaining_pct=0
|
|
280
|
+
[[ "$remaining_pct" -gt 100 ]] && remaining_pct=100
|
|
281
|
+
|
|
282
|
+
echo "$remaining_pct"
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
# ─── Error Maturity Score ───────────────────────────────────────────────────
|
|
286
|
+
# High unique/total ratio = new problems = lower score
|
|
287
|
+
# Low unique/total ratio = same issues = depends on convergence
|
|
288
|
+
_compute_error_maturity() {
|
|
289
|
+
local error_log="$1"
|
|
290
|
+
|
|
291
|
+
if [[ ! -f "$error_log" ]]; then
|
|
292
|
+
echo "80"
|
|
293
|
+
return
|
|
294
|
+
fi
|
|
295
|
+
|
|
296
|
+
local total_errors
|
|
297
|
+
total_errors=$(wc -l < "$error_log" 2>/dev/null | tr -d ' ' || echo "0")
|
|
298
|
+
total_errors=$(_safe_num "$total_errors")
|
|
299
|
+
|
|
300
|
+
if [[ "$total_errors" -eq 0 ]]; then
|
|
301
|
+
echo "80"
|
|
302
|
+
return
|
|
303
|
+
fi
|
|
304
|
+
|
|
305
|
+
# Count unique error signatures
|
|
306
|
+
local unique_errors
|
|
307
|
+
unique_errors=$(jq -r '.signature // "unknown"' "$error_log" 2>/dev/null | sort -u | wc -l | tr -d ' ' || echo "0")
|
|
308
|
+
unique_errors=$(_safe_num "$unique_errors")
|
|
309
|
+
|
|
310
|
+
if [[ "$unique_errors" -eq 0 ]]; then
|
|
311
|
+
echo "80"
|
|
312
|
+
return
|
|
313
|
+
fi
|
|
314
|
+
|
|
315
|
+
# Ratio: unique / total. Low ratio = same issues repeating
|
|
316
|
+
# High ratio (>0.8) = lots of different errors = unstable (score 20)
|
|
317
|
+
# Medium ratio (0.4-0.8) = some variety (score 50)
|
|
318
|
+
# Low ratio (<0.4) = stuck on same issues (score 60, mature but stuck)
|
|
319
|
+
local ratio_pct=$(( unique_errors * 100 / total_errors ))
|
|
320
|
+
|
|
321
|
+
if [[ "$ratio_pct" -gt 80 ]]; then
|
|
322
|
+
echo "20"
|
|
323
|
+
elif [[ "$ratio_pct" -gt 40 ]]; then
|
|
324
|
+
echo "50"
|
|
325
|
+
else
|
|
326
|
+
echo "60"
|
|
327
|
+
fi
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
# ─── File Locking Helpers ──────────────────────────────────────────────────
|
|
331
|
+
_vitals_acquire_lock() {
|
|
332
|
+
local lockfile="$1.lock"
|
|
333
|
+
local fd=200
|
|
334
|
+
eval "exec $fd>\"$lockfile\""
|
|
335
|
+
flock -w 5 "$fd" || { warn "Vitals lock timeout"; return 1; }
|
|
336
|
+
}
|
|
337
|
+
_vitals_release_lock() {
|
|
338
|
+
local fd=200
|
|
339
|
+
flock -u "$fd" 2>/dev/null || true
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
343
|
+
# pipeline_emit_progress_snapshot
|
|
344
|
+
# Records a point-in-time snapshot for progress tracking
|
|
345
|
+
# Args: <issue_num> <stage> <iteration> <diff_lines> <files_changed> <last_error>
|
|
346
|
+
# Side effect: writes to ~/.shipwright/progress/issue-<N>.json
|
|
347
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
348
|
+
pipeline_emit_progress_snapshot() {
|
|
349
|
+
local issue_num="${1:-}"
|
|
350
|
+
local stage="${2:-unknown}"
|
|
351
|
+
local iteration="${3:-0}"
|
|
352
|
+
local diff_lines="${4:-0}"
|
|
353
|
+
local files_changed="${5:-0}"
|
|
354
|
+
local last_error="${6:-}"
|
|
355
|
+
|
|
356
|
+
[[ -z "$issue_num" ]] && return 0
|
|
357
|
+
|
|
358
|
+
mkdir -p "$PROGRESS_DIR"
|
|
359
|
+
local progress_file="${PROGRESS_DIR}/issue-${issue_num}.json"
|
|
360
|
+
|
|
361
|
+
# Acquire lock
|
|
362
|
+
_vitals_acquire_lock "$progress_file" || return 1
|
|
363
|
+
|
|
364
|
+
# Build new snapshot entry
|
|
365
|
+
local snapshot_json
|
|
366
|
+
snapshot_json=$(jq -n \
|
|
367
|
+
--arg stage "$stage" \
|
|
368
|
+
--argjson iteration "$(_safe_num "$iteration")" \
|
|
369
|
+
--argjson diff_lines "$(_safe_num "$diff_lines")" \
|
|
370
|
+
--argjson files_changed "$(_safe_num "$files_changed")" \
|
|
371
|
+
--arg last_error "$last_error" \
|
|
372
|
+
--arg ts "$(now_iso)" \
|
|
373
|
+
'{
|
|
374
|
+
stage: $stage,
|
|
375
|
+
iteration: $iteration,
|
|
376
|
+
diff_lines: $diff_lines,
|
|
377
|
+
files_changed: $files_changed,
|
|
378
|
+
last_error: $last_error,
|
|
379
|
+
ts: $ts
|
|
380
|
+
}')
|
|
381
|
+
|
|
382
|
+
# Initialize file if missing
|
|
383
|
+
if [[ ! -f "$progress_file" ]]; then
|
|
384
|
+
echo '{"snapshots":[],"no_progress_count":0}' > "$progress_file"
|
|
385
|
+
fi
|
|
386
|
+
|
|
387
|
+
# Determine if progress was made (stage or iteration advanced)
|
|
388
|
+
local prev_stage prev_iteration no_progress_count
|
|
389
|
+
prev_stage=$(jq -r '.snapshots[-1].stage // ""' "$progress_file" 2>/dev/null || echo "")
|
|
390
|
+
prev_iteration=$(jq -r '.snapshots[-1].iteration // -1' "$progress_file" 2>/dev/null || echo "-1")
|
|
391
|
+
prev_iteration=$(_safe_num "$prev_iteration")
|
|
392
|
+
no_progress_count=$(jq -r '.no_progress_count // 0' "$progress_file" 2>/dev/null || echo "0")
|
|
393
|
+
no_progress_count=$(_safe_num "$no_progress_count")
|
|
394
|
+
|
|
395
|
+
local cur_iter_num
|
|
396
|
+
cur_iter_num=$(_safe_num "$iteration")
|
|
397
|
+
|
|
398
|
+
if [[ "$stage" != "$prev_stage" || "$cur_iter_num" -gt "$prev_iteration" ]]; then
|
|
399
|
+
no_progress_count=0
|
|
400
|
+
else
|
|
401
|
+
no_progress_count=$((no_progress_count + 1))
|
|
402
|
+
fi
|
|
403
|
+
|
|
404
|
+
# Append snapshot, cap at 20 entries, update no_progress_count
|
|
405
|
+
local tmp_pf="${progress_file}.tmp.$$"
|
|
406
|
+
jq --argjson snap "$snapshot_json" \
|
|
407
|
+
--argjson npc "$no_progress_count" \
|
|
408
|
+
'.snapshots += [$snap] | .snapshots = .snapshots[-20:] | .no_progress_count = $npc' \
|
|
409
|
+
"$progress_file" > "$tmp_pf" 2>/dev/null && mv "$tmp_pf" "$progress_file" || {
|
|
410
|
+
rm -f "$tmp_pf"
|
|
411
|
+
_vitals_release_lock
|
|
412
|
+
return 1
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
_vitals_release_lock
|
|
416
|
+
|
|
417
|
+
emit_event "vitals.snapshot" \
|
|
418
|
+
"issue=$issue_num" \
|
|
419
|
+
"stage=$stage" \
|
|
420
|
+
"iteration=$iteration" \
|
|
421
|
+
"diff_lines=$diff_lines" \
|
|
422
|
+
"no_progress=$no_progress_count"
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
426
|
+
# pipeline_compute_vitals
|
|
427
|
+
# Main entry: computes composite health score from 4 weighted signals
|
|
428
|
+
# Args: [pipeline_state_file] [artifacts_dir] [issue_number]
|
|
429
|
+
# Output: JSON to stdout
|
|
430
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
431
|
+
pipeline_compute_vitals() {
|
|
432
|
+
local state_file="${1:-${PIPELINE_STATE:-${REPO_DIR}/.claude/pipeline-state.md}}"
|
|
433
|
+
local artifacts_dir="${2:-${REPO_DIR}/.claude/pipeline-artifacts}"
|
|
434
|
+
local issue_num="${3:-}"
|
|
435
|
+
|
|
436
|
+
# ── Read current pipeline state ──
|
|
437
|
+
local current_stage="unknown" current_iteration=0 elapsed="0s"
|
|
438
|
+
if [[ -f "$state_file" ]]; then
|
|
439
|
+
current_stage=$(grep -m1 '^current_stage:' "$state_file" 2>/dev/null | sed 's/^current_stage: *//' || echo "unknown")
|
|
440
|
+
[[ -z "$current_stage" ]] && current_stage="unknown"
|
|
441
|
+
|
|
442
|
+
local stage_progress
|
|
443
|
+
stage_progress=$(grep -m1 '^stage_progress:' "$state_file" 2>/dev/null | sed 's/^stage_progress: *//' || echo "")
|
|
444
|
+
if [[ "$stage_progress" =~ iteration\ ([0-9]+) ]]; then
|
|
445
|
+
current_iteration="${BASH_REMATCH[1]}"
|
|
446
|
+
fi
|
|
447
|
+
|
|
448
|
+
elapsed=$(grep -m1 '^elapsed:' "$state_file" 2>/dev/null | sed 's/^elapsed: *//' || echo "0s")
|
|
449
|
+
fi
|
|
450
|
+
|
|
451
|
+
# ── Detect issue number if not provided ──
|
|
452
|
+
if [[ -z "$issue_num" && -f "$state_file" ]]; then
|
|
453
|
+
issue_num=$(grep -m1 '^issue:' "$state_file" 2>/dev/null | sed 's/^issue: *//' | tr -d '"' || echo "")
|
|
454
|
+
fi
|
|
455
|
+
|
|
456
|
+
# ── Determine progress file ──
|
|
457
|
+
local progress_file=""
|
|
458
|
+
if [[ -n "$issue_num" ]]; then
|
|
459
|
+
progress_file="${PROGRESS_DIR}/issue-${issue_num}.json"
|
|
460
|
+
fi
|
|
461
|
+
|
|
462
|
+
# ── Error log ──
|
|
463
|
+
local error_log="${artifacts_dir}/error-log.jsonl"
|
|
464
|
+
|
|
465
|
+
# ── Get diff stats from git ──
|
|
466
|
+
local current_diff=0
|
|
467
|
+
current_diff=$(cd "$REPO_DIR" && git diff --stat 2>/dev/null | tail -1 | grep -o '[0-9]* insertion' | grep -o '[0-9]*' || echo "0")
|
|
468
|
+
[[ -z "$current_diff" ]] && current_diff=0
|
|
469
|
+
|
|
470
|
+
# ── Compute individual signals ──
|
|
471
|
+
local momentum convergence budget_score error_maturity
|
|
472
|
+
momentum=$(_compute_momentum "${progress_file}" "$current_stage" "$current_iteration" "$current_diff")
|
|
473
|
+
convergence=$(_compute_convergence "$error_log" "$progress_file")
|
|
474
|
+
budget_score=$(_compute_budget)
|
|
475
|
+
error_maturity=$(_compute_error_maturity "$error_log")
|
|
476
|
+
|
|
477
|
+
# ── Weighted composite score ──
|
|
478
|
+
local health_score=$(( (momentum * WEIGHT_MOMENTUM + convergence * WEIGHT_CONVERGENCE + budget_score * WEIGHT_BUDGET + error_maturity * WEIGHT_ERROR_MATURITY) / 100 ))
|
|
479
|
+
[[ "$health_score" -lt 0 ]] && health_score=0
|
|
480
|
+
[[ "$health_score" -gt 100 ]] && health_score=100
|
|
481
|
+
|
|
482
|
+
# ── Previous score for trajectory ──
|
|
483
|
+
local prev_score=""
|
|
484
|
+
if [[ -n "$progress_file" && -f "$progress_file" ]]; then
|
|
485
|
+
prev_score=$(jq -r '.last_health_score // ""' "$progress_file" 2>/dev/null || echo "")
|
|
486
|
+
fi
|
|
487
|
+
|
|
488
|
+
# ── Verdict ──
|
|
489
|
+
local verdict
|
|
490
|
+
verdict=$(pipeline_health_verdict "$health_score" "$prev_score")
|
|
491
|
+
|
|
492
|
+
# ── Recommended action ──
|
|
493
|
+
local recommended_action="continue"
|
|
494
|
+
case "$verdict" in
|
|
495
|
+
continue) recommended_action="continue" ;;
|
|
496
|
+
warn) recommended_action="extend patience, monitor closely" ;;
|
|
497
|
+
intervene) recommended_action="prepare intervention, consider reducing scope" ;;
|
|
498
|
+
abort) recommended_action="abort pipeline, escalate to human" ;;
|
|
499
|
+
esac
|
|
500
|
+
|
|
501
|
+
# ── Store health score in progress file for trajectory tracking ──
|
|
502
|
+
if [[ -n "$progress_file" && -f "$progress_file" ]]; then
|
|
503
|
+
if _vitals_acquire_lock "$progress_file" 2>/dev/null; then
|
|
504
|
+
local tmp_pf="${progress_file}.tmp.$$"
|
|
505
|
+
jq --argjson score "$health_score" '.last_health_score = $score' \
|
|
506
|
+
"$progress_file" > "$tmp_pf" 2>/dev/null && mv "$tmp_pf" "$progress_file" || rm -f "$tmp_pf"
|
|
507
|
+
_vitals_release_lock
|
|
508
|
+
fi
|
|
509
|
+
fi
|
|
510
|
+
|
|
511
|
+
# ── Budget details ──
|
|
512
|
+
local remaining_budget="unlimited" today_spent="0.00"
|
|
513
|
+
if [[ -f "$BUDGET_FILE" ]]; then
|
|
514
|
+
local be
|
|
515
|
+
be=$(jq -r '.enabled' "$BUDGET_FILE" 2>/dev/null || echo "false")
|
|
516
|
+
if [[ "$be" == "true" ]]; then
|
|
517
|
+
local bu
|
|
518
|
+
bu=$(jq -r '.daily_budget_usd' "$BUDGET_FILE" 2>/dev/null || echo "0")
|
|
519
|
+
local today_start
|
|
520
|
+
today_start=$(date -u +"%Y-%m-%dT00:00:00Z")
|
|
521
|
+
local today_epoch
|
|
522
|
+
today_epoch=$(date -u -jf "%Y-%m-%dT%H:%M:%SZ" "$today_start" +%s 2>/dev/null \
|
|
523
|
+
|| date -u -d "$today_start" +%s 2>/dev/null || echo "0")
|
|
524
|
+
if [[ -f "$COST_FILE" ]]; then
|
|
525
|
+
today_spent=$(jq --argjson cutoff "$today_epoch" \
|
|
526
|
+
'[.entries[] | select(.ts_epoch >= $cutoff) | .cost_usd] | add // 0' \
|
|
527
|
+
"$COST_FILE" 2>/dev/null || echo "0")
|
|
528
|
+
fi
|
|
529
|
+
remaining_budget=$(awk -v b="$bu" -v s="$today_spent" 'BEGIN { printf "%.2f", b - s }')
|
|
530
|
+
fi
|
|
531
|
+
fi
|
|
532
|
+
|
|
533
|
+
# ── Error counts ──
|
|
534
|
+
local total_errors=0 unique_errors=0
|
|
535
|
+
if [[ -f "$error_log" ]]; then
|
|
536
|
+
total_errors=$(wc -l < "$error_log" 2>/dev/null | tr -d ' ' || echo "0")
|
|
537
|
+
unique_errors=$(jq -r '.signature // "unknown"' "$error_log" 2>/dev/null | sort -u | wc -l | tr -d ' ' || echo "0")
|
|
538
|
+
fi
|
|
539
|
+
|
|
540
|
+
# ── Output JSON ──
|
|
541
|
+
jq -n \
|
|
542
|
+
--argjson health_score "$health_score" \
|
|
543
|
+
--arg verdict "$verdict" \
|
|
544
|
+
--arg recommended_action "$recommended_action" \
|
|
545
|
+
--argjson momentum "$momentum" \
|
|
546
|
+
--argjson convergence "$convergence" \
|
|
547
|
+
--argjson budget_score "$budget_score" \
|
|
548
|
+
--argjson error_maturity "$error_maturity" \
|
|
549
|
+
--arg current_stage "$current_stage" \
|
|
550
|
+
--argjson current_iteration "$current_iteration" \
|
|
551
|
+
--arg elapsed "$elapsed" \
|
|
552
|
+
--arg prev_score "${prev_score:-}" \
|
|
553
|
+
--arg remaining_budget "$remaining_budget" \
|
|
554
|
+
--arg today_spent "$today_spent" \
|
|
555
|
+
--argjson total_errors "$total_errors" \
|
|
556
|
+
--argjson unique_errors "$unique_errors" \
|
|
557
|
+
--arg issue "${issue_num:-}" \
|
|
558
|
+
--arg ts "$(now_iso)" \
|
|
559
|
+
'{
|
|
560
|
+
health_score: $health_score,
|
|
561
|
+
verdict: $verdict,
|
|
562
|
+
recommended_action: $recommended_action,
|
|
563
|
+
signals: {
|
|
564
|
+
momentum: $momentum,
|
|
565
|
+
convergence: $convergence,
|
|
566
|
+
budget: $budget_score,
|
|
567
|
+
error_maturity: $error_maturity
|
|
568
|
+
},
|
|
569
|
+
pipeline: {
|
|
570
|
+
stage: $current_stage,
|
|
571
|
+
iteration: $current_iteration,
|
|
572
|
+
elapsed: $elapsed,
|
|
573
|
+
issue: $issue
|
|
574
|
+
},
|
|
575
|
+
budget: {
|
|
576
|
+
remaining: $remaining_budget,
|
|
577
|
+
today_spent: $today_spent
|
|
578
|
+
},
|
|
579
|
+
errors: {
|
|
580
|
+
total: $total_errors,
|
|
581
|
+
unique: $unique_errors
|
|
582
|
+
},
|
|
583
|
+
prev_score: (if $prev_score == "" then null else ($prev_score | tonumber) end),
|
|
584
|
+
ts: $ts
|
|
585
|
+
}'
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
589
|
+
# pipeline_health_verdict
|
|
590
|
+
# Maps health score to action, considering trajectory
|
|
591
|
+
# Args: current_score [previous_score]
|
|
592
|
+
# Output: continue | warn | intervene | abort
|
|
593
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
594
|
+
pipeline_health_verdict() {
|
|
595
|
+
local current_score="${1:-50}"
|
|
596
|
+
local prev_score="${2:-}"
|
|
597
|
+
|
|
598
|
+
current_score=$(_safe_num "$current_score")
|
|
599
|
+
|
|
600
|
+
# Determine trajectory
|
|
601
|
+
local trajectory="stable"
|
|
602
|
+
if [[ -n "$prev_score" && "$prev_score" != "" ]]; then
|
|
603
|
+
prev_score=$(_safe_num "$prev_score")
|
|
604
|
+
if [[ "$current_score" -gt "$prev_score" ]]; then
|
|
605
|
+
trajectory="improving"
|
|
606
|
+
elif [[ "$current_score" -lt "$prev_score" ]]; then
|
|
607
|
+
trajectory="declining"
|
|
608
|
+
fi
|
|
609
|
+
fi
|
|
610
|
+
|
|
611
|
+
# Score-based verdict with trajectory adjustment
|
|
612
|
+
if [[ "$current_score" -ge 70 ]]; then
|
|
613
|
+
echo "continue"
|
|
614
|
+
elif [[ "$current_score" -ge 50 ]]; then
|
|
615
|
+
# Sluggish zone: extend patience if improving
|
|
616
|
+
if [[ "$trajectory" == "improving" ]]; then
|
|
617
|
+
echo "continue"
|
|
618
|
+
else
|
|
619
|
+
echo "warn"
|
|
620
|
+
fi
|
|
621
|
+
elif [[ "$current_score" -ge 30 ]]; then
|
|
622
|
+
# Stalling zone: escalate faster if declining
|
|
623
|
+
if [[ "$trajectory" == "declining" ]]; then
|
|
624
|
+
echo "intervene"
|
|
625
|
+
elif [[ "$trajectory" == "improving" ]]; then
|
|
626
|
+
echo "warn"
|
|
627
|
+
else
|
|
628
|
+
echo "intervene"
|
|
629
|
+
fi
|
|
630
|
+
else
|
|
631
|
+
# Critical zone
|
|
632
|
+
echo "abort"
|
|
633
|
+
fi
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
637
|
+
# pipeline_adaptive_limit
|
|
638
|
+
# Determines cycle limit dynamically based on vitals + learned model
|
|
639
|
+
# Args: loop_type (build_test|compound_quality) [vitals_json]
|
|
640
|
+
# Output: integer cycle limit
|
|
641
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
642
|
+
pipeline_adaptive_limit() {
|
|
643
|
+
local loop_type="${1:-build_test}"
|
|
644
|
+
local vitals_json="${2:-}"
|
|
645
|
+
|
|
646
|
+
# Start with learned iteration model
|
|
647
|
+
local model_file="${OPTIMIZATION_DIR}/iteration-model.json"
|
|
648
|
+
local base_limit=5
|
|
649
|
+
|
|
650
|
+
if [[ -f "$model_file" ]]; then
|
|
651
|
+
local learned
|
|
652
|
+
learned=$(jq -r --arg ctx "$loop_type" '.[$ctx].recommended_cycles // 0' "$model_file" 2>/dev/null || echo "0")
|
|
653
|
+
learned=$(_safe_num "$learned")
|
|
654
|
+
if [[ "$learned" -gt 0 ]]; then
|
|
655
|
+
base_limit="$learned"
|
|
656
|
+
fi
|
|
657
|
+
fi
|
|
658
|
+
|
|
659
|
+
# Get template max (hard ceiling = 2x template max)
|
|
660
|
+
local hard_ceiling=$((base_limit * 2))
|
|
661
|
+
[[ "$hard_ceiling" -lt 4 ]] && hard_ceiling=4
|
|
662
|
+
|
|
663
|
+
# If no vitals provided, return base
|
|
664
|
+
if [[ -z "$vitals_json" ]]; then
|
|
665
|
+
echo "$base_limit"
|
|
666
|
+
return
|
|
667
|
+
fi
|
|
668
|
+
|
|
669
|
+
# Extract vitals signals
|
|
670
|
+
local health convergence budget_s
|
|
671
|
+
health=$(echo "$vitals_json" | jq -r '.health_score // 50' 2>/dev/null || echo "50")
|
|
672
|
+
health=$(_safe_num "$health")
|
|
673
|
+
convergence=$(echo "$vitals_json" | jq -r '.signals.convergence // 50' 2>/dev/null || echo "50")
|
|
674
|
+
convergence=$(_safe_num "$convergence")
|
|
675
|
+
budget_s=$(echo "$vitals_json" | jq -r '.signals.budget // 100' 2>/dev/null || echo "100")
|
|
676
|
+
budget_s=$(_safe_num "$budget_s")
|
|
677
|
+
|
|
678
|
+
local limit="$base_limit"
|
|
679
|
+
|
|
680
|
+
# Health > 70 + convergence > 60: allow +1 beyond model
|
|
681
|
+
if [[ "$health" -gt 70 && "$convergence" -gt 60 ]]; then
|
|
682
|
+
limit=$((base_limit + 1))
|
|
683
|
+
fi
|
|
684
|
+
|
|
685
|
+
# Health < 40: cap at current cycle (don't extend)
|
|
686
|
+
if [[ "$health" -lt 40 ]]; then
|
|
687
|
+
# Keep base_limit, don't extend
|
|
688
|
+
limit="$base_limit"
|
|
689
|
+
fi
|
|
690
|
+
|
|
691
|
+
# Budget score < 30: hard stop at minimum
|
|
692
|
+
if [[ "$budget_s" -lt 30 ]]; then
|
|
693
|
+
limit=1
|
|
694
|
+
fi
|
|
695
|
+
|
|
696
|
+
# Never exceed hard ceiling
|
|
697
|
+
[[ "$limit" -gt "$hard_ceiling" ]] && limit="$hard_ceiling"
|
|
698
|
+
[[ "$limit" -lt 1 ]] && limit=1
|
|
699
|
+
|
|
700
|
+
echo "$limit"
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
704
|
+
# pipeline_budget_trajectory
|
|
705
|
+
# Predicts if pipeline can afford to finish
|
|
706
|
+
# Output: ok | warn | stop
|
|
707
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
708
|
+
pipeline_budget_trajectory() {
|
|
709
|
+
local state_file="${1:-${PIPELINE_STATE:-${REPO_DIR}/.claude/pipeline-state.md}}"
|
|
710
|
+
|
|
711
|
+
# Check if budget is enabled
|
|
712
|
+
if [[ ! -f "$BUDGET_FILE" ]]; then
|
|
713
|
+
echo "ok"
|
|
714
|
+
return
|
|
715
|
+
fi
|
|
716
|
+
|
|
717
|
+
local budget_enabled
|
|
718
|
+
budget_enabled=$(jq -r '.enabled' "$BUDGET_FILE" 2>/dev/null || echo "false")
|
|
719
|
+
if [[ "$budget_enabled" != "true" ]]; then
|
|
720
|
+
echo "ok"
|
|
721
|
+
return
|
|
722
|
+
fi
|
|
723
|
+
|
|
724
|
+
# Get remaining budget
|
|
725
|
+
local budget_usd today_spent remaining_budget
|
|
726
|
+
budget_usd=$(jq -r '.daily_budget_usd' "$BUDGET_FILE" 2>/dev/null || echo "0")
|
|
727
|
+
budget_usd=$(_safe_num "$budget_usd")
|
|
728
|
+
|
|
729
|
+
if [[ "$budget_usd" == "0" ]]; then
|
|
730
|
+
echo "ok"
|
|
731
|
+
return
|
|
732
|
+
fi
|
|
733
|
+
|
|
734
|
+
local today_start
|
|
735
|
+
today_start=$(date -u +"%Y-%m-%dT00:00:00Z")
|
|
736
|
+
local today_epoch
|
|
737
|
+
today_epoch=$(date -u -jf "%Y-%m-%dT%H:%M:%SZ" "$today_start" +%s 2>/dev/null \
|
|
738
|
+
|| date -u -d "$today_start" +%s 2>/dev/null || echo "0")
|
|
739
|
+
|
|
740
|
+
today_spent="0"
|
|
741
|
+
if [[ -f "$COST_FILE" ]]; then
|
|
742
|
+
today_spent=$(jq --argjson cutoff "$today_epoch" \
|
|
743
|
+
'[.entries[] | select(.ts_epoch >= $cutoff) | .cost_usd] | add // 0' \
|
|
744
|
+
"$COST_FILE" 2>/dev/null || echo "0")
|
|
745
|
+
fi
|
|
746
|
+
today_spent=$(_safe_num "$today_spent")
|
|
747
|
+
|
|
748
|
+
remaining_budget=$(awk -v b="$budget_usd" -v s="$today_spent" 'BEGIN { printf "%.2f", b - s }')
|
|
749
|
+
|
|
750
|
+
# Calculate average cost per stage from events
|
|
751
|
+
local avg_cost_per_stage="0"
|
|
752
|
+
if [[ -f "$EVENTS_FILE" ]]; then
|
|
753
|
+
avg_cost_per_stage=$(grep '"type":"cost.record"' "$EVENTS_FILE" 2>/dev/null \
|
|
754
|
+
| jq -r '.cost_usd // 0' 2>/dev/null \
|
|
755
|
+
| awk '{ sum += $1; count++ } END { if (count > 0) printf "%.2f", sum/count; else print "0.50" }' \
|
|
756
|
+
|| echo "0.50")
|
|
757
|
+
fi
|
|
758
|
+
avg_cost_per_stage=$(_safe_num "$avg_cost_per_stage")
|
|
759
|
+
# Default to 0.50 if no data
|
|
760
|
+
if awk -v c="$avg_cost_per_stage" 'BEGIN { exit !(c <= 0) }'; then
|
|
761
|
+
avg_cost_per_stage="0.50"
|
|
762
|
+
fi
|
|
763
|
+
|
|
764
|
+
# Count remaining stages
|
|
765
|
+
local remaining_stages=6
|
|
766
|
+
if [[ -f "$state_file" ]]; then
|
|
767
|
+
local current_stage
|
|
768
|
+
current_stage=$(grep -m1 '^current_stage:' "$state_file" 2>/dev/null | sed 's/^current_stage: *//' || echo "")
|
|
769
|
+
case "$current_stage" in
|
|
770
|
+
intake) remaining_stages=11 ;;
|
|
771
|
+
plan) remaining_stages=10 ;;
|
|
772
|
+
design) remaining_stages=9 ;;
|
|
773
|
+
build) remaining_stages=8 ;;
|
|
774
|
+
test) remaining_stages=7 ;;
|
|
775
|
+
review) remaining_stages=6 ;;
|
|
776
|
+
compound_quality) remaining_stages=5 ;;
|
|
777
|
+
pr) remaining_stages=4 ;;
|
|
778
|
+
merge) remaining_stages=3 ;;
|
|
779
|
+
deploy) remaining_stages=2 ;;
|
|
780
|
+
validate) remaining_stages=1 ;;
|
|
781
|
+
monitor) remaining_stages=0 ;;
|
|
782
|
+
esac
|
|
783
|
+
fi
|
|
784
|
+
|
|
785
|
+
# Predict: can we afford to finish?
|
|
786
|
+
local needed
|
|
787
|
+
needed=$(awk -v avg="$avg_cost_per_stage" -v stages="$remaining_stages" -v factor="1.5" \
|
|
788
|
+
'BEGIN { printf "%.2f", avg * stages * factor }')
|
|
789
|
+
|
|
790
|
+
local can_afford
|
|
791
|
+
can_afford=$(awk -v rem="$remaining_budget" -v need="$needed" 'BEGIN { print (rem >= need) ? "yes" : "no" }')
|
|
792
|
+
|
|
793
|
+
local min_threshold
|
|
794
|
+
min_threshold=$(awk -v avg="$avg_cost_per_stage" 'BEGIN { printf "%.2f", avg * 2 }')
|
|
795
|
+
|
|
796
|
+
local above_min
|
|
797
|
+
above_min=$(awk -v rem="$remaining_budget" -v min="$min_threshold" 'BEGIN { print (rem >= min) ? "yes" : "no" }')
|
|
798
|
+
|
|
799
|
+
if [[ "$above_min" == "no" ]]; then
|
|
800
|
+
echo "stop"
|
|
801
|
+
elif [[ "$can_afford" == "no" ]]; then
|
|
802
|
+
echo "warn"
|
|
803
|
+
else
|
|
804
|
+
echo "ok"
|
|
805
|
+
fi
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
809
|
+
# vitals_dashboard
|
|
810
|
+
# CLI output for `shipwright vitals`
|
|
811
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
812
|
+
vitals_dashboard() {
|
|
813
|
+
local state_file="${1:-${PIPELINE_STATE:-${REPO_DIR}/.claude/pipeline-state.md}}"
|
|
814
|
+
local artifacts_dir="${2:-${REPO_DIR}/.claude/pipeline-artifacts}"
|
|
815
|
+
local issue_num="${3:-}"
|
|
816
|
+
|
|
817
|
+
# Compute vitals
|
|
818
|
+
local vitals
|
|
819
|
+
vitals=$(pipeline_compute_vitals "$state_file" "$artifacts_dir" "$issue_num")
|
|
820
|
+
|
|
821
|
+
# Extract fields
|
|
822
|
+
local health_score verdict recommended_action
|
|
823
|
+
health_score=$(echo "$vitals" | jq -r '.health_score')
|
|
824
|
+
verdict=$(echo "$vitals" | jq -r '.verdict')
|
|
825
|
+
recommended_action=$(echo "$vitals" | jq -r '.recommended_action')
|
|
826
|
+
|
|
827
|
+
local momentum convergence budget_s error_maturity
|
|
828
|
+
momentum=$(echo "$vitals" | jq -r '.signals.momentum')
|
|
829
|
+
convergence=$(echo "$vitals" | jq -r '.signals.convergence')
|
|
830
|
+
budget_s=$(echo "$vitals" | jq -r '.signals.budget')
|
|
831
|
+
error_maturity=$(echo "$vitals" | jq -r '.signals.error_maturity')
|
|
832
|
+
|
|
833
|
+
local stage iteration elapsed issue_display
|
|
834
|
+
stage=$(echo "$vitals" | jq -r '.pipeline.stage')
|
|
835
|
+
iteration=$(echo "$vitals" | jq -r '.pipeline.iteration')
|
|
836
|
+
elapsed=$(echo "$vitals" | jq -r '.pipeline.elapsed')
|
|
837
|
+
issue_display=$(echo "$vitals" | jq -r '.pipeline.issue')
|
|
838
|
+
|
|
839
|
+
local remaining_budget today_spent
|
|
840
|
+
remaining_budget=$(echo "$vitals" | jq -r '.budget.remaining')
|
|
841
|
+
today_spent=$(echo "$vitals" | jq -r '.budget.today_spent')
|
|
842
|
+
|
|
843
|
+
local total_errors unique_errors
|
|
844
|
+
total_errors=$(echo "$vitals" | jq -r '.errors.total')
|
|
845
|
+
unique_errors=$(echo "$vitals" | jq -r '.errors.unique')
|
|
846
|
+
|
|
847
|
+
local prev_score
|
|
848
|
+
prev_score=$(echo "$vitals" | jq -r '.prev_score // "none"')
|
|
849
|
+
|
|
850
|
+
# ── Color for health score ──
|
|
851
|
+
local score_color="$GREEN"
|
|
852
|
+
if [[ "$health_score" -lt 30 ]]; then
|
|
853
|
+
score_color="$RED"
|
|
854
|
+
elif [[ "$health_score" -lt 50 ]]; then
|
|
855
|
+
score_color="$YELLOW"
|
|
856
|
+
elif [[ "$health_score" -lt 70 ]]; then
|
|
857
|
+
score_color="$BLUE"
|
|
858
|
+
fi
|
|
859
|
+
|
|
860
|
+
# ── Verdict label ──
|
|
861
|
+
local verdict_label
|
|
862
|
+
case "$verdict" in
|
|
863
|
+
continue) verdict_label="${GREEN}healthy${RESET}" ;;
|
|
864
|
+
warn) verdict_label="${YELLOW}sluggish${RESET}" ;;
|
|
865
|
+
intervene) verdict_label="${RED}stalling${RESET}" ;;
|
|
866
|
+
abort) verdict_label="${RED}${BOLD}critical${RESET}" ;;
|
|
867
|
+
*) verdict_label="${DIM}unknown${RESET}" ;;
|
|
868
|
+
esac
|
|
869
|
+
|
|
870
|
+
# ── Header ──
|
|
871
|
+
echo ""
|
|
872
|
+
local title="Pipeline Vitals"
|
|
873
|
+
if [[ -n "$issue_display" && "$issue_display" != "" ]]; then
|
|
874
|
+
title="Pipeline Vitals — issue #${issue_display}"
|
|
875
|
+
fi
|
|
876
|
+
echo -e "${CYAN}${BOLD} ${title}${RESET}"
|
|
877
|
+
echo -e "${DIM} ══════════════════════════════════════════${RESET}"
|
|
878
|
+
echo ""
|
|
879
|
+
|
|
880
|
+
# ── Health Score ──
|
|
881
|
+
printf " ${BOLD}Health Score:${RESET} ${score_color}${BOLD}%d${RESET}/100 (%b)\n" "$health_score" "$verdict_label"
|
|
882
|
+
|
|
883
|
+
# ── Signal details ──
|
|
884
|
+
local m_desc c_desc b_desc e_desc
|
|
885
|
+
|
|
886
|
+
# Momentum description
|
|
887
|
+
if [[ "$momentum" -ge 70 ]]; then
|
|
888
|
+
m_desc="advancing"
|
|
889
|
+
elif [[ "$momentum" -ge 40 ]]; then
|
|
890
|
+
m_desc="steady"
|
|
891
|
+
else
|
|
892
|
+
m_desc="stagnant"
|
|
893
|
+
fi
|
|
894
|
+
if [[ "$stage" != "unknown" ]]; then
|
|
895
|
+
m_desc="${m_desc} (${stage})"
|
|
896
|
+
fi
|
|
897
|
+
|
|
898
|
+
# Convergence description
|
|
899
|
+
if [[ "$convergence" -ge 70 ]]; then
|
|
900
|
+
c_desc="issues decreasing"
|
|
901
|
+
elif [[ "$convergence" -ge 40 ]]; then
|
|
902
|
+
c_desc="flat"
|
|
903
|
+
else
|
|
904
|
+
c_desc="issues increasing"
|
|
905
|
+
fi
|
|
906
|
+
|
|
907
|
+
# Budget description
|
|
908
|
+
if [[ "$remaining_budget" == "unlimited" ]]; then
|
|
909
|
+
b_desc="no budget set"
|
|
910
|
+
else
|
|
911
|
+
b_desc="\$${remaining_budget} remaining (\$${today_spent} burned)"
|
|
912
|
+
fi
|
|
913
|
+
|
|
914
|
+
# Error maturity description
|
|
915
|
+
e_desc="${unique_errors} unique / ${total_errors} total"
|
|
916
|
+
|
|
917
|
+
printf " ${DIM}Momentum:${RESET} %3d ${DIM}%s${RESET}\n" "$momentum" "$m_desc"
|
|
918
|
+
printf " ${DIM}Convergence:${RESET} %3d ${DIM}%s${RESET}\n" "$convergence" "$c_desc"
|
|
919
|
+
printf " ${DIM}Budget:${RESET} %3d ${DIM}%s${RESET}\n" "$budget_s" "$b_desc"
|
|
920
|
+
printf " ${DIM}Error Maturity:${RESET}%3d ${DIM}%s${RESET}\n" "$error_maturity" "$e_desc"
|
|
921
|
+
echo ""
|
|
922
|
+
|
|
923
|
+
# ── Trajectory ──
|
|
924
|
+
if [[ "$prev_score" != "none" && "$prev_score" != "null" ]]; then
|
|
925
|
+
local trajectory_label trajectory_color
|
|
926
|
+
prev_score=$(_safe_num "$prev_score")
|
|
927
|
+
if [[ "$health_score" -gt "$prev_score" ]]; then
|
|
928
|
+
trajectory_label="improving"
|
|
929
|
+
trajectory_color="$GREEN"
|
|
930
|
+
elif [[ "$health_score" -lt "$prev_score" ]]; then
|
|
931
|
+
trajectory_label="declining"
|
|
932
|
+
trajectory_color="$RED"
|
|
933
|
+
else
|
|
934
|
+
trajectory_label="stable"
|
|
935
|
+
trajectory_color="$DIM"
|
|
936
|
+
fi
|
|
937
|
+
printf " ${BOLD}Trajectory:${RESET} ${trajectory_color}%s${RESET} ${DIM}(was %d → %d)${RESET}\n" \
|
|
938
|
+
"$trajectory_label" "$prev_score" "$health_score"
|
|
939
|
+
fi
|
|
940
|
+
|
|
941
|
+
printf " ${BOLD}Recommendation:${RESET} %s\n" "$recommended_action"
|
|
942
|
+
echo ""
|
|
943
|
+
|
|
944
|
+
# ── Active pipeline info ──
|
|
945
|
+
if [[ "$stage" != "unknown" ]]; then
|
|
946
|
+
printf " ${DIM}Active: stage=%s iter=%d elapsed=%s${RESET}\n" "$stage" "$iteration" "$elapsed"
|
|
947
|
+
echo ""
|
|
948
|
+
fi
|
|
949
|
+
|
|
950
|
+
# ── Budget trajectory ──
|
|
951
|
+
local bt
|
|
952
|
+
bt=$(pipeline_budget_trajectory "$state_file")
|
|
953
|
+
if [[ "$bt" == "warn" ]]; then
|
|
954
|
+
echo -e " ${YELLOW}${BOLD}⚠${RESET} ${YELLOW}Budget trajectory: may not have enough to finish${RESET}"
|
|
955
|
+
echo ""
|
|
956
|
+
elif [[ "$bt" == "stop" ]]; then
|
|
957
|
+
echo -e " ${RED}${BOLD}✗${RESET} ${RED}Budget trajectory: insufficient funds to continue${RESET}"
|
|
958
|
+
echo ""
|
|
959
|
+
fi
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
963
|
+
# pipeline_check_health_gate
|
|
964
|
+
# Returns 0 if health is above threshold, 1 if below
|
|
965
|
+
# Args: [state_file] [artifacts_dir] [issue_number]
|
|
966
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
967
|
+
pipeline_check_health_gate() {
|
|
968
|
+
local state_file="${1:-}"
|
|
969
|
+
local artifacts_dir="${2:-}"
|
|
970
|
+
local issue="${3:-}"
|
|
971
|
+
local threshold="${VITALS_GATE_THRESHOLD:-40}"
|
|
972
|
+
|
|
973
|
+
local vitals_json
|
|
974
|
+
vitals_json=$(pipeline_compute_vitals "$state_file" "$artifacts_dir" "$issue" 2>/dev/null) || return 0
|
|
975
|
+
|
|
976
|
+
local health
|
|
977
|
+
health=$(echo "$vitals_json" | jq -r '.health_score // 50' 2>/dev/null) || health=50
|
|
978
|
+
health=$(_safe_num "$health")
|
|
979
|
+
|
|
980
|
+
if [[ "$health" -lt "$threshold" ]]; then
|
|
981
|
+
warn "Health gate: score ${health} < threshold ${threshold}"
|
|
982
|
+
return 1
|
|
983
|
+
fi
|
|
984
|
+
return 0
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
# ─── Help ───────────────────────────────────────────────────────────────────
|
|
988
|
+
show_help() {
|
|
989
|
+
echo ""
|
|
990
|
+
echo -e "${CYAN}${BOLD} Shipwright Pipeline Vitals${RESET} ${DIM}v${VERSION}${RESET}"
|
|
991
|
+
echo -e "${DIM} ══════════════════════════════════════════${RESET}"
|
|
992
|
+
echo ""
|
|
993
|
+
echo -e " ${BOLD}USAGE${RESET}"
|
|
994
|
+
echo -e " shipwright vitals [options]"
|
|
995
|
+
echo ""
|
|
996
|
+
echo -e " ${BOLD}OPTIONS${RESET}"
|
|
997
|
+
echo -e " ${CYAN}--issue${RESET} <N> Issue number to check"
|
|
998
|
+
echo -e " ${CYAN}--state${RESET} <path> Pipeline state file (default: .claude/pipeline-state.md)"
|
|
999
|
+
echo -e " ${CYAN}--artifacts${RESET} <path> Artifacts directory (default: .claude/pipeline-artifacts)"
|
|
1000
|
+
echo -e " ${CYAN}--json${RESET} Output raw JSON instead of dashboard"
|
|
1001
|
+
echo -e " ${CYAN}--score${RESET} Output only the health score (0-100)"
|
|
1002
|
+
echo -e " ${CYAN}--verdict${RESET} Output only the verdict"
|
|
1003
|
+
echo -e " ${CYAN}--budget${RESET} Output only budget trajectory (ok/warn/stop)"
|
|
1004
|
+
echo -e " ${CYAN}--help${RESET} Show this help"
|
|
1005
|
+
echo ""
|
|
1006
|
+
echo -e " ${BOLD}SIGNALS${RESET} ${DIM}(weighted composite)${RESET}"
|
|
1007
|
+
echo -e " ${DIM}Momentum (35%) Stage advancement, iteration progress, diff growth${RESET}"
|
|
1008
|
+
echo -e " ${DIM}Convergence (30%) Error count trend across cycles${RESET}"
|
|
1009
|
+
echo -e " ${DIM}Budget (20%) Remaining budget vs burn rate${RESET}"
|
|
1010
|
+
echo -e " ${DIM}Error Maturity(15%) Unique errors vs total errors${RESET}"
|
|
1011
|
+
echo ""
|
|
1012
|
+
echo -e " ${BOLD}EXAMPLES${RESET}"
|
|
1013
|
+
echo -e " ${DIM}shipwright vitals${RESET} # Dashboard view"
|
|
1014
|
+
echo -e " ${DIM}shipwright vitals --issue 42${RESET} # Check specific issue"
|
|
1015
|
+
echo -e " ${DIM}shipwright vitals --json${RESET} # Machine-readable output"
|
|
1016
|
+
echo -e " ${DIM}shipwright vitals --score${RESET} # Just the number"
|
|
1017
|
+
echo ""
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
# ─── CLI Entry Point ────────────────────────────────────────────────────────
|
|
1021
|
+
main() {
|
|
1022
|
+
local issue_num="" state_file="" artifacts_dir="" output_mode="dashboard"
|
|
1023
|
+
|
|
1024
|
+
while [[ $# -gt 0 ]]; do
|
|
1025
|
+
case "$1" in
|
|
1026
|
+
--issue|-i)
|
|
1027
|
+
issue_num="$2"
|
|
1028
|
+
shift 2
|
|
1029
|
+
;;
|
|
1030
|
+
--state)
|
|
1031
|
+
state_file="$2"
|
|
1032
|
+
shift 2
|
|
1033
|
+
;;
|
|
1034
|
+
--artifacts)
|
|
1035
|
+
artifacts_dir="$2"
|
|
1036
|
+
shift 2
|
|
1037
|
+
;;
|
|
1038
|
+
--json)
|
|
1039
|
+
output_mode="json"
|
|
1040
|
+
shift
|
|
1041
|
+
;;
|
|
1042
|
+
--score)
|
|
1043
|
+
output_mode="score"
|
|
1044
|
+
shift
|
|
1045
|
+
;;
|
|
1046
|
+
--verdict)
|
|
1047
|
+
output_mode="verdict"
|
|
1048
|
+
shift
|
|
1049
|
+
;;
|
|
1050
|
+
--budget)
|
|
1051
|
+
output_mode="budget"
|
|
1052
|
+
shift
|
|
1053
|
+
;;
|
|
1054
|
+
--help|-h|help)
|
|
1055
|
+
show_help
|
|
1056
|
+
return 0
|
|
1057
|
+
;;
|
|
1058
|
+
*)
|
|
1059
|
+
error "Unknown option: $1"
|
|
1060
|
+
show_help
|
|
1061
|
+
return 1
|
|
1062
|
+
;;
|
|
1063
|
+
esac
|
|
1064
|
+
done
|
|
1065
|
+
|
|
1066
|
+
# Defaults
|
|
1067
|
+
state_file="${state_file:-${PIPELINE_STATE:-${REPO_DIR}/.claude/pipeline-state.md}}"
|
|
1068
|
+
artifacts_dir="${artifacts_dir:-${REPO_DIR}/.claude/pipeline-artifacts}"
|
|
1069
|
+
|
|
1070
|
+
case "$output_mode" in
|
|
1071
|
+
dashboard)
|
|
1072
|
+
vitals_dashboard "$state_file" "$artifacts_dir" "$issue_num"
|
|
1073
|
+
;;
|
|
1074
|
+
json)
|
|
1075
|
+
pipeline_compute_vitals "$state_file" "$artifacts_dir" "$issue_num"
|
|
1076
|
+
;;
|
|
1077
|
+
score)
|
|
1078
|
+
local vitals
|
|
1079
|
+
vitals=$(pipeline_compute_vitals "$state_file" "$artifacts_dir" "$issue_num")
|
|
1080
|
+
echo "$vitals" | jq -r '.health_score'
|
|
1081
|
+
;;
|
|
1082
|
+
verdict)
|
|
1083
|
+
local vitals
|
|
1084
|
+
vitals=$(pipeline_compute_vitals "$state_file" "$artifacts_dir" "$issue_num")
|
|
1085
|
+
echo "$vitals" | jq -r '.verdict'
|
|
1086
|
+
;;
|
|
1087
|
+
budget)
|
|
1088
|
+
pipeline_budget_trajectory "$state_file"
|
|
1089
|
+
;;
|
|
1090
|
+
esac
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
# Only run main when executed directly, not when sourced
|
|
1094
|
+
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
|
|
1095
|
+
main "$@"
|
|
1096
|
+
fi
|