loki-mode 9.18.4 → 9.20.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/SKILL.md +2 -2
- package/VERSION +1 -1
- package/autonomy/lib/deadline.py +21 -1
- package/autonomy/lib/outcome_contract.py +128 -0
- package/autonomy/lib/proof-generator.py +45 -0
- package/autonomy/lib/proof-verify.py +53 -0
- package/autonomy/loki +980 -24
- package/autonomy/receipt_jwt.py +308 -0
- package/autonomy/run.sh +30 -0
- package/autonomy/trigger-server.py +186 -1
- package/dashboard/__init__.py +1 -1
- package/dashboard/server.py +83 -0
- package/dashboard/static/index.html +144 -0
- package/docs/COMPETITIVE-SCORECARD.md +76 -0
- package/docs/PROOF-PASSPORT.md +29 -0
- package/docs/SIGNED-RECEIPTS.md +111 -0
- package/docs/metrics.md +32 -0
- package/events/emit.sh +31 -0
- package/loki-ts/dist/loki.js +2 -2
- package/mcp/__init__.py +1 -1
- package/package.json +1 -1
- package/plugins/loki-mode/.claude-plugin/plugin.json +1 -1
- package/tools/proof-passport.py +175 -0
- package/web-app/dist/assets/{AdminPage-CcCJ0Sjt.js → AdminPage-DNGNuAge.js} +1 -1
- package/web-app/dist/assets/{Avatar-DK8kmayw.js → Avatar-CqAveJaT.js} +1 -1
- package/web-app/dist/assets/{Badge-4uAWnemi.js → Badge-BtMejA0Z.js} +1 -1
- package/web-app/dist/assets/{Button-BBMk33tk.js → Button-CikxmqSs.js} +1 -1
- package/web-app/dist/assets/{ComparePage-bt9rwvST.js → ComparePage-BB8DJBWR.js} +1 -1
- package/web-app/dist/assets/EvidenceReceiptPanel-6oquKEjd.js +1 -0
- package/web-app/dist/assets/{GitHubIssuesPanel-WDbH47UM.js → GitHubIssuesPanel-5XLYWzWH.js} +1 -1
- package/web-app/dist/assets/{GitHubPRsPanel-C2CiYtTx.js → GitHubPRsPanel-DUfmFYeu.js} +1 -1
- package/web-app/dist/assets/{HomePage-BQk-MUjn.js → HomePage-DZ4MmOdH.js} +5 -5
- package/web-app/dist/assets/{LoginPage-DMOZVGGL.js → LoginPage-VMzI6ROD.js} +1 -1
- package/web-app/dist/assets/{MagicPage-Bzp2Nt1z.js → MagicPage-CFbX5gWg.js} +1 -1
- package/web-app/dist/assets/{MetricsPage-C39JVdsw.js → MetricsPage-DbL6xmRQ.js} +2 -2
- package/web-app/dist/assets/{NotFoundPage-6vT_U9UL.js → NotFoundPage-BUrjXPAR.js} +1 -1
- package/web-app/dist/assets/{ProjectPage-BfFcZp-E.js → ProjectPage-iP6dJihg.js} +3 -3
- package/web-app/dist/assets/{ProjectsPage-CPMBf8Wt.js → ProjectsPage-BibN9bHl.js} +1 -1
- package/web-app/dist/assets/{SettingsPage-BnNN6ETl.js → SettingsPage-C0WJp1hW.js} +1 -1
- package/web-app/dist/assets/{ShowcasePage-WDrMf-cx.js → ShowcasePage-DUv0Njp8.js} +1 -1
- package/web-app/dist/assets/{SystemSettingsPage-DX4jb2e8.js → SystemSettingsPage-BIaCB0Vn.js} +1 -1
- package/web-app/dist/assets/{TeamsPage-BCfqcXzu.js → TeamsPage-BE0szv9r.js} +1 -1
- package/web-app/dist/assets/{TemplatesPage-CZvmimDj.js → TemplatesPage-BzkA9kat.js} +1 -1
- package/web-app/dist/assets/{TerminalOutput-BlRqFwWV.js → TerminalOutput-DBrdT_Fp.js} +1 -1
- package/web-app/dist/assets/{activity-CacZsUyr.js → activity-AbfFz38r.js} +1 -1
- package/web-app/dist/assets/{bell-DK2qtHnk.js → bell-DF6Z6y5U.js} +1 -1
- package/web-app/dist/assets/{bot-CkcUtHad.js → bot-DQ4np_3n.js} +1 -1
- package/web-app/dist/assets/{check-CbCPjX3M.js → check-RWuguCwE.js} +1 -1
- package/web-app/dist/assets/{chevron-left-5NUKWw3i.js → chevron-left-VpbQPOkC.js} +1 -1
- package/web-app/dist/assets/{circle-alert-S7uFoxC2.js → circle-alert-DwT82lJx.js} +1 -1
- package/web-app/dist/assets/{clock-CaQRrIrs.js → clock-BBlUa93G.js} +1 -1
- package/web-app/dist/assets/{cloud-DBAX6c0r.js → cloud-Dgbz3VCA.js} +1 -1
- package/web-app/dist/assets/{code-xml-De5-EXv3.js → code-xml-r6qqION5.js} +1 -1
- package/web-app/dist/assets/{copy-CUkT6k1v.js → copy-DMHj52B8.js} +1 -1
- package/web-app/dist/assets/{database-BAWf1Gwt.js → database-uJqHSHR1.js} +1 -1
- package/web-app/dist/assets/{dollar-sign-Ji8zk86R.js → dollar-sign-BZagljek.js} +1 -1
- package/web-app/dist/assets/{file-code-corner-ChtXoBwS.js → file-code-corner-DMopmhu0.js} +1 -1
- package/web-app/dist/assets/{file-plus-bFa37P76.js → file-plus-DU_hT_3w.js} +1 -1
- package/web-app/dist/assets/{folder-open-DhXpXscO.js → folder-open-BP5TtQAW.js} +1 -1
- package/web-app/dist/assets/{git-commit-horizontal-DVPeDQ3j.js → git-commit-horizontal-zFk_IobS.js} +1 -1
- package/web-app/dist/assets/{globe-BPZgPeeu.js → globe-mthCRtfp.js} +1 -1
- package/web-app/dist/assets/{hammer-jLCaujYH.js → hammer-BWaLBvSA.js} +1 -1
- package/web-app/dist/assets/{index-B-0iHBPO.js → index-BXA3UT6P.js} +3 -3
- package/web-app/dist/assets/{index-BfZSDej1.css → index-DgpUuqwR.css} +1 -1
- package/web-app/dist/assets/{layers-B1vsrsFW.js → layers-Q1LXV5fy.js} +1 -1
- package/web-app/dist/assets/{lightbulb-C-uLoq9Y.js → lightbulb-vRRNJ3dB.js} +1 -1
- package/web-app/dist/assets/{loader-circle-JTfD-ZuM.js → loader-circle-KgjeHEcV.js} +1 -1
- package/web-app/dist/assets/{lock-G9rxD4gZ.js → lock-BerKqno7.js} +1 -1
- package/web-app/dist/assets/{mail-BJ0PTN_V.js → mail-XAxTbprX.js} +1 -1
- package/web-app/dist/assets/{package-CXClfLOO.js → package-DLPKWAvW.js} +1 -1
- package/web-app/dist/assets/{plus-EoL5OCB7.js → plus-NMxxwHMn.js} +1 -1
- package/web-app/dist/assets/{refresh-cw-BjREUnVq.js → refresh-cw-Dt42kYtM.js} +1 -1
- package/web-app/dist/assets/{rotate-ccw-DahWX07H.js → rotate-ccw-BCR4Tp9u.js} +1 -1
- package/web-app/dist/assets/{save-Dek3gCn1.js → save-sR4MJd42.js} +1 -1
- package/web-app/dist/assets/{server-D6V1BAia.js → server-SXkchokx.js} +1 -1
- package/web-app/dist/assets/{shield-alert-BtTK5Sxb.js → shield-alert-Bvgp6_Dz.js} +1 -1
- package/web-app/dist/assets/{trash-2-BT5o_g0r.js → trash-2-D0IGudH7.js} +1 -1
- package/web-app/dist/assets/{trending-down-D4Jk7KF3.js → trending-down-BbFFBq-v.js} +1 -1
- package/web-app/dist/assets/{trending-up-EQFTzhEo.js → trending-up-DDrJKL5i.js} +1 -1
- package/web-app/dist/assets/{upload-JfI5lCSE.js → upload-C54rjN8K.js} +1 -1
- package/web-app/dist/assets/{usePolling-BnhPUuGd.js → usePolling-D7fw4YSM.js} +1 -1
- package/web-app/dist/assets/{user-DSUiUYtj.js → user-Cg7fbzdh.js} +1 -1
- package/web-app/dist/index.html +2 -2
- package/web-app/server.py +78 -0
package/dashboard/server.py
CHANGED
|
@@ -206,6 +206,7 @@ _SENSITIVE_READ_PREFIXES = (
|
|
|
206
206
|
"/api/projects",
|
|
207
207
|
"/api/council",
|
|
208
208
|
"/api/proofs",
|
|
209
|
+
"/api/phases",
|
|
209
210
|
"/api/memory",
|
|
210
211
|
"/api/learnings",
|
|
211
212
|
"/api/learning",
|
|
@@ -9976,6 +9977,71 @@ def _build_metrics_text() -> str:
|
|
|
9976
9977
|
lines.append(f"loki_uptime_seconds {round(uptime_seconds, 1)}")
|
|
9977
9978
|
lines.append("")
|
|
9978
9979
|
|
|
9980
|
+
# -- Evidence Receipts -----------------------------------------------------
|
|
9981
|
+
# The receipt is the product's differentiator and was INVISIBLE to
|
|
9982
|
+
# monitoring: /metrics emitted 12 lines and none of them were about
|
|
9983
|
+
# verification. An operator could not alert on "builds stopped producing
|
|
9984
|
+
# receipts" or "the unknown-verdict share is climbing", which are the two
|
|
9985
|
+
# failures that make the whole guarantee worthless in production.
|
|
9986
|
+
#
|
|
9987
|
+
# `unknown` IS ITS OWN SERIES, deliberately. The summary endpoint refuses
|
|
9988
|
+
# to count a receipt as verified when it cannot prove it was, and folding
|
|
9989
|
+
# that into either verified or not_verified would launder the distinction
|
|
9990
|
+
# the receipt exists to preserve.
|
|
9991
|
+
#
|
|
9992
|
+
# Bucketed on honesty.headline exactly as proofs_summary does, reading the
|
|
9993
|
+
# same files, so the metric and the API cannot drift.
|
|
9994
|
+
_proof_total = _proof_verified = _proof_gaps = _proof_notver = _proof_unknown = 0
|
|
9995
|
+
_proof_attested = _proof_signed = 0
|
|
9996
|
+
try:
|
|
9997
|
+
_pdir = loki_dir / "proofs"
|
|
9998
|
+
_entries = sorted(_pdir.iterdir()) if _pdir.is_dir() else []
|
|
9999
|
+
except OSError:
|
|
10000
|
+
_entries = []
|
|
10001
|
+
for _entry in _entries:
|
|
10002
|
+
if not _entry.is_dir():
|
|
10003
|
+
continue
|
|
10004
|
+
_pj = _entry / "proof.json"
|
|
10005
|
+
if not _pj.is_file():
|
|
10006
|
+
continue
|
|
10007
|
+
_data = _safe_json_read(_pj, default=None)
|
|
10008
|
+
if not isinstance(_data, dict):
|
|
10009
|
+
continue
|
|
10010
|
+
_proof_total += 1
|
|
10011
|
+
_honesty = _data.get("honesty")
|
|
10012
|
+
_head = _honesty.get("headline") if isinstance(_honesty, dict) else None
|
|
10013
|
+
if _head == "VERIFIED":
|
|
10014
|
+
_proof_verified += 1
|
|
10015
|
+
elif _head == "VERIFIED WITH GAPS":
|
|
10016
|
+
_proof_gaps += 1
|
|
10017
|
+
elif _head == "NOT VERIFIED":
|
|
10018
|
+
_proof_notver += 1
|
|
10019
|
+
else:
|
|
10020
|
+
_proof_unknown += 1
|
|
10021
|
+
_ver = _data.get("verification")
|
|
10022
|
+
if isinstance(_ver, dict):
|
|
10023
|
+
if _ver.get("attestation"):
|
|
10024
|
+
_proof_attested += 1
|
|
10025
|
+
if _ver.get("gpg_signature"):
|
|
10026
|
+
_proof_signed += 1
|
|
10027
|
+
|
|
10028
|
+
lines.append("# HELP loki_receipts_total Evidence Receipts on disk")
|
|
10029
|
+
lines.append("# TYPE loki_receipts_total gauge")
|
|
10030
|
+
lines.append(f"loki_receipts_total {_proof_total}")
|
|
10031
|
+
lines.append("")
|
|
10032
|
+
lines.append("# HELP loki_receipts_by_verdict Receipts bucketed on the recorded honesty headline")
|
|
10033
|
+
lines.append("# TYPE loki_receipts_by_verdict gauge")
|
|
10034
|
+
lines.append(f'loki_receipts_by_verdict{{verdict="verified"}} {_proof_verified}')
|
|
10035
|
+
lines.append(f'loki_receipts_by_verdict{{verdict="with_gaps"}} {_proof_gaps}')
|
|
10036
|
+
lines.append(f'loki_receipts_by_verdict{{verdict="not_verified"}} {_proof_notver}')
|
|
10037
|
+
lines.append(f'loki_receipts_by_verdict{{verdict="unknown"}} {_proof_unknown}')
|
|
10038
|
+
lines.append("")
|
|
10039
|
+
lines.append("# HELP loki_receipts_with_provenance Receipts carrying a checkable provenance record")
|
|
10040
|
+
lines.append("# TYPE loki_receipts_with_provenance gauge")
|
|
10041
|
+
lines.append(f'loki_receipts_with_provenance{{kind="attestation"}} {_proof_attested}')
|
|
10042
|
+
lines.append(f'loki_receipts_with_provenance{{kind="gpg"}} {_proof_signed}')
|
|
10043
|
+
lines.append("")
|
|
10044
|
+
|
|
9979
10045
|
return "\n".join(lines) + "\n"
|
|
9980
10046
|
|
|
9981
10047
|
|
|
@@ -12226,6 +12292,23 @@ def _proof_pr_url(run_dir: _Path) -> Optional[str]:
|
|
|
12226
12292
|
return None
|
|
12227
12293
|
|
|
12228
12294
|
|
|
12295
|
+
@app.get("/api/phases", dependencies=[Depends(auth.require_scope("read"))])
|
|
12296
|
+
async def phase_timeline():
|
|
12297
|
+
"""Measured phase segments for the active run, from real phase_change events.
|
|
12298
|
+
|
|
12299
|
+
dashboard/api_phases.py had NO importer and NO consumer: a complete
|
|
12300
|
+
phase-timeline module, tested and unreachable. This route is the wiring.
|
|
12301
|
+
|
|
12302
|
+
The envelope is returned VERBATIM. Reshaping it here would fork the honesty
|
|
12303
|
+
contract the module carries -- an unmeasured start is None (never 0), an
|
|
12304
|
+
unreadable log reads differently from an empty one, and `sampled` says the
|
|
12305
|
+
history can be missing phases shorter than one poll interval. A second
|
|
12306
|
+
format is a second place for those distinctions to die.
|
|
12307
|
+
"""
|
|
12308
|
+
from . import api_phases
|
|
12309
|
+
return api_phases.phase_history(str(_get_loki_dir()))
|
|
12310
|
+
|
|
12311
|
+
|
|
12229
12312
|
@app.get("/api/proofs", dependencies=[Depends(auth.require_scope("read"))])
|
|
12230
12313
|
async def list_proofs():
|
|
12231
12314
|
"""List proof-of-run artifacts for the active project's .loki/proofs/."""
|
|
@@ -1294,6 +1294,17 @@
|
|
|
1294
1294
|
<loki-spec-panel id="spec-panel"></loki-spec-panel>
|
|
1295
1295
|
</div>
|
|
1296
1296
|
<loki-overview id="overview"></loki-overview>
|
|
1297
|
+
<!-- PIPELINE. dashboard/api_phases.py parsed real phase_change events
|
|
1298
|
+
into measured segments and had NO importer in server.py and NO
|
|
1299
|
+
consumer here: a user could see a build finish or not, never WHICH
|
|
1300
|
+
PHASE it was in or where it was stuck. Lives on Overview because
|
|
1301
|
+
this SPA has no Activity section (no data-section="activity"), and
|
|
1302
|
+
Overview is the live-run home. Hidden until data arrives. -->
|
|
1303
|
+
<div id="pipeline-panel" style="display:none;margin-bottom:24px;">
|
|
1304
|
+
<h3 style="font-family: var(--loki-font-family, 'Inter', system-ui, -apple-system, sans-serif); font-size: 1.15rem; font-weight: 400; color: var(--loki-text-primary); margin-bottom: 12px;">Pipeline</h3>
|
|
1305
|
+
<div id="pipeline-list"></div>
|
|
1306
|
+
<div id="pipeline-note" style="font-size:11px;color:var(--loki-text-muted);margin-top:8px;"></div>
|
|
1307
|
+
</div>
|
|
1297
1308
|
<loki-rarv-timeline id="rarv-timeline"></loki-rarv-timeline>
|
|
1298
1309
|
<loki-session-diff id="session-diff"></loki-session-diff>
|
|
1299
1310
|
<div style="margin-top: 28px;">
|
|
@@ -16184,6 +16195,127 @@ document.addEventListener('DOMContentLoaded', function() {
|
|
|
16184
16195
|
.catch(function () { /* leave hidden: no receipt surface is better than a wrong one */ });
|
|
16185
16196
|
};
|
|
16186
16197
|
|
|
16198
|
+
// PIPELINE. The measured phase timeline from /api/phases, which serves
|
|
16199
|
+
// dashboard/api_phases.py's envelope verbatim.
|
|
16200
|
+
//
|
|
16201
|
+
// WHAT IS NOT HERE, DELIBERATELY: a "not started" row. The envelope only
|
|
16202
|
+
// contains phases that HAVE started, and the runtime declares no canonical
|
|
16203
|
+
// ordered phase list -- _advance_current_phase takes any string, and the
|
|
16204
|
+
// envelope's own "sampled" flag says absence of a segment is not evidence
|
|
16205
|
+
// the phase did not occur. Drawing a fixed pipeline and marking the missing
|
|
16206
|
+
// rows "not started" would invent exactly the fiction api_phases.py was
|
|
16207
|
+
// written to replace (the old timeline rotated a hardcoded four-phase list
|
|
16208
|
+
// and gave each invented segment a Math.random() duration). The three
|
|
16209
|
+
// states we CAN measure are rendered distinctly, which is what keeps a
|
|
16210
|
+
// stalled build from reading as a healthy one:
|
|
16211
|
+
//
|
|
16212
|
+
// ENDED start and end both measured -- a real duration
|
|
16213
|
+
// RUNNING ongoing, no recorded end; elapsed is anchored to the server's
|
|
16214
|
+
// checked_at, never to the client clock
|
|
16215
|
+
// FAILED the phase the runtime itself named FAILED
|
|
16216
|
+
//
|
|
16217
|
+
// and a fourth the envelope reports separately: the leading phase, known to
|
|
16218
|
+
// have run but with no emitted start, so its duration is "not measured".
|
|
16219
|
+
//
|
|
16220
|
+
// An unmeasured duration renders "not measured", NEVER 0. A measured zero
|
|
16221
|
+
// (two events inside one second -- timestamps are second-granular) still
|
|
16222
|
+
// renders as 0s, so the guard is on the ENDPOINTS being absent, never on
|
|
16223
|
+
// the duration being falsy.
|
|
16224
|
+
window.loadPhases = function () {
|
|
16225
|
+
var panel = document.getElementById('pipeline-panel');
|
|
16226
|
+
var list = document.getElementById('pipeline-list');
|
|
16227
|
+
var note = document.getElementById('pipeline-note');
|
|
16228
|
+
if (!panel || !list) return;
|
|
16229
|
+
fetch('/api/phases', { headers: { 'Accept': 'application/json' } })
|
|
16230
|
+
.then(function (r) { return r.ok ? r.json() : null; })
|
|
16231
|
+
.then(function (d) {
|
|
16232
|
+
if (!d) return; // endpoint absent: leave hidden
|
|
16233
|
+
var rows = (d.segments || []).slice();
|
|
16234
|
+
if (d.leading_phase) rows.unshift(d.leading_phase);
|
|
16235
|
+
if (!rows.length) return; // no phase history: say nothing
|
|
16236
|
+
var html = '';
|
|
16237
|
+
for (var i = 0; i < rows.length; i++) {
|
|
16238
|
+
var x = rows[i] || {};
|
|
16239
|
+
// ESCAPED, unlike the verdict in loadReceipts. That comes from a
|
|
16240
|
+
// controlled set; a phase name does not -- api_phases.py passes
|
|
16241
|
+
// names through verbatim and _advance_current_phase accepts ANY
|
|
16242
|
+
// string, so the set is open and this is untrusted text going into
|
|
16243
|
+
// innerHTML on a dashboard that can be bound remotely.
|
|
16244
|
+
var raw = String(x.phase || 'UNKNOWN');
|
|
16245
|
+
var _d = document.createElement('div');
|
|
16246
|
+
_d.textContent = raw;
|
|
16247
|
+
var phase = _d.innerHTML;
|
|
16248
|
+
var failed = /^FAIL/i.test(raw);
|
|
16249
|
+
// The end a RUNNING segment is measured against is the server's
|
|
16250
|
+
// own checked_at. A client clock would drift against the log.
|
|
16251
|
+
var end = x.end;
|
|
16252
|
+
if (end === null || end === undefined) {
|
|
16253
|
+
if (x.ongoing) end = d.checked_at;
|
|
16254
|
+
}
|
|
16255
|
+
// Gate on the endpoints, not on the duration: end - start === 0 is
|
|
16256
|
+
// a real measurement (second-granular timestamps), and hiding it
|
|
16257
|
+
// as "-" would be the same lie in the other direction.
|
|
16258
|
+
var measured = (x.start !== null && x.start !== undefined
|
|
16259
|
+
&& end !== null && end !== undefined);
|
|
16260
|
+
var secs = measured ? Math.max(0, Math.round(end - x.start)) : null;
|
|
16261
|
+
var dur = measured
|
|
16262
|
+
? (secs < 60 ? (secs + 's')
|
|
16263
|
+
: (Math.floor(secs / 60) + 'm ' + (secs % 60) + 's'))
|
|
16264
|
+
: 'not measured';
|
|
16265
|
+
var state, col;
|
|
16266
|
+
if (failed) {
|
|
16267
|
+
state = 'FAILED'; col = 'var(--loki-error)';
|
|
16268
|
+
} else if (x.ongoing) {
|
|
16269
|
+
state = 'RUNNING'; col = 'var(--loki-warning)';
|
|
16270
|
+
} else if (x.start === null || x.start === undefined) {
|
|
16271
|
+
// The leading phase: it ran, but its start was never emitted, so
|
|
16272
|
+
// it has no coordinate on a time axis. Named, not drawn.
|
|
16273
|
+
state = 'RAN (start not recorded)'; col = 'var(--loki-text-muted)';
|
|
16274
|
+
} else {
|
|
16275
|
+
state = 'ENDED'; col = 'var(--loki-text-muted)';
|
|
16276
|
+
}
|
|
16277
|
+
var iter = (x.iteration === null || x.iteration === undefined)
|
|
16278
|
+
? '-' : ('iter ' + x.iteration);
|
|
16279
|
+
html += '<div style="display:flex;gap:12px;align-items:center;padding:6px 8px;'
|
|
16280
|
+
+ 'border-bottom:1px solid var(--loki-border);font-size:12px;">'
|
|
16281
|
+
+ '<span style="font-weight:600;min-width:130px;">' + phase + '</span>'
|
|
16282
|
+
+ '<span style="color:' + col + ';min-width:170px;">' + state + '</span>'
|
|
16283
|
+
+ '<span style="min-width:90px;">' + dur + '</span>'
|
|
16284
|
+
+ '<span style="color:var(--loki-text-muted);">' + iter + '</span>'
|
|
16285
|
+
+ '</div>';
|
|
16286
|
+
}
|
|
16287
|
+
list.innerHTML = html;
|
|
16288
|
+
if (note) {
|
|
16289
|
+
// The envelope's own caveats, surfaced. Sampled data presented as
|
|
16290
|
+
// complete is a claim the module explicitly refuses to make.
|
|
16291
|
+
var parts = [];
|
|
16292
|
+
// The VIEW's own age, not just the data's. A one-shot fetch of a
|
|
16293
|
+
// LIVE pipeline freezes: "TESTING RUNNING 1m 40s" would sit there
|
|
16294
|
+
// unchanged an hour later, which is the stalled-build-reads-as-
|
|
16295
|
+
// healthy shape arriving through the view instead of the data.
|
|
16296
|
+
// Polled below, and stamped here so a frozen row is self-labelling
|
|
16297
|
+
// even if the poll dies.
|
|
16298
|
+
if (d.checked_at) {
|
|
16299
|
+
parts.push('Measured as of '
|
|
16300
|
+
+ new Date(d.checked_at * 1000).toISOString().slice(11, 19) + 'Z.');
|
|
16301
|
+
}
|
|
16302
|
+
if (d.sampled) {
|
|
16303
|
+
parts.push('Sampled: the phase recorder polls, so a phase shorter '
|
|
16304
|
+
+ 'than one poll interval left no event and is absent here.');
|
|
16305
|
+
}
|
|
16306
|
+
if (d.freshness_s === null || d.freshness_s === undefined) {
|
|
16307
|
+
parts.push('Age of this data: not measured.');
|
|
16308
|
+
} else if (d.freshness_s > 120) {
|
|
16309
|
+
parts.push('STALE: the phase log was last written '
|
|
16310
|
+
+ Math.floor(d.freshness_s / 60) + 'm ago.');
|
|
16311
|
+
}
|
|
16312
|
+
note.textContent = parts.join(' ');
|
|
16313
|
+
}
|
|
16314
|
+
panel.style.display = 'block';
|
|
16315
|
+
})
|
|
16316
|
+
.catch(function () { /* leave hidden: no pipeline is better than a wrong one */ });
|
|
16317
|
+
};
|
|
16318
|
+
|
|
16187
16319
|
function poll() {
|
|
16188
16320
|
fetch('/api/proofs/summary', { headers: { 'Accept': 'application/json' } })
|
|
16189
16321
|
.then(function (r) { return r.ok ? r.json() : null; })
|
|
@@ -16228,6 +16360,18 @@ document.addEventListener('DOMContentLoaded', function() {
|
|
|
16228
16360
|
// document and cannot see the SPA's manual data-loki-theme toggle, so we
|
|
16229
16361
|
// pass the resolved theme as a query param (?theme=dark|light); the
|
|
16230
16362
|
// standalone page reads it and matches. v7.18.0.
|
|
16363
|
+
// The pipeline is a LIVE view, so unlike loadReceipts (a history view where
|
|
16364
|
+
// one-shot is correct) it polls. Guarded on the Overview page being the
|
|
16365
|
+
// visible one so a user sitting on another section spends no requests.
|
|
16366
|
+
if (sectionId === 'overview') {
|
|
16367
|
+
loadPhases();
|
|
16368
|
+
if (!window._lokiPhasePoll) {
|
|
16369
|
+
window._lokiPhasePoll = setInterval(function () {
|
|
16370
|
+
var pg = document.getElementById('page-overview');
|
|
16371
|
+
if (pg && pg.classList.contains('active')) loadPhases();
|
|
16372
|
+
}, 15000);
|
|
16373
|
+
}
|
|
16374
|
+
}
|
|
16231
16375
|
if (sectionId === 'insights') { loadLearnings(); }
|
|
16232
16376
|
if (sectionId === 'cost') { loadBudget(); }
|
|
16233
16377
|
if (sectionId === 'trust') {
|
|
@@ -397,6 +397,82 @@ bash tests/test-competitor-verify-surface.sh # the installed-CLI a
|
|
|
397
397
|
loki proof verify <id> # re-hash a receipt, exit 1 on tamper
|
|
398
398
|
```
|
|
399
399
|
|
|
400
|
+
### 3d. Our own numbers on the axes people ask about, measured 2026-08-08
|
|
401
|
+
|
|
402
|
+
These are ABSOLUTE measurements of this product with a CI guard behind each. They
|
|
403
|
+
are deliberately NOT presented as a comparison: no competitor number appears
|
|
404
|
+
here, because three of the four named competitors have no runnable binary on
|
|
405
|
+
this machine and the fourth exposes no equivalent command. A number next to a
|
|
406
|
+
blank is not a ratio.
|
|
407
|
+
|
|
408
|
+
| Axis | Measured | Budget | Guarded by |
|
|
409
|
+
|---|---|---|---|
|
|
410
|
+
| UI page weight | 787 KB | 1024 KB | `tests/test-dashboard-bundle-budget.sh` |
|
|
411
|
+
| Backend verify latency | 7 ms | under 1000 ms | `tests/test-fast-verify.sh` |
|
|
412
|
+
| Worker horizontal scale | 1..25 replicas render | n/a | `tests/test-helm-worker-scaling.sh` |
|
|
413
|
+
|
|
414
|
+
Two of those three guards already existed; only the page-weight budget was added
|
|
415
|
+
on 2026-08-08. Recording that because "we measured it" and "we added a
|
|
416
|
+
measurement" are different claims and the difference matters.
|
|
417
|
+
|
|
418
|
+
What these numbers do NOT establish: they say nothing about output quality,
|
|
419
|
+
which is the axis a user actually cares about, and nothing about how any
|
|
420
|
+
competitor performs on the same axes. A fast gate that verifies the wrong thing
|
|
421
|
+
is worse than a slow one that verifies the right thing.
|
|
422
|
+
|
|
423
|
+
Reproduce:
|
|
424
|
+
|
|
425
|
+
```bash
|
|
426
|
+
bash tests/test-dashboard-bundle-budget.sh # prints the KB and the budget
|
|
427
|
+
bash tests/test-fast-verify.sh # prints the ms and the budget
|
|
428
|
+
bash tests/test-helm-worker-scaling.sh # renders 1, 2, 5 and 25 replicas
|
|
429
|
+
```
|
|
430
|
+
|
|
431
|
+
### 3c. Deploy surface, measured 2026-08-08
|
|
432
|
+
|
|
433
|
+
Ran on the four competitor CLIs installed on this machine. Method matters here:
|
|
434
|
+
a `--help` grep is NOT sufficient -- an earlier audit in this repo counted
|
|
435
|
+
aider's `--verify-ssl` as a verification capability, which it is not. So each
|
|
436
|
+
CLI was invoked as `<cli> deploy --help` and the result classified as a real
|
|
437
|
+
subcommand only when it exited 0 AND its output named the subcommand, rather
|
|
438
|
+
than falling through to generic help.
|
|
439
|
+
|
|
440
|
+
| CLI | `deploy` subcommand |
|
|
441
|
+
|---|---|
|
|
442
|
+
| claude | none `[measured]` |
|
|
443
|
+
| aider | none `[measured]` |
|
|
444
|
+
| opencode | none `[measured]` |
|
|
445
|
+
| codex | none `[measured]` |
|
|
446
|
+
| loki | present, and evidence-gated `[measured]` |
|
|
447
|
+
|
|
448
|
+
**4 of 4 installed competitor CLIs expose no deploy verb.** Loki's is gated on a
|
|
449
|
+
verified receipt: it refuses on UNSIGNED, TAMPERED, anchor mismatch, or a dirty
|
|
450
|
+
tree, and writes a receipt for the deploy itself naming the authorizing run_id.
|
|
451
|
+
|
|
452
|
+
Scope limits, same as everywhere else in this file. This covers CLIs INSTALLED
|
|
453
|
+
HERE. Factory, Devin, Replit and 8090 have no runnable binary on this machine
|
|
454
|
+
(`bash benchmarks/head-to-head-readiness.sh` reports 3 ready / 4 blocked), so no
|
|
455
|
+
claim is made about them. A hosted product may deploy without exposing a CLI
|
|
456
|
+
verb -- Replit Agent's documented headline capability is exactly that. This
|
|
457
|
+
measures a CLI surface, not a product capability, and it is not a quality
|
|
458
|
+
comparison.
|
|
459
|
+
|
|
460
|
+
Reproduce:
|
|
461
|
+
|
|
462
|
+
```bash
|
|
463
|
+
# Exit code ALONE is not the test: every CLI here exits 0 on `deploy --help`
|
|
464
|
+
# because an unknown subcommand falls through to generic help. The first
|
|
465
|
+
# version of this recipe did exactly that and reported all four as having a
|
|
466
|
+
# deploy command. The output must NAME the subcommand.
|
|
467
|
+
for c in claude aider opencode codex loki; do
|
|
468
|
+
out="$($c deploy --help 2>&1 | head -3)"
|
|
469
|
+
printf '%s' "$out" | grep -qiE "$c deploy|deploy -" \
|
|
470
|
+
&& echo "$c: real deploy subcommand" \
|
|
471
|
+
|| echo "$c: no deploy subcommand (fell through to generic help)"
|
|
472
|
+
done
|
|
473
|
+
bash autonomy/loki deploy --execute # refuses, naming each failed check
|
|
474
|
+
```
|
|
475
|
+
|
|
400
476
|
## 4. What we do NOT know
|
|
401
477
|
|
|
402
478
|
This section is mandatory and is not empty.
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# Proof Passport
|
|
2
|
+
|
|
3
|
+
Proof Passport binds a provider-neutral Outcome Contract to an existing Loki
|
|
4
|
+
receipt. The JSON artifact preserves the exact contract and receipt digests,
|
|
5
|
+
the verifier verdict, executor/verifier separation, and explicit trust
|
|
6
|
+
limitations.
|
|
7
|
+
|
|
8
|
+
Generate the machine-readable passport and a portable PR/CI summary together:
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
python3 tools/proof-passport.py \
|
|
12
|
+
outcome-contract.json proof.json proof-passport.json \
|
|
13
|
+
--repo-dir . \
|
|
14
|
+
--markdown-output proof-passport.md
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
The command exits `0` only for `VERIFIED`, `1` for `FAILED`, and `2` for
|
|
18
|
+
`UNVERIFIABLE` or invalid input. Existing output files are never replaced
|
|
19
|
+
unless `--force` is supplied. Both outputs are checked before either is
|
|
20
|
+
written, so a conflicting Markdown path cannot leave a new JSON artifact
|
|
21
|
+
behind.
|
|
22
|
+
|
|
23
|
+
The Markdown artifact is suitable for a pull-request comment or
|
|
24
|
+
`$GITHUB_STEP_SUMMARY`. It contains the full SHA-256 evidence bindings and
|
|
25
|
+
trust limitations, but no repository or machine-specific paths.
|
|
26
|
+
|
|
27
|
+
Proof Passport does not currently sign the passport itself. Receipt signature
|
|
28
|
+
status is reported separately and must not be interpreted as a passport
|
|
29
|
+
signature.
|
package/docs/SIGNED-RECEIPTS.md
CHANGED
|
@@ -94,9 +94,120 @@ which exact code" without asking anyone to trust the agent that produced it.
|
|
|
94
94
|
- If the signing key is compromised, signed receipts from that key are worth
|
|
95
95
|
exactly what the key is worth. Normal key hygiene applies.
|
|
96
96
|
|
|
97
|
+
## Attestation: provenance without a key exchange
|
|
98
|
+
|
|
99
|
+
gpg settles the local case, where the operator already holds the keyring. It
|
|
100
|
+
does not survive the remote path. A submitter who ran `loki start --remote`
|
|
101
|
+
never had access to the cluster, so "import the publisher's public key" is the
|
|
102
|
+
step where independent verification stops happening in practice.
|
|
103
|
+
|
|
104
|
+
A receipt served by `trigger-server.py` therefore also carries an **attestation**
|
|
105
|
+
under `verification.attestation`: an Ed25519 JWT signed by the receiver, with
|
|
106
|
+
the signing keys published, unauthenticated, at `/.well-known/jwks.json`.
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
# Third party, offline. Needs proof.json and jwks.json -- nothing else.
|
|
110
|
+
curl -s https://loki.example.com/.well-known/jwks.json > jwks.json
|
|
111
|
+
loki proof verify <id> --jwks ./jwks.json
|
|
112
|
+
|
|
113
|
+
# Or fetch the key set directly from the server that produced the receipt.
|
|
114
|
+
loki proof verify <id> --jwks https://loki.example.com
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Four outcomes, deliberately kept distinct:
|
|
118
|
+
|
|
119
|
+
| Verdict | Meaning |
|
|
120
|
+
|---|---|
|
|
121
|
+
| `VERIFIED` | The token is valid **and** covers these exact bytes. |
|
|
122
|
+
| `FAILED` | The token does not cover these bytes: altered after signing, or lifted from another run. |
|
|
123
|
+
| `ABSENT` | This receipt carries no attestation. A fact about the receipt. |
|
|
124
|
+
| `NOT CHECKED` | The key set could not be read. **Not** a verdict, in either direction. |
|
|
125
|
+
|
|
126
|
+
`ABSENT` and `NOT CHECKED` are never collapsed. One is a property of the
|
|
127
|
+
receipt; the other is an absent measurement, and reporting an unchecked receipt
|
|
128
|
+
as merely unattested would overstate what was established.
|
|
129
|
+
|
|
130
|
+
The JWT binds `job_id`, `run_id` and `receipt_sha256`, and the verifier
|
|
131
|
+
**recomputes** that digest from the receipt body rather than trusting the
|
|
132
|
+
recorded `verification.hash`. Editing the body and rewriting the hash to match
|
|
133
|
+
therefore still fails: the signed claim is the anchor, not the field in the file.
|
|
134
|
+
|
|
135
|
+
### Key rotation
|
|
136
|
+
|
|
137
|
+
Every token carries a `kid`, and JWKS serves the active key plus any retired
|
|
138
|
+
ones (`LOKI_RECEIPT_RETIRED_PUBKEYS`, a colon-separated list of PEM paths). This
|
|
139
|
+
is required rather than optional: with a single unlabeled key, the first
|
|
140
|
+
rotation would make every previously-issued receipt fail verification -- and a
|
|
141
|
+
receipt that stops verifying is indistinguishable from a tampered one.
|
|
142
|
+
|
|
143
|
+
### In a Kubernetes cluster
|
|
144
|
+
|
|
145
|
+
The Helm chart wires this for you. Generate a key and pass it as a file:
|
|
146
|
+
|
|
147
|
+
```bash
|
|
148
|
+
openssl genpkey -algorithm ed25519 -out receipt-signing-key.pem
|
|
149
|
+
helm upgrade --install loki ./helm/loki-mode \
|
|
150
|
+
--set-file secrets.receiptSigningKey=receipt-signing-key.pem
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
The chart mounts it **into the receiver only**, read-only at mode `0400`, and
|
|
154
|
+
sets `LOKI_RECEIPT_SIGNING_KEY_FILE` to the projected path. The worker never
|
|
155
|
+
receives it, and that asymmetry is enforced by the chart rather than left to
|
|
156
|
+
convention: a worker runs model-directed code, so a key there would let a build
|
|
157
|
+
sign its own receipt.
|
|
158
|
+
|
|
159
|
+
Left unset, the receiver serves receipts unsigned and `/.well-known/jwks.json`
|
|
160
|
+
returns an empty key set -- honest, but a `--remote` submitter then has no way
|
|
161
|
+
to prove who produced their receipt without an out-of-band key import.
|
|
162
|
+
|
|
163
|
+
### With docker-compose
|
|
164
|
+
|
|
165
|
+
Opt-in, and deliberately shipped commented out. Docker has no optional bind
|
|
166
|
+
mount: a bind to a missing path is a hard container start failure, so enabling
|
|
167
|
+
it by default would stop the receiver from starting for everyone who has not
|
|
168
|
+
generated a key.
|
|
169
|
+
|
|
170
|
+
```bash
|
|
171
|
+
openssl genpkey -algorithm ed25519 -out ./receipt-signing-key.pem
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
Then uncomment the two lines the compose file points at (the
|
|
175
|
+
`LOKI_RECEIPT_SIGNING_KEY_FILE` env var and the `receipt-signing-key.pem`
|
|
176
|
+
mount) on the `receiver` service. Never commit that key file.
|
|
177
|
+
|
|
178
|
+
### Configuration
|
|
179
|
+
|
|
180
|
+
| Variable | Effect |
|
|
181
|
+
|---|---|
|
|
182
|
+
| `LOKI_RECEIPT_SIGNING_KEY_FILE` | PEM path to the Ed25519 private key (normal Kubernetes mounted-secret path). |
|
|
183
|
+
| `LOKI_RECEIPT_SIGNING_KEY` | The PEM inline, for non-Kubernetes deployments. |
|
|
184
|
+
| `LOKI_RECEIPT_RETIRED_PUBKEYS` | Colon-separated PEM paths for retired public keys. |
|
|
185
|
+
|
|
186
|
+
Unset means unsigned: no attestation is attached and the receipt keeps its
|
|
187
|
+
existing verdict.
|
|
188
|
+
|
|
189
|
+
The same two variables also work for a **local** build. Set
|
|
190
|
+
`LOKI_RECEIPT_SIGNING_KEY_FILE` before `loki start` and the generator attests
|
|
191
|
+
the receipt directly, so a laptop or CI receipt is checkable by the same
|
|
192
|
+
`--jwks` path a cluster receipt uses. Publish the matching JWKS wherever your
|
|
193
|
+
consumers can reach it (or hand them `jwks.json` alongside the receipt).
|
|
194
|
+
|
|
195
|
+
**In a cluster, the receiver signs -- never the worker.** A worker runs
|
|
196
|
+
model-directed code and already holds provider credentials, so a key there would
|
|
197
|
+
let a build sign its own receipt, which attests to nothing. The local case is
|
|
198
|
+
different in kind, not an exception to this: there is no separation between
|
|
199
|
+
submitter and builder on your own machine, and the attestation says "this
|
|
200
|
+
receipt came from a holder of this key" rather than "an independent party
|
|
201
|
+
witnessed this build."
|
|
202
|
+
|
|
97
203
|
## Source
|
|
98
204
|
|
|
99
205
|
- Signing: `autonomy/lib/proof-generator.py` (`_gpg_detached_sign`, and the
|
|
100
206
|
`LOKI_PROOF_GPG_KEY` gate)
|
|
101
207
|
- Verification: `autonomy/lib/proof-verify.py` (`_verify_gpg`)
|
|
102
208
|
- Scope test: `tests/test-proof-forgery-defense.sh`
|
|
209
|
+
- Attestation: `autonomy/receipt_jwt.py`; served by `autonomy/trigger-server.py`
|
|
210
|
+
(`_attest`, `_handle_jwks`); checked by `loki_proof_attestation_check` and
|
|
211
|
+
`loki_remote_attestation_status` in `autonomy/loki`
|
|
212
|
+
- Attestation tests: `tests/test-receipt-jwt-attestation.sh`,
|
|
213
|
+
`tests/test-remote-attestation-verdict.sh`, `tests/test-proof-verify-jwks.sh`
|
package/docs/metrics.md
CHANGED
|
@@ -74,6 +74,38 @@ Returns metrics in OpenMetrics text format. No authentication required by defaul
|
|
|
74
74
|
|--------|------|-------------|
|
|
75
75
|
| `loki_events_total` | counter | Total number of events recorded in events.jsonl |
|
|
76
76
|
|
|
77
|
+
### Evidence Receipt Metrics
|
|
78
|
+
|
|
79
|
+
The verification surface, exposed so an operator can alert on it. Two failures
|
|
80
|
+
make the receipt guarantee worthless in production and neither was observable
|
|
81
|
+
before these existed: builds silently stopping producing receipts, and the
|
|
82
|
+
`unknown` share climbing.
|
|
83
|
+
|
|
84
|
+
| Metric | Type | Description |
|
|
85
|
+
|--------|------|-------------|
|
|
86
|
+
| `loki_receipts_total` | gauge | Evidence Receipts on disk for the active project |
|
|
87
|
+
| `loki_receipts_by_verdict{verdict=...}` | gauge | Receipts bucketed on the recorded honesty headline: `verified`, `with_gaps`, `not_verified`, `unknown` |
|
|
88
|
+
| `loki_receipts_with_provenance{kind=...}` | gauge | Receipts carrying a checkable provenance record: `attestation` (Ed25519 JWT) or `gpg` |
|
|
89
|
+
|
|
90
|
+
`unknown` IS ITS OWN SERIES and must stay that way. The summary refuses to
|
|
91
|
+
count a receipt as verified when it cannot prove it was -- schema v1.0 receipts
|
|
92
|
+
carry no honesty block -- so folding that bucket into `verified` is dishonest
|
|
93
|
+
and folding it into `not_verified` is alarming and also wrong.
|
|
94
|
+
|
|
95
|
+
These read the same files as `GET /api/proofs/summary`, so the metric and the
|
|
96
|
+
API cannot disagree; a test asserts it. An empty project reports `0`, not an
|
|
97
|
+
error, so a fresh install does not look like a broken exporter.
|
|
98
|
+
|
|
99
|
+
Example alerts:
|
|
100
|
+
|
|
101
|
+
```
|
|
102
|
+
# receipts stopped being produced
|
|
103
|
+
increase(loki_receipts_total[6h]) == 0
|
|
104
|
+
|
|
105
|
+
# the unverifiable share is growing
|
|
106
|
+
loki_receipts_by_verdict{verdict="unknown"} / loki_receipts_total > 0.2
|
|
107
|
+
```
|
|
108
|
+
|
|
77
109
|
## Data Sources
|
|
78
110
|
|
|
79
111
|
Metrics are derived from `.loki/` flat files:
|
package/events/emit.sh
CHANGED
|
@@ -171,6 +171,37 @@ if [ "$_emit_max" -gt 0 ]; then
|
|
|
171
171
|
kill -0 "$_emit_target" 2>/dev/null || exit 0
|
|
172
172
|
_waited=$((_waited + 1))
|
|
173
173
|
done
|
|
174
|
+
# FORENSICS BEFORE THE KILL (issue #184).
|
|
175
|
+
#
|
|
176
|
+
# The watchdog bounds the damage but has never recorded WHY the hang
|
|
177
|
+
# happened, which is why the cause is still unknown after two disproved
|
|
178
|
+
# hypotheses (the lock-retry counter, and six synthetic race probes).
|
|
179
|
+
# A SIGKILL destroys the only evidence that existed.
|
|
180
|
+
#
|
|
181
|
+
# Captures a stack sample and the process tree to a file, then kills. On
|
|
182
|
+
# the next occurrence the reader gets a stack instead of a guess.
|
|
183
|
+
#
|
|
184
|
+
# Every part is bounded and best-effort: this runs INSIDE the watchdog,
|
|
185
|
+
# so a capture that blocks would replace a bounded failure with an
|
|
186
|
+
# unbounded one -- the exact thing the watchdog exists to prevent.
|
|
187
|
+
# `sample` is macOS-only and absent on Linux; both branches are guarded
|
|
188
|
+
# and neither is allowed to fail the watchdog.
|
|
189
|
+
_emit_diag_dir="${LOKI_EMIT_DIAG_DIR:-${TMPDIR:-/tmp}}"
|
|
190
|
+
_emit_diag="$_emit_diag_dir/loki-emit-hang-$_emit_target.txt"
|
|
191
|
+
{
|
|
192
|
+
echo "emit.sh watchdog fired after ${_emit_max}s on pid $_emit_target"
|
|
193
|
+
echo "captured: $(date -u '+%Y-%m-%dT%H:%M:%SZ' 2>/dev/null)"
|
|
194
|
+
echo "--- ps ---"
|
|
195
|
+
ps -o pid=,ppid=,pgid=,stat=,etime=,command= -p "$_emit_target" 2>/dev/null
|
|
196
|
+
echo "--- children ---"
|
|
197
|
+
ps -eo pid=,ppid=,stat=,command= 2>/dev/null | awk -v p="$_emit_target" '$2==p'
|
|
198
|
+
echo "--- stack (macOS sample, 1s) ---"
|
|
199
|
+
if command -v sample >/dev/null 2>&1; then
|
|
200
|
+
sample "$_emit_target" 1 -mayDie 2>/dev/null | head -60
|
|
201
|
+
else
|
|
202
|
+
echo "(sample unavailable on this platform)"
|
|
203
|
+
fi
|
|
204
|
+
} > "$_emit_diag" 2>/dev/null || true
|
|
174
205
|
kill -9 "$_emit_target" 2>/dev/null || true
|
|
175
206
|
) >/dev/null 2>&1 &
|
|
176
207
|
# Disown so the watchdog is not a tracked job of the caller's shell.
|
package/loki-ts/dist/loki.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// @bun
|
|
2
|
-
var p_=Object.create;var{getPrototypeOf:d_,defineProperty:eK,getOwnPropertyNames:c_}=Object;var l_=Object.prototype.hasOwnProperty;function i_(Z){return this[Z]}var a_,s_,n_=(Z,X,Q)=>{var Y=Z!=null&&typeof Z==="object";if(Y){var J=X?a_??=new WeakMap:s_??=new WeakMap,z=J.get(Z);if(z)return z}Q=Z!=null?p_(d_(Z)):{};let K=X||!Z||!Z.__esModule?eK(Q,"default",{value:Z,enumerable:!0}):Q;for(let $ of c_(Z))if(!l_.call(K,$))eK(K,$,{get:i_.bind(Z,$),enumerable:!0});if(Y)J.set(Z,K);return K};var HQ=(Z,X)=>()=>(X||Z((X={exports:{}}).exports,X),X.exports);var o_=(Z)=>Z;function r_(Z,X){this[Z]=o_.bind(null,X)}var l0=(Z,X)=>{for(var Q in X)eK(Z,Q,{get:X[Q],enumerable:!0,configurable:!0,set:r_.bind(X,Q)})};var p=(Z,X)=>()=>(Z&&(X=Z(Z=0)),X);var t0=import.meta.require;var xO={};l0(xO,{lokiDir:()=>j0,homeLokiDir:()=>R4,findRepoRootForVersion:()=>X$,REPO_ROOT:()=>i0});import{resolve as n7,dirname as Z$}from"path";import{fileURLToPath as t_}from"url";import{existsSync as UQ}from"fs";import{homedir as e_}from"os";function Zf(){let Z=kO;for(let X=0;X<6;X++){if(UQ(n7(Z,"VERSION"))&&UQ(n7(Z,"autonomy/run.sh")))return Z;let Q=Z$(Z);if(Q===Z)break;Z=Q}return n7(kO,"..","..","..")}function X$(Z){let X=Z;for(let Q=0;Q<6;Q++){if(UQ(n7(X,"VERSION"))&&UQ(n7(X,"autonomy/run.sh")))return X;let Y=Z$(X);if(Y===X)break;X=Y}return n7(Z,"..","..","..")}function j0(){return process.env.LOKI_DIR??n7(process.cwd(),".loki")}function R4(){return n7(e_(),".loki")}var kO,i0;var H8=p(()=>{kO=Z$(t_(import.meta.url));i0=Zf()});import{readFileSync as Xf}from"fs";import{resolve as Qf,dirname as Yf}from"path";import{fileURLToPath as Jf}from"url";function h3(){if(h5!==null)return h5;let Z="9.
|
|
2
|
+
var p_=Object.create;var{getPrototypeOf:d_,defineProperty:eK,getOwnPropertyNames:c_}=Object;var l_=Object.prototype.hasOwnProperty;function i_(Z){return this[Z]}var a_,s_,n_=(Z,X,Q)=>{var Y=Z!=null&&typeof Z==="object";if(Y){var J=X?a_??=new WeakMap:s_??=new WeakMap,z=J.get(Z);if(z)return z}Q=Z!=null?p_(d_(Z)):{};let K=X||!Z||!Z.__esModule?eK(Q,"default",{value:Z,enumerable:!0}):Q;for(let $ of c_(Z))if(!l_.call(K,$))eK(K,$,{get:i_.bind(Z,$),enumerable:!0});if(Y)J.set(Z,K);return K};var HQ=(Z,X)=>()=>(X||Z((X={exports:{}}).exports,X),X.exports);var o_=(Z)=>Z;function r_(Z,X){this[Z]=o_.bind(null,X)}var l0=(Z,X)=>{for(var Q in X)eK(Z,Q,{get:X[Q],enumerable:!0,configurable:!0,set:r_.bind(X,Q)})};var p=(Z,X)=>()=>(Z&&(X=Z(Z=0)),X);var t0=import.meta.require;var xO={};l0(xO,{lokiDir:()=>j0,homeLokiDir:()=>R4,findRepoRootForVersion:()=>X$,REPO_ROOT:()=>i0});import{resolve as n7,dirname as Z$}from"path";import{fileURLToPath as t_}from"url";import{existsSync as UQ}from"fs";import{homedir as e_}from"os";function Zf(){let Z=kO;for(let X=0;X<6;X++){if(UQ(n7(Z,"VERSION"))&&UQ(n7(Z,"autonomy/run.sh")))return Z;let Q=Z$(Z);if(Q===Z)break;Z=Q}return n7(kO,"..","..","..")}function X$(Z){let X=Z;for(let Q=0;Q<6;Q++){if(UQ(n7(X,"VERSION"))&&UQ(n7(X,"autonomy/run.sh")))return X;let Y=Z$(X);if(Y===X)break;X=Y}return n7(Z,"..","..","..")}function j0(){return process.env.LOKI_DIR??n7(process.cwd(),".loki")}function R4(){return n7(e_(),".loki")}var kO,i0;var H8=p(()=>{kO=Z$(t_(import.meta.url));i0=Zf()});import{readFileSync as Xf}from"fs";import{resolve as Qf,dirname as Yf}from"path";import{fileURLToPath as Jf}from"url";function h3(){if(h5!==null)return h5;let Z="9.20.0";if(typeof Z==="string"&&Z.length>0)return h5=Z,h5;try{let X=Yf(Jf(import.meta.url)),Q=X$(X);h5=Xf(Qf(Q,"VERSION"),"utf-8").trim()}catch{h5="unknown"}return h5}var h5=null;var BQ=p(()=>{H8()});var _O={};l0(_O,{runOrThrow:()=>Tf,run:()=>E0,readStreamCapped:()=>NQ,commandVersion:()=>Cf,commandExists:()=>X9,ShellError:()=>Q$,MAX_STDOUT_BYTES:()=>bO});async function NQ(Z,X=bO){let Q=Z.getReader(),Y=new TextDecoder,J="",z=0;try{while(z<X){let{done:K,value:$}=await Q.read();if(K)break;if(!$)continue;if(z+=$.byteLength,z>X){let V=$.byteLength-(z-X);J+=Y.decode($.subarray(0,V),{stream:!0});break}J+=Y.decode($,{stream:!0})}J+=Y.decode()}finally{try{await Q.cancel()}catch{}Q.releaseLock()}return J}async function E0(Z,X={}){let Q=Bun.spawn({cmd:[...Z],stdout:"pipe",stderr:"pipe",env:X.env?{...process.env,...X.env}:process.env,cwd:X.cwd}),Y,J;if(X.timeoutMs&&X.timeoutMs>0)Y=setTimeout(()=>{try{Q.kill("SIGTERM")}catch{}J=setTimeout(()=>{try{Q.kill("SIGKILL")}catch{}},2000)},X.timeoutMs);try{let[z,K,$]=await Promise.all([NQ(Q.stdout),new Response(Q.stderr).text(),Q.exited]);return{stdout:z,stderr:K,exitCode:$}}finally{if(Y)clearTimeout(Y);if(J)clearTimeout(J)}}async function Tf(Z,X={}){let Q=await E0(Z,X);if(Q.exitCode!==0)throw new Q$(`command failed (${Q.exitCode}): ${Z.join(" ")}`,Q.exitCode,Q.stdout,Q.stderr);return Q}async function X9(Z){let X=wf(Z),Q=await E0(["sh","-c",`command -v ${X}`],{timeoutMs:5000});if(Q.exitCode===0)return Q.stdout.trim()||null;return null}function wf(Z){if(!/^[A-Za-z0-9._/-]+$/.test(Z))throw Error(`refused to shell-escape suspect token: ${Z}`);return Z}async function Cf(Z,X="--version"){if(!await X9(Z))return null;let Y=await E0([Z,X],{timeoutMs:5000});if(Y.exitCode!==0)return null;return((Y.stdout||Y.stderr).split(/\r?\n/)[0]?.trim()??"")||null}var bO=16777216,Q$;var x9=p(()=>{Q$=class Q$ extends Error{message;exitCode;stdout;stderr;constructor(Z,X,Q,Y){super(Z);this.message=Z;this.exitCode=X;this.stdout=Q;this.stderr=Y;this.name="ShellError"}}});function o7(Z){return Ff?"":Z}var Ff,A0,F8,_0,$W0,a0,V8,Q9,h;var S6=p(()=>{Ff=(process.env.NO_COLOR??"").length>0;A0=o7("\x1B[0;31m"),F8=o7("\x1B[0;32m"),_0=o7("\x1B[1;33m"),$W0=o7("\x1B[0;34m"),a0=o7("\x1B[0;36m"),V8=o7("\x1B[1m"),Q9=o7("\x1B[2m"),h=o7("\x1B[0m")});import{existsSync as ff}from"fs";async function E7(){if(x4!==void 0)return x4;let Z="/opt/homebrew/bin/python3.12";if(ff(Z))return x4=Z,Z;let X=await X9("python3.12");if(X)return x4=X,X;let Q=await X9("python3");return x4=Q,Q}async function Y7(Z,X={}){let Q=await E7();if(!Q)return{stdout:"",stderr:"python3 not found",exitCode:127};return E0([Q,"-c",Z],X)}var x4;var r7=p(()=>{x9()});var XA={};l0(XA,{runStatus:()=>Vh});import{existsSync as Y9,readFileSync as g3,readdirSync as aO,statSync as sO}from"fs";import{resolve as h8,basename as ef}from"path";import{homedir as Zh}from"os";function nO(Z){let X=Math.trunc(Z);if(X>=1e6)return`${(Math.trunc(X/1e6*10)/10).toFixed(1)}M`;if(X>=1000)return`${(Math.trunc(X/1000*10)/10).toFixed(1)}K`;return String(X)}function oO(Z,X,Q){if(X===0)return null;let Y=Math.trunc(Z*100/X),J=Math.trunc(Z*AQ/X);if(J>AQ)J=AQ;let z=AQ-J,K=F8;if(Y>=80)K=A0;else if(Y>=50)K=_0;let $="=".repeat(Math.max(0,J))+" ".repeat(Math.max(0,z)),V=nO(Z),W=nO(X);return` ${V8}${Q}${h} ${K}[${$}]${h} ${Y}% (${V} / ${W})`}async function Qh(){if(await X9("jq"))return!0;return process.stdout.write(`${A0}Error: jq is required but not installed.${h}
|
|
3
3
|
`),process.stdout.write(`Install with:
|
|
4
4
|
`),process.stdout.write(` brew install jq (macOS)
|
|
5
5
|
`),process.stdout.write(` apt install jq (Debian/Ubuntu)
|
|
@@ -1236,4 +1236,4 @@ Set LOKI_LEGACY_BASH=1 to force the bash CLI for every command.
|
|
|
1236
1236
|
`),2}case"start":{let{runStart:Y}=await Promise.resolve().then(() => (g_(),v_));return Y(Q)}default:return process.stderr.write(`Unknown command: ${X}
|
|
1237
1237
|
`),process.stderr.write(m_),2}}lO();process.on("SIGINT",()=>process.exit(130));process.on("SIGTERM",()=>process.exit(143));var dV0=await pV0(Bun.argv.slice(2));process.exit(dV0);
|
|
1238
1238
|
|
|
1239
|
-
//# debugId=
|
|
1239
|
+
//# debugId=AF141439B5D0532864756E2164756E21
|
package/mcp/__init__.py
CHANGED
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "loki-mode",
|
|
3
3
|
"mcpName": "io.github.asklokesh/loki-mode",
|
|
4
|
-
"version": "9.
|
|
4
|
+
"version": "9.20.0",
|
|
5
5
|
"description": "Loki Mode by Autonomi. Autonomous spec-to-product system: takes a PRD, GitHub issue, OpenAPI/JSON/YAML, or one-line brief to a deployed app via the RARV-C closure loop with 8 quality gates. Provider-agnostic (Claude Code, OpenAI Codex, Cline, Aider).",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"agent",
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json",
|
|
3
3
|
"name": "loki-mode",
|
|
4
4
|
"displayName": "Loki Mode",
|
|
5
|
-
"version": "9.
|
|
5
|
+
"version": "9.20.0",
|
|
6
6
|
"description": "Autonomous spec-to-product build system with a built-in trust layer (RARV-C closure loop, 8 quality gates, completion council). Ships Loki's spec-hardening, drift-detection, and deterministic PR verification commands plus the Loki MCP server.",
|
|
7
7
|
"author": {
|
|
8
8
|
"name": "Autonomi",
|