loki-mode 7.15.0 → 7.16.1

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.
@@ -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
@@ -850,10 +854,7 @@
850
854
  }
851
855
  function renderViewer(file){
852
856
  if (!file){ viewEl.textContent = 'Select a file to view its contents.'; return; }
853
- var header = file.name + ' (' + fmtSize(file.size) + (file.truncated ? ', truncated' : '') + ')
854
- ' + file.path + '
855
-
856
- ';
857
+ var header = file.name + ' (' + fmtSize(file.size) + (file.truncated ? ', truncated' : '') + ')\n' + file.path + '\n\n';
857
858
  var body = file.content || '';
858
859
  if (file.kind === 'json'){
859
860
  try { body = JSON.stringify(JSON.parse(body), null, 2); } catch(e){ /* leave raw */ }
@@ -904,8 +905,7 @@
904
905
  // and paragraphs. Script tags are stripped from non-code text.
905
906
  function renderUsageMarkdown(md) {
906
907
  if (!md) return '';
907
- var lines = md.split('
908
- ');
908
+ var lines = md.split('\n');
909
909
  var html = '';
910
910
  var i = 0;
911
911
  var inList = null; // 'ul' or 'ol'
@@ -936,15 +936,15 @@
936
936
  var esc = escapeHtml(ch);
937
937
  // strip any residual <script tags that survived escaping (defensive)
938
938
  esc = esc.replace(/&lt;script/gi, '&lt;sc​ript');
939
- esc = esc.replace(/**([^*]+)**/g, '<strong>$1</strong>');
939
+ esc = esc.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>');
940
940
  esc = esc.replace(/__([^_]+)__/g, '<strong>$1</strong>');
941
- esc = esc.replace(/*([^*]+)*/g, '<em>$1</em>');
941
+ esc = esc.replace(/\*([^*]+)\*/g, '<em>$1</em>');
942
942
  esc = esc.replace(/_([^_]+)_/g, '<em>$1</em>');
943
943
  // v7.7.11 XSS guard (Opus 1 council): only allow http/https/mailto/anchor/relative href.
944
944
  // javascript: / data: / vbscript: are stripped to a plain code span so the URL stays visible
945
945
  // but is not clickable. USAGE.md absorbs agent output + PRD text; treat as untrusted.
946
- esc = esc.replace(/[([^]]+)](([^)]+))/g, function(_m, label, url){
947
- var safe = /^(https?://|mailto:|#|/(?!/))/.test(url);
946
+ esc = esc.replace(/\[([^\]]+)\]\(([^)]+)\)/g, function(_m, label, url){
947
+ var safe = /^(https?:\/\/|mailto:|#|\/(?!\/))/.test(url);
948
948
  if (safe) return '<a href="' + url + '" rel="noopener noreferrer">' + label + '</a>';
949
949
  return '<code>' + label + ' (' + url + ')</code>';
950
950
  });
@@ -968,15 +968,14 @@
968
968
  codeLines.push(lines[i]);
969
969
  i++;
970
970
  }
971
- var codeContent = escapeHtml(codeLines.join('
972
- '));
971
+ var codeContent = escapeHtml(codeLines.join('\n'));
973
972
  html += '<pre><code' + (lang ? ' class="language-' + escapeHtml(lang) + '"' : '') + '>' + codeContent + '</code></pre>';
974
973
  i++;
975
974
  continue;
976
975
  }
977
976
 
978
977
  // Headings
979
- var hMatch = line.match(/^(#{1,6})s+(.*)/);
978
+ var hMatch = line.match(/^(#{1,6})\s+(.*)/);
980
979
  if (hMatch) {
981
980
  closeList();
982
981
  var level = hMatch[1].length;
@@ -986,7 +985,7 @@
986
985
  }
987
986
 
988
987
  // Horizontal rule
989
- if (/^(-{3,}|*{3,}|_{3,})$/.test(line.trim())) {
988
+ if (/^(-{3,}|\*{3,}|_{3,})$/.test(line.trim())) {
990
989
  closeList();
991
990
  html += '<hr>';
992
991
  i++;
@@ -996,13 +995,13 @@
996
995
  // Blockquote
997
996
  if (/^>/.test(line)) {
998
997
  closeList();
999
- html += '<blockquote>' + inlineFormat(line.replace(/^>s?/, '')) + '</blockquote>';
998
+ html += '<blockquote>' + inlineFormat(line.replace(/^>\s?/, '')) + '</blockquote>';
1000
999
  i++;
1001
1000
  continue;
1002
1001
  }
1003
1002
 
1004
1003
  // Unordered list
1005
- var ulMatch = line.match(/^(s*[-*+])s+(.*)/);
1004
+ var ulMatch = line.match(/^(\s*[-*+])\s+(.*)/);
1006
1005
  if (ulMatch) {
1007
1006
  if (inList !== 'ul') { closeList(); html += '<ul>'; inList = 'ul'; }
1008
1007
  html += '<li>' + inlineFormat(ulMatch[2]) + '</li>';
@@ -1011,7 +1010,7 @@
1011
1010
  }
1012
1011
 
1013
1012
  // Ordered list
1014
- var olMatch = line.match(/^s*d+.s+(.*)/);
1013
+ var olMatch = line.match(/^\s*\d+\.\s+(.*)/);
1015
1014
  if (olMatch) {
1016
1015
  if (inList !== 'ol') { closeList(); html += '<ol>'; inList = 'ol'; }
1017
1016
  html += '<li>' + inlineFormat(olMatch[1]) + '</li>';
@@ -1109,6 +1108,17 @@
1109
1108
  <loki-cost-dashboard id="cost-dashboard"></loki-cost-dashboard>
1110
1109
  </div>
1111
1110
 
1111
+ <!-- Trust Trajectory (R4): embeds the standalone /trust panel so the SPA
1112
+ and the build-free page share one renderer + one /api/trust/trajectory
1113
+ source. Mirrors the cost panel wiring. -->
1114
+ <div class="section-page" id="page-trust">
1115
+ <div class="section-page-header">
1116
+ <h2 class="section-page-title">Trust Trajectory</h2>
1117
+ </div>
1118
+ <iframe id="trust-frame" title="Trust trajectory" src="about:blank"
1119
+ style="width:100%;height:calc(100vh - 160px);border:0;border-radius:8px;background:#0f1115;"></iframe>
1120
+ </div>
1121
+
1112
1122
  <!-- Checkpoints -->
1113
1123
  <div class="section-page" id="page-checkpoint">
1114
1124
  <div class="section-page-header">
@@ -13808,6 +13818,15 @@ document.addEventListener('DOMContentLoaded', function() {
13808
13818
  if (pageEl) {
13809
13819
  pageEl.classList.add('active');
13810
13820
  }
13821
+ // R4: lazy-load the trust panel iframe on first open (avoids a fetch on
13822
+ // every page that the user never visits).
13823
+ if (sectionId === 'trust') {
13824
+ var tframe = document.getElementById('trust-frame');
13825
+ if (tframe && (!tframe.src || tframe.src === 'about:blank' ||
13826
+ tframe.getAttribute('src') === 'about:blank')) {
13827
+ tframe.src = '/trust';
13828
+ }
13829
+ }
13811
13830
  // Update nav active state
13812
13831
  navLinks.forEach(function(link) { link.classList.remove('active'); });
13813
13832
  var navEl = document.querySelector('.nav-link[data-section="' + sectionId + '"]');
@@ -13834,7 +13853,7 @@ document.addEventListener('DOMContentLoaded', function() {
13834
13853
  document.addEventListener('keydown', function(e) {
13835
13854
  if ((e.metaKey || e.ctrlKey) && ((e.key >= '1' && e.key <= '9') || e.key === '0')) {
13836
13855
  e.preventDefault();
13837
- var sections = ['overview', 'insights', 'prd-checklist', 'app-runner', 'council', 'quality', 'cost', 'checkpoint', 'context', 'notifications', 'migration', 'analytics', 'escalations'];
13856
+ var sections = ['overview', 'insights', 'prd-checklist', 'app-runner', 'council', 'quality', 'cost', 'trust', 'checkpoint', 'context', 'notifications', 'migration', 'analytics', 'escalations'];
13838
13857
  var idx = e.key === '0' ? 9 : parseInt(e.key) - 1;
13839
13858
  if (idx < sections.length) switchSection(sections[idx]);
13840
13859
  }
@@ -13875,7 +13894,7 @@ document.addEventListener('DOMContentLoaded', function() {
13875
13894
  // Skip if modifier keys are held (let browser defaults work)
13876
13895
  if (e.metaKey || e.ctrlKey || e.altKey) return;
13877
13896
 
13878
- var sections = ['overview', 'insights', 'prd-checklist', 'app-runner', 'council', 'quality', 'cost', 'checkpoint', 'context', 'notifications', 'migration', 'analytics', 'escalations'];
13897
+ var sections = ['overview', 'insights', 'prd-checklist', 'app-runner', 'council', 'quality', 'cost', 'trust', 'checkpoint', 'context', 'notifications', 'migration', 'analytics', 'escalations'];
13879
13898
 
13880
13899
  switch (e.key) {
13881
13900
  // Section navigation: 1-9, 0
@@ -0,0 +1,271 @@
1
+ <!DOCTYPE html>
2
+ <!--
3
+ Loki Mode - Trust trajectory panel (R4, zero-build standalone).
4
+
5
+ Self-contained: all CSS + JS inlined, no external resources. Fetches
6
+ /api/trust/trajectory and renders, per project over runs/time, whether the
7
+ agent is EARNING autonomy on THIS repo: council pass-rate, gate pass-rate,
8
+ iterations-to-completion, and (when recorded) human interventions, each with
9
+ an up/down/flat direction and an inline-SVG sparkline.
10
+
11
+ The story no competitor tells. Honest-data rule: with fewer than 2 runs this
12
+ shows "not enough history yet", never a fabricated trend.
13
+ -->
14
+ <html lang="en">
15
+ <head>
16
+ <meta charset="utf-8">
17
+ <meta name="viewport" content="width=device-width, initial-scale=1">
18
+ <title>Loki Mode - Trust Trajectory</title>
19
+ <style>
20
+ :root {
21
+ --bg: #0f1115; --panel: #171a21; --panel-2: #1d2129; --border: #2a2f3a;
22
+ --text: #e7e9ee; --muted: #9aa1ad; --faint: #6b7280; --accent: #6f7bf7;
23
+ --green: #34d399; --red: #f87171; --amber: #fbbf24;
24
+ --mono: ui-monospace, "SF Mono", "Menlo", "Consolas", monospace;
25
+ --sans: 'Inter', system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
26
+ }
27
+ * { box-sizing: border-box; }
28
+ body { margin: 0; background: var(--bg); color: var(--text); font-family: var(--sans); line-height: 1.5; }
29
+ a { color: var(--accent); text-decoration: none; }
30
+ a:hover { text-decoration: underline; }
31
+ .wrap { max-width: 960px; margin: 0 auto; padding: 40px 20px 80px; }
32
+ .head { display: flex; align-items: baseline; justify-content: space-between; margin-bottom: 8px; }
33
+ h1 { font-size: 24px; font-weight: 650; letter-spacing: -0.3px; margin: 0; }
34
+ h2 { font-size: 15px; font-weight: 600; color: var(--muted); margin: 30px 0 12px; text-transform: uppercase; letter-spacing: 0.5px; }
35
+ .head a { font-size: 13px; }
36
+ .sub { color: var(--muted); font-size: 14px; margin: 0 0 26px; }
37
+ .cards { display: flex; gap: 14px; flex-wrap: wrap; }
38
+ .card { flex: 1 1 200px; background: var(--panel); border: 1px solid var(--border); border-radius: 12px; padding: 16px 18px; }
39
+ .card .label { color: var(--muted); font-size: 12px; text-transform: uppercase; letter-spacing: 0.5px; }
40
+ .card .val { font-family: var(--mono); font-size: 26px; font-weight: 650; margin-top: 6px; }
41
+ .card .note { color: var(--faint); font-size: 12px; margin-top: 4px; }
42
+ .axes { display: flex; flex-direction: column; gap: 12px; }
43
+ .axis { background: var(--panel); border: 1px solid var(--border); border-radius: 12px; padding: 16px 18px; display: flex; align-items: center; gap: 16px; }
44
+ .axis .meta { flex: 1 1 auto; min-width: 0; }
45
+ .axis .name { font-size: 14px; font-weight: 600; }
46
+ .axis .desc { color: var(--faint); font-size: 12px; margin-top: 2px; }
47
+ .axis .spark { flex: 0 0 200px; }
48
+ .axis .verdict { flex: 0 0 150px; text-align: right; }
49
+ .axis .dir { font-family: var(--mono); font-size: 14px; font-weight: 650; }
50
+ .axis .tag { font-size: 12px; margin-top: 2px; }
51
+ .dir.up { color: var(--green); }
52
+ .dir.down { color: var(--green); }
53
+ .dir.bad { color: var(--red); }
54
+ .dir.flat { color: var(--muted); }
55
+ .tag.good { color: var(--green); }
56
+ .tag.bad { color: var(--red); }
57
+ .tag.flat { color: var(--muted); }
58
+ .tag.na { color: var(--faint); }
59
+ svg { display: block; width: 100%; height: 40px; }
60
+ table { width: 100%; border-collapse: collapse; font-size: 13px; }
61
+ th, td { text-align: left; padding: 8px 10px; border-bottom: 1px solid var(--border); }
62
+ th { color: var(--muted); font-weight: 600; font-size: 12px; text-transform: uppercase; letter-spacing: 0.4px; }
63
+ td.num, th.num { text-align: right; font-family: var(--mono); }
64
+ .badge { font-size: 12px; font-weight: 600; padding: 2px 8px; border-radius: 6px; border: 1px solid var(--border); }
65
+ .b-approve { color: var(--green); border-color: rgba(52,211,153,0.4); }
66
+ .b-reject { color: var(--red); border-color: rgba(248,113,113,0.4); }
67
+ .empty { color: var(--muted); background: var(--panel); border: 1px solid var(--border); border-radius: 12px; padding: 24px; text-align: center; }
68
+ .empty code { font-family: var(--mono); color: var(--text); background: var(--panel-2); padding: 2px 6px; border-radius: 5px; }
69
+ .headline { background: var(--panel); border: 1px solid var(--border); border-radius: 12px; padding: 18px; font-size: 15px; }
70
+ .headline.good { border-color: rgba(52,211,153,0.4); }
71
+ .headline.bad { border-color: rgba(248,113,113,0.4); }
72
+ .mono { font-family: var(--mono); }
73
+ .muted { color: var(--muted); }
74
+ </style>
75
+ </head>
76
+ <body>
77
+ <div class="wrap">
78
+ <div class="head">
79
+ <h1>Trust Trajectory</h1>
80
+ <a href="/">Back to dashboard</a>
81
+ </div>
82
+ <p class="sub">Is the agent earning autonomy on THIS repo? Council pass-rate, gate pass-rate, iterations-to-completion, and human interventions over your run history. Real council and RARV-C data, never a fabricated trend.</p>
83
+ <div id="content"><p class="sub">Loading...</p></div>
84
+ </div>
85
+ <script>
86
+ (function () {
87
+ "use strict";
88
+ function esc(s) {
89
+ s = (s === null || s === undefined) ? "" : String(s);
90
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;")
91
+ .replace(/"/g, "&quot;").replace(/'/g, "&#39;");
92
+ }
93
+ function pct(n) {
94
+ if (n === null || n === undefined) return "n/a";
95
+ n = Number(n);
96
+ if (!isFinite(n)) return "n/a";
97
+ return (n * 100).toFixed(0) + "%";
98
+ }
99
+ function num(n) {
100
+ if (n === null || n === undefined) return "n/a";
101
+ n = Number(n);
102
+ if (!isFinite(n)) return "n/a";
103
+ return String(Math.round(n * 100) / 100);
104
+ }
105
+ function badgeClass(v) {
106
+ v = String(v || "").toUpperCase();
107
+ if (v.indexOf("APPROVE") === 0 || v.indexOf("COMPLETE") === 0 || v === "PASS" || v === "PASSED") return "b-approve";
108
+ if (v.indexOf("REJECT") === 0 || v.indexOf("BLOCK") === 0 || v === "FAIL") return "b-reject";
109
+ return "";
110
+ }
111
+ // Arrow glyphs only (no emoji): ^ up, v down, - flat.
112
+ function arrow(direction) {
113
+ if (direction === "up") return "^";
114
+ if (direction === "down") return "v";
115
+ return "-";
116
+ }
117
+ // Build an inline-SVG sparkline from a numeric series (nulls skipped).
118
+ function sparkline(values, higherIsBetter) {
119
+ var pts = [];
120
+ var idx = [];
121
+ for (var i = 0; i < values.length; i++) {
122
+ var v = values[i];
123
+ if (v === null || v === undefined || !isFinite(Number(v))) continue;
124
+ pts.push(Number(v)); idx.push(i);
125
+ }
126
+ if (pts.length === 0) return '<span class="muted" style="font-size:12px;">no data</span>';
127
+ var W = 200, H = 40, pad = 4;
128
+ var min = Math.min.apply(null, pts), max = Math.max.apply(null, pts);
129
+ var range = (max - min) || 1;
130
+ var coords = [];
131
+ for (var j = 0; j < pts.length; j++) {
132
+ var x = pts.length === 1 ? W / 2 : pad + (j / (pts.length - 1)) * (W - 2 * pad);
133
+ var y = H - pad - ((pts[j] - min) / range) * (H - 2 * pad);
134
+ coords.push(x.toFixed(1) + "," + y.toFixed(1));
135
+ }
136
+ // Color the line by whether the last value is better than the first.
137
+ var stroke = "#9aa1ad";
138
+ if (pts.length >= 2) {
139
+ var rising = pts[pts.length - 1] > pts[0];
140
+ var falling = pts[pts.length - 1] < pts[0];
141
+ if ((rising && higherIsBetter) || (falling && !higherIsBetter)) stroke = "#34d399";
142
+ else if ((rising && !higherIsBetter) || (falling && higherIsBetter)) stroke = "#f87171";
143
+ }
144
+ return '<svg viewBox="0 0 ' + W + ' ' + H + '" preserveAspectRatio="none">' +
145
+ '<polyline fill="none" stroke="' + stroke + '" stroke-width="2" points="' + coords.join(" ") + '"/>' +
146
+ '</svg>';
147
+ }
148
+
149
+ var AXIS_DESC = {
150
+ council_pass_rate: "Share of runs the 3-reviewer council approved",
151
+ gate_pass_rate: "Share of quality gates passed per run",
152
+ iterations: "RARV iterations needed to complete a run",
153
+ interventions: "Human interventions needed per run"
154
+ };
155
+
156
+ function renderAxis(key, ax, series) {
157
+ var label = ax.label || key;
158
+ var desc = AXIS_DESC[key] || "";
159
+ var values = series.map(function (s) { return s[key]; });
160
+ var spark = sparkline(values, !!ax.higher_is_better);
161
+ var verdict;
162
+ if (!ax.available) {
163
+ verdict = '<div class="dir flat">-</div><div class="tag na">no data</div>';
164
+ } else if (ax.insufficient) {
165
+ verdict = '<div class="dir flat">-</div><div class="tag na">need 2+ runs</div>';
166
+ } else {
167
+ var dir = ax.direction || "flat";
168
+ var dirClass = dir === "flat" ? "flat" : (ax.improving ? (dir === "up" ? "up" : "down") : "bad");
169
+ var tagClass = ax.improving === true ? "good" : (ax.improving === false ? "bad" : "flat");
170
+ var tagText = ax.improving === true ? "improving" : (ax.improving === false ? "regressing" : "stable");
171
+ var latestStr = ax.higher_is_better ? pct(ax.latest) : num(ax.latest);
172
+ verdict = '<div class="dir ' + dirClass + '">' + arrow(dir) + ' ' + esc(dir) + '</div>' +
173
+ '<div class="tag ' + tagClass + '">' + tagText + ' (now ' + esc(latestStr) + ')</div>';
174
+ }
175
+ return '<div class="axis">' +
176
+ '<div class="meta"><div class="name">' + esc(label) + '</div>' +
177
+ '<div class="desc">' + esc(desc) + (ax.higher_is_better ? " (higher is better)" : " (lower is better)") + '</div></div>' +
178
+ '<div class="spark">' + spark + '</div>' +
179
+ '<div class="verdict">' + verdict + '</div>' +
180
+ '</div>';
181
+ }
182
+
183
+ function renderRuns(series) {
184
+ var html = '<h2>Run history</h2>';
185
+ if (!series || series.length === 0) {
186
+ html += '<div class="empty">No completed runs yet. Trust trajectory comes' +
187
+ ' from proof-of-run artifacts (<code>.loki/proofs/</code>), written at' +
188
+ ' the end of each run.</div>';
189
+ return html;
190
+ }
191
+ var rows = "";
192
+ for (var i = 0; i < series.length; i++) {
193
+ var s = series[i];
194
+ var cp = s.council_pass_rate;
195
+ var verdict = (cp === 1) ? '<span class="badge b-approve">PASS</span>'
196
+ : (cp === 0 ? '<span class="badge b-reject">FAIL</span>' : '<span class="muted">-</span>');
197
+ rows += '<tr><td class="mono">' + esc(s.run_id) + '</td>' +
198
+ '<td class="muted">' + esc(s.generated_at || "") + '</td>' +
199
+ '<td>' + verdict + '</td>' +
200
+ '<td class="num">' + (s.gate_pass_rate === null || s.gate_pass_rate === undefined ? "-" : pct(s.gate_pass_rate)) + '</td>' +
201
+ '<td class="num">' + (s.iterations === null || s.iterations === undefined ? "-" : esc(s.iterations)) + '</td>' +
202
+ '<td class="num">' + (s.interventions === null || s.interventions === undefined ? "-" : esc(s.interventions)) + '</td></tr>';
203
+ }
204
+ html += '<table><thead><tr>' +
205
+ '<th>Run</th><th>When</th><th>Council</th><th class="num">Gates</th>' +
206
+ '<th class="num">Iters</th><th class="num">Interv.</th>' +
207
+ '</tr></thead><tbody>' + rows + '</tbody></table>';
208
+ return html;
209
+ }
210
+
211
+ function render(d) {
212
+ var c = document.getElementById("content");
213
+ if (d && d.available === false) {
214
+ c.innerHTML = '<div class="empty">Trust trajectory is unavailable in this' +
215
+ ' install. ' + esc((d.notes && d.notes[0]) || "") + '</div>';
216
+ return;
217
+ }
218
+ var series = d.series || [];
219
+ if (d.insufficient) {
220
+ var msg = '<div class="headline"><strong>Not enough history yet.</strong><br>' +
221
+ 'Trust trajectory needs 2 or more recorded runs to show a direction. ' +
222
+ esc(d.runs_count || 0) + ' run(s) recorded so far. Run <code>loki start</code> again' +
223
+ ' and the trend appears here, derived from real council and gate results.</div>';
224
+ c.innerHTML = msg + renderRuns(series);
225
+ return;
226
+ }
227
+ var axes = d.axes || {};
228
+ var imp = d.improving_count || 0, reg = d.regressing_count || 0;
229
+ var hClass = (imp && !reg) ? "good" : (reg && !imp ? "bad" : "");
230
+ var headline;
231
+ if (imp && !reg) headline = "Trending more trustworthy: " + imp + " axis improving, none regressing on this repo.";
232
+ else if (reg && !imp) headline = "Trust regressing: " + reg + " axis regressing. Review recent runs.";
233
+ else if (imp || reg) headline = "Mixed: " + imp + " improving, " + reg + " regressing.";
234
+ else headline = "Stable: no significant change across axes yet.";
235
+
236
+ var cards = '<div class="cards">' +
237
+ '<div class="card"><div class="label">Runs analyzed</div>' +
238
+ '<div class="val">' + esc(d.runs_count || 0) + '</div>' +
239
+ '<div class="note">from .loki/proofs/</div></div>' +
240
+ '<div class="card"><div class="label">Improving axes</div>' +
241
+ '<div class="val" style="color:var(--green);">' + esc(imp) + '</div>' +
242
+ '<div class="note">good direction</div></div>' +
243
+ '<div class="card"><div class="label">Regressing axes</div>' +
244
+ '<div class="val" style="color:' + (reg ? 'var(--red)' : 'var(--muted)') + ';">' + esc(reg) + '</div>' +
245
+ '<div class="note">needs attention</div></div>' +
246
+ '</div>';
247
+
248
+ var axesHtml = '<h2>Earned-autonomy signals</h2><div class="axes">';
249
+ var order = ["council_pass_rate", "gate_pass_rate", "iterations", "interventions"];
250
+ for (var k = 0; k < order.length; k++) {
251
+ if (axes[order[k]]) axesHtml += renderAxis(order[k], axes[order[k]], series);
252
+ }
253
+ axesHtml += '</div>';
254
+
255
+ c.innerHTML = cards +
256
+ '<div class="headline ' + hClass + '" style="margin-top:14px;">' + esc(headline) + '</div>' +
257
+ axesHtml +
258
+ renderRuns(series);
259
+ }
260
+ function renderError(msg) {
261
+ document.getElementById("content").innerHTML =
262
+ '<div class="empty">Could not load trust data. ' + esc(msg || "") + "</div>";
263
+ }
264
+ fetch("/api/trust/trajectory", { headers: { "Accept": "application/json" } })
265
+ .then(function (r) { if (!r.ok) throw new Error("HTTP " + r.status); return r.json(); })
266
+ .then(function (d) { render(d || {}); })
267
+ .catch(function (e) { renderError(e && e.message); });
268
+ })();
269
+ </script>
270
+ </body>
271
+ </html>
@@ -2,7 +2,7 @@
2
2
 
3
3
  The flagship product of [Autonomi](https://www.autonomi.dev/). Complete installation instructions for all platforms and use cases.
4
4
 
5
- **Version:** v7.15.0
5
+ **Version:** v7.16.1
6
6
 
7
7
  ---
8
8