@codyswann/lisa 2.275.0 → 2.277.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 (70) 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 +15 -3
  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/.codex-plugin/skills/lisa-setup-automations/SKILL.md +30 -0
  16. package/plugins/lisa/skills/lisa-setup-automations/SKILL.md +30 -0
  17. package/plugins/lisa-agy/plugin.json +1 -1
  18. package/plugins/lisa-agy/skills/lisa-setup-automations/SKILL.md +30 -0
  19. package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
  20. package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
  21. package/plugins/lisa-cdk-agy/plugin.json +1 -1
  22. package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
  23. package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
  24. package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
  25. package/plugins/lisa-copilot/skills/lisa-setup-automations/SKILL.md +30 -0
  26. package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
  27. package/plugins/lisa-cursor/skills/lisa-setup-automations/SKILL.md +30 -0
  28. package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
  29. package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
  30. package/plugins/lisa-expo-agy/plugin.json +1 -1
  31. package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
  32. package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
  33. package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
  34. package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
  35. package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
  36. package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
  37. package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
  38. package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
  39. package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
  40. package/plugins/lisa-nestjs-agy/plugin.json +1 -1
  41. package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
  42. package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
  43. package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
  44. package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
  45. package/plugins/lisa-openclaw-agy/plugin.json +1 -1
  46. package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
  47. package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
  48. package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
  49. package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
  50. package/plugins/lisa-phaser-agy/plugin.json +1 -1
  51. package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
  52. package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
  53. package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
  54. package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
  55. package/plugins/lisa-rails-agy/plugin.json +1 -1
  56. package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
  57. package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
  58. package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
  59. package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
  60. package/plugins/lisa-typescript-agy/plugin.json +1 -1
  61. package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
  62. package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
  63. package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
  64. package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
  65. package/plugins/lisa-wiki-agy/plugin.json +1 -1
  66. package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
  67. package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
  68. package/plugins/src/base/skills/lisa-setup-automations/SKILL.md +30 -0
  69. package/ui/README.md +18 -10
  70. package/ui/index.html +316 -96
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-wiki",
3
- "version": "2.275.0",
3
+ "version": "2.277.0",
4
4
  "description": "Distributable LLM Wiki kernel — ingest, query, lint, and maintain a git-native markdown knowledge base across Claude and Codex.",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-wiki",
3
- "version": "2.275.0",
3
+ "version": "2.277.0",
4
4
  "description": "LLM Wiki — a distributable, git-native markdown knowledge base for Claude Code and Codex",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-wiki",
3
- "version": "2.275.0",
3
+ "version": "2.277.0",
4
4
  "description": "LLM Wiki — a distributable, git-native markdown knowledge base for Claude Code and Codex",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-wiki",
3
- "version": "2.275.0",
3
+ "version": "2.277.0",
4
4
  "description": "LLM Wiki — a distributable, git-native markdown knowledge base for Claude Code and Codex",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -49,6 +49,36 @@ without a human between the loops and the pipeline. Pass `false` explicitly to o
49
49
  human triage. The two auto-start flags affect **only** the two exploratory automations; the intake
50
50
  gates' adversarial validation remains the quality control either way.
51
51
 
52
+ ## Repository-readiness advisory
53
+
54
+ Before creating or updating registrations, locate the standing repository-readiness report through
55
+ RRR-3's canonical `resolveReadinessReportPath` contract (currently `.lisa/readiness.json` under the
56
+ resolved project root). This consumes the shared report; it never creates a second readiness
57
+ assessment. Read the report exactly once. Do not re-run `lisa doctor --readiness`, because doing so
58
+ would execute journeys and replace the standing evidence the operator is about to act on.
59
+
60
+ A report is usable for this advisory only when all of these are true:
61
+
62
+ - the file parses to a JSON object whose `schema_version` is `1` and whose `verdict` is `NOT_READY`;
63
+ - `blocker_count` is a positive integer and the `blockers` array length exactly equals
64
+ `blocker_count`;
65
+ - each blocker has a unique `id` in the closed set B1 through B7 (duplicate blocker ids make the
66
+ report internally inconsistent, and uniqueness caps the count at seven) and a non-empty
67
+ operator-facing `label`; and
68
+ - `narrowed_claim` is a non-empty string.
69
+
70
+ For one usable report, emit exactly one warning naming the blocker count, each blocker's `id` and
71
+ `label`, and the narrowed claim. For example: "Repository readiness warning — 3 standing ship
72
+ blockers: B2 Release path bypasses validation; B4 Failing loop has no recovery; B7 Operability
73
+ cannot be proved. Narrowed claim: ready for supervised changes only." This is a warning only:
74
+ setup continues, and no registration or runbook step is skipped because blockers stand.
75
+
76
+ Treat a missing or unreadable file, invalid JSON, unsupported schema, or internally inconsistent
77
+ report as unavailable: emit no readiness warning and no error, then continue setup normally. Do not
78
+ invent an artifact freshness, age, or TTL rule; the persisted contract defines no such field. This
79
+ advisory is never a precondition and follows the same never-block-always-degrade posture as runbook
80
+ scaffolding below.
81
+
52
82
  ## The automations to create
53
83
 
54
84
  Each automation runs **one cycle** of a Lisa command and respects that command's confirmation policy
package/ui/README.md CHANGED
@@ -134,8 +134,13 @@ path, harness-neutral optional agentic composition API at
134
134
  `@codyswann/lisa/health`, `lisa health` CLI, and `/lisa:health` skill across all
135
135
  six supported harnesses. Completed results are validated and atomically stored
136
136
  at the gitignored `.lisa/health/latest.json`; `health.schedule` is the only v1
137
- configuration key and accepts `off`, `daily`, or `weekly`. The console button,
138
- readback, and scheduler remain downstream work.
137
+ configuration key and accepts `off`, `daily`, or `weekly`. The console now
138
+ reads that stored result on boot and its **Run deterministic health check** button invokes the
139
+ shared consumer directly against the directory served by `lisa ui`. Browser
140
+ runs are intentionally deterministic: the local server starts no child process
141
+ or nested coding agent. Agentic composition remains the responsibility of the
142
+ `/lisa:health` skill in the active coding harness. The scheduler remains
143
+ downstream work.
139
144
 
140
145
  **Is Lisa on the latest version?** — always-on status. Lisa's CLI already
141
146
  checks npm on every invocation; the console surfaces the result permanently
@@ -144,13 +149,14 @@ Health section's version card. When behind, `lisa update` prints (or runs
144
149
  with `--yes`) the package-manager update command.
145
150
 
146
151
  **Is the project completely in band?** — on-demand scan behind the
147
- "Run health check" button (plus an optional scheduled cadence via
152
+ "Run deterministic health check" button (plus an optional scheduled cadence via
148
153
  `health.schedule` that files a ticket when drift is found). "In band" means
149
154
  every Lisa-managed surface matches what the installed Lisa version would
150
- emit. The check is a mix of deterministic and agentic verification,
151
- packaged as one `/lisa:health` skill backed by the `lisa health` CLI so the
152
- CLI and every coding-agent harness share one implementation; downstream
153
- console and cron integrations can reuse it:
155
+ emit. The console and `lisa health` CLI invoke the same deterministic persisted
156
+ consumer. `/lisa:health` is the current-harness surface that may compose the
157
+ optional agentic layer before persisting a final result; the console can read
158
+ and render that stored full result, but a browser-initiated run remains
159
+ deterministic. The scheduled consumer remains downstream work:
154
160
 
155
161
  - **Deterministic layer** (fast, exact — reuses what exists today):
156
162
  `lisa doctor`, template diffing for copy-overwrite/managed-block files,
@@ -168,9 +174,11 @@ console and cron integrations can reuse it:
168
174
  finalization revalidates the evidence digest before persisting one final
169
175
  result.
170
176
 
171
- The downstream console will render results as a per-check table (pass / warn /
172
- fail, with the layer that produced each finding) and keep the last full check's
173
- date and verdict visible in the section.
177
+ The console renders every canonical finding field (`check`, `layer`, `status`,
178
+ and `reason`) in its existing per-check table, keeps the stored completion stamp
179
+ and verdict visible, and gives the health verdict priority in the top-bar chip
180
+ while retaining Lisa version context. A failed run clears prior findings and
181
+ the prior green verdict before surfacing a generic failure; it does not retry.
174
182
 
175
183
  ### Remote-environment requirements
176
184
 
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");