selftune 0.1.2 → 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 (89) 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 +38 -1
  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 +31 -12
  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 +479 -104
  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 +20 -3
  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 +145 -19
  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/codex-rollout.ts +1 -1
  51. package/cli/selftune/ingestors/openclaw-ingest.ts +440 -0
  52. package/cli/selftune/ingestors/opencode-ingest.ts +2 -2
  53. package/cli/selftune/init.ts +168 -5
  54. package/cli/selftune/last.ts +2 -2
  55. package/cli/selftune/memory/writer.ts +447 -0
  56. package/cli/selftune/monitoring/watch.ts +25 -2
  57. package/cli/selftune/status.ts +18 -15
  58. package/cli/selftune/types.ts +377 -5
  59. package/cli/selftune/utils/frontmatter.ts +217 -0
  60. package/cli/selftune/utils/llm-call.ts +29 -3
  61. package/cli/selftune/utils/transcript.ts +35 -0
  62. package/cli/selftune/utils/trigger-check.ts +89 -0
  63. package/cli/selftune/utils/tui.ts +156 -0
  64. package/dashboard/index.html +585 -19
  65. package/package.json +17 -6
  66. package/skill/SKILL.md +127 -10
  67. package/skill/Workflows/AutoActivation.md +144 -0
  68. package/skill/Workflows/Badge.md +118 -0
  69. package/skill/Workflows/Baseline.md +121 -0
  70. package/skill/Workflows/Composability.md +100 -0
  71. package/skill/Workflows/Contribute.md +91 -0
  72. package/skill/Workflows/Cron.md +155 -0
  73. package/skill/Workflows/Dashboard.md +203 -0
  74. package/skill/Workflows/Doctor.md +37 -1
  75. package/skill/Workflows/Evals.md +73 -5
  76. package/skill/Workflows/EvolutionMemory.md +152 -0
  77. package/skill/Workflows/Evolve.md +111 -6
  78. package/skill/Workflows/EvolveBody.md +159 -0
  79. package/skill/Workflows/ImportSkillsBench.md +111 -0
  80. package/skill/Workflows/Ingest.md +129 -15
  81. package/skill/Workflows/Initialize.md +58 -3
  82. package/skill/Workflows/Replay.md +70 -0
  83. package/skill/Workflows/Rollback.md +20 -1
  84. package/skill/Workflows/UnitTest.md +138 -0
  85. package/skill/Workflows/Watch.md +22 -0
  86. package/skill/settings_snippet.json +23 -0
  87. package/templates/activation-rules-default.json +27 -0
  88. package/templates/multi-skill-settings.json +64 -0
  89. 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,8 +633,17 @@
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
- <span class="version">v0.5</span>
646
+ <span class="version">v0.1.4</span>
425
647
  </div>
426
648
  <div class="status" id="headerStatus">Drop log files to get started</div>
427
649
  </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
@@ -726,6 +992,8 @@ function formatDate(ts) {
726
992
  return d.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
727
993
  }
728
994
 
995
+ function toDayKey(ts) { return new Date(ts).toISOString().slice(0, 10); }
996
+
729
997
  function formatTimestamp(ts) {
730
998
  const d = toDate(ts);
731
999
  return d.toLocaleString('en-US', {
@@ -746,23 +1014,25 @@ function escapeHtml(s) {
746
1014
  function groupByDay(records) {
747
1015
  const map = {};
748
1016
  for (const r of records) {
749
- const day = formatDate(r.timestamp);
1017
+ const day = toDayKey(r.timestamp);
750
1018
  map[day] = (map[day] || 0) + 1;
751
1019
  }
752
1020
  return map;
753
1021
  }
754
1022
 
755
1023
  function getSkillStatus(passRate, regressionDetected) {
756
- if (regressionDetected || passRate < 0.5) return 'regressed';
757
- 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';
758
1027
  return 'healthy';
759
1028
  }
760
1029
 
761
1030
  function getStatusBadge(status) {
762
1031
  const map = {
763
- healthy: '<span class="badge badge-green">Healthy</span>',
764
- drifting: '<span class="badge badge-amber">Drifting</span>',
765
- 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>',
766
1036
  };
767
1037
  return map[status] || '';
768
1038
  }
@@ -783,6 +1053,7 @@ function refreshAll() {
783
1053
  dropZone.style.display = 'none';
784
1054
  document.getElementById('healthSummary').style.display = 'block';
785
1055
  document.getElementById('skillHealthSection').style.display = 'block';
1056
+ document.getElementById('skillSearchInput').style.display = 'block';
786
1057
  }
787
1058
 
788
1059
  updateHeader();
@@ -824,7 +1095,7 @@ function updateHealthSummary() {
824
1095
  document.getElementById('kpi-avg-pass-rate').textContent = (avgPR * 100).toFixed(0) + '%';
825
1096
  const status = getSkillStatus(avgPR, false);
826
1097
  document.getElementById('kpi-pass-rate-sub').textContent =
827
- 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';
828
1099
  }
829
1100
 
830
1101
  document.getElementById('kpi-regressions').textContent = regressions.length;
@@ -906,6 +1177,16 @@ function updateSkillHealthGrid() {
906
1177
  row.addEventListener('click', handler);
907
1178
  row.addEventListener('keydown', e => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); handler(); } });
908
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
+ }
909
1190
  }
910
1191
 
911
1192
  // ========================================================================
@@ -922,6 +1203,8 @@ function openDrillDown(skillName) {
922
1203
  updateDrillMissedQueries(skillName);
923
1204
  updateDrillEvolution(skillName);
924
1205
  updateDrillSessions(skillName);
1206
+ updateDrillEvalFeed(skillName);
1207
+ updateDrillInvocationBreakdown(skillName);
925
1208
  }
926
1209
 
927
1210
  document.getElementById('drillDownClose').addEventListener('click', () => {
@@ -932,27 +1215,29 @@ document.getElementById('drillDownClose').addEventListener('click', () => {
932
1215
 
933
1216
  function updateDrillPassRateChart(skillName) {
934
1217
  // Group skill records by day and compute daily pass rate
935
- 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);
936
1220
  const byDay = {};
937
1221
  for (const r of records) {
938
- const day = formatDate(r.timestamp);
1222
+ const day = toDayKey(r.timestamp);
939
1223
  if (!byDay[day]) byDay[day] = { triggered: 0, total: 0 };
940
1224
  byDay[day].total++;
941
1225
  if (r.triggered) byDay[day].triggered++;
942
1226
  }
943
1227
 
944
- const labels = Object.keys(byDay);
945
- const data = labels.map(d => ((byDay[d].triggered / byDay[d].total) * 100).toFixed(1));
1228
+ const dayKeys = Object.keys(byDay).sort();
1229
+ const labels = dayKeys.map(d => formatDate(d + "T00:00:00Z"));
1230
+ const data = dayKeys.map(d => ((byDay[d].triggered / byDay[d].total) * 100).toFixed(1));
946
1231
 
947
1232
  // Deploy events as annotations
948
1233
  const deployDays = new Set(
949
1234
  state.evolution
950
1235
  .filter(e => e.action === 'deployed' && (e.details || '').toLowerCase().includes(skillName.toLowerCase()))
951
- .map(e => formatDate(e.timestamp))
1236
+ .map(e => toDayKey(e.timestamp))
952
1237
  );
953
1238
 
954
- const pointColors = labels.map(d => deployDays.has(d) ? '#d97757' : '#788c5d');
955
- const pointSizes = labels.map(d => deployDays.has(d) ? 8 : 3);
1239
+ const pointColors = dayKeys.map(d => deployDays.has(d) ? '#d97757' : '#788c5d');
1240
+ const pointSizes = dayKeys.map(d => deployDays.has(d) ? 8 : 3);
956
1241
 
957
1242
  if (charts.drillPassRate) charts.drillPassRate.destroy();
958
1243
  charts.drillPassRate = new Chart(document.getElementById('chartDrillPassRate'), {
@@ -1023,12 +1308,14 @@ function updateDrillSessions(skillName) {
1023
1308
  const sorted = [...sessions].sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp));
1024
1309
  tbody.innerHTML = sorted.slice(0, 30).map(r => {
1025
1310
  const skills = (r.skills_triggered || []).join(', ') || '\u2014';
1026
- const errorBadge = r.errors_encountered > 0
1027
- ? `<span class="badge badge-red">${r.errors_encountered}</span>`
1311
+ const errorCount = Number.isFinite(Number(r.errors_encountered)) ? Number(r.errors_encountered) : 0;
1312
+ const totalToolCalls = Number.isFinite(Number(r.total_tool_calls)) ? Number(r.total_tool_calls) : 0;
1313
+ const errorBadge = errorCount > 0
1314
+ ? `<span class="badge badge-red">${errorCount}</span>`
1028
1315
  : '<span class="badge badge-green">0</span>';
1029
1316
  return `<tr>
1030
1317
  <td class="mono">${escapeHtml(formatTimestamp(r.timestamp))}</td>
1031
- <td>${r.total_tool_calls || 0}</td>
1318
+ <td>${totalToolCalls}</td>
1032
1319
  <td>${escapeHtml(truncate(skills, 30))}</td>
1033
1320
  <td>${errorBadge}</td>
1034
1321
  </tr>`;
@@ -1102,12 +1389,291 @@ document.getElementById('exportCsvBtn').addEventListener('click', () => {
1102
1389
  a.click();
1103
1390
  });
1104
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
+
1105
1670
  // ========================================================================
1106
1671
  // Init: try loading embedded data
1107
1672
  // ========================================================================
1108
1673
  if (loadEmbeddedData()) {
1109
1674
  refreshAll();
1110
1675
  }
1676
+ initLiveMode();
1111
1677
  </script>
1112
1678
 
1113
1679
  </body>