loki-mode 9.54.0 → 9.54.2
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/completion-council.sh +10 -1
- package/autonomy/lib/proof-generator.py +36 -5
- package/autonomy/run.sh +234 -31
- package/dashboard/__init__.py +1 -1
- package/dashboard/api_v2.py +4 -0
- package/dashboard/audit.py +9 -1
- package/dashboard/auth.py +16 -4
- package/dashboard/registry.py +34 -12
- package/dashboard/rigour_integration.py +25 -17
- package/dashboard/server.py +252 -104
- package/dashboard/static/cost.html +55 -26
- package/dashboard/static/index.html +475 -1544
- package/dashboard/static/proofs.html +1 -1
- package/docs/INSTALLATION.md +1 -1
- package/docs/air-gapped.md +7 -2
- package/docs/image-provenance.md +4 -2
- package/docs/v10/BACKLOG.md +41 -2
- package/docs/v10/BOARD.md +105 -0
- package/docs/v10/DECISIONS.md +71 -0
- package/docs/v10/PROGRESS.md +43 -0
- package/docs/v10/SWARM-PROMPT.md +98 -0
- package/docs/v10/SWARM.md +237 -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/providers/aider.sh +8 -0
- package/providers/cline.sh +8 -0
- package/providers/codex.sh +9 -0
- package/providers/opencode.sh +9 -0
- package/web-app/dist/assets/AdminPage-CZrBkzRF.js +32 -0
- package/web-app/dist/assets/Avatar-gQIp6FBI.js +11 -0
- package/web-app/dist/assets/{Badge-BF3mhT57.js → Badge-D2_fBTFY.js} +1 -1
- package/web-app/dist/assets/{Button-BDp9aJ9q.js → Button-D5qDd2ef.js} +1 -1
- package/web-app/dist/assets/CockpitPage-DVMk2RUK.js +47 -0
- package/web-app/dist/assets/ComparePage-NMLyE1xL.js +1 -0
- package/web-app/dist/assets/{ErrorBoundary-ut3wrUmu.js → ErrorBoundary-BRABI-BT.js} +1 -1
- package/web-app/dist/assets/EvidenceReceiptPanel-D2LNKqUx.js +1 -0
- package/web-app/dist/assets/GitHubIssuesPanel-BC5GzKUN.js +7 -0
- package/web-app/dist/assets/{GitHubPRsPanel-DurZgSaj.js → GitHubPRsPanel-BSlmoVTS.js} +3 -3
- package/web-app/dist/assets/HomePage-D_CDMoen.js +12 -0
- package/web-app/dist/assets/{LoginPage-HeRGJeZy.js → LoginPage-BBNA_N9t.js} +1 -1
- package/web-app/dist/assets/{MagicPage-CUKrqsZs.js → MagicPage-CnFRZCMx.js} +1 -1
- package/web-app/dist/assets/MetricsPage-CPfLMRlh.js +6 -0
- package/web-app/dist/assets/{NotFoundPage-BZDZ-B77.js → NotFoundPage-lA0XSGTz.js} +1 -1
- package/web-app/dist/assets/ProjectPage-CNAPbr9z.js +256 -0
- package/web-app/dist/assets/ProjectsPage-CMtm8goN.js +6 -0
- package/web-app/dist/assets/SettingsPage-BhIjVMYE.js +10 -0
- package/web-app/dist/assets/{ShowcasePage-BGS2jTGH.js → ShowcasePage-BndtGeYo.js} +1 -1
- package/web-app/dist/assets/SystemSettingsPage-euXI-KXE.js +1 -0
- package/web-app/dist/assets/TeamsPage-ClUKecun.js +11 -0
- package/web-app/dist/assets/TemplatesPage-BWHWVk0L.js +29 -0
- package/web-app/dist/assets/{TerminalOutput-togzv9Zl.js → TerminalOutput-Cv3N3Szf.js} +1 -1
- package/web-app/dist/assets/{activity-Dreyb_Kr.js → activity-dEhz-bXR.js} +1 -1
- package/web-app/dist/assets/{bell-DWH0drES.js → bell-B3XUMq1t.js} +1 -1
- package/web-app/dist/assets/{bot-78MoCuDl.js → bot-X-ssNlwR.js} +1 -1
- package/web-app/dist/assets/{check-ikfylPst.js → check-C_9Nx7kb.js} +1 -1
- package/web-app/dist/assets/{chevron-left-DXUqkd8J.js → chevron-left-tvNswo3A.js} +1 -1
- package/web-app/dist/assets/{circle-alert-Da74VO8P.js → circle-alert-CSB88nEs.js} +1 -1
- package/web-app/dist/assets/{clock-mlGCFq8l.js → clock-C8p2e4NI.js} +1 -1
- package/web-app/dist/assets/{cloud-B_Y5ZrIO.js → cloud-CKp2ZvhG.js} +1 -1
- package/web-app/dist/assets/{code-xml-Bbom7YWV.js → code-xml-dY70LCHD.js} +1 -1
- package/web-app/dist/assets/{database-CLcofiVp.js → database-DObn5fmn.js} +1 -1
- package/web-app/dist/assets/{dollar-sign-C5mkjXIM.js → dollar-sign-DSu6n3K3.js} +1 -1
- package/web-app/dist/assets/{file-code-corner-CVrkaOQP.js → file-code-corner-DpCnS73z.js} +1 -1
- package/web-app/dist/assets/{file-plus-BV9240gX.js → file-plus-D3IPKCf1.js} +1 -1
- package/web-app/dist/assets/{globe-aZ0Zso4I.js → globe-C9QXkgH_.js} +1 -1
- package/web-app/dist/assets/{hammer-EgOQNukG.js → hammer-D_FtWe1y.js} +1 -1
- package/web-app/dist/assets/index-CI54rara.css +1 -0
- package/web-app/dist/assets/{index-DvZ0Kk2x.js → index-Cz7q1bvC.js} +2 -2
- package/web-app/dist/assets/{layers-BOVKmepE.js → layers-DsW7jnJX.js} +1 -1
- package/web-app/dist/assets/{loader-circle-0K8yLgHl.js → loader-circle-B3HspjKR.js} +1 -1
- package/web-app/dist/assets/{lock-XzlDMy8M.js → lock-DSI29pct.js} +1 -1
- package/web-app/dist/assets/{package-BtXTKusI.js → package-CLNxtA2x.js} +1 -1
- package/web-app/dist/assets/{plus-PxeiD8ju.js → plus-DyI4WC4_.js} +1 -1
- package/web-app/dist/assets/{refresh-cw-CjqzYczt.js → refresh-cw-D2dv0xA0.js} +1 -1
- package/web-app/dist/assets/{rotate-ccw-CY0os4DH.js → rotate-ccw-CBvihA-a.js} +1 -1
- package/web-app/dist/assets/scroll-text-DvoOvAl_.js +6 -0
- package/web-app/dist/assets/{server-pIIs9vNp.js → server-DenWvSUt.js} +1 -1
- package/web-app/dist/assets/{shield-alert-CsDqKXwA.js → shield-alert-40cX_FvG.js} +1 -1
- package/web-app/dist/assets/{thumbs-up-CAh3HVzH.js → thumbs-up-BrjZ6QuV.js} +12 -7
- package/web-app/dist/assets/trash-2-C6evwkEH.js +16 -0
- package/web-app/dist/assets/{trending-down-DrtBd_C9.js → trending-down-CfbZis3O.js} +1 -1
- package/web-app/dist/assets/{trending-up-D-DbCHXm.js → trending-up-DWuEK3WC.js} +1 -1
- package/web-app/dist/assets/{upload-D4l6DCOt.js → upload-B9DFBcPG.js} +1 -1
- package/web-app/dist/assets/{usePolling-D0aoTX80.js → usePolling-BryVC-T1.js} +1 -1
- package/web-app/dist/assets/{user-BQf_XJO_.js → user-DMRXwZiw.js} +1 -1
- package/web-app/dist/index.html +2 -2
- package/web-app/server.py +122 -47
- package/web-app/dist/assets/AdminPage-y6_b2Xo4.js +0 -32
- package/web-app/dist/assets/Avatar-BFPkXhIJ.js +0 -6
- package/web-app/dist/assets/CockpitPage-5TKICCoA.js +0 -47
- package/web-app/dist/assets/ComparePage-BSuNsUHy.js +0 -1
- package/web-app/dist/assets/EvidenceReceiptPanel-CcaBwEI-.js +0 -1
- package/web-app/dist/assets/GitHubIssuesPanel-DJeYgzAa.js +0 -12
- package/web-app/dist/assets/HomePage-Bs451LAy.js +0 -28
- package/web-app/dist/assets/MetricsPage-DvY1csgz.js +0 -6
- package/web-app/dist/assets/ProjectPage-CqHkEZkl.js +0 -257
- package/web-app/dist/assets/ProjectsPage-CLvCK1Y8.js +0 -6
- package/web-app/dist/assets/SettingsPage-DP7OmdwP.js +0 -10
- package/web-app/dist/assets/SystemSettingsPage-P_rlLh0x.js +0 -1
- package/web-app/dist/assets/TeamsPage-DxtBDcBC.js +0 -11
- package/web-app/dist/assets/TemplatesPage-5e-faH3W.js +0 -29
- package/web-app/dist/assets/folder-open-D_6yV78G.js +0 -11
- package/web-app/dist/assets/index-BSgmPwC0.css +0 -1
- package/web-app/dist/assets/lightbulb-DdASgqOG.js +0 -6
- package/web-app/dist/assets/mail-COu670_W.js +0 -6
- package/web-app/dist/assets/minus-bQpUt9ff.js +0 -6
- package/web-app/dist/assets/save-v8qep5cY.js +0 -6
- package/web-app/dist/assets/trash-2-Dt6urTyY.js +0 -6
package/dashboard/registry.py
CHANGED
|
@@ -16,6 +16,8 @@ from pathlib import Path
|
|
|
16
16
|
from typing import Iterator, Optional
|
|
17
17
|
import hashlib
|
|
18
18
|
|
|
19
|
+
from .api_runs import _efficiency_module
|
|
20
|
+
|
|
19
21
|
|
|
20
22
|
# Registry file location
|
|
21
23
|
REGISTRY_DIR = Path.home() / ".loki" / "dashboard"
|
|
@@ -686,13 +688,14 @@ def _read_project_run_snapshot(path: str) -> dict:
|
|
|
686
688
|
.loki/context/tracking.json fallback (totals.total_cost_usd)
|
|
687
689
|
|
|
688
690
|
Returns a dict with phase, iteration, cost_usd, started_at, and ended_at
|
|
689
|
-
(best-effort; missing values default to None
|
|
690
|
-
|
|
691
|
+
(best-effort; missing values default to None). iteration and cost_usd are
|
|
692
|
+
None, never 0 / 0.0, when nothing recorded them. Never raises: any file problem degrades the
|
|
693
|
+
affected field to its default.
|
|
691
694
|
"""
|
|
692
695
|
snap = {
|
|
693
696
|
"phase": "",
|
|
694
|
-
"iteration":
|
|
695
|
-
"cost_usd":
|
|
697
|
+
"iteration": None,
|
|
698
|
+
"cost_usd": None,
|
|
696
699
|
"started_at": None,
|
|
697
700
|
"ended_at": None,
|
|
698
701
|
}
|
|
@@ -708,8 +711,8 @@ def _read_project_run_snapshot(path: str) -> dict:
|
|
|
708
711
|
if isinstance(state, dict):
|
|
709
712
|
_p = state.get("phase", "")
|
|
710
713
|
snap["phase"] = _p if isinstance(_p, str) else ""
|
|
711
|
-
_i = state.get("iteration"
|
|
712
|
-
snap["iteration"] = _i if isinstance(_i, int) else
|
|
714
|
+
_i = state.get("iteration")
|
|
715
|
+
snap["iteration"] = _i if isinstance(_i, int) and not isinstance(_i, bool) else None
|
|
713
716
|
except (json.JSONDecodeError, OSError, ValueError):
|
|
714
717
|
pass
|
|
715
718
|
|
|
@@ -727,6 +730,15 @@ def _read_project_run_snapshot(path: str) -> dict:
|
|
|
727
730
|
pass
|
|
728
731
|
|
|
729
732
|
# Cost: sum per-iteration efficiency files; fall back to context tracking.
|
|
733
|
+
# A present record is not a measurement: only a record that carried an
|
|
734
|
+
# observed value (the canonical record_is_measured, the same predicate as
|
|
735
|
+
# dashboard/server.py _record_is_measured) and a numeric cost counts, so an
|
|
736
|
+
# all-zero or field-less record, or the 0.0 a fresh tracking.json is seeded
|
|
737
|
+
# with, reads as unknown instead of $0.00. A measured $0.00 stays 0.0.
|
|
738
|
+
# ponytail: a measured record with tokens but no cost_usd adds nothing (the
|
|
739
|
+
# fleet does not price tokens); price it here if unpriced records appear.
|
|
740
|
+
eff_mod = _efficiency_module()
|
|
741
|
+
measured = eff_mod.record_is_measured if eff_mod is not None else (lambda _rec: False)
|
|
730
742
|
cost = 0.0
|
|
731
743
|
found_cost = False
|
|
732
744
|
eff_dir = loki_dir / "metrics" / "efficiency"
|
|
@@ -738,7 +750,7 @@ def _read_project_run_snapshot(path: str) -> dict:
|
|
|
738
750
|
if not isinstance(data, dict):
|
|
739
751
|
continue
|
|
740
752
|
c = data.get("cost_usd")
|
|
741
|
-
if isinstance(c, (int, float)):
|
|
753
|
+
if isinstance(c, (int, float)) and not isinstance(c, bool) and measured(data):
|
|
742
754
|
cost += float(c)
|
|
743
755
|
found_cost = True
|
|
744
756
|
except (json.JSONDecodeError, OSError, ValueError):
|
|
@@ -754,11 +766,15 @@ def _read_project_run_snapshot(path: str) -> dict:
|
|
|
754
766
|
totals = ctx.get("totals", {})
|
|
755
767
|
if isinstance(totals, dict):
|
|
756
768
|
tc = totals.get("total_cost_usd")
|
|
757
|
-
if isinstance(tc, (int, float))
|
|
769
|
+
if isinstance(tc, (int, float)) and not isinstance(tc, bool) and measured({
|
|
770
|
+
"cost_usd": tc,
|
|
771
|
+
"input_tokens": totals.get("total_input"),
|
|
772
|
+
"output_tokens": totals.get("total_output")}):
|
|
758
773
|
cost = float(tc)
|
|
774
|
+
found_cost = True
|
|
759
775
|
except (json.JSONDecodeError, OSError, ValueError):
|
|
760
776
|
pass
|
|
761
|
-
snap["cost_usd"] = round(cost, 6)
|
|
777
|
+
snap["cost_usd"] = round(cost, 6) if found_cost else None
|
|
762
778
|
return snap
|
|
763
779
|
|
|
764
780
|
|
|
@@ -836,7 +852,7 @@ def get_fleet_runs(include_inactive: bool = True) -> list[dict]:
|
|
|
836
852
|
"running": running,
|
|
837
853
|
"phase": snap.get("phase", ""),
|
|
838
854
|
"iteration": snap.get("iteration", 0),
|
|
839
|
-
"cost_usd": snap.get("cost_usd"
|
|
855
|
+
"cost_usd": snap.get("cost_usd"),
|
|
840
856
|
"started_at": started_at,
|
|
841
857
|
"duration_seconds": duration_seconds,
|
|
842
858
|
"port": p.get("port"),
|
|
@@ -862,16 +878,22 @@ def get_fleet_summary(include_inactive: bool = True) -> dict:
|
|
|
862
878
|
Returns counts (total / running / stopped) and a summed cost across all
|
|
863
879
|
registered projects. v1 polls the shared metadata store (see
|
|
864
880
|
get_fleet_runs); not a controller. Never raises.
|
|
881
|
+
|
|
882
|
+
total_cost_usd sums only runs that recorded a cost and is None when none
|
|
883
|
+
did; total_cost_partial is True when some runs recorded no cost, so the
|
|
884
|
+
total is a lower bound, not the whole bill.
|
|
865
885
|
"""
|
|
866
886
|
runs = get_fleet_runs(include_inactive=include_inactive)
|
|
867
887
|
total = len(runs)
|
|
868
888
|
running = sum(1 for r in runs if r.get("running"))
|
|
869
|
-
|
|
889
|
+
costs = [float(r["cost_usd"]) for r in runs
|
|
890
|
+
if isinstance(r.get("cost_usd"), (int, float)) and not isinstance(r.get("cost_usd"), bool)]
|
|
870
891
|
return {
|
|
871
892
|
"total_runs": total,
|
|
872
893
|
"running_runs": running,
|
|
873
894
|
"stopped_runs": total - running,
|
|
874
|
-
"total_cost_usd":
|
|
895
|
+
"total_cost_usd": round(sum(costs), 6) if costs else None,
|
|
896
|
+
"total_cost_partial": 0 < len(costs) < total,
|
|
875
897
|
}
|
|
876
898
|
|
|
877
899
|
|
|
@@ -245,32 +245,36 @@ class RigourIntegration:
|
|
|
245
245
|
for sev in findings:
|
|
246
246
|
findings[sev] = int(_extract_num(raw_findings, sev, 0))
|
|
247
247
|
|
|
248
|
-
# Extract category scores
|
|
248
|
+
# Extract category scores. A category Rigour did not report is None
|
|
249
|
+
# (the panel shows "--"), never a scored 0.
|
|
249
250
|
raw_categories = raw.get("categories", raw.get("scores", {}))
|
|
251
|
+
if not isinstance(raw_categories, dict):
|
|
252
|
+
raw_categories = {}
|
|
250
253
|
categories = {
|
|
251
|
-
"security": _extract_num(raw_categories, "security",
|
|
252
|
-
"code_quality": _extract_num(raw_categories, "code_quality", _extract_num(raw_categories, "codeQuality",
|
|
253
|
-
"compliance": _extract_num(raw_categories, "compliance",
|
|
254
|
-
"best_practices": _extract_num(raw_categories, "best_practices", _extract_num(raw_categories, "bestPractices",
|
|
254
|
+
"security": _extract_num(raw_categories, "security", None),
|
|
255
|
+
"code_quality": _extract_num(raw_categories, "code_quality", _extract_num(raw_categories, "codeQuality", None)),
|
|
256
|
+
"compliance": _extract_num(raw_categories, "compliance", None),
|
|
257
|
+
"best_practices": _extract_num(raw_categories, "best_practices", _extract_num(raw_categories, "bestPractices", None)),
|
|
255
258
|
}
|
|
256
259
|
|
|
257
|
-
# Compute overall score
|
|
260
|
+
# Compute overall score: the reported one, else the average of the
|
|
261
|
+
# categories that were reported, else None (no score, no grade).
|
|
258
262
|
raw_score = raw.get("score", raw.get("overall_score"))
|
|
263
|
+
score = None
|
|
259
264
|
if raw_score is not None:
|
|
260
265
|
try:
|
|
261
266
|
score = float(raw_score)
|
|
262
267
|
except (ValueError, TypeError):
|
|
263
|
-
score =
|
|
268
|
+
score = None
|
|
264
269
|
else:
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
score = round(sum(non_zero) / len(non_zero), 1) if non_zero else 0.0
|
|
270
|
+
reported = [v for v in categories.values() if v is not None]
|
|
271
|
+
score = round(sum(reported) / len(reported), 1) if reported else None
|
|
268
272
|
|
|
269
273
|
result: dict[str, Any] = {
|
|
270
274
|
"available": True,
|
|
271
|
-
"score": round(score, 1),
|
|
275
|
+
"score": round(score, 1) if score is not None else None,
|
|
272
276
|
"max_score": 100,
|
|
273
|
-
"grade": _score_to_grade(score),
|
|
277
|
+
"grade": _score_to_grade(score) if score is not None else None,
|
|
274
278
|
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
275
279
|
"preset": preset,
|
|
276
280
|
"findings": findings,
|
|
@@ -301,8 +305,9 @@ class RigourIntegration:
|
|
|
301
305
|
"grade": None,
|
|
302
306
|
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
303
307
|
"preset": preset,
|
|
304
|
-
|
|
305
|
-
"
|
|
308
|
+
# Nothing was scanned: no counts and no category scores.
|
|
309
|
+
"findings": {"critical": None, "major": None, "minor": None, "info": None},
|
|
310
|
+
"categories": {"security": None, "code_quality": None, "compliance": None, "best_practices": None},
|
|
306
311
|
"reason": reason,
|
|
307
312
|
}
|
|
308
313
|
|
|
@@ -325,9 +330,12 @@ class RigourIntegration:
|
|
|
325
330
|
}
|
|
326
331
|
|
|
327
332
|
|
|
328
|
-
def _extract_num(d: dict, key: str, default: float = 0) -> float:
|
|
329
|
-
"""Safely extract a numeric value from a dict."""
|
|
333
|
+
def _extract_num(d: dict, key: str, default: Optional[float] = 0) -> Optional[float]:
|
|
334
|
+
"""Safely extract a numeric value from a dict; `default` when absent or not numeric."""
|
|
335
|
+
v = d.get(key)
|
|
336
|
+
if v is None or isinstance(v, bool):
|
|
337
|
+
return default
|
|
330
338
|
try:
|
|
331
|
-
return float(
|
|
339
|
+
return float(v)
|
|
332
340
|
except (ValueError, TypeError):
|
|
333
341
|
return default
|