YOU MUST READ CODING_STYLE.md before writing or
modifying any C# code. It's not optional. This project
deviates from standard .NET conventions on several points (notably:
no Async suffix on async methods; no XML docs on members; mixed brace
style). Default instincts from elsewhere will produce code that gets
rejected. If you haven't opened that file yet in this session, stop and
read it now.
You MUST NOT write a single comment, docstring, or XML doc without
first reading CODING_STYLE.md → "Regular comments, docstrings, XML
documentation comments".
You have a strong tendency to over-comment and to restate what the code
already says; that section explains exactly when a comment is justified
and when it isn't. Re-read it any time you're tempted to add a // or ///.
This codebase is mature. Reusing what already exists is more important than writing something new. A new helper that duplicates an existing one is a defect, not a feature. Always look first.
Use docs/api-index.md to discover existing
abstractions before writing new code. For the complete list, see
docs/api-index-full.md.
Every implementation plan MUST include a "Reuse" section with two parts:
-
Existing abstractions to reuse. Research first. List the concrete types/functions you intend to call from the indexes above. If you cannot find a fit, say so explicitly — silence is not acceptable.
-
Reusability of new components. For every new component the plan introduces, ask: is this likely useful elsewhere? If yes, the plan must list an option to put it in a shared project instead of the feature-specific one:
- C#:
ActualLab.Core - TypeScript:
ts/actuallab-core.
The plan should compare the local-vs-shared placement and recommend one. Default to shared when in doubt — promoting later is harder than placing correctly the first time.
- C#:
If the work is small enough that you skip a written plan, you still owe yourself the "look first" step: search the indexes for keywords related to what you're about to write.
pwsh (cross-platform PowerShell) command is available on any OS you run, so use it.
Before starting any task, read AGENTS.md files in every directory starting from the current one and above, up to the root one (project directory).
Once a plan is approved and the open questions in it have been resolved, push it to completion without stopping for confirmation between steps. Don't ask permission to move from one pre-approved step to the next. Don't pause to summarize "I'm about to do X" between pre-agreed phases. Don't ask the user to choose when the choice has minimal impact.
You stop and ask only when all of these are true:
- You hit a real obstacle you can't resolve from context alone.
- The choice likely obsoletes the plan or forces significant rework — not "minor implementation detail," but "the path branches into two very different futures."
- Your best guess at the right answer has a non-trivial chance of being wrong in a way that's hard to revert.
Concretely, do NOT ask when:
- The next step is a mechanical consequence of an earlier approved step.
- Two options exist and either is reversible in a few minutes.
- One option is clearly best (≥ ~80% probability) on the available evidence.
- You're already mid-plan and the next step is just "keep going."
- The build is broken between phases and the user already said that's fine.
When in doubt, act, then briefly note the choice in the result so the user can correct course if needed. A short "I picked X because Y; flag if you'd prefer Z" beats a question that stalls progress.
If a *.CI.slnf (solution filter) file exists in the project root, use it
instead of the main *.sln file for building. The CI solution filter
excludes projects that require additional workloads (like MAUI) that may
not be installed in your environment.
# Preferred — uses CI solution filter (excludes workload-heavy projects)
dotnet build <Project>.CI.slnf
# Only if you have all workloads installed (including maui-android, etc.)
dotnet build <Project>.slnStart with the simplest test: If tests take too long, hang, or multiple tests fail, find the simplest failing test in the group and debug that one first. Once fixed, move on to larger/more complex tests.
Isolate issues with small tests: If a larger test fails and you have a reasonable guess why, write a small dedicated test that isolates the specific issue. This gives you faster iteration cycles. Keep these isolation tests in the codebase—they have value as regression tests.
xUnit [Theory] tests with [InlineData] don't allow running a single test case in isolation. To debug a specific case:
- Create a temporary
[Fact]helper that calls the theory method with the specific arguments - Debug using this helper fact
- Remove the helper fact after you've finished debugging—these are temporary scaffolding only
// Temporary helper - DELETE after debugging
[Fact]
public void MyTheory_SpecificCase() => MyTheory("specificArg", 42);
[Theory]
[InlineData("case1", 1)]
[InlineData("specificArg", 42)] // The case you're debugging
public void MyTheory(string arg1, int arg2) { /* ... */ }Choose reasonable timeouts based on expected execution time. If a test should complete in seconds, don't set a 5-minute timeout—use 30 seconds or less. This helps you iterate faster.
Rule of thumb: When working on a single test, you shouldn't wait more than 1 minute if you know it should run faster. Pick a timeout that matches your expectations.
If you're missing information in test logs:
- Use
Warninglevel logging—it's more likely to appear in output - Worst case: use
Console.Error.WriteLine()to ensure messages appear in test output
Important: Do not create temporary files in the project root. Use the <projectRoot>/tmp folder instead for any temporary files, test scripts, debug outputs, screenshots, etc. This keeps the project root clean and makes it easier to gitignore temporary artifacts.
If AC_OS environment variable is defined, you're started with the AgentCli launcher (c.ps1), so your actual OS is specified in this environment variable.
You may be started via the c.ps1 launcher script. It can run any of the
following CLIs in a chosen environment:
- Claude Code (default) —
claude - OpenAI Codex —
codex - xAI Grok —
grok
…in any of the following environments:
- Docker (default) — sandboxed Linux container
- WSL — Windows Subsystem for Linux
- OS — directly on the host operating system
When started via the launcher, environment variables are set to help you understand your environment. Check these variables to determine where you're running and how to access projects.
The CLI is the optional first positional arg; if omitted, claude is used.
c → claude, Docker (default)
c claude → claude, Docker (explicit)
c codex → codex, Docker
c grok os → grok, host OS
c claude wsl → claude, WSL
c os → claude, host OS (default CLI)
c codex --dry-run → codex, Docker (dry run)
Inside the sandboxed Docker container, each CLI is invoked with its built-in
"skip-approvals" flag (claude --dangerously-skip-permissions, codex --full-auto, grok as-is). On the host OS no such flag is added — the CLI
runs in its normal interactive/approval mode.
Run once after cloning AgentCli to make the c command available everywhere
and build the Docker image (which contains all three CLIs pre-installed):
./c.ps1 install
What install does, by host OS:
- Windows — adds the AgentCli folder to the user
Pathenvironment variable soc(andc.cmd) resolve in any new shell. - macOS — adds
alias c='<AgentCli>/c.cmd'to~/.zshrcandchmod +x's the polyglotc.cmd. - Linux / WSL — same as macOS, but the alias goes into
~/.bashrc.
After the PATH/alias step, install also links AgentCli's shared
.claude/{commands,skills} into ~/.claude/{commands,skills}/team/ and
triggers a Docker build of the AgentCli image (claude-agentcli). Install is
idempotent — running it again only updates what's stale.
Re-open the shell (or source ~/.zshrc / ~/.bashrc) before using c.
To undo everything install did — unregister c, remove the team links,
stop the AgentCli docker-compose stack, and remove the AgentCli Docker image:
./c.ps1 uninstall
Uninstall leaves per-project Docker containers, generated AGENTS.md /
CLAUDE.md files, and worktrees alone.
AgentCli ships a small docker-compose.yml with side processes every CLI
session can reach — currently the two chrome-devtools-mcp services that
bridge stdio chrome-devtools-mcp to streamable HTTP on host ports 8765
and 8766. To start it explicitly:
c compose-start
You almost never need to run that yourself. Any CLI launch
(c, c claude, c codex os, c grok wsl, …) auto-starts the stack
once per OS boot session. The check is a tiny marker file in the OS temp
dir that stores the boot timestamp; if it matches the current boot,
auto-start is a no-op. Reboot invalidates it, so docker compose up -d
runs again the first time after a reboot. Admin commands (build,
install, compose-start, update-md, chrome, edge, audio,
wt/fwt/bwt/rwt) and dry runs skip the auto-start.
| Variable | Description |
|---|---|
AC_OS |
Operating system/environment description |
AC_ProjectRoot |
Root directory containing all projects (/proj in Docker) |
AC_ProjectPath |
Full path to current project (or worktree) |
AC_Worktree |
Worktree suffix (empty if not in a worktree) |
If AC_OS has no value, you're started directly, so none of this is in effect.
Check AC_OS to determine where you're running:
Linux in Docker- Running in a Docker container (sandboxed)Linux on WSL- Running in Windows Subsystem for LinuxWindows- Running directly on WindowsLinux- Running directly on LinuxmacOS- Running directly on macOS
When running in Docker (AC_OS = Linux in Docker), the following tools are available:
| Category | Tools |
|---|---|
| .NET | .NET 10 SDK, .NET 9 SDK, wasm-tools workload |
| Node.js | Node.js 20, npm |
| Shell | Zsh (default), Bash, PowerShell (pwsh) |
| Search | ripgrep (rg), fd-find (fdfind), fzf |
| Git | git, gh (GitHub CLI), git-delta (nicer diffs) |
| Editors | vim, nano |
| Python | Python 3, matplotlib, seaborn, plotly, pandas, numpy, pillow |
| Cloud | gcloud CLI (Google Cloud), with host's gcloud config mounted read-only |
| Testing | Playwright with Chromium pre-installed |
| Audio | PulseAudio client, ALSA utils, SoX (for voice mode) |
| Other | jq, curl, wget, imagemagick, sudo |
When running in Docker, /proj/<CurrentProject>/artifacts path is mapped to artifacts/claude-docker/ path in the OS's file system to avoid permission conflicts with the host.
Host service connectivity: The Docker container uses --network host mode, so localhost inside the container directly refers to the host. This means you can connect to host services (Redis, PostgreSQL, NATS, etc.) using localhost:port just like on the host. On macOS, --network host requires Docker Desktop 4.34+ (Sept 2024).
macOS / Apple Silicon: The Docker image supports both amd64 and arm64 architectures. c.cmd is a polyglot script that works on both Windows and macOS/Linux.
Propagated environment variables: The following environment variables are automatically propagated from the host to the Docker container:
- Variables containing
__in their names (e.g.,ChatSettings__OpenAIApiKeyfor .NET configuration) AC_GITHUB_TOKEN- GitHub authentication token (AC_ prefix to avoid conflicts with gh CLI)NPM_READ_TOKEN- NPM registry read tokenGOOGLE_CLOUD_PROJECT- Google Cloud project IDActualChat_*- Any variables prefixed withActualChat_ActualLab_*- Any variables prefixed withActualLab_Claude_*- Any variables prefixed withClaude_
Google Cloud credentials: The ~/.gcp folder is mounted read-only to /home/claude/.gcp. If GOOGLE_APPLICATION_CREDENTIALS is set on the host, it's automatically remapped to /home/claude/.gcp/key.json inside the container.
Container reuse: By default, c reuses an existing Docker container for the current worktree (matched by the worktree label). If multiple containers exist, you'll be prompted to select one. Use --new to force creating a fresh container instead.
Isolated mode: Set AC_CLAUDE_ISOLATE=true (or 1) to run with an isolated .claude.json config file. When enabled, the launcher copies .claude.json to artifacts/claude-docker/.claude-{timestamp}.json and mounts that copy instead of the original. Changes made inside the container are not synced back to the host's .claude.json. This is useful for parallel Claude instances or testing without affecting the main config.
The user starts Chrome with remote debugging via c chrome command (port 9222). On Windows, this also creates a firewall rule to allow connections from WSL/Docker.
chrome-devtools MCP (preferred over Playwright): AgentCli's docker-compose.yml ships two chrome-devtools MCP services (chrome-devtools-mcp-1 → host 8765, chrome-devtools-mcp-2 → host 8766), each bridging stdio chrome-devtools-mcp to streamable HTTP via supergateway. They target the host Chrome ports CHROME_DEBUG_PORT_1 (default 9222) and CHROME_DEBUG_PORT_2 (default 9223) respectively, and recycle themselves when host Chrome flaps. When the matching MCP server entries are wired up in .mcp.json (look for mcp__chrome-devtools-{1,2}__* tools), prefer them over Playwright — and pair them with the /debug-ui and /server-loop skills if those are available too.
Playwright: Playwright and Chromium are also pre-installed in the Docker image. Use Playwright when you need to write automated test scripts or when the chrome-devtools MCP is not available. When the user asks you to "use host Chrome", connect Playwright to Chrome on the host:
import { chromium } from 'playwright';
// Connect to host Chrome on standard debug port
const browser = await chromium.connectOverCDP('http://localhost:9222');
const page = await browser.newPage();
await page.goto('https://example.com');
// ... user sees this in their Chrome windowSince Docker uses --network host, localhost:9222 reaches the host's Chrome directly.
Docker host IP resolution: If localhost doesn't work (e.g., it resolves to ::1 IPv6 while Chrome listens on IPv4 only), resolve the host IP explicitly:
getent ahosts host.docker.internal | awk 'NR==1{print $1}'Then use the resulting IP (e.g., http://192.168.65.254:9222) instead of localhost.
AC_ProjectRoot always points to the directory that contains AgentCli — by
default, the folder one level above the AgentCli repo itself (override with
the AC_ProjectRoot env var). In Docker it is mounted as /proj, so sibling
projects sitting next to AgentCli are accessible at /proj/<name>.
| Environment | AC_ProjectRoot | Example sibling project |
|---|---|---|
| Docker | /proj |
/proj/ActualLab.Fusion |
| WSL | /mnt/d/Projects |
/mnt/d/Projects/ActualLab.Fusion |
| Windows | D:\Projects |
D:\Projects\ActualLab.Fusion |
| macOS | ~/Projects |
~/Projects/ActualLab.Fusion |
The launcher does not require the current folder to be a sibling of
AgentCli (or even a git repo). You can c from anywhere — your home dir,
a one-off scratch folder, a project under another drive — and it will work.
The behavior splits along the Docker vs. OS/WSL line:
- OS / WSL —
AC_ProjectPathis just the real, untranslated path to the current folder. Nothing fancy. - Docker — the container's filesystem doesn't have access to arbitrary
host paths, so when the launch folder is not under
AC_ProjectRoot, the launcher:- Sanitizes the full host path into a single segment (path separators and
the drive colon all become single underscores:
C:\Users\Alex\foo→C_Users_Alex_foo). - Adds an extra Docker mount that exposes that folder under
/proj/<sanitized>. - Sets
AC_ProjectPath=/proj/<sanitized>so paths inside the container line up. Sibling projects underAC_ProjectRootare still reachable at/proj/<sibling>as usual.
- Sanitizes the full host path into a single segment (path separators and
the drive colon all become single underscores:
The Docker image (claude-<AgentCli folder>) is shared by every project —
there is no per-project claude.Dockerfile anymore. The image is built by
c install or c build (always against AgentCli's own claude.Dockerfile).
Do not edit AGENTS.md or CLAUDE.md directly — they are auto-generated.
Both files are byte-identical and produced by c update-md, which
concatenates:
AGENTS-Source.mdfrom the project wherec.ps1was launched (the local, project-specific part — edit this for anything project-specific).AGENTS-Suffix.mdfrom the AgentCli repo (the shared boilerplate — edit this only if the change should apply to every project).
To change anything in AGENTS.md / CLAUDE.md, edit AGENTS-Source.md
(or AGENTS-Suffix.md) and then run c update-md to regenerate.
Run after editing either part:
./c.ps1 update-md
The launcher supports git worktrees, detected automatically via git.
Auto-detection: If you're in a folder like ActualLab.Fusion-feature1, the launcher detects it as a worktree of ActualLab.Fusion and sets:
AC_Worktree=feature1AC_ProjectPath= path to the worktree folder
Creating worktrees: Use the wt command to create and switch to a worktree:
c wt feature1 # Creates ActualLab.Fusion-feature1 if it doesn't exist and runs there
The worktree is created using git worktree add from the main project directory.