@codyswann/lisa 2.275.0 → 2.276.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/dist/cli/ui-cmd.d.ts +3 -0
  2. package/dist/cli/ui-cmd.d.ts.map +1 -1
  3. package/dist/cli/ui-cmd.js +11 -2
  4. package/dist/cli/ui-cmd.js.map +1 -1
  5. package/dist/cli/ui-health.d.ts +18 -0
  6. package/dist/cli/ui-health.d.ts.map +1 -0
  7. package/dist/cli/ui-health.js +140 -0
  8. package/dist/cli/ui-health.js.map +1 -0
  9. package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
  10. package/dist/core/upstream-evidence-manifest.js +13 -2
  11. package/dist/core/upstream-evidence-manifest.js.map +1 -1
  12. package/package.json +1 -1
  13. package/plugins/lisa/.claude-plugin/plugin.json +1 -1
  14. package/plugins/lisa/.codex-plugin/plugin.json +1 -1
  15. package/plugins/lisa-agy/plugin.json +1 -1
  16. package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
  17. package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
  18. package/plugins/lisa-cdk-agy/plugin.json +1 -1
  19. package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
  20. package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
  21. package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
  22. package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
  23. package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
  24. package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
  25. package/plugins/lisa-expo-agy/plugin.json +1 -1
  26. package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
  27. package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
  28. package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
  29. package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
  30. package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
  31. package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
  32. package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
  33. package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
  34. package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
  35. package/plugins/lisa-nestjs-agy/plugin.json +1 -1
  36. package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
  37. package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
  38. package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
  39. package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
  40. package/plugins/lisa-openclaw-agy/plugin.json +1 -1
  41. package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
  42. package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
  43. package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
  44. package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
  45. package/plugins/lisa-phaser-agy/plugin.json +1 -1
  46. package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
  47. package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
  48. package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
  49. package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
  50. package/plugins/lisa-rails-agy/plugin.json +1 -1
  51. package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
  52. package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
  53. package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
  54. package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
  55. package/plugins/lisa-typescript-agy/plugin.json +1 -1
  56. package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
  57. package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
  58. package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
  59. package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
  60. package/plugins/lisa-wiki-agy/plugin.json +1 -1
  61. package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
  62. package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
  63. package/ui/README.md +18 -10
  64. package/ui/index.html +316 -96
package/ui/index.html CHANGED
@@ -1837,12 +1837,11 @@
1837
1837
  DATA.sections.health = {
1838
1838
  title: "Health",
1839
1839
  desc: "Whether this project is on the latest Lisa and completely in band — every Lisa-managed surface matching what the installed version would emit. Version status is always visible (top bar); the in-band scan runs on demand.",
1840
- src: "lisa doctor · lisa sync --dry-run · planned /lisa:health skill",
1840
+ src: "lisa health · /lisa:health skill · .lisa/health/latest.json",
1841
1841
  blocks: [
1842
1842
  {
1843
1843
  type: "callout",
1844
- variant: "warn",
1845
- text: "<b>Planned feature.</b> The version status is real today (lisa's npm update check). The in-band scan below documents the contract for a health-check skill that mixes deterministic checks with agentic review — not wired up yet.",
1844
+ text: '<b>Live health.</b> The console runs the shared deterministic Health v1 consumer against this local project. It reads and writes the same canonical result as <span class="mono">lisa health</span>; optional agentic evaluation remains a coding-harness concern.',
1846
1845
  },
1847
1846
  {
1848
1847
  card: "Lisa version",
@@ -1874,17 +1873,18 @@
1874
1873
  },
1875
1874
  {
1876
1875
  card: "In-band scan",
1877
- note: "Deterministic checks + agentic review, packaged as a skill",
1876
+ note: "Deterministic Health v1 checks",
1878
1877
  rows: [
1879
1878
  {
1880
1879
  key: "",
1881
- label: "Run health check",
1882
- desc: "Scans every Lisa-managed surface and reports drift: managed files vs templates, package.json governance, config population, hooks, plugins, CI workflows, rulesets — then an agentic pass reviews what a diff can't judge.",
1880
+ label: "Run deterministic health check",
1881
+ desc: "Scans every Lisa-managed surface deterministically and reports drift: managed files vs templates, package.json governance, config population, hooks, plugins, CI workflows, and rulesets.",
1883
1882
  control: {
1884
1883
  type: "button",
1885
- label: "Run health check",
1886
- disabled: true,
1887
- reason: "read-only: the health engine has not shipped yet",
1884
+ id: "runHealthCheckButton",
1885
+ action: "run-health",
1886
+ label: "Run deterministic health check",
1887
+ statusId: "healthRunStatus",
1888
1888
  },
1889
1889
  },
1890
1890
  {
@@ -1895,86 +1895,30 @@
1895
1895
  },
1896
1896
  {
1897
1897
  key: "",
1898
- label: "Last full check",
1898
+ label: "Latest deterministic check",
1899
1899
  desc: "",
1900
- control: stat("2026-07-10 · in band (1 advisory)", "mono"),
1900
+ control: {
1901
+ ...stat("Never run", "mono"),
1902
+ id: "healthLastRun",
1903
+ },
1901
1904
  },
1902
1905
  ],
1903
1906
  },
1904
1907
  {
1905
1908
  type: "table",
1909
+ id: "healthResults",
1906
1910
  card: "Latest results",
1907
- note: "Sample output — what a health check reports",
1908
- cols: ["Check", "Layer", "Status"],
1909
- rows: [
1910
- [
1911
- {
1912
- t: "text",
1913
- v: "Managed files match templates (copy-overwrite)",
1914
- },
1915
- { t: "mono", v: "deterministic" },
1916
- { t: "badge", v: "pass", cls: "pass" },
1917
- ],
1918
- [
1919
- {
1920
- t: "text",
1921
- v: "package.json governance (force/defaults/merge)",
1922
- },
1923
- { t: "mono", v: "deterministic" },
1924
- { t: "badge", v: "pass", cls: "pass" },
1925
- ],
1926
- [
1927
- {
1928
- t: "text",
1929
- v: ".lisa.config.json fully populated (sync clean)",
1930
- },
1931
- { t: "mono", v: "deterministic" },
1932
- { t: "badge", v: "pass", cls: "pass" },
1933
- ],
1934
- [
1935
- { t: "text", v: "Git hooks installed and unmodified" },
1936
- { t: "mono", v: "deterministic" },
1937
- { t: "badge", v: "pass", cls: "pass" },
1938
- ],
1939
- [
1940
- { t: "text", v: "Plugins enabled and version-current" },
1941
- { t: "mono", v: "deterministic" },
1942
- { t: "badge", v: "pass", cls: "pass" },
1943
- ],
1944
- [
1945
- { t: "text", v: "CI workflows drifted from stack template" },
1946
- { t: "mono", v: "deterministic" },
1947
- { t: "badge", v: "warn", cls: "warn" },
1948
- ],
1949
- [
1950
- {
1951
- t: "text",
1952
- v: "Local overrides still justified (eslint.config.local.ts)",
1953
- },
1954
- { t: "mono", v: "agentic" },
1955
- { t: "badge", v: "pass", cls: "pass" },
1956
- ],
1957
- [
1958
- {
1959
- t: "text",
1960
- v: "Skipped CI jobs have a recorded reason",
1961
- },
1962
- { t: "mono", v: "agentic" },
1963
- { t: "badge", v: "pass", cls: "pass" },
1964
- ],
1965
- [
1966
- {
1967
- t: "text",
1968
- v: "Grandfathered lint globs still needed",
1969
- },
1970
- { t: "mono", v: "agentic" },
1971
- { t: "badge", v: "pass", cls: "pass" },
1972
- ],
1973
- ],
1911
+ note: "No completed health check yet",
1912
+ cols: ["Check", "Layer", "Status", "Reason"],
1913
+ rows: [],
1914
+ empty: {
1915
+ state: "never-run",
1916
+ message: "Run deterministic health check to measure this project",
1917
+ },
1974
1918
  },
1975
1919
  {
1976
1920
  type: "callout",
1977
- text: "<b>How it will work.</b> The deterministic layer reuses what already exists lisa doctor, template diffing, lisa sync --dry-run, plugin reconciliation and is fast and exact. The agentic layer reviews what a diff can't: whether local overrides still serve their original purpose, whether drift is intentional, whether skipped gates have justification. Both are packaged as one skill so the button, the cron, and the CLI share a single implementation.",
1921
+ text: '<b>How it works.</b> Browser runs are deliberately deterministic: the local server invokes the shared Health v1 consumer directly, without a child process or nested coding agent. The optional agentic layer is composed by <span class="mono">/lisa:health</span> in a supported coding harness.',
1978
1922
  },
1979
1923
  ],
1980
1924
  };
@@ -5131,6 +5075,15 @@
5131
5075
  "detecting" so the Stacks section never presents the metadata catalog
5132
5076
  as if it were the detection result. */
5133
5077
  let stackDetection = { status: "detecting", ids: [] };
5078
+ /* Version context and Health v1 verdict are painted together. Health
5079
+ takes priority once measured without erasing the update context. */
5080
+ let lisaVersionChipState = {
5081
+ dotClass: "",
5082
+ label: "@codyswann/lisa · checking…",
5083
+ };
5084
+ let healthChipState = { state: "idle" };
5085
+ /* Durable Health view model projected by every whole-page render. */
5086
+ let healthViewState = { state: "idle" };
5134
5087
 
5135
5088
  function el(tag, cls, html) {
5136
5089
  const node = document.createElement(tag);
@@ -5275,6 +5228,7 @@
5275
5228
  } else if (control.type === "button") {
5276
5229
  const b = el("button", "action-btn", esc(control.label));
5277
5230
  b.type = "button";
5231
+ if (control.id) b.id = control.id;
5278
5232
  if (control.disabled) {
5279
5233
  b.disabled = true;
5280
5234
  if (control.reason) {
@@ -5282,11 +5236,35 @@
5282
5236
  b.setAttribute("aria-describedby", id + "-reason");
5283
5237
  }
5284
5238
  } else {
5285
- b.addEventListener("click", () =>
5286
- showToast(control.toast || "Prototype action")
5287
- );
5239
+ b.addEventListener("click", () => {
5240
+ if (control.action === "run-health") {
5241
+ void runHealthCheck();
5242
+ return;
5243
+ }
5244
+ showToast(control.toast || "Prototype action");
5245
+ });
5288
5246
  }
5289
5247
  wrap.appendChild(b);
5248
+ if (control.statusId) {
5249
+ const status = el(
5250
+ "span",
5251
+ "status-why health-run-status",
5252
+ esc(
5253
+ control.statusMessage ||
5254
+ "Ready to run deterministic health check"
5255
+ )
5256
+ );
5257
+ status.id = control.statusId;
5258
+ status.setAttribute(
5259
+ "role",
5260
+ control.statusRole === "alert" ? "alert" : "status"
5261
+ );
5262
+ status.setAttribute(
5263
+ "aria-live",
5264
+ control.statusRole === "alert" ? "assertive" : "polite"
5265
+ );
5266
+ wrap.appendChild(status);
5267
+ }
5290
5268
  if (control.disabled && control.reason) {
5291
5269
  const reason = el(
5292
5270
  "span",
@@ -5297,9 +5275,13 @@
5297
5275
  wrap.appendChild(reason);
5298
5276
  }
5299
5277
  } else if (control.type === "static") {
5300
- wrap.appendChild(
5301
- el("span", control.cls === "mono" ? "mono" : "", esc(control.value))
5278
+ const value = el(
5279
+ "span",
5280
+ control.cls === "mono" ? "mono" : "",
5281
+ esc(control.value)
5302
5282
  );
5283
+ if (control.id) value.id = control.id;
5284
+ wrap.appendChild(value);
5303
5285
  }
5304
5286
  return wrap;
5305
5287
  }
@@ -5930,6 +5912,7 @@
5930
5912
  }
5931
5913
 
5932
5914
  function renderAll() {
5915
+ syncHealthViewModel();
5933
5916
  $main.innerHTML = "";
5934
5917
  $nav.innerHTML = "";
5935
5918
  ctlSeq = 0;
@@ -6422,8 +6405,6 @@
6422
6405
  * @param probes - Live-status probe map from /api/status
6423
6406
  */
6424
6407
  function applyLisaVersion(probes) {
6425
- const chip = document.getElementById("healthChip");
6426
- if (!chip) return;
6427
6408
  const raw =
6428
6409
  probes && Object.hasOwn(probes, "lisa-version")
6429
6410
  ? probes["lisa-version"]
@@ -6436,12 +6417,9 @@
6436
6417
  message: "The lisa-version probe was not reported",
6437
6418
  }
6438
6419
  : normalizeLiveStatusResult(raw);
6439
- const setChip = (dotClass, label) => {
6440
- chip.innerHTML = "";
6441
- const dot = el("span", "dot" + (dotClass ? " " + dotClass : ""));
6442
- dot.setAttribute("aria-hidden", "true");
6443
- chip.appendChild(dot);
6444
- chip.appendChild(document.createTextNode(" " + label));
6420
+ const setVersion = (dotClass, label) => {
6421
+ lisaVersionChipState = { dotClass, label };
6422
+ paintHealthChip();
6445
6423
  };
6446
6424
  if (
6447
6425
  result.state === "value" &&
@@ -6458,30 +6436,271 @@
6458
6436
  latest.length > 0
6459
6437
  ) {
6460
6438
  if (outdated) {
6461
- setChip(
6439
+ setVersion(
6462
6440
  "warn",
6463
6441
  "@codyswann/lisa " + current + " · latest " + latest
6464
6442
  );
6465
6443
  return;
6466
6444
  }
6467
- setChip("ok", "@codyswann/lisa " + current + " · up to date");
6445
+ setVersion("ok", "@codyswann/lisa " + current + " · up to date");
6468
6446
  return;
6469
6447
  }
6470
6448
  }
6471
6449
  if (result.state === "unknown") {
6472
6450
  const reason = result.reason || "unknown";
6473
6451
  const message = result.message || "Version status unavailable";
6474
- setChip(
6452
+ setVersion(
6475
6453
  "unknown",
6476
6454
  "@codyswann/lisa · unknown (" + reason + ": " + message + ")"
6477
6455
  );
6478
6456
  return;
6479
6457
  }
6480
- setChip(
6458
+ setVersion(
6481
6459
  "unknown",
6482
6460
  "@codyswann/lisa · unknown (invalid-value: lisa-version probe returned an unusable value)"
6483
6461
  );
6484
6462
  }
6463
+
6464
+ /** Paint one chip from the latest version context and health state. */
6465
+ function paintHealthChip() {
6466
+ const chip = document.getElementById("healthChip");
6467
+ if (!chip) return;
6468
+ let dotClass = lisaVersionChipState.dotClass;
6469
+ let label = lisaVersionChipState.label;
6470
+ if (healthChipState.state === "result") {
6471
+ dotClass = healthChipState.verdict === "in band" ? "ok" : "warn";
6472
+ label = healthChipState.verdict + " · " + lisaVersionChipState.label;
6473
+ } else if (healthChipState.state === "running") {
6474
+ dotClass = "unknown";
6475
+ label = "health check running · " + lisaVersionChipState.label;
6476
+ } else if (healthChipState.state === "error") {
6477
+ dotClass = "unknown";
6478
+ label = "health unavailable · " + lisaVersionChipState.label;
6479
+ }
6480
+ chip.innerHTML = "";
6481
+ const dot = el("span", "dot" + (dotClass ? " " + dotClass : ""));
6482
+ dot.setAttribute("aria-hidden", "true");
6483
+ chip.appendChild(dot);
6484
+ chip.appendChild(document.createTextNode(" " + label));
6485
+ }
6486
+
6487
+ /** Return the existing Health results-table descriptor. */
6488
+ function healthResultsBlock() {
6489
+ return DATA.sections.health.blocks.find(
6490
+ block => block.id === "healthResults"
6491
+ );
6492
+ }
6493
+
6494
+ /** Return the existing deterministic-run row descriptor. */
6495
+ function healthRunRow() {
6496
+ const scan = DATA.sections.health.blocks.find(
6497
+ block => block.card === "In-band scan"
6498
+ );
6499
+ return scan && scan.rows
6500
+ ? scan.rows.find(
6501
+ row => row.control && row.control.action === "run-health"
6502
+ )
6503
+ : undefined;
6504
+ }
6505
+
6506
+ /** Return the existing latest deterministic-check row descriptor. */
6507
+ function healthStampRow() {
6508
+ const scan = DATA.sections.health.blocks.find(
6509
+ block => block.card === "In-band scan"
6510
+ );
6511
+ return scan && scan.rows
6512
+ ? scan.rows.find(
6513
+ row => row.control && row.control.id === "healthLastRun"
6514
+ )
6515
+ : undefined;
6516
+ }
6517
+
6518
+ /** Project durable Health state into the existing section descriptors. */
6519
+ function syncHealthViewModel() {
6520
+ const runRow = healthRunRow();
6521
+ const stampRow = healthStampRow();
6522
+ const block = healthResultsBlock();
6523
+ if (!runRow || !stampRow || !block) return;
6524
+ const running = healthViewState.state === "running";
6525
+ runRow.control.label = running
6526
+ ? "Running…"
6527
+ : "Run deterministic health check";
6528
+ runRow.control.disabled = running;
6529
+ runRow.control.statusRole =
6530
+ healthViewState.state === "error" ? "alert" : "status";
6531
+ runRow.control.statusMessage =
6532
+ healthViewState.state === "running"
6533
+ ? "Deterministic health check running"
6534
+ : healthViewState.state === "error"
6535
+ ? healthViewState.message
6536
+ : healthViewState.state === "result"
6537
+ ? "Deterministic health check completed: " +
6538
+ healthViewState.result.summary.verdict
6539
+ : "Ready to run deterministic health check";
6540
+
6541
+ const visibleResult = healthViewState.result;
6542
+ if (visibleResult) {
6543
+ block.note =
6544
+ visibleResult.mode +
6545
+ " · " +
6546
+ visibleResult.summary.verdict +
6547
+ " · run " +
6548
+ visibleResult.runId;
6549
+ block.rows = visibleResult.findings.map(finding => [
6550
+ { t: "mono", v: finding.check },
6551
+ { t: "mono", v: finding.layer },
6552
+ { t: "badge", v: finding.status, cls: finding.status },
6553
+ { t: "text", v: finding.reason },
6554
+ ]);
6555
+ delete block.empty;
6556
+ stampRow.control.value =
6557
+ visibleResult.completedAt + " · " + visibleResult.summary.verdict;
6558
+ } else if (healthViewState.state === "error") {
6559
+ block.note = "Deterministic health check failed";
6560
+ block.rows = [];
6561
+ block.empty = { state: "error", message: healthViewState.message };
6562
+ stampRow.control.value = "Run failed · no current verdict";
6563
+ } else {
6564
+ block.note = "No completed deterministic health check yet";
6565
+ block.rows = [];
6566
+ block.empty = {
6567
+ state: "never-run",
6568
+ message: "Run deterministic health check to measure this project",
6569
+ };
6570
+ stampRow.control.value = "Never run";
6571
+ }
6572
+
6573
+ healthChipState =
6574
+ healthViewState.state === "running"
6575
+ ? { state: "running" }
6576
+ : healthViewState.state === "error"
6577
+ ? { state: "error" }
6578
+ : visibleResult
6579
+ ? {
6580
+ state: "result",
6581
+ verdict: visibleResult.summary.verdict,
6582
+ }
6583
+ : { state: "idle" };
6584
+ }
6585
+
6586
+ /** Replace only the existing Health results card after state changes. */
6587
+ function repaintHealthResults() {
6588
+ syncHealthViewModel();
6589
+ const block = healthResultsBlock();
6590
+ const current = document.getElementById("healthResults");
6591
+ if (block && current) current.replaceWith(buildTable(block));
6592
+ const runRow = healthRunRow();
6593
+ const currentButton = document.getElementById("runHealthCheckButton");
6594
+ const currentControl =
6595
+ currentButton && currentButton.closest(".control");
6596
+ if (runRow && currentControl) {
6597
+ currentControl.replaceWith(buildControl(runRow.control, runRow.key));
6598
+ }
6599
+ const stampRow = healthStampRow();
6600
+ const stamp = document.getElementById("healthLastRun");
6601
+ if (stampRow && stamp) stamp.textContent = stampRow.control.value;
6602
+ }
6603
+
6604
+ /** Validate the browser-facing closed Health v1 shape before rendering. */
6605
+ function requireHealthResult(candidate) {
6606
+ const isObject = value =>
6607
+ value !== null && typeof value === "object" && !Array.isArray(value);
6608
+ const hasExactKeys = (value, keys) =>
6609
+ isObject(value) &&
6610
+ Object.keys(value).sort().join(",") === [...keys].sort().join(",");
6611
+ if (
6612
+ !hasExactKeys(candidate, [
6613
+ "schemaVersion",
6614
+ "runId",
6615
+ "mode",
6616
+ "startedAt",
6617
+ "completedAt",
6618
+ "findings",
6619
+ "summary",
6620
+ ]) ||
6621
+ candidate.schemaVersion !== 1 ||
6622
+ typeof candidate.runId !== "string" ||
6623
+ !["deterministic", "full"].includes(candidate.mode) ||
6624
+ typeof candidate.startedAt !== "string" ||
6625
+ typeof candidate.completedAt !== "string" ||
6626
+ !Array.isArray(candidate.findings) ||
6627
+ candidate.findings.length === 0 ||
6628
+ !hasExactKeys(candidate.summary, ["verdict", "counts"]) ||
6629
+ !["in band", "drift detected"].includes(candidate.summary.verdict) ||
6630
+ !hasExactKeys(candidate.summary.counts, ["pass", "warn", "fail"])
6631
+ ) {
6632
+ throw new TypeError("Health response is not valid Health v1 JSON");
6633
+ }
6634
+ candidate.findings.forEach(finding => {
6635
+ if (
6636
+ !hasExactKeys(finding, ["check", "layer", "status", "reason"]) ||
6637
+ typeof finding.check !== "string" ||
6638
+ !["deterministic", "agentic"].includes(finding.layer) ||
6639
+ !["pass", "warn", "fail"].includes(finding.status) ||
6640
+ typeof finding.reason !== "string"
6641
+ ) {
6642
+ throw new TypeError("Health response contains an invalid finding");
6643
+ }
6644
+ });
6645
+ return candidate;
6646
+ }
6647
+
6648
+ /** Render the exact canonical finding fields into the existing table. */
6649
+ function applyHealthResult(candidate) {
6650
+ const result = requireHealthResult(candidate);
6651
+ healthViewState = { state: "result", result };
6652
+ repaintHealthResults();
6653
+ paintHealthChip();
6654
+ }
6655
+
6656
+ /** Clear prior findings and surface one current failure without retrying. */
6657
+ function applyHealthError(message) {
6658
+ healthViewState = { state: "error", message };
6659
+ repaintHealthResults();
6660
+ paintHealthChip();
6661
+ }
6662
+
6663
+ /** Read stored Health v1 state once when the console boots. */
6664
+ async function hydrateHealth() {
6665
+ try {
6666
+ const response = await fetch("/api/health", { cache: "no-store" });
6667
+ if (response.status === 404) return;
6668
+ if (!response.ok) throw new Error("Unable to read Lisa health");
6669
+ applyHealthResult(await response.json());
6670
+ } catch (error) {
6671
+ applyHealthError(
6672
+ error instanceof Error
6673
+ ? error.message
6674
+ : "Unable to read Lisa health"
6675
+ );
6676
+ }
6677
+ }
6678
+
6679
+ /** Run exactly one deterministic Health v1 check through the local server. */
6680
+ async function runHealthCheck() {
6681
+ const button = document.getElementById("runHealthCheckButton");
6682
+ if (!button || button.disabled || healthViewState.state === "running")
6683
+ return;
6684
+ const previousResult = healthViewState.result;
6685
+ healthViewState = {
6686
+ state: "running",
6687
+ ...(previousResult ? { result: previousResult } : {}),
6688
+ };
6689
+ repaintHealthResults();
6690
+ paintHealthChip();
6691
+ try {
6692
+ const response = await fetch("/api/health", {
6693
+ method: "POST",
6694
+ cache: "no-store",
6695
+ });
6696
+ if (!response.ok) throw new Error("Unable to run Lisa health");
6697
+ applyHealthResult(await response.json());
6698
+ } catch (error) {
6699
+ applyHealthError(
6700
+ error instanceof Error ? error.message : "Unable to run Lisa health"
6701
+ );
6702
+ }
6703
+ }
6485
6704
  /**
6486
6705
  * Paint the CI Quality jobs Active column from the ci-quality-jobs probe.
6487
6706
  * Never leaves a fabricated active/off value when the probe is unknown.
@@ -7005,6 +7224,7 @@
7005
7224
  hydrateFromLiveConfig();
7006
7225
  renderAll();
7007
7226
  void hydrateLiveStatuses();
7227
+ void hydrateHealth();
7008
7228
  showSection(location.hash ? location.hash.slice(1) : "overview");
7009
7229
  if (!document.getElementById("section-" + activeSection))
7010
7230
  showSection("overview");