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
|
@@ -282,7 +282,30 @@ daemon_reap_completed() {
|
|
|
282
282
|
|
|
283
283
|
# Check if process is still running
|
|
284
284
|
if kill -0 "$pid" 2>/dev/null; then
|
|
285
|
-
|
|
285
|
+
# Guard against PID reuse: if job has been running > 6 hours and
|
|
286
|
+
# the process tree doesn't contain sw-pipeline/sw-loop, it's stale
|
|
287
|
+
local _started_at _start_e _age_s
|
|
288
|
+
_started_at=$(echo "$job" | jq -r '.started_at // empty')
|
|
289
|
+
if [[ -n "$_started_at" ]]; then
|
|
290
|
+
_start_e=$(TZ=UTC date -j -f "%Y-%m-%dT%H:%M:%SZ" "$_started_at" +%s 2>/dev/null || date -d "$_started_at" +%s 2>/dev/null || echo "0")
|
|
291
|
+
_age_s=$(( $(now_epoch) - ${_start_e:-0} ))
|
|
292
|
+
if [[ "$_age_s" -gt 21600 ]]; then # 6 hours
|
|
293
|
+
# Verify this PID is actually our pipeline (not a reused PID)
|
|
294
|
+
local _proc_cmd
|
|
295
|
+
_proc_cmd=$(ps -p "$pid" -o command= 2>/dev/null || true)
|
|
296
|
+
if [[ -z "$_proc_cmd" ]] || ! echo "$_proc_cmd" | grep -qE 'sw-pipeline|sw-loop|claude' 2>/dev/null; then
|
|
297
|
+
daemon_log WARN "Stale job #${issue_num}: PID $pid running ${_age_s}s but not a pipeline process — force-reaping"
|
|
298
|
+
emit_event "daemon.stale_dead" "issue=$issue_num" "pid=$pid" "elapsed_s=$_age_s"
|
|
299
|
+
# Fall through to reap logic
|
|
300
|
+
else
|
|
301
|
+
continue
|
|
302
|
+
fi
|
|
303
|
+
else
|
|
304
|
+
continue
|
|
305
|
+
fi
|
|
306
|
+
else
|
|
307
|
+
continue
|
|
308
|
+
fi
|
|
286
309
|
fi
|
|
287
310
|
|
|
288
311
|
# Process is dead — determine exit code
|
|
File without changes
|
|
File without changes
|
|
@@ -3,6 +3,28 @@
|
|
|
3
3
|
[[ -n "${_DAEMON_PATROL_LOADED:-}" ]] && return 0
|
|
4
4
|
_DAEMON_PATROL_LOADED=1
|
|
5
5
|
|
|
6
|
+
# ─── Decision Engine Signal Mode ─────────────────────────────────────────────
|
|
7
|
+
# When DECISION_ENGINE_ENABLED=true, patrol writes candidates to the pending
|
|
8
|
+
# signals file instead of creating GitHub issues directly. The decision engine
|
|
9
|
+
# collects, scores, and acts on these signals with tiered autonomy.
|
|
10
|
+
SIGNALS_PENDING_FILE="${HOME}/.shipwright/signals/pending.jsonl"
|
|
11
|
+
|
|
12
|
+
_patrol_emit_signal() {
|
|
13
|
+
local id="$1" signal="$2" category="$3" title="$4" description="$5"
|
|
14
|
+
local risk="${6:-50}" confidence="${7:-0.80}" dedup_key="$8"
|
|
15
|
+
mkdir -p "$(dirname "$SIGNALS_PENDING_FILE")"
|
|
16
|
+
local ts
|
|
17
|
+
ts=$(now_iso)
|
|
18
|
+
local candidate
|
|
19
|
+
candidate=$(jq -n \
|
|
20
|
+
--arg id "$id" --arg signal "$signal" --arg category "$category" \
|
|
21
|
+
--arg title "$title" --arg desc "$description" \
|
|
22
|
+
--argjson risk "$risk" --arg conf "$confidence" \
|
|
23
|
+
--arg dedup "$dedup_key" --arg ts "$ts" \
|
|
24
|
+
'{id:$id, signal:$signal, category:$category, title:$title, description:$desc, evidence:{}, risk_score:$risk, confidence:$conf, dedup_key:$dedup, collected_at:$ts}')
|
|
25
|
+
echo "$candidate" >> "$SIGNALS_PENDING_FILE"
|
|
26
|
+
}
|
|
27
|
+
|
|
6
28
|
patrol_build_labels() {
|
|
7
29
|
local check_label="$1"
|
|
8
30
|
local labels="${PATROL_LABEL},${check_label}"
|
|
@@ -76,8 +98,16 @@ daemon_patrol() {
|
|
|
76
98
|
findings=$((findings + 1))
|
|
77
99
|
emit_event "patrol.finding" "check=security" "severity=$severity" "package=$name"
|
|
78
100
|
|
|
79
|
-
#
|
|
80
|
-
if [[ "$
|
|
101
|
+
# Route to decision engine or create issue directly
|
|
102
|
+
if [[ "${DECISION_ENGINE_ENABLED:-false}" == "true" ]]; then
|
|
103
|
+
local _cat="security_patch"
|
|
104
|
+
[[ "$severity" == "critical" ]] && _cat="security_critical"
|
|
105
|
+
_patrol_emit_signal "sec-${name}" "security" "$_cat" \
|
|
106
|
+
"Security: ${title} in ${name}" \
|
|
107
|
+
"Fix ${severity} vulnerability in ${name}" \
|
|
108
|
+
"$([[ "$severity" == "critical" ]] && echo 80 || echo 50)" \
|
|
109
|
+
"0.95" "security:${name}:${title}"
|
|
110
|
+
elif [[ "$NO_GITHUB" != "true" ]] && [[ "$dry_run" != "true" ]]; then
|
|
81
111
|
local existing
|
|
82
112
|
existing=$(gh issue list --label "$PATROL_LABEL" --label "security" \
|
|
83
113
|
--search "Security: $name" --json number -q 'length' 2>/dev/null || echo "0")
|
|
@@ -202,8 +232,13 @@ Auto-detected by \`shipwright daemon patrol\`." \
|
|
|
202
232
|
fi
|
|
203
233
|
done < <(echo "$outdated_json" | jq -c 'to_entries[]' 2>/dev/null)
|
|
204
234
|
|
|
205
|
-
#
|
|
206
|
-
if [[ "$findings" -gt 0 ]] && [[ "$
|
|
235
|
+
# Route to decision engine or create issue
|
|
236
|
+
if [[ "$findings" -gt 0 ]] && [[ "${DECISION_ENGINE_ENABLED:-false}" == "true" ]]; then
|
|
237
|
+
_patrol_emit_signal "deps-stale-${findings}" "deps" "deps_major" \
|
|
238
|
+
"Update ${findings} stale dependencies" \
|
|
239
|
+
"Packages 2+ major versions behind" \
|
|
240
|
+
45 "0.90" "deps:stale:${findings}"
|
|
241
|
+
elif [[ "$findings" -gt 0 ]] && [[ "$NO_GITHUB" != "true" ]] && [[ "$dry_run" != "true" ]]; then
|
|
207
242
|
local existing
|
|
208
243
|
existing=$(gh issue list --label "$PATROL_LABEL" --label "dependencies" \
|
|
209
244
|
--search "Stale dependencies" --json number -q 'length' 2>/dev/null || echo "0")
|
|
@@ -814,12 +849,12 @@ Auto-detected by \`shipwright daemon patrol\` on $(now_iso)." \
|
|
|
814
849
|
if [[ ! -f "$scripts_dir/sw-${name}-test.sh" ]]; then
|
|
815
850
|
# Count usage across other scripts
|
|
816
851
|
local usage_count
|
|
817
|
-
usage_count=$(grep -rl "sw-${name}" "$scripts_dir"/sw-*.sh 2>/dev/null | grep -cv "$basename" 2>/dev/null ||
|
|
852
|
+
usage_count=$(grep -rl "sw-${name}" "$scripts_dir"/sw-*.sh 2>/dev/null | grep -cv "$basename" 2>/dev/null || true)
|
|
818
853
|
usage_count=${usage_count:-0}
|
|
819
854
|
|
|
820
855
|
local line_count
|
|
821
|
-
line_count=$(wc -l < "$script" 2>/dev/null | tr -d ' ' ||
|
|
822
|
-
line_count
|
|
856
|
+
line_count=$(wc -l < "$script" 2>/dev/null | tr -d ' ' || true)
|
|
857
|
+
line_count="${line_count:-0}"
|
|
823
858
|
|
|
824
859
|
untested_entries="${untested_entries}${usage_count}|${basename}|${line_count}\n"
|
|
825
860
|
findings=$((findings + 1))
|
|
@@ -1196,6 +1196,23 @@ daemon_poll_loop() {
|
|
|
1196
1196
|
daemon_patrol --once || daemon_log WARN "daemon_patrol failed — continuing"
|
|
1197
1197
|
LAST_PATROL_EPOCH=$now_e
|
|
1198
1198
|
fi
|
|
1199
|
+
|
|
1200
|
+
# Decision engine cycle (if enabled)
|
|
1201
|
+
local _decision_enabled
|
|
1202
|
+
_decision_enabled=$(policy_get ".decision.enabled" "false" 2>/dev/null || echo "false")
|
|
1203
|
+
if [[ "$_decision_enabled" == "true" ]]; then
|
|
1204
|
+
local _decision_interval
|
|
1205
|
+
_decision_interval=$(policy_get ".decision.cycle_interval_seconds" "1800" 2>/dev/null || echo "1800")
|
|
1206
|
+
local _last_decision_epoch="${_LAST_DECISION_EPOCH:-0}"
|
|
1207
|
+
if [[ $((now_e - _last_decision_epoch)) -ge "$_decision_interval" ]]; then
|
|
1208
|
+
daemon_log INFO "Running decision engine cycle"
|
|
1209
|
+
if [[ -f "$SCRIPT_DIR/sw-decide.sh" ]]; then
|
|
1210
|
+
DECISION_ENGINE_ENABLED=true bash "$SCRIPT_DIR/sw-decide.sh" run --once 2>/dev/null || \
|
|
1211
|
+
daemon_log WARN "Decision engine cycle failed — continuing"
|
|
1212
|
+
fi
|
|
1213
|
+
_LAST_DECISION_EPOCH=$now_e
|
|
1214
|
+
fi
|
|
1215
|
+
fi
|
|
1199
1216
|
fi
|
|
1200
1217
|
|
|
1201
1218
|
# ── Adaptive poll interval: adjust sleep based on queue state ──
|
|
@@ -390,6 +390,16 @@ init_state() {
|
|
|
390
390
|
atomic_write_state "$init_json"
|
|
391
391
|
) 200>"$lock_file"
|
|
392
392
|
else
|
|
393
|
+
# Validate existing state file JSON before using it
|
|
394
|
+
if ! jq '.' "$STATE_FILE" >/dev/null 2>&1; then
|
|
395
|
+
daemon_log WARN "Corrupted state file detected — backing up and resetting"
|
|
396
|
+
cp "$STATE_FILE" "${STATE_FILE}.corrupted.$(date +%s)" 2>/dev/null || true
|
|
397
|
+
rm -f "$STATE_FILE"
|
|
398
|
+
# Re-initialize as fresh state (recursive call with file removed)
|
|
399
|
+
init_state
|
|
400
|
+
return
|
|
401
|
+
fi
|
|
402
|
+
|
|
393
403
|
# Update PID and start time in existing state
|
|
394
404
|
locked_state_update \
|
|
395
405
|
--arg pid "$$" \
|
|
@@ -448,6 +458,13 @@ get_active_count() {
|
|
|
448
458
|
echo 0
|
|
449
459
|
return
|
|
450
460
|
fi
|
|
461
|
+
# Validate state file JSON before parsing (mid-flight corruption check)
|
|
462
|
+
if ! jq empty "$STATE_FILE" 2>/dev/null; then
|
|
463
|
+
daemon_log WARN "State file corrupted mid-flight — backing up and resetting"
|
|
464
|
+
cp "$STATE_FILE" "${STATE_FILE}.corrupted.$(date +%s)" 2>/dev/null || true
|
|
465
|
+
init_state
|
|
466
|
+
return
|
|
467
|
+
fi
|
|
451
468
|
jq -r '.active_jobs | length' "$STATE_FILE" 2>/dev/null || echo 0
|
|
452
469
|
}
|
|
453
470
|
|
|
@@ -250,7 +250,7 @@ select_pipeline_template() {
|
|
|
250
250
|
_dora_events=$(tail -500 "${EVENTS_FILE:-$HOME/.shipwright/events.jsonl}" \
|
|
251
251
|
| grep '"type":"pipeline.completed"' 2>/dev/null \
|
|
252
252
|
| tail -5 || true)
|
|
253
|
-
_dora_total=$(echo "$_dora_events" | grep -c '.' 2>/dev/null ||
|
|
253
|
+
_dora_total=$(echo "$_dora_events" | grep -c '.' 2>/dev/null || true)
|
|
254
254
|
_dora_total="${_dora_total:-0}"
|
|
255
255
|
if [[ "$_dora_total" -ge 3 ]]; then
|
|
256
256
|
_dora_failures=$(echo "$_dora_events" | grep -c '"result":"failure"' 2>/dev/null || true)
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
# decide-autonomy.sh — Tier enforcement & rate limiting for the decision engine
|
|
2
|
+
# Source from sw-decide.sh. Requires helpers.sh, policy.sh.
|
|
3
|
+
[[ -n "${_DECIDE_AUTONOMY_LOADED:-}" ]] && return 0
|
|
4
|
+
_DECIDE_AUTONOMY_LOADED=1
|
|
5
|
+
|
|
6
|
+
# ─── State ────────────────────────────────────────────────────────────────────
|
|
7
|
+
DECISIONS_DIR="${HOME}/.shipwright/decisions"
|
|
8
|
+
HALT_FILE="${DECISIONS_DIR}/halt.json"
|
|
9
|
+
LAST_DECISION_FILE="${DECISIONS_DIR}/last-decision.json"
|
|
10
|
+
OUTCOMES_FILE="${DECISIONS_DIR}/outcomes.jsonl"
|
|
11
|
+
|
|
12
|
+
_ensure_decisions_dir() {
|
|
13
|
+
mkdir -p "$DECISIONS_DIR"
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
_daily_log_file() {
|
|
17
|
+
echo "${DECISIONS_DIR}/daily-log-$(date -u +%Y-%m-%d).jsonl"
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
# ─── Tier Configuration ──────────────────────────────────────────────────────
|
|
21
|
+
|
|
22
|
+
TIERS_DATA=""
|
|
23
|
+
CATEGORY_RULES=""
|
|
24
|
+
TIER_LIMITS=""
|
|
25
|
+
|
|
26
|
+
autonomy_load_tiers() {
|
|
27
|
+
local tiers_path="${TIERS_FILE:-}"
|
|
28
|
+
if [[ -z "$tiers_path" ]]; then
|
|
29
|
+
# Try repo-relative, then policy
|
|
30
|
+
local repo_dir="${_REPO_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || echo '.')}"
|
|
31
|
+
tiers_path="${repo_dir}/config/decision-tiers.json"
|
|
32
|
+
if [[ ! -f "$tiers_path" ]]; then
|
|
33
|
+
tiers_path=$(policy_get ".decision.tiers_file" "config/decision-tiers.json")
|
|
34
|
+
[[ "$tiers_path" != /* ]] && tiers_path="${repo_dir}/${tiers_path}"
|
|
35
|
+
fi
|
|
36
|
+
fi
|
|
37
|
+
|
|
38
|
+
if [[ ! -f "$tiers_path" ]]; then
|
|
39
|
+
return 1
|
|
40
|
+
fi
|
|
41
|
+
|
|
42
|
+
TIERS_FILE="$tiers_path"
|
|
43
|
+
TIERS_DATA=$(cat "$tiers_path")
|
|
44
|
+
CATEGORY_RULES=$(echo "$TIERS_DATA" | jq -c '.category_rules // {}')
|
|
45
|
+
TIER_LIMITS=$(echo "$TIERS_DATA" | jq -c '.limits // {}')
|
|
46
|
+
return 0
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
# ─── Tier Resolution ─────────────────────────────────────────────────────────
|
|
50
|
+
|
|
51
|
+
autonomy_resolve_tier() {
|
|
52
|
+
local category="$1"
|
|
53
|
+
if [[ -z "$CATEGORY_RULES" ]]; then
|
|
54
|
+
echo "draft"
|
|
55
|
+
return
|
|
56
|
+
fi
|
|
57
|
+
local tier
|
|
58
|
+
tier=$(echo "$CATEGORY_RULES" | jq -r --arg cat "$category" '.[$cat].tier // "draft"')
|
|
59
|
+
echo "${tier:-draft}"
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
autonomy_get_labels() {
|
|
63
|
+
local tier="$1"
|
|
64
|
+
if [[ -z "$TIERS_DATA" ]]; then
|
|
65
|
+
echo ""
|
|
66
|
+
return
|
|
67
|
+
fi
|
|
68
|
+
echo "$TIERS_DATA" | jq -r --arg t "$tier" '.tiers[$t].labels // [] | join(",")'
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
autonomy_get_template() {
|
|
72
|
+
local tier="$1"
|
|
73
|
+
if [[ -z "$TIERS_DATA" ]]; then
|
|
74
|
+
echo "standard"
|
|
75
|
+
return
|
|
76
|
+
fi
|
|
77
|
+
local tmpl
|
|
78
|
+
tmpl=$(echo "$TIERS_DATA" | jq -r --arg t "$tier" '.tiers[$t].pipeline_template // "standard"')
|
|
79
|
+
[[ "$tmpl" == "null" ]] && tmpl=""
|
|
80
|
+
echo "$tmpl"
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
# ─── Budget Checks ───────────────────────────────────────────────────────────
|
|
84
|
+
|
|
85
|
+
autonomy_check_budget() {
|
|
86
|
+
local tier="$1"
|
|
87
|
+
_ensure_decisions_dir
|
|
88
|
+
|
|
89
|
+
local daily_log
|
|
90
|
+
daily_log=$(_daily_log_file)
|
|
91
|
+
|
|
92
|
+
# Count today's issues created
|
|
93
|
+
local today_count=0
|
|
94
|
+
if [[ -f "$daily_log" ]]; then
|
|
95
|
+
today_count=$(jq -s '[.[] | select(.action == "issue_created" or .action == "draft_written")] | length' "$daily_log" 2>/dev/null || echo "0")
|
|
96
|
+
fi
|
|
97
|
+
|
|
98
|
+
local max_issues
|
|
99
|
+
max_issues=$(echo "${TIER_LIMITS:-{}}" | jq -r '.max_issues_per_day // 15')
|
|
100
|
+
|
|
101
|
+
if [[ "$today_count" -ge "$max_issues" ]]; then
|
|
102
|
+
return 1
|
|
103
|
+
fi
|
|
104
|
+
|
|
105
|
+
# Check cost budget
|
|
106
|
+
local max_cost
|
|
107
|
+
max_cost=$(echo "${TIER_LIMITS:-{}}" | jq -r '.max_cost_per_day_usd // 25')
|
|
108
|
+
local today_cost=0
|
|
109
|
+
if [[ -f "$daily_log" ]]; then
|
|
110
|
+
today_cost=$(jq -s '[.[] | .estimated_cost_usd // 0] | add // 0' "$daily_log" 2>/dev/null || echo "0")
|
|
111
|
+
fi
|
|
112
|
+
|
|
113
|
+
# Only check cost for auto tier (propose/draft are cheap)
|
|
114
|
+
if [[ "$tier" == "auto" ]]; then
|
|
115
|
+
local cost_exceeded
|
|
116
|
+
cost_exceeded=$(echo "$today_cost $max_cost" | awk '{print ($1 >= $2) ? "true" : "false"}')
|
|
117
|
+
if [[ "$cost_exceeded" == "true" ]]; then
|
|
118
|
+
return 1
|
|
119
|
+
fi
|
|
120
|
+
fi
|
|
121
|
+
|
|
122
|
+
return 0
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
# ─── Rate Limiting ────────────────────────────────────────────────────────────
|
|
126
|
+
|
|
127
|
+
autonomy_check_rate_limit() {
|
|
128
|
+
[[ ! -f "$LAST_DECISION_FILE" ]] && return 0
|
|
129
|
+
|
|
130
|
+
local last_epoch
|
|
131
|
+
last_epoch=$(jq -r '.epoch // 0' "$LAST_DECISION_FILE" 2>/dev/null || echo "0")
|
|
132
|
+
local now_e
|
|
133
|
+
now_e=$(now_epoch)
|
|
134
|
+
|
|
135
|
+
local cooldown
|
|
136
|
+
cooldown=$(echo "${TIER_LIMITS:-{}}" | jq -r '.cooldown_seconds // 300')
|
|
137
|
+
|
|
138
|
+
local elapsed=$((now_e - last_epoch))
|
|
139
|
+
if [[ "$elapsed" -lt "$cooldown" ]]; then
|
|
140
|
+
return 1
|
|
141
|
+
fi
|
|
142
|
+
return 0
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
# ─── Halt Management ─────────────────────────────────────────────────────────
|
|
146
|
+
|
|
147
|
+
autonomy_check_halt() {
|
|
148
|
+
[[ -f "$HALT_FILE" ]] && return 1
|
|
149
|
+
return 0
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
autonomy_halt() {
|
|
153
|
+
_ensure_decisions_dir
|
|
154
|
+
local reason="${1:-manual halt}"
|
|
155
|
+
local tmp
|
|
156
|
+
tmp=$(mktemp)
|
|
157
|
+
jq -n --arg reason "$reason" --arg ts "$(now_iso)" --argjson epoch "$(now_epoch)" \
|
|
158
|
+
'{halted: true, reason: $reason, halted_at: $ts, epoch: $epoch}' > "$tmp" && mv "$tmp" "$HALT_FILE"
|
|
159
|
+
emit_event "decision.halted" "reason=$reason"
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
autonomy_resume() {
|
|
163
|
+
if [[ -f "$HALT_FILE" ]]; then
|
|
164
|
+
rm -f "$HALT_FILE"
|
|
165
|
+
emit_event "decision.resumed"
|
|
166
|
+
fi
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
# ─── Consecutive Failure Tracking ─────────────────────────────────────────────
|
|
170
|
+
|
|
171
|
+
autonomy_check_consecutive_failures() {
|
|
172
|
+
_ensure_decisions_dir
|
|
173
|
+
local daily_log
|
|
174
|
+
daily_log=$(_daily_log_file)
|
|
175
|
+
[[ ! -f "$daily_log" ]] && return 0
|
|
176
|
+
|
|
177
|
+
local max_consecutive
|
|
178
|
+
max_consecutive=$(echo "${TIER_LIMITS:-{}}" | jq -r '.halt_after_consecutive_failures // 3')
|
|
179
|
+
|
|
180
|
+
# Get the last N decisions and check if all failed
|
|
181
|
+
local recent
|
|
182
|
+
recent=$(jq -s --argjson n "$max_consecutive" '. | reverse | .[:$n]' "$daily_log" 2>/dev/null || echo '[]')
|
|
183
|
+
local count
|
|
184
|
+
count=$(echo "$recent" | jq 'length' 2>/dev/null || echo "0")
|
|
185
|
+
[[ "$count" -lt "$max_consecutive" ]] && return 0
|
|
186
|
+
|
|
187
|
+
local all_failed
|
|
188
|
+
all_failed=$(echo "$recent" | jq --argjson n "$max_consecutive" \
|
|
189
|
+
'[.[] | select(.outcome == "failure")] | length == $n' 2>/dev/null || echo "false")
|
|
190
|
+
|
|
191
|
+
if [[ "$all_failed" == "true" ]]; then
|
|
192
|
+
autonomy_halt "Halted: ${max_consecutive} consecutive failures"
|
|
193
|
+
return 1
|
|
194
|
+
fi
|
|
195
|
+
return 0
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
# ─── Risk Ceiling ─────────────────────────────────────────────────────────────
|
|
199
|
+
|
|
200
|
+
autonomy_check_risk_ceiling() {
|
|
201
|
+
local category="$1"
|
|
202
|
+
local risk_score="$2"
|
|
203
|
+
[[ -z "$CATEGORY_RULES" ]] && return 0
|
|
204
|
+
|
|
205
|
+
local ceiling
|
|
206
|
+
ceiling=$(echo "$CATEGORY_RULES" | jq -r --arg cat "$category" '.[$cat].risk_ceiling // 100')
|
|
207
|
+
|
|
208
|
+
if [[ "$risk_score" -gt "$ceiling" ]]; then
|
|
209
|
+
return 1
|
|
210
|
+
fi
|
|
211
|
+
return 0
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
# ─── Decision Recording ──────────────────────────────────────────────────────
|
|
215
|
+
|
|
216
|
+
autonomy_record_decision() {
|
|
217
|
+
local decision_json="$1"
|
|
218
|
+
_ensure_decisions_dir
|
|
219
|
+
|
|
220
|
+
local daily_log
|
|
221
|
+
daily_log=$(_daily_log_file)
|
|
222
|
+
|
|
223
|
+
# Append to daily log (atomic via tmp + append)
|
|
224
|
+
echo "$decision_json" >> "$daily_log"
|
|
225
|
+
|
|
226
|
+
# Update last-decision pointer
|
|
227
|
+
local tmp
|
|
228
|
+
tmp=$(mktemp)
|
|
229
|
+
echo "$decision_json" | jq '. + {epoch: (now | floor)}' > "$tmp" && mv "$tmp" "$LAST_DECISION_FILE"
|
|
230
|
+
|
|
231
|
+
# Rotate old daily logs (keep 30 days)
|
|
232
|
+
find "$DECISIONS_DIR" -name "daily-log-*.jsonl" -mtime +30 -delete 2>/dev/null || true
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
autonomy_record_outcome() {
|
|
236
|
+
local decision_id="$1"
|
|
237
|
+
local result="$2"
|
|
238
|
+
local detail="${3:-}"
|
|
239
|
+
_ensure_decisions_dir
|
|
240
|
+
|
|
241
|
+
local outcome
|
|
242
|
+
outcome=$(jq -n \
|
|
243
|
+
--arg id "$decision_id" \
|
|
244
|
+
--arg result "$result" \
|
|
245
|
+
--arg detail "$detail" \
|
|
246
|
+
--arg ts "$(now_iso)" \
|
|
247
|
+
'{decision_id: $id, result: $result, detail: $detail, recorded_at: $ts}')
|
|
248
|
+
|
|
249
|
+
echo "$outcome" >> "$OUTCOMES_FILE"
|
|
250
|
+
|
|
251
|
+
# Update daily log entry with outcome
|
|
252
|
+
local daily_log
|
|
253
|
+
daily_log=$(_daily_log_file)
|
|
254
|
+
if [[ -f "$daily_log" ]]; then
|
|
255
|
+
local tmp
|
|
256
|
+
tmp=$(mktemp)
|
|
257
|
+
jq --arg id "$decision_id" --arg res "$result" \
|
|
258
|
+
'if .id == $id then . + {outcome: $res} else . end' \
|
|
259
|
+
"$daily_log" > "$tmp" && mv "$tmp" "$daily_log" || rm -f "$tmp"
|
|
260
|
+
fi
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
# ─── Daily Summary ────────────────────────────────────────────────────────────
|
|
264
|
+
|
|
265
|
+
autonomy_daily_summary() {
|
|
266
|
+
_ensure_decisions_dir
|
|
267
|
+
local daily_log
|
|
268
|
+
daily_log=$(_daily_log_file)
|
|
269
|
+
|
|
270
|
+
if [[ ! -f "$daily_log" ]]; then
|
|
271
|
+
jq -n '{date: (now | strftime("%Y-%m-%d")), total: 0, auto: 0, propose: 0, draft: 0, budget_remaining: {issues: 15, cost_usd: 25}}'
|
|
272
|
+
return
|
|
273
|
+
fi
|
|
274
|
+
|
|
275
|
+
local max_issues max_cost
|
|
276
|
+
max_issues=$(echo "${TIER_LIMITS:-{}}" | jq -r '.max_issues_per_day // 15')
|
|
277
|
+
max_cost=$(echo "${TIER_LIMITS:-{}}" | jq -r '.max_cost_per_day_usd // 25')
|
|
278
|
+
|
|
279
|
+
jq -s --argjson mi "$max_issues" --arg mc "$max_cost" '
|
|
280
|
+
{
|
|
281
|
+
date: (now | strftime("%Y-%m-%d")),
|
|
282
|
+
total: length,
|
|
283
|
+
auto: [.[] | select(.tier == "auto")] | length,
|
|
284
|
+
propose: [.[] | select(.tier == "propose")] | length,
|
|
285
|
+
draft: [.[] | select(.tier == "draft")] | length,
|
|
286
|
+
successes: [.[] | select(.outcome == "success")] | length,
|
|
287
|
+
failures: [.[] | select(.outcome == "failure")] | length,
|
|
288
|
+
budget_remaining: {
|
|
289
|
+
issues: ($mi - length),
|
|
290
|
+
cost_usd: ($mc - ([.[] | .estimated_cost_usd // 0] | add // 0))
|
|
291
|
+
},
|
|
292
|
+
halted: false
|
|
293
|
+
}
|
|
294
|
+
' "$daily_log" 2>/dev/null || echo '{}'
|
|
295
|
+
}
|