loki-mode 11.17.11 → 11.17.15
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/cost-summary.py +5 -2
- package/autonomy/lib/efficiency_cost.py +13 -0
- package/autonomy/lib/proof-generator.py +5 -0
- package/autonomy/lib/proof-template.html +21 -6
- package/autonomy/lib/trust_trajectory.py +11 -1
- package/autonomy/loki +148 -28
- package/autonomy/mcp-launch.sh +157 -0
- package/completions/_loki +1 -0
- package/completions/loki.bash +1 -1
- package/dashboard/__init__.py +1 -1
- package/docs/CLI-REFERENCE.md +2 -1
- package/docs/INSTALLATION.md +1 -1
- package/docs/dev/release-checklist.md +5 -5
- package/docs/v10/BOARD.md +101 -11
- package/docs/v10/CLI-MODERN.md +1 -1
- package/docs/v10/DECISIONS.md +13 -1
- package/docs/v10/DEPS.md +2 -6
- package/docs/v10/FAILURE-CLASSES.md +77 -0
- package/docs/v10/OPERATING-MODEL.md +2 -3
- package/docs/v10/PROGRESS.md +4 -0
- package/loki-ts/dist/loki.js +1339 -1226
- package/mcp/__init__.py +1 -1
- package/package.json +2 -2
- package/packages/control-plane/dist/ask-tools-server.js +16 -2
- package/packages/control-plane/dist/server.js +16967 -4228
- package/plugins/loki-mode/.claude-plugin/plugin.json +1 -1
- package/skills/factory-operations.md +6 -7
- package/skills/release-cadence.md +12 -16
- package/web-app/dist/assets/{AdminPage-CFqo-FpE.js → AdminPage-ChpOd95F.js} +1 -1
- package/web-app/dist/assets/{Avatar-CxDqlcAZ.js → Avatar-BCRrItym.js} +1 -1
- package/web-app/dist/assets/{Badge-CjZcSHSd.js → Badge-aTDM3e-d.js} +1 -1
- package/web-app/dist/assets/{Button-BsTd60w2.js → Button-Bp7dPZHt.js} +1 -1
- package/web-app/dist/assets/{CockpitPage-BivdM9_z.js → CockpitPage-DXuQVvsy.js} +1 -1
- package/web-app/dist/assets/{ComparePage-DEsuYGnF.js → ComparePage-DVf-QrAL.js} +1 -1
- package/web-app/dist/assets/{ErrorBoundary-Q2L0QzKv.js → ErrorBoundary-C4-CFvBo.js} +1 -1
- package/web-app/dist/assets/{EvidenceReceiptPanel-Da0-q_vi.js → EvidenceReceiptPanel-D7NCAclv.js} +1 -1
- package/web-app/dist/assets/{GitHubIssuesPanel-MSIQvU9a.js → GitHubIssuesPanel-DUWUS_gf.js} +1 -1
- package/web-app/dist/assets/{GitHubPRsPanel-Dw-t46_A.js → GitHubPRsPanel-nqnsewFG.js} +1 -1
- package/web-app/dist/assets/{HomePage-k_r93vg_.js → HomePage-myY1IPaZ.js} +2 -2
- package/web-app/dist/assets/{LoginPage-C-TlmbEy.js → LoginPage-Dp3Q-zqb.js} +1 -1
- package/web-app/dist/assets/{MagicPage-Dv_lGAX0.js → MagicPage-UGumm-Vb.js} +1 -1
- package/web-app/dist/assets/{MetricsPage-BINwnc6C.js → MetricsPage-UJifvRBZ.js} +1 -1
- package/web-app/dist/assets/{NotFoundPage-DGbdLAhm.js → NotFoundPage-CVVnnTdN.js} +1 -1
- package/web-app/dist/assets/{ProjectPage-D4GhODo7.js → ProjectPage-DXfW32Qw.js} +3 -3
- package/web-app/dist/assets/{ProjectsPage-C9iCGUvI.js → ProjectsPage-DYjNa3Ul.js} +1 -1
- package/web-app/dist/assets/{SettingsPage-08SiNkKI.js → SettingsPage-CYt9lALu.js} +1 -1
- package/web-app/dist/assets/{ShowcasePage-5cq8wXvh.js → ShowcasePage-CxUSXgdd.js} +1 -1
- package/web-app/dist/assets/{SystemSettingsPage-ch0Ex4iS.js → SystemSettingsPage-Dfe9kQ4p.js} +1 -1
- package/web-app/dist/assets/{TeamsPage-eE9Bevpx.js → TeamsPage-Cl6nQK8U.js} +1 -1
- package/web-app/dist/assets/{TemplatesPage-BLqK2AOM.js → TemplatesPage-B7NyDrDQ.js} +1 -1
- package/web-app/dist/assets/{TerminalOutput-Bw6lAjMM.js → TerminalOutput-BtdCEvxP.js} +1 -1
- package/web-app/dist/assets/{activity-mqeYLy0X.js → activity-CBaJkpBF.js} +1 -1
- package/web-app/dist/assets/{bell-RrSQYwnk.js → bell-BLwbfiL8.js} +1 -1
- package/web-app/dist/assets/{bot-BAYArzbY.js → bot-D4GG_jRl.js} +1 -1
- package/web-app/dist/assets/{check-DpZMgeCp.js → check-Beu8XP_r.js} +1 -1
- package/web-app/dist/assets/{chevron-left-HdiZ5i2v.js → chevron-left-BBljQPzv.js} +1 -1
- package/web-app/dist/assets/{circle-alert-CIGI9S7n.js → circle-alert-BKe5EycX.js} +1 -1
- package/web-app/dist/assets/{clock-DyGC5H7u.js → clock-BJznpp14.js} +1 -1
- package/web-app/dist/assets/{cloud-CCV2xWgX.js → cloud-DySaUo9u.js} +1 -1
- package/web-app/dist/assets/{code-xml-DZgWjvzk.js → code-xml-BAHWdutm.js} +1 -1
- package/web-app/dist/assets/{database-kzfwKPqD.js → database-B-rnmmI9.js} +1 -1
- package/web-app/dist/assets/{dollar-sign-jepNmrzR.js → dollar-sign-DPvLnmRA.js} +1 -1
- package/web-app/dist/assets/{file-code-corner-BaJhHJXN.js → file-code-corner-DxSeGCx5.js} +1 -1
- package/web-app/dist/assets/{file-plus-rONhSBM8.js → file-plus-DBe-sHuu.js} +1 -1
- package/web-app/dist/assets/{globe-DEKjsEXs.js → globe-C731sdYH.js} +1 -1
- package/web-app/dist/assets/{hammer-LFrBT_V4.js → hammer-ID64a_PJ.js} +1 -1
- package/web-app/dist/assets/{index-DTi6txFh.js → index-vVTm0HUs.js} +3 -3
- package/web-app/dist/assets/{layers-D0AF3hqY.js → layers-B5O1ewPy.js} +1 -1
- package/web-app/dist/assets/{loader-circle-5MojTTeG.js → loader-circle-CHCypEMG.js} +1 -1
- package/web-app/dist/assets/{lock-DKTO02bm.js → lock-CWe1ZGE8.js} +1 -1
- package/web-app/dist/assets/{package-0-plMCjY.js → package-Cg7pKkdo.js} +1 -1
- package/web-app/dist/assets/{plus-CjhYDJ6n.js → plus-B7Iacdk0.js} +1 -1
- package/web-app/dist/assets/{refresh-cw-CLC6VIEp.js → refresh-cw-974og1i_.js} +1 -1
- package/web-app/dist/assets/{rotate-ccw-DInfsBjr.js → rotate-ccw-C24LsDRT.js} +1 -1
- package/web-app/dist/assets/{scroll-text-DHh5UjH-.js → scroll-text-mdWBPKUQ.js} +1 -1
- package/web-app/dist/assets/{server-D0j8HyOw.js → server-B0cYzaeL.js} +1 -1
- package/web-app/dist/assets/{shield-alert-D8wHooc9.js → shield-alert-BgmGsVHG.js} +1 -1
- package/web-app/dist/assets/{thumbs-up-Cda_qfHU.js → thumbs-up-D8MPgXSO.js} +1 -1
- package/web-app/dist/assets/{trash-2-eyrU5HYs.js → trash-2-CuI9WflI.js} +1 -1
- package/web-app/dist/assets/{trending-down-Br_sStsY.js → trending-down-Dm-T7tH-.js} +1 -1
- package/web-app/dist/assets/{trending-up-DGmTcXWO.js → trending-up-CsyCaUiY.js} +1 -1
- package/web-app/dist/assets/{upload-CvCVXqWR.js → upload-CTzGge6W.js} +1 -1
- package/web-app/dist/assets/{usePolling-DT-cLGOG.js → usePolling-C2hDNNzp.js} +1 -1
- package/web-app/dist/assets/{user-tzdYvO9g.js → user-DgoCaZlg.js} +1 -1
- package/web-app/dist/index.html +1 -1
package/SKILL.md
CHANGED
|
@@ -3,7 +3,7 @@ name: loki-mode
|
|
|
3
3
|
description: An autonomous software factory that knows what it is supposed to deliver, and proves it did. Use when the user says Loki Mode or asks to build, fix or verify software autonomously.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
# Loki Mode v11.17.
|
|
6
|
+
# Loki Mode v11.17.15
|
|
7
7
|
|
|
8
8
|
**Current entry points (Loki 10 engine):** `loki "<task>"`, `loki owner/repo#N` and `loki quick "<task>"` run the Loki 10 engine and end with a signed receipt (`loki verify`). `loki backlog owner/repo --all` runs many issues in parallel. Guide: `docs/v10/GUIDE.md`. The skill instructions below, and `loki start`, describe the previous bash loop. PRD-file starts (`loki start ./prd.md`) and the opencode provider still run on it until FC38-SWEEP ports them; the legacy-engine command and the LOKI_ENGINE switch remain removed. Prefer `loki owner/repo#N` for issues.
|
|
9
9
|
|
|
@@ -483,4 +483,4 @@ See `CHANGELOG.md` entries [7.5.7], [7.5.8], [7.5.13] for the per-fix list and r
|
|
|
483
483
|
|
|
484
484
|
---
|
|
485
485
|
|
|
486
|
-
**v11.17.
|
|
486
|
+
**v11.17.15 | [Autonomi](https://www.autonomi.dev/) flagship product | core skill**
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
11.17.
|
|
1
|
+
11.17.15
|
|
@@ -50,7 +50,7 @@ _HERE = os.path.dirname(os.path.abspath(__file__))
|
|
|
50
50
|
if _HERE not in sys.path:
|
|
51
51
|
sys.path.insert(0, _HERE)
|
|
52
52
|
|
|
53
|
-
from efficiency_cost import record_is_measured # noqa: E402
|
|
53
|
+
from efficiency_cost import cost_block_is_partial, record_is_measured # noqa: E402
|
|
54
54
|
|
|
55
55
|
# iteration_attribution.py already reads and sorts the efficiency dir, skipping
|
|
56
56
|
# malformed records. Importing it keeps ONE reader of that directory; a third
|
|
@@ -167,6 +167,8 @@ def summarize(workspace="."):
|
|
|
167
167
|
"fully_measured": exists > 0 and measured == exists,
|
|
168
168
|
"total_cost_usd": round(total_usd, 4) if have_cost else None,
|
|
169
169
|
"cost_iterations_counted": len(cost_points),
|
|
170
|
+
"cost_partial": cost_block_is_partial({
|
|
171
|
+
"cost_partial": have_cost and (len(cost_points) < measured or measured < exists)}),
|
|
170
172
|
"avg_cost_per_iteration": (
|
|
171
173
|
round(total_usd / len(cost_points), 4) if have_cost else None),
|
|
172
174
|
"cache_hit_ratio": _ratio(
|
|
@@ -262,7 +264,8 @@ def render(s):
|
|
|
262
264
|
s["iterations_found"], s["iterations_measured"],
|
|
263
265
|
"" if s["fully_measured"] else " <- PARTIAL" if s["iterations_found"]
|
|
264
266
|
else ""))
|
|
265
|
-
L.append("Total cost: " + _usd(s["total_cost_usd"])
|
|
267
|
+
L.append("Total cost: " + _usd(s["total_cost_usd"])
|
|
268
|
+
+ (" <- LOWER BOUND" if s.get("cost_partial") else ""))
|
|
266
269
|
L.append("Avg / iter: " + _usd(s["avg_cost_per_iteration"]))
|
|
267
270
|
L.append("Cache ratio: " + _pct(s["cache_hit_ratio"])
|
|
268
271
|
+ " (cache_read / (input + cache_read))")
|
|
@@ -238,6 +238,19 @@ _LEDGER_TOKEN_KEYS = (
|
|
|
238
238
|
)
|
|
239
239
|
|
|
240
240
|
|
|
241
|
+
def cost_block_is_partial(cost):
|
|
242
|
+
"""The one partial-cost check (FC-COST-PARTIAL-b): True when a cost block's
|
|
243
|
+
dollar total is a lower bound or unknown, so consumers label it and never
|
|
244
|
+
print it as complete."""
|
|
245
|
+
if not isinstance(cost, dict):
|
|
246
|
+
return False
|
|
247
|
+
if cost.get("cost_partial") is True or cost.get("cost_available") is False:
|
|
248
|
+
return True
|
|
249
|
+
return any(
|
|
250
|
+
isinstance(cost.get(k), int) and not isinstance(cost.get(k), bool) and cost.get(k) > 0
|
|
251
|
+
for k in ("ledger_invalid_lines", "cost_unpriced_records"))
|
|
252
|
+
|
|
253
|
+
|
|
241
254
|
def read_child_ledger(loki_dir):
|
|
242
255
|
"""Read .loki/metrics/child-sessions.jsonl, one JSON object per child session.
|
|
243
256
|
|
|
@@ -37,6 +37,7 @@ if _HERE not in sys.path:
|
|
|
37
37
|
|
|
38
38
|
import proof_redact # noqa: E402
|
|
39
39
|
from efficiency_cost import collect_efficiency as _collect_efficiency # noqa: E402
|
|
40
|
+
from efficiency_cost import cost_block_is_partial as _cost_is_partial # noqa: E402
|
|
40
41
|
import effort_estimator # noqa: E402
|
|
41
42
|
from tree_digest import MANIFEST_VERSION, compute_tree_digest # noqa: E402
|
|
42
43
|
from workspace_diff import collect_workspace_diff # noqa: E402
|
|
@@ -2133,6 +2134,8 @@ def _build_social_hook(proof):
|
|
|
2133
2134
|
council ratio. When cost was not collected, omit the cost (never fabricate
|
|
2134
2135
|
a number, never print "$0.00")."""
|
|
2135
2136
|
usd = _fmt_usd_hook((proof.get("cost") or {}).get("usd"))
|
|
2137
|
+
if usd is not None and _cost_is_partial(proof.get("cost")):
|
|
2138
|
+
usd = "at least " + usd
|
|
2136
2139
|
lead = ("Built autonomously for " + usd) if usd is not None \
|
|
2137
2140
|
else "Built autonomously by Loki Mode"
|
|
2138
2141
|
parts = [lead]
|
|
@@ -2184,6 +2187,8 @@ def _render_fallback_html(proof):
|
|
|
2184
2187
|
|
|
2185
2188
|
usd_val = cost.get("usd", None)
|
|
2186
2189
|
usd_disp = _fmt_usd_hook(usd_val)
|
|
2190
|
+
if usd_disp is not None and _cost_is_partial(cost):
|
|
2191
|
+
usd_disp = "at least " + usd_disp
|
|
2187
2192
|
|
|
2188
2193
|
rows = []
|
|
2189
2194
|
rows.append("<h1>Loki Mode -- Proof of Run</h1>")
|
|
@@ -803,14 +803,28 @@
|
|
|
803
803
|
sec.classList.remove("hide");
|
|
804
804
|
}
|
|
805
805
|
|
|
806
|
+
// Mirror of cost_block_is_partial (autonomy/lib/efficiency_cost.py).
|
|
807
|
+
function costIsPartial(p) {
|
|
808
|
+
var c = g(p, "cost", null);
|
|
809
|
+
if (!c || typeof c !== "object") return false;
|
|
810
|
+
if (c.cost_partial === true || c.cost_available === false) return true;
|
|
811
|
+
var ks = ["ledger_invalid_lines", "cost_unpriced_records"];
|
|
812
|
+
for (var i = 0; i < ks.length; i++) {
|
|
813
|
+
var v = c[ks[i]];
|
|
814
|
+
if (typeof v === "number" && isFinite(v) && Math.floor(v) === v && v > 0) return true;
|
|
815
|
+
}
|
|
816
|
+
return false;
|
|
817
|
+
}
|
|
818
|
+
|
|
806
819
|
function renderHero(p) {
|
|
807
820
|
// cost.usd is null when cost was not collected for this run. g() coerces
|
|
808
821
|
// null to its default, so read with a null default and branch BEFORE
|
|
809
822
|
// fmtUsd: a skeptic seeing a zero-dollar total assumes the artifact is fake.
|
|
810
823
|
var usdRaw = g(p, "cost.usd", null);
|
|
824
|
+
var partial = costIsPartial(p);
|
|
811
825
|
var fc = num(g(p, "files_changed.count", 0));
|
|
812
826
|
var parts = [__PROOF_RECEIPT_TITLE_JSON__];
|
|
813
|
-
if (usdRaw !== null) parts.push(fmtUsd(usdRaw));
|
|
827
|
+
if (usdRaw !== null) parts.push((partial ? "at least " : "") + fmtUsd(usdRaw));
|
|
814
828
|
else parts.push("cost not recorded");
|
|
815
829
|
parts.push(fc + " file" + (fc === 1 ? "" : "s") + " changed");
|
|
816
830
|
var cs = councilSummary(p);
|
|
@@ -825,7 +839,7 @@
|
|
|
825
839
|
if (prov || model) sub.push("via " + esc(prov) + (model ? " / " + esc(model) : ""));
|
|
826
840
|
document.getElementById("heroSub").innerHTML = sub.join(" · ");
|
|
827
841
|
|
|
828
|
-
renderHeroStats(p, usdRaw, fc, cs);
|
|
842
|
+
renderHeroStats(p, usdRaw, fc, cs, partial);
|
|
829
843
|
|
|
830
844
|
var actions = document.getElementById("heroActions");
|
|
831
845
|
var html = "";
|
|
@@ -872,7 +886,7 @@
|
|
|
872
886
|
actions.innerHTML = html + share;
|
|
873
887
|
}
|
|
874
888
|
|
|
875
|
-
function renderHeroStats(p, usdRaw, fc, cs) {
|
|
889
|
+
function renderHeroStats(p, usdRaw, fc, cs, partial) {
|
|
876
890
|
var el = document.getElementById("heroStats");
|
|
877
891
|
if (!el) return;
|
|
878
892
|
var cells = [];
|
|
@@ -895,7 +909,7 @@
|
|
|
895
909
|
}
|
|
896
910
|
cells.push(["Verified", vMain, esc(vSub), cs ? "ok" : ""]);
|
|
897
911
|
// Cost: never render a zero-dollar total when cost was not collected.
|
|
898
|
-
if (usdRaw !== null) cells.push(["Cost", fmtUsd(usdRaw), "total for this run", ""]);
|
|
912
|
+
if (usdRaw !== null) cells.push(["Cost", (partial ? "at least " : "") + fmtUsd(usdRaw), partial ? "lower bound, some cost not priced" : "total for this run", ""]);
|
|
899
913
|
else cells.push(["Cost", "not recorded", "no cost data", ""]);
|
|
900
914
|
// Duration.
|
|
901
915
|
var dur = g(p, "wall_clock_sec", null);
|
|
@@ -962,6 +976,7 @@
|
|
|
962
976
|
function renderCost(p) {
|
|
963
977
|
var card = document.getElementById("costCard");
|
|
964
978
|
var usdRaw = g(p, "cost.usd", null);
|
|
979
|
+
var partial = costIsPartial(p);
|
|
965
980
|
function tokenValue(path) {
|
|
966
981
|
var raw = g(p, path, null);
|
|
967
982
|
return raw === null ? "not recorded" : fmtInt(raw);
|
|
@@ -981,8 +996,8 @@
|
|
|
981
996
|
// a zero-dollar total (which reads as fake/broken to a skeptic).
|
|
982
997
|
var top;
|
|
983
998
|
if (usdRaw !== null) {
|
|
984
|
-
top = '<div class="cost-top"><span class="cost-usd">' + fmtUsd(usdRaw) +
|
|
985
|
-
'</span><span class="cost-label">total cost for this run, itemized below</span></div>';
|
|
999
|
+
top = '<div class="cost-top"><span class="cost-usd">' + (partial ? "at least " : "") + fmtUsd(usdRaw) +
|
|
1000
|
+
'</span><span class="cost-label">' + (partial ? "lower bound: some cost was not priced or recorded" : "total cost for this run, itemized below") + '</span></div>';
|
|
986
1001
|
} else {
|
|
987
1002
|
top = '<div class="cost-top"><span class="cost-label">Cost was not recorded for this run. ' +
|
|
988
1003
|
'Token usage is itemized below.</span></div>';
|
|
@@ -171,6 +171,12 @@ def _interventions_value(proof):
|
|
|
171
171
|
_IV_TYPES = ("steer", "input", "authorization", "repair")
|
|
172
172
|
|
|
173
173
|
|
|
174
|
+
_LIB_DIR = os.path.dirname(os.path.abspath(__file__))
|
|
175
|
+
if _LIB_DIR not in sys.path:
|
|
176
|
+
sys.path.insert(0, _LIB_DIR)
|
|
177
|
+
from efficiency_cost import cost_block_is_partial # noqa: E402
|
|
178
|
+
|
|
179
|
+
|
|
174
180
|
def _cost_value(proof):
|
|
175
181
|
usd = _obj(proof.get("cost")).get("usd")
|
|
176
182
|
if usd is None or isinstance(usd, bool):
|
|
@@ -236,6 +242,7 @@ def _load_runs(loki_dir):
|
|
|
236
242
|
"verdict": verdict,
|
|
237
243
|
"false_verified": false_verified,
|
|
238
244
|
"cost_usd": _cost_value(proof),
|
|
245
|
+
"cost_partial": cost_block_is_partial(_obj(proof.get("cost"))),
|
|
239
246
|
"interventions_by_type": by_type,
|
|
240
247
|
"waited_s": waited_s,
|
|
241
248
|
"active_s": active_s,
|
|
@@ -336,6 +343,9 @@ def outcome_effort(series):
|
|
|
336
343
|
cost_note = "no runs"
|
|
337
344
|
elif accepted == 0:
|
|
338
345
|
cost_note = "no accepted run, cost per accepted is undefined"
|
|
346
|
+
elif any(r.get("cost_partial") for r in series):
|
|
347
|
+
cost_note = "cost partial for %d of %d runs, a lower bound is not a total" % (
|
|
348
|
+
sum(1 for r in series if r.get("cost_partial")), len(series))
|
|
339
349
|
elif unmeasured:
|
|
340
350
|
cost_note = "cost unmeasured for %d of %d runs, a partial sum is not a total" % (unmeasured, len(series))
|
|
341
351
|
else:
|
|
@@ -449,7 +459,7 @@ def compute_trajectory(loki_dir):
|
|
|
449
459
|
"regressing_axes": regressing_axes,
|
|
450
460
|
"series": series,
|
|
451
461
|
"notes": notes,
|
|
452
|
-
"outcome_effort": outcome_effort(
|
|
462
|
+
"outcome_effort": outcome_effort(runs),
|
|
453
463
|
}
|
|
454
464
|
|
|
455
465
|
|
package/autonomy/loki
CHANGED
|
@@ -397,6 +397,24 @@ _loki_update_cache_path() {
|
|
|
397
397
|
echo "$HOME/.loki/cache/update-check-bash.json"
|
|
398
398
|
}
|
|
399
399
|
|
|
400
|
+
# _loki_verdict_badge <headline> <url> -> prints a "Verified by Loki" markdown
|
|
401
|
+
# badge for an honesty.headline, or nothing. Exact-match only: VERIFIED
|
|
402
|
+
# (brightgreen), VERIFIED WITH GAPS (orange), NOT VERIFIED (red). Any other
|
|
403
|
+
# headline, or an empty url, omits the badge (never green, never forced red).
|
|
404
|
+
_loki_verdict_badge() {
|
|
405
|
+
local headline="${1:-}" url="${2:-}" message color
|
|
406
|
+
url="${url#"${url%%[![:space:]]*}"}"; url="${url%"${url##*[![:space:]]}"}"
|
|
407
|
+
[ -n "$url" ] || return 0
|
|
408
|
+
case "$headline" in
|
|
409
|
+
"VERIFIED") message="verified"; color="brightgreen" ;;
|
|
410
|
+
"VERIFIED WITH GAPS") message="verified with gaps"; color="orange" ;;
|
|
411
|
+
"NOT VERIFIED") message="not verified"; color="red" ;;
|
|
412
|
+
*) return 0 ;;
|
|
413
|
+
esac
|
|
414
|
+
printf '[](%s)\n' \
|
|
415
|
+
"$message" "${message// /%20}" "$color" "$url"
|
|
416
|
+
}
|
|
417
|
+
|
|
400
418
|
# _loki_semver_newer <latest> <current> -> 0 (true) iff latest is strictly newer.
|
|
401
419
|
# Rejects anything that is not a plain MAJOR.MINOR.PATCH (e.g. "unknown",
|
|
402
420
|
# prereleases): on any unparseable input it returns 1 (never nudge).
|
|
@@ -9973,7 +9991,7 @@ _deploy_latest_receipt() {
|
|
|
9973
9991
|
return 0
|
|
9974
9992
|
}
|
|
9975
9993
|
|
|
9976
|
-
# _deploy_record <dir> <command> <exit_code> <run_id> <anchor_sha> [state] [health_status] [health_url] [rb_attempted] [rb_exit] [receipt_sha256] [started_at] [finished_at] [target] [canary] [post_rollback_health] [health_http_code] [health_match] [tree_sha] [destination]
|
|
9994
|
+
# _deploy_record <dir> <command> <exit_code> <run_id> <anchor_sha> [state] [health_status] [health_url] [rb_attempted] [rb_exit] [receipt_sha256] [started_at] [finished_at] [target] [canary] [post_rollback_health] [health_http_code] [health_match] [tree_sha] [destination] [deployed_url] [health_url_source]
|
|
9977
9995
|
# Writes the executed deploy's own record, then signs it (loki.deploy/1, Ed25519 via the
|
|
9978
9996
|
# receipt signer in loki-ts/src/util/receipt_signer.ts) so it is bound to the receipt that
|
|
9979
9997
|
# authorized it; `loki verify <record>` re-checks it offline. An unsigned record is reported
|
|
@@ -9997,6 +10015,7 @@ _deploy_record() {
|
|
|
9997
10015
|
local rb_attempted="${9:-false}" rb_exit="${10:-}"
|
|
9998
10016
|
local receipt_sha="${11:-}" started_at="${12:-}" finished_at="${13:-}" target="${14:-}" canary="${15:-false}"
|
|
9999
10017
|
local post_rb="${16:-unchecked}" hcode_rec="${17:-}" hmatch="${18:-}" tree_sha="${19:-}" dest_rec="${20:-}"
|
|
10018
|
+
local deployed_url="${21:-}" hsrc="${22:-}"
|
|
10000
10019
|
local ddir="$dir/.loki/deploys"
|
|
10001
10020
|
[ -d "${LOKI_DIR:-}" ] && ddir="${LOKI_DIR}/deploys"
|
|
10002
10021
|
mkdir -p "$ddir" 2>/dev/null || return 0
|
|
@@ -10004,7 +10023,7 @@ _deploy_record() {
|
|
|
10004
10023
|
DR_CMD="$command" DR_RC="$rc" DR_RUN="$run_id" DR_ANCHOR="$anchor" DR_OUT="$f" \
|
|
10005
10024
|
DR_STATE="$anchor_state" DR_HS="$health_status" DR_HU="$health_url" \
|
|
10006
10025
|
DR_RBA="$rb_attempted" DR_RBX="$rb_exit" DR_RSHA="$receipt_sha" DR_T0="$started_at" \
|
|
10007
|
-
DR_T1="$finished_at" DR_TGT="$target" DR_CAN="$canary" DR_PRB="$post_rb" DR_HC="$hcode_rec" DR_HM="$hmatch" DR_TREE="$tree_sha" DR_DEST="$dest_rec" \
|
|
10026
|
+
DR_T1="$finished_at" DR_TGT="$target" DR_CAN="$canary" DR_PRB="$post_rb" DR_HC="$hcode_rec" DR_HM="$hmatch" DR_TREE="$tree_sha" DR_DEST="$dest_rec" DR_DURL="$deployed_url" DR_HSRC="$hsrc" \
|
|
10008
10027
|
python3 -c '
|
|
10009
10028
|
import json, os, datetime
|
|
10010
10029
|
state = os.environ.get("DR_STATE") or "anchored"
|
|
@@ -10034,6 +10053,8 @@ json.dump({
|
|
|
10034
10053
|
"health_match": os.environ.get("DR_HM", ""),
|
|
10035
10054
|
"tree_sha": os.environ.get("DR_TREE", ""),
|
|
10036
10055
|
"destination": os.environ.get("DR_DEST", ""),
|
|
10056
|
+
"deployed_url": os.environ.get("DR_DURL", ""),
|
|
10057
|
+
"health_url_source": os.environ.get("DR_HSRC", ""),
|
|
10037
10058
|
"health_http_code": int(os.environ["DR_HC"]) if os.environ.get("DR_HC", "").isdigit() else None,
|
|
10038
10059
|
}, open(os.environ["DR_OUT"], "w"), indent=2)
|
|
10039
10060
|
' 2>/dev/null || return 0
|
|
@@ -10339,6 +10360,8 @@ _deploy_try_execute() {
|
|
|
10339
10360
|
|
|
10340
10361
|
local rc=0 started_at finished_at purl="" canary_out=""
|
|
10341
10362
|
started_at="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
|
10363
|
+
local deployed_url="" health_src=""
|
|
10364
|
+
[ -n "$health_url" ] && health_src="flag"
|
|
10342
10365
|
local health_status="unchecked" rb_attempted=false rb_exit="" hcode="" rb_hcode="" post_rb="unchecked"
|
|
10343
10366
|
if [ "$canary_kind" = vercel ]; then
|
|
10344
10367
|
# Staged: a preview deploy (no --prod), probe the preview URL, promote only if healthy.
|
|
@@ -10349,7 +10372,7 @@ _deploy_try_execute() {
|
|
|
10349
10372
|
printf '%s\n' "$canary_out"
|
|
10350
10373
|
purl="$(grep -Eo 'https?://[^[:space:]]+' <<<"$canary_out" | tail -n 1)"
|
|
10351
10374
|
if [ "$rc" -eq 0 ] && [ -n "$purl" ]; then
|
|
10352
|
-
health_url="$purl"
|
|
10375
|
+
health_url="$purl"; deployed_url="$purl"; health_src="captured"
|
|
10353
10376
|
echo "Probing canary health: ${health_url}"
|
|
10354
10377
|
if hcode="$(_deploy_probe_health "$health_url" "$health_match")"; then
|
|
10355
10378
|
health_status="healthy"
|
|
@@ -10365,7 +10388,30 @@ _deploy_try_execute() {
|
|
|
10365
10388
|
rc=4
|
|
10366
10389
|
fi
|
|
10367
10390
|
else
|
|
10368
|
-
|
|
10391
|
+
# Stream provider output live while keeping a copy to parse the URL from.
|
|
10392
|
+
# The copy lives in a private mktemp -d dir removed on exit and on INT/TERM/HUP.
|
|
10393
|
+
local dep_dir dep_tmp dep_errexit=0
|
|
10394
|
+
dep_dir="$(mktemp -d "${TMPDIR:-/tmp}/loki-deploy-out.XXXXXXXX")" || dep_dir=""
|
|
10395
|
+
dep_tmp=/dev/null
|
|
10396
|
+
if [ -n "$dep_dir" ]; then
|
|
10397
|
+
dep_tmp="${dep_dir}/out"
|
|
10398
|
+
# shellcheck disable=SC2064
|
|
10399
|
+
trap "rm -rf -- '${dep_dir}'; trap - INT TERM HUP; exit 130" INT TERM HUP
|
|
10400
|
+
fi
|
|
10401
|
+
case $- in *e*) dep_errexit=1 ;; esac
|
|
10402
|
+
set +e
|
|
10403
|
+
( cd "$dir" && eval "$exec_cmd" ) 2>&1 | tee "$dep_tmp"
|
|
10404
|
+
rc=${PIPESTATUS[0]}
|
|
10405
|
+
[ "$dep_errexit" -eq 1 ] && set -e
|
|
10406
|
+
if [ "$rc" -eq 0 ] && [ "$dep_tmp" != /dev/null ]; then
|
|
10407
|
+
# Same last-https-URL parse as the canary path; never invented.
|
|
10408
|
+
deployed_url="$(grep -Eo 'https?://[^[:space:]]+' "$dep_tmp" | tail -n 1)" || deployed_url=""
|
|
10409
|
+
if [ -n "$deployed_url" ]; then
|
|
10410
|
+
echo "Live at: ${deployed_url}"
|
|
10411
|
+
if [ -z "$health_url" ]; then health_url="$deployed_url"; health_src="captured"; fi
|
|
10412
|
+
fi
|
|
10413
|
+
fi
|
|
10414
|
+
if [ -n "$dep_dir" ]; then rm -rf -- "$dep_dir"; trap - INT TERM HUP; fi
|
|
10369
10415
|
fi
|
|
10370
10416
|
|
|
10371
10417
|
# Health probe only after a deploy that exited 0. Without a URL, or after a
|
|
@@ -10410,7 +10456,7 @@ _deploy_try_execute() {
|
|
|
10410
10456
|
rsha="$(python3 -c 'import json,sys;print((json.load(open(sys.argv[1])).get("verification") or {}).get("hash") or "")' "$pj" 2>/dev/null || printf '')"
|
|
10411
10457
|
rec="$(_deploy_record "$dir" "$exec_cmd" "$rc" "${run_id:-unknown}" "${anchor:-unknown}" \
|
|
10412
10458
|
"$anchor_state" "$health_status" "$health_url" "$rb_attempted" "$rb_exit" \
|
|
10413
|
-
"$rsha" "$started_at" "$finished_at" "${command%% *}" "$canary" "$post_rb" "$hcode" "$health_match" "$(git -C "$dir" rev-parse 'HEAD^{tree}' 2>/dev/null || true)" "$dest")"
|
|
10459
|
+
"$rsha" "$started_at" "$finished_at" "${command%% *}" "$canary" "$post_rb" "$hcode" "$health_match" "$(git -C "$dir" rev-parse 'HEAD^{tree}' 2>/dev/null || true)" "$dest" "$deployed_url" "$health_src")"
|
|
10414
10460
|
echo ""
|
|
10415
10461
|
if [ "$rc" -eq 0 ]; then
|
|
10416
10462
|
echo -e "${GREEN}Deploy command exited 0.${NC}"
|
|
@@ -11041,6 +11087,97 @@ except: print(0)
|
|
|
11041
11087
|
fi
|
|
11042
11088
|
;;
|
|
11043
11089
|
|
|
11090
|
+
publish)
|
|
11091
|
+
# Receipt-stamped export: push the verified tree to GitHub and set a
|
|
11092
|
+
# loki/receipt commit status whose state comes ONLY from the receipt
|
|
11093
|
+
# verdict. Never stages files; private unless --public is passed.
|
|
11094
|
+
local _pub_vis="--private" _pub_name="" _pub_dir="." _pub_usage=0
|
|
11095
|
+
while [ $# -gt 0 ]; do
|
|
11096
|
+
case "$1" in
|
|
11097
|
+
--public) _pub_vis="--public"; shift ;;
|
|
11098
|
+
--private) _pub_vis="--private"; shift ;;
|
|
11099
|
+
--dir)
|
|
11100
|
+
case "${2:-}" in ""|-*) _pub_usage=1; break ;; esac
|
|
11101
|
+
_pub_dir="$2"; shift 2 ;;
|
|
11102
|
+
-*) _pub_usage=1; break ;;
|
|
11103
|
+
*)
|
|
11104
|
+
[ -z "$_pub_name" ] || { _pub_usage=1; break; }
|
|
11105
|
+
_pub_name="$1"; shift ;;
|
|
11106
|
+
esac
|
|
11107
|
+
done
|
|
11108
|
+
if [ "$_pub_usage" -ne 0 ]; then
|
|
11109
|
+
echo "Usage: loki github publish [name] [--public] [--dir <path>]" >&2
|
|
11110
|
+
exit 2
|
|
11111
|
+
fi
|
|
11112
|
+
local _pub_refuse
|
|
11113
|
+
_pub_refuse="$(_deploy_tree_clean "$_pub_dir")"
|
|
11114
|
+
if [ -n "$_pub_refuse" ]; then
|
|
11115
|
+
echo -e "${RED}Refused: ${_pub_refuse}${NC}" >&2
|
|
11116
|
+
exit 1
|
|
11117
|
+
fi
|
|
11118
|
+
if ! command -v gh &>/dev/null; then
|
|
11119
|
+
echo -e "${RED}Error: gh CLI not found. Install with: brew install gh${NC}" >&2
|
|
11120
|
+
exit 1
|
|
11121
|
+
fi
|
|
11122
|
+
if ! gh auth status &>/dev/null; then
|
|
11123
|
+
echo -e "${RED}Error: gh CLI not authenticated. Run: gh auth login${NC}" >&2
|
|
11124
|
+
exit 1
|
|
11125
|
+
fi
|
|
11126
|
+
local _pub_sha _pub_pj _pub_verdict="NONE" _pub_anchor="" _pub_state="none"
|
|
11127
|
+
_pub_sha="$(git -C "$_pub_dir" rev-parse HEAD 2>/dev/null)" || _pub_sha=""
|
|
11128
|
+
if [ -z "$_pub_sha" ]; then
|
|
11129
|
+
echo -e "${RED}Refused: no HEAD commit to publish${NC}" >&2
|
|
11130
|
+
exit 1
|
|
11131
|
+
fi
|
|
11132
|
+
_pub_pj="$(_deploy_latest_receipt "$_pub_dir")"
|
|
11133
|
+
if [ -n "$_pub_pj" ]; then
|
|
11134
|
+
_pub_verdict="$(_deploy_receipt_verdict "$_pub_pj")"
|
|
11135
|
+
_pub_anchor="$(_deploy_anchor_ok "$_pub_pj" "$_pub_dir")"
|
|
11136
|
+
if [ "$_pub_verdict" = "VERIFIED" ] && [ -z "$_pub_anchor" ]; then
|
|
11137
|
+
_pub_state="success"
|
|
11138
|
+
else
|
|
11139
|
+
_pub_state="failure"
|
|
11140
|
+
fi
|
|
11141
|
+
fi
|
|
11142
|
+
|
|
11143
|
+
local _pub_url="" _pub_out=""
|
|
11144
|
+
if git -C "$_pub_dir" remote get-url origin &>/dev/null; then
|
|
11145
|
+
if [ -n "$_pub_anchor" ] || [ -z "$_pub_pj" ]; then
|
|
11146
|
+
echo -e "${RED}Refused: HEAD is not confirmed as the receipt anchor (${_pub_anchor:-no Evidence Receipt found}). Nothing was pushed.${NC}" >&2
|
|
11147
|
+
exit 1
|
|
11148
|
+
fi
|
|
11149
|
+
git -C "$_pub_dir" push origin HEAD || exit 1
|
|
11150
|
+
else
|
|
11151
|
+
[ -n "$_pub_name" ] || _pub_name="$(basename "$(cd "$_pub_dir" && pwd)")"
|
|
11152
|
+
_pub_out="$(cd "$_pub_dir" && gh repo create "$_pub_name" "$_pub_vis" --source . --push 2>&1)" || {
|
|
11153
|
+
echo "$_pub_out" >&2
|
|
11154
|
+
echo -e "${RED}gh repo create failed; nothing published.${NC}" >&2
|
|
11155
|
+
exit 1
|
|
11156
|
+
}
|
|
11157
|
+
_pub_url="$(printf '%s\n' "$_pub_out" | grep -Eo 'https://github\.com/[^[:space:]]+' | head -1)"
|
|
11158
|
+
fi
|
|
11159
|
+
[ -n "$_pub_url" ] || _pub_url="$(git -C "$_pub_dir" remote get-url origin 2>/dev/null || true)"
|
|
11160
|
+
|
|
11161
|
+
local _pub_slug
|
|
11162
|
+
_pub_slug="$(printf '%s' "$_pub_url" | sed -E 's|^.*github\.com[:/]||; s|\.git$||')"
|
|
11163
|
+
if [ "$_pub_state" = "none" ]; then
|
|
11164
|
+
echo -e "${YELLOW}No Evidence Receipt found: no loki/receipt status set.${NC}"
|
|
11165
|
+
elif [ -z "$_pub_slug" ] || [ "$_pub_slug" = "$_pub_url" ]; then
|
|
11166
|
+
echo -e "${YELLOW}Could not resolve owner/repo: no loki/receipt status set.${NC}"
|
|
11167
|
+
else
|
|
11168
|
+
gh api "repos/${_pub_slug}/statuses/${_pub_sha}" \
|
|
11169
|
+
-f "state=${_pub_state}" -f "context=loki/receipt" \
|
|
11170
|
+
-f "description=Evidence Receipt verdict: ${_pub_verdict}" >/dev/null \
|
|
11171
|
+
|| echo -e "${YELLOW}Warning: could not set the loki/receipt status.${NC}" >&2
|
|
11172
|
+
echo "loki/receipt status: ${_pub_state} (receipt ${_pub_verdict})"
|
|
11173
|
+
fi
|
|
11174
|
+
if [ -n "$_pub_url" ]; then
|
|
11175
|
+
echo "Repository: ${_pub_url}"
|
|
11176
|
+
else
|
|
11177
|
+
echo "Repository URL could not be determined."
|
|
11178
|
+
fi
|
|
11179
|
+
;;
|
|
11180
|
+
|
|
11044
11181
|
help|*)
|
|
11045
11182
|
echo -e "${BOLD}loki github${NC} - GitHub integration management"
|
|
11046
11183
|
echo ""
|
|
@@ -11049,6 +11186,7 @@ except: print(0)
|
|
|
11049
11186
|
echo " sync Sync completed task status back to GitHub issues"
|
|
11050
11187
|
echo " export Export local tasks as new GitHub issues"
|
|
11051
11188
|
echo " pr [name] Create pull request from completed work"
|
|
11189
|
+
echo " publish [name] [--public] Push to GitHub and set a loki/receipt commit status"
|
|
11052
11190
|
echo ""
|
|
11053
11191
|
echo "Environment Variables:"
|
|
11054
11192
|
echo " LOKI_GITHUB_IMPORT=true Import open issues as tasks on start"
|
|
@@ -38275,39 +38413,21 @@ PYEOF
|
|
|
38275
38413
|
# The badge links to the gist we just published (no new egress).
|
|
38276
38414
|
# shields.io static/v1 query endpoint avoids structural hyphens.
|
|
38277
38415
|
local badge
|
|
38278
|
-
badge=$(LOKI_PROOF_JSON="$pj"
|
|
38416
|
+
badge=$(LOKI_PROOF_JSON="$pj" "$_share_py3" -E - <<'PYEOF' 2>/dev/null || true
|
|
38279
38417
|
# The cwd is the checkout being shared: drop it from sys.path before any
|
|
38280
38418
|
# other import, or a committed json.py/os.py could answer this badge text.
|
|
38281
38419
|
import sys; sys.path[:] = [p for p in sys.path if p not in ("", ".")]
|
|
38282
38420
|
import json, os
|
|
38283
|
-
from urllib.parse import quote
|
|
38284
38421
|
try:
|
|
38285
38422
|
d = json.load(open(os.environ["LOKI_PROOF_JSON"]))
|
|
38286
38423
|
except Exception:
|
|
38287
38424
|
d = {}
|
|
38288
|
-
|
|
38289
|
-
|
|
38290
|
-
|
|
38291
|
-
# overrides. Unmapped headline -> no entry -> badge omitted.
|
|
38292
|
-
mapping = {
|
|
38293
|
-
"VERIFIED": ("verified", "brightgreen"),
|
|
38294
|
-
"VERIFIED WITH GAPS": ("verified with gaps", "orange"),
|
|
38295
|
-
"NOT VERIFIED": ("not verified", "red"),
|
|
38296
|
-
}
|
|
38297
|
-
entry = mapping.get(headline)
|
|
38298
|
-
url = os.environ.get("LOKI_GIST_URL", "").strip()
|
|
38299
|
-
# Omit the badge for any unmapped headline (never green, never forced red),
|
|
38300
|
-
# and when we have no link target to point at.
|
|
38301
|
-
if entry and url:
|
|
38302
|
-
message, color = entry
|
|
38303
|
-
badge_img = (
|
|
38304
|
-
"https://img.shields.io/static/v1?label=Loki"
|
|
38305
|
-
"&message=" + quote(message)
|
|
38306
|
-
+ "&color=" + color
|
|
38307
|
-
)
|
|
38308
|
-
print("[]({url})".format(msg=message, img=badge_img, url=url))
|
|
38425
|
+
h = (d.get("honesty") or {}).get("headline")
|
|
38426
|
+
if isinstance(h, str):
|
|
38427
|
+
print(h)
|
|
38309
38428
|
PYEOF
|
|
38310
38429
|
)
|
|
38430
|
+
badge=$(_loki_verdict_badge "$badge" "$LOKI_LAST_GIST_URL")
|
|
38311
38431
|
if [ -n "$badge" ]; then
|
|
38312
38432
|
echo ""
|
|
38313
38433
|
echo "Markdown badge (paste into a README or PR):"
|
package/autonomy/mcp-launch.sh
CHANGED
|
@@ -150,6 +150,7 @@ _ml_help() {
|
|
|
150
150
|
Loki Mode -- launch the MCP (Model Context Protocol) server
|
|
151
151
|
|
|
152
152
|
Usage: loki mcp [--transport stdio|http] [--port N] [--help]
|
|
153
|
+
loki mcp install --client claude|cursor|vscode|windsurf [--project|--global] [--print]
|
|
153
154
|
|
|
154
155
|
Starts the Loki Mode MCP server so MCP-aware clients (Claude Code, IDEs)
|
|
155
156
|
can call Loki's tools (memory, task queue, code search, build management).
|
|
@@ -200,8 +201,164 @@ install command to stderr and exits 2.
|
|
|
200
201
|
EOF
|
|
201
202
|
}
|
|
202
203
|
|
|
204
|
+
# --- loki mcp install (TEN-ADOPTION-05) ------------------------------------
|
|
205
|
+
# Registers the loki-mode stdio server (`npx -y loki-mode mcp`) in an MCP
|
|
206
|
+
# client's config. The JSON merge keeps every other server and key, is a no-op
|
|
207
|
+
# (byte-identical, no .bak) when loki-mode is already registered with the same
|
|
208
|
+
# entry, writes <file>.bak before changing an existing file, and refuses to
|
|
209
|
+
# touch invalid JSON.
|
|
210
|
+
|
|
211
|
+
_ml_install_help() {
|
|
212
|
+
cat <<'EOF'
|
|
213
|
+
Usage: loki mcp install --client claude|cursor|vscode|windsurf [--project|--global] [--print]
|
|
214
|
+
|
|
215
|
+
Adds the loki-mode MCP server (npx -y loki-mode mcp) to a client's config.
|
|
216
|
+
|
|
217
|
+
claude runs `claude mcp add loki-mode -- npx -y loki-mode mcp` when the
|
|
218
|
+
claude CLI is on PATH, otherwise prints that command.
|
|
219
|
+
cursor .cursor/mcp.json (--project, default) or ~/.cursor/mcp.json (--global)
|
|
220
|
+
vscode .vscode/mcp.json (project only), under the "servers" key
|
|
221
|
+
windsurf ~/.codeium/windsurf/mcp_config.json (global only)
|
|
222
|
+
|
|
223
|
+
Options:
|
|
224
|
+
--project Write to the current project (claude: --scope project).
|
|
225
|
+
--global Write to the user config (claude: --scope user).
|
|
226
|
+
--print Print what would be written to stdout; write nothing.
|
|
227
|
+
|
|
228
|
+
Existing files are merged (other servers and keys are kept), backed up to
|
|
229
|
+
<file>.bak before a change, and left untouched if they are not valid JSON.
|
|
230
|
+
EOF
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
# _ml_json_merge <file> <topkey> <type|-> : merge the loki-mode entry into file.
|
|
234
|
+
# Exit codes: 0 ok (changed or already current), 3 invalid or unusable JSON.
|
|
235
|
+
_ml_json_merge() {
|
|
236
|
+
python3 - "$1" "$2" "$3" <<'PY'
|
|
237
|
+
import json, os, shutil, sys
|
|
238
|
+
path, top, typ = sys.argv[1], sys.argv[2], sys.argv[3]
|
|
239
|
+
entry = {"command": "npx", "args": ["-y", "loki-mode", "mcp"]}
|
|
240
|
+
if typ != "-":
|
|
241
|
+
entry = dict([("type", typ)] + list(entry.items()))
|
|
242
|
+
exists = os.path.exists(path)
|
|
243
|
+
data = {}
|
|
244
|
+
if exists:
|
|
245
|
+
try:
|
|
246
|
+
with open(path) as fh:
|
|
247
|
+
raw = fh.read()
|
|
248
|
+
data = json.loads(raw) if raw.strip() else {}
|
|
249
|
+
except (ValueError, OSError) as exc:
|
|
250
|
+
sys.stderr.write("Refusing to modify %s: not valid JSON (%s)\n" % (path, exc))
|
|
251
|
+
sys.exit(3)
|
|
252
|
+
if not isinstance(data, dict) or not isinstance(data.get(top, {}), dict):
|
|
253
|
+
sys.stderr.write("Refusing to modify %s: unexpected structure (need an object with an object '%s')\n" % (path, top))
|
|
254
|
+
sys.exit(3)
|
|
255
|
+
servers = data.setdefault(top, {})
|
|
256
|
+
if servers.get("loki-mode") == entry:
|
|
257
|
+
print("loki-mode already registered in %s" % path)
|
|
258
|
+
sys.exit(0)
|
|
259
|
+
servers["loki-mode"] = entry
|
|
260
|
+
if exists:
|
|
261
|
+
shutil.copy2(path, path + ".bak")
|
|
262
|
+
else:
|
|
263
|
+
os.makedirs(os.path.dirname(path) or ".", exist_ok=True)
|
|
264
|
+
with open(path, "w") as fh:
|
|
265
|
+
json.dump(data, fh, indent=2)
|
|
266
|
+
fh.write("\n")
|
|
267
|
+
print("Registered loki-mode in %s" % path)
|
|
268
|
+
PY
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
_ml_install_main() {
|
|
272
|
+
local client="" scope="" do_print=0
|
|
273
|
+
while [ $# -gt 0 ]; do
|
|
274
|
+
case "$1" in
|
|
275
|
+
--client) client="${2:-}"; shift 2 2>/dev/null || shift ;;
|
|
276
|
+
--client=*) client="${1#--client=}"; shift ;;
|
|
277
|
+
--project) scope="project"; shift ;;
|
|
278
|
+
--global) scope="global"; shift ;;
|
|
279
|
+
--print) do_print=1; shift ;;
|
|
280
|
+
--help|-h|help) _ml_install_help; return 0 ;;
|
|
281
|
+
*)
|
|
282
|
+
printf 'Unknown option for loki mcp install: %s\n' "$1" >&2
|
|
283
|
+
_ml_install_help >&2
|
|
284
|
+
return 2
|
|
285
|
+
;;
|
|
286
|
+
esac
|
|
287
|
+
done
|
|
288
|
+
case "$client" in
|
|
289
|
+
claude|cursor|vscode|windsurf) ;;
|
|
290
|
+
*)
|
|
291
|
+
printf 'loki mcp install: --client must be one of claude, cursor, vscode, windsurf (got: %s)\n' "${client:-none}" >&2
|
|
292
|
+
return 2
|
|
293
|
+
;;
|
|
294
|
+
esac
|
|
295
|
+
|
|
296
|
+
if [ "$client" = "claude" ]; then
|
|
297
|
+
local -a cmd=(claude mcp add)
|
|
298
|
+
case "$scope" in
|
|
299
|
+
project) cmd+=(--scope project) ;;
|
|
300
|
+
global) cmd+=(--scope user) ;;
|
|
301
|
+
esac
|
|
302
|
+
cmd+=(loki-mode -- npx -y loki-mode mcp)
|
|
303
|
+
if [ "$do_print" -eq 1 ] || ! command -v claude >/dev/null 2>&1; then
|
|
304
|
+
[ "$do_print" -eq 1 ] || printf 'claude CLI not found on PATH; run this command yourself:\n' >&2
|
|
305
|
+
printf '%s\n' "${cmd[*]}"
|
|
306
|
+
return 0
|
|
307
|
+
fi
|
|
308
|
+
"${cmd[@]}"
|
|
309
|
+
return $?
|
|
310
|
+
fi
|
|
311
|
+
|
|
312
|
+
local file top typ="-"
|
|
313
|
+
case "$client" in
|
|
314
|
+
cursor)
|
|
315
|
+
top="mcpServers"
|
|
316
|
+
if [ "$scope" = "global" ]; then file="$HOME/.cursor/mcp.json"; else file="$PWD/.cursor/mcp.json"; fi
|
|
317
|
+
;;
|
|
318
|
+
vscode)
|
|
319
|
+
top="servers"; typ="stdio"
|
|
320
|
+
if [ "$scope" = "global" ]; then
|
|
321
|
+
printf 'loki mcp install: vscode supports --project only (workspace .vscode/mcp.json)\n' >&2
|
|
322
|
+
return 2
|
|
323
|
+
fi
|
|
324
|
+
file="$PWD/.vscode/mcp.json"
|
|
325
|
+
;;
|
|
326
|
+
windsurf)
|
|
327
|
+
top="mcpServers"
|
|
328
|
+
if [ "$scope" = "project" ]; then
|
|
329
|
+
printf 'loki mcp install: windsurf supports --global only (~/.codeium/windsurf/mcp_config.json)\n' >&2
|
|
330
|
+
return 2
|
|
331
|
+
fi
|
|
332
|
+
file="$HOME/.codeium/windsurf/mcp_config.json"
|
|
333
|
+
;;
|
|
334
|
+
esac
|
|
335
|
+
|
|
336
|
+
if ! command -v python3 >/dev/null 2>&1; then
|
|
337
|
+
printf 'loki mcp install needs python3 on PATH for the JSON merge.\n' >&2
|
|
338
|
+
return 2
|
|
339
|
+
fi
|
|
340
|
+
if [ "$do_print" -eq 1 ]; then
|
|
341
|
+
printf 'Would write to %s\n' "$file" >&2
|
|
342
|
+
python3 - "$top" "$typ" <<'PY'
|
|
343
|
+
import json, sys
|
|
344
|
+
top, typ = sys.argv[1], sys.argv[2]
|
|
345
|
+
entry = {"command": "npx", "args": ["-y", "loki-mode", "mcp"]}
|
|
346
|
+
if typ != "-":
|
|
347
|
+
entry = dict([("type", typ)] + list(entry.items()))
|
|
348
|
+
print(json.dumps({top: {"loki-mode": entry}}, indent=2))
|
|
349
|
+
PY
|
|
350
|
+
return 0
|
|
351
|
+
fi
|
|
352
|
+
_ml_json_merge "$file" "$top" "$typ"
|
|
353
|
+
}
|
|
354
|
+
|
|
203
355
|
# mcp_launch_main: dispatcher invoked by cmd_mcp() (autonomy/loki) or directly.
|
|
204
356
|
mcp_launch_main() {
|
|
357
|
+
if [ "${1:-}" = "install" ]; then
|
|
358
|
+
shift
|
|
359
|
+
_ml_install_main "$@"
|
|
360
|
+
return $?
|
|
361
|
+
fi
|
|
205
362
|
# Split argv into launcher-owned flags (consumed here) and server argv
|
|
206
363
|
# (forwarded verbatim to the file-exec launch `python "$root/mcp/server.py"`).
|
|
207
364
|
# The server's argparse only
|
package/completions/_loki
CHANGED
|
@@ -242,6 +242,7 @@ function _loki_commands {
|
|
|
242
242
|
'secure:Secure-by-default gate (findings and waivers)'
|
|
243
243
|
'own:Finish-and-own ownership handoff'
|
|
244
244
|
'backup:Back up dashboard state (verified archive)'
|
|
245
|
+
'restore:Restore dashboard state from a verified backup archive'
|
|
245
246
|
'telemetry:OpenTelemetry management'
|
|
246
247
|
'agent:Agent type dispatch'
|
|
247
248
|
'doctor:Check system prerequisites'
|