selftune 0.1.4 → 0.2.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 (86) hide show
  1. package/.claude/agents/diagnosis-analyst.md +146 -0
  2. package/.claude/agents/evolution-reviewer.md +167 -0
  3. package/.claude/agents/integration-guide.md +200 -0
  4. package/.claude/agents/pattern-analyst.md +147 -0
  5. package/CHANGELOG.md +37 -0
  6. package/README.md +96 -256
  7. package/assets/BeforeAfter.gif +0 -0
  8. package/assets/FeedbackLoop.gif +0 -0
  9. package/assets/logo.svg +9 -0
  10. package/assets/skill-health-badge.svg +20 -0
  11. package/cli/selftune/activation-rules.ts +171 -0
  12. package/cli/selftune/badge/badge-data.ts +108 -0
  13. package/cli/selftune/badge/badge-svg.ts +212 -0
  14. package/cli/selftune/badge/badge.ts +103 -0
  15. package/cli/selftune/constants.ts +75 -1
  16. package/cli/selftune/contribute/bundle.ts +314 -0
  17. package/cli/selftune/contribute/contribute.ts +214 -0
  18. package/cli/selftune/contribute/sanitize.ts +162 -0
  19. package/cli/selftune/cron/setup.ts +266 -0
  20. package/cli/selftune/dashboard-server.ts +582 -0
  21. package/cli/selftune/dashboard.ts +25 -3
  22. package/cli/selftune/eval/baseline.ts +247 -0
  23. package/cli/selftune/eval/composability.ts +117 -0
  24. package/cli/selftune/eval/generate-unit-tests.ts +143 -0
  25. package/cli/selftune/eval/hooks-to-evals.ts +68 -2
  26. package/cli/selftune/eval/import-skillsbench.ts +221 -0
  27. package/cli/selftune/eval/synthetic-evals.ts +172 -0
  28. package/cli/selftune/eval/unit-test-cli.ts +152 -0
  29. package/cli/selftune/eval/unit-test.ts +196 -0
  30. package/cli/selftune/evolution/deploy-proposal.ts +142 -1
  31. package/cli/selftune/evolution/evolve-body.ts +492 -0
  32. package/cli/selftune/evolution/evolve.ts +466 -103
  33. package/cli/selftune/evolution/extract-patterns.ts +32 -1
  34. package/cli/selftune/evolution/pareto.ts +314 -0
  35. package/cli/selftune/evolution/propose-body.ts +171 -0
  36. package/cli/selftune/evolution/propose-description.ts +100 -2
  37. package/cli/selftune/evolution/propose-routing.ts +166 -0
  38. package/cli/selftune/evolution/refine-body.ts +141 -0
  39. package/cli/selftune/evolution/rollback.ts +19 -2
  40. package/cli/selftune/evolution/validate-body.ts +254 -0
  41. package/cli/selftune/evolution/validate-proposal.ts +257 -35
  42. package/cli/selftune/evolution/validate-routing.ts +177 -0
  43. package/cli/selftune/grading/grade-session.ts +138 -18
  44. package/cli/selftune/grading/pre-gates.ts +104 -0
  45. package/cli/selftune/hooks/auto-activate.ts +185 -0
  46. package/cli/selftune/hooks/evolution-guard.ts +165 -0
  47. package/cli/selftune/hooks/skill-change-guard.ts +112 -0
  48. package/cli/selftune/index.ts +88 -0
  49. package/cli/selftune/ingestors/claude-replay.ts +351 -0
  50. package/cli/selftune/ingestors/openclaw-ingest.ts +440 -0
  51. package/cli/selftune/init.ts +150 -3
  52. package/cli/selftune/memory/writer.ts +447 -0
  53. package/cli/selftune/monitoring/watch.ts +25 -2
  54. package/cli/selftune/status.ts +17 -13
  55. package/cli/selftune/types.ts +377 -5
  56. package/cli/selftune/utils/frontmatter.ts +217 -0
  57. package/cli/selftune/utils/llm-call.ts +29 -3
  58. package/cli/selftune/utils/transcript.ts +35 -0
  59. package/cli/selftune/utils/trigger-check.ts +89 -0
  60. package/cli/selftune/utils/tui.ts +156 -0
  61. package/dashboard/index.html +569 -8
  62. package/package.json +8 -4
  63. package/skill/SKILL.md +124 -8
  64. package/skill/Workflows/AutoActivation.md +144 -0
  65. package/skill/Workflows/Badge.md +118 -0
  66. package/skill/Workflows/Baseline.md +121 -0
  67. package/skill/Workflows/Composability.md +100 -0
  68. package/skill/Workflows/Contribute.md +91 -0
  69. package/skill/Workflows/Cron.md +155 -0
  70. package/skill/Workflows/Dashboard.md +203 -0
  71. package/skill/Workflows/Doctor.md +37 -1
  72. package/skill/Workflows/Evals.md +69 -1
  73. package/skill/Workflows/EvolutionMemory.md +152 -0
  74. package/skill/Workflows/Evolve.md +111 -6
  75. package/skill/Workflows/EvolveBody.md +159 -0
  76. package/skill/Workflows/ImportSkillsBench.md +111 -0
  77. package/skill/Workflows/Ingest.md +117 -3
  78. package/skill/Workflows/Initialize.md +57 -3
  79. package/skill/Workflows/Replay.md +70 -0
  80. package/skill/Workflows/Rollback.md +20 -1
  81. package/skill/Workflows/UnitTest.md +138 -0
  82. package/skill/Workflows/Watch.md +22 -0
  83. package/skill/settings_snippet.json +23 -0
  84. package/templates/activation-rules-default.json +27 -0
  85. package/templates/multi-skill-settings.json +64 -0
  86. package/templates/single-skill-settings.json +58 -0
@@ -274,7 +274,10 @@
274
274
  }
275
275
  .pass-rate-fill.healthy { background: var(--green); }
276
276
  .pass-rate-fill.drifting { background: var(--amber); }
277
+ .pass-rate-fill.warning { background: var(--amber); }
277
278
  .pass-rate-fill.regressed { background: var(--red); }
279
+ .pass-rate-fill.critical { background: var(--red); }
280
+ .pass-rate-fill.unknown { background: #ccc; }
278
281
  .pass-rate-label {
279
282
  font-family: 'Poppins', sans-serif;
280
283
  font-size: 0.75rem;
@@ -413,6 +416,216 @@
413
416
  border-color: var(--accent);
414
417
  color: var(--accent);
415
418
  }
419
+
420
+ /* ---- Live indicator ---- */
421
+ .live-indicator {
422
+ display: inline-flex;
423
+ align-items: center;
424
+ gap: 0.375rem;
425
+ font-family: var(--mono);
426
+ font-size: 0.6875rem;
427
+ color: var(--green);
428
+ }
429
+ .live-dot {
430
+ width: 6px;
431
+ height: 6px;
432
+ border-radius: 50%;
433
+ background: var(--green);
434
+ animation: pulse 2s ease-in-out infinite;
435
+ }
436
+ @keyframes pulse {
437
+ 0%, 100% { opacity: 1; }
438
+ 50% { opacity: 0.4; }
439
+ }
440
+
441
+ /* ---- Action buttons ---- */
442
+ .action-btn {
443
+ font-family: 'Poppins', sans-serif;
444
+ font-size: 0.6875rem;
445
+ font-weight: 500;
446
+ padding: 0.3rem 0.75rem;
447
+ border: 1px solid var(--border);
448
+ border-radius: var(--radius);
449
+ background: var(--surface);
450
+ color: var(--text-secondary);
451
+ cursor: pointer;
452
+ transition: all 0.15s;
453
+ white-space: nowrap;
454
+ }
455
+ .action-btn:hover:not(:disabled) {
456
+ border-color: var(--accent);
457
+ color: var(--accent);
458
+ }
459
+ .action-btn:disabled {
460
+ opacity: 0.5;
461
+ cursor: not-allowed;
462
+ }
463
+ .action-btn.loading {
464
+ position: relative;
465
+ color: transparent;
466
+ }
467
+ .action-btn.loading::after {
468
+ content: '';
469
+ position: absolute;
470
+ inset: 0;
471
+ display: flex;
472
+ align-items: center;
473
+ justify-content: center;
474
+ color: var(--accent);
475
+ font-size: 0.625rem;
476
+ }
477
+ .action-btn-group {
478
+ display: flex;
479
+ gap: 0.375rem;
480
+ flex-wrap: wrap;
481
+ }
482
+ .action-result {
483
+ font-family: var(--mono);
484
+ font-size: 0.6875rem;
485
+ padding: 0.5rem 0.75rem;
486
+ margin-top: 0.375rem;
487
+ border-radius: var(--radius);
488
+ max-height: 120px;
489
+ overflow-y: auto;
490
+ display: none;
491
+ }
492
+ .action-result.visible { display: block; }
493
+ .action-result.success { background: var(--green-bg); color: var(--green); }
494
+ .action-result.error { background: var(--red-bg); color: var(--red); }
495
+
496
+ /* ---- Evolution timeline ---- */
497
+ .evo-timeline {
498
+ position: relative;
499
+ padding-left: 1.5rem;
500
+ }
501
+ .evo-timeline::before {
502
+ content: '';
503
+ position: absolute;
504
+ left: 0.375rem;
505
+ top: 0;
506
+ bottom: 0;
507
+ width: 2px;
508
+ background: var(--border);
509
+ }
510
+ .evo-timeline-item {
511
+ position: relative;
512
+ padding: 0.625rem 0;
513
+ padding-left: 0.75rem;
514
+ border-bottom: none;
515
+ }
516
+ .evo-timeline-item::before {
517
+ content: '';
518
+ position: absolute;
519
+ left: -1.125rem;
520
+ top: 1rem;
521
+ width: 8px;
522
+ height: 8px;
523
+ border-radius: 50%;
524
+ background: var(--border);
525
+ border: 2px solid var(--surface);
526
+ }
527
+ .evo-timeline-item.action-evolved::before { background: var(--green); }
528
+ .evo-timeline-item.action-rolled-back::before { background: var(--red); }
529
+ .evo-timeline-item.action-watched::before { background: var(--blue); }
530
+ .evo-timeline-meta {
531
+ font-family: var(--mono);
532
+ font-size: 0.6875rem;
533
+ color: var(--text-muted);
534
+ margin-bottom: 0.25rem;
535
+ }
536
+ .evo-timeline-body {
537
+ font-size: 0.8125rem;
538
+ color: var(--text);
539
+ }
540
+ .evo-timeline-rationale {
541
+ font-size: 0.75rem;
542
+ color: var(--text-secondary);
543
+ margin-top: 0.125rem;
544
+ }
545
+
546
+ /* ---- Search/Filter ---- */
547
+ .search-filter {
548
+ font-family: var(--mono);
549
+ font-size: 0.8125rem;
550
+ padding: 0.5rem 0.75rem;
551
+ border: 1px solid var(--border);
552
+ border-radius: var(--radius);
553
+ background: var(--surface);
554
+ color: var(--text);
555
+ width: 100%;
556
+ max-width: 320px;
557
+ outline: none;
558
+ transition: border-color 0.15s;
559
+ }
560
+ .search-filter:focus {
561
+ border-color: var(--accent);
562
+ }
563
+ .search-filter::placeholder {
564
+ color: var(--text-muted);
565
+ }
566
+
567
+ /* ---- Time Period Selector ---- */
568
+ .time-period-selector {
569
+ display: inline-flex;
570
+ gap: 0;
571
+ border: 1px solid var(--border);
572
+ border-radius: var(--radius);
573
+ overflow: hidden;
574
+ }
575
+ .time-period-selector .period-btn {
576
+ font-family: 'Poppins', sans-serif;
577
+ font-size: 0.6875rem;
578
+ font-weight: 500;
579
+ padding: 0.3rem 0.75rem;
580
+ border: none;
581
+ background: var(--surface);
582
+ color: var(--text-secondary);
583
+ cursor: pointer;
584
+ transition: all 0.15s;
585
+ border-right: 1px solid var(--border);
586
+ }
587
+ .time-period-selector .period-btn:last-child {
588
+ border-right: none;
589
+ }
590
+ .time-period-selector .period-btn:hover {
591
+ background: var(--bg);
592
+ color: var(--accent);
593
+ }
594
+ .time-period-selector .period-btn.active {
595
+ background: var(--accent);
596
+ color: #fff;
597
+ }
598
+
599
+ /* ---- Eval Feed ---- */
600
+ .eval-feed {
601
+ width: 100%;
602
+ border-collapse: collapse;
603
+ font-size: 0.8125rem;
604
+ }
605
+ .eval-feed th, .eval-feed td {
606
+ padding: 0.5rem 0.75rem;
607
+ text-align: left;
608
+ border-bottom: 1px solid var(--border);
609
+ }
610
+ .eval-feed th {
611
+ font-family: 'Poppins', sans-serif;
612
+ font-weight: 500;
613
+ font-size: 0.6875rem;
614
+ text-transform: uppercase;
615
+ letter-spacing: 0.04em;
616
+ color: var(--text-muted);
617
+ background: var(--bg);
618
+ position: sticky;
619
+ top: 0;
620
+ }
621
+ .eval-feed tr:hover { background: var(--bg); }
622
+ .eval-feed td.mono { font-family: var(--mono); font-size: 0.75rem; }
623
+
624
+ /* ---- 4-state badge colors ---- */
625
+ .badge-warning { background: var(--amber-bg); color: var(--amber); }
626
+ .badge-critical { background: var(--red-bg); color: var(--red); }
627
+ .badge-healthy { background: var(--green-bg); color: var(--green); }
628
+ .badge-unknown { background: #f0f0ee; color: #999; }
416
629
  </style>
417
630
  </head>
418
631
  <body>
@@ -420,6 +633,15 @@
420
633
  <!-- ===== Header ===== -->
421
634
  <div class="header">
422
635
  <div class="header-left">
636
+ <svg xmlns="http://www.w3.org/2000/svg" width="28" height="28" viewBox="0 0 250 250" fill="none" style="flex-shrink:0" aria-hidden="true">
637
+ <path d="M 190.16,31.49 C 187.91,29.88 184.51,32.19 185.88,35.16 C 186.31,36.11 187.08,36.54 187.71,37.01 C 218.75,59.86 237.63,92.71 237.63,128.82 C 237.63,175.99 205.12,218.56 153.82,234.69 C 149.89,235.93 150.91,241.71 154.91,240.66 C 205.98,226.96 243.01,181.94 243,128.45 C 242.99,90.87 223.47,56.18 190.16,31.49 Z" fill="currentColor"/>
638
+ <path d="M 125.19,243.91 C 138.08,243.91 147.18,236.44 151.21,225.01 C 193.72,217.79 226.98,184.02 226.98,140.81 C 226.98,121.17 219.82,103.78 209.93,87.04 C 191.42,55.45 165.15,34.72 117.71,28.65 C 112.91,28.04 113.77,34.35 117.19,34.82 C 161.67,39.33 185.84,56.71 203.76,86.42 C 213.87,103.68 220.68,119.61 220.68,140.81 C 220.68,179.96 190.81,211.95 148.71,219.16 C 147.11,219.47 146.27,220.32 145.92,221.8 C 142.95,231.11 135.72,238.02 125.19,237.66 C 64.48,237.66 11.67,191.61 11.67,127.51 C 11.67,79.61 44.82,36.38 93.89,27.77 L 94.11,27.73 L 94.38,26.64 C 97.04,16.61 104.57,11.82 114.19,11.82 C 134.12,13.36 152.91,18.15 170.48,26.08 C 171.92,26.78 173.81,27.09 174.76,25.59 C 176.05,23.72 175.31,21.07 173.01,20.34 C 154.78,11.96 137.21,7.17 114.47,6 H 113.52 C 101.91,6 93.46,12.16 89.49,21.78 C 42.36,31.26 6.17,74.76 6.17,128.08 C 6.17,190.05 57.92,243.91 125.19,243.91 Z" fill="currentColor"/>
639
+ <path d="M 93.67,40.64 C 100.51,52.07 109.54,51.33 114.05,52.17 C 128.72,53.91 141.48,55.78 157.38,62.16 C 162.72,64.47 162.29,58.19 159.18,57.01 C 145.11,51.33 132.48,49.79 111.31,47.48 C 101.83,46.29 95.45,41.18 93.75,32.81 C 55.21,39.46 22.06,72.17 22.06,112.48 C 22.06,131.98 30.36,149.82 43.26,164.49 C 46.23,167.59 50.19,164.13 48.32,161.02 C 36.21,145.54 28.42,129.78 28.42,112.4 C 28.42,79.11 54.91,48.36 89.91,40.36 C 90.76,40.15 91.04,39.87 91.62,40.01 C 92.62,40.01 93.04,39.65 93.67,40.64 Z" fill="currentColor"/>
640
+ <path d="M 152.72,82.77 C 126.61,82.77 113.07,99.44 103.01,119.33 C 100.56,123.36 103.74,125.03 105.61,123.92 C 107.15,123.22 107.89,121.05 108.73,119.61 C 118.22,102.16 130.33,88.56 152.72,88.56 C 181.62,88.56 201.91,116.01 201.91,147.31 C 201.91,175.12 183.47,199.96 152.51,205.75 C 151.84,205.96 151.63,206.03 151.56,205.54 C 147.74,195.37 139.36,188.15 128.07,186.48 C 113.2,184.24 101.23,182.36 83.8,176.81 C 79.3,175.48 77.91,182.36 82.41,183.09 C 97.21,187.46 108.09,189.47 126.25,192.65 C 136.78,194.31 145.41,201.71 147.11,210.95 C 147.74,213.05 149.13,213.41 150.15,213.26 C 183.75,208.61 208.26,180.93 208.26,147.24 C 208.26,115.06 186.94,82.77 152.72,82.77 Z" fill="currentColor"/>
641
+ <path d="M 129.77,105.21 C 122.93,112.05 118.97,122.73 113.77,130.41 C 111.31,133.45 114.56,136.63 117.46,134.46 C 123.75,126.23 127.43,115.62 135.15,108.71 C 138.22,105.81 134.73,101.09 129.77,105.21 Z" fill="currentColor"/>
642
+ <path d="M 136.78,120.31 C 127.71,136.71 120.12,154.91 93.74,154.91 C 66.07,154.91 47.76,128.53 47.76,104.78 C 47.76,84.47 58.57,66.08 77.66,56.25 C 82.23,54.21 79.85,47.76 75.34,49.93 C 54.77,59.72 42.01,80.11 42.01,104.71 C 42.01,131.77 61.86,161.31 93.67,161.31 C 114.77,161.31 128.91,147.24 139.86,124.06 C 142.76,120.45 139.15,117.73 136.78,120.31 Z" fill="currentColor"/>
643
+ <path d="M 30.73,154.7 C 27.76,152.97 23.87,155.93 25.41,158.76 C 41.73,188.36 68.94,199.79 105.75,206.41 C 112.25,207.66 122.07,208.75 123.46,209.03 C 128.07,209.95 128.07,220.18 121.78,220.18 C 107.64,218.94 92.06,215.98 76.23,211.33 C 72.13,210.24 71.04,216.69 75.27,217.64 C 90.41,222.22 103.95,224.74 120.47,226.54 C 133.73,226.54 136.56,209.03 126.03,203.38 C 123.75,202.13 122.73,202.56 112.04,200.76 C 78.09,195.04 54.06,188.98 32.12,155.65 C 31.77,155.23 31.28,154.91 30.73,154.7 Z" fill="currentColor"/>
644
+ </svg>
423
645
  <h1>self<span>tune</span></h1>
424
646
  <span class="version">v0.1.4</span>
425
647
  </div>
@@ -480,6 +702,7 @@
480
702
  </div>
481
703
 
482
704
  <!-- ===== SKILL HEALTH GRID ===== -->
705
+ <input id="skillSearchInput" placeholder="Filter skills..." class="search-filter" aria-label="Filter skills by name" style="margin-bottom:0.5rem;display:none;">
483
706
  <div class="section" id="skillHealthSection" style="display:none;">
484
707
  <div class="section-header">
485
708
  <span>Skill Health Grid</span>
@@ -505,7 +728,15 @@
505
728
  </div>
506
729
  <div class="drill-down-content">
507
730
  <div class="drill-down-section">
508
- <h4 style="font-family:'Poppins',sans-serif;font-size:0.75rem;font-weight:500;text-transform:uppercase;letter-spacing:0.04em;color:var(--text-muted);margin-bottom:0.75rem;">Pass Rate Over Time</h4>
731
+ <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:0.75rem;">
732
+ <h4 style="font-family:'Poppins',sans-serif;font-size:0.75rem;font-weight:500;text-transform:uppercase;letter-spacing:0.04em;color:var(--text-muted);">Pass Rate Over Time</h4>
733
+ <div class="time-period-selector" id="timePeriodSelector">
734
+ <button class="period-btn" data-days="7">7d</button>
735
+ <button class="period-btn" data-days="30">30d</button>
736
+ <button class="period-btn" data-days="90">90d</button>
737
+ <button class="period-btn active" data-days="0">All</button>
738
+ </div>
739
+ </div>
509
740
  <div class="chart-container"><canvas id="chartDrillPassRate"></canvas></div>
510
741
  </div>
511
742
  <div class="drill-down-section">
@@ -532,6 +763,19 @@
532
763
  </table>
533
764
  </div>
534
765
  </div>
766
+ <div class="drill-down-section">
767
+ <h4 style="font-family:'Poppins',sans-serif;font-size:0.75rem;font-weight:500;text-transform:uppercase;letter-spacing:0.04em;color:var(--text-muted);margin-bottom:0.75rem;">Evaluation Feed</h4>
768
+ <div class="table-scroll" style="max-height:260px;">
769
+ <table class="eval-feed" id="drillEvalFeed">
770
+ <thead><tr><th>Time</th><th>Query</th><th>Triggered</th><th>Type</th></tr></thead>
771
+ <tbody></tbody>
772
+ </table>
773
+ </div>
774
+ </div>
775
+ <div class="drill-down-section">
776
+ <h4 style="font-family:'Poppins',sans-serif;font-size:0.75rem;font-weight:500;text-transform:uppercase;letter-spacing:0.04em;color:var(--text-muted);margin-bottom:0.75rem;">Invocation Breakdown</h4>
777
+ <div class="chart-container"><canvas id="chartInvocationBreakdown"></canvas></div>
778
+ </div>
535
779
  </div>
536
780
  </div>
537
781
 
@@ -556,6 +800,27 @@
556
800
  </div>
557
801
  </div>
558
802
 
803
+ <!-- ===== SKILL ACTIONS (live mode only) ===== -->
804
+ <div class="section" id="actionsSection" style="display:none;">
805
+ <div class="section-header">
806
+ <span>Skill Actions</span>
807
+ <span class="live-indicator" id="liveIndicator"><span class="live-dot"></span> LIVE</span>
808
+ </div>
809
+ <div class="section-body" id="actionsBody">
810
+ <div class="empty-state">Select a skill from the health grid to see actions</div>
811
+ </div>
812
+ </div>
813
+
814
+ <!-- ===== EVOLUTION TIMELINE (live mode only) ===== -->
815
+ <div class="section" id="evoTimelineSection" style="display:none;">
816
+ <div class="section-header">Evolution Timeline</div>
817
+ <div class="section-body">
818
+ <div class="evo-timeline" id="evoTimeline">
819
+ <div class="empty-state">No evolution decisions recorded</div>
820
+ </div>
821
+ </div>
822
+ </div>
823
+
559
824
  </div>
560
825
 
561
826
  <script>
@@ -572,6 +837,7 @@ const state = {
572
837
 
573
838
  const charts = {};
574
839
  let selectedSkill = null;
840
+ let selectedPeriodDays = 0; // 0 = All
575
841
 
576
842
  // ========================================================================
577
843
  // File identification
@@ -755,16 +1021,18 @@ function groupByDay(records) {
755
1021
  }
756
1022
 
757
1023
  function getSkillStatus(passRate, regressionDetected) {
758
- if (regressionDetected || passRate < 0.5) return 'regressed';
759
- if (passRate < 0.7) return 'drifting';
1024
+ if (passRate === null || passRate === undefined) return 'unknown';
1025
+ if (regressionDetected || passRate < 0.4) return 'critical';
1026
+ if (passRate < 0.7) return 'warning';
760
1027
  return 'healthy';
761
1028
  }
762
1029
 
763
1030
  function getStatusBadge(status) {
764
1031
  const map = {
765
- healthy: '<span class="badge badge-green">Healthy</span>',
766
- drifting: '<span class="badge badge-amber">Drifting</span>',
767
- regressed: '<span class="badge badge-red">Regressed</span>',
1032
+ healthy: '<span class="badge badge-healthy">Healthy</span>',
1033
+ warning: '<span class="badge badge-warning">Warning</span>',
1034
+ critical: '<span class="badge badge-critical">Critical</span>',
1035
+ unknown: '<span class="badge badge-unknown">Unknown</span>',
768
1036
  };
769
1037
  return map[status] || '';
770
1038
  }
@@ -785,6 +1053,7 @@ function refreshAll() {
785
1053
  dropZone.style.display = 'none';
786
1054
  document.getElementById('healthSummary').style.display = 'block';
787
1055
  document.getElementById('skillHealthSection').style.display = 'block';
1056
+ document.getElementById('skillSearchInput').style.display = 'block';
788
1057
  }
789
1058
 
790
1059
  updateHeader();
@@ -826,7 +1095,7 @@ function updateHealthSummary() {
826
1095
  document.getElementById('kpi-avg-pass-rate').textContent = (avgPR * 100).toFixed(0) + '%';
827
1096
  const status = getSkillStatus(avgPR, false);
828
1097
  document.getElementById('kpi-pass-rate-sub').textContent =
829
- status === 'healthy' ? 'system healthy' : status === 'drifting' ? 'needs monitoring' : 'action required';
1098
+ status === 'healthy' ? 'system healthy' : status === 'warning' ? 'needs monitoring' : status === 'critical' ? 'action required' : 'no data';
830
1099
  }
831
1100
 
832
1101
  document.getElementById('kpi-regressions').textContent = regressions.length;
@@ -908,6 +1177,16 @@ function updateSkillHealthGrid() {
908
1177
  row.addEventListener('click', handler);
909
1178
  row.addEventListener('keydown', e => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); handler(); } });
910
1179
  });
1180
+
1181
+ // Reapply search filter after grid rebuild
1182
+ const searchInput = document.getElementById('skillSearchInput');
1183
+ if (searchInput && searchInput.value) {
1184
+ const query = searchInput.value.toLowerCase();
1185
+ grid.querySelectorAll('.skill-health-row').forEach(row => {
1186
+ const name = (row.dataset.skill || '').toLowerCase();
1187
+ row.style.display = name.includes(query) ? '' : 'none';
1188
+ });
1189
+ }
911
1190
  }
912
1191
 
913
1192
  // ========================================================================
@@ -924,6 +1203,8 @@ function openDrillDown(skillName) {
924
1203
  updateDrillMissedQueries(skillName);
925
1204
  updateDrillEvolution(skillName);
926
1205
  updateDrillSessions(skillName);
1206
+ updateDrillEvalFeed(skillName);
1207
+ updateDrillInvocationBreakdown(skillName);
927
1208
  }
928
1209
 
929
1210
  document.getElementById('drillDownClose').addEventListener('click', () => {
@@ -934,7 +1215,8 @@ document.getElementById('drillDownClose').addEventListener('click', () => {
934
1215
 
935
1216
  function updateDrillPassRateChart(skillName) {
936
1217
  // Group skill records by day and compute daily pass rate
937
- const records = state.skills.filter(r => r.skill_name === skillName);
1218
+ const allRecords = state.skills.filter(r => r.skill_name === skillName);
1219
+ const records = filterByPeriod(allRecords, selectedPeriodDays);
938
1220
  const byDay = {};
939
1221
  for (const r of records) {
940
1222
  const day = toDayKey(r.timestamp);
@@ -1107,12 +1389,291 @@ document.getElementById('exportCsvBtn').addEventListener('click', () => {
1107
1389
  a.click();
1108
1390
  });
1109
1391
 
1392
+ // ========================================================================
1393
+ // Live mode: SSE client + action buttons + evolution timeline
1394
+ // ========================================================================
1395
+ let sseSource = null;
1396
+
1397
+ function isLiveMode() {
1398
+ return window.__SELFTUNE_LIVE__ === true;
1399
+ }
1400
+
1401
+ function startSSE() {
1402
+ if (!isLiveMode()) return;
1403
+ if (sseSource) { sseSource.close(); sseSource = null; }
1404
+
1405
+ sseSource = new EventSource('/api/events');
1406
+ sseSource.addEventListener('data', (e) => {
1407
+ try {
1408
+ const data = JSON.parse(e.data);
1409
+ if (data.telemetry) state.telemetry = data.telemetry;
1410
+ if (data.skills) state.skills = data.skills;
1411
+ if (data.queries) state.queries = data.queries;
1412
+ if (data.evolution) state.evolution = data.evolution;
1413
+ if (data.decisions) state.decisions = data.decisions;
1414
+ if (data.computed) {
1415
+ state.computed = data.computed;
1416
+ } else {
1417
+ state.computed = computeClientSide();
1418
+ }
1419
+ refreshAll();
1420
+ updateEvolutionTimeline();
1421
+ } catch (err) { console.warn('[selftune] SSE parse error:', err); }
1422
+ });
1423
+ sseSource.onerror = () => {
1424
+ // Reconnect after 3 seconds on error
1425
+ setTimeout(() => { if (isLiveMode()) startSSE(); }, 3000);
1426
+ };
1427
+ }
1428
+
1429
+ // Decisions state (populated from server in live mode)
1430
+ if (!state.decisions) state.decisions = [];
1431
+
1432
+ function updateEvolutionTimeline() {
1433
+ if (!isLiveMode()) return;
1434
+ const decisions = state.decisions || [];
1435
+ const section = document.getElementById('evoTimelineSection');
1436
+ if (!section) return;
1437
+
1438
+ section.style.display = 'block';
1439
+ const container = document.getElementById('evoTimeline');
1440
+ if (!decisions.length) {
1441
+ container.innerHTML = '<div class="empty-state">No evolution decisions recorded</div>';
1442
+ return;
1443
+ }
1444
+
1445
+ // Show most recent first
1446
+ const sorted = [...decisions].reverse();
1447
+ container.innerHTML = sorted.slice(0, 50).map(d => {
1448
+ const actionClass = 'action-' + escapeHtml(d.action || '');
1449
+ return `<div class="evo-timeline-item ${actionClass}">
1450
+ <div class="evo-timeline-meta">${escapeHtml(formatTimestamp(d.timestamp))} &middot; ${escapeHtml(d.skillName)}</div>
1451
+ <div class="evo-timeline-body">
1452
+ <span class="badge ${d.action === 'evolved' ? 'badge-green' : d.action === 'rolled-back' ? 'badge-red' : 'badge-blue'}">${escapeHtml(d.action)}</span>
1453
+ <span style="margin-left:0.375rem;">${escapeHtml(d.actionType)}</span>
1454
+ </div>
1455
+ <div class="evo-timeline-rationale">${escapeHtml(truncate(d.rationale, 100))}</div>
1456
+ </div>`;
1457
+ }).join('');
1458
+ }
1459
+
1460
+ function showActionButtons(skillName) {
1461
+ if (!isLiveMode()) return;
1462
+ const section = document.getElementById('actionsSection');
1463
+ const body = document.getElementById('actionsBody');
1464
+ if (!section || !body) return;
1465
+
1466
+ section.style.display = 'block';
1467
+
1468
+ // Find skill path from skill records
1469
+ const skillRecord = state.skills.find(r => r.skill_name === skillName);
1470
+ const skillPath = skillRecord ? skillRecord.skill_path : '';
1471
+ const safeSkill = escapeHtml(skillName);
1472
+ const safeSkillPath = escapeHtml(skillPath);
1473
+
1474
+ body.innerHTML = `
1475
+ <div style="margin-bottom:0.5rem;font-family:'Poppins',sans-serif;font-size:0.8125rem;font-weight:600;">${safeSkill}</div>
1476
+ <div class="action-btn-group">
1477
+ <button class="action-btn" id="btn-watch" data-skill="${safeSkill}" data-path="${safeSkillPath}">Watch</button>
1478
+ <button class="action-btn" id="btn-evolve" data-skill="${safeSkill}" data-path="${safeSkillPath}">Evolve</button>
1479
+ <button class="action-btn" id="btn-rollback" data-skill="${safeSkill}" data-path="${safeSkillPath}">Rollback</button>
1480
+ </div>
1481
+ <div class="action-result" id="action-result"></div>
1482
+ `;
1483
+
1484
+ // Bind action handlers
1485
+ document.getElementById('btn-watch').addEventListener('click', () => runSkillAction('watch', skillName, skillPath));
1486
+ document.getElementById('btn-evolve').addEventListener('click', () => runSkillAction('evolve', skillName, skillPath));
1487
+ document.getElementById('btn-rollback').addEventListener('click', () => runSkillAction('rollback', skillName, skillPath));
1488
+ }
1489
+
1490
+ async function runSkillAction(action, skill, skillPath) {
1491
+ const btn = document.getElementById('btn-' + action);
1492
+ const resultEl = document.getElementById('action-result');
1493
+ if (!btn || !resultEl) return;
1494
+
1495
+ // Set loading state
1496
+ btn.classList.add('loading');
1497
+ btn.disabled = true;
1498
+ btn.textContent = '...';
1499
+ resultEl.className = 'action-result';
1500
+ resultEl.style.display = 'none';
1501
+
1502
+ try {
1503
+ const payload = { skill, skillPath };
1504
+ if (action === 'rollback') {
1505
+ // For rollback, find the latest pending proposal
1506
+ const pending = (state.computed && state.computed.pendingProposals) || [];
1507
+ const needle = skill.toLowerCase();
1508
+ const match = pending.find(p => (p.details || '').toLowerCase().includes(needle));
1509
+ if (match) payload.proposalId = match.proposal_id;
1510
+ }
1511
+
1512
+ const res = await fetch('/api/actions/' + action, {
1513
+ method: 'POST',
1514
+ headers: { 'Content-Type': 'application/json' },
1515
+ body: JSON.stringify(payload),
1516
+ });
1517
+ const data = await res.json();
1518
+
1519
+ resultEl.style.display = 'block';
1520
+ if (data.success) {
1521
+ resultEl.className = 'action-result visible success';
1522
+ resultEl.textContent = data.output || 'Action completed successfully';
1523
+ } else {
1524
+ resultEl.className = 'action-result visible error';
1525
+ resultEl.textContent = data.error || data.output || 'Action failed';
1526
+ }
1527
+ } catch (err) {
1528
+ resultEl.style.display = 'block';
1529
+ resultEl.className = 'action-result visible error';
1530
+ resultEl.textContent = 'Network error: ' + (err.message || err);
1531
+ } finally {
1532
+ btn.classList.remove('loading');
1533
+ btn.disabled = false;
1534
+ btn.textContent = action.charAt(0).toUpperCase() + action.slice(1);
1535
+ }
1536
+ }
1537
+
1538
+ // ========================================================================
1539
+ // Search filter
1540
+ // ========================================================================
1541
+ document.getElementById('skillSearchInput').addEventListener('input', function() {
1542
+ const query = this.value.toLowerCase();
1543
+ document.querySelectorAll('.skill-health-row').forEach(row => {
1544
+ const name = (row.dataset.skill || '').toLowerCase();
1545
+ row.style.display = name.includes(query) ? '' : 'none';
1546
+ });
1547
+ });
1548
+
1549
+ // ========================================================================
1550
+ // Evaluation Feed
1551
+ // ========================================================================
1552
+ function updateDrillEvalFeed(skillName) {
1553
+ const records = state.skills.filter(r => r.skill_name === skillName);
1554
+ const sorted = [...records].sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp));
1555
+ const tbody = document.querySelector('#drillEvalFeed tbody');
1556
+ tbody.innerHTML = sorted.slice(0, 50).map(r => {
1557
+ const triggeredBadge = r.triggered
1558
+ ? '<span class="badge badge-healthy">Yes</span>'
1559
+ : '<span class="badge badge-critical">No</span>';
1560
+ const sourceType = escapeHtml(r.source || r.type || 'implicit');
1561
+ return `<tr>
1562
+ <td class="mono">${escapeHtml(formatTimestamp(r.timestamp))}</td>
1563
+ <td>${escapeHtml(truncate(r.query, 50))}</td>
1564
+ <td>${triggeredBadge}</td>
1565
+ <td class="mono">${sourceType}</td>
1566
+ </tr>`;
1567
+ }).join('') || '<tr><td colspan="4" class="empty-state">No evaluations</td></tr>';
1568
+ }
1569
+
1570
+ // ========================================================================
1571
+ // Invocation Breakdown
1572
+ // ========================================================================
1573
+ function updateDrillInvocationBreakdown(skillName) {
1574
+ const computed = state.computed;
1575
+ const snapshot = computed && computed.snapshots ? computed.snapshots[skillName] : null;
1576
+ const byType = (snapshot && snapshot.by_invocation_type) || {};
1577
+
1578
+ // If no invocation type data, compute from skill records
1579
+ let labels, values;
1580
+ if (Object.keys(byType).length > 0) {
1581
+ labels = Object.keys(byType);
1582
+ values = Object.values(byType);
1583
+ } else {
1584
+ // Fallback: count source/type fields from skill records
1585
+ const records = state.skills.filter(r => r.skill_name === skillName);
1586
+ const counts = {};
1587
+ for (const r of records) {
1588
+ const t = r.source || r.type || 'implicit';
1589
+ counts[t] = (counts[t] || 0) + 1;
1590
+ }
1591
+ labels = Object.keys(counts);
1592
+ values = Object.values(counts);
1593
+ }
1594
+
1595
+ if (charts.invocationBreakdown) charts.invocationBreakdown.destroy();
1596
+
1597
+ if (!labels.length) return;
1598
+
1599
+ charts.invocationBreakdown = new Chart(document.getElementById('chartInvocationBreakdown'), {
1600
+ type: 'doughnut',
1601
+ data: {
1602
+ labels,
1603
+ datasets: [{
1604
+ data: values,
1605
+ backgroundColor: CHART_COLORS.slice(0, labels.length),
1606
+ borderWidth: 1,
1607
+ borderColor: '#fff',
1608
+ }]
1609
+ },
1610
+ options: {
1611
+ responsive: true,
1612
+ maintainAspectRatio: false,
1613
+ plugins: {
1614
+ legend: {
1615
+ position: 'right',
1616
+ labels: {
1617
+ font: { family: "'Poppins', sans-serif", size: 11 },
1618
+ padding: 12,
1619
+ }
1620
+ }
1621
+ }
1622
+ }
1623
+ });
1624
+ }
1625
+
1626
+ // ========================================================================
1627
+ // Time period filtering
1628
+ // ========================================================================
1629
+ function filterByPeriod(records, days) {
1630
+ if (!days || days === 0) return records;
1631
+ // Anchor cutoff to latest timestamp in dataset, not viewer's clock,
1632
+ // so archived/historical datasets filter correctly.
1633
+ const latest = records.reduce((max, r) => {
1634
+ const t = new Date(r.timestamp).getTime();
1635
+ return t > max ? t : max;
1636
+ }, 0);
1637
+ if (!latest) return records;
1638
+ const cutoff = new Date(latest);
1639
+ cutoff.setDate(cutoff.getDate() - days);
1640
+ return records.filter(r => new Date(r.timestamp) >= cutoff);
1641
+ }
1642
+
1643
+ document.getElementById('timePeriodSelector').addEventListener('click', function(e) {
1644
+ const btn = e.target.closest('.period-btn');
1645
+ if (!btn) return;
1646
+ this.querySelectorAll('.period-btn').forEach(b => b.classList.remove('active'));
1647
+ btn.classList.add('active');
1648
+ selectedPeriodDays = parseInt(btn.dataset.days, 10);
1649
+ if (selectedSkill) {
1650
+ updateDrillPassRateChart(selectedSkill);
1651
+ }
1652
+ });
1653
+
1654
+ // Hook into drill-down to show action buttons in live mode
1655
+ const origOpenDrillDown = typeof openDrillDown === 'function' ? openDrillDown : null;
1656
+ openDrillDown = function(skillName) {
1657
+ if (origOpenDrillDown) origOpenDrillDown(skillName);
1658
+ showActionButtons(skillName);
1659
+ };
1660
+
1661
+ function initLiveMode() {
1662
+ if (!isLiveMode()) return;
1663
+ // Show live sections
1664
+ document.getElementById('actionsSection').style.display = 'block';
1665
+
1666
+ startSSE();
1667
+ updateEvolutionTimeline();
1668
+ }
1669
+
1110
1670
  // ========================================================================
1111
1671
  // Init: try loading embedded data
1112
1672
  // ========================================================================
1113
1673
  if (loadEmbeddedData()) {
1114
1674
  refreshAll();
1115
1675
  }
1676
+ initLiveMode();
1116
1677
  </script>
1117
1678
 
1118
1679
  </body>