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 +2 -2
- package/VERSION +1 -1
- package/autonomy/loki +449 -12
- package/autonomy/run.sh +94 -0
- package/dashboard/__init__.py +1 -1
- package/dashboard/server.py +242 -0
- package/dashboard/static/cost.html +274 -0
- package/dashboard/static/index.html +94 -0
- package/docs/INSTALLATION.md +1 -1
- package/docs/R3-COST-OBSERVABILITY-DESIGN.md +147 -0
- package/docs/R7-ZERO-CONFIG-FIRST-RUN-PLAN.md +137 -0
- package/loki-ts/dist/loki.js +144 -144
- package/mcp/__init__.py +1 -1
- package/package.json +1 -1
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.
|
|
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.
|
|
384
|
+
**v7.12.0 | [Autonomi](https://www.autonomi.dev/) flagship product | ~260 lines core**
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
7.
|
|
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 >
|
|
1221
|
+
# Precedence: --brief > --issue > --prd > positional auto-detect >
|
|
1222
|
+
# LOKI_PRD_FILE env
|
|
1187
1223
|
local detected_type=""
|
|
1188
|
-
if [ -n "$
|
|
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
|
-
|
|
8938
|
-
|
|
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
|