shipwright-cli 3.2.0 → 3.3.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 +2 -0
- package/.claude/agents/devops-engineer.md +2 -0
- package/.claude/agents/doc-fleet-agent.md +2 -0
- package/.claude/agents/pipeline-agent.md +2 -0
- package/.claude/agents/shell-script-specialist.md +2 -0
- package/.claude/agents/test-specialist.md +2 -0
- package/.claude/hooks/agent-crash-capture.sh +32 -0
- package/.claude/hooks/post-tool-use.sh +3 -2
- package/.claude/hooks/pre-tool-use.sh +35 -3
- package/README.md +4 -4
- package/claude-code/hooks/config-change.sh +18 -0
- package/claude-code/hooks/instructions-reloaded.sh +7 -0
- package/claude-code/hooks/worktree-create.sh +25 -0
- package/claude-code/hooks/worktree-remove.sh +20 -0
- package/config/code-constitution.json +130 -0
- package/dashboard/middleware/auth.ts +134 -0
- package/dashboard/middleware/constants.ts +21 -0
- package/dashboard/public/index.html +2 -6
- package/dashboard/public/styles.css +100 -97
- package/dashboard/routes/auth.ts +38 -0
- package/dashboard/server.ts +66 -25
- package/dashboard/services/config.ts +26 -0
- package/dashboard/services/db.ts +118 -0
- package/dashboard/src/canvas/pixel-agent.ts +298 -0
- package/dashboard/src/canvas/pixel-sprites.ts +440 -0
- package/dashboard/src/canvas/shipyard-effects.ts +367 -0
- package/dashboard/src/canvas/shipyard-scene.ts +616 -0
- package/dashboard/src/canvas/submarine-layout.ts +267 -0
- package/dashboard/src/components/header.ts +8 -7
- package/dashboard/src/core/router.ts +1 -0
- package/dashboard/src/design/submarine-theme.ts +253 -0
- package/dashboard/src/main.ts +2 -0
- package/dashboard/src/types/api.ts +2 -1
- package/dashboard/src/views/activity.ts +2 -1
- package/dashboard/src/views/shipyard.ts +39 -0
- package/dashboard/types/index.ts +166 -0
- package/docs/plans/2026-02-28-compound-audit-and-shipyard-design.md +186 -0
- package/docs/plans/2026-02-28-skipper-shipwright-implementation-plan.md +1182 -0
- package/docs/plans/2026-02-28-skipper-shipwright-integration-design.md +531 -0
- package/docs/plans/2026-03-01-ai-powered-skill-injection-design.md +298 -0
- package/docs/plans/2026-03-01-ai-powered-skill-injection-plan.md +1109 -0
- package/docs/plans/2026-03-01-capabilities-cleanup-plan.md +658 -0
- package/docs/plans/2026-03-01-clean-architecture-plan.md +924 -0
- package/docs/plans/2026-03-01-compound-audit-cascade-design.md +191 -0
- package/docs/plans/2026-03-01-compound-audit-cascade-plan.md +921 -0
- package/docs/plans/2026-03-01-deep-integration-plan.md +851 -0
- package/docs/plans/2026-03-01-pipeline-audit-trail-design.md +145 -0
- package/docs/plans/2026-03-01-pipeline-audit-trail-plan.md +770 -0
- package/docs/plans/2026-03-01-refined-depths-brand-design.md +382 -0
- package/docs/plans/2026-03-01-refined-depths-implementation.md +599 -0
- package/docs/plans/2026-03-01-skipper-kernel-integration-design.md +203 -0
- package/docs/plans/2026-03-01-unified-platform-design.md +272 -0
- package/docs/plans/2026-03-07-claude-code-feature-integration-design.md +189 -0
- package/docs/plans/2026-03-07-claude-code-feature-integration-plan.md +1165 -0
- package/docs/research/BACKLOG_QUICK_REFERENCE.md +352 -0
- package/docs/research/CUTTING_EDGE_RESEARCH_2026.md +546 -0
- package/docs/research/RESEARCH_INDEX.md +439 -0
- package/docs/research/RESEARCH_SOURCES.md +440 -0
- package/docs/research/RESEARCH_SUMMARY.txt +275 -0
- package/docs/superpowers/specs/2026-03-10-pipeline-quality-revolution-design.md +341 -0
- package/package.json +2 -2
- package/scripts/lib/adaptive-model.sh +427 -0
- package/scripts/lib/adaptive-timeout.sh +316 -0
- package/scripts/lib/audit-trail.sh +309 -0
- package/scripts/lib/auto-recovery.sh +471 -0
- package/scripts/lib/bandit-selector.sh +431 -0
- package/scripts/lib/bootstrap.sh +104 -2
- package/scripts/lib/causal-graph.sh +455 -0
- package/scripts/lib/compat.sh +126 -0
- package/scripts/lib/compound-audit.sh +337 -0
- package/scripts/lib/constitutional.sh +454 -0
- package/scripts/lib/context-budget.sh +359 -0
- package/scripts/lib/convergence.sh +594 -0
- package/scripts/lib/cost-optimizer.sh +634 -0
- package/scripts/lib/daemon-adaptive.sh +10 -0
- package/scripts/lib/daemon-dispatch.sh +106 -17
- package/scripts/lib/daemon-failure.sh +34 -4
- package/scripts/lib/daemon-patrol.sh +23 -2
- package/scripts/lib/daemon-poll-github.sh +361 -0
- package/scripts/lib/daemon-poll-health.sh +299 -0
- package/scripts/lib/daemon-poll.sh +27 -611
- package/scripts/lib/daemon-state.sh +112 -66
- package/scripts/lib/daemon-triage.sh +10 -0
- package/scripts/lib/dod-scorecard.sh +442 -0
- package/scripts/lib/error-actionability.sh +300 -0
- package/scripts/lib/formal-spec.sh +461 -0
- package/scripts/lib/helpers.sh +177 -4
- package/scripts/lib/intent-analysis.sh +409 -0
- package/scripts/lib/loop-convergence.sh +350 -0
- package/scripts/lib/loop-iteration.sh +682 -0
- package/scripts/lib/loop-progress.sh +48 -0
- package/scripts/lib/loop-restart.sh +185 -0
- package/scripts/lib/memory-effectiveness.sh +506 -0
- package/scripts/lib/mutation-executor.sh +352 -0
- package/scripts/lib/outcome-feedback.sh +521 -0
- package/scripts/lib/pipeline-cli.sh +336 -0
- package/scripts/lib/pipeline-commands.sh +1216 -0
- package/scripts/lib/pipeline-detection.sh +100 -2
- package/scripts/lib/pipeline-execution.sh +897 -0
- package/scripts/lib/pipeline-github.sh +28 -3
- package/scripts/lib/pipeline-intelligence-compound.sh +431 -0
- package/scripts/lib/pipeline-intelligence-scoring.sh +407 -0
- package/scripts/lib/pipeline-intelligence-skip.sh +181 -0
- package/scripts/lib/pipeline-intelligence.sh +100 -1136
- package/scripts/lib/pipeline-quality-bash-compat.sh +182 -0
- package/scripts/lib/pipeline-quality-checks.sh +17 -715
- package/scripts/lib/pipeline-quality-gates.sh +563 -0
- package/scripts/lib/pipeline-stages-build.sh +730 -0
- package/scripts/lib/pipeline-stages-delivery.sh +965 -0
- package/scripts/lib/pipeline-stages-intake.sh +1133 -0
- package/scripts/lib/pipeline-stages-monitor.sh +407 -0
- package/scripts/lib/pipeline-stages-review.sh +1022 -0
- package/scripts/lib/pipeline-stages.sh +59 -2929
- package/scripts/lib/pipeline-state.sh +36 -5
- package/scripts/lib/pipeline-util.sh +487 -0
- package/scripts/lib/policy-learner.sh +438 -0
- package/scripts/lib/process-reward.sh +493 -0
- package/scripts/lib/project-detect.sh +649 -0
- package/scripts/lib/quality-profile.sh +334 -0
- package/scripts/lib/recruit-commands.sh +885 -0
- package/scripts/lib/recruit-learning.sh +739 -0
- package/scripts/lib/recruit-roles.sh +648 -0
- package/scripts/lib/reward-aggregator.sh +458 -0
- package/scripts/lib/rl-optimizer.sh +362 -0
- package/scripts/lib/root-cause.sh +427 -0
- package/scripts/lib/scope-enforcement.sh +445 -0
- package/scripts/lib/session-restart.sh +493 -0
- package/scripts/lib/skill-memory.sh +300 -0
- package/scripts/lib/skill-registry.sh +775 -0
- package/scripts/lib/spec-driven.sh +476 -0
- package/scripts/lib/test-helpers.sh +18 -7
- package/scripts/lib/test-holdout.sh +429 -0
- package/scripts/lib/test-optimizer.sh +511 -0
- package/scripts/shipwright-file-suggest.sh +45 -0
- package/scripts/skills/adversarial-quality.md +61 -0
- package/scripts/skills/api-design.md +44 -0
- package/scripts/skills/architecture-design.md +50 -0
- package/scripts/skills/brainstorming.md +43 -0
- package/scripts/skills/data-pipeline.md +44 -0
- package/scripts/skills/deploy-safety.md +64 -0
- package/scripts/skills/documentation.md +38 -0
- package/scripts/skills/frontend-design.md +45 -0
- package/scripts/skills/generated/.gitkeep +0 -0
- package/scripts/skills/generated/_refinements/.gitkeep +0 -0
- package/scripts/skills/generated/_refinements/adversarial-quality.patch.md +3 -0
- package/scripts/skills/generated/_refinements/architecture-design.patch.md +3 -0
- package/scripts/skills/generated/_refinements/brainstorming.patch.md +3 -0
- package/scripts/skills/generated/cli-version-management.md +29 -0
- package/scripts/skills/generated/collection-system-validation.md +99 -0
- package/scripts/skills/generated/large-scale-c-refactoring-coordination.md +97 -0
- package/scripts/skills/generated/pattern-matching-similarity-scoring.md +195 -0
- package/scripts/skills/generated/test-parallelization-detection.md +65 -0
- package/scripts/skills/observability.md +79 -0
- package/scripts/skills/performance.md +48 -0
- package/scripts/skills/pr-quality.md +49 -0
- package/scripts/skills/product-thinking.md +43 -0
- package/scripts/skills/security-audit.md +49 -0
- package/scripts/skills/systematic-debugging.md +40 -0
- package/scripts/skills/testing-strategy.md +47 -0
- package/scripts/skills/two-stage-review.md +52 -0
- package/scripts/skills/validation-thoroughness.md +55 -0
- package/scripts/sw +9 -3
- package/scripts/sw-activity.sh +9 -2
- package/scripts/sw-adaptive.sh +2 -1
- package/scripts/sw-adversarial.sh +2 -1
- package/scripts/sw-architecture-enforcer.sh +3 -1
- package/scripts/sw-auth.sh +12 -2
- package/scripts/sw-autonomous.sh +5 -1
- package/scripts/sw-changelog.sh +4 -1
- package/scripts/sw-checkpoint.sh +2 -1
- package/scripts/sw-ci.sh +5 -1
- package/scripts/sw-cleanup.sh +4 -26
- package/scripts/sw-code-review.sh +10 -4
- package/scripts/sw-connect.sh +2 -1
- package/scripts/sw-context.sh +2 -1
- package/scripts/sw-cost.sh +48 -3
- package/scripts/sw-daemon.sh +66 -9
- package/scripts/sw-dashboard.sh +3 -1
- package/scripts/sw-db.sh +59 -16
- package/scripts/sw-decide.sh +8 -2
- package/scripts/sw-decompose.sh +360 -17
- package/scripts/sw-deps.sh +4 -1
- package/scripts/sw-developer-simulation.sh +4 -1
- package/scripts/sw-discovery.sh +325 -2
- package/scripts/sw-doc-fleet.sh +4 -1
- package/scripts/sw-docs-agent.sh +3 -1
- package/scripts/sw-docs.sh +2 -1
- package/scripts/sw-doctor.sh +453 -2
- package/scripts/sw-dora.sh +4 -1
- package/scripts/sw-durable.sh +4 -3
- package/scripts/sw-e2e-orchestrator.sh +17 -16
- package/scripts/sw-eventbus.sh +7 -1
- package/scripts/sw-evidence.sh +364 -12
- package/scripts/sw-feedback.sh +550 -9
- package/scripts/sw-fix.sh +20 -1
- package/scripts/sw-fleet-discover.sh +6 -2
- package/scripts/sw-fleet-viz.sh +4 -1
- package/scripts/sw-fleet.sh +5 -1
- package/scripts/sw-github-app.sh +16 -3
- package/scripts/sw-github-checks.sh +3 -2
- package/scripts/sw-github-deploy.sh +3 -2
- package/scripts/sw-github-graphql.sh +18 -7
- package/scripts/sw-guild.sh +5 -1
- package/scripts/sw-heartbeat.sh +5 -30
- package/scripts/sw-hello.sh +67 -0
- package/scripts/sw-hygiene.sh +6 -1
- package/scripts/sw-incident.sh +265 -1
- package/scripts/sw-init.sh +18 -2
- package/scripts/sw-instrument.sh +10 -2
- package/scripts/sw-intelligence.sh +42 -6
- package/scripts/sw-jira.sh +5 -1
- package/scripts/sw-launchd.sh +2 -1
- package/scripts/sw-linear.sh +4 -1
- package/scripts/sw-logs.sh +4 -1
- package/scripts/sw-loop.sh +432 -1128
- package/scripts/sw-memory.sh +356 -2
- package/scripts/sw-mission-control.sh +6 -1
- package/scripts/sw-model-router.sh +481 -26
- package/scripts/sw-otel.sh +13 -4
- package/scripts/sw-oversight.sh +14 -5
- package/scripts/sw-patrol-meta.sh +334 -0
- package/scripts/sw-pipeline-composer.sh +5 -1
- package/scripts/sw-pipeline-vitals.sh +2 -1
- package/scripts/sw-pipeline.sh +53 -2664
- package/scripts/sw-pm.sh +12 -5
- package/scripts/sw-pr-lifecycle.sh +2 -1
- package/scripts/sw-predictive.sh +7 -1
- package/scripts/sw-prep.sh +185 -2
- package/scripts/sw-ps.sh +5 -25
- package/scripts/sw-public-dashboard.sh +15 -3
- package/scripts/sw-quality.sh +2 -1
- package/scripts/sw-reaper.sh +8 -25
- package/scripts/sw-recruit.sh +156 -2303
- package/scripts/sw-regression.sh +19 -12
- package/scripts/sw-release-manager.sh +3 -1
- package/scripts/sw-release.sh +4 -1
- package/scripts/sw-remote.sh +3 -1
- package/scripts/sw-replay.sh +7 -1
- package/scripts/sw-retro.sh +158 -1
- package/scripts/sw-review-rerun.sh +3 -1
- package/scripts/sw-scale.sh +10 -3
- package/scripts/sw-security-audit.sh +6 -1
- package/scripts/sw-self-optimize.sh +6 -3
- package/scripts/sw-session.sh +9 -3
- package/scripts/sw-setup.sh +3 -1
- package/scripts/sw-stall-detector.sh +406 -0
- package/scripts/sw-standup.sh +15 -7
- package/scripts/sw-status.sh +3 -1
- package/scripts/sw-strategic.sh +4 -1
- package/scripts/sw-stream.sh +7 -1
- package/scripts/sw-swarm.sh +18 -6
- package/scripts/sw-team-stages.sh +13 -6
- package/scripts/sw-templates.sh +5 -29
- package/scripts/sw-testgen.sh +7 -1
- package/scripts/sw-tmux-pipeline.sh +4 -1
- package/scripts/sw-tmux-role-color.sh +2 -0
- package/scripts/sw-tmux-status.sh +1 -1
- package/scripts/sw-tmux.sh +3 -1
- package/scripts/sw-trace.sh +3 -1
- package/scripts/sw-tracker-github.sh +3 -0
- package/scripts/sw-tracker-jira.sh +3 -0
- package/scripts/sw-tracker-linear.sh +3 -0
- package/scripts/sw-tracker.sh +3 -1
- package/scripts/sw-triage.sh +2 -1
- package/scripts/sw-upgrade.sh +3 -1
- package/scripts/sw-ux.sh +5 -2
- package/scripts/sw-webhook.sh +3 -1
- package/scripts/sw-widgets.sh +3 -1
- package/scripts/sw-worktree.sh +15 -3
- package/scripts/test-skill-injection.sh +1233 -0
- package/templates/pipelines/autonomous.json +27 -3
- package/templates/pipelines/cost-aware.json +34 -8
- package/templates/pipelines/deployed.json +12 -0
- package/templates/pipelines/enterprise.json +12 -0
- package/templates/pipelines/fast.json +6 -0
- package/templates/pipelines/full.json +27 -3
- package/templates/pipelines/hotfix.json +6 -0
- package/templates/pipelines/standard.json +12 -0
- package/templates/pipelines/tdd.json +12 -0
|
@@ -0,0 +1,634 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# ╔═══════════════════════════════════════════════════════════════════════════╗
|
|
3
|
+
# ║ shipwright cost-optimizer — Dynamic Cost-Performance Pipeline Optimizer ║
|
|
4
|
+
# ║ Real-time budget monitoring · Cost reduction suggestions · Burst mode ║
|
|
5
|
+
# ╚═══════════════════════════════════════════════════════════════════════════╝
|
|
6
|
+
|
|
7
|
+
# Module guard
|
|
8
|
+
[[ -n "${_COST_OPTIMIZER_LOADED:-}" ]] && return 0
|
|
9
|
+
_COST_OPTIMIZER_LOADED=1
|
|
10
|
+
|
|
11
|
+
# ─── Default Paths ──────────────────────────────────────────────────────────
|
|
12
|
+
COST_DIR="${COST_DIR:-${HOME}/.shipwright}"
|
|
13
|
+
BUDGET_FILE="${BUDGET_FILE:-${COST_DIR}/budget.json}"
|
|
14
|
+
COST_FILE="${COST_FILE:-${COST_DIR}/costs.json}"
|
|
15
|
+
ARTIFACTS_DIR="${ARTIFACTS_DIR:-.claude/pipeline-artifacts}"
|
|
16
|
+
|
|
17
|
+
# ─── Safe Defaults (for set -u) ──────────────────────────────────────────────
|
|
18
|
+
SCRIPT_DIR="${SCRIPT_DIR:-.}"
|
|
19
|
+
NOW_ISO="${NOW_ISO:-}"
|
|
20
|
+
NOW_EPOCH="${NOW_EPOCH:-}"
|
|
21
|
+
|
|
22
|
+
# ─── Helper Functions (fallback if not sourced from main script) ─────────────
|
|
23
|
+
[[ "$(type -t info 2>/dev/null)" == "function" ]] || info() { echo -e "\033[38;2;0;212;255m\033[1m▸\033[0m $*"; }
|
|
24
|
+
[[ "$(type -t success 2>/dev/null)" == "function" ]] || success() { echo -e "\033[38;2;74;222;128m\033[1m✓\033[0m $*"; }
|
|
25
|
+
[[ "$(type -t warn 2>/dev/null)" == "function" ]] || warn() { echo -e "\033[38;2;250;204;21m\033[1m⚠\033[0m $*"; }
|
|
26
|
+
[[ "$(type -t error 2>/dev/null)" == "function" ]] || error() { echo -e "\033[38;2;248;113;113m\033[1m✗\033[0m $*" >&2; }
|
|
27
|
+
|
|
28
|
+
if [[ "$(type -t now_iso 2>/dev/null)" != "function" ]]; then
|
|
29
|
+
now_iso() { date -u +"%Y-%m-%dT%H:%M:%SZ"; }
|
|
30
|
+
now_epoch() { date +%s; }
|
|
31
|
+
fi
|
|
32
|
+
|
|
33
|
+
if [[ "$(type -t emit_event 2>/dev/null)" != "function" ]]; then
|
|
34
|
+
emit_event() {
|
|
35
|
+
local event_type="$1"; shift
|
|
36
|
+
mkdir -p "${COST_DIR}"
|
|
37
|
+
local payload
|
|
38
|
+
payload="{\"ts\":\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\",\"type\":\"$event_type\""
|
|
39
|
+
while [[ $# -gt 0 ]]; do
|
|
40
|
+
local key="${1%%=*}" val="${1#*=}"
|
|
41
|
+
payload="${payload},\"${key}\":\"${val}\""
|
|
42
|
+
shift
|
|
43
|
+
done
|
|
44
|
+
echo "${payload}}" >> "${COST_DIR}/events.jsonl"
|
|
45
|
+
}
|
|
46
|
+
fi
|
|
47
|
+
|
|
48
|
+
# ─── Helper to Validate JSON File Existence ──────────────────────────────────
|
|
49
|
+
_ensure_optimizer_files() {
|
|
50
|
+
mkdir -p "$COST_DIR" "$ARTIFACTS_DIR"
|
|
51
|
+
[[ -f "$COST_FILE" ]] || echo '{"entries":[],"summary":{}}' > "$COST_FILE"
|
|
52
|
+
[[ -f "$BUDGET_FILE" ]] || echo '{"daily_budget_usd":0,"enabled":false}' > "$BUDGET_FILE"
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
# ─── Model Pricing (fallback if not loaded from sw-cost.sh) ───────────────────
|
|
56
|
+
if [[ -z "${OPUS_INPUT_PER_M:-}" ]]; then
|
|
57
|
+
OPUS_INPUT_PER_M="15.00"
|
|
58
|
+
OPUS_OUTPUT_PER_M="75.00"
|
|
59
|
+
SONNET_INPUT_PER_M="3.00"
|
|
60
|
+
SONNET_OUTPUT_PER_M="15.00"
|
|
61
|
+
HAIKU_INPUT_PER_M="0.25"
|
|
62
|
+
HAIKU_OUTPUT_PER_M="1.25"
|
|
63
|
+
fi
|
|
64
|
+
|
|
65
|
+
# ─── Calculate Cost for Token Counts ─────────────────────────────────────────
|
|
66
|
+
_costopt_cost_for_tokens() {
|
|
67
|
+
local input_tokens="${1:-0}"
|
|
68
|
+
local output_tokens="${2:-0}"
|
|
69
|
+
local model="${3:-sonnet}"
|
|
70
|
+
|
|
71
|
+
local input_rate output_rate
|
|
72
|
+
case "$model" in
|
|
73
|
+
opus|claude-opus-4*)
|
|
74
|
+
input_rate="${OPUS_INPUT_PER_M}"
|
|
75
|
+
output_rate="${OPUS_OUTPUT_PER_M}"
|
|
76
|
+
;;
|
|
77
|
+
sonnet|claude-sonnet-4*)
|
|
78
|
+
input_rate="${SONNET_INPUT_PER_M}"
|
|
79
|
+
output_rate="${SONNET_OUTPUT_PER_M}"
|
|
80
|
+
;;
|
|
81
|
+
haiku|claude-haiku-4*)
|
|
82
|
+
input_rate="${HAIKU_INPUT_PER_M}"
|
|
83
|
+
output_rate="${HAIKU_OUTPUT_PER_M}"
|
|
84
|
+
;;
|
|
85
|
+
*)
|
|
86
|
+
input_rate="${SONNET_INPUT_PER_M}"
|
|
87
|
+
output_rate="${SONNET_OUTPUT_PER_M}"
|
|
88
|
+
;;
|
|
89
|
+
esac
|
|
90
|
+
|
|
91
|
+
awk -v it="$input_tokens" -v ot="$output_tokens" \
|
|
92
|
+
-v ir="$input_rate" -v or_="$output_rate" \
|
|
93
|
+
'BEGIN { printf "%.4f", (it / 1000000.0 * ir) + (ot / 1000000.0 * or_) }'
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
# ─── 1. costopt_init() ───────────────────────────────────────────────────────
|
|
97
|
+
# Initialize cost optimization for a pipeline run.
|
|
98
|
+
# Loads budget and historical cost data, calculates projections.
|
|
99
|
+
costopt_init() {
|
|
100
|
+
_ensure_optimizer_files
|
|
101
|
+
|
|
102
|
+
local budget_enabled budget_usd
|
|
103
|
+
budget_enabled=$(jq -r '.enabled // false' "$BUDGET_FILE" 2>/dev/null || echo "false")
|
|
104
|
+
budget_usd=$(jq -r '.daily_budget_usd // 0' "$BUDGET_FILE" 2>/dev/null || echo "0")
|
|
105
|
+
|
|
106
|
+
# If budget not enabled, gracefully return
|
|
107
|
+
if [[ "$budget_enabled" != "true" || "$budget_usd" == "0" ]]; then
|
|
108
|
+
# Silent no-op: budget not configured
|
|
109
|
+
return 0
|
|
110
|
+
fi
|
|
111
|
+
|
|
112
|
+
# Calculate today's spending
|
|
113
|
+
local today_start
|
|
114
|
+
today_start=$(date -u +"%Y-%m-%dT00:00:00Z")
|
|
115
|
+
local today_epoch
|
|
116
|
+
today_epoch=$(date -u -jf "%Y-%m-%dT%H:%M:%SZ" "$today_start" +%s 2>/dev/null || date -u -d "$today_start" +%s 2>/dev/null || echo "0")
|
|
117
|
+
|
|
118
|
+
local today_spent
|
|
119
|
+
today_spent=$(jq --argjson cutoff "$today_epoch" \
|
|
120
|
+
'[.entries[] | select(.ts_epoch >= $cutoff) | .cost_usd] | add // 0' \
|
|
121
|
+
"$COST_FILE" 2>/dev/null || echo "0")
|
|
122
|
+
|
|
123
|
+
# Calculate historical average cost per stage
|
|
124
|
+
local stage_costs
|
|
125
|
+
stage_costs=$(jq -r '
|
|
126
|
+
[.entries | group_by(.stage) | .[] |
|
|
127
|
+
{
|
|
128
|
+
stage: .[0].stage,
|
|
129
|
+
avg_cost: (map(.cost_usd) | add / length)
|
|
130
|
+
}] | map("\(.stage):\(.avg_cost)") | join(",")
|
|
131
|
+
' "$COST_FILE" 2>/dev/null || echo "")
|
|
132
|
+
|
|
133
|
+
local remaining_budget
|
|
134
|
+
remaining_budget=$(awk -v budget="$budget_usd" -v spent="$today_spent" 'BEGIN { printf "%.2f", budget - spent }')
|
|
135
|
+
|
|
136
|
+
# Write initial cost optimization state
|
|
137
|
+
local opt_state
|
|
138
|
+
opt_state=$(cat <<EOF
|
|
139
|
+
{
|
|
140
|
+
"initialized_at": "$(now_iso)",
|
|
141
|
+
"daily_budget_usd": $budget_usd,
|
|
142
|
+
"today_spent_usd": $today_spent,
|
|
143
|
+
"remaining_budget_usd": $remaining_budget,
|
|
144
|
+
"avg_pipeline_cost": 0,
|
|
145
|
+
"stage_costs": "$stage_costs",
|
|
146
|
+
"reductions_applied": [],
|
|
147
|
+
"burst_active": false,
|
|
148
|
+
"burst_end_ts": ""
|
|
149
|
+
}
|
|
150
|
+
EOF
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
local tmp_file
|
|
154
|
+
tmp_file=$(mktemp "$ARTIFACTS_DIR/cost-optimization.json.tmp.XXXXXX" 2>/dev/null) || tmp_file="/tmp/costopt-$$.tmp"
|
|
155
|
+
echo "$opt_state" > "$tmp_file"
|
|
156
|
+
mv "$tmp_file" "$ARTIFACTS_DIR/cost-optimization.json" 2>/dev/null || true
|
|
157
|
+
|
|
158
|
+
emit_event "costopt.init" \
|
|
159
|
+
"daily_budget=$budget_usd" \
|
|
160
|
+
"today_spent=$today_spent" \
|
|
161
|
+
"remaining=$remaining_budget"
|
|
162
|
+
|
|
163
|
+
success "Cost optimization initialized: \$$remaining_budget remaining of \$$budget_usd daily budget"
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
# ─── 2. costopt_check_budget() ───────────────────────────────────────────────
|
|
167
|
+
# Real-time budget check with projections.
|
|
168
|
+
# Returns status and writes projection info.
|
|
169
|
+
# Status: under_budget, on_track, over_projecting, budget_exceeded
|
|
170
|
+
costopt_check_budget() {
|
|
171
|
+
local current_pipeline_cost="${1:-0}"
|
|
172
|
+
local remaining_stages="${2:-5}"
|
|
173
|
+
|
|
174
|
+
_ensure_optimizer_files
|
|
175
|
+
|
|
176
|
+
local budget_enabled budget_usd
|
|
177
|
+
budget_enabled=$(jq -r '.enabled // false' "$BUDGET_FILE" 2>/dev/null || echo "false")
|
|
178
|
+
budget_usd=$(jq -r '.daily_budget_usd // 0' "$BUDGET_FILE" 2>/dev/null || echo "0")
|
|
179
|
+
|
|
180
|
+
# If budget not enabled, return under_budget
|
|
181
|
+
if [[ "$budget_enabled" != "true" || "$budget_usd" == "0" ]]; then
|
|
182
|
+
echo "under_budget"
|
|
183
|
+
return 0
|
|
184
|
+
fi
|
|
185
|
+
|
|
186
|
+
# Get today's spending
|
|
187
|
+
local today_start
|
|
188
|
+
today_start=$(date -u +"%Y-%m-%dT00:00:00Z")
|
|
189
|
+
local today_epoch
|
|
190
|
+
today_epoch=$(date -u -jf "%Y-%m-%dT%H:%M:%SZ" "$today_start" +%s 2>/dev/null || date -u -d "$today_start" +%s 2>/dev/null || echo "0")
|
|
191
|
+
|
|
192
|
+
local today_spent
|
|
193
|
+
today_spent=$(jq --argjson cutoff "$today_epoch" \
|
|
194
|
+
'[.entries[] | select(.ts_epoch >= $cutoff) | .cost_usd] | add // 0' \
|
|
195
|
+
"$COST_FILE" 2>/dev/null || echo "0")
|
|
196
|
+
|
|
197
|
+
# Calculate historical average cost per remaining stage
|
|
198
|
+
local avg_per_stage
|
|
199
|
+
avg_per_stage=$(jq -r \
|
|
200
|
+
'[.entries[].cost_usd] | length as $count | if $count > 0 then add / $count / 5 else 0.50 end' \
|
|
201
|
+
"$COST_FILE" 2>/dev/null || echo "0.50")
|
|
202
|
+
|
|
203
|
+
local estimated_remaining
|
|
204
|
+
estimated_remaining=$(awk -v avg="$avg_per_stage" -v stages="$remaining_stages" \
|
|
205
|
+
'BEGIN { printf "%.2f", avg * stages }')
|
|
206
|
+
|
|
207
|
+
local projected_total
|
|
208
|
+
projected_total=$(awk -v current="$current_pipeline_cost" -v est="$estimated_remaining" \
|
|
209
|
+
'BEGIN { printf "%.2f", current + est }')
|
|
210
|
+
|
|
211
|
+
local total_projected
|
|
212
|
+
total_projected=$(awk -v spent="$today_spent" -v proj="$projected_total" \
|
|
213
|
+
'BEGIN { printf "%.2f", spent + proj }')
|
|
214
|
+
|
|
215
|
+
local remaining_budget
|
|
216
|
+
remaining_budget=$(awk -v budget="$budget_usd" -v spent="$today_spent" \
|
|
217
|
+
'BEGIN { printf "%.2f", budget - spent }')
|
|
218
|
+
|
|
219
|
+
local status
|
|
220
|
+
if awk -v total="$total_projected" -v budget="$budget_usd" 'BEGIN { exit !(total > budget) }'; then
|
|
221
|
+
status="budget_exceeded"
|
|
222
|
+
elif awk -v total="$total_projected" -v budget="$budget_usd" -v threshold="$budget_usd * 0.8" \
|
|
223
|
+
'BEGIN { exit !(total > threshold) }'; then
|
|
224
|
+
status="over_projecting"
|
|
225
|
+
elif awk -v total="$total_projected" -v budget="$budget_usd" -v threshold="$budget_usd * 0.6" \
|
|
226
|
+
'BEGIN { exit !(total > threshold) }'; then
|
|
227
|
+
status="on_track"
|
|
228
|
+
else
|
|
229
|
+
status="under_budget"
|
|
230
|
+
fi
|
|
231
|
+
|
|
232
|
+
# Write budget check state
|
|
233
|
+
local check_state
|
|
234
|
+
check_state=$(cat <<EOF
|
|
235
|
+
{
|
|
236
|
+
"checked_at": "$(now_iso)",
|
|
237
|
+
"daily_budget_usd": $budget_usd,
|
|
238
|
+
"today_spent_usd": $today_spent,
|
|
239
|
+
"current_pipeline_usd": $current_pipeline_cost,
|
|
240
|
+
"remaining_stages": $remaining_stages,
|
|
241
|
+
"avg_cost_per_stage": $avg_per_stage,
|
|
242
|
+
"estimated_remaining_usd": $estimated_remaining,
|
|
243
|
+
"projected_pipeline_total_usd": $projected_total,
|
|
244
|
+
"projected_today_total_usd": $total_projected,
|
|
245
|
+
"remaining_budget_usd": $remaining_budget,
|
|
246
|
+
"status": "$status"
|
|
247
|
+
}
|
|
248
|
+
EOF
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
local tmp_file
|
|
252
|
+
tmp_file=$(mktemp "$ARTIFACTS_DIR/cost-check.json.tmp.XXXXXX" 2>/dev/null) || tmp_file="/tmp/costchk-$$.tmp"
|
|
253
|
+
echo "$check_state" > "$tmp_file"
|
|
254
|
+
mv "$tmp_file" "$ARTIFACTS_DIR/cost-check.json" 2>/dev/null || true
|
|
255
|
+
|
|
256
|
+
echo "$status"
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
# ─── 3. costopt_suggest_reductions() ─────────────────────────────────────────
|
|
260
|
+
# Suggest cost reduction actions when over budget.
|
|
261
|
+
# Returns JSON array of suggested reductions with savings estimates.
|
|
262
|
+
costopt_suggest_reductions() {
|
|
263
|
+
local remaining_stages="${1:-5}"
|
|
264
|
+
local current_model="${2:-opus}"
|
|
265
|
+
local current_cost="${3:-0}"
|
|
266
|
+
local budget_usd="${4:-100}"
|
|
267
|
+
local today_spent="${5:-0}"
|
|
268
|
+
|
|
269
|
+
local suggestions="[]"
|
|
270
|
+
|
|
271
|
+
# Calculate total projected cost
|
|
272
|
+
local avg_per_stage
|
|
273
|
+
avg_per_stage=$(jq -r \
|
|
274
|
+
'[.entries[].cost_usd] | length as $count | if $count > 0 then add / $count / 5 else 0.50 end' \
|
|
275
|
+
"$COST_FILE" 2>/dev/null || echo "0.50")
|
|
276
|
+
|
|
277
|
+
local estimated_remaining
|
|
278
|
+
estimated_remaining=$(awk -v avg="$avg_per_stage" -v stages="$remaining_stages" \
|
|
279
|
+
'BEGIN { printf "%.2f", avg * stages }')
|
|
280
|
+
|
|
281
|
+
local projected_today_total
|
|
282
|
+
projected_today_total=$(awk -v spent="$today_spent" -v current="$current_cost" -v est="$estimated_remaining" \
|
|
283
|
+
'BEGIN { printf "%.2f", spent + current + est }')
|
|
284
|
+
|
|
285
|
+
# Suggestion 1: Downgrade model (opus -> sonnet -> haiku)
|
|
286
|
+
if [[ "$current_model" == "opus" ]]; then
|
|
287
|
+
local opus_cost
|
|
288
|
+
opus_cost=$(jq -r '[.entries[] | select(.model == "opus") | .cost_usd] | length as $c | if $c > 0 then add / $c else 15 end' "$COST_FILE" 2>/dev/null || echo "15")
|
|
289
|
+
local sonnet_cost
|
|
290
|
+
sonnet_cost=$(jq -r '[.entries[] | select(.model == "sonnet") | .cost_usd] | length as $c | if $c > 0 then add / $c else 5 end' "$COST_FILE" 2>/dev/null || echo "5")
|
|
291
|
+
local savings
|
|
292
|
+
savings=$(awk -v opus="$opus_cost" -v sonnet="$sonnet_cost" -v stages="$remaining_stages" \
|
|
293
|
+
'BEGIN { printf "%.2f", (opus - sonnet) * stages }')
|
|
294
|
+
|
|
295
|
+
suggestions=$(jq --arg sav "$savings" '.+=[{
|
|
296
|
+
"action": "downgrade_model",
|
|
297
|
+
"from": "opus",
|
|
298
|
+
"to": "sonnet",
|
|
299
|
+
"estimated_savings_usd": ($sav | tonumber),
|
|
300
|
+
"impact": "slower but acceptable for some stages"
|
|
301
|
+
}]' <<< "$suggestions")
|
|
302
|
+
elif [[ "$current_model" == "sonnet" ]]; then
|
|
303
|
+
local sonnet_cost
|
|
304
|
+
sonnet_cost=$(jq -r '[.entries[] | select(.model == "sonnet") | .cost_usd] | length as $c | if $c > 0 then add / $c else 5 end' "$COST_FILE" 2>/dev/null || echo "5")
|
|
305
|
+
local haiku_cost
|
|
306
|
+
haiku_cost=$(jq -r '[.entries[] | select(.model == "haiku") | .cost_usd] | length as $c | if $c > 0 then add / $c else 0.5 end' "$COST_FILE" 2>/dev/null || echo "0.5")
|
|
307
|
+
local savings
|
|
308
|
+
savings=$(awk -v sonnet="$sonnet_cost" -v haiku="$haiku_cost" -v stages="$remaining_stages" \
|
|
309
|
+
'BEGIN { printf "%.2f", (sonnet - haiku) * stages }')
|
|
310
|
+
|
|
311
|
+
suggestions=$(jq --arg sav "$savings" '.+=[{
|
|
312
|
+
"action": "downgrade_model",
|
|
313
|
+
"from": "sonnet",
|
|
314
|
+
"to": "haiku",
|
|
315
|
+
"estimated_savings_usd": ($sav | tonumber),
|
|
316
|
+
"impact": "lowest cost, suitable for routine tasks"
|
|
317
|
+
}]' <<< "$suggestions")
|
|
318
|
+
fi
|
|
319
|
+
|
|
320
|
+
# Suggestion 2: Skip optional stages
|
|
321
|
+
local optional_stage_cost
|
|
322
|
+
optional_stage_cost=$(jq -r '[.entries[] | select(.stage == "compound_quality" or .stage == "adversarial") | .cost_usd] | length as $c | if $c > 0 then add / $c else 2 end' "$COST_FILE" 2>/dev/null || echo "2")
|
|
323
|
+
|
|
324
|
+
suggestions=$(jq --arg sav "$optional_stage_cost" '.+=[{
|
|
325
|
+
"action": "skip_optional_stages",
|
|
326
|
+
"stages": ["compound_quality", "adversarial"],
|
|
327
|
+
"estimated_savings_usd": ($sav | tonumber),
|
|
328
|
+
"impact": "skips quality stages but keeps test coverage"
|
|
329
|
+
}]' <<< "$suggestions")
|
|
330
|
+
|
|
331
|
+
# Suggestion 3: Reduce loop iterations
|
|
332
|
+
suggestions=$(jq '.+=[{
|
|
333
|
+
"action": "reduce_iterations",
|
|
334
|
+
"max_iterations": 3,
|
|
335
|
+
"from_max": 10,
|
|
336
|
+
"estimated_savings_usd": 5.0,
|
|
337
|
+
"impact": "faster convergence but less exploration"
|
|
338
|
+
}]' <<< "$suggestions")
|
|
339
|
+
|
|
340
|
+
# Suggestion 4: Use fast-test-cmd more frequently
|
|
341
|
+
suggestions=$(jq '.+=[{
|
|
342
|
+
"action": "increase_fast_test_frequency",
|
|
343
|
+
"fast_test_interval": 2,
|
|
344
|
+
"from_interval": 5,
|
|
345
|
+
"estimated_savings_usd": 1.5,
|
|
346
|
+
"impact": "run full test suite less often"
|
|
347
|
+
}]' <<< "$suggestions")
|
|
348
|
+
|
|
349
|
+
echo "$suggestions"
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
# ─── 4. costopt_apply_reduction() ────────────────────────────────────────────
|
|
353
|
+
# Apply a cost reduction action to the pipeline config.
|
|
354
|
+
# $1: reduction action name (downgrade_model, skip_optional_stages, reduce_iterations, etc.)
|
|
355
|
+
# $2: JSON config path (if modifiable in-memory)
|
|
356
|
+
# Returns 0 on success
|
|
357
|
+
costopt_apply_reduction() {
|
|
358
|
+
local action="${1:-}"
|
|
359
|
+
local config_path="${2:-}"
|
|
360
|
+
local estimated_savings="${3:-0}"
|
|
361
|
+
|
|
362
|
+
[[ -z "$action" ]] && { error "costopt_apply_reduction: action required"; return 1; }
|
|
363
|
+
|
|
364
|
+
local opt_state
|
|
365
|
+
opt_state="$ARTIFACTS_DIR/cost-optimization.json"
|
|
366
|
+
|
|
367
|
+
# Record applied reduction
|
|
368
|
+
local reduction_record
|
|
369
|
+
reduction_record=$(cat <<EOF
|
|
370
|
+
{
|
|
371
|
+
"applied_at": "$(now_iso)",
|
|
372
|
+
"action": "$action",
|
|
373
|
+
"estimated_savings_usd": $estimated_savings,
|
|
374
|
+
"config_modified": "$config_path"
|
|
375
|
+
}
|
|
376
|
+
EOF
|
|
377
|
+
)
|
|
378
|
+
|
|
379
|
+
# Update optimization state if file exists
|
|
380
|
+
if [[ -f "$opt_state" ]]; then
|
|
381
|
+
local tmp_file
|
|
382
|
+
tmp_file=$(mktemp "$opt_state.tmp.XXXXXX" 2>/dev/null) || tmp_file="/tmp/costopt-red-$$.tmp"
|
|
383
|
+
jq --argjson red "$reduction_record" '.reductions_applied += [$red]' "$opt_state" > "$tmp_file" 2>/dev/null && \
|
|
384
|
+
mv "$tmp_file" "$opt_state" || rm -f "$tmp_file"
|
|
385
|
+
fi
|
|
386
|
+
|
|
387
|
+
emit_event "costopt.reduction_applied" \
|
|
388
|
+
"action=$action" \
|
|
389
|
+
"estimated_savings=$estimated_savings"
|
|
390
|
+
|
|
391
|
+
info "Cost reduction applied: $action (estimated savings: \$$estimated_savings)"
|
|
392
|
+
return 0
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
# ─── 5. costopt_burst_mode() ────────────────────────────────────────────────
|
|
396
|
+
# Temporarily increase budget for critical work (near completion, high convergence).
|
|
397
|
+
# Returns 0 if burst mode activated, 1 if conditions not met.
|
|
398
|
+
# $1: convergence_score (0-100)
|
|
399
|
+
# $2: iteration_count
|
|
400
|
+
# $3: base_iteration_limit
|
|
401
|
+
# $4: max_burst_multiplier (default: 2)
|
|
402
|
+
costopt_burst_mode() {
|
|
403
|
+
local convergence_score="${1:-0}"
|
|
404
|
+
local iteration_count="${2:-0}"
|
|
405
|
+
local base_limit="${3:-10}"
|
|
406
|
+
local max_multiplier="${4:-2}"
|
|
407
|
+
|
|
408
|
+
_ensure_optimizer_files
|
|
409
|
+
|
|
410
|
+
local budget_enabled budget_usd
|
|
411
|
+
budget_enabled=$(jq -r '.enabled // false' "$BUDGET_FILE" 2>/dev/null || echo "false")
|
|
412
|
+
budget_usd=$(jq -r '.daily_budget_usd // 0' "$BUDGET_FILE" 2>/dev/null || echo "0")
|
|
413
|
+
|
|
414
|
+
# Burst requires budget to be enabled
|
|
415
|
+
if [[ "$budget_enabled" != "true" || "$budget_usd" == "0" ]]; then
|
|
416
|
+
return 1
|
|
417
|
+
fi
|
|
418
|
+
|
|
419
|
+
# Conditions for burst: tests passing, convergence > 60, iterations > base
|
|
420
|
+
if [[ "$convergence_score" -lt 60 || "$iteration_count" -le "$base_limit" ]]; then
|
|
421
|
+
return 1
|
|
422
|
+
fi
|
|
423
|
+
|
|
424
|
+
# Calculate burst extension
|
|
425
|
+
local burst_budget
|
|
426
|
+
burst_budget=$(awk -v budget="$budget_usd" -v mult="$max_multiplier" \
|
|
427
|
+
'BEGIN { printf "%.2f", budget * mult }')
|
|
428
|
+
|
|
429
|
+
local burst_end_ts
|
|
430
|
+
burst_end_ts=$(date -u -v+2H +"%Y-%m-%dT%H:%M:%SZ" 2>/dev/null || \
|
|
431
|
+
date -u -d "+2 hours" +"%Y-%m-%dT%H:%M:%SZ" 2>/dev/null || \
|
|
432
|
+
echo "")
|
|
433
|
+
|
|
434
|
+
# Update cost-optimization.json
|
|
435
|
+
if [[ -f "$ARTIFACTS_DIR/cost-optimization.json" ]]; then
|
|
436
|
+
local tmp_file
|
|
437
|
+
tmp_file=$(mktemp "$ARTIFACTS_DIR/cost-optimization.json.tmp.XXXXXX" 2>/dev/null) || tmp_file="/tmp/costopt-burst-$$.tmp"
|
|
438
|
+
jq --arg burst_ts "$burst_end_ts" --arg burst_bud "$burst_budget" \
|
|
439
|
+
'.burst_active = true | .burst_end_ts = $burst_ts | .burst_budget_usd = $burst_bud' \
|
|
440
|
+
"$ARTIFACTS_DIR/cost-optimization.json" > "$tmp_file" 2>/dev/null && \
|
|
441
|
+
mv "$tmp_file" "$ARTIFACTS_DIR/cost-optimization.json" || rm -f "$tmp_file"
|
|
442
|
+
fi
|
|
443
|
+
|
|
444
|
+
emit_event "costopt.burst_activated" \
|
|
445
|
+
"convergence=$convergence_score" \
|
|
446
|
+
"iteration=$iteration_count" \
|
|
447
|
+
"burst_budget=$burst_budget" \
|
|
448
|
+
"expires=$burst_end_ts"
|
|
449
|
+
|
|
450
|
+
warn "Burst mode activated: temporary budget extension to \$$burst_budget (expires $burst_end_ts)"
|
|
451
|
+
return 0
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
# ─── 6. costopt_efficiency_score() ──────────────────────────────────────────
|
|
455
|
+
# Calculate pipeline cost efficiency (0-100).
|
|
456
|
+
# Factors: cost per stage, cost per test, cost per line changed.
|
|
457
|
+
# Higher score = more efficient (less cost per unit work).
|
|
458
|
+
costopt_efficiency_score() {
|
|
459
|
+
local total_cost="${1:-0}"
|
|
460
|
+
local tests_passed="${2:-0}"
|
|
461
|
+
local lines_changed="${3:-0}"
|
|
462
|
+
local stages_completed="${4:-0}"
|
|
463
|
+
|
|
464
|
+
# Metrics with reasonable defaults
|
|
465
|
+
local cost_per_test=999
|
|
466
|
+
local cost_per_line=999
|
|
467
|
+
local cost_per_stage=999
|
|
468
|
+
|
|
469
|
+
[[ "$tests_passed" -gt 0 ]] && \
|
|
470
|
+
cost_per_test=$(awk -v cost="$total_cost" -v tests="$tests_passed" \
|
|
471
|
+
'BEGIN { printf "%.4f", cost / tests }')
|
|
472
|
+
|
|
473
|
+
[[ "$lines_changed" -gt 0 ]] && \
|
|
474
|
+
cost_per_line=$(awk -v cost="$total_cost" -v lines="$lines_changed" \
|
|
475
|
+
'BEGIN { printf "%.6f", cost / lines }')
|
|
476
|
+
|
|
477
|
+
[[ "$stages_completed" -gt 0 ]] && \
|
|
478
|
+
cost_per_stage=$(awk -v cost="$total_cost" -v stages="$stages_completed" \
|
|
479
|
+
'BEGIN { printf "%.2f", cost / stages }')
|
|
480
|
+
|
|
481
|
+
# Normalize to 0-100 score
|
|
482
|
+
# Lower costs = higher score
|
|
483
|
+
# Benchmarks: cost_per_test < $0.05 is excellent
|
|
484
|
+
# cost_per_line < $0.001 is excellent
|
|
485
|
+
# cost_per_stage < $1.00 is excellent
|
|
486
|
+
local test_score
|
|
487
|
+
test_score=$(awk -v cpt="$cost_per_test" 'BEGIN {
|
|
488
|
+
s = 100 * (1 - cpt / 0.10)
|
|
489
|
+
if (s < 0) s = 0
|
|
490
|
+
if (s > 100) s = 100
|
|
491
|
+
printf "%.0f", s
|
|
492
|
+
}')
|
|
493
|
+
|
|
494
|
+
local line_score
|
|
495
|
+
line_score=$(awk -v cpl="$cost_per_line" 'BEGIN {
|
|
496
|
+
s = 100 * (1 - cpl / 0.002)
|
|
497
|
+
if (s < 0) s = 0
|
|
498
|
+
if (s > 100) s = 100
|
|
499
|
+
printf "%.0f", s
|
|
500
|
+
}')
|
|
501
|
+
|
|
502
|
+
local stage_score
|
|
503
|
+
stage_score=$(awk -v cps="$cost_per_stage" 'BEGIN {
|
|
504
|
+
s = 100 * (1 - cps / 2.00)
|
|
505
|
+
if (s < 0) s = 0
|
|
506
|
+
if (s > 100) s = 100
|
|
507
|
+
printf "%.0f", s
|
|
508
|
+
}')
|
|
509
|
+
|
|
510
|
+
# Average the three scores
|
|
511
|
+
local efficiency
|
|
512
|
+
efficiency=$(awk -v ts="$test_score" -v ls="$line_score" -v ss="$stage_score" \
|
|
513
|
+
'BEGIN { printf "%.0f", (ts + ls + ss) / 3 }')
|
|
514
|
+
|
|
515
|
+
echo "$efficiency"
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
# ─── 7. costopt_report() ────────────────────────────────────────────────────
|
|
519
|
+
# Generate cost optimization dashboard/report.
|
|
520
|
+
# $1: output format (text|json) - default: text
|
|
521
|
+
costopt_report() {
|
|
522
|
+
local format="${1:-text}"
|
|
523
|
+
|
|
524
|
+
_ensure_optimizer_files
|
|
525
|
+
|
|
526
|
+
local budget_enabled budget_usd
|
|
527
|
+
budget_enabled=$(jq -r '.enabled // false' "$BUDGET_FILE" 2>/dev/null || echo "false")
|
|
528
|
+
budget_usd=$(jq -r '.daily_budget_usd // 0' "$BUDGET_FILE" 2>/dev/null || echo "0")
|
|
529
|
+
|
|
530
|
+
# If budget not enabled, return minimal report
|
|
531
|
+
if [[ "$budget_enabled" != "true" || "$budget_usd" == "0" ]]; then
|
|
532
|
+
if [[ "$format" == "json" ]]; then
|
|
533
|
+
echo '{"status":"budget_not_configured","daily_budget_usd":0}'
|
|
534
|
+
else
|
|
535
|
+
echo "Budget not configured. Use 'shipwright cost budget set <amount>' to enable cost tracking."
|
|
536
|
+
fi
|
|
537
|
+
return 0
|
|
538
|
+
fi
|
|
539
|
+
|
|
540
|
+
# Calculate today's spending
|
|
541
|
+
local today_start
|
|
542
|
+
today_start=$(date -u +"%Y-%m-%dT00:00:00Z")
|
|
543
|
+
local today_epoch
|
|
544
|
+
today_epoch=$(date -u -jf "%Y-%m-%dT%H:%M:%SZ" "$today_start" +%s 2>/dev/null || date -u -d "$today_start" +%s 2>/dev/null || echo "0")
|
|
545
|
+
|
|
546
|
+
local today_spent
|
|
547
|
+
today_spent=$(jq --argjson cutoff "$today_epoch" \
|
|
548
|
+
'[.entries[] | select(.ts_epoch >= $cutoff) | .cost_usd] | add // 0' \
|
|
549
|
+
"$COST_FILE" 2>/dev/null || echo "0")
|
|
550
|
+
|
|
551
|
+
local remaining
|
|
552
|
+
remaining=$(awk -v budget="$budget_usd" -v spent="$today_spent" \
|
|
553
|
+
'BEGIN { printf "%.2f", budget - spent }')
|
|
554
|
+
|
|
555
|
+
local pct_used
|
|
556
|
+
pct_used=$(awk -v spent="$today_spent" -v budget="$budget_usd" \
|
|
557
|
+
'BEGIN { printf "%.0f", (spent / budget) * 100 }')
|
|
558
|
+
|
|
559
|
+
# Stage breakdown
|
|
560
|
+
local stage_summary
|
|
561
|
+
stage_summary=$(jq -r '
|
|
562
|
+
[.entries | group_by(.stage) | .[] |
|
|
563
|
+
{
|
|
564
|
+
stage: .[0].stage,
|
|
565
|
+
count: length,
|
|
566
|
+
total_cost: (map(.cost_usd) | add),
|
|
567
|
+
avg_cost: (map(.cost_usd) | add / length)
|
|
568
|
+
}] | sort_by(-.total_cost) | .[:10]
|
|
569
|
+
' "$COST_FILE" 2>/dev/null || echo "[]")
|
|
570
|
+
|
|
571
|
+
# Model breakdown
|
|
572
|
+
local model_summary
|
|
573
|
+
model_summary=$(jq -r '
|
|
574
|
+
[.entries | group_by(.model) | .[] |
|
|
575
|
+
{
|
|
576
|
+
model: .[0].model,
|
|
577
|
+
count: length,
|
|
578
|
+
total_cost: (map(.cost_usd) | add)
|
|
579
|
+
}] | sort_by(-.total_cost)
|
|
580
|
+
' "$COST_FILE" 2>/dev/null || echo "[]")
|
|
581
|
+
|
|
582
|
+
if [[ "$format" == "json" ]]; then
|
|
583
|
+
cat <<EOF
|
|
584
|
+
{
|
|
585
|
+
"report_timestamp": "$(now_iso)",
|
|
586
|
+
"budget": {
|
|
587
|
+
"daily_limit_usd": $budget_usd,
|
|
588
|
+
"today_spent_usd": $today_spent,
|
|
589
|
+
"remaining_usd": $remaining,
|
|
590
|
+
"percent_used": $pct_used
|
|
591
|
+
},
|
|
592
|
+
"stage_breakdown": $stage_summary,
|
|
593
|
+
"model_breakdown": $model_summary,
|
|
594
|
+
"burst_enabled": false,
|
|
595
|
+
"optimizations_applied": []
|
|
596
|
+
}
|
|
597
|
+
EOF
|
|
598
|
+
else
|
|
599
|
+
# Text format
|
|
600
|
+
cat <<EOF
|
|
601
|
+
|
|
602
|
+
╔═══════════════════════════════════════════════════════════════╗
|
|
603
|
+
║ Cost Optimization Report ║
|
|
604
|
+
╚═══════════════════════════════════════════════════════════════╝
|
|
605
|
+
|
|
606
|
+
Budget Status:
|
|
607
|
+
Daily limit: \$$budget_usd
|
|
608
|
+
Today spent: \$$today_spent (${pct_used}%)
|
|
609
|
+
Remaining: \$$remaining
|
|
610
|
+
|
|
611
|
+
Top Cost Drivers (by stage):
|
|
612
|
+
$(echo "$stage_summary" | jq -r '.[] | " \(.stage): $\(.total_cost) (\(.count) calls)"' || echo " (no data)")
|
|
613
|
+
|
|
614
|
+
Model Cost Breakdown:
|
|
615
|
+
$(echo "$model_summary" | jq -r '.[] | " \(.model): $\(.total_cost) (\(.count) calls)"' || echo " (no data)")
|
|
616
|
+
|
|
617
|
+
Burst Mode: Disabled (activate when near completion)
|
|
618
|
+
Optimizations Applied: None yet
|
|
619
|
+
|
|
620
|
+
EOF
|
|
621
|
+
fi
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
# ─── Public API ──────────────────────────────────────────────────────────────
|
|
625
|
+
# These functions are the main entry points:
|
|
626
|
+
# costopt_init
|
|
627
|
+
# costopt_check_budget
|
|
628
|
+
# costopt_suggest_reductions
|
|
629
|
+
# costopt_apply_reduction
|
|
630
|
+
# costopt_burst_mode
|
|
631
|
+
# costopt_efficiency_score
|
|
632
|
+
# costopt_report
|
|
633
|
+
|
|
634
|
+
true
|
|
@@ -3,6 +3,16 @@
|
|
|
3
3
|
[[ -n "${_DAEMON_ADAPTIVE_LOADED:-}" ]] && return 0
|
|
4
4
|
_DAEMON_ADAPTIVE_LOADED=1
|
|
5
5
|
|
|
6
|
+
# Defaults for variables normally set by sw-daemon.sh (safe under set -u).
|
|
7
|
+
DAEMON_DIR="${DAEMON_DIR:-${HOME}/.shipwright}"
|
|
8
|
+
STATE_FILE="${STATE_FILE:-${DAEMON_DIR}/daemon-state.json}"
|
|
9
|
+
POLL_INTERVAL="${POLL_INTERVAL:-60}"
|
|
10
|
+
MAX_PARALLEL="${MAX_PARALLEL:-4}"
|
|
11
|
+
EMPTY_QUEUE_CYCLES="${EMPTY_QUEUE_CYCLES:-0}"
|
|
12
|
+
EST_COST_PER_JOB="${EST_COST_PER_JOB:-5.0}"
|
|
13
|
+
WORKER_MEM_GB="${WORKER_MEM_GB:-4}"
|
|
14
|
+
EVENTS_FILE="${EVENTS_FILE:-${DAEMON_DIR}/events.jsonl}"
|
|
15
|
+
|
|
6
16
|
# Adapt poll interval based on queue state
|
|
7
17
|
# Empty queue 5+ cycles → 120s; queue has items → 30s; processing → 60s
|
|
8
18
|
get_adaptive_poll_interval() {
|