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
@@ -30,10 +30,11 @@ Three checks (mirrors dashboard/audit.py verify-CLI style):
30
30
  ensure_ascii setting), sha256, compare to the recorded verification.hash.
31
31
  Any mismatch means the JSON was edited after signing.
32
32
 
33
- 2. DRIFT CHECK (diff_drift): from the recorded git base sha, re-run
34
- `git diff --shortstat <base> <head>` in the repo and compare the file /
35
- insertion / deletion counts (and diff_sha256 when present) to what the
36
- receipt recorded. A mismatch means the repo no longer matches the receipt.
33
+ 2. DRIFT CHECK (diff_drift): from the recorded git base sha, re-derive the
34
+ final committed, staged, unstaged, deleted, and untracked workspace diff.
35
+ Compare its stat and diff_sha256 to the receipt. When tree_sha256 is
36
+ present, also bind exact final file content so equal line counts cannot
37
+ hide content drift.
37
38
 
38
39
  3. GPG (gpg_ok): if a detached signature is present and gpg is available,
39
40
  verify it over the canonical bytes. Otherwise "n/a".
@@ -67,6 +68,13 @@ import os
67
68
  import subprocess
68
69
  import sys
69
70
 
71
+ _HERE = os.path.dirname(os.path.abspath(__file__))
72
+ if _HERE not in sys.path:
73
+ sys.path.insert(0, _HERE)
74
+
75
+ from tree_digest import compute_tree_digest
76
+ from workspace_diff import collect_workspace_diff
77
+
70
78
 
71
79
  # ---------------------------------------------------------------------------
72
80
  # canonicalization (MUST match proof-generator._canonical exactly)
@@ -105,50 +113,36 @@ def _is_git_repo(repo_dir):
105
113
 
106
114
 
107
115
  def _rev_resolvable(repo_dir, ref):
108
- """True iff `ref` resolves to a commit in repo_dir."""
116
+ """True iff `ref` names something in repo_dir that a diff can be taken from.
117
+
118
+ Accepts a COMMIT or a TREE. The tree case is load-bearing: a greenfield run
119
+ starts in a repo with no commits, so the generator records the EMPTY TREE
120
+ (4b825dc6...) as its baseline -- "everything that now exists". That object
121
+ is a tree, not a commit, so a commit-only check rejects it and the verifier
122
+ reports "base ref unresolvable" for a perfectly genuine proof.
123
+
124
+ `git diff <empty-tree>` works fine, so the diff really is re-derivable; only
125
+ the resolvability probe was too narrow. This does NOT loosen the forgery
126
+ defense -- an unknown or fabricated SHA still resolves as neither and is
127
+ still rejected.
128
+ """
109
129
  if not ref:
110
130
  return False
111
- out = _git(repo_dir, ["rev-parse", "--verify", "--quiet", str(ref) + "^{commit}"])
112
- return bool(out and out.strip())
131
+ for kind in ("commit", "tree"):
132
+ out = _git(repo_dir, ["rev-parse", "--verify", "--quiet", "%s^{%s}" % (ref, kind)])
133
+ if out and out.strip():
134
+ return True
135
+ return False
113
136
 
114
137
 
115
138
  def _numstat(repo_dir, base, head):
116
- """Return {count, insertions, deletions, files} for
117
- `git diff --numstat base head`, or None if the diff could not be computed.
118
-
119
- The `files` list mirrors proof-generator._git_diffstat EXACTLY (path /
120
- insertions / deletions / status) so that hashing the canonical stat here
121
- reproduces the generator's diff_sha256. (Earlier this hashed the full patch
122
- text while the generator hashed the stat object -- so every untampered v1.1
123
- proof falsely reported drift. BUG-DIFFSHA.)"""
124
- raw = _git(repo_dir, ["diff", "--numstat", str(base), str(head)])
125
- if raw is None:
126
- return None
127
- files = []
128
- ins_total = 0
129
- del_total = 0
130
- for line in raw.splitlines():
131
- parts = line.split("\t")
132
- if len(parts) < 3:
133
- continue
134
- ins_s, del_s, path = parts[0], parts[1], parts[2]
135
- # binary files show "-" for both columns; count the file, add 0.
136
- ins = 0 if ins_s == "-" else _to_int(ins_s)
137
- dele = 0 if del_s == "-" else _to_int(del_s)
138
- ins_total += ins
139
- del_total += dele
140
- files.append({
141
- "path": path,
142
- "insertions": ins,
143
- "deletions": dele,
144
- "status": "binary" if ins_s == "-" else "modified",
145
- })
146
- return {
147
- "count": len(files),
148
- "insertions": ins_total,
149
- "deletions": del_total,
150
- "files": files,
151
- }
139
+ """Return final-worktree stats relative to ``base``.
140
+
141
+ ``head`` remains in the signature for installed-layout compatibility. The
142
+ live working tree is the proof target, not only its current commit.
143
+ """
144
+ stat, _ = collect_workspace_diff(repo_dir, str(base), False)
145
+ return stat
152
146
 
153
147
 
154
148
  def _diff_sha256_from_stat(files_changed):
@@ -205,12 +199,42 @@ def _compute_headline(facts, degraded):
205
199
 
206
200
  sec = facts.get("security") or {}
207
201
  sec_high = bool(sec.get("ran") and (sec.get("high_active") or 0) > 0)
202
+ fn = facts.get("functional") or {}
203
+ fn_failed = bool(
204
+ os.environ.get("LOKI_FV_GATE") == "1"
205
+ and fn.get("ran")
206
+ and fn.get("functional_status") == "failed"
207
+ )
208
+ execution = facts.get("execution") or {}
209
+ execution_outcome = str(execution.get("outcome") or "").lower()
210
+ failed_run_statuses = {
211
+ "failed",
212
+ "force_stopped",
213
+ "inconclusive_spec_contradiction",
214
+ "interrupted",
215
+ "max_iterations_reached",
216
+ "max_retries_exceeded",
217
+ "paused",
218
+ "policy_blocked",
219
+ "provider_deadline_partial_mutation",
220
+ }
221
+ execution_failed = bool(
222
+ execution.get("terminated")
223
+ or execution.get("exit_code") not in (None, 0)
224
+ or str(execution.get("run_status") or "").lower() in failed_run_statuses
225
+ or (
226
+ execution_outcome
227
+ and execution_outcome not in ("complete", "completed", "success")
228
+ )
229
+ )
208
230
  any_failed = (
209
- tests.get("status") == "failed"
231
+ execution_failed
232
+ or tests.get("status") == "failed"
210
233
  or build.get("status") == "failed"
211
234
  or any(g.get("status") == "failed"
212
235
  for g in (facts.get("quality_gates") or []))
213
236
  or sec_high
237
+ or fn_failed
214
238
  )
215
239
  if any_failed:
216
240
  return "NOT VERIFIED"
@@ -294,6 +318,16 @@ def _recorded_diff_sha256(proof):
294
318
  return None
295
319
 
296
320
 
321
+ def _recorded_tree_sha256(proof):
322
+ facts = proof.get("facts")
323
+ if isinstance(facts, dict):
324
+ git = facts.get("git")
325
+ if isinstance(git, dict) and git.get("tree_sha256"):
326
+ return str(git["tree_sha256"])
327
+ value = proof.get("tree_sha256")
328
+ return str(value) if value else None
329
+
330
+
297
331
  def _recorded_degraded(proof):
298
332
  """Return the honesty.degraded list the generator disclosed, or []."""
299
333
  honesty = proof.get("honesty")
@@ -404,6 +438,75 @@ def _load_proof(proof_path):
404
438
  return data
405
439
 
406
440
 
441
+ def verify_integrity(proof):
442
+ """Verify the receipt hash, signature, and recorded headline.
443
+
444
+ This is the canonical in-memory integrity check shared by the CLI verifier
445
+ and supervised execution binding. Repository drift is intentionally left to
446
+ verify(), whose caller supplies the repository path.
447
+ """
448
+ result = {
449
+ "hash_ok": False,
450
+ "gpg_ok": "n/a",
451
+ "generator_trusted": True,
452
+ "headline_consistent": None,
453
+ "degraded": _recorded_degraded(proof) if isinstance(proof, dict) else [],
454
+ "reason": "",
455
+ "ok": False,
456
+ }
457
+ if not isinstance(proof, dict):
458
+ result["reason"] = "proof root is not a JSON object"
459
+ return result
460
+
461
+ verification = proof.get("verification")
462
+ if not isinstance(verification, dict) or not verification.get("hash"):
463
+ result["reason"] = "no verification.hash recorded; cannot prove integrity"
464
+ return result
465
+
466
+ unsigned = dict(proof)
467
+ unsigned.pop("verification", None)
468
+ canonical_bytes = _canonical(unsigned).encode("utf-8")
469
+ recorded_hash = str(verification.get("hash"))
470
+ recomputed = hashlib.sha256(canonical_bytes).hexdigest()
471
+ result["hash_ok"] = recomputed == recorded_hash
472
+ if not result["hash_ok"]:
473
+ result["reason"] = (
474
+ "integrity hash mismatch (proof.json was edited after signing)"
475
+ )
476
+
477
+ result["gpg_ok"] = _verify_gpg(
478
+ canonical_bytes, verification.get("gpg_signature")
479
+ )
480
+ result["generator_trusted"] = result["gpg_ok"] is not True
481
+
482
+ recorded_headline = _recorded_headline(proof)
483
+ facts = proof.get("facts")
484
+ if recorded_headline is not None and isinstance(facts, dict):
485
+ derived = _compute_headline(facts, _recorded_degraded_raw(proof))
486
+ result["headline_consistent"] = derived == recorded_headline
487
+ if not result["headline_consistent"] and not result["reason"]:
488
+ result["reason"] = (
489
+ "honesty.headline (%r) disagrees with the headline re-derived "
490
+ "from the recorded facts (%r); the headline was edited to "
491
+ "misrepresent the facts" % (recorded_headline, derived)
492
+ )
493
+
494
+ result["ok"] = bool(
495
+ result["hash_ok"]
496
+ and result["gpg_ok"] in (True, "n/a")
497
+ and result["headline_consistent"] is not False
498
+ )
499
+ if result["ok"]:
500
+ result["reason"] = ""
501
+ elif not result["reason"]:
502
+ result["reason"] = (
503
+ "gpg signature verification failed"
504
+ if result["gpg_ok"] is False
505
+ else "verification failed"
506
+ )
507
+ return result
508
+
509
+
407
510
  def verify(proof_path, repo_dir="."):
408
511
  """Re-verify a proof.json against the repo.
409
512
 
@@ -444,47 +547,21 @@ def verify(proof_path, repo_dir="."):
444
547
  """
445
548
  proof = _load_proof(proof_path)
446
549
 
550
+ integrity = verify_integrity(proof)
447
551
  result = {
448
- "hash_ok": False,
552
+ **integrity,
449
553
  "diff_drift": None,
450
554
  "diff_recheck": {"recorded": None, "current": None},
451
- "gpg_ok": "n/a",
452
- "generator_trusted": True,
453
- "headline_consistent": None,
454
- "degraded": _recorded_degraded(proof),
455
- "reason": "",
555
+ "tree_drift": None,
556
+ "tree_recheck": {"recorded": None, "current": None},
456
557
  "ok": False,
457
558
  }
458
559
 
459
- # ----- 1. TAMPER CHECK -------------------------------------------------
460
- verification = proof.get("verification")
461
- if not isinstance(verification, dict) or not verification.get("hash"):
462
- result["hash_ok"] = False
463
- result["reason"] = "no verification.hash recorded; cannot prove integrity"
560
+ if not integrity["hash_ok"] and not (
561
+ isinstance(proof.get("verification"), dict)
562
+ and proof["verification"].get("hash")
563
+ ):
464
564
  return result
465
- recorded_hash = str(verification.get("hash"))
466
-
467
- # Recompute over the canonical form with verification REMOVED, exactly as
468
- # the generator hashed it (hash computed before verification was attached).
469
- unsigned = dict(proof)
470
- unsigned.pop("verification", None)
471
- canonical_str = _canonical(unsigned)
472
- canonical_bytes = canonical_str.encode("utf-8")
473
- recomputed = hashlib.sha256(canonical_bytes).hexdigest()
474
- result["hash_ok"] = (recomputed == recorded_hash)
475
- if not result["hash_ok"]:
476
- result["reason"] = "integrity hash mismatch (proof.json was edited after signing)"
477
- # Continue to gather drift/gpg signals for the report, but ok stays False.
478
-
479
- # ----- 3. GPG (compute before returning so the report is complete) -----
480
- gpg_sig = verification.get("gpg_signature")
481
- result["gpg_ok"] = _verify_gpg(canonical_bytes, gpg_sig)
482
-
483
- # generator_trusted: only a VALID signature (gpg_ok is True) means the
484
- # generator is NOT trusted (neutral non-forgeability). "n/a" or a bad sig
485
- # leaves the generator trusted -- the facts are taken at face value and a
486
- # consistent forger is NOT caught. Stated so ok=True still discloses it.
487
- result["generator_trusted"] = (result["gpg_ok"] is not True)
488
565
 
489
566
  # ----- 2. DRIFT CHECK --------------------------------------------------
490
567
  recorded_stat = _recorded_diff_stat(proof)
@@ -559,37 +636,24 @@ def verify(proof_path, repo_dir="."):
559
636
  if drift and not result["reason"]:
560
637
  result["reason"] = "recorded diff no longer matches the repo (drift detected)"
561
638
 
562
- # ----- 4. HEADLINE CONSISTENCY (defense-in-depth) ----------------------
563
- # Re-derive the headline from the recorded facts and compare to the stored
564
- # honesty.headline. A mismatch means the headline was edited to disagree
565
- # with the facts it claims to summarize (an INCONSISTENT forgery, e.g.
566
- # headline flipped to VERIFIED while facts.tests.status is still not_run).
567
- # This catches careless/partial tampering only. It does NOT catch a
568
- # CONSISTENT forger who rewrites both the facts and the headline to a
569
- # matching lie and recomputes the integrity hash -- on the unsigned path
570
- # that still passes (see generator_trusted). Neutral non-forgeability needs
571
- # the signed record, not this check.
572
- recorded_headline = _recorded_headline(proof)
573
- facts = proof.get("facts")
574
- if recorded_headline is not None and isinstance(facts, dict):
575
- derived = _compute_headline(facts, _recorded_degraded_raw(proof))
576
- result["headline_consistent"] = (derived == recorded_headline)
577
- if not result["headline_consistent"] and not result["reason"]:
578
- result["reason"] = (
579
- "honesty.headline (%r) disagrees with the headline re-derived "
580
- "from the recorded facts (%r); the headline was edited to "
581
- "misrepresent the facts" % (recorded_headline, derived)
582
- )
583
- else:
584
- # No recorded headline, or no facts to re-derive from: cannot check.
585
- result["headline_consistent"] = None
639
+ recorded_tree = _recorded_tree_sha256(proof)
640
+ result["tree_recheck"]["recorded"] = recorded_tree
641
+ if recorded_tree:
642
+ current_tree = compute_tree_digest(repo_dir)
643
+ result["tree_recheck"]["current"] = current_tree or None
644
+ if not current_tree:
645
+ if not result["reason"]:
646
+ result["reason"] = "final workspace tree could not be re-derived"
647
+ else:
648
+ result["tree_drift"] = current_tree != recorded_tree
649
+ if result["tree_drift"] and not result["reason"]:
650
+ result["reason"] = "recorded final workspace tree no longer matches the repo"
586
651
 
587
652
  # ----- overall verdict -------------------------------------------------
588
653
  result["ok"] = bool(
589
- result["hash_ok"]
654
+ integrity["ok"]
590
655
  and result["diff_drift"] is False
591
- and result["gpg_ok"] in (True, "n/a")
592
- and result["headline_consistent"] is not False
656
+ and (not recorded_tree or result["tree_drift"] is False)
593
657
  )
594
658
  if result["ok"]:
595
659
  result["reason"] = ""