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.
Files changed (87) hide show
  1. package/SKILL.md +2 -2
  2. package/VERSION +1 -1
  3. package/autonomy/lib/cost-summary.py +5 -2
  4. package/autonomy/lib/efficiency_cost.py +13 -0
  5. package/autonomy/lib/proof-generator.py +5 -0
  6. package/autonomy/lib/proof-template.html +21 -6
  7. package/autonomy/lib/trust_trajectory.py +11 -1
  8. package/autonomy/loki +148 -28
  9. package/autonomy/mcp-launch.sh +157 -0
  10. package/completions/_loki +1 -0
  11. package/completions/loki.bash +1 -1
  12. package/dashboard/__init__.py +1 -1
  13. package/docs/CLI-REFERENCE.md +2 -1
  14. package/docs/INSTALLATION.md +1 -1
  15. package/docs/dev/release-checklist.md +5 -5
  16. package/docs/v10/BOARD.md +101 -11
  17. package/docs/v10/CLI-MODERN.md +1 -1
  18. package/docs/v10/DECISIONS.md +13 -1
  19. package/docs/v10/DEPS.md +2 -6
  20. package/docs/v10/FAILURE-CLASSES.md +77 -0
  21. package/docs/v10/OPERATING-MODEL.md +2 -3
  22. package/docs/v10/PROGRESS.md +4 -0
  23. package/loki-ts/dist/loki.js +1339 -1226
  24. package/mcp/__init__.py +1 -1
  25. package/package.json +2 -2
  26. package/packages/control-plane/dist/ask-tools-server.js +16 -2
  27. package/packages/control-plane/dist/server.js +16967 -4228
  28. package/plugins/loki-mode/.claude-plugin/plugin.json +1 -1
  29. package/skills/factory-operations.md +6 -7
  30. package/skills/release-cadence.md +12 -16
  31. package/web-app/dist/assets/{AdminPage-CFqo-FpE.js → AdminPage-ChpOd95F.js} +1 -1
  32. package/web-app/dist/assets/{Avatar-CxDqlcAZ.js → Avatar-BCRrItym.js} +1 -1
  33. package/web-app/dist/assets/{Badge-CjZcSHSd.js → Badge-aTDM3e-d.js} +1 -1
  34. package/web-app/dist/assets/{Button-BsTd60w2.js → Button-Bp7dPZHt.js} +1 -1
  35. package/web-app/dist/assets/{CockpitPage-BivdM9_z.js → CockpitPage-DXuQVvsy.js} +1 -1
  36. package/web-app/dist/assets/{ComparePage-DEsuYGnF.js → ComparePage-DVf-QrAL.js} +1 -1
  37. package/web-app/dist/assets/{ErrorBoundary-Q2L0QzKv.js → ErrorBoundary-C4-CFvBo.js} +1 -1
  38. package/web-app/dist/assets/{EvidenceReceiptPanel-Da0-q_vi.js → EvidenceReceiptPanel-D7NCAclv.js} +1 -1
  39. package/web-app/dist/assets/{GitHubIssuesPanel-MSIQvU9a.js → GitHubIssuesPanel-DUWUS_gf.js} +1 -1
  40. package/web-app/dist/assets/{GitHubPRsPanel-Dw-t46_A.js → GitHubPRsPanel-nqnsewFG.js} +1 -1
  41. package/web-app/dist/assets/{HomePage-k_r93vg_.js → HomePage-myY1IPaZ.js} +2 -2
  42. package/web-app/dist/assets/{LoginPage-C-TlmbEy.js → LoginPage-Dp3Q-zqb.js} +1 -1
  43. package/web-app/dist/assets/{MagicPage-Dv_lGAX0.js → MagicPage-UGumm-Vb.js} +1 -1
  44. package/web-app/dist/assets/{MetricsPage-BINwnc6C.js → MetricsPage-UJifvRBZ.js} +1 -1
  45. package/web-app/dist/assets/{NotFoundPage-DGbdLAhm.js → NotFoundPage-CVVnnTdN.js} +1 -1
  46. package/web-app/dist/assets/{ProjectPage-D4GhODo7.js → ProjectPage-DXfW32Qw.js} +3 -3
  47. package/web-app/dist/assets/{ProjectsPage-C9iCGUvI.js → ProjectsPage-DYjNa3Ul.js} +1 -1
  48. package/web-app/dist/assets/{SettingsPage-08SiNkKI.js → SettingsPage-CYt9lALu.js} +1 -1
  49. package/web-app/dist/assets/{ShowcasePage-5cq8wXvh.js → ShowcasePage-CxUSXgdd.js} +1 -1
  50. package/web-app/dist/assets/{SystemSettingsPage-ch0Ex4iS.js → SystemSettingsPage-Dfe9kQ4p.js} +1 -1
  51. package/web-app/dist/assets/{TeamsPage-eE9Bevpx.js → TeamsPage-Cl6nQK8U.js} +1 -1
  52. package/web-app/dist/assets/{TemplatesPage-BLqK2AOM.js → TemplatesPage-B7NyDrDQ.js} +1 -1
  53. package/web-app/dist/assets/{TerminalOutput-Bw6lAjMM.js → TerminalOutput-BtdCEvxP.js} +1 -1
  54. package/web-app/dist/assets/{activity-mqeYLy0X.js → activity-CBaJkpBF.js} +1 -1
  55. package/web-app/dist/assets/{bell-RrSQYwnk.js → bell-BLwbfiL8.js} +1 -1
  56. package/web-app/dist/assets/{bot-BAYArzbY.js → bot-D4GG_jRl.js} +1 -1
  57. package/web-app/dist/assets/{check-DpZMgeCp.js → check-Beu8XP_r.js} +1 -1
  58. package/web-app/dist/assets/{chevron-left-HdiZ5i2v.js → chevron-left-BBljQPzv.js} +1 -1
  59. package/web-app/dist/assets/{circle-alert-CIGI9S7n.js → circle-alert-BKe5EycX.js} +1 -1
  60. package/web-app/dist/assets/{clock-DyGC5H7u.js → clock-BJznpp14.js} +1 -1
  61. package/web-app/dist/assets/{cloud-CCV2xWgX.js → cloud-DySaUo9u.js} +1 -1
  62. package/web-app/dist/assets/{code-xml-DZgWjvzk.js → code-xml-BAHWdutm.js} +1 -1
  63. package/web-app/dist/assets/{database-kzfwKPqD.js → database-B-rnmmI9.js} +1 -1
  64. package/web-app/dist/assets/{dollar-sign-jepNmrzR.js → dollar-sign-DPvLnmRA.js} +1 -1
  65. package/web-app/dist/assets/{file-code-corner-BaJhHJXN.js → file-code-corner-DxSeGCx5.js} +1 -1
  66. package/web-app/dist/assets/{file-plus-rONhSBM8.js → file-plus-DBe-sHuu.js} +1 -1
  67. package/web-app/dist/assets/{globe-DEKjsEXs.js → globe-C731sdYH.js} +1 -1
  68. package/web-app/dist/assets/{hammer-LFrBT_V4.js → hammer-ID64a_PJ.js} +1 -1
  69. package/web-app/dist/assets/{index-DTi6txFh.js → index-vVTm0HUs.js} +3 -3
  70. package/web-app/dist/assets/{layers-D0AF3hqY.js → layers-B5O1ewPy.js} +1 -1
  71. package/web-app/dist/assets/{loader-circle-5MojTTeG.js → loader-circle-CHCypEMG.js} +1 -1
  72. package/web-app/dist/assets/{lock-DKTO02bm.js → lock-CWe1ZGE8.js} +1 -1
  73. package/web-app/dist/assets/{package-0-plMCjY.js → package-Cg7pKkdo.js} +1 -1
  74. package/web-app/dist/assets/{plus-CjhYDJ6n.js → plus-B7Iacdk0.js} +1 -1
  75. package/web-app/dist/assets/{refresh-cw-CLC6VIEp.js → refresh-cw-974og1i_.js} +1 -1
  76. package/web-app/dist/assets/{rotate-ccw-DInfsBjr.js → rotate-ccw-C24LsDRT.js} +1 -1
  77. package/web-app/dist/assets/{scroll-text-DHh5UjH-.js → scroll-text-mdWBPKUQ.js} +1 -1
  78. package/web-app/dist/assets/{server-D0j8HyOw.js → server-B0cYzaeL.js} +1 -1
  79. package/web-app/dist/assets/{shield-alert-D8wHooc9.js → shield-alert-BgmGsVHG.js} +1 -1
  80. package/web-app/dist/assets/{thumbs-up-Cda_qfHU.js → thumbs-up-D8MPgXSO.js} +1 -1
  81. package/web-app/dist/assets/{trash-2-eyrU5HYs.js → trash-2-CuI9WflI.js} +1 -1
  82. package/web-app/dist/assets/{trending-down-Br_sStsY.js → trending-down-Dm-T7tH-.js} +1 -1
  83. package/web-app/dist/assets/{trending-up-DGmTcXWO.js → trending-up-CsyCaUiY.js} +1 -1
  84. package/web-app/dist/assets/{upload-CvCVXqWR.js → upload-CTzGge6W.js} +1 -1
  85. package/web-app/dist/assets/{usePolling-DT-cLGOG.js → usePolling-C2hDNNzp.js} +1 -1
  86. package/web-app/dist/assets/{user-tzdYvO9g.js → user-DgoCaZlg.js} +1 -1
  87. 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.11
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.11 | [Autonomi](https://www.autonomi.dev/) flagship product | core skill**
486
+ **v11.17.15 | [Autonomi](https://www.autonomi.dev/) flagship product | core skill**
package/VERSION CHANGED
@@ -1 +1 @@
1
- 11.17.11
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(series),
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 '[![Loki: %s](https://img.shields.io/static/v1?label=Loki&message=%s&color=%s)](%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
- ( cd "$dir" && eval "$exec_cmd" ) || rc=$?
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" LOKI_GIST_URL="$LOKI_LAST_GIST_URL" "$_share_py3" -E - <<'PYEOF' 2>/dev/null || true
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
- headline = (d.get("honesty") or {}).get("headline")
38289
- # Exact-match ONLY. council.final_verdict is intentionally NOT consulted: it is
38290
- # an LLM opinion, the precise fake-green source the deterministic headline
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("[![Loki: {msg}]({img})]({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):"
@@ -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'