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
27 changes: 19 additions & 8 deletions .claude/commands/spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,14 +72,18 @@ 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. 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.
3. Present the **first unresolved** review comment only:
- State the comment and its location.
- Give your analysis — trade-offs, recommendation, and why.
**PAUSE — wait for the user's decision before doing anything else.**
4. Once the user decides:
a. Update the spec to reflect the decision; remove the review marker.
b. Tell the user what changed.
c. Present the **next** unresolved review comment (go to step 3).
5. After all comments are resolved, invite another review pass.

**Never present more than one review comment at a time. Always pause for a
decision before moving to the next comment or making any edits.**

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

Expand Down Expand Up @@ -119,6 +123,13 @@ 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`)

**Implementation ordering:** Order tasks so that primary consumers are
implemented before the dependencies they rely on. For each dependency that
isn't ready yet, introduce a stub that provides known-good static data
(e.g. hardcoded or serialized from the current implementation). Replace
stubs with real implementations one step at a time. Every task's exit
criteria must include: all existing unit and E2E tests pass.
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.
Expand Down
87 changes: 71 additions & 16 deletions .claude/commands/team-task.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,16 @@ this task from spec to merged PR. Keep your own context lean:
- Direct sub-agents to read from and write to GitHub PR and Jira directly — do not relay
large payloads through yourself

**Before starting**, capture the current working branch — this is the **base branch**
(PR target, Developer branches off this):
**Before starting**, capture the base branch — this is the PR target and the branch the
Developer will branch off. The skill may be invoked on an ephemeral harness branch, so
find the upstream remote branch at the current commit rather than using `git branch --show-current`:

```bash
# Find the remote branch this harness branch was created from
git branch -r --contains HEAD | grep -v 'HEAD\|claude/' | head -1 | sed 's|.*origin/||' | xargs echo
```
git branch --show-current
```

If that returns nothing (no matching remote branch), fall back to `git branch --show-current`.

---

Expand Down Expand Up @@ -126,6 +130,11 @@ Developer instructions:
> **Task key:** TASK_KEY
> **Base branch:** BASE_BRANCH
>
> **Non-negotiable build rule:** After making any changes, you MUST run
> `scripts/validate-build.sh` before committing. Never run `dotnet build` directly.
> Never run `dotnet test`, `scripts/validate-tests.sh`, or any test command — a
> dedicated Tester agent handles all testing.
>
> **Task brief:**
> TASK_BRIEF
>
Expand Down Expand Up @@ -168,7 +177,7 @@ Developer instructions:
>
> **Step 4 — Build**
>
> Do not run tests — that is the Tester agent's responsibility. Only run the build:
> Run the build validation script and nothing else:
>
> ```
> scripts/validate-build.sh
Expand All @@ -177,6 +186,9 @@ Developer instructions:
> The script stages new files and cleans before building — do not run `git add -A`
> separately. Fix all warnings and errors and re-run until the build is clean.
>
> **NEVER run `dotnet test`, `scripts/validate-tests.sh`, or any other test command.**
> A dedicated Tester agent handles all testing.
>
> **Step 5 — Commit and push**
>
> Commit format: `feat: description [TASK_KEY]`
Expand Down Expand Up @@ -250,8 +262,8 @@ Tester instructions:
>
> Check out the branch and fix each failure. You may re-run individual tests to
> verify a specific fix (e.g. `dotnet test --filter "FullyQualifiedName~TEST_NAME"`),
> but do not run the full suite — that is the Tester agent's job. When done, confirm
> the build is still clean, then commit and push:
> but **NEVER run `scripts/validate-tests.sh` or the full test suite** — that is the
> Tester agent's job. When done, confirm the build is still clean, then commit and push:
>
> ```
> git checkout BRANCH_NAME
Expand Down Expand Up @@ -338,12 +350,32 @@ Reviewer instructions:
> headless E2E coverage
> - `.editorconfig` compliance
>
> For each issue found, post a comment **directly to the PR**:
> ```
> gh pr review PR_URL --comment -b "path/to/File.cs:LINE — your comment"
> Collect all issues into a JSON array (schema below). If the array is non-empty,
> post them as a **single formal PR review** with line-anchored comments so each
> becomes a resolvable discussion thread:
>
> ```bash
> # Extract PR number from PR_URL (e.g. .../pull/171 → 171)
> PR_NUMBER=<number>
> COMMIT_SHA=$(git rev-parse HEAD)
>
> # Write review payload to a temp file
> cat > /tmp/review.json << EOF
> {
> "commit_id": "$COMMIT_SHA",
> "event": "COMMENT",
> "body": "Automated review — see inline comments.",
> "comments": [
> { "path": "relative/path/to/File.cs", "line": 42, "body": "your comment" }
> ]
> }
> EOF
>
> gh api repos/jodavis/adaptiveremote/pulls/$PR_NUMBER/reviews \
> -X POST --input /tmp/review.json
> ```
>
> After posting all comments, return a JSON array. Return only the JSON — no other text.
> Return a JSON array. Return only the JSON — no other text.
> An empty array means no issues.
>
> ```json
Expand Down Expand Up @@ -374,6 +406,11 @@ Developer instructions:
> **Branch:** BRANCH_NAME
> **PR URL:** PR_URL
>
> **Non-negotiable build rule:** After making any changes, you MUST run
> `scripts/validate-build.sh` before committing. Never run `dotnet build` directly.
> Never run `dotnet test`, `scripts/validate-tests.sh`, or any test command — a
> dedicated Tester agent handles all testing.
>
> **Task brief:**
> TASK_BRIEF
>
Expand All @@ -395,13 +432,16 @@ Developer instructions:
> gh pr review PR_URL --comment -b "File.cs:LINE — [your rebuttal]"
> ```
>
> **Step 4** — Build, commit, and push:
> **Step 4** — Build, commit, and push. Run only the build script — no test commands:
> ```
> scripts/validate-build.sh
> git commit -m "review: address feedback [TASK_KEY]"
> git push
> ```
>
> **NEVER run `dotnet test`, `scripts/validate-tests.sh`, or any other test command.**
> A dedicated Tester agent handles all testing.
>
> Return: `{ "status": "done", "branch": "BRANCH_NAME" }`

After Developer finishes, update `REVIEWER_BASELINE` to the current HEAD commit:
Expand Down Expand Up @@ -437,12 +477,27 @@ Then spawn **Tester and scoped Reviewer in parallel** and wait for both.
> accessibility regressions, or clear spec non-compliance. Do not raise style, naming,
> or minor cleanup issues.
>
> For each issue, post a comment directly to the PR:
> ```
> gh pr review PR_URL --comment -b "path/to/File.cs:LINE — your comment"
> Collect all issues into a JSON array (same schema as Phase 5). If non-empty,
> post them as a single formal PR review with line-anchored comments:
>
> ```bash
> PR_NUMBER=<number>
> COMMIT_SHA=$(git rev-parse HEAD)
> cat > /tmp/review.json << EOF
> {
> "commit_id": "$COMMIT_SHA",
> "event": "COMMENT",
> "body": "Follow-up review — see inline comments.",
> "comments": [
> { "path": "relative/path/to/File.cs", "line": 42, "body": "your comment" }
> ]
> }
> EOF
> gh api repos/jodavis/adaptiveremote/pulls/$PR_NUMBER/reviews \
> -X POST --input /tmp/review.json
> ```
>
> Return a JSON array (same schema as before). Return only the JSON — no other text.
> Return a JSON array. Return only the JSON — no other text.
> An empty array means all previous comments are resolved and no new significant issues exist.

**Routing after both complete:**
Expand Down
18 changes: 18 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,12 +75,30 @@ mock verification. Group setup calls into `Expect_*` helper methods.
### E2E tests
Prefer the Headless host for new E2E tests — cross-platform, no display required:

**IMPORTANT:** Before running E2E tests for the first time, you must set up Playwright browsers.

**On developer machines (Windows/Mac/Linux with internet access):**
```bash
# Build the Headless host first (required to generate the Playwright installation script)
dotnet build src/AdaptiveRemote.Headless/AdaptiveRemote.Headless.csproj

# Install Playwright browsers (one-time setup)
pwsh src/AdaptiveRemote.Headless/bin/Debug/net10.0/playwright.ps1 install chromium # if tests crash at startup with a JSON-RPC disconnect

dotnet test --filter "FullyQualifiedName~Host.Headless"
```

**In Claude Code cloud sandbox environments** (where `cdn.playwright.dev` is blocked by network
policy): browsers are pre-installed at `/opt/pw-browsers` and the environment is configured to point
Playwright there automatically. No extra setup is required:
```bash
dotnet test --filter "FullyQualifiedName~Host.Headless"
```

If Headless E2E tests fail with JSON-RPC connection errors in a cloud sandbox environment, this indicates
the environment configuration is broken — stop and report the problem rather than working around it with
the setup script. The goal is to be alerted when the environment stops working, not to silently fall back.

## Documentation

### `_spec_*.md` — pre-implementation design docs
Expand Down
4 changes: 2 additions & 2 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@
<PackageVersion Include="Microsoft.Extensions.Configuration.Ini" Version="10.0.0" />
<PackageVersion Include="Microsoft.Extensions.Hosting" Version="10.0.3" />
<!-- OpenTelemetry -->
<PackageVersion Include="OpenTelemetry.Exporter.Console" Version="1.8.1" />
<PackageVersion Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.8.1" />
<PackageVersion Include="OpenTelemetry.Exporter.Console" Version="1.15.3" />
<PackageVersion Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.15.3" />
<PackageVersion Include="OpenTelemetry.Extensions.Hosting" Version="1.15.3" />
<!-- Third-party Libraries -->
<PackageVersion Include="I8Beef.TiVo" Version="1.0.0.14" />
Expand Down
2 changes: 1 addition & 1 deletion scripts/validate-tests.cmd
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@
pushd %~dp0..
dotnet test --no-build "%~dp0validate-unit-tests.proj"
if %ERRORLEVEL% neq 0 ( popd & exit /b %ERRORLEVEL% )
dotnet test --no-build "%~dp0validate-e2e-tests.proj"
dotnet test --no-build "%~dp0validate-e2e-tests.proj" -m:1
if %ERRORLEVEL% neq 0 ( popd & exit /b %ERRORLEVEL% )
popd
2 changes: 1 addition & 1 deletion scripts/validate-tests.sh
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,4 @@ cd "$SCRIPT_DIR/.."
echo 'Testing unit test projects...'
dotnet test --no-build "$SCRIPT_DIR/validate-unit-tests.proj"
echo 'Testing E2E test projects...'
dotnet test --no-build "$SCRIPT_DIR/validate-e2e-tests.proj"
dotnet test --no-build "$SCRIPT_DIR/validate-e2e-tests.proj" -m:1
11 changes: 6 additions & 5 deletions src/AdaptiveRemote.App/Components/BlazorAppScope.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
using AdaptiveRemote.Services.Lifecycle;
using AdaptiveRemote.Services.Lifecycle;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.JSInterop;

namespace AdaptiveRemote.Components;

Expand Down Expand Up @@ -47,12 +49,11 @@ public Task InvokeInScopeAsync(Func<IServiceProvider, CancellationToken, Task> w
return workItem(_serviceProvider, cancellationToken);
}

public Task RecycleAsync()
public async Task RecycleAsync()
{
_logger.LogInformation("Recycling Blazor application scope.");

// In a real implementation, this would refresh the browser which should result
// in a new scope
throw new NotImplementedException();
IJSRuntime jsRuntime = _serviceProvider.GetRequiredService<IJSRuntime>();
await jsRuntime.InvokeVoidAsync("location.reload");
}
}
5 changes: 5 additions & 0 deletions src/AdaptiveRemote.App/Components/Remote.razor
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
@inject Services.IRemoteDefinitionService RemoteDefinitions
@inject Services.IDynamicStylesheetProvider Stylesheet

@if (Stylesheet.GetCss() is { } css)
{
<style>@((MarkupString)css)</style>
}
<ModalMessageUI />
<RemoteLayout LayoutElement="@RemoteDefinitions.RemoteRoot" ProgramMode="@ProgramMode" />

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
using AdaptiveRemote.Models.CloudAssets;
using AdaptiveRemote.Services;
using AdaptiveRemote.Services.CloudAssets;
using AdaptiveRemote.Services.IdleDetection;
using AdaptiveRemote.Services.Lifecycle;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;

namespace AdaptiveRemote.Configuration;

internal static class CloudAssetServiceExtensions
{
internal static IServiceCollection AddCloudAssetServices(this IServiceCollection services, IConfiguration configuration)
=> services
.AddSingleton<ICloudAssetStore, CloudAssetStore>()
.AddSingleton<IIdleDetector, IdleDetector>()
.AddScopedLifecycleService<IdleDetector.ScopedIdleDetector>()
.AddSingleton<ICloudAssetCache, CloudAssetCache>()
.AddSingleton<ICloudAssetDownloader, FileSystemCloudAssetDownloader>()
.AddSingleton<FileSystemCloudAssetWatchService>()
.AddSingleton<ICloudAssetChangeNotifier>(sp => sp.GetRequiredService<FileSystemCloudAssetWatchService>())
.AddHostedService(sp => sp.GetRequiredService<FileSystemCloudAssetWatchService>())
.AddSingleton<CloudAssetOrchestrator>()
.AddSingleton<IPreScopeInitializer>(sp => sp.GetRequiredService<CloudAssetOrchestrator>())
.AddHostedService(sp => sp.GetRequiredService<CloudAssetOrchestrator>())
.Configure<CloudSettings>(configuration);

internal static IServiceCollection AddScopedCloudAsset<T>(
this IServiceCollection services, ICloudAsset<T> asset)
where T : class
=> services
.AddSingleton<ICloudAsset>(asset)
.AddScoped(sp => sp.GetRequiredService<ICloudAssetStore>().Get<T>(asset.Name));
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ internal static IServiceCollection AddConversationServices(this IServiceCollecti
.AddSingleton<IListeningController, ListeningController>()
.AddScoped(GetConversationViewModel)
.AddSingleton<Models.ModalMessageView>()
.AddSingleton<IModalMessageService, ModalMessageService>();
.AddSingleton<IModalMessageService, ModalMessageService>()
.AddScoped<IUserActivityDetector, ConversationActivityDetector>();

internal static IServiceCollection AddConversationServices(this IServiceCollection services, IConfiguration config)
=> services
Expand Down
34 changes: 28 additions & 6 deletions src/AdaptiveRemote.App/Configuration/HostBuilderExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
using AdaptiveRemote.Services;
using AdaptiveRemote.Contracts;
using AdaptiveRemote.Models.CloudAssets;
using AdaptiveRemote.Services;
using AdaptiveRemote.Services.Commands;
using AdaptiveRemote.Services.Layout;
using AdaptiveRemote.Services.Lifecycle;
using AdaptiveRemote.Services.ProgrammaticSettings;
using Microsoft.Extensions.Configuration;
Expand All @@ -25,10 +28,15 @@ internal static IHostBuilder AddRemoteServices(this IHostBuilder builder)
internal static IServiceCollection AddRemoteServices(this IServiceCollection services, IConfiguration configuration)
=> services
.AddApplicationLifecycleServices()
.AddScopedLifecycleService<LifecycleCommandService>()
.AddScoped<IRemoteDefinitionService, StaticCommandGroupProvider>()
.AddSingleton<IPersistSettings, PersistSettings>()
.Configure<ProgrammaticSettings>(configuration.GetSection(SettingsKeys.ProgrammaticSettings));
.AddCloudAssetServices(configuration.GetSection(SettingsKeys.CloudSettings))
.AddScopedCloudAsset(new JsonCloudAsset<CompiledLayout>(
name: "layout",
streamUrl: "/notifications/layouts/stream",
eventName: "layout-ready",
resourcePath: "/layouts/compiled",
jsonContext: LayoutContractsJsonContext.Default))
.AddCommandSystemServices()
.AddProgrammaticSettingsServices(configuration.GetSection(SettingsKeys.ProgrammaticSettings));

internal static IServiceCollection AddScopedLifecycleService<ServiceType>(this IServiceCollection services)
where ServiceType : class, IScopedLifecycle
Expand All @@ -48,5 +56,19 @@ private static IServiceCollection AddApplicationLifecycleServices(this IServiceC
.AddScoped<ScopedLifecycleContainer>()
.AddScoped<Components.BlazorAppScope>()
.AddSingleton<IApplicationScopeContainer, ApplicationScopeContainer>()
.AddSingleton(sp => (IApplicationScopeProvider)sp.GetRequiredService<IApplicationScopeContainer>());
.AddSingleton(sp => (IApplicationScopeProvider)sp.GetRequiredService<IApplicationScopeContainer>())
.AddSingleton<IApplicationRecycleSignal, ApplicationRecycleSignal>()
.AddScopedLifecycleService<LifecycleCommandService>()
.AddScoped<IUserActivityDetector, ProgrammingModeActivityDetector>();

private static IServiceCollection AddCommandSystemServices(this IServiceCollection services)
=> services
.AddScoped<IUserActivityDetector, CommandsActivityDetector>()
.AddScoped<IRemoteDefinitionService, RemoteLayoutDefinitionService>()
.AddScoped<IDynamicStylesheetProvider, LayoutStylesheetProvider>();

private static IServiceCollection AddProgrammaticSettingsServices(this IServiceCollection services, IConfiguration configuration)
=> services
.AddSingleton<IPersistSettings, PersistSettings>()
.Configure<ProgrammaticSettings>(configuration);
}
Loading
Loading