loki-mode 7.10.1 → 7.12.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/SKILL.md CHANGED
@@ -3,7 +3,7 @@ name: loki-mode
3
3
  description: Autonomous spec-to-product system. Triggers on "Loki Mode". Takes a spec (PRD, GitHub issue, OpenAPI doc, etc.) to deployed product via the RARV-C closure loop, with minimal human intervention. Provider-agnostic. Requires --dangerously-skip-permissions flag.
4
4
  ---
5
5
 
6
- # Loki Mode v7.10.1
6
+ # Loki Mode v7.12.0
7
7
 
8
8
  **You are an autonomous agent. You make decisions. You do not ask questions. You do not stop.**
9
9
 
@@ -381,4 +381,4 @@ See `CHANGELOG.md` entries [7.5.7], [7.5.8], [7.5.13] for the per-fix list and r
381
381
 
382
382
  ---
383
383
 
384
- **v7.10.1 | [Autonomi](https://www.autonomi.dev/) flagship product | ~260 lines core**
384
+ **v7.12.0 | [Autonomi](https://www.autonomi.dev/) flagship product | ~260 lines core**
package/VERSION CHANGED
@@ -1 +1 @@
1
- 7.10.1
1
+ 7.12.0
package/autonomy/loki CHANGED
@@ -543,6 +543,7 @@ show_help() {
543
543
  echo " optimize Optimize prompts based on session history"
544
544
  echo " enterprise Enterprise feature management (tokens, OIDC)"
545
545
  echo " metrics [opts] Session productivity report (--json, --last N, --save, --share)"
546
+ echo " cost [opts] Transparent cost view: per-run/project spend + budget (--json, --last N)"
546
547
  echo " dogfood Show self-development statistics"
547
548
  echo " secrets [cmd] API key status and validation (status|validate)"
548
549
  echo " reset [target] Reset session state (all|retries|failed)"
@@ -738,6 +739,17 @@ detect_arg_type() {
738
739
  return 0
739
740
  fi
740
741
 
742
+ # R7 (zero-config first run): a one-line brief. An arg that contains
743
+ # whitespace and matched none of the file/issue/path patterns above is a
744
+ # natural-language brief (e.g. "build a todo app"), NOT a PRD path. This is
745
+ # additive: a single-token arg with no whitespace still falls through to
746
+ # "unknown" (PRD-path back-compat) below. The `--brief` flag is the
747
+ # deterministic escape hatch for the rare single-word brief.
748
+ if [[ "$arg" == *[[:space:]]* ]]; then
749
+ echo "brief"
750
+ return 0
751
+ fi
752
+
741
753
  # Fallback: unknown. Caller treats as PRD for backward compat.
742
754
  echo "unknown"
743
755
  }
@@ -751,6 +763,7 @@ cmd_start() {
751
763
  local args=()
752
764
  local prd_file=""
753
765
  local provider=""
766
+ local brief_text="" # R7: explicit one-line brief (--brief "...")
754
767
  local bmad_project_path=""
755
768
  local openspec_change_path=""
756
769
  local mirofish_url=""
@@ -783,6 +796,8 @@ cmd_start() {
783
796
  echo " PRD mode - path ending in .md/.json/.txt/.yaml -> build from PRD"
784
797
  echo " ISSUE mode - GitHub/GitLab/Jira/Azure DevOps URL, owner/repo#N,"
785
798
  echo " PROJ-123, #123, or bare number -> generate PRD from issue"
799
+ echo " BRIEF mode - a quoted one-line description (with spaces) -> fast"
800
+ echo " zero-config first run; visible artifact + proof quickly"
786
801
  echo " no input - analyze current directory, auto-generate PRD"
787
802
  echo ""
788
803
  echo "Arguments:"
@@ -792,6 +807,7 @@ cmd_start() {
792
807
  echo "Explicit mode flags (override auto-detection):"
793
808
  echo " --prd FILE Force PRD mode with FILE"
794
809
  echo " --issue URL|NUM Force issue mode with URL or number"
810
+ echo " --brief \"TEXT\" Force zero-config brief mode (fast first run)"
795
811
  echo ""
796
812
  echo "Options:"
797
813
  echo " --provider NAME AI provider: claude (default), codex, cline, aider"
@@ -815,6 +831,7 @@ cmd_start() {
815
831
  echo " --mirofish-bg Run MiroFish pipeline in background"
816
832
  echo " --no-mirofish Disable MiroFish even if env var is set"
817
833
  echo " --no-plan Skip auto-shown PRD analysis at startup"
834
+ echo " --brief \"TEXT\" Zero-config fast first run from a one-line brief"
818
835
  echo " --yes, -y Skip confirmation prompts (auto-confirm)"
819
836
  echo ""
820
837
  echo "Issue-mode options (only used when input is an ISSUE-REF):"
@@ -835,6 +852,8 @@ cmd_start() {
835
852
  echo ""
836
853
  echo "Examples:"
837
854
  echo " loki start # Interactive, analyze current dir"
855
+ echo " loki start \"build a todo app\" # BRIEF mode (zero-config fast run)"
856
+ echo " loki start --brief \"snake\" # BRIEF mode (single-word escape hatch)"
838
857
  echo " loki start ./prd.md # PRD mode"
839
858
  echo " loki start https://github.com/o/r/issues/42 # ISSUE mode (GitHub)"
840
859
  echo " loki start 123 # ISSUE mode (current repo GitHub issue)"
@@ -1064,6 +1083,22 @@ cmd_start() {
1064
1083
  no_plan=true
1065
1084
  shift
1066
1085
  ;;
1086
+ --brief)
1087
+ # R7: explicit one-line brief (escape hatch for single-word
1088
+ # briefs that detect_arg_type would otherwise treat as a PRD
1089
+ # path). Forces the zero-config first-run brief sub-path.
1090
+ if [[ -n "${2:-}" ]] && [[ "${2:-}" != --* ]]; then
1091
+ brief_text="$2"
1092
+ shift 2
1093
+ else
1094
+ echo -e "${RED}--brief requires a one-line description (e.g., --brief \"build a todo app\")${NC}"
1095
+ exit 1
1096
+ fi
1097
+ ;;
1098
+ --brief=*)
1099
+ brief_text="${1#*=}"
1100
+ shift
1101
+ ;;
1067
1102
  --budget)
1068
1103
  if [[ -n "${2:-}" ]]; then
1069
1104
  if ! echo "$2" | grep -qE '^[0-9]+(\.[0-9]+)?$'; then
@@ -1183,12 +1218,21 @@ cmd_start() {
1183
1218
  done
1184
1219
 
1185
1220
  # v6.84.0: Unified dispatch based on explicit flags or auto-detection
1186
- # Precedence: --issue > --prd > positional auto-detect > LOKI_PRD_FILE env
1221
+ # Precedence: --brief > --issue > --prd > positional auto-detect >
1222
+ # LOKI_PRD_FILE env
1187
1223
  local detected_type=""
1188
- if [ -n "$explicit_mode" ]; then
1224
+ if [ -n "$brief_text" ]; then
1225
+ # R7: explicit --brief always forces the zero-config brief sub-path.
1226
+ detected_type="brief"
1227
+ elif [ -n "$explicit_mode" ]; then
1189
1228
  detected_type="$explicit_mode"
1190
1229
  elif [ -n "$positional_arg" ]; then
1191
1230
  detected_type=$(detect_arg_type "$positional_arg")
1231
+ # R7: a positional one-line brief (whitespace, not a file/issue) maps to
1232
+ # the brief sub-path; capture the text from the positional arg.
1233
+ if [ "$detected_type" = "brief" ]; then
1234
+ brief_text="$positional_arg"
1235
+ fi
1192
1236
  elif [ -n "${LOKI_PRD_FILE:-}" ]; then
1193
1237
  detected_type="prd"
1194
1238
  prd_file="$LOKI_PRD_FILE"
@@ -1228,6 +1272,51 @@ cmd_start() {
1228
1272
  return $?
1229
1273
  fi
1230
1274
 
1275
+ # R7 (zero-config first run): BRIEF mode. The user gave a one-line brief
1276
+ # (`loki start "build a todo app"` or `loki start --brief "..."`). Synthesize
1277
+ # a forward-looking PRD, switch to the lightweight TTFV profile (honest fast:
1278
+ # capped iterations, council off, simple tier, heavy phases off), and mark
1279
+ # LOKI_TTFV=1 so run.sh prints the "what next / go deeper" framing at the
1280
+ # end. Then fall through to the normal flow, which appends prd_file to args
1281
+ # and execs run.sh. This is additive -- nothing here changes the PRD/issue
1282
+ # paths above.
1283
+ if [ "$detected_type" = "brief" ]; then
1284
+ local version
1285
+ version=$(get_version)
1286
+ local _ttfv_max_iter="${LOKI_MAX_ITERATIONS:-3}"
1287
+ mkdir -p "$LOKI_DIR" 2>/dev/null || true
1288
+ local brief_prd="$LOKI_DIR/brief-prd-$$.md"
1289
+ synthesize_brief_prd "$brief_prd" "$brief_text"
1290
+ prd_file="$brief_prd"
1291
+
1292
+ # Apply the shared lightweight profile and flag the TTFV first-run path.
1293
+ # The signal value ("brief") drives the end-of-run wording in run.sh so
1294
+ # the message matches what actually ran (lightweight, council off).
1295
+ set_ttfv_lightweight_profile "$_ttfv_max_iter"
1296
+ export LOKI_TTFV=brief
1297
+ # Skip the heavy auto-plan analysis -- the brief path is the fast path,
1298
+ # and we already printed the upfront framing below.
1299
+ no_plan=true
1300
+
1301
+ echo -e "${BOLD}Loki Mode v$version - Zero-config first run${NC}"
1302
+ echo ""
1303
+ echo -e "${CYAN}Brief:${NC} $brief_text"
1304
+ echo -e "${DIM}Fast first pass: $_ttfv_max_iter iterations max, council off, simple tier.${NC}"
1305
+ echo -e "${DIM}You will get a runnable artifact and a proof-of-run quickly.${NC}"
1306
+ echo -e "${DIM}Go deeper later with: loki start (full RARV-C depth).${NC}"
1307
+ echo -e "${GREEN}Brief PRD written${NC} to $brief_prd"
1308
+ echo ""
1309
+ fi
1310
+
1311
+ # R7: existing-repo no-arg path also gets the TTFV "what next" framing at the
1312
+ # end of the run. Execution here is UNCHANGED (full-depth no-PRD codebase
1313
+ # analysis + generated-PRD-reuse); we only add accurate end-of-run framing.
1314
+ # The signal value ("repo") drives the full-depth wording in run.sh so the
1315
+ # message does not falsely claim a lightweight pass.
1316
+ if [ "$detected_type" = "empty" ] && [ -z "${LOKI_TTFV:-}" ]; then
1317
+ export LOKI_TTFV=repo
1318
+ fi
1319
+
1231
1320
  # PRD mode: positional arg is the PRD file (if not already set via --prd)
1232
1321
  if [ "$detected_type" = "prd" ] && [ -z "$prd_file" ]; then
1233
1322
  prd_file="$positional_arg"
@@ -8846,6 +8935,65 @@ cmd_demo() {
8846
8935
  return $start_exit
8847
8936
  }
8848
8937
 
8938
+ # R7 (zero-config first run): set the lightweight TTFV execution profile.
8939
+ # Shared by `cmd_quick` and the `loki start "<brief>"` brief sub-path so the
8940
+ # fast first pass means the same thing in both: capped iterations, completion
8941
+ # council off, simple complexity tier, heavy phases off. This is HONEST fast --
8942
+ # it genuinely shortens the path to first visible value; it does not fake
8943
+ # progress. Depth is opt-in via a plain `loki start` re-run.
8944
+ # Usage: set_ttfv_lightweight_profile [max_iter]
8945
+ set_ttfv_lightweight_profile() {
8946
+ local max_iter="${1:-${LOKI_MAX_ITERATIONS:-3}}"
8947
+ export LOKI_MAX_ITERATIONS="$max_iter"
8948
+ export LOKI_COMPLEXITY=simple
8949
+ export LOKI_COUNCIL_ENABLED=false
8950
+ export LOKI_PHASE_CODE_REVIEW=false
8951
+ export LOKI_PHASE_PERFORMANCE=false
8952
+ export LOKI_PHASE_ACCESSIBILITY=false
8953
+ export LOKI_PHASE_REGRESSION=false
8954
+ export LOKI_PHASE_UAT=false
8955
+ export LOKI_PHASE_WEB_RESEARCH=false
8956
+ }
8957
+
8958
+ # R7: synthesize a forward-looking PRD from a one-line brief. Writes to a
8959
+ # unique path (caller-provided) and echoes nothing -- the file is the output.
8960
+ # Kept DISTINCT from .loki/generated-prd.md (codebase-analysis artifact) so it
8961
+ # never pollutes the v7.8.1 generated-PRD-reuse signature logic. The brief text
8962
+ # is the project intent; the rest is a minimal scaffold the agent fills in.
8963
+ # Usage: synthesize_brief_prd <output_file> <brief_text>
8964
+ synthesize_brief_prd() {
8965
+ local out_file="$1"
8966
+ local brief_text="$2"
8967
+ local out_dir
8968
+ out_dir="$(dirname "$out_file")"
8969
+ mkdir -p "$out_dir" 2>/dev/null || true
8970
+ cat > "$out_file" << BRIEFEOF
8971
+ # Project Brief
8972
+
8973
+ ## Overview
8974
+ $brief_text
8975
+
8976
+ ## Requirements
8977
+ - Build the smallest working version of the above that a user can see and run.
8978
+ - Prefer a runnable artifact (a page, a CLI, an API endpoint) over scaffolding.
8979
+ - Follow conventional structure for the chosen stack; keep dependencies minimal.
8980
+ - Write a short README describing how to run it.
8981
+
8982
+ ## Success Criteria
8983
+ - A user can run the result and observe the core behavior described above.
8984
+ - No errors on a clean start; the happy path works end to end.
8985
+
8986
+ ## Constraints
8987
+ - This is a fast first pass (zero-config first run). Keep scope tight.
8988
+ - Do not over-engineer; depth and hardening come on a later full run.
8989
+ - No emojis, no em dashes in code, comments, or docs.
8990
+
8991
+ ---
8992
+
8993
+ **Mode:** Brief (zero-config first run, lightweight first pass)
8994
+ BRIEFEOF
8995
+ }
8996
+
8849
8997
  # Quick mode - lightweight single-task execution
8850
8998
  cmd_quick() {
8851
8999
  # v7.6.3 B-11 fix: --help previously fell through to provider invocation
@@ -8933,16 +9081,9 @@ QPRDEOF
8933
9081
  echo -e "${GREEN}Quick PRD generated${NC} at $quick_prd"
8934
9082
  echo ""
8935
9083
 
8936
- # Set lightweight execution environment
8937
- export LOKI_MAX_ITERATIONS="$max_iter"
8938
- export LOKI_COMPLEXITY=simple
8939
- export LOKI_COUNCIL_ENABLED=false
8940
- export LOKI_PHASE_CODE_REVIEW=false
8941
- export LOKI_PHASE_PERFORMANCE=false
8942
- export LOKI_PHASE_ACCESSIBILITY=false
8943
- export LOKI_PHASE_REGRESSION=false
8944
- export LOKI_PHASE_UAT=false
8945
- export LOKI_PHASE_WEB_RESEARCH=false
9084
+ # Set lightweight execution environment (shared TTFV profile -- see
9085
+ # set_ttfv_lightweight_profile; same profile the R7 brief sub-path uses).
9086
+ set_ttfv_lightweight_profile "$max_iter"
8946
9087
 
8947
9088
  # Record start for efficiency tracking
8948
9089
  record_session_start
@@ -13034,6 +13175,9 @@ main() {
13034
13175
  metrics)
13035
13176
  cmd_metrics "$@"
13036
13177
  ;;
13178
+ cost)
13179
+ cmd_cost "$@"
13180
+ ;;
13037
13181
  syslog)
13038
13182
  cmd_syslog "$@"
13039
13183
  ;;
@@ -17833,6 +17977,299 @@ cmd_syslog() {
17833
17977
  esac
17834
17978
  }
17835
17979
 
17980
+ # Transparent cost view (R3): per-run + per-project spend, model routing, and
17981
+ # budget status with the 80% warn line. Reuses efficiency_cost.collect_efficiency
17982
+ # for the current-run aggregate (single source of truth) and reads .loki/proofs/
17983
+ # for persistent per-run history. Honest: prints "not recorded" when cost was
17984
+ # never collected, never a fabricated $0.00.
17985
+ cmd_cost() {
17986
+ local show_json=false
17987
+ local last_n=0
17988
+
17989
+ while [[ $# -gt 0 ]]; do
17990
+ case "$1" in
17991
+ --help|-h)
17992
+ echo -e "${BOLD}loki cost${NC} - Transparent cost and budget view"
17993
+ echo ""
17994
+ echo "Usage: loki cost [options]"
17995
+ echo ""
17996
+ echo "Shows the current run's spend (from .loki/metrics/efficiency/),"
17997
+ echo "per-run history (from .loki/proofs/), model routing by spend, and"
17998
+ echo "budget status. Budgets warn at 80% and hard-stop at 100%."
17999
+ echo ""
18000
+ echo "Options:"
18001
+ echo " --json Machine-readable JSON output"
18002
+ echo " --last N Show only the last N runs in history (default: all)"
18003
+ echo " --help, -h Show this help"
18004
+ echo ""
18005
+ echo "Examples:"
18006
+ echo " loki cost # Cost summary + budget status"
18007
+ echo " loki cost --json # Machine-readable output"
18008
+ echo " loki cost --last 10 # Last 10 runs of history"
18009
+ echo ""
18010
+ echo "Budget cap: set LOKI_BUDGET_LIMIT (USD). Warns at 80%, stops at 100%."
18011
+ exit 0
18012
+ ;;
18013
+ --json) show_json=true; shift ;;
18014
+ --last) last_n="${2:-0}"; shift 2 ;;
18015
+ --last=*) last_n="${1#*=}"; shift ;;
18016
+ *) echo -e "${RED}Unknown option: $1${NC}"; echo "Run 'loki cost --help' for usage."; exit 1 ;;
18017
+ esac
18018
+ done
18019
+
18020
+ local loki_dir="${LOKI_DIR:-.loki}"
18021
+
18022
+ if ! command -v python3 &>/dev/null; then
18023
+ echo -e "${RED}python3 is required for the cost view${NC}"
18024
+ exit 1
18025
+ fi
18026
+
18027
+ LOKI_DIR="$loki_dir" \
18028
+ LOKI_SKILL_DIR="$SKILL_DIR" \
18029
+ COST_JSON="$show_json" \
18030
+ COST_LAST_N="$last_n" \
18031
+ COST_BUDGET_LIMIT="${LOKI_BUDGET_LIMIT:-}" \
18032
+ python3 << 'COST_SCRIPT'
18033
+ import json
18034
+ import os
18035
+ import sys
18036
+
18037
+ loki_dir = os.environ.get("LOKI_DIR", ".loki")
18038
+ skill_dir = os.environ.get("LOKI_SKILL_DIR", "")
18039
+ show_json = os.environ.get("COST_JSON", "false") == "true"
18040
+ try:
18041
+ last_n = int(os.environ.get("COST_LAST_N", "0") or "0")
18042
+ except ValueError:
18043
+ last_n = 0
18044
+ budget_limit_env = os.environ.get("COST_BUDGET_LIMIT", "").strip()
18045
+
18046
+ # ANSI (suppressed under --json / non-tty)
18047
+ use_color = (not show_json) and sys.stdout.isatty()
18048
+ BOLD = "\033[1m" if use_color else ""
18049
+ DIM = "\033[2m" if use_color else ""
18050
+ CYAN = "\033[36m" if use_color else ""
18051
+ GREEN = "\033[32m" if use_color else ""
18052
+ YELLOW = "\033[33m" if use_color else ""
18053
+ RED = "\033[31m" if use_color else ""
18054
+ NC = "\033[0m" if use_color else ""
18055
+
18056
+ # Reuse the shared cost lib (single source of truth). Never duplicate the
18057
+ # cost-summing logic; collect_efficiency returns usd=None when nothing was
18058
+ # recorded, which we surface honestly.
18059
+ collect_efficiency = None
18060
+ if skill_dir:
18061
+ lib_dir = os.path.join(skill_dir, "autonomy", "lib")
18062
+ if lib_dir not in sys.path:
18063
+ sys.path.insert(0, lib_dir)
18064
+ try:
18065
+ from efficiency_cost import collect_efficiency as _ce
18066
+ collect_efficiency = _ce
18067
+ except Exception:
18068
+ collect_efficiency = None
18069
+
18070
+ def _fmt_usd(v):
18071
+ if v is None:
18072
+ return "not recorded"
18073
+ try:
18074
+ n = float(v)
18075
+ except (TypeError, ValueError):
18076
+ return "not recorded"
18077
+ s = ("%.4f" % n).rstrip("0").rstrip(".")
18078
+ if "." not in s:
18079
+ s += ".00"
18080
+ elif len(s.split(".")[1]) == 1:
18081
+ s += "0"
18082
+ return "$" + s
18083
+
18084
+ # --- current run aggregate (reuse collect_efficiency, single source) -----
18085
+ # We do NOT re-implement the cost sum here: efficiency_cost.collect_efficiency
18086
+ # is the single source of truth (shared with the proof generator and the R2
18087
+ # benchmark adapters). On a broken install where the lib is missing, we degrade
18088
+ # honestly rather than ship a divergent 5th copy of the cost math.
18089
+ current_cost = None
18090
+ current_model = ""
18091
+ lib_available = collect_efficiency is not None
18092
+ if lib_available:
18093
+ try:
18094
+ cost_dict, current_model = collect_efficiency(loki_dir)
18095
+ current_cost = cost_dict.get("usd")
18096
+ except Exception:
18097
+ current_cost = None
18098
+
18099
+ # --- per-run history from .loki/proofs/ ----------------------------------
18100
+ runs = []
18101
+ project_total = 0.0
18102
+ proofs_dir = os.path.join(loki_dir, "proofs")
18103
+ if os.path.isdir(proofs_dir):
18104
+ for name in sorted(os.listdir(proofs_dir)):
18105
+ run_dir = os.path.join(proofs_dir, name)
18106
+ proof_json = os.path.join(run_dir, "proof.json")
18107
+ if not os.path.isfile(proof_json):
18108
+ continue
18109
+ try:
18110
+ d = json.load(open(proof_json))
18111
+ except Exception:
18112
+ continue
18113
+ if not isinstance(d, dict):
18114
+ continue
18115
+ run_cost = (d.get("cost") or {}).get("usd")
18116
+ run_cost_num = None
18117
+ if run_cost is not None:
18118
+ try:
18119
+ run_cost_num = float(run_cost)
18120
+ project_total += run_cost_num
18121
+ except (TypeError, ValueError):
18122
+ run_cost_num = None
18123
+ runs.append({
18124
+ "run_id": d.get("run_id", name),
18125
+ "generated_at": d.get("generated_at"),
18126
+ "model": (d.get("provider") or {}).get("model"),
18127
+ "cost_usd": run_cost_num,
18128
+ "files_changed": (d.get("files_changed") or {}).get("count"),
18129
+ "final_verdict": (d.get("council") or {}).get("final_verdict"),
18130
+ })
18131
+ runs.sort(key=lambda x: (x.get("generated_at") or ""), reverse=True)
18132
+ if last_n > 0:
18133
+ runs = runs[:last_n]
18134
+
18135
+ # --- budget status (read-time; warn at 80%, exceeded at 100%) ------------
18136
+ budget_limit = None
18137
+ budget_file = os.path.join(loki_dir, "metrics", "budget.json")
18138
+ if os.path.isfile(budget_file):
18139
+ try:
18140
+ bd = json.load(open(budget_file))
18141
+ budget_limit = bd.get("limit") or bd.get("budget_limit")
18142
+ except Exception:
18143
+ budget_limit = None
18144
+ if budget_limit is None and budget_limit_env:
18145
+ try:
18146
+ budget_limit = float(budget_limit_env)
18147
+ except ValueError:
18148
+ budget_limit = None
18149
+ if budget_limit is not None:
18150
+ try:
18151
+ budget_limit = float(budget_limit)
18152
+ except (TypeError, ValueError):
18153
+ budget_limit = None
18154
+
18155
+ budget_used = current_cost if isinstance(current_cost, (int, float)) else 0.0
18156
+ status = "none"
18157
+ percent_used = None
18158
+ remaining = None
18159
+ if budget_limit is not None and budget_limit > 0:
18160
+ percent_used = round(budget_used / budget_limit * 100, 2)
18161
+ remaining = max(0.0, budget_limit - budget_used)
18162
+ if budget_used >= budget_limit:
18163
+ status = "exceeded"
18164
+ elif budget_used >= 0.80 * budget_limit:
18165
+ status = "warn"
18166
+ else:
18167
+ status = "ok"
18168
+
18169
+ # --- model routing by spend (from run history) ---------------------------
18170
+ by_model = {}
18171
+ for r in runs:
18172
+ c = r.get("cost_usd")
18173
+ if c is None:
18174
+ continue
18175
+ m = r.get("model") or "unknown"
18176
+ by_model[m] = by_model.get(m, 0.0) + c
18177
+
18178
+ if show_json:
18179
+ out = {
18180
+ "current_run": {
18181
+ "cost_usd": current_cost,
18182
+ "model": current_model or None,
18183
+ "cost_recorded": current_cost is not None,
18184
+ "cost_lib_available": lib_available,
18185
+ },
18186
+ "runs": runs,
18187
+ "runs_count": len(runs),
18188
+ "project_total_usd": round(project_total, 6) if runs else 0.0,
18189
+ "by_model": {k: round(v, 6) for k, v in by_model.items()},
18190
+ "budget": {
18191
+ "limit": budget_limit,
18192
+ "used": round(budget_used, 6),
18193
+ "remaining": round(remaining, 6) if remaining is not None else None,
18194
+ "percent_used": percent_used,
18195
+ "status": status,
18196
+ "warn_threshold_percent": 80,
18197
+ "exceeded": status == "exceeded",
18198
+ },
18199
+ }
18200
+ print(json.dumps(out, indent=2))
18201
+ sys.exit(0)
18202
+
18203
+ # --- human-readable ------------------------------------------------------
18204
+ ds = chr(36)
18205
+ print()
18206
+ print(BOLD + "Loki Cost" + NC)
18207
+ print(DIM + "=" * 50 + NC)
18208
+
18209
+ print()
18210
+ print(CYAN + "Current run" + NC)
18211
+ if not lib_available:
18212
+ print(DIM + " Cost library unavailable (efficiency_cost.py not found)." + NC)
18213
+ print(DIM + " Current-run spend cannot be computed on this install." + NC)
18214
+ elif current_cost is None:
18215
+ print(" Cost not recorded for this run.")
18216
+ else:
18217
+ mtxt = (" (" + current_model + ")") if current_model else ""
18218
+ print(" Spend: " + BOLD + _fmt_usd(current_cost) + NC + mtxt)
18219
+
18220
+ print()
18221
+ print(CYAN + "Project history" + NC)
18222
+ print(" Runs recorded: " + str(len(runs)))
18223
+ print(" Total spend: " + BOLD + (_fmt_usd(project_total) if runs else "$0.00") + NC)
18224
+
18225
+ if by_model:
18226
+ print()
18227
+ print(CYAN + "Model routing (by spend)" + NC)
18228
+ total_m = sum(by_model.values()) or 1.0
18229
+ for m in sorted(by_model, key=lambda k: by_model[k], reverse=True):
18230
+ v = by_model[m]
18231
+ pct = v / total_m * 100
18232
+ bar_len = int(pct / 5)
18233
+ bar = "#" * bar_len + "." * (20 - bar_len)
18234
+ print(" {:<16} {}{:>9} ({:4.1f}%) [{}]".format(m[:16], "", _fmt_usd(v), pct, bar))
18235
+
18236
+ print()
18237
+ print(CYAN + "Budget" + NC)
18238
+ if budget_limit is None:
18239
+ print(" No cap set. Set LOKI_BUDGET_LIMIT (USD) to cap spend.")
18240
+ print(DIM + " When set, Loki warns at 80% and hard-stops at 100%." + NC)
18241
+ else:
18242
+ col = GREEN
18243
+ if status == "warn":
18244
+ col = YELLOW
18245
+ elif status == "exceeded":
18246
+ col = RED
18247
+ print(" Cap: " + _fmt_usd(budget_limit))
18248
+ print(" Used: " + _fmt_usd(budget_used) + " (" + col + str(percent_used) + "%" + NC + ")")
18249
+ print(" Remaining: " + _fmt_usd(remaining))
18250
+ print(" Status: " + col + BOLD + status.upper() + NC)
18251
+ if status == "warn":
18252
+ print(YELLOW + " Warning: at or above 80% of cap. Run continues; hard-stop at 100%." + NC)
18253
+ elif status == "exceeded":
18254
+ print(RED + " Cap reached. The run is paused to prevent a surprise bill." + NC)
18255
+
18256
+ if runs:
18257
+ print()
18258
+ print(CYAN + "Recent runs" + NC)
18259
+ print(DIM + " {:<28} {:<10} {:>9} {}".format("Run", "Model", "Cost", "Verdict") + NC)
18260
+ for r in runs[:max(last_n, 10) if last_n else 10]:
18261
+ rid = str(r.get("run_id") or "")[:28]
18262
+ mdl = str(r.get("model") or "")[:10]
18263
+ cst = _fmt_usd(r.get("cost_usd"))
18264
+ vrd = str(r.get("final_verdict") or "")
18265
+ print(" {:<28} {:<10} {:>9} {}".format(rid, mdl, cst, vrd))
18266
+
18267
+ print()
18268
+ print(DIM + "Dashboard cost panel: /cost | JSON: loki cost --json" + NC)
18269
+ print()
18270
+ COST_SCRIPT
18271
+ }
18272
+
17836
18273
  # Fetch and display Prometheus metrics from dashboard
17837
18274
  cmd_metrics() {
17838
18275
  local show_json=false