Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,9 @@ For this app:
- `DotPilot/DotPilot.csproj` keeps `GenerateDocumentationFile=true` with `CS1591` suppressed so `IDE0005` stays enforceable in CI across all target frameworks without inventing command-line-only build flags
- architecture work must keep a vertical-slice shape: each feature owns its contracts, orchestration, and tests behind clear boundaries instead of growing a shared horizontal service layer
- keep the Uno app project presentation-only; domain, runtime host, orchestration, integrations, and persistence code must live in separate class-library projects so UI composition does not mix with feature implementation
- GitHub is the backlog, not the product: use issues and PRs only to drive task scope and traceability, and never copy GitHub issue text, labels, workflow language, or tracker metadata into production code, runtime snapshots, or user-facing UI
- Desktop responsiveness is a product requirement: avoid synchronous probe, filesystem, network, or process work on UI-facing construction and navigation paths so the app stays fast and immediately reactive
- Do not invent a repo-specific product framing such as "workbench" unless the active issue or feature spec explicitly uses it; implement the app features described in the backlog instead of turning internal implementation language into the product narrative
- GitHub Actions workflows must use descriptive names and filenames that reflect their purpose; do not use a generic `ci.yml` catch-all because build validation and release automation are separate operator flows
- GitHub Actions must be split into at least one validation workflow for normal builds/tests and one release workflow for CI-driven version resolution, release-note generation, desktop publishing, and GitHub Release publication
- meaningful GitHub review comments must be evaluated and fixed when they still apply even if the original PR was closed; closed review threads are not a reason to ignore valid engineering feedback
Expand Down Expand Up @@ -302,6 +305,7 @@ Local `AGENTS.md` files may tighten these values, but they must not loosen them
- Hardcoded values are forbidden.
- String literals are forbidden in implementation code. Declare them once as named constants, enums, configuration entries, or dedicated value objects, then reuse those symbols.
- Avoid magic literals. Extract shared values into constants, enums, configuration, or dedicated types.
- Backlog metadata does not belong in product code: issue numbers, PR numbers, review language, and planning terminology must never appear in production runtime models, diagnostics, or user-facing text unless the feature explicitly exposes source-control metadata.
- Design boundaries so real behaviour can be tested through public interfaces.
- For `.NET`, the repo-root `.editorconfig` is the source of truth for formatting, naming, style, and analyzer severity.
- Use nested `.editorconfig` files when they serve a clear subtree-specific purpose. Do not let IDE defaults, pipeline flags, and repo config disagree.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,38 +1,44 @@
using DotPilot.Core.Features.ControlPlaneDomain;
using DotPilot.Core.Features.RuntimeFoundation;
using DotPilot.Runtime.Features.ToolchainCenter;

namespace DotPilot.Runtime.Features.RuntimeFoundation;

public sealed class RuntimeFoundationCatalog : IRuntimeFoundationCatalog
{
private const string EpicSummary =
"Issue #12 is staged into isolated contracts, communication, host, and orchestration slices so the Uno workbench can stay presentation-only.";
"Runtime contracts, host sequencing, and orchestration seams stay isolated so the Uno app can remain presentation-only.";
private const string EpicLabelValue = "LOCAL RUNTIME READINESS";
private const string DeterministicProbePrompt =
"Summarize the runtime foundation readiness for a local-first session that may require approval.";
private const string DeterministicClientStatusSummary = "Always available for in-repo and CI validation.";
private const string DomainModelLabel = "DOMAIN";
private const string DomainModelName = "Domain contracts";
private const string DomainModelSummary =
"Typed identifiers and durable agent, session, fleet, provider, and runtime contracts live outside the Uno app.";
private const string CommunicationLabel = "CONTRACTS";
private const string CommunicationName = "Communication contracts";
private const string CommunicationSummary =
"Public result and problem boundaries are isolated so later provider and orchestration slices share one contract language.";
private const string HostLabel = "HOST";
private const string HostName = "Embedded host";
private const string HostSummary =
"The Orleans host integration point is sequenced behind dedicated runtime contracts instead of being baked into page code.";
private const string OrchestrationLabel = "ORCHESTRATION";
private const string OrchestrationName = "Orchestration runtime";
private const string OrchestrationSummary =
"Agent Framework integration is prepared as a separate slice that can plug into the embedded host without reshaping the UI layer.";
private readonly IReadOnlyList<ProviderDescriptor> _providers;

public RuntimeFoundationCatalog() => _providers = CreateProviders();

public RuntimeFoundationSnapshot GetSnapshot()
{
return new(
RuntimeFoundationIssues.FormatIssueLabel(RuntimeFoundationIssues.EmbeddedAgentRuntimeHostEpic),
EpicLabelValue,
EpicSummary,
ProviderToolchainNames.DeterministicClientDisplayName,
DeterministicProbePrompt,
CreateSlices(),
CreateProviders());
_providers);
}

private static IReadOnlyList<RuntimeSliceDescriptor> CreateSlices()
Expand All @@ -41,25 +47,25 @@ private static IReadOnlyList<RuntimeSliceDescriptor> CreateSlices()
[
new(
RuntimeFoundationIssues.DomainModel,
RuntimeFoundationIssues.FormatIssueLabel(RuntimeFoundationIssues.DomainModel),
DomainModelLabel,
DomainModelName,
DomainModelSummary,
RuntimeSliceState.ReadyForImplementation),
new(
RuntimeFoundationIssues.CommunicationContracts,
RuntimeFoundationIssues.FormatIssueLabel(RuntimeFoundationIssues.CommunicationContracts),
CommunicationLabel,
CommunicationName,
CommunicationSummary,
RuntimeSliceState.Sequenced),
new(
RuntimeFoundationIssues.EmbeddedOrleansHost,
RuntimeFoundationIssues.FormatIssueLabel(RuntimeFoundationIssues.EmbeddedOrleansHost),
HostLabel,
HostName,
HostSummary,
RuntimeSliceState.Sequenced),
new(
RuntimeFoundationIssues.AgentFrameworkRuntime,
RuntimeFoundationIssues.FormatIssueLabel(RuntimeFoundationIssues.AgentFrameworkRuntime),
OrchestrationLabel,
OrchestrationName,
OrchestrationSummary,
RuntimeSliceState.Sequenced),
Expand All @@ -79,8 +85,6 @@ private static IReadOnlyList<ProviderDescriptor> CreateProviders()
StatusSummary = DeterministicClientStatusSummary,
RequiresExternalToolchain = false,
},
.. ToolchainProviderSnapshotFactory.Create(TimeProvider.System.GetUtcNow())
.Select(snapshot => snapshot.Provider),
];
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,25 +4,31 @@ namespace DotPilot.Runtime.Features.ToolchainCenter;

public sealed class ToolchainCenterCatalog : IToolchainCenterCatalog, IDisposable
{
private const string EpicLabelValue = "PRESESSION READINESS";
private const string EpicSummary =
"Issue #14 keeps provider installation, auth, diagnostics, configuration, and polling visible before the first live session.";
"Provider installation, launch checks, authentication, configuration, and refresh state stay visible before the first live session.";
private const string UiWorkstreamLabel = "SURFACE";
private const string UiWorkstreamName = "Toolchain Center UI";
private const string UiWorkstreamSummary =
"The settings shell exposes a first-class desktop Toolchain Center with provider cards, detail panes, and operator actions.";
private const string DiagnosticsWorkstreamLabel = "DIAGNOSTICS";
private const string DiagnosticsWorkstreamName = "Connection diagnostics";
private const string DiagnosticsWorkstreamSummary =
"Launch, connection, resume, tool access, and auth diagnostics stay attributable before live work starts.";
private const string ConfigurationWorkstreamLabel = "CONFIGURATION";
private const string ConfigurationWorkstreamName = "Secrets and environment";
private const string ConfigurationWorkstreamSummary =
"Provider secrets, local overrides, and non-secret environment configuration stay visible without leaking values.";
private const string PollingWorkstreamLabel = "POLLING";
private const string PollingWorkstreamName = "Background polling";
private const string PollingWorkstreamSummary =
"Version and auth readiness refresh in the background so the workbench can surface stale state early.";
"Version and auth readiness refresh in the background so the app can surface stale state early.";
private readonly TimeProvider _timeProvider;
private readonly CancellationTokenSource _disposeTokenSource = new();
private readonly PeriodicTimer? _pollingTimer;
private readonly Task _pollingTask;
private ToolchainCenterSnapshot _snapshot;
private int _disposeState;

public ToolchainCenterCatalog()
: this(TimeProvider.System, startBackgroundPolling: true)
Expand All @@ -46,13 +52,29 @@ public ToolchainCenterCatalog(TimeProvider timeProvider, bool startBackgroundPol
}
}

public ToolchainCenterSnapshot GetSnapshot() => _snapshot;
public ToolchainCenterSnapshot GetSnapshot() => Volatile.Read(ref _snapshot);

public void Dispose()
{
if (Interlocked.Exchange(ref _disposeState, 1) != 0)
{
return;
}

_disposeTokenSource.Cancel();
_pollingTimer?.Dispose();

try
{
_pollingTask.GetAwaiter().GetResult();
}
catch (OperationCanceledException)
{
// Expected during shutdown.
}

_disposeTokenSource.Dispose();
GC.SuppressFinalize(this);
}

private async Task PollAsync()
Expand All @@ -66,21 +88,25 @@ private async Task PollAsync()
{
while (await _pollingTimer.WaitForNextTickAsync(_disposeTokenSource.Token))
{
_snapshot = EvaluateSnapshot();
Volatile.Write(ref _snapshot, EvaluateSnapshot());
}
}
catch (OperationCanceledException)
{
// Expected during app shutdown.
}
catch (ObjectDisposedException) when (_disposeTokenSource.IsCancellationRequested)
{
// Expected when the timer is disposed during shutdown.
}
}

private ToolchainCenterSnapshot EvaluateSnapshot()
{
var evaluatedAt = _timeProvider.GetUtcNow();
var providers = ToolchainProviderSnapshotFactory.Create(evaluatedAt);
return new(
ToolchainCenterIssues.FormatIssueLabel(ToolchainCenterIssues.ToolchainCenterEpic),
EpicLabelValue,
EpicSummary,
CreateWorkstreams(),
providers,
Expand All @@ -95,22 +121,22 @@ private static IReadOnlyList<ToolchainCenterWorkstreamDescriptor> CreateWorkstre
[
new(
ToolchainCenterIssues.ToolchainCenterUi,
ToolchainCenterIssues.FormatIssueLabel(ToolchainCenterIssues.ToolchainCenterUi),
UiWorkstreamLabel,
UiWorkstreamName,
UiWorkstreamSummary),
new(
ToolchainCenterIssues.ConnectionDiagnostics,
ToolchainCenterIssues.FormatIssueLabel(ToolchainCenterIssues.ConnectionDiagnostics),
DiagnosticsWorkstreamLabel,
DiagnosticsWorkstreamName,
DiagnosticsWorkstreamSummary),
new(
ToolchainCenterIssues.ProviderConfiguration,
ToolchainCenterIssues.FormatIssueLabel(ToolchainCenterIssues.ProviderConfiguration),
ConfigurationWorkstreamLabel,
ConfigurationWorkstreamName,
ConfigurationWorkstreamSummary),
new(
ToolchainCenterIssues.BackgroundPolling,
ToolchainCenterIssues.FormatIssueLabel(ToolchainCenterIssues.BackgroundPolling),
PollingWorkstreamLabel,
PollingWorkstreamName,
PollingWorkstreamSummary),
];
Expand Down
74 changes: 60 additions & 14 deletions DotPilot.Runtime/Features/ToolchainCenter/ToolchainCommandProbe.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,23 @@ internal static class ToolchainCommandProbe
{
private static readonly TimeSpan CommandTimeout = TimeSpan.FromSeconds(2);
private const string VersionSeparator = "version";
private const string EmptyOutput = "";

public static string? ResolveExecutablePath(string commandName) =>
RuntimeFoundation.ProviderToolchainProbe.ResolveExecutablePath(commandName);

public static string ReadVersion(string executablePath, IReadOnlyList<string> arguments)
=> ProbeVersion(executablePath, arguments).Version;

public static ToolchainVersionProbeResult ProbeVersion(string executablePath, IReadOnlyList<string> arguments)
{
ArgumentException.ThrowIfNullOrWhiteSpace(executablePath);
ArgumentNullException.ThrowIfNull(arguments);

var execution = Execute(executablePath, arguments);
if (!execution.Succeeded)
{
return string.Empty;
return ToolchainVersionProbeResult.Missing with { Launched = execution.Launched };
}

var output = string.IsNullOrWhiteSpace(execution.StandardOutput)
Expand All @@ -31,13 +35,15 @@ public static string ReadVersion(string executablePath, IReadOnlyList<string> ar

if (string.IsNullOrWhiteSpace(firstLine))
{
return string.Empty;
return ToolchainVersionProbeResult.Missing with { Launched = execution.Launched };
}

var separatorIndex = firstLine.IndexOf(VersionSeparator, StringComparison.OrdinalIgnoreCase);
return separatorIndex >= 0
var version = separatorIndex >= 0
? firstLine[(separatorIndex + VersionSeparator.Length)..].Trim(' ', ':')
: firstLine.Trim();

return new(execution.Launched, version);
}

public static bool CanExecute(string executablePath, IReadOnlyList<string> arguments)
Expand All @@ -64,22 +70,57 @@ private static ToolchainCommandExecution Execute(string executablePath, IReadOnl
startInfo.ArgumentList.Add(argument);
}

using var process = Process.Start(startInfo);
Process? process;
try
{
process = Process.Start(startInfo);
}
catch
{
return ToolchainCommandExecution.LaunchFailed;
}

if (process is null)
{
return ToolchainCommandExecution.Failed;
return ToolchainCommandExecution.LaunchFailed;
}

if (!process.WaitForExit((int)CommandTimeout.TotalMilliseconds))
using (process)
{
TryTerminate(process);
return ToolchainCommandExecution.Failed;
var standardOutputTask = process.StandardOutput.ReadToEndAsync();
var standardErrorTask = process.StandardError.ReadToEndAsync();

if (!process.WaitForExit((int)CommandTimeout.TotalMilliseconds))
{
TryTerminate(process);
ObserveRedirectedStreamFaults(standardOutputTask, standardErrorTask);
return new(true, false, EmptyOutput, EmptyOutput);
}

return new(
true,
process.ExitCode == 0,
AwaitStreamRead(standardOutputTask),
AwaitStreamRead(standardErrorTask));
}
}

return new(
process.ExitCode == 0,
process.StandardOutput.ReadToEnd(),
process.StandardError.ReadToEnd());
private static string AwaitStreamRead(Task<string> readTask)
{
try
{
return readTask.GetAwaiter().GetResult();
}
catch
{
return EmptyOutput;
}
}

private static void ObserveRedirectedStreamFaults(Task<string> standardOutputTask, Task<string> standardErrorTask)
{
_ = standardOutputTask.Exception;
_ = standardErrorTask.Exception;
}
Comment on lines +120 to 124

private static void TryTerminate(Process process)
Expand All @@ -97,8 +138,13 @@ private static void TryTerminate(Process process)
}
}

private readonly record struct ToolchainCommandExecution(bool Succeeded, string StandardOutput, string StandardError)
public readonly record struct ToolchainVersionProbeResult(bool Launched, string Version)
{
public static ToolchainVersionProbeResult Missing => new(false, EmptyOutput);
}

private readonly record struct ToolchainCommandExecution(bool Launched, bool Succeeded, string StandardOutput, string StandardError)
{
public static ToolchainCommandExecution Failed => new(false, string.Empty, string.Empty);
public static ToolchainCommandExecution LaunchFailed => new(false, false, EmptyOutput, EmptyOutput);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ namespace DotPilot.Runtime.Features.ToolchainCenter;

internal sealed record ToolchainProviderProfile(
int IssueNumber,
string SectionLabel,
string DisplayName,
string CommandName,
IReadOnlyList<string> VersionArguments,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,16 @@ internal static class ToolchainProviderProfiles
private const string GitHubTokenSummary = "GitHub token for Copilot and GitHub CLI authenticated flows.";
private const string GitHubHostTokenSummary = "Alternative GitHub host token for CLI-authenticated Copilot flows.";
private const string GitHubModelsApiKeySummary = "Optional BYOK key for GitHub Models-backed Copilot routing.";
private const string CodexSectionLabel = "CODEX";
private const string ClaudeSectionLabel = "CLAUDE";
private const string GitHubSectionLabel = "GITHUB";
private static readonly string[] VersionArguments = ["--version"];

public static IReadOnlyList<ToolchainProviderProfile> All { get; } =
[
new(
ToolchainCenterIssues.CodexReadiness,
CodexSectionLabel,
ProviderToolchainNames.CodexDisplayName,
ProviderToolchainNames.CodexCommandName,
VersionArguments,
Expand All @@ -40,6 +44,7 @@ internal static class ToolchainProviderProfiles
]),
new(
ToolchainCenterIssues.ClaudeCodeReadiness,
ClaudeSectionLabel,
ProviderToolchainNames.ClaudeCodeDisplayName,
ProviderToolchainNames.ClaudeCodeCommandName,
VersionArguments,
Expand All @@ -55,6 +60,7 @@ internal static class ToolchainProviderProfiles
]),
new(
ToolchainCenterIssues.GitHubCopilotReadiness,
GitHubSectionLabel,
ProviderToolchainNames.GitHubCopilotDisplayName,
ProviderToolchainNames.GitHubCopilotCommandName,
VersionArguments,
Expand Down
Loading
Loading