loki-mode 7.8.3 → 7.9.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/SKILL.md +2 -2
- package/VERSION +1 -1
- package/autonomy/lib/proof-generator.py +721 -0
- package/autonomy/lib/proof-template.html +803 -0
- package/autonomy/lib/proof_redact.py +297 -0
- package/autonomy/loki +313 -2
- package/autonomy/run.sh +36 -0
- package/bin/loki +1 -1
- package/completions/_loki +9 -0
- package/completions/loki.bash +12 -1
- package/dashboard/__init__.py +1 -1
- package/dashboard/server.py +90 -0
- package/dashboard/static/proofs.html +119 -0
- package/docs/INSTALLATION.md +1 -1
- package/loki-ts/dist/loki.js +233 -170
- package/mcp/__init__.py +1 -1
- package/package.json +1 -1
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'
|
package/completions/loki.bash
CHANGED
|
@@ -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
|
package/dashboard/__init__.py
CHANGED
package/dashboard/server.py
CHANGED
|
@@ -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, "&").replace(/</g, "<").replace(/>/g, ">")
|
|
65
|
+
.replace(/"/g, """).replace(/'/g, "'");
|
|
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>
|