@codyswann/lisa 2.234.0 → 2.236.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 (117) hide show
  1. package/dist/cli/ui-automations-adapters.d.ts +50 -0
  2. package/dist/cli/ui-automations-adapters.d.ts.map +1 -0
  3. package/dist/cli/ui-automations-adapters.js +162 -0
  4. package/dist/cli/ui-automations-adapters.js.map +1 -0
  5. package/dist/cli/ui-automations.d.ts +89 -0
  6. package/dist/cli/ui-automations.d.ts.map +1 -0
  7. package/dist/cli/ui-automations.js +148 -0
  8. package/dist/cli/ui-automations.js.map +1 -0
  9. package/dist/cli/ui-ci-quality-jobs-compute.d.ts +46 -0
  10. package/dist/cli/ui-ci-quality-jobs-compute.d.ts.map +1 -0
  11. package/dist/cli/ui-ci-quality-jobs-compute.js +178 -0
  12. package/dist/cli/ui-ci-quality-jobs-compute.js.map +1 -0
  13. package/dist/cli/ui-ci-quality-jobs-parse.d.ts +15 -0
  14. package/dist/cli/ui-ci-quality-jobs-parse.d.ts.map +1 -0
  15. package/dist/cli/ui-ci-quality-jobs-parse.js +109 -0
  16. package/dist/cli/ui-ci-quality-jobs-parse.js.map +1 -0
  17. package/dist/cli/ui-ci-quality-jobs.d.ts +22 -0
  18. package/dist/cli/ui-ci-quality-jobs.d.ts.map +1 -0
  19. package/dist/cli/ui-ci-quality-jobs.js +160 -0
  20. package/dist/cli/ui-ci-quality-jobs.js.map +1 -0
  21. package/dist/cli/ui-cmd.d.ts +6 -0
  22. package/dist/cli/ui-cmd.d.ts.map +1 -1
  23. package/dist/cli/ui-cmd.js +20 -0
  24. package/dist/cli/ui-cmd.js.map +1 -1
  25. package/dist/cli/ui-deploy-pipeline-model.d.ts +70 -0
  26. package/dist/cli/ui-deploy-pipeline-model.d.ts.map +1 -0
  27. package/dist/cli/ui-deploy-pipeline-model.js +127 -0
  28. package/dist/cli/ui-deploy-pipeline-model.js.map +1 -0
  29. package/dist/cli/ui-deploy-pipeline.d.ts +25 -0
  30. package/dist/cli/ui-deploy-pipeline.d.ts.map +1 -0
  31. package/dist/cli/ui-deploy-pipeline.js +192 -0
  32. package/dist/cli/ui-deploy-pipeline.js.map +1 -0
  33. package/dist/cli/ui-detected-stacks.d.ts +20 -0
  34. package/dist/cli/ui-detected-stacks.d.ts.map +1 -0
  35. package/dist/cli/ui-detected-stacks.js +28 -0
  36. package/dist/cli/ui-detected-stacks.js.map +1 -0
  37. package/dist/cli/ui-enabled-plugins.d.ts +52 -0
  38. package/dist/cli/ui-enabled-plugins.d.ts.map +1 -0
  39. package/dist/cli/ui-enabled-plugins.js +253 -0
  40. package/dist/cli/ui-enabled-plugins.js.map +1 -0
  41. package/dist/cli/ui-github-repo-expected.d.ts +36 -0
  42. package/dist/cli/ui-github-repo-expected.d.ts.map +1 -0
  43. package/dist/cli/ui-github-repo-expected.js +118 -0
  44. package/dist/cli/ui-github-repo-expected.js.map +1 -0
  45. package/dist/cli/ui-github-repo-gh.d.ts +36 -0
  46. package/dist/cli/ui-github-repo-gh.d.ts.map +1 -0
  47. package/dist/cli/ui-github-repo-gh.js +112 -0
  48. package/dist/cli/ui-github-repo-gh.js.map +1 -0
  49. package/dist/cli/ui-github-repo-map.d.ts +25 -0
  50. package/dist/cli/ui-github-repo-map.d.ts.map +1 -0
  51. package/dist/cli/ui-github-repo-map.js +150 -0
  52. package/dist/cli/ui-github-repo-map.js.map +1 -0
  53. package/dist/cli/ui-github-repo.d.ts +51 -0
  54. package/dist/cli/ui-github-repo.d.ts.map +1 -0
  55. package/dist/cli/ui-github-repo.js +117 -0
  56. package/dist/cli/ui-github-repo.js.map +1 -0
  57. package/dist/cli/ui-observability-checks.d.ts +23 -0
  58. package/dist/cli/ui-observability-checks.d.ts.map +1 -0
  59. package/dist/cli/ui-observability-checks.js +156 -0
  60. package/dist/cli/ui-observability-checks.js.map +1 -0
  61. package/dist/cli/ui-observability-providers.d.ts +69 -0
  62. package/dist/cli/ui-observability-providers.d.ts.map +1 -0
  63. package/dist/cli/ui-observability-providers.js +99 -0
  64. package/dist/cli/ui-observability-providers.js.map +1 -0
  65. package/package.json +1 -1
  66. package/plugins/lisa/.claude-plugin/plugin.json +1 -1
  67. package/plugins/lisa/.codex-plugin/plugin.json +1 -1
  68. package/plugins/lisa-agy/plugin.json +1 -1
  69. package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
  70. package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
  71. package/plugins/lisa-cdk-agy/plugin.json +1 -1
  72. package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
  73. package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
  74. package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
  75. package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
  76. package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
  77. package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
  78. package/plugins/lisa-expo-agy/plugin.json +1 -1
  79. package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
  80. package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
  81. package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
  82. package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
  83. package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
  84. package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
  85. package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
  86. package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
  87. package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
  88. package/plugins/lisa-nestjs-agy/plugin.json +1 -1
  89. package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
  90. package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
  91. package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
  92. package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
  93. package/plugins/lisa-openclaw-agy/plugin.json +1 -1
  94. package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
  95. package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
  96. package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
  97. package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
  98. package/plugins/lisa-phaser-agy/plugin.json +1 -1
  99. package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
  100. package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
  101. package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
  102. package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
  103. package/plugins/lisa-rails-agy/plugin.json +1 -1
  104. package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
  105. package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
  106. package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
  107. package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
  108. package/plugins/lisa-typescript-agy/plugin.json +1 -1
  109. package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
  110. package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
  111. package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
  112. package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
  113. package/plugins/lisa-wiki-agy/plugin.json +1 -1
  114. package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
  115. package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
  116. package/ui/README.md +5 -3
  117. package/ui/index.html +1284 -457
package/ui/index.html CHANGED
@@ -326,6 +326,29 @@
326
326
  font-family: var(--mono);
327
327
  font-size: 11px;
328
328
  }
329
+ .plugins-state {
330
+ padding: 18px 20px;
331
+ border: 1px dashed var(--line);
332
+ border-radius: 10px;
333
+ background: var(--surface2);
334
+ color: var(--muted);
335
+ font-size: 13px;
336
+ line-height: 1.45;
337
+ }
338
+ .plugins-state.unknown {
339
+ border-style: solid;
340
+ border-color: var(--warn, #c9872a);
341
+ }
342
+ .plugins-state.unknown b {
343
+ display: block;
344
+ color: var(--text);
345
+ margin-bottom: 4px;
346
+ }
347
+ .plugins-live-block .plugin-status-badge.available {
348
+ background: transparent;
349
+ border: 1px solid var(--line);
350
+ color: var(--muted);
351
+ }
329
352
  .live-status-item .status-why {
330
353
  display: inline;
331
354
  margin: 0;
@@ -854,6 +877,23 @@
854
877
  grid-template-columns: 1fr;
855
878
  }
856
879
  }
880
+ .stacks-state {
881
+ border: 1px dashed var(--line);
882
+ border-radius: 10px;
883
+ background: var(--surface);
884
+ padding: 16px 18px;
885
+ color: var(--muted);
886
+ display: flex;
887
+ flex-direction: column;
888
+ gap: 4px;
889
+ }
890
+ .stacks-state.unknown {
891
+ border-color: var(--warn, #c99a2e);
892
+ border-style: solid;
893
+ }
894
+ .stacks-state.unknown b {
895
+ color: var(--ink);
896
+ }
857
897
  .stack-card {
858
898
  border: 1px solid var(--line);
859
899
  border-radius: 10px;
@@ -3134,77 +3174,22 @@
3134
3174
  },
3135
3175
  {
3136
3176
  type: "table",
3177
+ id: "deploy-pipeline-stages",
3137
3178
  card: "Deploy pipeline stages",
3138
- note: "Runs when an environment branch (dev / staging / main) receives a merge",
3179
+ note: "Live from deploy.yml + GitHub environment protection rules never fabricated",
3139
3180
  cols: ["Stage", "What it does", "Active"],
3140
3181
  rows: [
3141
3182
  [
3142
- { t: "mono", v: "🏗️ Build & package" },
3143
- {
3144
- t: "text",
3145
- v: "Builds the artifacts for the environment mapped to the pushed branch",
3146
- },
3147
- { t: "status", v: true },
3148
- ],
3149
- [
3150
- { t: "mono", v: "🚦 Release approval — production" },
3151
- {
3152
- t: "text",
3153
- v: "Pauses at the release_approval job until a configured reviewer approves the GitHub environment",
3154
- },
3155
- { t: "status", v: true },
3156
- ],
3157
- [
3158
- { t: "mono", v: "🚦 Release approval — staging" },
3183
+ { t: "mono", v: "" },
3159
3184
  {
3160
3185
  t: "text",
3161
- v: "Approval hold before staging deploys",
3186
+ v: "Loading live deploy pipeline stages…",
3162
3187
  },
3163
3188
  {
3164
3189
  t: "status",
3165
- v: false,
3166
- reason: "require_approval is off for staging",
3167
- },
3168
- ],
3169
- [
3170
- { t: "mono", v: "🚀 Deploy" },
3171
- {
3172
- t: "text",
3173
- v: "Runs the stack’s deploy (EAS / Serverless / CDK / Fabric) against the target environment",
3174
- },
3175
- { t: "status", v: true },
3176
- ],
3177
- [
3178
- { t: "mono", v: "📝 Version bump & changelog" },
3179
- {
3180
- t: "text",
3181
- v: "standard-version tags the release and pushes the chore(release) commit through the deploy key’s ruleset bypass",
3182
- },
3183
- { t: "status", v: true },
3184
- ],
3185
- [
3186
- { t: "mono", v: "🔄 Sync-down branches" },
3187
- {
3188
- t: "text",
3189
- v: "After a promotion merges, back-merges higher environment branches into lower ones",
3190
- },
3191
- { t: "status", v: true },
3192
- ],
3193
- [
3194
- { t: "mono", v: "🧯 Deploy auto-fix" },
3195
- {
3196
- t: "text",
3197
- v: "On a failed deploy, a Claude workflow investigates and opens a fix PR",
3198
- },
3199
- { t: "status", v: true },
3200
- ],
3201
- [
3202
- { t: "mono", v: "🎫 Issue on failure" },
3203
- {
3204
- t: "text",
3205
- v: "Files a tracker issue when a deploy run fails",
3190
+ v: "unknown",
3191
+ reason: "waiting for deploy-pipeline-stages probe",
3206
3192
  },
3207
- { t: "status", v: true },
3208
3193
  ],
3209
3194
  ],
3210
3195
  },
@@ -3392,183 +3377,14 @@
3392
3377
  {
3393
3378
  type: "table",
3394
3379
  card: "Scheduled jobs",
3395
- note: "Status and next run are mocked \u2014 live wiring is LCW-1 (#1544). Runs on = which scheduler fires the job; the nightly agents are pipeline files, inventoried under Pipelines.",
3396
- cols: [
3397
- "Automation",
3398
- "What it does",
3399
- "Runs on",
3400
- "Cadence",
3401
- "Last run",
3402
- "Next run",
3403
- "Enabled",
3404
- ],
3405
- rows: [
3406
- [
3407
- { t: "mono", v: "intake-tickets" },
3408
- {
3409
- t: "text",
3410
- v: "Claims the next build-ready ticket and runs the full build flow on it \u2014 one item per cycle",
3411
- },
3412
- { t: "badge", v: "harness", cls: "default" },
3413
- {
3414
- t: "select",
3415
- v: "every 10 min",
3416
- options: ["every 10 min", "every 30 min", "every 60 min"],
3417
- },
3418
- {
3419
- t: "run",
3420
- ok: true,
3421
- v: "12:50",
3422
- reason: "ok \u2014 queue empty, exited clean",
3423
- },
3424
- { t: "mono", v: "13:00" },
3425
- { t: "toggle", v: true },
3426
- ],
3427
- [
3428
- { t: "mono", v: "intake-prd" },
3429
- {
3430
- t: "text",
3431
- v: "Validates the next ready PRD at the gate and either creates its work units or blocks it with clarifying questions",
3432
- },
3433
- { t: "badge", v: "harness", cls: "default" },
3434
- {
3435
- t: "select",
3436
- v: "every 60 min",
3437
- options: ["every 10 min", "every 30 min", "every 60 min"],
3438
- },
3439
- {
3440
- t: "run",
3441
- ok: true,
3442
- v: "12:04",
3443
- reason: "ok \u2014 1 PRD ticketed",
3444
- },
3445
- { t: "mono", v: "13:04" },
3446
- { t: "toggle", v: true },
3447
- ],
3448
- [
3449
- { t: "mono", v: "intake-repair" },
3450
- {
3451
- t: "text",
3452
- v: "Finds stuck work \u2014 stalled builds, half-closed items, unreconciled rollups \u2014 and repairs each one",
3453
- },
3454
- { t: "badge", v: "harness", cls: "default" },
3455
- {
3456
- t: "select",
3457
- v: "every 60 min",
3458
- options: ["every 30 min", "every 60 min", "every 3 h"],
3459
- },
3460
- {
3461
- t: "run",
3462
- ok: true,
3463
- v: "12:04",
3464
- reason: "ok \u2014 0 stuck items",
3465
- },
3466
- { t: "mono", v: "13:04" },
3467
- { t: "toggle", v: true },
3468
- ],
3469
- [
3470
- { t: "mono", v: "exploratory-bugs" },
3471
- {
3472
- t: "text",
3473
- v: "QA loop: explores the product like a first-time user (respecting the mutation policy) and files bugs build-ready",
3474
- },
3475
- { t: "badge", v: "harness", cls: "default" },
3476
- { t: "select", v: "daily", options: ["daily", "weekly"] },
3477
- {
3478
- t: "run",
3479
- ok: true,
3480
- v: "04:00",
3481
- reason: "ok \u2014 2 bugs filed",
3482
- },
3483
- { t: "mono", v: "tomorrow 04:00" },
3484
- { t: "toggle", v: true },
3485
- ],
3486
- [
3487
- { t: "mono", v: "exploratory-prds" },
3488
- {
3489
- t: "text",
3490
- v: "Product Planning loop: ideates improvements and writes PRDs prd-ready",
3491
- },
3492
- { t: "badge", v: "harness", cls: "default" },
3493
- { t: "select", v: "daily", options: ["daily", "weekly"] },
3494
- {
3495
- t: "run",
3496
- ok: "warn",
3497
- v: "04:15",
3498
- reason: "skipped \u2014 queue pressure (1 prd-ready waiting)",
3499
- },
3500
- { t: "mono", v: "tomorrow 04:15" },
3501
- { t: "toggle", v: true },
3502
- ],
3503
- [
3504
- { t: "mono", v: "monitor" },
3505
- {
3506
- t: "text",
3507
- v: "Monitoring loop: audits the connected observability providers against the alert thresholds and files regression tickets",
3508
- },
3509
- { t: "badge", v: "harness", cls: "default" },
3510
- { t: "select", v: "daily", options: ["daily", "weekly"] },
3511
- {
3512
- t: "run",
3513
- ok: true,
3514
- v: "05:00",
3515
- reason: "ok \u2014 1 regression filed",
3516
- },
3517
- { t: "mono", v: "tomorrow 05:00" },
3518
- { t: "toggle", v: true },
3519
- ],
3520
- [
3521
- { t: "mono", v: "nightly-test-coverage" },
3522
- {
3523
- t: "text",
3524
- v: "Raises test coverage toward the thresholds and opens a PR \u2014 one module per run",
3525
- },
3526
- { t: "badge", v: "pipeline host", cls: "default" },
3527
- { t: "mono", v: "03:00 Mon\u2013Fri" },
3528
- {
3529
- t: "run",
3530
- ok: true,
3531
- v: "03:00",
3532
- reason: "ok \u2014 PR opened",
3533
- },
3534
- { t: "mono", v: "tomorrow 03:00" },
3535
- { t: "toggle", v: true },
3536
- ],
3537
- [
3538
- { t: "mono", v: "nightly-test-improvement" },
3539
- {
3540
- t: "text",
3541
- v: "Strengthens weak assertions in existing tests and opens a PR",
3542
- },
3543
- { t: "badge", v: "pipeline host", cls: "default" },
3544
- { t: "mono", v: "04:00 Mon\u2013Fri" },
3545
- {
3546
- t: "run",
3547
- ok: true,
3548
- v: "04:00",
3549
- reason: "ok \u2014 PR opened",
3550
- },
3551
- { t: "mono", v: "tomorrow 04:00" },
3552
- { t: "toggle", v: true },
3553
- ],
3554
- [
3555
- { t: "mono", v: "nightly-code-complexity" },
3556
- {
3557
- t: "text",
3558
- v: "Refactors a complexity hotspot without changing behavior and opens a PR",
3559
- },
3560
- { t: "badge", v: "pipeline host", cls: "default" },
3561
- { t: "mono", v: "05:00 Mon\u2013Fri" },
3562
- {
3563
- t: "run",
3564
- ok: true,
3565
- v: "05:00",
3566
- reason: "ok \u2014 no hotspot over budget",
3567
- },
3568
- { t: "mono", v: "tomorrow 05:00" },
3569
- { t: "toggle", v: true },
3570
- ],
3571
- ],
3380
+ id: "automations-scheduled-jobs",
3381
+ note: "Live from the harness scheduler (Codex automations or Claude /schedule). Only lisa-auto-<project>-* jobs are listed. Cadence is read-only in this phase \u2014 change jobs with /lisa:setup-automations. Nightly agents are pipeline files, inventoried under Pipelines.",
3382
+ cols: ["Automation", "Runs on", "Cadence", "Last run", "Status"],
3383
+ rows: [],
3384
+ empty: {
3385
+ state: "loading",
3386
+ message: "Loading harness scheduler\u2026",
3387
+ },
3572
3388
  },
3573
3389
  {
3574
3390
  card: "Fleet controls",
@@ -3672,7 +3488,7 @@
3672
3488
  t: "text",
3673
3489
  v: "Error events, issue volume, release health",
3674
3490
  },
3675
- { t: "status", v: true },
3491
+ { t: "status", v: true, provider: "sentry" },
3676
3492
  ],
3677
3493
  [
3678
3494
  { t: "mono", v: "AWS CloudWatch (Alarms)" },
@@ -3680,7 +3496,11 @@
3680
3496
  t: "text",
3681
3497
  v: "Alarm states, error-rate and latency metrics",
3682
3498
  },
3683
- { t: "status", v: true },
3499
+ {
3500
+ t: "status",
3501
+ v: true,
3502
+ provider: "cloudwatch-alarms",
3503
+ },
3684
3504
  ],
3685
3505
  [
3686
3506
  { t: "mono", v: "AWS X-Ray" },
@@ -3688,6 +3508,7 @@
3688
3508
  {
3689
3509
  t: "status",
3690
3510
  v: false,
3511
+ provider: "x-ray",
3691
3512
  reason: "no traces found — X-Ray is not instrumented",
3692
3513
  },
3693
3514
  ],
@@ -4454,7 +4275,12 @@
4454
4275
  t: "text",
4455
4276
  v: "oxlint then ESLint over the whole repo — the fast rule set",
4456
4277
  },
4457
- { t: "status", v: true },
4278
+ {
4279
+ t: "status",
4280
+ jobId: "lint",
4281
+ v: null,
4282
+ reason: "checking…",
4283
+ },
4458
4284
  ],
4459
4285
  [
4460
4286
  { t: "mono", v: "🐢 Slow Lint Rules" },
@@ -4462,22 +4288,42 @@
4462
4288
  t: "text",
4463
4289
  v: "The expensive rules deferred from local lint (import cycles, namespace checks)",
4464
4290
  },
4465
- { t: "status", v: true },
4291
+ {
4292
+ t: "status",
4293
+ jobId: "lint_slow",
4294
+ v: null,
4295
+ reason: "checking…",
4296
+ },
4466
4297
  ],
4467
4298
  [
4468
4299
  { t: "mono", v: "🔍 Type Check" },
4469
4300
  { t: "text", v: "tsc --noEmit across the project" },
4470
- { t: "status", v: true },
4301
+ {
4302
+ t: "status",
4303
+ jobId: "typecheck",
4304
+ v: null,
4305
+ reason: "checking…",
4306
+ },
4471
4307
  ],
4472
4308
  [
4473
4309
  { t: "mono", v: "📐 Check Formatting" },
4474
4310
  { t: "text", v: "Prettier in check mode — no writes" },
4475
- { t: "status", v: true },
4311
+ {
4312
+ t: "status",
4313
+ jobId: "format",
4314
+ v: null,
4315
+ reason: "checking…",
4316
+ },
4476
4317
  ],
4477
4318
  [
4478
4319
  { t: "mono", v: "🏗️ Build" },
4479
4320
  { t: "text", v: "Compiles the project to catch build breaks" },
4480
- { t: "status", v: true },
4321
+ {
4322
+ t: "status",
4323
+ jobId: "build",
4324
+ v: null,
4325
+ reason: "checking…",
4326
+ },
4481
4327
  ],
4482
4328
  [
4483
4329
  { t: "mono", v: "🧪 Run Unit Tests" },
@@ -4485,12 +4331,22 @@
4485
4331
  t: "text",
4486
4332
  v: "Unit suite with the coverage floors from Testing & coverage enforced",
4487
4333
  },
4488
- { t: "status", v: true },
4334
+ {
4335
+ t: "status",
4336
+ jobId: "test:unit",
4337
+ v: null,
4338
+ reason: "checking…",
4339
+ },
4489
4340
  ],
4490
4341
  [
4491
4342
  { t: "mono", v: "🧪 Run Integration Tests" },
4492
4343
  { t: "text", v: "Integration suite (30-minute timeout)" },
4493
- { t: "status", v: true },
4344
+ {
4345
+ t: "status",
4346
+ jobId: "test:integration",
4347
+ v: null,
4348
+ reason: "checking…",
4349
+ },
4494
4350
  ],
4495
4351
  [
4496
4352
  { t: "mono", v: "🧪 Run E2E Tests" },
@@ -4498,7 +4354,12 @@
4498
4354
  t: "text",
4499
4355
  v: "Playwright end-to-end tests, auto-detected from playwright.config + a test:e2e script",
4500
4356
  },
4501
- { t: "status", v: true },
4357
+ {
4358
+ t: "status",
4359
+ jobId: "test:e2e",
4360
+ v: null,
4361
+ reason: "checking…",
4362
+ },
4502
4363
  ],
4503
4364
  [
4504
4365
  { t: "mono", v: "🎭 Playwright E2E Tests" },
@@ -4506,7 +4367,12 @@
4506
4367
  t: "text",
4507
4368
  v: "Web-export e2e run — a required check on staging via ruleset",
4508
4369
  },
4509
- { t: "status", v: true },
4370
+ {
4371
+ t: "status",
4372
+ jobId: "playwright_e2e",
4373
+ v: null,
4374
+ reason: "checking…",
4375
+ },
4510
4376
  ],
4511
4377
  [
4512
4378
  { t: "mono", v: "📱 Maestro Cloud E2E" },
@@ -4514,7 +4380,12 @@
4514
4380
  t: "text",
4515
4381
  v: "Mobile end-to-end flows on Maestro Cloud",
4516
4382
  },
4517
- { t: "status", v: true },
4383
+ {
4384
+ t: "status",
4385
+ jobId: "maestro_e2e",
4386
+ v: null,
4387
+ reason: "checking…",
4388
+ },
4518
4389
  ],
4519
4390
  [
4520
4391
  { t: "mono", v: "🧭 E2E Route Coverage" },
@@ -4522,7 +4393,12 @@
4522
4393
  t: "text",
4523
4394
  v: "Static gate: % of expo-router routes reached by Playwright specs and Maestro flows, against the floors in e2e.thresholds.json — auto-enabled on Expo projects, a no-op elsewhere",
4524
4395
  },
4525
- { t: "status", v: true },
4396
+ {
4397
+ t: "status",
4398
+ jobId: "e2e_coverage",
4399
+ v: null,
4400
+ reason: "checking…",
4401
+ },
4526
4402
  ],
4527
4403
  [
4528
4404
  { t: "mono", v: "🧬 Mutation Testing Gate" },
@@ -4532,9 +4408,9 @@
4532
4408
  },
4533
4409
  {
4534
4410
  t: "status",
4535
- v: false,
4536
- reason:
4537
- "mutation gate is disabled (quality.mutation.gate.enabled)",
4411
+ jobId: "test:mutation",
4412
+ v: null,
4413
+ reason: "checking…",
4538
4414
  },
4539
4415
  ],
4540
4416
  [
@@ -4543,7 +4419,12 @@
4543
4419
  t: "text",
4544
4420
  v: "Requires feat/fix PRs to add or extend an e2e verification spec",
4545
4421
  },
4546
- { t: "status", v: false, reason: "verify_enforced is off" },
4422
+ {
4423
+ t: "status",
4424
+ jobId: "verification_coverage",
4425
+ v: null,
4426
+ reason: "checking…",
4427
+ },
4547
4428
  ],
4548
4429
  [
4549
4430
  { t: "mono", v: "🗑️ Dead Code Detection" },
@@ -4551,7 +4432,12 @@
4551
4432
  t: "text",
4552
4433
  v: "knip — unused files, exports, and dependencies",
4553
4434
  },
4554
- { t: "status", v: true },
4435
+ {
4436
+ t: "status",
4437
+ jobId: "dead_code",
4438
+ v: null,
4439
+ reason: "checking…",
4440
+ },
4555
4441
  ],
4556
4442
  [
4557
4443
  { t: "mono", v: "🔎 AST Grep Scan" },
@@ -4559,7 +4445,12 @@
4559
4445
  t: "text",
4560
4446
  v: "Structural code rules from sgconfig.yml",
4561
4447
  },
4562
- { t: "status", v: true },
4448
+ {
4449
+ t: "status",
4450
+ jobId: "sg_scan",
4451
+ v: null,
4452
+ reason: "checking…",
4453
+ },
4563
4454
  ],
4564
4455
  [
4565
4456
  { t: "mono", v: "🔒 Security Scan" },
@@ -4567,20 +4458,31 @@
4567
4458
  t: "text",
4568
4459
  v: "Dependency audit — high and critical advisories in production dependencies",
4569
4460
  },
4570
- { t: "status", v: true },
4461
+ {
4462
+ t: "status",
4463
+ jobId: "npm_security_scan",
4464
+ v: null,
4465
+ reason: "checking…",
4466
+ },
4571
4467
  ],
4572
4468
  [
4573
4469
  { t: "mono", v: "🔍 SonarCloud SAST" },
4574
4470
  { t: "text", v: "Static analysis via SonarCloud" },
4575
- { t: "status", v: true },
4471
+ {
4472
+ t: "status",
4473
+ jobId: "sonarcloud",
4474
+ v: null,
4475
+ reason: "checking…",
4476
+ },
4576
4477
  ],
4577
4478
  [
4578
4479
  { t: "mono", v: "🛡️ Snyk" },
4579
4480
  { t: "text", v: "Snyk dependency vulnerability scan" },
4580
4481
  {
4581
4482
  t: "status",
4582
- v: false,
4583
- reason: "SNYK_TOKEN secret is not set",
4483
+ jobId: "snyk",
4484
+ v: null,
4485
+ reason: "checking…",
4584
4486
  },
4585
4487
  ],
4586
4488
  [
@@ -4589,15 +4491,21 @@
4589
4491
  t: "text",
4590
4492
  v: "Secret scanning — also a required check via the base ruleset",
4591
4493
  },
4592
- { t: "status", v: true },
4494
+ {
4495
+ t: "status",
4496
+ jobId: "secret_scanning",
4497
+ v: null,
4498
+ reason: "checking…",
4499
+ },
4593
4500
  ],
4594
4501
  [
4595
4502
  { t: "mono", v: "📜 FOSSA" },
4596
4503
  { t: "text", v: "License compliance check" },
4597
4504
  {
4598
4505
  t: "status",
4599
- v: false,
4600
- reason: "FOSSA_API_KEY secret is not set",
4506
+ jobId: "license_compliance",
4507
+ v: null,
4508
+ reason: "checking…",
4601
4509
  },
4602
4510
  ],
4603
4511
  [
@@ -4606,7 +4514,12 @@
4606
4514
  t: "text",
4607
4515
  v: "DAST baseline scan against zap_target_url with per-rule IGNORE/WARN/FAIL decisions",
4608
4516
  },
4609
- { t: "status", v: true },
4517
+ {
4518
+ t: "status",
4519
+ jobId: "zap_baseline",
4520
+ v: null,
4521
+ reason: "checking…",
4522
+ },
4610
4523
  ],
4611
4524
  [
4612
4525
  { t: "mono", v: "🧾 Compliance Validation" },
@@ -4616,8 +4529,9 @@
4616
4529
  },
4617
4530
  {
4618
4531
  t: "status",
4619
- v: false,
4620
- reason: "no compliance_framework configured",
4532
+ jobId: "compliance_validation",
4533
+ v: null,
4534
+ reason: "checking…",
4621
4535
  },
4622
4536
  ],
4623
4537
  [
@@ -4626,7 +4540,12 @@
4626
4540
  t: "text",
4627
4541
  v: "Pauses the run until the target GitHub environment’s reviewers approve",
4628
4542
  },
4629
- { t: "status", v: true },
4543
+ {
4544
+ t: "status",
4545
+ jobId: "approval_gate",
4546
+ v: null,
4547
+ reason: "checking…",
4548
+ },
4630
4549
  ],
4631
4550
  ],
4632
4551
  },
@@ -5015,175 +4934,83 @@
5015
4934
  };
5016
4935
 
5017
4936
  /* ---------------------------------- Plugins ---------------------------------- */
4937
+ /* Catalog copy only — enablement comes from the enabled-plugins probe.
4938
+ Demo toggles are never rendered as a fallback. */
5018
4939
  DATA.sections.plugins = {
5019
4940
  title: "Plugins & MCP",
5020
4941
  desc: "Lisa’s own plugin set (base + one per detected stack), curated third-party plugins, and the MCP servers each one carries. Non-Claude agents receive generated equivalents of everything here.",
5021
4942
  src: ".claude/settings.json enabledPlugins · plugins/src/",
5022
4943
  blocks: [
4944
+ {
4945
+ type: "plugins-live",
4946
+ lisaCatalog: {
4947
+ "lisa@lisa":
4948
+ "Base governance: ~145 skills, 26 agents, 12 hooks, /lisa:* commands, rules, Sentry MCP",
4949
+ "lisa-typescript@lisa":
4950
+ "format-on-edit, lint-on-edit, ast-grep-on-edit, suppress-directive blocking",
4951
+ "lisa-expo@lisa":
4952
+ "~40 skills (EAS, expo-router, gluestack), ops-specialist agent, Expo MCP",
4953
+ "lisa-nestjs@lisa":
4954
+ "GraphQL/TypeORM skills, migration-edit blocking",
4955
+ "lisa-phaser@lisa": "8 skills, 20 game-dev agents, Phaser rules",
4956
+ "lisa-rails@lisa":
4957
+ "20 skills, rubocop-on-edit, Rails conventions",
4958
+ "lisa-harper-fabric@lisa":
4959
+ "12 skills, generated-artifact protection",
4960
+ "lisa-wiki@lisa":
4961
+ "In-repo LLM wiki: 22 skills, ingestion connectors (default-on for phaser only)",
4962
+ "lisa-openclaw@lisa":
4963
+ "Telegram/Slack routing via OpenClaw (opt-in)",
4964
+ "lisa-cdk@lisa": "AWS CDK plugin",
4965
+ },
4966
+ thirdPartyCatalog: {
4967
+ "code-review@claude-plugins-official":
4968
+ "Pre-ship diff defect hunt",
4969
+ "code-simplifier@claude-plugins-official":
4970
+ "Behavior-preserving cleanup",
4971
+ "coderabbit@claude-plugins-official":
4972
+ "AI PR review (also a required check)",
4973
+ "sentry@claude-plugins-official":
4974
+ "Issue debugging, Seer, instrumentation",
4975
+ "safety-net@cc-marketplace": "Guardrail rulebooks",
4976
+ "typescript-lsp@claude-plugins-official":
4977
+ "TypeScript language server",
4978
+ "playwright@claude-plugins-official": "Browser automation MCP",
4979
+ "posthog@claude-plugins-official": "Product analytics",
4980
+ "impeccable@impeccable": "UI design polish",
4981
+ "skill-creator@claude-plugins-official":
4982
+ "Author and optimize skills",
4983
+ "ruby-lsp@claude-plugins-official":
4984
+ "Ruby language server (rails stacks)",
4985
+ "atlassian@claude-plugins-official": "Jira / Confluence access",
4986
+ "hookify@claude-plugins-official": "Custom hook authoring",
4987
+ },
4988
+ },
5023
4989
  {
5024
4990
  type: "table",
5025
- card: "Lisa plugins",
5026
- cols: ["Plugin", "Bundles", "Enabled"],
4991
+ card: "MCP servers",
4992
+ note: "Catalog of MCP servers commonly carried by plugins — enablement follows the hosting plugin (read-only in this phase).",
4993
+ cols: ["Server", "Transport", "Enablement"],
5027
4994
  rows: [
5028
4995
  [
5029
- { t: "mono", v: "lisa@lisa" },
5030
- {
5031
- t: "text",
5032
- v: "Base governance: ~145 skills, 26 agents, 12 hooks, /lisa:* commands, rules, Sentry MCP",
5033
- },
5034
- { t: "toggle", v: true },
5035
- ],
5036
- [
5037
- { t: "mono", v: "lisa-typescript@lisa" },
5038
- {
5039
- t: "text",
5040
- v: "format-on-edit, lint-on-edit, ast-grep-on-edit, suppress-directive blocking",
5041
- },
5042
- { t: "toggle", v: true },
5043
- ],
5044
- [
5045
- { t: "mono", v: "lisa-expo@lisa" },
5046
- {
5047
- t: "text",
5048
- v: "~40 skills (EAS, expo-router, gluestack), ops-specialist agent, Expo MCP",
5049
- },
5050
- { t: "toggle", v: true },
5051
- ],
5052
- [
5053
- { t: "mono", v: "lisa-nestjs@lisa" },
5054
- {
5055
- t: "text",
5056
- v: "GraphQL/TypeORM skills, migration-edit blocking",
5057
- },
5058
- { t: "toggle", v: false },
5059
- ],
5060
- [
5061
- { t: "mono", v: "lisa-phaser@lisa" },
5062
- { t: "text", v: "8 skills, 20 game-dev agents, Phaser rules" },
5063
- { t: "toggle", v: false },
4996
+ { t: "mono", v: "sentry" },
4997
+ { t: "mono", v: "http" },
4998
+ { t: "text", v: "always-on via base plugin" },
5064
4999
  ],
5065
5000
  [
5066
- { t: "mono", v: "lisa-rails@lisa" },
5067
- {
5068
- t: "text",
5069
- v: "20 skills, rubocop-on-edit, Rails conventions",
5070
- },
5071
- { t: "toggle", v: false },
5001
+ { t: "mono", v: "expo" },
5002
+ { t: "mono", v: "http" },
5003
+ { t: "text", v: "on when the expo stack is detected" },
5072
5004
  ],
5073
5005
  [
5074
- { t: "mono", v: "lisa-harper-fabric@lisa" },
5075
- { t: "text", v: "12 skills, generated-artifact protection" },
5076
- { t: "toggle", v: false },
5006
+ { t: "mono", v: "linear-server" },
5007
+ { t: "mono", v: "http" },
5008
+ { t: "text", v: "project-level .mcp.json" },
5077
5009
  ],
5078
5010
  [
5079
- { t: "mono", v: "lisa-wiki@lisa" },
5080
- {
5081
- t: "text",
5082
- v: "In-repo LLM wiki: 22 skills, ingestion connectors (default-on for phaser only)",
5083
- },
5084
- { t: "toggle", v: false },
5085
- ],
5086
- [
5087
- { t: "mono", v: "lisa-openclaw@lisa" },
5088
- {
5089
- t: "text",
5090
- v: "Telegram/Slack routing via OpenClaw (opt-in)",
5091
- },
5092
- { t: "toggle", v: false },
5093
- ],
5094
- ],
5095
- },
5096
- {
5097
- type: "table",
5098
- card: "Curated third-party plugins",
5099
- note: "Flip an entry to enable or disable; Lisa never touches third-party keys on re-apply",
5100
- cols: ["Plugin", "Purpose", "Enabled"],
5101
- rows: [
5102
- [
5103
- { t: "mono", v: "code-review@claude-plugins-official" },
5104
- { t: "text", v: "Pre-ship diff defect hunt" },
5105
- { t: "toggle", v: true },
5106
- ],
5107
- [
5108
- { t: "mono", v: "code-simplifier@claude-plugins-official" },
5109
- { t: "text", v: "Behavior-preserving cleanup" },
5110
- { t: "toggle", v: true },
5111
- ],
5112
- [
5113
- { t: "mono", v: "coderabbit@claude-plugins-official" },
5114
- { t: "text", v: "AI PR review (also a required check)" },
5115
- { t: "toggle", v: true },
5116
- ],
5117
- [
5118
- { t: "mono", v: "sentry@claude-plugins-official" },
5119
- { t: "text", v: "Issue debugging, Seer, instrumentation" },
5120
- { t: "toggle", v: true },
5121
- ],
5122
- [
5123
- { t: "mono", v: "safety-net@cc-marketplace" },
5124
- { t: "text", v: "Guardrail rulebooks" },
5125
- { t: "toggle", v: false },
5126
- ],
5127
- [
5128
- { t: "mono", v: "typescript-lsp@claude-plugins-official" },
5129
- { t: "text", v: "TypeScript language server" },
5130
- { t: "toggle", v: true },
5131
- ],
5132
- [
5133
- { t: "mono", v: "playwright@claude-plugins-official" },
5134
- { t: "text", v: "Browser automation MCP" },
5135
- { t: "toggle", v: true },
5136
- ],
5137
- [
5138
- { t: "mono", v: "posthog@claude-plugins-official" },
5139
- { t: "text", v: "Product analytics" },
5140
- { t: "toggle", v: true },
5141
- ],
5142
- [
5143
- { t: "mono", v: "impeccable@impeccable" },
5144
- { t: "text", v: "UI design polish" },
5145
- { t: "toggle", v: true },
5146
- ],
5147
- [
5148
- { t: "mono", v: "skill-creator@claude-plugins-official" },
5149
- { t: "text", v: "Author and optimize skills" },
5150
- { t: "toggle", v: false },
5151
- ],
5152
- [
5153
- { t: "mono", v: "ruby-lsp@claude-plugins-official" },
5154
- { t: "text", v: "Ruby language server (rails stacks)" },
5155
- { t: "toggle", v: false },
5156
- ],
5157
- ],
5158
- },
5159
- {
5160
- type: "table",
5161
- card: "MCP servers",
5162
- cols: ["Server", "Transport", "Enablement", "Enabled"],
5163
- rows: [
5164
- [
5165
- { t: "mono", v: "sentry" },
5166
- { t: "mono", v: "http" },
5167
- { t: "text", v: "always-on via base plugin" },
5168
- { t: "toggle", v: true },
5169
- ],
5170
- [
5171
- { t: "mono", v: "expo" },
5172
- { t: "mono", v: "http" },
5173
- { t: "text", v: "on when the expo stack is detected" },
5174
- { t: "toggle", v: true },
5175
- ],
5176
- [
5177
- { t: "mono", v: "linear-server" },
5178
- { t: "mono", v: "http" },
5179
- { t: "text", v: "project-level .mcp.json" },
5180
- { t: "toggle", v: false },
5181
- ],
5182
- [
5183
- { t: "mono", v: "phaser-editor" },
5184
- { t: "mono", v: "stdio" },
5185
- { t: "text", v: "shipped disabled (disabledMcpjsonServers)" },
5186
- { t: "toggle", v: false },
5011
+ { t: "mono", v: "phaser-editor" },
5012
+ { t: "mono", v: "stdio" },
5013
+ { t: "text", v: "shipped disabled (disabledMcpjsonServers)" },
5187
5014
  ],
5188
5015
  ],
5189
5016
  },
@@ -5293,6 +5120,18 @@
5293
5120
  const $toast = document.getElementById("toast");
5294
5121
  const dirty = new Set();
5295
5122
 
5123
+ /* Live enabled-plugins state, hydrated from GET /api/status. Starts as
5124
+ "loading" so the Plugins section never presents demo toggles as truth. */
5125
+ let pluginsLive = {
5126
+ status: "loading",
5127
+ settingsPresent: false,
5128
+ plugins: [],
5129
+ };
5130
+ /* Live detected-stacks state, hydrated from GET /api/status. Starts as
5131
+ "detecting" so the Stacks section never presents the metadata catalog
5132
+ as if it were the detection result. */
5133
+ let stackDetection = { status: "detecting", ids: [] };
5134
+
5296
5135
  function el(tag, cls, html) {
5297
5136
  const node = document.createElement(tag);
5298
5137
  if (cls) node.className = cls;
@@ -5501,14 +5340,88 @@
5501
5340
  return card;
5502
5341
  }
5503
5342
 
5343
+ function paintProviderStatus(td, cell) {
5344
+ td.innerHTML = "";
5345
+ if (cell.v === true || cell.v === "connected") {
5346
+ td.appendChild(el("span", "badge pass", "✓ connected"));
5347
+ return;
5348
+ }
5349
+ if (cell.v === "connected-but-empty") {
5350
+ td.appendChild(el("span", "badge warn", "◐ connected-but-empty"));
5351
+ if (cell.reason) {
5352
+ td.appendChild(el("span", "status-why", esc(cell.reason)));
5353
+ }
5354
+ return;
5355
+ }
5356
+ if (cell.v === "pending") {
5357
+ td.appendChild(el("span", "badge default", "checking…"));
5358
+ return;
5359
+ }
5360
+ if (cell.v === "unknown") {
5361
+ td.appendChild(el("span", "badge warn", "unknown"));
5362
+ if (cell.reason) {
5363
+ td.appendChild(el("span", "status-why", esc(cell.reason)));
5364
+ }
5365
+ return;
5366
+ }
5367
+ if (cell.v) {
5368
+ td.appendChild(el("span", "badge pass", "✓ active"));
5369
+ return;
5370
+ }
5371
+ td.appendChild(el("span", "badge fail", "✗ off"));
5372
+ if (cell.reason) {
5373
+ td.appendChild(el("span", "status-why", esc(cell.reason)));
5374
+ }
5375
+ }
5376
+ function buildStatusCell(cell) {
5377
+ const td = el("td");
5378
+ if (cell.provider) td.dataset.provider = cell.provider;
5379
+ if (cell.jobId) td.dataset.jobId = cell.jobId;
5380
+ if (cell.provider) {
5381
+ paintProviderStatus(td, cell);
5382
+ return td;
5383
+ }
5384
+ if (cell.v === "unknown" || cell.v === null || cell.v === undefined) {
5385
+ td.appendChild(el("span", "badge warn", "unknown"));
5386
+ if (cell.reason) {
5387
+ td.appendChild(el("span", "status-why", esc(cell.reason)));
5388
+ }
5389
+ return td;
5390
+ }
5391
+ if (cell.v) {
5392
+ td.appendChild(el("span", "badge pass", "✓ active"));
5393
+ return td;
5394
+ }
5395
+ td.appendChild(el("span", "badge fail", "✗ off"));
5396
+ if (cell.reason) {
5397
+ td.appendChild(el("span", "status-why", esc(cell.reason)));
5398
+ }
5399
+ return td;
5400
+ }
5401
+
5504
5402
  function buildTable(block) {
5505
5403
  const card = el("div", "card");
5404
+ if (block.id) {
5405
+ card.id = block.id;
5406
+ card.dataset.testid = block.id;
5407
+ }
5506
5408
  if (block.card) {
5507
5409
  const head = el("div", "card-head");
5508
5410
  head.appendChild(el("h2", "", esc(block.card)));
5509
5411
  if (block.note) head.appendChild(el("span", "note", esc(block.note)));
5510
5412
  card.appendChild(head);
5511
5413
  }
5414
+ if (block.empty && (!block.rows || block.rows.length === 0)) {
5415
+ const empty = el(
5416
+ "div",
5417
+ "status-why automations-empty",
5418
+ esc(block.empty.message || "No rows")
5419
+ );
5420
+ empty.dataset.testid = (block.id || "table") + "-empty";
5421
+ if (block.empty.state) empty.dataset.state = block.empty.state;
5422
+ card.appendChild(empty);
5423
+ return card;
5424
+ }
5512
5425
  const scroll = el("div", "table-scroll");
5513
5426
  const table = el("table", "grid");
5514
5427
  const thead = el("thead");
@@ -5529,7 +5442,14 @@
5529
5442
  const tbody = el("tbody");
5530
5443
  block.rows.forEach(cells => {
5531
5444
  const tr = el("tr");
5445
+ if (cells && cells._automationId) {
5446
+ tr.dataset.automationId = cells._automationId;
5447
+ }
5532
5448
  cells.forEach(cell => {
5449
+ if (cell.t === "status") {
5450
+ tr.appendChild(buildStatusCell(cell));
5451
+ return;
5452
+ }
5533
5453
  const td = el("td");
5534
5454
  if (cell.t === "mono") td.className = "mono";
5535
5455
  if (cell.t === "text" || cell.t === "mono") {
@@ -5546,13 +5466,20 @@
5546
5466
  chip.appendChild(el("span", "", esc(cell.v)));
5547
5467
  td.appendChild(chip);
5548
5468
  } else if (cell.t === "status") {
5549
- if (cell.v) {
5469
+ if (cell.jobId) td.dataset.jobId = cell.jobId;
5470
+ if (cell.v === true) {
5550
5471
  td.appendChild(el("span", "badge pass", "✓ active"));
5551
- } else {
5472
+ } else if (cell.v === false) {
5552
5473
  td.appendChild(el("span", "badge fail", "✗ off"));
5553
5474
  if (cell.reason) {
5554
5475
  td.appendChild(el("span", "status-why", esc(cell.reason)));
5555
5476
  }
5477
+ } else {
5478
+ // null/undefined = unknown — never fabricate "off" or "active"
5479
+ td.appendChild(el("span", "badge warn", "unknown"));
5480
+ if (cell.reason) {
5481
+ td.appendChild(el("span", "status-why", esc(cell.reason)));
5482
+ }
5556
5483
  }
5557
5484
  } else if (cell.t === "run") {
5558
5485
  const cls =
@@ -5613,46 +5540,118 @@
5613
5540
  return card;
5614
5541
  }
5615
5542
 
5616
- function buildStacks(block) {
5617
- const grid = el("div", "stack-grid");
5543
+ /* The 8-entry stack metadata, keyed by id. This is a catalog of what each
5544
+ stack installs NOT the detection result; only ids the probe reports
5545
+ are rendered from it. */
5546
+ function stackCatalog(block) {
5547
+ const catalog = new Map();
5618
5548
  block.items.forEach(s => {
5619
- const card = el("div", "stack-card" + (s.on ? " on" : ""));
5620
- const head = el("div", "head");
5621
- head.appendChild(el("span", "glyph", esc(s.glyph)));
5622
- const t = el("div", "t");
5623
- t.appendChild(el("b", "", esc(s.name)));
5624
- t.appendChild(el("i", "", esc(s.tagline)));
5625
- head.appendChild(t);
5626
- const toggleWrap = buildControl(
5627
- { type: "toggle", value: s.on },
5628
- "stack." + s.id
5549
+ catalog.set(s.id, s);
5550
+ });
5551
+ return catalog;
5552
+ }
5553
+
5554
+ /* A detected id with no catalog entry still gets a card (id as name) so
5555
+ detection truth is never dropped just because copy is missing. */
5556
+ function minimalStackMeta(id) {
5557
+ return {
5558
+ id,
5559
+ glyph: id.slice(0, 2).toUpperCase(),
5560
+ name: id,
5561
+ tagline: "",
5562
+ detect: "",
5563
+ badges: [],
5564
+ more: [],
5565
+ };
5566
+ }
5567
+
5568
+ function stackCard(s) {
5569
+ const card = el("div", "stack-card on");
5570
+ const head = el("div", "head");
5571
+ head.appendChild(el("span", "glyph", esc(s.glyph)));
5572
+ const t = el("div", "t");
5573
+ t.appendChild(el("b", "", esc(s.name)));
5574
+ t.appendChild(el("i", "", esc(s.tagline)));
5575
+ head.appendChild(t);
5576
+ const toggleWrap = buildControl(
5577
+ { type: "toggle", value: true },
5578
+ "stack." + s.id
5579
+ );
5580
+ toggleWrap.querySelector("input").addEventListener("change", e => {
5581
+ card.classList.toggle("on", e.target.checked);
5582
+ markDirty("stack." + s.id, null);
5583
+ });
5584
+ head.appendChild(toggleWrap);
5585
+ card.appendChild(head);
5586
+ const meta = el("div", "meta");
5587
+ (s.badges || []).forEach(b =>
5588
+ meta.appendChild(el("span", "tag", esc(b)))
5589
+ );
5590
+ card.appendChild(meta);
5591
+ card.appendChild(el("div", "detect", s.detect || ""));
5592
+ if (s.more && s.more.length) {
5593
+ const det = el("details", "stack-more");
5594
+ det.appendChild(el("summary", "", "What this stack installs"));
5595
+ const ul = el("ul");
5596
+ s.more.forEach(m => ul.appendChild(el("li", "", esc(m))));
5597
+ det.appendChild(ul);
5598
+ card.appendChild(det);
5599
+ }
5600
+ return card;
5601
+ }
5602
+
5603
+ function renderStacksInto(wrap, block) {
5604
+ wrap.innerHTML = "";
5605
+ const d = stackDetection;
5606
+ if (d.status === "detecting") {
5607
+ wrap.appendChild(
5608
+ el("div", "stacks-state detecting", "Detecting project stacks…")
5629
5609
  );
5630
- toggleWrap.querySelector("input").addEventListener("change", e => {
5631
- card.classList.toggle("on", e.target.checked);
5632
- markDirty("stack." + s.id, null);
5633
- });
5634
- head.appendChild(toggleWrap);
5635
- card.appendChild(head);
5636
- const meta = el("div", "meta");
5637
- s.badges.forEach(b => meta.appendChild(el("span", "tag", esc(b))));
5638
- card.appendChild(meta);
5639
- card.appendChild(el("div", "detect", s.detect));
5640
- if (s.more && s.more.length) {
5641
- const det = el("details", "stack-more");
5642
- det.appendChild(el("summary", "", "What this stack installs"));
5643
- const ul = el("ul");
5644
- s.more.forEach(m => ul.appendChild(el("li", "", esc(m))));
5645
- det.appendChild(ul);
5646
- card.appendChild(det);
5647
- }
5648
- grid.appendChild(card);
5610
+ return;
5611
+ }
5612
+ if (d.status === "unknown") {
5613
+ const box = el("div", "stacks-state unknown");
5614
+ box.appendChild(el("b", "", "Stack detection unavailable"));
5615
+ box.appendChild(
5616
+ el("span", "status-why", esc(d.reason + ": " + d.message))
5617
+ );
5618
+ wrap.appendChild(box);
5619
+ return;
5620
+ }
5621
+ if (!d.ids || d.ids.length === 0) {
5622
+ wrap.appendChild(
5623
+ el(
5624
+ "div",
5625
+ "stacks-state empty",
5626
+ "No Lisa stack detected in this project."
5627
+ )
5628
+ );
5629
+ return;
5630
+ }
5631
+ const catalog = stackCatalog(block);
5632
+ const grid = el("div", "stack-grid");
5633
+ d.ids.forEach(id => {
5634
+ grid.appendChild(stackCard(catalog.get(id) || minimalStackMeta(id)));
5649
5635
  });
5650
- const wrap = el("div");
5651
5636
  wrap.appendChild(grid);
5652
5637
  wrap.appendChild(el("div", "", "&nbsp;"));
5638
+ }
5639
+
5640
+ function buildStacks(block) {
5641
+ const wrap = el("div", "stacks-block");
5642
+ renderStacksInto(wrap, block);
5653
5643
  return wrap;
5654
5644
  }
5655
5645
 
5646
+ /* Re-render the Stacks section in place after detection hydrates. */
5647
+ function refreshStacks() {
5648
+ const block = DATA.sections.stacks.blocks.find(
5649
+ b => b.type === "stacks"
5650
+ );
5651
+ const wrap = document.querySelector(".stacks-block");
5652
+ if (block && wrap) renderStacksInto(wrap, block);
5653
+ }
5654
+
5656
5655
  function buildHooks(block) {
5657
5656
  const grid = el("div", "hooks-grid");
5658
5657
  block.items.forEach(h => {
@@ -5792,6 +5791,131 @@
5792
5791
  return card;
5793
5792
  }
5794
5793
 
5794
+ function pluginDescription(id, probeDescription, catalog) {
5795
+ if (Object.hasOwn(catalog, id) && typeof catalog[id] === "string") {
5796
+ return catalog[id];
5797
+ }
5798
+ return typeof probeDescription === "string" ? probeDescription : "";
5799
+ }
5800
+
5801
+ function pluginTableRows(entries, catalog) {
5802
+ return entries.map(plugin => {
5803
+ const enabled = plugin.status === "enabled";
5804
+ return [
5805
+ { t: "mono", v: plugin.id },
5806
+ {
5807
+ t: "text",
5808
+ v: pluginDescription(plugin.id, plugin.description, catalog),
5809
+ },
5810
+ {
5811
+ t: "badge",
5812
+ v: enabled ? "enabled" : "available, not enabled",
5813
+ cls: enabled ? "on" : "plugin-status-badge available",
5814
+ },
5815
+ ];
5816
+ });
5817
+ }
5818
+
5819
+ function renderPluginsInto(wrap, block) {
5820
+ wrap.innerHTML = "";
5821
+ const state = pluginsLive;
5822
+ if (state.status === "loading") {
5823
+ wrap.appendChild(
5824
+ el("div", "plugins-state loading", "Loading plugin enablement…")
5825
+ );
5826
+ return;
5827
+ }
5828
+ if (state.status === "unknown") {
5829
+ const box = el("div", "plugins-state unknown");
5830
+ box.appendChild(el("b", "", "Plugin enablement unavailable"));
5831
+ box.appendChild(
5832
+ el(
5833
+ "span",
5834
+ "status-why",
5835
+ esc((state.reason || "unknown") + ": " + (state.message || ""))
5836
+ )
5837
+ );
5838
+ wrap.appendChild(box);
5839
+ return;
5840
+ }
5841
+ if (!state.settingsPresent) {
5842
+ wrap.appendChild(
5843
+ el(
5844
+ "div",
5845
+ "plugins-state empty",
5846
+ "No .claude/settings.json in this project — plugin enablement is not configured."
5847
+ )
5848
+ );
5849
+ return;
5850
+ }
5851
+ const plugins = Array.isArray(state.plugins) ? state.plugins : [];
5852
+ const lisaCatalog =
5853
+ block.lisaCatalog && typeof block.lisaCatalog === "object"
5854
+ ? block.lisaCatalog
5855
+ : {};
5856
+ const thirdPartyCatalog =
5857
+ block.thirdPartyCatalog && typeof block.thirdPartyCatalog === "object"
5858
+ ? block.thirdPartyCatalog
5859
+ : {};
5860
+ const lisa = [];
5861
+ const thirdParty = [];
5862
+ plugins.forEach(plugin => {
5863
+ if (!plugin || typeof plugin !== "object") return;
5864
+ const id = plugin.id;
5865
+ if (typeof id !== "string" || id.trim().length === 0) return;
5866
+ if (id.endsWith("@lisa") || Object.hasOwn(lisaCatalog, id)) {
5867
+ lisa.push(plugin);
5868
+ } else {
5869
+ thirdParty.push(plugin);
5870
+ }
5871
+ });
5872
+ if (lisa.length === 0 && thirdParty.length === 0) {
5873
+ wrap.appendChild(
5874
+ el(
5875
+ "div",
5876
+ "plugins-state empty",
5877
+ "No plugins enabled or available in the marketplace catalog."
5878
+ )
5879
+ );
5880
+ return;
5881
+ }
5882
+ if (lisa.length > 0) {
5883
+ wrap.appendChild(
5884
+ buildTable({
5885
+ type: "table",
5886
+ card: "Lisa plugins",
5887
+ cols: ["Plugin", "Bundles", "Status"],
5888
+ rows: pluginTableRows(lisa, lisaCatalog),
5889
+ })
5890
+ );
5891
+ }
5892
+ if (thirdParty.length > 0) {
5893
+ wrap.appendChild(
5894
+ buildTable({
5895
+ type: "table",
5896
+ card: "Third-party plugins",
5897
+ note: "Read-only: enablement comes from .claude/settings.json enabledPlugins",
5898
+ cols: ["Plugin", "Purpose", "Status"],
5899
+ rows: pluginTableRows(thirdParty, thirdPartyCatalog),
5900
+ })
5901
+ );
5902
+ }
5903
+ }
5904
+
5905
+ function buildPluginsLive(block) {
5906
+ const wrap = el("div", "plugins-live-block");
5907
+ renderPluginsInto(wrap, block);
5908
+ return wrap;
5909
+ }
5910
+
5911
+ function refreshPlugins() {
5912
+ const block = DATA.sections.plugins.blocks.find(
5913
+ b => b.type === "plugins-live"
5914
+ );
5915
+ const wrap = document.querySelector(".plugins-live-block");
5916
+ if (block && wrap) renderPluginsInto(wrap, block);
5917
+ }
5918
+
5795
5919
  function buildBlock(block) {
5796
5920
  if (block.type === "tiles") return buildTiles(block);
5797
5921
  if (block.type === "flow") return buildFlow(block);
@@ -5801,6 +5925,7 @@
5801
5925
  if (block.type === "table") return buildTable(block);
5802
5926
  if (block.type === "tabs") return buildTabs(block);
5803
5927
  if (block.type === "checklist") return buildChecklist(block);
5928
+ if (block.type === "plugins-live") return buildPluginsLive(block);
5804
5929
  return buildCard(block);
5805
5930
  }
5806
5931
 
@@ -5992,6 +6117,7 @@
5992
6117
  if (value !== undefined) hydrateControl(row.control, value);
5993
6118
  });
5994
6119
  });
6120
+ markObservabilityProvidersPending();
5995
6121
  }
5996
6122
  configureRemoteEnvironment(cfg);
5997
6123
  const repoEl = document.querySelector(".project-switch .repo");
@@ -6051,6 +6177,115 @@
6051
6177
  }
6052
6178
  return snapshot.probes;
6053
6179
  }
6180
+ function connectedProvidersBlock() {
6181
+ const blocks = DATA.sections.monitor && DATA.sections.monitor.blocks;
6182
+ if (!Array.isArray(blocks)) return null;
6183
+ return (
6184
+ blocks.find(
6185
+ block =>
6186
+ block &&
6187
+ block.type === "table" &&
6188
+ block.card === "Connected providers"
6189
+ ) || null
6190
+ );
6191
+ }
6192
+ function setProviderStatusCell(provider, cell) {
6193
+ const block = connectedProvidersBlock();
6194
+ if (!block || !Array.isArray(block.rows)) return;
6195
+ block.rows.forEach(row => {
6196
+ if (!Array.isArray(row)) return;
6197
+ row.forEach((entry, index) => {
6198
+ if (entry && entry.t === "status" && entry.provider === provider) {
6199
+ row[index] = cell;
6200
+ }
6201
+ });
6202
+ });
6203
+ const td = document.querySelector(
6204
+ 'td[data-provider="' + provider + '"]'
6205
+ );
6206
+ if (td) paintProviderStatus(td, cell);
6207
+ }
6208
+ function markObservabilityProvidersPending() {
6209
+ ["sentry", "cloudwatch-alarms", "x-ray"].forEach(provider => {
6210
+ setProviderStatusCell(provider, {
6211
+ t: "status",
6212
+ v: "pending",
6213
+ provider,
6214
+ });
6215
+ });
6216
+ }
6217
+ /**
6218
+ * Paint Connected providers from sentry / cloudwatch-alarms / x-ray probes.
6219
+ * unknown ≠ disconnected — never invent an unevidenced "off" claim.
6220
+ * @param probes - Live-status probe map from /api/status
6221
+ */
6222
+ function applyObservabilityProviders(probes) {
6223
+ const specs = [
6224
+ {
6225
+ id: "sentry",
6226
+ emptyReason: "reachable — no Sentry data observed",
6227
+ },
6228
+ {
6229
+ id: "cloudwatch-alarms",
6230
+ emptyReason: "connected — no alarms",
6231
+ },
6232
+ {
6233
+ id: "x-ray",
6234
+ emptyReason: "connected — no traces",
6235
+ },
6236
+ ];
6237
+ specs.forEach(spec => {
6238
+ const raw =
6239
+ probes && Object.hasOwn(probes, spec.id)
6240
+ ? probes[spec.id]
6241
+ : undefined;
6242
+ const result =
6243
+ raw === undefined
6244
+ ? {
6245
+ state: "unknown",
6246
+ reason: "missing-probe",
6247
+ message: "The " + spec.id + " probe was not reported",
6248
+ }
6249
+ : normalizeLiveStatusResult(raw);
6250
+ if (
6251
+ result.state === "value" &&
6252
+ result.value &&
6253
+ typeof result.value === "object" &&
6254
+ result.value.status === "connected"
6255
+ ) {
6256
+ setProviderStatusCell(spec.id, {
6257
+ t: "status",
6258
+ v: "connected",
6259
+ provider: spec.id,
6260
+ });
6261
+ return;
6262
+ }
6263
+ if (
6264
+ result.state === "value" &&
6265
+ result.value &&
6266
+ typeof result.value === "object" &&
6267
+ result.value.status === "connected-but-empty"
6268
+ ) {
6269
+ setProviderStatusCell(spec.id, {
6270
+ t: "status",
6271
+ v: "connected-but-empty",
6272
+ provider: spec.id,
6273
+ reason: spec.emptyReason,
6274
+ });
6275
+ return;
6276
+ }
6277
+ const reason =
6278
+ result.state === "unknown"
6279
+ ? result.reason + ": " + result.message
6280
+ : "invalid-value: provider probe returned an unusable value";
6281
+ setProviderStatusCell(spec.id, {
6282
+ t: "status",
6283
+ v: "unknown",
6284
+ provider: spec.id,
6285
+ reason,
6286
+ });
6287
+ });
6288
+ }
6054
6289
  function renderLiveStatuses(probes) {
6055
6290
  const list = document.getElementById("liveStatusList");
6056
6291
  list.innerHTML = "";
@@ -6085,6 +6320,101 @@
6085
6320
  list.appendChild(item);
6086
6321
  });
6087
6322
  }
6323
+ /**
6324
+ * Map the enabled-plugins probe into pluginsLive. A missing probe or an
6325
+ * unusable value becomes an explicit unknown state — never demo toggles.
6326
+ * @param probes - Live-status probe map from /api/status
6327
+ */
6328
+ function applyEnabledPlugins(probes) {
6329
+ const raw =
6330
+ probes && Object.hasOwn(probes, "enabled-plugins")
6331
+ ? probes["enabled-plugins"]
6332
+ : undefined;
6333
+ if (raw === undefined) {
6334
+ pluginsLive = {
6335
+ status: "unknown",
6336
+ settingsPresent: false,
6337
+ plugins: [],
6338
+ reason: "missing-probe",
6339
+ message: "The enabled-plugins probe was not reported",
6340
+ };
6341
+ refreshPlugins();
6342
+ return;
6343
+ }
6344
+ const result = normalizeLiveStatusResult(raw);
6345
+ if (
6346
+ result.state === "value" &&
6347
+ result.value &&
6348
+ typeof result.value === "object"
6349
+ ) {
6350
+ const settingsPresent = result.value.settingsPresent === true;
6351
+ const plugins = result.value.plugins;
6352
+ if (!Array.isArray(plugins)) {
6353
+ pluginsLive = {
6354
+ status: "unknown",
6355
+ settingsPresent: false,
6356
+ plugins: [],
6357
+ reason: "invalid-value",
6358
+ message: "enabled-plugins did not return a plugins array",
6359
+ };
6360
+ } else {
6361
+ const rows = [];
6362
+ let invalid = false;
6363
+ for (const entry of plugins) {
6364
+ if (
6365
+ !entry ||
6366
+ typeof entry !== "object" ||
6367
+ typeof entry.id !== "string" ||
6368
+ entry.id.trim().length === 0 ||
6369
+ (entry.status !== "enabled" && entry.status !== "available")
6370
+ ) {
6371
+ invalid = true;
6372
+ break;
6373
+ }
6374
+ rows.push({
6375
+ id: entry.id,
6376
+ status: entry.status,
6377
+ description:
6378
+ typeof entry.description === "string"
6379
+ ? entry.description
6380
+ : "",
6381
+ });
6382
+ }
6383
+ if (invalid) {
6384
+ pluginsLive = {
6385
+ status: "unknown",
6386
+ settingsPresent: false,
6387
+ plugins: [],
6388
+ reason: "invalid-value",
6389
+ message: "enabled-plugins returned an unusable plugin row",
6390
+ };
6391
+ } else {
6392
+ pluginsLive = {
6393
+ status: "value",
6394
+ settingsPresent,
6395
+ plugins: rows,
6396
+ };
6397
+ }
6398
+ }
6399
+ } else if (result.state === "not-applicable") {
6400
+ pluginsLive = {
6401
+ status: "unknown",
6402
+ settingsPresent: false,
6403
+ plugins: [],
6404
+ reason: "not-applicable",
6405
+ message: "enabled-plugins reported not applicable",
6406
+ };
6407
+ } else {
6408
+ pluginsLive = {
6409
+ status: "unknown",
6410
+ settingsPresent: false,
6411
+ plugins: [],
6412
+ reason: result.reason || "unknown",
6413
+ message: result.message || "Plugin enablement unavailable",
6414
+ };
6415
+ }
6416
+ refreshPlugins();
6417
+ }
6088
6418
  /**
6089
6419
  * Paint `#healthChip` from the lisa-version probe.
6090
6420
  * green=ok (up to date), amber=warn (behind), unknown≠ok.
@@ -6152,6 +6482,465 @@
6152
6482
  "@codyswann/lisa · unknown (invalid-value: lisa-version probe returned an unusable value)"
6153
6483
  );
6154
6484
  }
6485
+ /**
6486
+ * Paint the CI Quality jobs Active column from the ci-quality-jobs probe.
6487
+ * Never leaves a fabricated active/off value when the probe is unknown.
6488
+ * @param probes - Live-status probe map from /api/status
6489
+ */
6490
+ function applyCiQualityJobs(probes) {
6491
+ const table = (DATA.sections.ci.blocks || []).find(
6492
+ block => block.type === "table" && block.card === "Quality jobs"
6493
+ );
6494
+ if (!table) return;
6495
+ const result = normalizeLiveStatusResult(
6496
+ probes && Object.hasOwn(probes, "ci-quality-jobs")
6497
+ ? probes["ci-quality-jobs"]
6498
+ : {
6499
+ state: "unknown",
6500
+ reason: "missing-probe",
6501
+ message: "The ci-quality-jobs probe was not reported",
6502
+ }
6503
+ );
6504
+ const byId = new Map();
6505
+ if (
6506
+ result.state === "value" &&
6507
+ result.value &&
6508
+ typeof result.value === "object" &&
6509
+ Array.isArray(result.value.jobs)
6510
+ ) {
6511
+ result.value.jobs.forEach(job => {
6512
+ if (job && typeof job.id === "string") byId.set(job.id, job);
6513
+ });
6514
+ }
6515
+ table.rows.forEach(cells => {
6516
+ const status = cells.find(cell => cell.t === "status" && cell.jobId);
6517
+ if (!status) return;
6518
+ if (result.state !== "value") {
6519
+ status.v = null;
6520
+ status.reason =
6521
+ result.reason + (result.message ? ": " + result.message : "");
6522
+ return;
6523
+ }
6524
+ const job = byId.get(status.jobId);
6525
+ if (!job) {
6526
+ status.v = null;
6527
+ status.reason = "missing-job: probe omitted " + status.jobId;
6528
+ return;
6529
+ }
6530
+ status.v = job.active;
6531
+ status.reason = typeof job.reason === "string" ? job.reason : "";
6532
+ });
6533
+ const current = activeSection;
6534
+ renderAll();
6535
+ showSection(current);
6536
+ }
6537
+ function githubRepoHostBlock() {
6538
+ return {
6539
+ card: "Host",
6540
+ rows: [
6541
+ {
6542
+ key: "",
6543
+ label: "Git host",
6544
+ desc: "Which host this repository lives on. GitHub is the only host Lisa provisions today.",
6545
+ control: sel("GitHub", ["GitHub", "GitLab", "Bitbucket"]),
6546
+ },
6547
+ ],
6548
+ };
6549
+ }
6550
+ function githubRepoSettingsRows(settings) {
6551
+ return [
6552
+ {
6553
+ label: "Allow merge commits",
6554
+ desc: "The fleet is merge-only — squash and rebase are disabled because squashing flattens release commits and breaks promotion.",
6555
+ control: tog(!!settings.allow_merge_commit),
6556
+ },
6557
+ {
6558
+ label: "Allow squash merge",
6559
+ desc: "",
6560
+ control: tog(!!settings.allow_squash_merge),
6561
+ },
6562
+ {
6563
+ label: "Allow rebase merge",
6564
+ desc: "",
6565
+ control: tog(!!settings.allow_rebase_merge),
6566
+ },
6567
+ {
6568
+ label: "Auto-merge",
6569
+ desc: "Lisa PRs enable auto-merge and drive themselves to green.",
6570
+ control: tog(!!settings.allow_auto_merge),
6571
+ },
6572
+ {
6573
+ label: "Suggest updating PR branches",
6574
+ desc: "",
6575
+ control: tog(!!settings.allow_update_branch),
6576
+ },
6577
+ {
6578
+ label: "Delete head branch on merge",
6579
+ desc: "Environment branches survive via the prevent-delete ruleset.",
6580
+ control: tog(!!settings.delete_branch_on_merge),
6581
+ },
6582
+ {
6583
+ label: "Merge commit title",
6584
+ desc: "",
6585
+ control: sel(String(settings.merge_commit_title || ""), [
6586
+ "MERGE_MESSAGE",
6587
+ "PR_TITLE",
6588
+ ]),
6589
+ },
6590
+ {
6591
+ label: "Issues tab",
6592
+ desc: "",
6593
+ control: tog(!!settings.has_issues),
6594
+ },
6595
+ {
6596
+ label: "Wiki tab",
6597
+ desc: "Off — Lisa uses the in-repo wiki/ directory instead.",
6598
+ control: tog(!!settings.has_wiki),
6599
+ },
6600
+ {
6601
+ label: "Secret scanning + push protection",
6602
+ desc: "Best-effort; skipped when the plan lacks GitHub Advanced Security.",
6603
+ control: tog(!!settings.secret_scanning),
6604
+ },
6605
+ {
6606
+ label: "Default branch",
6607
+ desc: "Live default branch from the repository.",
6608
+ control: txt(String(settings.default_branch || ""), {
6609
+ mono: true,
6610
+ }),
6611
+ },
6612
+ ];
6613
+ }
6614
+ function applyGithubRepoPanel(result) {
6615
+ const host = githubRepoHostBlock();
6616
+ if (result.state !== "value") {
6617
+ const reason =
6618
+ result.state === "unknown"
6619
+ ? result.reason + ": " + result.message
6620
+ : "not applicable";
6621
+ DATA.sections.repository.blocks = [
6622
+ host,
6623
+ {
6624
+ type: "callout",
6625
+ variant: "warn",
6626
+ text:
6627
+ "<b>unknown — not authenticated / unavailable</b> — " +
6628
+ esc(reason) +
6629
+ ". No setting, ruleset, label, or secret state is shown as a concrete value.",
6630
+ },
6631
+ ];
6632
+ return;
6633
+ }
6634
+ const value = result.value || {};
6635
+ const settings = value.settings || {};
6636
+ const rulesets = Array.isArray(value.rulesets) ? value.rulesets : [];
6637
+ const labels = Array.isArray(value.labels) ? value.labels : [];
6638
+ const secrets = Array.isArray(value.secrets) ? value.secrets : [];
6639
+ DATA.sections.repository.blocks = [
6640
+ host,
6641
+ {
6642
+ card: "Repository settings",
6643
+ note:
6644
+ "Live from gh api repos/" +
6645
+ String(value.owner || "") +
6646
+ "/" +
6647
+ String(value.repo || ""),
6648
+ rows: githubRepoSettingsRows(settings),
6649
+ },
6650
+ {
6651
+ type: "table",
6652
+ card: "Rulesets",
6653
+ note: "Live from gh api repos/{owner}/{repo}/rulesets",
6654
+ cols: ["Ruleset", "Applies to", "Enforces", "Active"],
6655
+ rows: rulesets.map(row => [
6656
+ { t: "mono", v: String(row.name || "") },
6657
+ { t: "text", v: String(row.appliesTo || "—") },
6658
+ { t: "text", v: String(row.enforces || "—") },
6659
+ {
6660
+ t: "badge",
6661
+ v: row.active ? "active" : "disabled",
6662
+ cls: row.active ? "on" : "off",
6663
+ },
6664
+ ]),
6665
+ },
6666
+ {
6667
+ type: "table",
6668
+ card: "Lifecycle labels",
6669
+ note: "Config-expected labels vs live gh label list",
6670
+ cols: ["Label", "Role", "Color", "Status"],
6671
+ rows: labels.map(row => [
6672
+ { t: "mono", v: String(row.name || "") },
6673
+ { t: "text", v: String(row.role || "") },
6674
+ row.present && row.color
6675
+ ? {
6676
+ t: "color",
6677
+ v: "#" + String(row.color).replace(/^#/u, ""),
6678
+ }
6679
+ : { t: "text", v: "—" },
6680
+ {
6681
+ t: "badge",
6682
+ v: row.present ? "present" : "missing",
6683
+ cls: row.present ? "on" : "off",
6684
+ },
6685
+ ]),
6686
+ },
6687
+ {
6688
+ type: "table",
6689
+ card: "Secrets",
6690
+ note: "Presence only from gh secret list — values are never read",
6691
+ cols: ["Secret", "Purpose", "Status"],
6692
+ rows: secrets.map(row => [
6693
+ { t: "mono", v: String(row.name || "") },
6694
+ { t: "text", v: String(row.purpose || "") },
6695
+ {
6696
+ t: "badge",
6697
+ v: row.set ? "set" : "missing",
6698
+ cls: row.set ? "on" : "off",
6699
+ },
6700
+ ]),
6701
+ },
6702
+ ];
6703
+ }
6704
+ function reRenderAfterGithubRepoHydration() {
6705
+ const current = activeSection;
6706
+ renderAll();
6707
+ showSection(current || "overview");
6708
+ }
6709
+ /**
6710
+ * Replace the Deploy pipeline stages table from the live probe.
6711
+ * Never leaves demo hold/no-hold claims in place.
6712
+ * @param probes - Live-status probe map from /api/status
6713
+ */
6714
+ function applyDeployPipelineStages(probes) {
6715
+ const section = DATA.sections.deploy;
6716
+ if (!section || !Array.isArray(section.blocks)) return;
6717
+ const block = section.blocks.find(
6718
+ candidate =>
6719
+ candidate &&
6720
+ candidate.type === "table" &&
6721
+ candidate.id === "deploy-pipeline-stages"
6722
+ );
6723
+ if (!block) return;
6724
+ const raw =
6725
+ probes && Object.hasOwn(probes, "deploy-pipeline-stages")
6726
+ ? probes["deploy-pipeline-stages"]
6727
+ : undefined;
6728
+ const result =
6729
+ raw === undefined
6730
+ ? {
6731
+ state: "unknown",
6732
+ reason: "missing-probe",
6733
+ message: "The deploy-pipeline-stages probe was not reported",
6734
+ }
6735
+ : normalizeLiveStatusResult(raw);
6736
+ if (result.state === "not-applicable") {
6737
+ block.rows = [
6738
+ [
6739
+ { t: "mono", v: "—" },
6740
+ {
6741
+ t: "text",
6742
+ v: "No deploy.yml or github.environments configured for this project",
6743
+ },
6744
+ { t: "status", v: "unknown", reason: "not-applicable" },
6745
+ ],
6746
+ ];
6747
+ } else if (
6748
+ result.state === "value" &&
6749
+ result.value &&
6750
+ typeof result.value === "object" &&
6751
+ Array.isArray(result.value.stages)
6752
+ ) {
6753
+ block.rows = result.value.stages.map(stage => [
6754
+ {
6755
+ t: "mono",
6756
+ v: String(stage.name || stage.id || "stage"),
6757
+ },
6758
+ {
6759
+ t: "text",
6760
+ v: String(stage.description || ""),
6761
+ },
6762
+ {
6763
+ t: "status",
6764
+ v: stage.active === "unknown" ? "unknown" : stage.active === true,
6765
+ reason:
6766
+ typeof stage.reason === "string" && stage.reason.length > 0
6767
+ ? stage.reason
6768
+ : undefined,
6769
+ },
6770
+ ]);
6771
+ if (block.rows.length === 0) {
6772
+ block.rows = [
6773
+ [
6774
+ { t: "mono", v: "—" },
6775
+ { t: "text", v: "Deploy pipeline reported zero stages" },
6776
+ {
6777
+ t: "status",
6778
+ v: "unknown",
6779
+ reason: "empty-stages",
6780
+ },
6781
+ ],
6782
+ ];
6783
+ }
6784
+ } else {
6785
+ const reason =
6786
+ result.state === "unknown"
6787
+ ? result.reason + ": " + result.message
6788
+ : "invalid-value: deploy-pipeline-stages probe returned an unusable value";
6789
+ block.rows = [
6790
+ [
6791
+ { t: "mono", v: "—" },
6792
+ {
6793
+ t: "text",
6794
+ v: "Deploy pipeline stages unavailable",
6795
+ },
6796
+ { t: "status", v: "unknown", reason: reason },
6797
+ ],
6798
+ ];
6799
+ }
6800
+ const existing = document.getElementById("deploy-pipeline-stages");
6801
+ if (existing) {
6802
+ existing.replaceWith(buildTable(block));
6803
+ }
6804
+ }
6805
+ /* Map the detected-stacks probe into stackDetection. A missing probe or a
6806
+ non-value/non-array result becomes an explicit unknown state — never a
6807
+ false empty and never the metadata catalog. */
6808
+ function applyDetectedStacks(probes) {
6809
+ const raw =
6810
+ probes && Object.hasOwn(probes, "detected-stacks")
6811
+ ? probes["detected-stacks"]
6812
+ : undefined;
6813
+ if (raw === undefined) {
6814
+ stackDetection = {
6815
+ status: "unknown",
6816
+ ids: [],
6817
+ reason: "missing-probe",
6818
+ message: "The detected-stacks probe was not reported",
6819
+ };
6820
+ refreshStacks();
6821
+ return;
6822
+ }
6823
+ const result = normalizeLiveStatusResult(raw);
6824
+ if (result.state === "value" && Array.isArray(result.value)) {
6825
+ const ids = result.value.filter(v => typeof v === "string");
6826
+ if (ids.length !== result.value.length) {
6827
+ // Any non-string element makes the whole array untrustworthy;
6828
+ // an empty state here would fabricate a "no stacks" certainty.
6829
+ stackDetection = {
6830
+ status: "unknown",
6831
+ ids: [],
6832
+ reason: "invalid-value",
6833
+ message: "detected-stacks did not return a string array",
6834
+ };
6835
+ } else {
6836
+ stackDetection =
6837
+ ids.length > 0
6838
+ ? { status: "value", ids }
6839
+ : { status: "empty", ids: [] };
6840
+ }
6841
+ } else if (result.state === "value") {
6842
+ stackDetection = {
6843
+ status: "unknown",
6844
+ ids: [],
6845
+ reason: "invalid-value",
6846
+ message: "detected-stacks did not return a string array",
6847
+ };
6848
+ } else if (result.state === "not-applicable") {
6849
+ stackDetection = {
6850
+ status: "unknown",
6851
+ ids: [],
6852
+ reason: "not-applicable",
6853
+ message: "detected-stacks reported not applicable",
6854
+ };
6855
+ } else {
6856
+ stackDetection = {
6857
+ status: "unknown",
6858
+ ids: [],
6859
+ reason: result.reason,
6860
+ message: result.message,
6861
+ };
6862
+ }
6863
+ refreshStacks();
6864
+ }
6865
+ function markStacksUnreachable(error) {
6866
+ stackDetection = {
6867
+ status: "unknown",
6868
+ ids: [],
6869
+ reason: "status-unavailable",
6870
+ message: error instanceof Error ? error.message : String(error),
6871
+ };
6872
+ refreshStacks();
6873
+ }
6874
+ function automationsScheduledJobsBlock() {
6875
+ const blocks =
6876
+ (DATA.sections.automations && DATA.sections.automations.blocks) || [];
6877
+ return blocks.find(block => block.id === "automations-scheduled-jobs");
6878
+ }
6879
+ function automationRowFromProbe(entry) {
6880
+ const cadence =
6881
+ typeof entry.cadence === "string" && entry.cadence.trim().length > 0
6882
+ ? entry.cadence
6883
+ : "unknown";
6884
+ const lastRun =
6885
+ typeof entry.lastRunAt === "string" &&
6886
+ entry.lastRunAt.trim().length > 0
6887
+ ? entry.lastRunAt
6888
+ : "—";
6889
+ const status =
6890
+ typeof entry.status === "string" && entry.status.trim().length > 0
6891
+ ? entry.status
6892
+ : "unknown";
6893
+ const runtime =
6894
+ entry.runtime === "claude" ? "claude /schedule" : "codex";
6895
+ const row = [
6896
+ { t: "mono", v: entry.id },
6897
+ { t: "badge", v: runtime, cls: "default" },
6898
+ { t: "mono", v: cadence },
6899
+ { t: "mono", v: lastRun },
6900
+ {
6901
+ t: "badge",
6902
+ v: status,
6903
+ cls: status.toUpperCase() === "ACTIVE" ? "pass" : "default",
6904
+ },
6905
+ ];
6906
+ row._automationId = entry.id;
6907
+ return row;
6908
+ }
6909
+ function hydrateAutomationsSection(probeResult) {
6910
+ const block = automationsScheduledJobsBlock();
6911
+ if (!block) return;
6912
+ const result = normalizeLiveStatusResult(probeResult);
6913
+ if (result.state === "value") {
6914
+ const automations = Array.isArray(result.value.automations)
6915
+ ? result.value.automations
6916
+ : [];
6917
+ block.rows = automations.map(automationRowFromProbe);
6918
+ if (block.rows.length === 0) {
6919
+ block.empty = {
6920
+ state: "empty",
6921
+ message:
6922
+ "No lisa-auto-<project>- automations found in the harness scheduler",
6923
+ };
6924
+ } else {
6925
+ delete block.empty;
6926
+ }
6927
+ } else if (result.state === "not-applicable") {
6928
+ block.rows = [];
6929
+ block.empty = {
6930
+ state: "not-applicable",
6931
+ message: "Automations are not applicable on this machine",
6932
+ };
6933
+ } else {
6934
+ block.rows = [];
6935
+ block.empty = {
6936
+ state: "unknown",
6937
+ message: result.reason + ": " + result.message,
6938
+ };
6939
+ }
6940
+ renderAll();
6941
+ showSection(activeSection);
6942
+ }
6943
+
6155
6944
  async function hydrateLiveStatuses() {
6156
6945
  try {
6157
6946
  const response = await fetch("/api/status", {
@@ -6161,15 +6950,53 @@
6161
6950
  const snapshot = await response.json();
6162
6951
  const probes = liveStatusProbeMap(snapshot);
6163
6952
  renderLiveStatuses(probes);
6953
+ applyEnabledPlugins(probes);
6954
+ applyDetectedStacks(probes);
6955
+ // Quality jobs / automations re-render the page; paint the version chip after.
6956
+ applyCiQualityJobs(probes);
6957
+ hydrateAutomationsSection(
6958
+ probes.automations || {
6959
+ state: "unknown",
6960
+ reason: "probe-missing",
6961
+ message: "Automations probe was omitted from /api/status",
6962
+ }
6963
+ );
6164
6964
  applyLisaVersion(probes);
6965
+ const repoResult = normalizeLiveStatusResult(
6966
+ probes["github-repo"] || {
6967
+ state: "unknown",
6968
+ reason: "probe-missing",
6969
+ message: "GitHub repository probe did not run",
6970
+ }
6971
+ );
6972
+ applyGithubRepoPanel(repoResult);
6973
+ reRenderAfterGithubRepoHydration();
6974
+ applyDeployPipelineStages(probes);
6975
+ applyObservabilityProviders(probes);
6165
6976
  } catch (error) {
6977
+ const message =
6978
+ error instanceof Error ? error.message : String(error);
6166
6979
  const unavailable = {
6167
6980
  state: "unknown",
6168
6981
  reason: "status-unavailable",
6169
- message: error instanceof Error ? error.message : String(error),
6982
+ message,
6170
6983
  };
6171
6984
  renderLiveStatuses({ transport: unavailable });
6985
+ applyEnabledPlugins({ "enabled-plugins": unavailable });
6986
+ applyCiQualityJobs({ "ci-quality-jobs": unavailable });
6987
+ hydrateAutomationsSection(unavailable);
6172
6988
  applyLisaVersion({ "lisa-version": unavailable });
6989
+ applyGithubRepoPanel(unavailable);
6990
+ reRenderAfterGithubRepoHydration();
6991
+ applyDeployPipelineStages({
6992
+ "deploy-pipeline-stages": unavailable,
6993
+ });
6994
+ applyObservabilityProviders({
6995
+ sentry: unavailable,
6996
+ "cloudwatch-alarms": unavailable,
6997
+ "x-ray": unavailable,
6998
+ });
6999
+ markStacksUnreachable(error);
6173
7000
  }
6174
7001
  }
6175
7002