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/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 Reserved for hosted publishing (coming in R9)"
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
@@ -7,7 +7,7 @@ Modules:
7
7
  control: Session control API (start/stop/pause/resume)
8
8
  """
9
9
 
10
- __version__ = "7.14.0"
10
+ __version__ = "7.16.0"
11
11
 
12
12
  # Expose the control app for easy import
13
13
  try:
@@ -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