loki-mode 7.14.0 → 7.16.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/trust_trajectory.py +437 -0
- package/autonomy/loki +238 -5
- package/bin/loki +2 -1
- package/dashboard/__init__.py +1 -1
- package/dashboard/server.py +112 -0
- package/dashboard/static/index.html +26 -2
- package/dashboard/static/trust.html +271 -0
- package/docs/INSTALLATION.md +1 -1
- package/docs/OPEN-CORE-BOUNDARY.md +58 -0
- package/docs/R4-TRUST-TRAJECTORY-DESIGN.md +127 -0
- package/docs/R9-OPEN-CORE-HOOKS-PLAN.md +113 -0
- package/loki-ts/dist/loki.js +263 -221
- package/mcp/__init__.py +1 -1
- package/package.json +1 -1
package/autonomy/loki
CHANGED
|
@@ -544,6 +544,7 @@ show_help() {
|
|
|
544
544
|
echo " enterprise Enterprise feature management (tokens, OIDC)"
|
|
545
545
|
echo " metrics [opts] Session productivity report (--json, --last N, --save, --share)"
|
|
546
546
|
echo " cost [opts] Transparent cost view: per-run/project spend + budget (--json, --last N)"
|
|
547
|
+
echo " trust [--json] Visible trust trajectory: council/gate pass-rate + interventions over runs [R4]"
|
|
547
548
|
echo " dogfood Show self-development statistics"
|
|
548
549
|
echo " secrets [cmd] API key status and validation (status|validate)"
|
|
549
550
|
echo " reset [target] Reset session state (all|retries|failed)"
|
|
@@ -13181,6 +13182,9 @@ main() {
|
|
|
13181
13182
|
cost)
|
|
13182
13183
|
cmd_cost "$@"
|
|
13183
13184
|
;;
|
|
13185
|
+
trust)
|
|
13186
|
+
cmd_trust "$@"
|
|
13187
|
+
;;
|
|
13184
13188
|
syslog)
|
|
13185
13189
|
cmd_syslog "$@"
|
|
13186
13190
|
;;
|
|
@@ -18152,6 +18156,59 @@ cmd_syslog() {
|
|
|
18152
18156
|
esac
|
|
18153
18157
|
}
|
|
18154
18158
|
|
|
18159
|
+
# Visible trust trajectory (R4): is the agent earning autonomy on THIS repo?
|
|
18160
|
+
# Shows council pass-rate, gate pass-rate, iterations-to-completion, and human
|
|
18161
|
+
# interventions trending up/down/flat across the per-run proof-of-run history
|
|
18162
|
+
# in .loki/proofs/. Bash fallback for the Bun route (loki-ts/src/commands/
|
|
18163
|
+
# trust.ts); both derive from the same proof.json files. Shells out to the
|
|
18164
|
+
# shared derivation module (autonomy/lib/trust_trajectory.py) so the CLI, the
|
|
18165
|
+
# dashboard endpoint, and the tests all agree. Honest: with <2 runs it prints
|
|
18166
|
+
# "not enough history yet", never a fabricated trend.
|
|
18167
|
+
cmd_trust() {
|
|
18168
|
+
local pass_args=()
|
|
18169
|
+
while [[ $# -gt 0 ]]; do
|
|
18170
|
+
case "$1" in
|
|
18171
|
+
--help|-h)
|
|
18172
|
+
echo -e "${BOLD}loki trust${NC} - Visible trust trajectory (R4)"
|
|
18173
|
+
echo ""
|
|
18174
|
+
echo "Usage: loki trust [options]"
|
|
18175
|
+
echo ""
|
|
18176
|
+
echo "Shows whether the agent is earning autonomy on THIS repo over"
|
|
18177
|
+
echo "time: council pass-rate, gate pass-rate, iterations-to-completion,"
|
|
18178
|
+
echo "and human interventions, each trending up/down/flat. Derived"
|
|
18179
|
+
echo "read-only from proof-of-run history in .loki/proofs/."
|
|
18180
|
+
echo ""
|
|
18181
|
+
echo "Options:"
|
|
18182
|
+
echo " --json Machine-readable JSON output"
|
|
18183
|
+
echo " --help, -h Show this help"
|
|
18184
|
+
echo ""
|
|
18185
|
+
echo "With fewer than 2 recorded runs this prints 'not enough history"
|
|
18186
|
+
echo "yet' rather than a fabricated trend. Complements 'loki kpis'"
|
|
18187
|
+
echo "(single-run snapshot)."
|
|
18188
|
+
exit 0
|
|
18189
|
+
;;
|
|
18190
|
+
--json) pass_args+=("--json"); shift ;;
|
|
18191
|
+
*) echo -e "${RED}Unknown option: $1${NC}"; echo "Run 'loki trust --help' for usage."; exit 1 ;;
|
|
18192
|
+
esac
|
|
18193
|
+
done
|
|
18194
|
+
|
|
18195
|
+
if ! command -v python3 &>/dev/null; then
|
|
18196
|
+
echo -e "${RED}python3 is required for the trust trajectory view${NC}"
|
|
18197
|
+
exit 1
|
|
18198
|
+
fi
|
|
18199
|
+
|
|
18200
|
+
local trust_mod="$_LOKI_SCRIPT_DIR/lib/trust_trajectory.py"
|
|
18201
|
+
if [ ! -f "$trust_mod" ]; then
|
|
18202
|
+
echo -e "${RED}trust_trajectory.py not found at $trust_mod${NC}"
|
|
18203
|
+
exit 1
|
|
18204
|
+
fi
|
|
18205
|
+
|
|
18206
|
+
local loki_dir="${LOKI_DIR:-.loki}"
|
|
18207
|
+
# Safe empty-array expansion (bash 3.2 + set -u): ${arr[@]+"${arr[@]}"}
|
|
18208
|
+
# expands to nothing when pass_args is empty instead of an unbound error.
|
|
18209
|
+
python3 "$trust_mod" --loki-dir "$loki_dir" ${pass_args[@]+"${pass_args[@]}"}
|
|
18210
|
+
}
|
|
18211
|
+
|
|
18155
18212
|
# Transparent cost view (R3): per-run + per-project spend, model routing, and
|
|
18156
18213
|
# budget status with the 80% warn line. Reuses efficiency_cost.collect_efficiency
|
|
18157
18214
|
# for the current-run aggregate (single source of truth) and reads .loki/proofs/
|
|
@@ -25275,6 +25332,173 @@ _loki_gist_upload() {
|
|
|
25275
25332
|
echo -e "${GREEN}Shared: ${gist_url}${NC}"
|
|
25276
25333
|
}
|
|
25277
25334
|
|
|
25335
|
+
# loki_tier_gate - R9 open-core tier/license seam.
|
|
25336
|
+
#
|
|
25337
|
+
# OSS-FIRST CONTRACT: this is a no-op ALLOW for OSS users. LOKI_TIER defaults
|
|
25338
|
+
# to "oss" and every existing free feature stays fully free. This function is
|
|
25339
|
+
# the single place where a future hosted/enterprise build would gate a
|
|
25340
|
+
# hosted-only capability. It is NEVER called from any existing free command
|
|
25341
|
+
# path; its only caller is the opt-in --hosted publish seam below. For OSS
|
|
25342
|
+
# (the default), it always returns 0 (allow).
|
|
25343
|
+
#
|
|
25344
|
+
# Args: $1 = capability name (informational; e.g. "hosted_publish").
|
|
25345
|
+
# Returns: 0 = allowed, 1 = gated (non-OSS tier without entitlement).
|
|
25346
|
+
# Env: LOKI_TIER (default "oss"), LOKI_LICENSE_KEY (optional, non-OSS only).
|
|
25347
|
+
loki_tier_gate() {
|
|
25348
|
+
local capability="${1:-}"
|
|
25349
|
+
local tier="${LOKI_TIER:-oss}"
|
|
25350
|
+
|
|
25351
|
+
# OSS tier: everything is allowed, always. No license, no network, no gate.
|
|
25352
|
+
if [ "$tier" = "oss" ]; then
|
|
25353
|
+
return 0
|
|
25354
|
+
fi
|
|
25355
|
+
|
|
25356
|
+
# Non-OSS tiers (hosted/enterprise) are a SEAM only. The hosted backend
|
|
25357
|
+
# and license-verification service do not exist yet, so we cannot validate
|
|
25358
|
+
# an entitlement. Be honest: do not pretend to grant a paid capability.
|
|
25359
|
+
# A real hosted build replaces this branch with a verified license check.
|
|
25360
|
+
if [ -z "${LOKI_LICENSE_KEY:-}" ]; then
|
|
25361
|
+
echo -e "${YELLOW}LOKI_TIER='${tier}' requested but no LOKI_LICENSE_KEY set.${NC}" >&2
|
|
25362
|
+
echo "Hosted/enterprise license verification is not available yet." >&2
|
|
25363
|
+
echo "OSS users: leave LOKI_TIER unset (or 'oss') -- everything stays free." >&2
|
|
25364
|
+
return 1
|
|
25365
|
+
fi
|
|
25366
|
+
|
|
25367
|
+
# A license key is present but there is no verification backend yet. We do
|
|
25368
|
+
# NOT fabricate a successful verification. The capability stays ungated for
|
|
25369
|
+
# OSS-equivalent use; the seam is documented in docs/OPEN-CORE-BOUNDARY.md.
|
|
25370
|
+
echo -e "${YELLOW}LOKI_LICENSE_KEY set but the verification backend is not available yet (R9 seam).${NC}" >&2
|
|
25371
|
+
return 0
|
|
25372
|
+
}
|
|
25373
|
+
|
|
25374
|
+
# _loki_hosted_publish_proof - R9 hosted proof-publish client stub.
|
|
25375
|
+
#
|
|
25376
|
+
# Posts an ALREADY-REDACTED proof page to a self-hosted/SaaS endpoint given by
|
|
25377
|
+
# LOKI_HOSTED_ENDPOINT. There is NO official Loki hosted backend yet; this is a
|
|
25378
|
+
# clean client seam an operator can point at their own endpoint. We never
|
|
25379
|
+
# fabricate a hosted URL: on success we print the URL the endpoint returned (or
|
|
25380
|
+
# the endpoint itself); on any failure we print an honest error and exit non-0.
|
|
25381
|
+
#
|
|
25382
|
+
# Args: $1 = proof id, $2 = redacted index.html path, $3 = proof.json path.
|
|
25383
|
+
# Returns: 0 on success, non-zero on missing endpoint / transport / non-2xx.
|
|
25384
|
+
_loki_hosted_publish_proof() {
|
|
25385
|
+
local id="$1"
|
|
25386
|
+
local html="$2"
|
|
25387
|
+
local pj="$3"
|
|
25388
|
+
|
|
25389
|
+
# Tier seam (no-op allow for OSS). Hosted publish is opt-in regardless.
|
|
25390
|
+
loki_tier_gate "hosted_publish" || true
|
|
25391
|
+
|
|
25392
|
+
local endpoint="${LOKI_HOSTED_ENDPOINT:-}"
|
|
25393
|
+
if [ -z "$endpoint" ]; then
|
|
25394
|
+
echo -e "${YELLOW}Hosted publishing backend not available.${NC}" >&2
|
|
25395
|
+
echo "There is no official Loki hosted service yet (R9 ships the seam, not a live backend)." >&2
|
|
25396
|
+
echo "To publish to your own hosted endpoint, set LOKI_HOSTED_ENDPOINT to its URL." >&2
|
|
25397
|
+
echo "Or publish to a GitHub Gist instead: loki proof share ${id}" >&2
|
|
25398
|
+
return 1
|
|
25399
|
+
fi
|
|
25400
|
+
|
|
25401
|
+
if ! command -v curl &>/dev/null; then
|
|
25402
|
+
echo -e "${RED}curl not found${NC}" >&2
|
|
25403
|
+
echo "Hosted publishing requires curl. Install curl or use: loki proof share ${id}" >&2
|
|
25404
|
+
return 1
|
|
25405
|
+
fi
|
|
25406
|
+
|
|
25407
|
+
# CREDIBILITY: we upload the file the generator already redacted (the same
|
|
25408
|
+
# bytes 'loki proof share' would put on a gist). We do not build a fresh
|
|
25409
|
+
# body that could bypass redaction. If proof.json reports redaction was not
|
|
25410
|
+
# applied, refuse -- never publish an unredacted artifact.
|
|
25411
|
+
if [ -f "$pj" ]; then
|
|
25412
|
+
local redaction_ok
|
|
25413
|
+
redaction_ok=$(LOKI_PROOF_JSON="$pj" python3 - <<'PYEOF' 2>/dev/null || echo "unknown"
|
|
25414
|
+
import json, os
|
|
25415
|
+
try:
|
|
25416
|
+
d = json.load(open(os.environ["LOKI_PROOF_JSON"]))
|
|
25417
|
+
except Exception:
|
|
25418
|
+
print("unknown")
|
|
25419
|
+
else:
|
|
25420
|
+
print("yes" if (d.get("redaction") or {}).get("applied") else "no")
|
|
25421
|
+
PYEOF
|
|
25422
|
+
)
|
|
25423
|
+
if [ "$redaction_ok" = "no" ]; then
|
|
25424
|
+
echo -e "${RED}Refusing to publish: proof redaction was not applied.${NC}" >&2
|
|
25425
|
+
echo "Regenerate the proof (LOKI_PROOF=1) so the redactor runs, then retry." >&2
|
|
25426
|
+
return 1
|
|
25427
|
+
fi
|
|
25428
|
+
fi
|
|
25429
|
+
|
|
25430
|
+
echo -e "${BOLD}Publishing proof '${id}' to hosted endpoint${NC}"
|
|
25431
|
+
echo " endpoint: ${endpoint}"
|
|
25432
|
+
echo " payload: ${html} (already redacted by the generator)"
|
|
25433
|
+
echo ""
|
|
25434
|
+
|
|
25435
|
+
# POST the redacted HTML. Auth header is sent only if a license key exists;
|
|
25436
|
+
# OSS users with their own endpoint need no key.
|
|
25437
|
+
local tmp_body tmp_code
|
|
25438
|
+
tmp_body=$(mktemp "/tmp/loki-hosted-XXXXXX.out")
|
|
25439
|
+
local -a curl_args=(-sS -o "$tmp_body" -w '%{http_code}' -X POST
|
|
25440
|
+
-H "Content-Type: text/html"
|
|
25441
|
+
-H "X-Loki-Proof-Id: ${id}"
|
|
25442
|
+
--data-binary "@${html}")
|
|
25443
|
+
if [ -n "${LOKI_LICENSE_KEY:-}" ]; then
|
|
25444
|
+
curl_args+=(-H "Authorization: Bearer ${LOKI_LICENSE_KEY}")
|
|
25445
|
+
fi
|
|
25446
|
+
tmp_code=$(curl "${curl_args[@]}" "$endpoint" 2>/dev/null)
|
|
25447
|
+
local curl_exit=$?
|
|
25448
|
+
|
|
25449
|
+
if [ "$curl_exit" -ne 0 ]; then
|
|
25450
|
+
echo -e "${RED}Failed to reach hosted endpoint (curl exit ${curl_exit}).${NC}" >&2
|
|
25451
|
+
echo "Check LOKI_HOSTED_ENDPOINT or publish to a gist: loki proof share ${id}" >&2
|
|
25452
|
+
rm -f "$tmp_body"
|
|
25453
|
+
return 1
|
|
25454
|
+
fi
|
|
25455
|
+
|
|
25456
|
+
# Accept any 2xx. The published URL comes from the endpoint response if it
|
|
25457
|
+
# returns one (we look for a "url" field), else we report the endpoint. We
|
|
25458
|
+
# NEVER print a fabricated URL.
|
|
25459
|
+
case "$tmp_code" in
|
|
25460
|
+
2*)
|
|
25461
|
+
local published_url
|
|
25462
|
+
published_url=$(LOKI_HOSTED_BODY="$tmp_body" LOKI_HOSTED_EP="$endpoint" python3 - <<'PYEOF' 2>/dev/null || true
|
|
25463
|
+
import json, os
|
|
25464
|
+
body_path = os.environ["LOKI_HOSTED_BODY"]
|
|
25465
|
+
try:
|
|
25466
|
+
txt = open(body_path).read().strip()
|
|
25467
|
+
except Exception:
|
|
25468
|
+
txt = ""
|
|
25469
|
+
url = ""
|
|
25470
|
+
try:
|
|
25471
|
+
d = json.loads(txt)
|
|
25472
|
+
if isinstance(d, dict):
|
|
25473
|
+
url = d.get("url") or d.get("public_url") or ""
|
|
25474
|
+
except Exception:
|
|
25475
|
+
url = ""
|
|
25476
|
+
print(url)
|
|
25477
|
+
PYEOF
|
|
25478
|
+
)
|
|
25479
|
+
rm -f "$tmp_body"
|
|
25480
|
+
if [ -n "$published_url" ]; then
|
|
25481
|
+
echo -e "${GREEN}Published: ${published_url}${NC}"
|
|
25482
|
+
else
|
|
25483
|
+
echo -e "${GREEN}Published to ${endpoint} (HTTP ${tmp_code}).${NC}"
|
|
25484
|
+
echo "The endpoint did not return a 'url' field; check your endpoint's response."
|
|
25485
|
+
fi
|
|
25486
|
+
return 0
|
|
25487
|
+
;;
|
|
25488
|
+
*)
|
|
25489
|
+
echo -e "${RED}Hosted endpoint returned HTTP ${tmp_code}.${NC}" >&2
|
|
25490
|
+
if [ -s "$tmp_body" ]; then
|
|
25491
|
+
echo "Response:" >&2
|
|
25492
|
+
head -c 500 "$tmp_body" >&2
|
|
25493
|
+
echo "" >&2
|
|
25494
|
+
fi
|
|
25495
|
+
echo "Nothing was published. Or publish to a gist: loki proof share ${id}" >&2
|
|
25496
|
+
rm -f "$tmp_body"
|
|
25497
|
+
return 1
|
|
25498
|
+
;;
|
|
25499
|
+
esac
|
|
25500
|
+
}
|
|
25501
|
+
|
|
25278
25502
|
# loki bench - head-to-head benchmark harness (R2).
|
|
25279
25503
|
# Subcommands: run <task> | vs <task> | list | verify <result.json>.
|
|
25280
25504
|
# Thin pass-through to benchmarks/bench/run.sh (shared python core runner.py).
|
|
@@ -25342,7 +25566,7 @@ cmd_proof() {
|
|
|
25342
25566
|
echo "Options for 'share':"
|
|
25343
25567
|
echo " --yes Skip the redaction-preview confirmation prompt"
|
|
25344
25568
|
echo " --private Create a secret gist (default: public)"
|
|
25345
|
-
echo " --hosted
|
|
25569
|
+
echo " --hosted Publish to LOKI_HOSTED_ENDPOINT (open-core seam; no official backend yet)"
|
|
25346
25570
|
echo ""
|
|
25347
25571
|
echo "Proofs are generated automatically at run completion (LOKI_PROOF=0 to opt out)."
|
|
25348
25572
|
[ "$sub" = "" ] && exit 1
|
|
@@ -25441,15 +25665,13 @@ PYEOF
|
|
|
25441
25665
|
local id=""
|
|
25442
25666
|
local skip_confirm=0
|
|
25443
25667
|
local visibility="--public"
|
|
25668
|
+
local hosted=0
|
|
25444
25669
|
while [[ $# -gt 0 ]]; do
|
|
25445
25670
|
case "$1" in
|
|
25446
25671
|
--yes|-y) skip_confirm=1; shift ;;
|
|
25447
25672
|
--private) visibility=""; shift ;;
|
|
25448
25673
|
--public) visibility="--public"; shift ;;
|
|
25449
|
-
--hosted)
|
|
25450
|
-
echo -e "${RED}Hosted publishing is not available yet (coming in R9).${NC}"
|
|
25451
|
-
exit 1
|
|
25452
|
-
;;
|
|
25674
|
+
--hosted) hosted=1; shift ;;
|
|
25453
25675
|
-*) echo -e "${RED}Unknown option: $1${NC}"; exit 1 ;;
|
|
25454
25676
|
*) id="$1"; shift ;;
|
|
25455
25677
|
esac
|
|
@@ -25464,6 +25686,17 @@ PYEOF
|
|
|
25464
25686
|
echo "Use 'loki proof list' to see available proofs."
|
|
25465
25687
|
exit 1
|
|
25466
25688
|
fi
|
|
25689
|
+
# R9 open-core hosted-publish seam. Only taken when the user
|
|
25690
|
+
# explicitly passes --hosted. The default gist path below stays
|
|
25691
|
+
# byte-for-byte unchanged for OSS users (zero hosted backend
|
|
25692
|
+
# required). We never silent-fall-back to gist here: the user asked
|
|
25693
|
+
# for hosted, so we POST to a configured LOKI_HOSTED_ENDPOINT or
|
|
25694
|
+
# print an honest "no endpoint configured" message and exit non-zero.
|
|
25695
|
+
# We never fabricate a hosted URL.
|
|
25696
|
+
if [ "$hosted" -eq 1 ]; then
|
|
25697
|
+
_loki_hosted_publish_proof "$id" "$html" "${proofs_dir}/${id}/proof.json"
|
|
25698
|
+
exit $?
|
|
25699
|
+
fi
|
|
25467
25700
|
if ! command -v gh &>/dev/null; then
|
|
25468
25701
|
echo -e "${RED}gh CLI not found${NC}"
|
|
25469
25702
|
echo "Install the GitHub CLI to publish a proof:"
|
package/bin/loki
CHANGED
|
@@ -116,10 +116,11 @@ fi
|
|
|
116
116
|
# Two-token routes (provider show/list, memory list/index) match on the first
|
|
117
117
|
# token only; the Bun dispatcher handles subcommand routing internally.
|
|
118
118
|
case "${1:-}" in
|
|
119
|
-
version|--version|-v|status|stats|doctor|provider|memory|rollback|internal|kpis|proof|wiki)
|
|
119
|
+
version|--version|-v|status|stats|doctor|provider|memory|rollback|internal|kpis|trust|proof|wiki)
|
|
120
120
|
# v7.5.2: rollback added (wires loki-ts/src/commands/rollback.ts).
|
|
121
121
|
# v7.5.3: internal added for autonomy/run.sh phase1-hooks calls.
|
|
122
122
|
# v7.5.28: kpis added (Phase K MVP: read-only KPI snapshot).
|
|
123
|
+
# R4: trust added (visible trust trajectory; Bun route, bash fallback).
|
|
123
124
|
#
|
|
124
125
|
# v7.8.2: emit the cli_command product-analytics event for Bun-routed
|
|
125
126
|
# commands. The bash CLI fires this from autonomy/loki main(), but the
|
package/dashboard/__init__.py
CHANGED
package/dashboard/server.py
CHANGED
|
@@ -460,6 +460,7 @@ async def _push_loki_state_loop() -> None:
|
|
|
460
460
|
last_mtime: float = 0.0
|
|
461
461
|
_last_skill_hash: str = "" # Track skill-session state changes
|
|
462
462
|
_last_budget_status: str = "" # Track budget-status transitions (R3)
|
|
463
|
+
_last_trust_signature: str = "" # Track trust-trajectory changes (R4)
|
|
463
464
|
while True:
|
|
464
465
|
try:
|
|
465
466
|
if not manager.active_connections:
|
|
@@ -490,6 +491,30 @@ async def _push_loki_state_loop() -> None:
|
|
|
490
491
|
except (OSError, ValueError, KeyError):
|
|
491
492
|
pass
|
|
492
493
|
|
|
494
|
+
# R4 visible trust trajectory: proactively push a trust_update when
|
|
495
|
+
# the trajectory's improving/regressing tally changes (e.g. a new
|
|
496
|
+
# run just landed a council pass), so an open dashboard reflects the
|
|
497
|
+
# earned-autonomy trend without a manual refresh. Mirrors the R3
|
|
498
|
+
# budget_status transition push; reuses manager.broadcast (no second
|
|
499
|
+
# channel). Signature gates the push so we only broadcast on change.
|
|
500
|
+
try:
|
|
501
|
+
_tmod = _load_trust_module()
|
|
502
|
+
if _tmod is not None:
|
|
503
|
+
_traj = _tmod.compute_trajectory(str(loki_dir))
|
|
504
|
+
_sig = "%d:%d:%d" % (
|
|
505
|
+
_traj.get("runs_count", 0),
|
|
506
|
+
_traj.get("improving_count", 0),
|
|
507
|
+
_traj.get("regressing_count", 0),
|
|
508
|
+
)
|
|
509
|
+
if _sig != _last_trust_signature:
|
|
510
|
+
await manager.broadcast({
|
|
511
|
+
"type": "trust_update",
|
|
512
|
+
"data": _traj,
|
|
513
|
+
})
|
|
514
|
+
_last_trust_signature = _sig
|
|
515
|
+
except (OSError, ValueError, KeyError):
|
|
516
|
+
pass
|
|
517
|
+
|
|
493
518
|
_broadcast_sent = False
|
|
494
519
|
|
|
495
520
|
if state_file.exists():
|
|
@@ -4780,6 +4805,81 @@ async def get_cost_timeline():
|
|
|
4780
4805
|
}
|
|
4781
4806
|
|
|
4782
4807
|
|
|
4808
|
+
# =============================================================================
|
|
4809
|
+
# Trust trajectory API (R4): is the agent earning autonomy on THIS repo?
|
|
4810
|
+
# =============================================================================
|
|
4811
|
+
|
|
4812
|
+
_TRUST_MODULE = None # cached import of autonomy/lib/trust_trajectory.py
|
|
4813
|
+
|
|
4814
|
+
|
|
4815
|
+
def _load_trust_module():
|
|
4816
|
+
"""Import the shared trust-trajectory derivation (single source of truth).
|
|
4817
|
+
|
|
4818
|
+
The derivation lives in autonomy/lib/trust_trajectory.py so the dashboard
|
|
4819
|
+
endpoint, the bash `cmd_trust`, and the test suite all agree. Loaded via
|
|
4820
|
+
importlib because autonomy/lib is not an importable package. Cached after
|
|
4821
|
+
first load. Returns None if the module cannot be found (degraded mode).
|
|
4822
|
+
"""
|
|
4823
|
+
global _TRUST_MODULE
|
|
4824
|
+
if _TRUST_MODULE is not None:
|
|
4825
|
+
return _TRUST_MODULE
|
|
4826
|
+
repo_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
4827
|
+
mod_path = os.path.join(repo_root, "autonomy", "lib", "trust_trajectory.py")
|
|
4828
|
+
if not os.path.isfile(mod_path):
|
|
4829
|
+
return None
|
|
4830
|
+
try:
|
|
4831
|
+
import importlib.util as _ilu
|
|
4832
|
+
spec = _ilu.spec_from_file_location("trust_trajectory", mod_path)
|
|
4833
|
+
if spec is None or spec.loader is None:
|
|
4834
|
+
return None
|
|
4835
|
+
mod = _ilu.module_from_spec(spec)
|
|
4836
|
+
spec.loader.exec_module(mod)
|
|
4837
|
+
_TRUST_MODULE = mod
|
|
4838
|
+
return mod
|
|
4839
|
+
except Exception:
|
|
4840
|
+
return None
|
|
4841
|
+
|
|
4842
|
+
|
|
4843
|
+
@app.get("/api/trust/trajectory")
|
|
4844
|
+
async def get_trust_trajectory():
|
|
4845
|
+
"""Per-project trust trajectory derived from proof-of-run history.
|
|
4846
|
+
|
|
4847
|
+
Mirrors /api/cost/timeline: reads the persistent per-run records under
|
|
4848
|
+
.loki/proofs/<run_id>/proof.json (the same source R3 cost history uses) and
|
|
4849
|
+
derives whether the agent is earning autonomy on THIS repo over time:
|
|
4850
|
+
council pass-rate, gate pass-rate, iterations-to-completion, and (when
|
|
4851
|
+
recorded) human interventions, each with an up/down/flat direction and an
|
|
4852
|
+
`improving` flag that already accounts for per-axis polarity.
|
|
4853
|
+
|
|
4854
|
+
Honest-data rule: with fewer than 2 recorded runs the response is
|
|
4855
|
+
insufficient=True and NO direction is fabricated. Every number derives from
|
|
4856
|
+
real proof.json values; a missing axis is reported available=False, never a
|
|
4857
|
+
misleading zero. No PII leaves the derivation (only run_id, timestamps, and
|
|
4858
|
+
derived numeric axes).
|
|
4859
|
+
"""
|
|
4860
|
+
loki_dir = _get_loki_dir()
|
|
4861
|
+
mod = _load_trust_module()
|
|
4862
|
+
if mod is None:
|
|
4863
|
+
return {
|
|
4864
|
+
"schema_version": 1,
|
|
4865
|
+
"available": False,
|
|
4866
|
+
"error": "trust_trajectory module not found",
|
|
4867
|
+
"runs_count": 0,
|
|
4868
|
+
"insufficient": True,
|
|
4869
|
+
"axes": {},
|
|
4870
|
+
"series": [],
|
|
4871
|
+
"notes": ["trust derivation module unavailable in this install"],
|
|
4872
|
+
}
|
|
4873
|
+
traj = mod.compute_trajectory(str(loki_dir))
|
|
4874
|
+
# Best-effort cache write so other surfaces share one source of truth.
|
|
4875
|
+
try:
|
|
4876
|
+
mod.write_trajectory_cache(str(loki_dir), traj)
|
|
4877
|
+
except Exception:
|
|
4878
|
+
pass
|
|
4879
|
+
traj["available"] = True
|
|
4880
|
+
return traj
|
|
4881
|
+
|
|
4882
|
+
|
|
4783
4883
|
# =============================================================================
|
|
4784
4884
|
# Pricing API
|
|
4785
4885
|
# =============================================================================
|
|
@@ -6764,6 +6864,18 @@ async def serve_cost_panel():
|
|
|
6764
6864
|
return Response(status_code=404)
|
|
6765
6865
|
|
|
6766
6866
|
|
|
6867
|
+
# R4: standalone trust-trajectory page that fetches /api/trust/trajectory.
|
|
6868
|
+
# Mirrors the cost.html / /cost pattern: works without the SPA build.
|
|
6869
|
+
@app.get("/trust", include_in_schema=False)
|
|
6870
|
+
async def serve_trust_panel():
|
|
6871
|
+
"""Serve the standalone trust-trajectory HTML panel."""
|
|
6872
|
+
if STATIC_DIR:
|
|
6873
|
+
trust_path = os.path.join(STATIC_DIR, "trust.html")
|
|
6874
|
+
if os.path.isfile(trust_path):
|
|
6875
|
+
return FileResponse(trust_path, media_type="text/html")
|
|
6876
|
+
return Response(status_code=404)
|
|
6877
|
+
|
|
6878
|
+
|
|
6767
6879
|
# Serve index.html or standalone HTML for root
|
|
6768
6880
|
@app.get("/", include_in_schema=False)
|
|
6769
6881
|
async def serve_index():
|
|
@@ -679,6 +679,10 @@
|
|
|
679
679
|
<svg viewBox="0 0 24 24"><line x1="12" y1="1" x2="12" y2="23"/><path d="M17 5H9.5a3.5 3.5 0 000 7h5a3.5 3.5 0 010 7H6"/></svg>
|
|
680
680
|
Cost
|
|
681
681
|
</button>
|
|
682
|
+
<button class="nav-link" data-section="trust" id="nav-trust">
|
|
683
|
+
<svg viewBox="0 0 24 24"><polyline points="3 17 9 11 13 15 21 7" fill="none" stroke="currentColor" stroke-width="2"/><polyline points="15 7 21 7 21 13" fill="none" stroke="currentColor" stroke-width="2"/></svg>
|
|
684
|
+
Trust
|
|
685
|
+
</button>
|
|
682
686
|
<button class="nav-link" data-section="checkpoint" id="nav-checkpoint">
|
|
683
687
|
<svg viewBox="0 0 24 24"><path d="M19 21H5a2 2 0 01-2-2V5a2 2 0 012-2h11l5 5v11a2 2 0 01-2 2z"/><polyline points="17 21 17 13 7 13 7 21"/><polyline points="7 3 7 8 15 8"/></svg>
|
|
684
688
|
Checkpoints
|
|
@@ -1109,6 +1113,17 @@
|
|
|
1109
1113
|
<loki-cost-dashboard id="cost-dashboard"></loki-cost-dashboard>
|
|
1110
1114
|
</div>
|
|
1111
1115
|
|
|
1116
|
+
<!-- Trust Trajectory (R4): embeds the standalone /trust panel so the SPA
|
|
1117
|
+
and the build-free page share one renderer + one /api/trust/trajectory
|
|
1118
|
+
source. Mirrors the cost panel wiring. -->
|
|
1119
|
+
<div class="section-page" id="page-trust">
|
|
1120
|
+
<div class="section-page-header">
|
|
1121
|
+
<h2 class="section-page-title">Trust Trajectory</h2>
|
|
1122
|
+
</div>
|
|
1123
|
+
<iframe id="trust-frame" title="Trust trajectory" src="about:blank"
|
|
1124
|
+
style="width:100%;height:calc(100vh - 160px);border:0;border-radius:8px;background:#0f1115;"></iframe>
|
|
1125
|
+
</div>
|
|
1126
|
+
|
|
1112
1127
|
<!-- Checkpoints -->
|
|
1113
1128
|
<div class="section-page" id="page-checkpoint">
|
|
1114
1129
|
<div class="section-page-header">
|
|
@@ -13808,6 +13823,15 @@ document.addEventListener('DOMContentLoaded', function() {
|
|
|
13808
13823
|
if (pageEl) {
|
|
13809
13824
|
pageEl.classList.add('active');
|
|
13810
13825
|
}
|
|
13826
|
+
// R4: lazy-load the trust panel iframe on first open (avoids a fetch on
|
|
13827
|
+
// every page that the user never visits).
|
|
13828
|
+
if (sectionId === 'trust') {
|
|
13829
|
+
var tframe = document.getElementById('trust-frame');
|
|
13830
|
+
if (tframe && (!tframe.src || tframe.src === 'about:blank' ||
|
|
13831
|
+
tframe.getAttribute('src') === 'about:blank')) {
|
|
13832
|
+
tframe.src = '/trust';
|
|
13833
|
+
}
|
|
13834
|
+
}
|
|
13811
13835
|
// Update nav active state
|
|
13812
13836
|
navLinks.forEach(function(link) { link.classList.remove('active'); });
|
|
13813
13837
|
var navEl = document.querySelector('.nav-link[data-section="' + sectionId + '"]');
|
|
@@ -13834,7 +13858,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
|
|
13834
13858
|
document.addEventListener('keydown', function(e) {
|
|
13835
13859
|
if ((e.metaKey || e.ctrlKey) && ((e.key >= '1' && e.key <= '9') || e.key === '0')) {
|
|
13836
13860
|
e.preventDefault();
|
|
13837
|
-
var sections = ['overview', 'insights', 'prd-checklist', 'app-runner', 'council', 'quality', 'cost', 'checkpoint', 'context', 'notifications', 'migration', 'analytics', 'escalations'];
|
|
13861
|
+
var sections = ['overview', 'insights', 'prd-checklist', 'app-runner', 'council', 'quality', 'cost', 'trust', 'checkpoint', 'context', 'notifications', 'migration', 'analytics', 'escalations'];
|
|
13838
13862
|
var idx = e.key === '0' ? 9 : parseInt(e.key) - 1;
|
|
13839
13863
|
if (idx < sections.length) switchSection(sections[idx]);
|
|
13840
13864
|
}
|
|
@@ -13875,7 +13899,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
|
|
13875
13899
|
// Skip if modifier keys are held (let browser defaults work)
|
|
13876
13900
|
if (e.metaKey || e.ctrlKey || e.altKey) return;
|
|
13877
13901
|
|
|
13878
|
-
var sections = ['overview', 'insights', 'prd-checklist', 'app-runner', 'council', 'quality', 'cost', 'checkpoint', 'context', 'notifications', 'migration', 'analytics', 'escalations'];
|
|
13902
|
+
var sections = ['overview', 'insights', 'prd-checklist', 'app-runner', 'council', 'quality', 'cost', 'trust', 'checkpoint', 'context', 'notifications', 'migration', 'analytics', 'escalations'];
|
|
13879
13903
|
|
|
13880
13904
|
switch (e.key) {
|
|
13881
13905
|
// Section navigation: 1-9, 0
|