selftune 0.2.23 → 0.2.25

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 (219) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/README.md +93 -15
  3. package/apps/local-dashboard/dist/assets/index-DgY2KGP-.css +1 -0
  4. package/apps/local-dashboard/dist/assets/index-Dhgv5BQO.js +15 -0
  5. package/apps/local-dashboard/dist/assets/vendor-react-C5oyHiV1.js +11 -0
  6. package/apps/local-dashboard/dist/assets/{vendor-table-BIiI3YhS.js → vendor-table-Bc_bbKd8.js} +1 -1
  7. package/apps/local-dashboard/dist/assets/vendor-ui-B3BPIYy7.js +1 -0
  8. package/apps/local-dashboard/dist/index.html +5 -5
  9. package/cli/selftune/adapters/codex/install.ts +310 -78
  10. package/cli/selftune/adapters/opencode/install.ts +3 -4
  11. package/cli/selftune/alpha-upload/build-payloads.ts +3 -3
  12. package/cli/selftune/alpha-upload/stage-canonical.ts +17 -11
  13. package/cli/selftune/auto-update.ts +200 -8
  14. package/cli/selftune/canonical-export.ts +55 -25
  15. package/cli/selftune/command-surface.ts +397 -0
  16. package/cli/selftune/contribute/contribute.ts +64 -13
  17. package/cli/selftune/contribution-config.ts +57 -3
  18. package/cli/selftune/contribution-preferences.ts +117 -0
  19. package/cli/selftune/contribution-signals.ts +8 -4
  20. package/cli/selftune/contribution-staging.ts +13 -2
  21. package/cli/selftune/contributions.ts +55 -121
  22. package/cli/selftune/creator-contributions.ts +29 -10
  23. package/cli/selftune/cron/setup.ts +7 -3
  24. package/cli/selftune/dashboard-contract.ts +73 -0
  25. package/cli/selftune/dashboard-server.ts +168 -17
  26. package/cli/selftune/dashboard.ts +350 -17
  27. package/cli/selftune/eval/baseline.ts +21 -5
  28. package/cli/selftune/eval/execution-eval.ts +170 -0
  29. package/cli/selftune/eval/family-overlap.ts +2 -2
  30. package/cli/selftune/eval/hooks-to-evals.ts +228 -82
  31. package/cli/selftune/eval/import-skillsbench.ts +2 -2
  32. package/cli/selftune/eval/invocation-classifier.ts +56 -0
  33. package/cli/selftune/eval/synthetic-evals.ts +5 -3
  34. package/cli/selftune/eval/unit-test-cli.ts +7 -4
  35. package/cli/selftune/evolution/apply-proposal.ts +295 -0
  36. package/cli/selftune/evolution/engines/replay-engine.ts +79 -57
  37. package/cli/selftune/evolution/evolve-body.ts +100 -39
  38. package/cli/selftune/evolution/evolve.ts +244 -52
  39. package/cli/selftune/evolution/rollback.ts +0 -1
  40. package/cli/selftune/evolution/validate-body.ts +68 -42
  41. package/cli/selftune/evolution/validate-host-replay.ts +510 -60
  42. package/cli/selftune/evolution/validate-proposal.ts +11 -150
  43. package/cli/selftune/evolution/validate-routing.ts +43 -41
  44. package/cli/selftune/evolution/validation-contract.ts +91 -0
  45. package/cli/selftune/grading/auto-grade.ts +11 -7
  46. package/cli/selftune/grading/grade-session.ts +10 -16
  47. package/cli/selftune/index.ts +35 -10
  48. package/cli/selftune/ingestors/claude-replay.ts +15 -10
  49. package/cli/selftune/ingestors/codex-wrapper.ts +3 -3
  50. package/cli/selftune/ingestors/opencode-ingest.ts +2 -2
  51. package/cli/selftune/ingestors/pi-ingest.ts +3 -2
  52. package/cli/selftune/init.ts +27 -3
  53. package/cli/selftune/localdb/direct-write.ts +35 -1
  54. package/cli/selftune/localdb/queries/cron.ts +34 -0
  55. package/cli/selftune/localdb/queries/dashboard.ts +834 -0
  56. package/cli/selftune/localdb/queries/evolution.ts +158 -0
  57. package/cli/selftune/localdb/queries/execution.ts +133 -0
  58. package/cli/selftune/localdb/queries/json.ts +18 -0
  59. package/cli/selftune/localdb/queries/monitoring.ts +263 -0
  60. package/cli/selftune/localdb/queries/raw.ts +95 -0
  61. package/cli/selftune/localdb/queries/staging.ts +270 -0
  62. package/cli/selftune/localdb/queries/trust.ts +392 -0
  63. package/cli/selftune/localdb/queries.ts +60 -2288
  64. package/cli/selftune/localdb/schema.ts +21 -0
  65. package/cli/selftune/monitoring/watch.ts +96 -29
  66. package/cli/selftune/normalization.ts +3 -0
  67. package/cli/selftune/observability.ts +4 -2
  68. package/cli/selftune/orchestrate/cli.ts +161 -0
  69. package/cli/selftune/orchestrate/execute.ts +295 -0
  70. package/cli/selftune/orchestrate/finalize.ts +157 -0
  71. package/cli/selftune/orchestrate/locks.ts +40 -0
  72. package/cli/selftune/orchestrate/plan.ts +131 -0
  73. package/cli/selftune/orchestrate/post-run.ts +59 -0
  74. package/cli/selftune/orchestrate/prepare.ts +334 -0
  75. package/cli/selftune/orchestrate/report.ts +182 -0
  76. package/cli/selftune/orchestrate/runtime.ts +120 -0
  77. package/cli/selftune/orchestrate/signals.ts +48 -0
  78. package/cli/selftune/orchestrate.ts +150 -1173
  79. package/cli/selftune/repair/skill-usage.ts +5 -2
  80. package/cli/selftune/routes/overview.ts +5 -2
  81. package/cli/selftune/routes/skill-report.ts +15 -2
  82. package/cli/selftune/schedule.ts +5 -5
  83. package/cli/selftune/status.ts +39 -2
  84. package/cli/selftune/testing-readiness.ts +597 -0
  85. package/cli/selftune/types.ts +44 -4
  86. package/cli/selftune/uninstall.ts +2 -1
  87. package/cli/selftune/utils/canonical-log.ts +1 -9
  88. package/cli/selftune/utils/cli-error.ts +9 -0
  89. package/cli/selftune/utils/llm-call.ts +126 -6
  90. package/cli/selftune/utils/skill-discovery.ts +2 -0
  91. package/cli/selftune/workflows/proposals.ts +184 -0
  92. package/cli/selftune/workflows/skill-scaffold.ts +241 -0
  93. package/cli/selftune/workflows/workflows.ts +100 -26
  94. package/node_modules/@selftune/telemetry-contract/fixtures/complete-push.ts +1 -1
  95. package/node_modules/@selftune/telemetry-contract/fixtures/evidence-only-push.ts +1 -1
  96. package/node_modules/@selftune/telemetry-contract/fixtures/partial-push-no-sessions.ts +1 -1
  97. package/node_modules/@selftune/telemetry-contract/fixtures/partial-push-unresolved-parents.ts +1 -1
  98. package/node_modules/@selftune/telemetry-contract/src/schemas.ts +41 -1
  99. package/node_modules/@selftune/telemetry-contract/src/types.ts +103 -2
  100. package/package.json +25 -9
  101. package/packages/dashboard-core/AGENTS.md +18 -0
  102. package/packages/dashboard-core/README.md +30 -0
  103. package/packages/dashboard-core/index.ts +3 -0
  104. package/packages/dashboard-core/package.json +39 -0
  105. package/packages/dashboard-core/src/chrome/DashboardChrome.tsx +74 -0
  106. package/packages/dashboard-core/src/chrome/DashboardHeader.tsx +200 -0
  107. package/packages/dashboard-core/src/chrome/DashboardSidebar.tsx +219 -0
  108. package/packages/dashboard-core/src/chrome/RuntimeBadge.tsx +46 -0
  109. package/packages/dashboard-core/src/chrome/index.ts +14 -0
  110. package/packages/dashboard-core/src/chrome/types.ts +81 -0
  111. package/packages/dashboard-core/src/chrome/utils.ts +23 -0
  112. package/packages/dashboard-core/src/gates/FeatureGate.tsx +11 -0
  113. package/packages/dashboard-core/src/gates/LockedRoute.tsx +29 -0
  114. package/packages/dashboard-core/src/gates/UpgradeCard.tsx +89 -0
  115. package/packages/dashboard-core/src/gates/index.ts +3 -0
  116. package/packages/dashboard-core/src/host/DashboardHostProvider.tsx +62 -0
  117. package/packages/dashboard-core/src/host/adapter.ts +47 -0
  118. package/packages/dashboard-core/src/host/capabilities.ts +55 -0
  119. package/packages/dashboard-core/src/host/index.ts +3 -0
  120. package/packages/dashboard-core/src/models/analytics.ts +39 -0
  121. package/packages/dashboard-core/src/models/index.ts +4 -0
  122. package/packages/dashboard-core/src/models/overview.ts +98 -0
  123. package/packages/dashboard-core/src/models/runtime.ts +7 -0
  124. package/packages/dashboard-core/src/models/skills.ts +34 -0
  125. package/packages/dashboard-core/src/routes/index.ts +2 -0
  126. package/packages/dashboard-core/src/routes/manifest.test.ts +70 -0
  127. package/packages/dashboard-core/src/routes/manifest.ts +451 -0
  128. package/packages/dashboard-core/src/routes/types.ts +39 -0
  129. package/packages/dashboard-core/src/screens/analytics/AnalyticsScreen.tsx +278 -0
  130. package/packages/dashboard-core/src/screens/analytics/index.ts +1 -0
  131. package/packages/dashboard-core/src/screens/index.ts +37 -0
  132. package/packages/dashboard-core/src/screens/overview/OverviewComparisonSurface.test.ts +101 -0
  133. package/packages/dashboard-core/src/screens/overview/OverviewComparisonSurface.tsx +393 -0
  134. package/packages/dashboard-core/src/screens/overview/OverviewCompositionSurface.test.tsx +113 -0
  135. package/packages/dashboard-core/src/screens/overview/OverviewCompositionSurface.tsx +72 -0
  136. package/packages/dashboard-core/src/screens/overview/OverviewCoreSurface.tsx +71 -0
  137. package/packages/dashboard-core/src/screens/overview/OverviewOnboardingBanner.tsx +90 -0
  138. package/packages/dashboard-core/src/screens/overview/OverviewRunSummary.tsx +40 -0
  139. package/packages/dashboard-core/src/screens/overview/index.ts +16 -0
  140. package/packages/dashboard-core/src/screens/overview/types.ts +13 -0
  141. package/packages/dashboard-core/src/screens/skill-report/SkillReportDailyBreakdownSection.tsx +99 -0
  142. package/packages/dashboard-core/src/screens/skill-report/SkillReportDataQualityTabContent.tsx +35 -0
  143. package/packages/dashboard-core/src/screens/skill-report/SkillReportEvidenceRail.tsx +71 -0
  144. package/packages/dashboard-core/src/screens/skill-report/SkillReportEvidenceSection.tsx +63 -0
  145. package/packages/dashboard-core/src/screens/skill-report/SkillReportEvidenceTabContent.tsx +25 -0
  146. package/packages/dashboard-core/src/screens/skill-report/SkillReportInvocationsSection.tsx +24 -0
  147. package/packages/dashboard-core/src/screens/skill-report/SkillReportMissedQueriesSection.tsx +79 -0
  148. package/packages/dashboard-core/src/screens/skill-report/SkillReportScaffold.tsx +150 -0
  149. package/packages/dashboard-core/src/screens/skill-report/SkillReportSections.test.tsx +224 -0
  150. package/packages/dashboard-core/src/screens/skill-report/SkillReportTabs.test.tsx +76 -0
  151. package/packages/dashboard-core/src/screens/skill-report/SkillReportTabs.tsx +88 -0
  152. package/packages/dashboard-core/src/screens/skill-report/SkillReportTrendSection.tsx +33 -0
  153. package/packages/dashboard-core/src/screens/skill-report/SkillReportTrustBadge.tsx +67 -0
  154. package/packages/dashboard-core/src/screens/skill-report/index.ts +45 -0
  155. package/packages/dashboard-core/src/screens/skills/SkillsLibraryScreen.tsx +162 -0
  156. package/packages/dashboard-core/src/screens/skills/index.ts +6 -0
  157. package/packages/telemetry-contract/fixtures/complete-push.ts +1 -1
  158. package/packages/telemetry-contract/fixtures/evidence-only-push.ts +1 -1
  159. package/packages/telemetry-contract/fixtures/partial-push-no-sessions.ts +1 -1
  160. package/packages/telemetry-contract/fixtures/partial-push-unresolved-parents.ts +1 -1
  161. package/packages/telemetry-contract/src/schemas.ts +41 -1
  162. package/packages/telemetry-contract/src/types.ts +103 -2
  163. package/packages/ui/src/components/EvidenceViewer.tsx +80 -25
  164. package/packages/ui/src/components/OverviewPanels.tsx +67 -26
  165. package/packages/ui/src/primitives/tabs.tsx +7 -6
  166. package/packages/ui/src/types.ts +10 -0
  167. package/skill/SKILL.md +130 -332
  168. package/skill/agents/diagnosis-analyst.md +3 -3
  169. package/skill/agents/evolution-reviewer.md +3 -3
  170. package/skill/agents/integration-guide.md +3 -3
  171. package/skill/agents/pattern-analyst.md +2 -2
  172. package/skill/references/cli-quick-reference.md +89 -0
  173. package/skill/references/creator-playbook.md +131 -0
  174. package/skill/references/examples.md +48 -0
  175. package/skill/references/troubleshooting.md +47 -0
  176. package/skill/references/version-history.md +1 -1
  177. package/skill/selftune.contribute.json +11 -0
  178. package/skill/{Workflows → workflows}/Baseline.md +20 -1
  179. package/skill/{Workflows → workflows}/Contribute.md +23 -10
  180. package/skill/{Workflows → workflows}/Contributions.md +13 -5
  181. package/skill/workflows/CreateTestDeploy.md +170 -0
  182. package/skill/{Workflows → workflows}/CreatorContributions.md +18 -6
  183. package/skill/{Workflows → workflows}/Cron.md +1 -1
  184. package/skill/{Workflows → workflows}/Dashboard.md +20 -0
  185. package/skill/{Workflows → workflows}/Doctor.md +1 -1
  186. package/skill/{Workflows → workflows}/Evals.md +67 -2
  187. package/skill/{Workflows → workflows}/Evolve.md +119 -30
  188. package/skill/{Workflows → workflows}/EvolveBody.md +41 -1
  189. package/skill/{Workflows → workflows}/Grade.md +1 -1
  190. package/skill/{Workflows → workflows}/Initialize.md +8 -4
  191. package/skill/{Workflows → workflows}/Orchestrate.md +13 -3
  192. package/skill/{Workflows → workflows}/Schedule.md +3 -3
  193. package/skill/workflows/SignalsDashboard.md +87 -0
  194. package/skill/{Workflows → workflows}/UnitTest.md +19 -0
  195. package/skill/{Workflows → workflows}/Watch.md +42 -2
  196. package/skill/{Workflows → workflows}/Workflows.md +39 -2
  197. package/apps/local-dashboard/dist/assets/index-CwOtTrUS.css +0 -1
  198. package/apps/local-dashboard/dist/assets/index-f1HQpbeH.js +0 -59
  199. package/apps/local-dashboard/dist/assets/vendor-react-CKkiCskZ.js +0 -11
  200. package/apps/local-dashboard/dist/assets/vendor-ui-jVSaIZey.js +0 -12
  201. /package/skill/{Workflows → workflows}/AlphaUpload.md +0 -0
  202. /package/skill/{Workflows → workflows}/AutoActivation.md +0 -0
  203. /package/skill/{Workflows → workflows}/Badge.md +0 -0
  204. /package/skill/{Workflows → workflows}/Composability.md +0 -0
  205. /package/skill/{Workflows → workflows}/EvolutionMemory.md +0 -0
  206. /package/skill/{Workflows → workflows}/ExportCanonical.md +0 -0
  207. /package/skill/{Workflows → workflows}/Hook.md +0 -0
  208. /package/skill/{Workflows → workflows}/ImportSkillsBench.md +0 -0
  209. /package/skill/{Workflows → workflows}/Ingest.md +0 -0
  210. /package/skill/{Workflows → workflows}/PlatformHooks.md +0 -0
  211. /package/skill/{Workflows → workflows}/Quickstart.md +0 -0
  212. /package/skill/{Workflows → workflows}/Recover.md +0 -0
  213. /package/skill/{Workflows → workflows}/Registry.md +0 -0
  214. /package/skill/{Workflows → workflows}/RepairSkillUsage.md +0 -0
  215. /package/skill/{Workflows → workflows}/Replay.md +0 -0
  216. /package/skill/{Workflows → workflows}/Rollback.md +0 -0
  217. /package/skill/{Workflows → workflows}/Sync.md +0 -0
  218. /package/skill/{Workflows → workflows}/Telemetry.md +0 -0
  219. /package/skill/{Workflows → workflows}/Uninstall.md +0 -0
@@ -0,0 +1,393 @@
1
+ "use client";
2
+
3
+ import type { ReactNode } from "react";
4
+ import { useEffect, useMemo, useRef, useState } from "react";
5
+ import { EyeIcon } from "lucide-react";
6
+
7
+ import {
8
+ Badge,
9
+ Button,
10
+ Card,
11
+ CardAction,
12
+ CardContent,
13
+ CardDescription,
14
+ CardHeader,
15
+ Tabs,
16
+ TabsList,
17
+ TabsTrigger,
18
+ } from "@selftune/ui/primitives";
19
+ import { timeAgo } from "@selftune/ui/lib";
20
+ import type { TrustBucket } from "@selftune/ui/types";
21
+
22
+ import { useOptionalDashboardHostAdapter } from "../../host/index";
23
+ import type { OverviewComparisonRow } from "./types";
24
+
25
+ const BUCKET_ORDER: TrustBucket[] = ["at_risk", "improving", "uncertain", "stable"];
26
+
27
+ const BUCKET_CFG: Record<TrustBucket, { label: string; accent: string; dot: string }> = {
28
+ at_risk: { label: "At Risk", accent: "text-red-400", dot: "bg-red-400" },
29
+ improving: { label: "Improving", accent: "text-primary", dot: "bg-primary" },
30
+ uncertain: { label: "Uncertain", accent: "text-amber-400", dot: "bg-amber-400" },
31
+ stable: {
32
+ label: "Stable",
33
+ accent: "text-muted-foreground",
34
+ dot: "bg-muted-foreground/60",
35
+ },
36
+ };
37
+
38
+ export interface OverviewComparisonWatchlistConfig {
39
+ initialSkills: string[];
40
+ onChange?(skills: string[]): Promise<string[] | void>;
41
+ emptyMessage?: ReactNode;
42
+ }
43
+
44
+ export interface OverviewComparisonSurfaceProps {
45
+ rows: OverviewComparisonRow[];
46
+ renderSkillLink?: (skillName: string) => ReactNode;
47
+ libraryAction?: ReactNode;
48
+ watchlist?: OverviewComparisonWatchlistConfig;
49
+ }
50
+
51
+ export function resolveOverviewWatchlistChange(
52
+ watchlist: OverviewComparisonWatchlistConfig | undefined,
53
+ hostAdapter: ReturnType<typeof useOptionalDashboardHostAdapter>,
54
+ ) {
55
+ return watchlist?.onChange ?? hostAdapter?.actions.updateOverviewWatchlist;
56
+ }
57
+
58
+ export function resolveOverviewWatchlistLoad(
59
+ hostAdapter: ReturnType<typeof useOptionalDashboardHostAdapter>,
60
+ ) {
61
+ return hostAdapter?.actions.getOverviewWatchlist;
62
+ }
63
+
64
+ export function getOverviewWatchlistSyncKey(initialSkills: string[] | undefined): string {
65
+ return JSON.stringify(initialSkills ?? []);
66
+ }
67
+
68
+ function parseOverviewWatchlistSyncKey(syncKey: string): string[] {
69
+ return JSON.parse(syncKey) as string[];
70
+ }
71
+
72
+ function formatEvolutionAction(action: string): string {
73
+ switch (action) {
74
+ case "created":
75
+ return "Proposal created";
76
+ case "validated":
77
+ return "Validated";
78
+ case "deployed":
79
+ return "Deployed";
80
+ case "rolled_back":
81
+ return "Rolled back";
82
+ case "watch":
83
+ return "Watching";
84
+ case "rejected":
85
+ return "Rejected";
86
+ default:
87
+ return action.replace(/_/g, " ");
88
+ }
89
+ }
90
+
91
+ function deriveDefaultWatchlist(rows: OverviewComparisonRow[], limit = 10): string[] {
92
+ return rows
93
+ .toSorted((a, b) => {
94
+ const aRank = BUCKET_ORDER.indexOf(a.bucket);
95
+ const bRank = BUCKET_ORDER.indexOf(b.bucket);
96
+ if (aRank !== bRank) return aRank - bRank;
97
+ return (b.sortTimestamp ?? "").localeCompare(a.sortTimestamp ?? "");
98
+ })
99
+ .slice(0, limit)
100
+ .map((row) => row.skillName);
101
+ }
102
+
103
+ export function OverviewComparisonSurface({
104
+ rows,
105
+ renderSkillLink,
106
+ libraryAction,
107
+ watchlist,
108
+ }: OverviewComparisonSurfaceProps) {
109
+ const hostAdapter = useOptionalDashboardHostAdapter();
110
+ const interactive = Boolean(watchlist);
111
+ const watchlistInitialSkills = watchlist?.initialSkills ?? [];
112
+ const watchlistSyncKey = getOverviewWatchlistSyncKey(watchlistInitialSkills);
113
+ const [viewMode, setViewMode] = useState<"watched" | "all">(interactive ? "watched" : "all");
114
+ const [watchedSkills, setWatchedSkills] = useState<string[]>(() =>
115
+ parseOverviewWatchlistSyncKey(watchlistSyncKey),
116
+ );
117
+ const watchlistRequestSeq = useRef(0);
118
+ const watchlistLoadSeq = useRef(0);
119
+ const loadWatchlist = resolveOverviewWatchlistLoad(hostAdapter);
120
+ const onWatchlistChange = resolveOverviewWatchlistChange(watchlist, hostAdapter);
121
+
122
+ useEffect(() => {
123
+ if (!interactive) return;
124
+ setWatchedSkills(parseOverviewWatchlistSyncKey(watchlistSyncKey));
125
+ }, [interactive, watchlistSyncKey]);
126
+
127
+ useEffect(() => {
128
+ if (!interactive || !loadWatchlist || watchlistInitialSkills.length > 0) return;
129
+
130
+ const requestSeq = watchlistLoadSeq.current + 1;
131
+ watchlistLoadSeq.current = requestSeq;
132
+ let cancelled = false;
133
+
134
+ Promise.resolve(loadWatchlist())
135
+ .then((result) => {
136
+ if (cancelled || watchlistLoadSeq.current !== requestSeq) return;
137
+ if (Array.isArray(result) && result.every((value) => typeof value === "string")) {
138
+ setWatchedSkills(result);
139
+ }
140
+ return undefined;
141
+ })
142
+ .catch(() => {
143
+ /* keep default watchlist fallback */
144
+ });
145
+
146
+ return () => {
147
+ cancelled = true;
148
+ };
149
+ }, [interactive, loadWatchlist, watchlistInitialSkills.length]);
150
+
151
+ const orderedRows = useMemo(() => {
152
+ return rows.toSorted((a, b) => {
153
+ const aRank = BUCKET_ORDER.indexOf(a.bucket);
154
+ const bRank = BUCKET_ORDER.indexOf(b.bucket);
155
+ if (aRank !== bRank) return aRank - bRank;
156
+ return (b.sortTimestamp ?? "").localeCompare(a.sortTimestamp ?? "");
157
+ });
158
+ }, [rows]);
159
+
160
+ const effectiveWatchlist = useMemo(() => {
161
+ if (!watchlist) return [];
162
+ const available = new Set(rows.map((row) => row.skillName));
163
+ const cleaned = watchedSkills.filter((skill) => available.has(skill));
164
+ if (cleaned.length > 0) return cleaned;
165
+ return deriveDefaultWatchlist(rows);
166
+ }, [rows, watchedSkills, watchlist]);
167
+
168
+ const visibleRows = useMemo(() => {
169
+ if (!watchlist || viewMode === "all") return orderedRows;
170
+ const watched = new Set(effectiveWatchlist);
171
+ return orderedRows.filter((row) => watched.has(row.skillName));
172
+ }, [effectiveWatchlist, orderedRows, viewMode, watchlist]);
173
+
174
+ const toggleWatched = async (skillName: string) => {
175
+ if (!watchlist) return;
176
+
177
+ const next = effectiveWatchlist.includes(skillName)
178
+ ? effectiveWatchlist.filter((name) => name !== skillName)
179
+ : [...effectiveWatchlist, skillName];
180
+
181
+ if (!onWatchlistChange) {
182
+ setWatchedSkills(next);
183
+ return;
184
+ }
185
+
186
+ const previous = effectiveWatchlist;
187
+ const requestSeq = watchlistRequestSeq.current + 1;
188
+ watchlistRequestSeq.current = requestSeq;
189
+ setWatchedSkills(next);
190
+
191
+ try {
192
+ const result = await onWatchlistChange(next);
193
+ if (
194
+ watchlistRequestSeq.current === requestSeq &&
195
+ Array.isArray(result) &&
196
+ result.every((value) => typeof value === "string")
197
+ ) {
198
+ setWatchedSkills(result);
199
+ }
200
+ } catch {
201
+ if (watchlistRequestSeq.current === requestSeq) {
202
+ setWatchedSkills(previous);
203
+ }
204
+ }
205
+ };
206
+
207
+ return (
208
+ <Card className="col-span-12 border-none bg-muted shadow-none py-0">
209
+ <CardHeader className="px-5 pt-5 pb-0">
210
+ <div>
211
+ <p className="font-headline text-[10px] uppercase tracking-[0.2em] text-muted-foreground">
212
+ Skill Comparison
213
+ </p>
214
+ <CardDescription className="mt-1 text-[13px]">
215
+ Compare skill performance before drilling into the details.
216
+ </CardDescription>
217
+ </div>
218
+ {(interactive || libraryAction) && (
219
+ <CardAction>
220
+ <div className="flex items-center gap-3">
221
+ {interactive ? (
222
+ <Tabs
223
+ value={viewMode}
224
+ onValueChange={(value) => setViewMode(value as "watched" | "all")}
225
+ >
226
+ <TabsList variant="line" className="h-auto gap-2">
227
+ <TabsTrigger
228
+ value="watched"
229
+ className="font-headline text-[10px] uppercase tracking-[0.18em]"
230
+ >
231
+ Watched
232
+ <span className="ml-1.5 text-muted-foreground">
233
+ {effectiveWatchlist.length}
234
+ </span>
235
+ </TabsTrigger>
236
+ <TabsTrigger
237
+ value="all"
238
+ className="font-headline text-[10px] uppercase tracking-[0.18em]"
239
+ >
240
+ All Skills
241
+ <span className="ml-1.5 text-muted-foreground">{rows.length}</span>
242
+ </TabsTrigger>
243
+ </TabsList>
244
+ </Tabs>
245
+ ) : null}
246
+ {libraryAction}
247
+ </div>
248
+ </CardAction>
249
+ )}
250
+ </CardHeader>
251
+
252
+ <CardContent className="themed-scroll overflow-x-auto px-5 py-5">
253
+ <div
254
+ data-parity-root="overview-comparison-grid"
255
+ className={interactive ? "min-w-[780px]" : "min-w-[680px]"}
256
+ >
257
+ {interactive ? (
258
+ <div className="mb-3 rounded-xl bg-background/35 px-3 py-2 text-xs text-muted-foreground">
259
+ {viewMode === "watched"
260
+ ? "Your watched skills stay pinned here. Add or remove them directly from the grid."
261
+ : "All installed skills, sorted by current trust priority."}
262
+ </div>
263
+ ) : null}
264
+
265
+ <div
266
+ className={`grid gap-3 px-3 pb-2 text-[10px] uppercase tracking-[0.18em] text-muted-foreground ${
267
+ interactive
268
+ ? "grid-cols-[minmax(220px,2.3fr)_0.95fr_1.1fr_0.9fr_1.3fr_1fr_0.9fr]"
269
+ : "grid-cols-[minmax(220px,2.3fr)_0.95fr_1.1fr_0.9fr_1.3fr_1fr]"
270
+ }`}
271
+ >
272
+ <span>Skill</span>
273
+ <span>Trigger Rate</span>
274
+ <span>Routing Conf.</span>
275
+ <span>Sessions</span>
276
+ <span>Last Evolution</span>
277
+ <span>Status</span>
278
+ {interactive ? <span className="text-right">Watch</span> : null}
279
+ </div>
280
+
281
+ <div className="space-y-1.5">
282
+ {visibleRows.map((row) => {
283
+ const bucketCfg = BUCKET_CFG[row.bucket];
284
+ const isWatched = effectiveWatchlist.includes(row.skillName);
285
+
286
+ return (
287
+ <div
288
+ key={row.skillName}
289
+ className={`grid items-center gap-3 rounded-xl bg-background/35 px-3 py-3 text-sm transition-colors hover:bg-background/50 ${
290
+ interactive
291
+ ? "grid-cols-[minmax(220px,2.3fr)_0.95fr_1.1fr_0.9fr_1.3fr_1fr_0.9fr]"
292
+ : "grid-cols-[minmax(220px,2.3fr)_0.95fr_1.1fr_0.9fr_1.3fr_1fr]"
293
+ }`}
294
+ >
295
+ <div className="min-w-0">
296
+ {renderSkillLink ? (
297
+ renderSkillLink(row.skillName)
298
+ ) : (
299
+ <p className="truncate font-medium">{row.skillName}</p>
300
+ )}
301
+ {row.subtext ? (
302
+ <p className="truncate text-xs text-muted-foreground">{row.subtext}</p>
303
+ ) : null}
304
+ </div>
305
+
306
+ <div className="font-medium">
307
+ {row.triggerRate != null ? `${Math.round(row.triggerRate * 100)}%` : "—"}
308
+ </div>
309
+
310
+ <div className="min-w-0">
311
+ {row.routingConfidence != null && row.confidenceCoverage >= 0.5 ? (
312
+ <>
313
+ <p className="text-sm font-medium">
314
+ {Math.round(row.routingConfidence * 100)}%
315
+ </p>
316
+ <p className="truncate text-xs text-muted-foreground">
317
+ {Math.round(row.confidenceCoverage * 100)}% coverage
318
+ </p>
319
+ </>
320
+ ) : (
321
+ <>
322
+ <p className="text-sm font-medium">—</p>
323
+ <p className="truncate text-xs text-muted-foreground">Low coverage</p>
324
+ </>
325
+ )}
326
+ </div>
327
+
328
+ <div className="text-muted-foreground">{row.sessions}</div>
329
+
330
+ <div className="min-w-0">
331
+ {row.lastEvolution ? (
332
+ <>
333
+ <p className="truncate text-sm">
334
+ {formatEvolutionAction(row.lastEvolution.action)}
335
+ </p>
336
+ <p className="text-xs text-muted-foreground">
337
+ {timeAgo(row.lastEvolution.timestamp)}
338
+ </p>
339
+ </>
340
+ ) : (
341
+ <span className="text-xs text-muted-foreground">No evolutions yet</span>
342
+ )}
343
+ </div>
344
+
345
+ <div>
346
+ <Badge
347
+ variant="outline"
348
+ className={`border-transparent ${bucketCfg.accent} bg-background/55`}
349
+ >
350
+ <span
351
+ className={`mr-1.5 inline-block size-1.5 rounded-full ${bucketCfg.dot}`}
352
+ />
353
+ {bucketCfg.label}
354
+ </Badge>
355
+ </div>
356
+
357
+ {interactive ? (
358
+ <div className="flex justify-end">
359
+ <Button
360
+ type="button"
361
+ variant={isWatched ? "secondary" : "ghost"}
362
+ size="sm"
363
+ className="h-8 gap-1.5 px-2 text-xs"
364
+ onClick={() => void toggleWatched(row.skillName)}
365
+ >
366
+ <EyeIcon className="size-3.5" />
367
+ {isWatched ? "Watching" : "Watch"}
368
+ </Button>
369
+ </div>
370
+ ) : null}
371
+ </div>
372
+ );
373
+ })}
374
+
375
+ {visibleRows.length === 0 ? (
376
+ <div className="rounded-xl bg-background/30 px-3 py-6 text-sm text-muted-foreground">
377
+ {interactive
378
+ ? (watchlist?.emptyMessage ?? (
379
+ <>
380
+ No watched skills yet. Switch to{" "}
381
+ <span className="font-medium text-foreground">All Skills</span> and add the
382
+ ones you want to track closely.
383
+ </>
384
+ ))
385
+ : "No skills available."}
386
+ </div>
387
+ ) : null}
388
+ </div>
389
+ </div>
390
+ </CardContent>
391
+ </Card>
392
+ );
393
+ }
@@ -0,0 +1,113 @@
1
+ import type { ReactNode } from "react";
2
+ import { renderToStaticMarkup } from "react-dom/server";
3
+ import { describe, expect, it, vi } from "vitest";
4
+
5
+ vi.mock("./OverviewCoreSurface", () => ({
6
+ OverviewCoreSurface: ({
7
+ beforeHero,
8
+ betweenHeroAndFeed,
9
+ afterFeed,
10
+ }: {
11
+ beforeHero?: ReactNode;
12
+ betweenHeroAndFeed?: ReactNode;
13
+ afterFeed?: ReactNode;
14
+ }) => (
15
+ <div>
16
+ <div data-slot="before-hero">{beforeHero}</div>
17
+ <div data-slot="between-hero-feed">{betweenHeroAndFeed}</div>
18
+ <div data-slot="after-feed">{afterFeed}</div>
19
+ </div>
20
+ ),
21
+ }));
22
+
23
+ vi.mock("./OverviewOnboardingBanner", () => ({
24
+ OverviewOnboardingBanner: ({ skillCount }: { skillCount: number }) => (
25
+ <div>Onboarding {skillCount}</div>
26
+ ),
27
+ }));
28
+
29
+ vi.mock("./OverviewComparisonSurface", () => ({
30
+ OverviewComparisonSurface: ({ rows }: { rows: Array<unknown> }) => (
31
+ <div>Comparison {rows.length}</div>
32
+ ),
33
+ }));
34
+
35
+ vi.mock("./OverviewRunSummary", () => ({
36
+ OverviewRunSummary: ({ runCount }: { runCount: number }) => <div>Run Summary {runCount}</div>,
37
+ }));
38
+
39
+ import { OverviewCompositionSurface } from "./OverviewCompositionSurface";
40
+
41
+ describe("OverviewCompositionSurface", () => {
42
+ it("renders the shared overview sections in one canonical order", () => {
43
+ const html = renderToStaticMarkup(
44
+ <OverviewCompositionSurface
45
+ autonomyStatus={{
46
+ level: "needs_review",
47
+ summary: "Needs review",
48
+ skills_observed: 2,
49
+ attention_required: 1,
50
+ pending_reviews: 0,
51
+ }}
52
+ lastRun="2026-04-11T00:00:00Z"
53
+ trustWatchlist={[]}
54
+ attentionItems={[]}
55
+ autonomousDecisions={[]}
56
+ onboarding={{ skillCount: 0 }}
57
+ comparison={{
58
+ rows: [
59
+ {
60
+ skillName: "selftune",
61
+ triggerRate: 0.8,
62
+ routingConfidence: 0.7,
63
+ confidenceCoverage: 0.9,
64
+ sessions: 10,
65
+ lastEvolution: null,
66
+ bucket: "stable",
67
+ },
68
+ ],
69
+ }}
70
+ sectionsBeforeFeed={<div>Before Feed</div>}
71
+ runSummary={{
72
+ lastRun: "2026-04-11T00:00:00Z",
73
+ deployed: 1,
74
+ evolved: 2,
75
+ watched: 3,
76
+ runCount: 4,
77
+ }}
78
+ sectionsAfterFeed={<div>After Feed</div>}
79
+ />,
80
+ );
81
+
82
+ expect(html).toContain("Onboarding 0");
83
+ expect(html).toContain("Comparison 1");
84
+ expect(html).toContain("Before Feed");
85
+ expect(html).toContain("Run Summary 4");
86
+ expect(html).toContain("After Feed");
87
+ expect(html.indexOf("Comparison 1")).toBeLessThan(html.indexOf("Before Feed"));
88
+ expect(html.indexOf("Run Summary 4")).toBeLessThan(html.indexOf("After Feed"));
89
+ });
90
+
91
+ it("omits the comparison block when there are no rows", () => {
92
+ const html = renderToStaticMarkup(
93
+ <OverviewCompositionSurface
94
+ autonomyStatus={{
95
+ level: "needs_review",
96
+ summary: "Needs review",
97
+ skills_observed: 0,
98
+ attention_required: 0,
99
+ pending_reviews: 0,
100
+ }}
101
+ lastRun={null}
102
+ trustWatchlist={[]}
103
+ attentionItems={[]}
104
+ autonomousDecisions={[]}
105
+ comparison={{
106
+ rows: [],
107
+ }}
108
+ />,
109
+ );
110
+
111
+ expect(html).not.toContain("Comparison");
112
+ });
113
+ });
@@ -0,0 +1,72 @@
1
+ "use client";
2
+
3
+ import type { ReactNode } from "react";
4
+
5
+ import {
6
+ OverviewComparisonSurface,
7
+ type OverviewComparisonSurfaceProps,
8
+ } from "./OverviewComparisonSurface";
9
+ import { OverviewCoreSurface, type OverviewCoreSurfaceProps } from "./OverviewCoreSurface";
10
+ import { OverviewOnboardingBanner } from "./OverviewOnboardingBanner";
11
+ import { OverviewRunSummary, type OverviewRunSummaryProps } from "./OverviewRunSummary";
12
+
13
+ export interface OverviewCompositionSurfaceProps extends Omit<
14
+ OverviewCoreSurfaceProps,
15
+ "beforeHero" | "betweenHeroAndFeed" | "afterFeed"
16
+ > {
17
+ onboarding?: {
18
+ skillCount: number;
19
+ storageKey?: string;
20
+ } | null;
21
+ comparison?: Omit<OverviewComparisonSurfaceProps, "renderSkillLink"> | null;
22
+ sectionsBeforeFeed?: ReactNode;
23
+ runSummary?: OverviewRunSummaryProps | null;
24
+ sectionsAfterFeed?: ReactNode;
25
+ }
26
+
27
+ export function OverviewCompositionSurface({
28
+ onboarding,
29
+ comparison,
30
+ sectionsBeforeFeed,
31
+ runSummary,
32
+ sectionsAfterFeed,
33
+ renderSkillLink,
34
+ ...coreProps
35
+ }: OverviewCompositionSurfaceProps) {
36
+ const beforeHero = onboarding ? (
37
+ <OverviewOnboardingBanner
38
+ skillCount={onboarding.skillCount}
39
+ storageKey={onboarding.storageKey}
40
+ />
41
+ ) : null;
42
+
43
+ const betweenHeroAndFeed =
44
+ comparison || sectionsBeforeFeed ? (
45
+ <>
46
+ {comparison && comparison.rows.length > 0 ? (
47
+ <div className="col-span-12">
48
+ <OverviewComparisonSurface {...comparison} renderSkillLink={renderSkillLink} />
49
+ </div>
50
+ ) : null}
51
+ {sectionsBeforeFeed}
52
+ </>
53
+ ) : null;
54
+
55
+ const afterFeed =
56
+ runSummary || sectionsAfterFeed ? (
57
+ <>
58
+ {runSummary ? <OverviewRunSummary {...runSummary} /> : null}
59
+ {sectionsAfterFeed}
60
+ </>
61
+ ) : null;
62
+
63
+ return (
64
+ <OverviewCoreSurface
65
+ {...coreProps}
66
+ renderSkillLink={renderSkillLink}
67
+ beforeHero={beforeHero}
68
+ betweenHeroAndFeed={betweenHeroAndFeed}
69
+ afterFeed={afterFeed}
70
+ />
71
+ );
72
+ }
@@ -0,0 +1,71 @@
1
+ "use client";
2
+
3
+ import type { ReactNode } from "react";
4
+
5
+ import { AutonomyHeroCard, SupervisionFeed, TrustWatchlistRail } from "@selftune/ui/components";
6
+ import type {
7
+ AttentionItem,
8
+ AutonomousDecision,
9
+ AutonomyStatus,
10
+ TrustWatchlistEntry,
11
+ } from "@selftune/ui/types";
12
+
13
+ export interface OverviewCoreSurfaceProps {
14
+ autonomyStatus: AutonomyStatus;
15
+ lastRun: string | null;
16
+ trustWatchlist: TrustWatchlistEntry[];
17
+ attentionItems: AttentionItem[];
18
+ autonomousDecisions: AutonomousDecision[];
19
+ renderSkillLink?: (skillName: string) => ReactNode;
20
+ heroActions?: ReactNode;
21
+ trustRailFooter?: ReactNode;
22
+ beforeHero?: ReactNode;
23
+ betweenHeroAndFeed?: ReactNode;
24
+ afterFeed?: ReactNode;
25
+ }
26
+
27
+ export function OverviewCoreSurface({
28
+ autonomyStatus,
29
+ lastRun,
30
+ trustWatchlist,
31
+ attentionItems,
32
+ autonomousDecisions,
33
+ renderSkillLink,
34
+ heroActions,
35
+ trustRailFooter,
36
+ beforeHero,
37
+ betweenHeroAndFeed,
38
+ afterFeed,
39
+ }: OverviewCoreSurfaceProps) {
40
+ return (
41
+ <div className="@container/main flex flex-1 flex-col py-6">
42
+ <div className="grid grid-cols-12 gap-6 px-4 lg:px-6">
43
+ {beforeHero}
44
+
45
+ <div className="col-span-12 @4xl/main:col-span-8">
46
+ <AutonomyHeroCard status={autonomyStatus} lastRun={lastRun} actions={heroActions} />
47
+ </div>
48
+
49
+ <div className="col-span-12 @4xl/main:col-span-4 self-start">
50
+ <TrustWatchlistRail
51
+ entries={trustWatchlist}
52
+ renderSkillLink={renderSkillLink}
53
+ footer={trustRailFooter}
54
+ />
55
+ </div>
56
+
57
+ {betweenHeroAndFeed}
58
+
59
+ <div className="col-span-12">
60
+ <SupervisionFeed
61
+ attention={attentionItems}
62
+ decisions={autonomousDecisions}
63
+ renderSkillLink={renderSkillLink}
64
+ />
65
+ </div>
66
+
67
+ {afterFeed}
68
+ </div>
69
+ </div>
70
+ );
71
+ }