diff --git a/PolyPilot.Console/PolyPilot.csproj b/PolyPilot.Console/PolyPilot.csproj
index 15ed5ee5a8..1d08813b89 100644
--- a/PolyPilot.Console/PolyPilot.csproj
+++ b/PolyPilot.Console/PolyPilot.csproj
@@ -8,8 +8,13 @@
-
+
+
+
+
+
+
diff --git a/PolyPilot/Models/AgentSessionInfo.cs b/PolyPilot.Core/Models/AgentSessionInfo.cs
similarity index 100%
rename from PolyPilot/Models/AgentSessionInfo.cs
rename to PolyPilot.Core/Models/AgentSessionInfo.cs
diff --git a/PolyPilot/Models/AuditLogEntry.cs b/PolyPilot.Core/Models/AuditLogEntry.cs
similarity index 100%
rename from PolyPilot/Models/AuditLogEntry.cs
rename to PolyPilot.Core/Models/AuditLogEntry.cs
diff --git a/PolyPilot/Models/BridgeMessages.cs b/PolyPilot.Core/Models/BridgeMessages.cs
similarity index 100%
rename from PolyPilot/Models/BridgeMessages.cs
rename to PolyPilot.Core/Models/BridgeMessages.cs
diff --git a/PolyPilot/Models/ChatCopyPayloads.cs b/PolyPilot.Core/Models/ChatCopyPayloads.cs
similarity index 100%
rename from PolyPilot/Models/ChatCopyPayloads.cs
rename to PolyPilot.Core/Models/ChatCopyPayloads.cs
diff --git a/PolyPilot/Models/ChatMessage.cs b/PolyPilot.Core/Models/ChatMessage.cs
similarity index 100%
rename from PolyPilot/Models/ChatMessage.cs
rename to PolyPilot.Core/Models/ChatMessage.cs
diff --git a/PolyPilot/Models/CommandHistory.cs b/PolyPilot.Core/Models/CommandHistory.cs
similarity index 100%
rename from PolyPilot/Models/CommandHistory.cs
rename to PolyPilot.Core/Models/CommandHistory.cs
diff --git a/PolyPilot/Models/ConnectionSettings.cs b/PolyPilot.Core/Models/ConnectionSettings.cs
similarity index 100%
rename from PolyPilot/Models/ConnectionSettings.cs
rename to PolyPilot.Core/Models/ConnectionSettings.cs
diff --git a/PolyPilot/Models/DiffParser.cs b/PolyPilot.Core/Models/DiffParser.cs
similarity index 100%
rename from PolyPilot/Models/DiffParser.cs
rename to PolyPilot.Core/Models/DiffParser.cs
diff --git a/PolyPilot/Models/ErrorMessageHelper.cs b/PolyPilot.Core/Models/ErrorMessageHelper.cs
similarity index 100%
rename from PolyPilot/Models/ErrorMessageHelper.cs
rename to PolyPilot.Core/Models/ErrorMessageHelper.cs
diff --git a/PolyPilot/Models/ExternalSessionInfo.cs b/PolyPilot.Core/Models/ExternalSessionInfo.cs
similarity index 100%
rename from PolyPilot/Models/ExternalSessionInfo.cs
rename to PolyPilot.Core/Models/ExternalSessionInfo.cs
diff --git a/PolyPilot/Models/FiestaModels.cs b/PolyPilot.Core/Models/FiestaModels.cs
similarity index 100%
rename from PolyPilot/Models/FiestaModels.cs
rename to PolyPilot.Core/Models/FiestaModels.cs
diff --git a/PolyPilot/Models/LinkHelper.cs b/PolyPilot.Core/Models/LinkHelper.cs
similarity index 70%
rename from PolyPilot/Models/LinkHelper.cs
rename to PolyPilot.Core/Models/LinkHelper.cs
index 6888292180..36428486e9 100644
--- a/PolyPilot/Models/LinkHelper.cs
+++ b/PolyPilot.Core/Models/LinkHelper.cs
@@ -23,15 +23,16 @@ public static void OpenInBackground(string url)
{
if (!IsValidExternalUrl(url)) return;
-#if MACCATALYST
- try
+ if (OperatingSystem.IsMacOS() || OperatingSystem.IsMacCatalyst())
{
- var psi = new ProcessStartInfo("open") { UseShellExecute = false };
- psi.ArgumentList.Add("-g");
- psi.ArgumentList.Add(url);
- Process.Start(psi)?.Dispose();
+ try
+ {
+ var psi = new ProcessStartInfo("open") { UseShellExecute = false };
+ psi.ArgumentList.Add("-g");
+ psi.ArgumentList.Add(url);
+ Process.Start(psi)?.Dispose();
+ }
+ catch { }
}
- catch { }
-#endif
}
}
diff --git a/PolyPilot/Models/ModelCapabilities.cs b/PolyPilot.Core/Models/ModelCapabilities.cs
similarity index 100%
rename from PolyPilot/Models/ModelCapabilities.cs
rename to PolyPilot.Core/Models/ModelCapabilities.cs
diff --git a/PolyPilot/Models/ModelHelper.cs b/PolyPilot.Core/Models/ModelHelper.cs
similarity index 100%
rename from PolyPilot/Models/ModelHelper.cs
rename to PolyPilot.Core/Models/ModelHelper.cs
diff --git a/PolyPilot/Models/PendingImage.cs b/PolyPilot.Core/Models/PendingImage.cs
similarity index 100%
rename from PolyPilot/Models/PendingImage.cs
rename to PolyPilot.Core/Models/PendingImage.cs
diff --git a/PolyPilot/Models/PlatformHelper.cs b/PolyPilot.Core/Models/PlatformHelper.cs
similarity index 82%
rename from PolyPilot/Models/PlatformHelper.cs
rename to PolyPilot.Core/Models/PlatformHelper.cs
index 3a37d15aa8..17f6417922 100644
--- a/PolyPilot/Models/PlatformHelper.cs
+++ b/PolyPilot.Core/Models/PlatformHelper.cs
@@ -5,34 +5,20 @@ namespace PolyPilot.Models;
public static class PlatformHelper
{
public static bool IsDesktop =>
-#if MACCATALYST || WINDOWS
- true;
-#elif IOS || ANDROID
- false;
-#else
- // Linux GTK and other non-mobile platforms are desktop
- !OperatingSystem.IsIOS() && !OperatingSystem.IsAndroid();
-#endif
+ // Runtime detection — OperatingSystem.IsIOS() returns true on Mac Catalyst,
+ // so we must exclude it explicitly to correctly identify Mac Catalyst as desktop.
+ OperatingSystem.IsMacCatalyst() || (!OperatingSystem.IsIOS() && !OperatingSystem.IsAndroid());
public static bool IsMobile =>
-#if IOS || ANDROID
- true;
-#else
- false;
-#endif
+ (OperatingSystem.IsIOS() && !OperatingSystem.IsMacCatalyst()) || OperatingSystem.IsAndroid();
public static string PlatformName =>
-#if MACCATALYST
- "maccatalyst";
-#elif WINDOWS
- "windows";
-#elif IOS
- "ios";
-#elif ANDROID
- "android";
-#else
+ OperatingSystem.IsMacCatalyst() ? "maccatalyst" :
+ OperatingSystem.IsMacOS() ? "maccatalyst" :
+ OperatingSystem.IsWindows() ? "windows" :
+ OperatingSystem.IsIOS() ? "ios" :
+ OperatingSystem.IsAndroid() ? "android" :
OperatingSystem.IsLinux() ? "linux" : "unknown";
-#endif
public static ConnectionMode[] AvailableModes => IsDesktop
? [ConnectionMode.Embedded, ConnectionMode.Persistent, ConnectionMode.Remote]
diff --git a/PolyPilot/Models/PluginSettings.cs b/PolyPilot.Core/Models/PluginSettings.cs
similarity index 100%
rename from PolyPilot/Models/PluginSettings.cs
rename to PolyPilot.Core/Models/PluginSettings.cs
diff --git a/PolyPilot/Models/PromptLibrary.cs b/PolyPilot.Core/Models/PromptLibrary.cs
similarity index 100%
rename from PolyPilot/Models/PromptLibrary.cs
rename to PolyPilot.Core/Models/PromptLibrary.cs
diff --git a/PolyPilot/Models/ReflectionCycle.cs b/PolyPilot.Core/Models/ReflectionCycle.cs
similarity index 100%
rename from PolyPilot/Models/ReflectionCycle.cs
rename to PolyPilot.Core/Models/ReflectionCycle.cs
diff --git a/PolyPilot/Models/RenderThrottle.cs b/PolyPilot.Core/Models/RenderThrottle.cs
similarity index 100%
rename from PolyPilot/Models/RenderThrottle.cs
rename to PolyPilot.Core/Models/RenderThrottle.cs
diff --git a/PolyPilot/Models/RepositoryInfo.cs b/PolyPilot.Core/Models/RepositoryInfo.cs
similarity index 100%
rename from PolyPilot/Models/RepositoryInfo.cs
rename to PolyPilot.Core/Models/RepositoryInfo.cs
diff --git a/PolyPilot/Models/SessionOrganization.cs b/PolyPilot.Core/Models/SessionOrganization.cs
similarity index 100%
rename from PolyPilot/Models/SessionOrganization.cs
rename to PolyPilot.Core/Models/SessionOrganization.cs
diff --git a/PolyPilot/Models/SettingDescriptor.cs b/PolyPilot.Core/Models/SettingDescriptor.cs
similarity index 100%
rename from PolyPilot/Models/SettingDescriptor.cs
rename to PolyPilot.Core/Models/SettingDescriptor.cs
diff --git a/PolyPilot/Models/SmartPunctuationNormalizer.cs b/PolyPilot.Core/Models/SmartPunctuationNormalizer.cs
similarity index 100%
rename from PolyPilot/Models/SmartPunctuationNormalizer.cs
rename to PolyPilot.Core/Models/SmartPunctuationNormalizer.cs
diff --git a/PolyPilot/Models/SquadDiscovery.cs b/PolyPilot.Core/Models/SquadDiscovery.cs
similarity index 100%
rename from PolyPilot/Models/SquadDiscovery.cs
rename to PolyPilot.Core/Models/SquadDiscovery.cs
diff --git a/PolyPilot/Models/SquadWriter.cs b/PolyPilot.Core/Models/SquadWriter.cs
similarity index 100%
rename from PolyPilot/Models/SquadWriter.cs
rename to PolyPilot.Core/Models/SquadWriter.cs
diff --git a/PolyPilot/Models/SynchronizedMessageQueue.cs b/PolyPilot.Core/Models/SynchronizedMessageQueue.cs
similarity index 100%
rename from PolyPilot/Models/SynchronizedMessageQueue.cs
rename to PolyPilot.Core/Models/SynchronizedMessageQueue.cs
diff --git a/PolyPilot/Models/TutorialStep.cs b/PolyPilot.Core/Models/TutorialStep.cs
similarity index 100%
rename from PolyPilot/Models/TutorialStep.cs
rename to PolyPilot.Core/Models/TutorialStep.cs
diff --git a/PolyPilot/Models/UsageStatistics.cs b/PolyPilot.Core/Models/UsageStatistics.cs
similarity index 100%
rename from PolyPilot/Models/UsageStatistics.cs
rename to PolyPilot.Core/Models/UsageStatistics.cs
diff --git a/PolyPilot.Core/PolyPilot.Core.csproj b/PolyPilot.Core/PolyPilot.Core.csproj
new file mode 100644
index 0000000000..399293c9dd
--- /dev/null
+++ b/PolyPilot.Core/PolyPilot.Core.csproj
@@ -0,0 +1,40 @@
+
+
+
+ net10.0;net10.0-android
+ $(TargetFrameworks);net10.0-ios;net10.0-maccatalyst
+ $(TargetFrameworks);net10.0-windows10.0.19041.0
+ PolyPilot
+ enable
+ enable
+ true
+ true
+
+ true
+
+ Library
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/PolyPilot/Services/AuditLogService.cs b/PolyPilot.Core/Services/AuditLogService.cs
similarity index 100%
rename from PolyPilot/Services/AuditLogService.cs
rename to PolyPilot.Core/Services/AuditLogService.cs
diff --git a/PolyPilot/Services/ChatDatabase.cs b/PolyPilot.Core/Services/ChatDatabase.cs
similarity index 100%
rename from PolyPilot/Services/ChatDatabase.cs
rename to PolyPilot.Core/Services/ChatDatabase.cs
diff --git a/PolyPilot/Services/CodespaceService.Diagnostics.cs b/PolyPilot.Core/Services/CodespaceService.Diagnostics.cs
similarity index 100%
rename from PolyPilot/Services/CodespaceService.Diagnostics.cs
rename to PolyPilot.Core/Services/CodespaceService.Diagnostics.cs
diff --git a/PolyPilot/Services/CodespaceService.Lifecycle.cs b/PolyPilot.Core/Services/CodespaceService.Lifecycle.cs
similarity index 100%
rename from PolyPilot/Services/CodespaceService.Lifecycle.cs
rename to PolyPilot.Core/Services/CodespaceService.Lifecycle.cs
diff --git a/PolyPilot/Services/CodespaceService.cs b/PolyPilot.Core/Services/CodespaceService.cs
similarity index 100%
rename from PolyPilot/Services/CodespaceService.cs
rename to PolyPilot.Core/Services/CodespaceService.cs
diff --git a/PolyPilot/Services/CopilotService.Bridge.cs b/PolyPilot.Core/Services/CopilotService.Bridge.cs
similarity index 100%
rename from PolyPilot/Services/CopilotService.Bridge.cs
rename to PolyPilot.Core/Services/CopilotService.Bridge.cs
diff --git a/PolyPilot/Services/CopilotService.Codespace.cs b/PolyPilot.Core/Services/CopilotService.Codespace.cs
similarity index 100%
rename from PolyPilot/Services/CopilotService.Codespace.cs
rename to PolyPilot.Core/Services/CopilotService.Codespace.cs
diff --git a/PolyPilot/Services/CopilotService.Continuation.cs b/PolyPilot.Core/Services/CopilotService.Continuation.cs
similarity index 100%
rename from PolyPilot/Services/CopilotService.Continuation.cs
rename to PolyPilot.Core/Services/CopilotService.Continuation.cs
diff --git a/PolyPilot/Services/CopilotService.Events.cs b/PolyPilot.Core/Services/CopilotService.Events.cs
similarity index 100%
rename from PolyPilot/Services/CopilotService.Events.cs
rename to PolyPilot.Core/Services/CopilotService.Events.cs
diff --git a/PolyPilot/Services/CopilotService.Organization.cs b/PolyPilot.Core/Services/CopilotService.Organization.cs
similarity index 100%
rename from PolyPilot/Services/CopilotService.Organization.cs
rename to PolyPilot.Core/Services/CopilotService.Organization.cs
diff --git a/PolyPilot/Services/CopilotService.Persistence.cs b/PolyPilot.Core/Services/CopilotService.Persistence.cs
similarity index 100%
rename from PolyPilot/Services/CopilotService.Persistence.cs
rename to PolyPilot.Core/Services/CopilotService.Persistence.cs
diff --git a/PolyPilot/Services/CopilotService.Providers.cs b/PolyPilot.Core/Services/CopilotService.Providers.cs
similarity index 100%
rename from PolyPilot/Services/CopilotService.Providers.cs
rename to PolyPilot.Core/Services/CopilotService.Providers.cs
diff --git a/PolyPilot/Services/CopilotService.Utilities.cs b/PolyPilot.Core/Services/CopilotService.Utilities.cs
similarity index 100%
rename from PolyPilot/Services/CopilotService.Utilities.cs
rename to PolyPilot.Core/Services/CopilotService.Utilities.cs
diff --git a/PolyPilot/Services/CopilotService.cs b/PolyPilot.Core/Services/CopilotService.cs
similarity index 99%
rename from PolyPilot/Services/CopilotService.cs
rename to PolyPilot.Core/Services/CopilotService.cs
index 2e5f29c72c..15729b50f6 100644
--- a/PolyPilot/Services/CopilotService.cs
+++ b/PolyPilot.Core/Services/CopilotService.cs
@@ -974,16 +974,17 @@ public async Task InitializeAsync(CancellationToken cancellationToken = default)
return;
}
-#if ANDROID
- // Android can't run Copilot CLI locally — must connect to remote server
- settings.Mode = ConnectionMode.Persistent;
- CurrentMode = ConnectionMode.Persistent;
- if (settings.Host == "localhost" || settings.Host == "127.0.0.1")
+ if (OperatingSystem.IsAndroid())
{
- Debug("Android detected with localhost — update Host in settings to your Mac's IP");
+ // Android can't run Copilot CLI locally — must connect to remote server
+ settings.Mode = ConnectionMode.Persistent;
+ CurrentMode = ConnectionMode.Persistent;
+ if (settings.Host == "localhost" || settings.Host == "127.0.0.1")
+ {
+ Debug("Android detected with localhost — update Host in settings to your Mac's IP");
+ }
+ Debug($"Android: connecting to remote server at {settings.CliUrl}");
}
- Debug($"Android: connecting to remote server at {settings.CliUrl}");
-#endif
// In Persistent mode, auto-start the server if not already running
if (settings.Mode == ConnectionMode.Persistent)
{
@@ -4864,11 +4865,13 @@ public async ValueTask DisposeAsync()
private void StartExternalSessionScannerIfNeeded()
{
-#if ANDROID || IOS
- // No local filesystem access on mobile
- return;
-#else
- // UI-thread only -- callers are InitializeAsync, InitializeDemo, and ReconnectAsync
+ if (OperatingSystem.IsAndroid() || (OperatingSystem.IsIOS() && !OperatingSystem.IsMacCatalyst()))
+ {
+ // No local filesystem access on mobile
+ return;
+ }
+
+ // UI-thread only -- callers are InitializeAsync, InitializeDemo, and ReconnectAsync
if (_externalSessionScanner != null) return; // already running
// CWD-based exclusion: sessions whose CWD is inside ~/.polypilot/ are typically PolyPilot's own
@@ -4890,7 +4893,6 @@ private void StartExternalSessionScannerIfNeeded()
_externalSessionScanner.OnChanged += () => NotifyStateChangedCoalesced();
_externalSessionScanner.Start();
Debug("External session scanner started");
-#endif
}
private void StopExternalSessionScanner()
diff --git a/PolyPilot/Services/DemoService.cs b/PolyPilot.Core/Services/DemoService.cs
similarity index 100%
rename from PolyPilot/Services/DemoService.cs
rename to PolyPilot.Core/Services/DemoService.cs
diff --git a/PolyPilot/Services/DevTunnelService.cs b/PolyPilot.Core/Services/DevTunnelService.cs
similarity index 100%
rename from PolyPilot/Services/DevTunnelService.cs
rename to PolyPilot.Core/Services/DevTunnelService.cs
diff --git a/PolyPilot/Services/EfficiencyAnalysisService.cs b/PolyPilot.Core/Services/EfficiencyAnalysisService.cs
similarity index 100%
rename from PolyPilot/Services/EfficiencyAnalysisService.cs
rename to PolyPilot.Core/Services/EfficiencyAnalysisService.cs
diff --git a/PolyPilot/Services/ExternalSessionScanner.cs b/PolyPilot.Core/Services/ExternalSessionScanner.cs
similarity index 100%
rename from PolyPilot/Services/ExternalSessionScanner.cs
rename to PolyPilot.Core/Services/ExternalSessionScanner.cs
diff --git a/PolyPilot/Services/FiestaService.cs b/PolyPilot.Core/Services/FiestaService.cs
similarity index 100%
rename from PolyPilot/Services/FiestaService.cs
rename to PolyPilot.Core/Services/FiestaService.cs
diff --git a/PolyPilot/Services/GitAutoUpdateService.cs b/PolyPilot.Core/Services/GitAutoUpdateService.cs
similarity index 100%
rename from PolyPilot/Services/GitAutoUpdateService.cs
rename to PolyPilot.Core/Services/GitAutoUpdateService.cs
diff --git a/PolyPilot/Services/GitHubReferenceLinker.cs b/PolyPilot.Core/Services/GitHubReferenceLinker.cs
similarity index 100%
rename from PolyPilot/Services/GitHubReferenceLinker.cs
rename to PolyPilot.Core/Services/GitHubReferenceLinker.cs
diff --git a/PolyPilot/Services/HolidayThemeHelper.cs b/PolyPilot.Core/Services/HolidayThemeHelper.cs
similarity index 100%
rename from PolyPilot/Services/HolidayThemeHelper.cs
rename to PolyPilot.Core/Services/HolidayThemeHelper.cs
diff --git a/PolyPilot/Services/IChatDatabase.cs b/PolyPilot.Core/Services/IChatDatabase.cs
similarity index 100%
rename from PolyPilot/Services/IChatDatabase.cs
rename to PolyPilot.Core/Services/IChatDatabase.cs
diff --git a/PolyPilot/Services/IDemoService.cs b/PolyPilot.Core/Services/IDemoService.cs
similarity index 100%
rename from PolyPilot/Services/IDemoService.cs
rename to PolyPilot.Core/Services/IDemoService.cs
diff --git a/PolyPilot/Services/INotificationManagerService.cs b/PolyPilot.Core/Services/INotificationManagerService.cs
similarity index 100%
rename from PolyPilot/Services/INotificationManagerService.cs
rename to PolyPilot.Core/Services/INotificationManagerService.cs
diff --git a/PolyPilot/Services/IServerManager.cs b/PolyPilot.Core/Services/IServerManager.cs
similarity index 100%
rename from PolyPilot/Services/IServerManager.cs
rename to PolyPilot.Core/Services/IServerManager.cs
diff --git a/PolyPilot/Services/IWsBridgeClient.cs b/PolyPilot.Core/Services/IWsBridgeClient.cs
similarity index 100%
rename from PolyPilot/Services/IWsBridgeClient.cs
rename to PolyPilot.Core/Services/IWsBridgeClient.cs
diff --git a/PolyPilot/Services/MarkdownRenderer.cs b/PolyPilot.Core/Services/MarkdownRenderer.cs
similarity index 100%
rename from PolyPilot/Services/MarkdownRenderer.cs
rename to PolyPilot.Core/Services/MarkdownRenderer.cs
diff --git a/PolyPilot/Services/NotificationMessageBuilder.cs b/PolyPilot.Core/Services/NotificationMessageBuilder.cs
similarity index 100%
rename from PolyPilot/Services/NotificationMessageBuilder.cs
rename to PolyPilot.Core/Services/NotificationMessageBuilder.cs
diff --git a/PolyPilot/Services/PluginFileLogger.cs b/PolyPilot.Core/Services/PluginFileLogger.cs
similarity index 100%
rename from PolyPilot/Services/PluginFileLogger.cs
rename to PolyPilot.Core/Services/PluginFileLogger.cs
diff --git a/PolyPilot/Services/PluginLoader.cs b/PolyPilot.Core/Services/PluginLoader.cs
similarity index 100%
rename from PolyPilot/Services/PluginLoader.cs
rename to PolyPilot.Core/Services/PluginLoader.cs
diff --git a/PolyPilot/Services/PrLinkService.cs b/PolyPilot.Core/Services/PrLinkService.cs
similarity index 100%
rename from PolyPilot/Services/PrLinkService.cs
rename to PolyPilot.Core/Services/PrLinkService.cs
diff --git a/PolyPilot/Services/ProcessHelper.cs b/PolyPilot.Core/Services/ProcessHelper.cs
similarity index 100%
rename from PolyPilot/Services/ProcessHelper.cs
rename to PolyPilot.Core/Services/ProcessHelper.cs
diff --git a/PolyPilot/Services/PromptLibraryService.cs b/PolyPilot.Core/Services/PromptLibraryService.cs
similarity index 100%
rename from PolyPilot/Services/PromptLibraryService.cs
rename to PolyPilot.Core/Services/PromptLibraryService.cs
diff --git a/PolyPilot/Services/ProviderHostContext.cs b/PolyPilot.Core/Services/ProviderHostContext.cs
similarity index 100%
rename from PolyPilot/Services/ProviderHostContext.cs
rename to PolyPilot.Core/Services/ProviderHostContext.cs
diff --git a/PolyPilot/Services/RepoManager.cs b/PolyPilot.Core/Services/RepoManager.cs
similarity index 100%
rename from PolyPilot/Services/RepoManager.cs
rename to PolyPilot.Core/Services/RepoManager.cs
diff --git a/PolyPilot/Services/ServerManager.cs b/PolyPilot.Core/Services/ServerManager.cs
similarity index 100%
rename from PolyPilot/Services/ServerManager.cs
rename to PolyPilot.Core/Services/ServerManager.cs
diff --git a/PolyPilot/Services/SessionMetricsExtractor.cs b/PolyPilot.Core/Services/SessionMetricsExtractor.cs
similarity index 100%
rename from PolyPilot/Services/SessionMetricsExtractor.cs
rename to PolyPilot.Core/Services/SessionMetricsExtractor.cs
diff --git a/PolyPilot/Services/SettingsRegistry.cs b/PolyPilot.Core/Services/SettingsRegistry.cs
similarity index 100%
rename from PolyPilot/Services/SettingsRegistry.cs
rename to PolyPilot.Core/Services/SettingsRegistry.cs
diff --git a/PolyPilot/Services/ShowImageTool.cs b/PolyPilot.Core/Services/ShowImageTool.cs
similarity index 100%
rename from PolyPilot/Services/ShowImageTool.cs
rename to PolyPilot.Core/Services/ShowImageTool.cs
diff --git a/PolyPilot/Services/TailscaleService.cs b/PolyPilot.Core/Services/TailscaleService.cs
similarity index 100%
rename from PolyPilot/Services/TailscaleService.cs
rename to PolyPilot.Core/Services/TailscaleService.cs
diff --git a/PolyPilot/Services/UsageStatsService.cs b/PolyPilot.Core/Services/UsageStatsService.cs
similarity index 95%
rename from PolyPilot/Services/UsageStatsService.cs
rename to PolyPilot.Core/Services/UsageStatsService.cs
index fc6ebfc3cd..3078617feb 100644
--- a/PolyPilot/Services/UsageStatsService.cs
+++ b/PolyPilot.Core/Services/UsageStatsService.cs
@@ -215,12 +215,13 @@ private void SaveStats()
PreallocationSize = json.Length
};
-#if !ANDROID && !IOS && !MACCATALYST
- if (OperatingSystem.IsLinux() || OperatingSystem.IsMacOS())
+ if (!OperatingSystem.IsAndroid() && !OperatingSystem.IsIOS() && !OperatingSystem.IsMacCatalyst())
{
- options.UnixCreateMode = UnixFileMode.UserRead | UnixFileMode.UserWrite;
+ if (OperatingSystem.IsLinux() || OperatingSystem.IsMacOS())
+ {
+ options.UnixCreateMode = UnixFileMode.UserRead | UnixFileMode.UserWrite;
+ }
}
-#endif
using var stream = new FileStream(StatsPath, options);
using var writer = new StreamWriter(stream);
diff --git a/PolyPilot/Services/WindowFocusHelper.cs b/PolyPilot.Core/Services/WindowFocusHelper.cs
similarity index 100%
rename from PolyPilot/Services/WindowFocusHelper.cs
rename to PolyPilot.Core/Services/WindowFocusHelper.cs
diff --git a/PolyPilot/Services/WsBridgeClient.cs b/PolyPilot.Core/Services/WsBridgeClient.cs
similarity index 100%
rename from PolyPilot/Services/WsBridgeClient.cs
rename to PolyPilot.Core/Services/WsBridgeClient.cs
diff --git a/PolyPilot/Services/WsBridgeServer.cs b/PolyPilot.Core/Services/WsBridgeServer.cs
similarity index 100%
rename from PolyPilot/Services/WsBridgeServer.cs
rename to PolyPilot.Core/Services/WsBridgeServer.cs
diff --git a/PolyPilot.Gtk/PolyPilot.Gtk.csproj b/PolyPilot.Gtk/PolyPilot.Gtk.csproj
index e5d8070367..68ac50ef74 100644
--- a/PolyPilot.Gtk/PolyPilot.Gtk.csproj
+++ b/PolyPilot.Gtk/PolyPilot.Gtk.csproj
@@ -47,6 +47,7 @@
+
@@ -55,15 +56,9 @@
-
-
-
-
-
-
diff --git a/PolyPilot.Provider.Abstractions/PolyPilot.Provider.Abstractions.csproj b/PolyPilot.Provider.Abstractions/PolyPilot.Provider.Abstractions.csproj
index c12ac0d76e..64ed643004 100644
--- a/PolyPilot.Provider.Abstractions/PolyPilot.Provider.Abstractions.csproj
+++ b/PolyPilot.Provider.Abstractions/PolyPilot.Provider.Abstractions.csproj
@@ -11,6 +11,6 @@
-
+
diff --git a/PolyPilot.Tests/ChatExperienceSafetyTests.cs b/PolyPilot.Tests/ChatExperienceSafetyTests.cs
index b4f84fab07..4e3e18c07e 100644
--- a/PolyPilot.Tests/ChatExperienceSafetyTests.cs
+++ b/PolyPilot.Tests/ChatExperienceSafetyTests.cs
@@ -819,7 +819,7 @@ public async Task FlushCurrentResponse_EmptyContent_NoOp()
public void WatchdogCallback_HasGenerationGuard()
{
var source = File.ReadAllText(
- Path.Combine(GetRepoRoot(), "PolyPilot", "Services", "CopilotService.Events.cs"));
+ Path.Combine(GetRepoRoot(), "PolyPilot.Core", "Services", "CopilotService.Events.cs"));
// Find the watchdog callback inside InvokeOnUI
var watchdogIdx = source.IndexOf("watchdogGeneration != currentGen", StringComparison.Ordinal);
@@ -835,7 +835,7 @@ public void WatchdogCallback_HasGenerationGuard()
public void CompleteResponse_Source_ClearsSendingFlag()
{
var source = File.ReadAllText(
- Path.Combine(GetRepoRoot(), "PolyPilot", "Services", "CopilotService.Events.cs"));
+ Path.Combine(GetRepoRoot(), "PolyPilot.Core", "Services", "CopilotService.Events.cs"));
// Find CompleteResponse method
var crIdx = source.IndexOf("private void CompleteResponse(", StringComparison.Ordinal);
@@ -854,7 +854,7 @@ public void CompleteResponse_Source_ClearsSendingFlag()
public void ReconnectPath_IncludesMcpServersAndSkills()
{
var source = File.ReadAllText(
- Path.Combine(GetRepoRoot(), "PolyPilot", "Services", "CopilotService.cs"));
+ Path.Combine(GetRepoRoot(), "PolyPilot.Core", "Services", "CopilotService.cs"));
// After extraction to BuildFreshSessionConfig, verify the reconnect path calls the helper
var sessionNotFoundIdx = source.IndexOf("resumeEx.Message.Contains(\"Session not found\"", StringComparison.Ordinal);
diff --git a/PolyPilot.Tests/ConnectionRecoveryTests.cs b/PolyPilot.Tests/ConnectionRecoveryTests.cs
index 29eb5a1ab2..46347761d5 100644
--- a/PolyPilot.Tests/ConnectionRecoveryTests.cs
+++ b/PolyPilot.Tests/ConnectionRecoveryTests.cs
@@ -233,7 +233,7 @@ public void SendPromptAsync_ReconnectPath_RefreshesLocalClientAfterRecreation()
// SendPromptAsync, the local `client` variable is refreshed after `_client`
// is recreated. Without this, ResumeSessionAsync/CreateSessionAsync operate
// on the old disposed CopilotClient, throwing "Client not connected".
- var source = File.ReadAllText(Path.Combine(GetRepoRoot(), "PolyPilot", "Services", "CopilotService.cs"));
+ var source = File.ReadAllText(Path.Combine(GetRepoRoot(), "PolyPilot.Core", "Services", "CopilotService.cs"));
// Find the reconnect block where _client is recreated
var recreateIndex = source.IndexOf("_client = CreateClient(connSettings);");
@@ -255,7 +255,7 @@ public void SendPromptAsync_ReconnectPath_UsesRefreshedClientForCreateSession()
{
// STRUCTURAL REGRESSION GUARD: The stale reference also affects the
// "Session not found" fallback where client.CreateSessionAsync is called.
- var source = File.ReadAllText(Path.Combine(GetRepoRoot(), "PolyPilot", "Services", "CopilotService.cs"));
+ var source = File.ReadAllText(Path.Combine(GetRepoRoot(), "PolyPilot.Core", "Services", "CopilotService.cs"));
var recreateIndex = source.IndexOf("_client = CreateClient(connSettings);");
var refreshIndex = source.IndexOf("client = _client", recreateIndex);
@@ -277,7 +277,7 @@ public void SendPromptAsync_FreshSessionConfig_IncludesMcpServers()
// STRUCTURAL REGRESSION GUARD: The "Session not found" fallback must assign
// McpServers in the freshConfig so MCP tools survive reconnection.
// After extraction to BuildFreshSessionConfig helper, verify the helper contains it.
- var source = File.ReadAllText(Path.Combine(GetRepoRoot(), "PolyPilot", "Services", "CopilotService.cs"));
+ var source = File.ReadAllText(Path.Combine(GetRepoRoot(), "PolyPilot.Core", "Services", "CopilotService.cs"));
// Verify the reconnect path calls the helper
var sessionNotFoundIdx = source.IndexOf("resumeEx.Message.Contains(\"Session not found\"", StringComparison.Ordinal);
@@ -297,7 +297,7 @@ public void SendPromptAsync_FreshSessionConfig_IncludesSkillDirectories()
{
// STRUCTURAL REGRESSION GUARD: The "Session not found" fallback must assign
// SkillDirectories in the freshConfig so skills survive reconnection.
- var source = File.ReadAllText(Path.Combine(GetRepoRoot(), "PolyPilot", "Services", "CopilotService.cs"));
+ var source = File.ReadAllText(Path.Combine(GetRepoRoot(), "PolyPilot.Core", "Services", "CopilotService.cs"));
var helperIdx = source.IndexOf("BuildFreshSessionConfig(SessionState state");
Assert.True(helperIdx > 0, "Could not find BuildFreshSessionConfig helper");
@@ -310,7 +310,7 @@ public void SendPromptAsync_FreshSessionConfig_IncludesSystemMessage()
{
// STRUCTURAL REGRESSION GUARD: The "Session not found" fallback must include
// SystemMessage so the session retains its system prompt after reconnection.
- var source = File.ReadAllText(Path.Combine(GetRepoRoot(), "PolyPilot", "Services", "CopilotService.cs"));
+ var source = File.ReadAllText(Path.Combine(GetRepoRoot(), "PolyPilot.Core", "Services", "CopilotService.cs"));
var helperIdx = source.IndexOf("BuildFreshSessionConfig(SessionState state");
Assert.True(helperIdx > 0, "Could not find BuildFreshSessionConfig helper");
@@ -324,7 +324,7 @@ public void SendPromptAsync_FreshSessionConfig_MatchesCreateSessionFields()
{
// STRUCTURAL REGRESSION GUARD: The BuildFreshSessionConfig helper must
// set the same critical fields as the original CreateSessionAsync config.
- var source = File.ReadAllText(Path.Combine(GetRepoRoot(), "PolyPilot", "Services", "CopilotService.cs"));
+ var source = File.ReadAllText(Path.Combine(GetRepoRoot(), "PolyPilot.Core", "Services", "CopilotService.cs"));
var helperIdx = source.IndexOf("BuildFreshSessionConfig(SessionState state");
Assert.True(helperIdx > 0);
@@ -451,7 +451,7 @@ public void RestorePreviousSessions_FallbackCoversProcessErrors()
// STRUCTURAL REGRESSION GUARD: RestorePreviousSessionsAsync must include
// IsProcessError in the fallback condition so worker sessions with stale CLI
// server process handles get recreated instead of silently dropped.
- var source = File.ReadAllText(Path.Combine(GetRepoRoot(), "PolyPilot", "Services", "CopilotService.Persistence.cs"));
+ var source = File.ReadAllText(Path.Combine(GetRepoRoot(), "PolyPilot.Core", "Services", "CopilotService.Persistence.cs"));
// Find the fallback condition that checks ex.Message for "Session not found"
var conditionIndex = source.IndexOf("ex.Message.Contains(\"Session not found\"");
@@ -586,7 +586,7 @@ public void SendPromptAsync_ReconnectPath_HasLazyReinitializationGuard()
// must check for a dead client (!IsInitialized || _client == null) before
// calling GetClientForGroup. Without this, a previous reconnect failure
// that nulled _client makes ALL sessions permanently dead.
- var source = File.ReadAllText(Path.Combine(GetRepoRoot(), "PolyPilot", "Services", "CopilotService.cs"));
+ var source = File.ReadAllText(Path.Combine(GetRepoRoot(), "PolyPilot.Core", "Services", "CopilotService.cs"));
// Find the reconnect block (inside the SendAsync catch)
var reconnectIndex = source.IndexOf("attempting reconnect...");
@@ -607,7 +607,7 @@ public void SendPromptAsync_LazyReinit_AttemptsServerRestart()
{
// STRUCTURAL REGRESSION GUARD: The lazy re-init path must attempt to restart
// the persistent server (via _serverManager) before creating a new client.
- var source = File.ReadAllText(Path.Combine(GetRepoRoot(), "PolyPilot", "Services", "CopilotService.cs"));
+ var source = File.ReadAllText(Path.Combine(GetRepoRoot(), "PolyPilot.Core", "Services", "CopilotService.cs"));
var reinitIndex = source.IndexOf("lazy re-initialization so the session can self-heal");
Assert.True(reinitIndex > 0);
@@ -624,7 +624,7 @@ public void SendPromptAsync_LazyReinit_SkipsForCodespaceSessions()
{
// STRUCTURAL REGRESSION GUARD: Lazy re-init should NOT run for codespace
// sessions (they have their own health check reconnection mechanism).
- var source = File.ReadAllText(Path.Combine(GetRepoRoot(), "PolyPilot", "Services", "CopilotService.cs"));
+ var source = File.ReadAllText(Path.Combine(GetRepoRoot(), "PolyPilot.Core", "Services", "CopilotService.cs"));
var reinitIndex = source.IndexOf("lazy re-initialization so the session can self-heal");
Assert.True(reinitIndex > 0);
@@ -713,7 +713,7 @@ public void ResumeSessionAsync_Structural_DedupsDuplicateDisplayName()
// STRUCTURAL REGRESSION GUARD: ResumeSessionAsync must de-duplicate
// display names instead of throwing on collision.
var source = File.ReadAllText(
- Path.Combine(GetRepoRoot(), "PolyPilot", "Services", "CopilotService.cs"));
+ Path.Combine(GetRepoRoot(), "PolyPilot.Core", "Services", "CopilotService.cs"));
var methodIndex = source.IndexOf("public async Task ResumeSessionAsync");
Assert.True(methodIndex > 0, "Could not find ResumeSessionAsync method");
@@ -755,7 +755,7 @@ public void ChatDatabase_AddMessageAsync_HasBroadCatch()
// STRUCTURAL REGRESSION GUARD: AddMessageAsync must use `catch (Exception`
// not a narrow filter like `catch (SQLiteException)`. Historical regression
// used narrow catch → uncaught exceptions became UnobservedTaskException.
- var source = File.ReadAllText(Path.Combine(GetRepoRoot(), "PolyPilot", "Services", "ChatDatabase.cs"));
+ var source = File.ReadAllText(Path.Combine(GetRepoRoot(), "PolyPilot.Core", "Services", "ChatDatabase.cs"));
// Find the AddMessageAsync method
var methodIndex = source.IndexOf("public async Task AddMessageAsync");
@@ -773,8 +773,8 @@ public void AllChatDbCalls_UseSafeFireAndForget()
{
// STRUCTURAL REGRESSION GUARD: All fire-and-forget _chatDb calls in
// CopilotService must use SafeFireAndForget, not bare `_ = ...`.
- var csFile = File.ReadAllText(Path.Combine(GetRepoRoot(), "PolyPilot", "Services", "CopilotService.cs"));
- var eventsFile = File.ReadAllText(Path.Combine(GetRepoRoot(), "PolyPilot", "Services", "CopilotService.Events.cs"));
+ var csFile = File.ReadAllText(Path.Combine(GetRepoRoot(), "PolyPilot.Core", "Services", "CopilotService.cs"));
+ var eventsFile = File.ReadAllText(Path.Combine(GetRepoRoot(), "PolyPilot.Core", "Services", "CopilotService.Events.cs"));
// No bare fire-and-forget patterns should exist
Assert.DoesNotContain("_ = _chatDb.", csFile);
diff --git a/PolyPilot.Tests/ConsecutiveStuckSessionTests.cs b/PolyPilot.Tests/ConsecutiveStuckSessionTests.cs
index bd9ea36ea2..a6d89da5d1 100644
--- a/PolyPilot.Tests/ConsecutiveStuckSessionTests.cs
+++ b/PolyPilot.Tests/ConsecutiveStuckSessionTests.cs
@@ -155,7 +155,7 @@ public void WatchdogSource_IncrementsConsecutiveStuckCount()
// Verify the watchdog timeout path increments ConsecutiveStuckCount.
// This is a structural guard — if someone removes the increment, this test fails.
var repoRoot = GetRepoRoot();
- var eventsSource = File.ReadAllText(Path.Combine(repoRoot, "PolyPilot", "Services", "CopilotService.Events.cs"));
+ var eventsSource = File.ReadAllText(Path.Combine(repoRoot, "PolyPilot.Core", "Services", "CopilotService.Events.cs"));
Assert.Contains("ConsecutiveStuckCount++", eventsSource);
}
@@ -166,7 +166,7 @@ public void WatchdogSource_SkipsHistoryGrowthAfterRepeatedStucks()
// Verify the watchdog conditionally skips adding system messages to history
// when ConsecutiveStuckCount >= 3 to break the positive feedback loop.
var repoRoot = GetRepoRoot();
- var eventsSource = File.ReadAllText(Path.Combine(repoRoot, "PolyPilot", "Services", "CopilotService.Events.cs"));
+ var eventsSource = File.ReadAllText(Path.Combine(repoRoot, "PolyPilot.Core", "Services", "CopilotService.Events.cs"));
Assert.Contains("ConsecutiveStuckCount < 3", eventsSource);
}
@@ -177,7 +177,7 @@ public void WatchdogSource_ClearsMessageQueueOnRepeatedStucks()
// Verify that after 3+ consecutive stucks, the message queue is cleared
// to prevent auto-dispatch from immediately re-sending into a stuck session.
var repoRoot = GetRepoRoot();
- var eventsSource = File.ReadAllText(Path.Combine(repoRoot, "PolyPilot", "Services", "CopilotService.Events.cs"));
+ var eventsSource = File.ReadAllText(Path.Combine(repoRoot, "PolyPilot.Core", "Services", "CopilotService.Events.cs"));
// The else branch (ConsecutiveStuckCount >= 3) must clear the queue
Assert.Contains("MessageQueue.Clear()", eventsSource);
@@ -188,7 +188,7 @@ public void CompleteResponseSource_ResetsConsecutiveStuckCount()
{
// Verify CompleteResponse resets the stuck counter on success.
var repoRoot = GetRepoRoot();
- var eventsSource = File.ReadAllText(Path.Combine(repoRoot, "PolyPilot", "Services", "CopilotService.Events.cs"));
+ var eventsSource = File.ReadAllText(Path.Combine(repoRoot, "PolyPilot.Core", "Services", "CopilotService.Events.cs"));
Assert.Contains("ConsecutiveStuckCount = 0", eventsSource);
}
@@ -287,7 +287,7 @@ public void SendAsyncTimeoutSource_WrapsWithTaskWhenAny()
// Verify that SendAsync is wrapped with Task.WhenAny for timeout detection.
// The CancellationToken.None workaround remains, but we have a client-side timeout.
var repoRoot = GetRepoRoot();
- var source = File.ReadAllText(Path.Combine(repoRoot, "PolyPilot", "Services", "CopilotService.cs"));
+ var source = File.ReadAllText(Path.Combine(repoRoot, "PolyPilot.Core", "Services", "CopilotService.cs"));
Assert.Contains("Task.WhenAny(sendTask", source);
Assert.Contains("SendAsyncTimeoutMs", source);
@@ -299,7 +299,7 @@ public void SendAsyncTimeoutSource_RetryPathAlsoHasTimeout()
// Verify that the reconnect+retry SendAsync path ALSO has the timeout wrapper.
// Without this, the retry could hang indefinitely just like the primary send.
var repoRoot = GetRepoRoot();
- var source = File.ReadAllText(Path.Combine(repoRoot, "PolyPilot", "Services", "CopilotService.cs"));
+ var source = File.ReadAllText(Path.Combine(repoRoot, "PolyPilot.Core", "Services", "CopilotService.cs"));
Assert.Contains("Task.WhenAny(retryTask", source);
}
diff --git a/PolyPilot.Tests/LongRunningSessionSafetyTests.cs b/PolyPilot.Tests/LongRunningSessionSafetyTests.cs
index 9805baefbc..54c7e99425 100644
--- a/PolyPilot.Tests/LongRunningSessionSafetyTests.cs
+++ b/PolyPilot.Tests/LongRunningSessionSafetyTests.cs
@@ -71,7 +71,7 @@ private CopilotService CreateService() =>
private static class TestPaths
{
private static readonly string ProjectRoot = Path.GetFullPath(
- Path.Combine(AppContext.BaseDirectory, "..", "..", "..", "..", "PolyPilot"));
+ Path.Combine(AppContext.BaseDirectory, "..", "..", "..", "..", "PolyPilot.Core"));
public static string CopilotServiceCs => Path.Combine(ProjectRoot, "Services", "CopilotService.cs");
public static string EventsCs => Path.Combine(ProjectRoot, "Services", "CopilotService.Events.cs");
diff --git a/PolyPilot.Tests/MultiAgentRegressionTests.cs b/PolyPilot.Tests/MultiAgentRegressionTests.cs
index eb5b819709..ead49e1aeb 100644
--- a/PolyPilot.Tests/MultiAgentRegressionTests.cs
+++ b/PolyPilot.Tests/MultiAgentRegressionTests.cs
@@ -1448,7 +1448,7 @@ public void DiagnosticLogFilter_IncludesDispatchTag()
// The Debug() method's file filter must include [DISPATCH] so orchestration
// events are written to event-diagnostics.log for post-mortem analysis.
// This was a bug: [DISPATCH] was written to Console but not persisted.
- var source = File.ReadAllText(Path.Combine(GetRepoRoot(), "PolyPilot", "Services", "CopilotService.cs"));
+ var source = File.ReadAllText(Path.Combine(GetRepoRoot(), "PolyPilot.Core", "Services", "CopilotService.cs"));
Assert.Contains("[DISPATCH", source.Substring(source.IndexOf("message.StartsWith(\"[EVT")));
}
@@ -1458,7 +1458,7 @@ public void ReconnectState_ShouldCarryIsMultiAgentSession()
// After reconnect in SendPromptAsync, the new SessionState must carry forward
// IsMultiAgentSession from the old state. Without this, the watchdog uses the
// 120s inactivity timeout instead of 600s, killing long-running worker tasks.
- var source = File.ReadAllText(Path.Combine(GetRepoRoot(), "PolyPilot", "Services", "CopilotService.cs"));
+ var source = File.ReadAllText(Path.Combine(GetRepoRoot(), "PolyPilot.Core", "Services", "CopilotService.cs"));
// Find the reconnect block where state is replaced
var marker = "[RECONNECT] '{sessionName}' replacing state";
@@ -1479,7 +1479,7 @@ public void MonitorAndSynthesize_ShouldFilterByDispatchTimestamp()
// MonitorAndSynthesizeAsync must filter worker results by dispatch timestamp
// to avoid picking up stale pre-dispatch assistant messages from prior conversations.
// This was a 3/3 consensus finding from multi-model review.
- var source = File.ReadAllText(Path.Combine(GetRepoRoot(), "PolyPilot", "Services", "CopilotService.Organization.cs"));
+ var source = File.ReadAllText(Path.Combine(GetRepoRoot(), "PolyPilot.Core", "Services", "CopilotService.Organization.cs"));
// Find the result collection section in MonitorAndSynthesizeAsync
var monitorSection = source.Substring(source.IndexOf("Collect worker results from their chat history"));
@@ -1497,7 +1497,7 @@ public void PendingOrchestration_ShouldClearInFinallyBlock()
// ClearPendingOrchestration must be in a finally block so it's cleaned up
// even on cancellation/error. Otherwise stale pending files cause spurious
// resume on next launch. Opus review finding.
- var source = File.ReadAllText(Path.Combine(GetRepoRoot(), "PolyPilot", "Services", "CopilotService.Organization.cs"));
+ var source = File.ReadAllText(Path.Combine(GetRepoRoot(), "PolyPilot.Core", "Services", "CopilotService.Organization.cs"));
// Non-reflect path: must have finally { ClearPendingOrchestration }
var nonReflectDispatch = source.Substring(source.IndexOf("Phase 3: Dispatch tasks to workers"));
@@ -1525,7 +1525,7 @@ public void MonitorAndSynthesize_ShouldRedispatchUnstartedWorkers()
// When the app restarts and workers never started (TaskCanceledException killed dispatch),
// MonitorAndSynthesizeAsync should detect idle workers with no post-dispatch response
// and re-dispatch them instead of reporting "no response found".
- var source = File.ReadAllText(Path.Combine(GetRepoRoot(), "PolyPilot", "Services", "CopilotService.Organization.cs"));
+ var source = File.ReadAllText(Path.Combine(GetRepoRoot(), "PolyPilot.Core", "Services", "CopilotService.Organization.cs"));
var startIdx = source.IndexOf("private async Task MonitorAndSynthesizeAsync");
Assert.True(startIdx >= 0, "MonitorAndSynthesizeAsync method not found in source");
@@ -1566,7 +1566,7 @@ public void RetryOrchestration_ResetsReflectState()
{
// RetryOrchestrationAsync should reset the reflect state so the loop can re-enter.
// If ReflectionState.IsActive is false (loop completed), retry should re-activate it.
- var source = File.ReadAllText(Path.Combine(GetRepoRoot(), "PolyPilot", "Services", "CopilotService.Organization.cs"));
+ var source = File.ReadAllText(Path.Combine(GetRepoRoot(), "PolyPilot.Core", "Services", "CopilotService.Organization.cs"));
var startIdx = source.IndexOf("public async Task RetryOrchestrationAsync");
Assert.True(startIdx >= 0, "RetryOrchestrationAsync method not found in source");
@@ -1588,7 +1588,7 @@ public void RetryOrchestration_FallsBackToResumePrompt()
{
// When no explicit prompt is given and no user message found in history,
// RetryOrchestrationAsync should use a fallback resume instruction.
- var source = File.ReadAllText(Path.Combine(GetRepoRoot(), "PolyPilot", "Services", "CopilotService.Organization.cs"));
+ var source = File.ReadAllText(Path.Combine(GetRepoRoot(), "PolyPilot.Core", "Services", "CopilotService.Organization.cs"));
var startIdx = source.IndexOf("public async Task RetryOrchestrationAsync");
Assert.True(startIdx >= 0, "RetryOrchestrationAsync method not found in source");
@@ -2391,7 +2391,7 @@ public void PrematureIdleSignal_ExistsOnSessionState()
public void PrematureIdleSignal_SetInRearmPath()
{
// Structural: the EVT-REARM path must call PrematureIdleSignal.Set()
- var eventsPath = Path.Combine(GetRepoRoot(), "PolyPilot", "Services", "CopilotService.Events.cs");
+ var eventsPath = Path.Combine(GetRepoRoot(), "PolyPilot.Core", "Services", "CopilotService.Events.cs");
var source = File.ReadAllText(eventsPath);
// Find the EVT-REARM block
@@ -2407,7 +2407,7 @@ public void PrematureIdleSignal_SetInRearmPath()
public void PrematureIdleSignal_ResetInSendPromptAsync()
{
// Structural: SendPromptAsync must reset PrematureIdleSignal on each new turn
- var servicePath = Path.Combine(GetRepoRoot(), "PolyPilot", "Services", "CopilotService.cs");
+ var servicePath = Path.Combine(GetRepoRoot(), "PolyPilot.Core", "Services", "CopilotService.cs");
var source = File.ReadAllText(servicePath);
// Find SendPromptAsync method
@@ -2422,7 +2422,7 @@ public void PrematureIdleSignal_ResetInSendPromptAsync()
public void RecoverFromPrematureIdleIfNeededAsync_ExistsInOrganization()
{
// Structural: the recovery method must exist and be called from ExecuteWorkerAsync
- var orgPath = Path.Combine(GetRepoRoot(), "PolyPilot", "Services", "CopilotService.Organization.cs");
+ var orgPath = Path.Combine(GetRepoRoot(), "PolyPilot.Core", "Services", "CopilotService.Organization.cs");
var source = File.ReadAllText(orgPath);
Assert.Contains("RecoverFromPrematureIdleIfNeededAsync", source);
@@ -2439,7 +2439,7 @@ public void RecoverFromPrematureIdleIfNeededAsync_OnlyForMultiAgentSessions()
{
// Structural: the recovery check must be guarded by IsMultiAgentSession
// to avoid adding latency to normal single-session completions
- var orgPath = Path.Combine(GetRepoRoot(), "PolyPilot", "Services", "CopilotService.Organization.cs");
+ var orgPath = Path.Combine(GetRepoRoot(), "PolyPilot.Core", "Services", "CopilotService.Organization.cs");
var source = File.ReadAllText(orgPath);
var execIdx = source.IndexOf("private async Task ExecuteWorkerAsync", StringComparison.Ordinal);
@@ -2458,7 +2458,7 @@ public void RecoverFromPrematureIdleIfNeededAsync_SubscribesToOnSessionComplete(
{
// Structural: the recovery method must subscribe to OnSessionComplete to detect
// the worker's real completion (after premature idle re-arm)
- var orgPath = Path.Combine(GetRepoRoot(), "PolyPilot", "Services", "CopilotService.Organization.cs");
+ var orgPath = Path.Combine(GetRepoRoot(), "PolyPilot.Core", "Services", "CopilotService.Organization.cs");
var source = File.ReadAllText(orgPath);
// Find the method definition (not a call site)
@@ -2474,7 +2474,7 @@ public void RecoverFromPrematureIdleIfNeededAsync_SubscribesToOnSessionComplete(
public void RecoverFromPrematureIdleIfNeededAsync_HasDiskFallback()
{
// Structural: if History doesn't have full content, fall back to LoadHistoryFromDisk
- var orgPath = Path.Combine(GetRepoRoot(), "PolyPilot", "Services", "CopilotService.Organization.cs");
+ var orgPath = Path.Combine(GetRepoRoot(), "PolyPilot.Core", "Services", "CopilotService.Organization.cs");
var source = File.ReadAllText(orgPath);
// Find the method definition (not a call site)
@@ -2489,7 +2489,7 @@ public void RecoverFromPrematureIdleIfNeededAsync_HasDiskFallback()
public void RecoverFromPrematureIdleIfNeededAsync_HasDiagnosticLogging()
{
// Every recovery path must have diagnostic log entries
- var orgPath = Path.Combine(GetRepoRoot(), "PolyPilot", "Services", "CopilotService.Organization.cs");
+ var orgPath = Path.Combine(GetRepoRoot(), "PolyPilot.Core", "Services", "CopilotService.Organization.cs");
var source = File.ReadAllText(orgPath);
// Find the method definition (not a call site)
@@ -2505,7 +2505,7 @@ public void MutationBeforeCommit_SessionIdSetAfterTryUpdate()
{
// Structural: SessionId must be set AFTER TryUpdate succeeds, not before.
// This prevents mutating shared Info on a path that might discard the state.
- var orgPath = Path.Combine(GetRepoRoot(), "PolyPilot", "Services", "CopilotService.Organization.cs");
+ var orgPath = Path.Combine(GetRepoRoot(), "PolyPilot.Core", "Services", "CopilotService.Organization.cs");
var source = File.ReadAllText(orgPath);
// Find the revival block in ExecuteWorkerAsync
@@ -2529,7 +2529,7 @@ public void RecoverFromPrematureIdleIfNeededAsync_UsesEventsFileFreshness()
// Structural: recovery must check events.jsonl freshness as a parallel detection
// signal alongside WasPrematurelyIdled flag. This catches cases where EVT-REARM
// takes 30-60s to fire but the CLI is still writing events.
- var orgPath = Path.Combine(GetRepoRoot(), "PolyPilot", "Services", "CopilotService.Organization.cs");
+ var orgPath = Path.Combine(GetRepoRoot(), "PolyPilot.Core", "Services", "CopilotService.Organization.cs");
var source = File.ReadAllText(orgPath);
var methodIdx = source.IndexOf("private async Task RecoverFromPrematureIdleIfNeededAsync", StringComparison.Ordinal);
@@ -2543,7 +2543,7 @@ public void RecoverFromPrematureIdleIfNeededAsync_UsesEventsFileFreshness()
public void IsEventsFileActive_HelperExists()
{
// Structural: IsEventsFileActive must exist as a helper for events.jsonl freshness checks
- var orgPath = Path.Combine(GetRepoRoot(), "PolyPilot", "Services", "CopilotService.Organization.cs");
+ var orgPath = Path.Combine(GetRepoRoot(), "PolyPilot.Core", "Services", "CopilotService.Organization.cs");
var source = File.ReadAllText(orgPath);
var helperIdx = source.IndexOf("private bool IsEventsFileActive(", StringComparison.Ordinal);
@@ -2560,7 +2560,7 @@ public void RecoverFromPrematureIdleIfNeededAsync_LoopsOnRepeatedPrematureIdle()
// Structural: recovery must loop to handle repeated premature idle (observed: 4x in a row).
// After each OnSessionComplete, it checks if events.jsonl is still active before deciding
// the worker is truly done.
- var orgPath = Path.Combine(GetRepoRoot(), "PolyPilot", "Services", "CopilotService.Organization.cs");
+ var orgPath = Path.Combine(GetRepoRoot(), "PolyPilot.Core", "Services", "CopilotService.Organization.cs");
var source = File.ReadAllText(orgPath);
var methodIdx = source.IndexOf("private async Task RecoverFromPrematureIdleIfNeededAsync", StringComparison.Ordinal);
@@ -2578,7 +2578,7 @@ public void RecoverFromPrematureIdleIfNeededAsync_LoopsOnRepeatedPrematureIdle()
public void PrematureIdleEventsFileFreshnessSeconds_ConstantExists()
{
// The freshness threshold constant must exist and be reasonable (10-60s range)
- var orgPath = Path.Combine(GetRepoRoot(), "PolyPilot", "Services", "CopilotService.Organization.cs");
+ var orgPath = Path.Combine(GetRepoRoot(), "PolyPilot.Core", "Services", "CopilotService.Organization.cs");
var source = File.ReadAllText(orgPath);
Assert.Contains("PrematureIdleEventsFileFreshnessSeconds", source);
@@ -2598,7 +2598,7 @@ public void PrematureIdleEventsGracePeriodMs_ConstantExists()
Assert.True(CopilotService.PrematureIdleEventsGracePeriodMs <= 5000,
"Grace period must be <= 5s to not delay normal completions excessively");
- var orgPath = Path.Combine(GetRepoRoot(), "PolyPilot", "Services", "CopilotService.Organization.cs");
+ var orgPath = Path.Combine(GetRepoRoot(), "PolyPilot.Core", "Services", "CopilotService.Organization.cs");
var source = File.ReadAllText(orgPath);
var constIdx = source.IndexOf("internal const int PrematureIdleEventsGracePeriodMs", StringComparison.Ordinal);
Assert.True(constIdx >= 0, "PrematureIdleEventsGracePeriodMs must be an internal const int");
@@ -2609,7 +2609,7 @@ public void GetEventsFileMtime_HelperExists()
{
// GetEventsFileMtime must exist as an internal helper returning DateTime?
// Used by RecoverFromPrematureIdleIfNeededAsync for mtime-comparison detection.
- var orgPath = Path.Combine(GetRepoRoot(), "PolyPilot", "Services", "CopilotService.Organization.cs");
+ var orgPath = Path.Combine(GetRepoRoot(), "PolyPilot.Core", "Services", "CopilotService.Organization.cs");
var source = File.ReadAllText(orgPath);
var helperIdx = source.IndexOf("internal DateTime? GetEventsFileMtime(", StringComparison.Ordinal);
@@ -2626,7 +2626,7 @@ public void RecoverFromPrematureIdleIfNeededAsync_UsesMtimeComparisonForInitialD
// Structural: instead of raw IsEventsFileActive (which sees the idle event's own write
// as "fresh" and false-positives), the method must snapshot mtime, wait the grace period,
// then compare mtimes. Only a changed mtime proves the CLI is still writing.
- var orgPath = Path.Combine(GetRepoRoot(), "PolyPilot", "Services", "CopilotService.Organization.cs");
+ var orgPath = Path.Combine(GetRepoRoot(), "PolyPilot.Core", "Services", "CopilotService.Organization.cs");
var source = File.ReadAllText(orgPath);
var methodIdx = source.IndexOf("private async Task RecoverFromPrematureIdleIfNeededAsync", StringComparison.Ordinal);
@@ -2651,7 +2651,7 @@ public void RecoverFromPrematureIdleIfNeededAsync_PollingLoopUsesMtimeComparison
// Structural: the secondary polling loop must also use mtime comparison (not raw
// IsEventsFileActive) so that a stale-but-fresh file doesn't trigger false detection
// in subsequent poll cycles.
- var orgPath = Path.Combine(GetRepoRoot(), "PolyPilot", "Services", "CopilotService.Organization.cs");
+ var orgPath = Path.Combine(GetRepoRoot(), "PolyPilot.Core", "Services", "CopilotService.Organization.cs");
var source = File.ReadAllText(orgPath);
var methodIdx = source.IndexOf("private async Task RecoverFromPrematureIdleIfNeededAsync", StringComparison.Ordinal);
diff --git a/PolyPilot.Tests/PermissionDenialDetectionTests.cs b/PolyPilot.Tests/PermissionDenialDetectionTests.cs
index bddab3e222..034fbe4412 100644
--- a/PolyPilot.Tests/PermissionDenialDetectionTests.cs
+++ b/PolyPilot.Tests/PermissionDenialDetectionTests.cs
@@ -236,7 +236,7 @@ public void RecoverableError_IncludesMcpFailure()
// STRUCTURAL REGRESSION GUARD: isRecoverableError must include isMcpFailure
// so that repeated MCP errors trigger auto-recovery (session reconnect).
var source = File.ReadAllText(
- Path.Combine(GetRepoRoot(), "PolyPilot", "Services", "CopilotService.Events.cs"));
+ Path.Combine(GetRepoRoot(), "PolyPilot.Core", "Services", "CopilotService.Events.cs"));
Assert.Contains("var isRecoverableError = isPermissionDenial || isShellFailure || isMcpFailure;", source);
}
@@ -245,7 +245,7 @@ public void McpRecoveryMessage_IncludesReloadHint()
{
// When MCP recovery triggers, the user message must mention /mcp reload
var source = File.ReadAllText(
- Path.Combine(GetRepoRoot(), "PolyPilot", "Services", "CopilotService.Events.cs"));
+ Path.Combine(GetRepoRoot(), "PolyPilot.Core", "Services", "CopilotService.Events.cs"));
Assert.Contains("/mcp reload", source);
}
@@ -270,7 +270,7 @@ public void ReloadMcpServersAsync_PreservesHistoryInPlace()
// in-place using _sessions.TryUpdate (preserving AgentSessionInfo/history).
// It must NOT call CreateSessionAsync with a renamed session (which loses history).
var source = File.ReadAllText(
- Path.Combine(GetRepoRoot(), "PolyPilot", "Services", "CopilotService.cs"));
+ Path.Combine(GetRepoRoot(), "PolyPilot.Core", "Services", "CopilotService.cs"));
Assert.Contains("_sessions.TryUpdate(sessionName, newState, state)", source);
Assert.Contains("Info = state.Info", source);
}
@@ -281,7 +281,7 @@ public void ReloadMcpServersAsync_ThrowsOnConcurrentReload()
// STRUCTURAL: Concurrent reload must throw (not silently succeed) so the Dashboard
// can display an honest error message instead of a false "✅ reloaded" confirmation.
var source = File.ReadAllText(
- Path.Combine(GetRepoRoot(), "PolyPilot", "Services", "CopilotService.cs"));
+ Path.Combine(GetRepoRoot(), "PolyPilot.Core", "Services", "CopilotService.cs"));
// The guard must throw, not return silently
Assert.Contains("throw new InvalidOperationException(\"MCP reload is already in progress", source);
}
@@ -299,7 +299,7 @@ public void ReloadMcpServersAsync_ClearsPermissionDenials_Unconditionally()
// accompanies the out-of-if-block call. If ClearPermissionDenials() were moved back
// inside the if-block, this comment would not be present and the test would fail.
var source = File.ReadAllText(
- Path.Combine(GetRepoRoot(), "PolyPilot", "Services", "CopilotService.cs"));
+ Path.Combine(GetRepoRoot(), "PolyPilot.Core", "Services", "CopilotService.cs"));
// The unconditional call is preceded by a comment that is unique to that call site.
Assert.Contains("Always clear the sliding-window denial queue regardless of processing state", source);
@@ -379,7 +379,7 @@ public void IsInitializationError_HelperIsInternal_CanBeTestedViaPublicSurface()
// The helper is internal — verify it's accessible from tests (InternalsVisibleTo).
// Also verify the helper exists in CopilotService.Utilities.cs at the right location.
var repoRoot = GetRepoRoot();
- var source = File.ReadAllText(Path.Combine(repoRoot, "PolyPilot", "Services", "CopilotService.Utilities.cs"));
+ var source = File.ReadAllText(Path.Combine(repoRoot, "PolyPilot.Core", "Services", "CopilotService.Utilities.cs"));
Assert.Contains("internal static bool IsInitializationError(Exception ex)", source);
Assert.Contains("not initialized", source);
}
@@ -390,7 +390,7 @@ public void ExecuteWorkerAsync_RetryGate_IncludesInitializationError()
// Structural: the retry catch in ExecuteWorkerAsync must check IsInitializationError
// so "Service not initialized" triggers a retry with lazy re-init, not an immediate fail.
var repoRoot = GetRepoRoot();
- var source = File.ReadAllText(Path.Combine(repoRoot, "PolyPilot", "Services", "CopilotService.Organization.cs"));
+ var source = File.ReadAllText(Path.Combine(repoRoot, "PolyPilot.Core", "Services", "CopilotService.Organization.cs"));
Assert.Contains("IsInitializationError(ex)", source);
// The retry gate must combine both checks
Assert.Contains("IsConnectionError(ex) || IsInitializationError(ex)", source);
diff --git a/PolyPilot.Tests/PlatformHelperTests.cs b/PolyPilot.Tests/PlatformHelperTests.cs
index 051fc91100..9f74fac2a8 100644
--- a/PolyPilot.Tests/PlatformHelperTests.cs
+++ b/PolyPilot.Tests/PlatformHelperTests.cs
@@ -18,6 +18,32 @@ public void IsMobile_OnTestHost_IsFalse()
Assert.False(PlatformHelper.IsMobile);
}
+ [Fact]
+ public void IsDesktop_And_IsMobile_AreExclusive()
+ {
+ // Desktop and mobile must never both be true at the same time
+ Assert.NotEqual(PlatformHelper.IsDesktop, PlatformHelper.IsMobile);
+ }
+
+ [Fact]
+ public void PlatformName_OnTestHost_IsKnownValue()
+ {
+ // On a desktop test host, PlatformName should be a recognized value, never "unknown"
+ var name = PlatformHelper.PlatformName;
+ Assert.Contains(name, new[] { "maccatalyst", "windows", "linux" });
+ }
+
+ [Fact]
+ public void PlatformName_NeverReturnsUnknown_OnDesktopHost()
+ {
+ // Regression guard: runtime checks must correctly identify the platform.
+ // OperatingSystem.IsIOS() returns true on Mac Catalyst, so the check order matters.
+ if (PlatformHelper.IsDesktop)
+ {
+ Assert.NotEqual("unknown", PlatformHelper.PlatformName);
+ }
+ }
+
[Fact]
public void AvailableModes_OnNonDesktop_IncludesRemote()
{
diff --git a/PolyPilot.Tests/PolyPilot.Tests.csproj b/PolyPilot.Tests/PolyPilot.Tests.csproj
index 96472642db..9aae6b3792 100644
--- a/PolyPilot.Tests/PolyPilot.Tests.csproj
+++ b/PolyPilot.Tests/PolyPilot.Tests.csproj
@@ -9,13 +9,10 @@
+
-
-
-
-
@@ -24,85 +21,15 @@
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
diff --git a/PolyPilot.Tests/ProcessingWatchdogTests.cs b/PolyPilot.Tests/ProcessingWatchdogTests.cs
index 5433cedf58..a6c65c3f58 100644
--- a/PolyPilot.Tests/ProcessingWatchdogTests.cs
+++ b/PolyPilot.Tests/ProcessingWatchdogTests.cs
@@ -2196,7 +2196,7 @@ public void WatchdogDecision_ActiveTool_ServerAlive_ShouldResetTimer()
// the watchdog must reset the inactivity timer rather than killing the session.
// This is verified via source-code assertion (since SessionState is private).
var source = File.ReadAllText(
- Path.Combine(GetRepoRoot(), "PolyPilot", "Services", "CopilotService.Events.cs"));
+ Path.Combine(GetRepoRoot(), "PolyPilot.Core", "Services", "CopilotService.Events.cs"));
var methodIdx = source.IndexOf("private async Task RunProcessingWatchdogAsync");
var elapsedIdx = source.IndexOf("elapsed >= effectiveTimeout", methodIdx);
// Find the end of the method dynamically (next top-level member after RunProcessingWatchdogAsync)
@@ -2230,7 +2230,7 @@ public void WatchdogDecision_NoActiveTool_ToolsWereUsed_ShouldCompleteCleanly()
// the watchdog must complete the session CLEANLY (call CompleteResponse, no error msg).
// This is the "SessionIdleEvent lost" scenario — response is done, just terminal event missed.
var source = File.ReadAllText(
- Path.Combine(GetRepoRoot(), "PolyPilot", "Services", "CopilotService.Events.cs"));
+ Path.Combine(GetRepoRoot(), "PolyPilot.Core", "Services", "CopilotService.Events.cs"));
var methodIdx = source.IndexOf("private async Task RunProcessingWatchdogAsync");
var elapsedIdx = source.IndexOf("elapsed >= effectiveTimeout", methodIdx);
var methodEndIdx = source.IndexOf(" private readonly ConcurrentDictionary", methodIdx);
@@ -2264,7 +2264,7 @@ public void WatchdogDecision_MaxTimeExceeded_AlwaysKills()
// When total processing time exceeds WatchdogMaxProcessingTimeSeconds, the watchdog
// must kill regardless of server liveness or tool state — no session runs forever.
var source = File.ReadAllText(
- Path.Combine(GetRepoRoot(), "PolyPilot", "Services", "CopilotService.Events.cs"));
+ Path.Combine(GetRepoRoot(), "PolyPilot.Core", "Services", "CopilotService.Events.cs"));
var methodIdx = source.IndexOf("private async Task RunProcessingWatchdogAsync");
var elapsedIdx = source.IndexOf("elapsed >= effectiveTimeout", methodIdx);
var methodEndIdx = source.IndexOf(" private readonly ConcurrentDictionary", methodIdx);
@@ -2287,7 +2287,7 @@ public void WatchdogPeriodicFlush_HasGenerationGuard()
// validate it inside the lambda — preventing stale watchdog ticks from flushing
// new-turn content into old-turn history if the user aborts + resends.
var source = File.ReadAllText(
- Path.Combine(GetRepoRoot(), "PolyPilot", "Services", "CopilotService.Events.cs"));
+ Path.Combine(GetRepoRoot(), "PolyPilot.Core", "Services", "CopilotService.Events.cs"));
var methodIdx = source.IndexOf("private async Task RunProcessingWatchdogAsync");
var flushCommentIdx = source.IndexOf("Periodic mid-watchdog flush", methodIdx);
Assert.True(flushCommentIdx > 0, "Periodic flush comment must exist in RunProcessingWatchdogAsync");
@@ -2313,7 +2313,7 @@ public void ExternalToolRequestedEvent_IsInEventMatrix()
// ExternalToolRequestedEvent was arriving as "Unhandled" in logs, causing spam
// and incorrectly updating LastEventAtTicks. It must be explicitly classified.
var source = File.ReadAllText(
- Path.Combine(GetRepoRoot(), "PolyPilot", "Services", "CopilotService.Events.cs"));
+ Path.Combine(GetRepoRoot(), "PolyPilot.Core", "Services", "CopilotService.Events.cs"));
Assert.True(source.Contains("ExternalToolRequestedEvent"),
"ExternalToolRequestedEvent must be explicitly listed in SdkEventMatrix to prevent 'Unhandled' log spam");
}
@@ -2324,7 +2324,7 @@ public void ExternalToolRequestedEvent_ClassifiedAsTimelineOnly()
// Must be TimelineOnly — it doesn't need chat projection but should not suppress
// LastEventAtTicks updates (it does represent live activity on the session).
var source = File.ReadAllText(
- Path.Combine(GetRepoRoot(), "PolyPilot", "Services", "CopilotService.Events.cs"));
+ Path.Combine(GetRepoRoot(), "PolyPilot.Core", "Services", "CopilotService.Events.cs"));
var idx = source.IndexOf("ExternalToolRequestedEvent");
Assert.True(idx >= 0);
var context = source.Substring(idx, 80);
@@ -2340,7 +2340,7 @@ public void WatchdogPeriodicFlush_PresenceInSource()
// The watchdog must flush CurrentResponse to History periodically so partial
// responses are visible even while IsProcessing=true (e.g., stuck mid-stream).
var source = File.ReadAllText(
- Path.Combine(GetRepoRoot(), "PolyPilot", "Services", "CopilotService.Events.cs"));
+ Path.Combine(GetRepoRoot(), "PolyPilot.Core", "Services", "CopilotService.Events.cs"));
var methodIdx = source.IndexOf("private async Task RunProcessingWatchdogAsync");
// The flush must be inside the watchdog method
var watchdogBody = source.Substring(methodIdx, 6000);
@@ -2356,7 +2356,7 @@ public void WatchdogPeriodicFlush_RunsBeforeTimeoutCheck()
// The periodic flush must run BEFORE the elapsed >= effectiveTimeout check,
// so content is visible even before the session is declared stuck.
var source = File.ReadAllText(
- Path.Combine(GetRepoRoot(), "PolyPilot", "Services", "CopilotService.Events.cs"));
+ Path.Combine(GetRepoRoot(), "PolyPilot.Core", "Services", "CopilotService.Events.cs"));
var methodIdx = source.IndexOf("private async Task RunProcessingWatchdogAsync");
var flushIdx = source.IndexOf("Periodic mid-watchdog flush", methodIdx);
var elapsedCheckIdx = source.IndexOf("elapsed >= effectiveTimeout", methodIdx);
@@ -2376,7 +2376,7 @@ public void WatchdogDecision_MultiAgent_NoTools_CaseBIncludesMultiAgent_InSource
// follow-on sub-turn. Without this, the 120s watchdog fires Case C (error kill) instead
// of Case B (clean complete), showing an unnecessary error message.
var source = File.ReadAllText(
- Path.Combine(GetRepoRoot(), "PolyPilot", "Services", "CopilotService.Events.cs"));
+ Path.Combine(GetRepoRoot(), "PolyPilot.Core", "Services", "CopilotService.Events.cs"));
var methodIdx = source.IndexOf("private async Task RunProcessingWatchdogAsync");
var endIdx = source.IndexOf(" private readonly ConcurrentDictionary", methodIdx);
var watchdogBody = source.Substring(methodIdx, endIdx - methodIdx);
@@ -2419,7 +2419,7 @@ public void AssistantTurnStartEvent_LoggedInEvtDiagnostics_InSource()
// event-diagnostics.log shows a gap with no explanation — making stuck-session
// forensics impossible (root cause of the PR #332 debug session bug).
var source = File.ReadAllText(
- Path.Combine(GetRepoRoot(), "PolyPilot", "Services", "CopilotService.Events.cs"));
+ Path.Combine(GetRepoRoot(), "PolyPilot.Core", "Services", "CopilotService.Events.cs"));
var evtLogIdx = source.IndexOf("[EVT]");
Assert.True(evtLogIdx >= 0, "[EVT] log must exist in event handler");
// The EVT filter block must include AssistantTurnStartEvent
@@ -2475,7 +2475,7 @@ public void CaseA_ExceedingMaxResets_FallsThroughToKill_InSource()
// This is the core fix for the stuck-session bug where a dead session's tool
// appears active but no events ever arrive.
var source = File.ReadAllText(
- Path.Combine(GetRepoRoot(), "PolyPilot", "Services", "CopilotService.Events.cs"));
+ Path.Combine(GetRepoRoot(), "PolyPilot.Core", "Services", "CopilotService.Events.cs"));
var methodIdx = source.IndexOf("private async Task RunProcessingWatchdogAsync");
Assert.True(methodIdx >= 0, "RunProcessingWatchdogAsync must exist");
var endIdx = source.IndexOf(" private readonly ConcurrentDictionary", methodIdx);
@@ -2496,7 +2496,7 @@ public void CaseA_ResetCounter_ClearedOnRealEvents_InSource()
// must be cleared. This proves the session's connection is alive, so future
// Case A resets should be fresh (not counting against the cap).
var source = File.ReadAllText(
- Path.Combine(GetRepoRoot(), "PolyPilot", "Services", "CopilotService.Events.cs"));
+ Path.Combine(GetRepoRoot(), "PolyPilot.Core", "Services", "CopilotService.Events.cs"));
var handlerIdx = source.IndexOf("private void HandleSessionEvent");
Assert.True(handlerIdx >= 0, "HandleSessionEvent must exist");
// Find the block that resets LastEventAtTicks (only for real events)
@@ -2515,7 +2515,7 @@ public void CaseA_ResetCounter_ClearedOnWatchdogStart_InSource()
// StartProcessingWatchdog must reset WatchdogCaseAResets to 0 so each new
// watchdog instance starts with a clean reset counter.
var source = File.ReadAllText(
- Path.Combine(GetRepoRoot(), "PolyPilot", "Services", "CopilotService.Events.cs"));
+ Path.Combine(GetRepoRoot(), "PolyPilot.Core", "Services", "CopilotService.Events.cs"));
var startIdx = source.IndexOf("private void StartProcessingWatchdog");
Assert.True(startIdx >= 0, "StartProcessingWatchdog must exist");
var methodEnd = source.IndexOf("_ = RunProcessingWatchdogAsync", startIdx);
@@ -2531,7 +2531,7 @@ public void ExceededMaxTime_TrueWhenProcessingStartedAtNull_InSource()
// exceededMaxTime must be true. Without this, Case A can reset forever
// because totalProcessingSeconds=0 makes exceededMaxTime always false.
var source = File.ReadAllText(
- Path.Combine(GetRepoRoot(), "PolyPilot", "Services", "CopilotService.Events.cs"));
+ Path.Combine(GetRepoRoot(), "PolyPilot.Core", "Services", "CopilotService.Events.cs"));
var methodIdx = source.IndexOf("private async Task RunProcessingWatchdogAsync");
Assert.True(methodIdx >= 0, "RunProcessingWatchdogAsync must exist");
var endIdx = source.IndexOf(" private readonly ConcurrentDictionary", methodIdx);
@@ -2558,7 +2558,7 @@ public void ReconnectPath_ResetsHasUsedToolsThisTurn_InSource()
// uses the 120s inactivity timeout, not the 600s tool timeout inherited from
// the dead connection. Without this, reconnected sessions wait 5x longer.
var source = File.ReadAllText(
- Path.Combine(GetRepoRoot(), "PolyPilot", "Services", "CopilotService.cs"));
+ Path.Combine(GetRepoRoot(), "PolyPilot.Core", "Services", "CopilotService.cs"));
var reconnectIdx = source.IndexOf("[RECONNECT] '{sessionName}' replacing state");
Assert.True(reconnectIdx >= 0, "Reconnect block must exist");
// Find StartProcessingWatchdog in the reconnect block (marks the end of state setup)
@@ -2579,7 +2579,7 @@ public void ReconnectPath_ResetsProcessingStartedAt_InSource()
// so the watchdog's max-time safety net measures from the reconnect time,
// not the original send time.
var source = File.ReadAllText(
- Path.Combine(GetRepoRoot(), "PolyPilot", "Services", "CopilotService.cs"));
+ Path.Combine(GetRepoRoot(), "PolyPilot.Core", "Services", "CopilotService.cs"));
var reconnectIdx = source.IndexOf("[RECONNECT] '{sessionName}' replacing state");
Assert.True(reconnectIdx >= 0, "Reconnect block must exist");
var watchdogIdx = source.IndexOf("StartProcessingWatchdog(state, sessionName)", reconnectIdx);
@@ -2596,7 +2596,7 @@ public void ReconnectPath_ResetsActiveToolCallCount_InSource()
// After reconnect, ActiveToolCallCount must be 0. No tools have started
// on the new connection. A stale count > 0 would trigger Case A resets.
var source = File.ReadAllText(
- Path.Combine(GetRepoRoot(), "PolyPilot", "Services", "CopilotService.cs"));
+ Path.Combine(GetRepoRoot(), "PolyPilot.Core", "Services", "CopilotService.cs"));
var reconnectIdx = source.IndexOf("[RECONNECT] '{sessionName}' replacing state");
Assert.True(reconnectIdx >= 0, "Reconnect block must exist");
var watchdogIdx = source.IndexOf("StartProcessingWatchdog(state, sessionName)", reconnectIdx);
@@ -2641,7 +2641,7 @@ public void WatchdogCaseB_UsesMultiAgentFreshness_InSource()
// Case B freshness check must select threshold based on isMultiAgentSession.
// This prevents premature force-completion of worker sessions (issue #365).
var source = File.ReadAllText(
- Path.Combine(GetRepoRoot(), "PolyPilot", "Services", "CopilotService.Events.cs"));
+ Path.Combine(GetRepoRoot(), "PolyPilot.Core", "Services", "CopilotService.Events.cs"));
var methodIdx = source.IndexOf("private async Task RunProcessingWatchdogAsync");
var endIdx = source.IndexOf(" private readonly ConcurrentDictionary", methodIdx);
var watchdogBody = source.Substring(methodIdx, endIdx - methodIdx);
@@ -2665,7 +2665,7 @@ public void WatchdogCaseB_FreshnessThresholdSelection_InSource()
// Verify that the freshness threshold is selected based on isMultiAgentSession
// using a ternary before the events.jsonl check.
var source = File.ReadAllText(
- Path.Combine(GetRepoRoot(), "PolyPilot", "Services", "CopilotService.Events.cs"));
+ Path.Combine(GetRepoRoot(), "PolyPilot.Core", "Services", "CopilotService.Events.cs"));
var methodIdx = source.IndexOf("private async Task RunProcessingWatchdogAsync");
var endIdx = source.IndexOf(" private readonly ConcurrentDictionary", methodIdx);
var watchdogBody = source.Substring(methodIdx, endIdx - methodIdx);
@@ -2686,7 +2686,7 @@ public void WatchdogCatchBlock_ClearsIsProcessing_InSource()
// MUST clear IsProcessing — otherwise the session is permanently stuck.
// Regression test for: sessions stuck at "Sending..." forever after watchdog crash.
var source = File.ReadAllText(
- Path.Combine(GetRepoRoot(), "PolyPilot", "Services", "CopilotService.Events.cs"));
+ Path.Combine(GetRepoRoot(), "PolyPilot.Core", "Services", "CopilotService.Events.cs"));
var methodIdx = source.IndexOf("private async Task RunProcessingWatchdogAsync");
var endIdx = source.IndexOf(" private readonly ConcurrentDictionary", methodIdx);
var watchdogBody = source.Substring(methodIdx, endIdx - methodIdx);
@@ -2719,7 +2719,7 @@ public void WatchdogCatchBlock_CompletesResponseCompletion_InSource()
// The crash recovery must complete the TCS so callers (e.g., orchestrators)
// waiting on SendPromptAsync aren't blocked forever.
var source = File.ReadAllText(
- Path.Combine(GetRepoRoot(), "PolyPilot", "Services", "CopilotService.Events.cs"));
+ Path.Combine(GetRepoRoot(), "PolyPilot.Core", "Services", "CopilotService.Events.cs"));
var methodIdx = source.IndexOf("private async Task RunProcessingWatchdogAsync");
var endIdx = source.IndexOf(" private readonly ConcurrentDictionary", methodIdx);
var watchdogBody = source.Substring(methodIdx, endIdx - methodIdx);
@@ -2746,7 +2746,7 @@ public void WatchdogKillCallback_ProtectsFlushCurrentResponse_InSource()
// wrapped in try-catch within the kill callback.
// Regression test for: sessions permanently stuck after FlushCurrentResponse failure.
var source = File.ReadAllText(
- Path.Combine(GetRepoRoot(), "PolyPilot", "Services", "CopilotService.Events.cs"));
+ Path.Combine(GetRepoRoot(), "PolyPilot.Core", "Services", "CopilotService.Events.cs"));
var methodIdx = source.IndexOf("private async Task RunProcessingWatchdogAsync");
var endIdx = source.IndexOf(" private readonly ConcurrentDictionary", methodIdx);
var watchdogBody = source.Substring(methodIdx, endIdx - methodIdx);
@@ -2927,7 +2927,7 @@ public void WatchdogCaseB_FileSizeGrowthCheck_InSource()
// window (especially the 1800s multi-agent window). Without a growth check,
// multi-agent sessions stay stuck for up to 30 minutes.
var source = File.ReadAllText(
- Path.Combine(GetRepoRoot(), "PolyPilot", "Services", "CopilotService.Events.cs"));
+ Path.Combine(GetRepoRoot(), "PolyPilot.Core", "Services", "CopilotService.Events.cs"));
var methodIdx = source.IndexOf("private async Task RunProcessingWatchdogAsync");
var endIdx = source.IndexOf(" private readonly ConcurrentDictionary", methodIdx);
var watchdogBody = source.Substring(methodIdx, endIdx - methodIdx);
@@ -2955,7 +2955,7 @@ public void WatchdogCaseB_StaleDetection_SkipsDeferral_InSource()
// When stale checks exceed the max, caseBEventsActive must be set to false
// so the deferral is skipped and the session is force-completed.
var source = File.ReadAllText(
- Path.Combine(GetRepoRoot(), "PolyPilot", "Services", "CopilotService.Events.cs"));
+ Path.Combine(GetRepoRoot(), "PolyPilot.Core", "Services", "CopilotService.Events.cs"));
var methodIdx = source.IndexOf("private async Task RunProcessingWatchdogAsync");
var endIdx = source.IndexOf(" private readonly ConcurrentDictionary", methodIdx);
var watchdogBody = source.Substring(methodIdx, endIdx - methodIdx);
@@ -2975,7 +2975,7 @@ public void WatchdogCaseB_GrowthResetsStaleCount_InSource()
// When events.jsonl grows between Case B checks, the stale counter must reset.
// This prevents false positives when the CLI is actively writing but slowly.
var source = File.ReadAllText(
- Path.Combine(GetRepoRoot(), "PolyPilot", "Services", "CopilotService.Events.cs"));
+ Path.Combine(GetRepoRoot(), "PolyPilot.Core", "Services", "CopilotService.Events.cs"));
var methodIdx = source.IndexOf("private async Task RunProcessingWatchdogAsync");
var endIdx = source.IndexOf(" private readonly ConcurrentDictionary", methodIdx);
var watchdogBody = source.Substring(methodIdx, endIdx - methodIdx);
@@ -3011,7 +3011,7 @@ public void WatchdogCaseB_StaleFieldsResetOnEventArrival_InSource()
// When real SDK events arrive, the Case B stale tracking must reset alongside
// WatchdogCaseBResets. This ensures a revived connection clears stale state.
var source = File.ReadAllText(
- Path.Combine(GetRepoRoot(), "PolyPilot", "Services", "CopilotService.Events.cs"));
+ Path.Combine(GetRepoRoot(), "PolyPilot.Core", "Services", "CopilotService.Events.cs"));
// Find the event handler section where CaseBResets is reset on real event arrival
var handlerSection = source.Substring(0, source.IndexOf("private async Task RunProcessingWatchdogAsync"));
@@ -3029,7 +3029,7 @@ public void WatchdogCaseB_StaleFieldsResetOnWatchdogStart_InSource()
// StartProcessingWatchdog must reset the new stale tracking fields
// alongside the existing WatchdogCaseBResets reset.
var source = File.ReadAllText(
- Path.Combine(GetRepoRoot(), "PolyPilot", "Services", "CopilotService.Events.cs"));
+ Path.Combine(GetRepoRoot(), "PolyPilot.Core", "Services", "CopilotService.Events.cs"));
// Find the StartProcessingWatchdog method
var startIdx = source.IndexOf("private void StartProcessingWatchdog(");
@@ -3049,7 +3049,7 @@ public void WatchdogCaseB_UsesFileInfoForSizeAndTime_InSource()
// filesystem call, avoiding a TOCTOU race between separate File.GetLastWriteTimeUtc
// and FileInfo.Length calls.
var source = File.ReadAllText(
- Path.Combine(GetRepoRoot(), "PolyPilot", "Services", "CopilotService.Events.cs"));
+ Path.Combine(GetRepoRoot(), "PolyPilot.Core", "Services", "CopilotService.Events.cs"));
var methodIdx = source.IndexOf("private async Task RunProcessingWatchdogAsync");
var endIdx = source.IndexOf(" private readonly ConcurrentDictionary", methodIdx);
var watchdogBody = source.Substring(methodIdx, endIdx - methodIdx);
diff --git a/PolyPilot.Tests/RenderThrottleTests.cs b/PolyPilot.Tests/RenderThrottleTests.cs
index eae7d1f4cc..f2d7f4378f 100644
--- a/PolyPilot.Tests/RenderThrottleTests.cs
+++ b/PolyPilot.Tests/RenderThrottleTests.cs
@@ -205,7 +205,7 @@ public void CompleteResponse_OnSessionComplete_FiresBeforeOnStateChanged()
// to bypass throttle. If order is reversed, throttle drops the render.
var eventsPath = Path.Combine(
AppDomain.CurrentDomain.BaseDirectory, "..", "..", "..", "..",
- "PolyPilot", "Services", "CopilotService.Events.cs");
+ "PolyPilot.Core", "Services", "CopilotService.Events.cs");
Assert.True(File.Exists(eventsPath), $"CopilotService.Events.cs not found at {eventsPath}");
var source = File.ReadAllText(eventsPath);
diff --git a/PolyPilot.Tests/SessionPersistenceTests.cs b/PolyPilot.Tests/SessionPersistenceTests.cs
index 991942ed84..4f13561b0e 100644
--- a/PolyPilot.Tests/SessionPersistenceTests.cs
+++ b/PolyPilot.Tests/SessionPersistenceTests.cs
@@ -458,7 +458,7 @@ public void MergeSessionEntries_PreservesLastPrompt()
public void RestorePreviousSessionsAsync_QueuesEagerResumeForInterruptedSessions()
{
var source = File.ReadAllText(
- Path.Combine(GetRepoRoot(), "PolyPilot", "Services", "CopilotService.Persistence.cs"));
+ Path.Combine(GetRepoRoot(), "PolyPilot.Core", "Services", "CopilotService.Persistence.cs"));
var placeholderIdx = source.IndexOf("Loaded session placeholder", StringComparison.Ordinal);
Assert.True(placeholderIdx > 0, "Placeholder restore block not found");
@@ -472,7 +472,7 @@ public void RestorePreviousSessionsAsync_QueuesEagerResumeForInterruptedSessions
public void RestorePreviousSessionsAsync_RunsInterruptedSessionResumesAfterPlaceholderLoad()
{
var source = File.ReadAllText(
- Path.Combine(GetRepoRoot(), "PolyPilot", "Services", "CopilotService.Persistence.cs"));
+ Path.Combine(GetRepoRoot(), "PolyPilot.Core", "Services", "CopilotService.Persistence.cs"));
var loadIdx = source.IndexOf("Loaded session placeholder", StringComparison.Ordinal);
var eagerResumeIdx = source.IndexOf("await EnsureSessionConnectedAsync(pendingResume.SessionName, pendingResume.State, cancellationToken)", StringComparison.Ordinal);
@@ -551,7 +551,7 @@ public void RestoreFallback_LoadsHistoryFromOldSession()
// RestorePreviousSessionsAsync must call LoadHistoryFromDisk(entry.SessionId)
// before CreateSessionAsync so conversation history is recovered.
var source = File.ReadAllText(
- Path.Combine(GetRepoRoot(), "PolyPilot", "Services", "CopilotService.Persistence.cs"));
+ Path.Combine(GetRepoRoot(), "PolyPilot.Core", "Services", "CopilotService.Persistence.cs"));
var fallbackIdx = source.IndexOf("Falling back to CreateSessionAsync", StringComparison.Ordinal);
Assert.True(fallbackIdx > 0, "Could not find fallback path in RestorePreviousSessionsAsync");
@@ -569,7 +569,7 @@ public void RestoreFallback_InjectsHistoryIntoRecreatedSession()
// STRUCTURAL REGRESSION GUARD: After CreateSessionAsync, the fallback must
// inject the recovered history into the new session's Info.History.
var source = File.ReadAllText(
- Path.Combine(GetRepoRoot(), "PolyPilot", "Services", "CopilotService.Persistence.cs"));
+ Path.Combine(GetRepoRoot(), "PolyPilot.Core", "Services", "CopilotService.Persistence.cs"));
var fallbackIdx = source.IndexOf("Falling back to CreateSessionAsync", StringComparison.Ordinal);
Assert.True(fallbackIdx > 0);
@@ -585,7 +585,7 @@ public void RestoreFallback_RestoresUsageStats()
// STRUCTURAL REGRESSION GUARD: The fallback must call RestoreUsageStats(entry)
// to preserve token counts, CreatedAt, and other stats from the old session.
var source = File.ReadAllText(
- Path.Combine(GetRepoRoot(), "PolyPilot", "Services", "CopilotService.Persistence.cs"));
+ Path.Combine(GetRepoRoot(), "PolyPilot.Core", "Services", "CopilotService.Persistence.cs"));
var fallbackIdx = source.IndexOf("Falling back to CreateSessionAsync", StringComparison.Ordinal);
Assert.True(fallbackIdx > 0);
@@ -600,7 +600,7 @@ public void RestoreFallback_SyncsHistoryToDatabase()
// STRUCTURAL REGRESSION GUARD: The fallback must sync recovered history
// to the chat database under the new session ID so it persists.
var source = File.ReadAllText(
- Path.Combine(GetRepoRoot(), "PolyPilot", "Services", "CopilotService.Persistence.cs"));
+ Path.Combine(GetRepoRoot(), "PolyPilot.Core", "Services", "CopilotService.Persistence.cs"));
var fallbackIdx = source.IndexOf("Falling back to CreateSessionAsync", StringComparison.Ordinal);
Assert.True(fallbackIdx > 0);
@@ -616,7 +616,7 @@ public void RestoreFallback_CopiesEventsJsonlToNewSessionDirectory()
// events.jsonl into the recreated session directory so a later restart
// can reload history from the new session ID.
var source = File.ReadAllText(
- Path.Combine(GetRepoRoot(), "PolyPilot", "Services", "CopilotService.Persistence.cs"));
+ Path.Combine(GetRepoRoot(), "PolyPilot.Core", "Services", "CopilotService.Persistence.cs"));
var fallbackIdx = source.IndexOf("Falling back to CreateSessionAsync", StringComparison.Ordinal);
if (fallbackIdx < 0)
@@ -635,7 +635,7 @@ public void RestoreFallback_NormalizesIncompleteToolAndReasoningEntries()
// STRUCTURAL REGRESSION GUARD: The fallback must mark stale incomplete
// tool-call and reasoning entries complete, matching ResumeSessionAsync.
var source = File.ReadAllText(
- Path.Combine(GetRepoRoot(), "PolyPilot", "Services", "CopilotService.Persistence.cs"));
+ Path.Combine(GetRepoRoot(), "PolyPilot.Core", "Services", "CopilotService.Persistence.cs"));
var fallbackIdx = source.IndexOf("Falling back to CreateSessionAsync", StringComparison.Ordinal);
Assert.True(fallbackIdx > 0);
@@ -653,7 +653,7 @@ public void RestoreFallback_AddsReconnectionIndicator()
// indicating the session was recreated with recovered history, so the
// user knows the session state was reconstructed.
var source = File.ReadAllText(
- Path.Combine(GetRepoRoot(), "PolyPilot", "Services", "CopilotService.Persistence.cs"));
+ Path.Combine(GetRepoRoot(), "PolyPilot.Core", "Services", "CopilotService.Persistence.cs"));
var fallbackIdx = source.IndexOf("Falling back to CreateSessionAsync", StringComparison.Ordinal);
Assert.True(fallbackIdx > 0);
@@ -671,7 +671,7 @@ public void RestoreFallback_MessageCount_SetAfterSystemMessage()
// message ("🔄 Session recreated") isn't counted, and the unread indicator
// doesn't trigger for it.
var source = File.ReadAllText(
- Path.Combine(GetRepoRoot(), "PolyPilot", "Services", "CopilotService.Persistence.cs"));
+ Path.Combine(GetRepoRoot(), "PolyPilot.Core", "Services", "CopilotService.Persistence.cs"));
var fallbackIdx = source.IndexOf("Falling back to CreateSessionAsync", StringComparison.Ordinal);
Assert.True(fallbackIdx > 0);
@@ -705,7 +705,7 @@ public void InitializeAsync_FlushesSessionsAfterRestore()
// Without this, active-sessions.json retains stale IDs and the next restart
// reads the wrong events.jsonl, causing history loss.
var source = File.ReadAllText(
- Path.Combine(GetRepoRoot(), "PolyPilot", "Services", "CopilotService.cs"));
+ Path.Combine(GetRepoRoot(), "PolyPilot.Core", "Services", "CopilotService.cs"));
// Find the RestorePreviousSessionsAsync call in InitializeAsync
var restoreCallIdx = source.IndexOf("await RestorePreviousSessionsAsync(cancellationToken);", StringComparison.Ordinal);
@@ -722,7 +722,7 @@ public void ReconnectAsync_FlushesSessionsAfterRestore()
// STRUCTURAL REGRESSION GUARD: Same as InitializeAsync — the ReconnectAsync
// path must also flush after restore to persist recreated session IDs.
var source = File.ReadAllText(
- Path.Combine(GetRepoRoot(), "PolyPilot", "Services", "CopilotService.cs"));
+ Path.Combine(GetRepoRoot(), "PolyPilot.Core", "Services", "CopilotService.cs"));
// Find all RestorePreviousSessionsAsync calls
var indices = new List();
@@ -756,7 +756,7 @@ public void Reconnect_CallsSaveActiveSessionsToDisk_AfterUpdatingSessionId()
// and _sessions[sessionName] = newState, SaveActiveSessionsToDisk() must be
// called so the new session ID is persisted immediately.
var source = File.ReadAllText(
- Path.Combine(GetRepoRoot(), "PolyPilot", "Services", "CopilotService.cs"));
+ Path.Combine(GetRepoRoot(), "PolyPilot.Core", "Services", "CopilotService.cs"));
// Find the specific assignment where the new state replaces the old one
var sessionsAssign = source.IndexOf("_sessions[sessionName] = newState", StringComparison.Ordinal);
@@ -776,7 +776,7 @@ public void Restore_DoesNotSkipSessionsBeforeFallbackCanHandleMissingEvents()
// short-circuit on missing events.jsonl before the existing fallback path
// can recreate legitimate never-used sessions.
var source = File.ReadAllText(
- Path.Combine(GetRepoRoot(), "PolyPilot", "Services", "CopilotService.Persistence.cs"));
+ Path.Combine(GetRepoRoot(), "PolyPilot.Core", "Services", "CopilotService.Persistence.cs"));
var restoreIdx = source.IndexOf("RestorePreviousSessionsAsync", StringComparison.Ordinal);
Assert.True(restoreIdx > 0);
@@ -792,7 +792,7 @@ public void SaveActiveSessionsToDisk_AcceptsEventsOrRecentDirectories()
// WriteActiveSessionsFile/SaveActiveSessionsToDisk must keep used sessions
// via events.jsonl and also preserve newly created directories briefly.
var source = File.ReadAllText(
- Path.Combine(GetRepoRoot(), "PolyPilot", "Services", "CopilotService.Persistence.cs"));
+ Path.Combine(GetRepoRoot(), "PolyPilot.Core", "Services", "CopilotService.Persistence.cs"));
var mergeCallIdx = source.IndexOf("MergeSessionEntries(entries", StringComparison.Ordinal);
Assert.True(mergeCallIdx > 0);
@@ -1415,7 +1415,7 @@ public void ResumeSessionAsync_ShouldUseActualSessionId_NotInputId()
// Structural test verifying that ResumeSessionAsync uses
// copilotSession.SessionId (actual) instead of sessionId (input).
var serviceFile = File.ReadAllText(
- Path.Combine(GetRepoRoot(), "PolyPilot", "Services", "CopilotService.cs"));
+ Path.Combine(GetRepoRoot(), "PolyPilot.Core", "Services", "CopilotService.cs"));
// Verify the session ID mismatch detection block exists
Assert.Contains("[RESUME-REMAP] Session ID changed", serviceFile);
@@ -1432,7 +1432,7 @@ public void ReconnectPath_ShouldDetectSessionIdMismatch()
{
// Structural test verifying the reconnect path also detects ID mismatch
var serviceFile = File.ReadAllText(
- Path.Combine(GetRepoRoot(), "PolyPilot", "Services", "CopilotService.cs"));
+ Path.Combine(GetRepoRoot(), "PolyPilot.Core", "Services", "CopilotService.cs"));
Assert.Contains("[RECONNECT] Session ID changed on resume", serviceFile);
Assert.Contains("CopyEventsToNewSession(state.Info.SessionId, actualId)", serviceFile);
@@ -1444,7 +1444,7 @@ public void FallbackRestore_ShouldUseFindBestEventsSource()
// Structural test verifying the fallback restore path uses FindBestEventsSource
// instead of always loading from the stale entry.SessionId
var persistenceFile = File.ReadAllText(
- Path.Combine(GetRepoRoot(), "PolyPilot", "Services", "CopilotService.Persistence.cs"));
+ Path.Combine(GetRepoRoot(), "PolyPilot.Core", "Services", "CopilotService.Persistence.cs"));
Assert.Contains("FindBestEventsSource(entry.SessionId, entry.WorkingDirectory)", persistenceFile);
Assert.Contains("LoadBestHistoryAsync(bestSourceId)", persistenceFile);
@@ -1455,7 +1455,7 @@ public void CopyEventsToNewSession_SharedHelper_ExistsInPersistence()
{
// Structural test verifying CopyEventsToNewSession is a reusable helper
var persistenceFile = File.ReadAllText(
- Path.Combine(GetRepoRoot(), "PolyPilot", "Services", "CopilotService.Persistence.cs"));
+ Path.Combine(GetRepoRoot(), "PolyPilot.Core", "Services", "CopilotService.Persistence.cs"));
// Should be a private method (not inlined copy logic)
Assert.Contains("private void CopyEventsToNewSession(string oldSessionId, string newSessionId)", persistenceFile);
@@ -1604,7 +1604,7 @@ public void FindBestEventsSource_LimitsDirectoryScan()
{
// Structural test: verify MaxSessionDirsToScan constant exists for performance bounding
var persistenceFile = File.ReadAllText(
- Path.Combine(GetRepoRoot(), "PolyPilot", "Services", "CopilotService.Persistence.cs"));
+ Path.Combine(GetRepoRoot(), "PolyPilot.Core", "Services", "CopilotService.Persistence.cs"));
Assert.Contains("MaxSessionDirsToScan", persistenceFile);
Assert.Contains(".Take(MaxSessionDirsToScan)", persistenceFile);
@@ -1616,7 +1616,7 @@ public void CopyEventsToNewSession_CleansTempFileOnFailure()
{
// Structural test: verify temp file cleanup in catch block
var persistenceFile = File.ReadAllText(
- Path.Combine(GetRepoRoot(), "PolyPilot", "Services", "CopilotService.Persistence.cs"));
+ Path.Combine(GetRepoRoot(), "PolyPilot.Core", "Services", "CopilotService.Persistence.cs"));
// Verify tmpFile is declared outside try and cleaned in catch
Assert.Contains("string? tmpFile = null;", persistenceFile);
diff --git a/PolyPilot.Tests/SessionStabilityTests.cs b/PolyPilot.Tests/SessionStabilityTests.cs
index babaf81f89..781da26c19 100644
--- a/PolyPilot.Tests/SessionStabilityTests.cs
+++ b/PolyPilot.Tests/SessionStabilityTests.cs
@@ -368,12 +368,14 @@ private static int CountPatterns(string text, string[] patterns)
///
private static class TestPaths
{
- private static readonly string ProjectRoot = Path.GetFullPath(
+ private static readonly string CoreRoot = Path.GetFullPath(
+ Path.Combine(AppContext.BaseDirectory, "..", "..", "..", "..", "PolyPilot.Core"));
+ private static readonly string UiRoot = Path.GetFullPath(
Path.Combine(AppContext.BaseDirectory, "..", "..", "..", "..", "PolyPilot"));
- public static string CopilotServiceCs => Path.Combine(ProjectRoot, "Services", "CopilotService.cs");
- public static string EventsCs => Path.Combine(ProjectRoot, "Services", "CopilotService.Events.cs");
- public static string OrganizationCs => Path.Combine(ProjectRoot, "Services", "CopilotService.Organization.cs");
- public static string SessionSidebarRazor => Path.Combine(ProjectRoot, "Components", "Layout", "SessionSidebar.razor");
+ public static string CopilotServiceCs => Path.Combine(CoreRoot, "Services", "CopilotService.cs");
+ public static string EventsCs => Path.Combine(CoreRoot, "Services", "CopilotService.Events.cs");
+ public static string OrganizationCs => Path.Combine(CoreRoot, "Services", "CopilotService.Organization.cs");
+ public static string SessionSidebarRazor => Path.Combine(UiRoot, "Components", "Layout", "SessionSidebar.razor");
}
}
diff --git a/PolyPilot.Tests/SteeringMessageTests.cs b/PolyPilot.Tests/SteeringMessageTests.cs
index 991bf82512..596d90576e 100644
--- a/PolyPilot.Tests/SteeringMessageTests.cs
+++ b/PolyPilot.Tests/SteeringMessageTests.cs
@@ -341,7 +341,7 @@ public async Task SteerSession_SoftSteerPath_ContainsUserMessageAddition()
System.IO.Path.Combine(
System.IO.Path.GetDirectoryName(typeof(CopilotService).Assembly.Location)!
.Replace("PolyPilot.Tests", "PolyPilot"),
- "..", "..", "..", "..", "PolyPilot", "Services", "CopilotService.cs"));
+ "..", "..", "..", "..", "PolyPilot.Core", "Services", "CopilotService.cs"));
// Find the soft steer block (between "[STEER]" debug log and hard steer comment)
var softSteerStart = source.IndexOf("[STEER] '{sessionName}' soft steer");
@@ -366,7 +366,7 @@ public async Task SteerSession_SoftSteerPath_ContainsConnectionErrorFallback()
System.IO.Path.Combine(
System.IO.Path.GetDirectoryName(typeof(CopilotService).Assembly.Location)!
.Replace("PolyPilot.Tests", "PolyPilot"),
- "..", "..", "..", "..", "PolyPilot", "Services", "CopilotService.cs"));
+ "..", "..", "..", "..", "PolyPilot.Core", "Services", "CopilotService.cs"));
var softSteerStart = source.IndexOf("[STEER] '{sessionName}' soft steer");
Assert.True(softSteerStart >= 0, "Soft steer debug log not found");
diff --git a/PolyPilot.slnx b/PolyPilot.slnx
index 49f6f1b63e..ed668e0f92 100644
--- a/PolyPilot.slnx
+++ b/PolyPilot.slnx
@@ -2,6 +2,7 @@
+
diff --git a/PolyPilot/PolyPilot.csproj b/PolyPilot/PolyPilot.csproj
index 38bbdd976e..f63e6615cb 100644
--- a/PolyPilot/PolyPilot.csproj
+++ b/PolyPilot/PolyPilot.csproj
@@ -68,6 +68,7 @@
+
diff --git a/docs/multi-agent-orchestration.md b/docs/multi-agent-orchestration.md
index 5d516c86f5..e34c7c89cc 100644
--- a/docs/multi-agent-orchestration.md
+++ b/docs/multi-agent-orchestration.md
@@ -10,13 +10,13 @@ PolyPilot's multi-agent system lets you create a **team of AI sessions** that wo
| File | Purpose |
|------|---------|
-| `PolyPilot/Services/CopilotService.Organization.cs` | Orchestration engine (dispatch, reflection loop, reconciliation, group deletion) |
-| `PolyPilot/Models/SessionOrganization.cs` | `SessionGroup`, `SessionMeta`, `MultiAgentMode`, `MultiAgentRole` |
-| `PolyPilot/Models/ReflectionCycle.cs` | Reflection state, stall detection, sentinel parsing, evaluator prompts |
-| `PolyPilot/Models/ModelCapabilities.cs` | `GroupPreset`, `UserPresets` (three-tier merge), built-in presets |
-| `PolyPilot/Models/SquadDiscovery.cs` | Squad directory parser (`.squad/` → `GroupPreset`) |
-| `PolyPilot/Models/SquadWriter.cs` | Squad directory writer (`GroupPreset` → `.squad/`) |
-| `PolyPilot/Services/CopilotService.Events.cs` | TCS completion (IsProcessing → TrySetResult ordering) |
+| `PolyPilot.Core/Services/CopilotService.Organization.cs` | Orchestration engine (dispatch, reflection loop, reconciliation, group deletion) |
+| `PolyPilot.Core/Models/SessionOrganization.cs` | `SessionGroup`, `SessionMeta`, `MultiAgentMode`, `MultiAgentRole` |
+| `PolyPilot.Core/Models/ReflectionCycle.cs` | Reflection state, stall detection, sentinel parsing, evaluator prompts |
+| `PolyPilot.Core/Models/ModelCapabilities.cs` | `GroupPreset`, `UserPresets` (three-tier merge), built-in presets |
+| `PolyPilot.Core/Models/SquadDiscovery.cs` | Squad directory parser (`.squad/` → `GroupPreset`) |
+| `PolyPilot.Core/Models/SquadWriter.cs` | Squad directory writer (`GroupPreset` → `.squad/`) |
+| `PolyPilot.Core/Services/CopilotService.Events.cs` | TCS completion (IsProcessing → TrySetResult ordering) |
| `PolyPilot/Components/Layout/SessionSidebar.razor` | Preset picker UI (sectioned: From Repo / Built-in / My Presets) |
| `PolyPilot.Tests/MultiAgentRegressionTests.cs` | 37 regression tests covering all known bugs |
| `PolyPilot.Tests/SessionOrganizationTests.cs` | 15 grouping stability tests |