loki-mode 9.8.0 → 9.12.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.
Files changed (79) hide show
  1. package/README.md +19 -14
  2. package/SKILL.md +3 -2
  3. package/VERSION +1 -1
  4. package/autonomy/loki +122 -1
  5. package/autonomy/run.sh +49 -2
  6. package/dashboard/__init__.py +1 -1
  7. package/dashboard/api_evidence.py +411 -0
  8. package/dashboard/api_operator.py +283 -0
  9. package/dashboard/api_phases.py +262 -0
  10. package/dashboard/api_releases.py +242 -0
  11. package/dashboard/api_runs.py +477 -0
  12. package/dashboard/api_tests.py +444 -0
  13. package/dashboard/api_v2.py +47 -1
  14. package/dashboard/server.py +54 -0
  15. package/dashboard/static/index.html +246 -135
  16. package/docs/ARCHITECTURE-OVERVIEW.md +5 -3
  17. package/docs/CAPABILITY-BACKLOG.md +53 -0
  18. package/docs/COMPARISON.md +2 -2
  19. package/docs/COMPETITIVE-ANALYSIS.md +1 -1
  20. package/docs/COMPETITIVE-SCORECARD.md +422 -0
  21. package/docs/DASHBOARD-9.12-EVIDENCE.md +97 -0
  22. package/docs/DASHBOARD-ARCHITECTURE.md +423 -0
  23. package/docs/DEMOS.md +21 -23
  24. package/docs/HANDOFF-2026-08-03.md +439 -0
  25. package/docs/INSTALLATION.md +17 -10
  26. package/docs/OUTCOME-FRONTIER.md +536 -0
  27. package/docs/PROMPT-ABLATION-RESULT.md +97 -0
  28. package/docs/TOOLS.md +800 -0
  29. package/docs/alternative-installations.md +2 -3
  30. package/docs/audit-logging.md +44 -35
  31. package/docs/authentication.md +13 -2
  32. package/docs/authorization.md +87 -81
  33. package/docs/git-workflow.md +6 -3
  34. package/docs/metrics.md +15 -16
  35. package/docs/network-security.md +16 -13
  36. package/docs/openclaw-integration.md +36 -556
  37. package/docs/show-hn-post.md +2 -2
  38. package/docs/siem-integration.md +39 -36
  39. package/loki-ts/dist/loki.js +18 -18
  40. package/mcp/__init__.py +1 -1
  41. package/package.json +2 -2
  42. package/plugins/loki-mode/.claude-plugin/plugin.json +1 -1
  43. package/references/confidence-routing.md +18 -1
  44. package/references/invariant-checks.md +13 -8
  45. package/references/magic-rarv-integration.md +0 -1
  46. package/references/multi-provider.md +27 -5
  47. package/skills/healing.md +4 -2
  48. package/tools/audit-docs.py +488 -0
  49. package/tools/baseline-pin.py +19 -1
  50. package/tools/calibration-audit.py +523 -0
  51. package/tools/ci-gate.py +19 -1
  52. package/tools/cost-forecast.py +344 -0
  53. package/tools/cost-guard.py +19 -1
  54. package/tools/cost-history.py +19 -1
  55. package/tools/cost-per-outcome.py +394 -0
  56. package/tools/estimate-run.py +19 -1
  57. package/tools/evidence-freshness.py +307 -0
  58. package/tools/gate-init.py +19 -1
  59. package/tools/gate-report.py +19 -1
  60. package/tools/gate-simulate.py +570 -0
  61. package/tools/gate-trend.py +354 -0
  62. package/tools/model-advisor.py +52 -1
  63. package/tools/policy-load.py +19 -1
  64. package/tools/prompt-cost.py +363 -0
  65. package/tools/prompt-diff.py +448 -0
  66. package/tools/prompt-lint.py +448 -0
  67. package/tools/receipt-bundle.py +72 -2
  68. package/tools/receipt-diff.py +19 -1
  69. package/tools/receipt-find.py +19 -1
  70. package/tools/receipt-stats.py +380 -0
  71. package/tools/receipt-timeline.py +478 -0
  72. package/tools/receipt-verify-batch.py +291 -0
  73. package/tools/run-replay.py +19 -1
  74. package/tools/signing-status.py +19 -1
  75. package/tools/token-guard.py +19 -1
  76. package/tools/token-tax.py +375 -0
  77. package/tools/tool-index.py +19 -1
  78. package/tools/verification-tax.py +277 -0
  79. package/tools/verify-chain.py +361 -0
@@ -0,0 +1,380 @@
1
+ #!/usr/bin/env python3
2
+ """Summarise an archive of receipts: how many, how verified, what they cost.
3
+
4
+ WHY THIS EXISTS. receipt-bundle.py rolls an archive into ONE verdict for a
5
+ compliance reviewer, and receipt-find.py returns the individual runs matching a
6
+ filter. Neither answers the question an operator asks first: what IS this
7
+ archive. How many receipts, how many actually verified, what did the whole
8
+ thing cost, and over what span. Answering that by eye means opening every
9
+ proof.json; answering it with `jq | paste | awk` is how the cost figure gets
10
+ quietly wrong, because every ad-hoc pipeline gets it wrong in the same
11
+ direction -- toward a smaller, tidier, more confident number.
12
+
13
+ THE RULES. Each is a specific way a summary can claim more than it measured.
14
+
15
+ 1. AN UNMEASURED COST IS EXCLUDED FROM EVERY COST STATISTIC, AND THE EXCLUSION
16
+ IS COUNTED. A receipt that never recorded cost did not cost $0. Summing it
17
+ as 0 understates the total by exactly the spend nobody captured, and does
18
+ something worse to the MEDIAN: nulls read as 0 cluster at the bottom of the
19
+ sorted list and drag the midpoint down, so an archive of expensive runs with
20
+ broken instrumentation reports a cheap median. That is the defect this
21
+ codebase paid for on four surfaces (v8.51.0-v8.54.0) and it is sharpest
22
+ here, because a median is the one statistic where adding fake low values
23
+ changes the answer without changing any real one.
24
+
25
+ The predicate is record_is_measured() in autonomy/lib/efficiency_cost.py,
26
+ reached through receipt-diff.py's measured_cost(), which already maps the
27
+ receipt's `cost.usd` onto the per-iteration `cost_usd` key that predicate
28
+ reads. A second copy of either half is how the honesty rule drifts, so
29
+ there is not one in this file.
30
+
31
+ Measured is necessary but not sufficient: record_is_measured is true when
32
+ ANY of five fields is non-zero, so a receipt with tokens and a null `usd` is
33
+ honestly measured and still carries no dollar figure. Both are required --
34
+ see _usd().
35
+
36
+ 2. NOTHING MEASURED READS UNKNOWN, NEVER $0.00. When no receipt in the archive
37
+ carried a cost, the total and the median are absent facts, not zero ones.
38
+ The count of measured receipts decides this, never the total -- `if not
39
+ total` would erase a real, measured $0.0000 archive, which is rule 2 failing
40
+ in the opposite direction and just as untrue.
41
+
42
+ 3. A MEASURED ZERO SURVIVES AS ZERO. A run that genuinely cost $0.00 is an
43
+ observation. `is None` throughout, never truthiness: the two ways to be
44
+ dishonest about cost are to invent a number and to discard one.
45
+
46
+ 4. A MALFORMED RECEIPT IS COUNTED AND NAMED, NEVER SILENTLY SKIPPED. A summary
47
+ that drops what it cannot read reports a tidier archive than exists, and
48
+ does it invisibly. Note this bucket is kept SEPARATE from UNVERIFIABLE:
49
+ receipt_state() folds an unloadable receipt into UNVERIFIABLE, which is
50
+ right for a verdict and wrong for a census -- "this file is not JSON" and
51
+ "this receipt cannot be re-derived outside its repo" are different facts
52
+ with different fixes.
53
+
54
+ 5. ZERO RECEIPTS IS NOT A CLEAN ARCHIVE. "We summarised an archive and it was
55
+ empty" is not a finding; it is an invocation pointed at the wrong directory.
56
+ It gets its own exit code and says so in words.
57
+
58
+ WHAT THIS IS NOT. This is an ADVISOR, not a gate, and the distinction is
59
+ deliberate and pinned by tests/test_tool_exit_contract.py.
60
+
61
+ - A FAILED receipt in the archive does NOT make this exit 1. The census
62
+ reports the count; receipt-bundle.py is the gate that refuses to merge on
63
+ it. Re-deriving the weakest-link rule here would be a second copy of a
64
+ verdict predicate, which is the exact drift the rules above warn about.
65
+
66
+ - An archive where NOTHING measured cost still exits 0. "Every receipt is
67
+ present and readable, and none of them recorded a cost" is a complete,
68
+ successful, honest answer to the question asked -- the output says UNKNOWN
69
+ in words. Forcing it non-zero would make the honest answer look like a tool
70
+ failure, and train operators to ignore it.
71
+
72
+ Verdict classification is receipt_state() from receipt-bundle.py, which wraps
73
+ verify() from autonomy/lib/proof-verify.py. Nothing here re-implements it.
74
+
75
+ Usage:
76
+ tools/receipt-stats.py [workspace] [--repo-dir DIR] [--json]
77
+
78
+ Exit codes:
79
+ 0 receipts were found and summarised
80
+ 3 the workspace exists but holds no receipts -- nothing to summarise
81
+ 64 usage error (unknown flag, bad argument)
82
+ 66 the workspace path does not exist
83
+ """
84
+
85
+ import argparse
86
+ import datetime
87
+ import importlib.util
88
+ import json
89
+ import os
90
+ import pathlib
91
+ import sys
92
+
93
+ # A stale .pyc can mask a mutation and turn a real probe into a false
94
+ # "MUTATION SURVIVED", since invalidation is mtime+size and a restore is
95
+ # byte-identical. Set before the loader below runs.
96
+ sys.dont_write_bytecode = True
97
+
98
+ _ROOT = pathlib.Path(__file__).resolve().parents[1]
99
+
100
+
101
+ def _load(name, path):
102
+ spec = importlib.util.spec_from_file_location(name, path)
103
+ mod = importlib.util.module_from_spec(spec)
104
+ spec.loader.exec_module(mod)
105
+ return mod
106
+
107
+
108
+ # receipt_state() wraps verify() and already keeps VERIFIED / FAILED /
109
+ # UNVERIFIABLE apart with the `is`-comparisons that make that correct
110
+ # (gpg_ok is the truthy string "n/a"; diff_drift None means unverifiable, not
111
+ # clean). measured_cost() reuses record_is_measured() AND maps cost.usd ->
112
+ # cost_usd. Both are imported rather than restated.
113
+ _rb = _load("receipt_bundle", _ROOT / "tools" / "receipt-bundle.py")
114
+ receipt_state = _rb.receipt_state
115
+ measured_cost = _rb.measured_cost
116
+
117
+ VERIFIED = _rb.VERIFIED
118
+ UNVERIFIABLE = _rb.UNVERIFIABLE
119
+ FAILED = _rb.FAILED
120
+
121
+ MALFORMED = "MALFORMED"
122
+
123
+ # Reported in this order every time, so two runs are diffable. MALFORMED is
124
+ # last because it is a census bucket, not one of receipt_state's verdicts.
125
+ _BUCKETS = (VERIFIED, FAILED, UNVERIFIABLE, MALFORMED)
126
+
127
+
128
+ def _usd(proof):
129
+ """The receipt's cost in dollars, or None when there is no such number.
130
+
131
+ None means UNMEASURED, and every caller must exclude rather than
132
+ substitute. Returning 0.0 here is rule 1 -- it would both understate the
133
+ total and drag the median toward the bottom of the range.
134
+ """
135
+ rec = measured_cost(proof)
136
+ if rec is None:
137
+ return None
138
+ return rec.get("cost_usd")
139
+
140
+
141
+ def _date(proof):
142
+ """The receipt's UTC date as YYYY-MM-DD, or None.
143
+
144
+ Sliced, not parsed. generated_at ends in "Z", which datetime.fromisoformat
145
+ rejects before Python 3.11, and this tool must behave identically on every
146
+ interpreter it runs under. ISO dates compare correctly as strings, so a
147
+ slice is both the portable answer and the smaller one.
148
+ """
149
+ v = proof.get("generated_at")
150
+ if not isinstance(v, str) or len(v) < 10:
151
+ return None
152
+ d = v[:10]
153
+ try:
154
+ datetime.date.fromisoformat(d)
155
+ except ValueError:
156
+ return None
157
+ return d
158
+
159
+
160
+ def median(values):
161
+ """The midpoint of `values`, or None when there is nothing to take it of.
162
+
163
+ Takes only what it was given. The caller is responsible for having already
164
+ excluded unmeasured receipts -- passing 0.0 for an absent measurement is
165
+ rule 1, and it corrupts this function's answer silently because 0.0 is a
166
+ perfectly valid cost.
167
+ """
168
+ if not values:
169
+ return None
170
+ s = sorted(values)
171
+ n = len(s)
172
+ mid = n // 2
173
+ if n % 2:
174
+ return s[mid]
175
+ return (s[mid - 1] + s[mid]) / 2.0
176
+
177
+
178
+ def find_receipts(workspace):
179
+ """Every proof.json under the workspace, sorted for a stable report.
180
+
181
+ ponytail: same rglob as receipt-bundle.find_receipts, so a receipt archived
182
+ outside .loki/proofs/ is still counted. Not imported, because that one
183
+ assumes the directory exists and this tool must tell a missing workspace
184
+ (exit 66) apart from an empty one (exit 3).
185
+ """
186
+ root = pathlib.Path(workspace)
187
+ if not root.is_dir():
188
+ return []
189
+ return sorted(p for p in root.rglob("proof.json") if p.is_file())
190
+
191
+
192
+ def stats(workspace, repo_dir="."):
193
+ """Summarise every receipt under `workspace`. Pure: no writes, no network."""
194
+ paths = find_receipts(workspace)
195
+
196
+ receipts = []
197
+ malformed = []
198
+ costs = []
199
+ dates = []
200
+
201
+ for path in paths:
202
+ try:
203
+ with open(path, "r", encoding="utf-8") as f:
204
+ proof = json.load(f)
205
+ if not isinstance(proof, dict):
206
+ raise ValueError("receipt is not a JSON object")
207
+ except Exception as exc:
208
+ # Rule 4: counted and NAMED, with the reason, in its own bucket.
209
+ # Never dropped, and never folded into UNVERIFIABLE -- unreadable
210
+ # bytes and an unre-derivable diff have different fixes.
211
+ malformed.append({"path": str(path), "reason": str(exc)})
212
+ # Same keys as every other row, `reason` included. A consumer
213
+ # reading receipts[i]["reason"] must not KeyError on exactly the
214
+ # rows rule 4 exists to surface.
215
+ receipts.append({
216
+ "path": str(path), "state": MALFORMED, "reason": str(exc),
217
+ "cost_usd": None, "date": None,
218
+ })
219
+ continue
220
+
221
+ state, reason = receipt_state(path, repo_dir)
222
+
223
+ usd = _usd(proof)
224
+ if usd is not None:
225
+ costs.append(usd)
226
+
227
+ date = _date(proof)
228
+ if date is not None:
229
+ dates.append(date)
230
+
231
+ receipts.append({
232
+ "path": str(path), "state": state, "reason": reason,
233
+ "cost_usd": usd, "date": date,
234
+ })
235
+
236
+ # len(costs), NOT sum(costs), decides UNKNOWN. Rules 2 and 3 in one line:
237
+ # an archive of three receipts that each genuinely measured $0.0000 has a
238
+ # real total of 0.0 and a real median of 0.0, and `if not total` would
239
+ # report both as UNKNOWN -- erasing a measurement is the same dishonesty as
240
+ # inventing one, pointed the other way.
241
+ measured_n = len(costs)
242
+ cost_block = {
243
+ "measured_receipts": measured_n,
244
+ "unmeasured_receipts": len(receipts) - measured_n,
245
+ "total_usd": sum(costs) if measured_n else None,
246
+ "median_usd": median(costs),
247
+ }
248
+
249
+ counts = {b: sum(1 for r in receipts if r["state"] == b) for b in _BUCKETS}
250
+
251
+ return {
252
+ "report": "loki-receipt-stats/v1",
253
+ "workspace": os.path.abspath(str(workspace)),
254
+ "checked_from": os.path.abspath(repo_dir),
255
+ "receipts": receipts,
256
+ "receipt_count": len(receipts),
257
+ "counts": counts,
258
+ "cost": cost_block,
259
+ "dates": {
260
+ "first": min(dates) if dates else None,
261
+ "last": max(dates) if dates else None,
262
+ "dated_receipts": len(dates),
263
+ },
264
+ "malformed": malformed,
265
+ "malformed_count": len(malformed),
266
+ "summary": _summary(len(receipts), counts, cost_block, dates),
267
+ }
268
+
269
+
270
+ def _cost_line(cost):
271
+ """The cost figures, each carrying the basis it was computed from."""
272
+ # Either signal absent means UNKNOWN. The count and the figures should
273
+ # never disagree, and when they do this must still render an honest
274
+ # sentence rather than raise inside %.4f -- a renderer that crashes on the
275
+ # unknown path is a surface with no honest state at all.
276
+ if (cost["measured_receipts"] == 0
277
+ or cost["total_usd"] is None or cost["median_usd"] is None):
278
+ # Phrased off the data, never a hardcoded "0 of N". This branch is
279
+ # reachable with measured_receipts > 0 (a figure went missing without
280
+ # the count going with it), and a file arguing that a summary must not
281
+ # claim more than it measured cannot ship a sentence that can state a
282
+ # false count.
283
+ return ("cost UNKNOWN -- no usable cost figure across %d receipt(s) "
284
+ "(%d measured), so there is no total and no median. "
285
+ "Not $0.00: unmeasured is not free."
286
+ % (cost["measured_receipts"] + cost["unmeasured_receipts"],
287
+ cost["measured_receipts"]))
288
+ line = ("total $%.4f, median $%.4f across %d measured receipt(s)"
289
+ % (cost["total_usd"], cost["median_usd"],
290
+ cost["measured_receipts"]))
291
+ if cost["unmeasured_receipts"]:
292
+ line += (". %d receipt(s) EXCLUDED from both figures: cost was never "
293
+ "measured, and summing an absent measurement as 0 would "
294
+ "understate the total and drag the median down"
295
+ % cost["unmeasured_receipts"])
296
+ return line
297
+
298
+
299
+ def _date_line(dates):
300
+ """The span, or UNKNOWN. An absent range is never rendered as a real one."""
301
+ if not dates:
302
+ return "date range UNKNOWN -- no receipt carried a readable generated_at"
303
+ return "%s to %s across %d dated receipt(s)" % (
304
+ min(dates), max(dates), len(dates))
305
+
306
+
307
+ def _summary(n, counts, cost, dates):
308
+ if n == 0:
309
+ # Rule 5. Distinct in words from "we summarised an empty archive".
310
+ return ("NO RECEIPTS -- no proof.json found under this workspace, so "
311
+ "there was nothing to summarise. Zero receipts is not a clean "
312
+ "archive; it is most often the wrong directory.")
313
+ head = "%d receipt(s): %s." % (
314
+ n, ", ".join("%d %s" % (counts[b], b) for b in _BUCKETS if counts[b]))
315
+ head += " Cost: %s." % _cost_line(cost)
316
+ head += " Dates: %s." % _date_line(dates)
317
+ return head
318
+
319
+
320
+ class _Parser(argparse.ArgumentParser):
321
+ """argparse exits 2 on a usage error; here 2 means "could not check".
322
+
323
+ A mistyped flag would otherwise be indistinguishable from a blind gate --
324
+ the operator sees the code that means "your instrumentation is broken" and
325
+ goes looking for broken instrumentation. 64 is the usage error.
326
+
327
+ error() only. --help routes through exit(), not error(), and overriding
328
+ exit() would break the exit-0 contract that test_tool_exit_contract.py
329
+ asserts for every tool's --help.
330
+ """
331
+
332
+ def error(self, message):
333
+ self.print_usage(sys.stderr)
334
+ sys.stderr.write("%s: error: %s\n" % (self.prog, message))
335
+ raise SystemExit(64)
336
+
337
+
338
+ def main(argv=None):
339
+ ap = _Parser(
340
+ description="Summarise an archive of receipts: how many, how "
341
+ "verified, what they cost.")
342
+ ap.add_argument("workspace", nargs="?", default=".",
343
+ help="workspace holding the receipts (default: .)")
344
+ ap.add_argument("--repo-dir", default=".",
345
+ help="repository the receipts are verified against "
346
+ "(default: .)")
347
+ ap.add_argument("--json", action="store_true",
348
+ help="emit the full report as JSON")
349
+ args = ap.parse_args(argv)
350
+
351
+ if not os.path.isdir(args.workspace):
352
+ # 66, not 3. "You pointed me at nothing" and "this archive is empty"
353
+ # are different facts, and only one of them is about the archive.
354
+ sys.stderr.write(
355
+ "receipt-stats: workspace does not exist: %s\n" % args.workspace)
356
+ return 66
357
+
358
+ report = stats(args.workspace, args.repo_dir)
359
+
360
+ if args.json:
361
+ print(json.dumps(report, indent=2))
362
+ else:
363
+ # "-" for an unmeasured cost, never "$0.0000". The table is the surface
364
+ # an operator eyeballs, and it must not be the one place the archive
365
+ # looks free.
366
+ reasons = {b["path"]: b["reason"] for b in report["malformed"]}
367
+ for r in report["receipts"]:
368
+ usd = "-" if r["cost_usd"] is None else "$%.4f" % r["cost_usd"]
369
+ line = "%-13s %-10s %s" % (r["state"], usd, r["path"])
370
+ if r["path"] in reasons:
371
+ line += " (%s)" % reasons[r["path"]]
372
+ print(line)
373
+ print("")
374
+ print(report["summary"])
375
+
376
+ return 0 if report["receipt_count"] else 3
377
+
378
+
379
+ if __name__ == "__main__":
380
+ sys.exit(main())