@codyswann/lisa 4.18.0 → 4.20.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 (75) hide show
  1. package/dist/core/lisa-owned-hash-ledger.d.ts.map +1 -1
  2. package/dist/core/lisa-owned-hash-ledger.js +14 -0
  3. package/dist/core/lisa-owned-hash-ledger.js.map +1 -1
  4. package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
  5. package/dist/core/upstream-evidence-manifest.js +24 -6
  6. package/dist/core/upstream-evidence-manifest.js.map +1 -1
  7. package/expo/copy-overwrite/scripts/bdd/baseline.mjs +10 -2
  8. package/expo/copy-overwrite/scripts/bdd/render.mjs +3 -2
  9. package/expo/copy-overwrite/scripts/bdd-matrix.mjs +3 -3
  10. package/package.json +7 -2
  11. package/plugins/lisa/.claude-plugin/plugin.json +1 -1
  12. package/plugins/lisa/.codex-plugin/plugin.json +1 -1
  13. package/plugins/lisa/.codex-plugin/skills/lisa-secrets-access/scripts/publish-aws-bootstrap.mjs +349 -0
  14. package/plugins/lisa/.codex-plugin/skills/lisa-setup-remote-aws/SKILL.md +23 -5
  15. package/plugins/lisa/skills/lisa-secrets-access/scripts/publish-aws-bootstrap.mjs +349 -0
  16. package/plugins/lisa/skills/lisa-setup-remote-aws/SKILL.md +23 -5
  17. package/plugins/lisa-agy/plugin.json +1 -1
  18. package/plugins/lisa-agy/skills/lisa-secrets-access/scripts/publish-aws-bootstrap.mjs +349 -0
  19. package/plugins/lisa-agy/skills/lisa-setup-remote-aws/SKILL.md +23 -5
  20. package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
  21. package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
  22. package/plugins/lisa-cdk-agy/plugin.json +1 -1
  23. package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
  24. package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
  25. package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
  26. package/plugins/lisa-copilot/skills/lisa-secrets-access/scripts/publish-aws-bootstrap.mjs +349 -0
  27. package/plugins/lisa-copilot/skills/lisa-setup-remote-aws/SKILL.md +23 -5
  28. package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
  29. package/plugins/lisa-cursor/skills/lisa-secrets-access/scripts/publish-aws-bootstrap.mjs +349 -0
  30. package/plugins/lisa-cursor/skills/lisa-setup-remote-aws/SKILL.md +23 -5
  31. package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
  32. package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
  33. package/plugins/lisa-expo-agy/plugin.json +1 -1
  34. package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
  35. package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
  36. package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
  37. package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
  38. package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
  39. package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
  40. package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
  41. package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
  42. package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
  43. package/plugins/lisa-nestjs-agy/plugin.json +1 -1
  44. package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
  45. package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
  46. package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
  47. package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
  48. package/plugins/lisa-openclaw-agy/plugin.json +1 -1
  49. package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
  50. package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
  51. package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
  52. package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
  53. package/plugins/lisa-phaser-agy/plugin.json +1 -1
  54. package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
  55. package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
  56. package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
  57. package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
  58. package/plugins/lisa-rails-agy/plugin.json +1 -1
  59. package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
  60. package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
  61. package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
  62. package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
  63. package/plugins/lisa-typescript-agy/plugin.json +1 -1
  64. package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
  65. package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
  66. package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
  67. package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
  68. package/plugins/lisa-wiki-agy/plugin.json +1 -1
  69. package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
  70. package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
  71. package/plugins/src/base/skills/lisa-secrets-access/scripts/publish-aws-bootstrap.mjs +349 -0
  72. package/plugins/src/base/skills/lisa-setup-remote-aws/SKILL.md +23 -5
  73. package/scripts/check-ui-demo-data.mjs +102 -0
  74. package/ui/README.md +32 -0
  75. package/ui/index.html +901 -115
package/ui/index.html CHANGED
@@ -1,4 +1,5 @@
1
1
  <!doctype html>
2
+ <!-- DESIGN-SOURCE: none — not in Figma -->
2
3
  <html lang="en">
3
4
  <head>
4
5
  <meta charset="utf-8" />
@@ -1856,6 +1857,7 @@
1856
1857
  * control: { type: 'toggle'|'select'|'text'|'number'|'tags'|'envmap'|'static', ... }
1857
1858
  * ------------------------------------------------------------------ */
1858
1859
 
1860
+ /* LISA_UI_CATALOG_START */
1859
1861
  const sel = (value, options) => ({ type: "select", value, options });
1860
1862
  const tog = value => ({ type: "toggle", value });
1861
1863
  const txt = (value, opts) =>
@@ -1870,6 +1872,68 @@
1870
1872
  });
1871
1873
  const stat = (value, cls) => ({ type: "static", value, cls });
1872
1874
 
1875
+ /**
1876
+ * Build one live control from a present value without retaining demo
1877
+ * sub-values. Returning null makes a malformed live value render unknown
1878
+ * instead of preserving whichever plausible value the prototype carried.
1879
+ * Each renderer validates its input before coercion: JavaScript truthiness
1880
+ * and object stringification turn malformed configuration into convincing
1881
+ * lies such as `"false"` → on and `{}` → `[object Object]`.
1882
+ */
1883
+ function hydrateControl(control, value) {
1884
+ if (control.type === "toggle" && typeof value === "boolean") {
1885
+ return { ...control, value };
1886
+ }
1887
+ if (
1888
+ control.type === "number" &&
1889
+ typeof value === "number" &&
1890
+ Number.isFinite(value)
1891
+ ) {
1892
+ return { ...control, value };
1893
+ }
1894
+ if (
1895
+ (control.type === "text" || control.type === "select") &&
1896
+ typeof value === "string"
1897
+ ) {
1898
+ return { ...control, value };
1899
+ }
1900
+ if (
1901
+ control.type === "tags" &&
1902
+ Array.isArray(value) &&
1903
+ value.every(item => typeof item === "string")
1904
+ ) {
1905
+ return { ...control, values: value };
1906
+ }
1907
+ if (control.type === "envmap") {
1908
+ const hydrated = {
1909
+ ...control,
1910
+ dev: "unknown",
1911
+ staging: "unknown",
1912
+ production: "unknown",
1913
+ };
1914
+ if (typeof value === "string") {
1915
+ hydrated.production = value;
1916
+ return hydrated;
1917
+ }
1918
+ if (value && typeof value === "object" && !Array.isArray(value)) {
1919
+ ["dev", "staging", "production"].forEach(envName => {
1920
+ if (
1921
+ Object.hasOwn(value, envName) &&
1922
+ typeof value[envName] === "string"
1923
+ ) {
1924
+ hydrated[envName] = value[envName];
1925
+ }
1926
+ });
1927
+ return hydrated;
1928
+ }
1929
+ return null;
1930
+ }
1931
+ if (control.type === "static" && typeof value === "string") {
1932
+ return { ...control, value };
1933
+ }
1934
+ return null;
1935
+ }
1936
+
1873
1937
  const SEVERITIES = ["error", "warn", "off"];
1874
1938
 
1875
1939
  const DATA = {
@@ -1958,6 +2022,7 @@
1958
2022
  },
1959
2023
  {
1960
2024
  type: "callout",
2025
+ staticCopy: "fixed Lisa factory and gate model",
1961
2026
  text: "<b>The gates are adversarial.</b> Agents, humans, and automations submit inputs; an intake agent admits only work that is unambiguous and that the factory has provable tooling access to execute — everything else is rejected and raised. Three loops feed the pipeline on schedule: <b>QA</b> (exploratory bugs), <b>Product Planning</b> (ideated PRDs), and <b>Monitoring</b> (observability regressions). Verify failures flow straight back into Implement as build-ready tickets. The goal: <b>non-technical people creating scalable software</b> — everything that crosses a gate is written for them, and end users never talk to a coding agent.",
1962
2027
  },
1963
2028
  {
@@ -2013,6 +2078,7 @@
2013
2078
  rows: [
2014
2079
  {
2015
2080
  key: "",
2081
+ staticCopy: "fixed Lisa configuration precedence",
2016
2082
  label: "Resolution order",
2017
2083
  desc: "Skill arguments always win, then the per-developer local file, then the committed file, then Lisa’s built-in default.",
2018
2084
  control: stat(
@@ -2022,6 +2088,7 @@
2022
2088
  },
2023
2089
  {
2024
2090
  key: "",
2091
+ staticCopy: "fixed Lisa secret-handling contract",
2025
2092
  label: "Secrets",
2026
2093
  desc: "Tokens never live in config — only IDs, keys and slugs. Secrets stay in env vars or the OS keychain.",
2027
2094
  control: stat(
@@ -2362,8 +2429,12 @@
2362
2429
  };
2363
2430
  }
2364
2431
 
2365
- function fLegend(text) {
2366
- return { type: "callout", text: text };
2432
+ /**
2433
+ * Attach provenance where each factory legend is authored so a
2434
+ * project-dependent claim cannot inherit a plausible static source.
2435
+ */
2436
+ function fLegend(text, provenance) {
2437
+ return { type: "callout", text: text, ...provenance };
2367
2438
  }
2368
2439
 
2369
2440
  /* ===== Staff: which coding agent runs this factory =====================
@@ -2747,7 +2818,8 @@
2747
2818
  ],
2748
2819
  },
2749
2820
  fLegend(
2750
- '<b>Without a context source, the PRDs are guesses.</b> The factory can reach the code but not the project\'s accumulated knowledge, so it re-derives context every run and cannot know what was already decided and why. Connect a wiki with <span class="mono">/lisa:agent-ready</span>, or point this factory at another knowledge source. Switching the PRD destination above changes where every PRD this factory produces is filed — the lifecycle roles for the new provider are configured on the <b>PRD source</b> page.'
2821
+ '<b>Without a context source, the PRDs are guesses.</b> The factory can reach the code but not the project\'s accumulated knowledge, so it re-derives context every run and cannot know what was already decided and why. Connect a wiki with <span class="mono">/lisa:agent-ready</span>, or point this factory at another knowledge source. Switching the PRD destination above changes where every PRD this factory produces is filed — the lifecycle roles for the new provider are configured on the <b>PRD source</b> page.',
2822
+ { demoOnly: true }
2751
2823
  ),
2752
2824
  ],
2753
2825
  };
@@ -3099,7 +3171,8 @@
3099
3171
  ],
3100
3172
  },
3101
3173
  fLegend(
3102
- "<b>The failure nobody sees is a requirement that never became a ticket.</b> Every other gate here checks the quality of the work items that exist; none of them notices one that is missing. Coverage — every PRD requirement traceable to at least one work item — is the only check that catches it, and it currently runs as an advisory audit rather than a gate."
3174
+ "<b>The failure nobody sees is a requirement that never became a ticket.</b> Every other gate here checks the quality of the work items that exist; none of them notices one that is missing. Coverage — every PRD requirement traceable to at least one work item — is the only check that catches it, and it currently runs as an advisory audit rather than a gate.",
3175
+ { demoOnly: true }
3103
3176
  ),
3104
3177
  ],
3105
3178
  };
@@ -3962,7 +4035,8 @@
3962
4035
  ],
3963
4036
  },
3964
4037
  fLegend(
3965
- '<b>The CI column is the one that gates.</b> Agent-hook and git-hook are fast-feedback mirrors a worker can skip; whether an obligation is actually unskippable is inside its CI cell — click it and read <i>Required to merge</i>. Three rows fall short: mutation testing runs in CI but nightly and unrequired, and infrastructure scanning and contract testing have no provider selected at all. Those are the honest gaps — each is one choice away from being a real gate. The <span class="mono">n/a</span> rows are different: those gates need a deployed web or API surface, and this project is a CLI, so they do not apply here rather than being gaps — they become real gates on a frontend or service stack. The agent-hook and git-hook mirrors must be generated from the same config as CI, or they drift and people learn to distrust them.'
4038
+ '<b>The CI column is the one that gates.</b> Agent-hook and git-hook are fast-feedback mirrors a worker can skip; whether an obligation is actually unskippable is inside its CI cell — click it and read <i>Required to merge</i>. Three rows fall short: mutation testing runs in CI but nightly and unrequired, and infrastructure scanning and contract testing have no provider selected at all. Those are the honest gaps — each is one choice away from being a real gate. The <span class="mono">n/a</span> rows are different: those gates need a deployed web or API surface, and this project is a CLI, so they do not apply here rather than being gaps — they become real gates on a frontend or service stack. The agent-hook and git-hook mirrors must be generated from the same config as CI, or they drift and people learn to distrust them.',
4039
+ { demoOnly: true }
3966
4040
  ),
3967
4041
  ],
3968
4042
  };
@@ -4310,7 +4384,8 @@
4310
4384
  ],
4311
4385
  },
4312
4386
  fLegend(
4313
- "<b>Verify and the QA loop are the same competence in two positions.</b> Verify drives the product to produce a bounded go/no-go on one work item at a gate; the QA loop drives it continuously to produce unbounded exploratory signal. Both re-enter the pipeline through intake — a finding from either gets no privileged path around the adversarial gate. And neither is a scanner: an automated check that inspects a diff or a running app is an Implement gate, not UAT."
4387
+ "<b>Verify and the QA loop are the same competence in two positions.</b> Verify drives the product to produce a bounded go/no-go on one work item at a gate; the QA loop drives it continuously to produce unbounded exploratory signal. Both re-enter the pipeline through intake — a finding from either gets no privileged path around the adversarial gate. And neither is a scanner: an automated check that inspects a diff or a running app is an Implement gate, not UAT.",
4388
+ { staticCopy: "fixed Lisa verification and QA roles" }
4314
4389
  ),
4315
4390
  ],
4316
4391
  };
@@ -4532,7 +4607,8 @@
4532
4607
  ],
4533
4608
  },
4534
4609
  fLegend(
4535
- "<b>Factories fail loudly; sensors fail silently.</b> A factory that cannot run emits a recovery-required outcome. A dead sensor emits nothing — which is pixel-identical to a healthy system with no problems. So a sensor needs a liveness signal independent of its findings: “no bugs this week” and “the bug pipe is broken” must never render the same. The disagreement between sensors is itself a signal — a crash a customer reported that never appeared in errors means the instrumentation has a hole; a spike no customer felt means you are alerting on something users do not."
4610
+ "<b>Factories fail loudly; sensors fail silently.</b> A factory that cannot run emits a recovery-required outcome. A dead sensor emits nothing — which is pixel-identical to a healthy system with no problems. So a sensor needs a liveness signal independent of its findings: “no bugs this week” and “the bug pipe is broken” must never render the same. The disagreement between sensors is itself a signal — a crash a customer reported that never appeared in errors means the instrumentation has a hole; a spike no customer felt means you are alerting on something users do not.",
4611
+ { staticCopy: "fixed Lisa sensing model" }
4536
4612
  ),
4537
4613
  ],
4538
4614
  };
@@ -4832,7 +4908,8 @@
4832
4908
  ],
4833
4909
  },
4834
4910
  fLegend(
4835
- '<b>The agent has its own identity by default — everywhere, including your workstation.</b> Your laptop is not a privileged environment; it is just another runner with better latency and worse uptime. When the agent authenticates as itself on every surface, a local pass genuinely predicts a headless pass, because it is the same identity taking the same path. The <span class="mono">yours</span> row above is the exception that proves it: borrowing your session is exactly why that connection works at your desk and nowhere else. Where a surface can federate (CI, via OIDC) it should — keyless beats hidden. The remote-headless static token is the structural weak link, so scope it narrowly and rotate it often; a short-lived credential is worth more than a well-hidden one.'
4911
+ '<b>The agent has its own identity by default — everywhere, including your workstation.</b> Your laptop is not a privileged environment; it is just another runner with better latency and worse uptime. When the agent authenticates as itself on every surface, a local pass genuinely predicts a headless pass, because it is the same identity taking the same path. The <span class="mono">yours</span> row above is the exception that proves it: borrowing your session is exactly why that connection works at your desk and nowhere else. Where a surface can federate (CI, via OIDC) it should — keyless beats hidden. The remote-headless static token is the structural weak link, so scope it narrowly and rotate it often; a short-lived credential is worth more than a well-hidden one.',
4912
+ { demoOnly: true }
4836
4913
  ),
4837
4914
  ],
4838
4915
  };
@@ -5118,7 +5195,8 @@
5118
5195
  fStaffPicker(),
5119
5196
  { type: "environments" },
5120
5197
  fLegend(
5121
- "<b>Remote is not one place, and local is not manual-only.</b> Every agent ships its own hosted runtime with its own credential store and its own scheduler, so “does this work headless?” has a different answer per agent — which is why this page and the factory pages share one staff selector. Every environment can be scheduled; what differs is durability and granularity. A hosted scheduler survives a closed laptop but imposes a floor on how often it can fire — <b>which is why a loop specified at every 10 minutes cannot run as a Claude cloud routine at all</b>, and has to live locally or in CI. On credentials the ranking is: CI is strongest because it can federate and hold no secret at all; local is next because a human is nearby; the vendor remotes are weakest, since they have neither a human nor federation and fall back to a long-lived token. Scope those narrowly, rotate them often, and put a gateway in front of them — a short-lived credential is worth more than a well-hidden one."
5198
+ "<b>Remote is not one place, and local is not manual-only.</b> Every agent ships its own hosted runtime with its own credential store and its own scheduler, so “does this work headless?” has a different answer per agent — which is why this page and the factory pages share one staff selector. Every environment can be scheduled; what differs is durability and granularity. A hosted scheduler survives a closed laptop but imposes a floor on how often it can fire — <b>which is why a loop specified at every 10 minutes cannot run as a Claude cloud routine at all</b>, and has to live locally or in CI. On credentials the ranking is: CI is strongest because it can federate and hold no secret at all; local is next because a human is nearby; the vendor remotes are weakest, since they have neither a human nor federation and fall back to a long-lived token. Scope those narrowly, rotate them often, and put a gateway in front of them — a short-lived credential is worth more than a well-hidden one.",
5199
+ { demoOnly: true }
5122
5200
  ),
5123
5201
  ],
5124
5202
  };
@@ -6918,7 +6996,8 @@
6918
6996
  ],
6919
6997
  },
6920
6998
  fLegend(
6921
- "<b>This is a readiness assessment, not a grade.</b> Like Vanta, it earns its value by measuring something you actually feel — the autonomy rate the first question asks for. An unanswered row is a named gap you can go fix on its detail page; a red one somewhere downstream is a control that is not yet real. And note the shape of the back half: ship, verify, operate, govern are exactly the places a person used to be the control. A no-human SDLC is not one with the humans deleted — it is one where every human-shaped hole has a named, exercised replacement."
6999
+ "<b>This is a readiness assessment, not a grade.</b> Like Vanta, it earns its value by measuring something you actually feel — the autonomy rate the first question asks for. An unanswered row is a named gap you can go fix on its detail page; a red one somewhere downstream is a control that is not yet real. And note the shape of the back half: ship, verify, operate, govern are exactly the places a person used to be the control. A no-human SDLC is not one with the humans deleted — it is one where every human-shaped hole has a named, exercised replacement.",
7000
+ { demoOnly: true }
6922
7001
  ),
6923
7002
  ],
6924
7003
  };
@@ -7022,6 +7101,7 @@
7022
7101
  },
7023
7102
  {
7024
7103
  type: "callout",
7104
+ staticCopy: "installed Lisa setup guidance",
7025
7105
  text: "The Health section’s in-band scan re-verifies most of these continuously once setup is complete; lisa doctor diagnoses any step that went sideways.",
7026
7106
  },
7027
7107
  ],
@@ -7243,6 +7323,7 @@
7243
7323
  },
7244
7324
  {
7245
7325
  type: "callout",
7326
+ staticCopy: "installed Lisa command taxonomy",
7246
7327
  text: "Setup commands (/lisa:setup:*) live in the Setup checklist; quality one-offs (/lisa:improve:*, /lisa:security:*, mutation testing) are stack tools rather than core workflow.",
7247
7328
  },
7248
7329
  ],
@@ -7255,6 +7336,7 @@
7255
7336
  blocks: [
7256
7337
  {
7257
7338
  type: "callout",
7339
+ staticCopy: "shipped Lisa deterministic health contract",
7258
7340
  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.',
7259
7341
  },
7260
7342
  {
@@ -7332,6 +7414,7 @@
7332
7414
  },
7333
7415
  {
7334
7416
  type: "callout",
7417
+ staticCopy: "shipped Lisa deterministic health contract",
7335
7418
  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.',
7336
7419
  },
7337
7420
  ],
@@ -7399,6 +7482,7 @@
7399
7482
  },
7400
7483
  {
7401
7484
  type: "callout",
7485
+ staticCopy: "fixed Lisa configuration file semantics",
7402
7486
  text: "<b>Two config files.</b> .lisa.config.json is committed and shared; .lisa.config.local.json is gitignored and overrides it per-key. Keys flagged <i>local-only</i> below belong in the local file — lisa doctor warns when they are committed.",
7403
7487
  },
7404
7488
  ],
@@ -7609,6 +7693,7 @@
7609
7693
  blocks: [
7610
7694
  {
7611
7695
  type: "callout",
7696
+ staticCopy: "published starter-sync scope",
7612
7697
  variant: "warn",
7613
7698
  text: "<b>Planned feature.</b> This page documents the starter-sync contract and its settings; the sync engine and upstream-proposal flow are not wired up yet. Controls here preview the intended behavior.",
7614
7699
  },
@@ -7712,6 +7797,7 @@
7712
7797
  },
7713
7798
  {
7714
7799
  key: "",
7800
+ staticCopy: "installed Lisa command reference",
7715
7801
  label: "Check for upstream changes",
7716
7802
  desc: "Diffs the starter since the last synced commit and applies the changes to this project, respecting Lisa template semantics (create-only files the project now owns are never clobbered).",
7717
7803
  control: {
@@ -7740,6 +7826,7 @@
7740
7826
  },
7741
7827
  {
7742
7828
  key: "",
7829
+ staticCopy: "installed Lisa command reference",
7743
7830
  label: "Scan for upstreamable changes",
7744
7831
  desc: "One-off scan without a full sync.",
7745
7832
  control: {
@@ -7755,12 +7842,14 @@
7755
7842
  rows: [
7756
7843
  {
7757
7844
  key: "",
7845
+ staticCopy: "fixed starter ownership guidance",
7758
7846
  label: "Upstreamable",
7759
7847
  desc: "New shared utilities or hooks, dependency/security bumps, CI workflow fixes, lint-rule or config improvements with no project-specific identifiers.",
7760
7848
  control: stat("proposed as starter issue", ""),
7761
7849
  },
7762
7850
  {
7763
7851
  key: "",
7852
+ staticCopy: "fixed starter ownership guidance",
7764
7853
  label: "Not upstreamable",
7765
7854
  desc: "Anything referencing project names, product features, secrets/IDs, tracker keys, or business logic.",
7766
7855
  control: stat("stays in the project", ""),
@@ -7810,6 +7899,7 @@
7810
7899
  },
7811
7900
  {
7812
7901
  type: "callout",
7902
+ staticCopy: "fixed Lisa harness parity policy",
7813
7903
  text: "<b>Parity guarantee.</b> Claude is the reference harness. Every other agent receives an equivalent generated surface; where a capability cannot be represented, the gap is documented rather than silently dropped.",
7814
7904
  },
7815
7905
  ],
@@ -7837,6 +7927,7 @@
7837
7927
  },
7838
7928
  {
7839
7929
  type: "callout",
7930
+ staticCopy: "fixed Lisa remote-environment discovery contract",
7840
7931
  text: "<b>Project-aware by design.</b> Lisa combines the active tracker/source, detected project types, integration signals, and an optional <code>.lisa/remote-environment.json</code> project contract. A credential is never listed merely because Lisa knows how to support it.",
7841
7932
  },
7842
7933
  ],
@@ -8092,6 +8183,7 @@
8092
8183
  },
8093
8184
  {
8094
8185
  type: "callout",
8186
+ staticCopy: "fixed Linear mapping semantics",
8095
8187
  text: 'Fixed native mappings: priority 0=None…4=Low, native estimates, components via a <span class="mono">component:</span> label prefix. Epics map to Projects, Stories to Issues, Sub-tasks to Sub-issues.',
8096
8188
  },
8097
8189
  ],
@@ -8210,6 +8302,7 @@
8210
8302
  blocks: [
8211
8303
  {
8212
8304
  type: "callout",
8305
+ staticCopy: "fixed Confluence lifecycle semantics",
8213
8306
  text: "<b>Lifecycle via parent pages.</b> A PRD’s state is which parent page it sits under; transitions re-parent the page. This works with scoped tokens that cannot write Confluence labels.",
8214
8307
  },
8215
8308
  {
@@ -8490,6 +8583,7 @@
8490
8583
  },
8491
8584
  {
8492
8585
  type: "callout",
8586
+ staticCopy: "fixed Lisa deployment environment semantics",
8493
8587
  text: "GitHub silently auto-creates an unprotected environment the first time a workflow references it — Lisa provisions these explicitly so an approval gate always gates on something. Deployment branch policy is always pinned to the environment’s branch.",
8494
8588
  },
8495
8589
  {
@@ -8701,6 +8795,7 @@
8701
8795
  },
8702
8796
  {
8703
8797
  type: "callout",
8798
+ staticCopy: "shipped Lisa pipeline support matrix",
8704
8799
  text: "Not every stack gets every pipeline. The quality and release callers are seeded for all stacks; the agent and upkeep pipelines come from the TypeScript stack and are inherited by everything built on it; <b>zap-baseline.yml</b> is Harper Fabric and <b>publish-to-npm.yml</b> is npm-package. Rails swaps the quality caller for <b>quality-rails.yml</b>.",
8705
8800
  },
8706
8801
  ],
@@ -8805,6 +8900,7 @@
8805
8900
  },
8806
8901
  {
8807
8902
  type: "callout",
8903
+ staticCopy: "shipped Lisa automation support matrix",
8808
8904
  text: "exploratory-bugs is created only for stacks that ship exploratory QA (expo, rails, harper-fabric); monitor is created unconditionally \u2014 it reports observability gaps rather than failing when no provider is connected.",
8809
8905
  },
8810
8906
  ],
@@ -8909,6 +9005,7 @@
8909
9005
  },
8910
9006
  {
8911
9007
  type: "callout",
9008
+ staticCopy: "fixed Lisa monitor migration semantics",
8912
9009
  text: "lisa sync migrates a legacy monitor block when provenance proves Lisa auto-populated the old default, while preserving human-chosen legacy values. The runtime keeps accepting the deprecated aliases, prefers the provider-neutral key when both are present, and lisa doctor warns until migration is complete.",
8913
9010
  },
8914
9011
  ],
@@ -9282,6 +9379,7 @@
9282
9379
  },
9283
9380
  {
9284
9381
  type: "callout",
9382
+ staticCopy: "fixed Lisa lint override guidance",
9285
9383
  text: "<b>Project overrides live in eslint.config.local.ts</b> (seeded once, never overwritten, spread after the factory so it wins). Two sanctioned patterns: re-declare a rule with looser <i>options</i> globally, or grandfather a path with a files-scoped block. Hand-refactoring to chase a tightened rule is the anti-pattern.",
9286
9384
  },
9287
9385
  ],
@@ -9543,6 +9641,7 @@
9543
9641
  },
9544
9642
  {
9545
9643
  type: "callout",
9644
+ staticCopy: "fixed Lisa verification policy",
9546
9645
  variant: "warn",
9547
9646
  text: "<b>Never --no-verify.</b> When a gate fails, the root cause gets fixed or a specific documented ignore is added with human sign-off. Blanket bypasses are blocked by a harness hook as well.",
9548
9647
  },
@@ -9865,6 +9964,7 @@
9865
9964
  },
9866
9965
  {
9867
9966
  type: "callout",
9967
+ staticCopy: "shipped Lisa Rails quality pipeline",
9868
9968
  text: "Rails calls quality-rails.yml instead: rubocop lint, brakeman security, rspec on Postgres and MySQL, reek code quality, mutation, secret scanning, ast-grep, license compliance and ZAP.",
9869
9969
  },
9870
9970
  ],
@@ -10272,6 +10372,7 @@
10272
10372
  },
10273
10373
  {
10274
10374
  type: "callout",
10375
+ staticCopy: "shipped Lisa plugin parity catalog",
10275
10376
  text: "<b>Parity reimplementations.</b> For non-Claude agents, curated plugin behavior is reimplemented as version-pinned Lisa skills: code-review, code-simplifier, coderabbit, safety-net rules, sentry SDK setup, sentry Seer, skill-creator. A drift detector fails CI when an upstream plugin moves ahead of its pin.",
10276
10377
  },
10277
10378
  ],
@@ -10366,6 +10467,609 @@
10366
10467
  ],
10367
10468
  };
10368
10469
 
10470
+ /*
10471
+ * Explicit provenance for renderer shapes whose values are not ordinary
10472
+ * config-key rows. Keys are semantic catalog paths, never row counts or
10473
+ * array indexes, so inserting or reordering a block cannot silently move
10474
+ * a declaration onto a different value.
10475
+ */
10476
+ const UI_LIVE_BLOCKS = new Map([
10477
+ ["doctor/doctor-report", "api:gate-report"],
10478
+ ["setup/Required", "api:setup-readiness"],
10479
+ ["setup/Optional", "api:setup-readiness"],
10480
+ ["health/healthResults", "api:health"],
10481
+ ["stacks/stacks", "probe:detected-stacks"],
10482
+ ["remote/Variables", "window:LISA_REMOTE_ENVIRONMENT"],
10483
+ ["remote/Startup Script", "window:LISA_REMOTE_ENVIRONMENT"],
10484
+ ["deploy/deploy-pipeline-stages", "probe:deploy-pipeline-stages"],
10485
+ ["automations/automations-scheduled-jobs", "probe:automations"],
10486
+ ["monitor/Connected providers", "probe:observability-providers"],
10487
+ ["ci/Quality jobs", "probe:ci-quality-jobs"],
10488
+ ["repository/Rulesets", "api:github-repo"],
10489
+ ["repository/Secrets", "api:github-repo"],
10490
+ ["plugins/plugins-live", "probe:enabled-plugins"],
10491
+ ]);
10492
+ const UI_STATIC_BLOCKS = new Map([
10493
+ [
10494
+ "overview/The four factories — humans hand off at the gates, never inside",
10495
+ "fixed Lisa factory model",
10496
+ ],
10497
+ [
10498
+ "overview/Build lifecycle (fixed — only the status names are configurable)",
10499
+ "fixed Lisa lifecycle semantics",
10500
+ ],
10501
+ ["overview/PRD lifecycle (fixed)", "fixed Lisa lifecycle semantics"],
10502
+ [
10503
+ "overview/How configuration resolves/Resolution order",
10504
+ "fixed Lisa configuration precedence",
10505
+ ],
10506
+ [
10507
+ "overview/How configuration resolves/Secrets",
10508
+ "fixed Lisa secret-handling contract",
10509
+ ],
10510
+ [
10511
+ "workflow/The delivery loop these commands drive",
10512
+ "fixed Lisa delivery model",
10513
+ ],
10514
+ ["workflow/Delivery loop", "installed Lisa command reference"],
10515
+ ["workflow/Commit & ship", "installed Lisa command reference"],
10516
+ ["workflow/Feeder loops", "installed Lisa command reference"],
10517
+ ["workflow/Onboarding & upkeep", "installed Lisa command reference"],
10518
+ ["starters/Sync cycle", "fixed starter synchronization model"],
10519
+ [
10520
+ "starters/Sync behavior (starter → project)/Check for upstream changes",
10521
+ "installed Lisa command reference",
10522
+ ],
10523
+ [
10524
+ "starters/Upstreaming (project → starter)/Scan for upstreamable changes",
10525
+ "installed Lisa command reference",
10526
+ ],
10527
+ [
10528
+ "starters/What counts as generic/Upstreamable",
10529
+ "fixed starter ownership guidance",
10530
+ ],
10531
+ [
10532
+ "starters/What counts as generic/Not upstreamable",
10533
+ "fixed starter ownership guidance",
10534
+ ],
10535
+ [
10536
+ "prd/PRD lifecycle (fixed semantics, configurable names)",
10537
+ "fixed Lisa lifecycle semantics",
10538
+ ],
10539
+ [
10540
+ "linting/Custom rules — code organization",
10541
+ "shipped Lisa rule catalog",
10542
+ ],
10543
+ [
10544
+ "linting/Custom rules — component structure",
10545
+ "shipped Lisa rule catalog",
10546
+ ],
10547
+ ["linting/Custom rules — UI standards", "shipped Lisa rule catalog"],
10548
+ ["linting/Custom rules — Phaser scenes", "shipped Lisa rule catalog"],
10549
+ ["linting/Notable base-config severities", "shipped Lisa rule catalog"],
10550
+ ["linting/ast-grep structural rules", "shipped Lisa rule catalog"],
10551
+ ["testing/Runner per stack", "shipped Lisa runner support matrix"],
10552
+ ["hooks/hooks", "shipped Lisa hook catalog"],
10553
+ ["plugins/MCP servers", "shipped Lisa plugin catalog"],
10554
+ ]);
10555
+ const UI_DEMO_BLOCKS = new Set([
10556
+ "overview/tiles",
10557
+ "factory-research/staff",
10558
+ "factory-research/How to run it",
10559
+ "factory-research/What a run takes",
10560
+ "factory-research/Connections — is the factory wired up?",
10561
+ "factory-plan/staff",
10562
+ "factory-plan/How to run it",
10563
+ "factory-plan/What a run takes",
10564
+ "factory-plan/Connections — is the factory wired up?",
10565
+ "factory-plan/PRD lifecycle roles",
10566
+ "factory-implement/staff",
10567
+ "factory-implement/How to run it",
10568
+ "factory-implement/What a run takes",
10569
+ "factory-implement/Connections — is the factory wired up?",
10570
+ "factory-implement/Gates",
10571
+ "factory-implement/Work-unit lifecycle roles",
10572
+ "factory-verify/staff",
10573
+ "factory-verify/How to run it",
10574
+ "factory-verify/What a run takes",
10575
+ "factory-verify/Connections — is the factory wired up?",
10576
+ "signals/How it senses",
10577
+ "signals/Signal coverage",
10578
+ "credentials/Agent identities",
10579
+ "environments/staff",
10580
+ "environments/environments",
10581
+ "readiness/intake",
10582
+ "agents/Fleet targets",
10583
+ "pipelines/Pipeline inventory",
10584
+ ]);
10585
+ const UI_LIVE_ROWS = new Map([
10586
+ ["health/Lisa version/Installed", "probe:lisa-version"],
10587
+ ["health/Lisa version/Latest published", "probe:lisa-version"],
10588
+ ["health/Lisa version/Update", "probe:lisa-version"],
10589
+ ["health/In-band scan/Latest deterministic check", "api:health"],
10590
+ ]);
10591
+ const UI_STATIC_ROWS = new Map([
10592
+ [
10593
+ "health/In-band scan/Run deterministic health check",
10594
+ "installed Lisa health action",
10595
+ ],
10596
+ ["deploy/Pipeline/CI/CD provider", "fixed supported pipeline host"],
10597
+ ["deploy/Pipeline/Workflows", "fixed Lisa workflow reference"],
10598
+ ["pipelines/Host/Pipeline host", "fixed supported pipeline host"],
10599
+ ["pipelines/Host/Delivery model", "fixed Lisa delivery model"],
10600
+ ["repository/Host/Git host", "fixed supported repository host"],
10601
+ ]);
10602
+ const UI_DEMO_ROWS = new Set([
10603
+ "automations/Fleet controls/Fleet health",
10604
+ "automations/Fleet controls/Run an intake cycle now",
10605
+ "automations/Fleet controls/Pause the fleet",
10606
+ ]);
10607
+ const UI_PRESERVED_TABLE_ROWS = new Map([
10608
+ [
10609
+ "ci/Quality jobs",
10610
+ {
10611
+ identity: "jobId",
10612
+ ids: new Set([
10613
+ "lint",
10614
+ "lint_slow",
10615
+ "typecheck",
10616
+ "format",
10617
+ "build",
10618
+ "test:unit",
10619
+ "test:integration",
10620
+ "playwright_e2e",
10621
+ "maestro_e2e",
10622
+ "e2e_coverage",
10623
+ "test:mutation",
10624
+ "verification_coverage",
10625
+ "dead_code",
10626
+ "sg_scan",
10627
+ "npm_security_scan",
10628
+ "sonarcloud",
10629
+ "snyk",
10630
+ "secret_scanning",
10631
+ "license_compliance",
10632
+ "compliance_validation",
10633
+ "approval_gate",
10634
+ ]),
10635
+ staticCopy: "shipped Lisa quality-job catalog",
10636
+ },
10637
+ ],
10638
+ [
10639
+ "monitor/Connected providers",
10640
+ {
10641
+ identity: "provider",
10642
+ ids: new Set(["sentry", "cloudwatch-alarms", "x-ray"]),
10643
+ staticCopy: "shipped Lisa observability-provider catalog",
10644
+ },
10645
+ ],
10646
+ ]);
10647
+
10648
+ /**
10649
+ * Declare the fixed and probe-painted leaves of a partially live table.
10650
+ * The identifier allowlist is intentionally closed: a newly added row
10651
+ * receives no declaration and the audit bites until its source is named.
10652
+ */
10653
+ function declarePreservedTableLeaves(block, path) {
10654
+ const registry = UI_PRESERVED_TABLE_ROWS.get(path);
10655
+ if (!registry) return;
10656
+ const seen = new Set();
10657
+ (block.rows || []).forEach(row => {
10658
+ if (!Array.isArray(row)) return;
10659
+ const liveCell = row.find(
10660
+ cell =>
10661
+ cell &&
10662
+ typeof cell[registry.identity] === "string" &&
10663
+ registry.ids.has(cell[registry.identity])
10664
+ );
10665
+ if (!liveCell || seen.has(liveCell[registry.identity])) return;
10666
+ seen.add(liveCell[registry.identity]);
10667
+ row.staticCopy = registry.staticCopy;
10668
+ row.forEach(cell => {
10669
+ if (!cell || typeof cell !== "object") return;
10670
+ if (cell === liveCell) cell.liveSource = block.liveSource;
10671
+ else cell.staticCopy = registry.staticCopy;
10672
+ });
10673
+ });
10674
+ }
10675
+
10676
+ /** Apply the explicit semantic-path registry to its catalog shapes. */
10677
+ function declareUiCatalogProvenance(catalog) {
10678
+ const declareBlocks = (sectionId, blocks, ancestry) => {
10679
+ (blocks || []).forEach((block, blockIndex) => {
10680
+ const blockName =
10681
+ block.id ||
10682
+ block.card ||
10683
+ block.title ||
10684
+ block.type ||
10685
+ `block ${blockIndex + 1}`;
10686
+ const pathParts = [sectionId, ...ancestry, blockName];
10687
+ const path = pathParts.join("/");
10688
+ if (block.type === "tabs") {
10689
+ (block.tabs || []).forEach((tab, tabIndex) =>
10690
+ declareBlocks(sectionId, tab.blocks, [
10691
+ ...ancestry,
10692
+ tab.label || `tab ${tabIndex + 1}`,
10693
+ ])
10694
+ );
10695
+ return;
10696
+ }
10697
+ if (UI_LIVE_BLOCKS.has(path)) {
10698
+ block.liveSource = UI_LIVE_BLOCKS.get(path);
10699
+ if (
10700
+ path === "ci/Quality jobs" ||
10701
+ path === "monitor/Connected providers"
10702
+ ) {
10703
+ block.preserveStructure = true;
10704
+ declarePreservedTableLeaves(block, path);
10705
+ }
10706
+ } else if (UI_STATIC_BLOCKS.has(path)) {
10707
+ block.staticCopy = UI_STATIC_BLOCKS.get(path);
10708
+ } else if (UI_DEMO_BLOCKS.has(path)) {
10709
+ block.demoOnly = true;
10710
+ }
10711
+ if (block.type) return;
10712
+ (block.rows || []).forEach((row, rowIndex) => {
10713
+ const rowPath = [
10714
+ ...pathParts,
10715
+ row.label || row.key || `row ${rowIndex + 1}`,
10716
+ ].join("/");
10717
+ if (UI_LIVE_ROWS.has(rowPath)) {
10718
+ row.liveSource = UI_LIVE_ROWS.get(rowPath);
10719
+ } else if (UI_STATIC_ROWS.has(rowPath)) {
10720
+ row.staticCopy = UI_STATIC_ROWS.get(rowPath);
10721
+ } else if (UI_DEMO_ROWS.has(rowPath)) {
10722
+ row.demoOnly = true;
10723
+ }
10724
+ });
10725
+ });
10726
+ };
10727
+ Object.entries(catalog.sections || {}).forEach(([sectionId, section]) =>
10728
+ declareBlocks(sectionId, section.blocks, [])
10729
+ );
10730
+ }
10731
+ declareUiCatalogProvenance(DATA);
10732
+
10733
+ /**
10734
+ * Return the one declared provenance for a renderer-bearing catalog shape.
10735
+ * The declaration lives on the shape that owns the value: a config row's
10736
+ * `key`, an asynchronous `liveSource`, an intentional `demoOnly` value, or
10737
+ * a narrowly explained `staticCopy` value. More than one declaration is as
10738
+ * ambiguous as none and is rejected by the same audit.
10739
+ */
10740
+ function catalogValueProvenance(value) {
10741
+ if (!value || typeof value !== "object") return null;
10742
+ const declared = [];
10743
+ if (typeof value.key === "string" && value.key.trim().length > 0) {
10744
+ declared.push({ kind: "key", source: value.key });
10745
+ }
10746
+ if (
10747
+ typeof value.liveSource === "string" &&
10748
+ value.liveSource.trim().length > 0
10749
+ ) {
10750
+ declared.push({ kind: "liveSource", source: value.liveSource });
10751
+ }
10752
+ if (value.demoOnly === true) {
10753
+ declared.push({ kind: "demoOnly", source: "direct-file demo" });
10754
+ }
10755
+ if (
10756
+ typeof value.staticCopy === "string" &&
10757
+ value.staticCopy.trim().length > 0
10758
+ ) {
10759
+ declared.push({ kind: "staticCopy", source: value.staticCopy });
10760
+ }
10761
+ return declared.length === 1
10762
+ ? declared[0]
10763
+ : { kind: "unclassified", source: "", declared: declared.length };
10764
+ }
10765
+
10766
+ /** Attach audit metadata without changing the JSON-like catalog surface. */
10767
+ function rememberCatalogProvenance(value, provenance, path) {
10768
+ if (!value || typeof value !== "object") return;
10769
+ Object.defineProperty(value, "__lisaValueProvenance", {
10770
+ configurable: true,
10771
+ enumerable: false,
10772
+ value: { ...provenance, path },
10773
+ writable: true,
10774
+ });
10775
+ }
10776
+
10777
+ /** Whether a block renderer consumes project-shaped values from the block. */
10778
+ function isValueBearingBlock(block) {
10779
+ return [
10780
+ "table",
10781
+ "tiles",
10782
+ "flow",
10783
+ "stacks",
10784
+ "hooks",
10785
+ "callout",
10786
+ "checklist",
10787
+ "plugins-live",
10788
+ "staff",
10789
+ "environments",
10790
+ "intake",
10791
+ ].includes(block.type);
10792
+ }
10793
+
10794
+ /** Every typed block the renderer knows how to build. */
10795
+ const UI_BLOCK_RENDERERS = new Set([
10796
+ "doctor-report",
10797
+ "tiles",
10798
+ "flow",
10799
+ "stacks",
10800
+ "hooks",
10801
+ "callout",
10802
+ "table",
10803
+ "tabs",
10804
+ "checklist",
10805
+ "plugins-live",
10806
+ "staff",
10807
+ "environments",
10808
+ "intake",
10809
+ ]);
10810
+
10811
+ /** Stable table-row name used in audit diagnostics and DOM evidence. */
10812
+ function tableRowName(row, rowIndex) {
10813
+ if (!Array.isArray(row)) return `row ${rowIndex + 1}`;
10814
+ const firstValue = row.find(
10815
+ cell => cell && typeof cell.v === "string" && cell.v.length > 0
10816
+ );
10817
+ return firstValue ? firstValue.v : `row ${rowIndex + 1}`;
10818
+ }
10819
+
10820
+ /**
10821
+ * Inspect the same shapes the renderer can turn into visible values.
10822
+ * This deliberately discovers sections, blocks, and rows from the catalog;
10823
+ * no row or section count can go stale when the console grows.
10824
+ */
10825
+ function auditUiCatalog(catalog) {
10826
+ const violations = [];
10827
+ let inspected = 0;
10828
+ const inspect = (value, path, inherited = null) => {
10829
+ inspected += 1;
10830
+ const direct = catalogValueProvenance(value) || {
10831
+ kind: "unclassified",
10832
+ source: "",
10833
+ declared: 0,
10834
+ };
10835
+ const provenance =
10836
+ direct.kind === "unclassified" &&
10837
+ direct.declared === 0 &&
10838
+ inherited &&
10839
+ inherited.kind !== "unclassified"
10840
+ ? { kind: inherited.kind, source: inherited.source }
10841
+ : direct;
10842
+ rememberCatalogProvenance(value, provenance, path);
10843
+ if (provenance.kind === "unclassified") {
10844
+ const suffix =
10845
+ provenance.declared > 1
10846
+ ? ` (${provenance.declared} provenance declarations)`
10847
+ : "";
10848
+ violations.push(path.join(" > ") + suffix);
10849
+ }
10850
+ return provenance;
10851
+ };
10852
+ const inspectBlocks = (sectionId, blocks, ancestry) => {
10853
+ (blocks || []).forEach((block, blockIndex) => {
10854
+ const blockName =
10855
+ block.id ||
10856
+ block.card ||
10857
+ block.title ||
10858
+ block.type ||
10859
+ `block ${blockIndex + 1}`;
10860
+ const path = [sectionId, ...ancestry, blockName];
10861
+ if (block.type === "tabs") {
10862
+ (block.tabs || []).forEach((tab, tabIndex) =>
10863
+ inspectBlocks(sectionId, tab.blocks, [
10864
+ ...ancestry,
10865
+ tab.label || `tab ${tabIndex + 1}`,
10866
+ ])
10867
+ );
10868
+ return;
10869
+ }
10870
+ if (block.type && !UI_BLOCK_RENDERERS.has(block.type)) {
10871
+ inspected += 1;
10872
+ violations.push(
10873
+ path.join(" > ") + ` (unsupported renderer type ${block.type})`
10874
+ );
10875
+ return;
10876
+ }
10877
+ const directBlockProvenance = catalogValueProvenance(block);
10878
+ const blockProvenance =
10879
+ isValueBearingBlock(block) ||
10880
+ directBlockProvenance.kind !== "unclassified"
10881
+ ? inspect(block, path)
10882
+ : null;
10883
+ if (block.type === "table") {
10884
+ (block.rows || []).forEach((row, rowIndex) => {
10885
+ const rowPath = [...path, tableRowName(row, rowIndex)];
10886
+ const inherited =
10887
+ block.preserveStructure === true ? null : blockProvenance;
10888
+ const rowProvenance = inspect(row, rowPath, inherited);
10889
+ (row || []).forEach((cell, cellIndex) =>
10890
+ inspect(
10891
+ cell,
10892
+ [...rowPath, `cell ${cellIndex + 1}`],
10893
+ block.preserveStructure === true ? null : rowProvenance
10894
+ )
10895
+ );
10896
+ });
10897
+ return;
10898
+ }
10899
+ if (block.type) return;
10900
+ (block.rows || []).forEach((row, rowIndex) => {
10901
+ if (!row || !row.control) return;
10902
+ inspect(
10903
+ row,
10904
+ [...path, row.label || row.key || `row ${rowIndex + 1}`],
10905
+ blockProvenance
10906
+ );
10907
+ });
10908
+ });
10909
+ };
10910
+ Object.entries(catalog.sections || {}).forEach(([sectionId, section]) =>
10911
+ inspectBlocks(sectionId, section.blocks, [])
10912
+ );
10913
+ if (inspected === 0) {
10914
+ violations.push("catalog audit inspected zero renderer values");
10915
+ }
10916
+ const result = {
10917
+ inspected,
10918
+ sections: Object.keys(catalog.sections || {}).length,
10919
+ violations,
10920
+ };
10921
+ if (violations.length > 0) {
10922
+ throw new Error(
10923
+ "UI demo-data audit found unclassified rendered values:\n- " +
10924
+ violations.join("\n- ")
10925
+ );
10926
+ }
10927
+ return result;
10928
+ }
10929
+
10930
+ /** Read a dot path while preserving the difference between missing and falsey. */
10931
+ function liveConfigEntry(config, dotPath) {
10932
+ let node = config;
10933
+ for (const segment of dotPath.split(".")) {
10934
+ if (
10935
+ !node ||
10936
+ typeof node !== "object" ||
10937
+ Array.isArray(node) ||
10938
+ !Object.hasOwn(node, segment)
10939
+ ) {
10940
+ return { present: false, value: undefined };
10941
+ }
10942
+ node = node[segment];
10943
+ }
10944
+ return { present: true, value: node };
10945
+ }
10946
+
10947
+ /** Replace a missing config value with a renderer-native unknown state. */
10948
+ function unknownControl(row, reason) {
10949
+ row.control = {
10950
+ ...(row.control || {}),
10951
+ type: "static",
10952
+ value: "unknown",
10953
+ cls: "warn",
10954
+ reason,
10955
+ };
10956
+ }
10957
+
10958
+ /** Initialize a probe/API table without leaving any demo row visible. */
10959
+ function resetLiveBlock(block) {
10960
+ if (block.preserveStructure === true) {
10961
+ (block.rows || []).forEach(row => {
10962
+ if (!Array.isArray(row)) return;
10963
+ row.forEach(cell => {
10964
+ if (cell && cell.t === "status") {
10965
+ cell.v = null;
10966
+ cell.reason = "pending live source: " + block.liveSource;
10967
+ }
10968
+ });
10969
+ });
10970
+ return;
10971
+ }
10972
+ if (block.type === "table") {
10973
+ block.rows = [];
10974
+ block.empty = {
10975
+ state: "unknown",
10976
+ message: "Waiting for " + block.liveSource,
10977
+ };
10978
+ }
10979
+ }
10980
+
10981
+ /**
10982
+ * Scrub demo state before the first live render and hydrate config-keyed
10983
+ * controls. The function mutates the in-memory catalog once so later full
10984
+ * rerenders cannot resurrect a demo fallback.
10985
+ */
10986
+ function prepareUiCatalogForLive(catalog, config) {
10987
+ const usableConfig =
10988
+ config && typeof config === "object" && !Array.isArray(config)
10989
+ ? config
10990
+ : {};
10991
+ let removedDemo = 0;
10992
+ let pending = 0;
10993
+ const prepareBlocks = blocks =>
10994
+ (blocks || []).flatMap(block => {
10995
+ if (block.type === "tabs") {
10996
+ (block.tabs || []).forEach(tab => {
10997
+ tab.blocks = prepareBlocks(tab.blocks);
10998
+ });
10999
+ return [block];
11000
+ }
11001
+ const blockProvenance = block.__lisaValueProvenance;
11002
+ if (blockProvenance?.kind === "demoOnly") {
11003
+ removedDemo += 1;
11004
+ return [];
11005
+ }
11006
+ if (blockProvenance?.kind === "liveSource") {
11007
+ pending += 1;
11008
+ resetLiveBlock(block);
11009
+ }
11010
+ if (!block.type && Array.isArray(block.rows)) {
11011
+ block.rows = block.rows.flatMap(row => {
11012
+ const provenance = row.__lisaValueProvenance;
11013
+ if (provenance?.kind === "demoOnly") {
11014
+ removedDemo += 1;
11015
+ return [];
11016
+ }
11017
+ if (provenance?.kind === "key") {
11018
+ const entry = liveConfigEntry(
11019
+ usableConfig,
11020
+ provenance.source
11021
+ );
11022
+ if (entry.present) {
11023
+ const liveControl = hydrateControl(
11024
+ row.control,
11025
+ entry.value
11026
+ );
11027
+ if (liveControl) row.control = liveControl;
11028
+ else unknownControl(row, "invalid live config value");
11029
+ } else unknownControl(row, "missing live config key");
11030
+ } else if (provenance?.kind === "liveSource") {
11031
+ pending += 1;
11032
+ unknownControl(row, "pending " + provenance.source);
11033
+ }
11034
+ return [row];
11035
+ });
11036
+ }
11037
+ return [block];
11038
+ });
11039
+ Object.values(catalog.sections || {}).forEach(section => {
11040
+ section.blocks = prepareBlocks(section.blocks);
11041
+ });
11042
+ const remainingDemo = countCatalogProvenance(catalog, "demoOnly");
11043
+ return {
11044
+ liveMode: true,
11045
+ removedDemo,
11046
+ pending,
11047
+ remainingDemo,
11048
+ };
11049
+ }
11050
+
11051
+ /** Count audited shapes of one provenance kind after preparation. */
11052
+ function countCatalogProvenance(catalog, kind) {
11053
+ let count = 0;
11054
+ const visitBlocks = blocks =>
11055
+ (blocks || []).forEach(block => {
11056
+ if (block.__lisaValueProvenance?.kind === kind) count += 1;
11057
+ if (block.type === "tabs") {
11058
+ (block.tabs || []).forEach(tab => visitBlocks(tab.blocks));
11059
+ }
11060
+ if (!block.type) {
11061
+ (block.rows || []).forEach(row => {
11062
+ if (row.__lisaValueProvenance?.kind === kind) count += 1;
11063
+ });
11064
+ }
11065
+ });
11066
+ Object.values(catalog.sections || {}).forEach(section =>
11067
+ visitBlocks(section.blocks)
11068
+ );
11069
+ return count;
11070
+ }
11071
+ /* LISA_UI_CATALOG_END */
11072
+
10369
11073
  /* ------------------------------------------------------------------ *
10370
11074
  * Rendering + interactions (prototype: state is in-memory only)
10371
11075
  * ------------------------------------------------------------------ */
@@ -10375,6 +11079,9 @@
10375
11079
  const $savebar = document.getElementById("savebar");
10376
11080
  const $toast = document.getElementById("toast");
10377
11081
  const dirty = new Set();
11082
+ let lisaLiveMode = false;
11083
+ let lisaCatalogAudit = null;
11084
+ let lisaCatalogPreparation = null;
10378
11085
 
10379
11086
  /* Live enabled-plugins state, hydrated from GET /api/status. Starts as
10380
11087
  "loading" so the Plugins section never presents demo toggles as truth. */
@@ -10497,6 +11204,27 @@
10497
11204
  updateSavebar();
10498
11205
  }
10499
11206
 
11207
+ /** Mark the DOM owner of a catalog value with its audited provenance. */
11208
+ function markRenderedValue(node, value) {
11209
+ if (!node || !value) return node;
11210
+ const provenance =
11211
+ value.__lisaValueProvenance || catalogValueProvenance(value);
11212
+ if (!provenance || provenance.kind === "unclassified") return node;
11213
+ node.dataset.lisaValueSource = provenance.kind;
11214
+ node.dataset.lisaValuePath = provenance.path
11215
+ ? provenance.path.join(" > ")
11216
+ : provenance.source;
11217
+ return node;
11218
+ }
11219
+
11220
+ /** Re-audit after catalog mutation and expose the latest browser evidence. */
11221
+ function refreshCatalogAudit() {
11222
+ lisaCatalogAudit = auditUiCatalog(DATA);
11223
+ if (window.LISA_UI_CATALOG_STATE) {
11224
+ window.LISA_UI_CATALOG_STATE.audit = lisaCatalogAudit;
11225
+ }
11226
+ }
11227
+
10500
11228
  let ctlSeq = 0;
10501
11229
  /* Propagate a shared setting to every control bound to the same syncKey:
10502
11230
  first into DATA so a re-render keeps the new value, then into any
@@ -10575,11 +11303,22 @@
10575
11303
  } else if (control.type === "select") {
10576
11304
  const s = document.createElement("select");
10577
11305
  s.className = "ctl" + (control.mono ? " mono" : "");
10578
- (control.options || []).forEach(o => {
11306
+ const options = control.options || [];
11307
+ const hasMatchingValue =
11308
+ control.value !== "" && options.some(o => o === control.value);
11309
+ if (!hasMatchingValue) {
11310
+ const unknown = document.createElement("option");
11311
+ unknown.value = "";
11312
+ unknown.textContent = "unknown";
11313
+ unknown.selected = true;
11314
+ s.dataset.state = "unknown";
11315
+ s.appendChild(unknown);
11316
+ }
11317
+ options.forEach(o => {
10579
11318
  const opt = document.createElement("option");
10580
11319
  opt.value = o;
10581
11320
  opt.textContent = o;
10582
- if (o === control.value) opt.selected = true;
11321
+ if (hasMatchingValue && o === control.value) opt.selected = true;
10583
11322
  s.appendChild(opt);
10584
11323
  });
10585
11324
  bind(s, i => i.closest(".row") || i.closest("tr"));
@@ -10786,7 +11525,7 @@
10786
11525
  ? { ...row.control, syncKey: row.key }
10787
11526
  : row.control;
10788
11527
  r.appendChild(buildControl(control, row.key));
10789
- return r;
11528
+ return markRenderedValue(r, row);
10790
11529
  }
10791
11530
 
10792
11531
  function buildCard(block) {
@@ -10906,12 +11645,13 @@
10906
11645
  const tbody = el("tbody");
10907
11646
  block.rows.forEach(cells => {
10908
11647
  const tr = el("tr");
11648
+ markRenderedValue(tr, cells);
10909
11649
  if (cells && cells._automationId) {
10910
11650
  tr.dataset.automationId = cells._automationId;
10911
11651
  }
10912
11652
  cells.forEach(cell => {
10913
11653
  if (cell.t === "status") {
10914
- tr.appendChild(buildStatusCell(cell));
11654
+ tr.appendChild(markRenderedValue(buildStatusCell(cell), cell));
10915
11655
  return;
10916
11656
  }
10917
11657
  const td = el("td");
@@ -11039,7 +11779,7 @@
11039
11779
  buildControl({ ...cell, type: cell.t, value: cell.v }, null)
11040
11780
  );
11041
11781
  }
11042
- tr.appendChild(td);
11782
+ tr.appendChild(markRenderedValue(td, cell));
11043
11783
  });
11044
11784
  tbody.appendChild(tr);
11045
11785
  });
@@ -11219,6 +11959,7 @@
11219
11959
  "div",
11220
11960
  "callout" + (block.variant === "warn" ? " warn" : "")
11221
11961
  );
11962
+ box.dataset.lisaRenderer = "callout";
11222
11963
  // Single child span: the callout is flex, and multiple inline nodes
11223
11964
  // would each become flex items and render as broken columns.
11224
11965
  box.appendChild(el("span", "", block.text));
@@ -11499,20 +12240,24 @@
11499
12240
  }
11500
12241
 
11501
12242
  function buildBlock(block) {
11502
- if (block.type === "doctor-report") return buildDoctorReport();
11503
- if (block.type === "tiles") return buildTiles(block);
11504
- if (block.type === "flow") return buildFlow(block);
11505
- if (block.type === "stacks") return buildStacks(block);
11506
- if (block.type === "hooks") return buildHooks(block);
11507
- if (block.type === "callout") return buildCallout(block);
11508
- if (block.type === "table") return buildTable(block);
11509
- if (block.type === "tabs") return buildTabs(block);
11510
- if (block.type === "checklist") return buildChecklist(block);
11511
- if (block.type === "plugins-live") return buildPluginsLive(block);
11512
- if (block.type === "staff") return buildStaff(block);
11513
- if (block.type === "environments") return buildEnvironments();
11514
- if (block.type === "intake") return buildIntake(block);
11515
- return buildCard(block);
12243
+ let rendered;
12244
+ if (block.type === "doctor-report") rendered = buildDoctorReport();
12245
+ else if (block.type === "tiles") rendered = buildTiles(block);
12246
+ else if (block.type === "flow") rendered = buildFlow(block);
12247
+ else if (block.type === "stacks") rendered = buildStacks(block);
12248
+ else if (block.type === "hooks") rendered = buildHooks(block);
12249
+ else if (block.type === "callout") rendered = buildCallout(block);
12250
+ else if (block.type === "table") rendered = buildTable(block);
12251
+ else if (block.type === "tabs") rendered = buildTabs(block);
12252
+ else if (block.type === "checklist") rendered = buildChecklist(block);
12253
+ else if (block.type === "plugins-live") {
12254
+ rendered = buildPluginsLive(block);
12255
+ } else if (block.type === "staff") rendered = buildStaff(block);
12256
+ else if (block.type === "environments") rendered = buildEnvironments();
12257
+ else if (block.type === "intake") rendered = buildIntake(block);
12258
+ else if (!block.type) rendered = buildCard(block);
12259
+ else throw new TypeError("unsupported renderer type " + block.type);
12260
+ return markRenderedValue(rendered, block);
11516
12261
  }
11517
12262
 
11518
12263
  /* The readiness intake: one flat, answerable question per row, grouped by
@@ -11734,6 +12479,7 @@
11734
12479
 
11735
12480
  function renderAll() {
11736
12481
  syncHealthViewModel();
12482
+ refreshCatalogAudit();
11737
12483
  $main.innerHTML = "";
11738
12484
  $nav.innerHTML = "";
11739
12485
  ctlSeq = 0;
@@ -11760,7 +12506,25 @@
11760
12506
  if (section.src)
11761
12507
  head.appendChild(el("span", "src-chip", "⚙ " + esc(section.src)));
11762
12508
  sec.appendChild(head);
11763
- section.blocks.forEach(b => sec.appendChild(buildBlock(b)));
12509
+ if (section.blocks.length === 0) {
12510
+ const empty = el(
12511
+ "div",
12512
+ "callout section-empty",
12513
+ lisaLiveMode
12514
+ ? "No live value is available for this section."
12515
+ : "No values are available for this section."
12516
+ );
12517
+ empty.dataset.testid = "section-empty";
12518
+ empty.dataset.state = lisaLiveMode ? "unknown" : "empty";
12519
+ sec.appendChild(empty);
12520
+ } else {
12521
+ section.blocks.forEach(b => sec.appendChild(buildBlock(b)));
12522
+ }
12523
+ sec.dataset.liveState = lisaLiveMode
12524
+ ? section.blocks.length === 0
12525
+ ? "empty"
12526
+ : "live"
12527
+ : "demo";
11764
12528
  $main.appendChild(sec);
11765
12529
  });
11766
12530
  }
@@ -11794,65 +12558,31 @@
11794
12558
  const q = (query || "").trim().toLowerCase();
11795
12559
  const sec = document.getElementById("section-" + activeSection);
11796
12560
  if (!sec) return;
11797
- sec
11798
- .querySelectorAll(".row, tbody tr, .stack-card, .hook-card")
11799
- .forEach(node => {
11800
- node.style.display =
11801
- !q || node.textContent.toLowerCase().includes(q) ? "" : "none";
11802
- });
11803
- }
11804
-
11805
- /* --- live-config hydration (lisa ui injects window.LISA_LIVE_CONFIG) --- */
11806
- function walkRows(blocks, fn) {
11807
- (blocks || []).forEach(block => {
11808
- (block.rows || []).forEach(fn);
11809
- (block.tabs || []).forEach(tab => walkRows(tab.blocks, fn));
12561
+ const existing = sec.querySelector(".filter-empty");
12562
+ if (existing) existing.remove();
12563
+ const candidates = [
12564
+ ...sec.querySelectorAll(".row, tbody tr, .stack-card, .hook-card"),
12565
+ ];
12566
+ candidates.forEach(node => {
12567
+ node.style.display =
12568
+ !q || node.textContent.toLowerCase().includes(q) ? "" : "none";
11810
12569
  });
11811
- }
11812
- function liveGet(obj, dotPath) {
11813
- return dotPath
11814
- .split(".")
11815
- .reduce(
11816
- (node, seg) =>
11817
- node && typeof node === "object" && !Array.isArray(node)
11818
- ? node[seg]
11819
- : undefined,
11820
- obj
12570
+ if (
12571
+ q &&
12572
+ candidates.length > 0 &&
12573
+ candidates.every(node => node.style.display === "none")
12574
+ ) {
12575
+ const empty = el(
12576
+ "div",
12577
+ "callout filter-empty",
12578
+ "No live or static values match this filter."
11821
12579
  );
11822
- }
11823
- function hydrateControl(control, value) {
11824
- if (control.type === "toggle") {
11825
- control.value = !!value;
11826
- return;
11827
- }
11828
- if (control.type === "number" && typeof value === "number") {
11829
- control.value = value;
11830
- return;
11831
- }
11832
- if (control.type === "text" || control.type === "select") {
11833
- control.value = String(value);
11834
- return;
11835
- }
11836
- if (control.type === "tags" && Array.isArray(value)) {
11837
- control.values = value.map(String);
11838
- return;
11839
- }
11840
- if (control.type === "envmap") {
11841
- if (typeof value === "string") {
11842
- control.production = value;
11843
- return;
11844
- }
11845
- if (value && typeof value === "object") {
11846
- ["dev", "staging", "production"].forEach(envName => {
11847
- if (value[envName] !== undefined)
11848
- control[envName] = String(value[envName]);
11849
- });
11850
- }
11851
- return;
12580
+ empty.dataset.state = lisaLiveMode ? "unknown" : "empty";
12581
+ sec.appendChild(empty);
11852
12582
  }
11853
- if (control.type === "static" && typeof value === "string")
11854
- control.value = value;
11855
12583
  }
12584
+
12585
+ /* --- live-config hydration (lisa ui injects window.LISA_LIVE_CONFIG) --- */
11856
12586
  const REMOTE_AGENT_ORDER = [
11857
12587
  "claude",
11858
12588
  "codex",
@@ -11918,22 +12648,32 @@
11918
12648
  DATA.sections.remote.blocks[1].rows = startupRows;
11919
12649
  }
11920
12650
  function hydrateFromLiveConfig() {
11921
- const cfg = window.LISA_LIVE_CONFIG || {};
11922
- if (window.LISA_LIVE_CONFIG) {
11923
- Object.values(DATA.sections).forEach(section => {
11924
- walkRows(section.blocks, row => {
11925
- if (!row.key || !row.control) return;
11926
- const value = liveGet(cfg, row.key);
11927
- if (value !== undefined) hydrateControl(row.control, value);
11928
- });
11929
- });
12651
+ lisaLiveMode = Object.hasOwn(window, "LISA_LIVE_CONFIG");
12652
+ const cfg = lisaLiveMode ? window.LISA_LIVE_CONFIG : {};
12653
+ lisaCatalogAudit = auditUiCatalog(DATA);
12654
+ if (lisaLiveMode) {
12655
+ lisaCatalogPreparation = prepareUiCatalogForLive(DATA, cfg);
11930
12656
  markObservabilityProvidersPending();
12657
+ configureRemoteEnvironment(cfg);
12658
+ } else {
12659
+ lisaCatalogPreparation = null;
11931
12660
  }
11932
- configureRemoteEnvironment(cfg);
11933
12661
  const repoEl = document.querySelector(".project-switch .repo");
11934
- if (repoEl && cfg.github && cfg.github.org && cfg.github.repo) {
12662
+ if (
12663
+ repoEl &&
12664
+ cfg &&
12665
+ typeof cfg === "object" &&
12666
+ cfg.github &&
12667
+ cfg.github.org &&
12668
+ cfg.github.repo
12669
+ ) {
11935
12670
  repoEl.textContent = cfg.github.org + "/" + cfg.github.repo;
11936
12671
  }
12672
+ window.LISA_UI_CATALOG_STATE = {
12673
+ liveMode: lisaLiveMode,
12674
+ audit: lisaCatalogAudit,
12675
+ preparation: lisaCatalogPreparation,
12676
+ };
11937
12677
  }
11938
12678
 
11939
12679
  /* --- live-status hydration (same-origin GET /api/status) --- */
@@ -12002,6 +12742,7 @@
12002
12742
  function setProviderStatusCell(provider, cell) {
12003
12743
  const block = connectedProvidersBlock();
12004
12744
  if (!block || !Array.isArray(block.rows)) return;
12745
+ cell.liveSource = block.liveSource;
12005
12746
  block.rows.forEach(row => {
12006
12747
  if (!Array.isArray(row)) return;
12007
12748
  row.forEach((entry, index) => {
@@ -12010,10 +12751,14 @@
12010
12751
  }
12011
12752
  });
12012
12753
  });
12754
+ refreshCatalogAudit();
12013
12755
  const td = document.querySelector(
12014
12756
  'td[data-provider="' + provider + '"]'
12015
12757
  );
12016
- if (td) paintProviderStatus(td, cell);
12758
+ if (td) {
12759
+ paintProviderStatus(td, cell);
12760
+ markRenderedValue(td, cell);
12761
+ }
12017
12762
  }
12018
12763
  function markObservabilityProvidersPending() {
12019
12764
  ["sentry", "cloudwatch-alarms", "x-ray"].forEach(provider => {
@@ -12413,6 +13158,7 @@
12413
13158
  /** Replace only the existing Health results card after state changes. */
12414
13159
  function repaintHealthResults() {
12415
13160
  syncHealthViewModel();
13161
+ refreshCatalogAudit();
12416
13162
  const block = healthResultsBlock();
12417
13163
  const current = document.getElementById("healthResults");
12418
13164
  if (block && current) current.replaceWith(buildTable(block));
@@ -12708,6 +13454,7 @@
12708
13454
  rows: [
12709
13455
  {
12710
13456
  key: "",
13457
+ staticCopy: "fixed supported repository host",
12711
13458
  label: "Git host",
12712
13459
  desc: "Which host this repository lives on. GitHub is the only host Lisa provisions today.",
12713
13460
  control: sel("GitHub", ["GitHub", "GitLab", "Bitbucket"]),
@@ -12716,66 +13463,82 @@
12716
13463
  };
12717
13464
  }
12718
13465
  function githubRepoSettingsRows(settings) {
13466
+ const setting = (name, render) =>
13467
+ Object.hasOwn(settings, name)
13468
+ ? render(settings[name])
13469
+ : stat("unknown", "warn");
12719
13470
  return [
12720
13471
  {
12721
13472
  label: "Allow merge commits",
12722
13473
  desc: "The fleet is merge-only — squash and rebase are disabled because squashing flattens release commits and breaks promotion.",
12723
- control: tog(!!settings.allow_merge_commit),
13474
+ control: setting("allow_merge_commit", value =>
13475
+ tog(Boolean(value))
13476
+ ),
12724
13477
  },
12725
13478
  {
12726
13479
  label: "Allow squash merge",
12727
13480
  desc: "",
12728
- control: tog(!!settings.allow_squash_merge),
13481
+ control: setting("allow_squash_merge", value =>
13482
+ tog(Boolean(value))
13483
+ ),
12729
13484
  },
12730
13485
  {
12731
13486
  label: "Allow rebase merge",
12732
13487
  desc: "",
12733
- control: tog(!!settings.allow_rebase_merge),
13488
+ control: setting("allow_rebase_merge", value =>
13489
+ tog(Boolean(value))
13490
+ ),
12734
13491
  },
12735
13492
  {
12736
13493
  label: "Auto-merge",
12737
13494
  desc: "Lisa PRs enable auto-merge and drive themselves to green.",
12738
- control: tog(!!settings.allow_auto_merge),
13495
+ control: setting("allow_auto_merge", value => tog(Boolean(value))),
12739
13496
  },
12740
13497
  {
12741
13498
  label: "Suggest updating PR branches",
12742
13499
  desc: "",
12743
- control: tog(!!settings.allow_update_branch),
13500
+ control: setting("allow_update_branch", value =>
13501
+ tog(Boolean(value))
13502
+ ),
12744
13503
  },
12745
13504
  {
12746
13505
  label: "Delete head branch on merge",
12747
13506
  desc: "Environment branches survive via the prevent-delete ruleset.",
12748
- control: tog(!!settings.delete_branch_on_merge),
13507
+ control: setting("delete_branch_on_merge", value =>
13508
+ tog(Boolean(value))
13509
+ ),
12749
13510
  },
12750
13511
  {
12751
13512
  label: "Merge commit title",
12752
13513
  desc: "",
12753
- control: sel(String(settings.merge_commit_title || ""), [
12754
- "MERGE_MESSAGE",
12755
- "PR_TITLE",
12756
- ]),
13514
+ control: setting("merge_commit_title", value =>
13515
+ sel(value === null ? "" : String(value), [
13516
+ "MERGE_MESSAGE",
13517
+ "PR_TITLE",
13518
+ ])
13519
+ ),
12757
13520
  },
12758
13521
  {
12759
13522
  label: "Issues tab",
12760
13523
  desc: "",
12761
- control: tog(!!settings.has_issues),
13524
+ control: setting("has_issues", value => tog(Boolean(value))),
12762
13525
  },
12763
13526
  {
12764
13527
  label: "Wiki tab",
12765
13528
  desc: "Off — Lisa uses the in-repo wiki/ directory instead.",
12766
- control: tog(!!settings.has_wiki),
13529
+ control: setting("has_wiki", value => tog(Boolean(value))),
12767
13530
  },
12768
13531
  {
12769
13532
  label: "Secret scanning + push protection",
12770
13533
  desc: "Best-effort; skipped when the plan lacks GitHub Advanced Security.",
12771
- control: tog(!!settings.secret_scanning),
13534
+ control: setting("secret_scanning", value => tog(Boolean(value))),
12772
13535
  },
12773
13536
  {
12774
13537
  label: "Default branch",
12775
13538
  desc: "Live default branch from the repository.",
12776
- control: txt(String(settings.default_branch || ""), {
12777
- mono: true,
12778
- }),
13539
+ control: setting("default_branch", value =>
13540
+ txt(value === null ? "" : String(value), { mono: true })
13541
+ ),
12779
13542
  },
12780
13543
  ];
12781
13544
  }
@@ -12790,6 +13553,7 @@
12790
13553
  host,
12791
13554
  {
12792
13555
  type: "callout",
13556
+ liveSource: "api:github-repo",
12793
13557
  variant: "warn",
12794
13558
  text:
12795
13559
  "<b>unknown — not authenticated / unavailable</b> — " +
@@ -12808,6 +13572,7 @@
12808
13572
  host,
12809
13573
  {
12810
13574
  card: "Repository settings",
13575
+ liveSource: "api:github-repo",
12811
13576
  note:
12812
13577
  "Live from gh api repos/" +
12813
13578
  String(value.owner || "") +
@@ -12817,9 +13582,15 @@
12817
13582
  },
12818
13583
  {
12819
13584
  type: "table",
13585
+ id: "github-rulesets",
13586
+ liveSource: "api:github-repo",
12820
13587
  card: "Rulesets",
12821
13588
  note: "Live from gh api repos/{owner}/{repo}/rulesets",
12822
13589
  cols: ["Ruleset", "Applies to", "Enforces", "Active"],
13590
+ empty: {
13591
+ state: "empty",
13592
+ message: "No live rulesets were reported.",
13593
+ },
12823
13594
  rows: rulesets.map(row => [
12824
13595
  { t: "mono", v: String(row.name || "") },
12825
13596
  { t: "text", v: String(row.appliesTo || "—") },
@@ -12833,9 +13604,15 @@
12833
13604
  },
12834
13605
  {
12835
13606
  type: "table",
13607
+ id: "github-labels",
13608
+ liveSource: "api:github-repo",
12836
13609
  card: "Lifecycle labels",
12837
13610
  note: "Config-expected labels vs live gh label list",
12838
13611
  cols: ["Label", "Role", "Color", "Status"],
13612
+ empty: {
13613
+ state: "empty",
13614
+ message: "No live lifecycle labels were reported.",
13615
+ },
12839
13616
  rows: labels.map(row => [
12840
13617
  { t: "mono", v: String(row.name || "") },
12841
13618
  { t: "text", v: String(row.role || "") },
@@ -12854,9 +13631,15 @@
12854
13631
  },
12855
13632
  {
12856
13633
  type: "table",
13634
+ id: "github-secrets",
13635
+ liveSource: "api:github-repo",
12857
13636
  card: "Secrets",
12858
13637
  note: "Presence only from gh secret list — values are never read",
12859
13638
  cols: ["Secret", "Purpose", "Status"],
13639
+ empty: {
13640
+ state: "empty",
13641
+ message: "No live secret-presence rows were reported.",
13642
+ },
12860
13643
  rows: secrets.map(row => [
12861
13644
  { t: "mono", v: String(row.name || "") },
12862
13645
  { t: "text", v: String(row.purpose || "") },
@@ -12965,6 +13748,7 @@
12965
13748
  ],
12966
13749
  ];
12967
13750
  }
13751
+ refreshCatalogAudit();
12968
13752
  const existing = document.getElementById("deploy-pipeline-stages");
12969
13753
  if (existing) {
12970
13754
  existing.replaceWith(buildTable(block));
@@ -13175,8 +13959,10 @@
13175
13959
  showSection(location.hash ? location.hash.slice(1) : "overview");
13176
13960
  if (!document.getElementById("section-" + activeSection))
13177
13961
  showSection("overview");
13178
- void hydrateLiveStatuses();
13179
- void hydrateHealth();
13962
+ if (lisaLiveMode) {
13963
+ void hydrateLiveStatuses();
13964
+ void hydrateHealth();
13965
+ }
13180
13966
  }
13181
13967
  if (document.readyState === "loading") {
13182
13968
  document.addEventListener("DOMContentLoaded", boot);