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.
- package/README.md +121 -9
- package/SKILL.md +66 -5
- package/VERSION +1 -1
- package/autonomy/app-runner.sh +37 -0
- package/autonomy/completion-council.sh +862 -28
- package/autonomy/council-v2.sh +39 -0
- package/autonomy/crash.sh +3 -2
- package/autonomy/grill.sh +42 -0
- package/autonomy/hooks/validate-bash.sh +301 -4
- package/autonomy/lib/cockpit-render.sh +8 -2
- package/autonomy/lib/cr-rematerialize.py +101 -4
- package/autonomy/lib/deadline.py +957 -0
- package/autonomy/lib/dependency-setup.sh +205 -0
- package/autonomy/lib/done-recognition.sh +45 -0
- package/autonomy/lib/efficiency_cost.py +13 -6
- package/autonomy/lib/no_mock_scan.py +793 -0
- package/autonomy/lib/prd-enrich.sh +42 -0
- package/autonomy/lib/proof-analytics-props.py +69 -0
- package/autonomy/lib/proof-generator.py +326 -92
- package/autonomy/lib/proof-template.html +15 -12
- package/autonomy/lib/proof-verify.py +169 -105
- package/autonomy/lib/requirements_contract.py +848 -0
- package/autonomy/lib/sdk-mode.sh +103 -0
- package/autonomy/lib/secret-scan.sh +139 -0
- package/autonomy/lib/spec-expand.sh +208 -0
- package/autonomy/lib/tree_digest.py +266 -0
- package/autonomy/lib/voter-agents.sh +58 -8
- package/autonomy/lib/workspace_diff.py +141 -0
- package/autonomy/loki +196 -17
- package/autonomy/playwright-verify.sh +501 -0
- package/autonomy/prd-checklist.sh +17 -8
- package/autonomy/provider-offer.sh +111 -0
- package/autonomy/run.sh +4587 -671
- package/autonomy/sandbox.sh +42 -0
- package/autonomy/spec-interrogation.sh +148 -5
- package/autonomy/spec.sh +118 -1
- package/autonomy/telemetry.sh +133 -7
- package/autonomy/verify.sh +107 -0
- package/bin/loki +110 -3
- package/completions/_loki +10 -0
- package/completions/loki.bash +1 -1
- package/dashboard/__init__.py +1 -1
- package/dashboard/audit.py +96 -7
- package/dashboard/build_supervisor.py +1619 -0
- package/dashboard/control.py +8 -0
- package/dashboard/prompt_optimizer.py +7 -0
- package/dashboard/server.py +577 -22
- package/dashboard/static/trust.html +1 -1
- package/docs/ARCHITECTURE-OVERVIEW.md +207 -0
- package/docs/AUTONOMI-ECOSYSTEM.md +107 -0
- package/docs/INSTALLATION.md +19 -2
- package/docs/MODEL-EQUIVALENCE-HARNESS-PLAN.md +70 -0
- package/docs/PLANS-INDEX.md +33 -0
- package/docs/PRIVACY.md +29 -1
- package/docs/SIGNED-RECEIPTS.md +102 -0
- package/docs/SONNET5-DEFAULT-PLAN.md +1 -1
- package/docs/V8-ACCEPTANCE-TRIAGE-2026-07-24.md +114 -0
- package/docs/V8-AGENT-SDK-PLAN.md +692 -0
- package/docs/V8-COMPLEXITY-AUDIT-2026-07-24.md +45 -0
- package/docs/V8-MAJOR-RELEASE-PLAN.md +137 -0
- package/docs/V8-OVERNIGHT-PLAN-2026-07-25.md +82 -0
- package/docs/V8-RUNTIME-TRUTH-2026-07-25.md +232 -0
- package/docs/V8-SDK-RESEARCH-RAW.md +129 -0
- package/docs/alternative-installations.md +4 -4
- package/loki-ts/dist/loki.js +674 -285
- package/loki-ts/package.json +2 -0
- package/mcp/__init__.py +1 -1
- package/package.json +7 -6
- package/plugins/loki-mode/.claude-plugin/plugin.json +1 -1
- package/providers/claude.sh +75 -0
- package/providers/codex.sh +85 -13
- package/references/sdk-mode.md +106 -0
- package/skills/model-selection.md +1 -1
- package/skills/providers.md +42 -0
- 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
|
|
606
|
-
|
|
607
|
-
|
|
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
|
-
|
|
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
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
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
|
-
|
|
665
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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("&", "&").replace('"', """)
|
|
@@ -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
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
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
|
|
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
|
-
|
|
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.
|
|
24
|
-
|
|
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
|
|
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="
|
|
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: "
|
|
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 = [
|
|
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",
|
|
934
|
-
["Output tokens",
|
|
935
|
-
["Cache read",
|
|
936
|
-
["Cache write",
|
|
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 ||
|
|
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;
|