loki-mode 7.8.3 → 7.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/autonomy/run.sh CHANGED
@@ -4091,6 +4091,36 @@ print(json.dumps(rec))
4091
4091
  " > "$tmp" 2>/dev/null && mv -f "$tmp" "$loki_dir/state/prd-signature.json" 2>/dev/null || rm -f "$tmp" 2>/dev/null
4092
4092
  }
4093
4093
 
4094
+ # generate_proof_of_run: thin fire-and-forget wrapper around the standalone
4095
+ # proof-of-run generator (autonomy/lib/proof-generator.py). Runs on both
4096
+ # success and failure session ends. The generator owns the schema, redaction
4097
+ # chokepoint, and HTML rendering; this wrapper only resolves the path and
4098
+ # invokes python3. Never fails the session (|| true at the call site).
4099
+ # NOTE: no inline python here on purpose -- keep this wrapper apostrophe-free
4100
+ # to avoid the bash single-quote trap.
4101
+ generate_proof_of_run() {
4102
+ # $1 (session result) is accepted for call-site symmetry but the generator
4103
+ # derives success/failure from queue state, so it is intentionally unused.
4104
+ local _result="${1:-0}"
4105
+ : "$_result"
4106
+ local gen="$SCRIPT_DIR/lib/proof-generator.py"
4107
+ [ -f "$gen" ] || return 0
4108
+ local loki_dir="${TARGET_DIR:-.}/.loki"
4109
+ [ -d "$loki_dir" ] || return 0
4110
+ local ver provider
4111
+ ver="$(get_version 2>/dev/null || echo unknown)"
4112
+ provider="${PROVIDER_NAME:-claude}"
4113
+ ITERATION_COUNT="${ITERATION_COUNT:-0}" \
4114
+ PROVIDER_NAME="$provider" \
4115
+ PRD_PATH="${prd_path:-}" \
4116
+ python3 "$gen" \
4117
+ --loki-dir "$loki_dir" \
4118
+ --loki-version "$ver" \
4119
+ --provider "$provider" \
4120
+ --quiet >/dev/null 2>&1 || true
4121
+ return 0
4122
+ }
4123
+
4094
4124
  track_iteration_complete() {
4095
4125
  local iteration="$1"
4096
4126
  local exit_code="${2:-0}"
@@ -13277,6 +13307,12 @@ main() {
13277
13307
  # Write structured handoff for future sessions (v5.49.0)
13278
13308
  write_structured_handoff "session_end_result_${result}" 2>/dev/null || true
13279
13309
 
13310
+ # Generate shareable proof-of-run artifact (R1). Default-on, opt out with
13311
+ # LOKI_PROOF=0. Fire-and-forget on both success and failure runs.
13312
+ if [ "${LOKI_PROOF:-1}" != "0" ]; then
13313
+ generate_proof_of_run "$result" || true
13314
+ fi
13315
+
13280
13316
  # Create PR from agent branch if branch protection was enabled
13281
13317
  create_session_pr
13282
13318
  audit_agent_action "session_stop" "Session ended" "result=$result,iterations=$ITERATION_COUNT"
package/bin/loki CHANGED
@@ -116,7 +116,7 @@ fi
116
116
  # Two-token routes (provider show/list, memory list/index) match on the first
117
117
  # token only; the Bun dispatcher handles subcommand routing internally.
118
118
  case "${1:-}" in
119
- version|--version|-v|status|stats|doctor|provider|memory|rollback|internal|kpis)
119
+ version|--version|-v|status|stats|doctor|provider|memory|rollback|internal|kpis|proof)
120
120
  # v7.5.2: rollback added (wires loki-ts/src/commands/rollback.ts).
121
121
  # v7.5.3: internal added for autonomy/run.sh phase1-hooks calls.
122
122
  # v7.5.28: kpis added (Phase K MVP: read-only KPI snapshot).
package/completions/_loki CHANGED
@@ -88,6 +88,14 @@ function _loki {
88
88
  '--format[Report format]:format:(text markdown html)' \
89
89
  '--help[Show help]'
90
90
  ;;
91
+ proof)
92
+ _arguments \
93
+ '--yes[Skip redaction-preview confirmation]' \
94
+ '--private[Create a secret gist when sharing]' \
95
+ '--hosted[Hosted publishing (coming in R9)]' \
96
+ '--help[Show help]' \
97
+ '1:subcommand:(list show open share)'
98
+ ;;
91
99
  completions)
92
100
  _arguments '1:shell:(bash zsh)'
93
101
  ;;
@@ -140,6 +148,7 @@ function _loki_commands {
140
148
  'onboard:Analyze repo and generate CLAUDE.md'
141
149
  'metrics:Session productivity report'
142
150
  'share:Share session report as GitHub Gist'
151
+ 'proof:Inspect/share proof-of-run artifacts'
143
152
  'context:Context window management'
144
153
  'code:Codebase intelligence'
145
154
  'version:Show version'
@@ -5,7 +5,7 @@ _loki_completion() {
5
5
  _init_completion || return
6
6
 
7
7
  # Main subcommands (must match autonomy/loki main case statement)
8
- local main_commands="start quick monitor demo init stop pause resume status dashboard logs serve api sandbox notify import github issue config provider reset memory compound checkpoint council dogfood projects enterprise secrets doctor watchdog audit metrics syslog onboard share explain plan report test ci watch telemetry agent context code run export review optimize heal migrate cluster worktree trigger failover remote version completions help"
8
+ local main_commands="start quick monitor demo init stop pause resume status dashboard logs serve api sandbox notify import github issue config provider reset memory compound checkpoint council dogfood projects enterprise secrets doctor watchdog audit metrics syslog onboard share proof explain plan report test ci watch telemetry agent context code run export review optimize heal migrate cluster worktree trigger failover remote version completions help"
9
9
 
10
10
  # 1. If we are on the first argument (subcommand)
11
11
  if [[ $cword -eq 1 ]]; then
@@ -178,6 +178,17 @@ _loki_completion() {
178
178
  fi
179
179
  ;;
180
180
 
181
+ proof)
182
+ if [[ "$cur" == -* ]]; then
183
+ COMPREPLY=( $(compgen -W "--yes --private --hosted --help" -- "$cur") )
184
+ return 0
185
+ fi
186
+ if [[ $cword -eq 2 ]]; then
187
+ COMPREPLY=( $(compgen -W "list show open share" -- "$cur") )
188
+ return 0
189
+ fi
190
+ ;;
191
+
181
192
  monitor)
182
193
  # Complete with directories
183
194
  _filedir -d
@@ -7,7 +7,7 @@ Modules:
7
7
  control: Session control API (start/stop/pause/resume)
8
8
  """
9
9
 
10
- __version__ = "7.8.3"
10
+ __version__ = "7.9.0"
11
11
 
12
12
  # Expose the control app for easy import
13
13
  try:
@@ -7175,6 +7175,96 @@ async def get_escalation(filename: str):
7175
7175
  return PlainTextResponse(content=body, media_type="text/markdown")
7176
7176
 
7177
7177
 
7178
+ # ---------------------------------------------------------------------------
7179
+ # Proof-of-run artifacts (R1, Slice C).
7180
+ #
7181
+ # Directory-layout contract with the generator (autonomy/lib/proof-generator.py,
7182
+ # Slice A): each run's proof lives at
7183
+ # <active project>/.loki/proofs/<run_id>/proof.json
7184
+ # <active project>/.loki/proofs/<run_id>/index.html
7185
+ # This assumption is asserted explicitly so it is checkable: if Slice A emits a
7186
+ # flat layout instead, the /api/proofs routes below return empty / 404 rather
7187
+ # than reading the wrong files. Run-id segments are sanitized and every resolved
7188
+ # path is realpath-contained under .loki/proofs (mirrors the escalations route
7189
+ # guard at the get_escalation handler above).
7190
+ # ---------------------------------------------------------------------------
7191
+ def _proofs_dir() -> _Path:
7192
+ return _get_loki_dir() / "proofs"
7193
+
7194
+
7195
+ def _safe_proof_run_dir(run_id: str) -> _Path:
7196
+ """Resolve and validate a proof run directory, traversal-safe.
7197
+
7198
+ Rejects path separators, NUL, leading dot, and parent references in the
7199
+ run_id, then realpath-contains the result under .loki/proofs. Raises
7200
+ HTTPException(400) on any rejection.
7201
+ """
7202
+ if (not run_id or "/" in run_id or "\\" in run_id or "\x00" in run_id
7203
+ or run_id.startswith(".") or ".." in run_id):
7204
+ raise HTTPException(status_code=400, detail="invalid run id")
7205
+ base = os.path.realpath(str(_proofs_dir()))
7206
+ target = os.path.realpath(os.path.join(base, run_id))
7207
+ if not target.startswith(base + os.sep):
7208
+ raise HTTPException(status_code=400, detail="invalid run id")
7209
+ return _Path(target)
7210
+
7211
+
7212
+ @app.get("/api/proofs")
7213
+ async def list_proofs():
7214
+ """List proof-of-run artifacts for the active project's .loki/proofs/."""
7215
+ proofs_dir = _proofs_dir()
7216
+ items: list[dict] = []
7217
+ try:
7218
+ entries = sorted(proofs_dir.iterdir())
7219
+ except (OSError, FileNotFoundError):
7220
+ return {"proofs": []}
7221
+ for entry in entries:
7222
+ if not entry.is_dir():
7223
+ continue
7224
+ proof_json = entry / "proof.json"
7225
+ if not proof_json.is_file():
7226
+ continue
7227
+ data = _safe_json_read(proof_json, default=None)
7228
+ if not isinstance(data, dict):
7229
+ continue
7230
+ items.append({
7231
+ "run_id": data.get("run_id", entry.name),
7232
+ "generated_at": data.get("generated_at"),
7233
+ "loki_version": data.get("loki_version"),
7234
+ "cost_usd": (data.get("cost") or {}).get("usd"),
7235
+ "files_changed": (data.get("files_changed") or {}).get("count"),
7236
+ "final_verdict": (data.get("council") or {}).get("final_verdict"),
7237
+ "has_html": (entry / "index.html").is_file(),
7238
+ })
7239
+ # Newest first when generated_at is present.
7240
+ items.sort(key=lambda x: (x.get("generated_at") or ""), reverse=True)
7241
+ return {"proofs": items}
7242
+
7243
+
7244
+ @app.get("/api/proofs/{run_id}")
7245
+ async def get_proof(run_id: str):
7246
+ """Return the redacted proof.json for one run."""
7247
+ run_dir = _safe_proof_run_dir(run_id)
7248
+ proof_json = run_dir / "proof.json"
7249
+ if not proof_json.is_file():
7250
+ raise HTTPException(status_code=404, detail=f"proof not found: {run_id}")
7251
+ data = _safe_json_read(proof_json, default=None)
7252
+ if not isinstance(data, dict):
7253
+ raise HTTPException(status_code=500, detail="proof.json unreadable")
7254
+ return JSONResponse(content=data)
7255
+
7256
+
7257
+ @app.get("/api/proofs/{run_id}/html")
7258
+ async def get_proof_html(run_id: str):
7259
+ """Serve the self-contained shareable proof page for one run."""
7260
+ run_dir = _safe_proof_run_dir(run_id)
7261
+ index_html = run_dir / "index.html"
7262
+ if not index_html.is_file():
7263
+ raise HTTPException(status_code=404,
7264
+ detail=f"proof page not found: {run_id}")
7265
+ return FileResponse(str(index_html), media_type="text/html")
7266
+
7267
+
7178
7268
  # ---------------------------------------------------------------------------
7179
7269
  # SPA catch-all: serve index.html for any path not matched by API routes
7180
7270
  # or static asset mounts. This lets the dashboard UI handle client-side routing.
@@ -0,0 +1,119 @@
1
+ <!DOCTYPE html>
2
+ <!--
3
+ Loki Mode - Proofs index (zero-build standalone fallback, R1 Slice C).
4
+
5
+ Self-contained: all CSS inlined, no external resources. Fetches /api/proofs
6
+ and links each entry to /api/proofs/<run_id>/html. This page exists so the
7
+ proofs surface works without knowing the dashboard SPA build source dir.
8
+ -->
9
+ <html lang="en">
10
+ <head>
11
+ <meta charset="utf-8">
12
+ <meta name="viewport" content="width=device-width, initial-scale=1">
13
+ <title>Loki Mode - Proofs of Run</title>
14
+ <style>
15
+ :root {
16
+ --bg: #0f1115; --panel: #171a21; --panel-2: #1d2129; --border: #2a2f3a;
17
+ --text: #e7e9ee; --muted: #9aa1ad; --faint: #6b7280; --accent: #6f7bf7;
18
+ --green: #34d399; --red: #f87171; --amber: #fbbf24;
19
+ --mono: ui-monospace, "SF Mono", "Menlo", "Consolas", monospace;
20
+ --sans: 'Inter', system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
21
+ }
22
+ * { box-sizing: border-box; }
23
+ body { margin: 0; background: var(--bg); color: var(--text); font-family: var(--sans); line-height: 1.5; }
24
+ a { color: var(--accent); text-decoration: none; }
25
+ a:hover { text-decoration: underline; }
26
+ .wrap { max-width: 880px; margin: 0 auto; padding: 40px 20px 80px; }
27
+ .head { display: flex; align-items: baseline; justify-content: space-between; margin-bottom: 8px; }
28
+ h1 { font-size: 24px; font-weight: 650; letter-spacing: -0.3px; margin: 0; }
29
+ .head a { font-size: 13px; }
30
+ .sub { color: var(--muted); font-size: 14px; margin: 0 0 26px; }
31
+ .list { display: flex; flex-direction: column; gap: 12px; }
32
+ .row {
33
+ display: block; background: var(--panel); border: 1px solid var(--border);
34
+ border-radius: 12px; padding: 16px 18px; color: var(--text);
35
+ }
36
+ .row:hover { border-color: var(--accent); text-decoration: none; }
37
+ .row .top { display: flex; align-items: baseline; gap: 12px; flex-wrap: wrap; }
38
+ .row .rid { font-family: var(--mono); font-size: 14px; font-weight: 600; }
39
+ .row .usd { font-family: var(--mono); font-size: 16px; font-weight: 650; }
40
+ .row .ts { color: var(--faint); font-size: 12px; margin-left: auto; }
41
+ .row .meta { color: var(--muted); font-size: 13px; margin-top: 6px; display: flex; gap: 14px; flex-wrap: wrap; }
42
+ .badge { font-size: 12px; font-weight: 600; padding: 2px 8px; border-radius: 6px; border: 1px solid var(--border); }
43
+ .b-approve { color: var(--green); border-color: rgba(52,211,153,0.4); }
44
+ .b-reject { color: var(--red); border-color: rgba(248,113,113,0.4); }
45
+ .b-concern { color: var(--amber); border-color: rgba(251,191,36,0.4); }
46
+ .empty { color: var(--muted); background: var(--panel); border: 1px solid var(--border); border-radius: 12px; padding: 28px; text-align: center; }
47
+ .empty code { font-family: var(--mono); color: var(--text); background: var(--panel-2); padding: 2px 6px; border-radius: 5px; }
48
+ </style>
49
+ </head>
50
+ <body>
51
+ <div class="wrap">
52
+ <div class="head">
53
+ <h1>Proofs of Run</h1>
54
+ <a href="/">Back to dashboard</a>
55
+ </div>
56
+ <p class="sub">Shareable, self-contained proof pages generated at the end of each run. Anyone can open them, no account needed.</p>
57
+ <div id="content"><p class="sub">Loading...</p></div>
58
+ </div>
59
+ <script>
60
+ (function () {
61
+ "use strict";
62
+ function esc(s) {
63
+ s = (s === null || s === undefined) ? "" : String(s);
64
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;")
65
+ .replace(/"/g, "&quot;").replace(/'/g, "&#39;");
66
+ }
67
+ function fmtUsd(n) {
68
+ n = Number(n);
69
+ if (!isFinite(n)) return "";
70
+ var s = n.toFixed(4).replace(/0+$/, "").replace(/\.$/, "");
71
+ if (s.indexOf(".") === -1) s += ".00";
72
+ else if (s.split(".")[1].length === 1) s += "0";
73
+ return "$" + s;
74
+ }
75
+ function badgeClass(v) {
76
+ v = String(v || "").toUpperCase();
77
+ if (v.indexOf("APPROVE") === 0 || v === "PASS" || v === "PASSED") return "b-approve";
78
+ if (v.indexOf("REJECT") === 0 || v.indexOf("BLOCK") === 0 || v === "FAIL") return "b-reject";
79
+ if (v.indexOf("CONCERN") === 0) return "b-concern";
80
+ return "";
81
+ }
82
+ function render(proofs) {
83
+ var c = document.getElementById("content");
84
+ if (!proofs || proofs.length === 0) {
85
+ c.innerHTML = '<div class="empty">No proofs yet. They are generated automatically at the end of a run' +
86
+ ' (set <code>LOKI_PROOF=0</code> to opt out).</div>';
87
+ return;
88
+ }
89
+ var rows = "";
90
+ for (var i = 0; i < proofs.length; i++) {
91
+ var p = proofs[i];
92
+ var rid = esc(p.run_id);
93
+ var usd = (p.cost_usd !== null && p.cost_usd !== undefined) ? fmtUsd(p.cost_usd) : "";
94
+ var fc = (p.files_changed !== null && p.files_changed !== undefined) ? (p.files_changed + " files") : "";
95
+ var verdict = p.final_verdict ? '<span class="badge ' + badgeClass(p.final_verdict) + '">' + esc(p.final_verdict) + "</span>" : "";
96
+ var ver = p.loki_version ? ("v" + esc(p.loki_version)) : "";
97
+ var ts = p.generated_at ? esc(p.generated_at) : "";
98
+ var href = p.has_html ? ("/api/proofs/" + encodeURIComponent(p.run_id) + "/html") : ("/api/proofs/" + encodeURIComponent(p.run_id));
99
+ var meta = [usd ? '<span class="usd">' + usd + "</span>" : "", fc, ver].filter(Boolean).join(" ");
100
+ rows += '<a class="row" href="' + href + '">' +
101
+ '<div class="top"><span class="rid">' + rid + "</span>" + verdict +
102
+ (ts ? '<span class="ts">' + ts + "</span>" : "") + "</div>" +
103
+ (meta ? '<div class="meta">' + meta + "</div>" : "") +
104
+ "</a>";
105
+ }
106
+ c.innerHTML = '<div class="list">' + rows + "</div>";
107
+ }
108
+ function renderError(msg) {
109
+ document.getElementById("content").innerHTML =
110
+ '<div class="empty">Could not load proofs. ' + esc(msg || "") + "</div>";
111
+ }
112
+ fetch("/api/proofs", { headers: { "Accept": "application/json" } })
113
+ .then(function (r) { if (!r.ok) throw new Error("HTTP " + r.status); return r.json(); })
114
+ .then(function (d) { render((d && d.proofs) || []); })
115
+ .catch(function (e) { renderError(e && e.message); });
116
+ })();
117
+ </script>
118
+ </body>
119
+ </html>
@@ -2,7 +2,7 @@
2
2
 
3
3
  The flagship product of [Autonomi](https://www.autonomi.dev/). Complete installation instructions for all platforms and use cases.
4
4
 
5
- **Version:** v7.8.3
5
+ **Version:** v7.9.0
6
6
 
7
7
  ---
8
8