Skip to content
Draft
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
49 changes: 38 additions & 11 deletions .claude/commands/spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,12 +72,14 @@ When the user signals they're ready:

1. Re-read the spec file with the Read tool.
2. Collect all `> **Review:** ...` markers and note any direct edits.
3. If any review comment is ambiguous or requires a design decision,
ask clarifying questions first (again, all at once).
**PAUSE if you asked questions — wait for answers before editing.**
4. Update the spec: apply changes, resolve review markers by removing them
or incorporating the feedback, and update Open Questions accordingly.
5. Tell the user what changed and invite another review pass.
3. Address review comments **one at a time** in document order:
a. Present your analysis of the comment — the trade-offs, your
recommendation, and why.
b. **PAUSE — wait for the user's decision before editing.**
c. Update the spec to reflect the resolved decision; remove the
review marker.
d. Tell the user what changed, then move to the next comment.
4. After all comments are resolved, invite another review pass.

Repeat Phase 3 until the user says the document is ready.

Expand Down Expand Up @@ -117,16 +119,29 @@ When Phase 4 is complete:
- Exit criteria as a checkbox list; for tasks that include new E2E tests,
write those exit criteria as Gherkin-style acceptance scenarios
(`Given / When / Then`)
3. Save the updated spec and ask the user to review the task breakdown.
3. If the spec has a `## Related Epics` section listing features to be
spec'd separately, add those as placeholder entries in `## Tasks` as
well — titled "Create epic: \<name\>" with a one-line scope description.
These will become Jira epics (not tasks) in step 5.
4. Save the updated spec and ask the user to review the task breakdown.
**PAUSE — wait for approval or change requests. Apply any changes
before proceeding.**
4. Create a Jira task for each item. If the user provided an epic key,
assign it as the parent of all tasks. If not, create tasks without a
parent — the parent can be added later.
5. Update the `## Tasks` section: replace each task title with a hyperlink
5. Create Jira issues for each item:
- For tasks: create as Task issues. If the user provided an epic key,
assign it as the parent. If not, create without a parent.
- For "Create epic" placeholder items: create as Epic issues (no parent).
Use the scope description as the epic summary.
6. Update the `## Tasks` section: replace each item title with a hyperlink
to its Jira ticket. Keep all descriptions and exit criteria in place.
The section remains in the spec permanently — future agents may not
have Jira access.
7. Update the `## Related Epics` table with the Jira keys assigned to each
related epic in step 5.
8. Update the Jira epic's description with a concise summary of the
finalized design decisions from the spec. The original description
typically contains early design thoughts that are now superseded; replace
it with a brief overview and a bulleted list of the key decisions and
their outcomes. Link to the spec file in the repo.

---

Expand Down Expand Up @@ -177,6 +192,18 @@ Planned classes, their roles, and important relationships.

How data moves through the feature from trigger to output.

## Related Epics

Features identified during spec drafting that are out of scope here and will
be spec'd separately. Each row becomes a Jira epic in Phase 5.

| Epic | Scope |
|------|-------|
| (this epic) | ... |
| ADR-XXX | ... |

_(Omit this section if there are no related epics to create.)_

## Open Questions

- [ ] Unresolved question (carry forward any unresolved TBDs from above)
Expand Down
3 changes: 2 additions & 1 deletion .claude/settings.local.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@
"mcp__jira__getJiraIssue",
"Bash(xargs:*)",
"mcp__jira__createJiraIssue",
"mcp__jira__editJiraIssue"
"mcp__jira__editJiraIssue",
"mcp__jira__getJiraProjectIssueTypesMetadata"
]
}
}
48 changes: 48 additions & 0 deletions AdaptiveRemote.sln
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ VisualStudioVersion = 18.0.11217.181
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AdaptiveRemote.App", "src\AdaptiveRemote.App\AdaptiveRemote.App.csproj", "{6C7C380B-D7A4-412E-8487-2AFC89EA802F}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AdaptiveRemote.Contracts", "src\AdaptiveRemote.Contracts\AdaptiveRemote.Contracts.csproj", "{F81FEF3B-DB7A-4C04-9DF0-72E98382097A}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AdaptiveRemote", "src\AdaptiveRemote\AdaptiveRemote.csproj", "{7BE31162-0D09-4F80-8CE5-978F7AECC1EF}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AdaptiveRemote.Console", "src\AdaptiveRemote.Console\AdaptiveRemote.Console.csproj", "{345B73FC-07F9-490F-B566-2677D10B1834}"
Expand Down Expand Up @@ -46,6 +48,14 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AdaptiveRemote.EndToEndTest
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AdaptiveRemote.EndToEndTests.Host.Wpf", "test\AdaptiveRemote.EndToEndTests.Host.Wpf\AdaptiveRemote.EndToEndTests.Host.Wpf.csproj", "{54522D5A-CEB3-F5B9-2654-1005EF1C3262}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{827E0CD3-B72D-47B6-A68D-7590B98EB39B}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AdaptiveRemote.Backend.CompiledLayoutService", "src\AdaptiveRemote.Backend.CompiledLayoutService\AdaptiveRemote.Backend.CompiledLayoutService.csproj", "{ADEA7AD3-C614-4280-A6BA-DE412C4D6FBF}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{0C88DD14-F956-CE84-757C-A364CCF449FC}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AdaptiveRemote.Backend.ApiTests", "test\AdaptiveRemote.Backend.ApiTests\AdaptiveRemote.Backend.ApiTests.csproj", "{E581823B-8EA9-4C54-A05E-859632CE1B78}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand All @@ -68,6 +78,18 @@ Global
{6C7C380B-D7A4-412E-8487-2AFC89EA802F}.Release|x64.Build.0 = Release|Any CPU
{6C7C380B-D7A4-412E-8487-2AFC89EA802F}.Release|x86.ActiveCfg = Release|Any CPU
{6C7C380B-D7A4-412E-8487-2AFC89EA802F}.Release|x86.Build.0 = Release|Any CPU
{F81FEF3B-DB7A-4C04-9DF0-72E98382097A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F81FEF3B-DB7A-4C04-9DF0-72E98382097A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F81FEF3B-DB7A-4C04-9DF0-72E98382097A}.Debug|x64.ActiveCfg = Debug|Any CPU
{F81FEF3B-DB7A-4C04-9DF0-72E98382097A}.Debug|x64.Build.0 = Debug|Any CPU
{F81FEF3B-DB7A-4C04-9DF0-72E98382097A}.Debug|x86.ActiveCfg = Debug|Any CPU
{F81FEF3B-DB7A-4C04-9DF0-72E98382097A}.Debug|x86.Build.0 = Debug|Any CPU
{F81FEF3B-DB7A-4C04-9DF0-72E98382097A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F81FEF3B-DB7A-4C04-9DF0-72E98382097A}.Release|Any CPU.Build.0 = Release|Any CPU
{F81FEF3B-DB7A-4C04-9DF0-72E98382097A}.Release|x64.ActiveCfg = Release|Any CPU
{F81FEF3B-DB7A-4C04-9DF0-72E98382097A}.Release|x64.Build.0 = Release|Any CPU
{F81FEF3B-DB7A-4C04-9DF0-72E98382097A}.Release|x86.ActiveCfg = Release|Any CPU
{F81FEF3B-DB7A-4C04-9DF0-72E98382097A}.Release|x86.Build.0 = Release|Any CPU
{7BE31162-0D09-4F80-8CE5-978F7AECC1EF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{7BE31162-0D09-4F80-8CE5-978F7AECC1EF}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7BE31162-0D09-4F80-8CE5-978F7AECC1EF}.Debug|x64.ActiveCfg = Debug|Any CPU
Expand Down Expand Up @@ -188,6 +210,30 @@ Global
{54522D5A-CEB3-F5B9-2654-1005EF1C3262}.Release|x64.Build.0 = Release|Any CPU
{54522D5A-CEB3-F5B9-2654-1005EF1C3262}.Release|x86.ActiveCfg = Release|Any CPU
{54522D5A-CEB3-F5B9-2654-1005EF1C3262}.Release|x86.Build.0 = Release|Any CPU
{ADEA7AD3-C614-4280-A6BA-DE412C4D6FBF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{ADEA7AD3-C614-4280-A6BA-DE412C4D6FBF}.Debug|Any CPU.Build.0 = Debug|Any CPU
{ADEA7AD3-C614-4280-A6BA-DE412C4D6FBF}.Debug|x64.ActiveCfg = Debug|Any CPU
{ADEA7AD3-C614-4280-A6BA-DE412C4D6FBF}.Debug|x64.Build.0 = Debug|Any CPU
{ADEA7AD3-C614-4280-A6BA-DE412C4D6FBF}.Debug|x86.ActiveCfg = Debug|Any CPU
{ADEA7AD3-C614-4280-A6BA-DE412C4D6FBF}.Debug|x86.Build.0 = Debug|Any CPU
{ADEA7AD3-C614-4280-A6BA-DE412C4D6FBF}.Release|Any CPU.ActiveCfg = Release|Any CPU
{ADEA7AD3-C614-4280-A6BA-DE412C4D6FBF}.Release|Any CPU.Build.0 = Release|Any CPU
{ADEA7AD3-C614-4280-A6BA-DE412C4D6FBF}.Release|x64.ActiveCfg = Release|Any CPU
{ADEA7AD3-C614-4280-A6BA-DE412C4D6FBF}.Release|x64.Build.0 = Release|Any CPU
{ADEA7AD3-C614-4280-A6BA-DE412C4D6FBF}.Release|x86.ActiveCfg = Release|Any CPU
{ADEA7AD3-C614-4280-A6BA-DE412C4D6FBF}.Release|x86.Build.0 = Release|Any CPU
{E581823B-8EA9-4C54-A05E-859632CE1B78}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{E581823B-8EA9-4C54-A05E-859632CE1B78}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E581823B-8EA9-4C54-A05E-859632CE1B78}.Debug|x64.ActiveCfg = Debug|Any CPU
{E581823B-8EA9-4C54-A05E-859632CE1B78}.Debug|x64.Build.0 = Debug|Any CPU
{E581823B-8EA9-4C54-A05E-859632CE1B78}.Debug|x86.ActiveCfg = Debug|Any CPU
{E581823B-8EA9-4C54-A05E-859632CE1B78}.Debug|x86.Build.0 = Debug|Any CPU
{E581823B-8EA9-4C54-A05E-859632CE1B78}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E581823B-8EA9-4C54-A05E-859632CE1B78}.Release|Any CPU.Build.0 = Release|Any CPU
{E581823B-8EA9-4C54-A05E-859632CE1B78}.Release|x64.ActiveCfg = Release|Any CPU
{E581823B-8EA9-4C54-A05E-859632CE1B78}.Release|x64.Build.0 = Release|Any CPU
{E581823B-8EA9-4C54-A05E-859632CE1B78}.Release|x86.ActiveCfg = Release|Any CPU
{E581823B-8EA9-4C54-A05E-859632CE1B78}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand All @@ -202,6 +248,8 @@ Global
{72062D2E-6FDF-42F8-8360-98130E2A9861} = {CC3DAD92-6D91-40F5-B57A-C5620CF4F1C7}
{F631ED02-DB0B-4CE4-8462-89BA239AFB3A} = {CC3DAD92-6D91-40F5-B57A-C5620CF4F1C7}
{54522D5A-CEB3-F5B9-2654-1005EF1C3262} = {CC3DAD92-6D91-40F5-B57A-C5620CF4F1C7}
{ADEA7AD3-C614-4280-A6BA-DE412C4D6FBF} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
{E581823B-8EA9-4C54-A05E-859632CE1B78} = {0C88DD14-F956-CE84-757C-A364CCF449FC}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {556A11E4-2F89-4600-9831-8162F067EC3E}
Expand Down
10 changes: 10 additions & 0 deletions backend.slnf
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"solution": {
"path": "AdaptiveRemote.sln",
"projects": [
"src\\AdaptiveRemote.Contracts\\AdaptiveRemote.Contracts.csproj",
"src\\AdaptiveRemote.Backend.CompiledLayoutService\\AdaptiveRemote.Backend.CompiledLayoutService.csproj",
"test\\AdaptiveRemote.Backend.ApiTests\\AdaptiveRemote.Backend.ApiTests.csproj"
]
}
}
19 changes: 19 additions & 0 deletions client.slnf
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"solution": {
"path": "AdaptiveRemote.sln",
"projects": [
"src\\AdaptiveRemote.Contracts\\AdaptiveRemote.Contracts.csproj",
"src\\AdaptiveRemote.App\\AdaptiveRemote.App.csproj",
"src\\AdaptiveRemote\\AdaptiveRemote.csproj",
"src\\AdaptiveRemote.Console\\AdaptiveRemote.Console.csproj",
"src\\AdaptiveRemote.Headless\\AdaptiveRemote.Headless.csproj",
"test\\AdaptiveRemote.App.Tests\\AdaptiveRemote.App.Tests.csproj",
"test\\AdaptiveRemote.Speech.Tests\\AdaptiveRemote.Speech.Tests.csproj",
"test\\AdaptiveRemote.EndtoEndTests.TestServices\\AdaptiveRemote.EndtoEndTests.TestServices.csproj",
"test\\AdaptiveRemote.EndToEndTests.Steps\\AdaptiveRemote.EndToEndTests.Steps.csproj",
"test\\AdaptiveRemote.EndToEndTests.Host.Headless\\AdaptiveRemote.EndToEndTests.Host.Headless.csproj",
"test\\AdaptiveRemote.EndToEndTests.Host.Wpf\\AdaptiveRemote.EndToEndTests.Host.Wpf.csproj",
"test\\AdaptiveRemote.EndtoEndTests.Host.Console\\AdaptiveRemote.EndToEndTests.Host.Console.csproj"
]
}
}
18 changes: 18 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
version: '3.8'

services:
compiledlayoutservice:
build:
context: .
dockerfile: src/AdaptiveRemote.Backend.CompiledLayoutService/Dockerfile
ports:
- "8080:8080"
environment:
- ASPNETCORE_ENVIRONMENT=Development
- ASPNETCORE_URLS=http://+:8080
networks:
- backend

networks:
backend:
driver: bridge
4 changes: 4 additions & 0 deletions src/AdaptiveRemote.App/AdaptiveRemote.App.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@
<InternalsVisibleTo Include="AdaptiveRemote.EndtoEndTests.TestServices" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\AdaptiveRemote.Contracts\AdaptiveRemote.Contracts.csproj" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="Azure.Identity" />
<PackageReference Include="Azure.Monitor.OpenTelemetry.Exporter" />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk.Web">

<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<RootNamespace>AdaptiveRemote.Backend.CompiledLayoutService</RootNamespace>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\AdaptiveRemote.Contracts\AdaptiveRemote.Contracts.csproj" />
</ItemGroup>

</Project>
24 changes: 24 additions & 0 deletions src/AdaptiveRemote.Backend.CompiledLayoutService/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
WORKDIR /src

# Copy csproj files and restore
COPY ["src/AdaptiveRemote.Contracts/AdaptiveRemote.Contracts.csproj", "AdaptiveRemote.Contracts/"]
COPY ["src/AdaptiveRemote.Backend.CompiledLayoutService/AdaptiveRemote.Backend.CompiledLayoutService.csproj", "AdaptiveRemote.Backend.CompiledLayoutService/"]
COPY ["Directory.Build.props", "./"]
COPY ["Directory.Packages.props", "./"]
RUN dotnet restore "AdaptiveRemote.Backend.CompiledLayoutService/AdaptiveRemote.Backend.CompiledLayoutService.csproj"

# Copy source and build
COPY ["src/AdaptiveRemote.Contracts/", "AdaptiveRemote.Contracts/"]
COPY ["src/AdaptiveRemote.Backend.CompiledLayoutService/", "AdaptiveRemote.Backend.CompiledLayoutService/"]
WORKDIR "/src/AdaptiveRemote.Backend.CompiledLayoutService"
RUN dotnet build "AdaptiveRemote.Backend.CompiledLayoutService.csproj" -c Release -o /app/build

FROM build AS publish
RUN dotnet publish "AdaptiveRemote.Backend.CompiledLayoutService.csproj" -c Release -o /app/publish /p:UseAppHost=false

FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS final
WORKDIR /app
COPY --from=publish /app/publish .
EXPOSE 8080
ENTRYPOINT ["dotnet", "AdaptiveRemote.Backend.CompiledLayoutService.dll"]
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
using System.Reflection;
using AdaptiveRemote.Backend.CompiledLayoutService.Logging;
using AdaptiveRemote.Contracts;

namespace AdaptiveRemote.Backend.CompiledLayoutService.Endpoints;

public static class HealthEndpoints
{
public static void MapHealthEndpoints(this IEndpointRouteBuilder app)
{
app.MapGet("/health", GetHealth)
.WithName(nameof(GetHealth))
.Produces<HealthResponse>(StatusCodes.Status200OK);
}

private static IResult GetHealth(ILogger<Program> logger)
{
logger.HealthCheckRequested();

string? version = Assembly.GetExecutingAssembly()
.GetCustomAttribute<AssemblyInformationalVersionAttribute>()
?.InformationalVersion ?? "unknown";

HealthResponse response = new HealthResponse(
ServiceName: "CompiledLayoutService",
Version: version,
Status: "healthy"
);

logger.HealthCheckSuccessful();

return Results.Ok(response);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
using AdaptiveRemote.Backend.CompiledLayoutService.Logging;
using AdaptiveRemote.Contracts;

namespace AdaptiveRemote.Backend.CompiledLayoutService.Endpoints;

public static class LayoutEndpoints
{
public static void MapLayoutEndpoints(this IEndpointRouteBuilder app)
{
app.MapGet("/layouts/compiled/active", GetActiveLayout)
.WithName(nameof(GetActiveLayout))
.Produces<CompiledLayout>(StatusCodes.Status200OK);
}

private static async Task<IResult> GetActiveLayout(
ILogger<Program> logger,
ICompiledLayoutRepository repository,
CancellationToken cancellationToken)
{
logger.GetActiveLayoutRequested();

// For MVP, we use a hardcoded userId. Auth will provide real userId in ADR-168.
string userId = "mvp-user";
CompiledLayout? layout = await repository.GetActiveForUserAsync(userId, cancellationToken);

if (layout == null)
{
return Results.NotFound();
}

logger.ReturningActiveLayout(layout.Id);

// Use the LayoutContractsJsonContext for serialization
return Results.Json(
layout,
LayoutContractsJsonContext.Default.CompiledLayout);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
using Microsoft.Extensions.Logging;

namespace AdaptiveRemote.Backend.CompiledLayoutService.Logging;

/// <summary>
/// Centralized logging messages for CompiledLayoutService.
/// All log messages MUST be defined here as [LoggerMessage] source-generated methods.
/// Event ID ranges:
/// 1100-1199: CompiledLayoutService
/// </summary>
public static partial class MessageLogger
{
[LoggerMessage(EventId = 1100, Level = LogLevel.Information, Message = "CompiledLayoutService starting")]
public static partial void ServiceStarting(this ILogger logger);

[LoggerMessage(EventId = 1101, Level = LogLevel.Information, Message = "CompiledLayoutService started successfully on {ListenAddress}")]
public static partial void ServiceStarted(this ILogger logger, string listenAddress);

[LoggerMessage(EventId = 1102, Level = LogLevel.Information, Message = "GET /layouts/compiled/active request received")]
public static partial void GetActiveLayoutRequested(this ILogger logger);

[LoggerMessage(EventId = 1103, Level = LogLevel.Information, Message = "Returning active compiled layout Id={LayoutId}")]
public static partial void ReturningActiveLayout(this ILogger logger, Guid layoutId);

[LoggerMessage(EventId = 1104, Level = LogLevel.Information, Message = "GET /health request received")]
public static partial void HealthCheckRequested(this ILogger logger);

[LoggerMessage(EventId = 1105, Level = LogLevel.Information, Message = "Health check successful")]
public static partial void HealthCheckSuccessful(this ILogger logger);

[LoggerMessage(EventId = 1106, Level = LogLevel.Error, Message = "Error retrieving active layout for userId={UserId}")]
public static partial void ErrorRetrievingActiveLayout(this ILogger logger, string userId, Exception exception);

[LoggerMessage(EventId = 1107, Level = LogLevel.Error, Message = "Error processing health check request")]
public static partial void ErrorProcessingHealthCheck(this ILogger logger, Exception exception);
}
Loading