loki-mode 11.76.0 → 11.78.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/lib/check_author.py +146 -0
- package/autonomy/lib/claude-flags.sh +5 -1
- package/autonomy/lib/mcp-config.sh +41 -1
- package/autonomy/lib/proof-analytics-props.py +115 -6
- package/autonomy/lib/proof-verify.py +22 -0
- package/autonomy/lib/telemetry-schema.json +52 -0
- package/autonomy/loki +398 -7
- package/autonomy/telemetry.sh +70 -31
- package/completions/_loki +19 -0
- package/completions/loki.bash +17 -1
- package/dashboard/__init__.py +1 -1
- package/dashboard/api_start.py +2 -10
- package/dashboard/secure_tmp.py +92 -0
- package/dashboard/telemetry.py +81 -15
- package/docs/ARCHITECTURE.md +6 -4
- package/docs/{v10/CLI-MODERN.md → CLI-INVENTORY.md} +6 -6
- package/docs/CLI-REFERENCE.md +3 -2
- package/docs/COMPARISON.md +6 -8
- package/docs/COMPONENTS.md +1 -1
- package/docs/INSTALLATION.md +1 -1
- package/docs/PRIVACY.md +47 -11
- package/docs/V10-RESEARCH.md +1 -1
- package/docs/architecture/ADR-001-runtime-migration.md +7 -7
- package/docs/cursor-comparison.md +1 -1
- package/docs/dev/architecture-reference.md +7 -2
- package/docs/dev/project-structure.md +2 -1
- package/docs/image-provenance.md +32 -7
- package/docs/slack.md +2 -0
- package/docs/v11/COST-HALF.md +2 -1
- package/loki-ts/dist/loki.js +1744 -1604
- package/loki-ts/package.json +2 -2
- package/mcp/__init__.py +1 -1
- package/mcp/requirements.txt +1 -1
- package/package.json +3 -3
- package/packages/control-plane/dist/server.js +20045 -19119
- package/plugins/loki-mode/.claude-plugin/plugin.json +1 -1
- package/providers/aider.sh +7 -0
- package/providers/claude.sh +8 -1
- package/providers/cline.sh +3 -0
- package/providers/loader.sh +5 -0
- package/providers/model_catalog.json +3 -0
- package/providers/models.sh +59 -0
- package/providers/opencode.sh +9 -3
- package/skills/providers.md +1 -0
- package/web-app/dist/assets/{AdminPage-DoGPqZIa.js → AdminPage-BPF2Xehf.js} +1 -1
- package/web-app/dist/assets/{Avatar-Dyf7xJsz.js → Avatar-CSW9BhBU.js} +1 -1
- package/web-app/dist/assets/{Badge-Cm4dADew.js → Badge-wgm79wim.js} +1 -1
- package/web-app/dist/assets/{Button-LGxjldY5.js → Button-BcIAcPKr.js} +1 -1
- package/web-app/dist/assets/{CockpitPage-DuC9g48J.js → CockpitPage-BFi-0Kwj.js} +1 -1
- package/web-app/dist/assets/{ComparePage-DuTrmedK.js → ComparePage-B7Wibrfn.js} +1 -1
- package/web-app/dist/assets/{ErrorBoundary-B0f7NEiT.js → ErrorBoundary-BYyOVh2c.js} +1 -1
- package/web-app/dist/assets/{EvidenceReceiptPanel-DsE8B9Hw.js → EvidenceReceiptPanel-C9QcrjNl.js} +1 -1
- package/web-app/dist/assets/{GitHubIssuesPanel-aYflOqd4.js → GitHubIssuesPanel-CWbuBxVZ.js} +1 -1
- package/web-app/dist/assets/{GitHubPRsPanel-CCJGck6c.js → GitHubPRsPanel-qcBHK615.js} +1 -1
- package/web-app/dist/assets/{HomePage-BJU3PVkJ.js → HomePage-B1mYFU4t.js} +2 -2
- package/web-app/dist/assets/{LoginPage-UzYoF5CJ.js → LoginPage-CmIO1xL7.js} +1 -1
- package/web-app/dist/assets/{MagicPage-5X3hQ7lt.js → MagicPage-L7UP4qKP.js} +1 -1
- package/web-app/dist/assets/{MetricsPage-CFsTNyOu.js → MetricsPage-DVPCjN1U.js} +1 -1
- package/web-app/dist/assets/{NotFoundPage-Ba0nMlqG.js → NotFoundPage-D3XZmCH5.js} +1 -1
- package/web-app/dist/assets/{ProjectPage-fvU-EYZb.js → ProjectPage-BlaMzlVi.js} +3 -3
- package/web-app/dist/assets/{ProjectsPage-CVEKCorG.js → ProjectsPage-DN7W28Zq.js} +1 -1
- package/web-app/dist/assets/{SettingsPage-JGbsNmqD.js → SettingsPage-2u9ODYB5.js} +1 -1
- package/web-app/dist/assets/{ShowcasePage-Dr2c_V-y.js → ShowcasePage-CoKuTfab.js} +1 -1
- package/web-app/dist/assets/{SystemSettingsPage-D9dCc4Ir.js → SystemSettingsPage-Crhj2XAU.js} +1 -1
- package/web-app/dist/assets/{TeamsPage-B56u-bod.js → TeamsPage-DehMgnKh.js} +1 -1
- package/web-app/dist/assets/{TemplatesPage-CUwqj6ge.js → TemplatesPage-DGtwfZgU.js} +1 -1
- package/web-app/dist/assets/{TerminalOutput-Cm920SDi.js → TerminalOutput-7HZRtXoC.js} +1 -1
- package/web-app/dist/assets/{activity-DDF6juD-.js → activity-DAp5wswo.js} +1 -1
- package/web-app/dist/assets/{bell-7MW_oAcb.js → bell-Ncvnrz7d.js} +1 -1
- package/web-app/dist/assets/{bot-b63uz2Ov.js → bot-BWob3K3E.js} +1 -1
- package/web-app/dist/assets/{check-2oBMAALa.js → check-DReWA2vm.js} +1 -1
- package/web-app/dist/assets/{chevron-left-UuhI6pbU.js → chevron-left-CBJpzVU1.js} +1 -1
- package/web-app/dist/assets/{circle-alert-BikRzQA7.js → circle-alert-2c_vs5PW.js} +1 -1
- package/web-app/dist/assets/{clock-BdkTaAgO.js → clock-BrS_ZKKi.js} +1 -1
- package/web-app/dist/assets/{cloud-oKg24kTO.js → cloud-Czvuo51R.js} +1 -1
- package/web-app/dist/assets/{code-xml-CpDmdnYQ.js → code-xml-BWEqJ2I8.js} +1 -1
- package/web-app/dist/assets/{database-BT133u2w.js → database-CJ6M-oK-.js} +1 -1
- package/web-app/dist/assets/{dollar-sign-khUEgmXm.js → dollar-sign-DSObKmVv.js} +1 -1
- package/web-app/dist/assets/{file-code-corner-_TlPqN5g.js → file-code-corner-CVEW-gF5.js} +1 -1
- package/web-app/dist/assets/{file-plus-B5UT0SZU.js → file-plus-CAK0I0tE.js} +1 -1
- package/web-app/dist/assets/{globe-Chhrgg2K.js → globe-DeUgcsSZ.js} +1 -1
- package/web-app/dist/assets/{hammer-DJ6Z4tiA.js → hammer-B74PyjS0.js} +1 -1
- package/web-app/dist/assets/{index-BprNVRFx.js → index-DW3_HRqp.js} +3 -3
- package/web-app/dist/assets/{layers-CtYWuenz.js → layers-Dw7kaGyJ.js} +1 -1
- package/web-app/dist/assets/{loader-circle-BRxnh_Va.js → loader-circle-B6xzlAjw.js} +1 -1
- package/web-app/dist/assets/{lock-btaZm2bN.js → lock-5ncCPzIG.js} +1 -1
- package/web-app/dist/assets/{package-BFU4d6gL.js → package-x59_dTx0.js} +1 -1
- package/web-app/dist/assets/{plus-BogXrZQi.js → plus-BcVBzjpS.js} +1 -1
- package/web-app/dist/assets/{refresh-cw-CsdQkR5n.js → refresh-cw-CNrJhks6.js} +1 -1
- package/web-app/dist/assets/{rotate-ccw-BeLxChlE.js → rotate-ccw-BpmS_ezI.js} +1 -1
- package/web-app/dist/assets/{scroll-text-DAXSJc0i.js → scroll-text-CwBYNyoj.js} +1 -1
- package/web-app/dist/assets/{server-Cf_yJUqU.js → server-C4hnxkk6.js} +1 -1
- package/web-app/dist/assets/{shield-alert-B2gh1hM3.js → shield-alert-CSVUyWCM.js} +1 -1
- package/web-app/dist/assets/{thumbs-up-DxYjJ1v8.js → thumbs-up-DEU9fmDX.js} +1 -1
- package/web-app/dist/assets/{trash-2-BaAsSOYk.js → trash-2-DJKMevAm.js} +1 -1
- package/web-app/dist/assets/{trending-down-Kc-Tady7.js → trending-down-Dq3wAM59.js} +1 -1
- package/web-app/dist/assets/{trending-up-BDsns895.js → trending-up-DribptN4.js} +1 -1
- package/web-app/dist/assets/{upload-CCJzLwO9.js → upload-DUlZxsH1.js} +1 -1
- package/web-app/dist/assets/{usePolling-4aqc_A7F.js → usePolling-PecqVZ0k.js} +1 -1
- package/web-app/dist/assets/{user-BzoHXLDe.js → user-BcLYLDVU.js} +1 -1
- package/web-app/dist/index.html +1 -1
package/autonomy/loki
CHANGED
|
@@ -1318,7 +1318,7 @@ loki_remote_verify_receipt() {
|
|
|
1318
1318
|
_attest="$(loki_remote_attestation_status "$pj" "$_attest_url")"
|
|
1319
1319
|
local _vroot
|
|
1320
1320
|
_vroot="$(loki_verify_root)" || { echo " NOT VERIFIED: no project tree to check against" >&2; return 0; }
|
|
1321
|
-
out="$(python3 -
|
|
1321
|
+
out="$(python3 -I -S "$verifier" "$pj" "$_vroot" 2>/dev/null)"
|
|
1322
1322
|
hash_ok="$(printf '%s' "$out" | jq -r '.hash_ok // false' 2>/dev/null)"
|
|
1323
1323
|
gpg_ok="$(printf '%s' "$out" | jq -r '.gpg_ok | tostring' 2>/dev/null)"
|
|
1324
1324
|
|
|
@@ -5832,6 +5832,112 @@ _loki_next_action() {
|
|
|
5832
5832
|
esac
|
|
5833
5833
|
}
|
|
5834
5834
|
|
|
5835
|
+
# loki next --suggest: with no active run, propose up to 3 small tasks drawn
|
|
5836
|
+
# from open GitHub issues (only when gh is authenticated), TODO/FIXME lines and
|
|
5837
|
+
# the last recorded failing test run. est_usd comes from the one estimator
|
|
5838
|
+
# ('loki estimate --json', cost_preview.ts) and is null / NOT AVAILABLE when it
|
|
5839
|
+
# has no usable history (Engine Law L0: never a number from task text).
|
|
5840
|
+
# Sources are interleaved so one noisy source cannot fill all 3.
|
|
5841
|
+
_loki_next_suggest() {
|
|
5842
|
+
local as_json=false
|
|
5843
|
+
case "${1:-}" in
|
|
5844
|
+
--json) as_json=true ;;
|
|
5845
|
+
"") : ;;
|
|
5846
|
+
*) echo "Usage: loki next --suggest [--json]" >&2; return 1 ;;
|
|
5847
|
+
esac
|
|
5848
|
+
local status
|
|
5849
|
+
status="$(_loki_resolve_run_status)"
|
|
5850
|
+
if [ "$status" = "running" ]; then
|
|
5851
|
+
echo "loki next --suggest: a run is active here; use 'loki next' instead." >&2
|
|
5852
|
+
return 1
|
|
5853
|
+
fi
|
|
5854
|
+
local tmp note=""
|
|
5855
|
+
tmp="$(mktemp -d "${TMPDIR:-/tmp}/loki-run.XXXXXXXX")" || return 1
|
|
5856
|
+
: > "$tmp/issues"; : > "$tmp/todos"; : > "$tmp/tests"
|
|
5857
|
+
# gh runs under a python timeout so a hung network call cannot hang the command.
|
|
5858
|
+
python3 - > "$tmp/issues" 2>/dev/null <<'PYEOF' || note="issues skipped: gh not installed, not authenticated, or timed out"
|
|
5859
|
+
import json, subprocess, sys
|
|
5860
|
+
try:
|
|
5861
|
+
if subprocess.run(["gh", "auth", "status"], capture_output=True, timeout=10).returncode != 0:
|
|
5862
|
+
sys.exit(1)
|
|
5863
|
+
r = subprocess.run(["gh", "issue", "list", "--state", "open", "--limit", "10", "--json", "number,title"],
|
|
5864
|
+
capture_output=True, text=True, timeout=15)
|
|
5865
|
+
if r.returncode != 0:
|
|
5866
|
+
sys.exit(1)
|
|
5867
|
+
for i in json.loads(r.stdout):
|
|
5868
|
+
print("#%s %s" % (i["number"], str(i["title"]).replace("\t", " ")))
|
|
5869
|
+
except Exception:
|
|
5870
|
+
sys.exit(1)
|
|
5871
|
+
PYEOF
|
|
5872
|
+
local est_json="" est_pid="" est_ticks=0 est_max est_kid
|
|
5873
|
+
est_max=$(( ${LOKI_NEXT_ESTIMATE_TIMEOUT:-20} * 4 ))
|
|
5874
|
+
# The estimate runs in the background and is polled, so a slow or hung
|
|
5875
|
+
# estimator cannot hang the command (no timeout binary is assumed: macOS
|
|
5876
|
+
# bash 3.2 may lack one). Only the PID started here, and its direct
|
|
5877
|
+
# children, are ever signalled.
|
|
5878
|
+
( cmd_estimate --json > "$tmp/est" 2>/dev/null ) &
|
|
5879
|
+
est_pid=$!
|
|
5880
|
+
while kill -0 "$est_pid" 2>/dev/null; do
|
|
5881
|
+
if [ "$est_ticks" -ge "$est_max" ]; then
|
|
5882
|
+
for est_kid in $(pgrep -P "$est_pid" 2>/dev/null); do kill "$est_kid" 2>/dev/null || true; done
|
|
5883
|
+
kill "$est_pid" 2>/dev/null || true
|
|
5884
|
+
note="${note:+$note; }estimate unavailable: timed out after ${LOKI_NEXT_ESTIMATE_TIMEOUT:-20}s"
|
|
5885
|
+
: > "$tmp/est"
|
|
5886
|
+
break
|
|
5887
|
+
fi
|
|
5888
|
+
sleep 0.25
|
|
5889
|
+
est_ticks=$((est_ticks + 1))
|
|
5890
|
+
done
|
|
5891
|
+
wait "$est_pid" 2>/dev/null || true
|
|
5892
|
+
est_json="$(cat "$tmp/est" 2>/dev/null)" || est_json=""
|
|
5893
|
+
{ git grep -nIE 'TODO|FIXME' -- . ':!.loki' 2>/dev/null \
|
|
5894
|
+
|| grep -rnIE 'TODO|FIXME' --exclude-dir=.git --exclude-dir=.loki --exclude-dir=node_modules . 2>/dev/null; } \
|
|
5895
|
+
| head -20 > "$tmp/todos" || true
|
|
5896
|
+
python3 - "${LOKI_DIR:-.loki}/quality/test-results.json" > "$tmp/tests" 2>/dev/null <<'PYEOF' || true
|
|
5897
|
+
import json, sys
|
|
5898
|
+
try:
|
|
5899
|
+
d = json.load(open(sys.argv[1]))
|
|
5900
|
+
if d.get("pass") in (False, "false", "False"):
|
|
5901
|
+
print("failing tests (%s): %s" % (d.get("runner", "unknown"), str(d.get("summary", ""))[:120].replace("\n", " ")))
|
|
5902
|
+
except Exception:
|
|
5903
|
+
pass
|
|
5904
|
+
PYEOF
|
|
5905
|
+
SUGGEST_JSON=$as_json SUGGEST_NOTE="$note" SUGGEST_EST="$est_json" python3 - "$tmp" <<'PYEOF'
|
|
5906
|
+
import json, os, sys
|
|
5907
|
+
d = sys.argv[1]
|
|
5908
|
+
def rd(n):
|
|
5909
|
+
return [l.rstrip("\n") for l in open(os.path.join(d, n)) if l.strip()]
|
|
5910
|
+
est, na = None, "estimator returned nothing"
|
|
5911
|
+
try:
|
|
5912
|
+
r = json.loads(os.environ.get("SUGGEST_EST", ""))
|
|
5913
|
+
if r.get("ok") is True and isinstance(r.get("est", {}).get("usd"), list):
|
|
5914
|
+
est = r["est"]["usd"]
|
|
5915
|
+
else:
|
|
5916
|
+
na = str(r.get("reason", "no usable run history"))
|
|
5917
|
+
except Exception:
|
|
5918
|
+
pass
|
|
5919
|
+
src = [("failing_test", "small", rd("tests")), ("issue", "medium", rd("issues")), ("todo", "small", rd("todos"))]
|
|
5920
|
+
out = []
|
|
5921
|
+
while len(out) < 3 and any(x[2] for x in src):
|
|
5922
|
+
for name, size, items in src:
|
|
5923
|
+
if items and len(out) < 3:
|
|
5924
|
+
out.append({"source": name, "task": items.pop(0), "size": size, "est_usd": est})
|
|
5925
|
+
note = os.environ.get("SUGGEST_NOTE", "")
|
|
5926
|
+
if os.environ.get("SUGGEST_JSON") == "true":
|
|
5927
|
+
print(json.dumps({"suggestions": out, "notes": [note] if note else [], "est_usd_reason": None if est else na}))
|
|
5928
|
+
else:
|
|
5929
|
+
if note:
|
|
5930
|
+
print("note: " + note)
|
|
5931
|
+
if not out:
|
|
5932
|
+
print("No suggestions found (no open issues, TODO/FIXME lines or failing tests).")
|
|
5933
|
+
for i, o in enumerate(out, 1):
|
|
5934
|
+
e = ("est_usd %.2f-%.2f" % tuple(o["est_usd"])) if o["est_usd"] else "est_usd NOT AVAILABLE (%s)" % na
|
|
5935
|
+
print("%d. [%s] %s (%s)" % (i, o["source"], o["task"], e))
|
|
5936
|
+
PYEOF
|
|
5937
|
+
rm -rf "$tmp"
|
|
5938
|
+
return 0
|
|
5939
|
+
}
|
|
5940
|
+
|
|
5835
5941
|
# loki next -- one command that runs the right next step for you.
|
|
5836
5942
|
# Reads the SAME already-captured state as cmd_why (no new state, no spend),
|
|
5837
5943
|
# resolves the terminal status, and proposes the single mapped next step, then
|
|
@@ -5846,7 +5952,7 @@ cmd_next() {
|
|
|
5846
5952
|
--help|-h|help)
|
|
5847
5953
|
echo -e "${BOLD}Loki Mode -- run the right next step for you${NC}"
|
|
5848
5954
|
echo ""
|
|
5849
|
-
echo "Usage: loki next [--yes] [--dry-run]"
|
|
5955
|
+
echo "Usage: loki next [--yes] [--dry-run] [--suggest [--json]]"
|
|
5850
5956
|
echo ""
|
|
5851
5957
|
echo "Reads the last build's recorded outcome (the same state 'loki why'"
|
|
5852
5958
|
echo "reads -- no new run, no cost) and carries you forward by running the"
|
|
@@ -5860,13 +5966,15 @@ cmd_next() {
|
|
|
5860
5966
|
echo "Options:"
|
|
5861
5967
|
echo " --yes Run the proposed step without asking"
|
|
5862
5968
|
echo " --dry-run Print the proposed step only; do not run it"
|
|
5969
|
+
echo " --suggest No active run: propose up to 3 small tasks (issues, TODO/FIXME, failing tests)"
|
|
5863
5970
|
echo " --help, -h Show this help and exit"
|
|
5864
5971
|
return 0
|
|
5865
5972
|
;;
|
|
5973
|
+
--suggest) shift; _loki_next_suggest "$@"; return $? ;;
|
|
5866
5974
|
--yes|-y) assume_yes=true ;;
|
|
5867
5975
|
--dry-run) dry_run=true ;;
|
|
5868
5976
|
"") : ;;
|
|
5869
|
-
*) echo -e "${RED}Unknown flag: $1${NC}" >&2; echo "Usage: loki next [--yes] [--dry-run]" >&2; return 1 ;;
|
|
5977
|
+
*) echo -e "${RED}Unknown flag: $1${NC}" >&2; echo "Usage: loki next [--yes] [--dry-run] [--suggest [--json]]" >&2; return 1 ;;
|
|
5870
5978
|
esac
|
|
5871
5979
|
# Allow a second flag (e.g. --dry-run --yes order-agnostic).
|
|
5872
5980
|
case "${2:-}" in
|
|
@@ -10001,7 +10109,7 @@ _deploy_receipt_verdict() {
|
|
|
10001
10109
|
|
|
10002
10110
|
local out hash_ok _vroot
|
|
10003
10111
|
_vroot="$(loki_verify_root 2>/dev/null)" || { printf '%s' "UNCHECKED"; return 0; }
|
|
10004
|
-
out="$(python3 -
|
|
10112
|
+
out="$(python3 -I -S "$verifier" "$pj" "$_vroot" 2>/dev/null || true)"
|
|
10005
10113
|
|
|
10006
10114
|
# "The verifier did not RUN" and "the verifier says the hash is bad" are
|
|
10007
10115
|
# different facts and must not collapse. If they do, a broken python3
|
|
@@ -11581,6 +11689,80 @@ cmd_import() {
|
|
|
11581
11689
|
}
|
|
11582
11690
|
|
|
11583
11691
|
# GitHub integration management (v5.41.0)
|
|
11692
|
+
# _gh_protect_py: JSON/URL helper for `loki github protect`.
|
|
11693
|
+
# enc BRANCH | put GETJSON ADD | has GETJSON | same GETJSON WRITTEN
|
|
11694
|
+
# mkstate SLUG BRANCH MODE PRIOR WRITTEN | sget STATEJSON KEY
|
|
11695
|
+
# `put` turns a GET branch-protection body into the PUT body, carrying every
|
|
11696
|
+
# setting (reviews, admins, restrictions, linear history, check app_ids)
|
|
11697
|
+
# unchanged; ADD=1 appends the loki/receipt check.
|
|
11698
|
+
_gh_protect_py() {
|
|
11699
|
+
python3 -c '
|
|
11700
|
+
import json, sys, urllib.parse
|
|
11701
|
+
op = sys.argv[1]
|
|
11702
|
+
def names(lst, key):
|
|
11703
|
+
return [x if isinstance(x, str) else x.get(key) for x in (lst or [])]
|
|
11704
|
+
def actors(d):
|
|
11705
|
+
if d is None:
|
|
11706
|
+
return None
|
|
11707
|
+
return {"users": names(d.get("users"), "login"), "teams": names(d.get("teams"), "slug"), "apps": names(d.get("apps"), "slug")}
|
|
11708
|
+
def put(p, add):
|
|
11709
|
+
rsc = p.get("required_status_checks")
|
|
11710
|
+
rs = None
|
|
11711
|
+
if rsc is not None or add:
|
|
11712
|
+
rsc = rsc or {}
|
|
11713
|
+
checks = rsc.get("checks")
|
|
11714
|
+
if checks is None:
|
|
11715
|
+
checks = [{"context": c} for c in rsc.get("contexts", [])]
|
|
11716
|
+
checks = [{k: v for k, v in c.items() if not (k == "app_id" and v is None)} for c in checks]
|
|
11717
|
+
if add and not any(c.get("context") == "loki/receipt" for c in checks):
|
|
11718
|
+
checks.append({"context": "loki/receipt"})
|
|
11719
|
+
rs = {"strict": bool(rsc.get("strict")), "checks": checks}
|
|
11720
|
+
rv = p.get("required_pull_request_reviews")
|
|
11721
|
+
if rv is not None:
|
|
11722
|
+
r = {"dismiss_stale_reviews": bool(rv.get("dismiss_stale_reviews")),
|
|
11723
|
+
"require_code_owner_reviews": bool(rv.get("require_code_owner_reviews")),
|
|
11724
|
+
"required_approving_review_count": rv.get("required_approving_review_count", 0),
|
|
11725
|
+
"require_last_push_approval": bool(rv.get("require_last_push_approval"))}
|
|
11726
|
+
for k in ("dismissal_restrictions", "bypass_pull_request_allowances"):
|
|
11727
|
+
if rv.get(k) is not None:
|
|
11728
|
+
r[k] = actors(rv[k])
|
|
11729
|
+
rv = r
|
|
11730
|
+
def en(k):
|
|
11731
|
+
return bool((p.get(k) or {}).get("enabled"))
|
|
11732
|
+
body = {"required_status_checks": rs, "enforce_admins": en("enforce_admins"),
|
|
11733
|
+
"required_pull_request_reviews": rv, "restrictions": actors(p.get("restrictions"))}
|
|
11734
|
+
for k in ("required_linear_history", "allow_force_pushes", "allow_deletions", "block_creations",
|
|
11735
|
+
"required_conversation_resolution", "lock_branch", "allow_fork_syncing"):
|
|
11736
|
+
body[k] = en(k)
|
|
11737
|
+
return body
|
|
11738
|
+
def norm(b):
|
|
11739
|
+
b = json.loads(json.dumps(b))
|
|
11740
|
+
if b.get("required_status_checks"):
|
|
11741
|
+
for c in b["required_status_checks"]["checks"]:
|
|
11742
|
+
c.pop("app_id", None)
|
|
11743
|
+
return b
|
|
11744
|
+
def dump(o):
|
|
11745
|
+
return json.dumps(o, sort_keys=True, separators=(",", ":"))
|
|
11746
|
+
if op == "enc":
|
|
11747
|
+
print(urllib.parse.quote(sys.argv[2], safe="/"))
|
|
11748
|
+
elif op == "put":
|
|
11749
|
+
print(dump(put(json.loads(sys.argv[2]), sys.argv[3] == "1")))
|
|
11750
|
+
elif op == "has":
|
|
11751
|
+
p = json.loads(sys.argv[2]); r = p.get("required_status_checks") or {}
|
|
11752
|
+
have = [c.get("context") for c in r.get("checks", [])] + list(r.get("contexts", []))
|
|
11753
|
+
print("1" if "loki/receipt" in have else "0")
|
|
11754
|
+
elif op == "same":
|
|
11755
|
+
print("1" if norm(put(json.loads(sys.argv[2]), False)) == norm(json.loads(sys.argv[3])) else "0")
|
|
11756
|
+
elif op == "mkstate":
|
|
11757
|
+
print(dump({"slug": sys.argv[2], "branch": sys.argv[3], "mode": sys.argv[4],
|
|
11758
|
+
"prior": json.loads(sys.argv[5]) if sys.argv[5] else None,
|
|
11759
|
+
"written": json.loads(sys.argv[6])}))
|
|
11760
|
+
elif op == "sget":
|
|
11761
|
+
v = json.loads(sys.argv[2])[sys.argv[3]]
|
|
11762
|
+
print(v if isinstance(v, str) else dump(v))
|
|
11763
|
+
' "$@"
|
|
11764
|
+
}
|
|
11765
|
+
|
|
11584
11766
|
cmd_github() {
|
|
11585
11767
|
local subcmd="${1:-help}"
|
|
11586
11768
|
shift 2>/dev/null || true
|
|
@@ -11819,6 +12001,128 @@ except: print(0)
|
|
|
11819
12001
|
fi
|
|
11820
12002
|
;;
|
|
11821
12003
|
|
|
12004
|
+
protect)
|
|
12005
|
+
# Make loki/receipt a required status check on the default branch.
|
|
12006
|
+
# The FULL prior protection is saved (with slug and branch) and the
|
|
12007
|
+
# PUT carries every other setting back unchanged, so --off restores
|
|
12008
|
+
# the exact prior protection. --dry-run performs one read only.
|
|
12009
|
+
local _pr_dry=0 _pr_off=0 _pr_usage=0
|
|
12010
|
+
while [ $# -gt 0 ]; do
|
|
12011
|
+
case "$1" in
|
|
12012
|
+
--dry-run) _pr_dry=1; shift ;;
|
|
12013
|
+
--off) _pr_off=1; shift ;;
|
|
12014
|
+
*) _pr_usage=1; break ;;
|
|
12015
|
+
esac
|
|
12016
|
+
done
|
|
12017
|
+
if [ "$_pr_usage" -ne 0 ]; then
|
|
12018
|
+
echo "Usage: loki github protect [--dry-run] [--off]" >&2
|
|
12019
|
+
exit 2
|
|
12020
|
+
fi
|
|
12021
|
+
if ! command -v gh &>/dev/null; then
|
|
12022
|
+
echo -e "${RED}Error: gh CLI not found. Install with: brew install gh${NC}" >&2
|
|
12023
|
+
exit 1
|
|
12024
|
+
fi
|
|
12025
|
+
if ! gh auth status &>/dev/null; then
|
|
12026
|
+
echo -e "${RED}Error: gh CLI not authenticated. Run: gh auth login${NC}" >&2
|
|
12027
|
+
exit 1
|
|
12028
|
+
fi
|
|
12029
|
+
local _pr_url _pr_slug _pr_branch _pr_enc
|
|
12030
|
+
_pr_url="$(git remote get-url origin 2>/dev/null || true)"
|
|
12031
|
+
_pr_slug="$(printf '%s' "$_pr_url" | sed -E 's|^.*github\.com[:/]||; s|\.git$||')"
|
|
12032
|
+
if [ -z "$_pr_slug" ] || [ "$_pr_slug" = "$_pr_url" ]; then
|
|
12033
|
+
echo -e "${RED}Error: no GitHub origin remote to protect.${NC}" >&2
|
|
12034
|
+
exit 1
|
|
12035
|
+
fi
|
|
12036
|
+
_pr_branch="$(git symbolic-ref --short refs/remotes/origin/HEAD 2>/dev/null | sed 's|^origin/||' || true)"
|
|
12037
|
+
[ -n "$_pr_branch" ] || _pr_branch="$(git branch --show-current 2>/dev/null)"
|
|
12038
|
+
if [ -z "$_pr_branch" ]; then
|
|
12039
|
+
echo -e "${RED}Error: could not determine the default branch.${NC}" >&2
|
|
12040
|
+
exit 1
|
|
12041
|
+
fi
|
|
12042
|
+
_pr_enc="$(_gh_protect_py enc "$_pr_branch")" || exit 1
|
|
12043
|
+
local _pr_api="repos/${_pr_slug}/branches/${_pr_enc}/protection"
|
|
12044
|
+
local _pr_state="${LOKI_DIR}/state/github-protect.json"
|
|
12045
|
+
|
|
12046
|
+
# Read the full current protection: mode is "none" (unprotected)
|
|
12047
|
+
# or "protected" (cur holds the JSON).
|
|
12048
|
+
local _pr_out _pr_rc=0 _pr_mode="protected" _pr_cur=""
|
|
12049
|
+
_pr_out="$(gh api "$_pr_api" 2>&1)" || _pr_rc=$?
|
|
12050
|
+
if [ "$_pr_rc" -eq 0 ]; then
|
|
12051
|
+
_pr_cur="$_pr_out"
|
|
12052
|
+
elif printf '%s' "$_pr_out" | grep -qi "Branch not protected"; then
|
|
12053
|
+
_pr_mode="none"
|
|
12054
|
+
else
|
|
12055
|
+
echo "$_pr_out" >&2
|
|
12056
|
+
echo -e "${RED}Error: could not read branch protection for ${_pr_slug}@${_pr_branch}.${NC}" >&2
|
|
12057
|
+
exit 1
|
|
12058
|
+
fi
|
|
12059
|
+
|
|
12060
|
+
if [ "$_pr_off" -eq 1 ]; then
|
|
12061
|
+
if [ ! -f "$_pr_state" ]; then
|
|
12062
|
+
echo -e "${RED}Error: nothing to restore (no saved state at ${_pr_state}).${NC}" >&2
|
|
12063
|
+
exit 1
|
|
12064
|
+
fi
|
|
12065
|
+
local _pr_st _pr_ssl _pr_sbr _pr_smode _pr_prior _pr_written _pr_body
|
|
12066
|
+
_pr_st="$(cat "$_pr_state")"
|
|
12067
|
+
_pr_ssl="$(_gh_protect_py sget "$_pr_st" slug)" || { echo -e "${RED}Error: unreadable saved state ${_pr_state}.${NC}" >&2; exit 1; }
|
|
12068
|
+
_pr_sbr="$(_gh_protect_py sget "$_pr_st" branch)" || exit 1
|
|
12069
|
+
_pr_smode="$(_gh_protect_py sget "$_pr_st" mode)" || exit 1
|
|
12070
|
+
if [ "$_pr_ssl" != "$_pr_slug" ] || [ "$_pr_sbr" != "$_pr_branch" ]; then
|
|
12071
|
+
echo -e "${RED}Refused: saved state is for ${_pr_ssl}@${_pr_sbr}, but this checkout is ${_pr_slug}@${_pr_branch}. Nothing changed.${NC}" >&2
|
|
12072
|
+
exit 1
|
|
12073
|
+
fi
|
|
12074
|
+
if [ "$_pr_mode" = "none" ]; then
|
|
12075
|
+
echo -e "${RED}Refused: ${_pr_slug}@${_pr_branch} has no protection now, so loki/receipt is gone. Nothing changed.${NC}" >&2
|
|
12076
|
+
exit 1
|
|
12077
|
+
fi
|
|
12078
|
+
_pr_written="$(_gh_protect_py sget "$_pr_st" written)" || exit 1
|
|
12079
|
+
if [ "$(_gh_protect_py has "$_pr_cur")" != "1" ]; then
|
|
12080
|
+
echo -e "${RED}Refused: loki/receipt is no longer a required check on ${_pr_slug}@${_pr_branch}; protection was changed since protect. Nothing changed.${NC}" >&2
|
|
12081
|
+
exit 1
|
|
12082
|
+
fi
|
|
12083
|
+
if [ "$(_gh_protect_py same "$_pr_cur" "$_pr_written")" != "1" ]; then
|
|
12084
|
+
echo -e "${RED}Refused: protection on ${_pr_slug}@${_pr_branch} differs from what protect wrote; restoring would overwrite later changes. Nothing changed.${NC}" >&2
|
|
12085
|
+
exit 1
|
|
12086
|
+
fi
|
|
12087
|
+
if [ "$_pr_smode" = "none" ]; then
|
|
12088
|
+
gh api -X DELETE "$_pr_api" >/dev/null || { echo -e "${RED}Error: could not remove branch protection.${NC}" >&2; exit 1; }
|
|
12089
|
+
else
|
|
12090
|
+
_pr_prior="$(_gh_protect_py sget "$_pr_st" prior)" || exit 1
|
|
12091
|
+
_pr_body="$(_gh_protect_py put "$_pr_prior" 0)" || exit 1
|
|
12092
|
+
printf '%s' "$_pr_body" | gh api -X PUT "$_pr_api" --input - >/dev/null \
|
|
12093
|
+
|| { echo -e "${RED}Error: could not restore branch protection.${NC}" >&2; exit 1; }
|
|
12094
|
+
fi
|
|
12095
|
+
rm -f "$_pr_state"
|
|
12096
|
+
echo "Restored branch protection on ${_pr_slug}@${_pr_branch} (was: ${_pr_smode})"
|
|
12097
|
+
exit 0
|
|
12098
|
+
fi
|
|
12099
|
+
|
|
12100
|
+
local _pr_has=0
|
|
12101
|
+
if [ "$_pr_mode" = "protected" ] && [ "$(_gh_protect_py has "$_pr_cur")" = "1" ]; then
|
|
12102
|
+
_pr_has=1
|
|
12103
|
+
fi
|
|
12104
|
+
if [ "$_pr_dry" -eq 1 ]; then
|
|
12105
|
+
echo "Dry run for ${_pr_slug}@${_pr_branch}: protection is '${_pr_mode}'; would $([ "$_pr_has" -eq 1 ] && echo 'change nothing (loki/receipt already required)' || echo 'require loki/receipt'). Nothing changed."
|
|
12106
|
+
exit 0
|
|
12107
|
+
fi
|
|
12108
|
+
if [ "$_pr_has" -eq 1 ]; then
|
|
12109
|
+
echo "loki/receipt is already a required check on ${_pr_slug}@${_pr_branch}; nothing to do."
|
|
12110
|
+
exit 0
|
|
12111
|
+
fi
|
|
12112
|
+
local _pr_new _pr_prior_in="" _pr_src="{}" _pr_stjson
|
|
12113
|
+
if [ "$_pr_mode" = "protected" ]; then _pr_prior_in="$_pr_cur"; _pr_src="$_pr_cur"; fi
|
|
12114
|
+
_pr_new="$(_gh_protect_py put "$_pr_src" 1)" || exit 1
|
|
12115
|
+
_pr_stjson="$(_gh_protect_py mkstate "$_pr_slug" "$_pr_branch" "$_pr_mode" "$_pr_prior_in" "$_pr_new")" || exit 1
|
|
12116
|
+
mkdir -p "${LOKI_DIR}/state"
|
|
12117
|
+
printf '%s\n' "$_pr_stjson" > "$_pr_state"
|
|
12118
|
+
if ! printf '%s' "$_pr_new" | gh api -X PUT "$_pr_api" --input - >/dev/null; then
|
|
12119
|
+
rm -f "$_pr_state"
|
|
12120
|
+
echo -e "${RED}Error: could not update branch protection; nothing changed.${NC}" >&2
|
|
12121
|
+
exit 1
|
|
12122
|
+
fi
|
|
12123
|
+
echo "loki/receipt is now a required check on ${_pr_slug}@${_pr_branch} (was: ${_pr_mode}). Undo with: loki github protect --off"
|
|
12124
|
+
;;
|
|
12125
|
+
|
|
11822
12126
|
help|*)
|
|
11823
12127
|
echo -e "${BOLD}loki github${NC} - GitHub integration management"
|
|
11824
12128
|
echo ""
|
|
@@ -11828,6 +12132,7 @@ except: print(0)
|
|
|
11828
12132
|
echo " export Export local tasks as new GitHub issues"
|
|
11829
12133
|
echo " pr [name] Create pull request from completed work"
|
|
11830
12134
|
echo " publish [name] [--public] Push to GitHub and set a loki/receipt commit status"
|
|
12135
|
+
echo " protect [--dry-run] [--off] Require loki/receipt on the default branch (--off restores)"
|
|
11831
12136
|
echo ""
|
|
11832
12137
|
echo "Environment Variables:"
|
|
11833
12138
|
echo " LOKI_GITHUB_IMPORT=true Import open issues as tasks on start"
|
|
@@ -16279,6 +16584,9 @@ cmd_notify() {
|
|
|
16279
16584
|
webhook)
|
|
16280
16585
|
cmd_notify_send "webhook" "$@"
|
|
16281
16586
|
;;
|
|
16587
|
+
desktop)
|
|
16588
|
+
cmd_notify_send "desktop" "$@"
|
|
16589
|
+
;;
|
|
16282
16590
|
status)
|
|
16283
16591
|
cmd_notify_status
|
|
16284
16592
|
;;
|
|
@@ -16308,6 +16616,8 @@ cmd_notify_help() {
|
|
|
16308
16616
|
echo " slack <message> Send notification to Slack only"
|
|
16309
16617
|
echo " discord <message> Send notification to Discord only"
|
|
16310
16618
|
echo " webhook <message> Send notification to webhook only"
|
|
16619
|
+
echo " desktop <message> Send a desktop notification (macOS or Linux notify-send)"
|
|
16620
|
+
echo " test --channel desktop [message] Test only the desktop channel"
|
|
16311
16621
|
echo " status Show which notification channels are configured"
|
|
16312
16622
|
echo " help Show this help"
|
|
16313
16623
|
echo ""
|
|
@@ -16315,7 +16625,7 @@ cmd_notify_help() {
|
|
|
16315
16625
|
echo " LOKI_SLACK_WEBHOOK Slack incoming webhook URL"
|
|
16316
16626
|
echo " LOKI_DISCORD_WEBHOOK Discord webhook URL"
|
|
16317
16627
|
echo " LOKI_WEBHOOK_URL Custom webhook URL (POST JSON)"
|
|
16318
|
-
echo " LOKI_NOTIFY_CHANNELS Channels to use (comma-separated: slack,discord,webhook)"
|
|
16628
|
+
echo " LOKI_NOTIFY_CHANNELS Channels to use (comma-separated: slack,discord,webhook,desktop)"
|
|
16319
16629
|
echo " Default: all configured channels"
|
|
16320
16630
|
echo ""
|
|
16321
16631
|
echo "Examples:"
|
|
@@ -16381,9 +16691,35 @@ cmd_notify_status() {
|
|
|
16381
16691
|
fi
|
|
16382
16692
|
}
|
|
16383
16693
|
|
|
16694
|
+
# Desktop channel: reuse run.sh send_notification (the same function the run
|
|
16695
|
+
# loop fires on blocked, done and failed) by extracting it, so the macOS and
|
|
16696
|
+
# Linux logic lives in one place. Returns 1 when no desktop notifier exists.
|
|
16697
|
+
send_desktop_notification() {
|
|
16698
|
+
local message="$1" title="${2:-Notification}"
|
|
16699
|
+
command -v osascript >/dev/null 2>&1 || command -v notify-send >/dev/null 2>&1 || return 1
|
|
16700
|
+
[ -f "$RUN_SH" ] || return 1
|
|
16701
|
+
local fn
|
|
16702
|
+
fn="$(sed -n '/^send_notification() {/,/^}/p' "$RUN_SH")"
|
|
16703
|
+
[ -n "$fn" ] || return 1
|
|
16704
|
+
(
|
|
16705
|
+
# shellcheck disable=SC2034
|
|
16706
|
+
NOTIFICATIONS_ENABLED=true
|
|
16707
|
+
# shellcheck disable=SC2034
|
|
16708
|
+
NOTIFICATION_SOUND=false
|
|
16709
|
+
eval "$fn"
|
|
16710
|
+
send_notification "$title" "$message" "normal"
|
|
16711
|
+
)
|
|
16712
|
+
}
|
|
16713
|
+
|
|
16384
16714
|
cmd_notify_test() {
|
|
16715
|
+
local channel_arg=""
|
|
16716
|
+
if [ "${1:-}" = "--channel" ]; then
|
|
16717
|
+
channel_arg="${2:-}"
|
|
16718
|
+
shift 2 2>/dev/null || shift $#
|
|
16719
|
+
fi
|
|
16385
16720
|
local message="${1:-Loki Mode test notification}"
|
|
16386
16721
|
local channels_notified=0
|
|
16722
|
+
[ -n "$channel_arg" ] && LOKI_NOTIFY_CHANNELS="$channel_arg"
|
|
16387
16723
|
|
|
16388
16724
|
echo -e "${BOLD}Testing Notification Channels${NC}"
|
|
16389
16725
|
echo ""
|
|
@@ -16430,6 +16766,17 @@ cmd_notify_test() {
|
|
|
16430
16766
|
fi
|
|
16431
16767
|
fi
|
|
16432
16768
|
|
|
16769
|
+
# Desktop is opt-in (explicit channel) so default runs never pop a banner
|
|
16770
|
+
if [[ "${channels}" == *"desktop"* ]]; then
|
|
16771
|
+
echo -n " Desktop... "
|
|
16772
|
+
if send_desktop_notification "$message" "Test"; then
|
|
16773
|
+
echo -e "${GREEN}OK${NC}"
|
|
16774
|
+
channels_notified=$((channels_notified + 1))
|
|
16775
|
+
else
|
|
16776
|
+
echo -e "${RED}FAILED (no osascript or notify-send)${NC}"
|
|
16777
|
+
fi
|
|
16778
|
+
fi
|
|
16779
|
+
|
|
16433
16780
|
echo ""
|
|
16434
16781
|
|
|
16435
16782
|
if [ $channels_notified -eq 0 ]; then
|
|
@@ -16489,6 +16836,14 @@ cmd_notify_send() {
|
|
|
16489
16836
|
exit 1
|
|
16490
16837
|
fi
|
|
16491
16838
|
;;
|
|
16839
|
+
desktop)
|
|
16840
|
+
if send_desktop_notification "$message" "Manual"; then
|
|
16841
|
+
echo -e "${GREEN}Sent desktop notification${NC}"
|
|
16842
|
+
else
|
|
16843
|
+
echo -e "${RED}No desktop notifier (osascript or notify-send) available${NC}"
|
|
16844
|
+
exit 1
|
|
16845
|
+
fi
|
|
16846
|
+
;;
|
|
16492
16847
|
esac
|
|
16493
16848
|
}
|
|
16494
16849
|
|
|
@@ -30153,6 +30508,7 @@ cmd_cost() {
|
|
|
30153
30508
|
local show_json=false
|
|
30154
30509
|
local last_n=0
|
|
30155
30510
|
local detail=false
|
|
30511
|
+
local month="" csv=false
|
|
30156
30512
|
|
|
30157
30513
|
while [[ $# -gt 0 ]]; do
|
|
30158
30514
|
case "$1" in
|
|
@@ -30168,6 +30524,9 @@ cmd_cost() {
|
|
|
30168
30524
|
echo "Options:"
|
|
30169
30525
|
echo " --json Machine-readable JSON output"
|
|
30170
30526
|
echo " --last N Show only the last N runs in history (default: all)"
|
|
30527
|
+
echo " --month YYYY-MM Only runs generated in that month"
|
|
30528
|
+
echo " --csv One CSV row per run: repo,user,usd,verdict,run_id,date"
|
|
30529
|
+
echo " (unmeasured cost prints \"unmeasured\", never 0)"
|
|
30171
30530
|
echo " --detail Per-iteration breakdown: cache hit ratio, cost trend,"
|
|
30172
30531
|
echo " and the measured/found ratio (autonomy/lib/cost-summary.py)"
|
|
30173
30532
|
echo " --help, -h Show this help"
|
|
@@ -30188,12 +30547,24 @@ cmd_cost() {
|
|
|
30188
30547
|
;;
|
|
30189
30548
|
--json) show_json=true; shift ;;
|
|
30190
30549
|
--detail) detail=true; shift ;;
|
|
30550
|
+
--csv) csv=true; shift ;;
|
|
30551
|
+
--month) month="${2:-}"; shift 2 || shift ;;
|
|
30552
|
+
--month=*) month="${1#*=}"; shift ;;
|
|
30191
30553
|
--last) last_n="${2:-0}"; shift 2 ;;
|
|
30192
30554
|
--last=*) last_n="${1#*=}"; shift ;;
|
|
30193
30555
|
*) echo -e "${RED}Unknown option: $1${NC}" >&2; echo "Run 'loki cost --help' for usage." >&2; exit 1 ;;
|
|
30194
30556
|
esac
|
|
30195
30557
|
done
|
|
30196
30558
|
|
|
30559
|
+
if [ -n "$month" ] && ! [[ "$month" =~ ^[0-9]{4}-(0[1-9]|1[0-2])$ ]]; then
|
|
30560
|
+
echo -e "${RED}--month must be YYYY-MM (got: ${month})${NC}" >&2
|
|
30561
|
+
return 1
|
|
30562
|
+
fi
|
|
30563
|
+
if { [ -n "$month" ] || [ "$csv" = "true" ]; } && [ "$detail" = "true" ]; then
|
|
30564
|
+
echo -e "${RED}--month and --csv are not supported with --detail${NC}" >&2
|
|
30565
|
+
return 1
|
|
30566
|
+
fi
|
|
30567
|
+
|
|
30197
30568
|
# --detail routes to the per-run summarizer, which reports what the budget
|
|
30198
30569
|
# view cannot: cache hit ratio, cost trend, and the measured/found ratio.
|
|
30199
30570
|
# It reuses the SAME honesty predicate (efficiency_cost.record_is_measured),
|
|
@@ -30240,6 +30611,8 @@ cmd_cost() {
|
|
|
30240
30611
|
LOKI_SKILL_DIR="$SKILL_DIR" \
|
|
30241
30612
|
COST_JSON="$show_json" \
|
|
30242
30613
|
COST_LAST_N="$last_n" \
|
|
30614
|
+
COST_MONTH="$month" \
|
|
30615
|
+
COST_CSV="$csv" \
|
|
30243
30616
|
COST_BUDGET_LIMIT="${LOKI_BUDGET_LIMIT:-}" \
|
|
30244
30617
|
python3 << 'COST_SCRIPT'
|
|
30245
30618
|
import json
|
|
@@ -30254,6 +30627,8 @@ try:
|
|
|
30254
30627
|
except ValueError:
|
|
30255
30628
|
last_n = 0
|
|
30256
30629
|
budget_limit_env = os.environ.get("COST_BUDGET_LIMIT", "").strip()
|
|
30630
|
+
cost_month = os.environ.get("COST_MONTH", "")
|
|
30631
|
+
cost_csv = os.environ.get("COST_CSV", "false") == "true"
|
|
30257
30632
|
|
|
30258
30633
|
# ANSI (suppressed under --json / non-tty)
|
|
30259
30634
|
use_color = (not show_json) and sys.stdout.isatty()
|
|
@@ -30361,6 +30736,9 @@ if os.path.isdir(proofs_dir):
|
|
|
30361
30736
|
"files_changed": (d.get("files_changed") or {}).get("count"),
|
|
30362
30737
|
"final_verdict": (d.get("council") or {}).get("final_verdict"),
|
|
30363
30738
|
})
|
|
30739
|
+
if cost_month:
|
|
30740
|
+
runs = [r for r in runs if (r.get("generated_at") or "").startswith(cost_month)]
|
|
30741
|
+
project_total = sum(r["cost_usd"] for r in runs if r.get("cost_usd") is not None)
|
|
30364
30742
|
runs.sort(key=lambda x: (x.get("generated_at") or ""), reverse=True)
|
|
30365
30743
|
if last_n > 0:
|
|
30366
30744
|
runs = runs[:last_n]
|
|
@@ -30435,6 +30813,19 @@ for r in runs:
|
|
|
30435
30813
|
m = r.get("model") or "unknown"
|
|
30436
30814
|
by_model[m] = by_model.get(m, 0.0) + c
|
|
30437
30815
|
|
|
30816
|
+
if cost_csv:
|
|
30817
|
+
import csv
|
|
30818
|
+
ws = os.path.basename(os.path.dirname(os.path.abspath(loki_dir))) or "unknown"
|
|
30819
|
+
who = os.environ.get("USER") or os.environ.get("LOGNAME") or "unknown"
|
|
30820
|
+
w = csv.writer(sys.stdout, lineterminator="\n")
|
|
30821
|
+
w.writerow(["repo", "user", "usd", "verdict", "run_id", "date"])
|
|
30822
|
+
for r in sorted(runs, key=lambda x: (x.get("generated_at") or "", str(x.get("run_id")))):
|
|
30823
|
+
c = r.get("cost_usd")
|
|
30824
|
+
w.writerow([ws, who, "unmeasured" if c is None else "%.6f" % c,
|
|
30825
|
+
r.get("final_verdict") or "unknown", r.get("run_id"),
|
|
30826
|
+
(r.get("generated_at") or "")[:10]])
|
|
30827
|
+
sys.exit(0)
|
|
30828
|
+
|
|
30438
30829
|
if show_json:
|
|
30439
30830
|
out = {
|
|
30440
30831
|
"current_run": {
|
|
@@ -39236,9 +39627,9 @@ print(json.dumps(json.load(open(os.environ['LOKI_PROOF_JSON'])), indent=2))
|
|
|
39236
39627
|
# route (proof.ts verifyProof) -- ok already agrees with the exit
|
|
39237
39628
|
# code there, so there is nothing to patch.
|
|
39238
39629
|
if [ "$_pv_human" -eq 1 ]; then
|
|
39239
|
-
_pv_out="$(python3 -
|
|
39630
|
+
_pv_out="$(python3 -I -S "$verifier" --human "$pj" "$_pv_root")" || _pv_rc=$?
|
|
39240
39631
|
else
|
|
39241
|
-
_pv_out="$(python3 -
|
|
39632
|
+
_pv_out="$(python3 -I -S "$verifier" "$pj" "$_pv_root")" || _pv_rc=$?
|
|
39242
39633
|
fi
|
|
39243
39634
|
_pv_base_rc="$_pv_rc"
|
|
39244
39635
|
|