loki-mode 7.129.5 → 8.0.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.
Files changed (75) hide show
  1. package/README.md +121 -9
  2. package/SKILL.md +66 -5
  3. package/VERSION +1 -1
  4. package/autonomy/app-runner.sh +37 -0
  5. package/autonomy/completion-council.sh +862 -28
  6. package/autonomy/council-v2.sh +39 -0
  7. package/autonomy/crash.sh +3 -2
  8. package/autonomy/grill.sh +42 -0
  9. package/autonomy/hooks/validate-bash.sh +301 -4
  10. package/autonomy/lib/cockpit-render.sh +8 -2
  11. package/autonomy/lib/cr-rematerialize.py +101 -4
  12. package/autonomy/lib/deadline.py +957 -0
  13. package/autonomy/lib/dependency-setup.sh +205 -0
  14. package/autonomy/lib/done-recognition.sh +45 -0
  15. package/autonomy/lib/efficiency_cost.py +13 -6
  16. package/autonomy/lib/no_mock_scan.py +793 -0
  17. package/autonomy/lib/prd-enrich.sh +42 -0
  18. package/autonomy/lib/proof-analytics-props.py +69 -0
  19. package/autonomy/lib/proof-generator.py +326 -92
  20. package/autonomy/lib/proof-template.html +15 -12
  21. package/autonomy/lib/proof-verify.py +169 -105
  22. package/autonomy/lib/requirements_contract.py +848 -0
  23. package/autonomy/lib/sdk-mode.sh +103 -0
  24. package/autonomy/lib/secret-scan.sh +139 -0
  25. package/autonomy/lib/spec-expand.sh +208 -0
  26. package/autonomy/lib/tree_digest.py +266 -0
  27. package/autonomy/lib/voter-agents.sh +58 -8
  28. package/autonomy/lib/workspace_diff.py +141 -0
  29. package/autonomy/loki +196 -17
  30. package/autonomy/playwright-verify.sh +501 -0
  31. package/autonomy/prd-checklist.sh +17 -8
  32. package/autonomy/provider-offer.sh +111 -0
  33. package/autonomy/run.sh +4587 -671
  34. package/autonomy/sandbox.sh +42 -0
  35. package/autonomy/spec-interrogation.sh +148 -5
  36. package/autonomy/spec.sh +118 -1
  37. package/autonomy/telemetry.sh +133 -7
  38. package/autonomy/verify.sh +107 -0
  39. package/bin/loki +110 -3
  40. package/completions/_loki +10 -0
  41. package/completions/loki.bash +1 -1
  42. package/dashboard/__init__.py +1 -1
  43. package/dashboard/audit.py +96 -7
  44. package/dashboard/build_supervisor.py +1619 -0
  45. package/dashboard/control.py +8 -0
  46. package/dashboard/prompt_optimizer.py +7 -0
  47. package/dashboard/server.py +577 -22
  48. package/dashboard/static/trust.html +1 -1
  49. package/docs/ARCHITECTURE-OVERVIEW.md +207 -0
  50. package/docs/AUTONOMI-ECOSYSTEM.md +107 -0
  51. package/docs/INSTALLATION.md +19 -2
  52. package/docs/MODEL-EQUIVALENCE-HARNESS-PLAN.md +70 -0
  53. package/docs/PLANS-INDEX.md +33 -0
  54. package/docs/PRIVACY.md +29 -1
  55. package/docs/SIGNED-RECEIPTS.md +102 -0
  56. package/docs/SONNET5-DEFAULT-PLAN.md +1 -1
  57. package/docs/V8-ACCEPTANCE-TRIAGE-2026-07-24.md +114 -0
  58. package/docs/V8-AGENT-SDK-PLAN.md +692 -0
  59. package/docs/V8-COMPLEXITY-AUDIT-2026-07-24.md +45 -0
  60. package/docs/V8-MAJOR-RELEASE-PLAN.md +137 -0
  61. package/docs/V8-OVERNIGHT-PLAN-2026-07-25.md +82 -0
  62. package/docs/V8-RUNTIME-TRUTH-2026-07-25.md +232 -0
  63. package/docs/V8-SDK-RESEARCH-RAW.md +129 -0
  64. package/docs/alternative-installations.md +4 -4
  65. package/loki-ts/dist/loki.js +674 -285
  66. package/loki-ts/package.json +2 -0
  67. package/mcp/__init__.py +1 -1
  68. package/package.json +7 -6
  69. package/plugins/loki-mode/.claude-plugin/plugin.json +1 -1
  70. package/providers/claude.sh +75 -0
  71. package/providers/codex.sh +85 -13
  72. package/references/sdk-mode.md +106 -0
  73. package/skills/model-selection.md +1 -1
  74. package/skills/providers.md +42 -0
  75. package/skills/quality-gates.md +22 -0
@@ -22,6 +22,7 @@ import hashlib
22
22
  import json
23
23
  import os
24
24
  import random
25
+ import re
25
26
  import string
26
27
  import subprocess
27
28
  import sys
@@ -37,6 +38,8 @@ if _HERE not in sys.path:
37
38
  import proof_redact # noqa: E402
38
39
  from efficiency_cost import collect_efficiency as _collect_efficiency # noqa: E402
39
40
  import effort_estimator # noqa: E402
41
+ from tree_digest import MANIFEST_VERSION, compute_tree_digest # noqa: E402
42
+ from workspace_diff import collect_workspace_diff # noqa: E402
40
43
 
41
44
 
42
45
  # ---------------------------------------------------------------------------
@@ -326,6 +329,38 @@ def _collect_quality_gates(loki_dir):
326
329
  {"name": "unit_tests", "status": _norm_gate_status(st)}
327
330
  )
328
331
  total += 1
332
+ # The iteration loop writes the authoritative unresolved-gate set here.
333
+ # These failures must be merged even when individual pass markers exist.
334
+ # Otherwise a run can stop on code_review while the receipt reports only
335
+ # static_analysis and unit_tests, producing a cryptographically valid but
336
+ # semantically false green receipt.
337
+ failure_path = os.path.join(loki_dir, "quality", "gate-failures.txt")
338
+ try:
339
+ with open(failure_path, "r", encoding="utf-8") as handle:
340
+ raw_failures = handle.read(8192)
341
+ except OSError:
342
+ raw_failures = ""
343
+ failed_names = []
344
+ for token in re.split(r"[,\s]+", raw_failures):
345
+ name = token.strip()
346
+ if not name or not re.fullmatch(r"[A-Za-z0-9_.:-]+", name):
347
+ continue
348
+ name = re.sub(r"_(PAUSED|ESCALATED)$", "", name,
349
+ flags=re.IGNORECASE)
350
+ if name not in failed_names:
351
+ failed_names.append(name)
352
+ if failed_names:
353
+ by_name = {str(g.get("name") or ""): g for g in gates}
354
+ for name in failed_names:
355
+ if name in by_name:
356
+ by_name[name]["status"] = "failed"
357
+ else:
358
+ gate = {"name": name, "status": "failed"}
359
+ gates.append(gate)
360
+ by_name[name] = gate
361
+
362
+ total = len(gates)
363
+ passed = sum(1 for gate in gates if gate.get("status") == "passed")
329
364
  return {"passed": passed, "total": total, "gates": gates}
330
365
 
331
366
 
@@ -378,6 +413,88 @@ def _collect_build(loki_dir):
378
413
  return out
379
414
 
380
415
 
416
+ def _collect_termination(loki_dir, session_exit_code=None):
417
+ """Read the supervised-process termination marker.
418
+
419
+ The marker is written before proof generation when INT or TERM ends a
420
+ supervised build. File presence is fail-closed: a truncated marker still
421
+ proves that the process did not complete normally.
422
+ """
423
+ path = os.path.join(loki_dir, "state", "termination.json")
424
+ out = {
425
+ "terminated": False,
426
+ "status": "completed",
427
+ "reason": "",
428
+ "signal": "",
429
+ "exit_code": None,
430
+ "outcome": "",
431
+ "run_status": "",
432
+ }
433
+ state_paths = [os.path.join(loki_dir, "autonomy-state.json")]
434
+ sessions_dir = os.path.join(loki_dir, "sessions")
435
+ try:
436
+ for entry in os.listdir(sessions_dir):
437
+ state_paths.append(
438
+ os.path.join(sessions_dir, entry, "autonomy-state.json")
439
+ )
440
+ except OSError:
441
+ pass
442
+ state_paths = [path for path in state_paths if os.path.isfile(path)]
443
+ if state_paths:
444
+ latest_state = max(state_paths, key=os.path.getmtime)
445
+ state = _read_json(latest_state, default=None)
446
+ if isinstance(state, dict):
447
+ out["run_status"] = str(state.get("status") or "").strip().lower()
448
+ completion = _read_json(
449
+ os.path.join(loki_dir, "state", "completion.json"), default=None
450
+ )
451
+ if isinstance(completion, dict):
452
+ outcome = str(completion.get("outcome") or "").strip().lower()
453
+ out["outcome"] = outcome
454
+ if outcome:
455
+ out["status"] = outcome
456
+ if outcome not in ("complete", "completed", "success"):
457
+ out["reason"] = outcome
458
+ if session_exit_code is not None:
459
+ out["exit_code"] = session_exit_code
460
+ if session_exit_code != 0 and not out["reason"]:
461
+ out["reason"] = "nonzero_session_exit"
462
+ if not os.path.exists(path):
463
+ return out
464
+ raw = _read_json(path, default=None)
465
+ out["terminated"] = True
466
+ out["status"] = "interrupted"
467
+ if not isinstance(raw, dict):
468
+ out["reason"] = "invalid_termination_record"
469
+ return out
470
+ out["reason"] = str(raw.get("reason") or "supervisor_signal")
471
+ out["signal"] = str(raw.get("signal") or "")
472
+ ec = raw.get("exit_code")
473
+ out["exit_code"] = _to_int(ec, None) if ec is not None else None
474
+ return out
475
+
476
+
477
+ def _collect_model(loki_dir, observed):
478
+ """Return the dispatched model when recorded, otherwise ``unavailable``."""
479
+ for value in (
480
+ observed,
481
+ os.environ.get("LOKI_CURRENT_MODEL"),
482
+ os.environ.get("LOKI_SESSION_MODEL"),
483
+ os.environ.get("SESSION_MODEL"),
484
+ ):
485
+ if str(value or "").strip():
486
+ return str(value).strip()
487
+ policy = _read_json(
488
+ os.path.join(loki_dir, "state", "execution-policy.json"), default={}
489
+ )
490
+ model = policy.get("model") if isinstance(policy, dict) else None
491
+ if isinstance(model, dict):
492
+ value = model.get("sdk_id") or model.get("alias")
493
+ if str(value or "").strip():
494
+ return str(value).strip()
495
+ return "unavailable"
496
+
497
+
381
498
  def _collect_security(loki_dir):
382
499
  """Read .loki/quality/security-findings.json (the secure-by-default gate).
383
500
 
@@ -584,6 +701,53 @@ def _collect_evidence_gate(loki_dir):
584
701
  return out
585
702
 
586
703
 
704
+ def _classify_func_axis(raw):
705
+ """Map one recorded functional axis {ok, inconclusive, reason} to an HONEST
706
+ tri-state. This is the trust-critical rule -- get it wrong and the receipt
707
+ fabricates a green:
708
+
709
+ - PROVEN iff ok is True AND NOT inconclusive (a fresh positive proof:
710
+ the record survived / the 401 was observed / the scan was clean).
711
+ - GAP iff ok is False AND NOT inconclusive (freshly disproven).
712
+ - NOT_CHECKED otherwise (inconclusive, or absent). Never a green, never a
713
+ gap -- it was not proven and it was not disproven.
714
+
715
+ inconclusive DOMINATES ok: an axis flagged inconclusive is never proven even
716
+ if ok happens to be true (the gate writes ok:true as its non-blocking default
717
+ when it could not run, so ok alone is not evidence)."""
718
+ if not isinstance(raw, dict):
719
+ return "not_checked", ""
720
+ reason = str(raw.get("reason") or "")
721
+ if raw.get("inconclusive") is True:
722
+ return "not_checked", reason
723
+ if raw.get("ok") is True:
724
+ return "proven", reason
725
+ if raw.get("ok") is False:
726
+ return "gap", reason
727
+ return "not_checked", reason
728
+
729
+
730
+ def _collect_functionality(loki_dir):
731
+ """Read the nomock/persistence/auth axes from evidence-gate-details.json and
732
+ surface each as an HONEST proof fact. Deterministic + re-derivable: the values
733
+ come straight from the recorded axes, no LLM opinion.
734
+
735
+ Shape (per axis): {state: proven|gap|not_checked, reason}. Only `proven` is a
736
+ green receipt row; `gap` is an honest disproven row (lands in degraded[]);
737
+ `not_checked` is omitted from the receipt's green rows entirely. Absent file
738
+ or absent axis -> not_checked (the gate did not record it -> nothing proven)."""
739
+ raw = _read_json(
740
+ os.path.join(loki_dir, "council", "evidence-gate-details.json"),
741
+ default=None,
742
+ )
743
+ axes = raw if isinstance(raw, dict) else {}
744
+ out = {}
745
+ for axis in ("nomock", "persistence", "auth", "authorization"):
746
+ state, reason = _classify_func_axis(axes.get(axis))
747
+ out[axis] = {"state": state, "reason": reason}
748
+ return out
749
+
750
+
587
751
  def _diff_sha256(files_changed):
588
752
  """sha256 of the canonical diff stat (count/insertions/deletions/files).
589
753
 
@@ -602,93 +766,58 @@ def _diff_sha256(files_changed):
602
766
 
603
767
 
604
768
  def _git_diffstat(target_dir, include_diffs):
605
- """Return (files_changed dict, diffs list|None).
606
-
607
- base = $_LOKI_ITER_START_SHA, else HEAD~1. Non-git -> empty.
769
+ """Return the final worktree diff relative to the RUN's base.
770
+
771
+ The receipt is a run-level document, so its diff stat must span the WHOLE
772
+ run. This previously passed _LOKI_ITER_START_SHA -- the per-ITERATION
773
+ baseline -- so a multi-iteration run attested only to the changes since its
774
+ last iteration. Measured on a real 2-commit run: 850 insertions + 76
775
+ deletions reported where the run actually produced 1300 insertions.
776
+
777
+ That matters more than an ordinary display bug: the returned object flows
778
+ into _diff_sha256, the receipt's integrity hash, which is written on EVERY
779
+ run. A verifier recomputing it therefore attests to the understated stat,
780
+ and the receipt is exactly the artifact users are told to trust. (Detached
781
+ GPG signing is a separate opt-in layer gated on LOKI_PROOF_GPG_KEY and
782
+ default OFF; when enabled it signs these same bytes, but the integrity hash
783
+ is the always-on path.)
784
+
785
+ Order of preference:
786
+ 1. _LOKI_RUN_START_SHA -- the run's own baseline (run.sh exports it).
787
+ 2. The empty tree -- correct for a GREENFIELD run (a repo with no commits
788
+ at start), where "everything that now exists" IS the run's output and
789
+ there is no earlier commit to diff against.
790
+ 3. Empty string -- let workspace_diff apply its own fallbacks.
608
791
  """
609
- empty = {"count": 0, "insertions": 0, "deletions": 0, "files": []}
610
-
611
- def _git(args):
612
- try:
613
- out = subprocess.run(
614
- ["git", "-C", target_dir] + args,
615
- capture_output=True, text=True, timeout=30,
616
- )
617
- if out.returncode != 0:
618
- return None
619
- return out.stdout
620
- except Exception:
621
- return None
622
-
623
- # Confirm we are in a git repo.
624
- if _git(["rev-parse", "--is-inside-work-tree"]) is None:
625
- return empty, (None if not include_diffs else None)
626
-
627
- base = os.environ.get("_LOKI_ITER_START_SHA", "").strip()
792
+ base = os.environ.get("_LOKI_RUN_START_SHA", "").strip()
628
793
  if not base:
629
- base = "HEAD~1"
630
-
631
- numstat = _git(["diff", "--numstat", base, "HEAD"])
632
- if numstat is None:
633
- # base may be invalid (shallow / first commit); fall back to HEAD only.
634
- numstat = _git(["diff", "--numstat", "HEAD"])
635
- if numstat is None:
636
- return empty, (None if not include_diffs else None)
637
-
638
- files = []
639
- ins_total = 0
640
- del_total = 0
641
- for line in numstat.splitlines():
642
- parts = line.split("\t")
643
- if len(parts) < 3:
644
- continue
645
- ins_s, del_s, path = parts[0], parts[1], parts[2]
646
- ins = _to_int(ins_s) if ins_s != "-" else 0
647
- dele = _to_int(del_s) if del_s != "-" else 0
648
- ins_total += ins
649
- del_total += dele
650
- files.append({
651
- "path": path,
652
- "insertions": ins,
653
- "deletions": dele,
654
- "status": "binary" if ins_s == "-" else "modified",
655
- })
794
+ # Greenfield: no baseline commit existed when the run started.
795
+ base = _empty_tree_sha(target_dir)
796
+ files_changed, diffs = collect_workspace_diff(target_dir, base, include_diffs)
797
+ # Return the base too. git_facts records base_sha NEXT TO diff and
798
+ # diff_sha256; if they disagree the signed receipt is internally
799
+ # inconsistent and a verifier recomputing the diff from base_sha gets a
800
+ # different answer than the one that was signed.
801
+ return files_changed, diffs, base
656
802
 
657
- files_changed = {
658
- "count": len(files),
659
- "insertions": ins_total,
660
- "deletions": del_total,
661
- "files": files,
662
- }
663
803
 
664
- diffs = None
665
- if include_diffs:
666
- diffs = []
667
- patch = _git(["diff", base, "HEAD"])
668
- if patch is None:
669
- patch = _git(["diff", "HEAD"])
670
- if patch:
671
- # Split per file on the diff --git markers, preserving the header.
672
- chunks = []
673
- current = []
674
- for line in patch.splitlines(keepends=True):
675
- if line.startswith("diff --git ") and current:
676
- chunks.append("".join(current))
677
- current = [line]
678
- else:
679
- current.append(line)
680
- if current:
681
- chunks.append("".join(current))
682
- for chunk in chunks:
683
- # Best-effort path extraction from the "diff --git a/x b/x" line.
684
- p = ""
685
- first = chunk.splitlines()[0] if chunk else ""
686
- bits = first.split(" b/")
687
- if len(bits) == 2:
688
- p = bits[1].strip()
689
- diffs.append({"path": p, "patch": chunk})
804
+ def _empty_tree_sha(repo_dir):
805
+ """The canonical empty-tree object, or "" when git is unavailable.
690
806
 
691
- return files_changed, diffs
807
+ Diffing against this yields "everything that currently exists", which is the
808
+ truthful baseline for a run that started from a repo with no commits.
809
+ """
810
+ try:
811
+ out = subprocess.run(
812
+ ["git", "hash-object", "-t", "tree", os.devnull],
813
+ cwd=repo_dir,
814
+ capture_output=True,
815
+ text=True,
816
+ timeout=10,
817
+ )
818
+ except Exception:
819
+ return ""
820
+ return out.stdout.strip() if out.returncode == 0 else ""
692
821
 
693
822
 
694
823
  def _collect_iterations(loki_dir):
@@ -823,20 +952,22 @@ def _build_proof(args, loki_dir, target_dir, repo_root):
823
952
 
824
953
  cost, model_from_eff = _collect_efficiency(loki_dir)
825
954
  provider_name = args.provider or os.environ.get("PROVIDER_NAME") or "claude"
826
- model = model_from_eff or os.environ.get("SESSION_MODEL") or ""
955
+ model = _collect_model(loki_dir, model_from_eff)
827
956
 
828
- files_changed, diffs = _git_diffstat(target_dir, args.include_diffs)
957
+ files_changed, diffs, diff_base_sha = _git_diffstat(target_dir, args.include_diffs)
829
958
  iterations = _collect_iterations(loki_dir)
830
959
  spec = _collect_spec(loki_dir, target_dir)
831
960
  council = _collect_council(loki_dir)
832
961
  quality_gates = _collect_quality_gates(loki_dir)
833
962
 
834
963
  build = _collect_build(loki_dir)
964
+ termination = _collect_termination(loki_dir, args.session_exit_code)
835
965
  tests = _collect_tests(loki_dir)
836
966
  security = _collect_security(loki_dir)
837
967
  functional = _collect_functional(loki_dir) # FV-2 record-half: descriptive only
838
968
  healthcheck = _collect_healthcheck(loki_dir) # Evidence Receipt record-half
839
969
  evidence_gate = _collect_evidence_gate(loki_dir)
970
+ functionality = _collect_functionality(loki_dir) # func axes as HONEST facts
840
971
 
841
972
  deployed_url = os.environ.get("LOKI_DEPLOYED_URL") or None
842
973
 
@@ -859,13 +990,19 @@ def _build_proof(args, loki_dir, target_dir, repo_root):
859
990
  # recompute the hash. True non-forgeability requires the neutral signed record
860
991
  # (service-held key). See proof-verify.py verify() docstring.
861
992
  git_facts = {
862
- "base_sha": os.environ.get("_LOKI_ITER_START_SHA", "").strip(),
993
+ # Must be the SAME base the diff above was computed against.
994
+ "base_sha": diff_base_sha,
863
995
  "head_sha": _git_head_sha(target_dir),
864
996
  "diff": files_changed,
865
997
  "diff_sha256": _diff_sha256(files_changed),
998
+ # Exact source snapshot verified by this receipt. Dashboard supervisors
999
+ # recompute it after runner exit to reject proofs followed by code edits.
1000
+ "tree_sha256": compute_tree_digest(target_dir),
1001
+ "tree_manifest_version": MANIFEST_VERSION,
866
1002
  }
867
1003
  facts = {
868
1004
  "git": git_facts,
1005
+ "execution": termination,
869
1006
  "build": build,
870
1007
  "tests": tests,
871
1008
  "quality_gates": [
@@ -878,6 +1015,14 @@ def _build_proof(args, loki_dir, target_dir, repo_root):
878
1015
  # _compute_degraded, so it does not (yet) change the verdict. Wiring it into
879
1016
  # the green headline is the founder-gated trust-semantics decision.
880
1017
  "functional": functional,
1018
+ # Functionality-proving axes (nomock / persistence / auth), surfaced as
1019
+ # HONEST facts straight from the recorded evidence-gate axes. Each is
1020
+ # {state: proven|gap|not_checked, reason}. ONLY `proven` (a fresh ok:true)
1021
+ # is a green receipt row; `not_checked` (inconclusive/absent) is never a
1022
+ # green and never a gap; `gap` (a fresh ok:false) is a disproven row and
1023
+ # ALSO lands in degraded[] (below), so it forces VERIFIED WITH GAPS and can
1024
+ # never hide behind a green headline. Deterministic + re-derivable.
1025
+ "functionality": functionality,
881
1026
  # Evidence Receipt (record half): did the built app come up + respond?
882
1027
  # Descriptive; NOT read by _compute_headline (gating is founder-gated).
883
1028
  "healthcheck": healthcheck,
@@ -966,6 +1111,9 @@ def _build_proof(args, loki_dir, target_dir, repo_root):
966
1111
  "quality_gates": quality_gates,
967
1112
  "cost": cost,
968
1113
  "deployment": deployment,
1114
+ # Typed compatibility mirror for consumers that do not traverse facts.
1115
+ "tree_sha256": git_facts["tree_sha256"],
1116
+ "tree_manifest_version": git_facts["tree_manifest_version"],
969
1117
  # Rank 6: work-based engineering-hours estimate (top-level, NOT a fact).
970
1118
  "effort_estimate": effort_estimate,
971
1119
  # v1.1 evidence model (additive).
@@ -988,6 +1136,28 @@ def _compute_degraded(facts):
988
1136
  # must SHOW it (otherwise a failed test would render an amber banner whose
989
1137
  # "items below" list is empty -- the exact misleading state we forbid).
990
1138
  weak = ("not_run", "inconclusive", "skipped", "failed")
1139
+ execution = facts.get("execution") or {}
1140
+ if execution.get("terminated"):
1141
+ signal_name = execution.get("signal") or "unknown"
1142
+ reason = execution.get("reason") or "interrupted"
1143
+ out.append({"item": "execution", "status": "failed",
1144
+ "reason": "%s (%s)" % (reason, signal_name)})
1145
+ else:
1146
+ outcome = str(execution.get("outcome") or "").lower()
1147
+ run_status = str(execution.get("run_status") or "").lower()
1148
+ if execution.get("exit_code") not in (None, 0):
1149
+ out.append({"item": "execution", "status": "failed",
1150
+ "reason": "exit_code=%s" % execution.get("exit_code")})
1151
+ elif outcome and outcome not in ("complete", "completed", "success"):
1152
+ out.append({"item": "execution", "status": "failed",
1153
+ "reason": outcome})
1154
+ elif run_status in {
1155
+ "failed", "force_stopped", "inconclusive_spec_contradiction",
1156
+ "interrupted", "max_iterations_reached", "max_retries_exceeded",
1157
+ "paused", "policy_blocked", "provider_deadline_partial_mutation",
1158
+ }:
1159
+ out.append({"item": "execution", "status": "failed",
1160
+ "reason": run_status})
991
1161
  tests = facts.get("tests") or {}
992
1162
  if tests.get("status") in weak:
993
1163
  reason = "no test command recorded" if not tests.get("command") \
@@ -1018,6 +1188,19 @@ def _compute_degraded(facts):
1018
1188
  if not (git.get("diff") or {}).get("count"):
1019
1189
  out.append({"item": "git.diff", "status": "not_run",
1020
1190
  "reason": "no file changes detected"})
1191
+ # Functionality axes: ONLY a freshly-disproven axis (state == gap, i.e. the
1192
+ # gate ran and the axis FAILED -- a record did not survive, auth was NOT
1193
+ # enforced, the diff shipped mock data) is a gap in the proof of done. A
1194
+ # `not_checked` axis (inconclusive / not attempted) is deliberately NOT a gap:
1195
+ # the honesty rule is that not-proven is not the same as disproven, and the
1196
+ # gate already passes those through. Surfacing them here would spam the ledger
1197
+ # with "we didn't check X" for every axis the driver could not exercise.
1198
+ fnc = facts.get("functionality") or {}
1199
+ for axis in ("nomock", "persistence", "auth", "authorization"):
1200
+ rec = fnc.get(axis) or {}
1201
+ if rec.get("state") == "gap":
1202
+ out.append({"item": "functionality:%s" % axis, "status": "failed",
1203
+ "reason": rec.get("reason") or "axis disproven"})
1021
1204
  return out
1022
1205
 
1023
1206
 
@@ -1057,8 +1240,31 @@ def _compute_headline(facts, degraded):
1057
1240
  and fn.get("ran")
1058
1241
  and fn.get("functional_status") == "failed"
1059
1242
  )
1243
+ execution = facts.get("execution") or {}
1244
+ execution_outcome = str(execution.get("outcome") or "").lower()
1245
+ failed_run_statuses = {
1246
+ "failed",
1247
+ "force_stopped",
1248
+ "inconclusive_spec_contradiction",
1249
+ "interrupted",
1250
+ "max_iterations_reached",
1251
+ "max_retries_exceeded",
1252
+ "paused",
1253
+ "policy_blocked",
1254
+ "provider_deadline_partial_mutation",
1255
+ }
1256
+ execution_failed = bool(
1257
+ execution.get("terminated")
1258
+ or execution.get("exit_code") not in (None, 0)
1259
+ or str(execution.get("run_status") or "").lower() in failed_run_statuses
1260
+ or (
1261
+ execution_outcome
1262
+ and execution_outcome not in ("complete", "completed", "success")
1263
+ )
1264
+ )
1060
1265
  any_failed = (
1061
- tests.get("status") == "failed"
1266
+ execution_failed
1267
+ or tests.get("status") == "failed"
1062
1268
  or build.get("status") == "failed"
1063
1269
  or any(g.get("status") == "failed"
1064
1270
  for g in (facts.get("quality_gates") or []))
@@ -1165,6 +1371,15 @@ def _build_social_hook(proof):
1165
1371
  return " - ".join(parts)
1166
1372
 
1167
1373
 
1374
+ def _receipt_title(proof):
1375
+ """Conservative user-facing verdict label for the rendered receipt."""
1376
+ headline = str((proof.get("honesty") or {}).get("headline") or "").upper()
1377
+ return {
1378
+ "VERIFIED": "Recorded checks passed",
1379
+ "VERIFIED WITH GAPS": "Checks completed with gaps",
1380
+ }.get(headline, "Not verified")
1381
+
1382
+
1168
1383
  def _attr_esc(s):
1169
1384
  """HTML-attribute-escape a string destined for content="...".`"""
1170
1385
  return (str(s).replace("&", "&amp;").replace('"', "&quot;")
@@ -1213,10 +1428,14 @@ def _render_fallback_html(proof):
1213
1428
  rows.append("<ul>")
1214
1429
  rows.append("<li>Cost (USD): %s</li>" % (
1215
1430
  esc(usd_disp) if usd_disp is not None else "not recorded for this run"))
1216
- rows.append("<li>Input tokens: %s</li>" % esc(cost.get("input_tokens", 0)))
1217
- rows.append("<li>Output tokens: %s</li>" % esc(cost.get("output_tokens", 0)))
1218
- rows.append("<li>Cache read tokens: %s</li>" % esc(cost.get("cache_read_tokens", 0)))
1219
- rows.append("<li>Cache creation tokens: %s</li>" % esc(cost.get("cache_creation_tokens", 0)))
1431
+ def token_value(key):
1432
+ value = cost.get(key)
1433
+ return value if value is not None else "not recorded"
1434
+
1435
+ rows.append("<li>Input tokens: %s</li>" % esc(token_value("input_tokens")))
1436
+ rows.append("<li>Output tokens: %s</li>" % esc(token_value("output_tokens")))
1437
+ rows.append("<li>Cache read tokens: %s</li>" % esc(token_value("cache_read_tokens")))
1438
+ rows.append("<li>Cache creation tokens: %s</li>" % esc(token_value("cache_creation_tokens")))
1220
1439
  rows.append("<li>Wall clock (sec): %s</li>" % esc(proof.get("wall_clock_sec", 0)))
1221
1440
  rows.append("</ul>")
1222
1441
 
@@ -1269,6 +1488,9 @@ def _render_html(proof, repo_root):
1269
1488
  tpl = _read_text(template_path, default="")
1270
1489
  marker = "__PROOF_JSON__"
1271
1490
  if tpl and marker in tpl:
1491
+ receipt_title = _receipt_title(proof)
1492
+ tpl = tpl.replace("__PROOF_RECEIPT_TITLE__", _attr_esc(receipt_title))
1493
+ tpl = tpl.replace("__PROOF_RECEIPT_TITLE_JSON__", json.dumps(receipt_title))
1272
1494
  # Substitute the dynamic social hook BEFORE the JSON payload, so a proof
1273
1495
  # value that happens to contain the hook token cannot get clobbered.
1274
1496
  # The hook embeds the real measured cost + files-changed + council ratio
@@ -1406,7 +1628,14 @@ def generate(args):
1406
1628
  return out_dir
1407
1629
 
1408
1630
 
1409
- def main(argv=None):
1631
+ def build_parser():
1632
+ """The generator's CLI parser.
1633
+
1634
+ Split out of main() so callers (notably the tests, which drive generate()
1635
+ directly with a synthetic args object) can obtain every argument at its
1636
+ declared default instead of hand-listing fields that go stale whenever a new
1637
+ argument lands here.
1638
+ """
1410
1639
  parser = argparse.ArgumentParser(description="Loki Mode proof-of-run generator")
1411
1640
  parser.add_argument("--loki-dir", default=".loki")
1412
1641
  parser.add_argument("--out-dir", default="")
@@ -1414,8 +1643,13 @@ def main(argv=None):
1414
1643
  parser.add_argument("--run-id", default="")
1415
1644
  parser.add_argument("--loki-version", default="")
1416
1645
  parser.add_argument("--provider", default="")
1646
+ parser.add_argument("--session-exit-code", type=int, default=None)
1417
1647
  parser.add_argument("--quiet", action="store_true")
1418
- args = parser.parse_args(argv)
1648
+ return parser
1649
+
1650
+
1651
+ def main(argv=None):
1652
+ args = build_parser().parse_args(argv)
1419
1653
 
1420
1654
  try:
1421
1655
  generate(args)
@@ -20,9 +20,8 @@
20
20
  the literal token anywhere else (this comment deliberately spells it out
21
21
  instead of repeating it, and the renderer detects an un-substituted
22
22
  blob structurally rather than by string-matching the token).
23
- 4. No other token substitution is required. All rendering happens client
24
- side from the parsed blob. This keeps Bun and bash routes at exact
25
- parity (one string replace, one template).
23
+ 4. The generator also substitutes the receipt title, social description,
24
+ and share-button setting before embedding the parsed blob.
26
25
 
27
26
  The renderer below is defensive: every nullable / optional schema field is
28
27
  guarded (deployed_url, public_url, diffs, council.enabled false, empty
@@ -42,10 +41,10 @@
42
41
  <head>
43
42
  <meta charset="utf-8">
44
43
  <meta name="viewport" content="width=device-width, initial-scale=1">
45
- <title>Loki Mode - Proof of Run</title>
44
+ <title>__PROOF_RECEIPT_TITLE__ - Loki Mode Evidence Receipt</title>
46
45
  <!-- Static text-only social meta. No og:image (would break self-containment). -->
47
46
  <meta property="og:type" content="website">
48
- <meta property="og:title" content="Built and verified autonomously by Loki Mode">
47
+ <meta property="og:title" content="__PROOF_RECEIPT_TITLE__ - Loki Mode Evidence Receipt">
49
48
  <!-- og:description and twitter:description are filled at generation time with
50
49
  the real measured cost + files-changed + council ratio (cost-free variant
51
50
  when cost was not collected). The generator replaces the og token below in
@@ -589,7 +588,7 @@
589
588
  function honestyVariant(headline) {
590
589
  var h = String(headline || "").toUpperCase();
591
590
  if (h === "VERIFIED") return { cls: "is-verified", mark: "OK",
592
- sub: "Tests verified, a real diff exists, and nothing was left unverified. Every claim below is deterministic and re-checkable." };
591
+ sub: "The recorded tests passed and this receipt contains a non-empty source change. Review the evidence below for the exact scope." };
593
592
  if (h.indexOf("GAPS") !== -1) return { cls: "is-gaps", mark: "!",
594
593
  sub: "Some evidence is verified, but the items listed below were not. A gap is never hidden: read them before trusting this run." };
595
594
  return { cls: "is-failed", mark: "X",
@@ -776,7 +775,7 @@
776
775
  // fmtUsd: a skeptic seeing a zero-dollar total assumes the artifact is fake.
777
776
  var usdRaw = g(p, "cost.usd", null);
778
777
  var fc = num(g(p, "files_changed.count", 0));
779
- var parts = ["Built and verified autonomously"];
778
+ var parts = [__PROOF_RECEIPT_TITLE_JSON__];
780
779
  if (usdRaw !== null) parts.push(fmtUsd(usdRaw));
781
780
  else parts.push("cost not recorded");
782
781
  parts.push(fc + " file" + (fc === 1 ? "" : "s") + " changed");
@@ -929,11 +928,15 @@
929
928
  function renderCost(p) {
930
929
  var card = document.getElementById("costCard");
931
930
  var usdRaw = g(p, "cost.usd", null);
931
+ function tokenValue(path) {
932
+ var raw = g(p, path, null);
933
+ return raw === null ? "not recorded" : fmtInt(raw);
934
+ }
932
935
  var cells = [
933
- ["Input tokens", fmtInt(g(p, "cost.input_tokens", 0))],
934
- ["Output tokens", fmtInt(g(p, "cost.output_tokens", 0))],
935
- ["Cache read", fmtInt(g(p, "cost.cache_read_tokens", 0))],
936
- ["Cache write", fmtInt(g(p, "cost.cache_creation_tokens", 0))]
936
+ ["Input tokens", tokenValue("cost.input_tokens")],
937
+ ["Output tokens", tokenValue("cost.output_tokens")],
938
+ ["Cache read", tokenValue("cost.cache_read_tokens")],
939
+ ["Cache write", tokenValue("cost.cache_creation_tokens")]
937
940
  ];
938
941
  var grid = "";
939
942
  for (var i = 0; i < cells.length; i++) {
@@ -1249,7 +1252,7 @@
1249
1252
  function shareHook() {
1250
1253
  var m = document.querySelector('meta[property="og:description"]');
1251
1254
  var c = m ? (m.getAttribute("content") || "") : "";
1252
- return c || "Built and verified autonomously by Loki Mode";
1255
+ return c || (__PROOF_RECEIPT_TITLE_JSON__ + " - Loki Mode Evidence Receipt");
1253
1256
  }
1254
1257
  document.addEventListener("click", function (e) {
1255
1258
  var t = e.target && e.target.closest ? e.target.closest("[data-share]") : null;